add main runloop tickler

This commit is contained in:
Steve Kirbach 2024-02-16 09:08:18 -08:00
parent e2d352467b
commit 4c246a1352
3 changed files with 125 additions and 1 deletions

View File

@ -24,6 +24,13 @@ The bindings are generated from WinMD files, found in NuGet packages on Nuget.or
Please file any issues you have with this repository on https://github.com/thebrowsercompany/swift-winrt
## Known Issues and Limitations
- Only x64 architecture is supported for now
- The developer experience for consuming WinRT APIs from Swift is a work in progress. Due to current limitations, not all APIs can be generated as this causes export limit issues.
- The APIs listed in projections.json are required for the other `swift-*` projects to build. Modify a projections.json in any one of those projects could require an update here.
## Using WinUI
In order to use WinUI, you need to download the Windows App SDK from here: https://aka.ms/windowsappsdk/1.5/1.5.240205001-preview1/windowsappruntimeinstall-x64.exe

View File

@ -0,0 +1,109 @@
import Foundation
import WinSDK
/// Ensures that RunLoop.main gets drained in the context of a traditional Win32
/// message loop running on the main thread.
final class MainRunLoopTickler {
private var timerID: UINT_PTR = 0
private var readyToProcessMessages = false
private var doWorkRecursionGuard = false
fileprivate static let minIdleDelay: TimeInterval = 0.05
fileprivate static let maxIdleDelay: TimeInterval = 1
private static let doWorkMessage = UINT(WM_USER + 0xbc0)
/// At any point in time `nextIdleDelay` describes the delay we'll schedule between the next runloop service and the one after that. Each time we have to wait for this delay,
/// we will wait slightly longer on the next go-around (up to a max delay of `maxIdleDelay`).
///
/// It is possible that the runloop will be serviced in the meantime (due to the `WH_CALLWNDPROCRET` hook), in which case we will reset this delay back to its minimum value.
private var nextIdleDelay: TimeInterval = MainRunLoopTickler.minIdleDelay
fileprivate static let instance: MainRunLoopTickler = .init()
static func setup() {
instance.start()
}
static func shutdown() {
instance.shutdown()
}
private var hook: HHOOK?
private func start() {
// Hook all window message processing on the current thread. Use this as a
// trigger to process RunLoop events, in case some are pending.
hook = SetWindowsHookExW(WH_CALLWNDPROCRET, runLoopTicklerWindowHook, nil, GetCurrentThreadId())
scheduleImmediateWork()
}
fileprivate func scheduleDelayedWork(after delay: TimeInterval) {
// If requested to delay longer than 'nextIdleDelay', cap the delay at 'nextIdleDelay' and bump up 'nextIdleDelay' for the next time.
let cappedDelay: TimeInterval
if delay >= nextIdleDelay {
cappedDelay = nextIdleDelay
nextIdleDelay = min(nextIdleDelay + Self.minIdleDelay, Self.maxIdleDelay)
} else {
cappedDelay = max(delay, 0)
}
let delayMilliseconds = UInt32(cappedDelay * 1000)
timerID = SetTimer(nil, timerID, delayMilliseconds, runLoopTicklerTimerProc)
}
fileprivate func scheduleImmediateWork() {
// Whenever an immediate event comes in, reset our delay so that we will slowly decay in responsiveness when
// interaction ceases.
MainRunLoopTickler.instance.nextIdleDelay = MainRunLoopTickler.minIdleDelay
if readyToProcessMessages {
guard PostMessageW(nil, MainRunLoopTickler.doWorkMessage, 0, 0) else {
print("Failed to post message to message window. Win32 Error Code: \(GetLastError())")
return
}
} else {
scheduleDelayedWork(after: 0)
}
}
fileprivate func shutdown() {
UnhookWindowsHookEx(hook)
KillTimer(nil, timerID)
}
fileprivate func doWork() {
guard doWorkRecursionGuard == false else { return }
doWorkRecursionGuard = true
defer { doWorkRecursionGuard = false }
let nextDate = RunLoop.main.limitDate(forMode: .default)
// A nil result is unexpected, but if it happens, we'll just try again promptly.
let nextDelay = nextDate?.timeIntervalSinceNow ?? 0
// Messages created via PostMessageW have scheduling priority over UI messages. To invert the priority,
// scheduling immediate work is delayed if the thread has other pending messages. PostMessageW will
// ultimately be called after all queued messages have been flushed thanks to the WH_CALLWNDPROCRET hook.
scheduleDelayedWork(after: nextDelay)
}
}
private let runLoopTicklerWindowHook: HOOKPROC = { (nCode: Int32, wParam: WPARAM, lParam: LPARAM) in
if nCode >= 0 {
let ptr = UnsafeRawPointer(bitPattern: Int(lParam))?.assumingMemoryBound(to: CWPRETSTRUCT.self)
if let msgInfo = ptr?.pointee {
// Give higher priority to input events.
if (msgInfo.message >= WM_KEYFIRST && msgInfo.message < WM_KEYLAST)
|| (msgInfo.message >= WM_MOUSEFIRST && msgInfo.message < WM_MOUSELAST) {
// Instead of calling doWork here, schedule immediate work to be done. That way we avoid dispatching
// RunLoop events from this call stack, which might crash code that is not safe for re-entry.
MainRunLoopTickler.instance.scheduleImmediateWork()
} else if msgInfo.message != WM_GETICON {
// Windows may deliver periodic WM_GETICON messages without any user input, don't pay attention to
// these.
MainRunLoopTickler.instance.scheduleDelayedWork(after: 0)
}
}
}
return CallNextHookEx(nil, nCode, wParam, lParam)
}
private let runLoopTicklerTimerProc: TIMERPROC = { (_: HWND?, _: UINT, _: UINT_PTR, _: DWORD) in
MainRunLoopTickler.instance.doWork()
}

View File

@ -34,9 +34,13 @@ open class SwiftApplication: Application, IXamlMetadataProvider {
}
/// Override this method to provide your application's main entry point.
/// The first window for your application should be created and activated here.
open func onLaunched(_ args: LaunchActivatedEventArgs) {
}
/// Override this method to provide any necessary shutdown code.
open func onShutdown() { }
public static func main() {
do {
try withExtendedLifetime(WindowsAppRuntimeInitializer()) {
@ -44,9 +48,13 @@ open class SwiftApplication: Application, IXamlMetadataProvider {
guard let instance = NSClassFromString(appClass) else {
fatalError("unable to find application class \(appClass)")
}
var application: SwiftApplication!
Application.start { _ in
_ = (instance as! SwiftApplication.Type).init()
MainRunLoopTickler.setup()
application = (instance as! SwiftApplication.Type).init()
}
application.onShutdown()
MainRunLoopTickler.shutdown()
}
}
catch {