Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion FreeDisplay.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
4AA949AC237EE0320969D676 /* DisplayModeListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F049322038220CBB75B6985 /* DisplayModeListView.swift */; };
4C8A0C530FDFF72F78F5DFD3 /* BrightnessHUDService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D50B61D6248DB61E562B4E3D /* BrightnessHUDService.swift */; };
4E6DA9CD1781AAB2A814E56A /* ArrangementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6804E6622DE956669AE0DBCC /* ArrangementView.swift */; };
4F57F12852AE8C6A0E98B51D /* DisplayConnectionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E6669E8E7560CCDA207539E /* DisplayConnectionService.swift */; };
5E6835D82CA20AB0673F102F /* NotchOverlayManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3739D4F9F8A958DE959C133E /* NotchOverlayManager.swift */; };
611792FF4019DE0B3D2D8091 /* HiDPIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2CFC4A7F963D418082D6B61 /* HiDPIView.swift */; };
63E0F610C2DB27B05C23D665 /* UpdateService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63BF0C6897ED7891956F2715 /* UpdateService.swift */; };
Expand All @@ -38,12 +39,14 @@
80F888F001C7F53E4FA362AD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 90F55D10A5241A8DFA941E4C /* Assets.xcassets */; };
87A763D8B6A6F9D9D9351AB7 /* MenuBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA0E0202B5E5AC58680DDDB /* MenuBarView.swift */; };
8BD7199384F977B50A5EAB5B /* MirrorService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 754025E6D6152C81F145C63C /* MirrorService.swift */; };
8EF0216DF111267D59FB4785 /* DisplayPowerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4765F29B2E36F40EDD0FE52 /* DisplayPowerView.swift */; };
94A44781BBE01DF170DF6F64 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C11B24C0698CE807BA1E28D0 /* AppDelegate.swift */; };
B170F04C4FDA2E9A8D0AD6B0 /* BrightnessService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6ED9828FF4F2823DDB03014 /* BrightnessService.swift */; };
B3452C06010BEFEEBF0FD1FC /* ResolutionSliderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 603658292A824E3A3968A4D7 /* ResolutionSliderView.swift */; };
CBC8B0900A104181893B69B2 /* ImageAdjustmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F22B3A7EF51E72B078901699 /* ImageAdjustmentView.swift */; };
CD3C6687C2B23F46A3678E94 /* NotchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2419A06679F0A55C454EF1F /* NotchView.swift */; };
D14D979F3149D232244DD701 /* MainDisplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4720147B44C6328686440A16 /* MainDisplayView.swift */; };
D4A9A35703129085CA5A6EFA /* DisplayPowerService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C4AAB1941D982CA85BFA199 /* DisplayPowerService.swift */; };
DEFA538D0EED4A16A0DC987D /* NSScreenExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59F1FD9EEC72051EBC3473C1 /* NSScreenExtension.swift */; };
E061011F318208D7FF94D963 /* CGHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5BF8AFF876572E97D370A0 /* CGHelpers.swift */; };
E201AC4CE5BCEC009C926206 /* ArrangementService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22030BFC8A680A596A5764C9 /* ArrangementService.swift */; };
Expand All @@ -54,6 +57,7 @@

