diff --git a/app/widget/scope/histogram/histogram.cpp b/app/widget/scope/histogram/histogram.cpp index 05c7a82ce..49a966693 100644 --- a/app/widget/scope/histogram/histogram.cpp +++ b/app/widget/scope/histogram/histogram.cpp @@ -21,6 +21,7 @@ #include "histogram.h" +#include #include #include #include @@ -148,4 +149,105 @@ void HistogramScope::DrawScope(TexturePtr managed_tex, QVariant pipeline) p.drawLines(histogram_lines); } +void HistogramScope::DrawScopeSoftware(QPainter &p, const QImage &image) +{ + const float histogram_scale = 0.80f; + const float histogram_base = 2.5f; + + const int histogram_dim_x = qCeil((width() - 1.0) * histogram_scale); + const int histogram_dim_y = qCeil((height() - 1.0) * histogram_scale); + const int histogram_start_dim_x = + qFloor(((width() - 1.0) - histogram_dim_x) / 2.0f); + const int histogram_start_dim_y = + qFloor(((height() - 1.0) - histogram_dim_y) / 2.0f); + const int histogram_end_dim_x = width() - 1 - histogram_start_dim_x; + + std::array r_counts{}; + std::array g_counts{}; + std::array b_counts{}; + + const int src_w = image.width(); + const int src_h = image.height(); + + // Limit analysis resolution to keep CPU usage reasonable. + const int step_x = qMax(1, src_w / 512); + const int step_y = qMax(1, src_h / 512); + + for (int sy = 0; sy < src_h; sy += step_y) { + const uchar *src_line = image.constScanLine(sy); + for (int sx = 0; sx < src_w; sx += step_x) { + const uchar *src = src_line + sx * 4; + r_counts[src[0]]++; + g_counts[src[1]]++; + b_counts[src[2]]++; + } + } + + int max_count = 1; + for (int i = 0; i < 256; ++i) { + max_count = qMax(max_count, r_counts[i]); + max_count = qMax(max_count, g_counts[i]); + max_count = qMax(max_count, b_counts[i]); + } + + auto draw_channel = [&](const std::array &counts, const QColor &color) { + QPen pen(color); + pen.setWidth(2); + p.setPen(pen); + + QVector points; + points.reserve(256); + for (int i = 0; i < 256; ++i) { + float x = histogram_start_dim_x + + (float(i) / 255.0f) * histogram_dim_x; + float normalized = float(counts[i]) / float(max_count); + float y = histogram_start_dim_y + + histogram_dim_y * + (1.0f - pow(normalized, 1.0f / histogram_base)); + points.append(QPointF(x, y)); + } + p.drawPolyline(points.constData(), points.size()); + }; + + p.setCompositionMode(QPainter::CompositionMode_Plus); + draw_channel(r_counts, QColor(255, 0, 0)); + draw_channel(g_counts, QColor(0, 255, 0)); + draw_channel(b_counts, QColor(0, 0, 255)); + + // Draw percentage line overlays + QFont font = p.font(); + font.setPixelSize(10); + QFontMetrics font_metrics = QFontMetrics(font); + QString label; + std::vector histogram_increments = { 0.00, 0.25, 0.50, 1.0 }; + + int histogram_steps = histogram_increments.size(); + QVector histogram_lines(histogram_steps + 1); + int font_x_offset = 0; + int font_y_offset = font_metrics.capHeight() / 2.0f; + + p.setCompositionMode(QPainter::CompositionMode_SourceOver); + p.setPen(QColor(0.0, 0.6 * 255.0, 0.0)); + p.setFont(font); + + for (std::vector::iterator it = histogram_increments.begin(); + it != histogram_increments.end(); it++) { + histogram_lines[it - histogram_increments.begin()].setLine( + histogram_start_dim_x, + (histogram_dim_y * pow(1.0 - *it, histogram_base)) + + histogram_start_dim_y, + histogram_end_dim_x, + (histogram_dim_y * pow(1.0 - *it, histogram_base)) + + histogram_start_dim_y); + label = QString::number(*it * 100, 'f', 1) + "%"; + font_x_offset = QtUtils::QFontMetricsWidth(font_metrics, label) + 4; + + p.drawText(histogram_start_dim_x - font_x_offset, + (histogram_dim_y * pow(1.0 - *it, histogram_base)) + + histogram_start_dim_y + font_y_offset, + label); + } + p.drawLines(histogram_lines); +} + } diff --git a/app/widget/scope/histogram/histogram.h b/app/widget/scope/histogram/histogram.h index dd33591d9..53037c57c 100644 --- a/app/widget/scope/histogram/histogram.h +++ b/app/widget/scope/histogram/histogram.h @@ -45,6 +45,8 @@ protected: virtual void DrawScope(TexturePtr managed_tex, QVariant pipeline) override; + virtual void DrawScopeSoftware(QPainter &p, const QImage &image) override; + private: QVariant pipeline_secondary_; TexturePtr texture_row_sums_; diff --git a/app/widget/scope/scopebase/scopebase.cpp b/app/widget/scope/scopebase/scopebase.cpp index 42e910bfa..363835dce 100644 --- a/app/widget/scope/scopebase/scopebase.cpp +++ b/app/widget/scope/scopebase/scopebase.cpp @@ -33,6 +33,7 @@ ScopeBase::ScopeBase(QWidget *parent) : super(parent) , texture_(nullptr) , managed_tex_up_to_date_(false) + , software_image_up_to_date_(false) { EnableDefaultContextMenu(); } @@ -41,6 +42,7 @@ void ScopeBase::SetBuffer(TexturePtr frame) { texture_ = frame; managed_tex_up_to_date_ = false; + software_image_up_to_date_ = false; update(); } @@ -60,17 +62,103 @@ void ScopeBase::DrawScope(TexturePtr managed_tex, QVariant pipeline) renderer()->Blit(pipeline, job, GetViewportParams()); } +void ScopeBase::UpdateSoftwareImage() +{ + if (!texture_ || texture_->IsDummy() || !renderer()) { + software_image_ = QImage(); + software_image_up_to_date_ = true; + return; + } + + // Backend-neutral widgets each have their own renderer instance (e.g. a + // separate Vulkan device). The reference texture emitted by the viewer lives + // in the viewer's renderer, so we must download it to the CPU and re-upload + // it into this scope's renderer before we can sample it. + TexturePtr source_tex = texture_; + if (texture_->renderer() && texture_->renderer() != renderer()) { + FramePtr temp_frame = Frame::Create(); + temp_frame->set_video_params(texture_->params()); + temp_frame->allocate(); + texture_->Download(temp_frame->data(), temp_frame->linesize_pixels()); + + local_texture_ = renderer()->CreateTexture( + temp_frame->video_params(), temp_frame->data(), + temp_frame->linesize_pixels()); + source_tex = local_texture_; + } + + if (!source_tex || source_tex->IsDummy()) { + software_image_ = QImage(); + software_image_up_to_date_ = true; + return; + } + + const int texture_width = static_cast(width() * devicePixelRatioF()); + const int texture_height = static_cast(height() * devicePixelRatioF()); + + const VideoParams offscreen_params( + texture_width, texture_height, PixelFormat::U8, + VideoParams::kRGBAChannelCount); + + if (!software_tex_ || software_tex_->params() != offscreen_params) { + software_tex_ = renderer()->CreateTexture(offscreen_params); + software_buffer_.resize( + texture_width * texture_height * + VideoParams::GetBytesPerPixel(PixelFormat::U8, + VideoParams::kRGBAChannelCount)); + } + + if (!software_tex_ || software_tex_->IsDummy()) { + software_image_ = QImage(); + software_image_up_to_date_ = true; + return; + } + + ColorTransformJob job; + job.SetColorProcessor(color_service()); + job.SetInputTexture(source_tex); + job.SetInputAlphaAssociation(kAlphaNone); + job.SetClearDestinationEnabled(true); + job.SetForceOpaque(true); + + renderer()->BlitColorManaged(job, software_tex_.get()); + renderer()->DownloadFromTexture(software_tex_->id(), software_tex_->params(), + software_buffer_.data(), 0); + + software_image_ = QImage( + reinterpret_cast(software_buffer_.constData()), + texture_width, texture_height, + texture_width * + VideoParams::GetBytesPerPixel(PixelFormat::U8, + VideoParams::kRGBAChannelCount), + QImage::Format_RGBA8888_Premultiplied); + software_image_.setDevicePixelRatio(devicePixelRatioF()); + + software_image_up_to_date_ = true; +} + void ScopeBase::OnInit() { super::OnInit(); - pipeline_ = renderer()->CreateNativeShader(GenerateShaderCode()); + if (!IsBackendNeutral()) { + pipeline_ = renderer()->CreateNativeShader(GenerateShaderCode()); + } } void ScopeBase::OnPaint() { if (IsBackendNeutral()) { - // TODO: implement backend-neutral scope display + if (!software_image_up_to_date_) { + UpdateSoftwareImage(); + } + + QPainter p(paint_device()); + p.fillRect(rect(), Qt::black); + + if (!software_image_.isNull()) { + DrawScopeSoftware(p, software_image_); + } return; } @@ -89,6 +177,7 @@ void ScopeBase::OnPaint() job.SetInputAlphaAssociation(kAlphaNone); renderer()->BlitColorManaged(job, managed_tex_.get()); + managed_tex_up_to_date_ = true; } DrawScope(managed_tex_, pipeline_); @@ -97,6 +186,10 @@ void ScopeBase::OnPaint() void ScopeBase::OnDestroy() { + local_texture_ = nullptr; + software_tex_ = nullptr; + software_buffer_.clear(); + software_image_ = QImage(); managed_tex_ = nullptr; texture_ = nullptr; pipeline_.clear(); diff --git a/app/widget/scope/scopebase/scopebase.h b/app/widget/scope/scopebase/scopebase.h index 6f493d197..37e81e617 100644 --- a/app/widget/scope/scopebase/scopebase.h +++ b/app/widget/scope/scopebase/scopebase.h @@ -51,13 +51,23 @@ protected: virtual ShaderCode GenerateShaderCode() = 0; /** - * @brief Draw function + * @brief GPU-accelerated draw function used on OpenGL backends. * * Override this if your sub-class scope needs extra drawing. */ virtual void DrawScope(TexturePtr managed_tex, QVariant pipeline); + /** + * @brief Software draw function used on backend-neutral paths (e.g. Vulkan). + * + * Implementations receive an 8-bit sRGB/display-ready image and should draw + * the scope visualization with QPainter. + */ + virtual void DrawScopeSoftware(QPainter &p, const QImage &image) = 0; + private: + void UpdateSoftwareImage(); + QVariant pipeline_; TexturePtr texture_; @@ -65,6 +75,13 @@ private: TexturePtr managed_tex_; bool managed_tex_up_to_date_; + + TexturePtr software_tex_; + QByteArray software_buffer_; + QImage software_image_; + bool software_image_up_to_date_; + + TexturePtr local_texture_; }; } diff --git a/app/widget/scope/vectorscope/vectorscope.cpp b/app/widget/scope/vectorscope/vectorscope.cpp index 4680aee14..a7e7201bb 100644 --- a/app/widget/scope/vectorscope/vectorscope.cpp +++ b/app/widget/scope/vectorscope/vectorscope.cpp @@ -129,4 +129,106 @@ void VectorscopeScope::DrawScope(TexturePtr managed_tex, QVariant pipeline) } } +void VectorscopeScope::DrawScopeSoftware(QPainter &p, const QImage &image) +{ + const float vectorscope_scale = 0.80f; + const float vectorscope_gain = 1.45f; + const float vectorscope_intensity = 0.035f; + + QImage buf(width(), height(), QImage::Format_ARGB32_Premultiplied); + buf.fill(Qt::transparent); + + double luma_coeffs[3] = { 0.0, 0.0, 0.0 }; + color_manager()->GetDefaultLumaCoefs(luma_coeffs); + + const int src_w = image.width(); + const int src_h = image.height(); + + // Limit analysis resolution to keep CPU usage reasonable. + const int step_x = qMax(1, src_w / 256); + const int step_y = qMax(1, src_h / 256); + + float scope_size = qMin(width(), height()) * vectorscope_scale; + QPointF center(width() * 0.5, height() * 0.5); + float radius = scope_size * 0.5; + + for (int sy = 0; sy < src_h; sy += step_y) { + const uchar *src_line = image.constScanLine(sy); + for (int sx = 0; sx < src_w; sx += step_x) { + const uchar *src = src_line + sx * 4; + float r = src[0] / 255.0f; + float g = src[1] / 255.0f; + float b = src[2] / 255.0f; + + float y = r * luma_coeffs[0] + g * luma_coeffs[1] + + b * luma_coeffs[2]; + float cb = (b - y) / qMax(2.0f * (1.0f - luma_coeffs[2]), 0.0001f); + float cr = (r - y) / qMax(2.0f * (1.0f - luma_coeffs[0]), 0.0001f); + + QPointF point = center + + QPointF(cr * vectorscope_gain * radius, + -cb * vectorscope_gain * radius); + + int px = qRound(point.x()); + int py = qRound(point.y()); + if (px < 0 || px >= width() || py < 0 || py >= height()) { + continue; + } + + QRgb *dst_line = reinterpret_cast(buf.scanLine(py)); + QRgb cur = dst_line[px]; + int add = qRound(255.0f * vectorscope_intensity); + int nr = qMin(255, qRed(cur) + int(r * add)); + int ng = qMin(255, qGreen(cur) + int(g * add)); + int nb = qMin(255, qBlue(cur) + int(b * add)); + int na = qMax(qMax(nr, ng), nb); + dst_line[px] = qRgba(nr, ng, nb, na); + } + } + + p.setCompositionMode(QPainter::CompositionMode_Plus); + p.drawImage(0, 0, buf); + + // Draw overlay (circle, cross-hairs, targets) + QFont font = p.font(); + font.setPixelSize(10); + QFontMetrics font_metrics = QFontMetrics(font); + + p.setCompositionMode(QPainter::CompositionMode_SourceOver); + p.setPen(QColor(0, 153, 0)); + p.setFont(font); + + p.drawEllipse(center, radius, radius); + p.drawLine(QPointF(center.x() - radius, center.y()), + QPointF(center.x() + radius, center.y())); + p.drawLine(QPointF(center.x(), center.y() - radius), + QPointF(center.x(), center.y() + radius)); + + struct Target { + const char *label; + float angle; + }; + const Target targets[] = { + { "R", 0.0f }, { "Mg", 60.0f }, { "B", 120.0f }, + { "Cy", 180.0f }, { "G", 240.0f }, { "Yl", 300.0f }, + }; + + const float label_radius = radius + 12.0f; + const float marker_radius = radius * 0.72f; + constexpr float kPi = 3.14159265358979323846f; + + for (const Target &target : targets) { + float radians = target.angle * kPi / 180.0f; + QPointF direction(qCos(radians), -qSin(radians)); + QPointF marker = center + direction * marker_radius; + QPointF label_pos = center + direction * label_radius; + QString label = QString::fromUtf8(target.label); + + p.drawEllipse(marker, 3.0, 3.0); + p.drawText(label_pos.x() - + QtUtils::QFontMetricsWidth(font_metrics, label) * 0.5, + label_pos.y() + font_metrics.capHeight() * 0.5, label); + } +} + } diff --git a/app/widget/scope/vectorscope/vectorscope.h b/app/widget/scope/vectorscope/vectorscope.h index 2dbbe3eb3..9cc804351 100644 --- a/app/widget/scope/vectorscope/vectorscope.h +++ b/app/widget/scope/vectorscope/vectorscope.h @@ -38,6 +38,8 @@ protected: virtual ShaderCode GenerateShaderCode() override; virtual void DrawScope(TexturePtr managed_tex, QVariant pipeline) override; + + virtual void DrawScopeSoftware(QPainter &p, const QImage &image) override; }; } diff --git a/app/widget/scope/waveform/waveform.cpp b/app/widget/scope/waveform/waveform.cpp index a34827687..058546e9f 100644 --- a/app/widget/scope/waveform/waveform.cpp +++ b/app/widget/scope/waveform/waveform.cpp @@ -120,4 +120,97 @@ void WaveformScope::DrawScope(TexturePtr managed_tex, QVariant pipeline) p.drawLines(ire_lines); } +void WaveformScope::DrawScopeSoftware(QPainter &p, const QImage &image) +{ + const float waveform_scale = 0.80f; + const int waveform_dim_x = qCeil((width() - 1.0) * waveform_scale); + const int waveform_dim_y = qCeil((height() - 1.0) * waveform_scale); + const int waveform_start_dim_x = + qFloor(((width() - 1.0) - waveform_dim_x) / 2.0f); + const int waveform_start_dim_y = + qFloor(((height() - 1.0) - waveform_dim_y) / 2.0f); + const int waveform_end_dim_x = width() - 1 - waveform_start_dim_x; + + QImage buf(width(), height(), QImage::Format_ARGB32_Premultiplied); + buf.fill(Qt::transparent); + + const int src_w = image.width(); + const int src_h = image.height(); + + // Limit analysis resolution to keep CPU usage reasonable on large frames. + const int step_x = qMax(1, src_w / 512); + const int step_y = qMax(1, src_h / 512); + + for (int sy = 0; sy < src_h; sy += step_y) { + const uchar *src_line = image.constScanLine(sy); + for (int sx = 0; sx < src_w; sx += step_x) { + const uchar *src = src_line + sx * 4; + float r = src[0] / 255.0f; + float g = src[1] / 255.0f; + float b = src[2] / 255.0f; + + int scope_x = waveform_start_dim_x + + int((float(sx) / float(src_w)) * waveform_dim_x); + if (scope_x < waveform_start_dim_x || + scope_x >= waveform_end_dim_x) { + continue; + } + + auto mark = [&](float value, int add_r, int add_g, int add_b) { + int scope_y = waveform_start_dim_y + + int((1.0f - value) * waveform_dim_y); + if (scope_y < waveform_start_dim_y || + scope_y >= waveform_start_dim_y + waveform_dim_y) { + return; + } + QRgb *dst_line = reinterpret_cast(buf.scanLine(scope_y)); + QRgb cur = dst_line[scope_x]; + int nr = qMin(255, qRed(cur) + add_r); + int ng = qMin(255, qGreen(cur) + add_g); + int nb = qMin(255, qBlue(cur) + add_b); + int na = qMax(qMax(nr, ng), nb); + dst_line[scope_x] = qRgba(nr, ng, nb, na); + }; + + mark(r, 30, 0, 0); + mark(g, 0, 30, 0); + mark(b, 0, 0, 30); + } + } + + p.setCompositionMode(QPainter::CompositionMode_Plus); + p.drawImage(0, 0, buf); + + // Draw IRE line overlays + QFont font; + font.setPixelSize(10); + QFontMetrics font_metrics = QFontMetrics(font); + QString label; + float ire_increment = 0.1f; + int ire_steps = qRound(1.0 / ire_increment); + QVector ire_lines(ire_steps + 1); + int font_x_offset = 0; + int font_y_offset = font_metrics.capHeight() / 2.0f; + + p.setPen(QColor(0.0, 0.6 * 255.0, 0.0)); + p.setFont(font); + + for (int i = 0; i <= ire_steps; i++) { + ire_lines[i].setLine( + waveform_start_dim_x, + (waveform_dim_y * (i * ire_increment)) + waveform_start_dim_y, + waveform_end_dim_x, + (waveform_dim_y * (i * ire_increment)) + waveform_start_dim_y); + label = QString::number(1.0 - (i * ire_increment), 'f', 1); + font_x_offset = QtUtils::QFontMetricsWidth(font_metrics, label) + 4; + + p.drawText(waveform_start_dim_x - font_x_offset, + (waveform_dim_y * (i * ire_increment)) + + waveform_start_dim_y + font_y_offset, + label); + } + + p.drawLines(ire_lines); +} + } diff --git a/app/widget/scope/waveform/waveform.h b/app/widget/scope/waveform/waveform.h index 4cfc847ab..1b9dfd332 100644 --- a/app/widget/scope/waveform/waveform.h +++ b/app/widget/scope/waveform/waveform.h @@ -38,6 +38,8 @@ protected: virtual ShaderCode GenerateShaderCode() override; virtual void DrawScope(TexturePtr managed_tex, QVariant pipeline) override; + + virtual void DrawScopeSoftware(QPainter &p, const QImage &image) override; }; }