Close display verification races
This commit is contained in:
@@ -36,6 +36,19 @@ ShellRoot {
|
|||||||
return Displays.apply(monitor.name, mode, scale, monitor.transform);
|
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 {
|
function applyBad(kind: string): bool {
|
||||||
const monitor = Displays.monitors[0];
|
const monitor = Displays.monitors[0];
|
||||||
if (!monitor) return false;
|
if (!monitor) return false;
|
||||||
|
|||||||
@@ -64,9 +64,8 @@ Column {
|
|||||||
|
|
||||||
required property var modelData
|
required property var modelData
|
||||||
|
|
||||||
readonly property bool selected: root.monitor
|
readonly property bool selected: resolution.isCurrent
|
||||||
&& resolution.isCurrent
|
&& Displays.modeIsCurrent(root.monitor, rate.modelData)
|
||||||
&& Math.round(root.monitor.refreshRate) === rate.modelData.refresh
|
|
||||||
|
|
||||||
implicitWidth: Math.max(74, rateCaption.implicitWidth + 22)
|
implicitWidth: Math.max(74, rateCaption.implicitWidth + 22)
|
||||||
implicitHeight: 30
|
implicitHeight: 30
|
||||||
|
|||||||
@@ -127,14 +127,16 @@ SettingsPage {
|
|||||||
TextRow {
|
TextRow {
|
||||||
label: "Color mode"
|
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."
|
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 {
|
TextRow {
|
||||||
label: "Variable refresh"
|
label: "Variable refresh"
|
||||||
detail: SystemSettings.monitorVrrActive
|
detail: root.monitor && root.monitor.vrr
|
||||||
? "Active for current fullscreen content"
|
? "Active on this output for current fullscreen content"
|
||||||
: "Ready when game or video content requests it"
|
: "This output is ready when game or video content requests it"
|
||||||
value: SystemSettings.monitorVrrActive ? "Active" : "Standby"
|
value: root.monitor && root.monitor.vrr ? "Active" : "Standby"
|
||||||
divider: Displays.isOverridden(root.monitor ? root.monitor.name : "")
|
divider: Displays.isOverridden(root.monitor ? root.monitor.name : "")
|
||||||
}
|
}
|
||||||
ActionRow {
|
ActionRow {
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ Singleton {
|
|||||||
property bool revertQueued: false
|
property bool revertQueued: false
|
||||||
property var revertExpected: null
|
property var revertExpected: null
|
||||||
property string revertReason: ""
|
property string revertReason: ""
|
||||||
|
property bool revertVerificationActive: false
|
||||||
|
property int operationGeneration: 0
|
||||||
|
property int revertGeneration: -1
|
||||||
property int secondsLeft: 0
|
property int secondsLeft: 0
|
||||||
|
|
||||||
readonly property bool awaitingConfirmation: root.pendingOutput !== ""
|
readonly property bool awaitingConfirmation: root.pendingOutput !== ""
|
||||||
@@ -63,13 +66,16 @@ Singleton {
|
|||||||
|
|
||||||
Process {
|
Process {
|
||||||
id: query
|
id: query
|
||||||
|
property int generation: 0
|
||||||
command: ["hyprctl", "-j", "monitors"]
|
command: ["hyprctl", "-j", "monitors"]
|
||||||
stdout: StdioCollector {
|
stdout: StdioCollector {
|
||||||
onStreamFinished: root.parse(this.text)
|
onStreamFinished: root.parse(this.text, query.generation)
|
||||||
}
|
}
|
||||||
onExited: (exitCode, exitStatus) => {
|
onExited: (exitCode, exitStatus) => {
|
||||||
if (exitCode !== 0)
|
if (exitCode !== 0)
|
||||||
root.lastError = "Could not read the connected displays.";
|
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) => {
|
onExited: (exitCode, exitStatus) => {
|
||||||
// Exit status is advisory only. Hyprland's Lua bridge can report
|
// Exit status is advisory only. Hyprland's Lua bridge can report
|
||||||
// success without applying a value, so exact readback decides.
|
// success without applying a value, so exact readback decides.
|
||||||
|
root.revertVerificationActive = true;
|
||||||
revertVerifyTimer.attempts = 0;
|
revertVerifyTimer.attempts = 0;
|
||||||
revertVerifyTimer.restart();
|
revertVerifyTimer.restart();
|
||||||
}
|
}
|
||||||
@@ -104,11 +111,13 @@ Singleton {
|
|||||||
Component.onCompleted: root.refresh()
|
Component.onCompleted: root.refresh()
|
||||||
|
|
||||||
function refresh(): void {
|
function refresh(): void {
|
||||||
if (!query.running)
|
if (!query.running) {
|
||||||
|
query.generation = root.operationGeneration;
|
||||||
query.running = true;
|
query.running = true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function parse(text: string): void {
|
function parse(text: string, generation: int): void {
|
||||||
try {
|
try {
|
||||||
const raw = JSON.parse(text);
|
const raw = JSON.parse(text);
|
||||||
root.monitors = raw.map(monitor => {
|
root.monitors = raw.map(monitor => {
|
||||||
@@ -129,6 +138,9 @@ Singleton {
|
|||||||
mode: current?.mode ?? `${width}x${height}@${refreshRate}`,
|
mode: current?.mode ?? `${width}x${height}@${refreshRate}`,
|
||||||
scale: monitor.scale ?? 1,
|
scale: monitor.scale ?? 1,
|
||||||
transform: monitor.transform ?? 0,
|
transform: monitor.transform ?? 0,
|
||||||
|
currentFormat: monitor.currentFormat ?? "",
|
||||||
|
colorPreset: monitor.colorManagementPreset ?? "",
|
||||||
|
vrr: monitor.vrr === true,
|
||||||
modes: modes
|
modes: modes
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -137,9 +149,13 @@ Singleton {
|
|||||||
root.pendingVerified = true;
|
root.pendingVerified = true;
|
||||||
verifyTimer.stop();
|
verifyTimer.stop();
|
||||||
root.lastError = "";
|
root.lastError = "";
|
||||||
} else if (root.revertExpected
|
} else if (root.revertVerificationActive
|
||||||
|
&& generation === root.revertGeneration
|
||||||
|
&& root.revertExpected
|
||||||
&& root.matchesRequest(root.monitorNamed(root.revertExpected.output), root.revertExpected)) {
|
&& root.matchesRequest(root.monitorNamed(root.revertExpected.output), root.revertExpected)) {
|
||||||
revertVerifyTimer.stop();
|
revertVerifyTimer.stop();
|
||||||
|
root.revertVerificationActive = false;
|
||||||
|
root.revertGeneration = -1;
|
||||||
root.revertExpected = null;
|
root.revertExpected = null;
|
||||||
if (root.revertReason === "")
|
if (root.revertReason === "")
|
||||||
root.lastError = "";
|
root.lastError = "";
|
||||||
@@ -240,6 +256,13 @@ Singleton {
|
|||||||
&& monitor.transform === requested.transform;
|
&& 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
|
// Applies immediately and starts the countdown. Nothing is stored yet: the
|
||||||
// settings file is only written by confirm().
|
// settings file is only written by confirm().
|
||||||
function apply(output: string, mode: string, scale: real, transform: int): bool {
|
function apply(output: string, mode: string, scale: real, transform: int): bool {
|
||||||
@@ -275,6 +298,7 @@ Singleton {
|
|||||||
scale: monitor.scale,
|
scale: monitor.scale,
|
||||||
transform: monitor.transform
|
transform: monitor.transform
|
||||||
};
|
};
|
||||||
|
root.operationGeneration++;
|
||||||
root.pendingRequested = {
|
root.pendingRequested = {
|
||||||
output: output,
|
output: output,
|
||||||
mode: mode,
|
mode: mode,
|
||||||
@@ -348,7 +372,7 @@ Singleton {
|
|||||||
root.revertReason = message;
|
root.revertReason = message;
|
||||||
if (message !== "")
|
if (message !== "")
|
||||||
root.lastError = message;
|
root.lastError = message;
|
||||||
if (applyRun.running) {
|
if (applyRun.running || query.running) {
|
||||||
root.revertQueued = true;
|
root.revertQueued = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -357,7 +381,10 @@ Singleton {
|
|||||||
|
|
||||||
function performRevert(): void {
|
function performRevert(): void {
|
||||||
const previous = root.pendingPrevious;
|
const previous = root.pendingPrevious;
|
||||||
|
root.operationGeneration++;
|
||||||
|
root.revertGeneration = root.operationGeneration;
|
||||||
root.revertExpected = previous;
|
root.revertExpected = previous;
|
||||||
|
root.revertVerificationActive = false;
|
||||||
root.clearPending();
|
root.clearPending();
|
||||||
if (previous) {
|
if (previous) {
|
||||||
revertRun.exec(["hyprctl", "eval",
|
revertRun.exec(["hyprctl", "eval",
|
||||||
@@ -405,6 +432,8 @@ Singleton {
|
|||||||
attempts++;
|
attempts++;
|
||||||
if (attempts > 25) {
|
if (attempts > 25) {
|
||||||
stop();
|
stop();
|
||||||
|
root.revertVerificationActive = false;
|
||||||
|
root.revertGeneration = -1;
|
||||||
root.revertExpected = null;
|
root.revertExpected = null;
|
||||||
root.revertReason = "";
|
root.revertReason = "";
|
||||||
root.lastError = "The previous display setting could not be verified. Open Displays and restore it manually.";
|
root.lastError = "The previous display setting could not be verified. Open Displays and restore it manually.";
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ fail() {
|
|||||||
for contract in \
|
for contract in \
|
||||||
'property var pendingRequested:' \
|
'property var pendingRequested:' \
|
||||||
'property var revertExpected:' \
|
'property var revertExpected:' \
|
||||||
|
'property bool revertVerificationActive:' \
|
||||||
|
'property int revertGeneration:' \
|
||||||
'readonly property bool canConfirm:' \
|
'readonly property bool canConfirm:' \
|
||||||
'function matchesRequest(' \
|
'function matchesRequest(' \
|
||||||
'function scalesForMode(' \
|
'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'
|
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 }')"
|
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
|
for _ in $(seq 1 50); do
|
||||||
[[ "$(status | jq -r .count)" != "0" ]] && break
|
[[ "$(status | jq -r .count)" != "0" ]] && break
|
||||||
sleep 0.1
|
sleep 0.1
|
||||||
|
|||||||
Reference in New Issue
Block a user