/*** 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 . ***/ #include "openglrenderer.h" #include #include #include #include #include #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 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 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, ¤t_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(), 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(); 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(); 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(); 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, ¤t_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(); if (!texture_id || !functions_->glIsTexture(texture_id)) { qWarning() << "DownloadFromTexture called with invalid texture"; return; } GLint current_tex; functions_->glGetIntegerv(GL_TEXTURE_BINDING_2D, ¤t_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(a_job); ShaderJob job(s_job); // If this node is iterative, we'll pick up which input here QMap texture_index_map; QVector textures_to_bind; GLuint shader = s.value(); 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(&v)); break; } case NodeValue::k_vec3: { QVector3D v = value.to_vec3(); functions_->glUniform3fv(variable_location, 1, reinterpret_cast(&v)); break; } case NodeValue::k_vec4: { QVector4D v = value.to_vec4(); functions_->glUniform4fv(variable_location, 1, reinterpret_cast(&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() : 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() : 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()); // 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; } }