diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 6c24424..0cc5bcf 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -252,6 +252,30 @@ A non-zero error or `FAILED` means the frames cannot reach the window. Before 20 An additional NVIDIA report on `68f816d` showed successful optimal-tiling import but repeated `Metal Device timed out giving us a Drawable to present` messages. Its [presentation trace](https://github.com/lucannez64/darling-lol/issues/3#issuecomment-6214732591) shows all three drawables queued and all three main-thread presentation callbacks running, followed by timeouts with three frames still queued. The compositor only prepared a Metal layer when it was the root layer, skipping nested Metal layers. It now prepares Metal layers throughout the tree. A standalone nested-layer reproduction exhausts the pool before this correction and passes ten presentations with pixel readback after it; the root-layer probe also passes. Confirmation on the reporter's NVIDIA system is still needed. +### NVIDIA resize signals, CPU readback and completion latency + +The EGL-compatible window visual, compositor back buffer and collision-safe RPC +socket allocation fixes are already part of the runtime. The remaining NVIDIA +work covers these independent paths: + +- A terminal resize can deliver `SIGWINCH` to a native driver thread without a + Darling RPC socket. The runtime forwards that resize notification to the guest + main thread; it keeps the existing handling for other signals and faults. +- With `RCLOL_GPU_DIRECT_PRESENT=0`, Chromium frames arrive through CPU readback. + The local compositor skips non-Metal trees to avoid covering the live client + with stale cached textures. Mixed trees still prepare and present Metal layers. +- NVIDIA timeline waits can wake late after GPU completion. Each device now polls + briefly while completions are pending before blocking: 2000 microseconds by + default on NVIDIA and zero on other vendors. An idle device blocks immediately. + The guest file `/tmp/metal-wait-spin-us` overrides the budget when the device is + created; `0` disables polling. Values are capped at 10000 microseconds and + malformed values use the vendor default. Restart Darling to change the budget. + +The contributor reported custom matches on a single-GPU RTX 3060, driver 615.71, +with sway/Xwayland using CPU readback. The initial polling change improved their +reported rate from about 20 to 27 FPS. Those observations predate the narrowed +integration; they do not establish hybrid-GPU or general NVIDIA support. + ### Host-side checks These are host settings, not game settings. Run on AC power, and use a performance power profile (`powerprofilesctl set performance`, or `asusctl profile -P Performance` on ASUS laptops). On the tested desktop the game is limited mostly by its own main thread on the CPU, not by the GPU (see [status](STATUS.md#game-fps-2026-10-1314)), so laptop CPU power limits matter as much as the GPU choice. diff --git a/patches/cocotron.patch b/patches/cocotron.patch index b70d394..351e394 100644 --- a/patches/cocotron.patch +++ b/patches/cocotron.patch @@ -8756,10 +8756,10 @@ index 5dcac115..5de4f03f 100644 + @end diff --git a/QuartzCore/CALayerContext.m b/QuartzCore/CALayerContext.m -index 5dce8c56..d325ba5f 100644 +index 5dce8c5..3a7f937 100644 --- a/QuartzCore/CALayerContext.m +++ b/QuartzCore/CALayerContext.m -@@ -1,47 +1,69 @@ +@@ -1,47 +1,77 @@ #import +#import #import @@ -8799,6 +8799,14 @@ index 5dce8c56..d325ba5f 100644 + for(CALayer *child in layer.sublayers) + if(hasFullWindowSurface(child,size))return YES; + return NO; ++} ++/* CPU readback already presents non-Metal trees remotely. Locally ++ * compositing their cached textures can leave a frozen child over the client. ++ * Metal trees must always prepare their drawables, including mixed trees. */ ++static BOOL skipLocalRender(CALayer *layer, CGSize size) { ++ return layer && getenv("DARLING_CA_SKIP_LOCAL_IOSURFACE_RENDER") && ++ !hasMetalLayer(layer) && ++ (!getenv("RCLOL_GPU_DIRECT_PRESENT") || hasFullWindowSurface(layer, size)); +} @implementation CALayerContext @@ -8848,7 +8856,7 @@ index 5dce8c56..d325ba5f 100644 [super dealloc]; } -@@ -51,12 +73,22 @@ +@@ -51,12 +81,22 @@ } - (void) setLayer: (CALayer *) layer { @@ -8871,7 +8879,7 @@ index 5dce8c56..d325ba5f 100644 } - (void) setSubwindow: (CGSubWindow*) subwindow -@@ -68,16 +100,50 @@ +@@ -68,16 +108,50 @@ } _subwindow = [subwindow retain]; @@ -8923,7 +8931,7 @@ index 5dce8c56..d325ba5f 100644 } - (void) assignTextureIdsToLayerTree: (CALayer *) layer { -@@ -93,8 +159,27 @@ +@@ -93,8 +167,27 @@ [self assignTextureIdsToLayerTree: child]; } @@ -8951,7 +8959,7 @@ index 5dce8c56..d325ba5f 100644 glEnable(GL_DEPTH_TEST); glShadeModel(GL_SMOOTH); -@@ -118,27 +203,63 @@ +@@ -118,27 +211,62 @@ [_deleteTextureIds removeAllObjects]; @@ -8975,8 +8983,7 @@ index 5dce8c56..d325ba5f 100644 - (void) render { + if (_cglWindow == NULL || _layer == nil) + return; -+ if(getenv("DARLING_CA_SKIP_LOCAL_IOSURFACE_RENDER") && -+ !hasMetalLayer(_layer) && hasFullWindowSurface(_layer,_frame.size))return; ++ if(skipLocalRender(_layer, _frame.size))return; [self renderLayer: _layer]; } @@ -9020,7 +9027,7 @@ index 5dce8c56..d325ba5f 100644 } - (void) startTimerIfNeeded { -@@ -147,7 +268,7 @@ +@@ -147,7 +275,7 @@ target: self selector: @selector(timer:) userInfo: nil @@ -9029,13 +9036,12 @@ index 5dce8c56..d325ba5f 100644 } - (void) deleteTextureId: (NSNumber *) textureId { -@@ -155,7 +276,10 @@ +@@ -155,7 +283,9 @@ } - (void) flush { - CGLFlushDrawable(_glContext); -+ if(getenv("DARLING_CA_SKIP_LOCAL_IOSURFACE_RENDER") && _layer && -+ !hasMetalLayer(_layer) && hasFullWindowSurface(_layer,_frame.size))return; ++ if(skipLocalRender(_layer, _frame.size))return; + if (_glContext != NULL && _cglWindow != NULL) + CGLFlushDrawable(_glContext); } diff --git a/patches/darling.patch b/patches/darling.patch index b399000..66477f9 100644 --- a/patches/darling.patch +++ b/patches/darling.patch @@ -10706,3 +10706,19 @@ index 0000000..1f3c473 + } +} +#endif +diff --git a/src/startup/mldr/elfcalls/threads.c b/src/startup/mldr/elfcalls/threads.c +index e789986..0d56b4e 100644 +--- a/src/startup/mldr/elfcalls/threads.c ++++ b/src/startup/mldr/elfcalls/threads.c +@@ -387,8 +387,9 @@ int __darling_thread_rpc_socket(void) { + // this is the main thread + t_server_socket = __dserver_main_thread_socket_fd; + } else { +- // threads should already have a per-thread socket assigned when they're created +- abort(); ++ // Native host-library threads do not have a Darling RPC connection. ++ // Let the caller report EPIPE, including the resize-signal bridge. ++ return -1; + } + } + return t_server_socket; diff --git a/patches/metal--deps--indium.patch b/patches/metal--deps--indium.patch index 09cd9a4..9ae8d9b 100644 --- a/patches/metal--deps--indium.patch +++ b/patches/metal--deps--indium.patch @@ -781,7 +781,7 @@ index 8546daa..7e4993c 100644 void updateBindings(); diff --git a/private-include/indium/device.private.hpp b/private-include/indium/device.private.hpp -index 58f1794..be7dbdc 100644 +index 58f1794..1f4bd5a 100644 --- a/private-include/indium/device.private.hpp +++ b/private-include/indium/device.private.hpp @@ -13,6 +13,8 @@ @@ -793,7 +793,7 @@ index 58f1794..be7dbdc 100644 namespace Indium { class PrivateDevice; -@@ -25,6 +27,8 @@ namespace Indium { +@@ -25,8 +27,11 @@ namespace Indium { class PrivateDevice: public Device, public std::enable_shared_from_this { private: @@ -801,8 +801,11 @@ index 58f1794..be7dbdc 100644 + std::vector> _scratchFree; std::mutex _eventLoopMutex; std::mutex _pollingMutex; ++ uint64_t _completionSpinNanoseconds = 0; -@@ -59,6 +63,8 @@ namespace Indium { + // these are stored separately (rather than in a single structure) + // so that we can easily copy them individually in the event loop polling function. +@@ -59,6 +64,8 @@ namespace Indium { } virtual std::string name() const override; @@ -811,7 +814,7 @@ index 58f1794..be7dbdc 100644 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 +79,12 @@ namespace Indium { +@@ -73,6 +80,12 @@ namespace Indium { virtual void pollEvents(uint64_t timeoutNanoseconds) override; virtual void wakeupEventLoop() override; @@ -824,7 +827,7 @@ index 58f1794..be7dbdc 100644 void waitForSemaphore(VkSemaphore semaphore, uint64_t targetValue, std::function callback); /** -@@ -86,6 +98,11 @@ namespace Indium { +@@ -86,6 +99,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. */ @@ -836,7 +839,7 @@ index 58f1794..be7dbdc 100644 std::shared_ptr getWrappedTimelineSemaphore(); BinarySemaphore getBinarySemaphore(bool exportable = false); -@@ -111,5 +128,34 @@ namespace Indium { +@@ -111,5 +129,34 @@ namespace Indium { INDIUM_PROPERTY(VkPhysicalDeviceMemoryProperties, m, M,emoryProperties); INDIUM_PROPERTY_READONLY(Feature, f, F,eatures); @@ -2835,17 +2838,20 @@ index 5f62ad0..5957bec 100644 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..78b77ae 100644 +index 322e186..4da420a 100644 --- a/src/indium/device.cpp +++ b/src/indium/device.cpp -@@ -1,4 +1,6 @@ +@@ -1,4 +1,9 @@ ++#include ++#include ++#include #include +#include +#include #include #include #include -@@ -12,39 +14,207 @@ +@@ -12,39 +17,207 @@ #include @@ -3059,7 +3065,7 @@ index 322e186..78b77ae 100644 continue; } -@@ -67,12 +237,105 @@ void Indium::initGlobalDeviceList() { +@@ -67,12 +240,105 @@ void Indium::initGlobalDeviceList() { DynamicVK::vkGetPhysicalDeviceFeatures2(device, &features); if (!features12.timelineSemaphore) { @@ -3155,11 +3161,11 @@ index 322e186..78b77ae 100644 + } + creationOrder.push_back(i); + } -+ } + } + + for (size_t index: creationOrder) { + globalDeviceList.push_back(std::make_shared(candidates[index].device)); - } ++ } + + appendDeviceLog("created VkDevice for " + std::to_string(globalDeviceList.size()) + " of " + + std::to_string(candidates.size()) + " supported device(s)" @@ -3167,7 +3173,7 @@ index 322e186..78b77ae 100644 }; void Indium::finitGlobalDeviceList() { -@@ -83,7 +346,8 @@ Indium::Device::~Device() {}; +@@ -83,8 +349,16 @@ Indium::Device::~Device() {}; Indium::PrivateDevice::PrivateDevice(VkPhysicalDevice physicalDevice): _physicalDevice(physicalDevice) @@ -3175,9 +3181,17 @@ index 322e186..78b77ae 100644 +{ Indium::SubmitThread::ensureStarted(); + DynamicVK::vkGetPhysicalDeviceProperties(_physicalDevice, &_properties); ++ _completionSpinNanoseconds = completionSpinBudget(_properties.vendorID, nullptr); ++ if (FILE* file = fopen("/tmp/metal-wait-spin-us", "r")) { ++ char value[64] = {}; ++ if (fscanf(file, "%63s", value) == 1) ++ _completionSpinNanoseconds = completionSpinBudget(_properties.vendorID, value); ++ fclose(file); ++ } std::vector queueFamilies; -@@ -233,6 +497,36 @@ Indium::PrivateDevice::PrivateDevice(VkPhysicalDevice physicalDevice): + uint32_t count = 0; +@@ -233,6 +507,36 @@ Indium::PrivateDevice::PrivateDevice(VkPhysicalDevice physicalDevice): } } @@ -3214,7 +3228,7 @@ index 322e186..78b77ae 100644 VkDeviceCreateInfo deviceCreateInfo {}; deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; deviceCreateInfo.pQueueCreateInfos = queueCreateInfos.data(); -@@ -246,6 +540,7 @@ Indium::PrivateDevice::PrivateDevice(VkPhysicalDevice physicalDevice): +@@ -246,6 +550,7 @@ Indium::PrivateDevice::PrivateDevice(VkPhysicalDevice physicalDevice): } _features = indiumFeatures; @@ -3222,7 +3236,7 @@ index 322e186..78b77ae 100644 for (const auto& index: queueFamilyIndices) { VkQueue queue; -@@ -297,6 +592,9 @@ Indium::PrivateDevice::PrivateDevice(VkPhysicalDevice physicalDevice): +@@ -297,6 +602,9 @@ Indium::PrivateDevice::PrivateDevice(VkPhysicalDevice physicalDevice): }; Indium::PrivateDevice::~PrivateDevice() { @@ -3232,7 +3246,7 @@ index 322e186..78b77ae 100644 if (_oneshotCommandPool) { DynamicVK::vkDestroyCommandPool(_device, _oneshotCommandPool, nullptr); } -@@ -308,6 +606,23 @@ std::string Indium::PrivateDevice::name() const { +@@ -308,6 +616,23 @@ std::string Indium::PrivateDevice::name() const { return _properties.deviceName; }; @@ -3256,7 +3270,7 @@ index 322e186..78b77ae 100644 std::shared_ptr Indium::PrivateDevice::newCommandQueue() { return std::make_shared(shared_from_this()); }; -@@ -317,9 +632,9 @@ std::shared_ptr Indium::PrivateDevice::newRenderPip +@@ -317,9 +642,9 @@ std::shared_ptr Indium::PrivateDevice::newRenderPip }; std::shared_ptr Indium::PrivateDevice::newComputePipelineState(const ComputePipelineDescriptor& descriptor, PipelineOption options, std::shared_ptr reflection) { @@ -3269,7 +3283,7 @@ index 322e186..78b77ae 100644 return std::make_shared(shared_from_this(), descriptor); }; -@@ -357,6 +672,7 @@ std::shared_ptr Indium::PrivateDevice::newLibrary(const void* d +@@ -357,6 +682,7 @@ std::shared_ptr Indium::PrivateDevice::newLibrary(const void* d break; } @@ -3277,7 +3291,34 @@ index 322e186..78b77ae 100644 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 +796,18 @@ void Indium::PrivateDevice::waitForSemaphore(VkSemaphore semaphore, uint64_t tar +@@ -396,6 +722,26 @@ void Indium::PrivateDevice::pollEvents(uint64_t timeoutNanoseconds) { + + lock.unlock(); + ++ // NVIDIA's blocking timeline wait can wake 1-2 ms after completion. Poll ++ // briefly while work is pending; an idle device should block immediately. ++ if (_completionSpinNanoseconds && timeoutNanoseconds && semaphores.size() > 1) { ++ const auto started = std::chrono::steady_clock::now(); ++ const auto deadline = started + std::chrono::nanoseconds( ++ std::min(_completionSpinNanoseconds, timeoutNanoseconds)); ++ bool anyReady = false; ++ do { ++ for (size_t i = 0; i < semaphores.size() && !anyReady; ++i) { ++ uint64_t count = 0; ++ anyReady = DynamicVK::vkGetSemaphoreCounterValue(_device, semaphores[i], &count) == VK_SUCCESS && count >= values[i]; ++ } ++ if (anyReady) break; ++ __builtin_ia32_pause(); ++ } while (std::chrono::steady_clock::now() < deadline); ++ const uint64_t elapsed = std::chrono::duration_cast( ++ std::chrono::steady_clock::now() - started).count(); ++ timeoutNanoseconds = anyReady ? 0 : timeoutNanoseconds - std::min(timeoutNanoseconds, elapsed); ++ } ++ + VkSemaphoreWaitInfo info {}; + info.sType = VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO; + info.semaphoreCount = semaphores.size(); +@@ -480,9 +826,18 @@ void Indium::PrivateDevice::waitForSemaphore(VkSemaphore semaphore, uint64_t tar wakeupEventLoop(); }; @@ -3297,7 +3338,7 @@ index 322e186..78b77ae 100644 VkSemaphoreTypeCreateInfo typeInfo {}; typeInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO; typeInfo.semaphoreType = VK_SEMAPHORE_TYPE_TIMELINE; -@@ -505,7 +830,14 @@ Indium::TimelineSemaphore Indium::PrivateDevice::getTimelineSemaphore() { +@@ -505,7 +860,14 @@ Indium::TimelineSemaphore Indium::PrivateDevice::getTimelineSemaphore() { }; void Indium::PrivateDevice::putTimelineSemaphore(const TimelineSemaphore& semaphore) { @@ -3313,7 +3354,7 @@ index 322e186..78b77ae 100644 }; std::shared_ptr Indium::PrivateDevice::getWrappedTimelineSemaphore() { -@@ -551,3 +883,160 @@ std::shared_ptr Indium::PrivateDevice::getWrappedBinary +@@ -551,3 +913,160 @@ std::shared_ptr Indium::PrivateDevice::getWrappedBinary delete ptr; }); }; @@ -8767,3 +8808,27 @@ index 1b5876e..03af560 100644 }; void Iridium::DynamicLLVM::finit() { +diff --git a/private-include/indium/completion-spin.private.hpp b/private-include/indium/completion-spin.private.hpp +new file mode 100644 +index 0000000..962fdad +--- /dev/null ++++ b/private-include/indium/completion-spin.private.hpp +@@ -0,0 +1,18 @@ ++#pragma once ++#include ++ ++namespace Indium { ++// A malformed override uses the vendor default. Saturation bounds both CPU use ++// and the microseconds-to-nanoseconds conversion, even for arbitrarily long input. ++inline uint64_t completionSpinBudget(uint32_t vendorID, const char* overrideValue) { ++ const uint64_t fallback = vendorID == 0x10de ? 2000000 : 0; ++ if (!overrideValue || !*overrideValue) return fallback; ++ uint64_t microseconds = 0; ++ for (const char* p = overrideValue; *p; ++p) { ++ if (*p < '0' || *p > '9') return fallback; ++ microseconds = microseconds * 10 + (*p - '0'); ++ if (microseconds > 10000) microseconds = 10000; ++ } ++ return microseconds * 1000; ++} ++} diff --git a/patches/xnu.patch b/patches/xnu.patch index be0c967..efbf7e3 100644 --- a/patches/xnu.patch +++ b/patches/xnu.patch @@ -1576,7 +1576,7 @@ index dc8fbb8..171746a 100644 + __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 +index 206e92e..1a763fe 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 @@ -1595,7 +1595,7 @@ index 206e92e..0646cb7 100644 } if (signum == SIGNAL_SIGEXC_SUSPEND) { -@@ -267,13 +272,150 @@ static void state_from_kernel(struct linux_ucontext* ctxt, const void* tstate, c +@@ -267,13 +272,165 @@ static void state_from_kernel(struct linux_ucontext* ctxt, const void* tstate, c #endif } @@ -1672,10 +1672,25 @@ index 206e92e..0646cb7 100644 +} + +#endif ++ ++/* Native driver threads have no Darling RPC connection. Forward resize ++ * notifications to the guest main thread without allocating or using RPC. ++ * Other signals, including synchronous faults and queued payloads, retain ++ * their existing handling. Never swallow a failed forward. */ ++static int forward_native_resize_signal(int linux_signum, int status) { ++ if (status != -32 /* -LINUX_EPIPE */ || linux_signum != LINUX_SIGWINCH) ++ return 0; ++ long pid = LINUX_SYSCALL(__NR_getpid); ++ if (LINUX_SYSCALL(__NR_gettid) == pid) ++ return 0; ++ return LINUX_SYSCALL(__NR_tgkill, pid, pid, linux_signum) == 0; ++} + void sigexc_handler(int linux_signum, struct linux_siginfo* info, struct linux_ucontext* ctxt) { int status = dserver_rpc_interrupt_enter(); ++ if (forward_native_resize_signal(linux_signum, status)) ++ return; + int relaxed = 0; + +#ifdef __x86_64__ @@ -1747,7 +1762,7 @@ index 206e92e..0646cb7 100644 } 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 +@@ -359,7 +516,13 @@ void sigexc_handler(int linux_signum, struct linux_siginfo* info, struct linux_u sizeof(sa.sa_mask)); // Resend signal to self diff --git a/tests/test_nvidia_runtime.py b/tests/test_nvidia_runtime.py new file mode 100644 index 0000000..aaa37dd --- /dev/null +++ b/tests/test_nvidia_runtime.py @@ -0,0 +1,199 @@ +"""Compile the patched native-thread signal bridge and completion budget policy.""" +import shutil +import subprocess +from pathlib import Path +import tempfile +import unittest +import test_runtime_compatibility as sources + + +class NativeThreadChecks(unittest.TestCase): + patched = sources.CompatibilityChecks.patched + run_c = sources.CompatibilityChecks.run_c + + def test_resize_forwarding_is_limited_to_foreign_threads_and_successful_delivery(self): + source = self.patched('xnu', 'darling/src/libsystem_kernel/emulation/src/linux_premigration/signal/sigexc.c') + bridge = sources.function(source, 'forward_native_resize_signal') + self.run_c(''' +#include +#define LINUX_SIGWINCH 28 +#define __NR_getpid 1 +#define __NR_gettid 2 +#define __NR_tgkill 3 +static long tid=101, result=0; +static int sends; +static long fake_syscall(int number, ...) { + if (number==__NR_getpid) return 100; + if (number==__NR_gettid) return tid; + ++sends; return result; +} +#define LINUX_SYSCALL(...) fake_syscall(__VA_ARGS__) +''' + bridge + ''' +int main(void) { + assert(forward_native_resize_signal(28,-32)==1 && sends==1); + assert(forward_native_resize_signal(28,0)==0 && sends==1); + assert(forward_native_resize_signal(28,-9)==0 && sends==1); + for(int sig=1;sig<=64;++sig) + if(sig!=28) assert(forward_native_resize_signal(sig,-32)==0); + assert(sends==1); + tid=100; assert(forward_native_resize_signal(28,-32)==0 && sends==1); + tid=101; result=-3; + assert(forward_native_resize_signal(28,-32)==0 && sends==2); +} +''') + + def test_native_thread_without_rpc_socket_returns_an_error_instead_of_aborting(self): + source = self.patched('darling', 'src/startup/mldr/elfcalls/threads.c') + self.run_c(''' +#include +#include +#define SYS_gettid 1 +static int t_server_socket=-1, __dserver_main_thread_socket_fd=71; +static int tid=101; +static int getpid(void) { return 100; } +static int syscall(int number) { return tid; } +''' + sources.function(source, '__darling_thread_rpc_socket') + ''' +int main(void) { + assert(__darling_thread_rpc_socket()==-1); + t_server_socket=73; assert(__darling_thread_rpc_socket()==73); + t_server_socket=-1; tid=100; assert(__darling_thread_rpc_socket()==71); +} +''') + + def test_cpu_readback_skips_cached_surfaces_but_preserves_mixed_metal_trees(self): + source = self.patched('cocotron', 'QuartzCore/CALayerContext.m') + self.run_c('''#define _POSIX_C_SOURCE 200809L +#include +#include +typedef int BOOL; +typedef struct { int metal, full; } CALayer; +typedef struct { int width, height; } CGSize; +static BOOL hasMetalLayer(CALayer* layer) { return layer->metal; } +static BOOL hasFullWindowSurface(CALayer* layer, CGSize size) { return layer->full; } +''' + sources.function(source, 'skipLocalRender') + ''' +int main(void) { + CGSize size={16,16}; + assert(!skipLocalRender(NULL,size)); + for(int shortcut=0;shortcut<=1;++shortcut) + for(int direct=0;direct<=1;++direct) + for(int metal=0;metal<=1;++metal) + for(int full=0;full<=1;++full) { + CALayer layer={metal,full}; + if(shortcut) setenv("DARLING_CA_SKIP_LOCAL_IOSURFACE_RENDER","1",1); + else unsetenv("DARLING_CA_SKIP_LOCAL_IOSURFACE_RENDER"); + if(direct) setenv("RCLOL_GPU_DIRECT_PRESENT","1",1); + else unsetenv("RCLOL_GPU_DIRECT_PRESENT"); + assert(!!skipLocalRender(&layer,size)==(shortcut && !metal && (!direct || full))); + } +} +''') + + +@unittest.skipUnless(shutil.which('c++'), 'C++ compiler required') +class CompletionBudgetChecks(unittest.TestCase): + patched = sources.CompatibilityChecks.patched + + def test_vendor_defaults_disable_clamping_and_malformed_overrides(self): + header = self.patched('metal--deps--indium', 'private-include/indium/completion-spin.private.hpp') + with tempfile.TemporaryDirectory() as folder: + path = Path(folder) + (path / 'budget.hpp').write_text(header) + (path / 'check.cpp').write_text(''' +#include "budget.hpp" +#include +#include +int main() { + using Indium::completionSpinBudget; + assert(completionSpinBudget(0x1002,nullptr)==0); + assert(completionSpinBudget(0x10de,nullptr)==2000000); + assert(completionSpinBudget(0x1002,nullptr)==0); + assert(completionSpinBudget(0x10de,"0")==0); + assert(completionSpinBudget(0x1002,"15")==15000); + assert(completionSpinBudget(0x10de,"10001")==10000000); + const std::string huge(10000,'9'); + assert(completionSpinBudget(0x10de,huge.c_str())==10000000); + for(const char* invalid : {"", "-1", "1ms", "18446744073709551616x"}) { + assert(completionSpinBudget(0x10de,invalid)==2000000); + assert(completionSpinBudget(0x1002,invalid)==0); + } +} +''') + subprocess.run(['c++','-std=c++17',str(path/'check.cpp'),'-o',str(path/'check')], check=True, capture_output=True) + subprocess.run([str(path/'check')], check=True, capture_output=True, timeout=10) + + def test_polling_preserves_idle_blocking_callbacks_and_timeout_budget(self): + source = self.patched('metal--deps--indium', 'src/indium/device.cpp') + poll = sources.function(source, 'Indium::PrivateDevice::pollEvents') + harness = ''' +#include +#include +#include +#include +#include +#include +#include +#include +using VkSemaphore = int; +struct VkSemaphoreWaitInfo { + int sType, flags; + size_t semaphoreCount; + const VkSemaphore* pSemaphores; + const uint64_t* pValues; +}; +constexpr int VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO=1, VK_SEMAPHORE_WAIT_ANY_BIT=1; +constexpr int VK_SUCCESS=0, VK_TIMEOUT=1; +static int counterQueries, waitResult=VK_TIMEOUT; +static uint64_t lastTimeout; +static bool ready; +namespace DynamicVK { +int vkGetSemaphoreCounterValue(int, int semaphore, uint64_t* count) { + ++counterQueries; *count=ready && semaphore==2 ? 1 : 0; return VK_SUCCESS; +} +int vkWaitSemaphores(int, const VkSemaphoreWaitInfo*, uint64_t timeout) { + lastTimeout=timeout; return waitResult; +} +} +namespace Indium { +class PrivateDevice { +public: + std::mutex _pollingMutex, _eventLoopMutex; + int _device=0; + uint64_t _completionSpinNanoseconds=1000000; + std::vector _eventLoopSemaphores={1}; + std::vector _eventLoopWaitValues={1}; + std::vector> _eventLoopCallbacks={nullptr}; + void pollEvents(uint64_t timeoutNanoseconds); +}; +} +''' + poll + ''' +int main() { + Indium::PrivateDevice device; + device.pollEvents(2000000); + assert(counterQueries==0 && lastTimeout==2000000); + int callbacks=0; + device._eventLoopSemaphores.push_back(2); + device._eventLoopWaitValues.push_back(1); + device._eventLoopCallbacks.push_back([&]{++callbacks;}); + device.pollEvents(2000000); + assert(counterQueries>0 && lastTimeout<2000000 && callbacks==0); + ready=true; waitResult=VK_SUCCESS; + device.pollEvents(2000000); + assert(lastTimeout==0 && callbacks==1); + assert(device._eventLoopSemaphores.size()==1); + counterQueries=0; waitResult=VK_TIMEOUT; + device._eventLoopSemaphores.push_back(2); + device._eventLoopWaitValues.push_back(1); + device._eventLoopCallbacks.push_back(nullptr); + device.pollEvents(0); + assert(counterQueries==0 && lastTimeout==0); + device._completionSpinNanoseconds=0; + device.pollEvents(2000000); + assert(counterQueries==0 && lastTimeout==2000000); +} +''' + with tempfile.TemporaryDirectory() as folder: + path = Path(folder) + (path / 'poll.cpp').write_text(harness) + subprocess.run(['c++', '-std=c++17', '-pthread', str(path/'poll.cpp'), '-o', str(path/'poll')], + check=True, capture_output=True) + subprocess.run([str(path/'poll')], check=True, capture_output=True, timeout=10) diff --git a/tools/riot-ca-render-policy-check.mm b/tools/riot-ca-render-policy-check.mm index 5d3d23a..ad21b97 100644 --- a/tools/riot-ca-render-policy-check.mm +++ b/tools/riot-ca-render-policy-check.mm @@ -38,6 +38,7 @@ int main() { // No NSApplication, window, graphics context, GPU work, or published IOSurface. // Objects intentionally live until process exit; the sentinel is never passed to CGL. [NSAutoreleasePool new]; + unsetenv("RCLOL_GPU_DIRECT_PRESENT"); int dimensions[]={16,16,4,64}; const void* keys[]={kIOSurfaceWidth,kIOSurfaceHeight,kIOSurfaceBytesPerElement,kIOSurfaceBytesPerRow}; const void* values[4]; @@ -77,6 +78,24 @@ int main() { [surfaceOnly render]; printf("surface_only_shortcut_enabled: renderCalls=%u expected=0\n",renderCalls); if(renderCalls!=0) return 6; + // Partial IOSurfaces bypass local rendering only in CPU-readback mode. + int partialDimensions[]={8,8,4,32}; + for(unsigned i=0;i<4;++i) values[i]=CFNumberCreate(NULL,kCFNumberIntType,&partialDimensions[i]); + CFDictionaryRef partialProperties=CFDictionaryCreate(NULL,keys,values,4,&kCFTypeDictionaryKeyCallBacks,&kCFTypeDictionaryValueCallBacks); + IOSurfaceRef partial=IOSurfaceCreate(partialProperties); + if(!partial) return 8; + RenderPolicySurfaceLayer* partialLayer=[[RenderPolicySurfaceLayer alloc] initWithSurface:partial]; + RenderPolicyContext* partialContext=[[RenderPolicyContext alloc] initWithRoot:partialLayer]; + renderCalls=0; + [partialContext render]; + if(renderCalls!=0) return 9; + setenv("RCLOL_GPU_DIRECT_PRESENT","1",1); + [partialContext render]; + if(renderCalls!=1) return 10; + unsetenv("DARLING_CA_SKIP_LOCAL_IOSURFACE_RENDER"); + unsetenv("RCLOL_GPU_DIRECT_PRESENT"); + [partialContext render]; + if(renderCalls!=2) return 11; printf("PASS: mixed-tree Metal preparation survives IOSurface shortcut\n"); return 0; } diff --git a/upstream/manifest.json b/upstream/manifest.json index 7ad829f..0629351 100644 --- a/upstream/manifest.json +++ b/upstream/manifest.json @@ -8,7 +8,7 @@ "revision": "60ba801decee7a00782f74f6be4c8ffb013f79ff", "repository": "https://github.com/darlinghq/darling.git", "patch": "patches/darling.patch", - "modified_files": 88, + "modified_files": 89, "new_files": [ "src/CoreAudio/CoreAudio/AudioConverterShim.c", "src/CoreAudio/AudioTrace.h", @@ -307,7 +307,8 @@ "private-include/indium/submission-batch.private.hpp", "private-include/indium/submit-batch.private.hpp", "private-include/indium/submit-thread.private.hpp", - "private-include/indium/waits.private.hpp" + "private-include/indium/waits.private.hpp", + "private-include/indium/completion-spin.private.hpp" ] }, {