more coherent render threading for now

Once again, conceptually this system should work, however it does not seem to
be the most efficient and it wouldn't surprise me if the multithreading was
eventually upgraded to an even more coherent system one day. However for
"core principles" this should be fairly decent.
This commit is contained in:
itsmattkc
2019-11-03 04:05:43 +11:00
parent 9c65cb854b
commit 663ae56ac4
18 changed files with 447 additions and 107 deletions
+2 -22
View File
@@ -47,32 +47,12 @@ void TimeRange::set_range(const rational &in, const rational &out)
bool TimeRange::operator<(const TimeRange &r) const
{
return length() < r.length();
}
bool TimeRange::operator<=(const TimeRange &r) const
{
return length() <= r.length();
return (in() + out()) < (r.in() + r.out());
}
bool TimeRange::operator>(const TimeRange &r) const
{
return length() > r.length();
}
bool TimeRange::operator>=(const TimeRange &r) const
{
return length() >= r.length();
}
bool TimeRange::operator==(const TimeRange &r) const
{
return length() == r.length();
}
bool TimeRange::operator!=(const TimeRange &r) const
{
return length() != r.length();
return (in() + out()) > (r.in() + r.out());
}
void TimeRange::normalize()
-4
View File
@@ -17,11 +17,7 @@ public:
void set_range(const rational& in, const rational& out);
bool operator<(const TimeRange &r) const;
bool operator<=(const TimeRange &r) const;
bool operator>(const TimeRange &r) const;
bool operator>=(const TimeRange &r) const;
bool operator==(const TimeRange &r) const;
bool operator!=(const TimeRange &r) const;
private:
void normalize();
+37 -34
View File
@@ -203,49 +203,52 @@ FramePtr FFmpegDecoder::Retrieve(const rational &timecode, const rational &lengt
return nullptr;
}
// Cache FFmpeg error code returns
int ret = 0;
// Check if this is already the frame we have cached
if (frame_->pts != target_ts) {
// Cache FFmpeg error code returns
int ret = 0;
// Set up seeking loop
int64_t seek_ts = target_ts;
int64_t second_ts = qRound(rational(avstream_->time_base).flipped().toDouble());
bool got_frame = false;
bool last_backtrack = false;
// Set up seeking loop
int64_t seek_ts = target_ts;
int64_t second_ts = qRound(rational(avstream_->time_base).flipped().toDouble());
bool got_frame = false;
bool last_backtrack = false;
// FFmpeg frame retrieve loop
while (ret >= 0 && frame_->pts != target_ts) {
// FFmpeg frame retrieve loop
while (ret >= 0 && frame_->pts != target_ts) {
// If the frame timestamp is too large, we need to seek back a little
if (got_frame && (frame_->pts > target_ts || frame_->pts == AV_NOPTS_VALUE)) {
// If we already tried seeking to 0 though, there's nothing we can do so we error here
if (last_backtrack) {
// Must be the earliest frame in the file
break;
// If the frame timestamp is too large, we need to seek back a little
if (got_frame && (frame_->pts > target_ts || frame_->pts == AV_NOPTS_VALUE)) {
// If we already tried seeking to 0 though, there's nothing we can do so we error here
if (last_backtrack) {
// Must be the earliest frame in the file
break;
}
// We can't seek earlier than 0, so if this is a 0-seek, don't try any more times after this attempt
if (seek_ts <= 0) {
seek_ts = 0;
last_backtrack = true;
}
Seek(seek_ts);
// FFmpeg doesn't always seek correctly, if we have to seek again we wrangle it into seeking back far enough
seek_ts -= second_ts;
}
// We can't seek earlier than 0, so if this is a 0-seek, don't try any more times after this attempt
if (seek_ts <= 0) {
seek_ts = 0;
last_backtrack = true;
}
Seek(seek_ts);
// FFmpeg doesn't always seek correctly, if we have to seek again we wrangle it into seeking back far enough
seek_ts -= second_ts;
ret = GetFrame();
got_frame = true;
}
ret = GetFrame();
got_frame = true;
// Handle any errors received during the frame retrieve process
if (ret < 0) {
FFmpegError(ret);
return nullptr;
}
}
// Handle any errors received during the frame retrieve process
if (ret < 0) {
FFmpegError(ret);
return nullptr;
}
// Frame was valid, now we create an Olive frame to place the data into
// Frame was valid, now we convert it to a native Olive frame
FramePtr frame_container = Frame::Create();
frame_container->set_width(frame_->width);
frame_container->set_height(frame_->height);
+21
View File
@@ -135,6 +135,16 @@ void Node::UnlockProcessing()
processing_lock_.unlock();
}
bool Node::IsProcessingLocked()
{
if (processing_lock_.tryLock()) {
processing_lock_.unlock();
return false;
} else {
return true;
}
}
void Node::CopyInputs(Node *source, Node *destination)
{
Q_ASSERT(source->id() == destination->id());
@@ -290,6 +300,17 @@ QString Node::Code(NodeOutput *output)
return QString();
}
NodeParam *Node::GetParameterWithID(const QString &id)
{
foreach (NodeParam* param, params_) {
if (param->id() == id) {
return param;
}
}
return nullptr;
}
bool Node::OutputsTo(Node *n)
{
foreach (NodeParam* param, params_) {
+6
View File
@@ -129,6 +129,11 @@ public:
*/
virtual QString Code(NodeOutput* output);
/**
* @brief Returns the parameter with the specified ID (or nullptr if it doesn't exist)
*/
NodeParam* GetParameterWithID(const QString& id);
/**
* @brief Returns whether this Node outputs data to the Node `n` in any way
*/
@@ -199,6 +204,7 @@ public:
*/
void LockProcessing();
void UnlockProcessing();
bool IsProcessingLocked();
/**
* @brief Copies inputs from from Node to another including connections
+3
View File
@@ -19,11 +19,14 @@ add_subdirectory(vulkan)
set(OLIVE_SOURCES
${OLIVE_SOURCES}
render/backend/renderbackend.h
render/backend/renderbackend.cpp
render/backend/audiorenderbackend.h
render/backend/audiorenderbackend.cpp
render/backend/videorenderbackend.h
render/backend/videorenderbackend.cpp
PARENT_SCOPE
)
+4
View File
@@ -16,6 +16,8 @@
set(OLIVE_SOURCES
${OLIVE_SOURCES}
render/backend/opengl/decodercache.h
render/backend/opengl/decodercache.cpp
render/backend/opengl/functions.h
render/backend/opengl/functions.cpp
render/backend/opengl/openglbackend.h
@@ -24,6 +26,8 @@ set(OLIVE_SOURCES
render/backend/opengl/openglframebuffer.cpp
render/backend/opengl/openglshader.h
render/backend/opengl/openglshader.cpp
render/backend/opengl/openglshadercache.h
render/backend/opengl/openglshadercache.cpp
render/backend/opengl/opengltexture.h
render/backend/opengl/opengltexture.cpp
render/backend/opengl/openglworker.h
@@ -4,3 +4,18 @@ DecoderCache::DecoderCache()
{
}
void DecoderCache::Clear()
{
decoders_.clear();
}
void DecoderCache::AddDecoder(Stream *stream, DecoderPtr shader)
{
decoders_.insert(stream, shader);
}
DecoderPtr DecoderCache::GetDecoder(Stream *stream)
{
return decoders_.value(stream);
}
+15
View File
@@ -1,11 +1,26 @@
#ifndef DECODERCACHE_H
#define DECODERCACHE_H
#include "decoder/decoder.h"
#include "project/item/footage/stream.h"
/**
* @brief Thread-safe cache of decoders
*/
class DecoderCache
{
public:
DecoderCache();
void Clear();
void AddDecoder(Stream* stream, DecoderPtr shader);
DecoderPtr GetDecoder(Stream* stream);
private:
QMap<Stream*, DecoderPtr> decoders_;
};
#endif // DECODERCACHE_H
+5 -22
View File
@@ -34,7 +34,7 @@ bool OpenGLBackend::Init()
QThread* thread = threads().at(i);
// Create one processor object for each thread
OpenGLWorker* processor = new OpenGLWorker(share_ctx);
OpenGLWorker* processor = new OpenGLWorker(share_ctx, &shader_cache_, &decoder_cache_);
processor->SetParameters(params());
// Finally, we can move it to its own thread
@@ -130,12 +130,12 @@ bool OpenGLBackend::Compile()
void OpenGLBackend::Decompile()
{
compiled_nodes_.clear();
shader_cache_.Clear();
}
bool OpenGLBackend::TraverseCompiling(Node *n)
bool OpenGLBackend::TraverseCompiling(Node *)
{
foreach (NodeParam* param, n->parameters()) {
/*foreach (NodeParam* param, n->parameters()) {
if (param->type() == NodeParam::kInput && param->IsConnected()) {
NodeOutput* connected_output = static_cast<NodeInput*>(param)->get_connected_output();
@@ -183,28 +183,11 @@ bool OpenGLBackend::TraverseCompiling(Node *n)
return false;
}
}
}
}*/
return true;
}
OpenGLShaderPtr OpenGLBackend::GetShaderFromID(const QString &id)
{
foreach (const CompiledNode& info, compiled_nodes_) {
if (info.id == id) {
return info.program;
}
}
return nullptr;
}
QString OpenGLBackend::GenerateShaderID(NodeOutput *output)
{
// Creates a unique identifier for this specific node and this specific output
return QString("%1:%2").arg(output->parent()->id(), output->id());
}
void OpenGLBackend::ThreadCallback(OpenGLTexturePtr texture, const rational& time, const QByteArray& hash)
{
// Threads are all done now, time to proceed
+5 -11
View File
@@ -2,10 +2,12 @@
#define OPENGLBACKEND_H
#include "../videorenderbackend.h"
#include "decodercache.h"
#include "openglframebuffer.h"
#include "openglworker.h"
#include "opengltexture.h"
#include "openglshader.h"
#include "openglshadercache.h"
class OpenGLBackend : public VideoRenderBackend
{
@@ -30,19 +32,8 @@ protected:
virtual void GenerateFrame(const rational& time) override;
private:
struct CompiledNode {
QString id;
OpenGLShaderPtr program;
};
bool TraverseCompiling(Node* n);
OpenGLShaderPtr GetShaderFromID(const QString& id);
QString GenerateShaderID(NodeOutput* output);
QList<CompiledNode> compiled_nodes_;
QVector<OpenGLWorker*> processors_;
OpenGLTexturePtr master_texture_;
@@ -51,6 +42,9 @@ private:
OpenGLFramebuffer copy_buffer_;
OpenGLShaderPtr copy_pipeline_;
OpenGLShaderCache shader_cache_;
DecoderCache decoder_cache_;
private slots:
void ThreadCallback(OpenGLTexturePtr texture, const rational& time, const QByteArray& hash);
@@ -1,6 +1,29 @@
#include "openglshadercache.h"
#include "node/node.h"
OpenGLShaderCache::OpenGLShaderCache()
{
}
QString OpenGLShaderCache::GenerateShaderID(NodeOutput *output)
{
// Creates a unique identifier for this specific node and this specific output
return QString("%1:%2").arg(output->parent()->id(), output->id());
}
void OpenGLShaderCache::Clear()
{
compiled_nodes_.clear();
}
void OpenGLShaderCache::AddShader(NodeOutput *output, OpenGLShaderPtr shader)
{
compiled_nodes_.insert(GenerateShaderID(output), shader);
}
OpenGLShaderPtr OpenGLShaderCache::GetShader(NodeOutput *output)
{
return compiled_nodes_.value(GenerateShaderID(output));
}
@@ -1,11 +1,30 @@
#ifndef OPENGLSHADERCACHE_H
#define OPENGLSHADERCACHE_H
#include <QMutex>
#include "node/output.h"
#include "openglshader.h"
/**
* @brief Thread-safe cache of OpenGL shaders
*/
class OpenGLShaderCache
{
public:
OpenGLShaderCache();
void Clear();
void AddShader(NodeOutput* output, OpenGLShaderPtr shader);
OpenGLShaderPtr GetShader(NodeOutput* output);
private:
QString GenerateShaderID(NodeOutput* output);
QMap<QString, OpenGLShaderPtr> compiled_nodes_;
};
#endif // OPENGLSHADERCACHE_H
@@ -75,6 +75,16 @@ void OpenGLTexture::Create(QOpenGLContext *ctx, int width, int height, const oli
}
}
void OpenGLTexture::Create(QOpenGLContext *ctx, FramePtr frame, const Type &type)
{
Create(ctx, frame->width(), frame->height(), frame->format(), type, frame->data());
}
void OpenGLTexture::Create(QOpenGLContext *ctx, FramePtr frame)
{
Create(ctx, frame, kSingleBuffer);
}
void OpenGLTexture::Destroy()
{
if (context_ != nullptr) {
@@ -24,6 +24,7 @@
#include <memory>
#include <QOpenGLFunctions>
#include "decoder/frame.h"
#include "render/pixelformat.h"
/**
@@ -45,6 +46,8 @@ public:
void Create(QOpenGLContext* ctx, int width, int height, const olive::PixelFormat &format, void *data = nullptr);
void Create(QOpenGLContext* ctx, int width, int height, const olive::PixelFormat &format, const Type& type, void *data = nullptr);
void Create(QOpenGLContext* ctx, FramePtr frame, const Type& type);
void Create(QOpenGLContext* ctx, FramePtr frame);
bool IsCreated() const;
+264 -13
View File
@@ -1,12 +1,17 @@
#include "openglworker.h"
#include <QThread>
#include "node/block/block.h"
#include "node/node.h"
OpenGLWorker::OpenGLWorker(QOpenGLContext *share_ctx, QObject *parent) :
OpenGLWorker::OpenGLWorker(QOpenGLContext *share_ctx, OpenGLShaderCache *shader_cache, DecoderCache *decoder_cache, QObject *parent) :
QObject(parent),
share_ctx_(share_ctx),
ctx_(nullptr),
functions_(nullptr)
functions_(nullptr),
shader_cache_(shader_cache),
decoder_cache_(decoder_cache)
{
surface_.create();
}
@@ -93,6 +98,169 @@ void OpenGLWorker::UpdateViewportFromParams()
}
}
Node *OpenGLWorker::ValidateBlock(Node *n, const rational& time)
{
if (n->IsBlock()) {
Block* block = static_cast<Block*>(n);
while (block->in() > time && block != nullptr) {
// This Block is too late, find an earlier one
block = block->previous();
}
while (block->out() <= time && block != nullptr) {
// This block is too early, find a later one
block = block->next();
}
// By this point, we should have the correct Block or nullptr if there's no Block here
return block;
}
return n;
}
QList<NodeInput*> OpenGLWorker::ProcessNodeInputsForTime(Node *n, const TimeRange &time)
{
QList<NodeInput*> connected_inputs;
// Now we need to gather information about this Node's inputs
foreach (NodeParam* param, n->parameters()) {
// Check if this parameter is an input and if the Node is dependent on it
if (param->type() == NodeParam::kInput) {
NodeInput* input = static_cast<NodeInput*>(param);
if (input->dependent()) {
// If we're here, this input is necessary and we need to acquire the value for this Node
if (input->IsConnected()) {
// If it's connected to something, we need to retrieve that output at some point
connected_inputs.append(input);
} else {
// If it isn't connected, it'll have the value we need inside it. We just need to store it for the node.
input->set_stored_value(input->get_value_at_time(n->InputTimeAdjustment(input, time).in()));
}
// Special types like FOOTAGE require extra work from us (to decrease node complexity dealing with decoders)
if (input->data_type() == NodeParam::kFootage) {
input->set_stored_value(0);
// Access a map of Node inputs and decoder instances and retrieve a frame!
StreamPtr stream = input->get_value_at_time(0).value<StreamPtr>();
DecoderPtr decoder = decoder_cache_->GetDecoder(stream.get());
if (decoder == nullptr && stream != nullptr) {
// Init decoder
decoder = Decoder::CreateFromID(stream->footage()->decoder());
decoder->set_stream(stream);
decoder_cache_->AddDecoder(stream.get(), decoder);
}
// By this point we should definitely have a decoder, and if we don't something's gone terribly wrong
if (decoder != nullptr) {
FramePtr frame = decoder->Retrieve(time.in());
if (frame != nullptr) {
OpenGLTexturePtr footage_tex = std::make_shared<OpenGLTexture>();
footage_tex->Create(ctx_, frame, OpenGLTexture::kDoubleBuffer);
// FIXME: Alpha association and color management
input->set_stored_value(QVariant::fromValue(footage_tex));
}
}
}
}
}
}
return connected_inputs;
}
void OpenGLWorker::RunNodeAsShader(Node* node, OpenGLShaderPtr shader)
{
shader->bind();
unsigned int input_texture_count = 0;
foreach (NodeParam* param, node->parameters()) {
if (param->type() == NodeParam::kInput) {
// See if the shader has takes this parameter as an input
int variable_location = shader->uniformLocation(param->id());
if (variable_location > -1) {
// This variable is used in the shader, let's set it to our value
NodeInput* input = static_cast<NodeInput*>(param);
switch (input->data_type()) {
case NodeInput::kInt:
shader->setUniformValue(variable_location, input->value().toInt());
break;
case NodeInput::kFloat:
shader->setUniformValue(variable_location, input->value().toFloat());
break;
case NodeInput::kVec2:
shader->setUniformValue(variable_location, input->value().value<QVector2D>());
break;
case NodeInput::kVec3:
shader->setUniformValue(variable_location, input->value().value<QVector3D>());
break;
case NodeInput::kVec4:
shader->setUniformValue(variable_location, input->value().value<QVector4D>());
break;
case NodeInput::kMatrix:
shader->setUniformValue(variable_location, input->value().value<QMatrix4x4>());
break;
case NodeInput::kColor:
shader->setUniformValue(variable_location, input->value().value<QColor>());
break;
case NodeInput::kBoolean:
shader->setUniformValue(variable_location, input->value().toBool());
break;
case NodeInput::kTexture:
case NodeInput::kFootage:
{
OpenGLTexturePtr texture = input->value().value<OpenGLTexturePtr>();
functions_->glActiveTexture(GL_TEXTURE0 + input_texture_count);
functions_->glBindTexture(GL_TEXTURE_2D, texture->texture());
// Set value to bound texture
shader->setUniformValue(variable_location, input_texture_count);
input_texture_count++;
break;
}
case NodeInput::kAny:
case NodeInput::kSamples:
case NodeInput::kTrack:
case NodeInput::kString:
case NodeInput::kRational:
case NodeInput::kBlock:
case NodeInput::kFont:
case NodeInput::kFile:
case NodeInput::kNone:
break;
}
}
}
}
// Attach texture to framebuffer
// Bind framebuffer
// Release framebuffer
// Detach texture
// Release any textures we bound before
while (input_texture_count > 0) {
input_texture_count--;
// Release texture here
functions_->glActiveTexture(GL_TEXTURE0 + input_texture_count);
functions_->glBindTexture(GL_TEXTURE_2D, 0);
}
shader->release();
}
void OpenGLWorker::FinishInit()
{
// Make context current on that surface
@@ -117,20 +285,103 @@ void OpenGLWorker::RenderAsSibling(const NodeDependency &dep)
{
NodeOutput* output = dep.node();
Node* node = output->parent();
rational time = dep.in();
node->LockProcessing();
// Check if block
// If yes, traverse previous and next until we find the right block
// Check all inputs for ones that are dependents
// If input is NOT connected, set stored value to keyframe at time
// Some inputs we handle ourselves such as FOOTAGE
// If input IS connected, we need to traverse down it
// If more than one input is connected, signal out for a sibling to handle it
// Keep iterating inputs until all are up to date
// Finally get value from output
// If we have shader code, the output is a texture and we handle the I/O
// If we don't, the output is some other value and the Node will produce the output
// Firstly we check if this node is a "Block", if it is that means it's part of a linked list of mutually exclusive
// nodes based on time and we might need to locate which Block to attach to
if ((node = ValidateBlock(node, time)) == nullptr) {
// ValidateBlock() may have returned nullptr if there was no Block found at this time so no texture to return
dep.node()->cache_value(dep.range(), 0);
node->UnlockProcessing();
return;
}
// Ensure output is the output matching the node as it may have changed
output = static_cast<NodeOutput*>(node->GetParameterWithID(output->id()));
// Check if the output already has a value for this time
if (output->has_cached_value(dep.range())) {
// If so, we don't need to do anything, we can just send this value and exit here
dep.node()->cache_value(dep.range(), output->get_cached_value(dep.range()));
node->UnlockProcessing();
return;
}
// We need to run the Node's code to get the correct value for this time
QList<NodeInput*> connected_inputs = ProcessNodeInputsForTime(node, dep.range());
// For each connected input, we need to acquire the value from another node
while (!connected_inputs.isEmpty()) {
// Remove any inputs from the list that we have valid cached values for already
for (int i=0;i<connected_inputs.size();i++) {
NodeInput* input = connected_inputs.at(i);
NodeOutput* connected_output = input->get_connected_output();
TimeRange input_time = node->InputTimeAdjustment(input, dep.range());
if (connected_output->has_cached_value(input_time)) {
// This output already has this value, no need to process it again
input->set_stored_value(connected_output->get_cached_value(input_time));
connected_inputs.removeAt(i);
i--;
}
}
// For every connected input except the first, we'll request another Node to do it
int input_for_this_thread = -1;
for (int i=0;i<connected_inputs.size();i++) {
NodeInput* input = connected_inputs.at(i);
// If this node is locked, we assume it's already being processed. Otherwise we need to request a sibling
if (!input->get_connected_node()->IsProcessingLocked()) {
if (input_for_this_thread == -1) {
// Store this later since we can process it on this thread as we wait for other threads
input_for_this_thread = i;
} else {
TimeRange input_time = node->InputTimeAdjustment(input, dep.range());
emit RequestSibling(NodeDependency(input->get_connected_output(),
input_time));
}
}
}
if (input_for_this_thread > -1) {
// In the mean time, this thread can go off to do the first parameter
NodeInput* input = connected_inputs.at(input_for_this_thread);
TimeRange input_range = node->InputTimeAdjustment(input, dep.range());
RenderAsSibling(NodeDependency(input->get_connected_output(),
input_range));
input->set_stored_value(input->get_connected_output()->get_cached_value(input_range));
connected_inputs.removeAt(input_for_this_thread);
} else {
// Nothing for this thread to do. We'll wait 0.5 sec and check again for other nodes
// FIXME: It would be nicer if this thread could do other nodes during this time
QThread::msleep(500);
}
}
// By this point, the node should have all the inputs it needs to render correctly
// Check if we have a shader for this output
OpenGLShaderPtr shader = shader_cache_->GetShader(output);
if (shader != nullptr) {
// Run code
RunNodeAsShader(node, shader);
} else {
// Generate the value as expected
QVariant value = node->Value(output);
// Place the value into the output
output->cache_value(dep.range(), value);
}
// We're done!
node->UnlockProcessing();
}
+14 -1
View File
@@ -5,14 +5,16 @@
#include <QOffscreenSurface>
#include <QOpenGLContext>
#include "decodercache.h"
#include "node/dependency.h"
#include "openglframebuffer.h"
#include "openglshadercache.h"
#include "render/videoparams.h"
class OpenGLWorker : public QObject {
Q_OBJECT
public:
OpenGLWorker(QOpenGLContext* share_ctx, QObject* parent = nullptr);
OpenGLWorker(QOpenGLContext* share_ctx, OpenGLShaderCache* shader_cache, DecoderCache* decoder_cache, QObject* parent = nullptr);
virtual ~OpenGLWorker() override;
@@ -51,12 +53,19 @@ public slots:
void Render(const NodeDependency& path);
signals:
void RequestSibling(const NodeDependency& path);
private:
void ProcessNode();
void UpdateViewportFromParams();
Node* ValidateBlock(Node* n, const rational& time);
QList<NodeInput*> ProcessNodeInputsForTime(Node* n, const TimeRange& time);
void RunNodeAsShader(Node *node, OpenGLShaderPtr shader);
QOpenGLContext* share_ctx_;
QOpenGLContext* ctx_;
@@ -68,6 +77,10 @@ private:
VideoRenderingParams video_params_;
OpenGLShaderCache* shader_cache_;
DecoderCache* decoder_cache_;
private slots:
void FinishInit();
@@ -187,6 +187,7 @@ void VideoRenderBackend::Close()
foreach (QThread* thread, threads_) {
thread->quit();
thread->wait(); // FIXME: Maximum time in case a thread is stuck?
}
threads_.clear();