Files
Mike-Solar 712badbaa7 build/ci: green builds and tests on all three platforms
Compiler hygiene (all platforms):
- Silence warnings across the tree: missing override, -Wreorder ctor
  init, -Wshadow, -Wsign-compare, missing switch cases, unused
  functions/captures, Qt 6.11 deprecations (QMouseEvent/QDropEvent
  accessors, qAsConst, Q_FOREACH over non-shared containers,
  AA_UseHighDpiPixmaps) and the .bak/ backup tree removal.
- Fix regressions from the cleanup: missing clip decls in
  capi/timeline.cpp, plugin.cpp rename fallout, panel setFocus
  ambiguity, QGraphicsItem::pos vs event->position(), duplicate
  k_push_button case, boolean test variable.

Windows:
- Qt portability: CommandLineParser is_set/add_option, QTimeZone
  systemTimeZone (QTimeZone::LocalTime is 6.11-only), k_progress_* enum.
- Linking: stop adding oakengine to OLIVE_LIBRARIES (import lib plus
  oakengine-obj caused multiple definitions); add OAKENGINE_STATIC so
  internal consumers no longer reference __imp_* stubs.
- oakengine.ver: export olive::Renderer typeinfo so liboakgl.so can be
  dlopened (Linux), DynamicRenderer no longer dlcloses backend libraries
  (crash in RenderManager's dtor calling into unmapped memory).
- OTIO runtime: copy DLLs next to every binary on Windows instead of
  relying on PATH (0xc0000135 in gtest discovery).
- Headless GL: the runner only has GDI OpenGL 1.1, killing every render
  worker. Deploy Mesa llvmpipe as opengl32sw.dll (Qt's software-GL
  channel) with QT_OPENGL=software, and let QT_OPENGL override the
  AA_UseDesktopOpenGL default. ExportTask fails fast after 8 consecutive
  undelivered frames instead of segfaulting or grinding forever;
  FFmpegEncoder::write_frame tolerates null frames.
- Tests: GetTempPathA+PID temp dirs, GetLongPathNameA for 8.3 names,
  forward-slash normalization when comparing project filenames.

Linux:
- Install libshaderc-dev so oakvulkan compiles GLSL (Vulkan tests).
- Accept UNORM floor-or-round (63/64) in the blit ping-pong test.
- Skip MainWindow construction test on the offscreen QPA (cannot paint
  QOpenGLWidget).

Also: oak_cli_transcode gets a 300s ctest timeout, worker logs GL
context version and LoadGraph/render_frame stages, and
docs/plans/eliminate-event-bridge-issues.md (English translation).
2026-08-04 21:34:31 +08:00

1109 lines
29 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 "openglrenderer.h"
#include <iostream>
#include <QDateTime>
#include <QDebug>
#include <QOpenGLExtraFunctions>
#include <QRegularExpression>
#include "common/filefunctions.h"
#if !defined(OAK_RENDER_BACKEND_PLUGIN)
#include "config/config.h"
#endif
#include "render/job/shaderjob.h"
namespace olive
{
const int OpenGLRenderer::k_texture_cache_max_size = 5000;
const QVector<GLfloat> blit_vertices = { -1.0f, -1.0f, 0.0f, 1.0f, -1.0f,
0.0f, 1.0f, 1.0f, 0.0f,
-1.0f, -1.0f, 0.0f, -1.0f, 1.0f,
0.0f, 1.0f, 1.0f, 0.0f };
const QVector<GLfloat> blit_texcoords = { 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f,
0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f };
class ErrorPrinter {
public:
ErrorPrinter(const char *name, QOpenGLFunctions *f)
{
GLuint err = f->glGetError();
if (err > 0)
qDebug() << name << "entered with" << err;
name_ = name;
functions_ = f;
}
~ErrorPrinter()
{
GLuint err = functions_->glGetError();
if (err > 0)
qDebug() << name_ << "exited with" << err;
}
private:
const char *name_;
QOpenGLFunctions *functions_;
};
#define OAK_PRINT_GL_ERRORS ErrorPrinter __e(__FUNCTION__, functions_)
#define OAK_GL_PREAMBLE //QMutexLocker __l(&global_opengl_mutex);
//QMutex global_opengl_mutex;
OpenGLRenderer::OpenGLRenderer(QObject *parent)
: Renderer(parent)
, context_(nullptr)
, functions_(nullptr)
, surface_(nullptr, this)
, framebuffer_(0)
{
}
OpenGLRenderer::~OpenGLRenderer()
{
destroy();
post_destroy();
}
void OpenGLRenderer::init(QOpenGLContext *existing_ctx)
{
if (context_) {
qCritical() << "Can't initialize already initialized OpenGLRenderer";
return;
}
context_ = existing_ctx;
}
bool OpenGLRenderer::init()
{
OAK_GL_PREAMBLE;
if (context_) {
qCritical() << "Can't initialize already initialized OpenGLRenderer";
return false;
}
context_ = new QOpenGLContext(this);
context_->setShareContext(QOpenGLContext::globalShareContext());
if (!context_->create()) {
qCritical() << "Failed to create OpenGL context";
return false;
}
context_->moveToThread(this->thread());
return true;
}
void OpenGLRenderer::post_destroy()
{
// Destroy surface if we created it
if (surface_.isValid()) {
surface_.destroy();
}
}
void OpenGLRenderer::post_init()
{
OAK_GL_PREAMBLE;
if (!context_) {
qWarning() << __FUNCTION__ << "called without an OpenGL context";
return;
}
if (context_->thread() != QThread::currentThread()) {
qWarning() << __FUNCTION__
<< "called from the wrong thread for this OpenGL context";
return;
}
// Create the offscreen surface in the thread that will actually use it.
// When OpenGLRenderer is moved to a render thread, surface_ follows as a
// child QObject; creating it here avoids making the context current on a
// surface whose platform backing still belongs to the construction thread,
// which crashes drivers on the first GL call.
if (context_->parent() == this && !surface_.isValid()) {
surface_.create();
}
if (QOpenGLContext::currentContext() == context_) {
functions_ = context_->functions();
}
}
void OpenGLRenderer::destroy_internal()
{
// context_ is guarded: if a caller-owned context was already destroyed,
// this is null and there is nothing GL-side left to release.
if (context_) {
OAK_GL_PREAMBLE;
if (functions_ && framebuffer_) {
functions_->glDeleteFramebuffers(1, &framebuffer_);
}
// Delete context if it belongs to us
if (context_->parent() == this) {
delete context_.data();
}
}
// functions_ is derived from the context, so it is unusable once the
// context is gone; reset both regardless of the path taken above.
framebuffer_ = 0;
context_ = nullptr;
functions_ = nullptr;
}
void OpenGLRenderer::clear_destination(Texture *texture, double r, double g,
double b, double a)
{
OAK_GL_PREAMBLE;
if (!ensure_context_current(__FUNCTION__)) {
return;
}
if (texture) {
attach_texture_as_destination(texture->id());
}
clear_destination_internal(r, g, b, a);
if (texture) {
detach_texture_as_destination();
}
}
QVariant OpenGLRenderer::create_native_texture(int width, int height, int depth,
PixelFormat format,
int channel_count,
const void *data, int linesize)
{
OAK_GL_PREAMBLE;
if (!ensure_context_current(__FUNCTION__)) {
return QVariant();
}
bool is_3d = depth > 1;
// Generate new texture
GLuint texture;
functions_->glGenTextures(1, &texture);
texture_params_.insert(texture,
{ width, height, depth, format, channel_count });
functions_->glPixelStorei(GL_UNPACK_ROW_LENGTH, linesize);
GLenum target_current = is_3d ? GL_TEXTURE_BINDING_3D :
GL_TEXTURE_BINDING_2D;
GLenum target = is_3d ? GL_TEXTURE_3D : GL_TEXTURE_2D;
GLint current_tex;
functions_->glGetIntegerv(target_current, &current_tex);
functions_->glBindTexture(target, texture);
if (is_3d) {
context_->extraFunctions()->glTexImage3D(
target, 0, get_internal_format(format, channel_count), width, height,
depth, 0, get_pixel_format(channel_count), get_pixel_type(format),
data);
} else {
functions_->glTexImage2D(
target, 0, get_internal_format(format, channel_count), width, height,
0, get_pixel_format(channel_count), get_pixel_type(format), data);
}
functions_->glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
functions_->glBindTexture(target, current_tex);
return texture;
}
void OpenGLRenderer::attach_texture_as_destination(const QVariant &texture)
{
OAK_PRINT_GL_ERRORS;
if (!framebuffer_) {
functions_->glGenFramebuffers(1, &framebuffer_);
}
functions_->glBindFramebuffer(GL_FRAMEBUFFER, framebuffer_);
functions_->glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
GL_TEXTURE_2D, texture.value<GLuint>(),
0);
}
void OpenGLRenderer::detach_texture_as_destination()
{
// QOpenGLWidget renders to a non-zero default FBO.
const GLuint default_fbo = context_ ? context_->defaultFramebufferObject() :
0;
functions_->glBindFramebuffer(GL_FRAMEBUFFER, default_fbo);
}
void OpenGLRenderer::destroy_native_texture(QVariant texture)
{
if (!ensure_context_current(__FUNCTION__)) {
return;
}
GLuint t = texture.value<GLuint>();
if (t > 0) {
functions_->glDeleteTextures(1, &t);
}
}
QVariant OpenGLRenderer::create_native_shader(ShaderCode code)
{
OAK_GL_PREAMBLE;
if (!ensure_context_current(__FUNCTION__)) {
return QVariant();
}
OAK_PRINT_GL_ERRORS;
GLuint vert = compile_shader(GL_VERTEX_SHADER, code.vert_code());
GLuint frag = compile_shader(GL_FRAGMENT_SHADER, code.frag_code());
GLuint program = 0;
if (frag && vert) {
program = functions_->glCreateProgram();
functions_->glAttachShader(program, frag);
functions_->glAttachShader(program, vert);
functions_->glLinkProgram(program);
GLint success;
functions_->glGetProgramiv(program, GL_LINK_STATUS, &success);
if (!success) {
qWarning() << "Failed to link OpenGL shader program";
functions_->glDeleteProgram(program);
program = 0;
}
}
functions_->glDeleteShader(frag);
functions_->glDeleteShader(vert);
return program;
}
void OpenGLRenderer::destroy_native_shader(QVariant shader)
{
OAK_GL_PREAMBLE;
if (!ensure_context_current(__FUNCTION__)) {
return;
}
GLuint program = shader.value<GLuint>();
functions_->glDeleteProgram(program);
}
void OpenGLRenderer::upload_to_texture(const QVariant &handle,
const VideoParams &p, const void *data,
int linesize)
{
OAK_GL_PREAMBLE;
GLuint t = handle.value<GLuint>();
bool is_3d = p.is_3d();
GLenum tex_type = !is_3d ? GL_TEXTURE_2D : GL_TEXTURE_3D;
GLenum tex_binding = !is_3d ? GL_TEXTURE_BINDING_2D : GL_TEXTURE_BINDING_3D;
// Store currently bound texture so it can be restored later
GLint current_tex;
functions_->glGetIntegerv(tex_binding, &current_tex);
functions_->glBindTexture(tex_type, t);
functions_->glPixelStorei(GL_UNPACK_ROW_LENGTH, linesize);
{
OAK_PRINT_GL_ERRORS;
if (!is_3d) {
functions_->glTexSubImage2D(tex_type, 0, 0, 0, p.effective_width(),
p.effective_height(),
get_pixel_format(p.channel_count()),
get_pixel_type(p.format()), data);
} else {
context_->extraFunctions()->glTexSubImage3D(
tex_type, 0, 0, 0, 0, p.effective_width(), p.effective_height(),
p.effective_depth(), get_pixel_format(p.channel_count()),
get_pixel_type(p.format()), data);
}
}
functions_->glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
functions_->glBindTexture(tex_type, current_tex);
}
void OpenGLRenderer::download_from_texture(const QVariant &id,
const VideoParams &p, void *data,
int linesize)
{
OAK_GL_PREAMBLE;
if (!ensure_context_current(__FUNCTION__)) {
return;
}
GLuint texture_id = id.value<GLuint>();
if (!texture_id || !functions_->glIsTexture(texture_id)) {
qWarning() << "DownloadFromTexture called with invalid texture";
return;
}
GLint current_tex;
functions_->glGetIntegerv(GL_TEXTURE_BINDING_2D, &current_tex);
attach_texture_as_destination(id);
GLenum status = functions_->glCheckFramebufferStatus(GL_FRAMEBUFFER);
if (status != GL_FRAMEBUFFER_COMPLETE) {
qWarning() << "DownloadFromTexture framebuffer incomplete" << status;
detach_texture_as_destination();
return;
}
functions_->glPixelStorei(GL_PACK_ROW_LENGTH, linesize);
// Ensure all rendering is complete before reading back on TBDR architectures (macOS)
functions_->glFinish();
{
OAK_PRINT_GL_ERRORS;
functions_->glReadPixels(0, 0, p.effective_width(),
p.effective_height(),
get_pixel_format(p.channel_count()),
get_pixel_type(p.format()), data);
}
functions_->glPixelStorei(GL_PACK_ROW_LENGTH, 0);
detach_texture_as_destination();
functions_->glBindTexture(GL_TEXTURE_2D, current_tex);
}
void OpenGLRenderer::flush()
{
OAK_GL_PREAMBLE;
if (!ensure_context_current(__FUNCTION__)) {
return;
}
#if !defined(OAK_RENDER_BACKEND_PLUGIN)
if (OAK_CONFIG("UseGLFinish").toBool()) {
functions_->glFinish();
return;
}
#endif
#if defined(Q_OS_MAC)
// The dynamically loaded OpenGL backend cannot depend on the editor Config
// singleton because that pulls UI/Core symbols into the plugin. This
// platform default also preserves the previous macOS-safe behavior for
// texture consumers on TBDR drivers.
functions_->glFinish();
#else
functions_->glFlush();
#endif
}
// Adapts the generic Renderer output attachment hook to OpenGL's framebuffer
// attachment path used by OFX OpenGL rendering.
void OpenGLRenderer::attach_output_texture(olive::Texture *texture)
{
if (!ensure_context_current(__FUNCTION__)) {
return;
}
if (texture) {
attach_texture_as_destination(texture->id());
}
}
// Clears the framebuffer attachment installed by AttachOutputTexture().
void OpenGLRenderer::detach_output_texture()
{
if (!ensure_context_current(__FUNCTION__)) {
return;
}
detach_texture_as_destination();
}
Color OpenGLRenderer::get_pixel_from_texture(Texture *texture, const QPointF &pt)
{
if (!texture || !ensure_context_current(__FUNCTION__)) {
return Color();
}
attach_texture_as_destination(texture->id());
QByteArray data(VideoParams::get_bytes_per_pixel(texture->format(),
texture->channel_count()),
Qt::Uninitialized);
functions_->glReadPixels(pt.x(), pt.y(), 1, 1,
get_pixel_format(texture->channel_count()),
get_pixel_type(texture->format()), data.data());
Color c = Color::from_data(data.data(), texture->format(),
texture->channel_count());
if (texture->channel_count() == VideoParams::k_rgb_channel_count) {
// No alpha channel, set to 1.0
c.set_alpha(1.0);
}
detach_texture_as_destination();
return c;
}
struct TextureToBind {
TexturePtr texture;
Texture::Interpolation interpolation;
};
void OpenGLRenderer::blit(QVariant s, AcceleratedJob &a_job,
Texture *destination, VideoParams destination_params,
bool clear_destination)
{
OAK_GL_PREAMBLE;
if (!ensure_context_current(__FUNCTION__)) {
return;
}
try {
if (!destination) {
// Ensure we're drawing to the default framebuffer for this context.
GLuint fbo = context_ ? context_->defaultFramebufferObject() : 0;
functions_->glBindFramebuffer(GL_FRAMEBUFFER, fbo);
}
ShaderJob &s_job = dynamic_cast<ShaderJob &>(a_job);
ShaderJob job(s_job);
// If this node is iterative, we'll pick up which input here
QMap<QString, GLuint> texture_index_map;
QVector<TextureToBind> textures_to_bind;
GLuint shader = s.value<GLuint>();
functions_->glUseProgram(shader);
for (auto it = job.get_values().constBegin();
it != job.get_values().constEnd(); it++) {
// See if the shader has takes this parameter as an input
GLint variable_location = functions_->glGetUniformLocation(
shader, it.key().toUtf8().constData());
if (variable_location == -1) {
continue;
}
// This variable is used in the shader, let's set it
const NodeValue &value = it.value();
// Arrays are not currently supported in this system
if (value.array()) {
continue;
}
switch (value.type()) {
case NodeValue::k_int:
// kInt technically specifies a LongLong, but OpenGL doesn't support those. This may lead to
// over/underflows if the number is large enough, but the likelihood of that is quite low.
functions_->glUniform1i(variable_location, value.to_int());
break;
default:
break;
case NodeValue::k_float:
// kFloat technically specifies a double but as above, OpenGL doesn't support those.
functions_->glUniform1f(variable_location, value.to_double());
break;
case NodeValue::k_vec2: {
QVector2D v = value.to_vec2();
functions_->glUniform2fv(variable_location, 1,
reinterpret_cast<const GLfloat *>(&v));
break;
}
case NodeValue::k_vec3: {
QVector3D v = value.to_vec3();
functions_->glUniform3fv(variable_location, 1,
reinterpret_cast<const GLfloat *>(&v));
break;
}
case NodeValue::k_vec4: {
QVector4D v = value.to_vec4();
functions_->glUniform4fv(variable_location, 1,
reinterpret_cast<const GLfloat *>(&v));
break;
}
case NodeValue::k_matrix:
functions_->glUniformMatrix4fv(variable_location, 1, false,
value.to_matrix().constData());
break;
case NodeValue::k_combo:
functions_->glUniform1i(variable_location, value.to_int());
break;
case NodeValue::k_color: {
Color color = value.to_color();
functions_->glUniform4f(variable_location, color.red(),
color.green(), color.blue(),
color.alpha());
break;
}
case NodeValue::k_boolean:
functions_->glUniform1i(variable_location, value.to_bool());
break;
case NodeValue::k_texture: {
TexturePtr texture = value.to_texture();
// Set value to bound texture
functions_->glUniform1i(variable_location,
textures_to_bind.size());
texture_index_map.insert(it.key(), textures_to_bind.size());
textures_to_bind.append(
{ texture, job.get_interpolation(it.key()) });
// Set enable flag if shader wants it
GLuint tex_id = texture ? texture->id().value<GLuint>() : 0;
int enable_param_location = functions_->glGetUniformLocation(
shader, QStringLiteral("%1_enabled")
.arg(it.key())
.toUtf8()
.constData());
if (enable_param_location > -1) {
functions_->glUniform1i(enable_param_location, tex_id > 0);
}
break;
}
case NodeValue::k_samples:
case NodeValue::k_text:
case NodeValue::k_rational:
case NodeValue::k_font:
case NodeValue::k_file:
case NodeValue::k_video_params:
case NodeValue::k_audio_params:
case NodeValue::k_subtitle_params:
case NodeValue::k_bezier:
case NodeValue::k_binary:
case NodeValue::k_none:
case NodeValue::k_data_type_count:
break;
}
}
// Bind all textures
for (int i = 0; i < textures_to_bind.size(); i++) {
const TextureToBind &t = textures_to_bind.at(i);
TexturePtr texture = t.texture;
GLuint tex_id = texture ? texture->id().value<GLuint>() : 0;
functions_->glActiveTexture(GL_TEXTURE0 + i);
GLenum target = (texture && texture->params().is_3d()) ?
GL_TEXTURE_3D :
GL_TEXTURE_2D;
functions_->glBindTexture(target, tex_id);
if (tex_id) {
prepare_input_texture(target, t.interpolation);
if (texture->channel_count() == 1 &&
destination_params.channel_count() != 1) {
// Interpret this texture as a grayscale texture
functions_->glTexParameteri(GL_TEXTURE_2D,
GL_TEXTURE_SWIZZLE_R, GL_RED);
functions_->glTexParameteri(GL_TEXTURE_2D,
GL_TEXTURE_SWIZZLE_G, GL_RED);
functions_->glTexParameteri(GL_TEXTURE_2D,
GL_TEXTURE_SWIZZLE_B, GL_RED);
}
}
}
// Ensure matrix is set, at least to identity
GLint mvpmat_location =
functions_->glGetUniformLocation(shader, "ove_mvpmat");
if (mvpmat_location > -1) {
functions_->glUniformMatrix4fv(
mvpmat_location, 1, false,
job.get(QStringLiteral("ove_mvpmat")).to_matrix().constData());
}
// Set the viewport to the "physical" resolution of the destination
functions_->glViewport(0, 0, destination_params.effective_width(),
destination_params.effective_height());
// Bind vertex array object
QOpenGLVertexArrayObject vao;
vao.create();
vao.bind();
// Set buffers
QOpenGLBuffer vert_vbo;
vert_vbo.create();
vert_vbo.bind();
// If the job has vertex coordinate overrides use them instead of the defaults.
if (!job.get_vertex_coordinates().isEmpty()) {
Q_ASSERT(job.get_vertex_coordinates().size() == 18);
vert_vbo.allocate(job.get_vertex_coordinates().constData(),
job.get_vertex_coordinates().size() *
sizeof(float));
} else {
vert_vbo.allocate(blit_vertices.constData(),
blit_vertices.size() * sizeof(GLfloat));
}
vert_vbo.release();
QOpenGLBuffer frag_vbo;
frag_vbo.create();
frag_vbo.bind();
frag_vbo.allocate(blit_texcoords.constData(),
blit_texcoords.size() * sizeof(GLfloat));
frag_vbo.release();
GLint vertex_location =
functions_->glGetAttribLocation(shader, "a_position");
if (vertex_location != -1) {
vert_vbo.bind();
functions_->glEnableVertexAttribArray(vertex_location);
functions_->glVertexAttribPointer(vertex_location, 3, GL_FLOAT,
GL_FALSE, 0, nullptr);
vert_vbo.release();
}
GLint tex_location =
functions_->glGetAttribLocation(shader, "a_texcoord");
if (tex_location != -1) {
frag_vbo.bind();
functions_->glEnableVertexAttribArray(tex_location);
functions_->glVertexAttribPointer(tex_location, 2, GL_FLOAT,
GL_FALSE, 0, nullptr);
frag_vbo.release();
}
// Some shaders optimize through multiple iterations which requires ping-ponging textures
// - If there are only two iterations, we can just create one backend texture and then the
// destination can be the second
// - If there are more than two iterations, we need to ping pong back and forth between two
// textures. We can still use the destination as the last iteration, but we'll need textures
// for the iterative process.
int real_iteration_count;
if (job.get_iteration_count() > 1 && !job.get_iterative_input().isEmpty()) {
real_iteration_count = job.get_iteration_count();
} else {
real_iteration_count = 1;
}
TexturePtr output_tex, input_tex;
if (real_iteration_count > 1) {
// Create one texture to bounce off
output_tex = create_texture(destination_params);
if (real_iteration_count > 2) {
// Create a second texture bounce off
input_tex = create_texture(destination_params);
}
}
GLint iteration_location =
functions_->glGetUniformLocation(shader, "ove_iteration");
for (int iteration = 0; iteration < real_iteration_count; iteration++) {
// Set iteration number
if (iteration_location > -1) {
functions_->glUniform1i(iteration_location, iteration);
}
// Replace iterative input
if (iteration == real_iteration_count - 1) {
// This is the last iteration, draw to the destination
if (destination) {
// If we have a destination texture, draw to it
attach_texture_as_destination(destination->id());
} else if (iteration > 0) {
// Otherwise, if we were iterating before, detach texture now
detach_texture_as_destination();
}
// Clear the destination if the caller requested it
if (clear_destination) {
clear_destination_internal();
}
} else {
// Always draw to output_tex, which gets swapped with input_tex every iteration
attach_texture_as_destination(output_tex->id());
}
if (iteration > 0) {
// If this is not the first iteration, replace the iterative texture with the one we
// last drew
const QString &iterative_input = job.get_iterative_input();
functions_->glActiveTexture(
GL_TEXTURE0 + texture_index_map.value(iterative_input));
functions_->glBindTexture(GL_TEXTURE_2D,
input_tex->id().value<GLuint>());
// At this time, we only support iterating 2D textures
prepare_input_texture(GL_TEXTURE_2D,
job.get_interpolation(iterative_input));
}
// Swap so that the next iteration, the texture we draw now will be the input texture next
std::swap(output_tex, input_tex);
// Blit this texture through this shader
{
OAK_PRINT_GL_ERRORS;
functions_->glDrawArrays(GL_TRIANGLES, 0,
blit_vertices.size() / 3);
}
}
if (destination) {
// Reset framebuffer to default if we were drawing to a texture
detach_texture_as_destination();
}
// Release any textures we bound before
for (int i = textures_to_bind.size() - 1; i >= 0; i--) {
TexturePtr texture = textures_to_bind.at(i).texture;
GLenum target = (texture && texture->params().is_3d()) ?
GL_TEXTURE_3D :
GL_TEXTURE_2D;
functions_->glActiveTexture(GL_TEXTURE0 + i);
functions_->glBindTexture(target, 0);
}
// Release shader
functions_->glUseProgram(0);
// Release vertex array object
frag_vbo.destroy();
vert_vbo.destroy();
vao.release();
vao.destroy();
} catch (const std::bad_cast &e) {
}
}
GLint OpenGLRenderer::get_internal_format(PixelFormat format, int channel_layout)
{
switch (format) {
case PixelFormat::u8:
switch (channel_layout) {
case 1:
return GL_R8;
case 2:
return GL_RG8;
case 3:
return GL_RGB8;
case 4:
return GL_RGBA8;
}
break;
case PixelFormat::u10:
if (channel_layout == 4) {
return GL_RGB10_A2;
}
break;
case PixelFormat::u16:
switch (channel_layout) {
case 1:
return GL_R16;
case 2:
return GL_RG16;
case 3:
return GL_RGB16;
case 4:
return GL_RGBA16;
}
break;
case PixelFormat::f16:
switch (channel_layout) {
case 1:
return GL_R16F;
case 2:
return GL_RG16F;
case 3:
return GL_RGB16F;
case 4:
return GL_RGBA16F;
}
break;
case PixelFormat::f32:
switch (channel_layout) {
case 1:
return GL_R32F;
case 2:
return GL_RG32F;
case 3:
return GL_RGB32F;
case 4:
return GL_RGBA32F;
}
break;
case PixelFormat::invalid:
case PixelFormat::count:
break;
}
return GL_INVALID_VALUE;
}
GLenum OpenGLRenderer::get_pixel_type(PixelFormat format)
{
switch (format) {
case PixelFormat::u8:
return GL_UNSIGNED_BYTE;
case PixelFormat::u10:
return GL_UNSIGNED_INT_2_10_10_10_REV;
case PixelFormat::u16:
return GL_UNSIGNED_SHORT;
case PixelFormat::f16:
return GL_HALF_FLOAT;
case PixelFormat::f32:
return GL_FLOAT;
case PixelFormat::invalid:
case PixelFormat::count:
break;
}
return GL_INVALID_VALUE;
}
GLenum OpenGLRenderer::get_pixel_format(int channel_count)
{
switch (channel_count) {
case 1:
return GL_RED;
case 3:
return GL_RGB;
case 4:
return GL_RGBA;
default:
return GL_INVALID_VALUE;
}
}
void OpenGLRenderer::prepare_input_texture(GLenum target,
Texture::Interpolation interp)
{
switch (interp) {
case Texture::k_nearest:
functions_->glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
functions_->glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
break;
case Texture::k_linear:
functions_->glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
functions_->glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
break;
case Texture::k_mipmapped_linear:
functions_->glGenerateMipmap(target);
functions_->glTexParameteri(target, GL_TEXTURE_MIN_FILTER,
GL_LINEAR_MIPMAP_LINEAR);
functions_->glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
break;
}
functions_->glTexParameteri(target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
functions_->glTexParameteri(target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
if (target == GL_TEXTURE_3D) {
functions_->glTexParameteri(target, GL_TEXTURE_WRAP_R,
GL_CLAMP_TO_EDGE);
}
}
void OpenGLRenderer::clear_destination_internal(double r, double g, double b,
double a)
{
if (!functions_) {
return;
}
functions_->glClearColor(r, g, b, a);
functions_->glClear(GL_COLOR_BUFFER_BIT);
}
GLuint OpenGLRenderer::compile_shader(GLenum type, const QString &code)
{
const bool is_gles = context_ && context_->isOpenGLES();
const int major = context_ ? context_->format().majorVersion() : 0;
const bool is_gles2 = is_gles && (major < 3);
const QString gles_preamble =
is_gles2 ? QStringLiteral("#version 100\n\n"
"precision highp float;\n\n"
"#define frag_color gl_FragColor\n") :
QStringLiteral("#version 300 es\n\n"
"precision highp float;\n\n");
const QString desktop_preamble =
// Use appropriate GL 3.2 shader header
QStringLiteral("#version 150\n\n"
"precision highp float;\n\n");
const QString shader_preamble = is_gles ? gles_preamble : desktop_preamble;
QString base_code = code;
if (base_code.isEmpty()) {
// Use default code
if (type == GL_FRAGMENT_SHADER) {
base_code = FileFunctions::read_file_as_string(
QStringLiteral(":/shaders/default.frag"));
} else if (type == GL_VERTEX_SHADER) {
base_code = FileFunctions::read_file_as_string(
QStringLiteral(":/shaders/default.vert"));
}
}
QString complete_code;
if (base_code.startsWith(QStringLiteral("#version"))) {
if (is_gles ||
!desktop_preamble.startsWith(QStringLiteral("#version"))) {
int newline = base_code.indexOf('\n');
if (newline >= 0) {
complete_code = shader_preamble + base_code.mid(newline + 1);
} else {
complete_code = shader_preamble;
}
} else {
complete_code = base_code;
}
} else {
complete_code = shader_preamble + base_code;
}
if (is_gles2) {
if (type == GL_VERTEX_SHADER) {
complete_code.replace(
QRegularExpression(QStringLiteral("\\bin\\b")),
QStringLiteral("attribute"));
complete_code.replace(
QRegularExpression(QStringLiteral("\\bout\\b")),
QStringLiteral("varying"));
} else if (type == GL_FRAGMENT_SHADER) {
complete_code.replace(
QRegularExpression(QStringLiteral("\\bin\\b")),
QStringLiteral("varying"));
complete_code.replace(QRegularExpression(QStringLiteral(
"\\bout\\s+vec4\\s+frag_color\\s*;")),
QStringLiteral("// frag_color output"));
complete_code.replace(
QRegularExpression(QStringLiteral("\\btexture\\b")),
QStringLiteral("texture2D"));
}
}
QByteArray code_utf8 = complete_code.toUtf8();
const char *code_cstr = code_utf8.constData();
GLuint shader = functions_->glCreateShader(type);
functions_->glShaderSource(shader, 1, &code_cstr, nullptr);
functions_->glCompileShader(shader);
GLint success;
functions_->glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
if (!success) {
qWarning() << "Failed to compile OpenGL shader";
QByteArray error_log(10240, Qt::Uninitialized);
functions_->glGetShaderInfoLog(shader, error_log.size(), nullptr,
error_log.data());
std::cout << error_log.constData() << std::endl
<< code_cstr << std::endl;
functions_->glDeleteShader(shader);
shader = 0;
}
return shader;
}
bool OpenGLRenderer::ensure_context_current(const char *caller)
{
if (!context_) {
qWarning() << caller << "called without an OpenGL context";
return false;
}
// QOpenGLContext may only be made current from its owning thread. Viewer
// paint code can receive textures produced by a render-thread OpenGL
// renderer, so guard here before makeCurrent() can crash inside Qt/GL.
if (context_->thread() != QThread::currentThread()) {
qWarning()
<< caller << "called from the wrong thread for this OpenGL context";
return false;
}
if (QOpenGLContext::currentContext() != context_) {
if (context_->parent() == this && surface_.isValid()) {
if (!context_->makeCurrent(&surface_)) {
qWarning() << caller << "failed to make context current";
return false;
}
} else {
qWarning() << caller << "OpenGL context not current";
return false;
}
}
if (!functions_) {
functions_ = context_->functions();
}
if (!functions_) {
qWarning() << caller << "OpenGL functions not available";
return false;
}
return true;
}
}