/* Begin PBXFileReference section */
0509F1D7107310B84B34B825 /* LaunchService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchService.swift; sourceTree = "<group>"; };
0C4AAB1941D982CA85BFA199 /* DisplayPowerService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayPowerService.swift; sourceTree = "<group>"; };
0C8DE3994AD8E543BA765829 /* VirtualDisplayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VirtualDisplayView.swift; sourceTree = "<group>"; };
181D9C66D6F3D332AAC9AC0B /* SystemColorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemColorView.swift; sourceTree = "<group>"; };
1C7E2B33E54A6BAC64A84990 /* DisplayDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayDetailView.swift; sourceTree = "<group>"; };
Expand All @@ -62,6 +66,7 @@
3739D4F9F8A958DE959C133E /* NotchOverlayManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchOverlayManager.swift; sourceTree = "<group>"; };
3B6EC27CA822E1D2F38769E0 /* FreeDisplay.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FreeDisplay.app; sourceTree = BUILT_PRODUCTS_DIR; };
4720147B44C6328686440A16 /* MainDisplayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainDisplayView.swift; sourceTree = "<group>"; };
4E6669E8E7560CCDA207539E /* DisplayConnectionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayConnectionService.swift; sourceTree = "<group>"; };
4F6823DA8C05CB0B6070AA74 /* ColorProfileService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorProfileService.swift; sourceTree = "<group>"; };
58474F9C406F992241963112 /* SavePresetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavePresetView.swift; sourceTree = "<group>"; };
598AEDB5733E6B0E108E9EB5 /* AutoBrightnessView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoBrightnessView.swift; sourceTree = "<group>"; };
Expand All @@ -86,6 +91,7 @@
BEC02E3AAD0509479979CBEA /* FreeDisplay.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FreeDisplay.entitlements; sourceTree = "<group>"; };
C11B24C0698CE807BA1E28D0 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
C2419A06679F0A55C454EF1F /* NotchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchView.swift; sourceTree = "<group>"; };
C4765F29B2E36F40EDD0FE52 /* DisplayPowerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayPowerView.swift; sourceTree = "<group>"; };
C6ED9828FF4F2823DDB03014 /* BrightnessService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrightnessService.swift; sourceTree = "<group>"; };
C76FBEDC2A57F5FBBEE11AA0 /* PresetService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresetService.swift; sourceTree = "<group>"; };
C79B61823CE0057EF1E64215 /* ColorProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorProfileView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -137,7 +143,9 @@
DB5BF8AFF876572E97D370A0 /* CGHelpers.swift */,
4F6823DA8C05CB0B6070AA74 /* ColorProfileService.swift */,
719FFBCF3D3A63A6FFFC7172 /* DDCService.swift */,
4E6669E8E7560CCDA207539E /* DisplayConnectionService.swift */,
C8919AFF5445E0571C06B148 /* DisplayManager.swift */,
0C4AAB1941D982CA85BFA199 /* DisplayPowerService.swift */,
7419D1AC41D1C369FFDCD09D /* GammaService.swift */,
819474C0E46B105CBBFE7A7F /* HiDPIService.swift */,
0509F1D7107310B84B34B825 /* LaunchService.swift */,
Expand Down Expand Up @@ -204,6 +212,7 @@
C79B61823CE0057EF1E64215 /* ColorProfileView.swift */,
1C7E2B33E54A6BAC64A84990 /* DisplayDetailView.swift */,
6F049322038220CBB75B6985 /* DisplayModeListView.swift */,
C4765F29B2E36F40EDD0FE52 /* DisplayPowerView.swift */,
D2CFC4A7F963D418082D6B61 /* HiDPIView.swift */,
F22B3A7EF51E72B078901699 /* ImageAdjustmentView.swift */,
4720147B44C6328686440A16 /* MainDisplayView.swift */,
Expand Down Expand Up @@ -254,7 +263,6 @@
};
};
buildConfigurationList = 8EA410054BD32F42B7BB9BC3 /* Build configuration list for PBXProject "FreeDisplay" */;
compatibilityVersion = "Xcode 14.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
Expand All @@ -264,6 +272,7 @@
mainGroup = AD8C2B4D7F682DFFA53EF6B7;
minimizedProjectReferenceProxies = 1;
preferredProjectObjectVersion = 77;
productRefGroup = 4D2DDE7731949C0697A6167C /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
Expand Down Expand Up @@ -302,11 +311,14 @@
6C97C09AAFC84C7DF9FFFC4F /* ColorProfileService.swift in Sources */,
2A82397FAC6CCCA0966AAED5 /* ColorProfileView.swift in Sources */,
80CA5725D4B2AA1D35E1C527 /* DDCService.swift in Sources */,
4F57F12852AE8C6A0E98B51D /* DisplayConnectionService.swift in Sources */,
67C5BDA583DF55CAE1C63822 /* DisplayDetailView.swift in Sources */,
20BC0BD119FFB0F3BAC751FD /* DisplayInfo.swift in Sources */,
65CDC231F2FF743ACA68C862 /* DisplayManager.swift in Sources */,
17F9F1E3464F57FEFF92591C /* DisplayMode.swift in Sources */,
4AA949AC237EE0320969D676 /* DisplayModeListView.swift in Sources */,
D4A9A35703129085CA5A6EFA /* DisplayPowerService.swift in Sources */,
8EF0216DF111267D59FB4785 /* DisplayPowerView.swift in Sources */,
1E5F0FF7497392061D4444CA /* DisplayPreset.swift in Sources */,
3EB262F0A45A2A8F1E9FBCE9 /* FreeDisplayApp.swift in Sources */,
69413272AA7D6B777D028855 /* GammaService.swift in Sources */,
Expand Down
4 changes: 2 additions & 2 deletions FreeDisplay/App/FreeDisplayApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ struct FreeDisplayApp: App {
// Give WindowServer 2 seconds to stabilize after wake before
// touching display state.
try? await Task.sleep(nanoseconds: 2_000_000_000)
dm.refreshDisplays()
dm.refreshDisplays(invalidateTransportCaches: true)
try? await Task.sleep(nanoseconds: 500_000_000)
for display in dm.displays {
// Apply software brightness factor first so GammaService
Expand All @@ -45,7 +45,7 @@ struct FreeDisplayApp: App {
}
}
} label: {
Image(systemName: "display")
Label("FreeDisplay", systemImage: "display")
}
.menuBarExtraStyle(.window)
}
Expand Down
140 changes: 138 additions & 2 deletions FreeDisplay/Services/BrightnessService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Foundation
import IOKit
import IOKit.graphics
import CoreGraphics
import AppKit

