Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
Fix concurrency bugs, DDC reliability, crashes, and memory leaks
Thread safety:
- Fix swBrightnessSemaphore misuse causing races in software brightness
- Signal semaphore before async dispatch to prevent deadlock with polling job
- Wrap CGSetDisplayTransferByTable in DispatchQueue.main.sync for thread safety
- Wrap shade alpha updates in DispatchQueue.main.async for AppKit safety
- Move DDC probe work off main thread with configureID staleness guard
- Defer gamma interference alert to next run loop iteration

DDC reliability:
- Only persist DDC write state after confirming hardware write succeeded
- Clear stale arm64avService handles before rematching on wake/reconfig
- Validate DDC/CI reply header fields before accepting read values
- Cap DDC retry attempts at 30 to prevent multi-minute startup hangs
- Guard against division by zero in DDC value normalization

Crash fixes:
- Replace force-unwrap on currentDisplay in MenuHandler
- Handle gamma table acquisition failure safely
- Guard against zero defaultGammaTablePeak in getSwBrightness

Cleanup:
- Remove shadeGrave memory leak (closed windows retained forever)
- Remove dead sortDisplays() with stray Turkish comments
- Remove duplicate audioSpeakerVolume DDC default assignment
- Remove tautological self-comparisons in SliderHandler
- Replace deprecated SMLoginItemSetEnabled with SMAppService on macOS 13+
- Replace deprecated kIOMasterPortDefault with kIOMainPortDefault on macOS 12+
- Replace direct plist read with UserDefaults.persistentDomain API
  • Loading branch information
MyronKoch committed Sep 21, 2026
commit 85b5a652891b1919b6e1c6ecca57daf8704914fc
57 changes: 35 additions & 22 deletions MonitorControl/Model/Display.swift
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,6 @@ class Display: Equatable {
_ = self.setDirectBrightness(self.smoothBrightnessTransient, transient: true)
self.smoothBrightnessRunning = false
}
self.swBrightnessSemaphore.signal()
return true
}

