Close display verification races

This commit is contained in:
Gabriel Brown
2026-08-18 02:55:43 -04:00
parent efc53435a2
commit 0a18290137
5 changed files with 66 additions and 13 deletions
@@ -36,6 +36,19 @@ ShellRoot {
return Displays.apply(monitor.name, mode, scale, monitor.transform);
}
function refreshIdentityFixture(): string {
const modes = Displays.normaliseModes([
"[email protected]",
"[email protected]"
]);
const monitor = { width: 1920, height: 1080, refreshRate: 59.94 };
return JSON.stringify({
count: modes.length,
modes: modes.map(mode => mode.mode),
selected: modes.filter(mode => Displays.modeIsCurrent(monitor, mode)).map(mode => mode.mode)
});
}
function applyBad(kind: string): bool {
const monitor = Displays.monitors[0];
if (!monitor) return false;
@@ -64,9 +64,8 @@ Column {
required property var modelData
readonly property bool selected: root.monitor
&& resolution.isCurrent
&& Math.round(root.monitor.refreshRate) === rate.modelData.refresh
readonly property bool selected: resolution.isCurrent
&& Displays.modeIsCurrent(root.monitor, rate.modelData)
implicitWidth: Math.max(74, rateCaption.implicitWidth + 22)
implicitHeight: 30
@@ -127,14 +127,16 @@ SettingsPage {
TextRow {
label: "Color mode"
detail: "Wide-gamut SDR at 10-bit. Full-time HDR is left to the Hyprland config: it currently breaks screenshots, OBS, and the lock screen's blurred background."
value: SystemSettings.colorPreset || "wide"
value: root.monitor
? `${root.monitor.colorPreset || "standard"} · ${root.monitor.currentFormat || "detecting format"}`
: "Detecting"
}
TextRow {
label: "Variable refresh"
detail: SystemSettings.monitorVrrActive
? "Active for current fullscreen content"
: "Ready when game or video content requests it"
value: SystemSettings.monitorVrrActive ? "Active" : "Standby"
detail: root.monitor && root.monitor.vrr
? "Active on this output for current fullscreen content"
: "This output is ready when game or video content requests it"
value: root.monitor && root.monitor.vrr ? "Active" : "Standby"
divider: Displays.isOverridden(root.monitor ? root.monitor.name : "")
}
ActionRow {
+34 -5
View File
@@ -38,6 +38,9 @@ Singleton {
property bool revertQueued: false
property var revertExpected: null
property string revertReason: ""
property bool revertVerificationActive: false
property int operationGeneration: 0
property int revertGeneration: -1
property int secondsLeft: 0
readonly property bool awaitingConfirmation: root.pendingOutput !== ""
@@ -63,13 +66,16 @@ Singleton {
Process {
id: query
property int generation: 0
command: ["hyprctl", "-j", "monitors"]
stdout: StdioCollector {
onStreamFinished: root.parse(this.text)
onStreamFinished: root.parse(this.text, query.generation)
}
onExited: (exitCode, exitStatus) => {
if (exitCode !== 0)
root.lastError = "Could not read the connected displays.";
if (root.revertQueued && !applyRun.running && root.awaitingConfirmation)
root.performRevert();
}
}
@@ -96,6 +102,7 @@ Singleton {
onExited: (exitCode, exitStatus) => {
// Exit status is advisory only. Hyprland's Lua bridge can report
// success without applying a value, so exact readback decides.
root.revertVerificationActive = true;
revertVerifyTimer.attempts = 0;
revertVerifyTimer.restart();
}
@@ -104,11 +111,13 @@ Singleton {
Component.onCompleted: root.refresh()
function refresh(): void {
if (!query.running)
if (!query.running) {
query.generation = root.operationGeneration;
query.running = true;
}
}
function parse(text: string): void {
function parse(text: string, generation: int): void {
try {
const raw = JSON.parse(text);
root.monitors = raw.map(monitor => {
@@ -129,6 +138,9 @@ Singleton {
mode: current?.mode ?? `${width}x${height}@${refreshRate}`,
scale: monitor.scale ?? 1,
transform: monitor.transform ?? 0,
currentFormat: monitor.currentFormat ?? "",
colorPreset: monitor.colorManagementPreset ?? "",
vrr: monitor.vrr === true,
modes: modes
};
});
@@ -137,9 +149,13 @@ Singleton {
root.pendingVerified = true;
verifyTimer.stop();
root.lastError = "";
} else if (root.revertExpected
} else if (root.revertVerificationActive
&& generation === root.revertGeneration
&& root.revertExpected
&& root.matchesRequest(root.monitorNamed(root.revertExpected.output), root.revertExpected)) {
revertVerifyTimer.stop();
root.revertVerificationActive = false;
root.revertGeneration = -1;
root.revertExpected = null;
if (root.revertReason === "")
root.lastError = "";
@@ -240,6 +256,13 @@ Singleton {
&& monitor.transform === requested.transform;
}
function modeIsCurrent(monitor: var, candidate: var): bool {
return !!monitor && !!candidate
&& monitor.width === candidate.width
&& monitor.height === candidate.height
&& Math.abs(monitor.refreshRate - candidate.refresh) < 0.01;
}
// Applies immediately and starts the countdown. Nothing is stored yet: the
// settings file is only written by confirm().
function apply(output: string, mode: string, scale: real, transform: int): bool {
@@ -275,6 +298,7 @@ Singleton {
scale: monitor.scale,
transform: monitor.transform
};
root.operationGeneration++;
root.pendingRequested = {
output: output,
mode: mode,
@@ -348,7 +372,7 @@ Singleton {
root.revertReason = message;
if (message !== "")
root.lastError = message;
if (applyRun.running) {
if (applyRun.running || query.running) {
root.revertQueued = true;
return;
}
@@ -357,7 +381,10 @@ Singleton {
function performRevert(): void {
const previous = root.pendingPrevious;
root.operationGeneration++;
root.revertGeneration = root.operationGeneration;
root.revertExpected = previous;
root.revertVerificationActive = false;
root.clearPending();
if (previous) {
revertRun.exec(["hyprctl", "eval",
@@ -405,6 +432,8 @@ Singleton {
attempts++;
if (attempts > 25) {
stop();
root.revertVerificationActive = false;
root.revertGeneration = -1;
root.revertExpected = null;
root.revertReason = "";
root.lastError = "The previous display setting could not be verified. Open Displays and restore it manually.";
+10
View File
@@ -37,6 +37,8 @@ fail() {
for contract in \
'property var pendingRequested:' \
'property var revertExpected:' \
'property bool revertVerificationActive:' \
'property int revertGeneration:' \
'readonly property bool canConfirm:' \
'function matchesRequest(' \
'function scalesForMode(' \
@@ -180,6 +182,14 @@ done
run ipc show 2>/dev/null | rg -q '^target displays-test$' || fail 'test IPC target did not start'
harness_pid="$(run list | awk '/Process ID:/ { print $3; exit }')"
refresh_fixture="$(run ipc call displays-test refreshIdentityFixture)"
jq -e '
.count == 2
and .modes == ["[email protected]", "[email protected]"]
and .selected == ["[email protected]"]
' <<<"$refresh_fixture" >/dev/null \
|| fail "59.94 Hz and 60.00 Hz lost their distinct selection identity: $refresh_fixture"
for _ in $(seq 1 50); do
[[ "$(status | jq -r .count)" != "0" ]] && break
sleep 0.1