R7-A (Qwen 3.8 Max): oakengine/display.h rewritten to the POD contract from r7-pure-abi-plan.md - oak_video_params everywhere, opaque texture/frame handles with retain/free protocol (engine-heap control blocks), OakSharedBuffer refcounted wrapper for the QVariant playback path. All TexturePtr/FramePtr gone from app (47 sites). R7-B (Qwen 3.8 Max): liboakengine.so exports 3486 -> 19 C++ symbols (version script oakengine.ver: oakengine_* plus the documented oakgl/oakvulkan dlopen plugin ABI). oakengine-obj OBJECT library feeds both the shared lib and the test binaries (-rdynamic so dlopen'd backends resolve engine objects). Fix (Kimi K3): producer/consumer type mismatch - viewerdisplay unpacks OakSharedBufferPtr but viewer.cpp pushed raw void* handles, so no frame ever reached the display widget (all 5 vulkan viewer tests timed out with 'never received a texture'). Producers now wrap with oak_make_shared_frame / oak_make_shared_texture(retain). Verified: build 0 errors, ctest 45/45, nm U _ZN5olive = 0 in oak-editor/oak-render-worker/oak-cli, 19 exported C++ symbols in liboakengine.so (all documented plugin ABI).
535 lines
15 KiB
C++
535 lines
15 KiB
C++
/***
|
|
|
|
Olive - Non-Linear Video Editor
|
|
Copyright (C) 2022 Olive Team
|
|
Modifications Copyright (C) 2025 mikesolar
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
***/
|
|
|
|
#include "manageddisplay.h"
|
|
|
|
#include <QHBoxLayout>
|
|
#include <QMessageBox>
|
|
|
|
#include "panel/panelmanager.h"
|
|
#include "oakengine/videoparams.h"
|
|
#include "oakengine/renderer.h"
|
|
#include "oakengine/display.h"
|
|
#include "widget/viewer/vieweroutpututils.h"
|
|
#include "common/configwrapper.h"
|
|
|
|
namespace olive
|
|
{
|
|
|
|
#define super QWidget
|
|
|
|
ManagedDisplayWidget::ManagedDisplayWidget(QWidget *parent)
|
|
: QWidget(parent)
|
|
, color_manager_(nullptr)
|
|
, color_service_(nullptr)
|
|
, is_backend_neutral_(false)
|
|
{
|
|
QHBoxLayout *layout = new QHBoxLayout(this);
|
|
layout->setSpacing(0);
|
|
layout->setContentsMargins(0, 0, 0, 0);
|
|
|
|
// Create renderer
|
|
#ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
|
|
{
|
|
const QString backend = oak_query_string([](char *buf, int sz) {
|
|
int backend = oakengine_render_manager_requested_backend();
|
|
return oakengine_render_manager_backend_to_string(backend, buf,
|
|
sz);
|
|
});
|
|
attached_renderer_ =
|
|
oakengine_display_renderer_create_dynamic(
|
|
backend.toUtf8().constData(), this);
|
|
if (!attached_renderer_) {
|
|
qWarning()
|
|
<< "Failed to load dynamic render backend for viewer, falling back to OpenGL";
|
|
attached_renderer_ =
|
|
oakengine_display_renderer_create_opengl(this);
|
|
}
|
|
}
|
|
#else
|
|
attached_renderer_ =
|
|
oakengine_display_renderer_create_opengl(this);
|
|
#endif
|
|
|
|
if (oakengine_display_renderer_is_open_gl(attached_renderer_)) {
|
|
// OpenGL path
|
|
inner_widget_ = new ManagedDisplayWidgetOpenGL();
|
|
inner_widget_->setAttribute(Qt::WA_TranslucentBackground, false);
|
|
connect(static_cast<ManagedDisplayWidgetOpenGL *>(inner_widget_),
|
|
&ManagedDisplayWidgetOpenGL::on_init, this,
|
|
&ManagedDisplayWidget::on_init, Qt::DirectConnection);
|
|
connect(static_cast<ManagedDisplayWidgetOpenGL *>(inner_widget_),
|
|
&ManagedDisplayWidgetOpenGL::on_destroy, this,
|
|
&ManagedDisplayWidget::on_destroy, Qt::DirectConnection);
|
|
connect(static_cast<ManagedDisplayWidgetOpenGL *>(inner_widget_),
|
|
&ManagedDisplayWidgetOpenGL::on_paint, this,
|
|
&ManagedDisplayWidget::on_paint, Qt::DirectConnection);
|
|
connect(static_cast<ManagedDisplayWidgetOpenGL *>(inner_widget_),
|
|
&ManagedDisplayWidgetOpenGL::frameSwapped, this,
|
|
&ManagedDisplayWidget::frame_swapped, Qt::DirectConnection);
|
|
|
|
inner_widget_->installEventFilter(this);
|
|
|
|
// Create widget wrapper for OpenGL window
|
|
#ifdef USE_QOPENGLWINDOW
|
|
wrapper_ = QWidget::createWindowContainer(
|
|
static_cast<ManagedDisplayWidgetOpenGL *>(inner_widget_));
|
|
#else
|
|
wrapper_ = inner_widget_;
|
|
#endif
|
|
layout->addWidget(wrapper_);
|
|
} else {
|
|
// Backend-neutral path (Vulkan, etc.)
|
|
is_backend_neutral_ = true;
|
|
auto *bn_widget = new ManagedDisplayWidgetBackendNeutral(this);
|
|
inner_widget_ = bn_widget;
|
|
inner_widget_->setAttribute(Qt::WA_OpaquePaintEvent);
|
|
inner_widget_->installEventFilter(this);
|
|
connect(bn_widget, &ManagedDisplayWidgetBackendNeutral::on_paint, this,
|
|
&ManagedDisplayWidget::on_paint, Qt::DirectConnection);
|
|
wrapper_ = inner_widget_;
|
|
layout->addWidget(wrapper_);
|
|
}
|
|
}
|
|
|
|
ManagedDisplayWidget::~ManagedDisplayWidget()
|
|
{
|
|
if (!is_backend_neutral_) {
|
|
MANAGEDDISPLAYWIDGET_DEFAULT_DESTRUCTOR_INNER;
|
|
|
|
disconnect(static_cast<ManagedDisplayWidgetOpenGL *>(inner_widget_),
|
|
&ManagedDisplayWidgetOpenGL::on_destroy, this,
|
|
&ManagedDisplayWidget::on_destroy);
|
|
} else {
|
|
on_destroy();
|
|
}
|
|
}
|
|
|
|
void ManagedDisplayWidget::connect_color_manager(OakEngineColorManager *color_manager)
|
|
{
|
|
if (color_manager_ == color_manager) {
|
|
return;
|
|
}
|
|
|
|
if (color_manager_ != nullptr) {
|
|
for (int64_t id : color_subs_) {
|
|
oakengine_event_unsubscribe(id);
|
|
}
|
|
color_subs_.clear();
|
|
}
|
|
|
|
color_manager_ = color_manager;
|
|
|
|
if (color_manager_ != nullptr) {
|
|
auto sub = [this](int ev) {
|
|
int64_t id = oakengine_event_subscribe(
|
|
color_manager_, ev,
|
|
[](const oakengine_event *, void *userdata) {
|
|
static_cast<ManagedDisplayWidget *>(userdata)
|
|
->color_config_changed();
|
|
},
|
|
this);
|
|
if (id > 0) {
|
|
color_subs_.append(id);
|
|
}
|
|
};
|
|
sub(OAKENGINE_EVENT_COLOR_MANAGER_CONFIG_CHANGED);
|
|
sub(OAKENGINE_EVENT_COLOR_MANAGER_REFERENCE_SPACE_CHANGED);
|
|
}
|
|
|
|
color_config_changed();
|
|
emit color_manager_changed(color_manager_);
|
|
}
|
|
|
|
OakEngineColorManager *ManagedDisplayWidget::color_manager() const
|
|
{
|
|
return color_manager_;
|
|
}
|
|
|
|
void ManagedDisplayWidget::disconnect_color_manager()
|
|
{
|
|
connect_color_manager(nullptr);
|
|
}
|
|
|
|
const ColorTransform &ManagedDisplayWidget::get_color_transform() const
|
|
{
|
|
return color_transform_;
|
|
}
|
|
|
|
Menu *ManagedDisplayWidget::get_color_space_menu(QMenu *parent, bool auto_connect)
|
|
{
|
|
QStringList colorspaces = oak_query_string_list(
|
|
[this]() {
|
|
return oakengine_color_manager_colorspace_count(color_manager_);
|
|
},
|
|
[this](int i, char *buf, int size) {
|
|
return oakengine_color_manager_colorspace_at(color_manager_, i, buf,
|
|
size);
|
|
});
|
|
|
|
Menu *ocio_colorspace_menu = new Menu(tr("Color Space"), parent);
|
|
|
|
if (auto_connect) {
|
|
connect(ocio_colorspace_menu, &Menu::triggered, this,
|
|
&ManagedDisplayWidget::menu_colorspace_select);
|
|
}
|
|
|
|
foreach (const QString &c, colorspaces) {
|
|
QAction *action = ocio_colorspace_menu->addAction(c);
|
|
action->setCheckable(true);
|
|
action->setChecked(color_transform_.output() == c);
|
|
action->setData(c);
|
|
}
|
|
|
|
return ocio_colorspace_menu;
|
|
}
|
|
|
|
void ManagedDisplayWidget::color_config_changed()
|
|
{
|
|
if (!color_manager_) {
|
|
color_service_ = nullptr;
|
|
return;
|
|
}
|
|
|
|
// When no explicit transform has been chosen, default to the config's
|
|
// display/view transform so the viewer shows a sensible image. Otherwise
|
|
// an empty transform falls back to the project's default input colorspace,
|
|
// which is usually a scene-referred space (e.g. ACEScg / Linear) and makes
|
|
// the picture look raw/wrong on a monitor.
|
|
if (color_transform_.output().isEmpty()) {
|
|
QString display = oak_query_string([this](char *buf, int size) {
|
|
return oakengine_color_manager_default_display(color_manager_, buf,
|
|
size);
|
|
});
|
|
QString view = oak_query_string([this, &display](char *buf, int size) {
|
|
QByteArray d = display.toUtf8();
|
|
return oakengine_color_manager_default_view(color_manager_,
|
|
d.constData(), buf,
|
|
size);
|
|
});
|
|
set_color_transform(oak_compliant_transform(
|
|
reinterpret_cast<olive::ColorManager *>(color_manager_),
|
|
ColorTransform(display, view, QString()), true));
|
|
} else {
|
|
set_color_transform(oak_compliant_transform(
|
|
reinterpret_cast<olive::ColorManager *>(color_manager_),
|
|
color_transform_, false));
|
|
}
|
|
}
|
|
|
|
ColorProcessorHandlePtr ManagedDisplayWidget::color_service()
|
|
{
|
|
return color_service_;
|
|
}
|
|
|
|
void ManagedDisplayWidget::show_default_context_menu()
|
|
{
|
|
Menu m(this);
|
|
|
|
if (color_manager_) {
|
|
m.addMenu(get_color_space_menu(&m));
|
|
m.addSeparator();
|
|
m.addMenu(get_display_menu(&m));
|
|
m.addMenu(get_view_menu(&m));
|
|
m.addMenu(get_look_menu(&m));
|
|
} else {
|
|
QAction *a = m.addAction(tr("No color manager connected"));
|
|
a->setEnabled(false);
|
|
}
|
|
|
|
m.exec(QCursor::pos());
|
|
}
|
|
|
|
void ManagedDisplayWidget::menu_display_select(QAction *action)
|
|
{
|
|
const ColorTransform &old_transform = get_color_transform();
|
|
|
|
ColorTransform new_transform = oak_compliant_transform(
|
|
reinterpret_cast<olive::ColorManager *>(color_manager_),
|
|
ColorTransform(action->data().toString(), old_transform.view(),
|
|
old_transform.look()));
|
|
|
|
set_color_transform(new_transform);
|
|
}
|
|
|
|
void ManagedDisplayWidget::menu_view_select(QAction *action)
|
|
{
|
|
const ColorTransform &old_transform = get_color_transform();
|
|
|
|
ColorTransform new_transform = oak_compliant_transform(
|
|
reinterpret_cast<olive::ColorManager *>(color_manager_),
|
|
ColorTransform(old_transform.display(), action->data().toString(),
|
|
old_transform.look()));
|
|
|
|
set_color_transform(new_transform);
|
|
}
|
|
|
|
void ManagedDisplayWidget::menu_look_select(QAction *action)
|
|
{
|
|
const ColorTransform &old_transform = get_color_transform();
|
|
|
|
ColorTransform new_transform = oak_compliant_transform(
|
|
reinterpret_cast<olive::ColorManager *>(color_manager_),
|
|
ColorTransform(old_transform.display(), old_transform.view(),
|
|
action->data().toString()));
|
|
|
|
set_color_transform(new_transform);
|
|
}
|
|
|
|
void ManagedDisplayWidget::menu_colorspace_select(QAction *action)
|
|
{
|
|
set_color_transform(oak_compliant_transform(
|
|
reinterpret_cast<olive::ColorManager *>(color_manager_),
|
|
ColorTransform(action->data().toString())));
|
|
}
|
|
|
|
void ManagedDisplayWidget::on_destroy()
|
|
{
|
|
oakengine_display_renderer_destroy(attached_renderer_);
|
|
}
|
|
|
|
void ManagedDisplayWidget::set_color_transform(const ColorTransform &transform)
|
|
{
|
|
color_transform_ = transform;
|
|
|
|
setup_color_processor();
|
|
|
|
color_processor_changed_event();
|
|
}
|
|
|
|
void ManagedDisplayWidget::on_init()
|
|
{
|
|
if (!is_backend_neutral_) {
|
|
QOpenGLContext *context =
|
|
static_cast<ManagedDisplayWidgetOpenGL *>(inner_widget_)->context();
|
|
oakengine_display_renderer_init(attached_renderer_, context);
|
|
} else {
|
|
oakengine_display_renderer_init(attached_renderer_, nullptr);
|
|
}
|
|
}
|
|
|
|
void ManagedDisplayWidget::enable_default_context_menu()
|
|
{
|
|
connect(this, &ManagedDisplayWidget::customContextMenuRequested, this,
|
|
&ManagedDisplayWidget::show_default_context_menu);
|
|
}
|
|
|
|
void ManagedDisplayWidget::color_processor_changed_event()
|
|
{
|
|
update();
|
|
}
|
|
|
|
void ManagedDisplayWidget::make_current()
|
|
{
|
|
if (!is_backend_neutral_) {
|
|
static_cast<ManagedDisplayWidgetOpenGL *>(inner_widget_)->makeCurrent();
|
|
}
|
|
}
|
|
|
|
void ManagedDisplayWidget::done_current()
|
|
{
|
|
if (!is_backend_neutral_) {
|
|
static_cast<ManagedDisplayWidgetOpenGL *>(inner_widget_)->doneCurrent();
|
|
}
|
|
}
|
|
|
|
QPaintDevice *ManagedDisplayWidget::paint_device() const
|
|
{
|
|
return inner_widget_;
|
|
}
|
|
|
|
void ManagedDisplayWidget::set_inner_mouse_tracking(bool e)
|
|
{
|
|
if (wrapper_) {
|
|
wrapper_->setMouseTracking(e);
|
|
}
|
|
}
|
|
|
|
oak_video_params ManagedDisplayWidget::get_viewport_params() const
|
|
{
|
|
int device_width = width() * devicePixelRatioF();
|
|
int device_height = height() * devicePixelRatioF();
|
|
int device_format = OAK_CONFIG("OfflinePixelFormat").toInt();
|
|
oak_video_params pod = {};
|
|
pod.width = device_width;
|
|
pod.height = device_height;
|
|
pod.format = device_format;
|
|
return pod;
|
|
}
|
|
|
|
void ManagedDisplayWidget::update()
|
|
{
|
|
if (inner_widget_) {
|
|
inner_widget_->update();
|
|
}
|
|
}
|
|
|
|
bool ManagedDisplayWidget::eventFilter(QObject *o, QEvent *e)
|
|
{
|
|
if (o != inner_widget_) {
|
|
return super::eventFilter(o, e);
|
|
}
|
|
|
|
switch (e->type()) {
|
|
case QEvent::FocusIn:
|
|
// HACK: QWindow focus isn't accounted for in QApplication::focusChanged, so we handle it
|
|
// manually here.
|
|
PanelManager::instance()->focus_changed(nullptr, this);
|
|
break;
|
|
case QEvent::ContextMenu: {
|
|
QContextMenuEvent *ctx = static_cast<QContextMenuEvent *>(e);
|
|
emit customContextMenuRequested(ctx->pos());
|
|
return true;
|
|
}
|
|
case QEvent::MouseButtonPress: {
|
|
// HACK: QWindows don't seem to receive ContextMenu events on right click (only when pressing
|
|
// the menu button on the keyboard) so we handle it manually here
|
|
/*QMouseEvent *ev = static_cast<QMouseEvent*>(e);
|
|
if (ev->button() == Qt::RightButton) {
|
|
emit customContextMenuRequested(ev->pos());
|
|
return true;
|
|
}*/
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return super::eventFilter(o, e);
|
|
}
|
|
|
|
Menu *ManagedDisplayWidget::get_display_menu(QMenu *parent, bool auto_connect)
|
|
{
|
|
QStringList displays = oak_query_string_list(
|
|
[this]() {
|
|
return oakengine_color_manager_display_count(color_manager_);
|
|
},
|
|
[this](int i, char *buf, int size) {
|
|
return oakengine_color_manager_display_at(color_manager_, i, buf,
|
|
size);
|
|
});
|
|
|
|
Menu *ocio_display_menu = new Menu(tr("Display"), parent);
|
|
|
|
if (auto_connect) {
|
|
connect(ocio_display_menu, &Menu::triggered, this,
|
|
&ManagedDisplayWidget::menu_display_select);
|
|
}
|
|
|
|
foreach (const QString &d, displays) {
|
|
QAction *action = ocio_display_menu->addAction(d);
|
|
action->setCheckable(true);
|
|
action->setChecked(color_transform_.display() == d);
|
|
action->setData(d);
|
|
}
|
|
|
|
return ocio_display_menu;
|
|
}
|
|
|
|
Menu *ManagedDisplayWidget::get_view_menu(QMenu *parent, bool auto_connect)
|
|
{
|
|
QByteArray disp = color_transform_.display().toUtf8();
|
|
QStringList views = oak_query_string_list(
|
|
[this, &disp]() {
|
|
return oakengine_color_manager_view_count(color_manager_,
|
|
disp.constData());
|
|
},
|
|
[this, &disp](int i, char *buf, int size) {
|
|
return oakengine_color_manager_view_at(color_manager_,
|
|
disp.constData(), i, buf,
|
|
size);
|
|
});
|
|
|
|
Menu *ocio_view_menu = new Menu(tr("View"), parent);
|
|
|
|
if (auto_connect) {
|
|
connect(ocio_view_menu, &Menu::triggered, this,
|
|
&ManagedDisplayWidget::menu_view_select);
|
|
}
|
|
|
|
foreach (const QString &v, views) {
|
|
QAction *action = ocio_view_menu->addAction(v);
|
|
action->setCheckable(true);
|
|
action->setChecked(color_transform_.view() == v);
|
|
action->setData(v);
|
|
}
|
|
|
|
return ocio_view_menu;
|
|
}
|
|
|
|
Menu *ManagedDisplayWidget::get_look_menu(QMenu *parent, bool auto_connect)
|
|
{
|
|
QStringList looks = oak_query_string_list(
|
|
[this]() {
|
|
return oakengine_color_manager_look_count(color_manager_);
|
|
},
|
|
[this](int i, char *buf, int size) {
|
|
return oakengine_color_manager_look_at(color_manager_, i, buf,
|
|
size);
|
|
});
|
|
|
|
Menu *ocio_look_menu = new Menu(tr("Look"), parent);
|
|
|
|
if (auto_connect) {
|
|
connect(ocio_look_menu, &Menu::triggered, this,
|
|
&ManagedDisplayWidget::menu_look_select);
|
|
}
|
|
|
|
// Setup "no look" action
|
|
QAction *no_look_action = ocio_look_menu->addAction(tr("(None)"));
|
|
no_look_action->setCheckable(true);
|
|
no_look_action->setChecked(color_transform_.look().isEmpty());
|
|
no_look_action->setData(QString());
|
|
|
|
// Set up the rest of the looks
|
|
foreach (const QString &l, looks) {
|
|
QAction *action = ocio_look_menu->addAction(l);
|
|
action->setCheckable(true);
|
|
action->setChecked(color_transform_.look() == l);
|
|
action->setData(l);
|
|
}
|
|
|
|
return ocio_look_menu;
|
|
}
|
|
|
|
void ManagedDisplayWidget::setup_color_processor()
|
|
{
|
|
color_service_ = nullptr;
|
|
|
|
if (color_manager_) {
|
|
// (Re)create color processor. The facade never throws: OCIO failures
|
|
// are caught inside the engine and surface as an invalid processor.
|
|
QString ref_cs = oak_query_string([this](char *buf, int size) {
|
|
return oakengine_color_manager_reference_color_space(
|
|
color_manager_, buf, size);
|
|
});
|
|
color_service_ = oak_make_color_processor(
|
|
reinterpret_cast<olive::ColorManager *>(color_manager_),
|
|
ref_cs, color_transform_);
|
|
} else {
|
|
color_service_ = nullptr;
|
|
}
|
|
|
|
emit color_processor_changed(color_service_);
|
|
}
|
|
|
|
}
|