From 9ea26f54789ab12755f0a6dcbc5e0619bee71aa0 Mon Sep 17 00:00:00 2001 From: swellweb Date: Thu, 1 Oct 2026 13:22:47 +0200 Subject: [PATCH 1/3] fix(input): keep mouse relay off the logging hot path --- .../TBDisplaySenderService.swift | 18 ++++- .../TBDisplayShared/TBInputDebugLog.swift | 68 ++++++++++++++----- 2 files changed, 65 insertions(+), 21 deletions(-) diff --git a/TargetBridge-Sender/TBDisplaySender/TBDisplaySenderService.swift b/TargetBridge-Sender/TBDisplaySender/TBDisplaySenderService.swift index 53bf1712..480b09ac 100644 --- a/TargetBridge-Sender/TBDisplaySender/TBDisplaySenderService.swift +++ b/TargetBridge-Sender/TBDisplaySender/TBDisplaySenderService.swift @@ -1274,6 +1274,7 @@ final class TBDisplaySenderSession: NSObject, ObservableObject, Identifiable, @u private var cursorDisplayID: CGDirectDisplayID = kCGNullDirectDisplay private var lastCursorPacket: TBMonitorCursor? private var injectedRemoteMouseLocation: CGPoint? + private var cachedLocalInputEventSource: CGEventSource? private var injectedLeftClickTracker = TBInjectedClickStateTracker() private let localPointerModifierBridge = TBLocalPointerModifierBridge() private var injectedCommandDown = false @@ -2117,14 +2118,21 @@ final class TBDisplaySenderSession: NSObject, ObservableObject, Identifiable, @u } private func localInputEventSource() -> CGEventSource? { + if let cachedLocalInputEventSource { + return cachedLocalInputEventSource + } let source = CGEventSource(stateID: .hidSystemState) source?.localEventsSuppressionInterval = 0 + cachedLocalInputEventSource = source return source } private func logLocalInputInjectionStateIfNeeded(context: String) { - let trusted = AXIsProcessTrusted() - TBInputDebugLog.log("sender input injection state trusted=\(trusted) context=\(context)") + if context == "mouseMove" { + TBInputDebugLog.logInputEvent("sender input injection state trusted=\(AXIsProcessTrusted()) context=\(context)") + } else { + TBInputDebugLog.log("sender input injection state trusted=\(AXIsProcessTrusted()) context=\(context)") + } } private func postLocalMouseMove(dx: Int, dy: Int, type: CGEventType = .mouseMoved, button: CGMouseButton = .left) { @@ -2277,7 +2285,11 @@ final class TBDisplaySenderSession: NSObject, ObservableObject, Identifiable, @u } private func applyIncomingInputEvent(_ event: TBMonitorInputEvent, payload: Data) { - TBInputDebugLog.log("sender applying incoming event kind=\(event.kind)") + if event.kind == "move" || event.kind.hasSuffix("Drag") || event.kind == "scroll" { + TBInputDebugLog.logInputEvent("sender applying incoming event kind=\(event.kind)") + } else { + TBInputDebugLog.log("sender applying incoming event kind=\(event.kind)") + } switch event.kind { case "move": postLocalMouseMove(dx: event.dx ?? 0, dy: event.dy ?? 0) diff --git a/TargetBridge-Sender/TBDisplayShared/TBInputDebugLog.swift b/TargetBridge-Sender/TBDisplayShared/TBInputDebugLog.swift index acdef6af..0c6af063 100644 --- a/TargetBridge-Sender/TBDisplayShared/TBInputDebugLog.swift +++ b/TargetBridge-Sender/TBDisplayShared/TBInputDebugLog.swift @@ -1,37 +1,69 @@ import Foundation -@MainActor -enum TBInputDebugLog { - private static let fileManager = FileManager.default +private final class TBAsyncInputDebugLogWriter: @unchecked Sendable { + private let fileManager = FileManager.default + private let writeQueue = DispatchQueue(label: "fd.tbdisplaysender.input-debug-log", qos: .utility) - private static var logURL: URL { + func enqueue(_ message: String) { + writeQueue.async { [self] in + write(message) + } + } + + private func write(_ message: String) { let base = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first ?? URL(fileURLWithPath: NSHomeDirectory()).appendingPathComponent("Library/Application Support", isDirectory: true) - return base + let url = base .appendingPathComponent("TargetBridge", isDirectory: true) .appendingPathComponent("Logs", isDirectory: true) .appendingPathComponent("input-debug.log", isDirectory: false) - } - - static func log(_ message: String) { let timestamp = ISO8601DateFormatter().string(from: Date()) let line = "\(timestamp) \(message)\n" - let url = logURL let directory = url.deletingLastPathComponent() try? fileManager.createDirectory(at: directory, withIntermediateDirectories: true) - if let data = line.data(using: .utf8) { - if fileManager.fileExists(atPath: url.path) { - if let handle = try? FileHandle(forWritingTo: url) { - try? handle.seekToEnd() - try? handle.write(contentsOf: data) - try? handle.close() - } - } else { - try? data.write(to: url, options: .atomic) + guard let data = line.data(using: .utf8) else { return } + if fileManager.fileExists(atPath: url.path) { + if let handle = try? FileHandle(forWritingTo: url) { + try? handle.seekToEnd() + try? handle.write(contentsOf: data) + try? handle.close() } + } else { + try? data.write(to: url, options: .atomic) } } +} + +@MainActor +enum TBInputDebugLog { + private static let writer = TBAsyncInputDebugLogWriter() + private static var inputEventCount: UInt64 = 0 + private static let inputEventBurstLimit: UInt64 = 20 + private static let inputEventSampleInterval: UInt64 = 100 + + private static var logURL: URL { + let base = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first + ?? URL(fileURLWithPath: NSHomeDirectory()).appendingPathComponent("Library/Application Support", isDirectory: true) + return base + .appendingPathComponent("TargetBridge", isDirectory: true) + .appendingPathComponent("Logs", isDirectory: true) + .appendingPathComponent("input-debug.log", isDirectory: false) + } + + static func log(_ message: String) { + writer.enqueue(message) + } + + /// Mouse motion can arrive hundreds of times per second. Keep the first few + /// records plus a periodic sample, without putting file I/O on the input path. + static func logInputEvent(_ message: @autoclosure () -> String) { + inputEventCount &+= 1 + let count = inputEventCount + + guard count <= inputEventBurstLimit || count.isMultiple(of: inputEventSampleInterval) else { return } + log(message()) + } static var currentLogPath: String { logURL.path From b6f54248ab3681e605e66764e46129ba655801b5 Mon Sep 17 00:00:00 2001 From: swellweb Date: Thu, 1 Oct 2026 13:24:16 +0200 Subject: [PATCH 2/3] fix(display): prefer HiDPI backing mode for render matching --- .../ReceiverBackedVirtualDisplaySession.swift | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/TargetBridge-Sender/TBDisplaySender/ReceiverBackedVirtualDisplaySession.swift b/TargetBridge-Sender/TBDisplaySender/ReceiverBackedVirtualDisplaySession.swift index d9564927..897ab0f5 100644 --- a/TargetBridge-Sender/TBDisplaySender/ReceiverBackedVirtualDisplaySession.swift +++ b/TargetBridge-Sender/TBDisplaySender/ReceiverBackedVirtualDisplaySession.swift @@ -156,6 +156,7 @@ final class ReceiverBackedVirtualDisplaySession { activatePreferredMode(for: display.displayID, mode: resolvedMode, refreshRate: preferredRefreshRate, + hiDPI: profile.hiDPI, savedChoice: savedChoice) virtualDisplay = display @@ -183,13 +184,14 @@ final class ReceiverBackedVirtualDisplaySession { private func activatePreferredMode(for displayID: CGDirectDisplayID, mode: TBVirtualDisplayModeSize, refreshRate: Double, + hiDPI: Bool, savedChoice: TBVirtualDisplayModeMemory.Choice?) -> Bool { let timeout = Date().addingTimeInterval(2.0) while Date() < timeout { var success = false autoreleasepool { let chosenMode = savedChoice.flatMap { savedMode(for: displayID, choice: $0) } - ?? preferredMode(for: displayID, mode: mode, refreshRate: refreshRate) + ?? preferredMode(for: displayID, mode: mode, refreshRate: refreshRate, hiDPI: hiDPI) if let chosenMode { success = CGDisplaySetDisplayMode(displayID, chosenMode, nil) == .success } @@ -223,8 +225,16 @@ final class ReceiverBackedVirtualDisplaySession { return candidates.first } - private func preferredMode(for displayID: CGDirectDisplayID, mode: TBVirtualDisplayModeSize, refreshRate: Double) -> CGDisplayMode? { - guard let modesCF = CGDisplayCopyAllDisplayModes(displayID, nil) else { + /// Select the mode by its backing pixels as well as its logical size. A + /// virtual HiDPI display exposes both a Retina 2x and a low-resolution 1x + /// variant that can report the same logical 2560x1440 size. Choosing only + /// by points made Render Matching restore the blurry 1x variant. + private func preferredMode(for displayID: CGDirectDisplayID, + mode: TBVirtualDisplayModeSize, + refreshRate: Double, + hiDPI: Bool) -> CGDisplayMode? { + let options = [kCGDisplayShowDuplicateLowResolutionModes: kCFBooleanTrue] as CFDictionary + guard let modesCF = CGDisplayCopyAllDisplayModes(displayID, options) else { return nil } let modes = modesCF as? [CGDisplayMode] ?? [] @@ -233,10 +243,16 @@ final class ReceiverBackedVirtualDisplaySession { candidate.width == mode.width && candidate.height == mode.height }.sorted { $0.refreshRate > $1.refreshRate } - if let exactMatch = matchingModes.first(where: { abs($0.refreshRate - refreshRate) < 0.5 }) { + let expectedPixelWidth = hiDPI ? mode.backingWidth : mode.width + let expectedPixelHeight = hiDPI ? mode.backingHeight : mode.height + let backingMatches = matchingModes.filter { candidate in + candidate.pixelWidth == expectedPixelWidth && candidate.pixelHeight == expectedPixelHeight + } + + if let exactMatch = backingMatches.first(where: { abs($0.refreshRate - refreshRate) < 0.5 }) { return exactMatch } - return matchingModes.first + return backingMatches.first ?? matchingModes.first } } From 3a74aad381ffdf4ad91203c8e668bebeb84c52ec Mon Sep 17 00:00:00 2001 From: swellweb Date: Thu, 1 Oct 2026 13:25:29 +0200 Subject: [PATCH 3/3] fix(network): declare local network and Bonjour access --- TargetBridge-Sender/TargetBridgeSupport/Info.plist | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/TargetBridge-Sender/TargetBridgeSupport/Info.plist b/TargetBridge-Sender/TargetBridgeSupport/Info.plist index 66460518..cc8a32cc 100644 --- a/TargetBridge-Sender/TargetBridgeSupport/Info.plist +++ b/TargetBridge-Sender/TargetBridgeSupport/Info.plist @@ -12,6 +12,12 @@ TargetBridge requires screen recording permission to capture the virtual display or mirror the desktop to the iMac. NSAppleEventsUsageDescription TargetBridge uses System Events to run shortcuts selected for Receiver is Master. + NSBonjourServices + + _targetbridge._tcp + + NSLocalNetworkUsageDescription + TargetBridge connects to your receiver over Thunderbolt Bridge or your local network. CFBundleURLTypes