diff --git a/Sources/COpenSwiftUI/CoreVideo/DisplayLink.c b/Sources/COpenSwiftUI/CoreVideo/DisplayLink.c new file mode 100644 index 000000000..d2fb21b7f --- /dev/null +++ b/Sources/COpenSwiftUI/CoreVideo/DisplayLink.c @@ -0,0 +1,363 @@ +// +// DisplayLink.c +// COpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete + +#include "DisplayLink.h" + +#if OPENSWIFTUI_TARGET_OS_OSX + +#include +#include +#include + +extern void CVDisplayLinkSetPaused(CVDisplayLinkRef displayLink, bool paused); +static void remove_link(DisplayLinkRef displayLink); + +typedef struct DisplayLinkManager { + struct DisplayLinkManager *nextManager; + + CGDirectDisplayID displayID; + + uint32_t _padding1; + + CVDisplayLinkRef displayLink; + + struct DisplayLink *listHead; + + uint32_t totalDisplayLinks; + + uint32_t pausedDisplayLinks; + + uint32_t state; + + uint32_t _padding2; + + _Atomic uint64_t timestamp; +} DisplayLinkManager; + +struct DisplayLink { + uint32_t unknown_field_1; + + uint32_t _padding1; + + struct DisplayLinkManager *manager; + + struct DisplayLink *nextDisplayLink; + + DisplayLinkCallback callback; + + double nextTime; + + uint32_t state; + + struct { + uint8_t isScheduled; + uint8_t isDestroyed; + uint16_t _padding2; + } flags; +}; + +static os_unfair_lock link_lock = OS_UNFAIR_LOCK_INIT; +static dispatch_queue_t link_queue = NULL; +static DisplayLinkManager *link_list = NULL; + +static void dispatch_items(void *context) { + DisplayLinkManager *manager = (DisplayLinkManager *)context; + + double callback_time = *(double*)&manager->timestamp; + + os_unfair_lock_lock(&link_lock); + + DisplayLinkRef *links_to_process = NULL; + + size_t num_links = 0; + + struct DisplayLink *current = manager->listHead; + size_t count = 0; + while (current) { + count++; + current = current->nextDisplayLink; + } + + if (count > 0) { + links_to_process = (DisplayLinkRef *)alloca(count * sizeof(DisplayLinkRef)); + + current = manager->listHead; + while (current) { + os_unfair_lock_lock((os_unfair_lock_t)current); + + if (!current->flags.isDestroyed) { + if (current->nextTime == INFINITY) { + if (current->state != 0) { + current->state--; + if (current->state == 0) { + manager->pausedDisplayLinks++; + manager->state = 5; + } + } + } else { + links_to_process[num_links++] = current; + current->nextTime = INFINITY; + current->flags.isScheduled = 1; + } + } + + os_unfair_lock_unlock((os_unfair_lock_t)current); + current = current->nextDisplayLink; + } + } + + os_unfair_lock_unlock(&link_lock); + + for (size_t i = 0; i < num_links; i++) { + DisplayLinkRef link = links_to_process[i]; + if (link->callback) { + link->callback(link, callback_time); + } + } + + os_unfair_lock_lock(&link_lock); + + for (size_t i = 0; i < num_links; i++) { + DisplayLinkRef link = links_to_process[i]; + + os_unfair_lock_lock((os_unfair_lock_t)link); + link->flags.isScheduled = 0; + bool should_free = link->flags.isDestroyed; + os_unfair_lock_unlock((os_unfair_lock_t)link); + + if (should_free) { + remove_link(link); + Block_release(link->callback); + free(link); + } + } + + bool should_pause = false; + bool should_destroy = false; + + if (manager->pausedDisplayLinks == manager->totalDisplayLinks) { + if (manager->state != 0) { + manager->state--; + if (manager->state == 0 && manager->pausedDisplayLinks != 0) { + should_pause = true; + } + } + } + + if (manager->pausedDisplayLinks == 0 && + manager->totalDisplayLinks == 0) { + should_destroy = true; + + DisplayLinkManager **current_mgr = &link_list; + while (*current_mgr) { + if (*current_mgr == manager) { + *current_mgr = manager->nextManager; + break; + } + current_mgr = &(*current_mgr)->nextManager; + } + } + + os_unfair_lock_unlock(&link_lock); + + if (should_pause) { + CVDisplayLinkSetPaused(manager->displayLink, true); + } + + atomic_store(&manager->timestamp, 0); + + if (should_destroy) { + CVDisplayLinkRelease(manager->displayLink); + free(manager); + } + +} + +static CVReturn link_callback( + CVDisplayLinkRef displayLink, + const CVTimeStamp* inNow, + const CVTimeStamp* inOutputTime, + CVOptionFlags flagsIn, + CVOptionFlags* flagsOut, + void* displayLinkContext +) { + DisplayLinkManager* manager = (DisplayLinkManager*)displayLinkContext; + + double current_time = CACurrentMediaTime(); + uint64_t time_bits = *(uint64_t*)¤t_time; + + uint64_t expected = 0; + if (atomic_compare_exchange_strong(&manager->timestamp, &expected, time_bits)) { + dispatch_async_f(link_queue, manager, dispatch_items); + } + + return kCVReturnSuccess; +} + +static void remove_link(DisplayLinkRef displayLink) { + DisplayLinkManager *manager = displayLink->manager; + + struct DisplayLink **current = &manager->listHead; + while (*current) { + if (*current == displayLink) { + *current = displayLink->nextDisplayLink; + + manager->totalDisplayLinks--; + break; + } + current = &(*current)->nextDisplayLink; + } + + if (displayLink->state == 0) { + manager->pausedDisplayLinks--; + } +} + +DisplayLinkRef DisplayLinkCreate(CGDirectDisplayID displayID, DisplayLinkCallback callback) { + DisplayLinkManager *manager = NULL; + DisplayLinkRef displayLink = NULL; + + os_unfair_lock_lock(&link_lock); + + if (!link_queue) { + dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class( + DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INTERACTIVE, 0 + ); + link_queue = dispatch_queue_create("com.apple.SwiftUI.DisplayLink", attr); + } + + manager = link_list; + while (manager) { + if (manager->displayID == displayID) { + break; + } + manager = manager->nextManager; + } + + if (!manager) { + manager = (DisplayLinkManager *)calloc(1, sizeof(DisplayLinkManager)); + if (!manager) goto cleanup_and_fail; + + manager->displayID = displayID; + manager->state = 5; + + CVReturn status = CVDisplayLinkCreateWithCGDisplay(displayID, &manager->displayLink); + + if (status != kCVReturnSuccess) { + free(manager); + goto cleanup_and_fail; + } + + manager->nextManager = link_list; + link_list = manager; + + CVDisplayLinkSetOutputCallback(manager->displayLink, link_callback, manager); + CVDisplayLinkStart(manager->displayLink); + } + + displayLink = (DisplayLinkRef)calloc(1, sizeof(struct DisplayLink)); + + if (!displayLink) goto cleanup_and_fail; + + displayLink->manager = manager; + displayLink->callback = Block_copy(callback); + displayLink->nextTime = INFINITY; + displayLink->state = 0; + + displayLink->nextDisplayLink = manager->listHead; + manager->listHead = displayLink; + + manager->pausedDisplayLinks++; + manager->totalDisplayLinks++; + + os_unfair_lock_unlock(&link_lock); + return displayLink; +cleanup_and_fail: + os_unfair_lock_unlock(&link_lock); + return NULL; +} + +void DisplayLinkDestroy(DisplayLinkRef displayLink) { + if (!displayLink) return; + + os_unfair_lock_lock((os_unfair_lock_t)displayLink); + + bool wasScheduled = displayLink->flags.isScheduled; + displayLink->flags.isDestroyed = true; + + os_unfair_lock_unlock((os_unfair_lock_t)displayLink); + + if (wasScheduled) return; + + os_unfair_lock_lock(&link_lock); + + DisplayLinkManager *manager = displayLink->manager; + + remove_link(displayLink); + + bool shouldDestroyManager = (manager->totalDisplayLinks == 0 && + manager->state == 0); + + if (shouldDestroyManager) { + DisplayLinkManager **current = &link_list; + while (*current) { + if (*current == manager) { + *current = manager->nextManager; + break; + } + current = &(*current)->nextManager; + } + } + + os_unfair_lock_unlock(&link_lock); + + if (shouldDestroyManager) { + CVDisplayLinkRelease(manager->displayLink); + free(manager); + } + + Block_release(displayLink->callback); + free(displayLink); +} + +void DisplayLinkSetNextTime(DisplayLinkRef displayLink, double nextTime) { + if (!displayLink) return; + + os_unfair_lock_lock(&link_lock); + + if (displayLink->nextTime != nextTime) { + if (nextTime != INFINITY && displayLink->state == 0) { + + DisplayLinkManager *manager = displayLink->manager; + + manager->pausedDisplayLinks--; + + if (manager->state == 0) { + CVDisplayLinkSetPaused(manager->displayLink, false); + } + + manager->state = 5; + displayLink->state = 5; + } + + displayLink->nextTime = nextTime; + } + + os_unfair_lock_unlock(&link_lock); +} + +double DisplayLinkGetNextTime(DisplayLinkRef displayLink) { + if (!displayLink) return NAN; + + os_unfair_lock_lock(&link_lock); + double time = displayLink->nextTime; + os_unfair_lock_unlock(&link_lock); + + return time; +} + +#endif diff --git a/Sources/COpenSwiftUI/CoreVideo/DisplayLink.h b/Sources/COpenSwiftUI/CoreVideo/DisplayLink.h new file mode 100644 index 000000000..6bed909ae --- /dev/null +++ b/Sources/COpenSwiftUI/CoreVideo/DisplayLink.h @@ -0,0 +1,32 @@ +// +// DisplayLink.h +// COpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete + +#ifndef DisplayLink_h +#define DisplayLink_h + +#include "OpenSwiftUIBase.h" + +#if OPENSWIFTUI_TARGET_OS_OSX + +#include +#include + +typedef struct DisplayLink * OPENSWIFTUI_SWIFT_STRUCT DisplayLinkRef OPENSWIFTUI_SWIFT_NAME(DisplayLink); + +typedef void(^ DisplayLinkCallback)(DisplayLinkRef __nonnull, double); + +extern DisplayLinkRef __nullable DisplayLinkCreate(CGDirectDisplayID displayID, DisplayLinkCallback __nonnull callback); + +extern void DisplayLinkDestroy(DisplayLinkRef __nonnull displayLink); + +extern void DisplayLinkSetNextTime(DisplayLinkRef __nonnull displayLink, double nextTime); + +extern double DisplayLinkGetNextTime(DisplayLinkRef __nonnull displayLink); + +#endif + +#endif /* DisplayLink_h */ diff --git a/Sources/OpenSwiftUI/Integration/Hosting/AppKit/View/NSHostingView.swift b/Sources/OpenSwiftUI/Integration/Hosting/AppKit/View/NSHostingView.swift index 80c78feb9..ace8f65bd 100644 --- a/Sources/OpenSwiftUI/Integration/Hosting/AppKit/View/NSHostingView.swift +++ b/Sources/OpenSwiftUI/Integration/Hosting/AppKit/View/NSHostingView.swift @@ -11,6 +11,7 @@ public import OpenSwiftUICore public import AppKit import OpenSwiftUI_SPI +import COpenSwiftUI /// An AppKit view that hosts a SwiftUI view hierarchy. /// @@ -95,12 +96,20 @@ open class NSHostingView: NSView, XcodeViewDebugDataProvider where Cont } } + private var displayLink: DisplayLink? + package var externalUpdateCount: Int = .zero private var canAdvanceTimeAutomatically = true private var needsDeferredUpdate = false + package var updateTimer: Timer? + + package var lastUpdateTime: Time = .zero + + package var nextTimerTime: Time? + private var isPerformingLayout: Bool { if renderingPhase == .rendering { return true @@ -196,10 +205,26 @@ open class NSHostingView: NSView, XcodeViewDebugDataProvider where Cont context.allowsImplicitAnimation = false isUpdating = true // TODO - render(targetTimestamp: Time()) + render(targetTimestamp: nil) // TODO isUpdating = false // TODO + if needsDeferredUpdate { + if !viewGraph.updateRequiredMainThread { + if isLinkedOnOrAfter(.v3) { + if startAsyncRendering() { + needsDeferredUpdate = false + } + } + } + } + if needsDeferredUpdate { + onNextMainRunLoop { [weak self] in + guard let self else { return } + setNeedsUpdate() + } + needsDeferredUpdate = false + } } } @@ -248,11 +273,103 @@ open class NSHostingView: NSView, XcodeViewDebugDataProvider where Cont public final func _viewDebugData() -> [_ViewDebug.Data] { [] } func clearUpdateTimer() { - // TODO + guard Thread.isMainThread else { + return + } + updateTimer?.invalidate() + updateTimer = nil + nextTimerTime = nil + } + + private func startAsyncRendering() -> Bool { + if let displayLink { + DisplayLinkSetNextTime(displayLink, .zero) + return true + } + guard let window, + let screen = window.screen + else { + return false + } + + // TODO: screen.displayID + let displayID = CGMainDisplayID() + + let link = DisplayLinkCreate(displayID) { [weak self] link, seconds in + guard let self else { return } + Update.locked { + guard let displayLink = self.displayLink, displayLink == link else { + return + } + let targetTimestamp = Time(seconds: seconds) + self.advanceTime(with: targetTimestamp) + let nextRenderTime = self.renderAsync(targetTimestamp: nil) + if let nextRenderTime { + if nextRenderTime.seconds.isFinite && !self.viewGraph.updateRequiredMainThread { + let interval = max(nextRenderTime - self.currentTimestamp, 1e-6) + DisplayLinkSetNextTime(link, interval + seconds) + } else { + DisplayLinkDestroy(link) + self.displayLink = nil + + let targetTime: Time + if nextRenderTime.seconds.isFinite { + targetTime = Time(seconds: max(nextRenderTime - self.currentTimestamp, 1e-6)) + } else { + targetTime = .zero + } + onNextMainRunLoop { [weak self] in + guard let self else { return } + requestUpdate(after: targetTime.seconds) + } + } + CATransaction.flush() + } else { + DisplayLinkDestroy(link) + self.displayLink = nil + onNextMainRunLoop { [weak self] in + guard let self else { return } + requestUpdate(after: .zero) + } + } + } + } + displayLink = link + if let displayLink { + DisplayLinkSetNextTime(displayLink, .zero) + } + return displayLink != nil } func cancelAsyncRendering() { - // TODO + if let displayLink { + DisplayLinkDestroy(displayLink) + self.displayLink = nil + setNeedsUpdate() + } + } + + package func advanceTime(with time: Time) { + if lastUpdateTime.seconds == 0.0 || time < lastUpdateTime { + lastUpdateTime = time + return + } + + let timeDelta = time.seconds - lastUpdateTime.seconds + + var timeScaleFactor = 1.0 + + if NSEvent.modifierFlags.contains(.shift) { + if UserDefaults.standard.bool(forKey: "NSAnimationSlowMotionOnShift") { + timeScaleFactor = 10.0 + } + } + + let scaledDelta = timeDelta / timeScaleFactor + + currentTimestamp = Time(seconds: currentTimestamp.seconds + scaledDelta) + + lastUpdateTime = time } package func makeViewDebugData() -> Data? { @@ -261,6 +378,23 @@ open class NSHostingView: NSView, XcodeViewDebugDataProvider where Cont } } + func setUpdateTimer(delay: Double) { + let delay = max(delay, 0.1) + cancelAsyncRendering() + let updateTime = currentTimestamp + delay + guard updateTime < (nextTimerTime ?? .infinity) else { + return + } + updateTimer?.invalidate() + nextTimerTime = updateTime + updateTimer = withDelay(delay) { [weak self] in + guard let self else { return } + updateTimer = nil + nextTimerTime = nil + setNeedsUpdate() + } + } + static func defaultViewGraphOutputs() -> ViewGraph.Outputs { .defaults } func setNeedsUpdate() { @@ -353,9 +487,34 @@ extension NSHostingView: ViewRendererHost { } } - package func requestUpdate(after: Double) { - // TODO - setNeedsUpdate() + package func requestUpdate(after delay: Double) { + if Thread.isMainThread { + Update.locked { + cancelAsyncRendering() + } + + var adjustedDelay = delay + + if NSEvent.modifierFlags.contains(.shift), + UserDefaults.standard.bool(forKey: "NSAnimationSlowMotionOnShift") { + adjustedDelay *= 10.0 + } + + if adjustedDelay >= 0.25 { + setUpdateTimer(delay: adjustedDelay) + } else if isUpdating { + needsDeferredUpdate = true + } else { + setNeedsUpdate() + } + + // TODO: Notify delegate + } else { + onNextMainRunLoop { [weak self] in + guard let self else { return } + requestUpdate(after: delay) + } + } } } diff --git a/Sources/OpenSwiftUI_SPI/OpenSwiftUIBase.h b/Sources/OpenSwiftUI_SPI/OpenSwiftUIBase.h index 43d3ce60d..f9a077367 100644 --- a/Sources/OpenSwiftUI_SPI/OpenSwiftUIBase.h +++ b/Sources/OpenSwiftUI_SPI/OpenSwiftUIBase.h @@ -68,6 +68,12 @@ # define OPENSWIFTUI_SWIFT_NAME(_name) #endif +#if __has_attribute(swift_wrapper) +#define OPENSWIFTUI_SWIFT_STRUCT __attribute__((swift_wrapper(struct))) +#else +#define OPENSWIFTUI_SWIFT_STRUCT +#endif + #define OPENSWIFTUI_ENUM CF_ENUM #define OPENSWIFTUI_CLOSED_ENUM CF_CLOSED_ENUM #define OPENSWIFTUI_OPTIONS CF_OPTIONS