diff --git a/QOpenHD.pro b/QOpenHD.pro index 04da21c11..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 @@ -103,7 +102,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/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..d01819bd0 100644 --- a/app/util/qopenhd.cpp +++ b/app/util/qopenhd.cpp @@ -7,17 +7,42 @@ #include #include #include +#include +#include +#include +#include +#include +#include #include #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 +#include +#include +#include +#include "telemetry/models/aohdsystem.h" +#include "telemetry/models/openhd_core/platform.hpp" +#endif + #if defined(ENABLE_SPEECH) #include #include @@ -32,6 +57,356 @@ #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; +}; + +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; + } + 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 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(); + 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_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; + } + ctx.owns_fd = true; + } + + drmModeRes* res = drmModeGetResources(ctx.fd); + if (!res) { + 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) { + error_out = "drmModeGetResources failed"; + 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) { + error_out = "missing connector/crtc"; + 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"; + error_out = "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"; + error_out = "drmModeGetCrtc failed"; + release_drm_context(ctx); + return false; + } + + 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; + } + 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.]+)"); + 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 + QOpenHD &QOpenHD::instance() { static QOpenHD instance=QOpenHD(); @@ -152,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__ @@ -392,6 +776,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 } @@ -405,6 +805,220 @@ bool QOpenHD::is_platform_nxp() #endif } +QStringList QOpenHD::get_screen_modes() +{ +#if defined(__linux__) + if (!is_platform_rock()) { + m_screen_modes_last_error = "not rock platform"; + return {}; + } + DrmContext 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 {}; + } + 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); + 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 +} + +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; + 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"); + } + 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); + 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 +} + +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; + QString error; + if (!get_drm_context(ctx, error)) { + m_screen_modes_last_error = error; + 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; + } + drmModeModeInfo chosen_mode = *chosen; + const int ret = drmModeSetCrtc(ctx.fd, + ctx.crtc_id, + ctx.crtc->buffer_id, + 0, + 0, + &ctx.connector_id, + 1, + &chosen_mode); + 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 +} + +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 +} + +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 e38748d2e..32d32b123 100644 --- a/app/util/qopenhd.h +++ b/app/util/qopenhd.h @@ -7,6 +7,7 @@ #include #include #include +#include #ifdef ENABLE_SPEECH #include @@ -34,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(); @@ -70,6 +73,12 @@ 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); + 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 // @@ -121,6 +130,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 diff --git a/app/util/qrenderstats.cpp b/app/util/qrenderstats.cpp index c823273b7..fee7f458b 100644 --- a/app/util/qrenderstats.cpp +++ b/app/util/qrenderstats.cpp @@ -1,10 +1,13 @@ #include "qrenderstats.h" -#if defined(__linux__) && defined(IS_PLATFORM_ROCK) +#if defined(__linux__) #include "videostreaming/avcodec/drm_kms/rk3588_video_link.h" #endif #include +#include "util/qopenhd.h" +#include +#include QRenderStats::QRenderStats(QObject *parent) : QObject{parent} @@ -35,11 +38,53 @@ 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(); + set_external_video_fps_str("0 fps"); + if (!m_present_timer) { + m_present_timer = new QTimer(this); + m_present_timer->setTimerType(Qt::PreciseTimer); + 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(); + } + + m_ui_fps_cap = QOpenHD::instance().get_ui_fps_cap(); + if (m_ui_fps_cap > 0) { + window->installEventFilter(this); + window->setPersistentSceneGraph(true); + window->setClearBeforeRendering(false); + window->setColor(Qt::transparent); + } #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; @@ -56,10 +101,18 @@ 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) - Rk3588VideoLink::instance().present_if_pending(); -#endif + 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() @@ -75,6 +128,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; @@ -87,6 +141,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 ee38b80fd..a1252fc57 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" @@ -19,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) @@ -44,14 +47,25 @@ 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(); 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{}; 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(); + 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(); }; 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 diff --git a/app/videostreaming/avcodec/drm_kms/rk3588_video_link.cpp b/app/videostreaming/avcodec/drm_kms/rk3588_video_link.cpp index dd1778f2d..6aca84bab 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 @@ -17,6 +17,10 @@ #include #include +#include +#include +#include + #include "util/qrenderstats.h" namespace { @@ -39,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() { @@ -51,8 +77,11 @@ Rk3588VideoLink::~Rk3588VideoLink() { if (recv_thread.joinable()) { recv_thread.join(); } + if (present_thread.joinable()) { + present_thread.join(); + } cleanup_cache(); - if (drm_fd >= 0) { + if (drm_fd >= 0 && owns_fd) { close(drm_fd); drm_fd = -1; } @@ -93,24 +122,76 @@ 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() { - 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"; @@ -131,6 +212,15 @@ 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; } @@ -189,6 +279,9 @@ bool Rk3588VideoLink::pick_overlay_plane(uint32_t fourcc, uint64_t modifier) { return false; } 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) { @@ -202,21 +295,32 @@ bool Rk3588VideoLink::pick_overlay_plane(uint32_t fourcc, uint64_t modifier) { drmModeFreePlane(plane); continue; } + 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) { - chosen = plane->plane_id; - drmModeFreePlane(plane); - break; - } - if (!chosen) { - chosen = plane->plane_id; + 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 << " 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; } @@ -331,6 +435,10 @@ void Rk3588VideoLink::receiver_thread() { close(sock); return; } + 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)]; @@ -357,10 +465,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; } @@ -395,6 +512,11 @@ 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: import failed for frame " + << meta.width << "x" << meta.height << "\n"; + } } close_fds(fds); } @@ -512,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); @@ -531,6 +643,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; @@ -550,8 +670,37 @@ void Rk3588VideoLink::present_if_pending() { } if (drmModeAtomicCommit(drm_fd, req, DRM_MODE_ATOMIC_NONBLOCK, nullptr) == 0) { current_fb_id = frame.fb_id; + } else { + 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) { + 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: switching to drmModeSetPlane path\n"; + } else { + std::cerr << "Rk3588VideoLink: drmModeSetPlane fallback failed: " + << strerror(errno) << "\n"; + } + } } 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 9257c2142..e0f5547a8 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 @@ -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,12 +75,16 @@ 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; uint32_t connector_id = 0; uint32_t crtc_id = 0; int crtc_index = -1; @@ -108,8 +113,8 @@ 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}; }; #else diff --git a/install_build_dep.sh b/install_build_dep.sh index 57d11dd65..e0ca81090 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="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" diff --git a/qml/main.qml b/qml/main.qml index b578c75d7..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 ) @@ -106,19 +112,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/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/configpopup/qopenhd_settings/AppScreenSettingsView.qml b/qml/ui/configpopup/qopenhd_settings/AppScreenSettingsView.qml index c494ddb12..cb9730f51 100755 --- a/qml/ui/configpopup/qopenhd_settings/AppScreenSettingsView.qml +++ b/qml/ui/configpopup/qopenhd_settings/AppScreenSettingsView.qml @@ -18,6 +18,17 @@ ScrollView { contentHeight: screenColumn.height 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 @@ -129,6 +140,58 @@ ScrollView { m_description: "Advanced settings" m_hide_elements: true + SettingBaseElement{ + visible: _qopenhd.is_platform_rock() + m_short_description: "Screen mode" + + Row { + spacing: 8 + anchors.right: parent.right + anchors.rightMargin: Qt.inputMethod.visible ? 78 : 18 + anchors.verticalCenter: parent.verticalCenter + 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; + } + 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() + 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) + } + } + } + } + } SettingBaseElement{ m_short_description: "Screen rotation" // anything other than 0 and 180 can breaks things @@ -203,6 +266,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 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 { 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 {