Expand Down Expand Up @@ -191,7 +190,10 @@ class Display: Equatable {
guard !self.isDummy else {
return
}
CGGetDisplayTransferByTable(self.identifier, 256, &self.defaultGammaTableRed, &self.defaultGammaTableGreen, &self.defaultGammaTableBlue, &self.defaultGammaTableSampleCount)
guard CGGetDisplayTransferByTable(self.identifier, 256, &self.defaultGammaTableRed, &self.defaultGammaTableGreen, &self.defaultGammaTableBlue, &self.defaultGammaTableSampleCount) == CGError.success else {
self.defaultGammaTablePeak = 1.0
return
}
let redPeak = self.defaultGammaTableRed.max() ?? 0
let greenPeak = self.defaultGammaTableGreen.max() ?? 0
let bluePeak = self.defaultGammaTableBlue.max() ?? 0
Expand Down Expand Up @@ -222,23 +224,28 @@ class Display: Equatable {
currentValue = self.swBrightnessTransform(value: currentValue)
newValue = self.swBrightnessTransform(value: newValue)
if smooth {
self.swBrightnessSemaphore.signal()
DispatchQueue.global(qos: .userInteractive).async {
for transientValue in stride(from: currentValue, to: newValue, by: 0.005 * (currentValue > newValue ? -1 : 1)) {
guard app.reconfigureID == 0 else {
self.swBrightnessSemaphore.signal()
return
}
if self.isVirtual || self.readPrefAsBool(key: .avoidGamma) {
_ = DisplayManager.shared.setShadeAlpha(value: 1 - transientValue, displayID: DisplayManager.resolveEffectiveDisplayID(self.identifier))
DispatchQueue.main.async {
_ = DisplayManager.shared.setShadeAlpha(value: 1 - transientValue, displayID: DisplayManager.resolveEffectiveDisplayID(self.identifier))
}
} else {
let gammaTableRed = self.defaultGammaTableRed.map { $0 * transientValue }
let gammaTableGreen = self.defaultGammaTableGreen.map { $0 * transientValue }
let gammaTableBlue = self.defaultGammaTableBlue.map { $0 * transientValue }
CGSetDisplayTransferByTable(self.identifier, self.defaultGammaTableSampleCount, gammaTableRed, gammaTableGreen, gammaTableBlue)
DispatchQueue.main.sync {
CGSetDisplayTransferByTable(self.identifier, self.defaultGammaTableSampleCount, gammaTableRed, gammaTableGreen, gammaTableBlue)
}
}
Thread.sleep(forTimeInterval: 0.001) // Let's make things quick if not performed in the background
}
}
return true
Comment on lines 226 to +246
} else {
if self.isVirtual || self.readPrefAsBool(key: .avoidGamma) {
self.swBrightnessSemaphore.signal()
Expand Down Expand Up @@ -270,6 +277,10 @@ class Display: Equatable {
self.swBrightnessSemaphore.signal()
return self.swBrightnessTransform(value: rawBrightnessValue, reverse: true)
}
guard self.defaultGammaTablePeak != 0 else {
self.swBrightnessSemaphore.signal()
return 1.0
}
var gammaTableRed = [CGGammaValue](repeating: 0, count: 256)
var gammaTableGreen = [CGGammaValue](repeating: 0, count: 256)
var gammaTableBlue = [CGGammaValue](repeating: 0, count: 256)
Expand Down Expand Up @@ -297,24 +308,26 @@ class Display: Equatable {
os_log("Gamma table interference detected, number of events: %{public}@", type: .info, String(DisplayManager.shared.gammaInterferenceCounter))
if DisplayManager.shared.gammaInterferenceCounter >= 3 {
DisplayManager.shared.gammaInterferenceWarningShown = true
let alert = NSAlert()
alert.messageText = NSLocalizedString("Is f.lux or similar running?", comment: "Shown in the alert dialog")
alert.informativeText = NSLocalizedString("An other app seems to change the brightness or colors which causes issues.\n\nTo solve this, you need to quit the other app or disable gamma control for your displays in MonitorControl!", comment: "Shown in the alert dialog")
alert.addButton(withTitle: NSLocalizedString("I'll quit the other app", comment: "Shown in the alert dialog"))
alert.addButton(withTitle: NSLocalizedString("Disable gamma control for my displays", comment: "Shown in the alert dialog"))
alert.alertStyle = NSAlert.Style.critical
if alert.runModal() != .alertFirstButtonReturn {
for otherDisplay in DisplayManager.shared.getOtherDisplays() {
_ = otherDisplay.setSwBrightness(1)
_ = otherDisplay.setDirectBrightness(1)
otherDisplay.savePref(true, key: .avoidGamma)
_ = otherDisplay.setSwBrightness(1)
DisplayManager.shared.gammaInterferenceWarningShown = false
DisplayManager.shared.gammaInterferenceCounter = 0
displaysPrefsVc?.loadDisplayList()
DispatchQueue.main.async {
let alert = NSAlert()
alert.messageText = NSLocalizedString("Is f.lux or similar running?", comment: "Shown in the alert dialog")
alert.informativeText = NSLocalizedString("An other app seems to change the brightness or colors which causes issues.\n\nTo solve this, you need to quit the other app or disable gamma control for your displays in MonitorControl!", comment: "Shown in the alert dialog")
alert.addButton(withTitle: NSLocalizedString("I'll quit the other app", comment: "Shown in the alert dialog"))
alert.addButton(withTitle: NSLocalizedString("Disable gamma control for my displays", comment: "Shown in the alert dialog"))
alert.alertStyle = NSAlert.Style.critical
if alert.runModal() != .alertFirstButtonReturn {
for otherDisplay in DisplayManager.shared.getOtherDisplays() {
_ = otherDisplay.setSwBrightness(1)
_ = otherDisplay.setDirectBrightness(1)
otherDisplay.savePref(true, key: .avoidGamma)
_ = otherDisplay.setSwBrightness(1)
DisplayManager.shared.gammaInterferenceWarningShown = false
DisplayManager.shared.gammaInterferenceCounter = 0
displaysPrefsVc?.loadDisplayList()
}
} else {
os_log("We won't watch for gamma table interference anymore", type: .info)
}
} else {
os_log("We won't watch for gamma table interference anymore", type: .info)
}
}
}
Expand Down
32 changes: 23 additions & 9 deletions MonitorControl/Model/OtherDisplay.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,6 @@ class OtherDisplay: Display {
case .contrast: currentDDCValue = UInt16(Float(DDC_MAX_DETECT_LIMIT) * 0.750)
default: currentDDCValue = UInt16(Float(DDC_MAX_DETECT_LIMIT) * 1.000)
}
if command == .audioSpeakerVolume {
currentDDCValue = UInt16(Float(DDC_MAX_DETECT_LIMIT) * 0.125) // lower default audio value as high volume might rattle the user.
}
os_log("Setting up display %{public}@ for %{public}@", type: .info, String(self.identifier), String(reflecting: command))
if !self.isSw() {
if prefs.integer(forKey: PrefKey.startupAction.rawValue) == StartupAction.read.rawValue, self.pollingCount != 0, !app.safeMode {
Expand Down Expand Up @@ -399,21 +396,32 @@ class OtherDisplay: Display {
guard value != UInt16.max, value != lastValue else {
return
}
self.writeDDCQueue.async(flags: .barrier) {
self.writeDDCLastSavedValue[command] = value
self.savePref(true, key: PrefKey.isTouched, for: command)
}
var controlCodes = self.getRemapControlCodes(command: command)
if controlCodes.count == 0 {
controlCodes.append(command.rawValue)
}
var writeAttempted = false
var writeSucceeded = true
for controlCode in controlCodes {
if Arm64DDC.isArm64 {
if self.arm64ddc {
_ = Arm64DDC.write(service: self.arm64avService, command: controlCode, value: value)
writeAttempted = true
writeSucceeded = Arm64DDC.write(service: self.arm64avService, command: controlCode, value: value)
}
} else {
_ = self.ddc?.write(command: controlCode, value: value, errorRecoveryWaitTime: 2000) ?? false
if self.ddc != nil {
writeAttempted = true
writeSucceeded = self.ddc?.write(command: controlCode, value: value, errorRecoveryWaitTime: 2000) ?? false
}
}
if writeAttempted, !writeSucceeded {
break
}
}
if writeAttempted, writeSucceeded {
self.writeDDCQueue.async(flags: .barrier) {
self.writeDDCLastSavedValue[command] = value
self.savePref(true, key: PrefKey.isTouched, for: command)
}
}
Comment on lines +403 to 427
}
Expand Down Expand Up @@ -487,6 +495,9 @@ class OtherDisplay: Display {
let curveMultiplier = self.getCurveMultiplier(self.readPrefAsInt(key: .curveDDC, for: command))
let minDDCValue = Float(self.readPrefAsInt(key: .minDDCOverride, for: command))
let maxDDCValue = Float(self.readPrefAsInt(key: .maxDDC, for: command))
guard maxDDCValue != minDDCValue else {
return UInt16(minDDCValue)
}
let curvedValue = pow(max(min(value, 1), 0), curveMultiplier)
let deNormalizedValue = (maxDDCValue - minDDCValue) * curvedValue + minDDCValue
var intDDCValue = UInt16(min(max(deNormalizedValue.rounded(), minDDCValue), maxDDCValue))
Expand All @@ -500,6 +511,9 @@ class OtherDisplay: Display {
let curveMultiplier = self.getCurveMultiplier(self.readPrefAsInt(key: .curveDDC, for: command))
let minDDCValue = Float(self.readPrefAsInt(key: .minDDCOverride, for: command))
let maxDDCValue = Float(self.readPrefAsInt(key: .maxDDC, for: command))
guard maxDDCValue != minDDCValue else {
return 0
}
let normalizedValue = ((min(max(Float(from), minDDCValue), maxDDCValue) - minDDCValue) / (maxDDCValue - minDDCValue))
let deCurvedValue = pow(normalizedValue, 1.0 / curveMultiplier)
var value = deCurvedValue
Expand Down
69 changes: 48 additions & 21 deletions MonitorControl/Support/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
var accessibilityObserver: NSObjectProtocol!
var statusItemObserver: NSObjectProtocol!
var statusItemVisibilityChangedByUser = true
var configureID: Int = 0 // dispatched configure command ID
var reconfigureID: Int = 0 // dispatched reconfigure command ID
var sleepID: Int = 0 // sleep event ID
var safeMode = false
Expand Down Expand Up @@ -156,22 +157,45 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}
os_log("Request for configuration with reconfigreID %{public}@", type: .info, String(dispatchedReconfigureID))
self.reconfigureID = 0
self.configureID += 1
let dispatchedConfigureID = self.configureID
DisplayManager.shared.gammaInterferenceCounter = 0
DisplayManager.shared.configureDisplays()
DisplayManager.shared.addDisplayCounterSuffixes()
DisplayManager.shared.updateArm64AVServices()
if firstrun && prefs.integer(forKey: PrefKey.startupAction.rawValue) != StartupAction.write.rawValue {
DisplayManager.shared.resetSwBrightnessForAllDisplays(prefsOnly: true)
}
DisplayManager.shared.setupOtherDisplays(firstrun: firstrun)
self.updateMenusAndKeys()
if !firstrun || prefs.integer(forKey: PrefKey.startupAction.rawValue) == StartupAction.write.rawValue {
if !prefs.bool(forKey: PrefKey.disableCombinedBrightness.rawValue) {
DisplayManager.shared.restoreSwBrightnessForAllDisplays(async: !prefs.bool(forKey: PrefKey.disableSmoothBrightness.rawValue))
DispatchQueue.global(qos: .userInitiated).async {
guard self.sleepID == 0, self.reconfigureID == 0, dispatchedConfigureID == self.configureID else {
return
}
DisplayManager.shared.setupOtherDisplays(firstrun: firstrun)
guard self.sleepID == 0, self.reconfigureID == 0, dispatchedConfigureID == self.configureID else {
return
}
DispatchQueue.main.async {
guard self.sleepID == 0, self.reconfigureID == 0, dispatchedConfigureID == self.configureID else {
return
}
self.updateMenusAndKeys()
}
guard self.sleepID == 0, self.reconfigureID == 0, dispatchedConfigureID == self.configureID else {
return
}
if !firstrun || prefs.integer(forKey: PrefKey.startupAction.rawValue) == StartupAction.write.rawValue {
if !prefs.bool(forKey: PrefKey.disableCombinedBrightness.rawValue) {
DisplayManager.shared.restoreSwBrightnessForAllDisplays(async: !prefs.bool(forKey: PrefKey.disableSmoothBrightness.rawValue))
}
}
DispatchQueue.main.async {
guard self.sleepID == 0, self.reconfigureID == 0, dispatchedConfigureID == self.configureID else {
return
}
displaysPrefsVc?.loadDisplayList()
self.job(start: true)
}
}
displaysPrefsVc?.loadDisplayList()
self.job(start: true)
}

func updateMenusAndKeys() {
Expand Down Expand Up @@ -318,22 +342,25 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}

func setStartAtLogin(enabled: Bool) {
let identifier = "\(Bundle.main.bundleIdentifier!)Helper" as CFString
SMLoginItemSetEnabled(identifier, enabled)
let identifier = "\(Bundle.main.bundleIdentifier!)Helper"
if #available(macOS 13.0, *) {
let service = SMAppService.loginItem(identifier: identifier)
do {
if enabled {
try service.register()
} else {
try service.unregister()
}
} catch {
os_log("Unable to update login item registration: %{public}@", type: .error, error.localizedDescription)
}
} else {
SMLoginItemSetEnabled(identifier as CFString, enabled)
}
Comment on lines +346 to +359
Comment on lines +345 to +359
}

func getSystemSettings() -> [String: AnyObject]? {
var propertyListFormat = PropertyListSerialization.PropertyListFormat.xml
let plistPath = NSString(string: "~/Library/Preferences/.GlobalPreferences.plist").expandingTildeInPath
guard let plistXML = FileManager.default.contents(atPath: plistPath) else {
return nil
}
do {
return try PropertyListSerialization.propertyList(from: plistXML, options: .mutableContainersAndLeaves, format: &propertyListFormat) as? [String: AnyObject]
} catch {
os_log("Error reading system prefs plist: %{public}@", type: .info, error.localizedDescription)
return nil
}
func getSystemSettings() -> [String: Any]? {
UserDefaults.standard.persistentDomain(forName: ".GlobalPreferences")
}
Comment on lines +362 to 364

func macOS10() -> Bool {
Expand Down
14 changes: 12 additions & 2 deletions MonitorControl/Support/Arm64DDC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ class Arm64DDC: NSObject {
var send: [UInt8] = [command]
var reply = [UInt8](repeating: 0, count: 11)
if Self.performDDCCommunication(service: service, send: &send, reply: &reply, writeSleepTime: writeSleepTime, numOfWriteCycles: numOfWriteCycles, readSleepTime: readSleepTime, numOfRetryAttemps: numOfRetryAttemps, retrySleepTime: retrySleepTime) {
guard reply.count > 9, reply[0] == ARM64_DDC_7BIT_ADDRESS << 1, reply[2] == 0x02, reply[4] == command else {
return nil
}
let max = UInt16(reply[6]) * 256 + UInt16(reply[7])
let current = UInt16(reply[8]) * 256 + UInt16(reply[9])
values = (current, max)
Expand All @@ -97,7 +100,8 @@ class Arm64DDC: NSObject {
}
var packet: [UInt8] = [UInt8(0x80 | (send.count + 1)), UInt8(send.count)] + send + [0] // Note: the last byte is the place of the checksum, see next line!
packet[packet.count - 1] = self.checksum(chk: send.count == 1 ? ARM64_DDC_7BIT_ADDRESS << 1 : ARM64_DDC_7BIT_ADDRESS << 1 ^ dataAddress, data: &packet, start: 0, end: packet.count - 2)
for _ in 1 ... (numOfRetryAttemps ?? 4) + 1 {
let attempts = max(1, Int(min(numOfRetryAttemps ?? 4, UInt8(30))))
for _ in 0 ..< attempts {
for _ in 1 ... max((numOfWriteCycles ?? 2) + 0, 1) {
usleep(writeSleepTime ?? 10000)
success = IOAVServiceWriteI2C(service, UInt32(ARM64_DDC_7BIT_ADDRESS), UInt32(dataAddress), &packet, UInt32(packet.count)) == 0
Expand Down Expand Up @@ -232,7 +236,13 @@ class Arm64DDC: NSObject {
static func getIoregServicesForMatching() -> [IOregService] {
var serviceLocation = 0
var ioregServicesForMatching: [IOregService] = []
let ioregRoot: io_registry_entry_t = IORegistryGetRootEntry(kIOMasterPortDefault)
let masterPort: mach_port_t
if #available(macOS 12.0, *) {
masterPort = kIOMainPortDefault
} else {
masterPort = kIOMasterPortDefault
}
let ioregRoot: io_registry_entry_t = IORegistryGetRootEntry(masterPort)
Comment on lines +239 to +245
defer {
IOObjectRelease(ioregRoot)
}
Expand Down
24 changes: 5 additions & 19 deletions MonitorControl/Support/DisplayManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ class DisplayManager {
}

var shades: [CGDirectDisplayID: NSWindow] = [:]
var shadeGrave: [NSWindow] = []

func isDisqualifiedFromShade(_ displayID: CGDirectDisplayID) -> Bool {
if CGDisplayIsInHWMirrorSet(displayID) != 0 || CGDisplayIsInMirrorSet(displayID) != 0 {
Expand Down Expand Up @@ -117,7 +116,6 @@ class DisplayManager {
func destroyShade(displayID: CGDirectDisplayID) -> Bool {
if let shade = shades[displayID] {
os_log("Destroying shade for display %{public}@", type: .info, String(displayID))
self.shadeGrave.append(shade)
self.shades.removeValue(forKey: displayID)
shade.close()
return true
Expand Down Expand Up @@ -241,21 +239,6 @@ class DisplayManager {
self.displays.compactMap { $0 as? OtherDisplay }
}

func sortDisplays() {
// Opsiyonel: sıralamadan önce log al
let before = self.displays.map(\.name)
os_log("Displays before sorting: %{public}@", before)

// In‑place sıralama
self.displays.sort { lhs, rhs in
lhs.name.localizedStandardCompare(rhs.name) == .orderedAscending
}

// Opsiyonel: sıralamadan sonra log al
let after = self.displays.map(\.name)
os_log("Displays after sorting: %{public}@", after)
}

func sortDisplaysByFriendlyName() -> [Display] {
self.displays.sorted { lhs, rhs in
let lhsTitle = lhs.readPrefAsString(key: .friendlyName).isEmpty
Expand Down Expand Up @@ -337,11 +320,14 @@ class DisplayManager {
if Arm64DDC.isArm64 {
os_log("arm64 AVService update requested", type: .info)
var displayIDs: [CGDirectDisplayID] = []
for otherDisplay in self.getOtherDisplays() {
let otherDisplays = self.getOtherDisplays()
for otherDisplay in otherDisplays {
otherDisplay.arm64avService = nil
otherDisplay.arm64ddc = false
displayIDs.append(otherDisplay.identifier)
}
for serviceMatch in Arm64DDC.getServiceMatches(displayIDs: displayIDs) {
for otherDisplay in self.getOtherDisplays() where otherDisplay.identifier == serviceMatch.displayID && serviceMatch.service != nil {
for otherDisplay in otherDisplays where otherDisplay.identifier == serviceMatch.displayID && serviceMatch.service != nil {
otherDisplay.arm64avService = serviceMatch.service
os_log("Display service match successful for display %{public}@", type: .info, String(serviceMatch.displayID))
if serviceMatch.discouraged {
Expand Down
3 changes: 2 additions & 1 deletion MonitorControl/Support/MenuHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,12 @@ class MenuHandler: NSMenu, NSMenuDelegate {
displays = DisplayManager.shared.sortDisplaysByFriendlyName()
let relevant = prefs.integer(forKey: PrefKey.multiSliders.rawValue) == MultiSliders.relevant.rawValue
let combine = prefs.integer(forKey: PrefKey.multiSliders.rawValue) == MultiSliders.combine.rawValue
let currentDisplayID = currentDisplay.map { DisplayManager.resolveEffectiveDisplayID($0.identifier) }
let numOfDisplays = displays.filter { !$0.isDummy }.count
if numOfDisplays != 0 {
let asSubMenu: Bool = (displays.count > 3 && !relevant && !combine && app.macOS10()) ? true : false
var iterator = 0
for display in displays where (!relevant || DisplayManager.resolveEffectiveDisplayID(display.identifier) == DisplayManager.resolveEffectiveDisplayID(currentDisplay!.identifier)) && !display.isDummy {
for display in displays where (!relevant || currentDisplayID.map { DisplayManager.resolveEffectiveDisplayID(display.identifier) == $0 } ?? false) && !display.isDummy {
iterator += 1
if !relevant, !combine, iterator != 1, app.macOS10() {
self.insertItem(NSMenuItem.separator(), at: 0)
Expand Down
Loading