From 07eb5b30c5594f324086a9bb5c8e5c9d05d0e2d3 Mon Sep 17 00:00:00 2001 From: Raphael <68374617+raphaelscholle@users.noreply.github.com> Date: Sat, 24 Jan 2026 19:39:15 +0100 Subject: [PATCH 01/21] dmabuf --- app/util/qrenderstats.cpp | 6 +++--- .../avcodec/drm_kms/rk3588_video_link.cpp | 16 +++++++++++++++- .../avcodec/drm_kms/rk3588_video_link.h | 2 +- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/app/util/qrenderstats.cpp b/app/util/qrenderstats.cpp index c823273b7..455c3cd77 100644 --- a/app/util/qrenderstats.cpp +++ b/app/util/qrenderstats.cpp @@ -1,6 +1,6 @@ #include "qrenderstats.h" -#if defined(__linux__) && defined(IS_PLATFORM_ROCK) +#if defined(__linux__) #include "videostreaming/avcodec/drm_kms/rk3588_video_link.h" #endif @@ -35,7 +35,7 @@ void QRenderStats::registerOnWindow(QQuickWindow *window) connect(window, &QQuickWindow::afterRendering, this, &QRenderStats::m_QQuickWindow_afterRendering, Qt::DirectConnection); connect(window, &QQuickWindow::beforeRenderPassRecording, this, &QRenderStats::m_QQuickWindow_beforeRenderPassRecording, Qt::DirectConnection); connect(window, &QQuickWindow::afterRenderPassRecording, this, &QRenderStats::m_QQuickWindow_afterRenderPassRecording, Qt::DirectConnection); -#if defined(__linux__) && defined(IS_PLATFORM_ROCK) +#if defined(__linux__) Rk3588VideoLink::instance().ensure_started(); #endif } @@ -57,7 +57,7 @@ void QRenderStats::set_display_width_height(int width, int height) void QRenderStats::m_QQuickWindow_beforeRendering() { //m_avg_rendering_time.start(); -#if defined(__linux__) && defined(IS_PLATFORM_ROCK) +#if defined(__linux__) Rk3588VideoLink::instance().present_if_pending(); #endif } diff --git a/app/videostreaming/avcodec/drm_kms/rk3588_video_link.cpp b/app/videostreaming/avcodec/drm_kms/rk3588_video_link.cpp index dd1778f2d..00e3e280d 100644 --- a/app/videostreaming/avcodec/drm_kms/rk3588_video_link.cpp +++ b/app/videostreaming/avcodec/drm_kms/rk3588_video_link.cpp @@ -1,6 +1,6 @@ #include "rk3588_video_link.h" -#if defined(__linux__) && defined(IS_PLATFORM_ROCK) +#if defined(__linux__) #include #include @@ -131,6 +131,8 @@ bool Rk3588VideoLink::init_drm() { std::cerr << "Rk3588VideoLink: missing required plane properties\n"; return false; } + std::cerr << "Rk3588VideoLink: using connector " << connector_id << " crtc " << crtc_id + << " plane " << plane_id << " display " << display_width << "x" << display_height << "\n"; return true; } @@ -331,6 +333,8 @@ void Rk3588VideoLink::receiver_thread() { close(sock); return; } + std::cerr << "Rk3588VideoLink: listening on " << kSocketPath << "\n"; + uint64_t recv_count = 0; while (!shutdown_requested) { DmabufFrameInfo info{}; char cmsgbuf[CMSG_SPACE(sizeof(int) * 4)]; @@ -395,6 +399,14 @@ void Rk3588VideoLink::receiver_thread() { pending.fb_id = fb_id; pending.meta = meta; has_pending = true; + recv_count++; + if (recv_count % 120 == 0) { + std::cerr << "Rk3588VideoLink: received " << recv_count << " frames (" + << meta.width << "x" << meta.height << ")\n"; + } + } else { + std::cerr << "Rk3588VideoLink: import failed for frame " + << meta.width << "x" << meta.height << "\n"; } close_fds(fds); } @@ -550,6 +562,8 @@ void Rk3588VideoLink::present_if_pending() { } if (drmModeAtomicCommit(drm_fd, req, DRM_MODE_ATOMIC_NONBLOCK, nullptr) == 0) { current_fb_id = frame.fb_id; + } else { + std::cerr << "Rk3588VideoLink: atomic commit failed: " << strerror(errno) << "\n"; } drmModeAtomicFree(req); } diff --git a/app/videostreaming/avcodec/drm_kms/rk3588_video_link.h b/app/videostreaming/avcodec/drm_kms/rk3588_video_link.h index 9257c2142..278b7832d 100644 --- a/app/videostreaming/avcodec/drm_kms/rk3588_video_link.h +++ b/app/videostreaming/avcodec/drm_kms/rk3588_video_link.h @@ -3,7 +3,7 @@ #include -#if defined(__linux__) && defined(IS_PLATFORM_ROCK) +#if defined(__linux__) #include #include #include From 3af2df5b1557578d657f17c8d21ca6e299a33fa7 Mon Sep 17 00:00:00 2001 From: Raphael <68374617+raphaelscholle@users.noreply.github.com> Date: Sat, 24 Jan 2026 19:49:20 +0100 Subject: [PATCH 02/21] Update rk3588_video_link.cpp --- .../avcodec/drm_kms/rk3588_video_link.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/videostreaming/avcodec/drm_kms/rk3588_video_link.cpp b/app/videostreaming/avcodec/drm_kms/rk3588_video_link.cpp index 00e3e280d..5ff7586b2 100644 --- a/app/videostreaming/avcodec/drm_kms/rk3588_video_link.cpp +++ b/app/videostreaming/avcodec/drm_kms/rk3588_video_link.cpp @@ -335,6 +335,8 @@ void Rk3588VideoLink::receiver_thread() { } std::cerr << "Rk3588VideoLink: listening on " << kSocketPath << "\n"; uint64_t recv_count = 0; + uint64_t short_count = 0; + uint64_t bad_magic_count = 0; while (!shutdown_requested) { DmabufFrameInfo info{}; char cmsgbuf[CMSG_SPACE(sizeof(int) * 4)]; @@ -361,10 +363,19 @@ void Rk3588VideoLink::receiver_thread() { } } if (static_cast(ret) < sizeof(DmabufFrameInfo)) { + short_count++; + if (short_count % 120 == 0) { + std::cerr << "Rk3588VideoLink: short packet size=" << ret << "\n"; + } close_fds(fds); continue; } if (info.magic != kMagic || info.version != kVersion) { + bad_magic_count++; + if (bad_magic_count % 120 == 0) { + std::cerr << "Rk3588VideoLink: bad magic/version " << info.magic + << " v" << info.version << "\n"; + } close_fds(fds); continue; } From 06622482d699b1512463e2d62cb818dcfbde9b83 Mon Sep 17 00:00:00 2001 From: Raphael <68374617+raphaelscholle@users.noreply.github.com> Date: Sat, 24 Jan 2026 19:55:58 +0100 Subject: [PATCH 03/21] kms tests --- .../avcodec/drm_kms/rk3588_video_link.cpp | 73 +++++++++++++++++-- .../avcodec/drm_kms/rk3588_video_link.h | 1 + 2 files changed, 66 insertions(+), 8 deletions(-) diff --git a/app/videostreaming/avcodec/drm_kms/rk3588_video_link.cpp b/app/videostreaming/avcodec/drm_kms/rk3588_video_link.cpp index 5ff7586b2..0e07bdaed 100644 --- a/app/videostreaming/avcodec/drm_kms/rk3588_video_link.cpp +++ b/app/videostreaming/avcodec/drm_kms/rk3588_video_link.cpp @@ -17,6 +17,10 @@ #include #include +#include +#include +#include + #include "util/qrenderstats.h" namespace { @@ -52,7 +56,7 @@ Rk3588VideoLink::~Rk3588VideoLink() { recv_thread.join(); } cleanup_cache(); - if (drm_fd >= 0) { + if (drm_fd >= 0 && owns_fd) { close(drm_fd); drm_fd = -1; } @@ -99,18 +103,70 @@ void Rk3588VideoLink::ensure_started() { } bool Rk3588VideoLink::init_drm() { - drm_fd = open("/dev/dri/card0", O_RDWR | O_CLOEXEC); + owns_fd = true; + if (QGuiApplication::platformName().contains("eglfs", Qt::CaseInsensitive)) { + auto* pni = QGuiApplication::platformNativeInterface(); + if (pni) { + void* fd_ptr = pni->nativeResourceForIntegration("dri_fd"); + if (fd_ptr) { + drm_fd = static_cast(reinterpret_cast(fd_ptr)); + owns_fd = false; + QScreen* screen = QGuiApplication::primaryScreen(); + if (screen) { + void* crtc_ptr = pni->nativeResourceForScreen("dri_crtcid", screen); + void* conn_ptr = pni->nativeResourceForScreen("dri_connectorid", screen); + if (crtc_ptr) { + crtc_id = static_cast(reinterpret_cast(crtc_ptr)); + } + if (conn_ptr) { + connector_id = static_cast(reinterpret_cast(conn_ptr)); + } + if (screen) { + const auto size = screen->size(); + display_width = size.width(); + display_height = size.height(); + } + } + std::cerr << "Rk3588VideoLink: using EGLFS drm fd=" << drm_fd + << " is_master=" << drmIsMaster(drm_fd) + << " crtc=" << crtc_id << " connector=" << connector_id << "\n"; + } + } + } if (drm_fd < 0) { - std::cerr << "Rk3588VideoLink: failed to open /dev/dri/card0: " << strerror(errno) << "\n"; - return false; + drm_fd = open("/dev/dri/card0", O_RDWR | O_CLOEXEC); + if (drm_fd < 0) { + std::cerr << "Rk3588VideoLink: failed to open /dev/dri/card0: " << strerror(errno) << "\n"; + return false; + } + } + if (drmSetClientCap(drm_fd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1) != 0) { + std::cerr << "Rk3588VideoLink: failed to enable universal planes: " << strerror(errno) << "\n"; } if (drmSetClientCap(drm_fd, DRM_CLIENT_CAP_ATOMIC, 1) != 0) { std::cerr << "Rk3588VideoLink: failed to enable atomic: " << strerror(errno) << "\n"; return false; } - if (!find_active_crtc()) { - std::cerr << "Rk3588VideoLink: failed to find active CRTC\n"; - return false; + if (crtc_id == 0 || connector_id == 0) { + if (!find_active_crtc()) { + std::cerr << "Rk3588VideoLink: failed to find active CRTC\n"; + return false; + } + } else { + drmModeRes* res = drmModeGetResources(drm_fd); + if (res) { + for (int c = 0; c < res->count_crtcs; ++c) { + if (res->crtcs[c] == crtc_id) { + crtc_index = c; + break; + } + } + drmModeFreeResources(res); + } + if (crtc_index < 0) { + std::cerr << "Rk3588VideoLink: failed to map crtc index for crtc " << crtc_id << "\n"; + return false; + } } if (!pick_overlay_plane(DRM_FORMAT_NV12, DRM_FORMAT_MOD_INVALID)) { std::cerr << "Rk3588VideoLink: failed to find overlay plane\n"; @@ -574,7 +630,8 @@ void Rk3588VideoLink::present_if_pending() { if (drmModeAtomicCommit(drm_fd, req, DRM_MODE_ATOMIC_NONBLOCK, nullptr) == 0) { current_fb_id = frame.fb_id; } else { - std::cerr << "Rk3588VideoLink: atomic commit failed: " << strerror(errno) << "\n"; + std::cerr << "Rk3588VideoLink: atomic commit failed: " << strerror(errno) + << " is_master=" << drmIsMaster(drm_fd) << "\n"; } drmModeAtomicFree(req); } diff --git a/app/videostreaming/avcodec/drm_kms/rk3588_video_link.h b/app/videostreaming/avcodec/drm_kms/rk3588_video_link.h index 278b7832d..23520de42 100644 --- a/app/videostreaming/avcodec/drm_kms/rk3588_video_link.h +++ b/app/videostreaming/avcodec/drm_kms/rk3588_video_link.h @@ -80,6 +80,7 @@ class Rk3588VideoLink { std::thread recv_thread; int drm_fd = -1; + bool owns_fd = true; uint32_t connector_id = 0; uint32_t crtc_id = 0; int crtc_index = -1; From 131acb8e0b9fa302efdd7db2ba22a2483e534f50 Mon Sep 17 00:00:00 2001 From: Raphael <68374617+raphaelscholle@users.noreply.github.com> Date: Sat, 24 Jan 2026 22:27:03 +0100 Subject: [PATCH 04/21] testing --- QOpenHD.pro | 2 +- app/videostreaming/avcodec/drm_kms/rk3588_video_link.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/QOpenHD.pro b/QOpenHD.pro index 04da21c11..a7b0b81a7 100755 --- a/QOpenHD.pro +++ b/QOpenHD.pro @@ -103,7 +103,7 @@ QT +=core quick qml gui \ widgets QT += opengl QT += charts -#QT += gui-private +QT += gui-private #LIBS += Ldrm INCLUDEPATH += $$PWD/lib diff --git a/app/videostreaming/avcodec/drm_kms/rk3588_video_link.cpp b/app/videostreaming/avcodec/drm_kms/rk3588_video_link.cpp index 0e07bdaed..754e11fcf 100644 --- a/app/videostreaming/avcodec/drm_kms/rk3588_video_link.cpp +++ b/app/videostreaming/avcodec/drm_kms/rk3588_video_link.cpp @@ -18,7 +18,7 @@ #include #include -#include +#include #include #include "util/qrenderstats.h" From 49bae74724cf754e3ff2c59ff1ef244b33d37844 Mon Sep 17 00:00:00 2001 From: Raphael <68374617+raphaelscholle@users.noreply.github.com> Date: Sat, 24 Jan 2026 22:34:41 +0100 Subject: [PATCH 05/21] Update rk3588_video_link.cpp --- .../avcodec/drm_kms/rk3588_video_link.cpp | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/app/videostreaming/avcodec/drm_kms/rk3588_video_link.cpp b/app/videostreaming/avcodec/drm_kms/rk3588_video_link.cpp index 754e11fcf..d98d90377 100644 --- a/app/videostreaming/avcodec/drm_kms/rk3588_video_link.cpp +++ b/app/videostreaming/avcodec/drm_kms/rk3588_video_link.cpp @@ -247,6 +247,7 @@ bool Rk3588VideoLink::pick_overlay_plane(uint32_t fourcc, uint64_t modifier) { return false; } uint32_t chosen = 0; + uint32_t chosen_in_use = 0; for (uint32_t i = 0; i < planes->count_planes; ++i) { drmModePlane* plane = drmModeGetPlane(drm_fd, planes->planes[i]); if (!plane) { @@ -260,17 +261,19 @@ bool Rk3588VideoLink::pick_overlay_plane(uint32_t fourcc, uint64_t modifier) { drmModeFreePlane(plane); continue; } - if (plane->crtc_id == 0) { - chosen = plane->plane_id; - drmModeFreePlane(plane); - break; - } - if (!chosen) { + if (plane->crtc_id == 0 && !chosen) { chosen = plane->plane_id; + } else if (plane->crtc_id != 0 && !chosen_in_use) { + chosen_in_use = plane->plane_id; } drmModeFreePlane(plane); } drmModeFreePlaneResources(planes); + if (!chosen && chosen_in_use) { + std::cerr << "Rk3588VideoLink: no free overlay plane, falling back to in-use plane " + << chosen_in_use << "\n"; + chosen = chosen_in_use; + } if (!chosen) { return false; } @@ -630,8 +633,21 @@ void Rk3588VideoLink::present_if_pending() { if (drmModeAtomicCommit(drm_fd, req, DRM_MODE_ATOMIC_NONBLOCK, nullptr) == 0) { current_fb_id = frame.fb_id; } else { - std::cerr << "Rk3588VideoLink: atomic commit failed: " << strerror(errno) - << " is_master=" << drmIsMaster(drm_fd) << "\n"; + const int saved_errno = errno; + std::cerr << "Rk3588VideoLink: atomic commit failed: " << strerror(saved_errno) + << " is_master=" << drmIsMaster(drm_fd) + << " plane=" << plane_id << " crtc=" << crtc_id << "\n"; + if (saved_errno == EBUSY) { + if (drmModeSetPlane(drm_fd, plane_id, crtc_id, frame.fb_id, 0, + 0, 0, display_width, display_height, + 0, 0, frame.meta.width << 16, frame.meta.height << 16) == 0) { + current_fb_id = frame.fb_id; + std::cerr << "Rk3588VideoLink: drmModeSetPlane fallback succeeded\n"; + } else { + std::cerr << "Rk3588VideoLink: drmModeSetPlane fallback failed: " + << strerror(errno) << "\n"; + } + } } drmModeAtomicFree(req); } From 1952e16141b2ae54d324f8ff9189cde5dd1d3e05 Mon Sep 17 00:00:00 2001 From: Raphael <68374617+raphaelscholle@users.noreply.github.com> Date: Sat, 24 Jan 2026 22:51:52 +0100 Subject: [PATCH 06/21] testing rl3588 decoder --- app/util/qrenderstats.cpp | 9 +++++++++ app/util/qrenderstats.h | 2 ++ .../avcodec/drm_kms/rk3588_video_link.cpp | 11 ++++++++++- .../avcodec/drm_kms/rk3588_video_link.h | 2 ++ 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/app/util/qrenderstats.cpp b/app/util/qrenderstats.cpp index 455c3cd77..9d9ce3aad 100644 --- a/app/util/qrenderstats.cpp +++ b/app/util/qrenderstats.cpp @@ -37,6 +37,15 @@ void QRenderStats::registerOnWindow(QQuickWindow *window) connect(window, &QQuickWindow::afterRenderPassRecording, this, &QRenderStats::m_QQuickWindow_afterRenderPassRecording, Qt::DirectConnection); #if defined(__linux__) Rk3588VideoLink::instance().ensure_started(); + if (!m_present_timer) { + m_present_timer = new QTimer(this); + m_present_timer->setTimerType(Qt::PreciseTimer); + m_present_timer->setInterval(8); // ~120Hz tick + connect(m_present_timer, &QTimer::timeout, this, []() { + Rk3588VideoLink::instance().present_if_pending(); + }); + m_present_timer->start(); + } #endif } diff --git a/app/util/qrenderstats.h b/app/util/qrenderstats.h index ee38b80fd..c7ed6db63 100644 --- a/app/util/qrenderstats.h +++ b/app/util/qrenderstats.h @@ -4,6 +4,7 @@ #include #include #include +#include #include "../common/TimeHelper.hpp" #include "util/lqutils_include.h" @@ -52,6 +53,7 @@ public slots: // looks like there is a glFLush() or somethin in QT. //Chronometer m_avg_rendering_time{}; Chronometer m_avg_renderpass_time{}; + QTimer* m_present_timer = nullptr; }; diff --git a/app/videostreaming/avcodec/drm_kms/rk3588_video_link.cpp b/app/videostreaming/avcodec/drm_kms/rk3588_video_link.cpp index d98d90377..06328d90f 100644 --- a/app/videostreaming/avcodec/drm_kms/rk3588_video_link.cpp +++ b/app/videostreaming/avcodec/drm_kms/rk3588_video_link.cpp @@ -613,6 +613,14 @@ void Rk3588VideoLink::present_if_pending() { frame = pending; has_pending = false; } + if (force_legacy_setplane.load(std::memory_order_relaxed)) { + if (drmModeSetPlane(drm_fd, plane_id, crtc_id, frame.fb_id, 0, + 0, 0, display_width, display_height, + 0, 0, frame.meta.width << 16, frame.meta.height << 16) == 0) { + current_fb_id = frame.fb_id; + } + return; + } drmModeAtomicReq* req = drmModeAtomicAlloc(); if (!req) { return; @@ -638,11 +646,12 @@ void Rk3588VideoLink::present_if_pending() { << " is_master=" << drmIsMaster(drm_fd) << " plane=" << plane_id << " crtc=" << crtc_id << "\n"; if (saved_errno == EBUSY) { + force_legacy_setplane.store(true, std::memory_order_relaxed); if (drmModeSetPlane(drm_fd, plane_id, crtc_id, frame.fb_id, 0, 0, 0, display_width, display_height, 0, 0, frame.meta.width << 16, frame.meta.height << 16) == 0) { current_fb_id = frame.fb_id; - std::cerr << "Rk3588VideoLink: drmModeSetPlane fallback succeeded\n"; + std::cerr << "Rk3588VideoLink: switching to drmModeSetPlane path\n"; } else { std::cerr << "Rk3588VideoLink: drmModeSetPlane fallback failed: " << strerror(errno) << "\n"; diff --git a/app/videostreaming/avcodec/drm_kms/rk3588_video_link.h b/app/videostreaming/avcodec/drm_kms/rk3588_video_link.h index 23520de42..9c595e2c4 100644 --- a/app/videostreaming/avcodec/drm_kms/rk3588_video_link.h +++ b/app/videostreaming/avcodec/drm_kms/rk3588_video_link.h @@ -111,6 +111,8 @@ class Rk3588VideoLink { std::atomic received_frames{0}; std::chrono::steady_clock::time_point fps_last_time{}; uint64_t fps_last_count = 0; + + std::atomic force_legacy_setplane{false}; }; #else From 385d8a2cbbec945ecd541e6417734259506e8d4a Mon Sep 17 00:00:00 2001 From: Raphael <68374617+raphaelscholle@users.noreply.github.com> Date: Sat, 24 Jan 2026 22:55:54 +0100 Subject: [PATCH 07/21] Update rk3588_video_link.cpp --- app/videostreaming/avcodec/drm_kms/rk3588_video_link.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/app/videostreaming/avcodec/drm_kms/rk3588_video_link.cpp b/app/videostreaming/avcodec/drm_kms/rk3588_video_link.cpp index 06328d90f..a263395d5 100644 --- a/app/videostreaming/avcodec/drm_kms/rk3588_video_link.cpp +++ b/app/videostreaming/avcodec/drm_kms/rk3588_video_link.cpp @@ -470,13 +470,10 @@ void Rk3588VideoLink::receiver_thread() { pending.meta = meta; has_pending = true; recv_count++; - if (recv_count % 120 == 0) { - std::cerr << "Rk3588VideoLink: received " << recv_count << " frames (" - << meta.width << "x" << meta.height << ")\n"; - } - } else { - std::cerr << "Rk3588VideoLink: import failed for frame " + if (recv_count % 120 != 0) { + std::cerr << "Rk3588VideoLink: import failed for frame " << meta.width << "x" << meta.height << "\n"; + } } close_fds(fds); } From 8c822361937ebc98b280bdf938e66db0c7cbf170 Mon Sep 17 00:00:00 2001 From: Raphael <68374617+raphaelscholle@users.noreply.github.com> Date: Sat, 24 Jan 2026 23:12:45 +0100 Subject: [PATCH 08/21] kms z pos --- app/util/qrenderstats.cpp | 1 + .../avcodec/drm_kms/rk3588_video_link.cpp | 43 ++++++++++++++++--- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/app/util/qrenderstats.cpp b/app/util/qrenderstats.cpp index 9d9ce3aad..8cf3b4f12 100644 --- a/app/util/qrenderstats.cpp +++ b/app/util/qrenderstats.cpp @@ -37,6 +37,7 @@ void QRenderStats::registerOnWindow(QQuickWindow *window) connect(window, &QQuickWindow::afterRenderPassRecording, this, &QRenderStats::m_QQuickWindow_afterRenderPassRecording, Qt::DirectConnection); #if defined(__linux__) Rk3588VideoLink::instance().ensure_started(); + set_external_video_fps_str("0 fps"); if (!m_present_timer) { m_present_timer = new QTimer(this); m_present_timer->setTimerType(Qt::PreciseTimer); diff --git a/app/videostreaming/avcodec/drm_kms/rk3588_video_link.cpp b/app/videostreaming/avcodec/drm_kms/rk3588_video_link.cpp index a263395d5..74c3d50c6 100644 --- a/app/videostreaming/avcodec/drm_kms/rk3588_video_link.cpp +++ b/app/videostreaming/avcodec/drm_kms/rk3588_video_link.cpp @@ -43,6 +43,28 @@ struct DmabufFrameInfo { uint64_t pts_ms; }; +static int64_t get_prop_value_by_name(int fd, uint32_t obj_id, uint32_t obj_type, const char* name) { + drmModeObjectProperties* props = drmModeObjectGetProperties(fd, obj_id, obj_type); + if (!props) { + return -1; + } + int64_t value = -1; + for (uint32_t i = 0; i < props->count_props; ++i) { + drmModePropertyRes* prop = drmModeGetProperty(fd, props->props[i]); + if (!prop) { + continue; + } + if (strcmp(prop->name, name) == 0) { + value = static_cast(props->prop_values[i]); + drmModeFreeProperty(prop); + break; + } + drmModeFreeProperty(prop); + } + drmModeFreeObjectProperties(props); + return value; +} + } // namespace Rk3588VideoLink& Rk3588VideoLink::instance() { @@ -248,6 +270,8 @@ bool Rk3588VideoLink::pick_overlay_plane(uint32_t fourcc, uint64_t modifier) { } uint32_t chosen = 0; uint32_t chosen_in_use = 0; + int64_t chosen_zpos = INT64_MAX; + int64_t chosen_in_use_zpos = INT64_MAX; for (uint32_t i = 0; i < planes->count_planes; ++i) { drmModePlane* plane = drmModeGetPlane(drm_fd, planes->planes[i]); if (!plane) { @@ -261,23 +285,32 @@ bool Rk3588VideoLink::pick_overlay_plane(uint32_t fourcc, uint64_t modifier) { drmModeFreePlane(plane); continue; } - if (plane->crtc_id == 0 && !chosen) { - chosen = plane->plane_id; - } else if (plane->crtc_id != 0 && !chosen_in_use) { - chosen_in_use = plane->plane_id; + const int64_t zpos_val = get_prop_value_by_name(drm_fd, plane->plane_id, DRM_MODE_OBJECT_PLANE, "zpos"); + if (plane->crtc_id == 0) { + if (!chosen || (zpos_val >= 0 && zpos_val < chosen_zpos)) { + chosen = plane->plane_id; + chosen_zpos = zpos_val; + } + } else { + if (!chosen_in_use || (zpos_val >= 0 && zpos_val < chosen_in_use_zpos)) { + chosen_in_use = plane->plane_id; + chosen_in_use_zpos = zpos_val; + } } drmModeFreePlane(plane); } drmModeFreePlaneResources(planes); if (!chosen && chosen_in_use) { std::cerr << "Rk3588VideoLink: no free overlay plane, falling back to in-use plane " - << chosen_in_use << "\n"; + << chosen_in_use << " zpos=" << chosen_in_use_zpos << "\n"; chosen = chosen_in_use; + chosen_zpos = chosen_in_use_zpos; } if (!chosen) { return false; } plane_id = chosen; + std::cerr << "Rk3588VideoLink: selected plane " << plane_id << " zpos=" << chosen_zpos << "\n"; return true; } From f2896206e8ffedf261423d4c2db11ccea26095c4 Mon Sep 17 00:00:00 2001 From: Raphael <68374617+raphaelscholle@users.noreply.github.com> Date: Sat, 24 Jan 2026 23:16:18 +0100 Subject: [PATCH 09/21] Update rk3588_video_link.cpp --- app/videostreaming/avcodec/drm_kms/rk3588_video_link.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/videostreaming/avcodec/drm_kms/rk3588_video_link.cpp b/app/videostreaming/avcodec/drm_kms/rk3588_video_link.cpp index 74c3d50c6..2207b6b67 100644 --- a/app/videostreaming/avcodec/drm_kms/rk3588_video_link.cpp +++ b/app/videostreaming/avcodec/drm_kms/rk3588_video_link.cpp @@ -209,6 +209,13 @@ bool Rk3588VideoLink::init_drm() { std::cerr << "Rk3588VideoLink: missing required plane properties\n"; return false; } + if (prop_zpos) { + if (drmModeObjectSetProperty(drm_fd, plane_id, DRM_MODE_OBJECT_PLANE, prop_zpos, 0) == 0) { + std::cerr << "Rk3588VideoLink: set plane zpos=0\n"; + } else { + std::cerr << "Rk3588VideoLink: failed to set plane zpos=0: " << strerror(errno) << "\n"; + } + } std::cerr << "Rk3588VideoLink: using connector " << connector_id << " crtc " << crtc_id << " plane " << plane_id << " display " << display_width << "x" << display_height << "\n"; return true; From 2a6bc9e42c47e394e93346928acc652575833e92 Mon Sep 17 00:00:00 2001 From: Raphael <68374617+raphaelscholle@users.noreply.github.com> Date: Sat, 24 Jan 2026 23:53:18 +0100 Subject: [PATCH 10/21] improve --- app/main.cpp | 10 + app/util/qopenhd.cpp | 305 ++++++++++++++++++ app/util/qopenhd.h | 4 + app/util/qrenderstats.cpp | 24 +- app/util/qrenderstats.h | 4 + .../avcodec/drm_kms/rk3588_video_link.cpp | 29 +- .../avcodec/drm_kms/rk3588_video_link.h | 6 +- qml/main.qml | 13 - .../AppScreenSettingsView.qml | 38 +++ qml/ui/widgets/QRenderStatsWidget.qml | 94 ++++++ 10 files changed, 494 insertions(+), 33 deletions(-) diff --git a/app/main.cpp b/app/main.cpp index 4b4a1bd1a..37710ce87 100755 --- a/app/main.cpp +++ b/app/main.cpp @@ -330,6 +330,16 @@ int main(int argc, char *argv[]) { QApplication app(argc, argv); + if (QOpenHD::instance().is_platform_rock()) { + const QString mode_override = settings.value("screen_mode_override", "").toString(); + if (!mode_override.isEmpty()) { + const QString current_mode = QOpenHD::instance().get_screen_mode_current(); + if (current_mode != mode_override) { + QOpenHD::instance().set_screen_mode(mode_override); + } + } + } + // Load translation based on saved locale (requires an active Q(Core)Application on Qt5) QString localeStr = settings.value("locale", "en").toString(); QOpenHD::instance().installTranslatorForLanguage(localeStr, &settings); diff --git a/app/util/qopenhd.cpp b/app/util/qopenhd.cpp index 9d7c7a0c7..01938dc12 100644 --- a/app/util/qopenhd.cpp +++ b/app/util/qopenhd.cpp @@ -10,14 +10,27 @@ #include #include +#include +#include #include #include #include +#include +#include +#include #if defined(__linux__) || defined(__macos__) #include "common/openhd-util.hpp" #endif +#if defined(__linux__) +#include +#include +#include +#include +#include +#endif + #if defined(ENABLE_SPEECH) #include #include @@ -32,6 +45,187 @@ #include "mousehelper.h" +#if defined(__linux__) +namespace { +struct DrmContext { + int fd = -1; + bool owns_fd = false; + uint32_t crtc_id = 0; + uint32_t connector_id = 0; + drmModeConnector* connector = nullptr; + drmModeCrtc* crtc = nullptr; +}; + +static double calc_vrefresh(const drmModeModeInfo& mode) { + if (mode.htotal == 0 || mode.vtotal == 0) { + return 0.0; + } + double refresh = (mode.clock * 1000.0) / (static_cast(mode.htotal) * static_cast(mode.vtotal)); + if (mode.vscan > 1) { + refresh /= mode.vscan; + } + if (mode.flags & DRM_MODE_FLAG_INTERLACE) { + refresh *= 2.0; + } + return refresh; +} + +static QString mode_to_string(const drmModeModeInfo& mode) { + const double refresh = calc_vrefresh(mode); + const int refresh_int = static_cast(std::round(refresh)); + return QString("%1x%2@%3").arg(mode.hdisplay).arg(mode.vdisplay).arg(refresh_int); +} + +static bool parse_mode_string(const QString& mode_str, int& width, int& height, int& refresh) { + const QStringList parts = mode_str.split("@"); + if (parts.isEmpty()) { + return false; + } + const QStringList size_parts = parts[0].split("x"); + if (size_parts.size() != 2) { + return false; + } + bool ok_w = false; + bool ok_h = false; + width = size_parts[0].toInt(&ok_w); + height = size_parts[1].toInt(&ok_h); + if (!ok_w || !ok_h) { + return false; + } + refresh = 0; + if (parts.size() > 1) { + bool ok_r = false; + refresh = parts[1].toInt(&ok_r); + if (!ok_r) { + refresh = 0; + } + } + return true; +} + +static void release_drm_context(DrmContext& ctx) { + if (ctx.connector) { + drmModeFreeConnector(ctx.connector); + ctx.connector = nullptr; + } + if (ctx.crtc) { + drmModeFreeCrtc(ctx.crtc); + ctx.crtc = nullptr; + } + if (ctx.owns_fd && ctx.fd >= 0) { + close(ctx.fd); + ctx.fd = -1; + } +} + +static bool get_drm_context(DrmContext& ctx) { + if (QGuiApplication::platformName().contains("eglfs", Qt::CaseInsensitive)) { + auto* pni = QGuiApplication::platformNativeInterface(); + if (pni) { + void* fd_ptr = pni->nativeResourceForIntegration("dri_fd"); + if (fd_ptr) { + ctx.fd = static_cast(reinterpret_cast(fd_ptr)); + } + QScreen* screen = QGuiApplication::primaryScreen(); + if (screen) { + void* crtc_ptr = pni->nativeResourceForScreen("dri_crtcid", screen); + void* conn_ptr = pni->nativeResourceForScreen("dri_connectorid", screen); + if (crtc_ptr) { + ctx.crtc_id = static_cast(reinterpret_cast(crtc_ptr)); + } + if (conn_ptr) { + ctx.connector_id = static_cast(reinterpret_cast(conn_ptr)); + } + } + } + } + + if (ctx.fd < 0) { + ctx.fd = open("/dev/dri/card0", O_RDWR | O_CLOEXEC); + if (ctx.fd < 0) { + qWarning() << "get_screen_modes: failed to open /dev/dri/card0"; + return false; + } + ctx.owns_fd = true; + } + + drmModeRes* res = drmModeGetResources(ctx.fd); + if (!res) { + qWarning() << "get_screen_modes: drmModeGetResources failed"; + release_drm_context(ctx); + return false; + } + + if (ctx.connector_id != 0) { + ctx.connector = drmModeGetConnector(ctx.fd, ctx.connector_id); + if (!ctx.connector || ctx.connector->connection != DRM_MODE_CONNECTED) { + if (ctx.connector) { + drmModeFreeConnector(ctx.connector); + ctx.connector = nullptr; + } + ctx.connector_id = 0; + } + } + + if (ctx.connector_id == 0) { + for (int i = 0; i < res->count_connectors; ++i) { + drmModeConnector* conn = drmModeGetConnector(ctx.fd, res->connectors[i]); + if (!conn) { + continue; + } + if (conn->connection == DRM_MODE_CONNECTED && conn->count_modes > 0) { + ctx.connector = conn; + ctx.connector_id = conn->connector_id; + break; + } + drmModeFreeConnector(conn); + } + } + + if (ctx.connector && ctx.crtc_id == 0) { + for (int i = 0; i < ctx.connector->count_encoders; ++i) { + drmModeEncoder* enc = drmModeGetEncoder(ctx.fd, ctx.connector->encoders[i]); + if (!enc) { + continue; + } + if (enc->crtc_id) { + ctx.crtc_id = enc->crtc_id; + drmModeFreeEncoder(enc); + break; + } + drmModeFreeEncoder(enc); + } + } + + drmModeFreeResources(res); + + if (ctx.connector_id == 0 || ctx.crtc_id == 0) { + qWarning() << "get_screen_modes: missing connector/crtc"; + release_drm_context(ctx); + return false; + } + + if (!ctx.connector) { + ctx.connector = drmModeGetConnector(ctx.fd, ctx.connector_id); + } + if (!ctx.connector) { + qWarning() << "get_screen_modes: drmModeGetConnector failed"; + release_drm_context(ctx); + return false; + } + + ctx.crtc = drmModeGetCrtc(ctx.fd, ctx.crtc_id); + if (!ctx.crtc) { + qWarning() << "get_screen_modes: drmModeGetCrtc failed"; + release_drm_context(ctx); + return false; + } + + return true; +} +} // namespace +#endif + QOpenHD &QOpenHD::instance() { static QOpenHD instance=QOpenHD(); @@ -405,6 +599,117 @@ bool QOpenHD::is_platform_nxp() #endif } +QStringList QOpenHD::get_screen_modes() +{ +#if defined(__linux__) + if (!is_platform_rock()) { + return {}; + } + DrmContext ctx; + if (!get_drm_context(ctx)) { + return {}; + } + QStringList modes; + for (int i = 0; i < ctx.connector->count_modes; ++i) { + modes.push_back(mode_to_string(ctx.connector->modes[i])); + } + modes.removeDuplicates(); + release_drm_context(ctx); + return modes; +#else + return {}; +#endif +} + +QString QOpenHD::get_screen_mode_current() +{ +#if defined(__linux__) + if (!is_platform_rock()) { + return QString("NA"); + } + DrmContext ctx; + if (!get_drm_context(ctx)) { + return QString("NA"); + } + QString mode = QString("NA"); + if (ctx.crtc && ctx.crtc->mode_valid) { + mode = mode_to_string(ctx.crtc->mode); + } else if (ctx.connector && ctx.connector->count_modes > 0) { + mode = mode_to_string(ctx.connector->modes[0]); + } + release_drm_context(ctx); + return mode; +#else + return QString("NA"); +#endif +} + +bool QOpenHD::set_screen_mode(QString mode) +{ +#if defined(__linux__) + if (!is_platform_rock()) { + return false; + } + int width = 0; + int height = 0; + int refresh = 0; + if (!parse_mode_string(mode, width, height, refresh)) { + qWarning() << "set_screen_mode: invalid mode string" << mode; + return false; + } + DrmContext ctx; + if (!get_drm_context(ctx)) { + return false; + } + if (!ctx.crtc || !ctx.connector) { + release_drm_context(ctx); + return false; + } + const drmModeModeInfo* chosen = nullptr; + for (int i = 0; i < ctx.connector->count_modes; ++i) { + const drmModeModeInfo& m = ctx.connector->modes[i]; + if (m.hdisplay != width || m.vdisplay != height) { + continue; + } + const int refresh_int = static_cast(std::round(calc_vrefresh(m))); + if (refresh == 0 || refresh_int == refresh) { + chosen = &m; + break; + } + } + if (!chosen) { + qWarning() << "set_screen_mode: mode not found" << mode; + release_drm_context(ctx); + return false; + } + if (ctx.crtc->buffer_id == 0) { + qWarning() << "set_screen_mode: current CRTC has no FB"; + release_drm_context(ctx); + return false; + } + const int ret = drmModeSetCrtc(ctx.fd, + ctx.crtc_id, + ctx.crtc->buffer_id, + 0, + 0, + &ctx.connector_id, + 1, + chosen); + if (ret != 0) { + qWarning() << "set_screen_mode: drmModeSetCrtc failed" << strerror(errno); + release_drm_context(ctx); + return false; + } + QSettings settings; + settings.setValue("screen_mode_override", mode); + release_drm_context(ctx); + return true; +#else + Q_UNUSED(mode); + return false; +#endif +} + void QOpenHD::keep_screen_on(bool on) { #if defined(__android__) diff --git a/app/util/qopenhd.h b/app/util/qopenhd.h index e38748d2e..93f0cf8c7 100644 --- a/app/util/qopenhd.h +++ b/app/util/qopenhd.h @@ -7,6 +7,7 @@ #include #include #include +#include #ifdef ENABLE_SPEECH #include @@ -70,6 +71,9 @@ class QOpenHD : public QObject Q_INVOKABLE bool is_platform_rpi(); Q_INVOKABLE bool is_platform_rock(); Q_INVOKABLE bool is_platform_nxp(); + Q_INVOKABLE QStringList get_screen_modes(); + Q_INVOKABLE QString get_screen_mode_current(); + Q_INVOKABLE bool set_screen_mode(QString mode); // // Tries to mimic android toast as much as possible // diff --git a/app/util/qrenderstats.cpp b/app/util/qrenderstats.cpp index 8cf3b4f12..40eb36f00 100644 --- a/app/util/qrenderstats.cpp +++ b/app/util/qrenderstats.cpp @@ -41,9 +41,19 @@ void QRenderStats::registerOnWindow(QQuickWindow *window) if (!m_present_timer) { m_present_timer = new QTimer(this); m_present_timer->setTimerType(Qt::PreciseTimer); - m_present_timer->setInterval(8); // ~120Hz tick - connect(m_present_timer, &QTimer::timeout, this, []() { - Rk3588VideoLink::instance().present_if_pending(); + m_present_timer->setInterval(250); + connect(m_present_timer, &QTimer::timeout, this, [this]() { + const auto now = std::chrono::steady_clock::now(); + const auto elapsed = std::chrono::duration_cast(now - m_last_external_ts); + if (elapsed.count() <= 0) { + return; + } + const uint64_t total = Rk3588VideoLink::instance().get_received_frames(); + const uint64_t delta = total - m_last_external_frames; + const double fps = (static_cast(delta) * 1000.0) / static_cast(elapsed.count()); + m_last_external_frames = total; + m_last_external_ts = now; + set_external_video_fps_str(QString("%1 fps").arg(fps, 0, 'f', 0)); }); m_present_timer->start(); } @@ -67,9 +77,6 @@ void QRenderStats::set_display_width_height(int width, int height) void QRenderStats::m_QQuickWindow_beforeRendering() { //m_avg_rendering_time.start(); -#if defined(__linux__) - Rk3588VideoLink::instance().present_if_pending(); -#endif } void QRenderStats::m_QQuickWindow_afterRendering() @@ -97,6 +104,11 @@ void QRenderStats::m_QQuickWindow_beforeRenderPassRecording() const auto main_stats=QString(self.getAvgReadable().c_str()); //qDebug()<<"QRenderStats main frame time:"< 0) { + const double fps = 1000000000.0 / static_cast(avg_ns); + set_screen_fps_str(QString("%1 fps").arg(fps, 0, 'f', 0)); + } }); } diff --git a/app/util/qrenderstats.h b/app/util/qrenderstats.h index c7ed6db63..2bf3dd807 100644 --- a/app/util/qrenderstats.h +++ b/app/util/qrenderstats.h @@ -20,6 +20,8 @@ class QRenderStats : public QObject // Resolution of the screen / display itself L_RO_PROP(QString, display_width_height_str, set_display_width_height_str, "NA") L_RO_PROP(QString, screen_width_height_str, set_screen_width_height_str, "NA") + // Screen FPS derived from Qt render pass tick + L_RO_PROP(QString, screen_fps_str, set_screen_fps_str, "NA") // Resolution qopenhd is rendering at L_RW_PROP(int, window_width, set_window_width, -1) L_RW_PROP(int, window_height, set_window_height, -1) @@ -54,6 +56,8 @@ public slots: //Chronometer m_avg_rendering_time{}; Chronometer m_avg_renderpass_time{}; QTimer* m_present_timer = nullptr; + uint64_t m_last_external_frames = 0; + std::chrono::steady_clock::time_point m_last_external_ts = std::chrono::steady_clock::now(); }; diff --git a/app/videostreaming/avcodec/drm_kms/rk3588_video_link.cpp b/app/videostreaming/avcodec/drm_kms/rk3588_video_link.cpp index 2207b6b67..6aca84bab 100644 --- a/app/videostreaming/avcodec/drm_kms/rk3588_video_link.cpp +++ b/app/videostreaming/avcodec/drm_kms/rk3588_video_link.cpp @@ -77,6 +77,9 @@ Rk3588VideoLink::~Rk3588VideoLink() { if (recv_thread.joinable()) { recv_thread.join(); } + if (present_thread.joinable()) { + present_thread.join(); + } cleanup_cache(); if (drm_fd >= 0 && owns_fd) { close(drm_fd); @@ -119,9 +122,9 @@ void Rk3588VideoLink::ensure_started() { if (!init_drm()) { return; } - fps_last_time = std::chrono::steady_clock::now(); - fps_last_count = 0; recv_thread = std::thread(&Rk3588VideoLink::receiver_thread, this); + present_thread_running.store(true, std::memory_order_relaxed); + present_thread = std::thread(&Rk3588VideoLink::present_loop, this); } bool Rk3588VideoLink::init_drm() { @@ -631,16 +634,6 @@ void Rk3588VideoLink::present_if_pending() { if (!started) { return; } - auto now = std::chrono::steady_clock::now(); - auto elapsed = std::chrono::duration_cast(now - fps_last_time); - if (elapsed.count() >= 1000) { - const uint64_t total = received_frames.load(std::memory_order_relaxed); - const uint64_t delta = total - fps_last_count; - fps_last_count = total; - fps_last_time = now; - const QString fps = QString("%1 fps").arg(static_cast(delta)); - QRenderStats::instance().set_external_video_fps_str(fps); - } PendingFrame frame{}; { std::lock_guard lock(pending_mutex); @@ -698,4 +691,16 @@ void Rk3588VideoLink::present_if_pending() { drmModeAtomicFree(req); } +uint64_t Rk3588VideoLink::get_received_frames() const { + return received_frames.load(std::memory_order_relaxed); +} + +void Rk3588VideoLink::present_loop() { + while (!shutdown_requested.load(std::memory_order_relaxed)) { + present_if_pending(); + std::this_thread::sleep_for(std::chrono::milliseconds(8)); + } + present_thread_running.store(false, std::memory_order_relaxed); +} + #endif diff --git a/app/videostreaming/avcodec/drm_kms/rk3588_video_link.h b/app/videostreaming/avcodec/drm_kms/rk3588_video_link.h index 9c595e2c4..e0f5547a8 100644 --- a/app/videostreaming/avcodec/drm_kms/rk3588_video_link.h +++ b/app/videostreaming/avcodec/drm_kms/rk3588_video_link.h @@ -15,6 +15,7 @@ class Rk3588VideoLink { static Rk3588VideoLink& instance(); void ensure_started(); void present_if_pending(); + uint64_t get_received_frames() const; private: Rk3588VideoLink() = default; @@ -74,10 +75,13 @@ class Rk3588VideoLink { void destroy_fb_entry(FbEntry& entry); void cleanup_cache(); uint32_t get_prop_id(uint32_t obj_id, uint32_t obj_type, const char* name); + void present_loop(); std::atomic started{false}; std::atomic shutdown_requested{false}; std::thread recv_thread; + std::thread present_thread; + std::atomic present_thread_running{false}; int drm_fd = -1; bool owns_fd = true; @@ -109,8 +113,6 @@ class Rk3588VideoLink { std::unordered_map fb_cache; std::atomic received_frames{0}; - std::chrono::steady_clock::time_point fps_last_time{}; - uint64_t fps_last_count = 0; std::atomic force_legacy_setplane{false}; }; diff --git a/qml/main.qml b/qml/main.qml index b578c75d7..1975aad8e 100755 --- a/qml/main.qml +++ b/qml/main.qml @@ -106,19 +106,6 @@ ApplicationWindow { layer.enabled: false } - Text { - id: externalVideoFps - text: _qrenderstats.external_video_fps_str - visible: _qrenderstats.external_video_fps_str !== "NA" - color: "white" - font.pixelSize: 16 - anchors.top: parent.top - anchors.right: parent.right - anchors.topMargin: 6 - anchors.rightMargin: 10 - z: 4.0 - } - ConfigPopup { id: settings_panel visible: false diff --git a/qml/ui/configpopup/qopenhd_settings/AppScreenSettingsView.qml b/qml/ui/configpopup/qopenhd_settings/AppScreenSettingsView.qml index c494ddb12..4c5677374 100755 --- a/qml/ui/configpopup/qopenhd_settings/AppScreenSettingsView.qml +++ b/qml/ui/configpopup/qopenhd_settings/AppScreenSettingsView.qml @@ -18,6 +18,8 @@ ScrollView { contentHeight: screenColumn.height clip: true + property var screen_modes: _qopenhd.get_screen_modes() + property string screen_mode_current: _qopenhd.get_screen_mode_current() Item { anchors.fill: parent @@ -129,6 +131,42 @@ ScrollView { m_description: "Advanced settings" m_hide_elements: true + SettingBaseElement{ + visible: _qopenhd.is_platform_rock() + m_short_description: "Screen mode" + + ComboBox { + id: screenModeCombo + height: elementHeight + anchors.right: parent.right + anchors.rightMargin: Qt.inputMethod.visible ? 78 : 18 + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizonatalCenter + width: 320 + model: appScreenSettingsView.screen_modes + enabled: model && model.length > 0 + Component.onCompleted: { + for (var i = 0; i < model.length; i++) { + if (model[i] === appScreenSettingsView.screen_mode_current) { + currentIndex = i; + break; + } + } + } + onActivated: { + const mode = model[currentIndex] + if (mode === appScreenSettingsView.screen_mode_current) { + return; + } + if (_qopenhd.set_screen_mode(mode)) { + appScreenSettingsView.screen_mode_current = mode + _restartqopenhdmessagebox.show_with_text("Screen mode changed. Restart QOpenHD if the UI looks wrong.") + } else { + _qopenhd.show_toast("Failed to set screen mode " + mode, true) + } + } + } + } SettingBaseElement{ m_short_description: "Screen rotation" // anything other than 0 and 180 can breaks things diff --git a/qml/ui/widgets/QRenderStatsWidget.qml b/qml/ui/widgets/QRenderStatsWidget.qml index 19c5bda1e..b2f05da5d 100644 --- a/qml/ui/widgets/QRenderStatsWidget.qml +++ b/qml/ui/widgets/QRenderStatsWidget.qml @@ -54,6 +54,7 @@ BaseWidget { ColumnLayout{ width: 400 + visible: !_qopenhd.is_platform_rock() // QT main thread render time (E.g. OpenGL frame time), independent of decoding Item { width: parent.width @@ -366,6 +367,99 @@ BaseWidget { } } } + + ColumnLayout{ + width: 400 + visible: _qopenhd.is_platform_rock() + Item { + width: parent.width + height: 32 + Text { + text: qsTr("Screen:") + color: "white" + font.bold: true + height: parent.height + font.pixelSize: detailPanelFontPixels + anchors.left: parent.left + verticalAlignment: Text.AlignVCenter + } + Text { + text: _qrenderstats.screen_width_height_str + color: "white"; + font.bold: true; + height: parent.height + font.pixelSize: detailPanelFontPixels; + anchors.right: parent.right + verticalAlignment: Text.AlignVCenter + } + } + Item { + width: parent.width + height: 32 + Text { + text: qsTr("Display:") + color: "white" + font.bold: true + height: parent.height + font.pixelSize: detailPanelFontPixels + anchors.left: parent.left + verticalAlignment: Text.AlignVCenter + } + Text { + text: _qrenderstats.display_width_height_str + color: "white"; + font.bold: true; + height: parent.height + font.pixelSize: detailPanelFontPixels; + anchors.right: parent.right + verticalAlignment: Text.AlignVCenter + } + } + Item { + width: parent.width + height: 32 + Text { + text: qsTr("Screen FPS:") + color: "white" + font.bold: true + height: parent.height + font.pixelSize: detailPanelFontPixels + anchors.left: parent.left + verticalAlignment: Text.AlignVCenter + } + Text { + text: _qrenderstats.screen_fps_str + color: "white"; + font.bold: true; + height: parent.height + font.pixelSize: detailPanelFontPixels; + anchors.right: parent.right + verticalAlignment: Text.AlignVCenter + } + } + Item { + width: parent.width + height: 32 + Text { + text: qsTr("Video FPS:") + color: "white" + font.bold: true + height: parent.height + font.pixelSize: detailPanelFontPixels + anchors.left: parent.left + verticalAlignment: Text.AlignVCenter + } + Text { + text: _qrenderstats.external_video_fps_str + color: "white"; + font.bold: true; + height: parent.height + font.pixelSize: detailPanelFontPixels; + anchors.right: parent.right + verticalAlignment: Text.AlignVCenter + } + } + } } Item { From 5d2b0241d4002e1db27ea74c6c6aca0a0dff75fd Mon Sep 17 00:00:00 2001 From: Raphael <68374617+raphaelscholle@users.noreply.github.com> Date: Sat, 24 Jan 2026 23:55:59 +0100 Subject: [PATCH 11/21] Update qopenhd.cpp --- app/util/qopenhd.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/util/qopenhd.cpp b/app/util/qopenhd.cpp index 01938dc12..a6b9496c4 100644 --- a/app/util/qopenhd.cpp +++ b/app/util/qopenhd.cpp @@ -687,6 +687,7 @@ bool QOpenHD::set_screen_mode(QString mode) release_drm_context(ctx); return false; } + drmModeModeInfo chosen_mode = *chosen; const int ret = drmModeSetCrtc(ctx.fd, ctx.crtc_id, ctx.crtc->buffer_id, @@ -694,7 +695,7 @@ bool QOpenHD::set_screen_mode(QString mode) 0, &ctx.connector_id, 1, - chosen); + &chosen_mode); if (ret != 0) { qWarning() << "set_screen_mode: drmModeSetCrtc failed" << strerror(errno); release_drm_context(ctx); From 3a21ad88a674454105f7ca26c1a8e3365dc0d22d Mon Sep 17 00:00:00 2001 From: Raphael <68374617+raphaelscholle@users.noreply.github.com> Date: Sun, 25 Jan 2026 01:00:47 +0100 Subject: [PATCH 12/21] testing --- QOpenHD.pro | 1 - qml/main.qml | 6 ++++++ .../qopenhd_settings/AppScreenSettingsView.qml | 10 ++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/QOpenHD.pro b/QOpenHD.pro index a7b0b81a7..465abbd35 100755 --- a/QOpenHD.pro +++ b/QOpenHD.pro @@ -63,7 +63,6 @@ CONFIG(debug, debug|release) { DEFINES += QT_NO_DEBUG CONFIG += installer DESTDIR = $${OUT_PWD}/release - DEFINES += QMLJSDEBUGGER } #https://doc.qt.io/qt-6/portingguide.html diff --git a/qml/main.qml b/qml/main.qml index 1975aad8e..857bd11d4 100755 --- a/qml/main.qml +++ b/qml/main.qml @@ -79,6 +79,12 @@ ApplicationWindow { if(QOPENHD_ENABLE_VIDEO_VIA_ANDROID){ return "../video/ExpMainVideoAndroid.qml" } + if (_qopenhd.is_platform_rock() && settings.dev_always_use_generic_external_decode_service) { + return "" + } + if (settings.dev_disable_primary_video) { + return "" + } // If we have avcodec at compile time, we prefer it over qmlglsink since it provides lower latency // (not really avcodec itself, but in this namespace we have 1) the preferred sw decode path and // 2) also the mmal rpi path ) diff --git a/qml/ui/configpopup/qopenhd_settings/AppScreenSettingsView.qml b/qml/ui/configpopup/qopenhd_settings/AppScreenSettingsView.qml index 4c5677374..9fdd9957d 100755 --- a/qml/ui/configpopup/qopenhd_settings/AppScreenSettingsView.qml +++ b/qml/ui/configpopup/qopenhd_settings/AppScreenSettingsView.qml @@ -20,6 +20,15 @@ ScrollView { clip: true property var screen_modes: _qopenhd.get_screen_modes() property string screen_mode_current: _qopenhd.get_screen_mode_current() + function refresh_modes() { + screen_modes = _qopenhd.get_screen_modes() + screen_mode_current = _qopenhd.get_screen_mode_current() + } + onVisibleChanged: { + if (visible) { + refresh_modes() + } + } Item { anchors.fill: parent @@ -146,6 +155,7 @@ ScrollView { model: appScreenSettingsView.screen_modes enabled: model && model.length > 0 Component.onCompleted: { + appScreenSettingsView.refresh_modes() for (var i = 0; i < model.length; i++) { if (model[i] === appScreenSettingsView.screen_mode_current) { currentIndex = i; From eb3087936ce54bb0edb0ba6ade7c9e508d7852e2 Mon Sep 17 00:00:00 2001 From: Raphael <68374617+raphaelscholle@users.noreply.github.com> Date: Sun, 25 Jan 2026 01:39:47 +0100 Subject: [PATCH 13/21] fps cap test --- app/util/qopenhd.cpp | 99 ++++++++++++++++++- app/util/qopenhd.h | 1 + app/util/qrenderstats.cpp | 31 +++++- app/util/qrenderstats.h | 5 + .../AppScreenSettingsView.qml | 35 +++++++ qml/ui/elements/AppSettings.qml | 1 + 6 files changed, 167 insertions(+), 5 deletions(-) diff --git a/app/util/qopenhd.cpp b/app/util/qopenhd.cpp index a6b9496c4..89df2943e 100644 --- a/app/util/qopenhd.cpp +++ b/app/util/qopenhd.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #if defined(__linux__) || defined(__macos__) #include "common/openhd-util.hpp" @@ -141,7 +142,7 @@ static bool get_drm_context(DrmContext& ctx) { } if (ctx.fd < 0) { - ctx.fd = open("/dev/dri/card0", O_RDWR | O_CLOEXEC); + ctx.fd = open("/dev/dri/card0", O_RDONLY | O_CLOEXEC); if (ctx.fd < 0) { qWarning() << "get_screen_modes: failed to open /dev/dri/card0"; return false; @@ -151,9 +152,25 @@ static bool get_drm_context(DrmContext& ctx) { drmModeRes* res = drmModeGetResources(ctx.fd); if (!res) { - qWarning() << "get_screen_modes: drmModeGetResources failed"; - release_drm_context(ctx); - return false; + if (!ctx.owns_fd) { + // Retry with a fresh /dev/dri/card0 fd if EGLFS fd doesn't work. + int retry_fd = open("/dev/dri/card0", O_RDONLY | O_CLOEXEC); + if (retry_fd >= 0) { + drmModeRes* retry_res = drmModeGetResources(retry_fd); + if (retry_res) { + ctx.fd = retry_fd; + ctx.owns_fd = true; + res = retry_res; + } else { + close(retry_fd); + } + } + } + if (!res) { + qWarning() << "get_screen_modes: drmModeGetResources failed"; + release_drm_context(ctx); + return false; + } } if (ctx.connector_id != 0) { @@ -223,6 +240,43 @@ static bool get_drm_context(DrmContext& ctx) { return true; } + +static QStringList get_modes_from_modetest() { + const QStringList candidates = { + "/usr/bin/modetest", + "/usr/local/bin/modetest", + "modetest" + }; + QProcess proc; + for (const QString& path : candidates) { + proc.start(path, {"-M", "rockchip", "-c"}); + if (!proc.waitForStarted(1000)) { + continue; + } + if (!proc.waitForFinished(2000)) { + proc.kill(); + proc.waitForFinished(); + continue; + } + break; + } + const QString output = QString::fromUtf8(proc.readAllStandardOutput()); + const QStringList lines = output.split('\n'); + QRegularExpression re("^\\s*#?\\d+\\s+(\\d+x\\d+)\\s+([0-9.]+)"); + QStringList modes; + for (const QString& line : lines) { + const QRegularExpressionMatch m = re.match(line); + if (!m.hasMatch()) { + continue; + } + const QString size = m.captured(1); + const double refresh = m.captured(2).toDouble(); + const int refresh_int = static_cast(std::round(refresh)); + modes.push_back(QString("%1@%2").arg(size).arg(refresh_int)); + } + modes.removeDuplicates(); + return modes; +} } // namespace #endif @@ -607,6 +661,14 @@ QStringList QOpenHD::get_screen_modes() } DrmContext ctx; if (!get_drm_context(ctx)) { + const auto fallback = get_modes_from_modetest(); + if (!fallback.isEmpty()) { + return fallback; + } + const QString current = get_screen_mode_current(); + if (current != "NA") { + return {current}; + } return {}; } QStringList modes; @@ -615,6 +677,12 @@ QStringList QOpenHD::get_screen_modes() } modes.removeDuplicates(); release_drm_context(ctx); + if (modes.isEmpty()) { + const auto fallback = get_modes_from_modetest(); + if (!fallback.isEmpty()) { + return fallback; + } + } return modes; #else return {}; @@ -629,6 +697,10 @@ QString QOpenHD::get_screen_mode_current() } DrmContext ctx; if (!get_drm_context(ctx)) { + const auto fallback = get_modes_from_modetest(); + if (!fallback.isEmpty()) { + return fallback.first(); + } return QString("NA"); } QString mode = QString("NA"); @@ -638,6 +710,12 @@ QString QOpenHD::get_screen_mode_current() mode = mode_to_string(ctx.connector->modes[0]); } release_drm_context(ctx); + if (mode == "NA") { + const auto fallback = get_modes_from_modetest(); + if (!fallback.isEmpty()) { + return fallback.first(); + } + } return mode; #else return QString("NA"); @@ -711,6 +789,19 @@ bool QOpenHD::set_screen_mode(QString mode) #endif } +int QOpenHD::get_ui_fps_cap() +{ +#if defined(__linux__) + if (!is_platform_rock()) { + return 0; + } + QSettings settings; + return settings.value("ui_fps_cap", 30).toInt(); +#else + return 0; +#endif +} + void QOpenHD::keep_screen_on(bool on) { #if defined(__android__) diff --git a/app/util/qopenhd.h b/app/util/qopenhd.h index 93f0cf8c7..c8b278f27 100644 --- a/app/util/qopenhd.h +++ b/app/util/qopenhd.h @@ -74,6 +74,7 @@ class QOpenHD : public QObject Q_INVOKABLE QStringList get_screen_modes(); Q_INVOKABLE QString get_screen_mode_current(); Q_INVOKABLE bool set_screen_mode(QString mode); + Q_INVOKABLE int get_ui_fps_cap(); // // Tries to mimic android toast as much as possible // diff --git a/app/util/qrenderstats.cpp b/app/util/qrenderstats.cpp index 40eb36f00..f007c164a 100644 --- a/app/util/qrenderstats.cpp +++ b/app/util/qrenderstats.cpp @@ -5,6 +5,8 @@ #endif #include +#include "util/qopenhd.h" +#include QRenderStats::QRenderStats(QObject *parent) : QObject{parent} @@ -57,6 +59,18 @@ void QRenderStats::registerOnWindow(QQuickWindow *window) }); m_present_timer->start(); } + + m_ui_fps_cap = QOpenHD::instance().get_ui_fps_cap(); + if (m_ui_fps_cap > 0 && !m_ui_fps_timer) { + window->setPersistentSceneGraph(true); + window->setClearBeforeRendering(false); + window->setColor(Qt::transparent); + m_ui_fps_timer = new QTimer(this); + m_ui_fps_timer->setTimerType(Qt::PreciseTimer); + m_ui_fps_timer->setInterval(std::max(1, 1000 / m_ui_fps_cap)); + connect(m_ui_fps_timer, &QTimer::timeout, window, QOverload<>::of(&QQuickWindow::update)); + m_ui_fps_timer->start(); + } #endif } @@ -76,7 +90,21 @@ void QRenderStats::set_display_width_height(int width, int height) void QRenderStats::m_QQuickWindow_beforeRendering() { - //m_avg_rendering_time.start(); + if (m_seen_render_pass) { + return; + } + const auto delta = std::chrono::steady_clock::now() - last_frame_before; + last_frame_before = std::chrono::steady_clock::now(); + avgMainRenderFrameDeltaBefore.add(delta); + avgMainRenderFrameDeltaBefore.recalculate_in_fixed_time_intervals(std::chrono::seconds(1),[this](const AvgCalculator& self){ + const auto main_stats=QString(self.getAvgReadable().c_str()); + set_main_render_stats(main_stats); + const auto avg_ns = self.getAvg().count(); + if (avg_ns > 0) { + const double fps = 1000000000.0 / static_cast(avg_ns); + set_screen_fps_str(QString("%1 fps").arg(fps, 0, 'f', 0)); + } + }); } void QRenderStats::m_QQuickWindow_afterRendering() @@ -92,6 +120,7 @@ void QRenderStats::m_QQuickWindow_afterRendering() void QRenderStats::m_QQuickWindow_beforeRenderPassRecording() { + m_seen_render_pass = true; m_avg_renderpass_time.start(); // Calculate frame time by calculating the delta between calls to render pass recording const auto delta=std::chrono::steady_clock::now()-last_frame; diff --git a/app/util/qrenderstats.h b/app/util/qrenderstats.h index 2bf3dd807..36152f070 100644 --- a/app/util/qrenderstats.h +++ b/app/util/qrenderstats.h @@ -51,6 +51,9 @@ public slots: // for the main render thread (render pass recording) std::chrono::steady_clock::time_point last_frame=std::chrono::steady_clock::now(); AvgCalculator avgMainRenderFrameDelta{}; + bool m_seen_render_pass = false; + std::chrono::steady_clock::time_point last_frame_before=std::chrono::steady_clock::now(); + AvgCalculator avgMainRenderFrameDeltaBefore{}; // NOTE: For some reason there seems to be no difference between frame time and before / after rendering - // looks like there is a glFLush() or somethin in QT. //Chronometer m_avg_rendering_time{}; @@ -58,6 +61,8 @@ public slots: QTimer* m_present_timer = nullptr; uint64_t m_last_external_frames = 0; std::chrono::steady_clock::time_point m_last_external_ts = std::chrono::steady_clock::now(); + QTimer* m_ui_fps_timer = nullptr; + int m_ui_fps_cap = 0; }; diff --git a/qml/ui/configpopup/qopenhd_settings/AppScreenSettingsView.qml b/qml/ui/configpopup/qopenhd_settings/AppScreenSettingsView.qml index 9fdd9957d..76f369546 100755 --- a/qml/ui/configpopup/qopenhd_settings/AppScreenSettingsView.qml +++ b/qml/ui/configpopup/qopenhd_settings/AppScreenSettingsView.qml @@ -251,6 +251,41 @@ ScrollView { onCheckedChanged: settings.dev_set_swap_interval_zero = checked } } + SettingBaseElement{ + visible: _qopenhd.is_platform_rock() + m_short_description: "UI FPS cap" + m_long_description: "Limits QML render rate to reduce CPU usage (restart required)." + ComboBox { + height: elementHeight + anchors.right: parent.right + anchors.rightMargin: Qt.inputMethod.visible ? 78 : 18 + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizonatalCenter + width: 320 + model: ListModel { + ListElement { text: qsTr("0 (uncapped)") ; value: 0 } + ListElement { text: qsTr("30 fps") ; value: 30 } + ListElement { text: qsTr("60 fps") ; value: 60 } + ListElement { text: qsTr("120 fps") ; value: 120 } + } + textRole: "text" + Component.onCompleted: { + for (var i = 0; i < model.count; i++) { + var choice = model.get(i); + if (choice.value == settings.ui_fps_cap) { + currentIndex = i; + } + } + } + onActivated:{ + const value_cap = model.get(currentIndex).value + if(settings.ui_fps_cap != value_cap){ + settings.ui_fps_cap = value_cap + _restartqopenhdmessagebox.show_with_text("UI FPS cap changed. Restart QOpenHD to apply.") + } + } + } + } // SettingBaseElement{ // m_short_description: "Settings window scale" diff --git a/qml/ui/elements/AppSettings.qml b/qml/ui/elements/AppSettings.qml index b2e899ae5..4c38c9f11 100644 --- a/qml/ui/elements/AppSettings.qml +++ b/qml/ui/elements/AppSettings.qml @@ -415,6 +415,7 @@ Settings { property bool dev_enable_live_audio_playback: true // might / might not work property bool dev_set_swap_interval_zero: false + property int ui_fps_cap: 30 // Discard actual video ratio and fit primary video to whatever ratio the screen is at (might distort video) property bool primary_video_scale_to_fit: false From 1190999a06df456e4648443f02ccd0c346d6133d Mon Sep 17 00:00:00 2001 From: Raphael <68374617+raphaelscholle@users.noreply.github.com> Date: Sun, 25 Jan 2026 01:45:59 +0100 Subject: [PATCH 14/21] stats --- app/util/qopenhd.cpp | 3 +++ app/util/qrenderstats.cpp | 23 +++++++++++++++++------ app/util/qrenderstats.h | 3 +++ 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/app/util/qopenhd.cpp b/app/util/qopenhd.cpp index 89df2943e..fdd6ef401 100644 --- a/app/util/qopenhd.cpp +++ b/app/util/qopenhd.cpp @@ -260,6 +260,9 @@ static QStringList get_modes_from_modetest() { } break; } + if (proc.exitStatus() != QProcess::NormalExit || proc.exitCode() != 0) { + return {}; + } const QString output = QString::fromUtf8(proc.readAllStandardOutput()); const QStringList lines = output.split('\n'); QRegularExpression re("^\\s*#?\\d+\\s+(\\d+x\\d+)\\s+([0-9.]+)"); diff --git a/app/util/qrenderstats.cpp b/app/util/qrenderstats.cpp index f007c164a..cbd6c5cae 100644 --- a/app/util/qrenderstats.cpp +++ b/app/util/qrenderstats.cpp @@ -7,6 +7,7 @@ #include #include "util/qopenhd.h" #include +#include QRenderStats::QRenderStats(QObject *parent) : QObject{parent} @@ -61,19 +62,29 @@ void QRenderStats::registerOnWindow(QQuickWindow *window) } m_ui_fps_cap = QOpenHD::instance().get_ui_fps_cap(); - if (m_ui_fps_cap > 0 && !m_ui_fps_timer) { + if (m_ui_fps_cap > 0) { + window->installEventFilter(this); window->setPersistentSceneGraph(true); window->setClearBeforeRendering(false); window->setColor(Qt::transparent); - m_ui_fps_timer = new QTimer(this); - m_ui_fps_timer->setTimerType(Qt::PreciseTimer); - m_ui_fps_timer->setInterval(std::max(1, 1000 / m_ui_fps_cap)); - connect(m_ui_fps_timer, &QTimer::timeout, window, QOverload<>::of(&QQuickWindow::update)); - m_ui_fps_timer->start(); } #endif } +bool QRenderStats::eventFilter(QObject* watched, QEvent* event) +{ + if (m_ui_fps_cap > 0 && event->type() == QEvent::UpdateRequest) { + const auto now = std::chrono::steady_clock::now(); + const auto elapsed = std::chrono::duration_cast(now - m_last_ui_update); + const int min_interval_ms = std::max(1, 1000 / m_ui_fps_cap); + if (elapsed.count() < min_interval_ms) { + return true; + } + m_last_ui_update = now; + } + return QObject::eventFilter(watched, event); +} + void QRenderStats::set_screen_width_height(int width, int height) { std::stringstream ss; diff --git a/app/util/qrenderstats.h b/app/util/qrenderstats.h index 36152f070..a1252fc57 100644 --- a/app/util/qrenderstats.h +++ b/app/util/qrenderstats.h @@ -47,6 +47,8 @@ public slots: void m_QQuickWindow_afterRendering(); void m_QQuickWindow_beforeRenderPassRecording(); void m_QQuickWindow_afterRenderPassRecording(); +protected: + bool eventFilter(QObject* watched, QEvent* event) override; private: // for the main render thread (render pass recording) std::chrono::steady_clock::time_point last_frame=std::chrono::steady_clock::now(); @@ -63,6 +65,7 @@ public slots: std::chrono::steady_clock::time_point m_last_external_ts = std::chrono::steady_clock::now(); QTimer* m_ui_fps_timer = nullptr; int m_ui_fps_cap = 0; + std::chrono::steady_clock::time_point m_last_ui_update = std::chrono::steady_clock::now(); }; From f104bb16a2ab1fae90835d2fe22d9d4c6e179c34 Mon Sep 17 00:00:00 2001 From: Raphael <68374617+raphaelscholle@users.noreply.github.com> Date: Sun, 25 Jan 2026 01:46:08 +0100 Subject: [PATCH 15/21] Update AppScreenSettingsView.qml --- .../AppScreenSettingsView.qml | 55 +++++++++++-------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/qml/ui/configpopup/qopenhd_settings/AppScreenSettingsView.qml b/qml/ui/configpopup/qopenhd_settings/AppScreenSettingsView.qml index 76f369546..01676e92d 100755 --- a/qml/ui/configpopup/qopenhd_settings/AppScreenSettingsView.qml +++ b/qml/ui/configpopup/qopenhd_settings/AppScreenSettingsView.qml @@ -144,35 +144,44 @@ ScrollView { visible: _qopenhd.is_platform_rock() m_short_description: "Screen mode" - ComboBox { - id: screenModeCombo - height: elementHeight + Row { + spacing: 8 anchors.right: parent.right anchors.rightMargin: Qt.inputMethod.visible ? 78 : 18 anchors.verticalCenter: parent.verticalCenter - anchors.horizontalCenter: parent.horizonatalCenter - width: 320 - model: appScreenSettingsView.screen_modes - enabled: model && model.length > 0 - Component.onCompleted: { - appScreenSettingsView.refresh_modes() - for (var i = 0; i < model.length; i++) { - if (model[i] === appScreenSettingsView.screen_mode_current) { - currentIndex = i; - break; + ComboBox { + id: screenModeCombo + height: elementHeight + width: 240 + model: appScreenSettingsView.screen_modes + enabled: model && model.length > 0 + Component.onCompleted: { + appScreenSettingsView.refresh_modes() + for (var i = 0; i < model.length; i++) { + if (model[i] === appScreenSettingsView.screen_mode_current) { + currentIndex = i; + break; + } } } - } - onActivated: { - const mode = model[currentIndex] - if (mode === appScreenSettingsView.screen_mode_current) { - return; + onActivated: { + const mode = model[currentIndex] + if (mode === appScreenSettingsView.screen_mode_current) { + return; + } + if (_qopenhd.set_screen_mode(mode)) { + appScreenSettingsView.screen_mode_current = mode + _restartqopenhdmessagebox.show_with_text("Screen mode changed. Restart QOpenHD if the UI looks wrong.") + } else { + _qopenhd.show_toast("Failed to set screen mode " + mode, true) + } } - if (_qopenhd.set_screen_mode(mode)) { - appScreenSettingsView.screen_mode_current = mode - _restartqopenhdmessagebox.show_with_text("Screen mode changed. Restart QOpenHD if the UI looks wrong.") - } else { - _qopenhd.show_toast("Failed to set screen mode " + mode, true) + } + Button { + text: qsTr("Refresh") + height: elementHeight + onClicked: { + appScreenSettingsView.refresh_modes() } } } From 01d57d34430641d28fcc17a7c5a2ce4456ed18f1 Mon Sep 17 00:00:00 2001 From: Raphael <68374617+raphaelscholle@users.noreply.github.com> Date: Sun, 25 Jan 2026 01:53:14 +0100 Subject: [PATCH 16/21] Update AppScreenSettingsView.qml --- .../configpopup/qopenhd_settings/AppScreenSettingsView.qml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/qml/ui/configpopup/qopenhd_settings/AppScreenSettingsView.qml b/qml/ui/configpopup/qopenhd_settings/AppScreenSettingsView.qml index 01676e92d..cb9730f51 100755 --- a/qml/ui/configpopup/qopenhd_settings/AppScreenSettingsView.qml +++ b/qml/ui/configpopup/qopenhd_settings/AppScreenSettingsView.qml @@ -182,6 +182,12 @@ ScrollView { height: elementHeight onClicked: { appScreenSettingsView.refresh_modes() + const err = _qopenhd.get_screen_modes_last_error() + if (!screenModeCombo.model || screenModeCombo.model.length === 0) { + _qopenhd.show_toast("Screen modes empty: " + err, true) + } else if (err && err !== "ok") { + _qopenhd.show_toast("Screen modes: " + err, false) + } } } } From d295011df8376ed9540e980b67509ab87600577c Mon Sep 17 00:00:00 2001 From: Raphael <68374617+raphaelscholle@users.noreply.github.com> Date: Sun, 25 Jan 2026 01:53:22 +0100 Subject: [PATCH 17/21] test --- app/util/qopenhd.cpp | 40 ++++++++++++++++++++++++++++++++++++---- app/util/qopenhd.h | 2 ++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/app/util/qopenhd.cpp b/app/util/qopenhd.cpp index fdd6ef401..2feb3d548 100644 --- a/app/util/qopenhd.cpp +++ b/app/util/qopenhd.cpp @@ -119,7 +119,7 @@ static void release_drm_context(DrmContext& ctx) { } } -static bool get_drm_context(DrmContext& ctx) { +static bool get_drm_context(DrmContext& ctx, QString& error_out) { if (QGuiApplication::platformName().contains("eglfs", Qt::CaseInsensitive)) { auto* pni = QGuiApplication::platformNativeInterface(); if (pni) { @@ -144,6 +144,7 @@ static bool get_drm_context(DrmContext& ctx) { if (ctx.fd < 0) { ctx.fd = open("/dev/dri/card0", O_RDONLY | O_CLOEXEC); if (ctx.fd < 0) { + error_out = "open /dev/dri/card0 failed"; qWarning() << "get_screen_modes: failed to open /dev/dri/card0"; return false; } @@ -167,6 +168,7 @@ static bool get_drm_context(DrmContext& ctx) { } } if (!res) { + error_out = "drmModeGetResources failed"; qWarning() << "get_screen_modes: drmModeGetResources failed"; release_drm_context(ctx); return false; @@ -217,6 +219,7 @@ static bool get_drm_context(DrmContext& ctx) { drmModeFreeResources(res); if (ctx.connector_id == 0 || ctx.crtc_id == 0) { + error_out = "missing connector/crtc"; qWarning() << "get_screen_modes: missing connector/crtc"; release_drm_context(ctx); return false; @@ -227,6 +230,7 @@ static bool get_drm_context(DrmContext& ctx) { } if (!ctx.connector) { qWarning() << "get_screen_modes: drmModeGetConnector failed"; + error_out = "drmModeGetConnector failed"; release_drm_context(ctx); return false; } @@ -234,6 +238,7 @@ static bool get_drm_context(DrmContext& ctx) { ctx.crtc = drmModeGetCrtc(ctx.fd, ctx.crtc_id); if (!ctx.crtc) { qWarning() << "get_screen_modes: drmModeGetCrtc failed"; + error_out = "drmModeGetCrtc failed"; release_drm_context(ctx); return false; } @@ -660,16 +665,21 @@ QStringList QOpenHD::get_screen_modes() { #if defined(__linux__) if (!is_platform_rock()) { + m_screen_modes_last_error = "not rock platform"; return {}; } DrmContext ctx; - if (!get_drm_context(ctx)) { + QString error; + if (!get_drm_context(ctx, error)) { + m_screen_modes_last_error = error; const auto fallback = get_modes_from_modetest(); if (!fallback.isEmpty()) { + m_screen_modes_last_error = "ok (modetest fallback)"; return fallback; } const QString current = get_screen_mode_current(); if (current != "NA") { + m_screen_modes_last_error = "ok (current mode fallback)"; return {current}; } return {}; @@ -683,11 +693,17 @@ QStringList QOpenHD::get_screen_modes() if (modes.isEmpty()) { const auto fallback = get_modes_from_modetest(); if (!fallback.isEmpty()) { + m_screen_modes_last_error = "ok (modetest fallback)"; return fallback; } + m_screen_modes_last_error = "no modes from drm or modetest"; + } + if (!modes.isEmpty()) { + m_screen_modes_last_error = "ok"; } return modes; #else + m_screen_modes_last_error = "not linux"; return {}; #endif } @@ -696,12 +712,16 @@ QString QOpenHD::get_screen_mode_current() { #if defined(__linux__) if (!is_platform_rock()) { + m_screen_modes_last_error = "not rock platform"; return QString("NA"); } DrmContext ctx; - if (!get_drm_context(ctx)) { + QString error; + if (!get_drm_context(ctx, error)) { + m_screen_modes_last_error = error; const auto fallback = get_modes_from_modetest(); if (!fallback.isEmpty()) { + m_screen_modes_last_error = "ok (modetest fallback)"; return fallback.first(); } return QString("NA"); @@ -716,11 +736,16 @@ QString QOpenHD::get_screen_mode_current() if (mode == "NA") { const auto fallback = get_modes_from_modetest(); if (!fallback.isEmpty()) { + m_screen_modes_last_error = "ok (modetest fallback)"; return fallback.first(); } + m_screen_modes_last_error = "current mode not found"; + } else { + m_screen_modes_last_error = "ok"; } return mode; #else + m_screen_modes_last_error = "not linux"; return QString("NA"); #endif } @@ -739,7 +764,9 @@ bool QOpenHD::set_screen_mode(QString mode) return false; } DrmContext ctx; - if (!get_drm_context(ctx)) { + QString error; + if (!get_drm_context(ctx, error)) { + m_screen_modes_last_error = error; return false; } if (!ctx.crtc || !ctx.connector) { @@ -805,6 +832,11 @@ int QOpenHD::get_ui_fps_cap() #endif } +QString QOpenHD::get_screen_modes_last_error() +{ + return m_screen_modes_last_error; +} + void QOpenHD::keep_screen_on(bool on) { #if defined(__android__) diff --git a/app/util/qopenhd.h b/app/util/qopenhd.h index c8b278f27..664bac4cd 100644 --- a/app/util/qopenhd.h +++ b/app/util/qopenhd.h @@ -75,6 +75,7 @@ class QOpenHD : public QObject Q_INVOKABLE QString get_screen_mode_current(); Q_INVOKABLE bool set_screen_mode(QString mode); Q_INVOKABLE int get_ui_fps_cap(); + Q_INVOKABLE QString get_screen_modes_last_error(); // // Tries to mimic android toast as much as possible // @@ -126,6 +127,7 @@ class QOpenHD : public QObject private: void do_not_call_toast_add(QString text,bool long_toast); void show_toast_and_add_remove_timer(QString text,bool long_toast); + QString m_screen_modes_last_error; }; #endif // QOPENHD_H From fe07ecf370766989793e2d2f17a151e73276e1db Mon Sep 17 00:00:00 2001 From: Raphael <68374617+raphaelscholle@users.noreply.github.com> Date: Sun, 25 Jan 2026 02:15:15 +0100 Subject: [PATCH 18/21] sysutils --- app/util/qopenhd.cpp | 134 +++++++++++++++++++ app/util/qrenderstats.cpp | 3 - app/videostreaming/avcodec/avcodec_video.pri | 14 +- 3 files changed, 135 insertions(+), 16 deletions(-) diff --git a/app/util/qopenhd.cpp b/app/util/qopenhd.cpp index 2feb3d548..44980dc3d 100644 --- a/app/util/qopenhd.cpp +++ b/app/util/qopenhd.cpp @@ -7,6 +7,11 @@ #include #include #include +#include +#include +#include +#include +#include #include #include @@ -30,6 +35,11 @@ #include #include #include +#include +#include +#include +#include "telemetry/models/aohdsystem.h" +#include "telemetry/models/openhd_core/platform.hpp" #endif #if defined(ENABLE_SPEECH) @@ -57,6 +67,11 @@ struct DrmContext { drmModeCrtc* crtc = nullptr; }; +struct SysutilsPlatformInfo { + int platform_type = -1; + QString platform_name; +}; + static double calc_vrefresh(const drmModeModeInfo& mode) { if (mode.htotal == 0 || mode.vtotal == 0) { return 0.0; @@ -119,6 +134,109 @@ static void release_drm_context(DrmContext& ctx) { } } +static bool query_sysutils_platform(SysutilsPlatformInfo& info, QString& error_out) { + constexpr const char* kSocketPath = "/run/openhd/openhd_sys.sock"; + int fd = ::socket(AF_UNIX, SOCK_STREAM, 0); + if (fd < 0) { + error_out = "sysutils socket failed"; + return false; + } + sockaddr_un addr{}; + addr.sun_family = AF_UNIX; + if (std::strlen(kSocketPath) >= sizeof(addr.sun_path)) { + error_out = "sysutils socket path too long"; + ::close(fd); + return false; + } + std::strncpy(addr.sun_path, kSocketPath, sizeof(addr.sun_path) - 1); + if (::connect(fd, reinterpret_cast(&addr), sizeof(addr)) < 0) { + error_out = "sysutils connect failed"; + ::close(fd); + return false; + } + constexpr const char* payload = "{\"type\":\"sysutil.platform.request\"}\n"; + const ssize_t sent = ::send(fd, payload, std::strlen(payload), MSG_NOSIGNAL); + if (sent != static_cast(std::strlen(payload))) { + error_out = "sysutils send failed"; + ::close(fd); + return false; + } + + pollfd pfd{}; + pfd.fd = fd; + pfd.events = POLLIN; + const int poll_ret = ::poll(&pfd, 1, 200); + if (poll_ret <= 0 || !(pfd.revents & POLLIN)) { + error_out = "sysutils response timeout"; + ::close(fd); + return false; + } + + std::string response; + char buffer[256]; + while (true) { + const ssize_t read_bytes = ::recv(fd, buffer, sizeof(buffer), 0); + if (read_bytes <= 0) { + break; + } + response.append(buffer, buffer + read_bytes); + if (response.find('\n') != std::string::npos) { + break; + } + } + ::close(fd); + const auto newline_pos = response.find('\n'); + if (newline_pos != std::string::npos) { + response.resize(newline_pos); + } + if (response.empty()) { + error_out = "sysutils empty response"; + return false; + } + QJsonParseError parse_error{}; + const QJsonDocument doc = QJsonDocument::fromJson(QByteArray::fromStdString(response), &parse_error); + if (parse_error.error != QJsonParseError::NoError || !doc.isObject()) { + error_out = "sysutils json parse failed"; + return false; + } + const QJsonObject obj = doc.object(); + if (obj.value("type").toString() != "sysutil.platform.response") { + error_out = "sysutils response type mismatch"; + return false; + } + info.platform_type = obj.value("platform_type").toInt(-1); + info.platform_name = obj.value("platform_name").toString(); + if (info.platform_type < 0) { + error_out = "sysutils missing platform_type"; + return false; + } + return true; +} + +static bool get_sysutils_platform_cached(SysutilsPlatformInfo& info, QString& error_out) { + static QMutex mutex; + static SysutilsPlatformInfo cached; + static qint64 last_ms = 0; + static bool has_cache = false; + QMutexLocker lock(&mutex); + const qint64 now_ms = QDateTime::currentMSecsSinceEpoch(); + if (has_cache && (now_ms - last_ms) < 5000) { + info = cached; + return true; + } + SysutilsPlatformInfo fresh; + QString err; + if (query_sysutils_platform(fresh, err)) { + cached = fresh; + last_ms = now_ms; + has_cache = true; + info = cached; + return true; + } + error_out = err; + return false; +} + static bool get_drm_context(DrmContext& ctx, QString& error_out) { if (QGuiApplication::platformName().contains("eglfs", Qt::CaseInsensitive)) { auto* pni = QGuiApplication::platformNativeInterface(); @@ -648,6 +766,22 @@ bool QOpenHD::is_platform_rock() #ifdef IS_PLATFORM_ROCK return true; #else + const auto is_rock_platform = [](int type) { + return type >= X_PLATFORM_TYPE_ROCKCHIP_RK3566_RADXA_ZERO3W && + type < X_PLATFORM_TYPE_ALWINNER_X20; + }; +#if defined(__linux__) + SysutilsPlatformInfo info; + QString error; + if (get_sysutils_platform_cached(info, error)) { + return is_rock_platform(info.platform_type); + } +#endif + const int ground_platform = AOHDSystem::instanceGround().ohd_platform_type(); + const int air_platform = AOHDSystem::instanceAir().ohd_platform_type(); + if (is_rock_platform(ground_platform) || is_rock_platform(air_platform)) { + return true; + } return false; #endif } diff --git a/app/util/qrenderstats.cpp b/app/util/qrenderstats.cpp index cbd6c5cae..fee7f458b 100644 --- a/app/util/qrenderstats.cpp +++ b/app/util/qrenderstats.cpp @@ -101,9 +101,6 @@ void QRenderStats::set_display_width_height(int width, int height) void QRenderStats::m_QQuickWindow_beforeRendering() { - if (m_seen_render_pass) { - return; - } const auto delta = std::chrono::steady_clock::now() - last_frame_before; last_frame_before = std::chrono::steady_clock::now(); avgMainRenderFrameDeltaBefore.add(delta); diff --git a/app/videostreaming/avcodec/avcodec_video.pri b/app/videostreaming/avcodec/avcodec_video.pri index 4d90196ac..77b3891b5 100644 --- a/app/videostreaming/avcodec/avcodec_video.pri +++ b/app/videostreaming/avcodec/avcodec_video.pri @@ -47,19 +47,7 @@ packagesExist(mmal) { DEFINES += IS_PLATFORM_RPI } -exists(/usr/local/share/openhd/platform/rock/) { - message(This is a Rock) - DEFINES += IS_PLATFORM_ROCK -} else { - message(This is not a Rock) -} - -exists(/usr/local/share/openhd/platform/nxp/) { - message(This is an NXP device) - DEFINES += IS_PLATFORM_NXP -} else { - message(This is not an NXP device) -} +# Platform detection is handled at runtime via openhd-sysutils. # can be used in c++, also set to be exposed in qml DEFINES += QOPENHD_ENABLE_VIDEO_VIA_AVCODEC From 7cbef4d24cbdc231f945735fcb7b66a6a1f646ce Mon Sep 17 00:00:00 2001 From: Raphael <68374617+raphaelscholle@users.noreply.github.com> Date: Sun, 25 Jan 2026 02:54:09 +0100 Subject: [PATCH 19/21] ^testing restart mod --- app/util/qopenhd.cpp | 48 +++++++++++++++++++ app/util/qopenhd.h | 3 ++ .../dev/AppDeveloperStatsPanel.qml | 4 ++ qml/ui/elements/RestartQOpenHDMessageBox.qml | 2 +- 4 files changed, 56 insertions(+), 1 deletion(-) diff --git a/app/util/qopenhd.cpp b/app/util/qopenhd.cpp index 44980dc3d..d01819bd0 100644 --- a/app/util/qopenhd.cpp +++ b/app/util/qopenhd.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -526,6 +527,15 @@ void QOpenHD::quit_qopenhd() qDebug()<<"quit_qopenhd() end"; } +void QOpenHD::exit_for_restart() +{ + qDebug()<<"exit_for_restart() begin"; + QCoreApplication::quit(); + QTimer::singleShot(250, [] { + ::_exit(0); + }); +} + void QOpenHD::disable_service_and_quit() { #ifdef __linux__ @@ -971,6 +981,44 @@ QString QOpenHD::get_screen_modes_last_error() return m_screen_modes_last_error; } +QString QOpenHD::get_hw_cursor_status() +{ +#if defined(__linux__) + const QString platform = QGuiApplication::platformName(); + const QString env_value = QString::fromUtf8(qgetenv("QT_QPA_EGLFS_HWCURSOR")); + const QString env_note = env_value.isEmpty() ? "unset" : env_value; + const QString cfg_path = QString::fromUtf8(qgetenv("QT_QPA_EGLFS_KMS_CONFIG")); + QString status = QString("eglfs=%1 env=%2").arg(platform, env_note); + if (cfg_path.isEmpty()) { + status += " config=unset"; + return status; + } + status += QString(" config=%1").arg(cfg_path); + QFile cfg_file(cfg_path); + if (!cfg_file.open(QIODevice::ReadOnly)) { + status += " hwcursor=unreadable"; + return status; + } + const QByteArray payload = cfg_file.readAll(); + QJsonParseError parse_error{}; + const QJsonDocument doc = QJsonDocument::fromJson(payload, &parse_error); + if (parse_error.error != QJsonParseError::NoError || !doc.isObject()) { + status += " hwcursor=invalid-json"; + return status; + } + const QJsonObject obj = doc.object(); + if (!obj.contains("hwcursor")) { + status += " hwcursor=missing"; + return status; + } + const bool hwcursor = obj.value("hwcursor").toBool(false); + status += QString(" hwcursor=%1").arg(hwcursor ? "true" : "false"); + return status; +#else + return QString("not linux"); +#endif +} + void QOpenHD::keep_screen_on(bool on) { #if defined(__android__) diff --git a/app/util/qopenhd.h b/app/util/qopenhd.h index 664bac4cd..32d32b123 100644 --- a/app/util/qopenhd.h +++ b/app/util/qopenhd.h @@ -35,6 +35,8 @@ class QOpenHD : public QObject // This only terminates the App, on most OpenHD images the system service will then restart // QOpenHD. Can be usefully for debugging, if something's wrong with the app and you need to restart it Q_INVOKABLE void quit_qopenhd(); + // Exit fast for restart when KMS state is wedged. + Q_INVOKABLE void exit_for_restart(); // This not only quits qopenhd, but also disables the autostart service // (until next reboot) Q_INVOKABLE void disable_service_and_quit(); @@ -76,6 +78,7 @@ class QOpenHD : public QObject Q_INVOKABLE bool set_screen_mode(QString mode); Q_INVOKABLE int get_ui_fps_cap(); Q_INVOKABLE QString get_screen_modes_last_error(); + Q_INVOKABLE QString get_hw_cursor_status(); // // Tries to mimic android toast as much as possible // diff --git a/qml/ui/configpopup/dev/AppDeveloperStatsPanel.qml b/qml/ui/configpopup/dev/AppDeveloperStatsPanel.qml index b68acda75..5ada88cf3 100644 --- a/qml/ui/configpopup/dev/AppDeveloperStatsPanel.qml +++ b/qml/ui/configpopup/dev/AppDeveloperStatsPanel.qml @@ -59,6 +59,10 @@ Rectangle { height: 23 text: qsTr("Resolution: " + " Screen " + _qrenderstats.screen_width_height_str + " ADJ:" + _qrenderstats.display_width_height_str + " Window: " + _qrenderstats.window_width + "x" + _qrenderstats.window_height) } + Text { + height: 23 + text: qsTr("HW Cursor: " + _qopenhd.get_hw_cursor_status()) + } Text { height: 23 text: qsTr("Art Horizon mavlink update rate:" + _fcMavlinkSystem.curr_update_rate_mavlink_message_attitude + " Hz") diff --git a/qml/ui/elements/RestartQOpenHDMessageBox.qml b/qml/ui/elements/RestartQOpenHDMessageBox.qml index 066d09b1c..cc2c7159f 100644 --- a/qml/ui/elements/RestartQOpenHDMessageBox.qml +++ b/qml/ui/elements/RestartQOpenHDMessageBox.qml @@ -56,7 +56,7 @@ Card { onPressed: { _restartqopenhdmessagebox.hide_element() // Let service restart - _qopenhd.quit_qopenhd(); + _qopenhd.exit_for_restart(); } } Button { From 02dc443672aaaef78eedbf2f3378033272cf0d89 Mon Sep 17 00:00:00 2001 From: Raphael <68374617+raphaelscholle@users.noreply.github.com> Date: Sun, 25 Jan 2026 16:16:25 +0100 Subject: [PATCH 20/21] Update install_build_dep.sh --- install_build_dep.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install_build_dep.sh b/install_build_dep.sh index 57d11dd65..f3878530e 100755 --- a/install_build_dep.sh +++ b/install_build_dep.sh @@ -5,7 +5,7 @@ PLATFORM="$1" QTTYPE="$2" -BASE_PACKAGES="gnupg libjsoncpp-dev libtinyxml2-dev zlib1g libcurl4-gnutls-dev gnupg1 gnupg2 apt-transport-https apt-utils libgles2-mesa-dev libegl1-mesa-dev libgbm-dev libsdl2-dev libsdl1.2-dev" +BASE_PACKAGES="qtbase5-private-dev gnupg libjsoncpp-dev libtinyxml2-dev zlib1g libcurl4-gnutls-dev gnupg1 gnupg2 apt-transport-https apt-utils libgles2-mesa-dev libegl1-mesa-dev libgbm-dev libsdl2-dev libsdl1.2-dev" VIDEO_PACKAGES="libgstreamer-plugins-base1.0-dev gstreamer1.0-plugins-good libavcodec-dev libavformat-dev" BUILD_PACKAGES="ruby-dev meson build-essential cmake git ruby-dev python3-pip python3-future qttools5-dev-tools" From e97318a9d91cb6698f0effc380beb69e7954df33 Mon Sep 17 00:00:00 2001 From: Raphael <68374617+raphaelscholle@users.noreply.github.com> Date: Sun, 25 Jan 2026 17:04:26 +0100 Subject: [PATCH 21/21] Update install_build_dep.sh --- install_build_dep.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install_build_dep.sh b/install_build_dep.sh index f3878530e..e0ca81090 100755 --- a/install_build_dep.sh +++ b/install_build_dep.sh @@ -5,7 +5,7 @@ PLATFORM="$1" QTTYPE="$2" -BASE_PACKAGES="qtbase5-private-dev gnupg libjsoncpp-dev libtinyxml2-dev zlib1g libcurl4-gnutls-dev gnupg1 gnupg2 apt-transport-https apt-utils libgles2-mesa-dev libegl1-mesa-dev libgbm-dev libsdl2-dev libsdl1.2-dev" +BASE_PACKAGES="libdrm-dev qtbase5-private-dev gnupg libjsoncpp-dev libtinyxml2-dev zlib1g libcurl4-gnutls-dev gnupg1 gnupg2 apt-transport-https apt-utils libgles2-mesa-dev libegl1-mesa-dev libgbm-dev libsdl2-dev libsdl1.2-dev" VIDEO_PACKAGES="libgstreamer-plugins-base1.0-dev gstreamer1.0-plugins-good libavcodec-dev libavformat-dev" BUILD_PACKAGES="ruby-dev meson build-essential cmake git ruby-dev python3-pip python3-future qttools5-dev-tools"