@_silgen_name("CGDisplayIOServicePort")
private func CGDisplayIOServicePort(_ display: CGDirectDisplayID) -> io_service_t
Expand Down Expand Up @@ -141,6 +142,73 @@ final class BrightnessService: @unchecked Sendable {
/// Used to denormalize 0–100% into the display's native DDC range.
private var ddcMaxBrightness: [CGDirectDisplayID: UInt16] = [:]

private func displayUUIDString(for displayID: CGDirectDisplayID) -> String? {
guard let cfUUID = CGDisplayCreateUUIDFromDisplayID(displayID),
let uuid = CFUUIDCreateString(nil, cfUUID.takeRetainedValue()) else {
return nil
}
return uuid as String
}

private func physicalDDCIdentity(for displayID: CGDirectDisplayID) -> String {
"v\(CGDisplayVendorNumber(displayID))-m\(CGDisplayModelNumber(displayID))-s\(CGDisplaySerialNumber(displayID))"
}

private func ddcDisabledKeys(for displayID: CGDirectDisplayID) -> [String] {
let physical = physicalDDCIdentity(for: displayID)
var keys = [
"fd.ddc.disabled.physical.\(physical)",
"fd.power.powerVCPUnsafe.physical.\(physical)"
]
if let uuid = displayUUIDString(for: displayID) {
keys.append("fd.ddc.disabled.\(uuid)")
keys.append("fd.power.powerVCPUnsafe.\(uuid)")
keys.append("fd.power.wakeFailedAfterStandby.\(uuid)")
}
return keys
}

func isHardwareDDCDisabled(for displayID: CGDirectDisplayID) -> Bool {
let defaults = UserDefaults.standard
return ddcDisabledKeys(for: displayID).contains { defaults.bool(forKey: $0) }
}

func markHardwareDDCDisabled(for displayID: CGDirectDisplayID, reason: String) {
let defaults = UserDefaults.standard
let physical = physicalDDCIdentity(for: displayID)
defaults.set(true, forKey: "fd.ddc.disabled.physical.\(physical)")
if let uuid = displayUUIDString(for: displayID) {
defaults.set(true, forKey: "fd.ddc.disabled.\(uuid)")
}
ddcAvailableLock.withLock {
ddcAvailable[displayID] = false
ddcMaxBrightness.removeValue(forKey: displayID)
}
PowerDiagnostics.log("ddc-disabled mark displayID=\(displayID) physical=\(physical) reason=\(reason)")
}

@MainActor
@discardableResult
func markHardwareDDCDisabledIfKnownHighRisk(display: DisplayInfo) -> Bool {
guard isKnownHighRiskDDCDisplay(display) else { return false }
if isHardwareDDCDisabled(for: display.displayID) { return true }
markHardwareDDCDisabled(
for: display.displayID,
reason: "known-high-risk-display-\(display.name)"
)
return true
}

@MainActor
private func isKnownHighRiskDDCDisplay(_ display: DisplayInfo) -> Bool {
let normalizedName = display.name
.lowercased()
.replacingOccurrences(of: " ", with: "")
.replacingOccurrences(of: "-", with: "")
.replacingOccurrences(of: "_", with: "")
return normalizedName.contains("s27h85")
}

// MARK: - Public API

@MainActor
Expand All @@ -158,6 +226,15 @@ final class BrightnessService: @unchecked Sendable {
display.brightness = b
}
} else {
if isHardwareDDCDisabled(for: displayID) {
ddcAvailableLock.withLock {
ddcAvailable[displayID] = false
ddcMaxBrightness.removeValue(forKey: displayID)
}
PowerDiagnostics.log("brightness-refresh skip-ddc-disabled displayID=\(displayID)")
return
}

// First check if DDC is already known to be unavailable; if so skip the
// async DDC call and just read the current gamma-derived brightness.
let knownUnavailable: Bool = ddcAvailableLock.withLock {
Expand Down Expand Up @@ -210,6 +287,19 @@ final class BrightnessService: @unchecked Sendable {
self?.setInternalBrightness(value)
}
} else {
if isHardwareDDCDisabled(for: displayID) {
ddcAvailableLock.withLock {
ddcAvailable[displayID] = false
ddcMaxBrightness.removeValue(forKey: displayID)
}
display.brightness = clamped
PowerDiagnostics.log("brightness-set software-only displayID=\(displayID) reason=ddc-disabled")
queue.async { [weak self] in
self?.setSoftwareBrightness(clamped, for: displayID)
}
return
}

// Check current DDC availability status
let currentStatus: Bool? = ddcAvailableLock.withLock { ddcAvailable[displayID] }

Expand Down Expand Up @@ -289,7 +379,7 @@ final class BrightnessService: @unchecked Sendable {
self.queue.async { self.setInternalBrightness(floatVal) }
}
} else {
let currentStatus: Bool? = ddcAvailableLock.withLock { ddcAvailable[displayID] }
let currentStatus: Bool? = isDDCAvailable(for: displayID)

if currentStatus == false {
// Software (gamma) path: 8 steps over 200ms
Expand Down Expand Up @@ -390,7 +480,53 @@ final class BrightnessService: @unchecked Sendable {
/// Returns whether DDC is available for the given display.
/// nil means not yet determined (first use).
func isDDCAvailable(for displayID: CGDirectDisplayID) -> Bool? {
ddcAvailableLock.withLock { ddcAvailable[displayID] }
if isHardwareDDCDisabled(for: displayID) {
return false
}
return ddcAvailableLock.withLock { ddcAvailable[displayID] }
}

/// Determines whether DDC is available using the brightness VCP, which is safer
/// than probing power state directly. Existing false values are respected unless
/// `forceProbe` is true, which is used by the explicit refresh button.
func ensureDDCAvailability(for displayID: CGDirectDisplayID, forceProbe: Bool = false) async -> Bool {
if isHardwareDDCDisabled(for: displayID) {
ddcAvailableLock.withLock {
ddcAvailable[displayID] = false
ddcMaxBrightness.removeValue(forKey: displayID)
}
PowerDiagnostics.log("ddc-availability skip-disabled displayID=\(displayID) force=\(forceProbe)")
return false
}

if !forceProbe, let known = isDDCAvailable(for: displayID) {
PowerDiagnostics.log("ddc-availability cached displayID=\(displayID) available=\(known)")
return known
}

PowerDiagnostics.log("ddc-availability probe-start displayID=\(displayID) vcp=0x10 force=\(forceProbe)")
let result = await withCheckedContinuation { (continuation: CheckedContinuation<(current: UInt16, max: UInt16)?, Never>) in
DDCService.shared.readAsync(
displayID: displayID,
command: DDCService.brightnessVCP
) { result in
continuation.resume(returning: result)
}
}

return ddcAvailableLock.withLock {
if let result, result.max > 0 {
ddcAvailable[displayID] = true
ddcMaxBrightness[displayID] = result.max
PowerDiagnostics.log("ddc-availability probe-ok displayID=\(displayID) vcp=0x10 current=\(result.current) max=\(result.max)")
return true
}

ddcAvailable[displayID] = false
ddcMaxBrightness.removeValue(forKey: displayID)
PowerDiagnostics.log("ddc-availability probe-failed displayID=\(displayID) vcp=0x10")
return false
}
}

/// Clears DDC availability and max brightness cache for a disconnected display.
Expand Down
Loading