fix(nvidia): retain readback, resize and bounded completion fixes

Rebuild the remaining NVIDIA changes on current main, preserving the merged visual, draw-buffer, descriptor ownership and mixed Metal-tree fixes. Scope native-thread forwarding to resize notifications and make completion polling per device, bounded and part of the caller timeout budget.

Co-authored-by: Pirrandi <[email protected]>
This commit is contained in:
lucannez64andPirrandi committed 2026-10-18 13:59:49 +02:00
1 parent cb628a23f1
commit dceee00d10
8 files changed
+384 -39

No files matched your search

+24
View File
@@ -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.
+18 -12
View File
@@ -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 <Foundation/NSString.h>
+#import <Foundation/NSMutableArray.h>
#import <QuartzCore/CALayer.h>
@@ -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);
}
+16
View File
@@ -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;
+87 -22
View File
@@ -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<PrivateDevice> {
private:
@@ -801,8 +801,11 @@ index 58f1794..be7dbdc 100644
+ std::vector<std::shared_ptr<Buffer>> _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<CommandQueue> newCommandQueue() override;
virtual std::shared_ptr<RenderPipelineState> newRenderPipelineState(const RenderPipelineDescriptor& descriptor) override;
virtual std::shared_ptr<ComputePipelineState> newComputePipelineState(const ComputePipelineDescriptor& descriptor, PipelineOption options, std::shared_ptr<ComputePipelineReflection> 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<void()> 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<TimelineSemaphore> 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 <chrono>
+#include <cstdio>
+#include <indium/completion-spin.private.hpp>
#include <indium/device.private.hpp>
+#include <indium/submit-thread.private.hpp>
+#include <indium/command-encoder.private.hpp>
#include <indium/instance.private.hpp>
#include <indium/command-queue.private.hpp>
#include <indium/render-pipeline.private.hpp>
@@ -12,39 +14,207 @@
@@ -12,39 +17,207 @@
#include <iridium/iridium.hpp>
@@ -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<PrivateDevice>(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<VkQueueFamilyProperties> 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::CommandQueue> Indium::PrivateDevice::newCommandQueue() {
return std::make_shared<PrivateCommandQueue>(shared_from_this());
};
@@ -317,9 +632,9 @@ std::shared_ptr<Indium::RenderPipelineState> Indium::PrivateDevice::newRenderPip
@@ -317,9 +642,9 @@ std::shared_ptr<Indium::RenderPipelineState> Indium::PrivateDevice::newRenderPip
};
std::shared_ptr<Indium::ComputePipelineState> Indium::PrivateDevice::newComputePipelineState(const ComputePipelineDescriptor& descriptor, PipelineOption options, std::shared_ptr<ComputePipelineReflection> reflection) {
@@ -3269,7 +3283,7 @@ index 322e186..78b77ae 100644
return std::make_shared<PrivateComputePipelineState>(shared_from_this(), descriptor);
};
@@ -357,6 +672,7 @@ std::shared_ptr<Indium::Library> Indium::PrivateDevice::newLibrary(const void* d
@@ -357,6 +682,7 @@ std::shared_ptr<Indium::Library> 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::nanoseconds>(
+ 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::TimelineSemaphore> Indium::PrivateDevice::getWrappedTimelineSemaphore() {
@@ -551,3 +883,160 @@ std::shared_ptr<Indium::BinarySemaphore> Indium::PrivateDevice::getWrappedBinary
@@ -551,3 +913,160 @@ std::shared_ptr<Indium::BinarySemaphore> 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 <cstdint>
+
+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;
+}
+}
+18 -3
View File
@@ -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
+199
View File
@@ -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 <assert.h>
#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 <assert.h>
#include <stdlib.h>
#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 <assert.h>
#include <stdlib.h>
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 <cassert>
#include <string>
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 <algorithm>
#include <chrono>
#include <cassert>
#include <cstdint>
#include <cstdlib>
#include <functional>
#include <mutex>
#include <vector>
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<VkSemaphore> _eventLoopSemaphores={1};
std::vector<uint64_t> _eventLoopWaitValues={1};
std::vector<std::function<void()>> _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)
+19
View File
@@ -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;
}
+3 -2
View File
@@ -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"
]
},
{