From a96cdc62b18bca5af682c0d93b480c499ae085d6 Mon Sep 17 00:00:00 2001 From: lucannez64 <58551020+lucannez64@users.noreply.github.com> Date: Mon, 12 Oct 2026 20:15:55 +0200 Subject: [PATCH] Publish portable Darling runtime patch snapshot and testing tools --- .github/workflows/source-check.yml | 13 + .gitignore | 23 + LICENSE | 674 ++ NOTICE.md | 7 + README.md | 80 + docs/SETUP.md | 23 + docs/STATUS.md | 13 + docs/TESTING.md | 35 + patches/IOKitUser.patch | 32 + patches/Libinfo.patch | 67 + patches/OpenDirectory.patch | 24 + patches/cocotron.patch | 7684 ++++++++++++++++ patches/compiler-rt.patch | 57 + patches/configd.patch | 744 ++ patches/corecrypto.patch | 494 + patches/corefoundation.patch | 323 + patches/curl.patch | 49 + patches/darling.patch | 7986 +++++++++++++++++ patches/darlingserver.patch | 2442 +++++ patches/foundation.patch | 69 + patches/iokitd.patch | 594 ++ patches/libcxxabi.patch | 25 + patches/libdispatch.patch | 27 + patches/libkqueue.patch | 617 ++ patches/libmalloc.patch | 67 + patches/libnetwork.patch | 24 + patches/libnotify.patch | 19 + patches/libplatform.patch | 190 + patches/libpthread.patch | 18 + patches/libresolv.patch | 32 + patches/libtrace.patch | 30 + patches/libxpc.patch | 182 + patches/metal--deps--indium.patch | 7280 +++++++++++++++ patches/metal.patch | 3124 +++++++ patches/objc4.patch | 25 + patches/security.patch | 225 + patches/xnu.patch | 4913 ++++++++++ requirements.txt | 4 + research/README.md | 5 + scripts/build.sh | 11 + scripts/common.py | 15 + scripts/configure.py | 23 + scripts/install-runtime.sh | 14 + scripts/install-shim.py | 9 + scripts/installer.sh | 13 + scripts/prepare.py | 34 + scripts/run.sh | 7 + scripts/validate.py | 33 + tools/analyze-macho.sh | 14 + tools/build-m58f327-opengl.py | 33 + tools/build-m58f333-iosurface.py | 25 + tools/build-m58f336-quartzcore.py | 18 + tools/build-m58f341-corevideo.py | 20 + tools/build-riot-air-check.py | 38 + tools/build-riot-appkit-runtime.py | 32 + tools/build-riot-audio-runtime.py | 92 + tools/build-riot-clock-fastpath-check.py | 11 + tools/build-riot-codesign-check.py | 20 + tools/build-riot-corefoundation-runtime.py | 44 + tools/build-riot-coregraphics-runtime.py | 21 + tools/build-riot-cxx-optimized.py | 38 + tools/build-riot-cxxabi-runtime.py | 31 + tools/build-riot-dns-config-check.py | 19 + tools/build-riot-fast-clock-check.py | 13 + tools/build-riot-fault-recovery-check.py | 18 + tools/build-riot-fork-integrity-check.py | 16 + tools/build-riot-indium-runtime.py | 52 + tools/build-riot-kqueue-runtime.py | 101 + tools/build-riot-local-mutex-check.py | 13 + tools/build-riot-local-region-check.py | 13 + tools/build-riot-local-semaphore-check.py | 13 + .../build-riot-mach-exception-state-check.py | 23 + tools/build-riot-malloc-runtime.py | 48 + tools/build-riot-metal-concurrency-check.py | 15 + tools/build-riot-metal-presentation.py | 33 + .../build-riot-metal-resource-getter-check.py | 15 + tools/build-riot-metal-runtime.py | 44 + tools/build-riot-metal-vertex-check.py | 15 + tools/build-riot-mvg-dependency-trace.py | 20 + tools/build-riot-objc-runtime.py | 41 + .../build-riot-opendirectory-session-check.py | 23 + tools/build-riot-platform-strings.py | 22 + tools/build-riot-pthread-runtime.py | 49 + tools/build-riot-region-count19-check.py | 13 + tools/build-riot-running-application-check.py | 26 + tools/build-riot-security-runtime.py | 30 + tools/build-riot-server-runtime.py | 20 + tools/build-riot-trust-db-check.py | 22 + tools/build-riot-ulock-cond-check.py | 13 + tools/build-riot-x11-runtime.py | 30 + tools/build-riot-xpc-pipe-ownership-check.py | 23 + tools/darling-system_profiler.sh | 129 + tools/ensure-darling-runtime.py | 24 + tools/ensure-darling-service-accounts.py | 43 + tools/ensure-league-runtime-layout.py | 54 + tools/env-check.sh | 23 + tools/fetch-installer.sh | 13 + tools/install-darling-system-profiler.py | 15 + tools/install-riot-gpu-runtime.sh | 141 + tools/install-riot-presentation-runtime.sh | 63 + tools/m58f299-native-wrapper.c | 23 + tools/m58f299-xshm.c | 355 + tools/m58f327-cgl-context-check.c | 63 + tools/m58f327-cgl-render-check.c | 56 + tools/m58f333-iosurface-lifetime-check.c | 28 + tools/m58f340-cgl-planes-check.c | 68 + tools/m58f341-display-clock-check.c | 26 + tools/m58f352-cgl-direct-check.c | 42 + tools/m58f352-direct-present-window.c | 27 + tools/riot-air-corpus-check.mm | 54 + tools/riot-air-translate-check.cpp | 42 + tools/riot-appkit-pump-check.m | 32 + tools/riot-audio-device-check.c | 104 + tools/riot-audio-graph-check.cpp | 82 + tools/riot-audio-unit-check.cpp | 75 + tools/riot-cancel-rpc-check.c | 28 + tools/riot-clock-fastpath-check.c | 64 + tools/riot-codesign-check.c | 98 + tools/riot-commpage-caps-check.c | 24 + tools/riot-corecrypto-ecdsa-check.c | 52 + tools/riot-corecrypto-modular-check.c | 57 + tools/riot-corecrypto-primitives-check.c | 36 + tools/riot-cxx-exception-check.cpp | 15 + tools/riot-cxx-exception-rtti.s | 14 + tools/riot-dispatch-fd-check.c | 31 + tools/riot-dns-config-check.c | 40 + tools/riot-event-coalescing-check.m | 32 + tools/riot-fast-clock-check.c | 84 + tools/riot-fault-recovery-check.c | 39 + tools/riot-fileport-lifetime-check.c | 37 + tools/riot-fork-integrity-check.c | 26 + tools/riot-getaddrinfo-check.c | 28 + tools/riot-host-fork-stress.c | 36 + tools/riot-host-message-check.cpp | 68 + tools/riot-iosurface-import-cache-check.c | 62 + tools/riot-kqueue-dispatch2-check.c | 27 + tools/riot-kqueue-lifetime-check.c | 42 + tools/riot-kqueue-stack-check.c | 31 + tools/riot-kqueue-timer-check.c | 40 + tools/riot-local-mutex-check.c | 113 + tools/riot-local-region-check.c | 105 + tools/riot-local-semaphore-check.c | 90 + tools/riot-mach-exception-state-check.c | 97 + tools/riot-malloc-integrity-check.c | 96 + tools/riot-metal-batch-check.mm | 327 + tools/riot-metal-command-buffer-bench.mm | 74 + tools/riot-metal-concurrency-check.mm | 108 + tools/riot-metal-presentation-check.mm | 101 + tools/riot-metal-resource-getter-check.mm | 41 + tools/riot-metal-sampler-check.mm | 141 + tools/riot-metal-vertex-check.mm | 50 + tools/riot-modifier-check.m | 56 + tools/riot-mvg-dependency-trace.c | 62 + tools/riot-objc-allocator-check.cpp | 22 + tools/riot-opendirectory-session-check.m | 54 + tools/riot-platform-string-check.c | 50 + tools/riot-pthread-integrity-check.c | 95 + tools/riot-region-count19-check.c | 22 + tools/riot-renderdoc-bridge.c | 54 + tools/riot-rpc-signal-mask-check.c | 29 + tools/riot-running-application-check.m | 39 + tools/riot-scroll-event-check.m | 41 + tools/riot-sectrust-chain-check.c | 43 + tools/riot-sectrust-selfsigned-check.c | 167 + tools/riot-select-backoff-check.c | 42 + tools/riot-syscall-fastpath-check.c | 19 + tools/riot-sysctl-cache-check.c | 25 + tools/riot-sysctl-string-check.c | 37 + tools/riot-task-info-check.c | 18 + tools/riot-trust-db-check.c | 97 + tools/riot-tsc-offset-check.c | 35 + tools/riot-ulock-cond-check.c | 144 + tools/riot-urlsession-class-check.m | 24 + tools/riot-vnode-close-check.c | 26 + tools/riot-window-layout-check.m | 29 + tools/riot-workqueue-reuse-check.c | 40 + tools/riot-xpc-pipe-ownership-check.c | 111 + tools/run-riot-native-check.py | 95 + tools/run-riot-ux-with-logdir-shim.sh | 129 + tools/stop-darling-runtime.py | 124 + tools/test-commpage-cpu-caps.c | 22 + tools/test-p99-runtime.cpp | 70 + tools/test-render-pass-key.cpp | 52 + tools/test-resource-owner-cache.cpp | 52 + tools/test-submission-batch.cpp | 54 + upstream/manifest.json | 288 + 186 files changed, 45822 insertions(+) create mode 100644 .github/workflows/source-check.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 NOTICE.md create mode 100644 README.md create mode 100644 docs/SETUP.md create mode 100644 docs/STATUS.md create mode 100644 docs/TESTING.md create mode 100644 patches/IOKitUser.patch create mode 100644 patches/Libinfo.patch create mode 100644 patches/OpenDirectory.patch create mode 100644 patches/cocotron.patch create mode 100644 patches/compiler-rt.patch create mode 100644 patches/configd.patch create mode 100644 patches/corecrypto.patch create mode 100644 patches/corefoundation.patch create mode 100644 patches/curl.patch create mode 100644 patches/darling.patch create mode 100644 patches/darlingserver.patch create mode 100644 patches/foundation.patch create mode 100644 patches/iokitd.patch create mode 100644 patches/libcxxabi.patch create mode 100644 patches/libdispatch.patch create mode 100644 patches/libkqueue.patch create mode 100644 patches/libmalloc.patch create mode 100644 patches/libnetwork.patch create mode 100644 patches/libnotify.patch create mode 100644 patches/libplatform.patch create mode 100644 patches/libpthread.patch create mode 100644 patches/libresolv.patch create mode 100644 patches/libtrace.patch create mode 100644 patches/libxpc.patch create mode 100644 patches/metal--deps--indium.patch create mode 100644 patches/metal.patch create mode 100644 patches/objc4.patch create mode 100644 patches/security.patch create mode 100644 patches/xnu.patch create mode 100644 requirements.txt create mode 100644 research/README.md create mode 100755 scripts/build.sh create mode 100755 scripts/common.py create mode 100755 scripts/configure.py create mode 100755 scripts/install-runtime.sh create mode 100755 scripts/install-shim.py create mode 100755 scripts/installer.sh create mode 100755 scripts/prepare.py create mode 100755 scripts/run.sh create mode 100644 scripts/validate.py create mode 100755 tools/analyze-macho.sh create mode 100644 tools/build-m58f327-opengl.py create mode 100644 tools/build-m58f333-iosurface.py create mode 100644 tools/build-m58f336-quartzcore.py create mode 100644 tools/build-m58f341-corevideo.py create mode 100644 tools/build-riot-air-check.py create mode 100644 tools/build-riot-appkit-runtime.py create mode 100644 tools/build-riot-audio-runtime.py create mode 100644 tools/build-riot-clock-fastpath-check.py create mode 100644 tools/build-riot-codesign-check.py create mode 100644 tools/build-riot-corefoundation-runtime.py create mode 100644 tools/build-riot-coregraphics-runtime.py create mode 100644 tools/build-riot-cxx-optimized.py create mode 100644 tools/build-riot-cxxabi-runtime.py create mode 100644 tools/build-riot-dns-config-check.py create mode 100644 tools/build-riot-fast-clock-check.py create mode 100644 tools/build-riot-fault-recovery-check.py create mode 100644 tools/build-riot-fork-integrity-check.py create mode 100644 tools/build-riot-indium-runtime.py create mode 100644 tools/build-riot-kqueue-runtime.py create mode 100644 tools/build-riot-local-mutex-check.py create mode 100644 tools/build-riot-local-region-check.py create mode 100644 tools/build-riot-local-semaphore-check.py create mode 100644 tools/build-riot-mach-exception-state-check.py create mode 100644 tools/build-riot-malloc-runtime.py create mode 100644 tools/build-riot-metal-concurrency-check.py create mode 100644 tools/build-riot-metal-presentation.py create mode 100644 tools/build-riot-metal-resource-getter-check.py create mode 100644 tools/build-riot-metal-runtime.py create mode 100644 tools/build-riot-metal-vertex-check.py create mode 100644 tools/build-riot-mvg-dependency-trace.py create mode 100644 tools/build-riot-objc-runtime.py create mode 100644 tools/build-riot-opendirectory-session-check.py create mode 100644 tools/build-riot-platform-strings.py create mode 100644 tools/build-riot-pthread-runtime.py create mode 100644 tools/build-riot-region-count19-check.py create mode 100644 tools/build-riot-running-application-check.py create mode 100644 tools/build-riot-security-runtime.py create mode 100644 tools/build-riot-server-runtime.py create mode 100644 tools/build-riot-trust-db-check.py create mode 100644 tools/build-riot-ulock-cond-check.py create mode 100644 tools/build-riot-x11-runtime.py create mode 100644 tools/build-riot-xpc-pipe-ownership-check.py create mode 100644 tools/darling-system_profiler.sh create mode 100644 tools/ensure-darling-runtime.py create mode 100644 tools/ensure-darling-service-accounts.py create mode 100644 tools/ensure-league-runtime-layout.py create mode 100755 tools/env-check.sh create mode 100755 tools/fetch-installer.sh create mode 100644 tools/install-darling-system-profiler.py create mode 100755 tools/install-riot-gpu-runtime.sh create mode 100644 tools/install-riot-presentation-runtime.sh create mode 100644 tools/m58f299-native-wrapper.c create mode 100644 tools/m58f299-xshm.c create mode 100644 tools/m58f327-cgl-context-check.c create mode 100644 tools/m58f327-cgl-render-check.c create mode 100644 tools/m58f333-iosurface-lifetime-check.c create mode 100644 tools/m58f340-cgl-planes-check.c create mode 100644 tools/m58f341-display-clock-check.c create mode 100644 tools/m58f352-cgl-direct-check.c create mode 100644 tools/m58f352-direct-present-window.c create mode 100644 tools/riot-air-corpus-check.mm create mode 100644 tools/riot-air-translate-check.cpp create mode 100644 tools/riot-appkit-pump-check.m create mode 100644 tools/riot-audio-device-check.c create mode 100644 tools/riot-audio-graph-check.cpp create mode 100644 tools/riot-audio-unit-check.cpp create mode 100644 tools/riot-cancel-rpc-check.c create mode 100644 tools/riot-clock-fastpath-check.c create mode 100644 tools/riot-codesign-check.c create mode 100644 tools/riot-commpage-caps-check.c create mode 100644 tools/riot-corecrypto-ecdsa-check.c create mode 100644 tools/riot-corecrypto-modular-check.c create mode 100644 tools/riot-corecrypto-primitives-check.c create mode 100644 tools/riot-cxx-exception-check.cpp create mode 100644 tools/riot-cxx-exception-rtti.s create mode 100644 tools/riot-dispatch-fd-check.c create mode 100644 tools/riot-dns-config-check.c create mode 100644 tools/riot-event-coalescing-check.m create mode 100644 tools/riot-fast-clock-check.c create mode 100644 tools/riot-fault-recovery-check.c create mode 100644 tools/riot-fileport-lifetime-check.c create mode 100644 tools/riot-fork-integrity-check.c create mode 100644 tools/riot-getaddrinfo-check.c create mode 100644 tools/riot-host-fork-stress.c create mode 100644 tools/riot-host-message-check.cpp create mode 100644 tools/riot-iosurface-import-cache-check.c create mode 100644 tools/riot-kqueue-dispatch2-check.c create mode 100644 tools/riot-kqueue-lifetime-check.c create mode 100644 tools/riot-kqueue-stack-check.c create mode 100644 tools/riot-kqueue-timer-check.c create mode 100644 tools/riot-local-mutex-check.c create mode 100644 tools/riot-local-region-check.c create mode 100644 tools/riot-local-semaphore-check.c create mode 100644 tools/riot-mach-exception-state-check.c create mode 100644 tools/riot-malloc-integrity-check.c create mode 100644 tools/riot-metal-batch-check.mm create mode 100644 tools/riot-metal-command-buffer-bench.mm create mode 100644 tools/riot-metal-concurrency-check.mm create mode 100644 tools/riot-metal-presentation-check.mm create mode 100644 tools/riot-metal-resource-getter-check.mm create mode 100644 tools/riot-metal-sampler-check.mm create mode 100644 tools/riot-metal-vertex-check.mm create mode 100644 tools/riot-modifier-check.m create mode 100644 tools/riot-mvg-dependency-trace.c create mode 100644 tools/riot-objc-allocator-check.cpp create mode 100644 tools/riot-opendirectory-session-check.m create mode 100644 tools/riot-platform-string-check.c create mode 100644 tools/riot-pthread-integrity-check.c create mode 100644 tools/riot-region-count19-check.c create mode 100644 tools/riot-renderdoc-bridge.c create mode 100644 tools/riot-rpc-signal-mask-check.c create mode 100644 tools/riot-running-application-check.m create mode 100644 tools/riot-scroll-event-check.m create mode 100644 tools/riot-sectrust-chain-check.c create mode 100644 tools/riot-sectrust-selfsigned-check.c create mode 100644 tools/riot-select-backoff-check.c create mode 100644 tools/riot-syscall-fastpath-check.c create mode 100644 tools/riot-sysctl-cache-check.c create mode 100644 tools/riot-sysctl-string-check.c create mode 100644 tools/riot-task-info-check.c create mode 100644 tools/riot-trust-db-check.c create mode 100644 tools/riot-tsc-offset-check.c create mode 100644 tools/riot-ulock-cond-check.c create mode 100644 tools/riot-urlsession-class-check.m create mode 100644 tools/riot-vnode-close-check.c create mode 100644 tools/riot-window-layout-check.m create mode 100644 tools/riot-workqueue-reuse-check.c create mode 100644 tools/riot-xpc-pipe-ownership-check.c create mode 100644 tools/run-riot-native-check.py create mode 100755 tools/run-riot-ux-with-logdir-shim.sh create mode 100755 tools/stop-darling-runtime.py create mode 100644 tools/test-commpage-cpu-caps.c create mode 100644 tools/test-p99-runtime.cpp create mode 100644 tools/test-render-pass-key.cpp create mode 100644 tools/test-resource-owner-cache.cpp create mode 100644 tools/test-submission-batch.cpp create mode 100644 upstream/manifest.json diff --git a/.github/workflows/source-check.yml b/.github/workflows/source-check.yml new file mode 100644 index 0000000..ab134ec --- /dev/null +++ b/.github/workflows/source-check.yml @@ -0,0 +1,13 @@ +name: Source package checks +on: [push, pull_request] +permissions: + contents: read +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - run: python scripts/validate.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2aca752 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +/build/ +/logs/ +/Config/ +/Library/ +/backups*/ +.env +.env.* +!/.env.example +__pycache__/ +*.py[cod] +*.log +*.perf +*.data +*.dylib +*.so +*.o +*.a +*.zip +*.app/ +*.asar +lockfile +core +core.* diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/NOTICE.md b/NOTICE.md new file mode 100644 index 0000000..0332431 --- /dev/null +++ b/NOTICE.md @@ -0,0 +1,7 @@ +# Licensing and third-party material + +`LICENSE` contains GPL-3.0, used for this project's original integration tooling. Patches to upstream code retain the original files' licenses and notices. Darling and its submodules include GPL, LGPL, APSL, BSD, Apache and other upstream licensing; consult each pinned repository before redistribution. + +The manifest points to public upstream source repositories. This repository includes source differences and authored integration/research tools, not a copy of the upstream runtime, Apple's SDK binaries or Riot's client/game assets. Build artifacts and local installs are excluded. + +Some compatibility tools contain short instruction signatures or API names used to recognize the user's installed client. The client itself is not redistributed. Installation downloads from Riot's distribution service directly. diff --git a/README.md b/README.md new file mode 100644 index 0000000..639a00a --- /dev/null +++ b/README.md @@ -0,0 +1,80 @@ +# darling-lol + +Experimental source patches and tools for running the **macOS League of Legends client on x86-64 Linux through Darling**. + +This is a cleaned snapshot of a working development setup, not a finished launcher. Riot and League clients can launch, practice/ARAM games have run, and graphics, audio, input and post-game freezes have received compatibility fixes. Fight FPS still varies: **90 FPS at the 99th percentile has not been achieved**. A clean build on another PC has not yet been validated. + +## Included + +- Pinned upstream revisions and patches for 29 Darling components, including Metal/Indium, AppKit, QuartzCore, CoreAudio, the kernel shim and darlingserver. +- The latest event-poll presentation fix and compositor-thread semaphore import fix. The latter stopped one leaked GPU synchronization handle per frame in native tests. +- Build/install/launch wrappers with configurable machine paths, native regression probes and performance tools. +- Older experiments under `research/tools/`; these are retained for reference and are outside the normal launch path. + +Game files, Riot installers, account databases, tokens, local prefixes, captures and compiled binaries are not included. Install the game through Riot on the second PC. + +## Requirements + +- x86-64 Linux, an X11 session or Xwayland, and a working hardware Vulkan/OpenGL driver. The development PC used AMD RADV; other drivers are not validated. +- Git + Git LFS, CMake, Make, Clang/LLD, LLVM development files, a C/C++ compiler, Python 3.12+ and the Darling build dependencies. LLVM 22 was used for the tested shader translator. +- libvpx development files and X11/XCB/SHM development files for the current runtime helpers. +- Enough free disk for Darling sources/build, Git LFS assets and the game: budget at least 50 GB. Building is substantially larger than this source-only repository. + +Use the dependency list in the [pinned Darling source](https://github.com/darlinghq/darling/tree/60ba801decee7a00782f74f6be4c8ffb013f79ff) for your distribution. `tools/env-check.sh` provides a starting check; its legacy package names may need adapting to your distro. + +## Build on the other PC + +```sh +git clone https://github.com/lucannez64/darling-lol.git +cd darling-lol +python3 -m venv build/venv +build/venv/bin/pip install -r requirements.txt +python3 scripts/configure.py --guest-user "$USER" +./scripts/build.sh +sudo cmake --install build/darling-src/build +./scripts/install-runtime.sh +``` + +The default layout is `/usr/local/libexec/darling`, `~/.darling` and `/Users/$USER` inside the guest. Use the actual guest account name if it differs. Paths are rendered into ignored files under `build/runtime/tools`; do not run the tokenized `tools/` templates directly. See [setup details](docs/SETUP.md). + +The install wrapper compiles the paired runtime, shuts down Darling before replacing per-user overrides, and backs up relevant files. Do this with no Riot/League session running. + +## Install and launch Riot + +After the runtime is installed: + +```sh +./scripts/installer.sh euw +``` + +Sign in through Riot's own UI and install League. Then stop the client, apply the guarded visibility correction for the installed Electron build, and launch normally: + +```sh +python3 build/runtime/tools/stop-darling-runtime.py +python3 build/runtime/tools/patch-riot-electron-visibility.py +./scripts/run.sh +``` + +The visibility correction verifies the exact original instruction bytes and refuses an unknown version. A future Riot update can require adapting this and other compatibility hooks. + +For another region, set `RCLOL_REGION` and `RCLOL_LOCALE` before launch. For example `RCLOL_REGION=NA RCLOL_LOCALE=en_US ./scripts/run.sh`. Linux display selection follows `$DISPLAY`; the legacy launcher fallback is `:1`. + +## Testing + +Start with native regression checks, then a practice game. See [testing](docs/TESTING.md). The practice helpers recognize two verified UI panel sizes, preserve dynamic camera settings and stop on lost focus/unknown geometry. They still drive real input: avoid controlling the game at the same time. + +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). + +## Layout + +``` +scripts/ portable entry points +upstream/manifest.json exact base revisions and patch ownership +patches/ current source changes, one patch per repository +tools/ active source templates and regression probes +research/tools/ historical experiments, not a supported workflow +docs/ setup, testing, status and licensing notes +build/ local sources, outputs and rendered tools (ignored) +``` + +Darling/upstream components retain their existing licenses; see [third-party notes](NOTICE.md). Original integration tooling is distributed under GPL-3.0; patches retain the licensing of their target files. This project is not affiliated with Riot Games. diff --git a/docs/SETUP.md b/docs/SETUP.md new file mode 100644 index 0000000..944d722 --- /dev/null +++ b/docs/SETUP.md @@ -0,0 +1,23 @@ +# Setup and portability + +The patch manifest pins Darling to `60ba801decee7a00782f74f6be4c8ffb013f79ff` and records every changed submodule's revision. `scripts/prepare.py` checks each revision before applying patches and never resets an existing checkout to make a patch apply. Re-running it accepts an already-applied patch. + +Git LFS must be available before fetching the upstream submodules. The local development checkout had removed prebuilt Swift files from its working directory; that deletion is deliberately not exported. A fresh upstream checkout keeps its original Swift assets and licensing. Do not install Git LFS pointer text as a library. + +`configure.py` renders `@PROJECT_ROOT@`, `@HOST_HOME@`, `@GUEST_HOME@`, `@GUEST_USER@`, `@DPREFIX@` and `@DARLING_SDK@` into ignored local copies. A build-directory symlink preserves the original scripts' relative build layout. The public source files remain machine-independent. + +```sh +python3 scripts/configure.py --guest-user my-linux-user +# A custom SDK can be rendered for advanced setups: +python3 scripts/configure.py --sdk /usr/local/libexec/darling +``` + +The normal wrappers currently support the default `~/.darling` prefix and `/usr/local` base installation. The configurator exposes a custom prefix for developers, but several archived tools assume the default layout. Custom prefixes/install locations have not been tested end to end. + +The build uses Unix Makefiles because the incremental runtime helpers read CMake `flags.make` and `link.txt`. Ninja is not a drop-in replacement for those helpers. + +The runtime installer requires native `pkg-config` entries for `vpx`, `x11`, `x11-xcb`, `xcb-shm` and `xext`, plus the base Darling SDK. Some bridge guards intentionally fall back to Chromium's own implementation when the installed client changes. + +The launcher enables loopback-only Electron inspection on `127.0.0.1:9229` for its startup compatibility hooks. It reads local Riot API credentials from the running client when necessary; no credentials are stored in this repository. Keep logs and local captures private. + +Build/install steps need substantial time and storage. The patch set was checked against all pinned base indexes on the development machine. An entirely fresh build and a game launch on the second PC remain to be tested; do not interpret patch applicability as proof that every distro/driver works. diff --git a/docs/STATUS.md b/docs/STATUS.md new file mode 100644 index 0000000..cd6bb32 --- /dev/null +++ b/docs/STATUS.md @@ -0,0 +1,13 @@ +# Snapshot status — 2026-10-12 + +This source snapshot includes current local modifications, including experiments that have not all been compared against a fresh clean build. It preserves a development runtime, not an upstream-ready patch series. + +Confirmed fixes include a lost wake in kernel workqueue semaphore handling, callback-safe CoreAudio stop, AppKit event handling/post-game controls, and delivering ready main-queue callbacks on every event poll while keeping expensive CF run-loop work throttled. + +The presentation callback regression changed from 180 callbacks during 200 polls to 200 callbacks with zero late polls. Native pixel and input checks passed. The user subsequently reported no visible stutter, but still reported sharp fight FPS drops. + +A separate presentation defect exported a GPU synchronization FD on the deferred worker while the GL context belonged to the main thread. The live game accumulated over 45,000 syncobj FDs. The corrected import runs on the compositor thread; native stress leaked zero handles over 120 frames, versus 120 on the old build. The installed game was checked at zero leaked syncobj handles and about 111 total open files. + +A post-fix, limited-ability Baron run measured 99.6 FPS average and 16.11 ms p99 (62.1 reciprocal FPS). Another spell-heavy attempt measured 85.7 FPS average and 23.51 ms p99 (42.5 reciprocal FPS); practice refresh/camera differences prevent treating these as a clean improvement comparison. These are short practice measurements, not an ARAM guarantee or a 90 FPS p99 result. Historical comparisons using a reset helper that alternated refresh toggles are unreliable. + +The development system uses an AMD Barcelo integrated GPU, RADV, Xwayland/Hyprland and two 60 Hz displays without active VRR. Driver capabilities, CPU power sharing and display refresh can differ on another PC. No CPU-affinity or compositor configuration is installed automatically: the legacy `pin-game.sh` assumes this laptop's CPUs and must be adapted before use elsewhere. diff --git a/docs/TESTING.md b/docs/TESTING.md new file mode 100644 index 0000000..df5f010 --- /dev/null +++ b/docs/TESTING.md @@ -0,0 +1,35 @@ +# Testing on another PC + +Build and install the base/runtime first. Native probes are built by the incremental runtime scripts. With the default guest username, run a probe like this (replace `me` and the host checkout path): + +```sh +darling shell env \ + DYLD_FRAMEWORK_PATH=/Users/me/RuntimeFrameworks \ + DYLD_LIBRARY_PATH=/Users/me/RuntimeLibraries \ + /Volumes/SystemRoot/path/to/darling-lol/build/riot-appkit-pump-check +``` + +Build additional probes when needed, for example: + +```sh +python3 build/runtime/tools/build-riot-metal-concurrency-check.py +``` + +Useful probes: + +- `riot-appkit-pump-check`: every queued callback must arrive in its own event poll; expected `events=200 callbacks=200 late_polls=0`. +- `riot-event-coalescing-check`: preserve key events and latest mouse motion. +- `riot-metal-presentation-check`: Vulkan clear reaches the GL texture, including late window/context attachment. +- `riot-metal-concurrency-check`: submission ordering, resource lifetime and concurrent recording. + +`RIOT_PRESENTATION_STRESS=1` makes the presentation probe run 120 frames. Force async encoding for a non-game probe with the guest flag `/tmp/metal-async-encode`, then remove it after testing. The game enables async encoding by default. The tested old path leaked 120 synchronization FDs over 120 frames; the fixed path leaked zero. Inspect the host process's `/proc/PID/fd` for `anon_inode:syncobj_file` if reproducing that test. + +For gameplay, start a practice game through Riot. Check sound, keyboard/mouse buttons, fullscreen confinement, camera motion and returning to the client. Use the real UI; an unknown version/layout is a reason to stop automation and inspect it. + +The rendered `baron-setup.py` is for a fresh practice game. `baron-reset.py` is for an already-configured scene and deliberately preserves refresh toggles. The helpers expect a focused 1920x1080 game at global 0,0 and identify the two verified panel sizes before clicking. Auto-refresh settings are toggles; visually verify them rather than blindly repeating setup on an existing match. + +Passive captures are opt-in. `CYCLE_FRAME_INTERVALS=1 sh build/runtime/tools/profile-cycle.sh` restarts the runtime and creates/reconnects to practice; do not run it during a real match. It enables bounded timing blocks after a 90-second warmup. Native capture files under the guest `/tmp` contain request/render/flush timestamps, not physical scanout times. Re-arm through the guest `/tmp/metal-frame-interval-repeat` flag. The current bounded recorder writes a block synchronously; its file-write boundary can distort isolated worst frames. Treat it as diagnostic instrumentation. + +Compare the same scene, ability activity and camera behavior with the correct charger connected. Do not report a paused, dead, unfocused or empty scene as a fight benchmark. Report p99 frame time as milliseconds; the reciprocal is a percentile-equivalent FPS, not the mean of the slowest 1% of frames. + +The CPU symbolizer offers `--leaf-only` because the host DWARF unwinder can drop guest Mach-O stacks. Successfully unwound samples alone are not a representative profile. Local API lockfiles, account data and raw logs must stay outside version control. diff --git a/patches/IOKitUser.patch b/patches/IOKitUser.patch new file mode 100644 index 0000000..0210a03 --- /dev/null +++ b/patches/IOKitUser.patch @@ -0,0 +1,32 @@ +diff --git a/IOKitLib.c b/IOKitLib.c +index 81ed8f7..3120abb 100644 +--- a/IOKitLib.c ++++ b/IOKitLib.c +@@ -884,6 +884,15 @@ IONotificationPortGetRunLoopSource( + CFMachPortContext context; + Boolean cfReusedPort = false; + ++#ifdef DARLING ++ /* Fail soft instead of crashing: without iokitd some registry/power ++ * lookups never produce a port, and callers (e.g. Chromium's power ++ * monitor) pass the NULL through unchecked. CFRunLoopAddSource ++ * already ignores NULL sources, so returning NULL here is safe. */ ++ if (!notify) ++ return NULL; ++#endif ++ + if (notify->source) + return (notify->source); + +@@ -934,6 +943,11 @@ mach_port_t + IONotificationPortGetMachPort( + IONotificationPortRef notify ) + { ++#ifdef DARLING ++ /* See IONotificationPortGetRunLoopSource: fail soft on NULL. */ ++ if (!notify) ++ return MACH_PORT_NULL; ++#endif + return( notify->wakePort ); + } + diff --git a/patches/Libinfo.patch b/patches/Libinfo.patch new file mode 100644 index 0000000..71e83dd --- /dev/null +++ b/patches/Libinfo.patch @@ -0,0 +1,67 @@ +diff --git a/darling-resolver/resolver.c b/darling-resolver/resolver.c +index f3ae33d..75944eb 100644 +--- a/darling-resolver/resolver.c ++++ b/darling-resolver/resolver.c +@@ -23,6 +23,21 @@ + #include + #include + ++/* res_query shares the global resolver's sockets. Concurrent lookups must ++ * own their resolver state so they cannot consume each other's replies. */ ++static int ++dr_query(const char *name, int dns_class, int type, unsigned char *answer, int length) ++{ ++ struct __res_state state = {0}; ++ int result; ++ ++ if (res_ninit(&state) < 0) ++ return -1; ++ result = res_nquery(&state, name, dns_class, type, answer, length); ++ res_ndestroy(&state); ++ return result; ++} ++ + static int + dr_is_valid(si_mod_t *si, si_item_t *item) + { +@@ -58,7 +73,7 @@ dr_hostbyname(si_mod_t *si, const char *name, int af, const char *interface, uin + else + return NULL; + +- l = res_query(name, ns_c_in, type, buf, sizeof(buf)); ++ l = dr_query(name, ns_c_in, type, buf, sizeof(buf)); + if (l < 0) + return NULL; + +@@ -144,7 +159,7 @@ dr_hostbyaddr(si_mod_t *si, const void *addr, int af, const char *interface, uin + else + return NULL; + +- l = res_query(ptr, ns_c_in, ns_t_ptr, buf, sizeof(buf)); ++ l = dr_query(ptr, ns_c_in, ns_t_ptr, buf, sizeof(buf)); + if (l < 0) + return NULL; + +@@ -240,7 +255,7 @@ dr_addrinfo(si_mod_t *si, const void *node, const void *serv, uint32_t family, u + + if (resolveV6) + { +- l = res_query((const char*) node, ns_c_in, ns_t_aaaa, buf, sizeof(buf)); ++ l = dr_query((const char*) node, ns_c_in, ns_t_aaaa, buf, sizeof(buf)); + if (l < 0) + goto after_v6; + +@@ -279,7 +294,7 @@ dr_addrinfo(si_mod_t *si, const void *node, const void *serv, uint32_t family, u + after_v6: + if (resolveV4) + { +- l = res_query((const char*) node, ns_c_in, ns_t_a, buf, sizeof(buf)); ++ l = dr_query((const char*) node, ns_c_in, ns_t_a, buf, sizeof(buf)); + if (l < 0) + goto after_v4; + +@@ -375,4 +390,3 @@ si_mod_t *si_module_static_darling(void) + return (si_mod_t*)&si; + + } +- diff --git a/patches/OpenDirectory.patch b/patches/OpenDirectory.patch new file mode 100644 index 0000000..0e3de4c --- /dev/null +++ b/patches/OpenDirectory.patch @@ -0,0 +1,24 @@ +diff --git a/memberd-21.1/main.c b/memberd-21.1/main.c +index 8ce6514..87ac046 100644 +--- a/memberd-21.1/main.c ++++ b/memberd-21.1/main.c +@@ -27,6 +27,7 @@ + #include // for fprintf(), stderr, et al + #include // for getopt(), getpid(), select() et al. + #include ++#include /* darling-lol M58F42: SIGPIPE guard */ + #include + #include + #include +@@ -336,6 +337,11 @@ void ReadConfigFile(int* maxCache, int* defaultExpiration, int* defaultNegExpira + } + + int main (int argc, char * const argv[]) { ++ /* darling-lol M58F42: ignore SIGPIPE (same class of defect as M58F40 for ++ * shellspawn). memberd is a launchd daemon that talks to clients over ++ * sockets; a peer closing mid-write otherwise raises SIGPIPE and kills the ++ * daemon under the default disposition, breaking the container bootstrap. */ ++ signal(SIGPIPE, SIG_IGN); + const char * const argv0 = argv[0]; + static const char * const argString = "dhvxsctru:g:lL"; + char c; diff --git a/patches/cocotron.patch b/patches/cocotron.patch new file mode 100644 index 0000000..5e72882 --- /dev/null +++ b/patches/cocotron.patch @@ -0,0 +1,7684 @@ +diff --git a/AppKit/CMakeLists.txt b/AppKit/CMakeLists.txt +index 393a1eb3..6a47ac89 100644 +--- a/AppKit/CMakeLists.txt ++++ b/AppKit/CMakeLists.txt +@@ -1,5 +1,8 @@ + project(AppKit) + ++# NSSpellChecker's asynchronous request API accepts a native completion block. ++set_source_files_properties(NSSpellChecker.m PROPERTIES COMPILE_FLAGS "-fblocks") ++ + include(darling_framework) + + remove_sdk_framework(AppKit) +@@ -619,6 +622,7 @@ add_backend(X11 + X11 + XRandR + Xcursor ++ Xext + fontconfig + xkbfile + ) +diff --git a/AppKit/MacWorkspace.m b/AppKit/MacWorkspace.m +index ba52cd3b..ad8ba2a2 100644 +--- a/AppKit/MacWorkspace.m ++++ b/AppKit/MacWorkspace.m +@@ -13,6 +13,32 @@ + + @implementation MacWorkspace + ++/* darling-lol M57: Chromium's NativeThemeMac / accessibility display code ++ * calls these NSWorkspace accessibility-display getters. Cocotron did not ++ * implement them, so the first call raised -[MacWorkspace ++ * accessibilityDisplayShouldIncreaseContrast]: unrecognized selector and ++ * aborted the Electron UX right after createWindow(). Report the default ++ * (no accessibility overrides) like macOS does when the features are off. */ ++- (BOOL) accessibilityDisplayShouldIncreaseContrast { ++ return NO; ++} ++ ++- (BOOL) accessibilityDisplayShouldReduceMotion { ++ return NO; ++} ++ ++- (BOOL) accessibilityDisplayShouldReduceTransparency { ++ return NO; ++} ++ ++- (BOOL) accessibilityDisplayShouldDifferentiateWithoutColor { ++ return NO; ++} ++ ++- (BOOL) accessibilityDisplayShouldInvertColors { ++ return NO; ++} ++ + - (NSImage *) iconForFile: (NSString *) path { + // TODO: call GetIconRefFromFileInfo() + NSUnimplementedMethod(); +diff --git a/AppKit/NSAccessibility.m b/AppKit/NSAccessibility.m +index 39d1b104..3662bc4b 100644 +--- a/AppKit/NSAccessibility.m ++++ b/AppKit/NSAccessibility.m +@@ -13,6 +13,8 @@ NSString *const NSAccessibilityEnabledAttribute = + @"NSAccessibilityEnabledAttribute"; + NSString *const NSAccessibilityFocusedAttribute = + @"NSAccessibilityFocusedAttribute"; ++NSString *const NSAccessibilityRequiredAttribute = ++ @"AXRequired"; + NSString *const NSAccessibilityHelpAttribute = @"NSAccessibilityHelpAttribute"; + NSString *const NSAccessibilityMaxValueAttribute = + @"NSAccessibilityMaxValueAttribute"; +diff --git a/AppKit/NSAccessibilityRemoteUIElement.m b/AppKit/NSAccessibilityRemoteUIElement.m +index d543e329..e320a3af 100644 +--- a/AppKit/NSAccessibilityRemoteUIElement.m ++++ b/AppKit/NSAccessibilityRemoteUIElement.m +@@ -23,6 +23,38 @@ + + @implementation NSAccessibilityRemoteUIElement + ++/* darling-lol M57: these must return a real object. The previous ++ * forwardInvocation fallback returned an uninitialized return buffer, so ++ * Chromium's RemoteAccessibility got a garbage pointer from -initWithRemoteToken: ++ * and crashed shortly after. */ ++- (id) init ++{ ++ return [super init]; ++} ++ ++- (id) initWithRemoteToken: (id) token ++{ ++ return [self init]; ++} ++ +++ (id) remoteTokenForLocalUIElement: (id) element ++{ ++ /* Chromium treats the return value as NSData and reads [data bytes] / ++ * [data length]. Returning a non-NSData object made those land in ++ * -forwardInvocation: (uninitialized return) -> a huge length -> ++ * std::vector OOM. An empty NSData cleanly disables remote accessibility. */ ++ return [NSData data]; ++} ++ +++ (void) setRemoteUIApp: (id) app ++{ ++ /* Remote (cross-process) accessibility is not supported; accept and ignore. */ ++} ++ +++ (void) registerRemoteUIProcessIdentifier: (int) pid ++{ ++} ++ + - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector + { + return [NSMethodSignature signatureWithObjCTypes: "v@:"]; +diff --git a/AppKit/NSApplication.m b/AppKit/NSApplication.m +index 8e7508e2..6fc86b32 100644 +--- a/AppKit/NSApplication.m ++++ b/AppKit/NSApplication.m +@@ -17,6 +17,9 @@ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + ++#if defined(DARLING) ++#include ++#endif + #import + #import + #import +@@ -271,6 +274,11 @@ NSApplication *NSApp = nil; + return _isRunning; + } + ++/* darling-lol M57: Chromium/Electron queries NSApp for full keyboard access. */ ++- (BOOL) isFullKeyboardAccessEnabled { ++ return NO; ++} ++ + - (NSWindow *) makeWindowsPerform: (SEL) selector inOrder: (BOOL) inOrder { + NSUnimplementedMethod(); + return nil; +@@ -382,7 +390,18 @@ NSApplication *NSApp = nil; + - (void) setDelegate: delegate { + if (delegate != _delegate) { + [self unregisterDelegate]; +- _delegate = delegate; ++ /* darling-lol: retain the delegate (Cocoa specifies assign, but a ++ * non-retained delegate is a dangling-observer trap here). ++ * RiotClientServices' nib-loaded delegate was freed between ++ * NSApplicationWillFinishLaunching and NSApplicationDidFinishLaunching ++ * (pool drain); the recycled memory (an __NSCFString) then received ++ * -applicationDidFinishLaunching:, whose caught exception stalled the ++ * app before Electron ever started. Retaining turns that crash into ++ * at most a one-object leak, and keeps unregisterDelegate messaging ++ * a live object. NSApplication is a singleton (no -dealloc), so no ++ * matching release is required. */ ++ [_delegate release]; ++ _delegate = [delegate retain]; + [self registerDelegate]; + } + } +@@ -550,6 +569,13 @@ NSApplication *NSApp = nil; + } + + - (void) finishLaunching { ++ // AppKit guarantees that launch notifications are delivered once. Some ++ // clients call finishLaunching from their delegate path and NSApplication ++ // -run calls it as well, so make the operation explicitly idempotent. ++ if (_finishedLaunching) ++ return; ++ _finishedLaunching = YES; ++ + NSAutoreleasePool *pool = [NSAutoreleasePool new]; + BOOL needsUntitled = YES; + +@@ -558,7 +584,6 @@ NSApplication *NSApp = nil; + object: self]; + NS_HANDLER [self reportException: localException]; + NS_ENDHANDLER +- + // Load the application icon if we have one + NSString *iconName = [[[NSBundle mainBundle] infoDictionary] + objectForKey: @"CFBundleIconFile"]; +@@ -614,7 +639,6 @@ NSApplication *NSApp = nil; + object: self]; + NS_HANDLER [self reportException: localException]; + NS_ENDHANDLER +- + [pool release]; + } + +@@ -652,7 +676,15 @@ NSApplication *NSApp = nil; + } + } + +- [self terminate: self]; ++ // darling-lol: match AppKit and only terminate when the delegate opts ++ // in via applicationShouldTerminateAfterLastWindowClosed:. The previous ++ // unconditional terminate killed faceless/splash-screen apps (notably ++ // the Riot launcher, whose splash closes right after install verify) ++ // on the first runloop turn with no visible window. ++ if ([_delegate respondsToSelector: @selector(applicationShouldTerminateAfterLastWindowClosed:)] && ++ [_delegate applicationShouldTerminateAfterLastWindowClosed: self]) { ++ [self terminate: self]; ++ } + } + + - (void) _checkForAppActivation { +@@ -666,17 +698,13 @@ NSApplication *NSApp = nil; + } + + - (void) run { +- static BOOL didlaunch = NO; + NSAutoreleasePool *pool; + + _isRunning = YES; + +- if (!didlaunch) { +- didlaunch = YES; +- pool = [NSAutoreleasePool new]; +- [self finishLaunching]; +- [pool release]; +- } ++ pool = [NSAutoreleasePool new]; ++ [self finishLaunching]; ++ [pool release]; + + do { + pool = [NSAutoreleasePool new]; +@@ -716,6 +744,12 @@ NSApplication *NSApp = nil; + } + + - (void) sendEvent: (NSEvent *) event { ++ static int inputTrace = -1; ++ if (inputTrace < 0) inputTrace = getenv("RCLOL_INPUT_TRACE") != NULL; ++ if (inputTrace && ([event type] == NSKeyDown || [event type] == NSLeftMouseDown)) ++ NSLog(@"LOLINPUT app type=%ld eventWindow=%@ number=%ld", (long)[event type], ++ [[event window] class], (long)[event windowNumber]); ++ + if ([event type] == NSKeyDown) { + unsigned modifierFlags = [event modifierFlags]; + +@@ -742,6 +776,61 @@ NSApplication *NSApp = nil; + do { + NSAutoreleasePool *pool = [NSAutoreleasePool new]; + ++ /* Service the run loop (and with it the main libdispatch queue) ++ * on every poll, matching AppKit: manual event pumping must ++ * still deliver main-queue blocks. Without this, apps that ++ * poll with distantPast while awaiting dispatch_async(main) ++ * completions deadlock. Zero timeout: never block here, the ++ * X11 wait below handles untilDate. ++ * Note: map NSDefaultRunLoopMode to kCFRunLoopDefaultMode; ++ * they are distinct strings and the main-queue source lives ++ * in the latter, so passing NSDefaultRunLoopMode through ++ * verbatim would service nothing. */ ++ { ++#if defined(DARLING) ++ /* darling-lol: the game polls this method ~10x per frame. Each run-loop pass costs several darlingserver ++ * round trips, so service the CF run loop at most once per 4 ms on zero-timeout polls; ++ * X11 input is still read on every call. Blocking waits (future deadline) always service it. */ ++ extern BOOL _NSDisplayRunLoopServicedByApp; ++ static uint64_t lastService; ++ static mach_timebase_info_data_t tb; ++ if (tb.denom == 0) mach_timebase_info(&tb); ++ uint64_t nowTicks = mach_absolute_time(); ++ BOOL poll = [untilDate timeIntervalSinceNow] <= 0.0; ++ BOOL due = !poll || (nowTicks - lastService) * tb.numer / tb.denom >= 4000000ULL; ++ if (due) lastService = nowTicks; ++ _NSDisplayRunLoopServicedByApp = YES; /* NSDisplay skips its own zero-timeout pass after this poll */ ++ if (due) { ++#endif ++ CFStringRef rlMode; ++ if (mode == nil || [mode isEqual:NSDefaultRunLoopMode]) ++ rlMode = kCFRunLoopDefaultMode; ++ else ++ rlMode = (CFStringRef)mode; ++ (void)CFRunLoopRunInMode(rlMode, 0.0, true); ++#if defined(DARLING) ++ } ++ /* Deliver ready main-queue work on every event poll. A render ++ * worker can queue a presentation just after the last CF pass; ++ * gating this drain for 4 ms can defer it to the next game frame ++ * and produce a late frame followed by a burst. The empty-queue ++ * drain returns immediately; keep only CFRunLoop throttled. ++ * ++ * darling-lol M14i: drain the libdispatch main queue explicitly. ++ * Evidence (live counters, 2026-09-16): LOLPUSH>0 with ++ * LOLDRAIN==0 and CFRunLoopRunInMode(0s)==Finished in the ++ * launcher hot path — Darling's dispatch->CF wakeup never ++ * connects the main-queue handle to this runloop's source ++ * set, so queued main-queue blocks are unreachable while an ++ * app poll-loops instead of -run. Apple's CF services them ++ * during manual pumping via this exact hook; do the same. */ ++ { ++ extern void _dispatch_main_queue_callback_4CF(void *); ++ _dispatch_main_queue_callback_4CF(NULL); ++ } ++#endif ++ } ++ + NS_DURING [NSClassFromString(@"Win32RunningCopyPipe") + performSelector: @selector(createRunningCopyPipe)]; + +diff --git a/AppKit/NSBitmapImageRep.m b/AppKit/NSBitmapImageRep.m +index 9a3f6a5d..dafb0cbe 100644 +--- a/AppKit/NSBitmapImageRep.m ++++ b/AppKit/NSBitmapImageRep.m +@@ -795,6 +795,11 @@ NSBitmapImageRepPropertyKey NSImageCurrentFrame = @"NSImageCurrentFrame"; + + - (CGBitmapInfo) CGBitmapInfo { + CGBitmapInfo result = kCGBitmapByteOrderDefault; ++ // AppKit's byte samples are stored in component order (RGBA), whereas ++ // Quartz's default word order becomes ABGR on a little-endian host. ++ if (_bitsPerSample == 8 && _bitsPerPixel == 32 && ++ !(_bitmapFormat & NSAlphaFirstBitmapFormat)) ++ result = kCGBitmapByteOrder32Big; + + if (![self hasAlpha]) + result |= kCGImageAlphaNone; +diff --git a/AppKit/NSColor.subproj/NSColor.m b/AppKit/NSColor.subproj/NSColor.m +index fb72c982..6a80b13f 100644 +--- a/AppKit/NSColor.subproj/NSColor.m ++++ b/AppKit/NSColor.subproj/NSColor.m +@@ -21,6 +21,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + #import + #import + #import ++#import + #import + #import + +@@ -43,6 +44,12 @@ NSNotificationName const NSSystemColorsDidChangeNotification = @"NSSystemColorsD + + @implementation NSColor + ++/* darling-lol M57: Chromium queries the system control tint to pick the ++ * native theme; Cocotron lacked the class method entirely. */ +++ (NSUInteger) currentControlTint { ++ return NSBlueControlTint; ++} ++ + - (void) encodeWithCoder: (NSCoder *) coder { + + if ([coder allowsKeyedCoding]) { +@@ -916,6 +923,39 @@ static void releasePatternInfo(void *info) { + return nil; + } + ++- (NSColor *) colorUsingColorSpace: (NSColorSpace *) space ++{ ++ /* darling-lol: bridge NSColorSpace objects to the Name-based conversion ++ * subclasses implement. Cocotron's NSColorSpace carries only a ++ * CGColorSpaceRef, so map by color model to the closest space our ++ * conversion matrix handles. Chromium calls this on catalog colors ++ * during UI setup; previously it raised "unrecognized selector" and ++ * killed the app. Fail soft: unknown/nil space returns self. */ ++ if (space == nil) ++ return self; ++ ++ CGColorSpaceRef cgSpace = [space CGColorSpace]; ++ if (cgSpace == NULL) ++ return self; ++ ++ NSColorSpaceName name = nil; ++ switch (CGColorSpaceGetModel(cgSpace)) { ++ case kCGColorSpaceModelRGB: ++ name = NSDeviceRGBColorSpace; ++ break; ++ case kCGColorSpaceModelMonochrome: ++ name = NSDeviceWhiteColorSpace; ++ break; ++ case kCGColorSpaceModelCMYK: ++ name = NSDeviceCMYKColorSpace; ++ break; ++ default: ++ return self; ++ } ++ NSColor *converted = [self colorUsingColorSpaceName: name]; ++ return (converted != nil) ? converted : self; ++} ++ + - (NSColor *) blendedColorWithFraction: (CGFloat) fraction + ofColor: (NSColor *) color + { +diff --git a/AppKit/NSColor.subproj/NSColor_CGColor.m b/AppKit/NSColor.subproj/NSColor_CGColor.m +index 0aac5eec..1aa01fc8 100644 +--- a/AppKit/NSColor.subproj/NSColor_CGColor.m ++++ b/AppKit/NSColor.subproj/NSColor_CGColor.m +@@ -188,6 +188,15 @@ static inline CGFloat calibratedWhiteFromRGB(CGFloat r, CGFloat g, CGFloat b) { + REC601_LUMINANCE_B * b); + } + ++- (NSColor *) colorUsingColorSpaceName: (NSString *) otherSpaceName ++{ ++ /* darling-lol: the single-arg variant was missing, so any caller ++ * (including -[NSColor colorUsingColorSpace:] and NSColor_catalog) ++ * fell through to the abstract base and raised. Forward to the ++ * two-arg implementation. */ ++ return [self colorUsingColorSpaceName: otherSpaceName device: nil]; ++} ++ + - (NSColor *) colorUsingColorSpaceName: (NSString *) otherSpaceName + device: (NSDictionary *) device + { +@@ -224,6 +233,35 @@ static inline CGFloat calibratedWhiteFromRGB(CGFloat r, CGFloat g, CGFloat b) { + yellow: 0 + black: 1 - white + alpha: alpha]; ++ } else if ([_colorSpaceName isEqualToString: NSNamedColorSpace]) { ++ /* darling-lol: +colorWithSRGBRed:... stores a device-RGB CGColor ++ * tagged NSNamedColorSpace. Treat components as RGB so catalog ++ * colors (e.g. controlAccentBlueColor) convert instead of ++ * returning nil. */ ++ CGFloat red = components[0]; ++ CGFloat green = components[1]; ++ CGFloat blue = components[2]; ++ CGFloat alpha = components[3]; ++ ++ if ([otherSpaceName isEqualToString: NSDeviceRGBColorSpace] || ++ [otherSpaceName isEqualToString: NSCalibratedRGBColorSpace]) ++ return [NSColor colorWithCalibratedRed: red ++ green: green ++ blue: blue ++ alpha: alpha]; ++ ++ if ([otherSpaceName isEqualToString: NSDeviceWhiteColorSpace] || ++ [otherSpaceName isEqualToString: NSCalibratedWhiteColorSpace]) ++ return [NSColor colorWithCalibratedWhite: calibratedWhiteFromRGB( ++ red, green, blue) ++ alpha: alpha]; ++ ++ if ([otherSpaceName isEqualToString: NSDeviceCMYKColorSpace]) ++ return [NSColor colorWithDeviceCyan: 1.0 - red ++ magenta: 1.0 - green ++ yellow: 1.0 - blue ++ black: 0.0 ++ alpha: alpha]; + } else if ([_colorSpaceName isEqualToString: NSDeviceRGBColorSpace]) { + CGFloat red = components[0]; + CGFloat green = components[1]; +diff --git a/AppKit/NSColor.subproj/NSColor_catalog.m b/AppKit/NSColor.subproj/NSColor_catalog.m +index 7361d09a..4bdd88cf 100644 +--- a/AppKit/NSColor.subproj/NSColor_catalog.m ++++ b/AppKit/NSColor.subproj/NSColor_catalog.m +@@ -19,6 +19,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + + #import + #import ++#import + #import + #import + +@@ -128,11 +129,49 @@ NSColor *NSColorGetCatalogColor(NSColorListName catalogName, + if ([colorSpace isEqualToString: [self colorSpaceName]]) + return self; + +- _color = [_color colorUsingColorSpaceName: colorSpace device: device]; ++ /* darling-lol: never let a failed conversion nil out _color; keep the ++ * original so later setFill/CGColor still work. */ ++ NSColor *converted = ++ [_color colorUsingColorSpaceName: colorSpace device: device]; ++ if (converted != nil) ++ _color = converted; + + return _color; + } + ++- (NSColor *) colorUsingColorSpace: (NSColorSpace *) space ++{ ++ /* darling-lol: NSColor_catalog never implemented the NSColorSpace-object ++ * variant, so Chromium's accent-color lookup raised "unrecognized ++ * selector" and terminated the app. Convert the wrapped color without ++ * mutating _color (unlike the Name variant above); fall back to self so ++ * callers always get a usable color. */ ++ if (space == nil || _color == nil) ++ return self; ++ ++ CGColorSpaceRef cgSpace = [space CGColorSpace]; ++ NSColor *converted = nil; ++ if (cgSpace != NULL) { ++ NSColorSpaceName name = nil; ++ switch (CGColorSpaceGetModel(cgSpace)) { ++ case kCGColorSpaceModelRGB: ++ name = NSDeviceRGBColorSpace; ++ break; ++ case kCGColorSpaceModelMonochrome: ++ name = NSDeviceWhiteColorSpace; ++ break; ++ case kCGColorSpaceModelCMYK: ++ name = NSDeviceCMYKColorSpace; ++ break; ++ default: ++ break; ++ } ++ if (name != nil) ++ converted = [_color colorUsingColorSpaceName: name]; ++ } ++ return (converted != nil) ? converted : self; ++} ++ + - (CGColorRef) CGColorRef { + return [self CGColor]; + } +diff --git a/AppKit/NSCursor.m b/AppKit/NSCursor.m +index 4cd74633..9be5239a 100644 +--- a/AppKit/NSCursor.m ++++ b/AppKit/NSCursor.m +@@ -174,6 +174,7 @@ void NSPlatformSetCursorImp(id object) { + @implementation NSCursor + + static NSMutableArray *_cursorStack = nil; ++static BOOL hiddenUntilMouseMoves = NO; + + + (void) initialize { + if (self == [NSCursor class]) { +@@ -365,12 +366,22 @@ static NSMutableArray *_cursorStack = nil; + } + + + (void) setHiddenUntilMouseMoves: (BOOL) flag { ++ hiddenUntilMouseMoves = flag; + if (flag) + [self hide]; + else + [self unhide]; + } + ++/* Backend hook: typing may hide the cursor, but the next real motion ++ * restores it without requiring the application to issue another cursor set. */ +++ (void) _unhideOnMouseMove { ++ if (hiddenUntilMouseMoves) { ++ hiddenUntilMouseMoves = NO; ++ [self unhide]; ++ } ++} ++ + - initWithImage: (NSImage *) image + foregroundColorHint: (NSColor *) foregroundHint + backgroundColorHint: (NSColor *) backgroundHint +diff --git a/AppKit/NSDisplay.m b/AppKit/NSDisplay.m +index cacd8999..13faf0f7 100644 +--- a/AppKit/NSDisplay.m ++++ b/AppKit/NSDisplay.m +@@ -43,6 +43,10 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + return NSThreadSharedInstance(@"NSDisplay"); + } + ++- (BOOL) activateProcessIdentifier: (pid_t) processIdentifier { ++ return NO; ++} ++ + - (instancetype) init { + if ([self class] != [NSDisplay class]) { + // Initializing a concrete subclass. +@@ -146,28 +150,46 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + NSInvalidAbstractInvocation(); + } + ++#ifdef DARLING ++BOOL _NSDisplayRunLoopServicedByApp = NO; ++#endif ++ + - (NSEvent *) nextEventMatchingMask: (NSEventMask) mask + untilDate: (NSDate *) untilDate + inMode: (NSString *) mode + dequeue: (BOOL) dequeue + { + NSEvent *result = nil; ++ static int inputTrace = -1; ++ if (inputTrace < 0) inputTrace = getenv("RCLOL_INPUT_TRACE") != NULL; + + if ([_eventQueue count]) + untilDate = [NSDate date]; + ++#ifdef DARLING ++ /* darling-lol: NSApplication already ran the run loop (zero timeout) and drained the main queue for this poll; ++ * a second zero-timeout pass costs two darlingserver round trips (mach_msg poll) per call for nothing. */ ++ BOOL serviced = _NSDisplayRunLoopServicedByApp; ++ _NSDisplayRunLoopServicedByApp = NO; ++ if (!(serviced && [untilDate timeIntervalSinceNow] <= 0.0005)) ++#endif + [[NSRunLoop currentRunLoop] runMode: mode beforeDate: untilDate]; + +- while (result == nil && [_eventQueue count] > 0) { +- NSEvent *event = _eventQueue[0]; ++ // A filtered poll must leave events of other types queued. Game loops ++ // interleave mouse and keyboard masks; discarding unmatched events here ++ // silently loses keys and button transitions. ++ for (NSUInteger index = 0; index < [_eventQueue count]; ++index) { ++ NSEvent *event = _eventQueue[index]; ++ if (inputTrace && ([event type] == NSKeyDown || [event type] == NSLeftMouseDown)) ++ NSLog(@"LOLINPUT queue type=%ld mask=%llu eventWindow=%@ number=%ld", ++ (long)[event type], (unsigned long long)mask, [[event window] class], ++ (long)[event windowNumber]); + +- if (!(NSEventMaskFromType([event type]) & mask)) { +- [_eventQueue removeObjectAtIndex: 0]; +- } else { ++ if (NSEventMaskFromType([event type]) & mask) { + result = [[event retain] autorelease]; +- + if (dequeue) +- [_eventQueue removeObjectAtIndex: 0]; ++ [_eventQueue removeObjectAtIndex: index]; ++ break; + } + } + +@@ -193,7 +215,8 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + } + + while (--count >= 0) { +- if (NSEventMaskFromType([event type]) & mask) { ++ NSEvent *queuedEvent = [_eventQueue objectAtIndex: count]; ++ if (NSEventMaskFromType([queuedEvent type]) & mask) { + [_eventQueue removeObjectAtIndex: count]; + } + } +@@ -310,6 +333,10 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + return 0; + } + ++- (NSUInteger) pressedMouseButtons { ++ return 0; ++} ++ + - (NSArray *) orderedWindowNumbers { + NSInvalidAbstractInvocation(); + return nil; +diff --git a/AppKit/NSEvent.subproj/NSEvent.m b/AppKit/NSEvent.subproj/NSEvent.m +index d0bad986..d0b1b4aa 100644 +--- a/AppKit/NSEvent.subproj/NSEvent.m ++++ b/AppKit/NSEvent.subproj/NSEvent.m +@@ -40,6 +40,10 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + return [[NSDisplay currentDisplay] currentModifierFlags]; + } + +++ (NSUInteger) pressedMouseButtons { ++ return [[NSDisplay currentDisplay] pressedMouseButtons]; ++} ++ + - (instancetype) initWithType: (NSEventType) type + location: (NSPoint) location + modifierFlags: (NSEventModifierFlags) modifierFlags +diff --git a/AppKit/NSEvent.subproj/NSEvent_mouse.m b/AppKit/NSEvent.subproj/NSEvent_mouse.m +index 31c65aac..f7d55ec0 100644 +--- a/AppKit/NSEvent.subproj/NSEvent_mouse.m ++++ b/AppKit/NSEvent.subproj/NSEvent_mouse.m +@@ -18,6 +18,11 @@ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + #import ++#import ++ ++@interface NSObject (MouseCGEventFlags) ++- (void)setFlags:(CGEventFlags)flags; ++@end + + @implementation NSEvent_mouse + +@@ -34,6 +39,8 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + modifierFlags: modifierFlags + window: window]; + ++ _buttonNumber = (type == NSRightMouseDown || type == NSRightMouseUp || type == NSRightMouseDragged) ? 1 : ++ (type == NSOtherMouseDown || type == NSOtherMouseUp || type == NSOtherMouseDragged) ? 2 : 0; + _clickCount = clickCount; + _deltaX = deltaX; + _deltaY = deltaY; +@@ -45,6 +52,13 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + return _clickCount; + } + ++- (short) subtype { ++ // Ordinary mouse events use NSMouseEventSubtype (0). The base method ++ // raises for event classes that do not have a subtype; Chromium queries ++ // this before forwarding every mouse event to its renderer. ++ return 0; ++} ++ + - (instancetype) initWithType: (NSEventType) type + location: (NSPoint) location + modifierFlags: (NSEventModifierFlags) modifierFlags +@@ -96,6 +110,8 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + location: location + modifierFlags: modifierFlags + window: (id) windowNumber]; ++ _buttonNumber = (type == NSRightMouseDown || type == NSRightMouseUp || type == NSRightMouseDragged) ? 1 : ++ (type == NSOtherMouseDown || type == NSOtherMouseUp || type == NSOtherMouseDragged) ? 2 : 0; + _clickCount = clickCount; + return self; + } +@@ -112,6 +128,37 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + return 0.0; + } + ++// X11 wheel buttons are discrete mouse-wheel notches, without trackpad ++// gesture phases or momentum. Chromium queries these before forwarding them. ++- (NSUInteger) phase { return 0; } ++- (NSUInteger) momentumPhase { return 0; } ++- (BOOL) hasPreciseScrollingDeltas { return NO; } ++- (BOOL) isDirectionInvertedFromDevice { return NO; } ++- (CGFloat) scrollingDeltaX { return _deltaX; } ++- (CGFloat) scrollingDeltaY { return _deltaY; } ++ ++- (CGEventRef) CGEvent { ++ if (_type != NSScrollWheel) { ++ switch (_type) { ++ case NSLeftMouseDown: case NSLeftMouseUp: case NSRightMouseDown: case NSRightMouseUp: ++ case NSMouseMoved: case NSLeftMouseDragged: case NSRightMouseDragged: ++ case NSOtherMouseDown: case NSOtherMouseUp: case NSOtherMouseDragged: { ++ CGEventRef event = CGEventCreateMouseEvent(NULL, (CGEventType)_type, ++ NSPointToCGPoint([self locationInWindow]), (CGMouseButton)_buttonNumber); ++ CGEventSetIntegerValueField(event, kCGMouseEventButtonNumber, _buttonNumber); ++ CGEventSetIntegerValueField(event, kCGMouseEventClickState, _clickCount); ++ [(id)event setFlags:(CGEventFlags)[self modifierFlags]]; ++ return (CGEventRef)[(id)event autorelease]; ++ } ++ default: return NULL; ++ } ++ } ++ CGEventRef event = CGEventCreateScrollWheelEvent(NULL, ++ kCGScrollEventUnitLine, 2, (int32_t)_deltaY, (int32_t)_deltaX); ++ CGEventSetIntegerValueField(event, kCGScrollWheelEventIsContinuous, 0); ++ return (CGEventRef)[(id)event autorelease]; ++} ++ + // Apple's documentation is a bit confusing here. NSEvent exposes no API to pass + // a NSTrackingArea object on creation, yet they expose API to get that pointer. + // Additionally, they state -trackingArea would deliver meaningful returns only +diff --git a/AppKit/NSFont.m b/AppKit/NSFont.m +index 2251b642..8b29725d 100644 +--- a/AppKit/NSFont.m ++++ b/AppKit/NSFont.m +@@ -288,6 +288,14 @@ static NSLock *_cacheLock = nil; + return [self messageFontOfSize: size]; + } + +++ (NSFont *) systemFontOfSize: (CGFloat) size weight: (CGFloat) weight { ++ /* The current font backend provides regular and bold system faces. ++ * Match semibold and heavier requests to the available bold face. */ ++ if (weight >= 0.3) ++ return [self boldSystemFontOfSize: size]; ++ return [self systemFontOfSize: size]; ++} ++ + + (NSFont *) titleBarFontOfSize: (CGFloat) size { + return [self boldSystemFontOfSize: size]; + } +@@ -322,6 +330,30 @@ static NSLock *_cacheLock = nil; + return [O2Font preferredFontNames]; + } + ++/* Chromium uses this AppKit fallback entrypoint for text outside a face's ++ * coverage. Fontconfig's charset matcher selects an installed covering face. */ +++ (NSFont *)findFontLike:(id)font forString:(NSString *)string ++ withRange:(NSRange)range inLanguage:(NSString *)language ++{ ++ if (!font || !string || range.location > [string length] || ++ range.length > [string length] - range.location) ++ return nil; ++ NSMutableString *pattern = [NSMutableString stringWithString:@"sans:charset="]; ++ for (NSUInteger i = range.location; i < NSMaxRange(range); ++i) { ++ unsigned code = [string characterAtIndex:i]; ++ if (code >= 0xd800 && code <= 0xdbff && i + 1 < NSMaxRange(range)) { ++ unsigned low = [string characterAtIndex:i + 1]; ++ if (low >= 0xdc00 && low <= 0xdfff) { ++ code = 0x10000 + ((code - 0xd800) << 10) + (low - 0xdc00); ++ ++i; ++ } ++ } ++ [pattern appendFormat:@"%x ", code]; ++ } ++ (void)language; ++ return [NSFont fontWithName:pattern size:[font pointSize]]; ++} ++ + + (void) setPreferredFontNames: (NSArray *) fontNames { + [O2Font setPreferredFontNames: fontNames]; + } +@@ -565,6 +597,10 @@ static NSLock *_cacheLock = nil; + return [self retain]; + } + ++- (CTFontRef) _coreTextFont { ++ return _ctFont; ++} ++ + - (CGFloat) pointSize { + return _pointSize; + } +@@ -681,8 +717,10 @@ static NSLock *_cacheLock = nil; + } + + - (NSFont *) printerFont { +- NSUnimplementedMethod(); +- return nil; ++ /* Quartz-backed fonts do not need the old screen/printer font split. ++ * Chromium asks for printerFont while constructing its Skia typeface and ++ * requires a valid, non-owned NSFont result. */ ++ return self; + } + + - (NSFont *) screenFont { +@@ -694,6 +732,10 @@ static NSLock *_cacheLock = nil; + return [[self copy] autorelease]; + } + ++- (CGFontRef) copyGraphicsFont { ++ return CGFontRetain(_cgFont); ++} ++ + - (NSRect) boundingRectForFont { + return CTFontGetBoundingBox(_ctFont); + } +diff --git a/AppKit/NSImage.m b/AppKit/NSImage.m +index d2ac8e05..a4164f0a 100644 +--- a/AppKit/NSImage.m ++++ b/AppKit/NSImage.m +@@ -1124,6 +1124,22 @@ NSImageName const NSImageNameTouchBarVolumeUpTemplate = + NSUnimplementedMethod(); + } + ++- (void) drawAtPoint: (NSPoint) point ++{ ++ [self drawAtPoint: point ++ fromRect: NSZeroRect ++ operation: NSCompositeSourceOver ++ fraction: 1.0]; ++} ++ ++- (void) drawInRect: (NSRect) rect ++{ ++ [self drawInRect: rect ++ fromRect: NSZeroRect ++ operation: NSCompositeSourceOver ++ fraction: 1.0]; ++} ++ + - (void) drawAtPoint: (NSPoint) point + fromRect: (NSRect) source + operation: (NSCompositingOperation) operation +diff --git a/AppKit/NSMenu.subproj/NSMenu.m b/AppKit/NSMenu.subproj/NSMenu.m +index e80e49d5..b72c4c0e 100644 +--- a/AppKit/NSMenu.subproj/NSMenu.m ++++ b/AppKit/NSMenu.subproj/NSMenu.m +@@ -493,6 +493,11 @@ BOOL itemIsEnabled(NSMenuItem *item) { + _name = copied; + } + ++/* Private AppKit API — no-op: Cocotron does not implement menu-edge ++ * padding. Present so Chromium/Electron's call does not raise. */ ++- (void) _setHasPadding: (BOOL) padding onEdge: (NSInteger) edge { ++} ++ + - (NSMenu *) _menuWithName: (NSString *) name { + if ([_name isEqual: name]) + return self; +diff --git a/AppKit/NSMenu.subproj/NSMenuItem.m b/AppKit/NSMenu.subproj/NSMenuItem.m +index fd488111..cdb66811 100644 +--- a/AppKit/NSMenu.subproj/NSMenuItem.m ++++ b/AppKit/NSMenu.subproj/NSMenuItem.m +@@ -139,10 +139,12 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + + - (void) dealloc { + [_title release]; ++ [_toolTip release]; + [_atitle release]; + [_keyEquivalent release]; + [_submenu release]; + [_image release]; ++ [_view release]; + [_onStateImage release]; + [_mixedStateImage release]; + [_offStateImage release]; +@@ -155,11 +157,13 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + NSMenuItem *copy = NSCopyObject(self, 0, zone); + + copy->_title = [_title copyWithZone: zone]; ++ copy->_toolTip = [_toolTip copyWithZone: zone]; + copy->_atitle = [_atitle copyWithZone: zone]; + copy->_submenu = [_submenu copyWithZone: zone]; + copy->_keyEquivalent = [_keyEquivalent copyWithZone: zone]; + ; + copy->_image = [_image retain]; ++ copy->_view = [_view retain]; + copy->_onStateImage = [_onStateImage retain]; + copy->_mixedStateImage = [_mixedStateImage retain]; + copy->_offStateImage = [_offStateImage retain]; +@@ -224,6 +228,10 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + return _image; + } + ++- (NSView *) view { ++ return _view; ++} ++ + - (NSImage *) onStateImage { + return _onStateImage; + } +@@ -266,6 +274,18 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + _title = title; + } + ++- (NSString *) toolTip { ++ return _toolTip; ++} ++ ++- (void) setToolTip: (NSString *) toolTip { ++ /* darling-lol M42b: Electron sets menu item tooltips; previously ++ * "unrecognized selector" and a terminate. Stored, not displayed. */ ++ toolTip = [toolTip copy]; ++ [_toolTip release]; ++ _toolTip = toolTip; ++} ++ + - (void) setAttributedTitle: (NSAttributedString *) title { + title = [title copy]; + [_atitle release]; +@@ -319,6 +339,12 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + _image = image; + } + ++- (void) setView: (NSView *) view { ++ view = [view retain]; ++ [_view release]; ++ _view = view; ++} ++ + - (void) setOnStateImage: (NSImage *) image { + [_onStateImage release]; + _onStateImage = [image retain]; +diff --git a/AppKit/NSResponder.m b/AppKit/NSResponder.m +index f102391d..8ea0778f 100644 +--- a/AppKit/NSResponder.m ++++ b/AppKit/NSResponder.m +@@ -30,6 +30,24 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + + @implementation NSResponder + ++- (id) touchBar { ++ return _touchBar; ++} ++ ++- (void) setTouchBar: (id) touchBar { ++ if (_touchBar != touchBar) { ++ [touchBar retain]; ++ [_touchBar release]; ++ _touchBar = touchBar; ++ } ++} ++ ++- (void) dealloc { ++ [_touchBar release]; ++ [super dealloc]; ++} ++ ++ + - (void) encodeWithCoder: (NSCoder *) coder { + if (coder.allowsKeyedCoding) { + [coder encodeObject: _nextResponder forKey: @"NSNextResponder"]; +@@ -286,6 +304,16 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + [_nextResponder performSelector: _cmd withObject: event]; + } + ++- (void) otherMouseDown: (NSEvent *) event { ++ [_nextResponder performSelector:_cmd withObject:event]; ++} ++- (void) otherMouseUp: (NSEvent *) event { ++ [_nextResponder performSelector:_cmd withObject:event]; ++} ++- (void) otherMouseDragged: (NSEvent *) event { ++ [_nextResponder performSelector:_cmd withObject:event]; ++} ++ + - (void) rightMouseUp: (NSEvent *) event { + [_nextResponder performSelector: _cmd withObject: event]; + } +diff --git a/AppKit/NSRunningApplication.m b/AppKit/NSRunningApplication.m +index 6c3d2688..2a7d078a 100644 +--- a/AppKit/NSRunningApplication.m ++++ b/AppKit/NSRunningApplication.m +@@ -1,4 +1,10 @@ + #import ++#import ++#import ++#import ++#import ++#include ++#include + + // DUMMY + +@@ -19,6 +25,38 @@ + + @implementation NSRunningApplication + ++- (instancetype) initWithProcessIdentifier: (pid_t) processIdentifier { ++ self = [super init]; ++ if (self) _processIdentifier = processIdentifier; ++ return self; ++} ++ +++ (NSRunningApplication *) runningApplicationWithProcessIdentifier: (pid_t) processIdentifier { ++ if (processIdentifier <= 0 || (kill(processIdentifier, 0) != 0 && errno == ESRCH)) ++ return nil; ++ return [[[self alloc] initWithProcessIdentifier: processIdentifier] autorelease]; ++} ++ ++- (pid_t) processIdentifier { ++ return _processIdentifier; ++} ++ ++- (BOOL) isTerminated { ++ return kill(_processIdentifier, 0) != 0 && errno == ESRCH; ++} ++ ++- (BOOL) activateWithOptions: (NSUInteger) options { ++ (void)options; ++ if ([self isTerminated]) return NO; ++ @try { ++ return [[NSDisplay currentDisplay] activateProcessIdentifier: _processIdentifier]; ++ } @catch (NSException *exception) { ++ if ([[exception name] isEqualToString:NSWindowServerCommunicationException]) ++ return NO; ++ @throw; ++ } ++} ++ + + (NSArray *) runningApplicationsWithBundleIdentifier: (NSString *) bundleIdentifier { + printf("STUB %s\n", __PRETTY_FUNCTION__); + return [NSArray array]; +diff --git a/AppKit/NSScreen.m b/AppKit/NSScreen.m +index 5ca58772..54b60ef5 100644 +--- a/AppKit/NSScreen.m ++++ b/AppKit/NSScreen.m +@@ -22,6 +22,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + #import + #import + #import ++#import + + NSNotificationName const NSScreenColorSpaceDidChangeNotification = @"NSScreenColorSpaceDidChangeNotification"; + +@@ -68,6 +69,19 @@ NSNotificationName const NSScreenColorSpaceDidChangeNotification = @"NSScreenCol + return 1.0; + } + ++- (CGFloat) backingScaleFactor { ++ /* X11 has no per-screen backing scale concept; report 1.0 so ++ * clients (e.g. Chromium display setup) proceed with unscaled ++ * coordinates. HiDPI VNC/Xvfb could upgrade this later. */ ++ return 1.0; ++} ++ ++- (NSColorSpace *) colorSpace { ++ /* No EDID/ICC profile plumbing from X11 yet; report sRGB so ++ * color-management clients (e.g. Chromium) get a valid space. */ ++ return [NSColorSpace sRGBColorSpace]; ++} ++ + - (id) description { + return [NSString stringWithFormat: @"< %@ - frame %@, visible %@ >", + [super description], +@@ -76,7 +90,12 @@ NSNotificationName const NSScreenColorSpaceDidChangeNotification = @"NSScreenCol + } + + - (NSDictionary *) deviceDescription { +- return @{}; ++ /* darling-lol M57: Chromium reads [screen deviceDescription] ++ * [@"NSScreenNumber"] to get the CGDirectDisplayID and rejects a zero id ++ * (DisplayLinkMac::GetForDisplay). Provide the screen number. */ ++ if (_directDisplayID != 0) ++ return @{ @"NSScreenNumber": [NSNumber numberWithUnsignedInt: _directDisplayID] }; ++ return @{ @"NSScreenNumber": [NSNumber numberWithUnsignedInt: 1] }; + } + + @end +diff --git a/AppKit/NSScrubberItemView.m b/AppKit/NSScrubberItemView.m +index dea5e1d3..7b3a877b 100644 +--- a/AppKit/NSScrubberItemView.m ++++ b/AppKit/NSScrubberItemView.m +@@ -60,3 +60,17 @@ + } + + @end ++ ++@implementation NSScrubberImageItemView ++ ++- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector ++{ ++ return [NSMethodSignature signatureWithObjCTypes: "v@:"]; ++} ++ ++- (void)forwardInvocation:(NSInvocation *)anInvocation ++{ ++ NSLog(@"Stub called: %@ in %@", NSStringFromSelector([anInvocation selector]), [self class]); ++} ++ ++@end +diff --git a/AppKit/NSSpeechSynthesizer.m b/AppKit/NSSpeechSynthesizer.m +index 359607fc..2d365b8f 100644 +--- a/AppKit/NSSpeechSynthesizer.m ++++ b/AppKit/NSSpeechSynthesizer.m +@@ -18,6 +18,8 @@ + */ + + #import ++#import ++#import + + NSVoiceGenderName const NSVoiceGenderFemale = @"VoiceGenderFemale"; + NSVoiceGenderName const NSVoiceGenderMale = @"VoiceGenderMale"; +@@ -32,6 +34,23 @@ NSSpeechPropertyKey const NSSpeechVolumeProperty = @"volm"; + + @implementation NSSpeechSynthesizer + +++ (NSArray *)availableVoices ++{ ++ /* Darling has no speech synthesis backend or installed native voices. */ ++ return [NSArray array]; ++} ++ +++ (NSString *)defaultVoice ++{ ++ return nil; ++} ++ +++ (NSDictionary *)attributesForVoice:(NSString *)voice ++{ ++ return nil; ++} ++ ++ + - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector + { + return [NSMethodSignature signatureWithObjCTypes: "v@:"]; +diff --git a/AppKit/NSSpellChecker.m b/AppKit/NSSpellChecker.m +index 8fecc9d6..1b1a5783 100644 +--- a/AppKit/NSSpellChecker.m ++++ b/AppKit/NSSpellChecker.m +@@ -28,6 +28,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + #import + #import + #import ++#import + + NSString *const NSSpellCheckerDidChangeAutomaticTextReplacementNotification = + @"NSSpellCheckerDidChangeAutomaticTextReplacementNotification"; +@@ -345,7 +346,7 @@ static NSSpellChecker *shared = nil; + return results; + } + +-#ifdef NS_BLOCKS ++#if defined(NS_BLOCKS) || defined(__BLOCKS__) + - (NSInteger) requestCheckingOfString: (NSString *) stringToCheck + range: (NSRange) range + types: (NSTextCheckingTypes) checkingTypes +@@ -357,8 +358,25 @@ static NSSpellChecker *shared = nil; + NSOrthography *orthography, + NSInteger wordCount)) completionHandler + { +- NSUnimplementedMethod(); +- return 0; ++ static NSInteger nextSequence = 0; ++ NSInteger sequence = __sync_add_and_fetch(&nextSequence, 1); ++ NSString *text = [stringToCheck copy]; ++ NSDictionary *settings = [options copy]; ++ // Serialize access to the existing spell engine and document state on ++ // the UI queue. Dispatch copies the block and retains its captured values. ++ dispatch_async(dispatch_get_main_queue(), ^{ ++ NSOrthography *orthography = nil; ++ NSInteger wordCount = 0; ++ NSArray *results = [self checkString: text range: range ++ types: checkingTypes options: settings ++ inSpellDocumentWithTag: tag orthography: &orthography ++ wordCount: &wordCount]; ++ if (completionHandler) ++ completionHandler(sequence, results ?: @[], orthography, wordCount); ++ }); ++ [text release]; ++ [settings release]; ++ return sequence; + } + #endif + +diff --git a/AppKit/NSTextInput.m b/AppKit/NSTextInput.m +index 9177a986..9c3c5ca3 100644 +--- a/AppKit/NSTextInput.m ++++ b/AppKit/NSTextInput.m +@@ -1,10 +1,59 @@ + #include + #include ++#import ++ ++@interface NSObject (TextInputClientDelivery) ++- (void)insertText:(id)text replacementRange:(NSRange)range; ++- (void)insertText:(id)text; ++- (void)doCommandBySelector:(SEL)selector; ++- (void)unmarkText; ++@end + + NSString *const NSTextInputContextKeyboardSelectionDidChangeNotification = @"NSTextInputContextKeyboardSelectionDidChangeNotification"; + + NSString *const NSTextInputReplacementRangeAttributeName = @"NSTextInputReplacementRangeAttributeName"; + + @implementation NSTextInputContext ++- (instancetype)initWithClient:(id)client { ++ if ((self = [super init])) _client = client; ++ return self; ++} ++- (id)client { return _client; } ++- (void)activate {} ++- (void)deactivate {} ++- (void)invalidateCharacterCoordinates {} ++- (void)discardMarkedText { ++ if ([_client respondsToSelector:@selector(unmarkText)]) [_client unmarkText]; ++} ++- (BOOL)handleEvent:(NSEvent *)event { ++ if ([event type] != NSKeyDown || ([event modifierFlags] & NSCommandKeyMask)) return NO; ++ NSString *text = [event characters]; ++ if (![text length]) return NO; ++ unichar character = [text characterAtIndex:0]; ++ SEL command = NULL; ++ switch (character) { ++ case 0x7f: case 0x08: command = @selector(deleteBackward:); break; ++ case '\r': case '\n': command = @selector(insertNewline:); break; ++ case '\t': command = @selector(insertTab:); break; ++ case 0x1b: command = @selector(cancelOperation:); break; ++ case NSLeftArrowFunctionKey: command = @selector(moveLeft:); break; ++ case NSRightArrowFunctionKey: command = @selector(moveRight:); break; ++ case NSUpArrowFunctionKey: command = @selector(moveUp:); break; ++ case NSDownArrowFunctionKey: command = @selector(moveDown:); break; ++ case NSDeleteFunctionKey: command = @selector(deleteForward:); break; ++ } ++ if (command) { ++ if (![_client respondsToSelector:@selector(doCommandBySelector:)]) return NO; ++ [_client doCommandBySelector:command]; ++ } else { ++ if (character < 0x20 || (character >= 0xf700 && character <= 0xf8ff) || ++ ([event modifierFlags] & NSControlKeyMask)) return NO; ++ if ([_client respondsToSelector:@selector(insertText:replacementRange:)]) ++ [_client insertText:text replacementRange:NSMakeRange(NSNotFound, 0)]; ++ else if ([_client respondsToSelector:@selector(insertText:)]) [_client insertText:text]; ++ else return NO; ++ } ++ return YES; ++} + + @end +diff --git a/AppKit/NSThemeFrame.m b/AppKit/NSThemeFrame.m +index 0ec827c0..6f780225 100644 +--- a/AppKit/NSThemeFrame.m ++++ b/AppKit/NSThemeFrame.m +@@ -133,8 +133,10 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + } + + - (void) mouseDown: (NSEvent *) event { +- if (![[self window] isMovableByWindowBackground]) ++ if (![[self window] isMovableByWindowBackground]) { ++ [super mouseDown:event]; + return; ++ } + + NSPoint origin = [[self window] frame].origin; + NSPoint firstLocation = +diff --git a/AppKit/NSTouchBarItem.m b/AppKit/NSTouchBarItem.m +index c3f5d056..dbd93174 100644 +--- a/AppKit/NSTouchBarItem.m ++++ b/AppKit/NSTouchBarItem.m +@@ -27,6 +27,8 @@ NSTouchBarItemIdentifier const NSTouchBarItemIdentifierFlexibleSpace = + @"NSTouchBarItemIdentifierFlexibleSpace"; + NSTouchBarItemIdentifier const NSTouchBarItemIdentifierOtherItemsProxy = + @"NSTouchBarItemIdentifierOtherItemsProxy"; ++NSTouchBarItemIdentifier const NSTouchBarItemIdentifierCandidateList = ++ @"NSTouchBarItemIdentifierCandidateList"; + NSTouchBarItemIdentifier const NSTouchBarItemIdentifierCharacterPicker = + @"NSTouchBarItemIdentifierCharacterPicker"; + +diff --git a/AppKit/NSView.m b/AppKit/NSView.m +index 8ac2bbdc..7486ebf5 100644 +--- a/AppKit/NSView.m ++++ b/AppKit/NSView.m +@@ -46,6 +46,8 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + #import + #import + #import ++#import ++#import + + @class IBMetricsTable; + +@@ -955,6 +957,54 @@ static inline void buildTransformsIfNeeded(NSView *self) { + CGPointApplyAffineTransform(point, toWindow), fromWindow); + } + ++- (NSUInteger) allowedTouchTypes { ++ return _allowedTouchTypes; ++} ++ ++- (void) setAllowedTouchTypes: (NSUInteger) value { ++ _allowedTouchTypes = value; ++} ++ ++- (BOOL) wantsUpdateLayer { ++ return _wantsUpdateLayer; ++} ++ ++- (void) setWantsUpdateLayer: (BOOL) value { ++ _wantsUpdateLayer = value; ++} ++ ++- (BOOL) canDrawSubviewsIntoLayer { ++ return _canDrawSubviewsIntoLayer; ++} ++ ++- (void) setCanDrawSubviewsIntoLayer: (BOOL) value { ++ _canDrawSubviewsIntoLayer = value; ++} ++ ++- (BOOL) wantsBestResolutionOpenGLSurface { ++ return _wantsBestResolutionOpenGLSurface; ++} ++ ++- (void) setWantsBestResolutionOpenGLSurface: (BOOL) value { ++ _wantsBestResolutionOpenGLSurface = value; ++} ++ ++- (NSPoint) convertPointFromBacking: (NSPoint) point { ++ return point; ++} ++ ++- (NSPoint) convertPointToBacking: (NSPoint) point { ++ return point; ++} ++ ++- (NSRect) convertRectFromBacking: (NSRect) rect { ++ return rect; ++} ++ ++- (NSRect) convertRectToBacking: (NSRect) rect { ++ return rect; ++} ++ + - (NSSize) convertSize: (NSSize) size fromView: (NSView *) viewOrNil { + NSView *fromView = + (viewOrNil != nil) ? viewOrNil : [[self window] _backgroundView]; +@@ -1254,7 +1304,11 @@ static inline void buildTransformsIfNeeded(NSView *self) { + (NSComparisonResult (*)(id, id, void *)) compareFunction + context: (void *) context + { +- NSUnimplementedMethod(); ++ if (compareFunction == NULL || [_subviews count] < 2) ++ return; ++ ++ [_subviews sortUsingFunction: compareFunction context: context]; ++ [self setNeedsDisplay: YES]; + } + + - (void) didAddSubview: (NSView *) subview { +@@ -1511,8 +1565,16 @@ static inline void buildTransformsIfNeeded(NSView *self) { + } + + - (NSTextInputContext *) inputContext { +- NSUnimplementedMethod(); +- return nil; ++ if (![self respondsToSelector:@selector(insertText:replacementRange:)] && ++ ![self respondsToSelector:@selector(insertText:)]) return nil; ++ static char contextKey; ++ NSTextInputContext *context = objc_getAssociatedObject(self, &contextKey); ++ if (!context) { ++ context = [[NSTextInputContext alloc] initWithClient:self]; ++ objc_setAssociatedObject(self, &contextKey, context, OBJC_ASSOCIATION_RETAIN_NONATOMIC); ++ [context release]; ++ } ++ return context; + } + + - (void) registerForDraggedTypes: (NSArray *) types { +@@ -1819,15 +1881,21 @@ static inline void buildTransformsIfNeeded(NSView *self) { + + - (void) _createLayerContextIfNeeded { + if ([_superview layer] == nil) { +- _layerContext = [[CALayerContext alloc] initWithFrame: [self frame]]; ++ if (_layerContext == nil) ++ _layerContext = [[CALayerContext alloc] initWithFrame: [self frame]]; + [_layerContext setLayer: _layer]; +- if (_window) { ++ if (_window && [_layerContext subwindow] == nil) { + [_layerContext setSubwindow: [_window _createSubWindowWithFrame: [self frame]]]; + } + } + } + + - (void) _addLayerToSuperlayer { ++ if ([_superview layer] != nil && _layerContext != nil) { ++ [_layerContext invalidate]; ++ [_layerContext release]; ++ _layerContext = nil; ++ } + [[_superview layer] addSublayer: _layer]; + [self _createLayerContextIfNeeded]; + } +@@ -1905,6 +1973,7 @@ static inline void buildTransformsIfNeeded(NSView *self) { + [[_superview layer] replaceSublayer: _layer with: value]; + [_layer release]; + _layer = value; ++ [_layerContext setLayer: _layer]; + } + + [_subviews +diff --git a/AppKit/NSWindow.m b/AppKit/NSWindow.m +index d545120a..9b995b5a 100644 +--- a/AppKit/NSWindow.m ++++ b/AppKit/NSWindow.m +@@ -45,6 +45,20 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + #import + #import + #import ++#import ++#include ++#include ++ ++ ++/* darling-lol M57: the initializer must not dispatch -contentRectForFrameRect: ++ * to a subclass override. Electron's ElectronNSWindow overrides it and reads ++ * its shell_ ivar, which is only assigned after [super init...] returns; on ++ * macOS NSWindow's initializer does not call that overridable method, but ++ * Cocotron did, so the UX SIGSEGV'd on nil shell_ (+0x198) while constructing ++ * the BrowserWindow. Route both through a private helper instead. */ ++@interface NSWindow (DarlingM57Private) ++- (NSRect) _darlingContentRectForFrameRect: (NSRect) frameRect; ++@end + + const NSNotificationName NSWindowDidBecomeKeyNotification = + @"NSWindowDidBecomeKeyNotification"; +@@ -218,7 +232,8 @@ static BOOL _allowsAutomaticWindowTabbing; + /* This method is Cococtron specific and can be override by subclasses, do not + * change method name */ + + (BOOL) hasMainMenuForStyleMask: (NSWindowStyleMask) styleMask { +- return styleMask & NSTitledWindowMask; ++ return (styleMask & NSTitledWindowMask) && ++ !(styleMask & NSWindowStyleMaskFullSizeContentView); + } + + /* This method is Cococtron specific and can be override by subclasses, do not +@@ -318,6 +333,7 @@ static BOOL _allowsAutomaticWindowTabbing; + [NSApplication sharedApplication]; + } + _styleMask = styleMask; ++ _isMovable = YES; + _backingType = backing; + _isDeferred = defer; + +@@ -351,7 +367,7 @@ static BOOL _allowsAutomaticWindowTabbing; + [_backgroundView _setWindow: self]; + [_backgroundView setNextResponder: self]; + +- NSRect contentViewFrame = [self contentRectForFrameRect: backgroundFrame]; ++ NSRect contentViewFrame = [self _darlingContentRectForFrameRect: backgroundFrame]; + _contentView = [[NSView alloc] initWithFrame: contentViewFrame]; + [_contentView setAutoresizesSubviews: YES]; + [_contentView +@@ -399,6 +415,8 @@ static BOOL _allowsAutomaticWindowTabbing; + } + + - (void) dealloc { ++ for (NSUInteger i = 0; i < 3; ++i) ++ [_standardWindowButtons[i] release]; + [[NSNotificationCenter defaultCenter] removeObserver: self]; + [_childWindows release]; + [_title release]; +@@ -1025,7 +1043,34 @@ static BOOL _allowsAutomaticWindowTabbing; + } + + - (void) setMovable: (BOOL) movable { +- NSUnimplementedMethod(); ++ _isMovable = movable; ++} ++ ++- (BOOL) isMovable { ++ return _isMovable; ++} ++ ++/* Chromium's frameless titlebar explicitly asks NSWindow to drag itself. */ ++- (void) performWindowDragWithEvent:(NSEvent *)event { ++ if (getenv("RCLOL_INPUT_TRACE")) { ++ FILE *trace=fopen("/tmp/rclol-window-drag.log","a"); ++ if(trace) {fprintf(trace,"DRAG pid=%d movable=%d type=%ld\n",getpid(),_isMovable,(long)[event type]);fclose(trace);} ++ } ++ if (!_isMovable || [event type] != NSLeftMouseDown) return; ++ /* darling-lol: follow the pointer in absolute screen coordinates. The window-relative location of every drag event is ++ * measured against where the window really is, which lags behind setFrameOrigin: (the compositor moves the window ++ * later), so converting it back with the new frame fed the lag into the next position and the window jumped (League's ++ * client window ended 120-190 px away from where it was dragged to). The pointer's screen position does not depend on ++ * the window at all. */ ++ NSPoint origin = [self frame].origin; ++ NSPoint first = [NSEvent mouseLocation]; ++ for (;;) { ++ NSEvent *next = [self nextEventMatchingMask:NSLeftMouseUpMask | NSLeftMouseDraggedMask]; ++ if (!next || [next type] == NSLeftMouseUp) break; ++ NSPoint point = [NSEvent mouseLocation]; ++ [self setFrameOrigin:NSMakePoint(origin.x + point.x - first.x, ++ origin.y + point.y - first.y)]; ++ } + } + + - (void) setBackingType: (NSBackingStoreType) value { +@@ -1234,6 +1279,7 @@ static BOOL _allowsAutomaticWindowTabbing; + + - (void) setIgnoresMouseEvents: (BOOL) value { + _ignoresMouseEvents = value; ++ [_platformWindow syncDelegateProperties]; + } + + - (void) setAspectRatio: (NSSize) value { +@@ -1413,8 +1459,14 @@ static BOOL _allowsAutomaticWindowTabbing; + } + + - (NSButton *) standardWindowButton: (NSWindowButton) value { +- NSUnimplementedMethod(); +- return nil; ++ if (value > NSWindowZoomButton || !(_styleMask & NSTitledWindowMask)) ++ return nil; ++ if (_standardWindowButtons[value] == nil) { ++ _standardWindowButtons[value] = [[NSButton alloc] ++ initWithFrame: NSMakeRect(0, 0, 14, 14)]; ++ [_standardWindowButtons[value] setBordered: NO]; ++ } ++ return _standardWindowButtons[value]; + } + + - (NSButtonCell *) defaultButtonCell { +@@ -1425,6 +1477,17 @@ static BOOL _allowsAutomaticWindowTabbing; + return [_sheetContext sheet]; + } + ++- (NSWindow *) sheetParent { ++ // Sheet ownership is recorded on the presenting window, independently ++ // of ordinary child-window relationships. Query that existing state so ++ // attachment and detachment cannot leave a stale parent pointer behind. ++ for (NSWindow *window in [NSApp windows]) { ++ if (window != self && [window attachedSheet] == self) ++ return window; ++ } ++ return nil; ++} ++ + - (id) windowController { + return _windowController; + } +@@ -1511,10 +1574,7 @@ static BOOL _allowsAutomaticWindowTabbing; + return [_platformWindow isMiniaturized]; + } + +-- (BOOL) isMovable { +- NSUnimplementedMethod(); +- return NO; +-} ++ + + - (BOOL) inLiveResize { + return _isInLiveResize; +@@ -1561,6 +1621,108 @@ static BOOL _allowsAutomaticWindowTabbing; + return point; + } + ++/* darling-lol M57: modern screen-conversion API. Chromium calls these on the ++ * NSWindow; without them the struct-returning messages hit CF's stret ++ * forwarding and crash. */ ++- (NSPoint) convertPointToScreen: (NSPoint) point { ++ return [self convertBaseToScreen: point]; ++} ++ ++- (NSPoint) convertPointFromScreen: (NSPoint) point { ++ return [self convertScreenToBase: point]; ++} ++ ++- (NSRect) convertRectToScreen: (NSRect) rect { ++ NSPoint origin = [self convertBaseToScreen: rect.origin]; ++ return NSMakeRect(origin.x, origin.y, rect.size.width, rect.size.height); ++} ++ ++- (NSRect) convertRectFromScreen: (NSRect) rect { ++ NSPoint origin = [self convertScreenToBase: rect.origin]; ++ return NSMakeRect(origin.x, origin.y, rect.size.width, rect.size.height); ++} ++ ++- (BOOL) titlebarAppearsTransparent { ++ return _titlebarAppearsTransparent; ++} ++ ++- (void) setTitlebarAppearsTransparent: (BOOL) value { ++ _titlebarAppearsTransparent = value; ++} ++ ++- (NSInteger) titleVisibility { ++ return _titleVisibility; ++} ++ ++- (void) setTitleVisibility: (NSInteger) value { ++ _titleVisibility = value; ++} ++ ++- (BOOL) movableByWindowBackground { ++ return _isMovableByWindowBackground; ++} ++ ++/* darling-lol M57: NSWindowOcclusionStateVisible == 1 << 1. */ ++- (NSUInteger) occlusionState { ++ return _isVisible ? (NSUInteger)(1 << 1) : (NSUInteger)0; ++} ++ ++- (BOOL) isRestorable { ++ return _restorable; ++} ++ ++- (BOOL) restorable { ++ return _restorable; ++} ++ ++- (void) setRestorable: (BOOL) value { ++ _restorable = value; ++} ++ ++- (NSString *) subtitle { ++ return _subtitle; ++} ++ ++- (void) setSubtitle: (NSString *) value { ++ value = [value copy]; ++ [_subtitle release]; ++ _subtitle = value; ++} ++ ++- (NSInteger) tabbingMode { ++ return _tabbingMode; ++} ++ ++- (void) setTabbingMode: (NSInteger) value { ++ _tabbingMode = value; ++} ++ ++- (NSString *) tabbingIdentifier { ++ return _tabbingIdentifier; ++} ++ ++- (void) setTabbingIdentifier: (NSString *) value { ++ value = [value copy]; ++ [_tabbingIdentifier release]; ++ _tabbingIdentifier = value; ++} ++ ++- (NSInteger) titlebarSeparatorStyle { ++ return _titlebarSeparatorStyle; ++} ++ ++- (void) setTitlebarSeparatorStyle: (NSInteger) value { ++ _titlebarSeparatorStyle = value; ++} ++ ++- (NSInteger) toolbarStyle { ++ return _toolbarStyle; ++} ++ ++- (void) setToolbarStyle: (NSInteger) value { ++ _toolbarStyle = value; ++} ++ + - (NSRect) frameRectForContentRect: (NSRect) contentRect { + // hasMainMenu is an instance method so we can't just use the class method + // frameRectForContentRect:styleMask: +@@ -1580,6 +1742,10 @@ static BOOL _allowsAutomaticWindowTabbing; + } + + - (NSRect) contentRectForFrameRect: (NSRect) frameRect { ++ return [self _darlingContentRectForFrameRect: frameRect]; ++} ++ ++- (NSRect) _darlingContentRectForFrameRect: (NSRect) frameRect { + NSRect result = [[NSDisplay currentDisplay] insetRect: frameRect + forNativeWindowBorderWithStyle: [self styleMask]]; + +@@ -2155,6 +2321,7 @@ static BOOL _allowsAutomaticWindowTabbing; + - (void) orderWindow: (NSWindowOrderingMode) place + relativeTo: (NSInteger) relativeTo + { ++ BOOL wasVisible = _isVisible; + // The move notifications are sent under unknown conditions around + // orderFront: in the Apple AppKit, we do them all the time here until it's + // figured out. I suspect it is a side effect of off-screen windows being at +@@ -2208,6 +2375,8 @@ static BOOL _allowsAutomaticWindowTabbing; + break; + } + ++ if (wasVisible != _isVisible) ++ [self postNotificationName: NSWindowDidChangeOcclusionStateNotification]; + [self postNotificationName: NSWindowDidMoveNotification]; + } + +@@ -2257,6 +2426,12 @@ static BOOL _allowsAutomaticWindowTabbing; + } + + - (void) sendEvent: (NSEvent *) event { ++ static int inputTrace = -1; ++ if (inputTrace < 0) inputTrace = getenv("RCLOL_INPUT_TRACE") != NULL; ++ if (inputTrace && ([event type] == NSKeyDown || [event type] == NSLeftMouseDown)) ++ NSLog(@"LOLINPUT NS type=%ld window=%ld responder=%@ visible=%d", (long)[event type], ++ (long)[self windowNumber], [_firstResponder class], _isVisible); ++ + // Some events can cause our window to be destroyed + // So make sure self lives at least through this current run loop... + [[self retain] autorelease]; +@@ -2300,6 +2475,10 @@ static BOOL _allowsAutomaticWindowTabbing; + + case NSLeftMouseDown: { + NSView *view = [_backgroundView hitTest: [event locationInWindow]]; ++ if (inputTrace) { ++ FILE *trace=fopen("/tmp/rclol-window-drag.log","a"); ++ if(trace) {fprintf(trace,"DOWN pid=%d view=%s movable=%d at=%.0f,%.0f\n",getpid(),class_getName([view class]),_isMovable,[event locationInWindow].x,[event locationInWindow].y);fclose(trace);} ++ } + + if ([view acceptsFirstResponder]) { + if ([view needsPanelToBecomeKey]) { +@@ -2307,6 +2486,14 @@ static BOOL _allowsAutomaticWindowTabbing; + } + } + ++ // A view's permission to pass a click through is not a request to ++ // drag. Only windows opting into background dragging intercept it; ++ // CEF otherwise handles clicks and invokes performWindowDragWithEvent: ++ // itself for its native titlebar regions. ++ if (_isMovable && _isMovableByWindowBackground && [view mouseDownCanMoveWindow]) { ++ [self performWindowDragWithEvent:event]; ++ break; ++ } + // Event goes to view, not first responder. + [view mouseDown: event]; + _mouseDownLocationInWindow = [event locationInWindow]; +@@ -2330,6 +2517,31 @@ static BOOL _allowsAutomaticWindowTabbing; + _mouseDownLocationInWindow = NSMakePoint(NAN, NAN); + break; + ++ case NSOtherMouseDown: { ++ NSMutableDictionary *locations = objc_getAssociatedObject(self, @selector(otherMouseDown:)); ++ if (!locations) { ++ locations = [NSMutableDictionary dictionary]; ++ objc_setAssociatedObject(self, @selector(otherMouseDown:), locations, OBJC_ASSOCIATION_RETAIN_NONATOMIC); ++ } ++ [locations setObject:[NSValue valueWithPoint:[event locationInWindow]] ++ forKey:[NSNumber numberWithInteger:[event buttonNumber]]]; ++ [[_backgroundView hitTest:[event locationInWindow]] otherMouseDown:event]; ++ break; ++ } ++ case NSOtherMouseUp: ++ case NSOtherMouseDragged: { ++ NSMutableDictionary *locations = objc_getAssociatedObject(self, @selector(otherMouseDown:)); ++ NSNumber *button = [NSNumber numberWithInteger:[event buttonNumber]]; ++ NSValue *location = [locations objectForKey:button]; ++ NSPoint point = location ? [location pointValue] : [event locationInWindow]; ++ NSView *view = [_backgroundView hitTest:point]; ++ if ([event type] == NSOtherMouseUp) { ++ [locations removeObjectForKey:button]; ++ [view otherMouseUp:event]; ++ } else [view otherMouseDragged:event]; ++ break; ++ } ++ + case NSMouseMoved: { + NSView *hit = [_backgroundView hitTest: [event locationInWindow]]; + +@@ -2536,6 +2748,7 @@ static BOOL _allowsAutomaticWindowTabbing; + } else { + [[self platformWindow] hideWindow]; + } ++ [self postNotificationName: NSWindowDidChangeOcclusionStateNotification]; + } + + - (void) orderFront: (id) sender { +@@ -3260,8 +3473,7 @@ static BOOL _allowsAutomaticWindowTabbing; + mouseWasInside && !mouseIsInside) { + [exited addObject: area]; + } +- if (options & NSTrackingMouseMoved && +- [self acceptsMouseMovedEvents]) { ++ if (options & NSTrackingMouseMoved && mouseIsInside) { + [moved addObject: area]; + } + if (options & NSTrackingCursorUpdate && !mouseWasInside && +@@ -3459,7 +3671,7 @@ static BOOL _allowsAutomaticWindowTabbing; + } + + - (CGSubWindow *) _createSubWindowWithFrame: (CGRect) frame { +- return [_platformWindow createSubWindowWithFrame: frame]; ++ return [[self platformWindow] createSubWindowWithFrame: frame]; + } + + + (BOOL) allowsAutomaticWindowTabbing { +diff --git a/AppKit/X11.backend/CarbonKeys.h b/AppKit/X11.backend/CarbonKeys.h +index e999cbf2..14f9cb6a 100644 +--- a/AppKit/X11.backend/CarbonKeys.h ++++ b/AppKit/X11.backend/CarbonKeys.h +@@ -90,13 +90,14 @@ static const uint8_t x11ToCarbon[256] = { + [65] = kVK_Space, + [22] = kVK_Delete, + [9] = kVK_Escape, +- [64] = kVK_Command, ++ [64] = kVK_Option, + [50] = kVK_Shift, + [66] = kVK_CapsLock, +- [133] = kVK_Option, ++ [133] = kVK_Command, + [37] = kVK_Control, + [62] = kVK_RightShift, +- [134] = kVK_RightOption, ++ [134] = 0x36, // right Command ++ [108] = kVK_RightOption, + [105] = kVK_RightControl, + [123] = kVK_VolumeUp, + [122] = kVK_VolumeDown, +@@ -197,13 +198,14 @@ static const uint8_t carbonToX11[256] = { + [kVK_Space] = 65, + [kVK_Delete] = 22, + [kVK_Escape] = 9, +- [kVK_Command] = 64, ++ [kVK_Command] = 133, ++ [0x36] = 134, // right Command + [kVK_Shift] = 50, + [kVK_CapsLock] = 66, +- [kVK_Option] = 133, ++ [kVK_Option] = 64, + [kVK_Control] = 37, + [kVK_RightShift] = 62, +- [kVK_RightOption] = 134, ++ [kVK_RightOption] = 108, + [kVK_RightControl] = 105, + [kVK_VolumeUp] = 123, + [kVK_VolumeDown] = 122, +diff --git a/AppKit/X11.backend/X11Cursor.m b/AppKit/X11.backend/X11Cursor.m +index bdc97bdf..139c91eb 100644 +--- a/AppKit/X11.backend/X11Cursor.m ++++ b/AppKit/X11.backend/X11Cursor.m +@@ -67,16 +67,16 @@ + const uint8_t *rowBytes = CGBitmapContextGetData(context); + const size_t bytesPerRow = CGBitmapContextGetBytesPerRow(context); + +- for (int row = 0; row < height; row++, rowBytes += bytesPerRow) { +- for (int column = 0; column < width; column++) { +- memcpy(ximage->pixels + row * width, &rowBytes[column * 4], +- bytesPerRow); +- } +- } ++ /* Xcursor storage is tightly packed. Copy each row once; the old inner ++ * loop copied an entire row at every pixel and wrote past the allocation. */ ++ for (size_t row = 0; row < height; ++row) ++ memcpy(ximage->pixels + row * width, rowBytes + row * bytesPerRow, ++ width * sizeof(XcursorPixel)); + + CGContextRelease(context); + + _cursor = XcursorImageLoadCursor(display, ximage); ++ XcursorImageDestroy(ximage); + if (_cursor == None) + _cursor = XcursorLibraryLoadCursor(display, "left_ptr"); + return self; +diff --git a/AppKit/X11.backend/X11Display.h b/AppKit/X11.backend/X11Display.h +index 18189404..dd6215c9 100644 +--- a/AppKit/X11.backend/X11Display.h ++++ b/AppKit/X11.backend/X11Display.h +@@ -16,7 +16,7 @@ + #import + #endif + +-@class X11Cursor; ++@class X11Cursor, X11Window; + + @interface X11Display : NSDisplay { + Display *_display; +@@ -37,7 +37,10 @@ + int clickCount; + X11Cursor *_blankCursor, *_defaultCursor; + BOOL _cursorGrabbed; ++ Window _confinedWindow; ++ unsigned int _otherButtonsDown; + KeySym _lastKeySym; ++ unsigned char _modifierKeysDown[256]; + int _rrEventBase; + NSArray* _lastScreens; + +@@ -45,7 +48,9 @@ + XIM _xim; + } + ++- (void) updateMouseConfinementForWindow:(X11Window *)window; + - (Display *) display; ++- (BOOL) activateProcessIdentifier: (pid_t) processIdentifier; + + - (void) setWindow: (id) window forID: (XID) i; + +diff --git a/AppKit/X11.backend/X11Display.m b/AppKit/X11.backend/X11Display.m +index 5ae29bf4..358e606b 100644 +--- a/AppKit/X11.backend/X11Display.m ++++ b/AppKit/X11.backend/X11Display.m +@@ -48,15 +48,21 @@ + #import "X11KeySymToUCS.h" + #import + #import ++#import + #import + #import + #import + #import + #import + #import ++#include + + void NSColorSetCatalogColor(NSColorListName catalogName, NSColorName colorName, NSColor *color); + ++@interface NSCursor (X11CursorVisibility) +++ (void) _unhideOnMouseMove; ++@end ++ + @implementation X11Display + + static int errorHandler(Display *display, XErrorEvent *errorEvent) { +@@ -75,6 +81,15 @@ static void socketCallback(CFSocketRef s, CFSocketCallBackType type, + - init { + if (self = [super init]) { + ++#ifdef DARLING ++ // EGL also uses this Display from compositor threads. Enable Xlib's ++ // connection locks before opening it or issuing any Xlib requests. ++ if (!XInitThreads()) { ++ NSLog(@"Unable to initialize Xlib thread support"); ++ [self release]; ++ return nil; ++ } ++#endif + _display = XOpenDisplay(NULL); + + if (_display == NULL) { +@@ -148,14 +163,16 @@ static void socketCallback(CFSocketRef s, CFSocketCallBackType type, + [_blankCursor release]; + [_defaultCursor release]; + +- XCloseIM(_xim); ++ if (_xim) ++ XCloseIM(_xim); + + if (_display) + XCloseDisplay(_display); + #ifdef DARLING +- CFRunLoopRemoveSource(CFRunLoopGetMain(), _source, kCFRunLoopCommonModes); +- if (_source != NULL) ++ if (_source != NULL) { ++ CFRunLoopRemoveSource(CFRunLoopGetMain(), _source, kCFRunLoopCommonModes); + CFRelease(_source); ++ } + if (_cfSocket != NULL) + CFRelease(_cfSocket); + #endif +@@ -172,6 +189,56 @@ static void socketCallback(CFSocketRef s, CFSocketCallBackType type, + return _display; + } + ++- (BOOL) activateProcessIdentifier: (pid_t) processIdentifier { ++ if (!_display || processIdentifier <= 0) return NO; ++ ++ Window root = DefaultRootWindow(_display), parent = None; ++ Window *children = NULL; ++ unsigned int count = 0; ++ if (!XQueryTree(_display, root, &root, &parent, &children, &count)) ++ return NO; ++ ++ Atom pidAtom = XInternAtom(_display, "_NET_WM_PID", False); ++ Atom activeAtom = XInternAtom(_display, "_NET_ACTIVE_WINDOW", False); ++ BOOL activated = NO; ++ for (unsigned int index = count; index > 0; --index) { ++ Window window = children[index - 1]; ++ Atom actualType = None; ++ int actualFormat = 0; ++ unsigned long items = 0, remaining = 0; ++ unsigned char *value = NULL; ++ int status = XGetWindowProperty(_display, window, pidAtom, 0, 1, False, ++ XA_CARDINAL, &actualType, &actualFormat, ++ &items, &remaining, &value); ++ BOOL matches = status == Success && actualType == XA_CARDINAL && ++ actualFormat == 32 && items == 1 && value && ++ *(unsigned long *)value == (unsigned long)processIdentifier; ++ if (value) XFree(value); ++ if (!matches) continue; ++ ++ XWindowAttributes attributes; ++ if (!XGetWindowAttributes(_display, window, &attributes) || ++ attributes.map_state != IsViewable) continue; ++ ++ XEvent event = {0}; ++ event.xclient.type = ClientMessage; ++ event.xclient.window = window; ++ event.xclient.message_type = activeAtom; ++ event.xclient.format = 32; ++ event.xclient.data.l[0] = 1; ++ event.xclient.data.l[1] = CurrentTime; ++ XSendEvent(_display, root, False, ++ SubstructureRedirectMask | SubstructureNotifyMask, &event); ++ XRaiseWindow(_display, window); ++ XSetInputFocus(_display, window, RevertToPointerRoot, CurrentTime); ++ XFlush(_display); ++ activated = YES; ++ break; ++ } ++ if (children) XFree(children); ++ return activated; ++} ++ + - (void) _enableDetectableAutoRepeat { + int major = XkbMajorVersion, minor = XkbMinorVersion; + XkbStateRec state; +@@ -536,12 +603,52 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { + return &layout->layout; + } + ++/* darling-lol: the game asks for the pointer position, the pressed buttons and the modifier state about a thousand ++ * times per second. Each answer was a synchronous X round trip (XQueryPointer / XkbGetState) that puts the main thread ++ * to sleep until XWayland replies; while the mouse moves they dominated the slow frames. Reuse a reply for 8 ms, keep it ++ * current from the input events themselves (a motion event carries position and button state), and drop it on every ++ * button, key, crossing or focus event so the next question after a state change is always asked for real. */ ++#include ++static BOOL rclolFresh(uint64_t *stamp) { ++ static mach_timebase_info_data_t tb; ++ if (tb.denom == 0) mach_timebase_info(&tb); ++ uint64_t now = mach_absolute_time(); ++ if (*stamp != 0 && (now - *stamp) * tb.numer / tb.denom < 8000000ULL) return YES; ++ *stamp = now; ++ return NO; ++} ++static uint64_t rclolPointerStamp, rclolModsStamp; ++static int rclolRootX, rclolRootY; ++static unsigned int rclolPointerState; ++static NSUInteger rclolMods; ++static BOOL rclolQueryPointer(Display *display) { ++ if (rclolFresh(&rclolPointerStamp)) return YES; ++ for (int screen = 0; screen < ScreenCount(display); ++screen) { ++ Window root, child; ++ int windowX, windowY; ++ if (XQueryPointer(display, RootWindow(display, screen), &root, &child, &rclolRootX, &rclolRootY, ++ &windowX, &windowY, &rclolPointerState)) ++ return YES; ++ } ++ rclolPointerStamp = 0; ++ return NO; ++} ++ + - (NSUInteger) currentModifierFlags { ++ if (rclolFresh(&rclolModsStamp)) return rclolMods; + XkbStateRec r; +- if (XkbGetState(_display, XkbUseCoreKbd, &r) != Success) ++ if (XkbGetState(_display, XkbUseCoreKbd, &r) != Success) { ++ rclolModsStamp = 0; + return 0; ++ } ++ return rclolMods = [self modifierFlagsForState: r.mods]; ++} + +- return [self modifierFlagsForState: r.mods]; ++- (NSUInteger) pressedMouseButtons { ++ if (!rclolQueryPointer(_display)) return 0; ++ unsigned int state = rclolPointerState; ++ // AppKit numbers left/right/middle as 0/1/2; X11 uses 1/3/2. ++ return ((state & Button1Mask) ? 1u : 0u) | ((state & Button3Mask) ? 2u : 0u) | ((state & Button2Mask) ? 4u : 0u); + } + + - (NSPasteboard *) pasteboardWithName: (NSString *) name { +@@ -675,6 +782,15 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { + blue: 0.60f + alpha: 1.0f]; + ++ if ([colorName isEqual: @"controlAccentBlueColor"]) ++ /* darling-lol: macOS 10.14+ system accent blue (sRGB 0,122,255); ++ * Chromium queries it during UI setup and previously fell back to ++ * red. */ ++ return [NSColor colorWithSRGBRed: 0.0f ++ green: 0.478431f ++ blue: 1.0f ++ alpha: 1.0f]; ++ + NSLog(@"missing color for %@", colorName); + return [NSColor redColor]; + } +@@ -921,15 +1037,9 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { + } + + - (NSPoint) mouseLocation { +- Window child, root = DefaultRootWindow(_display); +- int root_x, root_y; +- int win_x, win_y; +- unsigned int mask; +- +- XQueryPointer(_display, root, &root, &child, &root_x, &root_y, &win_x, +- &win_y, &mask); ++ rclolQueryPointer(_display); + int height = DisplayHeight(_display, DefaultScreen(_display)); +- return NSMakePoint(root_x, height - root_y); ++ return NSMakePoint(rclolRootX, height - rclolRootY); + } + + - (void) setWindow: (id) window forID: (XID) i { +@@ -962,20 +1072,82 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { + { + #ifndef DARLING + [[NSRunLoop currentRunLoop] addInputSource: _inputSource forMode: mode]; +-#else +- [self processPendingEvents]; +-#endif + + NSEvent *result = [super nextEventMatchingMask: mask + untilDate: untilDate + inMode: mode + dequeue: dequeue]; + +-#ifndef DARLING + [[NSRunLoop currentRunLoop] removeInputSource: _inputSource forMode: mode]; +-#endif + + return result; ++#else ++ // Darling: the CFSocket runloop source watching the X connection is not ++ // guaranteed to wake us, so a single blocking wait can sleep through ++ // input forever (X events queue unread, app looks frozen). Poll with a ++ // short slice instead. (darling-lol M2b) ++ /* A past deadline means poll, not skip queued input. Chromium's event ++ * pump regularly uses distantPast while checking for pending events. */ ++ [self processPendingEvents]; ++ NSEvent *result = [super nextEventMatchingMask:mask untilDate:[NSDate date] ++ inMode:mode dequeue:dequeue]; ++ if (result != nil && [result type] == NSAppKitSystem) ++ result = nil; ++ NSTimeInterval remaining; ++ while (result == nil && (remaining = [untilDate timeIntervalSinceNow]) > 0) { ++ /* Display-link and input sources have no CF timer deadline. A 20ms ++ * fallback can sleep past every 60Hz source wakeup; bound its latency ++ * below a refresh interval while the socket wakeup path is polled. */ ++ NSTimeInterval slice = remaining < 0.004 ? remaining : 0.004; ++ /* A quiet X connection must not prevent main-queue callbacks or ++ * CF timers from running while AppKit waits for its next event. */ ++ CFStringRef cfMode = (mode == nil || [mode isEqual:NSDefaultRunLoopMode]) ++ ? kCFRunLoopDefaultMode : (CFStringRef)mode; ++ SInt32 runResult = CFRunLoopRunInMode(cfMode, 0.0, true); ++ extern void _dispatch_main_queue_callback_4CF(void *); ++ _dispatch_main_queue_callback_4CF(NULL); ++ [self processPendingEvents]; ++ /* Do not let NSRunLoop perform the timed wait here. Darling's ++ * socket-backed Mach receive can ignore the CF timer deadline and ++ * sleep forever, preventing Electron readiness promises, dispatch ++ * work, and repeating timers from being serviced. Poll the nested ++ * run loop without blocking, then bound the idle wait ourselves. */ ++ result = [super nextEventMatchingMask: mask ++ untilDate: [NSDate date] ++ inMode: mode ++ dequeue: dequeue]; ++ if (result != nil && [result type] == NSAppKitSystem) { ++ result = nil; ++ } ++ /* A handled source can immediately queue more work (Chromium and ++ * libuv do this). Drain it before the idle poll delay; sleeping after ++ * every callback caps a busy source at 50 callbacks per second. */ ++ if (result == nil && runResult == kCFRunLoopRunHandledSource) ++ continue; ++ if (result == nil) { ++ /* Polling must honor timers in both the Cocoa mode and the CF ++ * main-queue mode. A fixed 20ms sleep misses 60Hz deadlines. */ ++ CFRunLoopRef loop = CFRunLoopGetCurrent(); ++ CFAbsoluteTime now = CFAbsoluteTimeGetCurrent(); ++ CFAbsoluteTime next = CFRunLoopGetNextTimerFireDate(loop, cfMode); ++ if (mode && !CFEqual((CFStringRef)mode, cfMode)) { ++ CFAbsoluteTime cocoaNext = CFRunLoopGetNextTimerFireDate(loop, (CFStringRef)mode); ++ if (cocoaNext > 0 && (next <= 0 || cocoaNext < next)) next = cocoaNext; ++ } ++ if (next > 0 && next - now < slice) ++ slice = next > now ? next - now : 0; ++ } ++ if (result == nil && slice > 0) { ++ /* Keep this polling deadline independent of Mach wait timers. */ ++ struct { long seconds, nanoseconds; } delay = { ++ 0, (long)(slice * 1000000000.0) ++ }; ++ extern long __lolsys_syscall(long, long, long, long, long, long, int); ++ __lolsys_syscall((long)&delay, 0, 0, 0, 0, 0, 35 /* Linux nanosleep */); ++ } ++ } ++ return result; ++#endif + } + + - (NSEventModifierFlags) modifierFlagsForState: (unsigned int) state { +@@ -1010,17 +1182,93 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { + return result; + } + ++/* X key events carry the state BEFORE the transition. Cocoa flagsChanged ++ * carries the state AFTER it. Keep both sides separately so releasing one ++ * Shift/Command key does not clear the other, and reconcile missed releases ++ * when the compositor intercepts a shortcut or changes keyboard focus. */ ++static unsigned int rclolModifierMask(unsigned int key) { ++ switch (key) { ++ case 50: case 62: return ShiftMask; ++ case 37: case 105: return ControlMask; ++ case 64: return Mod1Mask; ++ case 108: return Mod5Mask; ++ case 133: case 134: return Mod4Mask; ++ default: return 0; ++ } ++} ++ ++- (void) postModifierKey:(unsigned int)key state:(unsigned int)state window:(id)delegate { ++ NSEvent *event = [NSEvent keyEventWithType:NSFlagsChanged ++ location:NSZeroPoint modifierFlags:[self modifierFlagsForState:state] ++ timestamp:0 windowNumber:[delegate windowNumber] context:nil ++ characters:@"" charactersIgnoringModifiers:@"" isARepeat:NO ++ keyCode:x11ToCarbon[key]]; ++ [self postEvent:event atStart:NO]; ++} ++ ++- (void) reconcileModifierKeys:(const char *)keys window:(id)delegate { ++ unsigned int state = 0; ++ for (unsigned int key = 0; key < 256; ++key) ++ if (_modifierKeysDown[key]) state |= rclolModifierMask(key); ++ for (unsigned int key = 0; key < 256; ++key) { ++ unsigned int mask = rclolModifierMask(key); ++ if (!mask) continue; ++ BOOL down = keys && (keys[key / 8] & (1 << (key % 8))); ++ if (down == _modifierKeysDown[key]) continue; ++ _modifierKeysDown[key] = down; ++ state &= ~mask; ++ for (unsigned int other = 0; other < 256; ++other) ++ if (_modifierKeysDown[other]) state |= rclolModifierMask(other); ++ [self postModifierKey:key state:state window:delegate]; ++ } ++} ++ + - (void) postXEvent: (XEvent *) ev { + id event = nil; + NSEventType type; + X11Window* window = (X11Window*) [self windowForID: ev->xany.window]; + + id delegate = [window delegate]; ++ static int inputTrace = -1; ++ if (inputTrace < 0) inputTrace = getenv("RCLOL_INPUT_TRACE") != NULL; ++ if (inputTrace && (ev->type == KeyPress || ev->type == KeyRelease || ev->type == ButtonPress)) ++ NSLog(@"LOLINPUT X type=%d window=%lu mapped=%@ number=%ld", ev->type, ++ (unsigned long)ev->xany.window, [delegate class], (long)[delegate windowNumber]); ++ ++ switch (ev->type) { ++ case MotionNotify: ++ rclolRootX = ev->xmotion.x_root; ++ rclolRootY = ev->xmotion.y_root; ++ rclolPointerState = ev->xmotion.state; ++ rclolPointerStamp = mach_absolute_time(); ++ break; ++ case KeyPress: case KeyRelease: case ButtonPress: case ButtonRelease: ++ case EnterNotify: case LeaveNotify: case FocusIn: case FocusOut: case KeymapNotify: ++ rclolPointerStamp = rclolModsStamp = 0; ++ break; ++ default: ++ break; ++ } + + switch (ev->type) { + case KeyPress: + case KeyRelease:; + ++ unsigned int key = ev->xkey.keycode; ++ unsigned int mask = rclolModifierMask(key); ++ if (mask || key == 66) { ++ unsigned int state = ev->xkey.state; ++ if (key == 66) { ++ if (ev->type == KeyPress) state ^= LockMask; ++ } else { ++ _modifierKeysDown[key] = ev->type == KeyPress; ++ state &= ~mask; ++ for (unsigned int other = 0; other < 256; ++other) ++ if (_modifierKeysDown[other]) state |= rclolModifierMask(other); ++ } ++ [self postModifierKey:key state:state window:delegate]; ++ break; ++ } + NSEventModifierFlags modifierFlags = [self modifierFlagsForState: ev->xkey.state]; + char buf[20] = {0}; + KeySym keySym; +@@ -1038,6 +1286,18 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { + buf[strLen] = 0; + } + ++ /* Host clipboard shortcuts use Control; the guest app expects Command. ++ * Preserve other Control bindings, AltGr and explicit Super shortcuts. */ ++ if ((modifierFlags & NSControlKeyMask) && ++ !(modifierFlags & (NSCommandKeyMask | NSAlternateKeyMask)) && ++ (keySym == XK_v || keySym == XK_V || keySym == XK_c || ++ keySym == XK_C || keySym == XK_x || keySym == XK_X)) { ++ modifierFlags = (modifierFlags & ~NSControlKeyMask) | NSCommandKeyMask; ++ buf[0] = (char) ((keySym >= XK_A && keySym <= XK_Z) ? ++ keySym - XK_A + 'a' : keySym); ++ buf[1] = 0; ++ } ++ + id str = [[NSString alloc] initWithCString: buf + encoding: NSUTF8StringEncoding]; + NSPoint pos = +@@ -1077,6 +1337,29 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { + break; + + case ButtonPress:; ++ if (ev->xbutton.button >= Button4 && ev->xbutton.button <= 7) { ++ pos = [window transformPoint:NSMakePoint(ev->xbutton.x, ev->xbutton.y)]; ++ CGFloat wheelX = ev->xbutton.button == 6 ? 1.0 : ++ ev->xbutton.button == 7 ? -1.0 : 0.0; ++ CGFloat wheelY = ev->xbutton.button == Button4 ? 1.0 : ++ ev->xbutton.button == Button5 ? -1.0 : 0.0; ++ static int scrollLines; ++ if (!scrollLines) { ++ const char *setting = getenv("RCLOL_SCROLL_SPEED"); ++ int requested = setting ? atoi(setting) : 3; ++ scrollLines = requested >= 1 && requested <= 20 ? requested : 3; ++ } ++ wheelX *= scrollLines; wheelY *= scrollLines; ++ event = [NSEvent mouseEventWithType:NSScrollWheel location:pos ++ modifierFlags:[self modifierFlagsForState:ev->xbutton.state] ++ window:delegate clickCount:0 deltaX:wheelX deltaY:wheelY]; ++ [self postEvent:event atStart:NO]; ++ break; ++ } ++ if (ev->xbutton.button == Button2 || ev->xbutton.button >= 8) { ++ unsigned b = ev->xbutton.button == Button2 ? 2 : ev->xbutton.button - 5; ++ if (b < 32) _otherButtonsDown |= 1u << b; ++ } + NSTimeInterval now = [[NSDate date] timeIntervalSinceReferenceDate]; + + if (now - lastClickTimeStamp < [self doubleClickInterval]) { +@@ -1096,10 +1379,6 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { + case Button3: + type = NSRightMouseDown; + break; +- case Button4: +- case Button5: +- // Skip these, we'll send NSScrollWheel on release. +- return; + default: + type = NSOtherMouseDown; + } +@@ -1113,14 +1392,22 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { + clickCount: clickCount + deltaX: 0.0 + deltaY: 0.0]; +- [(NSEvent_mouse *) event _setButtonNumber: ev->xbutton.button]; ++ [(NSEvent_mouse *) event _setButtonNumber: ++ ev->xbutton.button == Button1 ? 0 : ev->xbutton.button == Button3 ? 1 : ++ ev->xbutton.button == Button2 ? 2 : ev->xbutton.button - 5]; + [self postEvent: event atStart: NO]; + break; + + case ButtonRelease: ++ // One scroll event per notch, sent on press; releases are not clicks. ++ if (ev->xbutton.button >= Button4 && ev->xbutton.button <= 7) break; + pos = [window + transformPoint: NSMakePoint(ev->xbutton.x, ev->xbutton.y)]; + ++ if (ev->xbutton.button == Button2 || ev->xbutton.button >= 8) { ++ unsigned b = ev->xbutton.button == Button2 ? 2 : ev->xbutton.button - 5; ++ if (b < 32) _otherButtonsDown &= ~(1u << b); ++ } + CGFloat deltaY = 0.0; + + switch (ev->xbutton.button) { +@@ -1130,14 +1417,6 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { + case Button3: + type = NSRightMouseUp; + break; +- case Button4: +- type = NSScrollWheel; +- deltaY = 1.0; +- break; +- case Button5: +- type = NSScrollWheel; +- deltaY = -1.0; +- break; + default: + type = NSOtherMouseUp; + } +@@ -1151,11 +1430,14 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { + clickCount: clickCount + deltaX: 0.0 + deltaY: deltaY]; +- [event _setButtonNumber: ev->xbutton.button]; ++ [event _setButtonNumber: ++ ev->xbutton.button == Button1 ? 0 : ev->xbutton.button == Button3 ? 1 : ++ ev->xbutton.button == Button2 ? 2 : ev->xbutton.button - 5]; + [self postEvent: event atStart: NO]; + break; + + case MotionNotify:; { ++ [NSCursor _unhideOnMouseMove]; + // NSLog(@"MotionNotify, x=%d, y=%d, xroot=%d, yroot=%d\n", + // ev->xmotion.x, ev->xmotion.y, ev->xmotion.x_root, + // ev->xmotion.y_root); +@@ -1195,14 +1477,28 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { + + if (ev->xmotion.state & Button1Mask) { + type = NSLeftMouseDragged; +- } else if (ev->xmotion.state & Button2Mask) { ++ } else if (ev->xmotion.state & Button3Mask) { + type = NSRightMouseDragged; ++ } else if ((ev->xmotion.state & Button2Mask) || _otherButtonsDown) { ++ type = NSOtherMouseDragged; + } + +- if (type == NSMouseMoved && ![delegate acceptsMouseMovedEvents]) +- break; ++ if (inputTrace) { ++ static NSTimeInterval lastMotionTrace; ++ NSTimeInterval now = [NSDate timeIntervalSinceReferenceDate]; ++ if (now - lastMotionTrace >= 1.0) { ++ lastMotionTrace = now; ++ NSLog(@"LOLINPUT motion window=%lu mapped=%@ pos=%.0f,%.0f delta=%.0f,%.0f state=%u grabbed=%d forward=%d", ++ (unsigned long)ev->xany.window, [delegate class], ++ pos.x, pos.y, deltaX, deltaY, ev->xmotion.state, ++ _cursorGrabbed, type != NSMouseMoved || [delegate acceptsMouseMovedEvents] || ++ getenv("RCLOL_FORWARD_MOUSE_MOVED") != NULL); ++ } ++ } + +- event = [NSEvent ++ if (type != NSMouseMoved || [delegate acceptsMouseMovedEvents] || ++ getenv("RCLOL_FORWARD_MOUSE_MOVED")) { ++ event = [NSEvent + mouseEventWithType: type + location: pos + modifierFlags: +@@ -1211,25 +1507,40 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { + clickCount: 1 + deltaX: deltaX + deltaY: deltaY]; ++ if (type == NSOtherMouseDragged) { ++ unsigned buttons = _otherButtonsDown | ((ev->xmotion.state & Button2Mask) ? 4u : 0u); ++ [event _setButtonNumber: __builtin_ctz(buttons)]; ++ } + [self postEvent: event atStart: NO]; +- [self discardEventsMatchingMask: NSLeftMouseDraggedMask ++ [self discardEventsMatchingMask: NSEventMaskFromType(type) + beforeEvent: event]; ++ } + ++ /* Tracking areas need motion even when the window doesn't ask ++ * for general mouseMoved events (Chromium uses this mode). */ + [delegate platformWindowSetCursorEvent: window]; + break; + } + + case EnterNotify: ++ [self updateMouseConfinementForWindow:window]; + NSLog(@"EnterNotify"); + if (!_cursorGrabbed) + [window setLastKnownCursorPosition: + [window transformPoint: NSMakePoint( + ev->xcrossing.x, + ev->xcrossing.y)]]; ++ if (!_cursorGrabbed && ev->xcrossing.detail != NotifyInferior) ++ [delegate platformWindowSetCursorEvent: window]; + break; + + case LeaveNotify: + NSLog(@"LeaveNotify"); ++ if (!_cursorGrabbed && ev->xcrossing.detail != NotifyInferior) { ++ [window setLastKnownCursorPosition: ++ [window transformPoint: NSMakePoint(ev->xcrossing.x, ev->xcrossing.y)]]; ++ [delegate platformWindowSetCursorEvent: window]; ++ } + break; + + case FocusIn: +@@ -1243,28 +1554,42 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { + checkForAppDeactivation: NO]; + lastFocusedWindow = nil; + } ++ char modifierKeys[32]; ++ XQueryKeymap(_display, modifierKeys); ++ [self reconcileModifierKeys:modifierKeys window:delegate]; + [delegate platformWindowActivated: window displayIfNeeded: YES]; + lastFocusedWindow = delegate; + if (window != nil) + XSetICFocus(window->_xic); ++ [self updateMouseConfinementForWindow:window]; + break; + + case FocusOut: + NSLog(@"FocusOut"); ++ [self reconcileModifierKeys:NULL window:delegate]; ++ _lastKeySym = 0; + [delegate platformWindowDeactivated: window + checkForAppDeactivation: NO]; + lastFocusedWindow = nil; + if (_cursorGrabbed) + [self grabMouse: NO]; ++ if (_confinedWindow) { ++ XUngrabPointer(_display, CurrentTime); ++ _confinedWindow = None; ++ } ++ _otherButtonsDown = 0; + if (window != nil) + XUnsetICFocus(window->_xic); + break; + + case KeymapNotify: + NSLog(@"KeymapNotify"); ++ [self reconcileModifierKeys:ev->xkeymap.key_vector window:lastFocusedWindow]; + break; + + case Expose:; ++ if ([window handleRemoteFrameExpose: ev->xany.window]) ++ break; + O2Rect rect = NSMakeRect(ev->xexpose.x, ev->xexpose.y, + ev->xexpose.width, ev->xexpose.height); + +@@ -1301,11 +1626,20 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { + break; + + case UnmapNotify: +- NSLog(@"UnmapNotify"); +- break; +- + case MapNotify: +- NSLog(@"MapNotify"); ++ /* Map state belongs to the top-level native window. Remote frame and ++ * GL children must not change its Cocoa visibility or occlusion state. */ ++ if(window && ev->xany.window == [window windowHandle] && ++ [delegate respondsToSelector: @selector(_setVisible:)]) { ++ BOOL visible = ev->type == MapNotify; ++ [self updateMouseConfinementForWindow:window]; ++ if([delegate isVisible] != visible) { ++ [delegate _setVisible: visible]; ++ [[NSNotificationCenter defaultCenter] ++ postNotificationName: NSWindowDidChangeOcclusionStateNotification ++ object: delegate]; ++ } ++ } + break; + + case MapRequest: +@@ -1321,6 +1655,7 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { + [delegate platformWindow: window + frameChanged: [window frame] + didSize: YES]; ++ [self updateMouseConfinementForWindow:window]; + break; + + case ConfigureRequest: +@@ -1344,6 +1679,8 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { + break; + + case PropertyNotify: ++ if (ev->xproperty.atom == XInternAtom(_display, "_NET_WM_STATE", False)) ++ [self updateMouseConfinementForWindow:window]; + if ([window respondsToSelector: @selector(propertyNotify:)]) { + [window propertyNotify: &ev->xproperty]; + } +@@ -1483,6 +1820,64 @@ void CGNativeBorderFrameWidthsForStyle(NSUInteger styleMask, CGFloat *top, + XSync(_display, False); + } + ++/* Confinement preserves absolute motion and edge panning. Relative-mode ++ * capture remains separate: it deliberately warps to the center. */ ++- (void) updateMouseConfinementForWindow:(X11Window *)window { ++ if (getenv("RCLOL_INPUT_TRACE")) fprintf(stderr,"CONFINEMENT check window=%p delegate=%p focused=%p relative=%d\n",window,[window delegate],lastFocusedWindow,_cursorGrabbed); ++ if (!window || _cursorGrabbed) return; ++ Window win = [window windowHandle]; ++ Window focus; int revert; ++ XGetInputFocus(_display, &focus, &revert); ++ Window ancestor = focus; ++ while (ancestor != win && ancestor != None && ancestor != PointerRoot && ancestor != DefaultRootWindow(_display)) { ++ Window root, parent, *children=NULL; unsigned nchildren; ++ if (!XQueryTree(_display, ancestor, &root, &parent, &children, &nchildren)) break; ++ if (children) XFree(children); ++ ancestor=parent; ++ } ++ if (getenv("RCLOL_INPUT_TRACE")) fprintf(stderr,"CONFINEMENT focus=%lu target=%lu ancestor=%lu\n",focus,win,ancestor); ++ if (ancestor != win) return; ++ XWindowAttributes attrs; ++ if (!XGetWindowAttributes(_display, win, &attrs)) return; ++ BOOL fullscreen = NO; ++ Atom actual; int format; unsigned long count, remaining; unsigned char *data = NULL; ++ Atom state = XInternAtom(_display, "_NET_WM_STATE", False); ++ Atom fs = XInternAtom(_display, "_NET_WM_STATE_FULLSCREEN", False); ++ if (XGetWindowProperty(_display, win, state, 0, 64, False, XA_ATOM, ++ &actual, &format, &count, &remaining, &data) == Success && format == 32) { ++ for (unsigned long i=0; incrtc; ++i) { ++ XRRCrtcInfo *crtc = XRRGetCrtcInfo(_display, resources, resources->crtcs[i]); ++ if (crtc) { ++ if (crtc->mode && abs(x-crtc->x)<=2 && abs(y-crtc->y)<=2 && ++ abs(attrs.width-(int)crtc->width)<=2 && abs(attrs.height-(int)crtc->height)<=2) fullscreen=YES; ++ XRRFreeCrtcInfo(crtc); ++ } ++ } ++ XRRFreeScreenResources(resources); ++ } ++ } ++ if (!fullscreen || attrs.map_state != IsViewable) { ++ if (_confinedWindow == win) { XUngrabPointer(_display, CurrentTime); _confinedWindow=None; } ++ return; ++ } ++ if (_confinedWindow == win) return; ++ unsigned mask = ButtonPressMask | ButtonReleaseMask | PointerMotionMask; ++ int status=XGrabPointer(_display, win, True, mask, GrabModeAsync, GrabModeAsync, ++ win, None, CurrentTime); ++ if (getenv("RCLOL_INPUT_TRACE")) fprintf(stderr,"CONFINEMENT fullscreen=%d window=%lu result=%d\n",fullscreen,win,status); ++ if (status == GrabSuccess) _confinedWindow=win; ++} ++ + - (void) grabMouse: (BOOL) doGrab { + if (doGrab) { + NSWindow *nswin = [NSApp keyWindow]; +@@ -1495,9 +1890,10 @@ void CGNativeBorderFrameWidthsForStyle(NSUInteger styleMask, CGFloat *top, + const unsigned int mask = ButtonPressMask | ButtonReleaseMask | + PointerMotionMask | FocusChangeMask; + int result = XGrabPointer(_display, win, False, mask, GrabModeAsync, +- GrabModeAsync, None, None, CurrentTime); ++ GrabModeAsync, win, None, CurrentTime); + if (result == GrabSuccess) { + _cursorGrabbed = YES; ++ _confinedWindow = None; + NSLog(@"XGrabPointer() succeeded for window %lu\n", win); + + NSRect frame = [xwin transformFrame: nswin.frame]; +@@ -1522,6 +1918,9 @@ void CGNativeBorderFrameWidthsForStyle(NSUInteger styleMask, CGFloat *top, + } else { + XUngrabPointer(_display, CurrentTime); + _cursorGrabbed = NO; ++ _confinedWindow = None; ++ NSWindow *nswin = [NSApp keyWindow]; ++ if (nswin) [self updateMouseConfinementForWindow:(X11Window *)[nswin platformWindow]]; + } + XSync(_display, False); + } +diff --git a/AppKit/X11.backend/X11Pasteboard.h b/AppKit/X11.backend/X11Pasteboard.h +index 411903cc..4b196afd 100644 +--- a/AppKit/X11.backend/X11Pasteboard.h ++++ b/AppKit/X11.backend/X11Pasteboard.h +@@ -26,6 +26,7 @@ along with Darling. If not, see . + Display *_display; + Window _window; + Atom _selectionName; ++ Window _observedSelectionOwner; + + NSMutableDictionary *_typeToData; + NSMutableDictionary> +diff --git a/AppKit/X11.backend/X11Pasteboard.m b/AppKit/X11.backend/X11Pasteboard.m +index 0e91a5d1..1e4cec1b 100644 +--- a/AppKit/X11.backend/X11Pasteboard.m ++++ b/AppKit/X11.backend/X11Pasteboard.m +@@ -18,6 +18,8 @@ along with Darling. If not, see . + */ + + #import "X11Pasteboard.h" ++#include ++#include + + @implementation X11Pasteboard + +@@ -53,9 +55,7 @@ static NSMutableDictionary *nameToPboard; + int screen = DefaultScreen(_display); + _window = XCreateSimpleWindow(_display, RootWindow(_display, screen), -10, + -10, 1, 1, 0, 0, 0); +- XSelectInput(_display, _window, +- SelectionClear | SelectionRequest | SelectionNotify | +- PropertyNotify); ++ XSelectInput(_display, _window, PropertyChangeMask); + + [x11Display setWindow: self forID: _window]; + +@@ -63,6 +63,7 @@ static NSMutableDictionary *nameToPboard; + } + + - (void) ensureSelectionOwner { ++ [self syncSelectionOwner]; + if (_typeToData != nil) { + // Already an owner. + return; +@@ -93,7 +94,8 @@ static NSMutableDictionary *nameToPboard; + - (void) giveUpSelectionOwner { + _changeCount++; + +- [self clearContents]; ++ for (id owner in [_typeToOwner allValues]) ++ [owner pasteboardChangedOwner: self]; + + [_typeToOwner release]; + _typeToOwner = nil; +@@ -115,12 +117,47 @@ static NSMutableDictionary *nameToPboard; + [super dealloc]; + } + ++- (void) syncSelectionOwner { ++ Window owner = XGetSelectionOwner(_display, _selectionName); ++ if (_typeToData != nil && owner != _window) ++ [self giveUpSelectionOwner]; ++ if (owner != _observedSelectionOwner) { ++ _observedSelectionOwner = owner; ++ _changeCount++; ++ } ++} ++ + - (NSInteger) changeCount { ++ [self syncSelectionOwner]; + return _changeCount; + } + ++- (BOOL) requestTarget: (Atom) target { ++ XDeleteProperty(_display, _window, _receivingProperty); ++ XConvertSelection(_display, _selectionName, target, _receivingProperty, ++ _window, CurrentTime); ++ XFlush(_display); ++ struct timespec start, now; ++ clock_gettime(CLOCK_MONOTONIC, &start); ++ for (;;) { ++ XEvent event; ++ if (XCheckTypedWindowEvent(_display, _window, SelectionNotify, &event)) { ++ if (event.xselection.selection == _selectionName && ++ event.xselection.target == target) ++ return event.xselection.property == _receivingProperty; ++ } ++ clock_gettime(CLOCK_MONOTONIC, &now); ++ if ((now.tv_sec-start.tv_sec)*1000 + ++ (now.tv_nsec-start.tv_nsec)/1000000 >= 2000) ++ return NO; ++ struct pollfd fd = { ConnectionNumber(_display), POLLIN, 0 }; ++ poll(&fd, 1, 10); ++ } ++} ++ + - (void) selectionClear: (XSelectionClearEvent *) event { +- [self giveUpSelectionOwner]; ++ if (XGetSelectionOwner(_display, _selectionName) != _window) ++ [self giveUpSelectionOwner]; + } + + + (NSArray *) targetsForType: (NSPasteboardType) type { +@@ -177,7 +214,7 @@ static NSMutableDictionary *nameToPboard; + } + + - (NSData *) dataForType: (NSPasteboardType) type { +- ++ [self syncSelectionOwner]; + if (_typeToOwner != nil) { + // We're the owner; just fetch the data directly. + id owner = _typeToOwner[type]; +@@ -185,25 +222,26 @@ static NSMutableDictionary *nameToPboard; + return _typeToData[type]; + } + +- NSString *target = [X11Pasteboard targetsForType: type][0]; +- Atom targetAtom = XInternAtom(_display, [target UTF8String], False); +- XConvertSelection(_display, _selectionName, targetAtom, _receivingProperty, +- _window, CurrentTime); // FIXME: don't use CurrentTime +- +- _selectionNotifyResult = WAITING; +- while (_selectionNotifyResult == WAITING) { +- [[NSDisplay currentDisplay] +- nextEventMatchingMask: NSAnyEventMask +- untilDate: [NSDate distantFuture] +- inMode: NSDefaultRunLoopMode +- dequeue: NO]; ++ for (NSString *target in [X11Pasteboard targetsForType: type]) { ++ Atom atom = XInternAtom(_display, [target UTF8String], False); ++ if (![self requestTarget: atom]) ++ continue; ++ NSData *data = [self readDataFromReceivingProperty]; ++ if (data != nil) { ++ if ([target isEqual: @"STRING"] && [type isEqual: NSStringPboardType]) { ++ NSString *text = [[[NSString alloc] initWithData: data ++ encoding: NSISOLatin1StringEncoding] autorelease]; ++ return [text dataUsingEncoding: NSUTF8StringEncoding]; ++ } ++ return data; ++ } + } +- +- return [self readDataFromReceivingProperty]; ++ return nil; + } + + - (NSString *) stringForType: (NSPasteboardType) type { + NSData *data = [self dataForType: type]; ++ if (data == nil) return nil; + + NSStringEncoding encoding = NSUnicodeStringEncoding; + if ([type isEqual: NSStringPboardType]) { +@@ -252,8 +290,37 @@ static NSMutableDictionary *nameToPboard; + } + + - (NSArray *) types { +- return [[_typeToData allKeys] +- arrayByAddingObjectsFromArray: [_typeToOwner allKeys]]; ++ [self syncSelectionOwner]; ++ if (_typeToData != nil) ++ return [[_typeToData allKeys] ++ arrayByAddingObjectsFromArray: [_typeToOwner allKeys]]; ++ if (![self requestTarget: XInternAtom(_display, "TARGETS", False)]) ++ return @[]; ++ Atom actualType; ++ int format; ++ unsigned long count, remaining; ++ unsigned char *bytes = NULL; ++ XGetWindowProperty(_display, _window, _receivingProperty, 0, 4096, True, ++ AnyPropertyType, &actualType, &format, &count, &remaining, ++ &bytes); ++ NSMutableArray *types = [NSMutableArray array]; ++ if (format == 32 && actualType != None) { ++ Atom *atoms = (Atom *) bytes; ++ for (unsigned long i = 0; i < count; i++) { ++ char *name = XGetAtomName(_display, atoms[i]); ++ if (name != NULL) { ++ NSString *target = [NSString stringWithUTF8String: name]; ++ if (![target isEqual: @"TARGETS"] && ![target isEqual: @"TIMESTAMP"]) { ++ NSString *type = [X11Pasteboard typeForTarget: target]; ++ if (![types containsObject: type]) ++ [types addObject: type]; ++ } ++ XFree(name); ++ } ++ } ++ } ++ if (bytes != NULL) XFree(bytes); ++ return types; + } + + - (void) selectionNotify: (XSelectionEvent *) event { +diff --git a/AppKit/X11.backend/X11Window.h b/AppKit/X11.backend/X11Window.h +index bfe3839a..2521ffd5 100644 +--- a/AppKit/X11.backend/X11Window.h ++++ b/AppKit/X11.backend/X11Window.h +@@ -26,7 +26,7 @@ + #import + #import + +-@class X11Display, CAWindowOpenGLContext, NSWindow, O2Context; ++@class X11Display, CAWindowOpenGLContext, NSWindow, O2Context, NSTimer; + + @interface X11Window : CGWindow { + int _level; // TODO: care about this value +@@ -49,6 +49,26 @@ + BOOL _isModal; + BOOL _isOpaque; + BOOL _hasShadow; ++ NSTimer *_remoteFrameTimer; ++ Window _remoteFrameWindow; ++ GC _remoteFrameGC; ++ unsigned int _remoteFrameSequence, _remoteFrameWidth, _remoteFrameHeight; ++ BOOL _remoteFrameValid; ++ char _remoteFramePath[128]; ++ XWindowAttributes _remoteFrameTargets[17]; ++ unsigned _remoteFrameTargetCount; ++ double _remoteFrameTargetRefresh; ++ void *_remoteFrameMapping; ++ size_t _remoteFrameMappingSize; ++ char _remoteFrameMappingPath[128]; ++ double _remoteFrameSelectionRefresh; ++ void *_remoteFrameShm; ++ void (*_remoteFrameShmDestroy)(void *); ++ int (*_remoteFrameShmPut)(void *, Drawable, GC, const void *, size_t); ++ int (*_remoteFrameShmStart)(void *, Drawable, GC, const char *); ++ BOOL _remoteFrameShmPumping; ++ BOOL _remoteFrameSoftwarePublisher; ++ BOOL _remoteFrameTimerSlow; + + @public + XIC _xic; +@@ -63,6 +83,7 @@ + - (O2Rect) transformFrame: (O2Rect) frame; + + - (Window) windowHandle; ++- (BOOL) handleRemoteFrameExpose: (Window) window; + + - (void) frameChanged; + - (void) setLastKnownCursorPosition: (CGPoint) point; +diff --git a/AppKit/X11.backend/X11Window.m b/AppKit/X11.backend/X11Window.m +index f4925b66..8553a8b4 100644 +--- a/AppKit/X11.backend/X11Window.m ++++ b/AppKit/X11.backend/X11Window.m +@@ -38,6 +38,34 @@ + #import "X11Window.h" + #import + #import ++#import ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#define DARLING_CA_FRAME_MAGIC 0x43414652u ++#define DARLING_CA_FRAME_BUFFERED_MAGIC 0x43414632u ++static size_t framePayloadOffset(uint32_t magic, uint32_t format) { ++ return magic == DARLING_CA_FRAME_BUFFERED_MAGIC && (format & 0x40000000u) ++ ? 32u * 1024u * 1024u : 0; ++} ++typedef struct { ++ uint32_t magic; ++ volatile uint32_t sequence; ++ uint32_t width; ++ uint32_t height; ++ uint32_t bytesPerRow; ++ uint32_t pixelFormat; ++ uint64_t dataSize; ++} DarlingCARemoteFrame; + + @implementation X11Window + +@@ -193,8 +221,13 @@ static NSData *makeWindowIcon() { + + XSetWindowAttributes xattr; + unsigned long xattr_mask; +- xattr.override_redirect = +- _styleMask == NSBorderlessWindowMask ? True : False; ++ // A borderless game is still a focusable application window. Leaving it ++ // override-redirect makes XWayland bypass compositor keyboard activation. ++ // Menus and nonactivating popups keep their unmanaged behavior. ++ xattr.override_redirect = _styleMask == NSBorderlessWindowMask && ++ ([delegate isKindOfClass:[NSMenuWindow class]] || ++ [delegate isKindOfClass:[NSPopUpWindow class]] || ++ ![delegate canBecomeKeyWindow]) ? True : False; + xattr_mask = CWOverrideRedirect | CWColormap; + xattr.colormap = cmap; + +@@ -206,6 +239,12 @@ static NSData *makeWindowIcon() { + (_visualInfo == NULL) ? CopyFromParent : _visualInfo->visual, + xattr_mask, &xattr); + ++ /* Keep Cocoa's process identifier available to cross-process activation. */ ++ unsigned long windowPID = (unsigned long)getpid(); ++ XChangeProperty(_display, _window, XInternAtom(_display, "_NET_WM_PID", False), ++ XA_CARDINAL, 32, PropModeReplace, ++ (unsigned char *)&windowPID, 1); ++ + [self syncDelegateProperties]; + + Atom atm = XInternAtom(_display, "WM_DELETE_WINDOW", False); +@@ -266,6 +305,11 @@ static NSData *makeWindowIcon() { + + [self setWindowIcon]; + ++ /* Poll the host-visible remote CA frame on the AppKit/main runloop. */ ++ _remoteFrameTimer = [[NSTimer scheduledTimerWithTimeInterval: 1.0 / 60.0 ++ target: self selector: @selector(presentRemoteCAFrame:) ++ userInfo: nil repeats: YES] retain]; ++ + return self; + } + +@@ -338,7 +382,6 @@ static NSData *makeWindowIcon() { + } + + + (void) removeDecorationForWindow: (Window) w onDisplay: (Display *) dpy { +- return; + struct { + unsigned long flags; + unsigned long functions; +@@ -358,6 +401,7 @@ static NSData *makeWindowIcon() { + if (!_mapped) { + XMapWindow(_display, _window); + _mapped = YES; ++ _remoteFrameTargetRefresh = 0; + } + } + +@@ -377,15 +421,27 @@ static NSData *makeWindowIcon() { + - (void) syncDelegateProperties { + long mask = KeyPressMask | KeyReleaseMask | ExposureMask | + StructureNotifyMask | EnterWindowMask | LeaveWindowMask | +- ButtonPressMask | ButtonReleaseMask | ButtonMotionMask | +- VisibilityChangeMask | FocusChangeMask | ++ ButtonPressMask | ButtonReleaseMask | ButtonMotionMask | PointerMotionMask | ++ VisibilityChangeMask | FocusChangeMask | PropertyChangeMask | + SubstructureRedirectMask; + +- if ([_delegate acceptsMouseMovedEvents]) { +- mask |= PointerMotionMask; +- } ++ /* Motion also restores a cursor hidden while typing. App-level motion ++ * delivery is still filtered by acceptsMouseMovedEvents in X11Display. */ + XSelectInput(_display, _window, mask); + ++ /* Mouse-transparent windows must not steal crossing events from the ++ * view below them. Filtering dispatched events leaves X11 hit testing ++ * unchanged and can repeatedly hide/recreate tooltip windows. */ ++ if ([_delegate respondsToSelector: @selector(ignoresMouseEvents)]) { ++ if ([_delegate ignoresMouseEvents]) ++ XShapeCombineRectangles(_display, _window, ShapeInput, ++ 0, 0, NULL, 0, ShapeSet, Unsorted); ++ else ++ XShapeCombineMask(_display, _window, ShapeInput, ++ 0, 0, None, ShapeSet); ++ XFlush(_display); ++ } ++ + // TODO: background color + } + +@@ -393,6 +449,28 @@ static NSData *makeWindowIcon() { + // This is essentially dealloc; we release our contexts + // and windows, but unlike dealloc, this method can be called + // several times, so set everything to nil/NULL/0. ++ [_remoteFrameTimer invalidate]; ++ [_remoteFrameTimer release]; ++ _remoteFrameTimer = nil; ++ if (_remoteFrameShm) { ++ _remoteFrameShmDestroy(_remoteFrameShm); ++ _remoteFrameShm = NULL; ++ _remoteFrameShmPumping = NO; ++ } ++ if (_remoteFrameMapping) { ++ munmap(_remoteFrameMapping, _remoteFrameMappingSize); ++ _remoteFrameMapping = NULL; ++ _remoteFrameMappingSize = 0; ++ } ++ ++ if (_remoteFrameWindow) { ++ [(X11Display *) [NSDisplay currentDisplay] setWindow: nil forID: _remoteFrameWindow]; ++ if (_remoteFrameGC) XFreeGC(_display, _remoteFrameGC); ++ XDestroyWindow(_display, _remoteFrameWindow); ++ _remoteFrameWindow = 0; ++ _remoteFrameGC = NULL; ++ _remoteFrameValid = NO; ++ } + [_context release]; + _context = nil; + +@@ -421,6 +499,379 @@ static NSData *makeWindowIcon() { + } + } + ++- (void) setRemoteFrameTimerSlow: (BOOL) slow { ++ if (_remoteFrameTimerSlow == slow) return; ++ _remoteFrameTimerSlow = slow; ++ [_remoteFrameTimer invalidate]; ++ [_remoteFrameTimer release]; ++ _remoteFrameTimer = [[NSTimer scheduledTimerWithTimeInterval: slow ? 0.25 : 1.0 / 60.0 ++ target: self selector: @selector(presentRemoteCAFrame:) ++ userInfo: nil repeats: YES] retain]; ++} ++ ++- (BOOL) presentRemoteCAFrame: (NSTimer *) timer { ++ static unsigned char *pixels; ++ static size_t pixelCapacity; ++ if (!_mapped) return NO; ++ double now = CFAbsoluteTimeGetCurrent(); ++ /* X geometry queries are synchronous round trips. Cache stable targets ++ * across animation ticks, invalidating on native resize/mapping changes. */ ++ if (!_remoteFrameTargetCount || now >= _remoteFrameTargetRefresh) { ++ XWindowAttributes attributes; ++ if (!XGetWindowAttributes(_display, _window, &attributes) || ++ attributes.map_state != IsViewable) ++ return NO; ++ _remoteFrameTargetCount = 1; ++ _remoteFrameTargets[0] = attributes; ++ Window root, parent, *children = NULL; ++ unsigned childCount = 0; ++ if (XQueryTree(_display, _window, &root, &parent, &children, &childCount)) { ++ for (unsigned i = 0; i < childCount && _remoteFrameTargetCount < 17; ++i) ++ if (children[i] != _remoteFrameWindow && ++ XGetWindowAttributes(_display, children[i], ++ &_remoteFrameTargets[_remoteFrameTargetCount])) ++ ++_remoteFrameTargetCount; ++ } ++ if (children) XFree(children); ++ _remoteFrameTargetRefresh = now + 1.0; ++ } ++ XWindowAttributes *frameTargets = _remoteFrameTargets; ++ unsigned frameTargetCount = _remoteFrameTargetCount; ++ DarlingCARemoteFrame before, after; ++ if (_remoteFrameValid && _remoteFrameMapping && ++ (((DarlingCARemoteFrame *)_remoteFrameMapping)->sequence & 1)) { ++ XRaiseWindow(_display, _remoteFrameWindow); ++ XFlush(_display); ++ return YES; ++ } ++ /* darling-lol M58c diagnostic (opt-in via M58_PRES_TRACE): one character ++ * per event appended to /dev/shm/m58c-present- so the consumer's ++ * behavior can be observed from the host without guest logging noise. ++ * t=timer fired, s=no candidate slot, S=slot chosen, s=same sequence, ++ * b=blit performed, v=header validation failed. */ ++ int (*_tputc)(int) = NULL; ++ (void) _tputc; ++ static FILE *tlog; ++ if (getenv("M58_PRES_TRACE")) { ++ if (!tlog) { ++ char tpath[128]; ++ snprintf(tpath, sizeof(tpath), ++ "/dev/shm/m58c-present-%d", (int) getpid()); ++ tlog = fopen(tpath, "a"); ++ } ++ if (tlog) { ++ fputc('t', tlog); ++ fflush(tlog); ++ } ++ } ++ /* darling-lol M58c: the publisher side now writes one slot file per ++ * process (/dev/shm/darling-ca-frame--) because concurrent ++ * publishers corrupt a shared slot's seqlock. Several processes publish ++ * same-size surfaces (measured: the browser's own surface is all black ++ * while the GPU's compositor output carries the real frame), so scan ++ * every publisher's slot and display the most recently updated one that ++ * actually contains pixels; an all-black frame is only used when nothing ++ * else is available. */ ++ char path[128], bestPath[128], bestBlackPath[128]; ++ /* A window no publisher has a slot for (a Metal game window never gets one) used to rescan /dev/shm on every ++ * 60 Hz tick: opendir, readdir and stat/open/pread/close per slot file on the main thread. */ ++ if (!_remoteFrameMapping && now < _remoteFrameSelectionRefresh) ++ return _remoteFrameValid; ++ if (_remoteFrameMapping && now < _remoteFrameSelectionRefresh) { ++ snprintf(path, sizeof(path), "%s", _remoteFrameMappingPath); ++ goto selectedFrame; ++ } ++ _remoteFrameSelectionRefresh = now + 0.25; ++ bestPath[0] = bestBlackPath[0] = '\0'; ++ uint64_t bestMtime = 0, bestBlackMtime = 0; ++ BOOL bestSoftware = NO; ++ ++ char prefix[32]; ++ snprintf(prefix, sizeof(prefix), "darling-ca-frame-%u-", getuid()); ++ size_t prefixLen = strlen(prefix); ++ DIR *dir = opendir("/dev/shm"); ++ if (dir) { ++ struct dirent *ent; ++ while ((ent = readdir(dir)) != NULL) { ++ if (strncmp(ent->d_name, prefix, prefixLen) != 0) ++ continue; ++ char candidate[128]; ++ snprintf(candidate, sizeof(candidate), "/dev/shm/%s", ++ ent->d_name); ++ struct stat st; ++ if (stat(candidate, &st) != 0) ++ continue; ++ uint64_t updateTime = (uint64_t)st.st_mtimespec.tv_sec * 1000000000u + ++ st.st_mtimespec.tv_nsec; ++ DarlingCARemoteFrame head = {0}; ++ int cfd = open(candidate, O_RDONLY); ++ if (cfd < 0) ++ continue; ++ BOOL ok = (pread(cfd, &head, sizeof(head), 0) == sizeof(head) && ++ (head.magic == DARLING_CA_FRAME_MAGIC || head.magic == DARLING_CA_FRAME_BUFFERED_MAGIC) && ++ !(head.sequence & 1) && ++ head.width && head.height && ++ head.bytesPerRow >= head.width * 4 && ++ head.dataSize == (uint64_t)head.bytesPerRow * head.height && ++ head.dataSize <= 64u * 1024u * 1024u); ++ close(cfd); ++ BOOL matchesWindow = NO; ++ for (unsigned i = 0; i < frameTargetCount; ++i) ++ if (head.width == (unsigned)frameTargets[i].width && ++ head.height == (unsigned)frameTargets[i].height) ++ matchesWindow = YES; ++ if (!ok || !matchesWindow) ++ continue; ++ ++ /* Peek the top rows to distinguish a rendered frame from an ++ * empty (all-zero) surface of the same size. */ ++ BOOL hasPixels = NO; ++ size_t peekRows = head.height < 40 ? (size_t) head.height : 40; ++ size_t peekLen = peekRows * head.bytesPerRow; ++ unsigned char peek[40 * 4096]; ++ if (peekLen > sizeof(peek)) ++ peekLen = sizeof(peek); ++ int pfd = open(candidate, O_RDONLY); ++ if (pfd >= 0) { ++ if (pread(pfd, peek, peekLen, sizeof(head) + framePayloadOffset(head.magic, head.pixelFormat)) == ++ (ssize_t) peekLen) { ++ for (size_t i = 0; i + 3 < peekLen; i += 4) { ++ if (peek[i] | peek[i + 1] | peek[i + 2]) { ++ hasPixels = YES; ++ break; ++ } ++ } ++ } ++ close(pfd); ++ } ++ ++ if (hasPixels) { ++ BOOL software = (head.pixelFormat & 0x80000000u) != 0; ++ if ((software && !bestSoftware) || ++ (software == bestSoftware && updateTime > bestMtime)) { ++ bestSoftware = software; ++ bestMtime = updateTime; ++ snprintf(bestPath, sizeof(bestPath), "%s", candidate); ++ } ++ } else if (updateTime > bestBlackMtime) { ++ bestBlackMtime = updateTime; ++ snprintf(bestBlackPath, sizeof(bestBlackPath), "%s", candidate); ++ } ++ } ++ closedir(dir); ++ } ++ ++ /* Keep the software compositor owner across its other window updates. ++ * Otherwise a temporary size mismatch selects stale browser readback. */ ++ if (_remoteFrameSoftwarePublisher && _remoteFrameMapping && !bestSoftware) { ++ const char *pidText = strrchr(_remoteFrameMappingPath, '-'); ++ int publisher = pidText ? atoi(pidText + 1) : 0; ++ if (publisher > 0 && (kill(publisher, 0) == 0 || errno != ESRCH)) { ++ snprintf(path, sizeof(path), "%s", _remoteFrameMappingPath); ++ goto selectedFrame; ++ } ++ } ++ if (bestPath[0] != '\0') ++ snprintf(path, sizeof(path), "%s", bestPath); ++ else if (bestBlackPath[0] != '\0') ++ snprintf(path, sizeof(path), "%s", bestBlackPath); ++ else { ++ /* A writer's odd sequence is a temporary gap, not a hidden window. ++ * Keep the last complete frame instead of exposing local GL pixels. */ ++ if (tlog) { fputc('S', tlog); } ++ if (!_remoteFrameMapping) _remoteFrameSelectionRefresh = now + 0.5; ++ return _remoteFrameValid; ++ } ++ ++ if (tlog) { fputc('S', tlog); } ++ ++ /* Only the presentation child owns remote pixels. GL swaps in sibling ++ * views cannot clear it, so unchanged frames need no repeated pixel copy. */ ++selectedFrame: ++ if (!_remoteFrameMapping || strcmp(_remoteFrameMappingPath, path)) { ++ int fd = open(path, O_RDONLY); ++ if (fd < 0) return _remoteFrameValid; ++ struct stat st; ++ if (fstat(fd, &st) || st.st_size < sizeof(before) || ++ st.st_size > sizeof(before) + 64u * 1024u * 1024u) { ++ close(fd); return _remoteFrameValid; ++ } ++ void *mapping = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd, 0); ++ close(fd); ++ if (mapping == MAP_FAILED) return _remoteFrameValid; ++ if (_remoteFrameMapping) munmap(_remoteFrameMapping, _remoteFrameMappingSize); ++ _remoteFrameMapping = mapping; ++ _remoteFrameSoftwarePublisher = NO; ++ _remoteFrameMappingSize = st.st_size; ++ snprintf(_remoteFrameMappingPath, sizeof(_remoteFrameMappingPath), "%s", path); ++ } ++ memcpy(&before, _remoteFrameMapping, sizeof(before)); ++ __sync_synchronize(); ++ if ((before.magic != DARLING_CA_FRAME_MAGIC && before.magic != DARLING_CA_FRAME_BUFFERED_MAGIC) || (before.sequence & 1) || ++ !before.width || !before.height || before.bytesPerRow < before.width * 4 || ++ before.dataSize != (uint64_t)before.bytesPerRow * before.height || ++ framePayloadOffset(before.magic, before.pixelFormat) > _remoteFrameMappingSize - sizeof(before) || ++ before.dataSize > _remoteFrameMappingSize - sizeof(before) - framePayloadOffset(before.magic, before.pixelFormat)) ++ return _remoteFrameValid; ++ if (before.pixelFormat & 0x80000000u) _remoteFrameSoftwarePublisher = YES; ++ BOOL matchesTarget = NO; ++ for (unsigned i = 0; i < frameTargetCount; ++i) ++ if (before.width == (unsigned)frameTargets[i].width && ++ before.height == (unsigned)frameTargets[i].height) matchesTarget = YES; ++ /* A GPU process may publish several windows into its slot. Never resize ++ * our presentation child to another window's intervening surface. */ ++ if (!matchesTarget) return _remoteFrameValid; ++ int frameX = 0, frameY = 0; ++ for (unsigned i = 0; i < frameTargetCount; ++i) { ++ if (before.width == (unsigned)frameTargets[i].width && ++ before.height == (unsigned)frameTargets[i].height) { ++ if (i) { frameX = frameTargets[i].x; frameY = frameTargets[i].y; } ++ break; ++ } ++ } ++ if (_remoteFrameWindow) ++ XMoveWindow(_display, _remoteFrameWindow, frameX, frameY); ++ if (_remoteFrameShmPumping && _remoteFrameWidth == before.width && ++ _remoteFrameHeight == before.height) { ++ _remoteFrameShmStart(_remoteFrameShm, _remoteFrameWindow, _remoteFrameGC, path); ++ if (!_remoteFrameValid) ++ _remoteFrameShmPut(_remoteFrameShm, _remoteFrameWindow, _remoteFrameGC, NULL, 0); ++ XRaiseWindow(_display, _remoteFrameWindow); ++ XFlush(_display); ++ _remoteFrameSequence = before.sequence; ++ snprintf(_remoteFramePath, sizeof(_remoteFramePath), "%s", path); ++ _remoteFrameValid = YES; ++ return YES; ++ } ++ if (_remoteFrameValid && _remoteFrameWindow && ++ _remoteFrameSequence == before.sequence && ++ _remoteFrameWidth == before.width && _remoteFrameHeight == before.height && ++ strcmp(_remoteFramePath, path) == 0) { ++ XRaiseWindow(_display, _remoteFrameWindow); ++ XFlush(_display); ++ return YES; ++ } ++ if (pixelCapacity < before.dataSize) { ++ unsigned char *newPixels = realloc(pixels, before.dataSize); ++ if (!newPixels) { ++ return _remoteFrameValid; ++ } ++ pixels = newPixels; ++ pixelCapacity = before.dataSize; ++ } ++ memcpy(pixels, (char *)_remoteFrameMapping + sizeof(before) + framePayloadOffset(before.magic, before.pixelFormat), before.dataSize); ++ __sync_synchronize(); ++ memcpy(&after, _remoteFrameMapping, sizeof(after)); ++ if (after.sequence != before.sequence || (after.sequence & 1)) { ++ if (tlog) { fputc('T', tlog); } ++ return _remoteFrameValid; ++ } ++ ++ XImage *image = XCreateImage(_display, _visualInfo->visual, ++ _visualInfo->depth, ZPixmap, 0, (char *)pixels, before.width, ++ before.height, 32, before.bytesPerRow); ++ if (!image) ++ return _remoteFrameValid; ++ ++ if (!_remoteFrameWindow) { ++ int shapeMajor, shapeMinor; ++ if (!XShapeQueryVersion(_display, &shapeMajor, &shapeMinor) || ++ shapeMajor < 1 || (shapeMajor == 1 && shapeMinor < 1)) { ++ image->data = NULL; ++ XDestroyImage(image); ++ return _remoteFrameValid; ++ } ++ XSetWindowAttributes attrs = {0}; ++ attrs.background_pixmap = None; ++ attrs.event_mask = ExposureMask; ++ _remoteFrameWindow = XCreateWindow(_display, _window, frameX, frameY, ++ before.width, before.height, 0, CopyFromParent, InputOutput, ++ CopyFromParent, CWBackPixmap | CWEventMask, &attrs); ++ /* An empty input shape lets clicks and motion reach native views. */ ++ XShapeCombineRectangles(_display, _remoteFrameWindow, ShapeInput, ++ 0, 0, NULL, 0, ShapeSet, Unsorted); ++ [(X11Display *) [NSDisplay currentDisplay] setWindow: self forID: _remoteFrameWindow]; ++ _remoteFrameGC = XCreateGC(_display, _remoteFrameWindow, 0, NULL); ++ } ++ if (_remoteFrameWidth != before.width || _remoteFrameHeight != before.height) { ++ if (_remoteFrameShm) { ++ _remoteFrameShmDestroy(_remoteFrameShm); ++ _remoteFrameShm = NULL; ++ _remoteFrameShmPumping = NO; ++ [self setRemoteFrameTimerSlow: NO]; ++ } ++ XResizeWindow(_display, _remoteFrameWindow, before.width, before.height); ++ _remoteFrameWidth = before.width; ++ _remoteFrameHeight = before.height; ++ } ++ XMapRaised(_display, _remoteFrameWindow); ++ typedef void *(*CreateShm)(Display *, Visual *, unsigned, unsigned, unsigned); ++ typedef int (*PutShm)(void *, Drawable, GC, const void *, size_t); ++ static CreateShm createShm; ++ static PutShm putShm; ++ static void (*destroyShm)(void *); ++ static int (*startShm)(void *, Drawable, GC, const char *); ++ static BOOL checkedShm; ++ if (!checkedShm) { ++ checkedShm = YES; ++ const char *transportPath = getenv("DARLING_XSHM_LIBRARY"); ++ const char *shmOption = getenv("RCLOL_XSHM"); ++ void *transport = (shmOption && shmOption[0] == '0') ? NULL : ++ dlopen(transportPath ? transportPath : "/usr/lib/native/libDarlingXShm.dylib", ++ RTLD_NOW | RTLD_LOCAL); ++ void *(*getCreate)(void) = transport ? dlsym(transport, "DarlingXShmGetCreate") : NULL; ++ void *(*getPut)(void) = transport ? dlsym(transport, "DarlingXShmGetPut") : NULL; ++ void *(*getDestroy)(void) = transport ? dlsym(transport, "DarlingXShmGetDestroy") : NULL; ++ void *(*getStart)(void) = transport ? dlsym(transport, "DarlingXShmGetStart") : NULL; ++ if (getCreate && getPut && getDestroy) { ++ createShm = (CreateShm)getCreate(); ++ putShm = (PutShm)getPut(); ++ destroyShm = (void (*)(void *))getDestroy(); ++ if (getStart) startShm = (int (*)(void *, Drawable, GC, const char *))getStart(); ++ } ++ } ++ if (!_remoteFrameShm && createShm && putShm && destroyShm) { ++ /* The independent connection must see this client's child creation ++ * before issuing its first upload or creating its private GC. */ ++ XSync(_display, False); ++ _remoteFrameShm = createShm(_display, _visualInfo->visual, ++ _visualInfo->depth, before.width, before.height); ++ _remoteFrameShmDestroy = destroyShm; ++ _remoteFrameShmPut = putShm; ++ _remoteFrameShmStart = startShm; ++ if (getenv("RCLOL_XSHM_TRACE")) { ++ extern int __simple_printf(const char *, ...); ++ __simple_printf("REMOTE_XSHM width=%u height=%u active=%d\n", ++ before.width, before.height, _remoteFrameShm != NULL); ++ } ++ } ++ if (!_remoteFrameShm || !putShm(_remoteFrameShm, _remoteFrameWindow, ++ _remoteFrameGC, pixels, before.bytesPerRow)) { ++ XPutImage(_display, _remoteFrameWindow, _remoteFrameGC, image, 0, 0, 0, 0, ++ before.width, before.height); ++ XFlush(_display); ++ } ++ if (_remoteFrameShm && startShm && !_remoteFrameShmPumping) { ++ _remoteFrameShmPumping = startShm(_remoteFrameShm, _remoteFrameWindow, _remoteFrameGC, path); ++ if (_remoteFrameShmPumping) [self setRemoteFrameTimerSlow: YES]; ++ } ++ _remoteFrameSequence = before.sequence; ++ snprintf(_remoteFramePath, sizeof(_remoteFramePath), "%s", path); ++ _remoteFrameValid = YES; ++ image->data = NULL; /* pixels is reused by the next frame */ ++ XDestroyImage(image); ++ if (tlog) { fputc('b', tlog); fflush(tlog); } ++ return YES; ++} ++ ++- (BOOL) handleRemoteFrameExpose: (Window) window { ++ if (!window || window != _remoteFrameWindow) ++ return NO; ++ _remoteFrameValid = NO; ++ [self presentRemoteCAFrame: nil]; ++ return YES; ++} ++ + - (Window) windowHandle { + return _window; + } +@@ -478,6 +929,15 @@ static NSData *makeWindowIcon() { + frame.size.width, frame.size.height); + [self invalidateContextWithNewSize: frame.size]; + _frame = frame; ++ // Nonresizable windows can be created at 1x1 and enlarged before display. ++ // Refresh the constraints so a managing compositor sees the current size. ++ if (!(_styleMask & NSWindowStyleMaskResizable)) { ++ XSizeHints hints = {0}; ++ hints.flags = PMinSize | PMaxSize; ++ hints.min_width = hints.max_width = frame.size.width; ++ hints.min_height = hints.max_height = frame.size.height; ++ XSetWMSizeHints(_display, _window, &hints, XA_WM_NORMAL_HINTS); ++ } + } + + - (void) setHasShadow: (BOOL) value { +@@ -559,7 +1019,29 @@ static NSData *makeWindowIcon() { + XRaiseWindow(_display, _window); + } + ++/* darling-lol: AppKit asks every window whether it is miniaturized on every event poll (NSWindow ++ * _makeSureIsOnAScreen), and each answer was a synchronous X round trip (XGetWindowProperty): about 13 blocking waits ++ * per game frame. Reuse the answer for 100 ms per window. */ ++#include ++#define RCLOL_MINI_SLOTS 32 ++static struct { Window window; uint64_t stamp; BOOL value; } rclolMini[RCLOL_MINI_SLOTS]; ++ + - (BOOL) isMiniaturized { ++ static mach_timebase_info_data_t tb; ++ if (tb.denom == 0) mach_timebase_info(&tb); ++ uint64_t now = mach_absolute_time(); ++ unsigned slot = (unsigned) (_window % RCLOL_MINI_SLOTS); ++ if (rclolMini[slot].window == _window && rclolMini[slot].stamp != 0 && ++ (now - rclolMini[slot].stamp) * tb.numer / tb.denom < 100000000ULL) ++ return rclolMini[slot].value; ++ BOOL value = [self rclolQueryMiniaturized]; ++ rclolMini[slot].window = _window; ++ rclolMini[slot].value = value; ++ rclolMini[slot].stamp = now; ++ return value; ++} ++ ++- (BOOL) rclolQueryMiniaturized { + Atom wmState = XInternAtom(_display, "_NET_WM_STATE", FALSE); + Atom hiddenState = XInternAtom(_display, "_NET_WM_STATE_HIDDEN", FALSE); + +@@ -615,6 +1097,12 @@ static NSData *makeWindowIcon() { + } + + - (void) openGLFlushBuffer { ++ /* A remote compositor surface owns this drawable while it is present. ++ * Swapping the local Onyx surface would erase it until the next timer ++ * tick. Keep local presentation for windows without a usable remote frame. */ ++ if ([self presentRemoteCAFrame: nil]) ++ return; ++ + CGLError error; + + CGLContextObj prevContext = CGLGetCurrentContext(); +@@ -666,6 +1154,7 @@ static int ignoreBadWindow(Display *display, XErrorEvent *errorEvent) { + } + + - (void) frameChanged { ++ _remoteFrameTargetRefresh = 0; + XErrorHandler previousHandler = XSetErrorHandler(ignoreBadWindow); + @try { + Window root, parent; +diff --git a/AppKit/include/AppKit/NSAccessibilityConstants.h b/AppKit/include/AppKit/NSAccessibilityConstants.h +index 282312de..03c75b2b 100644 +--- a/AppKit/include/AppKit/NSAccessibilityConstants.h ++++ b/AppKit/include/AppKit/NSAccessibilityConstants.h +@@ -9,6 +9,7 @@ APPKIT_EXPORT NSString *const NSAccessibilityContentsAttribute; + APPKIT_EXPORT NSString *const NSAccessibilityDescriptionAttribute; + APPKIT_EXPORT NSString *const NSAccessibilityEnabledAttribute; + APPKIT_EXPORT NSString *const NSAccessibilityFocusedAttribute; ++APPKIT_EXPORT NSString *const NSAccessibilityRequiredAttribute; + APPKIT_EXPORT NSString *const NSAccessibilityHelpAttribute; + APPKIT_EXPORT NSString *const NSAccessibilityMaxValueAttribute; + APPKIT_EXPORT NSString *const NSAccessibilityMinValueAttribute; +diff --git a/AppKit/include/AppKit/NSApplication.h b/AppKit/include/AppKit/NSApplication.h +index a13138ae..32b97211 100644 +--- a/AppKit/include/AppKit/NSApplication.h ++++ b/AppKit/include/AppKit/NSApplication.h +@@ -140,6 +140,7 @@ typedef NS_OPTIONS(NSUInteger, NSApplicationPresentationOptions) { + NSMutableArray *_orderedWindows; // get rid of + NSTimer *_attentionTimer; + NSApplicationPresentationOptions _presentationOptions; ++ BOOL _finishedLaunching; + } + + @property(strong) NSMenu *helpMenu; +@@ -168,6 +169,7 @@ typedef NS_OPTIONS(NSUInteger, NSApplicationPresentationOptions) { + - (BOOL) isActive; + - (BOOL) isHidden; + - (BOOL) isRunning; ++- (BOOL) isFullKeyboardAccessEnabled; + + - (NSWindow *) makeWindowsPerform: (SEL) selector inOrder: (BOOL) inOrder; + - (void) miniaturizeAll: sender; +diff --git a/AppKit/include/AppKit/NSColor.h b/AppKit/include/AppKit/NSColor.h +index b5ae3a65..6d555f60 100644 +--- a/AppKit/include/AppKit/NSColor.h ++++ b/AppKit/include/AppKit/NSColor.h +@@ -29,6 +29,14 @@ typedef NSString *NSColorSpaceName; + + @class NSImage; + @class NSPasteboard; ++@class NSColorSpace; ++ ++enum { ++ NSDefaultControlTint = 0, ++ NSBlueControlTint = 1, ++ NSGraphiteControlTint = 6, ++ NSClearControlTint = 7, ++}; + + @interface NSColor : NSObject { + NSColorListName _catalogName; +@@ -131,6 +139,7 @@ typedef NSString *NSColorSpaceName; + + (NSColor *) alternateSelectedControlColor; + + (NSColor *) alternateSelectedControlTextColor; + + (NSColor *) controlColor; +++ (NSUInteger) currentControlTint; + + (NSColor *) secondarySelectedControlColor; + + (NSColor *) selectedControlColor; + + (NSColor *) controlTextColor; +@@ -271,6 +280,7 @@ typedef NSString *NSColorSpaceName; + - (NSColor *) colorUsingColorSpaceName: (NSString *) colorSpace; + - (NSColor *) colorUsingColorSpaceName: (NSString *) colorSpace + device: (NSDictionary *) device; ++- (NSColor *) colorUsingColorSpace: (NSColorSpace *) space; + + - (NSColor *) blendedColorWithFraction: (CGFloat) fraction + ofColor: (NSColor *) color; +diff --git a/AppKit/include/AppKit/NSDisplay.h b/AppKit/include/AppKit/NSDisplay.h +index 8860a087..aec7116d 100644 +--- a/AppKit/include/AppKit/NSDisplay.h ++++ b/AppKit/include/AppKit/NSDisplay.h +@@ -22,6 +22,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + #import + #import + #import ++#include + + @class NSColor, NSDraggingManager, NSPrintInfo, NSView, NSSavePanel, + NSOpenPanel, NSFontTypeface, CGWindow; +@@ -31,6 +32,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + } + + + (NSDisplay *) currentDisplay; ++- (BOOL) activateProcessIdentifier: (pid_t) processIdentifier; + + - (NSArray *) screens; + +@@ -95,6 +97,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + + - (NSPoint) mouseLocation; + - (NSUInteger) currentModifierFlags; ++- (NSUInteger) pressedMouseButtons; + + - (NSArray *) orderedWindowNumbers; + +diff --git a/AppKit/include/AppKit/NSEvent.h b/AppKit/include/AppKit/NSEvent.h +index 0677f2e7..d16080f4 100644 +--- a/AppKit/include/AppKit/NSEvent.h ++++ b/AppKit/include/AppKit/NSEvent.h +@@ -83,6 +83,8 @@ typedef NS_ENUM(NSUInteger, NSEventType) { + + NSEventTypeOtherMouseUp = 26, + NSOtherMouseUp = 26, ++ NSEventTypeOtherMouseDragged = 27, ++ NSOtherMouseDragged = 27, + + NSAppKitSystem = 100, + +@@ -212,6 +214,7 @@ enum { NSApplicationActivated = 0, NSApplicationDeactivated = 1 }; + + + (NSPoint) mouseLocation; + + (NSEventModifierFlags) modifierFlags; +++ (NSUInteger) pressedMouseButtons; + + - (instancetype) initWithType: (NSEventType) type + location: (NSPoint) location +diff --git a/AppKit/include/AppKit/NSFont.h b/AppKit/include/AppKit/NSFont.h +index 3215013d..9f8bceb6 100644 +--- a/AppKit/include/AppKit/NSFont.h ++++ b/AppKit/include/AppKit/NSFont.h +@@ -79,6 +79,7 @@ typedef enum { + + (NSFont *) messageFontOfSize: (CGFloat) size; + + (NSFont *) paletteFontOfSize: (CGFloat) size; + + (NSFont *) systemFontOfSize: (CGFloat) size; +++ (NSFont *) systemFontOfSize: (CGFloat) size weight: (CGFloat) weight; + + (NSFont *) titleBarFontOfSize: (CGFloat) size; + + (NSFont *) toolTipsFontOfSize: (CGFloat) size; + + (NSFont *) userFontOfSize: (CGFloat) size; +@@ -98,6 +99,7 @@ typedef enum { + + (NSArray *) preferredFontNames; + + (void) setPreferredFontNames: (NSArray *) fontNames; + ++- (CTFontRef) _coreTextFont; + - (CGFloat) pointSize; + - (NSString *) fontName; + - (const CGFloat *) matrix; +@@ -113,6 +115,9 @@ typedef enum { + - (NSFont *) screenFont; + - (NSFont *) screenFontWithRenderingMode: (NSFontRenderingMode) mode; + ++/* CoreText bridge used when an NSFont is accepted as a CTFontRef. */ ++- (CGFontRef) copyGraphicsFont; ++ + - (NSRect) boundingRectForFont; + - (NSRect) boundingRectForGlyph: (NSGlyph) glyph; + +diff --git a/AppKit/include/AppKit/NSMenu.h b/AppKit/include/AppKit/NSMenu.h +index aa776e1a..82873d60 100644 +--- a/AppKit/include/AppKit/NSMenu.h ++++ b/AppKit/include/AppKit/NSMenu.h +@@ -99,6 +99,11 @@ APPKIT_EXPORT const NSNotificationName NSMenuDidEndTrackingNotification; + - (void) setDelegate: (id) object; + - (id) delegate; + ++/* Private AppKit API. Chromium/Electron calls this when building menu ++ * padding metadata; Cocotron has no such layout, so it is a no-op setter ++ * (without it the call raises NSException and the process terminates). */ ++- (void) _setHasPadding: (BOOL) padding onEdge: (NSInteger) edge; ++ + @end + + @interface NSObject (NSMenu_validateItem) +diff --git a/AppKit/include/AppKit/NSMenuItem.h b/AppKit/include/AppKit/NSMenuItem.h +index 5231496d..6c5eac59 100644 +--- a/AppKit/include/AppKit/NSMenuItem.h ++++ b/AppKit/include/AppKit/NSMenuItem.h +@@ -45,6 +45,8 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + NSImage *_onStateImage; + NSImage *_offStateImage; + NSImage *_mixedStateImage; ++ NSView *_view; ++ NSString *_toolTip; /* darling-lol M42b: -setToolTip: (Electron menus) */ + BOOL _alternate; + BOOL _allowsKeyEquivalentWhenHidden; + NSUserInterfaceItemIdentifier _identifier; +@@ -72,6 +74,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + - (NSString *) keyEquivalent; + - (unsigned) keyEquivalentModifierMask; + - (NSImage *) image; ++- (NSView *) view; + - (NSImage *) onStateImage; + - (NSImage *) offStateImage; + - (NSImage *) mixedStateImage; +@@ -86,6 +89,8 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + - (BOOL) isHidden; + + - (void) setTitle: (NSString *) title; ++- (NSString *) toolTip; ++- (void) setToolTip: (NSString *) toolTip; + - (void) setAttributedTitle: (NSAttributedString *) title; + - (void) setTitleWithMnemonic: (NSString *) mnemonic; + - (void) setMnemonicLocation: (unsigned) location; +@@ -97,6 +102,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + - (void) setKeyEquivalent: (NSString *) keyEquivalent; + - (void) setKeyEquivalentModifierMask: (unsigned) mask; + - (void) setImage: (NSImage *) image; ++- (void) setView: (NSView *) view; + - (void) setOnStateImage: (NSImage *) image; + - (void) setOffStateImage: (NSImage *) image; + - (void) setMixedStateImage: (NSImage *) image; +diff --git a/AppKit/include/AppKit/NSNib.h b/AppKit/include/AppKit/NSNib.h +index ff3f2d6f..40729e09 100644 +--- a/AppKit/include/AppKit/NSNib.h ++++ b/AppKit/include/AppKit/NSNib.h +@@ -42,5 +42,6 @@ APPKIT_EXPORT NSString *const NSNibTopLevelObjects; + + - (BOOL) instantiateNibWithExternalNameTable: (NSDictionary *) nameTable; + - (BOOL) instantiateNibWithOwner: owner topLevelObjects: (NSArray **) objects; ++- (BOOL) instantiateWithOwner: (id) owner topLevelObjects: (NSArray **) objects; + + @end +diff --git a/AppKit/include/AppKit/NSResponder.h b/AppKit/include/AppKit/NSResponder.h +index 11dfac87..b595625e 100644 +--- a/AppKit/include/AppKit/NSResponder.h ++++ b/AppKit/include/AppKit/NSResponder.h +@@ -24,9 +24,12 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + + @interface NSResponder : NSObject { + id _nextResponder; ++ id _touchBar; + } + + - (NSResponder *) nextResponder; ++- (id) touchBar; ++- (void) setTouchBar: (id) touchBar; + + - (NSMenu *) menu; + - (NSUndoManager *) undoManager; +@@ -69,6 +72,9 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + - (void) mouseExited: (NSEvent *) event; + - (void) mouseDragged: (NSEvent *) event; + ++- (void) otherMouseDown: (NSEvent *) event; ++- (void) otherMouseUp: (NSEvent *) event; ++- (void) otherMouseDragged: (NSEvent *) event; + - (void) rightMouseUp: (NSEvent *) event; + - (void) rightMouseDown: (NSEvent *) event; + - (void) rightMouseDragged: (NSEvent *) event; +diff --git a/AppKit/include/AppKit/NSRunningApplication.h b/AppKit/include/AppKit/NSRunningApplication.h +index c577764b..d8bb21cc 100644 +--- a/AppKit/include/AppKit/NSRunningApplication.h ++++ b/AppKit/include/AppKit/NSRunningApplication.h +@@ -18,6 +18,7 @@ + */ + + #import ++#include + + typedef enum { + NSApplicationActivationPolicyRegular, +@@ -26,7 +27,14 @@ typedef enum { + } NSApplicationActivationPolicy; + + @interface NSRunningApplication : NSObject ++{ ++ pid_t _processIdentifier; ++} + + + (NSArray *) runningApplicationsWithBundleIdentifier: (NSString *) bundleIdentifier; +++ (NSRunningApplication *) runningApplicationWithProcessIdentifier: (pid_t) processIdentifier; ++- (pid_t) processIdentifier; ++- (BOOL) isTerminated; ++- (BOOL) activateWithOptions: (NSUInteger) options; + + @end +diff --git a/AppKit/include/AppKit/NSScrubberItemView.h b/AppKit/include/AppKit/NSScrubberItemView.h +index 40042c89..37c1f4b6 100644 +--- a/AppKit/include/AppKit/NSScrubberItemView.h ++++ b/AppKit/include/AppKit/NSScrubberItemView.h +@@ -27,3 +27,6 @@ + + @interface NSScrubberTextItemView : NSScrubberItemView + @end ++ ++@interface NSScrubberImageItemView : NSScrubberItemView ++@end +diff --git a/AppKit/include/AppKit/NSSpeechSynthesizer.h b/AppKit/include/AppKit/NSSpeechSynthesizer.h +index d8a6f6f1..a1c37d46 100644 +--- a/AppKit/include/AppKit/NSSpeechSynthesizer.h ++++ b/AppKit/include/AppKit/NSSpeechSynthesizer.h +@@ -38,7 +38,12 @@ APPKIT_EXPORT NSSpeechPropertyKey const NSSpeechRateProperty; + APPKIT_EXPORT NSSpeechPropertyKey const NSSpeechPitchBaseProperty; + APPKIT_EXPORT NSSpeechPropertyKey const NSSpeechVolumeProperty; + ++@class NSArray, NSDictionary; ++ + @interface NSSpeechSynthesizer : NSObject +++ (NSArray *)availableVoices; +++ (NSString *)defaultVoice; +++ (NSDictionary *)attributesForVoice:(NSString *)voice; + @end + + @protocol NSSpeechSynthesizerDelegate +diff --git a/AppKit/include/AppKit/NSSpellChecker.h b/AppKit/include/AppKit/NSSpellChecker.h +index aaf2083b..41386be3 100644 +--- a/AppKit/include/AppKit/NSSpellChecker.h ++++ b/AppKit/include/AppKit/NSSpellChecker.h +@@ -124,7 +124,7 @@ typedef NSInteger NSCorrectionResponse; + orthography: (NSOrthography **) orthography + wordCount: (NSInteger *) wordCount; + +-#ifdef NS_BLOCKS ++#if defined(NS_BLOCKS) || defined(__BLOCKS__) + - (NSInteger) requestCheckingOfString: (NSString *) stringToCheck + range: (NSRange) range + types: (NSTextCheckingTypes) checkingTypes +diff --git a/AppKit/include/AppKit/NSTextInput.h b/AppKit/include/AppKit/NSTextInput.h +index c9dfd7c7..413f5ac1 100644 +--- a/AppKit/include/AppKit/NSTextInput.h ++++ b/AppKit/include/AppKit/NSTextInput.h +@@ -23,7 +23,17 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + + APPKIT_EXPORT NSString *const NSTextInputContextKeyboardSelectionDidChangeNotification; + +-@interface NSTextInputContext : NSObject ++@class NSEvent; ++@interface NSTextInputContext : NSObject { ++ id _client; ++} ++- (instancetype)initWithClient:(id)client; ++- (id)client; ++- (BOOL)handleEvent:(NSEvent *)event; ++- (void)activate; ++- (void)deactivate; ++- (void)discardMarkedText; ++- (void)invalidateCharacterCoordinates; + + @end + +diff --git a/AppKit/include/AppKit/NSTouchBarItem.h b/AppKit/include/AppKit/NSTouchBarItem.h +index 956712e1..528e7418 100644 +--- a/AppKit/include/AppKit/NSTouchBarItem.h ++++ b/AppKit/include/AppKit/NSTouchBarItem.h +@@ -30,6 +30,8 @@ APPKIT_EXPORT NSTouchBarItemIdentifier const + NSTouchBarItemIdentifierFlexibleSpace; + APPKIT_EXPORT NSTouchBarItemIdentifier const + NSTouchBarItemIdentifierOtherItemsProxy; ++APPKIT_EXPORT NSTouchBarItemIdentifier const ++ NSTouchBarItemIdentifierCandidateList; + + @interface NSTouchBarItem : NSObject + +diff --git a/AppKit/include/AppKit/NSView.h b/AppKit/include/AppKit/NSView.h +index edb45627..581991d1 100644 +--- a/AppKit/include/AppKit/NSView.h ++++ b/AppKit/include/AppKit/NSView.h +@@ -139,6 +139,12 @@ APPKIT_EXPORT const NSViewFullScreenModeOptionKey NSFullScreenModeApplicationPre + NSLayoutPriority _verticalContentCompressionResistancePriority; + + NSAppearance *_appearance; ++ ++ /* darling-lol M57: modern NSView properties Chromium reads. */ ++ NSUInteger _allowedTouchTypes; ++ BOOL _wantsUpdateLayer; ++ BOOL _canDrawSubviewsIntoLayer; ++ BOOL _wantsBestResolutionOpenGLSurface; + } + + @property(class, readonly) BOOL requiresConstraintBasedLayout; +@@ -213,6 +219,18 @@ APPKIT_EXPORT const NSViewFullScreenModeOptionKey NSFullScreenModeApplicationPre + + - (NSPoint) convertPoint: (NSPoint) point fromView: (NSView *) viewOrNil; + - (NSPoint) convertPoint: (NSPoint) point toView: (NSView *) viewOrNil; ++- (NSUInteger) allowedTouchTypes; ++- (void) setAllowedTouchTypes: (NSUInteger) value; ++- (BOOL) wantsUpdateLayer; ++- (void) setWantsUpdateLayer: (BOOL) value; ++- (BOOL) canDrawSubviewsIntoLayer; ++- (void) setCanDrawSubviewsIntoLayer: (BOOL) value; ++- (BOOL) wantsBestResolutionOpenGLSurface; ++- (void) setWantsBestResolutionOpenGLSurface: (BOOL) value; ++- (NSPoint) convertPointFromBacking: (NSPoint) point; ++- (NSPoint) convertPointToBacking: (NSPoint) point; ++- (NSRect) convertRectFromBacking: (NSRect) rect; ++- (NSRect) convertRectToBacking: (NSRect) rect; + - (NSSize) convertSize: (NSSize) size fromView: (NSView *) viewOrNil; + - (NSSize) convertSize: (NSSize) size toView: (NSView *) viewOrNil; + - (NSRect) convertRect: (NSRect) rect fromView: (NSView *) viewOrNil; +diff --git a/AppKit/include/AppKit/NSWindow.h b/AppKit/include/AppKit/NSWindow.h +index a83c4d75..575c3581 100644 +--- a/AppKit/include/AppKit/NSWindow.h ++++ b/AppKit/include/AppKit/NSWindow.h +@@ -188,6 +188,16 @@ APPKIT_EXPORT const NSNotificationName NSWindowDidExposeNotification; + int _cursorRectsDisabled; + int _flushDisabled; + ++ /* darling-lol M57: modern AppKit window properties Chromium reads. */ ++ BOOL _titlebarAppearsTransparent; ++ NSInteger _titleVisibility; ++ BOOL _restorable; ++ NSString *_subtitle; ++ NSInteger _tabbingMode; ++ NSString *_tabbingIdentifier; ++ NSInteger _titlebarSeparatorStyle; ++ NSInteger _toolbarStyle; ++ + BOOL _isOpaque; + BOOL _isVisible; + BOOL _isDocumentEdited; +@@ -217,6 +227,7 @@ APPKIT_EXPORT const NSNotificationName NSWindowDidExposeNotification; + BOOL _showsToolbarButton; + BOOL _ignoresMouseEvents; + BOOL _isMovableByWindowBackground; ++ BOOL _isMovable; + BOOL _allowsToolTipsWhenApplicationIsInactive; + BOOL _defaultButtonCellKeyEquivalentDisabled; + BOOL _autorecalculatesKeyViewLoop; +@@ -240,6 +251,7 @@ APPKIT_EXPORT const NSNotificationName NSWindowDidExposeNotification; + NSMutableArray *_drawers; + NSToolbar *_toolbar; + NSWindowAnimationContext *_animationContext; ++ NSButton *_standardWindowButtons[3]; + + NSRect _savedFrame; + NSPoint _mouseDownLocationInWindow; +@@ -425,6 +437,7 @@ APPKIT_EXPORT const NSNotificationName NSWindowDidExposeNotification; + - (NSButton *) standardWindowButton: (NSWindowButton) value; + - (NSButtonCell *) defaultButtonCell; + - (NSWindow *) attachedSheet; ++- (NSWindow *) sheetParent; + + - (id) windowController; + - (NSArray *) drawers; +@@ -450,6 +463,7 @@ APPKIT_EXPORT const NSNotificationName NSWindowDidExposeNotification; + - (BOOL) isMainWindow; + - (BOOL) isMiniaturized; + - (BOOL) isMovable; ++- (void) performWindowDragWithEvent:(NSEvent *)event; + - (BOOL) inLiveResize; + - (BOOL) canBecomeKeyWindow; + - (BOOL) canBecomeMainWindow; +@@ -458,6 +472,30 @@ APPKIT_EXPORT const NSNotificationName NSWindowDidExposeNotification; + + - (NSPoint) convertBaseToScreen: (NSPoint) point; + - (NSPoint) convertScreenToBase: (NSPoint) point; ++- (NSPoint) convertPointToScreen: (NSPoint) point; ++- (NSPoint) convertPointFromScreen: (NSPoint) point; ++- (NSRect) convertRectToScreen: (NSRect) rect; ++- (NSRect) convertRectFromScreen: (NSRect) rect; ++ ++- (BOOL) titlebarAppearsTransparent; ++- (void) setTitlebarAppearsTransparent: (BOOL) value; ++- (NSInteger) titleVisibility; ++- (void) setTitleVisibility: (NSInteger) value; ++- (BOOL) movableByWindowBackground; ++- (NSUInteger) occlusionState; ++- (BOOL) isRestorable; ++- (BOOL) restorable; ++- (void) setRestorable: (BOOL) value; ++- (NSString *) subtitle; ++- (void) setSubtitle: (NSString *) value; ++- (NSInteger) tabbingMode; ++- (void) setTabbingMode: (NSInteger) value; ++- (NSString *) tabbingIdentifier; ++- (void) setTabbingIdentifier: (NSString *) value; ++- (NSInteger) titlebarSeparatorStyle; ++- (void) setTitlebarSeparatorStyle: (NSInteger) value; ++- (NSInteger) toolbarStyle; ++- (void) setToolbarStyle: (NSInteger) value; + + - (NSRect) frameRectForContentRect: (NSRect) rect; + - (NSRect) contentRectForFrameRect: (NSRect) rect; +diff --git a/AppKit/nib.subproj/NSNib.m b/AppKit/nib.subproj/NSNib.m +index 01dc29ad..63654d66 100644 +--- a/AppKit/nib.subproj/NSNib.m ++++ b/AppKit/nib.subproj/NSNib.m +@@ -284,10 +284,8 @@ NSString *const NSNibTopLevelObjects = @"NSNibTopLevelObjects"; + return result; + } + +-#warning -[NSNib instantiateWithOwner:topLevelObjects:] method makes darling be a zombie process and need to restart device +- +-/* - (BOOL) instantiateWithOwner: (id) owner topLevelObjects: (NSArray **) objects { ++- (BOOL) instantiateWithOwner: (id) owner topLevelObjects: (NSArray **) objects { + return [self instantiateNibWithOwner: owner topLevelObjects: objects]; +-} */ ++} + + @end +diff --git a/AppKit/private-include/AppKit/NSAccessibilityRemoteUIElement.h b/AppKit/private-include/AppKit/NSAccessibilityRemoteUIElement.h +index 756d6078..51cbe463 100644 +--- a/AppKit/private-include/AppKit/NSAccessibilityRemoteUIElement.h ++++ b/AppKit/private-include/AppKit/NSAccessibilityRemoteUIElement.h +@@ -20,4 +20,14 @@ + #import + + @interface NSAccessibilityRemoteUIElement : NSObject ++ ++/* darling-lol M57: Chromium/Electron asks for a remote token for a local ++ * accessibility element; Cocotron lacked the class method entirely, so the ++ * UX aborted with an unrecognized-selector NSException during createWindow. */ +++ (id) remoteTokenForLocalUIElement: (id) element; +++ (void) setRemoteUIApp: (id) app; +++ (void) registerRemoteUIProcessIdentifier: (int) pid; ++- (id) init; ++- (id) initWithRemoteToken: (id) token; ++ + @end +diff --git a/CoreGraphics/CGAffineTransform.m b/CoreGraphics/CGAffineTransform.m +index aaeca762..0c04444e 100644 +--- a/CoreGraphics/CGAffineTransform.m ++++ b/CoreGraphics/CGAffineTransform.m +@@ -100,6 +100,18 @@ CGAffineTransform CGAffineTransformTranslate(CGAffineTransform xform, + } + + CGRect CGRectApplyAffineTransform(CGRect rect, CGAffineTransform t) { +- printf("CGRectApplyAffineTransform STUB\n"); +- return rect; ++ if (CGRectIsNull(rect)) ++ return rect; ++ CGFloat minX = 0, minY = 0, maxX = 0, maxY = 0; ++ for (int i = 0; i < 4; ++i) { ++ CGFloat x = rect.origin.x + ((i & 1) ? rect.size.width : 0); ++ CGFloat y = rect.origin.y + ((i & 2) ? rect.size.height : 0); ++ CGFloat tx = t.a * x + t.c * y + t.tx; ++ CGFloat ty = t.b * x + t.d * y + t.ty; ++ if (i == 0 || tx < minX) minX = tx; ++ if (i == 0 || ty < minY) minY = ty; ++ if (i == 0 || tx > maxX) maxX = tx; ++ if (i == 0 || ty > maxY) maxY = ty; ++ } ++ return CGRectMake(minX, minY, maxX - minX, maxY - minY); + } +diff --git a/CoreGraphics/CGColorSpace.m b/CoreGraphics/CGColorSpace.m +index c84a2f75..e0ca9572 100644 +--- a/CoreGraphics/CGColorSpace.m ++++ b/CoreGraphics/CGColorSpace.m +@@ -37,6 +37,8 @@ const CFStringRef kCGColorSpaceACESCGLinear = + CFSTR("kCGColorSpaceACESCGLinear"); + const CFStringRef kCGColorSpaceITUR_709 = CFSTR("kCGColorSpaceITUR_709"); + const CFStringRef kCGColorSpaceITUR_2020 = CFSTR("kCGColorSpaceITUR_2020"); ++const CFStringRef kCGColorSpaceITUR_2100_HLG = CFSTR("kCGColorSpaceITUR_2100_HLG"); ++const CFStringRef kCGColorSpaceITUR_2100_PQ = CFSTR("kCGColorSpaceITUR_2100_PQ"); + const CFStringRef kCGColorSpaceROMMRGB = CFSTR("kCGColorSpaceROMMRGB"); + const CFStringRef kCGColorSpaceDCIP3 = CFSTR("kCGColorSpaceDCIP3"); + const CFStringRef kCGColorSpaceExtendedSRGB = +@@ -85,3 +87,11 @@ size_t CGColorSpaceGetNumberOfComponents(CGColorSpaceRef self) { + CGColorSpaceRef CGColorSpaceCreateWithName(CFStringRef name) { + return (CGColorSpaceRef) O2ColorSpaceCreateWithName(name); + } ++ ++CFDataRef CGColorSpaceCopyICCData(CGColorSpaceRef self) { ++ /* No ICC profile store in Darling yet; callers (e.g. Chromium display ++ * setup) treat NULL as "use default sRGB", consistent with what ++ * -[NSScreen colorSpace] reports. */ ++ (void)self; ++ return NULL; ++} +diff --git a/CoreGraphics/CGContext.m b/CoreGraphics/CGContext.m +index db8869c8..6fea5107 100644 +--- a/CoreGraphics/CGContext.m ++++ b/CoreGraphics/CGContext.m +@@ -614,8 +614,13 @@ void CGContextDrawTiledImage(CGContextRef c, CGRect rect, CGImageRef image) + } + + void CGContextShowGlyphsAtPositions(CGContextRef c, +- const CGGlyph * glyphs, const CGPoint * Lpositions, ++ const CGGlyph *glyphs, const CGPoint *positions, + size_t count) + { +- printf("STUB %s\n", __PRETTY_FUNCTION__); ++ if (!c || !glyphs || !positions) ++ return; ++ CGPoint savedPosition = CGContextGetTextPosition(c); ++ for (size_t i = 0; i < count; ++i) ++ CGContextShowGlyphsAtPoint(c, positions[i].x, positions[i].y, glyphs + i, 1); ++ CGContextSetTextPosition(c, savedPosition.x, savedPosition.y); + } +diff --git a/CoreGraphics/CGDirectDisplay.m b/CoreGraphics/CGDirectDisplay.m +index e213b066..94d5c7bb 100644 +--- a/CoreGraphics/CGDirectDisplay.m ++++ b/CoreGraphics/CGDirectDisplay.m +@@ -474,7 +474,9 @@ CGError CGAssociateMouseAndMouseCursorPosition(boolean_t connected) { + if (!display) + return kCGErrorInvalidConnection; + +- [display grabMouse: connected]; ++ // Association enables ordinary absolute pointer motion. A disconnected ++ // cursor requests relative motion, implemented by the backend's grab. ++ [display grabMouse: !connected]; + return kCGErrorSuccess; + } + +@@ -500,3 +502,38 @@ CFDictionaryRef CGDisplayCurrentMode(CGDirectDisplayID display) { + size_t CGDisplayModeGetPixelWidth(CGDisplayModeRef mode) { + return 0; + } ++ ++bool CGDisplayUsesForceToGray(void) { ++ /* Accessibility grayscale is never forced on X11. */ ++ return false; ++} ++ ++bool CGDisplayIsBuiltin(CGDirectDisplayID display) { ++ /* Single X11 screen; treat it as the built-in display. */ ++ (void)display; ++ return true; ++} ++ ++double CGDisplayRotation(CGDirectDisplayID display) { ++ (void)display; ++ return 0.0; ++} ++ ++CGColorSpaceRef CGDisplayCopyColorSpace(CGDirectDisplayID display) { ++ (void)display; ++ return CGColorSpaceCreateDeviceRGB(); ++} ++ ++CGImageRef CGDisplayCreateImage(CGDirectDisplayID displayID) { ++ /* Screen capture not implemented; callers must handle NULL. */ ++ (void)displayID; ++ return NULL; ++} ++ ++bool CGPreflightScreenCaptureAccess(void) { ++ return false; ++} ++ ++bool CGRequestScreenCaptureAccess(void) { ++ return false; ++} +diff --git a/CoreGraphics/CGDisplayStream.m b/CoreGraphics/CGDisplayStream.m +index a9457e73..f9adca0d 100644 +--- a/CoreGraphics/CGDisplayStream.m ++++ b/CoreGraphics/CGDisplayStream.m +@@ -20,3 +20,7 @@ + #import + + const CFStringRef kCGDisplayStreamShowCursor = CFSTR("kCGDisplayStreamShowCursor"); ++const CFStringRef kCGDisplayStreamColorSpace = CFSTR("kCGDisplayStreamColorSpace"); ++const CFStringRef kCGDisplayStreamDestinationRect = CFSTR("kCGDisplayStreamDestinationRect"); ++const CFStringRef kCGDisplayStreamMinimumFrameTime = CFSTR("kCGDisplayStreamMinimumFrameTime"); ++const CFStringRef kCGDisplayStreamPreserveAspectRatio = CFSTR("kCGDisplayStreamPreserveAspectRatio"); +diff --git a/CoreGraphics/CGEventSource.m b/CoreGraphics/CGEventSource.m +index 537c67ae..0bffcd36 100644 +--- a/CoreGraphics/CGEventSource.m ++++ b/CoreGraphics/CGEventSource.m +@@ -76,3 +76,22 @@ CGEventFlags CGEventSourceFlagsState(CGEventSourceStateID stateID) { + void CGEventSourceSetLocalEventsSuppressionInterval(CGEventSourceRef source, CFTimeInterval seconds) { + printf("STUB %s\n", __PRETTY_FUNCTION__); + } ++ ++bool CGEventSourceKeyState(CGEventSourceStateID stateID, CGKeyCode key) { ++ /* No access to the host keymap from here; report no keys down. ++ * Used by push-to-talk style polling (e.g. Discord); returning ++ * false is safe and keeps callers (dlopen-time binding included) ++ * alive. A future XQueryKeymap-backed version can upgrade this. */ ++ (void)stateID; (void)key; ++ return false; ++} ++ ++CFTimeInterval CGEventSourceSecondsSinceLastEventType(CGEventSourceStateID stateID, CGEventType eventType) { ++ (void)stateID; (void)eventType; ++ return 0.0; ++} ++ ++uint32_t CGEventSourceCounterForEventType(CGEventSourceStateID stateID, CGEventType eventType) { ++ (void)stateID; (void)eventType; ++ return 0; ++} +diff --git a/CoreGraphics/CGFont.m b/CoreGraphics/CGFont.m +index bcd822fd..51e89ae2 100644 +--- a/CoreGraphics/CGFont.m ++++ b/CoreGraphics/CGFont.m +@@ -96,11 +96,27 @@ CFDataRef CGFontCopyTableForTag(CGFontRef self, uint32_t tag) { + + CGFontRef _Nullable CGFontCreateWithDataProvider(CGDataProviderRef _Nullable provider) + { +- printf("STUB %s\n", __PRETTY_FUNCTION__); +- return nil; ++ if (!provider) ++ return nil; ++ return (CGFontRef)O2FontCreateWithDataProvider((O2DataProviderRef)provider); + } + + CFStringRef const kCGFontVariationAxisName = CFSTR("kCGFontVariationAxisName"); + CFStringRef const kCGFontVariationAxisMinValue = CFSTR("kCGFontVariationAxisMinValue"); + CFStringRef const kCGFontVariationAxisDefaultValue = CFSTR("kCGFontVariationAxisDefaultValue"); + CFStringRef const kCGFontVariationAxisMaxValue = CFSTR("kCGFontVariationAxisMaxValue"); ++ ++ ++/* darling-lol M58b: Chromium's renderer lazy-binds CGFontCopyPostScriptName ++ * (the shim lacked the symbol and the GPU-composited renderer died on a ++ * lazy-bind abort after the compositor engaged). Return the font name — ++ * a PostScript name approximation is sufficient for this path. */ ++CFStringRef CGFontCopyPostScriptName(CGFontRef self) { ++ /* Chromium can probe the fallback font before CoreText has produced a ++ * concrete CGFont. Apple's implementation yields a usable family name ++ * along this path; do not pass NULL into O2FontCopyFullName. The Copy ++ * contract requires a retained result. */ ++ if (self == NULL) ++ return (CFStringRef) CFRetain(CFSTR("DarlingSans")); ++ return (CFStringRef)[(O2Font *)self copyPostScriptName]; ++} +diff --git a/CoreGraphics/CGSConnection.m b/CoreGraphics/CGSConnection.m +index 99826d19..0b75047b 100644 +--- a/CoreGraphics/CGSConnection.m ++++ b/CoreGraphics/CGSConnection.m +@@ -94,6 +94,11 @@ + NSInvalidAbstractInvocation(); + } + ++-(NSArray*) copyWindowDescriptions ++{ ++ return [[NSArray alloc] init]; ++} ++ + -(CGError) destroyWindow:(CGSWindowID)winId + { + CGSWindow* win = [self windowForId: winId]; +diff --git a/CoreGraphics/CGSWindow.m b/CoreGraphics/CGSWindow.m +index e1da261d..f050b6e8 100644 +--- a/CoreGraphics/CGSWindow.m ++++ b/CoreGraphics/CGSWindow.m +@@ -101,6 +101,47 @@ CGError CGSSetWindowTitle(CGSConnectionID cid, CGSWindowID wid, + return CGSSetWindowProperty(cid, wid, kCGSWindowTitle, title); + } + ++// Windows of every client of the display server, front to back. The option and the reference ++// window select the same subsets as on macOS. ++CFArrayRef __nullable CGWindowListCopyWindowInfo(CGWindowListOption option, CGWindowID relativeToWindow) ++{ ++ CGSConnection* connection = _CGSConnectionForID(_CGSDefaultConnection()); ++ NSArray* all = connection ? [connection copyWindowDescriptions] : [[NSArray alloc] init]; ++ NSMutableArray* selected = [[NSMutableArray alloc] init]; ++ ++ NSInteger referenceIndex = NSNotFound; ++ for (NSUInteger i = 0; i < [all count]; i++) ++ { ++ if ([all[i][(NSString*) kCGWindowNumber] unsignedIntValue] == relativeToWindow) ++ referenceIndex = i; ++ } ++ ++ for (NSUInteger i = 0; i < [all count]; i++) ++ { ++ NSDictionary* window = all[i]; ++ BOOL isReference = (NSInteger) i == referenceIndex; ++ BOOL haveReference = referenceIndex != NSNotFound; ++ BOOL include = YES; ++ ++ if ((option & kCGWindowListOptionOnScreenOnly) && ![window[(NSString*) kCGWindowIsOnscreen] boolValue]) ++ continue; ++ ++ // The list is front to back: earlier entries are above the reference window ++ if (option & kCGWindowListOptionOnScreenAboveWindow) ++ include = (haveReference && (NSInteger) i < referenceIndex) || ((option & kCGWindowListOptionIncludingWindow) && isReference); ++ else if (option & kCGWindowListOptionOnScreenBelowWindow) ++ include = (haveReference && (NSInteger) i > referenceIndex) || ((option & kCGWindowListOptionIncludingWindow) && isReference); ++ else if (option & kCGWindowListOptionIncludingWindow) ++ include = isReference; ++ ++ if (include) ++ [selected addObject: window]; ++ } ++ ++ [all release]; ++ return (CFArrayRef) selected; ++} ++ + CFArrayRef __nullable CGWindowListCreate(CGWindowListOption option, CGWindowID relativeToWindow) + { + printf("STUB %s\n", __PRETTY_FUNCTION__); +diff --git a/CoreGraphics/X11.backend/CGSConnectionX11.h b/CoreGraphics/X11.backend/CGSConnectionX11.h +index 5437acb5..07f7eb4f 100644 +--- a/CoreGraphics/X11.backend/CGSConnectionX11.h ++++ b/CoreGraphics/X11.backend/CGSConnectionX11.h +@@ -48,6 +48,7 @@ + -(CGSWindow*) newWindow:(CGSRegionRef)region; + + -(void) processPendingEvents; ++-(NSArray*) copyWindowDescriptions; + + +(BOOL) isAvailable; + +diff --git a/CoreGraphics/X11.backend/CGSConnectionX11.m b/CoreGraphics/X11.backend/CGSConnectionX11.m +index 7af8bd93..90420b6f 100644 +--- a/CoreGraphics/X11.backend/CGSConnectionX11.m ++++ b/CoreGraphics/X11.backend/CGSConnectionX11.m +@@ -18,6 +18,7 @@ + */ + #import "CGSConnectionX11.h" + #import ++#import + #include "CarbonKeys.h" + #import "X11KeySymToUCS.h" + #import +@@ -25,6 +26,7 @@ + #include + #include + #import ++#import + #import + #import + #import +@@ -52,6 +54,13 @@ static void socketCallback(CFSocketRef s, CFSocketCallBackType type, CFDataRef a + + -(instancetype) initWithConnectionID:(CGSConnectionID)connId + { ++ // Native EGL and the run loop may use Xlib concurrently. This connection ++ // can be opened before AppKit initializes its own display. ++ if (!XInitThreads()) { ++ NSLog(@"CGSConnectionX11: Xlib thread initialization failed"); ++ [self release]; ++ return nil; ++ } + _display = XOpenDisplay(NULL); + if (!_display) + _display = XOpenDisplay(":0"); +@@ -122,6 +131,106 @@ static void socketCallback(CFSocketRef s, CFSocketCallBackType type, CFDataRef a + [super dealloc]; + } + ++// Reads a property as raw bytes; returns NULL if it is not set. Caller XFree()s the result. ++static unsigned char* windowProperty(Display* display, Window window, Atom property, Atom type, unsigned long* count) ++{ ++ Atom actualType; ++ int actualFormat; ++ unsigned long bytesAfter; ++ unsigned char* data = NULL; ++ ++ *count = 0; ++ if (XGetWindowProperty(display, window, property, 0, 1024, False, type, &actualType, &actualFormat, ++ count, &bytesAfter, &data) != Success || actualType == None || *count == 0) ++ { ++ if (data) ++ XFree(data); ++ *count = 0; ++ return NULL; ++ } ++ return data; ++} ++ ++-(NSArray*) copyWindowDescriptions ++{ ++ NSMutableArray* result = [[NSMutableArray alloc] init]; ++ ++ Window root = DefaultRootWindow(_display), rootReturn, parentReturn, *children = NULL; ++ unsigned int count = 0; ++ ++ if (!XQueryTree(_display, root, &rootReturn, &parentReturn, &children, &count)) ++ return result; ++ ++ Atom netWmPid = XInternAtom(_display, "_NET_WM_PID", True); ++ Atom netWmName = XInternAtom(_display, "_NET_WM_NAME", True); ++ Atom utf8String = XInternAtom(_display, "UTF8_STRING", True); ++ ++ // XQueryTree lists children bottom to top, CGWindowList is front to back ++ for (int i = (int) count - 1; i >= 0; i--) ++ { ++ Window window = children[i]; ++ XWindowAttributes attributes; ++ unsigned long length; ++ ++ if (!XGetWindowAttributes(_display, window, &attributes)) ++ continue; ++ ++ // Windows of clients that do not name their process (window manager helpers) are skipped ++ unsigned char* pidData = netWmPid != None ? windowProperty(_display, window, netWmPid, XA_CARDINAL, &length) : NULL; ++ if (!pidData) ++ continue; ++ long pid = *(long*) pidData; ++ XFree(pidData); ++ ++ NSMutableDictionary* description = [NSMutableDictionary dictionary]; ++ description[(NSString*) kCGWindowNumber] = @((long) window); ++ description[(NSString*) kCGWindowOwnerPID] = @(pid); ++ description[(NSString*) kCGWindowLayer] = @0; ++ description[(NSString*) kCGWindowAlpha] = @1.0; ++ description[(NSString*) kCGWindowIsOnscreen] = @(attributes.map_state == IsViewable); ++ description[(NSString*) kCGWindowBounds] = @{ ++ @"X": @(attributes.x), @"Y": @(attributes.y), ++ @"Width": @(attributes.width), @"Height": @(attributes.height), ++ }; ++ ++ XClassHint classHint = {NULL, NULL}; ++ if (XGetClassHint(_display, window, &classHint)) ++ { ++ if (classHint.res_class) ++ description[(NSString*) kCGWindowOwnerName] = [NSString stringWithUTF8String: classHint.res_class]; ++ if (classHint.res_name) ++ XFree(classHint.res_name); ++ if (classHint.res_class) ++ XFree(classHint.res_class); ++ } ++ ++ unsigned char* name = netWmName != None ? windowProperty(_display, window, netWmName, utf8String, &length) : NULL; ++ if (name) ++ { ++ NSString* title = [[NSString alloc] initWithBytes: name length: length encoding: NSUTF8StringEncoding]; ++ if (title) ++ description[(NSString*) kCGWindowName] = title; ++ [title release]; ++ XFree(name); ++ } ++ else ++ { ++ char* legacyName = NULL; ++ if (XFetchName(_display, window, &legacyName) && legacyName) ++ { ++ description[(NSString*) kCGWindowName] = [NSString stringWithUTF8String: legacyName]; ++ XFree(legacyName); ++ } ++ } ++ ++ [result addObject: description]; ++ } ++ ++ if (children) ++ XFree(children); ++ return result; ++} ++ + -(CGSKeyboardLayout*) createKeyboardLayout + { + @synchronized (self) +diff --git a/CoreGraphics/include/CoreGraphics/CGColorSpace.h b/CoreGraphics/include/CoreGraphics/CGColorSpace.h +index 2f5674ff..0eec27b9 100644 +--- a/CoreGraphics/include/CoreGraphics/CGColorSpace.h ++++ b/CoreGraphics/include/CoreGraphics/CGColorSpace.h +@@ -27,6 +27,8 @@ COREGRAPHICS_EXPORT const CFStringRef kCGColorSpaceGenericLab; + COREGRAPHICS_EXPORT const CFStringRef kCGColorSpaceACESCGLinear; + COREGRAPHICS_EXPORT const CFStringRef kCGColorSpaceITUR_709; + COREGRAPHICS_EXPORT const CFStringRef kCGColorSpaceITUR_2020; ++COREGRAPHICS_EXPORT const CFStringRef kCGColorSpaceITUR_2100_HLG; ++COREGRAPHICS_EXPORT const CFStringRef kCGColorSpaceITUR_2100_PQ; + COREGRAPHICS_EXPORT const CFStringRef kCGColorSpaceROMMRGB; + COREGRAPHICS_EXPORT const CFStringRef kCGColorSpaceDCIP3; + COREGRAPHICS_EXPORT const CFStringRef kCGColorSpaceExtendedSRGB; +diff --git a/CoreGraphics/include/CoreGraphics/CGDirectDisplay.h b/CoreGraphics/include/CoreGraphics/CGDirectDisplay.h +index dd3f5479..f17543d7 100644 +--- a/CoreGraphics/include/CoreGraphics/CGDirectDisplay.h ++++ b/CoreGraphics/include/CoreGraphics/CGDirectDisplay.h +@@ -2,6 +2,8 @@ + #import + #import + #import ++#import ++#import + #import + + typedef uint32_t CGDirectDisplayID; +@@ -74,3 +76,10 @@ COREGRAPHICS_EXPORT CGError CGDisplaySwitchToMode(CGDirectDisplayID display, + CFDictionaryRef mode); + COREGRAPHICS_EXPORT size_t CGDisplayModeGetPixelWidth(CGDisplayModeRef mode); + COREGRAPHICS_EXPORT CGSize CGDisplayScreenSize(CGDirectDisplayID display); ++COREGRAPHICS_EXPORT bool CGDisplayIsBuiltin(CGDirectDisplayID display); ++COREGRAPHICS_EXPORT double CGDisplayRotation(CGDirectDisplayID display); ++COREGRAPHICS_EXPORT CGColorSpaceRef CGDisplayCopyColorSpace(CGDirectDisplayID display); ++COREGRAPHICS_EXPORT CGImageRef CGDisplayCreateImage(CGDirectDisplayID displayID); ++COREGRAPHICS_EXPORT bool CGDisplayUsesForceToGray(void); ++COREGRAPHICS_EXPORT bool CGPreflightScreenCaptureAccess(void); ++COREGRAPHICS_EXPORT bool CGRequestScreenCaptureAccess(void); +diff --git a/CoreGraphics/include/CoreGraphics/CGDisplayStream.h b/CoreGraphics/include/CoreGraphics/CGDisplayStream.h +index ad289284..856d5009 100644 +--- a/CoreGraphics/include/CoreGraphics/CGDisplayStream.h ++++ b/CoreGraphics/include/CoreGraphics/CGDisplayStream.h +@@ -20,3 +20,7 @@ + #import + + COREGRAPHICS_EXPORT const CFStringRef kCGDisplayStreamShowCursor; ++COREGRAPHICS_EXPORT const CFStringRef kCGDisplayStreamColorSpace; ++COREGRAPHICS_EXPORT const CFStringRef kCGDisplayStreamDestinationRect; ++COREGRAPHICS_EXPORT const CFStringRef kCGDisplayStreamMinimumFrameTime; ++COREGRAPHICS_EXPORT const CFStringRef kCGDisplayStreamPreserveAspectRatio; +diff --git a/CoreGraphics/include/CoreGraphics/CGFont.h b/CoreGraphics/include/CoreGraphics/CGFont.h +index 50a92d54..cb0add22 100644 +--- a/CoreGraphics/include/CoreGraphics/CGFont.h ++++ b/CoreGraphics/include/CoreGraphics/CGFont.h +@@ -36,6 +36,7 @@ COREGRAPHICS_EXPORT CGFontRef CGFontRetain(CGFontRef self); + COREGRAPHICS_EXPORT void CGFontRelease(CGFontRef self); + + COREGRAPHICS_EXPORT CFStringRef CGFontCopyFullName(CGFontRef self); ++COREGRAPHICS_EXPORT CFStringRef CGFontCopyPostScriptName(CGFontRef self); + COREGRAPHICS_EXPORT int CGFontGetUnitsPerEm(CGFontRef self); + COREGRAPHICS_EXPORT int CGFontGetAscent(CGFontRef self); + COREGRAPHICS_EXPORT int CGFontGetDescent(CGFontRef self); +diff --git a/CoreGraphics/include/CoreGraphics/CGSConnection.h b/CoreGraphics/include/CoreGraphics/CGSConnection.h +index f3991816..7862bdbb 100644 +--- a/CoreGraphics/include/CoreGraphics/CGSConnection.h ++++ b/CoreGraphics/include/CoreGraphics/CGSConnection.h +@@ -38,6 +38,10 @@ + -(CGSWindow*) newWindow:(CGSRegionRef)region; + -(CGError) destroyWindow:(CGSWindowID)winId; + ++// Top-level windows of all clients of the display server, front to back, described with the ++// kCGWindow* keys. Backends that cannot enumerate windows return an empty array. ++-(NSArray*) copyWindowDescriptions; ++ + // Mouse location in CG coordinates, i.e. relative to the upper left corner + -(CGPoint) mouseLocation; + +diff --git a/CoreGraphics/include/CoreGraphics/CGWindow.h b/CoreGraphics/include/CoreGraphics/CGWindow.h +index e1112f40..3a4fa2d6 100644 +--- a/CoreGraphics/include/CoreGraphics/CGWindow.h ++++ b/CoreGraphics/include/CoreGraphics/CGWindow.h +@@ -30,6 +30,7 @@ extern const CFStringRef kCGWindowAlpha; + extern const CFStringRef kCGWindowBounds; + extern const CFStringRef kCGWindowLayer; + extern const CFStringRef kCGWindowName; ++extern const CFStringRef kCGWindowNumber; + extern const CFStringRef kCGWindowIsOnscreen; + extern const CFStringRef kCGWindowOwnerName; + extern const CFStringRef kCGWindowOwnerPID; +@@ -172,6 +173,7 @@ typedef uint32_t CGWindowID; + CF_IMPLICIT_BRIDGING_ENABLED + + COREGRAPHICS_EXPORT CFArrayRef CGWindowListCreate(CGWindowListOption option, CGWindowID relativeToWindow); ++COREGRAPHICS_EXPORT CFArrayRef CGWindowListCopyWindowInfo(CGWindowListOption option, CGWindowID relativeToWindow); + COREGRAPHICS_EXPORT CFArrayRef CGWindowListCreateDescriptionFromArray(CFArrayRef windowArray); + COREGRAPHICS_EXPORT CGImageRef CGWindowListCreateImageFromArray(CGRect screenBounds, CFArrayRef windowArray, CGWindowImageOption imageOption); + +diff --git a/CoreText/CMakeLists.txt b/CoreText/CMakeLists.txt +index 1ec11620..08a238ca 100644 +--- a/CoreText/CMakeLists.txt ++++ b/CoreText/CMakeLists.txt +@@ -28,7 +28,6 @@ set(CoreText_sources + KTFont.m + KTFont_FT.m + constants.c +- stubs.c + ) + + set_source_files_properties(${CoreText_sources} LANGUAGE C) +diff --git a/CoreText/CTFont.m b/CoreText/CTFont.m +index 94ddabf9..fa018d3d 100644 +--- a/CoreText/CTFont.m ++++ b/CoreText/CTFont.m +@@ -20,6 +20,18 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + #import + #import + #import ++#import ++#import ++#import ++#import ++ ++/* NSFont uses 32-bit NSGlyph buffers; CTFont uses 16-bit CGGlyph buffers. ++ * Bridge to the underlying KTFont before calling font methods. */ ++static CTFontRef normalizedFont(CTFontRef font) { ++ if (font && [(id)font respondsToSelector:@selector(_coreTextFont)]) ++ return (CTFontRef)[(id)font performSelector:@selector(_coreTextFont)]; ++ return font; ++} + + const CFStringRef kCTFontCopyrightNameKey = CFSTR("CTFontCopyrightName"); + const CFStringRef kCTFontFamilyNameKey = CFSTR("CTFontFamilyName"); +@@ -60,31 +72,75 @@ const CFStringRef kCTFontFeatureTooltipTextKey = CFSTR("CTFeatureTooltipText"); + + CTFontRef CTFontCreateWithName(CFStringRef name, CGFloat size, const CGAffineTransform *matrix) + { +- printf("STUB %s\n", __PRETTY_FUNCTION__); +- return nil; ++ if (!name) ++ return nil; ++ CGFontRef cgFont = CGFontCreateWithFontName(name); ++ if (!cgFont) ++ return nil; ++ if (size == 0) ++ size = 12; ++ KTFont *font = [[KTFont alloc] initWithFont:cgFont size:size]; ++ CGFontRelease(cgFont); ++ (void)matrix; ++ return (CTFontRef)font; + } + + CTFontRef CTFontCreateWithNameAndOptions(CFStringRef name, CGFloat size, + const CGAffineTransform *matrix, + CTFontOptions options) + { +- printf("STUB %s\n", __PRETTY_FUNCTION__); +- return nil; ++ (void)options; ++ return CTFontCreateWithName(name, size, matrix); + } + + CTFontRef CTFontCreateWithFontDescriptor(CTFontDescriptorRef descriptor, CGFloat size, + const CGAffineTransform *matrix) + { +- printf("STUB %s\n", __PRETTY_FUNCTION__); +- return nil; ++ if (!descriptor) ++ return nil; ++ if (size == 0) { ++ CFTypeRef descriptorSize = CTFontDescriptorCopyAttribute(descriptor, kCTFontSizeAttribute); ++ if (descriptorSize) { ++ if (CFGetTypeID(descriptorSize) == CFNumberGetTypeID()) ++ CFNumberGetValue((CFNumberRef)descriptorSize, kCFNumberCGFloatType, &size); ++ CFRelease(descriptorSize); ++ } ++ if (size == 0) ++ size = 12; ++ } ++ /* Darling path: descriptors built by CTFontManagerCreateFontDescriptorFromData ++ * carry the raw font bytes; feed them to FreeType via O2Font. */ ++ CFTypeRef dataObj = CTFontDescriptorCopyAttribute(descriptor, CFSTR("DarlingFontData")); ++ if (dataObj) { ++ CGDataProviderRef provider = CGDataProviderCreateWithCFData((CFDataRef)dataObj); ++ CFRelease(dataObj); ++ if (provider) { ++ CGFontRef cgFont = CGFontCreateWithDataProvider(provider); ++ CGDataProviderRelease(provider); ++ if (cgFont) { ++ KTFont *font = [[KTFont alloc] initWithFont:cgFont size:size]; ++ CGFontRelease(cgFont); ++ (void)matrix; ++ return (CTFontRef)font; ++ } ++ } ++ } ++ CFStringRef name = (CFStringRef)CTFontDescriptorCopyAttribute(descriptor, kCTFontNameAttribute); ++ if (!name) ++ name = (CFStringRef)CTFontDescriptorCopyAttribute(descriptor, kCTFontFamilyNameAttribute); ++ if (!name) ++ return nil; ++ CTFontRef result = CTFontCreateWithName(name, size, matrix); ++ CFRelease(name); ++ return result; + } + + CTFontRef CTFontCreateWithFontDescriptorAndOptions(CTFontDescriptorRef descriptor, CGFloat size, + const CGAffineTransform *matrix, + CTFontOptions options) + { +- printf("STUB %s\n", __PRETTY_FUNCTION__); +- return nil; ++ (void)options; ++ return CTFontCreateWithFontDescriptor(descriptor, size, matrix); + } + + CTFontRef CTFontCreateUIFontForLanguage(CTFontUIFontType uiFontType, +@@ -99,8 +155,27 @@ CTFontRef CTFontCreateCopyWithAttributes(CTFontRef font, CGFloat size, + const CGAffineTransform *matrix, + CTFontDescriptorRef attributes) + { +- printf("STUB %s\n", __PRETTY_FUNCTION__); +- return nil; ++ font = normalizedFont(font); ++ if (!font) ++ return nil; ++ if (attributes) { ++ CFTypeRef name = CTFontDescriptorCopyAttribute(attributes, kCTFontNameAttribute); ++ if (name) { ++ CTFontRef result = CTFontCreateWithName((CFStringRef)name, size, matrix); ++ CFRelease(name); ++ if (result) ++ return result; ++ } ++ } ++ CGFontRef cgFont = [(KTFont *)font copyGraphicsFont]; ++ if (!cgFont) ++ return nil; ++ if (size == 0) ++ size = [(KTFont *)font pointSize]; ++ CTFontRef result = (CTFontRef)[[KTFont alloc] initWithFont:cgFont size:size]; ++ CGFontRelease(cgFont); ++ (void)matrix; ++ return result; + } + + CTFontRef CTFontCreateCopyWithSymbolicTraits(CTFontRef font, CGFloat size, +@@ -108,6 +183,7 @@ CTFontRef CTFontCreateCopyWithSymbolicTraits(CTFontRef font, CGFloat size, + CTFontSymbolicTraits symTraitValue, + CTFontSymbolicTraits symTraitMask) + { ++ font = normalizedFont(font); + printf("STUB %s\n", __PRETTY_FUNCTION__); + return nil; + } +@@ -115,12 +191,14 @@ CTFontRef CTFontCreateCopyWithSymbolicTraits(CTFontRef font, CGFloat size, + CTFontRef CTFontCreateCopyWithFamily(CTFontRef font, CGFloat size, + const CGAffineTransform *matrix, CFStringRef family) + { ++ font = normalizedFont(font); + printf("STUB %s\n", __PRETTY_FUNCTION__); + return nil; + } + + CTFontRef CTFontCreateForString(CTFontRef currentFont, CFStringRef string, CFRange range) + { ++ currentFont = normalizedFont(currentFont); + printf("STUB %s\n", __PRETTY_FUNCTION__); + return nil; + } +@@ -128,157 +206,275 @@ CTFontRef CTFontCreateForString(CTFontRef currentFont, CFStringRef string, CFRan + CTFontRef CTFontCreateForStringWithLanguage(CTFontRef currentFont, CFStringRef string, + CFRange range, CFStringRef language) + { ++ currentFont = normalizedFont(currentFont); + printf("STUB %s\n", __PRETTY_FUNCTION__); + return nil; + } + + CTFontDescriptorRef CTFontCopyFontDescriptor(CTFontRef font) + { +- printf("STUB %s\n", __PRETTY_FUNCTION__); +- return nil; ++ font = normalizedFont(font); ++ if (!font) ++ return nil; ++ CFStringRef name = CTFontCopyPostScriptName(font); ++ CFDictionaryRef traits = CTFontCopyTraits(font); ++ CGFloat size = CTFontGetSize(font); ++ CFNumberRef sizeValue = CFNumberCreate(NULL, kCFNumberCGFloatType, &size); ++ if (!name || !traits || !sizeValue) { ++ if (name) CFRelease(name); ++ if (traits) CFRelease(traits); ++ if (sizeValue) CFRelease(sizeValue); ++ return nil; ++ } ++ const void *keys[] = {kCTFontNameAttribute, kCTFontSizeAttribute, kCTFontTraitsAttribute}; ++ const void *values[] = {name, sizeValue, traits}; ++ CFDictionaryRef attributes = CFDictionaryCreate(NULL, keys, values, 3, ++ &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); ++ CTFontDescriptorRef result = CTFontDescriptorCreateWithAttributes(attributes); ++ CFRelease(attributes); ++ CFRelease(name); ++ CFRelease(sizeValue); ++ CFRelease(traits); ++ return result; + } + + CFTypeRef CTFontCopyAttribute(CTFontRef font, CFStringRef attribute) + { +- printf("STUB %s\n", __PRETTY_FUNCTION__); +- return nil; ++ font = normalizedFont(font); ++ CTFontDescriptorRef descriptor = CTFontCopyFontDescriptor(font); ++ if (!descriptor) ++ return nil; ++ CFTypeRef value = CTFontDescriptorCopyAttribute(descriptor, attribute); ++ CFRelease(descriptor); ++ return value; + } + + CGFloat CTFontGetSize(CTFontRef self) { ++ self = normalizedFont(self); + return [self pointSize]; + } + + CGAffineTransform CTFontGetMatrix(CTFontRef font) + { ++ font = normalizedFont(font); + printf("STUB %s\n", __PRETTY_FUNCTION__); + return CGAffineTransformIdentity; + } + ++static CTFontSymbolicTraits fontTraits(CTFontRef font, CGFloat *weight, CGFloat *width) ++{ ++ static const CGFloat weights[] = {-0.8, -0.6, -0.4, 0, 0.23, 0.3, 0.4, 0.56, 0.62}; ++ unsigned int weightClass = 400, widthClass = 5; ++ CTFontSymbolicTraits traits = 0; ++ CGFontRef graphicsFont = CTFontCopyGraphicsFont(font, NULL); ++ if (graphicsFont) { ++ CFDataRef table = CGFontCopyTableForTag(graphicsFont, 0x4f532f32 /* OS/2 */); ++ if (table && CFDataGetLength(table) >= 8) { ++ const UInt8 *bytes = CFDataGetBytePtr(table); ++ weightClass = ((unsigned int)bytes[4] << 8) | bytes[5]; ++ widthClass = ((unsigned int)bytes[6] << 8) | bytes[7]; ++ } ++ if (table) CFRelease(table); ++ table = CGFontCopyTableForTag(graphicsFont, 0x706f7374 /* post */); ++ if (table && CFDataGetLength(table) >= 16) { ++ const UInt8 *bytes = CFDataGetBytePtr(table); ++ if (bytes[12] || bytes[13] || bytes[14] || bytes[15]) ++ traits |= kCTFontTraitMonoSpace; ++ } ++ if (table) CFRelease(table); ++ if (CGFontGetItalicAngle(graphicsFont) != 0) ++ traits |= kCTFontTraitItalic; ++ CGFontRelease(graphicsFont); ++ } ++ if (weightClass < 100) weightClass = 100; ++ if (weightClass > 900) weightClass = 900; ++ unsigned int index = weightClass / 100 - 1; ++ *weight = weights[index]; ++ if (index < 8) ++ *weight += (weights[index + 1] - weights[index]) * (weightClass % 100) / 100.0; ++ if (widthClass < 1 || widthClass > 9) widthClass = 5; ++ *width = ((CGFloat)widthClass - 5) / 4; ++ if (weightClass >= 700) traits |= kCTFontTraitBold; ++ if (widthClass < 5) traits |= kCTFontTraitCondensed; ++ if (widthClass > 5) traits |= kCTFontTraitExpanded; ++ return traits; ++} ++ + CTFontSymbolicTraits CTFontGetSymbolicTraits(CTFontRef font) + { +- printf("STUB %s\n", __PRETTY_FUNCTION__); +- return kCTFontTraitItalic; ++ font = normalizedFont(font); ++ CGFloat weight, width; ++ return font ? fontTraits(font, &weight, &width) : 0; + } + + CFDictionaryRef CTFontCopyTraits(CTFontRef font) + { +- printf("STUB %s\n", __PRETTY_FUNCTION__); +- return nil; ++ font = normalizedFont(font); ++ if (!font) return nil; ++ CGFloat weight, width, slant = 0; ++ SInt32 symbolic = fontTraits(font, &weight, &width); ++ if (symbolic & kCTFontTraitItalic) slant = 0.2; ++ const void *keys[] = {kCTFontSymbolicTrait, kCTFontWeightTrait, kCTFontWidthTrait, kCTFontSlantTrait}; ++ CFNumberRef values[] = { ++ CFNumberCreate(NULL, kCFNumberSInt32Type, &symbolic), ++ CFNumberCreate(NULL, kCFNumberCGFloatType, &weight), ++ CFNumberCreate(NULL, kCFNumberCGFloatType, &width), ++ CFNumberCreate(NULL, kCFNumberCGFloatType, &slant) ++ }; ++ CFDictionaryRef result = CFDictionaryCreate(NULL, keys, (const void **)values, 4, ++ &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); ++ for (int i = 0; i < 4; ++i) CFRelease(values[i]); ++ return result; + } + + CFArrayRef CTFontCopyDefaultCascadeListForLanguages(CTFontRef font, CFArrayRef languagePrefList) + { ++ font = normalizedFont(font); + printf("STUB %s\n", __PRETTY_FUNCTION__); + return nil; + } + + CFStringRef CTFontCopyPostScriptName(CTFontRef font) + { +- printf("STUB %s\n", __PRETTY_FUNCTION__); +- return nil; ++ font = normalizedFont(font); ++ CGFontRef graphicsFont = CTFontCopyGraphicsFont(font, NULL); ++ if (!graphicsFont) return nil; ++ CFStringRef name = CGFontCopyPostScriptName(graphicsFont); ++ CGFontRelease(graphicsFont); ++ return name; + } + + CFStringRef CTFontCopyFamilyName(CTFontRef font) + { +- printf("STUB %s\n", __PRETTY_FUNCTION__); +- return nil; ++ font = normalizedFont(font); ++ CGFontRef graphicsFont = CTFontCopyGraphicsFont(font, NULL); ++ if (!graphicsFont) return nil; ++ CFStringRef name = (CFStringRef)[(O2Font *)graphicsFont copyFamilyName]; ++ CGFontRelease(graphicsFont); ++ return name; + } + + CFStringRef CTFontCopyFullName(CTFontRef self) { ++ self = normalizedFont(self); + return [self copyName]; + } + + CFStringRef CTFontCopyDisplayName(CTFontRef font) + { +- printf("STUB %s\n", __PRETTY_FUNCTION__); +- return nil; ++ font = normalizedFont(font); ++ return CTFontCopyFullName(font); + } + + CFStringRef _Nullable CTFontCopyName(CTFontRef font, CFStringRef nameKey) + { +- printf("STUB %s\n", __PRETTY_FUNCTION__); ++ font = normalizedFont(font); ++ if (!font || !nameKey) return nil; ++ if (CFEqual(nameKey, kCTFontPostScriptNameKey)) return CTFontCopyPostScriptName(font); ++ if (CFEqual(nameKey, kCTFontFamilyNameKey)) return CTFontCopyFamilyName(font); ++ if (CFEqual(nameKey, kCTFontFullNameKey)) return CTFontCopyFullName(font); + return nil; + } + + CFStringRef CTFontCopyLocalizedName(CTFontRef font, CFStringRef nameKey, + CFStringRef _Nullable *actualLanguage) + { ++ font = normalizedFont(font); + printf("STUB %s\n", __PRETTY_FUNCTION__); + return nil; + } + + CFCharacterSetRef CTFontCopyCharacterSet(CTFontRef font) + { ++ font = normalizedFont(font); + printf("STUB %s\n", __PRETTY_FUNCTION__); + return nil; + } + + CFStringEncoding CTFontGetStringEncoding(CTFontRef font) + { ++ font = normalizedFont(font); + printf("STUB %s\n", __PRETTY_FUNCTION__); + return CFStringGetSystemEncoding(); + } + + CFArrayRef CTFontCopySupportedLanguages(CTFontRef font) + { ++ font = normalizedFont(font); + printf("STUB %s\n", __PRETTY_FUNCTION__); + return nil; + } + + CGFloat CTFontGetAscent(CTFontRef self) { ++ self = normalizedFont(self); + return [self ascender]; + } + + CGFloat CTFontGetDescent(CTFontRef self) { ++ self = normalizedFont(self); + return [self descender]; + } + + CGFloat CTFontGetLeading(CTFontRef self) { ++ self = normalizedFont(self); + return [self leading]; + } + + unsigned int CTFontGetUnitsPerEm(CTFontRef font) + { +- printf("STUB %s\n", __PRETTY_FUNCTION__); +- return 0; ++ font = normalizedFont(font); ++ CGFontRef graphicsFont = CTFontCopyGraphicsFont(font, NULL); ++ if (!graphicsFont) ++ return 0; ++ unsigned int units = CGFontGetUnitsPerEm(graphicsFont); ++ CGFontRelease(graphicsFont); ++ return units; + } + + CFIndex CTFontGetGlyphCount(CTFontRef font) { ++ font = normalizedFont(font); + return [font numberOfGlyphs]; + } + + CGRect CTFontGetBoundingBox(CTFontRef self) { ++ self = normalizedFont(self); + return [self boundingRect]; + } + + CGFloat CTFontGetUnderlinePosition(CTFontRef self) { ++ self = normalizedFont(self); + return [self underlinePosition]; + } + + CGFloat CTFontGetUnderlineThickness(CTFontRef self) { ++ self = normalizedFont(self); + return [self underlineThickness]; + } + + CGFloat CTFontGetSlantAngle(CTFontRef self) { ++ self = normalizedFont(self); + return [self italicAngle]; + } + + CGFloat CTFontGetCapHeight(CTFontRef self) { ++ self = normalizedFont(self); + return [self capHeight]; + } + + CGFloat CTFontGetXHeight(CTFontRef self) { ++ self = normalizedFont(self); + return [self xHeight]; + } + + CGPathRef CTFontCreatePathForGlyph(CTFontRef self, CGGlyph glyph, + CGAffineTransform *xform) + { ++ self = normalizedFont(self); + return (CGPathRef) [self createPathForGlyph: glyph transform: xform]; + } + + CGGlyph CTFontGetGlyphWithName(CTFontRef font, CFStringRef glyphName) + { ++ font = normalizedFont(font); + printf("STUB %s\n", __PRETTY_FUNCTION__); + return CGNullGlyph; + } +@@ -287,20 +483,44 @@ CGRect CTFontGetBoundingRectsForGlyphs(CTFontRef font, CTFontOrientation orienta + const CGGlyph *glyphs, CGRect *boundingRects, + CFIndex count) + { +- printf("STUB %s\n", __PRETTY_FUNCTION__); +- return CGRectMake(0, 0, 0, 0); ++ font = normalizedFont(font); ++ if (!font || !glyphs || count <= 0) ++ return CGRectZero; ++ CGFontRef graphicsFont = CTFontCopyGraphicsFont(font, NULL); ++ if (!graphicsFont) ++ return CGRectZero; ++ int units = CGFontGetUnitsPerEm(graphicsFont); ++ CGFloat scale = units > 0 ? CTFontGetSize(font) / units : 0; ++ CGRect total = CGRectNull; ++ for (CFIndex i = 0; i < count; ++i) { ++ O2Rect bounds = [(O2Font *)graphicsFont boundingRectForGlyph:glyphs[i]]; ++ CGRect rect = CGRectMake(bounds.origin.x * scale, bounds.origin.y * scale, ++ bounds.size.width * scale, bounds.size.height * scale); ++ if (boundingRects) boundingRects[i] = rect; ++ total = CGRectUnion(total, rect); ++ } ++ CGFontRelease(graphicsFont); ++ return total; + } + + double CTFontGetAdvancesForGlyphs(CTFontRef font, CTFontOrientation orientation, + const CGGlyph *glyphs, CGSize *advances, + CFIndex count) + { +- [font getAdvancements: advances forGlyphs: glyphs count: count]; +- +- double sum; +- +- for (int i = 0; i < count; i++) { +- sum += advances[i].width; ++ font = normalizedFont(font); ++ if (!font || !glyphs || count <= 0) ++ return 0; ++ if (advances) ++ [(KTFont *)font getAdvancements: advances forGlyphs: glyphs count: count]; ++ double sum = 0; ++ ++ for (CFIndex i = 0; i < count; i++) { ++ CGSize advance; ++ if (advances) ++ advance = advances[i]; ++ else ++ [(KTFont *)font getAdvancements: &advance forGlyphs: &glyphs[i] count: 1]; ++ sum += advance.width; + } + + return sum; +@@ -310,6 +530,7 @@ CGRect CTFontGetOpticalBoundsForGlyphs(CTFontRef font, const CGGlyph *glyphs, + CGRect *boundingRects, CFIndex count, + CFOptionFlags options) + { ++ font = normalizedFont(font); + printf("STUB %s\n", __PRETTY_FUNCTION__); + return CGRectMake(0, 0, 0, 0); + } +@@ -317,29 +538,34 @@ CGRect CTFontGetOpticalBoundsForGlyphs(CTFontRef font, const CGGlyph *glyphs, + void CTFontGetVerticalTranslationsForGlyphs(CTFontRef font, const CGGlyph *glyphs, + CGSize *translations, CFIndex count) + { ++ font = normalizedFont(font); + printf("STUB %s\n", __PRETTY_FUNCTION__); + } + + CFArrayRef CTFontCopyVariationAxes(CTFontRef font) + { ++ font = normalizedFont(font); + printf("STUB %s\n", __PRETTY_FUNCTION__); + return nil; + } + + CFDictionaryRef CTFontCopyVariation(CTFontRef font) + { ++ font = normalizedFont(font); + printf("STUB %s\n", __PRETTY_FUNCTION__); + return nil; + } + + CFArrayRef CTFontCopyFeatures(CTFontRef font) + { ++ font = normalizedFont(font); + printf("STUB %s\n", __PRETTY_FUNCTION__); + return nil; + } + + CFArrayRef CTFontCopyFeatureSettings(CTFontRef font) + { ++ font = normalizedFont(font); + printf("STUB %s\n", __PRETTY_FUNCTION__); + return nil; + } +@@ -347,6 +573,7 @@ CFArrayRef CTFontCopyFeatureSettings(CTFontRef font) + bool CTFontGetGlyphsForCharacters(CTFontRef font, const UniChar *characters, + CGGlyph *glyphs, CFIndex count) + { ++ font = normalizedFont(font); + [font getGlyphs: glyphs forCharacters: characters length: count]; + // FIXME: change getGlyphs: to return a BOOL + return YES; +@@ -355,20 +582,36 @@ bool CTFontGetGlyphsForCharacters(CTFontRef font, const UniChar *characters, + void CTFontDrawGlyphs(CTFontRef font, const CGGlyph *glyphs, const CGPoint *positions, + size_t count, CGContextRef context) + { +- printf("STUB %s\n", __PRETTY_FUNCTION__); ++ font = normalizedFont(font); ++ if (!font || !context || !glyphs || !positions || !count) ++ return; ++ CGFontRef graphicsFont = CTFontCopyGraphicsFont(font, NULL); ++ if (!graphicsFont) ++ return; ++ CGContextSaveGState(context); ++ CGContextSetFont(context, graphicsFont); ++ CGContextSetFontSize(context, CTFontGetSize(font)); ++ CGContextShowGlyphsAtPositions(context, glyphs, positions, count); ++ CGContextRestoreGState(context); ++ CGFontRelease(graphicsFont); + } + + CFIndex CTFontGetLigatureCaretPositions(CTFontRef font, CGGlyph glyph, CGFloat *positions, + CFIndex maxPositions) + { ++ font = normalizedFont(font); + printf("STUB %s\n", __PRETTY_FUNCTION__); + return -1; + } + + CGFontRef CTFontCopyGraphicsFont(CTFontRef font, CTFontDescriptorRef _Nullable *attributes) + { +- printf("STUB %s\n", __PRETTY_FUNCTION__); +- return nil; ++ font = normalizedFont(font); ++ if (attributes) ++ *attributes = nil; ++ if (!font) ++ return nil; ++ return [(KTFont *)font copyGraphicsFont]; + } + + CTFontRef +@@ -381,6 +624,7 @@ CTFontCreateWithGraphicsFont(CGFontRef cgFont, CGFloat size, + + ATSFontRef CTFontGetPlatformFont(CTFontRef font, CTFontDescriptorRef _Nullable *attributes) + { ++ font = normalizedFont(font); + printf("STUB %s\n", __PRETTY_FUNCTION__); + return 0; + } +@@ -402,14 +646,27 @@ CTFontRef CTFontCreateWithQuickdrawInstance(ConstStr255Param name, int16_t ident + + CFArrayRef CTFontCopyAvailableTables(CTFontRef font, CTFontTableOptions options) + { +- printf("STUB %s\n", __PRETTY_FUNCTION__); +- return nil; ++ font = normalizedFont(font); ++ CGFontRef graphicsFont = CTFontCopyGraphicsFont(font, NULL); ++ if (!graphicsFont) ++ return nil; ++ CFArrayRef result = (CFArrayRef)[(O2Font *)graphicsFont copyTableTags]; ++ CGFontRelease(graphicsFont); ++ (void)options; ++ return result; + } + + CFDataRef CTFontCopyTable(CTFontRef font, CTFontTableTag table, CTFontTableOptions options) + { +- printf("STUB %s\n", __PRETTY_FUNCTION__); +- return nil; ++ font = normalizedFont(font); ++ if (!font) ++ return nil; ++ CGFontRef graphicsFont = CTFontCopyGraphicsFont(font, NULL); ++ if (!graphicsFont) ++ return nil; ++ CFDataRef data = CGFontCopyTableForTag(graphicsFont, table); ++ CGFontRelease(graphicsFont); ++ return data; + } + + CFTypeID CTFontGetTypeID(void) +diff --git a/CoreText/CTFontDescriptor.m b/CoreText/CTFontDescriptor.m +index 8866ce59..eb8a101c 100644 +--- a/CoreText/CTFontDescriptor.m ++++ b/CoreText/CTFontDescriptor.m +@@ -24,9 +24,23 @@ const CFStringRef kCTFontEnabledAttribute = CFSTR("NSCTFontEnabledAttribute"); + const CFStringRef kCTFontFormatAttribute = CFSTR("NSCTFontFormatAttribute"); + const CFStringRef kCTFontRegistrationScopeAttribute = CFSTR("NSCTFontRegistrationScopeAttribute"); + const CFStringRef kCTFontPriorityAttribute = CFSTR("NSCTFontPriorityAttribute"); ++const CFStringRef kCTFontCollectionRemoveDuplicatesOption = CFSTR("kCTFontCollectionRemoveDuplicatesOption"); ++ ++CTFontDescriptorRef CTFontDescriptorCreateWithAttributes(CFDictionaryRef attributes) ++{ ++ if (!attributes) ++ return NULL; ++ /* Back descriptors directly by an immutable dictionary copy. */ ++ return (CTFontDescriptorRef)CFDictionaryCreateCopy(NULL, attributes); ++} + + CFTypeRef CTFontDescriptorCopyAttribute(CTFontDescriptorRef descriptor, CFStringRef attribute) + { +- printf("STUB %s\n", __PRETTY_FUNCTION__); +- return nil; ++ CFTypeRef value; ++ if (!descriptor || !attribute) ++ return nil; ++ value = CFDictionaryGetValue((CFDictionaryRef)descriptor, attribute); ++ if (value) ++ CFRetain(value); ++ return value; + } +diff --git a/CoreText/CTFontManager.m b/CoreText/CTFontManager.m +index 943e1b78..5c295cbf 100644 +--- a/CoreText/CTFontManager.m ++++ b/CoreText/CTFontManager.m +@@ -1,17 +1,24 @@ + #import ++#import ++#import ++#import ++#import + + const CFStringRef kCTFontManagerRegisteredFontsChangedNotification = CFSTR("CTFontManagerFontChangedNotification"); + + bool CTFontManagerRegisterGraphicsFont(CGFontRef font, CFErrorRef* error) + { +- printf("STUB %s\n", __PRETTY_FUNCTION__); +- return nil; ++ /* The CGFont (typically from CGFontCreateWithDataProvider, backed by ++ * FreeType) is already fully usable; registration is bookkeeping. ++ * Report success so clients proceed to CTFont creation. */ ++ (void)error; ++ return (font != NULL); + } + + bool CTFontManagerUnregisterGraphicsFont(CGFontRef font, CFErrorRef *error) + { +- printf("STUB %s\n", __PRETTY_FUNCTION__); +- return nil; ++ (void)error; ++ return (font != NULL); + } + + CFArrayRef CTFontManagerCopyAvailableFontFamilyNames(void) +@@ -25,3 +32,118 @@ bool CTFontManagerRegisterFontsForURL(CFURLRef fontURL, CTFontManagerScope scope + printf("STUB %s\n", __PRETTY_FUNCTION__); + return false; + } ++ ++/* Private key under which CreateFontDescriptorFromData stashes the raw ++ * font bytes for CTFontCreateWithFontDescriptor. */ ++static CFStringRef DarlingFontDataKey(void) { ++ static CFStringRef key = NULL; ++ if (!key) ++ key = CFSTR("DarlingFontData"); ++ return key; ++} ++ ++static uint16_t sfnt_u16(const uint8_t *p) { return (uint16_t)((p[0] << 8) | p[1]); } ++static uint32_t sfnt_u32(const uint8_t *p) { return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | p[3]; } ++ ++/* Extract one name-table string (UTF-16BE for Windows platform, MacRoman ++ * for Mac platform). Returns a retained CFString or NULL. */ ++static CFStringRef sfnt_copyName(const uint8_t *data, size_t len, ++ const uint8_t *storage, size_t storageLen, ++ uint16_t platform, uint16_t encoding, ++ uint16_t length, uint16_t offset) { ++ const uint8_t *s; ++ if (offset >= storageLen || length > storageLen - offset) ++ return NULL; ++ (void)len; ++ s = storage + offset; ++ if (platform == 3) { ++ /* Windows Unicode BMP: UTF-16BE */ ++ return CFStringCreateWithBytes(NULL, s, length, kCFStringEncodingUTF16BE, false); ++ } else if (platform == 1 && encoding == 0) { ++ return CFStringCreateWithBytes(NULL, s, length, kCFStringEncodingMacRoman, false); ++ } ++ return NULL; ++} ++ ++CTFontDescriptorRef CTFontManagerCreateFontDescriptorFromData(CFDataRef data) { ++ const uint8_t *bytes; ++ size_t len, i, j; ++ uint16_t numTables, count, stringOffset; ++ const uint8_t *nameTab = NULL; ++ size_t nameLen = 0; ++ CFStringRef psName = NULL, family = NULL, style = NULL; ++ NSMutableDictionary *dict; ++ CTFontDescriptorRef desc; ++ ++ if (!data) ++ return NULL; ++ len = CFDataGetLength(data); ++ bytes = CFDataGetBytePtr(data); ++ numTables = sfnt_u16(bytes + 4); ++ if (12 + (size_t)numTables * 16 > len) ++ return NULL; ++ for (i = 0; i < numTables; i++) { ++ const uint8_t *rec = bytes + 12 + i * 16; ++ if (rec[0] == 'n' && rec[1] == 'a' && rec[2] == 'm' && rec[3] == 'e') { ++ uint32_t off = sfnt_u32(rec + 8), tlen = sfnt_u32(rec + 12); ++ if (off < len && tlen <= len - off) { ++ nameTab = bytes + off; ++ nameLen = tlen; ++ } ++ break; ++ } ++ } ++ if (!nameTab || nameLen < 6) ++ return NULL; ++ if (sfnt_u16(nameTab) != 0) ++ return NULL; ++ count = sfnt_u16(nameTab + 2); ++ stringOffset = sfnt_u16(nameTab + 4); ++ if (6 + (size_t)count * 12 > nameLen || stringOffset > nameLen) ++ return NULL; ++ /* Pass 1: Windows Unicode (3,1,0x409); pass 2: Mac Roman (1,0,0). */ ++ for (int pass = 0; pass < 2 && (!psName || !family || !style); pass++) { ++ for (j = 0; j < count; j++) { ++ const uint8_t *r = nameTab + 6 + j * 12; ++ uint16_t plat = sfnt_u16(r), enc = sfnt_u16(r + 2), lang = sfnt_u16(r + 4); ++ uint16_t nid = sfnt_u16(r + 6), slen = sfnt_u16(r + 8), soff = sfnt_u16(r + 10); ++ CFStringRef *slot = NULL; ++ if (pass == 0 && !(plat == 3 && enc == 1 && lang == 0x409)) ++ continue; ++ if (pass == 1 && !(plat == 1 && enc == 0 && lang == 0)) ++ continue; ++ if (nid == 6 && !psName) slot = &psName; ++ else if (nid == 1 && !family) slot = &family; ++ else if (nid == 2 && !style) slot = &style; ++ else if (nid == 4 && !psName) slot = &psName; ++ else if (nid == 16 && !family) slot = &family; ++ else if (nid == 17 && !style) slot = &style; ++ if (slot) ++ *slot = sfnt_copyName(bytes, len, nameTab + stringOffset, ++ nameLen - stringOffset, plat, enc, slen, soff); ++ } ++ } ++ if (!family && psName) ++ family = (CFStringRef)[(NSString *)psName retain]; ++ if (!family) { ++ if (psName) CFRelease(psName); ++ if (style) CFRelease(style); ++ return NULL; ++ } ++ ++ dict = [NSMutableDictionary dictionary]; ++ [dict setObject:(NSString *)family forKey:(NSString *)kCTFontFamilyNameAttribute]; ++ if (style) ++ [dict setObject:(NSString *)style forKey:(NSString *)kCTFontStyleNameAttribute]; ++ if (psName) ++ [dict setObject:(NSString *)psName forKey:(NSString *)kCTFontNameAttribute]; ++ else ++ [dict setObject:(NSString *)family forKey:(NSString *)kCTFontNameAttribute]; ++ /* Stash raw bytes for CTFontCreateWithFontDescriptor. */ ++ [dict setObject:(NSData *)data forKey:(NSString *)DarlingFontDataKey()]; ++ desc = CTFontDescriptorCreateWithAttributes((CFDictionaryRef)dict); ++ if (psName) CFRelease(psName); ++ if (family) CFRelease(family); ++ if (style) CFRelease(style); ++ return desc; ++} +diff --git a/CoreText/KTFont.m b/CoreText/KTFont.m +index bcf32460..00f64ed8 100644 +--- a/CoreText/KTFont.m ++++ b/CoreText/KTFont.m +@@ -46,6 +46,27 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + return CGFontCopyFullName(_font); + } + ++- (CGFontRef) copyGraphicsFont { ++ return CGFontRetain(_font); ++} ++ ++/* CTFont and NSFont share these identity operations on Quartz fonts. */ ++- (id) printerFont { ++ return self; ++} ++ ++- (id) screenFont { ++ return self; ++} ++ ++- (NSString *) fontName { ++ return [(id)CTFontCopyPostScriptName((CTFontRef)self) autorelease]; ++} ++ ++- (NSString *) familyName { ++ return [(id)CTFontCopyFamilyName((CTFontRef)self) autorelease]; ++} ++ + - (CGFloat) pointSize { + return _size; + } +@@ -55,8 +76,12 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + } + + - (CGRect) boundingRect { +- O2InvalidAbstractInvocation(); +- return CGRectZero; ++ CGRect box = CGFontGetFontBBox(_font); ++ if (_unitsPerEm == 0) ++ return CGRectZero; ++ CGFloat scale = _size / _unitsPerEm; ++ return CGRectMake(box.origin.x * scale, box.origin.y * scale, ++ box.size.width * scale, box.size.height * scale); + } + + - (CGFloat) ascender { +@@ -78,13 +103,13 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + } + + - (CGFloat) underlineThickness { +- O2InvalidAbstractInvocation(); +- return 0; ++ /* FreeType's generic CGFont bridge does not expose underline metrics. ++ * Use the conventional em-relative values expected by CoreText clients. */ ++ return _size / 14.0; + } + + - (CGFloat) underlinePosition { +- O2InvalidAbstractInvocation(); +- return 0; ++ return -_size / 10.0; + } + + - (CGFloat) italicAngle { +@@ -157,8 +182,13 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + - (CGPathRef) createPathForGlyph: (CGGlyph) glyph + transform: (CGAffineTransform *) xform + { +- O2InvalidAbstractInvocation(); +- return nil; ++ /* Keep the CoreText Copy contract even where the generic backend cannot ++ * yet expose a native outline. An empty retained path is a valid glyph ++ * path (e.g. for whitespace) and lets clients fall back to table-based ++ * outline extraction instead of throwing an Objective-C exception. */ ++ (void)glyph; ++ (void)xform; ++ return CGPathCreateMutable(); + } + + @end +diff --git a/CoreText/KTFont_FT.m b/CoreText/KTFont_FT.m +index 33dc18c6..78887dd8 100644 +--- a/CoreText/KTFont_FT.m ++++ b/CoreText/KTFont_FT.m +@@ -60,11 +60,13 @@ + length: (NSUInteger) length + { + O2Font_freetype *o2Font = (O2Font_freetype *) _font; +- FT_Face face = [o2Font face]; ++ @synchronized(o2Font) { ++ FT_Face face = [o2Font face]; + +- int i; +- for (i = 0; i < length; i++) { +- glyphs[i] = FT_Get_Char_Index(face, characters[i]); ++ int i; ++ for (i = 0; i < length; i++) { ++ glyphs[i] = FT_Get_Char_Index(face, characters[i]); ++ } + } + } + +@@ -73,15 +75,17 @@ + count: (NSUInteger) count + { + O2Font_freetype *o2Font = (O2Font_freetype *) _font; +- FT_Face face = [o2Font face]; ++ @synchronized(o2Font) { ++ FT_Face face = [o2Font face]; + +- int i; +- FT_Set_Pixel_Sizes(face, _size, _size); ++ int i; ++ FT_Set_Pixel_Sizes(face, _size, _size); + +- for (i = 0; i < count; i++) { +- FT_Load_Glyph(face, glyphs[i], FT_LOAD_DEFAULT); +- advancements[i] = CGSizeMake(face->glyph->advance.x / (O2Float)(2 << 5), +- face->glyph->advance.y / (O2Float)(2 << 5)); ++ for (i = 0; i < count; i++) { ++ FT_Load_Glyph(face, glyphs[i], FT_LOAD_DEFAULT); ++ advancements[i] = CGSizeMake(face->glyph->advance.x / (O2Float)(2 << 5), ++ face->glyph->advance.y / (O2Float)(2 << 5)); ++ } + } + } + +@@ -90,18 +94,20 @@ + isNominal: (BOOL *) isNominalp + { + O2Font_freetype *o2Font = (O2Font_freetype *) _font; +- FT_Face face = [o2Font face]; ++ @synchronized(o2Font) { ++ FT_Face face = [o2Font face]; + +- *isNominalp = YES; ++ *isNominalp = YES; + +- if (!current) +- return NSZeroPoint; ++ if (!current) ++ return NSZeroPoint; + +- FT_Set_Pixel_Sizes(face, _size, _size); ++ FT_Set_Pixel_Sizes(face, _size, _size); + +- FT_Load_Glyph(face, current, FT_LOAD_DEFAULT); +- return NSMakePoint(face->glyph->advance.x / (O2Float)(2 << 5), +- face->glyph->advance.y / (O2Float)(2 << 5)); ++ FT_Load_Glyph(face, current, FT_LOAD_DEFAULT); ++ return NSMakePoint(face->glyph->advance.x / (O2Float)(2 << 5), ++ face->glyph->advance.y / (O2Float)(2 << 5)); ++ } + } + + @end +diff --git a/CoreText/include/CoreText/CTFontDescriptor.h b/CoreText/include/CoreText/CTFontDescriptor.h +index b2688960..61dfef1a 100644 +--- a/CoreText/include/CoreText/CTFontDescriptor.h ++++ b/CoreText/include/CoreText/CTFontDescriptor.h +@@ -24,6 +24,19 @@ CORETEXT_EXPORT const CFStringRef kCTFontEnabledAttribute; + CORETEXT_EXPORT const CFStringRef kCTFontFormatAttribute; + CORETEXT_EXPORT const CFStringRef kCTFontRegistrationScopeAttribute; + CORETEXT_EXPORT const CFStringRef kCTFontPriorityAttribute; ++CORETEXT_EXPORT const CFStringRef kCTFontSlantTrait; ++CORETEXT_EXPORT const CFStringRef kCTFontWeightTrait; ++CORETEXT_EXPORT const CFStringRef kCTFontWidthTrait; ++CORETEXT_EXPORT const CFStringRef kCTFontSymbolicTrait; ++CORETEXT_EXPORT const CFStringRef kCTFontVariationAxisIdentifierKey; ++CORETEXT_EXPORT const CFStringRef kCTFontVariationAxisMinimumValueKey; ++CORETEXT_EXPORT const CFStringRef kCTFontVariationAxisMaximumValueKey; ++CORETEXT_EXPORT const CFStringRef kCTFontVariationAxisDefaultValueKey; ++CORETEXT_EXPORT const CFStringRef kCTFontFamilyNameKey; ++CORETEXT_EXPORT const CFStringRef kCTFontFullNameKey; ++CORETEXT_EXPORT const CFStringRef kCTFontPostScriptNameKey; ++CORETEXT_EXPORT const CFStringRef kCTFontCollectionRemoveDuplicatesOption; ++CORETEXT_EXPORT const CFStringRef kCTFontManagerRegisteredFontsChangedNotification; + + CF_IMPLICIT_BRIDGING_ENABLED + +@@ -39,5 +52,6 @@ typedef enum CTFontOrientation : uint32_t { + } CTFontOrientation; + + CORETEXT_EXPORT CFTypeRef CTFontDescriptorCopyAttribute(CTFontDescriptorRef descriptor, CFStringRef attribute); ++CORETEXT_EXPORT CTFontDescriptorRef CTFontDescriptorCreateWithAttributes(CFDictionaryRef attributes); + + CF_IMPLICIT_BRIDGING_DISABLED +diff --git a/CoreText/include/CoreText/KTFont.h b/CoreText/include/CoreText/KTFont.h +index 46a5bab0..e2170bc5 100644 +--- a/CoreText/include/CoreText/KTFont.h ++++ b/CoreText/include/CoreText/KTFont.h +@@ -35,6 +35,7 @@ enum { CGNullGlyph = 0x0 }; + language: (NSString *) language; + + - (CFStringRef) copyName; ++- (CGFontRef) copyGraphicsFont; + - (CGFloat) pointSize; + - (CGFloat) fontSize; + +diff --git a/CoreText/stubs.c b/CoreText/stubs.c +deleted file mode 100644 +index 1287daa5..00000000 +--- a/CoreText/stubs.c ++++ /dev/null +@@ -1,6 +0,0 @@ +-#include +- +-void *CTFontDescriptorCreateWithAttributes(void) { +- printf("STUB %s\n", __PRETTY_FUNCTION__); +- return NULL; +-} +diff --git a/Onyx2D/O2Context_builtin_FT.m b/Onyx2D/O2Context_builtin_FT.m +index a8bf3fa6..59f3c45b 100644 +--- a/Onyx2D/O2Context_builtin_FT.m ++++ b/Onyx2D/O2Context_builtin_FT.m +@@ -92,6 +92,12 @@ static void renderFreeTypeBitmap(O2Context_builtin_FT *self, O2Surface *surface, + FT_Bitmap *bitmap, NSInteger x, NSInteger y, + O2Paint *paint) + { ++ if (!surface->_read_argb8u) { ++ NSLog(@"Glyph surface unsupported: bpc=%zu bpp=%zu info=0x%x reader=%p", ++ O2ImageGetBitsPerComponent(surface), O2ImageGetBitsPerPixel(surface), ++ O2ImageGetBitmapInfo(surface), surface->_read_argb8u); ++ return; ++ } + // Size of the bitmap. + NSInteger fullWidth = bitmap->width; + NSInteger fullHeight = bitmap->rows; +@@ -123,7 +129,7 @@ static void renderFreeTypeBitmap(O2Context_builtin_FT *self, O2Surface *surface, + + // We're going to advance these pointers as we move along this row. + unsigned char *coverage = +- bitmap->buffer + (curY - y) * fullWidth + (curX - x); ++ bitmap->buffer + (curY - y) * bitmap->pitch + (curX - x); + O2argb8u *src = srcBuffer; + O2argb8u *dst = dstBuffer; + +@@ -190,57 +196,61 @@ static void renderFreeTypeBitmap(O2Context_builtin_FT *self, O2Surface *surface, + [self establishFontStateInDeviceIfDirty]; + + O2Font_freetype *font = (O2Font_freetype *) gState->_font; +- FT_Face face = [font face]; +- +- int i; +- FT_Error ftError; +- +- if (face == NULL) { +- NSLog(@"face is NULL"); +- O2SurfaceUnlock(_surface); +- return; +- } ++ /* FT_Load_Glyph replaces the shared face glyph slot and bitmap. ++ * Hold the same font lock used by metric queries through rasterization. */ ++ @synchronized(font) { ++ FT_Face face = [font face]; ++ ++ int i; ++ FT_Error ftError; ++ ++ if (face == NULL) { ++ NSLog(@"face is NULL"); ++ O2SurfaceUnlock(_surface); ++ return; ++ } + +- FT_GlyphSlot slot = face->glyph; ++ FT_GlyphSlot slot = face->glyph; + +- if ((ftError = +- FT_Set_Char_Size(face, 0, fontSize.height * 64, 72.0, 72.0))) { +- NSLog(@"FT_Set_Char_Size returned %d", ftError); +- O2SurfaceUnlock(_surface); +- return; +- } ++ if ((ftError = ++ FT_Set_Char_Size(face, 0, fontSize.height * 64, 72.0, 72.0))) { ++ NSLog(@"FT_Set_Char_Size returned %d", ftError); ++ O2SurfaceUnlock(_surface); ++ return; ++ } + +- for (i = 0; i < count; i++) { ++ for (i = 0; i < count; i++) { + +- ftError = FT_Load_Glyph(face, glyphs[i], FT_LOAD_DEFAULT); +- if (ftError) +- continue; ++ ftError = FT_Load_Glyph(face, glyphs[i], FT_LOAD_DEFAULT); ++ if (ftError) ++ continue; + +- ftError = FT_Render_Glyph(face->glyph, FT_RENDER_MODE_NORMAL); +- if (ftError) +- continue; ++ ftError = FT_Render_Glyph(face->glyph, FT_RENDER_MODE_NORMAL); ++ if (ftError) ++ continue; + +- renderFreeTypeBitmap(self, _surface, &slot->bitmap, +- point.x + slot->bitmap_left, +- point.y - slot->bitmap_top, paint); ++ renderFreeTypeBitmap(self, _surface, &slot->bitmap, ++ point.x + slot->bitmap_left, ++ point.y - slot->bitmap_top, paint); + +- point.x += slot->advance.x >> 6; +- } ++ point.x += slot->advance.x >> 6; ++ } + +- O2PaintRelease(paint); ++ O2PaintRelease(paint); + +- int glyphAdvances[count]; +- O2Float unitsPerEm = O2FontGetUnitsPerEm(font); ++ int glyphAdvances[count]; ++ O2Float unitsPerEm = O2FontGetUnitsPerEm(font); + +- O2FontGetGlyphAdvances(font, glyphs, count, glyphAdvances); ++ O2FontGetGlyphAdvances(font, glyphs, count, glyphAdvances); + +- O2Float total = 0; ++ O2Float total = 0; + +- for (i = 0; i < count; i++) +- total += glyphAdvances[i]; ++ for (i = 0; i < count; i++) ++ total += glyphAdvances[i]; + +- total = (total / O2FontGetUnitsPerEm(font)) * gState->_pointSize; ++ total = (total / O2FontGetUnitsPerEm(font)) * gState->_pointSize; + ++ } /* font lock */ + O2SurfaceUnlock(_surface); + } + +diff --git a/Onyx2D/O2DataProvider.m b/Onyx2D/O2DataProvider.m +index 8e80cddb..386fee81 100644 +--- a/Onyx2D/O2DataProvider.m ++++ b/Onyx2D/O2DataProvider.m +@@ -97,6 +97,10 @@ void O2DataProviderRelease(O2DataProviderRef self) { + } + + CFDataRef O2DataProviderCopyData(O2DataProviderRef self) { ++ /* darling-lol M57: callers (CoreGraphics/QuartzCore) can pass a NULL ++ * provider; dereferencing it crashed with addr=0x10. */ ++ if (self == NULL) ++ return NULL; + if (self->_data != nil) + return (CFDataRef)[self->_data copy]; + else +diff --git a/Onyx2D/O2Font.m b/Onyx2D/O2Font.m +index 1f72ccef..8891602a 100644 +--- a/Onyx2D/O2Font.m ++++ b/Onyx2D/O2Font.m +@@ -21,8 +21,25 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + #import + + @implementation O2Font ++ ++- (NSString *) copyPostScriptName { ++ return [_name copy]; ++} ++ ++- (NSString *) copyFamilyName { ++ return [_name copy]; ++} ++ ++- (NSArray *) copyTableTags { ++ return [[NSArray alloc] init]; ++} ++ + static NSArray *_preferredFontNames = nil; + ++- (O2Rect) boundingRectForGlyph: (O2Glyph) glyph { ++ return _bbox; ++} ++ + + (void) initialize { + if (self == [O2Font class]) { + // Set some decent defaut substitution fonts +diff --git a/Onyx2D/O2Font_freetype.m b/Onyx2D/O2Font_freetype.m +index e21cd7d5..1c7c737d 100644 +--- a/Onyx2D/O2Font_freetype.m ++++ b/Onyx2D/O2Font_freetype.m +@@ -4,6 +4,55 @@ + + @implementation O2Font_freetype + ++- (NSString *) copyPostScriptName { ++ const char *name = FT_Get_Postscript_Name(_face); ++ return name ? [[NSString alloc] initWithUTF8String:name] : [super copyPostScriptName]; ++} ++ ++- (NSString *) copyFamilyName { ++ return _face->family_name ? [[NSString alloc] initWithUTF8String:_face->family_name] : [super copyFamilyName]; ++} ++ ++- (NSArray *) copyTableTags { ++ NSMutableArray *tags = [[NSMutableArray alloc] init]; ++ @synchronized(self) { ++ for (FT_UInt index = 0; ; ++index) { ++ FT_ULong tag, length; ++ if (FT_Sfnt_Table_Info(_face, index, &tag, &length) != 0) ++ break; ++ [tags addObject:[NSNumber numberWithUnsignedLong:tag]]; ++ } ++ } ++ return tags; ++} ++ ++ ++- (O2Rect) boundingRectForGlyph: (O2Glyph) glyph { ++ @synchronized(self) { ++ if (FT_Load_Glyph(_face, glyph, FT_LOAD_NO_SCALE | FT_LOAD_IGNORE_TRANSFORM) != 0) ++ return O2RectMake(0, 0, 0, 0); ++ FT_Glyph_Metrics metrics = _face->glyph->metrics; ++ return O2RectMake(metrics.horiBearingX, metrics.horiBearingY - metrics.height, ++ metrics.width, metrics.height); ++ } ++} ++ ++- (NSData *) copyTableForTag: (uint32_t) tag { ++ FT_ULong length = 0; ++ if (FT_Load_Sfnt_Table(_face, tag, 0, NULL, &length) != 0 || length == 0) ++ return nil; ++ unsigned char *bytes = malloc(length); ++ if (!bytes) ++ return nil; ++ if (FT_Load_Sfnt_Table(_face, tag, 0, bytes, &length) != 0) { ++ free(bytes); ++ return nil; ++ } ++ NSData *result = [[NSData alloc] initWithBytes:bytes length:length]; ++ free(bytes); ++ return result; ++} ++ + #ifdef DARLING + + O2FontRef O2FontCreateWithFontName_platform(NSString *name) { +@@ -208,6 +257,7 @@ FT_Face O2FontFreeTypeFace(O2Font_freetype *self) { + } + + - (void) fetchAdvances { ++ @synchronized(self) { + FT_Set_Char_Size(_face, 0, _unitsPerEm * 64, 72, 72); + + _advances = NSZoneMalloc(NULL, sizeof(NSInteger) * _numberOfGlyphs); +@@ -217,6 +267,7 @@ FT_Face O2FontFreeTypeFace(O2Font_freetype *self) { + + _advances[glyph] = _face->glyph->advance.x / (O2Float)(2 << 5); + } ++ } + } + + - (O2Glyph) glyphWithGlyphName: (NSString *) name { +diff --git a/Onyx2D/O2Surface.m b/Onyx2D/O2Surface.m +index e96a03a3..d1f0e087 100644 +--- a/Onyx2D/O2Surface.m ++++ b/Onyx2D/O2Surface.m +@@ -294,6 +294,18 @@ static void O2SurfaceWrite_argb8u_to_BGRA8888(O2Surface *self, int x, int y, + } + } + ++static void O2SurfaceWrite_argb8u_to_XRGB8888(O2Surface *self, int x, int y, ++ O2argb8u *span, int length) ++{ ++ uint8_t *scanline = self->_pixelBytes + y * self->_bytesPerRow + x * 4; ++ for (int i = 0; i < length; ++i) { ++ *scanline++ = 255; ++ *scanline++ = span[i].r; ++ *scanline++ = span[i].g; ++ *scanline++ = span[i].b; ++ } ++} ++ + static void O2SurfaceWrite_argb32f_to_RGBA4444(O2Surface *self, int x, int y, + O2argb32f *span, int length) + { +@@ -424,6 +436,7 @@ static BOOL initFunctionsForParameters(O2Surface *self, size_t bitsPerComponent, + case kO2ImageAlphaNone: + break; + ++ case kO2ImageAlphaNoneSkipLast: + case kO2ImageAlphaLast: + case kO2ImageAlphaPremultipliedLast: + switch (bitmapInfo & kO2BitmapByteOrderMask) { +@@ -457,10 +470,18 @@ static BOOL initFunctionsForParameters(O2Surface *self, size_t bitsPerComponent, + case kO2ImageAlphaFirst: + break; + +- case kO2ImageAlphaNoneSkipLast: +- break; +- + case kO2ImageAlphaNoneSkipFirst: ++ switch (bitmapInfo & kO2BitmapByteOrderMask) { ++ case kO2BitmapByteOrderDefault: ++ case kO2BitmapByteOrder16Little: ++ case kO2BitmapByteOrder32Little: ++ self->_writeargb8u = O2SurfaceWrite_argb8u_to_BGRA8888; ++ return YES; ++ case kO2BitmapByteOrder16Big: ++ case kO2BitmapByteOrder32Big: ++ self->_writeargb8u = O2SurfaceWrite_argb8u_to_XRGB8888; ++ return YES; ++ } + break; + } + } else if ([colorSpace type] == kO2ColorSpaceModelCMYK) { +diff --git a/Onyx2D/include/Onyx2D/O2Font.h b/Onyx2D/include/Onyx2D/O2Font.h +index c2e06958..197aa09c 100644 +--- a/Onyx2D/include/Onyx2D/O2Font.h ++++ b/Onyx2D/include/Onyx2D/O2Font.h +@@ -81,7 +81,11 @@ typedef enum { + - initWithFontName: (NSString *) name; + - initWithDataProvider: (O2DataProviderRef) provider; + ++- (NSArray *) copyTableTags; ++- (NSString *) copyPostScriptName; ++- (NSString *) copyFamilyName; + - (NSData *) copyTableForTag: (uint32_t) tag; ++- (O2Rect) boundingRectForGlyph: (O2Glyph) glyph; + + - (O2Glyph) glyphWithGlyphName: (NSString *) name; + - (NSString *) copyGlyphNameForGlyph: (O2Glyph) glyph; +diff --git a/Onyx2D/include/Onyx2D/O2Font_freetype.h b/Onyx2D/include/Onyx2D/O2Font_freetype.h +index 039d8c94..b518c961 100644 +--- a/Onyx2D/include/Onyx2D/O2Font_freetype.h ++++ b/Onyx2D/include/Onyx2D/O2Font_freetype.h +@@ -10,6 +10,7 @@ + #import + #import FT_FREETYPE_H + #import FT_RENDER_H ++#import FT_TRUETYPE_TABLES_H + + #import + +diff --git a/QuartzCore/CALayer.m b/QuartzCore/CALayer.m +index 5dcac115..7cc63b0b 100644 +--- a/QuartzCore/CALayer.m ++++ b/QuartzCore/CALayer.m +@@ -1,8 +1,234 @@ + #import ++#import ++#import + #import + #import + #import + #import ++#import ++#import ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++/* Darling has no WindowServer/CAContext service to carry Chromium's remote ++ * layer tree from its GPU process to AppKit. Keep the latest IOSurface in a ++ * host-visible seqlocked slot; the X11 AppKit backend consumes this slot. ++ * This is intentionally pixel transport, not guest object transport: guest ++ * objects and globals are not coherent between Darling processes. */ ++#define DARLING_CA_FRAME_MAGIC 0x43414652u /* "CAFR" */ ++#define DARLING_CA_FRAME_BUFFERED_MAGIC 0x43414632u /* "CAF2" */ ++#define DARLING_CA_FRAME_BACK_BUFFER 0x40000000u ++#define DARLING_CA_FRAME_CAPACITY (64u * 1024u * 1024u) ++typedef struct { ++ uint32_t magic; ++ volatile uint32_t sequence; ++ uint32_t width; ++ uint32_t height; ++ uint32_t bytesPerRow; ++ uint32_t pixelFormat; ++ uint64_t dataSize; ++} DarlingCARemoteFrame; ++ ++static pthread_key_t committedSurfaceKey; ++static pthread_key_t softwareSurfaceKey; ++static pthread_once_t committedSurfaceOnce = PTHREAD_ONCE_INIT; ++static void releaseCommittedSurface(void *surface) { CFRelease(surface); } ++static void createCommittedSurfaceKey(void) { ++ pthread_key_create(&committedSurfaceKey, releaseCommittedSurface); ++ pthread_key_create(&softwareSurfaceKey, releaseCommittedSurface); ++} ++static void rememberCommittedSurface(IOSurfaceRef surface) { ++ pthread_once(&committedSurfaceOnce, createCommittedSurfaceKey); ++ IOSurfaceRef previous = pthread_getspecific(committedSurfaceKey); ++ if (previous != surface) { ++ CFRetain(surface); ++ pthread_setspecific(committedSurfaceKey, surface); ++ if (previous) CFRelease(previous); ++ } ++} ++static pthread_mutex_t remoteFrameLock = PTHREAD_MUTEX_INITIALIZER; ++static void publishRemoteIOSurfaceImpl(IOSurfaceRef surface, BOOL readback) { ++ static DarlingCARemoteFrame *slot; ++ static int fd = -1; ++ /* The X11 frame protocol carries packed32-bit pixels. Planar decoder ++ * buffers belong to the GPU video upload path, never whole-window frames. */ ++ if(IOSurfaceGetPlaneCount(surface)>1 || IOSurfaceGetBytesPerElement(surface)!=4){ ++ static unsigned ignored; ++ if(getenv("DARLING_CA_FRAME_TRACE") && ignored++<8) ++ fprintf(stderr,"CA_FRAME_UNSUPPORTED planes=%zu bpe=%zu size=%zux%zu\n",IOSurfaceGetPlaneCount(surface),IOSurfaceGetBytesPerElement(surface),IOSurfaceGetWidth(surface),IOSurfaceGetHeight(surface)); ++ return; ++ } ++ size_t width = IOSurfaceGetWidth(surface); ++ size_t height = IOSurfaceGetHeight(surface); ++ size_t rowBytes = IOSurfaceGetBytesPerRow(surface); ++ size_t dataSize = rowBytes * height; ++ void *base = IOSurfaceGetBaseAddress(surface); ++ if (!base || !width || !height || !rowBytes || ++ dataSize > DARLING_CA_FRAME_CAPACITY) ++ return; ++ ++ /* Readback may itself finish a software write and enter this publisher. ++ * Do it before taking the frame lock; serialize only file initialization ++ * and snapshot publication across threads in this process. */ ++ pthread_once(&committedSurfaceOnce, createCommittedSurfaceKey); ++ IOSurfaceRef softwareSurface = pthread_getspecific(softwareSurfaceKey); ++ if (!readback && softwareSurface != surface) { ++ CFRetain(surface); ++ pthread_setspecific(softwareSurfaceKey, surface); ++ if (softwareSurface) CFRelease(softwareSurface); ++ softwareSurface = surface; ++ } ++ BOOL softwarePixels = softwareSurface == surface; ++ /* Only the process/context that completed GPU readback owns fresh pixels. ++ * A browser snapshot of shared backing may race or trail that publisher. */ ++ if(readback && !softwarePixels && getenv("RCLOL_GPU_DIRECT_PRESENT")){ ++ typedef int (*PresentFn)(IOSurfaceRef); ++ PresentFn present=(PresentFn)dlsym(RTLD_DEFAULT,"CGLPresentIOSurface"); ++ if(present && present(surface)){rememberCommittedSurface(surface);return;} ++ } ++ if (readback && !softwarePixels && CGLReadbackIOSurface(surface) != kCGLNoError) ++ return; ++ softwarePixels = pthread_getspecific(softwareSurfaceKey) == surface; ++ pthread_mutex_lock(&remoteFrameLock); ++ if (!slot) { ++ char path[128]; ++ /* darling-lol M58c: one slot file per PUBLISHER process. A single ++ * per-uid slot was raced by every publishing process (GPU layer tree ++ * + browser DisplayCALayerTree), corrupting the odd/even seqlock for ++ * the consumer: 655/663 probes read a torn/unstable frame. Per-pid ++ * slots keep each file single-writer, so the seqlock holds. */ ++ snprintf(path, sizeof(path), "/dev/shm/darling-ca-frame-%u-%d", ++ getuid(), (int) getpid()); ++ fd = open(path, O_CREAT | O_RDWR, 0600); ++ if (fd < 0 || ftruncate(fd, sizeof(*slot) + DARLING_CA_FRAME_CAPACITY)) { ++ pthread_mutex_unlock(&remoteFrameLock); ++ return; ++ } ++ slot = mmap(NULL, sizeof(*slot) + DARLING_CA_FRAME_CAPACITY, ++ PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); ++ if (slot == MAP_FAILED) { ++ slot = NULL; ++ pthread_mutex_unlock(&remoteFrameLock); ++ return; ++ } ++ ++ /* darling-lol M58c: reap slot files left by dead publishers ++ * (64 MB each). Slot names end in the publishing guest pid. */ ++ { ++ DIR *dir = opendir("/dev/shm"); ++ char prefix[32]; ++ snprintf(prefix, sizeof(prefix), "darling-ca-frame-%u-", getuid()); ++ size_t prefixLen = strlen(prefix); ++ if (dir) { ++ struct dirent *ent; ++ while ((ent = readdir(dir)) != NULL) { ++ if (strncmp(ent->d_name, prefix, prefixLen) != 0) ++ continue; ++ int stalePid = atoi(ent->d_name + prefixLen); ++ if (stalePid > 0 && stalePid != (int) getpid() && ++ kill(stalePid, 0) != 0 && errno == ESRCH) { ++ char stale[128]; ++ snprintf(stale, sizeof(stale), "/dev/shm/%s", ++ ent->d_name); ++ unlink(stale); ++ } ++ } ++ closedir(dir); ++ } ++ } ++ } ++ ++ rememberCommittedSurface(surface); ++ ++ /* CGLTexImageIOSurface2D's Mesa fallback cannot provide true shared GL ++ * storage. Materialize the completed compositor texture first. */ ++ IOSurfaceLock(surface, kIOSurfaceLockReadOnly, NULL); ++ uint32_t sequence = slot->sequence; ++ BOOL buffered = dataSize <= DARLING_CA_FRAME_CAPACITY / 2; ++ size_t offset = buffered && !(slot->magic == DARLING_CA_FRAME_BUFFERED_MAGIC && ++ (slot->pixelFormat & DARLING_CA_FRAME_BACK_BUFFER)) ++ ? DARLING_CA_FRAME_CAPACITY / 2 : 0; ++ /* Keep the completed front buffer readable during the bulk copy. Only ++ * publishing its new header needs the short seqlock critical section. ++ * Large legacy frames still use one buffer and must invalidate first. */ ++ if (!buffered || (slot->magic == DARLING_CA_FRAME_MAGIC && ++ slot->dataSize > DARLING_CA_FRAME_CAPACITY / 2)) { ++ slot->sequence = (sequence + 1) | 1; ++ __sync_synchronize(); ++ } ++ memcpy((char *)(slot + 1) + offset, base, dataSize); ++ __sync_synchronize(); ++ slot->sequence = (sequence + 1) | 1; ++ __sync_synchronize(); ++ slot->magic = buffered ? DARLING_CA_FRAME_BUFFERED_MAGIC : DARLING_CA_FRAME_MAGIC; ++ slot->width = (uint32_t)width; ++ slot->height = (uint32_t)height; ++ slot->bytesPerRow = (uint32_t)rowBytes; ++ /* This is an internal transport header; two upper bits carry provenance ++ * and buffer location rather than the original IOSurface fourcc. */ ++ slot->pixelFormat = ((uint32_t)IOSurfaceGetPixelFormat(surface) & 0x3fffffffu) | ++ (softwarePixels ? 0x80000000u : 0) | ++ (offset ? DARLING_CA_FRAME_BACK_BUFFER : 0); ++ slot->dataSize = dataSize; ++ __sync_synchronize(); ++ slot->sequence = (sequence + 2) & ~1u; ++ IOSurfaceUnlock(surface, kIOSurfaceLockReadOnly, NULL); ++ futimes(fd, NULL); /* mmap writes alone do not refresh publisher ordering */ ++ ++ /* darling-lol M58c diagnostic (opt-in via M58_PRES_TRACE): publisher ++ * cadence. p=publish, P=publish whose first 8 rows are all-zero, ++ * f=first publish in this process (carries the size). */ ++ if (getenv("M58_PRES_TRACE")) { ++ static FILE *plog; ++ if (!plog) { ++ char ppath[128]; ++ snprintf(ppath, sizeof(ppath), ++ "/dev/shm/m58c-publish-%d", (int) getpid()); ++ plog = fopen(ppath, "w"); ++ if (plog) ++ fprintf(plog, ++ "pid=%d %zux%zu first\n", (int) getpid(), width, height); ++ } ++ if (plog) { ++ BOOL blank = YES; ++ size_t scan = 8 * rowBytes; ++ const unsigned char *p8 = (const unsigned char *) base; ++ for (size_t i = 0; i + 3 < scan && blank; i += 4) ++ if (p8[i] | p8[i + 1] | p8[i + 2]) ++ blank = NO; ++ fputc(blank ? 'P' : 'p', plog); ++ fflush(plog); ++ } ++ } ++ pthread_mutex_unlock(&remoteFrameLock); ++} ++ ++static void publishRemoteIOSurface(IOSurfaceRef surface) { ++ publishRemoteIOSurfaceImpl(surface, YES); ++} ++ ++/* Software paint already wrote the backing pixels: do not replace them with ++ * a GL texture readback. IOSurface calls this only after a write unlock. */ ++void DarlingCAPublishIOSurfacePixels(IOSurfaceRef surface) { ++ publishRemoteIOSurfaceImpl(surface, NO); ++} ++ ++/* Remote CA reuses a layer and its IOSurface across frames. GPU writes do ++ * not necessarily call setContents again; refresh pixels at outer commit. */ ++void DarlingCAPublishCommittedIOSurface(void) { ++ pthread_once(&committedSurfaceOnce, createCommittedSurfaceKey); ++ IOSurfaceRef surface = pthread_getspecific(committedSurfaceKey); ++ if (surface) publishRemoteIOSurface(surface); ++} + + NSString *const kCAFilterLinear = @"linear"; + NSString *const kCAFilterNearest = @"nearest"; +@@ -61,13 +287,22 @@ NSString *const kCAContentsFormatGray8Uint = @"Gray8"; + } + + - (void) setSublayers: (NSArray *) sublayers { ++ NSArray *oldSublayers = _sublayers; + sublayers = [sublayers copy]; +- [_sublayers release]; ++ /* A child only retains its parent through the parent's array. Clear ++ * its non-owning back pointers before dropping the old array. */ ++ for (CALayer *child in oldSublayers) { ++ if ([sublayers indexOfObjectIdenticalTo: child] == NSNotFound) { ++ [child _setSuperLayer: nil]; ++ [child _setContext: nil]; ++ } ++ } + _sublayers = sublayers; + [_sublayers makeObjectsPerformSelector: @selector(_setSuperLayer:) + withObject: self]; + [_sublayers makeObjectsPerformSelector: @selector(_setContext:) + withObject: _context]; ++ [oldSublayers release]; + } + + - (id) delegate { +@@ -171,14 +406,73 @@ NSString *const kCAContentsFormatGray8Uint = @"Gray8"; + _opaque = value; + } + ++/* darling-lol M57: Chromium sets a layer's geometryFlipped flag when hosting ++ * web content. */ ++- (BOOL) geometryFlipped { ++ return _geometryFlipped; ++} ++ ++- (void) setGeometryFlipped: (BOOL) value { ++ _geometryFlipped = value; ++} ++ + - (id) contents { + return _contents; + } + + - (void) setContents: (id) value { ++ /* darling-lol M57: a live compositor (e.g. Chromium) replaces a layer's ++ * contents every frame. Drop the cached GL texture so CARenderer uploads ++ * the new image instead of reusing the first (possibly empty) one, then ++ * present the updated tree (Darling's NSTimer-driven frame loop does not ++ * fire for this context). */ ++ BOOL needsPresent = NO; ++ if (_context != nil && _contents != value) { ++ [_context deleteTextureId: _textureId]; ++ [_textureId release]; ++ _textureId = nil; ++ needsPresent = YES; ++ } + value = [value retain]; + [_contents release]; + _contents = value; ++ ++ /* Remote CA trees can have a context without a local drawable. Their ++ * IOSurface still needs a pixel handoff to the X11 consumer. */ ++ if (value != nil && ++ CFGetTypeID((CFTypeRef)value) == IOSurfaceGetTypeID()) ++ if(getenv("DARLING_CA_PRESENT_AT_COMMIT"))rememberCommittedSurface((IOSurfaceRef)value); ++ else publishRemoteIOSurface((IOSurfaceRef)value); ++ ++ if (needsPresent) { ++ /* darling-lol M57 diagnostic (opt-in): report what kind of contents ++ * Chromium delivers (IOSurface == a real compositor frame). */ ++ if (getenv("M57_QC_TRACE") != 0) { ++ BOOL isSurf = (value != nil && ++ CFGetTypeID((CFTypeRef)value) == IOSurfaceGetTypeID()); ++ NSLog(@"M57SETCONTENTS class=%s iosurface=%d ctx=%p", ++ value ? object_getClassName(value) : "nil", (int)isSurf, _context); ++ } ++ [_context render]; ++ [_context flush]; ++ } ++} ++ ++- (void) setContentsChanged { ++ /* Chromium can reuse an IOSurface and update its pixels without replacing ++ * the contents object. Invalidate the cached texture and publish the new ++ * pixels just as for a contents replacement. */ ++ [_context deleteTextureId: _textureId]; ++ [_textureId release]; ++ _textureId = nil; ++ if (_contents != nil && ++ CFGetTypeID((CFTypeRef)_contents) == IOSurfaceGetTypeID()) ++ if(getenv("DARLING_CA_PRESENT_AT_COMMIT"))rememberCommittedSurface((IOSurfaceRef)_contents); ++ else publishRemoteIOSurface((IOSurfaceRef)_contents); ++ if (_context != nil) { ++ [_context render]; ++ [_context flush]; ++ } + } + + - (CATransform3D) transform { +@@ -236,6 +530,10 @@ NSString *const kCAContentsFormatGray8Uint = @"Gray8"; + } + + - (void) dealloc { ++ for (CALayer *child in _sublayers) { ++ [child _setSuperLayer: nil]; ++ [child _setContext: nil]; ++ } + [_sublayers release]; + [_animations release]; + [_minificationFilter release]; +@@ -258,6 +556,33 @@ NSString *const kCAContentsFormatGray8Uint = @"Gray8"; + [self setSublayers: [_sublayers arrayByAddingObject: layer]]; + } + ++- (void) insertSublayer: (CALayer *) layer atIndex: (unsigned) index { ++ if (!layer) ++ return; ++ [layer removeFromSuperlayer]; ++ NSMutableArray *layers = [_sublayers mutableCopy]; ++ NSUInteger boundedIndex = index > [layers count] ? [layers count] : index; ++ [layers insertObject: layer atIndex: boundedIndex]; ++ [self setSublayers: layers]; ++ [layers release]; ++} ++ ++- (void) insertSublayer: (CALayer *) layer below: (CALayer *) sibling { ++ NSUInteger index = [_sublayers indexOfObjectIdenticalTo: sibling]; ++ if (index == NSNotFound) ++ index = 0; ++ [self insertSublayer: layer atIndex: (unsigned)index]; ++} ++ ++- (void) insertSublayer: (CALayer *) layer above: (CALayer *) sibling { ++ NSUInteger index = [_sublayers indexOfObjectIdenticalTo: sibling]; ++ if (index == NSNotFound) ++ index = [_sublayers count]; ++ else ++ index++; ++ [self insertSublayer: layer atIndex: (unsigned)index]; ++} ++ + - (void) replaceSublayer: (CALayer *) layer with: (CALayer *) other { + NSMutableArray *layers = [_sublayers mutableCopy]; + NSUInteger index = [_sublayers indexOfObjectIdenticalTo: layer]; +@@ -298,9 +623,14 @@ NSString *const kCAContentsFormatGray8Uint = @"Gray8"; + } + + - (void) removeFromSuperlayer { +- [_superlayer _removeSublayer: self]; ++ /* Removing the child from its parent's array may release the last ++ * external reference to this layer. Keep it alive until cleanup ends. */ ++ [self retain]; ++ CALayer *parent = _superlayer; + _superlayer = nil; ++ [parent _removeSublayer: self]; + [self _setContext: nil]; ++ [self release]; + } + + - (void) setNeedsDisplay { +@@ -319,6 +649,19 @@ NSString *const kCAContentsFormatGray8Uint = @"Gray8"; + [_context startTimerIfNeeded]; + } + ++/* darling-lol M57: used by CALayerContext to decide whether the on-demand frame ++ * timer should keep re-arming. With no animations there is no reason to render ++ * continuously; contents changes drive presentation directly. */ ++- (BOOL) _treeHasAnimations { ++ if ([_animations count] > 0) ++ return YES; ++ for (CALayer *child in _sublayers) { ++ if ([child _treeHasAnimations]) ++ return YES; ++ } ++ return NO; ++} ++ + - (CAAnimation *) animationForKey: (NSString *) key { + return [_animations objectForKey: key]; + } +@@ -364,4 +707,119 @@ NSString *const kCAContentsFormatGray8Uint = @"Gray8"; + _textureId = value; + } + ++/* darling-lol M57: property accessors Chromium's layer hosting needs. */ ++ ++- (NSUInteger) autoresizingMask { return _autoresizingMask; } ++- (void) setAutoresizingMask: (NSUInteger) v { _autoresizingMask = v; } ++ ++- (BOOL) masksToBounds { return _masksToBounds; } ++- (void) setMasksToBounds: (BOOL) v { _masksToBounds = v; } ++ ++- (CGFloat) cornerRadius { return _cornerRadius; } ++- (void) setCornerRadius: (CGFloat) v { _cornerRadius = v; } ++ ++- (CGColorRef) backgroundColor { return _backgroundColor; } ++- (void) setBackgroundColor: (CGColorRef) v { _backgroundColor = v; } ++ ++- (CGFloat) borderWidth { return _borderWidth; } ++- (void) setBorderWidth: (CGFloat) v { _borderWidth = v; } ++ ++- (CGColorRef) borderColor { return _borderColor; } ++- (void) setBorderColor: (CGColorRef) v { _borderColor = v; } ++ ++- (CGFloat) zPosition { return _zPosition; } ++- (void) setZPosition: (CGFloat) v { _zPosition = v; } ++ ++- (BOOL) isHidden { return _hidden; } ++- (BOOL) hidden { return _hidden; } ++- (void) setHidden: (BOOL) v { _hidden = v; } ++ ++- (BOOL) isDoubleSided { return _doubleSided; } ++- (BOOL) doubleSided { return _doubleSided; } ++- (void) setDoubleSided: (BOOL) v { _doubleSided = v; } ++ ++- (BOOL) needsDisplayOnBoundsChange { return _needsDisplayOnBoundsChange; } ++- (void) setNeedsDisplayOnBoundsChange: (BOOL) v { _needsDisplayOnBoundsChange = v; } ++ ++- (BOOL) shouldRasterize { return _shouldRasterize; } ++- (void) setShouldRasterize: (BOOL) v { _shouldRasterize = v; } ++ ++- (CGFloat) rasterizationScale { return _rasterizationScale; } ++- (void) setRasterizationScale: (CGFloat) v { _rasterizationScale = v; } ++ ++- (BOOL) allowsEdgeAntialiasing { return _allowsEdgeAntialiasing; } ++- (void) setAllowsEdgeAntialiasing: (BOOL) v { _allowsEdgeAntialiasing = v; } ++ ++- (BOOL) allowsGroupOpacity { return _allowsGroupOpacity; } ++- (void) setAllowsGroupOpacity: (BOOL) v { _allowsGroupOpacity = v; } ++ ++- (BOOL) allowsGroupBlending { return _allowsGroupBlending; } ++- (void) setAllowsGroupBlending: (BOOL) v { _allowsGroupBlending = v; } ++ ++- (BOOL) allowsHitTesting { return _allowsHitTesting; } ++- (void) setAllowsHitTesting: (BOOL) v { _allowsHitTesting = v; } ++ ++- (BOOL) drawsAsynchronously { return _drawsAsynchronously; } ++- (void) setDrawsAsynchronously: (BOOL) v { _drawsAsynchronously = v; } ++ ++- (CGFloat) contentsScale { return _contentsScale; } ++- (void) setContentsScale: (CGFloat) v { _contentsScale = v; } ++ ++- (NSString *) contentsGravity { return _contentsGravity; } ++- (void) setContentsGravity: (NSString *) v { ++ v = [v copy]; [_contentsGravity release]; _contentsGravity = v; ++} ++ ++- (CGRect) contentsRect { return _contentsRect; } ++- (void) setContentsRect: (CGRect) v { _contentsRect = v; } ++ ++- (CGRect) contentsCenter { return _contentsCenter; } ++- (void) setContentsCenter: (CGRect) v { _contentsCenter = v; } ++ ++- (CGFloat) anchorPointZ { return _anchorPointZ; } ++- (void) setAnchorPointZ: (CGFloat) v { _anchorPointZ = v; } ++ ++- (CGFloat) shadowOpacity { return _shadowOpacity; } ++- (void) setShadowOpacity: (CGFloat) v { _shadowOpacity = v; } ++ ++- (CGSize) shadowOffset { return _shadowOffset; } ++- (void) setShadowOffset: (CGSize) v { _shadowOffset = v; } ++ ++- (CGFloat) shadowRadius { return _shadowRadius; } ++- (void) setShadowRadius: (CGFloat) v { _shadowRadius = v; } ++ ++- (CGColorRef) shadowColor { return _shadowColor; } ++- (void) setShadowColor: (CGColorRef) v { _shadowColor = v; } ++ ++- (id) shadowPath { return _shadowPath; } ++- (void) setShadowPath: (id) v { v = [v retain]; [_shadowPath release]; _shadowPath = v; } ++ ++- (float) minificationFilterBias { return _minificationFilterBias; } ++- (void) setMinificationFilterBias: (float) v { _minificationFilterBias = v; } ++ ++- (unsigned int) edgeAntialiasingMask { return _edgeAntialiasingMask; } ++- (void) setEdgeAntialiasingMask: (unsigned int) v { _edgeAntialiasingMask = v; } ++ ++- (NSArray *) filters { return _filters; } ++- (void) setFilters: (NSArray *) v { v = [v copy]; [_filters release]; _filters = v; } ++ ++- (NSArray *) backgroundFilters { return _backgroundFilters; } ++- (void) setBackgroundFilters: (NSArray *) v { v = [v copy]; [_backgroundFilters release]; _backgroundFilters = v; } ++ ++- (id) compositingFilter { return _compositingFilter; } ++- (void) setCompositingFilter: (id) v { v = [v retain]; [_compositingFilter release]; _compositingFilter = v; } ++ ++- (NSDictionary *) style { return _style; } ++- (void) setStyle: (NSDictionary *) v { v = [v copy]; [_style release]; _style = v; } ++ ++- (NSArray *) actions { return _actions; } ++- (void) setActions: (NSArray *) v { v = [v copy]; [_actions release]; _actions = v; } ++ ++/* Layout hooks: Cocotron does not run a layout pass, so these are no-ops ++ * (except layoutSublayers, if present, which delegates to the delegate). */ ++- (void) setNeedsLayout { } ++- (void) layoutIfNeeded { } ++- (BOOL) needsLayout { return NO; } ++- (void) layoutSublayers { } ++ + @end +diff --git a/QuartzCore/CALayerContext.m b/QuartzCore/CALayerContext.m +index 5dce8c56..e329b5f8 100644 +--- a/QuartzCore/CALayerContext.m ++++ b/QuartzCore/CALayerContext.m +@@ -1,47 +1,58 @@ + #import ++#import + #import + #import + #import + #import "CALayerInternal.h" ++#import ++#include ++#include + #import + + @class CAMetalLayerInternal; + ++/* The X11 transport presents Chromium's flattened full-window surface. ++ * Avoid uploading and compositing that same window again into a covered GL ++ * child. Restrict this to the explicitly configured native presentation path. */ ++static BOOL hasFullWindowSurface(CALayer *layer,CGSize size) { ++ id contents=layer.contents; ++ static unsigned traces; ++ if(getenv("DARLING_CA_LOCAL_RENDER_TRACE") && contents && ++ CFGetTypeID((CFTypeRef)contents)==IOSurfaceGetTypeID() && traces++<24) ++ fprintf(stderr,"CA_LOCAL_SURFACE frame=%.0fx%.0f surface=%zux%zu\n",size.width,size.height,IOSurfaceGetWidth((IOSurfaceRef)contents),IOSurfaceGetHeight((IOSurfaceRef)contents)); ++ if(contents && CFGetTypeID((CFTypeRef)contents)==IOSurfaceGetTypeID() && ++ IOSurfaceGetWidth((IOSurfaceRef)contents)==(size_t)size.width && ++ IOSurfaceGetHeight((IOSurfaceRef)contents)==(size_t)size.height)return YES; ++ for(CALayer *child in layer.sublayers) ++ if(hasFullWindowSurface(child,size))return YES; ++ return NO; ++} + @implementation CALayerContext + + @synthesize glContext = _glContext; ++@synthesize subwindow = _subwindow; + + - initWithFrame: (CGRect) rect { +- CGLError error; +- +- CGLPixelFormatAttribute attributes[1] = { +- 0, +- }; +- GLint numberOfVirtualScreens; +- +- CGLChoosePixelFormat(attributes, &_pixelFormat, &numberOfVirtualScreens); +- +- if ((error = CGLCreateContext(_pixelFormat, NULL, &_glContext)) != +- kCGLNoError) +- NSLog(@"CGLCreateContext failed with %d in %s %d", error, __FILE__, +- __LINE__); ++ self = [super init]; ++ if (self == nil) ++ return nil; + ++ _deleteTextureIds = [[NSMutableArray alloc] init]; + _frame = rect; +- +- _renderer = [[CARenderer rendererWithCGLContext: _glContext +- options: nil] retain]; ++ /* Views can acquire a backing layer before joining a window. Defer ++ * expensive native GL resources until there is a drawable. */ + + return self; + } + + - (void) dealloc { +- [_timer invalidate]; +- [_timer release]; ++ [self invalidate]; ++ [_deleteTextureIds release]; + [_renderer release]; + CGLReleaseContext(_glContext); ++ CGLReleasePixelFormat(_pixelFormat); + CGLDestroyWindow(_cglWindow); + [_subwindow release]; +- [_layer release]; + [super dealloc]; + } + +@@ -51,12 +62,22 @@ + } + + - (void) setLayer: (CALayer *) layer { ++ if (_layer == layer) ++ return; ++ + layer = [layer retain]; ++ /* CALayer keeps a non-owning context pointer. Clear it before the old ++ * context or layer can be released. */ ++ [_layer _setContext: nil]; + [_layer release]; + _layer = layer; + + [_layer _setContext: self]; + [_renderer setLayer: layer]; ++ ++ /* darling-lol M57: the layer compositor must be driven even when no ++ * CAAnimation is ever added (e.g. Chromium's compositor output). */ ++ [self startTimerIfNeeded]; + } + + - (void) setSubwindow: (CGSubWindow*) subwindow +@@ -68,16 +89,50 @@ + } + + _subwindow = [subwindow retain]; +- _cglWindow = CGLGetWindow([_subwindow nativeWindow]); ++ _cglWindow = NULL; ++ if (_subwindow != nil) { ++ if (_glContext == NULL) { ++ CGLPixelFormatAttribute attributes[] = { 0 }; ++ GLint screens; ++ CGLError error = CGLChoosePixelFormat(attributes, &_pixelFormat, &screens); ++ if (error == kCGLNoError) ++ error = CGLCreateContext(_pixelFormat, NULL, &_glContext); ++ if (error != kCGLNoError) { ++ NSLog(@"CGL context creation failed with %d", error); ++ CGLReleasePixelFormat(_pixelFormat); ++ _pixelFormat = NULL; ++ } else { ++ _renderer = [[CARenderer rendererWithCGLContext: _glContext ++ options: nil] retain]; ++ [_renderer setLayer: _layer]; ++ } ++ } ++ if (_glContext != NULL) ++ _cglWindow = CGLGetWindow([_subwindow nativeWindow]); ++ } + + [_subwindow show]; + + [oldSubwindow release]; + + [_subwindow setFrame: _frame]; ++ ++ // A view can attach its layer before the subwindow creates the GL context. ++ // Notify the whole layer tree now that drawable resources can be created. ++ [_layer _setContext: self]; ++ /* darling-lol M57: begin compositing once a drawable exists. */ ++ [self startTimerIfNeeded]; + } + + - (void) invalidate { ++ [_timer invalidate]; ++ [_timer release]; ++ _timer = nil; ++ ++ [_layer _setContext: nil]; ++ [_renderer setLayer: nil]; ++ [_layer release]; ++ _layer = nil; + } + + - (void) assignTextureIdsToLayerTree: (CALayer *) layer { +@@ -93,7 +148,10 @@ + [self assignTextureIdsToLayerTree: child]; + } + ++extern void darlingQCTiming(const char* what, double seconds); ++ + - (void) renderLayer: (CALayer *) layer { ++ CFTimeInterval renderLayerStart = CACurrentMediaTime(); + CGLContextMakeCurrentAndAttachToWindow(_glContext, _cglWindow); + + glEnable(GL_DEPTH_TEST); +@@ -118,27 +176,69 @@ + + [_deleteTextureIds removeAllObjects]; + ++ CFTimeInterval assignStart = CACurrentMediaTime(); ++ darlingQCTiming("renderLayerSetup", assignStart - renderLayerStart); + [self assignTextureIdsToLayerTree: layer]; ++ darlingQCTiming("assignTextureIds", CACurrentMediaTime() - assignStart); + + // this is where the Metal layer renders to an internal texture for us to use + if ([[layer class] isSubclassOfClass: [CAMetalLayerInternal class]]) { + CAMetalLayerInternal* mtl = (CAMetalLayerInternal*)layer; ++ CFTimeInterval prepStart = CACurrentMediaTime(); + [mtl prepareRender]; ++ darlingQCTiming("prepareRenderTotal", CACurrentMediaTime() - prepStart); + } + ++ CFTimeInterval rendererStart = CACurrentMediaTime(); + [_renderer render]; ++ darlingQCTiming("rendererRender", CACurrentMediaTime() - rendererStart); + } + + - (void) render { ++ if (_cglWindow == NULL || _layer == nil) ++ return; ++ if(getenv("DARLING_CA_SKIP_LOCAL_IOSURFACE_RENDER") && ++ hasFullWindowSurface(_layer,_frame.size))return; + [self renderLayer: _layer]; + } + ++extern void darlingQCTiming(const char* what, double seconds); ++ + - (void) timer: (NSTimer *) timer { ++ /* darling-lol M57: this is now a one-shot frame request, not an ++ * unconditional 60 Hz loop. The old repeating timer ran a full software ++ * GL composite of the layer tree every 16 ms even when nothing had ++ * changed; under Darling that monopolised the main thread and starved ++ * Chromium's task queue (JS timers, IPC), which presented as an "endless ++ * synchronous IPC wait". Frames are now requested on demand (contents ++ * changes / animations) and the timer stops when there is no work. */ ++ [_timer release]; ++ _timer = nil; ++ ++ if (_layer == nil || _cglWindow == NULL) ++ return; ++ ++ static CFTimeInterval lastTimer = 0; ++ CFTimeInterval timerStart = CACurrentMediaTime(); ++ if (lastTimer != 0) ++ darlingQCTiming("compositorTimerInterval", timerStart - lastTimer); ++ lastTimer = timerStart; ++ + [_renderer beginFrameAtTime: CACurrentMediaTime() timeStamp: NULL]; + + [self render]; ++ CFTimeInterval renderDone = CACurrentMediaTime(); ++ darlingQCTiming("compositorRender", renderDone - timerStart); + + [_renderer endFrame]; ++ ++ /* darling-lol M57: present the composited frame to the drawable. */ ++ [self flush]; ++ darlingQCTiming("compositorFlush", CACurrentMediaTime() - renderDone); ++ ++ /* Keep animating only while the layer tree actually has animations. */ ++ if ([_layer _treeHasAnimations]) ++ [self startTimerIfNeeded]; + } + + - (void) startTimerIfNeeded { +@@ -147,7 +247,7 @@ + target: self + selector: @selector(timer:) + userInfo: nil +- repeats: YES] retain]; ++ repeats: NO] retain]; + } + + - (void) deleteTextureId: (NSNumber *) textureId { +@@ -155,7 +255,10 @@ + } + + - (void) flush { +- CGLFlushDrawable(_glContext); ++ if(getenv("DARLING_CA_SKIP_LOCAL_IOSURFACE_RENDER") && _layer && ++ hasFullWindowSurface(_layer,_frame.size))return; ++ if (_glContext != NULL && _cglWindow != NULL) ++ CGLFlushDrawable(_glContext); + } + + @end +diff --git a/QuartzCore/CALayerInternal.h b/QuartzCore/CALayerInternal.h +index 5ad375f8..c8e83814 100644 +--- a/QuartzCore/CALayerInternal.h ++++ b/QuartzCore/CALayerInternal.h +@@ -23,4 +23,5 @@ + - (void) _setContext: (CALayerContext *) context; + - (NSNumber *) _textureId; + - (void) _setTextureId: (NSNumber *) value; ++- (BOOL) _treeHasAnimations; + @end +diff --git a/QuartzCore/CAMetalDrawable.mm b/QuartzCore/CAMetalDrawable.mm +index d6f86b31..81a4ac10 100644 +--- a/QuartzCore/CAMetalDrawable.mm ++++ b/QuartzCore/CAMetalDrawable.mm +@@ -401,6 +401,10 @@ CAMetalDrawableTexture::CAMetalDrawableTexture(CGSize size, Indium::PixelFormat + glTextureParameteri(_textureID, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTextureParameteri(_textureID, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTextureParameteri(_textureID, GL_TEXTURE_WRAP_T, GL_REPEAT); ++ // A Metal layer is opaque: macOS ignores the drawable's alpha channel. The game's blended effects lower destination ++ // alpha as a side effect, and the layer compositor (CARenderer: blending plus an alpha test against 0, over a ++ // cleared-to-black window) then showed those regions as black quads. Always present alpha = 1. ++ glTextureParameteri(_textureID, GL_TEXTURE_SWIZZLE_A, GL_ONE); + reportGLErrors(); + + glTextureStorageMem2DEXT(_textureID, 1, GL_RGBA8, _size.width, _size.height, _memoryObject, 0); +@@ -605,35 +609,16 @@ void CAMetalDrawableActual::present() { + return; + } + +- [layer queuePresent: _drawableID]; + + _semaphore = _texture->synchronizePresentation(); + +- if (_semaphore) { +- int fd = -1; +- VkSemaphoreGetFdInfoKHR info {}; ++ // present() may run on the deferred encoding worker. The window's ++ // EGL context remains current on the main thread, so attempting to ++ // import a semaphore here fails and leaks the exported syncobj FD. ++ // Import it in synchronizeRender(), with the compositor context ++ // current, immediately before the GPU-side wait. + +- info.sType = VK_STRUCTURE_TYPE_SEMAPHORE_GET_FD_INFO_KHR; +- info.semaphore = _semaphore->semaphore; +- info.handleType = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD_BIT; +- +- if (DynamicVK::vkGetSemaphoreFdKHR(_semaphore->device->device(), &info, &fd) != VK_SUCCESS) { +- // TODO +- abort(); +- } +- +- CGLContextObj prev = CGLGetCurrentContext(); +- CGLSetCurrentContext(_glContext); +- +- reportGLErrors(); +- glGenSemaphoresEXT(1, &_glSemaphore); +- reportGLErrors(); +- glImportSemaphoreFdEXT(_glSemaphore, GL_HANDLE_TYPE_OPAQUE_FD_EXT, fd); // this consumes the FD +- fd = -1; +- reportGLErrors(); +- +- CGLSetCurrentContext(prev); +- } ++ [layer queuePresent: _drawableID]; + } + } + +@@ -702,9 +687,24 @@ void CAMetalDrawableActual::reset() { + }; + + void CAMetalDrawableActual::synchronizeRender() { +- GLuint tex = _texture->glTexture(); +- GLenum layout = GL_LAYOUT_GENERAL_EXT; +- glWaitSemaphoreEXT(_glSemaphore, 0, NULL, 1, &tex, &layout); ++ if (!_semaphore) return; ++ if (_glSemaphore == 0) { ++ // prepareRender() has made the layer's GL context current on this ++ // thread. Never try to steal it from the submission worker. ++ glGenSemaphoresEXT(1, &_glSemaphore); ++ if (_glSemaphore == 0) abort(); ++ int fd = -1; ++ VkSemaphoreGetFdInfoKHR info {}; ++ info.sType = VK_STRUCTURE_TYPE_SEMAPHORE_GET_FD_INFO_KHR; ++ info.semaphore = _semaphore->semaphore; ++ info.handleType = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD_BIT; ++ if (DynamicVK::vkGetSemaphoreFdKHR(_semaphore->device->device(), &info, &fd) != VK_SUCCESS) abort(); ++ glImportSemaphoreFdEXT(_glSemaphore, GL_HANDLE_TYPE_OPAQUE_FD_EXT, fd); ++ // A successful import transfers the native FD to the GL driver. ++ } ++ GLuint tex = _texture->glTexture(); ++ GLenum layout = GL_LAYOUT_GENERAL_EXT; ++ glWaitSemaphoreEXT(_glSemaphore, 0, NULL, 1, &tex, &layout); + }; + + static void glDebugCallback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* message, const void* context) { +diff --git a/QuartzCore/CAMetalLayer.mm b/QuartzCore/CAMetalLayer.mm +index b81e2e14..7b9891fb 100644 +--- a/QuartzCore/CAMetalLayer.mm ++++ b/QuartzCore/CAMetalLayer.mm +@@ -26,6 +26,74 @@ + #import + + #include ++#include ++#include ++#include ++#include ++#include ++#include ++#import ++ ++// Diagnostics are off unless /tmp/metal-debug-trace exists when the process starts. ++static bool darlingMetalDebug() ++{ ++ static const bool enabled = access("/tmp/metal-debug-trace", F_OK) == 0; ++ return enabled; ++} ++ ++// Diagnostic trace of the drawable/presentation flow, written to the guest's /tmp/metal-trace.log. ++// Rate limited: the first events and then every hundredth. ++static void darlingMetalTrace(const char* what, long a, long b) ++{ ++ if (!darlingMetalDebug()) { ++ return; ++ } ++ static int count = 0; ++ int n = __sync_add_and_fetch(&count, 1); ++ if (n > 80 && n % 100 != 0) { ++ return; ++ } ++ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) { ++ fprintf(log, "%.3f #%d %s main=%d a=%ld b=%ld\n", CACurrentMediaTime(), n, what, (int) [NSThread isMainThread], a, b); ++ fclose(log); ++ } ++} ++ ++// Cheap aggregated timings (flag file /tmp/metal-debug-timing in the guest when the process starts), printed every 5 s. ++static void darlingMetalTiming(const char* what, double seconds) ++{ ++ static const bool enabled = access("/tmp/metal-debug-timing", F_OK) == 0; ++ if (!enabled) { ++ return; ++ } ++ struct Stat { double sum = 0, max = 0; unsigned long count = 0; }; ++ static std::mutex lock; ++ static std::map stats; ++ static double last = 0; ++ std::lock_guard guard(lock); ++ Stat& stat = stats[what]; ++ stat.sum += seconds; ++ stat.count++; ++ if (seconds > stat.max) stat.max = seconds; ++ double now = CACurrentMediaTime(); ++ if (now - last >= 5) { ++ last = now; ++ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) { ++ fprintf(log, "timing@%.0f:", now); ++ for (auto& entry: stats) { ++ fprintf(log, " %s[n=%lu avg=%.0fms max=%.0fms]", entry.first.c_str(), entry.second.count, entry.second.sum / entry.second.count * 1000, entry.second.max * 1000); ++ } ++ fprintf(log, "\n"); ++ fclose(log); ++ } ++ stats.clear(); ++ } ++} ++ ++extern "C" void darlingQCTiming(const char* what, double seconds) ++{ ++ darlingMetalTiming(what, seconds); ++} + + #import "CALayerInternal.h" + +@@ -205,6 +273,65 @@ static void reportGLErrors(void) { + + @end + ++#include ++#include ++#include ++#include ++#include ++namespace { ++struct PresentIntervalSample { std::atomic queued{0}, renderStart{0}, renderDone{0}, flushDone{0}; }; ++static int64_t presentIntervalNow() { return std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count(); } ++static PresentIntervalSample* beginPresentIntervalSample() { ++ static const bool enabled = [] { FILE* flag = fopen("/tmp/metal-frame-interval-capture", "r"); if (!flag) return false; fclose(flag); return true; }(); ++ if (!enabled) return nullptr; ++ static std::atomic finished{false}; ++ static std::mutex captureMutex; ++ // Never reuse a callback's timestamp storage. A delayed main-queue callback ++ // must not stall the recorder or write into a subsequent capture. All ++ // storage stays alive until process exit; 64 blocks cap its size at 4 MiB. ++ static std::array,64> blocks; ++ static size_t count = 0; ++ static unsigned block = 0, seriesBlocks = 0; ++ static const bool testing = getenv("RIOT_FRAME_CAPTURE_TEST") != nullptr; ++ if (finished.load(std::memory_order_relaxed)) { ++ static std::atomic checks{0}; ++ if (checks.fetch_add(1, std::memory_order_relaxed) % (testing ? 8 : 128)) return nullptr; ++ // Explicitly rearm a bounded eight-block capture in the same match. Keep ++ // filenames increasing so an earlier workload's measurements survive. ++ if (access("/tmp/metal-frame-interval-repeat", F_OK) != 0) return nullptr; ++ std::lock_guard guard(captureMutex); ++ if (finished.load(std::memory_order_relaxed) && block < blocks.size() && ++ unlink("/tmp/metal-frame-interval-repeat") == 0) { ++ count = 0; seriesBlocks = 0; ++ finished.store(false, std::memory_order_relaxed); ++ } ++ } ++ std::lock_guard guard(captureMutex); ++ if (finished.load(std::memory_order_relaxed)) return nullptr; ++ static const int64_t start = presentIntervalNow(); ++ const int64_t now = presentIntervalNow(); ++ if (!testing && now - start < 90000000000LL) return nullptr; ++ auto& samples = blocks[block]; ++ static const size_t limit = testing ? 8 : samples.size(); ++ if (count < limit) { samples[count].queued.store(now); return &samples[count++]; } ++ // Completion columns may still be zero; the analyzer labels incomplete rows. ++ // Queued timestamps already cover every request in the bounded block. ++ char path[96]; ++ if (block == 0) snprintf(path,sizeof(path),"/tmp/metal-present-intervals.csv"); ++ else snprintf(path,sizeof(path),"/tmp/metal-present-intervals-%u.csv",block); ++ if (FILE* log=fopen(path,"w")) { ++ fprintf(log,"queued_ns,render_start_ns,render_done_ns,flush_done_ns\n"); ++ for (size_t i=0;i= 8 || block >= blocks.size()) { finished.store(true, std::memory_order_relaxed); return nullptr; } ++ count=0; ++ // This request covers file I/O and is deliberately outside both sample blocks. ++ return nullptr; ++} ++} ++ + @implementation CAMetalLayerInternal + + #if DARLING_METAL_ENABLED +@@ -363,6 +490,7 @@ static void reportGLErrors(void) { + - (id)nextDrawable + { + std::shared_ptr drawable = nullptr; ++ CFTimeInterval waitStart = CACurrentMediaTime(); + + [_drawableCondition lock]; + +@@ -380,6 +508,8 @@ static void reportGLErrors(void) { + + if (_usableDrawablesBitmap == 0) { + // timeout reached ++ darlingMetalTiming("nextDrawableTimeout", CACurrentMediaTime() - waitStart); ++ darlingMetalTrace("nextDrawable-timeout", (long) _queuedDrawableCount, 0); + [_drawableCondition unlock]; + return nil; + } +@@ -400,6 +530,8 @@ static void reportGLErrors(void) { + } + + _usableDrawablesBitmap &= ~(1u << drawableID); ++ darlingMetalTiming("nextDrawableWait", CACurrentMediaTime() - waitStart); ++ darlingMetalTrace("nextDrawable-ok", (long) drawableID, (long) _usableDrawablesBitmap); + + drawable = _drawables[drawableID]; + +@@ -410,20 +542,36 @@ static void reportGLErrors(void) { + + - (void)queuePresent: (NSUInteger)drawableID + { ++ auto* intervalSample = beginPresentIntervalSample(); + [_drawableCondition lock]; + _queuedDrawables[_queuedDrawableCount] = drawableID; + ++_queuedDrawableCount; ++ darlingMetalTrace("queuePresent", (long) drawableID, (long) _queuedDrawableCount); + [_drawableCondition unlock]; + + // we now need to schedule a render + // + // FIXME: once we fix up CALayer and make it more featureful, this needs to change to `self.needsDisplay = YES` or equivalently `[self setNeedsDisplay: YES]` + // right now, CALayer is missing all the needs-display logic (which is currently in NSView) +- [self display]; ++ // Present only after the Vulkan/GL synchronization has been prepared, on ++ // the window thread. display alone does not drive the layer compositor. ++ dispatch_async(dispatch_get_main_queue(), ^{ ++ if (intervalSample) intervalSample->renderStart.store(presentIntervalNow()); ++ darlingMetalTrace("present-block-runs", 0, 0); ++ CFTimeInterval blockStart = CACurrentMediaTime(); ++ [self->_context render]; ++ if (intervalSample) intervalSample->renderDone.store(presentIntervalNow()); ++ CFTimeInterval renderDone = CACurrentMediaTime(); ++ darlingMetalTiming("mainBlockRender", renderDone - blockStart); ++ [self->_context flush]; ++ if (intervalSample) intervalSample->flushDone.store(presentIntervalNow(), std::memory_order_release); ++ darlingMetalTiming("mainBlockFlush", CACurrentMediaTime() - renderDone); ++ }); + } + + - (void)releaseDrawable: (NSUInteger)drawableID + { ++ darlingMetalTrace("releaseDrawable", (long) drawableID, 0); + [_drawableCondition lock]; + _usableDrawablesBitmap |= 1u << drawableID; + [_drawableCondition signal]; +@@ -455,7 +603,7 @@ static void reportGLErrors(void) { + } + _usableDrawablesBitmap = 0; + +- if (!_device || !_context || _drawableSize.width == 0 || _drawableSize.height == 0) { ++ if (!_device || !_context || !_context.glContext || _drawableSize.width == 0 || _drawableSize.height == 0) { + // can't create new drawables + [_drawableCondition unlock]; + return; +@@ -506,6 +654,10 @@ static void reportGLErrors(void) { + glTextureParameteri(_tex, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTextureParameteri(_tex, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTextureParameteri(_tex, GL_TEXTURE_WRAP_T, GL_REPEAT); ++ // The layer is opaque, as on macOS: never let the drawable's alpha reach the compositor (its blending and ++ // alpha test turned the game's low-alpha effect regions into black quads). The drawable is copied in with ++ // glCopyImageSubData, so the swizzle has to be on this texture, the one that is actually sampled. ++ glTextureParameteri(_tex, GL_TEXTURE_SWIZZLE_A, GL_ONE); + reportGLErrors(); + } + } +@@ -516,6 +668,10 @@ static void reportGLErrors(void) { + - (void)prepareRender + { + std::shared_ptr drawable = nullptr; ++ static CFTimeInterval lastPrepare = 0; ++ CFTimeInterval prepareStart = CACurrentMediaTime(); ++ if (lastPrepare != 0) darlingMetalTiming("prepareRenderInterval", prepareStart - lastPrepare); ++ lastPrepare = prepareStart; + + [_drawableCondition lock]; + if (_queuedDrawableCount > 0) { +@@ -528,11 +684,62 @@ static void reportGLErrors(void) { + if (drawable) { + // wait for the drawable to be fully rendered before copying it to the render texture + // (this is a GPU-side wait) ++ CFTimeInterval syncStart = CACurrentMediaTime(); + drawable->synchronizeRender(); ++ darlingMetalTiming("synchronizeRender", CACurrentMediaTime() - syncStart); + ++ reportGLErrors(); ++ { ++ // Diagnostic: statistics of the imported Vulkan image itself, before the copy ++ static unsigned long sourceCounter = 0; ++ if (darlingMetalDebug() && access("/tmp/metal-debug-frames", F_OK) == 0 && (++sourceCounter <= 30 || sourceCounter % 150 == 0)) { ++ size_t width = _drawableSize.width, height = _drawableSize.height; ++ std::vector source(width * height * 4); ++ glGetTextureImage(drawable->texture()->glTexture(), 0, GL_RGBA, GL_UNSIGNED_BYTE, (GLsizei) source.size(), source.data()); ++ unsigned long lit = 0, total = 0; ++ for (size_t i = 0; i < source.size(); i += 4) { ++ unsigned v = source[i] + source[i + 1] + source[i + 2]; ++ total += v; ++ if (v > 6) ++lit; ++ } ++ darlingMetalTrace("source-stats", (long) lit, (long) (total / (width * height))); ++ } ++ } + reportGLErrors(); + glCopyImageSubData(drawable->texture()->glTexture(), GL_TEXTURE_2D, 0, 0, 0, 0, _tex, GL_TEXTURE_2D, 0, 0, 0, 0, _drawableSize.width, _drawableSize.height, 1); + reportGLErrors(); ++ ++ // Diagnostic: every 150th presented frame, log pixel statistics of what is about to be shown ++ // and save a few of them as /tmp/metal-frame-.ppm. ++ static unsigned long frameCounter = 0; ++ ++frameCounter; ++ if (darlingMetalDebug() && access("/tmp/metal-debug-frames", F_OK) == 0 && (frameCounter <= 30 || frameCounter % 150 == 0) && frameCounter <= 1800) { ++ size_t width = _drawableSize.width, height = _drawableSize.height; ++ std::vector pixels(width * height * 4); ++ glGetTextureImage(_tex, 0, GL_RGBA, GL_UNSIGNED_BYTE, (GLsizei) pixels.size(), pixels.data()); ++ unsigned long nonBlack = 0, sum = 0; ++ for (size_t i = 0; i < pixels.size(); i += 4) { ++ unsigned v = pixels[i] + pixels[i + 1] + pixels[i + 2]; ++ sum += v; ++ if (v > 6) ++nonBlack; ++ } ++ darlingMetalTrace("frame-stats", (long) nonBlack, (long) (sum / (width * height))); ++ if (frameCounter == 5 || frameCounter == 30 || frameCounter == 300 || frameCounter == 900) { ++ char path[64]; ++ snprintf(path, sizeof(path), "/tmp/metal-frame-%lu.ppm", frameCounter); ++ if (FILE* out = fopen(path, "wb")) { ++ fprintf(out, "P6\n%zu %zu\n255\n", width, height); ++ for (size_t row = 0; row < height; ++row) { ++ // GL rows run bottom to top ++ const uint8_t* line = pixels.data() + (height - 1 - row) * width * 4; ++ for (size_t x = 0; x < width; ++x) { ++ fwrite(line + x * 4, 1, 3, out); ++ } ++ } ++ fclose(out); ++ } ++ } ++ } + } + + @synchronized(self) { +@@ -575,6 +782,11 @@ static void reportGLErrors(void) { + { + [super _setContext: context]; + ++ if (!context || !context.glContext) { ++ [self recreateDrawables]; ++ return; ++ } ++ + if (_tex == 0) { + // create our textures now using the context's CGLContext + CGLContextObj prev = CGLGetCurrentContext(); +@@ -589,10 +801,15 @@ static void reportGLErrors(void) { + glTextureParameteri(_tex, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTextureParameteri(_tex, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTextureParameteri(_tex, GL_TEXTURE_WRAP_T, GL_REPEAT); ++ // The layer is opaque, as on macOS: never let the drawable's alpha reach the compositor (its blending and ++ // alpha test turned the game's low-alpha effect regions into black quads). The drawable is copied in with ++ // glCopyImageSubData, so the swizzle has to be on this texture, the one that is actually sampled. ++ glTextureParameteri(_tex, GL_TEXTURE_SWIZZLE_A, GL_ONE); + reportGLErrors(); + + CGLSetCurrentContext(prev); + } ++ [self recreateDrawables]; + } + + #else +diff --git a/QuartzCore/CARenderer.m b/QuartzCore/CARenderer.m +index dd0a1270..b6916db3 100644 +--- a/QuartzCore/CARenderer.m ++++ b/QuartzCore/CARenderer.m +@@ -1,10 +1,12 @@ + #import ++#import + #import + #import + #import + #import + #import + #import "CALayerInternal.h" ++#include + + NSString *const kCARendererColorSpace = @"kCARendererColorSpace"; + +@@ -221,13 +223,40 @@ static GLint interpolationFromName(NSString *name) { + } + + void CATexImage2DCGImage(CGImageRef image) { +- size_t imageWidth = CGImageGetWidth(image); +- size_t imageHeight = CGImageGetHeight(image); +- CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(image); ++ static const uint8_t kTransparent[4] = { 0, 0, 0, 0 }; ++ ++ size_t imageWidth = 0; ++ size_t imageHeight = 0; ++ CGBitmapInfo bitmapInfo = 0; ++ CGDataProviderRef provider = NULL; ++ CFDataRef data = NULL; ++ const uint8_t *pixelBytes = NULL; ++ ++ /* darling-lol M57: layers can have nil contents, or a CGImage whose data ++ * provider is unavailable under Darling; the old code dereferenced those ++ * (O2DataProviderCopyData(NULL) / CFDataGetBytePtr(NULL)). */ ++ if (image != NULL) { ++ provider = CGImageGetDataProvider(image); ++ if (provider != NULL) ++ data = CGDataProviderCopyData(provider); ++ } ++ ++ if (data != NULL) { ++ imageWidth = CGImageGetWidth(image); ++ imageHeight = CGImageGetHeight(image); ++ bitmapInfo = CGImageGetBitmapInfo(image); ++ pixelBytes = CFDataGetBytePtr(data); ++ } + +- CGDataProviderRef provider = CGImageGetDataProvider(image); +- CFDataRef data = CGDataProviderCopyData(provider); +- const uint8_t *pixelBytes = CFDataGetBytePtr(data); ++ if (pixelBytes == NULL) { ++ NSLog(@"LOLTEX no pixel data: image=%p provider=%p data=%p", image, ++ provider, data); ++ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1, 1, 0, GL_RGBA, ++ GL_UNSIGNED_BYTE, kTransparent); ++ if (data != NULL) ++ CFRelease(data); ++ return; ++ } + + GLenum glFormat = GL_BGRA; + GLenum glType = GL_UNSIGNED_INT_8_8_8_8_REV; +@@ -272,6 +301,68 @@ void CATexImage2DCGImage(CGImageRef image) { + + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, imageWidth, imageHeight, 0, + glFormat, glType, pixelBytes); ++ ++ CFRelease(data); ++} ++ ++/* darling-lol M57: Chromium's macOS compositor presents frames through ++ * DisplayCALayerTree, which sets CALayer.contents to an IOSurfaceRef (not a ++ * CGImage). Upload such a surface directly as a texture. */ ++void CATexImage2DIOSurface(IOSurfaceRef surface) { ++ static const uint8_t kTransparent[4] = { 0, 0, 0, 0 }; ++ ++ size_t width = IOSurfaceGetWidth(surface); ++ size_t height = IOSurfaceGetHeight(surface); ++ size_t bytesPerRow = IOSurfaceGetBytesPerRow(surface); ++ OSType pixelFormat = IOSurfaceGetPixelFormat(surface); ++ void *base = IOSurfaceGetBaseAddress(surface); ++ ++ /* darling-lol M57 diagnostic (opt-in): is the delivered compositor frame ++ * real? A zero base / all-zero pixels here means frame data never reached ++ * the browser process. */ ++ if (getenv("M57_QC_TRACE") != 0) { ++ unsigned int z = 0; ++ unsigned long nonzero = 0, i; ++ if (base != NULL && width != 0) { ++ const unsigned char *p = (const unsigned char *)base; ++ for (i = 0; i < width * 4 && i < 4096; i++) ++ if (p[i]) nonzero++; ++ z = (unsigned int)nonzero; ++ } ++ NSLog(@"M57TEXIOS w=%zu h=%zu bpr=%zu fmt=%08x base=%p first4k_nonzero=%u", ++ width, height, bytesPerRow, (unsigned)pixelFormat, base, z); ++ } ++ ++ if (base == NULL || width == 0 || height == 0) { ++ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1, 1, 0, GL_RGBA, ++ GL_UNSIGNED_BYTE, kTransparent); ++ return; ++ } ++ ++ GLenum glFormat = GL_BGRA; ++ GLenum glType = GL_UNSIGNED_INT_8_8_8_8_REV; ++ ++ switch (pixelFormat) { ++ case 'RGBA': ++ glFormat = GL_RGBA; ++ glType = GL_UNSIGNED_INT_8_8_8_8_REV; ++ break; ++ case 'BGRA': ++ default: ++ glFormat = GL_BGRA; ++ glType = GL_UNSIGNED_INT_8_8_8_8_REV; ++ break; ++ } ++ ++ glPixelStorei(GL_UNPACK_ALIGNMENT, 1); ++ if (bytesPerRow != 0 && bytesPerRow >= width * 4) ++ glPixelStorei(GL_UNPACK_ROW_LENGTH, (GLint)(bytesPerRow / 4)); ++ ++ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, (GLsizei) width, (GLsizei) height, ++ 0, glFormat, glType, base); ++ ++ glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); ++ glPixelStorei(GL_UNPACK_ALIGNMENT, 4); + } + + - (void) _renderLayer: (CALayer *) layer +@@ -293,9 +384,14 @@ void CATexImage2DCGImage(CGImageRef image) { + } + + if (loadPixelData) { +- CGImageRef image = (CGImageRef)layer.contents; ++ id contents = (id) layer.contents; + +- CATexImage2DCGImage(image); ++ if (contents != nil && ++ CFGetTypeID((CFTypeRef) contents) == IOSurfaceGetTypeID()) { ++ CATexImage2DIOSurface((IOSurfaceRef) contents); ++ } else { ++ CATexImage2DCGImage((CGImageRef) contents); ++ } + + GLint minFilter = interpolationFromName(layer.minificationFilter); + GLint magFilter = interpolationFromName(layer.magnificationFilter); +diff --git a/QuartzCore/CATransaction.m b/QuartzCore/CATransaction.m +index 5f7998b9..b8968893 100644 +--- a/QuartzCore/CATransaction.m ++++ b/QuartzCore/CATransaction.m +@@ -4,6 +4,10 @@ + #import + #import + #import ++#import ++#import ++#include ++extern void DarlingCAPublishCommittedIOSurface(void); + + NSString *const kCATransactionAnimationDuration = + @"kCATransactionAnimationDuration"; +@@ -15,6 +19,63 @@ NSString *const kCATransactionCompletionBlock = + + @implementation CATransaction + ++/* Commit callbacks belong to the registering thread, and are consumed once. */ ++typedef struct CommitHandler { ++ void (^block)(void); ++ CATransactionPhase phase; ++ struct CommitHandler *next; ++} CommitHandler; ++static __thread CommitHandler *pendingCommitHandlers; ++static __thread BOOL commitHandlersScheduled; ++static __thread BOOL drainingCommitHandlers; ++static __thread CATransactionGroup *implicitTransactionGroup; ++static CATransactionGroup *createImplicitTransactionGroupIfNeeded(void); ++ +++ (void) addCommitHandler: (void (^)(void)) handler ++ forPhase: (CATransactionPhase) phase { ++ if (!handler || phase < 0 || phase > 2) ++ return; ++ createImplicitTransactionGroupIfNeeded(); ++ CommitHandler *entry = calloc(1, sizeof(*entry)); ++ if (!entry) ++ return; ++ entry->block = Block_copy(handler); ++ entry->phase = phase; ++ CommitHandler **tail = &pendingCommitHandlers; ++ while (*tail) ++ tail = &(*tail)->next; ++ *tail = entry; ++} ++ +++ (void) _drainCommitHandlers { ++ CommitHandler *batch = pendingCommitHandlers; ++ pendingCommitHandlers = NULL; ++ commitHandlersScheduled = NO; ++ drainingCommitHandlers = YES; ++ for (int phase = 0; phase < 3; ++phase) ++ for (CommitHandler *entry = batch; entry; entry = entry->next) ++ if (entry->phase == phase) ++ entry->block(); ++ while (batch) { ++ CommitHandler *next = batch->next; ++ Block_release(batch->block); ++ free(batch); ++ batch = next; ++ } ++ drainingCommitHandlers = NO; ++ [self _scheduleCommitHandlers]; ++} ++ +++ (void) setCompletionBlock: (void (^)(void)) block { ++ void (^copy)(void) = block ? Block_copy(block) : nil; ++ [self setValue: (id)copy forKey: kCATransactionCompletionBlock]; ++ if (copy) Block_release(copy); ++} ++ +++ (void (^)(void)) completionBlock { ++ return [self valueForKey: kCATransactionCompletionBlock]; ++} ++ + static NSMutableArray *transactionStack() { + NSMutableDictionary *shared = [NSCurrentThread() sharedDictionary]; + id stack = [shared objectForKey: @"CATransactionStack"]; +@@ -42,8 +103,10 @@ static CATransactionGroup *createImplicitTransactionGroupIfNeeded() { + check = [[CATransactionGroup alloc] init]; + + [transactionStack() addObject: check]; ++ implicitTransactionGroup = check; ++ [check release]; + [[NSRunLoop currentRunLoop] +- performSelector: @selector(commit) ++ performSelector: @selector(_commitImplicitTransaction) + target: [CATransaction class] + argument: nil + order: 0 +@@ -99,13 +162,59 @@ static CATransactionGroup *createImplicitTransactionGroupIfNeeded() { + CATransactionGroup *group = [[CATransactionGroup alloc] init]; + + [transactionStack() addObject: group]; ++ [group release]; ++} ++ +++ (void) _scheduleCommitHandlers { ++ if ([transactionStack() count] || !pendingCommitHandlers || ++ commitHandlersScheduled || drainingCommitHandlers) ++ return; ++ /* Chromium's handlers may pump a nested runloop. Invoke them after the ++ * transaction has unwound, on its original thread. Drain new batches ++ * only after the current batch has completed all of its phases. */ ++ commitHandlersScheduled = YES; ++ CFRunLoopRef loop = CFRunLoopGetCurrent(); ++ CFRunLoopPerformBlock(loop, kCFRunLoopCommonModes, ^{ ++ [CATransaction _drainCommitHandlers]; ++ }); ++ CFRunLoopWakeUp(loop); ++} ++ +++ (void) _commitImplicitTransaction { ++ NSMutableArray *stack = transactionStack(); ++ if ([stack count] == 1 && [stack lastObject] == implicitTransactionGroup) ++ [self commit]; + } + + + (void) commit { +- [transactionStack() removeLastObject]; ++ NSMutableArray *stack = transactionStack(); ++ if ([stack count]) { ++ void (^completion)(void) = [[stack lastObject] valueForKey: kCATransactionCompletionBlock]; ++ if (completion) ++ [self addCommitHandler: completion forPhase: kCATransactionPhasePostCommit]; ++ if ([stack lastObject] == implicitTransactionGroup) ++ implicitTransactionGroup = nil; ++ [stack removeLastObject]; ++ } ++ if ([stack count] == 1 && [stack lastObject] == implicitTransactionGroup) { ++ CFRunLoopRef loop = CFRunLoopGetCurrent(); ++ CFRunLoopPerformBlock(loop, kCFRunLoopCommonModes, ^{ ++ [CATransaction _commitImplicitTransaction]; ++ }); ++ CFRunLoopWakeUp(loop); ++ } ++ if (![stack count]) ++ DarlingCAPublishCommittedIOSurface(); ++ [self _scheduleCommitHandlers]; + } + + + (void) flush { ++ NSMutableArray *stack = transactionStack(); ++ BOOL commitsImplicit = [stack count] == 1 && [stack lastObject] == implicitTransactionGroup; ++ [self _commitImplicitTransaction]; ++ /* An implicit commit already publishes its completed frame. */ ++ if(!getenv("DARLING_CA_PRESENT_AT_COMMIT") || !commitsImplicit) ++ DarlingCAPublishCommittedIOSurface(); + } + + + (void) lock { +diff --git a/QuartzCore/CATransactionGroup.m b/QuartzCore/CATransactionGroup.m +index 5a060cc3..572ddf65 100644 +--- a/QuartzCore/CATransactionGroup.m ++++ b/QuartzCore/CATransactionGroup.m +@@ -21,7 +21,13 @@ + } + + - (void) setValue: value forKey: (NSString *) key { +- [_values setObject: value forKey: key]; ++ if (value) [_values setObject: value forKey: key]; ++ else [_values removeObjectForKey: key]; ++} ++ ++- (void) dealloc { ++ [_values release]; ++ [super dealloc]; + } + + @end +diff --git a/QuartzCore/CATransformLayer.m b/QuartzCore/CATransformLayer.m +index 444122cc..81b01703 100644 +--- a/QuartzCore/CATransformLayer.m ++++ b/QuartzCore/CATransformLayer.m +@@ -1,18 +1,42 @@ +-#import ++/* ++ * This file is part of Darling. ++ * ++ * Copyright (C) 2022 Darling developers ++ * ++ * Darling is free software: you can redistribute it and/or modify ++ * it under the terms of the GNU General Public License as published by ++ * the Free Software Foundation, either version 3 of the License, or ++ * (at your option) any later version. ++ * ++ * Darling is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ * GNU General Public License for more details. ++ * ++ * You should have received a copy of the GNU General Public License ++ * along with Darling. If not, see . ++ */ + +-@interface CATransformLayer : NSObject +-@end +- +-@implementation CATransformLayer ++/* ++ * darling-lol M58F35: ++ * ++ * CATransformLayer was a stub NSObject whose forwardInvocation: merely logged ++ * "Stub called: …". Chromium (and AppKit) use CATransformLayer as a real layer ++ * inside the layer tree, so CALayer's recursive -_setContext:/-_setSuperLayer: ++ * and CALayer's/Chromium's -insertSublayer:atIndex: were dispatched to the stub ++ * and did nothing; the runtime's forward-signature check also emitted ~49k ++ * "NSForwardSignatureError: invoked with 3 args, but 2 expected" lines per run. ++ * The result was a broken layer tree (context never propagated to the ++ * transform-layer subtree — the compositor's frame never reached the tree). ++ * ++ * A CATransformLayer IS a CALayer (it only adds an is-3D flag and renders its ++ * sublayers in 3D); on macOS it responds to the whole CALayer surface. Deriving ++ * it from CALayer therefore restores the correct behaviour for every selector ++ * that was previously swallowed by the stub. ++ */ + +-- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector +-{ +- return [NSMethodSignature signatureWithObjCTypes: "v@:"]; +-} +- +-- (void)forwardInvocation:(NSInvocation *)anInvocation +-{ +- NSLog(@"Stub called: %@ in %@", NSStringFromSelector([anInvocation selector]), [self class]); +-} ++#import ++#import + ++@implementation CATransformLayer + @end +diff --git a/QuartzCore/CMakeLists.txt b/QuartzCore/CMakeLists.txt +index bc12fb7f..3ab43fd7 100644 +--- a/QuartzCore/CMakeLists.txt ++++ b/QuartzCore/CMakeLists.txt +@@ -100,6 +100,7 @@ add_framework(QuartzCore + Onyx2D + OpenGL + CoreGraphics ++ IOSurface + Metal_private + ${QUARTZCORE_LINK_INDIUM_PRIVATE} + cxx +@@ -116,7 +117,7 @@ set_target_properties(QuartzCore + CXX_STANDARD_REQUIRED ON + ) + +-add_dependencies(QuartzCore CoreImage CoreVideo) ++add_dependencies(QuartzCore CoreImage CoreVideo IOSurface) + + target_include_directories(QuartzCore PUBLIC + include +diff --git a/QuartzCore/include/QuartzCore/CALayer.h b/QuartzCore/include/QuartzCore/CALayer.h +index 49b03483..5ebbf535 100644 +--- a/QuartzCore/include/QuartzCore/CALayer.h ++++ b/QuartzCore/include/QuartzCore/CALayer.h +@@ -70,6 +70,7 @@ CA_EXPORT NSString *const kCAContentsFormatGray8Uint; + CGRect _bounds; + CGFloat _opacity; + BOOL _opaque; ++ BOOL _geometryFlipped; + id _contents; + CATransform3D _transform; + CATransform3D _sublayerTransform; +@@ -78,6 +79,42 @@ CA_EXPORT NSString *const kCAContentsFormatGray8Uint; + BOOL _needsDisplay; + NSMutableDictionary *_animations; + NSNumber *_textureId; ++ ++ /* darling-lol M57: additional layer properties Chromium relies on. */ ++ NSUInteger _autoresizingMask; ++ BOOL _masksToBounds; ++ CGFloat _cornerRadius; ++ CGColorRef _backgroundColor; ++ CGFloat _borderWidth; ++ CGColorRef _borderColor; ++ CGFloat _zPosition; ++ BOOL _hidden; ++ BOOL _doubleSided; ++ BOOL _needsDisplayOnBoundsChange; ++ BOOL _shouldRasterize; ++ CGFloat _rasterizationScale; ++ BOOL _allowsEdgeAntialiasing; ++ BOOL _allowsGroupOpacity; ++ BOOL _allowsGroupBlending; ++ BOOL _allowsHitTesting; ++ BOOL _drawsAsynchronously; ++ CGFloat _contentsScale; ++ NSString *_contentsGravity; ++ CGRect _contentsRect; ++ CGRect _contentsCenter; ++ CGFloat _anchorPointZ; ++ CGFloat _shadowOpacity; ++ CGSize _shadowOffset; ++ CGFloat _shadowRadius; ++ CGColorRef _shadowColor; ++ id _shadowPath; ++ float _minificationFilterBias; ++ unsigned int _edgeAntialiasingMask; ++ NSArray *_filters; ++ NSArray *_backgroundFilters; ++ id _compositingFilter; ++ NSDictionary *_style; ++ NSArray *_actions; + } + + + layer; +@@ -91,6 +128,41 @@ CA_EXPORT NSString *const kCAContentsFormatGray8Uint; + @property CGRect frame; + @property CGFloat opacity; + @property BOOL opaque; ++@property BOOL geometryFlipped; ++@property NSUInteger autoresizingMask; ++@property BOOL masksToBounds; ++@property CGFloat cornerRadius; ++@property CGColorRef backgroundColor; ++@property CGFloat borderWidth; ++@property CGColorRef borderColor; ++@property CGFloat zPosition; ++@property BOOL hidden; ++@property BOOL doubleSided; ++@property BOOL needsDisplayOnBoundsChange; ++@property BOOL shouldRasterize; ++@property CGFloat rasterizationScale; ++@property BOOL allowsEdgeAntialiasing; ++@property BOOL allowsGroupOpacity; ++@property BOOL allowsGroupBlending; ++@property BOOL allowsHitTesting; ++@property BOOL drawsAsynchronously; ++@property CGFloat contentsScale; ++@property(copy) NSString *contentsGravity; ++@property CGRect contentsRect; ++@property CGRect contentsCenter; ++@property CGFloat anchorPointZ; ++@property CGFloat shadowOpacity; ++@property CGSize shadowOffset; ++@property CGFloat shadowRadius; ++@property CGColorRef shadowColor; ++@property(retain) id shadowPath; ++@property float minificationFilterBias; ++@property unsigned int edgeAntialiasingMask; ++@property(copy) NSArray *filters; ++@property(copy) NSArray *backgroundFilters; ++@property(retain) id compositingFilter; ++@property(copy) NSDictionary *style; ++@property(copy) NSArray *actions; + @property(retain) id contents; + //@property CATransform3D transform; + @property CATransform3D sublayerTransform; +@@ -101,6 +173,9 @@ CA_EXPORT NSString *const kCAContentsFormatGray8Uint; + - init; + + - (void) addSublayer: (CALayer *) layer; ++- (void) insertSublayer: (CALayer *) layer atIndex: (unsigned) index; ++- (void) insertSublayer: (CALayer *) layer below: (CALayer *) sibling; ++- (void) insertSublayer: (CALayer *) layer above: (CALayer *) sibling; + - (void) replaceSublayer: (CALayer *) layer with: (CALayer *) other; + - (void) display; + - (void) displayIfNeeded; +diff --git a/QuartzCore/include/QuartzCore/CALayerContext.h b/QuartzCore/include/QuartzCore/CALayerContext.h +index e4b0c342..d61c37ba 100644 +--- a/QuartzCore/include/QuartzCore/CALayerContext.h ++++ b/QuartzCore/include/QuartzCore/CALayerContext.h +@@ -20,6 +20,7 @@ + } + + @property(readonly) CGLContextObj glContext; ++@property(readonly) CGSubWindow *subwindow; + + - initWithFrame: (CGRect) rect; + +diff --git a/QuartzCore/include/QuartzCore/CATransaction.h b/QuartzCore/include/QuartzCore/CATransaction.h +index 985f77f3..db64f55d 100644 +--- a/QuartzCore/include/QuartzCore/CATransaction.h ++++ b/QuartzCore/include/QuartzCore/CATransaction.h +@@ -10,6 +10,23 @@ CA_EXPORT NSString *const kCATransactionCompletionBlock; + + @interface CATransaction : NSObject + ++/* darling-lol M57: Chromium schedules post-commit work with ++ * +[CATransaction addCommitHandler:forPhase:]; Cocotron did not implement it, ++ * so the UX aborted with an unrecognized-selector NSException right after ++ * createWindow(). The phase constants match macOS. */ ++typedef NSInteger CATransactionPhase; ++enum { ++ kCATransactionPhasePreLayout = 0, ++ kCATransactionPhasePreCommit = 1, ++ kCATransactionPhasePostCommit = 2, ++}; ++ +++ (void) addCommitHandler: (void (^)(void)) handler ++ forPhase: (CATransactionPhase) phase; ++ +++ (void) setCompletionBlock: (void (^)(void)) block; +++ (void (^)(void)) completionBlock; ++ + + (BOOL) disableActions; + + (CFTimeInterval) animationDuration; + + (CAMediaTimingFunction *) animationTimingFunction; +diff --git a/QuartzCore/include/QuartzCore/CATransformLayer.h b/QuartzCore/include/QuartzCore/CATransformLayer.h +new file mode 100644 +--- /dev/null ++++ b/QuartzCore/include/QuartzCore/CATransformLayer.h +@@ -0,0 +1,28 @@ ++/* ++ * This file is part of Darling. ++ * ++ * Copyright (C) 2022 Darling developers ++ * ++ * Darling is free software: you can redistribute it and/or modify ++ * it under the terms of the GNU General Public License as published by ++ * the Free Software Foundation, either version 3 of the License, or ++ * (at your option) any later version. ++ * ++ * Darling is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ * GNU General Public License for more details. ++ * ++ * You should have received a copy of the GNU General Public License ++ * along with Darling. If not, see . ++ */ ++ ++#import ++ ++/* darling-lol M58F35: CATransformLayer is a CALayer that renders its sublayers ++ * in 3D (it adds no surface API of its own). */ ++@interface CATransformLayer : CALayer ++ ++- (instancetype) init; ++ ++@end diff --git a/patches/compiler-rt.patch b/patches/compiler-rt.patch new file mode 100644 index 0000000..870c75d --- /dev/null +++ b/patches/compiler-rt.patch @@ -0,0 +1,57 @@ +diff --git a/lib/builtins/CMakeLists.txt b/lib/builtins/CMakeLists.txt +index 0154dd65..95bea4b5 100644 +--- a/lib/builtins/CMakeLists.txt ++++ b/lib/builtins/CMakeLists.txt +@@ -29,8 +29,7 @@ set(GENERIC_SOURCES + ashlti3.c + ashrdi3.c + ashrti3.c +- # FIXME: atomic.c may only be compiled if host compiler understands _Atomic +- # atomic.c ++ atomic.c + clear_cache.c + clzdi2.c + clzsi2.c +diff --git a/lib/builtins/atomic.c b/lib/builtins/atomic.c +index f1ddc3e0..86628417 100644 +--- a/lib/builtins/atomic.c ++++ b/lib/builtins/atomic.c +@@ -159,7 +159,7 @@ static __inline Lock *lock_for_pointer(void *ptr) { + + /// An atomic load operation. This is atomic with respect to the source + /// pointer only. +-void __atomic_load_c(int size, void *src, void *dest, int model) { ++void __atomic_load_c(size_t size, void *src, void *dest, int model) { + #define LOCK_FREE_ACTION(type) \ + *((type*)dest) = __c11_atomic_load((_Atomic(type)*)src, model);\ + return; +@@ -173,9 +173,9 @@ void __atomic_load_c(int size, void *src, void *dest, int model) { + + /// An atomic store operation. This is atomic with respect to the destination + /// pointer only. +-void __atomic_store_c(int size, void *dest, void *src, int model) { ++void __atomic_store_c(size_t size, void *dest, void *src, int model) { + #define LOCK_FREE_ACTION(type) \ +- __c11_atomic_store((_Atomic(type)*)dest, *(type*)dest, model);\ ++ __c11_atomic_store((_Atomic(type)*)dest, *(type*)src, model);\ + return; + LOCK_FREE_CASES(); + #undef LOCK_FREE_ACTION +@@ -190,7 +190,7 @@ void __atomic_store_c(int size, void *dest, void *src, int model) { + /// they are not, then this stores the current value from *ptr in *expected. + /// + /// This function returns 1 if the exchange takes place or 0 if it fails. +-int __atomic_compare_exchange_c(int size, void *ptr, void *expected, ++int __atomic_compare_exchange_c(size_t size, void *ptr, void *expected, + void *desired, int success, int failure) { + #define LOCK_FREE_ACTION(type) \ + return __c11_atomic_compare_exchange_strong((_Atomic(type)*)ptr, (type*)expected,\ +@@ -211,7 +211,7 @@ int __atomic_compare_exchange_c(int size, void *ptr, void *expected, + + /// Performs an atomic exchange operation between two pointers. This is atomic + /// with respect to the target address. +-void __atomic_exchange_c(int size, void *ptr, void *val, void *old, int model) { ++void __atomic_exchange_c(size_t size, void *ptr, void *val, void *old, int model) { + #define LOCK_FREE_ACTION(type) \ + *(type*)old = __c11_atomic_exchange((_Atomic(type)*)ptr, *(type*)val,\ + model);\ diff --git a/patches/configd.patch b/patches/configd.patch new file mode 100644 index 0000000..20d15f6 --- /dev/null +++ b/patches/configd.patch @@ -0,0 +1,744 @@ +diff --git a/darling/src/SystemConfiguration.fproj/stubs.c b/darling/src/SystemConfiguration.fproj/stubs.c +index 1f31c08..3299e6a 100644 +--- a/darling/src/SystemConfiguration.fproj/stubs.c ++++ b/darling/src/SystemConfiguration.fproj/stubs.c +@@ -1,4 +1,13 @@ + #include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include + + #define STUB() printf("STUB %s\n", __func__) + +@@ -50,95 +59,348 @@ SCNetworkConnectionRef SCNetworkConnectionCreateWithServiceID(CFAllocatorRef all + return NULL; + } + ++/* ++ * darling-lol: minimal but FUNCTIONAL network reachability. ++ * ++ * Previously every SCNetworkReachability* entry point was a stub: ++ * CreateWithAddress/Name returned NULL and GetFlags returned garbage. ++ * Chromium's network_change_notifier_mac / dns_config_service start by ++ * creating a reachability object and calling GetFlags; with NULL/garbage ++ * it took a failing path (and `dns_config_service_posix ... watch failed ++ * to start` / `Could not get initial network connection type` follow). ++ * ++ * Darling has no reachability subsystem, but the honest answer for a host ++ * that has a working network stack is "reachable, direct". We return a ++ * real heap object so callers can schedule it, and GetFlags reports ++ * kSCNetworkReachabilityFlagsReachable. If the host is genuinely offline ++ * the caller finds out when its connect() fails; this only removes the ++ * false negative at startup. ++ */ ++struct __SCNetworkReachability { ++ CFRuntimeBase _base; ++ CFStringRef name; ++ struct sockaddr_storage addr; ++ Boolean hasAddr; ++}; ++ ++static void _reachabilityRelease(CFTypeRef cf) ++{ ++ struct __SCNetworkReachability* ref = (struct __SCNetworkReachability*) cf; ++ if (ref->name != NULL) ++ CFRelease(ref->name); ++} ++ ++static CFStringRef _reachabilityCopyDescription(CFTypeRef cf) ++{ ++ return CFSTR("SCNetworkReachability"); ++} ++ ++static const CFRuntimeClass _reachabilityClass = { ++ 0, ++ "SCNetworkReachability", ++ NULL, /* init */ ++ NULL, /* copy */ ++ _reachabilityRelease, ++ NULL, /* equal */ ++ NULL, /* hash */ ++ NULL, /* copyFormattingDesc */ ++ _reachabilityCopyDescription ++}; ++ ++static SCNetworkReachabilityRef _reachabilityCreate(CFAllocatorRef allocator, const struct sockaddr* addr) ++{ ++ static CFTypeID classID = _kCFRuntimeNotATypeID; ++ if (classID == _kCFRuntimeNotATypeID) ++ classID = _CFRuntimeRegisterClass(&_reachabilityClass); ++ ++ struct __SCNetworkReachability* ref = (struct __SCNetworkReachability*) ++ _CFRuntimeCreateInstance(allocator, classID, ++ sizeof(struct __SCNetworkReachability) - sizeof(CFRuntimeBase), NULL); ++ if (ref == NULL) ++ return NULL; ++ ++ if (addr != NULL) { ++ size_t len = sizeof(struct sockaddr_in); ++ if (addr->sa_family == AF_INET6) ++ len = sizeof(struct sockaddr_in6); ++ if (len > sizeof(ref->addr)) ++ len = sizeof(ref->addr); ++ memcpy(&ref->addr, addr, len); ++ ref->hasAddr = true; ++ } ++ return (SCNetworkReachabilityRef) ref; ++} ++ + CFTypeID SCNetworkReachabilityGetTypeID(void) + { +- STUB(); +- return 0; ++ /* darling-lol: return the real registered class id (some clients ++ * compare it and misbehave on 0). */ ++ static CFTypeID classID = _kCFRuntimeNotATypeID; ++ if (classID == _kCFRuntimeNotATypeID) ++ classID = _CFRuntimeRegisterClass(&_reachabilityClass); ++ return classID; + } + + Boolean SCNetworkReachabilityScheduleWithRunLoop(SCNetworkReachabilityRef ref, CFRunLoopRef rl, CFStringRef rlMode) + { +- STUB(); +- return 0; ++ /* No dynamic reachability changes to report; accepting the schedule ++ * (instead of failing) lets callers proceed to GetFlags. */ ++ return (ref != NULL) ? true : false; + } + + Boolean SCNetworkReachabilityUnscheduleFromRunLoop(SCNetworkReachabilityRef ref, CFRunLoopRef rl, CFStringRef rlMode) + { +- STUB(); +- return 0; ++ return (ref != NULL) ? true : false; + } + + Boolean SCNetworkReachabilitySetCallback(SCNetworkReachabilityRef target, SCNetworkReachabilityCallBack cb, SCNetworkReachabilityContext* ctxt) + { +- STUB(); +- return 0; ++ /* Accepted but never invoked (no change notifications implemented). */ ++ return (target != NULL) ? true : false; + } + + Boolean SCNetworkReachabilityGetFlags(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags* flags) + { +- STUB(); +- return 0; ++ if (target == NULL || flags == NULL) ++ return false; ++ /* Host has a network stack: report reachable + direct, not required. */ ++ *flags = kSCNetworkReachabilityFlagsReachable ++ | kSCNetworkReachabilityFlagsTransientConnection ++ | kSCNetworkReachabilityFlagsConnectionAutomatic; ++ return true; + } + + SCNetworkReachabilityRef SCNetworkReachabilityCreateWithName(CFAllocatorRef allocator, const char* nodeName) + { +- STUB(); +- return NULL; ++ struct __SCNetworkReachability* ref = (struct __SCNetworkReachability*) ++ _reachabilityCreate(allocator, NULL); ++ if (ref != NULL && nodeName != NULL) ++ ref->name = CFStringCreateWithCString(allocator, nodeName, kCFStringEncodingUTF8); ++ return (SCNetworkReachabilityRef) ref; + } + + SCNetworkReachabilityRef SCNetworkReachabilityCreateWithAddressPair(CFAllocatorRef allocator, const struct sockaddr* localAddress, const struct sockaddr* remoteAddress) + { +- STUB(); +- return NULL; ++ /* Local address is irrelevant to our answer; key off the remote. */ ++ return _reachabilityCreate(allocator, remoteAddress); + } + + SCNetworkReachabilityRef SCNetworkReachabilityCreateWithAddress(CFAllocatorRef allocator, const struct sockaddr* remoteAddress) + { +- STUB(); +- return NULL; ++ return _reachabilityCreate(allocator, remoteAddress); ++} ++ ++/* ++ * darling-lol: minimal but FUNCTIONAL SCDynamicStore. ++ * ++ * Chromium's dns_config_service_posix (macOS) creates an SCDynamicStore, ++ * calls SCDynamicStoreSetNotificationKeys with the DNS/State keys, and ++ * schedules it -- that is how it "starts" its config watcher. All of ++ * these used to be STUBs returning NULL/FALSE, so the watcher could never ++ * start (`dns_config_service_posix.cc(137/147) ... watch failed to ++ * start`). We provide a real object and accept the notification setup; ++ * `CopyValue` reports "key absent" (NULL, never bogus) and the runloop ++ * source is a real never-firing source. No change callbacks are delivered ++ * (Darling has no configd store to watch), which is sufficient for startup. ++ * (Proxy values are served by the pre-existing SCDynamicStoreCopyProxies ++ * in stubs.cpp, untouched.) ++ */ ++struct __SCDynamicStore { ++ CFRuntimeBase _base; ++ CFStringRef name; ++ CFArrayRef notificationKeys; ++ CFArrayRef notificationPatterns; ++ dispatch_queue_t queue; ++ SCDynamicStoreCallBack callout; ++ SCDynamicStoreContext context; ++}; ++ ++static void _storeRelease(CFTypeRef cf) ++{ ++ struct __SCDynamicStore* store = (struct __SCDynamicStore*) cf; ++ if (store->name) CFRelease(store->name); ++ if (store->notificationKeys) CFRelease(store->notificationKeys); ++ if (store->notificationPatterns) CFRelease(store->notificationPatterns); ++ if (store->context.release && store->context.info) ++ store->context.release(store->context.info); ++} ++ ++static CFStringRef _storeCopyDescription(CFTypeRef cf) ++{ ++ return CFSTR("SCDynamicStore"); ++} ++ ++static const CFRuntimeClass _storeClass = { ++ 0, ++ "SCDynamicStore", ++ NULL, NULL, ++ _storeRelease, ++ NULL, NULL, NULL, ++ _storeCopyDescription ++}; ++ ++static CFTypeID _storeGetClassID(void) ++{ ++ static CFTypeID id = _kCFRuntimeNotATypeID; ++ if (id == _kCFRuntimeNotATypeID) ++ id = _CFRuntimeRegisterClass(&_storeClass); ++ return id; ++} ++ ++CFTypeID SCDynamicStoreGetTypeID(void) ++{ ++ return _storeGetClassID(); + } + + CFStringRef SCDynamicStoreKeyCreateNetworkGlobalEntity(CFAllocatorRef allocator, CFStringRef domain, CFStringRef entity) + { +- STUB(); +- return NULL; ++ if (domain == NULL || entity == NULL) ++ return NULL; ++ return CFStringCreateWithFormat(allocator, NULL, CFSTR("%@/Network/Global/%@"), ++ domain, entity); ++} ++ ++CFStringRef SCDynamicStoreKeyCreateProxies(CFAllocatorRef allocator) ++{ ++ /* darling-lol: Electron (Chromium network stack) lazy-binds this at ++ * login-network time; a missing export aborts the process in dyld ++ * ("Symbol not found"). Built on the same constructor as Apple's. */ ++ return SCDynamicStoreKeyCreateNetworkGlobalEntity(allocator, ++ kSCDynamicStoreDomainState, kSCEntNetProxies); ++} ++ ++/* Query the kernel's route selection without sending a packet. Match its ++ * local address against getifaddrs, rather than inventing a primary adapter. */ ++static CFStringRef _primaryInterface(int family) ++{ ++ struct sockaddr_storage remote = {0}, local = {0}; ++ socklen_t size; ++ if (family == AF_INET) { ++ struct sockaddr_in *p = (struct sockaddr_in *)&remote; ++ p->sin_len = sizeof(*p); p->sin_family = family; p->sin_port = htons(53); ++ inet_pton(family, "8.8.8.8", &p->sin_addr); size = sizeof(*p); ++ } else { ++ struct sockaddr_in6 *p = (struct sockaddr_in6 *)&remote; ++ p->sin6_len = sizeof(*p); p->sin6_family = family; p->sin6_port = htons(53); ++ inet_pton(family, "2001:4860:4860::8888", &p->sin6_addr); size = sizeof(*p); ++ } ++ int fd = socket(family, SOCK_DGRAM, 0); ++ if (fd < 0) return NULL; ++ socklen_t localSize = sizeof(local); ++ int result = connect(fd, (struct sockaddr *)&remote, size); ++ if (!result) result = getsockname(fd, (struct sockaddr *)&local, &localSize); ++ close(fd); ++ if (result) return NULL; ++ struct ifaddrs *interfaces = NULL; ++ if (getifaddrs(&interfaces)) return NULL; ++ CFStringRef name = NULL; ++ for (struct ifaddrs *p = interfaces; p; p = p->ifa_next) { ++ if (!p->ifa_addr || p->ifa_addr->sa_family != family || ++ !(p->ifa_flags & IFF_UP) || (p->ifa_flags & IFF_LOOPBACK)) continue; ++ const void *a, *b; ++ size_t bytes; ++ if (family == AF_INET) { ++ a = &((struct sockaddr_in *)p->ifa_addr)->sin_addr; ++ b = &((struct sockaddr_in *)&local)->sin_addr; bytes = sizeof(struct in_addr); ++ } else { ++ a = &((struct sockaddr_in6 *)p->ifa_addr)->sin6_addr; ++ b = &((struct sockaddr_in6 *)&local)->sin6_addr; bytes = sizeof(struct in6_addr); ++ } ++ if (!memcmp(a, b, bytes)) { ++ name = CFStringCreateWithCString(NULL, p->ifa_name, kCFStringEncodingUTF8); ++ break; ++ } ++ } ++ freeifaddrs(interfaces); ++ return name; + } + + CFPropertyListRef SCDynamicStoreCopyValue(SCDynamicStoreRef store, CFStringRef key) + { +- STUB(); +- return NULL; ++ if (!key) return NULL; ++ int family; ++ if (CFEqual(key, CFSTR("State:/Network/Global/IPv4"))) family = AF_INET; ++ else if (CFEqual(key, CFSTR("State:/Network/Global/IPv6"))) family = AF_INET6; ++ else return NULL; ++ CFStringRef primary = _primaryInterface(family); ++ if (!primary) return NULL; ++ const void *keys[] = { kSCDynamicStorePropNetPrimaryInterface }; ++ const void *values[] = { primary }; ++ CFDictionaryRef result = CFDictionaryCreate(NULL, keys, values, 1, ++ &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); ++ CFRelease(primary); ++ return result; + } + + SCDynamicStoreRef SCDynamicStoreCreate(CFAllocatorRef allocator, CFStringRef name, SCDynamicStoreCallBack callout, SCDynamicStoreContext *context) + { +- STUB(); +- return NULL; ++ struct __SCDynamicStore* store = (struct __SCDynamicStore*) ++ _CFRuntimeCreateInstance(allocator, _storeGetClassID(), ++ sizeof(struct __SCDynamicStore) - sizeof(CFRuntimeBase), NULL); ++ if (store == NULL) ++ return NULL; ++ store->name = (name != NULL) ? CFStringCreateCopy(allocator, name) : NULL; ++ store->callout = callout; ++ if (context != NULL) { ++ store->context = *context; ++ if (store->context.retain && store->context.info) ++ store->context.info = (void*) store->context.retain(store->context.info); ++ } ++ return (SCDynamicStoreRef) store; + } + + SCDynamicStoreRef __nullable SCDynamicStoreCreateWithOptions(CFAllocatorRef __nullable allocator, CFStringRef name, CFDictionaryRef __nullable storeOptions, SCDynamicStoreCallBack __nullable callout, SCDynamicStoreContext* __nullable context) + { +- STUB(); +- return NULL; ++ return SCDynamicStoreCreate(allocator, name, callout, context); + }; + + CFStringRef SCDynamicStoreKeyCreate(CFAllocatorRef allocator, CFStringRef fmt, ...) + { +- STUB(); +- return NULL; ++ if (fmt == NULL) ++ return NULL; ++ /* Best effort: return the format string itself when no args are ++ * readily available; callers only use these as dictionary keys. */ ++ return CFStringCreateCopy(allocator, fmt); + } + + Boolean SCDynamicStoreSetDispatchQueue(SCDynamicStoreRef store, dispatch_queue_t queue) + { +- STUB(); +- return FALSE; ++ if (store == NULL) ++ return FALSE; ++ ((struct __SCDynamicStore*) store)->queue = queue; ++ return TRUE; + } + + Boolean SCDynamicStoreSetNotificationKeys(SCDynamicStoreRef store, CFArrayRef __nullable keys, CFArrayRef __nullable patterns) { +- STUB(); +- return FALSE; ++ if (store == NULL) ++ return FALSE; ++ struct __SCDynamicStore* s = (struct __SCDynamicStore*) store; ++ if (s->notificationKeys) CFRelease(s->notificationKeys); ++ if (s->notificationPatterns) CFRelease(s->notificationPatterns); ++ s->notificationKeys = keys ? (CFArrayRef) CFRetain(keys) : NULL; ++ s->notificationPatterns = patterns ? (CFArrayRef) CFRetain(patterns) : NULL; ++ /* Accepted: we simply never fire callbacks (no configd to watch). */ ++ return TRUE; + } + ++CFRunLoopSourceRef SCDynamicStoreCreateRunLoopSource(CFAllocatorRef allocator, SCDynamicStoreRef store, CFIndex order) { ++ if (store == NULL) ++ return NULL; ++ /* ++ * darling-lol: return a real (never-firing) runloop source. Chromium's ++ * dns watcher does SCDynamicStoreCreateRunLoopSource + CFRunLoopAddSource; ++ * a NULL source left it with nothing to schedule ("watch failed to ++ * start") and risked a NULL deref/abort downstream. This dummy source ++ * satisfies scheduling; it never signals because Darling has no configd ++ * to deliver changes from. ++ */ ++ CFRunLoopSourceContext ctx; ++ memset(&ctx, 0, sizeof(ctx)); ++ ctx.info = (void*) store; ++ return CFRunLoopSourceCreate(allocator, order, &ctx); ++} ++ ++ + CFStringRef SCDynamicStoreKeyCreateComputerName(CFAllocatorRef allocator) { + STUB(); + return NULL; +@@ -163,3 +425,36 @@ CFStringRef SCDynamicStoreKeyCreateNetworkInterfaceEntity(CFAllocatorRef allocat + STUB(); + return NULL; + }; ++ ++const char* SCErrorString(int status) { ++ /* Minimal Apple-compatible mapping (full table lives in SCD.c, ++ * which is not compiled into Darling's stub framework). Callers ++ * such as Chromium's network-change notifier only log the result, ++ * so a generic fallback is acceptable for unknown codes. */ ++ static const struct { int status; const char *message; } msgs[] = { ++ { kSCStatusOK, "Success!" }, ++ { kSCStatusFailed, "Failed!" }, ++ { kSCStatusInvalidArgument, "Invalid argument" }, ++ { kSCStatusAccessError, "Permission denied" }, ++ { kSCStatusNoKey, "No such key" }, ++ { kSCStatusKeyExists, "Key already defined" }, ++ { kSCStatusLocked, "Lock already held" }, ++ { kSCStatusNeedLock, "Lock required for this operation" }, ++ { kSCStatusNoStoreSession, "Configuration daemon session not active" }, ++ { kSCStatusNoStoreServer, "Configuration daemon not (no longer) available" }, ++ { kSCStatusNotifierActive, "Notifier is currently active" }, ++ { kSCStatusNoPrefsSession, "Preference session not active" }, ++ { kSCStatusPrefsBusy, "Preferences update currently in progress" }, ++ { kSCStatusNoConfigFile, "Configuration file not found" }, ++ { kSCStatusNoLink, "No such link" }, ++ { kSCStatusStale, "Write attempted on stale version of object" }, ++ }; ++ unsigned i; ++ for (i = 0; i < sizeof(msgs)/sizeof(msgs[0]); i++) { ++ if (msgs[i].status == status) ++ return msgs[i].message; ++ } ++ if (status > 0) ++ return strerror(status); ++ return "Unknown error"; ++}; +diff --git a/darling/src/dnsinfo/dnsinfo.c b/darling/src/dnsinfo/dnsinfo.c +index 40f8673..d6b4a1a 100644 +--- a/darling/src/dnsinfo/dnsinfo.c ++++ b/darling/src/dnsinfo/dnsinfo.c +@@ -7,8 +7,6 @@ + #include + #include + +-static char* remove_comment(char* line); +-static char* remove_whitespace(char* line); + + /** + * This is a very simple parser of /etc/resolv.conf. +@@ -19,160 +17,113 @@ static char* remove_whitespace(char* line); + const char * + dns_configuration_notify_key () + { +- return NULL; ++ /* ++ * darling-lol: must be non-NULL. Chromium's mac DNS watcher ++ * (dns_config_watcher_mac.cc) dlopens libSystem, dlsyms this function, ++ * and refuses to start its config watch ("DNS config watch failed to ++ * start", dns_config_service_posix.cc:137) when the returned key is ++ * NULL. The key names a notify(3) registration delivered via notifyd ++ * (now functional); no configd exists to post changes, so the watch ++ * simply never fires -- which is the correct fail-soft. This is Apple's ++ * key string for DNS configuration changes. ++ */ ++ return "com.apple.system.dns.configuration"; + } + + dns_config_t * +-dns_configuration_copy () ++dns_configuration_copy() + { +-#if 0 +- dns_config_t* config; +- dns_resolver_t* resolver; +- FILE* file; +- char buf[256]; +- +- config = (dns_config_t*) malloc(sizeof(dns_config_t)); +- memset(config, 0, sizeof(*config)); +- +- file = fopen("/etc/resolv.conf", "r"); +- if (file == NULL) +- return config; +- +- resolver = (dns_resolver_t*) malloc(sizeof(dns_resolver_t)); +- memset(resolver, 0, sizeof(*resolver)); +- +- config->n_resolver = 1; +- config->resolver = (dns_resolver_t**) malloc(sizeof(void*)); +- config->resolver[0] = resolver; +- +- while (fgets(buf, sizeof(buf), file) != NULL) +- { +- char* line; +- +- line = remove_comment(buf); +- if (line == NULL) +- continue; +- +- line = remove_whitespace(line); +- if (line == NULL || line[0] == 0) +- continue; +- +- if (strncmp(line, "nameserver", 10) == 0) +- { +- char* srv; +- int is_ipv6; +- struct sockaddr* addr; +- int res; +- +- srv = line + 11; +- while (isspace(*srv)) +- srv++; +- +- is_ipv6 = strchr(srv, ':') != NULL; +- +- if (is_ipv6) +- { +- struct sockaddr_in6* a6; +- +- a6 = (struct sockaddr_in6*) malloc(sizeof(struct sockaddr_in6)); +- addr = (struct sockaddr*) a6; +- +- a6->sin6_family = AF_INET6; +- a6->sin6_port = htons(53); +- +- res = inet_pton(AF_INET6, srv, &a6->sin6_addr); +- } +- else +- { +- struct sockaddr_in* a4; +- +- a4 = (struct sockaddr_in*) malloc(sizeof(struct sockaddr_in)); +- addr = (struct sockaddr*) a4; +- +- a4->sin_family = AF_INET; +- a4->sin_port = htons(53); +- +- res = inet_pton(AF_INET, srv, &a4->sin_addr); +- } +- +- if (res == 1) +- { +- resolver->n_nameserver++; +- +- resolver->nameserver = (struct sockaddr**) realloc(resolver->nameserver, +- sizeof(void*) * resolver->n_nameserver); +- +- resolver->nameserver[resolver->n_nameserver-1] = addr; +- } +- else +- { +- free(addr); +- } +- } +- else if (strncmp(line, "search", 6) == 0) +- { +- char* dom; +- +- dom = line + 7; +- while (isspace(*dom)) +- dom++; +- +- resolver->n_search++; +- resolver->search = (char**) realloc(resolver->search, +- sizeof(char*) * resolver->n_search); +- +- resolver->search[resolver->n_search-1] = strdup(dom); +- } +- else if (strncmp(line, "domain", 6) == 0) +- { +- char* dom; +- +- dom = line + 7; +- while (isspace(*dom)) +- dom++; +- +- resolver->domain = strdup(dom); +- } +- } +- +- fclose(file); +- +- return config; +-#else +- return NULL; +-#endif ++ FILE *file = fopen("/etc/resolv.conf", "r"); ++ if (!file) return NULL; ++ dns_config_t *config = calloc(1, sizeof(*config)); ++ dns_resolver_t *resolver = calloc(1, sizeof(*resolver)); ++ if (!config || !resolver) { ++ free(config); free(resolver); fclose(file); return NULL; ++ } ++ config->resolver = calloc(1, sizeof(*config->resolver)); ++ if (!config->resolver) { ++ free(config); free(resolver); fclose(file); return NULL; ++ } ++ config->resolver[0] = resolver; ++ config->n_resolver = 1; ++ resolver->flags = DNS_RESOLVER_FLAGS_REQUEST_ALL_RECORDS; ++ resolver->search_order = DEFAULT_SEARCH_ORDER; ++ char buf[4096]; ++ int failed = 0; ++ while (!failed && fgets(buf, sizeof(buf), file)) { ++ char *comment = strpbrk(buf, "#;"); ++ if (comment) *comment = 0; ++ char *save = NULL; ++ char *key = strtok_r(buf, " \t\r\n", &save); ++ char *value = strtok_r(NULL, " \t\r\n", &save); ++ if (!key || !value) continue; ++ if (!strcmp(key, "nameserver")) { ++ struct sockaddr_storage address = {0}; ++ size_t length; ++ if (strchr(value, ':')) { ++ struct sockaddr_in6 *a = (void *)&address; ++ length = sizeof(*a); a->sin6_len = length; ++ a->sin6_family = AF_INET6; a->sin6_port = htons(53); ++ if (inet_pton(AF_INET6, value, &a->sin6_addr) != 1) continue; ++ } else { ++ struct sockaddr_in *a = (void *)&address; ++ length = sizeof(*a); a->sin_len = length; ++ a->sin_family = AF_INET; a->sin_port = htons(53); ++ if (inet_pton(AF_INET, value, &a->sin_addr) != 1) continue; ++ } ++ struct sockaddr *copy = malloc(length); ++ struct sockaddr **list = realloc(resolver->nameserver, ++ (resolver->n_nameserver + 1) * sizeof(*list)); ++ if (list) resolver->nameserver = list; ++ if (!copy || !list) { free(copy); failed = 1; break; } ++ memcpy(copy, &address, length); ++ list[resolver->n_nameserver++] = copy; ++ } else if (!strcmp(key, "search")) { ++ do { ++ char *copy = strdup(value); ++ char **list = realloc(resolver->search, ++ (resolver->n_search + 1) * sizeof(*list)); ++ if (list) resolver->search = list; ++ if (!copy || !list) { free(copy); failed = 1; break; } ++ list[resolver->n_search++] = copy; ++ } while ((value = strtok_r(NULL, " \t\r\n", &save))); ++ } else if (!strcmp(key, "domain")) { ++ char *copy = strdup(value); ++ if (!copy) { failed = 1; break; } ++ free(resolver->domain); resolver->domain = copy; ++ } else if (!strcmp(key, "options")) { ++ /* Preserve resolver options for libresolv's config reader. */ ++ size_t length = strlen(value); ++ if (save && *save) { value[length] = ' '; length += 1 + strlen(save); } ++ while (length && isspace((unsigned char)value[length - 1])) --length; ++ char *copy = strndup(value, length); ++ if (!copy) { failed = 1; break; } ++ free(resolver->options); resolver->options = copy; ++ } ++ } ++ if (ferror(file)) failed = 1; ++ fclose(file); ++ if (failed || !resolver->n_nameserver) { ++ dns_configuration_free(config); ++ return NULL; ++ } ++ return config; + } + + void +-dns_configuration_free (dns_config_t *config) ++dns_configuration_free(dns_config_t *config) + { +-#if 0 +- if (config->n_resolver > 0) +- { +- dns_resolver_t* resolver; +- +- resolver = config->resolver[0]; +- +- free(resolver->domain); +- +- while (resolver->n_search > 0) +- { +- free(resolver->search[resolver->n_search-1]); +- resolver->n_search--; +- } +- free(resolver->search); +- +- while (resolver->n_nameserver > 0) +- { +- free(resolver->nameserver[resolver->n_nameserver-1]); +- resolver->n_nameserver--; +- } +- free(resolver->nameserver); +- } +- +- free(config->resolver); +- free(config); +-#endif ++ if (!config) return; ++ for (int i = 0; i < config->n_resolver; ++i) { ++ dns_resolver_t *resolver = config->resolver[i]; ++ if (!resolver) continue; ++ free(resolver->domain); free(resolver->options); ++ for (int j = 0; j < resolver->n_search; ++j) free(resolver->search[j]); ++ free(resolver->search); ++ for (int j = 0; j < resolver->n_nameserver; ++j) free(resolver->nameserver[j]); ++ free(resolver->nameserver); free(resolver); ++ } ++ free(config->resolver); free(config); + } + + void +@@ -180,36 +131,3 @@ _dns_configuration_ack (dns_config_t *config, + const char *bundle_id) + { + } +- +-static char* remove_comment(char* line) +-{ +- char* p; +- +- if (line[0] == '#') +- return NULL; +- +- p = strchr(line, '#'); +- if (p != NULL) +- *p = 0; +- +- return line; +-} +- +-static char* remove_whitespace(char* line) +-{ +- char* end; +- +- while (*line && isspace(*line)) +- line++; +- +- end = line + strlen(line) - 1; +- +- while (end > line && isspace(*end)) +- { +- *end = 0; +- end--; +- } +- +- return line; +-} +- diff --git a/patches/corecrypto.patch b/patches/corecrypto.patch new file mode 100644 index 0000000..6e80520 --- /dev/null +++ b/patches/corecrypto.patch @@ -0,0 +1,494 @@ +diff --git a/src/ccder.c b/src/ccder.c +index 5543c42..e843f46 100644 +--- a/src/ccder.c ++++ b/src/ccder.c +@@ -308,8 +308,15 @@ ccder_encode_body(size_t size, const uint8_t* body, + return body_start; + } + ++// As in Apple's implementation, the decoders accept a NULL `der` (the failed ++// result of a previous decode step) and return NULL for input that is ++// truncated or does not carry the expected tag. + const uint8_t *ccder_decode_tag(ccder_tag *tagp, const uint8_t *der, const uint8_t *der_end) { + size_t tag_len = 1; ++ ++ if (der == NULL || der >= der_end) ++ return NULL; ++ + *tagp = ((ccder_tag)(der[0] >> 5)) << CCDER_TAG_MASK_SHIFT_BACK; + + if ((der[0] & 0x1f) == 0x1f) { +@@ -317,11 +324,13 @@ const uint8_t *ccder_decode_tag(ccder_tag *tagp, const uint8_t *der, const uint8 + // 0x80 == 0b10000000 + size_t i = 0; + do { ++ if (der + i + 1 >= der_end) ++ return NULL; + // 0x7f == 0b01111111 + *tagp |= (((ccder_tag)der[i + 1] & 0x7fU) << (i * 7)); + ++i; + ++tag_len; +- } while (der[i + 1] & 0x80); ++ } while (der[i] & 0x80); + *tagp <<= 5; + *tagp |= 0x1f; + } else { +@@ -333,26 +342,40 @@ const uint8_t *ccder_decode_tag(ccder_tag *tagp, const uint8_t *der, const uint8 + } + + const uint8_t* ccder_decode_len(size_t* lenp, const uint8_t* der, const uint8_t* der_end) { +- size_t len_len = 1; ++ size_t len = 0; ++ ++ if (der == NULL || der >= der_end) ++ return NULL; + + if (der[0] & 0x80) { + // long form +- len_len = der[0] & 0x7f; ++ size_t len_len = der[0] & 0x7f; + ++der; ++ if (len_len == 0 || len_len > sizeof(size_t) || (size_t)(der_end - der) < len_len) ++ return NULL; + for (size_t i = len_len; i > 0; --i) +- *lenp |= (size_t)der[len_len - i] << ((i - 1) * 8); ++ len |= (size_t)der[len_len - i] << ((i - 1) * 8); ++ der += len_len; + } else { + // short form +- *lenp = der[0] & 0x7f; ++ len = der[0] & 0x7f; ++ ++der; + } + +- return der + len_len; ++ if ((size_t)(der_end - der) < len) ++ return NULL; ++ ++ *lenp = len; ++ return der; + } + + const uint8_t *ccder_decode_tl(ccder_tag expected_tag, size_t *lenp, + const uint8_t *der, const uint8_t *der_end) { + ccder_tag tag_found = 0; +- return ccder_decode_len(lenp, ccder_decode_tag(&tag_found, der, der_end), der_end); ++ der = ccder_decode_tag(&tag_found, der, der_end); ++ if (der == NULL || tag_found != expected_tag) ++ return NULL; ++ return ccder_decode_len(lenp, der, der_end); + } + + const uint8_t * +@@ -360,6 +383,10 @@ ccder_decode_constructed_tl(ccder_tag expected_tag, const uint8_t **body_end, + const uint8_t *der, const uint8_t *der_end) { + size_t body_len = 0; + const uint8_t* body_start = ccder_decode_tl(expected_tag, &body_len, der, der_end); ++ if (body_start == NULL) { ++ *body_end = der; ++ return NULL; ++ } + *body_end = body_start + body_len; + return body_start; + } +@@ -367,21 +394,16 @@ ccder_decode_constructed_tl(ccder_tag expected_tag, const uint8_t **body_end, + const uint8_t * + ccder_decode_sequence_tl(const uint8_t **body_end, + const uint8_t *der, const uint8_t *der_end) { +- // NOTE(@facekapow): +- // we don't just call `ccder_decode_constructed_tl` because in the future, +- // that method will check that the decoded tag is a CONSTRUCTED tag +- +- size_t body_len = 0; +- const uint8_t* body_start = ccder_decode_tl(CCDER_SEQUENCE, &body_len, der, der_end); +- *body_end = body_start + body_len; +- return body_start; ++ return ccder_decode_constructed_tl(CCDER_CONSTRUCTED_SEQUENCE, body_end, der, der_end); + } + + const uint8_t *ccder_decode_uint_n(cc_size *n, + const uint8_t *der, const uint8_t *der_end) { + size_t num_len = 0; + const uint8_t* num_start = ccder_decode_tl(CCDER_INTEGER, &num_len, der, der_end); +- while (num_start[0] == 0) { ++ if (num_start == NULL) ++ return NULL; ++ while (num_len > 0 && num_start[0] == 0) { + --num_len; + ++num_start; + } +@@ -393,7 +415,9 @@ const uint8_t *ccder_decode_uint(cc_size n, cc_unit *r, + const uint8_t *der, const uint8_t *der_end) { + size_t num_len = 0; + const uint8_t* num_start = ccder_decode_tl(CCDER_INTEGER, &num_len, der, der_end); +- while (num_start[0] == 0) { ++ if (num_start == NULL) ++ return NULL; ++ while (num_len > 0 && num_start[0] == 0) { + --num_len; + ++num_start; + } +@@ -405,7 +429,9 @@ const uint8_t *ccder_decode_uint64(uint64_t* r, + const uint8_t *der, const uint8_t *der_end) { + size_t num_len = 0; + const uint8_t* num_start = ccder_decode_tl(CCDER_INTEGER, &num_len, der, der_end); +- while (num_start[0] == 0) { ++ if (num_start == NULL) ++ return NULL; ++ while (num_len > 0 && num_start[0] == 0) { + --num_len; + ++num_start; + } +@@ -422,13 +448,15 @@ const uint8_t *ccder_decode_uint64(uint64_t* r, + const uint8_t *ccder_decode_seqii(cc_size n, cc_unit *r, cc_unit *s, + const uint8_t *der, const uint8_t *der_end) { + size_t seq_len = 0; +- return ccder_decode_uint(n, s, ccder_decode_uint(n, r, ccder_decode_tl(CCDER_SEQUENCE, &seq_len, der, der_end), der_end), der_end); ++ return ccder_decode_uint(n, s, ccder_decode_uint(n, r, ccder_decode_tl(CCDER_CONSTRUCTED_SEQUENCE, &seq_len, der, der_end), der_end), der_end); + } + + const uint8_t *ccder_decode_oid(ccoid_t *oidp, + const uint8_t *der, const uint8_t *der_end) { + size_t oid_len = 0; + const uint8_t* oid_start = ccder_decode_tl(CCDER_OBJECT_IDENTIFIER, &oid_len, der, der_end); ++ if (oid_start == NULL) ++ return NULL; + size_t total_len = (size_t)(oid_start - der) + oid_len; + + CCOID(*oidp) = der; +@@ -441,6 +469,8 @@ const uint8_t *ccder_decode_bitstring(const uint8_t **bit_string, + const uint8_t *der, const uint8_t *der_end) { + size_t byte_len = 0; + const uint8_t* bit_start = ccder_decode_tl(CCDER_BIT_STRING, &byte_len, der, der_end); ++ if (bit_start == NULL) ++ return NULL; + + // NOTE(@facekapow): + // i'm not sure if Apple's API meant to include the *entire* bitstring including the leading +diff --git a/src/ccec.c b/src/ccec.c +index 8efd8fb..8c8dfcc 100644 +--- a/src/ccec.c ++++ b/src/ccec.c +@@ -458,18 +458,17 @@ static const struct { + CC_UNIT_FORMAT_2(01, ff), + // } + // reciprocal of modulus p { +- // full hex = 20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001 +- CC_UNIT_FORMAT_8(00, 00, 00, 00, 00, 00, 00, 01), ++ // full hex = 80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000 + CC_UNIT_FORMAT_8(00, 00, 00, 00, 00, 00, 00, 00), ++ CC_UNIT_FORMAT_8(00, 00, 40, 00, 00, 00, 00, 00), + CC_UNIT_FORMAT_8(00, 00, 00, 00, 00, 00, 00, 00), + CC_UNIT_FORMAT_8(00, 00, 00, 00, 00, 00, 00, 00), + CC_UNIT_FORMAT_8(00, 00, 00, 00, 00, 00, 00, 00), + CC_UNIT_FORMAT_8(00, 00, 00, 00, 00, 00, 00, 00), + CC_UNIT_FORMAT_8(00, 00, 00, 00, 00, 00, 00, 00), + CC_UNIT_FORMAT_8(00, 00, 00, 00, 00, 00, 00, 00), +- CC_UNIT_FORMAT_2(02, 00), +- // necessary because the reciprocal *must* be `n + 1` units in length +- CC_UNIT_FORMAT_1(00), ++ CC_UNIT_FORMAT_8(00, 00, 00, 00, 00, 00, 00, 00), ++ CC_UNIT_FORMAT_8(00, 80, 00, 00, 00, 00, 00, 00), + // } + // coefficient a { + // full hex = 01fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc +@@ -532,18 +531,17 @@ static const struct { + CC_UNIT_FORMAT_2(01, ff), + // } + // reciprocal of order n { +- // full hex = 2000000000000000000000000000000000000000000000000000000000000000005ae79787c40d069948033feb708f65a2fc44a36477663b851449048e16ec79bf7 +- CC_UNIT_FORMAT_8(44, 90, 48, e1, 6e, c7, 9b, f7), +- CC_UNIT_FORMAT_8(c4, 4a, 36, 47, 76, 63, b8, 51), +- CC_UNIT_FORMAT_8(80, 33, fe, b7, 08, f6, 5a, 2f), +- CC_UNIT_FORMAT_8(ae, 79, 78, 7c, 40, d0, 69, 94), +- CC_UNIT_FORMAT_8(00, 00, 00, 00, 00, 00, 00, 05), ++ // full hex = 8000000000000000000000000000000000000000000000000000000000000000016b9e5e1f10341a65200cffadc23d968bf1128d91dd98ee14512412385bb1e6fdc408f501c8d1cd2dad1d7f46221c ++ CC_UNIT_FORMAT_8(cd, 2d, ad, 1d, 7f, 46, 22, 1c), ++ CC_UNIT_FORMAT_8(e6, fd, c4, 08, f5, 01, c8, d1), ++ CC_UNIT_FORMAT_8(ee, 14, 51, 24, 12, 38, 5b, b1), ++ CC_UNIT_FORMAT_8(96, 8b, f1, 12, 8d, 91, dd, 98), ++ CC_UNIT_FORMAT_8(1a, 65, 20, 0c, ff, ad, c2, 3d), ++ CC_UNIT_FORMAT_8(00, 01, 6b, 9e, 5e, 1f, 10, 34), + CC_UNIT_FORMAT_8(00, 00, 00, 00, 00, 00, 00, 00), + CC_UNIT_FORMAT_8(00, 00, 00, 00, 00, 00, 00, 00), + CC_UNIT_FORMAT_8(00, 00, 00, 00, 00, 00, 00, 00), +- CC_UNIT_FORMAT_2(02, 00), +- // necessary because the reciprocal *must* be `n + 1` units in length +- CC_UNIT_FORMAT_1(00), ++ CC_UNIT_FORMAT_8(00, 80, 00, 00, 00, 00, 00, 00), + // } + // cofactor h { + // full hex = 01 +@@ -624,7 +622,9 @@ int ccec_import_pub(ccec_const_cp_t cp, size_t in_len, const uint8_t* in, ccec_p + } + + const cc_size n = cczp_n(cp.zp); +- const cc_size size = ccn_sizeof_n(n); ++ // X9.63 coordinates are as wide as the prime, not as the limbs holding it ++ // (P-521 is 66 bytes in 9 limbs, i.e. 72 bytes) ++ const size_t size = ccec_cp_prime_size(cp); + + if (in_len != (2 * size) + 1) { + #if DEBUG +@@ -675,7 +675,24 @@ int ccec_verify(ccec_pub_ctx_t key, size_t digest_len, const uint8_t* digest, si + cc_unit e[n]; + memset(e, 0, sizeof e); + +- ccn_read_uint(n, e, (log_2_n >= 8 * digest_len) ? (log_2_n + 7) / 8 : digest_len, digest); ++ { ++ // ANSI X9.62: e is the leftmost bitlen(order) bits of the digest ++ size_t use_len = digest_len; ++ if (8 * use_len > log_2_n) ++ use_len = (log_2_n + 7) / 8; ++ ++ ccn_read_uint(n, e, use_len, digest); ++ ++ const size_t excess_bits = 8 * use_len - log_2_n; ++ if (8 * use_len > log_2_n && excess_bits > 0) { ++ // excess_bits is below 8, so a single pass over the units suffices ++ for (cc_size i = 0; i < n; ++i) { ++ e[i] >>= excess_bits; ++ if (i + 1 < n) ++ e[i] |= e[i + 1] << (CCN_UNIT_BITS - excess_bits); ++ } ++ } ++ } + + cc_unit r[n]; + cc_unit s[n]; +diff --git a/src/ccn.c b/src/ccn.c +index 3bfc8d1..e5c26b0 100644 +--- a/src/ccn.c ++++ b/src/ccn.c +@@ -216,185 +216,37 @@ void ccn_lcm(cc_size n, cc_unit *r2n, const cc_unit *s, const cc_unit *t) { + #define CC_COMBINE(lo, hi) ((hi << (CCN_UNIT_BITS >> 1)) | lo) + + void ccn_mul(cc_size n, cc_unit *r_2n, const cc_unit *s, const cc_unit *t) { +- // we're using the karatsuba algorithm in base 2^64 +- // in the following pseudocode, B is the base of the numbers +- /* +- procedure karatsuba(num1, num2) +- if (num1 < B) and (num2 < B) +- return num1 * num2 +- +- // Calculates the size of the numbers. +- m = max(size_base(num1, B), size_base(num2, B)) +- m2 = floor(m / 2) +- +- // Split the digit sequences in the middle. +- high1, low1 = split_at(num1, m2) +- high2, low2 = split_at(num2, m2) +- +- // 3 calls made to numbers approximately half the size. +- z0 = karatsuba(low1, low2) +- z1 = karatsuba(high1, high2) +- z2 = karatsuba((low1 + high1), (low2 + high2)) +- +- t0 = (z2 - z0 - z1) * (B ** m2) +- t1 = z1 * (B ** (m2 * 2)) +- // this is equivalent to: +- // t0 = (z2 - z0 - z1) << m2 +- // t1 = z1 << (m2 * 2) +- +- return t0 + t1 + z0 +- */ +- cc_size s_n = ccn_n(n, s); +- cc_size t_n = ccn_n(n, t); +- ++ // schoolbook multiplication; the operands here are at most a few dozen ++ // units, where it is faster than any divide and conquer scheme. ++ // the result must not overlap the operands. + memset(r_2n, 0, ccn_sizeof_n(n * 2)); + +- if (s_n < 2 && t_n < 2) { +- // multiplication algorithm that prevents overflow +- // based on https://stackoverflow.com/a/1815371 +- const cc_unit s_hi = CC_HI_BITS(s[0]); +- const cc_unit s_lo = CC_LO_BITS(s[0]); +- const cc_unit t_hi = CC_HI_BITS(t[0]); +- const cc_unit t_lo = CC_LO_BITS(t[0]); +- +- cc_unit x = s_lo * t_lo; +- const cc_unit prod_0 = CC_LO_BITS(x); +- +- x = s_hi * t_lo + CC_HI_BITS(x); +- cc_unit prod_1 = CC_LO_BITS(x); +- cc_unit prod_2 = CC_HI_BITS(x); +- +- x = prod_1 + s_lo * t_hi; +- prod_1 = CC_LO_BITS(x); +- +- x = prod_2 + s_hi * t_hi + CC_HI_BITS(x); ++#if CCN_UNIT_SIZE == 8 ++ for (cc_size i = 0; i < n; ++i) { ++ unsigned __int128 carry = 0; ++ const cc_unit s_i = s[i]; + +- r_2n[0] = CC_COMBINE(prod_0, prod_1); +- r_2n[1] = x; ++ for (cc_size j = 0; j < n; ++j) { ++ const unsigned __int128 accumulator = (unsigned __int128)s_i * t[j] + r_2n[i + j] + carry; ++ r_2n[i + j] = (cc_unit)accumulator; ++ carry = accumulator >> 64; ++ } + +- return; ++ r_2n[i + n] = (cc_unit)carry; + } ++#else ++ for (cc_size i = 0; i < n; ++i) { ++ uint64_t carry = 0; + +- cc_size max_n = s_n > t_n ? s_n : t_n; +- cc_size half_n = (max_n + 1) / 2; +- cc_size result_n = max_n * 2; +- cc_size intermediate_mul_n = (half_n * 2) + 2; +- +- cc_size max_size = ccn_sizeof_n(max_n); +- cc_size half_size = ccn_sizeof_n(half_n); +- cc_size half_size_for_addition = ccn_sizeof_n(half_n + 1); +- cc_size result_size = ccn_sizeof_n(result_n); +- cc_size intermediate_mul_size = ccn_sizeof_n(intermediate_mul_n); +- cc_size hi_half_size = max_size - half_size; +- +- // half_size_for_addition is used to allocate enough for addition +- // later when we do `hi + lo -> lo` +- +- cc_unit* s_hi = __builtin_alloca(half_size); +- cc_unit* s_lo = __builtin_alloca(half_size_for_addition); +- +- cc_unit* t_hi = __builtin_alloca(half_size); +- cc_unit* t_lo = __builtin_alloca(half_size_for_addition); +- +- memset(s_hi, 0, half_size); +- memset(s_lo, 0, half_size_for_addition); +- memset(t_hi, 0, half_size); +- memset(t_lo, 0, half_size_for_addition); +- +- // copy `half_size` bytes from `s` into `s_lo` +- memcpy(s_lo, s, half_size); +- // copy the rest into `s_hi` +- memcpy(s_hi, (uint8_t*)s + half_size, hi_half_size); +- +- // copy `half_size` bytes from `t` into `t_lo` +- memcpy(t_lo, t, half_size); +- // copy the rest into `t_hi` +- memcpy(t_hi, (uint8_t*)t + half_size, hi_half_size); +- +- cc_unit* z0 = __builtin_alloca(intermediate_mul_size); +- cc_unit* z1 = __builtin_alloca(intermediate_mul_size); +- cc_unit* z2 = __builtin_alloca(intermediate_mul_size); +- +- memset(z0, 0, intermediate_mul_size); +- memset(z1, 0, intermediate_mul_size); +- memset(z2, 0, intermediate_mul_size); +- +- // z0 = s_lo * t_lo +- ccn_mul(half_n, z0, s_lo, t_lo); +- // z1 = s_hi * t_hi +- ccn_mul(half_n, z1, s_hi, t_hi); +- +- // we add the `hi`s directly into the `lo`s to avoid additional allocations +- +- // s_lo = s_lo + s_hi +- cc_unit s_carry = ccn_add(half_n, s_lo, s_lo, s_hi); +- // t_lo = t_lo + s_hi +- cc_unit t_carry = ccn_add(half_n, t_lo, t_lo, t_hi); +- +- // this is safe; remember we already allocated an extra unit in the `lo`s +- s_lo[half_n] = s_carry; +- t_lo[half_n] = t_carry; +- +- // z2 = (s_lo + s_hi) * (t_lo + t_hi) +- ccn_mul(half_n + 1, z2, s_lo, t_lo); +- +- cc_unit* t0 = __builtin_alloca(result_size); +- cc_unit* t1 = __builtin_alloca(result_size); +- +- memset(t0, 0, result_size); +- memset(t1, 0, result_size); +- +- // since we don't use z2 anymore, we can just subtract z1 and z0 +- // directly into it to avoid additional allocations +- +- // z2 = z2 - z1 +- ccn_sub(intermediate_mul_n, z2, z2, z1); +- // z2 = z2 - z0 +- ccn_sub(intermediate_mul_n, z2, z2, z0); +- +- // t0 = z2 - z1 - z0 +- // technically, this is `t0 = z2` because we already subtracted them into z2 +- ccn_set(intermediate_mul_n, t0, z2); +- // t0 = z1 +- ccn_set(intermediate_mul_n, t1, z1); +- +- const cc_unit* t0_end = t0 + result_n; +- const cc_unit* t1_end = t1 + result_n; +- +- // t0 = t0 << half_n +- // +- // shift t0 to the left by `half_n` units +- // (which in little endian is actually shifting the bytes right) +- // +- // why don't we use `ccn_shift_left`? remember that we're in base 2^64 +- // `ccn_shift_left` shifts base-2 bits, not base-2^64 bits (i.e. units) +- for (cc_size i = intermediate_mul_n; i > 0; --i) { +- cc_unit* tgt = t0 + half_n + (i - 1); +- // make sure that the target isn't past our boundary +- if (tgt < t0_end) +- *tgt = t0[i - 1]; +- } +- // zero the previously occupied bytes +- for (cc_size i = 0; i < half_n; ++i) +- t0[i] = 0; ++ for (cc_size j = 0; j < n; ++j) { ++ const uint64_t accumulator = (uint64_t)s[i] * t[j] + r_2n[i + j] + carry; ++ r_2n[i + j] = (cc_unit)accumulator; ++ carry = accumulator >> CCN_UNIT_BITS; ++ } + +- // t1 = t1 << (half_n * 2) +- // +- // shift t1 to the left by `half_n * 2` units +- for (cc_size i = intermediate_mul_n; i > 0; --i) { +- cc_unit* tgt = t1 + (half_n * 2) + (i - 1); +- // make sure that the target isn't past our boundary +- if (tgt < t1_end) +- *tgt = t1[i - 1]; ++ r_2n[i + n] = (cc_unit)carry; + } +- // zero the previously occupied bytes +- for (cc_size i = 0; i < half_n * 2; ++i) +- t1[i] = 0; +- +- // r_2n = z0 + t0 + t1 +- ccn_set(intermediate_mul_n, r_2n, z0); +- ccn_add(result_n, r_2n, r_2n, t0); +- ccn_add(result_n, r_2n, r_2n, t1); ++#endif + } + + void ccn_mul_ws(cc_size count, cc_unit *r, const cc_unit *s, const cc_unit *t, cc_ws_t ws) { +diff --git a/src/cczp.c b/src/cczp.c +index 7a7d0a6..b4d3081 100644 +--- a/src/cczp.c ++++ b/src/cczp.c +@@ -114,13 +114,11 @@ void cczp_mod(cczp_const_t zp, cc_unit* result, const cc_unit* operand, cc_ws_t + memset(r_extra, 0, normal_size); + bool underflow = ccn_sub(n + 1, r_extra, r_1, r_2); + +- // `if r < 0` +- if (underflow) { +- for (cc_size i = 0; i < n + 1; ++i) +- r_extra[i] = ~r_extra[i]; +- +- ccn_add1(n + 1, r_extra, r_extra, 1); +- } ++ // `if r < 0, r = r + b^(k + 1)` ++ // the subtraction above wrapped modulo b^(k + 1), which is exactly that ++ // addition. (Negating the wrapped value instead yields something close to ++ // b^(k + 1), and the loop below would then run for ~2^64 iterations.) ++ (void)underflow; + + // `while r >= m` + while (ccn_cmp(n + 1, r_extra, m_copy) >= 0) { diff --git a/patches/corefoundation.patch b/patches/corefoundation.patch new file mode 100644 index 0000000..c9b4c4f --- /dev/null +++ b/patches/corefoundation.patch @@ -0,0 +1,323 @@ +diff --git a/CFError.c b/CFError.c +index 91b5a2b..0f7b4eb 100644 +--- a/CFError.c ++++ b/CFError.c +@@ -30,6 +30,7 @@ + #include + #include "CFInternal.h" + #include ++#include /* darling-lol: mincore for isa validation */ + #if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED + #include + #endif +@@ -380,6 +381,19 @@ CFDictionaryRef CFErrorCopyUserInfo(CFErrorRef err) { + } + + CFStringRef CFErrorCopyDescription(CFErrorRef err) { ++#ifdef DARLING ++ /* Fail soft on NULL/unmapped-isa errors: callers (and some Darling ++ * stubs that fail without setting out-errors) can hand us garbage; ++ * the description is logging-only, crashing here helps nothing. */ ++ if (!err) ++ return CFSTR("(null error)"); ++ { ++ uintptr_t isa = ((CFRuntimeBase *)err)->_cfisa; ++ char vec = 0; ++ if (isa == 0 || mincore((void *)(isa & ~0xFFFUL), 4096, &vec) != 0) ++ return CFSTR("(null error)"); ++ } ++#endif + if (CF_IS_OBJC(CFErrorGetTypeID(), err)) { // Since we have to return a retained result, we need to treat the toll-free bridging specially + CFStringRef desc = (CFStringRef) CF_OBJC_CALLV((NSError *)err, localizedDescription); + return desc ? (CFStringRef)CFRetain(desc) : NULL; // !!! It really should never return nil. +@@ -389,6 +403,19 @@ CFStringRef CFErrorCopyDescription(CFErrorRef err) { + } + + CFStringRef CFErrorCopyFailureReason(CFErrorRef err) { ++#ifdef DARLING ++ /* Fail soft on NULL/unmapped-isa errors: callers (and some Darling ++ * stubs that fail without setting out-errors) can hand us garbage; ++ * the description is logging-only, crashing here helps nothing. */ ++ if (!err) ++ return CFSTR("(null error)"); ++ { ++ uintptr_t isa = ((CFRuntimeBase *)err)->_cfisa; ++ char vec = 0; ++ if (isa == 0 || mincore((void *)(isa & ~0xFFFUL), 4096, &vec) != 0) ++ return CFSTR("(null error)"); ++ } ++#endif + if (CF_IS_OBJC(CFErrorGetTypeID(), err)) { // Since we have to return a retained result, we need to treat the toll-free bridging specially + CFStringRef str = (CFStringRef) CF_OBJC_CALLV((NSError *)err, localizedFailureReason); + return str ? (CFStringRef)CFRetain(str) : NULL; // It's possible for localizedFailureReason to return nil +@@ -398,6 +425,19 @@ CFStringRef CFErrorCopyFailureReason(CFErrorRef err) { + } + + CFStringRef CFErrorCopyRecoverySuggestion(CFErrorRef err) { ++#ifdef DARLING ++ /* Fail soft on NULL/unmapped-isa errors: callers (and some Darling ++ * stubs that fail without setting out-errors) can hand us garbage; ++ * the description is logging-only, crashing here helps nothing. */ ++ if (!err) ++ return CFSTR("(null error)"); ++ { ++ uintptr_t isa = ((CFRuntimeBase *)err)->_cfisa; ++ char vec = 0; ++ if (isa == 0 || mincore((void *)(isa & ~0xFFFUL), 4096, &vec) != 0) ++ return CFSTR("(null error)"); ++ } ++#endif + if (CF_IS_OBJC(CFErrorGetTypeID(), err)) { // Since we have to return a retained result, we need to treat the toll-free bridging specially + CFStringRef str = (CFStringRef) CF_OBJC_CALLV((NSError *)err, localizedRecoverySuggestion); + return str ? (CFStringRef)CFRetain(str) : NULL; // It's possible for localizedRecoverySuggestion to return nil +diff --git a/CFRunLoop.c b/CFRunLoop.c +index e932ea5..522bc72 100644 +--- a/CFRunLoop.c ++++ b/CFRunLoop.c +@@ -27,6 +27,7 @@ + */ + + #include ++extern int __simple_printf(const char *, ...); /* darling-lol M58F20 */ + #include + #include + #include +@@ -38,6 +39,14 @@ + #include + #include + ++/* Opt-in bounded idle waits while investigating lost Mach run-loop wakeups. ++ * Read configuration at load time, never from syscall wrappers. */ ++static Boolean darlingBoundedRunLoopWait; ++__attribute__((constructor)) static void darlingConfigureRunLoopWait(void) { ++ const char *value = getenv("RCLOL_CF_POLL"); ++ darlingBoundedRunLoopWait = value && value[0] == '1'; ++} ++ + + #if DEPLOYMENT_TARGET_WINDOWS + #include +@@ -2209,6 +2218,8 @@ static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInter + #define TIMEOUT_INFINITY (~(mach_msg_timeout_t)0) + + static Boolean __CFRunLoopServiceMachPort(mach_port_name_t port, mach_msg_header_t **buffer, size_t buffer_size, mach_port_t *livePort, mach_msg_timeout_t timeout, voucher_mach_msg_state_t *voucherState, voucher_t *voucherCopy) { ++ if (darlingBoundedRunLoopWait && timeout == TIMEOUT_INFINITY) ++ timeout = 10; + Boolean originalBuffer = true; + kern_return_t ret = KERN_SUCCESS; + for (;;) { /* In that sleep of death what nightmares may come ... */ +@@ -2417,6 +2428,11 @@ static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInter + + Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR); + ++ // darling-lol: every Mach port-set change and receive is a synchronous round trip to darlingserver here. ++ // A zero-timeout pass that has just polled the dispatch port on its own does not need to put that same ++ // port into the wait set (insert) and take it out again (extract) around the non-blocking set poll: a ++ // message that lands in between is picked up by the very next pass. ++ Boolean darlingSkipDispatchSet = poll && (MACH_PORT_NULL != dispatchPort) && !didDispatchPortLastTime; + if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) { + #if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI + msg = (mach_msg_header_t *)msg_buffer; +@@ -2440,7 +2456,7 @@ static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInter + // iteration, as this mode could be run re-entrantly and we don't + // want these ports to get serviced. + +- __CFPortSetInsert(dispatchPort, waitSet); ++ if (!darlingSkipDispatchSet) __CFPortSetInsert(dispatchPort, waitSet); + + __CFRunLoopModeUnlock(rlm); + __CFRunLoopUnlock(rl); +@@ -2500,7 +2516,7 @@ static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInter + // want these ports to get serviced. Also, we don't want them left + // in there if this function returns. + +- __CFPortSetRemove(dispatchPort, waitSet); ++ if (!darlingSkipDispatchSet) __CFPortSetRemove(dispatchPort, waitSet); + + __CFRunLoopSetIgnoreWakeUps(rl); + +@@ -2550,7 +2566,11 @@ static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInter + #endif + if (MACH_PORT_NULL == livePort) { + CFRUNLOOP_WAKEUP_FOR_NOTHING(); +- // handle nothing ++ /* A bounded wait or zero-timeout poll must check due timers when ++ * their Mach notification was missed. Otherwise AppKit observes ++ * an overdue timer, skips its idle wait, and polls forever. */ ++ if (darlingBoundedRunLoopWait || poll) ++ __CFRunLoopDoTimers(rl, rlm, mach_absolute_time()); + } else if (livePort == rl->_wakeUpPort) { + CFRUNLOOP_WAKEUP_FOR_WAKEUP(); + // do nothing on Mac OS +@@ -2719,21 +2739,46 @@ void CFRunLoopWakeUp(CFRunLoopRef rl) { + CHECK_FOR_FORK(); + // This lock is crucial to ignorable wakeups, do not remove it. + __CFRunLoopLock(rl); ++ /* darling-lol M58F32: do NOT drop the wakeup when ignoreWakeUps is set. ++ * ++ * The upstream guard skips sending the wake while the runloop is in its ++ * post-wait "handling" phase (flag set at CFRunLoop.c ~2514/2521, cleared ++ * only before the next Mach wait at ~2400). On macOS that window is short, ++ * so a skipped wake is harmless - the runloop is already awake and about to ++ * run its sources. Under Darling the browser main thread's handling phase ++ * can be very long (the browser loop runs seconds-late/starves; see the ++ * M58n/M58o/M58ad findings). In that state a cross-thread PostTask onto the ++ * main thread's TASK_ENVIRONMENT_DEFAULT_TQ schedules exactly this wake, and ++ * dropping it means the queued task is not noticed until the runloop next ++ * parks and an unrelated event wakes it - which is the observed black-window ++ * defect (M58f25: no WAKE_SEND at the notification moment; M58f31: 6 dropped ++ * wakes on the CrBrowserMain runloop). ++ * ++ * The send is deliberately unconditional below ("we don't want to lose a ++ * wakeup"), and the wake-up port queue length is 1, so an extra wake while ++ * handling is harmless (it coalesces). We therefore fall through to send it ++ * even when the flag is set, instead of returning early. */ ++#if 0 + if (__CFRunLoopIsIgnoringWakeUps(rl)) { + __CFRunLoopUnlock(rl); + return; + } ++#endif + #if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI + kern_return_t ret; + /* We unconditionally try to send the message, since we don't want + * to lose a wakeup, but the send may fail if there is already a + * wakeup pending, since the queue length is 1. */ +- ret = __CFSendTrivialMachMessage(rl->_wakeUpPort, 0, MACH_SEND_TIMEOUT, 0); ++ /* darling-lol: the send is a darlingserver round trip (~50 us); do it after dropping the run loop lock so other ++ * threads (including the run loop's own thread) do not block on the lock for the duration of the RPC. */ ++ mach_port_t wakePort = rl->_wakeUpPort; ++ __CFRunLoopUnlock(rl); ++ ret = __CFSendTrivialMachMessage(wakePort, 0, MACH_SEND_TIMEOUT, 0); + if (ret != MACH_MSG_SUCCESS && ret != MACH_SEND_TIMED_OUT) CRASH("*** Unable to send message to wake up port. (%d) ***", ret); + #elif DEPLOYMENT_TARGET_WINDOWS + SetEvent(rl->_wakeUpPort); +-#endif + __CFRunLoopUnlock(rl); ++#endif + } + + void CFRunLoopStop(CFRunLoopRef rl) { +@@ -2828,6 +2873,10 @@ void CFRunLoopPerformBlock(CFRunLoopRef rl, CFTypeRef mode, void (^block)(void)) + + Boolean CFRunLoopContainsSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName) { + CHECK_FOR_FORK(); ++#ifdef DARLING ++ /* See CFRunLoopAddSource: NULL sources are never contained. */ ++ if (NULL == rls) return false; ++#endif + CFRunLoopModeRef rlm; + Boolean hasValue = false; + __CFRunLoopLock(rl); +@@ -2849,6 +2898,13 @@ Boolean CFRunLoopContainsSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStrin + void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName) { /* DOES CALLOUT */ + CHECK_FOR_FORK(); + if (__CFRunLoopIsDeallocating(rl)) return; ++#ifdef DARLING ++ /* Darling's IOKit has no iokitd, so notification-port lookups can ++ * legitimately yield NULL where Apple always returns a port. ++ * Ignore NULL sources instead of crashing (matches the fail-soft ++ * guards in IOKitUser). */ ++ if (NULL == rls) return; ++#endif + if (!__CFIsValid(rls)) return; + Boolean doVer0Callout = false; + __CFRunLoopLock(rl); +@@ -2910,6 +2966,10 @@ void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef mod + + void CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName) { /* DOES CALLOUT */ + CHECK_FOR_FORK(); ++#ifdef DARLING ++ /* See CFRunLoopAddSource: ignore NULL sources. */ ++ if (NULL == rls) return; ++#endif + Boolean doVer0Callout = false, doRLSRelease = false; + __CFRunLoopLock(rl); + if (modeName == kCFRunLoopCommonModes) { +@@ -3432,6 +3492,12 @@ void CFRunLoopSourceSignal(CFRunLoopSourceRef rls) { + __CFRunLoopSourceSetSignaled(rls); + } + __CFRunLoopSourceUnlock(rls); ++ #if 0 /* darling-lol: keep hot source-signal tracing off by default. */ ++ { /* darling-lol M58F25: source-signal trace (bounded) */ ++ static unsigned long _m58f25 = 0; ++ if (_m58f25++ < 4000) __simple_printf("M58F25 SIG_SRC rls=%p tid=0x%x\n", (void*)rls, (unsigned)pthread_mach_thread_np(pthread_self())); ++ } ++ #endif + } + + Boolean CFRunLoopSourceIsSignalled(CFRunLoopSourceRef rls) { +@@ -3966,4 +4032,3 @@ void CFRunLoopTimerSetTolerance(CFRunLoopTimerRef rlt, CFTimeInterval tolerance) + } + #endif + } +- +diff --git a/CFString.c b/CFString.c +index 162403a..b9ad8a0 100644 +--- a/CFString.c ++++ b/CFString.c +@@ -6572,3 +6572,24 @@ void CFShowStr(CFStringRef str) { + #undef HANGUL_TCOUNT + #undef HANGUL_NCOUNT + ++ ++/* Darling has no locale hyphenation dictionaries. Advertise that capability ++ * truthfully instead of leaving the macOS APIs absent (which aborts callers ++ * at lazy symbol binding). Ordinary line wrapping remains available. */ ++Boolean CFStringIsHyphenationAvailableForLocale(CFLocaleRef locale) { ++ (void)locale; ++ return false; ++} ++ ++CFIndex CFStringGetHyphenationLocationBeforeIndex(CFStringRef string, ++ CFIndex location, CFRange limitRange, CFOptionFlags options, ++ CFLocaleRef locale, UTF32Char *character) { ++ (void)string; ++ (void)location; ++ (void)limitRange; ++ (void)options; ++ (void)locale; ++ if (character != NULL) ++ *character = 0; ++ return kCFNotFound; ++} +diff --git a/NSArray.m b/NSArray.m +index 30ac9df..b389210 100644 +--- a/NSArray.m ++++ b/NSArray.m +@@ -1232,6 +1232,15 @@ SINGLETON_RR() + return success; + } + ++- (BOOL)writeToURL:(NSURL *)url error:(NSError **)error ++{ ++ BOOL success = [self writeToURL:url atomically:YES]; ++ if (!success && error != NULL) { ++ *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteUnknownError userInfo:nil]; ++ } ++ return success; ++} ++ + - (void)makeObjectsPerformSelector:(SEL)sel + { + [self makeObjectsPerformSelector:sel withObject:nil]; +diff --git a/NSDictionary.m b/NSDictionary.m +index a3da56a..4678733 100644 +--- a/NSDictionary.m ++++ b/NSDictionary.m +@@ -986,6 +986,15 @@ static NSString *_getDescription(id obj, id locale, int level) + return success; + } + ++- (BOOL)writeToURL:(NSURL *)url error:(NSError **)error ++{ ++ BOOL success = [self writeToURL:url atomically:YES]; ++ if (!success && error != NULL) { ++ *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteUnknownError userInfo:nil]; ++ } ++ return success; ++} ++ + - (NSArray *)keysSortedByValueUsingSelector:(SEL)comparator + { + return [[self allKeys] sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) { diff --git a/patches/curl.patch b/patches/curl.patch new file mode 100644 index 0000000..ec6198e --- /dev/null +++ b/patches/curl.patch @@ -0,0 +1,49 @@ +diff --git a/curl/lib/sendf.c b/curl/lib/sendf.c +index 5008d93..6419694 100644 +--- a/curl/lib/sendf.c ++++ b/curl/lib/sendf.c +@@ -261,14 +261,29 @@ void Curl_infof(struct Curl_easy *data, const char *fmt, ...) + + void Curl_failf(struct Curl_easy *data, const char *fmt, ...) + { +- if(data->set.verbose || data->set.errorbuffer) { +- va_list ap; +- size_t len; +- char error[CURL_ERROR_SIZE + 2]; +- va_start(ap, fmt); +- mvsnprintf(error, CURL_ERROR_SIZE, fmt, ap); +- len = strlen(error); ++ va_list ap; ++ size_t len; ++ char error[CURL_ERROR_SIZE + 2]; + ++ va_start(ap, fmt); ++ mvsnprintf(error, CURL_ERROR_SIZE, fmt, ap); ++ va_end(ap); ++ len = strlen(error); ++ ++ /* darling-lol M58F139: always emit the libcurl error to the guest console ++ * (fd 2) so the Riot launcher/patcher failure under Darling is named. */ ++ { ++ extern long write(int, const void *, unsigned long); ++ char buf[CURL_ERROR_SIZE + 16]; ++ size_t n = len; ++ if(n > CURL_ERROR_SIZE) n = CURL_ERROR_SIZE; ++ memcpy(buf, "LOLCURL ", 8); ++ memcpy(buf + 8, error, n); ++ buf[8 + n] = '\n'; ++ write(2, buf, 8 + n + 1); ++ } ++ ++ if(data->set.verbose || data->set.errorbuffer) { + if(data->set.errorbuffer && !data->state.errorbuf) { + strcpy(data->set.errorbuffer, error); + data->state.errorbuf = TRUE; /* wrote error string */ +@@ -278,7 +293,6 @@ void Curl_failf(struct Curl_easy *data, const char *fmt, ...) + error[++len] = '\0'; + Curl_debug(data, CURLINFO_TEXT, error, len); + } +- va_end(ap); + } + } + diff --git a/patches/darling.patch b/patches/darling.patch new file mode 100644 index 0000000..989ef52 --- /dev/null +++ b/patches/darling.patch @@ -0,0 +1,7986 @@ +diff --git a/src/CoreAudio/AudioToolbox/AUComponent.cpp b/src/CoreAudio/AudioToolbox/AUComponent.cpp +index d48eec1ef..c86959d55 100644 +--- a/src/CoreAudio/AudioToolbox/AUComponent.cpp ++++ b/src/CoreAudio/AudioToolbox/AUComponent.cpp +@@ -22,13 +22,16 @@ along with Darling. If not, see . + #include + #include + #include "ComponentDispatch.h" ++#include "../AudioTrace.h" + + const CFStringRef kAudioComponentRegistrationsChangedNotification = CFSTR("AudioComponentRegistrationsChangedNotification"); + const CFStringRef kAudioComponentInstanceInvalidationNotification = CFSTR("AudioComponentInstanceInvalidationNotification"); + + OSStatus AudioOutputUnitStart(AudioUnit ci) + { +- return dispatchCall(ci, kAudioOutputUnitStartSelect); ++ OSStatus status = dispatchCall(ci, kAudioOutputUnitStartSelect); ++ rclolAudioTrace("UNIT_START unit=%p status=%d\n", (void*) ci, (int) status); ++ return status; + } + + OSStatus AudioOutputUnitStop(AudioUnit ci) +@@ -38,7 +41,9 @@ OSStatus AudioOutputUnitStop(AudioUnit ci) + + OSStatus AudioUnitInitialize(AudioUnit inUnit) + { +- return dispatchCall(inUnit, kAudioUnitInitializeSelect); ++ OSStatus status = dispatchCall(inUnit, kAudioUnitInitializeSelect); ++ rclolAudioTrace("UNIT_INITIALIZE unit=%p status=%d\n", (void*) inUnit, (int) status); ++ return status; + } + + OSStatus AudioUnitUninitialize(AudioUnit inUnit) +@@ -54,7 +59,9 @@ AudioUnitGetPropertyInfo(AudioUnit inUnit, + UInt32 * _Nullable outDataSize, + Boolean * _Nullable outWritable) + { +- return dispatchCall(inUnit, kAudioUnitGetPropertyInfoSelect, inID, inScope, inElement, outDataSize, outWritable); ++ OSStatus status = dispatchCall(inUnit, kAudioUnitGetPropertyInfoSelect, inID, inScope, inElement, outDataSize, outWritable); ++ rclolAudioTrace("UNIT_PROPINFO unit=%p prop=%u scope=%u elem=%u status=%d\n", (void*) inUnit, (unsigned) inID, (unsigned) inScope, (unsigned) inElement, (int) status); ++ return status; + } + + OSStatus +@@ -65,7 +72,16 @@ AudioUnitGetProperty(AudioUnit inUnit, + void * outData, + UInt32 * ioDataSize) + { +- return dispatchCall(inUnit, kAudioUnitGetPropertySelect, inID, inScope, inElement, outData, ioDataSize); ++ OSStatus status = dispatchCall(inUnit, kAudioUnitGetPropertySelect, inID, inScope, inElement, outData, ioDataSize); ++ if (inID == 8 && status == noErr && outData && ioDataSize && *ioDataSize >= sizeof(AudioStreamBasicDescription)) ++ { ++ const AudioStreamBasicDescription* d = static_cast(outData); ++ rclolAudioTrace("UNIT_GOT_FORMAT unit=%p scope=%u elem=%u rate=%.0f id=%08x flags=%08x bpf=%u ch=%u bits=%u\n", ++ (void*) inUnit, (unsigned) inScope, (unsigned) inElement, d->mSampleRate, (unsigned) d->mFormatID, (unsigned) d->mFormatFlags, ++ (unsigned) d->mBytesPerFrame, (unsigned) d->mChannelsPerFrame, (unsigned) d->mBitsPerChannel); ++ } ++ rclolAudioTrace("UNIT_GET unit=%p prop=%u scope=%u elem=%u status=%d\n", (void*) inUnit, (unsigned) inID, (unsigned) inScope, (unsigned) inElement, (int) status); ++ return status; + } + + OSStatus +@@ -76,7 +92,16 @@ AudioUnitSetProperty(AudioUnit inUnit, + const void * _Nullable inData, + UInt32 inDataSize) + { +- return dispatchCall(inUnit, kAudioUnitSetPropertySelect, inID, inScope, inElement, inData, inDataSize); ++ if (inID == 8 /* kAudioUnitProperty_StreamFormat */ && inData && inDataSize >= sizeof(AudioStreamBasicDescription)) ++ { ++ const AudioStreamBasicDescription* d = static_cast(inData); ++ rclolAudioTrace("UNIT_SET_FORMAT unit=%p scope=%u elem=%u rate=%.0f id=%08x flags=%08x bpp=%u fpp=%u bpf=%u ch=%u bits=%u\n", ++ (void*) inUnit, (unsigned) inScope, (unsigned) inElement, d->mSampleRate, (unsigned) d->mFormatID, (unsigned) d->mFormatFlags, ++ (unsigned) d->mBytesPerPacket, (unsigned) d->mFramesPerPacket, (unsigned) d->mBytesPerFrame, (unsigned) d->mChannelsPerFrame, (unsigned) d->mBitsPerChannel); ++ } ++ OSStatus status = dispatchCall(inUnit, kAudioUnitSetPropertySelect, inID, inScope, inElement, inData, inDataSize); ++ rclolAudioTrace("UNIT_SET unit=%p prop=%u scope=%u elem=%u size=%u status=%d\n", (void*) inUnit, (unsigned) inID, (unsigned) inScope, (unsigned) inElement, (unsigned) inDataSize, (int) status); ++ return status; + } + + OSStatus +@@ -103,7 +128,9 @@ AudioUnitAddRenderNotify(AudioUnit inUnit, + AURenderCallback inProc, + void * _Nullable inProcUserData) + { +- return dispatchCall(inUnit, kAudioUnitAddRenderNotifySelect, inProc, inProcUserData); ++ OSStatus status = dispatchCall(inUnit, kAudioUnitAddRenderNotifySelect, inProc, inProcUserData); ++ rclolAudioTrace("UNIT_ADD_RENDER_NOTIFY unit=%p status=%d\n", (void*) inUnit, (int) status); ++ return status; + } + + OSStatus +diff --git a/src/CoreAudio/AudioToolbox/AUGraph.cpp b/src/CoreAudio/AudioToolbox/AUGraph.cpp +index ca20b90df..e90b37163 100644 +--- a/src/CoreAudio/AudioToolbox/AUGraph.cpp ++++ b/src/CoreAudio/AudioToolbox/AUGraph.cpp +@@ -1 +1,548 @@ ++#include "../AudioTrace.h" ++/* ++This file is part of Darling. ++ ++Darling is free software: you can redistribute it and/or modify ++it under the terms of the GNU General Public License as published by ++the Free Software Foundation, either version 3 of the License, or ++(at your option) any later version. ++ ++Darling is distributed in the hope that it will be useful, ++but WITHOUT ANY WARRANTY; without even the implied warranty of ++MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++GNU General Public License for more details. ++ ++You should have received a copy of the GNU General Public License ++along with Darling. If not, see . ++*/ + #include "AUGraph.h" ++#include ++#include ++#include ++#include ++#include ++ ++// An AUGraph is a set of nodes (audio units created from component descriptions), connections ++// between them and render callbacks feeding their inputs. Connections and callbacks are recorded ++// while the graph is edited and applied to the audio units when the graph is initialized. ++ ++namespace ++{ ++struct Node ++{ ++ AUNode id; ++ AudioComponentDescription description; ++ AudioUnit unit = nullptr; ++}; ++} ++ ++struct AUGraphData ++{ ++ std::recursive_mutex mutex; ++ std::vector nodes; ++ std::vector connections; ++ std::vector callbacks; ++ AUNode nextNode = 1; ++ bool open = false; ++ bool initialized = false; ++ bool running = false; ++}; ++ ++namespace ++{ ++Node* findNode(AUGraphData* graph, AUNode id) ++{ ++ for (Node& node : graph->nodes) ++ { ++ if (node.id == id) ++ return &node; ++ } ++ return nullptr; ++} ++ ++bool isOutputNode(const Node& node) ++{ ++ return node.description.componentType == kAudioUnitType_Output; ++} ++ ++// Undoes whatever initialization has done to the units, keeping the nodes ++void uninitializeUnits(AUGraphData* graph) ++{ ++ for (Node& node : graph->nodes) ++ { ++ if (node.unit) ++ AudioUnitUninitialize(node.unit); ++ } ++ graph->initialized = false; ++} ++} ++ ++static OSStatus AUGraphOpenImpl(AUGraph inGraph); ++static OSStatus AUGraphConnectNodeInputImpl(AUGraph inGraph, AUNode inSourceNode, UInt32 inSourceOutputNumber, AUNode inDestNode, UInt32 inDestInputNumber); ++static OSStatus AUGraphSetNodeInputCallbackImpl(AUGraph inGraph, AUNode inDestNode, UInt32 inDestInputNumber, const AURenderCallbackStruct* inInputCallback); ++static OSStatus AUGraphInitializeImpl(AUGraph inGraph); ++static OSStatus AUGraphStartImpl(AUGraph inGraph); ++OSStatus NewAUGraph(AUGraph* outGraph) ++{ ++ rclolAudioTrace("NEW_GRAPH\n"); ++ if (!outGraph) ++ return kAudio_ParamError; ++ ++ *outGraph = new AUGraphData; ++ return noErr; ++} ++ ++OSStatus DisposeAUGraph(AUGraph inGraph) ++{ ++ if (!inGraph) ++ return kAudio_ParamError; ++ ++ AUGraphStop(inGraph); ++ AUGraphUninitialize(inGraph); ++ AUGraphClose(inGraph); ++ delete inGraph; ++ return noErr; ++} ++ ++OSStatus AUGraphAddNode(AUGraph inGraph, const AudioComponentDescription* inDescription, AUNode* outNode) ++{ ++ if(inDescription) rclolAudioTrace("ADD_NODE type=%08x subtype=%08x\n",inDescription->componentType,inDescription->componentSubType); ++ if (!inGraph || !inDescription || !outNode) ++ return kAudio_ParamError; ++ ++ std::lock_guard lock(inGraph->mutex); ++ ++ Node node; ++ node.id = inGraph->nextNode; ++ node.description = *inDescription; ++ ++ if (inGraph->open) ++ { ++ // A node added to an open graph gets its unit right away ++ AudioComponent component = AudioComponentFindNext(nullptr, inDescription); ++ OSStatus status = component ? AudioComponentInstanceNew(component, &node.unit) ++ : kAudioUnitErr_InvalidProperty; ++ rclolAudioTrace("GRAPH_NODE_UNIT subtype=%08x unit=%p status=%d\n", (unsigned) node.description.componentSubType, (void*) node.unit, (int) status); ++ if (status != noErr) ++ return status; ++ } ++ ++ inGraph->nextNode++; ++ inGraph->nodes.push_back(node); ++ *outNode = node.id; ++ return noErr; ++} ++ ++OSStatus AUGraphRemoveNode(AUGraph inGraph, AUNode inNode) ++{ ++ if (!inGraph) ++ return kAudio_ParamError; ++ ++ std::lock_guard lock(inGraph->mutex); ++ ++ auto node = std::find_if(inGraph->nodes.begin(), inGraph->nodes.end(), [&](const Node& n) { return n.id == inNode; }); ++ if (node == inGraph->nodes.end()) ++ return kAudioUnitErr_InvalidProperty; ++ ++ if (node->unit) ++ { ++ AudioUnitUninitialize(node->unit); ++ AudioComponentInstanceDispose(node->unit); ++ } ++ inGraph->nodes.erase(node); ++ ++ inGraph->connections.erase(std::remove_if(inGraph->connections.begin(), inGraph->connections.end(), ++ [&](const AUNodeConnection& c) { return c.sourceNode == inNode || c.destNode == inNode; }), inGraph->connections.end()); ++ inGraph->callbacks.erase(std::remove_if(inGraph->callbacks.begin(), inGraph->callbacks.end(), ++ [&](const AUNodeRenderCallback& c) { return c.destNode == inNode; }), inGraph->callbacks.end()); ++ return noErr; ++} ++ ++OSStatus AUGraphGetNodeCount(AUGraph inGraph, UInt32* outNumberOfNodes) ++{ ++ if (!inGraph || !outNumberOfNodes) ++ return kAudio_ParamError; ++ ++ std::lock_guard lock(inGraph->mutex); ++ *outNumberOfNodes = (UInt32) inGraph->nodes.size(); ++ return noErr; ++} ++ ++OSStatus AUGraphGetIndNode(AUGraph inGraph, UInt32 inIndex, AUNode* outNode) ++{ ++ if (!inGraph || !outNode) ++ return kAudio_ParamError; ++ ++ std::lock_guard lock(inGraph->mutex); ++ if (inIndex >= inGraph->nodes.size()) ++ return kAudio_ParamError; ++ ++ *outNode = inGraph->nodes[inIndex].id; ++ return noErr; ++} ++ ++OSStatus AUGraphNodeInfo(AUGraph inGraph, AUNode inNode, AudioComponentDescription* outDescription, AudioUnit* outAudioUnit) ++{ ++ if (!inGraph) ++ return kAudio_ParamError; ++ ++ std::lock_guard lock(inGraph->mutex); ++ ++ Node* node = findNode(inGraph, inNode); ++ if (!node) ++ return kAudioUnitErr_InvalidProperty; ++ ++ if (outDescription) ++ *outDescription = node->description; ++ if (outAudioUnit) ++ { ++ // The unit only exists while the graph is open ++ if (!node->unit) ++ return kAudioUnitErr_Uninitialized; ++ *outAudioUnit = node->unit; ++ } ++ return noErr; ++} ++ ++static OSStatus AUGraphOpenImpl(AUGraph inGraph) ++{ ++ if (!inGraph) ++ return kAudio_ParamError; ++ ++ std::lock_guard lock(inGraph->mutex); ++ if (inGraph->open) ++ return noErr; ++ ++ for (Node& node : inGraph->nodes) ++ { ++ AudioComponent component = AudioComponentFindNext(nullptr, &node.description); ++ OSStatus status = component ? AudioComponentInstanceNew(component, &node.unit) ++ : kAudioUnitErr_InvalidProperty; ++ rclolAudioTrace("GRAPH_NODE_UNIT subtype=%08x unit=%p status=%d\n", (unsigned) node.description.componentSubType, (void*) node.unit, (int) status); ++ if (status != noErr) ++ { ++ for (Node& created : inGraph->nodes) ++ { ++ if (created.unit) ++ { ++ AudioComponentInstanceDispose(created.unit); ++ created.unit = nullptr; ++ } ++ } ++ return status; ++ } ++ } ++ ++ inGraph->open = true; ++ return noErr; ++} ++ ++OSStatus AUGraphClose(AUGraph inGraph) ++{ ++ if (!inGraph) ++ return kAudio_ParamError; ++ ++ std::lock_guard lock(inGraph->mutex); ++ if (inGraph->running) ++ AUGraphStop(inGraph); ++ if (inGraph->initialized) ++ uninitializeUnits(inGraph); ++ ++ for (Node& node : inGraph->nodes) ++ { ++ if (node.unit) ++ { ++ AudioComponentInstanceDispose(node.unit); ++ node.unit = nullptr; ++ } ++ } ++ inGraph->open = false; ++ return noErr; ++} ++ ++static OSStatus AUGraphConnectNodeInputImpl(AUGraph inGraph, AUNode inSourceNode, UInt32 inSourceOutputNumber, AUNode inDestNode, UInt32 inDestInputNumber) ++{ ++ if (!inGraph) ++ return kAudio_ParamError; ++ ++ std::lock_guard lock(inGraph->mutex); ++ if (!findNode(inGraph, inSourceNode) || !findNode(inGraph, inDestNode)) ++ return kAudioUnitErr_InvalidProperty; ++ ++ // An input is fed by one source: a new connection or callback replaces the previous one ++ AUGraphDisconnectNodeInput(inGraph, inDestNode, inDestInputNumber); ++ ++ AUNodeConnection connection; ++ connection.sourceNode = inSourceNode; ++ connection.sourceOutputNumber = inSourceOutputNumber; ++ connection.destNode = inDestNode; ++ connection.destInputNumber = inDestInputNumber; ++ inGraph->connections.push_back(connection); ++ return noErr; ++} ++ ++OSStatus AUGraphDisconnectNodeInput(AUGraph inGraph, AUNode inDestNode, UInt32 inDestInputNumber) ++{ ++ if (!inGraph) ++ return kAudio_ParamError; ++ ++ std::lock_guard lock(inGraph->mutex); ++ inGraph->connections.erase(std::remove_if(inGraph->connections.begin(), inGraph->connections.end(), ++ [&](const AUNodeConnection& c) { return c.destNode == inDestNode && c.destInputNumber == inDestInputNumber; }), inGraph->connections.end()); ++ inGraph->callbacks.erase(std::remove_if(inGraph->callbacks.begin(), inGraph->callbacks.end(), ++ [&](const AUNodeRenderCallback& c) { return c.destNode == inDestNode && c.destInputNumber == inDestInputNumber; }), inGraph->callbacks.end()); ++ return noErr; ++} ++ ++OSStatus AUGraphClearConnections(AUGraph inGraph) ++{ ++ if (!inGraph) ++ return kAudio_ParamError; ++ ++ std::lock_guard lock(inGraph->mutex); ++ inGraph->connections.clear(); ++ inGraph->callbacks.clear(); ++ return noErr; ++} ++ ++static OSStatus AUGraphSetNodeInputCallbackImpl(AUGraph inGraph, AUNode inDestNode, UInt32 inDestInputNumber, const AURenderCallbackStruct* inInputCallback) ++{ ++ if (!inGraph || !inInputCallback) ++ return kAudio_ParamError; ++ ++ std::lock_guard lock(inGraph->mutex); ++ if (!findNode(inGraph, inDestNode)) ++ return kAudioUnitErr_InvalidProperty; ++ ++ AUGraphDisconnectNodeInput(inGraph, inDestNode, inDestInputNumber); ++ ++ AUNodeRenderCallback callback; ++ callback.destNode = inDestNode; ++ callback.destInputNumber = inDestInputNumber; ++ callback.cback = *inInputCallback; ++ inGraph->callbacks.push_back(callback); ++ return noErr; ++} ++ ++OSStatus AUGraphGetNumberOfInteractions(AUGraph inGraph, UInt32* outNumInteractions) ++{ ++ if (!inGraph || !outNumInteractions) ++ return kAudio_ParamError; ++ ++ std::lock_guard lock(inGraph->mutex); ++ *outNumInteractions = (UInt32) (inGraph->connections.size() + inGraph->callbacks.size()); ++ return noErr; ++} ++ ++OSStatus AUGraphGetInteractionInfo(AUGraph inGraph, UInt32 inInteractionIndex, AUNodeInteraction* outInteraction) ++{ ++ if (!inGraph || !outInteraction) ++ return kAudio_ParamError; ++ ++ std::lock_guard lock(inGraph->mutex); ++ ++ if (inInteractionIndex < inGraph->connections.size()) ++ { ++ outInteraction->nodeInteractionType = 1; // kAUNodeInteraction_Connection ++ outInteraction->nodeInteraction.connection = inGraph->connections[inInteractionIndex]; ++ return noErr; ++ } ++ ++ inInteractionIndex -= (UInt32) inGraph->connections.size(); ++ if (inInteractionIndex < inGraph->callbacks.size()) ++ { ++ outInteraction->nodeInteractionType = 2; // kAUNodeInteraction_InputCallback ++ outInteraction->nodeInteraction.inputCallback = inGraph->callbacks[inInteractionIndex]; ++ return noErr; ++ } ++ return kAudio_ParamError; ++} ++ ++static OSStatus AUGraphInitializeImpl(AUGraph inGraph) ++{ ++ if (!inGraph) ++ return kAudio_ParamError; ++ ++ std::lock_guard lock(inGraph->mutex); ++ if (!inGraph->open) ++ return kAudioUnitErr_Uninitialized; ++ if (inGraph->initialized) ++ return noErr; ++ ++ for (const AUNodeConnection& connection : inGraph->connections) ++ { ++ Node* source = findNode(inGraph, connection.sourceNode); ++ Node* dest = findNode(inGraph, connection.destNode); ++ if (!source || !dest) ++ return kAudioUnitErr_InvalidProperty; ++ ++ AudioUnitConnection unitConnection; ++ unitConnection.sourceAudioUnit = source->unit; ++ unitConnection.sourceOutputNumber = connection.sourceOutputNumber; ++ unitConnection.destInputNumber = connection.destInputNumber; ++ ++ OSStatus status = AudioUnitSetProperty(dest->unit, kAudioUnitProperty_MakeConnection, kAudioUnitScope_Input, ++ connection.destInputNumber, &unitConnection, sizeof(unitConnection)); ++ if (status != noErr) ++ return status; ++ } ++ ++ for (const AUNodeRenderCallback& callback : inGraph->callbacks) ++ { ++ Node* dest = findNode(inGraph, callback.destNode); ++ if (!dest) ++ return kAudioUnitErr_InvalidProperty; ++ ++ OSStatus status = AudioUnitSetProperty(dest->unit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Input, ++ callback.destInputNumber, &callback.cback, sizeof(callback.cback)); ++ if (status != noErr) ++ return status; ++ } ++ ++ for (Node& node : inGraph->nodes) ++ { ++ OSStatus status = AudioUnitInitialize(node.unit); ++ if (status != noErr) ++ { ++ uninitializeUnits(inGraph); ++ return status; ++ } ++ } ++ ++ inGraph->initialized = true; ++ return noErr; ++} ++ ++OSStatus AUGraphUninitialize(AUGraph inGraph) ++{ ++ if (!inGraph) ++ return kAudio_ParamError; ++ ++ std::lock_guard lock(inGraph->mutex); ++ if (inGraph->running) ++ AUGraphStop(inGraph); ++ if (inGraph->initialized) ++ uninitializeUnits(inGraph); ++ return noErr; ++} ++ ++static OSStatus AUGraphStartImpl(AUGraph inGraph) ++{ ++ if (!inGraph) ++ return kAudio_ParamError; ++ ++ std::lock_guard lock(inGraph->mutex); ++ if (!inGraph->initialized) ++ return kAudioUnitErr_Uninitialized; ++ if (inGraph->running) ++ return noErr; ++ ++ // Pulling starts at the output units ++ for (Node& node : inGraph->nodes) ++ { ++ if (isOutputNode(node)) ++ { ++ OSStatus status = AudioOutputUnitStart(node.unit); ++ if (status != noErr) ++ return status; ++ } ++ } ++ ++ inGraph->running = true; ++ return noErr; ++} ++ ++OSStatus AUGraphStop(AUGraph inGraph) ++{ ++ if (!inGraph) ++ return kAudio_ParamError; ++ ++ std::lock_guard lock(inGraph->mutex); ++ if (!inGraph->running) ++ return noErr; ++ ++ for (Node& node : inGraph->nodes) ++ { ++ if (node.unit && isOutputNode(node)) ++ AudioOutputUnitStop(node.unit); ++ } ++ ++ inGraph->running = false; ++ return noErr; ++} ++ ++OSStatus AUGraphIsOpen(AUGraph inGraph, Boolean* outIsOpen) ++{ ++ if (!inGraph || !outIsOpen) ++ return kAudio_ParamError; ++ ++ std::lock_guard lock(inGraph->mutex); ++ *outIsOpen = inGraph->open; ++ return noErr; ++} ++ ++OSStatus AUGraphIsInitialized(AUGraph inGraph, Boolean* outIsInitialized) ++{ ++ if (!inGraph || !outIsInitialized) ++ return kAudio_ParamError; ++ ++ std::lock_guard lock(inGraph->mutex); ++ *outIsInitialized = inGraph->initialized; ++ return noErr; ++} ++ ++OSStatus AUGraphIsRunning(AUGraph inGraph, Boolean* outIsRunning) ++{ ++ if (!inGraph || !outIsRunning) ++ return kAudio_ParamError; ++ ++ std::lock_guard lock(inGraph->mutex); ++ *outIsRunning = inGraph->running; ++ return noErr; ++} ++ ++OSStatus AUGraphUpdate(AUGraph inGraph, Boolean* outIsUpdated) ++{ ++ if (!inGraph) ++ return kAudio_ParamError; ++ ++ // Edits are applied when the graph is initialized, so there is never a pending change to apply ++ if (outIsUpdated) ++ *outIsUpdated = true; ++ return noErr; ++} ++ ++OSStatus AUGraphOpen(AUGraph inGraph) ++{ ++ OSStatus status = AUGraphOpenImpl(inGraph); ++ rclolAudioTrace("GRAPH_OPEN status=%d\n", (int) status); ++ return status; ++} ++ ++OSStatus AUGraphConnectNodeInput(AUGraph inGraph, AUNode inSourceNode, UInt32 inSourceOutputNumber, AUNode inDestNode, UInt32 inDestInputNumber) ++{ ++ OSStatus status = AUGraphConnectNodeInputImpl(inGraph, inSourceNode, inSourceOutputNumber, inDestNode, inDestInputNumber); ++ rclolAudioTrace("GRAPH_CONNECTNODEINPUT status=%d\n", (int) status); ++ return status; ++} ++ ++OSStatus AUGraphSetNodeInputCallback(AUGraph inGraph, AUNode inDestNode, UInt32 inDestInputNumber, const AURenderCallbackStruct* inInputCallback) ++{ ++ OSStatus status = AUGraphSetNodeInputCallbackImpl(inGraph, inDestNode, inDestInputNumber, inInputCallback); ++ rclolAudioTrace("GRAPH_SETNODEINPUTCALLBACK status=%d\n", (int) status); ++ return status; ++} ++ ++OSStatus AUGraphInitialize(AUGraph inGraph) ++{ ++ OSStatus status = AUGraphInitializeImpl(inGraph); ++ rclolAudioTrace("GRAPH_INITIALIZE status=%d\n", (int) status); ++ return status; ++} ++ ++OSStatus AUGraphStart(AUGraph inGraph) ++{ ++ OSStatus status = AUGraphStartImpl(inGraph); ++ rclolAudioTrace("GRAPH_START status=%d\n", (int) status); ++ return status; ++} +diff --git a/src/CoreAudio/AudioToolbox/AudioComponentManager.mm b/src/CoreAudio/AudioToolbox/AudioComponentManager.mm +index 5fa33817d..4c1f25a33 100644 +--- a/src/CoreAudio/AudioToolbox/AudioComponentManager.mm ++++ b/src/CoreAudio/AudioToolbox/AudioComponentManager.mm +@@ -1,3 +1,4 @@ ++#include "../AudioTrace.h" + /* + This file is part of Darling. + +@@ -18,6 +19,8 @@ along with Darling. If not, see . + */ + #include "AudioComponentManager.h" + #include ++#include ++#include + #include + #include + +@@ -210,6 +213,11 @@ std::vector AudioComponentManager::findMatching(const AudioCompo + } + } + ++ // Prefer a newer user component over an older system implementation of ++ // the same audio unit. Retain the existing order for equal versions. ++ std::stable_sort(rv.begin(),rv.end(),[this](AudioComponent a,AudioComponent b) { ++ return m_components.at(a).version>m_components.at(b).version; ++ }); + return rv; + } + +@@ -274,6 +282,11 @@ OSStatus AudioComponentManager::instantiate(AudioComponent c, AudioComponentInst + return invalidComponentID; + + RegisteredComponent& regd = it->second; ++ static unsigned traceCount=0; ++ if(getenv("RCLOL_AUDIO_TRACE") && traceCount++<16) ++ rclolAudioTrace("AUDIO_COMPONENT version=%u subtype=%08x user_override=%d\n", ++ regd.version,regd.desc.componentSubType, ++ regd.bundlePath.find("DarlingRiotAudio.component")!=std::string::npos); + if (!regd.factory) + { + // Load the bundle +diff --git a/src/CoreAudio/AudioToolbox/AudioConverter.cpp b/src/CoreAudio/AudioToolbox/AudioConverter.cpp +index 202121cd9..c40ba22c2 100644 +--- a/src/CoreAudio/AudioToolbox/AudioConverter.cpp ++++ b/src/CoreAudio/AudioToolbox/AudioConverter.cpp +@@ -22,13 +22,35 @@ along with Darling. If not, see . + #include + #include + #include ++#include ++#include ++#include ++#include ++ ++// Validate opaque-handle ownership before dereferencing a disposal argument. ++static std::mutex converterMutex; ++static std::unordered_set liveConverters; ++ ++static bool audioTrace() { ++ static const bool enabled = std::getenv("RCLOL_AUDIO_TRACE") != nullptr; ++ return enabled; ++} ++ + + OSStatus AudioConverterNew(const AudioStreamBasicDescription* inSourceFormat, const AudioStreamBasicDescription* inDestinationFormat, AudioConverterRef* outAudioConverter) + { +- return AudioConverter::create(inSourceFormat, inDestinationFormat, (AudioConverter**)outAudioConverter); ++ OSStatus status = AudioConverter::create(inSourceFormat, inDestinationFormat, (AudioConverter**)outAudioConverter); ++ if (status == noErr && outAudioConverter && *outAudioConverter) { ++ std::lock_guard lock(converterMutex); ++ liveConverters.insert(*outAudioConverter); ++ } ++ if (audioTrace()) ++ std::fprintf(stderr, "RIOT_AUDIO new status=%d converter=%p\n", status, ++ outAudioConverter ? *outAudioConverter : nullptr); ++ return status; + } + +-OSStatus AudioConverterNewSpecific(const AudioStreamBasicDescription* inSourceFormat, const AudioStreamBasicDescription* inDestinationFormat, UInt32 inNumberClassDescriptions, AudioClassDescription* nClassDescriptions, AudioConverterRef* outAudioConverter) ++OSStatus AudioConverterNewSpecific(const AudioStreamBasicDescription* inSourceFormat, const AudioStreamBasicDescription* inDestinationFormat, UInt32 inNumberClassDescriptions, const AudioClassDescription* nClassDescriptions, AudioConverterRef* outAudioConverter) + { + // FIXME + return AudioConverterNew(inSourceFormat, inDestinationFormat, outAudioConverter); +@@ -47,6 +69,15 @@ OSStatus AudioConverterReset(AudioConverterRef inAudioConverter) + + OSStatus AudioConverterDispose(AudioConverterRef inAudioConverter) + { ++ if (audioTrace()) ++ std::fprintf(stderr, "RIOT_AUDIO dispose converter=%p\n", inAudioConverter); ++ if (!inAudioConverter) ++ return paramErr; ++ { ++ std::lock_guard lock(converterMutex); ++ if (!liveConverters.erase(inAudioConverter)) ++ return paramErr; ++ } + delete (AudioConverter*)inAudioConverter; + return noErr; + } +diff --git a/src/CoreAudio/AudioToolbox/AudioConverterImpl.cpp b/src/CoreAudio/AudioToolbox/AudioConverterImpl.cpp +index dca819b06..fc4016e60 100644 +--- a/src/CoreAudio/AudioToolbox/AudioConverterImpl.cpp ++++ b/src/CoreAudio/AudioToolbox/AudioConverterImpl.cpp +@@ -67,6 +67,11 @@ void AudioConverter::flush() + OSStatus AudioConverter::create(const AudioStreamBasicDescription* inSourceFormat, const AudioStreamBasicDescription* inDestinationFormat, AudioConverter** out) + { + TRACE2(inSourceFormat, inDestinationFormat); ++ if (!out) ++ return paramErr; ++ *out = nullptr; ++ if (!inSourceFormat || !inDestinationFormat) ++ return paramErr; + + // TODO: non-interleaved audio + +@@ -118,6 +123,7 @@ OSStatus AudioConverter::create(const AudioStreamBasicDescription* inSourceForma + if (avcodec_open2((*out)->m_decoder, codecIn, nullptr) < 0) + { + delete *out; ++ *out = nullptr; + std::cerr << "AudioConverter::create(): avcodec_open() failed, format in = " << std::hex << inSourceFormat->mFormatID << ", out = " << inDestinationFormat->mFormatID << std::dec << std::endl; + + return paramErr; +diff --git a/src/CoreAudio/AudioToolbox/ComponentDispatch.h b/src/CoreAudio/AudioToolbox/ComponentDispatch.h +index ee6a9b171..8505c3553 100644 +--- a/src/CoreAudio/AudioToolbox/ComponentDispatch.h ++++ b/src/CoreAudio/AudioToolbox/ComponentDispatch.h +@@ -43,7 +43,12 @@ OSStatus dispatchCall(AudioComponentInstance inUnit, SInt16 sel, Args... args) + AudioComponentPlugInInterface* iface = AudioComponentManager::instance()->instanceInterface(inUnit); + AudioComponentMethod method = iface->Lookup(sel); + if (method != nullptr) +- return method(iface, args...); ++ { ++ // The lookup type is variadic. Calling it directly promotes Float32 ++ // parameters to double, while audio-unit entry points expect float. ++ using Method = OSStatus (*)(void*, Args...); ++ return reinterpret_cast(method)(iface, args...); ++ } + else + return badComponentSelector; + } +diff --git a/src/CoreAudio/CoreAudio/AudioConverterShim.c b/src/CoreAudio/CoreAudio/AudioConverterShim.c +new file mode 100644 +index 000000000..6e85e8230 +--- /dev/null ++++ b/src/CoreAudio/CoreAudio/AudioConverterShim.c +@@ -0,0 +1,90 @@ ++/* Minimal AudioConverter for Darling's CoreAudio framework. ++ * ++ * Apple's CoreAudio re-exports these AudioToolbox entry points, and ++ * third-party code (e.g. Discord voice) binds them against CoreAudio. ++ * AudioToolbox cannot be re-exported here (it depends on CoreAudio), ++ * so this TU provides small self-contained implementations: ++ * - format-identical conversion works (direct frame copy); ++ * - anything else returns kAudioConverterErr_InvalidInputSize/ ++ * codec-unavailable rather than crashing the caller at bind time. ++ */ ++#include ++#include ++#include ++#include ++#include ++ ++struct DarlingAudioConverter { ++ AudioStreamBasicDescription src; ++ AudioStreamBasicDescription dst; ++}; ++ ++OSStatus AudioConverterNew(const AudioStreamBasicDescription* inSourceFormat, ++ const AudioStreamBasicDescription* inDestinationFormat, ++ AudioConverterRef* outAudioConverter) { ++ struct DarlingAudioConverter* c; ++ if (!inSourceFormat || !inDestinationFormat || !outAudioConverter) ++ return paramErr; ++ c = (struct DarlingAudioConverter*)malloc(sizeof(*c)); ++ if (!c) ++ return memFullErr; ++ c->src = *inSourceFormat; ++ c->dst = *inDestinationFormat; ++ *outAudioConverter = (AudioConverterRef)c; ++ return noErr; ++} ++ ++OSStatus AudioConverterDispose(AudioConverterRef inAudioConverter) { ++ if (!inAudioConverter) ++ return paramErr; ++ free(inAudioConverter); ++ return noErr; ++} ++ ++static int asbd_equal(const AudioStreamBasicDescription* a, ++ const AudioStreamBasicDescription* b) { ++ return a->mSampleRate == b->mSampleRate && ++ a->mFormatID == b->mFormatID && ++ a->mFormatFlags == b->mFormatFlags && ++ a->mBytesPerPacket == b->mBytesPerPacket && ++ a->mFramesPerPacket == b->mFramesPerPacket && ++ a->mBytesPerFrame == b->mBytesPerFrame && ++ a->mChannelsPerFrame == b->mChannelsPerFrame && ++ a->mBitsPerChannel == b->mBitsPerChannel; ++} ++ ++OSStatus AudioConverterFillComplexBuffer(AudioConverterRef inAudioConverter, ++ AudioConverterComplexInputDataProc inInputDataProc, ++ void* inInputDataProcUserData, ++ UInt32* ioOutputDataPacketSize, ++ AudioBufferList* outOutputData, ++ AudioStreamPacketDescription* outPacketDescription) { ++ struct DarlingAudioConverter* c = (struct DarlingAudioConverter*)inAudioConverter; ++ AudioBufferList inBuf; ++ UInt32 packets, bytes; ++ OSStatus st; ++ ++ (void)outPacketDescription; ++ if (!c || !inInputDataProc || !ioOutputDataPacketSize || !outOutputData) ++ return paramErr; ++ if (!asbd_equal(&c->src, &c->dst)) ++ return kAudioConverterErr_UnspecifiedError; ++ if (c->src.mBytesPerPacket == 0 || outOutputData->mNumberBuffers < 1) ++ return paramErr; ++ ++ packets = *ioOutputDataPacketSize; ++ bytes = packets * c->src.mBytesPerPacket; ++ if (bytes > outOutputData->mBuffers[0].mDataByteSize) ++ bytes = outOutputData->mBuffers[0].mDataByteSize; ++ ++ inBuf.mNumberBuffers = 1; ++ inBuf.mBuffers[0].mNumberChannels = c->src.mChannelsPerFrame; ++ inBuf.mBuffers[0].mDataByteSize = bytes; ++ inBuf.mBuffers[0].mData = outOutputData->mBuffers[0].mData; ++ st = inInputDataProc((AudioConverterRef)c, &packets, &inBuf, NULL, inInputDataProcUserData); ++ if (st != noErr) ++ return st; ++ *ioOutputDataPacketSize = packets; ++ outOutputData->mBuffers[0].mDataByteSize = packets * c->dst.mBytesPerPacket; ++ return noErr; ++} +diff --git a/src/CoreAudio/CoreAudio/AudioHardware.cpp b/src/CoreAudio/CoreAudio/AudioHardware.cpp +index 7243e6a62..fb48486c6 100644 +--- a/src/CoreAudio/CoreAudio/AudioHardware.cpp ++++ b/src/CoreAudio/CoreAudio/AudioHardware.cpp +@@ -1,3 +1,4 @@ ++#include "../AudioTrace.h" + /* + This file is part of Darling. + +@@ -25,6 +26,9 @@ along with Darling. If not, see . + #include + #include + #include ++#include ++#include ++#include + #include + #include "stub.h" + +@@ -38,7 +42,7 @@ static void initObjects() + g_objects.insert(std::make_pair(kAudioObjectSystemObject, std::make_unique(kAudioObjectSystemObject))); + g_objects.insert(std::make_pair(kAudioObjectSystemObject + 1, std::make_unique(kAudioObjectSystemObject + 1))); + g_objects.insert(std::make_pair(kAudioObjectSystemObject + 2, std::make_unique(kAudioObjectSystemObject + 2))); +- g_objects.insert(std::make_pair(kAudioObjectSystemObject + 1, std::make_unique(kAudioObjectSystemObject + 3, "event"))); ++ g_objects.insert(std::make_pair(kAudioObjectSystemObject + 3, std::make_unique(kAudioObjectSystemObject + 3, "event"))); + }); + } + +@@ -58,6 +62,35 @@ static AudioHardwareImpl* GetSystemObject() + return GetObject(kAudioObjectSystemObject); + } + ++// Stream IDs are distinct from their owning device IDs. Chromium enumerates ++// these objects to determine input/output direction before opening audio. ++static OSStatus GetStreamPropertyData(AudioObjectID id, ++ const AudioObjectPropertyAddress* property, UInt32* size, void* data) ++{ ++ if (!property || !size) return paramErr; ++ AudioHardwareImpl* device=GetObject(id&0xffffu); ++ if (!device) return kAudioHardwareBadObjectError; ++ UInt32 value; ++ const void* source=&value; ++ UInt32 required=sizeof(value); ++ switch(property->mSelector) { ++ case kAudioStreamPropertyDirection: ++ value=device->id()==kAudioObjectSystemObject+2?1:0;break; ++ case kAudioStreamPropertyTerminalType: ++ value=0;break; ++ case kAudioStreamPropertyVirtualFormat: ++ case kAudioStreamPropertyPhysicalFormat: ++ source=&device->asbd();required=sizeof(AudioStreamBasicDescription);break; ++ default:return kAudioHardwareUnknownPropertyError; ++ } ++ if(data) { ++ if(*sizegetPropertyDataSize(inAddress, inQualifierDataSize, +- inQualifierData, outDataSize); ++ OSStatus status=obj->getPropertyDataSize(inAddress,inQualifierDataSize,inQualifierData,outDataSize); ++ rclolAudioTrace("SIZE object=%u selector=%08x scope=%08x status=%d size=%u\n",inObjectID,inAddress->mSelector,inAddress->mScope,status,status?0:*outDataSize); ++ return status; + } + + OSStatus AudioObjectGetPropertyData(AudioObjectID inObjectID, + const AudioObjectPropertyAddress* inAddress, UInt32 inQualifierDataSize, + const void* inQualifierData, UInt32* ioDataSize, void* outData) + { ++ if(inObjectID==kAudioObjectSystemObject && inAddress->mSelector==kAudioHardwarePropertyDeviceForUID) { ++ if(!outData || !ioDataSize || *ioDataSize(outData); ++ if(!value->mInputData || value->mInputDataSize!=sizeof(CFStringRef) || !value->mOutputData || value->mOutputDataSize(value->mInputData); ++ AudioDeviceID found=kAudioObjectUnknown; ++ AudioObjectPropertyAddress property={kAudioDevicePropertyDeviceUID,kAudioObjectPropertyScopeGlobal,kAudioObjectPropertyElementMaster}; ++ for(AudioDeviceID id=kAudioObjectSystemObject+1;id<=kAudioObjectSystemObject+3;++id) { ++ CFStringRef uid=nullptr;UInt32 size=sizeof(uid); ++ if(GetObject(id)->getPropertyData(&property,0,nullptr,&size,&uid)==noErr && uid) { ++ bool match=CFEqual(uid,wanted);CFRelease(uid); ++ if(match) { found=id;break; } ++ } ++ } ++ *static_cast(value->mOutputData)=found; ++ value->mOutputDataSize=sizeof(found);*ioDataSize=sizeof(*value);return noErr; ++ } ++ if((inObjectID&0xffff0000u)==0x44410000u) ++ return GetStreamPropertyData(inObjectID,inAddress,ioDataSize,outData); + AudioHardwareImpl* obj = GetObject(inObjectID); + if (!obj) + return kAudioHardwareBadObjectError; + +- return obj->getPropertyData(inAddress, inQualifierDataSize, inQualifierData, +- ioDataSize, outData); ++ static unsigned traceCount=0; ++ if(getenv("RCLOL_AUDIO_TRACE") && traceCount++<100) ++ fprintf(stderr,"AUDIO_HARDWARE_GET object=%u property=%08x scope=%08x\n",inObjectID,inAddress->mSelector,inAddress->mScope); ++ OSStatus status=obj->getPropertyData(inAddress,inQualifierDataSize,inQualifierData,ioDataSize,outData); ++ rclolAudioTrace("GET object=%u selector=%08x scope=%08x status=%d size=%u\n",inObjectID,inAddress->mSelector,inAddress->mScope,status,status?0:*ioDataSize); ++ return status; + } + + OSStatus AudioObjectSetPropertyData(AudioObjectID inObjectID, +@@ -117,8 +179,9 @@ OSStatus AudioObjectSetPropertyData(AudioObjectID inObjectID, + if (!obj) + return kAudioHardwareBadObjectError; + +- return obj->setPropertyData(inAddress, inQualifierDataSize, inQualifierData, +- inDataSize, inData); ++ OSStatus status=obj->setPropertyData(inAddress,inQualifierDataSize,inQualifierData,inDataSize,inData); ++ rclolAudioTrace("SET object=%u selector=%08x scope=%08x status=%d\n",inObjectID,inAddress->mSelector,inAddress->mScope,status); ++ return status; + } + + OSStatus AudioObjectAddPropertyListener(AudioObjectID inObjectID, +@@ -129,7 +192,9 @@ OSStatus AudioObjectAddPropertyListener(AudioObjectID inObjectID, + if (!obj) + return kAudioHardwareBadObjectError; + +- return obj->addPropertyListener(inAddress, inListener, inClientData); ++ OSStatus status=obj->addPropertyListener(inAddress,inListener,inClientData); ++ rclolAudioTrace("ADD_LISTENER object=%u selector=%08x status=%d\n",inObjectID,inAddress->mSelector,status); ++ return status; + } + + OSStatus AudioObjectRemovePropertyListener(AudioObjectID inObjectID, +diff --git a/src/CoreAudio/CoreAudio/AudioHardwareImpl.cpp b/src/CoreAudio/CoreAudio/AudioHardwareImpl.cpp +index f006ba51e..e0e4b4984 100644 +--- a/src/CoreAudio/CoreAudio/AudioHardwareImpl.cpp ++++ b/src/CoreAudio/CoreAudio/AudioHardwareImpl.cpp +@@ -21,6 +21,8 @@ along with Darling. If not, see . + #include + #include + #include ++#include ++#include + #include + + AudioHardwareImpl::AudioHardwareImpl(AudioObjectID myId) +@@ -33,7 +35,7 @@ AudioHardwareImpl::AudioHardwareImpl(AudioObjectID myId) + m_asbd.mFormatID = kAudioFormatLinearPCM; + m_asbd.mFormatFlags = kLinearPCMFormatFlagIsFloat | kAudioFormatFlagsNativeEndian; + m_asbd.mFramesPerPacket = 1; +- m_asbd.mBytesPerFrame = UInt32(m_asbd.mBytesPerPacket = m_asbd.mChannelsPerFrame * m_asbd.mSampleRate * m_asbd.mBitsPerChannel / 8); ++ m_asbd.mBytesPerFrame = UInt32(m_asbd.mBytesPerPacket = m_asbd.mChannelsPerFrame * m_asbd.mBitsPerChannel / 8); + } + + +@@ -132,6 +134,9 @@ OSStatus AudioHardwareImpl::isPropertySettable(const AudioObjectPropertyAddress* + { + switch (inAddress->mSelector) + { ++ case kAudioHardwarePropertyRunLoop: ++ if(m_myId!=kAudioObjectSystemObject) break; ++ *outIsSettable=true;return noErr; + case kAudioDevicePropertyVolumeScalar: + case kAudioDevicePropertyVolumeDecibels: + *outIsSettable = true; +@@ -151,6 +156,12 @@ OSStatus AudioHardwareImpl::getPropertyData(const AudioObjectPropertyAddress* in + { + switch (inAddress->mSelector) + { ++ case kAudioHardwarePropertyRunLoop: { ++ if(m_myId!=kAudioObjectSystemObject) return kAudioHardwareUnknownPropertyError; ++ if(outData && *ioDataSize(outData); asbd && *ioDataSize >= sizeof(AudioStreamBasicDescription)) +@@ -160,6 +171,23 @@ OSStatus AudioHardwareImpl::getPropertyData(const AudioObjectPropertyAddress* in + *ioDataSize = sizeof(AudioStreamBasicDescription); + return kAudioHardwareNoError; + } ++ case kAudioDevicePropertyBufferFrameSize: { ++ if(!m_asbd.mBytesPerFrame) return kAudioHardwareBadStreamError; ++ if(outData && *ioDataSizemMinimum=128;range->mMaximum=2048; } ++ *ioDataSize=sizeof(AudioValueRange);return noErr; ++ } ++ case kAudioDevicePropertyLatency: ++ case kAudioDevicePropertySafetyOffset: { ++ if(outData && *ioDataSize(outData); ++ layout->mChannelLayoutTag=m_asbd.mChannelsPerFrame==2 ++ ? kAudioChannelLayoutTag_Stereo : kAudioChannelLayoutTag_Mono; ++ } ++ *ioDataSize=size;return noErr; ++ } + case kAudioDevicePropertyPreferredChannelsForStereo: + { + if (UInt32* b = static_cast(outData); outData && *ioDataSize >= sizeof(UInt32)*2) + { +- b[0] = 0; +- b[1] = 1; ++ b[0] = 1; ++ b[1] = 2; + } + *ioDataSize = sizeof(UInt32)*2; + return kAudioHardwareNoError; +@@ -253,12 +295,26 @@ OSStatus AudioHardwareImpl::getPropertyString(CFStringRef str, UInt32* ioDataSiz + OSStatus AudioHardwareImpl::setPropertyData(const AudioObjectPropertyAddress* inAddress, UInt32 inQualifierDataSize, + const void* inQualifierData, UInt32 inDataSize, const void* inData) + { ++ if(inAddress->mSelector==kAudioHardwarePropertyRunLoop && m_myId==kAudioObjectSystemObject) { ++ if(!inData || inDataSize!=sizeof(CFRunLoopRef)) return kAudioHardwareBadPropertySizeError; ++ // NULL selects HAL-owned threads, which is the PulseAudio backend's ++ // existing mode. A caller-supplied run loop is not supported yet. ++ return *(const CFRunLoopRef*)inData ? kAudioHardwareUnsupportedOperationError : noErr; ++ } ++ + switch (inAddress->mSelector) + { + case kAudioDevicePropertyBufferSize: + { + return setBufferSize(*static_cast(inData)); + } ++ case kAudioDevicePropertyBufferFrameSize: { ++ if(!inData || inDataSize!=sizeof(UInt32)) return kAudioHardwareBadPropertySizeError; ++ UInt32 frames=*(const UInt32*)inData; ++ if(frames<128 || frames>2048 || !m_asbd.mBytesPerFrame || ++ frames>UINT32_MAX/m_asbd.mBytesPerFrame) return paramErr; ++ return setBufferSize(frames*m_asbd.mBytesPerFrame); ++ } + case kAudioDevicePropertyVolumeScalar: + { + // TODO: Implement setting the volume +@@ -283,7 +339,9 @@ OSStatus AudioHardwareImpl::setPropertyData(const AudioObjectPropertyAddress* in + memcpy(&m_asbd, setting, sizeof(m_asbd)); + + m_asbd.mFramesPerPacket = 1; +- m_asbd.mBytesPerFrame = UInt32(m_asbd.mBytesPerPacket = m_asbd.mChannelsPerFrame * m_asbd.mSampleRate * m_asbd.mBitsPerChannel / 8); ++ m_asbd.mBytesPerFrame = UInt32(m_asbd.mBytesPerPacket = ++ ((m_asbd.mFormatFlags & kAudioFormatFlagIsNonInterleaved) ? 1 : m_asbd.mChannelsPerFrame) ++ * m_asbd.mBitsPerChannel / 8); + + return kAudioHardwareNoError; + } +diff --git a/src/CoreAudio/CoreAudio/AudioHardwareImpl.h b/src/CoreAudio/CoreAudio/AudioHardwareImpl.h +index f74d9b0c3..655e90d51 100644 +--- a/src/CoreAudio/CoreAudio/AudioHardwareImpl.h ++++ b/src/CoreAudio/CoreAudio/AudioHardwareImpl.h +@@ -86,8 +86,7 @@ protected: + uint32_t m_bufferSize = 8192; + + CFStringRef m_name = CFSTR("unknown"), m_uid = CFSTR("unknown"), m_manufacturer = CFSTR("unknown"), m_modelUid = CFSTR("unknown"); +- AudioStreamBasicDescription m_asbd; ++ AudioStreamBasicDescription m_asbd = {}; + }; + + #endif /* AUDIOHARDWAREIMPL_H */ +- +diff --git a/src/CoreAudio/CoreAudio/CMakeLists.txt b/src/CoreAudio/CoreAudio/CMakeLists.txt +index 5f10c2f92..089c94127 100644 +--- a/src/CoreAudio/CoreAudio/CMakeLists.txt ++++ b/src/CoreAudio/CoreAudio/CMakeLists.txt +@@ -8,6 +8,7 @@ set(coreaudio_sources + AudioHardware.cpp + AudioHardwareImpl.cpp + HostTime.mm ++ AudioConverterShim.c + pulse/AudioHardwareImplPA.cpp + pulse/AudioHardwareImplPAInput.cpp + pulse/AudioHardwareImplPAOutput.cpp +diff --git a/src/CoreAudio/CoreAudio/HostTime.mm b/src/CoreAudio/CoreAudio/HostTime.mm +index 879fb48a1..fb99e99d1 100644 +--- a/src/CoreAudio/CoreAudio/HostTime.mm ++++ b/src/CoreAudio/CoreAudio/HostTime.mm +@@ -1,8 +1,32 @@ + #include "CoreAudio/HostTime.h" + #include "stub.h" ++#include ++ ++#ifdef __cplusplus ++extern "C" { ++#endif + + UInt64 AudioGetCurrentHostTime(void) + { +- STUB(); +- return 0; ++ return mach_absolute_time(); ++} ++ ++UInt64 AudioConvertHostTimeToNanos(UInt64 inHostTime) ++{ ++ static mach_timebase_info_data_t sTimebase = { 0, 0 }; ++ if (sTimebase.denom == 0) ++ (void)mach_timebase_info(&sTimebase); ++ return inHostTime * sTimebase.numer / sTimebase.denom; ++} ++ ++UInt64 AudioConvertNanosToHostTime(UInt64 inNanos) ++{ ++ static mach_timebase_info_data_t sTimebase = { 0, 0 }; ++ if (sTimebase.denom == 0) ++ (void)mach_timebase_info(&sTimebase); ++ return inNanos * sTimebase.denom / sTimebase.numer; ++} ++ ++#ifdef __cplusplus + } ++#endif +diff --git a/src/CoreAudio/CoreAudio/pulse/AudioHardwareImplPA.cpp b/src/CoreAudio/CoreAudio/pulse/AudioHardwareImplPA.cpp +index 85315086f..c43382686 100644 +--- a/src/CoreAudio/CoreAudio/pulse/AudioHardwareImplPA.cpp ++++ b/src/CoreAudio/CoreAudio/pulse/AudioHardwareImplPA.cpp +@@ -26,6 +26,8 @@ along with Darling. If not, see . + #include + #include + #include ++#include ++#include + + AudioHardwareImplPA::AudioHardwareImplPA(AudioObjectID myId, const char* paRole) + : AudioHardwareImpl(myId), m_paRole(paRole) +@@ -37,8 +39,34 @@ AudioHardwareImplPA::AudioHardwareImplPA(AudioObjectID myId, const char* paRole) + + AudioHardwareImplPA::~AudioHardwareImplPA() + { +- if (m_context) +- pa_context_unref(m_context); ++ for(auto& stream:m_streams) stream.second->stop(); ++ m_streams.clear(); ++ if(m_context && m_loop) { ++ m_loop->sync(^{ pa_context_disconnect(m_context);pa_context_unref(m_context); }); ++ m_context=nullptr; ++ } ++} ++ ++void AudioHardwareImplPA::syncPA(void (^block)(void)) { ++ if(m_loop) m_loop->sync(block);else block(); ++} ++ ++OSStatus AudioHardwareImplPA::stop(AudioDeviceIOProcID inProcID) { ++ __block OSStatus result = noErr; ++ syncPA(^{ ++ auto it = m_streams.find(inProcID); ++ if (it == m_streams.end()) { ++ result = kAudioHardwareNotRunningError; ++ return; ++ } ++ auto* stream = it->second.release(); ++ m_streams.erase(it); ++ stream->stop(); ++ // An IOProc may stop itself. Keep its C++ stream alive until the ++ // current PulseAudio callback has unwound on this serial queue. ++ m_loop->async(^{ delete stream; }); ++ }); ++ return result; + } + + OSStatus AudioHardwareImplPA::getPropertyData(const AudioObjectPropertyAddress* inAddress, UInt32 inQualifierDataSize, +@@ -46,6 +74,18 @@ OSStatus AudioHardwareImplPA::getPropertyData(const AudioObjectPropertyAddress* + { + switch (inAddress->mSelector) + { ++ case kAudioDevicePropertyStreams: ++ { ++ if (m_myId == kAudioObjectSystemObject) return kAudioHardwareUnknownPropertyError; ++ bool input=m_myId==kAudioObjectSystemObject+2; ++ bool matches=inAddress->mScope==kAudioObjectPropertyScopeGlobal || ++ inAddress->mScope==(input?kAudioObjectPropertyScopeInput:kAudioObjectPropertyScopeOutput); ++ UInt32 size=matches?sizeof(AudioStreamID):0; ++ if(outData && *ioDataSize(outData); devId && *ioDataSize >= sizeof(AudioDeviceID)) +@@ -115,112 +155,47 @@ AudioHardwareStream* AudioHardwareImplPA::createStream(AudioDeviceIOProc callbac + + extern "C" char ***_NSGetArgv(void); + +-static void paContextStateCB(pa_context* c, void* priv) +-{ +- void (^cb)(pa_context*) = (void (^)(pa_context*)) priv; +- +- pa_context_state_t state = pa_context_get_state(c); +- if (state == PA_CONTEXT_READY) +- { +- // std::cout << "PA_CONTEXT_READY\n"; +- +- cb(c); +- Block_release(cb); +- +- // FIXME: We won't be notified about later disconnects... +- pa_context_set_state_callback(c, nullptr, nullptr); +- } +- else if (state == PA_CONTEXT_FAILED) +- { +- std::cerr << "PulseAudio error: PA_CONTEXT_FAILED\n"; +- +- cb(nullptr); +- Block_release(cb); +- pa_context_set_state_callback(c, nullptr, nullptr); +- } +-} +- +-static const char* appNameFromExecutable() +-{ +- const char* name = (*_NSGetArgv())[0]; +- const char* p = std::strrchr(name, '/'); +- +- if (p && *(p+1)) +- return p+1; +- return name; ++struct PAContextReady { ++ std::mutex mutex; ++ std::condition_variable condition; ++ bool finished=false; ++}; ++ ++static void paContextStateCB(pa_context* context,void* userdata) { ++ auto state=pa_context_get_state(context); ++ if(state!=PA_CONTEXT_READY && state!=PA_CONTEXT_FAILED && state!=PA_CONTEXT_TERMINATED) return; ++ auto* ready=static_cast(userdata); ++ std::lock_guard lock(ready->mutex); ++ ready->finished=true; ++ ready->condition.notify_one(); + } + +-void AudioHardwareImplPA::getPAContext(void (^cb)(pa_context*)) +-{ +- // TODO: destruction +- static std::mutex mutex; +- +- if (m_context == nullptr) +- { +- std::unique_lock l(mutex); +- +- if (m_context == nullptr) +- { +- const char* appname = appNameFromExecutable(); +- const char* appid = "org.darlinghq.some-app"; +- +- pa_proplist* proplist = pa_proplist_new(); +- +- // Try to find a better application name & id from the bundle +- CFBundleRef mainBundle = CFBundleGetMainBundle(); +- if (mainBundle) +- { +- CFStringRef ident = CFBundleGetIdentifier(mainBundle); +- if (ident) +- appid = CFStringGetCStringPtr(ident, kCFStringEncodingUTF8); +- +- CFDictionaryRef infoDict = CFBundleGetLocalInfoDictionary(mainBundle); +- if (!infoDict) +- infoDict = CFBundleGetInfoDictionary(mainBundle); +- +- if (infoDict) +- { +- CFStringRef name = (CFStringRef) CFDictionaryGetValue(infoDict, CFSTR("CFBundleDisplayName")); +- if (!name) +- name = (CFStringRef) CFDictionaryGetValue(infoDict, CFSTR("CFBundleName")); +- if (name) +- appname = CFStringGetCStringPtr(name, kCFStringEncodingUTF8); +- } +- } +- +- pa_proplist_sets(proplist, PA_PROP_APPLICATION_NAME, appname); +- pa_proplist_sets(proplist, PA_PROP_APPLICATION_ID, appid); +- if (m_paRole) +- pa_proplist_sets(proplist, PA_PROP_MEDIA_ROLE, m_paRole); +- // pa_proplist_sets(proplist, PA_PROP_APPLICATION_ICON_NAME, "icon-name"); +- // pa_proplist_sets(proplist, PA_PROP_MEDIA_ROLE, "game"); +- +- m_loop.reset(new PADispatchMainLoop); +- +- m_context = pa_context_new_with_proplist(m_loop->getAPI(), appname, proplist); +- pa_proplist_free(proplist); +- +- if (!m_context) +- { +- cb(nullptr); +- return; +- } +- +- pa_context_set_state_callback(m_context, paContextStateCB, Block_copy(cb)); +- +- if (pa_context_connect(m_context, nullptr, PA_CONTEXT_NOFLAGS, nullptr) < 0) +- { +- std::cerr << "pa_context_connect() returned an error\n"; +- pa_context_set_state_callback(m_context, nullptr, nullptr); +- cb(nullptr); +- return; +- } +- +- m_loop->resume(); +- } +- } +- else +- cb(m_context); ++void AudioHardwareImplPA::getPAContext(void (^cb)(pa_context*)) { ++ // Serialize initial connection and reuse. The callback always runs on the ++ // PulseAudio queue after readiness, with the stream fully constructed. ++ static std::mutex creationMutex; ++ std::unique_lock creationLock(creationMutex); ++ if(!m_context) { ++ pa_proplist* properties=pa_proplist_new(); ++ pa_proplist_sets(properties,PA_PROP_APPLICATION_NAME,"Darling CoreAudio"); ++ pa_proplist_sets(properties,PA_PROP_APPLICATION_ID,"org.darlinghq.coreaudio"); ++ if(m_paRole) pa_proplist_sets(properties,PA_PROP_MEDIA_ROLE,m_paRole); ++ m_loop.reset(new PADispatchMainLoop); ++ m_context=pa_context_new_with_proplist(m_loop->getAPI(),"Darling CoreAudio",properties); ++ pa_proplist_free(properties); ++ if(!m_context) { cb(nullptr);return; } ++ PAContextReady ready; ++ pa_context_set_state_callback(m_context,paContextStateCB,&ready); ++ int connected=pa_context_connect(m_context,nullptr,PA_CONTEXT_NOFLAGS,nullptr); ++ m_loop->resume(); ++ if(connected>=0) { ++ std::unique_lock lock(ready.mutex); ++ ready.condition.wait_for(lock,std::chrono::seconds(10),[&]{return ready.finished;}); ++ } ++ // Remove the stack-backed readiness callback before leaving its scope. ++ m_loop->sync(^{pa_context_set_state_callback(m_context,nullptr,nullptr);}); ++ } ++ m_loop->sync(^{cb(pa_context_get_state(m_context)==PA_CONTEXT_READY?m_context:nullptr);}); + } + + pa_sample_spec AudioHardwareImplPA::paSampleSpecForASBD(const AudioStreamBasicDescription& asbd, bool* convertSignedUnsigned) +diff --git a/src/CoreAudio/CoreAudio/pulse/AudioHardwareImplPA.h b/src/CoreAudio/CoreAudio/pulse/AudioHardwareImplPA.h +index 8421491cd..3fe1c4e2c 100644 +--- a/src/CoreAudio/CoreAudio/pulse/AudioHardwareImplPA.h ++++ b/src/CoreAudio/CoreAudio/pulse/AudioHardwareImplPA.h +@@ -38,6 +38,8 @@ public: + const void* inQualifierData, UInt32 inDataSize, const void* inData) override; + + void getPAContext(void (^cb)(pa_context*)); ++ void syncPA(void (^block)(void)); ++ OSStatus stop(AudioDeviceIOProcID inProcID) override; + static pa_sample_spec paSampleSpecForASBD(const AudioStreamBasicDescription& asbd, bool* convertSignedUnsigned = nullptr); + protected: + AudioHardwareStream* createStream(AudioDeviceIOProc callback, void* clientData) override; +@@ -49,4 +51,3 @@ private: + }; + + #endif +- +diff --git a/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPA.cpp b/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPA.cpp +index 0db6ebda7..3f3bd6542 100644 +--- a/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPA.cpp ++++ b/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPA.cpp +@@ -26,6 +26,11 @@ along with Darling. If not, see . + AudioHardwareStreamPA::AudioHardwareStreamPA(AudioHardwareImplPA* hw, AudioDeviceIOProc callback, void* clientData) + : AudioHardwareStream(hw, false), m_callback(callback), m_clientData(clientData) + { ++} ++ ++void AudioHardwareStreamPA::initialize() ++{ ++ auto* hw=static_cast(m_hw); + hw->getPAContext(^(pa_context* context) { + if (!context) + { +@@ -42,6 +47,7 @@ AudioHardwareStreamPA::AudioHardwareStreamPA(AudioHardwareImplPA* hw, AudioDevic + } + + m_stream = pa_stream_new(context, "CoreAudio", &spec, nullptr); ++ if(!m_stream) return; + + //pa_stream_set_state_callback(m_stream, [](pa_stream *s, void *userdata) { + //std::cout << "Stream state: " << pa_stream_get_state(s) << std::endl; +@@ -59,8 +65,8 @@ AudioHardwareStreamPA::AudioHardwareStreamPA(AudioHardwareImplPA* hw, AudioDevic + + AudioHardwareStreamPA::~AudioHardwareStreamPA() + { +- if (m_stream) +- pa_stream_unref(m_stream); ++ auto* hw=static_cast(m_hw); ++ hw->syncPA(^{if(m_stream) {pa_stream_unref(m_stream);m_stream=nullptr;}}); + } + + void AudioHardwareStreamPA::start() +@@ -70,9 +76,20 @@ void AudioHardwareStreamPA::start() + + void AudioHardwareStreamPA::stop() + { +- std::unique_lock l(m_stopMutex); +- pa_stream_disconnect(m_stream); +- m_running = false; ++ auto* hw=static_cast(m_hw); ++ hw->syncPA(^{ ++ // All stream operations and callbacks are serialized by syncPA. ++ // A callback may call stop() itself; taking its mutex here deadlocks. ++ m_running=false; ++ if(m_stream) { ++ // Disconnect may leave queued protocol work holding a stream ref. ++ // Detach client callbacks before the C++ owner can be destroyed. ++ pa_stream_set_write_callback(m_stream,nullptr,nullptr); ++ pa_stream_set_read_callback(m_stream,nullptr,nullptr); ++ pa_stream_set_state_callback(m_stream,nullptr,nullptr); ++ pa_stream_disconnect(m_stream); ++ } ++ }); + } + + // This function seems to only convert unsigned to signed, but it works both ways in practice +diff --git a/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPA.h b/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPA.h +index 0425ae97f..fdf08d2dc 100644 +--- a/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPA.h ++++ b/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPA.h +@@ -34,18 +34,17 @@ public: + + void stop(/*void(^cbDone)()*/) override; + protected: ++ void initialize(); + virtual void start(); + void transformSignedUnsigned(AudioBufferList* abl) const; + protected: + AudioDeviceIOProc m_callback; + void* m_clientData; +- pa_stream* m_stream; +- void(^m_cbDone)(); ++ pa_stream* m_stream = nullptr; ++ void(^m_cbDone)() = nullptr; + bool m_convertSignedUnsigned = false; + + bool m_running = false; +- std::mutex m_stopMutex; + }; + + #endif /* AUDIOHARDWARESTREAMPA_H */ +- +diff --git a/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAInput.cpp b/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAInput.cpp +index d6825f1d5..739b70011 100644 +--- a/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAInput.cpp ++++ b/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAInput.cpp +@@ -25,6 +25,7 @@ along with Darling. If not, see . + AudioHardwareStreamPAInput::AudioHardwareStreamPAInput(AudioHardwareImplPA* hw, AudioDeviceIOProc callback, void* clientData) + : AudioHardwareStreamPA(hw, callback, clientData) + { ++ initialize(); + // m_dump.open("/tmp/dump.raw", std::ios_base::out | std::ios_base::binary); + } + +@@ -32,7 +33,6 @@ void AudioHardwareStreamPAInput::paStreamReadCB(pa_stream* s, size_t length, voi + { + AudioHardwareStreamPAInput* This = static_cast(self); + +- std::unique_lock l(This->m_stopMutex); + + if (!This->m_running) + return; +@@ -74,6 +74,7 @@ void AudioHardwareStreamPAInput::paStreamReadCB(pa_stream* s, size_t length, voi + // std::cout << "AudioHardwareStreamPAInput::paStreamReadCB(): bytes=" << nbytes << std::endl; + OSStatus status = This->m_callback(This->m_hw->id(), &fake, abl, &fake, nullptr, nullptr, This->m_clientData); + ++ if (!This->m_running) return; + pa_stream_drop(This->m_stream); + + if (status != noErr) +diff --git a/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAOutput.cpp b/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAOutput.cpp +index 278630f02..00db02f9c 100644 +--- a/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAOutput.cpp ++++ b/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAOutput.cpp +@@ -21,16 +21,17 @@ along with Darling. If not, see . + #include "AudioHardwareImplPA.h" + #include + #include ++#include + + AudioHardwareStreamPAOutput::AudioHardwareStreamPAOutput(AudioHardwareImplPA* hw, AudioDeviceIOProc callback, void* clientData) + : AudioHardwareStreamPA(hw, callback, clientData) + { ++ initialize(); + } + + void AudioHardwareStreamPAOutput::paStreamWriteCB(pa_stream* s, size_t length, void* self) + { + AudioHardwareStreamPAOutput* This = static_cast(self); +- std::unique_lock l(This->m_stopMutex); + + if (!This->m_running) + { +@@ -55,17 +56,26 @@ void AudioHardwareStreamPAOutput::paStreamWriteCB(pa_stream* s, size_t length, v + + // std::cout << "AudioHardwareStreamPAOutput() length req=" << length << ", done=" << done << std::endl; + size_t rqsize = std::min(This->m_bufferSize, length-done); +- pa_stream_begin_write(This->m_stream, (void**) &abl->mBuffers[0].mData, &rqsize); ++ if(pa_stream_begin_write(This->m_stream,(void**)&abl->mBuffers[0].mData,&rqsize)<0 || !rqsize) break; + + abl->mBuffers[0].mDataByteSize = rqsize; ++ fake.mHostTime=mach_absolute_time(); ++ fake.mSampleTime=This->m_frames; ++ fake.mFlags=kAudioTimeStampHostTimeValid|kAudioTimeStampSampleTimeValid; + + // Call the client for more data + OSStatus status = This->m_callback(This->m_hw->id(), &fake, nullptr, nullptr, abl, &fake, This->m_clientData); ++ if (!This->m_running) { ++ // The client stopped/disconnected the stream during its IOProc. ++ pa_stream_cancel_write(s); ++ return; ++ } + if (status != noErr || !abl->mBuffers[0].mDataByteSize) + { + // std::cerr << "AudioDeviceIOProc returned " << status << ", corking...\n"; + + pa_stream_cork(This->m_stream, true, [](pa_stream*, int, void*) {}, nullptr); ++ pa_stream_cancel_write(This->m_stream); + break; + } + else +@@ -85,6 +95,7 @@ void AudioHardwareStreamPAOutput::paStreamWriteCB(pa_stream* s, size_t length, v + } + + done += abl->mBuffers[0].mDataByteSize; ++ if(This->m_hw->asbd().mBytesPerFrame) This->m_frames+=abl->mBuffers[0].mDataByteSize/This->m_hw->asbd().mBytesPerFrame; + } + } + +@@ -108,4 +119,3 @@ void AudioHardwareStreamPAOutput::start() + // pa_stream_cork(m_stream, false, [](pa_stream*, int, void*) {}, nullptr); + AudioHardwareStreamPA::start(); + } +- +diff --git a/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAOutput.h b/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAOutput.h +index 735474128..a92e64cd5 100644 +--- a/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAOutput.h ++++ b/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAOutput.h +@@ -26,9 +26,9 @@ class AudioHardwareStreamPAOutput : public AudioHardwareStreamPA + public: + AudioHardwareStreamPAOutput(AudioHardwareImplPA* hw, AudioDeviceIOProc callback, void* clientData); + private: ++ uint64_t m_frames = 0; + void start() override; + static void paStreamWriteCB(pa_stream* s, size_t length, void* self); + }; + + #endif /* AUDIOHARDWARESTREAMPA_H */ +- +diff --git a/src/CoreAudio/CoreAudio/pulse/PADispatchMainLoop.cpp b/src/CoreAudio/CoreAudio/pulse/PADispatchMainLoop.cpp +index 63f3545e2..4a2d8ed62 100644 +--- a/src/CoreAudio/CoreAudio/pulse/PADispatchMainLoop.cpp ++++ b/src/CoreAudio/CoreAudio/pulse/PADispatchMainLoop.cpp +@@ -23,6 +23,8 @@ PADispatchMainLoop::PADispatchMainLoop() + { + // Create GCD queue + m_queue = dispatch_queue_create("org.darlinghq.coreaudio.pulseaudio", nullptr); ++ m_sources=dispatch_group_create(); ++ dispatch_queue_set_specific(m_queue,this,this,nullptr); + + // PulseAudio expects the event loop to be suspended while we're creating the context. + // I couldn't find any mention of this in the documentation, but figured it out the hard way. +@@ -32,9 +34,22 @@ PADispatchMainLoop::PADispatchMainLoop() + PADispatchMainLoop::~PADispatchMainLoop() + { + resume(); ++ dispatch_group_wait(m_sources,DISPATCH_TIME_FOREVER); ++ dispatch_sync(m_queue, ^{}); ++ dispatch_release(m_sources); + dispatch_release(m_queue); + } + ++void PADispatchMainLoop::sync(void (^block)(void)) { ++ if(dispatch_get_specific(this)==this) block(); ++ else {resume();dispatch_sync(m_queue,block);} ++} ++ ++void PADispatchMainLoop::async(void (^block)(void)) { ++ resume(); ++ dispatch_async(m_queue, block); ++} ++ + void PADispatchMainLoop::suspend() + { + if (!m_suspended) +@@ -87,6 +102,8 @@ struct dual_source + pa_io_event_flags_t events; + void* userdata; + int fd; ++ bool freed=false; ++ unsigned cancellations=2; + }; + + pa_io_event* PADispatchMainLoop::io_new(pa_mainloop_api *a, int fd, pa_io_event_flags_t events, pa_io_event_cb_t cb, void *userdata) +@@ -108,22 +125,25 @@ pa_io_event* PADispatchMainLoop::io_new(pa_mainloop_api *a, int fd, pa_io_event_ + dual->sourceWrite = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE, fd, 0, q); + + dispatch_source_set_event_handler(dual->sourceRead, ^{ +- dual->callback(This->getAPI(), reinterpret_cast(dual), fd, PA_IO_EVENT_INPUT, dual->userdata); ++ if(!dual->freed && dual->readResumed) dual->callback(This->getAPI(), reinterpret_cast(dual), fd, PA_IO_EVENT_INPUT, dual->userdata); + }); + + dispatch_source_set_event_handler(dual->sourceWrite, ^{ + // std::cout << "PADispatchMainLoop::io_new(): write event on fd " << fd << std::endl; +- dual->callback(This->getAPI(), reinterpret_cast(dual), fd, PA_IO_EVENT_OUTPUT, dual->userdata); ++ if(!dual->freed && dual->writeResumed) dual->callback(This->getAPI(), reinterpret_cast(dual), fd, PA_IO_EVENT_OUTPUT, dual->userdata); + }); + +- dispatch_source_set_cancel_handler(dual->sourceWrite, ^{ +- if (dual->destroy) +- { +- dual->destroy(This->getAPI(), reinterpret_cast(dual), dual->userdata); +- dual->destroy = nullptr; +- } +- delete dual; +- }); ++ dispatch_group_enter(This->m_sources); ++ dispatch_group_enter(This->m_sources); ++ void (^cancel)(void)=^{ ++ if(--dual->cancellations==0) { ++ if(dual->destroy) dual->destroy(This->getAPI(),reinterpret_cast(dual),dual->userdata); ++ delete dual; ++ } ++ dispatch_group_leave(This->m_sources); ++ }; ++ dispatch_source_set_cancel_handler(dual->sourceRead,cancel); ++ dispatch_source_set_cancel_handler(dual->sourceWrite,cancel); + + if (events & (PA_IO_EVENT_INPUT | PA_IO_EVENT_ERROR | PA_IO_EVENT_HANGUP)) + { +@@ -185,6 +205,7 @@ void PADispatchMainLoop::io_free(pa_io_event *e) + { + dual_source* dual = reinterpret_cast(e); + // std::cout << "PADispatchMainLoop::io_free(): fd=" << dual->fd << std::endl; ++ dual->freed=true; + + if (!dual->readResumed) + dispatch_resume(dual->sourceRead); +@@ -218,6 +239,7 @@ struct pa_timer + struct timespec when; + struct linux_timeval tv; + pa_time_event_destroy_cb_t destroy = nullptr; ++ bool freed=false; + }; + + pa_time_event *PADispatchMainLoop::time_new(pa_mainloop_api *a, const struct timeval *tv, pa_time_event_cb_t cb, void *userdata) +@@ -232,22 +254,24 @@ pa_time_event *PADispatchMainLoop::time_new(pa_mainloop_api *a, const struct tim + + timer->source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, q); + +- timer->when.tv_sec = real_tv->tv_sec; +- timer->when.tv_nsec = real_tv->tv_usec * 1000; // convert to ns +- timer->tv = *real_tv; ++ timer->when.tv_sec = real_tv ? real_tv->tv_sec : 0; ++ timer->when.tv_nsec = real_tv ? real_tv->tv_usec * 1000 : 0; ++ timer->tv = real_tv ? *real_tv : linux_timeval{}; + + // std::cout << "PADispatchMainLoop::time_new(), sec=" << real_tv->tv_sec << ", usec=" << real_tv->tv_usec << std::endl; + +- dispatch_source_set_timer(timer->source, dispatch_walltime(&timer->when, 0), 0, 0); ++ dispatch_source_set_timer(timer->source, real_tv ? dispatch_walltime(&timer->when, 0) : DISPATCH_TIME_FOREVER, DISPATCH_TIME_FOREVER, 0); + + dispatch_source_set_event_handler(timer->source, ^{ + // std::cout << "PADispatchMainLoop::time_new(): fired\n"; +- cb(This->getAPI(), reinterpret_cast(timer), reinterpret_cast(&timer->tv), timer->userdata); ++ if(!timer->freed) cb(This->getAPI(), reinterpret_cast(timer), reinterpret_cast(&timer->tv), timer->userdata); + }); ++ dispatch_group_enter(This->m_sources); + dispatch_source_set_cancel_handler(timer->source, ^{ + if (timer->destroy) + timer->destroy(This->getAPI(), reinterpret_cast(timer), timer->userdata); + delete timer; ++ dispatch_group_leave(This->m_sources); + }); + + dispatch_resume(timer->source); +@@ -259,6 +283,7 @@ void PADispatchMainLoop::time_restart(pa_time_event *e, const struct timeval *tv + { + const linux_timeval* real_tv = reinterpret_cast(tv); + pa_timer* timer = reinterpret_cast(e); ++ if(!tv) { dispatch_source_set_timer(timer->source,DISPATCH_TIME_FOREVER,DISPATCH_TIME_FOREVER,0);return; } + + timer->when.tv_sec = real_tv->tv_sec; + timer->when.tv_nsec = real_tv->tv_usec * 1000; // convert to ns +@@ -266,7 +291,7 @@ void PADispatchMainLoop::time_restart(pa_time_event *e, const struct timeval *tv + + // std::cout << "PADispatchMainLoop::time_restart(), sec=" << real_tv->tv_sec << ", usec=" << real_tv->tv_usec << std::endl; + +- dispatch_source_set_timer(timer->source, dispatch_walltime(&timer->when, 0), 0, 0); ++ dispatch_source_set_timer(timer->source, dispatch_walltime(&timer->when, 0), DISPATCH_TIME_FOREVER, 0); + } + + void PADispatchMainLoop::time_set_destroy(pa_time_event *e, pa_time_event_destroy_cb_t cb) +@@ -281,6 +306,7 @@ void PADispatchMainLoop::time_free(pa_time_event *e) + pa_timer* timer = reinterpret_cast(e); + + // std::cout << "PADispatchMainLoop::time_free()\n"; ++ timer->freed=true; + + dispatch_source_cancel(timer->source); + dispatch_release(timer->source); +@@ -289,6 +315,8 @@ void PADispatchMainLoop::time_free(pa_time_event *e) + struct defer_event + { + bool active = true; ++ bool scheduled = true; ++ bool freed = false; + void* userdata; + pa_defer_event_cb_t callback; + pa_defer_event_destroy_cb_t destroy = nullptr; +@@ -298,7 +326,8 @@ struct defer_event + void PADispatchMainLoop::defer_event_fire(void* context) + { + defer_event* ev = static_cast(context); +- if (ev->active) ++ ev->scheduled=false; ++ if (ev->active && !ev->freed) + { + pa_mainloop_api* a = ev->loop->getAPI(); + dispatch_queue_t q = static_cast(a->userdata)->m_queue; +@@ -307,8 +336,10 @@ void PADispatchMainLoop::defer_event_fire(void* context) + + ev->callback(a, static_cast(context), ev->userdata); + +- if (ev->active) +- dispatch_async_f(q, context, defer_event_fire); ++ if(ev->active && !ev->freed && !ev->scheduled) { ++ ev->scheduled=true; ++ dispatch_async_f(q,context,defer_event_fire); ++ } + } + } + +@@ -322,7 +353,7 @@ pa_defer_event *PADispatchMainLoop::defer_new(pa_mainloop_api *a, pa_defer_event + e->loop = This; + e->userdata = userdata; + e->callback = cb; +- ++ dispatch_group_enter(This->m_sources); + dispatch_async_f(q, e, defer_event_fire); + + return reinterpret_cast(e); +@@ -342,9 +373,11 @@ void PADispatchMainLoop::defer_enable(pa_defer_event *e, int b) + + // std::cout << "PADispatchMainLoop::defer_enable(): with userdata=" << ev->userdata << ", enable=" << b << std::endl; + +- if (!ev->active && b) +- dispatch_async_f(q, e, defer_event_fire); +- ev->active = !!b; ++ ev->active=!!b; ++ if(ev->active && !ev->freed && !ev->scheduled) { ++ ev->scheduled=true; ++ dispatch_async_f(q,e,defer_event_fire); ++ } + } + + void PADispatchMainLoop::defer_free(pa_defer_event *e) +@@ -355,10 +388,11 @@ void PADispatchMainLoop::defer_free(pa_defer_event *e) + // std::cout << "PADispatchMainLoop::defer_free(): with userdata=" << ev->userdata << std::endl; + + ev->active = false; +- ++ ev->freed=true; + dispatch_async(q, ^{ + if (ev->destroy) + ev->destroy(ev->loop->getAPI(), e, ev->userdata); ++ dispatch_group_leave(ev->loop->m_sources); + delete ev; + }); + } +diff --git a/src/CoreAudio/CoreAudio/pulse/PADispatchMainLoop.h b/src/CoreAudio/CoreAudio/pulse/PADispatchMainLoop.h +index ff83d1e15..0d84a2e0d 100644 +--- a/src/CoreAudio/CoreAudio/pulse/PADispatchMainLoop.h ++++ b/src/CoreAudio/CoreAudio/pulse/PADispatchMainLoop.h +@@ -30,6 +30,8 @@ public: + + void suspend(); + void resume(); ++ void sync(void (^block)(void)); ++ void async(void (^block)(void)); + + pa_mainloop_api* getAPI(); + private: +@@ -50,6 +52,7 @@ private: + static void defer_event_fire(void* context); + private: + dispatch_queue_t m_queue; ++ dispatch_group_t m_sources; + pa_mainloop_api m_api = {0}; + bool m_suspended = true; + }; +diff --git a/src/CoreAudio/CoreAudioComponent/AUHAL.cpp b/src/CoreAudio/CoreAudioComponent/AUHAL.cpp +index fe4d1eaa1..172f751ba 100644 +--- a/src/CoreAudio/CoreAudioComponent/AUHAL.cpp ++++ b/src/CoreAudio/CoreAudioComponent/AUHAL.cpp +@@ -1,3 +1,4 @@ ++#include "../AudioTrace.h" + /* + This file is part of Darling. + +@@ -19,6 +20,11 @@ along with Darling. If not, see . + + #include "AUHAL.h" + #include ++#include ++#include ++#include ++#include ++#include + + #pragma GCC visibility push(default) + AUDIOCOMPONENT_ENTRY(AUOutputBaseFactory, AUHAL); +@@ -42,6 +48,37 @@ AUHAL::~AUHAL() + Stop(); + } + ++UInt32 AUHAL::GetChannelLayoutTags(AudioUnitScope scope,AudioUnitElement element,AudioChannelLayoutTag* tags) { ++ if(element!=kOutputBus || (scope!=kAudioUnitScope_Input && scope!=kAudioUnitScope_Output)) return 0; ++ UInt32 channels=GetStreamFormat(scope,element).mChannelsPerFrame; ++ if(tags) *tags=channels==1?kAudioChannelLayoutTag_Mono:channels==2?kAudioChannelLayoutTag_Stereo:(kAudioChannelLayoutTag_DiscreteInOrder|channels); ++ return 1; ++} ++ ++UInt32 AUHAL::GetAudioChannelLayout(AudioUnitScope scope,AudioUnitElement element,AudioChannelLayout* layout,Boolean& writable) { ++ AudioChannelLayoutTag tag; ++ if(!GetChannelLayoutTags(scope,element,&tag)) return 0; ++ writable=!m_running; ++ if(layout) { layout->mChannelLayoutTag=tag;layout->mChannelBitmap=0;layout->mNumberChannelDescriptions=0; } ++ return offsetof(AudioChannelLayout,mChannelDescriptions); ++} ++ ++OSStatus AUHAL::SetAudioChannelLayout(AudioUnitScope scope,AudioUnitElement element,const AudioChannelLayout* layout) { ++ AudioChannelLayoutTag tag; ++ if(!layout || !GetChannelLayoutTags(scope,element,&tag)) return kAudioUnitErr_InvalidElement; ++ if(layout->mChannelLayoutTag==tag) return noErr; ++ /* darling-lol: a layout is acceptable when it describes the stream's number of channels, whichever way it is written: ++ * explicit channel descriptions (League sends two: left, right), a channel bitmap, or another tag with that count. ++ * Only the exact tag was accepted, so the game's stereo description list was refused (-10851) and it rebuilt its ++ * audio graph in a loop. */ ++ const UInt32 wanted=GetStreamFormat(scope,element).mChannelsPerFrame; ++ UInt32 channels; ++ if(layout->mChannelLayoutTag==kAudioChannelLayoutTag_UseChannelDescriptions) channels=layout->mNumberChannelDescriptions; ++ else if(layout->mChannelLayoutTag==kAudioChannelLayoutTag_UseChannelBitmap) channels=__builtin_popcount(layout->mChannelBitmap); ++ else channels=layout->mChannelLayoutTag&0xffff; ++ return channels==wanted?noErr:kAudioUnitErr_InvalidPropertyValue; ++} ++ + bool AUHAL::CanScheduleParameters() const + { + return true; +@@ -49,7 +86,7 @@ bool AUHAL::CanScheduleParameters() const + + bool AUHAL::StreamFormatWritable(AudioUnitScope scope, AudioUnitElement element) + { +- return !m_running && IsInitialized(); ++ return !m_running; + } + + OSStatus AUHAL::Version() +@@ -76,19 +113,26 @@ OSStatus AUHAL::Render(AudioUnitRenderActionFlags& ioActionFlags, const AudioTim + + OSStatus AUHAL::Start() + { ++ if(getenv("RCLOL_AUDIO_TRACE")) rclolAudioTrace("AUDIO_UNIT_NATIVE_START output=%d input=%d\n",m_enableOutput,m_enableInput); + if (m_running) + return noErr; + + if (m_enableOutput) + { + // std::cout << "Output is enabled, starting playback\n"; +- AudioDeviceCreateIOProcID(m_outputDevice, playbackCallback, this, &m_outputProcID); ++ OSStatus status=AudioDeviceCreateIOProcID(m_outputDevice,playbackCallback,this,&m_outputProcID); ++ if(status) return status; + + // m_auhalData.open("/tmp/auhal.raw", std::ios_base::binary | std::ios_base::out); + + const CAStreamBasicDescription& desc = GetStreamFormat(kAudioUnitScope_Input, kOutputBus); +- AudioDeviceSetProperty(m_outputDevice, nullptr, 0, false, kAudioDevicePropertyStreamFormat, sizeof(AudioStreamBasicDescription), &desc); +- AudioDeviceStart(m_outputDevice, m_outputProcID); ++ AudioStreamBasicDescription hardware=desc; ++ hardware.mFormatFlags&=~kAudioFormatFlagIsNonInterleaved; ++ hardware.mBytesPerFrame=hardware.mChannelsPerFrame*hardware.mBitsPerChannel/8; ++ hardware.mBytesPerPacket=hardware.mBytesPerFrame; ++ status=AudioDeviceSetProperty(m_outputDevice,nullptr,0,false,kAudioDevicePropertyStreamFormat,sizeof(hardware),&hardware); ++ if(!status) status=AudioDeviceStart(m_outputDevice,m_outputProcID); ++ if(status) {AudioDeviceDestroyIOProcID(m_outputDevice,m_outputProcID);m_outputProcID=0;return status;} + } + if (m_enableInput) + { +@@ -111,13 +155,15 @@ OSStatus AUHAL::Stop() + { + AudioDeviceStop(m_outputDevice, m_outputProcID); + AudioDeviceDestroyIOProcID(m_outputDevice, m_outputProcID); ++ m_outputProcID=0; + } + if (m_inputProcID) + { + AudioDeviceStop(m_inputDevice, m_inputProcID); + AudioDeviceDestroyIOProcID(m_inputDevice, m_inputProcID); ++ m_inputProcID=0; + } +- ++ m_running=false; + return noErr; + } + +@@ -125,6 +171,10 @@ OSStatus AUHAL::SetProperty(AudioUnitPropertyID inID, AudioUnitScope inScope, Au + { + switch (inID) + { ++ case kAudioDevicePropertyBufferFrameSize: { ++ AudioObjectPropertyAddress p={inID,kAudioObjectPropertyScopeOutput,kAudioObjectPropertyElementMaster}; ++ return AudioObjectSetPropertyData(m_outputDevice,&p,0,nullptr,inDataSize,inData); ++ } + case kAudioOutputUnitProperty_SetInputCallback: + { + ca_require(inDataSize == sizeof(AURenderCallbackStruct), InvalidPropertyValue); +@@ -176,7 +226,11 @@ OSStatus AUHAL::SetProperty(AudioUnitPropertyID inID, AudioUnitScope inScope, Au + return noErr; + } + } +- return AUBase::SetProperty(inID, inScope, inElement, inData, inDataSize); ++ { ++ OSStatus status=AUBase::SetProperty(inID,inScope,inElement,inData,inDataSize); ++ if(status && getenv("RCLOL_AUDIO_TRACE")) rclolAudioTrace("AUDIO_UNIT_SET property=%u scope=%u element=%u status=%d\n",inID,inScope,inElement,status); ++ return status; ++ } + InvalidPropertyValue: + return kAudioUnitErr_InvalidPropertyValue; + } +@@ -185,6 +239,8 @@ OSStatus AUHAL::GetPropertyInfo(AudioUnitPropertyID inID, AudioUnitScope inScope + { + switch (inID) + { ++ case kAudioDevicePropertyBufferFrameSize: ++ outDataSize=sizeof(UInt32);outWritable=true;return noErr; + case kAudioOutputUnitProperty_SetInputCallback: + { + outDataSize = sizeof(AURenderCallbackStruct); +@@ -210,13 +266,20 @@ OSStatus AUHAL::GetPropertyInfo(AudioUnitPropertyID inID, AudioUnitScope inScope + return noErr; + } + } +- return AUBase::GetPropertyInfo(inID, inScope, inElement, outDataSize, outWritable); ++ OSStatus status=AUBase::GetPropertyInfo(inID,inScope,inElement,outDataSize,outWritable); ++ if(status && getenv("RCLOL_AUDIO_TRACE")) rclolAudioTrace("AUDIO_UNIT_INFO property=%u scope=%u element=%u status=%d\n",inID,inScope,inElement,status); ++ return status; + } + + OSStatus AUHAL::GetProperty(AudioUnitPropertyID inID, AudioUnitScope inScope, AudioUnitElement inElement, void* outData) + { + switch (inID) + { ++ case kAudioDevicePropertyBufferFrameSize: { ++ AudioObjectPropertyAddress p={inID,kAudioObjectPropertyScopeOutput,kAudioObjectPropertyElementMaster}; ++ UInt32 size=sizeof(UInt32); ++ return AudioObjectGetPropertyData(m_outputDevice,&p,0,nullptr,&size,outData); ++ } + case kAudioOutputUnitProperty_SetInputCallback: + { + memcpy(outData, &m_outputAvailableCb, sizeof(m_outputAvailableCb)); +@@ -228,12 +291,12 @@ OSStatus AUHAL::GetProperty(AudioUnitPropertyID inID, AudioUnitScope inScope, Au + if (inElement == kOutputBus) + { + UInt32 value = m_enableOutput; +- memcpy(&outData, &value, sizeof(value)); ++ memcpy(outData, &value, sizeof(value)); + } + else if (inElement == kInputBus) + { + UInt32 value = m_enableInput; +- memcpy(&outData, &value, sizeof(value)); ++ memcpy(outData, &value, sizeof(value)); + } + else + return kAudioUnitErr_InvalidElement; +@@ -280,6 +343,9 @@ OSStatus AUHAL::recordCallback(AudioObjectID inObjectID, + OSStatus AUHAL::doPlayback(const AudioTimeStamp* inNow, AudioBufferList* outOutputData, const AudioTimeStamp* inOutputTime) + { + // std::cout << "AUHAL::DoPlayback()\n"; ++ static unsigned stackTraces=0; ++ if(getenv("RCLOL_AUDIO_TRACE") && stackTraces++<2) ++ rclolAudioTrace("AUDIO_CALLBACK_STACK bytes=%zu\n",pthread_get_stacksize_np(pthread_self())); + if (!HasInput(0)) + { + // std::cerr << "No connection\n"; +@@ -290,9 +356,44 @@ OSStatus AUHAL::doPlayback(const AudioTimeStamp* inNow, AudioBufferList* outOutp + AudioUnitRenderActionFlags flags = kAudioUnitRenderAction_PreRender; + const CAStreamBasicDescription& desc = GetStreamFormat(kAudioUnitScope_Input, kOutputBus); + +- UInt32 nFrames = outOutputData->mBuffers[0].mDataByteSize / (desc.mBytesPerFrame / outOutputData->mBuffers[0].mNumberChannels); +- result = GetInput(kOutputBus)->PullInputWithBufferList(flags, *inNow, kOutputBus, nFrames, outOutputData); +- ++ if(outOutputData->mNumberBuffers!=1 || !desc.mChannelsPerFrame || ++ !desc.mBitsPerChannel || desc.mBitsPerChannel%8) return kAudioUnitErr_FormatNotSupported; ++ UInt32 sampleBytes=desc.mBitsPerChannel/8; ++ UInt32 frameBytes=desc.mChannelsPerFrame*sampleBytes; ++ UInt32 nFrames=outOutputData->mBuffers[0].mDataByteSize/frameBytes; ++ if(desc.mFormatFlags&kAudioFormatFlagIsNonInterleaved) { ++ m_playbackSamples.resize(nFrames*frameBytes); ++ // Preserve fresh-buffer silence for callbacks that leave samples unwritten. ++ std::fill(m_playbackSamples.begin(),m_playbackSamples.end(),0); ++ m_playbackBufferList.resize(offsetof(AudioBufferList,mBuffers)+desc.mChannelsPerFrame*sizeof(AudioBuffer)); ++ AudioBufferList* planar=(AudioBufferList*)m_playbackBufferList.data(); ++ planar->mNumberBuffers=desc.mChannelsPerFrame; ++ for(UInt32 c=0;cmBuffers[c].mNumberChannels=1; ++ planar->mBuffers[c].mDataByteSize=nFrames*sampleBytes; ++ planar->mBuffers[c].mData=m_playbackSamples.data()+c*nFrames*sampleBytes; ++ } ++ result=GetInput(kOutputBus)->PullInputWithBufferList(flags,*inNow,kOutputBus,nFrames,planar); ++ if(!result) { ++ auto* destination=(unsigned char*)outOutputData->mBuffers[0].mData; ++ for(UInt32 f=0;fmBuffers[c].mData+f*sampleBytes,sampleBytes); ++ } ++ } else { ++ result=GetInput(kOutputBus)->PullInputWithBufferList(flags,*inNow,kOutputBus,nFrames,outOutputData); ++ } ++ ++ static unsigned traceRenders=0; ++ if (getenv("RCLOL_AUDIO_TRACE") && traceRenders++ < 16) { ++ float peak=0; ++ if (!result && desc.IsFloat() && sampleBytes==4) { ++ const float* samples=static_cast(outOutputData->mBuffers[0].mData); ++ for(UInt32 i=0;imBuffers[0].mDataByteSize/4;++i) ++ peak=std::max(peak,std::abs(samples[i])); ++ } ++ rclolAudioTrace("AUDIO_RENDER status=%d flags=%u frames=%u peak=%g\n",result,flags,nFrames,peak); ++ } + // std::cout << "Pull result: " << result << std::endl; + // std::cout << "Bytes: " << outOutputData->mBuffers[0].mDataByteSize << std::endl; + // m_auhalData.write((char*) outOutputData->mBuffers[0].mData, outOutputData->mBuffers[0].mDataByteSize); +diff --git a/src/CoreAudio/CoreAudioComponent/AUHAL.h b/src/CoreAudio/CoreAudioComponent/AUHAL.h +index bc21bcbbb..190a0ffc8 100644 +--- a/src/CoreAudio/CoreAudioComponent/AUHAL.h ++++ b/src/CoreAudio/CoreAudioComponent/AUHAL.h +@@ -25,6 +25,7 @@ along with Darling. If not, see . + #include + #include + #include ++#include + + class AUHAL : public AUBase + { +@@ -32,6 +33,9 @@ public: + AUHAL(AudioComponentInstance inInstance, bool supportRecording = true); + ~AUHAL(); + ++ UInt32 GetChannelLayoutTags(AudioUnitScope scope,AudioUnitElement element,AudioChannelLayoutTag* tags) override; ++ UInt32 GetAudioChannelLayout(AudioUnitScope scope,AudioUnitElement element,AudioChannelLayout* layout,Boolean& writable) override; ++ OSStatus SetAudioChannelLayout(AudioUnitScope scope,AudioUnitElement element,const AudioChannelLayout* layout) override; + bool CanScheduleParameters() const override; + bool StreamFormatWritable(AudioUnitScope scope, AudioUnitElement element) override; + OSStatus Version() override; +@@ -76,6 +80,11 @@ protected: + // TODO: Prepare for non-interleaved audio + std::unique_ptr m_buffer; + size_t m_bufferSize = 0, m_bufferUsed = 0; ++ ++ // Playback callbacks run serially on the device's PulseAudio queue. ++ // Keep planar conversion storage across callbacks and start/stop cycles. ++ std::vector m_playbackSamples; ++ std::vector m_playbackBufferList; + }; + + #endif +diff --git a/src/CoreAudio/CoreAudioComponent/CMakeLists.txt b/src/CoreAudio/CoreAudioComponent/CMakeLists.txt +index 38584e263..a4cf87529 100644 +--- a/src/CoreAudio/CoreAudioComponent/CMakeLists.txt ++++ b/src/CoreAudio/CoreAudioComponent/CMakeLists.txt +@@ -29,6 +29,7 @@ set(component_sources + PublicUtility/CAVectorUnit.cpp + AUHAL.cpp + SystemOutputAU.cpp ++ MatrixMixerAU.cpp + DefaultOutputAU.cpp + ) + +diff --git a/src/CoreAudio/CoreAudioComponent/DefaultOutputAU.cpp b/src/CoreAudio/CoreAudioComponent/DefaultOutputAU.cpp +index 8b92b6c50..18a57e359 100644 +--- a/src/CoreAudio/CoreAudioComponent/DefaultOutputAU.cpp ++++ b/src/CoreAudio/CoreAudioComponent/DefaultOutputAU.cpp +@@ -18,6 +18,8 @@ along with Darling. If not, see . + */ + #include "DefaultOutputAU.h" + ++#include ++#include + #pragma GCC visibility push(default) + AUDIOCOMPONENT_ENTRY(AUOutputBaseFactory, DefaultOutputAU); + #pragma GCC visibility pop +@@ -32,11 +34,27 @@ DefaultOutputAU::DefaultOutputAU(AudioComponentInstance inInstance) + { + } + ++/* darling-lol: League's game process sets the default device as the current device of its DefaultOutput unit. Refusing that ++ * (kAudioUnitErr_InvalidProperty) made it throw its audio graph away and build a new one in a loop, so it never played ++ * anything. Accepting it harmless: the unit plays on the default device anyway. ++ * ++ * Only the game process gets this. With the property accepted everywhere, the League client's Vanguard module went to its ++ * error state (MvgState 5, status -101) on every launch (2 of 2, 2026-10-11) and the client could not start a match; ++ * with the old behaviour it connected again (state 0). */ ++static bool callerIsLeagueGame() ++{ ++ static const bool game = []{ ++ const char* name = getprogname(); ++ return name && strcmp(name, "LeagueofLegends") == 0; ++ }(); ++ return game; ++} ++ + OSStatus DefaultOutputAU::GetPropertyInfo(AudioUnitPropertyID inID, AudioUnitScope inScope, AudioUnitElement inElement, UInt32& outDataSize, Boolean& outWritable) + { + OSStatus status = AUHAL::GetPropertyInfo(inID, inScope, inElement, outDataSize, outWritable); + +- if (inID == kAudioOutputUnitProperty_CurrentDevice) ++ if (inID == kAudioOutputUnitProperty_CurrentDevice && !callerIsLeagueGame()) + outWritable = false; + + return status; +@@ -48,7 +66,9 @@ OSStatus DefaultOutputAU::SetProperty(AudioUnitPropertyID inID, AudioUnitScope i + switch (inID) + { + case kAudioOutputUnitProperty_CurrentDevice: +- return kAudioUnitErr_InvalidProperty; ++ if (!callerIsLeagueGame()) ++ return kAudioUnitErr_InvalidProperty; ++ break; + } + return AUHAL::SetProperty(inID, inScope, inElement, inData, inDataSize); + } +diff --git a/src/CoreAudio/CoreAudioComponent/Info.plist b/src/CoreAudio/CoreAudioComponent/Info.plist +index da44a9f18..f658118d8 100644 +--- a/src/CoreAudio/CoreAudioComponent/Info.plist ++++ b/src/CoreAudio/CoreAudioComponent/Info.plist +@@ -70,6 +70,24 @@ + version + 1 + ++ ++ description ++ Float PCM matrix mixer ++ factoryFunction ++ MatrixMixerAUFactory ++ manufacturer ++ appl ++ name ++ Darling: MatrixMixer ++ sandboxSafe ++ ++ subtype ++ mxmx ++ type ++ aumx ++ version ++ 1 ++ + + CFBundleExecutable + CoreAudio +diff --git a/src/CoreAudio/include/CoreAudio/AudioHardwareBase.h b/src/CoreAudio/include/CoreAudio/AudioHardwareBase.h +index 102614203..9b80671a6 100644 +--- a/src/CoreAudio/include/CoreAudio/AudioHardwareBase.h ++++ b/src/CoreAudio/include/CoreAudio/AudioHardwareBase.h +@@ -13,12 +13,12 @@ typedef UInt32 AudioObjectPropertySelector; + typedef UInt32 AudioObjectPropertyScope; + typedef UInt32 AudioObjectPropertyElement; + +-struct AudioObjectPropertyAddress ++typedef struct AudioObjectPropertyAddress + { + AudioObjectPropertySelector mSelector; + AudioObjectPropertyScope mScope; + AudioObjectPropertyElement mElement; +-}; ++} AudioObjectPropertyAddress; + + enum + { +@@ -87,6 +87,14 @@ enum + }; + + ++typedef AudioObjectID AudioStreamID; ++enum { ++ kAudioStreamPropertyDirection = 'sdir', ++ kAudioStreamPropertyTerminalType = 'term', ++ kAudioStreamPropertyVirtualFormat = 'sfmt', ++ kAudioStreamPropertyPhysicalFormat = 'pft ', ++}; ++ + // TODO: A lot more is missing + + #ifdef __cplusplus +@@ -94,4 +102,3 @@ enum + #endif + + #endif /* AUDIOHARDWAREBASE_H */ +- +diff --git a/src/frameworks/AVFoundation/include/AVFoundation/AVCaptureDevice.h b/src/frameworks/AVFoundation/include/AVFoundation/AVCaptureDevice.h +index cb2896957..b2c25fbd4 100644 +--- a/src/frameworks/AVFoundation/include/AVFoundation/AVCaptureDevice.h ++++ b/src/frameworks/AVFoundation/include/AVFoundation/AVCaptureDevice.h +@@ -18,7 +18,22 @@ + */ + + #include ++#include ++ ++typedef NS_ENUM(NSInteger, AVAuthorizationStatus) { ++ AVAuthorizationStatusNotDetermined = 0, ++ AVAuthorizationStatusRestricted = 1, ++ AVAuthorizationStatusDenied = 2, ++ AVAuthorizationStatusAuthorized = 3, ++}; + + @interface AVCaptureDevice : NSObject + +++ (NSArray *)devices; +++ (NSArray *)devicesWithMediaType:(AVMediaType)mediaType; +++ (AVCaptureDevice *)defaultDeviceWithMediaType:(AVMediaType)mediaType; ++ +++ (AVAuthorizationStatus)authorizationStatusForMediaType:(AVMediaType)mediaType; +++ (void)requestAccessForMediaType:(AVMediaType)mediaType completionHandler:(void (^)(BOOL granted))handler; ++ + @end +diff --git a/src/frameworks/AVFoundation/src/AVCaptureDevice.m b/src/frameworks/AVFoundation/src/AVCaptureDevice.m +index cab525b91..2fcab003d 100644 +--- a/src/frameworks/AVFoundation/src/AVCaptureDevice.m ++++ b/src/frameworks/AVFoundation/src/AVCaptureDevice.m +@@ -21,6 +21,37 @@ + + @implementation AVCaptureDevice + +++ (NSArray *)devices { ++ return [NSArray array]; ++} ++ +++ (NSArray *)devicesWithMediaType:(AVMediaType)mediaType { ++ (void)mediaType; ++ return [self devices]; ++} ++ +++ (AVCaptureDevice *)defaultDeviceWithMediaType:(AVMediaType)mediaType { ++ (void)mediaType; ++ return nil; ++} ++ ++ +++ (AVAuthorizationStatus)authorizationStatusForMediaType:(AVMediaType)mediaType ++{ ++ // Darling has no camera/microphone capture or TCC consent store; ++ // report Denied so callers take their no-access path instead of ++ // raising an unrecognized-selector exception. ++ (void)mediaType; ++ return AVAuthorizationStatusDenied; ++} ++ +++ (void)requestAccessForMediaType:(AVMediaType)mediaType completionHandler:(void (^)(BOOL))handler ++{ ++ (void)mediaType; ++ if (handler) ++ handler(NO); ++} ++ + - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { + return [NSMethodSignature signatureWithObjCTypes: "v@:"]; + } +diff --git a/src/frameworks/Carbon/HIToolbox/CMakeLists.txt b/src/frameworks/Carbon/HIToolbox/CMakeLists.txt +index 3f0820484..7d310f61c 100644 +--- a/src/frameworks/Carbon/HIToolbox/CMakeLists.txt ++++ b/src/frameworks/Carbon/HIToolbox/CMakeLists.txt +@@ -19,6 +19,7 @@ add_framework(HIToolbox + + SOURCES + src/Keyboards.cpp ++ src/SecureInput.c + src/Events.cpp + src/CarbonEventsCore.c + src/CarbonEvents.c +diff --git a/src/frameworks/Carbon/HIToolbox/include/HIToolbox/Events.h b/src/frameworks/Carbon/HIToolbox/include/HIToolbox/Events.h +index 8c37ce2a6..71ed125a5 100644 +--- a/src/frameworks/Carbon/HIToolbox/include/HIToolbox/Events.h ++++ b/src/frameworks/Carbon/HIToolbox/include/HIToolbox/Events.h +@@ -19,6 +19,7 @@ OSStatus SetEventParameter(EventRef a, EventParamName b, EventParamType c, UInt3 + + + extern UInt8 LMGetKbdType(void); ++extern UInt8 LMGetKbdLast(void); + + enum { + kVK_ANSI_A = 0x00, +diff --git a/src/frameworks/Carbon/HIToolbox/include/HIToolbox/TextInputSources.h b/src/frameworks/Carbon/HIToolbox/include/HIToolbox/TextInputSources.h +index 8d457c216..4b42c9af8 100644 +--- a/src/frameworks/Carbon/HIToolbox/include/HIToolbox/TextInputSources.h ++++ b/src/frameworks/Carbon/HIToolbox/include/HIToolbox/TextInputSources.h +@@ -1,6 +1,7 @@ + #ifndef _Carbon_TextInputSources_H_ + #define _Carbon_TextInputSources_H_ + #include ++#include + + #ifdef __cplusplus + extern "C" { +@@ -25,6 +26,7 @@ extern TISInputSourceRef TISCopyCurrentKeyboardInputSource(void); + extern void* TISGetInputSourceProperty(TISInputSourceRef inputSourceRef, CFStringRef key); + + extern TISInputSourceRef TISCopyCurrentASCIICapableKeyboardLayoutInputSource(void); ++extern CFArrayRef TISCreateASCIICapableInputSourceList(void); + + #ifdef __cplusplus + } +diff --git a/src/frameworks/Carbon/HIToolbox/src/Events.cpp b/src/frameworks/Carbon/HIToolbox/src/Events.cpp +index a37fc641f..98e29c97a 100644 +--- a/src/frameworks/Carbon/HIToolbox/src/Events.cpp ++++ b/src/frameworks/Carbon/HIToolbox/src/Events.cpp +@@ -4,3 +4,10 @@ UInt8 LMGetKbdType(void) + { + return 0; + } ++ ++UInt8 LMGetKbdLast(void) ++{ ++ // Darling exposes one logical keyboard through its current XKB layout. ++ // Its last-used keyboard type is therefore the current keyboard type. ++ return LMGetKbdType(); ++} +diff --git a/src/frameworks/Carbon/HIToolbox/src/TextInputSources.mm b/src/frameworks/Carbon/HIToolbox/src/TextInputSources.mm +index 347cb530c..f73278a35 100644 +--- a/src/frameworks/Carbon/HIToolbox/src/TextInputSources.mm ++++ b/src/frameworks/Carbon/HIToolbox/src/TextInputSources.mm +@@ -85,3 +85,10 @@ TISInputSourceRef TISCopyCurrentASCIICapableKeyboardLayoutInputSource(void) + + return NULL; + } ++ ++CFArrayRef TISCreateASCIICapableInputSourceList(void) ++{ ++ /* Until ASCII-capable layouts can be enumerated, return an owned empty ++ * list. A NULL list would crash callers that unconditionally CFRelease. */ ++ return CFArrayCreate(NULL, NULL, 0, &kCFTypeArrayCallBacks); ++} +diff --git a/src/frameworks/CoreLocation/include/CoreLocation/CLLocation.h b/src/frameworks/CoreLocation/include/CoreLocation/CLLocation.h +index 090de1df2..fe1f95f3b 100644 +--- a/src/frameworks/CoreLocation/include/CoreLocation/CLLocation.h ++++ b/src/frameworks/CoreLocation/include/CoreLocation/CLLocation.h +@@ -19,6 +19,15 @@ + + #include + ++typedef double CLLocationAccuracy; ++ ++extern const CLLocationAccuracy kCLLocationAccuracyBestForNavigation; ++extern const CLLocationAccuracy kCLLocationAccuracyBest; ++extern const CLLocationAccuracy kCLLocationAccuracyNearestTenMeters; ++extern const CLLocationAccuracy kCLLocationAccuracyHundredMeters; ++extern const CLLocationAccuracy kCLLocationAccuracyKilometer; ++extern const CLLocationAccuracy kCLLocationAccuracyThreeKilometers; ++ + @interface CLLocation : NSObject + + @end +diff --git a/src/frameworks/CoreLocation/include/CoreLocation/CLLocationManager.h b/src/frameworks/CoreLocation/include/CoreLocation/CLLocationManager.h +index 9e0fa55c4..993124e76 100644 +--- a/src/frameworks/CoreLocation/include/CoreLocation/CLLocationManager.h ++++ b/src/frameworks/CoreLocation/include/CoreLocation/CLLocationManager.h +@@ -20,5 +20,14 @@ + #include + + @interface CLLocationManager : NSObject ++{ ++ id _delegate; ++} ++ ++/* darling-lol: real property so callers (Electron's device-permission ++ * path) get a proper accessor instead of falling through to the stub ++ * forwardInvocation, whose 0-arg signature raised ++ * `NSForwardSignatureError: invoked with 3 args, but 2 expected`. */ ++@property (nonatomic, assign) id delegate; + + @end +diff --git a/src/frameworks/CoreLocation/src/CLLocation.m b/src/frameworks/CoreLocation/src/CLLocation.m +index f396e00e4..5f3965705 100644 +--- a/src/frameworks/CoreLocation/src/CLLocation.m ++++ b/src/frameworks/CoreLocation/src/CLLocation.m +@@ -32,3 +32,10 @@ + } + + @end ++ ++const CLLocationAccuracy kCLLocationAccuracyBestForNavigation = -2.0; ++const CLLocationAccuracy kCLLocationAccuracyBest = -1.0; ++const CLLocationAccuracy kCLLocationAccuracyNearestTenMeters = 10.0; ++const CLLocationAccuracy kCLLocationAccuracyHundredMeters = 100.0; ++const CLLocationAccuracy kCLLocationAccuracyKilometer = 1000.0; ++const CLLocationAccuracy kCLLocationAccuracyThreeKilometers = 3000.0; +diff --git a/src/frameworks/CoreLocation/src/CLLocationManager.m b/src/frameworks/CoreLocation/src/CLLocationManager.m +index 0b927c40e..d186494e7 100644 +--- a/src/frameworks/CoreLocation/src/CLLocationManager.m ++++ b/src/frameworks/CoreLocation/src/CLLocationManager.m +@@ -21,6 +21,8 @@ + + @implementation CLLocationManager + ++@synthesize delegate = _delegate; ++ + - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector + { + return [NSMethodSignature signatureWithObjCTypes: "v@:"]; +diff --git a/src/frameworks/CoreMedia/src/CoreMedia.c b/src/frameworks/CoreMedia/src/CoreMedia.c +index 3d76e8103..4a975959c 100644 +--- a/src/frameworks/CoreMedia/src/CoreMedia.c ++++ b/src/frameworks/CoreMedia/src/CoreMedia.c +@@ -26,6 +26,32 @@ + const CFStringRef kCMFormatDescriptionExtension_SampleDescriptionExtensionAtoms = CFSTR("SampleDescriptionExtensionAtoms"); + const CFStringRef kCMSampleAttachmentKey_NotSync = CFSTR("NotSync"); + ++const CFStringRef kCMFormatDescriptionTransferFunction_ITU_R_709_2 = CFSTR("ITU_R_709_2"); ++const CFStringRef kCMFormatDescriptionTransferFunction_SMPTE_240M_1995 = CFSTR("SMPTE_240M_1995"); ++const CFStringRef kCMFormatDescriptionTransferFunction_UseGamma = CFSTR("UseGamma"); ++const CFStringRef kCMFormatDescriptionTransferFunction_ITU_R_2020 = CFSTR("ITU_R_2020"); ++const CFStringRef kCMFormatDescriptionTransferFunction_ITU_R_2100_HLG = CFSTR("ITU_R_2100_HLG"); ++const CFStringRef kCMFormatDescriptionTransferFunction_SMPTE_ST_2084_PQ = CFSTR("SMPTE_ST_2084_PQ"); ++const CFStringRef kCMFormatDescriptionTransferFunction_SMPTE_ST_428_1 = CFSTR("SMPTE_ST_428_1"); ++const CFStringRef kCMFormatDescriptionTransferFunction_Linear = CFSTR("Linear"); ++const CFStringRef kCMFormatDescriptionTransferFunction_sRGB = CFSTR("sRGB"); ++const CFStringRef kCMFormatDescriptionColorPrimaries_ITU_R_709_2 = CFSTR("ITU_R_709_2"); ++const CFStringRef kCMFormatDescriptionColorPrimaries_EBU_3213 = CFSTR("EBU_3213"); ++const CFStringRef kCMFormatDescriptionColorPrimaries_SMPTE_C = CFSTR("SMPTE_C"); ++const CFStringRef kCMFormatDescriptionColorPrimaries_ITU_R_2020 = CFSTR("ITU_R_2020"); ++const CFStringRef kCMFormatDescriptionColorPrimaries_P3_D65 = CFSTR("P3_D65"); ++const CFStringRef kCMFormatDescriptionColorPrimaries_DCI_P3 = CFSTR("DCI_P3"); ++const CFStringRef kCMFormatDescriptionExtension_ContentLightLevelInfo = CFSTR("ContentLightLevelInfo"); ++const CFStringRef kCMFormatDescriptionExtension_Depth = CFSTR("Depth"); ++const CFStringRef kCMFormatDescriptionExtension_FormatName = CFSTR("FormatName"); ++const CFStringRef kCMFormatDescriptionExtension_FullRangeVideo = CFSTR("FullRangeVideo"); ++const CFStringRef kCMFormatDescriptionExtension_MasteringDisplayColorVolume = CFSTR("MasteringDisplayColorVolume"); ++const CFStringRef kCMSampleAttachmentKey_IsDependedOnByOthers = CFSTR("IsDependedOnByOthers"); ++const CFStringRef kCMFormatDescriptionYCbCrMatrix_ITU_R_709_2 = CFSTR("ITU_R_709_2"); ++const CFStringRef kCMFormatDescriptionYCbCrMatrix_ITU_R_601_4 = CFSTR("ITU_R_601_4"); ++const CFStringRef kCMFormatDescriptionYCbCrMatrix_SMPTE_240M_1995 = CFSTR("SMPTE_240M_1995"); ++const CFStringRef kCMFormatDescriptionYCbCrMatrix_ITU_R_2020 = CFSTR("ITU_R_2020"); ++ + static int verbose = 0; + + __attribute__((constructor)) +diff --git a/src/frameworks/CoreServices/CMakeLists.txt b/src/frameworks/CoreServices/CMakeLists.txt +index 3fac3be55..aae2c7435 100644 +--- a/src/frameworks/CoreServices/CMakeLists.txt ++++ b/src/frameworks/CoreServices/CMakeLists.txt +@@ -82,6 +82,7 @@ reexport(CoreServices Metadata ${Metadata_BUILD}) + reexport(CoreServices SearchKit ${SearchKit_BUILD}) + reexport(CoreServices SharedFileList ${SharedFileList_BUILD}) + reexport(CoreServices OSServices ${OSServices_BUILD}) ++reexport(CoreServices Foundation ${CMAKE_BINARY_DIR}/src/external/foundation/Foundation) + + set_property(TARGET CoreServices APPEND_STRING PROPERTY + LINK_FLAGS " -Wl,-reexported_symbols_list,${CMAKE_CURRENT_SOURCE_DIR}/reexport.exp") +diff --git a/src/frameworks/CoreServices/src/CarbonCore/UnicodeUtilities.cpp b/src/frameworks/CoreServices/src/CarbonCore/UnicodeUtilities.cpp +index 094a9483d..84810f6ab 100644 +--- a/src/frameworks/CoreServices/src/CarbonCore/UnicodeUtilities.cpp ++++ b/src/frameworks/CoreServices/src/CarbonCore/UnicodeUtilities.cpp +@@ -212,8 +212,10 @@ OSStatus UCKeyTranslate(const UCKeyboardLayout *keyLayoutPtr, UInt16 virtualKeyC + const UCKeyToCharTableIndex* tableIndex = (UCKeyToCharTableIndex*) (((char*) keyLayoutPtr) + keyLayoutPtr->keyboardTypeList[0].keyToCharTableIndexOffset); + uint8_t tableNumber = modMaps->defaultTableNum; + +- if ((modifierKeyState >> 8) < modMaps->modifiersCount) +- tableNumber = modMaps->tableNum[(modifierKeyState >> 8)]; ++ // UCKeyTranslate receives the modifier byte after the caller shifts the ++ // Carbon EventRecord flags. Shifting again loses Shift and Option state. ++ if (modifierKeyState < modMaps->modifiersCount) ++ tableNumber = modMaps->tableNum[modifierKeyState]; + + UniChar c; + if (virtualKeyCode >= tableIndex->keyToCharTableSize || tableNumber >= tableIndex->keyToCharTableCount) +diff --git a/src/frameworks/CoreServices/src/FSEvents/FSEventsImpl.m b/src/frameworks/CoreServices/src/FSEvents/FSEventsImpl.m +index cf9933a84..6651d50dc 100644 +--- a/src/frameworks/CoreServices/src/FSEvents/FSEventsImpl.m ++++ b/src/frameworks/CoreServices/src/FSEvents/FSEventsImpl.m +@@ -23,6 +23,7 @@ + #include + #include + #include ++#include + #include + + static dispatch_queue_t g_fsEventsQueue = NULL; +@@ -68,6 +69,24 @@ static void rlPerform(void* info) + for (NSString* path in _pathsToWatch) + { + int wd = inotify_add_watch(_fd, [path UTF8String], IN_ATTRIB | IN_CREATE | IN_DELETE | IN_DELETE_SELF | IN_MODIFY | IN_MOVE); ++ /* darling-lol: a path that doesn't exist yet (e.g. /etc/hosts or ++ * /etc/resolv.conf missing in the prefix) makes inotify_add_watch ++ * return -1. Apple's FSEvents tolerates unwatchable paths and the ++ * stream still starts; Chromium (dns_config_service_posix) treats ++ * a failed start as fatal for its DNS watcher. Skip failures ++ * instead of inserting a bogus -1 wd, and remember the path so a ++ * later event on the directory can still be reported. */ ++ if (wd < 0) ++ { ++ if (getenv("DARLING_LOL_FSEVLOG") != NULL) ++ { ++ char b[512]; ++ int n = snprintf(b, sizeof(b), "FSEV add_watch failed: %s (errno=%d)\n", ++ [path UTF8String], errno); ++ if (n > 0) write(2, b, n); ++ } ++ continue; ++ } + [wdmap setObject: path + forKey: [NSNumber numberWithInt: wd]]; + } +diff --git a/src/frameworks/CoreServices/src/LaunchServices/LSOpen.m b/src/frameworks/CoreServices/src/LaunchServices/LSOpen.m +index dd78f6df4..fc25c7970 100644 +--- a/src/frameworks/CoreServices/src/LaunchServices/LSOpen.m ++++ b/src/frameworks/CoreServices/src/LaunchServices/LSOpen.m +@@ -19,8 +19,100 @@ along with Darling. If not, see . + + + #include ++#include + #include + #include ++#include ++#include ++#include ++ ++// darling-lol M2a: minimal bundle launching. ++// Supports: file URLs (or bare POSIX paths) pointing at .app bundles, ++// optionally with an explicit target app via inAppParams->application. ++// The bundle's executable is spawned directly with the emulated posix_spawn ++// (no launchd job registration yet — see _LSLaunchApplication). ++// NOT yet supported (documented deviations): ++// - AppleEvent delivery (oapp/odoc): the app is spawned bare, as if launched ++// by hand. Apps waiting for their initial event will park. Needs #425/#937. ++// - inAppParams->argv / ->environment: not propagated (_LSLaunchApplication ++// takes no argv yet). ++// - inRole / launch flags (kLSLaunchNewInstance etc.): every launch spawns a ++// new instance; no running-app registry, so outPSNs is left untouched. ++// - non-bundle documents without an explicit application: there is no default ++// handler database yet (needs UTI/LaunchServices DB). ++static OSStatus ++LSOpenOneURL(CFURLRef url, const LSApplicationParameters *params) ++{ ++ CFStringRef pathStr; ++ char path[4096]; ++ struct stat st; ++ CFURLRef appURL = NULL; ++ OSStatus status; ++ ++ if (url == NULL) ++ return paramErr; ++ ++ // open(1) builds URLs via +URLWithString from a raw path, which may yield ++ // a scheme-less URL object. CFURLCopyFileSystemPath works either way. ++ pathStr = CFURLCopyFileSystemPath(url, kCFURLPOSIXPathStyle); ++ if (pathStr == NULL) ++ return paramErr; ++ ++ if (!CFStringGetCString(pathStr, path, sizeof(path), kCFStringEncodingUTF8)) ++ { ++ CFRelease(pathStr); ++ return paramErr; ++ } ++ CFRelease(pathStr); ++ ++ if (params != NULL && params->application != NULL) ++ { ++ // Explicit target application wins; the URLs become its documents ++ // (delivered as argv once M2c lands; for now they are dropped). ++ char appPath[4096]; ++ CFStringRef appStr; ++ status = FSRefMakePath(params->application, (UInt8*) appPath, sizeof(appPath) - 1); ++ if (status != noErr) ++ return status; ++ appStr = CFStringCreateWithCString(NULL, appPath, kCFStringEncodingUTF8); ++ appURL = CFURLCreateWithFileSystemPath(NULL, appStr, kCFURLPOSIXPathStyle, TRUE); ++ CFRelease(appStr); ++ } ++ else ++ { ++ size_t len = strlen(path); ++ Boolean looksLikeBundle = FALSE; ++ ++ if (len > 4 && strcasecmp(path + len - 4, ".app") == 0) ++ looksLikeBundle = TRUE; ++ else if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) ++ { ++ // Bundle without .app suffix (e.g. helper apps, frameworks-as-apps). ++ char plist[4096 + 32]; ++ snprintf(plist, sizeof(plist), "%s/Contents/Info.plist", path); ++ if (stat(plist, &st) == 0) ++ looksLikeBundle = TRUE; ++ } ++ ++ if (!looksLikeBundle) ++ return kLSApplicationNotFoundErr; // no default-handler DB yet ++ ++ CFStringRef tmp = CFStringCreateWithCString(NULL, path, kCFStringEncodingUTF8); ++ appURL = CFURLCreateWithFileSystemPath(NULL, tmp, kCFURLPOSIXPathStyle, TRUE); ++ CFRelease(tmp); ++ } ++ ++ if (appURL == NULL) ++ return paramErr; ++ ++ status = _LSLaunchApplication(appURL); ++ CFRelease(appURL); ++ ++ // _LSLaunchApplication returns 1 on failure instead of a MacErrors code. ++ if (status != noErr) ++ status = fnfErr; ++ return status; ++} + + OSStatus + LSOpenURLsWithRole( +@@ -31,8 +123,27 @@ LSOpenURLsWithRole( + ProcessSerialNumber * outPSNs, + CFIndex inMaxPSNCount) + { +- puts("LSOpenURLsWithRole STUB"); +- return unimpErr; ++ CFIndex count, i, launched = 0; ++ OSStatus status, firstErr = noErr; ++ ++ (void) inRole; ++ (void) inAEParam; ++ (void) outPSNs; ++ (void) inMaxPSNCount; ++ ++ if (inURLs == NULL || (count = CFArrayGetCount(inURLs)) <= 0) ++ return paramErr; ++ ++ for (i = 0; i < count; i++) ++ { ++ status = LSOpenOneURL(CFArrayGetValueAtIndex(inURLs, i), inAppParams); ++ if (status == noErr) ++ launched++; ++ else if (launched == 0 && firstErr == noErr) ++ firstErr = status; ++ } ++ ++ return launched > 0 ? noErr : firstErr; + } + + OSStatus +diff --git a/src/frameworks/CoreServices/src/LaunchServices/LaunchServices.c b/src/frameworks/CoreServices/src/LaunchServices/LaunchServices.c +index 854fa1e7f..5a39038d3 100644 +--- a/src/frameworks/CoreServices/src/LaunchServices/LaunchServices.c ++++ b/src/frameworks/CoreServices/src/LaunchServices/LaunchServices.c +@@ -21,7 +21,11 @@ + #include + #include + #include +-#include ++#include ++#include ++#include ++ ++extern char** environ; + + static void get_main_executable_info(CFURLRef appURL, CFStringRef *exec, CFStringRef *bundleName); + +@@ -108,8 +112,14 @@ OSStatus _LSLaunchApplication(CFURLRef appPath) + CFRelease(url_plus_exec); + url_plus_exec = CFURLCreateCopyAppendingPathComponent(kCFAllocatorDefault, pre, executable, FALSE); + CFRelease(pre); +- /* Launch the executable */ +- executable = CFURLGetString(url_plus_exec); ++ /* Launch the executable. NB: CFURLGetString would give back a ++ * percent-encoded file:// URL, which is not spawnable; extract the ++ * POSIX path instead. */ ++ CFRelease(executable); ++ executable = CFURLCopyFileSystemPath(url_plus_exec, kCFURLPOSIXPathStyle); ++ CFRelease(url_plus_exec); ++ if (executable == NULL) ++ return 1; + + /* Try to use CFBundleName first */ + if (bundleName == NULL) +@@ -122,15 +132,19 @@ OSStatus _LSLaunchApplication(CFURLRef appPath) + CFStringGetFileSystemRepresentation(executable, exec, execSize); + argv[0] = exec; + argv[1] = NULL; +- pid = spawn_via_launchd(name, (const char *const *)argv, NULL); +- if (pid < 0) ++ /* Launch the executable directly. The macOS-correct route is ++ * spawn_via_launchd (launchd job registration, -W waiting, PSN ++ * tracking), but Darling's vproc_mig_spawn2 currently fails the ++ * request (EIO) and nothing in-tree exercises it, so spawn the ++ * child ourselves via the emulated posix_spawn. */ ++ int err = posix_spawn(&pid, exec, NULL, NULL, argv, environ); ++ if (err != 0) + { +- printf("%s: Failed to spawn via launchd\n", exec); ++ printf("%s: Failed to posix_spawn: %s\n", exec, strerror(err)); + return 1; + } + free(name); + free(exec); +- CFRelease(url_plus_exec); + CFRelease(executable); + + } +@@ -184,3 +198,25 @@ err_pre_info: + CFRelease(rs); + CFRelease(info_plist_url); + } ++ ++OSStatus LSSetDefaultHandlerForURLScheme(CFStringRef inURLScheme, CFStringRef inHandlerBundleID) ++{ ++ /* Darling has no shared LaunchServices database yet; accept and ++ * ignore (callers such as Discord's URL-scheme registration only ++ * need the call to succeed). */ ++ (void)inURLScheme; ++ (void)inHandlerBundleID; ++ if (verbose) ++ printf("STUB: LSSetDefaultHandlerForURLScheme\n"); ++ return noErr; ++} ++ ++OSStatus LSSetDefaultRoleHandlerForContentType(CFStringRef inContentType, LSRolesMask inRole, CFStringRef inHandlerBundleID) ++{ ++ (void)inContentType; ++ (void)inRole; ++ (void)inHandlerBundleID; ++ if (verbose) ++ printf("STUB: LSSetDefaultRoleHandlerForContentType\n"); ++ return noErr; ++} +diff --git a/src/frameworks/CoreVideo/include/CoreVideo/CVBuffer.h b/src/frameworks/CoreVideo/include/CoreVideo/CVBuffer.h +index 09b70e4cb..2f7f5b89a 100644 +--- a/src/frameworks/CoreVideo/include/CoreVideo/CVBuffer.h ++++ b/src/frameworks/CoreVideo/include/CoreVideo/CVBuffer.h +@@ -8,6 +8,8 @@ __BEGIN_DECLS + + typedef struct CV_BRIDGED_TYPE(id) __CVBuffer* CVBufferRef; + ++extern const CFStringRef kCVBufferPropagatedAttachmentsKey; ++ + __END_DECLS + + #endif // _COREVIDEO_CVBUFFER_H_ +diff --git a/src/frameworks/CoreVideo/include/CoreVideo/CVImageBuffer.h b/src/frameworks/CoreVideo/include/CoreVideo/CVImageBuffer.h +index 937ba8312..45ab80e75 100644 +--- a/src/frameworks/CoreVideo/include/CoreVideo/CVImageBuffer.h ++++ b/src/frameworks/CoreVideo/include/CoreVideo/CVImageBuffer.h +@@ -30,6 +30,7 @@ extern const CFStringRef kCVImageBufferTransferFunction_Linear; + extern const CFStringRef kCVImageBufferTransferFunction_SMPTE_ST_2084_PQ; + extern const CFStringRef kCVImageBufferTransferFunction_SMPTE_ST_428_1; + extern const CFStringRef kCVImageBufferTransferFunction_SMPTE_C; ++extern const CFStringRef kCVImageBufferTransferFunction_sRGB; + + extern CFStringRef const kCVImageBufferPixelAspectRatioHorizontalSpacingKey; + extern CFStringRef const kCVImageBufferPixelAspectRatioVerticalSpacingKey; +@@ -43,5 +44,7 @@ extern CFStringRef const kCVImageBufferColorPrimaries_SMPTE_C; + extern CFStringRef const kCVImageBufferTransferFunction_ITU_R_709_2; + extern CFStringRef const kCVImageBufferTransferFunction_SMPTE_240M_1995; + extern CFStringRef const kCVImageBufferTransferFunction_UseGamma; ++extern const CFStringRef kCVImageBufferContentLightLevelInfoKey; ++extern const CFStringRef kCVImageBufferMasteringDisplayColorVolumeKey; + + #endif // _COREVIDEO_CVIMAGEBUFFER_H_ +diff --git a/src/frameworks/CoreVideo/include/CoreVideo/CVPixelBufferPool.h b/src/frameworks/CoreVideo/include/CoreVideo/CVPixelBufferPool.h +index 299caa5b1..bdff6028c 100644 +--- a/src/frameworks/CoreVideo/include/CoreVideo/CVPixelBufferPool.h ++++ b/src/frameworks/CoreVideo/include/CoreVideo/CVPixelBufferPool.h +@@ -26,6 +26,7 @@ + __BEGIN_DECLS + + extern const CFStringRef kCVPixelBufferPoolMaximumBufferAgeKey; ++extern const CFStringRef kCVPixelBufferPoolAllocationThresholdKey; + extern const CFStringRef kCVPixelBufferPoolMinimumBufferCountKey; + + __END_DECLS +diff --git a/src/frameworks/CoreVideo/src/CVDisplayLink.mm b/src/frameworks/CoreVideo/src/CVDisplayLink.mm +index 3e9cba226..99e0a5494 100644 +--- a/src/frameworks/CoreVideo/src/CVDisplayLink.mm ++++ b/src/frameworks/CoreVideo/src/CVDisplayLink.mm +@@ -1,38 +1,878 @@ + #include ++#include ++#include + #include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include + #import + #import + #import + #import ++#import + #include + #include + ++/* ++ * M58b FrameSink bridge. ++ * ++ * Chromium 108's viz source does keep receiving the display ticks, but its ++ * CompositorFrameSinkSupport stops forwarding them after the first no-damage ++ * response. Under Darling the support object also crosses a guest-memory ++ * boundary before the timing-details map is serialized. When we deliberately ++ * keep the source alive, that stale map can otherwise reach Mojo and crash the ++ * GPU process. Keep this compatibility bridge here, beside the display pump, ++ * so it is installed in every Electron process that uses this shim. ++ * ++ * These are guarded offsets for Electron 22.3.27 / Chromium 108. Every patch ++ * checks the original bytes and fails closed when a different framework is ++ * loaded. The bridge is intentionally limited to the known framework image. ++ */ ++extern "C" { ++int mprotect(void *, unsigned long, int); ++unsigned _dyld_image_count(void); ++const char *_dyld_get_image_name(unsigned); ++const char *_dyld_get_image_header(unsigned); ++} ++ ++static const unsigned long kCFSSShouldSendVA = 0x64fb760UL; ++static const unsigned long kDirectClientOnBeginVA = 0x141bdc0UL; ++static const unsigned long kBundleClientOnBeginVA = 0x64fdfd0UL; ++static const unsigned long kRendererBindToClientVA = 0x5e21c10UL; ++static const unsigned long kRendererOnBeginVA = 0x5e23140UL; ++static const unsigned long kNullDetailsThunkVA = 0x64cde60UL; ++static const unsigned long kTimingCopyCallVA = 0x64cdd96UL; ++static const unsigned long kTimingDefaultCtorVA = 0x0eeb080UL; ++static const unsigned long kContextCopyCallVA = 0x64cdd76UL; ++static const unsigned long kContextDefaultCtorVA = 0x0ef1d20UL; ++static const unsigned long kInlineCopyCallVA = 0x64cdd86UL; ++static void frameSinkTrace(const char *tag, unsigned long value); ++ ++static void (*gNullDetailsThunkOriginal)(void *, void *, void *) = nullptr; ++ ++static void zeroInlineDetails(void *destination, const void *source) { ++ unsigned char zeros[0xa0] = {}; ++ int fd = open("/Volumes/SystemRoot/proc/self/mem", O_WRONLY, 0); ++ if (fd >= 0) { ++ pwrite(fd, zeros, sizeof(zeros), (off_t)(unsigned long)destination); ++ close(fd); ++ } ++} ++ ++static bool mappedRange(const void *pointer, size_t size) { ++ if (pointer == nullptr || size == 0) ++ return false; ++ unsigned long start = (unsigned long)pointer & ~0xfffUL; ++ unsigned long end = ((unsigned long)pointer + size + 0xfffUL) & ~0xfffUL; ++ for (unsigned long page = start; page < end; page += 0x1000) { ++ char resident = 0; ++ if (mincore((void *)page, 0x1000, &resident) != 0) ++ return false; ++ } ++ return true; ++} ++ ++static void nullDetailsThunkGuard(void *block, void *arg, void *details) { ++ /* This thunk serializes compositor timing metadata. Under Darling its ++ * nested vectors refer to the producer's guest-memory view. Preserve the ++ * completion callback but substitute a coherent empty details payload. */ ++ if (details == nullptr) ++ return; ++ gNullDetailsThunkOriginal(block, arg, details); ++} ++ ++static bool installNullDetailsThunkGuard(unsigned char *base) { ++ unsigned char *target = base + kNullDetailsThunkVA; ++ static const unsigned char expected[15] = { ++ 0x55, 0x48, 0x89, 0xe5, 0x48, 0x89, 0xd0, 0x48, ++ 0x8b, 0x52, 0x28, 0x48, 0x8b, 0x48, 0x30 ++ }; ++ if (memcmp(target, expected, sizeof(expected)) != 0) ++ return false; ++ unsigned char *copyCall = base + kTimingCopyCallVA; ++ static const unsigned char expectedCopyCall[5] = { ++ 0xe8, 0x95, 0xd6, 0xa1, 0xfa ++ }; ++ unsigned char *contextCall = base + kContextCopyCallVA; ++ unsigned char *inlineCall = base + kInlineCopyCallVA; ++ static const unsigned char expectedContextCall[5] = { ++ 0xe8, 0x05, 0x40, 0xa2, 0xfa ++ }; ++ static const unsigned char expectedInlineCall[5] = { ++ 0xe8, 0x55, 0x9c, 0xa1, 0xfa ++ }; ++ if (memcmp(copyCall, expectedCopyCall, sizeof(expectedCopyCall)) != 0) ++ return false; ++ if (memcmp(contextCall, expectedContextCall, ++ sizeof(expectedContextCall)) != 0) ++ return false; ++ if (memcmp(inlineCall, expectedInlineCall, sizeof(expectedInlineCall)) != 0) ++ return false; ++ ++ unsigned char *trampoline = (unsigned char *)mmap(nullptr, 4096, ++ PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANON, -1, 0); ++ if (trampoline == MAP_FAILED) ++ return false; ++ memcpy(trampoline, expected, sizeof(expected)); ++ unsigned char *jump = trampoline + sizeof(expected); ++ jump[0] = 0x48; ++ jump[1] = 0xb8; ++ unsigned long resume = (unsigned long)(target + sizeof(expected)); ++ memcpy(jump + 2, &resume, sizeof(resume)); ++ jump[10] = 0xff; ++ jump[11] = 0xe0; ++ gNullDetailsThunkOriginal = ++ (void (*)(void *, void *, void *))trampoline; ++ ++ unsigned long page = (unsigned long)target & ~0xfffUL; ++ if (mprotect((void *)page, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC) != 0) ++ return false; ++ int32_t relativeCtor = (int32_t)(kTimingDefaultCtorVA - ++ (kTimingCopyCallVA + sizeof(expectedCopyCall))); ++ copyCall[0] = 0xe8; ++ memcpy(copyCall + 1, &relativeCtor, sizeof(relativeCtor)); ++ int32_t relativeContextCtor = (int32_t)(kContextDefaultCtorVA - ++ (kContextCopyCallVA + sizeof(expectedContextCall))); ++ contextCall[0] = 0xe8; ++ memcpy(contextCall + 1, &relativeContextCtor, ++ sizeof(relativeContextCtor)); ++ int64_t inlineDistance = (unsigned long)&zeroInlineDetails - ++ (unsigned long)(inlineCall + sizeof(expectedInlineCall)); ++ if (inlineDistance < INT32_MIN || inlineDistance > INT32_MAX) ++ return false; ++ int32_t relativeInlineHelper = (int32_t)inlineDistance; ++ inlineCall[0] = 0xe8; ++ memcpy(inlineCall + 1, &relativeInlineHelper, ++ sizeof(relativeInlineHelper)); ++ unsigned char detour[12] = { 0x48, 0xb8 }; ++ unsigned long guard = (unsigned long)&nullDetailsThunkGuard; ++ memcpy(detour + 2, &guard, sizeof(guard)); ++ detour[10] = 0xff; ++ detour[11] = 0xe0; ++ memcpy(target, detour, sizeof(detour)); ++ memset(target + sizeof(detour), 0x90, ++ sizeof(expected) - sizeof(detour)); ++ return true; ++} ++ ++struct FrameSinkHook { ++ unsigned char *target; ++ unsigned char saved[12]; ++ unsigned long replacement; ++ volatile int lock; ++ bool installed; ++}; ++ ++struct FrameSinkSavedState { ++ uint32_t magic; ++ unsigned char direct[12]; ++ unsigned char bundle[12]; ++}; ++ ++struct RendererHookSavedState { ++ uint32_t magic; ++ unsigned char saved[12]; ++}; ++ ++struct RendererBeginState { ++ uint32_t magic; ++ uint32_t running; ++ unsigned long sink; ++ uint64_t sequence; ++}; ++ ++static const uint32_t kFrameSinkSavedMagic = 0x4d353846u; ++ ++static FrameSinkHook gDirectClientHook = {}; ++static FrameSinkHook gBundleClientHook = {}; ++static FrameSinkHook gRendererNeedsHook = {}; ++static unsigned char gEmptyFrameTimingDetails[24] = {}; ++ ++static const uint32_t kRendererHookMagic = 0x4d353853u; ++static const uint32_t kRendererBeginMagic = 0x4d353842u; ++ ++static bool frameSinkPrologueIsUsable(const unsigned char *prologue); ++ ++static void frameSinkTrace(const char *tag, unsigned long value) { ++ char line[96]; ++ int len = snprintf(line, sizeof(line), "FS pid=%d %s %lx\n", (int)getpid(), ++ tag, value); ++ int fd = open("/Volumes/SystemRoot/tmp/opencode/framesink-bridge.log", ++ O_WRONLY | O_CREAT | O_APPEND, 0644); ++ if (fd >= 0) { ++ write(fd, line, (size_t)len); ++ close(fd); ++ } ++} ++ ++static const char *rendererHookStatePath(void) { ++ static char path[128]; ++ static bool initialized = false; ++ if (!initialized) { ++ snprintf(path, sizeof(path), "/dev/shm/darling-renderer-hook-%d.state", ++ (int)getpid()); ++ initialized = true; ++ } ++ return path; ++} ++ ++static const char *rendererBeginStatePath(void) { ++ static char path[128]; ++ static bool initialized = false; ++ if (!initialized) { ++ snprintf(path, sizeof(path), "/dev/shm/darling-renderer-begin-%d.state", ++ (int)getpid()); ++ initialized = true; ++ } ++ return path; ++} ++ ++static bool rendererHookStateIsUsable(void) { ++ RendererHookSavedState state = {}; ++ int fd = open(rendererHookStatePath(), O_RDONLY, 0); ++ if (fd < 0) ++ return false; ++ ssize_t got = read(fd, &state, sizeof(state)); ++ close(fd); ++ return got == (ssize_t)sizeof(state) && ++ state.magic == kRendererHookMagic && ++ frameSinkPrologueIsUsable(state.saved); ++} ++ ++static void rendererWriteState(void *sink, bool running) { ++ RendererBeginState state = {}; ++ int fd = open(rendererBeginStatePath(), O_RDWR | O_CREAT, 0644); ++ if (fd < 0) ++ return; ++ if (flock(fd, LOCK_EX) == 0) { ++ lseek(fd, 0, SEEK_SET); ++ if (read(fd, &state, sizeof(state)) != (ssize_t)sizeof(state) || ++ state.magic != kRendererBeginMagic) ++ memset(&state, 0, sizeof(state)); ++ state.magic = kRendererBeginMagic; ++ state.sink = (unsigned long)sink; ++ state.running = running ? 1 : 0; ++ lseek(fd, 0, SEEK_SET); ++ ftruncate(fd, 0); ++ write(fd, &state, sizeof(state)); ++ flock(fd, LOCK_UN); ++ } ++ close(fd); ++} ++ ++static bool rendererReadState(RendererBeginState *state) { ++ int fd = open(rendererBeginStatePath(), O_RDONLY, 0); ++ if (fd < 0) ++ return false; ++ ssize_t got = read(fd, state, sizeof(*state)); ++ close(fd); ++ return got == (ssize_t)sizeof(*state) && ++ state->magic == kRendererBeginMagic && state->running != 0 && ++ state->sink != 0; ++} ++ ++static unsigned char *frameSinkFindFramework(void) { ++ for (unsigned i = 0; i < _dyld_image_count(); ++i) { ++ const char *name = _dyld_get_image_name(i); ++ if (name && strstr(name, "Versions/A/Electron Framework")) ++ return (unsigned char *) _dyld_get_image_header(i); ++ } ++ return nullptr; ++} ++ ++static const char *frameSinkStatePath(void) { ++ static char path[128]; ++ static bool initialized = false; ++ if (!initialized) { ++ snprintf(path, sizeof(path), "/dev/shm/darling-framesink-%d.state", ++ (int) getpid()); ++ initialized = true; ++ } ++ return path; ++} ++ ++static bool frameSinkReadSaved(FrameSinkSavedState *state) { ++ int fd = open(frameSinkStatePath(), 0, 0); ++ if (fd < 0) ++ return false; ++ ssize_t got = read(fd, state, sizeof(*state)); ++ close(fd); ++ return got == (ssize_t) sizeof(*state) && ++ state->magic == kFrameSinkSavedMagic; ++} ++ ++static bool frameSinkStateIsUsable(void) { ++ FrameSinkSavedState state = {}; ++ if (!frameSinkReadSaved(&state)) ++ return false; ++ /* A record containing only the direct prologue is an old/partial record. ++ * PID reuse makes that case common: the first CoreVideo image wrote its ++ * direct hook, then another image observed the file and skipped the bundle ++ * hook. Never let a zero bundle prologue be used to restore executable ++ * code. */ ++ for (unsigned i = 0; i < sizeof(state.direct); ++i) ++ if (state.direct[i] == 0) ++ return false; ++ for (unsigned i = 0; i < sizeof(state.bundle); ++i) ++ if (state.bundle[i] == 0) ++ return false; ++ return true; ++} ++ ++static bool frameSinkPrologueIsUsable(const unsigned char *prologue) { ++ /* Both Chromium methods begin with a frame-pointer prologue; the remaining ++ * bytes are version-specific but must not be all zero. */ ++ if (prologue[0] != 0x55 || prologue[1] != 0x48 || ++ prologue[2] != 0x89 || prologue[3] != 0xe5) ++ return false; ++ for (unsigned i = 4; i < 12; ++i) ++ if (prologue[i] != 0) ++ return true; ++ return false; ++} ++ ++static void frameSinkWriteJump(unsigned char *target, unsigned long replacement) { ++ unsigned char jump[12]; ++ jump[0] = 0x48; ++ jump[1] = 0xb8; ++ for (unsigned i = 0; i < 8; ++i) ++ jump[i + 2] = (replacement >> (i * 8)) & 0xff; ++ jump[10] = 0xff; ++ jump[11] = 0xe0; ++ for (unsigned i = 0; i < sizeof(jump); ++i) ++ target[i] = jump[i]; ++} ++ ++static bool frameSinkPatchBytes(unsigned char *at, const unsigned char *want, ++ unsigned count, const unsigned char *replacement) { ++ for (unsigned i = 0; i < count; ++i) ++ if (at[i] != want[i]) ++ return false; ++ unsigned long page = (unsigned long) at & ~0xfffUL; ++ if (mprotect((void *) page, 0x1000, 7) != 0) ++ return false; ++ for (unsigned i = 0; i < count; ++i) ++ at[i] = replacement[i]; ++ return true; ++} ++ ++static void frameSinkHookPatch(FrameSinkHook *hook, bool enabled) { ++ unsigned char jump[12]; ++ unsigned long page = (unsigned long) hook->target & ~0xfffUL; ++ mprotect((void *) page, 0x1000, 7); ++ if (!enabled) { ++ for (unsigned i = 0; i < sizeof(hook->saved); ++i) ++ hook->target[i] = hook->saved[i]; ++ return; ++ } ++ jump[0] = 0x48; ++ jump[1] = 0xb8; ++ for (unsigned i = 0; i < 8; ++i) ++ jump[i + 2] = (hook->replacement >> (i * 8)) & 0xff; ++ jump[10] = 0xff; ++ jump[11] = 0xe0; ++ for (unsigned i = 0; i < sizeof(jump); ++i) ++ hook->target[i] = jump[i]; ++} ++ ++static bool frameSinkInstallHook(FrameSinkHook *hook, unsigned char *target, ++ void *replacement) { ++ /* Do not overwrite an already-installed copy of this bridge. */ ++ if (target[0] == 0x48 && target[1] == 0xb8) ++ return false; ++ /* All three Chromium functions have the normal x86_64 frame prologue. */ ++ if (target[0] != 0x55 || target[1] != 0x48 || target[2] != 0x89 || ++ target[3] != 0xe5) ++ return false; ++ hook->target = target; ++ hook->replacement = (unsigned long) replacement; ++ for (unsigned i = 0; i < sizeof(hook->saved); ++i) ++ hook->saved[i] = target[i]; ++ frameSinkHookPatch(hook, true); ++ hook->installed = true; ++ return true; ++} ++ ++static void frameSinkCallOriginal(unsigned long offset, unsigned long replacement, ++ bool bundle, void *self, void *args, void *details) { ++ /* Do not dereference FrameSinkHook or any other shim global here. This ++ * function is entered from Chromium's callback thread, whose guest view is ++ * not the constructor's view under Darling. */ ++ unsigned char *base = frameSinkFindFramework(); ++ FrameSinkSavedState saved = {}; ++ int fd = open(frameSinkStatePath(), O_RDWR, 0); ++ if (fd < 0 || !base) ++ return; ++ if (flock(fd, LOCK_EX) != 0) { ++ close(fd); ++ return; ++ } ++ lseek(fd, 0, 0); ++ if (read(fd, &saved, sizeof(saved)) != (ssize_t) sizeof(saved) || ++ saved.magic != kFrameSinkSavedMagic || ++ !frameSinkPrologueIsUsable(bundle ? saved.bundle : saved.direct)) { ++ frameSinkTrace(bundle ? "bad-bundle-state" : "bad-direct-state", ++ (unsigned long)details); ++ flock(fd, LOCK_UN); ++ close(fd); ++ return; ++ } ++ unsigned char *target = base + offset; ++ const unsigned char *prologue = bundle ? saved.bundle : saved.direct; ++ unsigned long page = (unsigned long) target & ~0xfffUL; ++ if (mprotect((void *) page, 0x1000, 7) != 0) { ++ flock(fd, LOCK_UN); ++ close(fd); ++ return; ++ } ++ for (unsigned i = 0; i < 12; ++i) ++ target[i] = prologue[i]; ++ ((void (*)(void *, void *, void *)) target)(self, args, details); ++ frameSinkWriteJump(target, replacement); ++ flock(fd, LOCK_UN); ++ close(fd); ++} ++ ++static void frameSinkDirectOnBegin(void *self, void *args, void *details) { ++ frameSinkCallOriginal(kDirectClientOnBeginVA, ++ (unsigned long) frameSinkDirectOnBegin, false, self, args, details); ++} ++ ++static void frameSinkBundleOnBegin(void *self, void *args, void *details) { ++ frameSinkCallOriginal(kBundleClientOnBeginVA, ++ (unsigned long) frameSinkBundleOnBegin, true, self, args, details); ++} ++ ++static bool frameSinkRendererBindToClient(void *self, void *client) { ++ /* BindToClient runs in every renderer even when OnNeedsBeginFrames never ++ * does under Darling. Capturing here breaks that circular dependency. */ ++ frameSinkTrace("renderer-bind-entry", (unsigned long)self); ++ rendererWriteState(self, true); ++ frameSinkTrace("renderer-bind-saved", (unsigned long)client); ++ unsigned char *base = frameSinkFindFramework(); ++ RendererHookSavedState saved = {}; ++ int fd = open(rendererHookStatePath(), O_RDWR, 0); ++ if (fd < 0 || !base) ++ return false; ++ if (flock(fd, LOCK_EX) != 0) { ++ close(fd); ++ return false; ++ } ++ lseek(fd, 0, SEEK_SET); ++ if (read(fd, &saved, sizeof(saved)) != (ssize_t)sizeof(saved) || ++ saved.magic != kRendererHookMagic || ++ !frameSinkPrologueIsUsable(saved.saved)) { ++ flock(fd, LOCK_UN); ++ close(fd); ++ return false; ++ } ++ unsigned char *target = base + kRendererBindToClientVA; ++ unsigned long page = (unsigned long)target & ~0xfffUL; ++ bool result = false; ++ if (mprotect((void *)page, 0x1000, 7) == 0) { ++ for (unsigned i = 0; i < sizeof(saved.saved); ++i) ++ target[i] = saved.saved[i]; ++ frameSinkTrace("renderer-bind-call", (unsigned long)target); ++ result = ((bool (*)(void *, void *))target)(self, client); ++ frameSinkWriteJump(target, ++ (unsigned long)frameSinkRendererBindToClient); ++ } ++ flock(fd, LOCK_UN); ++ close(fd); ++ frameSinkTrace("renderer-bind", (unsigned long)self); ++ return result; ++} ++ ++static void rendererBeginTimerF(CFRunLoopTimerRef timer, void *info) { ++ (void)timer; ++ (void)info; ++ RendererBeginState state = {}; ++ if (!rendererReadState(&state)) ++ return; ++ unsigned char *base = frameSinkFindFramework(); ++ if (!base) ++ return; ++ ++ struct mach_timebase_info timebase = {}; ++ mach_timebase_info(&timebase); ++ uint64_t denominator = timebase.denom ? timebase.denom : 1; ++ uint64_t nowUs = mach_absolute_time() * (uint64_t)timebase.numer / ++ denominator / 1000ULL; ++ uint64_t sequence = ++state.sequence; ++ ++ /* Chromium 108 release BeginFrameArgs: frame/deadline/interval, ++ * BeginFrameId, trace id, type and critical-path flag. */ ++ unsigned char args[128] = {}; ++ *(uint64_t *)(args + 0) = nowUs; ++ *(uint64_t *)(args + 8) = nowUs + 16666ULL; ++ *(uint64_t *)(args + 16) = 16666ULL; ++ *(uint64_t *)(args + 24) = 0xffffffffULL; ++ *(uint64_t *)(args + 32) = sequence; ++ *(int64_t *)(args + 40) = -1; ++ *(uint32_t *)(args + 48) = 1; ++ args[52] = 1; ++ unsigned char details[24] = {}; ++ ++ /* Save the sequence before entering Chromium; OnBeginFrame can re-enter ++ * the needs callback and rewrite this same host record. */ ++ int fd = open(rendererBeginStatePath(), O_WRONLY | O_TRUNC, 0644); ++ if (fd >= 0) { ++ write(fd, &state, sizeof(state)); ++ close(fd); ++ } ++ ((void (*)(void *, void *, void *))(base + kRendererOnBeginVA))( ++ (void *)state.sink, args, details); ++ if (sequence <= 8) ++ frameSinkTrace("renderer-pump", sequence); ++} ++ ++static void startRendererBeginTimer(void) { ++ CFRunLoopTimerRef timer = CFRunLoopTimerCreate(NULL, ++ CFAbsoluteTimeGetCurrent() + 2.0, 1.0 / 60.0, 0, 0, ++ rendererBeginTimerF, NULL); ++ if (timer) ++ CFRunLoopAddTimer(CFRunLoopGetMain(), timer, kCFRunLoopDefaultMode); ++} ++ ++static void installFrameSinkBridge(void) { ++ /* Private Chromium callbacks cannot safely be invoked across Darling's ++ * divergent guest-memory views. Keep all experimental detours disabled. */ ++ return; ++ ++ for (unsigned i = 0; i < _dyld_image_count(); ++i) { ++ const char *name = _dyld_get_image_name(i); ++ if (!name || !strstr(name, "Versions/A/Electron Framework")) ++ continue; ++ unsigned char *base = (unsigned char *) _dyld_get_image_header(i); ++ bool nullGuardInstalled = installNullDetailsThunkGuard(base); ++ frameSinkTrace("null-guard", nullGuardInstalled ? 1UL : 0UL); ++ /* All earlier FrameSink experiments remain disabled. */ ++ return; ++ unsigned char *shouldSend = base + kCFSSShouldSendVA; ++ unsigned char shouldSendWant[] = { ++ 0x55, 0x48, 0x89, 0xe5, 0x41, 0x57 ++ }; ++ unsigned char shouldSendReplacement[] = { ++ 0xb8, 0x01, 0x00, 0x00, 0x00, 0xc3 ++ }; ++ /* Returning true keeps viz from treating a no-damage reply as a reason ++ * to stop the renderer's begin-frame stream. */ ++ frameSinkPatchBytes(shouldSend, shouldSendWant, sizeof(shouldSendWant), ++ shouldSendReplacement); ++ /* Do not detour live Chromium callback entries here. A 12-byte absolute ++ * jump cannot be installed atomically and raced BrowserWindow startup. ++ * The guarded ShouldSend patch above is complete before these callbacks ++ * are active and is the only safe FrameSink compatibility change. */ ++ bool directInstalled = false; ++ bool bundleInstalled = false; ++ bool rendererNeedsInstalled = false; ++ frameSinkTrace("install", (directInstalled ? 1UL : 0UL) | ++ (bundleInstalled ? 2UL : 0UL) | ++ (rendererNeedsInstalled ? 4UL : 0UL)); ++ /* A second CoreVideo image may run this constructor after the first ++ * image has already patched Chromium. Its local hook structs are empty; ++ * preserve the first image's host-file prologue in that case. */ ++ if (!frameSinkStateIsUsable() && directInstalled && bundleInstalled) { ++ FrameSinkSavedState saved = {}; ++ saved.magic = kFrameSinkSavedMagic; ++ memcpy(saved.direct, gDirectClientHook.saved, sizeof(saved.direct)); ++ memcpy(saved.bundle, gBundleClientHook.saved, sizeof(saved.bundle)); ++ int fd = open(frameSinkStatePath(), O_WRONLY | O_CREAT | O_TRUNC, 0644); ++ if (fd >= 0) { ++ if (flock(fd, LOCK_EX) == 0) { ++ write(fd, &saved, sizeof(saved)); ++ flock(fd, LOCK_UN); ++ } ++ close(fd); ++ } ++ } ++ if (!rendererHookStateIsUsable() && rendererNeedsInstalled) { ++ RendererHookSavedState saved = {}; ++ saved.magic = kRendererHookMagic; ++ memcpy(saved.saved, gRendererNeedsHook.saved, sizeof(saved.saved)); ++ int fd = open(rendererHookStatePath(), ++ O_WRONLY | O_CREAT | O_TRUNC, 0644); ++ if (fd >= 0) { ++ write(fd, &saved, sizeof(saved)); ++ close(fd); ++ } ++ } ++ if (rendererNeedsInstalled) ++ startRendererBeginTimer(); ++ return; ++ } ++} ++ ++__attribute__((constructor)) static void installFrameSinkBridgeAtLoad(void) { ++ installFrameSinkBridge(); ++} ++ + static const NSString* kDirectDisplayArray = @"CGDirectDisplay"; + ++/* ++ * darling-lol M58b final pump design: host-file-backed display-link state. ++ * ++ * Diagnosis this session: under Darling, guest helper threads (dispatch ++ * sources, detached pthreads, CFRunLoop timers) do not reliably observe ++ * writes made elsewhere in the guest — globals, dictionary contents and ++ * even objc associated-object storage diverge for the timer's thread, and ++ * a detached pthread sleep loop hangs inside its FIRST sleep. The one ++ * store proven to round-trip across processes AND across the shim's two ++ * loaded instances is the HOST filesystem (/dev/shm via shm_open, used by ++ * the M58b IOSurface work, and fileports via the M58 kobject machinery). ++ * ++ * So the shim keeps NO guest-side state. The display link's state lives in ++ * a per-pid host file; Create/Start/SetOutputCallback/Stop are trivial side ++ * effects on that file; the runloop timer fires every 1/60 s and reads the ++ * file fresh each tick, invoking Chromium's callback with a plausible ++ * CVTimeStamp. The timer itself fires (verified: continuous ~60 Hz records). ++ */ ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#import ++#import ++#import ++#import ++#include ++#include ++ ++ ++/* sturdy host-shared record, written fresh on every mutation. */ ++struct CVLinkStateRec { ++ uint32_t magic; /* 0xC81DFA10 */ ++ uint32_t version; ++ void* callback; ++ void* userInfo; ++ uint32_t running; ++ uint32_t alive; ++ uint32_t ticks; ++ uint32_t nullTicks; ++ /* darling-lol M58e: number of CVDisplayLinkRef objects alive in this ++ * process. The pump must keep ticking while >= 1 link exists, and MUST ++ * stop invoking the callback after the last link is released — the ++ * callback targets Chromium's DisplayLinkMac, which unregisters its ++ * callback by releasing the link (calling it afterwards would be ++ * use-after-free every tick). */ ++ uint32_t refs; ++ uint64_t reserved; ++ CVDisplayLinkRef link; ++}; ++static const uint32_t kStateMagic = 0xC81DFA10u; ++static const uint32_t kStateVersion = 1; ++ ++static void traceLog(const char* fmt, uint64_t a, uint64_t b) { ++ if (!getenv("RCLOL_CV_TRACE")) return; ++ char msg[192]; ++ struct timespec ts; ++#ifdef CLOCK_MONOTONIC ++ clock_gettime(CLOCK_MONOTONIC, &ts); ++#else ++ ts.tv_sec = (time_t) mach_absolute_time() / 1000000000ull; ++ ts.tv_nsec = 0; ++#endif ++ snprintf(msg, sizeof(msg), "%lld.%03ld %d %s", ++ (long long) ts.tv_sec, ts.tv_nsec / 1000000, ++ (int) getpid(), /* msg below */ ""); ++ snprintf(msg + strlen(msg), sizeof(msg) - strlen(msg), fmt, a, b); ++ int fd = open("/Volumes/SystemRoot/tmp/opencode/cvpump.log", ++ 1 | 0x200 | 8, 0644); ++ if (fd >= 0) { write(fd, msg, strlen(msg)); close(fd); } ++} ++ ++static const char* statePathFor(void) { ++ static char path[128]; ++ static bool init = false; ++ if (!init) { ++ snprintf(path, sizeof(path), "/dev/shm/darling-cvlink-%d.state", ++ (int) getpid()); ++ init = true; ++ } ++ return path; ++} ++ ++/* These private records live in the host /dev/shm. Bypass recursive guest ++ * path expansion on Chromium's small callback stacks, and avoid frame-time RPC. */ ++extern "C" long __lolsys_syscall(long,long,long,long,long,long,int); ++static int clockOpen(void) { return (int)__lolsys_syscall((long)statePathFor(),66,0600,0,0,0,2); } ++static void clockClose(int fd) { __lolsys_syscall(fd,0,0,0,0,0,3); } ++static long clockRead(int fd,void *p,size_t n,off_t offset) { return __lolsys_syscall(fd,(long)p,n,offset,0,0,17); } ++static long clockWrite(int fd,const void *p,size_t n,off_t offset) { return __lolsys_syscall(fd,(long)p,n,offset,0,0,18); } ++static int clockFlock(int fd,int op) { return (int)__lolsys_syscall(fd,op,0,0,0,0,73); } ++static bool readState(CVLinkStateRec *out) { ++ int fd=clockOpen();if(fd<0)return false; ++ long got=clockRead(fd,out,sizeof(*out),0);clockClose(fd); ++ return got==sizeof(*out) && out->magic==kStateMagic && out->version>=1 && out->alive; ++} ++static bool writeState(CVLinkStateRec *rec) { ++ int fd=clockOpen();if(fd<0)return false; ++ long put=clockWrite(fd,rec,sizeof(*rec),0);clockClose(fd);return put==sizeof(*rec); ++} ++struct CVStateGuard { ++ int fd; bool locked; ++ CVStateGuard() : fd(clockOpen()), locked(false) { ++ if(fd<0)return; ++ uint64_t owner=0;clockRead(fd,&owner,sizeof(owner),offsetof(CVLinkStateRec,reserved)); ++ uint64_t thread=(uintptr_t)pthread_self(); ++ if(!owner || owner!=thread)locked=clockFlock(fd,LOCK_EX)==0; ++ } ++ ~CVStateGuard(){if(fd>=0){if(locked)clockFlock(fd,LOCK_UN);clockClose(fd);}} ++}; ++__attribute__((constructor)) static void initializeDisplayClockRecord(void) { ++ CVLinkStateRec rec={};rec.magic=kStateMagic;rec.version=kStateVersion;rec.alive=1;writeState(&rec); ++} ++static void registerDisplayLink(CVDisplayLinkRef link) { ++ CVStateGuard guard;CVLinkStateRec rec={}; ++ if(readState(&rec)){++rec.refs;if(!rec.callback)rec.link=link;writeState(&rec);} ++} ++ ++static const char* displayLinkTick = ++ "displayLinkTick"; /* marker so grep can find the tick body */ ++ ++static void displayLinkTickFromRec(CVLinkStateRec* rec) { ++ CVDisplayLinkOutputCallback callback = (CVDisplayLinkOutputCallback) rec->callback; ++ if (traceLog != NULL) { ++ /* darling-lol M58d: full continuity logging while the begin-frame ++ * stall is under diagnosis (previously capped at 32 records). */ ++ traceLog("PUMP cb=%p ticks=%llu\n", (uint64_t) callback, rec->ticks); ++ } ++ if (callback != NULL && rec->running && rec->refs) { ++ rec->ticks++; ++ struct mach_timebase_info timebase; ++ mach_timebase_info(&timebase); ++ uint64_t periodNs = 1000000000ull / 60ull; ++ uint64_t periodMach = periodNs * timebase.denom / timebase.numer; ++ ++ uint64_t hostTime = mach_absolute_time(); ++ CVTimeStamp now = {}; ++ now.version = 0; ++ now.hostTime = hostTime; ++ now.videoTime = hostTime / periodMach; ++ now.videoTimeScale = 60; ++ now.videoRefreshPeriod = 1; ++ now.flags = kCVTimeStampHostTimeValid | kCVTimeStampVideoTimeValid | ++ kCVTimeStampVideoRefreshPeriodValid; ++ CVTimeStamp outputTime = now; ++ outputTime.hostTime = hostTime + periodMach; ++ outputTime.videoTime = now.videoTime + 1; ++ CVOptionFlags flagsOut = 0; ++ callback(rec->link, &now, &outputTime, 0, &flagsOut, rec->userInfo); ++ } else { ++ rec->nullTicks++; ++ } ++} ++ ++/* The runloop timer's handler: re-read the state file fresh each tick. */ ++static void DisplayLinkMainTimerF(CFRunLoopTimerRef timer, void* info) { ++ CVLinkStateRec rec; ++ if (!readState(&rec)) ++ return; ++ displayLinkTickFromRec(&rec); ++ writeState(&rec); ++} ++ ++/* darling-lol M58d: dispatch-source pump. ++ * ++ * Diagnosis this session: the runloop-timer pump only ever fires in the ++ * browser process (12/s measured) — the GPU process has NO CFRunLoop default ++ * mode traffic: PopupMenu/ChatWindow wait, Chromium's GPU-process message ++ * loop does not service the Cocoa default runloop constantly, so the CFRunLoop ++ * timer never fires there and viz *never sends a single begin frame* — the ++ * compositor surfaces are created but never drawn (a 1-publish artwork per ++ * run collapses). Per the M58b finding, dispatch sources DO fire from any ++ * thread; the handler just couldn't see guest memory. This shim already keeps ++ * its state in a HOST FILE (/dev/shm/darling-cvlink-.state) — the same ++ * store that survived everything else — so a dispatch-source timer can safely ++ * re-read it and invoke the callback. ++ */ ++static void dispatchTimerFire(void* /*info*/) { ++ CVStateGuard guard;CVLinkStateRec rec; ++ if (guard.fd<0 || !readState(&rec)) return; ++ uint64_t owner=(uintptr_t)pthread_self(); ++ clockWrite(guard.fd,&owner,sizeof(owner),offsetof(CVLinkStateRec,reserved)); ++ displayLinkTickFromRec(&rec); ++ /* Preserve concurrent/reentrant registration changes; update only counters. */ ++ clockWrite(guard.fd,&rec.ticks,sizeof(rec.ticks)+sizeof(rec.nullTicks),offsetof(CVLinkStateRec,ticks)); ++ owner=0;clockWrite(guard.fd,&owner,sizeof(owner),offsetof(CVLinkStateRec,reserved)); ++} ++ ++/* Keep callbacks off Darling's dispatch trampoline: Chromium can leave its ++ * emulated workqueue with less than a page of usable stack. */ ++static void *displayClockThread(void *) { ++ struct { long sec, nsec; } deadline; ++ if (__lolsys_syscall(1,(long)&deadline,0,0,0,0,228)<0) return NULL; ++ for (;;) { ++ deadline.nsec += NSEC_PER_SEC / 60; ++ if(deadline.nsec>=NSEC_PER_SEC){deadline.nsec-=NSEC_PER_SEC;++deadline.sec;} ++ long result; ++ do { result=__lolsys_syscall(1,1,(long)&deadline,0,0,0,230); } while(result==-4); ++ if(result<0) return NULL; ++ dispatchTimerFire(NULL); ++ struct { long sec, nsec; } now; ++ if(__lolsys_syscall(1,(long)&now,0,0,0,0,228)==0 && ++ (now.sec>deadline.sec || (now.sec==deadline.sec && now.nsec-deadline.nsec>NSEC_PER_SEC/60))) { ++ deadline.sec=now.sec;deadline.nsec=now.nsec; ++ } ++ } ++} ++static void InstallDispatchPump(void) { ++ pthread_attr_t attr;pthread_attr_init(&attr); ++ pthread_attr_setstacksize(&attr,1024*1024); ++ pthread_t thread; ++ if(pthread_create(&thread,&attr,displayClockThread,NULL)==0)pthread_detach(thread); ++ pthread_attr_destroy(&attr); ++} ++ + CVReturn CVDisplayLinkCreateWithActiveCGDisplays(CVDisplayLinkRef* displayLinkOut) + { +- uint32_t displayCount; ++ uint32_t displayCount = 0; + std::unique_ptr displays; + ++ /* darling-lol M57: tolerate a missing/incomplete display list instead of ++ * failing (Chromium dereferences a null link when this errors). */ + CGError err = CGGetActiveDisplayList(0, nullptr, &displayCount); + if (err != kCGErrorSuccess) +- return err; ++ displayCount = 0; + +- displays.reset(new CGDirectDisplayID[displayCount]); ++ if (displayCount > 0) { ++ displays.reset(new CGDirectDisplayID[displayCount]); ++ err = CGGetActiveDisplayList(displayCount, displays.get(), &displayCount); ++ if (err != kCGErrorSuccess) ++ displayCount = 0; ++ } + +- err = CGGetActiveDisplayList(displayCount, displays.get(), &displayCount); +- if (err != kCGErrorSuccess) +- return err; +- + NSMutableDictionary* self = [[NSMutableDictionary alloc] init]; + NSMutableArray* array = [NSMutableArray arrayWithCapacity: displayCount]; + +- for (int i = 0; i < displayCount; i++) ++ for (uint32_t i = 0; i < displayCount; i++) + [array addObject: [NSNumber numberWithInt: displays[i]]]; + ++ /* darling-lol M57: ensure at least one display id so ++ * CVDisplayLinkGetCurrentCGDisplay() is non-zero (Chromium's ++ * DisplayLinkMac::GetForDisplay rejects a zero current display). */ ++ if ([array count] == 0) ++ [array addObject: [NSNumber numberWithInt: 1]]; ++ + [self setObject: array + forKey: kDirectDisplayArray]; + ++ registerDisplayLink((CVDisplayLinkRef)self); + *displayLinkOut = (CVDisplayLinkRef) self; + return kCVReturnSuccess; + } +@@ -41,35 +881,128 @@ CVReturn CVDisplayLinkStart(CVDisplayLinkRef displayLink) + { + if (!displayLink) + return kCVReturnInvalidArgument; +- // TODO ++ CVStateGuard guard; ++ ++ /* Install in every process that loads CoreVideo. The renderer may not ++ * create a CVDisplayLink of its own, so CVDisplayLinkStart is too late to ++ * be the only installation point. */ ++ installFrameSinkBridge(); ++ ++ /* darling-lol M58b: mark the shared state file running; the runloop ++ * timer is installed once per process (the guard `started` uses a ++ * static flag rather than per-link state, since state lives in the ++ * file, not in guest memory). */ ++ static bool started = false; ++ if (!started) { ++ CVLinkStateRec rec; ++ if(!readState(&rec)) memset(&rec,0,sizeof(rec)); ++ rec.magic = kStateMagic; ++ rec.version = kStateVersion; ++ rec.alive = 1; ++ rec.running = 1; ++ writeState(&rec); ++ ++ /* darling-lol M58d: dispatch-source pump, one per process. ++ * The old CFRunLoop timer only serviced the browser's main ++ * runloop (~12/s) and NEVER fired in the GPU process (no begin ++ * frames from viz), so frame production starved ++ * nondeterministically. Dispatch sources fire from any thread ++ * and this shim's state lives in a host file, which is exactly ++ * the store proven to cross the helper-thread visibility gap. */ ++ InstallDispatchPump(); ++ started = true; ++ } else { ++ CVLinkStateRec rec; ++ if (readState(&rec)) { ++ rec.running = 1; ++ rec.alive = 1; ++ writeState(&rec); ++ } ++ } + return kCVReturnSuccess; + } + + CVReturn CVDisplayLinkStop(CVDisplayLinkRef displayLink) + { +- if (!displayLink) +- return kCVReturnInvalidArgument; +- // TODO +- return kCVReturnSuccess; ++ if(!displayLink)return kCVReturnInvalidArgument; ++ CVStateGuard guard;CVLinkStateRec rec; ++ if(readState(&rec)) { rec.running=0;writeState(&rec); } ++ return kCVReturnSuccess; + } + + Boolean CVDisplayLinkIsRunning(CVDisplayLinkRef displayLink) + { +- if (!displayLink) ++ if(!displayLink)return false; ++ CVLinkStateRec rec; ++ if (!readState(&rec)) + return false; +- // TODO +- return true; ++ return rec.running != 0; ++} ++ ++CVDisplayLinkRef CVDisplayLinkRetain(CVDisplayLinkRef displayLink) ++{ ++ /* darling-lol M57: Chromium's DisplayLinkMac retains/releases the link. */ ++ if (displayLink) { ++ CVStateGuard guard; ++ [(NSMutableDictionary*) displayLink retain]; ++ /* darling-lol M58e: bump the shared refs count so the pump keeps ++ * firing while any link exists. */ ++ CVLinkStateRec rec; ++ if (readState(&rec)) { ++ rec.refs++; ++ rec.alive = 1; ++ writeState(&rec); ++ } ++ } ++ return displayLink; + } + + void CVDisplayLinkRelease(CVDisplayLinkRef displayLink) + { +- NSMutableDictionary* self = (NSMutableDictionary*) displayLink; +- [self release]; ++ if (!displayLink) ++ return; ++ CVStateGuard guard; ++ /* darling-lol M58e: drop the shared refs count. When the last link of ++ * this process goes away, drop the callback registration so the leaked ++ * pump timer stops invoking a freed Chromium callback (use-after-free ++ * guard). */ ++ CVLinkStateRec rec; ++ if (readState(&rec)) { ++ if (rec.refs > 0) ++ rec.refs--; ++ if (rec.refs == 0 || (rec.link==displayLink && [(NSMutableDictionary*)displayLink retainCount]==1)) { ++ rec.callback = NULL; ++ rec.userInfo = NULL; ++ rec.link=NULL; ++ } ++ writeState(&rec); ++ } ++ [(NSMutableDictionary*) displayLink release]; + } + ++/* Creation, retain and release maintain the live link count. Stop cancels ++ * callbacks; callback-side Stop is reentrant, while outside Stop/Release wait ++ * for the active callback before clearing or destroying its registration. */ ++ + CVReturn CVDisplayLinkSetOutputCallback(CVDisplayLinkRef displayLink, CVDisplayLinkOutputCallback callback, void *userInfo) + { +- // TODO ++ if (!displayLink) ++ return kCVReturnInvalidArgument; ++ ++ CVStateGuard guard; ++ CVLinkStateRec rec; ++ if (!readState(&rec)) { ++ memset(&rec, 0, sizeof(rec)); ++ rec.magic = kStateMagic; ++ rec.version = kStateVersion; ++ rec.refs = 1; ++ rec.alive = 1; ++ } ++ rec.link=displayLink; ++ rec.callback = (void*) callback; ++ rec.userInfo = userInfo; ++ writeState(&rec); ++ traceLog("SETOUT cb=%p pid_ok\n", (uint64_t) callback, 0); + return kCVReturnSuccess; + } + +@@ -91,7 +1024,7 @@ CVReturn CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext(CVDisplayLinkRef disp + CGDirectDisplayID displayID = [window.screen cgDirectDisplayID]; + NSMutableDictionary* self = (NSMutableDictionary*) displayLink; + +- [self setObject: @[[NSNumber numberWithInt: displayID]] ++ [self setObject: @[[NSNumber numberWithInt: (int)(displayID != 0 ? displayID : 1)]] + forKey: kDirectDisplayArray]; + return kCVReturnSuccess; + } +@@ -117,14 +1050,31 @@ CVReturn CVDisplayLinkCreateWithCGDisplay( + CGDirectDisplayID displayID, + CV_RETURNS_RETAINED_PARAMETER CVDisplayLinkRef CV_NULLABLE * CV_NONNULL displayLinkOut ) + { +- if (displayID == kCGNullDirectDisplay) ++ /* darling-lol M57: Chromium's NativeWidgetMacNSWindowHost aborts (and then ++ * crashes on the null link) when this returns non-success; Darling's ++ * CGDirectDisplay ids are unreliable. Always hand back a valid object, and ++ * make sure the current display is non-zero (DisplayLinkMac::GetForDisplay ++ * rejects a zero current display). */ ++ if (displayLinkOut == nullptr) + return kCVReturnInvalidArgument; + + NSMutableDictionary* self = [[NSMutableDictionary alloc] init]; +- [self setObject: @[ [NSNumber numberWithInt: displayID] ] +- forKey: @"CGDirectDisplay"]; ++ [self setObject: @[ [NSNumber numberWithInt: (int)(displayID != 0 ? displayID : 1) ] ] ++ forKey: kDirectDisplayArray]; + ++ /* Optional creation diagnostics. Creation leaves the link stopped. */ ++ if (getenv("RCLOL_CV_TRACE")) { ++ int fd = open("/Volumes/SystemRoot/tmp/opencode/cvpump.log", 1 | 0x200 | 8, 0644); ++ if (fd >= 0) { ++ char msg[64]; ++ snprintf(msg, sizeof(msg), "CREATE_WITH_CG_DISPLAY id=%d\n", (int)(displayID)); ++ write(fd, msg, strlen(msg)); ++ close(fd); ++ } ++ } ++ registerDisplayLink((CVDisplayLinkRef)self); + *displayLinkOut = (CVDisplayLinkRef) self; ++ + return kCVReturnSuccess; + } + +diff --git a/src/frameworks/CoreVideo/src/constants.c b/src/frameworks/CoreVideo/src/constants.c +index 02f06704e..5d4fd4b2d 100644 +--- a/src/frameworks/CoreVideo/src/constants.c ++++ b/src/frameworks/CoreVideo/src/constants.c +@@ -42,6 +42,11 @@ const CFStringRef kCVImageBufferTransferFunction_Linear = CFSTR("Linear"); + const CFStringRef kCVImageBufferTransferFunction_SMPTE_ST_2084_PQ = CFSTR("SMPTE_ST_2084_PQ"); + const CFStringRef kCVImageBufferTransferFunction_SMPTE_ST_428_1 = CFSTR("SMPTE_ST_428_1"); + const CFStringRef kCVImageBufferTransferFunction_SMPTE_C = CFSTR("SMPTE_C"); ++const CFStringRef kCVImageBufferTransferFunction_sRGB = CFSTR("sRGB"); ++const CFStringRef kCVBufferPropagatedAttachmentsKey = CFSTR("CVBufferPropagatedAttachments"); ++const CFStringRef kCVImageBufferContentLightLevelInfoKey = CFSTR("ContentLightLevelInfo"); ++const CFStringRef kCVImageBufferMasteringDisplayColorVolumeKey = CFSTR("MasteringDisplayColorVolume"); ++const CFStringRef kCVPixelBufferPoolAllocationThresholdKey = CFSTR("AllocationThreshold"); + + const CFStringRef kCVPixelBufferIOSurfacePropertiesKey = CFSTR("IOSurfaceProperties"); + const CFStringRef kCVPixelBufferOpenGLCompatibilityKey = CFSTR("OpenGLCompatibility"); +diff --git a/src/frameworks/CryptoTokenKit/CMakeLists.txt b/src/frameworks/CryptoTokenKit/CMakeLists.txt +index 6260e9b35..c02096039 100644 +--- a/src/frameworks/CryptoTokenKit/CMakeLists.txt ++++ b/src/frameworks/CryptoTokenKit/CMakeLists.txt +@@ -30,8 +30,10 @@ add_framework(CryptoTokenKit + src/ctkclient.c + src/ctkclient_test.c + src/ctkloginhelper.c ++ src/TKTokenWatcher.m + + DEPENDENCIES + system + CoreFoundation ++ Foundation + ) +diff --git a/src/frameworks/CryptoTokenKit/include/CryptoTokenKit/TKTokenWatcher.h b/src/frameworks/CryptoTokenKit/include/CryptoTokenKit/TKTokenWatcher.h +index d7ad1ed23..02230985d 100755 +--- a/src/frameworks/CryptoTokenKit/include/CryptoTokenKit/TKTokenWatcher.h ++++ b/src/frameworks/CryptoTokenKit/include/CryptoTokenKit/TKTokenWatcher.h +@@ -2,6 +2,10 @@ + Image: /System/Library/PrivateFrameworks/CryptoTokenKit.framework/CryptoTokenKit + */ + ++#import ++ ++@protocol TKProtocolTokenWatcher; ++ + @interface TKTokenWatcher : NSObject { + NSXPCConnection * _connection; + id /* block */ _insertionHandler; +@@ -12,7 +16,6 @@ + + @property (readonly) NSArray *tokenIDs; + +-- (void).cxx_destruct; + - (void)addRemovalHandler:(id /* block */)arg1 forTokenID:(id)arg2; + - (void)dealloc; + - (id)init; +diff --git a/src/frameworks/DiskArbitration/DADisk.c b/src/frameworks/DiskArbitration/DADisk.c +index 83f1793b8..acc09c088 100644 +--- a/src/frameworks/DiskArbitration/DADisk.c ++++ b/src/frameworks/DiskArbitration/DADisk.c +@@ -82,6 +82,41 @@ DADiskInitialize (void) + _kDADiskTypeID = _CFRuntimeRegisterClass (&DADiskClass); + } + ++CFArrayRef kDADiskDescriptionWatchVolumePath = NULL; ++CFArrayRef kDADiskDescriptionWatchVolumeName = NULL; ++CFDictionaryRef kDADiskDescriptionMatchMediaWhole = NULL; ++CFDictionaryRef kDADiskDescriptionMatchMediaUnformatted = NULL; ++CFDictionaryRef kDADiskDescriptionMatchVolumeMountable = NULL; ++CFDictionaryRef kDADiskDescriptionMatchVolumeUnrecognized = NULL; ++ ++__attribute__((constructor)) ++static void ++DADiskInitializeWatchArrays (void) ++{ ++ const void *pathKeys[] = { kDADiskDescriptionVolumePathKey }; ++ const void *nameKeys[] = { kDADiskDescriptionVolumeNameKey }; ++ kDADiskDescriptionWatchVolumePath = CFArrayCreate(NULL, pathKeys, 1, ++ &kCFTypeArrayCallBacks); ++ kDADiskDescriptionWatchVolumeName = CFArrayCreate(NULL, nameKeys, 1, ++ &kCFTypeArrayCallBacks); ++ kDADiskDescriptionMatchMediaWhole = CFDictionaryCreate(NULL, ++ (const void **)&kDADiskDescriptionMediaWholeKey, ++ (const void **)&kCFBooleanTrue, 1, ++ &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); ++ kDADiskDescriptionMatchMediaUnformatted = CFDictionaryCreate(NULL, ++ (const void **)&kDADiskDescriptionMediaContentKey, ++ (const void **)&kCFBooleanTrue, 1, ++ &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); ++ kDADiskDescriptionMatchVolumeMountable = CFDictionaryCreate(NULL, ++ (const void **)&kDADiskDescriptionVolumeMountableKey, ++ (const void **)&kCFBooleanTrue, 1, ++ &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); ++ kDADiskDescriptionMatchVolumeUnrecognized = CFDictionaryCreate(NULL, ++ (const void **)&kDADiskDescriptionVolumeKindKey, ++ (const void **)&kCFBooleanTrue, 1, ++ &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); ++} ++ + #define DADISK_SIZE sizeof(struct __DADisk) - sizeof(CFRuntimeBase) + + static void +diff --git a/src/frameworks/IOSurface/include/IOSurface/IOSurfacePriv.h b/src/frameworks/IOSurface/include/IOSurface/IOSurfacePriv.h +index d8aed37f1..dd6266cad 100644 +--- a/src/frameworks/IOSurface/include/IOSurface/IOSurfacePriv.h ++++ b/src/frameworks/IOSurface/include/IOSurface/IOSurfacePriv.h +@@ -85,4 +85,43 @@ struct _IOSurfaceLockUnlock + uint32_t options; + }; + ++/* darling-lol M58b: cross-process identity for user-space IOSurfaces. ++ * ++ * Darling has no IOSurfaceRoot IOKit service, so IOSurfaces are user-space ++ * allocations. To let Chromium hand a compositor frame's surface between ++ * processes (GPU -> browser via a real Mach port right, required by ++ * ui/gfx/mac/io_surface.cc's mach_port_mod_refs checks), a fallback surface's ++ * backing is a host shared-memory file (/dev/shm with the dumbest portable ++ * call, shm_open) laid out as: ++ * ++ * offset 0 : struct _IOSurfaceWireHeader ++ * offset IOSURFACE_WIRE_DATA_OFFSET : plane 0 pixel data ++ * ++ * IOSurfaceCreateMachPort() wraps the backing fd in a fileport (see ++ * darlingserver fileport.c), producing a real Mach send right that rides a ++ * Mach message. IOSurfaceLookupFromMachPort() dups the fd back ++ * (fileport_makefd), maps it MAP_SHARED, and rebuilds the IOSurface object ++ * from the wire header. Processes share the *same memory*, so frame updates ++ * are live. */ ++ ++#define IOSURFACE_WIRE_MAGIC 0x46535349u /* 'ISMF' */ ++#define IOSURFACE_WIRE_VERSION 2u ++#define IOSURFACE_WIRE_DATA_OFFSET 4096u ++ ++struct _IOSurfaceWireHeader ++{ ++ uint32_t magic; /* IOSURFACE_WIRE_MAGIC */ ++ uint32_t version; /* IOSURFACE_WIRE_VERSION */ ++ uint32_t surfaceID; ++ uint32_t pixelFormat; ++ uint32_t planeCount; ++ uint32_t reserved; ++ uint64_t memoryOffset; /* == IOSURFACE_WIRE_DATA_OFFSET */ ++ uint32_t width; ++ uint32_t height; ++ uint32_t bytesPerRow; ++ uint32_t bytesPerElement; ++ struct _IOSurfacePlane planes[4]; ++}; ++ + #endif +diff --git a/src/frameworks/IOSurface/src/IOSurfaceObjC.m b/src/frameworks/IOSurface/src/IOSurfaceObjC.m +index 7821ba7a4..951810c92 100644 +--- a/src/frameworks/IOSurface/src/IOSurfaceObjC.m ++++ b/src/frameworks/IOSurface/src/IOSurfaceObjC.m +@@ -23,6 +23,20 @@ + #import + #import + #include ++#include ++ ++/* darling-lol M58b: the guest libc's POSIX shm/mmap API (implemented by ++ * elfcalls against the host, so the allocation is really /dev/shm memory ++ * that another Darling process can map). */ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include + + static io_service_t g_surfaceService; + +@@ -30,22 +44,69 @@ typedef struct + { + struct _IOSurfaceObjectRetval* surface; + _Atomic int32_t localUseCount; ++ /* darling-lol M58b: when the fallback allocation is backed by a host ++ * shared-memory file (see IOSurfacePriv.h), these hold the mapping so ++ * dealloc can unmap/close instead of free()ing platform-managed memory. ++ * shm_fd < 0 means a plain calloc'd private allocation. */ ++ int shm_fd; ++ uint64_t shm_size; ++ mach_port_t shm_port; ++ NSMutableDictionary* attachments; + } ImplData; + ++/* Surface IDs cross process boundaries and must not restart at one per process. */ ++extern long __lolsys_syscall(long,long,long,long,long,long,int); ++static IOSurfaceID allocateSurfaceID(void) ++{ ++ char name[80]; ++ snprintf(name, sizeof(name), "/dev/shm/darling-iosurface-counter-%u", (unsigned)getuid()); ++ /* The counter is host shared memory, never a guest fileport. Native ++ * syscalls preserve the same cross-process flock without routing every ++ * video-frame allocation through Darling's descriptor/event machinery. */ ++ int fd = (int)__lolsys_syscall((long)name,66|02000000 /* Linux O_CLOEXEC */,0600,0,0,0,2); ++ if (fd < 0) return 0; ++ long locked; ++ do { locked=__lolsys_syscall(fd,LOCK_EX,0,0,0,0,73); } while(locked==-4); ++ if (locked != 0) { __lolsys_syscall(fd,0,0,0,0,0,3); return 0; } ++ IOSurfaceID value = 0; ++ long count; ++ do { count=__lolsys_syscall(fd,(long)&value,sizeof(value),0,0,0,17); } while(count==-4); ++ if ((count != 0 && count != sizeof(value)) || value == UINT32_MAX) { ++ __lolsys_syscall(fd,0,0,0,0,0,3); return 0; ++ } ++ ++value; ++ long written; ++ do { written=__lolsys_syscall(fd,(long)&value,sizeof(value),0,0,0,18); } while(written==-4); ++ if (written != sizeof(value)) value = 0; ++ __lolsys_syscall(fd,0,0,0,0,0,3); ++ return value; ++} ++ + @implementation IOSurface + + + (void)initialize + { + if (self == [IOSurface self]) + { +- CFMutableDictionaryRef matching = IOServiceNameMatching("IOSurfaceRoot"); +- +- g_surfaceService = IOServiceGetMatchingService(kIOMasterPortDefault, matching); +- +- CFRelease(matching); +- +- if (!g_surfaceService) +- NSLog(@"Cannot obtain IOSurfaceRoot!\n"); ++ /* darling-lol M58e: do not touch IOKit here at all. ++ * ++ * This class initializer used to look the IOSurfaceRoot IOKit ++ * service up on the first IOSurface use of every Darling ++ * process. There is no way it can SUCCEED under Darling (no ++ * IOSurfaceRoot IOKit server exists — user-space fallbacks ++ * below already key everything off g_surfaceService == 0), but ++ * when the runtime's class-initialization machinery is unlucky ++ * (/helper processes racing CoreFoundation's first CF calls), ++ * the CF objects involved in the call chain can be reported ++ * with garbage isa pointers and the plain CFRelease of the ++ * matching dictionary SEGVs (measured: intermittent SIGSEGV at ++ * window startup in both browser and GPU processes, with LOLSIG ++ * chains ending in libsystem_malloc's free path and in ++ * objc_msgSend on a dead receiver — both reproducible ~1:3 runs ++ * before this change; everything symbolized through the M58e ++ * SIGFRAME walk). */ ++ g_surfaceService = 0; ++ NSLog(@"IOSurface: user-space route (no IOKit IOSurfaceRoot).\n"); + } + } + +@@ -56,6 +117,10 @@ typedef struct + memcpy(idata->surface, bytes, length); + + idata->localUseCount = 0; ++ idata->shm_fd = -1; ++ idata->shm_size = 0; ++ idata->shm_port = MACH_PORT_NULL; ++ idata->attachments = [[NSMutableDictionary alloc] init]; + self->_impl = idata; + + return self; +@@ -63,6 +128,10 @@ typedef struct + + - (nullable instancetype)initWithProperties:(NSDictionary *)properties + { ++ /* The fallback service is absent by construction. Avoid serializing and ++ * issuing an IOKit request that cannot succeed for every video frame. */ ++ if (g_surfaceService == 0) ++ return [self _initWithUserSpaceProperties: properties]; + CFDataRef data = IOCFSerialize((CFTypeRef) properties, kIOCFSerializeToBinary); + uint8_t responseBuffer[3500]; + +@@ -75,11 +144,183 @@ typedef struct + + if (ret != kIOSurfaceSuccess) + { ++ /* darling-lol M57: Darling has no IOSurfaceRoot IOKit service, so the ++ * real create path always fails and Chromium's GPU process can't ++ * allocate any GPU memory buffer (it then exits with "drivers can't ++ * recover"). Fall back to a plain user-space allocation. */ ++ return [self _initWithUserSpaceProperties: (NSDictionary*) properties]; ++ } ++ ++ return [self initWithResponse: responseBuffer length: responseLength]; ++} ++ ++- (nullable instancetype)_initWithUserSpaceProperties:(NSDictionary *)properties ++{ ++ NSNumber* widthNum = properties[(NSString*)kIOSurfaceWidth]; ++ NSNumber* heightNum = properties[(NSString*)kIOSurfaceHeight]; ++ NSNumber* bytesPerRowNum = properties[(NSString*)kIOSurfaceBytesPerRow]; ++ NSNumber* bytesPerElementNum = properties[(NSString*)kIOSurfaceBytesPerElement]; ++ NSNumber* pixelFormatNum = properties[(NSString*)kIOSurfacePixelFormat]; ++ ++ uint32_t width = widthNum ? widthNum.unsignedIntValue : 0; ++ uint32_t height = heightNum ? heightNum.unsignedIntValue : 0; ++ uint32_t bytesPerElement = bytesPerElementNum ? bytesPerElementNum.unsignedIntValue : 4; ++ if (bytesPerElement == 0) bytesPerElement = 4; ++ uint32_t bytesPerRow = bytesPerRowNum ? bytesPerRowNum.unsignedIntValue : (width * bytesPerElement); ++ if (bytesPerRow == 0) bytesPerRow = width * bytesPerElement; ++ uint32_t pixelFormat = pixelFormatNum ? pixelFormatNum.unsignedIntValue : 0; ++ NSArray *planeInfo = properties[(NSString*)kIOSurfacePlaneInfo]; ++ unsigned planeCount = planeInfo.count ? (unsigned)planeInfo.count : 1; ++ if (planeCount > 4) { [self release]; return nil; } ++ struct _IOSurfacePlane planes[4] = {{0}}; ++ uint64_t allocSize = 0; ++ for (unsigned i=0; iUINT32_MAX || ph>UINT32_MAX || pe>16 || ++ pr>UINT32_MAX || prSIZE_MAX/pr || po>SIZE_MAX-pr*ph || ++ (i && poallocSize) allocSize=requested.unsignedLongLongValue; ++ if (allocSize>SIZE_MAX-IOSURFACE_WIRE_DATA_OFFSET) { [self release]; return nil; } ++ ++ /* darling-lol M58b: back the surface with a host shared-memory file so ++ * another process can map the same memory through a fileport handed ++ * off by IOSurfaceCreateMachPort(). The file holds a wire header at ++ * offset 0 and plane 0's data at IOSURFACE_WIRE_DATA_OFFSET. */ ++ uint64_t shmSize = IOSURFACE_WIRE_DATA_OFFSET + allocSize; ++ ++ int shm_fd = -1; ++ void* address = NULL; ++ char shmName[128]; ++ static _Atomic unsigned shmCounter = 0; ++ ++ shmName[0] = '\0'; ++ { ++ _Atomic unsigned counter = ++shmCounter; ++ unsigned attempt; ++ for (attempt = 0; attempt < 16; ++attempt) ++ { ++ /* O_EXCL to guarantee a unique /dev/shm object. */ ++ snprintf(shmName, sizeof(shmName), "/IOSurface-%d-%u-%u", ++ getpid(), (unsigned) counter + attempt, 0u); ++ shm_fd = shm_open(shmName, O_RDWR | O_CREAT | O_EXCL, 0600); ++ if (shm_fd >= 0) ++ break; ++ /* EEXIST -> next name; any other error releases the loop ++ * via the post-check below. */ ++ shm_fd = -1; ++ } ++ } ++ ++ if (shm_fd >= 0) ++ { ++ if (shm_unlink(shmName) != 0) ++ { ++ /* Nobody else needs to find this by name; unlink ASAP so ++ * the object dies with the last fd. */ ++ } ++ ++ if (ftruncate(shm_fd, (off_t) shmSize) != 0) ++ { ++ close(shm_fd); ++ shm_fd = -1; ++ } else { ++ address = mmap(NULL, shmSize, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0); ++ if (address == NULL || address == MAP_FAILED) ++ { ++ close(shm_fd); ++ shm_fd = -1; ++ address = NULL; ++ } ++ } ++ } ++ ++ if (shm_fd < 0) ++ { ++ /* No shared memory available: plain private allocation (in-process ++ * only; IOSurfaceCreateMachPort has nothing to hand off). */ ++ shmSize = allocSize; ++ address = calloc(1, (size_t) allocSize); ++ if (address == NULL) ++ { ++ [self release]; ++ return nil; ++ } ++ } ++ ++ size_t structSize = sizeof(struct _IOSurfaceObjectRetval) + (planeCount-1)*sizeof(struct _IOSurfacePlane); ++ struct _IOSurfaceObjectRetval* surface = calloc(1, structSize); ++ if (surface == NULL) ++ { ++ if (shm_fd >= 0) ++ { ++ munmap(address, shmSize); ++ close(shm_fd); ++ } else { ++ free(address); ++ } + [self release]; + return nil; + } + +- return [self initWithResponse: responseBuffer length: responseLength]; ++ surface->surfaceID = allocateSurfaceID(); ++ if (!surface->surfaceID) { ++ if (shm_fd >= 0) { munmap(address, shmSize); close(shm_fd); } ++ else free(address); ++ free(surface); ++ [self release]; ++ return nil; ++ } ++ surface->pixelFormat = pixelFormat; ++ surface->address = (uint64_t) address; ++ surface->planeCount = planeCount; ++ for (unsigned i=0; iplanes[i]=planes[i]; ++ if (shm_fd>=0) surface->planes[i].memoryOffset+=IOSURFACE_WIRE_DATA_OFFSET; ++ } ++ ++ if (shm_fd >= 0) ++ { ++ /* Fill in the wire header so IOSurfaceLookupFromMachPort() in ++ * another process can rebuild this object from the mapping. */ ++ struct _IOSurfaceWireHeader wire; ++ memset(&wire, 0, sizeof(wire)); ++ wire.magic = IOSURFACE_WIRE_MAGIC; ++ wire.version = IOSURFACE_WIRE_VERSION; ++ wire.surfaceID = surface->surfaceID; ++ wire.pixelFormat = surface->pixelFormat; ++ wire.planeCount = surface->planeCount; ++ wire.reserved = 0; ++ wire.memoryOffset = surface->planes[0].memoryOffset; ++ wire.width = surface->planes[0].width; ++ wire.height = surface->planes[0].height; ++ wire.bytesPerRow = surface->planes[0].bytesPerRow; ++ wire.bytesPerElement = surface->planes[0].bytesPerElement; ++ memcpy(wire.planes, surface->planes, planeCount*sizeof(struct _IOSurfacePlane)); ++ (void) pwrite(shm_fd, &wire, sizeof(wire), 0); ++ } ++ ++ ImplData* idata = (ImplData*) malloc(sizeof(ImplData)); ++ idata->surface = surface; ++ idata->localUseCount = 0; ++ idata->shm_fd = shm_fd; ++ idata->shm_size = shmSize; ++ idata->shm_port = MACH_PORT_NULL; ++ idata->attachments = [[NSMutableDictionary alloc] init]; ++ self->_impl = idata; ++ ++ return self; + } + + - (nullable instancetype)initWithSurfaceID:(IOSurfaceID)surfaceID +@@ -88,10 +329,19 @@ typedef struct + size_t responseLength = sizeof(responseBuffer); + + uint64_t scalar = surfaceID; +- kern_return_t ret = IOConnectCallMethod(g_surfaceService, kIOSurfaceMethodLookupByID, &scalar, 1, NULL, 0, NULL, 0, responseBuffer, &responseLength); ++ kern_return_t ret = g_surfaceService != 0 ++ ? IOConnectCallMethod(g_surfaceService, kIOSurfaceMethodLookupByID, &scalar, 1, NULL, 0, NULL, 0, responseBuffer, &responseLength) ++ : KERN_FAILURE; + + if (ret != kIOSurfaceSuccess) + { ++ /* darling-lol: without the IOSurfaceRoot service (Darling) the ++ * lookup always fails, so re-resolve the ID against our own ++ * user-space registry instead of returning nil. */ ++ IOSurfaceRef surface = IOSurfaceLookup(surfaceID); ++ if (surface != NULL) ++ return (IOSurface*) surface; ++ + [self release]; + return nil; + } +@@ -99,18 +349,74 @@ typedef struct + return [self initWithResponse: responseBuffer length: responseLength]; + } + ++/* darling-lol: IOSurfaceLookupFromMachPort() calls this, but it was never ++ * implemented, so every cross-process frame handoff returned a half-built ++ * object. Recover a registered surface by token, else treat the token as an ++ * IOSurfaceID and try the registry through iosurface_lookup(). */ ++- (nullable instancetype)initWithMachPort:(mach_port_t)port ++{ ++ if (port == MACH_PORT_NULL) ++ { ++ [self release]; ++ return nil; ++ } ++ ++ IOSurfaceRef known = IOSurfaceLookup((IOSurfaceID) port); ++ if (known != NULL) ++ { ++ [self release]; ++ return (IOSurface*) known; ++ } ++ ++ /* Unknown token: build an empty placeholder rather than nil so callers ++ * that only need the object identity keep working. */ ++ return [self initWithResponse: "\0" length: 1]; ++} ++ ++/* Serialize final release with registry lookup/retain. The recursive lock ++ * permits dealloc to remove the entry while super's release is active. */ ++- (oneway void) release ++{ ++ extern void _IOSurfaceRegistryLock(void), _IOSurfaceRegistryUnlock(void); ++ _IOSurfaceRegistryLock(); ++ [super release]; ++ _IOSurfaceRegistryUnlock(); ++} ++ + - (void) dealloc + { ++ extern void _IOSurfaceRegistryForget(IOSurfaceRef); ++ if (_impl) _IOSurfaceRegistryForget((IOSurfaceRef)self); + ImplData* data = (ImplData*) _impl; + if (data != NULL) + { + if (data->surface != NULL) + { +- IOConnectCallMethod(g_surfaceService, kIOSurfaceMethodRelease, NULL, 0, &data->surface->surfaceID, 1, NULL, 0, NULL, 0); ++ if (g_surfaceService != 0) ++ IOConnectCallMethod(g_surfaceService, kIOSurfaceMethodRelease, NULL, 0, &data->surface->surfaceID, 1, NULL, 0, NULL, 0); ++ ++ /* darling-lol M58b: free the surface's memory according to ++ * its backing; shared mappings belong to mmap, not the ++ * general allocator. */ ++ if (data->shm_fd >= 0) ++ { ++ if (data->surface->address != 0) ++ munmap((void*)(uintptr_t) data->surface->address, data->shm_size); ++ close(data->shm_fd); ++ } else if (data->surface->address != 0) { ++ free((void*)(uintptr_t) data->surface->address); ++ } ++ ++ /* The fileport right created by IOSurfaceCreateMachPort() ++ * keeps the backing alive for whoever it was handed to; ++ * releasing our send right is enough here. */ ++ if (data->shm_port != MACH_PORT_NULL) ++ mach_port_deallocate(mach_task_self(), data->shm_port); + + free(data->surface); + } + ++ [data->attachments release]; + free(data); + } + [super dealloc]; +@@ -236,11 +542,56 @@ typedef struct + return (void*)(data->surface->address + data->surface->planes[planeIndex].memoryOffset); + } + ++- (void)setAttachment:(id)anObject forKey:(NSString *)key ++{ ++ if (!key) ++ return; ++ ImplData* data = (ImplData*) _impl; ++ if (anObject) ++ [data->attachments setObject:anObject forKey:key]; ++ else ++ [data->attachments removeObjectForKey:key]; ++} ++ ++- (id)attachmentForKey:(NSString *)key ++{ ++ ImplData* data = (ImplData*) _impl; ++ return [data->attachments objectForKey:key]; ++} ++ ++- (void)removeAttachmentForKey:(NSString *)key ++{ ++ ImplData* data = (ImplData*) _impl; ++ [data->attachments removeObjectForKey:key]; ++} ++ ++- (void)setAllAttachments:(NSDictionary *)dict ++{ ++ ImplData* data = (ImplData*) _impl; ++ [data->attachments removeAllObjects]; ++ if (dict) ++ [data->attachments addEntriesFromDictionary:dict]; ++} ++ ++- (NSDictionary *)allAttachments ++{ ++ ImplData* data = (ImplData*) _impl; ++ return data->attachments; ++} ++ ++- (void)removeAllAttachments ++{ ++ ImplData* data = (ImplData*) _impl; ++ [data->attachments removeAllObjects]; ++} ++ + - (void)incrementUseCount + { + ImplData* data = (ImplData*) _impl; + if ((data->localUseCount++) == 0) + { ++ if (g_surfaceService == 0) ++ return; + uint64_t scalar = data->surface->surfaceID; + IOConnectCallMethod(g_surfaceService, kIOSurfaceMethodIncrementUseCount, &scalar, 1, NULL, 0, NULL, 0, NULL, 0); + } +@@ -251,6 +602,8 @@ typedef struct + ImplData* data = (ImplData*) _impl; + if (--data->localUseCount == 0) + { ++ if (g_surfaceService == 0) ++ return; + uint64_t scalar = data->surface->surfaceID; + IOConnectCallMethod(g_surfaceService, kIOSurfaceMethodDecrementUseCount, &scalar, 1, NULL, 0, NULL, 0, NULL, 0); + } +@@ -262,6 +615,188 @@ typedef struct + return data->localUseCount; + } + ++/* darling-lol M58b: cross-process identity. IOSurfaceCreateMachPort() ++ * (in api.m) calls this to wrap this surface's shared backing in a fileport ++ * -- a REAL mach task-port send right referencing our /dev/shm file, so ++ * callers that do mach_port_mod_refs()/port-transfer checks get a valid ++ * name. Cached on the object so repeated creates reuse the same right. */ ++static mach_port_t copySurfaceSendRight(mach_port_t port) ++{ ++ /* The surface owns the cached reference; every Create caller owns another. */ ++ if (mach_port_mod_refs(mach_task_self(), port, MACH_PORT_RIGHT_SEND, 1) != KERN_SUCCESS) ++ return MACH_PORT_NULL; ++ return port; ++} ++ ++mach_port_t _IOSurfaceObjCShmFileportForSurface(IOSurfaceRef buffer) ++{ ++ IOSurface* self = (IOSurface*) buffer; ++ ImplData* data = (ImplData*) self->_impl; ++ ++ if (buffer == NULL) ++ return MACH_PORT_NULL; ++ ++ data = (ImplData*) self->_impl; ++ if (data == NULL) ++ return MACH_PORT_NULL; ++ ++ /* darling-lol M58cz: reuse any right we already handed out. This also ++ * covers surfaces restored via initWithMachPort: (which have no shm_fd ++ * but may carry a port). */ ++ if (data->shm_port != MACH_PORT_NULL) ++ return copySurfaceSendRight(data->shm_port); ++ ++ if (data->shm_fd >= 0) ++ { ++ mach_port_t port = MACH_PORT_NULL; ++ if (fileport_makeport(data->shm_fd, &port) != KERN_SUCCESS || port == MACH_PORT_NULL) ++ { ++ NSLog(@"_IOSurfaceObjCShmFileportForSurface: fileport_makeport failed (fd=%d)", data->shm_fd); ++ return MACH_PORT_NULL; ++ } ++ data->shm_port = port; ++ return copySurfaceSendRight(data->shm_port); ++ } ++ ++ /* darling-lol M58cz: a surface with NO shared backing must STILL hand ++ * out a REAL mach send right. The old M57 code returned a fake token ++ * ((mach_port_t) sid); Chromium's IOSurfaceMachPortTraits then failed ++ * mach_port_mod_refs() with KERN_INVALID_NAME on every surface handoff, ++ * and the token could not be carried in a mach message at all. Back the ++ * right with /dev/null: the name is valid for mach_port_mod_refs() and ++ * mojo's port transport; a cross-process lookup of a private surface ++ * still resolves to a placeholder, as before. */ ++ { ++ int nullfd = open("/dev/null", 0 /* O_RDONLY */); ++ mach_port_t port = MACH_PORT_NULL; ++ if (nullfd >= 0 && ++ fileport_makeport(nullfd, &port) == KERN_SUCCESS && ++ port != MACH_PORT_NULL) ++ { ++ /* the fileport kobject owns nullfd now and closes it on death */ ++ data->shm_port = port; ++ return copySurfaceSendRight(data->shm_port); ++ } ++ if (nullfd >= 0) ++ close(nullfd); ++ } ++ return MACH_PORT_NULL; ++} ++ ++/* darling-lol M58b: IOSurfaceLookupFromMachPort() (in api.m) calls this when ++ * the port is a fileport holding a shared IOSurface backing: map the fd and ++ * reconstruct the object from the wire header at offset 0. */ ++/* Keep at most eight live import mappings. File identity plus the complete ++ * validated wire layout prevents descriptor/Mach-name reuse from aliasing a ++ * different surface. The cache owns no Mach rights; every lookup still ++ * validates its caller's right through fileport_makefd before reaching here. */ ++struct ImportedSurfaceCacheEntry { ++ uint64_t device, inode, size, age; ++ struct _IOSurfaceWireHeader wire; ++ IOSurface *surface; ++}; ++static struct ImportedSurfaceCacheEntry importedSurfaces[8]; ++static uint64_t importedSurfaceAge; ++extern void _IOSurfaceRegistryLock(void), _IOSurfaceRegistryUnlock(void); ++static bool importedSurfaceMatches(struct ImportedSurfaceCacheEntry *entry, ++ const struct stat *st, const struct _IOSurfaceWireHeader *wire) { ++ return entry->surface && entry->device==(uint64_t)st->st_dev && ++ entry->inode==(uint64_t)st->st_ino && entry->size==(uint64_t)st->st_size && ++ !memcmp(&entry->wire,wire,sizeof(*wire)); ++} ++static IOSurface *cachedImportedSurface(const struct stat *st, ++ const struct _IOSurfaceWireHeader *wire, IOSurface *candidate) { ++ _IOSurfaceRegistryLock(); ++ for (unsigned i=0;i<8;i++) if(importedSurfaceMatches(&importedSurfaces[i],st,wire)) { ++ IOSurface *result=[importedSurfaces[i].surface retain]; ++ importedSurfaces[i].age=++importedSurfaceAge; ++ _IOSurfaceRegistryUnlock();[candidate release];return result; ++ } ++ if(!candidate){_IOSurfaceRegistryUnlock();return nil;} ++ unsigned index=0; ++ for(unsigned i=1;i<8;i++) if(importedSurfaces[i].agest_dev,.inode=(uint64_t)st->st_ino, ++ .size=(uint64_t)st->st_size,.age=++importedSurfaceAge, ++ .wire=*wire,.surface=[candidate retain]}; ++ _IOSurfaceRegistryUnlock();[old release];return candidate; ++} ++ ++IOSurfaceRef _IOSurfaceInitFromSharedMemfd(int fd) ++{ ++ struct stat st; ++ if (fd < 0 || fstat(fd, &st) != 0 || st.st_size < (off_t) sizeof(struct _IOSurfaceWireHeader)) ++ { ++ if (fd >= 0) ++ close(fd); ++ return NULL; ++ } ++ ++ uint64_t shmSize = (uint64_t) st.st_size; ++ ++ struct _IOSurfaceWireHeader wire; ++ if (pread(fd, &wire, sizeof(wire), 0) != sizeof(wire) || ++ wire.magic != IOSURFACE_WIRE_MAGIC || ++ wire.version != IOSURFACE_WIRE_VERSION || ++ !wire.planeCount || wire.planeCount > 4 || ++ wire.memoryOffset != IOSURFACE_WIRE_DATA_OFFSET || ++ st.st_size < (off_t)(wire.memoryOffset + (uint64_t) wire.bytesPerRow * (uint64_t) wire.height)) ++ { ++ close(fd); ++ return NULL; ++ } ++ ++ for (unsigned i=0; i16 || ++ (uint64_t)p.width*p.bytesPerElement>p.bytesPerRow || ++ p.memoryOffsetshmSize || bytes>shmSize-p.memoryOffset) { ++ close(fd); return NULL; ++ } ++ } ++ unsigned planeCount=wire.planeCount; ++ bool cacheEnabled=getenv("RCLOL_IOSURFACE_IMPORT_CACHE")!=NULL; ++ if(cacheEnabled) { ++ IOSurface *cached=cachedImportedSurface(&st,&wire,nil); ++ if(cached){close(fd);return (IOSurfaceRef)cached;} ++ } ++ ++ void* address = mmap(NULL, shmSize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); ++ if (address == NULL || address == MAP_FAILED) ++ { ++ close(fd); ++ return NULL; ++ } ++ ++ size_t structSize = sizeof(struct _IOSurfaceObjectRetval) + (planeCount-1)*sizeof(struct _IOSurfacePlane); ++ struct _IOSurfaceObjectRetval* surface = calloc(1, structSize); ++ if (surface == NULL) ++ { ++ munmap(address, shmSize); ++ close(fd); ++ return NULL; ++ } ++ ++ surface->surfaceID = wire.surfaceID; ++ surface->pixelFormat = wire.pixelFormat; ++ surface->address = (uint64_t) address; ++ surface->planeCount=planeCount; ++ memcpy(surface->planes,wire.planes,planeCount*sizeof(struct _IOSurfacePlane)); ++ ++ IOSurface* obj = [IOSurface alloc]; ++ ImplData* idata = (ImplData*) malloc(sizeof(ImplData)); ++ idata->surface = surface; ++ idata->localUseCount = 0; ++ idata->shm_fd = fd; ++ idata->shm_size = shmSize; ++ idata->shm_port = MACH_PORT_NULL; ++ idata->attachments = [[NSMutableDictionary alloc] init]; ++ obj->_impl = idata; ++ ++ return (IOSurfaceRef)(cacheEnabled?cachedImportedSurface(&st,&wire,obj):obj); ++} + - (kern_return_t)lockWithOptions:(IOSurfaceLockOptions)options seed:(nullable uint32_t *)seed + { + ImplData* data = (ImplData*) _impl; +@@ -269,6 +804,12 @@ typedef struct + .surfaceID = data->surface->surfaceID, + .options = options, + }; ++ /* darling-lol: user-space fallback surfaces have no IOSurfaceRoot ++ * connection, so calling the (null) service returns an error and the ++ * frame path treats the surface as unusable. There is no real lock to ++ * take for a private allocation, so succeed. */ ++ if (g_surfaceService == 0) ++ return kIOSurfaceSuccess; + return IOConnectCallMethod(g_surfaceService, kIOSurfaceMethodLock, NULL, 0, &args, sizeof(args), NULL, 0, NULL, 0); + } + +@@ -279,6 +820,8 @@ typedef struct + .surfaceID = data->surface->surfaceID, + .options = options, + }; ++ if (g_surfaceService == 0) ++ return kIOSurfaceSuccess; + return IOConnectCallMethod(g_surfaceService, kIOSurfaceMethodUnlock, NULL, 0, &args, sizeof(args), NULL, 0, NULL, 0); + } + +@@ -291,3 +834,8 @@ typedef struct + } + + @end ++ ++size_t _IOSurfaceObjCGetAllocSize(IOSurfaceRef buffer) { ++ ImplData *data=(ImplData*)((IOSurface*)buffer)->_impl; ++ return data->shm_fd>=0 ? data->shm_size-IOSURFACE_WIRE_DATA_OFFSET : data->shm_size; ++} +diff --git a/src/frameworks/IOSurface/src/api.m b/src/frameworks/IOSurface/src/api.m +index dea76c18a..e873bcda5 100644 +--- a/src/frameworks/IOSurface/src/api.m ++++ b/src/frameworks/IOSurface/src/api.m +@@ -19,6 +19,156 @@ + + #import + #import ++#import ++#import ++#import ++#include ++#include ++#include ++#include ++#include ++ ++/* darling-lol M58b: IOSurfaceCreateMachPort / IOSurfaceLookupFromMachPort are ++ * implemented on Darling's fileport machinery plus host shared-memory files; ++ * the send/receive helpers live in IOSurfaceObjC.m (they need the internal ++ * ImplData). The M57 fake-token registry is gone: fake names made Chromium's ++ * mach_port_mod_refs (ui/gfx/mac/io_surface.cc IOSurfaceMachPortTraits) fail ++ * with invalid_name on every surface handoff. */ ++extern mach_port_t _IOSurfaceObjCShmFileportForSurface(IOSurfaceRef buffer); ++extern IOSurfaceRef _IOSurfaceInitFromSharedMemfd(int fd); ++extern int fileport_makefd(mach_port_t port); ++ ++/* Keep the M57 token registry as the in-process fast path; the fileport is ++ * used when the surface has a shared backing. */ ++static NSMutableDictionary* gMachPortRegistry = nil; ++static NSRecursiveLock* gMachPortRegistryLock = nil; ++static pthread_once_t registryOnce = PTHREAD_ONCE_INIT; ++ ++/* darling-lol M58c: materialize the rendered GL texture into the IOSurface ++ * backing before the surface leaves this process. ++ * ++ * On macOS, CGLTexImageIOSurface2D makes the IOSurface the texture's storage, ++ * so GL rendering lands in IOSurface memory implicitly. Darling's CGL is ++ * backed by Mesa/EGL, where CGLTexImageIOSurface2D can only COPY the IOSurface ++ * into a private GL texture once at bind time. Chromium's compositor then ++ * renders every frame into that texture, and the IOSurface backing (the host ++ * shared-memory file shared with the next process) never receives the pixels — ++ * every published frame was metadata-correct but all-zero. ++ * ++ * The fix: at every cross-process handoff point (IOSurfaceCreateMachPort is ++ * called by the sender — GPU process for the display surface, renderer process ++ * for its GMB content surfaces), copy the bound GL texture back into the ++ * IOSurface memory via OpenGL.framework's CGLReadbackIOSurface. The binding ++ * table it consults is per-process, so this is a harmless no-op in processes ++ * that never GL-bound the surface (browser, software path). ++ * ++ * OpenGL.framework is resolved with a private dlopen: IOSurface cannot link ++ * OpenGL (OpenGL links IOSurface — a cycle), and per the M57bo lesson adding ++ * CGL symbols to IOSurface's dependency symbol scope would leak them to every ++ * dependent. In GL processes the framework is already loaded (ANGLE depends on ++ * it), so dlopen merely bumps the refcount. */ ++typedef unsigned int _DarlingCGLError; ++typedef _DarlingCGLError (*_DarlingReadbackFn)(IOSurfaceRef); ++ ++static _DarlingReadbackFn _DarlingResolveReadback(void) ++{ ++ static _DarlingReadbackFn fn; ++ static int resolved; ++ static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; ++ ++ pthread_mutex_lock(&lock); ++ if (!resolved) ++ { ++ resolved = 1; ++ void* h = dlopen("/System/Library/Frameworks/OpenGL.framework/OpenGL", ++ RTLD_NOW | RTLD_LOCAL); ++ if (h) ++ fn = (_DarlingReadbackFn) dlsym(h, "CGLReadbackIOSurface"); ++ } ++ pthread_mutex_unlock(&lock); ++ return fn; ++} ++ ++static void _DarlingWriteBackGLTexture(IOSurfaceRef buffer) ++{ ++ _DarlingReadbackFn fn = _DarlingResolveReadback(); ++ if (!fn) ++ return; ++ _DarlingCGLError err = fn(buffer); ++ if (err != 0 && getenv("M58_WB_TRACE") != NULL) ++ fprintf(stderr, "M58WB pid=%d sid=%u readback=0x%x\n", ++ (int) getpid(), (unsigned) IOSurfaceGetID(buffer), (unsigned) err); ++} ++ ++static void initializeSurfaceRegistry(void) ++{ ++ gMachPortRegistry = [[NSMutableDictionary alloc] init]; ++ gMachPortRegistryLock = [[NSRecursiveLock alloc] init]; ++} ++static void ensureMachPortRegistry(void) { pthread_once(®istryOnce, initializeSurfaceRegistry); } ++void _IOSurfaceRegistryLock(void) { ensureMachPortRegistry(); [gMachPortRegistryLock lock]; } ++void _IOSurfaceRegistryUnlock(void) { [gMachPortRegistryLock unlock]; } ++void _IOSurfaceRegistryForget(IOSurfaceRef buffer) ++{ ++ typedef void (*ForgetFn)(IOSurfaceRef); ++ ForgetFn forget = (ForgetFn)dlsym(RTLD_DEFAULT, "CGLForgetIOSurface"); ++ if (forget) forget(buffer); ++ NSNumber *key = [NSNumber numberWithUnsignedInt: IOSurfaceGetID(buffer)]; ++ NSValue *value = [gMachPortRegistry objectForKey:key]; ++ if ([value pointerValue] == buffer) [gMachPortRegistry removeObjectForKey:key]; ++} ++IOSurfaceRef _IOSurfaceLookupByID(IOSurfaceID sid) ++{ ++ _IOSurfaceRegistryLock(); ++ NSValue *value = [gMachPortRegistry objectForKey:[NSNumber numberWithUnsignedInt:sid]]; ++ IOSurface *surface = (IOSurface*)[value pointerValue]; ++ [surface retain]; ++ _IOSurfaceRegistryUnlock(); ++ return (IOSurfaceRef)surface; ++} ++ ++mach_port_t IOSurfaceCreateMachPort(IOSurfaceRef buffer) ++{ ++ if (buffer == NULL) ++ return MACH_PORT_NULL; ++ ++ /* darling-lol M58c: Chromium may have rendered this surface through a ++ * GL texture (CGLTexImageIOSurface2D's one-shot upload). Materialize ++ * the rendered texture into the IOSurface backing before the surface ++ * is handed to another process. No-op when this process never GL-bound ++ * the surface. */ ++ _DarlingWriteBackGLTexture(buffer); ++ ++ /* darling-lol M58b/M58cz: hand out a REAL mach send right. With a shared ++ * backing this is a fileport over the backing fd; with no shared backing ++ * _IOSurfaceObjCShmFileportForSurface() now also returns a real right ++ * (over /dev/null), so this never returns a fake token again. */ ++ mach_port_t port = _IOSurfaceObjCShmFileportForSurface(buffer); ++ if (port == MACH_PORT_NULL) ++ return MACH_PORT_NULL; ++ ++ /* ID lookup is distinct from Mach names. Final release is serialized ++ * with ID lookup, and deallocation removes this non-owning entry. */ ++ ensureMachPortRegistry(); ++ ++ IOSurfaceID sid = (IOSurfaceID) [(IOSurface*) buffer hash]; ++ if (sid == 0) ++ sid = 1; ++ ++ [gMachPortRegistryLock lock]; ++ [gMachPortRegistry setObject: [NSValue valueWithPointer: buffer] ++ forKey: [NSNumber numberWithUnsignedInt: sid]]; ++ [gMachPortRegistryLock unlock]; ++ ++ return port; ++} ++ ++IOSurfaceID IOSurfaceGetID(IOSurfaceRef buffer) ++{ ++ if (buffer == NULL) ++ return 0; ++ return (IOSurfaceID) [(IOSurface*) buffer hash]; ++} + + CFTypeID IOSurfaceGetTypeID(void) + { +@@ -32,7 +182,22 @@ IOSurfaceRef _Nullable IOSurfaceCreate(CFDictionaryRef properties) + + IOSurfaceRef _Nullable IOSurfaceLookupFromMachPort(mach_port_t port) + { +- return (IOSurfaceRef) [[IOSurface alloc] initWithMachPort: port]; ++ if (port == MACH_PORT_NULL) ++ return NULL; ++ ++ /* Mach names are reusable and must never index a weak object cache. ++ * Reconstruct from the send right's shared backing, including in-process. */ ++ /* darling-lol M58b: the port is a fileport holding the surface's ++ * shared backing; fileport_makefd() gives this process its own dup of ++ * that fd, which the helper takes ownership of (mmap + object ++ * construction from the wire header). */ ++ int fd = fileport_makefd(port); ++ if (fd >= 0) ++ { ++ return _IOSurfaceInitFromSharedMemfd(fd); ++ } ++ ++ return NULL; + } + + xpc_object_t IOSurfaceCreateXPCObject(IOSurfaceRef surface) +@@ -57,9 +222,35 @@ kern_return_t IOSurfaceLock(IOSurfaceRef buffer, IOSurfaceLockOptions options, u + return [(IOSurface*)buffer lockWithOptions: options seed: seed]; + } + ++static void _DarlingPublishSoftwarePixels(IOSurfaceRef surface) { ++ typedef void (*PublishFn)(IOSurfaceRef); ++ static PublishFn publish; ++ static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; ++ pthread_mutex_lock(&lock); ++ if (!publish) { ++ /* Avoid introducing a framework dependency cycle or loading CA into ++ * processes that do not use it. Retry if it was not yet loaded. */ ++ publish = (PublishFn)dlsym(RTLD_DEFAULT, "DarlingCAPublishIOSurfacePixels"); ++ } ++ PublishFn callback = publish; ++ pthread_mutex_unlock(&lock); ++ if (callback) callback(surface); ++} ++ ++/* GPU readback completes a lock without claiming software compositor ownership. */ ++kern_return_t _IOSurfaceUnlockGPUReadback(IOSurfaceRef buffer) ++{ ++ return [(IOSurface*)buffer unlockWithOptions:0 seed:NULL]; ++} ++ + kern_return_t IOSurfaceUnlock(IOSurfaceRef buffer, IOSurfaceLockOptions options, uint32_t * _Nullable seed) + { +- return [(IOSurface*)buffer unlockWithOptions: options seed: seed]; ++ kern_return_t result = [(IOSurface*)buffer unlockWithOptions: options seed: seed]; ++ /* A software compositor reuses its IOSurface without a CALayer setter. ++ * Publish completed writes, excluding the read-only snapshot locks. */ ++ if (result == KERN_SUCCESS && !(options & kIOSurfaceLockReadOnly)) ++ _DarlingPublishSoftwarePixels(buffer); ++ return result; + } + + size_t IOSurfaceGetWidth(IOSurfaceRef buffer) +diff --git a/src/frameworks/IOSurface/src/stubs.m b/src/frameworks/IOSurface/src/stubs.m +index 7e5976f2f..bd2566180 100644 +--- a/src/frameworks/IOSurface/src/stubs.m ++++ b/src/frameworks/IOSurface/src/stubs.m +@@ -23,6 +23,12 @@ + + #include + ++/* darling-lol M57: IOSurfaceLookup has to resolve IDs produced by our ++ * user-space IOSurface fallback (IOSurfaceCreateMachPort registers tokens in ++ * api.m). Returning 0 here broke IOSurface lookup-by-ID and the ++ * initWithSurfaceID: recovery path used during frame handoff. */ ++IOSurfaceRef _Nullable IOSurfaceLookupFromMachPort(mach_port_t port); ++ + static int verbose = 0; + + __attribute__((constructor)) +@@ -33,19 +39,23 @@ static void initme(void) { + + IOSurfaceRef _Nullable IOSurfaceLookup(IOSurfaceID csid){ + if (verbose) printf("STUB: %s called\n", __FUNCTION__); +- return 0; +-} +- +- +-IOSurfaceID IOSurfaceGetID(IOSurfaceRef buffer){ +- if (verbose) printf("STUB: %s called\n", __FUNCTION__); +- return 0; ++ if (csid == 0) ++ return 0; ++ extern IOSurfaceRef _IOSurfaceLookupByID(IOSurfaceID); ++ return _IOSurfaceLookupByID(csid); + } + + + size_t IOSurfaceGetAllocSize(IOSurfaceRef buffer){ + if (verbose) printf("STUB: %s called\n", __FUNCTION__); +- return 0; ++ if (buffer == NULL) ++ return 0; ++ /* darling-lol: Chromium checks the allocation size when wrapping a ++ * surface in a GpuMemoryBuffer; returning 0 made every buffer look ++ * invalid. The user-space fallback allocates exactly ++ * bytesPerRow * height for plane 0. */ ++ extern size_t _IOSurfaceObjCGetAllocSize(IOSurfaceRef); ++ return _IOSurfaceObjCGetAllocSize(buffer); + } + + +@@ -93,12 +103,6 @@ IOSurfaceSubsampling IOSurfaceGetSubsampling(IOSurfaceRef buffer){ + + + +-mach_port_t IOSurfaceCreateMachPort(IOSurfaceRef buffer){ +- if (verbose) printf("STUB: %s called\n", __FUNCTION__); +- return 0; +-} +- +- + size_t IOSurfaceGetPropertyMaximum(CFStringRef property){ + if (verbose) printf("STUB: %s called\n", __FUNCTION__); + return 0; +@@ -112,8 +116,17 @@ size_t IOSurfaceGetPropertyAlignment(CFStringRef property){ + + + size_t IOSurfaceAlignProperty(CFStringRef property, size_t value){ +- if (verbose) printf("STUB: %s called\n", __FUNCTION__); +- return 0; ++ /* Our shared backing requires valid non-overlapping plane layouts. ++ * Chromium aligns every plane stride, allocation and offset through this ++ * API; returning zero makes all chroma planes overlap the luma plane. */ ++ size_t alignment = 1; ++ if(property && CFEqual(property,kIOSurfaceAllocSize)) alignment=4096; ++ else if(property && (CFEqual(property,kIOSurfaceBytesPerRow) || ++ CFEqual(property,kIOSurfacePlaneBytesPerRow) || ++ CFEqual(property,kIOSurfacePlaneSize) || ++ CFEqual(property,kIOSurfacePlaneOffset))) alignment=64; ++ if(value>SIZE_MAX-(alignment-1))return 0; ++ return (value+alignment-1)&~(alignment-1); + } + + +@@ -123,4 +136,3 @@ Boolean IOSurfaceAllowsPixelSizeCasting(IOSurfaceRef buffer){ + return 0; + } + +- +diff --git a/src/frameworks/ImageCaptureCore/include/ImageCaptureCore/ICCameraItem.h b/src/frameworks/ImageCaptureCore/include/ImageCaptureCore/ICCameraItem.h +index 1b9216d1e..9e9f35386 100644 +--- a/src/frameworks/ImageCaptureCore/include/ImageCaptureCore/ICCameraItem.h ++++ b/src/frameworks/ImageCaptureCore/include/ImageCaptureCore/ICCameraItem.h +@@ -22,6 +22,10 @@ + typedef NSString* ICDownloadOption; + + extern const ICDownloadOption ICDownloadsDirectoryURL; ++extern const ICDownloadOption ICOverwrite; ++extern const ICDownloadOption ICSaveAsFilename; ++extern const ICDownloadOption ICSavedFilename; ++extern const ICDownloadOption ICTransportTypeMassStorage; + + @interface ICCameraItem : NSObject + +diff --git a/src/frameworks/ImageCaptureCore/src/ICCameraItem.m b/src/frameworks/ImageCaptureCore/src/ICCameraItem.m +index 350ae5f9a..0ca4ea610 100644 +--- a/src/frameworks/ImageCaptureCore/src/ICCameraItem.m ++++ b/src/frameworks/ImageCaptureCore/src/ICCameraItem.m +@@ -20,6 +20,10 @@ + #import + + ICDownloadOption const ICDownloadsDirectoryURL = @"ICDownloadsDirectoryURL"; ++ICDownloadOption const ICOverwrite = @"ICOverwrite"; ++ICDownloadOption const ICSaveAsFilename = @"ICSaveAsFilename"; ++ICDownloadOption const ICSavedFilename = @"ICSavedFilename"; ++ICDownloadOption const ICTransportTypeMassStorage = @"MassStorage"; + + @implementation ICCameraItem + +diff --git a/src/frameworks/MediaAccessibility/CMakeLists.txt b/src/frameworks/MediaAccessibility/CMakeLists.txt +index 216be5050..2edeaac81 100644 +--- a/src/frameworks/MediaAccessibility/CMakeLists.txt ++++ b/src/frameworks/MediaAccessibility/CMakeLists.txt +@@ -22,4 +22,5 @@ add_framework(MediaAccessibility + + DEPENDENCIES + system ++ Foundation + ) +diff --git a/src/frameworks/MediaAccessibility/include/MediaAccessibility/MediaAccessibility.h b/src/frameworks/MediaAccessibility/include/MediaAccessibility/MediaAccessibility.h +index 6be755c16..7916d13b1 100644 +--- a/src/frameworks/MediaAccessibility/include/MediaAccessibility/MediaAccessibility.h ++++ b/src/frameworks/MediaAccessibility/include/MediaAccessibility/MediaAccessibility.h +@@ -264,6 +264,10 @@ void* MAXCaptionAppearancePrefSetWindowColor(void); + void* MAXCaptionAppearancePrefSetWindowOpacity(void); + void* MAXCaptionAppearancePrefSetWindowRoundedCornerRadius(void); + void* MAXCaptionAppearanceSetPreferredLanguage(void); ++#include ++ + void* MAXCaptionAppearanceShowCaptionsWhenAvailable(void); + ++extern const CFStringRef kMACaptionAppearanceSettingsChangedNotification; ++ + #endif +diff --git a/src/frameworks/MediaAccessibility/src/MediaAccessibility.c b/src/frameworks/MediaAccessibility/src/MediaAccessibility.c +index d2b138594..62cc3c784 100644 +--- a/src/frameworks/MediaAccessibility/src/MediaAccessibility.c ++++ b/src/frameworks/MediaAccessibility/src/MediaAccessibility.c +@@ -19,11 +19,14 @@ + + + #include ++#include + #include + #include + + static int verbose = 0; + ++const CFStringRef kMACaptionAppearanceSettingsChangedNotification = CFSTR("kMACaptionAppearanceSettingsChangedNotification"); ++ + __attribute__((constructor)) + static void initme(void) { + verbose = getenv("STUB_VERBOSE") != NULL; +diff --git a/src/frameworks/OpenDirectory/CMakeLists.txt b/src/frameworks/OpenDirectory/CMakeLists.txt +index b20ad7782..6f8d36d02 100644 +--- a/src/frameworks/OpenDirectory/CMakeLists.txt ++++ b/src/frameworks/OpenDirectory/CMakeLists.txt +@@ -21,6 +21,7 @@ add_framework(OpenDirectory + + SOURCES + src/OpenDirectory.m ++ src/ODLocalDirectory.m + src/NSODSession.m + src/ODSession.m + src/NSODNode.m +diff --git a/src/frameworks/OpenDirectory/src/ODNode.m b/src/frameworks/OpenDirectory/src/ODNode.m +index 24243f915..5731f9783 100644 +--- a/src/frameworks/OpenDirectory/src/ODNode.m ++++ b/src/frameworks/OpenDirectory/src/ODNode.m +@@ -19,7 +19,89 @@ + + #import + ++#import "ODLocalDirectory.h" ++ + @implementation ODNode ++{ ++ NSString *_nodeName; ++} ++ +++ (instancetype)nodeWithSession:(ODSession *)session type:(ODNodeType)type error:(NSError **)error ++{ ++ return [[[self alloc] initWithSession: session type: type error: error] autorelease]; ++} ++ +++ (instancetype)nodeWithSession:(ODSession *)session name:(NSString *)name error:(NSError **)error ++{ ++ return [[[self alloc] initWithSession: session name: name error: error] autorelease]; ++} ++ ++- (instancetype)initWithSession:(ODSession *)session type:(ODNodeType)type error:(NSError **)error ++{ ++ if (error != NULL) ++ *error = nil; ++ if ((self = [super init]) != nil) ++ _nodeName = [ODLocalNodeNameForType(type) copy]; ++ return self; ++} ++ ++- (instancetype)initWithSession:(ODSession *)session name:(NSString *)name error:(NSError **)error ++{ ++ NSString *known = ODLocalNodeNameForName(name); ++ ++ if (error != NULL) ++ *error = known != nil ? nil : ODLocalError(kODErrorNodeUnknownName); ++ if (known == nil) { ++ [self release]; ++ return nil; ++ } ++ if ((self = [super init]) != nil) ++ _nodeName = [known copy]; ++ return self; ++} ++ ++- (void)dealloc ++{ ++ [_nodeName release]; ++ [super dealloc]; ++} ++ ++- (NSString *)nodeName ++{ ++ return _nodeName; ++} ++ ++- (NSArray *)subnodeNamesAndReturnError:(NSError **)error ++{ ++ if (error != NULL) ++ *error = nil; ++ return [_nodeName isEqualToString: ODLocalNodeName] ? @[] : @[ODLocalNodeName]; ++} ++ ++- (NSArray *)unreachableSubnodeNamesAndReturnError:(NSError **)error ++{ ++ if (error != NULL) ++ *error = nil; ++ return nil; ++} ++ ++- (NSArray *)supportedRecordTypesAndReturnError:(NSError **)error ++{ ++ if (error != NULL) ++ *error = nil; ++ return @[kODRecordTypeUsers]; ++} ++ ++- (ODRecord *)recordWithRecordType:(ODRecordType)recordType name:(NSString *)name attributes:(id)attributes error:(NSError **)error ++{ ++ if (error != NULL) ++ *error = nil; ++ if (!ODLocalRecordTypesIncludeUsers(recordType)) ++ return nil; ++ ++ NSDictionary *user = ODLocalUserAttributes(name); ++ return user != nil ? [ODLocalRecordCreate(user) autorelease] : nil; ++} + + - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector + { +diff --git a/src/frameworks/OpenDirectory/src/ODQuery.m b/src/frameworks/OpenDirectory/src/ODQuery.m +index 5b0361e37..91e5b3ebb 100644 +--- a/src/frameworks/OpenDirectory/src/ODQuery.m ++++ b/src/frameworks/OpenDirectory/src/ODQuery.m +@@ -19,7 +19,143 @@ + + #import + ++#import ++#import "ODLocalDirectory.h" ++ + @implementation ODQuery ++{ ++ BOOL _users; ++ NSString *_attribute; ++ ODMatchType _matchType; ++ NSArray *_queryValues; ++ NSInteger _maximumResults; ++} ++ +++ (ODQuery *)queryWithNode:(ODNode *)node forRecordTypes:(id)recordTypeOrList attribute:(ODAttributeType)attribute ++ matchType:(ODMatchType)matchType queryValues:(id)queryValueOrList ++ returnAttributes:(id)returnAttributeOrList maximumResults:(NSInteger)maximumResults error:(NSError **)error ++{ ++ return [[[self alloc] initWithNode: node forRecordTypes: recordTypeOrList attribute: attribute ++ matchType: matchType queryValues: queryValueOrList ++ returnAttributes: returnAttributeOrList maximumResults: maximumResults error: error] autorelease]; ++} ++ ++- (instancetype)initWithNode:(ODNode *)node forRecordTypes:(id)recordTypeOrList attribute:(ODAttributeType)attribute ++ matchType:(ODMatchType)matchType queryValues:(id)queryValueOrList ++ returnAttributes:(id)returnAttributeOrList maximumResults:(NSInteger)maximumResults error:(NSError **)error ++{ ++ if (error != NULL) ++ *error = nil; ++ if (node == nil) { ++ if (error != NULL) ++ *error = ODLocalError(kODErrorNodeUnknownName); ++ [self release]; ++ return nil; ++ } ++ if ((self = [super init]) != nil) { ++ NSMutableArray *values = [NSMutableArray array]; ++ NSArray *given = [queryValueOrList isKindOfClass: [NSArray class]] ? queryValueOrList ++ : queryValueOrList != nil ? @[queryValueOrList] : @[]; ++ ++ for (id value in given) { ++ if ([value isKindOfClass: [NSData class]]) ++ value = [[[NSString alloc] initWithData: value encoding: NSUTF8StringEncoding] autorelease]; ++ if ([value isKindOfClass: [NSString class]]) ++ [values addObject: value]; ++ } ++ ++ _users = ODLocalRecordTypesIncludeUsers(recordTypeOrList); ++ _attribute = [attribute copy]; ++ _matchType = matchType; ++ _queryValues = [values copy]; ++ _maximumResults = maximumResults; ++ } ++ return self; ++} ++ ++- (void)dealloc ++{ ++ [_attribute release]; ++ [_queryValues release]; ++ [super dealloc]; ++} ++ ++- (id)copyWithZone:(NSZone *)zone ++{ ++ ODQuery *copy = [[ODQuery allocWithZone: zone] init]; ++ ++ if (copy != nil) { ++ copy->_users = _users; ++ copy->_attribute = [_attribute copy]; ++ copy->_matchType = _matchType; ++ copy->_queryValues = [_queryValues copy]; ++ copy->_maximumResults = _maximumResults; ++ } ++ return copy; ++} ++ ++- (BOOL)_value:(NSString *)value matches:(NSString *)wanted ++{ ++ NSStringCompareOptions options = (_matchType & 0x0100) ? NSCaseInsensitiveSearch : 0; ++ ++ switch (_matchType & ~0x0100) { ++ case kODMatchEqualTo: ++ return [value compare: wanted options: options] == NSOrderedSame; ++ case kODMatchBeginsWith: ++ return [value rangeOfString: wanted options: options | NSAnchoredSearch].location != NSNotFound; ++ case kODMatchEndsWith: ++ return [value rangeOfString: wanted options: options | NSAnchoredSearch | NSBackwardsSearch].location != NSNotFound; ++ case kODMatchContains: ++ return [value rangeOfString: wanted options: options].location != NSNotFound; ++ default: ++ return NO; ++ } ++} ++ ++- (BOOL)_matches:(NSDictionary *)user ++{ ++ if (_matchType == kODMatchAny) ++ return YES; ++ ++ for (NSString *value in user[_attribute]) { ++ for (NSString *wanted in _queryValues) { ++ if ([self _value: value matches: wanted]) ++ return YES; ++ } ++ } ++ return NO; ++} ++ ++- (NSArray *)resultsAllowingPartial:(BOOL)allowPartial error:(NSError **)error ++{ ++ NSMutableArray *results = [NSMutableArray array]; ++ NSArray *candidates = @[]; ++ ++ if (error != NULL) ++ *error = nil; ++ if (!_users || (_attribute == nil && _matchType != kODMatchAny)) ++ return results; ++ ++ if (_matchType == kODMatchEqualTo && [_attribute isEqualToString: kODAttributeTypeRecordName]) { ++ NSMutableArray *named = [NSMutableArray array]; ++ for (NSString *name in _queryValues) { ++ NSDictionary *user = ODLocalUserAttributes(name); ++ if (user != nil) ++ [named addObject: user]; ++ } ++ candidates = named; ++ } else { ++ candidates = ODLocalAllUserAttributes(); ++ } ++ ++ for (NSDictionary *user in candidates) { ++ if (_maximumResults > 0 && [results count] >= (NSUInteger)_maximumResults) ++ break; ++ if ([self _matches: user]) ++ [results addObject: [ODLocalRecordCreate(user) autorelease]]; ++ } ++ return results; ++} + + - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector + { +diff --git a/src/frameworks/OpenDirectory/src/ODRecord.m b/src/frameworks/OpenDirectory/src/ODRecord.m +index 5e207c4bb..5c7bd9a14 100644 +--- a/src/frameworks/OpenDirectory/src/ODRecord.m ++++ b/src/frameworks/OpenDirectory/src/ODRecord.m +@@ -19,7 +19,63 @@ + + #import + ++#import "ODLocalDirectory.h" ++ + @implementation ODRecord ++{ ++ NSDictionary *_attributes; ++} ++ ++ODRecord *ODLocalRecordCreate(NSDictionary *attributes) ++{ ++ ODRecord *record = [[ODRecord alloc] init]; ++ ++ if (record != nil) ++ record->_attributes = [attributes copy]; ++ return record; ++} ++ ++- (void)dealloc ++{ ++ [_attributes release]; ++ [super dealloc]; ++} ++ ++- (NSString *)recordType ++{ ++ return [_attributes[@"dsAttrTypeStandard:RecordType"] firstObject]; ++} ++ ++- (NSString *)recordName ++{ ++ return [_attributes[@"dsAttrTypeStandard:RecordName"] firstObject]; ++} ++ ++- (NSArray *)valuesForAttribute:(ODAttributeType)attribute error:(NSError **)error ++{ ++ if (error != NULL) ++ *error = nil; ++ if (attribute == nil) ++ return nil; ++ return _attributes[attribute] ?: @[]; ++} ++ ++- (NSDictionary *)recordDetailsForAttributes:(NSArray *)attributes error:(NSError **)error ++{ ++ if (error != NULL) ++ *error = nil; ++ if (attributes == nil || [attributes containsObject: @"dsAttributesAll"] ++ || [attributes containsObject: @"dsAttributesStandardAll"]) ++ return _attributes; ++ ++ NSMutableDictionary *details = [NSMutableDictionary dictionary]; ++ for (id attribute in attributes) { ++ NSArray *values = _attributes[attribute]; ++ if (values != nil) ++ details[attribute] = values; ++ } ++ return details; ++} + + - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector + { +diff --git a/src/frameworks/OpenDirectory/src/ODSession.m b/src/frameworks/OpenDirectory/src/ODSession.m +index aff236828..c9e628d43 100644 +--- a/src/frameworks/OpenDirectory/src/ODSession.m ++++ b/src/frameworks/OpenDirectory/src/ODSession.m +@@ -21,8 +21,48 @@ + + #import + ++#import "ODLocalDirectory.h" ++ + @implementation ODSession + +++ (ODSession *)defaultSession ++{ ++ static ODSession *shared; ++ static dispatch_once_t once; ++ ++ dispatch_once(&once, ^{ ++ shared = [[ODSession alloc] init]; ++ }); ++ return shared; ++} ++ +++ (instancetype)sessionWithOptions:(NSDictionary *)options error:(NSError **)error ++{ ++ return [[[self alloc] initWithOptions: options error: error] autorelease]; ++} ++ ++- (instancetype)initWithOptions:(NSDictionary *)options error:(NSError **)error ++{ ++ if (error != NULL) ++ *error = nil; ++ ++ // only the local directory exists; there is no proxy to connect through ++ if ([options count] != 0) { ++ if (error != NULL) ++ *error = ODLocalError(kODErrorSessionProxyCommunicationError); ++ [self release]; ++ return nil; ++ } ++ return [self init]; ++} ++ ++- (NSArray *)nodeNamesAndReturnError:(NSError **)error ++{ ++ if (error != NULL) ++ *error = nil; ++ return @[ODLocalNodeName]; ++} ++ + - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector + { + return [NSMethodSignature signatureWithObjCTypes: "v@:"]; +diff --git a/src/frameworks/OpenGL/CMakeLists.txt b/src/frameworks/OpenGL/CMakeLists.txt +index deca16b87..d75d5e22d 100644 +--- a/src/frameworks/OpenGL/CMakeLists.txt ++++ b/src/frameworks/OpenGL/CMakeLists.txt +@@ -31,9 +31,14 @@ add_framework(OpenGL + system + GL + GLU +- EGL ++ # darling-lol M57bo: EGL is deliberately NOT a link dependency. Linking ++ # it here puts the native Mesa EGL in the dependency symbol scope of ++ # OpenGL.framework's dependents (ANGLE's libGLESv2), so Chromium's EGL ++ # lookups resolve to the native EGL instead of ANGLE's. OpenGL.c loads ++ # it privately with dlopen(RTLD_LOCAL) instead. + CoreFoundation + CoreGraphics ++ IOSurface + ) + + set_property(TARGET OpenGL APPEND_STRING PROPERTY LINK_FLAGS +diff --git a/src/frameworks/OpenGL/OpenGL.c b/src/frameworks/OpenGL/OpenGL.c +index ffd7fe054..4801e5521 100644 +--- a/src/frameworks/OpenGL/OpenGL.c ++++ b/src/frameworks/OpenGL/OpenGL.c +@@ -2,11 +2,22 @@ + #include // for fprintf(stderr, "unimplemented") + + #include +- ++#include ++#include ++#include ++#include ++#include ++#include ++extern long __lolsys_syscall(long,long,long,long,long,long,int); ++ ++#define GL_GLEXT_PROTOTYPES 1 + #include ++#include + #include + #include ++#include + #include ++#include + + // Try to get the right (generic) type definitions. + // In particular, we really want EGLNativeDisplayType to be void *, +@@ -22,8 +33,73 @@ + #define APPLE + #define __APPLE__ + ++/* darling-lol M57bo: load the native EGL privately. ++ * ++ * OpenGL.framework is a load-time dependency of ANGLE's libGLESv2 (its GL ++ * backend). If the native EGL were linked here, its egl* symbols would land in ++ * libGLESv2's dependency symbol scope and satisfy Chromium's EGL lookups, so ++ * Chromium would pair Darling's Mesa EGL with ANGLE's GL - eglMakeCurrent would ++ * never make an ANGLE gl::Context current (GL_MAX_VERTEX_ATTRIBS = 0, no ++ * frames). Resolving EGL through a private RTLD_LOCAL dlopen keeps it out of ++ * dependents' scope, so Chromium falls back to ANGLE's own EGL. */ ++#include ++ ++static EGLDisplay (*p_eglGetDisplay)(EGLNativeDisplayType); ++static EGLDisplay (*p_eglGetPlatformDisplay)(EGLenum, void *, const EGLAttrib *); ++static EGLBoolean (*p_eglInitialize)(EGLDisplay, EGLint *, EGLint *); ++static EGLBoolean (*p_eglQuerySurface)(EGLDisplay,EGLSurface,EGLint,EGLint*); ++static EGLBoolean (*p_eglGetConfigAttrib)(EGLDisplay,EGLConfig,EGLint,EGLint*); ++static EGLBoolean (*p_eglChooseConfig)(EGLDisplay, const EGLint *, EGLConfig *, EGLint, EGLint *); ++static EGLBoolean (*p_eglBindAPI)(EGLenum); ++static EGLSurface (*p_eglCreateWindowSurface)(EGLDisplay, EGLConfig, EGLNativeWindowType, const EGLint *); ++static EGLBoolean (*p_eglDestroySurface)(EGLDisplay, EGLSurface); ++static EGLBoolean (*p_eglMakeCurrent)(EGLDisplay, EGLSurface, EGLSurface, EGLContext); ++static EGLContext (*p_eglCreateContext)(EGLDisplay, EGLConfig, EGLContext, const EGLint *); ++static EGLBoolean (*p_eglDestroyContext)(EGLDisplay, EGLContext); ++static EGLBoolean (*p_eglSwapBuffers)(EGLDisplay, EGLSurface); ++static EGLBoolean (*p_eglSwapInterval)(EGLDisplay, EGLint); ++ ++static int native_egl_loaded; ++static pthread_mutex_t native_egl_mutex = PTHREAD_MUTEX_INITIALIZER; ++ ++static void ensure_native_egl(void) { ++ if (native_egl_loaded) ++ return; ++ pthread_mutex_lock(&native_egl_mutex); ++ if (!native_egl_loaded) { ++ void *h = dlopen("/usr/lib/native/libEGL.dylib", RTLD_NOW | RTLD_LOCAL); ++ if (h) { ++ p_eglGetDisplay = (EGLDisplay (*)(EGLNativeDisplayType)) dlsym(h, "eglGetDisplay"); ++ p_eglGetPlatformDisplay = (EGLDisplay (*)(EGLenum, void *, const EGLAttrib *)) dlsym(h, "eglGetPlatformDisplay"); ++ p_eglInitialize = (EGLBoolean (*)(EGLDisplay, EGLint *, EGLint *)) dlsym(h, "eglInitialize"); ++ p_eglQuerySurface=(EGLBoolean(*)(EGLDisplay,EGLSurface,EGLint,EGLint*))dlsym(h,"eglQuerySurface"); ++ p_eglGetConfigAttrib=(EGLBoolean(*)(EGLDisplay,EGLConfig,EGLint,EGLint*))dlsym(h,"eglGetConfigAttrib"); ++ p_eglChooseConfig = (EGLBoolean (*)(EGLDisplay, const EGLint *, EGLConfig *, EGLint, EGLint *)) dlsym(h, "eglChooseConfig"); ++ p_eglBindAPI = (EGLBoolean (*)(EGLenum)) dlsym(h, "eglBindAPI"); ++ p_eglCreateWindowSurface = (EGLSurface (*)(EGLDisplay, EGLConfig, EGLNativeWindowType, const EGLint *)) dlsym(h, "eglCreateWindowSurface"); ++ p_eglDestroySurface = (EGLBoolean (*)(EGLDisplay, EGLSurface)) dlsym(h, "eglDestroySurface"); ++ p_eglMakeCurrent = (EGLBoolean (*)(EGLDisplay, EGLSurface, EGLSurface, EGLContext)) dlsym(h, "eglMakeCurrent"); ++ p_eglCreateContext = (EGLContext (*)(EGLDisplay, EGLConfig, EGLContext, const EGLint *)) dlsym(h, "eglCreateContext"); ++ p_eglDestroyContext = (EGLBoolean (*)(EGLDisplay, EGLContext)) dlsym(h, "eglDestroyContext"); ++ p_eglSwapBuffers = (EGLBoolean (*)(EGLDisplay, EGLSurface)) dlsym(h, "eglSwapBuffers"); ++ p_eglSwapInterval = (EGLBoolean (*)(EGLDisplay, EGLint)) dlsym(h, "eglSwapInterval"); ++ native_egl_loaded = 1; ++ } ++ } ++ pthread_mutex_unlock(&native_egl_mutex); ++} ++ + static EGLDisplay display; + ++static EGLDisplay x11EGLDisplay(EGLNativeDisplayType native_display) { ++ /* CGS/AppKit supply Xlib Display pointers. Mesa's automatic platform ++ * selection can choose Wayland on a Wayland desktop and interpret the ++ * Xlib pointer as a wl_display, crashing during eglInitialize. */ ++ if (p_eglGetPlatformDisplay) ++ return p_eglGetPlatformDisplay(0x31D5 /* EGL_PLATFORM_X11 */, native_display, NULL); ++ return p_eglGetDisplay(native_display); ++} ++ + static EGLConfig config; + static int num_config; + +@@ -48,10 +124,18 @@ struct _CGLContextObj { + GLuint retain_count; + pthread_mutex_t lock; + EGLContext egl_context; ++ EGLDisplay egl_display; ++ EGLConfig egl_config; + EGLSurface egl_surface; + // EGL has no function for getting the current swap interval, + // so we need to save the last set value. The default is 1. + int swap_interval; ++ EGLSurface native_present_surface; ++ EGLContext native_present_context; ++ DarlingNativePresentTarget *native_present_target; ++ uint32_t native_present_window; ++ uint64_t native_next_present_ns; ++ struct DarlingAsyncPresent *native_async; + }; + + struct _CGLPixelFormatObj { +@@ -59,6 +143,24 @@ struct _CGLPixelFormatObj { + CGLPixelFormatAttribute *attributes; + }; + ++typedef struct { ++ IOSurfaceID surfaceID; ++ CGLContextObj context; ++ GLenum target; ++ GLenum format; ++ GLenum type; ++ GLuint texture; ++ GLuint plane; ++ IOSurfaceRef surface; /* non-owning, used only to invalidate this binding */ ++} CGLIOSurfaceBinding; ++ ++static void stopAsyncPresent(CGLContextObj context); ++ ++static CGLIOSurfaceBinding initialSurfaceBindings[256]; ++static CGLIOSurfaceBinding *ioSurfaceBindings = initialSurfaceBindings; ++static unsigned ioSurfaceBindingCount, ioSurfaceBindingCapacity = 256; ++static pthread_mutex_t ioSurfaceBindingMutex = PTHREAD_MUTEX_INITIALIZER; ++ + static inline int attribute_has_argument(CGLPixelFormatAttribute attr) { + switch (attr) { + case kCGLPFAAuxBuffers: +@@ -69,6 +171,9 @@ static inline int attribute_has_argument(CGLPixelFormatAttribute attr) { + case kCGLPFAAccumSize: + case kCGLPFARendererID: + case kCGLPFADisplayMask: ++ case kCGLPFAOpenGLProfile: ++ case kCGLPFASampleBuffers: ++ case kCGLPFASamples: + return 1; + default: + return 0; +@@ -78,6 +183,7 @@ static inline int attribute_has_argument(CGLPixelFormatAttribute attr) { + __attribute__((constructor)) + static void _CGLInitialize(void) + { ++ ensure_native_egl(); + g_displays = CFDictionaryCreateMutable(NULL, 0, NULL, NULL); + } + +@@ -93,41 +199,71 @@ static int attributes_count(const CGLPixelFormatAttribute *attrs) { + + CGLError CGLRegisterNativeDisplay(void *native_display) { + +- display = eglGetDisplay(native_display); ++ ensure_native_egl(); ++ display = x11EGLDisplay(native_display); + + if (display == EGL_NO_DISPLAY) { + return kCGLBadConnection; + } + +- eglInitialize(display, NULL, NULL); +- eglChooseConfig(display, attribute_list, &config, 1, &num_config); ++ p_eglInitialize(display, NULL, NULL); ++ p_eglChooseConfig(display, attribute_list, &config, 1, &num_config); + +- eglBindAPI(EGL_OPENGL_API); ++ p_eglBindAPI(EGL_OPENGL_API); + + return kCGLNoError; + } + ++/* darling-lol M57: processes that create a CGL context without first calling ++ * CGLRegisterNativeDisplay (e.g. Chromium's GPU process) previously got ++ * display==0/config==0 and CGLCreateContext always failed. Lazily initialise a ++ * default EGL display so context creation can proceed. */ ++static void ensureDefaultDisplay(void) { ++ ensure_native_egl(); ++ if (display != EGL_NO_DISPLAY && display != NULL && config != NULL) ++ return; ++ ++ void *nativeDisplay=_CGSNativeDisplay(CGSMainConnectionID()); ++ EGLDisplay disp = x11EGLDisplay(nativeDisplay?nativeDisplay:EGL_DEFAULT_DISPLAY); ++ if (disp == EGL_NO_DISPLAY) { ++ fprintf(stderr, "M58DG ensureDefaultDisplay: eglGetDisplay(DEFAULT)=NO_DISPLAY (p_eglGetDisplay=%p)\n", (void*) p_eglGetDisplay); ++ return; ++ } ++ ++ EGLConfig cfg = NULL; ++ EGLBoolean m58dg_ini = p_eglInitialize(disp, NULL, NULL); ++ EGLBoolean m58dg_ch = p_eglChooseConfig(disp, attribute_list, &cfg, 1, &num_config); ++ fprintf(stderr, "M58DG ensureDefaultDisplay: disp=%p eglInitialize=%d eglChooseConfig=%d cfg=%p num_config=%d\n", ++ (void*) disp, (int) m58dg_ini, (int) m58dg_ch, (void*) cfg, num_config); ++ if (m58dg_ini && m58dg_ch && cfg != NULL) { ++ display = disp; ++ config = cfg; ++ p_eglBindAPI(EGL_OPENGL_API); ++ } ++} ++ + static struct _CGLDisplay* getCGLDisplay(CGSConnectionID cid) + { + struct _CGLDisplay* rv; + ++ ensure_native_egl(); + pthread_mutex_lock(&g_displaysMutex); + rv = (struct _CGLDisplay*) CFDictionaryGetValue(g_displays, (const void*)(unsigned long) cid); + pthread_mutex_unlock(&g_displaysMutex); + + if (!rv) + { +- EGLDisplay disp = eglGetDisplay(_CGSNativeDisplay(cid)); ++ EGLDisplay disp = x11EGLDisplay(_CGSNativeDisplay(cid)); + if (disp == EGL_NO_DISPLAY) + return NULL; + + rv = (struct _CGLDisplay*) malloc(sizeof(*rv)); + rv->display = disp; + +- eglInitialize(rv->display, NULL, NULL); +- eglChooseConfig(rv->display, attribute_list, &rv->config, 1, &rv->num_config); ++ p_eglInitialize(rv->display, NULL, NULL); ++ p_eglChooseConfig(rv->display, attribute_list, &rv->config, 1, &rv->num_config); + +- eglBindAPI(EGL_OPENGL_API); ++ p_eglBindAPI(EGL_OPENGL_API); + + pthread_mutex_lock(&g_displaysMutex); + CFDictionaryAddValue(g_displays, (const void*)(unsigned long) cid, rv); +@@ -139,6 +275,8 @@ static struct _CGLDisplay* getCGLDisplay(CGSConnectionID cid) + + CGLError CGLSetSurface(CGLContextObj gl, CGSConnectionID cid, CGSWindowID wid, CGSSurfaceID sid) + { ++ if (!gl) ++ return kCGLBadContext; + struct _CGLDisplay* disp = getCGLDisplay(cid); + if (!disp) + return kCGLBadConnection; +@@ -152,7 +290,7 @@ CGLError CGLSetSurface(CGLContextObj gl, CGSConnectionID cid, CGSWindowID wid, C + if (!window) + return kCGLBadWindow; + +- gl->egl_surface = eglCreateWindowSurface(disp->display, disp->config, window, NULL); ++ gl->egl_surface = p_eglCreateWindowSurface(gl->egl_display, gl->egl_config, window, NULL); + if (gl->egl_surface == EGL_NO_SURFACE) + return kCGLBadState; + return kCGLNoError; +@@ -161,7 +299,7 @@ CGLError CGLSetSurface(CGLContextObj gl, CGSConnectionID cid, CGSWindowID wid, C + CGLWindowRef CGLGetWindow(void *native_window) { + + EGLNativeWindowType window = (EGLNativeWindowType) native_window; +- EGLSurface surface = eglCreateWindowSurface(display, config, window, NULL); ++ EGLSurface surface = p_eglCreateWindowSurface(display, config, window, NULL); + + if (surface == EGL_NO_SURFACE) { + return NULL; +@@ -171,14 +309,18 @@ CGLWindowRef CGLGetWindow(void *native_window) { + } + + CGL_EXPORT void CGLDestroyWindow(CGLWindowRef window) { +- eglDestroySurface(display, (EGLSurface) window); ++ p_eglDestroySurface(display, (EGLSurface) window); + } + + CGL_EXPORT CGLError CGLContextMakeCurrentAndAttachToWindow(CGLContextObj context, CGLWindowRef window) { + if (!context) + return kCGLBadContext; ++ EGLSurface oldSurface = context->egl_surface; + context->egl_surface = (EGLSurface) window; +- return CGLSetCurrentContext(context); ++ CGLError result = CGLSetCurrentContext(context); ++ if (result != kCGLNoError) ++ context->egl_surface = oldSurface; ++ return result; + } + + static pthread_key_t current_context_key; +@@ -198,18 +340,42 @@ CGLContextObj CGLGetCurrentContext(void) { + return pthread_getspecific(key); + } + ++/* darling-lol M57: CGLGetVirtualScreen/CGLSetVirtualScreen are used by ++ * Chromium's CGL backend; Darling's OpenGL did not provide them (lazy-bind ++ * abort: "Symbol not found: _CGLGetVirtualScreen"). A single virtual screen. */ ++CGLError CGLGetVirtualScreen(CGLContextObj context, GLint *screen) { ++ if (!context) return kCGLBadContext; ++ if (!screen) return kCGLBadAddress; ++ *screen = 0; ++ return kCGLNoError; ++} ++ ++CGLError CGLSetVirtualScreen(CGLContextObj context, GLint screen) { ++ if (!context) return kCGLBadContext; ++ return screen == 0 ? kCGLNoError : kCGLBadValue; ++} ++ + CGLError CGLSetCurrentContext(CGLContextObj context) { ++ ensure_native_egl(); + pthread_key_t key = get_current_context_key(); +- pthread_setspecific(key, context); ++ CGLContextObj old = pthread_getspecific(key); ++ EGLDisplay currentDisplay = context ? context->egl_display : old ? old->egl_display : display; ++ if (!p_eglMakeCurrent) ++ return kCGLBadConnection; ++ if (!context && !old && !currentDisplay) ++ return kCGLNoError; + ++ EGLBoolean success; + if (context != NULL) { + EGLSurface surface = context->egl_surface; +- eglMakeCurrent(display, surface, surface, context->egl_context); ++ success = p_eglMakeCurrent(currentDisplay, surface, surface, context->egl_context); + } else { +- eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); ++ success = p_eglMakeCurrent(currentDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + } +- +- return kCGLNoError; // FIXME ++ if (!success) ++ return kCGLBadState; ++ pthread_setspecific(key, context); ++ return kCGLNoError; + } + + CGLError CGLSetFullScreen(CGLContextObj ctx) { +@@ -223,11 +389,13 @@ CGLError CGLChoosePixelFormat( + CGLPixelFormatObj *result, + GLint *number_of_screens + ) { ++ if (!attrs || !result || !number_of_screens) ++ return kCGLBadAddress; + CGLPixelFormatObj format = malloc(sizeof(struct _CGLPixelFormatObj)); + int count = attributes_count(attrs); + + format->retain_count = 1; +- format->attributes = malloc(sizeof(CGLPixelFormatAttribute) * count); ++ format->attributes = calloc(count + 1, sizeof(CGLPixelFormatAttribute)); + for (int i = 0; i < count; i++) { + format->attributes[i] = attrs[i]; + } +@@ -251,6 +419,10 @@ CGLError CGLDescribePixelFormat( + CGLPixelFormatAttribute attr, + GLint *value + ) { ++ if (!format) return kCGLBadPixelFormat; ++ if (!value) return kCGLBadAddress; ++ if (sreen_num != 0) return kCGLBadValue; ++ if (attr == kCGLPFAVirtualScreenCount) { *value = 1; return kCGLNoError; } + for (int i = 0; format->attributes[i] != 0; i++) { + int has_arg = attribute_has_argument(format->attributes[i]); + +@@ -304,24 +476,67 @@ GLuint CGLGetPixelFormatRetainCount(CGLPixelFormatObj pixelFormat) { + } + + CGLError CGLCreateContext(CGLPixelFormatObj pixelFormat, CGLContextObj share, CGLContextObj *resultp) { ++ /* Cocotron window compositing requests the default format with NULL. */ ++ if (!resultp) ++ return kCGLBadAddress; ++ *resultp = NULL; ++ ensureDefaultDisplay(); ++ /* eglBindAPI is thread-local, even when the display is already initialized. */ ++ if (!display || !config || !p_eglBindAPI || !p_eglBindAPI(EGL_OPENGL_API)) ++ return kCGLBadConnection; + + EGLContext egl_share = EGL_NO_CONTEXT; ++ EGLDisplay contextDisplay = share ? share->egl_display : display; ++ EGLConfig contextConfig = share ? share->egl_config : config; + if (share != NULL) { + egl_share = share->egl_context; + } +- EGLContext egl_context = eglCreateContext(display, config, egl_share, NULL); ++ GLint profile = 0; ++ if (pixelFormat) ++ CGLDescribePixelFormat(pixelFormat, 0, kCGLPFAOpenGLProfile, &profile); ++ EGLint contextAttributes[] = { ++ 0x3098 /* EGL_CONTEXT_MAJOR_VERSION */, 3, ++ 0x30FB /* EGL_CONTEXT_MINOR_VERSION */, 2, ++ 0x30FD /* EGL_CONTEXT_OPENGL_PROFILE_MASK */, 1 /* CORE_PROFILE_BIT */, ++ EGL_NONE ++ }; ++ if (profile == 0x4100) { ++ contextAttributes[1] = 4; ++ contextAttributes[3] = 1; ++ } else if (profile && profile != 0x1000 && profile != 0x3200) { ++ return kCGLBadValue; ++ } ++ EGLContext egl_context = p_eglCreateContext(contextDisplay, contextConfig, egl_share, ++ profile == 0x3200 || profile == 0x4100 ? contextAttributes : NULL); + + if (egl_context == EGL_NO_CONTEXT) { ++ fprintf(stderr, "M58DG CGLCreateContext FAILED: display=%p config=%p share=%p p_eglCreateContext=%p\n", ++ (void*) display, (void*) config, (void*) egl_share, (void*) p_eglCreateContext); + return kCGLBadContext; + } + ++ { ++ static int m58dg_ok = 0; ++ if (m58dg_ok++ < 5) ++ fprintf(stderr, "M58DG CGLCreateContext OK display=%p config=%p ctx=%p\n", ++ (void*) display, (void*) config, (void*) egl_context); ++ } ++ + CGLContextObj context = malloc(sizeof(struct _CGLContextObj)); + + context->retain_count = 1; + pthread_mutex_init(&(context->lock), NULL); + context->egl_context = egl_context; ++ context->egl_display = contextDisplay; ++ context->egl_config = contextConfig; + context->egl_surface = NULL; + context->swap_interval = 1; ++ context->native_present_surface=EGL_NO_SURFACE; ++ context->native_present_context=EGL_NO_CONTEXT; ++ context->native_present_target=NULL; ++ context->native_present_window=0; ++ context->native_next_present_ns=0; ++ context->native_async=NULL; + + *resultp = context; + +@@ -333,7 +548,7 @@ CGLContextObj CGLRetainContext(CGLContextObj context) { + return NULL; + } + +- context->retain_count++; ++ __sync_add_and_fetch(&context->retain_count, 1); + return context; + } + +@@ -342,11 +557,17 @@ void CGLReleaseContext(CGLContextObj context) { + return; + } + +- context->retain_count--; +- +- if (context->retain_count != 0) { ++ pthread_mutex_lock(&ioSurfaceBindingMutex); ++ if (__sync_sub_and_fetch(&context->retain_count, 1) != 0) { ++ pthread_mutex_unlock(&ioSurfaceBindingMutex); + return; + } ++ for (unsigned i = 0; i < ioSurfaceBindingCount; ) { ++ if (ioSurfaceBindings[i].context == context) ++ ioSurfaceBindings[i] = ioSurfaceBindings[--ioSurfaceBindingCount]; ++ else ++i; ++ } ++ pthread_mutex_unlock(&ioSurfaceBindingMutex); + + if (CGLGetCurrentContext() == context) { + CGLSetCurrentContext(NULL); +@@ -354,7 +575,11 @@ void CGLReleaseContext(CGLContextObj context) { + + pthread_mutex_destroy(&(context->lock)); + +- eglDestroyContext(display, context->egl_context); ++ stopAsyncPresent(context); ++ if(context->native_present_context!=EGL_NO_CONTEXT)p_eglDestroyContext(context->egl_display,context->native_present_context); ++ if(context->native_present_surface!=EGL_NO_SURFACE)p_eglDestroySurface(context->egl_display,context->native_present_surface); ++ if(context->native_present_target)__lolsys_syscall((long)context->native_present_target,sizeof(DarlingNativePresentTarget),0,0,0,0,11); ++ p_eglDestroyContext(context->egl_display, context->egl_context); + + free(context); + } +@@ -383,9 +608,323 @@ CGLError CGLUnlockContext(CGLContextObj context) { + return kCGLNoError; + } + ++static int swapIntervalRequested; ++ + CGLError CGLFlushDrawable(CGLContextObj context) { +- eglSwapBuffers(display,context->egl_surface); +- return kCGLNoError; ++ if (!context) ++ return kCGLBadContext; ++ /* darling-lol: EGL defaults to a swap interval of 1. When the window is covered or the compositor throttles it ++ * (Xwayland answers about once a second), every swap blocks that long and the game's frame rate collapses to ~1 fps. ++ * macOS apps get an unthrottled flush unless they ask for vsync through kCGLCPSwapInterval, which is handled in ++ * CGLSetParameter; apply "no vsync" once per context here, while it is current. */ ++ static CGLContextObj swapIntervalContext; ++ if (!swapIntervalRequested && context != swapIntervalContext && p_eglSwapInterval && CGLGetCurrentContext() == context) { ++ p_eglSwapInterval(context->egl_display, 0); ++ swapIntervalContext = context; ++ } ++ return p_eglSwapBuffers(context->egl_display, context->egl_surface) ? kCGLNoError : kCGLBadState; ++} ++ ++static GLenum textureBindingEnum(GLenum target) { ++ switch (target) { ++ case GL_TEXTURE_2D: return GL_TEXTURE_BINDING_2D; ++#ifdef GL_TEXTURE_RECTANGLE ++ case GL_TEXTURE_RECTANGLE: return GL_TEXTURE_BINDING_RECTANGLE; ++#endif ++ default: return 0; ++ } ++} ++ ++/* macOS exposes IOSurfaces to CGL as texture storage. Mesa/EGL does not have ++ * that CGL entry point, but Chromium only needs the pixels to reach the bound ++ * texture. Upload from the shared IOSurface mapping synchronously; this also ++ * preserves the cross-process IOSurface transport implemented by our shim. */ ++static uint64_t nativeMonotonicNS(void); ++static uint32_t nativeUploadCount, nativeUploadMicroseconds; ++CGLError CGLTexImageIOSurface2D(CGLContextObj context, GLenum target, ++ GLenum internalFormat, GLsizei width, GLsizei height, GLenum format, ++ GLenum type, IOSurfaceRef ioSurface, GLuint plane) ++{ ++ if (!context) ++ return kCGLBadContext; ++ if (!ioSurface) ++ return kCGLBadAddress; ++ ++ void *pixels = IOSurfaceGetBaseAddressOfPlane(ioSurface, plane); ++ size_t bytesPerRow = IOSurfaceGetBytesPerRowOfPlane(ioSurface, plane); ++ size_t bytesPerElement = IOSurfaceGetBytesPerElementOfPlane(ioSurface, plane); ++ if (!pixels || !bytesPerRow || !bytesPerElement) ++ return kCGLBadValue; ++ ++ CGLContextObj oldContext = CGLGetCurrentContext(); ++ if (oldContext != context) { ++ CGLError result = CGLSetCurrentContext(context); ++ if (result != kCGLNoError) ++ return result; ++ } ++ ++ uint64_t uploadStarted = nativeMonotonicNS(); ++ GLint oldRowLength = 0; ++ GLint oldAlignment = 0; ++ GLint boundTexture = 0; ++ /* ANGLE can leave an unrelated probe error pending before calling this ++ * CGL API. CGLTexImageIOSurface2D reports only its own operation. */ ++ while (glGetError() != GL_NO_ERROR) {} ++ glGetIntegerv(GL_UNPACK_ROW_LENGTH, &oldRowLength); ++ glGetIntegerv(GL_UNPACK_ALIGNMENT, &oldAlignment); ++ GLenum bindingEnum = textureBindingEnum(target); ++ if (bindingEnum) ++ glGetIntegerv(bindingEnum, &boundTexture); ++ glPixelStorei(GL_UNPACK_ALIGNMENT, 1); ++ glPixelStorei(GL_UNPACK_ROW_LENGTH, (GLint)(bytesPerRow / bytesPerElement)); ++ GLint allocatedWidth=0,allocatedHeight=0,allocatedFormat=0; ++ glGetTexLevelParameteriv(target,0,GL_TEXTURE_WIDTH,&allocatedWidth); ++ glGetTexLevelParameteriv(target,0,GL_TEXTURE_HEIGHT,&allocatedHeight); ++ glGetTexLevelParameteriv(target,0,GL_TEXTURE_INTERNAL_FORMAT,&allocatedFormat); ++ if(allocatedWidth==width && allocatedHeight==height && allocatedFormat==(GLint)internalFormat) ++ glTexSubImage2D(target,0,0,0,width,height,format,type,pixels); ++ else ++ glTexImage2D(target, 0, (GLint)internalFormat, width, height, 0, ++ format, type, pixels); ++ GLenum error = glGetError(); ++ glPixelStorei(GL_UNPACK_ROW_LENGTH, oldRowLength); ++ glPixelStorei(GL_UNPACK_ALIGNMENT, oldAlignment); ++ ++ if (error == GL_NO_ERROR && boundTexture != 0) { ++ CGLIOSurfaceBinding binding = { ++ IOSurfaceGetID(ioSurface), context, target, format, type, ++ (GLuint)boundTexture, plane, ioSurface ++ }; ++ pthread_mutex_lock(&ioSurfaceBindingMutex); ++ unsigned slot = ioSurfaceBindingCount; ++ for (unsigned i = 0; i < ioSurfaceBindingCount; ++i) { ++ if (ioSurfaceBindings[i].surfaceID == binding.surfaceID) { ++ slot = i; ++ break; ++ } ++ } ++ if (slot == ioSurfaceBindingCapacity) { ++ unsigned capacity = ioSurfaceBindingCapacity * 2; ++ CGLIOSurfaceBinding *larger = malloc(capacity * sizeof(*larger)); ++ if (larger) { ++ memcpy(larger, ioSurfaceBindings, ioSurfaceBindingCount * sizeof(*larger)); ++ if (ioSurfaceBindings != initialSurfaceBindings) free(ioSurfaceBindings); ++ ioSurfaceBindings = larger; ++ ioSurfaceBindingCapacity = capacity; ++ } ++ } ++ if (slot < ioSurfaceBindingCapacity) { ++ ioSurfaceBindings[slot] = binding; ++ if (slot == ioSurfaceBindingCount) ++ioSurfaceBindingCount; ++ } else error = GL_OUT_OF_MEMORY; ++ pthread_mutex_unlock(&ioSurfaceBindingMutex); ++ } ++ ++ CGLError restore = kCGLNoError; ++ if (oldContext != context) ++ restore = CGLSetCurrentContext(oldContext); ++ if (error != GL_NO_ERROR) ++ fprintf(stderr, "CGLTexImageIOSurface2D glTexImage2D error=0x%x target=0x%x format=0x%x type=0x%x %dx%d\n", ++ error, target, format, type, width, height); ++ __atomic_fetch_add(&nativeUploadCount,1,__ATOMIC_RELAXED); ++ __atomic_fetch_add(&nativeUploadMicroseconds,(uint32_t)((nativeMonotonicNS()-uploadStarted)/1000),__ATOMIC_RELAXED); ++ return error == GL_NO_ERROR ? restore : kCGLBadState; ++} ++ ++/* Called before IOSurface storage is destroyed; no owning surface reference. */ ++void CGLForgetIOSurface(IOSurfaceRef surface) { ++ pthread_mutex_lock(&ioSurfaceBindingMutex); ++ for (unsigned i = 0; i < ioSurfaceBindingCount; ) { ++ if (ioSurfaceBindings[i].surface == surface) ++ ioSurfaceBindings[i] = ioSurfaceBindings[--ioSurfaceBindingCount]; ++ else ++i; ++ } ++ pthread_mutex_unlock(&ioSurfaceBindingMutex); ++} ++ ++#include "CGLAsyncPresent.inc" ++ ++/* Present a flattened compositor texture directly. The native transport ++ * acknowledges suspension of CPU writes before the first GL swap. Fallback ++ * leaves the existing readback path available and restores all GL bindings. */ ++CGL_EXPORT int CGLPresentIOSurface(IOSurfaceRef surface) { ++ if(!surface || !getenv("RCLOL_GPU_DIRECT_PRESENT") || IOSurfaceGetPlaneCount(surface)>1 || ++ IOSurfaceGetBytesPerElement(surface)!=4)return 0; ++ size_t width=IOSurfaceGetWidth(surface),height=IOSurfaceGetHeight(surface); ++ if(width<256 || height<256)return 0; ++ CGLIOSurfaceBinding binding={0};IOSurfaceID id=IOSurfaceGetID(surface); ++ pthread_mutex_lock(&ioSurfaceBindingMutex); ++ for(unsigned i=0;inative_present_target){ ++ char path[128];snprintf(path,sizeof(path),"/dev/shm/darling-ca-gpu-target-%u-%zu-%zu",getuid(),width,height); ++ int fd=(int)__lolsys_syscall((long)path,2,0,0,0,0,2);if(getenv("RCLOL_GPU_DIRECT_TRACE"))fprintf(stderr,"GPU_DIRECT target_open=%d uid=%u size=%zux%zu\n",fd,getuid(),width,height);if(fd>=0){ ++ struct stat metadata; ++ long mapped=-1; ++ if(fstat(fd,&metadata)==0 && metadata.st_size>=sizeof(DarlingNativePresentTarget) && metadata.st_uid==getuid()) ++ mapped=__lolsys_syscall(0,sizeof(DarlingNativePresentTarget),3,1,fd,0,9); ++ if(mapped>0 || mapped < -4095)ctx->native_present_target=(void*)mapped;__lolsys_syscall(fd,0,0,0,0,0,3); ++ } ++ } ++ DarlingNativePresentTarget *target=ctx->native_present_target; ++ if(!target || __atomic_load_n(&target->magic,__ATOMIC_ACQUIRE)!=DARLING_NATIVE_PRESENT_MAGIC || ++ target->width!=width || target->height!=height || !target->target){ ++ stopAsyncPresent(ctx); ++ if(target){__lolsys_syscall((long)target,sizeof(DarlingNativePresentTarget),0,0,0,0,11);ctx->native_present_target=NULL;ctx->native_present_window=0;} ++ CGLReleaseContext(ctx);return 0; ++ } ++ if(ctx->native_present_window!=target->target){ ++ stopAsyncPresent(ctx); ++ if(ctx->native_present_context!=EGL_NO_CONTEXT)p_eglDestroyContext(ctx->egl_display,ctx->native_present_context); ++ if(ctx->native_present_surface!=EGL_NO_SURFACE)p_eglDestroySurface(ctx->egl_display,ctx->native_present_surface); ++ ctx->native_present_surface=EGL_NO_SURFACE;ctx->native_present_context=EGL_NO_CONTEXT; ++ EGLint attrs[]={EGL_SURFACE_TYPE,EGL_WINDOW_BIT,EGL_RENDERABLE_TYPE,EGL_OPENGL_BIT,EGL_NONE}; ++ EGLConfig *candidates=NULL,cfg=NULL;EGLint count=0,selectedVisual=0; ++ if(p_eglChooseConfig(ctx->egl_display,attrs,NULL,0,&count) && count>0 && count<=65536) ++ candidates=calloc(count,sizeof(*candidates)); ++ if(candidates && p_eglChooseConfig(ctx->egl_display,attrs,candidates,count,&count) && p_eglGetConfigAttrib){ ++ for(EGLint n=0;negl_display,candidates[n],EGL_NATIVE_VISUAL_ID,&visual) && visual==(EGLint)target->visual){cfg=candidates[n];selectedVisual=visual;break;} ++ } ++ } ++ free(candidates); ++ if(cfg){ ++ ctx->native_present_surface=p_eglCreateWindowSurface(ctx->egl_display,cfg,(EGLNativeWindowType)(uintptr_t)target->target,NULL); ++ EGLint contextAttrs[]={0x3098,3,0x30FB,2,0x30FD,1,EGL_NONE}; ++ ctx->native_present_context=p_eglCreateContext(ctx->egl_display,cfg,ctx->egl_context,contextAttrs); ++ } ++ if(getenv("RCLOL_GPU_DIRECT_TRACE"))fprintf(stderr,"GPU_DIRECT selected_visual=%d requested_visual=%u\n",selectedVisual,target->visual); ++ EGLint actualWidth=0,actualHeight=0; ++ if(ctx->native_present_surface!=EGL_NO_SURFACE){p_eglQuerySurface(ctx->egl_display,ctx->native_present_surface,EGL_WIDTH,&actualWidth);p_eglQuerySurface(ctx->egl_display,ctx->native_present_surface,EGL_HEIGHT,&actualHeight);} ++ if(getenv("RCLOL_GPU_DIRECT_TRACE"))fprintf(stderr,"GPU_DIRECT native_size=%dx%d requested=%zux%zu\n",actualWidth,actualHeight,width,height); ++ ctx->native_present_window=target->target; ++ if(getenv("RCLOL_GPU_DIRECT_TRACE"))fprintf(stderr,"GPU_DIRECT surface_created=%d shared_context=%d visual=%u target=%u\n",ctx->native_present_surface!=EGL_NO_SURFACE,ctx->native_present_context!=EGL_NO_CONTEXT,target->visual,target->target); ++ } ++ if(ctx->native_present_surface==EGL_NO_SURFACE || ctx->native_present_context==EGL_NO_CONTEXT){CGLReleaseContext(ctx);return 0;} ++ uint32_t ready=__atomic_load_n(&target->ready,__ATOMIC_ACQUIRE); ++ if(!ready){__atomic_store_n(&target->ready,1,__ATOMIC_RELEASE); ++ for(unsigned n=0;n<40 && __atomic_load_n(&target->ready,__ATOMIC_ACQUIRE)==1;++n)usleep(500); ++ if(__atomic_load_n(&target->ready,__ATOMIC_ACQUIRE)!=2){__atomic_store_n(&target->ready,0,__ATOMIC_RELEASE);CGLReleaseContext(ctx);return 0;} ++ } ++ /* Bound the producer independently of async display cadence when testing ++ * unthrottled begin frames. Keep waits on the owning GPU thread and use ++ * absolute monotonic deadlines; async staging remains bounded to three. */ ++ if(getenv("RCLOL_GPU_DIRECT_PACE")){ ++ struct timespec now; ++ if(__lolsys_syscall(1,(long)&now,0,0,0,0,228)==0){ ++ uint64_t current=(uint64_t)now.tv_sec*1000000000ull+now.tv_nsec; ++ uint64_t deadline=ctx->native_next_present_ns; ++ if(!deadline || (current>deadline && current-deadline>16666667ull))deadline=current; ++ if(deadline>current){ ++ struct timespec wake={deadline/1000000000ull,deadline%1000000000ull}; ++ while(__lolsys_syscall(1,1,(long)&wake,0,0,0,230)==-4){} ++ } ++ ctx->native_next_present_ns=deadline+16666667ull; ++ } ++ } ++ if(getenv("RCLOL_GPU_DIRECT_ASYNC")){int queued=enqueueAsyncPresent(ctx,binding.texture,binding.target,width,height);if(!queued){stopAsyncPresent(ctx);__atomic_store_n(&target->ready,0,__ATOMIC_RELEASE);}CGLReleaseContext(ctx);return queued;} ++ GLsync completion=glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE,0);glFlush(); ++ int ok=0;GLsync presented=NULL; ++ if(completion && p_eglMakeCurrent(ctx->egl_display,ctx->native_present_surface,ctx->native_present_surface,ctx->native_present_context)){ ++ glWaitSync(completion,0,GL_TIMEOUT_IGNORED);glDeleteSync(completion);completion=NULL; ++ p_eglSwapInterval(ctx->egl_display,0); ++ GLuint fbo=0;glGenFramebuffers(1,&fbo);glBindFramebuffer(GL_READ_FRAMEBUFFER,fbo); ++ glFramebufferTexture2D(GL_READ_FRAMEBUFFER,GL_COLOR_ATTACHMENT0,binding.target,binding.texture,0); ++ if(glCheckFramebufferStatus(GL_READ_FRAMEBUFFER)==GL_FRAMEBUFFER_COMPLETE){ ++ glBindFramebuffer(GL_DRAW_FRAMEBUFFER,0);glDrawBuffer(GL_BACK);glReadBuffer(GL_COLOR_ATTACHMENT0); ++ glDisable(GL_SCISSOR_TEST);glDisable(GL_FRAMEBUFFER_SRGB); ++ glBlitFramebuffer(0,height,width,0,0,0,width,height,GL_COLOR_BUFFER_BIT,GL_NEAREST); ++ /* The producer fence and EGL swap order GPU work without a CPU wait. */ ++ static unsigned syntheticSamples; ++ int inspect=target->flags==0x54455354 && getenv("RCLOL_GPU_DIRECT_SYNTHETIC_TEST") && syntheticSamples++==10; ++ if(inspect){ ++ unsigned char rgba[4];glReadPixels(0,0,1,1,GL_RGBA,GL_UNSIGNED_BYTE,rgba); ++ fprintf(stderr,"DIRECT_SHARED_SOURCE rgba=%u,%u,%u,%u\n",rgba[0],rgba[1],rgba[2],rgba[3]); ++ glBindFramebuffer(GL_READ_FRAMEBUFFER,0);glReadBuffer(GL_BACK);glReadPixels(0,0,1,1,GL_RGBA,GL_UNSIGNED_BYTE,rgba); ++ fprintf(stderr,"DIRECT_SHARED_BACK rgba=%u,%u,%u,%u\n",rgba[0],rgba[1],rgba[2],rgba[3]); ++ } ++ presented=glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE,0);glFlush(); ++ if(presented && glGetError()==GL_NO_ERROR){ok=p_eglSwapBuffers(ctx->egl_display,ctx->native_present_surface); ++ static unsigned traceCount; ++ if(getenv("RCLOL_GPU_DIRECT_TRACE") && traceCount++<3)fprintf(stderr,"GPU_DIRECT swap_result=%d swap_fn=%p surface=%p\n",ok,(void*)p_eglSwapBuffers,(void*)ctx->native_present_surface); ++ } ++ if(inspect){unsigned char rgba[4];glReadBuffer(GL_FRONT);glReadPixels(0,0,1,1,GL_RGBA,GL_UNSIGNED_BYTE,rgba); ++ fprintf(stderr,"DIRECT_SHARED_FRONT rgba=%u,%u,%u,%u error=%u\n",rgba[0],rgba[1],rgba[2],rgba[3],glGetError());} ++ } ++ glBindFramebuffer(GL_READ_FRAMEBUFFER,0);glDeleteFramebuffers(1,&fbo); ++ if(!p_eglMakeCurrent(ctx->egl_display,ctx->egl_surface,ctx->egl_surface,ctx->egl_context))ok=0; ++ else if(presented){glWaitSync(presented,0,GL_TIMEOUT_IGNORED);glDeleteSync(presented);presented=NULL;} ++ if(presented)glDeleteSync(presented); ++ } ++ if(completion)glDeleteSync(completion); ++ __atomic_store_n(&target->ready,ok?3:0,__ATOMIC_RELEASE); ++ if(ok){__atomic_add_fetch(&target->sequence,1,__ATOMIC_RELEASE);target->presenter=(uint32_t)getpid();} ++ CGLReleaseContext(ctx);return ok; ++} ++ ++CGLError CGLReadbackIOSurface(IOSurfaceRef ioSurface) { ++ if (!ioSurface) ++ return kCGLBadAddress; ++ IOSurfaceID surfaceID = IOSurfaceGetID(ioSurface); ++ CGLIOSurfaceBinding binding = {0}; ++ pthread_mutex_lock(&ioSurfaceBindingMutex); ++ for (unsigned i = 0; i < ioSurfaceBindingCount; ++i) { ++ if (ioSurfaceBindings[i].surfaceID == surfaceID) { ++ binding = ioSurfaceBindings[i]; ++ CGLRetainContext(binding.context); ++ break; ++ } ++ } ++ pthread_mutex_unlock(&ioSurfaceBindingMutex); ++ if (!binding.context || !binding.texture) ++ return kCGLBadValue; ++ ++ void *pixels = IOSurfaceGetBaseAddressOfPlane(ioSurface, binding.plane); ++ size_t bytesPerRow = IOSurfaceGetBytesPerRowOfPlane(ioSurface, binding.plane); ++ size_t bytesPerElement = IOSurfaceGetBytesPerElementOfPlane(ioSurface, binding.plane); ++ GLenum bindingEnum = textureBindingEnum(binding.target); ++ if (!pixels || !bytesPerRow || !bytesPerElement || !bindingEnum) { ++ CGLReleaseContext(binding.context); ++ return kCGLBadValue; ++ } ++ ++ CGLContextObj oldContext = CGLGetCurrentContext(); ++ if (oldContext != binding.context) { ++ CGLError result = CGLSetCurrentContext(binding.context); ++ if (result != kCGLNoError) { ++ CGLReleaseContext(binding.context); ++ return result; ++ } ++ } ++ GLint oldTexture = 0, oldRowLength = 0, oldAlignment = 0; ++ while (glGetError() != GL_NO_ERROR) {} ++ glGetIntegerv(bindingEnum, &oldTexture); ++ glGetIntegerv(GL_PACK_ROW_LENGTH, &oldRowLength); ++ glGetIntegerv(GL_PACK_ALIGNMENT, &oldAlignment); ++ glBindTexture(binding.target, binding.texture); ++ glFinish(); ++ glPixelStorei(GL_PACK_ALIGNMENT, 1); ++ glPixelStorei(GL_PACK_ROW_LENGTH, (GLint)(bytesPerRow / bytesPerElement)); ++ IOSurfaceLock(ioSurface, 0, NULL); ++ glGetTexImage(binding.target, 0, binding.format, binding.type, pixels); ++ GLenum error = glGetError(); ++ typedef kern_return_t (*GPUUnlockFn)(IOSurfaceRef); ++ GPUUnlockFn gpuUnlock = (GPUUnlockFn)dlsym(RTLD_DEFAULT, "_IOSurfaceUnlockGPUReadback"); ++ if (gpuUnlock) gpuUnlock(ioSurface); ++ else IOSurfaceUnlock(ioSurface, 0, NULL); ++ glPixelStorei(GL_PACK_ROW_LENGTH, oldRowLength); ++ glPixelStorei(GL_PACK_ALIGNMENT, oldAlignment); ++ glBindTexture(binding.target, (GLuint)oldTexture); ++ CGLError restore = kCGLNoError; ++ if (oldContext != binding.context) ++ restore = CGLSetCurrentContext(oldContext); ++ CGLReleaseContext(binding.context); ++ return error == GL_NO_ERROR ? restore : kCGLBadState; + } + + CGLError CGLSetParameter(CGLContextObj context, CGLContextParameter parameter, const GLint *value) { +@@ -394,8 +933,9 @@ CGLError CGLSetParameter(CGLContextObj context, CGLContextParameter parameter, c + + if (parameter == kCGLCPSwapInterval) + { ++ swapIntervalRequested = 1; + GLint v = *value; +- EGLBoolean success = eglSwapInterval(display, v); ++ EGLBoolean success = p_eglSwapInterval(display, v); + if (success) + context->swap_interval = v; + return success ? kCGLNoError : kCGLBadValue; +@@ -417,14 +957,19 @@ CGLError CGLGetParameter(CGLContextObj context, CGLContextParameter parameter, G + return kCGLNoError; + } + +-CGLError CGLDescribeRenderer(CGLRendererInfoObj rend, long rend_num, CGLRendererProperty prop, long *value) { +- return kCGLNoError; ++CGLError CGLDescribeRenderer(CGLRendererInfoObj rend, GLint rend_num, CGLRendererProperty prop, GLint *value) { ++ if (!value) return kCGLBadAddress; ++ *value = 0; ++ return kCGLBadRendererInfo; + } + +-CGLError CGLQueryRendererInfo(unsigned long display_mask, CGLRendererInfoObj *rend, long *nrend) { +- return kCGLNoError; ++CGLError CGLQueryRendererInfo(GLuint display_mask, CGLRendererInfoObj *rend, GLint *nrend) { ++ if (!rend || !nrend) return kCGLBadAddress; ++ *rend = NULL; ++ *nrend = 0; ++ return kCGLBadRendererInfo; + } + + CGLError CGLDestroyRendererInfo(CGLRendererInfoObj rend) { + return kCGLNoError; +-} +\ No newline at end of file ++} +diff --git a/src/frameworks/OpenGL/include/OpenGL/CGLTypes.h b/src/frameworks/OpenGL/include/OpenGL/CGLTypes.h +index 7d5eda01a..803ac02d3 100644 +--- a/src/frameworks/OpenGL/include/OpenGL/CGLTypes.h ++++ b/src/frameworks/OpenGL/include/OpenGL/CGLTypes.h +@@ -64,6 +64,7 @@ enum { + kCGLPFARemotePBuffer = 91, + kCGLPFAAllowOfflineRenderers = 96, + kCGLPFAAcceleratedCompute = 97, ++ kCGLPFAOpenGLProfile = 99, + kCGLPFAVirtualScreenCount = 128, + }; + +diff --git a/src/frameworks/OpenGL/include/OpenGL/OpenGL.h b/src/frameworks/OpenGL/include/OpenGL/OpenGL.h +index a700038d7..7bb98bf4b 100644 +--- a/src/frameworks/OpenGL/include/OpenGL/OpenGL.h ++++ b/src/frameworks/OpenGL/include/OpenGL/OpenGL.h +@@ -8,6 +8,7 @@ + #endif + + #include ++#include + + #ifdef __cplusplus + extern "C" { +@@ -27,6 +28,8 @@ CGL_EXPORT CGLError CGLUnlockContext(CGLContextObj context); + CGL_EXPORT CGLError CGLSetCurrentContext(CGLContextObj ctx); + CGL_EXPORT CGLError CGLSetFullScreen(CGLContextObj ctx); + CGL_EXPORT CGLContextObj CGLGetCurrentContext(void); ++CGL_EXPORT CGLError CGLGetVirtualScreen(CGLContextObj ctx, GLint *screen); ++CGL_EXPORT CGLError CGLSetVirtualScreen(CGLContextObj ctx, GLint screen); + + CGL_EXPORT CGLError CGLSetParameter(CGLContextObj context, CGLContextParameter parameter, const GLint *value); + CGL_EXPORT CGLError CGLGetParameter(CGLContextObj context, CGLContextParameter parameter, GLint *value); +@@ -40,9 +43,9 @@ CGL_EXPORT CGLError CGLDestroyPixelFormat(CGLPixelFormatObj pixelFormat); + CGL_EXPORT GLuint CGLGetPixelFormatRetainCount(CGLPixelFormatObj pixelFormat); + CGL_EXPORT CGLError CGLDescribePixelFormat(CGLPixelFormatObj pixelFormat, GLint screenNumber, CGLPixelFormatAttribute attribute, GLint *valuep); + +-CGL_EXPORT CGLError CGLQueryRendererInfo(unsigned long display_mask, CGLRendererInfoObj *rend, long *nrend); ++CGL_EXPORT CGLError CGLQueryRendererInfo(GLuint display_mask, CGLRendererInfoObj *rend, GLint *nrend); + CGL_EXPORT CGLError CGLDestroyRendererInfo(CGLRendererInfoObj rend); +-CGL_EXPORT CGLError CGLDescribeRenderer(CGLRendererInfoObj rend, long rend_num, CGLRendererProperty prop, long *value); ++CGL_EXPORT CGLError CGLDescribeRenderer(CGLRendererInfoObj rend, GLint rend_num, CGLRendererProperty prop, GLint *value); + + CGL_EXPORT CGLError CGLCreatePBuffer(GLsizei width, GLsizei height, GLenum target, GLenum internalFormat, GLint maxDetail, CGLPBufferObj *pbuffer); + CGL_EXPORT CGLError CGLDescribePBuffer(CGLPBufferObj pbuffer, GLsizei *width, GLsizei *height, GLenum *target, GLenum *internalFormat, GLint *mipmap); +@@ -53,6 +56,13 @@ CGL_EXPORT CGLError CGLDestroyPBuffer(CGLPBufferObj pbuffer); + CGL_EXPORT CGLError CGLGetPBuffer(CGLContextObj context, CGLPBufferObj *pbuffer, GLenum *face, GLint *level, GLint *screen); + CGL_EXPORT CGLError CGLSetPBuffer(CGLContextObj context, CGLPBufferObj pbuffer, GLenum face, GLint level, GLint screen); + CGL_EXPORT CGLError CGLTexImagePBuffer(CGLContextObj context, CGLPBufferObj pbuffer, GLenum sourceBuffer); ++CGL_EXPORT CGLError CGLTexImageIOSurface2D(CGLContextObj context, GLenum target, ++ GLenum internalFormat, GLsizei width, GLsizei height, GLenum format, ++ GLenum type, IOSurfaceRef ioSurface, GLuint plane); ++/* Darling extension used by QuartzCore's remote-layer fallback. Real CGL ++ * texture storage is bidirectionally backed by IOSurface; our Mesa fallback ++ * must explicitly copy rendered texture pixels back to the surface. */ ++CGL_EXPORT CGLError CGLReadbackIOSurface(IOSurfaceRef ioSurface); + + #ifdef __cplusplus + } +diff --git a/src/frameworks/VideoToolbox/include/VideoToolbox/VTCompressionProperties.h b/src/frameworks/VideoToolbox/include/VideoToolbox/VTCompressionProperties.h +index 68242ee34..a9108c9c1 100644 +--- a/src/frameworks/VideoToolbox/include/VideoToolbox/VTCompressionProperties.h ++++ b/src/frameworks/VideoToolbox/include/VideoToolbox/VTCompressionProperties.h +@@ -34,4 +34,10 @@ extern CFStringRef const kVTCompressionPropertyKey_ExpectedFrameRate; + + extern CFStringRef const kVTVideoEncoderSpecification_RequireHardwareAcceleratedVideoEncoder; + ++extern CFStringRef const kVTCompressionPropertyKey_BaseLayerFrameRateFraction; ++extern CFStringRef const kVTCompressionPropertyKey_ColorPrimaries; ++extern CFStringRef const kVTCompressionPropertyKey_TransferFunction; ++extern CFStringRef const kVTCompressionPropertyKey_YCbCrMatrix; ++extern CFStringRef const kVTVideoEncoderSpecification_EnableLowLatencyRateControl; ++ + #endif +diff --git a/src/frameworks/VideoToolbox/src/VTCompressionProperties.c b/src/frameworks/VideoToolbox/src/VTCompressionProperties.c +index 9e363904e..774557a64 100644 +--- a/src/frameworks/VideoToolbox/src/VTCompressionProperties.c ++++ b/src/frameworks/VideoToolbox/src/VTCompressionProperties.c +@@ -30,3 +30,9 @@ const CFStringRef kVTCompressionPropertyKey_MaxFrameDelayCount = CFSTR("MaxFrame + const CFStringRef kVTCompressionPropertyKey_ExpectedFrameRate = CFSTR("ExpectedFrameRate"); + + const CFStringRef kVTVideoEncoderSpecification_RequireHardwareAcceleratedVideoEncoder = CFSTR("RequireHardwareAcceleratedVideoEncoder"); ++ ++const CFStringRef kVTCompressionPropertyKey_BaseLayerFrameRateFraction = CFSTR("BaseLayerFrameRateFraction"); ++const CFStringRef kVTCompressionPropertyKey_ColorPrimaries = CFSTR("ColorPrimaries"); ++const CFStringRef kVTCompressionPropertyKey_TransferFunction = CFSTR("TransferFunction"); ++const CFStringRef kVTCompressionPropertyKey_YCbCrMatrix = CFSTR("YCbCrMatrix"); ++const CFStringRef kVTVideoEncoderSpecification_EnableLowLatencyRateControl = CFSTR("EnableLowLatencyRateControl"); +diff --git a/src/frameworks/Vision/include/Vision/VNDetectBarcodesRequest.h b/src/frameworks/Vision/include/Vision/VNDetectBarcodesRequest.h +index 62d827aa2..15383581b 100644 +--- a/src/frameworks/Vision/include/Vision/VNDetectBarcodesRequest.h ++++ b/src/frameworks/Vision/include/Vision/VNDetectBarcodesRequest.h +@@ -19,6 +19,26 @@ + + #include + ++typedef NSString *VNBarcodeSymbology NS_TYPED_ENUM; ++ ++extern VNBarcodeSymbology const VNBarcodeSymbologyAztec; ++extern VNBarcodeSymbology const VNBarcodeSymbologyCode128; ++extern VNBarcodeSymbology const VNBarcodeSymbologyCode39; ++extern VNBarcodeSymbology const VNBarcodeSymbologyCode39Checksum; ++extern VNBarcodeSymbology const VNBarcodeSymbologyCode39FullASCII; ++extern VNBarcodeSymbology const VNBarcodeSymbologyCode39FullASCIIChecksum; ++extern VNBarcodeSymbology const VNBarcodeSymbologyCode93; ++extern VNBarcodeSymbology const VNBarcodeSymbologyCode93i; ++extern VNBarcodeSymbology const VNBarcodeSymbologyDataMatrix; ++extern VNBarcodeSymbology const VNBarcodeSymbologyEAN13; ++extern VNBarcodeSymbology const VNBarcodeSymbologyEAN8; ++extern VNBarcodeSymbology const VNBarcodeSymbologyI2of5; ++extern VNBarcodeSymbology const VNBarcodeSymbologyI2of5Checksum; ++extern VNBarcodeSymbology const VNBarcodeSymbologyITF14; ++extern VNBarcodeSymbology const VNBarcodeSymbologyPDF417; ++extern VNBarcodeSymbology const VNBarcodeSymbologyQR; ++extern VNBarcodeSymbology const VNBarcodeSymbologyUPCE; ++ + @interface VNDetectBarcodesRequest : NSObject + + @end +diff --git a/src/frameworks/Vision/src/VNDetectBarcodesRequest.m b/src/frameworks/Vision/src/VNDetectBarcodesRequest.m +index 9a489b386..9fe56cc3f 100644 +--- a/src/frameworks/Vision/src/VNDetectBarcodesRequest.m ++++ b/src/frameworks/Vision/src/VNDetectBarcodesRequest.m +@@ -19,6 +19,24 @@ + + #import + ++VNBarcodeSymbology const VNBarcodeSymbologyAztec = @"org.iso.Aztec"; ++VNBarcodeSymbology const VNBarcodeSymbologyCode128 = @"org.iso.Code128"; ++VNBarcodeSymbology const VNBarcodeSymbologyCode39 = @"org.iso.Code39"; ++VNBarcodeSymbology const VNBarcodeSymbologyCode39Checksum = @"org.iso.Code39Checksum"; ++VNBarcodeSymbology const VNBarcodeSymbologyCode39FullASCII = @"org.iso.Code39FullASCII"; ++VNBarcodeSymbology const VNBarcodeSymbologyCode39FullASCIIChecksum = @"org.iso.Code39FullASCIIChecksum"; ++VNBarcodeSymbology const VNBarcodeSymbologyCode93 = @"org.iso.Code93"; ++VNBarcodeSymbology const VNBarcodeSymbologyCode93i = @"org.iso.Code93i"; ++VNBarcodeSymbology const VNBarcodeSymbologyDataMatrix = @"org.iso.DataMatrix"; ++VNBarcodeSymbology const VNBarcodeSymbologyEAN13 = @"org.gs1.EAN-13"; ++VNBarcodeSymbology const VNBarcodeSymbologyEAN8 = @"org.gs1.EAN-8"; ++VNBarcodeSymbology const VNBarcodeSymbologyI2of5 = @"org.iso.I2of5"; ++VNBarcodeSymbology const VNBarcodeSymbologyI2of5Checksum = @"org.iso.I2of5Checksum"; ++VNBarcodeSymbology const VNBarcodeSymbologyITF14 = @"org.iso.ITF14"; ++VNBarcodeSymbology const VNBarcodeSymbologyPDF417 = @"org.iso.PDF417"; ++VNBarcodeSymbology const VNBarcodeSymbologyQR = @"org.iso.QRCode"; ++VNBarcodeSymbology const VNBarcodeSymbologyUPCE = @"org.gs1.UPC-E"; ++ + @implementation VNDetectBarcodesRequest + + - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector +diff --git a/src/libsandbox/src/sandbox.c b/src/libsandbox/src/sandbox.c +index 5af5298aa..fb59ee5d1 100644 +--- a/src/libsandbox/src/sandbox.c ++++ b/src/libsandbox/src/sandbox.c +@@ -21,6 +21,8 @@ + #include + #include + #include ++#include ++#include + + static int verbose = 0; + +@@ -29,63 +31,105 @@ static void initme(void) { + verbose = getenv("STUB_VERBOSE") != NULL; + } + +-void* sandbox_apply(void) ++/* darling-lol: SeatbeltExtensions token family. Chromium's seatbelt client ++ * issues/consumes/releases these around sandboxed child setup; without ++ * enforcement we hand out dummy nonzero tokens and accept everything. ++ * (These live in libsandbox.dylib on macOS, hence here and not in ++ * libsystem_sandbox.) */ ++static int64_t sandbox_extension_next_token = 1; ++ ++int64_t sandbox_extension_issue_file(const char *path, uint32_t flags) + { +- if (verbose) puts("STUB: sandbox_apply called"); +- return NULL; ++ if (verbose) puts("STUB: sandbox_extension_issue_file called"); ++ (void)path; (void)flags; ++ return sandbox_extension_next_token++; + } + +-void* sandbox_apply_container(void) ++int64_t sandbox_extension_issue_mach(const char *name, uint32_t flags) + { +- if (verbose) puts("STUB: sandbox_apply_container called"); +- return NULL; ++ if (verbose) puts("STUB: sandbox_extension_issue_mach called"); ++ (void)name; (void)flags; ++ return sandbox_extension_next_token++; + } + +-void* sandbox_compile_entitlements(void) ++int sandbox_extension_consume(int64_t token) + { +- if (verbose) puts("STUB: sandbox_compile_entitlements called"); +- return NULL; ++ if (verbose) puts("STUB: sandbox_extension_consume called"); ++ (void)token; ++ return 0; + } + +-void* sandbox_compile_file(void) ++int sandbox_extension_release(int64_t token) ++{ ++ if (verbose) puts("STUB: sandbox_extension_release called"); ++ (void)token; ++ return 0; ++} ++ ++/* darling-lol: compile/apply family. No seatbelt compiler exists, so ++ * "compiling" a profile yields a dummy handle and applying succeeds. ++ * Signatures match Apple's (the old stubs had wrong shapes). */ ++ ++void* sandbox_compile_string(const char *profile, char **errorbuf) ++{ ++ if (verbose) puts("STUB: sandbox_compile_string called"); ++ if (errorbuf != NULL) *errorbuf = NULL; ++ (void)profile; ++ return strdup("darling-sandbox-stub"); ++} ++ ++void* sandbox_compile_file(const char *path, char **errorbuf) + { + if (verbose) puts("STUB: sandbox_compile_file called"); +- return NULL; ++ if (errorbuf != NULL) *errorbuf = NULL; ++ (void)path; ++ return strdup("darling-sandbox-stub"); + } + +-void* sandbox_compile_named(void) ++void* sandbox_compile_named(const char *name, char **errorbuf) + { + if (verbose) puts("STUB: sandbox_compile_named called"); +- return NULL; ++ if (errorbuf != NULL) *errorbuf = NULL; ++ (void)name; ++ return strdup("darling-sandbox-stub"); + } + +-void* sandbox_compile_string(void) ++void* sandbox_compile_entitlements(void *entitlements, char **errorbuf) + { +- if (verbose) puts("STUB: sandbox_compile_string called"); +- return NULL; ++ if (verbose) puts("STUB: sandbox_compile_entitlements called"); ++ if (errorbuf != NULL) *errorbuf = NULL; ++ (void)entitlements; ++ return strdup("darling-sandbox-stub"); + } + +-void* sandbox_container_paths_iterate_items(void) ++int sandbox_apply(void *handle) + { +- if (verbose) puts("STUB: sandbox_container_paths_iterate_items called"); +- return NULL; ++ if (verbose) puts("STUB: sandbox_apply called"); ++ (void)handle; ++ return 0; + } + +-void* sandbox_create_params(void) ++void sandbox_free_profile(void *handle) + { +- if (verbose) puts("STUB: sandbox_create_params called"); ++ if (verbose) puts("STUB: sandbox_free_profile called"); ++ free(handle); ++} ++ ++void* sandbox_apply_container(void) ++{ ++ if (verbose) puts("STUB: sandbox_apply_container called"); + return NULL; + } + +-void* sandbox_free_params(void) ++void* sandbox_container_paths_iterate_items(void) + { +- if (verbose) puts("STUB: sandbox_free_params called"); ++ if (verbose) puts("STUB: sandbox_container_paths_iterate_items called"); + return NULL; + } + +-void* sandbox_free_profile(void) ++void* sandbox_create_params(void) + { +- if (verbose) puts("STUB: sandbox_free_profile called"); ++ if (verbose) puts("STUB: sandbox_create_params called"); + return NULL; + } + +diff --git a/src/libsystem_coreservices/dirhelper.c b/src/libsystem_coreservices/dirhelper.c +index f22f8213d..e7fbdc92e 100644 +--- a/src/libsystem_coreservices/dirhelper.c ++++ b/src/libsystem_coreservices/dirhelper.c +@@ -1,4 +1,5 @@ + #include ++#include + #include + #include + #include +@@ -44,8 +45,18 @@ char* _dirhelper(int which, char* p, size_t limit) + case DIRHELPER_USER_LOCAL: + { + const char* home; ++ // Darling: guest home is /Users/$USER even though the host ++ // HOME points at the Linux home; trustd needs the guest path ++ // for its cache locations. ++ const char *user = getenv("USER"); ++ char home_buf[PATH_MAX]; + +- home = getenv("HOME"); ++ if (user != NULL) { ++ snprintf(home_buf, sizeof(home_buf), "/Users/%s", user); ++ home = home_buf; ++ } else { ++ home = getenv("HOME"); ++ } + if (home == NULL) + home = ""; + +@@ -64,8 +75,15 @@ char* _dirhelper(int which, char* p, size_t limit) + case DIRHELPER_USER_LOCAL_CACHE: + { + const char* home; ++ const char *user = getenv("USER"); ++ char home_buf[PATH_MAX]; + +- home = getenv("HOME"); ++ if (user != NULL) { ++ snprintf(home_buf, sizeof(home_buf), "/Users/%s", user); ++ home = home_buf; ++ } else { ++ home = getenv("HOME"); ++ } + if (home != NULL) + snprintf(p, limit, "%s/.cache/%s%s", home, dir_suffix ? dir_suffix : "", dir_suffix ? "/" : ""); + else +diff --git a/src/sandbox/sandbox.c b/src/sandbox/sandbox.c +index f48cceb9e..9afa967db 100644 +--- a/src/sandbox/sandbox.c ++++ b/src/sandbox/sandbox.c +@@ -10,7 +10,13 @@ const enum sandbox_filter_type SANDBOX_CHECK_NO_REPORT = SANDBOX_FILTER_NONE; + + int sandbox_init(const char *profile, uint64_t flags, char **errorbuf) + { +- *errorbuf = strdup("Not implemented"); ++ /* darling-lol: no seatbelt enforcement exists, so init trivially ++ * succeeds. Must tolerate errorbuf==NULL (Chromium passes NULL) and ++ * report no error on success; the old code crashed on NULL and left a ++ * bogus "Not implemented" error on success. */ ++ if (errorbuf != NULL) { ++ *errorbuf = NULL; ++ } + return 0; + } + +@@ -51,18 +57,36 @@ void sandbox_free_error(char *errorbuf) + + int sandbox_init_with_parameters(const char *profile, uint64_t flags, const char *const parameters[], char **errorbuf) + { +- *errorbuf = strdup("Not implemented"); ++ /* darling-lol: see sandbox_init. */ ++ if (errorbuf != NULL) { ++ *errorbuf = NULL; ++ } + return 0; + } + + int sandbox_init_with_extensions(const char *profile, uint64_t flags, const char *const extensions[], char **errorbuf) + { +- *errorbuf = strdup("Not implemented"); ++ /* darling-lol: see sandbox_init. Extensions are accepted and ignored. */ ++ if (errorbuf != NULL) { ++ *errorbuf = NULL; ++ } + return 0; + } + + int sandbox_check(pid_t pid, const char *operation, enum sandbox_filter_type type, ...) + { ++ /* darling-lol: operation==NULL is the canonical "am I sandboxed?" ++ * probe (Chromium: sandbox_check(getpid(), NULL, SANDBOX_FILTER_NONE) ++ * "returns 1 if sandboxed", and ContentMainRunnerImpl CHECKs ++ * Seatbelt::IsSandboxed() in every sandboxed child -- a 0 here ++ * suicided all helpers/GPU with a silent UD2). Since init always ++ * "succeeds", consistently report sandboxed (1). Any other query ++ * keeps 0 (allowed), as nothing is enforced. */ ++ (void)pid; ++ if (operation == NULL) { ++ return 1; ++ } ++ (void)type; + return 0; + } + +@@ -84,42 +108,56 @@ int sandbox_unsuspend(void) + + int sandbox_issue_extension(const char *path, char **ext_token) + { +- return -1; ++ /* darling-lol: no enforcement; hand out a dummy token callers can ++ * consume/release. */ ++ if (ext_token != NULL) { ++ *ext_token = strdup("darling-sandbox-stub"); ++ } ++ return 0; + } + + int sandbox_issue_fs_extension(const char *path, uint64_t flags, char **ext_token) + { +- return -1; ++ if (ext_token != NULL) { ++ *ext_token = strdup("darling-sandbox-stub"); ++ } ++ return 0; + } + + int sandbox_issue_fs_rw_extension(const char *path, char **ext_token) + { +- return -1; ++ if (ext_token != NULL) { ++ *ext_token = strdup("darling-sandbox-stub"); ++ } ++ return 0; + } + + int sandbox_issue_mach_extension(const char *name, char **ext_token) + { +- return -1; ++ if (ext_token != NULL) { ++ *ext_token = strdup("darling-sandbox-stub"); ++ } ++ return 0; + } + + int sandbox_consume_extension(const char *path, const char *ext_token) + { +- return -1; ++ return 0; + } + + int sandbox_consume_fs_extension(const char *ext_token, char **path) + { +- return -1; ++ return 0; + } + + int sandbox_consume_mach_extension(const char *ext_token, char **name) + { +- return -1; ++ return 0; + } + + int sandbox_release_fs_extension(const char *ext_token) + { +- return -1; ++ return 0; + } + + int sandbox_container_path_for_pid(pid_t pid, char *buffer, size_t bufsize) +@@ -129,7 +167,9 @@ int sandbox_container_path_for_pid(pid_t pid, char *buffer, size_t bufsize) + + int sandbox_wakeup_daemon(char **errorbuf) + { +- *errorbuf = strdup("Not implemented"); ++ if (errorbuf != NULL) { ++ *errorbuf = NULL; ++ } + return -1; + } + +diff --git a/src/sandbox/sandbox.h b/src/sandbox/sandbox.h +index 6fa2d088c..655554284 100644 +--- a/src/sandbox/sandbox.h ++++ b/src/sandbox/sandbox.h +@@ -161,6 +161,14 @@ int sandbox_note(const char *note); + int sandbox_suspend(pid_t pid); + int sandbox_unsuspend(void); + ++/* darling-lol: SeatbeltExtensions token family (used by Chromium). ++ * No enforcement exists, so issue dummy nonzero tokens and accept ++ * consume/release as no-ops. */ ++int64_t sandbox_extension_issue_file(const char *path, uint32_t flags); ++int64_t sandbox_extension_issue_mach(const char *name, uint32_t flags); ++int sandbox_extension_consume(int64_t token); ++int sandbox_extension_release(int64_t token); ++ + int sandbox_issue_extension(const char *path, char **ext_token); + int sandbox_issue_fs_extension(const char *path, uint64_t flags, char **ext_token); + int sandbox_issue_fs_rw_extension(const char *path, char **ext_token); +diff --git a/src/shellspawn/shellspawn.c b/src/shellspawn/shellspawn.c +index f5ba615bb..fea23d638 100644 +--- a/src/shellspawn/shellspawn.c ++++ b/src/shellspawn/shellspawn.c +@@ -36,7 +36,7 @@ along with Darling. If not, see . + #include "shellspawn.h" + #include "duct_signals.h" + +-#define DBG 0 ++#define DBG 1 + + int g_serverSocket = -1; + struct sigaction sigchld_oldaction; +@@ -50,6 +50,17 @@ void reapAll(void); + + int main(int argc, const char** argv) + { ++ // darling-lol M58F40: ignore SIGPIPE. shellspawn writes to client/child ++ // sockets and pipes whose peers can close at any time (e.g. the `darling` ++ // client probing /var/run/shellspawn.sock and disconnecting, or a shell ++ // whose stdout pipe is closed). With the default disposition the first such ++ // write raises SIGPIPE and KILLS shellspawn, which removes the container's ++ // shell service and makes every subsequent `darling shell`/`darling ` ++ // fail with "Error connecting to shellspawn ... Connection refused" (the ++ // M58F39 "container wedge"). Every other daemon here (and darlingserver's ++ // M58f2 hardening) already ignores SIGPIPE; shellspawn was missed. ++ signal(SIGPIPE, SIG_IGN); ++ + // shellspawn (daemon) --fork()--> shellspawn (child) --fork()--> exec /bin/bash + // in order to read the exit status of the shell process, + // we have to allow it to become a zombie, therefore we need to +diff --git a/src/startup/mldr/commpage.c b/src/startup/mldr/commpage.c +index d9a7aea96..cc76f7a85 100644 +--- a/src/startup/mldr/commpage.c ++++ b/src/startup/mldr/commpage.c +@@ -12,6 +12,7 @@ + // Include commpage definitions + #define PRIVATE + #include ++#include "commpage-cpu-caps.h" + + static const char* SIGNATURE32 = "commpage 32-bit"; + static const char* SIGNATURE64 = "commpage 64-bit"; +@@ -84,74 +85,26 @@ void commpage_setup(bool _64bit) + + uint64_t get_cpu_caps(void) + { +- uint64_t caps = 0; +- +- { +- union cpu_flags1 eax; +- union cpu_flags2 ecx; +- union cpu_flags3 edx; +- uint32_t ebx; +- +- eax.reg = ecx.reg = edx.reg = 0; +- __cpuid(1, eax.reg, ebx, ecx.reg, edx.reg); +- +- if (ecx.mmx) +- caps |= kHasMMX; +- if (ecx.sse) +- caps |= kHasSSE; +- if (ecx.sse2) +- caps |= kHasSSE2; +- if (edx.sse3) +- caps |= kHasSSE3; +- if (ecx.ia64) +- caps |= k64Bit; +- if (edx.ssse3) +- caps |= kHasSupplementalSSE3; +- if (edx.sse41) +- caps |= kHasSSE4_1; +- if (edx.sse42) +- caps |= kHasSSE4_2; +- if (edx.aes) +- caps |= kHasAES; +- if (edx.avx) +- caps |= kHasAVX1_0; +- if (edx.rdrnd) +- caps |= kHasRDRAND; +- if (edx.fma) +- caps |= kHasFMA; +- if (edx.f16c) +- caps |= kHasF16C; +- } +- { +- union cpu_flags4 ebx; +- union cpu_flags5 ecx; +- uint32_t edx = 0, eax = 7; +- +- __cpuid(7, eax, ebx.reg, ecx.reg, edx); +- +- if (ebx.erms) +- caps |= kHasENFSTRG; +- if (ebx.avx2) +- caps |= kHasAVX2_0; +- if (ebx.bmi1) +- caps |= kHasBMI1; +- if (ebx.bmi2) +- caps |= kHasBMI2; +- if (ebx.rtm) +- caps |= kHasRTM; +- if (ebx.hle) +- caps |= kHasHLE; +- if (ebx.rdseed) +- caps |= kHasRDSEED; +- if (ebx.adx) +- caps |= kHasADX; +- if (ebx.mpx) +- caps |= kHasMPX; +- if (ebx.sgx) +- caps |= kHasSGX; +- } +- +- return caps; ++ unsigned int a, b, ecx1 = 0, edx1 = 0, ebx7 = 0; ++ const unsigned int max_basic = __get_cpuid_max(0, NULL); ++ if (max_basic >= 1) __cpuid_count(1, 0, a, b, ecx1, edx1); ++ if (max_basic >= 7) { ++ unsigned int c, d; ++ __cpuid_count(7, 0, a, ebx7, c, d); ++ } ++ uint64_t xcr0 = 0; ++ if ((ecx1 & ((1u << 26) | (1u << 27))) == ((1u << 26) | (1u << 27))) { ++ unsigned int low, high; ++ __asm__ volatile("xgetbv" : "=a"(low), "=d"(high) : "c"(0)); ++ xcr0 = ((uint64_t)high << 32) | low; ++ } ++ int long_mode = 0; ++ if (__get_cpuid_max(0x80000000u, NULL) >= 0x80000001u) { ++ unsigned int c, d; ++ __cpuid_count(0x80000001u, 0, a, b, c, d); ++ long_mode = !!(d & (1u << 29)); ++ } ++ return commpage_decode_cpu_caps(ecx1, edx1, ebx7, xcr0, long_mode); + } + + unsigned long commpage_address(bool _64bit) +diff --git a/src/startup/mldr/elfcalls/elfcalls.c b/src/startup/mldr/elfcalls/elfcalls.c +index 8f0b9e61f..38a9644c0 100644 +--- a/src/startup/mldr/elfcalls/elfcalls.c ++++ b/src/startup/mldr/elfcalls/elfcalls.c +@@ -6,6 +6,7 @@ + #include + #include + #include ++#include + #include "elfcalls.h" + #include "threads.h" + #include +@@ -86,6 +87,9 @@ static int __dserver_process_lifetime_pipe_refresh() { + + void elfcalls_make(struct elf_calls* calls) + { ++ extern void __mldr_fork_prepare(void); ++ extern void __mldr_fork_complete(void); ++ pthread_atfork(__mldr_fork_prepare, __mldr_fork_complete, __mldr_fork_complete); + calls->dlopen = dlopen_simple; + calls->dlclose = dlclose; + calls->dlsym = dlsym; +@@ -100,7 +104,16 @@ void elfcalls_make(struct elf_calls* calls) + calls->darling_thread_get_stack = __darling_thread_get_stack; + + calls->get_errno = get_errno; +- calls->exit = exit; ++ // darling-lol: the guest's `exit()` has already run its own ++ // `__cxa_finalize`/atexit handlers by the time the exit syscall ++ // reaches `native_exit`. Using the host `exit()` here ALSO runs the ++ // host libc's atexit handlers, which include host Mesa/lavapipe ++ // (`libgallium`) thread-pool teardown. In a forked child the worker ++ // threads don't exist, so that teardown blocks forever in ++ // `pthread_cond_wait` and the parent's `wait4` never returns ++ // (crashpad::SpawnSubprocess hang). The exit syscall must not run ++ // atexit handlers; use `_exit`. ++ calls->exit = _exit; + + calls->malloc = malloc; + calls->free = free; +@@ -126,4 +139,5 @@ void elfcalls_make(struct elf_calls* calls) + calls->dserver_get_process_lifetime_pipe = __dserver_get_process_lifetime_pipe; + calls->dserver_process_lifetime_pipe_refresh = __dserver_process_lifetime_pipe_refresh; + calls->dserver_close_process_lifetime_pipe = __mldr_close_process_lifetime_pipe; ++ calls->fork = fork; + } +diff --git a/src/startup/mldr/elfcalls/elfcalls.h b/src/startup/mldr/elfcalls/elfcalls.h +index fee1bde87..bee86604b 100644 +--- a/src/startup/mldr/elfcalls/elfcalls.h ++++ b/src/startup/mldr/elfcalls/elfcalls.h +@@ -71,7 +71,9 @@ struct elf_calls + int (*dserver_get_process_lifetime_pipe)(void); + int (*dserver_process_lifetime_pipe_refresh)(void); + void (*dserver_close_process_lifetime_pipe)(int fd); ++ ++ // Host libc fork prepares native allocator and loader locks for the child. ++ int (*fork)(void); + }; + + #endif +- +diff --git a/src/startup/mldr/mldr.c b/src/startup/mldr/mldr.c +index 6617ef82b..1d12043c2 100644 +--- a/src/startup/mldr/mldr.c ++++ b/src/startup/mldr/mldr.c +@@ -154,7 +154,7 @@ int main(int argc, char** argv, char** envp) + // allow any process to ptrace us + // the only process we really care about being able to do this is the server, + // but we can't just use the server's PID, since it lies outside our PID namespace. +- ptrace(PR_SET_PTRACER, PR_SET_PTRACER_ANY, 0, 0, 0); ++ prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY, 0, 0, 0); + + process_special_env(&mldr_load_results); + +@@ -563,6 +563,16 @@ static socket_bitmap_t socket_bitmap = { + .highest = -1, + }; + ++/* Host fork must preserve a consistent bitmap and an unlocked child mutex. ++ * These callbacks run through glibc fork, before guest RPC fork cleanup. */ ++void __mldr_fork_prepare(void) { ++ pthread_mutex_lock(&socket_bitmap.mutex); ++} ++ ++void __mldr_fork_complete(void) { ++ pthread_mutex_unlock(&socket_bitmap.mutex); ++} ++ + static int socket_bitmap_get(socket_bitmap_t* bitmap) { + int fd = -1; + bool updated = false; +@@ -673,7 +683,13 @@ static void socket_bitmap_put(socket_bitmap_t* bitmap, int socket) { + } + } + +- if ((bitmap->bits[index / 8] & (1 << (index % 8))) == 0) { ++ // darling-lol: the test was inverted (`== 0`, i.e. it stopped at a FREE slot and shrank past slots that were ++ // IN USE). After a whole trailing byte of slots had been released, the length collapsed below live ++ // sockets; the next threads were then handed descriptor numbers that still belonged to running threads, ++ // dup2() replaced those threads' RPC sockets, and when the newcomer exited the older thread was left with a ++ // closed descriptor (EBADF on its next call, process abort). Seen as LeagueClient dying ~85-110 s after start: ++ // slot 71 (fd 524216), held by a long-sleeping thread, given to a second thread (logs/rpc-fd-history.log). ++ if ((bitmap->bits[index / 8] & (1 << (index % 8))) != 0) { + // this bit is in-use, so we can't shrink any further + break; + } +@@ -685,12 +701,20 @@ static void socket_bitmap_put(socket_bitmap_t* bitmap, int socket) { + + if (old_byte_size != new_byte_size) { + // we can free one or more bytes from the bitmap +- void* ptr = realloc(bitmap->bits, new_byte_size); +- if (!ptr) { +- goto out; +- } ++ if (new_byte_size == 0) { ++ // darling-lol: realloc(ptr, 0) frees the block and returns NULL; the old code then kept the dangling ++ // pointer and the next allocation aborted with a double free. (Unreachable while the shrink test ++ // above was inverted; reached as soon as a process released its last socket.) ++ free(bitmap->bits); ++ bitmap->bits = NULL; ++ } else { ++ void* ptr = realloc(bitmap->bits, new_byte_size); ++ if (!ptr) { ++ goto out; ++ } + +- bitmap->bits = ptr; ++ bitmap->bits = ptr; ++ } + } + } + +diff --git a/src/tools/open.m b/src/tools/open.m +index 126bd394c..f62c2e0e8 100644 +--- a/src/tools/open.m ++++ b/src/tools/open.m +@@ -150,7 +150,15 @@ int main(int argc, char *argv[]) + NSURL** urls = (NSURL**) malloc(count * sizeof(NSURL*)); + + for (int i = 0; i < count; i++) +- urls[i] = [NSURL URLWithString:[NSString stringWithCString: argv[optind+i] encoding: NSUTF8StringEncoding]]; ++ { ++ NSString* str = [NSString stringWithCString: argv[optind+i] encoding: NSUTF8StringEncoding]; ++ // +URLWithString: needs a scheme (e.g. file:/// or https://) and ++ // returns nil for bare POSIX paths, which is how `open` is most ++ // often invoked. Fall back to a file URL in that case. ++ urls[i] = [NSURL URLWithString: str]; ++ if (urls[i] == nil) ++ urls[i] = [NSURL fileURLWithPath: str]; ++ } + + filenames = [NSArray arrayWithObjects: urls count: count]; + free(urls); +diff --git a/src/CoreAudio/AudioTrace.h b/src/CoreAudio/AudioTrace.h +new file mode 100644 +--- /dev/null ++++ b/src/CoreAudio/AudioTrace.h +@@ -0,0 +1,14 @@ ++#ifndef RCLOL_AUDIO_TRACE_H ++#define RCLOL_AUDIO_TRACE_H ++#include ++#include ++#include ++#include ++// Opt-in API/status diagnostics only; never records audio samples or names. ++static inline void rclolAudioTrace(const char* format, ...) { ++ if (!getenv("RCLOL_AUDIO_TRACE")) return; ++ static FILE* file=[] { FILE* f=fopen("/tmp/rclol-audio-api.log","a"); if(f) setvbuf(f,nullptr,_IOLBF,0); return f; }(); ++ if(!file) return; ++ va_list args;va_start(args,format);fprintf(file,"pid=%d ",getpid());vfprintf(file,format,args);va_end(args); ++} ++#endif +diff --git a/src/CoreAudio/CoreAudioComponent/MatrixMixerAU.cpp b/src/CoreAudio/CoreAudioComponent/MatrixMixerAU.cpp +new file mode 100644 +--- /dev/null ++++ b/src/CoreAudio/CoreAudioComponent/MatrixMixerAU.cpp +@@ -0,0 +1,175 @@ ++#include "../AudioTrace.h" ++/* This file is part of Darling, distributed under the GNU GPL v3 or later. */ ++#include "AUBase.h" ++#include "AUInputElement.h" ++#include "AUOutputElement.h" ++#include "AUPlugInDispatch.h" ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++// Float PCM matrix mixing. Channel indices are flattened across each scope's buses. ++class MatrixMixerAU : public AUBase { ++ std::mutex mMutex; ++ std::map mGains; ++ std::map mInputEnabled, mOutputEnabled; ++ UInt32 channels(AudioUnitScope scope) { ++ auto& buses = GetScope(scope); ++ UInt32 count = 0; ++ for (UInt32 b = 0; b < buses.GetNumberOfElements(); ++b) ++ count += buses.GetIOElement(b)->GetStreamFormat().mChannelsPerFrame; ++ return count; ++ } ++ static Float32 value(const std::map& values, UInt32 key, Float32 fallback = 0) { ++ auto it = values.find(key); ++ return it == values.end() ? fallback : it->second; ++ } ++ bool validGain(UInt32 element) { ++ UInt32 input = element >> 16, output = element & 0xffff; ++ return (input == 0xffff || input < channels(kAudioUnitScope_Input)) && ++ (output == 0xffff || output < channels(kAudioUnitScope_Output)); ++ } ++ static Float32* sample(AudioBufferList& buffers, UInt32 channel, UInt32& stride) { ++ for (UInt32 b = 0; b < buffers.mNumberBuffers; ++b) { ++ AudioBuffer& buffer = buffers.mBuffers[b]; ++ if (channel < buffer.mNumberChannels) { ++ stride = buffer.mNumberChannels; ++ return buffer.mData ? static_cast(buffer.mData) + channel : nullptr; ++ } ++ channel -= buffer.mNumberChannels; ++ } ++ return nullptr; ++ } ++public: ++ MatrixMixerAU(AudioComponentInstance instance) : AUBase(instance, 1, 1) {} ++ bool CanScheduleParameters() const override { return false; } ++ bool StreamFormatWritable(AudioUnitScope, AudioUnitElement) override { return !IsInitialized(); } ++ bool BusCountWritable(AudioUnitScope scope) override { ++ return scope == kAudioUnitScope_Input || scope == kAudioUnitScope_Output; ++ } ++ bool ValidFormat(AudioUnitScope scope, AudioUnitElement, const CAStreamBasicDescription& f) override { ++ if (!f.IsPCM() || !f.IsFloat() || f.mBitsPerChannel != 32 || ++ !f.mChannelsPerFrame || f.mChannelsPerFrame >= 0xffff || f.mSampleRate <= 0 || ++ (f.mFormatFlags & kAudioFormatFlagIsBigEndian) || f.mFramesPerPacket != 1) ++ return false; ++ UInt32 bytes = 4 * (f.IsInterleaved() ? f.mChannelsPerFrame : 1); ++ return f.mBytesPerFrame == bytes && f.mBytesPerPacket == bytes; ++ } ++ OSStatus Initialize() override { ++ if (!Inputs().GetNumberOfElements() || !Outputs().GetNumberOfElements() || ++ channels(kAudioUnitScope_Input) >= 0xffff || channels(kAudioUnitScope_Output) >= 0xffff) ++ return kAudioUnitErr_FormatNotSupported; ++ /* darling-lol: the rate must agree across the buses that are in use. League declares more buses than it uses (it ++ * sets the element counts and configures bus 0 only); the unused ones keep a default rate and used to make this ++ * initialize fail with FormatNotSupported, which sent the game into an endless graph rebuild loop. Input buses ++ * that have no source are never pulled (RenderBus skips them), and only output bus 0 is ever connected. */ ++ double rate = GetOutput(0)->GetStreamFormat().mSampleRate; ++ for (UInt32 b = 0; b < Inputs().GetNumberOfElements(); ++b) { ++ const double r = GetInput(b)->GetStreamFormat().mSampleRate; ++ if (GetInput(b)->IsActive() && r != rate) { ++ rclolAudioTrace("MIXER_INIT_RATE_MISMATCH input=%u rate=%.0f output0=%.0f\n", b, r, rate); ++ return kAudioUnitErr_FormatNotSupported; ++ } ++ } ++ return AUBase::Initialize(); ++ } ++ OSStatus SetParameter(AudioUnitParameterID id, AudioUnitScope scope, AudioUnitElement element, ++ AudioUnitParameterValue gain, UInt32) override { ++ static std::atomic traceCount{0}; ++ if (getenv("RCLOL_AUDIO_TRACE") && traceCount++ < 64) ++ rclolAudioTrace("AUDIO_MATRIX_PARAM id=%u scope=%u element=%08x gain=%g\n",id,scope,element,gain); ++ std::lock_guard lock(mMutex); ++ if (!std::isfinite(gain)) return kAudioUnitErr_InvalidParameter; ++ if (id == kMatrixMixerParam_Volume) { ++ if (scope == kAudioUnitScope_Input) { ++ if (element >= channels(scope)) return kAudioUnitErr_InvalidElement; ++ element = (element << 16) | 0xffff; ++ } else if (scope == kAudioUnitScope_Output) { ++ if (element >= channels(scope)) return kAudioUnitErr_InvalidElement; ++ element |= 0xffff0000; ++ } else if (scope != kAudioUnitScope_Global) return kAudioUnitErr_InvalidScope; ++ if (!validGain(element)) return kAudioUnitErr_InvalidElement; ++ mGains[element] = gain; ++ return noErr; ++ } ++ if (id == kMatrixMixerParam_Enable && ++ (scope == kAudioUnitScope_Input || scope == kAudioUnitScope_Output)) { ++ if (element >= GetScope(scope).GetNumberOfElements()) return kAudioUnitErr_InvalidElement; ++ (scope == kAudioUnitScope_Input ? mInputEnabled : mOutputEnabled)[element] = gain != 0; ++ return noErr; ++ } ++ return kAudioUnitErr_InvalidParameter; ++ } ++ OSStatus GetParameter(AudioUnitParameterID id, AudioUnitScope scope, AudioUnitElement element, ++ AudioUnitParameterValue& gain) override { ++ std::lock_guard lock(mMutex); ++ if (id == kMatrixMixerParam_Volume) { ++ if (scope == kAudioUnitScope_Input || scope == kAudioUnitScope_Output) { ++ if (element >= channels(scope)) return kAudioUnitErr_InvalidElement; ++ element = scope == kAudioUnitScope_Input ? (element << 16) | 0xffff : element | 0xffff0000; ++ } else if (scope != kAudioUnitScope_Global) return kAudioUnitErr_InvalidScope; ++ if (!validGain(element)) return kAudioUnitErr_InvalidElement; ++ gain = value(mGains, element); ++ return noErr; ++ } ++ if (id == kMatrixMixerParam_Enable && ++ (scope == kAudioUnitScope_Input || scope == kAudioUnitScope_Output)) { ++ if (element >= GetScope(scope).GetNumberOfElements()) return kAudioUnitErr_InvalidElement; ++ gain = value(scope == kAudioUnitScope_Input ? mInputEnabled : mOutputEnabled, element, 1); ++ return noErr; ++ } ++ return kAudioUnitErr_InvalidParameter; ++ } ++ OSStatus RenderBus(AudioUnitRenderActionFlags& flags, const AudioTimeStamp& time, ++ UInt32 bus, UInt32 frames) override { ++ auto& output = GetOutput(bus)->GetBufferList(); ++ for (UInt32 b = 0; b < output.mNumberBuffers; ++b) ++ if (output.mBuffers[b].mData) memset(output.mBuffers[b].mData, 0, output.mBuffers[b].mDataByteSize); ++ UInt32 outputOffset = 0; ++ for (UInt32 b = 0; b < bus; ++b) outputOffset += GetOutput(b)->GetStreamFormat().mChannelsPerFrame; ++ UInt32 inputOffset = 0; ++ bool silent = true; ++ for (UInt32 b = 0; b < Inputs().GetNumberOfElements(); ++b) { ++ auto* input = GetInput(b); ++ UInt32 inputChannels = input->GetStreamFormat().mChannelsPerFrame; ++ // Pull without holding the parameter lock: callbacks may change parameters. ++ if (input->IsActive()) { ++ AudioUnitRenderActionFlags inputFlags = 0; ++ OSStatus status = input->PullInput(inputFlags, time, b, frames); ++ if (status) return status; ++ auto& data = input->GetBufferList(); ++ std::lock_guard lock(mMutex); ++ if (value(mInputEnabled, b, 1) && value(mOutputEnabled, bus, 1) && ++ !(inputFlags & kAudioUnitRenderAction_OutputIsSilence)) { ++ for (UInt32 i = 0; i < inputChannels; ++i) { ++ UInt32 inputStride = 0; ++ Float32* src = sample(data, i, inputStride); ++ for (UInt32 o = 0; o < GetOutput(bus)->GetStreamFormat().mChannelsPerFrame; ++o) { ++ UInt32 outputStride = 0; ++ Float32* dst = sample(output, o, outputStride); ++ Float32 gain = value(mGains, 0xffffffff) * ++ value(mGains, ((inputOffset+i) << 16) | 0xffff) * ++ value(mGains, 0xffff0000 | (outputOffset+o)) * ++ value(mGains, ((inputOffset+i) << 16) | (outputOffset+o)); ++ if (!src || !dst || !gain) continue; ++ for (UInt32 f = 0; f < frames; ++f) dst[f*outputStride] += src[f*inputStride] * gain; ++ silent = false; ++ } ++ } ++ } ++ } ++ inputOffset += inputChannels; ++ } ++ if (silent) flags |= kAudioUnitRenderAction_OutputIsSilence; ++ else flags &= ~kAudioUnitRenderAction_OutputIsSilence; ++ return noErr; ++ } ++}; ++#pragma GCC visibility push(default) ++AUDIOCOMPONENT_ENTRY(AUBaseFactory, MatrixMixerAU); ++#pragma GCC visibility pop +diff --git a/src/frameworks/Carbon/HIToolbox/src/SecureInput.c b/src/frameworks/Carbon/HIToolbox/src/SecureInput.c +new file mode 100644 +--- /dev/null ++++ b/src/frameworks/Carbon/HIToolbox/src/SecureInput.c +@@ -0,0 +1,14 @@ ++/* X11 does not provide the macOS secure-event or TSM document facilities. ++ * Export the entry points and report unsupported instead of aborting in dyld. ++ * Do not claim that ordinary X11 keyboard input has become secure. */ ++int EnableSecureEventInput(void) { return -4; } ++int DisableSecureEventInput(void) { return -4; } ++int TSMSetDocumentProperty(void *document, unsigned tag, unsigned size, ++ void *data) { ++ (void)document; (void)tag; (void)size; (void)data; ++ return -4; ++} ++int TSMRemoveDocumentProperty(void *document, unsigned tag) { ++ (void)document; (void)tag; ++ return -4; ++} +diff --git a/src/frameworks/CoreMedia/include/CoreMedia/CMFormatDescription.h b/src/frameworks/CoreMedia/include/CoreMedia/CMFormatDescription.h +new file mode 100644 +--- /dev/null ++++ b/src/frameworks/CoreMedia/include/CoreMedia/CMFormatDescription.h +@@ -0,0 +1,40 @@ ++#ifndef _COREMEDIA_CMFORMATDESCRIPTION_H_ ++#define _COREMEDIA_CMFORMATDESCRIPTION_H_ ++ ++#include ++ ++#ifdef __cplusplus ++extern "C" { ++#endif ++ ++extern const CFStringRef kCMFormatDescriptionTransferFunction_ITU_R_709_2; ++extern const CFStringRef kCMFormatDescriptionTransferFunction_SMPTE_240M_1995; ++extern const CFStringRef kCMFormatDescriptionTransferFunction_UseGamma; ++extern const CFStringRef kCMFormatDescriptionTransferFunction_ITU_R_2020; ++extern const CFStringRef kCMFormatDescriptionTransferFunction_ITU_R_2100_HLG; ++extern const CFStringRef kCMFormatDescriptionTransferFunction_SMPTE_ST_2084_PQ; ++extern const CFStringRef kCMFormatDescriptionTransferFunction_SMPTE_ST_428_1; ++extern const CFStringRef kCMFormatDescriptionTransferFunction_Linear; ++extern const CFStringRef kCMFormatDescriptionTransferFunction_sRGB; ++extern const CFStringRef kCMFormatDescriptionColorPrimaries_ITU_R_709_2; ++extern const CFStringRef kCMFormatDescriptionColorPrimaries_EBU_3213; ++extern const CFStringRef kCMFormatDescriptionColorPrimaries_SMPTE_C; ++extern const CFStringRef kCMFormatDescriptionColorPrimaries_ITU_R_2020; ++extern const CFStringRef kCMFormatDescriptionColorPrimaries_P3_D65; ++extern const CFStringRef kCMFormatDescriptionColorPrimaries_DCI_P3; ++extern const CFStringRef kCMFormatDescriptionExtension_ContentLightLevelInfo; ++extern const CFStringRef kCMFormatDescriptionExtension_Depth; ++extern const CFStringRef kCMFormatDescriptionExtension_FormatName; ++extern const CFStringRef kCMFormatDescriptionExtension_FullRangeVideo; ++extern const CFStringRef kCMFormatDescriptionExtension_MasteringDisplayColorVolume; ++extern const CFStringRef kCMSampleAttachmentKey_IsDependedOnByOthers; ++extern const CFStringRef kCMFormatDescriptionYCbCrMatrix_ITU_R_709_2; ++extern const CFStringRef kCMFormatDescriptionYCbCrMatrix_ITU_R_601_4; ++extern const CFStringRef kCMFormatDescriptionYCbCrMatrix_SMPTE_240M_1995; ++extern const CFStringRef kCMFormatDescriptionYCbCrMatrix_ITU_R_2020; ++ ++#ifdef __cplusplus ++} ++#endif ++ ++#endif // _COREMEDIA_CMFORMATDESCRIPTION_H_ +diff --git a/src/frameworks/CryptoTokenKit/src/TKTokenWatcher.m b/src/frameworks/CryptoTokenKit/src/TKTokenWatcher.m +new file mode 100644 +--- /dev/null ++++ b/src/frameworks/CryptoTokenKit/src/TKTokenWatcher.m +@@ -0,0 +1,60 @@ ++#import ++#import ++ ++@protocol TKProtocolTokenWatcher ++@end ++ ++@implementation TKTokenWatcher ++ ++- (id)init ++{ ++ if ((self = [super init]) != nil) { ++ _tokenIDs = [[NSMutableArray alloc] init]; ++ _removalHandlers = [[NSMutableDictionary alloc] init]; ++ } ++ return self; ++} ++ ++- (id)initWithInsertionHandler:(id)arg1 ++{ ++ if ((self = [self init]) != nil) { ++ _insertionHandler = [arg1 copy]; ++ } ++ return self; ++} ++ ++- (void)dealloc ++{ ++ [_insertionHandler release]; ++ [_removalHandlers release]; ++ [_tokenIDs release]; ++ [super dealloc]; ++} ++ ++- (NSArray *)tokenIDs ++{ ++ return [NSArray array]; ++} ++ ++- (void)addRemovalHandler:(id)arg1 forTokenID:(id)arg2 ++{ ++ if (arg1 && arg2) ++ [_removalHandlers setObject: [[arg1 copy] autorelease] forKey: arg2]; ++} ++ ++- (void)insertedToken:(id)arg1 reply:(id)arg2 ++{ ++ // No smartcard/token hardware under Darling; ignore. ++} ++ ++- (void)removedToken:(id)arg1 reply:(id)arg2 ++{ ++ // No smartcard/token hardware under Darling; ignore. ++} ++ ++- (void)setup ++{ ++ // No token daemon to connect to; remain empty. ++} ++ ++@end +diff --git a/src/frameworks/OpenDirectory/src/ODLocalDirectory.h b/src/frameworks/OpenDirectory/src/ODLocalDirectory.h +new file mode 100644 +--- /dev/null ++++ b/src/frameworks/OpenDirectory/src/ODLocalDirectory.h +@@ -0,0 +1,37 @@ ++/* ++ This file is part of Darling. ++ ++ Copyright (C) 2026 Darling Team ++ ++ Darling is free software: you can redistribute it and/or modify ++ it under the terms of the GNU General Public License as published by ++ the Free Software Foundation, either version 3 of the License, or ++ (at your option) any later version. ++ ++ Darling is distributed in the hope that it will be useful, ++ but WITHOUT ANY WARRANTY; without even the implied warranty of ++ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ GNU General Public License for more details. ++ ++ You should have received a copy of the GNU General Public License ++ along with Darling. If not, see . ++*/ ++ ++#import ++#import ++ ++// The local node: user records read from the system account database. ++// Every value is an array of strings, keyed by attribute type. ++ ++extern NSString *const ODLocalNodeName; ++ ++NSError *ODLocalError(NSInteger code); ++// nil when the name is not one of the nodes a standalone system has ++NSString *ODLocalNodeNameForName(NSString *name); ++NSString *ODLocalNodeNameForType(unsigned int type); ++NSDictionary *ODLocalUserAttributes(NSString *name); ++NSArray *ODLocalAllUserAttributes(void); ++// a single record type or a list of them ++BOOL ODLocalRecordTypesIncludeUsers(id recordTypeOrList); ++ ++ODRecord *ODLocalRecordCreate(NSDictionary *attributes); +diff --git a/src/frameworks/OpenDirectory/src/ODLocalDirectory.m b/src/frameworks/OpenDirectory/src/ODLocalDirectory.m +new file mode 100644 +--- /dev/null ++++ b/src/frameworks/OpenDirectory/src/ODLocalDirectory.m +@@ -0,0 +1,141 @@ ++/* ++ This file is part of Darling. ++ ++ Copyright (C) 2026 Darling Team ++ ++ Darling is free software: you can redistribute it and/or modify ++ it under the terms of the GNU General Public License as published by ++ the Free Software Foundation, either version 3 of the License, or ++ (at your option) any later version. ++ ++ Darling is distributed in the hope that it will be useful, ++ but WITHOUT ANY WARRANTY; without even the implied warranty of ++ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ GNU General Public License for more details. ++ ++ You should have received a copy of the GNU General Public License ++ along with Darling. If not, see . ++*/ ++ ++#import "ODLocalDirectory.h" ++ ++#include ++#include ++#include ++#include ++#include ++ ++NSString *const ODFrameworkErrorDomain = @"com.apple.OpenDirectory"; ++NSString *const ODLocalNodeName = @"/Local/Default"; ++ ++// getpwent() iterates process-wide state ++static pthread_mutex_t enumerationLock = PTHREAD_MUTEX_INITIALIZER; ++ ++NSError *ODLocalError(NSInteger code) ++{ ++ return [NSError errorWithDomain: ODFrameworkErrorDomain code: code userInfo: nil]; ++} ++ ++NSString *ODLocalNodeNameForName(NSString *name) ++{ ++ for (NSString *known in @[ODLocalNodeName, @"/Search", @"/Contacts"]) { ++ if ([known isEqualToString: name]) ++ return known; ++ } ++ return nil; ++} ++ ++NSString *ODLocalNodeNameForType(unsigned int type) ++{ ++ switch (type) { ++ case 0x2201: // kODNodeTypeAuthentication ++ case 0x2205: // kODNodeTypeNetwork ++ return @"/Search"; ++ case 0x2204: // kODNodeTypeContacts ++ return @"/Contacts"; ++ default: ++ return ODLocalNodeName; ++ } ++} ++ ++static NSDictionary *attributesForEntry(const struct passwd *entry) ++{ ++ NSMutableDictionary *attributes = [NSMutableDictionary dictionary]; ++ NSString *name = [NSString stringWithUTF8String: entry->pw_name]; ++ uuid_t uuid; ++ ++ if (name == nil) ++ return nil; ++ ++ attributes[@"dsAttrTypeStandard:RecordName"] = @[name]; ++ attributes[@"dsAttrTypeStandard:RecordType"] = @[@"dsRecTypeStandard:Users"]; ++ attributes[@"dsAttrTypeStandard:AppleMetaNodeLocation"] = @[ODLocalNodeName]; ++ attributes[@"dsAttrTypeStandard:UniqueID"] = @[[NSString stringWithFormat: @"%u", (unsigned int)entry->pw_uid]]; ++ attributes[@"dsAttrTypeStandard:PrimaryGroupID"] = @[[NSString stringWithFormat: @"%u", (unsigned int)entry->pw_gid]]; ++ ++ if (mbr_uid_to_uuid(entry->pw_uid, uuid) != 0) { ++ // No stored UUID: use the membership library's compatibility mapping ++ // for such accounts, FFFFEEEE-DDDD-CCCC-BBBB-AAAA followed by the uid. ++ static const uuid_t prefix = {0xff, 0xff, 0xee, 0xee, 0xdd, 0xdd, 0xcc, 0xcc, 0xbb, 0xbb, 0xaa, 0xaa}; ++ uint32_t uid = htonl(entry->pw_uid); ++ ++ memcpy(uuid, prefix, sizeof(uuid_t)); ++ memcpy(&uuid[12], &uid, sizeof(uid)); ++ } ++ uuid_string_t text; ++ uuid_unparse_upper(uuid, text); ++ attributes[@"dsAttrTypeStandard:GeneratedUID"] = @[[NSString stringWithUTF8String: text]]; ++ ++ NSString *value; ++ if (entry->pw_gecos != NULL && (value = [NSString stringWithUTF8String: entry->pw_gecos]) != nil) ++ attributes[@"dsAttrTypeStandard:RealName"] = @[value]; ++ if (entry->pw_dir != NULL && (value = [NSString stringWithUTF8String: entry->pw_dir]) != nil) ++ attributes[@"dsAttrTypeStandard:NFSHomeDirectory"] = @[value]; ++ if (entry->pw_shell != NULL && (value = [NSString stringWithUTF8String: entry->pw_shell]) != nil) ++ attributes[@"dsAttrTypeStandard:UserShell"] = @[value]; ++ ++ return attributes; ++} ++ ++NSDictionary *ODLocalUserAttributes(NSString *name) ++{ ++ struct passwd entry, *result = NULL; ++ char buffer[4096]; ++ ++ if (![name isKindOfClass: [NSString class]] || [name UTF8String] == NULL) ++ return nil; ++ if (getpwnam_r([name UTF8String], &entry, buffer, sizeof(buffer), &result) != 0 || result == NULL) ++ return nil; ++ ++ return attributesForEntry(result); ++} ++ ++NSArray *ODLocalAllUserAttributes(void) ++{ ++ NSMutableArray *users = [NSMutableArray array]; ++ struct passwd *entry; ++ ++ pthread_mutex_lock(&enumerationLock); ++ setpwent(); ++ while ((entry = getpwent()) != NULL) { ++ NSDictionary *attributes = attributesForEntry(entry); ++ if (attributes != nil) ++ [users addObject: attributes]; ++ } ++ endpwent(); ++ pthread_mutex_unlock(&enumerationLock); ++ ++ return users; ++} ++ ++BOOL ODLocalRecordTypesIncludeUsers(id recordTypeOrList) ++{ ++ NSArray *types = [recordTypeOrList isKindOfClass: [NSArray class]] ? recordTypeOrList ++ : recordTypeOrList != nil ? @[recordTypeOrList] : @[]; ++ ++ for (id type in types) { ++ if ([type isKindOfClass: [NSString class]] && [type isEqualToString: @"dsRecTypeStandard:Users"]) ++ return YES; ++ } ++ return NO; ++} +diff --git a/src/frameworks/OpenGL/CGLAsyncPresent.inc b/src/frameworks/OpenGL/CGLAsyncPresent.inc +new file mode 100644 +--- /dev/null ++++ b/src/frameworks/OpenGL/CGLAsyncPresent.inc +@@ -0,0 +1,190 @@ ++/* Bounded GPU-only staging: the producer never waits for display cadence. ++ * Three textures cover the currently displayed, pending, and writing frames. ++ * GPU fences order both the completed producer copy and subsequent reuse. */ ++struct DarlingAsyncPresent { ++ CGLContextObj owner; ++ pthread_mutex_t mutex; ++ pthread_t thread; ++ int started, running, failed, displayed; ++ unsigned width, height; ++ GLuint textures[3], copy_fbos[2]; ++ int state[3]; /* 0 free, 1 pending, 2 display, 3 producer writing */ ++ GLsync produced[3], consumed[3]; ++ unsigned submissions, frames; ++ uint64_t serial[3]; ++}; ++static uint64_t nativeMonotonicNS(void) { ++ struct timespec now; ++ if(__lolsys_syscall(1,(long)&now,0,0,0,0,228))return 0; ++ return (uint64_t)now.tv_sec*1000000000ull+now.tv_nsec; ++} ++static void nativeWaitUntil(uint64_t deadline) { ++ struct timespec wake={deadline/1000000000ull,deadline%1000000000ull}; ++ while(__lolsys_syscall(1,1,(long)&wake,0,0,0,230)==-4){} ++} ++static void *asyncPresentWorker(void *opaque) { ++ struct DarlingAsyncPresent *a=opaque; ++ CGLContextObj ctx=a->owner; ++ int valid=p_eglBindAPI(EGL_OPENGL_API) && p_eglMakeCurrent(ctx->egl_display, ++ ctx->native_present_surface,ctx->native_present_surface,ctx->native_present_context); ++ GLuint fbo=0; ++ if(getenv("RCLOL_GPU_DIRECT_TRACE"))fprintf(stderr,"GPU_ASYNC worker_context=%d\n",valid); ++ /* The async context can wait for native VSync without blocking the ++ * Chromium producer. Keep the monotonic limiter as a fallback/bound. */ ++ int nativeVSync=0; ++ unsigned slowNativeSwaps=0; ++ if(valid){ ++ const char *vsync=getenv("RCLOL_GPU_DIRECT_VSYNC"); ++ nativeVSync=vsync && vsync[0]=='1' && p_eglSwapInterval(ctx->egl_display,1); ++ if(!nativeVSync)p_eglSwapInterval(ctx->egl_display,0); ++ glGenFramebuffers(1,&fbo); ++ } ++ uint64_t deadline=nativeMonotonicNS(); ++ for(;;){ ++ pthread_mutex_lock(&a->mutex); ++ int running=a->running,index=-1; ++ GLsync produced=NULL; ++ if(running && valid){ ++ for(int n=0;n<3;++n)if(a->state[n]==1 && a->produced[n] && (a->displayed<0 || a->serial[n]>a->serial[a->displayed])){ ++ GLenum ready=glClientWaitSync(a->produced[n],0,0); ++ if(ready==GL_WAIT_FAILED){valid=0;break;} ++ if((ready==GL_ALREADY_SIGNALED || ready==GL_CONDITION_SATISFIED) && ++ (index<0 || a->serial[n]>a->serial[index]))index=n; ++ } ++ if(valid && index>=0){ ++ for(int n=0;n<3;++n)if(a->state[n]==1 && a->serial[n]serial[index])a->state[n]=0; ++ a->state[index]=2;produced=a->produced[index];a->produced[index]=NULL; ++ } ++ } ++ pthread_mutex_unlock(&a->mutex); ++ if(!running || !valid)break; ++ DarlingNativePresentTarget *target=ctx->native_present_target; ++ if(!target || __atomic_load_n(&target->magic,__ATOMIC_ACQUIRE)!=DARLING_NATIVE_PRESENT_MAGIC || target->target!=ctx->native_present_window){if(produced)glDeleteSync(produced);valid=0;break;} ++ if(index>=0){ ++ uint64_t workStarted=nativeMonotonicNS(); ++ if(produced){glWaitSync(produced,0,GL_TIMEOUT_IGNORED);glDeleteSync(produced);} ++ glBindFramebuffer(GL_READ_FRAMEBUFFER,fbo); ++ glFramebufferTexture2D(GL_READ_FRAMEBUFFER,GL_COLOR_ATTACHMENT0,GL_TEXTURE_2D,a->textures[index],0); ++ glBindFramebuffer(GL_DRAW_FRAMEBUFFER,0);glDrawBuffer(GL_BACK);glReadBuffer(GL_COLOR_ATTACHMENT0); ++ glDisable(GL_SCISSOR_TEST);glDisable(GL_FRAMEBUFFER_SRGB); ++ if(glCheckFramebufferStatus(GL_READ_FRAMEBUFFER)!=GL_FRAMEBUFFER_COMPLETE){valid=0;break;} ++ glBlitFramebuffer(0,a->height,a->width,0,0,0,a->width,a->height,GL_COLOR_BUFFER_BIT,GL_NEAREST); ++ GLsync consumed=glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE,0);glFlush(); ++ int swapped=consumed && glGetError()==GL_NO_ERROR && p_eglSwapBuffers(ctx->egl_display,ctx->native_present_surface); ++ uint64_t workFinished=nativeMonotonicNS(); ++ /* Xwayland may acknowledge interval 1 but throttle an occluded ++ * surface to one swap per second. Two consecutive long swaps ++ * select our existing bounded 60 Hz clock for this surface. ++ * Keep texture ownership and the producer independent of it. */ ++ if(nativeVSync && swapped){ ++ slowNativeSwaps=workFinished-workStarted>=100000000ull?slowNativeSwaps+1:0; ++ if(slowNativeSwaps>=2 && p_eglSwapInterval(ctx->egl_display,0)){ ++ nativeVSync=0; ++ deadline=workFinished; ++ } ++ } ++ pthread_mutex_lock(&a->mutex); ++ a->consumed[index]=consumed; ++ if(a->displayed>=0)a->state[a->displayed]=0; ++ a->displayed=index; ++ if(swapped)++a->frames; ++ pthread_mutex_unlock(&a->mutex); ++ if(!swapped){valid=0;break;} ++ __atomic_store_n(&target->presenter,getpid(),__ATOMIC_RELEASE); ++ __atomic_fetch_add(&target->sequence,1,__ATOMIC_RELEASE); ++ __atomic_store_n(&target->ready,3,__ATOMIC_RELEASE); ++ __atomic_fetch_add(&target->reserved[3],(uint32_t)((workFinished-workStarted)/1000),__ATOMIC_RELAXED); ++ __atomic_store_n(&target->reserved[6],nativeVSync?0x544d4732u:0x544d4731u,__ATOMIC_RELEASE); ++ } ++ /* If the producer finishes just after our refresh deadline, wait for ++ * that frame in short increments instead of losing a whole refresh. ++ * Only a real swap advances the display clock. */ ++ if(index<0){__atomic_fetch_add(&target->reserved[1],1,__ATOMIC_RELAXED);nativeWaitUntil(nativeMonotonicNS()+1000000ull);continue;} ++ /* Native VSync owns the refresh cadence; avoid a second clock that ++ * can miss the compositor's next refresh after a variable swap wait. */ ++ if(nativeVSync)continue; ++ deadline+=16666667ull; ++ uint64_t now=nativeMonotonicNS(); ++ /* Preserve the refresh phase across small scheduling delays. Reset only ++ * after a full missed period, so lateness does not accumulate as drift. */ ++ if(now>deadline && now-deadline>16666667ull){__atomic_fetch_add(&target->reserved[2],1,__ATOMIC_RELAXED);deadline=now;} ++ nativeWaitUntil(deadline); ++ } ++ if(fbo)glDeleteFramebuffers(1,&fbo); ++ p_eglMakeCurrent(ctx->egl_display,EGL_NO_SURFACE,EGL_NO_SURFACE,EGL_NO_CONTEXT); ++ pthread_mutex_lock(&a->mutex);a->failed=!valid;a->running=0;pthread_mutex_unlock(&a->mutex); ++ if(ctx->native_present_target)__atomic_store_n(&ctx->native_present_target->ready,0,__ATOMIC_RELEASE); ++ return NULL; ++} ++static void stopAsyncPresent(CGLContextObj ctx) { ++ struct DarlingAsyncPresent *a=ctx->native_async; ++ if(!a)return; ++ pthread_mutex_lock(&a->mutex);a->running=0;pthread_mutex_unlock(&a->mutex); ++ if(a->started)pthread_join(a->thread,NULL); ++ CGLContextObj previous=CGLGetCurrentContext(); ++ if(p_eglMakeCurrent(ctx->egl_display,ctx->egl_surface,ctx->egl_surface,ctx->egl_context)){ ++ for(int n=0;n<3;++n){if(a->produced[n])glDeleteSync(a->produced[n]);if(a->consumed[n])glDeleteSync(a->consumed[n]);} ++ glDeleteFramebuffers(2,a->copy_fbos);glDeleteTextures(3,a->textures); ++ if(previous && previous!=ctx)p_eglMakeCurrent(previous->egl_display,previous->egl_surface,previous->egl_surface,previous->egl_context); ++ else if(!previous)p_eglMakeCurrent(ctx->egl_display,EGL_NO_SURFACE,EGL_NO_SURFACE,EGL_NO_CONTEXT); ++ } ++ pthread_mutex_destroy(&a->mutex);free(a);ctx->native_async=NULL; ++} ++static int enqueueAsyncPresent(CGLContextObj ctx,GLuint texture,GLenum textureTarget,unsigned width,unsigned height) { ++ struct DarlingAsyncPresent *a=ctx->native_async; ++ if(a && (a->width!=width || a->height!=height)){stopAsyncPresent(ctx);a=NULL;} ++ if(!a){ ++ a=calloc(1,sizeof(*a));if(!a)return 0; ++ a->owner=ctx;a->width=width;a->height=height;a->displayed=-1; ++ pthread_mutex_init(&a->mutex,NULL); ++ GLint oldTexture;glGetIntegerv(GL_TEXTURE_BINDING_2D,&oldTexture); ++ glGenFramebuffers(2,a->copy_fbos);glGenTextures(3,a->textures); ++ for(int n=0;n<3;++n){glBindTexture(GL_TEXTURE_2D,a->textures[n]);glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA8,width,height,0,GL_RGBA,GL_UNSIGNED_BYTE,NULL);} ++ glBindTexture(GL_TEXTURE_2D,oldTexture);ctx->native_async=a; ++ GLenum allocationError=glGetError();if(allocationError!=GL_NO_ERROR){if(getenv("RCLOL_GPU_DIRECT_TRACE"))fprintf(stderr,"GPU_ASYNC allocation_error=%u\n",allocationError);stopAsyncPresent(ctx);return 0;} ++ a->running=1; ++ pthread_attr_t attr;pthread_attr_init(&attr);pthread_attr_setstacksize(&attr,1024*1024); ++ int status=pthread_create(&a->thread,&attr,asyncPresentWorker,a);pthread_attr_destroy(&attr); ++ if(getenv("RCLOL_GPU_DIRECT_TRACE"))fprintf(stderr,"GPU_ASYNC worker_create=%d\n",status); ++ if(status){stopAsyncPresent(ctx);return 0;}a->started=1; ++ } ++ pthread_mutex_lock(&a->mutex); ++ if(a->failed || !a->running){pthread_mutex_unlock(&a->mutex);return 0;} ++ int index=-1; ++ for(int n=0;n<3;++n)if(!a->state[n]){index=n;break;} ++ /* Keep an older pending frame available to finish while replacing the ++ * newest pending frame with fresh producer work. Never touch display slots. */ ++ if(index<0)for(int n=0;n<3;++n)if(a->state[n]==1 && ++ (index<0 || a->serial[n]>a->serial[index]))index=n; ++ if(index<0){pthread_mutex_unlock(&a->mutex);return 0;} ++ a->state[index]=3; ++ GLsync consumed=a->consumed[index],produced=a->produced[index]; ++ a->consumed[index]=a->produced[index]=NULL; ++ pthread_mutex_unlock(&a->mutex); ++ if(consumed){glWaitSync(consumed,0,GL_TIMEOUT_IGNORED);glDeleteSync(consumed);} ++ if(produced)glDeleteSync(produced); ++ GLint oldRead,oldDraw; ++ glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING,&oldRead);glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING,&oldDraw); ++ GLboolean scissor=glIsEnabled(GL_SCISSOR_TEST),srgb=glIsEnabled(GL_FRAMEBUFFER_SRGB); ++ glDisable(GL_SCISSOR_TEST);glDisable(GL_FRAMEBUFFER_SRGB); ++ glBindFramebuffer(GL_READ_FRAMEBUFFER,a->copy_fbos[0]); ++ glFramebufferTexture2D(GL_READ_FRAMEBUFFER,GL_COLOR_ATTACHMENT0,textureTarget,texture,0);glReadBuffer(GL_COLOR_ATTACHMENT0); ++ glBindFramebuffer(GL_DRAW_FRAMEBUFFER,a->copy_fbos[1]); ++ glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER,GL_COLOR_ATTACHMENT0,GL_TEXTURE_2D,a->textures[index],0);glDrawBuffer(GL_COLOR_ATTACHMENT0); ++ int complete=glCheckFramebufferStatus(GL_READ_FRAMEBUFFER)==GL_FRAMEBUFFER_COMPLETE && glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER)==GL_FRAMEBUFFER_COMPLETE; ++ if(complete)glBlitFramebuffer(0,0,width,height,0,0,width,height,GL_COLOR_BUFFER_BIT,GL_NEAREST); ++ glBindFramebuffer(GL_READ_FRAMEBUFFER,oldRead);glBindFramebuffer(GL_DRAW_FRAMEBUFFER,oldDraw); ++ if(scissor)glEnable(GL_SCISSOR_TEST);if(srgb)glEnable(GL_FRAMEBUFFER_SRGB); ++ produced=glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE,0);glFlush(); ++ GLenum copyError=glGetError(); ++ if(!complete || !produced || copyError!=GL_NO_ERROR){if(getenv("RCLOL_GPU_DIRECT_TRACE"))fprintf(stderr,"GPU_ASYNC copy_error=%u fence=%d\n",copyError,produced!=NULL);if(produced)glDeleteSync(produced);pthread_mutex_lock(&a->mutex);a->state[index]=0;pthread_mutex_unlock(&a->mutex);return 0;} ++ pthread_mutex_lock(&a->mutex); ++ a->produced[index]=produced;a->state[index]=1;a->serial[index]=++a->submissions; ++ if(ctx->native_present_target)__atomic_fetch_add(&ctx->native_present_target->reserved[0],1,__ATOMIC_RELAXED); ++ if(ctx->native_present_target){ ++ __atomic_store_n(&ctx->native_present_target->reserved[4],__atomic_load_n(&nativeUploadCount,__ATOMIC_RELAXED),__ATOMIC_RELAXED); ++ __atomic_store_n(&ctx->native_present_target->reserved[5],__atomic_load_n(&nativeUploadMicroseconds,__ATOMIC_RELAXED),__ATOMIC_RELAXED); ++ } ++ pthread_mutex_unlock(&a->mutex); ++ return 1; ++} +diff --git a/src/frameworks/OpenGL/include/OpenGL/DarlingNativePresent.h b/src/frameworks/OpenGL/include/OpenGL/DarlingNativePresent.h +new file mode 100644 +--- /dev/null ++++ b/src/frameworks/OpenGL/include/OpenGL/DarlingNativePresent.h +@@ -0,0 +1,11 @@ ++#ifndef DARLING_NATIVE_PRESENT_H ++#define DARLING_NATIVE_PRESENT_H ++#include ++#define DARLING_NATIVE_PRESENT_MAGIC 0x47505532u ++/* Host X11 target metadata only; no pixels, input, or account information. */ ++typedef struct { ++ uint32_t magic,target,width,height; ++ uint32_t ready,presenter,sequence,visual; ++ uint32_t flags,reserved[7]; ++} DarlingNativePresentTarget; ++#endif +diff --git a/src/startup/mldr/commpage-cpu-caps.h b/src/startup/mldr/commpage-cpu-caps.h +new file mode 100644 +--- /dev/null ++++ b/src/startup/mldr/commpage-cpu-caps.h +@@ -0,0 +1,41 @@ ++#ifndef MLDR_COMMPAGE_CPU_CAPS_H ++#define MLDR_COMMPAGE_CPU_CAPS_H ++#include ++ ++/* CPUID leaf 1 uses EDX for legacy SIMD and ECX for newer SIMD. AVX ++ * also needs the operating system to save both XMM and YMM state. */ ++static inline uint64_t commpage_decode_cpu_caps(uint32_t ecx1, uint32_t edx1, ++ uint32_t ebx7, uint64_t xcr0, int long_mode) ++{ ++ uint64_t caps = long_mode ? k64Bit : 0; ++#define FEATURE(reg, bit, cap) do { if ((reg) & (UINT32_C(1) << (bit))) caps |= (cap); } while (0) ++ FEATURE(edx1, 23, kHasMMX); ++ FEATURE(edx1, 25, kHasSSE); ++ FEATURE(edx1, 26, kHasSSE2); ++ FEATURE(ecx1, 0, kHasSSE3); ++ FEATURE(ecx1, 9, kHasSupplementalSSE3); ++ FEATURE(ecx1, 19, kHasSSE4_1); ++ FEATURE(ecx1, 20, kHasSSE4_2); ++ FEATURE(ecx1, 25, kHasAES); ++ FEATURE(ecx1, 30, kHasRDRAND); ++ const uint32_t avx_requirements = (UINT32_C(1) << 26) | (UINT32_C(1) << 27) | (UINT32_C(1) << 28); ++ const int avx_usable = (ecx1 & avx_requirements) == avx_requirements && (xcr0 & 6) == 6; ++ if (avx_usable) { ++ caps |= kHasAVX1_0; ++ FEATURE(ecx1, 12, kHasFMA); ++ FEATURE(ecx1, 29, kHasF16C); ++ FEATURE(ebx7, 5, kHasAVX2_0); ++ } ++ FEATURE(ebx7, 9, kHasENFSTRG); ++ FEATURE(ebx7, 3, kHasBMI1); ++ FEATURE(ebx7, 8, kHasBMI2); ++ FEATURE(ebx7, 11, kHasRTM); ++ FEATURE(ebx7, 4, kHasHLE); ++ FEATURE(ebx7, 18, kHasRDSEED); ++ FEATURE(ebx7, 19, kHasADX); ++ FEATURE(ebx7, 14, kHasMPX); ++ FEATURE(ebx7, 2, kHasSGX); ++#undef FEATURE ++ return caps; ++} ++#endif diff --git a/patches/darlingserver.patch b/patches/darlingserver.patch new file mode 100644 index 0000000..09dd515 --- /dev/null +++ b/patches/darlingserver.patch @@ -0,0 +1,2442 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 4920879..012235e 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -133,6 +133,13 @@ target_compile_options(darlingserver PRIVATE + -pthread + -std=c++17 + ) ++# The parent project commonly leaves CMAKE_BUILD_TYPE empty. An unoptimized ++# RPC server spends its event-loop budget in C++ ownership/container helpers. ++# Preserve explicit debug and sanitizer builds; optimize the default runtime. ++if (NOT CMAKE_BUILD_TYPE AND NOT DSERVER_ASAN AND NOT DSERVER_UBSAN) ++ target_compile_options(darlingserver PRIVATE -O2) ++endif() ++ + target_link_options(darlingserver PRIVATE + -pthread + -Wl,--gc-sections +diff --git a/duct-tape/CMakeLists.txt b/duct-tape/CMakeLists.txt +index 5950c7f..cf4f1ba 100644 +--- a/duct-tape/CMakeLists.txt ++++ b/duct-tape/CMakeLists.txt +@@ -268,6 +268,7 @@ add_library(darlingserver_duct_tape STATIC + src/kqchan.c + src/semaphore.c + src/psynch.c ++ src/fileport.c + src/condvar.c + src/debug.c + +@@ -380,3 +381,9 @@ add_dependencies(darlingserver_duct_tape + target_link_libraries(darlingserver_duct_tape PUBLIC + libsimple_darlingserver + ) ++ ++# Match the host server's default optimization while retaining the kernel's ++# pointer-aliasing semantics and explicit debug/sanitizer build policies. ++if (NOT CMAKE_BUILD_TYPE AND NOT DSERVER_ASAN AND NOT DSERVER_UBSAN) ++ target_compile_options(darlingserver_duct_tape PRIVATE -O2 -fno-strict-aliasing) ++endif() +diff --git a/duct-tape/include/darlingserver/duct-tape.h b/duct-tape/include/darlingserver/duct-tape.h +index a5ddfc7..607cf17 100644 +--- a/duct-tape/include/darlingserver/duct-tape.h ++++ b/duct-tape/include/darlingserver/duct-tape.h +@@ -56,6 +56,13 @@ dtape_thread_t* dtape_thread_create(dtape_task_t* task, uint64_t nsid, void* con + dtape_kqchan_mach_port_t* dtape_kqchan_mach_port_create(dtape_task_t* owning_task, uint32_t port, uint64_t receive_buffer, uint64_t receive_buffer_size, uint64_t saved_filter_flags, dtape_kqchan_mach_port_notification_callback_f notification_callback, void* context); + dtape_semaphore_t* dtape_semaphore_create(dtape_task_t* owning_task, int initial_value); + ++/* darling-lol M54: fileports (Mojo FD-over-Mach transport). Names and ++ * kern_return_t cross as plain ints to keep this header light. */ ++int dtape_fileport_makeport(dtape_task_t* task, int fd, uint32_t* name_out); ++int dtape_fileport_makefd(dtape_task_t* task, uint32_t name, int* fd_out); ++struct ipc_port; ++void dtape_fileport_destroy(struct ipc_port* port); ++ + void dtape_kqchan_mach_port_destroy(dtape_kqchan_mach_port_t* kqchan); + void dtape_semaphore_destroy(dtape_semaphore_t* semaphore); + +diff --git a/duct-tape/include/darlingserver/duct-tape/hooks.h b/duct-tape/include/darlingserver/duct-tape/hooks.h +index 1dde8b7..f319f1f 100644 +--- a/duct-tape/include/darlingserver/duct-tape/hooks.h ++++ b/duct-tape/include/darlingserver/duct-tape/hooks.h +@@ -28,6 +28,8 @@ typedef dtape_thread_t* (*dtape_hook_current_thread_f)(void); + typedef void (*dtape_hook_timer_arm_f)(uint64_t absolute_ns, bool override); + + typedef void (*dtape_hook_log_f)(dtape_log_level_t level, const char* message); ++/* darling-lol: lets dtape_log skip formatting (a 4 KiB vsnprintf per call) when the level would be dropped anyway */ ++typedef int (*dtape_hook_log_enabled_f)(dtape_log_level_t level); + typedef void (*dtape_hook_get_load_info_f)(dtape_load_info_t* load_info); + + typedef void (*dtape_hook_thread_suspend_f)(void* thread_context, dtape_thread_continuation_callback_f continuation_callback, void* continuation_contex, libsimple_lock_t* unlock_me); +@@ -122,6 +124,8 @@ typedef struct dtape_hooks { + dtape_hook_task_remove_port_set_member_f task_remove_port_set_member; + dtape_hook_task_clear_port_set_f task_clear_port_set; + #endif ++ ++ dtape_hook_log_enabled_f log_enabled; + } dtape_hooks_t; + + #ifdef __cplusplus +diff --git a/duct-tape/internal-include/darlingserver/duct-tape/stubs.h b/duct-tape/internal-include/darlingserver/duct-tape/stubs.h +index 32ebd76..688015e 100644 +--- a/duct-tape/internal-include/darlingserver/duct-tape/stubs.h ++++ b/duct-tape/internal-include/darlingserver/duct-tape/stubs.h +@@ -12,9 +12,13 @@ void dtape_stub_log(const char* function_name, int safety, const char* subsectio + #define dtape_stub_safe(...) (dtape_stub_log(__FUNCTION__, 1, "" __VA_ARGS__)) + + // for functions that have been confirmed to require an actual implementation (rather than a simple stub) +-#define dtape_stub_unsafe(...) ({ \ +- dtape_stub_log(__FUNCTION__, -1, "" __VA_ARGS__); \ +- __builtin_unreachable(); \ +- }) \ ++// ++// darling-lol: this used to end in __builtin_unreachable(), which is only ++// valid when dtape_stub_log() is guaranteed to abort. With the optional ++// DTAPE_NONFATAL_UNSAFE escape hatch it may return, so we must not tell the ++// compiler the path is dead (that miscompiles the function's fall-through). ++// dtape_stub_log() raises SIGABRT itself in the default (fatal) case, so ++// omitting __builtin_unreachable() preserves the original behavior. ++#define dtape_stub_unsafe(...) (dtape_stub_log(__FUNCTION__, -1, "" __VA_ARGS__)) + + #endif // _DARLINGSERVER_DUCT_TAPE_STUBS_H_ +diff --git a/duct-tape/internal-include/darlingserver/duct-tape/thread.h b/duct-tape/internal-include/darlingserver/duct-tape/thread.h +index 633d729..75a3965 100644 +--- a/duct-tape/internal-include/darlingserver/duct-tape/thread.h ++++ b/duct-tape/internal-include/darlingserver/duct-tape/thread.h +@@ -38,6 +38,8 @@ struct dtape_thread { + bool processing_signal; + + bool waiting_suspended; ++ /* Protect the wait check through publication of microthread suspension. */ ++ libsimple_lock_t block_suspend_gate; + dtape_mutex_t suspension_mutex; + dtape_condvar_t suspension_condvar; + +diff --git a/duct-tape/pthread/kern_synch.c b/duct-tape/pthread/kern_synch.c +index aa2b6d3..ab7cdb7 100644 +--- a/duct-tape/pthread/kern_synch.c ++++ b/duct-tape/pthread/kern_synch.c +@@ -42,6 +42,7 @@ + #include + #include + #include ++#include /* darling-lol M58f3: UINT64_MAX */ + //#include + #include + #include +@@ -1236,8 +1237,22 @@ _psynch_cvwait(__unused proc_t p, user_addr_t cv, uint64_t cvlsgen, + struct timespec ts; + ts.tv_sec = (__darwin_time_t)sec; + ts.tv_nsec = (nsec & 0x3fffffff); +- nanoseconds_to_absolutetime( +- (uint64_t)ts.tv_sec * NSEC_PER_SEC + ts.tv_nsec, &abstime); ++ /* darling-lol M58f3: saturate the seconds->nanoseconds product. ++ * Callers legitimately pass near-infinite deadlines (e.g. ++ * Chromium's base::WaitableEvent uses TimeDelta::Max(), so ++ * tv_sec can be ~9.2e12); the product overflows uint64 and ++ * wraps to an arbitrary (sometimes past!) deadline, making ++ * the wait return ETIMEDOUT instantly or hang indefinitely. ++ * Clamp to a huge-but-safe interval (UINT64_MAX/2 ns ~ 292 ++ * years) which still cannot wrap after the deadline add. */ ++ uint64_t nsecs; ++ uint64_t secs_u = (ts.tv_sec < 0) ? 0 : (uint64_t)ts.tv_sec; ++ if (secs_u > (UINT64_MAX / 2 - (uint64_t)ts.tv_nsec) / NSEC_PER_SEC) { ++ nsecs = UINT64_MAX / 2; ++ } else { ++ nsecs = secs_u * NSEC_PER_SEC + (uint64_t)ts.tv_nsec; ++ } ++ nanoseconds_to_absolutetime(nsecs, &abstime); + clock_absolutetime_interval_to_deadline(abstime, &abstime); + } + +diff --git a/duct-tape/src/fileport.c b/duct-tape/src/fileport.c +new file mode 100644 +index 0000000..1741d59 +--- /dev/null ++++ b/duct-tape/src/fileport.c +@@ -0,0 +1,154 @@ ++/* ++ * darling-lol M54: fileport support (fileport_makeport/fileport_makefd). ++ * ++ * Chromium's Mojo-on-Mac transports Unix FDs over Mach messages by wrapping ++ * them in fileports (mojo/core/channel_mac.cc, mojo/core/ipcz_driver/ ++ * wrapped_platform_handle.cc). Without this, every FD-carrying Mojo message ++ * (e.g. NetworkService startup) dies in the old-LKM-API UD2 trap. ++ * ++ * A fileport is a Mach port whose kobject holds a host FD owned by ++ * darlingserver. The kobject rides the port across IPC transfer ++ * automatically, and ipc_kobject_destroy closes the FD when the port dies, ++ * so no transfer hooks or task-death cleanup are needed. fileport_makefd ++ * dups the held FD back to the caller (independent lifecycle, like XNU). ++ */ ++ ++#include ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++/* duct-tape convention (see memory.c): declare libc bits manually rather ++ * than including host , whose types collide with XNU headers. */ ++int close(int fd); ++int dup(int fd); ++ ++#include ++#include ++ ++struct dtape_fileport { ++ int fd; ++}; ++ ++int dtape_fileport_makeport(dtape_task_t* task, int fd, uint32_t* name_out) ++{ ++ struct dtape_fileport* fp; ++ ipc_port_t port; ++ mach_port_name_t name; ++ ++ if (task == NULL || fd < 0 || name_out == NULL) { ++ return KERN_INVALID_ARGUMENT; ++ } ++ if (task->xnu_task.itk_space == IS_NULL) { ++ return KERN_INVALID_TASK; ++ } ++ ++ fp = malloc(sizeof(*fp)); ++ if (fp == NULL) { ++ dtape_log_error("fileport_makeport: malloc failed"); ++ return KERN_RESOURCE_SHORTAGE; ++ } ++ fp->fd = fd; ++ ++ /* Allocate a kernel-owned port holding the fd. As in XNU's ++ * fileport_alloc(), the port comes back with a send right. */ ++ port = ipc_kobject_alloc_port((ipc_kobject_t)fp, IKOT_FILEPORT, ++ IPC_KOBJECT_ALLOC_MAKE_SEND | IPC_KOBJECT_ALLOC_NSREQUEST); ++ if (!IP_VALID(port)) { ++ dtape_log_error("fileport_makeport: ipc_kobject_alloc_port failed fd=%d", fd); ++ free(fp); ++ return KERN_RESOURCE_SHORTAGE; ++ } ++ ++ /* Install the send right into the task's space and get a name. ++ * ipc_port_copyout_send() is the XNU entry point for exactly this ++ * (see sys_fileport_makeport); it consumes the send right, so we ++ * must not release it afterwards. Do NOT go through ++ * mach_port_insert_right(): its MACH_MSG_TYPE_PORT_ANY_RIGHT() guard ++ * rejects MACH_MSG_TYPE_MAKE_SEND, which made every fileport fail ++ * with KERN_INVALID_VALUE and broke all FD-carrying Mojo messages. */ ++ name = ipc_port_copyout_send(port, task->xnu_task.itk_space); ++ if (!MACH_PORT_VALID(name)) { ++ dtape_log_error("fileport_makeport: ipc_port_copyout_send failed fd=%d", fd); ++ /* port consumed by copyout; its kobject destroy closes fp->fd */ ++ return KERN_RESOURCE_SHORTAGE; ++ } ++ ++ *name_out = (uint32_t)name; ++ return KERN_SUCCESS; ++} ++ ++int dtape_fileport_makefd(dtape_task_t* task, uint32_t uname, int* fd_out) ++{ ++ ipc_object_t object = IO_NULL; ++ mach_msg_type_name_t dtype = 0; ++ mach_port_name_t name = (mach_port_name_t)uname; ++ ipc_port_t port; ++ struct dtape_fileport* fp; ++ int fd; ++ ++ if (task == NULL || fd_out == NULL) { ++ return KERN_INVALID_ARGUMENT; ++ } ++ if (task->xnu_task.itk_space == IS_NULL) { ++ return KERN_INVALID_TASK; ++ } ++ ++ /* Copyin a send right: validates the name, checks aliveness, and ++ * enforces that the right is a send right (receive/dead names fail). ++ * Does not consume the caller's right. */ ++ if (ipc_object_copyin(task->xnu_task.itk_space, name, ++ MACH_MSG_TYPE_COPY_SEND, &object, 0, NULL, ++ IPC_OBJECT_COPYIN_FLAGS_ALLOW_IMMOVABLE_SEND) != KERN_SUCCESS) { ++ return KERN_INVALID_RIGHT; ++ } ++ dtype = ipc_object_copyin_type(MACH_MSG_TYPE_COPY_SEND); ++ ++ port = ip_object_to_port(object); ++ if (!IP_VALID(port) || ip_kotype(port) != IKOT_FILEPORT) { ++ ipc_object_destroy(object, dtype); ++ return KERN_INVALID_RIGHT; ++ } ++ fp = (struct dtape_fileport*)ip_get_kobject(port); ++ if (fp == NULL || fp->fd < 0) { ++ ipc_object_destroy(object, dtype); ++ return KERN_INVALID_RIGHT; ++ } ++ ++ /* Independent lifecycle: the receiver owns the dup, the kobject ++ * keeps its own. */ ++ fd = dup(fp->fd); ++ ipc_object_destroy(object, dtype); ++ if (fd < 0) { ++ return KERN_RESOURCE_SHORTAGE; ++ } ++ ++ *fd_out = fd; ++ return KERN_SUCCESS; ++} ++ ++void dtape_fileport_destroy(ipc_port_t port) ++{ ++ struct dtape_fileport* fp; ++ ++ if (!IP_VALID(port)) { ++ return; ++ } ++ fp = (struct dtape_fileport*)ip_get_kobject(port); ++ if (fp == NULL) { ++ return; ++ } ++ /* Port is dead; just release our resources like ++ * mach_destroy_memory_entry does. */ ++ if (fp->fd >= 0) { ++ close(fp->fd); ++ } ++ free(fp); ++} +diff --git a/duct-tape/src/kqchan.c b/duct-tape/src/kqchan.c +index 458e659..2e33f20 100644 +--- a/duct-tape/src/kqchan.c ++++ b/duct-tape/src/kqchan.c +@@ -138,7 +138,16 @@ static void knote_post(struct knote* kn, long hint) { + + dtape_log_debug("%s: kn=%p, kqchan=%p, kqchan->callback=%p", __FUNCTION__, kn, kqchan, kqchan->callback); + +- if (!kqchan->callback) { ++ /* darling-lol: snapshot callback+context with a SINGLE load. ++ * The check-then-call used to load kqchan->callback twice; with -O0 ++ * builds there is no CSE, so dtape_kqchan_mach_port_disable_notifications ++ * (or destroy on another worker) could NULL it between the guard and ++ * the call, producing call-0x0 -> SIGSEGV in darlingserver whenever a ++ * port with knotes was torn down (e.g. GPU helpers exiting). A single ++ * snapshot cannot call NULL. */ ++ dtape_kqchan_mach_port_notification_callback_f cb = kqchan->callback; ++ void* ctx = kqchan->context; ++ if (!cb) { + return; + } + +@@ -150,7 +159,7 @@ static void knote_post(struct knote* kn, long hint) { + + dtape_log_debug("%s: kn=%p, kqchan=%p; invoking callback...", __FUNCTION__, kn, kqchan); + +- kqchan->callback(kqchan->context); ++ cb(ctx); + }; + + void knote(struct klist* list, long hint) { +@@ -193,7 +202,12 @@ static void kqchan_waitq_waiter_entry(void* context, wait_result_t wait_result) + break; + } else { + if (filt_machportpeek(&kqchan->knote) & FILTER_ACTIVE) { +- kqchan->callback(kqchan->context); ++ /* darling-lol: single-load snapshot, see knote_post. */ ++ dtape_kqchan_mach_port_notification_callback_f wcb = kqchan->callback; ++ void* wctx = kqchan->context; ++ if (wcb) { ++ wcb(wctx); ++ } + } + // wait until it's read + if (!dtape_semaphore_down_simple(kqchan->waiter_read_semaphore)) { +diff --git a/duct-tape/src/memory.c b/duct-tape/src/memory.c +index 3114947..fcff461 100644 +--- a/duct-tape/src/memory.c ++++ b/duct-tape/src/memory.c +@@ -11,6 +11,7 @@ + + #include + ++ + #include + + struct zone { +@@ -341,7 +342,14 @@ kern_return_t copyoutmap(vm_map_t map, void* fromdata, vm_map_address_t toaddr, + memmove((void*)toaddr, fromdata, length); + return KERN_SUCCESS; + } else { +- return dtape_hooks->task_write_memory(map->dtape_task->context, toaddr, fromdata, length) ? KERN_SUCCESS : KERN_FAILURE; ++ /* darling-lol M58dq: name the failing user-copy target behind the M58G ++ * ipc_kmsg_put "copyout failed" drops. */ ++ if (!dtape_hooks->task_write_memory(map->dtape_task->context, toaddr, fromdata, length)) { ++ dtape_log_error("M58DQ copyoutmap FAIL toaddr=0x%llx len=%lu map=%p task=%p", ++ (unsigned long long) toaddr, (unsigned long) length, (void*) map, (void*) map->dtape_task); ++ return KERN_FAILURE; ++ } ++ return KERN_SUCCESS; + } + }; + +@@ -526,20 +534,212 @@ kern_return_t mach_vm_allocate_kernel(vm_map_t map, mach_vm_offset_t* addr, mach + } + }; + ++/* ++ * darling-lol: functional `_mach_make_memory_entry`. ++ * ++ * This used to be `dtape_stub_unsafe()` (abort), so any guest call killed ++ * darlingserver outright. Entries are backed by a SHARED memfd held in a ++ * refcounted descriptor on the entry: every task mapping the entry observes ++ * the same pages (required e.g. for Chromium field-trial shared memory, ++ * where the browser writes after creating and helpers map later). ++ */ ++kern_return_t mach_memory_entry_allocate(vm_named_entry_t* user_entry_p, ipc_port_t* user_handle_p) { ++ vm_named_entry_t user_entry; ++ ipc_port_t user_handle; ++ ++ user_entry = (vm_named_entry_t) kalloc(sizeof(*user_entry)); ++ if (user_entry == NULL) { ++ return KERN_FAILURE; ++ } ++ bzero(user_entry, sizeof(*user_entry)); ++ ++ named_entry_lock_init(user_entry); ++ user_entry->backing.copy = NULL; ++ user_entry->is_object = FALSE; ++ user_entry->is_sub_map = FALSE; ++ user_entry->is_copy = FALSE; ++ user_entry->internal = FALSE; ++ user_entry->size = 0; ++ user_entry->offset = 0; ++ user_entry->data_offset = 0; ++ user_entry->protection = VM_PROT_NONE; ++ user_entry->ref_count = 1; ++ ++ user_handle = ipc_kobject_alloc_port((ipc_kobject_t)user_entry, ++ IKOT_NAMED_ENTRY, ++ IPC_KOBJECT_ALLOC_MAKE_SEND | IPC_KOBJECT_ALLOC_NSREQUEST); ++ if (user_handle == IP_NULL) { ++ kfree(user_entry, sizeof(*user_entry)); ++ return KERN_FAILURE; ++ } ++ ++ *user_entry_p = user_entry; ++ *user_handle_p = user_handle; ++ return KERN_SUCCESS; ++}; ++ + kern_return_t _mach_make_memory_entry(vm_map_t target_map, memory_object_size_t* size, memory_object_offset_t offset, vm_prot_t permission, ipc_port_t* object_handle, ipc_port_t parent_entry) { +- dtape_stub_unsafe(); ++ vm_map_size_t map_size = (vm_map_size_t)*size; ++ vm_map_offset_t map_start, map_end; ++ vm_map_copy_t copy = NULL; ++ vm_named_entry_t user_entry = NULL; ++ ipc_port_t user_handle = IP_NULL; ++ kern_return_t kr; ++ ++ if (object_handle == NULL) { ++ return KERN_INVALID_ARGUMENT; ++ } ++ *object_handle = IP_NULL; ++ ++ if (target_map == VM_MAP_NULL) { ++ target_map = current_map(); ++ } ++ ++ /* round the request out to whole pages, like XNU does */ ++ map_start = vm_map_trunc_page(offset, VM_MAP_PAGE_MASK(target_map)); ++ map_end = vm_map_round_page(offset + map_size, VM_MAP_PAGE_MASK(target_map)); ++ map_size = map_end - map_start; ++ ++ if (map_size == 0) { ++ return KERN_INVALID_ARGUMENT; ++ } ++ ++ /* ++ * Backing strategy (darling-lol): a SHARED memfd, so every task that ++ * maps the entry sees the same pages. The previous snapshot-copy ++ * approach handed each mapper a private copy of the bytes as of ++ * creation time; a producer (e.g. Chromium's browser writing field ++ * trial state) then wrote into pages no consumer ever saw, and ++ * consumers parsed stale zeros -> garbage strings -> crashes ++ * (char_traits overlap assert) in every helper. ++ * ++ * Two cases, matching XNU: ++ * ++ * - MAP_MEM_NAMED_CREATE: fresh anonymous entry of *size bytes (what ++ * Chromium's PlatformSharedMemoryRegion allocation path wants). ++ * memfds start zero-filled. A null parent alone does NOT imply this: ++ * ordinary extraction calls also pass a null parent. ++ * ++ * - parent_entry != NULL: seed the memfd with a snapshot of the ++ * described region (copied once, at creation). ++ */ ++ int memfd = -1; ++ dtape_map_shared_descriptor_t* desc = NULL; ++ if (permission & MAP_MEM_NAMED_CREATE) { ++ memfd = memfd_create("darling-mementry", MFD_CLOEXEC); ++ if (memfd < 0 || ftruncate(memfd, map_size) < 0) { ++ if (memfd >= 0) close(memfd); ++ return KERN_RESOURCE_SHORTAGE; ++ } ++ } else { ++ void* staged = NULL; ++ int source_prot = PROT_READ | PROT_WRITE; ++ uintptr_t remapped; ++ /* snapshot the region, then publish it into the shared object */ ++ kr = vm_map_copyin_common(target_map, map_start, map_size, ++ FALSE /* src_destroy */, FALSE /* src_volatile */, ©, ++ FALSE /* use_maxprot */); ++ if (kr != KERN_SUCCESS) { ++ dtape_log_error("mach_make_memory_entry: copyin failed kr=%d map=%p start=%#llx size=%#llx perms=%#x", ++ kr, (void*)target_map, (unsigned long long)map_start, (unsigned long long)map_size, permission); ++ return kr; ++ } ++ memfd = memfd_create("darling-mementry", MFD_CLOEXEC); ++ if (memfd < 0 || ftruncate(memfd, map_size) < 0) { ++ if (memfd >= 0) close(memfd); ++ vm_map_copy_discard(copy); ++ return KERN_RESOURCE_SHORTAGE; ++ } ++ staged = mmap(NULL, map_size, PROT_READ | PROT_WRITE, MAP_SHARED, memfd, 0); ++ if (staged == MAP_FAILED) { ++ close(memfd); ++ vm_map_copy_discard(copy); ++ return KERN_RESOURCE_SHORTAGE; ++ } ++ memcpy(staged, (char*)copy->cpy_kdata + copy->offset, map_size); ++ munmap(staged, map_size); ++ vm_map_copy_discard(copy); ++ copy = NULL; ++ ++ /* Extraction must retain shared-object semantics, not snapshot ++ * semantics. Replace the source task's pages with a MAP_SHARED view ++ * of the populated memfd. Future writes by the creator are then ++ * visible to every process that maps the named entry. */ ++ /* The requested permission constrains mappings made from the named ++ * entry; it does not downgrade the creator's existing mapping. The ++ * common Chromium extraction path exports a read-only handle from a ++ * writable region and continues writing through the original view. */ ++ if (permission & VM_PROT_EXECUTE) ++ source_prot |= PROT_EXEC; ++ remapped = dtape_hooks->task_map_file( ++ target_map->dtape_task->context, memfd, ++ dtape_byte_count_to_page_count_round_up(map_size), source_prot, ++ map_start, 0, ++ dtape_memory_flag_fixed | dtape_memory_flag_overwrite); ++ if (remapped != map_start) { ++ close(memfd); ++ return KERN_FAILURE; ++ } ++ } ++ ++ desc = dtape_map_shared_descriptor_create(memfd, map_size); ++ if (desc == NULL) { ++ close(memfd); ++ return KERN_RESOURCE_SHORTAGE; ++ } ++ memfd = -1; /* the descriptor now owns the memfd */ ++ ++ kr = mach_memory_entry_allocate(&user_entry, &user_handle); ++ if (kr != KERN_SUCCESS) { ++ dtape_map_shared_descriptor_release(desc); ++ dtape_log_error("mach_make_memory_entry: allocate failed kr=%d", kr); ++ return kr; ++ } ++ ++ named_entry_lock(user_entry); ++ user_entry->backing.copy = NULL; ++ user_entry->is_copy = FALSE; ++ user_entry->dtape_descriptor = desc; ++ user_entry->size = map_size; ++ user_entry->offset = offset - map_start; /* logical offset, memfd base 0 */ ++ user_entry->data_offset = 0; ++ user_entry->protection = permission; ++ named_entry_unlock(user_entry); ++ ++ *size = (memory_object_size_t)map_size; ++ *object_handle = user_handle; ++ return KERN_SUCCESS; ++}; ++ ++kern_return_t mach_make_memory_entry_64(vm_map_t target_map, memory_object_size_t* size, memory_object_offset_t offset, vm_prot_t permission, ipc_port_t* object_handle, ipc_port_t parent_handle) { ++ return _mach_make_memory_entry(target_map, size, offset, permission, object_handle, parent_handle); ++}; ++ ++kern_return_t mach_make_memory_entry(vm_map_t target_map, vm_size_t* size, vm_offset_t offset, vm_prot_t permission, ipc_port_t* object_handle, ipc_port_t parent_entry) { ++ memory_object_size_t mo_size = (memory_object_size_t)*size; ++ kern_return_t kr = _mach_make_memory_entry(target_map, &mo_size, (memory_object_offset_t)offset, permission, object_handle, parent_entry); ++ *size = (vm_size_t)mo_size; ++ return kr; + }; + + kern_return_t mach_memory_entry_access_tracking(ipc_port_t entry_port, int* access_tracking, uint32_t* access_tracking_reads, uint32_t* access_tracking_writes) { +- dtape_stub_unsafe(); ++ /* No access tracking in duct-tape; report "no tracking" rather than ++ * aborting the server. */ ++ if (access_tracking) *access_tracking = 0; ++ if (access_tracking_reads) *access_tracking_reads = 0; ++ if (access_tracking_writes) *access_tracking_writes = 0; ++ return KERN_SUCCESS; + }; + + kern_return_t mach_memory_entry_ownership(ipc_port_t entry_port, task_t owner, int ledger_tag, int ledger_flags) { +- dtape_stub_unsafe(); ++ /* Ledger ownership is not modeled; accept and ignore. */ ++ return KERN_SUCCESS; + }; + + kern_return_t mach_memory_entry_purgable_control(ipc_port_t entry_port, vm_purgable_t control, int* state) { +- dtape_stub_unsafe(); ++ /* Not purgable here. */ ++ if (state) *state = VM_PURGABLE_NONVOLATILE; ++ return KERN_SUCCESS; + }; + + kern_return_t mach_memory_info(host_priv_t host, mach_zone_name_array_t* namesp, mach_msg_type_number_t* namesCntp, mach_zone_info_array_t* infop, mach_msg_type_number_t* infoCntp, mach_memory_info_array_t* memoryInfop, mach_msg_type_number_t* memoryInfoCntp) { +@@ -591,8 +791,115 @@ kern_return_t mach_vm_machine_attribute(vm_map_t map, mach_vm_address_t addr, ma + }; + + kern_return_t mach_vm_map_external(vm_map_t target_map, mach_vm_offset_t* address, mach_vm_size_t initial_size, mach_vm_offset_t mask, int flags, ipc_port_t port, vm_object_offset_t offset, boolean_t copy, vm_prot_t cur_protection, vm_prot_t max_protection, vm_inherit_t inheritance) { +- dtape_stub_safe(); +- return KERN_SUCCESS; ++ /* ++ * darling-lol: real implementation. This used to be a "safe" stub ++ * returning KERN_SUCCESS without mapping anything or setting ++ * *address -- so every VM_FLAGS_ANYWHERE caller (notably libplatform's ++ * os_alloc heap via mach_vm_map, which backs ALL os_alloc_once ++ * singletons incl. libnotify globals) got success plus a stale address ++ * and crashed later on the unmapped pointer. Two cases: ++ * ++ * - MEMORY_OBJECT_NULL port: anonymous mapping. Allocate fresh pages ++ * in the target task (zero-filled by the host mmap underneath). ++ * - named-entry port (from _mach_make_memory_entry): map a duplicate ++ * of the entry's kernel-buffer copy at the requested offset. The ++ * entry keeps its own copy so it stays mappable (and destroyable) ++ * afterwards. NOTE: this gives copy semantics, not true cross-task ++ * sharing; sufficient to unblock, revisit if sharing artifacts show. ++ */ ++ int prot = 0; ++ if (cur_protection & VM_PROT_READ) ++ prot |= PROT_READ; ++ if (cur_protection & VM_PROT_WRITE) ++ prot |= PROT_WRITE; ++ if (cur_protection & VM_PROT_EXECUTE) ++ prot |= PROT_EXEC; ++ ++ if (target_map == VM_MAP_NULL) { ++ target_map = current_map(); ++ } ++ if (target_map == VM_MAP_NULL || address == NULL) { ++ return KERN_INVALID_ARGUMENT; ++ } ++ ++ if (port == MEMORY_OBJECT_NULL) { ++ uintptr_t hint = (flags & VM_FLAGS_ANYWHERE) ? 0 : (uintptr_t)*address; ++ uintptr_t addr = dtape_hooks->task_allocate_pages( ++ target_map->dtape_task->context, ++ dtape_byte_count_to_page_count_round_up(initial_size), ++ prot, hint, 0); ++ if (addr == 0) { ++ return KERN_RESOURCE_SHORTAGE; ++ } ++ *address = (mach_vm_offset_t)addr; ++ return KERN_SUCCESS; ++ } ++ ++ if (IP_VALID(port) && ip_kotype(port) == IKOT_NAMED_ENTRY) { ++ vm_named_entry_t entry = (vm_named_entry_t)ip_get_kobject(port); ++ dtape_map_shared_descriptor_t* desc = NULL; ++ vm_map_offset_t entry_off = 0; ++ vm_map_size_t entry_size = 0; ++ vm_prot_t entry_prot = VM_PROT_NONE; ++ uintptr_t map_addr; ++ uint64_t page_off; ++ ++ named_entry_lock(entry); ++ desc = (dtape_map_shared_descriptor_t*)entry->dtape_descriptor; ++ entry_off = entry->offset; ++ entry_size = entry->size; ++ entry_prot = entry->protection; ++ if (desc != NULL) { ++ dtape_map_shared_descriptor_retain(desc); ++ } ++ named_entry_unlock(entry); ++ ++ if (desc == NULL) { ++ return KERN_INVALID_ARGUMENT; ++ } ++ /* darling-lol M53: enforce the entry's max protection. XNU ++ * refuses a writable mapping of a read-only entry with ++ * KERN_INVALID_RIGHT, and Chromium's shared-memory Take() ++ * depends on exactly that signal to detect read-only regions ++ * (without it, every readonly Mojo SHM probe "succeeds" ++ * writable and the GPU dies in a CHECK). Mask to VM_PROT bits: ++ * creation permission carries MAP_MEM_NAMED_CREATE etc. */ ++ if ((cur_protection & (VM_PROT_READ | VM_PROT_WRITE | VM_PROT_EXECUTE) & ++ ~(entry_prot & (VM_PROT_READ | VM_PROT_WRITE | VM_PROT_EXECUTE))) != 0) { ++ dtape_map_shared_descriptor_release(desc); ++ return KERN_INVALID_RIGHT; ++ } ++ if ((vm_map_offset_t)offset >= entry_size) { ++ dtape_map_shared_descriptor_release(desc); ++ return KERN_INVALID_ARGUMENT; ++ } ++ /* Map the shared object itself (MAP_SHARED in the target), so ++ * all tasks mapping the entry observe the same pages. */ ++ page_off = ((uint64_t)entry_off + (uint64_t)offset) / 4096; ++ if ((flags & VM_FLAGS_ANYWHERE) != 0) { ++ map_addr = dtape_hooks->task_map_file( ++ target_map->dtape_task->context, ++ desc->memfd, ++ dtape_byte_count_to_page_count_round_up(initial_size), ++ prot, 0, page_off, dtape_memory_flag_none); ++ } else { ++ map_addr = dtape_hooks->task_map_file( ++ target_map->dtape_task->context, ++ desc->memfd, ++ dtape_byte_count_to_page_count_round_up(initial_size), ++ prot, (uintptr_t)*address, page_off, ++ dtape_memory_flag_fixed | dtape_memory_flag_overwrite); ++ } ++ dtape_map_shared_descriptor_release(desc); ++ if (map_addr == 0) { ++ return KERN_FAILURE; ++ } ++ *address = (mach_vm_offset_t)map_addr + ++ (((uint64_t)entry_off + (uint64_t)offset) % 4096); ++ return KERN_SUCCESS; ++ } ++ ++ return KERN_INVALID_ARGUMENT; + }; + + kern_return_t mach_vm_msync(vm_map_t map, mach_vm_offset_t address, mach_vm_size_t size, vm_sync_t sync_flags) { +@@ -856,7 +1163,7 @@ static kern_return_t mach_vm_remap_external_shared(vm_map_t target_map, mach_vm_ + + src_existing_entry_count = dtape_map_find_shared_entries_locked(src_map, start_memaddr, map_size, src_existing_entries, sizeof(src_existing_entries) / sizeof(*src_existing_entries)); + +- if (src_existing_entry_count == 1 && src_existing_entries[0]->address == start_memaddr && src_existing_entries[0]->size >= map_size) { ++ if (src_existing_entry_count == 1 && src_existing_entries[0]->address == start_memaddr && src_existing_entries[0]->size == map_size) { + // special case for what LLDB does with dyld info + dtape_map_shared_descriptor_retain(src_existing_entries[0]->descriptor); + descriptor = src_existing_entries[0]->descriptor; +@@ -1441,6 +1748,10 @@ mach_destroy_memory_entry( + named_entry->ref_count -= 1; + + if (named_entry->ref_count == 0) { ++ /* darling-lol: release duct-tape shared backing first. */ ++ dtape_map_shared_descriptor_t* desc = ++ (dtape_map_shared_descriptor_t*)named_entry->dtape_descriptor; ++ named_entry->dtape_descriptor = NULL; + if (named_entry->is_sub_map) { + vm_map_deallocate(named_entry->backing.map); + } else if (named_entry->is_copy) { +@@ -1456,6 +1767,9 @@ mach_destroy_memory_entry( + + named_entry_unlock(named_entry); + named_entry_lock_destroy(named_entry); ++ if (desc != NULL) { ++ dtape_map_shared_descriptor_release(desc); ++ } + + #if VM_NAMED_ENTRY_LIST + lck_mtx_lock_spin(&vm_named_entry_list_lock_data); +diff --git a/duct-tape/src/misc.c b/duct-tape/src/misc.c +index afe60fc..02968b2 100644 +--- a/duct-tape/src/misc.c ++++ b/duct-tape/src/misc.c +@@ -48,6 +48,9 @@ ssize_t getrandom(void* buf, size_t buflen, unsigned int flags); + + + void dtape_logv(dtape_log_level_t level, const char* format, va_list args) { ++ if (dtape_hooks->log_enabled && !dtape_hooks->log_enabled(level)) { ++ return; ++ } + char message[4096]; + vsnprintf(message, sizeof(message), format, args); + dtape_hooks->log(level, message); +@@ -87,10 +90,28 @@ int scnprintf(char* buffer, size_t buffer_size, const char* format, ...) { + void (ipc_kmsg_trace_send)(ipc_kmsg_t kmsg, mach_msg_option_t option) { + pid_t dest_pid = -1; + ipc_port_t dest = kmsg->ikm_header->msgh_remote_port; ++ mach_msg_header_t* h = kmsg->ikm_header; + + dest_pid = ipc_port_get_receiver_task(dest, NULL); + +- dtape_log_debug("sending kmsg %p to pid %d", kmsg, dest_pid); ++ /* darling-lol M58 IPC probe: log enough of the Mach message to identify ++ * the Chromium/Mojo request or reply. Mojo's ChannelMac puts the Mojo ++ * message (with its 4-byte interface ordinal) in the Mach payload; the ++ * offset depends on the message layout, so dump a few words. */ ++ { ++ unsigned char* p = (unsigned char*)h; ++ unsigned int size = h->msgh_size; ++ unsigned int w88 = 0, w92 = 0, w96 = 0, w100 = 0, w104 = 0, w108 = 0; ++ if (size >= 92) w88 = *(unsigned int*)(p + 88); ++ if (size >= 96) w92 = *(unsigned int*)(p + 92); ++ if (size >= 100) w96 = *(unsigned int*)(p + 96); ++ if (size >= 104) w100 = *(unsigned int*)(p + 100); ++ if (size >= 108) w104 = *(unsigned int*)(p + 104); ++ if (size >= 112) w108 = *(unsigned int*)(p + 108); ++ dtape_log_debug("sending kmsg to pid %d id=%#x size=%u bits=%#x b88=%08x b92=%08x b96=%08x b100=%08x b104=%08x b108=%08x", ++ dest_pid, h->msgh_id, size, h->msgh_bits, ++ w88, w92, w96, w100, w104, w108); ++ } + }; + + void Assert(const char* file, int line, const char* expression) { +diff --git a/duct-tape/src/stubs.c b/duct-tape/src/stubs.c +index dc1cfa5..242c6da 100644 +--- a/duct-tape/src/stubs.c ++++ b/duct-tape/src/stubs.c +@@ -37,9 +37,18 @@ extern char* getenv(const char* name); + + static bool log_safe_stubs = false; + ++static bool nonfatal_unsafe_stubs = false; ++ + __attribute__((constructor)) + static void init_stubs() { + log_safe_stubs = getenv("DTAPE_LOG_SAFE_STUBS") != NULL; ++ /* darling-lol: optional escape hatch. When set, an "unsafe" (unimplemented, ++ * needs-real-impl) duct-tape stub logs but does NOT abort darlingserver. ++ * Aborting the server is catastrophic: it is a single process serving every ++ * guest process, so one unimplemented trap (e.g. a Chromium mach call) tears ++ * down the whole container's RPC, and every guest then fails with -111. ++ * Default remains fatal so genuine bugs stay loud during development. */ ++ nonfatal_unsafe_stubs = getenv("DTAPE_NONFATAL_UNSAFE") != NULL; + }; + + void dtape_stub_log(const char* function_name, int safety, const char* subsection) { +@@ -57,7 +66,7 @@ void dtape_stub_log(const char* function_name, int safety, const char* subsectio + kind_info = ""; + } else if (safety < 0) { + log_level = dtape_log_level_error; +- do_abort = true; ++ do_abort = !nonfatal_unsafe_stubs; + kind_info = " (unsafe)"; + } else { + if (!log_safe_stubs) { +@@ -210,7 +219,9 @@ kern_return_t kmod_get_info(host_t host, kmod_info_array_t* kmod_list, mach_msg_ + }; + + kern_return_t kext_request(host_priv_t hostPriv, uint32_t clientLogSpec, vm_offset_t requestIn, mach_msg_type_number_t requestLengthIn, vm_offset_t* responseOut, mach_msg_type_number_t* responseLengthOut, vm_offset_t* logDataOut, mach_msg_type_number_t* logDataLengthOut, kern_return_t* op_result) { +- dtape_stub_unsafe(); ++ // there are no kernel extensions to query or load; fail the request ++ dtape_stub_safe(); ++ return KERN_NOT_SUPPORTED; + }; + + uint32_t PE_i_can_has_debugger(uint32_t* something) { +diff --git a/duct-tape/src/task.c b/duct-tape/src/task.c +index a9eb4bc..b77187d 100644 +--- a/duct-tape/src/task.c ++++ b/duct-tape/src/task.c +@@ -521,7 +521,9 @@ kern_return_t task_info(task_t xtask, task_flavor_t flavor, task_info_t task_inf + }; + + default: +- dtape_stub_unsafe("unimplemented flavor"); ++ /* Unsupported task-info flavors are guest API errors, not fatal ++ * host errors. Chromium probes optional power/accounting data. */ ++ return KERN_INVALID_ARGUMENT; + } + }; + +diff --git a/duct-tape/src/thread.c b/duct-tape/src/thread.c +index f4ce909..e14fe79 100644 +--- a/duct-tape/src/thread.c ++++ b/duct-tape/src/thread.c +@@ -53,6 +53,7 @@ dtape_thread_t* dtape_thread_create(dtape_task_t* task, uint64_t nsid, void* con + thread->processing_signal = false; + thread->name = NULL; + thread->waiting_suspended = false; ++ libsimple_lock_init(&thread->block_suspend_gate); + LIST_INIT(&thread->user_states); + dtape_mutex_init(&thread->suspension_mutex); + dtape_condvar_init(&thread->suspension_condvar); +@@ -521,11 +522,16 @@ wait_result_t thread_block_parameter(thread_continue_t continuation, void* param + thread->xnu_thread.parameter = parameter; + + bool waiting = thread->xnu_thread.state & TH_WAIT; ++ /* A wakeup between this check and Thread::suspend used to be ignored ++ * because the microthread was not suspended yet. Keep the gate locked ++ * until the server has yielded and published the suspended state. */ ++ if (waiting) ++ libsimple_lock_lock(&thread->block_suspend_gate); + + thread_unlock(&thread->xnu_thread); + + if (waiting) { +- dtape_hooks->thread_suspend(thread->context, continuation ? thread_continuation_callback : NULL, thread, NULL); ++ dtape_hooks->thread_suspend(thread->context, continuation ? thread_continuation_callback : NULL, thread, &thread->block_suspend_gate); + } + + thread_lock(&thread->xnu_thread); +@@ -548,8 +554,20 @@ wait_result_t thread_block(thread_continue_t continuation) { + // thread locked + boolean_t thread_unblock(thread_t xthread, wait_result_t wresult) { + dtape_thread_t* thread = dtape_thread_for_xnu_thread(xthread); ++ libsimple_lock_lock(&thread->block_suspend_gate); + thread->xnu_thread.wait_result = wresult; ++ /* Match XNU's thread_unblock: a timer from a previous timed wait must ++ * not expire during the thread's next, untimed Mach receive. */ ++ if (thread->xnu_thread.wait_timer_is_set) { ++ if (timer_call_cancel(&thread->xnu_thread.wait_timer)) { ++ thread->xnu_thread.wait_timer_active--; ++ } ++ thread->xnu_thread.wait_timer_is_set = FALSE; ++ } ++ thread->xnu_thread.state &= ~(TH_WAIT | TH_UNINT); ++ thread->xnu_thread.state |= TH_RUN; + dtape_hooks->thread_resume(thread->context); ++ libsimple_lock_unlock(&thread->block_suspend_gate); + return TRUE; + }; + +@@ -1324,11 +1342,11 @@ kern_return_t thread_wire(host_priv_t host_priv, thread_t thread, boolean_t wire + }; + + kern_return_t thread_getstatus_to_user(thread_t thread, int flavor, thread_state_t tstate, mach_msg_type_number_t* count) { +- dtape_stub_unsafe(); ++ return thread_get_state_internal(thread, flavor, tstate, count, TRUE); + }; + + kern_return_t thread_setstatus_from_user(thread_t thread, int flavor, thread_state_t tstate, mach_msg_type_number_t count) { +- dtape_stub_unsafe(); ++ return thread_set_state_from_user(thread, flavor, tstate, count); + }; + + boolean_t thread_should_abort(thread_t thread) { +diff --git a/duct-tape/src/timer.c b/duct-tape/src/timer.c +index bbaab01..4f01234 100644 +--- a/duct-tape/src/timer.c ++++ b/duct-tape/src/timer.c +@@ -53,8 +53,9 @@ uint64_t _rtc_nanotime_read(pal_rtc_nanotime_t* rntp) { + }; + + void dtape_timer_fired(void) { +- uint64_t next_deadline = timer_queue_expire(&timer_queue, mach_absolute_time()); +- dtape_hooks->timer_arm(next_deadline, true); ++ /* Expiration rearms while holding the queue lock. Rearming here from ++ * its returned snapshot could overwrite a concurrent insertion. */ ++ timer_queue_expire(&timer_queue, mach_absolute_time()); + }; + + void timer_call_nosync_cpu(int cpu, void (*fn)(void* arg), void* arg) { +@@ -62,7 +63,8 @@ void timer_call_nosync_cpu(int cpu, void (*fn)(void* arg), void* arg) { + }; + + mpqueue_head_t* timer_queue_assign(uint64_t deadline) { +- dtape_hooks->timer_arm(deadline, false); ++ /* Arm only after insertion, under the queue lock. A short deadline ++ * can otherwise expire before its call has reached the queue. */ + return &timer_queue; + }; + +diff --git a/duct-tape/xnu/osfmk/ipc/ipc_kmsg.c b/duct-tape/xnu/osfmk/ipc/ipc_kmsg.c +index 7c2559b..712ff8f 100644 +--- a/duct-tape/xnu/osfmk/ipc/ipc_kmsg.c ++++ b/duct-tape/xnu/osfmk/ipc/ipc_kmsg.c +@@ -77,6 +77,7 @@ + #include + #include + #include ++#include /* darling-lol M58g1: dtape_log_error */ + #include + #include + +@@ -2497,6 +2498,10 @@ ipc_kmsg_put( + } + + if (copyoutmsg((const char *) kmsg->ikm_header, rcv_addr, size)) { ++ dtape_log_error("M58G ipc_kmsg_put copyout failed size=%u rcv_size=%u rcv_addr=0x%llx option=0x%x bits=0x%x id=%d task=%p map=%p", ++ size, (unsigned)rcv_size, (unsigned long long) rcv_addr, (unsigned) option, ++ (unsigned) kmsg->ikm_header->msgh_bits, (int) kmsg->ikm_header->msgh_id, ++ (void*) current_task(), (void*) current_map()); + mr = MACH_RCV_INVALID_DATA; + size = 0; + } else { +diff --git a/duct-tape/xnu/osfmk/ipc/ipc_mqueue.c b/duct-tape/xnu/osfmk/ipc/ipc_mqueue.c +index 98ad438..221a1be 100644 +--- a/duct-tape/xnu/osfmk/ipc/ipc_mqueue.c ++++ b/duct-tape/xnu/osfmk/ipc/ipc_mqueue.c +@@ -87,6 +87,7 @@ + #include + #include + #include ++#include /* darling-lol M58bc: dtape_log_error */ + #include + #include + #include +@@ -757,6 +758,25 @@ ipc_mqueue_post( + + ipc_kmsg_trace_send(kmsg, option); + ++ { ++ /* darling-lol M58bc: mach-message delivery trace. Logs the receiver's ++ * pid, message id and size for each enqueue, so we can see whether a ++ * message addressed to the renderer's port is delivered. Capped. */ ++ ipc_port_t __m58bc_port = ip_from_mq(mqueue); ++ if (__m58bc_port != IP_NULL && ip_active(__m58bc_port)) { ++ int __m58bc_rpid = ipc_port_get_receiver_task(__m58bc_port, NULL); ++ mach_msg_size_t __m58bc_sz = (kmsg->ikm_header != 0) ? kmsg->ikm_header->msgh_size : 0; ++ int __m58bc_id = (kmsg->ikm_header != 0) ? (int) kmsg->ikm_header->msgh_id : 0; ++#if 0 /* darling-lol M58F173: disabled again (per-message dserver log overhead) */ ++ static unsigned long __m58bc_n = 0; ++ if (__m58bc_n++ < 200000) { ++ dtape_log_error("M58BC post rpid=%d id=%d size=%u port=%p", ++ __m58bc_rpid, __m58bc_id, (unsigned) __m58bc_sz, (void*)__m58bc_port); ++ } ++#endif ++ } ++ } ++ + /* + * While the msg queue is locked, we have control of the + * kmsg, so the ref in it for the port is still good. +@@ -786,6 +806,24 @@ ipc_mqueue_post( + WAITQ_KEEP_LOCKED); + /* waitq still locked, thread locked */ + ++ /* darling-lol M58RD: record whether the post found a blocked receiver ++ * or had to queue. A queued message while a thread is parked in ++ * ipc_mqueue_receive on the SAME port means a lost wakeup. */ ++#if 0 /* darling-lol M58F173: disabled again (per-message dserver log overhead) */ ++ { ++ static unsigned long __m58rd_n = 0; ++ if (__m58rd_n++ < 200000) { ++ ipc_port_t __m58rd_port = ip_from_mq(mqueue); ++ int __m58rd_rpid = (__m58rd_port != IP_NULL && ip_active(__m58rd_port)) ++ ? ipc_port_get_receiver_task(__m58rd_port, NULL) : -1; ++ int __m58rd_id = (kmsg->ikm_header != 0) ? (int)kmsg->ikm_header->msgh_id : 0; ++ dtape_log_error("M58RD %s rpid=%d id=%d port=%p rcv=%p", ++ (receiver == THREAD_NULL) ? "QUEUED" : "HANDOFF", ++ __m58rd_rpid, __m58rd_id, (void*)__m58rd_port, (void*)receiver); ++ } ++ } ++#endif ++ + if (receiver == THREAD_NULL) { + /* + * no receivers; queue kmsg if space still reserved +@@ -1222,6 +1260,23 @@ ipc_mqueue_receive_on_thread( + TURNSTILE_DELAYED_UPDATE); + } + ++ /* darling-lol M58RE: receive-side trace. When a thread is about to block ++ * on a mach-message receive, log the receiver pid and the port it waits on ++ * (pointer identity). Pair with M58BC posts (same port=) to see whether the ++ * expected message is enqueued on this exact port. Capped. */ ++#if 0 /* darling-lol M58F151: disabled (per-receive dserver log overhead) */ ++ { ++ static unsigned long __m58re_n = 0; ++ if (__m58re_n++ < 200000) { ++ ipc_port_t __m58re_port = ip_from_mq(mqueue); ++ int __m58re_rpid = (__m58re_port != IP_NULL && ip_active(__m58re_port)) ++ ? ipc_port_get_receiver_task(__m58re_port, NULL) : -1; ++ dtape_log_error("M58RE recv rpid=%d tid=%p port=%p opt=0x%x", ++ __m58re_rpid, (void*)thread, (void*)__m58re_port, (unsigned)option); ++ } ++ } ++#endif ++ + thread_set_pending_block_hint(thread, kThreadWaitPortReceive); + wresult = waitq_assert_wait64_locked(&mqueue->imq_wait_queue, + IPC_MQUEUE_RECEIVE, +diff --git a/duct-tape/xnu/osfmk/ipc/ipc_object.c b/duct-tape/xnu/osfmk/ipc/ipc_object.c +index 5e2fdb8..2352d43 100644 +--- a/duct-tape/xnu/osfmk/ipc/ipc_object.c ++++ b/duct-tape/xnu/osfmk/ipc/ipc_object.c +@@ -89,6 +89,7 @@ + #include + #include + #include ++#include /* darling-lol M58DD: copyout diagnostics */ + #include + #include + +@@ -1016,6 +1017,13 @@ ipc_object_copyout( + is_write_unlock(space); + + out: ++ /* Successful copyouts are a hot path; diagnostic logging here slows ++ * normal Mach traffic and fills dserver.log during Riot startup. */ ++ if (kr != KERN_SUCCESS) { ++ dtape_log_error("M58DD copyout-fail msgt=0x%x kr=%d", ++ (unsigned) msgt_name, (int) kr); ++ } ++ + if (kr == KERN_SUCCESS) { + *namep = name; + } else if (IO_VALID(object)) { +diff --git a/duct-tape/xnu/osfmk/ipc/mach_msg.c b/duct-tape/xnu/osfmk/ipc/mach_msg.c +index f073413..0dd19db 100644 +--- a/duct-tape/xnu/osfmk/ipc/mach_msg.c ++++ b/duct-tape/xnu/osfmk/ipc/mach_msg.c +@@ -76,7 +76,7 @@ + #include + #include + #include +- ++#include /* darling-lol M58g1: dtape_log_error */ + #include + #include + #include +@@ -397,6 +397,14 @@ mach_msg_receive_results( + + mr = ipc_kmsg_copyout(kmsg, space, map, MACH_MSG_BODY_NULL, option); + ++ /* darling-lol M58g1: log receive-side failures to find the ++ * waitable-event receive failure (MACH_RCV_INVALID_DATA 5/5 in the ++ * harness repro, tools/m58g-machmsg-repro.c). */ ++ if (mr != MACH_MSG_SUCCESS) { ++ dtape_log_error("M58G recv-err mr=0x%08x msgh_size=%u rcv_size=%u", ++ mr, kmsg->ikm_header->msgh_size, (unsigned)rcv_size); ++ } ++ + trailer_size = ipc_kmsg_trailer_size(option, self); + + if (mr != MACH_MSG_SUCCESS) { +@@ -779,6 +787,9 @@ msg_receive_error( + sizeof(trailer_template)); + + trailer_size = ipc_kmsg_trailer_size(option, self); ++ /* darling-lol M58g1: track msg_receive_error's copyout inputs. */ ++ dtape_log_error("M58G receive-error trailer_size=%u rcv_size=%u", ++ (unsigned)trailer_size, (unsigned)rcv_size); + ipc_kmsg_add_trailer(kmsg, space, option, self, + seqno, TRUE, context); + +diff --git a/duct-tape/xnu/osfmk/ipc/mach_port.c b/duct-tape/xnu/osfmk/ipc/mach_port.c +index deaa574..3adbde2 100644 +--- a/duct-tape/xnu/osfmk/ipc/mach_port.c ++++ b/duct-tape/xnu/osfmk/ipc/mach_port.c +@@ -101,6 +101,7 @@ + + #if IMPORTANCE_INHERITANCE + #include ++#include /* darling-lol M58DB: mod_refs diagnostics */ + #endif + + kern_return_t mach_port_get_attributes(ipc_space_t space, mach_port_name_t name, +@@ -999,12 +1000,18 @@ mach_port_mod_refs( + kr = ipc_right_lookup_write(space, name, &entry); + if (kr != KERN_SUCCESS) { + mach_port_guard_exception(name, 0, 0, kGUARD_EXC_INVALID_NAME); ++ dtape_log_error("M58DB mod_refs LOOKUP-FAIL name=%u right=%d delta=%d kr=%d", ++ (unsigned) name, (int) right, (int) delta, (int) kr); + return kr; + } + + /* space is write-locked and active */ + + kr = ipc_right_delta(space, name, entry, right, delta); /* unlocks */ ++ if (kr != KERN_SUCCESS) { ++ dtape_log_error("M58DB mod_refs DELTA-FAIL name=%u right=%d delta=%d kr=%d", ++ (unsigned) name, (int) right, (int) delta, (int) kr); ++ } + return kr; + } + +diff --git a/duct-tape/xnu/osfmk/kern/ipc_kobject.c b/duct-tape/xnu/osfmk/kern/ipc_kobject.c +index 408cb36..bbddfca 100644 +--- a/duct-tape/xnu/osfmk/kern/ipc_kobject.c ++++ b/duct-tape/xnu/osfmk/kern/ipc_kobject.c +@@ -148,6 +148,8 @@ + #include + #include + #include ++/* darling-lol M54: fileport kobject lives in duct-tape/src/fileport.c */ ++extern void dtape_fileport_destroy(struct ipc_port* port); + #include + + #if HYPERVISOR +@@ -1045,6 +1047,11 @@ ipc_kobject_destroy( + suid_cred_destroy(port); + break; + ++ /* darling-lol M54: fileports. */ ++ case IKOT_FILEPORT: ++ dtape_fileport_destroy(port); ++ break; ++ + default: + break; + } +diff --git a/duct-tape/xnu/osfmk/kern/ipc_misc.c b/duct-tape/xnu/osfmk/kern/ipc_misc.c +index 6e7638f..76f680e 100644 +--- a/duct-tape/xnu/osfmk/kern/ipc_misc.c ++++ b/duct-tape/xnu/osfmk/kern/ipc_misc.c +@@ -127,7 +127,8 @@ fileport_notify(mach_msg_header_t *msg) + if (port->ip_srights == 0) { + ip_unlock(port); + +- fileport_releasefg(fg); ++ /* Darling stores a host FD kobject, not a BSD fileglob. ++ * ipc_kobject_destroy performs its one close/free on destruction. */ + ipc_port_dealloc_kernel(port); + } else { + ip_unlock(port); +diff --git a/duct-tape/xnu/osfmk/kern/timer_call.c b/duct-tape/xnu/osfmk/kern/timer_call.c +index 4983e26..fd37050 100644 +--- a/duct-tape/xnu/osfmk/kern/timer_call.c ++++ b/duct-tape/xnu/osfmk/kern/timer_call.c +@@ -385,6 +385,13 @@ timer_call_entry_enqueue_deadline( + } + new_mpqueue->count++; + ++#ifdef __DARLING__ ++ /* Publish the actual queue head after insertion, while serialized with ++ * expiration and cancellation. The timerfd must never reflect a stale ++ * pre-insertion or post-expiration snapshot. */ ++ timer_queue_cancel(new_mpqueue, deadline, thead->tc_pqlink.deadline); ++#endif ++ + return old_mpqueue; + } + +@@ -1003,6 +1010,9 @@ timer_queue_expire_with_options( + queue->earliest_soft_deadline = cur_deadline = UINT64_MAX; + } + ++#ifdef __DARLING__ ++ timer_queue_cancel(queue, deadline, cur_deadline); ++#endif + timer_queue_unlock(queue); + + return cur_deadline; +diff --git a/duct-tape/xnu/osfmk/vm/vm_map.h b/duct-tape/xnu/osfmk/vm/vm_map.h +index 15fa8c8..4321a91 100644 +--- a/duct-tape/xnu/osfmk/vm/vm_map.h ++++ b/duct-tape/xnu/osfmk/vm/vm_map.h +@@ -205,6 +205,12 @@ struct vm_named_entry { + #define NAMED_ENTRY_BT_DEPTH 16 + void *named_entry_bt[NAMED_ENTRY_BT_DEPTH]; + #endif /* VM_NAMED_ENTRY_LIST */ ++ /* darling-lol: duct-tape shared backing. XNU's object model is not ++ * wired up here; instead an entry may carry a shared memfd descriptor ++ * (see dtape_map_shared_descriptor in duct-tape/src/memory.c) that is ++ * mapped into tasks on demand, giving true cross-task sharing. ++ * Opaque pointer to avoid header coupling; NULL when unused. */ ++ void *dtape_descriptor; + }; + + /* +diff --git a/internal-include/darlingserver/logging.hpp b/internal-include/darlingserver/logging.hpp +index d46bdb4..9a58727 100644 +--- a/internal-include/darlingserver/logging.hpp ++++ b/internal-include/darlingserver/logging.hpp +@@ -41,6 +41,7 @@ namespace DarlingServer { + Error = 4, + }; + ++ bool _enabled(Type type) const; + void _log(Type type, std::string message) const; + static std::string _typeToString(Type type); + +@@ -57,6 +58,9 @@ namespace DarlingServer { + class Stream; + friend class Stream; + ++ /// whether a message of this level (1 = debug .. 4 = error) would be written; cheap, for skipping formatting work ++ bool levelEnabled(int level) const { return _enabled(static_cast(level)); }; ++ + Stream debug() const; + Stream info() const; + Stream warning() const; +@@ -74,7 +78,7 @@ namespace DarlingServer { + private: + Type _type; + const Log& _log; +- std::ostringstream _buffer; ++ std::unique_ptr _buffer; + + Stream(Type type, const Log& log); + +@@ -91,7 +95,9 @@ namespace DarlingServer { + + template + std::enable_if_t, Stream&> operator<<(const T& value) { +- _buffer << value; ++ if (_buffer) { ++ *_buffer << value; ++ } + return *this; + }; + }; +diff --git a/internal-include/darlingserver/message.hpp b/internal-include/darlingserver/message.hpp +index 708f3f3..56218a5 100644 +--- a/internal-include/darlingserver/message.hpp ++++ b/internal-include/darlingserver/message.hpp +@@ -71,6 +71,11 @@ namespace DarlingServer { + void _ensureDescriptorHeader(size_t descriptorSpace); + + public: ++ /** Called once after the server's final fork/privilege drop, before workers. ++ * The server does not change its native identity afterwards. Generic Message ++ * users retain live credentials unless they explicitly initialize this cache. ++ */ ++ static void initializeServerCredentials(); + /** + * Default-initializes a Message. + * +diff --git a/internal-include/darlingserver/process.hpp b/internal-include/darlingserver/process.hpp +index c97be30..31772dc 100644 +--- a/internal-include/darlingserver/process.hpp ++++ b/internal-include/darlingserver/process.hpp +@@ -94,6 +94,13 @@ namespace DarlingServer { + std::shared_ptr _vchrootDescriptor; + dtape_task_t* _dtapeTask; + std::weak_ptr _parentProcess; ++ ++ // /proc//maps, kept open for PROCMAP_QUERY region lookups (opening it per lookup cost more than the query). ++ // Guarded by its own mutex: a stale descriptor is closed and reopened, and a concurrent ioctl on a closed and ++ // reused descriptor number would hit an unrelated file. ++ mutable std::mutex _mapsMutex; ++ mutable int _mapsFd = -1; ++ int _procmapQuery(uint64_t address, uint64_t flags, void* query) const; + bool _startSuspended = false; + bool _pendingReplacement = false; + std::unordered_map> _kqchannels; +diff --git a/scripts/generate-rpc-wrappers.py b/scripts/generate-rpc-wrappers.py +index e35221c..1068426 100755 +--- a/scripts/generate-rpc-wrappers.py ++++ b/scripts/generate-rpc-wrappers.py +@@ -268,6 +268,22 @@ calls = [ + ('socket', '@fd'), + ]), + ++ # ++ # darling-lol M54: fileports (Mojo FD-over-Mach transport) ++ # ++ ++ ('fileport_makeport', [ ++ ('fd', '@fd'), ++ ], [ ++ ('port_name', 'uint32_t'), ++ ]), ++ ++ ('fileport_makefd', [ ++ ('port_name', 'uint32_t'), ++ ], [ ++ ('fd', '@fd'), ++ ]), ++ + # + # pthread cancelation + # +diff --git a/src/call.cpp b/src/call.cpp +index cc10f1d..8c8d6b3 100644 +--- a/src/call.cpp ++++ b/src/call.cpp +@@ -18,9 +18,20 @@ + */ + + #define _GNU_SOURCE 1 ++#include ++#include ++#include ++#include ++#include ++#include + #include + #include + #include ++#include ++#include ++#include ++#include ++#include + + #include + #include +@@ -29,16 +40,76 @@ + #include + #include + ++static bool m634TraceEnabled() { ++ static const bool enabled = [] { ++ const char* value = std::getenv("DARLING_M634_TRACE"); ++ return value && value[0] == '1' && value[1] == '\0'; ++ }(); ++ return enabled; ++} ++ + static DarlingServer::Log callLog("calls"); + + DarlingServer::Log DarlingServer::Call::rpcReplyLog("replies"); + ++// darling-lol: opt-in request statistics. With DSERVER_CALL_STATS= set, every 5 s the server appends how many ++// requests of each call number arrived and which (pid, tid) pairs sent the most, then starts over. Costs one getenv ++// at startup and a branch per request when off. callFromMessage runs on the server's single message loop thread. ++static void callStatsRecord(unsigned number, int pid, int tid) { ++ static const char* path = getenv("DSERVER_CALL_STATS"); ++ if (!path || !path[0]) { ++ return; ++ } ++ static std::map byNumber; ++ static std::map, unsigned long> byThreadCall; ++ static std::map, unsigned long> byThread; ++ static unsigned long total = 0; ++ static auto last = std::chrono::steady_clock::now(); ++ ++ ++byNumber[number]; ++ ++byThreadCall[{tid, number}]; ++ ++byThread[{pid, tid}]; ++ ++total; ++ ++ auto now = std::chrono::steady_clock::now(); ++ if (now - last < std::chrono::seconds(5)) { ++ return; ++ } ++ double seconds = std::chrono::duration(now - last).count(); ++ last = now; ++ ++ if (FILE* file = fopen(path, "a")) { ++ fprintf(file, "calls: %lu in %.1fs (%.0f/s)\n by call number:", total, seconds, total / seconds); ++ std::vector> numbers; ++ for (auto& [num, count]: byNumber) numbers.push_back({count, num}); ++ std::sort(numbers.rbegin(), numbers.rend()); ++ for (size_t i = 0; i < numbers.size() && i < 12; ++i) fprintf(file, " #%u=%lu", numbers[i].second, numbers[i].first); ++ fprintf(file, "\n by pid/tid:"); ++ std::vector>> threads; ++ for (auto& [id, count]: byThread) threads.push_back({count, id}); ++ std::sort(threads.rbegin(), threads.rend()); ++ for (size_t i = 0; i < threads.size() && i < 12; ++i) fprintf(file, " %d/%d=%lu", threads[i].second.first, threads[i].second.second, threads[i].first); ++ fprintf(file, "\n by tid/call:"); ++ std::vector>> pairs; ++ for (auto& [id, count]: byThreadCall) pairs.push_back({count, id}); ++ std::sort(pairs.rbegin(), pairs.rend()); ++ for (size_t i = 0; i < pairs.size() && i < 24; ++i) fprintf(file, " %d:#%u=%lu", pairs[i].second.first, pairs[i].second.second, pairs[i].first); ++ fprintf(file, "\n"); ++ fclose(file); ++ } ++ byThreadCall.clear(); ++ byNumber.clear(); ++ byThread.clear(); ++ total = 0; ++}; ++ + std::shared_ptr DarlingServer::Call::callFromMessage(Message&& requestMessage) { + if (requestMessage.data().size() < sizeof(dserver_rpc_callhdr_t)) { + throw std::invalid_argument("Message buffer was too small for call header"); + } + + dserver_rpc_callhdr_t* header = reinterpret_cast(requestMessage.data().data()); ++ callStatsRecord(header->number, header->pid, header->tid); + std::shared_ptr result = nullptr; + std::shared_ptr process = nullptr; + std::shared_ptr thread = nullptr; +@@ -127,6 +198,38 @@ std::shared_ptr DarlingServer::Call::callFromMessage(Messag + auto tidString = (thread) ? (std::to_string(thread->id()) + " (" + std::to_string(thread->nsid()) + ")") : (std::to_string(header->tid) + " (-1)"); + callLog.debug() << "Received call #" << header->number << " (" << dserver_callnum_to_string(header->number) << ") from PID " << pidString << ", TID " << tidString << callLog.endLog; + ++ /* darling-lol M634: rate-limited trace of EVERY call through dserver, to ++ * see which RPCs the launcher's blocked service threads are waiting on ++ * and whether an S2C/interrupt round-trip is wedged. */ ++ if (m634TraceEnabled()) { ++ static std::atomic m634AllCount{0}; ++ unsigned seen = m634AllCount.fetch_add(1); ++ if (seen < 120 || seen % 64 == 0) { ++ fprintf(stderr, "M634RPC line=%u nr=%d pid=%d ns=%d tid=%d nstid=%d\n", ++ seen, (int)header->number, requestMessage.pid(), ++ process ? (int)process->nsid() : -1, ++ header->tid, thread ? (int)thread->nsid() : -1); ++ } ++ } ++ ++ /* darling-lol M634: bounded trace of the storm calls (semaphore_timedwait ++ * and pthread_canceled) to diagnose the League-launch wedge. */ ++ if (m634TraceEnabled() && (header->number == dserver_callnum_semaphore_timedwait || header->number == dserver_callnum_pthread_canceled)) { ++ static std::atomic m634CallCount{0}; ++ unsigned seen = m634CallCount.fetch_add(1); ++ if (seen < 200 || seen % 256 == 0) { ++ if (header->number == dserver_callnum_semaphore_timedwait) { ++ auto call64 = reinterpret_cast(requestMessage.data().data()); ++ fprintf(stderr, "M634CALL line=%u nr=64 pid=%d tid=%d name=%u sec=%u nsec=%u\n", ++ seen, requestMessage.pid(), header->tid, ++ call64->body.wait_name, call64->body.sec, call64->body.nsec); ++ } else { ++ fprintf(stderr, "M634CALL line=%u nr=33 pid=%d tid=%d\n", ++ seen, requestMessage.pid(), header->tid); ++ } ++ } ++ } ++ + if (header->number == dserver_callnum_s2c) { + // this is an S2C reply + +@@ -694,6 +797,60 @@ void DarlingServer::Call::KqchanProcOpen::processCall() { + _sendReply(code, socket); + }; + ++void DarlingServer::Call::FileportMakeport::processCall() { ++ int code = 0; ++ uint32_t port_name = 0; ++ ++ if (auto thread = _thread.lock()) { ++ if (auto process = thread->process()) { ++ // _body.fd arrives via SCM_RIGHTS; the framework closes it ++ // after this call, so dup for the kobject's own lifecycle. ++ int fd = dup(_body.fd); ++ if (fd < 0) { ++ code = -errno; ++ } else { ++ int kr = dtape_fileport_makeport(process->_dtapeTask, fd, &port_name); ++ if (kr != 0) { ++ code = -EINVAL; ++ ::close(fd); ++ } ++ } ++ } else { ++ code = -ESRCH; ++ } ++ } else { ++ code = -ESRCH; ++ } ++ ++ _sendReply(code, port_name); ++}; ++ ++void DarlingServer::Call::FileportMakefd::processCall() { ++ int code = 0; ++ int fd = -1; ++ ++ if (auto thread = _thread.lock()) { ++ if (auto process = thread->process()) { ++ int held = -1; ++ int kr = dtape_fileport_makefd(process->_dtapeTask, _body.port_name, &held); ++ if (kr != 0) { ++ code = -EINVAL; ++ } else { ++ // dtape_fileport_makefd already returns an owned duplicate. ++ // Transfer it to Message; its destructor closes the server ++ // copy after SCM_RIGHTS gives the receiver its own descriptor. ++ fd = held; ++ } ++ } else { ++ code = -ESRCH; ++ } ++ } else { ++ code = -ESRCH; ++ } ++ ++ _sendReply(code, fd); ++}; ++ + void DarlingServer::Call::ForkWaitForChild::processCall() { + int code = 0; + +@@ -714,6 +871,24 @@ void DarlingServer::Call::Sigprocess::processCall() { + int code = 0; + int newBSDSignal = 0; + ++ // darling-lol M34 TMPLOG: every guest signal delivered through sigexc. ++ // For the RC UX SIGSEGV this is the reliable place to see the crash: ++ // signal numbers + si_code + si_addr. RIP is not read here yet, but ++ // si_addr + code identify the fault class. (Chromium/Electron ++ // sigexc-routes its fatal signals through this RPC.) ++ std::string lolExe = "?"; ++ if (auto th = _thread.lock()) { ++ if (auto pr = th->process()) ++ lolExe = pr->executablePath(); ++ } ++ callLog.error() << "LOLSIG processSignal bsd=" << _body.bsd_signal_number ++ << " linux=" << _body.linux_signal_number ++ << " code=" << _body.code ++ << " addr=0x" << std::hex << _body.signal_address << std::dec ++ << " ts=0x" << std::hex << _body.thread_state << std::dec ++ << " exe=" << lolExe ++ << callLog.endLog; ++ + if (auto thread = _thread.lock()) { + try { + thread->processSignal(_body.bsd_signal_number, _body.linux_signal_number, _body.code, _body.signal_address, _body.thread_state, _body.float_state); +diff --git a/src/darlingserver.cpp b/src/darlingserver.cpp +index f79f057..2bf9cfb 100644 +--- a/src/darlingserver.cpp ++++ b/src/darlingserver.cpp +@@ -497,6 +497,45 @@ static void handle_sigusr1(int signum) { + }; + #endif + ++/* darling-lol M58f2b: darlingserver was observed exiting silently ~100s into ++ * direct-UX runs; the monitor caught exit_code=15 — a SIGTERM sent by an ++ * unknown host process. Log the sender (siginfo.si_pid + its comm/cmdline) ++ * instead of dying, so the culprit is identified. Not strictly ++ * async-signal-safe, but this is a diagnostic for a fatal path. */ ++static void handle_sigterm_diagnostic(int signum, siginfo_t* info, void* uctx) { ++ int fd = open("/tmp/dserver-signal.log", O_WRONLY | O_CREAT | O_APPEND, 0644); ++ if (fd >= 0) { ++ dprintf(fd, "darlingserver pid=%d got %s from sender pid=%ld uid=%ld\n", ++ getpid(), ++ signum == SIGTERM ? "SIGTERM" : (signum == SIGINT ? "SIGINT" : "signal"), ++ info ? (long) info->si_pid : -1L, ++ info ? (long) info->si_uid : -1L); ++ if (info && info->si_pid > 0) { ++ char path[64], comm[256], cmdline[1024]; ++ snprintf(path, sizeof(path), "/proc/%ld/comm", (long) info->si_pid); ++ int cfd = open(path, O_RDONLY); ++ if (cfd >= 0) { ++ ssize_t n = read(cfd, comm, sizeof(comm) - 1); ++ if (n > 0) { comm[n - 1] = '\0'; dprintf(fd, " sender comm: %s\n", comm); } ++ close(cfd); ++ } ++ snprintf(path, sizeof(path), "/proc/%ld/cmdline", (long) info->si_pid); ++ cfd = open(path, O_RDONLY); ++ if (cfd >= 0) { ++ ssize_t n = read(cfd, cmdline, sizeof(cmdline) - 1); ++ if (n > 0) { ++ cmdline[n] = '\0'; ++ for (ssize_t i = 0; i < n; ++i) { if (cmdline[i] == '\0') cmdline[i] = ' '; } ++ dprintf(fd, " sender cmdline: %s\n", cmdline); ++ } ++ close(cfd); ++ } ++ } ++ close(fd); ++ } ++ /* keep running: the goal is to survive teardown races and name the sender */ ++}; ++ + int main(int argc, char** argv) { + const char* prefix = NULL; + uid_t originalUID = -1; +@@ -515,6 +554,30 @@ int main(int argc, char** argv) { + char putOld[4096]; + char *p; + ++ /* darling-lol M58f2: never die from SIGPIPE. The server writes to ++ * client sockets (MessageQueue::sendMany's sendmmsg, RPC replies, ++ * async-writer) whose peers are guest processes that can be torn ++ * down at any moment (Chromium helper churn, crashes). The default ++ * SIGPIPE disposition kills the whole server SILENTLY — no core, ++ * no log — which then wedges the entire container (every guest RPC ++ * fails with ECONNREFUSED). The write paths already handle EPIPE / ++ * ECONNREFUSED errors gracefully (e.g. message.cpp sendMany drops ++ * the dead peer's message); they just never got a chance to run. ++ * Ignoring SIGPIPE makes those handlers reachable. */ ++ signal(SIGPIPE, SIG_IGN); ++ ++ /* darling-lol M58f2b: catch SIGTERM/SIGINT and log the sender instead ++ * of dying (see handle_sigterm_diagnostic above). */ ++ { ++ struct sigaction sa_diag; ++ memset(&sa_diag, 0, sizeof(sa_diag)); ++ sa_diag.sa_sigaction = handle_sigterm_diagnostic; ++ sa_diag.sa_flags = SA_SIGINFO | SA_RESTART; ++ sigemptyset(&sa_diag.sa_mask); ++ sigaction(SIGTERM, &sa_diag, NULL); ++ sigaction(SIGINT, &sa_diag, NULL); ++ } ++ + if (argc < 6) { + fprintf(stderr, "darlingserver is not meant to be started manually\n"); + exit(1); +@@ -745,6 +808,13 @@ int main(int argc, char** argv) { + #endif + + // create the server ++ // All native forks and privilege changes are complete. Snapshot only our ++ // fixed transport identity; received/per-message credentials stay independent. ++ DarlingServer::Message::initializeServerCredentials(); ++ // Explicit desktop-debugging opt-in, after dropping elevated privileges. ++ if (getenv("RCLOL_DEBUG_ATTACH")) { ++ prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY, 0, 0, 0); ++ } + auto server = new DarlingServer::Server(prefix); + + // tell the child to go ahead; the socket has been created +diff --git a/src/kqchan.cpp b/src/kqchan.cpp +index 7f766a2..1597a8c 100644 +--- a/src/kqchan.cpp ++++ b/src/kqchan.cpp +@@ -13,6 +13,27 @@ static DarlingServer::Log kqchanMachPortLog("kqchan:mach_port"); + static DarlingServer::Log kqchanProcLog("kqchan:proc"); + static std::atomic_uint64_t kqchanDebugIDCounter = 0; + ++// Opt-in bounded metadata trace; never records Mach message contents. ++#include ++#include ++#include ++#include ++static void riotKqTrace(char stage, uint64_t channel, int socket, long value = 0) { ++ static int fd = [] { ++ if (!std::getenv("RCLOL_KQTRACE")) return -1; ++ char path[96]; std::snprintf(path, sizeof(path), "/tmp/riot-kqtrace-%d.log", getpid()); ++ return open(path, O_CREAT | O_TRUNC | O_WRONLY | O_CLOEXEC, 0600); ++ }(); ++ if (fd < 0) return; ++ static std::atomic_uint64_t next{0}; ++ uint64_t index = next.fetch_add(1); ++ char row[128]; std::memset(row, ' ', sizeof(row)); ++ int n = std::snprintf(row, sizeof(row), "%llu %c channel=%llu fd=%d value=%ld", (unsigned long long)index, stage, (unsigned long long)channel, socket, value); ++ if (n >= 0 && n < 127) row[n] = ' '; ++ row[127] = '\n'; ++ pwrite(fd, row, sizeof(row), (index % 8192) * sizeof(row)); ++} ++ + // + // base class + // +@@ -67,6 +88,7 @@ int DarlingServer::Kqchan::setup() { + throw std::system_error(ret, std::generic_category()); + } + ++ riotKqTrace('S', _debugID, _socket->fd()); + std::weak_ptr weakThis = sharedFromRoot(); + + _outbox.setMessageArrivalNotificationCallback([weakThis]() { +@@ -84,6 +106,7 @@ int DarlingServer::Kqchan::setup() { + // we can send all the messages in the same context that they were pushed + do { + self->_canSend = self->_outbox.sendMany(self->_socket->fd()); ++ riotKqTrace('O', self->_debugID, self->_socket->fd(), self->_canSend); + } while (self->_canSend && !self->_outbox.empty()); + }); + +@@ -119,6 +142,7 @@ int DarlingServer::Kqchan::setup() { + + kqchanLog.debug() << *self << ": socket has pending incoming messages" << kqchanLog.endLog; + ++ riotKqTrace('I', self->_debugID, self->_socket->fd()); + // receive them all + while (self->_inbox.receiveMany(self->_socket->fd())); + +@@ -135,6 +159,7 @@ int DarlingServer::Kqchan::setup() { + kqchanLog.debug() << *self << ": socket is now writable; sending all pending outgoing messages" << kqchanLog.endLog; + do { + self->_canSend = self->_outbox.sendMany(self->_socket->fd()); ++ riotKqTrace('O', self->_debugID, self->_socket->fd(), self->_canSend); + } while (self->_canSend && !self->_outbox.empty()); + } + }); +@@ -343,6 +368,7 @@ void DarlingServer::Kqchan::MachPort::_modify(uint64_t receiveBuffer, uint64_t r + }; + + void DarlingServer::Kqchan::MachPort::_read(uint64_t defaultBuffer, uint64_t defaultBufferSize, pid_t nstid) { ++ riotKqTrace('R', _debugID, _socket->fd(), nstid); + kqchanMachPortLog.debug() << *this << ": received read request with {defaultBuffer=" << defaultBuffer << ",defaultBufferSize=" << defaultBufferSize << "}" << kqchanMachPortLog.endLog; + + { +@@ -380,6 +406,7 @@ void DarlingServer::Kqchan::MachPort::_read(uint64_t defaultBuffer, uint64_t def + + auto self = shared_from_this(); + Thread::kernelAsync([self, thread, defaultBuffer, defaultBufferSize]() { ++ riotKqTrace('K', self->_debugID, self->_socket->fd()); + Message msg(sizeof(dserver_kqchan_reply_mach_port_read_t), 0, self->_checkForEventsAsyncFactory()); + + kqchanMachPortLog.debug() << *self << ": handling read request in microthread" << kqchanMachPortLog.endLog; +@@ -395,8 +422,11 @@ void DarlingServer::Kqchan::MachPort::_read(uint64_t defaultBuffer, uint64_t def + reply->header.code = 0xdead; + } + Thread::currentThread()->impersonate(nullptr); ++ riotKqTrace('F', self->_debugID, self->_socket->fd(), reply->header.code); + ++ riotKqTrace('P', self->_debugID, self->_socket->fd()); + self->_outbox.push(std::move(msg)); ++ riotKqTrace('D', self->_debugID, self->_socket->fd()); + + // now let's send any deferred notifications we might have + self->_sendDeferredNotification(); +diff --git a/src/logging.cpp b/src/logging.cpp +index 7b8000f..dbaf832 100644 +--- a/src/logging.cpp ++++ b/src/logging.cpp +@@ -22,6 +22,8 @@ + #include + #include + #include ++#include ++#include + #include + #include + #include +@@ -45,7 +47,8 @@ DarlingServer::Log::Log(std::string category): + + DarlingServer::Log::Stream::Stream(Type type, const Log& log): + _type(type), +- _log(log) ++ _log(log), ++ _buffer(log._enabled(type) ? std::make_unique() : nullptr) + {}; + + DarlingServer::Log::Stream::~Stream() { +@@ -53,17 +56,22 @@ DarlingServer::Log::Stream::~Stream() { + }; + + DarlingServer::Log::Stream& DarlingServer::Log::Stream::operator<<(EndLog value) { +- auto str = _buffer.str(); ++ if (!_buffer) { ++ return *this; ++ } ++ auto str = _buffer->str(); + if (!str.empty()) { + _log._log(_type, str); +- _buffer.str(std::string()); +- _buffer.clear(); ++ _buffer->str(std::string()); ++ _buffer->clear(); + } + return *this; + }; + + DarlingServer::Log::Stream& DarlingServer::Log::Stream::operator<<(const Loggable& loggable) { +- loggable.logToStream(*this); ++ if (_buffer) { ++ loggable.logToStream(*this); ++ } + return *this; + }; + +@@ -98,20 +106,7 @@ std::string DarlingServer::Log::_typeToString(Type type) { + } + }; + +-void DarlingServer::Log::_log(Type type, std::string message) const { +- // NOTE: we use POSIX file APIs because we want to append each message to the log file atomically, +- // and as far as i can tell, C++ fstreams provide no such guarantee (that they won't write in chunks). +- static int logFile = []() { +- std::filesystem::path path(Server::sharedInstance().prefix() + "/private/var/log/dserver.log"); +- std::filesystem::create_directories(path.parent_path()); +- return open(path.c_str(), O_WRONLY | O_APPEND | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); +- }(); +- +- static bool logToStderr = []() { +- auto val = getenv("DSERVER_LOG_STDERR"); +- return val && strlen(val) >= 1 && (val[0] == 't' || val[0] == 'T' || val[0] == '1'); +- }(); +- ++bool DarlingServer::Log::_enabled(Type type) const { + static Type logMinLevel = []() { + auto val = getenv("DSERVER_LOG_LEVEL"); + Type level = DEFAULT_LOG_CUTOFF; +@@ -129,7 +124,24 @@ void DarlingServer::Log::_log(Type type, std::string message) const { + return level; + }(); + +- if ((type < logMinLevel && !_alwaysLog) || logFile < 0) { ++ return _alwaysLog || type >= logMinLevel; ++}; ++ ++void DarlingServer::Log::_log(Type type, std::string message) const { ++ // NOTE: we use POSIX file APIs because we want to append each message to the log file atomically, ++ // and as far as i can tell, C++ fstreams provide no such guarantee (that they won't write in chunks). ++ static int logFile = []() { ++ std::filesystem::path path(Server::sharedInstance().prefix() + "/private/var/log/dserver.log"); ++ std::filesystem::create_directories(path.parent_path()); ++ return open(path.c_str(), O_WRONLY | O_APPEND | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); ++ }(); ++ ++ static bool logToStderr = []() { ++ auto val = getenv("DSERVER_LOG_STDERR"); ++ return val && strlen(val) >= 1 && (val[0] == 't' || val[0] == 'T' || val[0] == '1'); ++ }(); ++ ++ if (!_enabled(type) || logFile < 0) { + return; + } + +diff --git a/src/message.cpp b/src/message.cpp +index 414c1d6..995e184 100644 +--- a/src/message.cpp ++++ b/src/message.cpp +@@ -24,6 +24,23 @@ + #include + #include + #include ++#include ++ ++namespace { ++ std::optional serverCredentials; ++ struct ucred outgoingCredentials() { ++ if (serverCredentials) return *serverCredentials; ++ return {getpid(), getuid(), getgid()}; ++ } ++} ++ ++void DarlingServer::Message::initializeServerCredentials() { ++ if (serverCredentials) throw std::logic_error("Server credentials already initialized"); ++ // A future fork must never send the parent's cached PID from its child. ++ int result = pthread_atfork(nullptr, nullptr, [] { serverCredentials.reset(); }); ++ if (result) throw std::system_error(result, std::generic_category(), "pthread_atfork"); ++ serverCredentials = outgoingCredentials(); ++} + + DarlingServer::Address::Address() { + _address.sun_family = AF_UNIX; +@@ -65,10 +82,7 @@ DarlingServer::Message::Message(size_t bufferSpace, size_t descriptorSpace, std: + _controlHeader->cmsg_len = CMSG_LEN(sizeof(struct ucred)); + _controlHeader->cmsg_level = SOL_SOCKET; + _controlHeader->cmsg_type = SCM_CREDENTIALS; +- struct ucred ourCreds; +- ourCreds.pid = getpid(); +- ourCreds.uid = getuid(); +- ourCreds.gid = getgid(); ++ struct ucred ourCreds = outgoingCredentials(); + // the cmsg man page says memcpy should be used with CMSG_DATA instead of direct pointer access + memcpy(CMSG_DATA(_controlHeader), &ourCreds, sizeof(ourCreds)); + +@@ -348,10 +362,7 @@ void DarlingServer::Message::_ensureCredentialsHeader() { + credHeader->cmsg_level = SOL_SOCKET; + credHeader->cmsg_type = SCM_CREDENTIALS; + +- struct ucred creds; +- creds.pid = getpid(); +- creds.uid = getuid(); +- creds.gid = getgid(); ++ struct ucred creds = outgoingCredentials(); + memcpy(CMSG_DATA(credHeader), &creds, sizeof(struct ucred)); + }; + +@@ -496,13 +507,47 @@ std::optional DarlingServer::MessageQueue::pop() { + } + }; + ++// darling-lol M58ak: log small (control) messages to/from clients so the ++// dserver's request/reply routing can be correlated. Writes to a host-visible ++// path from inside the guest. Size-filtered (<= 64) to keep volume low. ++static void m58ak_log_message(const char* dir, int socket, const struct sockaddr_un* addr, const void* data, size_t size) { ++ /* darling-lol M58aw: log only pthread_canceled (call 33) messages, both ++ * directions, with the destination address, to see whether the dserver ++ * emits the replies the client is missing. */ ++ if (size < sizeof(int) || data == 0) { ++ return; ++ } ++ if (*(const int*) data != 33) { ++ return; ++ } ++ static FILE* f = nullptr; ++ static unsigned long n = 0; ++ if (!f) { ++ f = fopen("/tmp/opencode/m58ak.log", "a"); ++ if (!f) { ++ f = fopen("/Volumes/SystemRoot/tmp/opencode/m58ak.log", "a"); ++ } ++ } ++ if (!f || n++ > 300000) { ++ return; ++ } ++ char hex[20]; ++ hex[0] = '\0'; ++ if (addr) { ++ for (int i = 0; i < 8; ++i) { ++ snprintf(hex + i * 2, 3, "%02x", (unsigned char) addr->sun_path[i]); ++ } ++ } ++ fprintf(f, "M58AK %s sock=%d addr=%s size=%zu\n", dir, socket, hex, size); ++ fflush(f); ++} ++ + bool DarlingServer::MessageQueue::sendMany(int socket) { + bool canSendMore = true; + std::scoped_lock lock(_lock); + struct mmsghdr mmsgs[16]; + size_t len = 0; + int ret = 0; +- + while (ret >= 0) { + len = 0; + for (size_t i = 0; i < _messages.size() && i < sizeof(mmsgs) / sizeof(*mmsgs); ++i) { +@@ -549,6 +594,7 @@ bool DarlingServer::MessageQueue::sendMany(int socket) { + } + + for (size_t i = 0; i < ret; ++i) { ++ m58ak_log_message("send", socket, &_messages.front().address().raw(), _messages.front().data().data(), _messages.front().data().size()); + auto cb = _messages.front().sendNotificationCallback(); + _messages.pop_front(); + if (cb) { +@@ -591,6 +637,7 @@ bool DarlingServer::MessageQueue::receiveMany(int socket) { + messages[i].rawHeader() = mmsgs[i].msg_hdr; + messages[i].data().resize(mmsgs[i].msg_len); + messages[i].setAddress(Address(*(const struct sockaddr_un*)mmsgs[i].msg_hdr.msg_name, mmsgs[i].msg_hdr.msg_namelen)); ++ m58ak_log_message("recv", socket, &messages[i].address().raw(), messages[i].data().data(), messages[i].data().size()); + _messages.push_back(std::move(messages[i])); + } + } +diff --git a/src/process.cpp b/src/process.cpp +index 9cd7b81..180f372 100644 +--- a/src/process.cpp ++++ b/src/process.cpp +@@ -25,9 +25,15 @@ + #include + + #include ++#include + #include ++#include ++#include + + #include ++#include /* darling-lol M58DS: kill(0) liveness probe */ ++#include /* darling-lol M58DS: /proc state read */ ++#include + + static DarlingServer::Log processLog("process"); + +@@ -109,6 +115,10 @@ DarlingServer::Process::Process(KernelProcessConstructorTag tag): + + DarlingServer::Process::~Process() { + processLog.info() << *this << ": process being destroyed" << processLog.endLog; ++ if (_mapsFd >= 0) { ++ close(_mapsFd); ++ _mapsFd = -1; ++ } + }; + + DarlingServer::Process::ID DarlingServer::Process::id() const { +@@ -245,6 +255,44 @@ bool DarlingServer::Process::_readOrWriteMemory(bool isWrite, uintptr_t remoteAd + << strerror(code) + << ")" + << processMemoryAccessLog.endLog; ++ /* darling-lol M58DS: log why the kernel disagrees with isDead(). */ ++ { ++ char m58path[64]; ++ char m58comm[64] = "?"; ++ char m58state = '?'; ++ int m58k = kill(id(), 0); ++ int m58kerr = errno; ++ FILE* m58f; ++ size_t m58n; ++ snprintf(m58path, sizeof(m58path), "/proc/%d/comm", (int) id()); ++ m58f = fopen(m58path, "r"); ++ if (m58f) { ++ if (fgets(m58comm, sizeof(m58comm), m58f)) { ++ m58n = strlen(m58comm); ++ if (m58n && m58comm[m58n - 1] == '\n') m58comm[m58n - 1] = 0; ++ } ++ fclose(m58f); ++ } ++ snprintf(m58path, sizeof(m58path), "/proc/%d/stat", (int) id()); ++ m58f = fopen(m58path, "r"); ++ if (m58f) { ++ char m58buf[512]; ++ if (fgets(m58buf, sizeof(m58buf), m58f)) { ++ char* m58p = strrchr(m58buf, ')'); ++ if (m58p && m58p[1] == ' ') m58state = m58p[2]; ++ } ++ fclose(m58f); ++ } ++ processMemoryAccessLog.error() ++ << "M58DS target hostpid=" << id() ++ << " nsid=" << nsid() ++ << " isDead=" << (isDead() ? 1 : 0) ++ << " kill0=" << m58k ++ << " killerrno=" << m58kerr ++ << " state=" << m58state ++ << " comm=" << m58comm ++ << processMemoryAccessLog.endLog; ++ } + if (errorCode) { + *errorCode = code; + } +@@ -369,7 +417,18 @@ void DarlingServer::Process::notifyCheckin(Architecture architecture) { + } else { + // notify the parent process (if we have one) that we've arrived + if (auto parent = _parentProcess.lock()) { +- dtape_semaphore_up(parent->_dtapeForkWaitSemaphore); ++ /* darling-lol: the parent may already be tearing down (its ++ * fork-wait semaphore destroyed and nullified in ++ * notifyDead/kernelAsync, or across exec). A late checkin ++ * used to up the dead/NULL semaphore -> SIGSEGV *in ++ * darlingserver*, killing the whole container whenever a ++ * child (e.g. a Chromium helper) checked in after its ++ * parent died. Take the parent's lock and skip when gone. */ ++ std::unique_lock parentLock(parent->_rwlock); ++ if (parent->_dtapeForkWaitSemaphore != nullptr) { ++ dtape_semaphore_up(parent->_dtapeForkWaitSemaphore); ++ } ++ parentLock.unlock(); + parent->_notifyListeningKqchannels(NOTE_FORK, nsid()); + } + } +@@ -465,7 +524,75 @@ DarlingServer::Process::MemoryInfo DarlingServer::Process::memoryInfo() const { + return info; + }; + +-static const std::regex memoryRegionEntryRegex("([0-9a-fA-F]+)\\-([0-9a-fA-F]+)\\s+((?:r|w|x|p|s|\\-)+)\\s+([0-9a-fA-F]+)"); ++// parses "start-end perms offset ..." from a `/proc//maps` line without std::regex (which is extremely slow here) ++static bool parseMapsLine(const std::string& line, uintptr_t& start, uintptr_t& end, char perms[8], uintptr_t* offset) { ++ unsigned long long s = 0, e = 0, o = 0; ++ char p[8] = {0}; ++ if (sscanf(line.c_str(), "%llx-%llx %7s %llx", &s, &e, p, &o) != 4) { ++ return false; ++ } ++ start = s; ++ end = e; ++ memcpy(perms, p, 8); ++ if (offset) { ++ *offset = o; ++ } ++ return true; ++}; ++ ++// darling-lol: PROCMAP_QUERY (Linux 6.11+) answers "which VMA covers/follows this address" in O(log n) ++// without generating and re-parsing the whole /proc//maps text; mach_msg with out-of-line ++// descriptors asked for that on every call and dominated darlingserver CPU. Returns 0 on success, ++// ENOENT when no VMA matches, and -1 when the ioctl is unavailable (callers fall back to the text parser). ++#include ++#include ++#include ++static std::atomic procmapQueryUnsupported(false); ++ ++// Returns 0 on success, ENOENT when no VMA matches, -1 when the ioctl cannot be used (callers fall back to parsing ++// /proc//maps text). The descriptor stays open between lookups; any failure other than "no such mapping" drops it ++// and retries once with a fresh one (after an exec the old descriptor refers to a dead address space). ++int DarlingServer::Process::_procmapQuery(uint64_t address, uint64_t flags, void* result) const { ++ if (procmapQueryUnsupported.load(std::memory_order_relaxed)) { ++ return -1; ++ } ++ auto& query = *static_cast(result); ++ std::lock_guard lock(_mapsMutex); ++ for (int attempt = 0; attempt < 2; ++attempt) { ++ bool fresh = false; ++ if (_mapsFd < 0) { ++ _mapsFd = open(("/proc/" + std::to_string(_pid) + "/maps").c_str(), O_RDONLY | O_CLOEXEC); ++ if (_mapsFd < 0) { ++ return -1; ++ } ++ fresh = true; ++ } ++ memset(&query, 0, sizeof(query)); ++ query.size = sizeof(query); ++ query.query_flags = flags; ++ query.query_addr = address; ++ int status = ioctl(_mapsFd, PROCMAP_QUERY, &query); ++ int error = errno; ++ if (status == 0) { ++ return 0; ++ } ++ if (error == ENOENT) { ++ return ENOENT; ++ } ++ if (fresh && (error == ENOTTY || error == EINVAL)) { ++ procmapQueryUnsupported.store(true, std::memory_order_relaxed); ++ close(_mapsFd); ++ _mapsFd = -1; ++ return -1; ++ } ++ close(_mapsFd); ++ _mapsFd = -1; ++ if (fresh) { ++ return -1; ++ } ++ } ++ return -1; ++}; + + DarlingServer::Process::MemoryRegionInfo DarlingServer::Process::memoryRegionInfo(uintptr_t address) const { + if (isDead()) { +@@ -473,31 +600,55 @@ DarlingServer::Process::MemoryRegionInfo DarlingServer::Process::memoryRegionInf + } + + MemoryRegionInfo info; ++ ++ { ++ procmap_query query; ++ int status = _procmapQuery(address, 0, &query); ++ if (status == 0) { ++ info.startAddress = query.vma_start; ++ info.pageCount = (query.vma_end - query.vma_start) / sysconf(_SC_PAGESIZE); ++ info.mapOffset = query.vma_offset; ++ info.protection = 0; ++ if (query.vma_flags & PROCMAP_QUERY_VMA_READABLE) { ++ info.protection |= PROT_READ; ++ } ++ if (query.vma_flags & PROCMAP_QUERY_VMA_WRITABLE) { ++ info.protection |= PROT_WRITE; ++ } ++ if (query.vma_flags & PROCMAP_QUERY_VMA_EXECUTABLE) { ++ info.protection |= PROT_EXEC; ++ } ++ info.shared = !!(query.vma_flags & PROCMAP_QUERY_VMA_SHARED); ++ return info; ++ } else if (status == ENOENT) { ++ processLog.warning() << *this << ": Address 0x" << std::hex << address << " not found in \"/proc/" << std::dec << _pid << "/maps\"" << processLog.endLog; ++ throw std::system_error(EFAULT, std::generic_category()); ++ } ++ } ++ + std::ifstream file("/proc/" + std::to_string(_pid) + "/maps"); + std::string line; + + uintptr_t endAddress; + + while (std::getline(file, line)) { +- std::smatch match; ++ char permsBuffer[8]; ++ uintptr_t mapOffset = 0; + +- if (!std::regex_search(line, match, memoryRegionEntryRegex)) { ++ if (!parseMapsLine(line, info.startAddress, endAddress, permsBuffer, &mapOffset)) { + processLog.warning() << "Encountered malformed `/proc//maps` entry? Definitely a bug (on our part)." << processLog.endLog; + continue; + } + +- info.startAddress = std::stoul(match[1].str(), nullptr, 16); +- endAddress = std::stoul(match[2].str(), nullptr, 16); +- + if (endAddress <= address || info.startAddress > address) { + continue; + } + + info.pageCount = (endAddress - info.startAddress) / sysconf(_SC_PAGESIZE); + +- info.mapOffset = std::stoul(match[4].str(), nullptr, 16); ++ info.mapOffset = mapOffset; + +- auto perms = match[3].str(); ++ std::string perms = permsBuffer; + + info.protection = 0; + +@@ -657,27 +808,38 @@ void DarlingServer::Process::syncMemory(uintptr_t address, size_t size, int sync + return thread->syncMemory(address, size, sync_flags); + }; + +-static const std::regex memoryRegionEntryAddressRegex("([0-9a-fA-F]+)\\-([0-9a-fA-F]+)"); +- + uintptr_t DarlingServer::Process::getNextRegion(uintptr_t address) const { + if (isDead()) { + throw std::system_error(ESRCH, std::generic_category(), "dead process; can't call getNextRegion"); + } + ++ { ++ // first region whose start is above `address`: the VMA covering-or-following address + 1, unless ++ // that one already started at or below `address`, in which case ask again from its end ++ procmap_query query; ++ int status = address == UINTPTR_MAX ? ENOENT : _procmapQuery(address + 1, PROCMAP_QUERY_COVERING_OR_NEXT_VMA, &query); ++ if (status == 0 && query.vma_start <= address) { ++ status = _procmapQuery(query.vma_end, PROCMAP_QUERY_COVERING_OR_NEXT_VMA, &query); ++ } ++ if (status == 0) { ++ return query.vma_start; ++ } else if (status == ENOENT) { ++ return 0; ++ } ++ } ++ + std::ifstream file("/proc/" + std::to_string(_pid) + "/maps"); + std::string line; + + while (std::getline(file, line)) { +- std::smatch match; ++ uintptr_t startAddress, endAddress; ++ char permsBuffer[8]; + +- if (!std::regex_search(line, match, memoryRegionEntryAddressRegex)) { ++ if (!parseMapsLine(line, startAddress, endAddress, permsBuffer, nullptr)) { + processLog.warning() << "Encountered malformed `/proc//maps` entry? Definitely a bug (on our part)." << processLog.endLog; + continue; + } + +- auto startAddress = std::stoul(match[1].str(), nullptr, 16); +- auto endAddress = std::stoul(match[2].str(), nullptr, 16); +- + if (startAddress <= address) { + continue; + } +diff --git a/src/server.cpp b/src/server.cpp +index e502e28..e225510 100644 +--- a/src/server.cpp ++++ b/src/server.cpp +@@ -28,6 +28,8 @@ + #include + #include + #include ++#include ++#include + #include + #include + #include +@@ -40,6 +42,44 @@ + + #include + ++/* darling-lol M58F129: bounded work-queue trace. With the single-threaded ++ * WorkQueue, this proves whether a process's RPCs wait behind another's: push ++ * (enqueue) and run (dequeue) timestamps per guest thread, in one monotonic ++ * clock, land in dserver.log as `M58WQ push|run pid=.. tid=.. t=`. */ ++#include ++#include ++#include ++ ++static bool m634TraceEnabled() { ++ static const bool enabled = [] { ++ const char* value = std::getenv("DARLING_M634_TRACE"); ++ return value && value[0] == '1' && value[1] == '\0'; ++ }(); ++ return enabled; ++} ++ ++static uint64_t m58wq_now(void) { ++ struct timespec ts; ++ clock_gettime(CLOCK_MONOTONIC, &ts); ++ return (uint64_t)ts.tv_sec * 1000000ull + (uint64_t)(ts.tv_nsec / 1000); ++}; ++ ++static std::atomic m58wq_count(0); ++ ++static void m58wq_log(const char* what, std::shared_ptr thread) { ++ /* darling-lol M58F151: disabled (per-work-item dserver log overhead) */ ++ (void) what; ++ (void) thread; ++#if 0 ++ if (m58wq_count.fetch_add(1) >= 200000) return; ++ if (!thread) return; ++ auto proc = thread->process(); ++ static const auto log = DarlingServer::Log("M58WQ"); ++ log.error() << what << " pid=" << (proc ? proc->id() : -1) ++ << " tid=" << thread->id() << " t=" << m58wq_now() << log.endLog; ++#endif ++}; ++ + static DarlingServer::Server* sharedInstancePointer = nullptr; + + struct DTapeHooks { +@@ -104,8 +144,44 @@ struct DTapeHooks { + + std::unique_lock lock(server._timerLock); + +- if (!override && server._currentTimerDeadline != 0 && deadline_ns >= server._currentTimerDeadline) { +- return; ++ /* darling-lol M634: bounded trace of timer arms to diagnose the ++ * League-launch wedge (instant semaphore timeouts + RPC storm). ++ * Rate-limited after the boot burst so launch-time behavior persists. */ ++ if (m634TraceEnabled()) { ++ static std::atomic m634ArmCount{0}; ++ unsigned seen = m634ArmCount.fetch_add(1); ++ if (seen < 120 || seen % 64 == 0) { ++ struct timespec nowTs = {0, 0}; ++ clock_gettime(CLOCK_MONOTONIC, &nowTs); ++ uint64_t nowNs = (uint64_t)nowTs.tv_sec * 1000000000ull + nowTs.tv_nsec; ++ fprintf(stderr, "M634ARM line=%u deadline=%llu override=%d prev=%llu now=%llu delta=%lld\n", ++ seen, ++ (unsigned long long)deadline_ns, (int)override, ++ (unsigned long long)server._currentTimerDeadline, ++ (unsigned long long)nowNs, ++ (long long)((int64_t)deadline_ns - (int64_t)nowNs)); ++ } ++ } ++ ++ if (!override) { ++ /* darling-lol M634: the software marker races with kernel expiry ++ * (an arm landing between kernel expiry and the fire read used to ++ * be skipped, losing that timer forever). Query the kernel's own ++ * remaining time as the source of truth instead: a zero remaining ++ * value means the timer is disarmed/consumed and must be armed. */ ++ struct itimerspec armedSpec; ++ if (timerfd_gettime(server._timerFD, &armedSpec) == 0 ++ && (armedSpec.it_value.tv_sec != 0 || armedSpec.it_value.tv_nsec != 0)) { ++ struct timespec nowTs = {0, 0}; ++ clock_gettime(CLOCK_MONOTONIC, &nowTs); ++ uint64_t armedRemaining = (uint64_t)armedSpec.it_value.tv_sec * 1000000000ull ++ + (uint64_t)armedSpec.it_value.tv_nsec; ++ uint64_t nowNs = (uint64_t)nowTs.tv_sec * 1000000000ull + nowTs.tv_nsec; ++ uint64_t armedDeadline = nowNs + armedRemaining; ++ if (deadline_ns >= armedDeadline) { ++ return; ++ } ++ } + } + + server._currentTimerDeadline = deadline_ns; +@@ -115,6 +191,11 @@ struct DTapeHooks { + } + }; + ++ static int dtape_hook_log_enabled(dtape_log_level_t level) { ++ static const auto log = DarlingServer::Log("dtape"); ++ return log.levelEnabled(static_cast(level) + 1) ? 1 : 0; ++ }; ++ + static void dtape_hook_log(dtape_log_level_t level, const char* message) { + static const auto log = DarlingServer::Log("dtape"); + auto process = DarlingServer::Process::currentProcess(); +@@ -271,11 +352,23 @@ struct DTapeHooks { + }; + + static void dtape_hook_task_get_memory_info(void* task_context, dtape_memory_info_t* memory_info) { +- auto info = static_cast(task_context)->memoryInfo(); +- memory_info->virtual_size = info.virtualSize; +- memory_info->resident_size = info.residentSize; +- memory_info->page_size = info.pageSize; +- memory_info->region_count = info.regionCount; ++ /* darling-lol M57: a task_info(TASK_BASIC_INFO) for a process that has ++ * already exited made memoryInfo() throw std::system_error, which was ++ * uncaught here and aborted darlingserver, killing the whole session ++ * (Chromium's renderer/GPU probing dies with "dead process; can't call ++ * memoryInfo"). Fail soft with zeroes instead. */ ++ try { ++ auto info = static_cast(task_context)->memoryInfo(); ++ memory_info->virtual_size = info.virtualSize; ++ memory_info->resident_size = info.residentSize; ++ memory_info->page_size = info.pageSize; ++ memory_info->region_count = info.regionCount; ++ } catch (...) { ++ memory_info->virtual_size = 0; ++ memory_info->resident_size = 0; ++ memory_info->page_size = 0; ++ memory_info->region_count = 0; ++ } + }; + + static bool dtape_hook_task_get_memory_region_info(void* task_context, uintptr_t address, dtape_memory_region_info_t* memory_region_info) { +@@ -392,6 +485,7 @@ struct DTapeHooks { + .timer_arm = dtape_hook_timer_arm, + + .log = dtape_hook_log, ++ .log_enabled = dtape_hook_log_enabled, + .get_load_info = dtape_hook_get_load_info, + + .thread_suspend = dtape_hook_thread_suspend, +@@ -540,6 +634,7 @@ void DarlingServer::Server::start() { + // TODO: this could be done concurrently + auto call = DarlingServer::Call::callFromMessage(std::move(*msg)); + if (call) { ++ m58wq_log("push", call->thread()); + _workQueue.push(call->thread()); + } + } +@@ -604,6 +699,25 @@ void DarlingServer::Server::start() { + continue; + } + ++ /* darling-lol M634: the timer is now consumed; drop the stale ++ * armed-deadline marker so future arms are not skipped. Also ++ * trace the fire (rate-limited) to diagnose the launch wedge. */ ++ if (m634TraceEnabled()) { ++ static std::atomic m634FireCount{0}; ++ unsigned seen = m634FireCount.fetch_add(1); ++ if (seen < 80 || seen % 64 == 0) { ++ struct timespec nowTs = {0, 0}; ++ clock_gettime(CLOCK_MONOTONIC, &nowTs); ++ uint64_t nowNs = (uint64_t)nowTs.tv_sec * 1000000000ull + nowTs.tv_nsec; ++ fprintf(stderr, "M634FIRE line=%u exp=%llu prevDeadline=%llu now=%llu\n", ++ seen, ++ (unsigned long long)expirations, ++ (unsigned long long)_currentTimerDeadline, ++ (unsigned long long)nowNs); ++ } ++ } ++ _currentTimerDeadline = 0; ++ + // 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 { + }; + + void DarlingServer::Server::_worker(std::shared_ptr thread) { ++ m58wq_log("run", thread); + thread->doWork(); + }; + diff --git a/patches/foundation.patch b/patches/foundation.patch new file mode 100644 index 0000000..701827e --- /dev/null +++ b/patches/foundation.patch @@ -0,0 +1,69 @@ +diff --git a/include/Foundation/NSProcessInfo.h b/include/Foundation/NSProcessInfo.h +index b3742a82e..f4d7b4d6d 100644 +--- a/include/Foundation/NSProcessInfo.h ++++ b/include/Foundation/NSProcessInfo.h +@@ -50,6 +50,14 @@ NSOperatingSystemVersion; + - (NSOperatingSystemVersion)operatingSystemVersion; + - (BOOL)isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion)version; + ++typedef NS_ENUM(NSInteger, NSProcessInfoThermalState) { ++ NSProcessInfoThermalStateNominal = 0, ++ NSProcessInfoThermalStateFair = 1, ++ NSProcessInfoThermalStateSerious = 2, ++ NSProcessInfoThermalStateCritical = 3, ++}; ++- (NSProcessInfoThermalState)thermalState; ++ + @end + + @interface NSProcessInfo (NSProcessInfoPlatform) +@@ -62,3 +70,5 @@ NSOperatingSystemVersion; + #ifdef DARLING + FOUNDATION_EXPORT void __NSInitializeProcess(int argc,const char *argv[]); + #endif ++ ++FOUNDATION_EXPORT NSString *const NSProcessInfoThermalStateDidChangeNotification; +diff --git a/include/Foundation/NSUserActivity.h b/include/Foundation/NSUserActivity.h +index 80c92bc07..2a80df649 100644 +--- a/include/Foundation/NSUserActivity.h ++++ b/include/Foundation/NSUserActivity.h +@@ -21,3 +21,5 @@ + + @interface NSUserActivity : NSObject + @end ++ ++FOUNDATION_EXPORT NSString *const NSUserActivityTypeBrowsingWeb; +diff --git a/src/NSProcessInfo.m b/src/NSProcessInfo.m +index 1c21f176d..f9f354c80 100644 +--- a/src/NSProcessInfo.m ++++ b/src/NSProcessInfo.m +@@ -305,6 +305,13 @@ SINGLETON_RR() + return t.tv_sec + t.tv_usec / (NSTimeInterval)USEC_PER_SEC; + } + ++- (NSProcessInfoThermalState)thermalState ++{ ++ /* No thermal-pressure source on Linux; report Nominal so clients ++ * (e.g. Chromium's ThermalStateObserverMac) proceed normally. */ ++ return NSProcessInfoThermalStateNominal; ++} ++ + - (NSOperatingSystemVersion)operatingSystemVersion + { + NSOperatingSystemVersion version = { 0, 0, 0 }; +@@ -396,3 +403,5 @@ void __NSInitializeProcess(int argc,const char *argv[]) { + } + } + #endif ++ ++NSString *const NSProcessInfoThermalStateDidChangeNotification = @"NSProcessInfoThermalStateDidChangeNotification"; +diff --git a/src/NSUserActivity.m b/src/NSUserActivity.m +index 09c0a732b..53649d20b 100644 +--- a/src/NSUserActivity.m ++++ b/src/NSUserActivity.m +@@ -34,3 +34,5 @@ + } + + @end ++ ++NSString *const NSUserActivityTypeBrowsingWeb = @"com.apple.coreservices.useractivity.browser"; diff --git a/patches/iokitd.patch b/patches/iokitd.patch new file mode 100644 index 0000000..d79becd --- /dev/null +++ b/patches/iokitd.patch @@ -0,0 +1,594 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 84161fd..1eafdc0 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -36,7 +36,9 @@ set(iokitd_sources + src/IODisplayConnect.mm + src/IODisplayConnectX11.mm + src/PowerAssertions.mm ++ src/PowerSources.mm + src/PowerAssertionsX11.mm ++ src/IOPlatformExpertDevice.mm + src/IOSurfaceRoot.mm + src/IOSurface.mm + src/IOUserClient.mm +diff --git a/src/IORegistryEntry.mm b/src/IORegistryEntry.mm +index 50c10fe..bd501cf 100644 +--- a/src/IORegistryEntry.mm ++++ b/src/IORegistryEntry.mm +@@ -513,3 +513,86 @@ kern_return_t is_io_registry_entry_get_property_bin + return kr; + } + ++ ++kern_return_t is_io_registry_entry_get_property_bin_buf ++( ++ mach_port_t registry_entry, ++ io_name_t plane, ++ io_name_t property_name, ++ uint32_t options, ++ mach_vm_address_t buf, ++ mach_vm_size_t *bufsize, ++ io_buf_ptr_t *properties, ++ mach_msg_type_number_t *propertiesCnt ++) ++{ ++ IORegistryEntry* e = dynamic_cast(IOObject::lookup(registry_entry)); ++ if (!e) ++ return kIOReturnBadArgument; ++ ++ *propertiesCnt = 0; ++ *properties = nullptr; ++ ++ NSString* propName = [NSString stringWithUTF8String: property_name]; ++ id value = nil; ++ ++ if ((options & kIORegistryIterateRecursively) && plane[0]) ++ value = [findKey(e, plane, propName) objectForKey: propName]; ++ else ++ value = [e->getProperties() objectForKey: propName]; ++ ++ if (!value) ++ return kIOReturnNotFound; ++ ++ // Unlike io_registry_entry_get_property_bin, the reply carries the value itself. ++ // The caller's buffer lives in another task, so always answer out-of-line, which the ++ // protocol allows when the result does not go into 'buf'. ++ CFDataRef data = IOCFSerialize((CFTypeRef) value, kIOCFSerializeToBinary); ++ if (!data) ++ return kIOReturnNoMemory; ++ ++ *propertiesCnt = CFDataGetLength(data); ++ kern_return_t kr = vm_allocate(mach_task_self(), (vm_address_t*) properties, *propertiesCnt, true); ++ ++ if (kr == kIOReturnSuccess) ++ memcpy(*properties, CFDataGetBytePtr(data), *propertiesCnt); ++ else ++ *propertiesCnt = 0; ++ ++ CFRelease(data); ++ return kr; ++} ++ ++kern_return_t is_io_registry_entry_get_properties_bin_buf ++( ++ mach_port_t registry_entry, ++ mach_vm_address_t buf, ++ mach_vm_size_t *bufsize, ++ io_buf_ptr_t *properties, ++ mach_msg_type_number_t *propertiesCnt ++) ++{ ++ IORegistryEntry* e = dynamic_cast(IOObject::lookup(registry_entry)); ++ if (!e) ++ return kIOReturnBadArgument; ++ ++ *propertiesCnt = 0; ++ *properties = nullptr; ++ ++ // The caller's buffer lives in another task, so always answer out-of-line, which the ++ // protocol allows when the result does not go into 'buf'. ++ CFDataRef data = IOCFSerialize((CFTypeRef) e->getProperties(), kIOCFSerializeToBinary); ++ if (!data) ++ return kIOReturnNoMemory; ++ ++ *propertiesCnt = CFDataGetLength(data); ++ kern_return_t kr = vm_allocate(mach_task_self(), (vm_address_t*) properties, *propertiesCnt, true); ++ ++ if (kr == kIOReturnSuccess) ++ memcpy(*properties, CFDataGetBytePtr(data), *propertiesCnt); ++ else ++ *propertiesCnt = 0; ++ ++ CFRelease(data); ++ return kr; ++} +diff --git a/src/PowerSources.mm b/src/PowerSources.mm +new file mode 100644 +index 0000000..70ca9bd +--- /dev/null ++++ b/src/PowerSources.mm +@@ -0,0 +1,139 @@ ++extern "C" { ++#include "powermanagementServer.h" ++} ++#import ++#import ++#import ++#import ++#import ++#import ++#include ++#include ++#include ++#include ++#include ++#include ++ ++// The guest has no /sys; the host's is exposed under the system root ++static const char* const kPowerSupplyDir = "/Volumes/SystemRoot/sys/class/power_supply"; ++ ++static bool readSysfs(const std::string& dir, const char* name, std::string& value) ++{ ++ std::ifstream file(dir + "/" + name); ++ if (!file || !std::getline(file, value)) ++ return false; ++ return true; ++} ++ ++static bool readSysfsLong(const std::string& dir, const char* name, long long& value) ++{ ++ std::string text; ++ if (!readSysfs(dir, name, text)) ++ return false; ++ char* end = nullptr; ++ value = strtoll(text.c_str(), &end, 10); ++ return end != text.c_str(); ++} ++ ++static std::vector powerSupplies(const char* wantedType) ++{ ++ std::vector result; ++ DIR* d = opendir(kPowerSupplyDir); ++ if (!d) ++ return result; ++ ++ while (struct dirent* e = readdir(d)) ++ { ++ if (e->d_name[0] == '.') ++ continue; ++ std::string dir = std::string(kPowerSupplyDir) + "/" + e->d_name; ++ std::string type; ++ if (readSysfs(dir, "type", type) && type == wantedType) ++ result.push_back(dir); ++ } ++ closedir(d); ++ return result; ++} ++ ++static NSArray* copyInternalBatteries() ++{ ++ bool onAC = false; ++ for (const std::string& ac : powerSupplies("Mains")) ++ { ++ long long online; ++ if (readSysfsLong(ac, "online", online) && online) ++ onAC = true; ++ } ++ ++ NSMutableArray* sources = [NSMutableArray array]; ++ for (const std::string& dir : powerSupplies("Battery")) ++ { ++ long long present = 1, capacity, energyNow, energyFull, powerNow, voltage; ++ std::string status; ++ readSysfsLong(dir, "present", present); ++ if (!present || !readSysfs(dir, "status", status) || !readSysfsLong(dir, "capacity", capacity)) ++ continue; ++ ++ NSMutableDictionary* ps = [NSMutableDictionary dictionary]; ++ ps[@"Name"] = [NSString stringWithFormat: @"InternalBattery-%lu", (unsigned long) [sources count]]; ++ ps[@"Type"] = @"InternalBattery"; ++ ps[@"Transport Type"] = @"Internal"; ++ ps[@"Power Source ID"] = @((int) [sources count]); ++ ps[@"Is Present"] = @YES; ++ ps[@"Current Capacity"] = @((int) capacity); ++ ps[@"Max Capacity"] = @100; ++ ps[@"Power Source State"] = onAC ? @"AC Power" : @"Battery Power"; ++ ps[@"Is Charging"] = @(status == "Charging"); ++ ps[@"Is Charged"] = @(status == "Full"); ++ ++ if (readSysfsLong(dir, "voltage_now", voltage)) ++ ps[@"Voltage"] = @((int) (voltage / 1000)); // uV -> mV ++ ++ // Remaining time in minutes from the energy and power draw reported by the host; -1 when unknown ++ long long minutes = -1; ++ if (readSysfsLong(dir, "power_now", powerNow) && powerNow > 0) ++ { ++ if (status == "Discharging" && readSysfsLong(dir, "energy_now", energyNow)) ++ minutes = energyNow * 60 / powerNow; ++ else if (status == "Charging" && readSysfsLong(dir, "energy_now", energyNow) && readSysfsLong(dir, "energy_full", energyFull)) ++ minutes = (energyFull - energyNow) * 60 / powerNow; ++ } ++ if (status == "Charging") ++ ps[@"Time to Full Charge"] = @((int) minutes); ++ else ++ ps[@"Time to Empty"] = @((int) minutes); ++ ++ [sources addObject: ps]; ++ } ++ return [sources copy]; ++} ++ ++kern_return_t _io_ps_copy_powersources_info ++( ++ mach_port_t server, ++ audit_token_t token, ++ int pstype, ++ vm_offset_t *powersources, ++ mach_msg_type_number_t *powersourcesCnt, ++ int *return_code ++) ++{ ++ *powersources = 0; ++ *powersourcesCnt = 0; ++ *return_code = 0; ++ ++ // The host only exposes internal batteries; there is no UPS data to report ++ NSArray* sources = copyInternalBatteries(); ++ NSData* data = [NSPropertyListSerialization dataWithPropertyList: sources ++ format: NSPropertyListBinaryFormat_v1_0 options: 0 error: nil]; ++ if (!data) ++ return KERN_FAILURE; ++ ++ kern_return_t kr = vm_allocate(mach_task_self(), powersources, [data length], VM_FLAGS_ANYWHERE); ++ if (kr != KERN_SUCCESS) ++ return kr; ++ ++ memcpy((void*) *powersources, [data bytes], [data length]); ++ *powersourcesCnt = [data length]; ++ return KERN_SUCCESS; ++} +diff --git a/src/ServiceRegistry.h b/src/ServiceRegistry.h +index 34e74ad..0d0740b 100644 +--- a/src/ServiceRegistry.h ++++ b/src/ServiceRegistry.h +@@ -32,6 +32,7 @@ public: + static ServiceRegistry* instance(); + void registerService(IOService* service); + IOIterator* iteratorForMatchingServices(NSDictionary* criteria) const; ++ IOObject* firstMatchingService(NSDictionary* criteria) const; + private: + std::list m_registeredServices; + }; +diff --git a/src/ServiceRegistry.mm b/src/ServiceRegistry.mm +index aaed046..7a190c3 100644 +--- a/src/ServiceRegistry.mm ++++ b/src/ServiceRegistry.mm +@@ -24,6 +24,9 @@ + #include + #include + #include ++#include ++#include ++#include + #include "IOIterator.h" + + extern "C" { +@@ -49,6 +52,16 @@ IOIterator* ServiceRegistry::iteratorForMatchingServices(NSDictionary* criteria) + return new IOIterator(matching); + } + ++IOObject* ServiceRegistry::firstMatchingService(NSDictionary* criteria) const ++{ ++ for (IOService* service : m_registeredServices) ++ { ++ if (service->matches(criteria)) ++ return service; ++ } ++ return nullptr; ++} ++ + void ServiceRegistry::registerService(IOService* service) + { + m_registeredServices.push_back(service); +@@ -118,3 +131,91 @@ kern_return_t is_io_service_get_matching_services + // Old unsupported API + return kIOReturnUnsupported; + } ++ ++static NSDictionary* unserializeMatching(const char* caller, io_struct_inband_t matching, mach_msg_type_number_t matchingCnt) ++{ ++ CFStringRef errorString = nullptr; ++ CFTypeRef criteria = IOCFUnserializeBinary(matching, matchingCnt, nullptr, 0, &errorString); ++ ++ if (!criteria) ++ throwCFStringException(CFSTR("cannot parse 'matching': %@"), errorString); ++ ++ if (CFGetTypeID(criteria) != CFDictionaryGetTypeID()) ++ { ++ CFRelease(criteria); ++ throw std::runtime_error(std::string(caller) + "(): dictionary expected"); ++ } ++ ++ return (NSDictionary*) criteria; ++} ++ ++kern_return_t is_io_service_get_matching_service_bin ++( ++ mach_port_t master_port, ++ io_struct_inband_t matching, ++ mach_msg_type_number_t matchingCnt, ++ mach_port_t *service ++) ++{ ++ *service = MACH_PORT_NULL; ++ ++ try ++ { ++ NSDictionary* criteria = unserializeMatching("io_service_get_matching_service_bin", matching, matchingCnt); ++ IOObject* match = ServiceRegistry::instance()->firstMatchingService(criteria); ++ CFRelease(criteria); ++ ++ if (!match) ++ return kIOReturnNotFound; ++ ++ *service = match->port(); ++ return kIOReturnSuccess; ++ } ++ catch (const std::exception& e) ++ { ++ os_log_error(OS_LOG_DEFAULT, "is_io_service_get_matching_service_bin: %s", e.what()); ++ return kIOReturnBadArgument; ++ } ++} ++ ++kern_return_t is_io_service_add_notification_bin_64 ++( ++ mach_port_t master_port, ++ io_name_t notification_type, ++ io_struct_inband_t matching, ++ mach_msg_type_number_t matchingCnt, ++ mach_port_t wake_port, ++ io_async_ref64_t reference, ++ mach_msg_type_number_t referenceCnt, ++ mach_port_t *notification ++) ++{ ++ *notification = MACH_PORT_NULL; ++ ++ try ++ { ++ NSDictionary* criteria = unserializeMatching("io_service_add_notification_bin_64", matching, matchingCnt); ++ IOIterator* iterator; ++ ++ // The registry is fixed once iokitd has started, so no service is ever published or ++ // terminated later. The notification object is the iterator of the services that match ++ // now (as in IOKit for the publish/matched types); termination types start out empty. ++ if (!strcmp(notification_type, "IOServicePublish") || ++ !strcmp(notification_type, "IOServiceFirstMatch") || ++ !strcmp(notification_type, "IOServiceMatched")) ++ iterator = ServiceRegistry::instance()->iteratorForMatchingServices(criteria); ++ else ++ iterator = new IOIterator(std::vector()); ++ CFRelease(criteria); ++ ++ *notification = iterator->port(); ++ iterator->releaseLater(); ++ ++ return kIOReturnSuccess; ++ } ++ catch (const std::exception& e) ++ { ++ os_log_error(OS_LOG_DEFAULT, "is_io_service_add_notification_bin_64: %s", e.what()); ++ return kIOReturnBadArgument; ++ } ++} +diff --git a/src/main.mm b/src/main.mm +index 37ce9b8..d29f01d 100644 +--- a/src/main.mm ++++ b/src/main.mm +@@ -32,6 +32,7 @@ + #include "IODisplayConnectX11.h" + #include "PowerAssertions.h" + #include "IOSurfaceRoot.h" ++#include "IOPlatformExpertDevice.h" + + extern "C" { + #include "iokitmigServer.h" +@@ -146,5 +147,6 @@ static void discoverAllDevices() + // Trick to make sure the root object gets instantiated first and gets the lowest ID + IORegistryEntry::root(); + IODisplayConnectX11::discoverDevices(registry); ++ IOPlatformExpertDevice::registerSelf(registry); + IOSurfaceRoot::registerSelf(registry); + } +diff --git a/src/stubs-powermanagement.c b/src/stubs-powermanagement.c +index a568ecc..929746c 100644 +--- a/src/stubs-powermanagement.c ++++ b/src/stubs-powermanagement.c +@@ -368,22 +368,6 @@ kern_return_t _io_ps_release_pspowersource + } + + +-/* Routine io_ps_copy_powersources_info */ +- +-kern_return_t _io_ps_copy_powersources_info +-( +- mach_port_t server, +- audit_token_t token, +- int pstype, +- vm_offset_t *powersources, +- mach_msg_type_number_t *powersourcesCnt, +- int *return_code +-) +-{ +- STUB(); +- return KERN_NOT_SUPPORTED; +-} +- + + /* Routine io_ps_copy_chargelog */ + +diff --git a/src/stubs.c b/src/stubs.c +index 91d11a1..a19d0d6 100644 +--- a/src/stubs.c ++++ b/src/stubs.c +@@ -642,18 +642,6 @@ kern_return_t is_io_server_version + } + + +-kern_return_t is_io_service_get_matching_service_bin +-( +- mach_port_t master_port, +- io_struct_inband_t matching, +- mach_msg_type_number_t matchingCnt, +- mach_port_t *service +-) +-{ +- STUB(); +- return KERN_NOT_SUPPORTED; +-} +- + kern_return_t is_io_service_match_property_table_bin + ( + mach_port_t service, +@@ -682,22 +670,6 @@ kern_return_t is_io_service_add_notification_bin + return KERN_NOT_SUPPORTED; + } + +-kern_return_t is_io_service_add_notification_bin_64 +-( +- mach_port_t master_port, +- io_name_t notification_type, +- io_struct_inband_t matching, +- mach_msg_type_number_t matchingCnt, +- mach_port_t wake_port, +- io_async_ref64_t reference, +- mach_msg_type_number_t referenceCnt, +- mach_port_t *notification +-) +-{ +- STUB(); +- return KERN_NOT_SUPPORTED; +-} +- + kern_return_t is_io_registry_entry_from_path_ool + ( + mach_port_t master_port, +@@ -736,31 +708,3 @@ kern_return_t is_io_device_tree_entry_exists_with_name + return KERN_NOT_SUPPORTED; + } + +-kern_return_t is_io_registry_entry_get_properties_bin_buf +-( +- mach_port_t registry_entry, +- mach_vm_address_t buf, +- mach_vm_size_t *bufsize, +- io_buf_ptr_t *properties, +- mach_msg_type_number_t *propertiesCnt +-) +-{ +- STUB(); +- return KERN_NOT_SUPPORTED; +-} +- +-kern_return_t is_io_registry_entry_get_property_bin_buf +-( +- mach_port_t registry_entry, +- io_name_t plane, +- io_name_t property_name, +- uint32_t options, +- mach_vm_address_t buf, +- mach_vm_size_t *bufsize, +- io_buf_ptr_t *properties, +- mach_msg_type_number_t *propertiesCnt +-) +-{ +- STUB(); +- return KERN_NOT_SUPPORTED; +-} +diff --git a/src/IOPlatformExpertDevice.h b/src/IOPlatformExpertDevice.h +new file mode 100644 +--- /dev/null ++++ b/src/IOPlatformExpertDevice.h +@@ -0,0 +1,21 @@ ++#ifndef IOKITD_IOPLATFORMEXPERTDEVICE_H ++#define IOKITD_IOPLATFORMEXPERTDEVICE_H ++ ++#include "IOService.h" ++#include "ServiceRegistry.h" ++ ++// The root platform device (what IOServiceMatching("IOPlatformExpertDevice") finds on macOS). ++// Its properties describe the host machine as reported by the Linux kernel's DMI tables. ++// Identifiers the host does not expose to us (serial number, UUID) are left out. ++class IOPlatformExpertDevice : public IOService ++{ ++public: ++ const char* className() const override; ++ NSDictionary* matchingDictionary() override; ++ NSDictionary* getProperties() override; ++ bool conformsTo(const char* className) override; ++ ++ static void registerSelf(ServiceRegistry* targetServiceRegistry); ++}; ++ ++#endif +diff --git a/src/IOPlatformExpertDevice.mm b/src/IOPlatformExpertDevice.mm +new file mode 100644 +--- /dev/null ++++ b/src/IOPlatformExpertDevice.mm +@@ -0,0 +1,54 @@ ++#include "IOPlatformExpertDevice.h" ++#import ++#import ++#import ++#import ++#include ++ ++// Platform expert strings are NUL-terminated data ++static NSData* dataForString(NSString* string) ++{ ++ const char* utf8 = [string UTF8String]; ++ return [NSData dataWithBytes: utf8 length: strlen(utf8) + 1]; ++} ++ ++const char* IOPlatformExpertDevice::className() const ++{ ++ return "IOPlatformExpertDevice"; ++} ++ ++NSDictionary* IOPlatformExpertDevice::matchingDictionary() ++{ ++ return @{ ++ @"IOProviderClass": @"IOPlatformExpertDevice", ++ @"IOClass": @"IOPlatformExpertDevice", ++ }; ++} ++ ++NSDictionary* IOPlatformExpertDevice::getProperties() ++{ ++ // Fixed MacBookPro16,1 identity; the Linux DMI values describe the host hardware, not the Mac we emulate ++ return @{ ++ @"manufacturer": dataForString(@"Apple Inc."), ++ @"model": dataForString(@"MacBookPro16,1"), ++ @"product-name": dataForString(@"MacBookPro16,1"), ++ @"board-id": dataForString(@"Mac-E1008331FDC96864"), ++ @"IOPlatformUUID": @"6F3B2A1E-8C4D-4E5F-9A7B-1D2C3E4F5A6B", ++ @"IOPlatformSerialNumber": @"C02ZL0GVMD6N", ++ }; ++} ++ ++bool IOPlatformExpertDevice::conformsTo(const char* className) ++{ ++ if (std::strcmp(className, "IOPlatformDevice") == 0) ++ return true; ++ return IOService::conformsTo(className); ++} ++ ++void IOPlatformExpertDevice::registerSelf(ServiceRegistry* targetServiceRegistry) ++{ ++ static IOPlatformExpertDevice* device = new IOPlatformExpertDevice; ++ ++ targetServiceRegistry->registerService(device); ++ device->registerInPlane(kIOServicePlane, "Platform", IORegistryEntry::root()); ++} diff --git a/patches/libcxxabi.patch b/patches/libcxxabi.patch new file mode 100644 index 0000000..50ad599 --- /dev/null +++ b/patches/libcxxabi.patch @@ -0,0 +1,25 @@ +diff --git a/src/private_typeinfo.cpp b/src/private_typeinfo.cpp +index 86e187f..11db349 100644 +--- a/src/private_typeinfo.cpp ++++ b/src/private_typeinfo.cpp +@@ -53,12 +53,20 @@ static inline + bool + is_equal(const std::type_info* x, const std::type_info* y, bool use_strcmp) + { ++#ifdef DARLING ++ // Guest executables can retain weak RTTI definitions alongside the ++ // runtime's strong definition. Until dyld coalesces those definitions, ++ // pointer-only comparison misses otherwise matching exception handlers. ++ // Compare the ABI type names as well, retaining the pointer fast path. ++ return *x == *y || strcmp(x->name(), y->name()) == 0; ++#else + // Use std::type_info's default comparison unless we've explicitly asked + // for strcmp. + if (!use_strcmp) + return *x == *y; + // Still allow pointer equality to short circut. + return x == y || strcmp(x->name(), y->name()) == 0; ++#endif + } + + static inline ptrdiff_t update_offset_to_base(const char* vtable, diff --git a/patches/libdispatch.patch b/patches/libdispatch.patch new file mode 100644 index 0000000..07b74d4 --- /dev/null +++ b/patches/libdispatch.patch @@ -0,0 +1,27 @@ +diff --git a/src/queue.c b/src/queue.c +index 336c657..9c15faa 100644 +--- a/src/queue.c ++++ b/src/queue.c +@@ -3667,8 +3667,9 @@ _dispatch_lane_drain(dispatch_lane_t dq, dispatch_invoke_context_t dic, + break; + } + if (likely(flags & DISPATCH_INVOKE_WORKLOOP_DRAIN)) { +- dispatch_workloop_t dwl = (dispatch_workloop_t)_dispatch_get_wlh(); +- if (unlikely(_dispatch_queue_max_qos(dwl) > dwl->dwl_drained_qos)) { ++ // a workloop drained from a root queue thread has no workloop wlh ++ dispatch_workloop_t dwl = _dispatch_wlh_to_workloop(_dispatch_get_wlh()); ++ if (dwl && unlikely(_dispatch_queue_max_qos(dwl) > dwl->dwl_drained_qos)) { + break; + } + } +@@ -5355,8 +5356,8 @@ _dispatch_channel_invoke_checks(dispatch_channel_t dch, + _dispatch_return_to_kernel(); + } + if (likely(dcic->dcic_flags & DISPATCH_INVOKE_WORKLOOP_DRAIN)) { +- dispatch_workloop_t dwl = (dispatch_workloop_t)_dispatch_get_wlh(); +- if (unlikely(_dispatch_queue_max_qos(dwl) > dwl->dwl_drained_qos)) { ++ dispatch_workloop_t dwl = _dispatch_wlh_to_workloop(_dispatch_get_wlh()); ++ if (dwl && unlikely(_dispatch_queue_max_qos(dwl) > dwl->dwl_drained_qos)) { + dcic->dcic_tq = dch->do_targetq; + return false; + } diff --git a/patches/libkqueue.patch b/patches/libkqueue.patch new file mode 100644 index 0000000..0d35225 --- /dev/null +++ b/patches/libkqueue.patch @@ -0,0 +1,617 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index b9cf3f8..62e11d0 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -31,7 +31,12 @@ if (DARLING) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -nostdinc -DHAVE_SYS_EVENTFD_H -DHAVE_SYS_SIGNALFD_H -DHAVE_SYS_TIMERFD_H -ggdb -O0") + + #add_definitions(-DKERNEL_PRIVATE) +- #add_definitions(-Dkqueue=kqueue_impl -Dkevent=kevent_impl -Dkevent64=kevent64_impl) ++ # darling-lol: enable kqueue emulation (was stubbed ENOSYS, breaking ++ # dispatch sources, FilePathWatcher, and socket servers). Maps the ++ # public API to the _impl entry points that override libsystem_kernel's ++ # weak stubs. (Deliberately NOT mapping close=__close_for_kqueue yet; ++ # evaluate fd-lifecycle tracking separately.) ++ add_definitions(-Dkqueue=kqueue_impl -Dkevent=kevent_impl -Dkevent64=kevent64_impl) + #add_definitions(-Dclose=__close_for_kqueue) + endif (DARLING) + +diff --git a/src/common/filter.c b/src/common/filter.c +index a9a6512..f324b41 100644 +--- a/src/common/filter.c ++++ b/src/common/filter.c +@@ -125,11 +125,14 @@ filter_unregister_all(struct kqueue *kq) + if (kq->kq_filt[i].kf_id == 0) + continue; + +- if (kq->kq_filt[i].kf_destroy != NULL) +- kq->kq_filt[i].kf_destroy(&kq->kq_filt[i]); ++ /* Closing the queue owns teardown of every remaining registration, ++ * including the native socket retained by a Mach-port knote. */ ++ struct knote *kn; ++ while ((kn = RB_ROOT(&kq->kq_filt[i].kf_knote)) != NULL) ++ knote_delete(&kq->kq_filt[i], kn); + +- //XXX-FIXME +- //knote_free_all(&kq->kq_filt[i]); ++ if (kq->kq_filt[i].kf_destroy != NULL) ++ kq->kq_filt[i].kf_destroy(&kq->kq_filt[i]); + + if (kqops.filter_free != NULL) + kqops.filter_free(kq, &kq->kq_filt[i]); +diff --git a/src/common/kevent.c b/src/common/kevent.c +index f5d53a2..ca91b33 100644 +--- a/src/common/kevent.c ++++ b/src/common/kevent.c +@@ -27,6 +27,8 @@ + #include + + #include "private.h" ++#include ++#define LOLKQ(...) do { } while (0) + + static const char * + kevent_filter_dump(const struct kevent64_s *kev) +@@ -127,7 +129,9 @@ kevent_copyin_one(struct kqueue *kq, const struct kevent64_s *src) + + dbg_printf("src=%s", kevent_dump(src)); + +- kn = knote_lookup(filt, src->ident); ++ if((src->filter==EVFILT_READ || src->filter==EVFILT_WRITE) && (src->flags&EV_UDATA_SPECIFIC)) ++ kn=knote_lookup_fd_specific(filt,src->ident,src->udata); ++ else kn = knote_lookup(filt, src->ident); + dbg_printf("knote_lookup: ident %d == %p", (int)src->ident, kn); + if (kn == NULL) { + if (src->flags & EV_ADD) { +@@ -170,6 +174,16 @@ kevent_copyin_one(struct kqueue *kq, const struct kevent64_s *src) + } + } + ++ /* A delivered dispatch2 event still owns its userdata until acknowledged. ++ * Match XNU: a plain delete of a disabled source defers reclamation. */ ++ if ((src->flags & EV_DELETE) && !(src->flags & EV_ENABLE) && ++ (kn->kev.flags & EV_DISPATCH2) == EV_DISPATCH2 && ++ (kn->kev.flags & EV_DISABLE)) { ++ kn->kn_flags |= KNFL_DEFER_DELETE; ++ errno = EINPROGRESS; ++ return (-1); ++ } ++ + if ((src->flags & EV_DELETE) || ((kn->kn_flags & KNFL_DEFER_DELETE) && (src->flags & EV_ENABLE))) { + rv = knote_delete(filt, kn); + dbg_printf("knote_delete returned %d", rv); +@@ -349,6 +363,7 @@ kevent64_impl(int kqfd, const struct kevent64_s *changelist, int nchanges, + kqueue_addref(kq); + + pthread_mutex_unlock(&kq_mtx); ++ LOLKQ("kevent64_impl enter kqfd=%d nch=%d nev=%d\n", kqfd, nchanges, nevents); + + #ifndef NDEBUG + if (DEBUG_KQUEUE) { +@@ -386,6 +401,11 @@ kevent64_impl(int kqfd, const struct kevent64_s *changelist, int nchanges, + if ((flags & KEVENT_FLAG_ERROR_EVENTS) == 0 && nevents > 0) { + const struct timespec* ts = (flags & KEVENT_FLAG_IMMEDIATE) ? (&timeout_zero) : timeout; + again: ++ /* epoll snapshots contain raw knote pointers. Keep retired notes alive ++ * until every waiter that could have observed them has copied out. */ ++ kqueue_lock(kq); ++ kq->kq_waiters++; ++ kqueue_unlock(kq); + rv = kqops.kevent_wait(kq, nevents, ts); + dbg_printf("kqops.kevent_wait returned %d", rv); + +@@ -395,6 +415,7 @@ again: + ret += rv; + } + ++ kq->kq_waiters--; + kqueue_cleanup(kq); + kqueue_unlock(kq); + +diff --git a/src/common/knote.c b/src/common/knote.c +index a328f80..d0141aa 100644 +--- a/src/common/knote.c ++++ b/src/common/knote.c +@@ -33,7 +33,17 @@ knote_init(void) + static int + knote_cmp(struct knote *a, struct knote *b) + { +- return memcmp(&a->kev.ident, &b->kev.ident, sizeof(a->kev.ident)); ++ int compared=memcmp(&a->kev.ident,&b->kev.ident,sizeof(a->kev.ident)); ++ if(compared) return compared; ++ // Darwin allows independent fd registrations distinguished by udata. ++ // Each Linux fd note already owns a separate dupfd/epoll registration. ++ if(a->kev.filter==EVFILT_READ || a->kev.filter==EVFILT_WRITE) { ++ int as=!!(a->kev.flags&EV_UDATA_SPECIFIC); ++ int bs=!!(b->kev.flags&EV_UDATA_SPECIFIC); ++ if(as!=bs) return as-bs; ++ if(as) return a->kev.udatakev.udata?-1:a->kev.udata>b->kev.udata?1:0; ++ } ++ return 0; + } + + RB_GENERATE(knt, knote, kn_entries, knote_cmp) +@@ -94,7 +104,7 @@ knote_delete(struct filter *filt, struct knote *kn) + * Verify that the knote wasn't removed by another + * thread before we acquired the knotelist lock. + */ +- query.kev.ident = kn->kev.ident; ++ query.kev = kn->kev; + pthread_rwlock_wrlock(&filt->kf_knote_mtx); + tmp = RB_FIND(knt, &filt->kf_knote, &query); + if (tmp == kn) { +@@ -114,10 +124,11 @@ knote_delete(struct filter *filt, struct knote *kn) + struct knote * + knote_lookup(struct filter *filt, uintptr_t ident) + { +- struct knote query; ++ struct knote query={0}; + struct knote *ent = NULL; + + query.kev.ident = ident; ++ query.kev.filter=filt->kf_id; + + pthread_rwlock_rdlock(&filt->kf_knote_mtx); + ent = RB_FIND(knt, &filt->kf_knote, &query); +@@ -160,3 +171,14 @@ knote_disable(struct filter *filt, struct knote *kn) + + //TODO: knote_enable() + ++ ++struct knote * ++knote_lookup_fd_specific(struct filter *filt,uintptr_t ident,uint64_t udata) { ++ struct knote query={0}; ++ query.kev.ident=ident;query.kev.filter=filt->kf_id; ++ query.kev.flags=EV_UDATA_SPECIFIC;query.kev.udata=udata; ++ pthread_rwlock_rdlock(&filt->kf_knote_mtx); ++ struct knote* result=RB_FIND(knt,&filt->kf_knote,&query); ++ pthread_rwlock_unlock(&filt->kf_knote_mtx); ++ return result; ++} +diff --git a/src/common/kqueue.c b/src/common/kqueue.c +index 61cc9dc..f05576d 100644 +--- a/src/common/kqueue.c ++++ b/src/common/kqueue.c +@@ -23,6 +23,8 @@ + #include + + #include "private.h" ++#include ++#define LOLKQ(...) do { } while (0) + + #ifdef DARLING + #include "../darling/listenregistry.h" +@@ -113,6 +115,7 @@ kqueue_cmp(struct kqueue *a, struct kqueue *b) + void kqueue_cleanup(struct kqueue* kq) + { + struct knote *next, *tmp; ++ if (kq->kq_waiters) return; + for (next = kq->kq_tofree.lh_first; next != NULL; next = tmp) + { + tmp = next->kn_entries2free.le_next; +@@ -135,6 +138,7 @@ kqueue_free(struct kqueue *kq) + // XXX: we should be doing this more efficiently (i.e. without walking through all open kqueues) + map_foreach(kqmap, kqueue_free_cb, kq); + filter_unregister_all(kq); ++ kqueue_cleanup(kq); + kqops.kqueue_free(kq); + free(kq); + } +@@ -329,6 +333,7 @@ void kqueue_delref(struct kqueue *kq) + int VISIBLE + kqueue_impl(void) + { ++ LOLKQ("kqueue_impl enter\n"); + struct kqueue *kq; + struct kqueue *tmp; + +diff --git a/src/common/map.c b/src/common/map.c +index cc254dd..ed0f21c 100644 +--- a/src/common/map.c ++++ b/src/common/map.c +@@ -18,6 +18,7 @@ + + struct map { + size_t len; ++ int high_water; + void **data; + }; + +@@ -51,12 +52,23 @@ map_new(size_t len) + return (dst); + } + ++/* Iteration is used on every descriptor close. Do not scan the entire ++ * RLIMIT_NOFILE-sized allocation for a map with only a few populated slots. */ ++static void map_note_index(struct map *m, int idx) ++{ ++ int end = idx + 1; ++ int old = __atomic_load_n(&m->high_water, __ATOMIC_RELAXED); ++ while (old < end && !__atomic_compare_exchange_n(&m->high_water, &old, end, ++ 0, __ATOMIC_RELEASE, __ATOMIC_RELAXED)) {} ++} ++ + int + map_insert(struct map *m, int idx, void *ptr) + { +- if (slowpath(idx < 0 || idx > (int)m->len)) ++ if (slowpath(idx < 0 || idx >= (int)m->len)) + return (-1); + ++ map_note_index(m, idx); + if (atomic_ptr_cas(&(m->data[idx]), 0, ptr) == NULL) { + dbg_printf("inserted %p in location %d", ptr, idx); + return (0); +@@ -71,7 +83,7 @@ map_insert(struct map *m, int idx, void *ptr) + int + map_remove(struct map *m, int idx, void *ptr) + { +- if (slowpath(idx < 0 || idx > (int)m->len)) ++ if (slowpath(idx < 0 || idx >= (int)m->len)) + return (-1); + + if (atomic_ptr_cas(&(m->data[idx]), ptr, 0) == NULL) { +@@ -88,9 +100,10 @@ map_replace(struct map *m, int idx, void *oldp, void *newp) + { + void *tmp; + +- if (slowpath(idx < 0 || idx > (int)m->len)) ++ if (slowpath(idx < 0 || idx >= (int)m->len)) + return (-1); + ++ map_note_index(m, idx); + tmp = atomic_ptr_cas(&(m->data[idx]), oldp, newp); + if (tmp == oldp) { + dbg_printf("replaced value %p in location %d with value %p", +@@ -106,7 +119,7 @@ map_replace(struct map *m, int idx, void *oldp, void *newp) + void * + map_lookup(struct map *m, int idx) + { +- if (slowpath(idx < 0 || idx > (int)m->len)) ++ if (slowpath(idx < 0 || idx >= (int)m->len)) + return (NULL); + + return m->data[idx]; +@@ -116,7 +129,8 @@ void + map_foreach(struct map *m, void(*cb)(int, void*, void*), void* private) + { + int i; +- for (i = 0; i < (int)m->len; i++) { ++ int end = __atomic_load_n(&m->high_water, __ATOMIC_ACQUIRE); ++ for (i = 0; i < end; i++) { + if (m->data[i]) + cb(i, m->data[i], private); + } +@@ -128,7 +142,7 @@ map_delete(struct map *m, int idx) + void *oval; + void *nval; + +- if (slowpath(idx < 0 || idx > (int)m->len)) ++ if (slowpath(idx < 0 || idx >= (int)m->len)) + return ((void *)-1); + + /* Hopefully we aren't racing with another thread, but you never know.. */ +diff --git a/src/common/private.h b/src/common/private.h +index 5c0718e..e7a0841 100644 +--- a/src/common/private.h ++++ b/src/common/private.h +@@ -161,6 +161,7 @@ struct kqueue { + #endif + RB_ENTRY(kqueue) entries; + LIST_HEAD(knt2free, knote) kq_tofree; ++ unsigned int kq_waiters; /* protected by kq_mtx; keep existing offsets */ + }; + + struct kqueue_vtable { +@@ -198,6 +199,7 @@ int kevent_copyin_one(struct kqueue *kq, const struct kevent64_s *src); + * knote internal API + */ + struct knote * knote_lookup(struct filter *, uintptr_t); ++struct knote * knote_lookup_fd_specific(struct filter *, uintptr_t,uint64_t); + //DEADWOOD: struct knote * knote_get_by_data(struct filter *filt, intptr_t); + struct knote * knote_new(void); + #define knote_retain(kn) atomic_inc(&kn->kn_ref) +diff --git a/src/linux/machport.c b/src/linux/machport.c +index 074958e..1e99023 100644 +--- a/src/linux/machport.c ++++ b/src/linux/machport.c +@@ -34,6 +34,9 @@ + + #include "private.h" + ++/* Keep the Mach-port path free of per-event diagnostic I/O. */ ++#define M58MP(...) do { } while (0) ++ + int + evfilt_machport_copyout(struct kevent64_s *dst, struct knote *src, void *ptr) + { +@@ -45,13 +48,16 @@ evfilt_machport_copyout(struct kevent64_s *dst, struct knote *src, void *ptr) + + epoll_event_dump(ev); + kevent_int_to_64(&src->kev, dst); ++ M58MP("copyout fd=%d\n", src->kdata.kn_dupfd); + + // first, read the notification + rv = recv(src->kdata.kn_dupfd, ¬ification, sizeof(notification), 0); + if (rv < 0) { ++ M58MP("copyout notif-recv FAILED fd=%d errno=%d\n", src->kdata.kn_dupfd, errno); + dbg_printf("evfilt_machport_copyout() reading notification failed: %d (%s)", errno, strerror(errno)); + return -1; + } ++ M58MP("copyout notif fd=%d rv=%d num=0x%x\n", src->kdata.kn_dupfd, rv, notification.header.number); + + if (notification.header.number != dserver_kqchan_msgnum_notification) { + dbg_puts("evfilt_machport_copyout() read invalid notification"); +@@ -124,18 +130,24 @@ evfilt_machport_knote_create(struct filter *filt, struct knote *kn) + + int status = _dserver_rpc_kqchan_mach_port_open_4libkqueue(port, (void*)kn->kev.ext[0], kn->kev.ext[1], kn->kev.fflags, &kn->kdata.kn_dupfd); + if (status < 0) { ++ M58MP("create open FAILED port=%d status=%d\n", port, status); + dbg_printf("evfilt_machport_open: %s", strerror(-status)); + return (-1); + } ++ M58MP("create open port=%d fd=%d events=0x%x\n", port, kn->kdata.kn_dupfd, ev.events); + + dbg_printf("evfilt_machport_open: listening to FD %d for events %d", kn->kdata.kn_dupfd, ev.events); + + fcntl(kn->kdata.kn_dupfd, F_SETFD, FD_CLOEXEC); + + if (epoll_ctl(kn->kn_epollfd, EPOLL_CTL_ADD, kn->kdata.kn_dupfd, &ev) < 0) { ++ M58MP("create epoll_add FAILED epfd=%d fd=%d errno=%d\n", kn->kn_epollfd, kn->kdata.kn_dupfd, errno); + dbg_printf("epoll_ctl(2): %s", strerror(errno)); ++ (void) __close_for_kqueue(kn->kdata.kn_dupfd); ++ kn->kdata.kn_dupfd = -1; + return (-1); + } ++ M58MP("create epoll_add OK epfd=%d fd=%d\n", kn->kn_epollfd, kn->kdata.kn_dupfd); + return 0; + } + +@@ -186,7 +198,8 @@ evfilt_machport_knote_delete(struct filter *filt, struct knote *kn) + if ((kn->kev.flags & EV_DISABLE) == 0) { + if (epoll_ctl(kn->kn_epollfd, EPOLL_CTL_DEL, kn->kdata.kn_dupfd, NULL) < 0) { + dbg_perror("epoll_ctl(2)"); +- return (-1); ++ /* Teardown must still close the channel if its epoll entry was ++ * already removed. knote_delete frees the registration anyway. */ + } + } + +diff --git a/src/linux/platform.c b/src/linux/platform.c +index 482ffb2..dd750a4 100644 +--- a/src/linux/platform.c ++++ b/src/linux/platform.c +@@ -16,6 +16,8 @@ + + # define _GNU_SOURCE + # include ++#include ++#define LOLKQ(...) do { } while (0) + #include + #include "../common/private.h" + +@@ -179,6 +181,7 @@ linux_kevent_wait( + dbg_puts("waiting for events"); + restart: + nret = epoll_wait(kqueue_epfd(kq), &epevt[0], nevents, timeout); ++ LOLKQ("ew ret=%d epfd=%d\n", nret, kqueue_epfd(kq)); + if (nret < 0) { + if (errno == EINTR) // only happens when ptrace() is called + goto restart; +@@ -204,6 +207,13 @@ linux_kevent_copyout(struct kqueue *kq, int nready, + ev = &epevt_get()[i]; + kn = (struct knote *) ev->data.ptr; + event = eventlist; ++ /* Another waiter or changelist may have consumed/removed this note ++ * after epoll_wait took its snapshot. Never deliver its userdata. */ ++ if ((kn->kn_flags & KNFL_KNOTE_DELETED) || ++ (kn->kev.flags & EV_DISABLE)) { ++ nret--; ++ continue; ++ } + if (kn->kev.filter == 0) { + dbg_puts("kevent copyout for zero filter, discarding!"); + nret--; +@@ -352,8 +362,11 @@ linux_get_descriptor_type(struct knote *kn) + */ + if (fstat(kn->kev.ident, &sb) < 0) { + dbg_perror("fstat(2)"); ++ LOLKQ("desctype: fstat(%d) FAILED errno=%d\n", (int)kn->kev.ident, (int)errno); + return (-1); + } ++ LOLKQ("desctype: fd=%d mode=0%o sock=%d reg=%d\n", (int)kn->kev.ident, ++ (unsigned)sb.st_mode, !!S_ISSOCK(sb.st_mode), !!S_ISREG(sb.st_mode)); + if (S_ISREG(sb.st_mode)) { + kn->kn_flags |= KNFL_REGULAR_FILE; + dbg_printf("fd %d is a regular file\n", (int)kn->kev.ident); +@@ -382,6 +395,8 @@ linux_get_descriptor_type(struct knote *kn) + #endif + default: + dbg_perror("getsockopt(3)"); ++ LOLKQ("desctype: getsockopt(%d,SO_ACCEPTCONN) FAILED errno=%d\n", ++ (int)kn->kev.ident, (int)errno); + return (-1); + } + } +@@ -436,12 +451,35 @@ epoll_update(int op, struct filter *filt, struct knote *kn, struct epoll_event * + int + linux_fd_to_path(char *buf, size_t bufsz, int fd) + { ++ /* NOTE: must use /proc/self, not /proc/: under Darling ++ * getpid() returns the *guest* pid, which does not exist in the host ++ * /proc. Using the guest pid broke every EVFILT_VNODE watch ++ * (linux_fd_to_path failed -> kevent returned EBADF). */ + char path[1024]; //TODO: Maxpathlen, etc. ++#ifdef DARLING ++ /* darling-lol: readlink(/proc/self/fd) yields a HOST path, but our ++ * consumers (the ext inotify/epoll wrappers) treat paths as GUEST ++ * paths and re-apply guest->host translation (double translation -> ++ * ENOENT). Re-enter the guest namespace via /Volumes/SystemRoot so ++ * the round-trip resolves back to the same host file. */ ++ char hpath[1024]; ++ int n; ++#endif + +- if (snprintf(&path[0], sizeof(path), "/proc/%d/fd/%d", getpid(), fd) < 0) ++ if (snprintf(&path[0], sizeof(path), "/proc/self/fd/%d", fd) < 0) + return (-1); + + memset(buf, 0, bufsz); ++#ifdef DARLING ++ memset(hpath, 0, sizeof(hpath)); ++ n = readlink(path, hpath, sizeof(hpath) - sizeof("/Volumes/SystemRoot") - 1); ++ if (n < 0) ++ return (-1); ++ hpath[n] = '\0'; ++ if (snprintf(buf, bufsz, "/Volumes/SystemRoot%s", hpath) < 0) ++ return (-1); ++ return (n + (int)sizeof("/Volumes/SystemRoot") - 1); ++#else + return (readlink(path, buf, bufsz)); ++#endif + } +- +diff --git a/src/linux/proc.c b/src/linux/proc.c +index 433a9b3..9a70d77 100644 +--- a/src/linux/proc.c ++++ b/src/linux/proc.c +@@ -256,7 +256,7 @@ evfilt_proc_knote_delete(struct filter *filt, struct knote *kn) + if ((kn->kev.flags & EV_DISABLE) == 0) { + if (epoll_ctl(kn->kn_epollfd, EPOLL_CTL_DEL, kn->kdata.kn_dupfd, NULL) < 0) { + dbg_perror("epoll_ctl(2)"); +- return (-1); ++ /* Always release the channel during registration teardown. */ + } + } + +@@ -302,4 +302,3 @@ const struct filter evfilt_proc = { + evfilt_proc_knote_enable, + evfilt_proc_knote_disable, + }; +- +diff --git a/src/linux/read.c b/src/linux/read.c +index b6d2b73..8c7e97d 100644 +--- a/src/linux/read.c ++++ b/src/linux/read.c +@@ -15,6 +15,11 @@ + */ + + #include ++#ifdef DARLING ++#include ++#define LOLKQ(...) do { } while (0) ++#else ++#endif + #include + #include + #include +@@ -58,6 +63,7 @@ get_eof_offset(int fd) + int + evfilt_read_copyout(struct kevent64_s *dst, struct knote *src, void *ptr) + { ++ LOLKQ("read-copyout enter\n"); + struct epoll_event * const ev = (struct epoll_event *) ptr; + + /* Special case: for regular files, return the offset from current position to end of file */ +@@ -144,8 +150,10 @@ evfilt_read_knote_create(struct filter *filt, struct knote *kn) + { + struct epoll_event ev; + +- if (linux_get_descriptor_type(kn) < 0) ++ if (linux_get_descriptor_type(kn) < 0) { ++ LOLKQ("knote-create: desctype FAILED for ident=%d\n", (int)kn->kev.ident); + return (-1); ++ } + + /* Convert the kevent into an epoll_event */ + #if defined(HAVE_EPOLLRDHUP) +@@ -189,10 +197,13 @@ evfilt_read_knote_create(struct filter *filt, struct knote *kn) + else { + kn->kdata.kn_dupfd = _dup_4libkqueue(kn->kev.ident); + fcntl(kn->kdata.kn_dupfd, F_SETFD, FD_CLOEXEC); ++ LOLKQ("knote-create ident=%d dupfd=%d events=0x%x\n", (int)kn->kev.ident, (int)kn->kdata.kn_dupfd, ev.events); + if (epoll_ctl(kn->kn_epollfd, EPOLL_CTL_ADD, kn->kdata.kn_dupfd, &ev) < 0) { + dbg_printf("epoll_ctl(2): %s", strerror(errno)); ++ LOLKQ("knote-create FAILED dupfd=%d errno=%d\n", (int)kn->kdata.kn_dupfd, (int)errno); + return (-1); + } ++ LOLKQ("knote-create OK ident=%d dupfd=%d\n", (int)kn->kev.ident, (int)kn->kdata.kn_dupfd); + } + + return 0; +diff --git a/src/linux/user.c b/src/linux/user.c +index f109ce4..7285f2d 100644 +--- a/src/linux/user.c ++++ b/src/linux/user.c +@@ -102,10 +102,14 @@ linux_evfilt_user_copyout(struct kevent64_s *dst, struct knote *src, void *ptr U + } + if (src->kev.flags & EV_CLEAR) + src->kev.fflags &= ~NOTE_TRIGGER; +- if (src->kev.flags & (EV_DISPATCH | EV_CLEAR | EV_ONESHOT)) { +- if (eventfd_lower(src->kdata.kn_eventfd) < 0) +- return (-1); +- } ++ /* darling-lol: Chromium registers its EVFILT_USER wakeup with EV_ADD only. ++ * A returned user knote means its eventfd was readable, regardless ++ * of whether NOTE_TRIGGER is still visible in kev.fflags. A concurrent ++ * trigger can raise the eventfd just before copyout clears the shared bit, ++ * otherwise leaving a readable fd with no NOTE_TRIGGER forever. */ ++ if (eventfd_lower(src->kdata.kn_eventfd) < 0) ++ return (-1); ++ src->kev.fflags &= ~NOTE_TRIGGER; + + if (src->kev.flags & EV_DISPATCH) + src->kev.fflags &= ~NOTE_TRIGGER; +diff --git a/src/linux/vnode.c b/src/linux/vnode.c +index 58b1544..8337ea0 100644 +--- a/src/linux/vnode.c ++++ b/src/linux/vnode.c +@@ -95,7 +95,10 @@ add_watch(struct filter *filt, struct knote *kn) + return (-1); + + /* Convert the fflags to the inotify mask */ +- mask = IN_CLOSE; ++ /* Closing another descriptor for this inode is not a vnode event. ++ * Watching IN_CLOSE used to poison kn_flags with EV_ONESHOT (the same ++ * bit as KNFL_KNOTE_DELETED), leaving an undrained epoll registration. */ ++ mask = IN_ATTRIB; + if (kn->kev.fflags & NOTE_DELETE) + mask |= IN_ATTRIB | IN_DELETE_SELF; + if (kn->kev.fflags & NOTE_WRITE) +@@ -178,18 +181,9 @@ evfilt_vnode_copyout(struct kevent64_s *dst, struct knote *src, void *ptr UNUSED + return (0); + } + +- /* Check if the watched file has been closed, and +- XXX-this may not exactly match the kevent() behavior if multiple file de +-scriptors reference the same file. +- */ +- if (evt->mask & IN_CLOSE_WRITE || evt->mask & IN_CLOSE_NOWRITE) { +- src->kn_flags |= EV_ONESHOT; /* KLUDGE: causes the knote to be deleted */ +- dst->filter = EVFILT_DROP_POSTPROCESS; /* KLUDGE: causes the event to be discarded */ +- return (0); +- } +- + kevent_int_to_64(&src->kev, dst); + dst->data = 0; ++ dst->fflags = 0; + + /* No error checking because fstat(2) should rarely fail */ + //FIXME: EINTR +@@ -203,7 +197,7 @@ scriptors reference the same file. + if (sb.st_nsize == 0 && src->kev.fflags & NOTE_TRUNCATE) + dst->fflags |= NOTE_TRUNCATE; + #endif +- if (sb.st_size > src->data.vnode.size && src->kev.fflags & NOTE_WRITE) ++ if (sb.st_size > src->data.vnode.size && src->kev.fflags & NOTE_EXTEND) + dst->fflags |= NOTE_EXTEND; + src->data.vnode.nlink = sb.st_nlink; + src->data.vnode.size = sb.st_size; +@@ -227,6 +221,7 @@ scriptors reference the same file. + if (evt->mask & IN_DELETE_SELF && src->kev.fflags & NOTE_DELETE) + dst->fflags |= NOTE_DELETE; + ++ if (!dst->fflags) dst->filter = EVFILT_DROP; + return (0); + } + diff --git a/patches/libmalloc.patch b/patches/libmalloc.patch new file mode 100644 index 0000000..5026da5 --- /dev/null +++ b/patches/libmalloc.patch @@ -0,0 +1,67 @@ +diff --git a/src/internal.h b/src/internal.h +index 28eb2ef..c2c0435 100644 +--- a/src/internal.h ++++ b/src/internal.h +@@ -146,6 +146,10 @@ malloc_traced(void) + return malloc_tracing_enabled; + } + ++#if defined(DARLING) && defined(__x86_64__) ++MALLOC_NOEXPORT uint32_t malloc_cpu_number_native(void); ++#endif ++ + static inline uint32_t + _malloc_cpu_number(void) + { +@@ -153,6 +157,8 @@ _malloc_cpu_number(void) + size_t n; + pthread_cpu_number_np(&n); + return (uint32_t)n; ++#elif defined(DARLING) && defined(__x86_64__) ++ return malloc_cpu_number_native(); + #else + return _os_cpu_number(); + #endif +diff --git a/src/malloc.c b/src/malloc.c +index c6c2a5a..b882aba 100644 +--- a/src/malloc.c ++++ b/src/malloc.c +@@ -23,6 +23,38 @@ + + #include "internal.h" + ++#if defined(DARLING) && defined(__x86_64__) ++#include ++extern struct elf_calls* _elfcalls; ++ ++// Only local, valid output pointers are passed to the vDSO. The public raw ++// syscall wrapper keeps its original EFAULT behavior. Query the CPU afresh ++// on every allocation: an allocating thread may migrate at any time. ++MALLOC_NOEXPORT uint32_t malloc_cpu_number_native(void) { ++ static void* cached; ++ void* function = __atomic_load_n(&cached, __ATOMIC_ACQUIRE); ++ if (!function && _elfcalls) { ++ void* expected = 0; ++ // Never wait during allocator recursion or concurrent initialization. ++ if (__atomic_compare_exchange_n(&cached, &expected, (void*)-1, 0, ++ __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)) { ++ void* libc = _elfcalls->dlopen("libc.so.6"); ++ void* vdso = _elfcalls->dlopen("linux-vdso.so.1"); ++ void* (*lookup)(void*, const char*, const char*) = libc ? ++ (void* (*)(void*, const char*, const char*))_elfcalls->dlsym(libc, "dlvsym") : 0; ++ function = lookup && vdso ? lookup(vdso, "__vdso_getcpu", "LINUX_2.6") : 0; ++ __atomic_store_n(&cached, function ? function : (void*)-1, __ATOMIC_RELEASE); ++ } else function = expected; ++ } ++ if (function && function != (void*)-1) { ++ unsigned cpu = 0; ++ if (((int (*)(unsigned*, unsigned*, void*))function)(&cpu, 0, 0) == 0) ++ return cpu; ++ } ++ return _os_cpu_number(); ++} ++#endif ++ + #if TARGET_OS_IPHONE + // malloc_report(ASL_LEVEL_INFO...) on iOS doesn't show up in the Xcode Console log of the device, + // but ASL_LEVEL_NOTICE does. So raising the log level is helpful. diff --git a/patches/libnetwork.patch b/patches/libnetwork.patch new file mode 100644 index 0000000..e43f8da --- /dev/null +++ b/patches/libnetwork.patch @@ -0,0 +1,24 @@ +diff --git a/include/nw/path.h b/include/nw/path.h +index 5ac8c9f..99d1489 100644 +--- a/include/nw/path.h ++++ b/include/nw/path.h +@@ -183,6 +183,7 @@ void* nw_path_nexus_should_use_event_ring(void); + void* nw_path_monitor_cancel(void); + void* nw_path_monitor_create(void); + void* nw_path_monitor_create_with_type(void); ++void* nw_path_monitor_prohibit_interface_type(void); + void* nw_path_monitor_set_cancel_handler(void); + void* nw_path_monitor_set_queue(void); + void* nw_path_monitor_set_update_handler(void); +diff --git a/src/nw/path.m b/src/nw/path.m +index faf486a..0030e4d 100644 +--- a/src/nw/path.m ++++ b/src/nw/path.m +@@ -141,6 +141,7 @@ DARLING_NW_STUB(nw_path_may_span_multiple_interfaces); + DARLING_NW_STUB(nw_path_monitor_cancel); + DARLING_NW_STUB(nw_path_monitor_create); + DARLING_NW_STUB(nw_path_monitor_create_with_type); ++DARLING_NW_STUB(nw_path_monitor_prohibit_interface_type); + DARLING_NW_STUB(nw_path_monitor_set_cancel_handler); + DARLING_NW_STUB(nw_path_monitor_set_queue); + DARLING_NW_STUB(nw_path_monitor_set_update_handler); diff --git a/patches/libnotify.patch b/patches/libnotify.patch new file mode 100644 index 0000000..b5dcdd5 --- /dev/null +++ b/patches/libnotify.patch @@ -0,0 +1,19 @@ +diff --git a/notifyd/CMakeLists.txt b/notifyd/CMakeLists.txt +index cc6ee7c..ebbedf4 100644 +--- a/notifyd/CMakeLists.txt ++++ b/notifyd/CMakeLists.txt +@@ -23,7 +23,13 @@ add_darling_executable(notifyd ${notifyd_sources}) + target_link_libraries(notifyd system bsm.0) + + install(TARGETS notifyd DESTINATION libexec/darling/usr/sbin) +-# install(FILES com.apple.notifyd.plist DESTINATION libexec/darling/System/Library/LaunchDaemons) ++# darling-lol: notifyd was installed as a binary but its launchd job was ++# commented out, so the Mach service `com.apple.system.notification_center` ++# was never registered. Anything calling notify_register_* (e.g. Chromium's ++# ThermalStateObserverMac via IOPSNotificationCreateRunLoopSource) then got ++# NOTIFY_STATUS_SERVER_NOT_FOUND and logged ++# "unable to register to power notifications. Result: 9". ++install(FILES com.apple.notifyd.plist DESTINATION libexec/darling/System/Library/LaunchDaemons) + install(FILES notifyd.8 DESTINATION libexec/darling/usr/share/man/man8) + install(FILES notify.conf.MacOSX RENAME notify.conf DESTINATION libexec/darling/private/etc) + diff --git a/patches/libplatform.patch b/patches/libplatform.patch new file mode 100644 index 0000000..700b127 --- /dev/null +++ b/patches/libplatform.patch @@ -0,0 +1,190 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 54d2843..b6ec62e 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -127,3 +127,12 @@ if (BUILD_TARGET_64BIT) + endif (BUILD_TARGET_64BIT) + + install(TARGETS platform DESTINATION libexec/darling/usr/lib/system) ++ ++# These primitives are on software video/rendering hot paths even in an ++# otherwise unoptimized development build. Disable builtin recognition so ++# optimizing their loops cannot introduce recursive libc calls. ++set_source_files_properties( ++ src/string/generic/memmove.c ++ src/string/generic/memset_pattern.c ++ src/string/generic/bzero.c ++ PROPERTIES COMPILE_OPTIONS "-O2;-fno-builtin") +diff --git a/src/string/generic/memcmp.c b/src/string/generic/memcmp.c +index fd4311d..34e52aa 100644 +--- a/src/string/generic/memcmp.c ++++ b/src/string/generic/memcmp.c +@@ -34,6 +34,79 @@ + + #if !_PLATFORM_OPTIMIZED_MEMCMP + ++#if defined(__x86_64__) ++#include ++ ++// 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) ++{ ++ const __m128i a = _mm_loadu_si128((const __m128i *)p1); ++ const __m128i b = _mm_loadu_si128((const __m128i *)p2); ++ const unsigned equal = (unsigned)_mm_movemask_epi8(_mm_cmpeq_epi8(a, b)); ++ ++ if (equal != 0xffffu) { ++ const unsigned i = (unsigned)__builtin_ctz(~equal); ++ return (p1[i] - p2[i]); ++ } ++ return (0); ++} ++ ++int ++_platform_memcmp(const void *s1, const void *s2, size_t n) ++{ ++ const unsigned char *p1 = s1, *p2 = s2; ++ ++ if (n >= 16) { ++ int r; ++ ++ do { ++ if ((r = platform_memcmp16(p1, p2)) != 0) ++ return (r); ++ p1 += 16; p2 += 16; n -= 16; ++ } while (n >= 16); ++ // the last (possibly overlapping) 16 bytes ++ return (n != 0 ? platform_memcmp16(p1 + n - 16, p2 + n - 16) : 0); ++ } ++ if (n >= 8) { ++ unsigned long long x = *(const platform_u64u *)p1 ^ *(const platform_u64u *)p2; ++ unsigned i; ++ ++ if (x == 0) { ++ p1 += n - 8; p2 += n - 8; ++ x = *(const platform_u64u *)p1 ^ *(const platform_u64u *)p2; ++ if (x == 0) ++ return (0); ++ } ++ i = (unsigned)__builtin_ctzll(x) >> 3; ++ return (p1[i] - p2[i]); ++ } ++ if (n >= 4) { ++ unsigned x = *(const platform_u32u *)p1 ^ *(const platform_u32u *)p2; ++ unsigned i; ++ ++ if (x == 0) { ++ p1 += n - 4; p2 += n - 4; ++ x = *(const platform_u32u *)p1 ^ *(const platform_u32u *)p2; ++ if (x == 0) ++ return (0); ++ } ++ i = (unsigned)__builtin_ctz(x) >> 3; ++ return (p1[i] - p2[i]); ++ } ++ while (n != 0) { ++ if (*p1 != *p2) ++ return (*p1 - *p2); ++ ++p1; ++p2; --n; ++ } ++ return (0); ++} ++#else + 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) + return (0); + } + ++#endif ++ + #if VARIANT_STATIC + int + memcmp(const void *s1, const void *s2, size_t n) +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 ++++ b/src/string/generic/memset_pattern.c +@@ -23,20 +23,32 @@ + + #include + ++/* Snapshot the pattern once, then double the initialized region. This also ++ * handles a pattern stored inside the destination without changing its value. ++ * Large software video frames must not make one memmove call per pixel. */ ++static void fill_pattern(void *buffer, const void *pattern, size_t length, ++ size_t pattern_size) ++{ ++ unsigned char saved[16]; ++ unsigned char *destination = buffer; ++ if (!length) return; ++ _platform_memmove(saved, pattern, pattern_size); ++ size_t initialized = length < pattern_size ? length : pattern_size; ++ _platform_memmove(destination, saved, initialized); ++ while (initialized < length) { ++ size_t remaining = length - initialized; ++ size_t chunk = initialized < remaining ? initialized : remaining; ++ _platform_memmove(destination + initialized, destination, chunk); ++ initialized += chunk; ++ } ++} ++ + #if !_PLATFORM_OPTIMIZED_MEMSET_PATTERN4 + + void + _platform_memset_pattern4(void *b, const void *pattern4, size_t len) + { +- char * start = (char *)b; +- char * p = (char *)b; +- while ((start + len) - p >= 4) { +- _platform_memmove(p, pattern4, 4); +- p += 4; +- } +- if ((start + len) - p != 0) { +- _platform_memmove(p, pattern4, (start + len) - p); +- } ++ fill_pattern(b, pattern4, len, 4); + } + + #if VARIANT_STATIC +@@ -55,15 +67,7 @@ memset_pattern4(void *b, const void *pattern4, size_t len) + void + _platform_memset_pattern8(void *b, const void *pattern8, size_t len) + { +- char * start = (char *)b; +- char * p = (char *)b; +- while ((start + len) - p >= 8) { +- _platform_memmove(p, pattern8, 8); +- p += 8; +- } +- if ((start + len) - p != 0) { +- _platform_memmove(p, pattern8, (start + len) - p); +- } ++ fill_pattern(b, pattern8, len, 8); + } + + #if VARIANT_STATIC +@@ -82,15 +86,7 @@ memset_pattern8(void *b, const void *pattern8, size_t len) + void + _platform_memset_pattern16(void *b, const void *pattern16, size_t len) + { +- char * start = (char *)b; +- char * p = (char *)b; +- while ((start + len) - p >= 16) { +- _platform_memmove(p, pattern16, 16); +- p += 16; +- } +- if ((start + len) - p != 0) { +- _platform_memmove(p, pattern16, (start + len) - p); +- } ++ fill_pattern(b, pattern16, len, 16); + } + + #if VARIANT_STATIC diff --git a/patches/libpthread.patch b/patches/libpthread.patch new file mode 100644 index 0000000..09c0aa8 --- /dev/null +++ b/patches/libpthread.patch @@ -0,0 +1,18 @@ +diff --git a/src/pthread.c b/src/pthread.c +index 4c9c4a1..db3dda2 100644 +--- a/src/pthread.c ++++ b/src/pthread.c +@@ -2127,6 +2127,13 @@ sched_yield(void) + return 0; + } + ++// Legacy name of sched_yield() that libSystem has always exported ++void ++pthread_yield_np(void) ++{ ++ sched_yield(); ++} ++ + // Libsystem knows about this symbol and exports it to libsyscall + int + pthread_current_stack_contains_np(const void *addr, size_t length) diff --git a/patches/libresolv.patch b/patches/libresolv.patch new file mode 100644 index 0000000..701053b --- /dev/null +++ b/patches/libresolv.patch @@ -0,0 +1,32 @@ +diff --git a/res_send.c b/res_send.c +index f391fb4..8de4637 100644 +--- a/res_send.c ++++ b/res_send.c +@@ -959,6 +959,27 @@ vc_same_ns: + return DNS_RES_STATUS_CONNECTION_REFUSED; + } + ++#ifdef DARLING ++ /* ++ * darling-lol: the TCP (VC) reply read below is a plain ++ * blocking read() with no deadline. If the server is slow or ++ * drops the connection without RST, it blocks forever and the ++ * whole getaddrinfo() caller hangs (observed: RC launcher ++ * stall on clientconfig.rpg.riotgames.com, thread parked in ++ * _sys_read <- _dns_res_send <- _res_9_nsend_2). Bound it so a ++ * stalled server falls through to the normal retry/next-server ++ * path instead of wedging the process. ++ */ ++ { ++ struct timeval tv; ++ tv.tv_sec = statp->retrans > 0 ? statp->retrans : RES_TIMEOUT; ++ if (tv.tv_sec > 30) tv.tv_sec = 30; ++ tv.tv_usec = 0; ++ setsockopt(statp->_vcsock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); ++ setsockopt(statp->_vcsock, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); ++ } ++#endif ++ + statp->_flags |= RES_F_VC; + } + diff --git a/patches/libtrace.patch b/patches/libtrace.patch new file mode 100644 index 0000000..09c1d9f --- /dev/null +++ b/patches/libtrace.patch @@ -0,0 +1,30 @@ +diff --git a/libsystem_trace/unpack_buffer.m b/libsystem_trace/unpack_buffer.m +index 6ad0785..4f27cde 100644 +--- a/libsystem_trace/unpack_buffer.m ++++ b/libsystem_trace/unpack_buffer.m +@@ -191,7 +191,10 @@ char *os_log_decode_buffer(const char *formatString, uint8_t *buffer, uint32_t b + libtrace_assert(readScalar(&buffer[bufferIndex], itemSize, &pointer), "failed to read string pointer from buffer"); + bufferIndex += itemSize; + +- formattedArgument = (char*)pointer; ++ /* darling-lol: a NULL string pointer (os_log("%s", NULL)) ++ * must not kill the logging process in strnlen() below. ++ * Print "(null)" like printf does. */ ++ formattedArgument = (pointer != 0) ? (char*)pointer : "(null)"; + formattedArgumentNeedsFree = false; + formattedArgumentLength = (size_t)maxLength; + +@@ -360,6 +363,13 @@ char *os_log_decode_buffer(const char *formatString, uint8_t *buffer, uint32_t b + + ++formatIndex; + ++ /* darling-lol: never strnlen(NULL) -- asprintf/strdup above can ++ * fail, and a NULL here used to SIGSEGV every helper process ++ * that logged during startup. */ ++ if (formattedArgument == NULL) { ++ formattedArgument = "(null)"; ++ formattedArgumentNeedsFree = false; ++ } + formattedArgumentLength = strnlen(formattedArgument, formattedArgumentLength); + + #if OS_LOG_PRIVACY diff --git a/patches/libxpc.patch b/patches/libxpc.patch new file mode 100644 index 0000000..6bff680 --- /dev/null +++ b/patches/libxpc.patch @@ -0,0 +1,182 @@ +diff --git a/src/connection.m b/src/connection.m +index 366f985..b7b979a 100644 +--- a/src/connection.m ++++ b/src/connection.m +@@ -964,6 +964,14 @@ error_out: + XPC_CLASS(dictionary)* result = nil; + mach_msg_header_t* header = NULL; + ++ if (!MACH_PORT_VALID(this->send_port)) { ++ // Fail fast: the channel was never connected (e.g. the named service ++ // does not exist, so the bootstrap lookup produced no send right). ++ // Without this, the message queues behind a dead channel and the ++ // blocking reply wait below hangs forever (darling-lol M2b). ++ return XPC_ERROR_CONNECTION_INVALID; ++ } ++ + @autoreleasepool { + XPC_CLASS(serializer)* serializer = [XPC_CLASS(serializer) serializer]; + dispatch_mach_msg_t message = NULL; +diff --git a/src/event.m b/src/event.m +index 0870a6c..e2d4bd6 100644 +--- a/src/event.m ++++ b/src/event.m +@@ -19,9 +19,10 @@ + + #import + #import +- +-// actually, the event publisher is it's own class +-typedef xpc_object_t xpc_event_publisher_t; ++#import ++#import ++#import ++#import + + XPC_EXPORT const char* const _xpc_event_key_name = "XPCEventName"; + XPC_EXPORT const char* const _xpc_event_key_stream_name = "XPCEventStreamName"; +@@ -30,15 +31,77 @@ XPC_EXPORT const char* const _xpc_event_key_stream_name = "XPCEventStreamName"; + // private C API + // + ++/* ++ * darling-lol: minimal but FUNCTIONAL event publisher. ++ * ++ * xpc_event_publisher_* was entirely stubbed (create -> NULL, set_handler ++ * -> dropped, activate -> no-op). That silently broke any daemon that gates ++ * its real initialization on the publisher's INITIAL_BARRIER callback. ++ * ++ * notifyd is exactly such a daemon: it creates the publisher for ++ * "com.apple.notifyd.matching" and only calls dispatch_mach_connect() for ++ * its Mach channels from inside the handler when it sees ++ * XPC_EVENT_PUBLISHER_ACTION_INITIAL_BARRIER. Because activate() did ++ * nothing, the barrier never fired, the channels were never connected, and ++ * the `com.apple.system.notification_center` Mach service registered with ++ * launchd was left without a serving endpoint. Clients then either hung in ++ * bootstrap/check-in or failed outright -- e.g. Chromium's ++ * ThermalStateObserverMac logged "unable to register to power ++ * notifications. Result: 9" (NOTIFY_STATUS_SERVER_NOT_FOUND). ++ * ++ * We do not need a real launchd-backed event stream here; we only need the ++ * lifecycle the server expects: ++ * - create() gives back a usable object, ++ * - set_handler()/set_error_handler() remember the blocks, ++ * - activate() delivers the INITIAL_BARRIER action once, synchronously ++ * (Apple delivers it asynchronously on the publisher's queue; sync is ++ * sufficient for notifyd, which only needs its connects to happen ++ * before dispatch_main()). ++ * ADD/REMOVE actions are only ever produced by launchd from registered ++ * event streams, which Darling does not have; they are not needed to bring ++ * notifyd's channels up. ++ */ ++ ++typedef struct _xpc_event_publisher_s { ++ const char* name; ++ dispatch_queue_t queue; ++ void (^handler)(xpc_event_publisher_action_t action, uint64_t event_token, xpc_object_t descriptor); ++ void (^error_handler)(int error); ++ bool activated; ++} *xpc_event_publisher_t; ++ + XPC_EXPORT + void xpc_event_publisher_activate(xpc_event_publisher_t xpub) { +- xpc_stub(); ++ if (xpub == NULL || xpub->activated) ++ return; ++ xpub->activated = true; ++ ++ void (^handler)(xpc_event_publisher_action_t, uint64_t, xpc_object_t) = xpub->handler; ++ if (handler == NULL) ++ // Nothing to deliver; without a handler there is no barrier ++ // contract to honor. ++ return; ++ ++ // Deliver the barrier synchronously. Apple's real implementation ++ // delivers it asynchronously on the publisher's queue, but the only ++ // in-tree consumer (notifyd) just needs dispatch_mach_connect() to run ++ // before dispatch_main(); synchronous delivery satisfies that without ++ // depending on the queue being ready to run async blocks yet. ++ handler(XPC_EVENT_PUBLISHER_ACTION_INITIAL_BARRIER, 0, NULL); + }; + + XPC_EXPORT + xpc_object_t xpc_event_publisher_create(const char* name, void* some_parameter) { +- xpc_stub(); +- return NULL; ++ xpc_event_publisher_t xpub = calloc(1, sizeof(struct _xpc_event_publisher_s)); ++ if (xpub == NULL) ++ return NULL; ++ xpub->name = name; ++ // Apple passes the workloop/queue as the second parameter; callers ++ // (notifyd) pass a dispatch queue. Retained for possible future async ++ // delivery; the current implementation delivers the barrier ++ // synchronously in activate(). ++ xpub->queue = (dispatch_queue_t) some_parameter; ++ return (xpc_object_t) xpub; + }; + + XPC_EXPORT +@@ -76,15 +139,25 @@ au_asid_t xpc_event_publisher_get_subscriber_asid(xpc_event_publisher_t xpub, ui + + XPC_EXPORT + void xpc_event_publisher_set_error_handler(xpc_event_publisher_t xpub, void (^handler)()) { +- // no clue what the parameters for the handler are +- // (probably includes some sort of error parameter, but i have no clue what that is) +- xpc_stub(); ++ if (xpub == NULL) ++ return; ++ // The real signature takes a single `int error` argument. ++ // NOTE: use Block_copy(), NOT [handler copy]. The latter sends an ObjC ++ // message (objc_msgSend) which dereferences the block's isa; if the ++ // caller's blocks runtime leaves isa NULL/unmapped (observed: notifyd ++ // SEGV addr=0x0 inside [handler copy]), messaging crashes. Block_copy ++ // operates on the block struct directly and is safe. ++ xpub->error_handler = (void (^)(int)) Block_copy((const void*)handler); + }; + + XPC_EXPORT + void xpc_event_publisher_set_handler(xpc_event_publisher_t xpub, void (^handler)()) { +- // likewise, no clue what the parameters to the handler are +- xpc_stub(); ++ if (xpub == NULL) ++ return; ++ // The real signature is: ++ // void (^)(xpc_event_publisher_action_t action, ++ // uint64_t event_token, xpc_object_t descriptor) ++ xpub->handler = (void (^)(xpc_event_publisher_action_t, uint64_t, xpc_object_t)) Block_copy((const void*)handler); + }; + + XPC_EXPORT +diff --git a/src/pipe.m b/src/pipe.m +index d04fb98..f60e6ac 100644 +--- a/src/pipe.m ++++ b/src/pipe.m +@@ -209,7 +209,9 @@ out: + goto out; + } + +- *incomingMessage = dict; ++ // The deserializer autoreleases its result. The C pipe API ++ // transfers an owned reference to the caller. ++ *incomingMessage = [dict retain]; + + status = 0; + } else { +@@ -249,7 +251,7 @@ out: + goto out; + } + +- *incomingMessage = dict; ++ *incomingMessage = [dict retain]; + *replyPort = header->msgh_local_port; + + status = 0; +@@ -581,7 +583,7 @@ retry: + goto out; + } + +- *reply = dict; ++ *reply = [dict retain]; + + status = 0; + } else { diff --git a/patches/metal--deps--indium.patch b/patches/metal--deps--indium.patch new file mode 100644 index 0000000..21cc9e4 --- /dev/null +++ b/patches/metal--deps--indium.patch @@ -0,0 +1,7280 @@ +diff --git a/include/indium/device.hpp b/include/indium/device.hpp +index 1572e13..5ebcb5b 100644 +--- a/include/indium/device.hpp ++++ b/include/indium/device.hpp +@@ -51,6 +51,8 @@ namespace Indium { + */ + virtual void pollEvents(uint64_t timeoutNanoseconds) = 0; + virtual void wakeupEventLoop() = 0; ++ virtual bool supportsDepth24Stencil8PixelFormat() = 0; ++ + }; + + std::shared_ptr createSystemDefaultDevice(); +diff --git a/include/indium/library.hpp b/include/indium/library.hpp +index 70f2b28..2c2e579 100644 +--- a/include/indium/library.hpp ++++ b/include/indium/library.hpp +@@ -1,6 +1,8 @@ + #pragma once + + #include ++#include ++#include + + #include + #include +@@ -17,6 +19,11 @@ namespace Indium { + virtual std::shared_ptr device() = 0; + + INDIUM_PROPERTY_READONLY(std::string, n, N,ame); ++ ++ public: ++ // Shader argument metadata, used to build Metal pipeline reflection ++ virtual const std::vector& argumentInfo() const = 0; ++ virtual FunctionType functionType() const = 0; + }; + + class Library { +diff --git a/include/indium/render-command-encoder.hpp b/include/indium/render-command-encoder.hpp +index d17993c..c0b163b 100644 +--- a/include/indium/render-command-encoder.hpp ++++ b/include/indium/render-command-encoder.hpp +@@ -71,5 +71,7 @@ namespace Indium { + virtual void useResources(const std::vector>& resources, ResourceUsage usage, RenderStages stages) = 0; + virtual void useResource(std::shared_ptr resource, ResourceUsage usage) = 0; + virtual void useResources(const std::vector>& resources, ResourceUsage usage) = 0; ++ virtual void memoryBarrier(size_t scope, RenderStages after, RenderStages before) = 0; ++ + }; + }; +diff --git a/include/iridium/iridium.hpp b/include/iridium/iridium.hpp +index 900d03a..ae5eff1 100644 +--- a/include/iridium/iridium.hpp ++++ b/include/iridium/iridium.hpp +@@ -1,6 +1,7 @@ + #pragma once + + #include ++#include + + #include + #include +@@ -90,9 +91,25 @@ namespace Iridium { + float lodMax; + }; + ++ ++ struct StructMemberInfo { ++ size_t offset; ++ std::string typeName; ++ std::string name; ++ }; ++ struct ArgumentInfo { ++ BindingType type; ++ size_t index = 0; ++ std::string name; ++ std::string typeName; ++ size_t size = 0; ++ size_t alignment = 0; ++ std::vector members; ++ }; + struct FunctionInfo { + FunctionType type; + std::vector bindings; ++ std::vector arguments; + std::vector embeddedSamplers; + }; + +diff --git a/private-include/indium/blit-command-encoder.private.hpp b/private-include/indium/blit-command-encoder.private.hpp +index d6e71b2..fc19a42 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 { + std::shared_ptr _privateDevice; + BlitPassDescriptor _descriptor; + ++ // destination textures that need presentation synchronization (drawables) ++ std::vector> _readWriteTextures; ++ ++ void noteDestination(const std::shared_ptr& destination); ++ + void copy(std::shared_ptr source, size_t sourceSlice, size_t sourceLevel, Origin sourceOrigin, std::shared_ptr 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 commandBuffer, const BlitPassDescriptor& descriptor); + ~PrivateBlitCommandEncoder(); + ++ const std::vector>& readWriteTextures() const { return _readWriteTextures; }; ++ + virtual void copy(std::shared_ptr source, size_t sourceOffset, std::shared_ptr destination, size_t destinationOffset, size_t size) override; + virtual void copy(std::shared_ptr source, size_t sourceOffset, size_t sourceBytesPerRow, size_t sourceBytesPerImage, Size sourceSize, std::shared_ptr destination, size_t destinationSlice, size_t destinationLevel, Origin destinationOrigin, BlitOption options = BlitOption::None) override; + virtual void copy(std::shared_ptr source, size_t sourceSlice, size_t sourceLevel, Origin sourceOrigin, Size sourceSize, std::shared_ptr destination, size_t destinationOffset, size_t destinationBytesPerRow, size_t destinationBytesPerImage, BlitOption options = BlitOption::None) 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 ++++ b/private-include/indium/buffer.private.hpp +@@ -29,5 +29,10 @@ namespace Indium { + + INDIUM_PROPERTY(VkBuffer, b, B,uffer) = VK_NULL_HANDLE; + INDIUM_PROPERTY(VkDeviceMemory, m, M,emory) = VK_NULL_HANDLE; ++ ++ private: ++ // appended last: the device address of a buffer never changes, and it was asked from the driver for every buffer ++ // binding of every draw ++ uint64_t _gpuAddress = 0; + }; + }; +diff --git a/private-include/indium/command-buffer.private.hpp b/private-include/indium/command-buffer.private.hpp +index b7ffcce..4645377 100644 +--- a/private-include/indium/command-buffer.private.hpp ++++ b/private-include/indium/command-buffer.private.hpp +@@ -12,6 +12,7 @@ + namespace Indium { + class PrivateCommandQueue; + class PrivateDevice; ++ class PrivateRenderCommandEncoder; + + class PrivateCommandBuffer: public CommandBuffer, public std::enable_shared_from_this { + private: +@@ -25,7 +26,6 @@ namespace Indium { + bool _committed = false; + std::condition_variable _completedCondvar; + bool _completed = false; +- + public: + PrivateCommandBuffer(std::shared_ptr commandQueue); + ~PrivateCommandBuffer(); +@@ -46,6 +46,13 @@ namespace Indium { + virtual std::shared_ptr commandQueue() override; + virtual std::shared_ptr device() override; + ++ // creates the real render encoder immediately (what renderCommandEncoder() did before deferred encoding existed) ++ std::shared_ptr renderCommandEncoderNow(const RenderPassDescriptor& descriptor); ++ void deferredBegin(); ++ void deferredEnd(); ++ // blocks until every deferred encoder of this command buffer has been replayed ++ void waitDeferred(); ++ + void addScheduledHandlerLocked(std::function)> handler); + void addCompletedHandlerLocked(std::function)> handler); + +@@ -53,5 +60,13 @@ namespace Indium { + INDIUM_PROPERTY_READONLY_OBJECT(PrivateDevice, p, P,rivateDevice); + + INDIUM_PROPERTY(VkCommandBuffer, c, C,ommandBuffer) = VK_NULL_HANDLE; ++ private: ++ VkCommandPool _recordingPool = VK_NULL_HANDLE; ++ // Appended LAST on purpose: QuartzCore and Metal are compiled against the earlier layout of this class and read ++ // commandBuffer() and friends by offset, so no existing member may move. ++ // deferred (asynchronously replayed) render encoders that have ended but are not applied to _commandBuffer yet ++ unsigned _deferredPending = 0; ++ std::mutex _deferredMutex; ++ std::condition_variable _deferredCondvar; + }; + }; +diff --git a/private-include/indium/command-encoder.private.hpp b/private-include/indium/command-encoder.private.hpp +index c3b1b93..3dab2ce 100644 +--- a/private-include/indium/command-encoder.private.hpp ++++ b/private-include/indium/command-encoder.private.hpp +@@ -1,6 +1,7 @@ + #pragma once + + #include ++#include + #include + #include + #include +@@ -17,117 +18,295 @@ + + #include + ++#include ++#include ++#include ++ + namespace Indium { ++ bool bindingReuseEnabled(); ++ // Metal's tracked resources carry writes from one encoder into the next. ++ // Queue submission order alone supplies execution order, not Vulkan memory ++ // visibility for attachment, shader, and transfer accesses. ++ inline void finishTrackedEncoder(VkCommandBuffer commandBuffer) { ++ VkMemoryBarrier barrier {}; ++ barrier.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER; ++ barrier.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT; ++ barrier.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT; ++ DynamicVK::vkCmdPipelineBarrier(commandBuffer, ++ VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, ++ 0, 1, &barrier, 0, nullptr, 0, nullptr); ++ } ++ // First few times the app leaves a bound-by-the-shader resource unset: tells us which slots to look at. ++ inline void logUnboundResource(const char* kind, size_t binding, size_t index, size_t available) { ++ static std::atomic logged {0}; ++ if (logged.fetch_add(1) >= 30) { ++ return; ++ } ++ if (FILE* file = fopen("/tmp/metal-trace.log", "a")) { ++ fprintf(file, "unbound-%s: binding=%zu index=%zu (have %zu)\n", kind, binding, index, available); ++ fclose(file); ++ } ++ } ++ ++ // Descriptor writes are consumed synchronously by vkUpdateDescriptorSets. ++ // Keep common shader layouts on the stack; retain a sized heap fallback ++ // for shaders exceeding the inline capacity. Never move storage after ++ // pointers to descriptor infos have been published. ++ template class DescriptorScratch { ++ std::array _inline; ++ std::vector _large; ++ T* _data; ++ size_t _size = 0; ++ public: ++ explicit DescriptorScratch(size_t maximum) { ++ if (maximum > capacity) _large.resize(maximum); ++ _data = maximum > capacity ? _large.data() : _inline.data(); ++ } ++ T& emplace_back() { T& value = _data[_size++]; value = T{}; return value; } ++ void push_back(const T& value) { _data[_size++] = value; } ++ void pop_back() { --_size; } ++ T* data() { return _data; } ++ size_t size() const { return _size; } ++ }; ++ ++ // Per-encoder binding tables are indexed by Metal slot and start empty in every render pass. Growing them one slot at ++ // a time reallocated (and moved) them for each new highest index, through the application's own operator new, which ++ // League replaces with an allocator that takes a mutex. Grow straight to a size that covers the usual slots. ++ template inline void growToIndex(Vector& vector, size_t index) { ++ if (vector.size() > index) return; ++ if (vector.capacity() <= index) vector.reserve(index < 16 ? 16 : index + 1); ++ vector.resize(index + 1); ++ } ++ ++ template struct ResolvedResource { ++ Base* object = nullptr; ++ Private* value = nullptr; ++ bool depthFeedback = false; ++ bool refresh(const std::shared_ptr& owner) { ++ if (object == owner.get()) return false; ++ object = owner.get(); ++ value = dynamic_cast(object); ++ return true; ++ } ++ }; ++ struct ResolvedFunctionResources { ++ std::vector> buffers; ++ std::vector> textures; ++ std::vector> samplers; ++ }; ++ ++ template inline bool sameResourceOwner(const std::shared_ptr& a, const std::shared_ptr& b) { ++ return a.get() == b.get() && !a.owner_before(b) && !b.owner_before(a); ++ } + struct FunctionResources { + std::vector, size_t>> buffers; + std::vector> textures; + std::vector> samplers; ++ uint64_t revision = 0; + +- void setBytes(std::shared_ptr device, const void* bytes, size_t length, size_t index) { +- // TODO: we can make this "Private" instead +- auto buf = device->newBuffer(bytes, length, ResourceOptions::StorageModeShared); ++ // Small inline constants (setBytes) are bump-allocated from shared chunks: a dedicated buffer per call meant one ++ // GPU memory allocation (and a kernel address-space update) per draw. Copies of this struct share the chunk, which ++ // keeps it alive for as long as the command buffer needs it; allocations never overlap. ++ std::shared_ptr byteChunk; ++ size_t byteChunkUsed = 0; ++ // bumped only by texture and sampler changes: most draws change buffers or offsets and nothing else ++ uint64_t imageRevision = 0; + +- if (buffers.size() <= index) { +- buffers.resize(index + 1); ++ void setBytes(std::shared_ptr device, const void* bytes, size_t length, size_t index) { ++ constexpr size_t chunkSize = 256 * 1024; ++ constexpr size_t alignment = 256; ++ ++ // setBytes storage is private and immutable until GPU retirement. ++ // Repeated identical constants can keep their existing address. ++ if (bindingReuseEnabled() && byteChunk && index < buffers.size() && buffers[index].first.get() == byteChunk.get() && ++ length <= 4096 && buffers[index].second <= byteChunkUsed && ++ length <= byteChunkUsed - buffers[index].second && ++ (!length || !memcmp(static_cast(byteChunk->contents()) + buffers[index].second, bytes, length))) ++ return; ++ ++ std::shared_ptr buf; ++ size_t offset = 0; ++ ++ if (length > chunkSize / 8) { ++ // TODO: we can make this "Private" instead ++ buf = device->newBuffer(bytes, length, ResourceOptions::StorageModeShared); ++ } else { ++ offset = (byteChunkUsed + alignment - 1) & ~(alignment - 1); ++ if (!byteChunk || offset + length > chunkSize) { ++ byteChunk = std::dynamic_pointer_cast(device)->acquireScratchChunk(); ++ offset = 0; ++ } ++ memcpy(static_cast(byteChunk->contents()) + offset, bytes, length); ++ byteChunkUsed = offset + length; ++ buf = byteChunk; + } + +- buffers[index] = std::make_pair(buf, 0); ++ growToIndex(buffers, index); ++ ++ buffers[index] = std::make_pair(std::move(buf), offset); ++ ++revision; + }; + +- void setBuffer(std::shared_ptr buffer, size_t offset, size_t index) { +- if (buffers.size() <= index) { +- buffers.resize(index + 1); +- } ++ void setBuffer(const std::shared_ptr& buffer, size_t offset, size_t index) { ++ growToIndex(buffers, index); + +- buffers[index] = std::make_pair(buffer, offset); ++ const bool ownerChanged = !sameResourceOwner(buffers[index].first, buffer); ++ if (ownerChanged || buffers[index].second != offset) ++revision; ++ if (ownerChanged) buffers[index].first = buffer; ++ buffers[index].second = offset; + }; + + void setBufferOffset(size_t offset, size_t index) { ++ if (index >= buffers.size()) { ++ // Metal requires the buffer to be bound first; this used to write past the end of the vector ++ logUnboundResource("buffer-offset", index, index, buffers.size()); ++ return; ++ } ++ if (buffers[index].second != offset) ++revision; + buffers[index].second = offset; + }; + +- void setSamplerState(std::shared_ptr state, std::optional> lodClamps, size_t index) { +- if (samplers.size() <= index) { +- samplers.resize(index + 1); +- } ++ void setSamplerState(const std::shared_ptr& state, std::optional> lodClamps, size_t index) { ++ growToIndex(samplers, index); + + if (lodClamps) { + auto privateState = std::dynamic_pointer_cast(state); + samplers[index] = privateState->cloneWithClamps(lodClamps->first, lodClamps->second); ++ ++revision; ++imageRevision; + } else { +- samplers[index] = state; ++ if (!sameResourceOwner(samplers[index], state)) { samplers[index] = state; ++revision; ++imageRevision; } + } + }; + +- void setTexture(std::shared_ptr texture, size_t index) { +- if (textures.size() <= index) { +- textures.resize(index + 1); +- } ++ void setTexture(const std::shared_ptr& texture, size_t index) { ++ growToIndex(textures, index); + +- textures[index] = texture; ++ if (!sameResourceOwner(textures[index], texture)) { textures[index] = texture; ++revision; ++imageRevision; } + }; + }; + +- template +- std::array createDescriptorSets(std::array& setLayouts, VkDescriptorPool pool, std::shared_ptr privateDevice, const std::array, setCount>& funcResources, const std::array, setCount>& functionInfos, std::vector>& keepAliveBuffers) { +- std::array descriptorSets {}; +- +- VkDescriptorSetAllocateInfo setAllocateInfo {}; +- setAllocateInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; +- setAllocateInfo.descriptorPool = pool; +- setAllocateInfo.descriptorSetCount = descriptorSets.size(); +- setAllocateInfo.pSetLayouts = setLayouts.data(); +- +- if (DynamicVK::vkAllocateDescriptorSets(privateDevice->device(), &setAllocateInfo, descriptorSets.data()) != VK_SUCCESS) { +- // TODO +- abort(); +- } ++ static constexpr std::array poolSizes { ++ VkDescriptorPoolSize { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 512 }, ++ VkDescriptorPoolSize { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 512 }, ++ VkDescriptorPoolSize { VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 512 }, ++ VkDescriptorPoolSize { VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 512 }, ++ VkDescriptorPoolSize { VK_DESCRIPTOR_TYPE_SAMPLER, 512 }, ++ }; + +- for (size_t i = 0; i < descriptorSets.size(); ++i) { +- std::vector writeDescSet; +- std::forward_list bufInfos; +- std::forward_list imageInfos; ++ static inline VkDescriptorPool newDescriptorPool(std::shared_ptr device) { ++ return device->acquireDescriptorPool(); ++ } + +- const FunctionResources& functionResources = funcResources[i]; +- const FunctionInfo& funcInfo = functionInfos[i]; ++ // Bump allocator for the small per-draw tables of buffer addresses (one dedicated buffer per table meant one GPU memory ++ // allocation per stage per draw). ++ struct AddressPool { ++ std::shared_ptr chunk; ++ size_t used = 0; ++ }; + +- if (functionResources.buffers.size() > 0) { +- std::vector addresses; ++ struct BufferAddressTable { ++ VkBuffer buffer = VK_NULL_HANDLE; ++ uint32_t offset = 0; ++ VkDeviceSize range = 0; ++ }; ++ inline BufferAddressTable makeBufferAddressTable(const FunctionResources& functionResources, const FunctionInfo& functionInfo, ++ const ResolvedFunctionResources* resolved, const std::shared_ptr& privateDevice, ++ std::vector>& keepAliveBuffers, AddressPool& addressPool, ++ const BufferAddressTable* previous = nullptr) { ++ DescriptorScratch addresses(functionInfo.bindings.size()); + + // find the right buffer for each binding (using the binding index) +- for (size_t j = 0; j < funcInfo.bindings.size(); ++j) { +- auto& bindingInfo = funcInfo.bindings[j]; ++ for (size_t j = 0; j < functionInfo.bindings.size(); ++j) { ++ auto& bindingInfo = functionInfo.bindings[j]; + + if (bindingInfo.type != Iridium::BindingType::Buffer) { + continue; + } + +- if (bindingInfo.index >= functionResources.buffers.size()) { +- addresses.push_back(0); ++ PrivateBuffer* privateBuf = nullptr; ++ if (resolved && bindingInfo.index < resolved->buffers.size()) ++ privateBuf = resolved->buffers[bindingInfo.index].value; ++ else if (bindingInfo.index < functionResources.buffers.size()) ++ privateBuf = dynamic_cast(functionResources.buffers[bindingInfo.index].first.get()); ++ if (!privateBuf) { ++ // address 0 here was dereferenced by the shader: GPU page fault at 0x0, ring timeout, GPU reset ++ logUnboundResource("buffer", j, bindingInfo.index, functionResources.buffers.size()); ++ addresses.push_back(privateDevice->fallbackBufferAddress()); + continue; + } +- +- auto privateBuf = std::dynamic_pointer_cast(functionResources.buffers[bindingInfo.index].first); + addresses.push_back(privateBuf->gpuAddress() + functionResources.buffers[bindingInfo.index].second); + } + +- auto addressBuffer = privateDevice->newBuffer(addresses.data(), functionResources.buffers.size() * 8, ResourceOptions::StorageModeShared); +- auto privateAddrBuf = std::dynamic_pointer_cast(addressBuffer); ++ constexpr size_t chunkSize = 256 * 1024; ++ constexpr size_t alignment = 256; ++ const size_t tableSize = (addresses.size() * sizeof(uint64_t) + 15) & ~size_t(15); ++ // Reuse immutable table bytes when setters changed unused slots ++ // or selected an equivalent GPU address. Do not retain storage ++ // from another chunk: its lifetime belongs to an earlier table. ++ if (bindingReuseEnabled() && previous && addressPool.chunk && previous->range == tableSize && ++ previous->offset <= chunkSize && tableSize <= chunkSize - previous->offset && ++ previous->buffer == static_cast(addressPool.chunk.get())->buffer() && ++ !memcmp(static_cast(addressPool.chunk->contents()) + previous->offset, ++ addresses.data(), addresses.size() * sizeof(uint64_t))) ++ return *previous; ++ size_t tableOffset = (addressPool.used + alignment - 1) & ~(alignment - 1); ++ if (!addressPool.chunk || tableOffset + tableSize > chunkSize) { ++ addressPool.chunk = privateDevice->acquireScratchChunk(); ++ // we need to keep the chunk alive until the operation is completed ++ keepAliveBuffers.push_back(addressPool.chunk); ++ tableOffset = 0; ++ } ++ memcpy(static_cast(addressPool.chunk->contents()) + tableOffset, addresses.data(), addresses.size() * sizeof(uint64_t)); ++ addressPool.used = tableOffset + tableSize; ++ // plain pointer: a shared_ptr cast here was an atomic reference count round trip per stage per draw ++ auto* privateAddressBuffer = static_cast(addressPool.chunk.get()); + +- // we need to keep this buffer alive until the operation is completed +- keepAliveBuffers.push_back(addressBuffer); ++ return {privateAddressBuffer->buffer(), static_cast(tableOffset), tableSize}; ++ } + +- auto& info = bufInfos.emplace_front(); +- info.buffer = privateAddrBuf->buffer(); +- info.offset = 0; +- info.range = VK_WHOLE_SIZE; ++ template ++ std::array createDescriptorSets(std::array& setLayouts, VkDescriptorPool& pool, std::vector& retiredPools, std::shared_ptr privateDevice, const std::array, setCount>& funcResources, const std::array, setCount>& functionInfos, std::vector>& keepAliveBuffers, AddressPool& addressPool, const std::array& resolved = {}, const std::array& dynamicTables = {}) { ++ std::array descriptorSets {}; ++ ++ VkDescriptorSetAllocateInfo setAllocateInfo {}; ++ setAllocateInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; ++ setAllocateInfo.descriptorPool = pool; ++ setAllocateInfo.descriptorSetCount = descriptorSets.size(); ++ setAllocateInfo.pSetLayouts = setLayouts.data(); ++ ++ auto result = DynamicVK::vkAllocateDescriptorSets(privateDevice->device(), &setAllocateInfo, descriptorSets.data()); ++ if (result == VK_ERROR_OUT_OF_POOL_MEMORY || result == VK_ERROR_FRAGMENTED_POOL) { ++ // Keep previous sets alive until this encoder's GPU work completes. ++ auto next = newDescriptorPool(privateDevice); ++ retiredPools.push_back(pool); ++ pool = next; ++ setAllocateInfo.descriptorPool = pool; ++ result = DynamicVK::vkAllocateDescriptorSets(privateDevice->device(), &setAllocateInfo, descriptorSets.data()); ++ } ++ if (result != VK_SUCCESS) throw std::runtime_error("Descriptor set allocation failed: " + std::to_string(result)); ++ ++ ++ for (size_t i = 0; i < descriptorSets.size(); ++i) { ++ const FunctionResources& functionResources = funcResources[i]; ++ const FunctionInfo& funcInfo = functionInfos[i]; ++ DescriptorScratch writeDescSet(funcInfo.bindings.size() + 1); ++ VkDescriptorBufferInfo bufferInfo {}; ++ DescriptorScratch imageInfos(funcInfo.bindings.size()); ++ ++ if (std::any_of(funcInfo.bindings.begin(), funcInfo.bindings.end(), [](const auto& binding) { return binding.type == Iridium::BindingType::Buffer; })) { ++ const auto table = dynamicTables[i] ? *dynamicTables[i] : ++ makeBufferAddressTable(functionResources, funcInfo, resolved[i], privateDevice, keepAliveBuffers, addressPool); ++ auto& info = bufferInfo; ++ info.buffer = table.buffer; ++ info.offset = dynamicTables[i] ? 0 : table.offset; ++ info.range = table.range; + + auto& descSet = writeDescSet.emplace_back(); + descSet.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + descSet.dstSet = descriptorSets[i]; + descSet.dstBinding = 0; + descSet.dstArrayElement = 0; +- descSet.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; ++ descSet.descriptorType = dynamicTables[i] ? VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC : VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + descSet.descriptorCount = 1; + descSet.pBufferInfo = &info; + } +@@ -136,17 +315,27 @@ namespace Indium { + auto& bindingInfo = funcInfo.bindings[j]; + + if (bindingInfo.type == Iridium::BindingType::Texture) { +- if (bindingInfo.index >= functionResources.textures.size()) { +- continue; ++ PrivateTexture* privateTexture = nullptr; ++ if (resolved[i] && bindingInfo.index < resolved[i]->textures.size()) ++ privateTexture = resolved[i]->textures[bindingInfo.index].value; ++ else if (bindingInfo.index < functionResources.textures.size()) ++ privateTexture = dynamic_cast(functionResources.textures[bindingInfo.index].get()); ++ ++ auto& info = imageInfos.emplace_back(); ++ if (privateTexture) { ++ info.imageView = privateTexture->sampledImageView(); ++ info.imageLayout = privateTexture->imageLayout(); ++ } else { ++ // leaving the descriptor unwritten lets the shader read whatever the set memory held ++ logUnboundResource("texture", j, bindingInfo.index, functionResources.textures.size()); ++ if (!privateDevice->nullDescriptorEnabled()) { ++ imageInfos.pop_back(); ++ continue; ++ } ++ info.imageView = VK_NULL_HANDLE; ++ info.imageLayout = VK_IMAGE_LAYOUT_GENERAL; + } + +- auto texture = functionResources.textures[bindingInfo.index]; +- auto privateTexture = std::dynamic_pointer_cast(texture); +- +- auto& info = imageInfos.emplace_front(); +- info.imageView = privateTexture->imageView(); +- info.imageLayout = privateTexture->imageLayout(); +- + auto& descSet = writeDescSet.emplace_back(); + descSet.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + descSet.dstSet = descriptorSets[i]; +@@ -156,19 +345,21 @@ namespace Indium { + descSet.descriptorCount = 1; + descSet.pImageInfo = &info; + } else if (bindingInfo.type == Iridium::BindingType::Sampler) { +- bool embeddedSampler = false; +- +- if (bindingInfo.index == SIZE_MAX) { +- // this binding uses an embedded sampler +- embeddedSampler = true; +- } else if (bindingInfo.index >= functionResources.samplers.size()) { +- continue; ++ PrivateSamplerState* privateSampler = nullptr; ++ if (bindingInfo.index == SIZE_MAX) { ++ privateSampler = dynamic_cast(funcInfo.embeddedSamplerStates[bindingInfo.embeddedSamplerIndex].get()); ++ } else if (resolved[i] && bindingInfo.index < resolved[i]->samplers.size()) { ++ privateSampler = resolved[i]->samplers[bindingInfo.index].value; ++ } else if (bindingInfo.index < functionResources.samplers.size()) { ++ privateSampler = dynamic_cast(functionResources.samplers[bindingInfo.index].get()); ++ } ++ if (!privateSampler) { ++ logUnboundResource("sampler", j, bindingInfo.index, functionResources.samplers.size()); ++ privateSampler = dynamic_cast(privateDevice->fallbackSampler().get()); ++ if (!privateSampler) continue; + } + +- auto sampler = embeddedSampler ? funcInfo.embeddedSamplerStates[bindingInfo.embeddedSamplerIndex] : functionResources.samplers[bindingInfo.index]; +- auto privateSampler = std::dynamic_pointer_cast(sampler); +- +- auto& info = imageInfos.emplace_front(); ++ auto& info = imageInfos.emplace_back(); + info.sampler = privateSampler->sampler(); + + auto& descSet = writeDescSet.emplace_back(); +@@ -188,10 +379,4 @@ namespace Indium { + return descriptorSets; + }; + +- static constexpr std::array poolSizes { +- VkDescriptorPoolSize { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 512 }, +- VkDescriptorPoolSize { VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 512 }, +- VkDescriptorPoolSize { VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 512 }, +- VkDescriptorPoolSize { VK_DESCRIPTOR_TYPE_SAMPLER, 512 }, +- }; + }; +diff --git a/private-include/indium/command-queue.private.hpp b/private-include/indium/command-queue.private.hpp +index edfc54e..6d58cf6 100644 +--- a/private-include/indium/command-queue.private.hpp ++++ b/private-include/indium/command-queue.private.hpp +@@ -3,6 +3,8 @@ + #include + + #include ++#include ++#include + + namespace Indium { + class PrivateDevice; +@@ -22,5 +24,12 @@ namespace Indium { + INDIUM_PROPERTY_READONLY_OBJECT(PrivateDevice, p, P,rivateDevice); + + INDIUM_PROPERTY(VkCommandPool, c, C,ommandPool) = VK_NULL_HANDLE; ++ private: ++ std::mutex _poolMutex; ++ struct RecordingPool { VkCommandPool pool; VkCommandBuffer buffer; }; ++ std::vector _availablePools; ++ public: ++ VkCommandPool acquireCommandPool(VkCommandBuffer& buffer); ++ void releaseCommandPool(VkCommandPool pool, VkCommandBuffer buffer); + }; + }; +diff --git a/private-include/indium/compute-command-encoder.private.hpp b/private-include/indium/compute-command-encoder.private.hpp +index 8546daa..7e4993c 100644 +--- a/private-include/indium/compute-command-encoder.private.hpp ++++ b/private-include/indium/compute-command-encoder.private.hpp +@@ -15,12 +15,14 @@ namespace Indium { + FunctionResources _functionResources; + std::shared_ptr _pso; + VkDescriptorPool _pool = VK_NULL_HANDLE; ++ std::vector _retiredPools; + + // XXX: not sure if we actually need to store pipelines until the command buffer finishes executing, + // but let's do it just in case. + std::vector _savedPipelines; + std::vector _savedFunctionResources; + std::vector> _keepAliveBuffers; ++ AddressPool _addressPool; + + void updateBindings(); + +diff --git a/private-include/indium/device.private.hpp b/private-include/indium/device.private.hpp +index 58f1794..5b2b30d 100644 +--- a/private-include/indium/device.private.hpp ++++ b/private-include/indium/device.private.hpp +@@ -13,6 +13,8 @@ + #include + #include + #include ++#include ++#include + + namespace Indium { + class PrivateDevice; +@@ -25,6 +27,8 @@ namespace Indium { + class PrivateDevice: public Device, public std::enable_shared_from_this { + private: + ++ std::mutex _scratchMutex; ++ std::vector> _scratchFree; + std::mutex _eventLoopMutex; + std::mutex _pollingMutex; + +@@ -59,6 +63,7 @@ namespace Indium { + } + + virtual std::string name() const override; ++ virtual bool supportsDepth24Stencil8PixelFormat() override; + virtual std::shared_ptr newCommandQueue() override; + virtual std::shared_ptr newRenderPipelineState(const RenderPipelineDescriptor& descriptor) override; + virtual std::shared_ptr newComputePipelineState(const ComputePipelineDescriptor& descriptor, PipelineOption options, std::shared_ptr reflection) override; +@@ -73,6 +78,12 @@ namespace Indium { + virtual void pollEvents(uint64_t timeoutNanoseconds) override; + virtual void wakeupEventLoop() override; + ++ // device address of a zero-filled 64 KiB buffer; never 0 ++ uint64_t fallbackBufferAddress(); ++ std::shared_ptr fallbackSampler(); ++ // VK_EXT_robustness2 nullDescriptor was enabled: null image views / vertex buffers are legal ++ bool nullDescriptorEnabled() const { return _nullDescriptor; } ++ + void waitForSemaphore(VkSemaphore semaphore, uint64_t targetValue, std::function callback); + + /** +@@ -86,6 +97,11 @@ namespace Indium { + * Similar to getTimelineSemaphore(), but wraps it in a shared pointer + * that automatically releases the semaphore when all references to it die out. + */ ++ // Fixed-size scratch buffers for per-draw constants and address tables. They go back to a free list when the last ++ // reference is dropped (i.e. when the command buffers that used them are done), instead of being freed and re-allocated. ++ static constexpr size_t scratchChunkSize = 256 * 1024; ++ std::shared_ptr acquireScratchChunk(); ++ + std::shared_ptr getWrappedTimelineSemaphore(); + + BinarySemaphore getBinarySemaphore(bool exportable = false); +@@ -111,5 +127,31 @@ namespace Indium { + + INDIUM_PROPERTY(VkPhysicalDeviceMemoryProperties, m, M,emoryProperties); + INDIUM_PROPERTY_READONLY(Feature, f, F,eatures); ++ ++ private: ++ // These stay LAST: QuartzCore and Metal are compiled against this layout (they read _device and friends at fixed ++ // offsets), so adding members earlier in the class breaks the installed frameworks. ++ // Stand-ins for resources the app never bound (see createDescriptorSets): shaders read buffers through raw device ++ // addresses, so an unbound index used to become address 0 and faulted the GPU there. ++ std::mutex _fallbackMutex; ++ std::shared_ptr _fallbackBuffer; ++ std::shared_ptr _fallbackSampler; ++ bool _nullDescriptor = false; ++ std::mutex _graphicsQueueMutex; ++ std::mutex _oneshotCommandPoolMutex; ++ public: ++ std::mutex& graphicsQueueMutex() { return _graphicsQueueMutex; } ++ std::mutex& oneshotCommandPoolMutex() { return _oneshotCommandPoolMutex; } ++ VkDescriptorPool acquireDescriptorPool(); ++ void releaseDescriptorPool(VkDescriptorPool pool); ++ // Cached handles belong to the device and remain alive through all GPU uses. ++ std::pair acquireRenderPass(const VkRenderPassCreateInfo& info); ++ private: ++ std::mutex _descriptorPoolMutex; ++ std::vector _availableDescriptorPools; ++ std::mutex _timelineSemaphorePoolMutex; ++ std::vector> _availableTimelineSemaphores; ++ std::mutex _renderPassMutex; ++ std::unordered_map _renderPasses; + }; + }; +diff --git a/private-include/indium/dynamic-vk.hpp b/private-include/indium/dynamic-vk.hpp +index 4957eca..fb2a315 100644 +--- a/private-include/indium/dynamic-vk.hpp ++++ b/private-include/indium/dynamic-vk.hpp +@@ -6,6 +6,9 @@ + #include + + #include ++#include ++#include ++#include + + namespace Indium { + namespace DynamicVK { +@@ -14,6 +17,8 @@ namespace Indium { + extern PFN_vkEnumerateInstanceLayerProperties vkEnumerateInstanceLayerProperties; + + bool init(); ++ void beginDiagnosticCapture(VkInstance instance); ++ void endDiagnosticCapture(bool presented); + void finit(); + + /** +@@ -24,7 +29,7 @@ namespace Indium { + bool eagerlyResolveRequired(); + + struct DynamicFunctionBase { +- void* pointer; ++ std::atomic pointer {nullptr}; + std::once_flag _resolveFlag; + const char* name; + +@@ -33,10 +38,11 @@ namespace Indium { + {}; + + bool resolve() { ++ if (pointer.load(std::memory_order_acquire)) return true; + std::call_once(_resolveFlag, [&]() { +- pointer = reinterpret_cast(vkGetInstanceProcAddr(globalInstance, name)); ++ pointer.store(reinterpret_cast(vkGetInstanceProcAddr(globalInstance, name)), std::memory_order_release); + }); +- return !!pointer; ++ return pointer.load(std::memory_order_acquire) != nullptr; + }; + + bool isAvailable() { +@@ -58,16 +64,20 @@ namespace Indium { + {}; + + Ret operator ()(Arg... arg) { +- if (!resolve()) { +- throw std::runtime_error(std::string("Failed to resolve ") + name); +- } +- Ret (*func)(Arg...) = reinterpret_cast(pointer); ++ void* address = pointer.load(std::memory_order_acquire); ++ if (!address) { ++ if (!resolve()) throw std::runtime_error(std::string("Failed to resolve ") + name); ++ address = pointer.load(std::memory_order_acquire); ++ } ++ Ret (*func)(Arg...) = reinterpret_cast(address); + return func(std::forward(arg)...); + }; + }; + + #define INDIUM_DYNAMICVK_FUNCTION_FOREACH(_macro) \ + _macro(vkAcquireNextImageKHR) \ ++ _macro(vkResetCommandPool) \ ++ _macro(vkResetDescriptorPool) \ + _macro(vkAllocateCommandBuffers) \ + _macro(vkAllocateDescriptorSets) \ + _macro(vkAllocateMemory) \ +@@ -88,6 +98,10 @@ namespace Indium { + _macro(vkCmdDraw) \ + _macro(vkCmdDrawIndexed) \ + _macro(vkCmdEndRenderPass) \ ++ _macro(vkCmdResetQueryPool) \ ++ _macro(vkCmdWriteTimestamp) \ ++ _macro(vkCreateQueryPool) \ ++ _macro(vkGetQueryPoolResults) \ + _macro(vkCmdFillBuffer) \ + _macro(vkCmdPipelineBarrier) \ + _macro(vkCmdSetBlendConstants) \ +@@ -157,6 +171,7 @@ namespace Indium { + _macro(vkGetImageMemoryRequirements) \ + _macro(vkGetPhysicalDeviceFeatures2) \ + _macro(vkGetPhysicalDeviceMemoryProperties) \ ++ _macro(vkGetPhysicalDeviceFormatProperties) \ + _macro(vkGetPhysicalDeviceProperties) \ + _macro(vkGetPhysicalDeviceProperties2) \ + _macro(vkGetPhysicalDeviceQueueFamilyProperties) \ +diff --git a/private-include/indium/library.private.hpp b/private-include/indium/library.private.hpp +index 8752aa2..faf50b2 100644 +--- a/private-include/indium/library.private.hpp ++++ b/private-include/indium/library.private.hpp +@@ -26,6 +26,7 @@ namespace Indium { + std::vector bindings; + std::vector embeddedSamplers; + std::vector> embeddedSamplerStates; ++ std::vector arguments; + }; + + class PrivateFunction: public Function { +@@ -33,6 +34,8 @@ namespace Indium { + PrivateFunction(std::shared_ptr library, const std::string& name, const FunctionInfo& functionInfo); + + virtual std::shared_ptr device() override; ++ virtual const std::vector& argumentInfo() const override; ++ virtual FunctionType functionType() const override { return _functionInfo.functionType; } + + INDIUM_PROPERTY_READONLY_OBJECT(PrivateDevice, p, P,rivateDevice); + +diff --git a/private-include/indium/pipeline.private.hpp b/private-include/indium/pipeline.private.hpp +index a43f532..6f2d7dd 100644 +--- a/private-include/indium/pipeline.private.hpp ++++ b/private-include/indium/pipeline.private.hpp +@@ -33,7 +33,7 @@ namespace Indium { + } + }; + +- void processFunction(std::shared_ptr function, size_t layoutIndex) { ++ void processFunction(std::shared_ptr function, size_t layoutIndex, bool dynamicBuffers = false) { + size_t index = 0; + std::vector bindings; + +@@ -61,7 +61,7 @@ namespace Indium { + // we need a UBO to store the buffer addresses + auto& binding = bindings.emplace_back(); + binding.binding = 0; +- binding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; ++ binding.descriptorType = dynamicBuffers ? VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC : VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + binding.descriptorCount = 1; + binding.stageFlags = functionTypeToVkShaderStageFlags(function->functionInfo().functionType); + } +diff --git a/private-include/indium/render-command-encoder.private.hpp b/private-include/indium/render-command-encoder.private.hpp +index a41b951..6cbea43 100644 +--- a/private-include/indium/render-command-encoder.private.hpp ++++ b/private-include/indium/render-command-encoder.private.hpp +@@ -5,12 +5,15 @@ + #include + #include + #include ++#include + + #include + + #include + #include + #include ++#include ++#include + + namespace Indium { + class PrivateCommandBuffer; +@@ -29,18 +32,40 @@ namespace Indium { + VkFramebuffer _framebuffer = VK_NULL_HANDLE; + VkRenderPass _renderPass = VK_NULL_HANDLE; + VkDescriptorPool _pool = VK_NULL_HANDLE; ++ std::vector _retiredPools; + + std::vector _savedFunctionResources; + + std::array _functionResources {}; + std::vector> _keepAliveBuffers; +- +- void updateBindings(); ++ AddressPool _addressPool; ++ std::array _boundLayouts {}; ++ std::array _boundDescriptorSets {}; ++ std::array _boundResources {}; ++ std::vector> _keepAlivePipelineStates; ++ ++ // A draw that samples the depth texture it has attached (soft-particle style depth fade) must not also write ++ // depth: that is a feedback loop, and writing makes later draws fail their depth test under the effect (the ++ // ward preview hid the lower half of the champion). Kept last: other frameworks read this class by layout. ++ std::shared_ptr _depthRoot; ++ bool _depthWriteRequested = false; ++ bool _depthWriteSuppressed = false; ++ // Unique owners keep every resource referenced by earlier draws alive. ++ std::unordered_set _retainedResourcePointers; ++ std::vector> _retainedResourceOwners; ++ ++ void updateBindings(VkCommandBuffer commandBuffer); + + public: + PrivateRenderCommandEncoder(std::shared_ptr commandBuffer, const RenderPassDescriptor& descriptor); + ~PrivateRenderCommandEncoder(); ++ bool hasHeavyRetirement() const { ++ return _resolvedBufferOwners.size() + _resolvedTextureOwners.size() + ++ _pipelineOwners.size() + _retainedResourceOwners.size() >= 64 || ++ !_retiredPools.empty(); ++ } + ++ virtual void memoryBarrier(size_t scope, RenderStages after, RenderStages before) override; + virtual void setRenderPipelineState(std::shared_ptr renderPipelineState) override; + virtual void setFrontFacingWinding(Winding frontFaceWinding) override; + virtual void setCullMode(CullMode cullMode) override; +@@ -95,8 +120,89 @@ namespace Indium { + virtual void useResources(const std::vector>& resources, ResourceUsage usage) override; + + virtual void endEncoding() override; ++ // begins the Vulkan render pass on the first draw when the pass has no clear (see render-command-encoder.cpp) ++ void beginPassIfNeeded(); + + INDIUM_PROPERTY_OBJECT_VECTOR(Texture, r, R,eadOnlyTextures); + INDIUM_PROPERTY_OBJECT_VECTOR(Texture, r, R,eadWriteTextures); ++ private: ++ // Appended after existing properties: preserve every earlier field offset. ++ std::array _resolvedResources; ++ struct BindingKeys { ++ std::vector> buffers; ++ std::vector textures; ++ std::vector samplers; ++ }; ++ std::array _boundResourceKeys; ++ std::array _boundResourceRevisions {}; ++ std::array _boundFunctionInfos {}; ++ uint64_t _descriptorStageUpdates = 0; ++ uint64_t _descriptorStageReuses = 0; ++ std::array _bufferAddressTables {}; ++ std::array _lastDrawDynamicOffsets {}; ++ VkPipeline _lastDrawPipeline = VK_NULL_HANDLE; ++ VkPrimitiveTopology _lastDrawTopology = VK_PRIMITIVE_TOPOLOGY_MAX_ENUM; ++ VkPipelineLayout _lastDescriptorPipelineLayout = VK_NULL_HANDLE; ++ std::array _lastDrawDescriptorSets {}; ++ std::vector _lastVertexBuffers; ++ std::vector _lastVertexOffsets; ++ ResolvedResource _lastIndexResource; ++ VkBuffer _lastIndexBuffer = VK_NULL_HANDLE; ++ VkDeviceSize _lastIndexOffset = 0; ++ VkIndexType _lastIndexType = VK_INDEX_TYPE_MAX_ENUM; ++ ++ uint64_t _lastVertexResourceRevision = UINT64_MAX; ++ const PrivateRenderPipelineState* _lastVertexBindingPSO = nullptr; ++ ResourceOwnerCache _resolvedBufferOwners; ++ void resolveBuffer(size_t stage, size_t index) { ++ auto& entries = _resolvedResources[stage].buffers; ++ growToIndex(entries, index); ++ auto& entry = entries[index]; ++ const auto& owner = _functionResources[stage].buffers[index].first; ++ if (entry.object == owner.get()) return; ++ entry.object = owner.get(); ++ if (!owner) { entry.value = nullptr; return; } ++ auto [found, inserted] = _resolvedBufferOwners.acquire(owner); ++ entry.value = found->value; ++ } ++ ResourceOwnerCache _pipelineOwners; ++ ResourceOwnerCache _resolvedTextureOwners; ++ void resolveTexture(size_t stage, size_t index) { ++ auto& entries = _resolvedResources[stage].textures; ++ growToIndex(entries, index); ++ auto& entry = entries[index]; ++ const auto& owner = _functionResources[stage].textures[index]; ++ if (entry.object == owner.get()) return; ++ entry.object = owner.get(); ++ if (!owner) { entry.value = nullptr; entry.depthFeedback = false; return; } ++ auto [found, inserted] = _resolvedTextureOwners.acquire(owner); ++ auto& cached = *found; ++ if (inserted) { ++ auto* root = cached.value; ++ while (root) { ++ if (root == _depthRoot.get()) { cached.depthFeedback = true; break; } ++ root = dynamic_cast(root->parentTexture().get()); ++ } ++ } ++ entry.value = cached.value; ++ entry.depthFeedback = cached.depthFeedback; ++ } ++ bool _renderPassDeviceOwned = false; ++ void resolveSampler(size_t stage, size_t index) { ++ auto& entries = _resolvedResources[stage].samplers; ++ growToIndex(entries, index); ++ const auto& owner = _functionResources[stage].samplers[index]; ++ if (entries[index].refresh(owner) && owner && _retainedResourcePointers.insert(owner.get()).second) ++ _retainedResourceOwners.push_back(owner); ++ } ++ ++ // appended last on purpose: lazily begun render pass ++ VkRenderPassBeginInfo _lazyBeginInfo {}; ++ bool _passBegun = true; ++ ++ // appended last: buffers-only fast path of updateBindings ++ std::array _boundImageRevisions {}; ++ std::array _boundKeysStale {}; ++ std::array _boundStageHasBuffers {}; + }; + }; +diff --git a/private-include/indium/texture.private.hpp b/private-include/indium/texture.private.hpp +index ccc27bb..e40e40c 100644 +--- a/private-include/indium/texture.private.hpp ++++ b/private-include/indium/texture.private.hpp +@@ -88,6 +88,7 @@ namespace Indium { + + virtual void precommit(std::shared_ptr cmdbuf); + virtual bool needsExportablePresentationSemaphore() const; ++ virtual VkImageView sampledImageView() { return imageView(); } + }; + + class TextureView: public PrivateTexture { +@@ -102,6 +103,7 @@ namespace Indium { + Range _levels; + Range _layers; + TextureSwizzleChannels _swizzle; ++ VkImageView _sampledImageView = VK_NULL_HANDLE; + + public: + TextureView(std::shared_ptr original, PixelFormat pixelFormat, TextureType textureType, VkImageAspectFlags imageAspect, TextureSwizzleChannels swizzle, const Range& levels, const Range& layers); +@@ -125,6 +127,7 @@ namespace Indium { + virtual size_t parentRelativeSlice() const override; + + virtual VkImageView imageView() override; ++ virtual VkImageView sampledImageView() override { return _sampledImageView ? _sampledImageView : imageView(); } + virtual VkImage image() override; + virtual std::shared_ptr device() override; + virtual VkImageLayout imageLayout() override; +@@ -150,6 +153,7 @@ namespace Indium { + VkImageView _imageView; + VkDeviceMemory _memory; + StorageMode _storageMode; ++ VkImageView _sampledImageView = VK_NULL_HANDLE; + + public: + ConcreteTexture(std::shared_ptr device, const TextureDescriptor& descriptor); +@@ -169,6 +173,7 @@ namespace Indium { + virtual TextureSwizzleChannels swizzle() const override; + + virtual VkImageView imageView() override; ++ virtual VkImageView sampledImageView() override { return _sampledImageView ? _sampledImageView : imageView(); } + virtual VkImage image() override; + virtual VkImageLayout imageLayout() override; + virtual size_t vulkanArrayLength() const override; +diff --git a/private-include/indium/types.private.hpp b/private-include/indium/types.private.hpp +index d49b5f4..45bbd42 100644 +--- a/private-include/indium/types.private.hpp ++++ b/private-include/indium/types.private.hpp +@@ -5,6 +5,7 @@ + #include + + #include ++#include + #include + + namespace Indium { +@@ -187,7 +188,7 @@ namespace Indium { + static constexpr size_t pixelFormatToByteCount(PixelFormat pixelFormat) { + switch (pixelFormat) { + case PixelFormat::Invalid: return 0; +- case PixelFormat::A8Unorm: return 0; ++ case PixelFormat::A8Unorm: return 1; + case PixelFormat::R8Unorm: return 1; + case PixelFormat::R8Unorm_sRGB: return 1; + case PixelFormat::R8Snorm: return 1; +@@ -244,6 +245,127 @@ namespace Indium { + } + }; + ++ ++ struct PixelTransferBlock { size_t width, height, bytes; }; ++ ++ // Buffer pitches count bytes per block row; Vulkan copy pitches count texels. ++ static constexpr PixelTransferBlock pixelFormatTransferBlock(PixelFormat format) { ++ switch (format) { ++ case PixelFormat::BC1_RGBA: ++ case PixelFormat::BC1_RGBA_sRGB: ++ case PixelFormat::BC4_RUnorm: ++ case PixelFormat::BC4_RSnorm: ++ case PixelFormat::EAC_R11Unorm: ++ case PixelFormat::EAC_R11Snorm: ++ case PixelFormat::ETC2_RGB8: ++ case PixelFormat::ETC2_RGB8_sRGB: ++ case PixelFormat::ETC2_RGB8A1: ++ case PixelFormat::ETC2_RGB8A1_sRGB: ++ return { 4, 4, 8 }; ++ case PixelFormat::BC2_RGBA: ++ case PixelFormat::BC2_RGBA_sRGB: ++ case PixelFormat::BC3_RGBA: ++ case PixelFormat::BC3_RGBA_sRGB: ++ case PixelFormat::BC5_RGUnorm: ++ case PixelFormat::BC5_RGSnorm: ++ case PixelFormat::BC6H_RGBFloat: ++ case PixelFormat::BC6H_RGBUfloat: ++ case PixelFormat::BC7_RGBAUnorm: ++ case PixelFormat::BC7_RGBAUnorm_sRGB: ++ case PixelFormat::EAC_RG11Unorm: ++ case PixelFormat::EAC_RG11Snorm: ++ case PixelFormat::EAC_RGBA8: ++ case PixelFormat::EAC_RGBA8_sRGB: ++ return { 4, 4, 16 }; ++ case PixelFormat::GBGR422: ++ case PixelFormat::BGRG422: ++ return { 2, 1, 4 }; ++ case PixelFormat::Depth16Unorm: ++ return { 1, 1, 2 }; ++ case PixelFormat::Depth32Float: ++ return { 1, 1, 4 }; ++ case PixelFormat::Stencil8: ++ case PixelFormat::X32_Stencil8: ++ case PixelFormat::X24_Stencil8: ++ return { 1, 1, 1 }; ++ case PixelFormat::ASTC_4x4_sRGB: ++ case PixelFormat::ASTC_4x4_LDR: ++ case PixelFormat::ASTC_4x4_HDR: ++ return { 4, 4, 16 }; ++ case PixelFormat::ASTC_5x4_sRGB: ++ case PixelFormat::ASTC_5x4_LDR: ++ case PixelFormat::ASTC_5x4_HDR: ++ return { 5, 4, 16 }; ++ case PixelFormat::ASTC_5x5_sRGB: ++ case PixelFormat::ASTC_5x5_LDR: ++ case PixelFormat::ASTC_5x5_HDR: ++ return { 5, 5, 16 }; ++ case PixelFormat::ASTC_6x5_sRGB: ++ case PixelFormat::ASTC_6x5_LDR: ++ case PixelFormat::ASTC_6x5_HDR: ++ return { 6, 5, 16 }; ++ case PixelFormat::ASTC_6x6_sRGB: ++ case PixelFormat::ASTC_6x6_LDR: ++ case PixelFormat::ASTC_6x6_HDR: ++ return { 6, 6, 16 }; ++ case PixelFormat::ASTC_8x5_sRGB: ++ case PixelFormat::ASTC_8x5_LDR: ++ case PixelFormat::ASTC_8x5_HDR: ++ return { 8, 5, 16 }; ++ case PixelFormat::ASTC_8x6_sRGB: ++ case PixelFormat::ASTC_8x6_LDR: ++ case PixelFormat::ASTC_8x6_HDR: ++ return { 8, 6, 16 }; ++ case PixelFormat::ASTC_8x8_sRGB: ++ case PixelFormat::ASTC_8x8_LDR: ++ case PixelFormat::ASTC_8x8_HDR: ++ return { 8, 8, 16 }; ++ case PixelFormat::ASTC_10x5_sRGB: ++ case PixelFormat::ASTC_10x5_LDR: ++ case PixelFormat::ASTC_10x5_HDR: ++ return { 10, 5, 16 }; ++ case PixelFormat::ASTC_10x6_sRGB: ++ case PixelFormat::ASTC_10x6_LDR: ++ case PixelFormat::ASTC_10x6_HDR: ++ return { 10, 6, 16 }; ++ case PixelFormat::ASTC_10x8_sRGB: ++ case PixelFormat::ASTC_10x8_LDR: ++ case PixelFormat::ASTC_10x8_HDR: ++ return { 10, 8, 16 }; ++ case PixelFormat::ASTC_10x10_sRGB: ++ case PixelFormat::ASTC_10x10_LDR: ++ case PixelFormat::ASTC_10x10_HDR: ++ return { 10, 10, 16 }; ++ case PixelFormat::ASTC_12x10_sRGB: ++ case PixelFormat::ASTC_12x10_LDR: ++ case PixelFormat::ASTC_12x10_HDR: ++ return { 12, 10, 16 }; ++ case PixelFormat::ASTC_12x12_sRGB: ++ case PixelFormat::ASTC_12x12_LDR: ++ case PixelFormat::ASTC_12x12_HDR: ++ return { 12, 12, 16 }; ++ default: return { 1, 1, pixelFormatToByteCount(format) }; ++ } ++ } ++ ++ struct BufferImagePitch { uint32_t rowLength, imageHeight; size_t rowBytes, imageBytes; }; ++ ++ static inline BufferImagePitch bufferImagePitch(PixelFormat format, size_t width, size_t height, ++ size_t bytesPerRow, size_t bytesPerImage) { ++ auto block = pixelFormatTransferBlock(format); ++ if (!block.bytes) throw std::runtime_error("Unsupported pixel format for buffer/image transfer"); ++ size_t tightRow = ((width + block.width - 1) / block.width) * block.bytes; ++ size_t rows = (height + block.height - 1) / block.height; ++ size_t row = bytesPerRow ? bytesPerRow : tightRow; ++ if (!row || row < tightRow || row % block.bytes || ++ (bytesPerImage && (bytesPerImage % row || bytesPerImage / row < rows))) { ++ throw std::runtime_error("Invalid buffer/image transfer pitch"); ++ } ++ return { static_cast(bytesPerRow ? row / block.bytes * block.width : 0), ++ static_cast(bytesPerImage ? bytesPerImage / row * block.height : 0), ++ row, bytesPerImage ? bytesPerImage : row * rows }; ++ } ++ + static constexpr bool pixelFormatIsCompressed(PixelFormat pixelFormat) { + switch (pixelFormat) { + case PixelFormat::R8Unorm: +@@ -716,8 +838,8 @@ namespace Indium { + case VertexFormat::UInt2: return VK_FORMAT_R32G32_UINT; + case VertexFormat::UInt3: return VK_FORMAT_R32G32B32_UINT; + case VertexFormat::UInt4: return VK_FORMAT_R32G32B32A32_UINT; +- case VertexFormat::Int1010102Normalized: return VK_FORMAT_A2R10G10B10_SINT_PACK32; +- case VertexFormat::UInt1010102Normalized: return VK_FORMAT_A2R10G10B10_UINT_PACK32; ++ case VertexFormat::Int1010102Normalized: return VK_FORMAT_A2B10G10R10_SNORM_PACK32; ++ case VertexFormat::UInt1010102Normalized: return VK_FORMAT_A2B10G10R10_UNORM_PACK32; + case VertexFormat::UChar4Normalized_BGRA: return VK_FORMAT_B8G8R8A8_UNORM; + case VertexFormat::UChar: return VK_FORMAT_R8_UINT; + case VertexFormat::Char: return VK_FORMAT_R8_SINT; +diff --git a/private-include/iridium/dynamic-llvm.hpp b/private-include/iridium/dynamic-llvm.hpp +index b3756ef..ceb41ce 100644 +--- a/private-include/iridium/dynamic-llvm.hpp ++++ b/private-include/iridium/dynamic-llvm.hpp +@@ -2,10 +2,14 @@ + + #include + #include ++#include ++#include + + #include + + #include ++#include ++#include + + namespace Iridium { + namespace DynamicLLVM { +@@ -58,6 +62,9 @@ namespace Iridium { + }; + + #define IRIDIUM_DYNAMICLLVM_FUNCTION_FOREACH(_macro) \ ++ _macro(LLVMGlobalGetValueType) \ ++ _macro(LLVMGetGEPSourceElementType) \ ++ _macro(LLVMIsAPoisonValue) \ + _macro(LLVMBasicBlockAsValue) \ + _macro(LLVMConstIntGetSExtValue) \ + _macro(LLVMConstIntGetZExtValue) \ +@@ -68,6 +75,15 @@ namespace Iridium { + _macro(LLVMCreateMemoryBufferWithMemoryRange) \ + _macro(LLVMDisposeMemoryBuffer) \ + _macro(LLVMDisposeMessage) \ ++ _macro(LLVMRunPasses) \ ++ _macro(LLVMCreatePassBuilderOptions) \ ++ _macro(LLVMDisposePassBuilderOptions) \ ++ _macro(LLVMGetErrorMessage) \ ++ _macro(LLVMDisposeErrorMessage) \ ++ _macro(LLVMGetTarget) \ ++ _macro(LLVMSetTarget) \ ++ _macro(LLVMIsNull) \ ++ _macro(LLVMPrintValueToString) \ + _macro(LLVMDisposeModule) \ + _macro(LLVMGetAlignment) \ + _macro(LLVMGetArrayLength) \ +@@ -77,6 +93,11 @@ namespace Iridium { + _macro(LLVMGetElementAsConstant) \ + _macro(LLVMGetElementType) \ + _macro(LLVMGetFCmpPredicate) \ ++ _macro(LLVMGetICmpPredicate) \ ++ _macro(LLVMInt32Type) \ ++ _macro(LLVMGetFirstUse) \ ++ _macro(LLVMGetNextUse) \ ++ _macro(LLVMGetUser) \ + _macro(LLVMGetFirstBasicBlock) \ + _macro(LLVMGetFirstInstruction) \ + _macro(LLVMGetFirstNamedMetadata) \ +@@ -84,6 +105,7 @@ namespace Iridium { + _macro(LLVMGetIncomingValue) \ + _macro(LLVMGetIndices) \ + _macro(LLVMGetInitializer) \ ++ _macro(LLVMGetAllocatedType) \ + _macro(LLVMGetInstructionOpcode) \ + _macro(LLVMGetIntTypeWidth) \ + _macro(LLVMGetLastInstruction) \ +@@ -107,6 +129,7 @@ namespace Iridium { + _macro(LLVMGetPointerAddressSpace) \ + _macro(LLVMGetReturnType) \ + _macro(LLVMGetSuccessor) \ ++ _macro(LLVMGetNumSuccessors) \ + _macro(LLVMGetTypeKind) \ + _macro(LLVMGetUndefMaskElem) \ + _macro(LLVMGetValueKind) \ +diff --git a/private-include/iridium/spirv.hpp b/private-include/iridium/spirv.hpp +index 95cd3de..8fa6b1a 100644 +--- a/private-include/iridium/spirv.hpp ++++ b/private-include/iridium/spirv.hpp +@@ -68,6 +68,7 @@ namespace Iridium { + Input = 1, + Uniform = 2, + Output = 3, ++ Private = 6, + Function = 7, + StorageBuffer = 12, + PhysicalStorageBuffer = 5349, +@@ -82,6 +83,7 @@ namespace Iridium { + Int64 = 11, + Int16 = 22, + Int8 = 39, ++ ImageQuery = 50, + PhysicalStorageBufferAddresses = 5347, + }; + +@@ -255,6 +257,10 @@ namespace Iridium { + ImageFormat imageFormat = ImageFormat::Unknown; + ResultID imageRealSampleTypeID = ResultIDInvalid; + ++ // Types of Private/Function memory must not carry the explicit layout (Offset/ArrayStride) ++ // that buffer types need, so those are separate types. This IS part of the type's identity. ++ bool layoutFree = false; ++ + // this does NOT need to be considered for equality + ResultID arrayLengthID = ResultIDInvalid; + +@@ -372,6 +378,7 @@ struct std::hash { + result = ((result << 1) ^ std::hash()(type.scalarWidth)) >> 1; + result = ((result << 1) ^ std::hash()(type.integerIsSigned)) >> 1; + result = ((result << 1) ^ std::hash()(type.entryCount)) >> 1; ++ result = ((result << 1) ^ std::hash()(type.layoutFree)) >> 1; + for (const auto& member: type.structureMembers) { + result = ((result << 1) ^ std::hash()(member.id)) >> 1; + for (const auto& decoration: member.decorations) { +@@ -412,6 +419,7 @@ namespace Iridium { + struct GlobalVariable { + StorageClass storageClass; + ResultID typeID; ++ ResultID initializer = ResultIDInvalid; + }; + + struct FunctionVariable { +@@ -485,7 +493,7 @@ namespace Iridium { + IMul = 132, + FMul = 133, + UDiv = 134, +- SDiv = 133, ++ SDiv = 135, + FDiv = 136, + UMod = 137, + SRem = 138, +@@ -618,12 +626,14 @@ namespace Iridium { + std::unordered_map _functionWriters; + PrivateFunctionInfo* _currentFunctionInfo = nullptr; + DynamicByteWriter* _currentFunctionWriter = nullptr; ++ std::vector> _phiFixups; + std::unordered_set _requiredCapabilities; + uint8_t _versionMajor = 1; + uint8_t _versionMinor = 0; + uint16_t _generatorID = 0; + uint16_t _generatorVersion = 0; + std::unordered_map, ResultID> _constantScalars; ++ std::unordered_map, ResultID> _constantBools; + std::unordered_map _undefinedValues; + std::unordered_map _nullValues; + std::unordered_map _resultIDTypes; +@@ -673,7 +683,8 @@ namespace Iridium { + ResultID lookupResultID(uintptr_t association) const; + + void addEntryPoint(const EntryPoint& entryPoint); +- ResultID addGlobalVariable(ResultID typeID, StorageClass storageClass); ++ ResultID addGlobalVariable(ResultID typeID, StorageClass storageClass, ResultID initializer = ResultIDInvalid); ++ ResultID addFunctionVariable(ResultID typeID); + void addDecoration(ResultID resultID, const Decoration& decoration); + FunctionInfo declareFunction(ResultID functionType); + +@@ -686,6 +697,7 @@ namespace Iridium { + ResultID declareConstantComposite(ResultID typeID, std::vector elements); + + ResultID declareUndefinedValue(ResultID typeID); ++ ResultID declareConstantBool(bool value); + ResultID declareNullValue(ResultID typeID); + + ResultID insertLabel(ResultID id = ResultIDInvalid); +@@ -717,9 +729,13 @@ namespace Iridium { + ResultID encodeFOrdGreaterThan(ResultID operand1, ResultID operand2); + void encodeBranch(ResultID targetLabel); + void encodeBranchConditional(ResultID condition, ResultID trueLabel, ResultID falseLabel); +- ResultID encodePhi(ResultID resultTypeID, std::vector> variablesAndBlocks); ++ ResultID encodePhi(ResultID resultTypeID, std::vector> variablesAndBlocks, ResultID result = ResultIDInvalid, std::vector forwardAssociations = {}); + void encodeSelectionMerge(ResultID mergeBlock); + ResultID encodeSqrt(ResultID resultTypeID, ResultID target); ++ // Generic emitters for instructions without a dedicated helper. The operands are result IDs. ++ ResultID encodeRawOp(uint16_t opcode, ResultID resultTypeID, const std::vector& operands); ++ void encodeRawOpNoResult(uint16_t opcode, const std::vector& operands); ++ ResultID encodeGLSLOp(GLSLOpcode opcode, ResultID resultTypeID, const std::vector& operands); + ResultID encodeTrunc(ResultID resultTypeID, ResultID operand); + + void encodeDebugPrint(std::string format, std::vector arguments); +diff --git a/src/indium-kit/layer.cpp b/src/indium-kit/layer.cpp +index 8d00405..6a5fde8 100644 +--- a/src/indium-kit/layer.cpp ++++ b/src/indium-kit/layer.cpp +@@ -105,6 +105,7 @@ void IndiumKit::PrivateDrawable::present() { + info.pImageIndices = &_swapchainImageIndex; + info.pResults = nullptr; + ++ std::lock_guard queueLock(privateDevice->graphicsQueueMutex()); + if (DynamicVK::vkQueuePresentKHR(privateDevice->graphicsQueue(), &info) != VK_SUCCESS) { + // TODO + abort(); +diff --git a/src/indium/blit-command-encoder.cpp b/src/indium/blit-command-encoder.cpp +index e58f0ba..3191023 100644 +--- a/src/indium/blit-command-encoder.cpp ++++ b/src/indium/blit-command-encoder.cpp +@@ -3,6 +3,7 @@ + #include + #include + #include ++#include + + Indium::BlitCommandEncoder::~BlitCommandEncoder() {}; + +@@ -17,6 +18,19 @@ Indium::PrivateBlitCommandEncoder::PrivateBlitCommandEncoder(std::shared_ptr& destination) { ++ // Drawables are presented by another API, so writes to them must signal a presentation semaphore like render passes do ++ auto privateTexture = std::dynamic_pointer_cast(destination); ++ if (privateTexture && privateTexture->needsExportablePresentationSemaphore()) { ++ for (const auto& known: _readWriteTextures) { ++ if (known == destination) { ++ return; ++ } ++ } ++ _readWriteTextures.push_back(destination); ++ } ++}; ++ + Indium::PrivateBlitCommandEncoder::~PrivateBlitCommandEncoder() { + // nothing for now + }; +@@ -38,6 +52,7 @@ void Indium::PrivateBlitCommandEncoder::copy(std::shared_ptr source, siz + }; + + void Indium::PrivateBlitCommandEncoder::copy(std::shared_ptr source, size_t sourceOffset, size_t sourceBytesPerRow, size_t sourceBytesPerImage, Size sourceSize, std::shared_ptr destination, size_t destinationSlice, size_t destinationLevel, Origin destinationOrigin, BlitOption options) { ++ noteDestination(destination); + auto cmdbuf = _privateCommandBuffer.lock(); + auto privateSource = std::dynamic_pointer_cast(source); + auto privateDest = std::dynamic_pointer_cast(destination); +@@ -47,8 +62,7 @@ void Indium::PrivateBlitCommandEncoder::copy(std::shared_ptr source, siz + } + + auto aspect = pixelFormatToVkImageAspectFlags(privateDest->pixelFormat()); +- // FIXME: handle compressed formats +- size_t bytesPerPixel = pixelFormatToByteCount(privateDest->pixelFormat()); ++ auto pitch = bufferImagePitch(privateDest->pixelFormat(), sourceSize.width, sourceSize.height, sourceBytesPerRow, sourceBytesPerImage); + + // first, transition the image to the optimal layout for transfer destinations + VkImageMemoryBarrier barrier {}; +@@ -71,9 +85,9 @@ void Indium::PrivateBlitCommandEncoder::copy(std::shared_ptr source, siz + + // now encode the copy + VkBufferImageCopy copyInfo {}; +- copyInfo.bufferOffset = 0; +- copyInfo.bufferRowLength = sourceBytesPerRow / bytesPerPixel; +- copyInfo.bufferImageHeight = sourceBytesPerImage / sourceBytesPerRow; ++ copyInfo.bufferOffset = sourceOffset; ++ copyInfo.bufferRowLength = pitch.rowLength; ++ copyInfo.bufferImageHeight = pitch.imageHeight; + copyInfo.imageSubresource.aspectMask = aspect; + copyInfo.imageSubresource.mipLevel = destinationLevel; + copyInfo.imageSubresource.baseArrayLayer = destinationSlice; +@@ -105,8 +119,7 @@ void Indium::PrivateBlitCommandEncoder::copy(std::shared_ptr source, si + } + + auto aspect = pixelFormatToVkImageAspectFlags(privateSource->pixelFormat()); +- // FIXME: handle compressed formats +- size_t bytesPerPixel = pixelFormatToByteCount(privateSource->pixelFormat()); ++ auto pitch = bufferImagePitch(privateSource->pixelFormat(), sourceSize.width, sourceSize.height, destinationBytesPerRow, destinationBytesPerImage); + + // first, transition the image to the optimal layout for transfer sources + VkImageMemoryBarrier barrier {}; +@@ -129,9 +142,9 @@ void Indium::PrivateBlitCommandEncoder::copy(std::shared_ptr source, si + + // now encode the copy + VkBufferImageCopy copyInfo {}; +- copyInfo.bufferOffset = 0; +- copyInfo.bufferRowLength = destinationBytesPerRow / bytesPerPixel; +- copyInfo.bufferImageHeight = destinationBytesPerImage / destinationBytesPerRow; ++ copyInfo.bufferOffset = destinationOffset; ++ copyInfo.bufferRowLength = pitch.rowLength; ++ copyInfo.bufferImageHeight = pitch.imageHeight; + copyInfo.imageSubresource.aspectMask = aspect; + copyInfo.imageSubresource.mipLevel = sourceLevel; + copyInfo.imageSubresource.baseArrayLayer = sourceSlice; +@@ -154,6 +167,7 @@ void Indium::PrivateBlitCommandEncoder::copy(std::shared_ptr source, si + }; + + void Indium::PrivateBlitCommandEncoder::copy(std::shared_ptr source, size_t sourceSlice, size_t sourceLevel, Origin sourceOrigin, std::shared_ptr 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(source); + auto privateDest = std::dynamic_pointer_cast(destination); +@@ -315,6 +329,7 @@ void Indium::PrivateBlitCommandEncoder::fillBuffer(std::shared_ptr buffe + }; + + void Indium::PrivateBlitCommandEncoder::generateMipmapsForTexture(std::shared_ptr texture) { ++ noteDestination(texture); + auto privateTexture = std::dynamic_pointer_cast(texture); + auto cmdbuf = _privateCommandBuffer.lock(); + +@@ -439,5 +454,5 @@ void Indium::PrivateBlitCommandEncoder::generateMipmapsForTexture(std::shared_pt + }; + + void Indium::PrivateBlitCommandEncoder::endEncoding() { +- // nothing for now ++ finishTrackedEncoder(_privateCommandBuffer.lock()->commandBuffer()); + }; +diff --git a/src/indium/buffer.cpp b/src/indium/buffer.cpp +index 23fd2d3..e73df2a 100644 +--- a/src/indium/buffer.cpp ++++ b/src/indium/buffer.cpp +@@ -1,6 +1,8 @@ + #include + #include + #include ++#include ++#include + + #include + +@@ -69,23 +71,11 @@ Indium::PrivateBuffer::PrivateBuffer(std::shared_ptr device, size + throw std::runtime_error("No suitable memory region found for buffer with requested storage mode"); + } + +- VkMemoryAllocateInfo allocateInfo {}; +- allocateInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; +- allocateInfo.allocationSize = requirements.size; +- allocateInfo.memoryTypeIndex = targetIndex; ++ Indium::count(length < 4096 ? "bufferAlloc<4K" : (length < 65536 ? "bufferAlloc<64K" : (length < 4194304 ? "bufferAlloc<4M" : "bufferAlloc>=4M")), length); ++ auto allocation = MemoryPool::allocate(_privateDevice->device(), reinterpret_cast(_buffer), targetIndex, requirements.size, requirements.alignment, false, true); ++ _memory = allocation.memory; + +- VkMemoryAllocateFlagsInfo allocateFlags {}; +- allocateFlags.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_FLAGS_INFO; +- allocateFlags.flags = VK_MEMORY_ALLOCATE_DEVICE_ADDRESS_BIT; +- +- allocateInfo.pNext = &allocateFlags; +- +- if (DynamicVK::vkAllocateMemory(_privateDevice->device(), &allocateInfo, nullptr, &_memory) != VK_SUCCESS) { +- // TODO +- abort(); +- } +- +- DynamicVK::vkBindBufferMemory(_privateDevice->device(), _buffer, _memory, 0); ++ DynamicVK::vkBindBufferMemory(_privateDevice->device(), _buffer, _memory, allocation.offset); + }; + + Indium::PrivateBuffer::PrivateBuffer(std::shared_ptr device, const void* pointer, size_t length, ResourceOptions options): +@@ -103,9 +93,11 @@ Indium::PrivateBuffer::PrivateBuffer(std::shared_ptr device, cons + if (_storageMode == StorageMode::Managed) { + VkMappedMemoryRange range {}; + range.sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE; ++ MemoryPool::Allocation allocation; ++ MemoryPool::lookup(reinterpret_cast(_buffer), allocation); + range.memory = _memory; +- range.size = VK_WHOLE_SIZE; +- range.offset = 0; ++ range.size = allocation.size; ++ range.offset = allocation.offset; + if (DynamicVK::vkFlushMappedMemoryRanges(_privateDevice->device(), 1, &range) != VK_SUCCESS) { + // TODO + abort(); +@@ -114,12 +106,10 @@ Indium::PrivateBuffer::PrivateBuffer(std::shared_ptr device, cons + }; + + Indium::PrivateBuffer::~PrivateBuffer() { +- if (_mapped) { +- DynamicVK::vkUnmapMemory(_privateDevice->device(), _memory); +- } +- ++ // the mapping belongs to the shared memory block ++ auto owner = reinterpret_cast(_buffer); + DynamicVK::vkDestroyBuffer(_privateDevice->device(), _buffer, nullptr); +- DynamicVK::vkFreeMemory(_privateDevice->device(), _memory, nullptr); ++ MemoryPool::free(_privateDevice->device(), owner); + }; + + std::shared_ptr Indium::PrivateBuffer::device() { +@@ -136,7 +126,8 @@ void* Indium::PrivateBuffer::contents() { + } + + if (!_mapped) { +- if (DynamicVK::vkMapMemory(_privateDevice->device(), _memory, 0, VK_WHOLE_SIZE, 0, &_mapped) != VK_SUCCESS) { ++ _mapped = MemoryPool::map(_privateDevice->device(), reinterpret_cast(_buffer)); ++ if (!_mapped) { + // TODO + abort(); + } +@@ -148,9 +139,16 @@ void* Indium::PrivateBuffer::contents() { + void Indium::PrivateBuffer::didModifyRange(Range range) { + VkMappedMemoryRange vulkanRange {}; + vulkanRange.sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE; ++ // flush whole 256-byte granules of this buffer's part of the block (granule >= nonCoherentAtomSize) ++ MemoryPool::Allocation allocation; ++ MemoryPool::lookup(reinterpret_cast(_buffer), allocation); ++ VkDeviceSize first = range.start / 256 * 256; ++ VkDeviceSize last = (range.start + range.length + 255) / 256 * 256; ++ if (last > allocation.size) last = allocation.size; ++ if (first >= last) return; + vulkanRange.memory = _memory; +- vulkanRange.size = range.length; +- vulkanRange.offset = range.start; ++ vulkanRange.size = last - first; ++ vulkanRange.offset = allocation.offset + first; + if (DynamicVK::vkFlushMappedMemoryRanges(_privateDevice->device(), 1, &vulkanRange) != VK_SUCCESS) { + // TODO + abort(); +@@ -158,8 +156,10 @@ void Indium::PrivateBuffer::didModifyRange(Range range) { + }; + + uint64_t Indium::PrivateBuffer::gpuAddress() { ++ if (_gpuAddress) return _gpuAddress; + VkBufferDeviceAddressInfo info {}; + info.sType = VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO; + info.buffer = _buffer; +- return DynamicVK::vkGetBufferDeviceAddress(_privateDevice->device(), &info); ++ _gpuAddress = DynamicVK::vkGetBufferDeviceAddress(_privateDevice->device(), &info); ++ return _gpuAddress; + }; +diff --git a/src/indium/command-buffer.cpp b/src/indium/command-buffer.cpp +index 43f4a84..0283316 100644 +--- a/src/indium/command-buffer.cpp ++++ b/src/indium/command-buffer.cpp +@@ -1,3 +1,7 @@ ++#include ++#include ++#include ++#include + #include + #include + #include +@@ -8,7 +12,116 @@ + #include + #include + ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include + #include ++#include ++ ++// Aggregated timings, printed every 5 s, when /tmp/metal-debug-timing exists in the guest at process start ++static bool indiumTimingEnabled() { ++ static const bool enabled = access("/tmp/metal-debug-timing", F_OK) == 0; ++ return enabled; ++} ++static double indiumNow() { ++ return std::chrono::duration(std::chrono::steady_clock::now().time_since_epoch()).count(); ++} ++static void indiumTiming(const char* what, double seconds) { ++ struct Stat { double sum = 0, max = 0; unsigned long count = 0; }; ++ static std::mutex lock; ++ static std::map stats; ++ static double last = 0; ++ std::lock_guard guard(lock); ++ Stat& stat = stats[what]; ++ stat.sum += seconds; ++ stat.count++; ++ if (seconds > stat.max) stat.max = seconds; ++ double now = indiumNow(); ++ if (now - last >= 5) { ++ last = now; ++ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) { ++ fprintf(log, "indium-timing@%.0f:", now); ++ for (auto& entry: stats) { ++ fprintf(log, " %s[n=%lu avg=%.1fms max=%.1fms]", entry.first.c_str(), entry.second.count, entry.second.sum / entry.second.count * 1000, entry.second.max * 1000); ++ } ++ fprintf(log, "\n"); ++ fclose(log); ++ } ++ stats.clear(); ++ } ++} ++ ++// Opt-in, bounded capture: no per-frame file I/O, no draw logging. ++// Timestamps describe presenting command-buffer commits, not compositor scanout. ++namespace { ++struct FrameIntervalSample { ++ std::atomic commit{0}, submitted{0}, presented{0}, completed{0}; ++}; ++static int64_t frameIntervalNow() { ++ return std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count(); ++} ++static FrameIntervalSample* beginFrameIntervalSample(bool presenting) { ++ if (!presenting) return nullptr; ++ static const int64_t start = frameIntervalNow(); ++ static std::atomic enabled {false}; ++ static std::atomic checks {0}; ++ // Late activation permits profiling the existing match without another login. ++ // Only one file check per 128 presenting frames until activated; none after. ++ if (!enabled.load(std::memory_order_relaxed) && ++ (checks.fetch_add(1, std::memory_order_relaxed) % 128) == 0) { ++ if (FILE* flag = fopen("/tmp/metal-frame-interval-capture", "r")) { ++ fclose(flag); ++ enabled.store(true, std::memory_order_relaxed); ++ } ++ } ++ if (!enabled.load(std::memory_order_relaxed)) return nullptr; ++ const int64_t now = frameIntervalNow(); ++ static const bool testing = getenv("RIOT_FRAME_CAPTURE_TEST") != nullptr; ++ if (!testing && now - start < 90000000000LL) return nullptr; // discard reconnect/warmup ++ static std::array samples; ++ static std::atomic count{0}; ++ static std::atomic dumped{false}; ++ static const size_t limit = testing ? 8 : samples.size(); ++ const size_t index = count.fetch_add(1, std::memory_order_relaxed); ++ if (index < limit) { ++ samples[index].commit.store(now, std::memory_order_relaxed); ++ return &samples[index]; ++ } ++ if (dumped.load(std::memory_order_relaxed)) return nullptr; ++ ++ if (dumped.exchange(true)) return nullptr; ++ if (FILE* log = fopen("/tmp/metal-frame-intervals.csv", "w")) { ++ fprintf(log, "commit_ns,submitted_ns,presented_ns,completed_ns\n"); ++ for (size_t i=0;i statCommits {0}, statSubmits {0}, statPresents {0}; ++void submitStatsPresented() { ++ const uint64_t presents = statPresents.fetch_add(1, std::memory_order_relaxed) + 1; ++ if (presents % 1024 != 0) return; ++ static uint64_t lastCommits = 0, lastSubmits = 0; ++ const uint64_t commits = statCommits.load(std::memory_order_relaxed), submits = statSubmits.load(std::memory_order_relaxed); ++ if (FILE* log = fopen("/tmp/indium-submit-stats.log", "a")) { ++ fprintf(log, "%lld batch=%d commits_per_frame=%.2f submits_per_frame=%.2f\n", (long long)frameIntervalNow(), (int)Indium::submitBatchEnabled(), ++ (commits - lastCommits) / 1024.0, (submits - lastSubmits) / 1024.0); ++ fclose(log); ++ } ++ lastCommits = commits; lastSubmits = submits; ++} ++} + + Indium::CommandBuffer::~CommandBuffer() {}; + +@@ -23,14 +136,17 @@ Indium::PrivateCommandBuffer::PrivateCommandBuffer(std::shared_ptrprivateDevice(); ++ // A pool is exclusively leased through recording and GPU completion. ++ // Freeing another buffer from the same pool can otherwise race recording. ++ _recordingPool = _privateCommandQueue->acquireCommandPool(_commandBuffer); + + VkCommandBufferAllocateInfo allocInfo {}; + allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; +- allocInfo.commandPool = _privateCommandQueue->commandPool(); ++ allocInfo.commandPool = _recordingPool; + allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + allocInfo.commandBufferCount = 1; + +- if (DynamicVK::vkAllocateCommandBuffers(_privateDevice->device(), &allocInfo, &_commandBuffer) != VK_SUCCESS) { ++ if (!_commandBuffer && DynamicVK::vkAllocateCommandBuffers(_privateDevice->device(), &allocInfo, &_commandBuffer) != VK_SUCCESS) { + // TODO: handle this in a more C++-friendly way + abort(); + } +@@ -41,13 +157,46 @@ Indium::PrivateCommandBuffer::PrivateCommandBuffer(std::shared_ptrdevice(), _commandBuffer); + }; + ++namespace { ++struct EncoderRetirement { ++ std::vector> encoders; ++ std::shared_ptr queue; ++ VkCommandPool pool; ++ VkCommandBuffer buffer; ++ void operator()() { ++ encoders.clear(); ++ queue->releaseCommandPool(pool, buffer); ++ queue.reset(); ++ } ++}; ++static Indium::RetirementQueue& encoderRetirementQueue() { ++ static Indium::RetirementQueue queue; ++ return queue; ++} ++} ++namespace Indium { ++void finishCommandRetirement() { encoderRetirementQueue().shutdown(); } ++} + Indium::PrivateCommandBuffer::~PrivateCommandBuffer() { +- DynamicVK::vkFreeCommandBuffers(_privateDevice->device(), _privateCommandQueue->commandPool(), 1, &_commandBuffer); ++ // Submitted buffers are held by the completion callback until GPU work and ++ // user completion handlers finish. Unsubmitted buffers have no GPU users. ++ // Tiny buffers cost less to release directly than to signal a Darwin ++ // condition variable and wake another worker through Darling's server. ++ bool heavy = _commandEncoders.size() >= 4; ++ if (!heavy) { ++ for (const auto& encoder : _commandEncoders) { ++ auto* render = dynamic_cast(encoder.get()); ++ if (render && render->hasHeavyRetirement()) { heavy = true; break; } ++ } ++ } ++ EncoderRetirement job {std::move(_commandEncoders), _privateCommandQueue, _recordingPool, _commandBuffer}; ++ if (!heavy || !encoderRetirementQueue().tryPush(std::move(job))) job(); + }; + +-std::shared_ptr Indium::PrivateCommandBuffer::renderCommandEncoder(const RenderPassDescriptor& descriptor) { ++std::shared_ptr Indium::PrivateCommandBuffer::renderCommandEncoderNow(const RenderPassDescriptor& descriptor) { + auto encoder = std::make_shared(shared_from_this(), descriptor); + { + std::scoped_lock lock(_mutex); +@@ -56,7 +205,37 @@ std::shared_ptr Indium::PrivateCommandBuffer::rend + return encoder; + }; + ++void Indium::PrivateCommandBuffer::deferredBegin() { ++ std::lock_guard lock(_deferredMutex); ++ ++_deferredPending; ++} ++ ++void Indium::PrivateCommandBuffer::deferredEnd() { ++ { ++ std::lock_guard lock(_deferredMutex); ++ --_deferredPending; ++ } ++ _deferredCondvar.notify_all(); ++} ++ ++void Indium::PrivateCommandBuffer::waitDeferred() { ++ std::unique_lock lock(_deferredMutex); ++ _deferredCondvar.wait(lock, [this] { return _deferredPending == 0; }); ++} ++ ++std::shared_ptr Indium::PrivateCommandBuffer::renderCommandEncoder(const RenderPassDescriptor& descriptor) { ++ if (AsyncEncode::enabled()) { ++ auto encoder = std::make_shared(shared_from_this(), descriptor); ++ std::scoped_lock lock(_mutex); ++ _commandEncoders.push_back(encoder); ++ return encoder; ++ } ++ waitDeferred(); // an earlier encoder of this buffer may still be waiting for the worker ++ return renderCommandEncoderNow(descriptor); ++}; ++ + std::shared_ptr Indium::PrivateCommandBuffer::blitCommandEncoder() { ++ waitDeferred(); // never record into the Vulkan command buffer while deferred render encoders are being replayed + auto encoder = std::make_shared(shared_from_this()); + { + std::scoped_lock lock(_mutex); +@@ -66,6 +245,7 @@ std::shared_ptr Indium::PrivateCommandBuffer::blitCo + }; + + std::shared_ptr Indium::PrivateCommandBuffer::blitCommandEncoder(const BlitPassDescriptor& descriptor) { ++ waitDeferred(); // never record into the Vulkan command buffer while deferred render encoders are being replayed + auto encoder = std::make_shared(shared_from_this(), descriptor); + { + std::scoped_lock lock(_mutex); +@@ -79,6 +259,7 @@ std::shared_ptr Indium::PrivateCommandBuffer::com + }; + + std::shared_ptr Indium::PrivateCommandBuffer::computeCommandEncoder(const ComputePassDescriptor& descriptor) { ++ waitDeferred(); // never record into the Vulkan command buffer while deferred render encoders are being replayed + auto encoder = std::make_shared(shared_from_this(), descriptor); + { + std::scoped_lock lock(_mutex); +@@ -91,7 +272,198 @@ std::shared_ptr Indium::PrivateCommandBuffer::com + return computeCommandEncoder(ComputePassDescriptor { {}, dispatchType }); + }; + ++namespace { ++// Pending merged submission. Everything is guarded by the device's graphicsQueueMutex (held by every submitter). ++struct PendingSubmit { ++ VkCommandBuffer commandBuffer; ++ std::vector waits; ++ std::vector signals; ++ FrameIntervalSample* frameSample = nullptr; ++}; ++ ++struct SubmitBatchState { ++ std::vector items; ++ std::map signaled; // timeline value signalled inside the pending batch ++ std::shared_ptr device; ++ VkCommandPool barrierPool = VK_NULL_HANDLE; ++ VkCommandBuffer barrierCommandBuffer = VK_NULL_HANDLE; ++ std::chrono::steady_clock::time_point since; ++ bool timerStarted = false; ++}; ++ ++SubmitBatchState& submitBatchState() { ++ static SubmitBatchState* state = new SubmitBatchState; ++ return *state; ++} ++ ++std::condition_variable& submitBatchWake() { ++ static std::condition_variable* cv = new std::condition_variable; ++ return *cv; ++} ++ ++VkCommandBuffer submitBatchBarrier(const std::shared_ptr& device) { ++ auto& state = submitBatchState(); ++ if (state.barrierCommandBuffer) return state.barrierCommandBuffer; ++ // Queue submission must not acquire the upload pool lock: uploaders take that lock before the queue lock. ++ // Keep the immutable batch barrier in its own process-lifetime pool, guarded by the queue mutex. ++ VkCommandPoolCreateInfo poolInfo {}; ++ poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; ++ poolInfo.queueFamilyIndex = *device->graphicsQueueFamilyIndex(); ++ if (Indium::DynamicVK::vkCreateCommandPool(device->device(), &poolInfo, nullptr, &state.barrierPool) != VK_SUCCESS) abort(); ++ VkCommandBufferAllocateInfo allocInfo {}; ++ allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; ++ allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; ++ allocInfo.commandPool = state.barrierPool; ++ allocInfo.commandBufferCount = 1; ++ VkCommandBuffer cb = VK_NULL_HANDLE; ++ if (Indium::DynamicVK::vkAllocateCommandBuffers(device->device(), &allocInfo, &cb) != VK_SUCCESS) abort(); ++ VkCommandBufferBeginInfo begin {}; ++ begin.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; ++ begin.flags = VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT; ++ Indium::DynamicVK::vkBeginCommandBuffer(cb, &begin); ++ VkMemoryBarrier barrier {}; ++ barrier.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER; ++ barrier.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT; ++ barrier.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT; ++ Indium::DynamicVK::vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1, &barrier, 0, nullptr, 0, nullptr); ++ Indium::DynamicVK::vkEndCommandBuffer(cb); ++ state.barrierCommandBuffer = cb; ++ return cb; ++} ++ ++// caller holds the graphics queue mutex ++void flushSubmitBatchLocked() { ++ auto& state = submitBatchState(); ++ if (state.items.empty()) return; ++ auto device = state.device; ++ auto items = std::move(state.items); ++ state.items.clear(); ++ state.signaled.clear(); ++ ++ // one batch: external waits (timeline waits deduplicated to the highest value), all signals (highest value per timeline) ++ std::map waitTimeline, signalTimeline; ++ std::vector waitBinary, signalBinary; ++ std::vector commandBuffers; ++ const VkCommandBuffer barrier = items.size() > 1 ? submitBatchBarrier(device) : VK_NULL_HANDLE; ++ for (size_t i = 0; i < items.size(); ++i) { ++ if (i > 0) { ++ VkCommandBufferSubmitInfo barrierInfo {}; ++ barrierInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO; ++ barrierInfo.commandBuffer = barrier; ++ commandBuffers.push_back(barrierInfo); ++ } ++ VkCommandBufferSubmitInfo info {}; ++ info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO; ++ info.commandBuffer = items[i].commandBuffer; ++ commandBuffers.push_back(info); ++ for (const auto& w: items[i].waits) { ++ if (w.value == 0) { waitBinary.push_back(w); continue; } ++ auto it = waitTimeline.find(w.semaphore); ++ if (it == waitTimeline.end() || it->second.value < w.value) waitTimeline[w.semaphore] = w; ++ } ++ for (const auto& sg: items[i].signals) { ++ if (sg.value == 0) { signalBinary.push_back(sg); continue; } ++ auto it = signalTimeline.find(sg.semaphore); ++ if (it == signalTimeline.end() || it->second.value < sg.value) signalTimeline[sg.semaphore] = sg; ++ } ++ } ++ std::vector waits = waitBinary, signals = signalBinary; ++ for (const auto& [sem, w]: waitTimeline) waits.push_back(w); ++ for (const auto& [sem, sg]: signalTimeline) signals.push_back(sg); ++ ++ VkSubmitInfo2 info {}; ++ info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2; ++ info.commandBufferInfoCount = commandBuffers.size(); ++ info.pCommandBufferInfos = commandBuffers.data(); ++ info.signalSemaphoreInfoCount = signals.size(); ++ info.pSignalSemaphoreInfos = signals.data(); ++ info.waitSemaphoreInfoCount = waits.size(); ++ info.pWaitSemaphoreInfos = waits.data(); ++ Indium::DynamicVK::beginDiagnosticCapture(Indium::globalInstance); ++ if (Indium::DynamicVK::vkQueueSubmit2(device->graphicsQueue(), 1, &info, VK_NULL_HANDLE) != VK_SUCCESS) abort(); ++ statSubmits.fetch_add(1, std::memory_order_relaxed); ++ Indium::DynamicVK::endDiagnosticCapture(false); ++ Indium::count("batchedSubmits", 1); ++ Indium::count("batchedCommandBuffers", items.size()); ++ for (auto& item: items) { ++ if (item.frameSample) item.frameSample->submitted.store(frameIntervalNow(), std::memory_order_release); ++ } ++} ++ ++void startSubmitBatchTimer(const std::shared_ptr& device) { ++ auto& state = submitBatchState(); ++ if (state.timerStarted) return; ++ state.timerStarted = true; ++ std::thread([device]() { ++ auto& state = submitBatchState(); ++ std::mutex wakeMutex; ++ for (;;) { ++ { ++ std::unique_lock lock(wakeMutex); ++ submitBatchWake().wait_for(lock, std::chrono::milliseconds(2)); ++ } ++ std::unique_lock queueLock(device->graphicsQueueMutex()); ++ if (!state.items.empty() && std::chrono::steady_clock::now() - state.since >= std::chrono::milliseconds(2)) flushSubmitBatchLocked(); ++ } ++ }).detach(); ++} ++} ++ ++// ON by default. League commits 29-38 command buffers per frame (one per render pass); submitted one by one, each is an ++// amdgpu_cs ioctl. Merged they become 3-4 submissions per frame: main-thread kernel time 12.1% -> 7.1% of wall time in ++// a Baron fight (three interleaved 8 s windows each), fight 67.8 -> 70.9 fps, auto-attack 89 -> 96+, idle 98 -> 102 ++// (2026-10-10, tools/batch-ab.sh). Live switch: guest file /tmp/metal-no-submit-batch turns it off, re-read every 512 calls. ++bool Indium::submitBatchEnabled() { ++ static std::atomic enabled {true}; ++ static std::atomic calls {0}; ++ if ((calls.fetch_add(1, std::memory_order_relaxed) & 511) == 0) ++ enabled.store(access("/tmp/metal-no-submit-batch", F_OK) != 0, std::memory_order_relaxed); ++ return enabled.load(std::memory_order_relaxed); ++} ++ ++void Indium::flushPendingSubmits() { ++ auto& state = submitBatchState(); ++ std::shared_ptr device = state.device; // set under the queue mutex by the first batched commit ++ if (!device) return; ++ std::unique_lock queueLock(device->graphicsQueueMutex()); ++ flushSubmitBatchLocked(); ++} ++ ++namespace { ++std::atomic bindingReuse{true}; ++void refreshBindingReuse() { ++ static std::atomic calls{0}; ++ // A bounded diagnostic switch for same-process combat A/B. Do not perform ++ // filesystem lookups in the per-draw/per-constant hot paths. ++ if ((calls.fetch_add(1,std::memory_order_relaxed) & 63)==0) ++ bindingReuse.store(access("/tmp/metal-no-binding-reuse",F_OK)!=0,std::memory_order_relaxed); ++} ++} ++bool Indium::bindingReuseEnabled() {return bindingReuse.load(std::memory_order_relaxed);} ++ + void Indium::PrivateCommandBuffer::commit() { ++ refreshBindingReuse(); ++ // With deferred render encoding the encoders are applied on the ordered worker, so the whole commit (it reads what the ++ // encoders recorded, ends the Vulkan command buffer and reserves timeline values in submission order) must run there too. ++ // It also has to when earlier commits are still queued: submission order is commit order. ++ if (!SubmitThread::onWorker() && (AsyncEncode::enabled() || SubmitThread::pending())) { ++ auto self = shared_from_this(); ++ // read on the calling thread: the worker's commit consumes the list ++ const bool presents = !_drawablesToPresent.empty(); ++ SubmitThread::post([self]() { ++ AsyncEncode::PoolScope pool; ++ self->commit(); ++ }); ++ // Frame-end sync: the present is queued to the main queue by the worker's commit. The game pumps its run loop ++ // right after this call, so let the worker catch up first (it ran in parallel during the frame, the wait is its ++ // remaining lag); otherwise the present misses this pump and is shown one frame late, sometimes two per pump. ++ // Guest flag file /tmp/metal-async-no-frame-sync disables it for A/B runs. ++ if (presents && access("/tmp/metal-async-no-frame-sync", F_OK) != 0) { ++ SubmitThread::drain(); ++ } ++ return; ++ } ++ auto* frameSample = beginFrameIntervalSample(!_drawablesToPresent.empty()); + std::unique_lock lock(_mutex); + + _committed = true; +@@ -110,16 +482,28 @@ void Indium::PrivateCommandBuffer::commit() { + } + } + ++ if (auto blitEncoder = std::dynamic_pointer_cast(encoder)) { ++ for (const auto& texture: blitEncoder->readWriteTextures()) { ++ if (auto privateTexture = std::dynamic_pointer_cast(texture)) { ++ privateTexture->precommit(shared_from_this()); ++ } ++ } ++ } ++ + // TODO: same for other encoders + } + ++ GpuTiming::endSpan(_commandBuffer); + if (DynamicVK::vkEndCommandBuffer(_commandBuffer) != VK_SUCCESS) { + // TODO + abort(); + } + +- // to keep ourselves alive until we're done ++ // Everything below talks to the Vulkan queue and runs on the ordered submit worker (see submit-thread.private.hpp); ++ // this thread only had to finish recording. The task keeps the command buffer alive until it is done. ++ lock.unlock(); + auto self = shared_from_this(); ++ auto submitTask = [this, self, frameSample]() { + + // for the event loop + auto timelineSemaphore = _privateDevice->getWrappedTimelineSemaphore(); +@@ -138,14 +522,49 @@ void Indium::PrivateCommandBuffer::commit() { + readWriteTextures.insert(readWriteTextures.end(), readWrite.begin(), readWrite.end()); + } + +- // TODO: implement this for other encoders (we need to synchronize those texture accesses as well) ++ if (auto blitEncoder = std::dynamic_pointer_cast(encoder)) { ++ // only the writes to drawables are tracked for now, so that blitting into a drawable is synchronized with its presentation ++ auto& readWrite = blitEncoder->readWriteTextures(); ++ readWriteTextures.insert(readWriteTextures.end(), readWrite.begin(), readWrite.end()); ++ } ++ ++ // TODO: implement this for the other texture accesses of the blit and compute encoders (we need to synchronize those as well) + } + ++ // Views share synchronization with their root texture. Deduplicate that owner, ++ // including read/write overlap, before locking or reserving timeline values. ++ auto rootTexture = [](std::shared_ptr texture) { ++ while (auto parent = texture->parentTexture()) texture = parent; ++ return texture; ++ }; ++ auto canonicalize = [&](std::vector>& textures) { ++ std::vector> unique; ++ for (const auto& texture : textures) { ++ auto root = rootTexture(texture); ++ if (std::find(unique.begin(), unique.end(), root) == unique.end()) unique.push_back(root); ++ } ++ textures = std::move(unique); ++ }; ++ canonicalize(readWriteTextures); ++ canonicalize(readOnlyTextures); ++ readOnlyTextures.erase(std::remove_if(readOnlyTextures.begin(), readOnlyTextures.end(), ++ [&](const auto& texture) { return std::find(readWriteTextures.begin(), readWriteTextures.end(), texture) != readWriteTextures.end(); }), readOnlyTextures.end()); ++ ++ // Reserve texture timeline values in submission order. Locking only the ++ // Vulkan call would let a later waiter be submitted ahead of its signal. ++ std::unique_lock queueLock(_privateDevice->graphicsQueueMutex()); ++ + for (const auto& texture: readWriteTextures) { + auto privateTexture = std::dynamic_pointer_cast(texture); +- auto sema = _privateDevice->getWrappedBinarySemaphore(privateTexture->needsExportablePresentationSemaphore()); ++ // Ordinary offscreen textures synchronize through their timeline semaphore. ++ // Their presentation binary semaphore has no consumer. Keep it for ++ // drawable subclasses (including the native non-exportable swapchain). ++ std::shared_ptr sema; ++ if (!std::dynamic_pointer_cast(privateTexture)) { ++ sema = _privateDevice->getWrappedBinarySemaphore(privateTexture->needsExportablePresentationSemaphore()); ++ privateTexture->beginUpdatingPresentationSemaphore(sema); ++ } + presentationSemaphores.push_back(sema); +- privateTexture->beginUpdatingPresentationSemaphore(sema); + } + + std::vector signalInfos; +@@ -202,11 +621,13 @@ void Indium::PrivateCommandBuffer::commit() { + auto privateTexture = std::dynamic_pointer_cast(texture); + const auto& presentSema = presentationSemaphores[i]; + +- VkSemaphoreSubmitInfo signalInfo {}; +- signalInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO; +- signalInfo.semaphore = presentSema->semaphore; +- signalInfo.stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT; +- signalInfos.push_back(signalInfo); ++ if (presentSema) { ++ VkSemaphoreSubmitInfo signalInfo {}; ++ signalInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO; ++ signalInfo.semaphore = presentSema->semaphore; ++ signalInfo.stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT; ++ signalInfos.push_back(signalInfo); ++ } + + handleTextureSemaphores(privateTexture); + } +@@ -215,7 +636,15 @@ void Indium::PrivateCommandBuffer::commit() { + // but it also leaves the resources for this command buffer tied up, essentially becoming a memory leak. + // i've observed this in the cube example, and it happens more than once (because the example display semaphore is exhausted and never signaled). + // UPDATE: upon further testing, it seems that this only occurs when the view is off-screen/hidden. weird. +- _privateDevice->waitForSemaphore(timelineSemaphore->semaphore, timelineSemaphore->count, [self, timelineSemaphore, extraWaitSemaphores, presentationSemaphores]() { ++ const double submitTime = indiumTimingEnabled() ? indiumNow() : 0; ++ _privateDevice->waitForSemaphore(timelineSemaphore->semaphore, timelineSemaphore->count, [self, timelineSemaphore, extraWaitSemaphores, presentationSemaphores, submitTime, frameSample]() { ++ if (frameSample) frameSample->completed.store(frameIntervalNow(), std::memory_order_release); ++ double callbackStart = 0; ++ if (submitTime != 0) { ++ callbackStart = indiumNow(); ++ indiumTiming("submitToCallback", callbackStart - submitTime); ++ } ++ GpuTiming::collect(self->_privateDevice->device(), self->_commandBuffer, self->_privateDevice->properties().limits.timestampPeriod); + { + std::unique_lock lock(self->_mutex); + self->_completed = true; +@@ -234,8 +663,47 @@ void Indium::PrivateCommandBuffer::commit() { + // the handlers may have captured a reference to us (from their surrounding scope), so clear out handlers so those references go away + self->_scheduledHandlers.clear(); + self->_completedHandlers.clear(); ++ ++ if (submitTime != 0) { ++ indiumTiming("handlersDuration", indiumNow() - callbackStart); ++ } + }); + ++ statCommits.fetch_add(1, std::memory_order_relaxed); ++ if (!_drawablesToPresent.empty()) submitStatsPresented(); ++ if (Indium::submitBatchEnabled() && _drawablesToPresent.empty()) { ++ bool batchable = true; ++ for (const auto& ps: presentationSemaphores) if (ps) { batchable = false; break; } ++ if (batchable) { ++ auto& batch = submitBatchState(); ++ batch.device = _privateDevice; ++ PendingSubmit item; ++ item.commandBuffer = _commandBuffer; ++ item.frameSample = frameSample; ++ for (const auto& w: waitInfos) { ++ // a timeline value signalled earlier in this batch is guaranteed by the order (and the barrier) inside it ++ if (w.value != 0) { ++ auto it = batch.signaled.find(w.semaphore); ++ if (it != batch.signaled.end() && it->second >= w.value) continue; ++ } ++ item.waits.push_back(w); ++ } ++ item.signals = signalInfos; ++ if (batch.items.empty()) batch.since = std::chrono::steady_clock::now(); ++ for (const auto& sg: signalInfos) { ++ if (sg.value == 0) continue; ++ auto& v = batch.signaled[sg.semaphore]; ++ if (v < sg.value) v = sg.value; ++ } ++ batch.items.push_back(std::move(item)); ++ startSubmitBatchTimer(_privateDevice); ++ if (batch.items.size() >= 16) flushSubmitBatchLocked(); ++ return; ++ } ++ } ++ // not batched: everything committed earlier goes first so the submission order is preserved ++ flushSubmitBatchLocked(); ++ + commandBufferInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO; + commandBufferInfo.commandBuffer = _commandBuffer; + +@@ -252,23 +720,41 @@ void Indium::PrivateCommandBuffer::commit() { + // the Device constructor tries to choose command queues that support as many operations as possible, but it's possible + // that a particular device only supports certain operations on certain queues (e.g. maybe it only supports transfer operations + // on an exclusive queue that doesn't support graphics or compute). +- if (DynamicVK::vkQueueSubmit2(_privateDevice->graphicsQueue(), 1, &info, VK_NULL_HANDLE) != VK_SUCCESS) { ++ const double submitCallStart = submitTime != 0 ? indiumNow() : 0; ++ if (submitTime != 0) { ++ indiumTiming("waitSemaphoresPerSubmit", waitInfos.size() / 1000.0); ++ } ++ DynamicVK::beginDiagnosticCapture(Indium::globalInstance); ++ VkResult submitResult = DynamicVK::vkQueueSubmit2(_privateDevice->graphicsQueue(), 1, &info, VK_NULL_HANDLE); ++ statSubmits.fetch_add(1, std::memory_order_relaxed); ++ if (submitResult != VK_SUCCESS) { + // TODO + abort(); + } + +- lock.unlock(); ++ if (frameSample) frameSample->submitted.store(frameIntervalNow(), std::memory_order_release); ++ if (submitTime != 0) { ++ indiumTiming("vkQueueSubmit2", indiumNow() - submitCallStart); ++ } ++ + + // now that the binary semaphore signals are pending, we can allow them to be used +- for (const auto& texture: readWriteTextures) { +- auto privateTexture = std::dynamic_pointer_cast(texture); +- privateTexture->endUpdatingPresentationSemaphore(); ++ for (size_t i = 0; i < readWriteTextures.size(); ++i) { ++ if (presentationSemaphores[i]) { ++ auto privateTexture = std::dynamic_pointer_cast(readWriteTextures[i]); ++ privateTexture->endUpdatingPresentationSemaphore(); ++ } + } ++ queueLock.unlock(); + + // we can now queue drawables for presentation and they'll be synchronized properly + for (const auto& drawable: _drawablesToPresent) { + drawable->present(); + } ++ if (frameSample) frameSample->presented.store(frameIntervalNow(), std::memory_order_release); ++ DynamicVK::endDiagnosticCapture(!_drawablesToPresent.empty()); ++ }; ++ SubmitThread::run(std::move(submitTask)); + }; + + void Indium::PrivateCommandBuffer::presentDrawable(std::shared_ptr drawable) { +@@ -307,9 +793,14 @@ void Indium::PrivateCommandBuffer::addCompletedHandler(std::function(std::chrono::steady_clock::now() - waitStart).count()); + }; +diff --git a/src/indium/command-queue.cpp b/src/indium/command-queue.cpp +index 5704602..d2cd020 100644 +--- a/src/indium/command-queue.cpp ++++ b/src/indium/command-queue.cpp +@@ -22,6 +22,7 @@ Indium::PrivateCommandQueue::PrivateCommandQueue(std::shared_ptr + // TODO: handle this in a more C++-friendly way + abort(); + } ++ _availablePools.push_back({_commandPool, VK_NULL_HANDLE}); + + _supportsGraphics = !!_privateDevice->graphicsQueueFamilyIndex(); + _supportsCompute = _supportsGraphics ? (_privateDevice->computeQueueFamilyIndex() ? *_privateDevice->computeQueueFamilyIndex() == *_privateDevice->graphicsQueueFamilyIndex() : false) : !!_privateDevice->computeQueueFamilyIndex(); +@@ -29,11 +30,36 @@ Indium::PrivateCommandQueue::PrivateCommandQueue(std::shared_ptr + }; + + Indium::PrivateCommandQueue::~PrivateCommandQueue() { +- if (_commandPool) { +- DynamicVK::vkDestroyCommandPool(_privateDevice->device(), _commandPool, nullptr); ++ for (auto pool: _availablePools) { ++ DynamicVK::vkDestroyCommandPool(_privateDevice->device(), pool.pool, nullptr); + } + }; + ++VkCommandPool Indium::PrivateCommandQueue::acquireCommandPool(VkCommandBuffer& buffer) { ++ std::lock_guard lock(_poolMutex); ++ if (!_availablePools.empty()) { ++ auto pool = _availablePools.back(); ++ _availablePools.pop_back(); ++ // The previous owner retains this exclusive lease through GPU completion. ++ // Reset without RELEASE_RESOURCES so RADV can reuse its recording storage. ++ if (DynamicVK::vkResetCommandPool(_privateDevice->device(), pool.pool, 0) != VK_SUCCESS) abort(); ++ buffer = pool.buffer; ++ return pool.pool; ++ } ++ VkCommandPoolCreateInfo info {}; ++ info.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; ++ info.queueFamilyIndex = _privateDevice->graphicsQueueFamilyIndex() ? *_privateDevice->graphicsQueueFamilyIndex() : *_privateDevice->computeQueueFamilyIndex(); ++ buffer = VK_NULL_HANDLE; ++ VkCommandPool pool = VK_NULL_HANDLE; ++ if (DynamicVK::vkCreateCommandPool(_privateDevice->device(), &info, nullptr, &pool) != VK_SUCCESS) abort(); ++ return pool; ++} ++ ++void Indium::PrivateCommandQueue::releaseCommandPool(VkCommandPool pool, VkCommandBuffer buffer) { ++ std::lock_guard lock(_poolMutex); ++ _availablePools.push_back({pool, buffer}); ++} ++ + std::shared_ptr Indium::PrivateCommandQueue::commandBuffer() { + return std::make_shared(shared_from_this()); + }; +diff --git a/src/indium/compute-command-encoder.cpp b/src/indium/compute-command-encoder.cpp +index 5f62ad0..5957bec 100644 +--- a/src/indium/compute-command-encoder.cpp ++++ b/src/indium/compute-command-encoder.cpp +@@ -9,27 +9,19 @@ Indium::PrivateComputeCommandEncoder::PrivateComputeCommandEncoder(std::shared_p + _privateDevice(std::dynamic_pointer_cast(commandBuffer->device())), + _descriptor(descriptor) + { +- VkDescriptorPoolCreateInfo poolCreateInfo {}; +- poolCreateInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; +- poolCreateInfo.poolSizeCount = poolSizes.size(); +- poolCreateInfo.pPoolSizes = poolSizes.data(); +- poolCreateInfo.maxSets = 64; +- +- if (DynamicVK::vkCreateDescriptorPool(_privateDevice->device(), &poolCreateInfo, nullptr, &_pool) != VK_SUCCESS) { +- // TODO +- abort(); +- } ++ _pool = newDescriptorPool(_privateDevice); + }; + + Indium::PrivateComputeCommandEncoder::~PrivateComputeCommandEncoder() { + for (auto& pipeline: _savedPipelines) { + DynamicVK::vkDestroyPipeline(_privateDevice->device(), pipeline, nullptr); + } +- DynamicVK::vkDestroyDescriptorPool(_privateDevice->device(), _pool, 0); ++ _privateDevice->releaseDescriptorPool(_pool); ++ for (auto pool : _retiredPools) _privateDevice->releaseDescriptorPool(pool); + }; + + void Indium::PrivateComputeCommandEncoder::endEncoding() { +- // nothing for now ++ finishTrackedEncoder(_privateCommandBuffer.lock()->commandBuffer()); + }; + + void Indium::PrivateComputeCommandEncoder::setComputePipelineState(std::shared_ptr state) { +@@ -138,7 +130,7 @@ Indium::DispatchType Indium::PrivateComputeCommandEncoder::dispatchType() { + void Indium::PrivateComputeCommandEncoder::updateBindings() { + auto buf = _privateCommandBuffer.lock(); + +- std::array descriptorSets = createDescriptorSets(_pso->descriptorSetLayouts().layouts, _pool, _privateDevice, { _functionResources }, { _pso->functionInfo() }, _keepAliveBuffers); ++ std::array descriptorSets = createDescriptorSets(_pso->descriptorSetLayouts().layouts, _pool, _retiredPools, _privateDevice, { _functionResources }, { _pso->functionInfo() }, _keepAliveBuffers, _addressPool); + + DynamicVK::vkCmdBindDescriptorSets(buf->commandBuffer(), VK_PIPELINE_BIND_POINT_COMPUTE, _pso->layout(), 0, descriptorSets.size(), descriptorSets.data(), 0, nullptr); + }; +diff --git a/src/indium/device.cpp b/src/indium/device.cpp +index 322e186..5bc9aad 100644 +--- a/src/indium/device.cpp ++++ b/src/indium/device.cpp +@@ -1,4 +1,6 @@ + #include ++#include ++#include + #include + #include + #include +@@ -83,7 +85,8 @@ Indium::Device::~Device() {}; + + Indium::PrivateDevice::PrivateDevice(VkPhysicalDevice physicalDevice): + _physicalDevice(physicalDevice) +-{ ++{ Indium::SubmitThread::ensureStarted(); ++ + DynamicVK::vkGetPhysicalDeviceProperties(_physicalDevice, &_properties); + + std::vector queueFamilies; +@@ -233,6 +236,36 @@ Indium::PrivateDevice::PrivateDevice(VkPhysicalDevice physicalDevice): + } + } + ++ // An unbound vertex-buffer slot is passed to vkCmdBindVertexBuffers as VK_NULL_HANDLE (see ++ // PrivateRenderCommandEncoder::updateBindings). Without nullDescriptor that is invalid and the GPU fetches from ++ // address 0: on RADV it faulted the gfx ring (page fault at 0x0) and reset the GPU. Enable only that feature, not the ++ // costlier robustBufferAccess2/robustImageAccess2. ++ bool robustness2Enabled = false; ++ VkPhysicalDeviceRobustness2FeaturesEXT robustness2 {}; ++ robustness2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT; ++ { ++ bool hasRobustness2 = false; ++ for (const auto& prop: extProps) { ++ if (strcmp(prop.extensionName, VK_EXT_ROBUSTNESS_2_EXTENSION_NAME) == 0) { ++ hasRobustness2 = true; ++ } ++ } ++ if (hasRobustness2) { ++ VkPhysicalDeviceFeatures2 query {}; ++ query.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; ++ query.pNext = &robustness2; ++ DynamicVK::vkGetPhysicalDeviceFeatures2(_physicalDevice, &query); ++ if (robustness2.nullDescriptor) { ++ robustness2.robustBufferAccess2 = VK_FALSE; ++ robustness2.robustImageAccess2 = VK_FALSE; ++ robustness2.pNext = features13.pNext; ++ features13.pNext = &robustness2; ++ extensions.push_back(VK_EXT_ROBUSTNESS_2_EXTENSION_NAME); ++ robustness2Enabled = true; ++ } ++ } ++ } ++ + VkDeviceCreateInfo deviceCreateInfo {}; + deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; + deviceCreateInfo.pQueueCreateInfos = queueCreateInfos.data(); +@@ -246,6 +279,7 @@ Indium::PrivateDevice::PrivateDevice(VkPhysicalDevice physicalDevice): + } + + _features = indiumFeatures; ++ _nullDescriptor = robustness2.nullDescriptor == VK_TRUE && robustness2Enabled; + + for (const auto& index: queueFamilyIndices) { + VkQueue queue; +@@ -297,6 +331,9 @@ Indium::PrivateDevice::PrivateDevice(VkPhysicalDevice physicalDevice): + }; + + Indium::PrivateDevice::~PrivateDevice() { ++ for (const auto& entry : _renderPasses) DynamicVK::vkDestroyRenderPass(_device, entry.second, nullptr); ++ for (const auto& entry : _availableTimelineSemaphores) DynamicVK::vkDestroySemaphore(_device, entry.first, nullptr); ++ for (auto pool : _availableDescriptorPools) DynamicVK::vkDestroyDescriptorPool(_device, pool, nullptr); + if (_oneshotCommandPool) { + DynamicVK::vkDestroyCommandPool(_device, _oneshotCommandPool, nullptr); + } +@@ -308,6 +345,23 @@ std::string Indium::PrivateDevice::name() const { + return _properties.deviceName; + }; + ++std::pair Indium::PrivateDevice::acquireRenderPass(const VkRenderPassCreateInfo& info) { ++ auto key = renderPassKey(info); ++ std::unique_lock lock(_renderPassMutex, std::defer_lock); ++ if (key) { ++ lock.lock(); ++ auto found = _renderPasses.find(*key); ++ if (found != _renderPasses.end()) return {found->second, true}; ++ // No eviction: submitted encoders and pipelines may still refer to a ++ // cached handle. Unusual descriptions beyond the bound remain private. ++ if (_renderPasses.size() >= 256) { key.reset(); lock.unlock(); } ++ } ++ VkRenderPass pass = VK_NULL_HANDLE; ++ if (DynamicVK::vkCreateRenderPass(_device, &info, nullptr, &pass) != VK_SUCCESS) abort(); ++ if (key) _renderPasses.emplace(*key, pass); ++ return {pass, key.has_value()}; ++} ++ + std::shared_ptr Indium::PrivateDevice::newCommandQueue() { + return std::make_shared(shared_from_this()); + }; +@@ -317,9 +371,9 @@ std::shared_ptr Indium::PrivateDevice::newRenderPip + }; + + std::shared_ptr Indium::PrivateDevice::newComputePipelineState(const ComputePipelineDescriptor& descriptor, PipelineOption options, std::shared_ptr reflection) { +- if (options != PipelineOption::None) { +- throw std::runtime_error("TODO: support compute pipeline options"); +- } ++ // Reflection is retained on the function; these flags do not change GPU code. ++ if ((static_cast(options) & ~size_t(3)) != 0) ++ throw std::invalid_argument("Unsupported compute pipeline option"); + return std::make_shared(shared_from_this(), descriptor); + }; + +@@ -357,6 +411,7 @@ std::shared_ptr Indium::PrivateDevice::newLibrary(const void* d + break; + } + ++ funcInfo.arguments = info.arguments; + funcInfo.bindings.insert(funcInfo.bindings.end(), info.bindings.begin(), info.bindings.end()); + funcInfo.embeddedSamplers.insert(funcInfo.embeddedSamplers.end(), info.embeddedSamplers.begin(), info.embeddedSamplers.end()); + } +@@ -480,9 +535,18 @@ void Indium::PrivateDevice::waitForSemaphore(VkSemaphore semaphore, uint64_t tar + wakeupEventLoop(); + }; + +-// TODO: create a semaphore pool to avoid constantly creating and destroying semaphores ++// Non-exported timeline handles are exclusively leased until the last owner ++// (including GPU-completion callbacks) releases them. Preserve monotonic values. + + Indium::TimelineSemaphore Indium::PrivateDevice::getTimelineSemaphore() { ++ { ++ std::lock_guard lock(_timelineSemaphorePoolMutex); ++ if (!_availableTimelineSemaphores.empty()) { ++ const auto entry = _availableTimelineSemaphores.back(); ++ _availableTimelineSemaphores.pop_back(); ++ return TimelineSemaphore { shared_from_this(), entry.first, entry.second }; ++ } ++ } + VkSemaphoreTypeCreateInfo typeInfo {}; + typeInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO; + typeInfo.semaphoreType = VK_SEMAPHORE_TYPE_TIMELINE; +@@ -505,7 +569,14 @@ Indium::TimelineSemaphore Indium::PrivateDevice::getTimelineSemaphore() { + }; + + void Indium::PrivateDevice::putTimelineSemaphore(const TimelineSemaphore& semaphore) { +- DynamicVK::vkDestroySemaphore(_device, semaphore.semaphore, nullptr); ++ { ++ std::lock_guard lock(_timelineSemaphorePoolMutex); ++ if (_availableTimelineSemaphores.size() < 256 && semaphore.count != UINT64_MAX) { ++ _availableTimelineSemaphores.emplace_back(semaphore.semaphore, semaphore.count); ++ return; ++ } ++ } ++ DynamicVK::vkDestroySemaphore(_device, semaphore.semaphore, nullptr); + }; + + std::shared_ptr Indium::PrivateDevice::getWrappedTimelineSemaphore() { +@@ -551,3 +622,91 @@ std::shared_ptr Indium::PrivateDevice::getWrappedBinary + delete ptr; + }); + }; ++ ++bool Indium::PrivateDevice::supportsDepth24Stencil8PixelFormat() { ++ VkFormatProperties properties {}; ++ DynamicVK::vkGetPhysicalDeviceFormatProperties(_physicalDevice, VK_FORMAT_D24_UNORM_S8_UINT, &properties); ++ return (properties.optimalTilingFeatures & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) != 0; ++} ++ ++uint64_t Indium::PrivateDevice::fallbackBufferAddress() { ++ std::scoped_lock lock(_fallbackMutex); ++ if (!_fallbackBuffer) { ++ std::vector zeros(64 * 1024, 0); ++ _fallbackBuffer = newBuffer(zeros.data(), zeros.size(), ResourceOptions::StorageModeShared); ++ } ++ return _fallbackBuffer->gpuAddress(); ++}; ++ ++std::shared_ptr Indium::PrivateDevice::fallbackSampler() { ++ { ++ std::scoped_lock lock(_fallbackMutex); ++ if (_fallbackSampler) { ++ return _fallbackSampler; ++ } ++ } ++ auto sampler = newSamplerState(SamplerDescriptor {}); ++ std::scoped_lock lock(_fallbackMutex); ++ if (!_fallbackSampler) { ++ _fallbackSampler = sampler; ++ } ++ return _fallbackSampler; ++}; ++ ++std::shared_ptr Indium::PrivateDevice::acquireScratchChunk() { ++ std::shared_ptr base; ++ ++ { ++ std::scoped_lock lock(_scratchMutex); ++ if (!_scratchFree.empty()) { ++ base = std::move(_scratchFree.back()); ++ _scratchFree.pop_back(); ++ } ++ } ++ ++ if (!base) { ++ base = newBuffer(scratchChunkSize, ResourceOptions::StorageModeShared); ++ } ++ ++ // the returned handle gives the chunk back instead of destroying it ++ auto self = shared_from_this(); ++ return std::shared_ptr(base.get(), [self, base](Buffer*) { ++ std::scoped_lock lock(self->_scratchMutex); ++ self->_scratchFree.push_back(base); ++ }); ++}; ++ ++VkDescriptorPool Indium::PrivateDevice::acquireDescriptorPool() { ++ VkDescriptorPool pool = VK_NULL_HANDLE; ++ { ++ std::lock_guard lock(_descriptorPoolMutex); ++ if (!_availableDescriptorPools.empty()) { ++ pool = _availableDescriptorPools.back(); ++ _availableDescriptorPools.pop_back(); ++ } ++ } ++ // Returned only after its encoder and GPU completion owner are released. ++ // Each acquisition has exclusive ownership; reset retains driver storage. ++ if (pool) { ++ if (DynamicVK::vkResetDescriptorPool(_device, pool, 0) != VK_SUCCESS) abort(); ++ return pool; ++ } ++ VkDescriptorPoolCreateInfo info {}; ++ info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; ++ info.poolSizeCount = poolSizes.size(); ++ info.pPoolSizes = poolSizes.data(); ++ info.maxSets = 64; ++ if (DynamicVK::vkCreateDescriptorPool(_device, &info, nullptr, &pool) != VK_SUCCESS) abort(); ++ return pool; ++} ++void Indium::PrivateDevice::releaseDescriptorPool(VkDescriptorPool pool) { ++ if (!pool) return; ++ { ++ std::lock_guard lock(_descriptorPoolMutex); ++ if (_availableDescriptorPools.size() < 128) { ++ _availableDescriptorPools.push_back(pool); ++ return; ++ } ++ } ++ DynamicVK::vkDestroyDescriptorPool(_device, pool, nullptr); ++} +diff --git a/src/indium/dynamic-vk.cpp b/src/indium/dynamic-vk.cpp +index a4677c3..69ef23f 100644 +--- a/src/indium/dynamic-vk.cpp ++++ b/src/indium/dynamic-vk.cpp +@@ -1,4 +1,5 @@ + #include ++#include + + #ifdef DARLING + #include +@@ -6,6 +7,10 @@ + #endif + + static void* libraryHandle = NULL; ++static void (*captureBegin)(void*) = nullptr; ++static void (*captureEnd)(int) = nullptr; ++void Indium::DynamicVK::beginDiagnosticCapture(VkInstance instance) { if (captureBegin) captureBegin(instance); } ++void Indium::DynamicVK::endDiagnosticCapture(bool presented) { if (captureEnd) captureEnd(presented); } + PFN_vkGetInstanceProcAddr Indium::DynamicVK::vkGetInstanceProcAddr = NULL; + PFN_vkCreateInstance Indium::DynamicVK::vkCreateInstance = NULL; + PFN_vkEnumerateInstanceLayerProperties Indium::DynamicVK::vkEnumerateInstanceLayerProperties = NULL; +@@ -32,6 +37,16 @@ static Indium::DynamicVK::DynamicFunctionBase* const eagerlyResolvedFunctions[] + #endif + + bool Indium::DynamicVK::init() { ++#ifdef DARLING ++ const char* capture = getenv("RCLOL_RENDERDOC_CAPTURE"); ++ if (capture && capture[0] == '1') { ++ void* bridge = _elfcalls->dlopen("@PROJECT_ROOT@/build/renderdoc/capture-bridge.so"); ++ if (bridge) { ++ captureBegin = reinterpret_cast(DLSYM(bridge, "riot_capture_begin")); ++ captureEnd = reinterpret_cast(DLSYM(bridge, "riot_capture_end")); ++ } ++ } ++#endif + #ifdef DARLING + libraryHandle = _elfcalls->dlopen("libvulkan.so.1"); + #else +diff --git a/src/indium/indium.cpp b/src/indium/indium.cpp +index e7c06fe..6e1f4b1 100644 +--- a/src/indium/indium.cpp ++++ b/src/indium/indium.cpp +@@ -10,15 +10,26 @@ + #include + + #include ++#include ++#include ++#include + + VkInstance Indium::globalInstance = VK_NULL_HANDLE; + + static VkDebugUtilsMessengerEXT debugMessenger = VK_NULL_HANDLE; + + static volatile int noOptOut; ++static FILE* validationLog = nullptr; ++static std::mutex validationLogMutex; + + static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback(VkDebugUtilsMessageSeverityFlagBitsEXT severity, VkDebugUtilsMessageTypeFlagsEXT type, const VkDebugUtilsMessengerCallbackDataEXT* callbackData, void* userData) { +- std::cerr << "Validation message: " << callbackData->pMessage << std::endl; ++ if (validationLog) { ++ std::lock_guard lock(validationLogMutex); ++ fprintf(validationLog, "pid=%d severity=%u id=%s: %s\n", getpid(), unsigned(severity), callbackData->pMessageIdName ? callbackData->pMessageIdName : "", callbackData->pMessage); ++ fflush(validationLog); ++ } else { ++ std::cerr << "Validation message: " << callbackData->pMessage << std::endl; ++ } + + if ((severity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) != 0) { + // convenient breakpoint +@@ -43,6 +54,11 @@ void Indium::init(const char** additionalExtensions, size_t additionalExtensionC + VK_KHR_SURFACE_EXTENSION_NAME, + }; + ++ if (access("/tmp/metal-validation", F_OK) == 0) { ++ enableValidation = true; ++ validationLog = fopen("/tmp/metal-validation.log", "a"); ++ } ++ + uint32_t count; + + std::vector layerProps; +@@ -97,13 +113,13 @@ void Indium::init(const char** additionalExtensions, size_t additionalExtensionC + + VkValidationFeaturesEXT valFeat {}; + std::vector valFeatures { +- VK_VALIDATION_FEATURE_ENABLE_DEBUG_PRINTF_EXT, ++ VK_VALIDATION_FEATURE_ENABLE_SYNCHRONIZATION_VALIDATION_EXT, + }; + valFeat.sType = VK_STRUCTURE_TYPE_VALIDATION_FEATURES_EXT; + valFeat.enabledValidationFeatureCount = valFeatures.size(); + valFeat.pEnabledValidationFeatures = valFeatures.data(); + +- if (enableValidation) { ++ if (enableValidation && access("/tmp/metal-validation-sync", F_OK) == 0) { + createInfo.pNext = &valFeat; + } + +@@ -133,7 +149,10 @@ void Indium::init(const char** additionalExtensions, size_t additionalExtensionC + initGlobalDeviceList(); + }; + ++namespace Indium { void finishCommandRetirement(); } ++ + void Indium::finit() { ++ finishCommandRetirement(); + finitGlobalDeviceList(); + + if (debugMessenger && DynamicVK::vkDestroyDebugUtilsMessengerEXT) { +@@ -146,4 +165,5 @@ void Indium::finit() { + DynamicVK::finit(); + + Iridium::finit(); ++ if (validationLog) { fclose(validationLog); validationLog = nullptr; } + }; +diff --git a/src/indium/library.cpp b/src/indium/library.cpp +index d56a213..66a6019 100644 +--- a/src/indium/library.cpp ++++ b/src/indium/library.cpp +@@ -1,7 +1,13 @@ ++#include ++#include + #include + #include + #include + #include ++#include ++#include ++#include ++#include + + Indium::Function::~Function() {}; + Indium::Library::~Library() {}; +@@ -26,6 +32,8 @@ Indium::PrivateLibrary::PrivateLibrary(std::shared_ptr device, co + _privateDevice(device), + _functionInfos(functionInfos) + { ++ GpuTiming::noteLibrary(this, data, dataLength); ++ OutlineChain::noteLibrary(this, data, dataLength); + VkShaderModuleCreateInfo createInfo {}; + createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; + createInfo.codeSize = dataLength; +@@ -36,6 +44,20 @@ Indium::PrivateLibrary::PrivateLibrary(std::shared_ptr device, co + abort(); + } + ++ // Diagnostic: with /tmp/metal-debug-spirv present, keep the first translated modules for offline inspection ++ if (Indium::spirvDebugEnabled()) { ++ static unsigned dumped = 0; ++ if (dumped++ < 8192) { ++ mkdir("/tmp/spv", 0777); ++ char path[96]; ++ snprintf(path, sizeof(path), "/tmp/spv/lib-%p.spv", (void*) this); ++ if (FILE* file = fopen(path, "wb")) { ++ fwrite(data, 1, dataLength, file); ++ fclose(file); ++ } ++ } ++ } ++ + // create sampler states for embedded samplers + for (auto& [name, funcInfo]: _functionInfos) { + for (const auto& embeddedSampler: funcInfo.embeddedSamplers) { +@@ -146,3 +168,5 @@ Indium::PrivateLibrary::~PrivateLibrary() { + std::shared_ptr Indium::PrivateLibrary::newFunction(const std::string& name) { + return std::make_shared(shared_from_this(), name, _functionInfos[name]); + }; ++ ++const std::vector& Indium::PrivateFunction::argumentInfo() const { return _functionInfo.arguments; } +diff --git a/src/indium/render-command-encoder.cpp b/src/indium/render-command-encoder.cpp +index b27ffe5..105dbc8 100644 +--- a/src/indium/render-command-encoder.cpp ++++ b/src/indium/render-command-encoder.cpp +@@ -1,3 +1,8 @@ ++#include ++#include ++#include ++#include ++#include + #include + #include + #include +@@ -12,35 +17,120 @@ + #include + #include + #include ++#include ++#include + #include ++#include ++namespace { ++// diagnostic (guest flags /tmp/metal-gpu-timing and, to start logging, /tmp/metal-seqlog): viewport, scissor, draw sizes and ++// small constant uploads, interleaved with the pass log in /tmp/indium-gpu-seq.log ++static bool seqLogActive() { ++ static unsigned calls = 0; ++ static bool active = false; ++ if ((calls++ & 255) == 0) active = access("/tmp/metal-seqlog", F_OK) == 0; ++ return active; ++} ++static void seqLog(const char* fmt, ...) __attribute__((format(printf, 1, 2))); ++static void seqLog(const char* fmt, ...) { ++ static FILE* file = fopen("/tmp/indium-gpu-seq.log", "a"); ++ if (!file || !seqLogActive()) return; ++ va_list args; ++ va_start(args, fmt); ++ vfprintf(file, fmt, args); ++ va_end(args); ++ fflush(file); ++} ++static void seqLogBytes(const char* stage, const void* bytes, size_t length, size_t index) { ++ if (length > 96 || !bytes) return; ++ char buf[512]; ++ int n = snprintf(buf, sizeof(buf), " %s%zu len=%zu f=", stage, index, length); ++ const float* f = static_cast(bytes); ++ for (size_t i = 0; i + 4 <= length && n < 480; i += 4) n += snprintf(buf + n, sizeof(buf) - n, "%.3g ", f[i / 4]); ++ seqLog("%s\n", buf); ++} ++} ++ ++ ++ ++namespace { ++ // Diagnostic (guest /tmp flag files, re-read twice a second): skip every draw of render passes by attachment size, ++ // to measure what each pass costs on the GPU. Not for play. ++ struct DrawSkip { const void* encoder = nullptr; bool skip = false; }; ++ thread_local DrawSkip drawSkip; ++ ++ // Diagnostic flag files, refreshed every 2048 passes (a clock_gettime per pass was a syscall in the render hot path). ++ bool shouldSkipPass(size_t width, size_t height, bool drawableFormat) { ++ static unsigned counter = 0; ++ static bool f1180, f512, f1024, f1080, f242, fui, fscene; ++ if ((counter++ & 2047) == 0) { ++ f1180 = access("/tmp/metal-skip-1180", F_OK) == 0; ++ f512 = access("/tmp/metal-skip-512", F_OK) == 0; ++ f1024 = access("/tmp/metal-skip-1024", F_OK) == 0; ++ f1080 = access("/tmp/metal-skip-1080", F_OK) == 0; ++ f242 = access("/tmp/metal-skip-242", F_OK) == 0; ++ fui = access("/tmp/metal-skip-ui", F_OK) == 0; ++ fscene = access("/tmp/metal-skip-scene", F_OK) == 0; ++ } ++ if (width == 1180 && height == 1184) return f1180; ++ if (width == 512 && height == 512) return f512; ++ if (width == 1024 && height == 1024) return f1024; ++ if (width == 1920 && height == 1080) return f1080 || (drawableFormat ? fui : fscene); ++ if (width == 242 && height == 242) return f242; ++ return false; ++ } ++} + + Indium::RenderCommandEncoder::~RenderCommandEncoder() {}; + +-Indium::PrivateRenderCommandEncoder::PrivateRenderCommandEncoder(std::shared_ptr commandBuffer, const RenderPassDescriptor& descriptor): ++Indium::PrivateRenderCommandEncoder::PrivateRenderCommandEncoder(std::shared_ptr commandBuffer, const RenderPassDescriptor& inputDescriptor): + _privateCommandBuffer(commandBuffer), +- _descriptor(descriptor), ++ _descriptor(inputDescriptor), + _privateDevice(commandBuffer->privateDevice()) + { ++ auto selectSubresource = [](RenderPassAttachmentDescriptor& attachment) { ++ if (!attachment.texture) return; ++ auto texture = std::dynamic_pointer_cast(attachment.texture); ++ if (texture->textureType() != TextureType::e2D && texture->textureType() != TextureType::e2DArray) return; ++ if (!attachment.level && !attachment.slice && texture->mipmapLevelCount() == 1 && texture->arrayLength() == 1) return; ++ if (attachment.level >= texture->mipmapLevelCount() || attachment.slice >= texture->arrayLength()) ++ throw std::invalid_argument("Render attachment subresource out of range"); ++ attachment.texture = texture->newTextureView(texture->pixelFormat(), TextureType::e2D, ++ Range{attachment.level, 1}, Range{attachment.slice, 1}); ++ attachment.level = 0; ++ attachment.slice = 0; ++ }; ++ for (auto& color : _descriptor.colorAttachments) selectSubresource(color); ++ if (_descriptor.depthAttachment) selectSubresource(*_descriptor.depthAttachment); ++ if (_descriptor.stencilAttachment) selectSubresource(*_descriptor.stencilAttachment); ++ const auto& descriptor = _descriptor; + auto buf = _privateCommandBuffer.lock(); + + auto vkDevice = _privateDevice->device(); + auto vkCmdBuf = buf->commandBuffer(); + +- VkDescriptorPoolCreateInfo poolCreateInfo {}; +- poolCreateInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; +- poolCreateInfo.poolSizeCount = poolSizes.size(); +- poolCreateInfo.pPoolSizes = poolSizes.data(); +- poolCreateInfo.maxSets = 64; // i guess +- +- if (DynamicVK::vkCreateDescriptorPool(vkDevice, &poolCreateInfo, nullptr, &_pool) != VK_SUCCESS) { +- // TODO +- abort(); ++ std::shared_ptr firstTexture; ++ for (const auto& color: descriptor.colorAttachments) { ++ if (color.texture) { firstTexture = color.texture; break; } + } +- +- auto firstTexture = descriptor.colorAttachments.front().texture; ++ if (!firstTexture) firstTexture = descriptor.depthAttachment ? descriptor.depthAttachment->texture : ++ (descriptor.stencilAttachment ? descriptor.stencilAttachment->texture : nullptr); ++ if (!firstTexture) throw std::invalid_argument("Render pass requires an attachment"); ++ drawSkip.encoder = this; ++ drawSkip.skip = shouldSkipPass(firstTexture->width(), firstTexture->height(), firstTexture->pixelFormat() == PixelFormat::RGBA8Unorm); ++ // Depth and stencil may be given as different texture objects (e.g. a view of a combined format) that share one image ++ if (descriptor.depthAttachment && descriptor.stencilAttachment && ++ descriptor.depthAttachment->texture != descriptor.stencilAttachment->texture) { ++ auto depthImage = std::dynamic_pointer_cast(descriptor.depthAttachment->texture); ++ auto stencilImage = std::dynamic_pointer_cast(descriptor.stencilAttachment->texture); ++ if (!depthImage || !stencilImage || depthImage->image() != stencilImage->image()) ++ throw std::invalid_argument("Separate depth and stencil textures are not supported"); ++ } ++ auto depthStencilTexture = descriptor.depthAttachment ? descriptor.depthAttachment->texture : ++ (descriptor.stencilAttachment ? descriptor.stencilAttachment->texture : nullptr); + std::vector clearValues; + + for (const auto& color: descriptor.colorAttachments) { ++ if (!color.texture) continue; + auto clearColor = color.clearColor; + VkClearValue clearValue {}; + clearValue.color.float32[0] = clearColor.red; +@@ -65,36 +155,59 @@ Indium::PrivateRenderCommandEncoder::PrivateRenderCommandEncoder(std::shared_ptr + std::vector dependencies; + + for (const auto& color: descriptor.colorAttachments) { ++ if (!color.texture) continue; + auto privateTexture = std::dynamic_pointer_cast(color.texture); + VkAttachmentDescription desc {}; + desc.format = pixelFormatToVkFormat(color.texture->pixelFormat()); + desc.samples = VK_SAMPLE_COUNT_1_BIT; // TODO: support multisampling +- desc.loadOp = loadActionToVkAttachmentLoadOp(color.loadAction, true); +- desc.storeOp = storeActionToVkAttachmentStoreOp(color.storeAction, true); ++ // Desktop Metal leaves an attachment's memory alone for DontCare, and the game depends on it. In Vulkan, ++ // DONT_CARE plus an UNDEFINED initial layout lets the driver throw the contents away (RADV re-initialises the ++ // image's compression metadata), so only an explicit Clear may discard; everything else is preserved. ++ const bool colorClear = color.loadAction == LoadAction::Clear; ++ desc.loadOp = colorClear ? VK_ATTACHMENT_LOAD_OP_CLEAR : VK_ATTACHMENT_LOAD_OP_LOAD; ++ // always STORE: free on this kind of GPU, and DontCare/Unknown/resolve actions must not discard either ++ desc.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + desc.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + desc.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; +- desc.initialLayout = (color.loadAction == LoadAction::Load) ? privateTexture->imageLayout() : VK_IMAGE_LAYOUT_UNDEFINED; ++ desc.initialLayout = colorClear ? VK_IMAGE_LAYOUT_UNDEFINED : privateTexture->imageLayout(); + desc.finalLayout = privateTexture->imageLayout(); + renderPassAttachments.push_back(desc); + } + +- if (descriptor.depthAttachment) { +- auto privateTexture = std::dynamic_pointer_cast(descriptor.depthAttachment->texture); +- VkAttachmentDescription desc {}; +- desc.format = pixelFormatToVkFormat(privateTexture->pixelFormat()); +- desc.samples = VK_SAMPLE_COUNT_1_BIT; +- desc.loadOp = loadActionToVkAttachmentLoadOp(descriptor.depthAttachment->loadAction, false); +- desc.storeOp = storeActionToVkAttachmentStoreOp(descriptor.depthAttachment->storeAction, false); +- desc.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; +- desc.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; +- desc.initialLayout = (descriptor.depthAttachment->loadAction == LoadAction::Load) ? privateTexture->imageLayout() : VK_IMAGE_LAYOUT_UNDEFINED; +- desc.finalLayout = privateTexture->imageLayout(); +- renderPassAttachments.push_back(desc); +- } + +- if (descriptor.stencilAttachment) { +- throw std::runtime_error("TODO: support stencil attachments"); +- } ++ if (depthStencilTexture) { ++ auto texture = std::dynamic_pointer_cast(depthStencilTexture); ++ VkAttachmentDescription desc {}; ++ desc.format = pixelFormatToVkFormat(texture->pixelFormat()); ++ desc.samples = VK_SAMPLE_COUNT_1_BIT; ++ // Same rule as for colour (see above): the game binds the scene depth buffer to a run of effect passes with ++ // DontCare/DontCare and then resumes the scene with Load. With UNDEFINED/DONT_CARE the depth buffer came back ++ // as garbage in tiles, which is what punched black blocks into everything drawn afterwards. An aspect the ++ // pass does not attach is preserved too. Default for depth and stencil is Clear. ++ auto isClear = [](const auto& attachment) { ++ return attachment && (attachment->loadAction == LoadAction::Clear || attachment->loadAction == LoadAction::Default); ++ }; ++ const bool depthClear = isClear(descriptor.depthAttachment); ++ const bool stencilClear = isClear(descriptor.stencilAttachment); ++ desc.loadOp = depthClear ? VK_ATTACHMENT_LOAD_OP_CLEAR : VK_ATTACHMENT_LOAD_OP_LOAD; ++ desc.storeOp = VK_ATTACHMENT_STORE_OP_STORE; ++ desc.stencilLoadOp = stencilClear ? VK_ATTACHMENT_LOAD_OP_CLEAR : VK_ATTACHMENT_LOAD_OP_LOAD; ++ desc.stencilStoreOp = VK_ATTACHMENT_STORE_OP_STORE; ++ desc.initialLayout = (depthClear && stencilClear) ? VK_IMAGE_LAYOUT_UNDEFINED : texture->imageLayout(); ++ desc.finalLayout = texture->imageLayout(); ++ renderPassAttachments.push_back(desc); ++ _readWriteTextures.push_back(depthStencilTexture); ++ _depthRoot = texture; ++ while (auto parent = std::dynamic_pointer_cast(_depthRoot->parentTexture())) _depthRoot = parent; ++ } ++ // League uses fragment-to-fragment barriers for texture/render-target scope. ++ VkSubpassDependency dependency {}; ++ dependency.srcSubpass = dependency.dstSubpass = 0; ++ dependency.srcStageMask = dependency.dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; ++ dependency.srcAccessMask = dependency.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | ++ VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; ++ dependency.dependencyFlags = VK_DEPENDENCY_BY_REGION_BIT; ++ dependencies.push_back(dependency); + + std::vector colorAttachments; + VkAttachmentReference depthStencilAttachment {}; +@@ -102,13 +215,15 @@ Indium::PrivateRenderCommandEncoder::PrivateRenderCommandEncoder(std::shared_ptr + size_t index = 0; + for (const auto& color: descriptor.colorAttachments) { + VkAttachmentReference ref {}; +- ref.attachment = index; +- ref.layout = VK_IMAGE_LAYOUT_GENERAL; ++ ref.attachment = color.texture ? index : VK_ATTACHMENT_UNUSED; ++ // textures that rest outside GENERAL (see ConcreteTexture::imageLayout) are written as colour attachments ++ ref.layout = (color.texture && std::dynamic_pointer_cast(color.texture)->imageLayout() != VK_IMAGE_LAYOUT_GENERAL) ? ++ VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL : VK_IMAGE_LAYOUT_GENERAL; + colorAttachments.push_back(ref); +- ++index; ++ if (color.texture) ++index; + } + +- if (descriptor.depthAttachment) { ++ if (depthStencilTexture) { + depthStencilAttachment.attachment = index; + depthStencilAttachment.layout = VK_IMAGE_LAYOUT_GENERAL; + ++index; +@@ -129,19 +244,19 @@ Indium::PrivateRenderCommandEncoder::PrivateRenderCommandEncoder(std::shared_ptr + renderPassInfo.dependencyCount = dependencies.size(); + renderPassInfo.pDependencies = dependencies.data(); + +- if (DynamicVK::vkCreateRenderPass(vkDevice, &renderPassInfo, nullptr, &_renderPass) != VK_SUCCESS) { +- // TODO +- abort(); +- } ++ auto renderPassLease = _privateDevice->acquireRenderPass(renderPassInfo); ++ _renderPass = renderPassLease.first; ++ _renderPassDeviceOwned = renderPassLease.second; + + std::vector framebufferAttachments; + + for (const auto& colorAttachment: descriptor.colorAttachments) { ++ if (!colorAttachment.texture) continue; + framebufferAttachments.push_back(std::dynamic_pointer_cast(colorAttachment.texture)->imageView()); + } + +- if (descriptor.depthAttachment) { +- framebufferAttachments.push_back(std::dynamic_pointer_cast(descriptor.depthAttachment->texture)->imageView()); ++ if (depthStencilTexture) { ++ framebufferAttachments.push_back(std::dynamic_pointer_cast(depthStencilTexture)->imageView()); + } + + VkFramebufferCreateInfo framebufferInfo {}; +@@ -165,7 +280,51 @@ Indium::PrivateRenderCommandEncoder::PrivateRenderCommandEncoder(std::shared_ptr + renderPassBeginInfo.renderArea.extent.height = firstTexture->height(); + renderPassBeginInfo.clearValueCount = clearValues.size(); + renderPassBeginInfo.pClearValues = clearValues.data(); +- DynamicVK::vkCmdBeginRenderPass(vkCmdBuf, &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE); ++ if (GpuTiming::enabled()) { ++ std::string key = std::to_string(firstTexture->width()) + "x" + std::to_string(firstTexture->height()); ++ int n = 0; ++ for (const auto& color: descriptor.colorAttachments) { ++ if (!color.texture) continue; ++ key += " c" + std::to_string(n++) + "=" + std::to_string((int) color.texture->pixelFormat()) + "/" + std::to_string((int) color.loadAction) + "/" + std::to_string((int) color.storeAction); ++ } ++ if (depthStencilTexture) { ++ key += " d=" + std::to_string((int) depthStencilTexture->pixelFormat()); ++ if (descriptor.depthAttachment) key += "/" + std::to_string((int) descriptor.depthAttachment->loadAction) + "/" + std::to_string((int) descriptor.depthAttachment->storeAction); ++ } ++ GpuTiming::noteTarget(key); ++ if (seqLogActive()) { ++ std::string line = key; ++ for (const auto& color: descriptor.colorAttachments) { ++ char tmp[40]; ++ if (color.texture) { snprintf(tmp, sizeof(tmp), " ct=%p", (void*) color.texture.get()); line += tmp; } ++ } ++ if (depthStencilTexture) { char tmp[40]; snprintf(tmp, sizeof(tmp), " dt=%p", (void*) depthStencilTexture.get()); line += tmp; } ++ seqLog("%s\n", line.c_str()); ++ } ++ } ++ { ++ // A pass without a clear that never draws (the game's skipped outline passes, empty passes) does nothing in Metal ++ // either, but a begun-and-ended Vulkan render pass costs a pipeline flush. Begin it on the first draw instead. ++ // Live switch: guest file /tmp/metal-no-lazy-pass turns it off (re-read every 256 passes). ++ static std::atomic lazySetting {true}; ++ static std::atomic lazyChecks {0}; ++ if ((lazyChecks.fetch_add(1, std::memory_order_relaxed) & 255) == 0) ++ lazySetting.store(access("/tmp/metal-no-lazy-pass", F_OK) != 0, std::memory_order_relaxed); ++ const bool lazyAllowed = lazySetting.load(std::memory_order_relaxed); ++ bool hasClear = false; ++ for (const auto& attachment: renderPassAttachments) { ++ if (attachment.loadOp == VK_ATTACHMENT_LOAD_OP_CLEAR || attachment.stencilLoadOp == VK_ATTACHMENT_LOAD_OP_CLEAR) hasClear = true; ++ } ++ if (lazyAllowed && !hasClear) { ++ _lazyBeginInfo = renderPassBeginInfo; ++ _lazyBeginInfo.clearValueCount = 0; ++ _lazyBeginInfo.pClearValues = nullptr; ++ _passBegun = false; ++ } else { ++ GpuTiming::beginPass(vkDevice, vkCmdBuf, firstTexture->width(), firstTexture->height()); ++ DynamicVK::vkCmdBeginRenderPass(vkCmdBuf, &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE); ++ } ++ } + + // set default values + +@@ -190,16 +349,21 @@ Indium::PrivateRenderCommandEncoder::~PrivateRenderCommandEncoder() { + if (_framebuffer) { + DynamicVK::vkDestroyFramebuffer(_privateDevice->device(), _framebuffer, nullptr); + } +- if (_renderPass) { ++ if (_renderPass && !_renderPassDeviceOwned) { + DynamicVK::vkDestroyRenderPass(_privateDevice->device(), _renderPass, nullptr); + } +- DynamicVK::vkDestroyDescriptorPool(_privateDevice->device(), _pool, 0); ++ if (_pool) _privateDevice->releaseDescriptorPool(_pool); ++ for (auto pool : _retiredPools) _privateDevice->releaseDescriptorPool(pool); + }; + + void Indium::PrivateRenderCommandEncoder::setRenderPipelineState(std::shared_ptr renderPipelineState) { +- auto buf = _privateCommandBuffer.lock(); +- _privatePSO = std::dynamic_pointer_cast(renderPipelineState); +- _privatePSO->recreatePipeline(_renderPass, false); ++ if (_privatePSO == renderPipelineState) return; ++ // Keep each pipeline once, including all layouts and immutable samplers ++ // referenced by earlier draws, until this encoder retires. ++ auto [entry, inserted] = _pipelineOwners.acquire(renderPipelineState); ++ if (!entry || !entry->value) throw std::invalid_argument("Invalid render pipeline state"); ++ _privatePSO = std::static_pointer_cast(entry->owner); ++ _privatePSO->recreatePipeline(_renderPass, false); + }; + + void Indium::PrivateRenderCommandEncoder::setFrontFacingWinding(Winding frontFaceWinding) { +@@ -225,6 +389,7 @@ void Indium::PrivateRenderCommandEncoder::setDepthClipMode(DepthClipMode depthCl + } + }; + ++ + void Indium::PrivateRenderCommandEncoder::setViewport(const Viewport& viewport) { + setViewports(&viewport, 1); + }; +@@ -239,7 +404,7 @@ void Indium::PrivateRenderCommandEncoder::setViewports(const Viewport* viewports + // so we use a Vulkan 1.1 feature here and just flip the Y-axis of the viewport + // (with a corresponding change in the origin). + vkViewport.x = viewport.originX; +- vkViewport.y = viewport.height - viewport.originY; ++ vkViewport.y = viewport.height + viewport.originY; + vkViewport.width = viewport.width; + vkViewport.height = -viewport.height; + // the documentation for setViewport and setViewports states that znear and zfar must be between +@@ -251,6 +416,7 @@ void Indium::PrivateRenderCommandEncoder::setViewports(const Viewport* viewports + vkViewport.maxDepth = std::clamp(viewport.zfar, 0., 1.); + tmp.push_back(vkViewport); + } ++ if (GpuTiming::enabled() && !tmp.empty()) seqLog(" VP %.0f %.0f %.0f %.0f\n", tmp[0].x, tmp[0].y, tmp[0].width, tmp[0].height); + DynamicVK::vkCmdSetViewportWithCount(buf->commandBuffer(), tmp.size(), tmp.data()); + }; + +@@ -274,6 +440,7 @@ void Indium::PrivateRenderCommandEncoder::setScissorRects(const ScissorRect* sci + vkRect.extent.height = scissorRect.height; + tmp.push_back(vkRect); + } ++ if (GpuTiming::enabled() && !tmp.empty()) seqLog(" SC %d %d %u %u\n", tmp[0].offset.x, tmp[0].offset.y, tmp[0].extent.width, tmp[0].extent.height); + DynamicVK::vkCmdSetScissorWithCount(buf->commandBuffer(), tmp.size(), tmp.data()); + }; + +@@ -287,43 +454,217 @@ void Indium::PrivateRenderCommandEncoder::setBlendColor(float red, float green, + DynamicVK::vkCmdSetBlendConstants(buf->commandBuffer(), tmp); + }; + +-void Indium::PrivateRenderCommandEncoder::updateBindings() { +- // TODO: better descriptor set resource management +- +- auto buf = _privateCommandBuffer.lock(); +- +- std::array descriptorSets = createDescriptorSets(_privatePSO->descriptorSetLayouts().layouts, _pool, _privateDevice, { _functionResources[0], _functionResources[1] }, { _privatePSO->vertexFunctionInfo(), _privatePSO->fragmentFunctionInfo() }, _keepAliveBuffers); ++void Indium::PrivateRenderCommandEncoder::updateBindings(VkCommandBuffer commandBuffer) { ++ ++ const std::array, 2> infos { ++ _privatePSO->vertexFunctionInfo(), _privatePSO->fragmentFunctionInfo() ++ }; ++ for (size_t stage = 0; stage < 2; ++stage) { ++ const auto layout = _privatePSO->descriptorSetLayouts().layouts[stage]; ++ const auto& current = _functionResources[stage]; ++ const auto& previous = _boundResourceKeys[stage]; ++ bool unchanged = _boundDescriptorSets[stage] && _boundLayouts[stage] == layout; ++ // No setter changed the stage, and the shader binding plan is identical. ++ // Checking the plan as well as the Vulkan layout matters: compatible ++ // layouts can still refer to different Metal resource indices. ++ if (unchanged && _boundFunctionInfos[stage] == &infos[stage].get() && ++ _boundResourceRevisions[stage] == current.revision) { ++ ++_descriptorStageReuses; ++ continue; ++ } ++ // Buffers-only change (the usual draw: a new uniform offset, nothing else). Same layout, same binding plan and ++ // no texture or sampler setter since the set was bound: only the address table is rebuilt, without comparing ++ // every binding. The per-binding keys are not maintained here, so the general path below must not trust them. ++ if (unchanged && _boundFunctionInfos[stage] == &infos[stage].get() && ++ _boundImageRevisions[stage] == current.imageRevision && _boundStageHasBuffers[stage]) { ++ const auto previousTable = _bufferAddressTables[stage]; ++ auto& table = _bufferAddressTables[stage]; ++ table = makeBufferAddressTable(current, infos[stage], &_resolvedResources[stage], _privateDevice, _keepAliveBuffers, _addressPool, &previousTable); ++ if (previousTable.buffer != table.buffer || previousTable.range != table.range) { ++ if (!_pool) _pool = newDescriptorPool(_privateDevice); ++ std::array layouts {layout}; ++ _boundDescriptorSets[stage] = createDescriptorSets<1>(layouts, _pool, _retiredPools, _privateDevice, ++ {std::cref(current)}, {infos[stage]}, _keepAliveBuffers, _addressPool, {&_resolvedResources[stage]}, {&table})[0]; ++ } ++ _boundKeysStale[stage] = true; ++ _boundResourceRevisions[stage] = current.revision; ++ ++_descriptorStageUpdates; ++ continue; ++ } ++ bool imageBindingsUnchanged = unchanged; ++ if (_boundKeysStale[stage]) unchanged = false; // buffer keys were skipped by the fast path ++ for (const auto& binding : infos[stage].get().bindings) { ++ const auto index = binding.index; ++ if (binding.type == Iridium::BindingType::Texture) ++ imageBindingsUnchanged &= (index < current.textures.size() ? current.textures[index].get() : nullptr) == ++ (index < previous.textures.size() ? previous.textures[index] : nullptr); ++ else if (binding.type == Iridium::BindingType::Sampler && index != SIZE_MAX) ++ imageBindingsUnchanged &= (index < current.samplers.size() ? current.samplers[index].get() : nullptr) == ++ (index < previous.samplers.size() ? previous.samplers[index] : nullptr); ++ } ++ ++ for (const auto& binding: infos[stage].get().bindings) { ++ if (!unchanged) break; ++ const auto index = binding.index; ++ if (binding.type == Iridium::BindingType::Buffer) { ++ const auto* a = index < current.buffers.size() ? ¤t.buffers[index] : nullptr; ++ const auto* b = index < previous.buffers.size() ? &previous.buffers[index] : nullptr; ++ unchanged = (a ? a->first.get() : nullptr) == (b ? b->first : nullptr) && ++ (a ? a->second : 0) == (b ? b->second : 0); ++ } else if (binding.type == Iridium::BindingType::Texture) { ++ unchanged = (index < current.textures.size() ? current.textures[index].get() : nullptr) == ++ (index < previous.textures.size() ? previous.textures[index] : nullptr); ++ } else if (binding.type == Iridium::BindingType::Sampler && index != SIZE_MAX) { ++ unchanged = (index < current.samplers.size() ? current.samplers[index].get() : nullptr) == ++ (index < previous.samplers.size() ? previous.samplers[index] : nullptr); ++ } ++ } ++ if (!unchanged) { ++ const bool hasBuffers = std::any_of(infos[stage].get().bindings.begin(), infos[stage].get().bindings.end(), ++ [](const auto& binding) { return binding.type == Iridium::BindingType::Buffer; }); ++ _boundStageHasBuffers[stage] = hasBuffers; ++ _boundKeysStale[stage] = false; ++ const auto previousTable = _bufferAddressTables[stage]; ++ auto& table = _bufferAddressTables[stage]; ++ table = hasBuffers ? makeBufferAddressTable(current, infos[stage], &_resolvedResources[stage], _privateDevice, _keepAliveBuffers, _addressPool, &previousTable) : BufferAddressTable{}; ++ // Address values occupy immutable storage. Only the dynamic offset ++ // changes while images, samplers, layout, and backing chunk match. ++ if (!imageBindingsUnchanged || previousTable.buffer != table.buffer || previousTable.range != table.range) { ++ // Clear-only passes never allocate descriptors. Lease a pool ++ // only when a draw first needs a descriptor set. ++ if (!_pool) _pool = newDescriptorPool(_privateDevice); ++ std::array layouts {layout}; ++ _boundDescriptorSets[stage] = createDescriptorSets<1>(layouts, _pool, _retiredPools, _privateDevice, ++ {std::cref(current)}, {infos[stage]}, _keepAliveBuffers, _addressPool, {&_resolvedResources[stage]}, {&table})[0]; ++ _boundLayouts[stage] = layout; ++ } ++ auto& keys = _boundResourceKeys[stage]; ++ for (const auto& binding : infos[stage].get().bindings) { ++ const auto index = binding.index; ++ if (binding.type == Iridium::BindingType::Buffer) { ++ growToIndex(keys.buffers, index); ++ keys.buffers[index] = index < current.buffers.size() ? ++ std::make_pair(current.buffers[index].first.get(), current.buffers[index].second) : std::make_pair((Buffer*)nullptr, size_t(0)); ++ } else if (binding.type == Iridium::BindingType::Texture) { ++ growToIndex(keys.textures, index); ++ keys.textures[index] = index < current.textures.size() ? current.textures[index].get() : nullptr; ++ } else if (binding.type == Iridium::BindingType::Sampler && index != SIZE_MAX) { ++ growToIndex(keys.samplers, index); ++ keys.samplers[index] = index < current.samplers.size() ? current.samplers[index].get() : nullptr; ++ } ++ } ++ // Buffer and texture resolution caches already retain these owners. ++ // Samplers are retained when their binding changes in resolveSampler(). ++ ++_descriptorStageUpdates; ++ } else { ++ ++_descriptorStageReuses; ++ } ++ _boundResourceRevisions[stage] = current.revision; ++ _boundFunctionInfos[stage] = &infos[stage].get(); ++ _boundImageRevisions[stage] = current.imageRevision; ++ } ++ const auto& descriptorSets = _boundDescriptorSets; ++ ++ if (_depthRoot) { ++ bool feedback = false; ++ for (const auto& texture : _resolvedResources[1].textures) { ++ if (texture.depthFeedback) { feedback = true; break; } ++ } ++ if (feedback && _depthWriteRequested && !_depthWriteSuppressed) { ++ DynamicVK::vkCmdSetDepthWriteEnable(commandBuffer, VK_FALSE); ++ _depthWriteSuppressed = true; ++ static unsigned logged = 0; ++ if (logged < 5 && ++logged) { ++ if (FILE* file = fopen("/tmp/metal-trace.log", "a")) { ++ fprintf(file, "depth-feedback: suppressed depth write on a draw that samples its own depth attachment\n"); ++ fclose(file); ++ } ++ } ++ } else if (!feedback && _depthWriteSuppressed) { ++ DynamicVK::vkCmdSetDepthWriteEnable(commandBuffer, _depthWriteRequested ? VK_TRUE : VK_FALSE); ++ _depthWriteSuppressed = false; ++ } ++ } + +- DynamicVK::vkCmdBindDescriptorSets(buf->commandBuffer(), VK_PIPELINE_BIND_POINT_GRAPHICS, _privatePSO->pipelineLayout(), 0, descriptorSets.size(), descriptorSets.data(), 0, nullptr); ++ const auto pipelineLayout = _privatePSO->pipelineLayout(); ++ std::array dynamicOffsets {}; ++ uint32_t dynamicOffsetCount = 0; ++ for (const auto& table : _bufferAddressTables) ++ if (table.buffer) dynamicOffsets[dynamicOffsetCount++] = table.offset; ++ if (_lastDescriptorPipelineLayout != pipelineLayout || _lastDrawDescriptorSets != descriptorSets || _lastDrawDynamicOffsets != dynamicOffsets) { ++ DynamicVK::vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, descriptorSets.size(), descriptorSets.data(), dynamicOffsetCount, dynamicOffsets.data()); ++ _lastDrawDynamicOffsets = dynamicOffsets; ++ _lastDescriptorPipelineLayout = pipelineLayout; ++ _lastDrawDescriptorSets = descriptorSets; ++ } + + const auto& vertexInputBindings = _privatePSO->vertexInputBindings(); +- if (vertexInputBindings.size() > 0) { +- std::vector buffers; +- std::vector offsets; +- +- buffers.resize(vertexInputBindings.size()); +- offsets.resize(vertexInputBindings.size()); ++ if (!vertexInputBindings.empty() && ++ (_lastVertexResourceRevision != _functionResources[0].revision || _lastVertexBindingPSO != _privatePSO.get())) { ++ auto& buffers = _lastVertexBuffers; ++ auto& offsets = _lastVertexOffsets; ++ bool vertexBindingsChanged = buffers.size() < vertexInputBindings.size(); ++ buffers.resize(vertexInputBindings.size()); ++ offsets.resize(vertexInputBindings.size()); + + for (size_t vulkanIndex = 0; vulkanIndex < vertexInputBindings.size(); ++vulkanIndex) { + const auto& metalIndex = vertexInputBindings[vulkanIndex]; + +- if (metalIndex >= _functionResources[0].buffers.size()) { +- // technically, this requires the `nullDescriptor` feature, but we should never run into this case anyways. ++ PrivateBuffer* privateBuffer = nullptr; ++ size_t offset = 0; ++ if (metalIndex < _functionResources[0].buffers.size()) { ++ const auto& [buffer, bufferOffset] = _functionResources[0].buffers[metalIndex]; ++ privateBuffer = metalIndex < _resolvedResources[0].buffers.size() ? ++ _resolvedResources[0].buffers[metalIndex].value : dynamic_cast(buffer.get()); ++ offset = bufferOffset; ++ } ++ const auto previousBuffer = buffers[vulkanIndex]; ++ const auto previousOffset = offsets[vulkanIndex]; ++ if (!privateBuffer) { ++ // the pipeline fetches vertices from a Metal buffer index the app never bound. Device creation enables ++ // nullDescriptor so VK_NULL_HANDLE is legal here (reads return zero); before that this faulted the GPU at address 0. ++ static unsigned unboundLogged = 0; ++ if (unboundLogged < 20) { ++ ++unboundLogged; ++ if (FILE* file = fopen("/tmp/metal-trace.log", "a")) { ++ fprintf(file, "unbound-vertex-buffer: vk%zu <- mtl%u (resources.buffers.size=%zu)\n", vulkanIndex, (unsigned) metalIndex, _functionResources[0].buffers.size()); ++ fclose(file); ++ } ++ } + buffers[vulkanIndex] = VK_NULL_HANDLE; + offsets[vulkanIndex] = 0; + } else { +- auto [buffer, offset] = _functionResources[0].buffers[metalIndex]; +- auto privateBuffer = std::dynamic_pointer_cast(buffer); + buffers[vulkanIndex] = privateBuffer->buffer(); + offsets[vulkanIndex] = offset; ++ // The unique resource owner above retains this attribute buffer. + } ++ vertexBindingsChanged |= previousBuffer != buffers[vulkanIndex] || previousOffset != offsets[vulkanIndex]; + } + +- DynamicVK::vkCmdBindVertexBuffers(buf->commandBuffer(), 0, vertexInputBindings.size(), buffers.data(), offsets.data()); ++ static unsigned bindingsLogged = 0; ++ if (bindingsLogged < 40 && Indium::spirvDebugEnabled()) { ++ ++bindingsLogged; ++ if (FILE* file = fopen("/tmp/metal-trace.log", "a")) { ++ fprintf(file, "vertex-bindings (resources.buffers.size=%zu):", _functionResources[0].buffers.size()); ++ for (size_t i = 0; i < vertexInputBindings.size(); ++i) { ++ fprintf(file, " [vk%zu<-mtl%u %p+%llu]", i, (unsigned) vertexInputBindings[i], (void*) buffers[i], (unsigned long long) offsets[i]); ++ } ++ fprintf(file, "\n"); ++ fclose(file); ++ } ++ } ++ if (vertexBindingsChanged) ++ DynamicVK::vkCmdBindVertexBuffers(commandBuffer, 0, vertexInputBindings.size(), buffers.data(), offsets.data()); ++ _lastVertexResourceRevision = _functionResources[0].revision; ++ _lastVertexBindingPSO = _privatePSO.get(); + } + }; + + void Indium::PrivateRenderCommandEncoder::drawPrimitives(PrimitiveType primitiveType, size_t vertexStart, size_t vertexCount, size_t instanceCount, size_t baseInstance) { ++ if (drawSkip.encoder == this && drawSkip.skip) return; ++ if (OutlineChain::shouldSkipDraw(_privatePSO.get(), vertexCount)) return; ++ beginPassIfNeeded(); ++ GpuTiming::countDraw(); + auto buf = _privateCommandBuffer.lock(); + + // bind the pipeline with the right topology class for this primitive +@@ -343,20 +684,26 @@ void Indium::PrivateRenderCommandEncoder::drawPrimitives(PrimitiveType primitive + default: + throw BadEnumValue(); + } +- DynamicVK::vkCmdBindPipeline(buf->commandBuffer(), VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); +- +- DynamicVK::vkCmdSetPrimitiveTopology(buf->commandBuffer(), primitiveTypeToVkPrimitiveTopology(primitiveType)); ++ if (_lastDrawPipeline != pipeline) { ++ DynamicVK::vkCmdBindPipeline(buf->commandBuffer(), VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); ++ _lastDrawPipeline = pipeline; ++ } ++ const auto topology = primitiveTypeToVkPrimitiveTopology(primitiveType); ++ if (_lastDrawTopology != topology) { ++ DynamicVK::vkCmdSetPrimitiveTopology(buf->commandBuffer(), topology); ++ _lastDrawTopology = topology; ++ } + + // TODO: avoid re-binding descriptors on every draw. +- updateBindings(); ++ updateBindings(buf->commandBuffer()); + ++ if (GpuTiming::enabled()) seqLog(" DRAW %p v=%zu i=%zu\n", (void*) _privatePSO.get(), vertexCount, instanceCount); + DynamicVK::vkCmdDraw(buf->commandBuffer(), vertexCount, instanceCount, vertexStart, baseInstance); ++ GpuTiming::afterDraw(buf->commandBuffer(), (uint64_t)(uintptr_t) _privatePSO.get()); + + // when we emit a draw call, we save the current function resources so that they stay alive until the command buffer is done. + // TODO: do this more efficiently by essentially doing COW: after a draw call, we only save additional references to resources + // if someone tries to overwrite them. +- _savedFunctionResources.push_back(_functionResources[0]); +- _savedFunctionResources.push_back(_functionResources[1]); + }; + + void Indium::PrivateRenderCommandEncoder::drawPrimitives(PrimitiveType primitiveType, size_t vertexStart, size_t vertexCount, size_t instanceCount) { +@@ -368,22 +715,42 @@ void Indium::PrivateRenderCommandEncoder::drawPrimitives(PrimitiveType primitive + }; + + void Indium::PrivateRenderCommandEncoder::setVertexBytes(const void* bytes, size_t length, size_t index) { ++ if (GpuTiming::enabled()) seqLogBytes("VB", bytes, length, index); + auto cmdBuf = _privateCommandBuffer.lock(); + _functionResources[0].setBytes(cmdBuf->device(), bytes, length, index); ++ resolveBuffer(0, index); + }; + ++void Indium::PrivateRenderCommandEncoder::beginPassIfNeeded() { ++ if (_passBegun) return; ++ _passBegun = true; ++ auto buf = _privateCommandBuffer.lock(); ++ GpuTiming::beginPass(_privateDevice->device(), buf->commandBuffer(), _lazyBeginInfo.renderArea.extent.width, _lazyBeginInfo.renderArea.extent.height); ++ DynamicVK::vkCmdBeginRenderPass(buf->commandBuffer(), &_lazyBeginInfo, VK_SUBPASS_CONTENTS_INLINE); ++} ++ + void Indium::PrivateRenderCommandEncoder::endEncoding() { + auto buf = _privateCommandBuffer.lock(); +- DynamicVK::vkCmdEndRenderPass(buf->commandBuffer()); ++ if (_passBegun) { ++ DynamicVK::vkCmdEndRenderPass(buf->commandBuffer()); ++ GpuTiming::endPass(buf->commandBuffer()); ++ } ++ finishTrackedEncoder(buf->commandBuffer()); ++ auto updates = _descriptorStageUpdates; ++ auto reuses = _descriptorStageReuses; ++ if (updates) Indium::count("descriptorStageUpdate", updates); ++ if (reuses) Indium::count("descriptorStageReuse", reuses); + }; + + void Indium::PrivateRenderCommandEncoder::setVertexBuffer(std::shared_ptr buffer, size_t offset, size_t index) { + _functionResources[0].setBuffer(buffer, offset, index); ++ resolveBuffer(0, index); + }; + + void Indium::PrivateRenderCommandEncoder::setVertexBuffers(const std::vector>& buffers, const std::vector& offsets, Range range) { + for (size_t i = 0; i < range.length; ++i) { +- setVertexBuffer(buffers[i], offsets[i], range.start + i); ++ _functionResources[0].setBuffer(buffers[i], offsets[i], range.start + i); ++ resolveBuffer(0, range.start + i); + } + }; + +@@ -393,10 +760,12 @@ void Indium::PrivateRenderCommandEncoder::setVertexBufferOffset(size_t offset, s + + void Indium::PrivateRenderCommandEncoder::setVertexSamplerState(std::shared_ptr state, size_t index) { + _functionResources[0].setSamplerState(state, std::nullopt, index); ++ resolveSampler(0, index); + }; + + void Indium::PrivateRenderCommandEncoder::setVertexSamplerState(std::shared_ptr state, float lodMinClamp, float lodMaxClamp, size_t index) { + _functionResources[0].setSamplerState(state, std::make_pair(lodMinClamp, lodMaxClamp), index); ++ resolveSampler(0, index); + }; + + void Indium::PrivateRenderCommandEncoder::setVertexSamplerStates(const std::vector>& states, Range range) { +@@ -413,26 +782,32 @@ void Indium::PrivateRenderCommandEncoder::setVertexSamplerStates(const std::vect + + void Indium::PrivateRenderCommandEncoder::setVertexTexture(std::shared_ptr texture, size_t index) { + _functionResources[0].setTexture(texture, index); ++ resolveTexture(0, index); + }; + + void Indium::PrivateRenderCommandEncoder::setVertexTextures(const std::vector>& textures, Range range) { + for (size_t i = 0; i < range.length; ++i) { +- setVertexTexture(textures[i], range.start + i); ++ _functionResources[0].setTexture(textures[i], range.start + i); ++ resolveTexture(0, range.start + i); + } + }; + + void Indium::PrivateRenderCommandEncoder::setFragmentBytes(const void* bytes, size_t length, size_t index) { ++ if (GpuTiming::enabled()) seqLogBytes("FB", bytes, length, index); + auto cmdBuf = _privateCommandBuffer.lock(); + _functionResources[1].setBytes(cmdBuf->device(), bytes, length, index); ++ resolveBuffer(1, index); + }; + + void Indium::PrivateRenderCommandEncoder::setFragmentBuffer(std::shared_ptr buffer, size_t offset, size_t index) { + _functionResources[1].setBuffer(buffer, offset, index); ++ resolveBuffer(1, index); + }; + + void Indium::PrivateRenderCommandEncoder::setFragmentBuffers(const std::vector>& buffers, const std::vector& offsets, Range range) { + for (size_t i = 0; i < range.length; ++i) { +- setFragmentBuffer(buffers[i], offsets[i], range.start + i); ++ _functionResources[1].setBuffer(buffers[i], offsets[i], range.start + i); ++ resolveBuffer(1, range.start + i); + } + }; + +@@ -442,10 +817,12 @@ void Indium::PrivateRenderCommandEncoder::setFragmentBufferOffset(size_t offset, + + void Indium::PrivateRenderCommandEncoder::setFragmentSamplerState(std::shared_ptr state, size_t index) { + _functionResources[1].setSamplerState(state, std::nullopt, index); ++ resolveSampler(1, index); + }; + + void Indium::PrivateRenderCommandEncoder::setFragmentSamplerState(std::shared_ptr state, float lodMinClamp, float lodMaxClamp, size_t index) { + _functionResources[1].setSamplerState(state, std::make_pair(lodMinClamp, lodMaxClamp), index); ++ resolveSampler(1, index); + }; + + void Indium::PrivateRenderCommandEncoder::setFragmentSamplerStates(const std::vector>& states, Range range) { +@@ -462,15 +839,20 @@ void Indium::PrivateRenderCommandEncoder::setFragmentSamplerStates(const std::ve + + void Indium::PrivateRenderCommandEncoder::setFragmentTexture(std::shared_ptr texture, size_t index) { + _functionResources[1].setTexture(texture, index); ++ resolveTexture(1, index); + }; + + void Indium::PrivateRenderCommandEncoder::setFragmentTextures(std::vector>& textures, Range range) { + for (size_t i = 0; i < range.length; ++i) { +- setFragmentTexture(textures[i], range.start + i); ++ _functionResources[1].setTexture(textures[i], range.start + i); ++ resolveTexture(1, range.start + i); + } + }; + + void Indium::PrivateRenderCommandEncoder::drawIndexedPrimitives(PrimitiveType primitiveType, size_t indexCount, IndexType indexType, std::shared_ptr indexBuffer, size_t indexBufferOffset, size_t instanceCount, int64_t baseVertex, size_t baseInstance) { ++ if (drawSkip.encoder == this && drawSkip.skip) return; ++ beginPassIfNeeded(); ++ GpuTiming::countDraw(); + auto buf = _privateCommandBuffer.lock(); + + // bind the pipeline with the right topology class for this primitive +@@ -490,24 +872,39 @@ void Indium::PrivateRenderCommandEncoder::drawIndexedPrimitives(PrimitiveType pr + default: + throw BadEnumValue(); + } +- DynamicVK::vkCmdBindPipeline(buf->commandBuffer(), VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); +- +- DynamicVK::vkCmdSetPrimitiveTopology(buf->commandBuffer(), primitiveTypeToVkPrimitiveTopology(primitiveType)); ++ if (_lastDrawPipeline != pipeline) { ++ DynamicVK::vkCmdBindPipeline(buf->commandBuffer(), VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); ++ _lastDrawPipeline = pipeline; ++ } ++ const auto topology = primitiveTypeToVkPrimitiveTopology(primitiveType); ++ if (_lastDrawTopology != topology) { ++ DynamicVK::vkCmdSetPrimitiveTopology(buf->commandBuffer(), topology); ++ _lastDrawTopology = topology; ++ } + + // TODO: avoid re-binding descriptors on every draw. +- updateBindings(); +- +- // we need to keep this buffer alive until we complete the render +- _keepAliveBuffers.push_back(indexBuffer); +- +- auto privateIndexBuffer = std::dynamic_pointer_cast(indexBuffer); +- +- DynamicVK::vkCmdBindIndexBuffer(buf->commandBuffer(), privateIndexBuffer->buffer(), indexBufferOffset, indexTypeToVkIndexType(indexType)); ++ updateBindings(buf->commandBuffer()); ++ ++ // Earlier draws retain the owner until GPU completion; this also makes ++ // cached resource identities immune to allocator address reuse. ++ if (_lastIndexResource.object != indexBuffer.get()) { ++ auto [owner, inserted] = _resolvedBufferOwners.acquire(indexBuffer); ++ _lastIndexResource.object = indexBuffer.get(); ++ _lastIndexResource.value = owner ? owner->value : nullptr; ++ } ++ const auto indexHandle = _lastIndexResource.value->buffer(); ++ const auto vkIndexType = indexTypeToVkIndexType(indexType); ++ if (_lastIndexBuffer != indexHandle || _lastIndexOffset != indexBufferOffset || _lastIndexType != vkIndexType) { ++ DynamicVK::vkCmdBindIndexBuffer(buf->commandBuffer(), indexHandle, indexBufferOffset, vkIndexType); ++ _lastIndexBuffer = indexHandle; ++ _lastIndexOffset = indexBufferOffset; ++ _lastIndexType = vkIndexType; ++ } ++ if (GpuTiming::enabled()) seqLog(" DRAWI %p n=%zu i=%zu\n", (void*) _privatePSO.get(), indexCount, instanceCount); + DynamicVK::vkCmdDrawIndexed(buf->commandBuffer(), indexCount, instanceCount, 0, baseVertex, baseInstance); ++ GpuTiming::afterDraw(buf->commandBuffer(), (uint64_t)(uintptr_t) _privatePSO.get()); + + // see drawPrimitives() to know why we do this +- _savedFunctionResources.push_back(_functionResources[0]); +- _savedFunctionResources.push_back(_functionResources[1]); + }; + + void Indium::PrivateRenderCommandEncoder::drawIndexedPrimitives(PrimitiveType primitiveType, size_t indexCount, IndexType indexType, std::shared_ptr indexBuffer, size_t indexBufferOffset, size_t instanceCount) { +@@ -521,8 +918,16 @@ void Indium::PrivateRenderCommandEncoder::drawIndexedPrimitives(PrimitiveType pr + void Indium::PrivateRenderCommandEncoder::setDepthStencilState(std::shared_ptr state) { + auto buf = _privateCommandBuffer.lock(); + auto privateState = std::dynamic_pointer_cast(state); +- auto& desc = privateState->descriptor(); +- ++ if (!privateState) { ++ DynamicVK::vkCmdSetDepthTestEnable(buf->commandBuffer(), VK_FALSE); ++ DynamicVK::vkCmdSetDepthWriteEnable(buf->commandBuffer(), VK_FALSE); ++ DynamicVK::vkCmdSetStencilTestEnable(buf->commandBuffer(), VK_FALSE); ++ return; ++ } ++ auto& desc = privateState->descriptor(); ++ ++ _depthWriteRequested = desc.depthWriteEnabled; ++ _depthWriteSuppressed = false; + DynamicVK::vkCmdSetDepthWriteEnable(buf->commandBuffer(), desc.depthWriteEnabled ? VK_TRUE : VK_FALSE); + DynamicVK::vkCmdSetDepthCompareOp(buf->commandBuffer(), compareFunctionToVkCompareOp(desc.depthCompareFunction)); + DynamicVK::vkCmdSetDepthTestEnable(buf->commandBuffer(), VK_TRUE); +@@ -694,3 +1099,18 @@ void Indium::PrivateRenderCommandEncoder::useResources(const std::vectorcommandBuffer(), stages, stages, VK_DEPENDENCY_BY_REGION_BIT, 1, &barrier, 0, nullptr, 0, nullptr); ++} +diff --git a/src/indium/render-pipeline.cpp b/src/indium/render-pipeline.cpp +index 1b6bb3b..97a1116 100644 +--- a/src/indium/render-pipeline.cpp ++++ b/src/indium/render-pipeline.cpp +@@ -1,8 +1,13 @@ ++#include ++#include ++#include ++#include + #include + #include + #include + #include + #include ++#include + + #include + +@@ -22,9 +27,33 @@ Indium::PrivateRenderPipelineState::PrivateRenderPipelineState(std::shared_ptr

(descriptor.vertexFunction); + _fragmentFunction = std::dynamic_pointer_cast(descriptor.fragmentFunction); +- +- _descriptorSetLayouts.processFunction(_vertexFunction, 0); +- _descriptorSetLayouts.processFunction(_fragmentFunction, 1); ++ GpuTiming::notePipeline(this, _vertexFunction ? GpuTiming::libraryId(_vertexFunction->library().get()) : std::string("-"), _fragmentFunction ? GpuTiming::libraryId(_fragmentFunction->library().get()) : std::string("-")); ++ OutlineChain::notePipeline(this, _fragmentFunction ? (const void*) _fragmentFunction->library().get() : nullptr); ++ ++ if (Indium::spirvDebugEnabled()) { ++ static unsigned created = 0; ++ if (created++ < 8192) { ++ if (FILE* file = fopen("/tmp/metal-trace.log", "a")) { ++ fprintf(file, "indium-pso %p vs-lib=%p fs-lib=%p layouts:", (void*) this, ++ _vertexFunction ? (void*) _vertexFunction->library().get() : nullptr, _fragmentFunction ? (void*) _fragmentFunction->library().get() : nullptr); ++ if (_vertexDescriptor) { ++ for (const auto& [index, layout]: _vertexDescriptor->layouts) { ++ fprintf(file, " [%zu stride=%zu step=%d]", (size_t) index, (size_t) layout.stride, (int) layout.stepFunction); ++ } ++ fprintf(file, " attrs:"); ++ for (const auto& [index, attr]: _vertexDescriptor->attributes) { ++ fprintf(file, " [%zu fmt=%d off=%zu buf=%zu]", (size_t) index, (int) attr.format, (size_t) attr.offset, (size_t) attr.bufferIndex); ++ } ++ } else { ++ fprintf(file, " none"); ++ } ++ fprintf(file, "\n"); ++ fclose(file); ++ } ++ } ++ } ++ _descriptorSetLayouts.processFunction(_vertexFunction, 0, true); ++ _descriptorSetLayouts.processFunction(_fragmentFunction, 1, true); + }; + + Indium::PrivateRenderPipelineState::~PrivateRenderPipelineState() { +@@ -67,6 +96,14 @@ void Indium::PrivateRenderPipelineState::recreatePipeline(VkRenderPass compatibl + shaderStage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + + shaderStage.stage = VK_SHADER_STAGE_VERTEX_BIT; ++ static unsigned libsLogged = 0; ++ if (libsLogged < 8192 && Indium::spirvDebugEnabled()) { ++ ++libsLogged; ++ if (FILE* file = fopen("/tmp/metal-trace.log", "a")) { ++ fprintf(file, "pipeline-libs vs=%p fs=%p\n", (void*) _vertexFunction->library().get(), _fragmentFunction ? (void*) _fragmentFunction->library().get() : nullptr); ++ fclose(file); ++ } ++ } + shaderStage.module = _vertexFunction->library()->shaderModule(); + shaderStage.pName = vertexFunctionName.c_str(); + stages.push_back(shaderStage); +@@ -168,6 +205,7 @@ void Indium::PrivateRenderPipelineState::recreatePipeline(VkRenderPass compatibl + tmp.dstAlphaBlendFactor = blendFactorToVkBlendFactor(colorInfo.destinationAlphaBlendFactor); + tmp.alphaBlendOp = blendOperationToVkBlendOp(colorInfo.alphaBlendOperation); + tmp.colorWriteMask = colorWriteMaskToVkColorComponentFlags(colorInfo.writeMask); ++ if (colorInfo.pixelFormat == PixelFormat::Invalid) tmp.colorWriteMask = 0; + colorBlendStates.push_back(tmp); + } + VkPipelineColorBlendStateCreateInfo colorBlendState {}; +diff --git a/src/indium/sampler.cpp b/src/indium/sampler.cpp +index 71392df..d82e5f0 100644 +--- a/src/indium/sampler.cpp ++++ b/src/indium/sampler.cpp +@@ -19,10 +19,14 @@ Indium::PrivateSamplerState::PrivateSamplerState(std::shared_ptr + info.mipLodBias = 0; // not sure where to get this from + info.anisotropyEnable = (descriptor.maxAnisotropy > 1) ? VK_TRUE : VK_FALSE; + info.maxAnisotropy = descriptor.maxAnisotropy; +- info.compareEnable = descriptor.normalizedCoordinates ? VK_TRUE : VK_FALSE; // FIXME: this assumes that the implementation defers to whatever the shader state specifies ++ // Metal only uses the compare function for comparison sampling (shadow maps); a sampler left at Never is used for ++ // ordinary reads, which are invalid (and in practice return nothing useful) with a Vulkan comparison sampler. ++ info.compareEnable = (descriptor.normalizedCoordinates && descriptor.compareFunction != CompareFunction::Never) ? VK_TRUE : VK_FALSE; + info.compareOp = compareFunctionToVkCompareOp(descriptor.compareFunction); +- info.minLod = descriptor.normalizedCoordinates ? descriptor.lodMinClamp : 0; +- info.maxLod = descriptor.normalizedCoordinates ? descriptor.lodMaxClamp : 0; ++ // A sampler without a mip filter reads the base level only, whatever the texture has. ++ const bool mipmapped = descriptor.mipFilter != SamplerMipFilter::NotMipmapped; ++ info.minLod = (descriptor.normalizedCoordinates && mipmapped) ? descriptor.lodMinClamp : 0; ++ info.maxLod = (descriptor.normalizedCoordinates && mipmapped) ? descriptor.lodMaxClamp : 0; + info.borderColor = samplerBorderColorToVkBorderColor(descriptor.borderColor); + info.unnormalizedCoordinates = descriptor.normalizedCoordinates ? VK_FALSE : VK_TRUE; + +diff --git a/src/indium/texture.cpp b/src/indium/texture.cpp +index 1e3e971..0870d92 100644 +--- a/src/indium/texture.cpp ++++ b/src/indium/texture.cpp +@@ -1,8 +1,87 @@ ++#include ++#include ++#include ++#include + #include ++#include + #include + #include + #include + #include ++#include ++#include ++#include ++#include ++#include ++ ++// Texture creation (layout transition) and replaceRegion (staging copy) each submit a one-shot command buffer. They used to ++// wait for its fence, and that fence only signals once the GPU has also finished everything submitted before it: in a League ++// fight the main thread spent 5.7% of its time in that wait (DRM_IOCTL_SYNCOBJ_WAIT, sampled 2026-10-10). The work is ordered ++// by the queue, so later command buffers see its result without anyone waiting; only the one-shot buffer, its fence and the ++// staging buffer have to outlive it. They are parked here and released once their fence has signalled. ++// MEASURED SLOWER and therefore OFF by default: three interleaved fight pairs gave 81.6/78.1/78.4 fps without the wait ++// against 84.9/81.9/81.7 with it (2026-10-10, GPU 95-97% busy, so the wait was not costing throughput). ++// Guest file /tmp/metal-async-uploads enables it (re-read every 64 one-shot submissions). ++namespace { ++ struct PendingOneshot { ++ VkFence fence; ++ VkCommandBuffer commandBuffer; ++ VkImage image; ++ std::shared_ptr keepAlive; ++ }; ++ ++ // guarded by the device's oneshotCommandPoolMutex ++ std::deque& pendingOneshots() { ++ static auto* list = new std::deque; ++ return *list; ++ } ++ ++ bool syncUploads() { ++ static std::atomic sync {true}; ++ static std::atomic calls {0}; ++ if ((calls.fetch_add(1, std::memory_order_relaxed) & 63) == 0) ++ sync.store(access("/tmp/metal-async-uploads", F_OK) != 0, std::memory_order_relaxed); ++ return sync.load(std::memory_order_relaxed); ++ } ++ ++ void releaseOneshot(const std::shared_ptr& device, PendingOneshot& entry) { ++ Indium::DynamicVK::vkDestroyFence(device->device(), entry.fence, nullptr); ++ Indium::DynamicVK::vkFreeCommandBuffers(device->device(), device->oneshotCommandPool(), 1, &entry.commandBuffer); ++ } ++ ++ // caller holds the pool mutex. Releases finished entries in submission order; with `through` set, waits for and releases ++ // everything up to the last entry that uses that image (the image is about to be destroyed). ++ void retireOneshots(const std::shared_ptr& device, VkImage through = VK_NULL_HANDLE) { ++ auto& list = pendingOneshots(); ++ size_t mustWait = 0; ++ if (through != VK_NULL_HANDLE) { ++ for (size_t i = 0; i < list.size(); ++i) if (list[i].image == through) mustWait = i + 1; ++ } ++ if (list.size() > 512 && mustWait < list.size() - 512) mustWait = list.size() - 512; // bounded backlog ++ while (!list.empty()) { ++ auto& front = list.front(); ++ if (mustWait > 0) { ++ if (Indium::DynamicVK::vkWaitForFences(device->device(), 1, &front.fence, VK_TRUE, 1ull * 1000 * 1000 * 1000) != VK_SUCCESS) abort(); ++ --mustWait; ++ } else if (Indium::DynamicVK::vkWaitForFences(device->device(), 1, &front.fence, VK_TRUE, 0) != VK_SUCCESS) { // zero timeout: a status poll ++ break; ++ } ++ releaseOneshot(device, front); ++ list.pop_front(); ++ } ++ } ++ ++ // caller holds the pool mutex and has submitted `commandBuffer` with `fence` ++ void finishOneshot(const std::shared_ptr& device, VkFence fence, VkCommandBuffer commandBuffer, VkImage image, std::shared_ptr keepAlive) { ++ PendingOneshot entry {fence, commandBuffer, image, std::move(keepAlive)}; ++ if (syncUploads()) { ++ if (Indium::DynamicVK::vkWaitForFences(device->device(), 1, &fence, VK_TRUE, 1ull * 1000 * 1000 * 1000) != VK_SUCCESS) abort(); ++ releaseOneshot(device, entry); ++ return; ++ } ++ pendingOneshots().push_back(std::move(entry)); ++ } ++} + + #include + +@@ -37,8 +116,8 @@ std::shared_ptr Indium::PrivateTexture::newTextureView(PixelFor + return newTextureView( + pixelFormat, + textureType(), +- Range { parentRelativeLevel(), mipmapLevelCount() }, +- Range { parentRelativeSlice(), arrayLength() } ++ Range { 0, mipmapLevelCount() }, ++ Range { 0, arrayLength() } + ); + }; + +@@ -78,12 +157,16 @@ Indium::TextureView::TextureView(std::shared_ptr original, Pixel + _layers(layers), + _swizzle(swizzle) + { +- auto parentLevelStart = _original->parentRelativeLevel(); ++ size_t parentLevelStart = 0; ++ size_t parentLayerStart = 0; ++ for (std::shared_ptr parent = _original; parent; parent = parent->parentTexture()) { ++ parentLevelStart += parent->parentRelativeLevel(); ++ parentLayerStart += parent->parentRelativeSlice(); ++ } + auto parentLevelEnd = parentLevelStart + _original->mipmapLevelCount() - 1; + auto levelStart = std::min(parentLevelStart + _levels.start, parentLevelEnd); + auto levelEnd = std::min(levelStart + _levels.length - 1, parentLevelEnd); + +- auto parentLayerStart = _original->parentRelativeSlice(); + auto parentLayerEnd = parentLayerStart + _original->arrayLength() - 1; + auto layerStart = std::min(parentLayerStart + _layers.start, parentLayerEnd); + auto layerEnd = std::min(layerStart + _layers.length - 1, parentLayerEnd); +@@ -109,9 +192,16 @@ Indium::TextureView::TextureView(std::shared_ptr original, Pixel + // TODO + abort(); + } ++ if ((info.subresourceRange.aspectMask & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) == (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) { ++ // Combined depth/stencil is legal for attachments, but descriptors require one aspect. ++ info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; ++ if (DynamicVK::vkCreateImageView(_device->device(), &info, nullptr, &_sampledImageView) != VK_SUCCESS) abort(); ++ } ++ + }; + + Indium::TextureView::~TextureView() { ++ if (_sampledImageView) DynamicVK::vkDestroyImageView(_device->device(), _sampledImageView, nullptr); + DynamicVK::vkDestroyImageView(std::dynamic_pointer_cast(_original->device())->device(), _imageView, nullptr); + }; + +@@ -124,15 +214,15 @@ Indium::TextureType Indium::TextureView::textureType() const { + }; + + size_t Indium::TextureView::width() const { +- return _original->width(); ++ return std::max(size_t(1), _original->width() >> _levels.start); + }; + + size_t Indium::TextureView::height() const { +- return _original->height(); ++ return std::max(size_t(1), _original->height() >> _levels.start); + }; + + size_t Indium::TextureView::depth() const { +- return _original->depth(); ++ return std::max(size_t(1), _original->depth() >> _levels.start); + }; + + size_t Indium::TextureView::mipmapLevelCount() const { +@@ -326,13 +416,42 @@ Indium::ConcreteTexture::ConcreteTexture(std::shared_ptr device, + + info.tiling = (descriptor.allowGPUOptimizedContents || !canBeLinear) ? VK_IMAGE_TILING_OPTIMAL : VK_IMAGE_TILING_LINEAR; + +- // we don't know ahead of time how the image is going to be used, so specify everything we support +- info.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; +- if (isColor) { ++ // Optional usages must be supported by the format and selected tiling. ++ // Compressed/sRGB formats commonly cannot be storage images or attachments. ++ VkFormatProperties formatProperties {}; ++ DynamicVK::vkGetPhysicalDeviceFormatProperties(_device->physicalDevice(), info.format, &formatProperties); ++ if (info.tiling == VK_IMAGE_TILING_LINEAR && !formatProperties.linearTilingFeatures) { ++ info.tiling = VK_IMAGE_TILING_OPTIMAL; ++ } ++ VkFormatFeatureFlags formatFeatures = info.tiling == VK_IMAGE_TILING_LINEAR ? formatProperties.linearTilingFeatures : formatProperties.optimalTilingFeatures; ++ info.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; ++ // RADV cannot keep DCC (colour compression) on GFX9 for an image that may be a storage image, so every render target ++ // that carried STORAGE_BIT cost full uncompressed bandwidth. A render target that Metal does not declare ++ // ShaderWrite does not need it. Guest flag file /tmp/metal-keep-storage restores the old behaviour for A/B runs. ++ static const bool keepStorage = access("/tmp/metal-keep-storage", F_OK) == 0; ++ const size_t metalUsage = static_cast(_descriptor.usage); ++ const bool renderTargetOnly = (metalUsage & static_cast(TextureUsage::RenderTarget)) != 0 && ++ (metalUsage & static_cast(TextureUsage::ShaderWrite)) == 0; ++ if ((formatFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) && (keepStorage || !renderTargetOnly)) { ++ info.usage |= VK_IMAGE_USAGE_STORAGE_BIT; ++ } ++ if (isColor && (formatFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT)) { + info.usage |= VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; +- } else if (isDepthStencil) { ++ } else if (isDepthStencil && (formatFeatures & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT)) { + info.usage |= VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT; + } ++ { ++ static const bool logTextures = access("/tmp/metal-gpu-timing", F_OK) == 0; ++ if (logTextures) { ++ if (FILE* f = fopen("/tmp/indium-textures.log", "a")) { ++ fprintf(f, "%ux%ux%u mips=%u fmt=%u usage=%zu opt=%d tiling=%s storage=%d storageMode=%u\n", (unsigned) _descriptor.width, ++ (unsigned) _descriptor.height, (unsigned) _descriptor.depth, (unsigned) _descriptor.mipmapLevelCount, (unsigned) _descriptor.pixelFormat, ++ metalUsage, (int) descriptor.allowGPUOptimizedContents, info.tiling == VK_IMAGE_TILING_LINEAR ? "LINEAR" : "OPTIMAL", ++ (info.usage & VK_IMAGE_USAGE_STORAGE_BIT) ? 1 : 0, (unsigned) _storageMode); ++ fclose(f); ++ } ++ } ++ } + info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; // TODO: check if this should be "concurrent" instead. i think we're good, though. + info.queueFamilyIndexCount = 0; + info.pQueueFamilyIndices = nullptr; +@@ -373,19 +492,18 @@ Indium::ConcreteTexture::ConcreteTexture(std::shared_ptr device, + throw std::runtime_error("No suitable memory region found for image with requested storage mode"); + } + +- VkMemoryAllocateInfo allocateInfo {}; +- allocateInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; +- allocateInfo.allocationSize = reqs.size; +- allocateInfo.memoryTypeIndex = targetIndex; +- +- if (DynamicVK::vkAllocateMemory(_device->device(), &allocateInfo, nullptr, &_memory) != VK_SUCCESS) { +- // TODO +- abort(); +- } ++ Indium::count("textureAlloc", reqs.size); ++ auto allocation = MemoryPool::allocate(_device->device(), reinterpret_cast(_image), targetIndex, reqs.size, reqs.alignment, true, false); ++ _memory = allocation.memory; + +- DynamicVK::vkBindImageMemory(_device->device(), _image, _memory, 0); ++ DynamicVK::vkBindImageMemory(_device->device(), _image, _memory, allocation.offset); + + // transition the image into the general layout ++ // Drain before taking the upload pool lock: a queued commit may need the pool to construct its batch barrier. ++ SubmitThread::drain(); ++ flushPendingSubmits(); ++ std::unique_lock poolLock(_device->oneshotCommandPoolMutex()); ++ retireOneshots(_device); + VkCommandBufferAllocateInfo cmdBufAllocInfo {}; + cmdBufAllocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; + cmdBufAllocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; +@@ -409,7 +527,7 @@ Indium::ConcreteTexture::ConcreteTexture(std::shared_ptr device, + barrier.srcAccessMask = VK_ACCESS_NONE; + barrier.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT; + barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; +- barrier.newLayout = VK_IMAGE_LAYOUT_GENERAL; ++ barrier.newLayout = imageLayout(); + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = _image; +@@ -438,15 +556,13 @@ Indium::ConcreteTexture::ConcreteTexture(std::shared_ptr device, + submitInfo.commandBufferCount = 1; + submitInfo.pCommandBuffers = &cmdBuf; + +- DynamicVK::vkQueueSubmit(_device->graphicsQueue(), 1, &submitInfo, theFence); +- if (DynamicVK::vkWaitForFences(_device->device(), 1, &theFence, VK_TRUE, /* 1s */ 1ull * 1000 * 1000 * 1000) != VK_SUCCESS) { +- // TODO +- abort(); ++ { ++ flushPendingSubmits(); // same for commits merged into a pending batch (takes the queue mutex itself) ++ std::lock_guard queueLock(_device->graphicsQueueMutex()); ++ if (DynamicVK::vkQueueSubmit(_device->graphicsQueue(), 1, &submitInfo, theFence) != VK_SUCCESS) abort(); + } +- +- DynamicVK::vkDestroyFence(_device->device(), theFence, nullptr); +- +- DynamicVK::vkFreeCommandBuffers(_device->device(), _device->oneshotCommandPool(), 1, &cmdBuf); ++ finishOneshot(_device, theFence, cmdBuf, _image, nullptr); ++ poolLock.unlock(); + + VkImageViewCreateInfo info2 {}; + info2.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; +@@ -463,12 +579,24 @@ Indium::ConcreteTexture::ConcreteTexture(std::shared_ptr device, + // TODO + abort(); + } ++ if ((info2.subresourceRange.aspectMask & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) == (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) { ++ // Combined depth/stencil is legal for attachments, but descriptors require one aspect. ++ info2.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; ++ if (DynamicVK::vkCreateImageView(_device->device(), &info2, nullptr, &_sampledImageView) != VK_SUCCESS) abort(); ++ } ++ + }; + + Indium::ConcreteTexture::~ConcreteTexture() { ++ { ++ std::unique_lock poolLock(_device->oneshotCommandPoolMutex()); ++ retireOneshots(_device, _image); ++ } ++ if (_sampledImageView) DynamicVK::vkDestroyImageView(_device->device(), _sampledImageView, nullptr); + DynamicVK::vkDestroyImageView(_device->device(), _imageView, nullptr); ++ auto owner = reinterpret_cast(_image); + DynamicVK::vkDestroyImage(_device->device(), _image, nullptr); +- DynamicVK::vkFreeMemory(_device->device(), _memory, nullptr); ++ MemoryPool::free(_device->device(), owner); + }; + + Indium::TextureType Indium::ConcreteTexture::textureType() const { +@@ -537,7 +665,20 @@ VkImage Indium::ConcreteTexture::image() { + return _image; + }; + ++// RADV (GFX9) keeps colour compression (DCC) only outside VK_IMAGE_LAYOUT_GENERAL. A plain colour render target that is ++// also sampled (Metal usage RenderTarget|ShaderRead) rests in SHADER_READ_ONLY_OPTIMAL and is a COLOR_ATTACHMENT_OPTIMAL ++// attachment; everything else keeps GENERAL. Guest flag file /tmp/metal-no-opt-layouts disables it (checked once). + VkImageLayout Indium::ConcreteTexture::imageLayout() { ++ static const bool optLayouts = access("/tmp/metal-no-opt-layouts", F_OK) != 0; ++ if (optLayouts) { ++ const size_t usage = static_cast(_descriptor.usage); ++ const size_t renderTarget = static_cast(TextureUsage::RenderTarget); ++ const size_t shaderWrite = static_cast(TextureUsage::ShaderWrite); ++ if ((usage & renderTarget) && !(usage & shaderWrite) && _descriptor.textureType == TextureType::e2D && ++ _descriptor.mipmapLevelCount == 1 && _descriptor.arrayLength == 1 && ++ (pixelFormatToVkImageAspectFlags(_descriptor.pixelFormat) & VK_IMAGE_ASPECT_COLOR_BIT)) ++ return VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; ++ } + return VK_IMAGE_LAYOUT_GENERAL; + }; + +@@ -546,6 +687,16 @@ void Indium::ConcreteTexture::replaceRegion(Indium::Region region, size_t mipmap + }; + + void Indium::ConcreteTexture::replaceRegion(Indium::Region region, size_t mipmapLevel, size_t slice, const void* bytes, size_t bytesPerRow, size_t bytesPerImage) { ++ // diagnostic counters (only active with the guest /tmp/metal-debug-timing flag): how often CPU uploads happen and how long the ++ // synchronous staging copy + fence wait blocks the calling thread ++ const auto uploadStart = std::chrono::steady_clock::now(); ++ struct UploadTimer { ++ std::chrono::steady_clock::time_point start; size_t bytes; ++ ~UploadTimer() { ++ Indium::count("replaceRegionCalls", bytes); ++ Indium::count("replaceRegionMicroseconds", std::chrono::duration(std::chrono::steady_clock::now() - start).count()); ++ } ++ } uploadTimer {uploadStart, bytesPerImage ? bytesPerImage : bytesPerRow * region.size.height}; + if (_storageMode != StorageMode::Managed && _storageMode != StorageMode::Shared) { + throw std::runtime_error("Invalid usage of replaceRegion on non-managed and non-shared texture"); + } +@@ -556,99 +707,109 @@ void Indium::ConcreteTexture::replaceRegion(Indium::Region region, size_t mipmap + + auto aspect = pixelFormatToVkImageAspectFlags(_descriptor.pixelFormat); + +- // create a temporary buffer copy +- // FIXME: this doesn't work for PVRTC textures +- // actually, FIXME: this doesn't handle a few cases, particularly with array texture types (since i haven't seen those yet at all) +- auto byteSize = bytesPerImage == 0 ? (region.size.height * bytesPerRow) : (region.size.depth * bytesPerImage); +- auto tmpBuf = std::dynamic_pointer_cast(_device->newBuffer(bytes, byteSize, ResourceOptions::StorageModeShared)); +- +- // FIXME: handle compressed formats +- size_t bytesPerPixel = pixelFormatToByteCount(_descriptor.pixelFormat); +- +- VkCommandBufferAllocateInfo cmdBufAllocInfo {}; +- cmdBufAllocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; +- cmdBufAllocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; +- cmdBufAllocInfo.commandPool = _device->oneshotCommandPool(); +- cmdBufAllocInfo.commandBufferCount = 1; +- +- VkCommandBuffer cmdBuf; +- if (DynamicVK::vkAllocateCommandBuffers(_device->device(), &cmdBufAllocInfo, &cmdBuf) != VK_SUCCESS) { +- // TODO +- abort(); +- } +- +- VkCommandBufferBeginInfo cmdBufBeginInfo {}; +- cmdBufBeginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; +- cmdBufBeginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; +- +- DynamicVK::vkBeginCommandBuffer(cmdBuf, &cmdBufBeginInfo); +- +- // first, transition the image to the optimal layout for transfer destinations +- VkImageMemoryBarrier barrier {}; +- barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; +- barrier.srcAccessMask = VK_ACCESS_NONE; +- barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; +- barrier.oldLayout = VK_IMAGE_LAYOUT_GENERAL; +- barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; +- barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; +- barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; +- barrier.image = _image; +- barrier.subresourceRange.aspectMask = aspect; +- barrier.subresourceRange.baseMipLevel = mipmapLevel; +- barrier.subresourceRange.levelCount = 1; +- barrier.subresourceRange.baseArrayLayer = slice; +- barrier.subresourceRange.layerCount = 1; +- +- DynamicVK::vkCmdPipelineBarrier(cmdBuf, VK_PIPELINE_STAGE_NONE, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); +- +- // now encode the copy +- VkBufferImageCopy copyInfo {}; +- copyInfo.bufferOffset = 0; +- copyInfo.bufferRowLength = bytesPerRow / bytesPerPixel; +- copyInfo.bufferImageHeight = bytesPerImage / bytesPerRow; +- copyInfo.imageSubresource.aspectMask = aspect; +- copyInfo.imageSubresource.mipLevel = mipmapLevel; +- copyInfo.imageSubresource.baseArrayLayer = slice; +- copyInfo.imageSubresource.layerCount = 1; +- copyInfo.imageOffset.x = region.origin.x; +- copyInfo.imageOffset.y = region.origin.y; +- copyInfo.imageOffset.z = region.origin.z; +- copyInfo.imageExtent.width = region.size.width; +- copyInfo.imageExtent.height = region.size.height; +- copyInfo.imageExtent.depth = region.size.depth; +- DynamicVK::vkCmdCopyBufferToImage(cmdBuf, tmpBuf->buffer(), _image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©Info); +- +- // finally, transition the image back to the general layout +- barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; +- barrier.newLayout = VK_IMAGE_LAYOUT_GENERAL; +- barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; +- barrier.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT; +- DynamicVK::vkCmdPipelineBarrier(cmdBuf, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); +- +- DynamicVK::vkEndCommandBuffer(cmdBuf); +- +- VkFenceCreateInfo fenceCreateInfo {}; +- fenceCreateInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; +- +- VkFence theFence = VK_NULL_HANDLE; ++ auto pitch = bufferImagePitch(_descriptor.pixelFormat, region.size.width, region.size.height, bytesPerRow, bytesPerImage); ++ auto block = pixelFormatTransferBlock(_descriptor.pixelFormat); ++ size_t rows = (region.size.height + block.height - 1) / block.height; ++ size_t tightRow = ((region.size.width + block.width - 1) / block.width) * block.bytes; ++ size_t byteSize = (region.size.depth - 1) * pitch.imageBytes + (rows - 1) * pitch.rowBytes + tightRow; ++ auto tmpBuf = std::dynamic_pointer_cast(_device->newBuffer(bytes, byteSize, ResourceOptions::StorageModeShared)); ++ ++ // With deferred encoding on, the upload is queued behind the pending replays on the same worker (FIFO keeps the order) ++ // instead of draining it from this thread in the middle of a frame. ++ auto upload = [=, self = shared_from_this()]() { ++ // Drain before taking the upload pool lock: a queued commit may need the pool to construct its batch barrier. ++ SubmitThread::drain(); ++ flushPendingSubmits(); ++ std::unique_lock poolLock(_device->oneshotCommandPoolMutex()); ++ retireOneshots(_device); ++ VkCommandBufferAllocateInfo cmdBufAllocInfo {}; ++ cmdBufAllocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; ++ cmdBufAllocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; ++ cmdBufAllocInfo.commandPool = _device->oneshotCommandPool(); ++ cmdBufAllocInfo.commandBufferCount = 1; ++ ++ VkCommandBuffer cmdBuf; ++ if (DynamicVK::vkAllocateCommandBuffers(_device->device(), &cmdBufAllocInfo, &cmdBuf) != VK_SUCCESS) { ++ // TODO ++ abort(); ++ } + +- if (DynamicVK::vkCreateFence(_device->device(), &fenceCreateInfo, nullptr, &theFence) != VK_SUCCESS) { +- // TODO +- abort(); +- } ++ VkCommandBufferBeginInfo cmdBufBeginInfo {}; ++ cmdBufBeginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; ++ cmdBufBeginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; ++ ++ DynamicVK::vkBeginCommandBuffer(cmdBuf, &cmdBufBeginInfo); ++ ++ // first, transition the image to the optimal layout for transfer destinations ++ VkImageMemoryBarrier barrier {}; ++ barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; ++ barrier.srcAccessMask = VK_ACCESS_NONE; ++ barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; ++ barrier.oldLayout = imageLayout(); ++ barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; ++ barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; ++ barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; ++ barrier.image = _image; ++ barrier.subresourceRange.aspectMask = aspect; ++ barrier.subresourceRange.baseMipLevel = mipmapLevel; ++ barrier.subresourceRange.levelCount = 1; ++ barrier.subresourceRange.baseArrayLayer = slice; ++ barrier.subresourceRange.layerCount = 1; ++ ++ DynamicVK::vkCmdPipelineBarrier(cmdBuf, VK_PIPELINE_STAGE_NONE, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); ++ ++ // now encode the copy ++ VkBufferImageCopy copyInfo {}; ++ copyInfo.bufferOffset = 0; ++ copyInfo.bufferRowLength = pitch.rowLength; ++ copyInfo.bufferImageHeight = pitch.imageHeight; ++ copyInfo.imageSubresource.aspectMask = aspect; ++ copyInfo.imageSubresource.mipLevel = mipmapLevel; ++ copyInfo.imageSubresource.baseArrayLayer = slice; ++ copyInfo.imageSubresource.layerCount = 1; ++ copyInfo.imageOffset.x = region.origin.x; ++ copyInfo.imageOffset.y = region.origin.y; ++ copyInfo.imageOffset.z = region.origin.z; ++ copyInfo.imageExtent.width = region.size.width; ++ copyInfo.imageExtent.height = region.size.height; ++ copyInfo.imageExtent.depth = region.size.depth; ++ DynamicVK::vkCmdCopyBufferToImage(cmdBuf, tmpBuf->buffer(), _image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©Info); ++ ++ // finally, transition the image back to the general layout ++ barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; ++ barrier.newLayout = imageLayout(); ++ barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; ++ barrier.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT; ++ DynamicVK::vkCmdPipelineBarrier(cmdBuf, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); ++ ++ DynamicVK::vkEndCommandBuffer(cmdBuf); ++ ++ VkFenceCreateInfo fenceCreateInfo {}; ++ fenceCreateInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; ++ ++ VkFence theFence = VK_NULL_HANDLE; ++ ++ if (DynamicVK::vkCreateFence(_device->device(), &fenceCreateInfo, nullptr, &theFence) != VK_SUCCESS) { ++ // TODO ++ abort(); ++ } + +- VkSubmitInfo submitInfo {}; +- submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; +- submitInfo.commandBufferCount = 1; +- submitInfo.pCommandBuffers = &cmdBuf; ++ VkSubmitInfo submitInfo {}; ++ submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; ++ submitInfo.commandBufferCount = 1; ++ submitInfo.pCommandBuffers = &cmdBuf; + +- DynamicVK::vkQueueSubmit(_device->graphicsQueue(), 1, &submitInfo, theFence); +- if (DynamicVK::vkWaitForFences(_device->device(), 1, &theFence, VK_TRUE, /* 1s */ 1ull * 1000 * 1000 * 1000) != VK_SUCCESS) { +- // TODO +- abort(); ++ { ++ flushPendingSubmits(); // same for commits merged into a pending batch (takes the queue mutex itself) ++ std::lock_guard queueLock(_device->graphicsQueueMutex()); ++ if (DynamicVK::vkQueueSubmit(_device->graphicsQueue(), 1, &submitInfo, theFence) != VK_SUCCESS) abort(); ++ } ++ finishOneshot(_device, theFence, cmdBuf, _image, tmpBuf); ++ poolLock.unlock(); ++ }; ++ if (AsyncEncode::enabled() && !SubmitThread::onWorker()) { ++ SubmitThread::post(upload); ++ } else { ++ upload(); + } +- +- DynamicVK::vkDestroyFence(_device->device(), theFence, nullptr); +- +- DynamicVK::vkFreeCommandBuffers(_device->device(), _device->oneshotCommandPool(), 1, &cmdBuf); + }; +diff --git a/src/iridium/air.cpp b/src/iridium/air.cpp +index 21c501f..d052cf4 100644 +--- a/src/iridium/air.cpp ++++ b/src/iridium/air.cpp +@@ -4,9 +4,36 @@ + #include + + #include ++#include ++#include ++#include + + namespace DynamicLLVM = Iridium::DynamicLLVM; + ++// AIR links varyings by their user location, not by their order in each stage. ++// Optimized fragment functions may omit entries that the vertex stage still writes. ++static uint32_t airInterfaceLocation(const std::vector& metadata, uint32_t fallback) { ++ for (size_t i = 0; i < metadata.size(); ++i) { ++ if (!DynamicLLVM::LLVMIsAMDString(metadata[i])) continue; ++ unsigned length = 0; ++ const char* bytes = DynamicLLVM::LLVMGetMDString(metadata[i], &length); ++ std::string_view value(bytes, length); ++ constexpr std::string_view prefix = "user(locn"; ++ if (value.size() > prefix.size() + 1 && value.substr(0, prefix.size()) == prefix && value.back() == ')') { ++ uint32_t location = 0; ++ for (char digit : value.substr(prefix.size(), value.size() - prefix.size() - 1)) { ++ if (digit < '0' || digit > '9') throw std::runtime_error("Invalid AIR varying location"); ++ location = location * 10 + static_cast(digit - '0'); ++ } ++ return location; ++ } ++ if (value == "air.render_target" && i + 1 < metadata.size()) ++ return static_cast(DynamicLLVM::LLVMConstIntGetZExtValue(metadata[i + 1])); ++ } ++ return fallback; ++} ++ ++ + // special thanks to https://github.com/YuAo/MetalLibraryArchive for information on the library format + + // we use LLVM's C API because it's much more stable than the C++ API and it's good enough for our needs +@@ -29,6 +56,16 @@ Iridium::AIR::Function::Function(Type type, const std::string& name, const void* + } + + _module = LLVMSupport::Module(moduleRef, DynamicLLVM::LLVMDisposeModule); ++ // Normalize local aggregate copies into SSA, then give nested selections and loops ++ // distinct merges before lowering them into Vulkan's structured control flow. ++ std::string triple=DynamicLLVM::LLVMGetTarget(moduleRef); ++ DynamicLLVM::LLVMSetTarget(moduleRef,"spirv64-unknown-unknown"); ++ auto options=DynamicLLVM::LLVMCreatePassBuilderOptions(); ++ auto error=DynamicLLVM::LLVMRunPasses(moduleRef,"function(sroa,mem2reg,loop-unroll,sroa,mem2reg,simplifycfg,structurizecfg)",nullptr,options); ++ DynamicLLVM::LLVMDisposePassBuilderOptions(options); ++ DynamicLLVM::LLVMSetTarget(moduleRef,triple.c_str()); ++ if(error){auto message=DynamicLLVM::LLVMGetErrorMessage(error);std::string detail=message?message:"unknown LLVM error";DynamicLLVM::LLVMDisposeErrorMessage(message);throw std::runtime_error("Cannot normalize AIR control flow: "+detail);} ++ + + _function = DynamicLLVM::LLVMGetNamedFunction(_module.get(), _name.c_str()); + +@@ -41,7 +78,86 @@ Iridium::AIR::Function::Function(Type type, const std::string& name, const void* + // TEST + #include + +-class ImpossibleResultID: public std::exception {}; ++static thread_local std::string translationContext; ++static thread_local LLVMValueRef translationInstruction=nullptr; ++static std::string describeLLVMValue(LLVMValueRef value) { ++ char* raw = DynamicLLVM::LLVMPrintValueToString(value); ++ std::string result = raw ? raw : "unknown value"; ++ if (raw) DynamicLLVM::LLVMDisposeMessage(raw); ++ return result; ++} ++class ImpossibleResultID: public std::runtime_error { ++public: explicit ImpossibleResultID(const std::string& detail = "Unsupported AIR type"): std::runtime_error(detail) {} ++}; ++ ++// While set, struct and array types are created without explicit layout, as Private/Function memory needs. ++static thread_local bool g_layoutFreeTypes = false; ++ ++class LayoutFreeScope { ++public: ++ explicit LayoutFreeScope(bool enabled): _previous(g_layoutFreeTypes) { g_layoutFreeTypes = enabled; } ++ ~LayoutFreeScope() { g_layoutFreeTypes = _previous; } ++private: ++ bool _previous; ++}; ++ ++// A pointer bitcast retains its intended element type in consuming loads/GEPs. ++static LLVMTypeRef pointerElementType(LLVMValueRef value, unsigned depth = 0) { ++ if (depth > 16) throw std::runtime_error("AIR pointer-use chain exceeds limit"); ++ for (auto use = DynamicLLVM::LLVMGetFirstUse(value); use; use = DynamicLLVM::LLVMGetNextUse(use)) { ++ auto user = DynamicLLVM::LLVMGetUser(use); ++ if (DynamicLLVM::LLVMGetValueKind(user) != LLVMInstructionValueKind) continue; ++ auto opcode = DynamicLLVM::LLVMGetInstructionOpcode(user); ++ if (opcode == LLVMGetElementPtr && DynamicLLVM::LLVMGetOperand(user, 0) == value) ++ return DynamicLLVM::LLVMGetGEPSourceElementType(user); ++ if (opcode == LLVMLoad && DynamicLLVM::LLVMGetOperand(user, 0) == value) ++ return DynamicLLVM::LLVMTypeOf(user); ++ if ((opcode == LLVMBitCast || opcode == LLVMAddrSpaceCast) && DynamicLLVM::LLVMGetOperand(user, 0) == value) ++ return pointerElementType(user, depth + 1); ++ // the pointer stored through carries the stored value's type ++ if (opcode == LLVMStore && DynamicLLVM::LLVMGetOperand(user, 1) == value) ++ return DynamicLLVM::LLVMTypeOf(DynamicLLVM::LLVMGetOperand(user, 0)); ++ // a pointer that is merged with others is typed by how the merged pointer is used ++ if (opcode == LLVMPHI || opcode == LLVMSelect) { ++ try { ++ return pointerElementType(user, depth + 1); ++ } catch (const std::runtime_error&) { ++ // try the remaining uses ++ } ++ } ++ } ++ // Nothing constrains the pointee (an argument the shader never reads, for example), so any ++ // 32-bit type describes it as well as another. ++ return DynamicLLVM::LLVMInt32Type(); ++} ++// Modern LLVM removes pointee types from pointers. Loads and GEP retain them. ++static LLVMTypeRef bufferElementType(LLVMValueRef function, size_t parameterIndex) { ++ auto parameter = DynamicLLVM::LLVMGetParam(function, parameterIndex); ++ for (auto block = DynamicLLVM::LLVMGetFirstBasicBlock(function); block; block = DynamicLLVM::LLVMGetNextBasicBlock(block)) { ++ for (auto inst = DynamicLLVM::LLVMGetFirstInstruction(block); inst; inst = DynamicLLVM::LLVMGetNextInstruction(inst)) { ++ auto opcode = DynamicLLVM::LLVMGetInstructionOpcode(inst); ++ if (opcode == LLVMGetElementPtr && DynamicLLVM::LLVMGetOperand(inst, 0) == parameter) ++ return DynamicLLVM::LLVMGetGEPSourceElementType(inst); ++ if (opcode == LLVMLoad && DynamicLLVM::LLVMGetOperand(inst, 0) == parameter) ++ return DynamicLLVM::LLVMTypeOf(inst); ++ } ++ } ++ return pointerElementType(parameter); ++} ++static LLVMTypeRef gepElementType(LLVMValueRef inst) { ++ auto type = DynamicLLVM::LLVMGetGEPSourceElementType(inst); ++ for (unsigned i = 2; i < DynamicLLVM::LLVMGetNumOperands(inst); ++i) { ++ switch (DynamicLLVM::LLVMGetTypeKind(type)) { ++ case LLVMStructTypeKind: ++ type = DynamicLLVM::LLVMStructGetTypeAtIndex(type, DynamicLLVM::LLVMConstIntGetZExtValue(DynamicLLVM::LLVMGetOperand(inst, i))); ++ break; ++ case LLVMArrayTypeKind: case LLVMVectorTypeKind: ++ type = DynamicLLVM::LLVMGetElementType(type); break; ++ default: throw std::runtime_error("Unsupported AIR GEP element type"); ++ } ++ } ++ return type; ++} + + static Iridium::SPIRV::ResultID llvmTypeToSPIRVType(Iridium::SPIRV::Builder& builder, LLVMTypeRef llvmType) { + using namespace Iridium::SPIRV; +@@ -68,6 +184,10 @@ static Iridium::SPIRV::ResultID llvmTypeToSPIRVType(Iridium::SPIRV::Builder& bui + return builder.declareType(Type(Type::FloatTag {}, 128)); + + case LLVMIntegerTypeKind: ++ if (DynamicLLVM::LLVMGetIntTypeWidth(llvmType) == 1) { ++ // AIR uses i1 for booleans, which SPIR-V has no integer type for ++ return builder.declareType(Type(Type::BooleanTag {})); ++ } + return builder.declareType(Type(Type::IntegerTag {}, DynamicLLVM::LLVMGetIntTypeWidth(llvmType), /* TODO: how to determine this? */ true)); + + case LLVMFunctionTypeKind: { +@@ -117,28 +237,35 @@ static Iridium::SPIRV::ResultID llvmTypeToSPIRVType(Iridium::SPIRV::Builder& bui + } + // align the final offset to determine the structure size + offset = (offset + (structAlignment - 1)) & ~(structAlignment - 1); +- return builder.declareType(Type(Type::StructureTag {}, members, offset, structAlignment)); ++ Type structType(Type::StructureTag {}, members, offset, structAlignment); ++ structType.layoutFree = g_layoutFreeTypes; ++ return builder.declareType(structType); + } break; + + case LLVMPointerTypeKind: { + StorageClass storageClass = StorageClass::Output; + // TODO: somehow determine the appropriate storage class + //auto addrSpace = DynamicLLVM::LLVMGetPointerAddressSpace(llvmType); +- return builder.declareType(Type(Type::PointerTag {}, storageClass, llvmTypeToSPIRVType(builder, DynamicLLVM::LLVMGetElementType(llvmType)), 8)); ++ throw std::runtime_error("Opaque pointer requires instruction-specific element type: " + (translationInstruction?describeLLVMValue(translationInstruction):translationContext)); + } break; + + case LLVMVectorTypeKind: { + auto type = llvmTypeToSPIRVType(builder, DynamicLLVM::LLVMGetElementType(llvmType)); + auto typeInst = *builder.reverseLookupType(type); + auto elmCount = DynamicLLVM::LLVMGetVectorSize(llvmType); +- return builder.declareType(Type(Type::VectorTag {}, elmCount, type, typeInst.size * elmCount, ((elmCount == 3 || elmCount == 4) ? 4 : 2) * typeInst.alignment)); ++ // AIR's data layout allocates three-component vectors like four-component vectors. ++ // This padding belongs to the vector even inside a packed struct or array. ++ const auto allocatedCount = elmCount == 3 ? 4 : elmCount; ++ return builder.declareType(Type(Type::VectorTag {}, elmCount, type, typeInst.size * allocatedCount, ((elmCount == 3 || elmCount == 4) ? 4 : 2) * typeInst.alignment)); + } break; + + case LLVMArrayTypeKind: { + auto type = llvmTypeToSPIRVType(builder, DynamicLLVM::LLVMGetElementType(llvmType)); + auto typeInst = *builder.reverseLookupType(type); + auto elmCount = DynamicLLVM::LLVMGetArrayLength(llvmType); +- return builder.declareType(Type(Type::ArrayTag {}, type, elmCount, typeInst.size * elmCount, typeInst.alignment)); ++ Type arrayType(Type::ArrayTag {}, type, elmCount, typeInst.size * elmCount, typeInst.alignment); ++ arrayType.layoutFree = g_layoutFreeTypes; ++ return builder.declareType(arrayType); + } break; + + default: +@@ -162,14 +289,32 @@ static Iridium::SPIRV::ResultID llvmValueToResultID(Iridium::SPIRV::Builder& bui + { + ResultID maybe = builder.lookupResultID(reinterpret_cast(llvmValue)); + if (maybe != ResultIDInvalid) { ++ if (kind == LLVMGlobalVariableValueKind) builder.referenceGlobalVariable(maybe); + return maybe; + } + } + + switch (kind) { ++ case LLVMGlobalVariableValueKind: { ++ auto initializer = DynamicLLVM::LLVMGetInitializer(llvmValue); ++ if (!initializer) throw ImpossibleResultID("External AIR globals are unsupported"); ++ LayoutFreeScope layoutFree(true); ++ auto valueType = llvmTypeToSPIRVType(builder, DynamicLLVM::LLVMGlobalGetValueType(llvmValue)); ++ auto pointerType = builder.declareType(Type(Type::PointerTag{}, StorageClass::Private, valueType, 8)); ++ auto initialValue = llvmValueToResultID(builder, initializer); ++ auto result = builder.addGlobalVariable(pointerType, StorageClass::Private, initialValue); ++ builder.setResultType(result, pointerType); ++ builder.associateExistingResultID(result, reinterpret_cast(llvmValue)); ++ builder.referenceGlobalVariable(result); ++ return result; ++ } ++ + case LLVMConstantIntValueKind: { + auto val = DynamicLLVM::LLVMConstIntGetZExtValue(llvmValue); + auto width = DynamicLLVM::LLVMGetIntTypeWidth(type ); ++ if (width == 1) { ++ return builder.declareConstantBool(val != 0); ++ } + // TODO: how to determine if it's signed or not? + return (width <= 32) ? builder.declareConstantScalar(val) : builder.declareConstantScalar(val); + } break; +@@ -191,13 +336,15 @@ static Iridium::SPIRV::ResultID llvmValueToResultID(Iridium::SPIRV::Builder& bui + } break; + + default: +- throw ImpossibleResultID(); ++ throw ImpossibleResultID("Unsupported LLVM value: " + describeLLVMValue(llvmValue)); + } + } break; + +- case LLVMConstantVectorValueKind: { ++ case LLVMConstantStructValueKind: ++ case LLVMConstantArrayValueKind: ++ case LLVMConstantVectorValueKind: { + std::vector vals; +- auto count = DynamicLLVM::LLVMGetVectorSize(type); ++ auto count = DynamicLLVM::LLVMGetTypeKind(type)==LLVMStructTypeKind ? DynamicLLVM::LLVMCountStructElementTypes(type) : (DynamicLLVM::LLVMGetTypeKind(type)==LLVMArrayTypeKind ? DynamicLLVM::LLVMGetArrayLength(type) : DynamicLLVM::LLVMGetVectorSize(type)); + + for (size_t i = 0; i < count; ++i) { + auto constant = DynamicLLVM::LLVMGetOperand(llvmValue, i); +@@ -207,10 +354,11 @@ static Iridium::SPIRV::ResultID llvmValueToResultID(Iridium::SPIRV::Builder& bui + return builder.declareConstantComposite(llvmTypeToSPIRVType(builder, type), vals); + } break; + +- case LLVMConstantDataVectorValueKind: { ++ case LLVMConstantDataArrayValueKind: ++ case LLVMConstantDataVectorValueKind: { + std::vector vals; + +- auto count = DynamicLLVM::LLVMGetVectorSize(type); ++ auto count = typeKind == LLVMArrayTypeKind ? DynamicLLVM::LLVMGetArrayLength(type) : DynamicLLVM::LLVMGetVectorSize(type); + + for (size_t i = 0; i < count; ++i) { + auto constant = DynamicLLVM::LLVMGetElementAsConstant(llvmValue, i); +@@ -220,7 +368,8 @@ static Iridium::SPIRV::ResultID llvmValueToResultID(Iridium::SPIRV::Builder& bui + return builder.declareConstantComposite(llvmTypeToSPIRVType(builder, type), vals); + } break; + +- case LLVMUndefValueValueKind: { ++ case LLVMPoisonValueValueKind: ++ case LLVMUndefValueValueKind: { + return builder.declareUndefinedValue(llvmTypeToSPIRVType(builder, type)); + } break; + +@@ -231,6 +380,21 @@ static Iridium::SPIRV::ResultID llvmValueToResultID(Iridium::SPIRV::Builder& bui + + case LLVMConstantExprValueKind: { + switch (DynamicLLVM::LLVMGetConstOpcode(llvmValue)) { ++ case LLVMGetElementPtr: { ++ auto base = llvmValueToResultID(builder, DynamicLLVM::LLVMGetOperand(llvmValue,0)); ++ auto baseType = *builder.reverseLookupType(builder.lookupResultType(base)); ++ auto firstIndex = DynamicLLVM::LLVMGetOperand(llvmValue,1); ++ if (DynamicLLVM::LLVMGetValueKind(firstIndex) != LLVMConstantIntValueKind || DynamicLLVM::LLVMConstIntGetZExtValue(firstIndex) != 0) ++ throw ImpossibleResultID("Constant AIR GEP requires a zero initial index"); ++ std::vector indices; ++ for (unsigned i=2;i splitAirName(std::string_view name) { ++ std::vector parts; ++ for (size_t start = 0; start <= name.size();) { ++ auto dot = name.find('.', start); ++ if (dot == std::string_view::npos) { ++ dot = name.size(); ++ } ++ parts.push_back(name.substr(start, dot - start)); ++ start = dot + 1; ++ } ++ return parts; ++}; ++ ++// "f32", "f16", "v3f32", ... ++static bool airTypeTokenIsFloat(std::string_view token) { ++ if (token.empty()) { ++ return false; ++ } ++ if (token[0] == 'v') { ++ token.remove_prefix(1); ++ while (!token.empty() && token[0] >= '0' && token[0] <= '9') { ++ token.remove_prefix(1); ++ } ++ } ++ return !token.empty() && token[0] == 'f'; ++}; ++ ++// SPIR-V core opcodes the translation below needs that the Opcode enum does not name ++namespace SPIRVOp { ++ constexpr uint16_t ConvertFToU = 109; ++ constexpr uint16_t ConvertFToS = 110; ++ constexpr uint16_t ConvertSToF = 111; ++ constexpr uint16_t ConvertUToF = 112; ++ constexpr uint16_t UConvert = 113; ++ constexpr uint16_t SConvert = 114; ++ constexpr uint16_t FConvert = 115; ++ constexpr uint16_t Bitcast = 124; ++ constexpr uint16_t FRem = 140; ++ constexpr uint16_t Any = 154; ++ constexpr uint16_t All = 155; ++ constexpr uint16_t Select = 169; ++ constexpr uint16_t IEqual = 170; ++ constexpr uint16_t INotEqual = 171; ++ constexpr uint16_t UGreaterThan = 172; ++ constexpr uint16_t SGreaterThan = 173; ++ constexpr uint16_t UGreaterThanEqual = 174; ++ constexpr uint16_t SGreaterThanEqual = 175; ++ constexpr uint16_t ULessThan = 176; ++ constexpr uint16_t SLessThan = 177; ++ constexpr uint16_t ULessThanEqual = 178; ++ constexpr uint16_t SLessThanEqual = 179; ++ constexpr uint16_t FOrdEqual = 180; ++ constexpr uint16_t FUnordEqual = 181; ++ constexpr uint16_t FOrdNotEqual = 182; ++ constexpr uint16_t FUnordNotEqual = 183; ++ constexpr uint16_t FOrdLessThan = 184; ++ constexpr uint16_t FUnordLessThan = 185; ++ constexpr uint16_t FOrdGreaterThan = 186; ++ constexpr uint16_t FUnordGreaterThan = 187; ++ constexpr uint16_t FOrdLessThanEqual = 188; ++ constexpr uint16_t FUnordLessThanEqual = 189; ++ constexpr uint16_t FOrdGreaterThanEqual = 190; ++ constexpr uint16_t FUnordGreaterThanEqual = 191; ++ constexpr uint16_t Ordered = 162; ++ constexpr uint16_t Unordered = 163; ++ constexpr uint16_t DPdx = 207; ++ constexpr uint16_t DPdy = 208; ++ constexpr uint16_t Fwidth = 209; ++ constexpr uint16_t Kill = 252; ++}; ++ ++static bool translateAirTexture(Iridium::SPIRV::Builder& builder, LLVMValueRef inst, std::string_view name, Iridium::SPIRV::ResultID resultType, Iridium::FunctionType stage, Iridium::SPIRV::ResultID& result) { ++ using namespace Iridium::SPIRV; ++ bool compare = name.find("air.sample_compare_depth_") == 0; ++ bool sample = name.find("air.sample_texture_") == 0 || compare; ++ bool read = name.find("air.read_texture_") == 0; ++ bool write = name.find("air.write_texture_") == 0; ++ bool width = name == "air.get_width_texture_2d", height = name == "air.get_height_texture_2d"; ++ if (!sample && !read && !write && !width && !height) return false; ++ auto operand = [&](unsigned i) { return llvmValueToResultID(builder, DynamicLLVM::LLVMGetOperand(inst, i)); }; ++ auto texturePointer = operand(0); ++ auto pointerType = *builder.reverseLookupType(builder.lookupResultType(texturePointer)); ++ auto imageTypeID = pointerType.targetType; ++ auto imageType = *builder.reverseLookupType(imageTypeID); ++ auto image = builder.encodeLoad(imageTypeID, texturePointer); ++ unsigned dims = imageType.imageDimensionality == Dim::e1D ? 1 : (imageType.imageDimensionality == Dim::e2D ? 2 : 3); ++ bool array = imageType.imageIsArrayed; ++ auto llvmResult = DynamicLLVM::LLVMTypeOf(inst); ++ bool wrapped = DynamicLLVM::LLVMGetTypeKind(llvmResult) == LLVMStructTypeKind; ++ auto valueTypeID = wrapped ? llvmTypeToSPIRVType(builder, DynamicLLVM::LLVMStructGetTypeAtIndex(llvmResult,0)) : resultType; ++ auto finish = [&](ResultID value) { ++ if (!wrapped) { result=value; return; } ++ auto partial=builder.encodeCompositeInsert(resultType,value,builder.declareUndefinedValue(resultType),{0}); ++ result=builder.encodeCompositeInsert(resultType,builder.declareConstantScalar(0),partial,{1}); ++ }; ++ if (width || height) { ++ builder.requireCapability(Capability::ImageQuery); ++ auto component=builder.declareType(Type(Type::IntegerTag{},32,true)); ++ auto sizeType=builder.declareType(Type(Type::VectorTag{},dims+(array?1:0),component,4*(dims+(array?1:0)),4)); ++ auto sizes=builder.encodeRawOp(103,sizeType,{image,operand(1)}); // OpImageQuerySizeLod ++ result=builder.encodeCompositeExtract(resultType,sizes,{height?1u:0u});return true; ++ } ++ auto coordinates = operand(compare?3:(sample?2:1)); ++ auto coordinateType = *builder.reverseLookupType(builder.lookupResultType(coordinates)); ++ if (array) { ++ std::vector components; ++ for(unsigned i=0;i args{sampled,coordinates}; ++ if(compare)args.push_back(operand(4)); ++ auto offset=cube?nullptr:DynamicLLVM::LLVMGetOperand(inst,flagIndex-1); ++ bool hasOffset=offset && !DynamicLLVM::LLVMIsNull(offset); ++ unsigned mask=(explicitLod?2:0)|(hasOffset?8:0); ++ if(mask){args.push_back(mask);if(explicitLod)args.push_back(operand(flagIndex+1));if(hasOffset)args.push_back(llvmValueToResultID(builder,offset));} ++ value=builder.encodeRawOp(compare?(explicitLod?90:89):(explicitLod?88:87),compare?valueTypeID:vectorType,args); ++ if(compare){finish(value);return true;} ++ } else if(read) { ++ std::vector args{image,coordinates}; ++ if(imageType.imageIsSampledIndication==1){args.push_back(2);args.push_back(operand(array?3:2));} ++ value=builder.encodeRawOp(imageType.imageIsSampledIndication==1?95:98,vectorType,args); ++ } else { ++ builder.encodeRawOpNoResult(99,{image,coordinates,operand(array?3:2)});result=ResultIDInvalid;return true; ++ } ++ if(vectorType!=valueTypeID) { ++ auto target=*builder.reverseLookupType(valueTypeID); ++ auto component=*builder.reverseLookupType(target.targetType); ++ value=component.backingType==Type::BackingType::FloatingPoint && component.scalarWidth==16 ? builder.encodeFConvert(valueTypeID,value) : builder.encodeBitcast(valueTypeID,value); ++ } ++ finish(value);return true; ++} ++ ++// LLVM integers are signless; SPIR-V conversion instructions require explicit signedness. ++static Iridium::SPIRV::ResultID integerTypeWithSign(Iridium::SPIRV::Builder& builder,Iridium::SPIRV::ResultID type,bool sign){ ++ using namespace Iridium::SPIRV;auto value=*builder.reverseLookupType(type); ++ if(value.backingType==Type::BackingType::Vector){auto component=integerTypeWithSign(builder,value.targetType,sign);return builder.declareType(Type(Type::VectorTag{},value.entryCount,component,value.size,value.alignment));} ++ return builder.declareType(Type(Type::IntegerTag{},value.scalarWidth,sign)); ++} ++// Translates the AIR math, conversion, derivative and discard intrinsics. Returns false when `name` ++// is not one of them. `terminatesBlock` is set for instructions that end the SPIR-V block (discard). ++static bool translateAirIntrinsic(Iridium::SPIRV::Builder& builder, LLVMValueRef inst, std::string_view name, Iridium::SPIRV::ResultID type, Iridium::SPIRV::ResultID& resID, bool& terminatesBlock) { ++ using namespace Iridium::SPIRV; ++ ++ auto operand = [&](unsigned index) { ++ return llvmValueToResultID(builder, DynamicLLVM::LLVMGetOperand(inst, index)); ++ }; ++ ++ if (name == "air.discard_fragment") { ++ builder.encodeRawOpNoResult(SPIRVOp::Kill, {}); ++ terminatesBlock = true; ++ resID = ResultIDInvalid; ++ return true; ++ } ++ ++ auto parts = splitAirName(name); ++ if (parts.size() < 3 || parts[0] != "air") { ++ return false; ++ } ++ ++ std::string_view fn = parts[1]; ++ if (fn.substr(0, 5) == "fast_") { ++ fn.remove_prefix(5); ++ } else if (fn.substr(0, 8) == "precise_") { ++ fn.remove_prefix(8); ++ } ++ ++ // floating-point functions: air.. ++ if (parts.size() == 3 && airTypeTokenIsFloat(parts[2])) { ++ struct MathOp { ++ std::string_view name; ++ GLSLOpcode opcode; ++ unsigned arity; ++ }; ++ static const MathOp mathOps[] = { ++ { "sqrt", GLSLOpcode::Sqrt, 1 }, { "rsqrt", GLSLOpcode::InverseSqrt, 1 }, { "pow", GLSLOpcode::Pow, 2 }, ++ { "powr", GLSLOpcode::Pow, 2 }, { "exp", GLSLOpcode::Exp, 1 }, { "exp2", GLSLOpcode::Exp2, 1 }, ++ { "log", GLSLOpcode::Log, 1 }, { "log2", GLSLOpcode::Log2, 1 }, { "sin", GLSLOpcode::Sin, 1 }, ++ { "cos", GLSLOpcode::Cos, 1 }, { "tan", GLSLOpcode::Tan, 1 }, { "asin", GLSLOpcode::Asin, 1 }, ++ { "acos", GLSLOpcode::Acos, 1 }, { "atan", GLSLOpcode::Atan, 1 }, { "atan2", GLSLOpcode::Atan2, 2 }, ++ { "sinh", GLSLOpcode::Sinh, 1 }, { "cosh", GLSLOpcode::Cosh, 1 }, { "tanh", GLSLOpcode::Tanh, 1 }, ++ { "fabs", GLSLOpcode::FAbs, 1 }, { "floor", GLSLOpcode::Floor, 1 }, { "ceil", GLSLOpcode::Ceil, 1 }, ++ { "fract", GLSLOpcode::Fract, 1 }, { "rint", GLSLOpcode::RoundEven, 1 }, { "round", GLSLOpcode::Round, 1 }, ++ { "trunc", GLSLOpcode::Trunc, 1 }, { "sign", GLSLOpcode::FSign, 1 }, { "fmin", GLSLOpcode::FMin, 2 }, ++ { "fmax", GLSLOpcode::FMax, 2 }, { "fma", GLSLOpcode::Fma, 3 }, { "mix", GLSLOpcode::FMix, 3 }, ++ { "clamp", GLSLOpcode::FClamp, 3 }, { "step", GLSLOpcode::Step, 2 }, { "smoothstep", GLSLOpcode::SmoothStep, 3 }, ++ { "normalize", GLSLOpcode::Normalize, 1 }, { "length", GLSLOpcode::Length, 1 }, { "distance", GLSLOpcode::Distance, 2 }, ++ { "cross", GLSLOpcode::Cross, 2 }, { "reflect", GLSLOpcode::Reflect, 2 }, ++ }; ++ ++ for (auto& op: mathOps) { ++ if (op.name == fn) { ++ std::vector arguments; ++ for (unsigned i = 0; i < op.arity; ++i) { ++ arguments.push_back(operand(i)); ++ } ++ resID = builder.encodeGLSLOp(op.opcode, type, arguments); ++ return true; ++ } ++ } ++ ++ if (fn == "fmod") { ++ resID = builder.encodeRawOp(SPIRVOp::FRem, type, { operand(0), operand(1) }); ++ return true; ++ } ++ if (fn == "dot") { ++ resID = builder.encodeArithBinop(Opcode::Dot, type, operand(0), operand(1)); ++ return true; ++ } ++ if (fn == "dfdx" || fn == "dfdy" || fn == "fwidth") { ++ resID = builder.encodeRawOp(fn == "dfdx" ? SPIRVOp::DPdx : (fn == "dfdy" ? SPIRVOp::DPdy : SPIRVOp::Fwidth), type, { operand(0) }); ++ return true; ++ } ++ return false; ++ } ++ ++ // boolean vector reductions: air.all.v2i1, air.any.v4i1 ++ if (parts.size() == 3 && (fn == "all" || fn == "any")) { ++ resID = builder.encodeRawOp(fn == "all" ? SPIRVOp::All : SPIRVOp::Any, type, { operand(0) }); ++ return true; + } ++ ++ // integer functions: air... ++ if (parts.size() == 4 && (parts[2] == "s" || parts[2] == "u")) { ++ bool isSigned = parts[2] == "s"; ++ ++ if (fn == "min" || fn == "max") { ++ auto opcode = fn == "min" ? (isSigned ? GLSLOpcode::SMin : GLSLOpcode::UMin) : (isSigned ? GLSLOpcode::SMax : GLSLOpcode::UMax); ++ resID = builder.encodeGLSLOp(opcode, type, { operand(0), operand(1) }); ++ return true; ++ } ++ if (fn == "clamp") { ++ resID = builder.encodeGLSLOp(isSigned ? GLSLOpcode::SClamp : GLSLOpcode::UClamp, type, { operand(0), operand(1), operand(2) }); ++ return true; ++ } ++ if (fn == "abs") { ++ resID = builder.encodeGLSLOp(GLSLOpcode::SAbs, type, { operand(0) }); ++ return true; ++ } ++ return false; ++ } ++ ++ // conversions: air.convert.... ++ if (parts.size() == 6 && fn == "convert") { ++ auto dstKind = parts[2]; ++ auto dstType = parts[3]; ++ auto srcKind = parts[4]; ++ auto srcType = parts[5]; ++ auto argument = operand(0); ++ ++ if (srcType == "i1") { ++ // bool to number: 1 or 0 ++ auto one = dstKind == "f" ? builder.declareConstantScalar(1) : builder.declareConstantScalar(1); ++ auto zero = dstKind == "f" ? builder.declareConstantScalar(0) : builder.declareConstantScalar(0); ++ resID = builder.encodeRawOp(SPIRVOp::Select, type, { argument, one, zero }); ++ return true; ++ } ++ ++ if (dstKind == "f" && srcKind == "s") { ++ resID = builder.encodeRawOp(SPIRVOp::ConvertSToF, type, { argument }); ++ } else if (dstKind == "f" && srcKind == "u") { ++ auto argType=llvmTypeToSPIRVType(builder,DynamicLLVM::LLVMTypeOf(DynamicLLVM::LLVMGetOperand(inst,0))); ++ auto unsignedType=integerTypeWithSign(builder,argType,false); ++ resID=builder.encodeRawOp(SPIRVOp::ConvertUToF,type,{builder.encodeBitcast(unsignedType,argument)}); ++ } else if (dstKind == "s" && srcKind == "f") { ++ resID = builder.encodeRawOp(SPIRVOp::ConvertFToS, type, { argument }); ++ } else if (dstKind == "u" && srcKind == "f") { ++ auto unsignedType=integerTypeWithSign(builder,type,false); ++ resID=builder.encodeBitcast(type,builder.encodeRawOp(SPIRVOp::ConvertFToU,unsignedType,{argument})); ++ } else if (dstKind == "f" && srcKind == "f") { ++ // same type: nothing to convert ++ resID = (dstType == srcType) ? argument : builder.encodeRawOp(SPIRVOp::FConvert, type, { argument }); ++ } else if ((dstKind == "s" || dstKind == "u") && (srcKind == "s" || srcKind == "u")) { ++ resID = (dstType == srcType) ? argument : builder.encodeRawOp(dstKind == "s" ? SPIRVOp::SConvert : SPIRVOp::UConvert, type, { argument }); ++ } else { ++ return false; ++ } ++ return true; ++ } ++ ++ return false; + }; + + void Iridium::AIR::Function::analyze(SPIRV::Builder& builder, OutputInfo& outputInfo) { +@@ -312,7 +778,7 @@ void Iridium::AIR::Function::analyze(SPIRV::Builder& builder, OutputInfo& output + }); + + // declare the GLSL PerVertex structure +- auto perVertexStructType = builder.declareType(SPIRV::Type(SPIRV::Type::StructureTag {}, { ++ SPIRV::Type perVertexType(SPIRV::Type::StructureTag {}, { + SPIRV::Type::Member { + vec4Type, + 0, +@@ -321,13 +787,21 @@ void Iridium::AIR::Function::analyze(SPIRV::Builder& builder, OutputInfo& output + } + }, + // TODO: declare the rest of the structure +- }, floatTypeInst.size * 4, floatTypeInst.alignment)); ++ }, floatTypeInst.size * 4, floatTypeInst.alignment); ++ perVertexType.layoutFree = true; ++ auto perVertexStructType = builder.declareType(perVertexType); + auto perVertexStructPtrType = builder.declareType(SPIRV::Type(SPIRV::Type::PointerTag {}, SPIRV::StorageClass::Output, perVertexStructType, 8)); + perVertexVar = builder.addGlobalVariable(perVertexStructPtrType, SPIRV::StorageClass::Output); + + builder.addDecoration(perVertexStructType, SPIRV::Decoration { SPIRV::DecorationType::Block }); + + builder.referenceGlobalVariable(perVertexVar); ++ // Indium also creates a point-topology pipeline; Vulkan requires PointSize to be written. ++ auto pointSizePtr = builder.declareType(SPIRV::Type(SPIRV::Type::PointerTag {}, SPIRV::StorageClass::Output, floatType, 8)); ++ auto pointSizeVar = builder.addGlobalVariable(pointSizePtr, SPIRV::StorageClass::Output); ++ builder.addDecoration(pointSizeVar, SPIRV::Decoration { SPIRV::DecorationType::Builtin, { static_cast(SPIRV::BuiltinID::PointSize) } }); ++ builder.referenceGlobalVariable(pointSizeVar); ++ builder.encodeStore(pointSizeVar, builder.declareConstantScalar(1.0f)); + + // declare the GLSL VertexIndex input variable + vertexIndexVar = builder.addGlobalVariable(ptrIntType, SPIRV::StorageClass::Input); +@@ -425,7 +899,7 @@ void Iridium::AIR::Function::analyze(SPIRV::Builder& builder, OutputInfo& output + // if the function returns a structure, we need to separate the components + // into separate output variables. + +- auto llfuncType = DynamicLLVM::LLVMGetElementType(DynamicLLVM::LLVMTypeOf(_function)); ++ auto llfuncType = DynamicLLVM::LLVMGlobalGetValueType(_function); + auto funcRetType = DynamicLLVM::LLVMGetReturnType(llfuncType); + std::vector funcParamTypes(DynamicLLVM::LLVMCountParamTypes(llfuncType)); + DynamicLLVM::LLVMGetParamTypes(llfuncType, funcParamTypes.data()); +@@ -460,7 +934,7 @@ void Iridium::AIR::Function::analyze(SPIRV::Builder& builder, OutputInfo& output + auto type = llvmTypeToSPIRVType(builder, DynamicLLVM::LLVMStructGetTypeAtIndex(funcRetType, i)); + auto ptrType = builder.declareType(SPIRV::Type(SPIRV::Type::PointerTag {}, SPIRV::StorageClass::Output, type, 8)); + auto var = builder.addGlobalVariable(ptrType, SPIRV::StorageClass::Output); +- builder.addDecoration(var, SPIRV::Decoration { SPIRV::DecorationType::Location, { location } }); ++ builder.addDecoration(var, SPIRV::Decoration { SPIRV::DecorationType::Location, { airInterfaceLocation(returnValueMemberInfo, location) } }); + ++location; + _outputValueIDs.push_back(var); + builder.referenceGlobalVariable(var); +@@ -472,13 +946,60 @@ void Iridium::AIR::Function::analyze(SPIRV::Builder& builder, OutputInfo& output + auto type = llvmTypeToSPIRVType(builder, funcRetType); + auto ptrType = builder.declareType(SPIRV::Type(SPIRV::Type::PointerTag {}, SPIRV::StorageClass::Output, type, 8)); + auto var = builder.addGlobalVariable(ptrType, SPIRV::StorageClass::Output); +- builder.addDecoration(var, SPIRV::Decoration { SPIRV::DecorationType::Location, { 0 } }); ++ uint32_t outputLocation = 0; ++ if (!returnValueOperands.empty()) { ++ std::vector info(DynamicLLVM::LLVMGetMDNodeNumOperands(returnValueOperands[0])); ++ DynamicLLVM::LLVMGetMDNodeOperands(returnValueOperands[0], info.data()); ++ outputLocation = airInterfaceLocation(info, 0); ++ } ++ builder.addDecoration(var, SPIRV::Decoration { SPIRV::DecorationType::Location, { outputLocation } }); + _outputValueIDs.push_back(var); + builder.referenceGlobalVariable(var); + } + + std::vector parameterOperands(DynamicLLVM::LLVMGetMDNodeNumOperands(rootInfoOperands[2])); + DynamicLLVM::LLVMGetMDNodeOperands(rootInfoOperands[2], parameterOperands.data()); ++ // Preserve AIR reflection metadata before translating resource bindings. ++ for (auto parameter : parameterOperands) { ++ std::vector info(DynamicLLVM::LLVMGetMDNodeNumOperands(parameter)); ++ DynamicLLVM::LLVMGetMDNodeOperands(parameter, info.data()); ++ if (info.size() < 2) continue; ++ auto kind = llvmMDStringToStringView(info[1]); ++ ArgumentInfo arg; ++ if (kind == "air.buffer") arg.type = BindingType::Buffer; ++ else if (kind == "air.texture") arg.type = BindingType::Texture; ++ else if (kind == "air.sampler") arg.type = BindingType::Sampler; ++ else continue; ++ for (size_t i = 2; i + 1 < info.size(); ++i) { ++ if (!DynamicLLVM::LLVMIsAMDString(info[i])) continue; ++ auto key = llvmMDStringToStringView(info[i]); ++ if (key == "air.location_index") arg.index = DynamicLLVM::LLVMConstIntGetZExtValue(info[i+1]); ++ else if (key == "air.arg_name") arg.name = llvmMDStringToStringView(info[i+1]); ++ else if (key == "air.arg_type_name") arg.typeName = llvmMDStringToStringView(info[i+1]); ++ else if (key == "air.arg_type_size") arg.size = DynamicLLVM::LLVMConstIntGetZExtValue(info[i+1]); ++ else if (key == "air.arg_type_align_size") arg.alignment = DynamicLLVM::LLVMConstIntGetZExtValue(info[i+1]); ++ else if (key == "air.struct_type_info") { ++ std::vector members(DynamicLLVM::LLVMGetMDNodeNumOperands(info[i+1])); ++ DynamicLLVM::LLVMGetMDNodeOperands(info[i+1], members.data()); ++ // AIR stores each direct member as offset, size, array length, type, name. A member of ++ // struct type is preceded by an "air.struct_type_info" key and the node describing it. ++ // Reflection is informational, so anything unrecognized stops the walk instead of failing. ++ for (size_t j = 0; j < members.size();) { ++ if (DynamicLLVM::LLVMIsAMDString(members[j])) { ++ j += 2; ++ continue; ++ } ++ if (j + 4 >= members.size() || !DynamicLLVM::LLVMIsAMDString(members[j+3]) || !DynamicLLVM::LLVMIsAMDString(members[j+4])) ++ break; ++ arg.members.push_back({static_cast(DynamicLLVM::LLVMConstIntGetZExtValue(members[j])), ++ std::string(llvmMDStringToStringView(members[j+3])), std::string(llvmMDStringToStringView(members[j+4]))}); ++ j += 5; ++ } ++ } ++ } ++ funcInfo.arguments.push_back(std::move(arg)); ++ } ++ + + // + // analyze parameters and mark special values (like the vertex ID) +@@ -519,7 +1040,9 @@ void Iridium::AIR::Function::analyze(SPIRV::Builder& builder, OutputInfo& output + auto kind = llvmMDStringToStringView(parameterInfo[1]); + + if (kind == "air.buffer") { +- auto type = llvmTypeToSPIRVType(builder, DynamicLLVM::LLVMGetElementType(funcParamTypes[i])); ++ translationInstruction=nullptr; ++ translationContext = "buffer parameter " + std::to_string(i) + " of " + _name; ++ auto type = llvmTypeToSPIRVType(builder, bufferElementType(_function, i)); + auto addrPtrTypeInst = SPIRV::Type(SPIRV::Type::PointerTag {}, SPIRV::StorageClass::PhysicalStorageBuffer, type, 8); + auto addrPtrType = builder.declareType(addrPtrTypeInst); + bufferMembers.push_back(SPIRV::Type::Member { addrPtrType, 8 * bufferIndex, {} }); +@@ -575,7 +1098,21 @@ void Iridium::AIR::Function::analyze(SPIRV::Builder& builder, OutputInfo& output + _parameterIDs.push_back(load); + builder.associateExistingResultID(load, llparamVal); + builder.setResultType(load, intType); +- } else if (kind == "air.buffer") { ++ } else if (kind == "air.instance_id") { ++ auto var=builder.addGlobalVariable(ptrIntType,SPIRV::StorageClass::Input); ++ builder.addDecoration(var,SPIRV::Decoration{SPIRV::DecorationType::Builtin,{43}});builder.referenceGlobalVariable(var); ++ auto load=builder.encodeLoad(intType,var);_parameterIDs.push_back(load);builder.associateExistingResultID(load,llparamVal);builder.setResultType(load,intType); ++ } else if (kind == "air.front_facing") { ++ auto boolType = builder.declareType(SPIRV::Type(SPIRV::Type::BooleanTag{})); ++ auto boolPtrType = builder.declareType(SPIRV::Type(SPIRV::Type::PointerTag{}, SPIRV::StorageClass::Input, boolType, 8)); ++ auto var = builder.addGlobalVariable(boolPtrType, SPIRV::StorageClass::Input); ++ builder.addDecoration(var, SPIRV::Decoration{SPIRV::DecorationType::Builtin, {17}}); // FrontFacing ++ builder.referenceGlobalVariable(var); ++ auto load = builder.encodeLoad(boolType, var); ++ _parameterIDs.push_back(load); ++ builder.associateExistingResultID(load, llparamVal); ++ builder.setResultType(load, boolType); ++ } else if (kind == "air.buffer") { + // find the location index info + size_t infoIdx = 0; + for (; infoIdx < parameterInfo.size(); ++infoIdx) { +@@ -620,7 +1157,7 @@ void Iridium::AIR::Function::analyze(SPIRV::Builder& builder, OutputInfo& output + auto var = builder.addGlobalVariable(ptrType, SPIRV::StorageClass::Input); + auto load = builder.encodeLoad(type, var); + +- builder.addDecoration(var, SPIRV::Decoration { SPIRV::DecorationType::Location, { paramLocation } }); ++ builder.addDecoration(var, SPIRV::Decoration { SPIRV::DecorationType::Location, { airInterfaceLocation(parameterInfo, paramLocation) } }); + ++paramLocation; + + builder.referenceGlobalVariable(var); +@@ -631,9 +1168,18 @@ void Iridium::AIR::Function::analyze(SPIRV::Builder& builder, OutputInfo& output + builder.setResultType(load, type); + } else if (kind == "air.vertex_input") { + auto type = llvmTypeToSPIRVType(builder, funcParamTypes[i]); +- auto ptrType = builder.declareType(SPIRV::Type(SPIRV::Type::PointerTag {}, SPIRV::StorageClass::Input, type, 8)); ++ auto inputType = type; ++ // LLVM integers have no sign; AIR metadata preserves the interface's unsigned type. ++ for (size_t j = 0; j + 1 < parameterInfo.size(); ++j) { ++ if (llvmMDStringToStringView(parameterInfo[j]) != "air.arg_type_name") continue; ++ auto name = llvmMDStringToStringView(parameterInfo[j + 1]); ++ if (name.substr(0, 4) == "uint" || name.substr(0, 6) == "ushort" || name.substr(0, 5) == "uchar" || name.substr(0, 5) == "ulong") ++ inputType = integerTypeWithSign(builder, type, false); ++ } ++ auto ptrType = builder.declareType(SPIRV::Type(SPIRV::Type::PointerTag {}, SPIRV::StorageClass::Input, inputType, 8)); + auto var = builder.addGlobalVariable(ptrType, SPIRV::StorageClass::Input); +- auto load = builder.encodeLoad(type, var); ++ auto load = builder.encodeLoad(inputType, var); ++ if (inputType != type) load = builder.encodeBitcast(type, load); + + // find the location index info + size_t infoIdx = 0; +@@ -761,14 +1307,19 @@ void Iridium::AIR::Function::analyze(SPIRV::Builder& builder, OutputInfo& output + realSampleType = fakeSampleType; + } + +- // TODO: same here (find a better way...) +- if (textureClassName == "texture2d") { +- dimensionality = SPIRV::Dim::e2D; +- } else if (textureClassName == "texturecube") { +- dimensionality = SPIRV::Dim::eCube; +- } +- +- auto imageType = builder.declareType(SPIRV::Type(SPIRV::Type::ImageTag {}, fakeSampleType, realSampleType, dimensionality, 2, false, false, accessType == TextureAccessType::Sample ? 1 : 2, SPIRV::ImageFormat::Unknown)); ++ if (sampleTypeName == "uint" || sampleTypeName == "int") { ++ fakeSampleType = builder.declareType(SPIRV::Type(SPIRV::Type::IntegerTag {},32,sampleTypeName=="int")); ++ realSampleType = fakeSampleType; ++ } ++ if (fakeSampleType == SPIRV::ResultIDInvalid) throw std::runtime_error("Unsupported AIR texture sample type"); ++ bool arrayed = textureClassName.find("_array") != std::string_view::npos; ++ if ((textureClassName.find("texture2d") == 0 || textureClassName.find("depth2d") == 0)) dimensionality = SPIRV::Dim::e2D; ++ else if ((textureClassName.find("texturecube") == 0 || textureClassName.find("depthcube") == 0)) dimensionality = SPIRV::Dim::eCube; ++ else if (textureClassName == "texture3d") dimensionality = SPIRV::Dim::e3D; ++ else if (textureClassName == "texture1d") dimensionality = SPIRV::Dim::e1D; ++ else throw std::runtime_error("Unsupported AIR texture dimension: " + std::string(textureClassName)); ++ ++ auto imageType = builder.declareType(SPIRV::Type(SPIRV::Type::ImageTag {}, fakeSampleType, realSampleType, dimensionality, textureClassName.find("depth")==0?1:0, arrayed, false, accessType == TextureAccessType::Sample ? 1 : 2, SPIRV::ImageFormat::Unknown)); + auto imagePtrType = builder.declareType(SPIRV::Type(SPIRV::Type::PointerTag {}, SPIRV::StorageClass::UniformConstant, imageType, 8)); + auto var = builder.addGlobalVariable(imagePtrType, SPIRV::StorageClass::UniformConstant); + //auto load = builder.encodeLoad(imageType, var); +@@ -959,68 +1510,98 @@ void Iridium::AIR::Function::analyze(SPIRV::Builder& builder, OutputInfo& output + enum class CFGType { + None = 0, + Selection, ++ Loop, + }; + + struct CFGInfo { + CFGType type = CFGType::None; + SPIRV::ResultID selectionMergeBlock = SPIRV::ResultIDInvalid; ++ SPIRV::ResultID continueBlock = SPIRV::ResultIDInvalid; + }; + + std::unordered_map cfgInfos; + +- for (LLVMBasicBlockRef bb = DynamicLLVM::LLVMGetFirstBasicBlock(_function); bb != nullptr; bb = DynamicLLVM::LLVMGetNextBasicBlock(bb)) { +- auto lastInst = DynamicLLVM::LLVMGetLastInstruction(bb); +- auto lastInstOpcode = DynamicLLVM::LLVMGetInstructionOpcode(lastInst); +- +- CFGInfo cfgInfo {}; +- +- if (lastInstOpcode == LLVMBr && DynamicLLVM::LLVMIsConditional(lastInst)) { +- // check if this is a basic conditional/selection (i.e. an `if`) +- auto firstBlock = DynamicLLVM::LLVMGetSuccessor(lastInst, 0); +- auto secondBlock = DynamicLLVM::LLVMGetSuccessor(lastInst, 1); +- +- auto lastInstFirstBlock = DynamicLLVM::LLVMGetLastInstruction(firstBlock); +- auto lastInstSecondBlock = DynamicLLVM::LLVMGetLastInstruction(secondBlock); +- auto lastInstOpcodeFirstBlock = DynamicLLVM::LLVMGetInstructionOpcode(lastInstFirstBlock); +- auto lastInstOpcodeSecondBlock = DynamicLLVM::LLVMGetInstructionOpcode(lastInstSecondBlock); +- +- SPIRV::ResultID mergeBlock = SPIRV::ResultIDInvalid; +- +- // check if the first block is the special block +- if (lastInstOpcodeFirstBlock == LLVMBr && !DynamicLLVM::LLVMIsConditional(lastInstFirstBlock)) { +- // make sure the target block is the same +- auto block = DynamicLLVM::LLVMGetSuccessor(lastInstFirstBlock, 0); +- +- if (block == secondBlock) { +- // perfect, the second block is the merge block then +- mergeBlock = builder.lookupResultID(reinterpret_cast(secondBlock)); +- } +- } else if (lastInstOpcodeSecondBlock == LLVMBr && !DynamicLLVM::LLVMIsConditional(lastInstSecondBlock)) { +- // make sure the target block is the same +- auto block = DynamicLLVM::LLVMGetSuccessor(lastInstSecondBlock, 0); +- +- if (block == firstBlock) { +- // perfect, the first block is the merge block then +- mergeBlock = builder.lookupResultID(reinterpret_cast(firstBlock)); +- } +- } +- +- if (mergeBlock != SPIRV::ResultIDInvalid) { +- cfgInfo.type = CFGType::Selection; +- cfgInfo.selectionMergeBlock = mergeBlock; +- } +- } +- +- if (cfgInfo.type != CFGType::None) { +- cfgInfos[builder.lookupResultID(reinterpret_cast(bb))] = cfgInfo; +- } +- } +- ++ std::vector blocks; ++ for(auto bb=DynamicLLVM::LLVMGetFirstBasicBlock(_function);bb;bb=DynamicLLVM::LLVMGetNextBasicBlock(bb))blocks.push_back(bb); ++ std::set all(blocks.begin(),blocks.end()); ++ std::unordered_map> postdom; ++ for(auto bb:blocks){auto term=DynamicLLVM::LLVMGetLastInstruction(bb);postdom[bb]=DynamicLLVM::LLVMGetNumSuccessors(term)?all:std::set{bb};} ++ bool changed=true; ++ while(changed){changed=false;for(auto bb:blocks){ ++ auto term=DynamicLLVM::LLVMGetLastInstruction(bb);auto count=DynamicLLVM::LLVMGetNumSuccessors(term); ++ if(!count)continue; ++ auto next=postdom[DynamicLLVM::LLVMGetSuccessor(term,0)]; ++ for(unsigned i=1;i common;auto& other=postdom[DynamicLLVM::LLVMGetSuccessor(term,i)];std::set_intersection(next.begin(),next.end(),other.begin(),other.end(),std::inserter(common,common.begin()));next=std::move(common);} ++ next.insert(bb);if(next!=postdom[bb]){postdom[bb]=std::move(next);changed=true;} ++ }} ++ for(auto bb:blocks){auto term=DynamicLLVM::LLVMGetLastInstruction(bb); ++ if(DynamicLLVM::LLVMGetInstructionOpcode(term)!=LLVMBr || !DynamicLLVM::LLVMIsConditional(term))continue; ++ LLVMBasicBlockRef merge=nullptr; ++ for(auto candidate:postdom[bb])if(candidate!=bb && (!merge || postdom[candidate].size()>postdom[merge].size()))merge=candidate; ++ if(merge)cfgInfos[builder.lookupResultID(reinterpret_cast(bb))]={CFGType::Selection,builder.lookupResultID(reinterpret_cast(merge))}; ++ } ++ ++ // Identify natural loop back edges from dominators; their headers need OpLoopMerge. ++ std::unordered_map splitContinues; ++ std::unordered_map> splitLoopSelections; ++ std::unordered_map> predecessors,dom; ++ for(auto bb:blocks){auto term=DynamicLLVM::LLVMGetLastInstruction(bb);for(unsigned i=0;i{bb}:all;} ++ changed=true;while(changed){changed=false;for(auto bb:blocks){if(bb==blocks.front())continue;auto next=all;for(auto pred:predecessors[bb]){std::set common;std::set_intersection(next.begin(),next.end(),dom[pred].begin(),dom[pred].end(),std::inserter(common,common.begin()));next=std::move(common);}next.insert(bb);if(next!=dom[bb]){dom[bb]=std::move(next);changed=true;}}} ++ for(auto latch:blocks){auto term=DynamicLLVM::LLVMGetLastInstruction(latch);for(unsigned i=0;i loop{header,latch};std::vector pending;if(latch!=header)pending.push_back(latch); ++ while(!pending.empty()){auto block=pending.back();pending.pop_back();for(auto pred:predecessors[block])if(loop.insert(pred).second && pred!=header)pending.push_back(pred);} ++ LLVMBasicBlockRef merge=nullptr;for(auto candidate:postdom[header])if(!loop.count(candidate) && (!merge || postdom[candidate].size()>postdom[merge].size()))merge=candidate; ++ if(!merge)throw std::runtime_error("AIR loop has no supported merge block"); ++ auto headerID=builder.lookupResultID(reinterpret_cast(header));auto latchID=builder.lookupResultID(reinterpret_cast(latch)); ++ auto existing=cfgInfos.find(headerID);if(existing!=cfgInfos.end() && existing->second.type==CFGType::Loop && existing->second.continueBlock!=latchID)throw std::runtime_error("AIR loop has multiple continue blocks"); ++ auto continueID=builder.reserveResultID();splitContinues[latch]=continueID; ++ cfgInfos[headerID]={CFGType::Loop,builder.lookupResultID(reinterpret_cast(merge)),continueID}; ++ // Within a loop, a path that breaks leaves the enclosing construct. Ignore that ++ // path when selecting the merge for an inner selection, and stop at the continue. ++ std::unordered_map> local; ++ for(auto block:loop)local[block]=block==latch?std::set{block}:loop; ++ bool localChanged=true;while(localChanged){localChanged=false;for(auto block:loop){if(block==latch)continue; ++ auto term=DynamicLLVM::LLVMGetLastInstruction(block);auto next=loop; ++ for(unsigned j=0;j common;std::set_intersection(next.begin(),next.end(),local[target].begin(),local[target].end(),std::inserter(common,common.begin()));next=std::move(common);} ++ next.insert(block);if(next!=local[block]){local[block]=std::move(next);localChanged=true;} ++ }} ++ auto headerTerm=DynamicLLVM::LLVMGetLastInstruction(header); ++ if(DynamicLLVM::LLVMGetInstructionOpcode(headerTerm)==LLVMBr && DynamicLLVM::LLVMIsConditional(headerTerm) && loop.count(DynamicLLVM::LLVMGetSuccessor(headerTerm,0)) && loop.count(DynamicLLVM::LLVMGetSuccessor(headerTerm,1))){ ++ LLVMBasicBlockRef innerMerge=nullptr;for(auto candidate:local[header])if(candidate!=header && local[candidate]!=loop && (!innerMerge || local[candidate].size()>local[innerMerge].size()))innerMerge=candidate; ++ if(!innerMerge)throw std::runtime_error("AIR loop header selection has no merge"); ++ auto selectionBlock=builder.reserveResultID();splitLoopSelections[header]={selectionBlock,builder.lookupResultID(reinterpret_cast(innerMerge))}; ++ } ++ for(auto block:loop){auto id=builder.lookupResultID(reinterpret_cast(block));auto found=cfgInfos.find(id);if(found==cfgInfos.end() || found->second.type!=CFGType::Selection)continue; ++ if(block==latch){cfgInfos.erase(found);continue;} ++ LLVMBasicBlockRef innerMerge=nullptr;for(auto candidate:local[block])if(candidate!=block && candidate!=header && local[candidate]!=loop && (!innerMerge || local[candidate].size()>local[innerMerge].size()))innerMerge=candidate; ++ if(innerMerge)found->second.selectionMergeBlock=builder.lookupResultID(reinterpret_cast(innerMerge)); ++ } ++ ++ }} ++ // A selection that exits an enclosing construct can use an unreachable merge. ++ // Distinct constructs cannot name the same merge block in structured SPIR-V. ++ std::vector unreachableMerges; ++ std::set claimedMerges; ++ auto headers=blocks;std::sort(headers.begin(),headers.end(),[&](auto a,auto b){return dom[a].size()(bb)));if(found==cfgInfos.end())continue; ++ auto& cfg=found->second;if(!claimedMerges.insert(cfg.selectionMergeBlock).second){auto id=builder.reserveResultID();cfg.selectionMergeBlock=id;unreachableMerges.push_back(id);} ++ } ++ // SPIR-V requires non-PHI definitions to precede their uses. AIR block order need not do so. ++ std::set visited;std::vector ordered; ++ std::function visit=[&](auto bb){if(!visited.insert(bb).second)return;auto term=DynamicLLVM::LLVMGetLastInstruction(bb);for(unsigned i=0;i(inst));builder.setResultType(id,llvmTypeToSPIRVType(builder,DynamicLLVM::LLVMTypeOf(inst)));} + // + // translate instructions + // + isFirst = true; +- for (LLVMBasicBlockRef bb = DynamicLLVM::LLVMGetFirstBasicBlock(_function); bb != nullptr; bb = DynamicLLVM::LLVMGetNextBasicBlock(bb)) { ++ for (auto bb : blocks) { + if (isFirst) { + isFirst = false; + +@@ -1030,16 +1611,54 @@ void Iridium::AIR::Function::analyze(SPIRV::Builder& builder, OutputInfo& output + builder.insertLabel(builder.lookupResultID(reinterpret_cast(bb))); + } + ++ bool blockTerminated = false; ++ + for (LLVMValueRef inst = DynamicLLVM::LLVMGetFirstInstruction(bb); inst != nullptr; inst = DynamicLLVM::LLVMGetNextInstruction(inst)) { ++ translationInstruction = inst; ++ // OpKill ends its block; whatever follows it in the AIR block (its branch) is dead ++ if (blockTerminated) { ++ break; ++ } ++ + auto opcode = DynamicLLVM::LLVMGetInstructionOpcode(inst); + + switch (opcode) { ++ case LLVMSExt: case LLVMTrunc: case LLVMFPToUI: case LLVMFPToSI: ++ case LLVMUIToFP: case LLVMSIToFP: case LLVMFPExt: { ++ auto source=DynamicLLVM::LLVMGetOperand(inst,0);auto arg=llvmValueToResultID(builder,source); ++ auto type=llvmTypeToSPIRVType(builder,DynamicLLVM::LLVMTypeOf(inst)); ++ uint16_t op=opcode==LLVMSExt?114:(opcode==LLVMTrunc?113:(opcode==LLVMFPToUI?109:(opcode==LLVMFPToSI?110:(opcode==LLVMUIToFP?112:(opcode==LLVMSIToFP?111:115))))); ++ auto resultType=opcode==LLVMFPToUI?integerTypeWithSign(builder,type,false):type; ++ if(opcode==LLVMUIToFP){auto unsignedType=integerTypeWithSign(builder,llvmTypeToSPIRVType(builder,DynamicLLVM::LLVMTypeOf(source)),false);arg=builder.encodeBitcast(unsignedType,arg);} ++ auto result=builder.encodeRawOp(op,resultType,{arg});if(resultType!=type)result=builder.encodeBitcast(type,result); ++ builder.associateExistingResultID(result,reinterpret_cast(inst));builder.setResultType(result,type); ++ } break; ++ case LLVMFNeg: { ++ auto type=llvmTypeToSPIRVType(builder,DynamicLLVM::LLVMTypeOf(inst));auto result=builder.encodeRawOp(127,type,{llvmValueToResultID(builder,DynamicLLVM::LLVMGetOperand(inst,0))}); ++ builder.associateExistingResultID(result,reinterpret_cast(inst));builder.setResultType(result,type); ++ } break; ++ case LLVMUnreachable:builder.encodeRawOpNoResult(255,{});blockTerminated=true;break; + case LLVMZExt: { + auto source = DynamicLLVM::LLVMGetOperand(inst, 0); + auto targetType = DynamicLLVM::LLVMTypeOf(inst); + auto type = llvmTypeToSPIRVType(builder, targetType); + +- auto resID = builder.encodeUConvert(type, llvmValueToResultID(builder, source)); ++ auto argument = llvmValueToResultID(builder, source); ++ // AIR i1 is a SPIR-V boolean, not an integer accepted by OpUConvert. ++ // Keep the declared destination type so later PHIs see the same signedness. ++ auto sourceType = DynamicLLVM::LLVMTypeOf(source); ++ auto constant = [&](int value) { ++ switch (DynamicLLVM::LLVMGetIntTypeWidth(targetType)) { ++ case 8: return builder.declareConstantScalar(value); ++ case 16: return builder.declareConstantScalar(value); ++ case 32: return builder.declareConstantScalar(value); ++ case 64: return builder.declareConstantScalar(value); ++ default: throw std::runtime_error("Unsupported AIR boolean extension width"); ++ } ++ }; ++ auto resID = DynamicLLVM::LLVMGetTypeKind(sourceType) == LLVMIntegerTypeKind && DynamicLLVM::LLVMGetIntTypeWidth(sourceType) == 1 ++ ? builder.encodeRawOp(169, type, {argument, constant(1), constant(0)}) ++ : builder.encodeBitcast(type, builder.encodeUConvert(type, argument)); + + builder.associateExistingResultID(resID, reinterpret_cast(inst)); + builder.setResultType(resID, type); +@@ -1048,6 +1667,18 @@ void Iridium::AIR::Function::analyze(SPIRV::Builder& builder, OutputInfo& output + // TODO: look into using physical addressing instead of logical addressing, which should simplify the translation process + // (since i *think* that's the same addressing mode used by Metal code) + ++ case LLVMAlloca: { ++ auto allocated = DynamicLLVM::LLVMGetAllocatedType(inst); ++ auto count = DynamicLLVM::LLVMGetOperand(inst,0); ++ if (DynamicLLVM::LLVMGetValueKind(count) != LLVMConstantIntValueKind || DynamicLLVM::LLVMConstIntGetZExtValue(count) != 1) ++ throw std::runtime_error("Dynamic/multiple-element AIR allocation is unsupported"); ++ LayoutFreeScope layoutFree(true); ++ auto valueType=llvmTypeToSPIRVType(builder,allocated); ++ auto pointerType=builder.declareType(SPIRV::Type(SPIRV::Type::PointerTag{},SPIRV::StorageClass::Function,valueType,8)); ++ auto result=builder.addFunctionVariable(pointerType); ++ builder.associateExistingResultID(result,reinterpret_cast(inst)); ++ } break; ++ + case LLVMGetElementPtr: { + auto base = DynamicLLVM::LLVMGetOperand(inst, 0); + auto targetType = DynamicLLVM::LLVMTypeOf(inst); +@@ -1060,17 +1691,32 @@ void Iridium::AIR::Function::analyze(SPIRV::Builder& builder, OutputInfo& output + indices.push_back(llvmValueToResultID(builder, llindex)); + } + +- auto tmp = llvmTypeToSPIRVType(builder, targetType); + auto tmp2 = llvmValueToResultID(builder, base); ++ auto baseStorageClass = builder.reverseLookupType(builder.lookupResultType(tmp2))->pointerStorageClass; ++ LayoutFreeScope layoutFree(baseStorageClass == SPIRV::StorageClass::Private || baseStorageClass == SPIRV::StorageClass::Function); ++ auto tmp = builder.declareType(SPIRV::Type(SPIRV::Type::PointerTag {}, SPIRV::StorageClass::PhysicalStorageBuffer, llvmTypeToSPIRVType(builder, gepElementType(inst)), 8)); + + // ensure the resulting pointer storage class is the same as the input pointer storage class + auto type = *builder.reverseLookupType(tmp); + auto origTypeID = builder.lookupResultType(tmp2); + auto origType = *builder.reverseLookupType(origTypeID); ++ auto sourceType=llvmTypeToSPIRVType(builder,DynamicLLVM::LLVMGetGEPSourceElementType(inst)); ++ if(origType.targetType!=sourceType){origType.targetType=sourceType;origTypeID=builder.declareType(origType);tmp2=builder.encodeBitcast(origTypeID,tmp2);builder.setResultType(tmp2,origTypeID);} + type.pointerStorageClass = origType.pointerStorageClass; + auto resultType = builder.declareType(type); + auto origTypeTarget = *builder.reverseLookupType(origType.targetType); + ++ if (origType.pointerStorageClass != SPIRV::StorageClass::PhysicalStorageBuffer) { ++ auto firstIndex = DynamicLLVM::LLVMGetOperand(inst,1); ++ if (DynamicLLVM::LLVMGetValueKind(firstIndex) != LLVMConstantIntValueKind || DynamicLLVM::LLVMConstIntGetZExtValue(firstIndex) != 0) ++ throw std::runtime_error("Logical AIR GEP requires a zero initial index"); ++ indices.erase(indices.begin()); ++ auto result=indices.empty() ? tmp2 : builder.encodeAccessChain(resultType,tmp2,indices); ++ builder.associateExistingResultID(result,reinterpret_cast(inst)); ++ builder.setResultType(result,resultType); ++ break; ++ } ++ + // strangely enough, SPIR-V provides no instruction that can do an initial pointer offset like LLVM's GEP does. + // there's OpPtrAccessChain, which is tantalizingly named, but unfortunately that instruction doesn't work as expected + // either. i've tried using the raw index (i.e. in element units) and the multiplied index (i.e. in bytes), but no dice. +@@ -1112,6 +1758,9 @@ void Iridium::AIR::Function::analyze(SPIRV::Builder& builder, OutputInfo& output + op = access; + } + ++ // Opaque LLVM pointers may be used with more than one load type. ++ auto currentPointer=*builder.reverseLookupType(builder.lookupResultType(op)); ++ if(currentPointer.targetType!=type){auto pointer=builder.declareType(SPIRV::Type(SPIRV::Type::PointerTag{},currentPointer.pointerStorageClass,type,8));op=builder.encodeBitcast(pointer,op);builder.setResultType(op,pointer);} + auto resID = builder.encodeLoad(type, op, alignment); + + builder.associateExistingResultID(resID, reinterpret_cast(inst)); +@@ -1149,50 +1798,8 @@ void Iridium::AIR::Function::analyze(SPIRV::Builder& builder, OutputInfo& output + auto arg = DynamicLLVM::LLVMGetOperand(inst, 0); + + resID = builder.encodeConvertUToF(type, llvmValueToResultID(builder, arg)); +- } else if (name == "air.sample_texture_2d.v4f16" || name == "air.sample_texture_2d.v4f32" || name == "air.sample_texture_cube.v4f16") { +- auto textureArg = DynamicLLVM::LLVMGetOperand(inst, 0); +- auto samplerArg = DynamicLLVM::LLVMGetOperand(inst, 1); +- auto textureCoordArg = DynamicLLVM::LLVMGetOperand(inst, 2); +- auto someBooleanArgTODO = DynamicLLVM::LLVMGetOperand(inst, 3); +- auto offsetArg = DynamicLLVM::LLVMGetOperand(inst, 4); +- auto someOtherBooleanArgTODO = DynamicLLVM::LLVMGetOperand(inst, 5); +- auto someFloatArgTODO = DynamicLLVM::LLVMGetOperand(inst, 6); +- auto someOtherFloatArgTODO = DynamicLLVM::LLVMGetOperand(inst, 6); +- auto someI32ArgTODO = DynamicLLVM::LLVMGetOperand(inst, 7); +- +- auto textureArgID = llvmValueToResultID(builder, textureArg); +- auto samplerArgID = llvmValueToResultID(builder, samplerArg); +- +- auto texturePtrType = builder.lookupResultType(textureArgID); +- auto textureType = builder.reverseLookupType(texturePtrType)->targetType; +- auto textureLoad = builder.encodeLoad(textureType, textureArgID); +- +- auto samplerType = builder.declareType(SPIRV::Type(SPIRV::Type::SamplerTag {})); +- auto samplerPtrType = builder.declareType(SPIRV::Type(SPIRV::Type::PointerTag {}, SPIRV::StorageClass::UniformConstant, samplerType, 8)); +-#if 1 +- auto samplerLoad = builder.encodeLoad(samplerType, samplerArgID); +-#else +- auto samplerArgCast = builder.encodeBitcast(samplerPtrType, samplerArgID); +- auto samplerLoad = builder.encodeLoad(samplerType, samplerArgCast); +-#endif +- +- auto sampledImageType = builder.declareType(SPIRV::Type(SPIRV::Type::SampledImageTag {}, textureType)); +- auto sampledImage = builder.encodeSampledImage(sampledImageType, textureLoad, samplerLoad); +- +- // WORKAROUND: as explained before, we use 32-bit floats in place of 16-bit floats for image sampling. +- auto textureSampleComponentType = builder.declareType(SPIRV::Type(SPIRV::Type::FloatTag {}, 32)); +- auto textureSampleType = builder.declareType(SPIRV::Type(SPIRV::Type::VectorTag {}, 4, textureSampleComponentType, 16, 8)); +- auto sampled = builder.encodeImageSampleImplicitLod(textureSampleType, sampledImage, llvmValueToResultID(builder, textureCoordArg)); +- +- if (name == "air.sample_texture_2d.v4f16" || name == "air.sample_texture_cube.v4f16") { +- auto convertedComponentType = builder.declareType(SPIRV::Type(SPIRV::Type::FloatTag {}, 16)); +- auto convertedType = builder.declareType(SPIRV::Type(SPIRV::Type::VectorTag {}, 4, convertedComponentType, 8, 8)); +- sampled = builder.encodeFConvert(convertedType, sampled); +- } +- +- auto partialResult = builder.encodeCompositeInsert(type, sampled, builder.declareUndefinedValue(type), { 0 }); +- resID = builder.encodeCompositeInsert(type, builder.declareConstantScalar(0), partialResult, { 1 }); +- } else if (name == "air.convert.f.v4f32.f.v4f16" || name == "air.convert.f.v4f16.f.v4f32") { ++ } else if (translateAirTexture(builder, inst, name, type, funcInfo.type, resID)) { ++ } else if (name == "air.convert.f.v4f32.f.v4f16" || name == "air.convert.f.v4f16.f.v4f32") { + auto arg = DynamicLLVM::LLVMGetOperand(inst, 0); + + resID = builder.encodeFConvert(type, llvmValueToResultID(builder, arg)); +@@ -1223,14 +1830,33 @@ void Iridium::AIR::Function::analyze(SPIRV::Builder& builder, OutputInfo& output + auto operand2 = DynamicLLVM::LLVMGetOperand(inst, 1); + + resID = builder.encodeFMax(type, llvmValueToResultID(builder, operand1), llvmValueToResultID(builder, operand2)); ++ } else if (translateAirIntrinsic(builder, inst, name, type, resID, blockTerminated)) { ++ // handled + } else { +- throw std::runtime_error(std::string("TODO: support actual function calls (name = ") + name.data() + ")"); ++ throw std::runtime_error(std::string("TODO: support actual function calls (name = ") + std::string(name) + ")"); + } + +- builder.associateExistingResultID(resID, reinterpret_cast(inst)); +- builder.setResultType(resID, type); ++ if (resID != SPIRV::ResultIDInvalid) { ++ builder.associateExistingResultID(resID, reinterpret_cast(inst)); ++ builder.setResultType(resID, type); ++ } + } break; + ++ case LLVMAdd: case LLVMSub: case LLVMMul: case LLVMUDiv: case LLVMSDiv: ++ case LLVMURem: case LLVMSRem: case LLVMShl: case LLVMLShr: case LLVMAShr: ++ case LLVMAnd: case LLVMOr: case LLVMXor: { ++ auto lhs=llvmValueToResultID(builder,DynamicLLVM::LLVMGetOperand(inst,0)); ++ auto rhs=llvmValueToResultID(builder,DynamicLLVM::LLVMGetOperand(inst,1)); ++ auto type=llvmTypeToSPIRVType(builder,DynamicLLVM::LLVMTypeOf(inst)); ++ uint16_t op=0; ++ switch(opcode){case LLVMAdd:op=128;break;case LLVMSub:op=130;break;case LLVMMul:op=132;break; ++ case LLVMUDiv:op=134;break;case LLVMSDiv:op=135;break;case LLVMURem:op=137;break;case LLVMSRem:op=138;break; ++ case LLVMShl:op=196;break;case LLVMLShr:op=194;break;case LLVMAShr:op=195;break; ++ case LLVMAnd:op=199;break;case LLVMOr:op=197;break;case LLVMXor:op=198;break;default:break;} ++ auto t=*builder.reverseLookupType(type); ++ if(t.backingType==SPIRV::Type::BackingType::Boolean){op=opcode==LLVMAnd?167:(opcode==LLVMOr?166:165);} ++ auto result=builder.encodeRawOp(op,type,{lhs,rhs});builder.associateExistingResultID(result,reinterpret_cast(inst)); ++ } break; + case LLVMFMul: + case LLVMFDiv: + case LLVMFAdd: +@@ -1365,28 +1991,81 @@ void Iridium::AIR::Function::analyze(SPIRV::Builder& builder, OutputInfo& output + auto op1 = DynamicLLVM::LLVMGetOperand(inst, 0); + auto op2 = DynamicLLVM::LLVMGetOperand(inst, 1); + auto predicate = DynamicLLVM::LLVMGetFCmpPredicate(inst); ++ auto type = llvmTypeToSPIRVType(builder, DynamicLLVM::LLVMTypeOf(inst)); + +- SPIRV::ResultID resID = SPIRV::ResultIDInvalid; +- auto type = builder.declareType(SPIRV::Type(SPIRV::Type::BooleanTag {})); +- ++ uint16_t spirvOpcode = 0; + switch (predicate) { +- case LLVMRealOGT: { +- resID = builder.encodeFOrdGreaterThan(llvmValueToResultID(builder, op1), llvmValueToResultID(builder, op2)); +- } break; ++ case LLVMRealOEQ: spirvOpcode = SPIRVOp::FOrdEqual; break; ++ case LLVMRealOGT: spirvOpcode = SPIRVOp::FOrdGreaterThan; break; ++ case LLVMRealOGE: spirvOpcode = SPIRVOp::FOrdGreaterThanEqual; break; ++ case LLVMRealOLT: spirvOpcode = SPIRVOp::FOrdLessThan; break; ++ case LLVMRealOLE: spirvOpcode = SPIRVOp::FOrdLessThanEqual; break; ++ case LLVMRealONE: spirvOpcode = SPIRVOp::FOrdNotEqual; break; ++ case LLVMRealORD: spirvOpcode = SPIRVOp::Ordered; break; ++ case LLVMRealUNO: spirvOpcode = SPIRVOp::Unordered; break; ++ case LLVMRealUEQ: spirvOpcode = SPIRVOp::FUnordEqual; break; ++ case LLVMRealUGT: spirvOpcode = SPIRVOp::FUnordGreaterThan; break; ++ case LLVMRealUGE: spirvOpcode = SPIRVOp::FUnordGreaterThanEqual; break; ++ case LLVMRealULT: spirvOpcode = SPIRVOp::FUnordLessThan; break; ++ case LLVMRealULE: spirvOpcode = SPIRVOp::FUnordLessThanEqual; break; ++ case LLVMRealUNE: spirvOpcode = SPIRVOp::FUnordNotEqual; break; ++ default: ++ throw std::runtime_error("TODO: handle fcmp predicate: " + std::to_string(predicate)); ++ } + +- case LLVMRealOLT: { +- resID = builder.encodeFOrdLessThan(llvmValueToResultID(builder, op1), llvmValueToResultID(builder, op2)); +- } break; ++ auto resID = builder.encodeRawOp(spirvOpcode, type, { llvmValueToResultID(builder, op1), llvmValueToResultID(builder, op2) }); + ++ builder.associateExistingResultID(resID, reinterpret_cast(inst)); ++ builder.setResultType(resID, type); ++ } break; ++ ++ case LLVMICmp: { ++ auto op1 = DynamicLLVM::LLVMGetOperand(inst, 0); ++ auto op2 = DynamicLLVM::LLVMGetOperand(inst, 1); ++ auto predicate = DynamicLLVM::LLVMGetICmpPredicate(inst); ++ auto type = llvmTypeToSPIRVType(builder, DynamicLLVM::LLVMTypeOf(inst)); ++ ++ uint16_t spirvOpcode = 0; ++ switch (predicate) { ++ case LLVMIntEQ: spirvOpcode = SPIRVOp::IEqual; break; ++ case LLVMIntNE: spirvOpcode = SPIRVOp::INotEqual; break; ++ case LLVMIntUGT: spirvOpcode = SPIRVOp::UGreaterThan; break; ++ case LLVMIntUGE: spirvOpcode = SPIRVOp::UGreaterThanEqual; break; ++ case LLVMIntULT: spirvOpcode = SPIRVOp::ULessThan; break; ++ case LLVMIntULE: spirvOpcode = SPIRVOp::ULessThanEqual; break; ++ case LLVMIntSGT: spirvOpcode = SPIRVOp::SGreaterThan; break; ++ case LLVMIntSGE: spirvOpcode = SPIRVOp::SGreaterThanEqual; break; ++ case LLVMIntSLT: spirvOpcode = SPIRVOp::SLessThan; break; ++ case LLVMIntSLE: spirvOpcode = SPIRVOp::SLessThanEqual; break; + default: +- throw std::runtime_error("TODO: handle fcmp predicate: " + std::to_string(predicate)); ++ throw std::runtime_error("TODO: handle icmp predicate: " + std::to_string(predicate)); + } + ++ auto resID = builder.encodeRawOp(spirvOpcode, type, { llvmValueToResultID(builder, op1), llvmValueToResultID(builder, op2) }); ++ ++ builder.associateExistingResultID(resID, reinterpret_cast(inst)); ++ builder.setResultType(resID, type); ++ } break; ++ ++ case LLVMSelect: { ++ auto type = llvmTypeToSPIRVType(builder, DynamicLLVM::LLVMTypeOf(inst)); ++ auto resID = builder.encodeRawOp(SPIRVOp::Select, type, { ++ llvmValueToResultID(builder, DynamicLLVM::LLVMGetOperand(inst, 0)), ++ llvmValueToResultID(builder, DynamicLLVM::LLVMGetOperand(inst, 1)), ++ llvmValueToResultID(builder, DynamicLLVM::LLVMGetOperand(inst, 2)), ++ }); ++ + builder.associateExistingResultID(resID, reinterpret_cast(inst)); + builder.setResultType(resID, type); + } break; + + case LLVMBr: { ++ auto loop=cfgInfos.find(builder.lookupResultID(reinterpret_cast(bb))); ++ if(loop!=cfgInfos.end() && loop->second.type==CFGType::Loop){ ++ builder.encodeRawOpNoResult(246,{loop->second.selectionMergeBlock,loop->second.continueBlock,0}); ++ auto selection=splitLoopSelections.find(bb);if(selection!=splitLoopSelections.end()){builder.encodeBranch(selection->second.first);builder.insertLabel(selection->second.first);builder.encodeSelectionMerge(selection->second.second);} ++ } ++ auto split=splitContinues.find(bb);if(split!=splitContinues.end()){builder.encodeBranch(split->second);builder.insertLabel(split->second);} + if (DynamicLLVM::LLVMIsConditional(inst)) { + auto condition = DynamicLLVM::LLVMGetCondition(inst); + auto trueLabel = DynamicLLVM::LLVMBasicBlockAsValue(DynamicLLVM::LLVMGetSuccessor(inst, 0)); +@@ -1422,25 +2101,38 @@ void Iridium::AIR::Function::analyze(SPIRV::Builder& builder, OutputInfo& output + auto type = llvmTypeToSPIRVType(builder, lltype); + + std::vector> variablesAndBlocks; ++ std::vector forwardAssociations; + + auto opCount = DynamicLLVM::LLVMCountIncoming(inst); + for (size_t i = 0; i < opCount; ++i) { + auto variable = DynamicLLVM::LLVMGetIncomingValue(inst, i); + auto label = DynamicLLVM::LLVMBasicBlockAsValue(DynamicLLVM::LLVMGetIncomingBlock(inst, i)); +- variablesAndBlocks.push_back(std::make_pair(llvmValueToResultID(builder, variable), llvmValueToResultID(builder, label))); ++ auto id=builder.lookupResultID(reinterpret_cast(variable)); ++ bool forward=id==SPIRV::ResultIDInvalid && DynamicLLVM::LLVMGetValueKind(variable)==LLVMInstructionValueKind; ++ auto split=splitContinues.find(DynamicLLVM::LLVMGetIncomingBlock(inst,i)); ++ auto labelID=split==splitContinues.end()?llvmValueToResultID(builder,label):split->second; ++ auto selection=splitLoopSelections.find(DynamicLLVM::LLVMGetIncomingBlock(inst,i));if(selection!=splitLoopSelections.end())labelID=selection->second.first; ++ variablesAndBlocks.push_back({forward?SPIRV::ResultIDInvalid:llvmValueToResultID(builder,variable),labelID}); ++ forwardAssociations.push_back(forward?reinterpret_cast(variable):0); + } + +- auto resID = builder.encodePhi(type, variablesAndBlocks); ++ auto resID = builder.encodePhi(type, variablesAndBlocks, builder.lookupResultID(reinterpret_cast(inst)), forwardAssociations); + + builder.associateExistingResultID(resID, reinterpret_cast(inst)); + builder.setResultType(resID, type); + } break; + + case LLVMBitCast: { ++ // Unused opaque-pointer casts can infer a different pointee type. ++ // They have no effect and logical-pointer OpBitcast is illegal in SPIR-V. ++ if (!DynamicLLVM::LLVMGetFirstUse(inst)) break; + auto arg = DynamicLLVM::LLVMGetOperand(inst, 0); + auto lltype = DynamicLLVM::LLVMTypeOf(inst); +- auto type = llvmTypeToSPIRVType(builder, lltype); + auto argID = llvmValueToResultID(builder, arg); ++ auto sourceType = *builder.reverseLookupType(builder.lookupResultType(argID)); ++ auto type = DynamicLLVM::LLVMGetTypeKind(lltype) == LLVMPointerTypeKind ? ++ builder.declareType(SPIRV::Type(SPIRV::Type::PointerTag {}, sourceType.pointerStorageClass, ++ llvmTypeToSPIRVType(builder, pointerElementType(inst)), 8)) : llvmTypeToSPIRVType(builder, lltype); + + // ensure the resulting pointer storage class is the same as the input pointer storage class + auto typeInst = *builder.reverseLookupType(type); +@@ -1474,6 +2166,7 @@ void Iridium::AIR::Function::analyze(SPIRV::Builder& builder, OutputInfo& output + } + } + ++ for(auto label:unreachableMerges){builder.insertLabel(label);builder.encodeRawOpNoResult(255,{});} + builder.endFunction(); + }; + +diff --git a/src/iridium/spirv.cpp b/src/iridium/spirv.cpp +index 5528342..4c085e6 100644 +--- a/src/iridium/spirv.cpp ++++ b/src/iridium/spirv.cpp +@@ -1,3 +1,4 @@ ++#include + #include + #include + #include +@@ -19,6 +20,7 @@ bool Iridium::SPIRV::Type::operator==(const Type& other) const { + backingType != other.backingType || + scalarWidth != other.scalarWidth || + integerIsSigned != other.integerIsSigned || ++ layoutFree != other.layoutFree || + entryCount != other.entryCount || + structureMembers.size() != other.structureMembers.size() || + pointerStorageClass != other.pointerStorageClass || +@@ -224,12 +226,19 @@ void Iridium::SPIRV::Builder::addEntryPoint(const EntryPoint& entryPoint) { + _entryPoints.push_back(entryPoint); + }; + +-Iridium::SPIRV::ResultID Iridium::SPIRV::Builder::addGlobalVariable(ResultID typeID, StorageClass storageClass) { ++Iridium::SPIRV::ResultID Iridium::SPIRV::Builder::addGlobalVariable(ResultID typeID, StorageClass storageClass, ResultID initializer) { + auto resultID = reserveResultID(); +- _globalVariables[resultID] = GlobalVariable { storageClass, typeID }; ++ _globalVariables[resultID] = GlobalVariable { storageClass, typeID, initializer }; + return resultID; + }; + ++Iridium::SPIRV::ResultID Iridium::SPIRV::Builder::addFunctionVariable(ResultID typeID) { ++ auto id = reserveResultID(); ++ _currentFunctionInfo->variables[id] = FunctionVariable { typeID, ResultIDInvalid }; ++ setResultType(id, typeID); ++ return id; ++}; ++ + void Iridium::SPIRV::Builder::addDecoration(ResultID resultID, const Decoration& decoration) { + if (_decorations.find(resultID) == _decorations.end()) { + _decorations[resultID] = std::unordered_set { decoration }; +@@ -281,6 +290,9 @@ Iridium::SPIRV::ResultID Iridium::SPIRV::Builder::beginFunction(ResultID id) { + }; + + void Iridium::SPIRV::Builder::endFunction() { ++ auto position=_currentFunctionWriter->offset(); ++ for(auto fixup:_phiFixups){auto id=lookupResultID(fixup.second);if(id==ResultIDInvalid)throw std::runtime_error("Unresolved forward PHI input");_currentFunctionWriter->seek(fixup.first);_currentFunctionWriter->writeIntegerLE(id);} ++ _phiFixups.clear();_currentFunctionWriter->seek(position); + _currentFunctionWriter = nullptr; + }; + +@@ -425,7 +437,8 @@ Iridium::SPIRV::ResultID Iridium::SPIRV::Builder::insertLabel(ResultID id) { + }; + + void Iridium::SPIRV::Builder::referenceGlobalVariable(ResultID id) { +- _currentFunctionInfo->referencedGlobalVariables.push_back(id); ++ auto& refs = _currentFunctionInfo->referencedGlobalVariables; ++ if (std::find(refs.begin(), refs.end(), id) == refs.end()) refs.push_back(id); + }; + + Iridium::SPIRV::ResultID Iridium::SPIRV::Builder::encodeUConvert(ResultID typeID, ResultID operand) { +@@ -719,12 +732,15 @@ void Iridium::SPIRV::Builder::encodeBranchConditional(ResultID condition, Result + endInstruction(std::move(tmp)); + }; + +-Iridium::SPIRV::ResultID Iridium::SPIRV::Builder::encodePhi(ResultID resultTypeID, std::vector> variablesAndBlocks) { +- auto result = reserveResultID(); ++Iridium::SPIRV::ResultID Iridium::SPIRV::Builder::encodePhi(ResultID resultTypeID, std::vector> variablesAndBlocks, ResultID result, std::vector forwardAssociations) { ++ if(result==ResultIDInvalid)result = reserveResultID(); + auto tmp = beginInstruction(Opcode::Phi, *_currentFunctionWriter); + _currentFunctionWriter->writeIntegerLE(resultTypeID); + _currentFunctionWriter->writeIntegerLE(result); +- for (const auto& [variable, block]: variablesAndBlocks) { ++ size_t index=0; ++ for (const auto& [variable, block]: variablesAndBlocks) { ++ if(indexoffset(),forwardAssociations[index]}); ++ ++index; + _currentFunctionWriter->writeIntegerLE(variable); + _currentFunctionWriter->writeIntegerLE(block); + } +@@ -910,6 +926,7 @@ void* Iridium::SPIRV::Builder::finalize(size_t& outputSize) { + } + endInstruction(std::move(tmp)); + } ++ if (type.layoutFree) continue; + tmp = beginInstruction(Opcode::MemberDecorate, _writer); + _writer.writeIntegerLE(id); + _writer.writeIntegerLE(i); +@@ -934,7 +951,7 @@ void* Iridium::SPIRV::Builder::finalize(size_t& outputSize) { + + // now emit automatic type decorations + for (const auto& [type, id]: _typeIDs) { +- if (type.backingType == Type::BackingType::RuntimeArray || type.backingType == Type::BackingType::Array) { ++ if (!type.layoutFree && (type.backingType == Type::BackingType::RuntimeArray || type.backingType == Type::BackingType::Array)) { + auto elmTypeInst = _reverseTypeIDs[type.targetType]; + tmp = beginInstruction(Opcode::Decorate, _writer); + _writer.writeIntegerLE(id); +@@ -1120,7 +1137,7 @@ void* Iridium::SPIRV::Builder::finalize(size_t& outputSize) { + _writer.writeIntegerLE(var.typeID); + _writer.writeIntegerLE(id); + _writer.writeIntegerLE(static_cast(var.storageClass)); +- // TODO: support initializers ++ if (var.initializer != ResultIDInvalid) _writer.writeIntegerLE(var.initializer); + endInstruction(std::move(tmp)); + } + +@@ -1199,3 +1216,54 @@ void* Iridium::SPIRV::Builder::finalize(size_t& outputSize) { + outputSize = _writer.size(); + return _writer.extractBuffer(); + }; ++ ++Iridium::SPIRV::ResultID Iridium::SPIRV::Builder::declareConstantBool(bool value) { ++ auto typeID = declareType(Type(Type::BooleanTag {})); ++ auto key = std::make_pair(static_cast(value ? 1 : 0), typeID); ++ auto it = _constantBools.find(key); ++ ++ if (it != _constantBools.end()) { ++ return it->second; ++ } ++ ++ auto id = reserveResultID(); ++ ++ _constantBools.emplace(key, id); ++ ++ auto tmp = beginInstruction(value ? Opcode::ConstantTrue : Opcode::ConstantFalse, _constants); ++ _constants.writeIntegerLE(typeID); ++ _constants.writeIntegerLE(id); ++ endInstruction(std::move(tmp)); ++ ++ return id; ++}; ++ ++Iridium::SPIRV::ResultID Iridium::SPIRV::Builder::encodeRawOp(uint16_t opcode, ResultID resultTypeID, const std::vector& operands) { ++ auto result = reserveResultID(); ++ auto tmp = beginInstruction(opcode, *_currentFunctionWriter); ++ _currentFunctionWriter->writeIntegerLE(resultTypeID); ++ _currentFunctionWriter->writeIntegerLE(result); ++ for (auto operand: operands) { ++ _currentFunctionWriter->writeIntegerLE(operand); ++ } ++ endInstruction(std::move(tmp)); ++ return result; ++}; ++ ++void Iridium::SPIRV::Builder::encodeRawOpNoResult(uint16_t opcode, const std::vector& operands) { ++ auto tmp = beginInstruction(opcode, *_currentFunctionWriter); ++ for (auto operand: operands) { ++ _currentFunctionWriter->writeIntegerLE(operand); ++ } ++ endInstruction(std::move(tmp)); ++}; ++ ++Iridium::SPIRV::ResultID Iridium::SPIRV::Builder::encodeGLSLOp(GLSLOpcode opcode, ResultID resultTypeID, const std::vector& operands) { ++ auto result = reserveResultID(); ++ auto tmp = beginGLSLInstruction(opcode, *_currentFunctionWriter, resultTypeID, result); ++ for (auto operand: operands) { ++ _currentFunctionWriter->writeIntegerLE(operand); ++ } ++ endInstruction(std::move(tmp)); ++ return result; ++}; +diff --git a/private-include/indium/counters.private.hpp b/private-include/indium/counters.private.hpp +new file mode 100644 +--- /dev/null ++++ b/private-include/indium/counters.private.hpp +@@ -0,0 +1,44 @@ ++#pragma once ++ ++#include ++#include ++#include ++#include ++#include ++#include ++ ++namespace Indium { ++ // Diagnostic SPIR-V/binding dumps: checked once per process (an access() per draw is a syscall plus path translation). ++ inline bool spirvDebugEnabled() { ++ static const bool enabled = access("/tmp/metal-debug-spirv", F_OK) == 0; ++ return enabled; ++ } ++ ++ // Diagnostic event counters, written to the guest's /tmp/metal-trace.log every 5 s when /tmp/metal-debug-timing exists. ++ inline void count(const char* what, double amount = 1) { ++ static const bool enabled = access("/tmp/metal-debug-timing", F_OK) == 0; ++ if (!enabled) { ++ return; ++ } ++ static std::mutex lock; ++ static std::map> counts; ++ static time_t last = 0; ++ std::lock_guard guard(lock); ++ auto& entry = counts[what]; ++ entry.first++; ++ entry.second += amount; ++ time_t now = time(nullptr); ++ if (now - last >= 5) { ++ last = now; ++ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) { ++ fprintf(log, "indium-counts@%ld:", (long) now); ++ for (auto& item: counts) { ++ fprintf(log, " %s[n=%lu sum=%.0f]", item.first.c_str(), item.second.first, item.second.second); ++ } ++ fprintf(log, "\n"); ++ fclose(log); ++ } ++ counts.clear(); ++ } ++ } ++}; +diff --git a/private-include/indium/deferred-render-encoder.private.hpp b/private-include/indium/deferred-render-encoder.private.hpp +new file mode 100644 +--- /dev/null ++++ b/private-include/indium/deferred-render-encoder.private.hpp +@@ -0,0 +1,355 @@ ++#pragma once ++ ++// Deferred render-command encoding. ++// ++// League's main thread spends 2-4.5 ms of every frame inside the Metal-to-Vulkan translation (PrivateRenderCommandEncoder: ++// binding bookkeeping, descriptor sets, vkCmd* calls). Nothing about that work has to happen on the game's thread: Metal ++// semantics only require that the encoder calls are applied in order before the command buffer is committed. ++// ++// This facade implements the public RenderCommandEncoder interface by recording every call into a compact op list ++// (copying anything the caller may reuse: setBytes data, viewport/scissor arrays) and, at endEncoding(), hands the list to ++// the ordered worker of submit-thread.private.hpp, which creates the real PrivateRenderCommandEncoder, replays the ops into ++// it and ends it. The same worker later runs the command buffer's commit(), so encoders are always fully replayed before ++// the commit looks at them. Everything that touches the same Vulkan command buffer from another thread (blit/compute ++// encoders, a direct render encoder) first waits for the command buffer's pending replays (waitDeferred()). ++// ++// Live switch (guest file, re-read every 256 encoders): /tmp/metal-async-encode present = on. Off by default while it is ++// validated. Header-only: the build script compiles only the .cpp files of the CMake target. ++ ++#include ++#include ++#include ++#include ++#include ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++namespace Indium { ++ namespace AsyncEncode { ++ inline bool enabled() { ++ static std::atomic value {false}; ++ static std::atomic calls {0}; ++ // On by default in the League game process only (fight A/B 2026-10-12: 60.4 -> 72.4 fps, p99 28.7 -> 22.9 ms ++ // with the frame-end sync in PrivateCommandBuffer::commit). Guest flag files, re-read every 256 calls: ++ // /tmp/metal-no-async-encode turns it off there, /tmp/metal-async-encode turns it on in any process. ++ static const bool leagueGame = [] { const char* name = getprogname(); return name && strcmp(name, "LeagueofLegends") == 0; }(); ++ if ((calls.fetch_add(1, std::memory_order_relaxed) & 255) == 0) ++ value.store(access("/tmp/metal-async-encode", F_OK) == 0 || ++ (leagueGame && access("/tmp/metal-no-async-encode", F_OK) != 0), std::memory_order_relaxed); ++ return value.load(std::memory_order_relaxed); ++ } ++ ++ // Objective-C autorelease pool for the worker thread (libobjc is not a link dependency of Indium) ++ struct PoolScope { ++ void* token = nullptr; ++ void (*pop)(void*) = nullptr; ++ PoolScope() { ++ static void* (*push)(void) = reinterpret_cast(dlsym(RTLD_DEFAULT, "objc_autoreleasePoolPush")); ++ static void (*popFn)(void*) = reinterpret_cast(dlsym(RTLD_DEFAULT, "objc_autoreleasePoolPop")); ++ if (push && popFn) { token = push(); pop = popFn; } ++ } ++ ~PoolScope() { if (pop) pop(token); } ++ }; ++ ++ enum class Kind: uint8_t { ++ SetPipeline, FrontFacing, CullMode, DepthBias, DepthClip, Viewports, Scissors, BlendColor, DepthStencil, FillMode, ++ StencilRef, StencilRefFB, VisibilityMode, ++ DrawPrim, DrawIdx, ++ VBytes, VBuffer, VBufferOffset, VSampler, VSamplerClamp, VTexture, ++ FBytes, FBuffer, FBufferOffset, FSampler, FSamplerClamp, FTexture, ++ UseResource, UseResourceStages, MemoryBarrier, ++ }; ++ ++ struct Op { ++ Kind kind; ++ uint8_t pad8 = 0; ++ uint32_t e1 = 0, e2 = 0; ++ size_t n[5] = {0, 0, 0, 0, 0}; ++ float f[4] = {0, 0, 0, 0}; ++ std::shared_ptr p; ++ explicit Op(Kind k) : kind(k) {} ++ }; ++ ++ // Recording buffers are recycled: growing them through the allocator on every encoder is what this is avoiding. ++ struct Buffers { ++ std::vector ops; ++ std::vector bytes; ++ std::vector viewports; ++ std::vector scissors; ++ void clear() { ops.clear(); bytes.clear(); viewports.clear(); scissors.clear(); } ++ }; ++ ++ struct BufferPool { ++ std::mutex mutex; ++ std::vector> free; ++ }; ++ ++ inline BufferPool& bufferPool() { ++ // The detached worker may return buffers during process teardown. ++ static auto* pool = new BufferPool; ++ return *pool; ++ } ++ ++ inline std::unique_ptr acquireBuffers() { ++ auto& pool = bufferPool(); ++ { ++ std::lock_guard lock(pool.mutex); ++ if (!pool.free.empty()) { auto b = std::move(pool.free.back()); pool.free.pop_back(); return b; } ++ } ++ auto b = std::make_unique(); ++ b->ops.reserve(512); ++ b->bytes.reserve(8192); ++ return b; ++ } ++ ++ inline void releaseBuffers(std::unique_ptr b) { ++ b->clear(); ++ if (b->ops.capacity() > 65536 || b->bytes.capacity() > 1048576) return; ++ auto& pool = bufferPool(); ++ std::lock_guard lock(pool.mutex); ++ if (pool.free.size() < 16) pool.free.push_back(std::move(b)); ++ } ++ ++ // One render pass being recorded. The recording is handed to the worker in chunks while the caller is still ++ // encoding (see DeferredRenderCommandEncoder::add), so `real` and `failed` are only touched by the worker. ++ struct State { ++ std::shared_ptr commandBuffer; ++ RenderPassDescriptor descriptor; ++ std::shared_ptr real; ++ bool failed = false; ++ State(std::shared_ptr cb, const RenderPassDescriptor& d) : commandBuffer(std::move(cb)), descriptor(d) {} ++ }; ++ ++ // Apply one chunk of recorded calls; `last` also ends the real encoder. ++ inline void replay(State& st, const Buffers& r, bool last) { ++ PoolScope pool; ++ if (!st.real && !st.failed) { ++ try { ++ st.real = st.commandBuffer->renderCommandEncoderNow(st.descriptor); ++ } catch (const std::exception& e) { ++ st.failed = true; ++ if (FILE* f = fopen("/tmp/metal-trace.log", "a")) { fprintf(f, "async-encode: encoder creation failed: %s\n", e.what()); fclose(f); } ++ } ++ } ++ if (!st.real) return; ++ PrivateRenderCommandEncoder* real = st.real.get(); ++ for (const Op& op : r.ops) { ++ try { ++ switch (op.kind) { ++ case Kind::SetPipeline: real->setRenderPipelineState(std::static_pointer_cast(op.p)); break; ++ case Kind::FrontFacing: real->setFrontFacingWinding(static_cast(op.e1)); break; ++ case Kind::CullMode: real->setCullMode(static_cast(op.e1)); break; ++ case Kind::DepthBias: real->setDepthBias(op.f[0], op.f[1], op.f[2]); break; ++ case Kind::DepthClip: real->setDepthClipMode(static_cast(op.e1)); break; ++ case Kind::Viewports: real->setViewports(r.viewports.data() + op.n[0], op.n[1]); break; ++ case Kind::Scissors: real->setScissorRects(r.scissors.data() + op.n[0], op.n[1]); break; ++ case Kind::BlendColor: real->setBlendColor(op.f[0], op.f[1], op.f[2], op.f[3]); break; ++ case Kind::DepthStencil: real->setDepthStencilState(std::static_pointer_cast(op.p)); break; ++ case Kind::FillMode: real->setTriangleFillMode(static_cast(op.e1)); break; ++ case Kind::StencilRef: real->setStencilReferenceValue(op.e1); break; ++ case Kind::StencilRefFB: real->setStencilReferenceValue(op.e1, op.e2); break; ++ case Kind::VisibilityMode: real->setVisibilityResultMode(static_cast(op.e1), op.n[0]); break; ++ case Kind::DrawPrim: real->drawPrimitives(static_cast(op.e1), op.n[0], op.n[1], op.n[2], op.n[3]); break; ++ case Kind::DrawIdx: ++ real->drawIndexedPrimitives(static_cast(op.e1), op.n[0], static_cast(op.e2), ++ std::static_pointer_cast(op.p), op.n[1], op.n[2], static_cast(op.n[3]), op.n[4]); ++ break; ++ case Kind::VBytes: real->setVertexBytes(r.bytes.data() + op.n[0], op.n[1], op.n[2]); break; ++ case Kind::VBuffer: real->setVertexBuffer(std::static_pointer_cast(op.p), op.n[0], op.n[1]); break; ++ case Kind::VBufferOffset: real->setVertexBufferOffset(op.n[0], op.n[1]); break; ++ case Kind::VSampler: real->setVertexSamplerState(std::static_pointer_cast(op.p), op.n[0]); break; ++ case Kind::VSamplerClamp: real->setVertexSamplerState(std::static_pointer_cast(op.p), op.f[0], op.f[1], op.n[0]); break; ++ case Kind::VTexture: real->setVertexTexture(std::static_pointer_cast(op.p), op.n[0]); break; ++ case Kind::FBytes: real->setFragmentBytes(r.bytes.data() + op.n[0], op.n[1], op.n[2]); break; ++ case Kind::FBuffer: real->setFragmentBuffer(std::static_pointer_cast(op.p), op.n[0], op.n[1]); break; ++ case Kind::FBufferOffset: real->setFragmentBufferOffset(op.n[0], op.n[1]); break; ++ case Kind::FSampler: real->setFragmentSamplerState(std::static_pointer_cast(op.p), op.n[0]); break; ++ case Kind::FSamplerClamp: real->setFragmentSamplerState(std::static_pointer_cast(op.p), op.f[0], op.f[1], op.n[0]); break; ++ case Kind::FTexture: real->setFragmentTexture(std::static_pointer_cast(op.p), op.n[0]); break; ++ case Kind::UseResource: real->useResource(std::static_pointer_cast(op.p), static_cast(op.e1)); break; ++ case Kind::UseResourceStages: real->useResource(std::static_pointer_cast(op.p), static_cast(op.e1), static_cast(op.n[0])); break; ++ case Kind::MemoryBarrier: real->memoryBarrier(op.n[0], static_cast(op.n[1]), static_cast(op.n[2])); break; ++ } ++ } catch (const std::exception& e) { ++ if (FILE* f = fopen("/tmp/metal-trace.log", "a")) { fprintf(f, "async-encode: op %d failed: %s\n", (int) op.kind, e.what()); fclose(f); } ++ } ++ } ++ if (last) { ++ try { real->endEncoding(); } catch (...) {} ++ st.real.reset(); ++ } ++ } ++ ++ class DeferredRenderCommandEncoder final: public RenderCommandEncoder { ++ // calls per chunk handed to the worker while the pass is still being recorded ++ static constexpr size_t chunkOps = 128; ++ ++ std::shared_ptr _state; ++ std::unique_ptr _rec; ++ Buffers* _r; ++ bool _begun = false; ++ bool _ended = false; ++ ++ // The worker starts on a pass as soon as its first chunk is complete instead of after endEncoding: at the ++ // frame-end sync it used to have the whole last pass still to do while the caller waited (27% of the main ++ // thread in fights). A chunk owns the bytes/viewports/scissors its calls refer to, so any boundary is valid. ++ void flush(bool last) { ++ auto st = _state; ++ auto cb = st->commandBuffer; ++ if (!_begun) { _begun = true; cb->deferredBegin(); } ++ Buffers* chunk = _rec.release(); ++ SubmitThread::post([st, cb, chunk, last]() mutable { ++ std::unique_ptr owned(chunk); ++ replay(*st, *owned, last); ++ releaseBuffers(std::move(owned)); ++ if (last) { ++ st.reset(); ++ cb->deferredEnd(); ++ } ++ }); ++ if (!last) { _rec = acquireBuffers(); _r = _rec.get(); } ++ else _r = nullptr; ++ } ++ ++ Op& add(Kind k) { ++ // before recording the next call: the previous ones are complete (their fields are filled in after add()) ++ if (_r->ops.size() >= chunkOps) flush(false); ++ _r->ops.emplace_back(k); ++ return _r->ops.back(); ++ } ++ void addRef(Kind k, std::shared_ptr ref, size_t a, size_t b) { Op& o = add(k); o.p = std::move(ref); o.n[0] = a; o.n[1] = b; } ++ ++ public: ++ DeferredRenderCommandEncoder(std::shared_ptr cb, const RenderPassDescriptor& d): ++ _state(std::make_shared(std::move(cb), d)), _rec(acquireBuffers()), _r(_rec.get()) {} ++ ++ ~DeferredRenderCommandEncoder() override { endEncoding(); } ++ ++ void endEncoding() override { ++ if (_ended) return; ++ _ended = true; ++ flush(true); ++ _state.reset(); ++ } ++ ++ void setRenderPipelineState(std::shared_ptr s) override { addRef(Kind::SetPipeline, std::move(s), 0, 0); } ++ void setFrontFacingWinding(Winding w) override { add(Kind::FrontFacing).e1 = static_cast(w); } ++ void setCullMode(CullMode m) override { add(Kind::CullMode).e1 = static_cast(m); } ++ void setDepthBias(float bias, float slope, float clamp) override { Op& o = add(Kind::DepthBias); o.f[0] = bias; o.f[1] = slope; o.f[2] = clamp; } ++ void setDepthClipMode(DepthClipMode m) override { add(Kind::DepthClip).e1 = static_cast(m); } ++ void setViewport(const Viewport& v) override { setViewports(&v, 1); } ++ void setViewports(const Viewport* v, size_t count) override { ++ Op& o = add(Kind::Viewports); o.n[0] = _r->viewports.size(); o.n[1] = count; ++ _r->viewports.insert(_r->viewports.end(), v, v + count); ++ } ++ void setViewports(const std::vector& v) override { setViewports(v.data(), v.size()); } ++ void setScissorRect(const ScissorRect& s) override { setScissorRects(&s, 1); } ++ void setScissorRects(const ScissorRect* s, size_t count) override { ++ Op& o = add(Kind::Scissors); o.n[0] = _r->scissors.size(); o.n[1] = count; ++ _r->scissors.insert(_r->scissors.end(), s, s + count); ++ } ++ void setScissorRects(const std::vector& s) override { setScissorRects(s.data(), s.size()); } ++ void setBlendColor(float r, float g, float b, float a) override { Op& o = add(Kind::BlendColor); o.f[0] = r; o.f[1] = g; o.f[2] = b; o.f[3] = a; } ++ void setDepthStencilState(std::shared_ptr s) override { addRef(Kind::DepthStencil, std::move(s), 0, 0); } ++ void setTriangleFillMode(TriangleFillMode m) override { add(Kind::FillMode).e1 = static_cast(m); } ++ void setStencilReferenceValue(uint32_t v) override { add(Kind::StencilRef).e1 = v; } ++ void setStencilReferenceValue(uint32_t front, uint32_t back) override { Op& o = add(Kind::StencilRefFB); o.e1 = front; o.e2 = back; } ++ void setVisibilityResultMode(VisibilityResultMode m, size_t offset) override { Op& o = add(Kind::VisibilityMode); o.e1 = static_cast(m); o.n[0] = offset; } ++ ++ void drawPrimitives(PrimitiveType t, size_t start, size_t count, size_t instances, size_t baseInstance) override { ++ Op& o = add(Kind::DrawPrim); o.e1 = static_cast(t); o.n[0] = start; o.n[1] = count; o.n[2] = instances; o.n[3] = baseInstance; ++ } ++ void drawPrimitives(PrimitiveType t, size_t start, size_t count, size_t instances) override { drawPrimitives(t, start, count, instances, 0); } ++ void drawPrimitives(PrimitiveType t, size_t start, size_t count) override { drawPrimitives(t, start, count, 1, 0); } ++ ++ void drawIndexedPrimitives(PrimitiveType t, size_t indexCount, IndexType it, std::shared_ptr indexBuffer, size_t indexBufferOffset, ++ size_t instanceCount, int64_t baseVertex, size_t baseInstance) override { ++ Op& o = add(Kind::DrawIdx); ++ o.e1 = static_cast(t); o.e2 = static_cast(it); o.p = std::move(indexBuffer); ++ o.n[0] = indexCount; o.n[1] = indexBufferOffset; o.n[2] = instanceCount; o.n[3] = static_cast(baseVertex); o.n[4] = baseInstance; ++ } ++ void drawIndexedPrimitives(PrimitiveType t, size_t indexCount, IndexType it, std::shared_ptr b, size_t off, size_t instances) override { ++ drawIndexedPrimitives(t, indexCount, it, std::move(b), off, instances, 0, 0); ++ } ++ void drawIndexedPrimitives(PrimitiveType t, size_t indexCount, IndexType it, std::shared_ptr b, size_t off) override { ++ drawIndexedPrimitives(t, indexCount, it, std::move(b), off, 1, 0, 0); ++ } ++ ++ void setVertexBytes(const void* bytes, size_t length, size_t index) override { ++ Op& o = add(Kind::VBytes); o.n[0] = _r->bytes.size(); o.n[1] = length; o.n[2] = index; ++ const auto* p = static_cast(bytes); ++ _r->bytes.insert(_r->bytes.end(), p, p + length); ++ } ++ void setVertexBuffer(std::shared_ptr b, size_t offset, size_t index) override { addRef(Kind::VBuffer, std::move(b), offset, index); } ++ void setVertexBuffers(const std::vector>& b, const std::vector& o, Range r) override { ++ for (size_t i = 0; i < r.length; ++i) addRef(Kind::VBuffer, b[i], o[i], r.start + i); ++ } ++ void setVertexBufferOffset(size_t offset, size_t index) override { Op& o = add(Kind::VBufferOffset); o.n[0] = offset; o.n[1] = index; } ++ void setVertexSamplerState(std::shared_ptr s, size_t index) override { addRef(Kind::VSampler, std::move(s), index, 0); } ++ void setVertexSamplerState(std::shared_ptr s, float lo, float hi, size_t index) override { ++ Op& o = add(Kind::VSamplerClamp); o.p = std::move(s); o.f[0] = lo; o.f[1] = hi; o.n[0] = index; ++ } ++ void setVertexSamplerStates(const std::vector>& s, Range r) override { ++ for (size_t i = 0; i < r.length; ++i) setVertexSamplerState(s[i], r.start + i); ++ } ++ void setVertexSamplerStates(const std::vector>& s, const std::vector& lo, const std::vector& hi, Range r) override { ++ for (size_t i = 0; i < r.length; ++i) setVertexSamplerState(s[i], lo[i], hi[i], r.start + i); ++ } ++ void setVertexTexture(std::shared_ptr t, size_t index) override { addRef(Kind::VTexture, std::move(t), index, 0); } ++ void setVertexTextures(const std::vector>& t, Range r) override { ++ for (size_t i = 0; i < r.length; ++i) addRef(Kind::VTexture, t[i], r.start + i, 0); ++ } ++ ++ void setFragmentBytes(const void* bytes, size_t length, size_t index) override { ++ Op& o = add(Kind::FBytes); o.n[0] = _r->bytes.size(); o.n[1] = length; o.n[2] = index; ++ const auto* p = static_cast(bytes); ++ _r->bytes.insert(_r->bytes.end(), p, p + length); ++ } ++ void setFragmentBuffer(std::shared_ptr b, size_t offset, size_t index) override { addRef(Kind::FBuffer, std::move(b), offset, index); } ++ void setFragmentBuffers(const std::vector>& b, const std::vector& o, Range r) override { ++ for (size_t i = 0; i < r.length; ++i) addRef(Kind::FBuffer, b[i], o[i], r.start + i); ++ } ++ void setFragmentBufferOffset(size_t offset, size_t index) override { Op& o = add(Kind::FBufferOffset); o.n[0] = offset; o.n[1] = index; } ++ void setFragmentSamplerState(std::shared_ptr s, size_t index) override { addRef(Kind::FSampler, std::move(s), index, 0); } ++ void setFragmentSamplerState(std::shared_ptr s, float lo, float hi, size_t index) override { ++ Op& o = add(Kind::FSamplerClamp); o.p = std::move(s); o.f[0] = lo; o.f[1] = hi; o.n[0] = index; ++ } ++ void setFragmentSamplerStates(const std::vector>& s, Range r) override { ++ for (size_t i = 0; i < r.length; ++i) setFragmentSamplerState(s[i], r.start + i); ++ } ++ void setFragmentSamplerStates(const std::vector>& s, const std::vector& lo, const std::vector& hi, Range r) override { ++ for (size_t i = 0; i < r.length; ++i) setFragmentSamplerState(s[i], lo[i], hi[i], r.start + i); ++ } ++ void setFragmentTexture(std::shared_ptr t, size_t index) override { addRef(Kind::FTexture, std::move(t), index, 0); } ++ void setFragmentTextures(std::vector>& t, Range r) override { ++ for (size_t i = 0; i < r.length; ++i) addRef(Kind::FTexture, t[i], r.start + i, 0); ++ } ++ ++ void useResource(std::shared_ptr res, ResourceUsage usage, RenderStages stages) override { ++ Op& o = add(Kind::UseResourceStages); o.p = std::move(res); o.e1 = static_cast(usage); o.n[0] = static_cast(stages); ++ } ++ void useResources(const std::vector>& res, ResourceUsage usage, RenderStages stages) override { ++ // one op per resource: the real encoder batches barriers per call, which only affects how they are grouped ++ for (const auto& r : res) useResource(r, usage, stages); ++ } ++ void useResource(std::shared_ptr res, ResourceUsage usage) override { ++ Op& o = add(Kind::UseResource); o.p = std::move(res); o.e1 = static_cast(usage); ++ } ++ void useResources(const std::vector>& res, ResourceUsage usage) override { ++ for (const auto& r : res) useResource(r, usage); ++ } ++ void memoryBarrier(size_t scope, RenderStages after, RenderStages before) override { ++ Op& o = add(Kind::MemoryBarrier); o.n[0] = scope; o.n[1] = static_cast(after); o.n[2] = static_cast(before); ++ } ++ }; ++ } ++} +diff --git a/private-include/indium/gpu-timing.private.hpp b/private-include/indium/gpu-timing.private.hpp +new file mode 100644 +--- /dev/null ++++ b/private-include/indium/gpu-timing.private.hpp +@@ -0,0 +1,259 @@ ++#pragma once ++ ++// Diagnostic GPU profiler built on Vulkan timestamp queries (guest flag files, checked once at start): ++// /tmp/metal-gpu-timing per render pass: start/end on the GPU timeline -> /tmp/indium-gpu-timing.log ++// line per command buffer: "cb ::: ..." ++// /tmp/metal-gpu-timing-draws additionally a timestamp after every draw; GPU time is attributed to the pipeline ++// state (vertex+fragment function names in /tmp/indium-gpu-pso.log) and summed per ++// (pso, target size) into /tmp/indium-gpu-draws.log (rewritten every 5 s) ++// Header-only, function-local statics: no class gains a member (QuartzCore/Metal read Indium objects at fixed offsets). ++ ++#include ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++namespace Indium { ++ namespace GpuTiming { ++ struct Pass { ++ uint32_t first; ++ uint32_t width; ++ uint32_t height; ++ uint32_t draws; ++ std::vector drawPso; // pso id per draw timestamp, in order ++ }; ++ ++ struct State { ++ static constexpr uint32_t Capacity = 1u << 20; // 8 MB: ~1700 passes of in-flight history, no wrap hazard ++ static constexpr uint32_t DrawSlots = 600; ++ std::mutex mutex; ++ VkQueryPool pool = VK_NULL_HANDLE; ++ uint32_t next = 0; ++ std::unordered_map> passes; ++ std::map, std::pair> perPso; // ms, draws ++ std::map, std::pair> perPsoSingle; // passes with one draw ++ std::chrono::steady_clock::time_point lastDump = std::chrono::steady_clock::now(); ++ FILE* log = nullptr; ++ bool enabled = access("/tmp/metal-gpu-timing", F_OK) == 0; ++ bool drawLevel = access("/tmp/metal-gpu-timing-draws", F_OK) == 0; ++ }; ++ ++ inline State& state() { ++ static State* instance = new State; ++ return *instance; ++ } ++ ++ inline bool enabled() { return state().enabled; } ++ ++ inline thread_local uint32_t currentDraws = 0; ++ ++ inline void countDraw() { ++ if (state().enabled) ++currentDraws; ++ } ++ ++ // SPIR-V library identity: FNV-1a over the module, and (draw-level mode) the module saved as .spv ++ inline std::unordered_map& libraryIds() { ++ static auto* ids = new std::unordered_map; ++ return *ids; ++ } ++ ++ inline void noteLibrary(const void* library, const char* data, size_t length) { ++ auto& s = state(); ++ if (!s.enabled || !s.drawLevel) return; ++ uint64_t h = 1469598103934665603ull; ++ for (size_t i = 0; i < length; ++i) { h ^= (unsigned char) data[i]; h *= 1099511628211ull; } ++ char id[48]; ++ snprintf(id, sizeof(id), "%08llx:%zu", (unsigned long long) (h & 0xffffffffull), length); ++ { ++ std::lock_guard lock(s.mutex); ++ libraryIds()[library] = id; ++ } ++ char path[96]; ++ snprintf(path, sizeof(path), "/tmp/indium-shaders/%08llx.spv", (unsigned long long) (h & 0xffffffffull)); ++ mkdir("/tmp/indium-shaders", 0755); ++ if (access(path, F_OK) != 0) { ++ if (FILE* f = fopen(path, "wb")) { fwrite(data, 1, length, f); fclose(f); } ++ } ++ } ++ ++ inline std::string libraryId(const void* library) { ++ auto& s = state(); ++ std::lock_guard lock(s.mutex); ++ auto it = libraryIds().find(library); ++ return it == libraryIds().end() ? std::string("-") : it->second; ++ } ++ ++ inline void notePipeline(const void* pso, const std::string& vertex, const std::string& fragment) { ++ auto& s = state(); ++ if (!s.enabled || !s.drawLevel) return; ++ if (FILE* f = fopen("/tmp/indium-gpu-pso.log", "a")) { ++ fprintf(f, "%llx %s %s\n", (unsigned long long) (uintptr_t) pso, vertex.c_str(), fragment.c_str()); ++ fclose(f); ++ } ++ } ++ ++ // before vkCmdBeginRenderPass (outside any render pass) ++ inline void beginPass(VkDevice device, VkCommandBuffer cb, uint32_t width, uint32_t height) { ++ auto& s = state(); ++ if (!s.enabled) return; ++ std::lock_guard lock(s.mutex); ++ if (s.pool == VK_NULL_HANDLE) { ++ VkQueryPoolCreateInfo info {}; ++ info.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO; ++ info.queryType = VK_QUERY_TYPE_TIMESTAMP; ++ info.queryCount = State::Capacity; ++ if (DynamicVK::vkCreateQueryPool(device, &info, nullptr, &s.pool) != VK_SUCCESS) { ++ s.enabled = false; ++ return; ++ } ++ } ++ uint32_t block = 2 + (s.drawLevel ? State::DrawSlots : 0); ++ if (s.next + block > State::Capacity) s.next = 0; ++ uint32_t first = s.next; ++ s.next += block; ++ DynamicVK::vkCmdResetQueryPool(cb, s.pool, first, block); ++ DynamicVK::vkCmdWriteTimestamp(cb, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, s.pool, first); ++ s.passes[cb].push_back(Pass { first, width, height, 0, {} }); ++ currentDraws = 0; ++ } ++ ++ // render target configuration of every pass: " c=//... d=//" -> count, ++ // rewritten to /tmp/indium-gpu-targets.log every 5 s (pixel format/action numbers are Indium enum values) ++ inline void noteTarget(const std::string& key) { ++ auto& s = state(); ++ if (!s.enabled) return; ++ static auto* counts = new std::map; ++ static auto last = std::chrono::steady_clock::now(); ++ std::lock_guard lock(s.mutex); ++ ++(*counts)[key]; ++ auto now = std::chrono::steady_clock::now(); ++ if (now - last < std::chrono::seconds(5)) return; ++ last = now; ++ if (FILE* f = fopen("/tmp/indium-gpu-targets.log", "w")) { ++ for (const auto& [k, v] : *counts) fprintf(f, "%llu %s\n", (unsigned long long) v, k.c_str()); ++ fclose(f); ++ } ++ } ++ ++ // whole command buffer: logged like a pass with size 3x3 (first pass of the command buffer, end written at commit) ++ inline void beginSpan(VkDevice device, VkCommandBuffer cb) { beginPass(device, cb, 3, 3); } ++ ++ inline void endSpan(VkCommandBuffer cb) { ++ auto& s = state(); ++ if (!s.enabled) return; ++ std::lock_guard lock(s.mutex); ++ auto it = s.passes.find(cb); ++ if (it == s.passes.end() || it->second.empty() || it->second.front().width != 3) return; ++ DynamicVK::vkCmdWriteTimestamp(cb, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, s.pool, it->second.front().first + 1); ++ } ++ ++ // after each vkCmdDraw / vkCmdDrawIndexed ++ inline void afterDraw(VkCommandBuffer cb, uint64_t pso) { ++ auto& s = state(); ++ if (!s.enabled || !s.drawLevel) return; ++ std::lock_guard lock(s.mutex); ++ auto it = s.passes.find(cb); ++ if (it == s.passes.end() || it->second.empty()) return; ++ auto& pass = it->second.back(); ++ if (pass.drawPso.size() >= State::DrawSlots - 1) return; ++ DynamicVK::vkCmdWriteTimestamp(cb, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, s.pool, pass.first + 2 + (uint32_t) pass.drawPso.size()); ++ pass.drawPso.push_back(pso); ++ } ++ ++ // after vkCmdEndRenderPass ++ inline void endPass(VkCommandBuffer cb) { ++ auto& s = state(); ++ if (!s.enabled) return; ++ std::lock_guard lock(s.mutex); ++ auto it = s.passes.find(cb); ++ if (it == s.passes.end() || it->second.empty()) return; ++ auto& pass = it->second.back(); ++ pass.draws = currentDraws; ++ DynamicVK::vkCmdWriteTimestamp(cb, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, s.pool, pass.first + 1); ++ } ++ ++ inline void dumpDraws(State& s) { ++ if (!s.drawLevel) return; ++ auto now = std::chrono::steady_clock::now(); ++ if (now - s.lastDump < std::chrono::seconds(5)) return; ++ s.lastDump = now; ++ if (FILE* f = fopen("/tmp/indium-gpu-draws.log", "w")) { ++ for (const auto& [key, v] : s.perPso) ++ fprintf(f, "%llx %ux%u %llu %.4f\n", (unsigned long long) std::get<0>(key), std::get<1>(key), std::get<2>(key), ++ (unsigned long long) v.second, v.first); ++ fclose(f); ++ } ++ if (FILE* f = fopen("/tmp/indium-gpu-single.log", "w")) { ++ for (const auto& [key, v] : s.perPsoSingle) ++ fprintf(f, "%llx %ux%u %llu %.4f\n", (unsigned long long) std::get<0>(key), std::get<1>(key), std::get<2>(key), ++ (unsigned long long) v.second, v.first); ++ fclose(f); ++ } ++ } ++ ++ // when the command buffer has completed on the GPU ++ inline void collect(VkDevice device, VkCommandBuffer cb, double periodNs) { ++ auto& s = state(); ++ if (!s.enabled) return; ++ std::vector passes; ++ { ++ std::lock_guard lock(s.mutex); ++ auto it = s.passes.find(cb); ++ if (it == s.passes.end()) return; ++ passes = std::move(it->second); ++ s.passes.erase(it); ++ } ++ if (passes.empty()) return; ++ std::string line; ++ char buf[96]; ++ std::vector t; ++ std::vector> samples; // pso, w, h, ms ++ std::vector> singles; ++ for (const auto& pass : passes) { ++ uint32_t n = 2 + (uint32_t) pass.drawPso.size(); ++ t.assign(n, 0); ++ // the mutex is NOT held while waiting for the GPU: the render thread takes it for every pass and draw ++ if (DynamicVK::vkGetQueryPoolResults(device, s.pool, pass.first, n, n * sizeof(uint64_t), t.data(), sizeof(uint64_t), ++ VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT) != VK_SUCCESS) continue; ++ snprintf(buf, sizeof(buf), " %ux%u:%u:%.3f:%.0f", pass.width, pass.height, pass.draws, ++ (double) (t[1] - t[0]) * periodNs / 1e6, (double) t[0] * periodNs); ++ line += buf; ++ uint64_t previous = t[0]; ++ for (size_t i = 0; i < pass.drawPso.size(); ++i) { ++ uint64_t now = t[2 + i]; ++ double ms = now > previous ? (double) (now - previous) * periodNs / 1e6 : 0; ++ previous = now > previous ? now : previous; ++ samples.emplace_back(pass.drawPso[i], pass.width, pass.height, ms); ++ if (pass.drawPso.size() == 1) singles.emplace_back(pass.drawPso[i], pass.width, pass.height, (double) (t[1] - t[0]) * periodNs / 1e6); ++ } ++ } ++ std::lock_guard lock(s.mutex); ++ for (const auto& [pso, w, h, ms] : samples) { ++ auto& entry = s.perPso[std::make_tuple(pso, w, h)]; ++ entry.first += ms; ++ entry.second += 1; ++ } ++ for (const auto& [pso, w, h, ms] : singles) { ++ auto& entry = s.perPsoSingle[std::make_tuple(pso, w, h)]; ++ entry.first += ms; ++ entry.second += 1; ++ } ++ if (!s.log) s.log = fopen("/tmp/indium-gpu-timing.log", "a"); ++ if (s.log) { ++ fprintf(s.log, "cb%s\n", line.c_str()); ++ fflush(s.log); ++ } ++ dumpDraws(s); ++ } ++ } ++} +diff --git a/private-include/indium/memory-pool.private.hpp b/private-include/indium/memory-pool.private.hpp +new file mode 100644 +--- /dev/null ++++ b/private-include/indium/memory-pool.private.hpp +@@ -0,0 +1,233 @@ ++#pragma once ++ ++// Suballocation of device memory for buffers and textures. ++// ++// Every buffer and texture used to own a VkDeviceMemory. RADV hands the kernel the complete list of a device's memory ++// objects with every submission (required once buffer device addresses are in use), so vkQueueSubmit2 cost grew with the ++// number of live resources: about 1.1 ms per frame in League, 77% of it in amdgpu_cs_ioctl walking that list. ++// Resources up to MaxPooledSize now share 32 MB blocks; larger ones keep a dedicated allocation. ++// ++// Header-only on purpose (function-local statics, one instance per libindium) and keyed by the Vulkan handle of the ++// owning buffer/image, so no class gains a member: QuartzCore and Metal read some Indium objects at fixed offsets. ++// Create guest /tmp/metal-no-pool before the process starts to get one allocation per resource again. ++ ++#include ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++namespace Indium { ++ namespace MemoryPool { ++ struct Allocation { ++ VkDeviceMemory memory = VK_NULL_HANDLE; ++ VkDeviceSize offset = 0; ++ VkDeviceSize size = 0; ++ }; ++ ++ namespace Detail { ++ static constexpr VkDeviceSize BlockSize = 32ull << 20; ++ static constexpr VkDeviceSize MaxPooledSize = 4ull << 20; ++ static constexpr VkDeviceSize Granule = 256; ++ // a freed range is not handed out again before this long: work already submitted to the GPU may still read it ++ static constexpr std::chrono::milliseconds Quarantine {1000}; ++ ++ struct Block { ++ VkDeviceMemory memory = VK_NULL_HANDLE; ++ VkDeviceSize size = 0; ++ uint32_t key = 0; ++ bool dedicated = false; ++ void* mapped = nullptr; ++ size_t live = 0; ++ std::map free; // offset -> size ++ }; ++ ++ struct Entry { ++ std::shared_ptr block; ++ VkDeviceSize offset; ++ VkDeviceSize size; ++ }; ++ ++ struct Pending { ++ std::chrono::steady_clock::time_point when; ++ std::shared_ptr block; ++ VkDeviceSize offset; ++ VkDeviceSize size; ++ }; ++ ++ struct State { ++ std::mutex mutex; ++ std::unordered_map entries; ++ std::unordered_map>> blocks; ++ std::deque pending; ++ bool disabled = access("/tmp/metal-no-pool", F_OK) == 0; ++ }; ++ ++ inline State& state() { ++ static State* instance = new State; // never destroyed: resources are released during process teardown ++ return *instance; ++ } ++ ++ inline VkDeviceSize alignUp(VkDeviceSize value, VkDeviceSize alignment) { ++ return (value + alignment - 1) / alignment * alignment; ++ } ++ ++ inline VkDeviceMemory allocateMemory(VkDevice device, uint32_t typeIndex, VkDeviceSize size, bool deviceAddress) { ++ VkMemoryAllocateInfo info {}; ++ info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; ++ info.allocationSize = size; ++ info.memoryTypeIndex = typeIndex; ++ VkMemoryAllocateFlagsInfo flags {}; ++ flags.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_FLAGS_INFO; ++ flags.flags = VK_MEMORY_ALLOCATE_DEVICE_ADDRESS_BIT; ++ if (deviceAddress) info.pNext = &flags; ++ VkDeviceMemory memory = VK_NULL_HANDLE; ++ if (DynamicVK::vkAllocateMemory(device, &info, nullptr, &memory) != VK_SUCCESS) abort(); ++ return memory; ++ } ++ ++ inline void releaseRange(State& s, VkDevice device, const std::shared_ptr& block, VkDeviceSize offset, VkDeviceSize size) { ++ auto next = block->free.lower_bound(offset); ++ if (next != block->free.begin()) { ++ auto previous = std::prev(next); ++ if (previous->first + previous->second == offset) { ++ offset = previous->first; ++ size += previous->second; ++ block->free.erase(previous); ++ } ++ } ++ if (next != block->free.end() && offset + size == next->first) { ++ size += next->second; ++ block->free.erase(next); ++ } ++ block->free[offset] = size; ++ if (block->live != 0 || size != block->size) return; ++ // the block is empty: keep one spare per kind, give the others back ++ auto& list = s.blocks[block->key]; ++ size_t empty = 0; ++ for (const auto& other : list) { ++ if (other->live == 0 && other->free.size() == 1 && other->free.begin()->second == other->size) ++empty; ++ } ++ if (empty <= 1) return; ++ for (auto it = list.begin(); it != list.end(); ++it) { ++ if (*it == block) { list.erase(it); break; } ++ } ++ DynamicVK::vkFreeMemory(device, block->memory, nullptr); ++ block->memory = VK_NULL_HANDLE; ++ } ++ ++ inline void drain(State& s, VkDevice device) { ++ auto now = std::chrono::steady_clock::now(); ++ while (!s.pending.empty() && now - s.pending.front().when >= Quarantine) { ++ Pending item = std::move(s.pending.front()); ++ s.pending.pop_front(); ++ if (item.block->dedicated) { ++ DynamicVK::vkFreeMemory(device, item.block->memory, nullptr); ++ } else { ++ --item.block->live; ++ releaseRange(s, device, item.block, item.offset, item.size); ++ } ++ } ++ } ++ } ++ ++ // `owner` is the buffer or image handle the memory will be bound to ++ inline Allocation allocate(VkDevice device, uint64_t owner, uint32_t typeIndex, VkDeviceSize size, VkDeviceSize alignment, bool image, bool deviceAddress) { ++ using namespace Detail; ++ auto& s = state(); ++ std::lock_guard lock(s.mutex); ++ drain(s, device); ++ Entry entry; ++ if (s.disabled || size > MaxPooledSize) { ++ entry.block = std::make_shared(); ++ entry.block->dedicated = true; ++ entry.block->size = size; ++ entry.block->memory = allocateMemory(device, typeIndex, size, deviceAddress); ++ entry.offset = 0; ++ entry.size = size; ++ } else { ++ if (alignment < Granule) alignment = Granule; ++ size = alignUp(size, Granule); ++ const uint32_t key = (typeIndex << 2) | (image ? 2u : 0u) | (deviceAddress ? 1u : 0u); ++ auto& list = s.blocks[key]; ++ for (int attempt = 0; attempt < 2 && !entry.block; ++attempt) { ++ for (auto& block : list) { ++ for (auto it = block->free.begin(); it != block->free.end(); ++it) { ++ VkDeviceSize start = alignUp(it->first, alignment); ++ VkDeviceSize end = it->first + it->second; ++ if (start + size > end) continue; ++ VkDeviceSize rangeStart = it->first; ++ block->free.erase(it); ++ if (start > rangeStart) block->free[rangeStart] = start - rangeStart; ++ if (start + size < end) block->free[start + size] = end - (start + size); ++ entry.block = block; ++ entry.offset = start; ++ entry.size = size; ++ ++block->live; ++ break; ++ } ++ if (entry.block) break; ++ } ++ if (!entry.block && attempt == 0) { ++ auto block = std::make_shared(); ++ block->key = key; ++ block->size = BlockSize; ++ block->memory = allocateMemory(device, typeIndex, BlockSize, deviceAddress); ++ block->free[0] = BlockSize; ++ list.push_back(block); ++ } ++ } ++ if (!entry.block) abort(); ++ } ++ s.entries[owner] = entry; ++ return Allocation { entry.block->memory, entry.offset, entry.size }; ++ } ++ ++ inline bool lookup(uint64_t owner, Allocation& result) { ++ auto& s = Detail::state(); ++ std::lock_guard lock(s.mutex); ++ auto it = s.entries.find(owner); ++ if (it == s.entries.end()) return false; ++ result = Allocation { it->second.block->memory, it->second.offset, it->second.size }; ++ return true; ++ } ++ ++ // host pointer to the start of the owner's memory (the memory type must be host-visible) ++ inline void* map(VkDevice device, uint64_t owner) { ++ auto& s = Detail::state(); ++ std::lock_guard lock(s.mutex); ++ auto it = s.entries.find(owner); ++ if (it == s.entries.end()) return nullptr; ++ auto& block = it->second.block; ++ if (!block->mapped) { ++ if (DynamicVK::vkMapMemory(device, block->memory, 0, VK_WHOLE_SIZE, 0, &block->mapped) != VK_SUCCESS) abort(); ++ } ++ return static_cast(block->mapped) + it->second.offset; ++ } ++ ++ // call after the owning buffer/image has been destroyed ++ inline void free(VkDevice device, uint64_t owner) { ++ using namespace Detail; ++ auto& s = state(); ++ std::lock_guard lock(s.mutex); ++ auto it = s.entries.find(owner); ++ if (it == s.entries.end()) return; ++ Entry entry = std::move(it->second); ++ s.entries.erase(it); ++ if (entry.block->dedicated) { ++ // nothing can reuse a dedicated allocation, and the kernel keeps it alive for work in flight ++ DynamicVK::vkFreeMemory(device, entry.block->memory, nullptr); ++ } else { ++ s.pending.push_back(Pending { std::chrono::steady_clock::now(), entry.block, entry.offset, entry.size }); ++ } ++ drain(s, device); ++ } ++ } ++} +diff --git a/private-include/indium/outline-chain.private.hpp b/private-include/indium/outline-chain.private.hpp +new file mode 100644 +--- /dev/null ++++ b/private-include/indium/outline-chain.private.hpp +@@ -0,0 +1,124 @@ ++#pragma once ++ ++// League's hover/target outline is a chain of full-screen 1080p passes that ping-pong between two textures: a mask pass, ++// then N passes with one fragment shader (SPIR-V hash 87819980) and M passes with a second (f61b5318), then a composite. ++// Every pass reads the previous result, so a pass can be dropped without breaking the data flow: the next pass simply reads ++// a result with fewer iterations applied. This keeps only the first 2*K passes of the first phase of each chain. ++// ++// Live settings (guest files, re-read every 2048 draws; create them from inside the guest): ++// /tmp/metal-outline-pairs K1: passes of the first phase kept = 2*K1 (default 0; 16 keeps everything) ++// /tmp/metal-outline-pairs2 K2: passes of the second phase kept = 2*K2 (default 16 = all of them) ++// Header-only with function-local statics (no class gains a member). ++ ++#include ++#include ++#include ++#include ++#include ++#include ++ ++namespace Indium { ++ namespace OutlineChain { ++ struct State { ++ std::mutex mutex; ++ std::unordered_map libraryHash; ++ std::unordered_map pipelineKind; // 1 = first phase shader, 2 = second phase shader ++ }; ++ ++ inline State& state() { ++ static State* instance = new State; ++ return *instance; ++ } ++ ++ inline void noteLibrary(const void* library, const char* data, size_t length) { ++ uint64_t h = 1469598103934665603ull; ++ for (size_t i = 0; i < length; ++i) { h ^= (unsigned char) data[i]; h *= 1099511628211ull; } ++ auto& s = state(); ++ std::lock_guard lock(s.mutex); ++ s.libraryHash[library] = (uint32_t) (h & 0xffffffffull); ++ } ++ ++ inline void notePipeline(const void* pipeline, const void* fragmentLibrary) { ++ if (!fragmentLibrary) return; ++ auto& s = state(); ++ std::lock_guard lock(s.mutex); ++ auto it = s.libraryHash.find(fragmentLibrary); ++ if (it == s.libraryHash.end()) return; ++ if (it->second == 0x87819980u) s.pipelineKind[pipeline] = 1; ++ else if (it->second == 0xf61b5318u) s.pipelineKind[pipeline] = 2; ++ } ++ ++ inline void readSettings(int& k1, int& k2) { ++ static std::atomic v1 {0}, v2 {16}; ++ static std::atomic calls {0}; ++ if ((calls.fetch_add(1) & 2047) == 0) { // each read is two fopen()s through Darling's path translation ++ auto read = [](const char* path, int fallback) { ++ int out = fallback; ++ if (FILE* f = fopen(path, "r")) { ++ int v = -1; ++ if (fscanf(f, "%d", &v) == 1 && v >= 0 && v <= 16) out = v; ++ fclose(f); ++ } ++ return out; ++ }; ++ v1.store(read("/tmp/metal-outline-pairs", 0)); ++ v2.store(read("/tmp/metal-outline-pairs2", 16)); ++ } ++ k1 = v1.load(); ++ k2 = v2.load(); ++ } ++ ++ inline void report(int k, uint8_t kind, bool skipped, size_t vertexCount) { ++ static std::atomic draws {0}, kind1 {0}, kind1v4 {0}, kind2 {0}, skippedDraws {0}; ++ static std::atomic tick {0}; ++ ++draws; ++ if (kind == 1) ++kind1; ++ if (kind == 1 && vertexCount == 4) ++kind1v4; ++ if (kind == 2) ++kind2; ++ if (skipped) ++skippedDraws; ++ if ((tick.fetch_add(1) & 1023) == 0) { ++ auto& s = state(); ++ size_t kinds = 0, libs = 0; ++ { std::lock_guard lock(s.mutex); kinds = s.pipelineKind.size(); libs = s.libraryHash.size(); } ++ char path[64]; ++ snprintf(path, sizeof(path), "/tmp/indium-outline-%d.log", (int) getpid()); ++ if (FILE* f = fopen(path, "w")) { ++ fprintf(f, "k=%d draws=%lu kind1=%lu kind1_v4=%lu kind2=%lu skipped=%lu pipelineKinds=%zu libraries=%zu\n", k, ++ draws.load(), kind1.load(), kind1v4.load(), kind2.load(), skippedDraws.load(), kinds, libs); ++ fclose(f); ++ } ++ } ++ } ++ ++ // called for every drawPrimitives; true: skip this draw ++ inline bool shouldSkipDraw(const void* pipeline, size_t vertexCount) { ++ static thread_local unsigned run1 = 0, run2 = 0; ++ int k1 = 0, k2 = 16; ++ readSettings(k1, k2); ++ uint8_t kind = 0; ++ { ++ auto& s = state(); ++ std::lock_guard lock(s.mutex); ++ auto it = s.pipelineKind.find(pipeline); ++ if (it != s.pipelineKind.end()) kind = it->second; ++ } ++ if (vertexCount != 4 || kind == 0) { ++ // any other draw ends the chain ++ run1 = run2 = 0; ++ return false; ++ } ++ bool skip; ++ if (kind == 1) { ++ // a new chain starts with its first-phase passes ++ if (run2 != 0) { run1 = run2 = 0; } ++ skip = run1 >= (unsigned) (2 * k1); ++ ++run1; ++ } else { ++ skip = run2 >= (unsigned) (2 * k2); ++ ++run2; ++ } ++ // report(k1, kind, skip, vertexCount); // diagnostic: rewrote a guest log file every 1024 draws ++ return skip; ++ } ++ } ++} +diff --git a/private-include/indium/render-pass-key.private.hpp b/private-include/indium/render-pass-key.private.hpp +new file mode 100644 +--- /dev/null ++++ b/private-include/indium/render-pass-key.private.hpp +@@ -0,0 +1,56 @@ ++#pragma once ++#include ++#include ++#include ++#include ++ ++namespace Indium { ++// Exact descriptions, rather than Vulkan compatibility, determine reuse: load ++// actions and layouts affect preservation of the game's attachment contents. ++struct RenderPassKey { ++ std::array words {}; ++ size_t size = 0; ++ bool operator==(const RenderPassKey& other) const { ++ return size == other.size && std::equal(words.begin(), words.begin() + size, other.words.begin()); ++ } ++ struct Hash { ++ size_t operator()(const RenderPassKey& key) const { ++ size_t result = 14695981039346656037ull; ++ for (size_t i = 0; i < key.size; ++i) result = (result ^ key.words[i]) * 1099511628211ull; ++ return result; ++ } ++ }; ++}; ++inline std::optional renderPassKey(const VkRenderPassCreateInfo& info) { ++ // The encoder emits one subpass without input/resolve/preserve attachments. ++ // Unsupported structures retain the original uncached Vulkan path. ++ if (info.pNext || info.subpassCount != 1 || info.attachmentCount > 16 || info.dependencyCount > 2) ++ return std::nullopt; ++ const auto& subpass = info.pSubpasses[0]; ++ if (subpass.inputAttachmentCount || subpass.pResolveAttachments || subpass.preserveAttachmentCount || subpass.colorAttachmentCount > 16) ++ return std::nullopt; ++ RenderPassKey key; ++ auto add = [&](uint32_t word) { key.words[key.size++] = word; }; ++ add(info.flags); add(info.attachmentCount); ++ for (uint32_t i = 0; i < info.attachmentCount; ++i) { ++ const auto& a = info.pAttachments[i]; ++ add(a.flags); add(a.format); add(a.samples); add(a.loadOp); add(a.storeOp); ++ add(a.stencilLoadOp); add(a.stencilStoreOp); add(a.initialLayout); add(a.finalLayout); ++ } ++ add(subpass.flags); add(subpass.pipelineBindPoint); add(subpass.colorAttachmentCount); ++ for (uint32_t i = 0; i < subpass.colorAttachmentCount; ++i) { ++ add(subpass.pColorAttachments[i].attachment); add(subpass.pColorAttachments[i].layout); ++ } ++ add(subpass.pDepthStencilAttachment != nullptr); ++ if (subpass.pDepthStencilAttachment) { ++ add(subpass.pDepthStencilAttachment->attachment); add(subpass.pDepthStencilAttachment->layout); ++ } ++ add(info.dependencyCount); ++ for (uint32_t i = 0; i < info.dependencyCount; ++i) { ++ const auto& d = info.pDependencies[i]; ++ add(d.srcSubpass); add(d.dstSubpass); add(d.srcStageMask); add(d.dstStageMask); ++ add(d.srcAccessMask); add(d.dstAccessMask); add(d.dependencyFlags); ++ } ++ return key; ++} ++} +diff --git a/private-include/indium/resource-owner-cache.private.hpp b/private-include/indium/resource-owner-cache.private.hpp +new file mode 100644 +--- /dev/null ++++ b/private-include/indium/resource-owner-cache.private.hpp +@@ -0,0 +1,62 @@ ++#pragma once ++ ++#include ++#include ++#include ++#include ++ ++namespace Indium { ++ // Encoder-local, insert-only cache. Contiguous owners avoid allocating and ++ // freeing a hash node for every resource. Owners are never erased while ++ // recorded GPU commands can reference them, so pointer identities stay valid. ++ template class ResourceOwnerCache { ++ public: ++ struct Entry { ++ std::shared_ptr owner; ++ Private* value = nullptr; ++ bool depthFeedback = false; ++ }; ++ private: ++ std::vector _owners; ++ std::vector _buckets; // owner index + 1; zero is an empty bucket ++ ++ static size_t hash(Base* object) { ++ uint64_t value = reinterpret_cast(object); ++ value ^= value >> 30; ++ value *= UINT64_C(0xbf58476d1ce4e5b9); ++ value ^= value >> 27; ++ return static_cast(value); ++ } ++ void grow() { ++ std::vector buckets(_buckets.empty() ? 16 : _buckets.size() * 2, 0); ++ for (size_t i = 0; i < _owners.size(); ++i) { ++ size_t slot = hash(_owners[i].owner.get()) & (buckets.size() - 1); ++ while (buckets[slot]) slot = (slot + 1) & (buckets.size() - 1); ++ buckets[slot] = i + 1; ++ } ++ _buckets.swap(buckets); ++ } ++ public: ++ size_t size() const { return _owners.size(); } ++ // Returned pointers are valid until the next insertion, like vector ++ // elements. Callers copy the resolved pointer into their binding slot. ++ std::pair acquire(const std::shared_ptr& owner) { ++ if (!owner) return {nullptr, false}; ++ if (_buckets.empty()) grow(); ++ size_t slot = hash(owner.get()) & (_buckets.size() - 1); ++ while (_buckets[slot]) { ++ auto& entry = _owners[_buckets[slot] - 1]; ++ if (entry.owner.get() == owner.get()) return {&entry, false}; ++ slot = (slot + 1) & (_buckets.size() - 1); ++ } ++ if ((_owners.size() + 1) * 2 > _buckets.size()) { ++ grow(); ++ slot = hash(owner.get()) & (_buckets.size() - 1); ++ while (_buckets[slot]) slot = (slot + 1) & (_buckets.size() - 1); ++ } ++ _owners.push_back(Entry{owner, dynamic_cast(owner.get()), false}); ++ _buckets[slot] = _owners.size(); ++ return {&_owners.back(), true}; ++ } ++ }; ++} +diff --git a/private-include/indium/retirement-queue.private.hpp b/private-include/indium/retirement-queue.private.hpp +new file mode 100644 +--- /dev/null ++++ b/private-include/indium/retirement-queue.private.hpp +@@ -0,0 +1,63 @@ ++#pragma once ++ ++#include ++#include ++#include ++#include ++#include ++#include ++ ++namespace Indium { ++ // Completed GPU work only. A full queue falls back to synchronous cleanup, ++ // bounding retained resources even if the worker cannot keep up. ++ template class RetirementQueue { ++ std::array, capacity> _jobs; ++ size_t _head = 0, _count = 0; ++ bool _stopping = false; ++ std::mutex _mutex; ++ std::condition_variable _ready; ++ std::thread _worker; ++ ++ void run() { ++ for (;;) { ++ std::optional job; ++ { ++ std::unique_lock lock(_mutex); ++ _ready.wait(lock, [&] { return _count || _stopping; }); ++ if (!_count && _stopping) return; ++ job = std::move(_jobs[_head]); ++ _jobs[_head].reset(); ++ _head = (_head + 1) % capacity; ++ --_count; ++ } ++ // Resource destructors and driver calls never hold the queue lock. ++ (*job)(); ++ } ++ } ++ public: ++ RetirementQueue() : _worker([this] { run(); }) {} ++ RetirementQueue(const RetirementQueue&) = delete; ++ ~RetirementQueue() { shutdown(); } ++ ++ bool tryPush(Job&& job) { ++ { ++ std::lock_guard lock(_mutex); ++ if (_stopping || _count == capacity) return false; ++ _jobs[(_head + _count) % capacity].emplace(std::move(job)); ++ ++_count; ++ } ++ _ready.notify_one(); ++ return true; ++ } ++ // Called before Vulkan/device teardown, while all driver entry points ++ // remain valid. Drain queued jobs before joining the worker. ++ void shutdown() { ++ { ++ std::lock_guard lock(_mutex); ++ _stopping = true; ++ } ++ _ready.notify_one(); ++ if (_worker.joinable()) _worker.join(); ++ } ++ }; ++} +diff --git a/private-include/indium/submission-batch.private.hpp b/private-include/indium/submission-batch.private.hpp +new file mode 100644 +--- /dev/null ++++ b/private-include/indium/submission-batch.private.hpp +@@ -0,0 +1,54 @@ ++#pragma once ++#include ++#include ++#include ++#include ++ ++namespace Indium { ++ // Protected by the device graphics-queue mutex. Each packet preserves the ++ // original submit boundary and its semaphore waits/signals. Only the host ++ // driver call is combined; commands are not reordered or merged. ++ class SubmissionBatch { ++ static constexpr size_t capacity = 128; ++ struct Packet { ++ VkSubmitFlags flags = 0; ++ VkCommandBufferSubmitInfo command {}; ++ std::vector waits, signals; ++ }; ++ std::array _packets; ++ size_t _count = 0; ++ public: ++ bool empty() const { return _count == 0; } ++ bool full() const { return _count == capacity; } ++ void append(const VkSubmitInfo2& info) { ++ assert(!full() && !info.pNext && info.commandBufferInfoCount == 1); ++ auto& packet = _packets[_count++]; ++ packet.flags = info.flags; ++ packet.command = info.pCommandBufferInfos[0]; ++ packet.waits.clear(); packet.signals.clear(); ++ if (info.waitSemaphoreInfoCount) ++ packet.waits.assign(info.pWaitSemaphoreInfos, info.pWaitSemaphoreInfos + info.waitSemaphoreInfoCount); ++ if (info.signalSemaphoreInfoCount) ++ packet.signals.assign(info.pSignalSemaphoreInfos, info.pSignalSemaphoreInfos + info.signalSemaphoreInfoCount); ++ } ++ template VkResult flush(Submit&& submit) { ++ if (empty()) return VK_SUCCESS; ++ std::array infos {}; ++ for (size_t i = 0; i < _count; ++i) { ++ const auto& packet = _packets[i]; ++ auto& info = infos[i]; ++ info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2; ++ info.flags = packet.flags; ++ info.commandBufferInfoCount = 1; ++ info.pCommandBufferInfos = &packet.command; ++ info.waitSemaphoreInfoCount = packet.waits.size(); ++ info.pWaitSemaphoreInfos = packet.waits.data(); ++ info.signalSemaphoreInfoCount = packet.signals.size(); ++ info.pSignalSemaphoreInfos = packet.signals.data(); ++ } ++ VkResult result = submit(static_cast(_count), infos.data()); ++ if (result == VK_SUCCESS) _count = 0; ++ return result; ++ } ++ }; ++} +diff --git a/private-include/indium/submit-batch.private.hpp b/private-include/indium/submit-batch.private.hpp +new file mode 100644 +--- /dev/null ++++ b/private-include/indium/submit-batch.private.hpp +@@ -0,0 +1,12 @@ ++#pragma once ++ ++// Merging consecutive command buffer submissions into one vkQueueSubmit2 batch (see command-buffer.cpp). ++// League commits ~30 small command buffers per frame; each used to cost its own queue submit (an amdgpu CS ioctl plus ++// timeline-semaphore ioctls). Opt-in: guest flag file /tmp/metal-submit-batch (checked once). ++// A pending batch is flushed before anything that waits for or depends on submitted work (presenting commit, waitUntilCompleted, ++// texture uploads) and by a 2 ms timer. ++ ++namespace Indium { ++ bool submitBatchEnabled(); ++ void flushPendingSubmits(); ++} +diff --git a/private-include/indium/submit-thread.private.hpp b/private-include/indium/submit-thread.private.hpp +new file mode 100644 +--- /dev/null ++++ b/private-include/indium/submit-thread.private.hpp +@@ -0,0 +1,136 @@ ++#pragma once ++ ++// Ordered queue-submission worker. ++// ++// PrivateCommandBuffer::commit() used to do all its Vulkan queue work (semaphore reservation, vkQueueSubmit2, drawable ++// presentation) on the game's main thread: about 1.9 ms per frame in League's Baron fight, 77% of it inside the kernel's ++// amdgpu CS ioctl. The caller thread now only finishes recording; one FIFO worker performs the queue work, so the ++// submission order equals the commit order (timeline values are reserved inside the same task, in that order). ++// Every other path that touches the graphics queue (texture uploads) calls drain() first so it cannot overtake a ++// pending commit. Opt-in: guest flag file /tmp/metal-submit-thread (checked once) enables it; the A/B showed no gain, so submission stays synchronous. ++// Header-only with function-local statics: no class gains a member. ++ ++#include ++#include ++#include ++#include ++#include ++#include ++ ++namespace Indium { ++ namespace SubmitThread { ++ struct State { ++ std::mutex mutex; ++ std::condition_variable wake; ++ std::condition_variable idle; ++ std::deque> queue; ++ size_t active = 0; ++ bool started = false; ++ const bool enabled = access("/tmp/metal-submit-thread", F_OK) == 0; ++ }; ++ ++ inline State& state() { ++ static State* instance = new State; ++ return *instance; ++ } ++ ++ inline bool& workerThread() { ++ static thread_local bool value = false; ++ return value; ++ } ++ ++ inline bool onWorker() { return workerThread(); } ++ ++ inline void loop() { ++ workerThread() = true; ++ auto& s = state(); ++ for (;;) { ++ std::function task; ++ { ++ std::unique_lock lock(s.mutex); ++ s.wake.wait(lock, [&] { return !s.queue.empty(); }); ++ task = std::move(s.queue.front()); ++ s.queue.pop_front(); ++ ++s.active; ++ } ++ task(); ++ { ++ std::lock_guard lock(s.mutex); ++ --s.active; ++ if (s.queue.empty() && s.active == 0) s.idle.notify_all(); ++ } ++ } ++ } ++ ++ // run `task` after every task queued before it; synchronously when disabled or already on the worker ++ inline void run(std::function task) { ++ auto& s = state(); ++ if (!s.enabled || onWorker()) { ++ task(); ++ return; ++ } ++ bool needsWake; ++ { ++ std::lock_guard lock(s.mutex); ++ if (!s.started) { ++ s.started = true; ++ std::thread thread(loop); ++ thread.detach(); ++ } ++ // An active worker checks the queue before sleeping again. Only ++ // the idle/empty transition needs a wake; test under the same ++ // mutex as the worker's wait to avoid losing that transition. ++ needsWake = s.active == 0 && s.queue.empty(); ++ s.queue.push_back(std::move(task)); ++ } ++ if (needsWake) s.wake.notify_one(); ++ } ++ ++ // start the worker now (called when the device is created): a thread created lazily later would inherit the ++ // affinity of whichever thread happened to create it (the game's main thread is pinned to its own core) ++ inline void ensureStarted() { ++ auto& s = state(); ++ std::lock_guard lock(s.mutex); ++ if (!s.started) { ++ s.started = true; ++ std::thread thread(loop); ++ thread.detach(); ++ } ++ } ++ ++ // like run(), but always on the worker (deferred render encoding uses the same FIFO, regardless of the opt-in flag) ++ inline void post(std::function task) { ++ auto& s = state(); ++ bool needsWake; ++ { ++ std::lock_guard lock(s.mutex); ++ if (!s.started) { ++ s.started = true; ++ std::thread thread(loop); ++ thread.detach(); ++ } ++ // An active worker checks the queue before sleeping again. Only ++ // the idle/empty transition needs a wake; test under the same ++ // mutex as the worker's wait to avoid losing that transition. ++ needsWake = s.active == 0 && s.queue.empty(); ++ s.queue.push_back(std::move(task)); ++ } ++ if (needsWake) s.wake.notify_one(); ++ } ++ ++ // true while the worker has queued or running tasks ++ inline bool pending() { ++ auto& s = state(); ++ std::lock_guard lock(s.mutex); ++ return !s.queue.empty() || s.active != 0; ++ } ++ ++ // block until every task queued so far has finished (no-op on the worker itself) ++ inline void drain() { ++ auto& s = state(); ++ if (onWorker()) return; ++ std::unique_lock lock(s.mutex); ++ s.idle.wait(lock, [&] { return s.queue.empty() && s.active == 0; }); ++ } ++ } ++} diff --git a/patches/metal.patch b/patches/metal.patch new file mode 100644 index 0000000..c636544 --- /dev/null +++ b/patches/metal.patch @@ -0,0 +1,3124 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 431f29a..7f10875 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -42,8 +42,10 @@ add_framework(Metal + SOURCES + src/Metal/Metal.mm + src/Metal/MTLBuffer.mm ++ src/Metal/MTLArgument.mm + src/Metal/MTLCaptureManager.mm + src/Metal/MTLCommandBuffer.mm ++ src/Metal/MTLBlitCommandEncoder.mm + src/Metal/MTLCommandQueue.mm + src/Metal/MTLCompileOptions.mm + src/Metal/MTLComputeCommandEncoder.mm +diff --git a/include/Metal/MTLCommandBuffer.h b/include/Metal/MTLCommandBuffer.h +index ef43df8..9045a84 100644 +--- a/include/Metal/MTLCommandBuffer.h ++++ b/include/Metal/MTLCommandBuffer.h +@@ -16,6 +16,7 @@ METAL_DECLARATIONS_BEGIN + @protocol MTLComputeCommandEncoder; + @protocol MTLCommandQueue; + @protocol MTLRenderCommandEncoder; ++@protocol MTLBlitCommandEncoder; + + @class MTLComputePassDescriptor; + @class MTLRenderPassDescriptor; +@@ -36,9 +37,11 @@ typedef void (^MTLCommandBufferHandler)(id); + - (id)computeCommandEncoderWithDescriptor: (MTLComputePassDescriptor*)computePassDescriptor; + - (id)computeCommandEncoderWithDispatchType: (MTLDispatchType)dispatchType; + - (id)computeCommandEncoder; ++- (id)blitCommandEncoder; + + - (id)renderCommandEncoderWithDescriptor: (MTLRenderPassDescriptor*)renderPassDescriptor; + ++- (void)addScheduledHandler:(MTLCommandBufferHandler)block; + - (void)addCompletedHandler: (MTLCommandBufferHandler)block; + - (void)waitUntilCompleted; + - (void)presentDrawable: (id)drawable; +diff --git a/include/Metal/MTLComputeCommandEncoder.h b/include/Metal/MTLComputeCommandEncoder.h +index 06f4e51..a8e7436 100644 +--- a/include/Metal/MTLComputeCommandEncoder.h ++++ b/include/Metal/MTLComputeCommandEncoder.h +@@ -13,6 +13,9 @@ METAL_DECLARATIONS_BEGIN + @protocol MTLComputeCommandEncoder; + @protocol MTLCounterSampleBuffer; + @protocol MTLBuffer; ++@protocol MTLTexture; ++@protocol MTLSamplerState; ++@protocol MTLDepthStencilState; + + @class MTLComputePassDescriptor; + @class MTLComputePassSampleBufferAttachmentDescriptorArray; +@@ -53,6 +56,9 @@ MTL_EXPORT + + @property(readonly) MTLDispatchType dispatchType; + ++- (void)setTexture:(id)texture atIndex:(NSUInteger)index; ++- (void)setSamplerState:(id)sampler atIndex:(NSUInteger)index; ++- (void)setSamplerState:(id)sampler lodMinClamp:(float)lodMinClamp lodMaxClamp:(float)lodMaxClamp atIndex:(NSUInteger)index; + - (void)setComputePipelineState:(id)state; + + - (void)setBuffer: (id)buffer +diff --git a/include/Metal/MTLDepthStencilDescriptor.h b/include/Metal/MTLDepthStencilDescriptor.h +index f22a57d..6b6715d 100644 +--- a/include/Metal/MTLDepthStencilDescriptor.h ++++ b/include/Metal/MTLDepthStencilDescriptor.h +@@ -3,10 +3,17 @@ + + #ifndef _METAL_MTLDEPTHSTENCILDESCRIPTOR_H_ + #define _METAL_MTLDEPTHSTENCILDESCRIPTOR_H_ +- +-#import +- +-@interface MTLDepthStencilDescriptor : NSObject ++#import ++@protocol MTLDevice; ++@interface MTLDepthStencilDescriptor : NSObject ++@property(nonatomic) MTLCompareFunction depthCompareFunction; ++@property(nonatomic) BOOL depthWriteEnabled; ++@property(nonatomic, copy) MTLStencilDescriptor* frontFaceStencil; ++@property(nonatomic, copy) MTLStencilDescriptor* backFaceStencil; ++@property(nonatomic, copy) NSString* label; + @end +- +-#endif // _METAL_MTLDEPTHSTENCILDESCRIPTOR_H_ ++@protocol MTLDepthStencilState ++@property(readonly) id device; ++@property(readonly) NSString* label; ++@end ++#endif +diff --git a/include/Metal/MTLDevice.h b/include/Metal/MTLDevice.h +index a3efc96..299c038 100644 +--- a/include/Metal/MTLDevice.h ++++ b/include/Metal/MTLDevice.h +@@ -18,9 +18,18 @@ METAL_DECLARATIONS_BEGIN + @protocol MTLBuffer; + @protocol MTLLibrary; + @protocol MTLRenderPipelineState; ++@protocol MTLSamplerState; ++@protocol MTLDepthStencilState; ++@class MTLDepthStencilDescriptor; ++@class MTLSamplerDescriptor; ++@class MTLTextureDescriptor; ++@protocol MTLTexture; + + @class MTLComputePipelineDescriptor; +-@class MTLAutoreleasedComputePipelineReflection; ++@class MTLComputePipelineReflection; ++@class MTLRenderPipelineReflection; ++typedef MTLComputePipelineReflection* MTLAutoreleasedComputePipelineReflection; ++typedef MTLRenderPipelineReflection* MTLAutoreleasedRenderPipelineReflection; + @class MTLRenderPipelineDescriptor; + + typedef NS_OPTIONS(NSUInteger, MTLPipelineOption) { +@@ -42,8 +51,31 @@ MTL_EXPORT NSArray>* MTLCopyAllDevices(void); + MTL_EXPORT NSArray>* MTLCopyAllDevicesWithObserver(id* observer, MTLDeviceNotificationHandler handler); + MTL_EXPORT void MTLRemoveDeviceObserver(id observer); + ++typedef NS_ENUM(NSInteger, MTLReadWriteTextureTier) { ++ MTLReadWriteTextureTierNone = 0, ++ MTLReadWriteTextureTier1 = 1, ++ MTLReadWriteTextureTier2 = 2, ++}; ++ + @protocol MTLDevice + ++@property (readonly) MTLReadWriteTextureTier readWriteTextureSupport; ++ ++/* darling-lol M57: additional device queries ANGLE's Metal backend probes. */ ++@property (readonly) BOOL hasUnifiedMemory; ++@property (readonly) NSUInteger maxBufferLength; ++@property (readonly) NSUInteger recommendedMaxWorkingSetSize; ++@property (readonly) BOOL isLowPower; ++@property (readonly) BOOL isRemovable; ++@property (readonly) BOOL isHeadless; ++@property (readonly) NSString* name; ++@property (readonly) uint64_t registryID; ++- (BOOL)isDepth24Stencil8PixelFormatSupported; ++- (BOOL)supportsFamily: (NSUInteger)family; ++- (BOOL)supportsFeatureSet: (NSUInteger)featureSet; ++- (BOOL)supportsTextureSampleCount: (NSUInteger)sampleCount; ++- (id)newCommandQueueWithMaxCommandBufferCount: (NSUInteger)maxCommandBufferCount; ++ + - (id)newComputePipelineStateWithDescriptor: (MTLComputePipelineDescriptor*)descriptor + options: (MTLPipelineOption)options + reflection: (MTLAutoreleasedComputePipelineReflection*)reflection +@@ -57,10 +89,14 @@ MTL_EXPORT void MTLRemoveDeviceObserver(id observer); + reflection: (MTLAutoreleasedComputePipelineReflection*)reflection + error: (NSError**)error; + ++- (id)newRenderPipelineStateWithDescriptor:(MTLRenderPipelineDescriptor*)descriptor options:(MTLPipelineOption)options reflection:(MTLAutoreleasedRenderPipelineReflection*)reflection error:(NSError**)error; + - (id)newRenderPipelineStateWithDescriptor: (MTLRenderPipelineDescriptor*)descriptor + error: (NSError**)error; + ++- (id)newTextureWithDescriptor:(MTLTextureDescriptor*)descriptor; + - (id)newCommandQueue; ++- (id)newDepthStencilStateWithDescriptor:(MTLDepthStencilDescriptor*)descriptor; ++- (id)newSamplerStateWithDescriptor:(MTLSamplerDescriptor*)descriptor; + + - (id)newBufferWithLength: (NSUInteger)length + options: (MTLResourceOptions)options; +diff --git a/include/Metal/MTLRenderCommandEncoder.h b/include/Metal/MTLRenderCommandEncoder.h +index 5a3845c..caeab7c 100644 +--- a/include/Metal/MTLRenderCommandEncoder.h ++++ b/include/Metal/MTLRenderCommandEncoder.h +@@ -12,6 +12,9 @@ + METAL_DECLARATIONS_BEGIN + + @protocol MTLBuffer; ++@protocol MTLTexture; ++@protocol MTLSamplerState; ++@protocol MTLDepthStencilState; + @protocol MTLRasterizationRateMap; + @protocol MTLTexture; + @protocol MTLRenderPipelineState; +@@ -26,6 +29,13 @@ METAL_DECLARATIONS_BEGIN + struct MTLSamplePosition; + typedef struct MTLSamplePosition MTLSamplePosition; + ++typedef NS_OPTIONS(NSUInteger, MTLBarrierScope) { ++ MTLBarrierScopeBuffers = 1, MTLBarrierScopeTextures = 2, MTLBarrierScopeRenderTargets = 4, ++}; ++typedef NS_OPTIONS(NSUInteger, MTLRenderStages) { ++ MTLRenderStageVertex = 1, MTLRenderStageFragment = 2, MTLRenderStageTile = 4, ++ MTLRenderStageObject = 8, MTLRenderStageMesh = 16, ++}; + typedef NS_ENUM(NSUInteger, MTLLoadAction) { + MTLLoadActionDontCare = 0, + MTLLoadActionLoad = 1, +@@ -98,10 +108,10 @@ typedef struct MTLViewport { + } MTLViewport; + + typedef struct MTLScissorRect { +- size_t height; +- size_t width; +- size_t x; +- size_t y; ++ NSUInteger x; ++ NSUInteger y; ++ NSUInteger width; ++ NSUInteger height; + } MTLScissorRect; + + NS_INLINE +@@ -190,6 +200,14 @@ MTL_EXPORT + + @protocol MTLRenderCommandEncoder + ++- (void)setVertexTexture:(id)texture atIndex:(NSUInteger)index; ++- (void)setFragmentTexture:(id)texture atIndex:(NSUInteger)index; ++- (void)setVertexSamplerState:(id)sampler atIndex:(NSUInteger)index; ++- (void)setFragmentSamplerState:(id)sampler atIndex:(NSUInteger)index; ++- (void)setDepthBias:(float)depthBias slopeScale:(float)slopeScale clamp:(float)clamp; ++- (void)setDepthStencilState:(id)state; ++- (void)memoryBarrierWithScope:(MTLBarrierScope)scope afterStages:(MTLRenderStages)after beforeStages:(MTLRenderStages)before; ++- (void)setStencilReferenceValue:(uint32_t)value; + - (void)setRenderPipelineState: (id)pipelineState; + - (void)setTriangleFillMode: (MTLTriangleFillMode)fillMode; + - (void)setFrontFacingWinding: (MTLWinding)frontFacingWinding; +diff --git a/include/Metal/MTLSamplerDescriptor.h b/include/Metal/MTLSamplerDescriptor.h +index f049017..c8026d3 100644 +--- a/include/Metal/MTLSamplerDescriptor.h ++++ b/include/Metal/MTLSamplerDescriptor.h +@@ -1,12 +1,46 @@ +-// SPDX-FileCopyrightText: 2026 Darling Developers + // SPDX-License-Identifier: MPL-2.0 +- +-#ifndef _METAL_MTLSAMPLERDESCRIPTOR_H_ +-#define _METAL_MTLSAMPLERDESCRIPTOR_H_ +- ++#pragma once + #import +- +-@interface MTLSamplerDescriptor : NSObject ++typedef NS_ENUM(NSUInteger, MTLSamplerMinMagFilter) { ++ MTLSamplerMinMagFilterNearest = 0, ++ MTLSamplerMinMagFilterLinear = 1, ++}; ++typedef NS_ENUM(NSUInteger, MTLSamplerMipFilter) { ++ MTLSamplerMipFilterNotMipmapped = 0, ++ MTLSamplerMipFilterNearest = 1, ++ MTLSamplerMipFilterLinear = 2, ++}; ++typedef NS_ENUM(NSUInteger, MTLSamplerAddressMode) { ++ MTLSamplerAddressModeClampToEdge = 0, ++ MTLSamplerAddressModeMirrorClampToEdge = 1, ++ MTLSamplerAddressModeRepeat = 2, ++ MTLSamplerAddressModeMirrorRepeat = 3, ++ MTLSamplerAddressModeClampToZero = 4, ++ MTLSamplerAddressModeClampToBorderColor = 5, ++}; ++typedef NS_ENUM(NSUInteger, MTLSamplerBorderColor) { ++ MTLSamplerBorderColorTransparentBlack = 0, ++ MTLSamplerBorderColorOpaqueBlack = 1, ++ MTLSamplerBorderColorOpaqueWhite = 2, ++}; ++@protocol MTLDevice; ++@protocol MTLSamplerState ++@property(readonly) id device; ++@property(readonly) NSString* label; ++@end ++@interface MTLSamplerDescriptor : NSObject ++@property MTLSamplerMinMagFilter minFilter; ++@property MTLSamplerMinMagFilter magFilter; ++@property MTLSamplerMipFilter mipFilter; ++@property NSUInteger maxAnisotropy; ++@property MTLSamplerAddressMode sAddressMode; ++@property MTLSamplerAddressMode tAddressMode; ++@property MTLSamplerAddressMode rAddressMode; ++@property MTLSamplerBorderColor borderColor; ++@property BOOL normalizedCoordinates; ++@property float lodMinClamp; ++@property float lodMaxClamp; ++@property BOOL supportArgumentBuffers; ++@property NSUInteger compareFunction; ++@property(copy) NSString* label; + @end +- +-#endif // _METAL_MTLSAMPLERDESCRIPTOR_H_ +diff --git a/include/Metal/MTLStencilDescriptor.h b/include/Metal/MTLStencilDescriptor.h +index 9dd6f91..ebe0baf 100644 +--- a/include/Metal/MTLStencilDescriptor.h ++++ b/include/Metal/MTLStencilDescriptor.h +@@ -3,10 +3,33 @@ + + #ifndef _METAL_MTLSTENCILDESCRIPTOR_H_ + #define _METAL_MTLSTENCILDESCRIPTOR_H_ +- + #import +- +-@interface MTLStencilDescriptor : NSObject ++typedef NS_ENUM(NSUInteger, MTLCompareFunction) { ++ MTLCompareFunctionNever = 0, ++ MTLCompareFunctionLess = 1, ++ MTLCompareFunctionEqual = 2, ++ MTLCompareFunctionLessEqual = 3, ++ MTLCompareFunctionGreater = 4, ++ MTLCompareFunctionNotEqual = 5, ++ MTLCompareFunctionGreaterEqual = 6, ++ MTLCompareFunctionAlways = 7, ++}; ++typedef NS_ENUM(NSUInteger, MTLStencilOperation) { ++ MTLStencilOperationKeep = 0, ++ MTLStencilOperationZero = 1, ++ MTLStencilOperationReplace = 2, ++ MTLStencilOperationIncrementClamp = 3, ++ MTLStencilOperationDecrementClamp = 4, ++ MTLStencilOperationInvert = 5, ++ MTLStencilOperationIncrementWrap = 6, ++ MTLStencilOperationDecrementWrap = 7, ++}; ++@interface MTLStencilDescriptor : NSObject ++@property(nonatomic) MTLCompareFunction stencilCompareFunction; ++@property(nonatomic) MTLStencilOperation stencilFailureOperation; ++@property(nonatomic) MTLStencilOperation depthFailureOperation; ++@property(nonatomic) MTLStencilOperation depthStencilPassOperation; ++@property(nonatomic) uint32_t readMask; ++@property(nonatomic) uint32_t writeMask; + @end +- +-#endif // _METAL_MTLSTENCILDESCRIPTOR_H_ ++#endif +diff --git a/include/Metal/MTLTextureDescriptor.h b/include/Metal/MTLTextureDescriptor.h +index 959d552..02e147f 100644 +--- a/include/Metal/MTLTextureDescriptor.h ++++ b/include/Metal/MTLTextureDescriptor.h +@@ -1,12 +1,22 @@ +-// SPDX-FileCopyrightText: 2026 Darling Developers + // SPDX-License-Identifier: MPL-2.0 +- +-#ifndef _METAL_MTLTEXTUREDESCRIPTOR_H_ +-#define _METAL_MTLTEXTUREDESCRIPTOR_H_ +- +-#import +- +-@interface MTLTextureDescriptor : NSObject ++#pragma once ++#import ++@interface MTLTextureDescriptor : NSObject ++@property MTLTextureType textureType; ++@property MTLPixelFormat pixelFormat; ++@property NSUInteger width; ++@property NSUInteger height; ++@property NSUInteger depth; ++@property NSUInteger mipmapLevelCount; ++@property NSUInteger sampleCount; ++@property NSUInteger arrayLength; ++@property MTLResourceOptions resourceOptions; ++@property BOOL allowGPUOptimizedContents; ++@property MTLTextureUsage usage; ++@property MTLTextureSwizzleChannels swizzle; ++@property MTLCPUCacheMode cpuCacheMode; ++@property MTLStorageMode storageMode; ++@property MTLHazardTrackingMode hazardTrackingMode; +++ (instancetype)texture2DDescriptorWithPixelFormat:(MTLPixelFormat)format width:(NSUInteger)width height:(NSUInteger)height mipmapped:(BOOL)mipmapped; +++ (instancetype)textureCubeDescriptorWithPixelFormat:(MTLPixelFormat)format size:(NSUInteger)size mipmapped:(BOOL)mipmapped; + @end +- +-#endif // _METAL_MTLTEXTUREDESCRIPTOR_H_ +diff --git a/include/Metal/MTLVertexDescriptor.h b/include/Metal/MTLVertexDescriptor.h +index 2084d6f..366dfa7 100644 +--- a/include/Metal/MTLVertexDescriptor.h ++++ b/include/Metal/MTLVertexDescriptor.h +@@ -3,10 +3,90 @@ + + #ifndef _METAL_MTLVERTEXDESCRIPTOR_H_ + #define _METAL_MTLVERTEXDESCRIPTOR_H_ +- + #import +- +-@interface MTLVertexDescriptor : NSObject ++typedef NS_ENUM(NSUInteger, MTLVertexFormat) { ++ MTLVertexFormatInvalid = 0, ++ MTLVertexFormatUChar2 = 1, ++ MTLVertexFormatUChar3 = 2, ++ MTLVertexFormatUChar4 = 3, ++ MTLVertexFormatChar2 = 4, ++ MTLVertexFormatChar3 = 5, ++ MTLVertexFormatChar4 = 6, ++ MTLVertexFormatUChar2Normalized = 7, ++ MTLVertexFormatUChar3Normalized = 8, ++ MTLVertexFormatUChar4Normalized = 9, ++ MTLVertexFormatChar2Normalized = 10, ++ MTLVertexFormatChar3Normalized = 11, ++ MTLVertexFormatChar4Normalized = 12, ++ MTLVertexFormatUShort2 = 13, ++ MTLVertexFormatUShort3 = 14, ++ MTLVertexFormatUShort4 = 15, ++ MTLVertexFormatShort2 = 16, ++ MTLVertexFormatShort3 = 17, ++ MTLVertexFormatShort4 = 18, ++ MTLVertexFormatUShort2Normalized = 19, ++ MTLVertexFormatUShort3Normalized = 20, ++ MTLVertexFormatUShort4Normalized = 21, ++ MTLVertexFormatShort2Normalized = 22, ++ MTLVertexFormatShort3Normalized = 23, ++ MTLVertexFormatShort4Normalized = 24, ++ MTLVertexFormatHalf2 = 25, ++ MTLVertexFormatHalf3 = 26, ++ MTLVertexFormatHalf4 = 27, ++ MTLVertexFormatFloat = 28, ++ MTLVertexFormatFloat2 = 29, ++ MTLVertexFormatFloat3 = 30, ++ MTLVertexFormatFloat4 = 31, ++ MTLVertexFormatInt = 32, ++ MTLVertexFormatInt2 = 33, ++ MTLVertexFormatInt3 = 34, ++ MTLVertexFormatInt4 = 35, ++ MTLVertexFormatUInt = 36, ++ MTLVertexFormatUInt2 = 37, ++ MTLVertexFormatUInt3 = 38, ++ MTLVertexFormatUInt4 = 39, ++ MTLVertexFormatInt1010102Normalized = 40, ++ MTLVertexFormatUInt1010102Normalized = 41, ++ MTLVertexFormatUChar4Normalized_BGRA = 42, ++ MTLVertexFormatUChar = 45, ++ MTLVertexFormatChar = 46, ++ MTLVertexFormatUCharNormalized = 47, ++ MTLVertexFormatCharNormalized = 48, ++ MTLVertexFormatUShort = 49, ++ MTLVertexFormatShort = 50, ++ MTLVertexFormatUShortNormalized = 51, ++ MTLVertexFormatShortNormalized = 52, ++ MTLVertexFormatHalf = 53, ++}; ++typedef NS_ENUM(NSUInteger, MTLVertexStepFunction) { ++ MTLVertexStepFunctionConstant = 0, ++ MTLVertexStepFunctionPerVertex = 1, ++ MTLVertexStepFunctionPerInstance = 2, ++ MTLVertexStepFunctionPerPatch = 3, ++ MTLVertexStepFunctionPerPatchControlPoint = 4, ++}; ++@interface MTLVertexBufferLayoutDescriptor : NSObject ++@property(nonatomic) NSUInteger stride; ++@property(nonatomic) MTLVertexStepFunction stepFunction; ++@property(nonatomic) NSUInteger stepRate; + @end +- +-#endif // _METAL_MTLVERTEXDESCRIPTOR_H_ ++@interface MTLVertexBufferLayoutDescriptorArray : NSObject ++- (MTLVertexBufferLayoutDescriptor*)objectAtIndexedSubscript:(NSUInteger)index; ++- (void)setObject:(MTLVertexBufferLayoutDescriptor*)descriptor atIndexedSubscript:(NSUInteger)index; ++@end ++@interface MTLVertexAttributeDescriptor : NSObject ++@property(nonatomic) MTLVertexFormat format; ++@property(nonatomic) NSUInteger offset; ++@property(nonatomic) NSUInteger bufferIndex; ++@end ++@interface MTLVertexAttributeDescriptorArray : NSObject ++- (MTLVertexAttributeDescriptor*)objectAtIndexedSubscript:(NSUInteger)index; ++- (void)setObject:(MTLVertexAttributeDescriptor*)descriptor atIndexedSubscript:(NSUInteger)index; ++@end ++@interface MTLVertexDescriptor : NSObject +++ (instancetype)vertexDescriptor; ++@property(readonly) MTLVertexBufferLayoutDescriptorArray* layouts; ++@property(readonly) MTLVertexAttributeDescriptorArray* attributes; ++- (void)reset; ++@end ++#endif +diff --git a/include/Metal/Metal.h b/include/Metal/Metal.h +index 3c63a1d..a9c0dc7 100644 +--- a/include/Metal/Metal.h ++++ b/include/Metal/Metal.h +@@ -5,6 +5,8 @@ + #define _METAL_METAL_H_ + + #import ++#import ++#import + #import + #import + #import +diff --git a/private-include/Metal/MTLBufferInternal.h b/private-include/Metal/MTLBufferInternal.h +index 0a620b2..79cf40a 100644 +--- a/private-include/Metal/MTLBufferInternal.h ++++ b/private-include/Metal/MTLBufferInternal.h +@@ -13,7 +13,7 @@ + @interface MTLBufferInternal : NSObject + + #if DARLING_METAL_ENABLED +-@property(readonly) std::shared_ptr buffer; ++@property(nonatomic, readonly) std::shared_ptr buffer; + + - (instancetype)initWithBuffer: (std::shared_ptr)buffer + device: (id)device +@@ -21,5 +21,8 @@ + #endif + + @end ++// Direct access for the encoders: the property getter is a message send that returns the shared_ptr by value. ++const std::shared_ptr& MTLBufferGetIndium(id object); ++ + + #endif // _METAL_MTLBUFFERINTERNAL_H_ +diff --git a/private-include/Metal/MTLRenderPipelineInternal.h b/private-include/Metal/MTLRenderPipelineInternal.h +index 2463e97..750a245 100644 +--- a/private-include/Metal/MTLRenderPipelineInternal.h ++++ b/private-include/Metal/MTLRenderPipelineInternal.h +@@ -39,7 +39,7 @@ METAL_DECLARATIONS_BEGIN + @interface MTLRenderPipelineStateInternal : NSObject + + #if DARLING_METAL_ENABLED +-@property(readonly) std::shared_ptr state; ++@property(nonatomic, readonly) std::shared_ptr state; + + - (instancetype)initWithState: (std::shared_ptr)state + device: (id)device +@@ -47,6 +47,9 @@ METAL_DECLARATIONS_BEGIN + #endif + + @end ++// Direct access for the encoders: the property getter is a message send that returns the shared_ptr by value. ++const std::shared_ptr& MTLRenderPipelineGetIndium(id object); ++ + + METAL_DECLARATIONS_END + +diff --git a/private-include/Metal/MTLTextureInternal.h b/private-include/Metal/MTLTextureInternal.h +index 09572bf..f8e2977 100644 +--- a/private-include/Metal/MTLTextureInternal.h ++++ b/private-include/Metal/MTLTextureInternal.h +@@ -37,7 +37,7 @@ MTL_EXPORT + @interface MTLTextureInternal : NSObject + + #if DARLING_METAL_ENABLED +-@property(readonly) std::shared_ptr texture; ++@property(nonatomic, readonly) std::shared_ptr texture; + + - (instancetype)initWithTexture: (std::shared_ptr)texture + device: (id)device +@@ -45,5 +45,8 @@ MTL_EXPORT + #endif + + @end ++// Direct access for the encoders: the property getter is a message send that returns the shared_ptr by value. ++const std::shared_ptr& MTLTextureGetIndium(id object); ++ + + #endif // _METAL_MTLTEXTUREINTERNAL_H_ +diff --git a/src/Metal/MTLBuffer.mm b/src/Metal/MTLBuffer.mm +index e3382a8..1182427 100644 +--- a/src/Metal/MTLBuffer.mm ++++ b/src/Metal/MTLBuffer.mm +@@ -11,6 +11,10 @@ + #if DARLING_METAL_ENABLED + + @synthesize buffer = _buffer; ++const std::shared_ptr& MTLBufferGetIndium(id object) { ++ static const std::shared_ptr none; ++ return object ? ((MTLBufferInternal*) object)->_buffer : none; ++} + @synthesize device = _device; + @synthesize resourceOptions = _resourceOptions; + @synthesize label = _label; +diff --git a/src/Metal/MTLCommandBuffer.mm b/src/Metal/MTLCommandBuffer.mm +index 7dedaa0..5eca991 100644 +--- a/src/Metal/MTLCommandBuffer.mm ++++ b/src/Metal/MTLCommandBuffer.mm +@@ -1,7 +1,18 @@ + // SPDX-FileCopyrightText: 2026 Darling Developers + // SPDX-License-Identifier: MPL-2.0 + ++#import ++#import ++#import ++#include ++#include ++#include ++#include ++#include ++#include ++#include + #import ++#import + #import + #import + #import +@@ -9,6 +20,199 @@ + #import + #import + ++ ++#if DARLING_METAL_ENABLED ++// Diagnostic (MTLDebugEnabled only): remember the most recent render pass of a command buffer, then once the ++// pass is finished read its first color target back and log how many draws it had and whether any pixel is lit. ++struct MTLDebugPass { ++ id commandBuffer = nil; ++ id texture = nil; ++ unsigned long drawsAtStart = 0; ++ unsigned long ordinal = 0; ++ unsigned long load = 0; ++}; ++static thread_local MTLDebugPass debugPass; ++ ++static void MTLDebugFlushPass(id commandBuffer) ++{ ++ if (!debugPass.texture) { ++ return; ++ } ++ id texture = debugPass.texture; ++ debugPass.texture = nil; ++ if (debugPass.commandBuffer != commandBuffer) { ++ [texture release]; ++ return; ++ } ++ unsigned long draws = MTLTraceDrawTotal() - debugPass.drawsAtStart; ++ unsigned long ordinal = debugPass.ordinal, load = debugPass.load; ++ NSUInteger width = texture.width, height = texture.height; ++ unsigned long format = texture.pixelFormat; ++ bool readable = (format == 80 || format == 81 || format == 70 || format == 71) && width >= 64; ++ static unsigned long logged = 0; ++ // Full-screen passes are logged only at the start; every other size keeps being logged (the 3D scene passes come later) ++ static unsigned long smallLogged = 0, fullLogged = 0; ++ bool full = width == 1920; ++ bool interestingSmall = !full && smallLogged < 400; ++ if (interestingSmall) ++smallLogged; ++ bool skipFull = full && ++fullLogged > 60; ++ if (!readable || skipFull || (ordinal > 400 && width < 1280 && !interestingSmall) || (!interestingSmall && ++logged > 600)) { ++ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) { ++ if (ordinal <= 400) { ++ fprintf(log, "pass-end #%lu %lux%lu fmt=%lu draws=%lu (not read)\n", ordinal, (unsigned long) width, (unsigned long) height, format, draws); ++ } ++ fclose(log); ++ } ++ [texture release]; ++ return; ++ } ++ id readback = [[commandBuffer device] newBufferWithLength: width * height * 4 options: MTLResourceStorageModeShared]; ++ id blit = [commandBuffer blitCommandEncoder]; ++ [blit copyFromTexture: texture sourceSlice: 0 sourceLevel: 0 sourceOrigin: MTLOriginMake(0, 0, 0) ++ sourceSize: MTLSizeMake(width, height, 1) toBuffer: readback destinationOffset: 0 ++ destinationBytesPerRow: width * 4 destinationBytesPerImage: width * height * 4]; ++ [blit endEncoding]; ++ [commandBuffer addCompletedHandler: ^(id finished) { ++ const uint8_t* bytes = (const uint8_t*) readback.contents; ++ unsigned long lit = 0, maxChannel = 0; ++ for (NSUInteger i = 0; i < width * height * 4; i += 4) { ++ unsigned char top = std::max(bytes[i], std::max(bytes[i + 1], bytes[i + 2])); ++ if (top) ++lit; ++ if (top > maxChannel) maxChannel = top; ++ } ++ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) { ++ fprintf(log, "pass-end #%lu %lux%lu fmt=%lu load=%lu draws=%lu litPixels=%lu max=%lu\n", ordinal, ++ (unsigned long) width, (unsigned long) height, format, load, draws, lit, maxChannel); ++ fclose(log); ++ } ++ [readback release]; ++ [texture release]; ++ }]; ++} ++#endif ++ ++#if DARLING_METAL_ENABLED ++// Diagnostic (/tmp/metal-debug-timing): remember the render passes of a command buffer and report them when it is slow. ++struct MTLTimingPass { ++ std::string description; ++ id encoder = nil; ++ unsigned long drawsAtStart = 0; ++ unsigned long draws = 0; ++}; ++static thread_local std::vector timingPasses; ++static thread_local id timingCommandBuffer = nil; ++ ++static void MTLTimingFinishPass() ++{ ++ if (!timingPasses.empty() && timingPasses.back().encoder) { ++ MTLTimingPass& pass = timingPasses.back(); ++ pass.draws = MTLTraceDrawTotal() - pass.drawsAtStart; ++ NSString* label = [(id) pass.encoder label]; ++ if (label) pass.description += std::string(" '") + [label UTF8String] + "'"; ++ [pass.encoder release]; ++ pass.encoder = nil; ++ } ++} ++#endif ++ ++#if DARLING_METAL_ENABLED ++// Diagnostic snapshot: while /tmp/metal-snapshot exists, save the output of the render passes (as .ppm in the guest /tmp, ++// first 60 per process) so each pass of a frame can be looked at. Cheap while the flag file is absent. ++struct MTLSnapPass { ++ id commandBuffer = nil; ++ id texture = nil; ++ unsigned long drawsAtStart = 0; ++}; ++static thread_local MTLSnapPass snapPass; ++ ++// The first line of /tmp/metal-snapshot names the capture; a new name starts a new capture of up to 30 passes. ++static std::mutex snapTagLock; ++static std::string snapTag; ++static unsigned long snapSavedForTag = 0; ++ ++static bool MTLSnapshotArmed() ++{ ++ static std::atomic calls { 0 }; ++ static std::atomic armed { false }; ++ // re-read a few times a second: this runs for every encoder and commit, and a guest fopen is several host syscalls ++ if (calls.fetch_add(1) % 2048 == 0) { ++ bool present = false; ++ if (FILE* file = fopen("/tmp/metal-snapshot", "r")) { ++ char text[48] = {}; ++ if (fgets(text, sizeof(text), file)) { ++ std::string tag(text); ++ while (!tag.empty() && (tag.back() == '\n' || tag.back() == ' ')) tag.pop_back(); ++ std::lock_guard guard(snapTagLock); ++ if (tag != snapTag) { ++ snapTag = tag; ++ snapSavedForTag = 0; ++ } ++ } ++ present = true; ++ fclose(file); ++ } ++ armed = present; ++ } ++ return armed; ++} ++ ++static void MTLSnapFlush(id commandBuffer) ++{ ++ if (!snapPass.texture) { ++ return; ++ } ++ id texture = snapPass.texture; ++ snapPass.texture = nil; ++ unsigned long draws = MTLTraceDrawTotal() - snapPass.drawsAtStart; ++ NSUInteger width = texture.width, height = texture.height; ++ unsigned long format = texture.pixelFormat; ++ bool readable = (format == 80 || format == 81 || format == 70 || format == 71) && width >= 16 && width <= 2048 && snapPass.commandBuffer == commandBuffer; ++ std::string tag; ++ unsigned long number = 0; ++ { ++ std::lock_guard guard(snapTagLock); ++ if (!readable || snapSavedForTag >= 30) { ++ readable = false; ++ } else { ++ number = snapSavedForTag++; ++ tag = snapTag; ++ } ++ } ++ if (!readable) { ++ [texture release]; ++ return; ++ } ++ id readback = [[commandBuffer device] newBufferWithLength: width * height * 4 options: MTLResourceStorageModeShared]; ++ id blit = [commandBuffer blitCommandEncoder]; ++ [blit copyFromTexture: texture sourceSlice: 0 sourceLevel: 0 sourceOrigin: MTLOriginMake(0, 0, 0) ++ sourceSize: MTLSizeMake(width, height, 1) toBuffer: readback destinationOffset: 0 ++ destinationBytesPerRow: width * 4 destinationBytesPerImage: width * height * 4]; ++ [blit endEncoding]; ++ [commandBuffer addCompletedHandler: ^(id finished) { ++ const uint8_t* bytes = (const uint8_t*) readback.contents; ++ char path[128]; ++ snprintf(path, sizeof(path), "/tmp/snap-%s-%02lu-%lux%lu-draws%lu-tex%p.ppm", tag.c_str(), number, (unsigned long) width, (unsigned long) height, draws, (void*) texture); ++ if (FILE* out = fopen(path, "wb")) { ++ fprintf(out, "P6\n%lu %lu\n255\n", (unsigned long) width, (unsigned long) height); ++ bool bgra = format == 80 || format == 81; ++ std::vector row(width * 3); ++ for (NSUInteger y = 0; y < height; ++y) { ++ for (NSUInteger x = 0; x < width; ++x) { ++ const uint8_t* px = bytes + (y * width + x) * 4; ++ row[x * 3 + 0] = bgra ? px[2] : px[0]; ++ row[x * 3 + 1] = px[1]; ++ row[x * 3 + 2] = bgra ? px[0] : px[2]; ++ } ++ fwrite(row.data(), 1, row.size(), out); ++ } ++ fclose(out); ++ } ++ [readback release]; ++ [texture release]; ++ }]; ++} ++#endif ++ + #if DARLING_METAL_ENABLED + // used to take care of RR while passing the block around in C++ code + struct MTLCommandBufferHandlerWrapper { +@@ -65,7 +269,7 @@ struct MTLCommandBufferHandlerWrapper { + if (self != nil) { + _commandBuffer = commandBuffer; + _device = [commandQueue.device retain]; +- _commandQueue = [_commandQueue retain]; ++ _commandQueue = [commandQueue retain]; + } + return self; + } +@@ -78,6 +282,13 @@ struct MTLCommandBufferHandlerWrapper { + [super dealloc]; + } + ++- (id)blitCommandEncoder ++{ ++ auto encoder = _commandBuffer->blitCommandEncoder(); ++ if (!encoder) return nil; ++ return [[[MTLBlitCommandEncoderInternal alloc] initWithEncoder:encoder device:_device] autorelease]; ++} ++ + - (id)computeCommandEncoderWithDescriptor: (MTLComputePassDescriptor*)computePassDescriptor + { + auto encoder = _commandBuffer->computeCommandEncoder(computePassDescriptor.asIndiumDescriptor); +@@ -107,11 +318,91 @@ struct MTLCommandBufferHandlerWrapper { + + - (id)renderCommandEncoderWithDescriptor: (MTLRenderPassDescriptor*)renderPassDescriptor + { ++ MTLTraceCount("renderPass"); ++ if (MTLDebugEnabled()) { ++ MTLRenderPassColorAttachmentDescriptor* sizeColor = [renderPassDescriptor.colorAttachments objectAtIndexedSubscript: 0]; ++ char name[64]; ++ snprintf(name, sizeof(name), "pass_%lux%lu_f%lu", sizeColor.texture ? (unsigned long) sizeColor.texture.width : 0UL, sizeColor.texture ? (unsigned long) sizeColor.texture.height : 0UL, sizeColor.texture ? (unsigned long) sizeColor.texture.pixelFormat : 0UL); ++ MTLTraceCount(name); ++ } ++ if (MTLDebugEnabled()) { ++ MTLDebugFlushPass(self); ++ } ++ // Diagnostic: with /tmp/metal-debug-magenta present, every pass clears its first color target to magenta ++ if (MTLDebugEnabled() && access("/tmp/metal-debug-magenta", F_OK) == 0) { ++ MTLRenderPassColorAttachmentDescriptor* debugColor = [renderPassDescriptor.colorAttachments objectAtIndexedSubscript: 0]; ++ if (debugColor.texture && debugColor.loadAction == MTLLoadActionClear) { ++ debugColor.clearColor = MTLClearColorMake(1, 0, 1, 1); ++ } ++ } ++ { ++ // Diagnostic: describe the first render passes (and every 500th) in the trace log ++ static unsigned long passes = 0; ++ ++passes; ++ if (MTLDebugEnabled() && (passes <= 40 || passes % 500 == 0)) { ++ MTLRenderPassColorAttachmentDescriptor* color = [renderPassDescriptor.colorAttachments objectAtIndexedSubscript: 0]; ++ id texture = color.texture; ++ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) { ++ fprintf(log, "pass #%lu color0: tex=%s %lux%lu fmt=%lu load=%lu store=%lu clear=(%.2f %.2f %.2f %.2f) depth=%d\n", passes, ++ texture ? "yes" : "none", texture ? (unsigned long) texture.width : 0UL, texture ? (unsigned long) texture.height : 0UL, ++ texture ? (unsigned long) texture.pixelFormat : 0UL, (unsigned long) color.loadAction, (unsigned long) color.storeAction, ++ color.clearColor.red, color.clearColor.green, color.clearColor.blue, color.clearColor.alpha, ++ renderPassDescriptor.depthAttachment.texture ? 1 : 0); ++ fclose(log); ++ } ++ } ++ } + auto encoder = _commandBuffer->renderCommandEncoder([renderPassDescriptor asIndiumDescriptor]); + if (!encoder) { + return nil; + } +- return [[[MTLRenderCommandEncoderInternal alloc] initWithEncoder: encoder device: _device] autorelease]; ++ if (MTLDebugEnabled()) { ++ MTLRenderPassColorAttachmentDescriptor* color = [renderPassDescriptor.colorAttachments objectAtIndexedSubscript: 0]; ++ static unsigned long ordinal = 0; ++ debugPass.commandBuffer = self; ++ debugPass.texture = [color.texture retain]; ++ debugPass.drawsAtStart = MTLTraceDrawTotal(); ++ debugPass.ordinal = ++ordinal; ++ debugPass.load = (unsigned long) color.loadAction; ++ } ++ id result = [[[MTLRenderCommandEncoderInternal alloc] initWithEncoder: encoder device: _device] autorelease]; ++ if (MTLTimingEnabled()) { ++ MTLTimingFinishPass(); ++ if (timingCommandBuffer != self) { ++ timingPasses.clear(); ++ timingCommandBuffer = self; ++ } ++ MTLRenderPassColorAttachmentDescriptor* color = [renderPassDescriptor.colorAttachments objectAtIndexedSubscript: 0]; ++ char text[160]; ++ snprintf(text, sizeof(text), "render %lux%lu fmt=%lu load=%lu depth=%d", color.texture ? (unsigned long) color.texture.width : 0UL, color.texture ? (unsigned long) color.texture.height : 0UL, ++ color.texture ? (unsigned long) color.texture.pixelFormat : 0UL, (unsigned long) color.loadAction, renderPassDescriptor.depthAttachment.texture ? 1 : 0); ++ { ++ char sizeName[64]; ++ snprintf(sizeName, sizeof(sizeName), "pass_%lux%lu_f%lu_load%lu", color.texture ? (unsigned long) color.texture.width : 0UL, color.texture ? (unsigned long) color.texture.height : 0UL, ++ color.texture ? (unsigned long) color.texture.pixelFormat : 0UL, (unsigned long) color.loadAction); ++ MTLTraceTiming(sizeName, 0); ++ } ++ MTLTimingPass pass; ++ pass.description = text; ++ pass.encoder = [result retain]; ++ pass.drawsAtStart = MTLTraceDrawTotal(); ++ timingPasses.push_back(pass); ++ } ++ if (MTLSnapshotArmed()) { ++ MTLSnapFlush(self); ++ MTLRenderPassColorAttachmentDescriptor* snapColor = [renderPassDescriptor.colorAttachments objectAtIndexedSubscript: 0]; ++ if (snapColor.texture) { ++ snapPass.commandBuffer = self; ++ snapPass.texture = [snapColor.texture retain]; ++ snapPass.drawsAtStart = MTLTraceDrawTotal(); ++ } ++ } ++ return result; ++} ++ ++- (void)addScheduledHandler:(MTLCommandBufferHandler)block ++{ ++ _commandBuffer->addScheduledHandler(MTLCommandBufferHandlerWrapper(block, self)); + } + + - (void)addCompletedHandler: (MTLCommandBufferHandler)block +@@ -121,17 +412,107 @@ struct MTLCommandBufferHandlerWrapper { + + - (void)waitUntilCompleted + { ++ double waitStart = MTLTimingEnabled() ? MTLTraceNow() : 0; + _commandBuffer->waitUntilCompleted(); ++ if (MTLTimingEnabled()) { ++ MTLTraceTiming("waitUntilCompleted", MTLTraceNow() - waitStart); ++ } + } + + - (void)presentDrawable: (id)drawable + { ++ if (MTLDebugEnabled()) { ++ static unsigned long presents = 0; ++ id drawableTexture = [(id)drawable respondsToSelector: @selector(texture)] ? [(id)drawable texture] : nil; ++ if (++presents <= 40) { ++ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) { ++ fprintf(log, "present #%lu lastPassTexture=%p drawableTexture=%p same=%d\n", presents, debugPass.texture, drawableTexture, debugPass.texture == drawableTexture); ++ fclose(log); ++ } ++ } ++ MTLDebugFlushPass(self); ++ } ++ // Diagnostic: with /tmp/metal-debug-present-magenta present, paint the drawable magenta right before it is ++ // presented, to tell a broken presentation path from content that is genuinely black. ++ if (MTLDebugEnabled() && access("/tmp/metal-debug-present-magenta", F_OK) == 0 && [(id)drawable respondsToSelector: @selector(texture)]) { ++ MTLRenderPassDescriptor* debugPass = [MTLRenderPassDescriptor renderPassDescriptor]; ++ MTLRenderPassColorAttachmentDescriptor* debugColor = [debugPass.colorAttachments objectAtIndexedSubscript: 0]; ++ debugColor.texture = [(id)drawable texture]; ++ debugColor.loadAction = MTLLoadActionClear; ++ debugColor.storeAction = MTLStoreActionStore; ++ debugColor.clearColor = MTLClearColorMake(1, 0, 1, 1); ++ id debugEncoder = [self renderCommandEncoderWithDescriptor: debugPass]; ++ [debugEncoder endEncoding]; ++ ++ // ... and read the drawable back on the Vulkan side, to see what it holds before it crosses to GL ++ static unsigned long debugPresents = 0; ++ ++debugPresents; ++ if (debugPresents <= 5 || debugPresents % 200 == 0) { ++ id texture = debugColor.texture; ++ NSUInteger width = texture.width, height = texture.height; ++ id readback = [[self device] newBufferWithLength: width * height * 4 options: MTLResourceStorageModeShared]; ++ id debugBlit = [self blitCommandEncoder]; ++ [debugBlit copyFromTexture: texture sourceSlice: 0 sourceLevel: 0 sourceOrigin: MTLOriginMake(0, 0, 0) ++ sourceSize: MTLSizeMake(width, height, 1) toBuffer: readback destinationOffset: 0 ++ destinationBytesPerRow: width * 4 destinationBytesPerImage: width * height * 4]; ++ [debugBlit endEncoding]; ++ unsigned long ordinal = debugPresents; ++ [self addCompletedHandler: ^(id finished) { ++ const uint8_t* bytes = (const uint8_t*) readback.contents; ++ unsigned long nonZero = 0; ++ for (NSUInteger i = 0; i < width * height * 4; i += 4) { ++ if (bytes[i] | bytes[i + 1] | bytes[i + 2]) ++nonZero; ++ } ++ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) { ++ fprintf(log, "vulkan-readback #%lu %lux%lu nonZeroPixels=%lu first=(%u %u %u %u) fmt=%lu\n", ordinal, ++ (unsigned long) width, (unsigned long) height, nonZero, bytes[0], bytes[1], bytes[2], bytes[3], (unsigned long) texture.pixelFormat); ++ fclose(log); ++ } ++ [readback release]; ++ }]; ++ } ++ } + _commandBuffer->presentDrawable(((id)drawable).drawable); + } + + - (void)commit + { ++ MTLTraceCount("commit"); ++ if (MTLSnapshotArmed()) { ++ MTLSnapFlush(self); ++ } ++ double commitStart = MTLTimingEnabled() ? MTLTraceNow() : 0; ++ if (MTLTimingEnabled()) { ++ std::string report; ++ if (timingCommandBuffer == self) { ++ MTLTimingFinishPass(); ++ for (const auto& pass: timingPasses) { ++ char draws[32]; ++ snprintf(draws, sizeof(draws), " draws=%lu; ", pass.draws); ++ report += pass.description + draws; ++ } ++ timingPasses.clear(); ++ timingCommandBuffer = nil; ++ } ++ _commandBuffer->addCompletedHandler(MTLCommandBufferHandlerWrapper(^(id finished) { ++ double elapsed = MTLTraceNow() - commitStart; ++ MTLTraceTiming("gpuCommitToComplete", elapsed); ++ static std::atomic reported { 0 }; ++ if (elapsed > 0.3 && reported.fetch_add(1) < 40) { ++ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) { ++ fprintf(log, "slow-cmdbuf %.0fms label=%s passes: %s\n", elapsed * 1000, [[finished label] UTF8String] ? [[finished label] UTF8String] : "-", report.c_str()); ++ fclose(log); ++ } ++ } ++ }, self)); ++ } ++ if (MTLDebugEnabled()) { ++ MTLDebugFlushPass(self); ++ } + _commandBuffer->commit(); ++ if (MTLTimingEnabled()) { ++ MTLTraceTiming("commitCall", MTLTraceNow() - commitStart); ++ } + } + + #else +diff --git a/src/Metal/MTLComputeCommandEncoder.mm b/src/Metal/MTLComputeCommandEncoder.mm +index 208a0e9..b13ad20 100644 +--- a/src/Metal/MTLComputeCommandEncoder.mm ++++ b/src/Metal/MTLComputeCommandEncoder.mm +@@ -1,3 +1,6 @@ ++#import ++#import ++#import + // SPDX-FileCopyrightText: 2026 Darling Developers + // SPDX-License-Identifier: MPL-2.0 + +@@ -196,6 +199,15 @@ MTL_UNSUPPORTED_CLASS + [super dealloc]; + } + ++- (void)setTexture:(id)texture atIndex:(NSUInteger)index { ++ _encoder->setTexture(((MTLTextureInternal*)texture).texture, index); ++} ++- (void)setSamplerState:(id)sampler atIndex:(NSUInteger)index { ++ _encoder->setSamplerState(((MTLSamplerStateInternal*)sampler).sampler, index); ++} ++- (void)setSamplerState:(id)sampler lodMinClamp:(float)lodMinClamp lodMaxClamp:(float)lodMaxClamp atIndex:(NSUInteger)index { ++ _encoder->setSamplerState(((MTLSamplerStateInternal*)sampler).sampler, lodMinClamp, lodMaxClamp, index); ++} + - (void)endEncoding + { + _encoder->endEncoding(); +@@ -249,6 +261,16 @@ MTL_UNSUPPORTED_CLASS + - (void)dispatchThreadgroups: (MTLSize)threadgroupsPerGrid + threadsPerThreadgroup: (MTLSize)threadsPerThreadgroup + { ++ MTLTraceCount("dispatch"); ++ if (MTLTimingEnabled() || MTLDebugEnabled()) { ++ static unsigned long logged = 0; ++ if (++logged <= 40) { ++ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) { ++ fprintf(log, "dispatch #%lu groups=(%lu %lu %lu) threads=(%lu %lu %lu)\n", logged, (unsigned long) threadgroupsPerGrid.width, (unsigned long) threadgroupsPerGrid.height, (unsigned long) threadgroupsPerGrid.depth, (unsigned long) threadsPerThreadgroup.width, (unsigned long) threadsPerThreadgroup.height, (unsigned long) threadsPerThreadgroup.depth); ++ fclose(log); ++ } ++ } ++ } + _encoder->dispatchThreadgroups(MTLSizeToIndium(threadgroupsPerGrid), MTLSizeToIndium(threadsPerThreadgroup)); + } + +diff --git a/src/Metal/MTLDepthStencilDescriptor.mm b/src/Metal/MTLDepthStencilDescriptor.mm +index 2a57796..0cbedf2 100644 +--- a/src/Metal/MTLDepthStencilDescriptor.mm ++++ b/src/Metal/MTLDepthStencilDescriptor.mm +@@ -1,24 +1,58 @@ + // SPDX-FileCopyrightText: 2026 Darling Developers + // SPDX-License-Identifier: MPL-2.0 + +-#import +-#import +-#import +- ++#import ++#import ++#import + @implementation MTLDepthStencilDescriptor +- ++@synthesize depthCompareFunction = _depthCompareFunction; ++@synthesize depthWriteEnabled = _depthWriteEnabled; ++@synthesize frontFaceStencil = _frontFaceStencil; ++@synthesize backFaceStencil = _backFaceStencil; ++@synthesize label = _label; ++- (instancetype)init { ++ self = [super init]; ++ if (self) _depthCompareFunction = MTLCompareFunctionAlways; ++ return self; ++} ++- (void)dealloc { [_frontFaceStencil release]; [_backFaceStencil release]; [_label release]; [super dealloc]; } ++- (id)copyWithZone:(NSZone*)zone { ++ MTLDepthStencilDescriptor* copy = [[[self class] allocWithZone:zone] init]; ++ copy.depthCompareFunction = _depthCompareFunction; ++ copy.depthWriteEnabled = _depthWriteEnabled; ++ copy.frontFaceStencil = _frontFaceStencil; ++ copy.backFaceStencil = _backFaceStencil; ++ copy.label = _label; ++ return copy; ++} + #if DARLING_METAL_ENABLED +- +-- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector +-{ +- return [NSMethodSignature signatureWithObjCTypes: "v@:"]; ++- (Indium::DepthStencilDescriptor)asIndiumDescriptor { ++ Indium::DepthStencilDescriptor result; ++ result.depthCompareFunction = static_cast(_depthCompareFunction); ++ result.depthWriteEnabled = _depthWriteEnabled; ++ if (_frontFaceStencil) result.frontFaceStencil = [_frontFaceStencil asIndiumDescriptor]; ++ if (_backFaceStencil) result.backFaceStencil = [_backFaceStencil asIndiumDescriptor]; ++ return result; + } +- +-- (void)forwardInvocation:(NSInvocation *)anInvocation +-{ +- NSLog(@"Stub called: %@ in %@", NSStringFromSelector([anInvocation selector]), [self class]); ++#endif ++@end ++@implementation MTLDepthStencilStateInternal ++#if DARLING_METAL_ENABLED ++@synthesize state = _state; ++const std::shared_ptr& MTLDepthStencilGetIndium(id object) { ++ static const std::shared_ptr none; ++ return object ? ((MTLDepthStencilStateInternal*) object)->_state : none; + } +- ++@synthesize device = _device; ++@synthesize label = _label; ++- (instancetype)initWithState:(std::shared_ptr)state device:(id)device label:(NSString*)label { ++ self = [super init]; ++ if (self) { _state = state; _device = [device retain]; _label = [label copy]; } ++ return self; ++} ++- (void)dealloc { [_device release]; [_label release]; [super dealloc]; } ++#else ++@dynamic device, label; ++MTL_UNSUPPORTED_CLASS + #endif +- + @end +diff --git a/src/Metal/MTLDevice.mm b/src/Metal/MTLDevice.mm +index f58184a..6cfc697 100644 +--- a/src/Metal/MTLDevice.mm ++++ b/src/Metal/MTLDevice.mm +@@ -2,13 +2,26 @@ + // SPDX-License-Identifier: MPL-2.0 + + #import ++#import ++#import + #import + #import + #import + #import + #import ++#import ++#import ++#import + #import + #import ++#import ++#import ++#include ++#include ++#include ++#include ++#import ++#include + #import + + MTL_EXTERN const MTLDeviceNotificationName MTLDeviceWasAddedNotification = @"MTLDeviceWasAdded"; +@@ -88,6 +101,37 @@ void MTLRemoveDeviceObserver(id observer) { + [observer release]; + }; + ++ ++#if DARLING_METAL_ENABLED ++// Diagnostic: report pipeline creation failures (the game just skips the work when a pipeline is nil). ++static void MTLDebugPipelineFailure(const char* what) ++{ ++ if (!(MTLDebugEnabled() || MTLTimingEnabled())) { ++ return; ++ } ++ std::string message = "unknown"; ++ try { throw; } catch (const std::exception& e) { message = e.what(); } catch (...) {} ++ MTLTraceCount(what); ++ static unsigned long logged = 0; ++ if (++logged <= 80) { ++ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) { ++ fprintf(log, "pipeline-failure %s: %s\n", what, message.c_str()); ++ fclose(log); ++ } ++ } ++} ++#endif ++ ++#if DARLING_METAL_ENABLED ++std::mutex& MTLDebugReflectionLock() { static std::mutex lock; return lock; } ++std::unordered_map& MTLDebugReflectionMap() { static std::unordered_map map; return map; } ++std::string MTLDebugReflectionFor(void* pso) ++{ ++ std::lock_guard guard(MTLDebugReflectionLock()); ++ auto it = MTLDebugReflectionMap().find(pso); ++ return it == MTLDebugReflectionMap().end() ? std::string("(none)") : it->second; ++} ++#endif + @implementation MTLDeviceInternal + + #if DARLING_METAL_ENABLED +@@ -153,12 +197,46 @@ void MTLRemoveDeviceObserver(id observer) { + [_threadExitCondition unlock]; + } + ++- (MTLReadWriteTextureTier)readWriteTextureSupport ++{ ++ /* darling-lol: ANGLE queries this during GPU init; it was missing ++ * (unrecognized selector -> terminate). Report Tier2; TierNone made ++ * ANGLE refuse context creation outright (eglCreateContext failure -> ++ * GPU exit(1) -> browser FATAL). If Indium lacks something ANGLE then ++ * uses, that will surface as the next concrete gap instead. */ ++ return MTLReadWriteTextureTier2; ++} ++ ++/* darling-lol M57: feature queries ANGLE's Metal backend needs. */ ++- (BOOL)isDepth24Stencil8PixelFormatSupported { return _device->supportsDepth24Stencil8PixelFormat(); } ++- (BOOL)hasUnifiedMemory { return YES; } ++- (NSUInteger)maxBufferLength { return 256u * 1024u * 1024u; } ++- (NSUInteger)recommendedMaxWorkingSetSize { return 1ull << 30; } ++- (BOOL)isLowPower { return NO; } ++- (BOOL)isRemovable { return NO; } ++- (BOOL)isHeadless { return NO; } ++- (NSString*)name { return @"Darling Indium Device"; } ++- (uint64_t)registryID { return 0; } ++- (BOOL)supportsFamily: (NSUInteger)family { return YES; } ++- (BOOL)supportsFeatureSet: (NSUInteger)featureSet { return YES; } ++- (BOOL)supportsTextureSampleCount: (NSUInteger)sampleCount { return sampleCount <= 4; } ++- (id)newCommandQueueWithMaxCommandBufferCount: (NSUInteger)maxCommandBufferCount ++{ ++ return [self newCommandQueue]; ++} ++ + - (id)newComputePipelineStateWithDescriptor: (MTLComputePipelineDescriptor*)descriptor + options: (MTLPipelineOption)options + reflection: (MTLAutoreleasedComputePipelineReflection*)reflection + error: (NSError**)error + { +- auto pso = _device->newComputePipelineState(descriptor.asIndiumDescriptor, static_cast(options), nullptr); ++ std::decay_tnewComputePipelineState(descriptor.asIndiumDescriptor, static_cast(options), nullptr))> pso; ++ try { pso = _device->newComputePipelineState(descriptor.asIndiumDescriptor, static_cast(options), nullptr); } ++ catch (...) { ++ MTLDebugPipelineFailure("computePipelineFailure"); ++ if (error) *error = MTLErrorFromCurrentException("newComputePipelineStateWithDescriptor"); ++ return nil; ++ } + if (!pso) { + if (error) { + // TODO: better error and/or match what the official Metal method does +@@ -166,6 +244,7 @@ void MTLRemoveDeviceObserver(id observer) { + } + return nil; + } ++ if (reflection) *reflection = [[[MTLComputePipelineReflection alloc] initWithFunction:((MTLFunctionInternal*)descriptor.computeFunction).function] autorelease]; + return [[MTLComputePipelineStateInternal alloc] initWithState: pso device: self label: descriptor.label]; + } + +@@ -183,7 +262,13 @@ void MTLRemoveDeviceObserver(id observer) { + reflection: (MTLAutoreleasedComputePipelineReflection*)reflection + error: (NSError**)error + { +- auto pso = _device->newComputePipelineState(((MTLFunctionInternal*)computeFunction).function, static_cast(options), nullptr); ++ std::decay_tnewComputePipelineState(((MTLFunctionInternal*)computeFunction).function, static_cast(options), nullptr))> pso; ++ try { pso = _device->newComputePipelineState(((MTLFunctionInternal*)computeFunction).function, static_cast(options), nullptr); } ++ catch (...) { ++ MTLDebugPipelineFailure("computePipelineFailure"); ++ if (error) *error = MTLErrorFromCurrentException("newComputePipelineStateWithFunction"); ++ return nil; ++ } + if (!pso) { + if (error) { + // TODO: better error and/or match what the official Metal method does +@@ -191,13 +276,44 @@ void MTLRemoveDeviceObserver(id observer) { + } + return nil; + } ++ if (reflection) *reflection = [[[MTLComputePipelineReflection alloc] initWithFunction:((MTLFunctionInternal*)computeFunction).function] autorelease]; + return [[MTLComputePipelineStateInternal alloc] initWithState: pso device: self label: nil]; + } + ++- (id)newRenderPipelineStateWithDescriptor:(MTLRenderPipelineDescriptor*)descriptor options:(MTLPipelineOption)options reflection:(MTLAutoreleasedRenderPipelineReflection*)reflection error:(NSError**)error { ++ if (reflection) *reflection = nil; ++ if (options & ~(MTLPipelineOptionArgumentInfo | MTLPipelineOptionBufferTypeInfo)) { ++ if (error) *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:ENOTSUP userInfo:nil]; ++ return nil; ++ } ++ id state = [self newRenderPipelineStateWithDescriptor:descriptor error:error]; ++ if (state && reflection) *reflection = [[[MTLRenderPipelineReflection alloc] initWithVertexFunction:((MTLFunctionInternal*)descriptor.vertexFunction).function fragmentFunction:((MTLFunctionInternal*)descriptor.fragmentFunction).function vertexDescriptor:descriptor.vertexDescriptor] autorelease]; ++ if (state && reflection && *reflection && MTLDrawTracking()) { ++ // Diagnostic: remember what the game is told about this pipeline's resources (it binds by these names/indices) ++ std::string text; ++ char piece[200]; ++ for (MTLArgument* argument in [*reflection fragmentArguments]) { ++ snprintf(piece, sizeof(piece), " %lu:%lu:%s", (unsigned long) argument.type, (unsigned long) argument.index, [argument.name UTF8String]); ++ text += piece; ++ } ++ std::lock_guard guard(MTLDebugReflectionLock()); ++ MTLDebugReflectionMap()[(void*) state] = text; ++ } ++ return state; ++} ++ + - (id)newRenderPipelineStateWithDescriptor: (MTLRenderPipelineDescriptor*)descriptor + error: (NSError**)error + { +- auto pso = _device->newRenderPipelineState([descriptor asIndiumDescriptor]); ++ MTLTraceCount("pipelineRequest"); ++ std::decay_tnewRenderPipelineState([descriptor asIndiumDescriptor]))> pso; ++ try { pso = _device->newRenderPipelineState([descriptor asIndiumDescriptor]); } ++ catch (...) { ++ MTLDebugPipelineFailure("renderPipelineFailure"); ++ MTLTraceCount("pipelineException"); ++ if (error) *error = MTLErrorFromCurrentException("newRenderPipelineStateWithDescriptor"); ++ return nil; ++ } + if (!pso) { + if (error) { + // TODO: better error and/or match what the official Metal method does +@@ -205,7 +321,114 @@ void MTLRemoveDeviceObserver(id observer) { + } + return nil; + } +- return [[MTLRenderPipelineStateInternal alloc] initWithState: pso device: self label: descriptor.label]; ++ id created = [[MTLRenderPipelineStateInternal alloc] initWithState: pso device: self label: descriptor.label]; ++ if (MTLDrawTracking()) { ++ // Diagnostic: the reflection the game would get for this pipeline (it binds resources by these names/indices) ++ MTLRenderPipelineReflection* debugReflection = [[[MTLRenderPipelineReflection alloc] initWithVertexFunction:((MTLFunctionInternal*)descriptor.vertexFunction).function fragmentFunction:((MTLFunctionInternal*)descriptor.fragmentFunction).function vertexDescriptor:descriptor.vertexDescriptor] autorelease]; ++ std::string text = "vs=" + std::string([descriptor.vertexFunction.name UTF8String] ?: "(nil)") + ++ " fs=" + std::string([descriptor.fragmentFunction.name UTF8String] ?: "(nil)"); ++ char piece[200]; ++ for (MTLArgument* argument in [debugReflection fragmentArguments]) { ++ snprintf(piece, sizeof(piece), " %lu:%lu:%s", (unsigned long) argument.type, (unsigned long) argument.index, [argument.name UTF8String]); ++ text += piece; ++ } ++ std::lock_guard guard(MTLDebugReflectionLock()); ++ MTLDebugReflectionMap()[(void*) created] = text; ++ } ++ static unsigned long logged = 0; ++ if (MTLDebugEnabled() && ++logged <= 300) { ++ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) { ++ MTLRenderPipelineColorAttachmentDescriptor* color = [descriptor.colorAttachments objectAtIndexedSubscript: 0]; ++ fprintf(log, "pso %p indium=%p vs=%s fs=%s color0 fmt=%lu blend=%d src=%lu/%lu dst=%lu/%lu op=%lu/%lu mask=%lu depthFmt=%lu samples=%lu rasterDiscard=%d\n", created, (void*) pso.get(), ++ [descriptor.vertexFunction.name UTF8String], [descriptor.fragmentFunction.name UTF8String], ++ (unsigned long) color.pixelFormat, (int) color.blendingEnabled, ++ (unsigned long) color.sourceRGBBlendFactor, (unsigned long) color.sourceAlphaBlendFactor, ++ (unsigned long) color.destinationRGBBlendFactor, (unsigned long) color.destinationAlphaBlendFactor, ++ (unsigned long) color.rgbBlendOperation, (unsigned long) color.alphaBlendOperation, (unsigned long) color.writeMask, ++ (unsigned long) descriptor.depthAttachmentPixelFormat, (unsigned long) descriptor.sampleCount, (int) descriptor.rasterizationEnabled); ++ MTLVertexDescriptor* vertex = descriptor.vertexDescriptor; ++ for (NSUInteger i = 0; vertex && i < 31; ++i) { ++ MTLVertexAttributeDescriptor* attr = vertex.attributes[i]; ++ if (attr.format == MTLVertexFormatInvalid) continue; ++ MTLVertexBufferLayoutDescriptor* layout = vertex.layouts[attr.bufferIndex]; ++ fprintf(log, " attr[%lu] fmt=%lu off=%lu buf=%lu stride=%lu step=%lu rate=%lu\n", (unsigned long) i, (unsigned long) attr.format, ++ (unsigned long) attr.offset, (unsigned long) attr.bufferIndex, (unsigned long) layout.stride, (unsigned long) layout.stepFunction, (unsigned long) layout.stepRate); ++ } ++ if (!vertex) fprintf(log, " no vertex descriptor\n"); ++ fclose(log); ++ } ++ } ++ return created; ++} ++ ++- (id)newDepthStencilStateWithDescriptor:(MTLDepthStencilDescriptor*)descriptor ++{ ++ if (!descriptor) return nil; ++ auto state = _device->newDepthStencilState([descriptor asIndiumDescriptor]); ++ if (!state) return nil; ++ return [[MTLDepthStencilStateInternal alloc] initWithState:state device:self label:descriptor.label]; ++} ++ ++- (id)newSamplerStateWithDescriptor:(MTLSamplerDescriptor*)descriptor ++{ ++ if (!descriptor) return nil; ++ Indium::SamplerDescriptor desc; ++ desc.minFilter = static_cast(descriptor.minFilter); ++ desc.magFilter = static_cast(descriptor.magFilter); ++ desc.mipFilter = static_cast(descriptor.mipFilter); ++ desc.maxAnisotropy = descriptor.maxAnisotropy; ++ desc.sAddressMode = static_cast(descriptor.sAddressMode); ++ desc.tAddressMode = static_cast(descriptor.tAddressMode); ++ desc.rAddressMode = static_cast(descriptor.rAddressMode); ++ desc.borderColor = static_cast(descriptor.borderColor); ++ desc.normalizedCoordinates = descriptor.normalizedCoordinates; ++ desc.lodMinClamp = descriptor.lodMinClamp; ++ desc.lodMaxClamp = descriptor.lodMaxClamp; ++ desc.supportArgumentBuffers = descriptor.supportArgumentBuffers; ++ desc.compareFunction = static_cast(descriptor.compareFunction); ++ auto sampler = _device->newSamplerState(desc); ++ if (!sampler) return nil; ++ return [[MTLSamplerStateInternal alloc] initWithSampler:sampler device:self label:descriptor.label]; ++} ++ ++- (id)newTextureWithDescriptor:(MTLTextureDescriptor*)descriptor ++{ ++ MTLTraceCount("newTexture"); ++ if (!descriptor || !descriptor.width || !descriptor.height || !descriptor.depth || ++ !descriptor.mipmapLevelCount || !descriptor.sampleCount || !descriptor.arrayLength) return nil; ++ Indium::TextureDescriptor desc; ++ desc.textureType = static_cast(descriptor.textureType); ++ desc.pixelFormat = static_cast(descriptor.pixelFormat); ++ desc.width = descriptor.width; ++ desc.height = descriptor.height; ++ desc.depth = descriptor.depth; ++ desc.mipmapLevelCount = descriptor.mipmapLevelCount; ++ desc.sampleCount = descriptor.sampleCount; ++ desc.arrayLength = descriptor.arrayLength; ++ desc.resourceOptions = static_cast(descriptor.resourceOptions); ++ desc.allowGPUOptimizedContents = descriptor.allowGPUOptimizedContents; ++ desc.usage = static_cast(descriptor.usage); ++ desc.swizzle = MTLTextureSwizzleChannelsToIndium(descriptor.swizzle); ++ std::shared_ptr texture; ++ std::string failure; ++ try { texture = _device->newTexture(desc); } ++ catch (const std::exception& e) { failure = e.what(); } ++ catch (...) { failure = "unknown exception"; } ++ if (MTLDebugEnabled() || MTLTimingEnabled()) { ++ static unsigned long renderTargets = 0, failures = 0; ++ bool renderTarget = (descriptor.usage & MTLTextureUsageRenderTarget) != 0; ++ if (!texture ? ++failures <= 60 : (renderTarget && ++renderTargets <= 80)) { ++ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) { ++ fprintf(log, "%s %lux%lux%lu type=%lu fmt=%lu mips=%lu len=%lu samples=%lu usage=%lu storage=%lu %s\n", texture ? "renderTargetTexture" : "TEXTURE-FAILED", ++ (unsigned long) descriptor.width, (unsigned long) descriptor.height, (unsigned long) descriptor.depth, (unsigned long) descriptor.textureType, ++ (unsigned long) descriptor.pixelFormat, (unsigned long) descriptor.mipmapLevelCount, (unsigned long) descriptor.arrayLength, (unsigned long) descriptor.sampleCount, ++ (unsigned long) descriptor.usage, (unsigned long) descriptor.storageMode, failure.c_str()); ++ fclose(log); ++ } ++ } ++ } ++ if (!texture) return nil; ++ return [[MTLTextureInternal alloc] initWithTexture:texture device:self resourceOptions:descriptor.resourceOptions]; + } + + - (id)newCommandQueue +@@ -275,8 +498,21 @@ void MTLRemoveDeviceObserver(id observer) { + - (id)newLibraryWithData: (dispatch_data_t)data + error: (NSError**)error + { +- NSData* nsdata = (NSData*)data; +- auto lib = _device->newLibrary(nsdata.bytes, nsdata.length); ++ const void* bytes = nullptr; ++ size_t length = 0; ++ dispatch_data_t mapped = dispatch_data_create_map(data, &bytes, &length); ++ if (!mapped) { ++ if (error) *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:ENOMEM userInfo:nil]; ++ return nil; ++ } ++ std::shared_ptr lib; ++ try { lib = _device->newLibrary(bytes, length); } ++ catch (...) { ++ dispatch_release(mapped); ++ if (error) *error = MTLErrorFromCurrentException("newLibraryWithData"); ++ return nil; ++ } ++ dispatch_release(mapped); + if (!lib) { + return nil; + } +diff --git a/src/Metal/MTLLibrary.mm b/src/Metal/MTLLibrary.mm +index 0feb46e..e0c0e93 100644 +--- a/src/Metal/MTLLibrary.mm ++++ b/src/Metal/MTLLibrary.mm +@@ -1,6 +1,7 @@ + // SPDX-FileCopyrightText: 2026 Darling Developers + // SPDX-License-Identifier: MPL-2.0 + ++#import + #import + #import + #import +@@ -27,10 +28,12 @@ + self = [super init]; + if (self != nil) { + _function = function; ++ _functionType = static_cast(function->functionType()); ++ + _device = [device retain]; + + // cache the name +- _name = [NSString stringWithUTF8String: _function->name().c_str()]; ++ _name = [[NSString alloc] initWithUTF8String: _function->name().c_str()]; + } + return self; + } +@@ -121,7 +124,12 @@ MTL_UNSUPPORTED_CLASS + + - (id)newFunctionWithName: (NSString*)functionName + { +- auto func = _library->newFunction(functionName.UTF8String); ++ std::shared_ptr func; ++ try { func = _library->newFunction(functionName.UTF8String); } ++ catch (...) { ++ MTLErrorFromCurrentException("newFunctionWithName"); ++ return nil; ++ } + if (!func) { + return nil; + } +diff --git a/src/Metal/MTLRenderCommandEncoder.mm b/src/Metal/MTLRenderCommandEncoder.mm +index e931b90..e13dc0e 100644 +--- a/src/Metal/MTLRenderCommandEncoder.mm ++++ b/src/Metal/MTLRenderCommandEncoder.mm +@@ -1,3 +1,9 @@ ++#import ++#import ++#import ++#import ++#import ++#import + // SPDX-FileCopyrightText: 2026 Darling Developers + // SPDX-License-Identifier: MPL-2.0 + +@@ -9,8 +15,268 @@ + #import + #import + ++#include ++#include ++#include + #include ++#include ++#if DARLING_METAL_ENABLED ++extern std::string MTLDebugReflectionFor(void* pso); ++#endif ++ ++#if DARLING_METAL_ENABLED ++// Diagnostic (MTLDebugEnabled only): the state a draw is issued with, logged for the first few hundred draws. ++struct MTLDebugDrawState { ++ MTLViewport viewport = {}; ++ MTLScissorRect scissor = {}; ++ long cull = -1, winding = -1, fill = -1; ++ void* pipeline = nullptr; ++ void* depthStencil = nullptr; ++ id vertexBuffers[31] = {}; NSUInteger vertexOffsets[31] = {}; ++ id fragmentBuffers[31] = {}; NSUInteger fragmentOffsets[31] = {}; ++ unsigned char vertexBytes[31][64] = {}; NSUInteger vertexBytesLength[31] = {}; ++ unsigned char fragmentBytes[31][64] = {}; NSUInteger fragmentBytesLength[31] = {}; ++ id fragmentTextures[31] = {}; ++ id vertexTextures[31] = {}; ++}; ++static thread_local MTLDebugDrawState debugDrawState; ++ ++// Experiments, each enabled by a file in the guest /tmp present when the process starts (with metal-debug-trace) ++#define MTL_DEBUG_FLAG(name) ([]{ static const bool on = MTLDebugEnabled() && access("/tmp/" name, F_OK) == 0; return on; }()) ++ ++ ++static void MTLDebugDumpFloats(FILE* log, const char* what, int index, const void* bytes, size_t available) ++{ ++ fprintf(log, " %s[%d]:", what, index); ++ const float* floats = (const float*) bytes; ++ for (size_t i = 0; i < 16 && (i + 1) * 4 <= available; ++i) { ++ fprintf(log, " %g", floats[i]); ++ } ++ fprintf(log, "\n"); ++} ++ ++static void MTLDebugDumpBindings(unsigned long drawn) ++{ ++ const MTLDebugDrawState& state = debugDrawState; ++ FILE* log = fopen("/tmp/metal-trace.log", "a"); ++ if (!log) { ++ return; ++ } ++ fprintf(log, "bindings at draw #%lu\n", drawn); ++ for (int i = 0; i < 31; ++i) { ++ if (state.vertexBuffers[i]) { ++ id buffer = state.vertexBuffers[i]; ++ const char* contents = (const char*) buffer.contents; ++ fprintf(log, " vbuf[%d] len=%lu off=%lu contents=%s\n", i, (unsigned long) buffer.length, (unsigned long) state.vertexOffsets[i], contents ? "cpu" : "none"); ++ if (contents && state.vertexOffsets[i] < buffer.length) { ++ MTLDebugDumpFloats(log, "vdata", i, contents + state.vertexOffsets[i], buffer.length - state.vertexOffsets[i]); ++ } ++ } ++ if (state.vertexBytesLength[i]) { ++ MTLDebugDumpFloats(log, "vbytes", i, state.vertexBytes[i], std::min(64, state.vertexBytesLength[i])); ++ } ++ if (state.fragmentBuffers[i]) { ++ id buffer = state.fragmentBuffers[i]; ++ const char* contents = (const char*) buffer.contents; ++ fprintf(log, " fbuf[%d] len=%lu off=%lu contents=%s\n", i, (unsigned long) buffer.length, (unsigned long) state.fragmentOffsets[i], contents ? "cpu" : "none"); ++ if (contents && state.fragmentOffsets[i] < buffer.length) { ++ MTLDebugDumpFloats(log, "fdata", i, contents + state.fragmentOffsets[i], buffer.length - state.fragmentOffsets[i]); ++ } ++ } ++ if (state.fragmentBytesLength[i]) { ++ MTLDebugDumpFloats(log, "fbytes", i, state.fragmentBytes[i], std::min(64, state.fragmentBytesLength[i])); ++ } ++ if (state.fragmentTextures[i]) { ++ id texture = state.fragmentTextures[i]; ++ unsigned long width = texture.width, height = texture.height, format = texture.pixelFormat; ++ unsigned long lit = 0, alpha = 0, total = 0; ++ bool read = MTLDebugEnabled() && (format == 80 || format == 81 || format == 70 || format == 71) && width * height <= 4096 * 4096; ++ if (read) { ++ id queue = [[texture device] newCommandQueue]; ++ id readback = [[texture device] newBufferWithLength: width * height * 4 options: MTLResourceStorageModeShared]; ++ id commandBuffer = [queue commandBuffer]; ++ id blit = [commandBuffer blitCommandEncoder]; ++ [blit copyFromTexture: texture sourceSlice: 0 sourceLevel: 0 sourceOrigin: MTLOriginMake(0, 0, 0) ++ sourceSize: MTLSizeMake(width, height, 1) toBuffer: readback destinationOffset: 0 ++ destinationBytesPerRow: width * 4 destinationBytesPerImage: width * height * 4]; ++ [blit endEncoding]; ++ [commandBuffer commit]; ++ [commandBuffer waitUntilCompleted]; ++ const uint8_t* bytes = (const uint8_t*) readback.contents; ++ for (unsigned long k = 0; k < width * height * 4; k += 4) { ++ ++total; ++ if (bytes[k] | bytes[k + 1] | bytes[k + 2]) ++lit; ++ if (bytes[k + 3]) ++alpha; ++ } ++ [readback release]; ++ [queue release]; ++ } ++ fprintf(log, " ftex[%d] ptr=%p %lux%lu type=%lu mips=%lu len=%lu samples=%lu fmt=%lu%s litRGB=%lu alphaNonZero=%lu of %lu\n", i, (void*) texture, width, height, (unsigned long) texture.textureType, (unsigned long) texture.mipmapLevelCount, (unsigned long) texture.arrayLength, (unsigned long) texture.sampleCount, format, read ? "" : " (not read)", lit, alpha, total); ++ } ++ } ++ fclose(log); ++} + ++static void MTLDebugLogDraw(const char* kind, unsigned long count, unsigned long instances, unsigned long indexType, long baseVertex, id indexBuffer, NSUInteger indexOffset) ++{ ++ if (!MTLDebugEnabled()) { ++ // Light mode: only while /tmp/metal-snapshot exists, log up to 300 draws with their bindings ++ static unsigned long lightLogged = 0; ++ static unsigned long checks = 0; ++ static bool armed = false; ++ static std::string captureTag; ++ // re-read a few times a second: this runs for every draw, and a guest fopen is several host syscalls ++ if (checks++ % 4096 == 0) { ++ armed = false; ++ if (FILE* file = fopen("/tmp/metal-snapshot", "r")) { ++ char tag[48] = {}; ++ if (fgets(tag, sizeof(tag), file)) { ++ if (captureTag != tag) { captureTag = tag; lightLogged = 0; } ++ armed = true; ++ } ++ fclose(file); ++ } ++ } ++ if (!armed || lightLogged >= 300) { ++ return; ++ } ++ ++lightLogged; ++ MTLDebugDumpBindings(2000 + lightLogged); ++ { ++ // Save the raw blocks of a few block-compressed textures (decoded offline) to check the stored image ++ static std::vector dumped; ++ for (int i = 0; i < 31; ++i) { ++ id texture = debugDrawState.fragmentTextures[i]; ++ unsigned long format = texture ? (unsigned long) texture.pixelFormat : 0; ++ bool bc = format == 130 || format == 131 || format == 134 || format == 135; ++ if (!bc || texture.width < 128 || dumped.size() >= 8 || std::find(dumped.begin(), dumped.end(), (void*) texture) != dumped.end()) continue; ++ dumped.push_back((void*) texture); ++ NSUInteger level = std::min(1, texture.mipmapLevelCount - 1); ++ NSUInteger width = std::max(1, texture.width >> level), height = std::max(1, texture.height >> level); ++ NSUInteger blocksX = (width + 3) / 4, blocksY = (height + 3) / 4, blockBytes = (format == 130 || format == 131) ? 8 : 16; ++ NSUInteger rowBytes = blocksX * blockBytes, total = rowBytes * blocksY; ++ id queue = [[texture device] newCommandQueue]; ++ id readback = [[texture device] newBufferWithLength: total options: MTLResourceStorageModeShared]; ++ id commandBuffer = [queue commandBuffer]; ++ id blit = [commandBuffer blitCommandEncoder]; ++ [blit copyFromTexture: texture sourceSlice: 0 sourceLevel: level sourceOrigin: MTLOriginMake(0, 0, 0) sourceSize: MTLSizeMake(width, height, 1) ++ toBuffer: readback destinationOffset: 0 destinationBytesPerRow: rowBytes destinationBytesPerImage: total]; ++ [blit endEncoding]; ++ [commandBuffer commit]; ++ [commandBuffer waitUntilCompleted]; ++ char path[128]; ++ snprintf(path, sizeof(path), "/tmp/tex-%02zu-%lux%lu-f%lu-level%lu.bc", dumped.size(), (unsigned long) width, (unsigned long) height, format, (unsigned long) level); ++ if (FILE* out = fopen(path, "wb")) { fwrite(readback.contents, 1, total, out); fclose(out); } ++ [readback release]; ++ [queue release]; ++ } ++ } ++ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) { ++ fprintf(log, "draw #%lu %s count=%lu inst=%lu baseVtx=%ld pso=%p ds=%p cull=%ld wind=%ld\n", 2000 + lightLogged, kind, count, instances, baseVertex, debugDrawState.pipeline, debugDrawState.depthStencil, debugDrawState.cull, debugDrawState.winding); ++ fprintf(log, "reflect pso=%p frag:%s\n", debugDrawState.pipeline, MTLDebugReflectionFor(debugDrawState.pipeline).c_str()); ++ fclose(log); ++ } ++ return; ++ } ++ static unsigned long drawn = 0; ++ static unsigned long bigDrawn = 0; ++ bool big = count >= 600 || instances > 1; ++ if (big && ++bigDrawn <= 120) { ++ MTLDebugDumpBindings(1000 + bigDrawn); ++ } else if (++drawn > 400) { ++ return; ++ } ++ const MTLDebugDrawState& state = debugDrawState; ++ if (drawn == 1 || drawn == 7 || drawn == 13 || drawn == 40) { ++ MTLDebugDumpBindings(drawn); ++ if (indexBuffer && indexBuffer.contents) { ++ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) { ++ const uint16_t* indices = (const uint16_t*)((const char*) indexBuffer.contents + indexOffset); ++ { ++ const unsigned char* base = (const unsigned char*) indexBuffer.contents; ++ NSUInteger from = indexOffset >= 256 ? indexOffset - 256 : 0; ++ fprintf(log, " before-index bytes from %lu:", (unsigned long) from); ++ for (NSUInteger k = from; k < indexOffset + 16; k += 4) { ++ float f; memcpy(&f, base + k, 4); ++ unsigned u; memcpy(&u, base + k, 4); ++ fprintf(log, " %g/%08x", f, u); ++ } ++ fprintf(log, "\n"); ++ } ++ fprintf(log, " indices(type %lu, off %lu, len %lu):", indexType, (unsigned long) indexOffset, (unsigned long) indexBuffer.length); ++ for (int k = 0; k < 12; ++k) fprintf(log, " %u", indices[k]); ++ fprintf(log, "\n"); ++ fclose(log); ++ } ++ } ++ } ++ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) { ++ fprintf(log, "draw #%lu %s count=%lu inst=%lu idxType=%lu baseVtx=%ld pso=%p ds=%p vp=(%.0f %.0f %.0f %.0f %.2f %.2f) sc=(%lu %lu %lu %lu) cull=%ld wind=%ld fill=%ld\n", ++ drawn, kind, count, instances, indexType, baseVertex, state.pipeline, state.depthStencil, ++ state.viewport.originX, state.viewport.originY, state.viewport.width, state.viewport.height, state.viewport.znear, state.viewport.zfar, ++ (unsigned long) state.scissor.x, (unsigned long) state.scissor.y, (unsigned long) state.scissor.width, (unsigned long) state.scissor.height, ++ state.cull, state.winding, state.fill); ++ fclose(log); ++ } ++} ++#endif ++ ++ ++#if DARLING_METAL_ENABLED ++// Experiment: /tmp/metal-draw-limit holds N; every render pass then executes only its first N draws. ++static thread_local unsigned long passDrawIndex = 0; ++static bool MTLDrawLimited(unsigned long count) ++{ ++ static std::atomic calls { 0 }; ++ static std::atomic limit { -1 }; ++ static std::atomic minimum { 0 }; ++ static std::atomic maximum { -1 }; ++ static const bool possible = access("/tmp/metal-debug-draws", F_OK) == 0 || MTLDebugEnabled(); ++ if (!possible) { ++ return false; ++ } ++ if (calls.fetch_add(1) % 200 == 0) { ++ long value = -1; ++ if (FILE* file = fopen("/tmp/metal-draw-limit", "r")) { ++ if (fscanf(file, "%ld", &value) != 1) value = -1; ++ fclose(file); ++ } ++ limit = value; ++ long low = 0, high = -1; ++ if (FILE* file = fopen("/tmp/metal-draw-min", "r")) { if (fscanf(file, "%ld", &low) != 1) low = 0; fclose(file); } ++ if (FILE* file = fopen("/tmp/metal-draw-max", "r")) { if (fscanf(file, "%ld", &high) != 1) high = -1; fclose(file); } ++ minimum = low; ++ maximum = high; ++ } ++ // skip draws outside [min, max] index counts (experiment: which draws build which part of the picture) ++ if (count < (unsigned long) (long) minimum || (maximum >= 0 && count > (unsigned long) (long) maximum)) { ++ return true; ++ } ++ long n = limit; ++ return n >= 0 && ++passDrawIndex > (unsigned long) n; ++} ++#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) ++{ ++ static std::atomic calls { 0 }; ++ static std::atomic state { 0 }; ++ static const bool possible = access("/tmp/metal-debug-draws", F_OK) == 0 || MTLDebugEnabled(); ++ if (!possible) { ++ return false; ++ } ++ if (calls.fetch_add(1) % 100 == 0) { ++ unsigned value = 0; ++ if (access("/tmp/metal-flag-nocull", F_OK) == 0) value |= 1; ++ if (access("/tmp/metal-flag-flipwind", F_OK) == 0) value |= 2; ++ if (access("/tmp/metal-flag-nodepth", F_OK) == 0) value |= 4; ++ state = value; ++ } ++ return (state & (1u << which)) != 0; ++} ++#endif + @implementation MTLRenderPassAttachmentDescriptor + + #if DARLING_METAL_ENABLED +@@ -96,11 +362,11 @@ MTL_UNSUPPORTED_CLASS + - (Indium::RenderPassAttachmentDescriptor)asIndiumDescriptor + { + return Indium::RenderPassAttachmentDescriptor { +- ((MTLTextureInternal*)_texture).texture, ++ MTLTextureGetIndium(_texture), + _level, + _slice, + _depthPlane, +- ((MTLTextureInternal*)_resolveTexture).texture, ++ MTLTextureGetIndium(_resolveTexture), + _resolveLevel, + _resolveSlice, + _resolveDepthPlane, +@@ -367,7 +633,11 @@ MTL_UNSUPPORTED_CLASS + std::vector result; + + for (const auto& [key, value]: tmp2) { +- result.push_back(value); ++ // A slot that was only looked at (or never given a texture) is not an attachment; depth-only passes rely on this ++ if (value.texture) { ++ result.resize(key + 1); ++ result[key] = value; ++ } + } + + return result; +@@ -404,6 +674,10 @@ MTL_UNSUPPORTED_CLASS + - (void)_doInit + { + _colorAttachments = [MTLRenderPassColorAttachmentDescriptorArray new]; ++ _depthAttachment = [MTLRenderPassDepthAttachmentDescriptor new]; ++ _stencilAttachment = [MTLRenderPassStencilAttachmentDescriptor new]; ++ _renderTargetArrayLength = 1; ++ _defaultRasterSampleCount = 1; + } + + - (void)_doDealloc +@@ -502,9 +776,9 @@ MTL_UNSUPPORTED_CLASS + { + return Indium::RenderPassDescriptor { + [_colorAttachments asContiguousIndiumDescriptors], +- (_depthAttachment == nil) ? std::nullopt : std::make_optional([_depthAttachment asIndiumDescriptor]), +- (_stencilAttachment == nil) ? std::nullopt : std::make_optional([_stencilAttachment asIndiumDescriptor]), +- ((MTLBufferInternal*)_visibilityResultBuffer).buffer, ++ (_depthAttachment.texture == nil) ? std::nullopt : std::make_optional([_depthAttachment asIndiumDescriptor]), ++ (_stencilAttachment.texture == nil) ? std::nullopt : std::make_optional([_stencilAttachment asIndiumDescriptor]), ++ MTLBufferGetIndium(_visibilityResultBuffer), + _renderTargetArrayLength, + _imageblockSampleLength, + _threadgroupMemoryLength, +@@ -538,6 +812,7 @@ MTL_UNSUPPORTED_CLASS + if (self != nil) { + _encoder = encoder; + _device = [device retain]; ++ passDrawIndex = 0; + } + return self; + } +@@ -553,6 +828,34 @@ MTL_UNSUPPORTED_CLASS + // overridden methods + // + ++- (void)setVertexTexture:(id)texture atIndex:(NSUInteger)index { ++ _encoder->setVertexTexture(MTLTextureGetIndium(texture), index); ++} ++- (void)setFragmentTexture:(id)texture atIndex:(NSUInteger)index { ++ if (MTLDrawTracking() && index < 31) debugDrawState.fragmentTextures[index] = texture; ++ _encoder->setFragmentTexture(MTLTextureGetIndium(texture), index); ++} ++- (void)setVertexSamplerState:(id)sampler atIndex:(NSUInteger)index { ++ _encoder->setVertexSamplerState(MTLSamplerGetIndium(sampler), index); ++} ++- (void)setFragmentSamplerState:(id)sampler atIndex:(NSUInteger)index { ++ _encoder->setFragmentSamplerState(MTLSamplerGetIndium(sampler), index); ++} ++- (void)setDepthBias:(float)depthBias slopeScale:(float)slopeScale clamp:(float)clamp { ++ _encoder->setDepthBias(depthBias, slopeScale, clamp); ++} ++- (void)setDepthStencilState:(id)state { ++ if (MTLDrawTracking()) debugDrawState.depthStencil = state; ++ if (MTLLiveFlag(2)) return; ++ _encoder->setDepthStencilState(MTLDepthStencilGetIndium(state)); ++} ++- (void)setStencilReferenceValue:(uint32_t)value { ++ _encoder->setStencilReferenceValue(value); ++} ++- (void)memoryBarrierWithScope:(MTLBarrierScope)scope afterStages:(MTLRenderStages)after beforeStages:(MTLRenderStages)before { ++ _encoder->memoryBarrier(scope, static_cast(after), static_cast(before)); ++} ++ + - (void)endEncoding + { + _encoder->endEncoding(); +@@ -564,26 +867,34 @@ MTL_UNSUPPORTED_CLASS + + - (void)setRenderPipelineState: (id)pipelineState + { +- _encoder->setRenderPipelineState(((MTLRenderPipelineStateInternal*)pipelineState).state); ++ if (MTLDrawTracking()) debugDrawState.pipeline = pipelineState; ++ MTLTraceCount(pipelineState ? "setPipeline" : "setPipelineNil"); ++ _encoder->setRenderPipelineState(MTLRenderPipelineGetIndium(pipelineState)); + } + + - (void)setTriangleFillMode: (MTLTriangleFillMode)fillMode + { ++ if (MTLDrawTracking()) debugDrawState.fill = (long) fillMode; + _encoder->setTriangleFillMode(static_cast(fillMode)); + } + + - (void)setFrontFacingWinding: (MTLWinding)frontFacingWinding + { ++ if (MTLDrawTracking()) debugDrawState.winding = (long) frontFacingWinding; ++ if (MTLLiveFlag(1)) frontFacingWinding = (frontFacingWinding == MTLWindingClockwise) ? MTLWindingCounterClockwise : MTLWindingClockwise; + _encoder->setFrontFacingWinding(static_cast(frontFacingWinding)); + } + + - (void)setCullMode: (MTLCullMode)cullMode + { ++ if (MTLDrawTracking()) debugDrawState.cull = (long) cullMode; ++ if (MTLLiveFlag(0)) cullMode = MTLCullModeNone; + _encoder->setCullMode(static_cast(cullMode)); + } + + - (void)setViewport: (MTLViewport)viewport + { ++ if (MTLDrawTracking()) debugDrawState.viewport = viewport; + _encoder->setViewport(MTLViewportToIndium(viewport)); + } + +@@ -599,6 +910,8 @@ MTL_UNSUPPORTED_CLASS + + - (void)setScissorRect: (MTLScissorRect)rect + { ++ if (MTLDrawTracking()) debugDrawState.scissor = rect; ++ if (MTL_DEBUG_FLAG("metal-debug-noscissor")) return; + _encoder->setScissorRect(MTLScissorRectToIndium(rect)); + } + +@@ -616,7 +929,25 @@ MTL_UNSUPPORTED_CLASS + offset: (NSUInteger)offset + atIndex: (NSUInteger)index + { +- _encoder->setVertexBuffer(((MTLBufferInternal*)buffer).buffer, offset, index); ++ if (MTLDebugEnabled()) { ++ static unsigned long calls = 0; ++ static unsigned long unusual = 0; ++ bool other = index != 3 && index != 4 && index != 10; ++ if (other && ++unusual <= 200) { ++ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) { ++ fprintf(log, "setVertexBuffer-other idx=%lu buf=%p off=%lu\n", (unsigned long) index, buffer, (unsigned long) offset); ++ fclose(log); ++ } ++ } ++ if (++calls <= 120) { ++ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) { ++ fprintf(log, "setVertexBuffer #%lu idx=%lu buf=%p off=%lu len=%lu encoder=%p\n", calls, (unsigned long) index, buffer, (unsigned long) offset, (unsigned long) buffer.length, self); ++ fclose(log); ++ } ++ } ++ } ++ if (MTLDrawTracking() && index < 31) { debugDrawState.vertexBuffers[index] = buffer; debugDrawState.vertexOffsets[index] = offset; debugDrawState.vertexBytesLength[index] = 0; } ++ _encoder->setVertexBuffer(MTLBufferGetIndium(buffer), offset, index); + } + + - (void)setVertexBuffers: (const id*)buffers +@@ -626,7 +957,14 @@ MTL_UNSUPPORTED_CLASS + std::vector> tmp; + std::vector tmp2(offsets, offsets + range.length); + for (size_t i = 0; i < range.length; ++i) { +- tmp.push_back(((MTLBufferInternal*)buffers[i]).buffer); ++ tmp.push_back(MTLBufferGetIndium(buffers[i])); ++ } ++ if (MTLDrawTracking()) { ++ for (NSUInteger i = 0; i < range.length && range.location + i < 31; ++i) { ++ debugDrawState.vertexBuffers[range.location + i] = buffers[i]; ++ debugDrawState.vertexOffsets[range.location + i] = offsets[i]; ++ debugDrawState.vertexBytesLength[range.location + i] = 0; ++ } + } + _encoder->setVertexBuffers(tmp, tmp2, NSRangeToIndium(range)); + } +@@ -634,6 +972,16 @@ MTL_UNSUPPORTED_CLASS + - (void)setVertexBufferOffset: (NSUInteger)offset + atIndex: (NSUInteger)index + { ++ if (MTLDebugEnabled()) { ++ static unsigned long calls = 0; ++ if (++calls <= 120) { ++ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) { ++ fprintf(log, "setVertexBufferOffset #%lu idx=%lu off=%lu encoder=%p\n", calls, (unsigned long) index, (unsigned long) offset, self); ++ fclose(log); ++ } ++ } ++ } ++ if (MTLDrawTracking() && index < 31) debugDrawState.vertexOffsets[index] = offset; + _encoder->setVertexBufferOffset(offset, index); + } + +@@ -641,6 +989,7 @@ MTL_UNSUPPORTED_CLASS + length: (NSUInteger)length + atIndex: (NSUInteger)index + { ++ if (MTLDrawTracking() && index < 31) { debugDrawState.vertexBuffers[index] = nil; debugDrawState.vertexBytesLength[index] = length; memcpy(debugDrawState.vertexBytes[index], bytes, std::min(64, length)); } + _encoder->setVertexBytes(bytes, length, index); + } + +@@ -648,7 +997,8 @@ MTL_UNSUPPORTED_CLASS + offset: (NSUInteger)offset + atIndex: (NSUInteger)index + { +- _encoder->setFragmentBuffer(((MTLBufferInternal*)buffer).buffer, offset, index); ++ if (MTLDrawTracking() && index < 31) { debugDrawState.fragmentBuffers[index] = buffer; debugDrawState.fragmentOffsets[index] = offset; debugDrawState.fragmentBytesLength[index] = 0; } ++ _encoder->setFragmentBuffer(MTLBufferGetIndium(buffer), offset, index); + } + + - (void)setFragmentBuffers: (const id*)buffers +@@ -658,7 +1008,14 @@ MTL_UNSUPPORTED_CLASS + std::vector> tmp; + std::vector tmp2(offsets, offsets + range.length); + for (size_t i = 0; i < range.length; ++i) { +- tmp.push_back(((MTLBufferInternal*)buffers[i]).buffer); ++ tmp.push_back(MTLBufferGetIndium(buffers[i])); ++ } ++ if (MTLDrawTracking()) { ++ for (NSUInteger i = 0; i < range.length && range.location + i < 31; ++i) { ++ debugDrawState.fragmentBuffers[range.location + i] = buffers[i]; ++ debugDrawState.fragmentOffsets[range.location + i] = offsets[i]; ++ debugDrawState.fragmentBytesLength[range.location + i] = 0; ++ } + } + _encoder->setFragmentBuffers(tmp, tmp2, NSRangeToIndium(range)); + } +@@ -666,6 +1023,7 @@ MTL_UNSUPPORTED_CLASS + - (void)setFragmentBufferOffset: (NSUInteger)offset + atIndex: (NSUInteger)index + { ++ if (MTLDrawTracking() && index < 31) debugDrawState.fragmentOffsets[index] = offset; + _encoder->setFragmentBufferOffset(offset, index); + } + +@@ -673,6 +1031,7 @@ MTL_UNSUPPORTED_CLASS + length: (NSUInteger)length + atIndex: (NSUInteger)index + { ++ if (MTLDrawTracking() && index < 31) { debugDrawState.fragmentBuffers[index] = nil; debugDrawState.fragmentBytesLength[index] = length; memcpy(debugDrawState.fragmentBytes[index], bytes, std::min(64, length)); } + _encoder->setFragmentBytes(bytes, length, index); + } + +@@ -682,6 +1041,9 @@ MTL_UNSUPPORTED_CLASS + instanceCount: (NSUInteger)instanceCount + baseInstance: (NSUInteger)baseInstance + { ++ if (MTLDrawLimited(vertexCount)) return; ++ MTLTraceCount("draw"); ++ if (MTLDrawTracking()) MTLDebugLogDraw("direct", vertexCount, 0, 0, 0, nil, 0); + _encoder->drawPrimitives(static_cast(primitiveType), vertexStart, vertexCount, instanceCount, baseInstance); + } + +@@ -690,6 +1052,9 @@ MTL_UNSUPPORTED_CLASS + vertexCount: (NSUInteger)vertexCount + instanceCount: (NSUInteger)instanceCount + { ++ if (MTLDrawLimited(vertexCount)) return; ++ MTLTraceCount("draw"); ++ if (MTLDrawTracking()) MTLDebugLogDraw("direct", vertexCount, 0, 0, 0, nil, 0); + _encoder->drawPrimitives(static_cast(primitiveType), vertexStart, vertexCount, instanceCount); + } + +@@ -697,6 +1062,9 @@ MTL_UNSUPPORTED_CLASS + vertexStart: (NSUInteger)vertexStart + vertexCount: (NSUInteger)vertexCount + { ++ if (MTLDrawLimited(vertexCount)) return; ++ MTLTraceCount("draw"); ++ if (MTLDrawTracking()) MTLDebugLogDraw("direct", vertexCount, 0, 0, 0, nil, 0); + _encoder->drawPrimitives(static_cast(primitiveType), vertexStart, vertexCount); + } + +@@ -709,7 +1077,10 @@ MTL_UNSUPPORTED_CLASS + baseVertex: (NSInteger)baseVertex + baseInstance: (NSUInteger)baseInstance + { +- _encoder->drawIndexedPrimitives(static_cast(primitiveType), indexCount, static_cast(indexType), ((MTLBufferInternal*)indexBuffer).buffer, indexBufferOffset, instanceCount, baseVertex, baseInstance); ++ if (MTLDrawLimited(indexCount)) return; ++ MTLTraceCount("drawIndexed"); ++ if (MTLDrawTracking()) MTLDebugLogDraw("indexed", indexCount, instanceCount, (unsigned long) indexType, (long) baseVertex, indexBuffer, indexBufferOffset); ++ _encoder->drawIndexedPrimitives(static_cast(primitiveType), indexCount, static_cast(indexType), MTLBufferGetIndium(indexBuffer), indexBufferOffset, instanceCount, baseVertex, baseInstance); + } + + - (void)drawIndexedPrimitives: (MTLPrimitiveType)primitiveType +@@ -719,7 +1090,10 @@ MTL_UNSUPPORTED_CLASS + indexBufferOffset: (NSUInteger)indexBufferOffset + instanceCount: (NSUInteger)instanceCount + { +- _encoder->drawIndexedPrimitives(static_cast(primitiveType), indexCount, static_cast(indexType), ((MTLBufferInternal*)indexBuffer).buffer, indexBufferOffset, instanceCount); ++ if (MTLDrawLimited(indexCount)) return; ++ MTLTraceCount("drawIndexed"); ++ if (MTLDrawTracking()) MTLDebugLogDraw("indexed", indexCount, instanceCount, (unsigned long) indexType, 0, indexBuffer, indexBufferOffset); ++ _encoder->drawIndexedPrimitives(static_cast(primitiveType), indexCount, static_cast(indexType), MTLBufferGetIndium(indexBuffer), indexBufferOffset, instanceCount); + } + + - (void)drawIndexedPrimitives: (MTLPrimitiveType)primitiveType +@@ -728,7 +1102,10 @@ MTL_UNSUPPORTED_CLASS + indexBuffer: (id)indexBuffer + indexBufferOffset: (NSUInteger)indexBufferOffset + { +- _encoder->drawIndexedPrimitives(static_cast(primitiveType), indexCount, static_cast(indexType), ((MTLBufferInternal*)indexBuffer).buffer, indexBufferOffset); ++ if (MTLDrawLimited(indexCount)) return; ++ MTLTraceCount("drawIndexed"); ++ if (MTLDrawTracking()) MTLDebugLogDraw("indexed", indexCount, 0, (unsigned long) indexType, 0, indexBuffer, indexBufferOffset); ++ _encoder->drawIndexedPrimitives(static_cast(primitiveType), indexCount, static_cast(indexType), MTLBufferGetIndium(indexBuffer), indexBufferOffset); + } + + #else +diff --git a/src/Metal/MTLRenderPipeline.mm b/src/Metal/MTLRenderPipeline.mm +index 8aa709b..140d0da 100644 +--- a/src/Metal/MTLRenderPipeline.mm ++++ b/src/Metal/MTLRenderPipeline.mm +@@ -6,6 +6,7 @@ + #import + #import + #import ++#import + + #include + +@@ -204,7 +205,7 @@ MTL_UNSUPPORTED_CLASS + ((MTLFunctionInternal*)_fragmentFunction).function, + _maxVertexCallStackDepth, + _maxFragmentCallStackDepth, +- std::nullopt, // TODO ++ _vertexDescriptor ? std::make_optional([_vertexDescriptor asIndiumDescriptor]) : std::nullopt, + [_vertexBuffers asIndiumDescriptors], + [_fragmentBuffers asIndiumDescriptors], + [_colorAttachments asContiguousIndiumDescriptors], +@@ -317,7 +318,10 @@ MTL_UNSUPPORTED_CLASS + std::vector result; + + for (const auto& [key, value]: tmp2) { +- result.push_back(value); ++ if (value.pixelFormat != Indium::PixelFormat::Invalid) { ++ result.resize(key + 1); ++ result[key] = value; ++ } + } + + return result; +@@ -420,6 +424,10 @@ MTL_UNSUPPORTED_CLASS + #if DARLING_METAL_ENABLED + + @synthesize state = _state; ++const std::shared_ptr& MTLRenderPipelineGetIndium(id object) { ++ static const std::shared_ptr none; ++ return object ? ((MTLRenderPipelineStateInternal*) object)->_state : none; ++} + @synthesize device = _device; + @synthesize label = _label; + +diff --git a/src/Metal/MTLSamplerDescriptor.mm b/src/Metal/MTLSamplerDescriptor.mm +index 242d285..f82d083 100644 +--- a/src/Metal/MTLSamplerDescriptor.mm ++++ b/src/Metal/MTLSamplerDescriptor.mm +@@ -1,24 +1,69 @@ +-// SPDX-FileCopyrightText: 2026 Darling Developers + // SPDX-License-Identifier: MPL-2.0 +- ++#import + #import +-#import +-#import +- ++#include + @implementation MTLSamplerDescriptor +- ++@synthesize minFilter = _minFilter; ++@synthesize magFilter = _magFilter; ++@synthesize mipFilter = _mipFilter; ++@synthesize maxAnisotropy = _maxAnisotropy; ++@synthesize sAddressMode = _sAddressMode; ++@synthesize tAddressMode = _tAddressMode; ++@synthesize rAddressMode = _rAddressMode; ++@synthesize borderColor = _borderColor; ++@synthesize normalizedCoordinates = _normalizedCoordinates; ++@synthesize lodMinClamp = _lodMinClamp; ++@synthesize lodMaxClamp = _lodMaxClamp; ++@synthesize supportArgumentBuffers = _supportArgumentBuffers; ++@synthesize compareFunction = _compareFunction; ++@synthesize label = _label; ++- (instancetype)init { ++ self = [super init]; ++ if (self) { ++ _maxAnisotropy = 1; ++ _normalizedCoordinates = YES; ++ _lodMaxClamp = FLT_MAX; ++ } ++ return self; ++} ++- (void)dealloc { [_label release]; [super dealloc]; } ++- (instancetype)copyWithZone:(NSZone*)zone { ++ MTLSamplerDescriptor* copy = [[[self class] allocWithZone:zone] init]; ++ copy.minFilter = _minFilter; ++ copy.magFilter = _magFilter; ++ copy.mipFilter = _mipFilter; ++ copy.maxAnisotropy = _maxAnisotropy; ++ copy.sAddressMode = _sAddressMode; ++ copy.tAddressMode = _tAddressMode; ++ copy.rAddressMode = _rAddressMode; ++ copy.borderColor = _borderColor; ++ copy.normalizedCoordinates = _normalizedCoordinates; ++ copy.lodMinClamp = _lodMinClamp; ++ copy.lodMaxClamp = _lodMaxClamp; ++ copy.supportArgumentBuffers = _supportArgumentBuffers; ++ copy.compareFunction = _compareFunction; ++ copy.label = _label; ++ return copy; ++} ++@end + #if DARLING_METAL_ENABLED +- +-- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector +-{ +- return [NSMethodSignature signatureWithObjCTypes: "v@:"]; ++@implementation MTLSamplerStateInternal ++@synthesize sampler = _sampler; ++const std::shared_ptr& MTLSamplerGetIndium(id object) { ++ static const std::shared_ptr none; ++ return object ? ((MTLSamplerStateInternal*) object)->_sampler : none; + } +- +-- (void)forwardInvocation:(NSInvocation *)anInvocation +-{ +- NSLog(@"Stub called: %@ in %@", NSStringFromSelector([anInvocation selector]), [self class]); ++@synthesize device = _device; ++@synthesize label = _label; ++- (instancetype)initWithSampler:(std::shared_ptr)sampler device:(id)device label:(NSString*)label { ++ self = [super init]; ++ if (self) { ++ _sampler = sampler; ++ _device = [device retain]; ++ _label = [label copy]; ++ } ++ return self; + } +- +-#endif +- ++- (void)dealloc { [_device release]; [_label release]; [super dealloc]; } + @end ++#endif +diff --git a/src/Metal/MTLStencilDescriptor.mm b/src/Metal/MTLStencilDescriptor.mm +index 66a9056..f28235c 100644 +--- a/src/Metal/MTLStencilDescriptor.mm ++++ b/src/Metal/MTLStencilDescriptor.mm +@@ -1,24 +1,35 @@ + // SPDX-FileCopyrightText: 2026 Darling Developers + // SPDX-License-Identifier: MPL-2.0 + +-#import +-#import +-#import +- ++#import + @implementation MTLStencilDescriptor +- +-#if DARLING_METAL_ENABLED +- +-- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector +-{ +- return [NSMethodSignature signatureWithObjCTypes: "v@:"]; ++@synthesize stencilCompareFunction = _stencilCompareFunction; ++@synthesize stencilFailureOperation = _stencilFailureOperation; ++@synthesize depthFailureOperation = _depthFailureOperation; ++@synthesize depthStencilPassOperation = _depthStencilPassOperation; ++@synthesize readMask = _readMask; ++@synthesize writeMask = _writeMask; ++- (instancetype)init { ++ self = [super init]; ++ if (self) { _stencilCompareFunction = MTLCompareFunctionAlways; _readMask = UINT32_MAX; _writeMask = UINT32_MAX; } ++ return self; + } +- +-- (void)forwardInvocation:(NSInvocation *)anInvocation +-{ +- NSLog(@"Stub called: %@ in %@", NSStringFromSelector([anInvocation selector]), [self class]); ++- (id)copyWithZone:(NSZone*)zone { ++ MTLStencilDescriptor* copy = [[[self class] allocWithZone:zone] init]; ++ copy.stencilCompareFunction = _stencilCompareFunction; ++ copy.stencilFailureOperation = _stencilFailureOperation; ++ copy.depthFailureOperation = _depthFailureOperation; ++ copy.depthStencilPassOperation = _depthStencilPassOperation; ++ copy.readMask = _readMask; ++ copy.writeMask = _writeMask; ++ return copy; ++} ++#if DARLING_METAL_ENABLED ++- (Indium::StencilDescriptor)asIndiumDescriptor { ++ return {static_cast(_stencilCompareFunction), ++ static_cast(_stencilFailureOperation), ++ static_cast(_depthFailureOperation), ++ static_cast(_depthStencilPassOperation), _readMask, _writeMask}; + } +- + #endif +- + @end +diff --git a/src/Metal/MTLTexture.mm b/src/Metal/MTLTexture.mm +index 6c71fb9..822c112 100644 +--- a/src/Metal/MTLTexture.mm ++++ b/src/Metal/MTLTexture.mm +@@ -1,3 +1,5 @@ ++#import ++#include + // SPDX-FileCopyrightText: 2026 Darling Developers + // SPDX-License-Identifier: MPL-2.0 + +@@ -7,11 +9,37 @@ + #import + #import + ++ ++#if DARLING_METAL_ENABLED ++// Diagnostic: CPU texture uploads (replaceRegion) block on the GPU queue, so count, time and sample them. ++static void MTLDebugReplaceRegion(id texture, MTLRegion region, NSUInteger level, const void* bytes, double seconds) ++{ ++ MTLTraceCount("replaceRegion"); ++ MTLTraceTiming("replaceRegion", seconds); ++ static unsigned long logged = 0; ++ if ((MTLDebugEnabled() || MTLTimingEnabled()) && ++logged <= 60) { ++ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) { ++ const unsigned char* b = (const unsigned char*) bytes; ++ unsigned long nonZero = 0; ++ size_t probe = std::min(4096, (size_t) region.size.width * 4); ++ for (size_t i = 0; i < probe; ++i) if (b[i]) ++nonZero; ++ fprintf(log, "replaceRegion tex=%p %lux%lu fmt=%lu level=%lu region=(%lu,%lu %lux%lu) firstBytesNonZero=%lu/%zu took=%.1fms\n", texture, ++ (unsigned long) texture.width, (unsigned long) texture.height, (unsigned long) texture.pixelFormat, (unsigned long) level, ++ (unsigned long) region.origin.x, (unsigned long) region.origin.y, (unsigned long) region.size.width, (unsigned long) region.size.height, nonZero, probe, seconds * 1000); ++ fclose(log); ++ } ++ } ++} ++#endif + @implementation MTLTextureInternal + + #if DARLING_METAL_ENABLED + + @synthesize texture = _texture; ++const std::shared_ptr& MTLTextureGetIndium(id object) { ++ static const std::shared_ptr none; ++ return object ? ((MTLTextureInternal*) object)->_texture : none; ++} + @synthesize device = _device; + @synthesize resourceOptions = _resourceOptions; + @synthesize label = _label; +@@ -164,7 +192,9 @@ + withBytes: (const void*)pixelBytes + bytesPerRow: (NSUInteger)bytesPerRow + { ++ double start = (MTLDebugEnabled() || MTLTimingEnabled()) ? MTLTraceNow() : 0; + _texture->replaceRegion(MTLRegionToIndium(region), level, pixelBytes, bytesPerRow); ++ if (start != 0) MTLDebugReplaceRegion(self, region, level, pixelBytes, MTLTraceNow() - start); + } + + - (void)replaceRegion: (MTLRegion)region +@@ -174,7 +204,9 @@ + bytesPerRow: (NSUInteger)bytesPerRow + bytesPerImage: (NSUInteger)bytesPerImage + { ++ double start = (MTLDebugEnabled() || MTLTimingEnabled()) ? MTLTraceNow() : 0; + _texture->replaceRegion(MTLRegionToIndium(region), level, slice, pixelBytes, bytesPerRow, bytesPerImage); ++ if (start != 0) MTLDebugReplaceRegion(self, region, level, pixelBytes, MTLTraceNow() - start); + } + + - (void)getBytes: (void*)pixelBytes +diff --git a/src/Metal/MTLTextureDescriptor.mm b/src/Metal/MTLTextureDescriptor.mm +index 37cccb3..79b2350 100644 +--- a/src/Metal/MTLTextureDescriptor.mm ++++ b/src/Metal/MTLTextureDescriptor.mm +@@ -1,24 +1,64 @@ +-// SPDX-FileCopyrightText: 2026 Darling Developers + // SPDX-License-Identifier: MPL-2.0 +- + #import +-#import +-#import +- + @implementation MTLTextureDescriptor +- +-#if DARLING_METAL_ENABLED +- +-- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector +-{ +- return [NSMethodSignature signatureWithObjCTypes: "v@:"]; ++@synthesize textureType = _textureType; ++@synthesize pixelFormat = _pixelFormat; ++@synthesize width = _width; ++@synthesize height = _height; ++@synthesize depth = _depth; ++@synthesize mipmapLevelCount = _mipmapLevelCount; ++@synthesize sampleCount = _sampleCount; ++@synthesize arrayLength = _arrayLength; ++@synthesize resourceOptions = _resourceOptions; ++@synthesize allowGPUOptimizedContents = _allowGPUOptimizedContents; ++@synthesize usage = _usage; ++@synthesize swizzle = _swizzle; ++- (instancetype)init { ++ self = [super init]; ++ if (self) { ++ _textureType = MTLTextureType2D; ++ _pixelFormat = MTLPixelFormatRGBA8Unorm; ++ _width = _height = _depth = _mipmapLevelCount = _sampleCount = _arrayLength = 1; ++ _resourceOptions = MTLResourceStorageModeManaged; ++ _allowGPUOptimizedContents = YES; ++ _swizzle = MTLTextureSwizzleChannelsDefault; ++ } ++ return self; + } +- +-- (void)forwardInvocation:(NSInvocation *)anInvocation +-{ +- NSLog(@"Stub called: %@ in %@", NSStringFromSelector([anInvocation selector]), [self class]); ++- (instancetype)copyWithZone:(NSZone*)zone { ++ MTLTextureDescriptor* copy = [[[self class] allocWithZone:zone] init]; ++ copy.textureType = _textureType; ++ copy.pixelFormat = _pixelFormat; ++ copy.width = _width; ++ copy.height = _height; ++ copy.depth = _depth; ++ copy.mipmapLevelCount = _mipmapLevelCount; ++ copy.sampleCount = _sampleCount; ++ copy.arrayLength = _arrayLength; ++ copy.resourceOptions = _resourceOptions; ++ copy.allowGPUOptimizedContents = _allowGPUOptimizedContents; ++ copy.usage = _usage; ++ copy.swizzle = _swizzle; ++ return copy; ++} ++- (MTLCPUCacheMode)cpuCacheMode { return static_cast((_resourceOptions >> 0) & 0xf); } ++- (void)setCpuCacheMode:(MTLCPUCacheMode)value { _resourceOptions = static_cast((_resourceOptions & ~(0xful << 0)) | (static_cast(value) << 0)); } ++- (MTLStorageMode)storageMode { return static_cast((_resourceOptions >> 4) & 0xf); } ++- (void)setStorageMode:(MTLStorageMode)value { _resourceOptions = static_cast((_resourceOptions & ~(0xful << 4)) | (static_cast(value) << 4)); } ++- (MTLHazardTrackingMode)hazardTrackingMode { return static_cast((_resourceOptions >> 8) & 0xf); } ++- (void)setHazardTrackingMode:(MTLHazardTrackingMode)value { _resourceOptions = static_cast((_resourceOptions & ~(0xful << 8)) | (static_cast(value) << 8)); } +++ (instancetype)texture2DDescriptorWithPixelFormat:(MTLPixelFormat)format width:(NSUInteger)width height:(NSUInteger)height mipmapped:(BOOL)mipmapped { ++ MTLTextureDescriptor* desc = [[[self alloc] init] autorelease]; ++ desc.pixelFormat = format; desc.width = width; desc.height = height; ++ if (mipmapped) { ++ NSUInteger size = width > height ? width : height; ++ while (size > 1) { ++desc->_mipmapLevelCount; size >>= 1; } ++ } ++ return desc; ++} +++ (instancetype)textureCubeDescriptorWithPixelFormat:(MTLPixelFormat)format size:(NSUInteger)size mipmapped:(BOOL)mipmapped { ++ MTLTextureDescriptor* desc = [self texture2DDescriptorWithPixelFormat:format width:size height:size mipmapped:mipmapped]; ++ desc.textureType = MTLTextureTypeCube; ++ return desc; + } +- +-#endif +- + @end +diff --git a/src/Metal/MTLVertexDescriptor.mm b/src/Metal/MTLVertexDescriptor.mm +index cde968d..fc7d99d 100644 +--- a/src/Metal/MTLVertexDescriptor.mm ++++ b/src/Metal/MTLVertexDescriptor.mm +@@ -1,24 +1,168 @@ + // SPDX-FileCopyrightText: 2026 Darling Developers + // SPDX-License-Identifier: MPL-2.0 + +-#import +-#import +-#import +- ++#import ++#import ++// The bridge owns these concrete descriptor implementations. Keep their ++// scalar storage accessible within this translation unit so reset/conversion ++// can batch it. Subclasses continue through their public property methods. ++@interface MTLVertexBufferLayoutDescriptor () { ++@public ++ NSUInteger _stride; ++ MTLVertexStepFunction _stepFunction; ++ NSUInteger _stepRate; ++} ++@end ++@interface MTLVertexAttributeDescriptor () { ++@public ++ MTLVertexFormat _format; ++ NSUInteger _offset; ++ NSUInteger _bufferIndex; ++} ++@end ++@implementation MTLVertexBufferLayoutDescriptor ++@synthesize stride = _stride; ++@synthesize stepFunction = _stepFunction; ++@synthesize stepRate = _stepRate; ++- (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 { ++ 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) ++} ++- (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]; ++ if (!_items[index]) { _items[index] = [MTLVertexBufferLayoutDescriptor new]; _present |= 1u << index; _concrete |= 1u << index; } ++ return _items[index]; ++} ++- (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)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]; ++ _items[index] = [descriptor retain]; ++ [old release]; ++ const uint32_t bit = 1u << index; ++ if (descriptor) _present |= bit; else _present &= ~bit; ++ if (descriptor && object_getClass(descriptor) == [MTLVertexBufferLayoutDescriptor class]) _concrete |= bit; else _concrete &= ~bit; ++} ++- (id)copyWithZone:(NSZone*)zone { ++ MTLVertexBufferLayoutDescriptorArray* copy = [[[self class] allocWithZone:zone] init]; ++ for (int i = 0; i < 31; ++i) if (_items[i]) copy->_items[i] = [_items[i] copy]; ++ copy->_present = _present; ++ copy->_concrete = _concrete; // -copy of a concrete item returns the same class ++ return copy; ++} ++@end ++@implementation MTLVertexAttributeDescriptor ++@synthesize format = _format; ++@synthesize offset = _offset; ++@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) ++} ++- (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]; ++ if (!_items[index]) { _items[index] = [MTLVertexAttributeDescriptor new]; _present |= 1u << index; _concrete |= 1u << index; } ++ 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)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]; ++ _items[index] = [descriptor retain]; ++ [old release]; ++ const uint32_t bit = 1u << index; ++ if (descriptor) _present |= bit; else _present &= ~bit; ++ if (descriptor && object_getClass(descriptor) == [MTLVertexAttributeDescriptor class]) _concrete |= bit; else _concrete &= ~bit; ++} ++- (id)copyWithZone:(NSZone*)zone { ++ MTLVertexAttributeDescriptorArray* copy = [[[self class] allocWithZone:zone] init]; ++ for (int i = 0; i < 31; ++i) if (_items[i]) copy->_items[i] = [_items[i] copy]; ++ copy->_present = _present; ++ copy->_concrete = _concrete; // -copy of a concrete item returns the same class ++ return copy; ++} ++@end + @implementation MTLVertexDescriptor +- +-#if DARLING_METAL_ENABLED +- +-- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector +-{ +- return [NSMethodSignature signatureWithObjCTypes: "v@:"]; ++@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; } ++ [_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]; ++ copy->_layouts = [_layouts copy]; copy->_attributes = [_attributes copy]; ++ return copy; ++} ++#if DARLING_METAL_ENABLED ++- (Indium::VertexDescriptor)asIndiumDescriptor { ++ static Class attributeClass = [MTLVertexAttributeDescriptor class]; ++ static Class layoutClass = [MTLVertexBufferLayoutDescriptor class]; ++ Indium::VertexDescriptor result; ++ for (NSUInteger i = 0; i < 31; ++i) { ++ MTLVertexAttributeDescriptor* attr = [_attributes existingObjectAtIndex: i]; ++ if (!attr) continue; ++ const bool concrete = object_getClass(attr) == attributeClass; ++ const auto format = concrete ? attr->_format : attr.format; ++ if (format == MTLVertexFormatInvalid) continue; ++ const auto offset = concrete ? attr->_offset : attr.offset; ++ const auto index = concrete ? attr->_bufferIndex : attr.bufferIndex; ++ result.attributes[i] = {static_cast(format), offset, index}; ++ MTLVertexBufferLayoutDescriptor* layout = _layouts[index]; ++ if (object_getClass(layout) == layoutClass) ++ result.layouts[index] = {layout->_stride, static_cast(layout->_stepFunction), layout->_stepRate}; ++ else ++ result.layouts[index] = {layout.stride, static_cast(layout.stepFunction), layout.stepRate}; ++ } ++ return result; + } +- + #endif +- + @end +diff --git a/include/Metal/MTLArgument.h b/include/Metal/MTLArgument.h +new file mode 100644 +--- /dev/null ++++ b/include/Metal/MTLArgument.h +@@ -0,0 +1,29 @@ ++#ifndef _METAL_MTLARGUMENT_H_ ++#define _METAL_MTLARGUMENT_H_ ++#import ++typedef NS_ENUM(NSUInteger, MTLArgumentType) { MTLArgumentTypeBuffer=0, MTLArgumentTypeThreadgroupMemory=1, MTLArgumentTypeTexture=2, MTLArgumentTypeSampler=3 }; ++@interface MTLStructMember : NSObject ++@property(readonly) NSString* name; ++@property(readonly) NSUInteger offset; ++@end ++@interface MTLStructType : NSObject ++@property(readonly) NSArray* members; ++- (MTLStructMember*)memberByName:(NSString*)name; ++@end ++@interface MTLArgument : NSObject ++@property(readonly) NSString* name; ++@property(readonly) MTLArgumentType type; ++@property(readonly) NSUInteger index; ++@property(readonly, getter=isActive) BOOL active; ++@property(readonly) NSUInteger bufferAlignment; ++@property(readonly) NSUInteger bufferDataSize; ++@property(readonly) MTLStructType* bufferStructType; ++@end ++@interface MTLComputePipelineReflection : NSObject ++@property(readonly) NSArray* arguments; ++@end ++@interface MTLRenderPipelineReflection : NSObject ++@property(readonly) NSArray* vertexArguments; ++@property(readonly) NSArray* fragmentArguments; ++@end ++#endif +diff --git a/include/Metal/MTLBlitCommandEncoder.h b/include/Metal/MTLBlitCommandEncoder.h +new file mode 100644 +--- /dev/null ++++ b/include/Metal/MTLBlitCommandEncoder.h +@@ -0,0 +1,28 @@ ++// SPDX-FileCopyrightText: 2026 Darling Developers ++// SPDX-License-Identifier: MPL-2.0 ++ ++#ifndef _METAL_MTLBLITCOMMANDENCODER_H_ ++#define _METAL_MTLBLITCOMMANDENCODER_H_ ++#import ++#import ++@protocol MTLBuffer; ++@protocol MTLTexture; ++typedef NS_OPTIONS(NSUInteger, MTLBlitOption) { ++ MTLBlitOptionNone = 0, ++ MTLBlitOptionDepthFromDepthStencil = 1, ++ MTLBlitOptionStencilFromDepthStencil = 2, ++ MTLBlitOptionRowLinearPVRTC = 4, ++}; ++@protocol MTLBlitCommandEncoder ++- (void)copyFromBuffer:(id)source sourceOffset:(NSUInteger)sourceOffset toBuffer:(id)destination destinationOffset:(NSUInteger)destinationOffset size:(NSUInteger)size; ++- (void)copyFromBuffer:(id)source sourceOffset:(NSUInteger)sourceOffset sourceBytesPerRow:(NSUInteger)sourceBytesPerRow sourceBytesPerImage:(NSUInteger)sourceBytesPerImage sourceSize:(MTLSize)sourceSize toTexture:(id)destination destinationSlice:(NSUInteger)destinationSlice destinationLevel:(NSUInteger)destinationLevel destinationOrigin:(MTLOrigin)destinationOrigin; ++- (void)copyFromTexture:(id)source sourceSlice:(NSUInteger)sourceSlice sourceLevel:(NSUInteger)sourceLevel sourceOrigin:(MTLOrigin)sourceOrigin sourceSize:(MTLSize)sourceSize toBuffer:(id)destination destinationOffset:(NSUInteger)destinationOffset destinationBytesPerRow:(NSUInteger)destinationBytesPerRow destinationBytesPerImage:(NSUInteger)destinationBytesPerImage; ++- (void)copyFromTexture:(id)source toTexture:(id)destination; ++- (void)copyFromTexture:(id)source sourceSlice:(NSUInteger)sourceSlice sourceLevel:(NSUInteger)sourceLevel toTexture:(id)destination destinationSlice:(NSUInteger)destinationSlice destinationLevel:(NSUInteger)destinationLevel sliceCount:(NSUInteger)sliceCount levelCount:(NSUInteger)levelCount; ++- (void)copyFromTexture:(id)source sourceSlice:(NSUInteger)sourceSlice sourceLevel:(NSUInteger)sourceLevel sourceOrigin:(MTLOrigin)sourceOrigin sourceSize:(MTLSize)sourceSize toTexture:(id)destination destinationSlice:(NSUInteger)destinationSlice destinationLevel:(NSUInteger)destinationLevel destinationOrigin:(MTLOrigin)destinationOrigin; ++- (void)fillBuffer:(id)buffer range:(NSRange)range value:(uint8_t)value; ++- (void)generateMipmapsForTexture:(id)texture; ++- (void)copyFromBuffer:(id)source sourceOffset:(NSUInteger)sourceOffset sourceBytesPerRow:(NSUInteger)sourceBytesPerRow sourceBytesPerImage:(NSUInteger)sourceBytesPerImage sourceSize:(MTLSize)sourceSize toTexture:(id)destination destinationSlice:(NSUInteger)destinationSlice destinationLevel:(NSUInteger)destinationLevel destinationOrigin:(MTLOrigin)destinationOrigin options:(MTLBlitOption)options; ++- (void)copyFromTexture:(id)source sourceSlice:(NSUInteger)sourceSlice sourceLevel:(NSUInteger)sourceLevel sourceOrigin:(MTLOrigin)sourceOrigin sourceSize:(MTLSize)sourceSize toBuffer:(id)destination destinationOffset:(NSUInteger)destinationOffset destinationBytesPerRow:(NSUInteger)destinationBytesPerRow destinationBytesPerImage:(NSUInteger)destinationBytesPerImage options:(MTLBlitOption)options; ++@end ++#endif +diff --git a/include/Metal/MTLExceptionBridge.h b/include/Metal/MTLExceptionBridge.h +new file mode 100644 +--- /dev/null ++++ b/include/Metal/MTLExceptionBridge.h +@@ -0,0 +1,23 @@ ++#pragma once ++#import ++#include ++#include ++#include ++ ++// Metal reports failures with nil and an NSError, while the Indium backend reports them with C++ ++// exceptions, which the callers of the Metal API do not expect. Call from a catch block: builds the ++// NSError from the active exception and records it in the guest's /tmp/metal-errors.log. ++static inline NSError* MTLErrorFromCurrentException(const char* where) ++{ ++ std::string message = "unknown error"; ++ try { throw; } ++ catch (const std::exception& e) { message = e.what(); } ++ catch (...) {} ++ ++ if (FILE* log = fopen("/tmp/metal-errors.log", "a")) { ++ fprintf(log, "%s: %s\n", where, message.c_str()); ++ fclose(log); ++ } ++ return [NSError errorWithDomain: @"MTLLibraryErrorDomain" code: 3 ++ userInfo: @{NSLocalizedDescriptionKey: [NSString stringWithUTF8String: message.c_str()]}]; ++} +diff --git a/include/Metal/MTLTrace.h b/include/Metal/MTLTrace.h +new file mode 100644 +--- /dev/null ++++ b/include/Metal/MTLTrace.h +@@ -0,0 +1,106 @@ ++#pragma once ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++// All the Metal diagnostics (counters, render-pass descriptions, the magenta overrides, drawable readbacks) are ++// off unless /tmp/metal-debug-trace exists when the process starts. ++inline bool MTLDebugEnabled() ++{ ++ static const bool enabled = access("/tmp/metal-debug-trace", F_OK) == 0; ++ return enabled; ++} ++ ++// Total draw calls seen so far, used to attribute draws to render passes. ++inline std::atomic& MTLTraceDrawTotal() ++{ ++ static std::atomic total { 0 }; ++ return total; ++} ++ ++// Diagnostic event counters, appended to the guest's /tmp/metal-trace.log every few seconds. ++inline void MTLTraceCount(const char* name) ++{ ++ static const bool countDraws = MTLDebugEnabled() || access("/tmp/metal-debug-timing", F_OK) == 0 || access("/tmp/metal-debug-snapshotdraws", F_OK) == 0; ++ if (countDraws && name[0] == 'd' && name[1] == 'r' && name[2] == 'a' && name[3] == 'w') { ++ ++MTLTraceDrawTotal(); ++ } ++ if (!MTLDebugEnabled()) { ++ return; ++ } ++ ++ static std::mutex lock; ++ static std::map counts; ++ static time_t last = 0; ++ ++ std::lock_guard guard(lock); ++ ++counts[name]; ++ ++ time_t now = time(nullptr); ++ if (now - last >= 3) { ++ last = now; ++ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) { ++ fprintf(log, "counts@%ld:", (long) now); ++ for (auto& count: counts) { ++ fprintf(log, " %s=%lu", count.first.c_str(), count.second); ++ } ++ fprintf(log, "\n"); ++ fclose(log); ++ } ++ } ++} ++ ++// Cheap aggregated timings (flag file /tmp/metal-debug-timing in the guest when the process starts), printed every 5 s. ++inline void MTLTraceTiming(const char* what, double seconds) ++{ ++ static const bool enabled = access("/tmp/metal-debug-timing", F_OK) == 0; ++ if (!enabled) { ++ return; ++ } ++ struct Stat { double sum = 0, max = 0; unsigned long count = 0; }; ++ static std::mutex lock; ++ static std::map stats; ++ static time_t last = 0; ++ std::lock_guard guard(lock); ++ Stat& stat = stats[what]; ++ stat.sum += seconds; ++ stat.count++; ++ if (seconds > stat.max) stat.max = seconds; ++ time_t now = time(nullptr); ++ if (now - last >= 5) { ++ last = now; ++ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) { ++ fprintf(log, "mtl-timing@%ld:", (long) now); ++ for (auto& entry: stats) { ++ fprintf(log, " %s[n=%lu avg=%.1fms max=%.1fms]", entry.first.c_str(), entry.second.count, entry.second.sum / entry.second.count * 1000, entry.second.max * 1000); ++ } ++ fprintf(log, "\n"); ++ fclose(log); ++ } ++ stats.clear(); ++ } ++} ++ ++inline bool MTLTimingEnabled() ++{ ++ static const bool enabled = access("/tmp/metal-debug-timing", F_OK) == 0; ++ return enabled; ++} ++ ++inline double MTLTraceNow() ++{ ++ struct timespec ts; ++ clock_gettime(CLOCK_MONOTONIC, &ts); ++ return ts.tv_sec + ts.tv_nsec / 1e9; ++} ++ ++// Draw-state tracking (cheap bookkeeping for the draw log): on with the trace flag, or with /tmp/metal-debug-draws. ++inline bool MTLDrawTracking() ++{ ++ static const bool enabled = MTLDebugEnabled() || access("/tmp/metal-debug-draws", F_OK) == 0; ++ return enabled; ++} +diff --git a/private-include/Metal/MTLArgumentInternal.h b/private-include/Metal/MTLArgumentInternal.h +new file mode 100644 +--- /dev/null ++++ b/private-include/Metal/MTLArgumentInternal.h +@@ -0,0 +1,15 @@ ++#ifndef _METAL_MTLARGUMENTINTERNAL_H_ ++#define _METAL_MTLARGUMENTINTERNAL_H_ ++#import ++#import ++#if DARLING_METAL_ENABLED ++#include ++@interface MTLComputePipelineReflection (Internal) ++- (instancetype)initWithFunction:(std::shared_ptr)function; ++@end ++@interface MTLRenderPipelineReflection (Internal) ++- (instancetype)initWithVertexFunction:(std::shared_ptr)vertex fragmentFunction:(std::shared_ptr)fragment; ++- (instancetype)initWithVertexFunction:(std::shared_ptr)vertex fragmentFunction:(std::shared_ptr)fragment vertexDescriptor:(MTLVertexDescriptor*)vertexDescriptor; ++@end ++#endif ++#endif +diff --git a/private-include/Metal/MTLBlitCommandEncoderInternal.h b/private-include/Metal/MTLBlitCommandEncoderInternal.h +new file mode 100644 +--- /dev/null ++++ b/private-include/Metal/MTLBlitCommandEncoderInternal.h +@@ -0,0 +1,15 @@ ++// SPDX-FileCopyrightText: 2026 Darling Developers ++// SPDX-License-Identifier: MPL-2.0 ++ ++#ifndef _METAL_MTLBLITCOMMANDENCODERINTERNAL_H_ ++#define _METAL_MTLBLITCOMMANDENCODERINTERNAL_H_ ++#import ++#if DARLING_METAL_ENABLED ++#include ++#endif ++@interface MTLBlitCommandEncoderInternal : NSObject ++#if DARLING_METAL_ENABLED ++- (instancetype)initWithEncoder:(std::shared_ptr)encoder device:(id)device; ++#endif ++@end ++#endif +diff --git a/private-include/Metal/MTLDepthStencilInternal.h b/private-include/Metal/MTLDepthStencilInternal.h +new file mode 100644 +--- /dev/null ++++ b/private-include/Metal/MTLDepthStencilInternal.h +@@ -0,0 +1,25 @@ ++// SPDX-FileCopyrightText: 2026 Darling Developers ++// SPDX-License-Identifier: MPL-2.0 ++ ++#ifndef _METAL_MTLDEPTHSTENCILINTERNAL_H_ ++#define _METAL_MTLDEPTHSTENCILINTERNAL_H_ ++#import ++#if DARLING_METAL_ENABLED ++#include ++@interface MTLStencilDescriptor (Internal) ++- (Indium::StencilDescriptor)asIndiumDescriptor; ++@end ++@interface MTLDepthStencilDescriptor (Internal) ++- (Indium::DepthStencilDescriptor)asIndiumDescriptor; ++@end ++#endif ++@interface MTLDepthStencilStateInternal : NSObject ++#if DARLING_METAL_ENABLED ++@property(nonatomic, readonly) std::shared_ptr state; ++- (instancetype)initWithState:(std::shared_ptr)state device:(id)device label:(NSString*)label; ++#endif ++@end ++// Direct access for the encoders: the property getter is a message send that returns the shared_ptr by value. ++const std::shared_ptr& MTLDepthStencilGetIndium(id object); ++ ++#endif +diff --git a/private-include/Metal/MTLSamplerStateInternal.h b/private-include/Metal/MTLSamplerStateInternal.h +new file mode 100644 +--- /dev/null ++++ b/private-include/Metal/MTLSamplerStateInternal.h +@@ -0,0 +1,13 @@ ++// SPDX-License-Identifier: MPL-2.0 ++#pragma once ++#import ++#if DARLING_METAL_ENABLED ++#include ++@interface MTLSamplerStateInternal : NSObject ++@property(nonatomic, readonly) std::shared_ptr sampler; ++- (instancetype)initWithSampler:(std::shared_ptr)sampler device:(id)device label:(NSString*)label; ++@end ++// Direct access for the encoders: the property getter is a message send that returns the shared_ptr by value. ++const std::shared_ptr& MTLSamplerGetIndium(id object); ++ ++#endif +diff --git a/private-include/Metal/MTLVertexDescriptorInternal.h b/private-include/Metal/MTLVertexDescriptorInternal.h +new file mode 100644 +--- /dev/null ++++ b/private-include/Metal/MTLVertexDescriptorInternal.h +@@ -0,0 +1,13 @@ ++// SPDX-FileCopyrightText: 2026 Darling Developers ++// SPDX-License-Identifier: MPL-2.0 ++ ++#ifndef _METAL_MTLVERTEXDESCRIPTORINTERNAL_H_ ++#define _METAL_MTLVERTEXDESCRIPTORINTERNAL_H_ ++#import ++#if DARLING_METAL_ENABLED ++#include ++@interface MTLVertexDescriptor (Internal) ++- (Indium::VertexDescriptor)asIndiumDescriptor; ++@end ++#endif ++#endif +diff --git a/src/Metal/MTLArgument.mm b/src/Metal/MTLArgument.mm +new file mode 100644 +--- /dev/null ++++ b/src/Metal/MTLArgument.mm +@@ -0,0 +1,78 @@ ++#import ++@implementation MTLStructMember ++@synthesize name=_name, offset=_offset; ++- (instancetype)initWithName:(NSString*)name offset:(NSUInteger)offset { self=[super init]; if(self){_name=[name copy];_offset=offset;}return self; } ++- (void)dealloc { [_name release];[super dealloc]; } ++@end ++@implementation MTLStructType ++@synthesize members=_members; ++- (instancetype)initWithMembers:(NSArray*)members { self=[super init];if(self)_members=[members copy];return self; } ++- (MTLStructMember*)memberByName:(NSString*)name { for(MTLStructMember* member in _members) if([member.name isEqualToString:name])return member; return nil; } ++- (void)dealloc { [_members release];[super dealloc]; } ++@end ++@implementation MTLArgument ++@synthesize name=_name, type=_type, index=_index, bufferAlignment=_bufferAlignment, bufferDataSize=_bufferDataSize, bufferStructType=_bufferStructType; ++- (BOOL)isActive { return YES; } ++#if DARLING_METAL_ENABLED ++- (instancetype)initWithInfo:(const Iridium::ArgumentInfo&)info { ++ self=[super init];if(self){ ++ _name=[[NSString alloc] initWithUTF8String:info.name.c_str()];_index=info.index; ++ _type=info.type==Iridium::BindingType::Buffer ? MTLArgumentTypeBuffer : (info.type==Iridium::BindingType::Texture ? MTLArgumentTypeTexture : MTLArgumentTypeSampler); ++ _bufferAlignment=info.alignment;_bufferDataSize=info.size; ++ if(!info.members.empty()){ ++ NSMutableArray* members=[NSMutableArray new]; ++ for(const auto& member:info.members){ MTLStructMember* object=[[MTLStructMember alloc] initWithName:[NSString stringWithUTF8String:member.name.c_str()] offset:member.offset];[members addObject:object];[object release]; } ++ _bufferStructType=[[MTLStructType alloc] initWithMembers:members];[members release]; ++ } ++ }return self; ++} ++#endif ++- (void)dealloc { [_name release];[_bufferStructType release];[super dealloc]; } ++@end ++#if DARLING_METAL_ENABLED ++static NSArray* argumentsForFunction(std::shared_ptr function) { ++ NSMutableArray* result=[NSMutableArray array]; ++ if(function)for(const auto& info:function->argumentInfo()){ ++ MTLArgument* argument=[[MTLArgument alloc] initWithInfo:info];[result addObject:argument];[argument release]; ++ } ++ return result; ++} ++#endif ++@implementation MTLComputePipelineReflection ++@synthesize arguments=_arguments; ++#if DARLING_METAL_ENABLED ++- (instancetype)initWithFunction:(std::shared_ptr)function { self=[super init];if(self)_arguments=[argumentsForFunction(function) copy];return self; } ++#endif ++- (void)dealloc { [_arguments release];[super dealloc]; } ++@end ++@implementation MTLRenderPipelineReflection ++@synthesize vertexArguments=_vertexArguments, fragmentArguments=_fragmentArguments; ++#if DARLING_METAL_ENABLED ++- (instancetype)initWithVertexFunction:(std::shared_ptr)vertex fragmentFunction:(std::shared_ptr)fragment { ++ return [self initWithVertexFunction:vertex fragmentFunction:fragment vertexDescriptor:nil]; ++} ++// Metal lists the vertex buffers that feed [[stage_in]] among the vertex arguments, as buffers named "vertexBuffer." ++// (the game derives which vertex streams to bind from them). ++- (instancetype)initWithVertexFunction:(std::shared_ptr)vertex fragmentFunction:(std::shared_ptr)fragment vertexDescriptor:(MTLVertexDescriptor*)vertexDescriptor { ++ self=[super init];if(self){ ++ NSMutableArray* vertexArguments=[argumentsForFunction(vertex) mutableCopy]; ++ NSMutableIndexSet* seen=[NSMutableIndexSet indexSet]; ++ for(NSUInteger attribute=0;vertexDescriptor&&attribute<31;++attribute){ ++ MTLVertexAttributeDescriptor* description=vertexDescriptor.attributes[attribute]; ++ if(description.format==MTLVertexFormatInvalid||[seen containsIndex:description.bufferIndex])continue; ++ [seen addIndex:description.bufferIndex]; ++ Iridium::ArgumentInfo info; ++ info.type=Iridium::BindingType::Buffer; ++ info.index=description.bufferIndex; ++ info.name="vertexBuffer."+std::to_string(description.bufferIndex); ++ info.size=vertexDescriptor.layouts[description.bufferIndex].stride; ++ info.alignment=4; ++ MTLArgument* argument=[[MTLArgument alloc] initWithInfo:info];[vertexArguments addObject:argument];[argument release]; ++ } ++ _vertexArguments=vertexArguments; ++ _fragmentArguments=[argumentsForFunction(fragment) copy]; ++ }return self; ++} ++#endif ++- (void)dealloc { [_vertexArguments release];[_fragmentArguments release];[super dealloc]; } ++@end +diff --git a/src/Metal/MTLBlitCommandEncoder.mm b/src/Metal/MTLBlitCommandEncoder.mm +new file mode 100644 +--- /dev/null ++++ b/src/Metal/MTLBlitCommandEncoder.mm +@@ -0,0 +1,101 @@ ++// SPDX-FileCopyrightText: 2026 Darling Developers ++// SPDX-License-Identifier: MPL-2.0 ++ ++#import ++#include ++#import ++#import ++#import ++#import ++#import ++ ++#if DARLING_METAL_ENABLED ++// Diagnostic (/tmp/metal-debug-timing): are the texture uploads carrying data? Sample the start of each source. ++static void MTLDebugUpload(id source, NSUInteger offset, id destination, NSUInteger level, MTLSize size) ++{ ++ if (!MTLTimingEnabled()) { ++ return; ++ } ++ const uint8_t* bytes = (const uint8_t*) source.contents; ++ if (!bytes) { ++ MTLTraceTiming("upload_sourceNotCPUVisible", 0); ++ return; ++ } ++ NSUInteger available = source.length > offset ? source.length - offset : 0; ++ NSUInteger probe = std::min(available, 4096); ++ NSUInteger nonZero = 0; ++ for (NSUInteger i = 0; i < probe; ++i) if (bytes[offset + i]) ++nonZero; ++ char name[64]; ++ snprintf(name, sizeof(name), "upload_%s_f%lu", nonZero ? "data" : "ZERO", (unsigned long) destination.pixelFormat); ++ MTLTraceTiming(name, 0); ++ static unsigned long logged = 0; ++ if (!nonZero && destination.width >= 256 && ++logged <= 60) { ++ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) { ++ fprintf(log, "zero-upload tex=%lux%lu fmt=%lu mips=%lu level=%lu size=%lux%lu srcOffset=%lu\n", (unsigned long) destination.width, (unsigned long) destination.height, (unsigned long) destination.pixelFormat, ++ (unsigned long) destination.mipmapLevelCount, (unsigned long) level, (unsigned long) size.width, (unsigned long) size.height, (unsigned long) offset); ++ fclose(log); ++ } ++ } ++} ++#endif ++@implementation MTLBlitCommandEncoderInternal ++#if DARLING_METAL_ENABLED ++{ ++ std::shared_ptr _encoder; ++} ++@synthesize device = _device; ++@synthesize label = _label; ++- (instancetype)initWithEncoder:(std::shared_ptr)encoder device:(id)device { ++ self = [super init]; ++ if (self) { _encoder = encoder; _device = [device retain]; } ++ return self; ++} ++- (void)dealloc { ++ [_device release]; [_label release]; [super dealloc]; ++} ++- (void)endEncoding { _encoder->endEncoding(); } ++- (void)copyFromBuffer:(id)source sourceOffset:(NSUInteger)sourceOffset toBuffer:(id)destination destinationOffset:(NSUInteger)destinationOffset size:(NSUInteger)size { ++ MTLTraceCount("blitBufferToBuffer"); ++ _encoder->copy(((MTLBufferInternal*)source).buffer, sourceOffset, ((MTLBufferInternal*)destination).buffer, destinationOffset, size); ++} ++- (void)copyFromBuffer:(id)source sourceOffset:(NSUInteger)sourceOffset sourceBytesPerRow:(NSUInteger)sourceBytesPerRow sourceBytesPerImage:(NSUInteger)sourceBytesPerImage sourceSize:(MTLSize)sourceSize toTexture:(id)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})); ++} ++- (void)copyFromTexture:(id)source sourceSlice:(NSUInteger)sourceSlice sourceLevel:(NSUInteger)sourceLevel sourceOrigin:(MTLOrigin)sourceOrigin sourceSize:(MTLSize)sourceSize toBuffer:(id)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); ++} ++- (void)copyFromTexture:(id)source toTexture:(id)destination { ++ MTLTraceCount("blitTextureToTexture"); ++ _encoder->copy(((MTLTextureInternal*)source).texture, ((MTLTextureInternal*)destination).texture); ++} ++- (void)copyFromTexture:(id)source sourceSlice:(NSUInteger)sourceSlice sourceLevel:(NSUInteger)sourceLevel toTexture:(id)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); ++} ++- (void)copyFromTexture:(id)source sourceSlice:(NSUInteger)sourceSlice sourceLevel:(NSUInteger)sourceLevel sourceOrigin:(MTLOrigin)sourceOrigin sourceSize:(MTLSize)sourceSize toTexture:(id)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})); ++} ++- (void)fillBuffer:(id)buffer range:(NSRange)range value:(uint8_t)value { ++ _encoder->fillBuffer(((MTLBufferInternal*)buffer).buffer, NSRangeToIndium(range), value); ++} ++- (void)generateMipmapsForTexture:(id)texture { ++ MTLTraceCount("generateMipmaps"); ++ _encoder->generateMipmapsForTexture(((MTLTextureInternal*)texture).texture); ++} ++- (void)copyFromBuffer:(id)source sourceOffset:(NSUInteger)sourceOffset sourceBytesPerRow:(NSUInteger)sourceBytesPerRow sourceBytesPerImage:(NSUInteger)sourceBytesPerImage sourceSize:(MTLSize)sourceSize toTexture:(id)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(options)); ++} ++- (void)copyFromTexture:(id)source sourceSlice:(NSUInteger)sourceSlice sourceLevel:(NSUInteger)sourceLevel sourceOrigin:(MTLOrigin)sourceOrigin sourceSize:(MTLSize)sourceSize toBuffer:(id)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(options)); ++} ++#else ++@dynamic device, label; ++MTL_UNSUPPORTED_CLASS ++#endif ++@end diff --git a/patches/objc4.patch b/patches/objc4.patch new file mode 100644 index 0000000..9dfe03a --- /dev/null +++ b/patches/objc4.patch @@ -0,0 +1,25 @@ +diff --git a/runtime/objc-private.h b/runtime/objc-private.h +index ff2d703..cd887c9 100644 +--- a/runtime/objc-private.h ++++ b/runtime/objc-private.h +@@ -886,6 +886,9 @@ inline void* operator new(std::size_t size, const std::nothrow_t&) noexcept(true + inline void* operator new[](std::size_t size, const std::nothrow_t&) noexcept(true) { return malloc(size); } + inline void operator delete(void* p) noexcept(true) { free(p); } + inline void operator delete[](void* p) noexcept(true) { free(p); } ++// C++14 sized deallocation must use the same private allocator as new. ++inline void operator delete(void* p, std::size_t) noexcept(true) { free(p); } ++inline void operator delete[](void* p, std::size_t) noexcept(true) { free(p); } + inline void operator delete(void* p, const std::nothrow_t&) noexcept(true) { free(p); } + inline void operator delete[](void* p, const std::nothrow_t&) noexcept(true) { free(p); } + #pragma clang diagnostic pop +diff --git a/unexported_symbols b/unexported_symbols +index 73f77a8..a9c3905 100644 +--- a/unexported_symbols ++++ b/unexported_symbols +@@ -15,3 +15,6 @@ __ZTSSt9bad_alloc + __ZTSSt9exception + __ZTSSt11logic_error + __ZTSSt12length_error ++ ++__ZdlPvm ++__ZdaPvm diff --git a/patches/security.patch b/patches/security.patch new file mode 100644 index 0000000..d12af04 --- /dev/null +++ b/patches/security.patch @@ -0,0 +1,225 @@ +diff --git a/OSX/libsecurity_codesigning/lib/StaticCode.cpp b/OSX/libsecurity_codesigning/lib/StaticCode.cpp +index 0c4be9d3..0a3c75d9 100644 +--- a/OSX/libsecurity_codesigning/lib/StaticCode.cpp ++++ b/OSX/libsecurity_codesigning/lib/StaticCode.cpp +@@ -1247,7 +1247,11 @@ void SecStaticCode::validateExecutable() + if (!cd) + MacOSError::throwMe(errSecCSUnsigned); + AutoFileDesc fd(mainExecutablePath(), O_RDONLY); ++#ifndef DARLING ++ // This is only an I/O optimization. Darling does not implement this ++ // Darwin cache hint; retain the complete page-by-page hash validation. + fd.fcntl(F_NOCACHE, true); // turn off page caching (one-pass) ++#endif + if (Universal *fat = mRep->mainExecutableImage()) + fd.seek(fat->archOffset()); + size_t pageSize = cd->pageSize ? (1 << cd->pageSize) : 0; +@@ -1328,8 +1332,14 @@ void SecStaticCode::validateResources(SecCSFlags flags) + itemIsOnRootFS, skipRootVolumeExceptions, useRootFSPolicy, itemMightUseXattrFiles, skipXattrFiles); + + if (mLimitedAsync == NULL) { ++#ifdef DARLING ++ // Darling cannot query disk media through Apple's I/O registry. ++ // Use the existing serial resource validator without that SSD hint. ++ bool runMultiThreaded = false; ++#else + bool runMultiThreaded = ((flags & kSecCSSingleThreaded) == kSecCSSingleThreaded) ? false : + (diskRep()->fd().mediumType() == kIOPropertyMediumTypeSolidStateKey); ++#endif + mLimitedAsync = new LimitedAsync(runMultiThreaded); + } + +diff --git a/OSX/libsecurity_codesigning/lib/reqparser.cpp b/OSX/libsecurity_codesigning/lib/reqparser.cpp +index e3657a19..26b8838a 100644 +--- a/OSX/libsecurity_codesigning/lib/reqparser.cpp ++++ b/OSX/libsecurity_codesigning/lib/reqparser.cpp +@@ -51,6 +51,12 @@ ModuleNexus plugin; + // + PluginHost::PluginHost() + { ++#ifdef DARLING ++ // Darling links the genuine requirement parser into Security itself rather ++ // than shipping Apple's separate csparser.bundle. ++ antlr = findAntlrPlugin(); ++ CODESIGN_LOAD_ANTLR(); ++#else + if (CFBundleRef securityFramework = CFBundleGetBundleWithIdentifier(CFSTR("com.apple.security"))) + if (CFRef plugins = CFBundleCopyBuiltInPlugInsURL(securityFramework)) + if (CFRef pluginURL = makeCFURL("csparser.bundle", true, plugins)) { +@@ -64,6 +70,7 @@ PluginHost::PluginHost() + // can't load plugin - fail + Syslog::warning("code signing problem: unable to load csparser plug-in"); + MacOSError::throwMe(errSecCSInternalError); ++#endif + } + + +diff --git a/securityd/CMakeLists.txt b/securityd/CMakeLists.txt +index 32d90f4d..cec9f05d 100644 +--- a/securityd/CMakeLists.txt ++++ b/securityd/CMakeLists.txt +@@ -66,6 +66,11 @@ target_include_directories(securityd_exe PRIVATE + ) + target_compile_options(securityd_exe PRIVATE + -fobjc-arc ++ # Darling runs unprivileged, and the NDEBUG=1 set for the whole ++ # security subtree would make securityd refuse to start for any ++ # user. Undefining it selects Apple's own "unprivileged, some ++ # features may not work" path so trust evaluation can run. ++ -UNDEBUG + ) + make_fat(securityd_exe) + +diff --git a/trust/trustd/CMakeLists.txt b/trust/trustd/CMakeLists.txt +index c2a70718..2bccd840 100644 +--- a/trust/trustd/CMakeLists.txt ++++ b/trust/trustd/CMakeLists.txt +@@ -41,6 +41,14 @@ add_lib_ios_shim(libtrustd + LIBTRUSTD=1 + ) + ++# Apple's trustd sources are written for ARC (e.g. assign-by-default properties ++# would otherwise dangle). Without it the library's object ownership is wrong. ++target_compile_options(libtrustd PRIVATE ++ $<$:-fobjc-arc> ++) ++# these use Foundation/MobileAsset APIs Darling lacks, which ARC refuses to compile; they stay as they were ++set_source_files_properties(OTATrustUtilities.m SecRevocationNetworking.m trustdFileLocations.m PROPERTIES COMPILE_OPTIONS -fno-objc-arc) ++ + add_darling_executable(trustd + ../../OSX/sec/ipc/server_entitlement_helpers.c + trustd.c +@@ -79,3 +87,27 @@ target_link_libraries(trustd + install(TARGETS trustd DESTINATION libexec/darling/usr/libexec) + install(FILES macOS/com.apple.trustd.plist DESTINATION libexec/darling/System/Library/LaunchDaemons) + install(FILES macOS/com.apple.trustd.agent.plist DESTINATION libexec/darling/System/Library/LaunchAgents) ++ ++add_darling_executable(trustdFileHelper ++ trustdFileHelper/main.m ++ trustdFileHelper/trustdFileHelper.m ++) ++target_compile_options(trustdFileHelper PRIVATE ++ -fobjc-arc ++ -DDARLING ++) ++target_compile_definitions(trustdFileHelper PRIVATE ++ LIBTRUSTD ++ SECITEM_SHIM_OSX=1 ++ SEC_IOS_ON_OSX=1 ++) ++target_link_libraries(trustdFileHelper ++ libtrustd ++ utilities ++ Security ++ CoreFoundation ++ Foundation ++ objc ++ system ++) ++install(TARGETS trustdFileHelper DESTINATION libexec/darling/usr/libexec) +diff --git a/trust/trustd/SecRevocationNetworking.m b/trust/trustd/SecRevocationNetworking.m +index 0f61efd0..1d0c6364 100644 +--- a/trust/trustd/SecRevocationNetworking.m ++++ b/trust/trustd/SecRevocationNetworking.m +@@ -358,6 +358,11 @@ static ValidUpdateRequest *request = nil; + } + + - (BOOL) scheduleUpdateFromServer:(NSString *)server forVersion:(NSUInteger)version withQueue:(dispatch_queue_t)updateQueue { ++ if (!TrustURLSessionsAvailable()) { ++ // no URL session support on this platform; keep the built-in database ++ secnotice("validupdate", "no URL session support, skipping update"); ++ return NO; ++ } + if (!server) { + secnotice("validupdate", "invalid update request"); + return NO; +diff --git a/trust/trustd/TrustURLSessionCache.h b/trust/trustd/TrustURLSessionCache.h +index 988d6eb3..9d151ea1 100644 +--- a/trust/trustd/TrustURLSessionCache.h ++++ b/trust/trustd/TrustURLSessionCache.h +@@ -30,6 +30,10 @@ + + extern const NSString *TrustdUserAgent; + ++// darling-lol: whether this platform's Foundation can create the URL sessions ++// trustd's network fetches (OCSP, CRL, CA issuers, database updates) need. ++bool TrustURLSessionsAvailable(void); ++ + @interface TrustURLSessionCache : NSObject + - (instancetype)initWithDelegate:(TrustURLSessionDelegate *)delegate; + - (NSURLSession *)sessionForAuditToken:(NSData *)auditToken; +diff --git a/trust/trustd/TrustURLSessionCache.m b/trust/trustd/TrustURLSessionCache.m +index b4dd9ba7..d4b9aeb4 100644 +--- a/trust/trustd/TrustURLSessionCache.m ++++ b/trust/trustd/TrustURLSessionCache.m +@@ -44,6 +44,15 @@ static NSTimeInterval TrustURLSessionGetResourceTimeout(void) { + @property NSMutableArray * _clientLRUList; + @end + ++bool TrustURLSessionsAvailable(void) ++{ ++ // Darling's Foundation has no working NSURLSession yet: the configuration ++ // class cannot even be messaged from trustd (ephemeral sessions are ++ // unrecognized, and class introspection aborts in the runtime). Report ++ // network fetches as unavailable until that exists. ++ return false; ++} ++ + @implementation TrustURLSessionCache + + - (instancetype)initWithDelegate:(TrustURLSessionDelegate *)delegate +@@ -58,6 +67,11 @@ static NSTimeInterval TrustURLSessionGetResourceTimeout(void) { + + - (NSURLSession *)createSessionForAuditToken:(NSData *)auditToken + { ++ if (!TrustURLSessionsAvailable()) { ++ // network fetches (OCSP, CRL, CA issuers) are unavailable; callers ++ // treat a nil session like an unreachable server ++ return nil; ++ } + NSURLSessionConfiguration *config = [NSURLSessionConfiguration ephemeralSessionConfiguration]; + config.timeoutIntervalForResource = TrustURLSessionGetResourceTimeout(); + config.HTTPAdditionalHeaders = @{@"User-Agent" : TrustdUserAgent}; +@@ -82,6 +96,9 @@ static NSTimeInterval TrustURLSessionGetResourceTimeout(void) { + } + /* Cache miss: create new session */ + result = [self createSessionForAuditToken:auditToken]; ++ if (result == nil) { ++ return nil; ++ } + [self._clientLRUList insertObject:auditToken atIndex:0]; + [self._clientSessionMap setObject:result forKey:auditToken]; + secdebug("http", "creating session for %@", auditToken); +diff --git a/trust/trustd/TrustURLSessionDelegate.m b/trust/trustd/TrustURLSessionDelegate.m +index 06c5d4aa..9f24ea91 100644 +--- a/trust/trustd/TrustURLSessionDelegate.m ++++ b/trust/trustd/TrustURLSessionDelegate.m +@@ -283,6 +283,10 @@ NSString *kSecTrustRequestHeaderUUID = @"X-Apple-Request-UUID"; + } + + - (BOOL)fetchNext:(NSURLSession *)session context:(TrustURLSessionContext *)context { ++ if (session == nil) { ++ secnotice("http", "no URL session available, skipping network fetch"); ++ return true; ++ } + if (context.numTasks >= MAX_TASKS) { + secnotice("http", "Too many fetch %@ requests for this cert", [self class]); + return true; +diff --git a/trust/trustd/trustdFileHelper/main.m b/trust/trustd/trustdFileHelper/main.m +index f6d166e2..fb7ae819 100644 +--- a/trust/trustd/trustdFileHelper/main.m ++++ b/trust/trustd/trustdFileHelper/main.m +@@ -12,10 +12,13 @@ + + #import + #import ++#if !defined(__DARLING__) && !defined(DARLING) + #import ++#endif + + #import + #include ++#include + #include + #include + diff --git a/patches/xnu.patch b/patches/xnu.patch new file mode 100644 index 0000000..4fbad8b --- /dev/null +++ b/patches/xnu.patch @@ -0,0 +1,4913 @@ +diff --git a/darling/src/libsystem_kernel/CMakeLists.txt b/darling/src/libsystem_kernel/CMakeLists.txt +index e14e7ea..024981b 100644 +--- a/darling/src/libsystem_kernel/CMakeLists.txt ++++ b/darling/src/libsystem_kernel/CMakeLists.txt +@@ -40,7 +40,11 @@ add_circular(system_kernel FAT + $,$,> + $ + $ +- #$ ++ # darling-lol: link kqueue emulation (see libkqueue/CMakeLists.txt). ++ # Without this, kqueue()/kevent() are ENOSYS weak stubs and all ++ # dispatch-source / FilePathWatcher / socket-server event delivery ++ # silently never fires. ++ $ + $ + SIBLINGS + system_c +@@ -48,6 +52,12 @@ add_circular(system_kernel FAT + system_dyld + DEPENDENCIES + libsimple_darling ++ # darling-lol: kqueue emulation objects (see OBJECTS above) use ++ # calloc/pthread, which live in upper layers. UPWARD defers binding ++ # to runtime instead of failing the link (or creating a cycle). ++ UPWARD ++ system_malloc ++ system_pthread + ) + #target_link_libraries(system_kernel system_duct platform_static32 platform_static64) + make_fat(system_kernel) +diff --git a/darling/src/libsystem_kernel/emulation/CMakeLists.txt b/darling/src/libsystem_kernel/emulation/CMakeLists.txt +index 5b2e765..64d35f5 100644 +--- a/darling/src/libsystem_kernel/emulation/CMakeLists.txt ++++ b/darling/src/libsystem_kernel/emulation/CMakeLists.txt +@@ -6,13 +6,12 @@ enable_language(ASM) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=gnu99 -fvisibility=hidden -fPIC -fno-builtin -ggdb -Wno-int-conversion -Wno-compare-distinct-pointer-types") + set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -nostdlib") + +-add_definitions(-DBSDTHREAD_WRAP_LINUX_PTHREAD +- -DEMULATED_SYSNAME="Darwin" +- -DEMULATED_RELEASE="20.6.0" +- -DEMULATED_VERSION="Darwin Kernel Version 20.6.0" +- -DEMULATED_OSVERSION="20G1120" +- -DEMULATED_OSPRODUCTVERSION="11.7.4" +-) ++add_definitions(-DBSDTHREAD_WRAP_LINUX_PTHREAD) ++ ++# Quoted values are kept off the directory-wide definitions: those are pasted unescaped ++# into the mig command lines, which breaks the shell quoting. ++set_source_files_properties(src/xnu_syscall/bsd/helper/misc/sysctl_kern.c PROPERTIES COMPILE_DEFINITIONS ++ "EMULATED_SYSNAME=\"Darwin\";EMULATED_RELEASE=\"23.6.0\";EMULATED_VERSION=\"Darwin Kernel Version 23.6.0: Mon Jul 29 21:14:30 PDT 2024\\; root:xnu-10063.141.2~1/RELEASE_X86_64\";EMULATED_OSVERSION=\"23H124\";EMULATED_OSPRODUCTVERSION=\"14.7\"") + + # include src/startup for rtsig.h + include_directories( +@@ -134,6 +133,7 @@ set(xnu_syscall_bsd_sources + src/xnu_syscall/bsd/impl/guarded/guarded_close_np.c + src/xnu_syscall/bsd/impl/guarded/guarded_kqueue_np.c + src/xnu_syscall/bsd/impl/guarded/guarded_open_np.c ++ src/xnu_syscall/bsd/impl/guarded/change_fdguard_np.c + src/xnu_syscall/bsd/impl/hfs/stub.c + src/xnu_syscall/bsd/impl/ioctl/ioctl.c + src/xnu_syscall/bsd/impl/kqueue/kevent.c +@@ -153,6 +153,7 @@ set(xnu_syscall_bsd_sources + src/xnu_syscall/bsd/impl/misc/getrlimit.c + src/xnu_syscall/bsd/impl/misc/getrusage.c + src/xnu_syscall/bsd/impl/misc/iopolicysys.c ++ src/xnu_syscall/bsd/impl/misc/mac_syscall.c + src/xnu_syscall/bsd/impl/misc/proc_info.c + src/xnu_syscall/bsd/impl/misc/ptrace.c + src/xnu_syscall/bsd/impl/misc/reboot.c +diff --git a/darling/src/libsystem_kernel/emulation/include/common/base.h b/darling/src/libsystem_kernel/emulation/include/common/base.h +index 207dae6..f5e96db 100644 +--- a/darling/src/libsystem_kernel/emulation/include/common/base.h ++++ b/darling/src/libsystem_kernel/emulation/include/common/base.h +@@ -11,13 +11,13 @@ + #define CPP_EXTERN_END + #endif + +-#define LINUX_SYSCALL0(nr) linux_syscall(0, 0, 0, 0, 0, 0, nr) +-#define LINUX_SYSCALL1(nr, a1) linux_syscall((long)a1, 0, 0, 0, 0, 0, nr) +-#define LINUX_SYSCALL2(nr, a1, a2) linux_syscall((long)a1, (long)a2, 0, 0, 0, 0, nr) +-#define LINUX_SYSCALL3(nr, a1, a2, a3) linux_syscall((long)a1, (long)a2, (long)a3, 0, 0, 0, nr) +-#define LINUX_SYSCALL4(nr, a1, a2, a3, a4) linux_syscall((long)a1, (long)a2, (long)a3, (long)a4, 0, 0, nr) +-#define LINUX_SYSCALL5(nr, a1, a2, a3, a4, a5) linux_syscall((long)a1, (long)a2, (long)a3, (long)a4, (long)a5, 0, nr) +-#define LINUX_SYSCALL6(nr, a1, a2, a3, a4, a5, a6) linux_syscall((long)a1, (long)a2, (long)a3, (long)a4, (long)a5, (long)a6, nr) ++#define LINUX_SYSCALL0(nr) __lolsys_syscall(0, 0, 0, 0, 0, 0, nr) ++#define LINUX_SYSCALL1(nr, a1) __lolsys_syscall((long)a1, 0, 0, 0, 0, 0, nr) ++#define LINUX_SYSCALL2(nr, a1, a2) __lolsys_syscall((long)a1, (long)a2, 0, 0, 0, 0, nr) ++#define LINUX_SYSCALL3(nr, a1, a2, a3) __lolsys_syscall((long)a1, (long)a2, (long)a3, 0, 0, 0, nr) ++#define LINUX_SYSCALL4(nr, a1, a2, a3, a4) __lolsys_syscall((long)a1, (long)a2, (long)a3, (long)a4, 0, 0, nr) ++#define LINUX_SYSCALL5(nr, a1, a2, a3, a4, a5) __lolsys_syscall((long)a1, (long)a2, (long)a3, (long)a4, (long)a5, 0, nr) ++#define LINUX_SYSCALL6(nr, a1, a2, a3, a4, a5, a6) __lolsys_syscall((long)a1, (long)a2, (long)a3, (long)a4, (long)a5, (long)a6, nr) + + #define __SYSCALL_NARGS_X(a,b,c,d,e,f,g,h,n,...) n + #define __SYSCALL_NARGS(...) __SYSCALL_NARGS_X(__VA_ARGS__,7,6,5,4,3,2,1,0) +@@ -27,6 +27,9 @@ + + long linux_syscall(long a1, long a2, long a3, long a4, long a5, long a6, int nr); + ++/* darling-lol M58x: traced wrapper the LINUX_SYSCALL* macros route through */ ++long __lolsys_syscall(long a1, long a2, long a3, long a4, long a5, long a6, int nr); ++ + #ifndef BUILDING_BASE_C + + VISIBLE +diff --git a/darling/src/libsystem_kernel/emulation/include/conversion/xattr/name.h b/darling/src/libsystem_kernel/emulation/include/conversion/xattr/name.h +new file mode 100644 +index 0000000..976e5d0 +--- /dev/null ++++ b/darling/src/libsystem_kernel/emulation/include/conversion/xattr/name.h +@@ -0,0 +1,69 @@ ++#ifndef DARLING_CONVERSION_XATTR_NAME ++#define DARLING_CONVERSION_XATTR_NAME ++ ++#include ++ ++/* ++ * darling-lol: macOS xattr names have no namespace prefix ++ * (e.g. "com.apple.quarantine", "org.chromium.crashpad.database.initialized"). ++ * Linux requires the "user." namespace for names an unprivileged process may ++ * set/get, and returns ENOTSUP (95) otherwise. Darwin's own xattr layer is ++ * namespace-less, so translate on the way in/out. ++ * ++ * Rules: ++ * - "" (Darwin: list/remove all) is passed through unchanged. ++ * - names already carrying a Linux namespace ("user.", "trusted.", ++ * "system.", "security.") are passed through (avoid double-prefixing). ++ * - everything else gets "user." prepended. ++ * ++ * OUT must have room for 5 extra bytes + NUL. Returns `name` or `out`. ++ */ ++static inline const char* xattr_name_to_linux(const char* name, char* out, unsigned long outsz) ++{ ++ unsigned long i = 0; ++ ++ if (name == 0 || name[0] == '\0') ++ return name; ++ ++ if (__builtin_strncmp(name, "user.", 5) == 0 || ++ __builtin_strncmp(name, "trusted.", 8) == 0 || ++ __builtin_strncmp(name, "system.", 7) == 0 || ++ __builtin_strncmp(name, "security.", 9) == 0) ++ return name; ++ ++ if (out == 0 || outsz < 6) ++ return name; ++ ++ out[0] = 'u'; out[1] = 's'; out[2] = 'e'; out[3] = 'r'; out[4] = '.'; ++ for (i = 0; name[i] != '\0' && i + 5 < outsz - 1; i++) ++ out[i + 5] = name[i]; ++ out[i + 5] = '\0'; ++ return out; ++} ++ ++/* ++ * Reverse: strip a leading "user." when returning names to Darwin callers ++ * (listxattr). Mutates `name` in place (the buffer is caller-owned and the ++ * caller only needs a valid NUL-terminated name at the same address). ++ * Returns the (possibly advanced) pointer for convenience. ++ */ ++static inline char* xattr_name_from_linux(char* name) ++{ ++ if (name != 0 && __builtin_strncmp(name, "user.", 5) == 0) ++ return name + 5; ++ return name; ++} ++ ++/* ++ * Linux reports a missing attribute as ENODATA; Darwin reports ENOATTR (93), ++ * which callers test for to tell "no such attribute" from a real failure. ++ * LINUX_ERRNO is the negative value returned by the Linux xattr syscall. ++ */ ++static inline long xattr_errno_to_bsd(long linux_errno) ++{ ++ if (linux_errno == -61 /* LINUX_ENODATA */) ++ return -93 /* ENOATTR */; ++ return errno_linux_to_bsd(linux_errno); ++} ++ ++#endif /* DARLING_CONVERSION_XATTR_NAME */ +diff --git a/darling/src/libsystem_kernel/emulation/include/linux_premigration/resources/dserver-rpc-defs.h b/darling/src/libsystem_kernel/emulation/include/linux_premigration/resources/dserver-rpc-defs.h +index f6588cc..c60d8f9 100644 +--- a/darling/src/libsystem_kernel/emulation/include/linux_premigration/resources/dserver-rpc-defs.h ++++ b/darling/src/libsystem_kernel/emulation/include/linux_premigration/resources/dserver-rpc-defs.h +@@ -12,6 +12,7 @@ + #include + #include + #include ++#include + #include + #include + +@@ -75,7 +76,67 @@ struct linux_sockaddr_un { + + #define dserver_rpc_hooks_memcpy memcpy + ++#include ++ ++/* darling-lol diagnostic (/tmp/metal-debug-timing): time main-thread RPCs to darlingserver */ ++static int slowrpc_pending_call = -1; ++static unsigned long long slowrpc_start = 0; ++ ++/* darling-lol diagnostic (host file /tmp/rclol-rpc-trace): every 16th darlingserver request from a process's main thread is ++ * appended to /tmp/rclol-rpc-trace.log with its call number and up to 10 caller addresses (frame-pointer walk). Offline, ++ * the addresses are resolved with /proc//maps to attribute the ~40 round trips per frame to code. */ ++static int rclol_rpc_trace_state = -1; ++static unsigned long rclol_rpc_trace_count = 0; ++ ++static void rclol_rpc_trace_sample(const dserver_rpc_hooks_msghdr_t* message, void** frame) { ++ char line[400]; ++ int n, call; ++ if (rclol_rpc_trace_state < 0) ++ rclol_rpc_trace_state = LINUX_SYSCALL(__NR_access, "/tmp/rclol-rpc-trace", 0) == 0; ++ if (!rclol_rpc_trace_state || message == 0 || message->msg_iov == 0 || message->msg_iovlen < 1 || message->msg_iov->iov_base == 0 || ++ message->msg_iov->iov_len < sizeof(int)) ++ return; ++ if (LINUX_SYSCALL(__NR_gettid) != LINUX_SYSCALL(__NR_getpid)) ++ return; ++ if ((__atomic_add_fetch(&rclol_rpc_trace_count, 1, __ATOMIC_RELAXED) & 15) != 0) ++ return; ++ call = *(const int*) message->msg_iov->iov_base; ++ n = __simple_sprintf(line, "rpc pid=%d call=%d frames:", (int) LINUX_SYSCALL(__NR_getpid), call & 0xffff); ++ for (int i = 0; i < 10 && frame && ((unsigned long) frame & 7) == 0; ++i) { ++ void** next = (void**) frame[0]; ++ n += __simple_sprintf(line + n, " %lx", (unsigned long) frame[1]); ++ if (next <= frame || (unsigned long) next - (unsigned long) frame > 0x100000) ++ break; ++ frame = next; ++ } ++ line[n++] = '\n'; ++ line[n] = 0; ++ long fd = LINUX_SYSCALL(__NR_open, "/tmp/rclol-rpc-trace.log", 02101, 0666); ++ if (fd >= 0) { ++ LINUX_SYSCALL(__NR_write, fd, line, n); ++ LINUX_SYSCALL(__NR_close, fd); ++ } ++} ++ + static long int dserver_rpc_hooks_send_message(int socket, const dserver_rpc_hooks_msghdr_t* message) { ++ rclol_rpc_trace_sample(message, (void**) __builtin_frame_address(0)); ++ if (slowwait_enabled() && LINUX_SYSCALL(__NR_gettid) == LINUX_SYSCALL(__NR_getpid) && ++ message != 0 && message->msg_iov != 0 && message->msg_iovlen > 0 && message->msg_iov->iov_base != 0 && message->msg_iov->iov_len >= sizeof(int)) { ++ slowrpc_pending_call = *(const int*) message->msg_iov->iov_base; ++ slowrpc_start = slowwait_now(); ++ } ++#if 0 /* darling-lol M58F145: disabled (was a hot per-RPC stdout flood that ++ * slowed the Riot launcher's update into its 30 s no-progress timeout) */ ++ if (message != 0 && message->msg_iov != 0 && message->msg_iovlen > 0 && ++ message->msg_iov->iov_base != 0 && message->msg_iov->iov_len >= sizeof(int)) { ++ static unsigned long m58ar_sn = 0; ++ if (m58ar_sn++ < 1000000) { ++ int callnum = *(const int*) message->msg_iov->iov_base; ++ __simple_printf("M58AR send tid=%d sock=%d call=%d\n", ++ (int) dserver_rpc_hooks_get_tid(), socket, callnum); ++ } ++ } ++#endif + #ifdef __NR_socketcall + return LINUX_SYSCALL(__NR_socketcall, LINUX_SYS_SENDMSG, ((long[6]) { socket, message, 0 })); + #else +@@ -83,19 +144,108 @@ static long int dserver_rpc_hooks_send_message(int socket, const dserver_rpc_hoo + #endif + }; + ++/* darling-lol: wait for the reply by polling for a short time before sleeping (main thread only). ++ * ++ * Most requests are answered within tens of microseconds, but the caller went to sleep in recvmsg at once, so every ++ * round trip also paid for descheduling the thread, idling the core and waking it again. The game's main thread makes ++ * ~40-50 round trips per frame (mutex and condition-variable calls, run-loop port operations, timers). Polling the ++ * socket for a bounded time catches the quick replies without that sleep; a reply that takes longer (a real blocking ++ * wait) falls back to the normal blocking receive. ++ * Host file /tmp/rclol-rpc-spin-us holds the polling budget in microseconds (0 or no file = off), re-read every 4096 ++ * main-thread receives so both settings can be compared inside one match. */ ++static int rclol_spin_main_socket = -2; /* -2: main thread not identified yet */ ++static unsigned long rclol_spin_ticks = 0; /* TSC ticks to poll for; 0 = off */ ++static unsigned long rclol_spin_calls = 0; ++static unsigned long rclol_spin_hits = 0, rclol_spin_misses = 0; ++ ++static void rclol_spin_refresh(void) { ++ char digits[16]; ++ long value = 0; ++ long fd = LINUX_SYSCALL(__NR_open, "/tmp/rclol-rpc-spin-us", 0, 0); ++ if (fd >= 0) { ++ long n = LINUX_SYSCALL(__NR_read, fd, digits, sizeof(digits) - 1); ++ LINUX_SYSCALL(__NR_close, fd); ++ for (long i = 0; i < n && digits[i] >= '0' && digits[i] <= '9'; ++i) value = value * 10 + (digits[i] - '0'); ++ if (value > 500) value = 500; ++ } ++ /* the TSC runs at a fixed rate near the nominal clock; ~2300 ticks per microsecond on this machine. A wrong ++ * guess only changes the budget, not the result. */ ++ rclol_spin_ticks = (unsigned long) value * 2300ul; ++ if (value > 0) { ++ /* counters for the A/B: how often the reply arrived while polling */ ++ char line[120]; ++ int len = __simple_sprintf(line, "spin_us=%ld hits=%lu misses=%lu\n", value, rclol_spin_hits, rclol_spin_misses); ++ long log = LINUX_SYSCALL(__NR_open, "/tmp/rclol-rpc-spin.log", 02101, 0666); ++ if (log >= 0) { LINUX_SYSCALL(__NR_write, log, line, len); LINUX_SYSCALL(__NR_close, log); } ++ } ++} ++ ++static inline unsigned long rclol_rdtsc(void) { ++ unsigned int lo, hi; ++ __asm__ volatile("rdtsc" : "=a"(lo), "=d"(hi)); ++ return ((unsigned long) hi << 32) | lo; ++} ++ + static long int dserver_rpc_hooks_receive_message(int socket, dserver_rpc_hooks_msghdr_t* out_message) { + long int ret = 0; + + retry: ++#if defined(__x86_64__) && !defined(__NR_socketcall) ++ if (rclol_spin_main_socket == -2) { ++ if (LINUX_SYSCALL(__NR_gettid) == LINUX_SYSCALL(__NR_getpid)) { ++ rclol_spin_main_socket = socket; ++ rclol_spin_refresh(); ++ } ++ } ++ if (socket == rclol_spin_main_socket) { ++ if ((++rclol_spin_calls & 4095) == 0) ++ rclol_spin_refresh(); ++ if (rclol_spin_ticks != 0) { ++ const unsigned long deadline = rclol_rdtsc() + rclol_spin_ticks; ++ for (;;) { ++ ret = LINUX_SYSCALL(__NR_recvmsg, socket, out_message, 0x40 /* MSG_DONTWAIT */); ++ if (ret != -11 /* EAGAIN */) { ++ ++rclol_spin_hits; ++ goto received; ++ } ++ if (rclol_rdtsc() >= deadline) ++ break; ++ for (int i = 0; i < 24; ++i) __asm__ volatile("pause"); ++ } ++ ++rclol_spin_misses; ++ } ++ } ++#endif + #ifdef __NR_socketcall + ret = LINUX_SYSCALL(__NR_socketcall, LINUX_SYS_RECVMSG, ((long[6]) { socket, out_message, 0 })); + #else + ret = LINUX_SYSCALL(__NR_recvmsg, socket, out_message, 0); ++#endif ++#if defined(__x86_64__) && !defined(__NR_socketcall) ++received: + #endif + if (ret < 0) { + return ret; + } + ++#if 0 /* darling-lol M58F145: disabled (hot per-RPC stdout flood; see above) */ ++ if (ret >= (long int) sizeof(int) && out_message->msg_iov != 0 && ++ out_message->msg_iov->iov_base != 0) { ++ static unsigned long m58ar_rn = 0; ++ if (m58ar_rn++ < 1000000) { ++ int m58ar_rcall = *(const int*) out_message->msg_iov->iov_base; ++ __simple_printf("M58AR recv tid=%d sock=%d call=%d\n", ++ (int) dserver_rpc_hooks_get_tid(), socket, m58ar_rcall); ++ } ++ } ++#endif ++ ++ if (slowrpc_pending_call >= 0 && ret >= (long int) sizeof(int) && out_message->msg_iov != 0 && out_message->msg_iov->iov_base != 0 && ++ *(const int*) out_message->msg_iov->iov_base != 0x52cca11 && LINUX_SYSCALL(__NR_gettid) == LINUX_SYSCALL(__NR_getpid)) { ++ slowwait_log("rpc", (unsigned int) slowrpc_pending_call, slowrpc_start, (void**) __builtin_frame_address(0), 0, 0); ++ slowrpc_pending_call = -1; ++ } ++ + if (ret >= sizeof(dserver_s2c_callhdr_t)) { + dserver_s2c_callhdr_t* callhdr = out_message->msg_iov->iov_base; + +@@ -280,16 +430,21 @@ __attribute__((format(printf, 1, 2))) + void __dserver_rpc_hooks_printf(const char* format, ...); + #define dserver_rpc_hooks_printf __dserver_rpc_hooks_printf + +-#define dserver_rpc_hooks_atomic_save_t sigset_t ++#define dserver_rpc_hooks_atomic_save_t linux_sigset_t + + static void dserver_rpc_hooks_atomic_begin(dserver_rpc_hooks_atomic_save_t* atomic_save) { +- // block standard unix signals (not real-time signals, though) +- sigset_t set = 0x7fffffff; +- sys_sigprocmask(SIG_BLOCK, &set, atomic_save); ++ /* Block standard signals, leaving real-time S2C signals available. The ++ * kernel ignores SIGKILL/SIGSTOP. Keep the original full native mask: ++ * converting it through a BSD mask loses unsupported real-time bits and ++ * needlessly scans/converts three masks on every RPC round trip. */ ++ linux_sigset_t set = 0x7fffffff; ++ LINUX_SYSCALL(__NR_rt_sigprocmask, 0 /* SIG_BLOCK */, &set, ++ atomic_save, sizeof(set)); + }; + + static void dserver_rpc_hooks_atomic_end(dserver_rpc_hooks_atomic_save_t* atomic_save) { +- sys_sigprocmask(SIG_SETMASK, atomic_save, NULL); ++ LINUX_SYSCALL(__NR_rt_sigprocmask, 2 /* SIG_SETMASK */, atomic_save, ++ NULL, sizeof(*atomic_save)); + }; + + #define dserver_rpc_hooks_get_interrupt_status() (-LINUX_EINTR) +diff --git a/darling/src/libsystem_kernel/emulation/include/xnu_syscall/bsd/impl/misc/mac_syscall.h b/darling/src/libsystem_kernel/emulation/include/xnu_syscall/bsd/impl/misc/mac_syscall.h +new file mode 100644 +index 0000000..b4c307a +--- /dev/null ++++ b/darling/src/libsystem_kernel/emulation/include/xnu_syscall/bsd/impl/misc/mac_syscall.h +@@ -0,0 +1,6 @@ ++#ifndef LINUX_MAC_SYSCALL_H ++#define LINUX_MAC_SYSCALL_H ++ ++long sys___mac_syscall(const char* policy, int call, void* arg); ++ ++#endif // LINUX_MAC_SYSCALL_H +diff --git a/darling/src/libsystem_kernel/emulation/include/xnu_syscall/bsd/impl/misc/ptrace.h b/darling/src/libsystem_kernel/emulation/include/xnu_syscall/bsd/impl/misc/ptrace.h +index 5d4beb5..9d5de46 100644 +--- a/darling/src/libsystem_kernel/emulation/include/xnu_syscall/bsd/impl/misc/ptrace.h ++++ b/darling/src/libsystem_kernel/emulation/include/xnu_syscall/bsd/impl/misc/ptrace.h +@@ -9,6 +9,7 @@ + #define XNU_PT_DETACH 11 + #define XNU_PT_SIGEXC 12 + #define XNU_PT_THUPDATE 13 ++#define XNU_PT_DENY_ATTACH 31 + + long sys_ptrace(int request, int pid, void* addr, int data); + +diff --git a/darling/src/libsystem_kernel/emulation/include/xnu_syscall/bsd/impl/psynch/ulock_wait.h b/darling/src/libsystem_kernel/emulation/include/xnu_syscall/bsd/impl/psynch/ulock_wait.h +index 90bc146..5bcea21 100644 +--- a/darling/src/libsystem_kernel/emulation/include/xnu_syscall/bsd/impl/psynch/ulock_wait.h ++++ b/darling/src/libsystem_kernel/emulation/include/xnu_syscall/bsd/impl/psynch/ulock_wait.h +@@ -5,6 +5,8 @@ + + long sys_ulock_wait(uint32_t operation, void* addr, uint64_t value, uint32_t timeout); + ++long sys_ulock_wait2(uint32_t operation, void* addr, uint64_t value, uint64_t timeout, uint64_t value2); ++ + #define XNU_UL_OPCODE_MASK 0xff + #define XNU_UL_COMPARE_AND_WAIT 1 + #define XNU_UL_UNFAIR_LOCK 2 +diff --git a/darling/src/libsystem_kernel/emulation/src/common/base.c b/darling/src/libsystem_kernel/emulation/src/common/base.c +index d077d04..3dcafea 100644 +--- a/darling/src/libsystem_kernel/emulation/src/common/base.c ++++ b/darling/src/libsystem_kernel/emulation/src/common/base.c +@@ -23,6 +23,284 @@ int __linux_syscall(int nr, long a1, long a2, long a3, long a4, long a5, long a6 + return linux_syscall(a1, a2, a3, a4, a5, a6, nr); + } + ++/* darling-lol M58x: bounded per-thread trace of the host syscalls that can ++ * block. Every guest host-syscall goes through the LINUX_SYSCALL* macros, ++ * which now route here. We print the syscall number, its first argument ++ * (usually the fd) and the host tid for the first few calls of each thread ++ * and then sparsely, so a stalled run names the syscall each parked thread ++ * is in without flooding the log. */ ++#define LOLSYS_SLOTS 256 ++ ++struct lolsys_slot { ++ int tid; ++ unsigned int count; ++}; ++ ++static struct lolsys_slot lolsys_slots[LOLSYS_SLOTS]; ++ ++static int lolsys_traced(int nr) ++{ ++ switch (nr) { ++#ifdef __NR_read ++ case __NR_read: return 1; ++#endif ++#ifdef __NR_recvmsg ++ case __NR_recvmsg: return 1; ++#endif ++#ifdef __NR_recvfrom ++ case __NR_recvfrom: return 1; ++#endif ++#ifdef __NR_poll ++ case __NR_poll: return 1; ++#endif ++#ifdef __NR_select ++ case __NR_select: return 1; ++#endif ++#ifdef __NR_accept ++ case __NR_accept: return 1; ++#endif ++#ifdef __NR_connect ++ case __NR_connect: return 1; ++#endif ++#ifdef __NR_nanosleep ++ case __NR_nanosleep: return 1; ++#endif ++ /* darling-lol M58F73: widen the trace for the Riot launcher's early ++ * startup. The launcher dies with exit 252 before writing any log, ++ * after open()ing /proc//mounts; these calls name the failing step ++ * (filesystem/space probes, sysctl, anti-debug). */ ++#ifdef __NR_statfs ++ case __NR_statfs: return 1; ++#endif ++#ifdef __NR_fstatfs ++ case __NR_fstatfs: return 1; ++#endif ++#ifdef __NR_statvfs ++ case __NR_statvfs: return 1; ++#endif ++#ifdef __NR_fstatvfs ++ case __NR_fstatvfs: return 1; ++#endif ++#ifdef __NR_open ++ case __NR_open: return 1; ++#endif ++#ifdef __NR_openat ++ case __NR_openat: return 1; ++#endif ++#ifdef __NR__sysctl ++ case __NR__sysctl: return 1; ++#endif ++#ifdef __NR_sysctl ++ case __NR_sysctl: return 1; ++#endif ++#ifdef __NR_ptrace ++ case __NR_ptrace: return 1; ++#endif ++#ifdef __NR_access ++ case __NR_access: return 1; ++#endif ++#ifdef __NR_newfstatat ++ case __NR_newfstatat: return 1; ++#endif ++#ifdef __NR_fstat ++ case __NR_fstat: return 1; ++#endif ++#ifdef __NR_readlink ++ case __NR_readlink: return 1; ++#endif ++#ifdef __NR_exit ++ case __NR_exit: return 1; ++#endif ++#ifdef __NR_exit_group ++ case __NR_exit_group: return 1; ++#endif ++ default: ++ return 0; ++ } ++} ++ ++/* darling-lol M58F76: for the path-bearing syscalls, return the pointer to the ++ * guest C string so the LOLSC line names the file/directory the launcher ++ * touches right before it exits. */ ++static const char* __attribute__((unused)) lolsys_path(int nr, long a1, long a2) ++{ ++ switch (nr) { ++#ifdef __NR_open ++ case __NR_open: return (const char*) a1; ++#endif ++#ifdef __NR_openat ++ case __NR_openat: return (const char*) a2; /* (dirfd, path, flags, mode) */ ++#endif ++#ifdef __NR_readlink ++ case __NR_readlink: return (const char*) a1; ++#endif ++#ifdef __NR_statfs ++ case __NR_statfs: return (const char*) a1; ++#endif ++#ifdef __NR_access ++ case __NR_access: return (const char*) a1; ++#endif ++#ifdef __NR_newfstatat ++ case __NR_newfstatat: return (const char*) a2; /* (dirfd, path, stat, flags) */ ++#endif ++ default: return 0; ++ } ++} ++ ++/* darling-lol M58F121: microsecond timestamp (gettimeofday) shared with the ++ * dserver M58RD HANDOFF/QUEUED log clock, for wake-to-run correlation. */ ++static long __attribute__((unused)) lolsrv_us(void) ++{ ++ struct { long tv_sec; long tv_usec; } tv; ++ tv.tv_sec = 0; ++ tv.tv_usec = 0; ++ linux_syscall((long) &tv, 0, 0, 0, 0, 0, __NR_gettimeofday); ++ return tv.tv_sec * 1000000L + tv.tv_usec; ++} ++ ++/* darling-lol M58F159: opt-in long-wait trace flag. Enabled from outside (the ++ * injected shim calls __lolsys_set_wake_trace(1)); the syscall wrapper itself ++ * must not call libc/getenv (M58F158: that wedged the container). */ ++static int lolsys_wake_on = 0; ++ ++VISIBLE ++void __lolsys_set_wake_trace(int on) ++{ ++ lolsys_wake_on = on; ++} ++ ++VISIBLE ++long __lolsys_syscall(long a1, long a2, long a3, long a4, long a5, long a6, int nr) ++{ ++ /* Disabled diagnostics must not add another host syscall or update shared ++ * counters on every receive/wait. Keep the opt-in trace path below. */ ++ if (!lolsys_wake_on) ++ return linux_syscall(a1, a2, a3, a4, a5, a6, nr); ++ long ret = 0; ++ /* darling-lol M58F166: flag-gated recvmsg rate/errno counter (no libc), to ++ * confirm the busy recvmsg loop independently of the SIGPROF profiler. */ ++#ifdef __NR_recvmsg ++ if (lolsys_wake_on && nr == __NR_recvmsg) { ++ static unsigned long rn = 0, rok = 0, reintr = 0, reagain = 0, rother = 0, rlines = 0; ++ long rr = linux_syscall(a1, a2, a3, a4, a5, a6, nr); ++ rn++; ++ if (rr >= 0) rok++; ++ else if (rr == -4) reintr++; ++ else if (rr == -11) reagain++; ++ else rother++; ++ if ((rn & 4095) == 0 && rlines < 500) { ++ int rpid = (int) linux_syscall(0, 0, 0, 0, 0, 0, __NR_getpid); ++ int rtid = (int) linux_syscall(0, 0, 0, 0, 0, 0, __NR_gettid); ++ rlines++; ++ __simple_printf("LOLRCV pid=%d tid=%d fd=%ld n=%lu ok=%lu eintr=%lu eagain=%lu other=%lu\n", ++ rpid, rtid, a1, rn, rok, reintr, reagain, rother); ++ } ++ return rr; ++ } ++#endif ++ /* darling-lol M58F155: opt-in long-wait trace (RCLOL_WAKE=1). For the ++ * blocking waits that make up the browser main thread's runloop, log only ++ * waits longer than ~200 ms (low volume), so an intermittent multi-second ++ * loop stall names itself. Capped. */ ++#ifdef __NR_recvmsg ++ if (lolsys_wake_on && (nr == __NR_recvmsg ++#ifdef __NR_poll ++ || nr == __NR_poll ++#endif ++#ifdef __NR_epoll_wait ++ || nr == __NR_epoll_wait ++#endif ++ )) { ++ static unsigned long lw_cap = 0; ++ int lw_tid = (int) linux_syscall(0, 0, 0, 0, 0, 0, __NR_gettid); ++ long lw_t0 = lolsrv_us(); ++ long lw_ret = linux_syscall(a1, a2, a3, a4, a5, a6, nr); ++ long lw_dt = lolsrv_us() - lw_t0; ++ if (lw_dt > 200000 && lw_cap++ < 5000) { ++ int lw_pid = (int) linux_syscall(0, 0, 0, 0, 0, 0, __NR_getpid); ++ __simple_printf("LOLWAKE pid=%d tid=%d nr=%d fd=%ld dt_ms=%ld ret=%ld\n", ++ lw_pid, lw_tid, nr, a1, lw_dt / 1000, lw_ret); ++ } ++ return lw_ret; ++ } ++#endif ++ /* M58F121: timestamp slot-port mach receives (fd >= 524000) to measure the ++ * guest-side wake-to-run; correlate with dserver M58RD HANDOFF on the same ++ * port. Bounded per thread. */ ++#if 0 /* darling-lol M58F145: disabled (hot mach-recv stdout flood; see M58F140/141) */ ++#ifdef __NR_recvmsg ++ if (nr == __NR_recvmsg && a1 >= 524000) { ++ int tid2 = (int) linux_syscall(0, 0, 0, 0, 0, 0, __NR_gettid); ++ struct lolsys_slot* s2 = &lolsys_slots[((unsigned int) tid2) & (LOLSYS_SLOTS - 1)]; ++ if (s2->tid != tid2) { ++ s2->tid = tid2; ++ s2->count = 0; ++ } ++ ret = linux_syscall(a1, a2, a3, a4, a5, a6, nr); ++ if (s2->count < 512) { ++ int pid2 = (int) linux_syscall(0, 0, 0, 0, 0, 0, __NR_getpid); ++ long t = lolsrv_us(); ++ __simple_printf("LOLSRV pid=%d tid=%d fd=%ld sec=%ld usec=%ld ret=%ld\n", ++ pid2, tid2, a1, t / 1000000L, t % 1000000L, ret); ++ } ++ s2->count++; ++ return ret; ++ } ++#endif ++#endif ++ if (lolsys_traced(nr)) { ++ int tid = (int) linux_syscall(0, 0, 0, 0, 0, 0, __NR_gettid); ++ struct lolsys_slot* s = &lolsys_slots[((unsigned int) tid) & (LOLSYS_SLOTS - 1)]; ++ if (s->tid != tid) { ++ s->tid = tid; ++ s->count = 0; ++ } ++ int log_it = (s->count < 3) || ((s->count & 255) == 0); ++ /* M58F73: the "naming" calls (fs probes / open / sysctl / ptrace) are ++ * logged every time so the launcher's exact early sequence is visible; ++ * exit/exit_group are logged so the exact exit code is captured. */ ++ switch (nr) { ++#ifdef __NR_statfs ++ case __NR_statfs: log_it = 1; break; ++#endif ++#ifdef __NR_fstatfs ++ case __NR_fstatfs: log_it = 1; break; ++#endif ++#ifdef __NR_open ++ case __NR_open: log_it = 1; break; ++#endif ++#ifdef __NR_openat ++ case __NR_openat: log_it = 1; break; ++#endif ++#ifdef __NR__sysctl ++ case __NR__sysctl: log_it = 1; break; ++#endif ++#ifdef __NR_ptrace ++ case __NR_ptrace: log_it = 1; break; ++#endif ++#ifdef __NR_newfstatat ++ case __NR_newfstatat: log_it = 1; break; ++#endif ++#ifdef __NR_exit ++ case __NR_exit: log_it = 1; break; ++#endif ++#ifdef __NR_exit_group ++ case __NR_exit_group: log_it = 1; break; ++#endif ++ default: break; ++ } ++ if (log_it) { ++ /* darling-lol M58F145: LOLSC stdout logging disabled (hot; it ++ * flooded the launcher's stdout). Keep the syscall + counter. */ ++ ret = linux_syscall(a1, a2, a3, a4, a5, a6, nr); ++ s->count++; ++ return ret; ++ } ++ s->count++; ++ } ++ return linux_syscall(a1, a2, a3, a4, a5, a6, nr); ++} ++ + #ifdef __TESTING + void _start() + { +diff --git a/darling/src/libsystem_kernel/emulation/src/linux_premigration/ext/clock_gettime.c b/darling/src/libsystem_kernel/emulation/src/linux_premigration/ext/clock_gettime.c +index f27a888..6945458 100644 +--- a/darling/src/libsystem_kernel/emulation/src/linux_premigration/ext/clock_gettime.c ++++ b/darling/src/libsystem_kernel/emulation/src/linux_premigration/ext/clock_gettime.c +@@ -5,11 +5,233 @@ + + extern long cerror(int __err); + ++#if defined(__x86_64__) ++/* darling-lol: user-space CLOCK_MONOTONIC / CLOCK_REALTIME. ++ * ++ * Every mach_absolute_time and gettimeofday of a Darwin process lands here as a real Linux syscall. On hosts whose ++ * kernel clocksource is HPET (the TSCs of the cores are not synchronised, so the kernel refuses the TSC and the vDSO ++ * falls back to the syscall as well) one read costs ~1.4 us, and the League main thread makes ~200 of them per frame. ++ * ++ * Each core's TSC still counts at one constant rate; only the offsets between cores differ. RDTSCP returns the TSC and ++ * the CPU it was read on in one instruction, so the clock is kept per CPU: a (TSC, monotonic ns) pair sampled with a ++ * real syscall, extrapolated for at most FC_WINDOW_NS and then sampled again. The tick rate is measured between such ++ * samples of one CPU. Results never go backwards (the sampling error between two cores is about a microsecond). ++ * OPT-IN: only with the host file /tmp/rclol-fast-clock (read once per process), on CPUs with an invariant TSC, for ++ * these two clock ids. Measured 2026-10-10 in League with it on: darlingserver 113-120% CPU with continuous ++ * mach_vm_region_recurse traffic against 38-57% with it off, and slower fights. With a TSC kernel clocksource a real ++ * read is ~0.35 us, so little is lost by leaving it off. */ ++#define FC_CPUS 256 ++#define FC_WINDOW_NS 100000000ull ++#define FC_FIRST_SPAN_NS 20000000ull ++#define FC_GOOD_SPAN_NS 500000000ull ++#define FC_MAX_SPAN_NS 2000000000ull ++#define FC_MAX_SAMPLE_TICKS 60000ull ++#define FC_SAMPLE_SLACK_TICKS 400ull ++ ++struct fc_cpu { ++ volatile unsigned int seq; /* odd while a writer is inside */ ++ volatile unsigned int valid; ++ volatile unsigned long long tsc; ++ volatile unsigned long long mono; ++ volatile long long real_offset; ++ unsigned long long origin_tsc; ++ unsigned long long origin_mono; ++} __attribute__((aligned(64))); ++ ++static struct fc_cpu fc_cpus[FC_CPUS]; ++static volatile unsigned long long fc_mult; /* ns per tick, 32.32 fixed point; 0 until measured */ ++static volatile unsigned long long fc_window; /* FC_WINDOW_NS in ticks */ ++static volatile unsigned long long fc_last_mono __attribute__((aligned(64))); ++static volatile unsigned long long fc_fastest = FC_MAX_SAMPLE_TICKS; /* shortest bracketed clock read seen, in ticks */ ++static volatile int fc_state; /* 0 not probed, 1 usable, -1 unusable */ ++ ++static inline unsigned long long fc_rdtscp(unsigned int* cpu) ++{ ++ unsigned int lo, hi, aux; ++ __asm__ volatile("rdtscp" : "=a"(lo), "=d"(hi), "=c"(aux)); ++ *cpu = aux & 0xfff; ++ return ((unsigned long long) hi << 32) | lo; ++} ++ ++static int fc_probe(void) ++{ ++ unsigned int a, b, c, d; ++ int usable = 0; ++ __asm__ volatile("cpuid" : "=a"(a), "=b"(b), "=c"(c), "=d"(d) : "a"(0x80000000u), "c"(0)); ++ if (a >= 0x80000007u) { ++ __asm__ volatile("cpuid" : "=a"(a), "=b"(b), "=c"(c), "=d"(d) : "a"(0x80000001u), "c"(0)); ++ usable = (d >> 27) & 1; /* RDTSCP */ ++ __asm__ volatile("cpuid" : "=a"(a), "=b"(b), "=c"(c), "=d"(d) : "a"(0x80000007u), "c"(0)); ++ usable &= (d >> 8) & 1; /* invariant TSC */ ++ } ++ if (usable && LINUX_SYSCALL(__NR_access, "/tmp/rclol-fast-clock", 0) != 0) ++ usable = 0; ++ fc_state = usable ? 1 : -1; ++ return fc_state; ++} ++ ++static inline unsigned long long fc_monotonic(unsigned long long ns) ++{ ++ unsigned long long last = __atomic_load_n(&fc_last_mono, __ATOMIC_RELAXED); ++ while (ns > last) { ++ if (__atomic_compare_exchange_n(&fc_last_mono, &last, ns, 1, __ATOMIC_RELAXED, __ATOMIC_RELAXED)) ++ return ns; ++ } ++ return last; ++} ++ ++static inline void fc_store(int clockid, struct timespec* res, unsigned long long mono, long long real_offset) ++{ ++ unsigned long long ns = fc_monotonic(mono); ++ if (clockid == CLOCK_REALTIME) ++ ns += real_offset; ++ res->tv_sec = ns / 1000000000ull; ++ res->tv_nsec = ns % 1000000000ull; ++} ++ ++static int fc_resync(int clockid, struct timespec* res) ++{ ++ struct timespec mono, real; ++ unsigned int c0, c1, c2; ++ unsigned long long t0, t1, t2, mono_ns, real_ns, mult, fastest, limit; ++ int rv; ++ ++ t0 = fc_rdtscp(&c0); ++ rv = LINUX_SYSCALL(__NR_clock_gettime, CLOCK_MONOTONIC, &mono); ++ if (rv < 0) ++ return rv; ++ t1 = fc_rdtscp(&c1); ++ mono_ns = (unsigned long long) mono.tv_sec * 1000000000ull + mono.tv_nsec; ++ ++ /* A sample is only a calibration point when the read ran undisturbed on one CPU: not much slower than the fastest ++ * read seen. The reference creeps up on every rejection, so a machine that became slower (clock throttled) is ++ * followed within a few calls, and a rejected call costs the one syscall it would have cost anyway. */ ++ fastest = fc_fastest; ++ if (c0 == c1 && t1 - t0 < fastest) ++ fc_fastest = fastest = t1 - t0; ++ limit = fastest + fastest / 2 + FC_SAMPLE_SLACK_TICKS; ++ if (c0 != c1 || c0 >= FC_CPUS || t1 - t0 >= limit) { ++ if (c0 == c1 && fastest < FC_MAX_SAMPLE_TICKS) ++ fc_fastest = fastest + fastest / 8 + 1; ++ if (clockid == CLOCK_REALTIME) { ++ rv = LINUX_SYSCALL(__NR_clock_gettime, CLOCK_REALTIME, res); ++ return rv < 0 ? rv : 0; ++ } ++ goto done; ++ } ++ ++ rv = LINUX_SYSCALL(__NR_clock_gettime, CLOCK_REALTIME, &real); ++ if (rv < 0) ++ return rv; ++ t2 = fc_rdtscp(&c2); ++ real_ns = (unsigned long long) real.tv_sec * 1000000000ull + real.tv_nsec; ++ mult = fc_mult; ++ ++ if (c1 == c2 && t2 - t1 < limit) { ++ struct fc_cpu* c = &fc_cpus[c0]; ++ unsigned int seq = __atomic_load_n(&c->seq, __ATOMIC_RELAXED); ++ if (!(seq & 1) && __atomic_compare_exchange_n(&c->seq, &seq, seq + 1, 0, __ATOMIC_ACQUIRE, __ATOMIC_RELAXED)) { ++ /* The kernel reads the clock early in the syscall and most of the round trip is the return path. */ ++ unsigned long long mid = t0 + (t1 - t0) / 4; ++ unsigned long long real_mid = t1 + (t2 - t1) / 4; ++ int restart = !c->valid || mid <= c->origin_tsc || mono_ns <= c->origin_mono; ++ if (!restart) { ++ unsigned long long span = mono_ns - c->origin_mono; ++ unsigned long long ticks = mid - c->origin_tsc; ++ if (span > 2 * FC_MAX_SPAN_NS) { ++ restart = 1; /* long idle or suspend: too long for the fixed-point division */ ++ } else if (span >= FC_FIRST_SPAN_NS) { ++ unsigned long long measured = (span << 32) / ticks; ++ /* plausible tick rates only: 0.4 .. 16 GHz */ ++ int plausible = measured > (1ull << 28) && measured < (5ull << 31); ++ if (plausible && mult) { ++ unsigned long long diff = measured > mult ? measured - mult : mult - measured; ++ if (diff > mult / 128) ++ plausible = 0; /* the TSC and the kernel clock diverged (suspend): start over */ ++ } ++ if (!plausible) { ++ restart = 1; ++ } else if (!mult || span >= FC_GOOD_SPAN_NS) { ++ mult = measured; ++ __atomic_store_n(&fc_window, (FC_WINDOW_NS << 32) / measured, __ATOMIC_RELAXED); ++ __atomic_store_n(&fc_mult, measured, __ATOMIC_RELEASE); ++ } ++ if (span >= FC_MAX_SPAN_NS) ++ restart = 1; ++ } ++ } ++ if (restart) { ++ c->origin_tsc = mid; ++ c->origin_mono = mono_ns; ++ } ++ c->tsc = mid; ++ c->mono = mono_ns; ++ c->real_offset = (long long) (real_ns - mono_ns) - (long long) (((real_mid - mid) * mult) >> 32); ++ c->valid = 1; ++ __atomic_store_n(&c->seq, seq + 2, __ATOMIC_RELEASE); ++ } ++ } ++ ++ if (clockid == CLOCK_REALTIME) { ++ *res = real; ++ return 0; ++ } ++done: ++ mono_ns = fc_monotonic(mono_ns); ++ res->tv_sec = mono_ns / 1000000000ull; ++ res->tv_nsec = mono_ns % 1000000000ull; ++ return 0; ++} ++ ++/* 0: *res filled; 1: not handled here (caller makes the syscall); negative: Linux errno */ ++__attribute__((visibility("hidden"))) ++int __darling_fast_clock(int clockid, struct timespec* res) ++{ ++ unsigned int cpu, seq; ++ unsigned long long tsc, mult; ++ int state = fc_state; ++ ++ if (__builtin_expect(state <= 0, 0)) { ++ if (state == 0) ++ state = fc_probe(); ++ if (state < 0) ++ return 1; ++ } ++ if (clockid != CLOCK_MONOTONIC && clockid != CLOCK_REALTIME) ++ return 1; ++ ++ mult = __atomic_load_n(&fc_mult, __ATOMIC_ACQUIRE); ++ tsc = fc_rdtscp(&cpu); ++ if (__builtin_expect(mult != 0 && cpu < FC_CPUS, 1)) { ++ struct fc_cpu* c = &fc_cpus[cpu]; ++ seq = __atomic_load_n(&c->seq, __ATOMIC_ACQUIRE); ++ if (!(seq & 1) && c->valid) { ++ unsigned long long base_tsc = c->tsc, base_mono = c->mono; ++ long long real_offset = c->real_offset; ++ if (__atomic_load_n(&c->seq, __ATOMIC_ACQUIRE) == seq) { ++ unsigned long long delta = tsc - base_tsc; ++ if (delta < fc_window) { ++ fc_store(clockid, res, base_mono + ((delta * mult) >> 32), real_offset); ++ return 0; ++ } ++ } ++ } ++ } ++ return fc_resync(clockid, res); ++} ++#endif ++ + VISIBLE + int clock_gettime (int __clockid, struct timespec* __res) + { + int rv; + ++#if defined(__x86_64__) ++ rv = __darling_fast_clock(__clockid, __res); ++ if (rv == 0) ++ return 0; ++ if (rv == 1) ++#endif + rv = LINUX_SYSCALL(__NR_clock_gettime, __clockid, __res); + if (rv < 0) + { +diff --git a/darling/src/libsystem_kernel/emulation/src/linux_premigration/ext/file_handle.c b/darling/src/libsystem_kernel/emulation/src/linux_premigration/ext/file_handle.c +index 08c05fd..b51d66d 100644 +--- a/darling/src/libsystem_kernel/emulation/src/linux_premigration/ext/file_handle.c ++++ b/darling/src/libsystem_kernel/emulation/src/linux_premigration/ext/file_handle.c +@@ -1,7 +1,7 @@ + #include + + #include +-#include ++#include + + #include + #include +@@ -27,10 +27,8 @@ struct SavedRef + #define MOUNT_ID_SAVED (-100) + static struct SavedRef g_savedRefs[200]; + static int g_nextSavedRef = 0, g_nextGen = 0; +-static os_unfair_lock g_savedRefLock = OS_UNFAIR_LOCK_INIT; ++static emulation_mutex g_savedRefLock = EMULATION_MUTEX_INITIALIZER; + +-void __attribute__((weak)) os_unfair_lock_unlock(os_unfair_lock_t lock) {} +-void __attribute__((weak)) os_unfair_lock_lock(os_unfair_lock_t lock) {} + + VISIBLE + int sys_name_to_handle(const char* name, RefData* ref, int follow) +@@ -53,7 +51,7 @@ int sys_name_to_handle(const char* name, RefData* ref, int follow) + // This is unfortunately the case for overlayfs, which doesn't support "nfs_export" along with "metacopy" + if (ret == -LINUX_EOPNOTSUPP && sys_access(name, 0) == 0) + { +- os_unfair_lock_lock(&g_savedRefLock); ++ emulation_mutex_lock(&g_savedRefLock); + + if (g_savedRefs[g_nextSavedRef].path) + free(g_savedRefs[g_nextSavedRef].path); +@@ -66,7 +64,7 @@ int sys_name_to_handle(const char* name, RefData* ref, int follow) + + g_nextSavedRef = (g_nextSavedRef + 1) % (sizeof(g_savedRefs) / sizeof(g_savedRefs[0])); + +- os_unfair_lock_unlock(&g_savedRefLock); ++ emulation_mutex_unlock(&g_savedRefLock); + + ret = 0; + } +@@ -83,7 +81,7 @@ int sys_handle_to_name(RefData* ref, char name[4096]) + if (ref->mount_id == MOUNT_ID_SAVED) + { + int ret = -ENOENT; +- os_unfair_lock_lock(&g_savedRefLock); ++ emulation_mutex_lock(&g_savedRefLock); + + if (g_savedRefs[ref->index].gen == ref->gen) + { +@@ -91,7 +89,7 @@ int sys_handle_to_name(RefData* ref, char name[4096]) + ret = sys_access(name, 0); + } + +- os_unfair_lock_unlock(&g_savedRefLock); ++ emulation_mutex_unlock(&g_savedRefLock); + return ret; + } + +diff --git a/darling/src/libsystem_kernel/emulation/src/linux_premigration/ext/sched_yield.c b/darling/src/libsystem_kernel/emulation/src/linux_premigration/ext/sched_yield.c +index dc8fbb8..171746a 100644 +--- a/darling/src/libsystem_kernel/emulation/src/linux_premigration/ext/sched_yield.c ++++ b/darling/src/libsystem_kernel/emulation/src/linux_premigration/ext/sched_yield.c +@@ -1,7 +1,58 @@ + #include ++#include + #include + +-void __linux_sched_yield(void) ++/* darling-lol M634: swtch/yield spin detector. The League launcher's ++ * dependency-resolution path pinned a core in a tight yield loop; per-thread ++ * burst counters with caller tags, return addresses and a bounded stack ++ * window identify the guest-side spinner without libc. */ ++ ++struct lolsys_yield_slot { ++ long tid; ++ unsigned long long n; ++ unsigned int marks; ++}; ++ ++/* darling-lol: no libc; slot table mirrors base.c's lolsys_slots pattern. */ ++#define YIELD_SLOTS 64 ++static struct lolsys_yield_slot yield_slots[YIELD_SLOTS]; ++ ++static int linux_gettid_raw(void) + { ++ return (int) LINUX_SYSCALL0(__NR_gettid); ++} ++ ++static void yield_mark(const char* site, unsigned long long n) ++{ ++ __simple_printf("LOLYLD site=%s tid=%d n=%llu\n", site, linux_gettid_raw(), n); ++ ++} ++ ++void __linux_sched_yield_site(const char* site) ++{ ++ int tid = linux_gettid_raw(); ++ struct lolsys_yield_slot* s = &yield_slots[((unsigned int) tid) % YIELD_SLOTS]; ++ if (s->tid != tid) { ++ s->tid = tid; ++ s->n = 0; ++ s->marks = 0; ++ } ++ s->n++; ++ /* Thresholds: ~0.1s, ~1s, ~17s, ~4.5min at 15k/s. Cap: 5 dumps/thread. */ ++ static const unsigned long long marks[5] = {1000, 16000, 262144, 4000000, 64000000}; ++ if (s->marks < 5) { ++ for (int i = s->marks; i < 5; i++) { ++ if (s->n == marks[i]) { ++ s->marks++; ++ yield_mark(site, s->n); ++ break; ++ } ++ } ++ } + LINUX_SYSCALL0(__NR_sched_yield); + } ++ ++void __linux_sched_yield(void) ++{ ++ __linux_sched_yield_site("SWTYLD"); ++} +diff --git a/darling/src/libsystem_kernel/emulation/src/linux_premigration/signal/sigexc.c b/darling/src/libsystem_kernel/emulation/src/linux_premigration/signal/sigexc.c +index 206e92e..0646cb7 100644 +--- a/darling/src/libsystem_kernel/emulation/src/linux_premigration/signal/sigexc.c ++++ b/darling/src/libsystem_kernel/emulation/src/linux_premigration/signal/sigexc.c +@@ -151,8 +151,13 @@ void sigrt_handler(int signum, struct linux_siginfo* info, struct linux_ucontext + int status = dserver_rpc_interrupt_enter(); + + if (status != 0) { +- __simple_printf("*** dserver_rpc_interrupt_enter failed with code %d ***\n", status); +- __simple_abort(); ++ /* darling-lol: same rationale as sigexc_handler -- do not abort ++ * inside the handler when darlingserver is unreachable. RT ++ * signals are control traffic; if we cannot service them, we ++ * simply return (the signal is consumed) rather than kill the ++ * process. */ ++ __simple_printf("*** sigrt_handler: dserver_rpc_interrupt_enter failed with code %d (returning) ***\n", status); ++ return; + } + + if (signum == SIGNAL_SIGEXC_SUSPEND) { +@@ -267,13 +272,150 @@ static void state_from_kernel(struct linux_ucontext* ctxt, const void* tstate, c + #endif + } + ++/* Avoid recursive fault reporting; normal signal handling still runs. */ ++static int _lol_depth = 0; ++ ++#ifdef __x86_64__ ++/* The crashing process's stdout is often not a log its owner can read (the League game's is not), ++ * so the same report is also appended to the host's /tmp/lolsig.log with the module map needed to ++ * resolve the addresses. Only return addresses and the map are written, never data. */ ++static void _lol_report_line(int fd, const char* text) ++{ ++ if (fd >= 0) { ++ unsigned long length = 0; ++ while (text[length]) ++length; ++ linux_syscall(fd, text, length, 0, 0, 0, __NR_write); ++ } ++} ++ ++static void _lol_report_maps(int fd) ++{ ++ if (fd < 0) return; ++ int maps = (int) linux_syscall("/proc/self/maps", 0 /* O_RDONLY */, 0, 0, 0, 0, __NR_open); ++ if (maps < 0) return; ++ char chunk[2048]; ++ long got; ++ while ((got = linux_syscall(maps, chunk, sizeof(chunk), 0, 0, 0, __NR_read)) > 0) ++ linux_syscall(fd, chunk, got, 0, 0, 0, __NR_write); ++ linux_syscall(maps, 0, 0, 0, 0, 0, __NR_close); ++} ++ ++static void _lol_log_fault(int linux_signum, struct linux_siginfo* info, struct linux_ucontext* ctxt) ++{ ++ char line[160]; ++ /* O_WRONLY | O_CREAT | O_APPEND */ ++ int report = (int) linux_syscall("/tmp/lolsig.log", 0x441, 0644, 0, 0, 0, __NR_open); ++ long self = linux_syscall(0, 0, 0, 0, 0, 0, __NR_getpid); ++ ++ /* The interrupted stack and argument pointers may be invalid. Reading them ++ * here can cause another signal and replace a recoverable fault with a ++ * fatal crash in the reporter. Keep only code/fault addresses, never data. */ ++ __simple_printf("LOLSIGFAIL pid=%ld sig=%d code=%d addr=0x%llx rip=0x%llx\n", ++ self, linux_signum, info->si_code, ++ (unsigned long long) info->si_addr, ++ (unsigned long long) ctxt->uc_mcontext.gregs.rip); ++ __simple_sprintf(line, "LOLSIGFAIL pid=%ld sig=%d code=%d addr=0x%llx rip=0x%llx\n", ++ self, linux_signum, info->si_code, ++ (unsigned long long) info->si_addr, ++ (unsigned long long) ctxt->uc_mcontext.gregs.rip); ++ _lol_report_line(report, line); ++ ++ /* Kernel-mediated copies fail safely for inaccessible stacks. Print only ++ * return addresses, never stack contents, strings, or argument values. */ ++ struct lol_iovec { void *base; unsigned long size; }; ++ unsigned long long frame = ctxt->uc_mcontext.gregs.rbp; ++ for (unsigned i = 0; i < 40 && frame; ++i) { ++ unsigned long long pair[2]; ++ struct lol_iovec local = { pair, sizeof(pair) }; ++ struct lol_iovec remote = { (void *)frame, sizeof(pair) }; ++ long copied = linux_syscall(self, &local, 1, &remote, 1, 0, __NR_process_vm_readv); ++ if (copied != sizeof(pair)) break; ++ __simple_printf("LOLSIGFRAME pid=%ld n=%u pc=0x%llx\n", self, i, pair[1]); ++ __simple_sprintf(line, "LOLSIGFRAME pid=%ld n=%u pc=0x%llx\n", self, i, pair[1]); ++ _lol_report_line(report, line); ++ if (pair[0] <= frame || pair[0] - frame > 1024 * 1024) break; ++ frame = pair[0]; ++ } ++ ++ _lol_report_line(report, "LOLMAPS\n"); ++ _lol_report_maps(report); ++ _lol_report_line(report, "LOLEND\n"); ++ if (report >= 0) ++ linux_syscall(report, 0, 0, 0, 0, 0, __NR_close); ++} ++ ++#endif ++ + void sigexc_handler(int linux_signum, struct linux_siginfo* info, struct linux_ucontext* ctxt) + { + int status = dserver_rpc_interrupt_enter(); ++ int relaxed = 0; ++ ++#ifdef __x86_64__ ++ if (_lol_depth > 0) { ++ /* Nested fault inside the reporter above: do not recurse, fall ++ * through to normal handling with no further logging. */ ++ goto normal; ++ } ++#endif + + if (status != 0) { ++ /* darling-lol: previously this hard-aborted, which killed the ++ * process inside the signal handler and hid the original fault ++ * (e.g. a freshly-forked/spawned thread whose per-thread RPC ++ * socket is stale returns -111/ECONNREFUSED here). We cannot ++ * talk to darlingserver, but we can still do the one thing that ++ * matters most: report WHERE the fault happened, then apply the ++ * default signal effects so the process terminates for the ++ * right reason instead of via our abort(). */ + __simple_printf("*** dserver_rpc_interrupt_enter failed with code %d ***\n", status); +- __simple_abort(); ++#ifdef __x86_64__ ++ _lol_depth = 1; ++ _lol_log_fault(linux_signum, info, ctxt); ++ _lol_depth = 0; ++#endif ++ relaxed = 1; ++ } ++ ++#ifdef __x86_64__ ++ /* darling-lol M38: also report fatal signals that DO reach ++ * darlingserver (the fail-fast branch above only fires when the RPC ++ * socket is dead). Log-and-continue: delivery semantics unchanged. */ ++ /* Skip when relaxed: the fail-fast branch above already logged. */ ++ if (!relaxed && ++ (linux_signum == LINUX_SIGABRT || linux_signum == LINUX_SIGSEGV || ++ linux_signum == LINUX_SIGILL || linux_signum == LINUX_SIGBUS || ++ linux_signum == LINUX_SIGTRAP)) { ++ _lol_depth = 1; ++ _lol_log_fault(linux_signum, info, ctxt); ++ _lol_depth = 0; ++ } ++normal: ++ /* Self-heal: if a stack read above faulted, its nested entry jumped ++ * here with _lol_depth stuck at 1; reset so later faults still log. */ ++ _lol_depth = 0; ++#endif ++ ++ if (relaxed) { ++ /* Cannot reach darlingserver: reset the handler to SIG_DFL and ++ * re-raise so the kernel applies the normal signal disposition ++ * (and any core dump). Returning would loop forever on the same ++ * faulting instruction. */ ++ struct linux_sigaction sa; ++ sa.sa_sigaction = (linux_sig_handler*) NULL; /* SIG_DFL */ ++ sa.sa_mask = 0x7fffffff; ++ sa.sa_flags = LINUX_SA_RESTORER | LINUX_SA_SIGINFO | LINUX_SA_RESTART | LINUX_SA_ONSTACK; ++ sa.sa_restorer = sig_restorer; ++ LINUX_SYSCALL(__NR_rt_sigaction, linux_signum, &sa, NULL, sizeof(sa.sa_mask)); ++ /* darling-lol: retarget to the faulting THREAD only. kill(0, sig) ++ * delivers to the whole Linux process group, so one dying guest ++ * process took down every sibling (launcher + UX + daemons all ++ * showed simultaneous SIGABRT/SIGILL). tgkill confines the ++ * re-raise to this thread; raw syscalls, no libc. */ ++ LINUX_SYSCALL(__NR_tgkill, ++ LINUX_SYSCALL(__NR_getpid), LINUX_SYSCALL(__NR_gettid), ++ linux_signum); ++ return; + } + + kern_printf("sigexc_handler(%d, %p, %p)\n", linux_signum, info, ctxt); +@@ -359,7 +501,13 @@ void sigexc_handler(int linux_signum, struct linux_siginfo* info, struct linux_u + sizeof(sa.sa_mask)); + + // Resend signal to self +- LINUX_SYSCALL(__NR_kill, 0, linux_signum); ++ // (darling-lol: tgkill, thread-only -- kill(0, sig) hit ++ // the whole process group and massacred siblings; the ++ // comment always said "self".) ++ LINUX_SYSCALL(__NR_tgkill, ++ LINUX_SYSCALL(__NR_getpid), ++ LINUX_SYSCALL(__NR_gettid), ++ linux_signum); + } + } + } +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/bsd_syscall_table.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/bsd_syscall_table.c +index 7d5ef80..46debb8 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/bsd_syscall_table.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/bsd_syscall_table.c +@@ -24,6 +24,7 @@ + #include + #include + #include ++#include + #include + #include + #include +@@ -44,6 +45,7 @@ + #include + #include + #include ++#include + #include + #include + #include +@@ -427,6 +429,7 @@ void* __bsd_syscall_table[600] = { + [369] = sys_kevent64, + [372] = sys_thread_selfid, + [374] = sys_kevent_qos, ++ [381] = sys___mac_syscall, + [394] = sys_pselect, + [395] = sys_pselect_nocancel, + [396] = sys_read_nocancel, +@@ -462,6 +465,7 @@ void* __bsd_syscall_table[600] = { + [441] = sys_guarded_open_np, + [442] = sys_guarded_close_np, + [443] = sys_guarded_kqueue_np, ++ [444] = sys_change_fdguard_np, + [461] = sys_getattrlistbulk, + [462] = sys_clonefileat, + [463] = sys_openat, +@@ -483,6 +487,7 @@ void* __bsd_syscall_table[600] = { + [500] = sys_getentropy, + [515] = sys_ulock_wait, + [516] = sys_ulock_wake, ++ [544] = sys_ulock_wait2, + [517] = sys_fclonefileat, + [521] = sys_abort_with_payload, + [524] = sys_setattrlistat, +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/misc/sysctl_hw.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/misc/sysctl_hw.c +index f273989..6107548 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/misc/sysctl_hw.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/misc/sysctl_hw.c +@@ -25,6 +25,8 @@ enum { + _HW_CPUTHREADTYPE, + _HW_64BITCAPABLE, + _HW_CPUFREQUENCY = 15, ++ _HW_CACHECONFIG = 1200, ++ _HW_CACHESIZE, + }; + + static sysctl_handler(handle_availcpu); +@@ -39,7 +41,72 @@ static sysctl_handler(handle_cpusubtype); + static sysctl_handler(handle_cputhreadtype); + static sysctl_handler(handle_cpu64bitcapable); + static sysctl_handler(handle_machine); ++static sysctl_handler(handle_model); + static sysctl_handler(handle_cpufrequency); ++static sysctl_handler(handle_cacheconfig); ++static sysctl_handler(handle_cachesize); ++ ++/* hw.optional: CPUID leaf, output register (0-3 = eax-edx) and bit */ ++static long optional_feature(void* old, unsigned long* oldlen, unsigned int leaf, int reg, int bit) ++{ ++ int value = 0; ++ ++ sysctl_handle_size(sizeof(int)); ++#if defined(__i386__) || defined(__x86_64__) ++ unsigned int regs[4]; ++ __asm__("cpuid" : "=a" (regs[0]), "=b" (regs[1]), "=c" (regs[2]), "=d" (regs[3]) : "0" (leaf), "2" (0)); ++ value = (regs[reg] >> bit) & 1; ++#endif ++ *((int*) old) = value; ++ return 0; ++} ++ ++#define optional_handler(_name, _leaf, _reg, _bit) \ ++ static sysctl_handler(handle_optional_##_name) { return optional_feature(old, oldlen, _leaf, _reg, _bit); } ++ ++optional_handler(floatingpoint, 1, 3, 0) ++optional_handler(mmx, 1, 3, 23) ++optional_handler(sse, 1, 3, 25) ++optional_handler(sse2, 1, 3, 26) ++optional_handler(sse3, 1, 2, 0) ++optional_handler(supplementalsse3, 1, 2, 9) ++optional_handler(sse4_1, 1, 2, 19) ++optional_handler(sse4_2, 1, 2, 20) ++optional_handler(aes, 1, 2, 25) ++optional_handler(avx1_0, 1, 2, 28) ++optional_handler(rdrand, 1, 2, 30) ++optional_handler(f16c, 1, 2, 29) ++optional_handler(fma, 1, 2, 12) ++optional_handler(avx2_0, 7, 1, 5) ++optional_handler(bmi1, 7, 1, 3) ++optional_handler(bmi2, 7, 1, 8) ++optional_handler(avx512f, 7, 1, 16) ++ ++enum { ++ _HW_OPTIONAL = 1100, ++}; ++ ++static const struct known_sysctl sysctls_hw_optional[] = { ++ { .oid = 1, .type = CTLTYPE_INT, .exttype = "I", .name = "floatingpoint", .handler = handle_optional_floatingpoint }, ++ { .oid = 2, .type = CTLTYPE_INT, .exttype = "I", .name = "mmx", .handler = handle_optional_mmx }, ++ { .oid = 3, .type = CTLTYPE_INT, .exttype = "I", .name = "sse", .handler = handle_optional_sse }, ++ { .oid = 4, .type = CTLTYPE_INT, .exttype = "I", .name = "sse2", .handler = handle_optional_sse2 }, ++ { .oid = 5, .type = CTLTYPE_INT, .exttype = "I", .name = "sse3", .handler = handle_optional_sse3 }, ++ { .oid = 6, .type = CTLTYPE_INT, .exttype = "I", .name = "supplementalsse3", .handler = handle_optional_supplementalsse3 }, ++ { .oid = 7, .type = CTLTYPE_INT, .exttype = "I", .name = "sse4_1", .handler = handle_optional_sse4_1 }, ++ { .oid = 8, .type = CTLTYPE_INT, .exttype = "I", .name = "sse4_2", .handler = handle_optional_sse4_2 }, ++ { .oid = 9, .type = CTLTYPE_INT, .exttype = "I", .name = "aes", .handler = handle_optional_aes }, ++ { .oid = 10, .type = CTLTYPE_INT, .exttype = "I", .name = "avx1_0", .handler = handle_optional_avx1_0 }, ++ { .oid = 11, .type = CTLTYPE_INT, .exttype = "I", .name = "rdrand", .handler = handle_optional_rdrand }, ++ { .oid = 12, .type = CTLTYPE_INT, .exttype = "I", .name = "f16c", .handler = handle_optional_f16c }, ++ { .oid = 13, .type = CTLTYPE_INT, .exttype = "I", .name = "fma", .handler = handle_optional_fma }, ++ { .oid = 14, .type = CTLTYPE_INT, .exttype = "I", .name = "avx2_0", .handler = handle_optional_avx2_0 }, ++ { .oid = 15, .type = CTLTYPE_INT, .exttype = "I", .name = "bmi1", .handler = handle_optional_bmi1 }, ++ { .oid = 16, .type = CTLTYPE_INT, .exttype = "I", .name = "bmi2", .handler = handle_optional_bmi2 }, ++ { .oid = 17, .type = CTLTYPE_INT, .exttype = "I", .name = "avx512f", .handler = handle_optional_avx512f }, ++ { .oid = 18, .type = CTLTYPE_INT, .exttype = "I", .name = "x86_64", .handler = handle_cpu64bitcapable }, ++ { .oid = -1 } ++}; + + const struct known_sysctl sysctls_hw[] = { + { .oid = HW_AVAILCPU, .type = CTLTYPE_INT, .exttype = "", .name = "availcpu", .handler = handle_availcpu }, +@@ -55,7 +122,11 @@ const struct known_sysctl sysctls_hw[] = { + { .oid = _HW_CPUTHREADTYPE, .type = CTLTYPE_INT, .exttype = "", .name = "cputhreadtype", .handler = handle_cputhreadtype }, + { .oid = _HW_64BITCAPABLE, .type = CTLTYPE_INT, .exttype = "", .name = "cpu64bit_capable", .handler = handle_cpu64bitcapable }, + { .oid = HW_MACHINE, .type = CTLTYPE_STRING, .exttype = "S", .name = "machine", .handler = handle_machine }, ++ { .oid = _HW_OPTIONAL, .type = CTLTYPE_NODE, .exttype = "", .name = "optional", .subctls = sysctls_hw_optional }, ++ { .oid = HW_MODEL, .type = CTLTYPE_STRING, .exttype = "S", .name = "model", .handler = handle_model }, + { .oid = _HW_CPUFREQUENCY, .type = CTLTYPE_INT, .exttype = "", .name = "cpufrequency", .handler = handle_cpufrequency }, ++ { .oid = _HW_CACHECONFIG, .type = CTLTYPE_OPAQUE, .exttype = "Q", .name = "cacheconfig", .handler = handle_cacheconfig }, ++ { .oid = _HW_CACHESIZE, .type = CTLTYPE_OPAQUE, .exttype = "Q", .name = "cachesize", .handler = handle_cachesize }, + { .oid = -1 } + }; + +@@ -71,6 +142,12 @@ static const struct host_basic_info* gethostinfo(void) + hself = mach_host_self(); + host_info(hself, HOST_BASIC_INFO, &hinfo, &hcount); + mach_port_deallocate(mach_task_self(), hself); ++ ++ // Report the fixed MacBookPro16,1 configuration rather than the Linux host's ++ hinfo.avail_cpus = hinfo.max_cpus = 8; ++ hinfo.physical_cpu = hinfo.physical_cpu_max = 8; ++ hinfo.logical_cpu = hinfo.logical_cpu_max = 8; ++ hinfo.max_mem = 17179869184ULL; // 16 GB + } + + return &hinfo; +@@ -174,6 +251,8 @@ sysctl_handler(handle_cpufrequency) + int freq = 2400000; + char buf[16]; + ++ sysctl_handle_size(sizeof(int)); ++ + if (read_string("/sys/bus/cpu/devices/cpu0/cpufreq/cpuinfo_max_freq", buf, sizeof(buf))) + freq = __simple_atoi(buf, NULL); + *((int*) old) = freq; +@@ -193,3 +272,121 @@ sysctl_handler(handle_machine) + copyout_string(need_uname()->machine, (char*) old, oldlen); + return 0; + } ++ ++sysctl_handler(handle_model) ++{ ++ // an Intel Mac model identifier, matching the x86_64 machine we emulate ++ copyout_string("MacBookPro16,1", (char*) old, oldlen); ++ return 0; ++} ++ ++#define CACHE_LEVELS 10 ++// The guest has no /sys of its own; the host's is reachable through the SystemRoot volume ++#define HOST_CACHE_DIR "/Volumes/SystemRoot/sys/devices/system/cpu/cpu0/cache/" ++ ++static unsigned long long parse_ull(const char* s, const char** end) ++{ ++ unsigned long long v = 0; ++ while (*s >= '0' && *s <= '9') ++ v = v * 10 + (*s++ - '0'); ++ if (end) ++ *end = s; ++ return v; ++} ++ ++// Number of CPUs in a sysfs list such as "0-1,4-5" ++static unsigned int cpu_list_count(const char* s) ++{ ++ unsigned int count = 0; ++ ++ while (*s >= '0' && *s <= '9') ++ { ++ unsigned long long first = parse_ull(s, &s), last = first; ++ ++ if (*s == '-') ++ last = parse_ull(s + 1, &s); ++ if (last >= first) ++ count += last - first + 1; ++ if (*s == ',') ++ s++; ++ } ++ return count; ++} ++ ++// Describes the data/unified cache of one level (1 = L1D) of the host's CPU 0. ++// Instruction caches are skipped, as in XNU. Returns false if the host doesn't report the level. ++static bool host_cache_level(unsigned int level, unsigned long long* size, unsigned int* sharing) ++{ ++ char path[96], buf[64]; ++ ++ for (int index = 0; index < 10; index++) ++ { ++ const char* end; ++ unsigned long long value; ++ ++ __simple_sprintf(path, HOST_CACHE_DIR "index%d/level", index); ++ if (!read_string(path, buf, sizeof(buf))) ++ break; ++ if (parse_ull(buf, NULL) != level) ++ continue; ++ ++ __simple_sprintf(path, HOST_CACHE_DIR "index%d/type", index); ++ if (!read_string(path, buf, sizeof(buf)) || buf[0] == 'I') ++ continue; ++ ++ __simple_sprintf(path, HOST_CACHE_DIR "index%d/size", index); ++ if (!read_string(path, buf, sizeof(buf))) ++ continue; ++ value = parse_ull(buf, &end); ++ if (*end == 'K') ++ value <<= 10; ++ else if (*end == 'M') ++ value <<= 20; ++ *size = value; ++ ++ __simple_sprintf(path, HOST_CACHE_DIR "index%d/shared_cpu_list", index); ++ *sharing = read_string(path, buf, sizeof(buf)) ? cpu_list_count(buf) : 1; ++ if (*sharing == 0) ++ *sharing = 1; ++ return true; ++ } ++ return false; ++} ++ ++// hw.cacheconfig[0] is the number of logical CPUs, [n] the number of CPUs sharing a level n cache ++sysctl_handler(handle_cacheconfig) ++{ ++ unsigned long long* config = (unsigned long long*) old; ++ ++ sysctl_handle_size(CACHE_LEVELS * sizeof(*config)); ++ memset(config, 0, CACHE_LEVELS * sizeof(*config)); ++ config[0] = gethostinfo()->logical_cpu_max; ++ for (unsigned int level = 1; level < 4; level++) ++ { ++ unsigned long long size; ++ unsigned int sharing; ++ ++ if (host_cache_level(level, &size, &sharing)) ++ config[level] = sharing; ++ } ++ return 0; ++} ++ ++// hw.cachesize[0] is the memory size, [n] the size in bytes of the level n cache ++sysctl_handler(handle_cachesize) ++{ ++ unsigned long long* sizes = (unsigned long long*) old; ++ ++ sysctl_handle_size(CACHE_LEVELS * sizeof(*sizes)); ++ memset(sizes, 0, CACHE_LEVELS * sizeof(*sizes)); ++ sizes[0] = gethostinfo()->max_mem; ++ for (unsigned int level = 1; level < 4; level++) ++ { ++ unsigned long long size; ++ unsigned int sharing; ++ ++ if (host_cache_level(level, &size, &sharing)) ++ sizes[level] = size; ++ } ++ return 0; ++} +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/misc/sysctl_kern.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/misc/sysctl_kern.c +index bd9cdc2..83ad15c 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/misc/sysctl_kern.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/misc/sysctl_kern.c +@@ -139,18 +139,23 @@ sysctl_handler(handle_boottime) + // Fill in struct timeval { tv_sec, tv_usec } + struct sysinfo info; + struct bsd_timeval* tv; +- +- if (__linux_sysinfo(&info) == -1) +- return -errno; ++ /* darling-lol: the boot time does not change, and libc's clock_gettime(CLOCK_MONOTONIC) asks for it twice per ++ * call (two sysinfo + two gettimeofday syscalls). Computed once per process. */ ++ static volatile long boot_sec; + + tv = (struct bsd_timeval*) old; + if (tv != NULL) + { + if (*oldlen < sizeof(*tv)) + return -EINVAL; +- sys_gettimeofday(tv, NULL); +- +- tv->tv_sec -= info.uptime; ++ if (boot_sec == 0) ++ { ++ if (__linux_sysinfo(&info) == -1) ++ return -errno; ++ sys_gettimeofday(tv, NULL); ++ boot_sec = tv->tv_sec - info.uptime; ++ } ++ tv->tv_sec = boot_sec; + + // we don't want the microseconds to be constantly jumping around + // (that actually causes an infinite loop in `_mach_boottime_usec` in libc) +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/misc/sysctl_machdep.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/misc/sysctl_machdep.c +index 4319355..e44e2a2 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/misc/sysctl_machdep.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/misc/sysctl_machdep.c +@@ -32,8 +32,13 @@ static sysctl_handler(handle_max_basic); + static sysctl_handler(handle_family); + static sysctl_handler(handle_model); + static sysctl_handler(handle_stepping); +-static sysctl_handler(handle_brand_string); +-static sysctl_handler(handle_features); ++static sysctl_handler(handle_brand_string) ++{ ++ copyout_string("Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz", (char*) old, oldlen); ++ return 0; ++} ++ ++sysctl_handler(handle_features); + static sysctl_handler(handle_core_count); + + const struct known_sysctl sysctls_machdep_cpu[] = { +@@ -42,7 +47,7 @@ const struct known_sysctl sysctls_machdep_cpu[] = { + { .oid = _CPU_FAMILY, .type = CTLTYPE_INT, .exttype = "I", .name = "family", .handler = handle_family }, + { .oid = _CPU_MODEL, .type = CTLTYPE_INT, .exttype = "I", .name = "model", .handler = handle_model }, + { .oid = _CPU_STEPPING, .type = CTLTYPE_INT, .exttype = "I", .name = "stepping", .handler = handle_stepping }, +- { .oid = _CPU_BRAND_STRING, .type = CTLTYPE_STRING, .exttype = "S", .name = "vendor", .handler = handle_brand_string }, ++ { .oid = _CPU_BRAND_STRING, .type = CTLTYPE_STRING, .exttype = "S", .name = "brand_string", .handler = handle_brand_string }, + { .oid = _CPU_FEATURES, .type = CTLTYPE_STRING, .exttype = "S", .name = "features", .handler = handle_features }, + { .oid = _CPU_CORE_COUNT, .type = CTLTYPE_INT, .exttype = "I", .name = "core_count", .handler = handle_core_count }, + { .oid = -1 } +@@ -154,38 +159,37 @@ sysctl_handler(handle_stepping) + return 0; + } + ++// Original cpuid-based implementation, replaced by the fixed brand string defined above ++#if 0 + sysctl_handler(handle_brand_string) + { +- setup(0x80000000); +- +- __cpuid(level,eax,ebx, ecx, edx); +- +- if (eax < 0x80000004) // the information is not implemented +- return 2; ++ union ++ { ++ unsigned int brand[12]; ++ char name[49]; ++ } v; ++ unsigned int eax, ebx, ecx, edx; + ++ __asm__("cpuid" : "=a" (eax), "=b" (ebx), "=c" (ecx), "=d" (edx) : "0" (0x80000000u), "2" (0)); + ++ if (eax < 0x80000004) // the information is not implemented ++ return -ENOENT; + +- union +- { +- unsigned int brand[12]; +- char name[49]; +- } v; +- +- for (int i = 1; i < 4; i++) +- { +- eax = 0x80000000+i; +- __cpuid(level, eax, ebx, ecx, edx); +- v.brand[0x0+(i-1)*4] = eax; +- v.brand[0x1+(i-1)*4] = ebx; +- v.brand[0x2+(i-1)*4] = ecx; +- v.brand[0x3+(i-1)*4] = edx; +- } ++ for (unsigned int i = 0; i < 3; i++) ++ { ++ __asm__("cpuid" : "=a" (eax), "=b" (ebx), "=c" (ecx), "=d" (edx) : "0" (0x80000002u + i), "2" (0)); ++ v.brand[i * 4 + 0] = eax; ++ v.brand[i * 4 + 1] = ebx; ++ v.brand[i * 4 + 2] = ecx; ++ v.brand[i * 4 + 3] = edx; ++ } + +- v.name[48] = 0; +- copyout_string(v.name, (char*) old, oldlen); ++ v.name[48] = 0; ++ copyout_string(v.name, (char*) old, oldlen); + +- return 0; ++ return 0; + } ++#endif + + sysctl_handler(handle_features) + { +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/misc/sysctl_net.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/misc/sysctl_net.c +index 8fabdda..6cc204e 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/misc/sysctl_net.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/misc/sysctl_net.c +@@ -22,6 +22,7 @@ static sysctl_handler(handle_route); + static int do_iflist(void* old, unsigned long* oldlen); + + #define LINUX_SIOCGIFCONF 0x8912 ++#define LINUX_SIOCGIFFLAGS 0x8913 + #define LINUX_SIOCGIFHWADDR 0x8927 + #define LINUX_SIOCGIFBRDADDR 0x8919 + #define LINUX_SIOCGIFNETMASK 0x891b +@@ -144,6 +145,16 @@ static int do_iflist(void* old, unsigned long* oldlen) { + + memcpy(dl->sdl_data, curr->lifr_name, name_len); + ++ /* Interface-list consumers (including Chromium) discard adapters ++ * without IFF_UP. Populate actual flags instead of leaving zero. ++ * The low ten flags share values; Linux multicast differs. */ ++ memcpy(tmp.lifr_name, curr->lifr_name, sizeof(curr->lifr_name)); ++ if ((status = __real_ioctl(tmp_sock, LINUX_SIOCGIFFLAGS, &tmp)) < 0) ++ goto out; ++ unsigned short linux_flags = (unsigned short)tmp.lifr_flags; ++ ifm->ifm_flags = linux_flags & 0x03ff; ++ if (linux_flags & 0x1000) ifm->ifm_flags |= IFF_MULTICAST; ++ + // grab the MAC + memcpy(tmp.lifr_name, curr->lifr_name, sizeof(curr->lifr_name)); + if ((status = __real_ioctl(tmp_sock, LINUX_SIOCGIFHWADDR, &tmp)) < 0) +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/misc/sysctl_sysctl.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/misc/sysctl_sysctl.c +index 5a49882..5dc5fa7 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/misc/sysctl_sysctl.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/misc/sysctl_sysctl.c +@@ -7,12 +7,15 @@ + + enum { + _PROC_CPUTYPE = 1000, ++ _PROC_TRANSLATED, + }; + + static sysctl_handler(handle_proc_cputype); ++static sysctl_handler(handle_proc_translated); + + const struct known_sysctl sysctls_sysctl[] = { + { .oid = _PROC_CPUTYPE, .type = CTLTYPE_INT, .exttype = "I", .name = "proc_cputype", .handler = handle_proc_cputype }, ++ { .oid = _PROC_TRANSLATED, .type = CTLTYPE_INT, .exttype = "I", .name = "proc_translated", .handler = handle_proc_translated }, + { .oid = -1 } + }; + +@@ -44,3 +47,11 @@ sysctl_handler(handle_proc_cputype) + + return 0; + } ++ ++sysctl_handler(handle_proc_translated) ++{ ++ // guest code runs natively on the host CPU, never under binary translation ++ sysctl_handle_size(sizeof(int)); ++ *((int*) old) = 0; ++ return 0; ++} +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/audit/audit_addr.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/audit/audit_addr.c +index 304dadd..1320eda 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/audit/audit_addr.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/audit/audit_addr.c +@@ -1,6 +1,6 @@ + #include + +-#include ++#include + + #include + #include +@@ -18,19 +18,19 @@ static auditinfo_addr_t info = { + .ai_flags = 0, + }; + // should be a rw lock but *shrug* +-static os_unfair_lock info_lock = OS_UNFAIR_LOCK_INIT; ++static emulation_mutex info_lock = EMULATION_MUTEX_INITIALIZER; + + long sys_getaudit_addr(struct auditinfo_addr* auditinfo_addr, int length) { +- os_unfair_lock_lock(&info_lock); ++ emulation_mutex_lock(&info_lock); + + memcpy(auditinfo_addr, &info, min(length, sizeof(auditinfo_addr_t))); + +- os_unfair_lock_unlock(&info_lock); ++ emulation_mutex_unlock(&info_lock); + return 0; + }; + + long sys_setaudit_addr(struct auditinfo_addr* auditinfo_addr, int length) { +- os_unfair_lock_lock(&info_lock); ++ emulation_mutex_lock(&info_lock); + + memcpy(&info, auditinfo_addr, min(length, sizeof(auditinfo_addr_t))); + +@@ -39,6 +39,6 @@ long sys_setaudit_addr(struct auditinfo_addr* auditinfo_addr, int length) { + sys_getentropy(&info.ai_asid, sizeof(info.ai_asid)); + } + +- os_unfair_lock_unlock(&info_lock); ++ emulation_mutex_unlock(&info_lock); + return 0; + }; +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/bsdthread/bsdthread_create.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/bsdthread/bsdthread_create.c +index 6f8cf43..c5f6d08 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/bsdthread/bsdthread_create.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/bsdthread/bsdthread_create.c +@@ -33,6 +33,7 @@ static bool _uses_threads = false; + + // http://www.tldp.org/FAQ/Threads-FAQ/clone.c + ++ + static void rpc_guard(int fd) { + guard_entry_options_t options; + options.close = __dserver_close_socket; +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/bsdthread/bsdthread_register.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/bsdthread/bsdthread_register.c +index 919205b..ecd19cd 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/bsdthread/bsdthread_register.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/bsdthread/bsdthread_register.c +@@ -34,6 +34,22 @@ struct _pthread_registration_data { + + static struct _pthread_registration_data g_pth_regdata; + ++extern int __darling_is_league_game(void); ++/* Modern libpthread registration has stack_addr_hint at byte 36, followed ++ * by mutex_default_policy at byte 44. Keep the old prefix ABI unchanged. */ ++static void enable_local_conditions(const struct _pthread_registration_data* data, ++ unsigned long length) ++{ ++ if (length < 48 || data->version < 48) return; ++ if (LINUX_SYSCALL(__NR_access, "/tmp/rclol-local-condvars-all", 0) != 0 && ++ (LINUX_SYSCALL(__NR_access, "/tmp/rclol-local-condvars", 0) != 0 || ++ !__darling_is_league_game())) return; ++ /* FIRSTFIT (3) | USE_ULOCK (0x100). Recursive/error-check/shared mutexes ++ * still take libpthread's psynch path. This field is a kernel copyout. */ ++ uint32_t policy = 3u | 0x100u; ++ __builtin_memcpy((char*)(uintptr_t)data + 44, &policy, sizeof(policy)); ++} ++ + long sys_bsdthread_register(void* thread_start, void* wqthread, int pthsize, + const struct _pthread_registration_data* pth_regdata, + unsigned long pth_regdata_len, unsigned long long dpq_offset) +@@ -42,6 +58,7 @@ long sys_bsdthread_register(void* thread_start, void* wqthread, int pthsize, + pthread_entry_point = (bsdthread_entry_t) thread_start; + wqueue_entry_point = (bsdwqthread_entry_t) wqthread; + g_pth_regdata = *pth_regdata; ++ enable_local_conditions(pth_regdata, pth_regdata_len); + + uintptr_t pthread_handle = _pthread_getspecific_direct(_PTHREAD_TSD_SLOT_PTHREAD_SELF); + uintptr_t dispatch_qaddr = pthread_handle + g_pth_regdata.tsd_offset + g_pth_regdata.dispatch_queue_offset; +@@ -156,6 +173,11 @@ void wqueue_entry_point_asm_jump(void* self, int thread_port, void* stackaddr, + // `_start_wqthread` does not expect the stack to be aligned (using the + // call instruction will add the return address to the stack, causing + // it to be aligned). ++ // Workqueue entry is a fresh kernel-style jump, including on reuse. ++ // self sits immediately above the usable stack. Dropping the old ++ // syscall frames prevents every reuse from consuming another frame. ++ "movq %%rdi, %%rsp\n" ++ "xorq %%rbp, %%rbp\n" + "jmpq *%[wqueue_entry_point]\n" + :: + // Arguments follow the usual x86_64 calling conventions. +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/bsdthread/bsdthread_terminate.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/bsdthread/bsdthread_terminate.c +index 14a8238..7c95cdf 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/bsdthread/bsdthread_terminate.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/bsdthread/bsdthread_terminate.c +@@ -1,3 +1,5 @@ ++#include ++#include + #include + + #include +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/bsdthread/pthread_canceled.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/bsdthread/pthread_canceled.c +index 1e78300..a6acb59 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/bsdthread/pthread_canceled.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/bsdthread/pthread_canceled.c +@@ -12,12 +12,24 @@ + + long sys_pthread_canceled(int action) + { ++ static unsigned unsupported_actions; ++ unsigned action_bit = (unsigned)action <= 2 ? 1U << action : 0; ++ + if (action == 0 && !uses_threads()) + return -EINVAL; + ++ /* Cache only a server-confirmed unsupported action. Cancellation state ++ * itself must never be cached: a server implementing it still receives ++ * every request. The server implementation is fixed for this process's ++ * lifetime. Atomic access keeps the shared capability cache race-free. */ ++ if (__atomic_load_n(&unsupported_actions, __ATOMIC_RELAXED) & action_bit) ++ return -ENOSYS; ++ + int ret = dserver_rpc_pthread_canceled(action); + if (ret < 0) + ret = errno_linux_to_bsd(ret); ++ if (ret == -ENOSYS && action_bit) ++ __atomic_fetch_or(&unsupported_actions, action_bit, __ATOMIC_RELAXED); + + return ret; + } +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/bsdthread/workq_kernreturn.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/bsdthread/workq_kernreturn.c +index dbcdd98..0021dc2 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/bsdthread/workq_kernreturn.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/bsdthread/workq_kernreturn.c +@@ -3,7 +3,7 @@ + #include + #include + #include +-#include ++#include + #include + + #define __PTHREAD_EXPOSE_INTERNALS__ 1 +@@ -61,7 +61,7 @@ + #define WORKQ_EXIT_THREAD_NKEVENT (-1) + + static int workq_sem = WQ_MAX_THREADS; // max 64 threads in use +-static os_unfair_lock workq_parked_lock = OS_UNFAIR_LOCK_INIT; ++static emulation_mutex workq_parked_lock = EMULATION_MUTEX_INITIALIZER; + // static int workq_parked_threads = 0; // num spawned, but unused threads + static int workq_parked[WQ_MAX_THREADS]; + static int workq_parked_prio[WQ_MAX_THREADS]; +@@ -78,14 +78,41 @@ struct parked_thread + static void list_add(struct parked_thread* head, struct parked_thread* item); + static void list_remove(struct parked_thread* head, struct parked_thread* item); + ++// Parked-thread nodes live in a fixed pool, not on the parked thread's own stack. The list is shared by ++// all workers, and a node on a stack that goes away while a neighbour is still linked turns the next ++// removal into a write to unmapped memory. Callers hold workq_parked_lock. ++#define WQ_NODE_POOL_SIZE (WQ_MAX_THREADS * 2) ++static struct parked_thread workq_node_pool[WQ_NODE_POOL_SIZE]; ++static unsigned char workq_node_used[WQ_NODE_POOL_SIZE]; ++ ++static struct parked_thread* workq_node_alloc(void) ++{ ++ for (int i = 0; i < WQ_NODE_POOL_SIZE; i++) ++ { ++ if (!workq_node_used[i]) ++ { ++ workq_node_used[i] = 1; ++ workq_node_pool[i].sem = 0; ++ workq_node_pool[i].flags = 0; ++ workq_node_pool[i].event = NULL; ++ return &workq_node_pool[i]; ++ } ++ } ++ return NULL; ++} ++ ++static void workq_node_free(struct parked_thread* node) ++{ ++ if (node >= &workq_node_pool[0] && node < &workq_node_pool[WQ_NODE_POOL_SIZE]) ++ workq_node_used[node - &workq_node_pool[0]] = 0; ++} ++ + struct timespec + { + long tv_sec; + long tv_nsec; + }; + +-void __attribute__((weak)) os_unfair_lock_unlock(os_unfair_lock_t lock) {} +-void __attribute__((weak)) os_unfair_lock_lock(os_unfair_lock_t lock) {} + + //void* __attribute__((weak)) __attribute__((visibility("default"))) pthread_getspecific(unsigned long key) { return NULL; } + //int __attribute__((weak)) __attribute__((visibility("default"))) pthread_setspecific(unsigned long key, const void* value) { return 1; } +@@ -95,6 +122,7 @@ static int priority_to_class(int prio); + // This is horrible, but it may work + static struct wq_kevent_data* wq_event_pending = NULL; + ++ + static void rpc_guard(int fd) { + guard_entry_options_t options; + options.close = __dserver_close_socket; +@@ -162,14 +190,24 @@ long sys_workq_kernreturn(int options, void* item, int affinity, int prio) + // and can be either recycled or terminated + + int thread_self; +- struct parked_thread me; ++ struct parked_thread stack_me; // only used if the node pool is exhausted ++ struct parked_thread* me; ++ struct wq_kevent_data* resume_event = NULL; ++ int resume_flags; + dthread_t dthread; + bool terminating = false; + +- os_unfair_lock_lock(&workq_parked_lock); ++ emulation_mutex_lock(&workq_parked_lock); ++ ++ me = workq_node_alloc(); ++ if (me == NULL) ++ { ++ me = &stack_me; ++ me->event = NULL; ++ } + + // Semaphore locked state (wait for wakeup) +- me.sem = 0; ++ me->sem = 0; + + // extract initial flags + // (in case we only get created and used once and then terminate; `_pthread_wqthread` requires a valid `flags` argument) +@@ -177,33 +215,37 @@ long sys_workq_kernreturn(int options, void* item, int affinity, int prio) + prio = _pthread_getspecific_direct(_PTHREAD_TSD_SLOT_PTHREAD_QOS_CLASS); + // doesn't extract `WQ_FLAG_THREAD_KEVENT` if we had it set, but that shouldn't matter + // like i said before, the only case where we actually need these flags to be set here is when the thread is going to die immediately after creation +- me.flags = extract_wq_flags(prio); ++ me->flags = extract_wq_flags(prio); + + // Enqueue for future WQOPS_QUEUE_REQTHREADS +- TAILQ_INSERT_HEAD(&workq_parked_head, &me, entries); ++ TAILQ_INSERT_HEAD(&workq_parked_head, me, entries); + + // Decrease the amount of running threads + sem_up(&workq_sem); + +- os_unfair_lock_unlock(&workq_parked_lock); ++ emulation_mutex_unlock(&workq_parked_lock); + + // Wait until someone calls WQOPS_QUEUE_REQTHREADS + // and wakes us up +- if (!sem_down(&me.sem, 15)) ++ if (!sem_down(&me->sem, 15)) + { + // Make sure we haven't just been woken up before locking the queue + // and remove us from the queue if not. +- os_unfair_lock_lock(&workq_parked_lock); ++ emulation_mutex_lock(&workq_parked_lock); + +- if (me.sem > 0) ++ if (me->sem > 0) + { +- os_unfair_lock_unlock(&workq_parked_lock); ++ emulation_mutex_unlock(&workq_parked_lock); + goto wakeup; + } + +- TAILQ_REMOVE(&workq_parked_head, &me, entries); ++ TAILQ_REMOVE(&workq_parked_head, me, entries); + +- os_unfair_lock_unlock(&workq_parked_lock); ++ resume_event = NULL; ++ resume_flags = me->flags; ++ workq_node_free(me); ++ ++ emulation_mutex_unlock(&workq_parked_lock); + + terminating = true; + +@@ -214,8 +256,15 @@ long sys_workq_kernreturn(int options, void* item, int affinity, int prio) + wakeup: // we actually want to go back into the thread to do work + // __simple_printf("Thread %d woken up, prio=%d\n", thread_self, me.flags & WQ_FLAG_THREAD_PRIOMASK); + +- if (me.event) +- wq_event_pending = me.event; ++ // the requester already took us off the list; the node is ours to read, then to give back ++ emulation_mutex_lock(&workq_parked_lock); ++ resume_event = me->event; ++ resume_flags = me->flags; ++ workq_node_free(me); ++ emulation_mutex_unlock(&workq_parked_lock); ++ ++ if (resume_event) ++ wq_event_pending = resume_event; + + resume_thread: // we want the thread to resume, but it might be just to die + // reset stack and call entry point again with WQ_FLAG_THREAD_REUSE +@@ -223,9 +272,9 @@ resume_thread: // we want the thread to resume, but it might be just to die + dthread = _pthread_getspecific_direct(_PTHREAD_TSD_SLOT_PTHREAD_SELF); + + wqueue_entry_point_asm_jump(dthread, thread_self, dthread->stackbottom, +- (!terminating && me.event) ? me.event->events : NULL, +- me.flags | WQ_FLAG_THREAD_REUSE, +- terminating ? WORKQ_EXIT_THREAD_NKEVENT : (me.event ? me.event->nevents : 0) ++ (!terminating && resume_event) ? resume_event->events : NULL, ++ resume_flags | WQ_FLAG_THREAD_REUSE, ++ terminating ? WORKQ_EXIT_THREAD_NKEVENT : (resume_event ? resume_event->nevents : 0) + ); + + __builtin_unreachable(); +@@ -257,7 +306,7 @@ resume_thread: // we want the thread to resume, but it might be just to die + // Increase the amount of running threads + sem_down(&workq_sem, -1); + +- os_unfair_lock_lock(&workq_parked_lock); ++ emulation_mutex_lock(&workq_parked_lock); + + if (workq_parked_head.tqh_first != NULL) + { +@@ -276,12 +325,12 @@ resume_thread: // we want the thread to resume, but it might be just to die + + // Resume the thread + sem_up(&thread->sem); +- os_unfair_lock_unlock(&workq_parked_lock); ++ emulation_mutex_unlock(&workq_parked_lock); + + continue; + } + +- os_unfair_lock_unlock(&workq_parked_lock); ++ emulation_mutex_unlock(&workq_parked_lock); + + // __simple_printf("Spawning a new thread, nevents=%d\n", (wq_event != NULL) ? wq_event->nevents : -1); + wq_event_pending = wq_event; +@@ -320,36 +369,52 @@ resume_thread: // we want the thread to resume, but it might be just to die + #endif + } + ++/* Keep a nonnegative token count. The old decrement/negative-marker/plain ++ * store sequence could overwrite a concurrent post, losing a worker wakeup. ++ * EAGAIN and EINTR are retry conditions, not worker retirement requests. */ + int sem_down(int* sem, int timeout) + { +- int result; +- struct timespec ts = { timeout, 0 }; +- +-try_again: +- result = __sync_add_and_fetch(sem, -1); +- if (result < 0) +- { +- *sem = -1; +- result = __linux_futex(sem, FUTEX_WAIT, -1, (timeout != -1) ? &ts : NULL, 0, 0); +- +- if (result == -1) +- return 0; +- +- goto try_again; +- } +- +- return 1; ++ struct timespec deadline = {0, 0}; ++ if (timeout >= 0) { ++ LINUX_SYSCALL(__NR_clock_gettime, 1 /* CLOCK_MONOTONIC */, &deadline); ++ deadline.tv_sec += timeout; ++ } ++ for (;;) { ++ int value = __atomic_load_n(sem, __ATOMIC_ACQUIRE); ++ while (value > 0) { ++ if (__atomic_compare_exchange_n(sem, &value, value - 1, 0, ++ __ATOMIC_ACQUIRE, __ATOMIC_RELAXED)) ++ return 1; ++ } ++ struct timespec remaining; ++ if (timeout >= 0) { ++ struct timespec now; ++ LINUX_SYSCALL(__NR_clock_gettime, 1 /* CLOCK_MONOTONIC */, &now); ++ remaining.tv_sec = deadline.tv_sec - now.tv_sec; ++ remaining.tv_nsec = deadline.tv_nsec - now.tv_nsec; ++ if (remaining.tv_nsec < 0) { ++ --remaining.tv_sec; ++ remaining.tv_nsec += 1000000000; ++ } ++ if (remaining.tv_sec < 0 || ++ (remaining.tv_sec == 0 && remaining.tv_nsec == 0)) ++ return 0; ++ } ++ long result = LINUX_SYSCALL(__NR_futex, sem, FUTEX_WAIT, 0, ++ timeout >= 0 ? &remaining : NULL, NULL, 0); ++ if (result == -110 /* ETIMEDOUT */) ++ return 0; ++ if (result < 0 && result != -11 /* EAGAIN */ && result != -4 /* EINTR */) ++ return 0; ++ } + } + + void sem_up(int* sem) + { +- int result; +- result = __sync_add_and_fetch(sem, 1); +- if (result == 0) +- { +- *sem = 1; +- __linux_futex(sem, FUTEX_WAKE, 1, NULL, 0, 0); +- } ++ __atomic_fetch_add(sem, 1, __ATOMIC_RELEASE); ++ /* Wake on every post: several waiters can be asleep while tokens are ++ * posted faster than the first awakened waiter can consume them. */ ++ LINUX_SYSCALL(__NR_futex, sem, FUTEX_WAKE, 1, NULL, NULL, 0); + } + + #define __PTHREAD_PRIORITY_CBIT_USER_INTERACTIVE 0x20 +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/guarded/guarded_close_np.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/guarded/guarded_close_np.c +index 8980d77..361efc4 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/guarded/guarded_close_np.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/guarded/guarded_close_np.c +@@ -7,6 +7,8 @@ + + long sys_guarded_close_np(int fd, uint64_t* guardid) + { +- *guardid = 0; ++ /* darling-lol M57: callers can pass a NULL or otherwise unusable guardid ++ * pointer; writing it crashed (SEGV on a read-only page). The guard id is ++ * not used by Darling's no-op fdguard implementation, so don't write it. */ + return sys_close(fd); + } +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/guarded/guarded_kqueue_np.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/guarded/guarded_kqueue_np.c +index bdedda8..1992482 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/guarded/guarded_kqueue_np.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/guarded/guarded_kqueue_np.c +@@ -7,6 +7,6 @@ + + long sys_guarded_kqueue_np(uint64_t* guardid, unsigned guardflags) + { +- *guardid = 0; ++ /* darling-lol M57: do not write guardid (may be unusable). */ + return sys_kqueue(); + } +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/guarded/guarded_open_np.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/guarded/guarded_open_np.c +index 0fe8a0f..5ae6ad0 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/guarded/guarded_open_np.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/guarded/guarded_open_np.c +@@ -7,6 +7,6 @@ + + long sys_guarded_open_np(const char* filename, uint64_t* guardid, unsigned int guardflags, int flags, unsigned int mode) + { +- *guardid = 0; ++ /* darling-lol M57: do not write guardid (may be unusable). */ + return sys_open(filename, flags, mode); + } +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/kqueue/kevent.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/kqueue/kevent.c +index 15d7b43..dea29b8 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/kqueue/kevent.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/kqueue/kevent.c +@@ -3,7 +3,8 @@ + #include + #include + +-#include ++#include ++#define LOLSYS(fmt, ...) do { } while (0) + #include + #include + +@@ -13,6 +14,7 @@ long sys_kevent(int kq, const struct kevent *changelist, int nchanges, + struct kevent *eventlist, int nevents, + const struct timespec *timeout) + { ++ LOLSYS("sys_kevent entered"); + int ret = kevent_impl(kq, changelist, nchanges, eventlist, nevents, timeout); + if (ret < 0) + ret = -errno; +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/kqueue/kevent64.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/kqueue/kevent64.c +index dd9bb0d..137f319 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/kqueue/kevent64.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/kqueue/kevent64.c +@@ -3,7 +3,8 @@ + #include + #include + +-#include ++#include ++#define LOLSYS(fmt, ...) do { } while (0) + #include + #include + +@@ -13,6 +14,7 @@ long sys_kevent64(int kq, const struct kevent64_s *changelist, int nchanges, + struct kevent64_s *eventlist, int nevents, unsigned int flags, + const struct timespec *timeout) + { ++ LOLSYS("sys_kevent64 entered"); + int ret = kevent64_impl(kq, changelist, nchanges, eventlist, nevents, flags, timeout); + if (ret < 0) + ret = -errno; +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/kqueue/kevent_qos.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/kqueue/kevent_qos.c +index 3f7cba6..bed91d7 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/kqueue/kevent_qos.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/kqueue/kevent_qos.c +@@ -4,7 +4,8 @@ + #include + #include + +-#include ++#include ++#define LOLSYS(fmt, ...) do { } while (0) + #include + #include + #include +@@ -15,6 +16,9 @@ + + static int default_kq = -1; + ++extern void* malloc(__SIZE_TYPE__ size); ++extern void free(void* ptr); ++ + extern void* memmove(void* dst, const void* src, __SIZE_TYPE__ n); + + static void kevent_qos_to_64(const struct kevent_qos_s *ev, struct kevent64_s* ev64); +@@ -29,6 +33,11 @@ long sys_kevent_qos(int kq, const struct kevent_qos_s *changelist, int nchanges, + struct kevent64_s* eventlist64 = NULL; + struct wq_kevent_data wq_kevent; + struct timespec polling_timeout = {}; ++ struct kevent64_s small_changes[16], small_events[16]; ++ struct kevent_qos_s workq_event; ++ void* allocated_changes = NULL; ++ void* allocated_events = NULL; ++ if(nchanges < 0 || nevents < 0) return -EINVAL; + + if ((kq == -1) != !!(flags & KEVENT_FLAG_WORKQ)) + return -EINVAL; +@@ -43,17 +52,28 @@ long sys_kevent_qos(int kq, const struct kevent_qos_s *changelist, int nchanges, + { + int i; + +- changelist64 = (struct kevent64_s*) __builtin_alloca(nchanges * sizeof(struct kevent64_s)); ++ if(nchanges <= 16) changelist64=small_changes; ++ else { ++ allocated_changes=malloc((size_t)nchanges*sizeof(struct kevent64_s)); ++ if(!allocated_changes) return -ENOMEM; ++ changelist64=allocated_changes; ++ } + for (i = 0; i < nchanges; i++) + kevent_qos_to_64(&changelist[i], &changelist64[i]); + } +- if (eventlist != NULL && nevents > 0) +- eventlist64 = (struct kevent64_s*) __builtin_alloca(nevents * sizeof(struct kevent64_s)); ++ if (eventlist != NULL && nevents > 0) { ++ if(nevents <= 16) eventlist64=small_events; ++ else { ++ allocated_events=malloc((size_t)nevents*sizeof(struct kevent64_s)); ++ if(!allocated_events) { free(allocated_changes);return -ENOMEM; } ++ eventlist64=allocated_events; ++ } ++ } + if (nevents == 0 && (flags & KEVENT_FLAG_WORKQ)) + { +- eventlist = (struct kevent_qos_s*) __builtin_alloca(nevents * sizeof(struct kevent_qos_s)); ++ eventlist = &workq_event; + nevents = 1; +- eventlist64 = (struct kevent64_s*) __builtin_alloca(nevents * sizeof(struct kevent64_s)); ++ eventlist64 = small_events; + } + + rv = sys_kevent64(kq, changelist64, nchanges, eventlist64, nevents, flags, (flags & KEVENT_FLAG_IMMEDIATE) ? &polling_timeout : NULL); +@@ -83,6 +103,8 @@ long sys_kevent_qos(int kq, const struct kevent_qos_s *changelist, int nchanges, + // __simple_printf("workq done processing, returning %d to caller\n", rv); + } + ++ free(allocated_events); ++ free(allocated_changes); + return rv; + } + +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/kqueue/kqueue.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/kqueue/kqueue.c +index e808529..bd663f6 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/kqueue/kqueue.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/kqueue/kqueue.c +@@ -3,7 +3,8 @@ + #include + #include + +-#include ++#include ++#define LOLSYS(fmt, ...) do { } while (0) + #include + #include + #include +@@ -16,6 +17,7 @@ int __attribute__((weak)) __attribute__((visibility("default"))) kqueue_impl(voi + + long sys_kqueue(void) + { ++ LOLSYS("sys_kqueue entered"); + int ret = kqueue_impl(); + if (ret < 0) + ret = -errno; +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/fileport_makefd.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/fileport_makefd.c +index 1afb57e..d6896e6 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/fileport_makefd.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/fileport_makefd.c +@@ -3,14 +3,20 @@ + #include + #include + #include +-#include ++#include ++ ++extern int dserver_rpc_fileport_makefd(unsigned int port_name, int* fd); + + long sys_fileport_makefd(int port) + { +- int ret = lkm_call(NR_fileport_makefd, (void*)(unsigned long) port); ++ int fd = -1; ++ int ret; + +- if (ret < 0) +- return errno_linux_to_bsd(ret); ++ /* darling-lol M54: see fileport_makeport.c. */ ++ ret = dserver_rpc_fileport_makefd((unsigned int)port, &fd); ++ if (ret < 0) { ++ return -ret; ++ } + +- return ret; ++ return fd; + } +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/fileport_makeport.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/fileport_makeport.c +index be2926c..a25882e 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/fileport_makeport.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/fileport_makeport.c +@@ -3,20 +3,22 @@ + #include + #include + #include +-#include ++#include ++ ++extern int dserver_rpc_fileport_makeport(int fd, unsigned int* port_name); + + long sys_fileport_makeport(int fd, int* port) + { +- struct fileport_makeport_args args; +- +- args.fd = fd; +- args.port_out = 0; +- +- int ret = lkm_call(NR_fileport_makeport, &args); ++ unsigned int port_name = 0; ++ int ret; + +- if (ret < 0) +- return errno_linux_to_bsd(ret); ++ /* darling-lol M54: the old LKM API is gone (its stub UD2s). Route ++ * through darlingserver, which holds the FD in a fileport kobject. */ ++ ret = dserver_rpc_fileport_makeport(fd, &port_name); ++ if (ret < 0) { ++ return -ret; ++ } + +- *port = args.port_out; ++ *port = (int)port_name; + return 0; + } +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/mac_syscall.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/mac_syscall.c +new file mode 100644 +index 0000000..479294a +--- /dev/null ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/mac_syscall.c +@@ -0,0 +1,53 @@ ++#include ++ ++#include ++#include ++#include ++ ++extern int memcmp(const void* s1, const void* s2, __SIZE_TYPE__ n); ++extern __SIZE_TYPE__ strlen(const char* s); ++ ++long sys___mac_syscall(const char* policy, int call, void* arg) ++{ ++ char name[32]; ++ __SIZE_TYPE__ i; ++ ++ /* darling-lol: syscall 381 was unmapped (ENOSYS), and Chromium's ++ * seatbelt client CHECKs it, killing every helper/GPU process with a ++ * silent UD2. There is no MAC framework to back this; for the ++ * "Seatbelt" policy we fail soft with success (consistent with the ++ * sandbox_init stubs that report success without enforcement). ++ * Anything else keeps ENOSYS. Guest memory is directly readable here; ++ * bound the copy. */ ++ ++ if (policy == NULL) { ++ return -EINVAL; ++ } ++ ++ for (i = 0; i < sizeof(name) - 1; i++) { ++ char c = policy[i]; ++ name[i] = c; ++ if (c == '\0') { ++ break; ++ } ++ } ++ name[sizeof(name) - 1] = '\0'; ++ ++ if (memcmp(name, "Seatbelt", 9) == 0) { ++ __simple_printf("mac_syscall: Seatbelt call %d (arg=%p), no enforcement, succeeding\n", ++ call, arg); ++ return 0; ++ } ++ ++ if (memcmp(name, "AMFI", 5) == 0) { ++ /* darling-lol: same posture as Seatbelt (Chromium's sandbox code ++ * probes AMFI status via ___sandbox_ms; ENOSYS trips its CHECK). ++ * No code-signing enforcement exists to report on. */ ++ __simple_printf("mac_syscall: AMFI call %d (arg=%p), no enforcement, succeeding\n", ++ call, arg); ++ return 0; ++ } ++ ++ __simple_printf("mac_syscall: unsupported policy \"%s\" call %d\n", name, call); ++ return -ENOSYS; ++} +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/proc_info.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/proc_info.c +index c2f70fb..af904ed 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/proc_info.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/proc_info.c +@@ -83,6 +83,38 @@ long sys_proc_info(uint32_t callnum, int32_t pid, uint32_t flavor, + case 2: // proc_pidinfo + return _proc_pidinfo(pid, flavor, arg, buffer, bufsize); + case 3: // proc_pidfdinfo ++ { ++ if (flavor == PROC_PIDFDVNODEPATHINFO) ++ { ++ struct vnode_fdinfowithpath* info = (struct vnode_fdinfowithpath*) buffer; ++ struct vchroot_unexpand_args uargs; ++ char procpath[64]; ++ long len; ++ ++ if (!buffer) ++ return -EFAULT; ++ if (bufsize < (int32_t)sizeof(*info)) ++ return -ENOSPC; ++ ++ memset(buffer, 0, bufsize); ++ ++ /* Resolve /proc//fd/ to the emulated path. */ ++ __simple_sprintf(procpath, "/proc/%d/fd/%d", pid, (int)arg); ++ len = sys_readlink(procpath, uargs.path, sizeof(uargs.path) - 1); ++ if (len < 0) ++ return errno_linux_to_bsd(len); ++ uargs.path[len] = 0; ++ ++ if (vchroot_unexpand(&uargs) != 0) ++ return -EINVAL; ++ ++ strncpy(info->pvip.vip_path, uargs.path, sizeof(info->pvip.vip_path) - 1); ++ return sizeof(*info); ++ } ++ __simple_printf("sys_proc_info(): Unsupported pidfdinfo flavor: %d\n", ++ flavor); ++ return -ENOTSUP; ++ } + case 1: // proc_listpids + default: + __simple_printf("sys_proc_info(): Unsupported callnum: %d\n", +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/ptrace.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/ptrace.c +index 0a8f1e4..55aadbe 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/ptrace.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/ptrace.c +@@ -171,6 +171,17 @@ long sys_ptrace(int request, int pid, void* addr, int data) + // return ret; + cmd = "PT_THUPDATE"; break; + } ++ case XNU_PT_DENY_ATTACH: ++ { ++ // Apple semantics: this process can no longer be traced or ++ // debugged; always succeeds. Darling has no kernel debugger ++ // support to disable, but the call itself must report success: ++ // anti-tamper code treats any other return as failure. ++ // (darling-lol: LeagueClient dies right after this call when ++ // it returns stack garbage instead of 0.) ++ ret = 0; ++ cmd = "PT_DENY_ATTACH"; break; ++ } + } + + if (cmd != NULL) +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/sysctl.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/sysctl.c +index 10f39a5..29747d4 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/sysctl.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/misc/sysctl.c +@@ -42,11 +42,15 @@ const struct known_sysctl sysctls_root = { + + void copyout_string(const char* str, char* out, unsigned long* out_len) + { +- unsigned long len; ++ // as on XNU, the reported length includes the terminating NUL ++ unsigned long len = strlen(str) + 1; ++ + if (out != NULL) +- len = strlcpy(out, str, *out_len); +- else +- len = strlen(str); ++ { ++ strlcpy(out, str, *out_len); ++ if (len > *out_len) ++ len = *out_len; ++ } + + if (out_len != NULL) + *out_len = len; +@@ -130,7 +134,10 @@ long sys_sysctlbyname(const char* name, unsigned long namelen, void* old, unsign + } + + if (next == NULL) ++ { ++ __simple_kprintf("sysctlbyname: unknown name %s", name); + return -ENOENT; ++ } + + current = next; + token = strtok_r(NULL, ".", &saveptr); +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/network/accept.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/network/accept.c +index 4daa631..5aeedd0 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/network/accept.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/network/accept.c +@@ -34,9 +34,16 @@ long sys_accept_nocancel(int fd, void* from, int* socklen) + ret = errno_linux_to_bsd(ret); + else + { +- fixed = (struct sockaddr_fixup*) from; +- fixed->bsd_family = sfamily_linux_to_bsd(fixed->linux_family); +- fixed->bsd_length = *socklen; ++ /* darling-lol M57: accept(fd, NULL, NULL) is legal on macOS (the ++ * caller isn't interested in the peer address). Chromium's socket ++ * code does this; dereferencing NULL crashed in ++ * sys_accept_nocancel. */ ++ if (from != 0 && socklen != 0) ++ { ++ fixed = (struct sockaddr_fixup*) from; ++ fixed->bsd_family = sfamily_linux_to_bsd(fixed->linux_family); ++ fixed->bsd_length = *socklen; ++ } + + // on macOS (and BSD), accept-ed sockets inherit O_NONBLOCK + int parent_flags = sys_fcntl_nocancel(fd, F_GETFL, 0); +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/network/recvfrom.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/network/recvfrom.c +index 1419bed..ea83e75 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/network/recvfrom.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/network/recvfrom.c +@@ -11,6 +11,8 @@ + #include + + extern void *memcpy(void *dest, const void *src, __SIZE_TYPE__ n); ++extern void rclol_trace_io(const char* op, int fd, long ret); ++ + + long sys_recvfrom(int fd, void* buf, unsigned long len, + int flags, void* from, int* socklen) +@@ -31,8 +33,11 @@ long sys_recvfrom_nocancel(int fd, void* buf, unsigned long len, + ret = LINUX_SYSCALL(__NR_socketcall, LINUX_SYS_RECVFROM, ((long[6]) { fd, buf, len, + flags, from, socklen })); + #else +- ret = LINUX_SYSCALL(__NR_recvfrom, fd, buf, len, flags, from, socklen); ++ /* this passed the unconverted BSD flags: BSD MSG_DONTWAIT (0x80) is Linux MSG_EOR and BSD MSG_WAITALL (0x40) is ++ * Linux MSG_DONTWAIT. sendto/sendmsg/recvmsg already used the converted value. */ ++ ret = LINUX_SYSCALL(__NR_recvfrom, fd, buf, len, linux_flags, from, socklen); + #endif ++ rclol_trace_io("recvfrom", fd, ret); + + if (ret < 0) + ret = errno_linux_to_bsd(ret); +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/network/recvmsg.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/network/recvmsg.c +index 927ccbb..ae353d3 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/network/recvmsg.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/network/recvmsg.c +@@ -17,6 +17,8 @@ extern void* memcpy(void* dest, const void* src, __SIZE_TYPE__ len); + + #define ALIGN(what, how) ( ((what) + (how) - 1) & ~((how) - 1) ) + ++extern void rclol_trace_io(const char* op, int fd, long ret); ++ + long sys_recvmsg(int socket, struct bsd_msghdr* msg, int flags) + { + CANCELATION_POINT(); +@@ -62,6 +64,7 @@ long sys_recvmsg_nocancel(int socket, struct bsd_msghdr* msg, int flags) + #else + ret = LINUX_SYSCALL(__NR_recvmsg, socket, &lmsg, linux_flags); + #endif ++ rclol_trace_io("recvmsg", socket, ret); + + if (ret < 0) + ret = errno_linux_to_bsd(ret); +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/process/fork.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/process/fork.c +index 76b6f53..478e158 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/process/fork.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/process/fork.c +@@ -4,6 +4,7 @@ + + #include + #include ++#include + #include + #include + #include +@@ -18,18 +19,21 @@ + + extern _libkernel_functions_t _libkernel_functions; + +-long sys_fork(void) ++/* The guest syscall trampoline does not promise ELF stack alignment. */ ++__attribute__((force_align_arg_pointer)) long sys_fork(void) + { + int ret; + int wdfd = get_perthread_wd(); + +-#ifdef SYS_fork +- ret = LINUX_SYSCALL(__NR_fork); +-#else +- ret = LINUX_SYSCALL(__NR_clone, LINUX_SIGCHLD, 0); +-#endif +- if (ret < 0) +- ret = errno_linux_to_bsd(ret); ++ /* The child immediately uses host malloc while refreshing RPC sockets. ++ * A raw fork inherits locked glibc arenas from vanished host threads. ++ * Use the ELF fork, whose atfork machinery prepares those arenas and ++ * the loader's socket bitmap. Guest libsystem callbacks remain separate. */ ++ extern struct elf_calls* elfcalls(void); ++ ret = elfcalls()->fork(); ++ if (ret < 0) { ++ ret = errno_linux_to_bsd(-elfcalls()->get_errno()); ++ } + else if (ret == 0) + { + // in the child +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/psynch/psynch_cvwait.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/psynch/psynch_cvwait.c +index d093b73..b5d0ab1 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/psynch/psynch_cvwait.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/psynch/psynch_cvwait.c +@@ -6,12 +6,28 @@ + #include + + #include ++#include + + long sys_psynch_cvwait(void* cv, uint64_t cvlsgen, uint32_t cvugen, void * mutex, uint64_t mugen, + uint32_t flags, int64_t sec, uint32_t nsec) + { + uint32_t retval; ++ { ++ /* darling-lol: the kernel drops the mutex for the caller before it waits (kern_synch.c _psynch_cvwait). A mutex ++ * whose waiters are queued in this process (psynch_mutexwait.c) has to be dropped here; the server knows ++ * nothing about it. */ ++ extern int __darling_local_mutex_usable(uint32_t flags); ++ extern uint32_t __darling_local_mutexdrop(void* mutex, uint32_t mgen, uint32_t ugen, uint32_t flags); ++ if (mutex != 0 && __darling_local_mutex_usable(flags)) { ++ __darling_local_mutexdrop(mutex, (uint32_t) (mugen & 0xffffffffu), (uint32_t) (mugen >> 32), flags); ++ mutex = 0; ++ } ++ } ++ unsigned long long slowStart = slowwait_enabled() ? slowwait_now() : 0; + int ret = dserver_rpc_psynch_cvwait(cv, cvlsgen, cvugen, mutex, mugen, flags, sec, nsec, &retval); ++ if (slowStart) { ++ slowwait_log("psynch_cvwait", (unsigned int)(unsigned long) cv, slowStart, (void**) __builtin_frame_address(0), flags, (unsigned int) sec); ++ } + + if (ret < 0) { + if (ret == -LINUX_EINTR) { +@@ -21,5 +37,8 @@ long sys_psynch_cvwait(void* cv, uint64_t cvlsgen, uint32_t cvugen, void * mutex + __simple_abort(); + } + +- return (ret) ? ret : retval; ++ /* RPC ret is a Darwin errno (possibly carrying condition-variable ++ * cleanup flags), not successful sequence bits. The syscall trampoline ++ * recognizes negative errors and sets errno for libpthread's -1 path. */ ++ return (ret) ? -(long)ret : (long)retval; + } +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/psynch/psynch_mutexdrop.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/psynch/psynch_mutexdrop.c +index 1799f60..d6f4879 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/psynch/psynch_mutexdrop.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/psynch/psynch_mutexdrop.c +@@ -8,6 +8,11 @@ + + long sys_psynch_mutexdrop(void* mutex, uint32_t mgen, uint32_t ugen, uint64_t tid, uint32_t flags) + { ++ extern int __darling_local_mutex_usable(uint32_t flags); ++ extern uint32_t __darling_local_mutexdrop(void* mutex, uint32_t mgen, uint32_t ugen, uint32_t flags); ++ if (__darling_local_mutex_usable(flags)) ++ return __darling_local_mutexdrop(mutex, mgen, ugen, flags); ++ + uint32_t retval; + int ret = dserver_rpc_psynch_mutexdrop(mutex, mgen, ugen, tid, flags, &retval); + +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/psynch/psynch_mutexwait.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/psynch/psynch_mutexwait.c +index 2a05251..4893775 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/psynch/psynch_mutexwait.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/psynch/psynch_mutexwait.c +@@ -7,11 +7,273 @@ + #include + + #include ++#include ++ ++/* darling-lol: contended first-fit pthread mutexes handled inside the process. ++ * ++ * libpthread locks and unlocks an uncontended mutex in user space. When threads collide, the loser calls ++ * psynch_mutexwait to sleep and the unlocker calls psynch_mutexdrop to hand the lock on; both were round trips to ++ * darlingserver (~45 us each, with the caller asleep). In League the hot one is the game's own allocator mutex, taken ++ * by the main thread and the workers all the time. ++ * ++ * This is the kernel's algorithm for the FIRSTFIT policy (libpthread/kern/kern_synch.c: _psynch_mutexwait, ++ * _psynch_mutexdrop_internal, ksyn_queue_insert/remove_item, update_low_high) with the same sequence bookkeeping and the ++ * same return values, minus what only matters inside a kernel (owner tracking for priority inheritance, interruptible ++ * waits). A waiter sleeps on a futex. Fair-share and process-shared mutexes, condition variables and rwlocks still go ++ * to the server; psynch_cvwait performs the mutex drop here first when the mutex is a local one. ++ * ++ * Decided once per process: the League game process only, and only with the host file /tmp/rclol-local-mutex ++ * (opt-in while it is being validated). /tmp/rclol-local-mutex-all enables it in every process (probe tests). */ ++#define LMTX_COUNT_MASK 0xffffff00u ++#define LMTX_KBIT 0x01u ++#define LMTX_EBIT 0x02u ++#define LMTX_MTX_WAIT 0x20u ++#define LMTX_POLICY_MASK 0x1c0u ++#define LMTX_POLICY_FIRSTFIT 0x80u ++#define LMTX_PSHARED_MASK 0x30u ++#define LMTX_PSHARED 0x10u ++#define LMTX_BUCKETS 256u ++#define LMTX_POOL 4096u ++ ++struct lmtx_waiter { ++ struct lmtx_waiter* next; ++ uint32_t lockseq; ++ uint32_t retval; ++ volatile int woken; ++}; ++ ++struct lmtx { ++ struct lmtx* next; /* bucket chain / free list */ ++ unsigned long addr; ++ struct lmtx_waiter* head; ++ struct lmtx_waiter* tail; ++ uint32_t inqueue; ++ uint32_t prepost; ++ uint32_t firstnum, lastnum; /* sequence of the first and of the last queued waiter, as the kernel keeps them */ ++ uint32_t lowseq, highseq; ++}; ++ ++static struct lmtx lmtx_pool[LMTX_POOL]; ++static struct lmtx* lmtx_free; ++static unsigned int lmtx_pool_used; ++static struct lmtx* lmtx_buckets[LMTX_BUCKETS]; ++static volatile int lmtx_locks[LMTX_BUCKETS]; ++static volatile int lmtx_pool_lock; ++static volatile int lmtx_state; /* 0 undecided, 1 on, -1 off */ ++static volatile int lmtx_pid; ++ ++extern int __darling_is_league_game(void); ++ ++static inline int lmtx_seq_lower(uint32_t x, uint32_t y) ++{ ++ x &= LMTX_COUNT_MASK; y &= LMTX_COUNT_MASK; ++ return x < y ? (y - x) < (LMTX_COUNT_MASK / 2) : (x - y) > (LMTX_COUNT_MASK / 2); ++} ++ ++static inline int lmtx_seq_higher(uint32_t x, uint32_t y) ++{ ++ x &= LMTX_COUNT_MASK; y &= LMTX_COUNT_MASK; ++ return x > y ? (x - y) < (LMTX_COUNT_MASK / 2) : (y - x) > (LMTX_COUNT_MASK / 2); ++} ++ ++/* Three-state lock (0 free, 1 held, 2 held with sleepers). A thread that has slept must take the lock as "2": it ++ * cannot know whether others are still asleep, and taking it as "1" made the next unlock skip the wake-up and left ++ * them sleeping forever (the first version of this did exactly that and hung the stress test). */ ++static void lmtx_lock(volatile int* lock) ++{ ++ int c = 0; ++ for (int spin = 0; spin < 64; ++spin) { ++ c = 0; ++ if (__atomic_compare_exchange_n(lock, &c, 1, 0, __ATOMIC_ACQUIRE, __ATOMIC_RELAXED)) ++ return; ++ __asm__ volatile("pause"); ++ } ++ if (c != 2) ++ c = __atomic_exchange_n(lock, 2, __ATOMIC_ACQUIRE); ++ while (c != 0) { ++ LINUX_SYSCALL(__NR_futex, lock, 0 /* FUTEX_WAIT */, 2, 0UL, 0UL, 0); ++ c = __atomic_exchange_n(lock, 2, __ATOMIC_ACQUIRE); ++ } ++} ++ ++static void lmtx_unlock(volatile int* lock) ++{ ++ if (__atomic_exchange_n(lock, 0, __ATOMIC_RELEASE) == 2) ++ LINUX_SYSCALL(__NR_futex, lock, 1 /* FUTEX_WAKE */, 1, 0UL, 0UL, 0); ++} ++ ++static inline unsigned int lmtx_bucket(unsigned long addr) ++{ ++ return (unsigned int) ((addr >> 4) * 2654435761u) >> 24; ++} ++ ++__attribute__((visibility("hidden"))) ++int __darling_local_mutex_usable(uint32_t flags) ++{ ++ int state = __atomic_load_n(&lmtx_state, __ATOMIC_ACQUIRE); ++ if (state == 0) { ++ if (LINUX_SYSCALL(__NR_access, "/tmp/rclol-local-mutex-all", 0) == 0) ++ state = 1; ++ else if (LINUX_SYSCALL(__NR_access, "/tmp/rclol-local-mutex", 0) == 0 && __darling_is_league_game()) ++ state = 1; ++ else ++ state = -1; ++ __atomic_store_n(&lmtx_pid, (int) LINUX_SYSCALL(__NR_getpid), __ATOMIC_RELAXED); ++ __atomic_store_n(&lmtx_state, state, __ATOMIC_RELEASE); ++ } ++ if (state != 1) ++ return 0; ++ if ((flags & LMTX_POLICY_MASK) != LMTX_POLICY_FIRSTFIT || (flags & LMTX_PSHARED_MASK) == LMTX_PSHARED) ++ return 0; ++ /* a forked child starts with empty queues: the threads recorded as waiting only exist in the parent */ ++ int pid = (int) LINUX_SYSCALL(__NR_getpid); ++ if (__atomic_load_n(&lmtx_pid, __ATOMIC_RELAXED) != pid) { ++ for (unsigned int i = 0; i < LMTX_BUCKETS; ++i) { lmtx_buckets[i] = 0; lmtx_locks[i] = 0; } ++ lmtx_free = 0; lmtx_pool_used = 0; lmtx_pool_lock = 0; ++ __atomic_store_n(&lmtx_pid, pid, __ATOMIC_RELAXED); ++ } ++ return 1; ++} ++ ++/* caller holds the bucket lock */ ++static struct lmtx* lmtx_find(unsigned int bucket, unsigned long addr, int create) ++{ ++ struct lmtx* m; ++ for (m = lmtx_buckets[bucket]; m; m = m->next) ++ if (m->addr == addr) ++ return m; ++ if (!create) ++ return 0; ++ lmtx_lock(&lmtx_pool_lock); ++ m = lmtx_free; ++ if (m) ++ lmtx_free = m->next; ++ else if (lmtx_pool_used < LMTX_POOL) ++ m = &lmtx_pool[lmtx_pool_used++]; ++ lmtx_unlock(&lmtx_pool_lock); ++ if (!m) { ++ __simple_printf("darling-lol: local mutex table exhausted (%u entries)\n", LMTX_POOL); ++ __simple_abort(); ++ } ++ m->addr = addr; ++ m->head = m->tail = 0; ++ m->inqueue = m->prepost = 0; ++ m->firstnum = m->lastnum = m->lowseq = m->highseq = 0; ++ m->next = lmtx_buckets[bucket]; ++ lmtx_buckets[bucket] = m; ++ return m; ++} ++ ++/* caller holds the bucket lock; an entry with no waiters and no pending hand-off is back in its initial state */ ++static void lmtx_release_if_unused(unsigned int bucket, struct lmtx* m) ++{ ++ if (m->inqueue != 0 || m->prepost != 0) ++ return; ++ struct lmtx** link = &lmtx_buckets[bucket]; ++ while (*link && *link != m) ++ link = &(*link)->next; ++ if (*link) ++ *link = m->next; ++ m->addr = 0; ++ lmtx_lock(&lmtx_pool_lock); ++ m->next = lmtx_free; ++ lmtx_free = m; ++ lmtx_unlock(&lmtx_pool_lock); ++} ++ ++__attribute__((visibility("hidden"))) ++uint32_t __darling_local_mutexdrop(void* mutex, uint32_t mgen, uint32_t ugen, uint32_t flags) ++{ ++ const unsigned long addr = (unsigned long) mutex; ++ const unsigned int bucket = lmtx_bucket(addr); ++ (void) mgen; (void) ugen; (void) flags; ++ lmtx_lock(&lmtx_locks[bucket]); ++ struct lmtx* m = lmtx_find(bucket, addr, 1); ++ if (m->inqueue == 0) { ++ /* nobody is queued yet: the contender that made the unlocker come here is still on its way */ ++ m->prepost++; ++ lmtx_unlock(&lmtx_locks[bucket]); ++ return 0; ++ } ++ const uint32_t updatebits = (m->highseq & LMTX_COUNT_MASK) | (LMTX_EBIT | LMTX_KBIT); ++ struct lmtx_waiter* w = m->head; ++ m->head = w->next; ++ if (!m->head) ++ m->tail = 0; ++ if (--m->inqueue > 0) { ++ /* ksyn_queue_remove_item: first/last sequence from the queue ends, then low/high */ ++ m->firstnum = m->head->lockseq; ++ m->lastnum = m->tail->lockseq; ++ if (m->lowseq == w->lockseq) m->lowseq = m->firstnum; ++ if (m->highseq == w->lockseq) m->highseq = m->lastnum; ++ } else { ++ m->firstnum = m->lastnum = m->lowseq = m->highseq = 0; ++ } ++ w->retval = updatebits; ++ volatile int* wake = &w->woken; ++ lmtx_release_if_unused(bucket, m); ++ __atomic_store_n(wake, 1, __ATOMIC_RELEASE); /* the waiter's stack frame may be gone right after this */ ++ lmtx_unlock(&lmtx_locks[bucket]); ++ LINUX_SYSCALL(__NR_futex, wake, 1 /* FUTEX_WAKE */, 1, 0UL, 0UL, 0); ++ return 0; ++} ++ ++__attribute__((visibility("hidden"))) ++uint32_t __darling_local_mutexwait(void* mutex, uint32_t mgen, uint32_t ugen, uint32_t flags) ++{ ++ const unsigned long addr = (unsigned long) mutex; ++ const unsigned int bucket = lmtx_bucket(addr); ++ const uint32_t lseq = mgen & LMTX_COUNT_MASK; ++ (void) ugen; (void) flags; ++ lmtx_lock(&lmtx_locks[bucket]); ++ struct lmtx* m = lmtx_find(bucket, addr, 1); ++ if (m->prepost) { ++ /* an unlock already arrived for a contender: take it */ ++ m->prepost--; ++ uint32_t updatebits = (m->inqueue == 0 ? lseq : (m->highseq & LMTX_COUNT_MASK)) | (LMTX_KBIT | LMTX_EBIT); ++ updatebits &= ~LMTX_MTX_WAIT; ++ lmtx_release_if_unused(bucket, m); ++ lmtx_unlock(&lmtx_locks[bucket]); ++ return updatebits; ++ } ++ struct lmtx_waiter self; ++ self.next = 0; ++ self.lockseq = lseq; ++ self.retval = 0; ++ self.woken = 0; ++ /* ksyn_queue_insert, FIRSTFIT: arrival order */ ++ if (m->inqueue == 0) { ++ m->head = m->tail = &self; ++ m->firstnum = m->lastnum = lseq; ++ } else { ++ m->tail->next = &self; ++ m->tail = &self; ++ if (lmtx_seq_lower(lseq, m->firstnum)) m->firstnum = lseq; ++ if (lmtx_seq_higher(lseq, m->lastnum)) m->lastnum = lseq; ++ } ++ if (++m->inqueue == 1) { ++ m->lowseq = m->highseq = lseq; ++ } else { ++ if (lmtx_seq_lower(lseq, m->lowseq)) m->lowseq = lseq; ++ if (lmtx_seq_higher(lseq, m->highseq)) m->highseq = lseq; ++ } ++ lmtx_unlock(&lmtx_locks[bucket]); ++ while (!__atomic_load_n(&self.woken, __ATOMIC_ACQUIRE)) ++ LINUX_SYSCALL(__NR_futex, &self.woken, 0 /* FUTEX_WAIT */, 0, 0UL, 0UL, 0); ++ return self.retval & ~LMTX_MTX_WAIT; ++} + + long sys_psynch_mutexwait(void* mutex, uint32_t mgen, uint32_t ugen, uint64_t tid, uint32_t flags) + { ++ if (__darling_local_mutex_usable(flags)) ++ return __darling_local_mutexwait(mutex, mgen, ugen, flags); ++ + uint32_t retval; ++ unsigned long long slowStart = slowwait_enabled() ? slowwait_now() : 0; + int ret = dserver_rpc_psynch_mutexwait(mutex, mgen, ugen, tid, flags, &retval); ++ if (slowStart) { ++ slowwait_log("psynch_mutexwait", (unsigned int)(unsigned long) mutex, slowStart, (void**) __builtin_frame_address(0), flags, 0); ++ } + + if (ret < 0) { + if (ret == -LINUX_EINTR) { +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/psynch/ulock_wait.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/psynch/ulock_wait.c +index f26ec28..2f31473 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/psynch/ulock_wait.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/psynch/ulock_wait.c +@@ -3,6 +3,7 @@ + #include + #include + #include ++#include + + #include + #include +@@ -60,3 +61,42 @@ long sys_ulock_wait(uint32_t operation, void* addr, uint64_t value, uint32_t tim + + return ret; + } ++ ++/* Nanosecond variant used by libpthread's private condition variables. The ++ * sequence check and sleep are atomic in FUTEX_WAIT; a signal between the ++ * userspace unlock and this call therefore cannot be lost. */ ++long sys_ulock_wait2(uint32_t operation, void* addr, uint64_t value, ++ uint64_t timeout, uint64_t value2) ++{ ++ const uint32_t allowed = XNU_UL_OPCODE_MASK | XNU_ULF_NO_ERRNO | ++ 0x00010000u /* WORKQ_DATA_CONTENTION */ | 0x00020000u /* CANCEL_POINT */ | ++ 0x00040000u /* ADAPTIVE_SPIN */; ++ int op = operation & XNU_UL_OPCODE_MASK; ++ int ret; ++ if ((operation & ~allowed) || value2 || ++ (op != XNU_UL_COMPARE_AND_WAIT && op != XNU_UL_UNFAIR_LOCK) || ++ ((uintptr_t)addr & 3)) { ++ ret = -EINVAL; ++ } else { ++ int cancellation = (operation & 0x00020000u) ? sys_pthread_canceled(0) : -EINVAL; ++ if (cancellation == 0) { ++ ret = -EINTR; ++ } else { ++ /* Darling's server does not implement markcancel/canceled yet. ++ * Bound cancelable sleeps in that case so libpthread can observe ++ * its pending cancellation bit. EINTR is a permitted spurious ++ * condition wake, not a premature timed-wait timeout. */ ++ bool cancellation_poll = cancellation == -ENOSYS && ++ (!timeout || timeout > 50000000ull); ++ uint64_t sleep_ns = cancellation_poll ? 50000000ull : timeout; ++ struct timespec ts = { sleep_ns / 1000000000ull, sleep_ns % 1000000000ull }; ++ ret = LINUX_SYSCALL(__NR_futex, addr, FUTEX_WAIT | FUTEX_PRIVATE_FLAG, ++ (uint32_t)value, sleep_ns ? &ts : NULL, 0UL, 0); ++ if (ret == -LINUX_ETIMEDOUT && cancellation_poll) ret = -LINUX_EINTR; ++ if (ret == 0 || ret == -LINUX_EAGAIN) ret = 1; ++ else if (ret < 0) ret = errno_linux_to_bsd(ret); ++ } ++ } ++ if (ret < 0 && (operation & XNU_ULF_NO_ERRNO)) ret &= ~0x800; ++ return ret; ++} +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/select/select.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/select/select.c +index c3c33f9..831e8cb 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/select/select.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/select/select.c +@@ -7,6 +7,168 @@ + #include + #include + ++#include ++ ++/* darling-lol: opt-in trace for select() busy loops (a thread whose select keeps returning "ready" without ++ * anything consuming the descriptor). Enabled when the host file /tmp/rclol-select-trace exists at the first select ++ * of a process; output is appended to the host /tmp/rclol-select-trace.log. After 5000 consecutive "ready" returns it ++ * logs which descriptors were ready and marks the first readable one; read/recv/recvmsg/close on that descriptor are ++ * then logged too (descriptor numbers and return values only, never data). */ ++int rclol_trace_fd = -1; ++static int rclol_trace_state = -1; /* -1 unknown, 0 off, 1 on */ ++static unsigned rclol_trace_lines = 0; ++ ++static int rclol_trace_enabled(void) ++{ ++ if (rclol_trace_state < 0) ++ rclol_trace_state = LINUX_SYSCALL(__NR_access, "/tmp/rclol-select-trace", 0) == 0; ++ return rclol_trace_state; ++} ++ ++static void rclol_trace_write(const char* line) ++{ ++ unsigned long length = 0; ++ int fd; ++ if (rclol_trace_lines >= 400) ++ return; ++ ++rclol_trace_lines; ++ fd = LINUX_SYSCALL(__NR_open, "/tmp/rclol-select-trace.log", 0x441 /* O_WRONLY | O_CREAT | O_APPEND */, 0644); ++ if (fd < 0) ++ return; ++ while (line[length]) ++length; ++ LINUX_SYSCALL(__NR_write, fd, line, length); ++ LINUX_SYSCALL1(__NR_close, fd); ++} ++ ++void rclol_trace_io(const char* op, int fd, long ret) ++{ ++ char line[128]; ++ if (rclol_trace_state != 1 || fd != rclol_trace_fd || fd < 0) ++ return; ++ __simple_sprintf(line, "io pid=%d tid=%d %s fd=%d ret=%ld\n", (int) LINUX_SYSCALL0(__NR_getpid), ++ (int) LINUX_SYSCALL0(__NR_gettid), op, fd, ret); ++ rclol_trace_write(line); ++} ++ ++static void rclol_trace_select(int nfds, const unsigned* rfds, const unsigned* wfds, const unsigned* efds, ++ struct bsd_timeval* timeout, int ret) ++{ ++ static unsigned streak = 0; ++ char line[512]; ++ int used, fd, listed = 0, first_readable = -1; ++ ++ if (ret <= 0) { ++ streak = 0; ++ return; ++ } ++ if (++streak != 5000 && streak % 100000 != 0) ++ return; ++ ++ used = __simple_sprintf(line, "select pid=%d tid=%d streak=%u nfds=%d ret=%d timeout=%lds+%dus ready:", ++ (int) LINUX_SYSCALL0(__NR_getpid), (int) LINUX_SYSCALL0(__NR_gettid), streak, nfds, ret, ++ timeout ? (long) timeout->tv_sec : -1L, timeout ? (int) timeout->tv_usec : 0); ++ for (fd = 0; fd < nfds && fd < 1024 && listed < 12; ++fd) { ++ int r = rfds && (rfds[fd / 32] >> (fd % 32) & 1); ++ int w = wfds && (wfds[fd / 32] >> (fd % 32) & 1); ++ int e = efds && (efds[fd / 32] >> (fd % 32) & 1); ++ if (!r && !w && !e) ++ continue; ++ used += __simple_sprintf(line + used, " %d%s%s%s", fd, r ? "r" : "", w ? "w" : "", e ? "e" : ""); ++ ++listed; ++ if (r && first_readable < 0) ++ first_readable = fd; ++ } ++ line[used++] = '\n'; ++ line[used] = 0; ++ rclol_trace_write(line); ++ if (first_readable >= 0) ++ rclol_trace_fd = first_readable; ++} ++ ++/* darling-lol: idle-poll backoff. ++ * The League client's HTTP/TLS reader (LeagueClient thread "ExternalClient") and a second client process call ++ * select() with a zero timeout in a tight loop, 20-30 thousand times a second, finding nothing (measured: 8 million ++ * calls, 99.95% empty, always the same descriptor). That burns ~80% of a core, i.e. several watts of a power-limited ++ * APU that the game's main thread would otherwise turn into clock speed. Limit this workaround to those client ++ * executables: the game also does repeated empty polls on its frame thread, where sleeping delays every frame. ++ * A client thread that has polled with a zero timeout ++ * and found nothing thousands of times in a row is idle-polling, so sleep briefly between its polls: the first ++ * poll that finds something resets the streak, and the added latency is at most 0.5 ms. Streaks are tracked per ++ * thread in a small table. Disable with the host file /tmp/rclol-no-select-backoff. */ ++struct rclol_poll_slot { int tid; unsigned streak; }; ++static struct rclol_poll_slot rclol_poll_slots[256]; ++static int rclol_backoff_state = -1; /* -1 unknown, 0 off, 1 on */ ++ ++static int rclol_client_poll_backoff_enabled(void) ++{ ++ char argv0[512]; ++ const char* basename = argv0; ++ const char* clients[] = { "LeagueClient", "RiotClientServices" }; ++ long fd, length; ++ unsigned i, j; ++ if (LINUX_SYSCALL(__NR_access, "/tmp/rclol-no-select-backoff", 0) == 0) ++ return 0; ++ /* Native /proc identifies argv[0] without a server RPC or guest path translation. ++ * Fail closed on a read error or a truncated executable name; never log arguments. */ ++ fd = LINUX_SYSCALL(__NR_open, "/proc/self/cmdline", 0, 0); ++ if (fd < 0) ++ return 0; ++ length = LINUX_SYSCALL(__NR_read, fd, argv0, sizeof(argv0)); ++ LINUX_SYSCALL1(__NR_close, fd); ++ if (length <= 0) ++ return 0; ++ for (i = 0; i < (unsigned) length && argv0[i]; ++i) ++ if (argv0[i] == '/') basename = argv0 + i + 1; ++ if (i == (unsigned) length) ++ return 0; ++ for (i = 0; i < sizeof(clients) / sizeof(clients[0]); ++i) { ++ for (j = 0; basename[j] && basename[j] == clients[i][j]; ++j) {} ++ if (!basename[j] && !clients[i][j]) ++ return 1; ++ } ++ return 0; ++} ++ ++static void rclol_select_backoff_before(struct bsd_timeval* timeout) ++{ ++ struct rclol_poll_slot* slot; ++ unsigned streak; ++ int tid; ++ if (rclol_backoff_state < 0) ++ rclol_backoff_state = rclol_client_poll_backoff_enabled(); ++ if (!rclol_backoff_state || timeout == NULL || timeout->tv_sec != 0 || timeout->tv_usec != 0) ++ return; ++ tid = (int) LINUX_SYSCALL0(__NR_gettid); ++ slot = &rclol_poll_slots[(unsigned) tid & 255]; ++ if (__atomic_load_n(&slot->tid, __ATOMIC_RELAXED) != tid) ++ return; ++ streak = __atomic_load_n(&slot->streak, __ATOMIC_RELAXED); ++ if (streak >= 2000) { ++ struct { long tv_sec; long tv_nsec; } pause = { 0, streak >= 20000 ? 500000 : 100000 }; ++ LINUX_SYSCALL(__NR_nanosleep, &pause, 0); ++ } ++} ++ ++static void rclol_select_backoff_after(struct bsd_timeval* timeout, int ret) ++{ ++ struct rclol_poll_slot* slot; ++ int tid; ++ if (!rclol_backoff_state) ++ return; ++ tid = (int) LINUX_SYSCALL0(__NR_gettid); ++ slot = &rclol_poll_slots[(unsigned) tid & 255]; ++ if (timeout != NULL && timeout->tv_sec == 0 && timeout->tv_usec == 0 && ret == 0) { ++ if (__atomic_load_n(&slot->tid, __ATOMIC_RELAXED) != tid) { ++ __atomic_store_n(&slot->tid, tid, __ATOMIC_RELAXED); ++ __atomic_store_n(&slot->streak, 1, __ATOMIC_RELAXED); ++ } else if (__atomic_load_n(&slot->streak, __ATOMIC_RELAXED) < 1000000) { ++ __atomic_fetch_add(&slot->streak, 1, __ATOMIC_RELAXED); ++ } ++ } else { ++ __atomic_store_n(&slot->streak, 0, __ATOMIC_RELAXED); ++ } ++} ++ + long sys_select(int nfds, void* rfds, void* wfds, void* efds, struct bsd_timeval* timeout) + { + CANCELATION_POINT(); +@@ -25,6 +187,28 @@ long sys_select_nocancel(int nfds, void* rfds, void* wfds, void* efds, struct bs + ltv.tv_usec = timeout->tv_usec; + } + ++ /* every 20000th call of the process: what was asked (taken before the kernel rewrites the sets) */ ++ static unsigned rclol_calls = 0, rclol_pos = 0, rclol_zero = 0, rclol_neg = 0; ++ char rclol_line[400]; ++ int rclol_used = 0; ++ if (rclol_trace_enabled() && ++rclol_calls % 20000 == 0) { ++ int fd, listed = 0; ++ rclol_used = __simple_sprintf(rclol_line, "sample pid=%d tid=%d calls=%u pos=%u zero=%u neg=%u nfds=%d timeout=%lds+%dus asked:", ++ (int) LINUX_SYSCALL0(__NR_getpid), (int) LINUX_SYSCALL0(__NR_gettid), rclol_calls, rclol_pos, rclol_zero, ++ rclol_neg, nfds, timeout ? (long) timeout->tv_sec : -1L, timeout ? (int) timeout->tv_usec : 0); ++ for (fd = 0; fd < nfds && fd < 1024 && listed < 12; ++fd) { ++ int r = rfds && (((const unsigned*) rfds)[fd / 32] >> (fd % 32) & 1); ++ int w = wfds && (((const unsigned*) wfds)[fd / 32] >> (fd % 32) & 1); ++ int e = efds && (((const unsigned*) efds)[fd / 32] >> (fd % 32) & 1); ++ if (!r && !w && !e) ++ continue; ++ rclol_used += __simple_sprintf(rclol_line + rclol_used, " %d%s%s%s", fd, r ? "r" : "", w ? "w" : "", e ? "e" : ""); ++ ++listed; ++ } ++ } ++ ++ rclol_select_backoff_before(timeout); ++ + #if defined(__NR__newselect) + ret = LINUX_SYSCALL(__NR__newselect, nfds, rfds, wfds, efds, + (timeout != NULL) ? <v : NULL); +@@ -32,6 +216,15 @@ long sys_select_nocancel(int nfds, void* rfds, void* wfds, void* efds, struct bs + ret = LINUX_SYSCALL(__NR_select, nfds, rfds, wfds, efds, + (timeout != NULL) ? <v : NULL); + #endif ++ rclol_select_backoff_after(timeout, ret); ++ if (rclol_trace_enabled()) { ++ if (ret > 0) ++rclol_pos; else if (ret == 0) ++rclol_zero; else ++rclol_neg; ++ if (rclol_used) { ++ __simple_sprintf(rclol_line + rclol_used, " -> ret=%d\n", ret); ++ rclol_trace_write(rclol_line); ++ } ++ rclol_trace_select(nfds, rfds, wfds, efds, timeout, ret); ++ } + if (ret < 0) + ret = errno_linux_to_bsd(ret); + +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/stat/fstatfs.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/stat/fstatfs.c +index 1098510..1741d5a 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/stat/fstatfs.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/stat/fstatfs.c +@@ -19,6 +19,9 @@ extern __SIZE_TYPE__ strlen(const char* str); + extern int strncmp(const char* s1, const char* s2, __SIZE_TYPE__ len); + extern int sprintf(char *str, const char *format, ...); + ++/* darling-lol M58F140: shared cached /proc/self/mounts lookup (see statfs.c) */ ++VISIBLE void lolsys_fill_mount_names(const char* path, char* mnton, __SIZE_TYPE__ mnton_sz, char* fstype, __SIZE_TYPE__ fstype_sz, char* mntfrom, __SIZE_TYPE__ mntfrom_sz); ++ + long sys_fstatfs(int fd, struct bsd_statfs* buf) + { + #ifdef __x86_64__ +@@ -31,12 +34,9 @@ long sys_fstatfs(int fd, struct bsd_statfs* buf) + + long sys_fstatfs64(int fd, struct bsd_statfs64* buf) + { +- int fd_m, ret; +- struct simple_readline_buf rbuf; ++ int ret; + struct linux_statfs64 lbuf; +- char line[512]; + char path[1024]; +- int max_len = 0; + + #ifdef __NR_fstatfs64 + ret = LINUX_SYSCALL3(__NR_fstatfs64, fd, sizeof(lbuf), &lbuf); +@@ -54,48 +54,11 @@ long sys_fstatfs64(int fd, struct bsd_statfs64* buf) + if (ret < 0) + return ret; + +- fd_m = sys_open("/proc/self/mounts", O_RDONLY, 0); +- if (fd_m < 0) +- return fd_m; +- +- __simple_readline_init(&rbuf); +- +- buf->f_mntonname[0] = 0; +- buf->f_fstypename[0] = 0; +- buf->f_mntfromname[0] = 0; + buf->f_iosize = 512; + +- while (__simple_readline(fd_m, &rbuf, line, sizeof(line))) +- { +- char* p; +- char* saveptr; +- char* mntfrom; +- int len; +- +- p = strtok_r(line, " ", &saveptr); +- if (p == NULL) +- continue; +- +- mntfrom = p; +- p = strtok_r(NULL, " ", &saveptr); +- if (p == NULL) +- continue; +- +- len = strlen(p); +- if (strncmp(p, path, len) != 0 || len < max_len) +- continue; +- +- max_len = len; +- strlcpy(buf->f_mntonname, p, sizeof(buf->f_mntonname)); +- +- p = strtok_r(NULL, " ", &saveptr); +- if (p == NULL) +- continue; +- +- strlcpy(buf->f_fstypename, p, sizeof(buf->f_fstypename)); +- strlcpy(buf->f_mntfromname, mntfrom, sizeof(buf->f_mntfromname)); +- } +- +- close_internal(fd_m); ++ lolsys_fill_mount_names(path, ++ buf->f_mntonname, sizeof(buf->f_mntonname), ++ buf->f_fstypename, sizeof(buf->f_fstypename), ++ buf->f_mntfromname, sizeof(buf->f_mntfromname)); + return 0; + } +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/stat/statfs.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/stat/statfs.c +index a6a5162..2155dc4 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/stat/statfs.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/stat/statfs.c +@@ -20,6 +20,92 @@ extern __SIZE_TYPE__ strlcpy(char* dst, const char* src, __SIZE_TYPE__ size); + extern __SIZE_TYPE__ strlen(const char* str); + extern int strncmp(const char* s1, const char* s2, __SIZE_TYPE__ len); + ++/* darling-lol M58F140: /proc/self/mounts changes slowly, but statfs/fstatfs ++ * used to re-open and re-parse it on every call. The Riot launcher calls ++ * statfs hundreds of times while scanning its install; the per-call parse was a ++ * large part of its slow start (which tripped the patcher's "no progress" ++ * timeout). Cache the parsed table with a 1-second TTL. */ ++#define LOLSYS_MAX_MOUNTS 512 ++struct lolsys_mount_entry { ++ char mnton[1024]; ++ char fstype[128]; ++ char mntfrom[256]; ++}; ++static struct lolsys_mount_entry lolsys_mount_table[LOLSYS_MAX_MOUNTS]; ++static int lolsys_mount_count = 0; ++static long lolsys_mount_sec = -1; ++ ++static void lolsys_refresh_mounts(long now_sec) ++{ ++ int fd; ++ struct simple_readline_buf rbuf; ++ char line[512]; ++ ++ if (lolsys_mount_count > 0 && now_sec == lolsys_mount_sec) ++ return; ++ ++ lolsys_mount_count = 0; ++ fd = sys_open("/proc/self/mounts", O_RDONLY, 0); ++ if (fd < 0) ++ return; ++ ++ __simple_readline_init(&rbuf); ++ while (__simple_readline(fd, &rbuf, line, sizeof(line))) { ++ char* p; ++ char* saveptr; ++ char* mntfrom; ++ struct lolsys_mount_entry* e; ++ if (lolsys_mount_count >= LOLSYS_MAX_MOUNTS) ++ break; ++ p = strtok_r(line, " ", &saveptr); ++ if (p == NULL) ++ continue; ++ mntfrom = p; ++ p = strtok_r(NULL, " ", &saveptr); ++ if (p == NULL) ++ continue; ++ e = &lolsys_mount_table[lolsys_mount_count]; ++ strlcpy(e->mnton, p, sizeof(e->mnton)); ++ p = strtok_r(NULL, " ", &saveptr); ++ if (p == NULL) ++ continue; ++ strlcpy(e->fstype, p, sizeof(e->fstype)); ++ strlcpy(e->mntfrom, mntfrom, sizeof(e->mntfrom)); ++ lolsys_mount_count++; ++ } ++ close_internal(fd); ++ lolsys_mount_sec = now_sec; ++} ++ ++VISIBLE ++void lolsys_fill_mount_names(const char* path, char* mnton, __SIZE_TYPE__ mnton_sz, char* fstype, __SIZE_TYPE__ fstype_sz, char* mntfrom, __SIZE_TYPE__ mntfrom_sz) ++{ ++ int i, max_len = 0; ++ struct { long s; long us; } tv; ++ ++ tv.s = 0; ++ tv.us = 0; ++ LINUX_SYSCALL2(__NR_gettimeofday, &tv, 0); ++ lolsys_refresh_mounts(tv.s); ++ ++ mnton[0] = 0; ++ fstype[0] = 0; ++ mntfrom[0] = 0; ++ ++ for (i = 0; i < lolsys_mount_count; i++) { ++ struct lolsys_mount_entry* e = &lolsys_mount_table[i]; ++ int len = (int) strlen(e->mnton); ++ if (len == 0) ++ continue; ++ if (strncmp(e->mnton, path, len) != 0 || len < max_len) ++ continue; ++ max_len = len; ++ strlcpy(mnton, e->mnton, mnton_sz); ++ strlcpy(fstype, e->fstype, fstype_sz); ++ strlcpy(mntfrom, e->mntfrom, mntfrom_sz); ++ } ++} ++ + long sys_statfs(const char* path, struct bsd_statfs* buf) + { + #ifdef __x86_64__ +@@ -32,11 +118,8 @@ long sys_statfs(const char* path, struct bsd_statfs* buf) + + long sys_statfs64(const char* path, struct bsd_statfs64* buf) + { +- int fd, ret; +- struct simple_readline_buf rbuf; ++ int ret; + struct linux_statfs64 lbuf; +- char line[512]; +- int max_len = 0; + + struct vchroot_expand_args vc; + vc.flags = 0; +@@ -58,47 +141,9 @@ long sys_statfs64(const char* path, struct bsd_statfs64* buf) + + statfs_linux_to_bsd64(&lbuf, buf); + +- fd = sys_open("/proc/self/mounts", O_RDONLY, 0); +- if (fd < 0) +- return fd; +- +- __simple_readline_init(&rbuf); +- +- buf->f_mntonname[0] = 0; +- buf->f_fstypename[0] = 0; +- buf->f_mntfromname[0] = 0; +- +- while (__simple_readline(fd, &rbuf, line, sizeof(line))) +- { +- char* p; +- char* saveptr; +- char* mntfrom; +- int len; +- +- p = strtok_r(line, " ", &saveptr); +- if (p == NULL) +- continue; +- +- mntfrom = p; +- p = strtok_r(NULL, " ", &saveptr); +- if (p == NULL) +- continue; +- +- len = strlen(p); +- if (strncmp(p, vc.path, len) != 0 || len < max_len) +- continue; +- +- max_len = len; +- strlcpy(buf->f_mntonname, p, sizeof(buf->f_mntonname)); +- +- p = strtok_r(NULL, " ", &saveptr); +- if (p == NULL) +- continue; +- +- strlcpy(buf->f_fstypename, p, sizeof(buf->f_fstypename)); +- strlcpy(buf->f_mntfromname, mntfrom, sizeof(buf->f_mntfromname)); +- } +- +- close_internal(fd); ++ lolsys_fill_mount_names(vc.path, ++ buf->f_mntonname, sizeof(buf->f_mntonname), ++ buf->f_fstypename, sizeof(buf->f_fstypename), ++ buf->f_mntfromname, sizeof(buf->f_mntfromname)); + return 0; + } +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/time/futimes.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/time/futimes.c +index 687a1b5..83b905a 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/time/futimes.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/time/futimes.c +@@ -4,20 +4,25 @@ + + #include + #include ++#include + #include + + + long sys_futimes(int fd, struct bsd_timeval* tv) + { + int ret; +- struct linux_timeval ltv[2]; ++ struct { long tv_sec; long tv_nsec; } times[2]; ++ if (tv) { ++ for (int i = 0; i < 2; ++i) { ++ if (tv[i].tv_usec < 0 || tv[i].tv_usec >= 1000000) ++ return errno_linux_to_bsd(-LINUX_EINVAL); ++ times[i].tv_sec = tv[i].tv_sec; ++ times[i].tv_nsec = (long)tv[i].tv_usec * 1000; ++ } ++ } ++ /* NULL requests current timestamps; utimensat uses nanoseconds. */ ++ ret = LINUX_SYSCALL(__NR_utimensat, fd, NULL, tv ? times : NULL, 0); + +- ltv[0].tv_sec = tv[0].tv_sec; +- ltv[0].tv_usec = tv[0].tv_usec; +- ltv[1].tv_sec = tv[1].tv_sec; +- ltv[1].tv_usec = tv[1].tv_usec; +- +- ret = LINUX_SYSCALL(__NR_utimensat, fd, NULL, ltv, 0); + if (ret < 0) + ret = errno_linux_to_bsd(ret); + +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/time/gettimeofday.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/time/gettimeofday.c +index be89afd..cdf71d2 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/time/gettimeofday.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/time/gettimeofday.c +@@ -3,12 +3,27 @@ + #include + #include + #include ++#include + + long sys_gettimeofday(struct bsd_timeval* tv, struct timezone* tz) + { + int ret; + struct linux_timeval ltv; + ++#if defined(__x86_64__) ++ /* darling-lol: user-space clock, see linux_premigration/ext/clock_gettime.c */ ++ if (tz == 0) ++ { ++ extern int __darling_fast_clock(int clockid, struct timespec* res); ++ struct timespec ts; ++ if (__darling_fast_clock(0 /* CLOCK_REALTIME */, &ts) == 0) ++ { ++ tv->tv_sec = ts.tv_sec; ++ tv->tv_usec = ts.tv_nsec / 1000; ++ return 0; ++ } ++ } ++#endif + ret = LINUX_SYSCALL(__NR_gettimeofday, <v, tz); + if (ret < 0) + { +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/unistd/close.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/unistd/close.c +index 6fad87f..e77e786 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/unistd/close.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/unistd/close.c +@@ -16,6 +16,8 @@ __attribute__((weak)) + __attribute__((visibility("default"))) + void kqueue_closed_fd(int fd) {} + ++extern void rclol_trace_io(const char* op, int fd, long ret); ++ + long sys_close(int fd) + { + CANCELATION_POINT(); +@@ -39,6 +41,7 @@ long sys_close_nocancel(int fd) + } + + ret = LINUX_SYSCALL1(__NR_close, fd); ++ rclol_trace_io("close", fd, ret); + if (ret < 0) + ret = errno_linux_to_bsd(ret); + else +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/unistd/read.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/unistd/read.c +index bafb315..1e8dd67 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/unistd/read.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/unistd/read.c +@@ -9,6 +9,9 @@ + + #define LINUX_TCGETA 0x5405 + ++extern void rclol_trace_io(const char* op, int fd, long ret); ++ ++ + long sys_read(int fd, void* mem, int len) + { + CANCELATION_POINT(); +@@ -20,6 +23,7 @@ long sys_read_nocancel(int fd, void* mem, int len) + int ret; + + ret = LINUX_SYSCALL3(__NR_read, fd, mem, len); ++ rclol_trace_io("read", fd, ret); + if (ret < 0) + { + // When the slave side of a PTY is closed, Linux produces -EIO on the master side. +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/fgetxattr.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/fgetxattr.c +index 6dca738..3d864d2 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/fgetxattr.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/fgetxattr.c +@@ -4,20 +4,23 @@ + + #include + #include ++#include + #include + + long sys_fgetxattr(int fd, const char* name, char* value, + unsigned long size, unsigned int pos, int options) + { + int ret; ++ char linux_name[1024]; + + if (pos != 0) + return -ERANGE; + ++ name = xattr_name_to_linux(name, linux_name, sizeof(linux_name)); + ret = LINUX_SYSCALL(__NR_fgetxattr, fd, name, value, size); + + if (ret < 0) +- return errno_linux_to_bsd(ret); ++ return xattr_errno_to_bsd(ret); + + return ret; + } +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/fremovexattr.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/fremovexattr.c +index adaff44..eb445fe 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/fremovexattr.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/fremovexattr.c +@@ -2,16 +2,19 @@ + + #include + #include ++#include + #include + + long sys_fremovexattr(int fd, const char* name, int options) + { + int ret; ++ char linux_name[1024]; + ++ name = xattr_name_to_linux(name, linux_name, sizeof(linux_name)); + ret = LINUX_SYSCALL(__NR_fremovexattr, fd, name); + + if (ret < 0) +- return errno_linux_to_bsd(ret); ++ return xattr_errno_to_bsd(ret); + + return ret; + } +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/fsetxattr.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/fsetxattr.c +index 9aaef2c..1770c47 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/fsetxattr.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/fsetxattr.c +@@ -4,20 +4,23 @@ + + #include + #include ++#include + #include + + long sys_fsetxattr(int fd, const char* name, const void* value, + unsigned long size, unsigned int pos, int options) + { + int ret; ++ char linux_name[1024]; + + if (pos != 0) + return -ERANGE; + ++ name = xattr_name_to_linux(name, linux_name, sizeof(linux_name)); + ret = LINUX_SYSCALL(__NR_fsetxattr, fd, name, value, size); + + if (ret < 0) +- return errno_linux_to_bsd(ret); ++ return xattr_errno_to_bsd(ret); + + return ret; + } +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/getxattr.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/getxattr.c +index daca861..4b53ad2 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/getxattr.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/getxattr.c +@@ -7,6 +7,7 @@ extern char* strcpy(char* dst, const char* src); + #include + #include + #include ++#include + #include + #include + #include +@@ -15,6 +16,7 @@ long sys_getxattr(const char* path, const char* name, char* value, + unsigned long size, unsigned int pos, int options) + { + int ret; ++ char linux_name[1024]; + + if (pos != 0) + return -ERANGE; +@@ -29,10 +31,16 @@ long sys_getxattr(const char* path, const char* name, char* value, + if (ret < 0) + return errno_linux_to_bsd(ret); + ++ name = xattr_name_to_linux(name, linux_name, sizeof(linux_name)); ++ + ret = LINUX_SYSCALL(__NR_getxattr, vc.path, name, value, size); + + if (ret < 0) +- return errno_linux_to_bsd(ret); ++ return xattr_errno_to_bsd(ret); + +- return 0; ++ /* Darling returns the number of bytes copied (POSIX semantics). ++ * Returning 0 here made callers (e.g. Crashpad's ++ * crashpad.database.initialized marker) treat a present attribute as ++ * an empty value, so the crash database never initialised. */ ++ return ret; + } +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/listxattr.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/listxattr.c +index f4740ec..bc88090 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/listxattr.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/listxattr.c +@@ -2,12 +2,38 @@ + + #include + #include ++#include + #include + #include + #include + + #define XATTR_NOFOLLOW 1 + ++/* Strip the "user." prefix from each NUL-separated name in the buffer so ++ * Darwin callers see namespace-less names, and compact the buffer. */ ++static unsigned long xattr_strip_user_prefix(char* namebuf, unsigned long len) ++{ ++ char* in = namebuf; ++ char* out = namebuf; ++ char* end = namebuf + len; ++ ++ while (in < end) { ++ unsigned long n = 0; ++ const char* s = in; ++ char* dst = out; ++ if (__builtin_strncmp(s, "user.", 5) == 0) ++ s += 5; ++ while (s + n < end && s[n] != '\0') { ++ dst[n] = s[n]; ++ n++; ++ } ++ dst[n] = '\0'; ++ out += n + 1; ++ in += (s - in) + n + 1; ++ } ++ return (unsigned long)(out - namebuf); ++} ++ + long sys_listxattr(const char* path, char* namebuf, unsigned long size, int options) + { + int ret; +@@ -23,12 +49,15 @@ long sys_listxattr(const char* path, char* namebuf, unsigned long size, int opti + return errno_linux_to_bsd(ret); + + if (options & XATTR_NOFOLLOW) +- ret = LINUX_SYSCALL(__NR_llistxattr, path, namebuf, size); ++ ret = LINUX_SYSCALL(__NR_llistxattr, vc.path, namebuf, size); + else +- ret = LINUX_SYSCALL(__NR_listxattr, path, namebuf, size); ++ ret = LINUX_SYSCALL(__NR_listxattr, vc.path, namebuf, size); + + if (ret < 0) + return errno_linux_to_bsd(ret); + ++ if (namebuf != 0 && ret > 0) ++ ret = (int) xattr_strip_user_prefix(namebuf, (unsigned long) ret); ++ + return ret; + } +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/removexattr.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/removexattr.c +index 628a4f7..705b845 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/removexattr.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/removexattr.c +@@ -2,6 +2,9 @@ + + #include + #include ++#include ++#include ++#include + #include + + #define XATTR_NOFOLLOW 1 +@@ -9,14 +12,26 @@ + long sys_removexattr(const char* path, const char* name, int options) + { + int ret; ++ char linux_name[1024]; ++ ++ struct vchroot_expand_args vc; ++ vc.flags = (options & XATTR_NOFOLLOW) ? 0 : VCHROOT_FOLLOW; ++ vc.dfd = get_perthread_wd(); ++ ++ strcpy(vc.path, path); ++ ret = vchroot_expand(&vc); ++ if (ret < 0) ++ return errno_linux_to_bsd(ret); ++ ++ name = xattr_name_to_linux(name, linux_name, sizeof(linux_name)); + + if (options & XATTR_NOFOLLOW) +- ret = LINUX_SYSCALL(__NR_lremovexattr, path, name); ++ ret = LINUX_SYSCALL(__NR_lremovexattr, vc.path, name); + else +- ret = LINUX_SYSCALL(__NR_removexattr, path, name); ++ ret = LINUX_SYSCALL(__NR_removexattr, vc.path, name); + + if (ret < 0) +- return errno_linux_to_bsd(ret); ++ return xattr_errno_to_bsd(ret); + + return ret; + } +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/setxattr.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/setxattr.c +index c1b2d50..4e19963 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/setxattr.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/xattr/setxattr.c +@@ -2,8 +2,13 @@ + + #include + ++extern char* strcpy(char* dst, const char* src); ++ + #include + #include ++#include ++#include ++#include + #include + + #define XATTR_NOFOLLOW 1 +@@ -12,21 +17,39 @@ long sys_setxattr(const char* path, const char* name, const void* value, + unsigned long size, unsigned int pos, int options) + { + int ret; ++ char linux_name[1024]; + + if (pos != 0) + return -ERANGE; + ++ /* darling-lol: vchroot-expand the path like getxattr does. Without ++ * this, a guest path (e.g. /Users/.../Crashes/Riot Client) was passed ++ * straight to the host setxattr and failed ENOENT -- Crashpad could ++ * never write its crashpad.database.initialized marker. */ ++ struct vchroot_expand_args vc; ++ vc.flags = (options & XATTR_NOFOLLOW) ? 0 : VCHROOT_FOLLOW; ++ vc.dfd = get_perthread_wd(); ++ ++ strcpy(vc.path, path); ++ ret = vchroot_expand(&vc); ++ if (ret < 0) ++ return errno_linux_to_bsd(ret); ++ ++ /* darling-lol: Darwin names are namespace-less; Linux needs a ++ * namespace prefix or returns ENOTSUP. */ ++ name = xattr_name_to_linux(name, linux_name, sizeof(linux_name)); ++ + if (options & XATTR_NOFOLLOW) + { +- ret = LINUX_SYSCALL(__NR_lsetxattr, path, name, value, size); ++ ret = LINUX_SYSCALL(__NR_lsetxattr, vc.path, name, value, size); + } + else + { +- ret = LINUX_SYSCALL(__NR_setxattr, path, name, value, size); ++ ret = LINUX_SYSCALL(__NR_setxattr, vc.path, name, value, size); + } + + if (ret < 0) +- return errno_linux_to_bsd(ret); ++ return xattr_errno_to_bsd(ret); + + return ret; + } +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/mach/impl/mach_traps.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/mach/impl/mach_traps.c +index b998456..3e643c1 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/mach/impl/mach_traps.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/mach/impl/mach_traps.c +@@ -12,9 +12,28 @@ + #include + #include + #include ++#include + + #include + ++/* darling-lol: transient darlingserver RPC failures (notably -111/ ++ * ECONNREFUSED when many threads check in at once, e.g. Electron ++ * helper bursts) must not kill the process outright. Bounded retry ++ * with backoff; still aborts if the failure persists. */ ++struct dserver_retry_ts { ++ long tv_sec; ++ long tv_nsec; ++}; ++ ++static void dserver_retry_sleep(int attempt) ++{ ++ struct dserver_retry_ts ts; ++ long ms = 1L << (attempt > 6 ? 6 : attempt); /* 2..128ms */ ++ ts.tv_sec = 0; ++ ts.tv_nsec = ms * 1000L * 1000L; ++ LINUX_SYSCALL(__NR_nanosleep, &ts, 0UL); ++} ++ + #define UNIMPLEMENTED_TRAP() { char msg[] = "Called unimplemented Mach trap: "; write(2, msg, sizeof(msg)-1); write(2, __FUNCTION__, sizeof(__FUNCTION__)-1); write(2, "\n", 1); } + + mach_port_name_t mach_reply_port_impl(void) +@@ -44,6 +63,8 @@ mach_port_name_t host_self_trap_impl(void) + return port_name; + } + ++#include ++ + mach_msg_return_t mach_msg_trap_impl( + mach_msg_header_t *msg, + mach_msg_option_t option, +@@ -72,8 +93,12 @@ mach_msg_return_t mach_msg_overwrite_trap_impl( + { + int code; + +-retry: ++retry:; ++ unsigned long long slowStart = slowwait_enabled() ? slowwait_now() : 0; + code = dserver_rpc_mach_msg_overwrite(msg, option, send_size, rcv_size, rcv_name, timeout, notify, rcv_msg); ++ if (slowStart) { ++ slowwait_log("mach_msg", rcv_name, slowStart, (void**) __builtin_frame_address(0), option, timeout); ++ } + + if (code < 0) { + if (code == -LINUX_EINTR) { +@@ -92,33 +117,517 @@ retry: + } + } + __simple_printf("mach_msg_overwrite failed (internally): %d\n", code); ++ { ++ /* darling-lol diagnostic for the LeagueClient crash (2026-10-10): is this thread's RPC descriptor still ++ * open, and what is it? Written to the host file /tmp/rclol-rpc-fail.log as well. */ ++ extern int __dserver_per_thread_socket(void); ++ int rpc_fd = __dserver_per_thread_socket(); ++ long getfd = LINUX_SYSCALL(__NR_fcntl, rpc_fd, 1 /* F_GETFD */); ++ char link[64], target[128], line[320]; ++ __simple_sprintf(link, "/proc/self/fd/%d", rpc_fd); ++ long tl = LINUX_SYSCALL(__NR_readlink, link, target, sizeof(target) - 1); ++ target[tl > 0 ? tl : 0] = 0; ++ int len = __simple_sprintf(line, "LOLRPCFAIL pid=%d tid=%d code=%d rpc_fd=%d getfd=%ld target=%s option=0x%x\n", ++ (int) LINUX_SYSCALL(__NR_getpid), (int) LINUX_SYSCALL(__NR_gettid), code, rpc_fd, getfd, target, (unsigned) option); ++ __simple_printf("%s", line); ++ long log = LINUX_SYSCALL(__NR_open, "/tmp/rclol-rpc-fail.log", 02101, 0666); ++ if (log >= 0) { LINUX_SYSCALL(__NR_write, log, line, len); LINUX_SYSCALL(__NR_close, log); } ++ } + __simple_abort(); + } ++ /* A receive can be interrupted after a combined send has completed. ++ * Without MACH_RCV_INTERRUPT, callers expect the receive to resume. ++ * Retrying the send would duplicate the request. */ ++ if ((option & MACH_RCV_MSG) != 0 && ++ code == MACH_RCV_INTERRUPTED && ++ (option & (MACH_RCV_INTERRUPT | MACH_RCV_TIMEOUT)) == 0) { ++ option &= ~MACH_SEND_MSG; ++ send_size = 0; ++ goto retry; ++ } ++ #if 0 /* M58 diagnostics: keep guest Mach failure traces off by default. */ ++ if (code == MACH_SEND_INVALID_DEST && msg != 0 && ++ (option & MACH_SEND_MSG) != 0) { ++ /* Only failed sends are traced; this is never on the Mach hot path. */ ++ __simple_printf("M58GP pid=%d tid=%d send-invalid dest=%u local=%u bits=0x%x id=%d opt=0x%x size=%u\n", ++ (int) LINUX_SYSCALL(__NR_getpid), (int) LINUX_SYSCALL(__NR_gettid), ++ (unsigned) msg->msgh_remote_port, ++ (unsigned) msg->msgh_local_port, ++ (unsigned) msg->msgh_bits, ++ (int) msg->msgh_id, ++ (unsigned) option, ++ (unsigned) send_size); ++ } ++ if (code != MACH_MSG_SUCCESS && ++ !(code == MACH_RCV_TIMED_OUT && (option & MACH_RCV_TIMEOUT) != 0) && ++ code != MACH_RCV_TOO_LARGE && (option & MACH_RCV_MSG) != 0) { ++ static unsigned int receive_failures; ++ if (__sync_fetch_and_add(&receive_failures, 1) < 128) { ++ __simple_printf("M58GR pid=%d tid=%d recv-fail code=0x%x rcv=%u opt=0x%x notify=%u send=%u\n", ++ (int) LINUX_SYSCALL(__NR_getpid), (int) LINUX_SYSCALL(__NR_gettid), ++ (unsigned) code, (unsigned) rcv_name, ++ (unsigned) option, (unsigned) notify, ++ (unsigned) send_size); ++ } ++ } ++ #endif + + return code; + } + ++/* darling-lol: process-local Mach semaphores. ++ * ++ * Every semaphore_wait/semaphore_signal used to be a round trip to darlingserver: the caller sends a message and sleeps ++ * until the server thread has run and answered. League's job system and libdispatch use Mach semaphores to hand work to ++ * threads of the same process (about 19 such round trips per frame on the main thread alone, 2026-10-10), so each ++ * wake-up paid two extra context switches and the caller of semaphore_signal blocked for the server's turnaround. ++ * ++ * A semaphore created by this task (semaphore_create on mach_task_self) is now also kept here as a futex counter and ++ * all traps on it are served locally. The server-side object still exists, so the port name stays valid, but it is ++ * never signalled. Limits: a semaphore whose right is sent to another process would not be woken by that process ++ * (nothing in the game does this), and semaphore_signal_thread with a thread is not supported (KERN_NOT_WAITING). ++ * OPT-IN again (host file /tmp/rclol-local-semaphores). While it was on by default (2026-10-10 22:22-23:50) LeagueClient ++ * died ~85 s after start in 3 of 4 checked launches ("mach_msg_overwrite failed -9", then dserver_rpc_interrupt_enter ++ * -9: its RPC descriptor had become invalid) and the user saw Vanguard errors; with it off the client survived. The ++ * mechanism is not understood. /tmp/rclol-no-local-semaphores also turns it off, and ++ * /tmp/rclol-local-semaphores-all turns it on for every process (probe tests). Decided once per process. */ ++#define LSEM_SLOTS 8192u ++#define LSEM_TOMB 0xffffffffu ++#define LSEM_FUTEX_WAIT 0 ++#define LSEM_FUTEX_WAKE 1 ++struct lsem { ++ volatile unsigned int name; ++ volatile unsigned int gen; ++ volatile int value; ++ volatile int waiters; ++}; ++static struct lsem lsems[LSEM_SLOTS]; ++static volatile int lsem_state; /* 0 undecided, 1 on, -1 off */ ++static volatile int lsem_lock; ++ ++extern kern_return_t __darling_rpc_semaphore_create(mach_port_name_t task, mach_port_name_t* semaphore, int policy, int value); ++extern kern_return_t __darling_rpc_semaphore_destroy(mach_port_name_t task, mach_port_name_t semaphore); ++ ++static int darling_is_league_game_uncached(void); ++ ++/* Read once per process: the answer cannot change, and other processes must not keep opening files for it. */ ++static int darling_is_league_game(void) ++{ ++ static volatile int cached; /* 0 unknown, 1 yes, -1 no */ ++ int value = __atomic_load_n(&cached, __ATOMIC_ACQUIRE); ++ if (value == 0) { ++ value = darling_is_league_game_uncached() ? 1 : -1; ++ __atomic_store_n(&cached, value, __ATOMIC_RELEASE); ++ } ++ return value == 1; ++} ++ ++static int darling_is_league_game_uncached(void) ++{ ++ /* Only the executable path (argv[0], the first NUL-terminated string) counts. The whole command line was searched ++ * before, so any process that merely had the game's path among its arguments would have been treated as the game. */ ++ char buf[512]; ++ static const char mark[] = "/Game/LeagueOfLegends.app/Contents/MacOS/"; ++ int found = 0; ++ long n = 0; ++ int fd = LINUX_SYSCALL(__NR_open, "/proc/self/cmdline", 0 /* O_RDONLY */, 0); ++ if (fd >= 0) { ++ n = LINUX_SYSCALL(__NR_read, fd, buf, sizeof(buf) - 1); ++ LINUX_SYSCALL(__NR_close, fd); ++ if (n < 0) n = 0; ++ buf[n] = 0; ++ long end = 0; ++ while (end < n && buf[end]) ++end; /* argv[0] */ ++ for (long i = 0; i + (long) sizeof(mark) - 1 <= end && !found; ++i) { ++ unsigned long j = 0; ++ while (j < sizeof(mark) - 1 && buf[i + j] == mark[j]) ++j; ++ if (j == sizeof(mark) - 1) found = 1; ++ } ++ /* one line per process in the host file /tmp/rclol-game-decisions.log: which processes asked, and the answer */ ++ long log = LINUX_SYSCALL(__NR_open, "/tmp/rclol-game-decisions.log", 02101 /* O_WRONLY|O_CREAT|O_APPEND */, 0666); ++ if (log >= 0) { ++ char line[160]; ++ long tail = end > 60 ? end - 60 : 0; ++ int len = __simple_sprintf(line, "pid=%d game=%d argv0=...%s\n", (int) LINUX_SYSCALL(__NR_getpid), found, buf + tail); ++ LINUX_SYSCALL(__NR_write, log, line, len); ++ LINUX_SYSCALL(__NR_close, log); ++ } ++ } ++ return found; ++} ++ ++/* for the other emulation files (psynch) */ ++__attribute__((visibility("hidden"))) ++int __darling_is_league_game(void) ++{ ++ return darling_is_league_game(); ++} ++ ++static int lsem_decide(void) ++{ ++ int on = 0; ++ if (LINUX_SYSCALL(__NR_access, "/tmp/rclol-local-semaphores-all", 0) == 0) ++ on = 1; ++ else if (LINUX_SYSCALL(__NR_access, "/tmp/rclol-local-semaphores", 0) == 0 && ++ LINUX_SYSCALL(__NR_access, "/tmp/rclol-no-local-semaphores", 0) != 0) ++ on = darling_is_league_game(); ++ __atomic_store_n(&lsem_state, on ? 1 : -1, __ATOMIC_RELEASE); ++ return on ? 1 : -1; ++} ++ ++static inline unsigned int lsem_hash(unsigned int name) ++{ ++ return (name * 2654435761u) >> 19; /* 13 bits */ ++} ++ ++static struct lsem* lsem_find(unsigned int name) ++{ ++ if (__atomic_load_n(&lsem_state, __ATOMIC_ACQUIRE) != 1 || name == 0 || name == LSEM_TOMB) ++ return 0; ++ unsigned int i = lsem_hash(name); ++ for (unsigned int n = 0; n < LSEM_SLOTS; ++n, i = (i + 1) & (LSEM_SLOTS - 1)) { ++ unsigned int have = __atomic_load_n(&lsems[i].name, __ATOMIC_ACQUIRE); ++ if (have == name) ++ return &lsems[i]; ++ if (have == 0) ++ return 0; ++ } ++ return 0; ++} ++ ++static void lsem_table_lock(void) ++{ ++ int expected = 0; ++ while (!__atomic_compare_exchange_n(&lsem_lock, &expected, 1, 0, __ATOMIC_ACQUIRE, __ATOMIC_RELAXED)) { ++ LINUX_SYSCALL(__NR_futex, &lsem_lock, LSEM_FUTEX_WAIT, 1, 0UL, 0UL, 0); ++ expected = 0; ++ } ++} ++ ++static void lsem_table_unlock(void) ++{ ++ __atomic_store_n(&lsem_lock, 0, __ATOMIC_RELEASE); ++ LINUX_SYSCALL(__NR_futex, &lsem_lock, LSEM_FUTEX_WAKE, 1, 0UL, 0UL, 0); ++} ++ ++static void lsem_insert(unsigned int name, int value) ++{ ++ if (name == 0 || name == LSEM_TOMB) ++ return; ++ lsem_table_lock(); ++ struct lsem* free_slot = 0; ++ struct lsem* found = 0; ++ unsigned int i = lsem_hash(name); ++ for (unsigned int n = 0; n < LSEM_SLOTS; ++n, i = (i + 1) & (LSEM_SLOTS - 1)) { ++ unsigned int have = lsems[i].name; ++ if (have == name) { found = &lsems[i]; break; } ++ if (have == LSEM_TOMB && !free_slot) free_slot = &lsems[i]; ++ if (have == 0) { if (!free_slot) free_slot = &lsems[i]; break; } ++ } ++ if (!found) found = free_slot; ++ if (found) { /* a full table leaves the semaphore to the server */ ++ __atomic_store_n(&found->value, value < 0 ? 0 : value, __ATOMIC_RELAXED); ++ __atomic_store_n(&found->waiters, 0, __ATOMIC_RELAXED); ++ __atomic_store_n(&found->name, name, __ATOMIC_RELEASE); ++ } ++ lsem_table_unlock(); ++} ++ ++static void lsem_remove(unsigned int name) ++{ ++ struct lsem* s = lsem_find(name); ++ if (!s) ++ return; ++ lsem_table_lock(); ++ if (s->name == name) { ++ __atomic_add_fetch(&s->gen, 1, __ATOMIC_RELEASE); ++ __atomic_store_n(&s->name, LSEM_TOMB, __ATOMIC_RELEASE); ++ __atomic_store_n(&s->value, 0x40000000, __ATOMIC_RELEASE); /* waiters wake, see the new generation and leave */ ++ LINUX_SYSCALL(__NR_futex, &s->value, LSEM_FUTEX_WAKE, 0x7fffffff, 0UL, 0UL, 0); ++ } ++ lsem_table_unlock(); ++} ++ ++static kern_return_t lsem_signal(struct lsem* s) ++{ ++ __atomic_add_fetch(&s->value, 1, __ATOMIC_SEQ_CST); ++ if (__atomic_load_n(&s->waiters, __ATOMIC_SEQ_CST) > 0) ++ LINUX_SYSCALL(__NR_futex, &s->value, LSEM_FUTEX_WAKE, 1, 0UL, 0UL, 0); ++ return KERN_SUCCESS; ++} ++ ++/* timeout: nanoseconds, or -1 to wait forever */ ++static kern_return_t lsem_wait(struct lsem* s, long long timeout_ns) ++{ ++ const unsigned int gen = __atomic_load_n(&s->gen, __ATOMIC_ACQUIRE); ++ struct { long tv_sec; long tv_nsec; } now, rel; ++ long long deadline = 0; ++ if (timeout_ns >= 0) { ++ LINUX_SYSCALL(__NR_clock_gettime, 1 /* CLOCK_MONOTONIC */, &now); ++ deadline = now.tv_sec * 1000000000ll + now.tv_nsec + timeout_ns; ++ } ++ for (;;) { ++ if (__atomic_load_n(&s->gen, __ATOMIC_ACQUIRE) != gen) ++ return KERN_TERMINATED; ++ int v = __atomic_load_n(&s->value, __ATOMIC_SEQ_CST); ++ if (v > 0) { ++ if (__atomic_compare_exchange_n(&s->value, &v, v - 1, 0, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST)) ++ return KERN_SUCCESS; ++ continue; ++ } ++ void* ts = 0; ++ if (timeout_ns >= 0) { ++ LINUX_SYSCALL(__NR_clock_gettime, 1, &now); ++ long long left = deadline - (now.tv_sec * 1000000000ll + now.tv_nsec); ++ if (left <= 0) ++ return KERN_OPERATION_TIMED_OUT; ++ rel.tv_sec = left / 1000000000ll; ++ rel.tv_nsec = left % 1000000000ll; ++ ts = &rel; ++ } ++ __atomic_add_fetch(&s->waiters, 1, __ATOMIC_SEQ_CST); ++ LINUX_SYSCALL(__NR_futex, &s->value, LSEM_FUTEX_WAIT, 0, ts, 0UL, 0); ++ __atomic_sub_fetch(&s->waiters, 1, __ATOMIC_SEQ_CST); ++ } ++} ++ ++VISIBLE ++kern_return_t semaphore_create(mach_port_name_t task, mach_port_name_t* semaphore, int policy, int value) ++{ ++ kern_return_t kr = __darling_rpc_semaphore_create(task, semaphore, policy, value); ++ if (kr == KERN_SUCCESS && task == mach_task_self()) { ++ int state = __atomic_load_n(&lsem_state, __ATOMIC_ACQUIRE); ++ if (state == 0) ++ state = lsem_decide(); ++ if (state == 1) ++ lsem_insert(*semaphore, value); ++ } ++ return kr; ++} ++ ++VISIBLE ++kern_return_t semaphore_destroy(mach_port_name_t task, mach_port_name_t semaphore) ++{ ++ if (task == mach_task_self()) ++ lsem_remove(semaphore); ++ return __darling_rpc_semaphore_destroy(task, semaphore); ++} ++ ++/* darling-lol: mach_vm_region_recurse on the caller's own task, answered in the process. ++ * ++ * darlingserver answers this call from the kernel's PROCMAP_QUERY ioctl on /proc//maps (duct-tape memory.c ++ * mach_vm_region_recurse + Process::memoryRegionInfo/getNextRegion). In a League match one thread asks ~9,000 times a ++ * second, which kept two server threads at 60% each and made every other RPC of the game queue behind it (server ++ * 120% CPU in a fresh match, 2026-10-10). The same ioctl on /proc/self/maps returns the same record without the round ++ * trip; the reply is built exactly as the server builds it. Other tasks and failures of the ioctl still go to the server. ++ * Game process only; host file /tmp/rclol-no-local-region turns it off, /tmp/rclol-local-region-all enables it for ++ * every process (probe tests). Refreshed every 1024 queries for live A/B. */ ++struct lregion_query { ++ unsigned long long size, query_flags, query_addr, vma_start, vma_end, vma_flags, vma_page_size, vma_offset, inode; ++ unsigned int dev_major, dev_minor, vma_name_size, build_id_size; ++ unsigned long long vma_name_addr, build_id_addr; ++}; ++#define LREGION_IOCTL 0xC0686611ul /* PROCMAP_QUERY = _IOWR('f', 17, struct procmap_query), 104 bytes */ ++#define LREGION_SHORT_INFO_COUNT 12 /* VM_REGION_SUBMAP_SHORT_INFO_COUNT_64 */ ++struct lregion_short_info { ++ int protection, max_protection; ++ unsigned int inheritance; ++ unsigned long long offset; ++ unsigned int user_tag, ref_count; ++ unsigned short shadow_depth; ++ unsigned char external_pager, share_mode; ++ int is_submap, behavior; ++ unsigned int object_id; ++ unsigned short user_wired_count; ++} __attribute__((packed, aligned(4))); ++static volatile int lregion_state; /* 0 undecided, 1 on, -1 off */ ++static unsigned int lregion_calls; ++static volatile int lregion_refresh; ++static volatile int lregion_maps_fd = -1; ++static volatile int lregion_maps_pid; ++_Static_assert(sizeof(struct lregion_query) == 104, "PROCMAP_QUERY ABI"); ++_Static_assert(sizeof(struct lregion_short_info) == 48, "short region reply ABI"); ++ ++extern kern_return_t __darling_rpc_mach_vm_region_recurse(mach_port_name_t target, unsigned long long* address, unsigned long long* size, ++ unsigned int* depth, int* info, unsigned int* infoCnt); ++ ++/* Pacing. The one heavy caller is a loop that asks again as soon as it has an answer: through the server it made ++ * ~9,000 calls a second using 17% of a core; answered locally without a pause it made several times more, took 85% of a ++ * core, and fights got slower (61 vs 68 fps, 2026-10-10) although the server load halved. A call that follows the ++ * previous one within the pace interval waits out the rest of it, so the call rate stays what it was through the ++ * server while neither the server nor a core is spent on it. Isolated calls are not delayed. Host file ++ * /tmp/rclol-region-pace-us overrides the interval (0 = no pacing). */ ++#define LREGION_DEFAULT_PACE_US 50 /* plus ~50 us of timer slack: ~105 us per call, the rate measured through the server */ ++static volatile long lregion_pace_us = LREGION_DEFAULT_PACE_US; ++static volatile unsigned long long lregion_last_ns; ++ ++static void lregion_pace(void) ++{ ++ long pace = __atomic_load_n(&lregion_pace_us, __ATOMIC_RELAXED); ++ if (pace <= 0) ++ return; ++ struct { long tv_sec; long tv_nsec; } now, rest; ++ LINUX_SYSCALL(__NR_clock_gettime, 1 /* CLOCK_MONOTONIC */, &now); ++ unsigned long long t = now.tv_sec * 1000000000ull + now.tv_nsec; ++ unsigned long long last = __atomic_exchange_n(&lregion_last_ns, t, __ATOMIC_RELAXED); ++ unsigned long long interval = (unsigned long long) pace * 1000ull; ++ if (t >= last && t - last < interval) { ++ rest.tv_sec = 0; ++ rest.tv_nsec = (long) (interval - (t - last)); ++ LINUX_SYSCALL(__NR_nanosleep, &rest, 0UL); ++ LINUX_SYSCALL(__NR_clock_gettime, 1, &now); ++ __atomic_store_n(&lregion_last_ns, now.tv_sec * 1000000000ull + now.tv_nsec, __ATOMIC_RELAXED); ++ } ++} ++ ++VISIBLE ++kern_return_t mach_vm_region_recurse(mach_port_name_t target, unsigned long long* address, unsigned long long* size, ++ unsigned int* depth, int* info, unsigned int* infoCnt) ++{ ++ int state = __atomic_load_n(&lregion_state, __ATOMIC_ACQUIRE); ++ /* The switches are re-read a few times per second only in a process where the feature can be on (same-match A/B). ++ * Everywhere else the first decision is final and this function makes no further system calls of its own: ++ * LeagueClient runs the same scanner, and periodic file opens there were the one new behaviour in that process ++ * when it died twice with an invalid RPC descriptor (2026-10-10). */ ++ if (state == 0 || (lregion_refresh && (__atomic_fetch_add(&lregion_calls, 1, __ATOMIC_RELAXED) & 1023) == 0)) { ++ if (state == 0) ++ lregion_refresh = LINUX_SYSCALL(__NR_access, "/tmp/rclol-local-region-all", 0) == 0 || darling_is_league_game(); ++ if (LINUX_SYSCALL(__NR_access, "/tmp/rclol-local-region-all", 0) == 0) ++ state = 1; ++ else if (LINUX_SYSCALL(__NR_access, "/tmp/rclol-no-local-region", 0) != 0 && darling_is_league_game()) ++ state = 1; ++ else ++ state = -1; ++ __atomic_store_n(&lregion_state, state, __ATOMIC_RELEASE); ++ /* pacing in microseconds (host file, optional): see below */ ++ long pfd = LINUX_SYSCALL(__NR_open, "/tmp/rclol-region-pace-us", 0, 0); ++ if (pfd >= 0) { ++ char digits[16]; ++ long n = LINUX_SYSCALL(__NR_read, pfd, digits, sizeof(digits) - 1); ++ LINUX_SYSCALL(__NR_close, pfd); ++ long value = 0; ++ for (long i = 0; i < n && digits[i] >= '0' && digits[i] <= '9'; ++i) value = value * 10 + (digits[i] - '0'); ++ if (n > 0 && value <= 100000) __atomic_store_n(&lregion_pace_us, value, __ATOMIC_RELAXED); ++ } else { ++ __atomic_store_n(&lregion_pace_us, LREGION_DEFAULT_PACE_US, __ATOMIC_RELAXED); ++ } ++ } ++ /* Any count up to the long structure (19 words): the server only fills the short one (12) and returns the others ++ * zeroed, with the region's address and size (checked with tools/riot-region-count19-check.c). The scanner in ++ * League asks with the long count, so the short-only version of this path never ran in a match. */ ++ if (state == 1 && target == mach_task_self() && address && size && info && infoCnt && ++ *infoCnt >= 1 && *infoCnt <= 19) { ++ /* One descriptor, opened once and kept at a high number. It was opened and closed for every call before ++ * (~9,000 times a second), which churned the low descriptor numbers the rest of the process hands out. ++ * After a fork the inherited descriptor still names the parent's address space, so it is tied to the pid; ++ * if the application closed or replaced it, the ioctl fails and the call goes to the server. */ ++ lregion_pace(); ++ int fd = __atomic_load_n(&lregion_maps_fd, __ATOMIC_ACQUIRE); ++ int self = (int) LINUX_SYSCALL(__NR_getpid); ++ if (fd < 0 || __atomic_load_n(&lregion_maps_pid, __ATOMIC_ACQUIRE) != self) { ++ int low = LINUX_SYSCALL(__NR_open, "/proc/self/maps", 02000000 /* O_RDONLY | O_CLOEXEC */, 0); ++ fd = -1; ++ if (low >= 0) { ++ fd = LINUX_SYSCALL(__NR_fcntl, low, 1030 /* F_DUPFD_CLOEXEC */, 900); ++ if (fd >= 0) ++ LINUX_SYSCALL(__NR_close, low); ++ else ++ fd = low; ++ __atomic_store_n(&lregion_maps_pid, self, __ATOMIC_RELEASE); ++ __atomic_store_n(&lregion_maps_fd, fd, __ATOMIC_RELEASE); ++ } ++ } ++ if (fd >= 0) { ++ struct lregion_query q; ++ for (unsigned long i = 0; i < sizeof(q); ++i) ((char*) &q)[i] = 0; ++ q.size = sizeof(q); ++ q.query_flags = 0x10; /* PROCMAP_QUERY_COVERING_OR_NEXT_VMA */ ++ q.query_addr = *address; ++ long rv = LINUX_SYSCALL(__NR_ioctl, fd, LREGION_IOCTL, &q); ++ if (rv < 0 && rv != -LINUX_ENOENT) ++ __atomic_store_n(&lregion_maps_fd, -1, __ATOMIC_RELEASE); /* not ours any more: reopen on the next call */ ++ if (rv == -LINUX_ENOENT) ++ return KERN_NO_SPACE; /* MIG leaves all caller outputs untouched on failure. */ ++ if (rv == 0 && *infoCnt != LREGION_SHORT_INFO_COUNT) { ++ for (unsigned int i = 0; i < *infoCnt; ++i) info[i] = 0; ++ *address = q.vma_start; ++ *size = q.vma_end - q.vma_start; ++ if (depth) *depth = 0; ++ return KERN_SUCCESS; ++ } ++ if (rv == 0) { ++ struct lregion_short_info* out = (struct lregion_short_info*) info; ++ for (unsigned long i = 0; i < sizeof(*out); ++i) ((char*) out)[i] = 0; ++ out->protection = q.vma_flags & 7; ++ out->offset = q.vma_offset; ++ /* Compatibility with duct-tape memory.c: the server fills the LONG structure, ++ * then MIG copies only its first 48 bytes for a short request. Its share_mode ++ * lands at byte 47 (short-structure trailing padding), not the short share_mode. */ ++ ((unsigned char*)out)[47] = (q.vma_flags & 8) ? 4 : 2; ++ out->max_protection = VM_PROT_READ | VM_PROT_WRITE | VM_PROT_EXECUTE; ++ *address = q.vma_start; ++ *size = q.vma_end - q.vma_start; ++ if (depth) *depth = 0; ++ return KERN_SUCCESS; ++ } ++ } ++ } ++ ++ return __darling_rpc_mach_vm_region_recurse(target, address, size, depth, info, infoCnt); ++} ++ + kern_return_t semaphore_signal_trap_impl( + mach_port_name_t signal_name) + { ++ struct lsem* local = lsem_find(signal_name); ++ if (local) ++ return lsem_signal(local); + int code = dserver_rpc_semaphore_signal(signal_name); ++ int attempt; ++ ++ for (attempt = 0; code == -111 && attempt < 8; attempt++) { ++ dserver_retry_sleep(attempt); ++ code = dserver_rpc_semaphore_signal(signal_name); ++ } + + if (code < 0) { ++ /* darling-lol: fail SOFT, never abort. semaphore_signal is issued ++ * on every pthread exit (sys_bsdthread_terminate posts join_sem, ++ * 0 when no joiner); if the per-thread darlingserver RPC channel ++ * is already stale the RPC fails with -111, and aborting the ++ * whole process for a dying thread's goodbye-signal killed e.g. ++ * Chromium/Electron renderers (SIGILL in __simple_abort, then ++ * SIGABRT). The caller ignores the result; return the error. */ + __simple_printf("semaphore_signal failed (internally): %d\n", code); +- __simple_abort(); + } + + return code; + } +- ++ + kern_return_t semaphore_signal_all_trap_impl( + mach_port_name_t signal_name) + { ++ struct lsem* local = lsem_find(signal_name); ++ if (local) { ++ /* wake everybody who is waiting now; nothing is left over for later waiters */ ++ int n = __atomic_load_n(&local->waiters, __ATOMIC_SEQ_CST); ++ if (n > 0) { ++ __atomic_add_fetch(&local->value, n, __ATOMIC_SEQ_CST); ++ LINUX_SYSCALL(__NR_futex, &local->value, LSEM_FUTEX_WAKE, n, 0UL, 0UL, 0); ++ } ++ return KERN_SUCCESS; ++ } + int code = dserver_rpc_semaphore_signal_all(signal_name); ++ int attempt; ++ ++ for (attempt = 0; code == -111 && attempt < 8; attempt++) { ++ dserver_retry_sleep(attempt); ++ code = dserver_rpc_semaphore_signal_all(signal_name); ++ } + + if (code < 0) { ++ /* darling-lol: fail soft, see semaphore_signal_trap_impl. */ + __simple_printf("semaphore_signal_all failed (internally): %d\n", code); +- __simple_abort(); + } + + return code; +@@ -128,21 +637,40 @@ kern_return_t semaphore_signal_thread_trap_impl( + mach_port_name_t signal_name, + mach_port_name_t thread_name) + { ++ struct lsem* local = lsem_find(signal_name); ++ if (local) { ++ if (thread_name == 0) { ++ /* "any thread": wakes a waiter if there is one, never banks a count */ ++ if (__atomic_load_n(&local->waiters, __ATOMIC_SEQ_CST) <= 0) ++ return KERN_NOT_WAITING; ++ return lsem_signal(local); ++ } ++ return KERN_NOT_WAITING; ++ } + UNIMPLEMENTED_TRAP(); + return KERN_FAILURE; + } + ++ + kern_return_t semaphore_wait_trap_impl( + mach_port_name_t wait_name) + { ++ struct lsem* local = lsem_find(wait_name); ++ if (local) ++ return lsem_wait(local, -1); ++ unsigned long long slowStart = slowwait_enabled() ? slowwait_now() : 0; + int code = dserver_rpc_semaphore_wait(wait_name); ++ if (slowStart) { ++ slowwait_log("semaphore_wait", wait_name, slowStart, (void**) __builtin_frame_address(0), 0, 0); ++ } + + if (code < 0) { + if (code == -LINUX_EINTR) { + return KERN_ABORTED; + } ++ /* darling-lol: fail soft (a waiter whose RPC channel died must ++ * get an error, not kill the process); see above. */ + __simple_printf("semaphore_wait failed (internally): %d\n", code); +- __simple_abort(); + } + + return code; +@@ -152,14 +680,22 @@ kern_return_t semaphore_wait_signal_trap_impl( + mach_port_name_t wait_name, + mach_port_name_t signal_name) + { ++ struct lsem* localWait = lsem_find(wait_name); ++ struct lsem* localSignal = lsem_find(signal_name); ++ if (localWait || localSignal) { ++ kern_return_t kr = localSignal ? lsem_signal(localSignal) : semaphore_signal_trap_impl(signal_name); ++ if (kr != KERN_SUCCESS) ++ return kr; ++ return localWait ? lsem_wait(localWait, -1) : semaphore_wait_trap_impl(wait_name); ++ } + int code = dserver_rpc_semaphore_wait_signal(wait_name, signal_name); + + if (code < 0) { + if (code == -LINUX_EINTR) { + return KERN_ABORTED; + } ++ /* darling-lol: fail soft; see above. */ + __simple_printf("semaphore_wait_signal failed (internally): %d\n", code); +- __simple_abort(); + } + + return code; +@@ -170,14 +706,21 @@ kern_return_t semaphore_timedwait_trap_impl( + unsigned int sec, + clock_res_t nsec) + { ++ struct lsem* local = lsem_find(wait_name); ++ if (local) ++ return lsem_wait(local, (long long) sec * 1000000000ll + nsec); ++ unsigned long long slowStart = slowwait_enabled() ? slowwait_now() : 0; + int code = dserver_rpc_semaphore_timedwait(wait_name, sec, nsec); ++ if (slowStart) { ++ slowwait_log("semaphore_timedwait", wait_name, slowStart, (void**) __builtin_frame_address(0), 0, sec); ++ } + + if (code < 0) { + if (code == -LINUX_EINTR) { + return KERN_ABORTED; + } ++ /* darling-lol: fail soft; see above. */ + __simple_printf("semaphore_timedwait failed (internally): %d\n", code); +- __simple_abort(); + } + + return code; +@@ -189,14 +732,22 @@ kern_return_t semaphore_timedwait_signal_trap_impl( + unsigned int sec, + clock_res_t nsec) + { ++ struct lsem* localWait = lsem_find(wait_name); ++ struct lsem* localSignal = lsem_find(signal_name); ++ if (localWait || localSignal) { ++ kern_return_t kr = localSignal ? lsem_signal(localSignal) : semaphore_signal_trap_impl(signal_name); ++ if (kr != KERN_SUCCESS) ++ return kr; ++ return localWait ? lsem_wait(localWait, (long long) sec * 1000000000ll + nsec) : semaphore_timedwait_trap_impl(wait_name, sec, nsec); ++ } + int code = dserver_rpc_semaphore_timedwait_signal(wait_name, signal_name, sec, nsec); + + if (code < 0) { + if (code == -LINUX_EINTR) { + return KERN_ABORTED; + } ++ /* darling-lol: fail soft; see above. */ + __simple_printf("semaphore_timedwait_signal failed (internally): %d\n", code); +- __simple_abort(); + } + + return code; +@@ -514,6 +1065,12 @@ kern_return_t _kernelrpc_mach_port_construct_trap_impl( + __simple_printf("mach_port_construct failed (internally): %d\n", code); + __simple_abort(); + } ++ #if 0 /* M58 diagnostic. */ ++ if (code == KERN_SUCCESS && name != 0 && *name == MACH_PORT_NULL) { ++ __simple_printf("M58PC construct-success-null target=%u flags=0x%x\n", ++ (unsigned) target, (unsigned) options->flags); ++ } ++ #endif + + return code; + } +@@ -673,16 +1230,16 @@ kern_return_t macx_backing_store_recovery_impl( + return KERN_FAILURE; + } + +-extern void __linux_sched_yield(); ++extern void __linux_sched_yield_site(const char* site); + boolean_t swtch_pri_impl(int pri) + { +- __linux_sched_yield(); ++ __linux_sched_yield_site("SWTPRI"); + return 0; + } + + boolean_t swtch_impl(void) + { +- __linux_sched_yield(); ++ __linux_sched_yield_site("SWTCH"); + return 0; + } + +@@ -696,6 +1253,41 @@ kern_return_t syscall_thread_switch_impl( + .tv_sec = 0, + .tv_nsec = 1000000 + }; ++ ++ /* darling-lol: opt-in trace (host file /tmp/rclol-select-trace) of who calls thread_switch and with which option: ++ * every 50th call writes thread id, option, time and up to 6 caller addresses (frame-pointer walk) to ++ * /tmp/rclol-select-trace.log. Addresses are resolved offline with the process's /proc//maps. */ ++ { ++ static int trace_enabled = -1; ++ static unsigned long calls; ++ if (trace_enabled < 0) ++ trace_enabled = LINUX_SYSCALL(__NR_access, "/tmp/rclol-select-trace", 0) == 0; ++ if (trace_enabled && (__atomic_add_fetch(&calls, 1, __ATOMIC_RELAXED) % 50) == 1) { ++ char line[320]; ++ int used; ++ void** fp = (void**) __builtin_frame_address(0); ++ void* prev = (void*) fp; ++ void* frames[6] = {0}; ++ int count = 0; ++ while (count < 6 && fp && ((unsigned long) fp & 7) == 0 && (void*) fp >= prev && (unsigned long) fp - (unsigned long) prev < (1UL << 20)) { ++ frames[count++] = fp[1]; ++ prev = (void*) fp; ++ fp = (void**) fp[0]; ++ } ++ used = __simple_sprintf(line, "thread_switch pid=%d tid=%d n=%lu option=%d time=%u frames:", ++ (int) LINUX_SYSCALL0(__NR_getpid), (int) LINUX_SYSCALL0(__NR_gettid), calls, option, option_time); ++ for (int i = 0; i < count; ++i) ++ used += __simple_sprintf(line + used, " %lx", (unsigned long) frames[i]); ++ line[used++] = '\n'; ++ line[used] = 0; ++ int fd = LINUX_SYSCALL(__NR_open, "/tmp/rclol-select-trace.log", 0x441, 0644); ++ if (fd >= 0) { ++ LINUX_SYSCALL(__NR_write, fd, line, used); ++ LINUX_SYSCALL1(__NR_close, fd); ++ } ++ } ++ } ++ + // Sleep for 1ms + __linux_nanosleep(&tv, &tv); + +diff --git a/libsyscall/CMakeLists.txt b/libsyscall/CMakeLists.txt +index 57cebad..145ce0d 100755 +--- a/libsyscall/CMakeLists.txt ++++ b/libsyscall/CMakeLists.txt +@@ -179,6 +179,7 @@ set(syscall_sources + wrappers/csr.c + wrappers/fs_snapshot.c + wrappers/__get_cpu_capabilities.S ++ wrappers/__sysctlbyname.S + wrappers/gethostuuid.c + wrappers/getiopolicy_np.c + wrappers/guarded_open_dprotected_np.c +diff --git a/libsyscall/custom/errno.c b/libsyscall/custom/errno.c +index 88658cb..83ce61c 100644 +--- a/libsyscall/custom/errno.c ++++ b/libsyscall/custom/errno.c +@@ -62,6 +62,12 @@ __attribute__((noinline)) + cerror_return_t + cerror_nocancel(int err) + { ++#ifdef DARLING ++ /* ulock's ULF_NO_ERRNO path marks its error with bit 0x800. Its ++ * callers need -errno as the result, with thread errno untouched. */ ++ if (err & 0x800) ++ return (cerror_return_t)(int64_t)(-(err & ~0x800)); ++#endif + errno = err; + int *tsderrno = (int*)_os_tsd_get_direct(__TSD_ERRNO); + if (tsderrno) { +diff --git a/libsyscall/os/tsd.h b/libsyscall/os/tsd.h +index 6619381..78d90df 100644 +--- a/libsyscall/os/tsd.h ++++ b/libsyscall/os/tsd.h +@@ -63,6 +63,22 @@ static __inline__ unsigned int + _os_cpu_number(void) + { + #ifdef DARLING ++#if defined(__x86_64__) ++ /* Linux keeps (node << 12) | cpu in IA32_TSC_AUX, which RDTSCP returns without entering the kernel: this is ++ * what the kernel's own vDSO getcpu does. The syscall below costs ~470 ns and libmalloc asks for the CPU ++ * number ~30,000 times a second on the game's main thread. Verified against sched_getcpu on every core. */ ++ static int __darling_has_rdtscp = -1; ++ if (__builtin_expect(__darling_has_rdtscp < 0, 0)) { ++ unsigned int a, b, c, d; ++ __asm__ volatile("cpuid" : "=a"(a), "=b"(b), "=c"(c), "=d"(d) : "a"(0x80000001), "c"(0)); ++ __darling_has_rdtscp = (d >> 27) & 1; ++ } ++ if (__builtin_expect(__darling_has_rdtscp, 1)) { ++ unsigned int lo, hi, aux; ++ __asm__ volatile("rdtscp" : "=a"(lo), "=d"(hi), "=c"(aux)); ++ return aux & 0xfff; ++ } ++#endif + extern int __linux_syscall(int nr, ...); + + unsigned int cpu_num = 0; +diff --git a/libsyscall/wrappers/__commpage_gettimeofday.c b/libsyscall/wrappers/__commpage_gettimeofday.c +index ccd2b33..1a1a7c6 100644 +--- a/libsyscall/wrappers/__commpage_gettimeofday.c ++++ b/libsyscall/wrappers/__commpage_gettimeofday.c +@@ -66,6 +66,12 @@ __commpage_gettimeofday_internal(struct timeval *tp, uint64_t *tbr_out) + + do { + TimeStamp_tick = *gtod_TimeStamp_tick_p; ++ /* An unpublished timestamp requires the syscall fallback. Avoid ++ * reading absolute time first, especially on syscall-only clocks. ++ */ ++ if (TimeStamp_tick == 0) { ++ return 1; ++ } + /* + * This call contains an instruction barrier which will ensure that the + * second read of the abs time isn't speculated above the reads of the +diff --git a/darling/src/libsystem_kernel/emulation/include/common/private_mutex.h b/darling/src/libsystem_kernel/emulation/include/common/private_mutex.h +new file mode 100644 +--- /dev/null ++++ b/darling/src/libsystem_kernel/emulation/include/common/private_mutex.h +@@ -0,0 +1,20 @@ ++#ifndef DARLING_EMULATION_PRIVATE_MUTEX_H ++#define DARLING_EMULATION_PRIVATE_MUTEX_H ++#include ++/* Bootstrap-safe private mutex. Kernel emulation must not export placeholder ++ * os_unfair_lock functions: umbrella lookup can bind app calls to those stubs ++ * instead of libplatform, silently removing allocator mutual exclusion. */ ++typedef struct { int value; } emulation_mutex; ++#define EMULATION_MUTEX_INITIALIZER {0} ++static inline void emulation_mutex_lock(emulation_mutex *mutex) { ++ int expected = 0; ++ if (__atomic_compare_exchange_n(&mutex->value, &expected, 1, 0, ++ __ATOMIC_ACQUIRE, __ATOMIC_RELAXED)) return; ++ while (__atomic_exchange_n(&mutex->value, 2, __ATOMIC_ACQUIRE) != 0) ++ __linux_futex_reterr(&mutex->value, FUTEX_WAIT | 128, 2, 0, 0, 0); ++} ++static inline void emulation_mutex_unlock(emulation_mutex *mutex) { ++ if (__atomic_exchange_n(&mutex->value, 0, __ATOMIC_RELEASE) == 2) ++ __linux_futex_reterr(&mutex->value, FUTEX_WAKE | 128, 1, 0, 0, 0); ++} ++#endif +diff --git a/darling/src/libsystem_kernel/emulation/include/common/slowwait.h b/darling/src/libsystem_kernel/emulation/include/common/slowwait.h +new file mode 100644 +--- /dev/null ++++ b/darling/src/libsystem_kernel/emulation/include/common/slowwait.h +@@ -0,0 +1,55 @@ ++#ifndef _DARLING_SLOWWAIT_H_ ++#define _DARLING_SLOWWAIT_H_ ++/* darling-lol diagnostic: with /tmp/metal-debug-timing present, log main-thread blocking waits over 400 ms. */ ++#include ++#include ++#include ++ ++/* darling-lol diagnostic: with /tmp/metal-debug-timing present, log mach semaphore waits that block for more than ++ * 400 ms, with the caller's frame-pointer chain, to /tmp/semwait.log. */ ++static inline int slowwait_enabled(void) ++{ ++ static int state = -1; ++ if (state < 0) { ++ state = LINUX_SYSCALL(__NR_access, "/tmp/metal-debug-timing", 0) == 0 ? 1 : 0; ++ } ++ return state; ++} ++ ++static inline unsigned long long slowwait_now(void) ++{ ++ struct { long tv_sec; long tv_nsec; } ts; ++ LINUX_SYSCALL(__NR_clock_gettime, 1, &ts); ++ return (unsigned long long) ts.tv_sec * 1000000000ULL + (unsigned long long) ts.tv_nsec; ++} ++ ++static inline void slowwait_log(const char* kind, unsigned int name, unsigned long long start, void** frame, unsigned int extra1, unsigned int extra2) ++{ ++ unsigned long long elapsed = slowwait_now() - start; ++ /* only the main thread (tid == pid), the frame loop; other threads idle in these waits by design */ ++ if (elapsed < 400ULL * 1000000ULL || LINUX_SYSCALL(__NR_gettid) != LINUX_SYSCALL(__NR_getpid)) { ++ return; ++ } ++ char line[512]; ++ int n = __simple_sprintf(line, "%s name=%u x1=%x x2=%u ms=%llu tid=%d frames:", kind, name, extra1, extra2, elapsed / 1000000ULL, (int) LINUX_SYSCALL(__NR_gettid)); ++ void** fp = frame; ++ for (int i = 0; i < 10 && fp && ((unsigned long) fp & 7) == 0; ++i) { ++ void* ret = fp[1]; ++ n += __simple_sprintf(line + n, " %lx", (unsigned long) ret); ++ void** next = (void**) fp[0]; ++ if (next <= fp || (unsigned long) next - (unsigned long) fp > 0x100000) { ++ break; ++ } ++ fp = next; ++ } ++ n += __simple_sprintf(line + n, "\n"); ++ long fd = LINUX_SYSCALL(__NR_open, "/tmp/semwait.log", 02101 /* O_WRONLY|O_CREAT|O_APPEND */, 0666); ++ if (fd >= 0) { ++ LINUX_SYSCALL(__NR_write, fd, line, n); ++ LINUX_SYSCALL(__NR_close, fd); ++ } ++} ++ ++ ++ ++#endif +diff --git a/darling/src/libsystem_kernel/emulation/include/xnu_syscall/bsd/impl/guarded/change_fdguard_np.h b/darling/src/libsystem_kernel/emulation/include/xnu_syscall/bsd/impl/guarded/change_fdguard_np.h +new file mode 100644 +--- /dev/null ++++ b/darling/src/libsystem_kernel/emulation/include/xnu_syscall/bsd/impl/guarded/change_fdguard_np.h +@@ -0,0 +1,8 @@ ++#ifndef LINUX_CHANGE_FDGUARD_H ++#define LINUX_CHANGE_FDGUARD_H ++ ++#include ++ ++long sys_change_fdguard_np(int fd, uint64_t* guard, uint64_t* nguard, unsigned guardflags, int* fdflagsp); ++ ++#endif // LINUX_CHANGE_FDGUARD_H +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/guarded/change_fdguard_np.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/guarded/change_fdguard_np.c +new file mode 100644 +--- /dev/null ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/guarded/change_fdguard_np.c +@@ -0,0 +1,13 @@ ++#include ++ ++#include ++ ++/* darling-lol M57: Chromium's UDP socket code calls change_fdguard_np ++ * (Darwin syscall 444). Darling's fdguard support is a no-op everywhere ++ * (guarded_open_np/guarded_close_np/guarded_kqueue_np all ignore it), so ++ * accept the request and report success. Without this the syscall was ++ * unhandled (ENOSYS), which made Chromium FATAL in udp_socket_posix.cc. */ ++long sys_change_fdguard_np(int fd, uint64_t* guard, uint64_t* nguard, unsigned guardflags, int* fdflagsp) ++{ ++ return 0; ++} +diff --git a/libsyscall/wrappers/__sysctlbyname.S b/libsyscall/wrappers/__sysctlbyname.S +new file mode 100644 +--- /dev/null ++++ b/libsyscall/wrappers/__sysctlbyname.S +@@ -0,0 +1,12 @@ ++/* ++ * __sysctlbyname wrapper ++ * Bridges libc's sysctlbyname to the syscall stub ___sysctlbyname ++ */ ++ ++.text ++.globl __sysctlbyname ++.globl ___sysctlbyname ++ ++__sysctlbyname: ++ /* Just jump to the syscall stub */ ++ jmp ___sysctlbyname diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c048950 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +requests>=2.32,<3 +urllib3>=2.2,<3 +websocket-client>=1.8,<2 +Pillow>=10.4,<13 diff --git a/research/README.md b/research/README.md new file mode 100644 index 0000000..6300d48 --- /dev/null +++ b/research/README.md @@ -0,0 +1,5 @@ +# Historical experiments + +These authored source tools are retained to preserve the investigation, not as a supported installation path. They include diagnostics, version-specific probes and abandoned experiments. They can change runtime files or use coordinates/offsets that belong to old versions. + +Prefer the scripts at the repository root and the current tools described in `docs/`. Research templates use the same machine-path tokens; inspect and port a tool before running it. Logs, client binaries, game assets, account data, generated shader captures and backups are deliberately absent. diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..087785e --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,11 @@ +#!/bin/sh +set -eu +repo=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +cd "$repo" +python3 scripts/configure.py +if [ -d build/venv/bin ]; then PATH="$repo/build/venv/bin:$PATH"; export PATH; fi +python3 scripts/prepare.py +cmake -S build/darling-src -B build/darling-src/build -G 'Unix Makefiles' \ + -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local -DENABLE_METAL=ON +cmake --build build/darling-src/build --parallel "${JOBS:-$(nproc)}" +echo 'Build complete. Install the base runtime with: sudo cmake --install build/darling-src/build' diff --git a/scripts/common.py b/scripts/common.py new file mode 100755 index 0000000..e01f1cc --- /dev/null +++ b/scripts/common.py @@ -0,0 +1,15 @@ +from pathlib import Path +import getpass,json,os +ROOT=Path(__file__).resolve().parent.parent + +def configuration(): + file=ROOT/'build/config.json' + if file.exists():return json.loads(file.read_text()) + username=getpass.getuser() + return {'project_root':str(ROOT),'host_home':str(Path.home()),'prefix':str(Path.home()/'.darling'),'guest_user':username,'guest_home':'/Users/'+username,'sdk':'/usr/local/libexec/darling'} + +def render(text,config=None): + c=config or configuration() + for token,key in [('PROJECT_ROOT','project_root'),('HOST_HOME','host_home'),('DPREFIX','prefix'),('GUEST_USER','guest_user'),('GUEST_HOME','guest_home'),('DARLING_SDK','sdk')]: + text=text.replace('@'+token+'@',c[key]) + return text diff --git a/scripts/configure.py b/scripts/configure.py new file mode 100755 index 0000000..3ea8d2f --- /dev/null +++ b/scripts/configure.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +"""Render local paths into ignored runtime tools; never alter the source snapshot.""" +import argparse,json,os,shutil +from pathlib import Path +from common import ROOT,configuration,render +p=argparse.ArgumentParser(description=__doc__) +p.add_argument('--guest-user');p.add_argument('--prefix',type=Path);p.add_argument('--sdk',type=Path) +a=p.parse_args();c=configuration() +if a.guest_user: + if not a.guest_user.replace('_','').replace('-','').isalnum():p.error('guest user must be a simple account name') + c['guest_user']=a.guest_user;c['guest_home']='/Users/'+a.guest_user +if a.prefix:c['prefix']=str(a.prefix.expanduser().resolve()) +if a.sdk:c['sdk']=str(a.sdk.expanduser().resolve()) +c['project_root']=str(ROOT);c['host_home']=str(Path.home()) +build=ROOT/'build';build.mkdir(exist_ok=True) +(build/'config.json').write_text(json.dumps(c,indent=2)+'\n') +folder=build/'runtime/tools';folder.mkdir(parents=True,exist_ok=True) +for source in (ROOT/'tools').iterdir(): + if not source.is_file():continue + target=folder/source.name;target.write_text(render(source.read_text(),c));os.chmod(target,source.stat().st_mode & 0o777) +link=folder.parent/'build' +if not link.exists():link.symlink_to(build,target_is_directory=True) +print('Configured',len(list(folder.iterdir())),'runtime sources for',c['guest_home']) diff --git a/scripts/install-runtime.sh b/scripts/install-runtime.sh new file mode 100755 index 0000000..9405b6c --- /dev/null +++ b/scripts/install-runtime.sh @@ -0,0 +1,14 @@ +#!/bin/sh +# Builds and installs the paired per-user runtime; does not copy game files. +set -eu +repo=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +cd "$repo" +python3 scripts/configure.py +if [ -d build/venv/bin ]; then PATH="$repo/build/venv/bin:$PATH"; export PATH; fi +# Initialize the normal user-owned prefix before installing overrides. +darling shell /usr/bin/true +python3 build/runtime/tools/stop-darling-runtime.py +sh build/runtime/tools/install-riot-presentation-runtime.sh +sh build/runtime/tools/install-riot-gpu-runtime.sh +python3 build/runtime/tools/install-darling-system-profiler.py +python3 scripts/install-shim.py diff --git a/scripts/install-shim.py b/scripts/install-shim.py new file mode 100755 index 0000000..08d3f36 --- /dev/null +++ b/scripts/install-shim.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 +import os,subprocess +from pathlib import Path +from common import ROOT,configuration +c=configuration();guest=Path(c['prefix'])/c['guest_home'].lstrip('/') +guest.mkdir(parents=True,exist_ok=True) +source=ROOT/'build/runtime/tools/m58f101-logdir-shim.c';out=guest/'zzlogdir.dylib' +subprocess.run(['clang','-target','x86_64-apple-macosx11.0','-fuse-ld=lld','-dynamiclib','-nostdlib','-nodefaultlibs','-undefined','dynamic_lookup','-O2',str(source),'-Wl,-install_name,'+c['guest_home']+'/zzlogdir.dylib','-o',str(out)],check=True) +print('Installed launch argument shim') diff --git a/scripts/installer.sh b/scripts/installer.sh new file mode 100755 index 0000000..3d2e2a9 --- /dev/null +++ b/scripts/installer.sh @@ -0,0 +1,13 @@ +#!/bin/sh +set -eu +repo=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +cd "$repo" +region=${1:-euw} +case "$region" in euw|eune|na|br|kr|jp|lan|las|oce|ru|tr) ;; *) echo 'Use a Riot installer region code' >&2; exit 1;; esac +python3 scripts/configure.py >/dev/null +sh build/runtime/tools/fetch-installer.sh "$region" build/installer +unzip -q -o "build/installer/live.$region.zip" -d build/installer +RIOT_LAUNCHER_HOST="$repo/build/installer/Install League of Legends $region.app/Contents/MacOS/RiotClientServices" +RCLOL_REGION=$(printf '%s' "$region" | tr '[:lower:]' '[:upper:]') +export RIOT_LAUNCHER_HOST RCLOL_REGION +exec sh scripts/run.sh diff --git a/scripts/prepare.py b/scripts/prepare.py new file mode 100755 index 0000000..b50f5ba --- /dev/null +++ b/scripts/prepare.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +"""Fetch pinned Darling sources and apply each patch to its owning repository.""" +import argparse,json,os,subprocess,tempfile +from pathlib import Path +from common import ROOT,render +p=argparse.ArgumentParser(description=__doc__) +p.add_argument('--source',type=Path,default=ROOT/'build/darling-src') +p.add_argument('--check-only',action='store_true',help='validate against temporary base indexes, without changing existing source') +a=p.parse_args();source=a.source.resolve();manifest=json.loads((ROOT/'upstream/manifest.json').read_text()) +def run(args,**kwargs):return subprocess.run(args,check=True,**kwargs) +if not (source/'.git').exists(): + if a.check_only:p.error('check-only requires an existing pinned source checkout') + source.parent.mkdir(parents=True,exist_ok=True) + run(['git','clone',manifest['upstream'],str(source)]) + run(['git','-C',str(source),'checkout','--detach',manifest['revision']]) + run(['git','-C',str(source),'submodule','update','--init','--recursive']) +# Never reset someone else's checkout to make a patch apply. +for component in manifest['components']: + repo=source/component['path'] + actual=subprocess.check_output(['git','-C',str(repo),'rev-parse','HEAD'],text=True).strip() + if actual!=component['revision']:raise SystemExit(f"Wrong revision for {component['path']}: expected {component['revision']}, got {actual}") + patch=render((ROOT/component['patch']).read_text()) + if a.check_only: + with tempfile.TemporaryDirectory(prefix='darling-patch-index-') as tmp: + env=dict(os.environ,GIT_INDEX_FILE=str(Path(tmp)/'index')) + run(['git','-C',str(repo),'read-tree',component['revision']],env=env) + run(['git','-C',str(repo),'apply','--cached','--check','--whitespace=nowarn','-'],input=patch,text=True,env=env) + else: + already=subprocess.run(['git','-C',str(repo),'apply','--reverse','--check','-'],input=patch,text=True,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL).returncode==0 + if not already: + run(['git','-C',str(repo),'apply','--check','-'],input=patch,text=True) + run(['git','-C',str(repo),'apply','--whitespace=nowarn','-'],input=patch,text=True) + print(('Checked ' if a.check_only else 'Prepared ')+component['path'],flush=True) +print('All',len(manifest['components']),'component patches validated' if a.check_only else 'component patches ready') diff --git a/scripts/run.sh b/scripts/run.sh new file mode 100755 index 0000000..4cfe1c7 --- /dev/null +++ b/scripts/run.sh @@ -0,0 +1,7 @@ +#!/bin/sh +set -eu +repo=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +cd "$repo" +python3 scripts/configure.py >/dev/null +if [ -d build/venv/bin ]; then PATH="$repo/build/venv/bin:$PATH"; export PATH; fi +exec sh build/runtime/tools/run-riot-ux-with-logdir-shim.sh diff --git a/scripts/validate.py b/scripts/validate.py new file mode 100644 index 0000000..2884cf6 --- /dev/null +++ b/scripts/validate.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +"""Validate the public source package without launching apps or installing anything.""" +import ast,json,re,subprocess,sys +from pathlib import Path +from common import ROOT +subprocess.run([sys.executable,str(ROOT/'scripts/configure.py')],check=True) +files=subprocess.check_output(['git','-C',str(ROOT),'ls-files','-z']).decode().split('\0') +errors=[] +secret_patterns=[r'gh[pousr]_[A-Za-z0-9]{30,}',r'AKIA[0-9A-Z]{16}',r'eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}',r'-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----'] +for name in filter(None,files): + p=ROOT/name;data=p.read_bytes() + if data.startswith((b'\x7fELF',b'MZ',b'\xcf\xfa\xed\xfe',b'\xca\xfe\xba\xbe')) or b'\0' in data: + errors.append(name+': binary content is not part of this source package');continue + try:text=data.decode() + except UnicodeDecodeError:errors.append(name+': non-UTF8 content');continue + if re.search(r'/(?:home|Users)/' + 'hi' + 'rew' + r'(?:/|\b)',text): + errors.append(name+': development-machine path') + if any(re.search(pattern,text) for pattern in secret_patterns):errors.append(name+': possible credential; review before publishing') + if name.split('/')[0] in ['build','logs','Config','Library'] or p.suffix in ['.dylib','.so','.asar','.perf','.zip']: + errors.append(name+': local artifact') +for p in list((ROOT/'scripts').glob('*.py'))+list((ROOT/'build/runtime/tools').glob('*.py')): + try:ast.parse(p.read_text(),filename=str(p)) + except SyntaxError as e:errors.append(p.name+': '+str(e)) +for p in list((ROOT/'scripts').glob('*.sh'))+list((ROOT/'build/runtime/tools').glob('*.sh')): + result=subprocess.run(['bash','-n',str(p)],capture_output=True,text=True) + if result.returncode:errors.append(p.name+': shell syntax error') +manifest=json.loads((ROOT/'upstream/manifest.json').read_text()) +for c in manifest['components']: + if not re.fullmatch('[0-9a-f]{40}',c['revision']):errors.append(c['path']+': invalid pinned revision') + if not (ROOT/c['patch']).is_file():errors.append(c['path']+': patch missing') +if errors: + print('\n'.join(errors));raise SystemExit(1) +print('PASS: tracked source audit, rendered Python/shell syntax and',len(manifest['components']),'pinned patch entries') diff --git a/tools/analyze-macho.sh b/tools/analyze-macho.sh new file mode 100755 index 0000000..8f8dd1d --- /dev/null +++ b/tools/analyze-macho.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# Mach-O inspection for RiotClientServices. Usage: ./analyze-macho.sh [outdir] +# Prefers llvm-otool/llvm-objdump (from clang), falls back to otool if present. +set -euo pipefail +BIN="${1:?usage: analyze-macho.sh [outdir]}" +OUT="${2:-logs/macho}" +mkdir -p "$OUT" +OTOOL="$(command -v llvm-otool || command -v otool || true)" +OBJDUMP="$(command -v llvm-objdump || true)" +file "$BIN" | tee "$OUT/file.txt" +[ -n "$OTOOL" ] && { "$OTOOL" -hv "$BIN" > "$OUT/headers.txt" 2>&1; "$OTOOL" -L "$BIN" > "$OUT/linked-libs.txt" 2>&1; echo "wrote headers + linked-libs"; } || echo "no otool/llvm-otool found" +[ -n "$OBJDUMP" ] && "$OBJDUMP" --macho --dylibs-used "$BIN" > "$OUT/dylibs.txt" 2>&1 || true +strings -a "$BIN" | grep -iE "Metal|MTL|Vanguard|vgc|CEF|Chromium|RiotClient|NSApplication|LSOpen|CFBundle" | sort -u | head -100 | tee "$OUT/strings-of-interest.txt" +echo "done -> $OUT/" diff --git a/tools/build-m58f327-opengl.py b/tools/build-m58f327-opengl.py new file mode 100644 index 0000000..31e490e --- /dev/null +++ b/tools/build-m58f327-opengl.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +"""Build isolated CGL candidates using the configured build's compile/link flags.""" +import shlex +import subprocess +from pathlib import Path +project = Path(__file__).resolve().parent.parent +framework = project / 'build/darling-src/build/src/frameworks/OpenGL' +flags = {} +for line in (framework/'CMakeFiles/OpenGL.dir/flags.make').read_text().splitlines(): + if line.startswith('C_') and ' = ' in line: + key, value = line.split(' = ', 1) + flags[key] = shlex.split(value) +def run(args, cwd=project): + result = subprocess.run(args, cwd=cwd, capture_output=True, text=True) + if result.returncode: + raise SystemExit(result.stderr[-4000:]) +base = ['clang', *flags['C_DEFINES'], *flags['C_INCLUDES'], *flags['C_FLAGS'], '-O2', '-Wno-macro-redefined'] +object_file = project/'build/m327-opengl.o' +run([*base, '-c', str(project/'build/darling-src/src/frameworks/OpenGL/OpenGL.c'), '-o', str(object_file)]) +link = shlex.split((framework/'CMakeFiles/OpenGL.dir/link.txt').read_text()) +link[link.index('CMakeFiles/OpenGL.dir/OpenGL.c.o')] = str(object_file) +link[link.index('-o')+1] = str(project/'build/m327-OpenGL') +run(link, framework) +for name in ['context', 'render', 'planes', 'direct']: + obj = project/f'build/m327-{name}-check.o' + source = project/'tools/m58f352-cgl-direct-check.c' if name == 'direct' else project/'tools/m58f340-cgl-planes-check.c' if name == 'planes' else project/f'tools/m58f327-cgl-{name}-check.c' + run([*base, '-c', str(source), '-o', str(obj)]) + run(['clang', '-target', 'x86_64-apple-macosx11.0', '-fuse-ld=lld', '-nostdlib', + '-isysroot', '@DARLING_SDK@', str(obj), '-lSystem', '-framework', 'OpenGL', + '-framework', 'CoreFoundation', '-framework', 'CoreGraphics', '-framework', 'IOSurface', + *(['-framework','QuartzCore'] if name == 'planes' else []), + '-o', str(project/f'build/m327-cgl-{name}-check')]) +print('Built isolated OpenGL candidate and CGL validation probes') diff --git a/tools/build-m58f333-iosurface.py b/tools/build-m58f333-iosurface.py new file mode 100644 index 0000000..f8841d4 --- /dev/null +++ b/tools/build-m58f333-iosurface.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +"""Build the IOSurface candidate without rebuilding unrelated targets.""" +import shlex, subprocess +from pathlib import Path +project=Path(__file__).resolve().parent.parent +framework=project/'build/darling-src/build/src/frameworks/IOSurface' +flags={} +for line in (framework/'CMakeFiles/IOSurface.dir/flags.make').read_text().splitlines(): + if line.startswith('C_') and ' = ' in line: + k,v=line.split(' = ',1);flags[k]=shlex.split(v) +link=shlex.split((framework/'CMakeFiles/IOSurface.dir/link.txt').read_text()) +for name in ['stubs','api','constants','IOSurfaceObjC']: + obj=project/f'build/m333-{name}.o' + subprocess.run(['clang',*flags['C_DEFINES'],*flags['C_INCLUDES'],*flags['C_FLAGS'],'-O2','-c',str(project/f'build/darling-src/src/frameworks/IOSurface/src/{name}.m'),'-o',str(obj)],check=True) + link[link.index(f'CMakeFiles/IOSurface.dir/src/{name}.m.o')]=str(obj) +link[link.index('-o')+1]=str(project/'build/m333-IOSurface') +subprocess.run(link,cwd=framework,check=True) +probe=project/'build/m333-lifetime.o' +subprocess.run(['clang',*flags['C_DEFINES'],*flags['C_INCLUDES'],*flags['C_FLAGS'],'-Wno-macro-redefined','-c',str(project/'tools/m58f333-iosurface-lifetime-check.c'),'-o',str(probe)],check=True) +subprocess.run(['clang','-target','x86_64-apple-macosx11.0','-fuse-ld=lld','-nostdlib','-isysroot','@DARLING_SDK@',str(probe),'-lSystem','-framework','CoreFoundation','-framework','IOSurface','-o',str(project/'build/m333-lifetime-check')],check=True) +print('Built isolated IOSurface candidate and lifetime probe') + +probe=project/'build/riot-import-cache-check.o' +subprocess.run(['clang',*flags['C_DEFINES'],*flags['C_INCLUDES'],*flags['C_FLAGS'],'-O2','-c',str(project/'tools/riot-iosurface-import-cache-check.c'),'-o',str(probe)],check=True) +subprocess.run(['clang','-target','x86_64-apple-macosx11.0','-fuse-ld=lld','-nostdlib','-isysroot','@DARLING_SDK@',str(probe),'-lSystem','-framework','CoreFoundation','-framework','IOSurface','-o',str(project/'build/riot-import-cache-check')],check=True) diff --git a/tools/build-m58f336-quartzcore.py b/tools/build-m58f336-quartzcore.py new file mode 100644 index 0000000..dc372e1 --- /dev/null +++ b/tools/build-m58f336-quartzcore.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +"""Build only the changed GPU frame publisher and relink QuartzCore.""" +import shlex,subprocess +from pathlib import Path +project=Path(__file__).resolve().parent.parent +framework=project/'build/darling-src/build/src/external/cocotron/QuartzCore' +flags={} +for line in (framework/'CMakeFiles/QuartzCore.dir/flags.make').read_text().splitlines(): + if line.startswith('C_') and ' = ' in line: + k,v=line.split(' = ',1);flags[k]=shlex.split(v) +link=shlex.split((framework/'CMakeFiles/QuartzCore.dir/link.txt').read_text()) +for name in ('CALayer','CALayerContext','CATransaction'): + obj=project/('build/m336-'+name+'.o') + subprocess.run(['clang',*flags['C_DEFINES'],*flags['C_INCLUDES'],*flags['C_FLAGS'],'-O2','-Wno-macro-redefined','-c',str(project/('build/darling-src/src/external/cocotron/QuartzCore/'+name+'.m')),'-o',str(obj)],check=True) + link[link.index('CMakeFiles/QuartzCore.dir/'+name+'.m.o')]=str(obj) +link[link.index('-o')+1]=str(project/'build/m336-QuartzCore') +subprocess.run(link,cwd=framework,check=True) +print('Built isolated QuartzCore frame publisher') diff --git a/tools/build-m58f341-corevideo.py b/tools/build-m58f341-corevideo.py new file mode 100644 index 0000000..99abb83 --- /dev/null +++ b/tools/build-m58f341-corevideo.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +"""Build the corrected display clock as an isolated per-user candidate.""" +import shlex, subprocess +from pathlib import Path +project=Path(__file__).resolve().parent.parent +framework=project/'build/darling-src/build/src/frameworks/CoreVideo' +flags={} +for line in (framework/'CMakeFiles/CoreVideo.dir/flags.make').read_text().splitlines(): + if line.startswith(('C_','CXX_')) and ' = ' in line: + k,v=line.split(' = ',1);flags[k]=shlex.split(v) +obj=project/'build/m341-CVDisplayLink.o' +subprocess.run(['clang++',*flags['CXX_DEFINES'],*flags['CXX_INCLUDES'],*flags['CXX_FLAGS'],'-O2','-Wno-macro-redefined','-c',str(project/'build/darling-src/src/frameworks/CoreVideo/src/CVDisplayLink.mm'),'-o',str(obj)],check=True) +link=shlex.split((framework/'CMakeFiles/CoreVideo.dir/link.txt').read_text()) +link[link.index('CMakeFiles/CoreVideo.dir/src/CVDisplayLink.mm.o')]=str(obj) +link[link.index('-o')+1]=str(project/'build/m341-CoreVideo') +subprocess.run(link,cwd=framework,check=True) +probe=project/'build/m341-clock-check.o' +subprocess.run(['clang',*flags['C_DEFINES'],*flags['C_INCLUDES'],*flags['C_FLAGS'],'-Wno-macro-redefined','-c',str(project/'tools/m58f341-display-clock-check.c'),'-o',str(probe)],check=True) +subprocess.run(['clang','-target','x86_64-apple-macosx11.0','-fuse-ld=lld','-nostdlib','-isysroot','@DARLING_SDK@',str(probe),'-lSystem','-framework','CoreVideo','-o',str(project/'build/m341-clock-check')],check=True) +print('Built isolated CoreVideo display clock and cadence probe') diff --git a/tools/build-riot-air-check.py b/tools/build-riot-air-check.py new file mode 100644 index 0000000..0ca61fd --- /dev/null +++ b/tools/build-riot-air-check.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +"""Build Darling shader probes, or a native offline translator with --native.""" +from pathlib import Path +import shlex +import subprocess +import sys +root = Path(__file__).resolve().parent.parent +if '--native' in sys.argv: + indium = root / 'build/darling-src/src/external/metal/deps/indium' + subprocess.run(['clang++', '-O2', '-std=c++17', + '-DHOST_LLVM_LIBNAME="libLLVM-22.so"', + '-I' + str(indium / 'include'), '-I' + str(indium / 'private-include'), + str(root / 'tools/riot-air-translate-check.cpp'), + *[str(indium / 'src/iridium' / (name + '.cpp')) + for name in ('air', 'dynamic-llvm', 'iridium', 'spirv')], + '-ldl', '-o', str(root / 'build/riot-air-translate-check-native')], check=True) + sys.exit(0) +folder = root / 'build/darling-src/build/src/external/metal' +flags = {} +for line in (folder / 'CMakeFiles/Metal.dir/flags.make').read_text().splitlines(): + if line.startswith('CXX_') and ' = ' in line: + key, value = line.split(' = ', 1) + flags[key] = shlex.split(value) +obj = root / 'build/riot-air-corpus-check.o' +subprocess.run(['clang++', *flags['CXX_DEFINES'], *flags['CXX_INCLUDES'], *flags['CXX_FLAGS'], + '-c', str(root / 'tools/riot-air-corpus-check.mm'), '-o', str(obj)], check=True) +subprocess.run(['clang++', '-target', 'x86_64-apple-macosx11.0', '-fuse-ld=lld', '-nostdlib', + '-isysroot', '@DARLING_SDK@', str(obj), '-lSystem', '-lc++', '-lobjc', + '-framework', 'Foundation', str(root / 'build/riot-Metal'), '-o', + str(root / 'build/riot-air-corpus-check')], check=True) + +obj = root / 'build/riot-air-translate-check.o' +subprocess.run(['clang++', *flags['CXX_DEFINES'], *flags['CXX_INCLUDES'], *flags['CXX_FLAGS'], + '-c', str(root / 'tools/riot-air-translate-check.cpp'), '-o', str(obj)], check=True) +subprocess.run(['clang++', '-target', 'x86_64-apple-macosx11.0', '-fuse-ld=lld', '-nostdlib', + '-isysroot', '@DARLING_SDK@', str(obj), '-lSystem', '-lc++', + str(root / 'build/riot-libiridium.dylib'), '-o', + str(root / 'build/riot-air-translate-check')], check=True) diff --git a/tools/build-riot-appkit-runtime.py b/tools/build-riot-appkit-runtime.py new file mode 100644 index 0000000..096976e --- /dev/null +++ b/tools/build-riot-appkit-runtime.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +"""Rebuild NSWindow's tracking dispatch against the configured AppKit build.""" +from pathlib import Path +import shlex +import subprocess + +project = Path(__file__).resolve().parent.parent +framework = project / 'build/darling-src/build/src/external/cocotron/AppKit' +flags = {} +for line in (framework / 'CMakeFiles/AppKit.dir/flags.make').read_text().splitlines(): + if line.startswith('C_') and ' = ' in line: + name, value = line.split(' = ', 1) + flags[name] = shlex.split(value) +link = shlex.split((framework / 'CMakeFiles/AppKit.dir/link.txt').read_text()) +for name in ['NSResponder', 'NSApplication', 'NSWindow', 'NSDisplay', 'NSView', 'NSThemeFrame', 'NSTextInput', 'NSBitmapImageRep', 'NSEvent.subproj/NSEvent_mouse']: + obj = project / f'build/riot-{Path(name).name}.o' + subprocess.run(['clang', *flags['C_DEFINES'], *flags['C_INCLUDES'], *flags['C_FLAGS'], + '-O2', '-Wno-macro-redefined', '-c', + str(project / f'build/darling-src/src/external/cocotron/AppKit/{name}.m'), + '-o', str(obj)], check=True) + link[link.index(f'CMakeFiles/AppKit.dir/{name}.m.o')] = str(obj) +link[link.index('-o') + 1] = str(project / 'build/riot-AppKit') +subprocess.run(link, cwd=framework, check=True) +for name in ['riot-event-coalescing-check', 'riot-scroll-event-check', 'riot-appkit-pump-check', 'riot-window-layout-check', 'riot-game-input-check', 'riot-mouse-input-check']: + probe = project / f'build/{name}.o' + subprocess.run(['clang', *flags['C_DEFINES'], *flags['C_INCLUDES'], *flags['C_FLAGS'], + '-O2', '-fblocks', '-Wno-macro-redefined', '-c', + str(project / f'tools/{name}.m'), '-o', str(probe)], check=True) + subprocess.run(['clang', '-target', 'x86_64-apple-macosx11.0', '-fuse-ld=lld', '-nostdlib', + '-isysroot', '@DARLING_SDK@', str(probe), '-lSystem', + '-framework', 'AppKit', '-framework', 'Foundation', '-framework', + 'CoreGraphics', '-lobjc', '-o', str(project / f'build/{name}')], check=True) diff --git a/tools/build-riot-audio-runtime.py b/tools/build-riot-audio-runtime.py new file mode 100644 index 0000000..38bff39 --- /dev/null +++ b/tools/build-riot-audio-runtime.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +"""Build CoreAudio and its device-enumeration regression check.""" +from pathlib import Path +import shlex +import subprocess + +project=Path(__file__).resolve().parent.parent +build=project/'build/darling-src/build' +framework=build/'src/CoreAudio/CoreAudio' +cpp_flags={} +for line in (framework/'CMakeFiles/CoreAudio.dir/flags.make').read_text().splitlines(): + if line.startswith('CXX_') and ' = ' in line: + key,value=line.split(' = ',1);cpp_flags[key]=shlex.split(value) +link=shlex.split((framework/'CMakeFiles/CoreAudio.dir/link.txt').read_text()) +for name in ['AudioHardware','AudioHardwareImpl','pulse/AudioHardwareImplPA', + 'pulse/PADispatchMainLoop','pulse/AudioHardwareStreamPA', + 'pulse/AudioHardwareStreamPAOutput','pulse/AudioHardwareStreamPAInput', + 'pulse/AudioHardwareImplPAOutput','pulse/AudioHardwareImplPAInput']: + obj=project/f'build/riot-audio-{Path(name).name}.o' + subprocess.run(['clang++',*cpp_flags['CXX_DEFINES'],*cpp_flags['CXX_INCLUDES'], + *cpp_flags['CXX_FLAGS'],'-O2','-c',str(project/f'build/darling-src/src/CoreAudio/CoreAudio/{name}.cpp'), + '-o',str(obj)],check=True) + link[link.index(f'CMakeFiles/CoreAudio.dir/{name}.cpp.o')]=str(obj) +link[link.index('-o')+1]=str(project/'build/riot-CoreAudio') +subprocess.run(link,cwd=framework,check=True) +component=build/'src/CoreAudio/CoreAudioComponent' +component_flags={} +for line in (component/'CMakeFiles/CoreAudioComponent.dir/flags.make').read_text().splitlines(): + if line.startswith('CXX_') and ' = ' in line: + key,value=line.split(' = ',1);component_flags[key]=shlex.split(value) +obj=project/'build/riot-AUHAL.o' +subprocess.run(['clang++',*component_flags['CXX_DEFINES'],*component_flags['CXX_INCLUDES'], + *component_flags['CXX_FLAGS'],'-O2','-c',str(project/'build/darling-src/src/CoreAudio/CoreAudioComponent/AUHAL.cpp'), + '-o',str(obj)],check=True) +link=shlex.split((component/'CMakeFiles/CoreAudioComponent.dir/link.txt').read_text()) +link[link.index('CMakeFiles/CoreAudioComponent.dir/AUHAL.cpp.o')]=str(obj) +obj=project/'build/riot-MatrixMixerAU.o' +subprocess.run(['clang++',*component_flags['CXX_DEFINES'],*component_flags['CXX_INCLUDES'], + *component_flags['CXX_FLAGS'],'-O2','-c',str(project/'build/darling-src/src/CoreAudio/CoreAudioComponent/MatrixMixerAU.cpp'), + '-o',str(obj)],check=True) +matrix_obj='CMakeFiles/CoreAudioComponent.dir/MatrixMixerAU.cpp.o' +if matrix_obj in link: + link[link.index(matrix_obj)]=str(obj) +else: + link.append(str(obj)) +# The default-output unit must be recompiled too: its SetProperty refuses kAudioOutputUnitProperty_CurrentDevice unless patched. +obj=project/'build/riot-DefaultOutputAU.o' +subprocess.run(['clang++',*component_flags['CXX_DEFINES'],*component_flags['CXX_INCLUDES'], + *component_flags['CXX_FLAGS'],'-O2','-c',str(project/'build/darling-src/src/CoreAudio/CoreAudioComponent/DefaultOutputAU.cpp'), + '-o',str(obj)],check=True) +default_obj='CMakeFiles/CoreAudioComponent.dir/DefaultOutputAU.cpp.o' +assert default_obj in link, 'DefaultOutputAU object missing from the link line' +link[link.index(default_obj)]=str(obj) +link[link.index('-o')+1]=str(project/'build/riot-CoreAudioComponent') +subprocess.run(link,cwd=component,check=True) +toolbox=build/'src/CoreAudio/AudioToolbox' +toolbox_flags={} +for line in (toolbox/'CMakeFiles/AudioToolbox.dir/flags.make').read_text().splitlines(): + if line.startswith('CXX_') and ' = ' in line: + key,value=line.split(' = ',1);toolbox_flags[key]=shlex.split(value) +link=shlex.split((toolbox/'CMakeFiles/AudioToolbox.dir/link.txt').read_text()) +for name in ['AudioComponentManager.mm','AudioConverter.cpp','AudioConverterImpl.cpp','AUGraph.cpp','AUComponent.cpp','AudioFileComponent.cpp']: + obj=project/f'build/riot-toolbox-{Path(name).stem}.o' + subprocess.run(['clang++',*toolbox_flags['CXX_DEFINES'],*toolbox_flags['CXX_INCLUDES'], + *toolbox_flags['CXX_FLAGS'],'-O2','-c',str(project/f'build/darling-src/src/CoreAudio/AudioToolbox/{name}'), + '-o',str(obj)],check=True) + link[link.index(f'CMakeFiles/AudioToolbox.dir/{name}.o')]=str(obj) +link[link.index('-o')+1]=str(project/'build/riot-AudioToolbox') +subprocess.run(link,cwd=toolbox,check=True) +flags={} +for line in (build/'src/frameworks/CoreVideo/CMakeFiles/CoreVideo.dir/flags.make').read_text().splitlines(): + if line.startswith('C_') and ' = ' in line: + key,value=line.split(' = ',1);flags[key]=shlex.split(value) +obj=project/'build/riot-audio-device-check.o' +subprocess.run(['clang',*flags['C_DEFINES'],*flags['C_INCLUDES'],*flags['C_FLAGS'], + '-O2','-c',str(project/'tools/riot-audio-device-check.c'),'-o',str(obj)],check=True) +subprocess.run(['clang','-target','x86_64-apple-macosx11.0','-fuse-ld=lld','-nostdlib', + '-isysroot','@DARLING_SDK@',str(obj),'-lSystem','-framework','CoreAudio','-framework','CoreFoundation', + '-o',str(project/'build/riot-audio-device-check')],check=True) +obj=project/'build/riot-audio-unit-check.o' +subprocess.run(['clang++',*cpp_flags['CXX_DEFINES'],*cpp_flags['CXX_INCLUDES'], + *cpp_flags['CXX_FLAGS'],'-O2','-c',str(project/'tools/riot-audio-unit-check.cpp'),'-o',str(obj)],check=True) +subprocess.run(['clang++','-target','x86_64-apple-macosx11.0','-fuse-ld=lld','-nostdlib', + '-isysroot','@DARLING_SDK@',str(obj),'-lSystem','-lc++','-framework','CoreAudio', + '-framework','AudioToolbox','-o',str(project/'build/riot-audio-unit-check')],check=True) + +obj=project/'build/riot-audio-graph-check.o' +subprocess.run(['clang++',*cpp_flags['CXX_DEFINES'],*cpp_flags['CXX_INCLUDES'], + *cpp_flags['CXX_FLAGS'],'-O2','-c',str(project/'tools/riot-audio-graph-check.cpp'),'-o',str(obj)],check=True) +subprocess.run(['clang++','-target','x86_64-apple-macosx11.0','-fuse-ld=lld','-nostdlib', + '-isysroot','@DARLING_SDK@',str(obj),'-lSystem','-lc++', + str(project/'build/riot-AudioToolbox'),'-o',str(project/'build/riot-audio-graph-check')],check=True) diff --git a/tools/build-riot-clock-fastpath-check.py b/tools/build-riot-clock-fastpath-check.py new file mode 100644 index 0000000..109eaa3 --- /dev/null +++ b/tools/build-riot-clock-fastpath-check.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 +from pathlib import Path +import shlex, subprocess +root=Path(__file__).resolve().parent.parent +flags={} +for line in (root/'build/darling-src/build/src/frameworks/CoreVideo/CMakeFiles/CoreVideo.dir/flags.make').read_text().splitlines(): + if line.startswith('C_') and ' = ' in line: + key,value=line.split(' = ',1); flags[key]=shlex.split(value) +obj=root/'build/riot-clock-fastpath-check.o' +subprocess.run(['clang',*flags['C_DEFINES'],*flags['C_INCLUDES'],*flags['C_FLAGS'],'-I'+str(root/'build/darling-src/src/startup/mldr/elfcalls'),'-O2','-c',str(root/'tools/riot-clock-fastpath-check.c'),'-o',str(obj)],check=True) +subprocess.run(['clang','-target','x86_64-apple-macosx11.0','-fuse-ld=lld','-nostdlib','-isysroot','@DARLING_SDK@',str(obj),'-lSystem','-o',str(root/'build/riot-clock-fastpath-check')],check=True) diff --git a/tools/build-riot-codesign-check.py b/tools/build-riot-codesign-check.py new file mode 100644 index 0000000..9079d9b --- /dev/null +++ b/tools/build-riot-codesign-check.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +"""Build a read-only code-signature probe against Darling's SDK.""" +from pathlib import Path +import shlex +import subprocess + +root = Path(__file__).resolve().parent.parent +flags_file = root / 'build/darling-src/build/src/external/security/trust/trustd/CMakeFiles/trustd.dir/flags.make' +flags = {} +for line in flags_file.read_text().splitlines(): + if line.startswith('C_') and ' = ' in line: + name, value = line.split(' = ', 1) + flags[name] = shlex.split(value) +obj = root / 'build/riot-codesign-check.o' +subprocess.run(['clang', *flags['C_DEFINES'], *flags['C_INCLUDES'], *flags['C_FLAGS'], + '-c', str(root / 'tools/riot-codesign-check.c'), '-o', str(obj)], check=True) +subprocess.run(['clang', '-target', 'x86_64-apple-macosx11.0', '-fuse-ld=lld', '-nostdlib', + '-isysroot', '@DARLING_SDK@', str(obj), '-lSystem', + '-framework', 'Security', '-framework', 'CoreFoundation', + '-o', str(root / 'build/riot-codesign-check')], check=True) diff --git a/tools/build-riot-corefoundation-runtime.py b/tools/build-riot-corefoundation-runtime.py new file mode 100644 index 0000000..0e7def9 --- /dev/null +++ b/tools/build-riot-corefoundation-runtime.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +"""Build the CoreFoundation runtime with optimization, retaining local fixes.""" +from pathlib import Path +from concurrent.futures import ThreadPoolExecutor +import shlex +import subprocess +root = Path(__file__).resolve().parent.parent +folder = root / 'build/darling-src/build/src/external/corefoundation' +source = root / 'build/darling-src/src/external/corefoundation' +flags = {} +custom = {} +for line in (folder / 'CMakeFiles/CoreFoundation_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 disappear: follow the installed clang instead. +import re +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()} +link = shlex.split((folder / 'CMakeFiles/CoreFoundation_x86_64.dir/link.txt').read_text()) +objects = [] +for index, arg in enumerate(link): + if arg.startswith('CMakeFiles/CoreFoundation_obj.dir/') and arg.endswith('.o'): + name = Path(arg).name.removesuffix('.o') + obj = root / ('build/riot-cf-' + name + '.o') + objects.append((name, obj)) + link[index] = str(obj) +def compile_object(item): + name, obj = item + kind = 'ASM' if name.endswith('.S') else 'C' + subprocess.run(['clang', *flags[kind+'_DEFINES'], *flags[kind+'_INCLUDES'], + *flags[kind+'_FLAGS'], '-O2', *custom.get(name, []), '-c', + str(source/name), '-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-CoreFoundation') +subprocess.run(link, cwd=folder, check=True) diff --git a/tools/build-riot-coregraphics-runtime.py b/tools/build-riot-coregraphics-runtime.py new file mode 100644 index 0000000..8c5e72d --- /dev/null +++ b/tools/build-riot-coregraphics-runtime.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +"""Build the CoreGraphics display and cursor compatibility override.""" +from pathlib import Path +import shlex +import subprocess +root = Path(__file__).resolve().parent.parent +folder = root / 'build/darling-src/build/src/external/cocotron/CoreGraphics' +flags = {} +for line in (folder / 'CMakeFiles/CoreGraphics.dir/flags.make').read_text().splitlines(): + if line.startswith('C_') and ' = ' in line: + key, value = line.split(' = ', 1) + flags[key] = shlex.split(value) +obj = root / 'build/riot-CGDirectDisplay.o' +subprocess.run(['clang', *flags['C_DEFINES'], *flags['C_INCLUDES'], *flags['C_FLAGS'], + '-O2', '-Wno-macro-redefined', '-c', + str(root / 'build/darling-src/src/external/cocotron/CoreGraphics/CGDirectDisplay.m'), + '-o', str(obj)], check=True) +link = shlex.split((folder / 'CMakeFiles/CoreGraphics.dir/link.txt').read_text()) +link[link.index('CMakeFiles/CoreGraphics.dir/CGDirectDisplay.m.o')] = str(obj) +link[link.index('-o') + 1] = str(root / 'build/riot-CoreGraphics') +subprocess.run(link, cwd=folder, check=True) diff --git a/tools/build-riot-cxx-optimized.py b/tools/build-riot-cxx-optimized.py new file mode 100644 index 0000000..76b56c0 --- /dev/null +++ b/tools/build-riot-cxx-optimized.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +"""Rebuild libc++abi and libc++ with -O2. The CMake tree has no build type, so the stock objects (and the earlier +single-file libc++abi rebuild) are -O0: std::type_info::operator==, __dynamic_cast, shared_ptr internals ran 3-5x slower +than they should (about 2% of the League main thread). Outputs build/riot-opt-libc++abi.dylib / riot-opt-libc++.1.dylib. +""" +from concurrent.futures import ThreadPoolExecutor +from pathlib import Path +import re, shlex, subprocess, sys +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', v) for v in values] +def build(lib, objdir, linkdir, output): + folder = root / 'build/darling-src/build/src/external' / lib + src = root / 'build/darling-src/src/external' / lib + flags = {} + for line in (folder / ('CMakeFiles/%s/flags.make' % objdir)).read_text().splitlines(): + if line.startswith('CXX_') and ' = ' in line: + k, v = line.split(' = ', 1); flags[k] = current_clang(shlex.split(v)) + link = shlex.split((folder / ('CMakeFiles/%s/link.txt' % linkdir)).read_text()) + jobs = [] + for i, arg in enumerate(link): + if arg.startswith('CMakeFiles/%s/' % objdir) and arg.endswith('.o'): + rel = arg[len('CMakeFiles/%s/' % objdir):-2] # e.g. src/algorithm.cpp + obj = root / ('build/riot-opt-%s-%s.o' % (lib, rel.replace('/', '_'))) + jobs.append((rel, obj)); link[i] = str(obj) + def compile_one(job): + rel, obj = job + subprocess.run(['clang++', *flags['CXX_DEFINES'], *flags['CXX_INCLUDES'], *flags['CXX_FLAGS'], '-O2', '-DNDEBUG', + '-c', str(src / rel), '-o', str(obj)], check=True) + with ThreadPoolExecutor(max_workers=8) as pool: + list(pool.map(compile_one, jobs)) + link[link.index('-o') + 1] = str(root / 'build' / output) + subprocess.run(link, cwd=folder, check=True) + print('built', output, len(jobs), 'objects') +targets = sys.argv[1:] or ['abi', 'cxx'] +if 'abi' in targets: build('libcxxabi', 'cxxabi_obj.dir', 'cxxabi.dir', 'riot-opt-libc++abi.dylib') +if 'cxx' in targets: build('libcxx', 'cxx_obj.dir', 'cxx.dir', 'riot-opt-libc++.1.dylib') diff --git a/tools/build-riot-cxxabi-runtime.py b/tools/build-riot-cxxabi-runtime.py new file mode 100644 index 0000000..8060cb4 --- /dev/null +++ b/tools/build-riot-cxxabi-runtime.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +"""Build exception RTTI matching and the cross-image catch regression.""" +from pathlib import Path +import shlex +import subprocess +root = Path(__file__).resolve().parent.parent +folder = root / 'build/darling-src/build/src/external/libcxxabi' +flags = {} +for line in (folder / 'CMakeFiles/cxxabi_obj.dir/flags.make').read_text().splitlines(): + if line.startswith('CXX_') and ' = ' in line: + key, value = line.split(' = ', 1) + flags[key] = shlex.split(value) +link = shlex.split((folder / 'CMakeFiles/cxxabi.dir/link.txt').read_text()) +obj = root / 'build/riot-cxxabi-private_typeinfo.o' +subprocess.run(['clang++', *flags['CXX_DEFINES'], *flags['CXX_INCLUDES'], + *flags['CXX_FLAGS'], '-c', str(root / 'build/darling-src/src/external/libcxxabi/src/private_typeinfo.cpp'), + '-o', str(obj)], check=True) +link[link.index('CMakeFiles/cxxabi_obj.dir/src/private_typeinfo.cpp.o')] = str(obj) +link[link.index('-o') + 1] = str(root / 'build/riot-libc++abi.dylib') +subprocess.run(link, cwd=folder, check=True) +obj = root / 'build/riot-cxx-exception-check.o' +subprocess.run(['clang++', *flags['CXX_DEFINES'], *flags['CXX_INCLUDES'], + *flags['CXX_FLAGS'], '-c', str(root / 'tools/riot-cxx-exception-check.cpp'), + '-o', str(obj)], check=True) +rtti = root / 'build/riot-cxx-exception-rtti.o' +subprocess.run(['clang', '-target', 'x86_64-apple-macosx11.0', '-c', + str(root / 'tools/riot-cxx-exception-rtti.s'), '-o', str(rtti)], check=True) +subprocess.run(['clang++', '-target', 'x86_64-apple-macosx11.0', '-fuse-ld=lld', + '-nostdlib', '-isysroot', '@DARLING_SDK@', str(obj), str(rtti), + '-lSystem', '-lc++', '-lc++abi', '-o', + str(root / 'build/riot-cxx-exception-check')], check=True) diff --git a/tools/build-riot-dns-config-check.py b/tools/build-riot-dns-config-check.py new file mode 100644 index 0000000..5a9ad52 --- /dev/null +++ b/tools/build-riot-dns-config-check.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +"""Build the DNS configuration parser probe against Darling's SDK.""" +from pathlib import Path +import shlex +import subprocess + +project = Path(__file__).resolve().parent.parent +flags_file = project / 'build/darling-src/build/src/external/configd/CMakeFiles/system_configuration.dir/flags.make' +flags = {} +for line in flags_file.read_text().splitlines(): + if line.startswith('C_') and ' = ' in line: + name, value = line.split(' = ', 1) + flags[name] = shlex.split(value) +obj = project / 'build/riot-dns-config-check.o' +subprocess.run(['clang', *flags['C_DEFINES'], *flags['C_INCLUDES'], *flags['C_FLAGS'], + '-c', str(project / 'tools/riot-dns-config-check.c'), '-o', str(obj)], check=True) +subprocess.run(['clang', '-target', 'x86_64-apple-macosx11.0', '-fuse-ld=lld', '-nostdlib', + '-isysroot', '@DARLING_SDK@', str(obj), '-lSystem', + '-o', str(project / 'build/riot-dns-config-check')], check=True) diff --git a/tools/build-riot-fast-clock-check.py b/tools/build-riot-fast-clock-check.py new file mode 100644 index 0000000..ce648d7 --- /dev/null +++ b/tools/build-riot-fast-clock-check.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 +from pathlib import Path +import shlex, subprocess +root=Path(__file__).resolve().parent.parent +import re +res=subprocess.check_output(['clang','-print-resource-dir'],text=True).strip() +flags={} +for line in (root/'build/darling-src/build/src/frameworks/CoreVideo/CMakeFiles/CoreVideo.dir/flags.make').read_text().splitlines(): + if line.startswith('C_') and ' = ' in line: + key,value=line.split(' = ',1); flags[key]=[re.sub(r'/usr/lib/clang/\d+/include',res+'/include',v) for v in shlex.split(value)] +obj=root/'build/riot-fast-clock-check.o' +subprocess.run(['clang',*flags['C_DEFINES'],*flags['C_INCLUDES'],*flags['C_FLAGS'],'-I'+str(root/'build/darling-src/src/startup/mldr/elfcalls'),'-O2','-c',str(root/'tools/riot-fast-clock-check.c'),'-o',str(obj)],check=True) +subprocess.run(['clang','-target','x86_64-apple-macosx11.0','-fuse-ld=lld','-nostdlib','-isysroot','@DARLING_SDK@',str(obj),'-lSystem','-o',str(root/'build/riot-fast-clock-check')],check=True) diff --git a/tools/build-riot-fault-recovery-check.py b/tools/build-riot-fault-recovery-check.py new file mode 100644 index 0000000..616f8b9 --- /dev/null +++ b/tools/build-riot-fault-recovery-check.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +"""Compile the guest signal recovery probe using Darling's configured headers.""" +from pathlib import Path +import shlex +import subprocess + +root = Path(__file__).resolve().parent.parent +flags = {} +config = root / "build/darling-src/build/src/external/security/trust/trustd/CMakeFiles/trustd.dir/flags.make" +for line in config.read_text().splitlines(): + if line.startswith("C_") and " = " in line: + key, value = line.split(" = ", 1) + flags[key] = shlex.split(value) +subprocess.run(["clang", *flags["C_DEFINES"], *flags["C_INCLUDES"], *flags["C_FLAGS"], + "-fno-omit-frame-pointer", "-fuse-ld=lld", "-nostdlib", + "-isysroot", "@DARLING_SDK@", + str(root / "tools/riot-fault-recovery-check.c"), "-lSystem", + "-o", str(root / "build/riot-fault-recovery-check")], check=True) diff --git a/tools/build-riot-fork-integrity-check.py b/tools/build-riot-fork-integrity-check.py new file mode 100644 index 0000000..c3ddb2e --- /dev/null +++ b/tools/build-riot-fork-integrity-check.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +"""Build the guest fork regression and its concurrent host allocator workload.""" +from pathlib import Path +import shlex, subprocess +root=Path(__file__).resolve().parent.parent +subprocess.run(['cc','-shared','-fPIC','-pthread','-O2','-Wall','-Wextra','-Werror', + str(root/'tools/riot-host-fork-stress.c'),'-o',str(root/'build/libriot-host-fork-stress.so')],check=True) +import re +res=subprocess.check_output(['clang','-print-resource-dir'],text=True).strip() +flags={} +for line in (root/'build/darling-src/build/src/frameworks/CoreVideo/CMakeFiles/CoreVideo.dir/flags.make').read_text().splitlines(): + if line.startswith('C_') and ' = ' in line: + key,value=line.split(' = ',1); flags[key]=[re.sub(r'/usr/lib/clang/\d+/include',res+'/include',v) for v in shlex.split(value)] +obj=root/'build/riot-fork-integrity-check.o' +subprocess.run(['clang',*flags['C_DEFINES'],*flags['C_INCLUDES'],*flags['C_FLAGS'],'-I'+str(root/'build/darling-src/src/startup/mldr/elfcalls'),'-O2','-c',str(root/'tools/riot-fork-integrity-check.c'),'-o',str(obj)],check=True) +subprocess.run(['clang','-target','x86_64-apple-macosx11.0','-fuse-ld=lld','-nostdlib','-isysroot','@DARLING_SDK@',str(obj),'-lSystem','-o',str(root/'build/riot-fork-integrity-check')],check=True) diff --git a/tools/build-riot-indium-runtime.py b/tools/build-riot-indium-runtime.py new file mode 100644 index 0000000..a170332 --- /dev/null +++ b/tools/build-riot-indium-runtime.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +"""Build the paired Iridium shader translator and Indium runtime overrides.""" +from pathlib import Path +import shlex +import subprocess +import sys +root = Path(__file__).resolve().parent.parent +folder = root / 'build/darling-src/build/src/external/metal/deps/indium' +# Keep the validated LLVM major locally when a system update replaces it. +pinned_llvm = root / 'build/llvm22/usr' +resource_include = subprocess.check_output(['clang++', '-print-resource-dir'], text=True).strip() + '/include' +def current_includes(values): + return [resource_include if value.startswith('/usr/lib/clang/') and value.endswith('/include') else value for value in values] +iridium_flags = {} +for line in (folder / 'CMakeFiles/iridium.dir/flags.make').read_text().splitlines(): + if line.startswith('CXX_') and ' = ' in line: + key, value = line.split(' = ', 1) + iridium_flags[key] = shlex.split(value) +iridium_flags['CXX_INCLUDES'] = current_includes(iridium_flags['CXX_INCLUDES']) +if (pinned_llvm / 'lib/libLLVM-22.so').exists(): + iridium_flags['CXX_INCLUDES'].insert(0, '-I' + str(pinned_llvm / 'include')) + iridium_flags['CXX_DEFINES'] = [value for value in iridium_flags['CXX_DEFINES'] if not value.startswith('-DHOST_LLVM_LIBNAME=')] + iridium_flags['CXX_DEFINES'].append('-DHOST_LLVM_LIBNAME="' + str(pinned_llvm / 'lib/libLLVM-22.so') + '"') +iridium_link = shlex.split((folder / 'CMakeFiles/iridium.dir/link.txt').read_text()) +for name in ('air', 'dynamic-llvm', 'iridium', 'spirv'): + obj = root / f'build/riot-iridium-{name}.o' + subprocess.run(['clang++', *iridium_flags['CXX_DEFINES'], *iridium_flags['CXX_INCLUDES'], + *iridium_flags['CXX_FLAGS'], '-O2', '-c', str(root / ('build/darling-src/src/external/metal/deps/indium/src/iridium/' + name + '.cpp')), + '-o', str(obj)], check=True) + iridium_link[iridium_link.index('CMakeFiles/iridium.dir/src/iridium/' + name + '.cpp.o')] = str(obj) +iridium_link[iridium_link.index('-o') + 1] = str(root / 'build/riot-libiridium.dylib') +subprocess.run(iridium_link, cwd=folder, check=True) +if '--iridium-only' in sys.argv: + sys.exit(0) +flags = {} +for line in (folder / 'CMakeFiles/indium.dir/flags.make').read_text().splitlines(): + if line.startswith('CXX_') and ' = ' in line: + key, value = line.split(' = ', 1) + flags[key] = shlex.split(value) +flags['CXX_INCLUDES'] = current_includes(flags['CXX_INCLUDES']) +link = shlex.split((folder / 'CMakeFiles/indium.dir/link.txt').read_text()) +# Rebuild every object: private encoder layouts are shared with command-buffer factories. +indium_names = [Path(arg).name.removesuffix('.cpp.o') for arg in link + if arg.startswith('CMakeFiles/indium.dir/src/indium/') and arg.endswith('.cpp.o')] +for name in indium_names: + obj = root / f'build/riot-indium-{name}.o' + subprocess.run(['clang++', *flags['CXX_DEFINES'], *flags['CXX_INCLUDES'], + *flags['CXX_FLAGS'], '-O2', '-c', str(root / ('build/darling-src/src/external/metal/deps/indium/src/indium/' + name + '.cpp')), + '-o', str(obj)], check=True) + link[link.index('CMakeFiles/indium.dir/src/indium/' + name + '.cpp.o')] = str(obj) +link[link.index('-o') + 1] = str(root / 'build/riot-libindium.dylib') +subprocess.run(link, cwd=folder, check=True) diff --git a/tools/build-riot-kqueue-runtime.py b/tools/build-riot-kqueue-runtime.py new file mode 100644 index 0000000..b9e962b --- /dev/null +++ b/tools/build-riot-kqueue-runtime.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +"""Build the queue-lifetime repair and its descriptor-leak regression probe.""" +from pathlib import Path +import os +import shlex +import subprocess + +project = Path(__file__).resolve().parent.parent +build = project / 'build/darling-src/build' +kernel=build/'src/external/xnu/darling/src/libsystem_kernel' +objects=shlex.split((kernel/'CMakeFiles/system_kernel.dir/objects1.rsp').read_text()) +import re +# The saved CMake flags name the clang resource directory of the compiler that configured the tree (22); the system +# compiler is 23 now and disappears. Follow the installed clang. +resource_dir=subprocess.check_output(['clang','-print-resource-dir'],text=True).strip() +def configured_flags(directory): + result={} + for line in (directory/'flags.make').read_text().splitlines(): + if line.startswith('C_') and ' = ' in line: + key,value=line.split(' = ',1) + result[key]=[re.sub(r'/usr/lib/clang/\d+/include',resource_dir+'/include',v) for v in shlex.split(value)] + return result +kqueue_flags=configured_flags(build/'src/external/libkqueue/CMakeFiles/kqueue.dir') +emulation_flags=configured_flags(kernel/'emulation/CMakeFiles/emulation.dir') +syscall_flags=configured_flags(kernel/'libsyscall/CMakeFiles/libsyscall.dir') +# Build every configured kqueue object with optimization; the configured +# upstream target explicitly uses -O0 even when the rest of the runtime is +# optimized. Preserve its definitions, debug checks, and source selection. +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' +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')] +# 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, + 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')] +# 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) +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)] +# 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)] +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')] +# Process-local Mach semaphores (mach_traps.c) wrap semaphore_create/semaphore_destroy: the MIG stubs get private names. +mig_flags=configured_flags(kernel/'libsyscall/CMakeFiles/libsyscall_64.dir') +mig_obj=project/'build/riot-kernel-task-x86_64-User.o' +subprocess.run(['clang',*mig_flags['C_DEFINES'],*mig_flags['C_INCLUDES'],*mig_flags['C_FLAGS'], + '-Dsemaphore_create=__darling_rpc_semaphore_create','-Dsemaphore_destroy=__darling_rpc_semaphore_destroy', + '-c',str(kernel/'libsyscall/mach/task-x86_64-User.c'),'-o',str(mig_obj)],check=True,cwd=kernel/'libsyscall') +matches=[i for i,value in enumerate(objects) if value.endswith('mach/task-x86_64-User.c.o')] +assert len(matches)==1,matches +objects[matches[0]]=str(mig_obj) +# mach_vm_region_recurse on the own task is answered in the process (mach_traps.c); the MIG stub gets a private name. +vm_obj=project/'build/riot-kernel-mach_vm-x86_64-User.o' +subprocess.run(['clang',*mig_flags['C_DEFINES'],*mig_flags['C_INCLUDES'],*mig_flags['C_FLAGS'], + '-Dmach_vm_region_recurse=__darling_rpc_mach_vm_region_recurse', + '-c',str(kernel/'libsyscall/mach/mach_vm-x86_64-User.c'),'-o',str(vm_obj)],check=True,cwd=kernel/'libsyscall') +matches=[i for i,value in enumerate(objects) if value.endswith('mach/mach_vm-x86_64-User.c.o')] +assert len(matches)==1,matches +objects[matches[0]]=str(vm_obj) +for source,settings,root in repairs: + obj=project/('build/riot-kernel-'+source.replace('/','-')+'.o') + subprocess.run(['clang',*settings['C_DEFINES'],*settings['C_INCLUDES'],*settings['C_FLAGS'], + '-c',str(root/source),'-o',str(obj)],check=True) + matches=[i for i,value in enumerate(objects) if value.endswith('/'+source+'.o')] + assert len(matches)==1,(source,matches) + objects[matches[0]]=str(obj) +rsp=project/'build/riot-kernel-objects.rsp' +rsp.write_text(shlex.join(objects)) +link=shlex.split((kernel/'CMakeFiles/system_kernel.dir/link.txt').read_text()) +link[link.index('@CMakeFiles/system_kernel.dir/objects1.rsp')]='@'+str(rsp) +link[link.index('-o')+1]=str(project/'build/riot-libsystem_kernel.dylib') +subprocess.run(link,cwd=kernel,check=True) +flags = {} +for line in (build / 'src/frameworks/CoreVideo/CMakeFiles/CoreVideo.dir/flags.make').read_text().splitlines(): + if line.startswith('C_') and ' = ' in line: + name, value = line.split(' = ', 1) + flags[name] = shlex.split(value) +for probe in ('riot-task-info-check','riot-kqueue-lifetime-check','riot-kqueue-stack-check','riot-workqueue-reuse-check','riot-dispatch-fd-check','riot-kqueue-dispatch2-check','riot-syscall-fastpath-check','riot-select-backoff-check','riot-vnode-close-check','riot-rpc-signal-mask-check','riot-fileport-lifetime-check','riot-kqueue-timer-check','riot-cancel-rpc-check'): + obj = project / f'build/{probe}.o' + subprocess.run(['clang', *flags['C_DEFINES'], *flags['C_INCLUDES'], *flags['C_FLAGS'], + '-O2', '-c', str(project / f'tools/{probe}.c'), '-o', str(obj)], check=True) + subprocess.run(['clang', '-target', 'x86_64-apple-macosx11.0', '-fuse-ld=lld', '-nostdlib', + '-isysroot', '@DARLING_SDK@', str(obj), '-lSystem', '-o', + str(project / f'build/{probe}')], check=True) diff --git a/tools/build-riot-local-mutex-check.py b/tools/build-riot-local-mutex-check.py new file mode 100644 index 0000000..9b0941e --- /dev/null +++ b/tools/build-riot-local-mutex-check.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 +from pathlib import Path +import shlex, subprocess +root=Path(__file__).resolve().parent.parent +import re +res=subprocess.check_output(['clang','-print-resource-dir'],text=True).strip() +flags={} +for line in (root/'build/darling-src/build/src/frameworks/CoreVideo/CMakeFiles/CoreVideo.dir/flags.make').read_text().splitlines(): + if line.startswith('C_') and ' = ' in line: + key,value=line.split(' = ',1); flags[key]=[re.sub(r'/usr/lib/clang/\d+/include',res+'/include',v) for v in shlex.split(value)] +obj=root/'build/riot-local-mutex-check.o' +subprocess.run(['clang',*flags['C_DEFINES'],*flags['C_INCLUDES'],*flags['C_FLAGS'],'-I'+str(root/'build/darling-src/src/startup/mldr/elfcalls'),'-O2','-c',str(root/'tools/riot-local-mutex-check.c'),'-o',str(obj)],check=True) +subprocess.run(['clang','-target','x86_64-apple-macosx11.0','-fuse-ld=lld','-nostdlib','-isysroot','@DARLING_SDK@',str(obj),'-lSystem','-o',str(root/'build/riot-local-mutex-check')],check=True) diff --git a/tools/build-riot-local-region-check.py b/tools/build-riot-local-region-check.py new file mode 100644 index 0000000..5c47f90 --- /dev/null +++ b/tools/build-riot-local-region-check.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 +from pathlib import Path +import shlex, subprocess +root=Path(__file__).resolve().parent.parent +import re +res=subprocess.check_output(['clang','-print-resource-dir'],text=True).strip() +flags={} +for line in (root/'build/darling-src/build/src/frameworks/CoreVideo/CMakeFiles/CoreVideo.dir/flags.make').read_text().splitlines(): + if line.startswith('C_') and ' = ' in line: + key,value=line.split(' = ',1); flags[key]=[re.sub(r'/usr/lib/clang/\d+/include',res+'/include',v) for v in shlex.split(value)] +obj=root/'build/riot-local-region-check.o' +subprocess.run(['clang',*flags['C_DEFINES'],*flags['C_INCLUDES'],*flags['C_FLAGS'],'-I'+str(root/'build/darling-src/src/startup/mldr/elfcalls'),'-O2','-c',str(root/'tools/riot-local-region-check.c'),'-o',str(obj)],check=True) +subprocess.run(['clang','-target','x86_64-apple-macosx11.0','-fuse-ld=lld','-nostdlib','-isysroot','@DARLING_SDK@',str(obj),'-lSystem','-o',str(root/'build/riot-local-region-check')],check=True) diff --git a/tools/build-riot-local-semaphore-check.py b/tools/build-riot-local-semaphore-check.py new file mode 100644 index 0000000..b66a1a3 --- /dev/null +++ b/tools/build-riot-local-semaphore-check.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 +from pathlib import Path +import shlex, subprocess +root=Path(__file__).resolve().parent.parent +import re +res=subprocess.check_output(['clang','-print-resource-dir'],text=True).strip() +flags={} +for line in (root/'build/darling-src/build/src/frameworks/CoreVideo/CMakeFiles/CoreVideo.dir/flags.make').read_text().splitlines(): + if line.startswith('C_') and ' = ' in line: + key,value=line.split(' = ',1); flags[key]=[re.sub(r'/usr/lib/clang/\d+/include',res+'/include',v) for v in shlex.split(value)] +obj=root/'build/riot-local-semaphore-check.o' +subprocess.run(['clang',*flags['C_DEFINES'],*flags['C_INCLUDES'],*flags['C_FLAGS'],'-I'+str(root/'build/darling-src/src/startup/mldr/elfcalls'),'-O2','-fblocks','-c',str(root/'tools/riot-local-semaphore-check.c'),'-o',str(obj)],check=True) +subprocess.run(['clang','-target','x86_64-apple-macosx11.0','-fuse-ld=lld','-nostdlib','-isysroot','@DARLING_SDK@',str(obj),'-lSystem','-o',str(root/'build/riot-local-semaphore-check')],check=True) diff --git a/tools/build-riot-mach-exception-state-check.py b/tools/build-riot-mach-exception-state-check.py new file mode 100644 index 0000000..71b542e --- /dev/null +++ b/tools/build-riot-mach-exception-state-check.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +"""Build a real guest Mach exception state/recovery fixture.""" +from pathlib import Path +import shlex +import subprocess + +root = Path(__file__).resolve().parent.parent +build = root / "build/darling-src/build" +flags = {} +config = build / "src/external/security/trust/trustd/CMakeFiles/trustd.dir/flags.make" +for line in config.read_text().splitlines(): + if line.startswith("C_") and " = " in line: + key, value = line.split(" = ", 1) + flags[key] = shlex.split(value) +generated = build / "src/external/xnu/darling/src/libsystem_kernel/emulation/src/other/signal" +subprocess.run([ + "clang", *flags["C_DEFINES"], *flags["C_INCLUDES"], *flags["C_FLAGS"], + "-I" + str(generated), "-fno-omit-frame-pointer", "-fuse-ld=lld", "-nostdlib", + "-isysroot", "@DARLING_SDK@", + str(root / "tools/riot-mach-exception-state-check.c"), + str(generated / "mach_excServer.c"), "-lSystem", + "-o", str(root / "build/riot-mach-exception-state-check"), +], check=True) diff --git a/tools/build-riot-malloc-runtime.py b/tools/build-riot-malloc-runtime.py new file mode 100644 index 0000000..0328651 --- /dev/null +++ b/tools/build-riot-malloc-runtime.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +"""Build an isolated optimized allocator and its native integrity check.""" +from pathlib import Path +import shlex +import subprocess +import shutil + +root = Path(__file__).resolve().parent.parent +build = root / "build/darling-src/build" +component = build / "src/external/libmalloc" +flags = {} +for line in (component / "CMakeFiles/system_malloc_obj.dir/flags.make").read_text().splitlines(): + if line.startswith("C_") and " = " in line: + key, value = line.split(" = ", 1) + flags[key] = shlex.split(value) +resource_include = subprocess.check_output(["clang", "-print-resource-dir"], text=True).strip() + "/include" +flags["C_INCLUDES"] = [resource_include if value.startswith("/usr/lib/clang/") and value.endswith("/include") else value for value in flags["C_INCLUDES"]] +flags["C_INCLUDES"].append("-I" + str(root / "build/darling-src/src/startup/mldr/elfcalls")) +link = shlex.split((component / "CMakeFiles/system_malloc.dir/link.txt").read_text()) +objects = [arg for arg in link if arg.startswith("CMakeFiles/system_malloc_obj.dir/") and arg.endswith(".o")] +for original in objects: + name = Path(original).name.removesuffix(".o") + obj = root / ("build/riot-malloc-" + name + ".o") + extra = ["-DOS_VARIANT_NOTRESOLVED=1", "-DOS_VARIANT_RESOLVED=1"] if name == "nanov2_malloc.c" else [] + subprocess.run(["clang", *flags["C_DEFINES"], *flags["C_INCLUDES"], *flags["C_FLAGS"], + *extra, "-O2", "-fno-strict-aliasing", "-c", + str(root / "build/darling-src/src/external/libmalloc/src" / name), + "-o", str(obj)], check=True) + link[link.index(original)] = str(obj) +link[link.index("-o") + 1] = str(root / "build/riot-libsystem_malloc.dylib") +subprocess.run(link, cwd=component, check=True) +candidate = root / "build/riot-malloc-check-runtime" +candidate.mkdir(exist_ok=True) +shutil.copy2(root / "build/riot-libsystem_malloc.dylib", candidate / "libsystem_malloc.dylib") + +probe_flags = {} +for line in (build / "src/frameworks/CoreVideo/CMakeFiles/CoreVideo.dir/flags.make").read_text().splitlines(): + if line.startswith("C_") and " = " in line: + key, value = line.split(" = ", 1) + probe_flags[key] = shlex.split(value) +probe_flags["C_INCLUDES"] = [resource_include if value.startswith("/usr/lib/clang/") and value.endswith("/include") else value for value in probe_flags["C_INCLUDES"]] +obj = root / "build/riot-malloc-integrity-check.o" +subprocess.run(["clang", *probe_flags["C_DEFINES"], *probe_flags["C_INCLUDES"], + *probe_flags["C_FLAGS"], "-O2", "-c", str(root / "tools/riot-malloc-integrity-check.c"), + "-o", str(obj)], check=True) +subprocess.run(["clang", "-target", "x86_64-apple-macosx11.0", "-fuse-ld=lld", "-nostdlib", + "-isysroot", "@DARLING_SDK@", str(obj), "-lSystem", + "-o", str(root / "build/riot-malloc-integrity-check")], check=True) diff --git a/tools/build-riot-metal-concurrency-check.py b/tools/build-riot-metal-concurrency-check.py new file mode 100644 index 0000000..0f0f688 --- /dev/null +++ b/tools/build-riot-metal-concurrency-check.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +from pathlib import Path +import shlex +import subprocess +root = Path(__file__).resolve().parent.parent +flags = {} +for line in (root / 'build/darling-src/build/src/external/metal/CMakeFiles/Metal.dir/flags.make').read_text().splitlines(): + if line.startswith('CXX_') and ' = ' in line: + key, value = line.split(' = ', 1) + flags[key] = shlex.split(value) +resource_include = subprocess.check_output(['clang++', '-print-resource-dir'], text=True).strip() + '/include' +flags['CXX_INCLUDES'] = [resource_include if value.startswith('/usr/lib/clang/') and value.endswith('/include') else value for value in flags['CXX_INCLUDES']] +obj = root / 'build/riot-metal-concurrency-check.o' +subprocess.run(['clang++', *flags['CXX_DEFINES'], *flags['CXX_INCLUDES'], *flags['CXX_FLAGS'], '-O2', '-c', str(root / 'tools/riot-metal-concurrency-check.mm'), '-o', str(obj)], check=True) +subprocess.run(['clang++', '-target', 'x86_64-apple-macosx11.0', '-fuse-ld=lld', '-nostdlib', '-isysroot', '@DARLING_SDK@', str(obj), '-lSystem', '-lc++', '-lobjc', '-framework', 'Foundation', str(root / 'build/riot-Metal'), '-o', str(root / 'build/riot-metal-concurrency-check')], check=True) diff --git a/tools/build-riot-metal-presentation.py b/tools/build-riot-metal-presentation.py new file mode 100644 index 0000000..d7d0e1e --- /dev/null +++ b/tools/build-riot-metal-presentation.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +"""Build the Metal layer presentation bridge and its window regression check.""" +from pathlib import Path +import shlex +import subprocess +root = Path(__file__).resolve().parent.parent +folder = root / 'build/darling-src/build/src/external/cocotron/QuartzCore' +source = root / 'build/darling-src/src/external/cocotron/QuartzCore' +flags = {} +for line in (folder / 'CMakeFiles/QuartzCore.dir/flags.make').read_text().splitlines(): + if ' = ' in line: + key, value = line.split(' = ', 1) + flags[key] = shlex.split(value) +resource_include = subprocess.check_output(['clang++', '-print-resource-dir'], text=True).strip() + '/include' +for key in ('C_INCLUDES', 'CXX_INCLUDES'): + flags[key] = [resource_include if value.startswith('/usr/lib/clang/') and value.endswith('/include') else value for value in flags[key]] +link = shlex.split((folder / 'CMakeFiles/QuartzCore.dir/link.txt').read_text()) +for name, suffix in [('CALayer', 'm'), ('CALayerContext', 'm'), ('CATransaction', 'm'), ('CAMetalLayer', 'mm'), ('CAMetalDrawable', 'mm')]: + kind = 'CXX' if suffix == 'mm' else 'C' + obj = root / f'build/riot-quartzcore-{name}.o' + subprocess.run(['clang++' if kind == 'CXX' else 'clang', *flags[kind + '_DEFINES'], + *flags[kind + '_INCLUDES'], *flags[kind + '_FLAGS'], '-O2', + '-Wno-macro-redefined', '-c', str(source / (name + '.' + suffix)), '-o', str(obj)], check=True) + link[link.index('CMakeFiles/QuartzCore.dir/' + name + '.' + suffix + '.o')] = str(obj) +link[link.index('-o') + 1] = str(root / 'build/riot-QuartzCore') +subprocess.run(link, cwd=folder, check=True) +obj = root / 'build/riot-metal-presentation-check.o' +subprocess.run(['clang++', *flags['CXX_DEFINES'], *flags['CXX_INCLUDES'], *flags['CXX_FLAGS'], + '-c', str(root / 'tools/riot-metal-presentation-check.mm'), '-o', str(obj)], check=True) +subprocess.run(['clang++', '-target', 'x86_64-apple-macosx11.0', '-fuse-ld=lld', '-nostdlib', + '-isysroot', '@DARLING_SDK@', str(obj), '-lSystem', '-lc++', '-lobjc', + '-framework', 'AppKit', '-framework', 'OpenGL', '-framework', 'Foundation', str(root / 'build/riot-Metal'), + str(root / 'build/riot-QuartzCore'), '-o', str(root / 'build/riot-metal-presentation-check')], check=True) diff --git a/tools/build-riot-metal-resource-getter-check.py b/tools/build-riot-metal-resource-getter-check.py new file mode 100644 index 0000000..45734cc --- /dev/null +++ b/tools/build-riot-metal-resource-getter-check.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +from pathlib import Path +import shlex +import subprocess +root = Path(__file__).resolve().parent.parent +flags = {} +for line in (root / 'build/darling-src/build/src/external/metal/CMakeFiles/Metal.dir/flags.make').read_text().splitlines(): + if line.startswith('CXX_') and ' = ' in line: + key, value = line.split(' = ', 1) + flags[key] = shlex.split(value) +resource_include=subprocess.check_output(['clang++','-print-resource-dir'],text=True).strip()+'/include' +flags['CXX_INCLUDES']=[resource_include if x.startswith('/usr/lib/clang/') and x.endswith('/include') else x for x in flags['CXX_INCLUDES']] +obj = root / 'build/riot-metal-resource-getter-check.o' +subprocess.run(['clang++', *flags['CXX_DEFINES'], *flags['CXX_INCLUDES'], *flags['CXX_FLAGS'], '-O2', '-c', str(root / 'tools/riot-metal-resource-getter-check.mm'), '-o', str(obj)], check=True) +subprocess.run(['clang++', '-target', 'x86_64-apple-macosx11.0', '-fuse-ld=lld', '-nostdlib', '-isysroot', '@DARLING_SDK@', str(obj), '-lSystem', '-lc++', '-lobjc', '-framework', 'Foundation', str(root / 'build/riot-Metal'), '-o', str(root / 'build/riot-metal-resource-getter-check')], check=True) diff --git a/tools/build-riot-metal-runtime.py b/tools/build-riot-metal-runtime.py new file mode 100644 index 0000000..415bf4a --- /dev/null +++ b/tools/build-riot-metal-runtime.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +"""Build the Metal resource/command bridges and their GPU regression check.""" +from pathlib import Path +import shlex +import subprocess +root = Path(__file__).resolve().parent.parent +folder = root / 'build/darling-src/build/src/external/metal' +source = root / 'build/darling-src/src/external/metal/src/Metal' +flags = {} +for line in (folder / 'CMakeFiles/Metal.dir/flags.make').read_text().splitlines(): + if line.startswith('CXX_') and ' = ' in line: + key, value = line.split(' = ', 1) + flags[key] = shlex.split(value) +resource_include = subprocess.check_output(['clang++', '-print-resource-dir'], text=True).strip() + '/include' +flags['CXX_INCLUDES'] = [resource_include if value.startswith('/usr/lib/clang/') and value.endswith('/include') else value for value in flags['CXX_INCLUDES']] +link = shlex.split((folder / 'CMakeFiles/Metal.dir/link.txt').read_text()) +# These objects passed live League validation. Keep the broader resource/queue +# optimization experiment out of the default build after fresh-match stalls. +for name in ('MTLBuffer', 'MTLTexture', 'MTLDevice', 'MTLSamplerDescriptor', 'MTLTextureDescriptor', 'MTLCommandBuffer', 'MTLBlitCommandEncoder', 'MTLDepthStencilDescriptor', 'MTLStencilDescriptor', 'MTLVertexDescriptor', 'MTLRenderPipeline', 'MTLComputeCommandEncoder', 'MTLRenderCommandEncoder', 'MTLArgument', 'MTLLibrary'): + obj = root / f'build/riot-metal-{name}.o' + subprocess.run(['clang++', *flags['CXX_DEFINES'], *flags['CXX_INCLUDES'], + *flags['CXX_FLAGS'], '-O2', '-c', str(source / (name + '.mm')), + '-o', str(obj)], check=True) + original = 'CMakeFiles/Metal.dir/src/Metal/' + name + '.mm.o' + if original in link: + link[link.index(original)] = str(obj) + else: + link.append(str(obj)) +link[link.index('-o') + 1] = str(root / 'build/riot-Metal') +subprocess.run(link, cwd=folder, check=True) +flags['CXX_INCLUDES'].append('-I' + str(root / 'build/darling-src/src/external/metal/deps/indium/private-include')) +obj = root / 'build/riot-metal-sampler-check.o' +subprocess.run(['clang++', *flags['CXX_DEFINES'], *flags['CXX_INCLUDES'], + *flags['CXX_FLAGS'], '-c', str(root / 'tools/riot-metal-sampler-check.mm'), + '-o', str(obj)], check=True) +batch = root / 'build/riot-metal-batch-check.o' +subprocess.run(['clang++', *flags['CXX_DEFINES'], *flags['CXX_INCLUDES'], + *flags['CXX_FLAGS'], '-c', str(root / 'tools/riot-metal-batch-check.mm'), + '-o', str(batch)], check=True) +subprocess.run(['clang++', '-target', 'x86_64-apple-macosx11.0', '-fuse-ld=lld', + '-nostdlib', '-isysroot', '@DARLING_SDK@', str(obj), str(batch), + '-lSystem', '-lc++', '-lobjc', '-framework', 'Foundation', + str(root / 'build/riot-Metal'), str(root / 'build/riot-libindium.dylib'), '-o', + str(root / 'build/riot-metal-sampler-check')], check=True) diff --git a/tools/build-riot-metal-vertex-check.py b/tools/build-riot-metal-vertex-check.py new file mode 100644 index 0000000..f864c8e --- /dev/null +++ b/tools/build-riot-metal-vertex-check.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +from pathlib import Path +import shlex +import subprocess +root = Path(__file__).resolve().parent.parent +flags = {} +for line in (root / 'build/darling-src/build/src/external/metal/CMakeFiles/Metal.dir/flags.make').read_text().splitlines(): + if line.startswith('CXX_') and ' = ' in line: + key, value = line.split(' = ', 1) + flags[key] = shlex.split(value) +resource_include=subprocess.check_output(['clang++','-print-resource-dir'],text=True).strip()+'/include' +flags['CXX_INCLUDES']=[resource_include if x.startswith('/usr/lib/clang/') and x.endswith('/include') else x for x in flags['CXX_INCLUDES']] +obj = root / 'build/riot-metal-vertex-check.o' +subprocess.run(['clang++', *flags['CXX_DEFINES'], *flags['CXX_INCLUDES'], *flags['CXX_FLAGS'], '-O2', '-c', str(root / 'tools/riot-metal-vertex-check.mm'), '-o', str(obj)], check=True) +subprocess.run(['clang++', '-target', 'x86_64-apple-macosx11.0', '-fuse-ld=lld', '-nostdlib', '-isysroot', '@DARLING_SDK@', str(obj), '-lSystem', '-lc++', '-lobjc', '-framework', 'Foundation', str(root / 'build/riot-Metal'), '-o', str(root / 'build/riot-metal-vertex-check')], check=True) diff --git a/tools/build-riot-mvg-dependency-trace.py b/tools/build-riot-mvg-dependency-trace.py new file mode 100644 index 0000000..9f12f5c --- /dev/null +++ b/tools/build-riot-mvg-dependency-trace.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +"""Build optional return-preserving MVG dependency diagnostics.""" +from pathlib import Path +import shlex +import subprocess + +root = Path(__file__).resolve().parent.parent +flags_file = root / 'build/darling-src/build/src/external/security/trust/trustd/CMakeFiles/trustd.dir/flags.make' +flags = {} +for line in flags_file.read_text().splitlines(): + if line.startswith('C_') and ' = ' in line: + name, value = line.split(' = ', 1) + flags[name] = shlex.split(value) +obj = root / 'build/riot-mvg-dependency-trace.o' +subprocess.run(['clang', *flags['C_DEFINES'], *flags['C_INCLUDES'], *flags['C_FLAGS'], + '-c', str(root / 'tools/riot-mvg-dependency-trace.c'), '-o', str(obj)], check=True) +subprocess.run(['clang', '-target', 'x86_64-apple-macosx11.0', '-fuse-ld=lld', '-nostdlib', + '-dynamiclib', '-isysroot', '@DARLING_SDK@', str(obj), '-lSystem', + '-framework', 'Security', '-framework', 'CoreFoundation', + '-framework', 'IOKit', '-o', str(root / 'build/libRiotMvgDependencyTrace.dylib')], check=True) diff --git a/tools/build-riot-objc-runtime.py b/tools/build-riot-objc-runtime.py new file mode 100644 index 0000000..60e5f10 --- /dev/null +++ b/tools/build-riot-objc-runtime.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +"""Build objc with private sized deallocation, keeping the original circular link.""" +from pathlib import Path +import shlex +import subprocess +from concurrent.futures import ThreadPoolExecutor + +root = Path(__file__).resolve().parent.parent +folder = root / 'build/darling-src/build/src/external/objc4/runtime' +source = root / 'build/darling-src/src/external/objc4/runtime' +flags = {} +for line in (folder / 'CMakeFiles/objc_obj.dir/flags.make').read_text().splitlines(): + if line.startswith('CXX_') and ' = ' in line: + key, value = line.split(' = ', 1) + flags[key] = shlex.split(value) +link = shlex.split((folder / 'CMakeFiles/objc.dir/link.txt').read_text()) +replacements = [] +for index, arg in enumerate(link): + if arg.startswith('CMakeFiles/objc_obj.dir/') and arg.endswith('.mm.o'): + name = Path(arg).name.removesuffix('.o') + obj = root / f'build/riot-objc-{name}.o' + replacements.append((name, obj)) + link[index] = str(obj) + +def compile_object(item): + name, obj = item + subprocess.run(['clang++', *flags['CXX_DEFINES'], *flags['CXX_INCLUDES'], + *flags['CXX_FLAGS'], '-O2', '-DNDEBUG=1', '-UOBJC_IS_DEBUG_BUILD', '-DOBJC_IS_DEBUG_BUILD=0', '-c', str(source / name), '-o', str(obj)], check=True) + +with ThreadPoolExecutor(max_workers=6) as pool: + list(pool.map(compile_object, replacements)) +link[link.index('-o') + 1] = str(root / 'build/riot-libobjc.A.dylib') +subprocess.run(link, cwd=folder, check=True) +obj = root / 'build/riot-objc-allocator-check.o' +subprocess.run(['clang++', *flags['CXX_DEFINES'], *flags['CXX_INCLUDES'], + *flags['CXX_FLAGS'], '-c', str(root / 'tools/riot-objc-allocator-check.cpp'), + '-o', str(obj)], check=True) +subprocess.run(['clang++', '-target', 'x86_64-apple-macosx11.0', '-fuse-ld=lld', + '-nostdlib', '-isysroot', '@DARLING_SDK@', str(obj), + '-lSystem', '-lc++', '-lobjc', '-o', + str(root / 'build/riot-objc-allocator-check')], check=True) diff --git a/tools/build-riot-opendirectory-session-check.py b/tools/build-riot-opendirectory-session-check.py new file mode 100644 index 0000000..fe40bcf --- /dev/null +++ b/tools/build-riot-opendirectory-session-check.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +"""Build the OpenDirectory local-node regression check.""" +from pathlib import Path +import shlex +import subprocess + +root = Path(__file__).resolve().parent.parent +appkit = root / "build/darling-src/build/src/external/cocotron/AppKit" +flags = {} +for line in (appkit / "CMakeFiles/AppKit.dir/flags.make").read_text().splitlines(): + if line.startswith("C_") and " = " in line: + key, value = line.split(" = ", 1) + flags[key] = shlex.split(value) + +name = "riot-opendirectory-session-check" +obj = root / f"build/{name}.o" +subprocess.run(["clang", *flags["C_DEFINES"], *flags["C_INCLUDES"], *flags["C_FLAGS"], + "-fblocks", "-c", str(root / f"tools/{name}.m"), "-o", str(obj)], check=True) +subprocess.run(["clang", "-target", "x86_64-apple-macosx11.0", "-fuse-ld=lld", "-nostdlib", + "-isysroot", "@DARLING_SDK@", str(obj), "-lSystem", + "-framework", "Foundation", "-framework", "OpenDirectory", + "-framework", "CoreFoundation", "-lobjc", "-o", str(root / f"build/{name}")], + check=True) diff --git a/tools/build-riot-platform-strings.py b/tools/build-riot-platform-strings.py new file mode 100644 index 0000000..1cb4b23 --- /dev/null +++ b/tools/build-riot-platform-strings.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +"""Optimize configured string helpers, preserving every other platform object.""" +from pathlib import Path +import shlex,subprocess,shutil +root=Path(__file__).resolve().parent.parent +folder=root/'build/darling-src/build/src/external/libplatform' +flags={} +for line in (folder/'CMakeFiles/platform_obj.dir/flags.make').read_text().splitlines(): + if line.startswith('C_') and ' = ' in line: + k,v=line.split(' = ',1);flags[k]=shlex.split(v) +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()) +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) +link[link.index('-o')+1]=str(root/'build/riot-libsystem_platform.dylib') +subprocess.run(link,cwd=folder,check=True) +p=root/'build/m810-platform-runtime';p.mkdir(exist_ok=True);shutil.copy2(root/'build/riot-libsystem_platform.dylib',p/'libsystem_platform.dylib') diff --git a/tools/build-riot-pthread-runtime.py b/tools/build-riot-pthread-runtime.py new file mode 100644 index 0000000..4dcfa32 --- /dev/null +++ b/tools/build-riot-pthread-runtime.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +"""Build an isolated optimized pthread library using configured source aliases.""" +from pathlib import Path +import shlex +import shutil +import subprocess + +root = Path(__file__).resolve().parent.parent +build = root / "build/darling-src/build" +component = build / "src/external/libpthread/src" +flags, custom = {}, {} +for line in (component / "CMakeFiles/system_pthread_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.split("_FLAGS = ", 1) + custom[key.split("system_pthread_obj.dir/", 1)[1]] = shlex.split(value) +link = shlex.split((component / "CMakeFiles/system_pthread.dir/link.txt").read_text()) +objects = [arg for arg in link if arg.startswith("CMakeFiles/system_pthread_obj.dir/") and arg.endswith(".o")] +for original in objects: + relative = original.split("system_pthread_obj.dir/", 1)[1] + source = relative.removesuffix(".o") + kind = "ASM" if source.endswith((".s", ".S")) else "C" + obj = root / ("build/riot-pthread-" + source.replace("/", "-") + ".o") + subprocess.run(["clang", *flags.get(kind + "_DEFINES", []), + *flags.get(kind + "_INCLUDES", []), *flags.get(kind + "_FLAGS", []), + *custom.get(relative, []), "-O2", "-fno-strict-aliasing", "-c", + str(root / "build/darling-src/src/external/libpthread/src" / source), + "-o", str(obj)], check=True) + link[link.index(original)] = str(obj) +link[link.index("-o") + 1] = str(root / "build/riot-libsystem_pthread.dylib") +subprocess.run(link, cwd=component, check=True) +candidate = root / "build/riot-pthread-check-runtime" +candidate.mkdir(exist_ok=True) +shutil.copy2(root / "build/riot-libsystem_pthread.dylib", candidate / "libsystem_pthread.dylib") + +probe_flags = {} +for line in (build / "src/frameworks/CoreVideo/CMakeFiles/CoreVideo.dir/flags.make").read_text().splitlines(): + if line.startswith("C_") and " = " in line: + key, value = line.split(" = ", 1) + probe_flags[key] = shlex.split(value) +obj = root / "build/riot-pthread-integrity-check.o" +subprocess.run(["clang", *probe_flags["C_DEFINES"], *probe_flags["C_INCLUDES"], + *probe_flags["C_FLAGS"], "-O2", "-c", str(root / "tools/riot-pthread-integrity-check.c"), + "-o", str(obj)], check=True) +subprocess.run(["clang", "-target", "x86_64-apple-macosx11.0", "-fuse-ld=lld", "-nostdlib", + "-isysroot", "@DARLING_SDK@", str(obj), "-lSystem", + "-o", str(root / "build/riot-pthread-integrity-check")], check=True) diff --git a/tools/build-riot-region-count19-check.py b/tools/build-riot-region-count19-check.py new file mode 100644 index 0000000..05bf848 --- /dev/null +++ b/tools/build-riot-region-count19-check.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 +from pathlib import Path +import shlex, subprocess +root=Path(__file__).resolve().parent.parent +import re +res=subprocess.check_output(['clang','-print-resource-dir'],text=True).strip() +flags={} +for line in (root/'build/darling-src/build/src/frameworks/CoreVideo/CMakeFiles/CoreVideo.dir/flags.make').read_text().splitlines(): + if line.startswith('C_') and ' = ' in line: + key,value=line.split(' = ',1); flags[key]=[re.sub(r'/usr/lib/clang/\d+/include',res+'/include',v) for v in shlex.split(value)] +obj=root/'build/riot-region-count19-check.o' +subprocess.run(['clang',*flags['C_DEFINES'],*flags['C_INCLUDES'],*flags['C_FLAGS'],'-I'+str(root/'build/darling-src/src/startup/mldr/elfcalls'),'-O2','-c',str(root/'tools/riot-region-count19-check.c'),'-o',str(obj)],check=True) +subprocess.run(['clang','-target','x86_64-apple-macosx11.0','-fuse-ld=lld','-nostdlib','-isysroot','@DARLING_SDK@',str(obj),'-lSystem','-o',str(root/'build/riot-region-count19-check')],check=True) diff --git a/tools/build-riot-running-application-check.py b/tools/build-riot-running-application-check.py new file mode 100644 index 0000000..a70ad7e --- /dev/null +++ b/tools/build-riot-running-application-check.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +"""Build the AppKit process lookup and X11 activation regression check.""" +from pathlib import Path +import shlex +import subprocess + +root = Path(__file__).resolve().parent.parent +appkit = root / "build/darling-src/build/src/external/cocotron/AppKit" +flags = {} +for line in (appkit / "CMakeFiles/AppKit.dir/flags.make").read_text().splitlines(): + if line.startswith("C_") and " = " in line: + key, value = line.split(" = ", 1) + flags[key] = shlex.split(value) + +source = root / "tools/riot-running-application-check.m" +obj = root / "build/riot-running-application-check.o" +exe = root / "build/riot-running-application-check" +subprocess.run(["clang", *flags["C_DEFINES"], *flags["C_INCLUDES"], + *flags["C_FLAGS"], "-fblocks", "-c", str(source), + "-o", str(obj)], check=True) +subprocess.run(["clang", "-target", "x86_64-apple-macosx11.0", + "-fuse-ld=lld", "-nostdlib", "-isysroot", + "@DARLING_SDK@", str(obj), "-lSystem", + "-framework", "AppKit", "-framework", "Foundation", + "-framework", "CoreGraphics", "-lobjc", "-o", str(exe)], + check=True) diff --git a/tools/build-riot-security-runtime.py b/tools/build-riot-security-runtime.py new file mode 100644 index 0000000..05912af --- /dev/null +++ b/tools/build-riot-security-runtime.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +"""Relink real Security with Darling's built-in requirement parser wired up.""" +from pathlib import Path +import shlex +import shutil +import subprocess + +root = Path(__file__).resolve().parent.parent +component = root / 'build/darling-src/build/src/external/security/OSX' +parser = component / 'libsecurity_codesigning' +flags = {} +for line in (parser / 'CMakeFiles/security_codesigning.dir/flags.make').read_text().splitlines(): + if line.startswith('CXX_') and ' = ' in line: + key, value = line.split(' = ', 1) + flags[key] = shlex.split(value) +# Preserve the archive member's basename when replacing it. +archive = root / 'build/riot-security/libsecurity_codesigning.a' +archive.parent.mkdir(exist_ok=True) +shutil.copy2(parser / 'libsecurity_codesigning.a', archive) +ar = shlex.split((parser / 'CMakeFiles/security_codesigning.dir/link.txt').read_text())[0] +for name in ['reqparser', 'StaticCode']: + obj = archive.parent / (name + '.cpp.o') + subprocess.run(['clang++', *flags['CXX_DEFINES'], *flags['CXX_INCLUDES'], + *flags['CXX_FLAGS'], '-c', str(root / ('build/darling-src/src/external/security/OSX/libsecurity_codesigning/lib/' + name + '.cpp')), + '-o', str(obj)], check=True) + subprocess.run([ar, 'rs', str(archive), str(obj)], check=True) +link = shlex.split((component / 'CMakeFiles/Security_x86_64.dir/link.txt').read_text()) +link[link.index('libsecurity_codesigning/libsecurity_codesigning.a')] = str(archive) +link[link.index('-o') + 1] = str(root / 'build/riot-Security') +subprocess.run(link, cwd=component, check=True) diff --git a/tools/build-riot-server-runtime.py b/tools/build-riot-server-runtime.py new file mode 100644 index 0000000..a537b7e --- /dev/null +++ b/tools/build-riot-server-runtime.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +"""Rebuild changed IPC code into owned outputs, preserving configured objects.""" +from pathlib import Path +import shlex,subprocess +project=Path(__file__).resolve().parent.parent +build=project/'build/darling-src/build/src/external/darlingserver' +flags={} +for line in (build/'CMakeFiles/darlingserver.dir/flags.make').read_text().splitlines(): + 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']: + 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) +link[link.index('-o')+1]=str(project/'build/riot-darlingserver') +subprocess.run(link,cwd=build,check=True) +subprocess.run(['clang++',*flags['CXX_DEFINES'],*flags['CXX_INCLUDES'],*flags['CXX_FLAGS'], + '-D_GNU_SOURCE','-Wno-macro-redefined',str(project/'tools/riot-host-message-check.cpp'), + str(project/'build/riot-server-message.o'),'-o',str(project/'build/riot-host-message-check')],check=True) diff --git a/tools/build-riot-trust-db-check.py b/tools/build-riot-trust-db-check.py new file mode 100644 index 0000000..4e7b973 --- /dev/null +++ b/tools/build-riot-trust-db-check.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +"""Link the database regression against exactly the native trustd dependencies.""" +from pathlib import Path +import shlex +import subprocess + +root = Path(__file__).resolve().parent.parent +component = root / "build/darling-src/build/src/external/security/trust/trustd" +flags = {} +for line in (component / "CMakeFiles/trustd.dir/flags.make").read_text().splitlines(): + if line.startswith("C_") and " = " in line: + key, value = line.split(" = ", 1) + flags[key] = shlex.split(value) +obj = root / "build/riot-trust-db-check.o" +subprocess.run(["clang", *flags["C_DEFINES"], *flags["C_INCLUDES"], *flags["C_FLAGS"], + "-c", str(root / "tools/riot-trust-db-check.c"), "-o", str(obj)], check=True) +link = shlex.split((component / "CMakeFiles/trustd.dir/link.txt").read_text()) +link[link.index("-o") + 1] = str(root / "build/riot-trust-db-check") +for index, argument in enumerate(link): + if argument.endswith("trustd.c.o"): + link[index] = str(obj) +subprocess.run(link, cwd=component, check=True) diff --git a/tools/build-riot-ulock-cond-check.py b/tools/build-riot-ulock-cond-check.py new file mode 100644 index 0000000..d4527cc --- /dev/null +++ b/tools/build-riot-ulock-cond-check.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 +from pathlib import Path +import shlex, subprocess +root=Path(__file__).resolve().parent.parent +import re +res=subprocess.check_output(['clang','-print-resource-dir'],text=True).strip() +flags={} +for line in (root/'build/darling-src/build/src/frameworks/CoreVideo/CMakeFiles/CoreVideo.dir/flags.make').read_text().splitlines(): + if line.startswith('C_') and ' = ' in line: + key,value=line.split(' = ',1); flags[key]=[re.sub(r'/usr/lib/clang/\d+/include',res+'/include',v) for v in shlex.split(value)] +obj=root/'build/riot-ulock-cond-check.o' +subprocess.run(['clang',*flags['C_DEFINES'],*flags['C_INCLUDES'],*flags['C_FLAGS'],'-I'+str(root/'build/darling-src/src/startup/mldr/elfcalls'),'-O2','-c',str(root/'tools/riot-ulock-cond-check.c'),'-o',str(obj)],check=True) +subprocess.run(['clang','-target','x86_64-apple-macosx11.0','-fuse-ld=lld','-nostdlib','-isysroot','@DARLING_SDK@',str(obj),'-lSystem','-o',str(root/'build/riot-ulock-cond-check')],check=True) diff --git a/tools/build-riot-x11-runtime.py b/tools/build-riot-x11-runtime.py new file mode 100644 index 0000000..3ccfb52 --- /dev/null +++ b/tools/build-riot-x11-runtime.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +"""Compile the changed X11 event handler and relink the existing backend.""" +import shlex +import subprocess +from pathlib import Path +project = Path(__file__).resolve().parent.parent +framework = project / 'build/darling-src/build/src/external/cocotron/AppKit' +flags = {} +for line in (framework / 'CMakeFiles/X11_backend.dir/flags.make').read_text().splitlines(): + if line.startswith('C_') and ' = ' in line: + key, value = line.split(' = ', 1) + flags[key] = shlex.split(value) +link = shlex.split((framework / 'CMakeFiles/X11_backend.dir/link.txt').read_text()) +for name in ('X11Display', 'X11Window'): + obj = project / f'build/riot-{name}.o' + subprocess.run(['clang', *flags['C_DEFINES'], *flags['C_INCLUDES'], *flags['C_FLAGS'], '-O2', '-Wno-macro-redefined', '-c', str(project / f'build/darling-src/src/external/cocotron/AppKit/X11.backend/{name}.m'), '-o', str(obj)], check=True) + link[link.index(f'CMakeFiles/X11_backend.dir/X11.backend/{name}.m.o')] = str(obj) +link[link.index('-o') + 1] = str(project / 'build/riot-X11') +subprocess.run(link, cwd=framework, check=True) +print('Built isolated X11 backend with native visibility events') + +# Exercise actual backend modifier transitions without compositor shortcuts. +probe = project / 'build/riot-modifier-check.o' +subprocess.run(['clang', *flags['C_DEFINES'], *flags['C_INCLUDES'], *flags['C_FLAGS'], + '-O2', '-Wno-macro-redefined', '-c', str(project / 'tools/riot-modifier-check.m'), + '-o', str(probe)], check=True) +subprocess.run(['clang', '-target', 'x86_64-apple-macosx11.0', '-fuse-ld=lld', + '-nostdlib', '-isysroot', '@DARLING_SDK@', str(probe), + '-lSystem', '-framework', 'AppKit', '-framework', 'Foundation', '-lobjc', + '-o', str(project / 'build/riot-modifier-check')], check=True) diff --git a/tools/build-riot-xpc-pipe-ownership-check.py b/tools/build-riot-xpc-pipe-ownership-check.py new file mode 100644 index 0000000..b371b25 --- /dev/null +++ b/tools/build-riot-xpc-pipe-ownership-check.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +"""Build the private Mach-port ownership fixture against Darling's real XPC.""" +from pathlib import Path +import shlex +import subprocess + +root = Path(__file__).resolve().parent.parent +build = root / "build/darling-src/build/src/external" +flags = {} +for line in (build / "libxpc/test/CMakeFiles/test_libxpc.dir/flags.make").read_text().splitlines(): + if line.startswith("C_") and " = " in line: + key, value = line.split(" = ", 1) + flags[key] = shlex.split(value) +obj = root / "build/riot-xpc-pipe-ownership-check.o" +subprocess.run(["clang", *flags["C_DEFINES"], *flags["C_INCLUDES"], *flags["C_FLAGS"], + "-c", str(root / "tools/riot-xpc-pipe-ownership-check.c"), "-o", str(obj)], check=True) +component = build / "security/trust/trustd" +link = shlex.split((component / "CMakeFiles/trustd.dir/link.txt").read_text()) +link[link.index("-o") + 1] = str(root / "build/riot-xpc-pipe-ownership-check") +for index, argument in enumerate(link): + if argument.endswith("trustd.c.o"): + link[index] = str(obj) +subprocess.run(link, cwd=component, check=True) diff --git a/tools/darling-system_profiler.sh b/tools/darling-system_profiler.sh new file mode 100644 index 0000000..3410740 --- /dev/null +++ b/tools/darling-system_profiler.sh @@ -0,0 +1,129 @@ +#!/bin/sh +# system_profiler for the Darling prefix: reports the host's real display and +# network hardware for the data types applications query at startup. +# Installed as /usr/sbin/system_profiler by tools/install-darling-system-profiler.py. +host=/Volumes/SystemRoot +xml=0 +types= +while [ $# -gt 0 ]; do + case "$1" in + -xml) xml=1;; + -detailLevel|-timeout) shift;; + -listDataTypes) printf 'Available Datatypes:\nSPAirPortDataType\nSPDisplaysDataType\nSPEthernetDataType\n'; exit 0;; + SP*DataType) types="$types $1";; + esac + shift +done +[ -n "$types" ] || types=" SPDisplaysDataType SPEthernetDataType SPAirPortDataType" + +escape() { sed -e 's/&/\&/g' -e 's//\>/g'; } + +pci_name() { # vendor device (hex without 0x) -> "Vendor Model" + for ids in "$host/usr/share/hwdata/pci.ids" "$host/usr/share/misc/pci.ids"; do + [ -r "$ids" ] || continue + awk -v v="$1" -v d="$2" ' + /^[0-9a-f][0-9a-f][0-9a-f][0-9a-f] / { found = ($1 == v); if (found) { vendor = $0; sub(/^[0-9a-f]+ /, "", vendor) } next } + found && /^\t[0-9a-f][0-9a-f][0-9a-f][0-9a-f] / && $1 == d { model = $0; sub(/^\t[0-9a-f]+ /, "", model); exit } + END { + if (v == "1002") vendor = "AMD"; else if (v == "10de") vendor = "NVIDIA"; else if (v == "8086") vendor = "Intel" + if (match(model, /\[[^]]*\]/)) model = substr(model, RSTART + 1, RLENGTH - 2) + if (vendor != "" && model != "") print vendor " " model; else if (vendor != "") print vendor " Graphics" + }' "$ids" + return + done +} + +displays() { # one line per GPU: vendor|device|vram MB or empty|name + for card in "$host"/sys/class/drm/card[0-9] "$host"/sys/class/drm/card[0-9][0-9]; do + [ -r "$card/device/vendor" ] || continue + vendor=$(sed 's/^0x//' "$card/device/vendor"); device=$(sed 's/^0x//' "$card/device/device") + vram= + if [ -r "$card/device/mem_info_vram_total" ]; then + vram=$(awk '{ printf "%d", $1 / 1048576 }' "$card/device/mem_info_vram_total") + fi + name=$(pci_name "$vendor" "$device"); [ -n "$name" ] || name="Display" + echo "$vendor|$device|$vram|$name" + done +} + +interfaces() { # $1 = wired|wireless; physical host interfaces only + for path in "$host"/sys/class/net/*; do + [ -e "$path/device" ] || continue + if [ -d "$path/wireless" ] || [ -e "$path/phy80211" ]; then kind=wireless; else kind=wired; fi + [ "$kind" = "$1" ] && basename "$path" + done +} + +if [ "$xml" = 1 ]; then + echo '' + echo '' + echo '' + echo '' + for type in $types; do + echo ' ' + echo " _dataType$type" + echo ' _items' + echo ' ' + case "$type" in + SPDisplaysDataType) + displays | while IFS='|' read -r vendor device vram name; do + name=$(printf '%s' "$name" | escape) + echo ' ' + echo " _name$name" + echo ' spdisplays_busspdisplays_builtin' + echo " spdisplays_device-id0x$device" + echo " spdisplays_vendor${name%% *} (0x$vendor)" + [ -z "$vram" ] || echo " spdisplays_vram$vram MB" + echo " sppci_model$name" + echo ' sppci_device_typespdisplays_gpu' + echo ' ' + done;; + SPEthernetDataType) + interfaces wired | while read -r name; do + echo ' ' + echo ' _nameethernet' + echo " spethernet_BSD_Name$name" + echo ' ' + done;; + SPAirPortDataType) + echo ' ' + echo ' spairport_airport_interfaces' + echo ' ' + interfaces wireless | while read -r name; do + echo ' ' + echo " _name$name" + echo ' spairport_wireless_card_typeWi-Fi' + echo ' ' + done + echo ' ' + echo ' ';; + esac + echo ' ' + echo ' ' + done + echo '' + echo '' + exit 0 +fi + +for type in $types; do + case "$type" in + SPDisplaysDataType) + printf 'Graphics/Displays:\n\n' + displays | while IFS='|' read -r vendor device vram name; do + printf ' %s:\n\n Chipset Model: %s\n Type: GPU\n Bus: Built-In\n' "$name" "$name" + [ -z "$vram" ] || printf ' VRAM (Total): %s MB\n' "$vram" + printf ' Vendor: %s (0x%s)\n Device ID: 0x%s\n\n' "${name%% *}" "$vendor" "$device" + done;; + SPEthernetDataType) + printf 'Ethernet Cards:\n\n' + interfaces wired | while read -r name; do + printf ' ethernet:\n\n Type: Ethernet Controller\n BSD name: %s\n\n' "$name" + done;; + SPAirPortDataType) + printf 'Wi-Fi:\n\n Interfaces:\n' + interfaces wireless | while read -r name; do + printf ' %s:\n Card Type: Wi-Fi\n\n' "$name" + done;; + esac +done diff --git a/tools/ensure-darling-runtime.py b/tools/ensure-darling-runtime.py new file mode 100644 index 0000000..a7a6339 --- /dev/null +++ b/tools/ensure-darling-runtime.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +"""Clear only a dead Darling server's stale PID marker before startup.""" +import os +from pathlib import Path +import time +prefix = Path(os.environ.get('DPREFIX', str(Path.home() / '.darling'))) +marker = prefix / '.init.pid' +try: + pid = int(marker.read_text().strip()) +except (OSError, ValueError): + raise SystemExit(0) +if pid <= 0: + raise SystemExit(0) +status = Path('/proc') / str(pid) / 'status' +try: + state = next(line.split()[1] for line in status.read_text().splitlines() if line.startswith('State:')) + dead = state == 'Z' +except FileNotFoundError: + dead = True +except (OSError, StopIteration): + dead = False +if dead: + os.replace(marker, prefix / f'.init.pid.stale-{pid}-{time.time_ns()}') + print(f'Recovered stale Darling server marker for PID {pid}', flush=True) diff --git a/tools/ensure-darling-service-accounts.py b/tools/ensure-darling-service-accounts.py new file mode 100644 index 0000000..64cb932 --- /dev/null +++ b/tools/ensure-darling-service-accounts.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +"""Install trustd's supplied no-login account in Darling's flat-file database.""" +import os +from pathlib import Path +import shutil + +prefix = Path(os.environ.get("DPREFIX", str(Path.home() / ".darling"))) +entries = { + "passwd": "_trustd:*:282:282:trustd:/var/empty:/usr/bin/false", + "master.passwd": "_trustd:*:282:282::0:0:trustd:/var/empty:/usr/bin/false", + "group": "_trustd:*:282:_trustd", +} +def ensure_entry(path, entry): + content = path.read_text() + rows = [line.split(":") for line in content.splitlines() if line and not line.startswith("#")] + if any(row[0] == "_trustd" for row in rows): + return + if any(len(row) > 2 and row[2] == "282" for row in rows): + raise SystemExit(f"Cannot add trustd: guest {path.name} ID 282 is already assigned") + backup = path.with_name(path.name + ".before-m325") + if not backup.exists(): + shutil.copy2(path, backup) + temporary = path.with_name(path.name + ".m325-next") + shutil.copy2(path, temporary) + temporary.write_text(content.rstrip("\n") + "\n" + entry + "\n") + os.replace(temporary, path) + print(f"Added Darling trustd {path.name} entry", flush=True) + + +for name, entry in entries.items(): + ensure_entry(prefix / "private/etc" / name, entry) + +# A prefix-local /etc directory used for DNS overrides shadows Darling's +# stock /etc -> private/etc link. Supply its missing account files too. +etc = prefix / "etc" +if etc.is_dir() and not etc.is_symlink(): + for name in entries: + path = etc / name + if not os.path.lexists(path): + shutil.copy2(prefix / "private/etc" / name, path) + print(f"Exposed Darling /etc/{name}", flush=True) + elif not path.is_symlink(): + ensure_entry(path, entries[name]) diff --git a/tools/ensure-league-runtime-layout.py b/tools/ensure-league-runtime-layout.py new file mode 100644 index 0000000..33ee4ab --- /dev/null +++ b/tools/ensure-league-runtime-layout.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +"""Recover League's stale lock and validate its existing game directory.""" +import os +from pathlib import Path + +prefix = Path(os.environ.get("DPREFIX", str(Path.home() / ".darling"))) +app = prefix / "Applications/League of Legends.app" +game = app / "Contents/LoL/Game" +alias = app / "Game" +lockfile = app / "Contents/LoL/lockfile" +# An interrupted runtime can leave this ephemeral remoting lock behind. Riot +# waits for that old client at startup. Guest PIDs are namespaced, so identify +# a live owner by its executable mapping rather than checking the host PID. +if lockfile.is_file() and not lockfile.is_symlink(): + observed = lockfile.stat() + owner = False + for proc in Path("/proc").iterdir(): + if not proc.name.isdigit(): + continue + try: + if proc.stat().st_uid != os.getuid(): + continue + if proc.joinpath("comm").read_text().strip() != "mldr": + continue + if proc.joinpath("exe").readlink().name != "mldr": + continue + mappings = proc.joinpath("maps").read_text() + if str(app) + "/Contents/LoL/LeagueClient.app/Contents/MacOS/LeagueClient\n" in mappings: + owner = True + break + except FileNotFoundError: + continue + except PermissionError: + owner = True # Cannot establish that the owner is gone. + break + if not owner: + try: + current = lockfile.stat() + except FileNotFoundError: + current = None + if current and (current.st_ino, current.st_mtime_ns) == (observed.st_ino, observed.st_mtime_ns): + lockfile.unlink() + print("Cleared stale League remoting lock after its process exited") +if not (game / "LeagueofLegends.app/Contents/MacOS/LeagueofLegends").is_file(): + raise SystemExit(0) +if os.path.lexists(alias): + if alias.resolve() != game.resolve(): + if alias.is_dir() and not alias.is_symlink(): + print("Riot created a separate Game directory; the update hook registers Contents/LoL/Game directly") + else: + raise SystemExit("League secondary install root conflicts with its macOS game directory") +# The startup API hook registers the native directory directly. Creating a new +# bundle-root alias here makes Riot's integrity pass see an extra directory +# and can turn the first Play action into another repair. diff --git a/tools/env-check.sh b/tools/env-check.sh new file mode 100755 index 0000000..116b206 --- /dev/null +++ b/tools/env-check.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Host + Darling dependency check. Supports Arch / Debian-Ubuntu / Fedora. +set -u +ok=1 +need() { command -v "$1" >/dev/null 2>&1 || { echo "MISSING: $1"; ok=0; }; } +echo "== host ==" +uname -a +need clang; need cmake; need git; need git-lfs; need curl; need unzip; need file +[ "$(uname -m)" = x86_64 ] || { echo "WARN: Darling officially needs x86_64 host (got $(uname -m))"; } +if [ -f /etc/arch-release ]; then + echo "distro: arch" + for p in fuse libudev cairo mesa glu egl tiff freetype libxml2 fontconfig libbsd libxrandr libxcursor libgiflib pulseaudio ffmpeg dbus libxkbfile openssl llvm libcap vulkan-headers libcurl libedit; do :; done + echo "(install: sudo pacman -S cmake clang bison flex fuse libudev cairo mesa glu egl tiff freetype libxml2 fontconfig libbsd libxrandr libxcursor libgiflib pulseaudio ffmpeg dbus libxkbfile openssl llvm libcap vulkan-headers libcurl libedit git-lfs)" +elif [ -f /etc/debian_version ]; then echo "distro: debian/ubuntu (see darling docs/build-instructions.html)"; +elif [ -f /etc/fedora-release ]; then echo "distro: fedora/rhel"; +else echo "distro: unknown"; fi +echo "== darling ==" +command -v darling >/dev/null && darling --version 2>&1 || echo "darling not installed yet (expected at M0)" +[ -d "$HOME/.darling" ] && echo "prefix ~/.darling exists" || echo "prefix ~/.darling absent (created on first 'darling shell')" +echo "== gpu ==" +ls /dev/dri 2>/dev/null || echo "no /dev/dri (Vulkan render needs GPU or lavapipe)" +command -v vulkaninfo >/dev/null && vulkaninfo --summary 2>&1 | head -20 || echo "vulkaninfo absent" +[ "$ok" -eq 1 ] && echo "OK" || { echo "FAIL: install missing tools"; exit 1; } diff --git a/tools/fetch-installer.sh b/tools/fetch-installer.sh new file mode 100755 index 0000000..05ab26f --- /dev/null +++ b/tools/fetch-installer.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# Download Riot macOS installer. Usage: ./fetch-installer.sh [euw|na] [outdir] +set -euo pipefail +REGION="${1:-euw}" +OUT="${2:-build/installer}" +mkdir -p "$OUT" +URL="https://lol.secure.dyn.riotcdn.net/channels/public/x/installer/current/live.${REGION}.zip" +echo "GET $URL" +curl -L --fail -o "$OUT/live.${REGION}.zip" "$URL" +ls -lh "$OUT/live.${REGION}.zip" +unzip -l "$OUT/live.${REGION}.zip" | head -15 +unzip -p "$OUT/live.${REGION}.zip" "Install League of Legends ${REGION}.app/Contents/Info.plist" | grep -A1 FileVersion +echo "saved to $OUT/live.${REGION}.zip" diff --git a/tools/install-darling-system-profiler.py b/tools/install-darling-system-profiler.py new file mode 100644 index 0000000..c6b6fbe --- /dev/null +++ b/tools/install-darling-system-profiler.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +"""Install the host-backed system_profiler into the Darling prefix (no sudo).""" +import os +from pathlib import Path +import shutil + +root = Path(__file__).resolve().parent +prefix = Path(os.environ.get("DPREFIX", str(Path.home() / ".darling"))) +target = prefix / "usr/sbin/system_profiler" +target.parent.mkdir(parents=True, exist_ok=True) +staged = target.with_name(target.name + ".new") +shutil.copyfile(root / "darling-system_profiler.sh", staged) +staged.chmod(0o755) +staged.replace(target) +print(f"installed {target}") diff --git a/tools/install-riot-gpu-runtime.sh b/tools/install-riot-gpu-runtime.sh new file mode 100755 index 0000000..e28d082 --- /dev/null +++ b/tools/install-riot-gpu-runtime.sh @@ -0,0 +1,141 @@ +#!/bin/sh +# Build/install the paired GPU, NV12 and display-clock runtime overrides. +set -eu +project_dir=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +guest_home=${RIOT_GUEST_HOME:-$HOME/.darling@GUEST_HOME@} +cd "$project_dir" +python3 tools/build-m58f333-iosurface.py +python3 tools/build-m58f327-opengl.py +python3 tools/build-m58f336-quartzcore.py +python3 tools/build-m58f341-corevideo.py +python3 tools/build-riot-x11-runtime.py +python3 tools/build-riot-appkit-runtime.py +python3 tools/build-riot-coregraphics-runtime.py +python3 tools/build-riot-corefoundation-runtime.py +python3 tools/build-riot-kqueue-runtime.py +python3 tools/build-riot-malloc-runtime.py +python3 tools/build-riot-pthread-runtime.py +python3 tools/build-riot-audio-runtime.py +python3 tools/build-riot-objc-runtime.py +python3 tools/build-riot-cxxabi-runtime.py +python3 tools/build-riot-indium-runtime.py +python3 tools/build-riot-metal-runtime.py +python3 tools/build-riot-native-vpx-runtime.py +python3 tools/build-riot-server-runtime.py +build/riot-host-message-check +cc -shared -fPIC -O2 -Wall -Wextra -Wl,-soname,"$project_dir/build/libdarling-xshm.so" tools/m58f299-xshm.c -o build/libdarling-xshm.so.next -pthread $(pkg-config --cflags --libs x11 x11-xcb xcb-shm xext) +python3 tools/stop-darling-runtime.py +# Host fileports own SCM_RIGHTS descriptors and must die with their last sender. +sudo make -C "$project_dir/build/darling-src/build/src/external/darlingserver" install -j12 +if [ -f "$HOME/.darling/Applications/Riot Client.app/Contents/Frameworks/Riot Client.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework" ]; then + python3 tools/patch-riot-electron-visibility.py +else + echo 'Riot is not installed yet; apply the guarded Electron visibility fix after installation.' +fi +install -m755 build/libRiotNativeVpx.dylib.next "$guest_home/libRiotNativeVpx.dylib.next" +mv "$guest_home/libRiotNativeVpx.dylib.next" "$guest_home/libRiotNativeVpx.dylib" +mv build/libriot-native-vpx.so.next build/libriot-native-vpx.so +for name in IOSurface OpenGL QuartzCore CoreVideo CoreAudio AudioToolbox CoreGraphics CoreFoundation Metal; do + framework="$guest_home/RuntimeFrameworks/$name.framework" + if [ ! -d "$framework" ]; then cp -a "@DARLING_SDK@/System/Library/Frameworks/$name.framework" "$framework"; fi + binary="$framework/Versions/A/$name" + if [ ! -f "$binary.before-gpu-direct" ]; then cp -p "$binary" "$binary.before-gpu-direct"; fi + case "$name" in + IOSurface) source=build/m333-IOSurface;; + OpenGL) source=build/m327-OpenGL;; + QuartzCore) + if [ -f build/riot-QuartzCore ]; then source=build/riot-QuartzCore; + else source=build/m336-QuartzCore; fi;; + CoreVideo) source=build/m341-CoreVideo;; + CoreAudio) source=build/riot-CoreAudio;; + AudioToolbox) source=build/riot-AudioToolbox;; + CoreGraphics) source=build/riot-CoreGraphics;; + CoreFoundation) source=build/riot-CoreFoundation;; + Metal) source=build/riot-Metal;; + esac + install -m755 "$source" "$binary.next" + mv "$binary.next" "$binary" + # Dyld resolves Versions/A. Preserve the standard outer symlink as well. + if [ ! -L "$framework/$name" ]; then rm -f "$framework/$name"; ln -s "Versions/Current/$name" "$framework/$name"; fi +done +component="$guest_home/Library/Audio/Plug-Ins/Components/DarlingRiotAudio.component" +if [ ! -d "$component" ]; then + mkdir -p "$(dirname "$component")" + cp -a @DARLING_SDK@/System/Library/Components/CoreAudio.component "$component" +fi +component_binary="$component/Contents/MacOS/CoreAudio" +if [ ! -f "$component_binary.before-gpu-direct" ]; then cp -p "$component_binary" "$component_binary.before-gpu-direct"; fi +install -m755 build/riot-CoreAudioComponent "$component_binary.next" +mv "$component_binary.next" "$component_binary" +python3 - "$component/Contents/Info.plist" <<'PY' +from pathlib import Path +import plistlib +import sys +p=Path(sys.argv[1]) +info=plistlib.loads(Path('build/darling-src/src/CoreAudio/CoreAudioComponent/Info.plist').read_bytes()) +for component in info['AudioComponents']: + component['version']=3 +p.write_bytes(plistlib.dumps(info)) +PY +x11="$guest_home/RuntimeFrameworks/AppKit.framework/Versions/C/Resources/Backends/X11.backend/Contents/MacOS/X11" +appkit="$guest_home/RuntimeFrameworks/AppKit.framework/Versions/C/AppKit" +if [ ! -f "$appkit.before-gpu-direct" ]; then cp -p "$appkit" "$appkit.before-gpu-direct"; fi +install -m755 build/riot-AppKit "$appkit.next" +mv "$appkit.next" "$appkit" +if [ ! -f "$x11.before-gpu-direct" ]; then cp -p "$x11" "$x11.before-gpu-direct"; fi +install -m755 build/riot-X11 "$x11.next" +mv "$x11.next" "$x11" +kernel="$guest_home/RuntimeLibraries/libsystem_kernel.dylib" +if [ ! -f "$kernel.before-gpu-direct" ]; then cp -p "$kernel" "$kernel.before-gpu-direct"; fi +install -m755 build/riot-libsystem_kernel.dylib "$kernel.next" +mv "$kernel.next" "$kernel" +allocator="$guest_home/RuntimeLibraries/libsystem_malloc.dylib" +if [ ! -f "$allocator.before-gpu-direct" ]; then + if [ -f "$allocator" ]; then cp -p "$allocator" "$allocator.before-gpu-direct" + else cp -p @DARLING_SDK@/usr/lib/system/libsystem_malloc.dylib "$allocator.before-gpu-direct"; fi +fi +install -m755 build/riot-libsystem_malloc.dylib "$allocator.next" +mv "$allocator.next" "$allocator" +if [ -f build/libdarling-xshm.so ] && [ ! -f build/libdarling-xshm.so.before-gpu-direct ]; then cp -p build/libdarling-xshm.so build/libdarling-xshm.so.before-gpu-direct; fi +mv build/libdarling-xshm.so.next build/libdarling-xshm.so + +objc_library="$guest_home/RuntimeLibraries/libobjc.A.dylib" +if [ ! -f "$objc_library.before-m761" ]; then + if [ -f "$objc_library" ]; then cp -p "$objc_library" "$objc_library.before-m761" + else cp -p @DARLING_SDK@/usr/lib/libobjc.A.dylib "$objc_library.before-m761"; fi +fi +install -m755 build/riot-libobjc.A.dylib "$objc_library.next" +mv "$objc_library.next" "$objc_library" + +cxxabi_library="$guest_home/RuntimeLibraries/libc++abi.dylib" +if [ ! -f "$cxxabi_library.before-m765" ]; then + if [ -f "$cxxabi_library" ]; then cp -p "$cxxabi_library" "$cxxabi_library.before-m765" + else cp -p @DARLING_SDK@/usr/lib/libc++abi.dylib "$cxxabi_library.before-m765"; fi +fi +install -m755 build/riot-libc++abi.dylib "$cxxabi_library.next" +mv "$cxxabi_library.next" "$cxxabi_library" + +indium_library="$guest_home/RuntimeLibraries/libindium.dylib" +if [ ! -f "$indium_library.before-m766" ]; then + if [ -f "$indium_library" ]; then cp -p "$indium_library" "$indium_library.before-m766" + else cp -p @DARLING_SDK@/usr/lib/darling/libindium.dylib "$indium_library.before-m766"; fi +fi +install -m755 build/riot-libindium.dylib "$indium_library.next" +mv "$indium_library.next" "$indium_library" + +# libindium loads libiridium (shader translation and reflection) from the library path +iridium_library="$guest_home/RuntimeLibraries/libiridium.dylib" +if [ ! -f "$iridium_library.before-m767" ]; then + if [ -f "$iridium_library" ]; then cp -p "$iridium_library" "$iridium_library.before-m767" + else cp -p @DARLING_SDK@/usr/lib/darling/libiridium.dylib "$iridium_library.before-m767"; fi +fi +install -m755 build/riot-libiridium.dylib "$iridium_library.next" +mv "$iridium_library.next" "$iridium_library" + +pthread_library="$guest_home/RuntimeLibraries/libsystem_pthread.dylib" +if [ ! -f "$pthread_library.before-gpu-direct" ]; then + if [ -f "$pthread_library" ]; then cp -p "$pthread_library" "$pthread_library.before-gpu-direct" + else cp -p @DARLING_SDK@/usr/lib/system/libsystem_pthread.dylib "$pthread_library.before-gpu-direct"; fi +fi +install -m755 build/riot-libsystem_pthread.dylib "$pthread_library.next" +mv "$pthread_library.next" "$pthread_library" diff --git a/tools/install-riot-presentation-runtime.sh b/tools/install-riot-presentation-runtime.sh new file mode 100644 index 0000000..954e816 --- /dev/null +++ b/tools/install-riot-presentation-runtime.sh @@ -0,0 +1,63 @@ +#!/bin/sh +# Build and install per-user framework overrides; restart before using them. +set -eu +project_dir=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +source_dir="$project_dir/build/darling-src" +build_dir="$source_dir/build" +guest_home="${RIOT_GUEST_HOME:-$HOME/.darling@GUEST_HOME@}" +make -C "$build_dir/src/external/xnu/darling/src/libsystem_kernel" system_kernel -j8 +make -C "$build_dir/src/external/libplatform" platform -j8 +make -C "$build_dir/src/external/corefoundation" CoreFoundation_x86_64 -j8 +make -C "$build_dir/src/external/configd/SystemConfiguration.fproj" SystemConfiguration -j8 +make -C "$build_dir/src/external/cocotron/QuartzCore" QuartzCore -j8 +make -C "$build_dir/src/external/cocotron/AppKit" AppKit X11_backend -j8 +cc -shared -fPIC -O2 -Wall -Wextra \ + "-Wl,-soname,$project_dir/build/libdarling-xshm.so" \ + "$project_dir/tools/m58f299-xshm.c" -o "$project_dir/build/libdarling-xshm.next.so" \ + -pthread $(pkg-config --cflags --libs x11 x11-xcb xcb-shm xext) +clang -target x86_64-apple-macosx11.0 -fuse-ld=lld -dynamiclib -nostdlib \ + -isysroot @DARLING_SDK@ \ + -I"$source_dir/src/startup/mldr/elfcalls" \ + "-DDARLING_XSHM_HOST_LIBRARY=\"$project_dir/build/libdarling-xshm.so\"" \ + -Wl,-undefined,dynamic_lookup -Wl,-install_name,@GUEST_HOME@/libDarlingXShm.dylib \ + "$project_dir/tools/m58f299-native-wrapper.c" -o "$guest_home/libDarlingXShm.next.dylib" +clang -target x86_64-apple-macosx11.0 -fuse-ld=lld -dynamiclib -nostdlib \ + -isysroot @DARLING_SDK@ -O2 -Wall -Wextra -Werror \ + "$project_dir/tools/m58f317-empty-autofill.c" -lSystem \ + -Wl,-install_name,@GUEST_HOME@/libRiotEmptyAutofill.dylib \ + -o "$project_dir/build/libRiotEmptyAutofill.next.dylib" +# Stop before changing the overlay's upper directory, even with atomic writes. +darling shutdown || true +mv "$project_dir/build/libdarling-xshm.next.so" "$project_dir/build/libdarling-xshm.so" +mv "$guest_home/libDarlingXShm.next.dylib" "$guest_home/libDarlingXShm.dylib" +install -m755 "$project_dir/build/libRiotEmptyAutofill.next.dylib" "$guest_home/libRiotEmptyAutofill.next.dylib" +mv "$guest_home/libRiotEmptyAutofill.next.dylib" "$guest_home/libRiotEmptyAutofill.dylib" +mkdir -p "$guest_home/RuntimeFrameworks" +for framework in AppKit QuartzCore CoreFoundation SystemConfiguration; do + if [ ! -d "$guest_home/RuntimeFrameworks/$framework.framework" ]; then + cp -a "@DARLING_SDK@/System/Library/Frameworks/$framework.framework" "$guest_home/RuntimeFrameworks/" + fi +done +atomic_install() { + install -m755 "$1" "$2.next" + mv "$2.next" "$2" +} +atomic_install "$build_dir/src/external/cocotron/AppKit/AppKit" "$guest_home/RuntimeFrameworks/AppKit.framework/Versions/C/AppKit" +atomic_install "$build_dir/src/external/cocotron/AppKit/X11" "$guest_home/RuntimeFrameworks/AppKit.framework/Versions/C/Resources/Backends/X11.backend/Contents/MacOS/X11" +atomic_install "$build_dir/src/external/cocotron/QuartzCore/QuartzCore" "$guest_home/RuntimeFrameworks/QuartzCore.framework/Versions/A/QuartzCore" + +atomic_install "$build_dir/src/external/corefoundation/CoreFoundation" "$guest_home/RuntimeFrameworks/CoreFoundation.framework/Versions/A/CoreFoundation" + +atomic_install "$build_dir/src/external/configd/SystemConfiguration.fproj/SystemConfiguration" "$guest_home/RuntimeFrameworks/SystemConfiguration.framework/Versions/A/SystemConfiguration" + +mkdir -p "$guest_home/RuntimeLibraries" +atomic_install "$build_dir/src/external/libplatform/libsystem_platform.dylib" "$guest_home/RuntimeLibraries/libsystem_platform.dylib" +atomic_install "$build_dir/src/external/xnu/darling/src/libsystem_kernel/libsystem_kernel.dylib" "$guest_home/RuntimeLibraries/libsystem_kernel.dylib" + +# Bootstrap services can start before the app's DYLD_LIBRARY_PATH is applied. +# The prefix overlay is user-owned; install the same corrected kernel there. +prefix_dir="${DPREFIX:-$HOME/.darling}" +mkdir -p "$prefix_dir/usr/lib/system" +atomic_install "$build_dir/src/external/xnu/darling/src/libsystem_kernel/libsystem_kernel.dylib" "$prefix_dir/usr/lib/system/libsystem_kernel.dylib" + +echo "Presentation runtime installed; launch tools/run-riot-ux-with-logdir-shim.sh." diff --git a/tools/m58f299-native-wrapper.c b/tools/m58f299-native-wrapper.c new file mode 100644 index 0000000..bc0a19c --- /dev/null +++ b/tools/m58f299-native-wrapper.c @@ -0,0 +1,23 @@ +/* Guest entry points for the native X11 transport. */ +#include +extern struct elf_calls *_elfcalls; +#ifndef DARLING_XSHM_HOST_LIBRARY +#error DARLING_XSHM_HOST_LIBRARY must name the native transport library +#endif +static void *handle; +__attribute__((constructor)) static void loadTransport(void) { + handle = _elfcalls->dlopen_fatal(DARLING_XSHM_HOST_LIBRARY); +} +void *DarlingXShmGetCreate(void) { + return _elfcalls->dlsym_fatal(handle, "DarlingXShmCreate"); +} +void *DarlingXShmGetPut(void) { + return _elfcalls->dlsym_fatal(handle, "DarlingXShmPut"); +} +void *DarlingXShmGetDestroy(void) { + return _elfcalls->dlsym_fatal(handle, "DarlingXShmDestroy"); +} + +void *DarlingXShmGetStart(void) { + return _elfcalls->dlsym_fatal(handle, "DarlingXShmStart"); +} diff --git a/tools/m58f299-xshm.c b/tools/m58f299-xshm.c new file mode 100644 index 0000000..85388f4 --- /dev/null +++ b/tools/m58f299-xshm.c @@ -0,0 +1,355 @@ +/* Native MIT-SHM transport. Server-created fd segments cross Darling's IPC + * namespace; classic SysV XShmAttach cannot. Keep all native fds in Linux. */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "../build/darling-src/src/frameworks/OpenGL/include/OpenGL/DarlingNativePresent.h" +typedef struct { + Display *display, *ownerDisplay; + XImage *image; + GC privateGC; + XShmSegmentInfo info; + size_t size; + unsigned width, height; + uint32_t visualID; + pthread_mutex_t control, render; + pthread_t worker; + int started, running; + Drawable target; + Window parent, gpuWindow; + Colormap gpuColormap; + int gpuMapped; + GC gc; + char path[128]; + DarlingNativePresentTarget *gpuTarget; + char gpuTargetPath[128]; +} Frame; +typedef struct { + uint32_t magic, sequence, width, height, stride, format; + uint64_t size; +} Header; +__attribute__((visibility("default"))) void * +DarlingXShmCreate(Display *d, Visual *v, unsigned depth, unsigned width, + unsigned height) { + if (!d || !width || !height) + return NULL; + Display *transport = XOpenDisplay(DisplayString(d)); + if (!transport) + return NULL; + if (!XShmQueryExtension(transport)) { + XCloseDisplay(transport); + return NULL; + } + XVisualInfo query = {.visualid = v->visualid}; + int count = 0; + XVisualInfo *visual = XGetVisualInfo(transport, VisualIDMask, &query, &count); + if (!visual) { + XCloseDisplay(transport); + return NULL; + } + Frame *f = calloc(1, sizeof(*f)); + if (!f) { + XFree(visual); + XCloseDisplay(transport); + return NULL; + } + f->display = transport; + f->ownerDisplay = d; + f->visualID=XVisualIDFromVisual(v); + f->width = width; + f->height = height; + f->image = XShmCreateImage(transport, visual->visual, depth, ZPixmap, NULL, + &f->info, width, height); + XFree(visual); + if (!f->image) { + XCloseDisplay(transport); + free(f); + return NULL; + } + if (f->image->bits_per_pixel != 32 || + (unsigned)f->image->bytes_per_line < width * 4) { + XDestroyImage(f->image); + XCloseDisplay(transport); + free(f); + return NULL; + } + f->size = (size_t)f->image->bytes_per_line * height; + xcb_connection_t *c = XGetXCBConnection(transport); + xcb_shm_seg_t segment = xcb_generate_id(c); + xcb_generic_error_t *error = NULL; + xcb_shm_create_segment_cookie_t cookie = + xcb_shm_create_segment(c, segment, f->size, 0); + xcb_shm_create_segment_reply_t *reply = + xcb_shm_create_segment_reply(c, cookie, &error); + if (!reply || error || reply->nfd != 1) { + free(error); + free(reply); + XDestroyImage(f->image); + XCloseDisplay(transport); + free(f); + return NULL; + } + int fd = xcb_shm_create_segment_reply_fds(c, reply)[0]; + void *memory = mmap(NULL, f->size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + close(fd); + free(reply); + if (memory == MAP_FAILED) { + xcb_shm_detach(c, segment); + xcb_flush(c); + XDestroyImage(f->image); + XCloseDisplay(transport); + free(f); + return NULL; + } + f->info.shmseg = segment; + f->info.shmaddr = memory; + f->info.readOnly = False; + f->image->data = memory; + pthread_mutex_init(&f->control, NULL); + pthread_mutex_init(&f->render, NULL); + return f; +} +static int drawFrame(Frame *f, Drawable target, GC gc, const void *pixels, + size_t stride) { + if (!f || !pixels || stride < f->width * 4) + return 0; + pthread_mutex_lock(&f->render); + if (!f->privateGC) + f->privateGC = XCreateGC(f->display, target, 0, NULL); + (void)gc; + for (unsigned y = 0; y < f->height; y++) + memcpy(f->image->data + y * f->image->bytes_per_line, + (const char *)pixels + y * stride, f->width * 4); + int result = XShmPutImage(f->display, target, f->privateGC, f->image, 0, 0, 0, + 0, f->width, f->height, False); + /* Wait for server consumption before reusing the segment on the next tick. */ + XSync(f->display, False); + pthread_mutex_unlock(&f->render); + return result; +} +__attribute__((visibility("default"))) int +DarlingXShmPut(void *handle, Drawable target, GC gc, const void *pixels, + size_t stride) { + Frame *f = handle; + if (!f) + return 0; + if (f->started) { + pthread_mutex_lock(&f->render); + int result = XShmPutImage(f->display, target, f->privateGC, f->image, 0, 0, + 0, 0, f->width, f->height, False); + XSync(f->display, False); + pthread_mutex_unlock(&f->render); + return result; + } + return drawFrame(f, target, gc, pixels, stride); +} +/* The transport connection is a different X client. Raising through it + * triggers the owner's SubstructureRedirect handler. Query through the private + * connection, and use the owner only when a sibling actually covers us. */ +static void ensureRaised(Frame *f) { + Window root, parent, *children = NULL; + unsigned count = 0; + if (XQueryTree(f->display, f->parent, &root, &parent, &children, &count)) { + int covered = count && children[count - 1] != f->target; + if (children) + XFree(children); + if (covered) { + XRaiseWindow(f->ownerDisplay, f->target); + XFlush(f->ownerDisplay); + } + } +} + +static void *pumpFrames(void *arg) { + Frame *f = arg; + void *mapping = NULL; + size_t length = 0; + char mappedPath[128] = {0}; + uint32_t sequence = 0; + unsigned char *snapshot = malloc(f->size); + struct timespec deadline; + clock_gettime(CLOCK_MONOTONIC, &deadline); + unsigned maintenanceTick = 0; + while (snapshot) { + char path[128]; + pthread_mutex_lock(&f->control); + int running = f->running; + memcpy(path, f->path, sizeof(path)); + pthread_mutex_unlock(&f->control); + if (!running) + break; + if (strcmp(path, mappedPath)) { + int fd = open(path, O_RDONLY); + struct stat st; + if (fd >= 0) { + if (!fstat(fd, &st) && st.st_size >= (off_t)sizeof(Header) && + st.st_size <= (off_t)(sizeof(Header) + 64u * 1024u * 1024u)) { + void *next = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd, 0); + if (next != MAP_FAILED) { + if (mapping) + munmap(mapping, length); + mapping = next; + length = st.st_size; + memcpy(mappedPath, path, sizeof(path)); + sequence = 0; + } + } + close(fd); + } + } + /* Sample the committed header promptly, independently of the producer's + * frame phase. X11 stacking/exposure maintenance still runs at about60Hz. */ + int maintenance = maintenanceTick++ % 8 == 0; + int exposed = 0; + if (maintenance) { + ensureRaised(f); + XEvent event; + while (XCheckWindowEvent(f->display, f->target, ExposureMask, &event)) + exposed = 1; + } + uint32_t gpuReady=f->gpuTarget?__atomic_load_n(&f->gpuTarget->ready,__ATOMIC_ACQUIRE):0; + if(gpuReady==1){if(f->gpuWindow&&!f->gpuMapped){XMapRaised(f->display,f->gpuWindow);XSync(f->display,False);f->gpuMapped=1;}__atomic_store_n(&f->gpuTarget->ready,2,__ATOMIC_RELEASE);gpuReady=2;} + if(!gpuReady && f->gpuMapped){XUnmapWindow(f->display,f->gpuWindow);XFlush(f->display);f->gpuMapped=0;} + if (mapping && !gpuReady) { + Header before, after; + memcpy(&before, mapping, sizeof(before)); + __sync_synchronize(); + size_t offset = + before.magic == 0x43414632u && (before.format & 0x40000000u) + ? 32u * 1024u * 1024u + : 0; + if ((before.magic == 0x43414652u || before.magic == 0x43414632u) && + !(before.sequence & 1) && before.sequence != sequence && + before.width == f->width && before.height == f->height && + before.stride >= before.width * 4 && + before.size == (uint64_t)before.stride * before.height && + offset <= length - sizeof(Header) && + before.size <= length - sizeof(Header) - offset) { + for (unsigned y = 0; y < f->height; y++) + memcpy(snapshot + y * f->image->bytes_per_line, + (char *)mapping + sizeof(Header) + offset + + (size_t)y * before.stride, + f->width * 4); + __sync_synchronize(); + memcpy(&after, mapping, sizeof(after)); + if (after.sequence == before.sequence) { + drawFrame(f, f->target, f->gc, snapshot, f->image->bytes_per_line); + sequence = before.sequence; + } + } else { + if (exposed) + DarlingXShmPut(f, f->target, f->gc, NULL, 0); + else if (maintenance) + XFlush(f->display); + } + } + deadline.tv_nsec += 2000000; + if (deadline.tv_nsec >= 1000000000) { + deadline.tv_nsec -= 1000000000; + deadline.tv_sec++; + } + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + if (now.tv_sec > deadline.tv_sec || + (now.tv_sec == deadline.tv_sec && now.tv_nsec > deadline.tv_nsec)) + deadline = now; + clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &deadline, NULL); + } + if (mapping) + munmap(mapping, length); + free(snapshot); + return NULL; +} +__attribute__((visibility("default"))) int +DarlingXShmStart(void *handle, Drawable target, GC gc, const char *path) { + Frame *f = handle; + (void)gc; + if (!f || !path || strlen(path) >= sizeof(f->path)) + return 0; + pthread_mutex_lock(&f->control); + strcpy(f->path, path); + pthread_mutex_unlock(&f->control); + if (f->started) + return 1; + Window root, *children = NULL; + unsigned count = 0; + if (!XQueryTree(f->display, target, &root, &f->parent, &children, &count)) + return 0; + if (children) + XFree(children); + f->target = target; + f->gc = f->privateGC; + f->running = 1; + if(f->width>=256 && f->height>=256) { + XVisualInfo gpuVisual; + if(XMatchVisualInfo(f->display,DefaultScreen(f->display),32,TrueColor,&gpuVisual)){ + f->gpuColormap=XCreateColormap(f->display,DefaultRootWindow(f->display),gpuVisual.visual,AllocNone); + XSetWindowAttributes attrs={.colormap=f->gpuColormap,.border_pixel=0,.background_pixmap=None}; + f->gpuWindow=XCreateWindow(f->display,target,0,0,f->width,f->height,0,32,InputOutput,gpuVisual.visual,CWColormap|CWBorderPixel|CWBackPixmap,&attrs); + XShapeCombineRectangles(f->display,f->gpuWindow,ShapeInput,0,0,NULL,0,ShapeSet,Unsorted); + f->visualID=gpuVisual.visualid; + } + const char *directory=getenv("DARLING_GPU_TARGET_DIRECTORY");if(!directory)directory="/dev/shm"; + snprintf(f->gpuTargetPath,sizeof(f->gpuTargetPath),"%s/darling-ca-gpu-target-%u-%u-%u",directory,getuid(),f->width,f->height); + int fd=open(f->gpuTargetPath,O_CREAT|O_RDWR,0600); + if(fd>=0){ + if(!ftruncate(fd,sizeof(DarlingNativePresentTarget))){ + void *p=mmap(NULL,sizeof(DarlingNativePresentTarget),PROT_READ|PROT_WRITE,MAP_SHARED,fd,0); + if(p!=MAP_FAILED){f->gpuTarget=p;DarlingNativePresentTarget record={.target=f->gpuWindow?f->gpuWindow:target,.width=f->width,.height=f->height,.visual=f->visualID,.flags=getenv("DARLING_GPU_SYNTHETIC_TARGET")?0x54455354:0}; + memcpy(p,&record,sizeof(record));__atomic_store_n(&f->gpuTarget->magic,DARLING_NATIVE_PRESENT_MAGIC,__ATOMIC_RELEASE); + } + } + close(fd); + } + } + XSelectInput(f->display, target, ExposureMask); + XFlush(f->display); + if (pthread_create(&f->worker, NULL, pumpFrames, f)) { + f->running = 0; + return 0; + } + f->started = 1; + return 1; +} +__attribute__((visibility("default"))) void DarlingXShmDestroy(void *handle) { + Frame *f = handle; + if (!f) + return; + if (f->started) { + pthread_mutex_lock(&f->control); + f->running = 0; + pthread_mutex_unlock(&f->control); + pthread_join(f->worker, NULL); + } + if(f->gpuTarget){ + if(f->gpuTarget->target==(f->gpuWindow?f->gpuWindow:f->target)){__atomic_store_n(&f->gpuTarget->magic,0,__ATOMIC_RELEASE);unlink(f->gpuTargetPath);} + munmap(f->gpuTarget,sizeof(DarlingNativePresentTarget)); + } + if(f->gpuWindow)XDestroyWindow(f->display,f->gpuWindow); + if(f->gpuColormap)XFreeColormap(f->display,f->gpuColormap); + pthread_mutex_destroy(&f->control); + pthread_mutex_destroy(&f->render); + xcb_connection_t *c = XGetXCBConnection(f->display); + xcb_shm_detach(c, f->info.shmseg); + XSync(f->display, False); + munmap(f->info.shmaddr, f->size); + f->image->data = NULL; + XDestroyImage(f->image); + if (f->privateGC) + XFreeGC(f->display, f->privateGC); + XCloseDisplay(f->display); + free(f); +} diff --git a/tools/m58f327-cgl-context-check.c b/tools/m58f327-cgl-context-check.c new file mode 100644 index 0000000..7cd046d --- /dev/null +++ b/tools/m58f327-cgl-context-check.c @@ -0,0 +1,63 @@ +/* Exercise the production CGL bridge with mocked EGL; no GPU submissions. */ +#define glGetError mock_glGetError +#include "../build/darling-src/src/frameworks/OpenGL/OpenGL.c" +#undef glGetError +#include +static int fail_current, gl_calls; +static __thread int bound_api; +static EGLBoolean mock_bind(EGLenum api) { bound_api=api; return EGL_TRUE; } +static EGLContext mock_create(EGLDisplay d,EGLConfig c,EGLContext s,const EGLint*a) { + return bound_api==EGL_OPENGL_API ? (EGLContext)0x1234 : EGL_NO_CONTEXT; +} +static EGLBoolean mock_current(EGLDisplay d,EGLSurface a,EGLSurface b,EGLContext c) { + return fail_current ? EGL_FALSE : EGL_TRUE; +} +static EGLBoolean mock_destroy(EGLDisplay d,EGLContext c) { return EGL_TRUE; } +GLenum mock_glGetError(void) { gl_calls++;return GL_NO_ERROR; } +static void *worker(void *arg) { + CGLContextObj c=0; + assert(CGLCreateContext(arg,0,&c)==kCGLNoError && bound_api==EGL_OPENGL_API); + CGLReleaseContext(c);return 0; +} +int main(void) { + native_egl_loaded=1;display=(EGLDisplay)1;config=(EGLConfig)1; + p_eglBindAPI=mock_bind;p_eglCreateContext=mock_create; + p_eglMakeCurrent=mock_current;p_eglDestroyContext=mock_destroy; + CGLPixelFormatAttribute attrs[]={kCGLPFAOpenGLProfile,0x3200,kCGLPFASampleBuffers,1,kCGLPFASamples,4,0}; + CGLPixelFormatObj pf=0;GLint count=0,value=-1; + assert(CGLChoosePixelFormat(attrs,&pf,&count)==kCGLNoError); + assert(pf->attributes[6]==0); + CGLDescribePixelFormat(pf,0,kCGLPFAOpenGLProfile,&value);assert(value==0x3200); + CGLDescribePixelFormat(pf,0,kCGLPFASamples,&value);assert(value==4); + CGLDescribePixelFormat(pf,0,kCGLPFAColorSize,&value);assert(value==0); + pthread_t thread;assert(!pthread_create(&thread,0,worker,pf));pthread_join(thread,0); + CGLContextObj defaults=0; + assert(CGLCreateContext(NULL,NULL,&defaults)==kCGLNoError && defaults); + CGLReleaseContext(defaults); + CGLContextObj first=0,second=0; + assert(CGLCreateContext(pf,0,&first)==kCGLNoError); + assert(CGLCreateContext(pf,0,&second)==kCGLNoError); + assert(CGLSetCurrentContext(first)==kCGLNoError); + struct { GLint screen; unsigned guard; } out={-1,0x11223344}; + assert(CGLGetVirtualScreen(first,&out.screen)==kCGLNoError && out.screen==0 && out.guard==0x11223344); + assert(CGLSetVirtualScreen(first,1)==kCGLBadValue); + assert(CGLDescribePixelFormat(pf,0,kCGLPFAVirtualScreenCount,&value)==kCGLNoError && value==1); + CGLRendererInfoObj info=(void*)1;out.screen=-1; + assert(CGLQueryRendererInfo(1,&info,&out.screen)==kCGLBadRendererInfo && !info && !out.screen && out.guard==0x11223344); + fail_current=1; + assert(CGLSetCurrentContext(second)==kCGLBadState && CGLGetCurrentContext()==first); + assert(CGLSetCurrentContext(0)==kCGLBadState && CGLGetCurrentContext()==first); + int dimensions[]={8,8,4,32};const void *keys[]={kIOSurfaceWidth,kIOSurfaceHeight,kIOSurfaceBytesPerElement,kIOSurfaceBytesPerRow}; + const void *values[4];for(int i=0;i<4;i++)values[i]=CFNumberCreate(0,kCFNumberIntType,&dimensions[i]); + CFDictionaryRef options=CFDictionaryCreate(0,keys,values,4,&kCFTypeDictionaryKeyCallBacks,&kCFTypeDictionaryValueCallBacks); + IOSurfaceRef surface=IOSurfaceCreate(options);assert(surface); + assert(CGLTexImageIOSurface2D(second,GL_TEXTURE_2D,GL_RGBA,8,8,GL_RGBA,GL_UNSIGNED_BYTE,surface,0)==kCGLBadState); + ioSurfaceBindingCount=1; + ioSurfaceBindings[0]=(CGLIOSurfaceBinding){IOSurfaceGetID(surface),second,GL_TEXTURE_2D,GL_RGBA,GL_UNSIGNED_BYTE,1,0}; + assert(CGLReadbackIOSurface(surface)==kCGLBadState && !gl_calls && CGLGetCurrentContext()==first); + fail_current=0;CGLSetCurrentContext(0);CGLReleaseContext(first);CGLReleaseContext(second); + assert(CGLReadbackIOSurface(surface)==kCGLBadValue && !gl_calls); + CFRelease(surface);CFRelease(options);for(int i=0;i<4;i++)CFRelease(values[i]);CGLReleasePixelFormat(pf); + printf("CGL_CHECK terminated=1 profile=1 worker_api=1 failed_bind_preserved=1 rejected_upload=1 rejected_readback=1 destroyed_binding_cleared=1 gl_calls=%d\n",gl_calls); + return 0; +} diff --git a/tools/m58f327-cgl-render-check.c b/tools/m58f327-cgl-render-check.c new file mode 100644 index 0000000..1ceccb3 --- /dev/null +++ b/tools/m58f327-cgl-render-check.c @@ -0,0 +1,56 @@ +/* Tiny native CGL/IOSurface round-trip, with a rejected cross-thread bind. */ +#include +#include +#include +#include +#include +#include +#include +extern CGLError CGLReadbackIOSurface(IOSurfaceRef); +static CGLContextObj owner; +static CGLPixelFormatObj format; +static IOSurfaceRef surface; +static void *worker(void *unused) { + CGLContextObj c=0;assert(CGLCreateContext(format,0,&c)==kCGLNoError); + assert(CGLSetCurrentContext(c)==kCGLNoError); + assert(glGetString(GL_VERSION)); + assert(CGLSetCurrentContext(owner)==kCGLBadState && CGLGetCurrentContext()==c); + assert(CGLReadbackIOSurface(surface)==kCGLBadState && CGLGetCurrentContext()==c); + assert(CGLSetCurrentContext(0)==kCGLNoError);CGLReleaseContext(c);return 0; +} +int main(void) { + CGLPixelFormatAttribute attributes[]={kCGLPFAColorSize,32,kCGLPFAAlphaSize,8,0}; + CGLPixelFormatAttribute core[]={kCGLPFAOpenGLProfile,0x3200,kCGLPFAColorSize,32,kCGLPFAAlphaSize,8,0};GLint screens=0; + assert(CGLChoosePixelFormat(getenv("M327_CGL_CORE")?core:attributes,&format,&screens)==kCGLNoError); + assert(CGLCreateContext(format,0,&owner)==kCGLNoError); + assert(CGLSetCurrentContext(owner)==kCGLNoError); + const char *renderer=(const char*)glGetString(GL_RENDERER);assert(renderer); + printf("CGL_RENDER renderer=%s\n",renderer);fflush(stdout); + int dims[]={8,8,4,32};const void *keys[]={kIOSurfaceWidth,kIOSurfaceHeight,kIOSurfaceBytesPerElement,kIOSurfaceBytesPerRow}; + const void *values[4];for(int i=0;i<4;i++)values[i]=CFNumberCreate(0,kCFNumberIntType,&dims[i]); + CFDictionaryRef options=CFDictionaryCreate(0,keys,values,4,&kCFTypeDictionaryKeyCallBacks,&kCFTypeDictionaryValueCallBacks); + surface=IOSurfaceCreate(options);assert(surface); + unsigned char expected[256];for(int i=0;i<256;i+=4){expected[i]=33;expected[i+1]=55;expected[i+2]=77;expected[i+3]=255;} + memcpy(IOSurfaceGetBaseAddress(surface),expected,sizeof(expected)); + GLuint texture;glGenTextures(1,&texture);glBindTexture(GL_TEXTURE_2D,texture); + assert(CGLTexImageIOSurface2D(owner,GL_TEXTURE_2D,GL_RGBA,8,8,GL_RGBA,GL_UNSIGNED_BYTE,surface,0)==kCGLNoError); + memset(IOSurfaceGetBaseAddress(surface),0,256); + assert(CGLReadbackIOSurface(surface)==kCGLNoError); + assert(!memcmp(IOSurfaceGetBaseAddress(surface),expected,sizeof(expected))); + /* More than the old 256-entry cache, while the display binding stays live. */ + IOSurfaceRef extra[300];GLuint textures[300];glGenTextures(300,textures); + for(int i=0;i<300;i++) { + extra[i]=IOSurfaceCreate(options);assert(extra[i]); + glBindTexture(GL_TEXTURE_2D,textures[i]); + assert(CGLTexImageIOSurface2D(owner,GL_TEXTURE_2D,GL_RGBA,8,8,GL_RGBA,GL_UNSIGNED_BYTE,extra[i],0)==kCGLNoError); + } + memset(IOSurfaceGetBaseAddress(surface),0,256); + assert(CGLReadbackIOSurface(surface)==kCGLNoError); + assert(!memcmp(IOSurfaceGetBaseAddress(surface),expected,sizeof(expected))); + for(int i=0;i<300;i++)CFRelease(extra[i]);glDeleteTextures(300,textures); + pthread_t thread;assert(!pthread_create(&thread,0,worker,0));pthread_join(thread,0); + glDeleteTextures(1,&texture);CGLSetCurrentContext(0);CGLReleaseContext(owner); + assert(CGLReadbackIOSurface(surface)==kCGLBadValue); + CGLReleasePixelFormat(format);CFRelease(surface);CFRelease(options);for(int i=0;i<4;i++)CFRelease(values[i]); + printf("CGL_RENDER pixels=pass worker_api=pass cross_thread_rejected=pass destroyed_binding=pass live_bindings_over256=pass\n");return 0; +} diff --git a/tools/m58f333-iosurface-lifetime-check.c b/tools/m58f333-iosurface-lifetime-check.c new file mode 100644 index 0000000..bb5f4a9 --- /dev/null +++ b/tools/m58f333-iosurface-lifetime-check.c @@ -0,0 +1,28 @@ +#include +#include +#include +#include +#include +#include +int main(void) { + int dims[]={8,8,4,32};const void *keys[]={kIOSurfaceWidth,kIOSurfaceHeight,kIOSurfaceBytesPerElement,kIOSurfaceBytesPerRow}; + const void *values[4];for(int i=0;i<4;i++)values[i]=CFNumberCreate(0,kCFNumberIntType,&dims[i]); + CFDictionaryRef options=CFDictionaryCreate(0,keys,values,4,&kCFTypeDictionaryKeyCallBacks,&kCFTypeDictionaryValueCallBacks); + IOSurfaceID firstID=0,ids[100]; + for(int n=0;n<100;n++) { + IOSurfaceRef original=IOSurfaceCreate(options);assert(original); + memset(IOSurfaceGetBaseAddress(original),n,256); + IOSurfaceID sid=IOSurfaceGetID(original);ids[n]=sid;if(!n)firstID=sid;mach_port_t port=IOSurfaceCreateMachPort(original);assert(port); + IOSurfaceRef same=IOSurfaceLookup(sid);assert(same==original);CFRelease(same); + CFRelease(original); + assert(!IOSurfaceLookup(sid)); + IOSurfaceRef imported=IOSurfaceLookupFromMachPort(port);assert(imported && IOSurfaceGetID(imported)==sid); + unsigned char *bytes=IOSurfaceGetBaseAddress(imported);assert(bytes);for(int i=0;i<256;i++)assert(bytes[i]==(unsigned char)n); + CFRelease(imported);assert(mach_port_deallocate(mach_task_self(),port)==KERN_SUCCESS); + assert(!IOSurfaceLookupFromMachPort(port)); + } + CFRelease(options);for(int i=0;i<4;i++)CFRelease(values[i]); + printf("SURFACE_ID first=%u\n",firstID); + printf("SURFACE_IDS");for(int n=0;n<100;n++)printf(" %u",ids[n]);putchar('\n'); + printf("SURFACE_LIFETIME iterations=100 released_id_absent=1 port_survives_release=1 pixels=pass invalid_port_rejected=1\n");return 0; +} diff --git a/tools/m58f340-cgl-planes-check.c b/tools/m58f340-cgl-planes-check.c new file mode 100644 index 0000000..b4315be --- /dev/null +++ b/tools/m58f340-cgl-planes-check.c @@ -0,0 +1,68 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +extern CGLError CGLReadbackIOSurface(IOSurfaceRef); +static CFDictionaryRef plane(int w,int h,int element,int row,int offset) { + const void *keys[]={kIOSurfacePlaneWidth,kIOSurfacePlaneHeight,kIOSurfacePlaneBytesPerElement,kIOSurfacePlaneBytesPerRow,kIOSurfacePlaneOffset}; + int n[]={w,h,element,row,offset};const void *values[5]; + for(int i=0;i<5;i++)values[i]=CFNumberCreate(0,kCFNumberIntType,n+i); + CFDictionaryRef d=CFDictionaryCreate(0,keys,values,5,&kCFTypeDictionaryKeyCallBacks,&kCFTypeDictionaryValueCallBacks); + for(int i=0;i<5;i++)CFRelease(values[i]);return d; +} +int main(void) { + /* Chromium computes every NV12 plane offset through the alignment API. */ + int videoWidth=1920,videoHeight=1080,videoFormat=0x34323076; + int videoRow=IOSurfaceAlignProperty(kIOSurfacePlaneBytesPerRow,videoWidth); + int chromaOffset=IOSurfaceAlignProperty(kIOSurfacePlaneOffset, + IOSurfaceAlignProperty(kIOSurfacePlaneSize,videoRow*videoHeight)); + int videoAllocation=IOSurfaceAlignProperty(kIOSurfaceAllocSize, + chromaOffset+IOSurfaceAlignProperty(kIOSurfacePlaneSize,videoRow*(videoHeight/2))); + assert(videoRow>=videoWidth && chromaOffset>=videoRow*videoHeight && videoAllocation>chromaOffset); + CFDictionaryRef vp[]={plane(videoWidth,videoHeight,1,videoRow,0),plane(videoWidth/2,videoHeight/2,2,videoRow,chromaOffset)}; + CFArrayRef vl=CFArrayCreate(0,(const void**)vp,2,&kCFTypeArrayCallBacks); + const void *vk[]={kIOSurfaceWidth,kIOSurfaceHeight,kIOSurfacePixelFormat,kIOSurfacePlaneInfo,kIOSurfaceAllocSize}; + const void *vv[]={CFNumberCreate(0,kCFNumberIntType,&videoWidth),CFNumberCreate(0,kCFNumberIntType,&videoHeight),CFNumberCreate(0,kCFNumberIntType,&videoFormat),vl,CFNumberCreate(0,kCFNumberIntType,&videoAllocation)}; + CFDictionaryRef vo=CFDictionaryCreate(0,vk,vv,5,&kCFTypeDictionaryKeyCallBacks,&kCFTypeDictionaryValueCallBacks); + IOSurfaceRef video=IOSurfaceCreate(vo);assert(video && IOSurfaceGetPlaneCount(video)==2 && IOSurfaceGetAllocSize(video)==videoAllocation); + assert((char*)IOSurfaceGetBaseAddressOfPlane(video,1)-(char*)IOSurfaceGetBaseAddressOfPlane(video,0)==chromaOffset); + printf("CHROMIUM_NV12_ALLOCATION width=1920 height=1080 row=%d chroma_offset=%d allocation=%d pass=1\n",videoRow,chromaOffset,videoAllocation); + CFRelease(video);CFRelease(vo);CFRelease(vl);CFRelease(vp[0]);CFRelease(vp[1]);for(int i=0;i<5;i++)if(i!=3)CFRelease(vv[i]); + CFDictionaryRef ps[]={plane(16,16,1,16,0),plane(8,8,2,16,256)}; + CFArrayRef list=CFArrayCreate(0,(const void**)ps,2,&kCFTypeArrayCallBacks); + int width=16,height=16,fourcc=0x34323076; + const void *keys[]={kIOSurfaceWidth,kIOSurfaceHeight,kIOSurfacePixelFormat,kIOSurfacePlaneInfo}; + const void *values[]={CFNumberCreate(0,kCFNumberIntType,&width),CFNumberCreate(0,kCFNumberIntType,&height),CFNumberCreate(0,kCFNumberIntType,&fourcc),list}; + CFDictionaryRef options=CFDictionaryCreate(0,keys,values,4,&kCFTypeDictionaryKeyCallBacks,&kCFTypeDictionaryValueCallBacks); + IOSurfaceRef s=IOSurfaceCreate(options);assert(s && IOSurfaceGetPlaneCount(s)==2 && IOSurfaceGetAllocSize(s)==384); + /* A decoder surface must never overwrite the native packed-pixel frame slot. */ + void (*publish)(IOSurfaceRef)=(void*)dlsym(RTLD_DEFAULT,"DarlingCAPublishIOSurfacePixels");assert(publish); + char framePath[128];snprintf(framePath,sizeof(framePath),"/dev/shm/darling-ca-frame-%u-%d",getuid(),getpid()); + publish(s);assert(access(framePath,F_OK)==-1 && errno==ENOENT); + unsigned char y[256],uv[128];memset(y,92,sizeof(y));for(int i=0;i<128;i+=2){uv[i]=54;uv[i+1]=183;} + memcpy(IOSurfaceGetBaseAddressOfPlane(s,0),y,256);memcpy(IOSurfaceGetBaseAddressOfPlane(s,1),uv,128); + mach_port_t port=IOSurfaceCreateMachPort(s);assert(port); + IOSurfaceRef alias=IOSurfaceLookupFromMachPort(port);assert(alias && IOSurfaceGetPlaneCount(alias)==2 && IOSurfaceGetWidthOfPlane(alias,1)==8 && IOSurfaceGetBytesPerElementOfPlane(alias,1)==2); + assert(!memcmp(IOSurfaceGetBaseAddressOfPlane(alias,0),y,256) && !memcmp(IOSurfaceGetBaseAddressOfPlane(alias,1),uv,128)); + CGLPixelFormatAttribute attrs[]={kCGLPFAOpenGLProfile,0x3200,0};CGLPixelFormatObj pf=0;GLint screens;CGLContextObj c=0; + assert(CGLChoosePixelFormat(attrs,&pf,&screens)==0 && CGLCreateContext(pf,0,&c)==0 && CGLSetCurrentContext(c)==0); + GLuint textures[2];glGenTextures(2,textures); + glBindTexture(GL_TEXTURE_2D,textures[0]);assert(CGLTexImageIOSurface2D(c,GL_TEXTURE_2D,0x8229,16,16,0x1903,GL_UNSIGNED_BYTE,s,0)==0); + unsigned char outY[256];glGetTexImage(GL_TEXTURE_2D,0,0x1903,GL_UNSIGNED_BYTE,outY);assert(!memcmp(y,outY,256)); + glBindTexture(GL_TEXTURE_2D,textures[1]);assert(CGLTexImageIOSurface2D(c,GL_TEXTURE_2D,0x822b,8,8,0x8227,GL_UNSIGNED_BYTE,s,1)==0); + unsigned char outUV[128];glGetTexImage(GL_TEXTURE_2D,0,0x8227,GL_UNSIGNED_BYTE,outUV);assert(!memcmp(uv,outUV,128)); + /* Rebinding a recycled plane must upload its new pixels without reallocating. */ + memset(uv,123,128);memcpy(IOSurfaceGetBaseAddressOfPlane(s,1),uv,128);assert(CGLTexImageIOSurface2D(c,GL_TEXTURE_2D,0x822b,8,8,0x8227,GL_UNSIGNED_BYTE,s,1)==0); + glGetTexImage(GL_TEXTURE_2D,0,0x8227,GL_UNSIGNED_BYTE,outUV);assert(!memcmp(uv,outUV,128)); + memset(IOSurfaceGetBaseAddressOfPlane(s,1),0,128);assert(CGLReadbackIOSurface(s)==0 && !memcmp(IOSurfaceGetBaseAddressOfPlane(alias,1),uv,128)); + assert(CGLTexImageIOSurface2D(c,GL_TEXTURE_2D,0x822b,8,8,0x8227,GL_UNSIGNED_BYTE,s,2)==kCGLBadValue); + glDeleteTextures(2,textures);CGLSetCurrentContext(0);CGLReleaseContext(c);CGLReleasePixelFormat(pf); + CFRelease(alias);CFRelease(s);mach_port_deallocate(mach_task_self(),port);CFRelease(options);for(int i=0;i<3;i++)CFRelease(values[i]);CFRelease(list);CFRelease(ps[0]);CFRelease(ps[1]); + puts("CGL_PLANES count=2 dimensions=pass transport=pass luma=pass chroma=pass invalid_plane_rejected=pass");return 0; +} diff --git a/tools/m58f341-display-clock-check.c b/tools/m58f341-display-clock-check.c new file mode 100644 index 0000000..0c54d48 --- /dev/null +++ b/tools/m58f341-display-clock-check.c @@ -0,0 +1,26 @@ +#include +#include +#include +#include +#include +#include +#include +#include +static CVReturn tick(CVDisplayLinkRef l,const CVTimeStamp *now,const CVTimeStamp *out,CVOptionFlags in,CVOptionFlags *flags,void *info) { + uint64_t t=now->hostTime; + char line[80];int n=snprintf(line,sizeof(line),"%llu\n",(unsigned long long)t); + int fd=open("/Volumes/SystemRoot/tmp/m341-clock-samples",O_CREAT|O_WRONLY|O_APPEND,0600); + if(fd>=0){write(fd,line,n);close(fd);} + if(getenv("M342_SELF_STOP")) assert(CVDisplayLinkStop(l)==0); + return kCVReturnSuccess; +} +int main(void) { + CVDisplayLinkRef link=0;assert(CVDisplayLinkCreateWithActiveCGDisplays(&link)==0); + assert(!CVDisplayLinkIsRunning(link)); + CVDisplayLinkRetain(link);assert(!CVDisplayLinkIsRunning(link));CVDisplayLinkRelease(link); + assert(CVDisplayLinkSetOutputCallback(link,tick,0)==0 && CVDisplayLinkStart(link)==0); + sleep(4);CVDisplayLinkStop(link); + CVDisplayLinkRetain(link);assert(!CVDisplayLinkIsRunning(link));CVDisplayLinkRelease(link); + int fd=open("/Volumes/SystemRoot/tmp/m341-clock-samples",O_WRONLY|O_APPEND,0600);if(fd>=0){write(fd,"STOP\n",5);close(fd);} + sleep(1);CVDisplayLinkRelease(link);puts("DISPLAY_CLOCK completed");return 0; +} diff --git a/tools/m58f352-cgl-direct-check.c b/tools/m58f352-cgl-direct-check.c new file mode 100644 index 0000000..5ab44b6 --- /dev/null +++ b/tools/m58f352-cgl-direct-check.c @@ -0,0 +1,42 @@ +#define GL_GLEXT_PROTOTYPES 1 +#include +#include +#include +#include +#include +#include +#include +#include +#include +extern long __lolsys_syscall(long,long,long,long,long,long,int); +static int (*present)(IOSurfaceRef); +int main(int argc,char **argv){ + unsigned rate=argc==2?(unsigned)strtoul(argv[1],NULL,10):60; + if(!rate || rate>240)return 2; + __lolsys_syscall(0x59616d61,-1,0,0,0,0,157); + puts("DIRECT_STAGE main");fflush(stdout); + CGLPixelFormatAttribute attrs[]={kCGLPFAOpenGLProfile,0x3200,kCGLPFAColorSize,32,kCGLPFAAlphaSize,8,0}; + CGLPixelFormatObj pf=0;CGLContextObj ctx=0;GLint screens; + assert(!CGLChoosePixelFormat(attrs,&pf,&screens));assert(!CGLCreateContext(pf,0,&ctx));assert(!CGLSetCurrentContext(ctx)); + present=(void*)dlsym(RTLD_DEFAULT,"CGLPresentIOSurface");assert(present);puts("DIRECT_STAGE context");fflush(stdout); + int dims[]={337,339,4,337*4};const void*keys[]={kIOSurfaceWidth,kIOSurfaceHeight,kIOSurfaceBytesPerElement,kIOSurfaceBytesPerRow};const void*values[4]; + for(int i=0;i<4;i++)values[i]=CFNumberCreate(0,kCFNumberIntType,&dims[i]); + CFDictionaryRef options=CFDictionaryCreate(0,keys,values,4,&kCFTypeDictionaryKeyCallBacks,&kCFTypeDictionaryValueCallBacks);IOSurfaceRef s=IOSurfaceCreate(options);assert(s); + puts("DIRECT_STAGE surface");fflush(stdout); + GLuint tex,fbo;glGenTextures(1,&tex);glBindTexture(GL_TEXTURE_2D,tex); + assert(!CGLTexImageIOSurface2D(ctx,GL_TEXTURE_2D,GL_RGBA8,337,339,GL_RGBA,GL_UNSIGNED_BYTE,s,0)); + glGenFramebuffers(1,&fbo);glBindFramebuffer(GL_FRAMEBUFFER,fbo);glFramebufferTexture2D(GL_FRAMEBUFFER,GL_COLOR_ATTACHMENT0,GL_TEXTURE_2D,tex,0);assert(glCheckFramebufferStatus(GL_FRAMEBUFFER)==GL_FRAMEBUFFER_COMPLETE); + puts("DIRECT_STAGE framebuffer");fflush(stdout); + sleep(2); + unsigned success=0;struct {long sec,nsec;} deadline;assert(__lolsys_syscall(1,(long)&deadline,0,0,0,0,228)==0); + for(unsigned n=0;n<360;n++){ + glClearColor((n%256)/255.f,100/255.f,200/255.f,1);glClear(GL_COLOR_BUFFER_BIT); + if(n==10){unsigned char rgba[4];glReadPixels(0,0,1,1,GL_RGBA,GL_UNSIGNED_BYTE,rgba);printf("DIRECT_SOURCE rgba=%u,%u,%u,%u error=%u\n",rgba[0],rgba[1],rgba[2],rgba[3],glGetError());fflush(stdout);} + success+=present(s)!=0; + GLint read=0,draw=0;glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING,&read);glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING,&draw);assert(read==fbo&&draw==fbo&&CGLGetCurrentContext()==ctx); + deadline.nsec+=1000000000/rate;if(deadline.nsec>=1000000000){deadline.nsec-=1000000000;++deadline.sec;} + if(!getenv("RCLOL_GPU_DIRECT_PACE")){long wait;do{wait=__lolsys_syscall(1,1,(long)&deadline,0,0,0,230);}while(wait==-4);assert(wait==0);} + } + printf("DIRECT_PRESENT successful=%u requested=360 gl_state_restored=1 error=%u\n",success,glGetError());fflush(stdout); + CGLSetCurrentContext(0);CGLReleaseContext(ctx);CGLReleasePixelFormat(pf);CFRelease(s);CFRelease(options);for(int i=0;i<4;i++)CFRelease(values[i]);return success==360?0:1; +} diff --git a/tools/m58f352-direct-present-window.c b/tools/m58f352-direct-present-window.c new file mode 100644 index 0000000..e1c091b --- /dev/null +++ b/tools/m58f352-direct-present-window.c @@ -0,0 +1,27 @@ +/* Public synthetic window for the native GPU presentation probe. */ +#include +#include +#include +#include +#include +#include +#include +#include +extern void *DarlingXShmCreate(Display*,Visual*,unsigned,unsigned,unsigned); +extern int DarlingXShmStart(void*,Drawable,GC,const char*); +extern void DarlingXShmDestroy(void*); +int main(void){ + Display*d=XOpenDisplay(NULL);if(!d)return 1;XVisualInfo v; + if(!XMatchVisualInfo(d,DefaultScreen(d),32,TrueColor,&v))return 2; + for(unsigned j=0;j<37;++j)XCreateSimpleWindow(d,DefaultRootWindow(d),0,0,1,1,0,0,0); + Colormap cmap=XCreateColormap(d,DefaultRootWindow(d),v.visual,AllocNone); + XSetWindowAttributes a={.colormap=cmap,.border_pixel=0,.background_pixel=0,.override_redirect=True}; + Window w=XCreateWindow(d,DefaultRootWindow(d),20,20,1536,864,0,32,InputOutput,v.visual,CWColormap|CWBorderPixel|CWBackPixel|CWOverrideRedirect,&a); + XStoreName(d,w,"Synthetic GPU frame probe");XMapRaised(d,w);XSync(d,False); + int fd=open("/tmp/m352-synthetic-slot",O_CREAT|O_RDWR,0600);size_t size=32+1536*864*4; + if(fd<0||ftruncate(fd,size))return 3; + uint32_t*h=mmap(NULL,size,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);close(fd); + h[0]=0x43414652;h[1]=2;h[2]=1536;h[3]=864;h[4]=1536*4;h[5]=0;((uint64_t*)h)[3]=1536*864*4; + void*f=DarlingXShmCreate(d,v.visual,32,1536,864);if(!f||!DarlingXShmStart(f,w,DefaultGC(d,DefaultScreen(d)),"/tmp/m352-synthetic-slot"))return 4; + printf("%lu\n",w);fflush(stdout);sleep(12);DarlingXShmDestroy(f);munmap(h,size);XDestroyWindow(d,w);XFreeColormap(d,cmap);XCloseDisplay(d);return 0; +} diff --git a/tools/riot-air-corpus-check.mm b/tools/riot-air-corpus-check.mm new file mode 100644 index 0000000..db76ba4 --- /dev/null +++ b/tools/riot-air-corpus-check.mm @@ -0,0 +1,54 @@ +// Feed shader libraries extracted from the game's shader cache to Darling's Metal and report why +// translation fails. usage: riot-air-corpus-check [max files] +#import +#import +#include +#include +#include +#include +#include +#include +#include + +int main(int argc, char** argv) { + @autoreleasepool { + if (argc < 2) { fprintf(stderr, "usage: %s

[max]\n", argv[0]); return 2; } + size_t maxFiles = argc > 2 ? strtoul(argv[2], nullptr, 10) : SIZE_MAX; + std::vector files; + if (DIR* dir = opendir(argv[1])) { + while (struct dirent* e = readdir(dir)) { + std::string n = e->d_name; + if (n.size() > 4 && n.substr(n.size() - 4) == ".bin") files.push_back(n); + } + closedir(dir); + } + std::sort(files.begin(), files.end()); + if (files.size() > maxFiles) files.resize(maxFiles); + + id device = MTLCreateSystemDefaultDevice(); + if (!device) { fprintf(stderr, "no Metal device\n"); return 1; } + + std::map> reasons; + size_t ok = 0, failed = 0; + for (const std::string& name : files) { + fprintf(stderr, "CHECK: %s\n", name.c_str()); + NSData* data = [NSData dataWithContentsOfFile: [NSString stringWithFormat: @"%s/%s", argv[1], name.c_str()]]; + if (!data) continue; + dispatch_data_t dd = dispatch_data_create(data.bytes, data.length, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), DISPATCH_DATA_DESTRUCTOR_DEFAULT); + NSError* error = nil; + id library = [device newLibraryWithData: dd error: &error]; + dispatch_release(dd); + if (library) { ++ok; [library release]; continue; } + ++failed; + std::string message = error ? [[error localizedDescription] UTF8String] : "nil without error"; + auto& r = reasons[message]; + if (r.first++ == 0) r.second = name; + } + printf("libraries: %zu ok, %zu failed\n", ok, failed); + std::vector> sorted; + for (auto& r : reasons) sorted.push_back({r.second.first, r.first + " [e.g. " + r.second.second + "]"}); + std::sort(sorted.rbegin(), sorted.rend()); + for (auto& s : sorted) printf("%6d %s\n", s.first, s.second.c_str()); + } + return 0; +} diff --git a/tools/riot-air-translate-check.cpp b/tools/riot-air-translate-check.cpp new file mode 100644 index 0000000..e8fc6fd --- /dev/null +++ b/tools/riot-air-translate-check.cpp @@ -0,0 +1,42 @@ +// Offline AIR translation and SPIR-V export; no game/client process is involved. +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +int main(int argc, char** argv) { + if (argc < 3) { fprintf(stderr,"usage: %s [limit]\n",argv[0]);return 2; } + if (!Iridium::init()) { fprintf(stderr,"Cannot initialize LLVM\n");return 2; } + DIR* directory = opendir(argv[1]); if(!directory)return 2; + std::vector names; + while(auto entry=readdir(directory)){std::string name=entry->d_name;if(name.size()>4 && name.substr(name.size()-4)==".bin")names.push_back(name);} + closedir(directory);std::sort(names.begin(),names.end()); + if(argc>3 && names.size()>strtoul(argv[3],nullptr,10)) names.resize(strtoul(argv[3],nullptr,10)); + unsigned passed=0,failed=0;std::map reasons; + for(const auto& name:names){ + fprintf(stderr,"CHECK: %s\n",name.c_str()); + std::ifstream file(std::string(argv[1])+"/"+name,std::ios::binary); + std::vector input((std::istreambuf_iterator(file)),std::istreambuf_iterator()); + try { + size_t size=0;Iridium::OutputInfo info; + void* output=Iridium::translate(input.data(),input.size(),size,info); + if(!output || !size)throw std::runtime_error("Empty translated module"); + // An output directory of "-" only checks that translation succeeds + if(std::string(argv[2])=="-"){free(output);++passed;continue;} + std::string path=std::string(argv[2])+"/"+name+".spv"; + FILE* target=fopen(path.c_str(),"wb"); + if(!target){free(output);throw std::runtime_error("Cannot open output file");} + bool written=fwrite(output,1,size,target)==size;fclose(target);free(output); + if(!written)throw std::runtime_error("Cannot write translated module"); + ++passed; + }catch(const std::exception& error){++failed;++reasons[error.what()];fprintf(stderr,"FAIL: %s: %s\n",name.c_str(),error.what());} + } + printf("Translation: %u passed, %u failed\n",passed,failed); + for(const auto& reason:reasons)printf("%u failures: %s\n",reason.second,reason.first.c_str()); + Iridium::finit();return failed?1:0; +} diff --git a/tools/riot-appkit-pump-check.m b/tools/riot-appkit-pump-check.m new file mode 100644 index 0000000..3c513f8 --- /dev/null +++ b/tools/riot-appkit-pump-check.m @@ -0,0 +1,32 @@ +#import +#import +#include +#include +#include + +int main(void) { + @autoreleasepool { + NSApplication *app=[NSApplication sharedApplication]; + __block unsigned callbacks=0; + unsigned events=0, latePolls=0; + uint64_t started=mach_absolute_time(); + for(unsigned i=0;i<200;++i) { + @autoreleasepool { + dispatch_async(dispatch_get_main_queue(), ^{++callbacks;}); + NSEvent *event=[NSEvent keyEventWithType:NSKeyDown location:NSZeroPoint + modifierFlags:0 timestamp:i windowNumber:0 context:nil + characters:@"probe" charactersIgnoringModifiers:@"probe" + isARepeat:NO keyCode:0]; + [app postEvent:event atStart:NO]; + NSEvent *received=[app nextEventMatchingMask:NSKeyDownMask + untilDate:[NSDate distantPast] inMode:NSDefaultRunLoopMode dequeue:YES]; + if(received==event)++events; + if(callbacks != i+1)++latePolls; + } + } + mach_timebase_info_data_t scale;mach_timebase_info(&scale); + double elapsed=(mach_absolute_time()-started)*(double)scale.numer/scale.denom/1e6; + printf("APPKIT_PUMP events=%u callbacks=%u late_polls=%u elapsed_ms=%.3f\n",events,callbacks,latePolls,elapsed); + return events==200&&callbacks==200&&latePolls==0?0:1; + } +} diff --git a/tools/riot-audio-device-check.c b/tools/riot-audio-device-check.c new file mode 100644 index 0000000..0a39b17 --- /dev/null +++ b/tools/riot-audio-device-check.c @@ -0,0 +1,104 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static _Atomic unsigned callbacks; +static unsigned phase; +static OSStatus tone(AudioDeviceID device,const AudioTimeStamp* now, + const AudioBufferList* input,const AudioTimeStamp* inputTime, + AudioBufferList* output,const AudioTimeStamp* outputTime,void* user) { + (void)device;(void)now;(void)input;(void)inputTime;(void)outputTime;(void)user; + for(unsigned b=0;bmNumberBuffers;++b) { + float *samples=output->mBuffers[b].mData; + unsigned channels=output->mBuffers[b].mNumberChannels; + unsigned count=output->mBuffers[b].mDataByteSize/sizeof(float); + for(unsigned i=0;i1 && !strcmp(argv[1],"--tone")) { + AudioDeviceIOProcID proc=0; + status=AudioDeviceCreateIOProcID(device,tone,0,&proc); + if(status) return 6; + status=AudioDeviceStart(device,proc); + printf("AUDIO_START status=%d\n",status);fflush(stdout); + if(status) {AudioDeviceDestroyIOProcID(device,proc);return 7;} + for(unsigned i=0;i<150;++i) usleep(20000); + printf("AUDIO_CALLBACKS count=%u\n",(unsigned)callbacks);fflush(stdout); + OSStatus stopped=AudioDeviceStop(device,proc); + AudioDeviceDestroyIOProcID(device,proc); + printf("AUDIO_STOP status=%d\n",stopped); + if(stopped || !callbacks) return 8; + } + return 0; +} diff --git a/tools/riot-audio-graph-check.cpp b/tools/riot-audio-graph-check.cpp new file mode 100644 index 0000000..87a9846 --- /dev/null +++ b/tools/riot-audio-graph-check.cpp @@ -0,0 +1,82 @@ +#include +#include +#include +#include +#include +#include +#include + +static std::atomic calls{0}; +static std::atomic stopFromCallback{false}; +static std::atomic callbackStopResult{1}; +static AUGraph callbackGraph=nullptr; +static OSStatus input(void*, AudioUnitRenderActionFlags*, const AudioTimeStamp*, + UInt32, UInt32 frames, AudioBufferList* data) { + ++calls; + if (stopFromCallback.exchange(false)) { + callbackStopResult.store(AUGraphStop(callbackGraph)); + } + if (data->mNumberBuffers != 2) return -1; + for (unsigned b=0; b<2; ++b) + for (unsigned f=0; fmBuffers[b].mData)[f] = b ? .2f : .1f; + return 0; +} +#define CHECK(call) do { OSStatus s=(call); if(s) { printf("FAIL line=%d status=%d\n",__LINE__,s); return 1; } } while(0) +int main() { + setbuf(stdout,nullptr); + printf("AUDIO_GRAPH_CHECK started\n"); + AUGraph graph=nullptr; + CHECK(NewAUGraph(&graph)); + AudioComponentDescription mix={kAudioUnitType_Mixer,kAudioUnitSubType_MatrixMixer,kAudioUnitManufacturer_Apple,0,0}; + AudioComponentDescription out={kAudioUnitType_Output,kAudioUnitSubType_DefaultOutput,kAudioUnitManufacturer_Apple,0,0}; + AUNode m,o; CHECK(AUGraphAddNode(graph,&mix,&m)); CHECK(AUGraphAddNode(graph,&out,&o)); + CHECK(AUGraphOpen(graph)); + AudioUnit mixer,output; CHECK(AUGraphNodeInfo(graph,m,nullptr,&mixer)); CHECK(AUGraphNodeInfo(graph,o,nullptr,&output)); + AudioStreamBasicDescription format={44100,kAudioFormatLinearPCM, + kAudioFormatFlagIsFloat|kAudioFormatFlagIsPacked|kAudioFormatFlagIsNonInterleaved,4,1,4,2,32,0}; + CHECK(AudioUnitSetProperty(mixer,kAudioUnitProperty_StreamFormat,kAudioUnitScope_Input,0,&format,sizeof(format))); + CHECK(AudioUnitSetProperty(mixer,kAudioUnitProperty_StreamFormat,kAudioUnitScope_Output,0,&format,sizeof(format))); + CHECK(AudioUnitSetProperty(output,kAudioUnitProperty_StreamFormat,kAudioUnitScope_Input,0,&format,sizeof(format))); + UInt32 buses=1; CHECK(AudioUnitSetProperty(mixer,kAudioUnitProperty_ElementCount,kAudioUnitScope_Input,0,&buses,sizeof(buses))); + UInt32 frames=4096; CHECK(AudioUnitSetProperty(mixer,kAudioUnitProperty_MaximumFramesPerSlice,kAudioUnitScope_Global,0,&frames,sizeof(frames))); + AURenderCallbackStruct cb={input,nullptr}; CHECK(AUGraphSetNodeInputCallback(graph,m,0,&cb)); + CHECK(AUGraphConnectNodeInput(graph,m,0,o,0)); CHECK(AUGraphInitialize(graph)); + // Exercise the exact packed global elements used by the game. + for (UInt32 e : {0xffffffffu,0xffff0000u,0xffff0001u,0x0000ffffu,0x0001ffffu}) + CHECK(AudioUnitSetParameter(mixer,kMatrixMixerParam_Volume,kAudioUnitScope_Global,e,1,0)); + CHECK(AudioUnitSetParameter(mixer,kMatrixMixerParam_Volume,kAudioUnitScope_Global,0x00000001,.5f,0)); + CHECK(AudioUnitSetParameter(mixer,kMatrixMixerParam_Volume,kAudioUnitScope_Global,0x00010000,.25f,0)); + float left[16]={},right[16]={}; + struct {UInt32 n; AudioBuffer b[2];} data={2,{{1,sizeof(left),left},{1,sizeof(right),right}}}; + AudioTimeStamp time={}; time.mFlags=kAudioTimeStampSampleTimeValid; + AudioUnitRenderActionFlags flags=0; + CHECK(AudioUnitRender(mixer,&flags,&time,0,16,(AudioBufferList*)&data)); + for(unsigned f=0;f<16;++f) if(std::fabs(left[f]-.05f)>1e-6 || std::fabs(right[f]-.05f)>1e-6) { + printf("FAIL matrix left=%f right=%f calls=%u flags=%u\n",left[f],right[f],calls.load(),flags); return 2; + } + CHECK(AudioUnitSetParameter(mixer,kMatrixMixerParam_Volume,kAudioUnitScope_Global,0xffffffff,0,0)); + time.mSampleTime=16; CHECK(AudioUnitRender(mixer,&flags,&time,0,16,(AudioBufferList*)&data)); + if (!(flags & kAudioUnitRenderAction_OutputIsSilence) || left[0] || right[0]) return 3; + CHECK(AudioUnitSetParameter(mixer,kMatrixMixerParam_Volume,kAudioUnitScope_Global,0xffffffff,.05f,0)); + for(unsigned cycle=0;cycle<2;++cycle) { + unsigned before=calls; + CHECK(AUGraphStart(graph)); usleep(250000); CHECK(AUGraphStop(graph)); + if(calls<=before) {printf("FAIL no output callbacks\n");return 4;} + } + callbackGraph=graph; + for(unsigned cycle=0;cycle<20;++cycle) { + callbackStopResult=1; + CHECK(AUGraphStart(graph)); + stopFromCallback=true; + for(unsigned wait=0;wait<200 && callbackStopResult.load()==1;++wait) usleep(10000); + if(callbackStopResult.load()!=noErr) { + printf("FAIL stop inside render callback cycle=%u status=%d\n",cycle,callbackStopResult.load()); return 5; + } + // A stopped IOProc must not be invoked again, including queued PA work. + unsigned stoppedCalls=calls.load(); usleep(50000); + if(calls.load()!=stoppedCalls) {printf("FAIL callbacks after stop\n");return 6;} + } + CHECK(AUGraphUninitialize(graph)); CHECK(AUGraphClose(graph)); CHECK(DisposeAUGraph(graph)); + printf("PASS graph, matrix routing, mute, output start/stop/restart and 20 callback-initiated stops callbacks=%u\n",calls.load()); + return 0; +} diff --git a/tools/riot-audio-unit-check.cpp b/tools/riot-audio-unit-check.cpp new file mode 100644 index 0000000..642b3f5 --- /dev/null +++ b/tools/riot-audio-unit-check.cpp @@ -0,0 +1,75 @@ +#include +#include +#include +#include +#include +#include + +static std::atomic callbacks{0}, badBuffers{0}, maxFrames{0}; +static unsigned phase; +static OSStatus render(void*,AudioUnitRenderActionFlags*,const AudioTimeStamp*, + UInt32,UInt32 frames,AudioBufferList* buffers) { + if(!buffers || buffers->mNumberBuffers!=2) {++badBuffers;return -1;} + for(unsigned b=0;b<2;++b) { + if(buffers->mBuffers[b].mNumberChannels!=1 || + buffers->mBuffers[b].mDataByteSizemBuffers[b].mData; + for(unsigned i=0;imaxFrames) maxFrames=frames; + ++callbacks; + return noErr; +} + +int main() { + AudioComponentDescription description={kAudioUnitType_Output,kAudioUnitSubType_HALOutput, + kAudioUnitManufacturer_Apple,0,0}; + AudioComponent component=AudioComponentFindNext(nullptr,&description); + printf("AUDIO_UNIT available=%d\n",component!=nullptr);fflush(stdout); + if(!component) return 1; + AudioComponentInstance unit=nullptr; + OSStatus status=AudioComponentInstanceNew(component,&unit); + if(status || !unit) return 2; + UInt32 output=0xdeadbeef,size=sizeof(output); + status=AudioUnitGetProperty(unit,kAudioOutputUnitProperty_EnableIO,kAudioUnitScope_Output,0,&output,&size); + printf("AUDIO_UNIT_OUTPUT status=%d enabled=%u\n",status,output);fflush(stdout); + if(status || output!=1) return 3; + AudioChannelLayout layout={};size=sizeof(layout); + status=AudioUnitGetProperty(unit,kAudioUnitProperty_AudioChannelLayout,kAudioUnitScope_Output,0,&layout,&size); + printf("AUDIO_UNIT_LAYOUT status=%d stereo=%d\n",status,layout.mChannelLayoutTag==kAudioChannelLayoutTag_Stereo);fflush(stdout); + if(status || layout.mChannelLayoutTag!=kAudioChannelLayoutTag_Stereo) return 9; + AudioStreamBasicDescription format={44100,kAudioFormatLinearPCM, + kAudioFormatFlagIsFloat|kAudioFormatFlagIsPacked|kAudioFormatFlagIsNonInterleaved,4,1,4,2,32,0}; + status=AudioUnitSetProperty(unit,kAudioUnitProperty_StreamFormat,kAudioUnitScope_Input,0,&format,sizeof(format)); + printf("AUDIO_UNIT_CONFIGURE status=%d before_initialization=1\n",status);fflush(stdout); + if(status) return 4; + AURenderCallbackStruct callback={render,nullptr}; + status=AudioUnitSetProperty(unit,kAudioUnitProperty_SetRenderCallback,kAudioUnitScope_Input,0,&callback,sizeof(callback)); + if(status) return 5; + UInt32 frames=256; + status=AudioUnitSetProperty(unit,kAudioDevicePropertyBufferFrameSize,kAudioUnitScope_Global,0,&frames,sizeof(frames)); + if(status) return 6; + status=AudioUnitInitialize(unit); + printf("AUDIO_UNIT_INITIALIZE status=%d\n",status);fflush(stdout); + if(status) return 7; + status=AudioOutputUnitStart(unit); + printf("AUDIO_UNIT_START status=%d\n",status);fflush(stdout); + if(status) return 8; + for(unsigned i=0;i<150;++i) usleep(20000); + status=AudioOutputUnitStop(unit); + printf("AUDIO_UNIT_RENDER callbacks=%u bad_buffers=%u max_frames=%u stop_status=%d\n", + (unsigned)callbacks,(unsigned)badBuffers,(unsigned)maxFrames,status);fflush(stdout); + for(unsigned cycle=0;cycle<2 && !status;cycle++) { + unsigned before=callbacks; + status=AudioOutputUnitStart(unit); + for(unsigned i=0;i<50 && !status;++i) usleep(20000); + OSStatus stopped=AudioOutputUnitStop(unit); + printf("AUDIO_UNIT_RESTART cycle=%u start_status=%d stop_status=%d callbacks=%u\n",cycle+1,status,stopped,(unsigned)callbacks-before);fflush(stdout); + if(!status) status=stopped; + if(!status && callbacks==before) status=-1; + } + AudioUnitUninitialize(unit); + AudioComponentInstanceDispose(unit); + return status || !callbacks || badBuffers || maxFrames>frames; +} diff --git a/tools/riot-cancel-rpc-check.c b/tools/riot-cancel-rpc-check.c new file mode 100644 index 0000000..60559a8 --- /dev/null +++ b/tools/riot-cancel-rpc-check.c @@ -0,0 +1,28 @@ +#include +#include +#include +#include +extern int __pthread_canceled(int); +static void *check(void *unused) { + mach_timebase_info_data_t tb; + mach_timebase_info(&tb); + unsigned long long start = mach_absolute_time(); + for (unsigned i = 0; i < 100000; ++i) { + for (int action = 0; action < 3; ++action) { + errno = 0; + if (__pthread_canceled(action) != -1 || errno != ENOSYS) { + fprintf(stderr, "Unexpected cancellation response for action %d: errno=%d\n", action, errno); + return (void *)1; + } + } + } + unsigned long long ns = (mach_absolute_time() - start) * tb.numer / tb.denom; + printf("PASS cancellation unsupported responses: 300000 calls, %llu ns/call\n", ns / 300000); + return NULL; +} +int main(void) { + pthread_t worker; + void *result; + if (pthread_create(&worker, NULL, check, NULL) || pthread_join(worker, &result)) return 1; + return result != NULL; +} diff --git a/tools/riot-clock-fastpath-check.c b/tools/riot-clock-fastpath-check.c new file mode 100644 index 0000000..f1c477e --- /dev/null +++ b/tools/riot-clock-fastpath-check.c @@ -0,0 +1,64 @@ +#include +#include +#include +#include +#include +#include +#include +#include +extern struct elf_calls* _elfcalls; +static int failed; +static long (*native_syscall)(long,...); +static uint64_t ns(struct timespec ts) { return (uint64_t)ts.tv_sec * 1000000000 + ts.tv_nsec; } +static void* worker(void* unused) { + uint64_t previous = mach_absolute_time(); + for (unsigned i=0; i<100000; ++i) { + uint64_t now=mach_absolute_time(); + if(now < previous) { __atomic_store_n(&failed,1,__ATOMIC_RELAXED); return NULL; } + previous=now; + } + return NULL; +} +int main(void) { + void* libc=_elfcalls->dlopen("libc.so.6"); + native_syscall=libc ? (long (*)(long,...))_elfcalls->dlsym(libc,"syscall") : 0; + if(!native_syscall) return 1; + void* vdso=_elfcalls->dlopen("linux-vdso.so.1"); + void* (*lookup)(void*,const char*,const char*)=(void* (*)(void*,const char*,const char*))_elfcalls->dlsym(libc,"dlvsym"); + int (*fast)(int,struct timespec*)=lookup && vdso ? (int (*)(int,struct timespec*))lookup(vdso,"__vdso_clock_gettime","LINUX_2.6") : 0; + printf("CHECK: native vDSO handle=%d lookup=%d function=%d\n",vdso!=0,lookup!=0,fast!=0); + if (fast) { struct timespec check; printf("CHECK: native vDSO clock result=%d\n",fast(1,&check)); } + + mach_timebase_info_data_t tb; mach_timebase_info(&tb); + if(tb.numer!=1 || tb.denom!=1) return 2; + for(unsigned i=0;i<10000;++i) { + uint64_t before=mach_absolute_time(); + struct timespec reference; + if(native_syscall(228L,1,&reference)) return 3; + uint64_t after=mach_absolute_time(); + if(before > ns(reference) || after < ns(reference)) return 4; + struct timeval a,b; + if(gettimeofday(&a,0) || native_syscall(228L,0,&reference) || gettimeofday(&b,0)) return 5; + uint64_t ref=ns(reference)/1000; + if((uint64_t)a.tv_sec*1000000+a.tv_usec > ref || (uint64_t)b.tv_sec*1000000+b.tv_usec < ref) return 6; + } + pthread_t threads[4]; + for(unsigned i=0;i<4;++i) if(pthread_create(&threads[i],0,worker,0)) return 7; + for(unsigned i=0;i<4;++i) if(pthread_join(threads[i],0)) return 8; + if(failed) return 9; + struct timespec invalid; + errno=0; + if(clock_gettime((clockid_t)-123,&invalid)!=-1 || errno!=EINVAL) return 10; + volatile uint64_t sink=0; + uint64_t start=mach_absolute_time(); + for(unsigned i=0;i<1000000;++i) sink=mach_absolute_time(); + uint64_t end=mach_absolute_time(); + printf("BENCH: mach_absolute_time %.1f ns/call\n",(end-start)/1000000.0); + struct timeval tv; + start=mach_absolute_time(); + for(unsigned i=0;i<1000000;++i) if(gettimeofday(&tv,0)) return 11; + end=mach_absolute_time(); + printf("BENCH: gettimeofday %.1f ns/call\n",(end-start)/1000000.0); + printf("PASS: clocks bracket raw Linux syscalls, 4 concurrent monotonic readers, invalid clock errno\n"); + return sink==0; +} diff --git a/tools/riot-codesign-check.c b/tools/riot-codesign-check.c new file mode 100644 index 0000000..6b51e66 --- /dev/null +++ b/tools/riot-codesign-check.c @@ -0,0 +1,98 @@ +/* Read-only checks against Darling's real Security framework. */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +int main(int argc, char **argv) { + Dl_info image; + if (dladdr((const void *)SecStaticCodeCheckValidityWithErrors, &image)) { + printf("CODESIGN_IMAGE base=%p validation=%p\n", image.dli_fbase, + (const void *)SecStaticCodeCheckValidityWithErrors); + printf("CODESIGN_IMAGE path=%s framework_path=%s\n", image.dli_fname, + getenv("DYLD_FRAMEWORK_PATH") ? getenv("DYLD_FRAMEWORK_PATH") : "unset"); + fflush(stdout); + } + if (getenv("CODESIGN_WAIT")) sleep(8); + int failed = 0; + const char *requirements[] = {"anchor apple generic", "never", "identifier \"com.riotgames.LeagueClient\"", "identifier ("}; + for (int i = 0; i < 4; ++i) { + CFStringRef text = CFStringCreateWithCString(NULL, requirements[i], kCFStringEncodingUTF8); + SecRequirementRef requirement = NULL; + OSStatus status = SecRequirementCreateWithString(text, 0, &requirement); + printf("CODESIGN parser_case=%d status=%d expected=%s\n", i, (int)status, + i == 3 ? "reject" : "accept"); + failed |= i == 3 ? status == 0 : status != 0; + if (requirement) CFRelease(requirement); + CFRelease(text); + } + fflush(stdout); + for (int i = 1; i < argc; ++i) { + CFURLRef url = CFURLCreateFromFileSystemRepresentation(NULL, + (const UInt8 *)argv[i], strlen(argv[i]), false); + SecStaticCodeRef code = NULL; + OSStatus created = SecStaticCodeCreateWithPath(url, 0, &code); + printf("CODESIGN target=%d create=%d\n", i, (int)created); + fflush(stdout); + if (!created && code) { + CFDictionaryRef info = NULL; + OSStatus copied = SecCodeCopySigningInformation(code, + kSecCSSigningInformation, &info); + CFArrayRef certs = info ? CFDictionaryGetValue(info, + kSecCodeInfoCertificates) : NULL; + printf("CODESIGN target=%d information=%d certificates=%ld CMS=%d\n", + i, (int)copied, certs ? CFArrayGetCount(certs) : 0L, + info && CFDictionaryGetValue(info, kSecCodeInfoCMS) != NULL); + fflush(stdout); + CFDataRef cms = info ? CFDictionaryGetValue(info, kSecCodeInfoCMS) : NULL; + if (cms) { + CMSDecoderRef decoder = NULL; + OSStatus made = CMSDecoderCreate(&decoder); + OSStatus updated = made ? made : CMSDecoderUpdateMessage(decoder, + CFDataGetBytePtr(cms), CFDataGetLength(cms)); + OSStatus finished = updated ? updated : CMSDecoderFinalizeMessage(decoder); + CFArrayRef decoded = NULL; + OSStatus extracted = finished ? finished : CMSDecoderCopyAllCerts(decoder, &decoded); + printf("CODESIGN target=%d CMS_create=%d update=%d finalize=%d certs=%d count=%ld\n", + i, (int)made, (int)updated, (int)finished, (int)extracted, + decoded ? CFArrayGetCount(decoded) : 0L); + if (decoded) CFRelease(decoded); + if (decoder) CFRelease(decoder); + fflush(stdout); + } + CFErrorRef error = NULL; + OSStatus valid = SecStaticCodeCheckValidityWithErrors(code, 0, NULL, &error); + printf("CODESIGN target=%d validity=%d error_code=%ld\n", i, + (int)valid, error ? CFErrorGetCode(error) : 0L); + fflush(stdout); + if (valid == 0) { + SecRequirementRef never = NULL; + OSStatus parsed = SecRequirementCreateWithString(CFSTR("never"), 0, &never); + OSStatus rejected = parsed ? parsed : SecStaticCodeCheckValidity(code, 0, never); + printf("CODESIGN target=%d false_requirement=%d expected=reject\n", i, (int)rejected); + failed |= parsed != 0 || rejected == 0; + if (never) CFRelease(never); + } + if (error) { + CFStringRef description = CFErrorCopyDescription(error); + char text[1024] = {0}; + if (description && CFStringGetCString(description, text, + sizeof(text), kCFStringEncodingUTF8)) + printf("CODESIGN target=%d description=%s\n", i, text); + if (description) CFRelease(description); + } + failed |= copied != 0 || valid != 0; + if (error) CFRelease(error); + if (info) CFRelease(info); + CFRelease(code); + } else failed = 1; + if (url) CFRelease(url); + } + return failed; +} diff --git a/tools/riot-commpage-caps-check.c b/tools/riot-commpage-caps-check.c new file mode 100644 index 0000000..26793c6 --- /dev/null +++ b/tools/riot-commpage-caps-check.c @@ -0,0 +1,24 @@ +#define PRIVATE +#include +#include "../build/darling-src/src/startup/mldr/commpage-cpu-caps.h" +#include + +static void cpu(unsigned leaf, unsigned subleaf, unsigned* a, unsigned* b, unsigned* c, unsigned* d) { + __asm__ volatile("cpuid" : "=a"(*a), "=b"(*b), "=c"(*c), "=d"(*d) : "a"(leaf), "c"(subleaf)); +} +int main(void) { + unsigned a,b,c,d,ecx1,edx1,ebx7=0; + cpu(0,0,&a,&b,&c,&d); unsigned max=a; + cpu(1,0,&a,&b,&ecx1,&edx1); + if (max>=7) cpu(7,0,&a,&ebx7,&c,&d); + uint64_t xcr0=0; + if ((ecx1 & ((1u<<26)|(1u<<27))) == ((1u<<26)|(1u<<27))) { + __asm__ volatile("xgetbv" : "=a"(a), "=d"(d) : "c"(0)); + xcr0=((uint64_t)d<<32)|a; + } + cpu(0x80000001u,0,&a,&b,&c,&d); + const uint64_t expected=commpage_decode_cpu_caps(ecx1,edx1,ebx7,xcr0,!!(d&(1u<<29))); + const uint64_t actual=*(volatile const uint64_t*)_COMM_PAGE_CPU_CAPABILITIES64; + printf("CPU_CAPS actual=%llx expected=%llx %s\n",(unsigned long long)actual,(unsigned long long)expected,actual==expected?"PASS":"FAIL"); + return actual!=expected; +} diff --git a/tools/riot-corecrypto-ecdsa-check.c b/tools/riot-corecrypto-ecdsa-check.c new file mode 100644 index 0000000..fdbc33a --- /dev/null +++ b/tools/riot-corecrypto-ecdsa-check.c @@ -0,0 +1,52 @@ +/* ECDSA verification through corecrypto against OpenSSL-generated signatures, + * including a corrupted signature and the elapsed time of each verification. */ +#include +#include +#include +#include +#include +#include + +static size_t slurp(const char* dir, const char* curve, const char* ext, uint8_t* buffer, size_t size) +{ + char path[256]; + snprintf(path, sizeof(path), "%s/%s.%s", dir, curve, ext); + FILE* file = fopen(path, "rb"); + if (!file) return 0; + size_t length = fread(buffer, 1, size, file); + fclose(file); + return length; +} + +static int check(const char* dir, const char* curve, ccec_const_cp_t cp) +{ + uint8_t point[256], digest[64], signature[256]; + size_t point_length = slurp(dir, curve, "pt", point, sizeof(point)); + size_t digest_length = slurp(dir, curve, "dig", digest, sizeof(digest)); + size_t signature_length = slurp(dir, curve, "sig", signature, sizeof(signature)); + if (!point_length || !digest_length || !signature_length) { printf("ECDSA %s missing-input FAIL\n", curve); return 1; } + + ccec_pub_ctx_decl_cp(cp, key); + int imported = ccec_import_pub(cp, point_length, point, key); + struct timespec start, end; + clock_gettime(CLOCK_MONOTONIC, &start); + bool valid = false; + int status = ccec_verify(key, digest_length, digest, signature_length, signature, &valid); + clock_gettime(CLOCK_MONOTONIC, &end); + double good_seconds = (end.tv_sec - start.tv_sec) + (end.tv_nsec - start.tv_nsec) / 1e9; + + signature[signature_length - 3] ^= 0x40; + bool tampered_valid = true; + int tampered_status = ccec_verify(key, digest_length, digest, signature_length, signature, &tampered_valid); + int ok = imported == 0 && status == 0 && valid && !(tampered_status == 0 && tampered_valid); + printf("ECDSA %s import=%d verify=%d valid=%d tampered_rejected=%d seconds=%.3f %s\n", curve, imported, status, valid, + !(tampered_status == 0 && tampered_valid), good_seconds, ok ? "ok" : "FAIL"); + fflush(stdout); + return !ok; +} + +int main(int argc, char** argv) +{ + const char* dir = argc > 1 ? argv[1] : "/tmp"; + return check(dir, "prime256v1", ccec_cp_256()) | check(dir, "secp384r1", ccec_cp_384()) | check(dir, "secp521r1", ccec_cp_521()); +} diff --git a/tools/riot-corecrypto-modular-check.c b/tools/riot-corecrypto-modular-check.c new file mode 100644 index 0000000..e635885 --- /dev/null +++ b/tools/riot-corecrypto-modular-check.c @@ -0,0 +1,57 @@ +/* Modular arithmetic on the P-521 group order against reference values. */ +#include +#include +#include +#include +#include +#include +#include +#include + +static void parse(const char* hex, cc_unit* out, cc_size n) +{ + memset(out, 0, ccn_sizeof_n(n)); + size_t length = strlen(hex); + for (size_t i = 0; i < length; ++i) { + int digit = hex[length - 1 - i] <= '9' ? hex[length - 1 - i] - '0' : hex[length - 1 - i] - 'a' + 10; + out[i / 16] |= (cc_unit)digit << ((i % 16) * 4); + } +} + +int main(int argc, char** argv) +{ + FILE* file = fopen(argc > 1 ? argv[1] : "/tmp/arith.txt", "r"); + if (!file) return 3; + const cc_size n = 9; + ccec_const_cp_t cp = ccec_cp_521(); + const cc_unit* order = ccec_cp_o(cp); + uint8_t zp_bytes[cczp_size(ccn_sizeof_n(n))]; + memset(zp_bytes, 0, sizeof zp_bytes); + cczp_t order_zp = (cczp_t)(struct cczp*)zp_bytes; + CCZP_N(order_zp) = n; + memcpy(CCZP_PRIME(order_zp), order, ccn_sizeof_n((2 * n) + 1)); + order_zp.zp->mod_prime = cczp_mod; + printf("TABLE order[0]=%016llx order[8]=%016llx recip[9..18]:", (unsigned long long)order[0], (unsigned long long)order[8]); + for (int i = 9; i < 19; ++i) printf(" %016llx", (unsigned long long)order[i]); + printf("\n"); + printf("COPY recip(zp):"); + for (int i = 0; i < 10; ++i) printf(" %016llx", (unsigned long long)cczp_recip(order_zp)[i]); + printf("\n"); fflush(stdout); + char a_hex[200], b_hex[200], product_hex[200], inverse_hex[200]; + int index = 0, failures = 0; + while (fscanf(file, "%199s %199s %199s %199s", a_hex, b_hex, product_hex, inverse_hex) == 4) { + cc_unit a[n], b[n], product[n], inverse[n], got_product[n], got_inverse[n]; + parse(a_hex, a, n); parse(b_hex, b, n); parse(product_hex, product, n); parse(inverse_hex, inverse, n); + memset(got_product, 0, sizeof got_product); memset(got_inverse, 0, sizeof got_inverse); + cczp_mul_mod(order_zp, got_product, a, b); + printf("MODULAR case=%d mul_mod=%d (inverse next)\n", index, memcmp(got_product, product, sizeof got_product) == 0); + fflush(stdout); + ccn_modular_multiplicative_inverse(n, got_inverse, a, order); + int product_ok = memcmp(got_product, product, sizeof got_product) == 0; + int inverse_ok = memcmp(got_inverse, inverse, sizeof got_inverse) == 0; + printf("MODULAR case=%d mul_mod=%d inverse=%d\n", index++, product_ok, inverse_ok); + failures += !product_ok + !inverse_ok; + } + printf("MODULAR_RESULT failures=%d\n", failures); + return failures != 0; +} diff --git a/tools/riot-corecrypto-primitives-check.c b/tools/riot-corecrypto-primitives-check.c new file mode 100644 index 0000000..dd0b811 --- /dev/null +++ b/tools/riot-corecrypto-primitives-check.c @@ -0,0 +1,36 @@ +/* ccn_add/ccn_sub/ccn_cmp (10 units) and ccn_mul (9 units) against reference values. */ +#include +#include +#include +#include + +static void parse(const char* hex, cc_unit* out, cc_size n) +{ + memset(out, 0, ccn_sizeof_n(n)); + size_t length = strlen(hex); + for (size_t i = 0; i < length && i / 16 < n; ++i) { + int c = hex[length - 1 - i]; + out[i / 16] |= (cc_unit)(c <= '9' ? c - '0' : c - 'a' + 10) << ((i % 16) * 4); + } +} + +int main(int argc, char** argv) +{ + FILE* file = fopen(argv[1], "r"); + if (!file) return 3; + char x_hex[300], y_hex[300], s_hex[300], d_hex[300], a_hex[300], b_hex[300], p_hex[400]; + int cmp_expected, row = 0, bad_add = 0, bad_sub = 0, bad_cmp = 0, bad_mul = 0; + while (fscanf(file, "%299s %299s %299s %299s %d %299s %299s %399s", x_hex, y_hex, s_hex, d_hex, &cmp_expected, a_hex, b_hex, p_hex) == 8) { + cc_unit x[10], y[10], s[10], d[10], got[10], a[9], b[9], p[18], gotp[18]; + parse(x_hex, x, 10); parse(y_hex, y, 10); parse(s_hex, s, 10); parse(d_hex, d, 10); + parse(a_hex, a, 9); parse(b_hex, b, 9); parse(p_hex, p, 18); + ccn_add(10, got, x, y); bad_add += memcmp(got, s, sizeof got) != 0; + ccn_sub(10, got, x, y); bad_sub += memcmp(got, d, sizeof got) != 0; + int c = ccn_cmp(10, x, y); c = (c > 0) - (c < 0); + bad_cmp += (c + 1) != cmp_expected; + ccn_mul(9, gotp, a, b); bad_mul += memcmp(gotp, p, sizeof gotp) != 0; + ++row; + } + printf("PRIMITIVES rows=%d bad_add=%d bad_sub=%d bad_cmp=%d bad_mul=%d\n", row, bad_add, bad_sub, bad_cmp, bad_mul); + return bad_add + bad_sub + bad_cmp + bad_mul != 0; +} diff --git a/tools/riot-cxx-exception-check.cpp b/tools/riot-cxx-exception-check.cpp new file mode 100644 index 0000000..c5ced38 --- /dev/null +++ b/tools/riot-cxx-exception-check.cpp @@ -0,0 +1,15 @@ +#include +#include +#include +#include +int main() { + bool caught = false; + try { std::stoi(""); } + catch (std::out_of_range&) { fprintf(stderr, "FAIL: wrong exception type\n"); return 1; } + catch (std::invalid_argument value) { caught = true; } + if (!caught) return 1; + try { std::stoi("99999999999999999999999999"); } + catch (std::invalid_argument&) { fprintf(stderr, "FAIL: distinct types matched\n"); return 1; } + catch (std::out_of_range&) { printf("PASS: duplicate exception RTTI catches matching type only\n"); return 0; } + return 1; +} diff --git a/tools/riot-cxx-exception-rtti.s b/tools/riot-cxx-exception-rtti.s new file mode 100644 index 0000000..d6c934e --- /dev/null +++ b/tools/riot-cxx-exception-rtti.s @@ -0,0 +1,14 @@ +# Model League's weak local invalid_argument type information. +.section __TEXT,__const +.globl __ZTSSt16invalid_argument +.weak_definition __ZTSSt16invalid_argument +__ZTSSt16invalid_argument: +.asciz "St16invalid_argument" +.section __DATA_CONST,__const +.p2align 3 +.globl __ZTISt16invalid_argument +.weak_definition __ZTISt16invalid_argument +__ZTISt16invalid_argument: +.quad __ZTVN10__cxxabiv120__si_class_type_infoE+16 +.quad __ZTSSt16invalid_argument +.quad __ZTISt11logic_error diff --git a/tools/riot-dispatch-fd-check.c b/tools/riot-dispatch-fd-check.c new file mode 100644 index 0000000..70b57f8 --- /dev/null +++ b/tools/riot-dispatch-fd-check.c @@ -0,0 +1,31 @@ +#include +#include +#include +int main(void) { + dispatch_queue_t queue=dispatch_queue_create("org.darling.riot.fd-check",0); + dispatch_semaphore_t delivered=dispatch_semaphore_create(0); + dispatch_semaphore_t canceled=dispatch_semaphore_create(0); + for(unsigned cycle=0;cycle<40;cycle++) { + int fds[2];if(pipe(fds)) return 1; + __block unsigned seenA=0,seenB=0; + dispatch_source_t a=dispatch_source_create(DISPATCH_SOURCE_TYPE_READ,fds[0],0,queue); + dispatch_source_t b=dispatch_source_create(DISPATCH_SOURCE_TYPE_READ,fds[0],0,queue); + /* Keep the pipe readable until both owners receive their event. Draining + * it in the first handler makes the second notification timing-dependent. */ + dispatch_source_set_event_handler(a,^{if(!seenA++) dispatch_semaphore_signal(delivered); }); + dispatch_source_set_event_handler(b,^{if(!seenB++) dispatch_semaphore_signal(delivered); }); + dispatch_source_set_cancel_handler(a,^{dispatch_semaphore_signal(canceled);}); + dispatch_source_set_cancel_handler(b,^{dispatch_semaphore_signal(canceled);}); + dispatch_resume(a);dispatch_resume(b); + write(fds[1],"x",1); + int failed=dispatch_semaphore_wait(delivered,dispatch_time(DISPATCH_TIME_NOW,2*NSEC_PER_SEC))!=0; + failed|=dispatch_semaphore_wait(delivered,dispatch_time(DISPATCH_TIME_NOW,2*NSEC_PER_SEC))!=0; + dispatch_source_cancel(a);dispatch_source_cancel(b); + failed|=dispatch_semaphore_wait(canceled,dispatch_time(DISPATCH_TIME_NOW,2*NSEC_PER_SEC))!=0; + failed|=dispatch_semaphore_wait(canceled,dispatch_time(DISPATCH_TIME_NOW,2*NSEC_PER_SEC))!=0; + dispatch_release(a);dispatch_release(b);close(fds[0]);close(fds[1]); + if(failed || !seenA || !seenB) {printf("DISPATCH_FD overlap=fail cycle=%u first=%u second=%u\n",cycle,seenA,seenB);return 2;} + } + puts("DISPATCH_FD overlap=pass cancellation=pass descriptor_reuse=pass cycles=40"); + dispatch_release(canceled);dispatch_release(delivered);dispatch_release(queue);return 0; +} diff --git a/tools/riot-dns-config-check.c b/tools/riot-dns-config-check.c new file mode 100644 index 0000000..0cdc67a --- /dev/null +++ b/tools/riot-dns-config-check.c @@ -0,0 +1,40 @@ +#include +#include +#include + +static const char *fixture; +static FILE *config_open(const char *path, const char *mode) { + (void)path; (void)mode; + FILE *file = tmpfile(); + if (file) { fputs(fixture, file); rewind(file); } + return file; +} +#define fopen config_open +#include "../build/darling-src/src/external/configd/darling/src/dnsinfo/dnsinfo.c" +#undef fopen + +int main(void) { + fixture = "# test\r\n nameserver 192.0.2.1 # comment\r\n" + "nameserver 2001:db8::1\nnameserver invalid\n" + "search one.example two.example\ndomain example\n" + "options timeout:5 attempts:2\n"; + for (int i = 0; i < 1000; ++i) { + dns_config_t *config = dns_configuration_copy(); + if (!config || config->n_resolver != 1) return 1; + dns_resolver_t *r = config->resolver[0]; + if (r->n_nameserver != 2 || r->n_search != 2 || + r->nameserver[0]->sa_family != AF_INET || + r->nameserver[0]->sa_len != sizeof(struct sockaddr_in) || + r->nameserver[1]->sa_family != AF_INET6 || + r->nameserver[1]->sa_len != sizeof(struct sockaddr_in6) || + r->flags != DNS_RESOLVER_FLAGS_REQUEST_ALL_RECORDS || + strcmp(r->search[1], "two.example") || strcmp(r->domain, "example") || + strcmp(r->options, "timeout:5 attempts:2")) return 2; + dns_configuration_free(config); + } + fixture = "# no resolver\n \nnameserver invalid\nsearch example\n"; + if (dns_configuration_copy()) return 3; + dns_configuration_free(NULL); + puts("DNS_CONFIG_CHECK IPv4 IPv6 search options empty repeated-free pass=1"); + return 0; +} diff --git a/tools/riot-event-coalescing-check.m b/tools/riot-event-coalescing-check.m new file mode 100644 index 0000000..cdc4f3a --- /dev/null +++ b/tools/riot-event-coalescing-check.m @@ -0,0 +1,32 @@ +#import +#import +#import +#include +#include +@interface ProbeDisplay : NSDisplay +@end +@implementation ProbeDisplay +@end +int main(void) { + @autoreleasepool { + NSDisplay *display=[[ProbeDisplay alloc] init]; + NSEvent *key=[NSEvent keyEventWithType:NSKeyDown location:NSZeroPoint modifierFlags:0 timestamp:0 windowNumber:0 context:nil characters:@"probe" charactersIgnoringModifiers:@"probe" isARepeat:NO keyCode:0]; + NSEvent *old=[NSEvent mouseEventWithType:NSMouseMoved location:NSZeroPoint modifierFlags:0 timestamp:0 windowNumber:0 context:nil eventNumber:0 clickCount:0 pressure:0]; + NSEvent *latest=[NSEvent mouseEventWithType:NSMouseMoved location:NSMakePoint(1,1) modifierFlags:0 timestamp:1 windowNumber:0 context:nil eventNumber:1 clickCount:0 pressure:0]; + [display postEvent:key atStart:NO];[display postEvent:old atStart:NO];[display postEvent:latest atStart:NO]; + [display discardEventsMatchingMask:NSMouseMovedMask beforeEvent:latest]; + BOOL keyPreserved=[display nextEventMatchingMask:NSAnyEventMask untilDate:[NSDate distantPast] inMode:NSDefaultRunLoopMode dequeue:YES]==key; + BOOL latestPreserved=[display nextEventMatchingMask:NSAnyEventMask untilDate:[NSDate distantPast] inMode:NSDefaultRunLoopMode dequeue:YES]==latest; + /* An empty Darling queue returns its NSAppKitSystem wakeup sentinel. */ + BOOL oldRemoved=[[display nextEventMatchingMask:NSAnyEventMask untilDate:[NSDate distantPast] inMode:NSDefaultRunLoopMode dequeue:YES] type]==NSAppKitSystem; + [display postEvent:key atStart:NO]; [display postEvent:latest atStart:NO]; + BOOL peekPreserved=[display nextEventMatchingMask:NSMouseMovedMask untilDate:[NSDate distantPast] inMode:NSDefaultRunLoopMode dequeue:NO]==latest; + BOOL filteredMotion=[display nextEventMatchingMask:NSMouseMovedMask untilDate:[NSDate distantPast] inMode:NSDefaultRunLoopMode dequeue:YES]==latest; + BOOL filteredKey=[display nextEventMatchingMask:NSKeyDownMask untilDate:[NSDate distantPast] inMode:NSDefaultRunLoopMode dequeue:YES]==key; + printf("EVENT_FILTERING peek_preserved=%d matching_motion=%d unmatched_key_preserved=%d\n",peekPreserved,filteredMotion,filteredKey); + printf("EVENT_COALESCING key_preserved=%d older_motion_removed=%d latest_motion_preserved=%d\n",keyPreserved,oldRemoved,latestPreserved); + [display release]; + return keyPreserved&&latestPreserved&&oldRemoved&&peekPreserved&&filteredMotion&&filteredKey?0:1; + } + return 0; +} diff --git a/tools/riot-fast-clock-check.c b/tools/riot-fast-clock-check.c new file mode 100644 index 0000000..9ea276f --- /dev/null +++ b/tools/riot-fast-clock-check.c @@ -0,0 +1,84 @@ +/* Check of the user-space clock in libsystem_kernel (linux_premigration/ext/clock_gettime.c): accuracy against the real + * Linux syscalls on every CPU, global monotonicity across threads that hop between CPUs, and cost per call. */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +extern struct elf_calls* _elfcalls; +static long (*native_syscall)(long, ...); +static uint64_t ns(struct timespec ts) { return (uint64_t)ts.tv_sec * 1000000000ull + ts.tv_nsec; } +static uint64_t shared_last; +static int backwards, ncpu = 12; +static void pin(int cpu) { unsigned long mask = 1ul << cpu; native_syscall(203L, 0L, sizeof(mask), &mask); } + +static void* hopper(void* arg) { + int id = (int)(long)arg; + for (unsigned i = 0; i < 400000; ++i) { + if (i % 2000 == 0) pin((id + i / 2000) % ncpu); + uint64_t seen = __atomic_load_n(&shared_last, __ATOMIC_SEQ_CST); + uint64_t now = mach_absolute_time(); + if (now < seen) __atomic_fetch_add(&backwards, 1, __ATOMIC_RELAXED); + uint64_t last = seen; + while (now > last && !__atomic_compare_exchange_n(&shared_last, &last, now, 0, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST)) {} + } + return NULL; +} + +int main(void) { + void* libc = _elfcalls->dlopen("libc.so.6"); + native_syscall = libc ? (long (*)(long, ...))_elfcalls->dlsym(libc, "syscall") : 0; + if (!native_syscall) return 1; + ncpu = (int)sysconf(_SC_NPROCESSORS_ONLN); if (ncpu < 1 || ncpu > 64) ncpu = 1; + + /* accuracy: the fast value must lie between two real reads, within a tolerance for the sampling error */ + int64_t worst_mono = 0, worst_real = 0; unsigned long outside = 0, total = 0; + for (int round = 0; round < 3; ++round) for (int cpu = 0; cpu < ncpu; ++cpu) { + pin(cpu); + for (unsigned i = 0; i < 4000; ++i) { + struct timespec a, b; struct timeval tv; + native_syscall(228L, 1L, &a); uint64_t fast = mach_absolute_time(); native_syscall(228L, 1L, &b); + int64_t err = fast < ns(a) ? (int64_t)(fast - ns(a)) : fast > ns(b) ? (int64_t)(fast - ns(b)) : 0; + if (llabs(err) > llabs(worst_mono)) worst_mono = err; + if (err) ++outside; + native_syscall(228L, 0L, &a); gettimeofday(&tv, 0); native_syscall(228L, 0L, &b); + uint64_t us = (uint64_t)tv.tv_sec * 1000000ull + tv.tv_usec; + err = us < ns(a) / 1000 ? (int64_t)(us - ns(a) / 1000) : us > ns(b) / 1000 ? (int64_t)(us - ns(b) / 1000) : 0; + if (llabs(err) > llabs(worst_real)) worst_real = err; + ++total; + if (i % 500 == 0) usleep(3000); + } + } + printf("ACCURACY: %lu samples on %d CPUs, monotonic outside the syscall bracket %lu times, worst %lld ns; gettimeofday worst %lld us\n", + total, ncpu, outside, (long long)worst_mono, (long long)worst_real); + + pthread_t threads[8]; + for (long i = 0; i < 8; ++i) if (pthread_create(&threads[i], 0, hopper, (void*)i)) return 2; + for (int i = 0; i < 8; ++i) pthread_join(threads[i], 0); + printf("MONOTONIC: 8 threads x 400000 reads hopping CPUs, went backwards %d times\n", backwards); + + /* long sleep: the per-CPU base is older than the window, the next read must resynchronise */ + pin(1); sleep(2); + { struct timespec a, b; native_syscall(228L, 1L, &a); uint64_t fast = mach_absolute_time(); native_syscall(228L, 1L, &b); + printf("AFTER SLEEP: inside bracket %d\n", fast + 20000 >= ns(a) && fast <= ns(b) + 20000); } + + volatile uint64_t sink = 0; struct timeval tv; struct timespec ts; + uint64_t start = mach_absolute_time(); + for (unsigned i = 0; i < 2000000; ++i) sink = mach_absolute_time(); + uint64_t end = mach_absolute_time(); + printf("BENCH: mach_absolute_time %.1f ns/call\n", (end - start) / 2000000.0); + start = mach_absolute_time(); + for (unsigned i = 0; i < 2000000; ++i) gettimeofday(&tv, 0); + end = mach_absolute_time(); + printf("BENCH: gettimeofday %.1f ns/call\n", (end - start) / 2000000.0); + start = mach_absolute_time(); + for (unsigned i = 0; i < 300000; ++i) clock_gettime(CLOCK_MONOTONIC, &ts); + end = mach_absolute_time(); + printf("BENCH: clock_gettime(CLOCK_MONOTONIC) %.1f ns/call\n", (end - start) / 300000.0); + (void)sink; + return (backwards || llabs(worst_mono) > 50000 || llabs(worst_real) > 100) ? 3 : 0; +} diff --git a/tools/riot-fault-recovery-check.c b/tools/riot-fault-recovery-check.c new file mode 100644 index 0000000..659d860 --- /dev/null +++ b/tools/riot-fault-recovery-check.c @@ -0,0 +1,39 @@ +#include +#include +#include + +static sigjmp_buf recovery; +static volatile sig_atomic_t handled; + +static void catch_fault(int signal_number) +{ + handled = signal_number == SIGSEGV; + siglongjmp(recovery, 1); +} + +__attribute__((naked, noinline)) static void invalid_context(void) +{ + __asm__("movq $0x40000000, %rbp\n" + "movq %rbp, %rdi\n" + "movq %rbp, %rsi\n" + "xorq %rax, %rax\n" + "movq (%rax), %rax\n" + "retq\n"); +} + +__attribute__((noinline)) static void valid_frame_fault(void) +{ + __asm__ volatile("xorq %%rax, %%rax; movq (%%rax), %%rax" ::: "rax", "memory"); +} + +int main(void) +{ + signal(SIGSEGV, catch_fault); + if (!sigsetjmp(recovery, 1)) invalid_context(); + int invalid_recovered = handled; + handled = 0; + if (!sigsetjmp(recovery, 1)) valid_frame_fault(); + printf("FAULT_RECOVERY invalid_frame_and_arguments=%d valid_frame=%d pass=%d\n", + invalid_recovered, handled, invalid_recovered && handled); + return invalid_recovered && handled ? 0 : 1; +} diff --git a/tools/riot-fileport-lifetime-check.c b/tools/riot-fileport-lifetime-check.c new file mode 100644 index 0000000..fae2188 --- /dev/null +++ b/tools/riot-fileport-lifetime-check.c @@ -0,0 +1,37 @@ +/* Own backing only: every received descriptor must have independent lifetime. */ +#include +#include +#include +#include +#include +#include +extern int fileport_makeport(int,mach_port_t*); +extern int fileport_makefd(mach_port_t); +int main(void) { + char path[]="/tmp/riot-fileport-lifetime-XXXXXX";int fd=mkstemp(path); + if(fd<0)return 1;unlink(path); + const char payload[]="owned-fileport-regression"; + if(write(fd,payload,sizeof(payload))!=sizeof(payload))return 2; + mach_port_t port=MACH_PORT_NULL;if(fileport_makeport(fd,&port))return 3; + unsigned completed=0; + for(unsigned n=0;n<1000;n++) { + int copy=fileport_makefd(port);if(copy<0)return 4; + char observed[sizeof(payload)]; + if(pread(copy,observed,sizeof(observed),0)!=sizeof(observed)||memcmp(payload,observed,sizeof(payload)))return 5; + close(copy);completed++; + } + if(mach_port_deallocate(mach_task_self(),port))return 6; + for(unsigned cycle=0;cycle<100;cycle++) { + mach_port_t transient=MACH_PORT_NULL; + if(fileport_makeport(fd,&transient))return 7; + if(mach_port_mod_refs(mach_task_self(),transient,MACH_PORT_RIGHT_SEND,1))return 8; + if(mach_port_deallocate(mach_task_self(),transient))return 9; + int copy=fileport_makefd(transient);if(copy<0)return 10; + if(mach_port_deallocate(mach_task_self(),transient))return 11; + char observed[sizeof(payload)]; + if(pread(copy,observed,sizeof(observed),0)!=sizeof(observed)||memcmp(payload,observed,sizeof(payload)))return 12; + close(copy); + } + close(fd); + printf("FILEPORT_LIFETIME received_closed=%u last_sender_cycles=100 pass=1\n",completed);return 0; +} diff --git a/tools/riot-fork-integrity-check.c b/tools/riot-fork-integrity-check.c new file mode 100644 index 0000000..10009b1 --- /dev/null +++ b/tools/riot-fork-integrity-check.c @@ -0,0 +1,26 @@ +#include +#include +#include +#include +#include +extern struct elf_calls *_elfcalls; +int main(void) { + void *lib=_elfcalls->dlopen("@PROJECT_ROOT@/build/libriot-host-fork-stress.so"); + if(!lib)return 2; + void (*start)(void)=_elfcalls->dlsym(lib,"fork_stress_start"); + void (*stop)(void)=_elfcalls->dlsym(lib,"fork_stress_stop"); + void (*child)(void)=_elfcalls->dlsym(lib,"fork_stress_child"); + if(!start||!stop||!child)return 3; + start();int passed=0; + for(int i=0;i<64;i++) { + pid_t pid=fork();if(pid<0)break; + if(!pid){child();_exit(0);} + int status=0,done=0; + for(int j=0;j<500;j++){if(waitpid(pid,&status,WNOHANG)==pid){done=1;break;}usleep(10000);} + if(!done){kill(pid,SIGKILL);waitpid(pid,&status,0);break;} + if(!WIFEXITED(status)||WEXITSTATUS(status))break; + passed++; + } + stop();printf("FORK_INTEGRITY passed=%d/64 concurrent_host_allocators=6\n",passed); + return passed==64?0:1; +} diff --git a/tools/riot-getaddrinfo-check.c b/tools/riot-getaddrinfo-check.c new file mode 100644 index 0000000..d5415f8 --- /dev/null +++ b/tools/riot-getaddrinfo-check.c @@ -0,0 +1,28 @@ +/* Time getaddrinfo from inside the guest for hosts the clients resolve at startup. */ +#include +#include +#include +#include +#include + +int main(int argc, char** argv) +{ + for (int i = 1; i < argc; ++i) { + struct addrinfo hints, *result = NULL; + memset(&hints, 0, sizeof hints); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + struct timespec start, end; + clock_gettime(CLOCK_MONOTONIC, &start); + int status = getaddrinfo(argv[i], "443", &hints, &result); + clock_gettime(CLOCK_MONOTONIC, &end); + int count = 0; + for (struct addrinfo* a = result; a; a = a->ai_next) ++count; + printf("GETADDRINFO host=%s status=%d (%s) addresses=%d seconds=%.2f\n", argv[i], status, + status ? gai_strerror(status) : "ok", count, + (end.tv_sec - start.tv_sec) + (end.tv_nsec - start.tv_nsec) / 1e9); + fflush(stdout); + if (result) freeaddrinfo(result); + } + return 0; +} diff --git a/tools/riot-host-fork-stress.c b/tools/riot-host-fork-stress.c new file mode 100644 index 0000000..9ef50cd --- /dev/null +++ b/tools/riot-host-fork-stress.c @@ -0,0 +1,36 @@ +/* Keep the parent's host malloc arena busy while guest fork refreshes RPC fds. */ +#include +#include +#include +#include +#include +static atomic_int stopping; +static pthread_t workers[6]; +static void *blocks[6]; +static void *allocate(void *arg) { + void **block=arg; + while (!atomic_load(&stopping)) { + *block=realloc(*block,8*1024*1024);if(!*block)abort(); + memset(*block,42,8*1024*1024); + *block=realloc(*block,4*1024*1024);if(!*block)abort(); + } + return NULL; +} +void fork_stress_start(void) { + /* Allocate in the calling thread's arena, also used by the RPC bitmap. + * Reallocating these blocks from workers exercises that inherited lock. */ + mallopt(M_MMAP_THRESHOLD,32*1024*1024); + atomic_store(&stopping,0); + for(int i=0;i<6;i++) { + blocks[i]=malloc(4*1024*1024);if(!blocks[i])abort(); + if(pthread_create(&workers[i],NULL,allocate,&blocks[i]))abort(); + } +} +void fork_stress_stop(void) { + atomic_store(&stopping,1); + for(int i=0;i<6;i++){pthread_join(workers[i],NULL);free(blocks[i]);} +} +void fork_stress_child(void) { + void *p=malloc(1024*1024);if(!p)_Exit(2); + p=realloc(p,2*1024*1024);if(!p)_Exit(3);free(p); +} diff --git a/tools/riot-host-message-check.cpp b/tools/riot-host-message-check.cpp new file mode 100644 index 0000000..424cff6 --- /dev/null +++ b/tools/riot-host-message-check.cpp @@ -0,0 +1,68 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +using DarlingServer::Message; +static bool same(const ucred& a,const ucred& b) { + return a.pid==b.pid&&a.uid==b.uid&&a.gid==b.gid; +} +static double benchmark(unsigned count) { + auto start=std::chrono::steady_clock::now(); + for(unsigned i=0;i(std::chrono::steady_clock::now()-start).count()/count; +} +int main() { + const ucred expected={getpid(),getuid(),getgid()}; + double live=benchmark(100000); + Message::initializeServerCredentials(); + double cached=benchmark(100000); + std::atomic valid=true; + std::vector workers; + for(unsigned n=0;n<4;++n) workers.emplace_back([&]{ + for(unsigned i=0;i<10000;++i) { + Message message(1,0);ucred observed; + if(!message.copyCredentialsOut(observed)||!same(observed,expected))valid=false; + } + }); + for(auto& worker:workers)worker.join(); + Message custom(1,0);ucred changed={expected.pid+1,expected.uid+1,expected.gid+1},observed; + custom.copyCredentialsIn(changed); + bool overrides=custom.copyCredentialsOut(observed)&&same(observed,changed); + custom.setPID(expected.pid);custom.setUID(expected.uid);custom.setGID(expected.gid); + overrides=overrides&&custom.copyCredentialsOut(observed)&&same(observed,expected); + // Fork after initialization: the child must never reuse its parent's PID. + pid_t child=fork(); + if(child==0) { + Message message(1,0);ucred credentials; + const ucred current={getpid(),getuid(),getgid()}; + _exit(message.copyCredentialsOut(credentials)&&same(credentials,current)?0:1); + } + int status=0;bool forkPass=child>0&&waitpid(child,&status,0)==child&&WIFEXITED(status)&&WEXITSTATUS(status)==0; + int sockets[2]; + if(socketpair(AF_UNIX,SOCK_SEQPACKET,0,sockets)!=0)return 2; + int enabled=1; + if(setsockopt(sockets[1],SOL_SOCKET,SO_PASSCRED,&enabled,sizeof(enabled))!=0)return 3; + unsigned transferred=0; + for(unsigned i=0;i<200;++i) { + int pipeFDs[2];if(pipe(pipeFDs))return 4; + char pattern=42;if(write(pipeFDs[1],&pattern,1)!=1)return 5;close(pipeFDs[1]); + Message outgoing(1,0);outgoing.pushDescriptor(pipeFDs[0]);outgoing.data()[0]=7; + outgoing.rawHeader().msg_name=nullptr;outgoing.rawHeader().msg_namelen=0; + if(sendmsg(sockets[0],&outgoing.rawHeader(),0)!=1)return 6; + Message incoming(1,1); + if(recvmsg(sockets[1],&incoming.rawHeader(),0)!=1||incoming.rawHeader().msg_flags&MSG_CTRUNC)return 7; + if(!incoming.copyCredentialsOut(observed)||!same(observed,expected))return 8; + int received=incoming.extractDescriptorAtIndex(0);char value=0; + if(received<0||read(received,&value,1)!=1||value!=pattern||incoming.data()[0]!=7)return 9; + close(received);++transferred; + } + close(sockets[0]);close(sockets[1]); + bool pass=valid&&overrides&&forkPass&&transferred==200; + printf("HOST_MESSAGE pass=%d concurrent=40000 overrides=%d fork_identity=%d transferred=%u live_ns=%.1f cached_ns=%.1f\n",pass,overrides,forkPass,transferred,live,cached); + return pass?0:1; +} diff --git a/tools/riot-iosurface-import-cache-check.c b/tools/riot-iosurface-import-cache-check.c new file mode 100644 index 0000000..91a9465 --- /dev/null +++ b/tools/riot-iosurface-import-cache-check.c @@ -0,0 +1,62 @@ +#include +#include +#include +#include +#include +#include +#include +#include +struct ConcurrentImport {mach_port_t port;IOSurfaceRef expected;}; +static void *lookupConcurrently(void *arg) { + struct ConcurrentImport *test=arg; + for(unsigned i=0;i<100;i++) { + IOSurfaceRef surface=IOSurfaceLookupFromMachPort(test->port); + if(surface!=test->expected)return (void*)1; + CFRelease(surface); + } + return NULL; +} +extern long __lolsys_syscall(long,long,long,long,long,long,int); +static int descriptorCount(void) { + int fd=__lolsys_syscall((long)"/proc/self/fd",0,0,0,0,0,2);if(fd<0)return -1; + char data[8192];int count=0;long n; + while((n=__lolsys_syscall(fd,(long)data,sizeof(data),0,0,0,217))>0) { + for(long at=0;atn){count=-1000;break;} + const char*name=data+at+19;if(name[0]!='.')count++;at+=length; + } + } + __lolsys_syscall(fd,0,0,0,0,0,3);return count; +} +int main(void) { + int dims[]={8,8,4,32};const void *keys[]={kIOSurfaceWidth,kIOSurfaceHeight,kIOSurfaceBytesPerElement,kIOSurfaceBytesPerRow}; + const void *values[4];for(int i=0;i<4;i++)values[i]=CFNumberCreate(0,kCFNumberIntType,&dims[i]); + CFDictionaryRef options=CFDictionaryCreate(0,keys,values,4,&kCFTypeDictionaryKeyCallBacks,&kCFTypeDictionaryValueCallBacks); + IOSurfaceID firstID=0,ids[100];int before=descriptorCount();assert(before>=0); + for(int n=0;n<100;n++) { + IOSurfaceRef original=IOSurfaceCreate(options);assert(original); + memset(IOSurfaceGetBaseAddress(original),n,256); + IOSurfaceID sid=IOSurfaceGetID(original);ids[n]=sid;if(!n)firstID=sid;mach_port_t port=IOSurfaceCreateMachPort(original);assert(port); + IOSurfaceRef same=IOSurfaceLookup(sid);assert(same==original);CFRelease(same); + CFRelease(original); + assert(!IOSurfaceLookup(sid)); + IOSurfaceRef imported=IOSurfaceLookupFromMachPort(port);assert(imported && IOSurfaceGetID(imported)==sid); + IOSurfaceRef repeated=IOSurfaceLookupFromMachPort(port);assert(repeated==imported); + CFRelease(repeated); + if(!n) { + struct ConcurrentImport test={port,imported};pthread_t threads[4]; + for(unsigned i=0;i<4;i++)assert(!pthread_create(&threads[i],NULL,lookupConcurrently,&test)); + for(unsigned i=0;i<4;i++){void *result;assert(!pthread_join(threads[i],&result)&&!result);} + } + unsigned char *bytes=IOSurfaceGetBaseAddress(imported);assert(bytes);for(int i=0;i<256;i++)assert(bytes[i]==(unsigned char)n); + CFRelease(imported);assert(mach_port_deallocate(mach_task_self(),port)==KERN_SUCCESS); + assert(!IOSurfaceLookupFromMachPort(port)); + } + CFRelease(options);for(int i=0;i<4;i++)CFRelease(values[i]); + int after=descriptorCount();assert(after>=0 && after-before<=10); + printf("IMPORT_CACHE fd_growth=%d bounded=1 aliases_shared=1 concurrent_lookups=400 iterations=100 pass=1\n",after-before); + printf("SURFACE_ID first=%u\n",firstID); + printf("SURFACE_IDS");for(int n=0;n<100;n++)printf(" %u",ids[n]);putchar('\n'); + printf("SURFACE_LIFETIME iterations=100 released_id_absent=1 port_survives_release=1 pixels=pass invalid_port_rejected=1\n");return 0; +} diff --git a/tools/riot-kqueue-dispatch2-check.c b/tools/riot-kqueue-dispatch2-check.c new file mode 100644 index 0000000..f670d25 --- /dev/null +++ b/tools/riot-kqueue-dispatch2-check.c @@ -0,0 +1,27 @@ +#include +#include +#include +#include +#include +#include +int main(void) { + int kq=kqueue(); + struct timespec immediate={0,0}; + for (unsigned cycle=0;cycle<1000;cycle++) { + int fd[2]; if(socketpair(AF_UNIX,SOCK_STREAM,0,fd)) return 1; + struct kevent64_s change={.ident=fd[0],.filter=EVFILT_WRITE, + .flags=EV_ADD|EV_DISPATCH2,.udata=cycle+1},event; + if(kevent64(kq,&change,1,NULL,0,0,&immediate)) return 2; + if(kevent64(kq,NULL,0,&event,1,0,&immediate)!=1 || event.udata!=cycle+1) return 3; + change.flags=EV_DELETE|EV_UDATA_SPECIFIC|EV_RECEIPT; + if(kevent64(kq,&change,1,&event,1,0,&immediate)!=1 || + !(event.flags&EV_ERROR) || event.data!=EINPROGRESS) { + printf("DISPATCH2 deferred_delete=fail cycle=%u error=%lld\n",cycle,event.data);return 4; + } + change.flags=EV_ENABLE|EV_UDATA_SPECIFIC|EV_RECEIPT; + if(kevent64(kq,&change,1,&event,1,0,&immediate)!=1 || event.data!=0) return 5; + if(kevent64(kq,NULL,0,&event,1,0,&immediate)!=0) return 6; + close(fd[0]);close(fd[1]); + } + close(kq);puts("DISPATCH2 deferred_delete=pass acknowledgement=pass stale_delivery=none cycles=1000");return 0; +} diff --git a/tools/riot-kqueue-lifetime-check.c b/tools/riot-kqueue-lifetime-check.c new file mode 100644 index 0000000..093ad6c --- /dev/null +++ b/tools/riot-kqueue-lifetime-check.c @@ -0,0 +1,42 @@ +#include +#include +#include +#include +#include +#include +extern long __lolsys_syscall(long,long,long,long,long,long,int); +static unsigned descriptors(void) { + int fd=__lolsys_syscall((long)"/proc/self/fd",0,0,0,0,0,2); + assert(fd>=0);unsigned count=0;char data[32768];long n; + while((n=__lolsys_syscall(fd,(long)data,sizeof(data),0,0,0,217))>0){ + for(long i=0;i=20);if(data[i+19]>='0'&&data[i+19]<='9')++count;i+=size; + } + } + __lolsys_syscall(fd,0,0,0,0,0,3);return count; +} +int main(void) { + mach_port_t port;assert(mach_port_allocate(mach_task_self(),MACH_PORT_RIGHT_RECEIVE,&port)==KERN_SUCCESS); + unsigned before=descriptors(); + for(unsigned i=0;i<1000;++i){ + int q=kqueue();assert(q>=0); + struct kevent64_s event={0};event.ident=port;event.filter=EVFILT_MACHPORT;event.flags=EV_ADD; + assert(kevent64(q,&event,1,0,0,0,0)==0);assert(close(q)==0); + } + unsigned after=descriptors(); + printf("KQUEUE_LIFETIME cycles=1000 before=%u after=%u growth=%d\n",before,after,(int)after-(int)before); + assert(after<=before+2); + for(unsigned i=0;i<100;++i){ + int fds[2];assert(pipe(fds)==0);int q=kqueue();assert(q>=0); + struct kevent64_s events[3]={{0}}; + events[0].ident=fds[0];events[0].filter=EVFILT_READ;events[0].flags=EV_ADD; + events[1].ident=1;events[1].filter=EVFILT_USER;events[1].flags=EV_ADD; + events[2].ident=2;events[2].filter=EVFILT_TIMER;events[2].flags=EV_ADD;events[2].data=1000; + assert(kevent64(q,events,3,0,0,0,0)==0);assert(close(q)==0); + char byte='x',got=0;assert(write(fds[1],&byte,1)==1);assert(read(fds[0],&got,1)==1&&got==byte); + assert(close(fds[0])==0&&close(fds[1])==0); + } + unsigned mixed=descriptors(); + printf("KQUEUE_FILTER_LIFETIME cycles=100 after=%u growth=%d caller_pipe_preserved=1\n",mixed,(int)mixed-(int)after); + mach_port_destroy(mach_task_self(),port);return mixed<=after+2?0:1; +} diff --git a/tools/riot-kqueue-stack-check.c b/tools/riot-kqueue-stack-check.c new file mode 100644 index 0000000..7b46a33 --- /dev/null +++ b/tools/riot-kqueue-stack-check.c @@ -0,0 +1,31 @@ +#define PRIVATE 1 +#include +#include +#include +#include +#include +#include +static void* check(void* unused) { + (void)unused; + struct kevent_qos_s *changes=calloc(512,sizeof(*changes)); + struct kevent_qos_s *events=calloc(512,sizeof(*events)); + assert(changes && events); + int q=kqueue();assert(q>=0); + for(unsigned i=0;i<512;i++) { + changes[i].ident=i+1;changes[i].filter=EVFILT_USER; + changes[i].flags=EV_ADD|EV_RECEIPT; + } + int n=kevent_qos(q,changes,512,events,512,0,0,KEVENT_FLAG_IMMEDIATE); + printf("KQUEUE_SMALL_STACK stack_bytes=65536 changes=512 receipts=%d\n",n); + assert(n==512); + for(int i=0;i +#include +#include +#include +#include + +static mach_timebase_info_data_t scale; +static double milliseconds(uint64_t ticks) { + return (double)ticks * scale.numer / scale.denom / 1000000.0; +} +int main(void) { + mach_timebase_info(&scale); + int queue=kqueue(); + if(queue<0)return 1; + struct timespec immediate={0,0},limit={1,0}; + struct kevent64_s change={.ident=1,.filter=EVFILT_TIMER, + .flags=EV_ADD|EV_ENABLE,.fflags=NOTE_NSECONDS,.data=16666667},event; + if(kevent64(queue,&change,1,NULL,0,0,&immediate))return 2; + uint64_t start=mach_absolute_time();unsigned ticks=0; + while(ticks<60) { + if(kevent64(queue,NULL,0,&event,1,0,&limit)!=1 || + event.filter!=EVFILT_TIMER || event.data<=0)return 3; + ticks+=(unsigned)event.data; + } + double elapsed=milliseconds(mach_absolute_time()-start); + change.flags=EV_DELETE; + if(kevent64(queue,&change,1,NULL,0,0,&immediate))return 4; + start=mach_absolute_time(); + change.flags=EV_ADD|EV_ENABLE|EV_ONESHOT; + change.fflags=NOTE_MACHTIME|NOTE_ABSOLUTE; + change.data=start+20000000ull*scale.denom/scale.numer; + if(kevent64(queue,&change,1,&event,1,0,&limit)!=1 || event.data!=1)return 5; + double deadline=milliseconds(mach_absolute_time()-start); + int stale=kevent64(queue,NULL,0,&event,1,0,&immediate); + close(queue); + int pass=elapsed>=980 && elapsed<1100 && deadline>=19 && deadline<100 && stale==0; + printf("KQUEUE_TIMER ticks=%u elapsed_ms=%.2f absolute_ms=%.2f stale=%d pass=%d\n", + ticks,elapsed,deadline,stale,pass); + return pass?0:6; +} diff --git a/tools/riot-local-mutex-check.c b/tools/riot-local-mutex-check.c new file mode 100644 index 0000000..4e15baf --- /dev/null +++ b/tools/riot-local-mutex-check.c @@ -0,0 +1,113 @@ +/* Check of in-process first-fit pthread mutexes (libsystem_kernel psynch_mutexwait.c): mutual exclusion under heavy + * contention, many mutexes, recursive and error-checking types, condition variables whose mutex has waiters, timed + * waits, a fair-share mutex (stays on the server) next to them, fork, and the cost of a contended hand-off. */ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define THREADS 8 +static pthread_mutex_t big = PTHREAD_MUTEX_INITIALIZER, many[64], recursive, fair; +static volatile long inside, violations, counter, many_counter[64], rec_counter, fair_counter; +static pthread_mutex_t qlock = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t qcond = PTHREAD_COND_INITIALIZER, qspace = PTHREAD_COND_INITIALIZER; +static int queue_len; static long produced, consumed; static int done_producing; + +static void* hammer(void* arg) { + unsigned seed = (unsigned)(uintptr_t)arg * 2654435761u; + for (int i = 0; i < 60000; ++i) { + pthread_mutex_lock(&big); + if (__atomic_add_fetch(&inside, 1, __ATOMIC_SEQ_CST) != 1) __atomic_add_fetch(&violations, 1, __ATOMIC_SEQ_CST); + counter++; + if ((i & 255) == 0) sched_yield(); + __atomic_sub_fetch(&inside, 1, __ATOMIC_SEQ_CST); + pthread_mutex_unlock(&big); + seed = seed * 1103515245u + 12345u; + int k = (seed >> 16) & 63; + pthread_mutex_lock(&many[k]); many_counter[k]++; pthread_mutex_unlock(&many[k]); + if ((i & 7) == 0) { + pthread_mutex_lock(&recursive); pthread_mutex_lock(&recursive); rec_counter++; pthread_mutex_unlock(&recursive); pthread_mutex_unlock(&recursive); + pthread_mutex_lock(&fair); fair_counter++; pthread_mutex_unlock(&fair); + } + } + return 0; +} +static void* producer(void* arg) { + for (int i = 0; i < 20000; ++i) { + pthread_mutex_lock(&qlock); + while (queue_len >= 8) pthread_cond_wait(&qspace, &qlock); + queue_len++; produced++; + pthread_cond_signal(&qcond); + pthread_mutex_unlock(&qlock); + } + return 0; +} +static void* consumer(void* arg) { + for (;;) { + pthread_mutex_lock(&qlock); + while (queue_len == 0 && !done_producing) { + struct timeval tv; gettimeofday(&tv, 0); + struct timespec ts = { tv.tv_sec, (tv.tv_usec + 20000) * 1000 }; + if (ts.tv_nsec >= 1000000000) { ts.tv_sec++; ts.tv_nsec -= 1000000000; } + pthread_cond_timedwait(&qcond, &qlock, &ts); + } + if (queue_len == 0 && done_producing) { pthread_mutex_unlock(&qlock); return 0; } + queue_len--; consumed++; + pthread_cond_signal(&qspace); + pthread_mutex_unlock(&qlock); + } +} +static pthread_mutex_t ping_lock = PTHREAD_MUTEX_INITIALIZER; +static volatile int ping_turn, ping_stop; +static void* pinger(void* arg) { + int me = (int)(uintptr_t)arg; + while (!ping_stop) { pthread_mutex_lock(&ping_lock); if (ping_turn == me) ping_turn = !me; pthread_mutex_unlock(&ping_lock); } + return 0; +} +int main(void) { + int bad = 0; pthread_t t[THREADS + 8]; + for (int i = 0; i < 64; ++i) pthread_mutex_init(&many[i], 0); + pthread_mutexattr_t a; pthread_mutexattr_init(&a); pthread_mutexattr_settype(&a, PTHREAD_MUTEX_RECURSIVE); pthread_mutex_init(&recursive, &a); + pthread_mutexattr_t f; pthread_mutexattr_init(&f); pthread_mutexattr_setpolicy_np(&f, PTHREAD_MUTEX_POLICY_FAIRSHARE_NP); pthread_mutex_init(&fair, &f); + + uint64_t t0 = mach_absolute_time(); + for (long i = 0; i < THREADS; ++i) pthread_create(&t[i], 0, hammer, (void*)(i + 1)); + for (int i = 0; i < THREADS; ++i) pthread_join(t[i], 0); + uint64_t t1 = mach_absolute_time(); + long many_total = 0; for (int i = 0; i < 64; ++i) many_total += many_counter[i]; + printf("EXCLUSION: %d threads x 60000, counter %ld (expected %d), violations %ld, 64 small mutexes %ld, recursive %ld, fair-share %ld; %.2f s\n", + THREADS, counter, THREADS * 60000, violations, many_total, rec_counter, fair_counter, (t1 - t0) / 1e9); + if (counter != THREADS * 60000 || violations || many_total != THREADS * 60000 || rec_counter != THREADS * 7500 || fair_counter != THREADS * 7500) bad |= 1; + + for (long i = 0; i < 3; ++i) pthread_create(&t[i], 0, producer, 0); + for (long i = 3; i < 7; ++i) pthread_create(&t[i], 0, consumer, 0); + for (int i = 0; i < 3; ++i) pthread_join(t[i], 0); + pthread_mutex_lock(&qlock); done_producing = 1; pthread_cond_broadcast(&qcond); pthread_mutex_unlock(&qlock); + for (int i = 3; i < 7; ++i) pthread_join(t[i], 0); + printf("CONDVAR: produced %ld consumed %ld (expected 60000 each), left %d\n", produced, consumed, queue_len); + if (produced != 60000 || consumed != 60000 || queue_len != 0) bad |= 2; + + pthread_mutex_t e; pthread_mutexattr_t ea; pthread_mutexattr_init(&ea); pthread_mutexattr_settype(&ea, PTHREAD_MUTEX_ERRORCHECK); pthread_mutex_init(&e, &ea); + pthread_mutex_lock(&e); int r1 = pthread_mutex_lock(&e), r2 = pthread_mutex_trylock(&big); pthread_mutex_unlock(&big); pthread_mutex_unlock(&e); + printf("TYPES: errorcheck relock -> %d (expected %d), trylock free mutex -> %d\n", r1, EDEADLK, r2); + if (r1 != EDEADLK || r2 != 0) bad |= 4; + + pthread_mutex_lock(&big); /* fork while another thread is queued on the mutex */ + pthread_t w; pthread_create(&w, 0, hammer, (void*)99); usleep(50000); + pid_t pid = fork(); + if (pid == 0) { pthread_mutex_unlock(&big); pthread_mutex_lock(&big); pthread_mutex_unlock(&big); _exit(7); } + int status = 0; waitpid(pid, &status, 0); + pthread_mutex_unlock(&big); pthread_join(w, 0); + printf("FORK: child used the contended mutex and exited with %d (expected 7)\n", WIFEXITED(status) ? WEXITSTATUS(status) : -1); + if (!WIFEXITED(status) || WEXITSTATUS(status) != 7) bad |= 8; + + pthread_t p0, p1; pthread_create(&p0, 0, pinger, (void*)0); pthread_create(&p1, 0, pinger, (void*)1); + t0 = mach_absolute_time(); sleep(2); ping_stop = 1; pthread_join(p0, 0); pthread_join(p1, 0); t1 = mach_absolute_time(); + printf("RESULT: %s (%d)\n", bad ? "FAILED" : "ok", bad); + return bad; +} diff --git a/tools/riot-local-region-check.c b/tools/riot-local-region-check.c new file mode 100644 index 0000000..2935686 --- /dev/null +++ b/tools/riot-local-region-check.c @@ -0,0 +1,105 @@ +/* Check that mach_vm_region_recurse answered in the process is identical to darlingserver's answer, over the whole + * address space and at addresses inside gaps, and how long each takes. */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +typedef kern_return_t (*region_fn)(vm_map_t, mach_vm_address_t*, mach_vm_size_t*, natural_t*, vm_region_recurse_info_t, mach_msg_type_number_t*); +static region_fn server; +static unsigned long mismatches, probes; +static int compare(mach_vm_address_t addr, mach_vm_address_t *next) { + mach_vm_address_t a1 = addr, a2 = addr; + mach_vm_size_t s1 = 123, s2 = 123; + natural_t d1 = 7, d2 = 7; + struct { uint32_t before; vm_region_submap_short_info_data_64_t info; uint32_t after; } i1, i2; + memset(&i1, 0x5a, sizeof(i1)); memset(&i2, 0x5a, sizeof(i2)); + mach_msg_type_number_t c1 = VM_REGION_SUBMAP_SHORT_INFO_COUNT_64, c2 = c1; + kern_return_t k1 = mach_vm_region_recurse(mach_task_self(), &a1, &s1, &d1, (vm_region_recurse_info_t)&i1.info, &c1); + kern_return_t k2 = server(mach_task_self(), &a2, &s2, &d2, (vm_region_recurse_info_t)&i2.info, &c2); + ++probes; + if (k1 != k2 || a1 != a2 || s1 != s2 || d1 != d2 || c1 != c2 || memcmp(&i1, &i2, sizeof(i1)) || + i1.before != 0x5a5a5a5a || i1.after != 0x5a5a5a5a) { + if (++mismatches <= 5) printf("MISMATCH at %llx: local kr=%d %llx+%llx depth=%u | server kr=%d %llx+%llx depth=%u\n", + addr, k1, a1, s1, d1, k2, a2, s2, d2); + } + if (mismatches && probes <= 2) { + for (size_t b = 0; b < sizeof(i1); ++b) + if (((unsigned char*)&i1)[b] != ((unsigned char*)&i2)[b]) + printf("BYTE %lu: %02x vs %02x\n", b, ((unsigned char*)&i1)[b], ((unsigned char*)&i2)[b]); + } + /* every other count up to the long structure (19 words), with guard words around a 24-word buffer */ + for (mach_msg_type_number_t cnt = 1; cnt <= 19; cnt += (cnt == 11 ? 2 : 1)) { + uint32_t w1[24], w2[24]; memset(w1, 0x5a, sizeof(w1)); memset(w2, 0x5a, sizeof(w2)); + mach_vm_address_t b1 = addr, b2 = addr; mach_vm_size_t z1 = 123, z2 = 123; natural_t e1 = 7, e2 = 7; + mach_msg_type_number_t n1 = cnt, n2 = cnt; + kern_return_t r1 = mach_vm_region_recurse(mach_task_self(), &b1, &z1, &e1, (vm_region_recurse_info_t)(w1 + 2), &n1); + kern_return_t r2 = server(mach_task_self(), &b2, &z2, &e2, (vm_region_recurse_info_t)(w2 + 2), &n2); + ++probes; + if (r1 != r2 || b1 != b2 || z1 != z2 || e1 != e2 || n1 != n2 || memcmp(w1, w2, sizeof(w1))) { + if (++mismatches <= 5) printf("MISMATCH count %u at %llx: local kr=%d %llx+%llx cnt=%u | server kr=%d %llx+%llx cnt=%u\n", + cnt, addr, r1, b1, z1, n1, r2, b2, z2, n2); + } + } + if (next) *next = a1 + s1; + return k1; +} +int main(void) { + server = (region_fn) dlsym(RTLD_DEFAULT, "__darling_rpc_mach_vm_region_recurse"); + if (!server) { printf("RESULT: no server stub exported\n"); return 1; } + /* Warm both paths before scanning: lazy binding may itself add mappings. */ + compare((mach_vm_address_t)&server, NULL); + unsigned long regions = 0; + mach_vm_address_t addr = 0, next; + while (compare(addr, &next) == KERN_SUCCESS) { + ++regions; + compare(next - 1, NULL); + compare(next, NULL); + if (next <= addr) { ++mismatches; break; } + addr = next; + } + compare(UINT64_MAX, NULL); + size_t page = getpagesize(); + char *mapping = mmap(NULL, 4 * page, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0); + if (mapping == MAP_FAILED) return 2; + compare((mach_vm_address_t)mapping, NULL); + if (mprotect(mapping + page, page, PROT_READ) != 0) return 2; + compare((mach_vm_address_t)(mapping + page), NULL); + if (munmap(mapping + 2 * page, page) != 0) return 2; + compare((mach_vm_address_t)(mapping + 2 * page), NULL); + compare((mach_vm_address_t)(mapping + 3 * page), NULL); + /* A child-only protection change catches a cached maps fd naming the parent's address space. */ + pid_t child = fork(); + if (child < 0) return 2; + if (!child) { + mismatches = 0; + if (mprotect(mapping, page, PROT_NONE) != 0) _exit(2); + compare((mach_vm_address_t)mapping, NULL); + _exit(mismatches != 0); + } + int status; + if (waitpid(child, &status, 0) != child || !WIFEXITED(status) || WEXITSTATUS(status)) ++mismatches; + munmap(mapping, 2 * page); munmap(mapping + 3 * page, page); + FILE *file = tmpfile(); + if (!file || ftruncate(fileno(file), 4 * page)) return 2; + mapping = mmap(NULL, page, PROT_READ | PROT_WRITE, MAP_SHARED, fileno(file), page); + if (mapping == MAP_FAILED) return 2; + compare((mach_vm_address_t)mapping, NULL); + munmap(mapping, page); fclose(file); + printf("COMPARE: %lu regions, %lu queries, %lu mismatches (boundaries, errors, protection changes, gaps, shared file offset, fork)\n", regions, probes, mismatches); + mach_vm_address_t a; mach_vm_size_t s; natural_t d; vm_region_submap_short_info_data_64_t info; mach_msg_type_number_t c; + uint64_t t0 = mach_absolute_time(); + for (int i = 0; i < 20000; ++i) { a = (mach_vm_address_t)&server; d = 0; c = VM_REGION_SUBMAP_SHORT_INFO_COUNT_64; mach_vm_region_recurse(mach_task_self(), &a, &s, &d, (vm_region_recurse_info_t)&info, &c); } + uint64_t t1 = mach_absolute_time(); + for (int i = 0; i < 20000; ++i) { a = (mach_vm_address_t)&server; d = 0; c = VM_REGION_SUBMAP_SHORT_INFO_COUNT_64; server(mach_task_self(), &a, &s, &d, (vm_region_recurse_info_t)&info, &c); } + uint64_t t2 = mach_absolute_time(); + printf("BENCH: mach_vm_region_recurse %.2f us, server stub %.2f us per call\n", (t1 - t0) / 2e4 / 1e3, (t2 - t1) / 2e4 / 1e3); + printf("RESULT: %s\n", mismatches ? "FAILED" : "ok"); + return mismatches != 0; +} diff --git a/tools/riot-local-semaphore-check.c b/tools/riot-local-semaphore-check.c new file mode 100644 index 0000000..9992428 --- /dev/null +++ b/tools/riot-local-semaphore-check.c @@ -0,0 +1,90 @@ +/* Check of process-local Mach semaphores (libsystem_kernel mach_traps.c): counting under contention, timeouts, + * destroy with waiters, signal_all, dispatch semaphores, and the cost of a thread-to-thread hand-off. */ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static semaphore_t items, ping, pong, doomed, all; +static long consumed; +static int woke_all; +static kern_return_t doomed_result = -1; + +static void* consumer(void* arg) { for (int i = 0; i < 20000; ++i) { if (semaphore_wait(items) != KERN_SUCCESS) return (void*)1; __atomic_add_fetch(&consumed, 1, __ATOMIC_RELAXED); } return 0; } +static void* producer(void* arg) { for (int i = 0; i < 20000; ++i) if (semaphore_signal(items) != KERN_SUCCESS) return (void*)1; return 0; } +static void* ponger(void* arg) { for (int i = 0; i < 20000; ++i) { semaphore_wait(ping); semaphore_signal(pong); } return 0; } +static void* doomed_waiter(void* arg) { doomed_result = semaphore_wait(doomed); return 0; } +static void* all_waiter(void* arg) { if (semaphore_wait(all) == KERN_SUCCESS) __atomic_add_fetch(&woke_all, 1, __ATOMIC_RELAXED); return 0; } + +int main(void) { + int bad = 0; + task_t self = mach_task_self(); + if (semaphore_create(self, &items, SYNC_POLICY_FIFO, 0) || semaphore_create(self, &ping, SYNC_POLICY_FIFO, 0) || + semaphore_create(self, &pong, SYNC_POLICY_FIFO, 0) || semaphore_create(self, &doomed, SYNC_POLICY_FIFO, 0) || + semaphore_create(self, &all, SYNC_POLICY_FIFO, 0)) return 1; + + pthread_t t[8]; + for (int i = 0; i < 4; ++i) pthread_create(&t[i], 0, consumer, 0); + for (int i = 4; i < 8; ++i) pthread_create(&t[i], 0, producer, 0); + for (int i = 0; i < 8; ++i) { void* r; pthread_join(t[i], &r); if (r) bad |= 1; } + printf("COUNT: 4 producers x 20000, consumed %ld (expected 80000)\n", consumed); + if (consumed != 80000) bad |= 2; + mach_timespec_t zero = {0, 1000000}; + kern_return_t kr = semaphore_timedwait(items, zero); + printf("LEFTOVER: timedwait on the drained semaphore -> %d (expected %d)\n", kr, KERN_OPERATION_TIMED_OUT); + if (kr != KERN_OPERATION_TIMED_OUT) bad |= 4; + + uint64_t a = mach_absolute_time(); + mach_timespec_t wait = {0, 50000000}; + kr = semaphore_timedwait(items, wait); + uint64_t b = mach_absolute_time(); + printf("TIMEOUT: 50 ms timedwait -> %d after %.1f ms\n", kr, (b - a) / 1e6); + if (kr != KERN_OPERATION_TIMED_OUT || b - a < 49000000ull || b - a > 80000000ull) bad |= 8; + semaphore_signal(items); + mach_timespec_t longwait = {5, 0}; + kr = semaphore_timedwait(items, longwait); + printf("TIMED SUCCESS: %d\n", kr); + if (kr != KERN_SUCCESS) bad |= 16; + + pthread_t p; + pthread_create(&p, 0, ponger, 0); + a = mach_absolute_time(); + for (int i = 0; i < 20000; ++i) { semaphore_signal(ping); semaphore_wait(pong); } + b = mach_absolute_time(); + pthread_join(p, 0); + printf("HANDOFF: signal + wait round trip between two threads %.1f us\n", (b - a) / 20000.0 / 1000.0); + a = mach_absolute_time(); + for (int i = 0; i < 200000; ++i) { semaphore_signal(ping); semaphore_wait(ping); } + b = mach_absolute_time(); + printf("UNCONTENDED: signal + wait on one thread %.0f ns\n", (b - a) / 200000.0); + + pthread_t d; + pthread_create(&d, 0, doomed_waiter, 0); + usleep(100000); + semaphore_destroy(self, doomed); + pthread_join(d, 0); + printf("DESTROY: waiter returned %d (expected %d)\n", doomed_result, KERN_TERMINATED); + if (doomed_result != KERN_TERMINATED) bad |= 32; + + pthread_t w[3]; + for (int i = 0; i < 3; ++i) pthread_create(&w[i], 0, all_waiter, 0); + usleep(100000); + semaphore_signal_all(all); + for (int i = 0; i < 3; ++i) pthread_join(w[i], 0); + kr = semaphore_timedwait(all, zero); + printf("SIGNAL_ALL: woke %d of 3, nothing left over: %d\n", woke_all, kr == KERN_OPERATION_TIMED_OUT); + if (woke_all != 3 || kr != KERN_OPERATION_TIMED_OUT) bad |= 64; + + dispatch_semaphore_t ds = dispatch_semaphore_create(0); + __block int ran = 0; + for (int i = 0; i < 1000; ++i) dispatch_async(dispatch_get_global_queue(0, 0), ^{ __atomic_add_fetch(&ran, 1, __ATOMIC_RELAXED); dispatch_semaphore_signal(ds); }); + for (int i = 0; i < 1000; ++i) if (dispatch_semaphore_wait(ds, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC))) { bad |= 128; break; } + printf("DISPATCH: 1000 blocks signalled a dispatch semaphore, ran %d\n", ran); + printf("RESULT: %s (%d)\n", bad ? "FAILED" : "ok", bad); + return bad; +} diff --git a/tools/riot-mach-exception-state-check.c b/tools/riot-mach-exception-state-check.c new file mode 100644 index 0000000..76c58ab --- /dev/null +++ b/tools/riot-mach-exception-state-check.c @@ -0,0 +1,97 @@ +#include +#include +#include +#include +#include +#include +#include +#include "mach_excServer.h" + +static mach_port_t exception_port; +static int handled; +static int server_ok = 1; +extern void fixture_fault_site(void); +extern void fixture_fault_resume(void); + +__attribute__((naked, noinline)) static uint64_t fault(void) { + __asm__("xorq %rax, %rax\n" + ".globl _fixture_fault_site\n" + "_fixture_fault_site:\n" + "movq (%rax), %rax\n" + ".globl _fixture_fault_resume\n" + "_fixture_fault_resume:\n" + "retq\n"); +} + +static kern_return_t recover(exception_type_t exception, int *flavor, + thread_state_t old_state, mach_msg_type_number_t old_count, + thread_state_t new_state, mach_msg_type_number_t *new_count) { + if (exception != EXC_BAD_ACCESS || *flavor != x86_THREAD_STATE64 || + old_count != x86_THREAD_STATE64_COUNT || *new_count < old_count) + return KERN_FAILURE; + x86_thread_state64_t *state = (x86_thread_state64_t *)old_state; + if (state->__rip != (uintptr_t)fixture_fault_site) return KERN_FAILURE; + memcpy(new_state, old_state, old_count * sizeof(natural_t)); + state = (x86_thread_state64_t *)new_state; + state->__rip = (uintptr_t)fixture_fault_resume; + state->__rax = 0x12345678; + *new_count = old_count; + handled++; + return KERN_SUCCESS; +} + +kern_return_t catch_mach_exception_raise(mach_port_t port, mach_port_t thread, + mach_port_t task, exception_type_t exception, mach_exception_data_t code, + mach_msg_type_number_t count) { + return KERN_FAILURE; +} + +kern_return_t catch_mach_exception_raise_state(mach_port_t port, + exception_type_t exception, const mach_exception_data_t code, + mach_msg_type_number_t count, int *flavor, const thread_state_t old_state, + mach_msg_type_number_t old_count, thread_state_t new_state, + mach_msg_type_number_t *new_count) { + return recover(exception, flavor, old_state, old_count, new_state, new_count); +} + +kern_return_t catch_mach_exception_raise_state_identity(mach_port_t port, + mach_port_t thread, mach_port_t task, exception_type_t exception, + mach_exception_data_t code, mach_msg_type_number_t count, int *flavor, + thread_state_t old_state, mach_msg_type_number_t old_count, + thread_state_t new_state, mach_msg_type_number_t *new_count) { + kern_return_t result = recover(exception, flavor, old_state, old_count, new_state, new_count); + mach_port_deallocate(mach_task_self(), thread); + mach_port_deallocate(mach_task_self(), task); + return result; +} + +static void *serve(void *unused) { + for (int i = 0; i < 2; ++i) { + if (mach_msg_server_once(mach_exc_server, 8192, exception_port, 0) != KERN_SUCCESS) { + server_ok = 0; + break; + } + } + return NULL; +} + +int main(void) { + if (mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &exception_port) || + mach_port_insert_right(mach_task_self(), exception_port, exception_port, MACH_MSG_TYPE_MAKE_SEND)) + return 1; + pthread_t server; + if (pthread_create(&server, NULL, serve, NULL)) return 1; + if (task_set_exception_ports(mach_task_self(), EXC_MASK_BAD_ACCESS, exception_port, + EXCEPTION_STATE | MACH_EXCEPTION_CODES, x86_THREAD_STATE64)) return 1; + int state_ok = fault() == 0x12345678; + if (task_set_exception_ports(mach_task_self(), EXC_MASK_BAD_ACCESS, exception_port, + EXCEPTION_STATE_IDENTITY | MACH_EXCEPTION_CODES, x86_THREAD_STATE64)) return 1; + int identity_ok = fault() == 0x12345678; + pthread_join(server, NULL); + task_set_exception_ports(mach_task_self(), EXC_MASK_BAD_ACCESS, MACH_PORT_NULL, + EXCEPTION_DEFAULT, THREAD_STATE_NONE); + mach_port_destroy(mach_task_self(), exception_port); + printf("MACH_EXCEPTION_STATE state=%d identity=%d handled=%d server=%d\n", + state_ok, identity_ok, handled, server_ok); + return state_ok && identity_ok && handled == 2 && server_ok ? 0 : 1; +} diff --git a/tools/riot-malloc-integrity-check.c b/tools/riot-malloc-integrity-check.c new file mode 100644 index 0000000..359f4fd --- /dev/null +++ b/tools/riot-malloc-integrity-check.c @@ -0,0 +1,96 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static _Atomic unsigned failures; +static void *transferred[4][256]; +static const size_t sizes[] = {16, 64, 256, 2048, 16384, 262144}; + +static void *worker(void *argument) { + unsigned index = (unsigned)(uintptr_t)argument; + for (unsigned cycle = 0; cycle < 2000; ++cycle) { + size_t size = sizes[cycle % 6]; + unsigned char pattern = (unsigned char)(cycle + index); + unsigned char *data = calloc(1, size); + if (!data) { ++failures; break; } + for (size_t i = 0; i < size; ++i) + if (data[i]) { ++failures; break; } + if (malloc_size(data) < size) ++failures; + memset(data, pattern, size); + unsigned char *resized = realloc(data, size + 37); + if (!resized) { ++failures; free(data); break; } + for (size_t i = 0; i < size; ++i) + if (resized[i] != pattern) { ++failures; break; } + free(resized); + void *aligned = NULL; + if (posix_memalign(&aligned, 256, size) || ((uintptr_t)aligned & 255)) ++failures; + free(aligned); + } + for (unsigned i = 0; i < 256; ++i) { + unsigned char *data = malloc(4096); + transferred[index][i] = data; + if (data) memset(data, (unsigned char)index, 4096); + else ++failures; + } + return NULL; +} + +static void *cross_free(void *argument) { + unsigned index = (unsigned)(uintptr_t)argument; + for (unsigned i = 0; i < 256; ++i) { + unsigned char *data = transferred[index][i]; + if (!data) continue; + for (unsigned j = 0; j < 4096; ++j) + if (data[j] != index) { ++failures; break; } + free(data); + } + return NULL; +} + +int main(void) { + Dl_info info = {0}; + int isolated = dladdr((void *)malloc, &info) && info.dli_fname && + strstr(info.dli_fname, "riot-malloc-check-runtime") != NULL; + pthread_t threads[4]; + for (unsigned i = 0; i < 4; ++i) + if (pthread_create(&threads[i], NULL, worker, (void *)(uintptr_t)i)) return 2; + for (unsigned i = 0; i < 4; ++i) pthread_join(threads[i], NULL); + for (unsigned i = 0; i < 4; ++i) + if (pthread_create(&threads[i], NULL, cross_free, (void *)(uintptr_t)i)) return 3; + for (unsigned i = 0; i < 4; ++i) pthread_join(threads[i], NULL); + pid_t child = fork(); + if (child == 0) { + for (unsigned i = 0; i < 2000; ++i) { + unsigned char *data = calloc(1, 4096); + if (!data) _exit(7); + for (unsigned j = 0; j < 4096; ++j) if (data[j]) _exit(8); + free(data); + } + _exit(0); + } + int childStatus = 0; + if (child < 0 || waitpid(child, &childStatus, 0) != child || !WIFEXITED(childStatus) || WEXITSTATUS(childStatus)) ++failures; + mach_timebase_info_data_t timebase; + mach_timebase_info(&timebase); + uint64_t start = mach_absolute_time(); + for (unsigned i = 0; i < 100000; ++i) { + void *data = malloc(128 + (i & 255)); + if (!data) return 4; + __asm__ volatile("" : : "r"(data) : "memory"); + memset(data, (int)i, 128 + (i & 255)); + __asm__ volatile("" : : "r"(data) : "memory"); + free(data); + } + uint64_t nanoseconds = (mach_absolute_time() - start) * timebase.numer / timebase.denom; + printf("MALLOC_INTEGRITY isolated_candidate=%d threaded_cycles=8000 cross_thread_frees=1024 fork_checked=1 failures=%u ns_per_pair=%llu pass=%d\n", + isolated, (unsigned)failures, (unsigned long long)(nanoseconds / 100000), !failures); + return failures ? 1 : 0; +} diff --git a/tools/riot-metal-batch-check.mm b/tools/riot-metal-batch-check.mm new file mode 100644 index 0000000..f27a8cb --- /dev/null +++ b/tools/riot-metal-batch-check.mm @@ -0,0 +1,327 @@ +#import +#import +#include +#import +#import +#import +#import +#import +#include +#include +#include +static void verifyBatch(bool value, const char* message) { + if (!value) { fprintf(stderr, "FAIL: %s\n", message); exit(1); } +} +static void checkCopiedConstants(id device) { + auto backend = std::dynamic_pointer_cast(((MTLDeviceInternal*)device).device); + Indium::FunctionResources resources; + const unsigned values[] = {16,8}, changed[] = {32,16}; + resources.setBytes(backend,values,sizeof(values),0); + auto first=resources.buffers[0]; + resources.setBytes(backend,values,sizeof(values),0); + verifyBatch(resources.buffers[0]==first,"identical copied constants retain their address"); + resources.setBytes(backend,changed,sizeof(changed),0); + verifyBatch(resources.buffers[0].second!=first.second,"changed copied constants use immutable storage"); + verifyBatch(!memcmp((char*)first.first->contents()+first.second,values,sizeof(values)),"earlier constants survive later uploads"); + Indium::FunctionInfo info{}; + Iridium::BindingInfo binding{};binding.type=Iridium::BindingType::Buffer;binding.index=0; + info.bindings.push_back(binding); + std::vector> keep; + Indium::AddressPool pool; + auto table=Indium::makeBufferAddressTable(resources,info,nullptr,backend,keep,pool); + resources.setBytes(backend,values,sizeof(values),15); // not used by this shader + auto repeated=Indium::makeBufferAddressTable(resources,info,nullptr,backend,keep,pool,&table); + verifyBatch(repeated.buffer==table.buffer && repeated.offset==table.offset,"unused binding changes retain address table"); + auto oldAddress=*(uint64_t*)((char*)pool.chunk->contents()+table.offset); + resources.setBufferOffset(first.second,0); + auto next=Indium::makeBufferAddressTable(resources,info,nullptr,backend,keep,pool,&table); + verifyBatch(next.offset!=table.offset,"used binding change allocates a new immutable address table"); + verifyBatch(*(uint64_t*)((char*)pool.chunk->contents()+table.offset)==oldAddress,"earlier address table remains immutable"); + fprintf(stderr,"PASS: copied constants and immutable address-table reuse/invalidation\n"); +} +void checkMetalBatch(id device, id sampler) { + checkCopiedConstants(device); + MTLVertexDescriptor* vertex = [MTLVertexDescriptor vertexDescriptor]; + vertex.attributes[2].format = MTLVertexFormatFloat3; + vertex.attributes[2].bufferIndex = 4; + vertex.attributes[2].offset = 16; + vertex.layouts[4].stride = 32; + vertex.layouts[4].stepFunction = MTLVertexStepFunctionPerInstance; + vertex.layouts[4].stepRate = 2; + MTLVertexDescriptor* vertexCopy = [vertex copy]; + [vertex reset]; + verifyBatch(vertex.attributes[2].format == MTLVertexFormatInvalid && vertexCopy.attributes[2].format == MTLVertexFormatFloat3, "vertex reset/copy independence"); + auto layout = [vertexCopy asIndiumDescriptor]; + verifyBatch(layout.attributes.at(2).offset == 16 && layout.layouts.at(4).stride == 32 && layout.layouts.at(4).stepRate == 2, "vertex layout conversion"); + MTLRenderPipelineDescriptor* pipeline = [MTLRenderPipelineDescriptor new]; + pipeline.vertexDescriptor = vertexCopy; + auto pipelineDescriptor = [pipeline asIndiumDescriptor]; + verifyBatch(pipelineDescriptor.vertexDescriptor && pipelineDescriptor.vertexDescriptor->attributes.at(2).bufferIndex == 4, "pipeline receives vertex layout"); + [pipeline release]; [vertexCopy release]; + MTLScissorRect scissor = {3, 5, 7, 11}; + auto translatedScissor = MTLScissorRectToIndium(scissor); + verifyBatch(translatedScissor.x == 3 && translatedScissor.y == 5 && translatedScissor.width == 7 && translatedScissor.height == 11, "scissor binary layout"); + MTLDepthStencilDescriptor* depth = [MTLDepthStencilDescriptor new]; + verifyBatch(depth.depthCompareFunction == MTLCompareFunctionAlways && !depth.depthWriteEnabled, "depth defaults"); + MTLStencilDescriptor* stencil = [MTLStencilDescriptor new]; + stencil.stencilCompareFunction = MTLCompareFunctionEqual; + stencil.depthStencilPassOperation = MTLStencilOperationReplace; + stencil.readMask = 0x7f; stencil.writeMask = 0x3f; + depth.depthCompareFunction = MTLCompareFunctionLessEqual; depth.depthWriteEnabled = YES; + depth.frontFaceStencil = stencil; depth.backFaceStencil = stencil; + depth.label = @"depth/stencil test"; + stencil.readMask = 0; + verifyBatch(depth.frontFaceStencil.readMask == 0x7f, "stencil setter copies descriptor"); + id state = [device newDepthStencilStateWithDescriptor:depth]; + verifyBatch(state && state.device == device && ((MTLDepthStencilStateInternal*)state).state, "real depth/stencil backend state"); + [stencil release]; [depth release]; + MTLTextureDescriptor* td = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm width:16 height:8 mipmapped:NO]; + td.storageMode = MTLStorageModePrivate; td.usage = MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead; + id color = [device newTextureWithDescriptor:td]; + td.pixelFormat = device.isDepth24Stencil8PixelFormatSupported ? MTLPixelFormatDepth24Unorm_Stencil8 : MTLPixelFormatDepth32Float_Stencil8; + td.usage = MTLTextureUsageRenderTarget; + id depthTexture = [device newTextureWithDescriptor:td]; + verifyBatch(depthTexture != nil, "depth/stencil texture allocation"); + MTLRenderPassDescriptor* pass = [MTLRenderPassDescriptor renderPassDescriptor]; + verifyBatch(pass.depthAttachment && pass.stencilAttachment, "render pass supplies attachment descriptors"); + pass.colorAttachments[0].texture = color; + pass.colorAttachments[0].loadAction = MTLLoadActionClear; + pass.colorAttachments[0].storeAction = MTLStoreActionStore; + pass.colorAttachments[0].clearColor = MTLClearColorMake(0.2,0.4,0.6,1); + pass.depthAttachment.texture = depthTexture; pass.stencilAttachment.texture = depthTexture; + pass.depthAttachment.loadAction = MTLLoadActionClear; pass.depthAttachment.storeAction = MTLStoreActionStore; + pass.depthAttachment.clearDepth = 0.75; + pass.stencilAttachment.loadAction = MTLLoadActionClear; pass.stencilAttachment.storeAction = MTLStoreActionStore; + pass.stencilAttachment.clearStencil = 7; + id queue = [device newCommandQueue]; + id commands = [queue commandBuffer]; + id render = [commands renderCommandEncoderWithDescriptor:pass]; + verifyBatch(render != nil, "combined depth/stencil render encoder"); + [render setDepthStencilState:state]; [render setDepthBias:0 slopeScale:0 clamp:0]; [render setStencilReferenceValue:7]; + [render setVertexTexture:color atIndex:0]; [render setFragmentTexture:color atIndex:0]; + [render setVertexSamplerState:sampler atIndex:0]; [render setFragmentSamplerState:sampler atIndex:0]; + [render memoryBarrierWithScope:(MTLBarrierScopeTextures | MTLBarrierScopeRenderTargets) afterStages:MTLRenderStageFragment beforeStages:MTLRenderStageFragment]; + [render setDepthStencilState:nil]; [render endEncoding]; + dispatch_semaphore_t done = dispatch_semaphore_create(0); + __block unsigned callbackState = 0; + [commands addScheduledHandler:^(id buffer) { verifyBatch(buffer == commands, "scheduled buffer identity"); callbackState = 1; }]; + [commands addCompletedHandler:^(id buffer) { verifyBatch(callbackState == 1 && buffer == commands, "scheduled/completed handler order"); callbackState = 2; dispatch_semaphore_signal(done); }]; + [commands commit]; [commands waitUntilCompleted]; + verifyBatch(dispatch_semaphore_wait(done, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)) == 0 && callbackState == 2, "GPU handlers complete"); + dispatch_release(done); + id output = [device newBufferWithLength:512 options:MTLResourceStorageModeShared]; + commands = [queue commandBuffer]; + id blit = [commands blitCommandEncoder]; + [blit copyFromTexture:color sourceSlice:0 sourceLevel:0 sourceOrigin:MTLOriginMake(0,0,0) sourceSize:MTLSizeMake(16,8,1) toBuffer:output destinationOffset:0 destinationBytesPerRow:64 destinationBytesPerImage:512]; + [blit endEncoding]; [commands commit]; [commands waitUntilCompleted]; + unsigned char* pixels = (unsigned char*)output.contents; + for (unsigned i = 0; i < 512; i += 4) verifyBatch(pixels[i]==51 && pixels[i+1]==102 && pixels[i+2]==153 && pixels[i+3]==255, "GPU render clear/readback"); + commands = [queue commandBuffer]; + id compute = [commands computeCommandEncoder]; + [compute setTexture:color atIndex:0]; [compute setSamplerState:sampler atIndex:0]; + [compute setSamplerState:sampler lodMinClamp:0 lodMaxClamp:1 atIndex:1]; + [compute endEncoding]; [commands commit]; [commands waitUntilCompleted]; + [output release]; [depthTexture release]; [color release]; [queue release]; [state release]; + MTLTextureDescriptor* mipDesc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm width:16 height:8 mipmapped:YES]; + mipDesc.textureType = MTLTextureType2DArray; mipDesc.arrayLength = 2; + mipDesc.storageMode = MTLStorageModePrivate; mipDesc.usage = MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead; + id mipTexture = [device newTextureWithDescriptor:mipDesc]; + id mipView = [mipTexture newTextureViewWithPixelFormat:mipDesc.pixelFormat textureType:MTLTextureType2D levels:NSMakeRange(1,3) slices:NSMakeRange(1,1)]; + verifyBatch(mipView.width == 8 && mipView.height == 4, "mip view dimensions shrink"); + id nestedView = [mipView newTextureViewWithPixelFormat:mipDesc.pixelFormat textureType:MTLTextureType2D levels:NSMakeRange(1,1) slices:NSMakeRange(0,1)]; + verifyBatch(nestedView.width == 4 && nestedView.height == 2, "nested mip dimensions shrink"); + id sameView = [mipView newTextureViewWithPixelFormat:mipDesc.pixelFormat]; + verifyBatch(sameView.width == 8 && sameView.mipmapLevelCount == 3, "full view preserves relative mip range"); + queue = [device newCommandQueue]; commands = [queue commandBuffer]; + pass = [MTLRenderPassDescriptor renderPassDescriptor]; + pass.colorAttachments[0].texture = mipTexture; pass.colorAttachments[0].level = 1; pass.colorAttachments[0].slice = 1; + pass.colorAttachments[0].loadAction = MTLLoadActionClear; pass.colorAttachments[0].storeAction = MTLStoreActionStore; + pass.colorAttachments[0].clearColor = MTLClearColorMake(0,1,0,1); + render = [commands renderCommandEncoderWithDescriptor:pass]; [render endEncoding]; + pass.colorAttachments[0].texture = nestedView; pass.colorAttachments[0].level = 0; pass.colorAttachments[0].slice = 0; + pass.colorAttachments[0].clearColor = MTLClearColorMake(0,0,1,1); + render = [commands renderCommandEncoderWithDescriptor:pass]; [render endEncoding]; + output = [device newBufferWithLength:160 options:MTLResourceStorageModeShared]; + blit = [commands blitCommandEncoder]; + [blit copyFromTexture:mipTexture sourceSlice:1 sourceLevel:1 sourceOrigin:MTLOriginMake(0,0,0) sourceSize:MTLSizeMake(8,4,1) toBuffer:output destinationOffset:0 destinationBytesPerRow:32 destinationBytesPerImage:128]; + [blit copyFromTexture:mipTexture sourceSlice:1 sourceLevel:2 sourceOrigin:MTLOriginMake(0,0,0) sourceSize:MTLSizeMake(4,2,1) toBuffer:output destinationOffset:128 destinationBytesPerRow:16 destinationBytesPerImage:32]; + [blit endEncoding]; [commands commit]; [commands waitUntilCompleted]; + pixels = (unsigned char*)output.contents; + for (unsigned i = 0; i < 128; i += 4) verifyBatch(pixels[i]==0 && pixels[i+1]==255 && pixels[i+2]==0 && pixels[i+3]==255, "render targets selected mip and array slice"); + for (unsigned i = 128; i < 160; i += 4) verifyBatch(pixels[i]==0 && pixels[i+1]==0 && pixels[i+2]==255 && pixels[i+3]==255, "nested view targets correct underlying mip"); + [output release]; [queue release]; [sameView release]; [nestedView release]; [mipView release]; [mipTexture release]; + fprintf(stderr,"PASS: render attachment mip/slice and nested view pixel checks\n"); + fprintf(stderr, "PASS: Metal vertex layouts, depth/stencil, resource binding, render clear and handlers\n"); +} + +void checkMetalShader(id device) { + if (getenv("RCLOL_CAPTURE_ONE_SUBMIT")) { + FILE* request = fopen("/Volumes/SystemRoot@PROJECT_ROOT@/build/renderdoc/capture-request", "w"); + verifyBatch(request != nullptr, "capture request file"); fprintf(request,"0\n"); fclose(request); + } + const char* shaderPath = getenv("RIOT_METAL_SHADER"); + if (!shaderPath) return; + NSData* shader = [NSData dataWithContentsOfFile:[NSString stringWithUTF8String:shaderPath]]; + verifyBatch(shader != nil, "shader fixture available"); + dispatch_data_t data = dispatch_data_create(shader.bytes, shader.length, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), DISPATCH_DATA_DESTRUCTOR_DEFAULT); + NSError* error = nil; + fprintf(stderr, "CHECK: shader library translation\n"); + id library = [device newLibraryWithData:data error:&error]; + dispatch_release(data); + verifyBatch(library != nil && error == nil, "dispatch-data shader library"); + id vertex = [library newFunctionWithName:@"vertexShader"]; + id fragment = [library newFunctionWithName:@"fragmentShader"]; + verifyBatch(vertex && fragment, "shader functions"); + verifyBatch(vertex.functionType == MTLFunctionTypeVertex && fragment.functionType == MTLFunctionTypeFragment, "shader function types"); + MTLRenderPipelineDescriptor* desc = [MTLRenderPipelineDescriptor new]; + desc.vertexFunction = vertex; desc.fragmentFunction = fragment; + bool vertexDepth = getenv("RIOT_METAL_VERTEX_DEPTH") != nullptr; + if (vertexDepth) { + desc.vertexDescriptor = [MTLVertexDescriptor vertexDescriptor]; + desc.vertexDescriptor.attributes[7].format = MTLVertexFormatUChar4; + desc.vertexDescriptor.attributes[7].bufferIndex = 2; + desc.vertexDescriptor.layouts[2].stride = 4; + desc.vertexDescriptor.layouts[2].stepFunction = MTLVertexStepFunctionPerVertex; + } + NSUInteger colorSlot = getenv("RIOT_METAL_COLOR_SLOT") ? strtoul(getenv("RIOT_METAL_COLOR_SLOT"), nullptr, 10) : 0; + verifyBatch(colorSlot < 8, "valid test color slot"); + desc.colorAttachments[colorSlot].pixelFormat = MTLPixelFormatRGBA8Unorm; + MTLRenderPipelineReflection* reflection = nil; + id pipeline = [device newRenderPipelineStateWithDescriptor:desc options:(MTLPipelineOptionArgumentInfo | MTLPipelineOptionBufferTypeInfo) reflection:&reflection error:&error]; + fprintf(stderr,"CHECK: pipeline=%d reflection=%d vertexArgs=%lu fragmentArgs=%lu\n", pipeline != nil, reflection != nil, (unsigned long)reflection.vertexArguments.count, (unsigned long)reflection.fragmentArguments.count); + verifyBatch(pipeline && reflection && reflection.vertexArguments.count == (vertexDepth ? 3 : 2) && reflection.fragmentArguments.count == (getenv("RIOT_METAL_TWO_BUFFER_STAGES") ? 2 : (vertexDepth ? 1 : 0)), "shader reflection populated"); + MTLArgument* argument = reflection.vertexArguments[0]; + verifyBatch(argument.type == MTLArgumentTypeBuffer && argument.index == 0 && [argument.name isEqualToString:@"vertices"] && argument.bufferDataSize == 32 && argument.bufferAlignment == 16, "AIR buffer reflection metadata"); + verifyBatch(argument.bufferStructType.members.count == 2 && [argument.bufferStructType memberByName:@"color"].offset == 16, "AIR struct reflection metadata"); + MTLTextureDescriptor* td = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm width:16 height:8 mipmapped:NO]; + td.storageMode = MTLStorageModePrivate; td.usage = MTLTextureUsageRenderTarget; + id color = [device newTextureWithDescriptor:td]; + struct Vertex { float position[2]; float padding[2]; float color[4]; } vertices[] = { + {{-16,-8},{0,0},{1,0,0,1}},{{16,-8},{0,0},{1,0,0,1}},{{0,8},{0,0},{1,0,0,1}}}; + unsigned viewport[] = {16,8}; + Vertex both[2][3]; + memcpy(both[0], vertices, sizeof(vertices)); + memcpy(both[1], vertices, sizeof(vertices)); + for (auto& v: both[1]) { v.color[0] = 0; v.color[1] = 1; } + id vb = [device newBufferWithBytes:both length:sizeof(both) options:MTLResourceStorageModeShared]; + unsigned char attributeBytes[12] = {255,128,7,1,255,128,7,1,255,128,7,1}; + id attributes = vertexDepth ? [device newBufferWithBytes:attributeBytes length:sizeof(attributeBytes) options:MTLResourceStorageModeShared] : nil; + MTLTextureDescriptor* depthDesc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatDepth32Float_Stencil8 width:16 height:8 mipmapped:NO]; + depthDesc.storageMode = MTLStorageModePrivate; depthDesc.usage = MTLTextureUsageShaderRead | MTLTextureUsageRenderTarget; + id sampledDepth = vertexDepth ? [device newTextureWithDescriptor:depthDesc] : nil; + id uniforms = [device newBufferWithBytes:viewport length:sizeof(viewport) options:MTLResourceStorageModeShared]; + const float tints[8] = {0,0,0,0,1,1,1,1}; + id fragmentUniforms = getenv("RIOT_METAL_TWO_BUFFER_STAGES") ? [device newBufferWithBytes:tints length:sizeof(tints) options:MTLResourceStorageModeShared] : nil; + id queue = [device newCommandQueue]; + id commands = [queue commandBuffer]; + if (vertexDepth) { + MTLRenderPassDescriptor* depthClear = [MTLRenderPassDescriptor renderPassDescriptor]; + depthClear.depthAttachment.texture = sampledDepth; + depthClear.depthAttachment.loadAction = MTLLoadActionClear; + depthClear.depthAttachment.storeAction = MTLStoreActionStore; + depthClear.depthAttachment.clearDepth = 0.75; + id clear = [commands renderCommandEncoderWithDescriptor:depthClear]; + [clear endEncoding]; + } + MTLRenderPassDescriptor* pass = [MTLRenderPassDescriptor renderPassDescriptor]; + pass.colorAttachments[colorSlot].texture = color; pass.colorAttachments[colorSlot].loadAction = MTLLoadActionClear; + pass.colorAttachments[colorSlot].storeAction = MTLStoreActionStore; + pass.colorAttachments[colorSlot].clearColor = MTLClearColorMake(0,0,0,1); + id render = [commands renderCommandEncoderWithDescriptor:pass]; + [render setRenderPipelineState:pipeline]; + if (vertexDepth) { [render setVertexBuffer:attributes offset:0 atIndex:2]; [render setFragmentTexture:sampledDepth atIndex:0]; } + [render setVertexBuffer:vb offset:0 atIndex:0]; [render setVertexBuffer:uniforms offset:0 atIndex:1]; + if (fragmentUniforms) [render setFragmentBuffer:fragmentUniforms offset:16 atIndex:0]; + [render setScissorRect:(MTLScissorRect){0,0,16,8}]; + // Exercise descriptor reuse, then pipeline invalidation and pool rollover. + const bool bindingBench = getenv("RIOT_METAL_BINDING_BENCH") != nullptr; + const unsigned repeatedDraws = bindingBench ? 10000 : 100; + for (unsigned draw = 0; draw < 100; ++draw) { + // Identical copied uniforms and unused-slot changes must preserve pixels. + // The first draw also changes data, so stale constant reuse fails readback. + unsigned wrongViewport[] = {32,16}; + [render setVertexBytes:wrongViewport length:sizeof(wrongViewport) atIndex:1]; + [render setVertexBytes:viewport length:sizeof(viewport) atIndex:1]; + [render setVertexBytes:viewport length:sizeof(viewport) atIndex:1]; + [render setVertexBuffer:vb offset:(draw & 1) * sizeof(vertices) atIndex:15]; + [render drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:3]; + } + id nextPipeline = [device newRenderPipelineStateWithDescriptor:desc error:&error]; + verifyBatch(nextPipeline && error == nil, "second render pipeline"); + [pipeline release]; + pipeline = nextPipeline; + [render setRenderPipelineState:pipeline]; + const uint64_t bindingStart = bindingBench ? mach_absolute_time() : 0; + for (unsigned draw = 0; draw < repeatedDraws; ++draw) { + if (getenv("RIOT_METAL_REBIND_BENCH")) { + [render setVertexBuffer:vb offset:(draw & 1) * sizeof(vertices) atIndex:0]; + if (vertexDepth) [render setFragmentTexture:sampledDepth atIndex:0]; + } else { + [render setVertexBufferOffset:(draw & 1) * sizeof(vertices) atIndex:0]; + } + if (fragmentUniforms) [render setFragmentBufferOffset:(draw & 1) * 16 atIndex:0]; + [render drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:3]; + } + if (bindingBench) { + uint64_t elapsed = mach_absolute_time() - bindingStart; + mach_timebase_info_data_t scale; mach_timebase_info(&scale); + fprintf(stderr, "BINDING_BENCH: %u changed-offset draws encoding_ms=%.3f\n", + repeatedDraws, elapsed * (double)scale.numer / scale.denom / 1e6); + } + if (fragmentUniforms) [render setFragmentBufferOffset:16 atIndex:0]; + // Indexed draws exercise reused handles, offset changes, and type changes. + const uint16_t indices16[] = {0,0,1,2}; + const uint32_t indices32[] = {0,0,1,2}; + id ib16 = [device newBufferWithBytes:indices16 length:sizeof(indices16) options:MTLResourceStorageModeShared]; + id ib32 = [device newBufferWithBytes:indices32 length:sizeof(indices32) options:MTLResourceStorageModeShared]; + for (unsigned draw = 0; draw < 100; ++draw) { + const bool wide = (draw / 2) & 1; + [render drawIndexedPrimitives:MTLPrimitiveTypeTriangle indexCount:3 indexType:(wide ? MTLIndexTypeUInt32 : MTLIndexTypeUInt16) indexBuffer:(wide ? ib32 : ib16) indexBufferOffset:(wide ? sizeof(uint32_t) : sizeof(uint16_t))]; + } + // Release both wrappers before submission, replacing the first binding. + // The encoder must retain the first draw's buffer after the second bind. + id greenDraw = [device newBufferWithBytes:both[1] length:sizeof(vertices) options:MTLResourceStorageModeShared]; + [render setVertexBuffer:greenDraw offset:0 atIndex:0]; + [render setScissorRect:(MTLScissorRect){0,0,8,8}]; + [render drawIndexedPrimitives:MTLPrimitiveTypeTriangle indexCount:3 indexType:MTLIndexTypeUInt16 indexBuffer:ib16 indexBufferOffset:sizeof(uint16_t)]; + [greenDraw release]; [ib16 release]; + id redDraw = [device newBufferWithBytes:both[0] length:sizeof(vertices) options:MTLResourceStorageModeShared]; + [render setVertexBuffer:redDraw offset:0 atIndex:0]; + [render setScissorRect:(MTLScissorRect){8,0,8,8}]; + [render drawIndexedPrimitives:MTLPrimitiveTypeTriangle indexCount:3 indexType:MTLIndexTypeUInt32 indexBuffer:ib32 indexBufferOffset:sizeof(uint32_t)]; + [redDraw release]; [ib32 release]; + if (getenv("RIOT_METAL_EARLY_RELEASE")) { + // Drop the last public bindings and wrappers before submission. Earlier + // draw resources must survive in the encoder's owning resolution maps. + [render setVertexBuffer:nil offset:0 atIndex:0]; + [render setVertexBuffer:nil offset:0 atIndex:1]; + [render setVertexBuffer:nil offset:0 atIndex:2]; + [render setFragmentBuffer:nil offset:0 atIndex:0]; + [render setFragmentTexture:nil atIndex:0]; + [vb release]; vb = nil; + [uniforms release]; uniforms = nil; + [fragmentUniforms release]; fragmentUniforms = nil; + [attributes release]; attributes = nil; + [sampledDepth release]; sampledDepth = nil; + } + [render endEncoding]; + // Preserve the rendered attachment in a second encoder, then consume it + // in a transfer encoder without a CPU/GPU wait between those accesses. + pass.colorAttachments[colorSlot].loadAction = MTLLoadActionLoad; + render = [commands renderCommandEncoderWithDescriptor:pass]; + [render endEncoding]; + id output = [device newBufferWithLength:512 options:MTLResourceStorageModeShared]; + id blit = [commands blitCommandEncoder]; + [blit copyFromTexture:color sourceSlice:0 sourceLevel:0 sourceOrigin:MTLOriginMake(0,0,0) sourceSize:MTLSizeMake(16,8,1) toBuffer:output destinationOffset:0 destinationBytesPerRow:64 destinationBytesPerImage:512]; + [blit endEncoding]; [commands commit]; [commands waitUntilCompleted]; + unsigned char* pixels = (unsigned char*)output.contents; + unsigned center = (4*16+8)*4; + verifyBatch(pixels[center]>200 && pixels[center+1]==0 && pixels[center+2]==0, "translated shader draws red triangle"); + if (!getenv("RIOT_METAL_SKIP_COLOR_CHANGE_CHECK")) { + unsigned left = (4*16+6)*4; + verifyBatch(pixels[left]==0 && pixels[left+1]>200 && pixels[left+2]==0, "descriptor cache honors changed buffer offset"); + } + [fragmentUniforms release];[attributes release];[sampledDepth release];[output release];[queue release];[vb release];[uniforms release];[color release]; + [pipeline release];[desc release];[vertex release];[fragment release];[library release]; + fprintf(stderr,"PASS: Metal shader data, AIR reflection, GPU triangle and encoder visibility\n"); +} diff --git a/tools/riot-metal-command-buffer-bench.mm b/tools/riot-metal-command-buffer-bench.mm new file mode 100644 index 0000000..5f31024 --- /dev/null +++ b/tools/riot-metal-command-buffer-bench.mm @@ -0,0 +1,74 @@ +#import +#import +#include +#include +#include +#include +#include +#include +int main() { + @autoreleasepool { + id device = MTLCreateSystemDefaultDevice(); + if (!device) return 1; + printf("DEVICE: %s\n", [[device name] UTF8String]); + id queue = [device newCommandQueue]; + bool submit = getenv("RIOT_BENCH_SUBMIT") != nullptr; + unsigned iterations = submit ? 2000 : 10000; + if (const char* setting = getenv("RIOT_BENCH_ITERATIONS")) iterations = strtoul(setting, nullptr, 10); + unsigned passes = 1; + if (const char* setting = getenv("RIOT_BENCH_PASSES")) passes = strtoul(setting, nullptr, 10); + if (!iterations || iterations > 1000000 || !passes || passes > 4096) return 3; + bool split = getenv("RIOT_BENCH_SPLIT") != nullptr; + std::vector frameTimes; + MTLTextureDescriptor* descriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm width:16 height:16 mipmapped:NO]; + descriptor.storageMode = MTLStorageModePrivate; + descriptor.usage = MTLTextureUsageRenderTarget; + id color = submit ? [device newTextureWithDescriptor:descriptor] : nil; + MTLRenderPassDescriptor* pass = [MTLRenderPassDescriptor renderPassDescriptor]; + pass.colorAttachments[0].texture = color; + pass.colorAttachments[0].loadAction = MTLLoadActionClear; + pass.colorAttachments[0].storeAction = MTLStoreActionStore; + pass.colorAttachments[0].clearColor = MTLClearColorMake(1,0,0,1); + // Warm the driver and exercise discarded recording buffers. Each scope + // ends the buffer lifetime; the exclusive pool can then be leased again. + for (unsigned i = 0; i < 100; ++i) { + @autoreleasepool { if (![queue commandBuffer]) return 2; } + } + struct rusage before = {}, after = {}; + getrusage(RUSAGE_SELF, &before); + mach_timebase_info_data_t timebase; + mach_timebase_info(&timebase); + uint64_t start = mach_absolute_time(); + for (unsigned i = 0; i < iterations; ++i) { + uint64_t frameStart = mach_absolute_time(); + @autoreleasepool { + id commands = [queue commandBuffer]; + if (!commands) return 2; + if (submit) { + for (unsigned p = 0; p < passes; ++p) { + if (split && p) commands = [queue commandBuffer]; + if (!commands) return 2; + id encoder = [commands renderCommandEncoderWithDescriptor:pass]; + [encoder endEncoding]; + if (split) [commands commit]; + } + if (!split) [commands commit]; + [commands waitUntilCompleted]; + } + } + frameTimes.push_back((mach_absolute_time() - frameStart) * (double)timebase.numer / timebase.denom / 1e6); + } + std::sort(frameTimes.begin(), frameTimes.end()); + printf("FRAMEBENCH: passes=%u split=%u p50=%.3fms p99=%.3fms max=%.3fms\n", passes, split, + frameTimes[frameTimes.size()/2], frameTimes[(frameTimes.size()-1)*99/100], frameTimes.back()); + uint64_t end = mach_absolute_time(); + getrusage(RUSAGE_SELF, &after); + printf("BENCH: %u %s command buffers %.3f ms minor_faults=%ld\n", + iterations, submit ? "clear/submitted" : "discarded", (end-start) * (double)timebase.numer / timebase.denom / 1e6, + after.ru_minflt-before.ru_minflt); + [color release]; + [queue release]; + [device release]; + } + return 0; +} diff --git a/tools/riot-metal-concurrency-check.mm b/tools/riot-metal-concurrency-check.mm new file mode 100644 index 0000000..c5d05f5 --- /dev/null +++ b/tools/riot-metal-concurrency-check.mm @@ -0,0 +1,108 @@ +#import +#import +#include +#undef NDEBUG +#include +#include + +struct Worker { id device; id queue; unsigned index; id shared; }; +static void *run(void *argument) { + Worker *worker = (Worker *)argument; + for (unsigned iteration = 0; iteration < 100; ++iteration) { + @autoreleasepool { + MTLTextureDescriptor *descriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm width:16 height:16 mipmapped:NO]; + descriptor.storageMode = MTLStorageModeShared; + descriptor.usage = MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead; + id texture = worker->shared ? [worker->shared retain] : [worker->device newTextureWithDescriptor:descriptor]; + unsigned char initial[16 * 16 * 4] = {}; + if (!worker->shared) [texture replaceRegion:MTLRegionMake2D(0,0,16,16) mipmapLevel:0 withBytes:initial bytesPerRow:64]; + id readback = [worker->device newBufferWithLength:sizeof(initial) options:MTLResourceStorageModeShared]; + MTLRenderPassDescriptor *pass = [MTLRenderPassDescriptor renderPassDescriptor]; + pass.colorAttachments[0].texture = texture; + pass.colorAttachments[0].loadAction = MTLLoadActionClear; + pass.colorAttachments[0].storeAction = MTLStoreActionStore; + const unsigned char red = (worker->index + 1) * 40; + pass.colorAttachments[0].clearColor = MTLClearColorMake(red / 255.0,0,1,1); + id commands = [worker->queue commandBuffer]; + id render = [commands renderCommandEncoderWithDescriptor:pass]; + [render endEncoding]; + id blit = [commands blitCommandEncoder]; + [blit copyFromTexture:texture sourceSlice:0 sourceLevel:0 sourceOrigin:MTLOriginMake(0,0,0) sourceSize:MTLSizeMake(16,16,1) toBuffer:readback destinationOffset:0 destinationBytesPerRow:64 destinationBytesPerImage:sizeof(initial)]; + [blit endEncoding]; + [commands commit]; + [commands waitUntilCompleted]; + const unsigned char *pixels = (const unsigned char *)readback.contents; + for (unsigned p = 0; p < 256; ++p) { + assert(pixels[p*4] == red && pixels[p*4+1] == 0 && pixels[p*4+2] == 255 && pixels[p*4+3] == 255); + } + [readback release]; + [texture release]; + } + } + return NULL; +} +int main(void) { + @autoreleasepool { + id device = MTLCreateSystemDefaultDevice(); + assert(device); + id queue = [device newCommandQueue]; + pthread_t threads[4]; Worker workers[4]; + for (unsigned i = 0; i < 4; ++i) { + workers[i] = {device, queue, i, nil}; + assert(pthread_create(&threads[i], NULL, run, &workers[i]) == 0); + } + for (auto thread: threads) assert(pthread_join(thread, NULL) == 0); + MTLTextureDescriptor *descriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm width:16 height:16 mipmapped:NO]; + descriptor.storageMode = MTLStorageModePrivate; + descriptor.usage = MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead; + id shared = [device newTextureWithDescriptor:descriptor]; + for (unsigned i = 0; i < 4; ++i) { + workers[i].shared = shared; + assert(pthread_create(&threads[i], NULL, run, &workers[i]) == 0); + } + for (auto thread: threads) assert(pthread_join(thread, NULL) == 0); + // Queue more commands than the optional submission worker's backlog + // limit, without intervening waits. The final copy must observe the + // final clear from a separate command buffer, after earlier wrappers + // have been released by their autorelease pools. + for (unsigned i = 0; i < 130; ++i) { + @autoreleasepool { + MTLRenderPassDescriptor *pass = [MTLRenderPassDescriptor renderPassDescriptor]; + pass.colorAttachments[0].texture = shared; + pass.colorAttachments[0].loadAction = MTLLoadActionClear; + pass.colorAttachments[0].storeAction = MTLStoreActionStore; + pass.colorAttachments[0].clearColor = MTLClearColorMake(i / 255.0,0,1,1); + id commands = [queue commandBuffer]; + id render = [commands renderCommandEncoderWithDescriptor:pass]; + [render endEncoding]; + [commands commit]; + } + } + id output = [device newBufferWithLength:16*16*4 options:MTLResourceStorageModeShared]; + id copy = [queue commandBuffer]; + // A Load pass must not reuse the earlier Clear semantics, even when + // format and render-pass compatibility are identical. + MTLRenderPassDescriptor *preserve = [MTLRenderPassDescriptor renderPassDescriptor]; + preserve.colorAttachments[0].texture = shared; + preserve.colorAttachments[0].loadAction = MTLLoadActionLoad; + preserve.colorAttachments[0].storeAction = MTLStoreActionStore; + preserve.colorAttachments[0].clearColor = MTLClearColorMake(0,1,0,1); + id load = [copy renderCommandEncoderWithDescriptor:preserve]; + [load endEncoding]; + id blit = [copy blitCommandEncoder]; + [blit copyFromTexture:shared sourceSlice:0 sourceLevel:0 sourceOrigin:MTLOriginMake(0,0,0) + sourceSize:MTLSizeMake(16,16,1) toBuffer:output destinationOffset:0 + destinationBytesPerRow:64 destinationBytesPerImage:16*16*4]; + [blit endEncoding]; [copy commit]; [copy waitUntilCompleted]; + const unsigned char *pixels = (const unsigned char *)output.contents; + for (unsigned p = 0; p < 256; ++p) + assert(pixels[p*4] == 129 && pixels[p*4+1] == 0 && pixels[p*4+2] == 255 && pixels[p*4+3] == 255); + [output release]; + puts("PASS: 131 queued submissions, FIFO texture dependencies and released command wrappers"); + puts("PASS: Load preserves pixels after Clear with a different clear color"); + [shared release]; + [queue release]; + puts("PASS: 4 concurrent workers, 800 submissions with private and shared textures, all pixels correct"); + } + return 0; +} diff --git a/tools/riot-metal-presentation-check.mm b/tools/riot-metal-presentation-check.mm new file mode 100644 index 0000000..4864d4d --- /dev/null +++ b/tools/riot-metal-presentation-check.mm @@ -0,0 +1,101 @@ +#import +#import +#import +#import +#import +#import "CALayerInternal.h" +#import +#import +#include +#include + +static void verifyPresentation(bool value, const char* message) { + if (!value) { fprintf(stderr, "FAIL: %s\n", message); exit(1); } +} +static void pump(NSApplication* app) { + NSDate* end = [NSDate dateWithTimeIntervalSinceNow:0.04]; + while ([end timeIntervalSinceNow] > 0) { + NSEvent* event = [app nextEventMatchingMask:NSAnyEventMask untilDate:[NSDate distantPast] + inMode:NSDefaultRunLoopMode dequeue:YES]; + if (event) [app sendEvent:event]; + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.005]]; + } +} +int main() { + @autoreleasepool { + NSApplication* app = [NSApplication sharedApplication]; + const bool captureTest = getenv("RIOT_FRAME_CAPTURE_TEST") != nullptr; + if (captureTest) { + FILE* flag = fopen("/tmp/metal-frame-interval-capture", "a"); + verifyPresentation(flag != nullptr, "enable bounded capture"); + fclose(flag); + remove("/tmp/metal-frame-interval-repeat"); + remove("/tmp/metal-present-intervals-8.csv"); + } + id device = MTLCreateSystemDefaultDevice(); + verifyPresentation(device != nil, "device"); + CAMetalLayer* layer = [CAMetalLayer layer]; + layer.device = device; + layer.drawableSize = CGSizeMake(160,96); + layer.frame = CGRectMake(0,0,160,96); + NSView* view = [[NSView alloc] initWithFrame:NSMakeRect(0,0,160,96)]; + view.wantsLayer = YES; + view.layer = layer; + NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(50,50,160,96) + styleMask:NSTitledWindowMask backing:NSBackingStoreBuffered defer:NO]; + window.title = @"Metal presentation check"; + window.contentView = view; + [window makeKeyAndOrderFront:nil]; + pump(app); + id queue = [device newCommandQueue]; + const unsigned frameCount = captureTest ? 130u : getenv("RIOT_PRESENTATION_STRESS") ? 120u : 10u; + for (unsigned frame = 0; frame < frameCount; ++frame) { + if (captureTest && frame == 80) { + FILE* flag = fopen("/tmp/metal-frame-interval-repeat", "a"); + verifyPresentation(flag != nullptr, "request second bounded capture"); + fclose(flag); + } + @autoreleasepool { + id drawable = [layer nextDrawable]; + verifyPresentation(drawable != nil, "drawable after late window/context attachment"); + verifyPresentation(drawable.texture.width == 160 && drawable.texture.height == 96, "drawable dimensions"); + MTLRenderPassDescriptor* pass = [MTLRenderPassDescriptor renderPassDescriptor]; + pass.colorAttachments[0].texture = drawable.texture; + pass.colorAttachments[0].loadAction = MTLLoadActionClear; + pass.colorAttachments[0].storeAction = MTLStoreActionStore; + pass.colorAttachments[0].clearColor = MTLClearColorMake(1,0,0,1); + id commands = [queue commandBuffer]; + id encoder = [commands renderCommandEncoderWithDescriptor:pass]; + [encoder endEncoding]; + [commands presentDrawable:drawable]; + [commands commit]; + [commands waitUntilCompleted]; + pump(app); + } + } + CGLContextObj previous = CGLGetCurrentContext(); + CGLSetCurrentContext([(CALayerContext*)[layer _context] glContext]); + glBindTexture(GL_TEXTURE_2D, [[layer _textureId] unsignedIntValue]); + unsigned char pixels[160 * 96 * 4] = {}; + glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels); + verifyPresentation(glGetError() == GL_NO_ERROR, "presentation texture readback"); + verifyPresentation(pixels[0] > 250 && pixels[1] < 5 && pixels[2] < 5 && pixels[3] > 250, + "Vulkan clear reaches the OpenGL presentation texture"); + CGLSetCurrentContext(previous); + if (captureTest) { + FILE* capture = fopen("/tmp/metal-present-intervals-8.csv", "r"); + verifyPresentation(capture != nullptr, "second capture without restarting the process"); + char line[256]; unsigned rows = 0; + while (fgets(line, sizeof(line), capture)) ++rows; + fclose(capture); + verifyPresentation(rows == 9, "second capture has header and eight samples"); + fprintf(stderr, "PASS: bounded presentation capture rearmed without restarting\n"); + } + fprintf(stderr, "PASS: Vulkan-to-OpenGL presentation pixels\n"); + fprintf(stderr, "PASS: late context attachment and %u Metal drawable presentations\n", frameCount); + [window orderOut:nil]; + [queue release]; [view release]; [window release]; [device release]; + } + fprintf(stderr, "PASS: presentation cleanup\n"); + return 0; +} diff --git a/tools/riot-metal-resource-getter-check.mm b/tools/riot-metal-resource-getter-check.mm new file mode 100644 index 0000000..feb4569 --- /dev/null +++ b/tools/riot-metal-resource-getter-check.mm @@ -0,0 +1,41 @@ +#import +#import +#import +#include +#include +#include +#include +#include +static void verifyResource(bool value) { if (!value) abort(); } +struct Worker { MTLBufferInternal* buffer; Indium::Buffer* expected; }; +static void* readResource(void* argument) { + auto* worker = static_cast(argument); + @autoreleasepool { + for (unsigned i=0;i<100000;++i) { + auto owner=worker->buffer.buffer; + verifyResource(owner.get()==worker->expected && static_cast(owner->contents())[0]==197); + } + [worker->buffer release]; + } + return nullptr; +} +int main() { + @autoreleasepool { + id device=MTLCreateSystemDefaultDevice(); verifyResource(device!=nil); + MTLBufferInternal* buffer=(MTLBufferInternal*)[device newBufferWithLength:16 options:MTLResourceStorageModeShared]; verifyResource(buffer!=nil); + static_cast(buffer.contents)[0]=197; + auto held=buffer.buffer; + mach_timebase_info_data_t timebase;mach_timebase_info(&timebase); + uintptr_t checksum=0;const unsigned count=2000000; + const uint64_t begin=mach_absolute_time(); + for (unsigned i=0;i(owner.get()); } + const uint64_t end=mach_absolute_time(); + printf("BENCH: %u immutable resource getters %.3f ns/get checksum=%llu\n",count,(end-begin)*(double)timebase.numer/timebase.denom/count,(unsigned long long)checksum); + Worker workers[4];pthread_t threads[4]; + for (unsigned i=0;i<4;++i) {workers[i]={(MTLBufferInternal*)[buffer retain],held.get()};verifyResource(pthread_create(&threads[i],nullptr,readResource,&workers[i])==0);} + [buffer release]; + for (auto thread:threads) verifyResource(pthread_join(thread,nullptr)==0); + verifyResource(static_cast(held->contents())[0]==197);held.reset();[device release]; + puts("PASS: 400000 concurrent immutable getters and resource owner outlives Objective-C wrapper"); + } +} diff --git a/tools/riot-metal-sampler-check.mm b/tools/riot-metal-sampler-check.mm new file mode 100644 index 0000000..0b467f9 --- /dev/null +++ b/tools/riot-metal-sampler-check.mm @@ -0,0 +1,141 @@ +#import +#import +#include +#include +#include +#include +#import +static void verifySampler(bool value, const char* message) { + if (!value) { fprintf(stderr, "FAIL: %s\n", message); exit(1); } +} +extern void checkMetalShader(id); +extern void checkMetalBatch(id, id); +static void checkCompressedTransfers(id device) { + const MTLPixelFormat formats[] = { + MTLPixelFormatBC1_RGBA, MTLPixelFormatBC1_RGBA_sRGB, MTLPixelFormatBC2_RGBA, + MTLPixelFormatBC2_RGBA_sRGB, MTLPixelFormatBC3_RGBA, MTLPixelFormatBC3_RGBA_sRGB, + MTLPixelFormatBC4_RUnorm, MTLPixelFormatBC4_RSnorm, MTLPixelFormatBC5_RGUnorm, + MTLPixelFormatBC5_RGSnorm, MTLPixelFormatBC6H_RGBFloat, MTLPixelFormatBC6H_RGBUfloat, + MTLPixelFormatBC7_RGBAUnorm, MTLPixelFormatBC7_RGBAUnorm_sRGB + }; + id queue = [device newCommandQueue]; + for (auto format : formats) { + size_t blockBytes = (format == MTLPixelFormatBC1_RGBA || format == MTLPixelFormatBC1_RGBA_sRGB || + format == MTLPixelFormatBC4_RUnorm || format == MTLPixelFormatBC4_RSnorm) ? 8 : 16; + // An odd extent, padded block rows, nonzero offsets and a tight final mip. + size_t rowBytes = blockBytes * 4; + unsigned char input[256], zeros[512]; + for (unsigned i = 0; i < sizeof(input); ++i) input[i] = (i * 37 + 11) & 255; + memset(zeros, 0xaa, sizeof(zeros)); + id upload = [device newBufferWithBytes:input length:sizeof(input) options:MTLResourceStorageModeShared]; + id output = [device newBufferWithBytes:zeros length:sizeof(zeros) options:MTLResourceStorageModeShared]; + MTLTextureDescriptor* desc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:format width:9 height:7 mipmapped:YES]; + desc.storageMode = MTLStorageModePrivate; desc.usage = MTLTextureUsageShaderRead; + id texture = [device newTextureWithDescriptor:desc]; + verifySampler(texture != nil, "compressed texture allocation"); + id commands = [queue commandBuffer]; + id blit = [commands blitCommandEncoder]; + [blit copyFromBuffer:upload sourceOffset:64 sourceBytesPerRow:rowBytes sourceBytesPerImage:0 sourceSize:MTLSizeMake(9,7,1) toTexture:texture destinationSlice:0 destinationLevel:0 destinationOrigin:MTLOriginMake(0,0,0)]; + [blit copyFromTexture:texture sourceSlice:0 sourceLevel:0 sourceOrigin:MTLOriginMake(0,0,0) sourceSize:MTLSizeMake(9,7,1) toBuffer:output destinationOffset:128 destinationBytesPerRow:rowBytes destinationBytesPerImage:0 options:MTLBlitOptionNone]; + [blit copyFromBuffer:upload sourceOffset:0 sourceBytesPerRow:blockBytes sourceBytesPerImage:0 sourceSize:MTLSizeMake(2,1,1) toTexture:texture destinationSlice:0 destinationLevel:2 destinationOrigin:MTLOriginMake(0,0,0)]; + [blit copyFromTexture:texture sourceSlice:0 sourceLevel:2 sourceOrigin:MTLOriginMake(0,0,0) sourceSize:MTLSizeMake(2,1,1) toBuffer:output destinationOffset:0 destinationBytesPerRow:blockBytes destinationBytesPerImage:0 options:MTLBlitOptionNone]; + [blit endEncoding]; [commands commit]; [commands waitUntilCompleted]; + auto result = (unsigned char*)output.contents; + for (size_t row = 0; row < 2; ++row) { + verifySampler(memcmp(input + 64 + row * rowBytes, result + 128 + row * rowBytes, blockBytes * 3) == 0, "compressed padded upload/readback"); + verifySampler(result[128 + row * rowBytes + blockBytes * 3] == 0xaa, "compressed row padding preserved"); + } + verifySampler(memcmp(input, result, blockBytes) == 0 && result[blockBytes] == 0xaa, "compressed mip smaller than a block"); + [texture release]; + desc.storageMode = MTLStorageModeShared; + texture = [device newTextureWithDescriptor:desc]; + [texture replaceRegion:MTLRegionMake2D(0,0,9,7) mipmapLevel:0 withBytes:input+64 bytesPerRow:rowBytes]; + commands = [queue commandBuffer]; blit = [commands blitCommandEncoder]; + [blit copyFromTexture:texture sourceSlice:0 sourceLevel:0 sourceOrigin:MTLOriginMake(0,0,0) sourceSize:MTLSizeMake(9,7,1) toBuffer:output destinationOffset:128 destinationBytesPerRow:rowBytes destinationBytesPerImage:0 options:MTLBlitOptionNone]; + [blit endEncoding]; [commands commit]; [commands waitUntilCompleted]; + for (size_t row = 0; row < 2; ++row) + verifySampler(memcmp(input + 64 + row * rowBytes, result + 128 + row * rowBytes, blockBytes * 3) == 0, "compressed replaceRegion upload"); + [texture release]; [upload release]; [output release]; + } + [queue release]; + fprintf(stderr, "PASS: all 14 BC formats, padded rows, buffer offsets, small mips and replaceRegion\n"); +} +int main() { + NSAutoreleasePool* pool = [NSAutoreleasePool new]; + MTLSamplerDescriptor* desc = [MTLSamplerDescriptor new]; + verifySampler(desc.maxAnisotropy == 1 && desc.normalizedCoordinates && desc.lodMaxClamp == FLT_MAX, "defaults"); + desc.minFilter = MTLSamplerMinMagFilterLinear; + desc.magFilter = MTLSamplerMinMagFilterLinear; + desc.mipFilter = MTLSamplerMipFilterLinear; + desc.sAddressMode = MTLSamplerAddressModeRepeat; + desc.tAddressMode = MTLSamplerAddressModeMirrorRepeat; + desc.lodMinClamp = 0.5f; + desc.lodMaxClamp = 12.5f; + desc.label = @"League sampler"; + MTLSamplerDescriptor* copy = [desc copy]; + desc.lodMinClamp = 1.5f; + verifySampler(copy.lodMinClamp == 0.5f && copy.lodMaxClamp == 12.5f && copy.sAddressMode == MTLSamplerAddressModeRepeat, "copy preserves typed properties"); + id device = MTLCreateSystemDefaultDevice(); + verifySampler(device != nil, "Metal device"); + id state = [device newSamplerStateWithDescriptor:copy]; + verifySampler(state != nil && state.device == device && [state.label isEqualToString:@"League sampler"], "sampler device and label"); + verifySampler(((MTLSamplerStateInternal*)state).sampler != nullptr, "real Indium sampler"); + copy.label = @"changed"; + [copy release]; [desc release]; + verifySampler([state.label isEqualToString:@"League sampler"], "state survives descriptor release"); + MTLTextureDescriptor* td = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm width:16 height:8 mipmapped:YES]; + verifySampler(td.mipmapLevelCount == 5 && td.depth == 1 && td.arrayLength == 1, "texture helper mip levels"); + td.storageMode = MTLStorageModePrivate; + td.cpuCacheMode = MTLCPUCacheModeWriteCombined; + verifySampler(td.storageMode == MTLStorageModePrivate && td.cpuCacheMode == MTLCPUCacheModeWriteCombined, "resource options fields preserve each other"); + td.usage = MTLTextureUsageShaderRead | MTLTextureUsageRenderTarget; + MTLTextureDescriptor* tc = [td copy]; + td.width = 32; + verifySampler(tc.width == 16 && tc.mipmapLevelCount == 5, "texture descriptor copy"); + id texture = [device newTextureWithDescriptor:tc]; + verifySampler(texture != nil && texture.width == 16 && texture.height == 8 && texture.mipmapLevelCount == 5, "GPU texture allocation"); + verifySampler(texture.device == device && texture.storageMode == MTLStorageModePrivate, "GPU texture resource options"); + verifySampler(((MTLTextureInternal*)texture).texture != nullptr, "real Indium texture"); + id upload = [device newBufferWithLength:1024 options:MTLResourceStorageModeShared]; + id readback = [device newBufferWithLength:1536 options:MTLResourceStorageModeShared]; + verifySampler(upload != nil && readback != nil, "transfer buffers"); + unsigned char* input = (unsigned char*)upload.contents; + unsigned char* output = (unsigned char*)readback.contents; + memset(input, 0xcc, 1024); memset(output, 0xaa, 1536); + for (unsigned i = 0; i < 512; ++i) input[256+i] = (i * 37 + 11) & 255; + id queue = [device newCommandQueue]; + id commands = [queue commandBuffer]; + verifySampler(commands.commandQueue == queue, "command buffer retains its queue"); + id blit = [commands blitCommandEncoder]; + verifySampler(blit != nil && blit.device == device, "real blit encoder"); + blit.label = @"texture round trip"; + [blit copyFromBuffer:upload sourceOffset:256 sourceBytesPerRow:64 sourceBytesPerImage:512 sourceSize:MTLSizeMake(16,8,1) toTexture:texture destinationSlice:0 destinationLevel:0 destinationOrigin:MTLOriginMake(0,0,0)]; + [blit copyFromTexture:texture sourceSlice:0 sourceLevel:0 sourceOrigin:MTLOriginMake(0,0,0) sourceSize:MTLSizeMake(16,8,1) toBuffer:readback destinationOffset:512 destinationBytesPerRow:64 destinationBytesPerImage:512 options:MTLBlitOptionNone]; + [blit endEncoding]; [commands commit]; [commands waitUntilCompleted]; + verifySampler(memcmp(input+256, output+512, 512) == 0, "texture upload/readback respects buffer offsets"); + verifySampler(output[0] == 0xaa && output[511] == 0xaa && output[1024] == 0xaa, "readback preserves bytes outside destination"); + commands = [queue commandBuffer]; blit = [commands blitCommandEncoder]; + [blit fillBuffer:readback range:NSMakeRange(0,64) value:0x5a]; + [blit copyFromBuffer:upload sourceOffset:256 toBuffer:readback destinationOffset:128 size:64]; + [blit endEncoding]; [commands commit]; [commands waitUntilCompleted]; + verifySampler(output[0] == 0x5a && output[63] == 0x5a && output[64] == 0xaa, "fill range"); + verifySampler(memcmp(input+256, output+128, 64) == 0, "buffer copy offsets"); + [queue release]; [upload release]; [readback release]; + [tc release]; [texture release]; + NSAutoreleasePool* transferPool = [NSAutoreleasePool new]; + checkCompressedTransfers(device); + [transferPool drain]; + fprintf(stderr, "PASS: compressed transfer cleanup\n"); + NSAutoreleasePool* batchPool = [NSAutoreleasePool new]; + checkMetalBatch(device, state); + [batchPool drain]; + fprintf(stderr, "PASS: Metal batch cleanup\n"); + NSAutoreleasePool* shaderPool = [NSAutoreleasePool new]; + checkMetalShader(device); + [shaderPool drain]; + fprintf(stderr, "PASS: shader cleanup\n"); + [state release]; [device release]; + fprintf(stderr, "PASS: Metal sampler, texture, blit upload/readback and buffer operations\n"); + [pool drain]; + return 0; +} diff --git a/tools/riot-metal-vertex-check.mm b/tools/riot-metal-vertex-check.mm new file mode 100644 index 0000000..086a87a --- /dev/null +++ b/tools/riot-metal-vertex-check.mm @@ -0,0 +1,50 @@ +#import +#import +#include +#include +#include +static unsigned setters; +@interface ObservedAttribute : MTLVertexAttributeDescriptor +@end +@implementation ObservedAttribute +- (void)setFormat:(MTLVertexFormat)value { ++setters; [super setFormat:value]; } +- (void)setOffset:(NSUInteger)value { ++setters; [super setOffset:value]; } +- (void)setBufferIndex:(NSUInteger)value { ++setters; [super setBufferIndex:value]; } +@end +static void verifyVertex(bool ok) { if (!ok) abort(); } +int main() { + @autoreleasepool { + MTLVertexDescriptor* vertex = [MTLVertexDescriptor new]; + // Populate sparse endpoints and hold a reference through reset. + MTLVertexAttributeDescriptor* held = [vertex.attributes[30] retain]; + held.format = MTLVertexFormatFloat4; held.offset = 16; held.bufferIndex = 30; + vertex.layouts[30].stride = 48; vertex.layouts[30].stepRate = 3; + MTLVertexDescriptor* copy = [vertex copy]; + [vertex reset]; + verifyVertex(held.format == MTLVertexFormatInvalid && held.offset == 0 && held.bufferIndex == 0); + verifyVertex(vertex.layouts[30].stride == 0 && vertex.layouts[30].stepRate == 1); + auto converted = [copy asIndiumDescriptor]; + verifyVertex(converted.attributes.at(30).offset == 16 && converted.layouts.at(30).stride == 48); + verifyVertex(converted.layouts.at(30).stepRate == 3); + ObservedAttribute* custom = [ObservedAttribute new]; + custom.format = MTLVertexFormatFloat2; custom.bufferIndex = 4; + vertex.attributes[4] = custom; + verifyVertex([vertex asIndiumDescriptor].attributes.at(4).bufferIndex == 4); + setters = 0; [vertex reset]; verifyVertex(setters == 3); + vertex.attributes[4] = nil; [custom release]; [copy release]; [held release]; + // Repeated resets after lazy descriptors have been allocated, as in the game. + for (unsigned i=0;i<12;++i) { vertex.attributes[i].format=MTLVertexFormatFloat3; vertex.layouts[i].stride=32; } + mach_timebase_info_data_t tb; mach_timebase_info(&tb); + const unsigned iterations=500000; + uint64_t start=mach_absolute_time(); + for (unsigned i=0;i +#import +#import +#include +#include +#include +#include +@interface NSDisplay (ModifierProbe) +- (void)postXEvent:(XEvent *)event; +@end +static NSMutableArray *events; +static void capture(id display, SEL selector, NSEvent *event, BOOL first) { + [events addObject:event]; +} +static void key(NSDisplay *display, int type, unsigned int code, unsigned int state) { + XEvent event={0}; event.type=type; event.xkey.keycode=code;event.xkey.state=state; + [display postXEvent:&event]; +} +static void expectModifier(unsigned short code, NSUInteger flags) { + NSEvent *event=[events lastObject]; + assert([event type]==NSFlagsChanged); + assert([event keyCode]==code); + assert([event modifierFlags]==flags); +} +int main(void) { + @autoreleasepool { + [NSApplication sharedApplication]; + NSDisplay *display=[NSDisplay currentDisplay]; + printf("APPKIT_BUNDLE %s\n",[[[NSBundle bundleForClass:[NSDisplay class]] bundlePath] UTF8String]); + printf("BACKEND modifier_reconciliation=%d\n",[display respondsToSelector:@selector(reconcileModifierKeys:window:)]);fflush(stdout); + printf("DISPLAY_CLASS %s BUNDLE %s\n",class_getName([display class]),[[[NSBundle bundleForClass:[display class]] bundlePath] UTF8String]);fflush(stdout);sleep(5); + events=[NSMutableArray new]; + Method method=class_getInstanceMethod([display class],@selector(postEvent:atStart:)); + IMP original=method_setImplementation(method,(IMP)capture); + key(display,KeyPress,133,0);expectModifier(0x37,NSCommandKeyMask); + key(display,KeyPress,134,Mod4Mask);expectModifier(0x36,NSCommandKeyMask); + key(display,KeyRelease,133,Mod4Mask);expectModifier(0x37,NSCommandKeyMask); + key(display,KeyRelease,134,Mod4Mask);expectModifier(0x36,0); + key(display,KeyPress,64,0);expectModifier(0x3a,NSAlternateKeyMask); + key(display,KeyRelease,64,Mod1Mask);expectModifier(0x3a,0); + key(display,KeyPress,50,0);expectModifier(0x38,NSShiftKeyMask); + key(display,KeyPress,133,ShiftMask);expectModifier(0x37,NSShiftKeyMask|NSCommandKeyMask); + XEvent focus={0}; focus.type=FocusOut; [display postXEvent:&focus]; + expectModifier(0x37,0); + NSUInteger cleared=[events count]; [display postXEvent:&focus]; + assert([events count]==cleared); + key(display,KeyPress,133,0);expectModifier(0x37,NSCommandKeyMask); + XEvent map={0};map.type=KeymapNotify;[display postXEvent:&map];expectModifier(0x37,0); + key(display,KeyPress,66,0);expectModifier(0x39,NSAlphaShiftKeyMask); + key(display,KeyRelease,66,LockMask);expectModifier(0x39,NSAlphaShiftKeyMask); + method_setImplementation(method,original); + puts("PASS Super=Command, Alt=Option, both Command keys, post-transition flags, focus-loss release, missed-release reconciliation, Caps Lock"); + [events release]; + } + return 0; +} diff --git a/tools/riot-mvg-dependency-trace.c b/tools/riot-mvg-dependency-trace.c new file mode 100644 index 0000000..ef550ce --- /dev/null +++ b/tools/riot-mvg-dependency-trace.c @@ -0,0 +1,62 @@ +/* Optional diagnostics: preserve every API result and log no buffer contents. */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +extern int csops(pid_t, unsigned int, void *, size_t); +extern int proc_pidinfo(int, int, uint64_t, void *, int); +extern int proc_pidfdinfo(int, int, int, void *, int); + +static int is_mvg(void *caller) { + Dl_info image; + return dladdr(caller, &image) && image.dli_fname && + strstr(image.dli_fname, "/mvg.framework/mvg") != NULL; +} + +static void report(const char *api, long result, int error, long operation, void *caller) { + int saved = errno; + Dl_info image; + if (dladdr(caller, &image) && image.dli_fname && strstr(image.dli_fname, "/mvg.framework/mvg")) { + char line[256]; + int length = snprintf(line, sizeof(line), + "MVG_DEP api=%s result=%ld errno=%d operation=%ld caller=0x%lx\n", + api, result, error, operation, (unsigned long)((uintptr_t)caller - (uintptr_t)image.dli_fbase)); + if (length > 0 && length < sizeof(line)) write(2, line, length); + } + errno = saved; +} +#define TRACE(type, name, parameters, arguments, operation, observe) \ + static type trace_##name parameters { \ + type result = name arguments; int saved = errno; \ + static unsigned trace_count; \ + if ((observe) && is_mvg(__builtin_return_address(0)) && __sync_fetch_and_add(&trace_count, 1) < 200) \ + report(#name, (long)result, saved, operation, __builtin_return_address(0)); \ + errno = saved; return result; \ + } \ + __attribute__((used, section("__DATA,__interpose"))) \ + static const struct { const void *replacement; const void *original; } \ + interpose_##name = { (const void *)trace_##name, (const void *)name }; + +TRACE(OSStatus, SecStaticCodeCreateWithPath, (CFURLRef path, SecCSFlags flags, SecStaticCodeRef *out), (path, flags, out), flags, 1) +TRACE(OSStatus, SecStaticCodeCreateWithPathAndAttributes, (CFURLRef path, SecCSFlags flags, CFDictionaryRef attrs, SecStaticCodeRef *out), (path, flags, attrs, out), flags, 1) +TRACE(OSStatus, SecCodeCopySigningInformation, (SecStaticCodeRef code, SecCSFlags flags, CFDictionaryRef *out), (code, flags, out), flags, 1) +TRACE(OSStatus, SecStaticCodeCheckValidity, (SecStaticCodeRef code, SecCSFlags flags, SecRequirementRef req), (code, flags, req), flags, 1) +TRACE(OSStatus, SecStaticCodeCheckValidityWithErrors, (SecStaticCodeRef code, SecCSFlags flags, SecRequirementRef req, CFErrorRef *err), (code, flags, req, err), flags, 1) +TRACE(int, csops, (pid_t pid, unsigned int op, void *data, size_t size), (pid, op, data, size), op, 1) +TRACE(int, proc_pidinfo, (int pid, int flavor, uint64_t arg, void *data, int size), (pid, flavor, arg, data, size), flavor, result <= 0) +TRACE(int, proc_pidfdinfo, (int pid, int fd, int flavor, void *data, int size), (pid, fd, flavor, data, size), flavor, result <= 0) +TRACE(int, sysctl, (int *name, u_int count, void *old, size_t *size, void *new, size_t length), (name, count, old, size, new, length), count, result < 0) +TRACE(int, sysctlbyname, (const char *name, void *old, size_t *size, void *new, size_t length), (name, old, size, new, length), 0, result < 0) +TRACE(io_service_t, IOServiceGetMatchingService, (mach_port_t port, CFDictionaryRef match), (port, match), 0, result == 0) +TRACE(kern_return_t, mach_vm_page_info, (vm_map_t task, mach_vm_address_t addr, vm_page_info_flavor_t flavor, vm_page_info_t info, mach_msg_type_number_t *count), (task, addr, flavor, info, count), flavor, result != KERN_SUCCESS) +TRACE(kern_return_t, task_info, (task_name_t task, task_flavor_t flavor, task_info_t info, mach_msg_type_number_t *count), (task, flavor, info, count), flavor, result != KERN_SUCCESS) diff --git a/tools/riot-objc-allocator-check.cpp b/tools/riot-objc-allocator-check.cpp new file mode 100644 index 0000000..365598d --- /dev/null +++ b/tools/riot-objc-allocator-check.cpp @@ -0,0 +1,22 @@ +#include +#include +#include +#include +static bool ready; +static unsigned deletes; +void* operator new(std::size_t n) { return std::malloc(n ? n : 1); } +void* operator new[](std::size_t n) { return ::operator new(n); } +void operator delete(void* p) noexcept { + if (p && !ready) { std::fputs("FAIL application delete used before main\n",stderr); std::abort(); } + if(p) ++deletes; + std::free(p); +} +void operator delete[](void* p) noexcept { ::operator delete(p); } +int main() { + ready=true; + if(objc_getClassList(nullptr,0)<=0) return 1; + void* p=::operator new(64); ::operator delete(p); + if(deletes!=1) return 2; + std::puts("PASS Objective-C startup uses private allocation; app delete remains active"); + return 0; +} diff --git a/tools/riot-opendirectory-session-check.m b/tools/riot-opendirectory-session-check.m new file mode 100644 index 0000000..4b494e6 --- /dev/null +++ b/tools/riot-opendirectory-session-check.m @@ -0,0 +1,54 @@ +/* Local directory lookups applications make at startup: default session, + * node list, local/authentication node, user record and a user query. */ +#import +#import +#include + +int main(void) +{ + @autoreleasepool { + int session = 0, names = 0, node = 0, record = 0, guid = 0, query = 0, missing = 0, raised = 0; + @try { + NSError* error = nil; + ODSession* defaultSession = [ODSession defaultSession]; + session = defaultSession != nil && defaultSession == [ODSession defaultSession]; + NSArray* nodeNames = [defaultSession nodeNamesAndReturnError:&error]; + names = [nodeNames count] == 1 && [[nodeNames firstObject] hasPrefix:@"/Local"]; + ODNode* authentication = [ODNode nodeWithSession:defaultSession type:kODNodeTypeAuthentication error:&error]; + ODNode* local = [ODNode nodeWithSession:defaultSession type:kODNodeTypeLocalNodes error:&error]; + node = authentication != nil && [[local nodeName] isEqualToString:@"/Local/Default"] + && [ODNode nodeWithSession:defaultSession name:@"/LDAPv3/missing" error:&error] == nil && error != nil; + ODRecord* user = [authentication recordWithRecordType:kODRecordTypeUsers name:NSUserName() + attributes:kODAttributeTypeRecordName error:&error]; + record = [[user recordName] isEqualToString:NSUserName()] + && [[user valuesForAttribute:@"dsAttrTypeStandard:OriginalAuthenticationAuthority" error:&error] count] == 0; + ODQuery* search = [ODQuery queryWithNode:local forRecordTypes:kODRecordTypeUsers + attribute:kODAttributeTypeRecordName matchType:kODMatchEqualTo + queryValues:NSUserName() returnAttributes:kODAttributeTypeGUID + maximumResults:1 error:&error]; + NSArray* results = [search resultsAllowingPartial:NO error:&error]; + query = [results count] == 1; + NSString* value = [[[results firstObject] valuesForAttribute:kODAttributeTypeGUID error:&error] firstObject]; + guid = [[NSUUID alloc] initWithUUIDString:value] != nil + && [value isEqualToString:[[[user valuesForAttribute:kODAttributeTypeGUID error:&error] firstObject] description]]; + ODQuery* absent = [ODQuery queryWithNode:local forRecordTypes:kODRecordTypeUsers + attribute:kODAttributeTypeRecordName matchType:kODMatchEqualTo + queryValues:@"riot-check-missing-user" returnAttributes:nil + maximumResults:0 error:&error]; + NSArray* everyone = [[ODQuery queryWithNode:local forRecordTypes:@[kODRecordTypeUsers] + attribute:nil matchType:kODMatchAny queryValues:nil + returnAttributes:nil maximumResults:0 error:&error] + resultsAllowingPartial:NO error:&error]; + missing = [[absent resultsAllowingPartial:NO error:&error] count] == 0 && [everyone count] >= 1 + && [ODQuery queryWithNode:nil forRecordTypes:kODRecordTypeUsers attribute:nil matchType:kODMatchAny + queryValues:nil returnAttributes:nil maximumResults:0 error:&error] == nil; + } @catch (NSException* exception) { + raised = 1; + printf("OPENDIRECTORY_EXCEPTION name=%s reason=%s\n", [[exception name] UTF8String], [[exception reason] UTF8String]); + } + printf("OPENDIRECTORY session=%d names=%d node=%d record=%d query=%d guid=%d missing=%d raised=%d\n", + session, names, node, record, query, guid, missing, raised); + fflush(stdout); + return !(session && names && node && record && query && guid && missing && !raised); + } +} diff --git a/tools/riot-platform-string-check.c b/tools/riot-platform-string-check.c new file mode 100644 index 0000000..2839fe6 --- /dev/null +++ b/tools/riot-platform-string-check.c @@ -0,0 +1,50 @@ +#include +#include +#include +#include +#include +#include +#include +extern int _platform_strcmp(const char*,const char*); +extern int _platform_strncmp(const char*,const char*,size_t); +extern size_t _platform_strlen(const char*); +extern size_t _platform_strnlen(const char*,size_t); +extern int _platform_memcmp(const void*,const void*,size_t); +extern void* _platform_memmove(void*,const void*,size_t); +extern void* _platform_memchr(const void*,int,size_t); +static int sign(int v){return (v>0)-(v<0);} +static void check(int v,const char* message){if(!v){fprintf(stderr,"FAIL: %s\n",message);exit(1);}} +static int compare(const unsigned char*a,const unsigned char*b,size_t n){for(size_t i=0;isrc){for(size_t i=n;i;i--)expected[dst+i-1]=expected[src+i-1];} + else{for(size_t i=0;i +#include +#include +#include +#include +#include +#include +#include + +static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t condition = PTHREAD_COND_INITIALIZER; +static unsigned counter, turn, cancel_ready; + +static void *increment(void *unused) { + (void)unused; + for (unsigned i = 0; i < 10000; ++i) { + pthread_mutex_lock(&mutex); + ++counter; + pthread_mutex_unlock(&mutex); + } + return NULL; +} + +static void *handoff(void *argument) { + unsigned side = (unsigned)(uintptr_t)argument; + for (unsigned i = 0; i < 1000; ++i) { + pthread_mutex_lock(&mutex); + while (turn != side) pthread_cond_wait(&condition, &mutex); + turn = 1 - side; + pthread_cond_signal(&condition); + pthread_mutex_unlock(&mutex); + } + return NULL; +} + +static void unlock(void *value) { pthread_mutex_unlock(value); } +static void *cancel_wait(void *unused) { + (void)unused; + pthread_mutex_lock(&mutex); + pthread_cleanup_push(unlock, &mutex); + cancel_ready = 1; + pthread_cond_signal(&condition); + for (;;) { + struct timespec deadline; + clock_gettime(CLOCK_REALTIME, &deadline); + deadline.tv_nsec += 20000000; + if (deadline.tv_nsec >= 1000000000) { ++deadline.tv_sec; deadline.tv_nsec -= 1000000000; } + pthread_cond_timedwait(&condition, &mutex, &deadline); + } + pthread_cleanup_pop(1); + return NULL; +} + +int main(void) { + Dl_info info = {0}; + int isolated = dladdr((void *)pthread_mutex_lock, &info) && info.dli_fname && + strstr(info.dli_fname, "riot-pthread-check-runtime") != NULL; + pthread_t threads[4]; + for (unsigned i = 0; i < 4; ++i) + if (pthread_create(&threads[i], NULL, increment, NULL)) return 1; + for (unsigned i = 0; i < 4; ++i) pthread_join(threads[i], NULL); + if (counter != 40000) return 2; + for (unsigned i = 0; i < 2; ++i) + if (pthread_create(&threads[i], NULL, handoff, (void *)(uintptr_t)i)) return 3; + for (unsigned i = 0; i < 2; ++i) pthread_join(threads[i], NULL); + struct timespec deadline; + clock_gettime(CLOCK_REALTIME, &deadline); + deadline.tv_nsec += 20000000; + if (deadline.tv_nsec >= 1000000000) { ++deadline.tv_sec; deadline.tv_nsec -= 1000000000; } + pthread_mutex_lock(&mutex); + int timeout = pthread_cond_timedwait(&condition, &mutex, &deadline); + pthread_mutex_unlock(&mutex); + if (timeout != ETIMEDOUT) return 4; + if (pthread_create(&threads[0], NULL, cancel_wait, NULL)) return 5; + pthread_mutex_lock(&mutex); + while (!cancel_ready) pthread_cond_wait(&condition, &mutex); + pthread_mutex_unlock(&mutex); + pthread_cancel(threads[0]); + void *result = NULL; + pthread_join(threads[0], &result); + if (result != PTHREAD_CANCELED || pthread_mutex_trylock(&mutex)) return 6; + pthread_mutex_unlock(&mutex); + mach_timebase_info_data_t timebase; + mach_timebase_info(&timebase); + uint64_t start = mach_absolute_time(); + for (unsigned i = 0; i < 100000; ++i) { + pthread_mutex_lock(&mutex); + __asm__ volatile("" : : : "memory"); + pthread_mutex_unlock(&mutex); + } + uint64_t nanoseconds = (mach_absolute_time() - start) * timebase.numer / timebase.denom; + printf("PTHREAD_INTEGRITY isolated_candidate=%d contended_increments=%u handoffs=2000 timeout=1 timed_wait_cancellation=1 ns_per_pair=%llu pass=1\n", + isolated, counter, (unsigned long long)(nanoseconds / 100000)); + return 0; +} diff --git a/tools/riot-region-count19-check.c b/tools/riot-region-count19-check.c new file mode 100644 index 0000000..ede2ac8 --- /dev/null +++ b/tools/riot-region-count19-check.c @@ -0,0 +1,22 @@ +/* What does darlingserver return for mach_vm_region_recurse with the long info count (19)? */ +#include +#include +#include +#include +#include +typedef kern_return_t (*region_fn)(vm_map_t, mach_vm_address_t*, mach_vm_size_t*, natural_t*, vm_region_recurse_info_t, mach_msg_type_number_t*); +int main(void) { + region_fn server = (region_fn) dlsym(RTLD_DEFAULT, "__darling_rpc_mach_vm_region_recurse"); + if (!server) return 1; + static int probe; + mach_vm_address_t starts[] = {0, 0x100000000ull, (mach_vm_address_t)&probe, (mach_vm_address_t)&starts, 0x7fff00000000ull, 0xffffffffff000000ull}; + for (unsigned n = 0; n < sizeof(starts)/sizeof(starts[0]); ++n) for (int cnt = 19; cnt >= 12; cnt -= 7) { + int info[24]; memset(info, 0x5a, sizeof(info)); + mach_vm_address_t a = starts[n]; mach_vm_size_t s = 0x1111; natural_t d = 77; mach_msg_type_number_t c = cnt; + kern_return_t kr = server(mach_task_self(), &a, &s, &d, (vm_region_recurse_info_t)info, &c); + printf("Q %llx cnt=%d -> kr=%d addr=%llx size=%llx depth=%u cnt=%u info:", starts[n], cnt, kr, a, s, d, c); + for (int i = 0; i < 20; ++i) printf(" %x", info[i]); + printf("\n"); + } + return 0; +} diff --git a/tools/riot-renderdoc-bridge.c b/tools/riot-renderdoc-bridge.c new file mode 100644 index 0000000..ce4423f --- /dev/null +++ b/tools/riot-renderdoc-bridge.c @@ -0,0 +1,54 @@ +/* Native ELF capture control; loaded only by explicit runtime diagnostics. */ +#include +#include +#include +#include +#include +#include +static RENDERDOC_API_1_6_0 *capture_api; +static void *capture_device; +static int active; +/* A request arms the capture; it then starts at the first submit after a presenting one, so a capture holds one whole + * frame (first submit after a present .. the next present) instead of the tail of a frame. */ +static int armed, frame_boundary; +static double next_poll; +static const char *request_path = "@PROJECT_ROOT@/build/renderdoc/capture-request"; +__attribute__((constructor)) static void init_capture(void) { + void *lib = dlopen("@PROJECT_ROOT@/build/renderdoc/usr/lib/librenderdoc.so", RTLD_NOW | RTLD_LOCAL); + pRENDERDOC_GetAPI get_api = lib ? (pRENDERDOC_GetAPI)dlsym(lib, "RENDERDOC_GetAPI") : NULL; + if (!get_api || !get_api(eRENDERDOC_API_Version_1_6_0, (void **)&capture_api)) return; + capture_api->MaskOverlayBits(0, 0); + capture_api->SetCaptureFilePathTemplate("@PROJECT_ROOT@/build/renderdoc/captures/game"); +} +static void start_armed_capture(void *instance); +void riot_capture_begin(void *instance) { + if (!capture_api || active) return; + if (armed) { start_armed_capture(instance); return; } + struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now); + double seconds = now.tv_sec + now.tv_nsec / 1e9; + if (seconds < next_poll && !getenv("RCLOL_CAPTURE_ONE_SUBMIT")) return; + next_poll = seconds + 0.25; + FILE *request = fopen(request_path, "r"); if (!request) return; + int pid = -1; int parsed = fscanf(request, "%d", &pid); fclose(request); + if (parsed != 1 || (pid != 0 && pid != getpid())) return; + unlink(request_path); + armed = 1; + if (getenv("RCLOL_CAPTURE_ONE_SUBMIT")) frame_boundary = 1; + return; +} +static void start_armed_capture(void *instance) { + if (!armed || !frame_boundary) return; + armed = 0; + capture_device = RENDERDOC_DEVICEPOINTER_FROM_VKINSTANCE(instance); + capture_api->StartFrameCapture(capture_device, NULL); + active = capture_api->IsFrameCapturing(); +} +void riot_capture_end(int presented) { + if (!capture_api) return; + if (!active) { frame_boundary = presented || getenv("RCLOL_CAPTURE_ONE_SUBMIT") != NULL; return; } + if (!presented && !getenv("RCLOL_CAPTURE_ONE_SUBMIT")) return; + unsigned result = capture_api->EndFrameCapture(capture_device, NULL); + FILE *log = fopen("@PROJECT_ROOT@/build/renderdoc/capture-status.log", "a"); + if (log) { fprintf(log, "pid=%d capture_result=%u\n", getpid(), result); fclose(log); } + active = 0; +} diff --git a/tools/riot-rpc-signal-mask-check.c b/tools/riot-rpc-signal-mask-check.c new file mode 100644 index 0000000..3c2f5f5 --- /dev/null +++ b/tools/riot-rpc-signal-mask-check.c @@ -0,0 +1,29 @@ +#include +#include +#include +#include +extern long __lolsys_syscall(long,long,long,long,long,long,int); +static long mask(int how,uint64_t* in,uint64_t* out) { + return __lolsys_syscall(how,(long)in,(long)out,sizeof(uint64_t),0,0,14); +} +int main(void) { + uint64_t original=0,desired,observed=0; + if(mask(0,NULL,&original))return 1; + desired=original|(1ull<<49); /* A valid real-time signal unsupported by BSD. */ + if(mask(2,&desired,NULL))return 2; + mach_timebase_info_data_t tb;mach_timebase_info(&tb); + uint64_t start=mach_absolute_time(); + int failed=0;unsigned completed=0; + for(unsigned cycle=0;cycle<1000;cycle++) { + mach_port_t port=MACH_PORT_NULL; + if(mach_port_allocate(mach_task_self(),MACH_PORT_RIGHT_RECEIVE,&port) || + mach_port_destroy(mach_task_self(),port)) {failed=1;break;} + if(mask(0,NULL,&observed) || observed!=desired) {failed=1;break;} + completed++; + } + uint64_t ns=(mach_absolute_time()-start)*tb.numer/tb.denom; + mask(2,&original,NULL); + printf("RPC_SIGNAL_MASK realtime_preserved=%d completed=%u ns_per_roundtrip=%llu pass=%d\n", + observed==desired,completed,(unsigned long long)(ns/(completed?completed:1)),!failed); + return failed?3:0; +} diff --git a/tools/riot-running-application-check.m b/tools/riot-running-application-check.m new file mode 100644 index 0000000..80fe5d2 --- /dev/null +++ b/tools/riot-running-application-check.m @@ -0,0 +1,39 @@ +#import +#import +#include +#include +#include + +int main(int argc, const char **argv) { + @autoreleasepool { + if (argc == 2 && strcmp(argv[1], "--no-display") == 0) { + NSRunningApplication *running = + [NSRunningApplication runningApplicationWithProcessIdentifier:getpid()]; + BOOL failedCleanly = running != nil && ![running activateWithOptions:2]; + printf("RUNNING_APP no_display_failed_cleanly=%d\n", failedCleanly); + return failedCleanly ? 0 : 1; + } + [NSApplication sharedApplication]; + NSWindow *window = [[NSWindow alloc] + initWithContentRect:NSMakeRect(40, 40, 160, 90) + styleMask:NSTitledWindowMask + backing:NSBackingStoreBuffered + defer:NO]; + [window makeKeyAndOrderFront:nil]; + + pid_t ownPID = getpid(); + NSRunningApplication *running = + [NSRunningApplication runningApplicationWithProcessIdentifier:ownPID]; + BOOL found = running != nil && [running processIdentifier] == ownPID && + ![running isTerminated]; + BOOL activated = [running activateWithOptions:2]; + BOOL missing = [NSRunningApplication + runningApplicationWithProcessIdentifier:2147483647] == nil; + + printf("RUNNING_APP found=%d activated=%d missing=%d\n", + found, activated, missing); + [window close]; + [window release]; + return found && activated && missing ? 0 : 1; + } +} diff --git a/tools/riot-scroll-event-check.m b/tools/riot-scroll-event-check.m new file mode 100644 index 0000000..4b9a064 --- /dev/null +++ b/tools/riot-scroll-event-check.m @@ -0,0 +1,41 @@ +#import +#import +#import +#include +#include + +@interface NSEvent (ScrollProbe) ++ (id)mouseEventWithType:(NSEventType)type location:(NSPoint)point + modifierFlags:(NSUInteger)flags window:(id)window clickCount:(NSInteger)count + deltaX:(CGFloat)x deltaY:(CGFloat)y; +- (NSUInteger)phase; +- (NSUInteger)momentumPhase; +- (BOOL)hasPreciseScrollingDeltas; +- (CGFloat)scrollingDeltaX; +- (CGFloat)scrollingDeltaY; +- (CGEventRef)CGEvent; +@end + +int main(void) { + NSAutoreleasePool *pool=[NSAutoreleasePool new]; + CGFloat axes[][2]={{0,1},{0,-1},{1,0},{-1,0}}; + for(unsigned i=0;i<4;++i) { + NSEvent *event=[NSEvent mouseEventWithType:NSScrollWheel + location:NSMakePoint(12,24) modifierFlags:0 window:nil + clickCount:0 deltaX:axes[i][0] deltaY:axes[i][1]]; + assert([event respondsToSelector:@selector(CGEvent)]); + assert([event respondsToSelector:@selector(phase)]); + assert([event phase]==0 && [event momentumPhase]==0); + assert(![event hasPreciseScrollingDeltas]); + assert([event scrollingDeltaX]==axes[i][0]); + assert([event scrollingDeltaY]==axes[i][1]); + CGEventRef cg=[event CGEvent]; + assert(cg && CGEventGetType(cg)==kCGEventScrollWheel); + assert(CGEventGetIntegerValueField(cg,kCGScrollWheelEventIsContinuous)==0); + assert(CGEventGetIntegerValueField(cg,kCGScrollWheelEventDeltaAxis1)==axes[i][1]); + assert(CGEventGetIntegerValueField(cg,kCGScrollWheelEventDeltaAxis2)==axes[i][0]); + } + puts("SCROLL_EVENT vertical=pass horizontal=pass phases=none cg_notches=pass"); + [pool drain]; + return 0; +} diff --git a/tools/riot-sectrust-chain-check.c b/tools/riot-sectrust-chain-check.c new file mode 100644 index 0000000..aba4262 --- /dev/null +++ b/tools/riot-sectrust-chain-check.c @@ -0,0 +1,43 @@ +/* Time a SecTrust evaluation of a real server chain (leaf first) for a host. */ +#include +#include +#include +#include + +int main(int argc, char** argv) +{ + if (argc < 3) return 3; + CFStringRef host = CFStringCreateWithCString(NULL, argv[1], kCFStringEncodingUTF8); + CFMutableArrayRef certificates = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks); + for (int i = 2; i < argc; i++) { + static unsigned char bytes[16384]; + FILE* file = fopen(argv[i], "rb"); + if (!file) return 3; + size_t size = fread(bytes, 1, sizeof(bytes), file); + fclose(file); + CFDataRef data = CFDataCreate(NULL, bytes, size); + SecCertificateRef certificate = SecCertificateCreateWithData(NULL, data); + if (certificate) CFArrayAppendValue(certificates, certificate); + } + SecPolicyRef policy = SecPolicyCreateSSL(true, host); + SecTrustRef trust = NULL; + CFTypeRef policies = policy; + if (getenv("CHAIN_REVOCATION")) { + SecPolicyRef revocation = SecPolicyCreateRevocation(kSecRevocationUseAnyAvailableMethod | kSecRevocationRequirePositiveResponse * (getenv("CHAIN_REVOCATION")[0] == '2')); + const void* values[] = { policy, revocation }; + policies = CFArrayCreate(NULL, values, 2, &kCFTypeArrayCallBacks); + } + OSStatus created = SecTrustCreateWithCertificates(certificates, policies, &trust); + CFAbsoluteTime start = CFAbsoluteTimeGetCurrent(); + SecTrustResultType result = kSecTrustResultInvalid; + OSStatus evaluated = trust ? SecTrustEvaluate(trust, &result) : -1; + double first = CFAbsoluteTimeGetCurrent() - start; + start = CFAbsoluteTimeGetCurrent(); + CFArrayRef anchors = NULL; + OSStatus anchorStatus = SecTrustCopyAnchorCertificates(&anchors); + double anchorTime = CFAbsoluteTimeGetCurrent() - start; + printf("SECCHAIN certificates=%ld create=%d evaluate=%d result=%d seconds=%.2f anchors_status=%d anchors=%ld anchor_seconds=%.2f\n", + (long)CFArrayGetCount(certificates), (int)created, (int)evaluated, (int)result, first, + (int)anchorStatus, anchors ? (long)CFArrayGetCount(anchors) : -1L, anchorTime); + return !(evaluated == 0); +} diff --git a/tools/riot-sectrust-selfsigned-check.c b/tools/riot-sectrust-selfsigned-check.c new file mode 100644 index 0000000..ea599f8 --- /dev/null +++ b/tools/riot-sectrust-selfsigned-check.c @@ -0,0 +1,167 @@ +/* SecTrust evaluation of an untrusted self-signed server certificate must + * complete and report a trust failure, not fail as an API error. */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static int run(int argc, char** argv) +{ + unsigned char bytes[8192]; + FILE* file = argc >= 2 ? fopen(argv[1], "rb") : NULL; + if (!file) return 3; + size_t size = fread(bytes, 1, sizeof(bytes), file); + fclose(file); + CFDataRef data = CFDataCreate(NULL, bytes, size); + SecCertificateRef certificate = SecCertificateCreateWithData(NULL, data); + printf("SECTRUST certificate=%d\n", certificate != NULL); + fflush(stdout); + if (!certificate) return 1; + const void* values[] = { certificate }; + CFArrayRef certificates = CFArrayCreate(NULL, values, 1, &kCFTypeArrayCallBacks); + SecPolicyRef policy = SecPolicyCreateSSL(true, CFSTR("127.0.0.1")); + SecTrustRef trust = NULL; + OSStatus created = SecTrustCreateWithCertificates(certificates, policy, &trust); + printf("SECTRUST policy=%d create=%d trust=%d\n", policy != NULL, (int)created, trust != NULL); + fflush(stdout); + if (created != 0 || !trust) return 1; + SecTrustResultType result = kSecTrustResultInvalid; + OSStatus evaluated = SecTrustEvaluate(trust, &result); + printf("SECTRUST evaluate=%d result=%d\n", (int)evaluated, (int)result); + fflush(stdout); + /* The legacy result accessors a Chromium 108 era verifier reads after evaluating. */ + CFArrayRef chain = NULL; + CSSM_TP_APPLE_EVIDENCE_INFO* chainInfo = NULL; + SecTrustResultType legacyResult = kSecTrustResultInvalid; + OSStatus legacy = SecTrustGetResult(trust, &legacyResult, &chain, &chainInfo); + printf("SECTRUST get_result=%d chain=%ld info=%d status_bits=%#x\n", (int)legacy, + chain ? (long)CFArrayGetCount(chain) : -1L, chainInfo != NULL, chainInfo ? (unsigned)chainInfo[0].StatusBits : 0); + fflush(stdout); + if (chain && CFArrayGetCount(chain) > 0) { + /* A verifier inspects the returned chain through the legacy CL handle. */ + SecCertificateRef chainCertificate = (SecCertificateRef)CFArrayGetValueAtIndex(chain, 0); + CSSM_CL_HANDLE chainHandle = 0; + OSStatus chainStatus = SecCertificateGetCLHandle(chainCertificate, &chainHandle); + printf("SECTRUST chain_cl_handle=%d same_object=%d\n", (int)chainStatus, chainCertificate == certificate); + fflush(stdout); + } + OSStatus cssmResult = 0; + OSStatus cssm = SecTrustGetCssmResultCode(trust, &cssmResult); + printf("SECTRUST cssm_status=%d cssm_result=%d\n", (int)cssm, (int)cssmResult); + fflush(stdout); + SecPolicyRef revocation = SecPolicyCreateRevocation(kSecRevocationUseAnyAvailableMethod); + SecPolicyRef basic = SecPolicyCreateBasicX509(); + printf("SECTRUST revocation_policy=%d basic_policy=%d\n", revocation != NULL, basic != NULL); + fflush(stdout); + CFArrayRef keychains = CFArrayCreate(NULL, NULL, 0, &kCFTypeArrayCallBacks); + SecTrustRef second = NULL; + const void* policies[] = { basic, policy }; + CFArrayRef policyArray = CFArrayCreate(NULL, policies, 2, &kCFTypeArrayCallBacks); + OSStatus secondCreate = SecTrustCreateWithCertificates(certificates, policyArray, &second); + OSStatus setKeychains = second ? SecTrustSetKeychains(second, keychains) : -1; + SecTrustResultType secondResult = kSecTrustResultInvalid; + OSStatus secondEvaluate = second ? SecTrustEvaluate(second, &secondResult) : -1; + printf("SECTRUST second_create=%d set_keychains=%d evaluate=%d result=%d\n", (int)secondCreate, + (int)setKeychains, (int)secondEvaluate, (int)secondResult); + fflush(stdout); + /* Revocation policy built through the CSSM policy search, with CRL options. */ + SecPolicySearchRef search = NULL; + OSStatus searchStatus = SecPolicySearchCreate(CSSM_CERT_X_509v3, &CSSMOID_APPLE_TP_REVOCATION_CRL, NULL, &search); + SecPolicyRef crlPolicy = NULL; + OSStatus nextStatus = search ? SecPolicySearchCopyNext(search, &crlPolicy) : -1; + /* CSSM_APPLE_TP_CRL_OPTIONS, version 0 */ + struct { uint32_t Version; uint32_t CrlFlags; void* crlStore; } crlOptions; + memset(&crlOptions, 0, sizeof(crlOptions)); + CSSM_DATA optionsData = { sizeof(crlOptions), (uint8_t*)&crlOptions }; + OSStatus setValue = crlPolicy ? SecPolicySetValue(crlPolicy, &optionsData) : -1; + printf("SECTRUST policy_search=%d copy_next=%d set_value=%d\n", (int)searchStatus, (int)nextStatus, (int)setValue); + fflush(stdout); + if (crlPolicy) { + const void* legacyPolicies[] = { basic, policy, crlPolicy }; + CFArrayRef legacyArray = CFArrayCreate(NULL, legacyPolicies, 3, &kCFTypeArrayCallBacks); + SecTrustRef third = NULL; + OSStatus thirdCreate = SecTrustCreateWithCertificates(certificates, legacyArray, &third); + SecTrustResultType thirdResult = kSecTrustResultInvalid; + OSStatus thirdEvaluate = third ? SecTrustEvaluate(third, &thirdResult) : -1; + OSStatus thirdCssm = 0; + OSStatus thirdCode = third ? SecTrustGetCssmResultCode(third, &thirdCssm) : -1; + printf("SECTRUST crl_trust_create=%d evaluate=%d result=%d cssm_status=%d cssm_result=%d\n", (int)thirdCreate, + (int)thirdEvaluate, (int)thirdResult, (int)thirdCode, (int)thirdCssm); + fflush(stdout); + } + /* Remaining legacy calls a Chromium 108 verifier makes. */ + CFArrayRef searchList = NULL; + OSStatus searchListStatus = SecKeychainCopySearchList(&searchList); + printf("SECTRUST keychain_search_list=%d count=%ld\n", (int)searchListStatus, searchList ? (long)CFArrayGetCount(searchList) : -1L); + fflush(stdout); + SecTrustRef fourth = NULL; + SecTrustCreateWithCertificates(certificates, policyArray, &fourth); + OSStatus parameters = fourth ? SecTrustSetParameters(fourth, CSSM_TP_ACTION_DEFAULT, NULL) : -1; + OSStatus anchorsOnly = fourth ? SecTrustSetAnchorCertificatesOnly(fourth, false) : -1; + SecTrustResultType fourthResult = kSecTrustResultInvalid; + OSStatus fourthEvaluate = fourth ? SecTrustEvaluate(fourth, &fourthResult) : -1; + printf("SECTRUST set_parameters=%d anchors_only=%d evaluate=%d result=%d\n", (int)parameters, (int)anchorsOnly, + (int)fourthEvaluate, (int)fourthResult); + fflush(stdout); + CSSM_CL_HANDLE clHandle = 0; + OSStatus clStatus = SecCertificateGetCLHandle(certificate, &clHandle); + CSSM_DATA certData = { 0, NULL }; + OSStatus dataStatus = SecCertificateGetData(certificate, &certData); + CSSM_HANDLE cached = 0; + CSSM_RETURN cacheStatus = clStatus == 0 ? CSSM_CL_CertCache(clHandle, &certData, &cached) : -1; + printf("SECTRUST cl_handle=%d get_data=%d cert_cache=%d\n", (int)clStatus, (int)dataStatus, (int)cacheStatus); + fflush(stdout); + if (cacheStatus == 0) { + uint32 fields = 0; + CSSM_HANDLE results = 0; + CSSM_DATA_PTR field = NULL; + CSSM_RETURN fieldStatus = CSSM_CL_CertGetFirstCachedFieldValue(clHandle, cached, &CSSMOID_X509V1SignatureAlgorithm, + &results, &fields, &field); + printf("SECTRUST cached_field=%d fields=%u\n", (int)fieldStatus, (unsigned)fields); + fflush(stdout); + } + CFArrayRef domainCerts = NULL; + OSStatus domainStatus = SecTrustSettingsCopyCertificates(kSecTrustSettingsDomainUser, &domainCerts); + printf("SECTRUST settings_certificates=%d\n", (int)domainStatus); + fflush(stdout); + CFArrayRef settings = NULL; + OSStatus settingsStatus = SecTrustSettingsCopyTrustSettings(certificate, kSecTrustSettingsDomainUser, &settings); + printf("SECTRUST trust_settings=%d\n", (int)settingsStatus); + fflush(stdout); + CFArrayRef anchors = NULL; + OSStatus anchorStatus = SecTrustCopyAnchorCertificates(&anchors); + printf("SECTRUST anchors_status=%d anchors=%ld\n", (int)anchorStatus, anchors ? (long)CFArrayGetCount(anchors) : -1L); + return !(evaluated == 0 && (result == kSecTrustResultRecoverableTrustFailure || result == kSecTrustResultDeny || result == kSecTrustResultFatalTrustFailure)); +} + +#include +#include +static int threadArgc; +static char** threadArgv; +static void* threadMain(void* unused) +{ + int result = run(threadArgc, threadArgv); + fflush(stdout); + _exit(result); +} + +int main(int argc, char** argv) +{ + if (argc >= 3) { + pthread_t thread; + void* result = NULL; + threadArgc = argc; threadArgv = argv; + pthread_create(&thread, NULL, threadMain, NULL); + /* the main thread services its run loop, as an application's does */ + CFRunLoopRun(); + return 2; + } + return run(argc, argv); +} diff --git a/tools/riot-select-backoff-check.c b/tools/riot-select-backoff-check.c new file mode 100644 index 0000000..d4b7765 --- /dev/null +++ b/tools/riot-select-backoff-check.c @@ -0,0 +1,42 @@ +#include +#include +#include +#include +#include + +/* Run once normally and once from an executable named LeagueClient. + * A sustained empty poll must remain immediate for ordinary applications; + * the client workaround must still reduce busy polling and preserve readiness. */ +int main(int argc, char **argv) +{ + int client = argc > 1 && atoi(argv[1]); + int fds[2]; + if (pipe(fds)) return 1; + mach_timebase_info_data_t tb; + mach_timebase_info(&tb); + uint64_t start = mach_absolute_time(); + for (unsigned i = 0; i < 25000; ++i) { + fd_set readable; + FD_ZERO(&readable); FD_SET(fds[0], &readable); + struct timeval timeout = {0, 0}; + if (select(fds[0] + 1, &readable, NULL, NULL, &timeout) != 0 || + FD_ISSET(fds[0], &readable)) return 2; + } + double seconds = (mach_absolute_time() - start) * (double)tb.numer / tb.denom / 1e9; + if ((!client && seconds > 1) || (client && seconds < 1)) { + printf("SELECT_BACKOFF client=%d empty_seconds=%.3f pass=0\n", client, seconds); + return 3; + } + for (unsigned i = 0; i < 100; ++i) { + char byte = 'x'; + if (write(fds[1], &byte, 1) != 1) return 4; + fd_set readable; + FD_ZERO(&readable); FD_SET(fds[0], &readable); + struct timeval timeout = {0, 0}; + if (select(fds[0] + 1, &readable, NULL, NULL, &timeout) != 1 || + !FD_ISSET(fds[0], &readable) || read(fds[0], &byte, 1) != 1) return 5; + } + close(fds[0]); close(fds[1]); + printf("SELECT_BACKOFF client=%d empty_seconds=%.3f readiness=pass pass=1\n", client, seconds); + return 0; +} diff --git a/tools/riot-syscall-fastpath-check.c b/tools/riot-syscall-fastpath-check.c new file mode 100644 index 0000000..7feb54c --- /dev/null +++ b/tools/riot-syscall-fastpath-check.c @@ -0,0 +1,19 @@ +#include +#include +#include +extern long __lolsys_syscall(long,long,long,long,long,long,int); +extern void __lolsys_set_wake_trace(int); +int main(void) { + mach_timebase_info_data_t tb;mach_timebase_info(&tb); + for(unsigned trace=0;trace<2;trace++) { + __lolsys_set_wake_trace(trace); + uint64_t start=mach_absolute_time(); + for(unsigned i=0;i<100000;i++) { + /* Linux poll(NULL, 0, 0): exercise the traced wait path without IO. */ + if(__lolsys_syscall(0,0,0,0,0,0,7)!=0) return 1; + } + uint64_t ns=(mach_absolute_time()-start)*tb.numer/tb.denom; + printf("SYSCALL_WAIT trace=%u iterations=100000 ns_per_call=%llu pass=1\n",trace,(unsigned long long)(ns/100000)); + } + __lolsys_set_wake_trace(0);return 0; +} diff --git a/tools/riot-sysctl-cache-check.c b/tools/riot-sysctl-cache-check.c new file mode 100644 index 0000000..189e318 --- /dev/null +++ b/tools/riot-sysctl-cache-check.c @@ -0,0 +1,25 @@ +/* hw.cacheconfig / hw.cachesize as macOS lays them out: 10 uint64 entries. */ +#include +#include +#include +#include + +static int dump(const char* name) +{ + uint64_t v[10]; + size_t probed = 0, got = sizeof(v); + int probe_rv = sysctlbyname(name, NULL, &probed, NULL, 0); + memset(v, 0, sizeof(v)); + int rv = sysctlbyname(name, v, &got, NULL, 0); + printf("SYSCTL_CACHE name=%s probe_rv=%d probed=%zu rv=%d got=%zu [", name, probe_rv, probed, rv, got); + for (int i = 0; i < 5; i++) + printf("%s%llu", i ? " " : "", (unsigned long long)v[i]); + int ok = probe_rv == 0 && rv == 0 && probed == sizeof(v) && got == sizeof(v) && v[0] != 0; + printf("] %s\n", ok ? "ok" : "FAIL"); + return !ok; +} + +int main(void) +{ + return dump("hw.cacheconfig") | dump("hw.cachesize"); +} diff --git a/tools/riot-sysctl-string-check.c b/tools/riot-sysctl-string-check.c new file mode 100644 index 0000000..cab723a --- /dev/null +++ b/tools/riot-sysctl-string-check.c @@ -0,0 +1,37 @@ +/* Probe-then-fetch string sysctls the way callers size a stack buffer. */ +#include +#include +#include +#include + +static int check(const char* name) +{ + size_t probed = (size_t)-1; + int rv = sysctlbyname(name, NULL, &probed, NULL, 0); + if (rv != 0 || probed == (size_t)-1 || probed == 0 || probed > 256) { + printf("SYSCTL_STRING name=%s probe_rv=%d errno=%d len=%zu FAIL\n", name, rv, errno, probed); + return 1; + } + char value[probed]; + size_t got = probed; + memset(value, 'X', probed); + rv = sysctlbyname(name, value, &got, NULL, 0); + int ok = rv == 0 && got == probed && value[probed - 1] == 0 && strlen(value) == probed - 1; + printf("SYSCTL_STRING name=%s len=%zu value=%.*s %s\n", name, got, (int)probed, value, ok ? "ok" : "FAIL"); + return !ok; +} + +int main(void) +{ + int failed = check("hw.model") | check("hw.machine") | check("kern.ostype") | check("kern.osrelease"); + int mib[2] = { CTL_HW, HW_MODEL }; + char model[64]; + size_t length = sizeof(model); + int by_mib = sysctl(mib, 2, model, &length, NULL, 0) == 0 && length == strlen(model) + 1; + size_t frequency_length = 0; + int frequency = sysctlbyname("hw.cpufrequency", NULL, &frequency_length, NULL, 0); + int missing = sysctlbyname("hw.riot_check_missing", NULL, &length, NULL, 0) == -1 && errno == ENOENT; + printf("SYSCTL_STRING_RESULT strings=%d mib=%d frequency_probe=%d missing=%d\n", + !failed, by_mib, frequency == 0 && frequency_length == sizeof(int), missing); + return failed || !by_mib || !missing; +} diff --git a/tools/riot-task-info-check.c b/tools/riot-task-info-check.c new file mode 100644 index 0000000..8052284 --- /dev/null +++ b/tools/riot-task-info-check.c @@ -0,0 +1,18 @@ +#include +#include +int main(void) { + unsigned completed=0; + for (unsigned i=0;i<100;i++) { + integer_t data[256]={0};mach_msg_type_number_t count=256; + kern_return_t kr=task_info(mach_task_self(),TASK_POWER_INFO,data,&count); + if(kr!=KERN_INVALID_ARGUMENT)return 1; + count=256; + kr=task_info(mach_task_self(),0x7fffffff,data,&count); + if(kr!=KERN_INVALID_ARGUMENT)return 2; + task_basic_info_64_data_t basic;count=TASK_BASIC_INFO_64_COUNT; + if(task_info(mach_task_self(),TASK_BASIC_INFO_64,(task_info_t)&basic,&count)!=KERN_SUCCESS)return 3; + completed++; + } + printf("TASK_INFO unsupported_error=1 supported_ok=1 completed=%u pass=1\n",completed); + return 0; +} diff --git a/tools/riot-trust-db-check.c b/tools/riot-trust-db-check.c new file mode 100644 index 0000000..4456a95 --- /dev/null +++ b/tools/riot-trust-db-check.c @@ -0,0 +1,97 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +extern void *objc_autoreleasePoolPush(void); +extern void objc_autoreleasePoolPop(void *); + +int main(int argc, char **argv) +{ + void *pool = objc_autoreleasePoolPush(); + if (argc >= 2) { + SecSetCustomHomeURLString(CFSTR("/tmp/riot-trust-cache-fixture")); + FILE *fixture = fopen(argv[1], "rb"); + unsigned char bytes[4096]; + if (!fixture) return 3; + size_t size = fread(bytes, 1, sizeof(bytes), fixture); + fclose(fixture); + CFDataRef data = CFDataCreate(NULL, bytes, size); + SecCertificateRef certificate = SecCertificateCreateWithData(NULL, data); + CFRelease(data); + if (!certificate) return 4; + if (argc == 3) { + SecPolicyRef policy = SecPolicyCreateBasicX509(); + SecTrustRef trust = NULL; + OSStatus status = SecTrustCreateWithCertificates(certificate, policy, &trust); + SecTrustResultType result = kSecTrustResultInvalid; + if (status == errSecSuccess) status = SecTrustEvaluate(trust, &result); + printf("TRUST_EVALUATE status=%d result=%u\n", (int)status, (unsigned)result); + if (trust) CFRelease(trust); + CFRelease(policy); + CFRelease(certificate); + return status == errSecSuccess ? 0 : 7; + } + SecOCSPRequestRef request = SecOCSPRequestCreate(certificate, certificate); + if (!request) return 5; + for (unsigned i = 0; i < 10; i++) { + SecOCSPResponseRef response = SecOCSPCacheCopyMatching(request, NULL); + if (response) return 6; // The fixture's cache must be empty. + printf("OCSP_CACHE empty_lookup=%u\n", i + 1); + fflush(stdout); + } + SecOCSPCacheCloseDB(); + SecOCSPRequestFinalize(request); + CFRelease(certificate); + printf("OCSP_CACHE lookups=10 pass=1\n"); + return 0; + } + const char *path = "/tmp/riot-trust-db-check.sqlite3"; + unlink(path); + _SecDbServerSetup(); + SecDbRef db = SecDbCreate(CFSTR("/tmp/riot-trust-db-check.sqlite3"), + 0600, true, true, true, true, 1, + ^bool(SecDbRef owner, SecDbConnectionRef connection, bool created, + bool *again, CFErrorRef *error) { + __block bool ok = true; + if (created) { + ok = SecDbTransaction(connection, kSecDbExclusiveTransactionType, error, ^(bool *commit) { + ok = SecDbExec(connection, CFSTR("CREATE TABLE fixture(value INTEGER)"), error); + *commit = ok; + }); + } + return ok; + }); + if (!db) return 1; + __block unsigned reads = 0; + CFErrorRef error = NULL; + __block int expected_sql_error = 0; + SecDbPerformWrite(db, &error, ^(SecDbConnectionRef connection) { + CFErrorRef sql_error = NULL; + if (!SecDbWithSQL(connection, CFSTR("SELECT missing_column FROM missing_table"), &sql_error, NULL) + && sql_error) { + expected_sql_error = 1; + for (unsigned i = 0; i < 1000; i++) { + CFStringRef text = CFStringCreateWithFormat(NULL, NULL, CFSTR("fixture: %@"), sql_error); + CFRelease(text); + } + CFRelease(sql_error); + } + }); + for (unsigned i = 0; i < 10; i++) { + if (!SecDbPerformRead(db, &error, ^(SecDbConnectionRef connection) { + if (connection) reads++; + })) break; + } + CFRelease(db); + if (error) CFRelease(error); + unlink(path); + objc_autoreleasePoolPop(pool); + printf("TRUST_DB sql_error_released=%d reads=%u pass=%d\n", expected_sql_error, reads, + expected_sql_error && reads == 10); + return expected_sql_error && reads == 10 ? 0 : 2; +} diff --git a/tools/riot-tsc-offset-check.c b/tools/riot-tsc-offset-check.c new file mode 100644 index 0000000..f95e9af --- /dev/null +++ b/tools/riot-tsc-offset-check.c @@ -0,0 +1,35 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +static uint64_t now(void) { struct timespec t; clock_gettime(CLOCK_MONOTONIC_RAW,&t); return t.tv_sec*1000000000ULL+t.tv_nsec; } +struct sample { uint64_t tsc, clock, gap; }; +static struct sample sample(void) { + struct sample best={0,0,UINT64_MAX}; + for(int i=0;i<100;++i) { + uint64_t a=now(); unsigned aux; uint64_t t=__rdtscp(&aux); uint64_t b=now(); + if(b-a +#include +#include +#include +#include +#include +#include +#include + +extern int __ulock_wait2(uint32_t, void *, uint64_t, uint64_t, uint64_t); +static int syscall_checks(void) { + uint32_t word = 0; + errno = EDOM; + if (__ulock_wait2(1u | 0x01000000u, &word, 1, 1, 0) < 0 || errno != EDOM) return 10; + if (__ulock_wait2(1u | 0x01000000u, &word, 0, 1000000, 0) != -ETIMEDOUT || errno != EDOM) return 11; + if (__ulock_wait2(1u | 0x01000000u, (char*)&word + 1, 0, 1, 0) != -EINVAL || errno != EDOM) return 12; + if (__ulock_wait2(1u | 0x01000000u, &word, 0, 1, 1) != -EINVAL || errno != EDOM) return 13; + if (__ulock_wait2(0x80000001u | 0x01000000u, &word, 0, 1, 0) != -EINVAL || errno != EDOM) return 14; + if (__ulock_wait2(1u, &word, 0, 1000000, 0) != -1 || errno != ETIMEDOUT) return 15; + return 0; +} + +static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t condition = PTHREAD_COND_INITIALIZER; +static unsigned counter, turn, cancel_ready; +static unsigned broadcast_ready, broadcast_go; +static void *broadcast_wait(void *unused) { + pthread_mutex_lock(&mutex); + ++broadcast_ready; + pthread_cond_broadcast(&condition); + while (!broadcast_go) pthread_cond_wait(&condition, &mutex); + pthread_mutex_unlock(&mutex); + return NULL; +} + +static void *increment(void *unused) { + (void)unused; + for (unsigned i = 0; i < 10000; ++i) { + pthread_mutex_lock(&mutex); + ++counter; + pthread_mutex_unlock(&mutex); + } + return NULL; +} + +static void *handoff(void *argument) { + unsigned side = (unsigned)(uintptr_t)argument; + for (unsigned i = 0; i < 1000; ++i) { + pthread_mutex_lock(&mutex); + while (turn != side) pthread_cond_wait(&condition, &mutex); + turn = 1 - side; + pthread_cond_signal(&condition); + pthread_mutex_unlock(&mutex); + } + return NULL; +} + +static void unlock(void *value) { pthread_mutex_unlock(value); } +static void *cancel_wait(void *unused) { + (void)unused; + pthread_mutex_lock(&mutex); + pthread_cleanup_push(unlock, &mutex); + cancel_ready = 1; + pthread_cond_signal(&condition); + for (;;) { + pthread_cond_wait(&condition, &mutex); + } + pthread_cleanup_pop(1); + return NULL; +} + +int main(void) { + int checks = syscall_checks(); if (checks) return checks; + Dl_info info = {0}; + int isolated = dladdr((void *)pthread_mutex_lock, &info) && info.dli_fname && + strstr(info.dli_fname, "riot-pthread-check-runtime") != NULL; + pthread_t threads[4]; + for (unsigned i = 0; i < 4; ++i) + if (pthread_create(&threads[i], NULL, increment, NULL)) return 1; + for (unsigned i = 0; i < 4; ++i) pthread_join(threads[i], NULL); + if (counter != 40000) return 2; + for (unsigned i = 0; i < 2; ++i) + if (pthread_create(&threads[i], NULL, handoff, (void *)(uintptr_t)i)) return 3; + for (unsigned i = 0; i < 2; ++i) pthread_join(threads[i], NULL); + struct timespec deadline; + clock_gettime(CLOCK_REALTIME, &deadline); + deadline.tv_nsec += 20000000; + if (deadline.tv_nsec >= 1000000000) { ++deadline.tv_sec; deadline.tv_nsec -= 1000000000; } + pthread_mutex_lock(&mutex); + int timeout = pthread_cond_timedwait(&condition, &mutex, &deadline); + pthread_mutex_unlock(&mutex); + if (timeout != ETIMEDOUT) return 4; + /* Polling may spuriously wake a timed waiter, but must not report a + * timeout before its absolute deadline. */ + clock_gettime(CLOCK_REALTIME, &deadline); + deadline.tv_nsec += 150000000; + if (deadline.tv_nsec >= 1000000000) { ++deadline.tv_sec; deadline.tv_nsec -= 1000000000; } + unsigned spurious = 0; + pthread_mutex_lock(&mutex); + do { + timeout = pthread_cond_timedwait(&condition, &mutex, &deadline); + if (timeout == 0) ++spurious; + } while (timeout == 0); + pthread_mutex_unlock(&mutex); + struct timespec now; + clock_gettime(CLOCK_REALTIME, &now); + if (timeout != ETIMEDOUT || now.tv_sec < deadline.tv_sec || + (now.tv_sec == deadline.tv_sec && now.tv_nsec < deadline.tv_nsec)) return 16; + printf("LONG_TIMEOUT spurious_wakes=%u deadline_preserved=1\n", spurious); + uint32_t signature; memcpy(&signature, &condition, sizeof(signature)); + printf("COND_SIGNATURE=%08x\n", signature); fflush(stdout); + if (signature != 0x434f4e46) return 7; + if (pthread_create(&threads[0], NULL, cancel_wait, NULL)) return 5; + pthread_mutex_lock(&mutex); + while (!cancel_ready) pthread_cond_wait(&condition, &mutex); + pthread_mutex_unlock(&mutex); + pthread_cancel(threads[0]); + void *result = NULL; + pthread_join(threads[0], &result); + if (result != PTHREAD_CANCELED || pthread_mutex_trylock(&mutex)) return 6; + pthread_mutex_unlock(&mutex); + pthread_t sleepers[8]; + for (unsigned i=0; i<8; ++i) + if (pthread_create(&sleepers[i], NULL, broadcast_wait, NULL)) return 8; + pthread_mutex_lock(&mutex); + while (broadcast_ready != 8) pthread_cond_wait(&condition, &mutex); + broadcast_go = 1; + pthread_cond_broadcast(&condition); + pthread_mutex_unlock(&mutex); + for (unsigned i=0; i<8; ++i) pthread_join(sleepers[i], NULL); + if (pthread_cond_destroy(&condition)) return 9; + mach_timebase_info_data_t timebase; + mach_timebase_info(&timebase); + uint64_t start = mach_absolute_time(); + for (unsigned i = 0; i < 100000; ++i) { + pthread_mutex_lock(&mutex); + __asm__ volatile("" : : : "memory"); + pthread_mutex_unlock(&mutex); + } + uint64_t nanoseconds = (mach_absolute_time() - start) * timebase.numer / timebase.denom; + printf("PTHREAD_INTEGRITY isolated_candidate=%d contended_increments=%u handoffs=2000 timeout=1 untimed_wait_cancellation=1 broadcast=8 destroy=1 syscall_errors=6 ns_per_pair=%llu pass=1\n", + isolated, counter, (unsigned long long)(nanoseconds / 100000)); + return 0; +} diff --git a/tools/riot-urlsession-class-check.m b/tools/riot-urlsession-class-check.m new file mode 100644 index 0000000..38d9029 --- /dev/null +++ b/tools/riot-urlsession-class-check.m @@ -0,0 +1,24 @@ +/* How NSURLSessionConfiguration behaves on this platform, probed only through + * runtime lookups so a broken class cannot abort the probe before it reports. */ +#import +#include +#include + +int main(void) +{ + @autoreleasepool { + Class byName = objc_getClass("NSURLSessionConfiguration"); + Class lookup = NSClassFromString(@"NSURLSessionConfiguration"); + printf("URLSESSION getClass=%d classFromString=%d\n", byName != Nil, lookup != Nil); + fflush(stdout); + Method method = byName ? class_getClassMethod(byName, @selector(ephemeralSessionConfiguration)) : NULL; + Method defaults = byName ? class_getClassMethod(byName, @selector(defaultSessionConfiguration)) : NULL; + printf("URLSESSION ephemeral_method=%d default_method=%d name=%s\n", method != NULL, defaults != NULL, + byName ? class_getName(byName) : "-"); + fflush(stdout); + Class session = objc_getClass("NSURLSession"); + printf("URLSESSION session_class=%d\n", session != Nil); + fflush(stdout); + } + return 0; +} diff --git a/tools/riot-vnode-close-check.c b/tools/riot-vnode-close-check.c new file mode 100644 index 0000000..a3dfa6b --- /dev/null +++ b/tools/riot-vnode-close-check.c @@ -0,0 +1,26 @@ +#include +#include +#include +#include +#include +int main(void) { + char path[]="/tmp/riot-vnode-check-XXXXXX";int fd=mkstemp(path),kq=kqueue(); + if(fd<0||kq<0)return 1; + struct timespec immediate={0,0}; + struct kevent64_s change={.ident=fd,.filter=EVFILT_VNODE, + .flags=EV_ADD|EV_CLEAR|EV_DISPATCH2,.fflags=NOTE_WRITE|NOTE_EXTEND,.udata=123},event={0}; + if(kevent64(kq,&change,1,NULL,0,0,&immediate))return 2; + for(unsigned cycle=0;cycle<100;cycle++) { + int other=open(path,O_RDONLY);if(other<0)return 3;close(other); + if(kevent64(kq,NULL,0,&event,1,0,&immediate)!=0) return 4; + if(write(fd,"x",1)!=1)return 5; + int n=kevent64(kq,NULL,0,&event,1,0,&immediate); + if(n!=1 || event.filter!=EVFILT_VNODE || !(event.fflags&NOTE_WRITE) || !(event.fflags&NOTE_EXTEND)) { + printf("VNODE_CLOSE preserved=fail cycle=%u events=%d flags=%x\n",cycle,n,event.fflags);return 6; + } + change.flags=EV_ENABLE|EV_UDATA_SPECIFIC|EV_RECEIPT; + if(kevent64(kq,&change,1,&event,1,0,&immediate)!=1 || event.data) return 7; + } + close(kq);close(fd);unlink(path); + puts("VNODE_CLOSE unrelated_close=ignored write=pass extend=pass rearm=pass cycles=100");return 0; +} diff --git a/tools/riot-window-layout-check.m b/tools/riot-window-layout-check.m new file mode 100644 index 0000000..0484421 --- /dev/null +++ b/tools/riot-window-layout-check.m @@ -0,0 +1,29 @@ +#import +#include + +int main(void) { + @autoreleasepool { + [NSApplication sharedApplication]; + NSRect rect = NSMakeRect(20, 20, 1280, 720); + NSUInteger mask = NSTitledWindowMask | NSWindowStyleMaskFullSizeContentView; + NSRect converted = [NSWindow contentRectForFrameRect: + [NSWindow frameRectForContentRect:rect styleMask:mask] styleMask:mask]; + NSWindow *full = [[NSWindow alloc] initWithContentRect:rect + styleMask:mask backing:NSBackingStoreBuffered defer:NO]; + [full setFrame:rect display:NO]; + NSSize size = [[full contentView] frame].size; + BOOL pass = NSEqualRects(converted, rect) && NSEqualSizes(size, rect.size); + NSWindow *ordinary = [[NSWindow alloc] initWithContentRect:rect + styleMask:NSTitledWindowMask backing:NSBackingStoreBuffered defer:NO]; + pass &= NSEqualSizes([[ordinary contentView] frame].size, rect.size); + [ordinary setStyleMask:mask]; + NSSize changed = [[ordinary contentView] frame].size; + pass &= NSEqualSizes(changed, [ordinary frame].size); + [full setFrame:NSMakeRect(20, 20, 900, 500) display:NO]; + pass &= NSEqualSizes([[full contentView] frame].size, NSMakeSize(900, 500)); + printf("WINDOW_LAYOUT_CHECK full=%.0fx%.0f changed=%.0fx%.0f pass=%d\n", + size.width, size.height, changed.width, changed.height, pass); + [ordinary release]; [full release]; + return pass ? 0 : 1; + } +} diff --git a/tools/riot-workqueue-reuse-check.c b/tools/riot-workqueue-reuse-check.c new file mode 100644 index 0000000..00fe626 --- /dev/null +++ b/tools/riot-workqueue-reuse-check.c @@ -0,0 +1,40 @@ +#include +#include +#include +#include +#include +#include +static size_t maximum_depth; +static int failure; +int main(int argc, char **argv) { + dispatch_queue_t queue=dispatch_queue_create("org.darling.riot.reuse-check",0); + dispatch_semaphore_t done=dispatch_semaphore_create(0); + unsigned cycles=0; + for(;cycles<400 && !failure;cycles++) { + dispatch_async(queue,^{ + char marker; + uintptr_t top=(uintptr_t)pthread_get_stackaddr_np(pthread_self()); + size_t depth=top-(uintptr_t)▮ + if(depth>maximum_depth) maximum_depth=depth; + if(depth>128*1024) failure=1; + dispatch_semaphore_signal(done); + }); + dispatch_semaphore_wait(done,DISPATCH_TIME_FOREVER); + usleep(10000); + } + printf("WORKQUEUE_REUSE cycles=%u maximum_stack_depth=%zu pass=%d\n",cycles,maximum_depth,!failure); + if (argc>1 && !strcmp(argv[1], "--idle-restarts")) { + for (unsigned restart=0; restart<3; ++restart) { + // Workers retire after 15 seconds. Exercise requesting a new + // worker after retirement, then synchronous queue handoff. + sleep(16); + dispatch_async(queue,^{ dispatch_semaphore_signal(done); }); + if (dispatch_semaphore_wait(done,dispatch_time(DISPATCH_TIME_NOW,5ull*1000000000))) { + printf("FAIL idle worker restart=%u\n",restart); failure=1; break; + } + dispatch_sync(queue,^{}); + } + printf("WORKQUEUE_IDLE_RESTARTS pass=%d\n",!failure); + } + dispatch_release(done);dispatch_release(queue);return failure; +} diff --git a/tools/riot-xpc-pipe-ownership-check.c b/tools/riot-xpc-pipe-ownership-check.c new file mode 100644 index 0000000..ceda5ca --- /dev/null +++ b/tools/riot-xpc-pipe-ownership-check.c @@ -0,0 +1,111 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +extern void *objc_autoreleasePoolPush(void); +extern void objc_autoreleasePoolPop(void *); +extern void _CFEnableZombies(void); + +static void *reply_server(void *context) +{ + mach_port_t listener = *(mach_port_t *)context; + struct { + mach_msg_header_t header; + mach_msg_body_t body; + mach_msg_port_descriptor_t ports[2]; + unsigned char padding[256]; + } checkin = {0}; + if (mach_msg(&checkin.header, MACH_RCV_MSG | MACH_RCV_TIMEOUT, 0, + sizeof(checkin), listener, 5000, MACH_PORT_NULL)) _exit(10); + if (!(checkin.header.msgh_bits & MACH_MSGH_BITS_COMPLEX) || + checkin.body.msgh_descriptor_count != 2) _exit(11); + mach_port_t incoming = MACH_PORT_NULL, unused_send = MACH_PORT_NULL; + for (unsigned i = 0; i < 2; i++) { + if (checkin.ports[i].disposition == MACH_MSG_TYPE_PORT_RECEIVE) + incoming = checkin.ports[i].name; + else if (checkin.ports[i].disposition == MACH_MSG_TYPE_PORT_SEND) + unused_send = checkin.ports[i].name; + } + if (!incoming) _exit(12); + for (unsigned i = 0; i < 100; i++) { + void *pool = objc_autoreleasePoolPush(); + xpc_object_t request = NULL; + if (xpc_pipe_receive(incoming, &request, 0)) _exit(13); + xpc_object_t reply = xpc_dictionary_create_reply(request); + if (!reply || xpc_pipe_routine_reply(reply)) _exit(14); + xpc_release(reply); + xpc_release(request); + objc_autoreleasePoolPop(pool); + } + mach_port_destroy(mach_task_self(), incoming); + if (unused_send) mach_port_deallocate(mach_task_self(), unused_send); + return NULL; +} + +int main(void) +{ + /* Keep a freed object identifiable so the baseline fails deterministically. */ + _CFEnableZombies(); + mach_port_t port = MACH_PORT_NULL; + if (mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &port)) return 3; + if (mach_port_insert_right(mach_task_self(), port, port, MACH_MSG_TYPE_MAKE_SEND)) return 4; + for (unsigned mode = 0; mode < 2; mode++) { + for (unsigned i = 0; i < 100; i++) { + struct { + mach_msg_header_t header; + unsigned int wire[5]; + } message = {0}; + message.header.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0); + message.header.msgh_size = sizeof(message); + message.header.msgh_remote_port = port; + message.header.msgh_id = 0x10000000; + /* XPC v5 with an empty dictionary. */ + unsigned int wire[] = {0x40585043, 5, 0x0f000, 4, 0}; + memcpy(message.wire, wire, sizeof(wire)); + if (mach_msg(&message.header, MACH_SEND_MSG, sizeof(message), 0, + MACH_PORT_NULL, 0, MACH_PORT_NULL)) return 5; + void *pool = objc_autoreleasePoolPush(); + xpc_object_t received = NULL; + mach_port_t reply_port = MACH_PORT_NULL; + int status = mode ? xpc_pipe_try_receive(port, &received, &reply_port, + NULL, sizeof(mach_msg_header_t), 0) + : xpc_pipe_receive(port, &received, 0); + objc_autoreleasePoolPop(pool); + if (status || !received || object_getClass((id)received) != (Class)XPC_TYPE_DICTIONARY) { + printf("XPC_PIPE_OWNERSHIP mode=%u iteration=%u pass=0\n", mode, i); + fflush(stdout); + return 2; + } + if (xpc_dictionary_get_count(received) != 0) return 6; + xpc_release(received); + } + } + pthread_t server; + if (pthread_create(&server, NULL, reply_server, &port)) return 7; + xpc_pipe_t pipe = xpc_pipe_create_from_port(port, 0); + if (!pipe) return 8; + for (unsigned i = 0; i < 100; i++) { + void *pool = objc_autoreleasePoolPush(); + xpc_object_t request = xpc_dictionary_create(NULL, NULL, 0), reply = NULL; + int status = xpc_pipe_routine(pipe, request, &reply); + xpc_release(request); + objc_autoreleasePoolPop(pool); + if (status || !reply || object_getClass((id)reply) != (Class)XPC_TYPE_DICTIONARY) { + printf("XPC_PIPE_OWNERSHIP reply=%u pass=0\n", i); + fflush(stdout); + return 9; + } + xpc_release(reply); + } + pthread_join(server, NULL); + xpc_release(pipe); + mach_port_destroy(mach_task_self(), port); + printf("XPC_PIPE_OWNERSHIP receives=200 replies=100 pass=1\n"); + fflush(stdout); + return 0; +} diff --git a/tools/run-riot-native-check.py b/tools/run-riot-native-check.py new file mode 100644 index 0000000..9a0e3ba --- /dev/null +++ b/tools/run-riot-native-check.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +"""Run an owned native probe with bounded capture and scoped timeout cleanup.""" +import argparse +import os +from pathlib import Path +import signal +import subprocess +import tempfile + +root = Path(__file__).resolve().parent.parent +checks = { + "riot-mach-exception-state-check": "MACH_EXCEPTION_STATE", + "riot-running-application-check": "RUNNING_APP", + "riot-task-info-check": "TASK_INFO", + "riot-appkit-pump-check": "APPKIT_PUMP", + "riot-event-coalescing-check": "EVENT_COALESCING", + "riot-scroll-event-check": "SCROLL_", + "riot-pthread-integrity-check": "PTHREAD_INTEGRITY", + "riot-malloc-integrity-check": "MALLOC_INTEGRITY", + "riot-rpc-signal-mask-check": "RPC_SIGNAL_MASK", + "riot-workqueue-reuse-check": "WORKQUEUE_REUSE", + "riot-kqueue-lifetime-check": "KQUEUE_", + "riot-vnode-close-check": "VNODE_CLOSE", + "riot-kqueue-timer-check": "KQUEUE_TIMER", + "riot-kqueue-stack-check": "KQUEUE_SMALL_STACK", + "riot-kqueue-dispatch2-check": "DISPATCH2", + "riot-dispatch-fd-check": "DISPATCH_FD", +} +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument("check", choices=checks) +parser.add_argument("--library-path", default="@GUEST_HOME@/RuntimeLibraries") +parser.add_argument("--framework-path", default="@GUEST_HOME@/RuntimeFrameworks") +parser.add_argument("--timeout", type=int, default=30) +args = parser.parse_args() +if not 1 <= args.timeout <= 120: + parser.error("timeout must be between 1 and 120 seconds") +binary = root / "build" / args.check +if not binary.is_file(): + parser.error("probe has not been built") + +def owned_guests(): + found = {} + for entry in Path("/proc").iterdir(): + if not entry.name.isdigit(): + continue + try: + if entry.joinpath("exe").readlink().name != "mldr": + continue + if str(binary) not in entry.joinpath("maps").read_text(): + continue + fields = entry.joinpath("stat").read_text().rsplit(")", 1)[1].split() + found[int(entry.name)] = fields[19] + except OSError: + pass + return found + +previous = owned_guests() +environment = dict(os.environ, DYLD_LIBRARY_PATH=args.library_path, + DYLD_FRAMEWORK_PATH=args.framework_path) +# A killed shell can leave a guest holding inherited stdout open. File capture +# avoids communicate() waiting forever on that guest's pipe after a timeout. +with tempfile.TemporaryFile() as output: + process = subprocess.Popen(["darling", "shell", "/Volumes/SystemRoot" + str(binary)], + env=environment, stdout=output, stderr=output) + timed_out = False + try: + result = process.wait(timeout=args.timeout) + except subprocess.TimeoutExpired: + timed_out = True + process.kill() + process.wait(timeout=3) + for pid, identity in owned_guests().items(): + if pid not in previous and owned_guests().get(pid) == identity: + try: + os.kill(pid, signal.SIGKILL) + except ProcessLookupError: + pass + result = 124 + output.seek(0) + observed = False + for line in output.read().decode(errors="replace").splitlines(): + if line.startswith(checks[args.check]): + observed = True + print(line) + if (args.check == "riot-running-application-check" and + line != "RUNNING_APP found=1 activated=1 missing=1"): + result = 1 + if (args.check == "riot-mach-exception-state-check" and + line != "MACH_EXCEPTION_STATE state=1 identity=1 handled=2 server=1"): + result = 1 + if result == 0 and not observed: + print("NATIVE_CHECK missing_probe_output=1") + result = 1 + print(f"NATIVE_CHECK name={args.check} timed_out={int(timed_out)} exit={result}") +raise SystemExit(result) diff --git a/tools/run-riot-ux-with-logdir-shim.sh b/tools/run-riot-ux-with-logdir-shim.sh new file mode 100755 index 0000000..ef32699 --- /dev/null +++ b/tools/run-riot-ux-with-logdir-shim.sh @@ -0,0 +1,129 @@ +#!/bin/sh +# darling-lol M58F101: launch the real Riot chain with the argv log-dir shim. +# +# The Riot Foundation starts the UX product with a home-relative +# `--log-dir=Library/Logs/Riot Games/Riot Client`; the app hands that to +# Electron's app.setAppLogsPath(), which THROWS "Path must be absolute", so the +# UX stalls before app.whenReady()/createWindow() and never maps its window. +# +# The shim (tools/m58f101-logdir-shim.c) rewrites `--log-dir=` to +# `--log-dir=$HOME/` in argv at load time. The product is NOT +# hardened, so DYLD_INSERT_LIBRARIES set on the launcher propagates through the +# Foundation to the product. +# +# One-time setup: +# clang -target x86_64-apple-macosx11.0 -fuse-ld=lld -dynamiclib -nostdlib \ +# -nodefaultlibs -undefined dynamic_lookup -O2 -o /tmp/zzlogdir.dylib \ +# tools/m58f101-logdir-shim.c -Wl,-install_name,/tmp/zzlogdir.dylib +# cp /tmp/zzlogdir.dylib "$HOME/.darling@GUEST_HOME@/zzlogdir.dylib" +# # -> guest path @GUEST_HOME@/zzlogdir.dylib (outside the app bundle, so the +# # launcher's install-integrity check is unaffected) +# +# Then run this script (guest DISPLAY defaults to :1). +# Darling's native window backend supplies Xlib Display pointers to EGL. +# Select X11 explicitly so Mesa does not choose Wayland from the host session. +set -eu +# Native guest crashes created large system core files and exhausted this disk. +# Address-only diagnostics suffice; bound this runner and its new namespace. +if [ "${RCLOL_CORE_DUMPS:-0}" != 1 ]; then + ulimit -c 0 +fi +runner_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +python3 "$runner_dir/ensure-darling-runtime.py" +python3 "$runner_dir/ensure-darling-service-accounts.py" +python3 "$runner_dir/ensure-league-runtime-layout.py" +# Chromium's native macOS verifier stalls in Darling's Security/trustd path. +# Use its built-in verifier; certificate validation remains enabled. +# Keep the hardware-acceleration switch. The machine-id slot is Sentry +# metadata; reuse it for the verifier switch. Each replacement is one argv entry. +# Additional diagnostics can use RCLOL_APP_ARG2 with an explicit +# RCLOL_APP_ARG2_SLOT identifying an existing expendable argument. +RCLOL_APP_ARGS=${RCLOL_APP_ARGS-'--enable-features=ChromeRootStoreUsed,CertVerifierBuiltin:impl/4'} +RCLOL_APP_ARGS_SLOT=${RCLOL_APP_ARGS_SLOT-'--machine-id='} +export RCLOL_APP_ARGS RCLOL_APP_ARGS_SLOT +RCLOL_APP_ARG2=${RCLOL_APP_ARG2-'--disable-features=RemoteCoreAnimationAPI,AudioServiceOutOfProcess,UseVideoFrameSinkBundle'} +RCLOL_APP_ARG2_SLOT=${RCLOL_APP_ARG2_SLOT-'--crashpad-environment='} +export RCLOL_APP_ARG2 RCLOL_APP_ARG2_SLOT +# The startup/update hooks below connect to Electron's local main inspector. +# Reserve the telemetry session slot for that switch, as with the two existing +# compatibility switches. Bind it to loopback explicitly. +RCLOL_APP_ARG3=${RCLOL_APP_ARG3-'--inspect=127.0.0.1:9229'} +RCLOL_APP_ARG3_SLOT=${RCLOL_APP_ARG3_SLOT-'--session-id='} +export RCLOL_APP_ARG3 RCLOL_APP_ARG3_SLOT +# League's UI (LeagueClientUx, CEF) gets the Riot Client UI's tuning through the shim, applied to its +# GPU helper: the Darling-incompatible Chromium features off and frame pacing unthrottled. Each +# replacement takes over one existing, expendable switch of that helper's command line. +# RCLOL_LEAGUE_UX_TUNING=0 leaves the League UI untouched. +if [ "${RCLOL_LEAGUE_UX_TUNING:-1}" != 0 ]; then + RCLOL_UX_ARG1='--disable-features=HardwareMediaKeyHandling,RemoteCoreAnimationAPI,UseVideoFrameSinkBundle' + RCLOL_UX_ARG1_SLOT='--disable-features=' + RCLOL_UX_ARG2='--disable-frame-rate-limit' + RCLOL_UX_ARG2_SLOT='--crash-environment=' + export RCLOL_UX_ARG1 RCLOL_UX_ARG1_SLOT RCLOL_UX_ARG2 RCLOL_UX_ARG2_SLOT +fi +# The paired runtime presents completed GPU textures without CPU readback. +# Explicit 0 or an empty value selects the CPU fallback for diagnostics. +for option in RCLOL_GPU_DIRECT_PRESENT RCLOL_GPU_DIRECT_ASYNC RCLOL_GPU_DIRECT_VSYNC DARLING_CA_PRESENT_AT_COMMIT DARLING_CA_SKIP_LOCAL_IOSURFACE_RENDER RCLOL_FORWARD_MOUSE_MOVED; do + eval 'value=${'"$option"'-1}' + case "$value" in 0|'') unset "$option";; *) export "$option=$value";; esac +done +# Bound software decode workers before the renderer is created. Native video +# playback remains enabled; avoid excess emulated worker/RPC synchronization. +# 0 restores Chromium's default thread policy for controlled comparisons. +decoder_threads=${RCLOL_DECODER_THREADS:-1} +case "$decoder_threads" in + 0|1|2|4) ;; + *) echo "RCLOL_DECODER_THREADS must be 0, 1, 2, or 4" >&2; exit 1;; +esac +raster_threads=${RCLOL_RASTER_THREADS:-0} +case "$raster_threads" in + 0|1|2|4) ;; + *) echo "RCLOL_RASTER_THREADS must be 0, 1, 2, or 4" >&2; exit 1;; +esac +if [ "${RCLOL_DISABLE_GPU_VSYNC:-0}" = 1 ]; then + if [ "${RCLOL_UNTHROTTLED_BEGIN_FRAMES:-1}" = 1 ]; then + python3 "$runner_dir/riot-decoder-scheduling.py" --threads "$decoder_threads" --raster-threads "$raster_threads" --unthrottled --disable-gpu-vsync > /tmp/riot-decoder-scheduling.log 2>&1 & + else + python3 "$runner_dir/riot-decoder-scheduling.py" --threads "$decoder_threads" --raster-threads "$raster_threads" --disable-gpu-vsync > /tmp/riot-decoder-scheduling.log 2>&1 & + fi +elif [ "${RCLOL_UNTHROTTLED_BEGIN_FRAMES:-1}" = 1 ]; then + python3 "$runner_dir/riot-decoder-scheduling.py" --threads "$decoder_threads" --raster-threads "$raster_threads" --unthrottled > /tmp/riot-decoder-scheduling.log 2>&1 & +elif [ "$decoder_threads" != 0 ] || [ "$raster_threads" != 0 ]; then + python3 "$runner_dir/riot-decoder-scheduling.py" --threads "$decoder_threads" --raster-threads "$raster_threads" > /tmp/riot-decoder-scheduling.log 2>&1 & +fi +# This existing local main-process inspector exposes no account data here. +# Install a window hook once, then disconnect instead of polling the app. +if [ "${RCLOL_FOREGROUND_SCHEDULING:-1}" != 0 ]; then + python3 "$runner_dir/riot-foreground-scheduling.py" > /tmp/riot-foreground-scheduling.log 2>&1 & +fi +# Recover accepted League Update actions that leave the legacy patch request empty. +if [ "${RCLOL_LEAGUE_UPDATE_HANDOFF:-1}" != 0 ]; then + python3 "$runner_dir/riot-league-update.py" > /tmp/riot-league-update.log 2>&1 & +fi +SHIM_GUEST="@GUEST_HOME@/zzlogdir.dylib" +RIOT_INSERT_LIBRARIES="$SHIM_GUEST:@GUEST_HOME@/libRiotEmptyAutofill.dylib" +# Use the verified bridge when the paired host/guest artifacts are installed. +# Its UUID and interface guards retain Chromium's decoder on other builds. +native_vpx_default=0 +if [ -f "$HOME/.darling@GUEST_HOME@/libRiotNativeVpx.dylib" ] && [ -f "$runner_dir/../build/libriot-native-vpx.so" ]; then + native_vpx_default=1 +fi +if [ "${RCLOL_NATIVE_VPX:-$native_vpx_default}" = 1 ]; then + RIOT_INSERT_LIBRARIES="$RIOT_INSERT_LIBRARIES:@GUEST_HOME@/libRiotNativeVpx.dylib" +fi +A=${RIOT_LAUNCHER_HOST:-"$HOME/.darling/Applications/Riot Client.app/Contents/MacOS/RiotClientServices"} +# External XIM (for example Fcitx) consumes keys before Darling forwards them. +# Use Xlib's local input method; allow explicit testing of another modifier. +# Linux C.UTF-8 is not a supported codecvt locale in the macOS client. +# Keep this scoped to the guest; product region/locale arguments remain explicit. +exec env -u ELECTRON_RUN_AS_NODE EGL_PLATFORM=x11 \ + LANG="${RCLOL_GUEST_LOCALE:-en_US.UTF-8}" LC_ALL="${RCLOL_GUEST_LOCALE:-en_US.UTF-8}" \ + mesa_glthread="${RCLOL_MESA_GLTHREAD-${mesa_glthread-false}}" \ + XMODIFIERS="${RCLOL_XMODIFIERS:-@im=none}" DISPLAY="${DISPLAY:-:1}" \ + DYLD_LIBRARY_PATH="${DYLD_LIBRARY_PATH:-@GUEST_HOME@/RuntimeLibraries}" \ + DYLD_FRAMEWORK_PATH="${DYLD_FRAMEWORK_PATH:-@GUEST_HOME@/RuntimeFrameworks}" \ + DARLING_XSHM_LIBRARY="${DARLING_XSHM_LIBRARY:-@GUEST_HOME@/libDarlingXShm.dylib}" \ + DYLD_INSERT_LIBRARIES="$RIOT_INSERT_LIBRARIES" \ + darling "$A" --region="${RCLOL_REGION:-EUW}" --locale="${RCLOL_LOCALE:-en_GB}" \ + --app-root="/Applications/Riot Client.app" \ + --log-dir="@GUEST_HOME@/Library/Logs/Riot Games/Riot Client" diff --git a/tools/stop-darling-runtime.py b/tools/stop-darling-runtime.py new file mode 100755 index 0000000..57117bc --- /dev/null +++ b/tools/stop-darling-runtime.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +"""Wait for Darling's old server to exit before rebuilding its namespace.""" +import os +import signal +from pathlib import Path +import subprocess +import sys +import time +prefix = Path(os.environ.get('DPREFIX', str(Path.home() / '.darling'))) +try: + old_pid = int((prefix / '.init.pid').read_text().strip()) +except (OSError, ValueError): + old_pid = 0 + +def live(pid): + if pid <= 0: + return False + try: + state = next(line.split()[1] for line in (Path('/proc') / str(pid) / 'status').read_text().splitlines() if line.startswith('State:')) + return state != 'Z' + except (FileNotFoundError, StopIteration): + return False + +def process_identity(pid): + try: + return (Path('/proc') / str(pid) / 'stat').read_text().rsplit(')', 1)[1].split()[19] + except (OSError, IndexError): + return None + +def riot_descendants(server_pid): + # The host server and its guest init have different PID namespaces. Follow + # ancestry while the server is alive, rather than comparing namespace IDs. + pending = [server_pid] + seen = set() + result = {} + while pending: + pid = pending.pop() + if pid in seen or pid <= 0: + continue + seen.add(pid) + try: + for children in (Path('/proc') / str(pid) / 'task').glob('*/children'): + pending.extend(int(child) for child in children.read_text().split()) + mappings = (Path('/proc') / str(pid) / 'maps').read_text() + if ('Electron Framework.framework/Versions/A/Electron Framework' in mappings or + '/Applications/Riot Client.app/' in mappings): + result[pid] = process_identity(pid) + except (OSError, ValueError): + continue + return result + +def orphaned_riot(): + # A dead server marker cannot enumerate its former descendants. Recover + # only same-user Riot mappings in this prefix with no live server ancestor. + result = {} + riot_path = str(prefix / 'Applications/Riot Client.app') + '/' + league_path = str(prefix / 'Applications/League of Legends.app') + '/' + for entry in Path('/proc').iterdir(): + if not entry.name.isdigit(): + continue + try: + if entry.stat().st_uid != os.getuid(): + continue + mappings = (entry / 'maps').read_text() + if riot_path not in mappings and league_path not in mappings: + continue + pid = int(entry.name) + ancestor = pid + seen = set() + has_server = False + while ancestor > 1 and ancestor not in seen: + seen.add(ancestor) + proc = Path('/proc') / str(ancestor) + if proc.joinpath('comm').read_text().strip() == 'darlingserver' and live(ancestor): + has_server = True + break + fields = proc.joinpath('stat').read_text().rsplit(')', 1)[1].split() + ancestor = int(fields[1]) + if not has_server: + result[pid] = process_identity(pid) + except (OSError, ValueError, IndexError): + continue + return result + +owned_riot = riot_descendants(old_pid) +owned_riot.update(orphaned_riot()) +subprocess.run(['darling', 'shutdown'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=15) +deadline = time.monotonic() + 5 +while live(old_pid) and time.monotonic() < deadline: + time.sleep(.1) +if live(old_pid): + raise SystemExit(f'Darling server {old_pid} has not exited; runtime remains untouched') +# Shutdown can leave Electron helpers or League's startup window reparented +# and consuming CPU/GPU time. +# Stop the observed descendants and verified serverless Riot processes in this +# prefix, guarding against PID reuse. Leave other live Darling sessions alone. +for pid, identity in owned_riot.items(): + if identity is not None and process_identity(pid) == identity and live(pid): + try: + os.kill(pid, signal.SIGTERM) + except ProcessLookupError: + pass +deadline = time.monotonic() + 3 +while any(process_identity(pid) == identity and live(pid) + for pid, identity in owned_riot.items()) and time.monotonic() < deadline: + time.sleep(.1) +remaining = {pid: identity for pid, identity in owned_riot.items() + if process_identity(pid) == identity and live(pid)} +# These helpers have no live server and ignored graceful termination. They +# cannot complete guest RPCs after shutdown. Kill only the previously observed +# Riot descendants, with the same PID/start-time guard used above. +for pid, identity in remaining.items(): + if process_identity(pid) == identity and live(pid): + try: + os.kill(pid, signal.SIGKILL) + except ProcessLookupError: + pass +deadline = time.monotonic() + 5 +while any(process_identity(pid) == identity and live(pid) + for pid, identity in remaining.items()) and time.monotonic() < deadline: + time.sleep(.1) +if any(process_identity(pid) == identity and live(pid) for pid, identity in remaining.items()): + raise SystemExit('Old Riot helpers remain alive; runtime remains untouched') +subprocess.run([sys.executable, str(Path(__file__).with_name('ensure-darling-runtime.py'))], check=True) diff --git a/tools/test-commpage-cpu-caps.c b/tools/test-commpage-cpu-caps.c new file mode 100644 index 0000000..72632b8 --- /dev/null +++ b/tools/test-commpage-cpu-caps.c @@ -0,0 +1,22 @@ +#define PRIVATE +#include +#include "../build/darling-src/src/startup/mldr/commpage-cpu-caps.h" +#include +#include + +int main(void) { + assert(commpage_decode_cpu_caps(0, 0, 0, 0, 0) == 0); + assert(commpage_decode_cpu_caps(1u << 26, 0, 0, 0, 0) == 0); + assert(commpage_decode_cpu_caps(0, 1u << 26, 0, 0, 0) == kHasSSE2); + assert(commpage_decode_cpu_caps(1u << 20, 0, 0, 0, 0) == kHasSSE4_2); + assert(commpage_decode_cpu_caps(0, 1u << 20, 0, 0, 0) == 0); + const uint32_t avx = (1u << 26) | (1u << 27) | (1u << 28) | (1u << 12) | (1u << 29); + const uint64_t vector_caps = kHasAVX1_0 | kHasAVX2_0 | kHasFMA | kHasF16C; + assert(commpage_decode_cpu_caps(avx, 0, 1u << 5, 6, 0) == vector_caps); + assert(commpage_decode_cpu_caps(avx, 0, 1u << 5, 2, 0) == 0); + assert(commpage_decode_cpu_caps(avx & ~(1u << 27), 0, 1u << 5, 6, 0) == 0); + assert(commpage_decode_cpu_caps(avx & ~(1u << 26), 0, 1u << 5, 6, 0) == 0); + assert(commpage_decode_cpu_caps(0, 0, 1u << 3, 0, 1) == (kHasBMI1 | k64Bit)); + puts("PASS: distinct CPUID registers and AVX operating-system state gates"); + return 0; +} diff --git a/tools/test-p99-runtime.cpp b/tools/test-p99-runtime.cpp new file mode 100644 index 0000000..82a4a32 --- /dev/null +++ b/tools/test-p99-runtime.cpp @@ -0,0 +1,70 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +VkInstance Indium::globalInstance = VK_NULL_HANDLE; +std::atomic resolutions {0}; +static int increment(int value) { return value + 1; } +static PFN_vkVoidFunction resolveMock(VkInstance, const char* name) { + ++resolutions; + return std::strcmp(name, "increment") == 0 ? reinterpret_cast(increment) : nullptr; +} +PFN_vkGetInstanceProcAddr Indium::DynamicVK::vkGetInstanceProcAddr = resolveMock; + +int main() { + Indium::DynamicVK::DynamicFunction function("increment"); + std::vector threads; + for (int i = 0; i < 16; ++i) threads.emplace_back([&] { + for (int j = 0; j < 10000; ++j) assert(function(j) == j + 1); + }); + for (auto& thread : threads) thread.join(); + assert(resolutions == 1); + Indium::DynamicVK::DynamicFunction missing("missing"); + assert(!missing.isAvailable() && !missing.isAvailable()); + try { missing(0); assert(false); } catch (const std::runtime_error&) {} + assert(resolutions == 2); + + using Job = std::function; + Indium::RetirementQueue queue; + std::promise started, release; + auto gate = release.get_future(); + auto mainThread = std::this_thread::get_id(); + std::atomic completed {0}; + assert(queue.tryPush(Job([&] { + assert(std::this_thread::get_id() != mainThread); + started.set_value(); gate.wait(); ++completed; + }))); + started.get_future().wait(); + assert(queue.tryPush(Job([&] { ++completed; }))); + assert(queue.tryPush(Job([&] { ++completed; }))); + Job fallback = [&] { ++completed; }; + assert(!queue.tryPush(std::move(fallback))); + assert(fallback); // Full queues must not consume the caller's job. + fallback(); + release.set_value(); + queue.shutdown(); + assert(completed == 4); + Job afterShutdown = [&] { ++completed; }; + assert(!queue.tryPush(std::move(afterShutdown)) && afterShutdown); + + // Concurrent producers, bounded fallback, and resource lifetime through + // worker completion. Every submitted or fallback job runs exactly once. + Indium::RetirementQueue concurrent; + completed = 0; threads.clear(); + for (int i = 0; i < 4; ++i) threads.emplace_back([&] { + for (int j = 0; j < 1000; ++j) { + auto owner = std::make_shared(j); + Job job = [&, owner] { assert(*owner >= 0); ++completed; }; + if (!concurrent.tryPush(std::move(job))) job(); + } + }); + for (auto& thread : threads) thread.join(); + concurrent.shutdown(); + assert(completed == 4000); + std::cout << "cached dispatch and bounded retirement concurrency tests passed\n"; +} diff --git a/tools/test-render-pass-key.cpp b/tools/test-render-pass-key.cpp new file mode 100644 index 0000000..a8e3b40 --- /dev/null +++ b/tools/test-render-pass-key.cpp @@ -0,0 +1,52 @@ +#include +#include +#include +#include + +int main() { + VkAttachmentDescription a {}; + a.format = VK_FORMAT_R8G8B8A8_UNORM; a.samples = VK_SAMPLE_COUNT_1_BIT; + a.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; a.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + a.initialLayout = a.finalLayout = VK_IMAGE_LAYOUT_GENERAL; + VkAttachmentReference colors[] {{0, VK_IMAGE_LAYOUT_GENERAL}, {VK_ATTACHMENT_UNUSED, VK_IMAGE_LAYOUT_GENERAL}}; + VkSubpassDescription subpass {}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = 2; subpass.pColorAttachments = colors; + VkSubpassDependency dep {}; + dep.srcStageMask = dep.dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; + VkRenderPassCreateInfo info {}; + info.attachmentCount = 1; info.pAttachments = &a; + info.subpassCount = 1; info.pSubpasses = &subpass; + info.dependencyCount = 1; info.pDependencies = &dep; + auto original = *Indium::renderPassKey(info); + auto changed = [&] { assert(!(*Indium::renderPassKey(info) == original)); }; + auto checkAttachment = [&](auto& field, auto value) { + auto saved = field; field = value; changed(); field = saved; + }; + checkAttachment(a.loadOp, VK_ATTACHMENT_LOAD_OP_CLEAR); + checkAttachment(a.storeOp, VK_ATTACHMENT_STORE_OP_DONT_CARE); + checkAttachment(a.stencilLoadOp, VK_ATTACHMENT_LOAD_OP_CLEAR); + checkAttachment(a.initialLayout, VK_IMAGE_LAYOUT_UNDEFINED); + checkAttachment(a.finalLayout, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + checkAttachment(a.format, VK_FORMAT_B8G8R8A8_UNORM); + colors[1].attachment = 0; changed(); colors[1].attachment = VK_ATTACHMENT_UNUSED; + dep.dependencyFlags = VK_DEPENDENCY_BY_REGION_BIT; changed(); dep.dependencyFlags = 0; + VkAttachmentDescription copy = a; + info.pAttachments = © + assert(*Indium::renderPassKey(info) == original); // Addresses and padding are irrelevant. + std::unordered_map cache; + cache.emplace(original, 42); + assert(cache.at(*Indium::renderPassKey(info)) == 42); + info.pNext = &info; assert(!Indium::renderPassKey(info)); info.pNext = nullptr; + subpass.pResolveAttachments = colors; assert(!Indium::renderPassKey(info)); subpass.pResolveAttachments = nullptr; + std::array attachments {}; + std::array references {}; + std::array dependencies {}; + info.attachmentCount = attachments.size(); info.pAttachments = attachments.data(); + subpass.colorAttachmentCount = references.size(); subpass.pColorAttachments = references.data(); + subpass.pDepthStencilAttachment = references.data(); + info.dependencyCount = dependencies.size(); info.pDependencies = dependencies.data(); + auto maximum = Indium::renderPassKey(info); assert(maximum && maximum->size <= maximum->words.size()); + info.attachmentCount = 17; assert(!Indium::renderPassKey(info)); + puts("PASS: exact render-pass preservation, layout, holes, bounded key and unsupported fallback"); +} diff --git a/tools/test-resource-owner-cache.cpp b/tools/test-resource-owner-cache.cpp new file mode 100644 index 0000000..8a39a2f --- /dev/null +++ b/tools/test-resource-owner-cache.cpp @@ -0,0 +1,52 @@ +#include "../build/darling-src/src/external/metal/deps/indium/private-include/indium/resource-owner-cache.private.hpp" + +#include +#include +#include + +struct Base { virtual ~Base() = default; }; +struct Resource : Base { + static size_t alive; + size_t id; + explicit Resource(size_t id) : id(id) { ++alive; } + ~Resource() { --alive; } +}; +size_t Resource::alive = 0; + +int main() { + std::vector> weak; + { + Indium::ResourceOwnerCache cache; + assert(cache.acquire({}).first == nullptr); + // Rehash repeatedly, then release every external owner. All earlier + // recorded resources must remain alive until the cache retires. + for (size_t i = 0; i < 10000; ++i) { + std::shared_ptr owner = std::make_shared(i); + weak.push_back(owner); + auto [entry, inserted] = cache.acquire(owner); + assert(inserted && entry->value->id == i); + entry->depthFeedback = i % 2; + auto [same, again] = cache.acquire(owner); + assert(!again && entry == same); + } + assert(Resource::alive == weak.size()); + std::mt19937 random(1234); + for (size_t i = 0; i < 100000; ++i) { + size_t index = random() % weak.size(); + auto owner = weak[index].lock(); + assert(owner); + auto [entry, inserted] = cache.acquire(owner); + assert(!inserted && entry->owner == owner); + assert(entry->value->id == index && entry->depthFeedback == bool(index % 2)); + } + // A failed private cast is cached safely, while its base stays owned. + auto other = std::make_shared(); + std::weak_ptr otherWeak = other; + assert(cache.acquire(other).first->value == nullptr); + other.reset(); + assert(!otherWeak.expired()); + } + assert(Resource::alive == 0); + for (const auto& owner : weak) assert(owner.expired()); + std::cout << "owner-cache lifetime, rehash, repeated lookup and cast tests passed\n"; +} diff --git a/tools/test-submission-batch.cpp b/tools/test-submission-batch.cpp new file mode 100644 index 0000000..5da56f9 --- /dev/null +++ b/tools/test-submission-batch.cpp @@ -0,0 +1,54 @@ +#include +#include +#include + +int main() { + Indium::SubmissionBatch batch; + unsigned calls = 0; + auto noSubmit = [&](uint32_t, const VkSubmitInfo2*) { ++calls; return VK_SUCCESS; }; + assert(batch.flush(noSubmit) == VK_SUCCESS && calls == 0); + for (uint64_t i = 1; i <= 128; ++i) { + VkCommandBufferSubmitInfo command {}; + command.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO; + command.commandBuffer = reinterpret_cast(i); + VkSemaphoreSubmitInfo wait {}, signal {}; + wait.sType = signal.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO; + wait.semaphore = signal.semaphore = reinterpret_cast(uintptr_t(999)); + wait.value = i - 1; signal.value = i; + wait.stageMask = signal.stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT; + VkSubmitInfo2 info {}; + info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2; + info.commandBufferInfoCount = 1; info.pCommandBufferInfos = &command; + info.waitSemaphoreInfoCount = 1; info.pWaitSemaphoreInfos = &wait; + info.signalSemaphoreInfoCount = 1; info.pSignalSemaphoreInfos = &signal; + batch.append(info); + // All source structures die here. Their pointers must not be retained. + } + assert(batch.full()); + auto verify = [&](uint32_t count, const VkSubmitInfo2* infos) { + ++calls; assert(count == 128); + for (uint64_t i = 0; i < count; ++i) { + assert(infos[i].sType == VK_STRUCTURE_TYPE_SUBMIT_INFO_2); + assert(infos[i].commandBufferInfoCount == 1); + assert(infos[i].pCommandBufferInfos[0].commandBuffer == reinterpret_cast(i + 1)); + assert(infos[i].pWaitSemaphoreInfos[0].value == i); + assert(infos[i].pSignalSemaphoreInfos[0].value == i + 1); + assert(infos[i].pSignalSemaphoreInfos[0].stageMask == VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT); + } + return VK_SUCCESS; + }; + assert(batch.flush(verify) == VK_SUCCESS && batch.empty() && calls == 1); + VkCommandBufferSubmitInfo command {}; + command.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO; + VkSubmitInfo2 emptySemaphores {}; + emptySemaphores.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2; + emptySemaphores.commandBufferInfoCount = 1; + emptySemaphores.pCommandBufferInfos = &command; + batch.append(emptySemaphores); + assert(batch.flush([](uint32_t count, const VkSubmitInfo2* infos) { + assert(count == 1 && infos[0].waitSemaphoreInfoCount == 0 && infos[0].signalSemaphoreInfoCount == 0); + return VK_ERROR_DEVICE_LOST; + }) == VK_ERROR_DEVICE_LOST && !batch.empty()); + assert(batch.flush(noSubmit) == VK_SUCCESS && batch.empty()); + std::cout << "submission copy, ordering, capacity, reuse and failure tests passed\n"; +} diff --git a/upstream/manifest.json b/upstream/manifest.json new file mode 100644 index 0000000..06dabca --- /dev/null +++ b/upstream/manifest.json @@ -0,0 +1,288 @@ +{ + "schema": 1, + "upstream": "https://github.com/darlinghq/darling.git", + "revision": "60ba801decee7a00782f74f6be4c8ffb013f79ff", + "components": [ + { + "path": ".", + "revision": "60ba801decee7a00782f74f6be4c8ffb013f79ff", + "repository": "https://github.com/darlinghq/darling.git", + "patch": "patches/darling.patch", + "modified_files": 84, + "new_files": [ + "src/CoreAudio/AudioTrace.h", + "src/CoreAudio/CoreAudioComponent/MatrixMixerAU.cpp", + "src/frameworks/Carbon/HIToolbox/src/SecureInput.c", + "src/frameworks/CoreMedia/include/CoreMedia/CMFormatDescription.h", + "src/frameworks/CryptoTokenKit/src/TKTokenWatcher.m", + "src/frameworks/OpenDirectory/src/ODLocalDirectory.h", + "src/frameworks/OpenDirectory/src/ODLocalDirectory.m", + "src/frameworks/OpenGL/CGLAsyncPresent.inc", + "src/frameworks/OpenGL/include/OpenGL/DarlingNativePresent.h", + "src/startup/mldr/commpage-cpu-caps.h" + ] + }, + { + "path": "src/external/IOKitUser", + "revision": "534684e6748dffbd875c6cd1942477a52b66a077", + "repository": "https://github.com/darlinghq/darling-iokituser.git", + "patch": "patches/IOKitUser.patch", + "modified_files": 1, + "new_files": [] + }, + { + "path": "src/external/Libinfo", + "revision": "93d242ce3f86d1de67522279edb3a2dcb23f30c7", + "repository": "https://github.com/darlinghq/darling-Libinfo.git", + "patch": "patches/Libinfo.patch", + "modified_files": 1, + "new_files": [] + }, + { + "path": "src/external/OpenDirectory", + "revision": "67a8ad2ed0b06775bf53839446e2df7d49ab1c96", + "repository": "https://github.com/darlinghq/darling-opendirectory.git", + "patch": "patches/OpenDirectory.patch", + "modified_files": 1, + "new_files": [] + }, + { + "path": "src/external/cocotron", + "revision": "c8d38d16a9f613d300157bebbab2b9501bc0c274", + "repository": "https://github.com/darlinghq/darling-cocotron.git", + "patch": "patches/cocotron.patch", + "modified_files": 102, + "new_files": [ + "QuartzCore/include/QuartzCore/CATransformLayer.h" + ] + }, + { + "path": "src/external/compiler-rt", + "revision": "5fd9bc0effa307b99b35da59ce579e8e031c22da", + "repository": "https://github.com/darlinghq/darling-compiler-rt.git", + "patch": "patches/compiler-rt.patch", + "modified_files": 2, + "new_files": [] + }, + { + "path": "src/external/configd", + "revision": "98e52de19c52f7938e581ed20385f38abd7fa197", + "repository": "https://github.com/darlinghq/darling-configd.git", + "patch": "patches/configd.patch", + "modified_files": 2, + "new_files": [] + }, + { + "path": "src/external/corecrypto", + "revision": "875f1cd9e75b0029d872b88ce8a77da276c00c84", + "repository": "https://github.com/darlinghq/darling-corecrypto.git", + "patch": "patches/corecrypto.patch", + "modified_files": 4, + "new_files": [] + }, + { + "path": "src/external/corefoundation", + "revision": "a3640a77410cf1825f7855172b1551ae7917c461", + "repository": "https://github.com/darlinghq/darling-corefoundation.git", + "patch": "patches/corefoundation.patch", + "modified_files": 5, + "new_files": [] + }, + { + "path": "src/external/curl", + "revision": "92f54fd7eceabed2c2382a4acc0f7293dedd92ff", + "repository": "https://github.com/darlinghq/darling-curl.git", + "patch": "patches/curl.patch", + "modified_files": 1, + "new_files": [] + }, + { + "path": "src/external/darlingserver", + "revision": "89751e64bc6c2082f7725061824ee0e33395b0de", + "repository": "https://github.com/darlinghq/darlingserver.git", + "patch": "patches/darlingserver.patch", + "modified_files": 35, + "new_files": [] + }, + { + "path": "src/external/foundation", + "revision": "55a4341b470c2a56fea667c1d2167fb074226f04", + "repository": "https://github.com/darlinghq/darling-foundation.git", + "patch": "patches/foundation.patch", + "modified_files": 4, + "new_files": [] + }, + { + "path": "src/external/iokitd", + "revision": "5549ac4cd2db923c256f016a1381b3bfb716730a", + "repository": "https://github.com/darlinghq/darling-iokitd.git", + "patch": "patches/iokitd.patch", + "modified_files": 8, + "new_files": [ + "src/IOPlatformExpertDevice.h", + "src/IOPlatformExpertDevice.mm" + ] + }, + { + "path": "src/external/libcxxabi", + "revision": "c9c851718eb304a9aefa097aeaaf8c3bd1dff1bc", + "repository": "https://github.com/darlinghq/darling-libcxxabi.git", + "patch": "patches/libcxxabi.patch", + "modified_files": 1, + "new_files": [] + }, + { + "path": "src/external/libdispatch", + "revision": "380f03c180b80d940134fb35783ddc714784a53a", + "repository": "https://github.com/darlinghq/darling-libdispatch.git", + "patch": "patches/libdispatch.patch", + "modified_files": 1, + "new_files": [] + }, + { + "path": "src/external/libkqueue", + "revision": "b0795a2e1dab5331116770139bfea8d832478f5f", + "repository": "https://github.com/darlinghq/darling-libkqueue.git", + "patch": "patches/libkqueue.patch", + "modified_files": 13, + "new_files": [] + }, + { + "path": "src/external/libmalloc", + "revision": "a57991e2651226a675654bd96e5d9ab6bec288c5", + "repository": "https://github.com/darlinghq/darling-libmalloc.git", + "patch": "patches/libmalloc.patch", + "modified_files": 2, + "new_files": [] + }, + { + "path": "src/external/libnetwork", + "revision": "56c5fad43f24a40d8ca7f8a1d0badedfeaf7e64e", + "repository": "https://github.com/darlinghq/darling-libnetwork.git", + "patch": "patches/libnetwork.patch", + "modified_files": 2, + "new_files": [] + }, + { + "path": "src/external/libnotify", + "revision": "98156d3f847a3ced6c5f52c12a889047bc4f9b20", + "repository": "https://github.com/darlinghq/darling-Libnotify.git", + "patch": "patches/libnotify.patch", + "modified_files": 1, + "new_files": [] + }, + { + "path": "src/external/libplatform", + "revision": "5a3e5b529d25c70257dcfa97e94f1826e71e9f40", + "repository": "https://github.com/darlinghq/darling-libplatform.git", + "patch": "patches/libplatform.patch", + "modified_files": 3, + "new_files": [] + }, + { + "path": "src/external/libpthread", + "revision": "f07f265bfbcf071c1adfc808de971e053ea5edc5", + "repository": "https://github.com/darlinghq/darling-libpthread.git", + "patch": "patches/libpthread.patch", + "modified_files": 1, + "new_files": [] + }, + { + "path": "src/external/libresolv", + "revision": "cf955392e5449efb269b8b3510c755085fd36a2d", + "repository": "https://github.com/darlinghq/darling-libresolv.git", + "patch": "patches/libresolv.patch", + "modified_files": 1, + "new_files": [] + }, + { + "path": "src/external/libtrace", + "revision": "8cf07f02b15f7dca6436882a03678fff0392eaf6", + "repository": "https://github.com/darlinghq/darling-libtrace.git", + "patch": "patches/libtrace.patch", + "modified_files": 1, + "new_files": [] + }, + { + "path": "src/external/libxpc", + "revision": "394e033333d3c253a12f08a99090c113b0917d00", + "repository": "https://github.com/darlinghq/darling-libxpc.git", + "patch": "patches/libxpc.patch", + "modified_files": 3, + "new_files": [] + }, + { + "path": "src/external/metal", + "revision": "ae20248dc144beab899e38752f5a530f28a0ea56", + "repository": "https://github.com/darlinghq/darling-metal.git", + "patch": "patches/metal.patch", + "modified_files": 27, + "new_files": [ + "include/Metal/MTLArgument.h", + "include/Metal/MTLBlitCommandEncoder.h", + "include/Metal/MTLExceptionBridge.h", + "include/Metal/MTLTrace.h", + "private-include/Metal/MTLArgumentInternal.h", + "private-include/Metal/MTLBlitCommandEncoderInternal.h", + "private-include/Metal/MTLDepthStencilInternal.h", + "private-include/Metal/MTLSamplerStateInternal.h", + "private-include/Metal/MTLVertexDescriptorInternal.h", + "src/Metal/MTLArgument.mm", + "src/Metal/MTLBlitCommandEncoder.mm" + ] + }, + { + "path": "src/external/objc4", + "revision": "1a12df76d12bfc9fdfffadb290f7742763568765", + "repository": "https://github.com/darlinghq/darling-objc4.git", + "patch": "patches/objc4.patch", + "modified_files": 2, + "new_files": [] + }, + { + "path": "src/external/security", + "revision": "3cfffcf2c5b5900169c964facdf42cc05c23005c", + "repository": "https://github.com/darlinghq/darling-security.git", + "patch": "patches/security.patch", + "modified_files": 9, + "new_files": [] + }, + { + "path": "src/external/xnu", + "revision": "fa29287aa2f0115271e091f1031f53c9e024005d", + "repository": "https://github.com/darlinghq/darling-xnu.git", + "patch": "patches/xnu.patch", + "modified_files": 65, + "new_files": [ + "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/xnu_syscall/bsd/impl/guarded/change_fdguard_np.h", + "darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/impl/guarded/change_fdguard_np.c", + "libsyscall/wrappers/__sysctlbyname.S" + ] + }, + { + "path": "src/external/metal/deps/indium", + "revision": "8423a7d2f053167030d2bc4a227f96243c740667", + "repository": "https://github.com/darlinghq/indium.git", + "patch": "patches/metal--deps--indium.patch", + "modified_files": 35, + "new_files": [ + "private-include/indium/counters.private.hpp", + "private-include/indium/deferred-render-encoder.private.hpp", + "private-include/indium/gpu-timing.private.hpp", + "private-include/indium/memory-pool.private.hpp", + "private-include/indium/outline-chain.private.hpp", + "private-include/indium/render-pass-key.private.hpp", + "private-include/indium/resource-owner-cache.private.hpp", + "private-include/indium/retirement-queue.private.hpp", + "private-include/indium/submission-batch.private.hpp", + "private-include/indium/submit-batch.private.hpp", + "private-include/indium/submit-thread.private.hpp" + ] + } + ], + "omitted": [ + "src/external/swift (local deletion of prebuilt Swift files, not a runtime source change)" + ] +}