1103 lines
29 KiB
C++
1103 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::kTextureCacheMaxSize = 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 PRINT_GL_ERRORS ErrorPrinter __e(__FUNCTION__, functions_)
|
|
|
|
#define 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();
|
|
PostDestroy();
|
|
}
|
|
|
|
void OpenGLRenderer::Init(QOpenGLContext *existing_ctx)
|
|
{
|
|
if (context_) {
|
|
qCritical() << "Can't initialize already initialized OpenGLRenderer";
|
|
return;
|
|
}
|
|
|
|
context_ = existing_ctx;
|
|
}
|
|
|
|
bool OpenGLRenderer::Init()
|
|
{
|
|
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::PostDestroy()
|
|
{
|
|
// Destroy surface if we created it
|
|
if (surface_.isValid()) {
|
|
surface_.destroy();
|
|
}
|
|
}
|
|
|
|
void OpenGLRenderer::PostInit()
|
|
{
|
|
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::DestroyInternal()
|
|
{
|
|
if (context_) {
|
|
GL_PREAMBLE;
|
|
|
|
if (functions_ && framebuffer_) {
|
|
functions_->glDeleteFramebuffers(1, &framebuffer_);
|
|
}
|
|
framebuffer_ = 0;
|
|
|
|
// Delete context if it belongs to us
|
|
if (context_->parent() == this) {
|
|
delete context_;
|
|
}
|
|
context_ = nullptr;
|
|
functions_ = nullptr;
|
|
}
|
|
}
|
|
|
|
void OpenGLRenderer::ClearDestination(Texture *texture, double r, double g,
|
|
double b, double a)
|
|
{
|
|
GL_PREAMBLE;
|
|
|
|
if (!EnsureContextCurrent(__FUNCTION__)) {
|
|
return;
|
|
}
|
|
|
|
if (texture) {
|
|
AttachTextureAsDestination(texture->id());
|
|
}
|
|
|
|
ClearDestinationInternal(r, g, b, a);
|
|
|
|
if (texture) {
|
|
DetachTextureAsDestination();
|
|
}
|
|
}
|
|
|
|
QVariant OpenGLRenderer::CreateNativeTexture(int width, int height, int depth,
|
|
PixelFormat format,
|
|
int channel_count,
|
|
const void *data, int linesize)
|
|
{
|
|
GL_PREAMBLE;
|
|
if (!EnsureContextCurrent(__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, GetInternalFormat(format, channel_count), width, height,
|
|
depth, 0, GetPixelFormat(channel_count), GetPixelType(format),
|
|
data);
|
|
} else {
|
|
functions_->glTexImage2D(
|
|
target, 0, GetInternalFormat(format, channel_count), width, height,
|
|
0, GetPixelFormat(channel_count), GetPixelType(format), data);
|
|
}
|
|
|
|
functions_->glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
|
|
|
functions_->glBindTexture(target, current_tex);
|
|
|
|
return texture;
|
|
}
|
|
|
|
void OpenGLRenderer::AttachTextureAsDestination(const QVariant &texture)
|
|
{
|
|
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::DetachTextureAsDestination()
|
|
{
|
|
// QOpenGLWidget renders to a non-zero default FBO.
|
|
const GLuint default_fbo = context_ ? context_->defaultFramebufferObject() :
|
|
0;
|
|
functions_->glBindFramebuffer(GL_FRAMEBUFFER, default_fbo);
|
|
}
|
|
|
|
void OpenGLRenderer::DestroyNativeTexture(QVariant texture)
|
|
{
|
|
if (!EnsureContextCurrent(__FUNCTION__)) {
|
|
return;
|
|
}
|
|
|
|
GLuint t = texture.value<GLuint>();
|
|
|
|
if (t > 0) {
|
|
functions_->glDeleteTextures(1, &t);
|
|
}
|
|
}
|
|
|
|
QVariant OpenGLRenderer::CreateNativeShader(ShaderCode code)
|
|
{
|
|
GL_PREAMBLE;
|
|
|
|
if (!EnsureContextCurrent(__FUNCTION__)) {
|
|
return QVariant();
|
|
}
|
|
|
|
PRINT_GL_ERRORS;
|
|
|
|
GLuint vert = CompileShader(GL_VERTEX_SHADER, code.vert_code());
|
|
GLuint frag = CompileShader(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::DestroyNativeShader(QVariant shader)
|
|
{
|
|
GL_PREAMBLE;
|
|
|
|
if (!EnsureContextCurrent(__FUNCTION__)) {
|
|
return;
|
|
}
|
|
|
|
GLuint program = shader.value<GLuint>();
|
|
functions_->glDeleteProgram(program);
|
|
}
|
|
|
|
void OpenGLRenderer::UploadToTexture(const QVariant &handle,
|
|
const VideoParams &p, const void *data,
|
|
int linesize)
|
|
{
|
|
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, ¤t_tex);
|
|
|
|
functions_->glBindTexture(tex_type, t);
|
|
|
|
functions_->glPixelStorei(GL_UNPACK_ROW_LENGTH, linesize);
|
|
|
|
{
|
|
PRINT_GL_ERRORS;
|
|
|
|
if (!is_3d) {
|
|
functions_->glTexSubImage2D(tex_type, 0, 0, 0, p.effective_width(),
|
|
p.effective_height(),
|
|
GetPixelFormat(p.channel_count()),
|
|
GetPixelType(p.format()), data);
|
|
} else {
|
|
context_->extraFunctions()->glTexSubImage3D(
|
|
tex_type, 0, 0, 0, 0, p.effective_width(), p.effective_height(),
|
|
p.effective_depth(), GetPixelFormat(p.channel_count()),
|
|
GetPixelType(p.format()), data);
|
|
}
|
|
}
|
|
|
|
functions_->glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
|
|
|
functions_->glBindTexture(tex_type, current_tex);
|
|
}
|
|
|
|
void OpenGLRenderer::DownloadFromTexture(const QVariant &id,
|
|
const VideoParams &p, void *data,
|
|
int linesize)
|
|
{
|
|
GL_PREAMBLE;
|
|
|
|
if (!EnsureContextCurrent(__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, ¤t_tex);
|
|
|
|
AttachTextureAsDestination(id);
|
|
|
|
GLenum status = functions_->glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
|
if (status != GL_FRAMEBUFFER_COMPLETE) {
|
|
qWarning() << "DownloadFromTexture framebuffer incomplete" << status;
|
|
DetachTextureAsDestination();
|
|
return;
|
|
}
|
|
|
|
functions_->glPixelStorei(GL_PACK_ROW_LENGTH, linesize);
|
|
|
|
// Ensure all rendering is complete before reading back on TBDR architectures (macOS)
|
|
functions_->glFinish();
|
|
|
|
{
|
|
PRINT_GL_ERRORS;
|
|
functions_->glReadPixels(0, 0, p.effective_width(),
|
|
p.effective_height(),
|
|
GetPixelFormat(p.channel_count()),
|
|
GetPixelType(p.format()), data);
|
|
}
|
|
|
|
functions_->glPixelStorei(GL_PACK_ROW_LENGTH, 0);
|
|
|
|
DetachTextureAsDestination();
|
|
|
|
functions_->glBindTexture(GL_TEXTURE_2D, current_tex);
|
|
}
|
|
|
|
void OpenGLRenderer::Flush()
|
|
{
|
|
GL_PREAMBLE;
|
|
|
|
if (!EnsureContextCurrent(__FUNCTION__)) {
|
|
return;
|
|
}
|
|
|
|
#if !defined(OAK_RENDER_BACKEND_PLUGIN)
|
|
if (OLIVE_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::AttachOutputTexture(olive::Texture *texture)
|
|
{
|
|
if (!EnsureContextCurrent(__FUNCTION__)) {
|
|
return;
|
|
}
|
|
|
|
if (texture) {
|
|
AttachTextureAsDestination(texture->id());
|
|
}
|
|
}
|
|
|
|
// Clears the framebuffer attachment installed by AttachOutputTexture().
|
|
void OpenGLRenderer::DetachOutputTexture()
|
|
{
|
|
if (!EnsureContextCurrent(__FUNCTION__)) {
|
|
return;
|
|
}
|
|
|
|
DetachTextureAsDestination();
|
|
}
|
|
|
|
Color OpenGLRenderer::GetPixelFromTexture(Texture *texture, const QPointF &pt)
|
|
{
|
|
if (!texture || !EnsureContextCurrent(__FUNCTION__)) {
|
|
return Color();
|
|
}
|
|
|
|
AttachTextureAsDestination(texture->id());
|
|
|
|
QByteArray data(VideoParams::GetBytesPerPixel(texture->format(),
|
|
texture->channel_count()),
|
|
Qt::Uninitialized);
|
|
|
|
functions_->glReadPixels(pt.x(), pt.y(), 1, 1,
|
|
GetPixelFormat(texture->channel_count()),
|
|
GetPixelType(texture->format()), data.data());
|
|
|
|
Color c = Color::fromData(data.data(), texture->format(),
|
|
texture->channel_count());
|
|
|
|
if (texture->channel_count() == VideoParams::kRGBChannelCount) {
|
|
// No alpha channel, set to 1.0
|
|
c.set_alpha(1.0);
|
|
}
|
|
|
|
DetachTextureAsDestination();
|
|
|
|
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)
|
|
{
|
|
GL_PREAMBLE;
|
|
if (!EnsureContextCurrent(__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.GetValues().constBegin();
|
|
it != job.GetValues().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::kInt:
|
|
// 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.toInt());
|
|
break;
|
|
case NodeValue::kFloat:
|
|
// kFloat technically specifies a double but as above, OpenGL doesn't support those.
|
|
functions_->glUniform1f(variable_location, value.toDouble());
|
|
break;
|
|
case NodeValue::kVec2: {
|
|
QVector2D v = value.toVec2();
|
|
functions_->glUniform2fv(variable_location, 1,
|
|
reinterpret_cast<const GLfloat *>(&v));
|
|
break;
|
|
}
|
|
case NodeValue::kVec3: {
|
|
QVector3D v = value.toVec3();
|
|
functions_->glUniform3fv(variable_location, 1,
|
|
reinterpret_cast<const GLfloat *>(&v));
|
|
break;
|
|
}
|
|
case NodeValue::kVec4: {
|
|
QVector4D v = value.toVec4();
|
|
functions_->glUniform4fv(variable_location, 1,
|
|
reinterpret_cast<const GLfloat *>(&v));
|
|
break;
|
|
}
|
|
case NodeValue::kMatrix:
|
|
functions_->glUniformMatrix4fv(variable_location, 1, false,
|
|
value.toMatrix().constData());
|
|
break;
|
|
case NodeValue::kCombo:
|
|
functions_->glUniform1i(variable_location, value.toInt());
|
|
break;
|
|
case NodeValue::kColor: {
|
|
Color color = value.toColor();
|
|
functions_->glUniform4f(variable_location, color.red(),
|
|
color.green(), color.blue(),
|
|
color.alpha());
|
|
break;
|
|
}
|
|
case NodeValue::kBoolean:
|
|
functions_->glUniform1i(variable_location, value.toBool());
|
|
break;
|
|
case NodeValue::kTexture: {
|
|
TexturePtr texture = value.toTexture();
|
|
|
|
// 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.GetInterpolation(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::kSamples:
|
|
case NodeValue::kText:
|
|
case NodeValue::kRational:
|
|
case NodeValue::kFont:
|
|
case NodeValue::kFile:
|
|
case NodeValue::kVideoParams:
|
|
case NodeValue::kAudioParams:
|
|
case NodeValue::kSubtitleParams:
|
|
case NodeValue::kBezier:
|
|
case NodeValue::kBinary:
|
|
case NodeValue::kNone:
|
|
case NodeValue::kDataTypeCount:
|
|
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) {
|
|
PrepareInputTexture(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")).toMatrix().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.GetVertexCoordinates().isEmpty()) {
|
|
Q_ASSERT(job.GetVertexCoordinates().size() == 18);
|
|
vert_vbo_.allocate(job.GetVertexCoordinates().constData(),
|
|
job.GetVertexCoordinates().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.GetIterationCount() > 1 && !job.GetIterativeInput().isEmpty()) {
|
|
real_iteration_count = job.GetIterationCount();
|
|
} else {
|
|
real_iteration_count = 1;
|
|
}
|
|
|
|
TexturePtr output_tex, input_tex;
|
|
if (real_iteration_count > 1) {
|
|
// Create one texture to bounce off
|
|
output_tex = CreateTexture(destination_params);
|
|
|
|
if (real_iteration_count > 2) {
|
|
// Create a second texture bounce off
|
|
input_tex = CreateTexture(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
|
|
AttachTextureAsDestination(destination->id());
|
|
} else if (iteration > 0) {
|
|
// Otherwise, if we were iterating before, detach texture now
|
|
DetachTextureAsDestination();
|
|
}
|
|
|
|
// Clear the destination if the caller requested it
|
|
if (clear_destination) {
|
|
ClearDestinationInternal();
|
|
}
|
|
} else {
|
|
// Always draw to output_tex, which gets swapped with input_tex every iteration
|
|
AttachTextureAsDestination(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.GetIterativeInput();
|
|
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
|
|
PrepareInputTexture(GL_TEXTURE_2D,
|
|
job.GetInterpolation(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
|
|
{
|
|
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
|
|
DetachTextureAsDestination();
|
|
}
|
|
|
|
// 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 (std::bad_cast e) {
|
|
}
|
|
}
|
|
|
|
GLint OpenGLRenderer::GetInternalFormat(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::GetPixelType(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::GetPixelFormat(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::PrepareInputTexture(GLenum target,
|
|
Texture::Interpolation interp)
|
|
{
|
|
switch (interp) {
|
|
case Texture::kNearest:
|
|
functions_->glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
|
functions_->glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
|
break;
|
|
case Texture::kLinear:
|
|
functions_->glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
functions_->glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
break;
|
|
case Texture::kMipmappedLinear:
|
|
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::ClearDestinationInternal(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::CompileShader(GLenum type, const QString &code)
|
|
{
|
|
const bool is_gles = context_ && context_->isOpenGLES();
|
|
const int major = context_ ? context_->format().majorVersion() : 0;
|
|
const int minor = context_ ? context_->format().minorVersion() : 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::ReadFileAsString(
|
|
QStringLiteral(":/shaders/default.frag"));
|
|
} else if (type == GL_VERTEX_SHADER) {
|
|
base_code = FileFunctions::ReadFileAsString(
|
|
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::EnsureContextCurrent(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;
|
|
}
|
|
|
|
}
|