Skip to content
Merged
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
4 changes: 2 additions & 2 deletions .github/workflows/safari-wptrunner.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,9 @@ jobs:
REV: ${{ inputs.test-rev }}
run: |-
git switch --force --progress -d "$REV"
- name: Set display color profile
- name: Reconfigure the display(s)
run: |-
./wpt macos-color-profile
./wpt macos-display-configuration
- name: Set system caption style to ""
run: |-
defaults write com.apple.mediaaccessibility MACaptionActiveProfile -string ""
Expand Down
4 changes: 2 additions & 2 deletions tools/ci/azure/color_profile.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
steps:
- script: |
./wpt macos-color-profile
displayName: 'Set display color profile'
./wpt macos-display-configuration
displayName: 'Reconfigure the display(s)'
condition: and(succeeded(), eq(variables['Agent.OS'], 'Darwin'))
9 changes: 5 additions & 4 deletions tools/ci/commands.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@
"help": "Output a hosts file to stdout",
"virtualenv": false
},
"macos-color-profile": {
"path": "macos_color_profile.py",
"macos-display-configuration": {
"path": "macos_display_configuration.py",
"script": "run",
"help": "Change the macOS color profile to sRGB",
"parser": "create_parser",
"help": "Configure macOS displays (scale, color profile)",
"virtualenv": true,
"requirements": [
"requirements_macos_color_profile.txt"
"requirements_macos_display_configuration.txt"
]
},
"regen-certs": {
Expand Down
43 changes: 0 additions & 43 deletions tools/ci/macos_color_profile.py

This file was deleted.

239 changes: 239 additions & 0 deletions tools/ci/macos_display_configuration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
import argparse
import sys
from typing import Any, NewType, Optional, Tuple

from Cocoa import NSURL
from ColorSync import (
CGDisplayCreateUUIDFromDisplayID,
ColorSyncDeviceSetCustomProfiles,
kColorSyncDeviceDefaultProfileID,
kColorSyncDisplayDeviceClass,
)
from Quartz import (
CGBeginDisplayConfiguration,
CGCancelDisplayConfiguration,
CGCompleteDisplayConfiguration,
CGConfigureDisplayWithDisplayMode,
CGDisplayCopyAllDisplayModes,
CGDisplayCopyDisplayMode,
CGDisplayModeGetHeight,
CGDisplayModeGetIOFlags,
CGDisplayModeGetPixelHeight,
CGDisplayModeGetPixelWidth,
CGDisplayModeGetRefreshRate,
CGDisplayModeGetWidth,
CGDisplayModeIsUsableForDesktopGUI,
CGDisplayModeRef,
CGGetOnlineDisplayList,
kCGConfigurePermanently,
kCGErrorSuccess,
)

# Display mode flags
kDisplayModeDefaultFlag = 0x00000004 # noqa: N816

# Create a new type for display IDs
CGDirectDisplayID = NewType("CGDirectDisplayID", int)


def get_pixel_size(mode: CGDisplayModeRef) -> Tuple[int, int]:
return (CGDisplayModeGetPixelWidth(mode), CGDisplayModeGetPixelHeight(mode))


def get_size(mode: CGDisplayModeRef) -> Tuple[int, int]:
return (CGDisplayModeGetWidth(mode), CGDisplayModeGetHeight(mode))


def calculate_mode_similarity_score(
mode: CGDisplayModeRef, current_mode: CGDisplayModeRef
) -> int:
current_size = get_size(current_mode)
current_pixel_size = get_pixel_size(current_mode)
current_refresh_rate = CGDisplayModeGetRefreshRate(current_mode)
current_flags = CGDisplayModeGetIOFlags(current_mode)

size = get_size(mode)
pixel_size = get_pixel_size(mode)
refresh_rate = CGDisplayModeGetRefreshRate(mode)
flags = CGDisplayModeGetIOFlags(mode)

differences = 0

if size != current_size:
differences += 1
if pixel_size != current_pixel_size:
differences += 1
if refresh_rate != current_refresh_rate:
differences += 1

# Count how many individual flags are changing (XOR then count bits)
changed_flags = flags ^ current_flags
if sys.version_info >= (3, 10):
differences += changed_flags.bit_count()
else:
differences += bin(changed_flags).count("1")

return differences


def find_best_unscaled_mode(display_id: CGDirectDisplayID) -> CGDisplayModeRef:
current_mode: Optional[CGDisplayModeRef] = CGDisplayCopyDisplayMode(display_id)

# If we already have an unscaled mode, we're done.
if current_mode and (
get_size(current_mode) == get_pixel_size(current_mode)
):
return current_mode

all_modes = CGDisplayCopyAllDisplayModes(display_id, None)
if not all_modes:
raise Exception("No display modes")

# If we don't have a current mode, use the default mode instead.
if not current_mode:
default_modes = [
m for m in all_modes if CGDisplayModeGetIOFlags(m) & kDisplayModeDefaultFlag
]
if not default_modes:
raise Exception("No default display mode found")
current_mode = default_modes[0]
assert current_mode is not None

if get_size(current_mode) == get_pixel_size(current_mode):
return current_mode

candidates = [
m
for m in all_modes
if CGDisplayModeIsUsableForDesktopGUI(m) and get_size(m) == get_pixel_size(m)
]
if not candidates:
raise Exception("No suitable display modes")

same_size_candidates = [
m for m in candidates if get_size(m) == get_size(current_mode)
]
same_pixel_size_candidates = [
m for m in candidates if get_pixel_size(m) == get_pixel_size(current_mode)
]

if same_size_candidates:
candidates = same_size_candidates
elif same_pixel_size_candidates:
candidates = same_pixel_size_candidates

return min(
candidates,
key=lambda m: calculate_mode_similarity_score(m, current_mode),
)


def set_color_profiles(profile_url: NSURL, *, dry_run: bool = False) -> bool:
max_displays = 10

(err, display_ids, display_count) = CGGetOnlineDisplayList(max_displays, None, None)
if err != kCGErrorSuccess:
raise ValueError(err)

display_uuids = [CGDisplayCreateUUIDFromDisplayID(d) for d in display_ids]

for display_id, display_uuid in zip(display_ids, display_uuids):
if dry_run:
print(
f"Would set color profile for display {display_id} to {profile_url.path()}"
)
else:
profile_info = {kColorSyncDeviceDefaultProfileID: profile_url}
success = ColorSyncDeviceSetCustomProfiles(
kColorSyncDisplayDeviceClass,
display_uuid,
profile_info,
)
if not success:
raise Exception(f"failed to set profile on {display_uuid}")
print(f"Set color profile for display {display_id}")

return True


def set_display_modes(*, dry_run: bool = False) -> bool:
max_displays = 10

err, display_ids, display_count = CGGetOnlineDisplayList(max_displays, None, None)
if err != kCGErrorSuccess:
raise ValueError(err)

if dry_run:
for display_id in display_ids:
best_mode = find_best_unscaled_mode(display_id)
best_size = get_size(best_mode)
print(f"Would change display {display_id} to {best_size}")
return True

err, config_ref = CGBeginDisplayConfiguration(None)
if err != kCGErrorSuccess:
raise Exception("Failed to begin display configuration")

try:
for display_id in display_ids:
best_mode = find_best_unscaled_mode(display_id)
best_size = get_size(best_mode)

err = CGConfigureDisplayWithDisplayMode(
config_ref, display_id, best_mode, None
)
if err != kCGErrorSuccess:
raise Exception(
f"Failed to configure mode for display {display_id}: {err}"
)

print(f"Configured display {display_id} mode to {best_size}")

except Exception:
CGCancelDisplayConfiguration(config_ref)
raise

else:
err = CGCompleteDisplayConfiguration(config_ref, kCGConfigurePermanently)
if err != kCGErrorSuccess:
raise Exception(f"Failed to complete display configuration: {err}")

print("Display configuration applied permanently")

return True


def create_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser()
parser.add_argument(
"--dry-run",
action="store_true",
help="Show what would be done without making changes",
)
parser.add_argument(
"--no-color-profile",
action="store_false",
help="Don't set color profiles",
)
parser.add_argument(
"--no-display-mode",
action="store_false",
help="Don't set display mode",
)
parser.add_argument(
"--profile-path",
default="/System/Library/ColorSync/Profiles/sRGB Profile.icc",
help="Path to color profile to use (default: sRGB)",
)
return parser


def run(venv: Any, **kwargs: Any) -> None:
profile_url = NSURL.fileURLWithPath_(kwargs["profile_path"])
dry_run = kwargs["dry_run"]

if not kwargs["no_color_profile"]:
set_color_profiles(profile_url, dry_run=dry_run)

if not kwargs["no_display_mode"]:
set_display_modes(dry_run=dry_run)
Loading