giant rendering overhaul and many small fixes

This commit is contained in:
itsmattkc
2019-02-25 13:47:18 -08:00
parent b9d7efa0bc
commit c3373524b1
1429 changed files with 10234 additions and 194593 deletions
+401
View File
@@ -0,0 +1,401 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2019 Olive Team
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 "audio.h"
#include "oliveglobal.h"
#include "project/sequence.h"
#include "panels/panels.h"
#include "io/config.h"
#include "ui/audiomonitor.h"
#include "rendering/renderfunctions.h"
#include "debug.h"
#include <QApplication>
#include <QAudioOutput>
#include <QAudioInput>
#include <QtMath>
#include <QFile>
#include <QDir>
#include <QComboBox>
extern "C" {
#include <libavcodec/avcodec.h>
}
QAudioOutput* audio_output;
QIODevice* audio_io_device;
bool audio_device_set = false;
bool audio_scrub = false;
QMutex audio_write_lock;
QAudioInput* audio_input = nullptr;
QFile output_recording;
bool audio_rendering = false;
bool recording = false;
qint8 audio_ibuffer[audio_ibuffer_size];
qint64 audio_ibuffer_read = 0;
long audio_ibuffer_frame = 0;
double audio_ibuffer_timecode = 0;
AudioSenderThread* audio_thread = nullptr;
bool is_audio_device_set() {
return audio_device_set;
}
QAudioDeviceInfo get_audio_device(QAudio::Mode mode) {
QList<QAudioDeviceInfo> devs = QAudioDeviceInfo::availableDevices(mode);
// try to retrieve preferred device from config
QString preferred_device = (mode == QAudio::AudioOutput) ? olive::CurrentConfig.preferred_audio_output : olive::CurrentConfig.preferred_audio_input;
if (!preferred_device.isEmpty()) {
for (int i=0;i<devs.size();i++) {
// try to match available devices with preferred device
if (devs.at(i).deviceName() == preferred_device) {
return devs.at(i);
}
}
}
// if no preferred output is set, try to get the default device
QAudioDeviceInfo default_device = (mode == QAudio::AudioOutput) ? QAudioDeviceInfo::defaultOutputDevice() : QAudioDeviceInfo::defaultInputDevice();
if (!default_device.isNull()) {
return default_device;
}
// if no default output could be retrieved, just use the first in the list
if (devs.size() > 0) {
return devs.at(0);
}
// couldn't find any audio devices, return null device
return QAudioDeviceInfo();
}
void init_audio() {
stop_audio();
QAudioFormat audio_format;
audio_format.setSampleRate(olive::CurrentConfig.audio_rate);
audio_format.setChannelCount(2);
audio_format.setSampleSize(16);
audio_format.setCodec("audio/pcm");
audio_format.setByteOrder(QAudioFormat::LittleEndian);
audio_format.setSampleType(QAudioFormat::SignedInt);
QAudioDeviceInfo info = get_audio_device(QAudio::AudioOutput);
// see if desired format can be used by the device, use nearest if not
if (!info.isFormatSupported(audio_format)) {
qWarning() << "Audio format is not supported by backend, using nearest";
audio_format = info.nearestFormat(audio_format);
}
audio_output = new QAudioOutput(info, audio_format);
audio_output->moveToThread(QApplication::instance()->thread());
audio_output->setNotifyInterval(5);
// connect
audio_io_device = audio_output->start();
if (audio_io_device == nullptr) {
qWarning() << "Received nullptr audio device. No compatible audio output was found.";
} else {
audio_device_set = true;
// start sender thread
audio_thread = new AudioSenderThread();
QObject::connect(audio_output, SIGNAL(notify()), audio_thread, SLOT(notifyReceiver()));
audio_thread->start(QThread::TimeCriticalPriority);
clear_audio_ibuffer();
}
}
void stop_audio() {
if (audio_device_set) {
audio_thread->stop();
audio_output->stop();
delete audio_output;
audio_device_set = false;
}
}
void clear_audio_ibuffer() {
if (audio_thread != nullptr) audio_thread->lock.lock();
audio_write_lock.lock();
memset(audio_ibuffer, 0, audio_ibuffer_size);
audio_ibuffer_read = 0;
audio_write_lock.unlock();
if (audio_thread != nullptr) audio_thread->lock.unlock();
}
int current_audio_freq() {
return audio_rendering ? olive::ActiveSequence->audio_frequency : audio_output->format().sampleRate();
}
qint64 get_buffer_offset_from_frame(double framerate, long frame) {
if (frame >= audio_ibuffer_frame) {
int multiplier = av_get_bytes_per_sample(AV_SAMPLE_FMT_S16)*av_get_channel_layout_nb_channels(AV_CH_LAYOUT_STEREO);
return qFloor((double(frame - audio_ibuffer_frame)/framerate)*current_audio_freq())*multiplier;
} else {
qWarning() << "Invalid values passed to get_buffer_offset_from_frame" << frame << "<" << audio_ibuffer_frame;
return 0;
}
}
AudioSenderThread::AudioSenderThread() : close(false) {
connect(this, SIGNAL(finished()), this, SLOT(deleteLater()));
}
void AudioSenderThread::stop() {
close = true;
cond.wakeAll();
wait();
}
void AudioSenderThread::notifyReceiver() {
cond.wakeAll();
}
void AudioSenderThread::run() {
// start data loop
send_audio_to_output(0, audio_ibuffer_size);
lock.lock();
while (true) {
cond.wait(&lock);
if (close) {
break;
} else if (panel_sequence_viewer->playing || panel_footage_viewer->playing || audio_scrub) {
int written_bytes = 0;
int adjusted_read_index = audio_ibuffer_read%audio_ibuffer_size;
int max_write = audio_ibuffer_size - adjusted_read_index;
int actual_write = send_audio_to_output(adjusted_read_index, max_write);
written_bytes += actual_write;
if (actual_write == max_write) {
// got all the bytes, write again
written_bytes += send_audio_to_output(0, audio_ibuffer_size);
}
audio_scrub = false;
}
}
lock.unlock();
}
int AudioSenderThread::send_audio_to_output(qint64 offset, int max) {
// send audio to device
qint64 actual_write = audio_io_device->write(reinterpret_cast<const char*>(audio_ibuffer)+offset, max);
qint64 audio_ibuffer_limit = audio_ibuffer_read + actual_write;
if (actual_write > 0) {
// average values and send to audio monitor
int channels = audio_output->format().channelCount();
qint64 lim = offset + actual_write;
QVector<double> averages;
averages.resize(channels);
averages.fill(0);
int counter = 0;
qint16 sample;
for (qint64 i=offset;i<lim;i+=2) {
sample = qint16(((audio_ibuffer[i+1] & 0xFF) << 8) | (audio_ibuffer[i] & 0xFF));
averages[counter] = qMax((double(qAbs(sample))/32768.0), averages[counter]);
counter = (counter+1)%channels;
}
for (int i=0;i<channels;i++) {
averages[i] = log_volume(1.0-(averages[i]));
}
panel_timeline->audio_monitor->set_value(averages);
}
memset(audio_ibuffer+offset, 0, actual_write);
audio_ibuffer_read = audio_ibuffer_limit;
return actual_write;
}
double log_volume(double linear) {
// expects a value between 0 and 1 (or more if amplifying)
return (qExp(linear)-1)/(M_E-1);
}
void int32_to_char_array(qint32 i, char* array) {
memcpy(array, &i, 4);
}
void write_wave_header(QFile& f, const QAudioFormat& format) {
qint32 int32bit;
char arr[4];
// 4 byte riff header
f.write("RIFF");
// 4 byte file size, filled in later
for (int i=0;i<4;i++) f.putChar(0);
// 4 byte file type header + 4 byte format chunk marker
f.write("WAVEfmt");
f.putChar(0x20);
// 4 byte length of the above format data (always 16 bytes)
f.putChar(16);
for (int i=0;i<3;i++) f.putChar(0);
// 2 byte type format (1 is PCM)
f.putChar(1);
f.putChar(0);
// 2 byte channel count
int32bit = format.channelCount();
int32_to_char_array(int32bit, arr);
f.write(arr, 2);
// 4 byte integer for sample rate
int32bit = format.sampleRate();
int32_to_char_array(int32bit, arr);
f.write(arr, 4);
// 4 byte integer for bytes per second
int32bit = (format.sampleRate() * format.sampleSize() * format.channelCount()) / 8;
int32_to_char_array(int32bit, arr);
f.write(arr, 4);
// 2 byte integer for bytes per sample per channel
int32bit = (format.sampleSize() * format.channelCount()) / 8;
int32_to_char_array(int32bit, arr);
f.write(arr, 2);
// 2 byte integer for bits per sample (16)
int32bit = format.sampleSize();
int32_to_char_array(int32bit, arr);
f.write(arr, 2);
// data chunk header
f.write("data");
// 4 byte integer for data chunk size (filled in later)?
for (int i=0;i<4;i++) f.putChar(0);
}
void write_wave_trailer(QFile& f) {
char arr[4];
f.seek(4);
// 4 bytes for total file size - 8 bytes
qint32 file_size = qint32(f.size()) - 8;
int32_to_char_array(file_size, arr);
f.write(arr, 4);
f.seek(40);
// 4 bytes for data chunk size (file size - header)
file_size = qint32(f.size()) - 44;
int32_to_char_array(file_size, arr);
f.write(arr, 4);
}
bool start_recording() {
if (olive::ActiveSequence == nullptr) {
qCritical() << "No active sequence to record into";
return false;
}
QString audio_path = QCoreApplication::translate("Audio", "%1 Audio").arg(olive::ActiveProjectFilename);
QDir audio_dir(audio_path);
if (!audio_dir.exists() && !audio_dir.mkpath(".")) {
qCritical() << "Failed to create audio directory";
return false;
}
QString audio_file_path;
int file_number = 0;
do {
file_number++;
QString audio_filename = QString("%1.wav").arg(
QCoreApplication::translate("Audio", "Recording %1").arg(QString::number(file_number))
);
audio_file_path = audio_dir.filePath(audio_filename);
} while (QFile(audio_file_path).exists());
output_recording.setFileName(audio_file_path);
if (!output_recording.open(QFile::WriteOnly)) {
qCritical() << "Failed to open output file. Does Olive have permission to write to this directory?";
return false;
}
QAudioFormat audio_format = audio_output->format();
if (olive::CurrentConfig.recording_mode != audio_format.channelCount()) {
audio_format.setChannelCount(olive::CurrentConfig.recording_mode);
}
QAudioDeviceInfo info = get_audio_device(QAudio::AudioInput);
if (!info.isFormatSupported(audio_format)) {
qWarning() << "Default format not supported, using nearest";
audio_format = info.nearestFormat(audio_format);
}
write_wave_header(output_recording, audio_format);
audio_input = new QAudioInput(info, audio_format);
audio_input->start(&output_recording);
recording = true;
return true;
}
void stop_recording() {
if (recording) {
audio_input->stop();
write_wave_trailer(output_recording);
output_recording.close();
delete audio_input;
audio_input = nullptr;
recording = false;
}
}
QString get_recorded_audio_filename() {
return output_recording.fileName();
}
void combobox_audio_sample_rates(QComboBox *combobox) {
combobox->addItem("22050 Hz", 22050);
combobox->addItem("24000 Hz", 24000);
combobox->addItem("32000 Hz", 32000);
combobox->addItem("44100 Hz", 44100);
combobox->addItem("48000 Hz", 48000);
combobox->addItem("88200 Hz", 88200);
combobox->addItem("96000 Hz", 96000);
}
+81
View File
@@ -0,0 +1,81 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2019 Olive Team
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/>.
***/
#ifndef AUDIO_H
#define AUDIO_H
#include <QVector>
#include <QThread>
#include <QWaitCondition>
#include <QMutex>
#include <QIODevice>
#include <QAudioOutput>
#include <QComboBox>
#include "project/sequence.h"
class AudioSenderThread : public QThread {
Q_OBJECT
public:
AudioSenderThread();
void run();
void stop();
QWaitCondition cond;
bool close;
QMutex lock;
public slots:
void notifyReceiver();
private:
QVector<qint16> samples;
int send_audio_to_output(qint64 offset, int max);
};
double log_volume(double linear);
extern QAudioOutput* audio_output;
extern QIODevice* audio_io_device;
extern AudioSenderThread* audio_thread;
extern QMutex audio_write_lock;
#define audio_ibuffer_size 192000
extern qint8 audio_ibuffer[audio_ibuffer_size];
extern qint64 audio_ibuffer_read;
extern long audio_ibuffer_frame;
extern double audio_ibuffer_timecode;
extern bool audio_scrub;
extern bool recording;
extern bool audio_rendering;
void clear_audio_ibuffer();
int current_audio_freq();
bool is_audio_device_set();
void init_audio();
void stop_audio();
qint64 get_buffer_offset_from_frame(double framerate, long frame);
bool start_recording();
void stop_recording();
QString get_recorded_audio_filename();
void combobox_audio_sample_rates(QComboBox* combobox);
#endif // AUDIO_H
+1269
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+573
View File
@@ -0,0 +1,573 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2019 Olive Team
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/>.
***/
#ifndef CACHER_H
#define CACHER_H
extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libswscale/swscale.h>
#include <libswresample/swresample.h>
#include <libavfilter/avfilter.h>
#include <libavfilter/buffersrc.h>
#include <libavfilter/buffersink.h>
#include <libavutil/opt.h>
#include <libavutil/pixdesc.h>
}
#include <memory>
#include <QThread>
#include <QVector>
#include <QWaitCondition>
#include <QMutex>
#include "rendering/clipqueue.h"
class Clip;
using ClipPtr = std::shared_ptr<Clip>;
/**
* @brief The Cacher class
*
* For footage clips - usually the majority of clips - decoding can be strenuous on CPU and inconsistent in timing. As
* a result, we keep a memory cache of upcoming frames that we fill in a background thread so they can be retrieved from
* a rendering thread later. This class is the background thread filling up a clip's frame cache (also called a "queue"
* since video files are usually stored with frames in linear chronological order). It involves decoding routines to
* retrieve raw frames from the file (using libavformat/libavcodec), conversion routines to conform the raw frames to
* RGBA/S16LE for the rest of the workflow (using libavfilter/libswscale/libswresample), and memory handling routines
* for keeping the cache within limits defined by the user (see Config::upcoming_queue_type).
*
* Generally the Cacher workflow starts by calling Open() which will start the thread, open a file handle, and create a
* decoding instance. Open() is usually called directly from the parent Clip's Clip::Open() and thus expects the
* Clip::state_change_lock to be locked. It will unlock it when it's finished opening and is ready to start caching,
* meaning Clip::state_change_lock can be used to synchronize threads.
*
* ---
*
* **For video:**
*
* After the Cacher has finished opening, request a frame by calling Cache(). Cache() will tell the Cacher
* information about the current playback state, most importantly the current place in time according to the Sequence's
* playhead. Cache() determines whether the requested frame is already in the queue, and then signals the Cacher thread
* to cache ahead if there's room in the queue (and also remove old frames that are no longer necessary). To retrieve
* the requested frame, call Retrieve().
*
* If Cache() found the frame already in the queue, Retrieve() will return immediately with this frame. Otherwise
* Retrieve() may block while the cacher retrieves it. Therefore it is recommended never to call Retrieve() from
* the main thread. Retrieve() may also return `nullptr` if there was an issue, e.g. the cacher failed to retrieve
* the frame.
*
* **For audio:**
*
* After the Cacher has finished opening, calling Cache() will handle most of the work. It will decode the audio,
* convert to the correct sample rate and format, reverse or adjust speed if necessary, and send it to the audio
* buffer ready to be played by the output device. It is important to continually call Cache() as it doesn't get
* signalled when more samples are available in the audio buffer. Instead, it'll check every time it's called and
* fill as much of the buffer as it can.
*
* If the user seeks, ResetAudio() must be called to signal the Cacher to interrupt the current audio stream and
* move somewhere else before continuing.
*
* ---
*
* Finally, when the Cacher/parent Clip are no longer in use, call Close() to free all memory and file handling
* allocated for the cacher. You can choose whether to wait for Close() and all of its child processes to complete -
* e.g. if you need to change something with the Clip or attached Footage that changes how it opens and want to be
* thread-safe - or let the Close thread finish up on its own.
*
* Cacher expects to be multithreaded and all of its public functions are thread-safe.
*/
class Cacher : public QThread
{
Q_OBJECT
public:
/**
* @brief Cacher Constructor
*
* Create Cacher object. The thread is not started here. To start it, call Open().
*
* @param c
*/
Cacher(ClipPtr c);
/**
* @brief The main QThread loop
*
* Once the thread has started, all Cacher functions will be called from here until the Cacher closes at which point
* it will close and exit gracefully.
*/
void run();
/**
* @brief Open the cacher
*
* Starts the thread and all file/decode handlers. Really just sets some default values and starts the thread, which
* will in turn call OpenWorker() at the start of its functions.
*
* Make sure Clip::state_change_lock is LOCKED before calling this function as the opening process will try to unlock
* it when it's finished (leading to a crash if it's not already locked).
*/
void Open();
/**
* @brief Request a frame to be cached
*
* For video, this function is part 1 of the Cache()/Retrieve() workflow. It signals the thread to start caching and
* provides a few other details about the playback state. For optimization it'll also check the frame queue if it
* already contains the requested frame and use it if so, potentially speeding up Retrieve() later on. Otherwise
* it'll interrupt any currently caching operation and signal it to start again.
* While Retrieve() will block until the correct frame is retrieved, this function will return fairly quickly (either
* immediately if the frame was found in the queue, or once the cacher has restarted caching if not). This means
* Cache() can be called from another thread and then that other thread can do other work while the cacher is
* retrieving the frame, finally calling Retrieve() once the frame is absolutely necessary.
*
* For audio, this function will do all the work of signalling the thread to start caching and sending samples to
* the output audio buffer. It's used in tandem with ResetAudio() when the Timeline header is changed abruptly.
*
* @param playhead
*
* The current Timeline played position in frames
*
* @param scrubbing
*
* **TRUE** if the user is currently scrubbing. **FALSE** if not.
*
* @param nests
*
* A hierarchy of nested sequences, if the playback traversed any to get to this clip.
*
* @param playback_speed
*
* The current playback speed (controlled by Shuttle Left/Stop/Right)
*/
void Cache(long playhead, bool scrubbing, QVector<ClipPtr>& nests, int playback_speed);
/**
* @brief Retrieve frame requested by Cache()
*
* Part 2 of the Cache()/Retrieve() workflow, only used for video. Whichever frame was requested by Cache(), this
* function will try to retrieve it. In most cases, this function will be pretty quick as the frame will be available
* immediately from Cache()'s optimization or the cacher thread will be close to retrieving the correct frame anyway.
* However it does block for however long it takes to retrieve the correct frame (if the cacher is running) so it's
* not recommended to call this from any main/GUI thread.
*
* @return
*
* The frame requested by Cache(), or `nullptr` if there was an error (e.g. the cacher wasn't running and no frame was
* available).
*/
AVFrame* Retrieve();
/**
* @brief Close the cacher and free any allocated memory
*
* When the Cacher thread is no longer needed, Close() should be called in order to free system resources. This will
* signal the thread to exit gracefully, but will not delete the thread object since the cacher may need to be
* re-opened later by Open().
*
* @param wait_for_finish
*
* **TRUE** if this function should block the calling thread until the Clip has finished closing. Often necessary if
* the Clip is being closed specifically to make changes to it.
*/
void Close(bool wait_for_finish);
/**
* @brief Interrupt and reset audio state
*
* Used in tandem with Cache(), only for audio clips. Cache() will decode and send audio continually as it's
* repeatedly called. If the audio stream needs to be interrupted and moved somewhere else for any reason
* (e.g. the user seeked somewhere else), then it's necessary to call ResetAudio() to signal the cacher to
* seek to the next place indicated by Cache().
*/
void ResetAudio();
/**
* @brief Retrieve current media width
*
* In some situations, the actual media we're using may be a different resolution to how we're treating it (e.g.
* lower resolution proxies). While most functions will happily treat the media as its original resolution, some
* processes will need the absolute resolution from the file which can be acquired here.
*
* Only call after the thread has been opened by Open().
*
* @return
*
* The true width of the current video file.
*/
int media_width();
/**
* @brief Retrieve current media height
*
* See media_width().
*
* Only call after the thread has been opened by Open().
*
* @return
*
* The true height of the current video file.
*/
int media_height();
/**
* @brief Retrieve media time base
*
* For some timing operations, it's necessary to use the source media's timebase. Similar to media_width() and
* media_height(), we need the accurate timebase from the file as a proxy's timebase may or may not be the same
* as the source file.
*
* Only call after the thread has been opened by Open().
*
* @return
*
* The timebase of the file.
*/
AVRational media_time_base();
/**
* @brief Wrapper function for queue::lock()
*/
void QueueLock();
/**
* @brief Wrapper function for queue::unlock()
*/
void QueueUnlock();
private:
/**
* @brief Reference to the parent clip. Set in the constructor and never changed during this object's lifetime.
*/
ClipPtr clip;
/**
* @brief Frame queue
*
* Valid fames are cached into this, which also does memory handling when necessary.
*/
ClipQueue queue;
/**
* @brief Main wait condition
*
* Used with Clip::cache_lock as the main block while the the Cacher thread isn't running. Wake this condition
* to start caching.
*/
QWaitCondition wait_cond_;
/**
* @brief Main thread wait condition
*
* Used with main_thread_lock_ to block Cache() while waiting for a response from the cacher thread.
*/
QWaitCondition main_thread_wait_;
/**
* @brief Main thread mutex
*
* Used with main_thread_wait_ to block Cache() while waiting for a response from the cacher thread.
*/
QMutex main_thread_lock_;
/**
* @brief Retrieve() wait condition
*
* Used with retrieve_lock_ to block Retrieve() if the cacher hasn't retrieved the correct frame yet.
*/
QWaitCondition retrieve_wait_;
/**
* @brief Retrieve() mutex
*
* Used with retrieve_wait_ to block Retrieve() if the cacher hasn't retrieved the correct frame yet.
*/
QMutex retrieve_lock_;
/**
* @brief Set and used by CacheAudioWorker if the decoder receives an EOF.
*
* Deprecated. CacheAudioWorker() is functional but probably should be rewritten.
*/
bool reached_end;
/**
* @brief Current Sequence playhead set by Cache()
*/
long playhead_;
/**
* @brief Current Sequence scrubbing state set by Cache()
*/
bool scrubbing_;
/**
* @brief Current Sequence playback speed set by Cache()
*/
int playback_speed_;
/**
* @brief Current nested Sequence hierarchy set by Cache()
*/
QVector<ClipPtr> nests_;
/**
* @brief Signal cache to continue operation after one cycle rather than wait for another signal
*
* Each cycle of the cacher thread (see run()) will set this to false in the beginning. Each call of Cache() will set
* this to **TRUE**. If this variable is **TRUE**, the cacher won't wait for another signal before starting the next
* cache cycle, and will instead just start it.
*
* Used if Cache() is called and interrupts the cacher while it's already running so that the cacher will restart
* itself automatically rather than wait for the next cache signal.
*/
bool queued_;
/**
* @brief Interrupt the current cache cycle
*
* A cache cycle will cache several frames at a time. Since decoding can be strenuous and time consuming, the
* cycle can be interrupted if it needs to abruptly start caching somewhere else. Best used in tandem with
* queued_ to automatically start the next cache cycle.
*/
bool interrupt_;
// ffmpeg media handling
/**
* @brief FFmpeg format/file context - used for media decoding
*/
AVFormatContext* formatCtx;
/**
* @brief FFmpeg decoder context - used for media decoding
*/
AVCodecContext* codecCtx;
/**
* @brief FFmpeg stream - used for media decoding
*/
AVStream* stream;
/**
* @brief FFmpeg packet - used for media decoding
*/
AVPacket* pkt;
/**
* @brief FFmpeg frame - used for media decoding
*
* This is usually used as a raw decoded frame before the RGBA conversion/AVFilter stack. Converted/filtered frames go
* into Cacher::queue.
*/
AVFrame* frame_;
/**
* @brief Retrieved frame reference for Retrieve()
*
* If a frame was found by either Cache() or CacheVideoWorker(), it's set here. If no frame is ready yet, this is set
* to `nullptr`.
*/
AVFrame* retrieved_frame = nullptr;
// converters/filters
/**
* @brief FFmpeg filter stack
*
* Used for conversion from the media's pixel format to RGBA for OpenGL. Also any other FFmpeg filters are implemented
* here if necessary (e.g. yadif for deinterlacing). GLSL effects are preferred when available since FFmpeg filters
* aren't always fast enough for realtime playback.
*/
AVFilterGraph* filter_graph;
/**
* @brief FFmpeg buffer source
*
* Raw decoded frames are added to this for conversion/filtering
*/
AVFilterContext* buffersrc_ctx;
/**
* @brief FFmpeg buffer sink
*
* Converted/filtered frames are retrieved from here and sent to Cacher::queue.
*/
AVFilterContext* buffersink_ctx;
/**
* @brief FFmpeg codec reference
*/
AVCodec* codec;
/**
* @brief Options set by the cacher for FFmpeg's decoders (settings like multithreading or other optimizations)
*/
AVDictionary* opts;
// audio playback variables
/**
* @brief Internal audio reset variable
*
* Set by AudioReset() and read by CacheAudioWorker() when the audio state needs to be interrupted and reset.
*/
bool audio_reset_;
/**
* @brief Internal reverse target variable
*
* Used by CacheAudioWorker() to stitch audio frames together when reversing. Stores the current frame's timestamp
* so it knows how much to decode up to when it backtracks and decodes the next samples.
*/
int64_t reverse_target_;
/**
* @brief Internal frame sample index variable
*
* Used by CacheAudioWorker() to mark which part of the audio frame to read from
*/
int frame_sample_index_;
/**
* @brief Internal audio buffer write variable
*
* Used by CacheAudioWorker() to mark which part of the audio buffer to write to
*/
qint64 audio_buffer_write;
/**
* @brief Internal variable that holds the playhead the last time the audio state was reset
*/
long audio_target_frame;
/**
* @brief Main while loop condition to determine whether thread should continue looping
*
* Open() sets this to **TRUE**, Close() sets this to **FALSE**. If it's false, the main loop in run() will exit and
* the thread will exit cleanly. It's not recommended to set this variable directly, use Open() and Close() instead.
*/
bool caching_;
/**
* @brief Internal function for opening the file handles and decoder
*
* After the thread has started, it'll call this function to start all resources necessary for caching. Any
* FFmpeg decoding variables and filters are set up here.
*
* This is
* fundamentally different from Open(), this is only meant to be called within the cacher thread and never from
* outside and doesn't start the thread like Open() does.
*/
void OpenWorker();
/**
* @brief Internal function for starting a cache cycle
*
* This used to have more function, but now just differentiates between CacheVideoWorker() for video clips and
* CacheAudioWorker() for audio clips.
*/
void CacheWorker();
/**
* @brief Internal function for closing cacher
*
* Called if the main thread loop in run() exits by setting caching_ to **FALSE**. Free's up handles and memory
* allocated by OpenWorker().
*/
void CloseWorker();
/**
* @brief Internal function for resetting audio state
*
* This used to be a common function, but is now simply a legacy function for CacheAudioWorker(). Resets and
* flushes decoders and seeks to the correct timestamp.
*/
void Reset();
/**
* @brief Internal function for setting retrieved_frame and waking up any threads waiting for it
*
* @param f
*
* The frame to set as the retrieved frame.
*/
void SetRetrievedFrame(AVFrame* f);
/**
* @brief Internal function to wake an external calling thread
*
* In some situations, Cache() may wait for the cacher to respond before returning. This is to assist in thread
* synchronization, making sure the cacher has started working and has locked any resources it needs before any
* other threads can access them (e.g. with a function like Retrieve() ). This must be called at the start of
* any CacheVideoWorker() or CacheAudioWorker() control paths to ensure the render thread doesn't get stuck.
*/
void WakeMainThread();
/**
* @brief Retrieve frame from decoder
*
* Retrieves the next decoded frame from the decoder. Depending on the source media, this frame may or may not be
* suitable for usage later in the pipeline as it may or may not be the correct pixel/sample format. For a suitable
* frame for the pipeline, use RetrieveFrameAndProcess() instead (which in turn uses this function anyway).
*
* @param f
*
* Frame buffer to decode frame into
*
* @return
*
* FFmpeg error code (>= 0 on success, a negative error code on failure)
*/
int RetrieveFrameFromDecoder(AVFrame* f);
/**
* @brief Retrieve frame from decoder and run it through filter stack
*
* Retrieves the next decoded frame and runs it through the AVFilter stack to create an RGBA frame compatible with
* the rest of the pipeline and OpenGL. Use this function if you need a ready-made frame.
*
* @param f
*
* The AVFrame to retrieve. This function allocates an AVFrame so you shouldn't do so beforehand. You'll also need to
* free it later with av_frame_free() (though ClipQueue will do this automatically if the frame is added to it).
*
* @return
*
* FFmpeg error code (>= 0 on success, a negative error code on failure)
*/
int RetrieveFrameAndProcess(AVFrame *f);
/**
* @brief Internal video caching function
*
* Performs one video cache cycle. Seeks the media and cleans old frames from the queue if necessary. Decodes frames
* and adds them to the queue (after calculating whether they're necessary).
*/
void CacheVideoWorker();
/**
* @brief Internal audio caching function
*
* Perform one audio cache cycle. Retrieves audio from decoder, reverses and changes speed if necessary, and sends
* audio to the audio buffer which will later be sent to the audio output device.
*/
void CacheAudioWorker();
};
#endif // CACHER_H
+95
View File
@@ -0,0 +1,95 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2019 Olive Team
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 "clipqueue.h"
ClipQueue::ClipQueue()
{
}
ClipQueue::~ClipQueue()
{
clear();
}
void ClipQueue::lock()
{
queue_lock.lock();
}
bool ClipQueue::tryLock()
{
return queue_lock.tryLock();
}
void ClipQueue::unlock()
{
queue_lock.unlock();
}
void ClipQueue::append(AVFrame *frame)
{
queue.append(frame);
}
AVFrame *ClipQueue::at(int i)
{
return queue.at(i);
}
AVFrame *ClipQueue::first()
{
return queue.first();
}
AVFrame *ClipQueue::last()
{
return queue.last();
}
void ClipQueue::removeFirst()
{
removeAt(0);
}
void ClipQueue::removeLast()
{
removeAt(queue.size()-1);
}
void ClipQueue::removeAt(int i)
{
av_frame_free(&queue[i]);
queue.removeAt(i);
}
void ClipQueue::clear()
{
while (queue.size() > 0) {
removeAt(0);
}
}
int ClipQueue::size()
{
return queue.size();
}
+167
View File
@@ -0,0 +1,167 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2019 Olive Team
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/>.
***/
#ifndef CLIPQUEUE_H
#define CLIPQUEUE_H
extern "C" {
#include <libavformat/avformat.h>
}
#include <QVector>
#include <QMutex>
/**
* @brief The ClipQueue class
*
* A fairly simple wrapper for a QVector and QMutex that cleans up AVFrames automatically when removing them.
*/
class ClipQueue {
public:
/**
* @brief ClipQueue Constructor
*/
ClipQueue();
/**
* @brief ClipQueue Destructor
*
* Automatically clears queue freeing any memory consumed by any AVFrames
*/
~ClipQueue();
// Thread safety (QMutex compatible)
/**
* @brief Lock queue mutex
*
* Used for multithreading to ensure queue is only accessed by one thread at a time. See QMutex::lock() for more
* information.
*/
void lock();
/**
* @brief Try to lock queue mutex
*
* Used for multithreading to ensure queue is only accessed by one thread at a time. Tries to lock, but doesn't block
* the calling thread and wait if it can't lock it. See QMutex::tryLock() for more information.
*
* @return
*
* **TRUE** if the lock succeeded, **FALSE** if not.
*/
bool tryLock();
/**
* @brief Unlock queue mutex
*
* Used for multithreading to ensure queue is only accessed by one thread at a time. See QMutex::unlock() for more
* information.
*/
void unlock();
// Array handling (QVector compatible)
/**
* @brief Add a frame to the end of the queue
*
* @param frame
*
* The frame to add
*/
void append(AVFrame* frame);
/**
* @brief Retrieve a frame at a certain index
*
* @param i
*
* Index to retrieve frame from
*
* @return
*
* AVFrame at this index
*/
AVFrame* at(int i);
/**
* @brief Retrieve first frame in the queue
*
* @return
*
* The first AVFrame in the queue
*/
AVFrame* first();
/**
* @brief Retrieve last frame in the queue
*
* @return
*
* The last AVFrame in the queue
*/
AVFrame* last();
/**
* @brief Remove first frame in the queue
*
* Frees all memory occupied by this frame and removes it from the queue
*/
void removeFirst();
/**
* @brief Remove last frame in the queue
*
* Frees all memory occupied by this frame and removes it from the queue
*/
void removeLast();
/**
* @brief Remove frame in the queue at a certain index
*
* Frees all memory occupied by this frame and removes it from the queue
*
* @param i
*
* Index to remove a frame at
*/
void removeAt(int i);
/**
* @brief Clear entire queue
*
* Frees all memory occupied by all frames and clears the entire queue
*/
void clear();
/**
* @brief Retrieve current size of the queue
*
* @return
*
* Current the current size of the queue. All indexes in the queue are guaranteed to be valid references to an
* AVFrame.
*/
int size();
private:
QVector<AVFrame*> queue;
QMutex queue_lock;
};
#endif // CLIPQUEUE_H
+736
View File
@@ -0,0 +1,736 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2019 Olive Team
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 "renderfunctions.h"
extern "C" {
#include <libavformat/avformat.h>
}
#include <QOpenGLFramebufferObject>
#include <QApplication>
#include <QDesktopWidget>
#include <QDebug>
#ifdef OLIVE_OCIO
#include <OpenColorIO/OpenColorIO.h>
namespace OCIO = OCIO_NAMESPACE;
#endif
#include "project/clip.h"
#include "project/sequence.h"
#include "project/media.h"
#include "project/effect.h"
#include "project/footage.h"
#include "project/transition.h"
#include "ui/collapsiblewidget.h"
#include "rendering/audio.h"
#include "io/math.h"
#include "io/config.h"
#include "panels/timeline.h"
#include "panels/viewer.h"
const int kMaximumRetryCount = 10;
void full_blit() {
glPushMatrix();
glLoadIdentity();
glOrtho(0, 1, 0, 1, -1, 1);
glBegin(GL_QUADS);
glTexCoord2f(0, 0); // top left
glVertex2f(0, 0); // top left
glTexCoord2f(1, 0); // top right
glVertex2f(1, 0); // top right
glTexCoord2f(1, 1); // bottom right
glVertex2f(1, 1); // bottom right
glTexCoord2f(0, 1); // bottom left
glVertex2f(0, 1); // bottom left
glEnd();
glPopMatrix();
}
void draw_clip(QOpenGLContext* ctx, GLuint fbo, GLuint texture, bool clear) {
ctx->functions()->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);
if (clear) {
glClear(GL_COLOR_BUFFER_BIT);
}
glBindTexture(GL_TEXTURE_2D, texture);
full_blit();
glBindTexture(GL_TEXTURE_2D, 0);
ctx->functions()->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
}
GLuint draw_clip(QOpenGLFramebufferObject* fbo, GLuint texture, bool clear) {
fbo->bind();
if (clear) {
glClear(GL_COLOR_BUFFER_BIT);
}
glBindTexture(GL_TEXTURE_2D, texture);
full_blit();
glBindTexture(GL_TEXTURE_2D, 0);
fbo->release();
return fbo->texture();
}
void process_effect(ClipPtr c,
EffectPtr e,
double timecode,
GLTextureCoords& coords,
GLuint& composite_texture,
bool& fbo_switcher,
bool& texture_failed,
int data) {
if (e->is_enabled()) {
if (e->enable_coords) {
e->process_coords(timecode, coords, data);
}
bool can_process_shaders = (e->enable_shader && olive::CurrentRuntimeConfig.shaders_are_enabled);
if (can_process_shaders || e->enable_superimpose) {
e->startEffect();
if (can_process_shaders && e->is_glsl_linked()) {
for (int i=0;i<e->getIterations();i++) {
e->process_shader(timecode, coords, i);
composite_texture = draw_clip(c->fbo[fbo_switcher], composite_texture, true);
fbo_switcher = !fbo_switcher;
}
}
if (e->enable_superimpose) {
GLuint superimpose_texture = e->process_superimpose(timecode);
if (superimpose_texture == 0) {
qWarning() << "Superimpose texture was nullptr, retrying...";
texture_failed = true;
} else if (composite_texture == 0) {
// if there is no previous texture, just return the superimposes texture
// UNLESS this is a shader-extended superimpose effect in which case,
// we'll need to draw it below
composite_texture = superimpose_texture;
} else {
// if the source texture is not already a framebuffer texture,
// we'll need to make it one before drawing a superimpose effect on it
if (composite_texture != c->fbo[0]->texture() && composite_texture != c->fbo[1]->texture()) {
draw_clip(c->fbo[!fbo_switcher], composite_texture, true);
}
composite_texture = draw_clip(c->fbo[!fbo_switcher], superimpose_texture, false);
}
}
e->endEffect();
}
}
}
GLuint compose_sequence(ComposeSequenceParams &params) {
// qint64 time = QDateTime::currentMSecsSinceEpoch();
GLuint final_fbo = params.main_buffer;
SequencePtr s = params.seq;
long playhead = s->playhead;
if (!params.nests.isEmpty()) {
for (int i=0;i<params.nests.size();i++) {
s = params.nests.at(i)->media()->to_sequence();
playhead += params.nests.at(i)->clip_in(true) - params.nests.at(i)->timeline_in(true);
playhead = rescale_frame_number(playhead, params.nests.at(i)->sequence->frame_rate, s->frame_rate);
}
if (params.video && params.nests.last()->fbo != nullptr) {
params.nests.last()->fbo[0]->bind();
glClear(GL_COLOR_BUFFER_BIT);
final_fbo = params.nests.last()->fbo[0]->handle();
}
}
int audio_track_count = 0;
QVector<ClipPtr> current_clips;
// loop through clips, find currently active, and sort by track
for (int i=0;i<s->clips.size();i++) {
ClipPtr c = s->clips.at(i);
if (c != nullptr) {
// if clip is video and we're processing video
if ((c->track() < 0) == params.video) {
bool clip_is_active = false;
// is the clip a "footage" clip?
if (c->media() != nullptr && c->media()->get_type() == MEDIA_TYPE_FOOTAGE) {
FootagePtr m = c->media()->to_footage();
// does the clip have a valid media source?
if (!m->invalid && !(c->track() >= 0 && !is_audio_device_set())) {
// is the media process and ready?
if (m->ready) {
const FootageStream* ms = c->media_stream();
// does the media have a valid media stream source and is it active?
if (ms != nullptr && c->IsActiveAt(playhead)) {
// open if not open
if (!c->IsOpen()) {
c->Open();
}
clip_is_active = true;
// increment audio track count
if (c->track() >= 0) audio_track_count++;
} else if (c->IsOpen()) {
// close the clip if it isn't active anymore
c->Close(false);
}
} else {
// media wasn't ready, schedule a redraw
params.texture_failed = true;
}
}
} else {
// if the clip is a nested sequence or null clip, just open it
if (c->IsActiveAt(playhead)) {
if (!c->IsOpen()) {
c->Open();
}
clip_is_active = true;
} else if (c->IsOpen()) {
c->Close(false);
}
}
// if the clip is active, added it to "current_clips", sorted by track
if (clip_is_active) {
bool added = false;
// track sorting is only necessary for video clips
// audio clips are mixed equally, so we skip sorting for those
if (params.video) {
// insertion sort by track
for (int j=0;j<current_clips.size();j++) {
if (current_clips.at(j)->track() < c->track()) {
current_clips.insert(j, c);
added = true;
break;
}
}
}
if (!added) {
current_clips.append(c);
}
}
}
}
}
if (params.video) {
// set default coordinates based on the sequence, with 0 in the direct center
glPushMatrix();
glLoadIdentity();
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
int half_width = s->width/2;
int half_height = s->height/2;
glOrtho(-half_width, half_width, -half_height, half_height, -1, 10);
}
// loop through current clips
for (int i=0;i<current_clips.size();i++) {
ClipPtr c = current_clips.at(i);
bool got_mutex = true;
if (params.wait_for_mutexes) {
// wait for clip to finish opening
c->state_change_lock.lock();
} else {
got_mutex = c->state_change_lock.tryLock();
}
if (got_mutex && c->IsOpen()) {
// if clip is a video clip
if (c->track() < 0) {
// reset OpenGL to full color
glColor4f(1.0, 1.0, 1.0, 1.0);
// textureID variable contains texture to be drawn on screen at the end
GLuint textureID = 0;
// store video source dimensions
int video_width = c->media_width();
int video_height = c->media_height();
// if media is footage
if (c->media() != nullptr && c->media()->get_type() == MEDIA_TYPE_FOOTAGE) {
// retrieve video frame from cache and store it in c->texture
c->Cache(qMax(playhead, c->timeline_in()), false, false, params.nests, params.playback_speed);
if (!c->Retrieve()) {
params.texture_failed = true;
} else {
// retrieve ID from c->texture
textureID = c->texture->textureId();
}
if (textureID == 0) {
qWarning() << "Failed to create texture";
}
}
// prepare framebuffers for backend drawing operations
if (c->fbo == nullptr) {
// create 3 fbos for nested sequences, 2 for most clips
int fbo_count = (c->media() != nullptr && c->media()->get_type() == MEDIA_TYPE_SEQUENCE) ? 3 : 2;
c->fbo = new QOpenGLFramebufferObject* [size_t(fbo_count)];
for (int j=0;j<fbo_count;j++) {
c->fbo[j] = new QOpenGLFramebufferObject(video_width, video_height);
}
}
// if clip should actually be shown on screen in this frame
if (playhead >= c->timeline_in(true)
&& playhead < c->timeline_out(true)) {
glPushMatrix();
// simple bool for switching between the two framebuffers
bool fbo_switcher = false;
glViewport(0, 0, video_width, video_height);
if (c->media() != nullptr) {
if (c->media()->get_type() == MEDIA_TYPE_SEQUENCE) {
// for a nested sequence, run this function again on that sequence and retrieve the texture
// add nested sequence to nest list
params.nests.append(c);
// compose sequence
textureID = compose_sequence(params);
// remove sequence from nest list
params.nests.removeLast();
// compose_sequence() would have written to this clip's fbo[0], so we switch to fbo[1]
fbo_switcher = true;
} else if (c->media()->get_type() == MEDIA_TYPE_FOOTAGE) {
if (!c->media()->to_footage()->alpha_is_premultiplied) {
// alpha is not premultiplied, we'll need to multiply it for the rest of the pipeline
params.premultiply_program->bind();
textureID = draw_clip(c->fbo[0], textureID, true);
params.premultiply_program->release();
fbo_switcher = true;
}
#ifdef OLIVE_OCIO
// convert to linear colorspace
bool linear_convert = true;
if (linear_convert)
{
}
#endif
}
}
// set up default coordinates for drawing the clip
GLTextureCoords coords;
coords.grid_size = 1;
coords.vertexTopLeftX = coords.vertexBottomLeftX = -video_width/2;
coords.vertexTopLeftY = coords.vertexTopRightY = -video_height/2;
coords.vertexTopRightX = coords.vertexBottomRightX = video_width/2;
coords.vertexBottomLeftY = coords.vertexBottomRightY = video_height/2;
coords.vertexBottomLeftZ = coords.vertexBottomRightZ = coords.vertexTopLeftZ = coords.vertexTopRightZ = 1;
coords.textureTopLeftY = coords.textureTopRightY = coords.textureTopLeftX = coords.textureBottomLeftX = 0.0;
coords.textureBottomLeftY = coords.textureBottomRightY = coords.textureTopRightX = coords.textureBottomRightX = 1.0;
coords.textureTopLeftQ = coords.textureTopRightQ = coords.textureTopLeftQ = coords.textureBottomLeftQ = 1;
coords.blendmode = BLEND_MODE_NORMAL;
coords.opacity = 1.0;
// if auto-scale is enabled, auto-scale the clip
if (c->autoscaled() && (video_width != s->width && video_height != s->height)) {
float width_multiplier = float(s->width) / float(video_width);
float height_multiplier = float(s->height) / float(video_height);
float scale_multiplier = qMin(width_multiplier, height_multiplier);
glScalef(scale_multiplier, scale_multiplier, 1);
}
// == EFFECT CODE START ==
// get current sequence time in seconds (used for effects)
double timecode = get_timecode(c, playhead);
// set up variables for gizmos later
EffectPtr first_gizmo_effect = nullptr;
EffectPtr selected_effect = nullptr;
// run through all of the clip's effects
for (int j=0;j<c->effects.size();j++) {
EffectPtr e = c->effects.at(j);
process_effect(c, e, timecode, coords, textureID, fbo_switcher, params.texture_failed, kTransitionNone);
// retrieve gizmo data from effect
if (e->are_gizmos_enabled()) {
if (first_gizmo_effect == nullptr) first_gizmo_effect = e;
if (e->container->selected) selected_effect = e;
}
}
// using gizmo data, set definitive gizmo
if (selected_effect != nullptr) {
(*params.gizmos) = selected_effect;
} else if (is_clip_selected(c, true)) {
(*params.gizmos) = first_gizmo_effect;
}
// if the clip has an opening transition, process that now
if (c->opening_transition != nullptr) {
int transition_progress = playhead - c->timeline_in(true);
if (transition_progress < c->opening_transition->get_length()) {
process_effect(c, c->opening_transition, double(transition_progress)/double(c->opening_transition->get_length()), coords, textureID, fbo_switcher, params.texture_failed, kTransitionOpening);
}
}
// if the clip has a closing transition, process that now
if (c->closing_transition != nullptr) {
int transition_progress = playhead - (c->timeline_out(true) - c->closing_transition->get_length());
if (transition_progress >= 0 && transition_progress < c->closing_transition->get_length()) {
process_effect(c, c->closing_transition, double(transition_progress)/double(c->closing_transition->get_length()), coords, textureID, fbo_switcher, params.texture_failed, kTransitionClosing);
}
}
// == EFFECT CODE END ==
if (textureID > 0) {
// set viewport to sequence size
params.ctx->functions()->glViewport(0, 0, s->width, s->height);
// == START RENDER CLIP IN CONTEXT OF SEQUENCE ==
// use clip textures for nested sequences, otherwise use main frame buffers
GLuint back_buffer_1;
GLuint backend_tex_1;
GLuint backend_tex_2;
if (params.nests.size() > 0) {
back_buffer_1 = params.nests.last()->fbo[1]->handle();
backend_tex_1 = params.nests.last()->fbo[1]->texture();
backend_tex_2 = params.nests.last()->fbo[2]->texture();
} else {
back_buffer_1 = params.backend_buffer1;
backend_tex_1 = params.backend_attachment1;
backend_tex_2 = params.backend_attachment2;
}
// render a backbuffer
params.ctx->functions()->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, back_buffer_1);
glClearColor(0.0, 0.0, 0.0, 0.0);
glClear(GL_COLOR_BUFFER_BIT);
// bind final clip texture
glBindTexture(GL_TEXTURE_2D, textureID);
// set texture filter to bilinear
params.ctx->functions()->glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
params.ctx->functions()->glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// draw clip on screen according to gl coordinates
glBegin(GL_QUADS);
glTexCoord2f(coords.textureTopLeftX, coords.textureTopLeftY); // top left
glVertex2f(coords.vertexTopLeftX, coords.vertexTopLeftY); // top left
glTexCoord2f(coords.textureTopRightX, coords.textureTopRightY); // top right
glVertex2f(coords.vertexTopRightX, coords.vertexTopRightY); // top right
glTexCoord2f(coords.textureBottomRightX, coords.textureBottomRightY); // bottom right
glVertex2f(coords.vertexBottomRightX, coords.vertexBottomRightY); // bottom right
glTexCoord2f(coords.textureBottomLeftX, coords.textureBottomLeftY); // bottom left
glVertex2f(coords.vertexBottomLeftX, coords.vertexBottomLeftY); // bottom left
glEnd();
// release final clip texture
glBindTexture(GL_TEXTURE_2D, 0);
params.ctx->functions()->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
// == END RENDER CLIP IN CONTEXT OF SEQUENCE ==
//
//
// PROCESS POST-SHADERS
//
//
// copy front buffer to back buffer (only if we're using blending modes - which we usually will be)
if (!olive::CurrentRuntimeConfig.disable_blending) {
if (params.nests.size() > 0) {
draw_clip(params.ctx, params.nests.last()->fbo[2]->handle(), params.nests.last()->fbo[0]->texture(), true);
} else {
draw_clip(params.ctx, params.backend_buffer2, params.main_attachment, true);
}
}
// == START FINAL DRAW ON SEQUENCE BUFFER ==
// thread safety (see docs for ComposeSequenceParams::main_buffer_mutex)
if (final_fbo == params.main_buffer) {
params.main_buffer_mutex->lock();
}
// bind front buffer as draw buffer
params.ctx->functions()->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, final_fbo);
if (olive::CurrentRuntimeConfig.disable_blending) {
// some GPUs don't like the blending shader, so we provide a pure GL fallback here
params.ctx->functions()->glBindTexture(GL_TEXTURE_2D, backend_tex_1);
glColor4f(coords.opacity, coords.opacity, coords.opacity, coords.opacity);
full_blit();
params.ctx->functions()->glBindTexture(GL_TEXTURE_2D, 0);
} else {
// load background texture into texture unit 0
params.ctx->functions()->glActiveTexture(GL_TEXTURE0 + 0); // Texture unit 0
params.ctx->functions()->glBindTexture(GL_TEXTURE_2D, backend_tex_2);
// load foreground texture into texture unit 1
params.ctx->functions()->glActiveTexture(GL_TEXTURE0 + 1); // Texture unit 1
params.ctx->functions()->glBindTexture(GL_TEXTURE_2D, backend_tex_1);
// bind and configure blending mode shader
params.blend_mode_program->bind();
params.blend_mode_program->setUniformValue("blendmode", coords.blendmode);
params.blend_mode_program->setUniformValue("opacity", coords.opacity);
params.blend_mode_program->setUniformValue("background", 0);
params.blend_mode_program->setUniformValue("foreground", 1);
glClear(GL_COLOR_BUFFER_BIT);
full_blit();
// release blend mode shader
params.blend_mode_program->release();
// unbind texture from texture unit 1
params.ctx->functions()->glBindTexture(GL_TEXTURE_2D, 0);
// unbind texture from texture unit 0
params.ctx->functions()->glActiveTexture(GL_TEXTURE0 + 0); // Texture unit 0
params.ctx->functions()->glBindTexture(GL_TEXTURE_2D, 0);
}
// unbind framebuffer
params.ctx->functions()->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
// thread safety (see docs for ComposeSequenceParams::main_buffer_mutex)
if (final_fbo == params.main_buffer) {
params.main_buffer_mutex->unlock();
}
// == END FINAL DRAW ON SEQUENCE BUFFER ==
}
// prepare gizmos
if ((*params.gizmos) != nullptr
&& params.nests.isEmpty()
&& ((*params.gizmos) == first_gizmo_effect
|| (*params.gizmos) == selected_effect)) {
(*params.gizmos)->gizmo_draw(timecode, coords); // set correct gizmo coords
(*params.gizmos)->gizmo_world_to_screen(); // convert gizmo coords to screen coords
}
glPopMatrix();
}
} else {
if (c->media() != nullptr && c->media()->get_type() == MEDIA_TYPE_SEQUENCE) {
params.nests.append(c);
compose_sequence(params);
params.nests.removeLast();
} else {
// Check whether cacher is currently active, if not activate it now
if (c->cache_lock.tryLock()) {
c->cache_lock.unlock();
c->Cache(playhead,
false,
(params.viewer != nullptr && !params.viewer->playing),
params.nests,
params.playback_speed);
}
}
// visually update all the keyframe values
if (c->sequence == params.seq) { // only if you can currently see them
double ts = (playhead - c->timeline_in(true) + c->clip_in(true))/s->frame_rate;
for (int i=0;i<c->effects.size();i++) {
EffectPtr e = c->effects.at(i);
for (int j=0;j<e->row_count();j++) {
EffectRow* r = e->row(j);
for (int k=0;k<r->fieldCount();k++) {
r->field(k)->validate_keyframe_data(ts);
}
}
}
}
}
} else {
params.texture_failed = true;
}
if (got_mutex) {
c->state_change_lock.unlock();
}
}
if (audio_track_count == 0 && params.viewer != nullptr) {
params.viewer->play_wake();
}
if (params.video) {
glPopMatrix();
}
// qDebug() << "compose sequence took" << QDateTime::currentMSecsSinceEpoch() - time;
if (!params.nests.isEmpty() && params.nests.last()->fbo != nullptr) {
// returns nested clip's texture
return params.nests.last()->fbo[0]->texture();
}
return 0;
}
void compose_audio(Viewer* viewer, SequencePtr seq, int playback_speed, bool wait_for_mutexes) {
ComposeSequenceParams params;
params.viewer = viewer;
params.ctx = nullptr;
params.seq = seq;
params.video = false;
params.gizmos = nullptr;
params.wait_for_mutexes = wait_for_mutexes;
params.playback_speed = playback_speed;
params.blend_mode_program = nullptr;
compose_sequence(params);
}
long rescale_frame_number(long framenumber, double source_frame_rate, double target_frame_rate) {
return qRound((double(framenumber)/source_frame_rate)*target_frame_rate);
}
double get_timecode(ClipPtr c, long playhead) {
return double(playhead_to_clip_frame(c, playhead))/c->sequence->frame_rate;
}
long playhead_to_clip_frame(ClipPtr c, long playhead) {
return (qMax(0L, playhead - c->timeline_in(true)) + c->clip_in(true));
}
double playhead_to_clip_seconds(ClipPtr c, long playhead) {
// returns time in seconds
long clip_frame = playhead_to_clip_frame(c, playhead);
if (c->reversed()) {
clip_frame = c->media_length() - clip_frame - 1;
}
double secs = (double(clip_frame)/c->sequence->frame_rate)*c->speed().value;
if (c->media() != nullptr && c->media()->get_type() == MEDIA_TYPE_FOOTAGE) {
secs *= c->media()->to_footage()->speed;
}
return secs;
}
int64_t seconds_to_timestamp(ClipPtr c, double seconds) {
return qRound64(seconds * av_q2d(av_inv_q(c->time_base())));
}
int64_t playhead_to_timestamp(ClipPtr c, long playhead) {
return seconds_to_timestamp(c, playhead_to_clip_seconds(c, playhead));
}
void close_active_clips(SequencePtr s) {
if (s != nullptr) {
for (int i=0;i<s->clips.size();i++) {
ClipPtr c = s->clips.at(i);
if (c != nullptr) {
c->Close(true);
}
}
}
}
+416
View File
@@ -0,0 +1,416 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2019 Olive Team
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/>.
***/
#ifndef RENDERFUNCTIONS_H
#define RENDERFUNCTIONS_H
#include <QOpenGLContext>
#include <QVector>
#include <QOpenGLShaderProgram>
#include "project/sequence.h"
#include "project/effect.h"
#include "panels/viewer.h"
/**
* @brief The ComposeSequenceParams struct
*
* Struct sent to the compose_sequence() function.
*/
struct ComposeSequenceParams {
/**
* @brief Reference to the Viewer class that's calling compose_sequence()
*
* Primarily used for calling Viewer::play_wake() when appropriate.
*/
Viewer* viewer;
/**
* @brief The OpenGL context to use while rendering.
*
* For video rendering, this must be a valid OpenGL context. For audio, this variable is never accessed.
*
* \see ComposeSequenceParams::video
*/
QOpenGLContext* ctx;
/**
* @brief The sequence to compose
*
* In addition to clips, sequences also contain the playhead position so compose_sequence() knows which frame
* to render.
*/
SequencePtr seq;
/**
* @brief Array to store the nested sequence hierarchy
*
* Should be left empty. This array gets passed around compose_sequence() as it calls itself recursively to
* handle nested sequences.
*/
QVector<ClipPtr> nests;
/**
* @brief Set compose mode to video or audio
*
* **TRUE** if this function should render video, **FALSE** if this function should render audio.
*/
bool video;
/**
* @brief Set to the Effect whose gizmos were chosen to be drawn on screen
*
* A pointer to a pointer that will be set to the Effect whose gizmos are being rendered and should therefore
* be interacted with if the user uses them.
*/
EffectPtr* gizmos;
/**
* @brief A variable that compose_sequence() will set to **TRUE** if any of the clips couldn't be shown.
*
* A footage item or shader may not be ready at the time this frame is drawn. If compose_sequence() couldn't draw
* any of the clips in the scene, this variable is set to **TRUE** indicating that the image rendered is a
* "best effort", but not the actual image.
*
* This variable should be checked after compose_sequence() and a repaint should be triggered if it's **TRUE**.
*
* \note This variable is probably bad design and is a relic of an earlier rendering backend. There may be a better
* way to communicate this information.
*
* Additionally, since
* compose_sequence() for video will now always run in a separate thread anyway, there's no real issue with
* stalling it to wait for footage to complete opening or whatever may be lagging behind. A possible side effect
* of this though is that the preview may become less responsive if it's stuck trying to render one frame. With
* the current system, the preview may show incomplete frames occasionally but at least it will show something.
* This may be preferable. See ComposeSequenceParams::single_threaded for a similar function that could be
* removed.
*/
bool texture_failed;
/**
* @brief Run all cachers in the same thread that compose_sequence() is in
*
* Standard behavior is that all clips cache frames in their own thread and signals are sent between
* compose_sequence() and the clip's cacher thread regarding which frames to display and cache without stalling
* the compose_sequence() thread. Setting this to **TRUE** will run all cachers in the same thread creating a
* technically more "perfect" connection between them that will also stall the compose_sequence() thread. Used
* when rendering as timing isn't as important as creating output frames as quickly as possible.
*
* \note Exporting should probably be rewritten without this. While running all the cachers in one thread makes
* it easier to synchronize everything, export performance could probably benefit from keeping them in separate
* threads and syncing up with them. See ComposeSequenceParams::texture_failed for a similar function that could
* be removed.
*/
bool wait_for_mutexes;
/**
* @brief Set the current playback speed (adjusted with Shuttle Left/Right)
*
* Only used for audio rendering to determine how many samples to skip in order to play audio at the correct speed.
*
* \see ComposeSequenceParams::video
*/
int playback_speed;
/**
* @brief Blending mode shader
*
* Used only for video rendering. Never accessed with audio rendering.
*
* A program containing the current active
* blending mode shader that can be bound during rendering. Must be compiled and linked beforehand. See
* RenderThread::blend_mode_program for how this is properly set up.
*
* \see ComposeSequenceParams::video
*/
QOpenGLShaderProgram* blend_mode_program;
/**
* @brief Premultiply alpha shader
*
* Used only for video rendering. Never accessed with audio rendering.
*
* compose_sequence()'s internal composition
* expects premultipled alpha, but it will pre-emptively multiply any footage that is not set as already
* premultiplied (see Footage::alpha_is_premultiplied) using this shader. Must be compiled and linked beforehand.
* See RenderThread::premultiply_program for how this is properly set up.
*/
QOpenGLShaderProgram* premultiply_program;
/**
* @brief The OpenGL framebuffer object that the final texture to be shown is rendered to.
*
* Used only for video rendering. Never accessed with audio rendering.
*
* When compose_sequence() is rendering the final image, this framebuffer will be bound.
*/
GLuint main_buffer;
/**
* @brief The attachment to the framebuffer in main_buffer
*
* Used only for video rendering. Never accessed with audio rendering.
*
* The OpenGL texture attached to the framebuffer referenced by main_buffer.
*/
GLuint main_attachment;
/**
* @brief Mutex for the main framebuffer
*
* Used only for video rendering. Never accessed with audio rendering.
*
* If this is not nullptr, compose_sequence() will lock this mutex when rendering to main_buffer/main_attachment.
* Used to synchronize ViewerWidget and ViewerWindow with RenderThread.
*/
QMutex* main_buffer_mutex;
/**
* @brief Backend OpenGL framebuffer 1 used for further processing before rendering to main_buffer
*
* In some situations, compose_sequence() will do some processing through shaders that requires "ping-ponging"
* between framebuffers. backend_buffer1 and backend_buffer2 are used for this purpose.
*/
GLuint backend_buffer1;
/**
* @brief Backend OpenGL framebuffer 1's texture attachment
*
* The texture that ComposeSequenceParams::backend_buffer1 renders to. Bound and drawn to
* ComposeSequenceParams::backend_buffer2 to "ping-pong" between them and various shaders.
*/
GLuint backend_attachment1;
/**
* @brief Backend OpenGL framebuffer 2 used for further processing before rendering to main_buffer
*
* In some situations, compose_sequence() will do some processing through shaders that requires "ping-ponging"
* between framebuffers. backend_buffer1 and backend_buffer2 are used for this purpose.
*/
GLuint backend_buffer2;
/**
* @brief Backend OpenGL framebuffer 2's texture attachment
*
* The texture that ComposeSequenceParams::backend_buffer2 renders to. Bound and drawn to
* ComposeSequenceParams::backend_buffer1 to "ping-pong" between them and various shaders.
*/
GLuint backend_attachment2;
/**
* @brief OpenGL shader containing OpenColorIO shader information
*/
QOpenGLShaderProgram* ocio_shader;
/**
* @brief OpenGL texture containing LUT obtained form OpenColorIO
*/
GLuint ocio_lut_texture;
};
/**
* @brief Compose a frame of a given sequence
*
* For any given Sequence, this function will render the current frame indicated by Sequence::playhead. Will
* automatically open and close clips (memory allocation and file handles) as necessary, communicate with the
* Clip::cacher objects to retrieve upcoming frames and store them in memory, run Effect processing functions, and
* finally composite all the currently active clips together into a final texture.
*
* Will sometimes render a frame incomplete or inaccurately, e.g. if a video file hadn't finished opening by the time
* of the render or a clip's cacher didn't have the requested frame available at the time of the render. If so,
* the `texture_failed` variable of `params` will be set to **TRUE**. Check this after calling compose_sequence() and
* if it is **TRUE**, compose_sequence() should be called again later to attempt another render (unless the Sequence
* is being played, in which case just play the next frame rather than redrawing an old frame).
*
* @param params
*
* A struct of parameters to use while rendering.
*
* @return A reference to the OpenGL texture resulting from the render. Will usually be equal to
* ComposeSequenceParams::main_attachment unless it's rendering a nested sequence, in which case it'll be a reference
* to one of the textures referenced by Clip::fbo. Can be used directly to draw the rendered frame.
*/
GLuint compose_sequence(ComposeSequenceParams &params);
/**
* @brief Convenience wrapper function for compose_sequence() to render audio
*
* Much of the functionality provided (and parameters required) by compose_sequence() is only useful/necessary for
* video rendering. For audio rendering, this function is easier to handle and will correctly set up
* compose_sequence() to render audio without the cumbersome effort of setting up a ComposeSequenceParams object.
*
* @param viewer
*
* The Viewer object calling this function
*
* @param seq
*
* The Sequence whose audio to render.
*
* @param playback_speed
*
* The current playback speed (controlled by Shuttle Left/Right)
*
* @param
*
* Whether to wait for media to open or simply fail if the media is not yet open. This should usually be **FALSE**.
*/
void compose_audio(Viewer* viewer, SequencePtr seq, int playback_speed, bool wait_for_mutexes);
/**
* @brief Rescale a frame number between two frame rates
*
* Converts a frame number from one frame rate to its equivalent in another frame rate
*
* @param framenumber
*
* The frame number to convert
*
* @param source_frame_rate
*
* Frame rate that the frame number is currently in
*
* @param target_frame_rate
*
* Frame rate to convert to
*
* @return
*
* Rescaled frame number
*/
long rescale_frame_number(long framenumber, double source_frame_rate, double target_frame_rate);
/**
* @brief Get timecode
*
* Get the current clip/media time from the Timeline playhead in seconds. For instance if the playhead was at the start
* of a clip (whose in point wasn't trimmed), this would be 0.0 as it's the start of the clip/media;
*
* @param c
*
* Clip to get the timecode of
*
* @param playhead
*
* Sequence playhead to convert to a clip/media timecode
*
* @return
*
* Timecode in seconds
*/
double get_timecode(ClipPtr c, long playhead);
/**
* @brief Convert playhead frame number to a clip frame number
*
* Converts a Timeline playhead to a the current clip's frame. Equivalent to
* `PLAYHEAD - CLIP_TIMELINE_IN + CLIP_MEDIA_IN`. All keyframes are in clip frames.
*
* @param c
*
* The clip to get the current frame number of
*
* @param playhead
*
* The current Timeline frame number
*
* @return
*
* The curren frame number of the clip at `playhead`
*/
long playhead_to_clip_frame(ClipPtr c, long playhead);
/**
* @brief Converts the playhead to clip seconds
*
* Get the current timecode at the playhead in terms of clip seconds.
*
* FIXME: Possible duplicate of get_timecode()? Will need to research this more.
*
* @param c
*
* Clip to return clip seconds of.
*
* @param playhead
*
* Current Timeline playhead to convert to clip seconds
*
* @return
*
* Clip time in seconds
*/
double playhead_to_clip_seconds(ClipPtr c, long playhead);
/**
* @brief Convert seconds to FFmpeg timestamp
*
* Used for interaction with FFmpeg, converts seconds in a floating-point value to a timestamp in AVStream->time_base
* units.
*
* @param c
*
* Clip to get timestamp of
*
* @param seconds
*
* Clip time in seconds
*
* @return
*
* An FFmpeg-compatible timestamp in AVStream->time_base units.
*/
int64_t seconds_to_timestamp(ClipPtr c, double seconds);
/**
* @brief Convert Timeline playhead to FFmpeg timestamp
*
* Used for interaction with FFmpeg, converts the Timeline playhead to a timestamp in AVStream->time_base
* units.
*
* @param c
*
* Clip to get timestamp of
*
* @param playhead
*
* Timeline playhead to convert to a timestamp
*
* @return
*
* An FFmpeg-compatible timestamp in AVStream->time_base units.
*/
int64_t playhead_to_timestamp(ClipPtr c, long playhead);
/**
* @brief Close all open clips in a Sequence
*
* Closes any currently open clips on a Sequence and waits for them to close before returning. This may be slow as a
* result on large Sequence objects. If a Clip is a nested Sequence, this function calls itself recursively on that
* Sequence too.
*
* @param s
*
* The Sequence to close all clips on.
*/
void close_active_clips(SequencePtr s);
#endif // RENDERFUNCTIONS_H
+350
View File
@@ -0,0 +1,350 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2019 Olive Team
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 "renderthread.h"
#include <QApplication>
#include <QImage>
#include <QOpenGLFunctions>
#include <QDateTime>
#include <QDebug>
#ifdef OLIVE_OCIO
#include <OpenColorIO/OpenColorIO.h>
namespace OCIO = OCIO_NAMESPACE;
#endif
#include "rendering/renderfunctions.h"
#include "project/sequence.h"
RenderThread::RenderThread()
{
front_buffer1 = 0;
front_buffer2 = 0;
front_texture1 = 0;
front_texture2 = 0;
gizmos = nullptr;
share_ctx = nullptr;
ctx = nullptr;
blend_mode_program = nullptr;
premultiply_program = nullptr;
seq = nullptr;
tex_width = -1;
tex_height = -1;
queued = false;
texture_failed = false;
running = true;
surface.create();
}
RenderThread::~RenderThread() {
surface.destroy();
}
void RenderThread::run() {
wait_lock_.lock();
while (running) {
if (!queued) {
wait_cond_.wait(&wait_lock_);
}
if (!running) {
break;
}
queued = false;
if (share_ctx != nullptr) {
if (ctx != nullptr) {
ctx->makeCurrent(&surface);
// gen fbo
if (front_buffer1 == 0) {
// delete any existing framebuffers
delete_fbo();
// create framebuffers
ctx->functions()->glGenFramebuffers(1, &front_buffer1);
ctx->functions()->glGenFramebuffers(1, &front_buffer2);
ctx->functions()->glGenFramebuffers(1, &back_buffer_1);
ctx->functions()->glGenFramebuffers(1, &back_buffer_2);
}
// gen texture
if (front_texture1 == 0 || tex_width != seq->width || tex_height != seq->height) {
// cache texture size
tex_width = seq->width;
tex_height = seq->height;
// delete any existing textures
delete_texture();
// create texture
glGenTextures(1, &front_texture1);
glGenTextures(1, &front_texture2);
glGenTextures(1, &back_texture_1);
glGenTextures(1, &back_texture_2);
GLuint textures[4] = { front_texture1, front_texture2, back_texture_1, back_texture_2 };
GLuint fbos[4] = { front_buffer1, front_buffer2, back_buffer_1, back_buffer_2 };
for (int i=0;i<4;i++) {
allocate_texture(textures[i], fbos[i]);
}
}
if (blend_mode_program == nullptr) {
// create shader program to make blending modes work
delete_shader_program();
blend_mode_program = new QOpenGLShaderProgram();
blend_mode_program->addShaderFromSourceFile(QOpenGLShader::Vertex, ":/internalshaders/common.vert");
blend_mode_program->addShaderFromSourceFile(QOpenGLShader::Fragment, ":/internalshaders/blending.frag");
blend_mode_program->link();
premultiply_program = new QOpenGLShaderProgram();
premultiply_program->addShaderFromSourceFile(QOpenGLShader::Vertex, ":/internalshaders/common.vert");
premultiply_program->addShaderFromSourceFile(QOpenGLShader::Fragment, ":/internalshaders/premultiply.frag");
premultiply_program->link();
}
// draw frame
paint();
front_buffer_switcher = !front_buffer_switcher;
emit ready();
}
}
}
delete_ctx();
wait_lock_.unlock();
}
QMutex *RenderThread::get_texture_mutex()
{
// return the mutex for the opposite texture being drawn to by the renderer
return front_buffer_switcher ? &front_mutex2 : &front_mutex1;
}
const GLuint &RenderThread::get_texture()
{
// return the opposite texture to the texture being drawn to by the renderer
return front_buffer_switcher ? front_texture2 : front_texture1;
}
void RenderThread::allocate_texture(GLuint tex, GLuint fbo)
{
// bind framebuffer for attaching
ctx->functions()->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);
// bind texture
glBindTexture(GL_TEXTURE_2D, tex);
// allocate storage for texture
glTexImage2D(
GL_TEXTURE_2D, 0, GL_RGBA, seq->width, seq->height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr
);
// set texture filtering to bilinear
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// attach texture to framebuffer
ctx->functions()->glFramebufferTexture2D(
GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0
);
// release texture
glBindTexture(GL_TEXTURE_2D, 0);
// release framebuffer
ctx->functions()->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
}
void RenderThread::set_up_ocio()
{
}
void RenderThread::paint() {
// set up compose_sequence() parameters
ComposeSequenceParams params;
params.viewer = nullptr;
params.ctx = ctx;
params.seq = seq;
params.video = true;
params.texture_failed = false;
params.gizmos = &gizmos;
params.wait_for_mutexes = true;
params.playback_speed = 1;
params.blend_mode_program = blend_mode_program;
params.premultiply_program = premultiply_program;
params.backend_buffer1 = back_buffer_1;
params.backend_buffer2 = back_buffer_2;
params.backend_attachment1 = back_texture_1;
params.backend_attachment2 = back_texture_2;
params.main_buffer = front_buffer_switcher ? front_buffer1 : front_buffer2;
params.main_attachment = front_buffer_switcher ? front_texture1 : front_texture2;
params.main_buffer_mutex = front_buffer_switcher ? &front_mutex1 : &front_mutex2;
// bind framebuffer for drawing
ctx->functions()->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, params.main_buffer);
glLoadIdentity();
glClearColor(0.0, 0.0, 0.0, 0.0);
glClear(GL_COLOR_BUFFER_BIT);
glMatrixMode(GL_MODELVIEW);
glEnable(GL_TEXTURE_2D);
glEnable(GL_BLEND);
glEnable(GL_DEPTH);
gizmos = nullptr;
compose_sequence(params);
texture_failed = params.texture_failed;
if (!save_fn.isEmpty()) {
if (texture_failed) {
// texture failed, try again
queued = true;
} else {
ctx->functions()->glBindFramebuffer(GL_READ_FRAMEBUFFER, params.main_buffer);
QImage img(tex_width, tex_height, QImage::Format_RGBA8888);
glReadPixels(0, 0, tex_width, tex_height, GL_RGBA, GL_UNSIGNED_BYTE, img.bits());
img.save(save_fn);
ctx->functions()->glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
save_fn = "";
}
}
if (pixel_buffer != nullptr) {
// set main framebuffer to the current read buffer
ctx->functions()->glBindFramebuffer(GL_READ_FRAMEBUFFER, params.main_buffer);
// store pixels in buffer
glReadPixels(0,
0,
pixel_buffer_linesize == 0 ? tex_width : pixel_buffer_linesize,
tex_height,
GL_RGBA,
GL_UNSIGNED_BYTE,
pixel_buffer);
// release current read buffer
ctx->functions()->glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
pixel_buffer = nullptr;
}
glDisable(GL_DEPTH);
glDisable(GL_BLEND);
glDisable(GL_TEXTURE_2D);
// flush changes
ctx->functions()->glFinish();
// release
ctx->functions()->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
}
void RenderThread::start_render(QOpenGLContext *share, SequencePtr s, const QString& save, GLvoid* pixels, int pixel_linesize, int idivider) {
Q_UNUSED(idivider);
seq = s;
// stall any dependent actions
texture_failed = true;
if (share != nullptr && (ctx == nullptr || ctx->shareContext() != share_ctx)) {
share_ctx = share;
delete_ctx();
ctx = new QOpenGLContext();
ctx->setFormat(share_ctx->format());
ctx->setShareContext(share_ctx);
ctx->create();
ctx->moveToThread(this);
}
save_fn = save;
pixel_buffer = pixels;
pixel_buffer_linesize = pixel_linesize;
queued = true;
wait_cond_.wakeAll();
}
bool RenderThread::did_texture_fail() {
return texture_failed;
}
void RenderThread::cancel() {
running = false;
wait_cond_.wakeAll();
wait();
}
void RenderThread::delete_texture() {
if (front_texture1 > 0) {
GLuint tex[4] = {front_texture1, front_texture2, back_texture_1, back_texture_2};
glDeleteTextures(3, tex);
}
front_texture1 = 0;
front_texture2 = 0;
back_texture_1 = 0;
back_texture_2 = 0;
}
void RenderThread::delete_fbo() {
if (front_buffer1 > 0) {
GLuint fbos[4] = {front_buffer1, front_buffer2, back_buffer_1, back_buffer_2};
ctx->functions()->glDeleteFramebuffers(3, fbos);
}
front_buffer1 = 0;
front_buffer2 = 0;
back_buffer_1 = 0;
back_buffer_2 = 0;
}
void RenderThread::delete_shader_program() {
if (blend_mode_program != nullptr) {
delete blend_mode_program;
delete premultiply_program;
}
blend_mode_program = nullptr;
premultiply_program = nullptr;
}
void RenderThread::delete_ctx() {
if (ctx != nullptr) {
delete_shader_program();
delete_texture();
delete_fbo();
ctx->doneCurrent();
delete ctx;
}
ctx = nullptr;
}
+122
View File
@@ -0,0 +1,122 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2019 Olive Team
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/>.
***/
#ifndef RENDERTHREAD_H
#define RENDERTHREAD_H
#include <QThread>
#include <QMutex>
#include <QWaitCondition>
#include <QOffscreenSurface>
#include <QOpenGLContext>
#include <QOpenGLFramebufferObject>
#include <QOpenGLShaderProgram>
#include "project/sequence.h"
#include "project/effect.h"
// copied from source code to OCIODisplay
const int LUT3D_EDGE_SIZE = 32;
// copied from source code to OCIODisplay, expanded from 3*LUT3D_EDGE_SIZE*LUT3D_EDGE_SIZE*LUT3D_EDGE_SIZE
const int NUM_3D_ENTRIES = 98304;
class RenderThread : public QThread {
Q_OBJECT
public:
RenderThread();
~RenderThread();
void run();
QMutex* get_texture_mutex();
const GLuint& get_texture();
EffectPtr gizmos;
void paint();
void start_render(QOpenGLContext* share,
SequencePtr s,
const QString &save = nullptr,
GLvoid *pixels = nullptr,
int pixel_linesize = 0,
int idivider = 0);
bool did_texture_fail();
void cancel();
public slots:
// cleanup functions
void delete_ctx();
signals:
void ready();
private:
void allocate_texture(GLuint tex, GLuint fbo);
void set_up_ocio();
void destroy_ocio();
// cleanup functions
void delete_texture();
void delete_fbo();
void delete_shader_program();
GLuint front_buffer1;
GLuint front_texture1;
QMutex front_mutex1;
GLuint front_buffer2;
GLuint front_texture2;
QMutex front_mutex2;
bool front_buffer_switcher;
QWaitCondition wait_cond_;
QMutex wait_lock_;
QWaitCondition main_thread_wait_cond_;
QMutex main_thread_lock_;
QOffscreenSurface surface;
QOpenGLContext* share_ctx;
QOpenGLContext* ctx;
QOpenGLShaderProgram* blend_mode_program;
QOpenGLShaderProgram* premultiply_program;
GLuint back_buffer_1;
GLuint back_buffer_2;
GLuint back_texture_1;
GLuint back_texture_2;
float ocio_lut_data[NUM_3D_ENTRIES];
GLuint ocio_lut_texture;
QOpenGLShaderProgram* ocio_shader;
SequencePtr seq;
int divider;
int tex_width;
int tex_height;
bool queued;
bool texture_failed;
bool running;
QString save_fn;
GLvoid *pixel_buffer;
int pixel_buffer_linesize;
};
#endif // RENDERTHREAD_H