Bild weiter verbessert, Icon und .desktop hinzugefügt

This commit is contained in:
2026-10-17 23:39:50 +02:00
parent d1de850b60
commit 29a86d895a
20 changed files with 1086 additions and 14 deletions

View File

@@ -422,6 +422,30 @@ void CaptureThread::adjustHardwareControl(quint32 id, const char* settingsKey,
}
}
void CaptureThread::setHardwareValues(int brightness, int contrast, int saturation)
{
if (m_fd == -1) return;
auto set = [&](quint32 id, int value, int restoreValue) {
v4l2_queryctrl qc{};
qc.id = id;
if (xioctl(m_fd, VIDIOC_QUERYCTRL, &qc) == -1 || (qc.flags & V4L2_CTRL_FLAG_DISABLED))
return;
const int target = value >= 0 ? value : restoreValue;
if (target < 0) return; // weder Profilwert noch Ausgangswert bekannt
v4l2_control ctrl{};
ctrl.id = id;
ctrl.value = qBound(qc.minimum, target, qc.maximum);
xioctl(m_fd, VIDIOC_S_CTRL, &ctrl);
};
set(V4L2_CID_BRIGHTNESS, brightness, m_restoreBrightness);
set(V4L2_CID_CONTRAST, contrast, m_restoreContrast);
set(V4L2_CID_SATURATION, saturation, m_restoreSaturation);
}
void CaptureThread::adjustHardwareBrightness(int delta)
{
adjustHardwareControl(V4L2_CID_BRIGHTNESS, "hardware/brightness", "Hardware-Helligkeit", delta);

View File

@@ -27,6 +27,9 @@ public:
void adjustHardwareBrightness(int delta);
void adjustHardwareContrast(int delta);
void adjustHardwareSaturation(int delta);
// Setzt die Hardware-Regler absolut (für Profile). -1 = zurück auf den
// beim Start gemerkten Ausgangswert des Chips.
void setHardwareValues(int brightness, int contrast, int saturation);
int captureWidth() const { return m_width; }
int captureHeight() const { return m_height; }

View File

@@ -55,6 +55,22 @@ DeviceDialog::DeviceDialog(QWidget* parent)
form->addRow("Audio-Gerät (ALSA):", m_audioCombo);
form->addRow("Ziel-Framerate:", m_fpsCombo);
// Profile: gespeicherte Kombination aus Glättung/Retro/Schärfen/HW-Reglern
m_profileCombo = new QComboBox(this);
m_profileSaveBtn = new QPushButton("Speichern …", this);
m_profileDeleteBtn = new QPushButton("Löschen", this);
m_profileSaveBtn->setAutoDefault(false);
m_profileDeleteBtn->setAutoDefault(false);
m_profileCombo->setToolTip(
"Gespeicherte Bild-Einstellungen (Glättung, Retro, Schärfen,\n"
"Hardware-Regler) für eine Quelle, z. B. PlayStation oder PS3.\n"
"Im laufenden Betrieb mit [F1]..[F9] oder über das Menü \"Profile\" umschaltbar.");
auto* profileRow = new QHBoxLayout;
profileRow->addWidget(m_profileCombo, 1);
profileRow->addWidget(m_profileSaveBtn);
profileRow->addWidget(m_profileDeleteBtn);
form->addRow("Profil:", profileRow);
m_smoothCheck = new QCheckBox("Bildglättung aktivieren (empfohlen)", this);
m_smoothCheck->setChecked(true);
m_smoothCheck->setToolTip(
@@ -143,6 +159,24 @@ DeviceDialog::DeviceDialog(QWidget* parent)
connect(btnOk, &QPushButton::clicked, this, &QDialog::accept);
connect(btnClose, &QPushButton::clicked, this, &QDialog::reject);
// Profile: Auswahl lädt die Werte in die Felder; manuelle Änderungen
// führen die Auswahl nach (passendes Profil oder "kein Profil").
populateProfiles(QString());
connect(m_profileCombo, QOverload<int>::of(&QComboBox::activated), this, [this](int index) {
loadProfileIntoControls(m_profileCombo->itemData(index).toString());
});
connect(m_profileSaveBtn, &QPushButton::clicked, this, &DeviceDialog::saveProfile);
connect(m_profileDeleteBtn, &QPushButton::clicked, this, &DeviceDialog::deleteProfile);
auto onControlChanged = [this]() { refreshProfileSelection(); };
connect(m_smoothCheck, &QCheckBox::toggled, this, onControlChanged);
connect(m_retroCheck, &QCheckBox::toggled, this, onControlChanged);
connect(m_sharpenSpin, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, onControlChanged);
connect(m_hwBrightnessSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, onControlChanged);
connect(m_hwContrastSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, onControlChanged);
connect(m_hwSaturationSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, onControlChanged);
refreshProfileSelection();
auto* main = new QVBoxLayout(this);
main->addLayout(form);
main->addSpacing(8);
@@ -171,6 +205,77 @@ void DeviceDialog::setPreselectedHwBrightness(int value) { m_hwBrightnessSpin->s
void DeviceDialog::setPreselectedHwContrast(int value) { m_hwContrastSpin->setValue(value); }
void DeviceDialog::setPreselectedHwSaturation(int value) { m_hwSaturationSpin->setValue(value); }
Profile DeviceDialog::currentProfile() const
{
Profile p;
p.smooth = m_smoothCheck->isChecked();
p.retro = m_retroCheck->isChecked();
p.sharpen = static_cast<float>(m_sharpenSpin->value());
p.hwBrightness = m_hwBrightnessSpin->value();
p.hwContrast = m_hwContrastSpin->value();
p.hwSaturation = m_hwSaturationSpin->value();
return p;
}
void DeviceDialog::populateProfiles(const QString& select)
{
m_profileCombo->clear();
m_profileCombo->addItem("– kein Profil –", QString());
for (const QString& name : Profiles::names())
m_profileCombo->addItem(name, name);
selectByData(m_profileCombo, select);
m_profileDeleteBtn->setEnabled(m_profileCombo->currentIndex() > 0);
}
void DeviceDialog::refreshProfileSelection()
{
if (m_loadingProfile) return;
const Profile cur = currentProfile();
// Aktuell gewähltes Profil bevorzugen, solange es noch passt
// (relevant, wenn zwei Profile identische Werte haben).
QString match = m_profileCombo->currentData().toString();
if (match.isEmpty() || !Profiles::exists(match) || !Profiles::load(match).matches(cur))
match = Profiles::findMatching(cur);
selectByData(m_profileCombo, match);
m_profileDeleteBtn->setEnabled(m_profileCombo->currentIndex() > 0);
}
void DeviceDialog::loadProfileIntoControls(const QString& name)
{
m_profileDeleteBtn->setEnabled(!name.isEmpty());
if (name.isEmpty() || !Profiles::exists(name))
return;
const Profile p = Profiles::load(name);
m_loadingProfile = true;
m_smoothCheck->setChecked(p.smooth);
m_retroCheck->setChecked(p.retro);
m_sharpenSpin->setValue(p.sharpen);
m_hwBrightnessSpin->setValue(p.hwBrightness);
m_hwContrastSpin->setValue(p.hwContrast);
m_hwSaturationSpin->setValue(p.hwSaturation);
m_loadingProfile = false;
}
void DeviceDialog::saveProfile()
{
QString saved;
if (Profiles::saveWithPrompt(this, currentProfile(), &saved))
populateProfiles(saved);
}
void DeviceDialog::deleteProfile()
{
const QString name = m_profileCombo->currentData().toString();
if (name.isEmpty()) return;
if (Profiles::removeWithConfirm(this, name))
populateProfiles(QString());
}
void DeviceDialog::selectByData(QComboBox* combo, const QString& data)
{
for (int i = 0; i < combo->count(); ++i)

View File

@@ -3,8 +3,11 @@
#include <QDialog>
#include <QString>
#include "Profile.h"
QT_BEGIN_NAMESPACE
class QComboBox;
class QPushButton;
class QCheckBox;
class QDoubleSpinBox;
class QSpinBox;
@@ -37,10 +40,20 @@ public:
void setPreselectedHwContrast(int value); // -1 = unverändert
void setPreselectedHwSaturation(int value); // -1 = unverändert
// Profile: aktueller Stand der Bild-Einstellungen im Dialog, und
// Profil-Auswahl passend zu den aktuellen Werten setzen (nach den
// setPreselected*-Aufrufen verwenden).
Profile currentProfile() const;
void refreshProfileSelection();
private:
void populateVideoDevices();
void populateAudioDevices();
void selectByData(QComboBox* combo, const QString& data);
void populateProfiles(const QString& select);
void loadProfileIntoControls(const QString& name);
void saveProfile();
void deleteProfile();
QComboBox* m_videoCombo = nullptr;
QComboBox* m_audioCombo = nullptr;
@@ -51,4 +64,8 @@ private:
QSpinBox* m_hwBrightnessSpin = nullptr;
QSpinBox* m_hwContrastSpin = nullptr;
QSpinBox* m_hwSaturationSpin = nullptr;
QComboBox* m_profileCombo = nullptr;
QPushButton* m_profileSaveBtn = nullptr;
QPushButton* m_profileDeleteBtn = nullptr;
bool m_loadingProfile = false;
};

View File

@@ -20,6 +20,8 @@
#include <QDir>
#include <QDateTime>
#include "Profile.h"
MainWindow::MainWindow(const QString& videoDevice,
const QString& audioDevice,
int targetFps,
@@ -139,6 +141,12 @@ void MainWindow::setupMenuBar()
QAction* screenshotAct = viewMenu->addAction("Screenshot speichern\tP");
connect(screenshotAct, &QAction::triggered, this, &MainWindow::saveScreenshot);
// Profile werden beim Aufklappen frisch aufgebaut (Häkchen = passt zum
// aktuellen Stand), Umschalten auch per F1..F9.
m_profileMenu = menuBar()->addMenu("&Profile");
connect(m_profileMenu, &QMenu::aboutToShow, this, &MainWindow::rebuildProfileMenu);
rebuildProfileMenu();
QMenu* helpMenu = menuBar()->addMenu("&Hilfe");
QAction* aboutAct = helpMenu->addAction("Über HDMI Viewer");
@@ -146,6 +154,103 @@ void MainWindow::setupMenuBar()
connect(aboutAct, &QAction::triggered, this, &MainWindow::showAbout);
}
// ── Profile ──────────────────────────────────────────────────────────────────
Profile MainWindow::currentProfile() const
{
// Hardware-Werte stehen laut QSettings immer aktuell (Tasten + Dialog
// schreiben sie sofort), die Anzeige-Werte kommen direkt vom Widget.
Profile p = Profile::fromSettings();
p.smooth = m_video->smoothModeEnabled();
p.retro = m_video->retroModeEnabled();
p.sharpen = m_video->sharpenAmount();
return p;
}
void MainWindow::rebuildProfileMenu()
{
if (!m_profileMenu) return;
m_profileMenu->clear();
const QStringList names = Profiles::names();
const Profile current = currentProfile();
for (int i = 0; i < names.size(); ++i) {
const QString name = names.at(i);
QString label = name;
label.replace("&", "&&");
if (i < 9)
label += QString("\tF%1").arg(i + 1);
QAction* act = m_profileMenu->addAction(label);
act->setCheckable(true);
act->setChecked(Profiles::load(name).matches(current));
connect(act, &QAction::triggered, this, [this, name]() { applyProfile(name); });
}
if (names.isEmpty()) {
QAction* none = m_profileMenu->addAction("(noch keine Profile gespeichert)");
none->setEnabled(false);
}
m_profileMenu->addSeparator();
QAction* saveAct = m_profileMenu->addAction("Aktuelle Einstellungen als Profil speichern …");
connect(saveAct, &QAction::triggered, this, &MainWindow::saveCurrentAsProfile);
QMenu* delMenu = m_profileMenu->addMenu("Profil löschen");
delMenu->setEnabled(!names.isEmpty());
for (const QString& name : names) {
QString label = name;
label.replace("&", "&&");
QAction* act = delMenu->addAction(label);
connect(act, &QAction::triggered, this, [this, name]() { deleteProfile(name); });
}
}
void MainWindow::applyProfile(const QString& name)
{
if (!Profiles::exists(name)) return;
const Profile p = Profiles::load(name);
m_video->setSmoothModeEnabled(p.smooth);
m_video->setRetroModeEnabled(p.retro);
const float sharpen = qBound(VideoWidget::kSharpenMin, p.sharpen, VideoWidget::kSharpenMax);
m_video->setSharpenAmount(sharpen);
if (sharpen > 0.0f)
m_lastSharpenAmount = sharpen;
if (m_smoothAction) m_smoothAction->setChecked(p.smooth);
if (m_retroAction) m_retroAction->setChecked(p.retro);
if (m_sharpenAction) m_sharpenAction->setChecked(sharpen > 0.0f);
// Als aktuellen Stand persistieren (nächster Start übernimmt ihn) ...
p.applyToSettings();
// ... und die Hardware-Regler am Grabber sofort umstellen.
if (m_capture)
m_capture->setHardwareValues(p.hwBrightness, p.hwContrast, p.hwSaturation);
statusBar()->showMessage("Profil geladen: " + name, 3000);
showOverlayMessage("Profil: " + name);
}
void MainWindow::saveCurrentAsProfile()
{
QString saved;
if (Profiles::saveWithPrompt(this, currentProfile(), &saved)) {
statusBar()->showMessage("Profil gespeichert: " + saved, 3000);
showOverlayMessage("Profil gespeichert: " + saved);
}
}
void MainWindow::deleteProfile(const QString& name)
{
if (Profiles::removeWithConfirm(this, name))
statusBar()->showMessage("Profil gelöscht: " + name, 3000);
}
void MainWindow::showOverlayMessage(const QString& text)
{
if (!m_overlayLabel) return;
@@ -249,7 +354,19 @@ void MainWindow::toggleRaw()
const bool enabled = !m_video->rawEnabled();
m_video->setRawEnabled(enabled);
const QString msg = enabled ? "Roh-Modus: an (alle Effekte umgangen)" : "Roh-Modus: aus";
// Auch die Hardware-Regler am Grabber-Chip umgehen: An = zurück auf die
// beim Start gemerkten Ausgangswerte des Chips (-1), Aus = gespeicherte
// Nutzerwerte (QSettings bzw. aktives Profil) wieder setzen.
if (m_capture) {
if (enabled) {
m_capture->setHardwareValues(-1, -1, -1);
} else {
const Profile p = Profile::fromSettings();
m_capture->setHardwareValues(p.hwBrightness, p.hwContrast, p.hwSaturation);
}
}
const QString msg = enabled ? "Roh-Modus: an (alle Filter und HW-Regler aus)" : "Roh-Modus: aus";
statusBar()->showMessage(msg, 2000);
showOverlayMessage(msg);
}
@@ -287,7 +404,7 @@ void MainWindow::showAbout()
{
auto* dlg = new QDialog(this);
dlg->setWindowTitle("Über HDMI Viewer");
dlg->setFixedSize(460, 470);
dlg->setFixedSize(460, 490);
dlg->setModal(true);
auto* root = new QVBoxLayout(dlg);
@@ -336,7 +453,7 @@ void MainWindow::showAbout()
grid->addWidget(v, row, 1);
};
addRow(0, "Version", "1.0");
addRow(0, "Version", "1.1.0");
addRow(1, "Entwickler", "Dany Thinnes");
addRow(2, "Projekt", "Projekt Hirnfrei");
addRow(3, "Framework", QString("Qt %1").arg(QT_VERSION_STR));
@@ -360,6 +477,7 @@ void MainWindow::showAbout()
"<b>1</b>/<b>2</b> HW-Kontrast &nbsp;&nbsp; "
"<b>3</b>/<b>4</b> HW-Sättigung &nbsp;&nbsp; "
"<b>R</b> Roh-Diagnose<br>"
"<b>F1</b>–<b>F9</b> Profile<br>"
"<b>V</b> Vergleich &nbsp;&nbsp; "
"<b>P</b> Screenshot &nbsp;&nbsp; "
"<b>D</b> Frame dumpen &nbsp;&nbsp; "
@@ -404,6 +522,17 @@ void MainWindow::stopAll()
void MainWindow::keyPressEvent(QKeyEvent* event)
{
// F1..F9: Profil 1..9 (Reihenfolge wie im Profile-Menü, alphabetisch)
if (event->key() >= Qt::Key_F1 && event->key() <= Qt::Key_F9) {
const QStringList names = Profiles::names();
const int idx = event->key() - Qt::Key_F1;
if (idx < names.size())
applyProfile(names.at(idx));
else
showOverlayMessage(QString("Kein Profil auf F%1").arg(idx + 1));
return;
}
switch (event->key()) {
case Qt::Key_F:
toggleFullscreen();
@@ -494,7 +623,7 @@ void MainWindow::updateStatusBar()
info = QString("%1×%2 @ %3fps %4 ")
.arg(m_capWidth).arg(m_capHeight).arg(m_capFps).arg(m_capFmt);
info += QString("Video: %1 Audio: %2 [F] Vollbild [S] Glättung [C] Retro [X/+/-] Schärfen [,/.] HW-Helligkeit [1/2] HW-Kontrast [3/4] HW-Sättigung [R] Roh [V] Vergleich [P] Screenshot [D] Frame dumpen [Esc] Einstellungen")
info += QString("Video: %1 Audio: %2 [F] Vollbild [S] Glättung [C] Retro [X/+/-] Schärfen [,/.] HW-Helligkeit [1/2] HW-Kontrast [3/4] HW-Sättigung [R] Roh [F1-F9] Profile [V] Vergleich [P] Screenshot [D] Frame dumpen [Esc] Einstellungen")
.arg(m_videoDevice,
m_audioDevice.isEmpty() ? QStringLiteral("–") : m_audioDevice);

View File

@@ -3,12 +3,15 @@
#include <QMainWindow>
#include <QString>
#include "Profile.h"
class VideoWidget;
class CaptureThread;
class AudioThread;
QT_BEGIN_NAMESPACE
class QAction;
class QLabel;
class QMenu;
QT_END_NAMESPACE
class MainWindow : public QMainWindow
@@ -57,6 +60,14 @@ private:
void applySharpen(float value);
void showOverlayMessage(const QString& text);
// Profile
Profile currentProfile() const;
void rebuildProfileMenu();
void applyProfile(const QString& name);
void saveCurrentAsProfile();
void deleteProfile(const QString& name);
QMenu* m_profileMenu = nullptr;
VideoWidget* m_video = nullptr;
CaptureThread* m_capture = nullptr;
AudioThread* m_audio = nullptr;

168
src/Profile.cpp Normal file
View File

@@ -0,0 +1,168 @@
#include "Profile.h"
#include <QSettings>
#include <QInputDialog>
#include <QMessageBox>
#include <QtGlobal>
// ── Profile ──────────────────────────────────────────────────────────────────
bool Profile::matches(const Profile& o) const
{
return smooth == o.smooth
&& retro == o.retro
&& qAbs(sharpen - o.sharpen) < 0.05f // Spinbox hat 1 Nachkommastelle
&& hwBrightness == o.hwBrightness
&& hwContrast == o.hwContrast
&& hwSaturation == o.hwSaturation;
}
Profile Profile::fromSettings()
{
QSettings s;
Profile p;
p.smooth = s.value("display/smooth", true).toBool();
p.retro = s.value("display/retro", false).toBool();
p.sharpen = s.value("display/sharpen", 0.0).toFloat();
p.hwBrightness = s.contains("hardware/brightness") ? s.value("hardware/brightness").toInt() : -1;
p.hwContrast = s.contains("hardware/contrast") ? s.value("hardware/contrast").toInt() : -1;
p.hwSaturation = s.contains("hardware/saturation") ? s.value("hardware/saturation").toInt() : -1;
return p;
}
void Profile::applyToSettings() const
{
QSettings s;
s.setValue("display/smooth", smooth);
s.setValue("display/retro", retro);
s.setValue("display/sharpen", static_cast<double>(sharpen));
if (sharpen > 0.0f)
s.setValue("display/sharpen_last", static_cast<double>(sharpen));
auto setOrRemove = [&](const char* key, int value) {
if (value >= 0) s.setValue(key, value);
else s.remove(key);
};
setOrRemove("hardware/brightness", hwBrightness);
setOrRemove("hardware/contrast", hwContrast);
setOrRemove("hardware/saturation", hwSaturation);
}
// ── Profiles ─────────────────────────────────────────────────────────────────
namespace Profiles
{
QStringList names()
{
QSettings s;
s.beginGroup("profiles");
QStringList list = s.childGroups();
s.endGroup();
list.sort(Qt::CaseInsensitive);
return list;
}
bool exists(const QString& name)
{
return isValidName(name) && names().contains(name);
}
Profile load(const QString& name)
{
QSettings s;
s.beginGroup("profiles/" + name);
Profile p;
p.smooth = s.value("smooth", true).toBool();
p.retro = s.value("retro", false).toBool();
p.sharpen = s.value("sharpen", 0.0).toFloat();
p.hwBrightness = s.value("hwBrightness", -1).toInt();
p.hwContrast = s.value("hwContrast", -1).toInt();
p.hwSaturation = s.value("hwSaturation", -1).toInt();
s.endGroup();
return p;
}
void save(const QString& name, const Profile& p)
{
if (!isValidName(name)) return;
QSettings s;
s.beginGroup("profiles/" + name);
s.setValue("smooth", p.smooth);
s.setValue("retro", p.retro);
s.setValue("sharpen", static_cast<double>(p.sharpen));
s.setValue("hwBrightness", p.hwBrightness);
s.setValue("hwContrast", p.hwContrast);
s.setValue("hwSaturation", p.hwSaturation);
s.endGroup();
s.sync();
}
bool remove(const QString& name)
{
if (!exists(name)) return false;
QSettings s;
s.remove("profiles/" + name);
s.sync();
return true;
}
bool isValidName(const QString& name)
{
// '/' und '\' wären für QSettings Gruppentrenner
return !name.trimmed().isEmpty()
&& name == name.trimmed()
&& !name.contains('/')
&& !name.contains('\\');
}
QString findMatching(const Profile& profile)
{
for (const QString& n : names())
if (load(n).matches(profile))
return n;
return QString();
}
bool saveWithPrompt(QWidget* parent, const Profile& profile, QString* savedName)
{
const QStringList existing = names();
bool ok = false;
const QString input = QInputDialog::getItem(
parent, "Profil speichern",
"Name des Profils (neuen Namen eintippen\noder vorhandenes zum Überschreiben wählen):",
existing, 0, true, &ok);
if (!ok) return false;
const QString name = input.trimmed();
if (!isValidName(name)) {
QMessageBox::warning(parent, "Profil speichern",
"Ungültiger Name. Der Name darf nicht leer sein und weder '/' noch '\\' enthalten.");
return false;
}
if (existing.contains(name)) {
const auto answer = QMessageBox::question(
parent, "Profil überschreiben",
QString("Das Profil „%1“ existiert bereits. Überschreiben?").arg(name),
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
if (answer != QMessageBox::Yes) return false;
}
save(name, profile);
if (savedName) *savedName = name;
return true;
}
bool removeWithConfirm(QWidget* parent, const QString& name)
{
const auto answer = QMessageBox::question(
parent, "Profil löschen",
QString("Profil „%1“ wirklich löschen?").arg(name),
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
if (answer != QMessageBox::Yes) return false;
return remove(name);
}
} // namespace Profiles

49
src/Profile.h Normal file
View File

@@ -0,0 +1,49 @@
#pragma once
#include <QString>
#include <QStringList>
QT_BEGIN_NAMESPACE
class QWidget;
QT_END_NAMESPACE
// Ein Profil bündelt alle Bildeinstellungen, die sich pro Konsole/Quelle
// unterscheiden (z. B. PlayStation: aufhellen + Filter, PS3: alles neutral).
// Hardware-Werte: -1 = "unverändert" (Regler am Grabber-Chip nicht anfassen
// bzw. auf den Ausgangswert des Chips zurücksetzen).
struct Profile
{
bool smooth = true;
bool retro = false;
float sharpen = 0.0f; // 0 = aus
int hwBrightness = -1;
int hwContrast = -1;
int hwSaturation = -1;
bool matches(const Profile& other) const;
// Aktueller Stand laut QSettings (display/* und hardware/*).
static Profile fromSettings();
// Schreibt das Profil als aktuellen Stand nach display/* und hardware/*.
void applyToSettings() const;
};
// Profilverwaltung (gespeichert in QSettings unter profiles/<Name>/...).
namespace Profiles
{
QStringList names(); // alphabetisch sortiert
bool exists(const QString& name);
Profile load(const QString& name);
void save(const QString& name, const Profile& profile);
bool remove(const QString& name);
bool isValidName(const QString& name);
QString findMatching(const Profile& profile); // "" falls keins passt
// Dialog: Namen eingeben bzw. vorhandenes Profil wählen (Überschreiben
// wird nachgefragt) und speichern. true = gespeichert.
bool saveWithPrompt(QWidget* parent, const Profile& profile, QString* savedName = nullptr);
// Löschen nach Rückfrage. true = gelöscht.
bool removeWithConfirm(QWidget* parent, const QString& name);
}

View File

@@ -4,6 +4,7 @@
#include <QOpenGLTexture>
#include <QPainter>
#include <QVector2D>
#include <QDebug>
namespace {
@@ -64,6 +65,7 @@ uniform float u_sharpen;
uniform float u_gamma;
uniform int u_raw;
uniform int u_compare;
uniform int u_debug;
vec3 rgb2ycbcr(vec3 c)
{
@@ -159,6 +161,12 @@ void main()
{
vec2 uv = v_tex;
// Diagnose (nur per HDMIVIEWER_DEBUG=shaderwhite): Shader gibt reines Weiss aus.
if (u_debug == 1) {
fragColor = vec4(1.0, 1.0, 1.0, 1.0);
return;
}
// Diagnose: komplette Umgehung aller Verarbeitung, 1:1 wie im Speicher.
if (u_raw == 1) {
fragColor = vec4(texture(u_tex, uv).rgb, 1.0);
@@ -338,6 +346,14 @@ void VideoWidget::initializeGL()
initializeOpenGLFunctions();
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
// Diagnose-Ausgabe einmalig im Terminal: welche GL-Umgebung/Pixelformat aktiv ist.
qDebug().noquote() << "GL_RENDERER:" << reinterpret_cast<const char*>(glGetString(GL_RENDERER))
<< " GL_VERSION:" << reinterpret_cast<const char*>(glGetString(GL_VERSION));
qDebug().noquote() << QString("Surface: R%1 G%2 B%3 A%4 samples=%5")
.arg(format().redBufferSize()).arg(format().greenBufferSize())
.arg(format().blueBufferSize()).arg(format().alphaBufferSize())
.arg(format().samples());
m_program = new QOpenGLShaderProgram(this);
m_program->addShaderFromSourceCode(QOpenGLShader::Vertex, kVertexShaderSrc);
m_program->addShaderFromSourceCode(QOpenGLShader::Fragment, kFragmentShaderSrc);
@@ -403,11 +419,81 @@ void VideoWidget::paintGL()
glClear(GL_COLOR_BUFFER_BIT);
// Diagnose-Schalter per Umgebungsvariable (im Normalbetrieb ohne Wirkung):
// HDMIVIEWER_DEBUG=clearwhite -> Framebuffer nur mit Weiss loeschen, kein Shader
// HDMIVIEWER_DEBUG=shaderwhite -> Shader wird benutzt, gibt aber reines Weiss aus
static const QByteArray dbg = qgetenv("HDMIVIEWER_DEBUG");
if (dbg == "clearwhite") {
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
return;
}
if (!m_hasFrame || !m_texture) {
drawNoSignal();
return;
}
// Diagnose (HDMIVIEWER_DEBUG=state oder =reset): GL-Zustand direkt VOR dem Zeichnen
// einmalig ins Terminal schreiben (nach einem moeglichen QPainter in drawNoSignal).
if (dbg == "state" || dbg == "reset") {
static bool dumped = false;
if (!dumped) {
dumped = true;
auto en = [this](GLenum cap) { return glIsEnabled(cap) ? 1 : 0; };
GLint bsr = 0, bdr = 0, bsa = 0, bda = 0, beq = 0, fbo = 0, enc = 0, ctype = 0;
GLfloat bc[4] = {0, 0, 0, 0};
GLboolean cm[4] = {0, 0, 0, 0};
glGetIntegerv(0x80C9, &bsr); // GL_BLEND_SRC_RGB
glGetIntegerv(0x80C8, &bdr); // GL_BLEND_DST_RGB
glGetIntegerv(0x80CB, &bsa); // GL_BLEND_SRC_ALPHA
glGetIntegerv(0x80CA, &bda); // GL_BLEND_DST_ALPHA
glGetIntegerv(0x8009, &beq); // GL_BLEND_EQUATION_RGB
glGetIntegerv(0x8CA6, &fbo); // GL_DRAW_FRAMEBUFFER_BINDING
glGetFloatv(0x8005, bc); // GL_BLEND_COLOR
glGetBooleanv(0x0C23, cm); // GL_COLOR_WRITEMASK
glGetFramebufferAttachmentParameteriv(0x8CA9, 0x8CE0, 0x8210, &enc); // COLOR_ENCODING
glGetFramebufferAttachmentParameteriv(0x8CA9, 0x8CE0, 0x8211, &ctype); // COMPONENT_TYPE
const QStringList st = {
QString("BLEND=%1").arg(en(0x0BE2)),
QString("blendRGB src=0x%1 dst=0x%2").arg(bsr, 0, 16).arg(bdr, 0, 16),
QString("blendA src=0x%1 dst=0x%2").arg(bsa, 0, 16).arg(bda, 0, 16),
QString("eq=0x%1").arg(beq, 0, 16),
QString("blendColor=%1,%2,%3,%4").arg(bc[0]).arg(bc[1]).arg(bc[2]).arg(bc[3]),
QString("colorMask=%1%2%3%4").arg(int(cm[0])).arg(int(cm[1])).arg(int(cm[2])).arg(int(cm[3])),
QString("FB_SRGB=%1").arg(en(0x8DB9)),
QString("LOGIC_OP=%1").arg(en(0x0BF2)),
QString("A2C=%1").arg(en(0x809E)),
QString("DITHER=%1").arg(en(0x0BD0)),
QString("MULTISAMPLE=%1").arg(en(0x809D)),
QString("SCISSOR=%1").arg(en(0x0C11)),
QString("DEPTH=%1").arg(en(0x0B71)),
QString("STENCIL=%1").arg(en(0x0B90)),
QString("CULL=%1").arg(en(0x0B44)),
QString("drawFBO=%1").arg(fbo),
QString("attachEncoding=0x%1 (0x8c40=sRGB, 0x2601=linear)").arg(enc, 0, 16),
QString("attachType=0x%1 (0x8c17=UNORM, 0x8f9c=SNORM, 0x1406=FLOAT)").arg(ctype, 0, 16)
};
qDebug().noquote() << "GL-State vor dem Zeichnen:" << st.join(" ");
}
}
// Fester Zustands-Reset vor dem Zeichnen: drawNoSignal() nutzt QPainter, der GL-Zustand
// (u. a. Blending) zuruecklaesst. Ohne diesen Reset war das Bild live/im Screenshot
// exakt auf die Haelfte abgedunkelt (Weiss 255 -> 128), auch im Roh-Modus.
{
glDisable(0x0BE2); // GL_BLEND
glDisable(0x0B71); // GL_DEPTH_TEST
glDisable(0x0B90); // GL_STENCIL_TEST
glDisable(0x0C11); // GL_SCISSOR_TEST
glDisable(0x0B44); // GL_CULL_FACE
glDisable(0x8DB9); // GL_FRAMEBUFFER_SRGB
glDisable(0x0BF2); // GL_COLOR_LOGIC_OP
glDisable(0x809E); // GL_SAMPLE_ALPHA_TO_COVERAGE
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
}
const float viewW = static_cast<float>(width());
const float viewH = static_cast<float>(height());
const float imgW = static_cast<float>(m_imageSize.width());
@@ -441,6 +527,7 @@ void VideoWidget::paintGL()
m_program->setUniformValue("u_gamma", m_brightness);
m_program->setUniformValue("u_raw", m_rawEnabled ? 1 : 0);
m_program->setUniformValue("u_compare", m_compareEnabled ? 1 : 0);
m_program->setUniformValue("u_debug", dbg == "shaderwhite" ? 1 : 0);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

View File

@@ -2,6 +2,7 @@
#include <QSettings>
#include <QMessageBox>
#include <QSurfaceFormat>
#include <QIcon>
#include "DeviceDialog.h"
#include "MainWindow.h"
@@ -18,9 +19,13 @@ int main(int argc, char* argv[])
QApplication app(argc, argv);
app.setApplicationName("HDMIViewer");
app.setApplicationVersion("1.0");
app.setApplicationVersion("1.1.0");
app.setOrganizationName("hdmi-viewer");
// Fenster-/Taskleisten-Icon: installiertes Theme-Icon bevorzugen, sonst das
// eingebettete SVG (z. B. beim Start direkt aus dem build-Ordner).
app.setWindowIcon(QIcon::fromTheme("hdmi-viewer", QIcon(":/icons/hdmi-viewer.svg")));
QSettings settings;
while (true) {
@@ -34,6 +39,7 @@ int main(int argc, char* argv[])
dlg.setPreselectedHwBrightness(settings.value("hardware/brightness", -1).toInt());
dlg.setPreselectedHwContrast(settings.value("hardware/contrast", -1).toInt());
dlg.setPreselectedHwSaturation(settings.value("hardware/saturation", -1).toInt());
dlg.refreshProfileSelection(); // zeigt das Profil, das zu den Werten passt
if (dlg.exec() != QDialog::Accepted)
break;