diff --git a/providers/base/data/vrr_rectangles_test.qml b/providers/base/data/vrr_rectangles_test.qml new file mode 100644 index 0000000000..5033975ca9 --- /dev/null +++ b/providers/base/data/vrr_rectangles_test.qml @@ -0,0 +1,234 @@ +/* This file is part of Checkbox. + + Copyright 2026 Canonical Ltd. + Written by: + Zhongning Li + + Checkbox is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License version 3, + as published by the Free Software Foundation. + + Checkbox is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Checkbox. If not, see . +*/ + +import QtQuick 2.0 +import QtQuick.Window 2.0 +import QtQuick.Controls 1.4 +import QtQuick.Layouts 1.2 + +Window { + id: root + width: 1280 + height: 720 + visible: true + title: "Dynamic Refresh Rate Demo" + color: "#1a1a1a" + + property int targetFps: 60 + property int rectCount: 5 + + property int minFps: 10 + property int maxFps: 200 + + property int minRectCount: 1 + property int maxRectCount: 10 + + // old QT workaround, there's no 'Shortcuts' property on Window + // in new QTs don't do this + Item { + anchors.fill: parent + focus: true + Keys.onPressed: { + if (event.key === Qt.Key_Escape) { + root.close() + } + } + } + + Timer { + interval: 1000 / targetFps + running: true + repeat: true + onTriggered: { + // manually update the positions + // this also implicitly changes the framerate to the requested one + for (var i = 0; i < rectContainer.children.length; i++) { + // must use 'var' here, not let/const like modern js + // otherwise older QT won't understand it + var rect = rectContainer.children[i]; + // this QT version doesn't support optional chaining + // don't use rect?.updatePosition?.() + // method existence also must be checked + // since it doesn't exist on the very 1st frame + if (rect.updatePosition) { + rect.updatePosition(); + } + } + } + } + + // Container for the rectangles + Item { + id: rectContainer + anchors.fill: parent + + Repeater { + model: rectCount + Rectangle { + width: Screen.width * 0.05 + height: Screen.width * 0.05 + color: Qt.hsla(Math.random(), 0.6, 0.6, 0.9) + radius: 4 + + // velocity x and y + property real vx: (Math.random() - 0.5) * 500 + property real vy: (Math.random() - 0.5) * 500 + + Component.onCompleted: { + x = Math.random() * (root.width - width); + y = Math.random() * (root.height - height); + } + + // Custom function called by the Timer + function updatePosition() { + // normalize position change w.r.t. fps + x += vx / targetFps; + y += vy / targetFps; + + // bounce at the walls + if (x <= 0 || x >= root.width - width) { + vx *= -1; + } + if (y <= 0 || y >= root.height - height) { + vy *= -1 + } + } + } + } + } + + // Control Panel + Rectangle { + id: panel + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + width: contentColumn.implicitWidth + 50 + height: contentColumn.implicitHeight + 50 + color: "#cc000000" + radius: 12 + border.color: "white" + anchors.margins: 10 + + Column { + id: contentColumn + anchors.centerIn: parent + spacing: 5 + + Text { + text: "Press Esc to quit" + color: "white" + font.bold: true + anchors.left: parent.left + } + + Text { + text: "This test should be run at fullscreen" + color: "grey" + anchors.left: parent.left + } + + Text { + text: "Set GALLIUM_HUD=fps vblank_mode=3 MESA_VK_WSI_PRESENT_MODE=relaxed" + color: "grey" + anchors.left: parent.left + } + + Text { + text: "Look for tearing ONLY. Any stutter or fps mismatch is OK." + color: "red" + font.bold: true + anchors.left: parent.left + } + + Text { + text: "Requested Refresh Rate: " + targetFps + " FPS" + color: "white" + font.bold: true + anchors.horizontalCenter: parent.horizontalCenter + } + + RowLayout { + Layout.minimumHeight: 10 + Layout.fillWidth: true + anchors.horizontalCenter: parent.horizontalCenter + Button { + text: '-' + Layout.fillHeight: true + Layout.fillWidth: true + onClicked: { + targetFps = Math.max(minFps, targetFps - 1) + } + } + Slider { + minimumValue: minFps + maximumValue: maxFps + value: targetFps + onValueChanged: { + targetFps = Math.floor(value) + } + } + Button { + text: '+' + Layout.fillHeight: true + Layout.fillWidth: true + onClicked: { + targetFps = Math.min(maxFps, targetFps + 1) + } + } + } + + Text { + text: "Number of rectangles: " + rectCount + color: "white" + font.bold: true + anchors.horizontalCenter: parent.horizontalCenter + } + + RowLayout { + Layout.minimumHeight: 10 + Layout.fillWidth: true + anchors.horizontalCenter: parent.horizontalCenter + Button { + text: '-' + Layout.fillHeight: true + Layout.fillWidth: true + onClicked: { + rectCount = Math.max(minRectCount, rectCount - 1) + } + } + Slider { + minimumValue: 1 + maximumValue: 10 + value: rectCount + onValueChanged: { + rectCount = Math.floor(value) + } + } + Button { + text: '+' + Layout.fillHeight: true + Layout.fillWidth: true + onClicked: { + rectCount = Math.min(maxRectCount, rectCount + 1) + } + } + } + } + } +} \ No newline at end of file diff --git a/providers/base/units/graphics/test-plan.pxu b/providers/base/units/graphics/test-plan.pxu index 6976dc2232..fce3e0f70d 100644 --- a/providers/base/units/graphics/test-plan.pxu +++ b/providers/base/units/graphics/test-plan.pxu @@ -409,3 +409,9 @@ include: bootstrap_include: graphics_card executable + +id: vrr-test-plan +unit: test plan +_name: Variable-Refresh-Rate Test Plan +include: + graphics/variable-refresh-rate.* diff --git a/providers/base/units/graphics/vrr.yaml b/providers/base/units/graphics/vrr.yaml new file mode 100644 index 0000000000..fc5e3b4379 --- /dev/null +++ b/providers/base/units/graphics/vrr.yaml @@ -0,0 +1,39 @@ +unit: job +plugin: user-interact-verify +category_id: com.canonical.plainbox::graphics +id: graphics/variable-refresh-rate-manual +flags: + - also-after-suspend +imports: + - from com.canonical.plainbox import manifest +requires: + - executable.name == 'qmlscene' + - manifest.has_vrr_support == 'True' + - dmi.sane_product == 'portable' # don't run this if there's no built-in screen +# the hud shows an fps graph over time +# vblank_mode=3 forces vsync for opengl +# https://dri.freedesktop.org/wiki/ConfigurationOptions/ +# MESA_VK_WSI_PRESENT_MODE=relaxed is the relaxed vsync for vulkan +# https://docs.mesa3d.org/envvars.html#envvar-MESA_VK_WSI_PRESENT_MODE +# vblank is ignored by vulkan and the MESA_VK var is ignored by opengl => they don't conflict each other +command: >- + GALLIUM_HUD=fps + MESA_VK_WSI_PRESENT_MODE=relaxed + vblank_mode=3 + qmlscene "$PLAINBOX_PROVIDER_DATA"/vrr_rectangles_test.qml --fullscreen +estimated_duration: "20s" +summary: This test checks if Variable-Refresh-Rate (VRR) is working on the built-in display +steps: | + 1. Press Enter to start the test program + 2. Inside the test program, you will see multiple rectangles bouncing inside the window. + Adjust the FPS slider and look for screen tearing. + It's OK for the FPS meter to not match the requested FPS. + 3. If the DUT appears to be struggling to render all the rectangles, + use the bottom slider to adjust the amount. + 4. Press Esc to quit the program + 5. Mark as pass if no tearing was observed, fail otherwise. +--- +unit: manifest entry +id: has_vrr_support +name: Does the device have a built-in display with Variable Refresh Rate (VRR)? +value-type: bool