mirror of
https://github.com/MonitorControl/MonitorControl.git
synced 2026-05-16 06:05:52 -06:00
Add detailed brightness mechanisms analysis report
Supporting documentation from Display.swift analysis covering: - Software brightness implementation via gamma tables - Shade-based brightness for virtual displays - Smooth brightness animation algorithm - Gamma interference detection system - Thread safety mechanisms This complements the main implementation guide with detailed technical analysis of the base Display class mechanisms.
This commit is contained in:
parent
63e099686d
commit
6e7bc30d3c
1 changed files with 704 additions and 0 deletions
704
BRIGHTNESS_MECHANISMS_REPORT.md
Normal file
704
BRIGHTNESS_MECHANISMS_REPORT.md
Normal file
|
|
@ -0,0 +1,704 @@
|
|||
# Display.swift Brightness Control Mechanisms - Detailed Analysis
|
||||
|
||||
## Overview
|
||||
The `/home/user/brightness_checker/MonitorControl/Model/Display.swift` file implements a sophisticated multi-layered brightness control system that supports both hardware DDC/CI brightness and software-based brightness through gamma table manipulation or overlay shades.
|
||||
|
||||
---
|
||||
|
||||
## 1. Brightness Control Method Hierarchy
|
||||
|
||||
### 1.1 Primary Entry Point: `setBrightness()`
|
||||
**Location:** Lines 111-117
|
||||
|
||||
```swift
|
||||
func setBrightness(_ to: Float = -1, slow: Bool = false) -> Bool {
|
||||
if !prefs.bool(forKey: PrefKey.disableSmoothBrightness.rawValue) {
|
||||
return self.setSmoothBrightness(to, slow: slow)
|
||||
} else {
|
||||
return self.setDirectBrightness(to)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Purpose:** Main brightness setter that routes to either smooth or direct brightness based on user preferences.
|
||||
|
||||
**Flow:**
|
||||
- Checks global preference `disableSmoothBrightness`
|
||||
- If smooth transitions enabled → calls `setSmoothBrightness()`
|
||||
- If smooth transitions disabled → calls `setDirectBrightness()`
|
||||
|
||||
---
|
||||
|
||||
### 1.2 Smooth Brightness: `setSmoothBrightness()`
|
||||
**Location:** Lines 119-170
|
||||
|
||||
**Purpose:** Implements animated brightness transitions with gradual stepping to avoid jarring changes.
|
||||
|
||||
#### Algorithm:
|
||||
|
||||
1. **Safety Checks:**
|
||||
```swift
|
||||
guard app.sleepID == 0, app.reconfigureID == 0 else {
|
||||
// Stop if system is sleeping or reconfiguring displays
|
||||
return false
|
||||
}
|
||||
```
|
||||
|
||||
2. **Step Division Logic:**
|
||||
```swift
|
||||
var stepDivider: Float = 6 // Normal speed
|
||||
if self.smoothBrightnessSlow {
|
||||
stepDivider = 16 // Slow mode (2.67x slower)
|
||||
}
|
||||
```
|
||||
- Normal mode: divides remaining distance by 6
|
||||
- Slow mode: divides remaining distance by 16 (for more gradual transitions)
|
||||
|
||||
3. **Goal Setting (when `to != -1`):**
|
||||
```swift
|
||||
let value = max(min(to, 1), 0)
|
||||
self.savePref(value, for: .brightness)
|
||||
self.brightnessSyncSourceValue = value
|
||||
```
|
||||
- Clamps value to [0, 1]
|
||||
- Saves as target brightness preference
|
||||
- Prevents duplicate smooth transition if already running
|
||||
|
||||
4. **Transition Stepping:**
|
||||
```swift
|
||||
if brightness > self.smoothBrightnessTransient {
|
||||
self.smoothBrightnessTransient += max((brightness - self.smoothBrightnessTransient) / stepDivider, 1/100)
|
||||
} else {
|
||||
self.smoothBrightnessTransient += min((brightness - self.smoothBrightnessTransient) / stepDivider, 1/100)
|
||||
}
|
||||
```
|
||||
- Calculates next step as fraction of remaining distance
|
||||
- Minimum step size: 1/100 (0.01) to ensure progress
|
||||
- Maximum step size: (remaining_distance / stepDivider)
|
||||
|
||||
5. **Termination:**
|
||||
```swift
|
||||
if abs(brightness - self.smoothBrightnessTransient) < 0.01 {
|
||||
self.smoothBrightnessTransient = brightness
|
||||
dontPushAgain = true
|
||||
self.smoothBrightnessRunning = false
|
||||
}
|
||||
```
|
||||
- Stops when within 0.01 of target
|
||||
- Snaps to exact target value
|
||||
|
||||
6. **Recursive Scheduling:**
|
||||
```swift
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) {
|
||||
_ = self.setSmoothBrightness()
|
||||
}
|
||||
```
|
||||
- Schedules next step in 20ms (50 FPS)
|
||||
- Continues until target reached
|
||||
|
||||
7. **Applies each step via:**
|
||||
```swift
|
||||
_ = self.setDirectBrightness(self.smoothBrightnessTransient, transient: true)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 1.3 Direct Brightness: `setDirectBrightness()`
|
||||
**Location:** Lines 172-183
|
||||
|
||||
**Purpose:** Immediately sets brightness without animation.
|
||||
|
||||
```swift
|
||||
func setDirectBrightness(_ to: Float, transient: Bool = false) -> Bool {
|
||||
let value = max(min(to, 1), 0)
|
||||
if self.setSwBrightness(value) {
|
||||
if !transient {
|
||||
self.savePref(value, for: .brightness)
|
||||
self.brightnessSyncSourceValue = value
|
||||
self.smoothBrightnessTransient = value
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
```
|
||||
|
||||
**Flow:**
|
||||
1. Clamps value to [0, 1]
|
||||
2. Calls `setSwBrightness()` to apply
|
||||
3. If not transient (i.e., final value):
|
||||
- Saves to preferences
|
||||
- Updates sync source
|
||||
- Updates smooth brightness tracking variable
|
||||
|
||||
**Transient Mode:** Used by smooth brightness for intermediate steps that shouldn't be saved.
|
||||
|
||||
---
|
||||
|
||||
## 2. Software Brightness Implementation: `setSwBrightness()`
|
||||
|
||||
**Location:** Lines 213-260
|
||||
|
||||
**Purpose:** The core implementation that actually manipulates display brightness using either gamma tables or shades.
|
||||
|
||||
### 2.1 Thread Safety
|
||||
```swift
|
||||
self.swBrightnessSemaphore.wait()
|
||||
// ... critical section ...
|
||||
self.swBrightnessSemaphore.signal()
|
||||
```
|
||||
- Uses semaphore to prevent concurrent gamma table modifications
|
||||
- Prevents race conditions and flickering
|
||||
|
||||
### 2.2 Brightness Transformation
|
||||
```swift
|
||||
currentValue = self.swBrightnessTransform(value: currentValue)
|
||||
newValue = self.swBrightnessTransform(value: newValue)
|
||||
```
|
||||
|
||||
Calls `swBrightnessTransform()` (lines 204-211) which applies a non-linear mapping:
|
||||
|
||||
```swift
|
||||
func swBrightnessTransform(value: Float, reverse: Bool = false) -> Float {
|
||||
let lowTreshold: Float = prefs.bool(forKey: PrefKey.allowZeroSwBrightness.rawValue) ? 0.0 : 0.15
|
||||
if !reverse {
|
||||
return value * (1 - lowTreshold) + lowTreshold // Maps [0,1] → [0.15,1]
|
||||
} else {
|
||||
return (value - lowTreshold) / (1 - lowTreshold) // Maps [0.15,1] → [0,1]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Rationale:**
|
||||
- Default minimum brightness: 15% (prevents totally black screen)
|
||||
- Can be disabled with `allowZeroSwBrightness` preference
|
||||
- Reverse transformation used when reading current brightness
|
||||
|
||||
### 2.3 Dual Implementation Paths
|
||||
|
||||
#### Path A: Virtual Displays or Gamma-Avoiding Mode
|
||||
```swift
|
||||
if self.isVirtual || self.readPrefAsBool(key: .avoidGamma) {
|
||||
return DisplayManager.shared.setShadeAlpha(value: 1 - newValue, displayID: ...)
|
||||
}
|
||||
```
|
||||
|
||||
**When Used:**
|
||||
- Virtual displays (e.g., sidecar, airplay)
|
||||
- User enabled "Avoid Gamma" preference (lines 78-84)
|
||||
- Gamma interference detected from other apps (f.lux, Night Shift)
|
||||
|
||||
**Mechanism:**
|
||||
- Creates a semi-transparent overlay window (shade)
|
||||
- Sets alpha to `1 - brightness` (e.g., 70% brightness = 30% opacity shade)
|
||||
- Shade dims the display visually without touching hardware
|
||||
|
||||
#### Path B: Gamma Table Manipulation (Default)
|
||||
```swift
|
||||
let gammaTableRed = self.defaultGammaTableRed.map { $0 * newValue }
|
||||
let gammaTableGreen = self.defaultGammaTableGreen.map { $0 * newValue }
|
||||
let gammaTableBlue = self.defaultGammaTableBlue.map { $0 * newValue }
|
||||
CGSetDisplayTransferByTable(self.identifier, self.defaultGammaTableSampleCount,
|
||||
gammaTableRed, gammaTableGreen, gammaTableBlue)
|
||||
```
|
||||
|
||||
**Mechanism:**
|
||||
1. Takes default gamma tables (captured at init)
|
||||
2. Multiplies all values by brightness scalar (0.0 to 1.0)
|
||||
3. Applies modified tables via CoreGraphics API
|
||||
|
||||
**Example:**
|
||||
- Default gamma value: 0.8
|
||||
- Brightness: 50% (0.5)
|
||||
- New gamma value: 0.8 * 0.5 = 0.4
|
||||
|
||||
This effectively reduces the output intensity of all color channels proportionally.
|
||||
|
||||
### 2.4 Smooth Gamma Transitions
|
||||
```swift
|
||||
if smooth {
|
||||
DispatchQueue.global(qos: .userInteractive).async {
|
||||
for transientValue in stride(from: currentValue, to: newValue, by: 0.005 * (currentValue > newValue ? -1 : 1)) {
|
||||
// Apply gamma table or shade at transientValue
|
||||
Thread.sleep(forTimeInterval: 0.001) // 1ms delay between steps
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Optional smooth mode:**
|
||||
- Steps in increments of 0.005 (0.5%)
|
||||
- 1ms delay per step
|
||||
- Runs in background thread (user interactive QoS)
|
||||
- Can create very gradual fades when needed
|
||||
|
||||
---
|
||||
|
||||
## 3. Gamma Table Management
|
||||
|
||||
### 3.1 Default Gamma Table Capture: `swUpdateDefaultGammaTable()`
|
||||
**Location:** Lines 193-202
|
||||
|
||||
```swift
|
||||
func swUpdateDefaultGammaTable() {
|
||||
guard !self.isDummy else { return }
|
||||
|
||||
CGGetDisplayTransferByTable(self.identifier, 256,
|
||||
&self.defaultGammaTableRed,
|
||||
&self.defaultGammaTableGreen,
|
||||
&self.defaultGammaTableBlue,
|
||||
&self.defaultGammaTableSampleCount)
|
||||
|
||||
let redPeak = self.defaultGammaTableRed.max() ?? 0
|
||||
let greenPeak = self.defaultGammaTableGreen.max() ?? 0
|
||||
let bluePeak = self.defaultGammaTableBlue.max() ?? 0
|
||||
self.defaultGammaTablePeak = max(redPeak, greenPeak, bluePeak)
|
||||
}
|
||||
```
|
||||
|
||||
**Called:** During display initialization (line 76)
|
||||
|
||||
**Purpose:**
|
||||
1. Captures the "factory default" gamma tables
|
||||
2. Stores them in display instance variables
|
||||
3. Calculates peak values for later comparison
|
||||
|
||||
**Gamma Table Structure:**
|
||||
- 256 entries per color channel (R, G, B)
|
||||
- Type: `CGGammaValue` (floating point)
|
||||
- Represents input-to-output mapping curve
|
||||
- Peak value typically ~1.0 for linear or calibrated displays
|
||||
|
||||
### 3.2 Reading Current Brightness: `getSwBrightness()`
|
||||
**Location:** Lines 262-291
|
||||
|
||||
```swift
|
||||
func getSwBrightness() -> Float {
|
||||
// For shades: read alpha
|
||||
if self.isVirtual || self.readPrefAsBool(key: .avoidGamma) {
|
||||
let rawBrightnessValue = 1 - (DisplayManager.shared.getShadeAlpha(displayID: ...) ?? 1)
|
||||
return self.swBrightnessTransform(value: rawBrightnessValue, reverse: true)
|
||||
}
|
||||
|
||||
// For gamma: read current gamma tables
|
||||
var gammaTableRed = [CGGammaValue](repeating: 0, count: 256)
|
||||
var gammaTableGreen = [CGGammaValue](repeating: 0, count: 256)
|
||||
var gammaTableBlue = [CGGammaValue](repeating: 0, count: 256)
|
||||
var gammaTableSampleCount: UInt32 = 0
|
||||
|
||||
if CGGetDisplayTransferByTable(self.identifier, 256, &gammaTableRed,
|
||||
&gammaTableGreen, &gammaTableBlue,
|
||||
&gammaTableSampleCount) == CGError.success {
|
||||
let redPeak = gammaTableRed.max() ?? 0
|
||||
let greenPeak = gammaTableGreen.max() ?? 0
|
||||
let bluePeak = gammaTableBlue.max() ?? 0
|
||||
let gammaTablePeak = max(redPeak, greenPeak, bluePeak)
|
||||
|
||||
let peakRatio = gammaTablePeak / self.defaultGammaTablePeak
|
||||
brightnessValue = round(self.swBrightnessTransform(value: peakRatio, reverse: true) * 256) / 256
|
||||
}
|
||||
|
||||
return brightnessValue
|
||||
}
|
||||
```
|
||||
|
||||
**Brightness Calculation Algorithm:**
|
||||
1. Get current gamma table from system
|
||||
2. Find peak value across all channels
|
||||
3. Calculate ratio: `current_peak / default_peak`
|
||||
4. Apply reverse transformation to convert to user-facing brightness
|
||||
5. Round to 1/256 precision
|
||||
|
||||
**Example:**
|
||||
- Default peak: 1.0
|
||||
- Current peak: 0.5
|
||||
- Peak ratio: 0.5
|
||||
- After reverse transform (assuming 15% min): `(0.5 - 0.15) / 0.85 = 0.41`
|
||||
- Rounded: 0.41 (41% brightness)
|
||||
|
||||
---
|
||||
|
||||
## 4. Gamma Interference Detection
|
||||
|
||||
### 4.1 Detection Function: `checkGammaInterference()`
|
||||
**Location:** Lines 293-323
|
||||
|
||||
**Purpose:** Detects when external apps (f.lux, Night Shift, etc.) modify gamma tables.
|
||||
|
||||
#### Conditions Required for Detection:
|
||||
```swift
|
||||
guard !self.isDummy,
|
||||
!DisplayManager.shared.gammaInterferenceWarningShown,
|
||||
!(prefs.bool(forKey: PrefKey.disableCombinedBrightness.rawValue)),
|
||||
!self.readPrefAsBool(key: .avoidGamma),
|
||||
!self.isVirtual,
|
||||
!self.smoothBrightnessRunning,
|
||||
self.prefExists(key: .SwBrightness),
|
||||
abs(currentSwBrightness - self.readPrefAsFloat(key: .SwBrightness)) > 0.02
|
||||
else { return }
|
||||
```
|
||||
|
||||
**Checks:**
|
||||
1. Not a dummy display
|
||||
2. Warning hasn't been shown yet
|
||||
3. Combined brightness is enabled
|
||||
4. Gamma mode is active (not avoiding gamma)
|
||||
5. Not a virtual display
|
||||
6. Not currently animating brightness
|
||||
7. Previous brightness value exists in prefs
|
||||
8. **Mismatch detected:** Current gamma brightness differs from saved by >2%
|
||||
|
||||
#### Detection Response:
|
||||
```swift
|
||||
DisplayManager.shared.gammaInterferenceCounter += 1
|
||||
_ = self.setSwBrightness(1) // Reset to 100% to clear interference
|
||||
```
|
||||
|
||||
**After 3 Detections:**
|
||||
Shows alert dialog with two options:
|
||||
|
||||
1. **"I'll quit the other app"**
|
||||
- User handles manually
|
||||
- Stops watching for interference
|
||||
|
||||
2. **"Disable gamma control for my displays"**
|
||||
```swift
|
||||
for otherDisplay in DisplayManager.shared.getOtherDisplays() {
|
||||
_ = otherDisplay.setSwBrightness(1)
|
||||
_ = otherDisplay.setDirectBrightness(1)
|
||||
otherDisplay.savePref(true, key: .avoidGamma) // Enable shade mode
|
||||
_ = otherDisplay.setSwBrightness(1)
|
||||
DisplayManager.shared.gammaInterferenceWarningShown = false
|
||||
DisplayManager.shared.gammaInterferenceCounter = 0
|
||||
}
|
||||
```
|
||||
- Resets all displays to 100% brightness
|
||||
- Enables `.avoidGamma` mode (switches to shades)
|
||||
- Destroys gamma tables interference
|
||||
- Resets counter for future detections
|
||||
|
||||
---
|
||||
|
||||
## 5. Shade vs Gamma Mode Decision
|
||||
|
||||
### 5.1 Mode Selection (in `init()`)
|
||||
**Location:** Lines 78-84
|
||||
|
||||
```swift
|
||||
if self.isVirtual || self.readPrefAsBool(key: PrefKey.avoidGamma), !self.isDummy {
|
||||
os_log("Creating or updating shade for display %{public}@", type: .info, String(self.identifier))
|
||||
_ = DisplayManager.shared.updateShade(displayID: self.identifier)
|
||||
} else {
|
||||
os_log("Destroying shade (if exists) for display %{public}@", type: .info, String(self.identifier))
|
||||
_ = DisplayManager.shared.destroyShade(displayID: self.identifier)
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 When Shades Are Used:
|
||||
|
||||
1. **Virtual Displays (Always)**
|
||||
- Sidecar iPads
|
||||
- AirPlay displays
|
||||
- Virtual display adapters
|
||||
- Reason: May not support gamma table manipulation
|
||||
|
||||
2. **User Preference (Manual)**
|
||||
- `avoidGamma` preference enabled
|
||||
- User manually chose to avoid gamma
|
||||
- Perhaps due to color-critical work
|
||||
|
||||
3. **Gamma Interference (Automatic)**
|
||||
- Detected 3+ conflicts with other apps
|
||||
- User chose to disable gamma in alert
|
||||
- System switches all displays to shade mode
|
||||
|
||||
### 5.3 Shade Mechanism Details:
|
||||
|
||||
**Implementation:** (handled by DisplayManager, not in this file)
|
||||
- Creates overlay window on display
|
||||
- Sets window alpha: `alpha = 1 - brightness`
|
||||
- Example: 60% brightness → 40% opacity black shade
|
||||
- Positioned above all other windows
|
||||
- Ignores mouse events (pass-through)
|
||||
|
||||
**Advantages:**
|
||||
- No conflicts with color management apps
|
||||
- Works on all display types
|
||||
- Preserves original gamma curves
|
||||
|
||||
**Disadvantages:**
|
||||
- Visible overlay (very slight performance impact)
|
||||
- Cannot dim below ~10-15% effectively (too dark)
|
||||
- May not work correctly with HDR content
|
||||
|
||||
### 5.4 Gamma Mode Details:
|
||||
|
||||
**Advantages:**
|
||||
- No overlay artifacts
|
||||
- Native brightness control
|
||||
- Better low-brightness performance
|
||||
|
||||
**Disadvantages:**
|
||||
- Conflicts with f.lux, Night Shift, color calibrators
|
||||
- May interfere with professional color work
|
||||
- Requires gamma table restoration on quit
|
||||
|
||||
---
|
||||
|
||||
## 6. Preference Storage
|
||||
|
||||
### 6.1 Key Types
|
||||
```swift
|
||||
private func getKey(key: PrefKey? = nil, for command: Command? = nil) -> String {
|
||||
(key ?? PrefKey.value).rawValue +
|
||||
(command != nil ? String((command ?? Command.none).rawValue) : "") +
|
||||
self.prefsId
|
||||
}
|
||||
```
|
||||
|
||||
**prefsId Format (line 74):**
|
||||
```swift
|
||||
self.prefsId = "(\(name.filter { !$0.isWhitespace })\(vendorNumber ?? 0)\(modelNumber ?? 0)@\(self.isVirtual ? (self.serialNumber ?? 9999) : identifier))"
|
||||
```
|
||||
|
||||
**Example:** `"(DellU2720Q412345678@987654321)"`
|
||||
|
||||
### 6.2 Brightness-Related Preferences
|
||||
|
||||
| PrefKey | Command | Purpose | Type |
|
||||
|---------|---------|---------|------|
|
||||
| `.value` | `.brightness` | Target brightness | Float |
|
||||
| `.SwBrightness` | - | Software brightness value | Float |
|
||||
| `.avoidGamma` | - | Force shade mode | Bool |
|
||||
| `.unavailableDDC` | `.brightness` | DDC unavailable flag | Bool |
|
||||
|
||||
### 6.3 Helper Functions
|
||||
|
||||
```swift
|
||||
func savePref<T>(_ value: T, key: PrefKey? = nil, for command: Command? = nil)
|
||||
func readPrefAsFloat(key: PrefKey? = nil, for command: Command? = nil) -> Float
|
||||
func readPrefAsBool(key: PrefKey? = nil, for command: Command? = nil) -> Bool
|
||||
func prefExists(key: PrefKey? = nil, for command: Command? = nil) -> Bool
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Complete Brightness Flow Diagrams
|
||||
|
||||
### 7.1 User Adjusts Brightness
|
||||
|
||||
```
|
||||
User Action (hotkey/slider)
|
||||
↓
|
||||
stepBrightness() or setValue()
|
||||
↓
|
||||
setBrightness(value, slow)
|
||||
↓
|
||||
├─[Smooth enabled]─→ setSmoothBrightness(value, slow)
|
||||
│ ↓
|
||||
│ Save target to prefs
|
||||
│ ↓
|
||||
│ Start 50Hz loop
|
||||
│ ↓
|
||||
│ Calculate next step (distance/divisor)
|
||||
│ ↓
|
||||
│ setDirectBrightness(step, transient: true)
|
||||
│ ↓
|
||||
│ setSwBrightness(step, noPrefSave: false)
|
||||
│ ↓
|
||||
│ [Check: Virtual or avoidGamma?]
|
||||
│ ├─[YES]─→ setShadeAlpha(1 - brightness)
|
||||
│ └─[NO]──→ CGSetDisplayTransferByTable(gamma * brightness)
|
||||
│ ↓
|
||||
│ Sleep 20ms
|
||||
│ ↓
|
||||
│ [Distance < 0.01?]
|
||||
│ ├─[NO]──→ Loop again
|
||||
│ └─[YES]─→ Complete, save final value
|
||||
│
|
||||
└─[Smooth disabled]─→ setDirectBrightness(value)
|
||||
↓
|
||||
setSwBrightness(value)
|
||||
↓
|
||||
[Check: Virtual or avoidGamma?]
|
||||
├─[YES]─→ setShadeAlpha(1 - brightness)
|
||||
└─[NO]──→ CGSetDisplayTransferByTable(gamma * brightness)
|
||||
↓
|
||||
Save to prefs
|
||||
```
|
||||
|
||||
### 7.2 Reading Brightness
|
||||
|
||||
```
|
||||
getBrightness()
|
||||
↓
|
||||
[Pref exists?]
|
||||
├─[YES]─→ readPrefAsFloat(for: .brightness)
|
||||
└─[NO]──→ getSwBrightness()
|
||||
↓
|
||||
[Virtual or avoidGamma?]
|
||||
├─[YES]─→ getShadeAlpha() → return 1 - alpha
|
||||
└─[NO]──→ CGGetDisplayTransferByTable()
|
||||
↓
|
||||
Find max(peak_red, peak_green, peak_blue)
|
||||
↓
|
||||
ratio = current_peak / default_peak
|
||||
↓
|
||||
brightness = swBrightnessTransform(ratio, reverse: true)
|
||||
```
|
||||
|
||||
### 7.3 Gamma Interference Detection Flow
|
||||
|
||||
```
|
||||
Periodic Check (via Timer)
|
||||
↓
|
||||
checkGammaInterference()
|
||||
↓
|
||||
getSwBrightness() [Read current gamma]
|
||||
↓
|
||||
Compare to saved pref
|
||||
↓
|
||||
[Difference > 2%?]
|
||||
├─[NO]──→ Return (no interference)
|
||||
└─[YES]─→ interferenceCounter++
|
||||
↓
|
||||
setSwBrightness(1) [Reset to 100%]
|
||||
↓
|
||||
[Counter >= 3?]
|
||||
├─[NO]──→ Wait for next check
|
||||
└─[YES]─→ Show Alert
|
||||
↓
|
||||
User chooses:
|
||||
├─[Quit other app]─→ Stop watching
|
||||
└─[Disable gamma]──→ For all displays:
|
||||
- savePref(true, key: .avoidGamma)
|
||||
- updateShade()
|
||||
- Reset counter
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Key Properties and State Variables
|
||||
|
||||
```swift
|
||||
// Smooth brightness state
|
||||
var smoothBrightnessTransient: Float = 1 // Current animated value (0-1)
|
||||
var smoothBrightnessRunning: Bool = false // Is animation in progress?
|
||||
var smoothBrightnessSlow: Bool = false // Use slow speed (16 vs 6 divisor)?
|
||||
|
||||
// Gamma table storage
|
||||
var defaultGammaTableRed = [CGGammaValue](repeating: 0, count: 256)
|
||||
var defaultGammaTableGreen = [CGGammaValue](repeating: 0, count: 256)
|
||||
var defaultGammaTableBlue = [CGGammaValue](repeating: 0, count: 256)
|
||||
var defaultGammaTableSampleCount: UInt32 = 0
|
||||
var defaultGammaTablePeak: Float = 1 // Max value in default tables
|
||||
|
||||
// Thread safety
|
||||
let swBrightnessSemaphore = DispatchSemaphore(value: 1)
|
||||
|
||||
// Brightness sync
|
||||
var brightnessSyncSourceValue: Float = 1 // Last user-set value for sync
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Edge Cases and Special Handling
|
||||
|
||||
### 9.1 Display Sleep/Wake
|
||||
```swift
|
||||
guard app.sleepID == 0, app.reconfigureID == 0 else {
|
||||
self.savePref(self.smoothBrightnessTransient, for: .brightness)
|
||||
self.smoothBrightnessRunning = false
|
||||
return false
|
||||
}
|
||||
```
|
||||
- Stops smooth brightness during sleep
|
||||
- Saves current transient value
|
||||
- Prevents display issues during reconfiguration
|
||||
|
||||
### 9.2 Display Reconfiguration
|
||||
- Calls `swUpdateDefaultGammaTable()` to re-capture gamma
|
||||
- Gamma enforcer moves to active display
|
||||
- Reapplies brightness after reconfiguration
|
||||
|
||||
### 9.3 Dummy Displays
|
||||
```swift
|
||||
guard !self.isDummy else {
|
||||
return true // Pretend success
|
||||
}
|
||||
```
|
||||
- Used for testing/debugging
|
||||
- Skip all hardware operations
|
||||
- Return success without doing anything
|
||||
|
||||
### 9.4 Brightness Clamping
|
||||
All brightness values clamped to [0, 1]:
|
||||
```swift
|
||||
let value = max(min(to, 1), 0)
|
||||
```
|
||||
|
||||
### 9.5 Minimum Brightness Protection
|
||||
```swift
|
||||
let lowTreshold: Float = prefs.bool(forKey: PrefKey.allowZeroSwBrightness.rawValue) ? 0.0 : 0.15
|
||||
```
|
||||
- Default: Cannot go below 15% (prevents black screen)
|
||||
- Optional: Allow 0% with preference flag
|
||||
|
||||
---
|
||||
|
||||
## 10. CoreGraphics APIs Used
|
||||
|
||||
### 10.1 CGGetDisplayTransferByTable
|
||||
```swift
|
||||
CGGetDisplayTransferByTable(displayID, sampleCount,
|
||||
&redTable, &greenTable, &blueTable, &actualCount)
|
||||
```
|
||||
**Purpose:** Reads current gamma lookup tables from display hardware
|
||||
|
||||
**Parameters:**
|
||||
- `displayID`: CGDirectDisplayID
|
||||
- `sampleCount`: Requested samples (256)
|
||||
- `redTable`, `greenTable`, `blueTable`: Output arrays
|
||||
- `actualCount`: Actual samples returned
|
||||
|
||||
**Returns:** CGError.success on success
|
||||
|
||||
### 10.2 CGSetDisplayTransferByTable
|
||||
```swift
|
||||
CGSetDisplayTransferByTable(displayID, sampleCount,
|
||||
redTable, greenTable, blueTable)
|
||||
```
|
||||
**Purpose:** Writes new gamma lookup tables to display hardware
|
||||
|
||||
**Parameters:**
|
||||
- `displayID`: CGDirectDisplayID
|
||||
- `sampleCount`: Number of samples (256)
|
||||
- `redTable`, `greenTable`, `blueTable`: Input arrays
|
||||
|
||||
**Effect:** Immediately changes display color/brightness response curve
|
||||
|
||||
---
|
||||
|
||||
## 11. Summary
|
||||
|
||||
The Display.swift brightness control system implements a sophisticated four-layer architecture:
|
||||
|
||||
1. **User Interface Layer:** `setBrightness()` - Routes to smooth or direct
|
||||
2. **Animation Layer:** `setSmoothBrightness()` - 50Hz stepped transitions
|
||||
3. **Direct Control Layer:** `setDirectBrightness()` - Immediate changes
|
||||
4. **Hardware Layer:** `setSwBrightness()` - Gamma tables or shades
|
||||
|
||||
**Key Design Decisions:**
|
||||
|
||||
- **Dual Implementation:** Gamma tables (default) vs shades (fallback)
|
||||
- **Non-linear Brightness:** 15% minimum to prevent unusable displays
|
||||
- **Smooth Animations:** Adaptive step sizing with 20ms intervals
|
||||
- **Interference Detection:** Automatic fallback when conflicts detected
|
||||
- **Thread Safety:** Semaphore prevents gamma table corruption
|
||||
- **Preference Persistence:** Per-display identification and storage
|
||||
|
||||
**The gamma table approach** multiplies default color curves by brightness scalar, effectively dimming output while maintaining color accuracy.
|
||||
|
||||
**The shade approach** overlays a semi-transparent window, providing brightness control when gamma manipulation is unavailable or undesirable.
|
||||
|
||||
Together, these mechanisms provide universal software brightness control across all display types, with graceful degradation when conflicts arise.
|
||||
Loading…
Add table
Add a link
Reference in a new issue