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
}
}
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
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