refactor(render): de-Qt oakrender and wrap it in a pure C ABI
- copy engine/render to src/render/src (sunk param types excluded), de-Qt in five parallel groups: core machinery (tickets/worker pool/ jobs), caches, color/texture, preview/IPC, GPU backends - replace Qt GL/Vulkan wrappers with native context abstractions (CGL/EGL/WGL, raw vulkan.h), QProcess with POSIX WorkerProcess, QJsonObject with a minimal NDJSON-compatible workerjson (wire protocol unchanged), QDataStream disk state with a byte-compatible BinaryStream - signals become single std::function callbacks or facade-triggered calls per the documented signal/slot strategy - pure C ABI in include/render + src/render/c_api (renderer/cache/ color/manager families, OAKRENDER_E_* codes), 37 gtest cases - bridge src/node/transition/render/* stubs to the real oakrender headers, closing the node<->render cycle: liboaknode links liboakrender, zero dangling symbols - docs: M7 implementation status + oakrender semantic-change notes
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
add_subdirectory(src)
|
||||
add_subdirectory(c_api)
|
||||
|
||||
if(BUILD_TESTS)
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
@@ -0,0 +1,6 @@
|
||||
target_sources(oakrender PRIVATE
|
||||
renderer.cpp
|
||||
cache.cpp
|
||||
color.cpp
|
||||
manager.cpp
|
||||
)
|
||||
@@ -0,0 +1,40 @@
|
||||
/***
|
||||
|
||||
Oak Video Editor - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak 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 OAK_EDITOR_RENDER_ALIVECOUNT_H
|
||||
#define OAK_EDITOR_RENDER_ALIVECOUNT_H
|
||||
|
||||
/**
|
||||
* @brief Shared live-object counter hooks (internal, not installed).
|
||||
*
|
||||
* The counter itself and the public oakrender_debug_alive_count() live in
|
||||
* the cache family (src/render/c_api/cache.cpp); these hooks have
|
||||
* external linkage so the other families' create/free functions can
|
||||
* participate. Mirrors src/node/c_api/alivecount.h.
|
||||
*/
|
||||
namespace oakrender_c_api
|
||||
{
|
||||
|
||||
void alive_inc();
|
||||
void alive_dec();
|
||||
|
||||
}
|
||||
|
||||
#endif //OAK_EDITOR_RENDER_ALIVECOUNT_H
|
||||
@@ -0,0 +1,208 @@
|
||||
/***
|
||||
|
||||
Oak Video Editor - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak 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 "../../../include/render/cache.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <new>
|
||||
|
||||
#include "alivecount.h"
|
||||
#include "internalhandles.h"
|
||||
|
||||
#include "framehashcache.h"
|
||||
#include "playbackcache.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
std::atomic<int> g_alive_count(0);
|
||||
|
||||
/**
|
||||
* @brief FrameHashCache with the protected PlaybackCache::validate()
|
||||
* exposed for the ABI. Adds no data members, so a handle created
|
||||
* as OakRenderCacheImpl reinterpret-casts safely both ways.
|
||||
*/
|
||||
class OakRenderCacheImpl : public olive::FrameHashCache {
|
||||
public:
|
||||
OakRenderCacheImpl()
|
||||
: olive::FrameHashCache(nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
using olive::PlaybackCache::validate;
|
||||
};
|
||||
|
||||
OakRenderCacheImpl *impl(OakRenderCache *c)
|
||||
{
|
||||
return reinterpret_cast<OakRenderCacheImpl *>(c);
|
||||
}
|
||||
|
||||
const OakRenderCacheImpl *impl(const OakRenderCache *c)
|
||||
{
|
||||
return reinterpret_cast<const OakRenderCacheImpl *>(c);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Convert an int64 timestamp to a time using the cache's
|
||||
* timebase; timestamps are whole seconds when no valid timebase
|
||||
* is set.
|
||||
*/
|
||||
olive::Rational ts_to_time(const OakRenderCacheImpl *c, int64_t ts)
|
||||
{
|
||||
const olive::Rational &tb = c->get_timebase();
|
||||
if (tb.isNull()) {
|
||||
return olive::Rational::from_double(double(ts));
|
||||
}
|
||||
return olive::core::Timecode::timestamp_to_time(ts, tb);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace oakrender_c_api
|
||||
{
|
||||
|
||||
void alive_inc()
|
||||
{
|
||||
g_alive_count.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void alive_dec()
|
||||
{
|
||||
g_alive_count.fetch_sub(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int oakrender_debug_alive_count(void)
|
||||
{
|
||||
return g_alive_count.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
OakRenderCache *oakrender_cache_create(void)
|
||||
{
|
||||
try {
|
||||
auto *c = new OakRenderCacheImpl();
|
||||
oakrender_c_api::alive_inc();
|
||||
return reinterpret_cast<OakRenderCache *>(c);
|
||||
} catch (...) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void oakrender_cache_free(OakRenderCache *cache)
|
||||
{
|
||||
if (!cache) {
|
||||
return;
|
||||
}
|
||||
delete impl(cache);
|
||||
oakrender_c_api::alive_dec();
|
||||
}
|
||||
|
||||
int oakrender_cache_set_timebase(OakRenderCache *cache, int num, int den)
|
||||
{
|
||||
if (!cache || num <= 0 || den <= 0) {
|
||||
return OAKRENDER_E_INVALID;
|
||||
}
|
||||
impl(cache)->set_timebase(olive::Rational(num, den));
|
||||
return OAKRENDER_OK;
|
||||
}
|
||||
|
||||
int oakrender_cache_set_uuid(OakRenderCache *cache, const char *uuid)
|
||||
{
|
||||
if (!cache || !uuid) {
|
||||
return OAKRENDER_E_INVALID;
|
||||
}
|
||||
impl(cache)->set_uuid(uuid);
|
||||
return OAKRENDER_OK;
|
||||
}
|
||||
|
||||
void oakrender_cache_invalidate(OakRenderCache *cache, int64_t in_ts,
|
||||
int64_t out_ts)
|
||||
{
|
||||
if (!cache) {
|
||||
return;
|
||||
}
|
||||
OakRenderCacheImpl *c = impl(cache);
|
||||
c->invalidate(
|
||||
olive::core::TimeRange(ts_to_time(c, in_ts), ts_to_time(c, out_ts)));
|
||||
}
|
||||
|
||||
void oakrender_cache_validate(OakRenderCache *cache, int64_t in_ts,
|
||||
int64_t out_ts)
|
||||
{
|
||||
if (!cache) {
|
||||
return;
|
||||
}
|
||||
OakRenderCacheImpl *c = impl(cache);
|
||||
c->validate(
|
||||
olive::core::TimeRange(ts_to_time(c, in_ts), ts_to_time(c, out_ts)));
|
||||
}
|
||||
|
||||
int oakrender_cache_has_validated_ranges(const OakRenderCache *cache)
|
||||
{
|
||||
return cache && impl(cache)->has_validated_ranges() ? 1 : 0;
|
||||
}
|
||||
|
||||
int oakrender_cache_indicator_height(void)
|
||||
{
|
||||
return olive::PlaybackCache::get_cache_indicator_height();
|
||||
}
|
||||
|
||||
int oakrender_frame_cache_load(OakRenderCache *cache, const char *path,
|
||||
const char *uuid, int64_t ts,
|
||||
OakCodecFrame **out_frame)
|
||||
{
|
||||
if (!cache || !path || !uuid || !out_frame) {
|
||||
return OAKRENDER_E_INVALID;
|
||||
}
|
||||
try {
|
||||
olive::FramePtr f =
|
||||
olive::FrameHashCache::load_cache_frame(path, uuid, ts);
|
||||
if (!f) {
|
||||
return OAKRENDER_E_NOT_FOUND;
|
||||
}
|
||||
auto *block = new OakCodecFrame;
|
||||
block->ptr = std::move(f);
|
||||
oakrender_c_api::alive_inc();
|
||||
*out_frame = block;
|
||||
return OAKRENDER_OK;
|
||||
} catch (...) {
|
||||
return OAKRENDER_E_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
void oakrender_frame_cache_save(OakRenderCache *cache, const char *path,
|
||||
const char *uuid, const OakCodecFrame *frame)
|
||||
{
|
||||
if (!cache || !path || !uuid || !frame || !frame->ptr) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
OakRenderCacheImpl *c = impl(cache);
|
||||
olive::Rational tb = c->get_timebase();
|
||||
if (tb.isNull()) {
|
||||
tb = olive::Rational(1, 1);
|
||||
}
|
||||
olive::FrameHashCache::save_cache_frame(path, uuid,
|
||||
frame->ptr->timestamp(), tb,
|
||||
frame->ptr);
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
/***
|
||||
|
||||
Oak Video Editor - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak 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 "../../../include/render/color.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <new>
|
||||
#include <string>
|
||||
|
||||
#include "alivecount.h"
|
||||
#include "internalhandles.h"
|
||||
|
||||
#include "color/colormanager/colormanager.h"
|
||||
#include "filefunctions.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
int write_string(const std::string &s, char *buf, int n)
|
||||
{
|
||||
const int required = int(s.size()) + 1;
|
||||
if (buf && n >= required) {
|
||||
std::memcpy(buf, s.c_str(), size_t(required));
|
||||
}
|
||||
return required;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
OakColorProcessor *oakrender_color_processor_create(const char *src_space,
|
||||
const char *dst_transform,
|
||||
int direction)
|
||||
{
|
||||
if (!src_space || !*src_space || !dst_transform || !*dst_transform) {
|
||||
return nullptr;
|
||||
}
|
||||
if (direction != OAKRENDER_COLOR_DIRECTION_NORMAL &&
|
||||
direction != OAKRENDER_COLOR_DIRECTION_INVERSE) {
|
||||
return nullptr;
|
||||
}
|
||||
try {
|
||||
ocio::ConstConfigRcPtr config = olive::ColorManager::get_default_config();
|
||||
if (!config) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Resolve role names (e.g. "scene_linear") to canonical colorspace
|
||||
// names, mirroring ColorProcessor's constructor.
|
||||
std::string src = src_space;
|
||||
if (config->hasRole(src_space)) {
|
||||
src = config->getCanonicalName(src_space);
|
||||
}
|
||||
|
||||
// OCIO failures are non-fatal (matching the C++ behavior): the
|
||||
// handle is still returned, but holds a null processor and
|
||||
// conversions pass through.
|
||||
ocio::ConstProcessorRcPtr processor;
|
||||
try {
|
||||
if (direction == OAKRENDER_COLOR_DIRECTION_NORMAL) {
|
||||
processor = config->getProcessor(src.c_str(), dst_transform);
|
||||
} else {
|
||||
processor = config->getProcessor(dst_transform, src.c_str());
|
||||
}
|
||||
} catch (ocio::Exception &) {
|
||||
processor = nullptr;
|
||||
}
|
||||
|
||||
auto *block = new OakColorProcessor;
|
||||
block->ptr = olive::ColorProcessor::create(processor);
|
||||
oakrender_c_api::alive_inc();
|
||||
return block;
|
||||
} catch (...) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void oakrender_color_processor_free(OakColorProcessor *processor)
|
||||
{
|
||||
if (!processor) {
|
||||
return;
|
||||
}
|
||||
delete processor;
|
||||
oakrender_c_api::alive_dec();
|
||||
}
|
||||
|
||||
int oakrender_color_processor_is_valid(const OakColorProcessor *processor)
|
||||
{
|
||||
return processor && processor->ptr && processor->ptr->get_processor() ? 1 :
|
||||
0;
|
||||
}
|
||||
|
||||
int oakrender_color_processor_convert(OakColorProcessor *processor,
|
||||
double ir, double ig, double ib,
|
||||
double ia, double *out_r, double *out_g,
|
||||
double *out_b, double *out_a)
|
||||
{
|
||||
if (!processor || !processor->ptr || !out_r || !out_g || !out_b || !out_a) {
|
||||
return OAKRENDER_E_INVALID;
|
||||
}
|
||||
try {
|
||||
olive::Color out =
|
||||
processor->ptr->convert_color(olive::Color(ir, ig, ib, ia));
|
||||
*out_r = out.red();
|
||||
*out_g = out.green();
|
||||
*out_b = out.blue();
|
||||
*out_a = out.alpha();
|
||||
return OAKRENDER_OK;
|
||||
} catch (...) {
|
||||
return OAKRENDER_E_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- ColorManager statics ------------------------------------------------- */
|
||||
|
||||
int oakrender_color_manager_set_up_default_config(void)
|
||||
{
|
||||
try {
|
||||
olive::ColorManager::set_up_default_config();
|
||||
return olive::ColorManager::get_default_config() ? OAKRENDER_OK :
|
||||
OAKRENDER_E_FAILED;
|
||||
} catch (...) {
|
||||
return OAKRENDER_E_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
int oakrender_color_manager_get_config(char *buf, int n)
|
||||
{
|
||||
try {
|
||||
const char *ocio_env = std::getenv("OCIO");
|
||||
if (ocio_env && *ocio_env) {
|
||||
return write_string(ocio_env, buf, n);
|
||||
}
|
||||
if (!olive::ColorManager::get_default_config()) {
|
||||
return OAKRENDER_E_STATE;
|
||||
}
|
||||
// The default config is extracted next to the configuration
|
||||
// location (ColorManager::set_up_default_config()).
|
||||
return write_string(FileFunctions::get_configuration_location() +
|
||||
"/ocioconf/config.ocio",
|
||||
buf, n);
|
||||
} catch (...) {
|
||||
return OAKRENDER_E_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
int oakrender_color_manager_display_transform(const char *display,
|
||||
const char *view, char *buf,
|
||||
int n)
|
||||
{
|
||||
if (!display || !*display || !view || !*view) {
|
||||
return OAKRENDER_E_INVALID;
|
||||
}
|
||||
try {
|
||||
ocio::ConstConfigRcPtr config = olive::ColorManager::get_default_config();
|
||||
if (!config) {
|
||||
return OAKRENDER_E_STATE;
|
||||
}
|
||||
|
||||
bool display_found = false;
|
||||
for (int i = 0; i < config->getNumDisplays(); i++) {
|
||||
if (display == std::string(config->getDisplay(i))) {
|
||||
display_found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!display_found) {
|
||||
return OAKRENDER_E_NOT_FOUND;
|
||||
}
|
||||
|
||||
bool view_found = false;
|
||||
for (int i = 0; i < config->getNumViews(display); i++) {
|
||||
if (view == std::string(config->getView(display, i))) {
|
||||
view_found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!view_found) {
|
||||
return OAKRENDER_E_NOT_FOUND;
|
||||
}
|
||||
|
||||
// Source = the config's reference colorspace (role lookup).
|
||||
ocio::ConstColorSpaceRcPtr ref_cs =
|
||||
config->getColorSpace(ocio::ROLE_REFERENCE);
|
||||
if (!ref_cs) {
|
||||
return OAKRENDER_E_STATE;
|
||||
}
|
||||
|
||||
auto dvt = ocio::DisplayViewTransform::Create();
|
||||
dvt->setSrc(ref_cs->getName());
|
||||
dvt->setDisplay(display);
|
||||
dvt->setView(view);
|
||||
|
||||
ocio::ConstProcessorRcPtr processor = config->getProcessor(dvt);
|
||||
if (!processor) {
|
||||
return OAKRENDER_E_NOT_FOUND;
|
||||
}
|
||||
return write_string(processor->getCacheID(), buf, n);
|
||||
} catch (ocio::Exception &) {
|
||||
return OAKRENDER_E_NOT_FOUND;
|
||||
} catch (...) {
|
||||
return OAKRENDER_E_FAILED;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/***
|
||||
|
||||
Oak Video Editor - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak 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 OAK_EDITOR_RENDER_INTERNALHANDLES_H
|
||||
#define OAK_EDITOR_RENDER_INTERNALHANDLES_H
|
||||
|
||||
/**
|
||||
* @brief Control-block definitions behind the public opaque handles
|
||||
* (internal, not installed).
|
||||
*
|
||||
* Textures, frames and color processors wrap shared_ptr-managed engine
|
||||
* objects, so their handles are heap control blocks (the R7-A §A.2
|
||||
* ownership protocol). The refcount on textures/frames implements the
|
||||
* retain/free pairing rule; every alive control block participates in
|
||||
* oakrender_debug_alive_count().
|
||||
*/
|
||||
|
||||
#include <atomic>
|
||||
|
||||
#include "codec/frame.h"
|
||||
#include "colorprocessor.h"
|
||||
#include "texture.h"
|
||||
|
||||
struct OakRenderTexture {
|
||||
olive::TexturePtr ptr;
|
||||
std::atomic<int> refcount{ 1 };
|
||||
};
|
||||
|
||||
struct OakCodecFrame {
|
||||
olive::FramePtr ptr;
|
||||
std::atomic<int> refcount{ 1 };
|
||||
};
|
||||
|
||||
struct OakColorProcessor {
|
||||
olive::ColorProcessorPtr ptr;
|
||||
};
|
||||
|
||||
#endif //OAK_EDITOR_RENDER_INTERNALHANDLES_H
|
||||
@@ -0,0 +1,221 @@
|
||||
/***
|
||||
|
||||
Oak Video Editor - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak 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 "../../../include/render/manager.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <new>
|
||||
#include <string>
|
||||
|
||||
#include "alivecount.h"
|
||||
#include "internalhandles.h"
|
||||
|
||||
#include "diskmanager.h"
|
||||
#include "output/viewer/viewer.h"
|
||||
#include "previewautocacher.h"
|
||||
#include "rendermanager.h"
|
||||
#include "renderticket.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
int write_string(const std::string &s, char *buf, int n)
|
||||
{
|
||||
const int required = int(s.size()) + 1;
|
||||
if (buf && n >= required) {
|
||||
std::memcpy(buf, s.c_str(), size_t(required));
|
||||
}
|
||||
return required;
|
||||
}
|
||||
|
||||
std::mutex g_requests_mutex;
|
||||
std::map<int64_t, olive::RenderTicketPtr> g_requests;
|
||||
std::atomic<int64_t> g_next_request_id(1);
|
||||
|
||||
} // namespace
|
||||
|
||||
int oakrender_manager_init(void)
|
||||
{
|
||||
if (olive::RenderManager::instance()) {
|
||||
return OAKRENDER_E_STATE;
|
||||
}
|
||||
try {
|
||||
olive::RenderManager::create_instance();
|
||||
return OAKRENDER_OK;
|
||||
} catch (...) {
|
||||
return OAKRENDER_E_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
void oakrender_manager_shutdown(void)
|
||||
{
|
||||
try {
|
||||
olive::RenderManager::destroy_instance();
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
|
||||
int64_t oakrender_request_frame(OakNodeNode *viewer, int64_t ts,
|
||||
oakrender_frame_ready_fn cb, void *userdata)
|
||||
{
|
||||
if (!viewer || !cb) {
|
||||
return OAKRENDER_E_INVALID;
|
||||
}
|
||||
olive::RenderManager *manager = olive::RenderManager::instance();
|
||||
if (!manager) {
|
||||
return OAKRENDER_E_STATE;
|
||||
}
|
||||
auto *v = dynamic_cast<olive::ViewerOutput *>(
|
||||
reinterpret_cast<olive::Node *>(viewer));
|
||||
if (!v) {
|
||||
return OAKRENDER_E_INVALID;
|
||||
}
|
||||
try {
|
||||
// ts is a frame number in the viewer's video timebase; fall back
|
||||
// to whole seconds when the viewer carries no valid timebase.
|
||||
olive::Rational tb = v->get_video_params().time_base();
|
||||
const olive::Rational time = tb.isNull() ?
|
||||
olive::Rational::from_double(double(ts)) :
|
||||
olive::core::Timecode::timestamp_to_time(ts, tb);
|
||||
|
||||
olive::RenderTicketPtr ticket =
|
||||
manager->get_cacher()->get_single_frame(v, time);
|
||||
if (!ticket) {
|
||||
return OAKRENDER_E_FAILED;
|
||||
}
|
||||
|
||||
const int64_t id =
|
||||
g_next_request_id.fetch_add(1, std::memory_order_relaxed);
|
||||
|
||||
olive::RenderTicket *raw_ticket = ticket.get();
|
||||
ticket->set_finished_callback([ticket, cb, ts, userdata, id]() {
|
||||
OakCodecFrame *handle = nullptr;
|
||||
if (ticket->has_result()) {
|
||||
olive::FramePtr f = ticket->get().value<olive::FramePtr>();
|
||||
if (f) {
|
||||
handle = new (std::nothrow) OakCodecFrame;
|
||||
if (handle) {
|
||||
handle->ptr = std::move(f);
|
||||
oakrender_c_api::alive_inc();
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> locker(g_requests_mutex);
|
||||
g_requests.erase(id);
|
||||
}
|
||||
cb(handle, ts, userdata);
|
||||
});
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> locker(g_requests_mutex);
|
||||
g_requests[id] = ticket;
|
||||
}
|
||||
(void) raw_ticket;
|
||||
return id;
|
||||
} catch (...) {
|
||||
return OAKRENDER_E_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
int oakrender_cancel_request(int64_t request_id)
|
||||
{
|
||||
olive::RenderTicketPtr ticket;
|
||||
{
|
||||
std::lock_guard<std::mutex> locker(g_requests_mutex);
|
||||
auto it = g_requests.find(request_id);
|
||||
if (it == g_requests.end()) {
|
||||
return OAKRENDER_E_NOT_FOUND;
|
||||
}
|
||||
ticket = it->second;
|
||||
g_requests.erase(it);
|
||||
}
|
||||
ticket->cancel();
|
||||
if (olive::RenderManager::instance()) {
|
||||
olive::RenderManager::instance()->remove_ticket(ticket);
|
||||
}
|
||||
return OAKRENDER_OK;
|
||||
}
|
||||
|
||||
int oakrender_set_cacher_multicam(OakNodeNode *multicam_or_NULL)
|
||||
{
|
||||
olive::RenderManager *manager = olive::RenderManager::instance();
|
||||
if (!manager) {
|
||||
return OAKRENDER_E_STATE;
|
||||
}
|
||||
manager->get_cacher()->set_multicam_node(
|
||||
reinterpret_cast<olive::MultiCamNode *>(multicam_or_NULL));
|
||||
return OAKRENDER_OK;
|
||||
}
|
||||
|
||||
int oakrender_set_display_color_processor(OakColorProcessor *p_or_NULL)
|
||||
{
|
||||
olive::RenderManager *manager = olive::RenderManager::instance();
|
||||
if (!manager) {
|
||||
return OAKRENDER_E_STATE;
|
||||
}
|
||||
manager->get_cacher()->set_display_color_processor(
|
||||
p_or_NULL ? p_or_NULL->ptr : nullptr);
|
||||
return OAKRENDER_OK;
|
||||
}
|
||||
|
||||
/* ---- Disk cache ----------------------------------------------------------- */
|
||||
|
||||
int oakrender_disk_cache_path(char *buf, int n)
|
||||
{
|
||||
try {
|
||||
return write_string(olive::DiskManager::get_default_disk_cache_path(),
|
||||
buf, n);
|
||||
} catch (...) {
|
||||
return OAKRENDER_E_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
int64_t oakrender_disk_cache_size(void)
|
||||
{
|
||||
try {
|
||||
olive::DiskManager *dm = olive::DiskManager::instance();
|
||||
if (!dm || !dm->get_default_cache_folder()) {
|
||||
return OAKRENDER_E_FAILED;
|
||||
}
|
||||
return dm->get_default_cache_folder()->get_consumption();
|
||||
} catch (...) {
|
||||
return OAKRENDER_E_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
int oakrender_disk_cache_clear(void)
|
||||
{
|
||||
try {
|
||||
olive::DiskManager *dm = olive::DiskManager::instance();
|
||||
if (!dm) {
|
||||
return OAKRENDER_E_FAILED;
|
||||
}
|
||||
return dm->clear_disk_cache(
|
||||
olive::DiskManager::get_default_disk_cache_path()) ?
|
||||
OAKRENDER_OK :
|
||||
OAKRENDER_E_FAILED;
|
||||
} catch (...) {
|
||||
return OAKRENDER_E_FAILED;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,506 @@
|
||||
/***
|
||||
|
||||
Oak Video Editor - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak 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 "../../../include/render/renderer.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cstring>
|
||||
#include <new>
|
||||
#include <string>
|
||||
|
||||
#include "alivecount.h"
|
||||
#include "internalhandles.h"
|
||||
|
||||
#include "backend/dynamicrenderer.h"
|
||||
#include "job/colortransformjob.h"
|
||||
#include "opengl/openglrenderer.h"
|
||||
#include "renderer.h"
|
||||
#include "rendermanager.h"
|
||||
#include "texture.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
olive::VideoParams pod_to_cpp(const oakrender_video_params &v)
|
||||
{
|
||||
olive::VideoParams vp(
|
||||
v.width, v.height, olive::Rational(v.time_base_num, v.time_base_den),
|
||||
static_cast<olive::PixelFormat::Format>(v.format),
|
||||
olive::VideoParams::k_internal_channel_count,
|
||||
olive::Rational(v.pixel_aspect_num, v.pixel_aspect_den),
|
||||
static_cast<olive::VideoParams::Interlacing>(v.interlacing),
|
||||
v.divider > 0 ? v.divider : 1);
|
||||
vp.set_color_range(static_cast<olive::VideoParams::ColorRange>(v.color_range));
|
||||
vp.set_premultiplied_alpha(v.premultiplied_alpha != 0);
|
||||
return vp;
|
||||
}
|
||||
|
||||
oakrender_video_params cpp_to_pod(const olive::VideoParams &vp)
|
||||
{
|
||||
oakrender_video_params p = {};
|
||||
p.width = vp.width();
|
||||
p.height = vp.height();
|
||||
p.time_base_num = vp.time_base().numerator();
|
||||
p.time_base_den = vp.time_base().denominator();
|
||||
p.format = static_cast<int>(vp.format());
|
||||
p.pixel_aspect_num = vp.pixel_aspect_ratio().numerator();
|
||||
p.pixel_aspect_den = vp.pixel_aspect_ratio().denominator();
|
||||
p.interlacing = static_cast<int>(vp.interlacing());
|
||||
p.color_range = static_cast<int>(vp.color_range());
|
||||
p.divider = vp.divider();
|
||||
p.video_type = 0;
|
||||
p.premultiplied_alpha = vp.premultiplied_alpha() ? 1 : 0;
|
||||
return p;
|
||||
}
|
||||
|
||||
OakRenderTexture *tex(OakRenderTexture *h)
|
||||
{
|
||||
return h;
|
||||
}
|
||||
|
||||
OakCodecFrame *frm(OakCodecFrame *h)
|
||||
{
|
||||
return h;
|
||||
}
|
||||
|
||||
olive::Renderer *ren(OakRenderRenderer *h)
|
||||
{
|
||||
return reinterpret_cast<olive::Renderer *>(h);
|
||||
}
|
||||
|
||||
const olive::Renderer *ren(const OakRenderRenderer *h)
|
||||
{
|
||||
return reinterpret_cast<const olive::Renderer *>(h);
|
||||
}
|
||||
|
||||
olive::Matrix4x4 mat_from_float(const float *f)
|
||||
{
|
||||
olive::Matrix4x4 m;
|
||||
if (!f) {
|
||||
return m;
|
||||
}
|
||||
// All-zero means identity (R7-A §A.2 convention)
|
||||
bool all_zero = true;
|
||||
for (int i = 0; i < 16; i++) {
|
||||
if (f[i] != 0.0f) {
|
||||
all_zero = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (all_zero) {
|
||||
return m;
|
||||
}
|
||||
// POD is column-major (QMatrix4x4 layout); Matrix4x4 stores [row][col]
|
||||
for (int col = 0; col < 4; col++) {
|
||||
for (int row = 0; row < 4; row++) {
|
||||
m(row, col) = f[col * 4 + row];
|
||||
}
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
int write_string(const std::string &s, char *buf, int n)
|
||||
{
|
||||
const int required = int(s.size()) + 1;
|
||||
if (buf && n >= required) {
|
||||
std::memcpy(buf, s.c_str(), size_t(required));
|
||||
}
|
||||
return required;
|
||||
}
|
||||
|
||||
// Requested backend recorded through oakrender_set_backend(); applied to
|
||||
// the RenderManager instance when one is created by the facade.
|
||||
std::string g_requested_backend = "opengl";
|
||||
|
||||
} // namespace
|
||||
|
||||
/* ---- Renderer lifecycle -------------------------------------------------- */
|
||||
|
||||
OakRenderRenderer *oakrender_display_renderer_create_dynamic(
|
||||
const char *backend_id)
|
||||
{
|
||||
if (!backend_id || !*backend_id) {
|
||||
return nullptr;
|
||||
}
|
||||
try {
|
||||
auto *r = new olive::DynamicRenderer(backend_id);
|
||||
if (!r->load()) {
|
||||
delete r;
|
||||
return nullptr;
|
||||
}
|
||||
return reinterpret_cast<OakRenderRenderer *>(r);
|
||||
} catch (...) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
OakRenderRenderer *oakrender_display_renderer_create_opengl(void)
|
||||
{
|
||||
try {
|
||||
return reinterpret_cast<OakRenderRenderer *>(new olive::OpenGLRenderer());
|
||||
} catch (...) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
int oakrender_display_renderer_init(OakRenderRenderer *renderer,
|
||||
void *gl_context)
|
||||
{
|
||||
olive::Renderer *r = ren(renderer);
|
||||
if (!r) {
|
||||
return OAKRENDER_E_INVALID;
|
||||
}
|
||||
try {
|
||||
if (gl_context) {
|
||||
auto *ctx = static_cast<olive::OpenGLContext *>(gl_context);
|
||||
if (auto *gl = dynamic_cast<olive::OpenGLRenderer *>(r)) {
|
||||
gl->init(ctx);
|
||||
return OAKRENDER_OK;
|
||||
}
|
||||
if (auto *dyn = dynamic_cast<olive::DynamicRenderer *>(r)) {
|
||||
return dyn->init_with_open_gl_context(ctx) ? OAKRENDER_OK :
|
||||
OAKRENDER_E_FAILED;
|
||||
}
|
||||
return OAKRENDER_E_INVALID;
|
||||
}
|
||||
return r->init() ? OAKRENDER_OK : OAKRENDER_E_FAILED;
|
||||
} catch (...) {
|
||||
return OAKRENDER_E_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
void oakrender_display_renderer_destroy(OakRenderRenderer *renderer)
|
||||
{
|
||||
olive::Renderer *r = ren(renderer);
|
||||
if (!r) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
r->destroy();
|
||||
delete r;
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Renderer queries ---------------------------------------------------- */
|
||||
|
||||
int oakrender_display_renderer_is_open_gl(const OakRenderRenderer *renderer)
|
||||
{
|
||||
return renderer && ren(renderer)->is_open_gl() ? 1 : 0;
|
||||
}
|
||||
|
||||
int oakrender_display_renderer_is_vulkan(const OakRenderRenderer *renderer)
|
||||
{
|
||||
return renderer && ren(renderer)->is_vulkan() ? 1 : 0;
|
||||
}
|
||||
|
||||
/* ---- Texture handle ------------------------------------------------------ */
|
||||
|
||||
OakRenderTexture *oakrender_display_texture_create(
|
||||
OakRenderRenderer *renderer, const oakrender_video_params *params,
|
||||
const void *pixels, int linesize)
|
||||
{
|
||||
olive::Renderer *r = ren(renderer);
|
||||
if (!r || !params) {
|
||||
return nullptr;
|
||||
}
|
||||
try {
|
||||
olive::TexturePtr t = r->create_texture(pod_to_cpp(*params), pixels,
|
||||
linesize);
|
||||
if (!t) {
|
||||
return nullptr;
|
||||
}
|
||||
auto *block = new OakRenderTexture;
|
||||
block->ptr = std::move(t);
|
||||
oakrender_c_api::alive_inc();
|
||||
return block;
|
||||
} catch (...) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
OakRenderTexture *oakrender_display_texture_retain(OakRenderTexture *texture)
|
||||
{
|
||||
if (!texture) {
|
||||
return nullptr;
|
||||
}
|
||||
tex(texture)->refcount.fetch_add(1, std::memory_order_relaxed);
|
||||
return texture;
|
||||
}
|
||||
|
||||
void oakrender_display_texture_free(OakRenderTexture *texture)
|
||||
{
|
||||
if (!texture) {
|
||||
return;
|
||||
}
|
||||
if (tex(texture)->refcount.fetch_sub(1, std::memory_order_acq_rel) == 1) {
|
||||
delete tex(texture);
|
||||
oakrender_c_api::alive_dec();
|
||||
}
|
||||
}
|
||||
|
||||
int oakrender_display_texture_upload(OakRenderTexture *texture,
|
||||
const void *pixels, int linesize)
|
||||
{
|
||||
if (!texture || !pixels || !tex(texture)->ptr) {
|
||||
return OAKRENDER_E_INVALID;
|
||||
}
|
||||
try {
|
||||
tex(texture)->ptr->upload(const_cast<void *>(pixels), linesize);
|
||||
return OAKRENDER_OK;
|
||||
} catch (...) {
|
||||
return OAKRENDER_E_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
int oakrender_display_texture_download(OakRenderTexture *texture, void *pixels,
|
||||
int linesize)
|
||||
{
|
||||
if (!texture || !pixels || !tex(texture)->ptr) {
|
||||
return OAKRENDER_E_INVALID;
|
||||
}
|
||||
try {
|
||||
tex(texture)->ptr->download(pixels, linesize);
|
||||
return OAKRENDER_OK;
|
||||
} catch (...) {
|
||||
return OAKRENDER_E_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
int oakrender_display_texture_get_params(const OakRenderTexture *texture,
|
||||
oakrender_video_params *out)
|
||||
{
|
||||
if (!texture || !out || !tex(const_cast<OakRenderTexture *>(texture))->ptr) {
|
||||
return OAKRENDER_E_INVALID;
|
||||
}
|
||||
*out = cpp_to_pod(tex(const_cast<OakRenderTexture *>(texture))->ptr->params());
|
||||
return OAKRENDER_OK;
|
||||
}
|
||||
|
||||
int oakrender_display_texture_id(const OakRenderTexture *texture)
|
||||
{
|
||||
if (!texture || !tex(const_cast<OakRenderTexture *>(texture))->ptr) {
|
||||
return 0;
|
||||
}
|
||||
return tex(const_cast<OakRenderTexture *>(texture))->ptr->id().to_int();
|
||||
}
|
||||
|
||||
/* ---- Frame handle -------------------------------------------------------- */
|
||||
|
||||
OakCodecFrame *oakrender_codec_frame_create(void)
|
||||
{
|
||||
try {
|
||||
auto *block = new OakCodecFrame;
|
||||
block->ptr = olive::Frame::create();
|
||||
oakrender_c_api::alive_inc();
|
||||
return block;
|
||||
} catch (...) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
OakCodecFrame *oakrender_codec_frame_retain(OakCodecFrame *frame)
|
||||
{
|
||||
if (!frame) {
|
||||
return nullptr;
|
||||
}
|
||||
frm(frame)->refcount.fetch_add(1, std::memory_order_relaxed);
|
||||
return frame;
|
||||
}
|
||||
|
||||
void oakrender_codec_frame_free(OakCodecFrame *frame)
|
||||
{
|
||||
if (!frame) {
|
||||
return;
|
||||
}
|
||||
if (frm(frame)->refcount.fetch_sub(1, std::memory_order_acq_rel) == 1) {
|
||||
delete frm(frame);
|
||||
oakrender_c_api::alive_dec();
|
||||
}
|
||||
}
|
||||
|
||||
int oakrender_codec_frame_set_video_params(
|
||||
OakCodecFrame *frame, const oakrender_video_params *params)
|
||||
{
|
||||
if (!frame || !params || !frm(frame)->ptr) {
|
||||
return OAKRENDER_E_INVALID;
|
||||
}
|
||||
frm(frame)->ptr->set_video_params(pod_to_cpp(*params));
|
||||
return OAKRENDER_OK;
|
||||
}
|
||||
|
||||
int oakrender_codec_frame_get_params(const OakCodecFrame *frame,
|
||||
oakrender_video_params *out)
|
||||
{
|
||||
if (!frame || !out || !frm(const_cast<OakCodecFrame *>(frame))->ptr) {
|
||||
return OAKRENDER_E_INVALID;
|
||||
}
|
||||
*out = cpp_to_pod(
|
||||
frm(const_cast<OakCodecFrame *>(frame))->ptr->video_params());
|
||||
return OAKRENDER_OK;
|
||||
}
|
||||
|
||||
int oakrender_codec_frame_allocate(OakCodecFrame *frame)
|
||||
{
|
||||
if (!frame || !frm(frame)->ptr) {
|
||||
return OAKRENDER_E_INVALID;
|
||||
}
|
||||
return frm(frame)->ptr->allocate() ? OAKRENDER_OK : OAKRENDER_E_FAILED;
|
||||
}
|
||||
|
||||
void *oakrender_codec_frame_data(OakCodecFrame *frame)
|
||||
{
|
||||
if (!frame || !frm(frame)->ptr) {
|
||||
return nullptr;
|
||||
}
|
||||
return frm(frame)->ptr->data();
|
||||
}
|
||||
|
||||
const void *oakrender_codec_frame_const_data(const OakCodecFrame *frame)
|
||||
{
|
||||
if (!frame || !frm(const_cast<OakCodecFrame *>(frame))->ptr) {
|
||||
return nullptr;
|
||||
}
|
||||
return frm(const_cast<OakCodecFrame *>(frame))->ptr->const_data();
|
||||
}
|
||||
|
||||
int oakrender_codec_frame_linesize_bytes(const OakCodecFrame *frame)
|
||||
{
|
||||
if (!frame || !frm(const_cast<OakCodecFrame *>(frame))->ptr) {
|
||||
return 0;
|
||||
}
|
||||
return frm(const_cast<OakCodecFrame *>(frame))->ptr->linesize_bytes();
|
||||
}
|
||||
|
||||
int oakrender_codec_frame_is_allocated(const OakCodecFrame *frame)
|
||||
{
|
||||
if (!frame || !frm(const_cast<OakCodecFrame *>(frame))->ptr) {
|
||||
return 0;
|
||||
}
|
||||
return frm(const_cast<OakCodecFrame *>(frame))->ptr->is_allocated() ? 1 : 0;
|
||||
}
|
||||
|
||||
/* ---- Color-managed blit -------------------------------------------------- */
|
||||
|
||||
int oakrender_display_renderer_blit_color_managed(
|
||||
OakRenderRenderer *renderer, const oakrender_color_transform_job *job,
|
||||
OakRenderTexture *dst_texture, const oakrender_video_params *params)
|
||||
{
|
||||
olive::Renderer *r = ren(renderer);
|
||||
if (!r || !job) {
|
||||
return OAKRENDER_E_INVALID;
|
||||
}
|
||||
try {
|
||||
olive::ColorTransformJob ctj;
|
||||
if (job->processor) {
|
||||
ctj.set_color_processor(
|
||||
static_cast<const OakColorProcessor *>(job->processor)->ptr);
|
||||
}
|
||||
if (job->input_texture) {
|
||||
ctj.set_input_texture(
|
||||
static_cast<OakRenderTexture *>(job->input_texture)->ptr);
|
||||
}
|
||||
ctj.set_input_alpha_association(
|
||||
static_cast<olive::AlphaAssociated>(job->input_alpha_association));
|
||||
ctj.set_clear_destination_enabled(job->clear_destination != 0);
|
||||
ctj.set_force_opaque(job->force_opaque != 0);
|
||||
ctj.set_transform_matrix(mat_from_float(job->matrix));
|
||||
ctj.set_crop_matrix(mat_from_float(job->crop_matrix));
|
||||
|
||||
olive::Texture *dst = dst_texture ? tex(dst_texture)->ptr.get() : nullptr;
|
||||
if (params) {
|
||||
r->blit_color_managed(ctj, dst, pod_to_cpp(*params));
|
||||
} else if (dst) {
|
||||
r->blit_color_managed(ctj, dst, dst->params());
|
||||
} else {
|
||||
return OAKRENDER_E_INVALID;
|
||||
}
|
||||
return OAKRENDER_OK;
|
||||
} catch (...) {
|
||||
return OAKRENDER_E_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Cross-backend texture download -------------------------------------- */
|
||||
|
||||
int oakrender_display_renderer_download_from_texture(
|
||||
OakRenderRenderer *renderer, int texture_id,
|
||||
const oakrender_video_params *params, void *dst_pixels, int linesize)
|
||||
{
|
||||
olive::Renderer *r = ren(renderer);
|
||||
if (!r || !params || !dst_pixels) {
|
||||
return OAKRENDER_E_INVALID;
|
||||
}
|
||||
try {
|
||||
r->download_from_texture(texture_id, pod_to_cpp(*params), dst_pixels,
|
||||
linesize);
|
||||
return OAKRENDER_OK;
|
||||
} catch (...) {
|
||||
return OAKRENDER_E_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Backend management -------------------------------------------------- */
|
||||
|
||||
int oakrender_backend_count(void)
|
||||
{
|
||||
// olive::RenderManager::Backend: k_open_gl, k_vulkan, k_multi_process,
|
||||
// k_dummy
|
||||
return 4;
|
||||
}
|
||||
|
||||
int oakrender_backend_id_at(int i, char *buf, int n)
|
||||
{
|
||||
if (i < 0 || i >= oakrender_backend_count()) {
|
||||
return OAKRENDER_E_NOT_FOUND;
|
||||
}
|
||||
return write_string(
|
||||
olive::RenderManager::backend_to_string(
|
||||
static_cast<olive::RenderManager::Backend>(i)),
|
||||
buf, n);
|
||||
}
|
||||
|
||||
int oakrender_set_backend(const char *backend_id)
|
||||
{
|
||||
if (!backend_id) {
|
||||
return OAKRENDER_E_INVALID;
|
||||
}
|
||||
std::string lower = backend_id;
|
||||
std::transform(lower.begin(), lower.end(), lower.begin(),
|
||||
[](unsigned char c) { return char(std::tolower(c)); });
|
||||
if (lower != "opengl" && lower != "vulkan" && lower != "multiprocess" &&
|
||||
lower != "dummy") {
|
||||
return OAKRENDER_E_INVALID;
|
||||
}
|
||||
g_requested_backend = lower;
|
||||
return OAKRENDER_OK;
|
||||
}
|
||||
|
||||
int oakrender_current_backend(char *buf, int n)
|
||||
{
|
||||
if (olive::RenderManager::instance()) {
|
||||
return write_string(
|
||||
olive::RenderManager::backend_to_string(
|
||||
olive::RenderManager::instance()->backend()),
|
||||
buf, n);
|
||||
}
|
||||
return write_string(g_requested_backend, buf, n);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
# Olive - Non-Linear Video Editor
|
||||
# Copyright (C) 2022 Olive Team
|
||||
# Modifications Copyright (C) 2025 mikesolar
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
file(GLOB_RECURSE OAKRENDER_SOURCES CONFIGURE_DEPENDS *.cpp)
|
||||
|
||||
# The backend_c translation units are the C ABI entry points of the
|
||||
# dynamically loaded backend libraries (liboakgl/liboakvulkan, see
|
||||
# backend/dynamicrenderer.cpp). They export the same oak_renderer_* symbol
|
||||
# table, so they must NOT be linked into liboakrender itself.
|
||||
list(REMOVE_ITEM OAKRENDER_SOURCES
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/opengl/openglbackend_c.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vulkan/vulkanbackend_c.cpp
|
||||
)
|
||||
|
||||
add_library(oakrender SHARED ${OAKRENDER_SOURCES})
|
||||
|
||||
# Dynamically loaded render backends (oak_renderer_* C ABI). Loaded via
|
||||
# dlopen by DynamicRenderer from the app render_backends/ dir.
|
||||
add_library(oakgl SHARED opengl/openglbackend_c.cpp)
|
||||
add_library(oakvulkan SHARED vulkan/vulkanbackend_c.cpp)
|
||||
foreach(backend oakgl oakvulkan)
|
||||
target_link_libraries(${backend} PRIVATE oakrender)
|
||||
endforeach()
|
||||
|
||||
target_include_directories(oakrender PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${OAK_REPO_ROOT}/include
|
||||
${OAK_REPO_ROOT}/src/common/src
|
||||
${OAK_REPO_ROOT}/src/undo/src
|
||||
${OAK_REPO_ROOT}/src/node/src
|
||||
${OAK_REPO_ROOT}/core/include
|
||||
${OAK_REPO_ROOT}/ffmpeg_bridge/include
|
||||
${OAK_REPO_ROOT}/third_party/openfx/include
|
||||
${OCIO_INCLUDE_DIRS}
|
||||
${OIIO_INCLUDE_DIRS}
|
||||
)
|
||||
@@ -0,0 +1,32 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_ALPHAASSOC_H
|
||||
#define OAK_ALPHAASSOC_H
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
enum AlphaAssociated { k_alpha_none, k_alpha_unassociated, k_alpha_associated };
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_ALPHAASSOC_H
|
||||
@@ -0,0 +1,162 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "audioplaybackcache.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <vector>
|
||||
|
||||
#include "filefunctions.h"
|
||||
#include "output/viewer/viewer.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
const int64_t AudioPlaybackCache::k_default_segment_size_per_channel =
|
||||
10 * 1024 * 1024;
|
||||
|
||||
AudioPlaybackCache::AudioPlaybackCache(Node *parent)
|
||||
: PlaybackCache(parent)
|
||||
{
|
||||
}
|
||||
|
||||
AudioPlaybackCache::~AudioPlaybackCache()
|
||||
{
|
||||
}
|
||||
|
||||
void AudioPlaybackCache::set_parameters(const AudioParams ¶ms)
|
||||
{
|
||||
if (params_ == params) {
|
||||
return;
|
||||
}
|
||||
|
||||
params_ = params;
|
||||
}
|
||||
|
||||
void AudioPlaybackCache::write_pcm(const TimeRange &range,
|
||||
const TimeRangeList &valid_ranges,
|
||||
const SampleBuffer &samples)
|
||||
{
|
||||
for (const TimeRange &r : valid_ranges) {
|
||||
if (write_part_of_sample_buffer(samples, r.in(), r.in() - range.in(),
|
||||
r.length())) {
|
||||
validate(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AudioPlaybackCache::write_silence(const TimeRange &range)
|
||||
{
|
||||
// WritePCM will automatically fill non-existent bytes with silence, so we just have to send
|
||||
// it an empty sample buffer
|
||||
write_pcm(range, { range }, SampleBuffer());
|
||||
}
|
||||
|
||||
bool AudioPlaybackCache::write_part_of_sample_buffer(const SampleBuffer &samples,
|
||||
const Rational &write_start,
|
||||
const Rational &buffer_start,
|
||||
const Rational &length)
|
||||
{
|
||||
int64_t length_in_bytes = params_.time_to_bytes_per_channel(length);
|
||||
|
||||
int64_t start_cache_offset = params_.time_to_bytes_per_channel(write_start);
|
||||
int64_t end_cache_offset = start_cache_offset + length_in_bytes;
|
||||
|
||||
int64_t start_buffer_offset =
|
||||
params_.time_to_bytes_per_channel(buffer_start);
|
||||
int64_t end_buffer_offset =
|
||||
std::min(start_buffer_offset + length_in_bytes,
|
||||
params_.samples_to_bytes_per_channel(samples.sample_count()));
|
||||
|
||||
int64_t current_cache_offset = start_cache_offset;
|
||||
int64_t current_buffer_offset = start_buffer_offset;
|
||||
|
||||
bool success = true;
|
||||
|
||||
while (current_cache_offset != end_cache_offset) {
|
||||
int64_t segment = current_cache_offset / k_default_segment_size_per_channel;
|
||||
int64_t segment_start = segment * k_default_segment_size_per_channel;
|
||||
int64_t segment_end = segment_start + k_default_segment_size_per_channel;
|
||||
|
||||
int64_t offset_in_segment = current_cache_offset - segment_start;
|
||||
// Never write past the end of the requested range
|
||||
int64_t write_len = std::min(segment_end - current_cache_offset,
|
||||
end_cache_offset - current_cache_offset);
|
||||
int64_t max_buffer_len = end_buffer_offset - current_buffer_offset;
|
||||
int64_t zero_len = 0;
|
||||
|
||||
if (write_len > max_buffer_len) {
|
||||
zero_len = write_len - max_buffer_len;
|
||||
write_len = max_buffer_len;
|
||||
}
|
||||
|
||||
for (int channel = 0; channel < params_.channel_count(); channel++) {
|
||||
std::string filename = get_segment_filename(segment, channel);
|
||||
|
||||
std::filesystem::path dir =
|
||||
std::filesystem::path(filename).parent_path();
|
||||
if (!FileFunctions::directory_is_valid(dir.string())) {
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// QFile::ReadWrite creates the file if it doesn't exist; fopen's
|
||||
// "r+b" does not, so fall back to "w+b".
|
||||
std::FILE *f = std::fopen(filename.c_str(), "r+b");
|
||||
if (!f) {
|
||||
f = std::fopen(filename.c_str(), "w+b");
|
||||
}
|
||||
if (f) {
|
||||
std::fseek(f, offset_in_segment, SEEK_SET);
|
||||
if (write_len > 0) {
|
||||
std::fwrite(reinterpret_cast<const char *>(samples.data(channel)) +
|
||||
current_buffer_offset,
|
||||
1, write_len, f);
|
||||
}
|
||||
|
||||
if (zero_len > 0) {
|
||||
std::vector<char> b(zero_len, 0);
|
||||
std::fwrite(b.data(), 1, b.size(), f);
|
||||
}
|
||||
|
||||
std::fclose(f);
|
||||
} else {
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
|
||||
current_cache_offset += write_len + zero_len;
|
||||
current_buffer_offset += write_len;
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
std::string AudioPlaybackCache::get_segment_filename(int64_t segment_index,
|
||||
int channel)
|
||||
{
|
||||
return (std::filesystem::path(get_this_cache_directory()) /
|
||||
(std::to_string(segment_index) + "." + std::to_string(channel)))
|
||||
.string();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_AUDIOPLAYBACKCACHE_H
|
||||
#define OAK_AUDIOPLAYBACKCACHE_H
|
||||
|
||||
#include "audio/audiovisualwaveform.h"
|
||||
#include "playbackcache.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief A fully integrated system of storing and playing cached audio
|
||||
*
|
||||
* All audio in Olive is processed and rendered in advance. This makes playback extremely smooth
|
||||
* and reliable, but provides some challenges as far as storing and manipulating this audio while
|
||||
* minimizing the amount of re-renders necessary.
|
||||
*
|
||||
* Olive's PlaybackCaches support "shifting"; moving cached data to a different spot on the
|
||||
* timeline without requiring a costly re-render. While video is naturally stored on disk as
|
||||
* separate frames that are easy to swap out, audio works a little differently. It would be
|
||||
* extremely inefficient to store each sample as a separate file on the disk, but storing in
|
||||
* one single contiguous file would be detrimental to shifting, particularly for longer timelines
|
||||
* since the data will actually have to be shifted on disk.
|
||||
*
|
||||
* As such, AudioPlaybackCache compromises by storing audio in several "segments". This makes
|
||||
* operations like shifting much easier since segments can simply be removed from the playlist
|
||||
* rather than having to shift or re-render potentially hours of audio in every operation.
|
||||
*
|
||||
* Naturally, storing in segments means you can't simply play the PCM data like a file, so
|
||||
* AudioPlaybackCache also provides a playback device (accessible from CreatePlaybackDevice()) that
|
||||
* acts identically to a file-based IO device, transparently joining segments together and acting
|
||||
* like one contiguous file.
|
||||
*/
|
||||
class AudioPlaybackCache : public PlaybackCache {
|
||||
public:
|
||||
AudioPlaybackCache(Node *parent = nullptr);
|
||||
|
||||
virtual ~AudioPlaybackCache() override;
|
||||
|
||||
AudioParams get_parameters()
|
||||
{
|
||||
return params_;
|
||||
}
|
||||
|
||||
void set_parameters(const AudioParams ¶ms);
|
||||
|
||||
void write_pcm(const TimeRange &range, const TimeRangeList &valid_ranges,
|
||||
const SampleBuffer &samples);
|
||||
|
||||
void write_silence(const TimeRange &range);
|
||||
|
||||
private:
|
||||
bool write_part_of_sample_buffer(const SampleBuffer &samples,
|
||||
const Rational &write_start,
|
||||
const Rational &buffer_start,
|
||||
const Rational &length);
|
||||
|
||||
std::string get_segment_filename(int64_t segment_index, int channel);
|
||||
|
||||
static const int64_t k_default_segment_size_per_channel;
|
||||
|
||||
AudioParams params_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_AUDIOPLAYBACKCACHE_H
|
||||
@@ -0,0 +1,85 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "audiowaveformcache.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
#define super PlaybackCache
|
||||
|
||||
AudioWaveformCache::AudioWaveformCache(Node *parent)
|
||||
: super{ parent }
|
||||
{
|
||||
waveforms_ = std::make_shared<AudioVisualWaveform>();
|
||||
}
|
||||
|
||||
void AudioWaveformCache::write_waveform(const TimeRange &range,
|
||||
const TimeRangeList &valid_ranges,
|
||||
const AudioVisualWaveform *waveform)
|
||||
{
|
||||
// Write each valid range to the segments
|
||||
for (const TimeRange &r : valid_ranges) {
|
||||
if (waveform) {
|
||||
waveforms_->overwrite_sums(*waveform, r.in(), r.in() - range.in(),
|
||||
r.length());
|
||||
}
|
||||
|
||||
validate(r);
|
||||
}
|
||||
}
|
||||
|
||||
AudioVisualWaveform::Sample
|
||||
AudioWaveformCache::get_summary_from_time(const Rational &start,
|
||||
const Rational &length) const
|
||||
{
|
||||
return waveforms_->get_summary_from_time(start, length);
|
||||
}
|
||||
|
||||
Rational AudioWaveformCache::length() const
|
||||
{
|
||||
return waveforms_->length();
|
||||
}
|
||||
|
||||
void AudioWaveformCache::set_passthrough(PlaybackCache *cache)
|
||||
{
|
||||
AudioWaveformCache *c = static_cast<AudioWaveformCache *>(cache);
|
||||
|
||||
for (const TimeRange &r : c->get_validated_ranges()) {
|
||||
WaveformPassthrough t = r;
|
||||
t.waveform = c->waveforms_;
|
||||
passthroughs_.push_back(t);
|
||||
}
|
||||
passthroughs_.insert(passthroughs_.end(), c->passthroughs_.begin(),
|
||||
c->passthroughs_.end());
|
||||
|
||||
set_parameters(c->get_parameters());
|
||||
set_saving_enabled(c->is_saving_enabled());
|
||||
}
|
||||
|
||||
void AudioWaveformCache::InvalidateEvent(const TimeRange &range)
|
||||
{
|
||||
TimeRangeList::util_remove(&passthroughs_, range);
|
||||
|
||||
super::InvalidateEvent(range);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_AUDIOWAVEFORMCACHE_H
|
||||
#define OAK_AUDIOWAVEFORMCACHE_H
|
||||
|
||||
#include "audio/audiovisualwaveform.h"
|
||||
#include "playbackcache.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class AudioWaveformCache : public PlaybackCache {
|
||||
public:
|
||||
AudioWaveformCache(Node *parent = nullptr);
|
||||
|
||||
void write_waveform(const TimeRange &range,
|
||||
const TimeRangeList &valid_ranges,
|
||||
const AudioVisualWaveform *waveform);
|
||||
|
||||
const AudioParams &get_parameters() const
|
||||
{
|
||||
return params_;
|
||||
}
|
||||
void set_parameters(const AudioParams &p)
|
||||
{
|
||||
params_ = p;
|
||||
waveforms_->set_channel_count(p.channel_count());
|
||||
}
|
||||
|
||||
// The QPainter-based Draw() was UI rendering and moved to the app layer.
|
||||
|
||||
AudioVisualWaveform::Sample
|
||||
get_summary_from_time(const Rational &start, const Rational &length) const;
|
||||
|
||||
Rational length() const;
|
||||
|
||||
virtual void set_passthrough(PlaybackCache *cache) override;
|
||||
|
||||
protected:
|
||||
virtual void InvalidateEvent(const TimeRange &range) override;
|
||||
|
||||
private:
|
||||
using WaveformPtr = std::shared_ptr<AudioVisualWaveform>;
|
||||
|
||||
WaveformPtr waveforms_;
|
||||
|
||||
AudioParams params_;
|
||||
|
||||
class WaveformPassthrough : public TimeRange {
|
||||
public:
|
||||
WaveformPassthrough(const TimeRange &r)
|
||||
: TimeRange(r)
|
||||
{
|
||||
}
|
||||
|
||||
WaveformPtr waveform;
|
||||
};
|
||||
|
||||
std::vector<WaveformPassthrough> passthroughs_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_AUDIOWAVEFORMCACHE_H
|
||||
@@ -0,0 +1,407 @@
|
||||
#include "dynamicrenderer.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <vector>
|
||||
|
||||
#include "../paths.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
static std::string to_lower_copy(std::string s)
|
||||
{
|
||||
for (char &c : s) {
|
||||
if (c >= 'A' && c <= 'Z') {
|
||||
c = char(c - 'A' + 'a');
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
// Stores the requested backend name; the actual backend may later become
|
||||
// OpenGL if loading or availability checks require a Vulkan fallback.
|
||||
DynamicRenderer::DynamicRenderer(const std::string &backend)
|
||||
: backend_(to_lower_copy(backend))
|
||||
{
|
||||
}
|
||||
|
||||
// Tears down the backend in the reverse order used by Load(): release renderer
|
||||
// resources, then destroy the opaque backend object. The shared library itself
|
||||
// is deliberately NOT unloaded: multiple DynamicRenderer instances can wrap the
|
||||
// same backend library, and one instance's dlclose can unmap code that other
|
||||
// instances still reference, producing calls into unmapped memory.
|
||||
// Backend libraries stay mapped until process exit.
|
||||
DynamicRenderer::~DynamicRenderer()
|
||||
{
|
||||
destroy();
|
||||
post_destroy();
|
||||
if (handle_ && destroy_) {
|
||||
destroy_(handle_);
|
||||
handle_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// Builds the private backend library path for the current platform.
|
||||
// The search is intentionally restricted to Oak-controlled directories so a
|
||||
// system libGL/libvulkan loader is never mistaken for an Oak render backend.
|
||||
std::string DynamicRenderer::library_filename() const
|
||||
{
|
||||
std::string base;
|
||||
if (backend_ == "opengl") {
|
||||
base = "oakgl";
|
||||
} else if (backend_ == "vulkan") {
|
||||
base = "oakvulkan";
|
||||
} else {
|
||||
// Unknown backend: use the name verbatim so the load fails and the
|
||||
// caller's OpenGL fallback engages
|
||||
base = backend_;
|
||||
}
|
||||
#if defined(_WIN32)
|
||||
const std::string filename = base + ".dll";
|
||||
#elif defined(__APPLE__)
|
||||
const std::string filename = "lib" + base + ".dylib";
|
||||
#else
|
||||
const std::string filename = "lib" + base + ".so";
|
||||
#endif
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
const fs::path app_dir(application_dir_path());
|
||||
const std::vector<std::string> candidates = {
|
||||
(app_dir / filename).string(),
|
||||
(app_dir / "render_backends" / filename).lexically_normal().string(),
|
||||
(app_dir / ".." / "lib" / filename).lexically_normal().string(),
|
||||
(app_dir / ".." / ".." / "lib" / filename).lexically_normal().string(),
|
||||
(app_dir / ".." / "engine" / filename).lexically_normal().string(),
|
||||
(app_dir / ".." / ".." / "engine" / filename).lexically_normal().string()
|
||||
};
|
||||
for (const std::string &candidate : candidates) {
|
||||
std::error_code ec;
|
||||
if (fs::exists(candidate, ec)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return candidates.front();
|
||||
}
|
||||
|
||||
// Loads the selected backend, resolves its C ABI table, creates the opaque
|
||||
// backend object, and optionally falls back from Vulkan to OpenGL when runtime
|
||||
// availability checks fail.
|
||||
bool DynamicRenderer::load()
|
||||
{
|
||||
if (handle_) {
|
||||
return true;
|
||||
}
|
||||
|
||||
library_.set_file_name(library_filename());
|
||||
if (!library_.load()) {
|
||||
if (backend_ == "vulkan") {
|
||||
fprintf(stderr,
|
||||
"Failed to load Vulkan render backend %s: %s; falling back "
|
||||
"to OpenGL backend\n",
|
||||
library_.file_name().c_str(),
|
||||
library_.error_string().c_str());
|
||||
backend_ = "opengl";
|
||||
library_.set_file_name(library_filename());
|
||||
}
|
||||
|
||||
if (!library_.load()) {
|
||||
fprintf(stderr, "Failed to load render backend %s %s: %s\n",
|
||||
backend_.c_str(), library_.file_name().c_str(),
|
||||
library_.error_string().c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!resolve_functions()) {
|
||||
fprintf(stderr, "Render backend is missing required symbols %s\n",
|
||||
backend_.c_str());
|
||||
library_.unload();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Pass this so the backend renderer is anchored to the adapter; that way it
|
||||
// follows DynamicRenderer when the latter is adopted by the render thread.
|
||||
// Otherwise it stays in the thread where Load() was called and every GL
|
||||
// operation is rejected as "wrong thread", producing a black screen.
|
||||
handle_ = create_(this);
|
||||
if (!handle_) {
|
||||
library_.unload();
|
||||
return false;
|
||||
}
|
||||
if (is_available_ && !is_available_(handle_)) {
|
||||
fprintf(stderr, "Render backend is not available %s %s\n",
|
||||
backend_.c_str(), library_.file_name().c_str());
|
||||
if (backend_ == "vulkan") {
|
||||
return fallback_to_open_gl();
|
||||
}
|
||||
destroy_(handle_);
|
||||
handle_ = nullptr;
|
||||
library_.unload();
|
||||
return false;
|
||||
}
|
||||
return handle_ != nullptr;
|
||||
}
|
||||
|
||||
// Resolves the mandatory C ABI entry points from the loaded shared library.
|
||||
// Optional information probes are resolved after the required render interface.
|
||||
bool DynamicRenderer::resolve_functions()
|
||||
{
|
||||
reset_functions();
|
||||
#define RESOLVE(member, type, symbol) \
|
||||
member = reinterpret_cast<type>(library_.resolve(symbol)); \
|
||||
if (!member) \
|
||||
return false
|
||||
|
||||
RESOLVE(create_, OakBackendCreateFn, "oak_renderer_create");
|
||||
RESOLVE(destroy_, OakBackendDestroyFn, "oak_renderer_destroy");
|
||||
RESOLVE(init_, OakBackendInitFn, "oak_renderer_init");
|
||||
RESOLVE(init_with_context_, OakBackendInitWithContextFn,
|
||||
"oak_renderer_init_with_context");
|
||||
RESOLVE(post_init_, OakBackendPostInitFn, "oak_renderer_post_init");
|
||||
RESOLVE(post_destroy_, OakBackendPostDestroyFn,
|
||||
"oak_renderer_post_destroy");
|
||||
RESOLVE(destroy_internal_, OakBackendDestroyInternalFn,
|
||||
"oak_renderer_destroy_internal");
|
||||
RESOLVE(clear_destination_, OakBackendClearDestinationFn,
|
||||
"oak_renderer_clear_destination");
|
||||
RESOLVE(create_native_texture_, OakBackendCreateNativeTextureFn,
|
||||
"oak_renderer_create_native_texture");
|
||||
RESOLVE(destroy_native_texture_, OakBackendDestroyNativeTextureFn,
|
||||
"oak_renderer_destroy_native_texture");
|
||||
RESOLVE(create_native_shader_, OakBackendCreateNativeShaderFn,
|
||||
"oak_renderer_create_native_shader");
|
||||
RESOLVE(destroy_native_shader_, OakBackendDestroyNativeShaderFn,
|
||||
"oak_renderer_destroy_native_shader");
|
||||
RESOLVE(upload_to_texture_, OakBackendUploadToTextureFn,
|
||||
"oak_renderer_upload_to_texture");
|
||||
RESOLVE(download_from_texture_, OakBackendDownloadFromTextureFn,
|
||||
"oak_renderer_download_from_texture");
|
||||
RESOLVE(flush_, OakBackendFlushFn, "oak_renderer_flush");
|
||||
RESOLVE(get_pixel_from_texture_, OakBackendGetPixelFromTextureFn,
|
||||
"oak_renderer_get_pixel_from_texture");
|
||||
RESOLVE(blit_, OakBackendBlitFn, "oak_renderer_blit");
|
||||
RESOLVE(attach_output_texture_, OakBackendAttachOutputTextureFn,
|
||||
"oak_renderer_attach_output_texture");
|
||||
RESOLVE(detach_output_texture_, OakBackendDetachOutputTextureFn,
|
||||
"oak_renderer_detach_output_texture");
|
||||
RESOLVE(opengl_context_, OakBackendOpenGLContextFn,
|
||||
"oak_renderer_opengl_context");
|
||||
#undef RESOLVE
|
||||
get_info_ = reinterpret_cast<OakBackendGetInfoFn>(
|
||||
library_.resolve("oak_renderer_get_info"));
|
||||
is_available_ = reinterpret_cast<OakBackendIsAvailableFn>(
|
||||
library_.resolve("oak_renderer_is_available"));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Discards a partially-created backend and restarts loading with the OpenGL
|
||||
// backend. This keeps RenderManager's fallback path inside the adapter.
|
||||
bool DynamicRenderer::fallback_to_open_gl()
|
||||
{
|
||||
if (handle_ && destroy_) {
|
||||
destroy_(handle_);
|
||||
handle_ = nullptr;
|
||||
}
|
||||
if (library_.is_loaded()) {
|
||||
library_.unload();
|
||||
}
|
||||
reset_functions();
|
||||
backend_ = "opengl";
|
||||
return load();
|
||||
}
|
||||
|
||||
// Clears all cached C function pointers so a failed backend cannot leave stale
|
||||
// call targets behind for a later fallback load.
|
||||
void DynamicRenderer::reset_functions()
|
||||
{
|
||||
create_ = nullptr;
|
||||
destroy_ = nullptr;
|
||||
get_info_ = nullptr;
|
||||
is_available_ = nullptr;
|
||||
init_ = nullptr;
|
||||
init_with_context_ = nullptr;
|
||||
post_init_ = nullptr;
|
||||
post_destroy_ = nullptr;
|
||||
destroy_internal_ = nullptr;
|
||||
clear_destination_ = nullptr;
|
||||
create_native_texture_ = nullptr;
|
||||
destroy_native_texture_ = nullptr;
|
||||
create_native_shader_ = nullptr;
|
||||
destroy_native_shader_ = nullptr;
|
||||
upload_to_texture_ = nullptr;
|
||||
download_from_texture_ = nullptr;
|
||||
flush_ = nullptr;
|
||||
get_pixel_from_texture_ = nullptr;
|
||||
blit_ = nullptr;
|
||||
attach_output_texture_ = nullptr;
|
||||
detach_output_texture_ = nullptr;
|
||||
opengl_context_ = nullptr;
|
||||
}
|
||||
|
||||
// Returns backend metadata exposed by the dynamic library when available.
|
||||
bool DynamicRenderer::get_backend_info(OakRenderBackendInfo *out_info) const
|
||||
{
|
||||
return handle_ && get_info_ && out_info && get_info_(handle_, out_info);
|
||||
}
|
||||
|
||||
// Initializes the loaded backend using its own context/device creation path.
|
||||
bool DynamicRenderer::init()
|
||||
{
|
||||
return load() && init_(handle_);
|
||||
}
|
||||
|
||||
// Initializes an OpenGL backend against an existing widget context; non-OpenGL
|
||||
// backends may ignore the context on the library side.
|
||||
bool DynamicRenderer::init_with_open_gl_context(OpenGLContext *context)
|
||||
{
|
||||
if (!load()) {
|
||||
return false;
|
||||
}
|
||||
init_with_context_(handle_, context);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Forwards post-destroy cleanup to the backend while the library is still
|
||||
// loaded and its symbols are still valid.
|
||||
void DynamicRenderer::post_destroy()
|
||||
{
|
||||
if (handle_ && post_destroy_) {
|
||||
post_destroy_(handle_);
|
||||
}
|
||||
}
|
||||
|
||||
// Runs backend post-initialization after Init/InitWithOpenGLContext has
|
||||
// established the device or GL context.
|
||||
void DynamicRenderer::post_init()
|
||||
{
|
||||
if (handle_) {
|
||||
post_init_(handle_);
|
||||
}
|
||||
}
|
||||
|
||||
// Forwards render target clearing through the C ABI.
|
||||
void DynamicRenderer::clear_destination(Texture *texture, double r, double g,
|
||||
double b, double a)
|
||||
{
|
||||
clear_destination_(handle_, texture, r, g, b, a);
|
||||
}
|
||||
|
||||
// Creates a backend-native shader and receives the result as an opaque Variant
|
||||
// because this first-generation ABI still shares C++ types between modules.
|
||||
Variant DynamicRenderer::create_native_shader(ShaderCode code)
|
||||
{
|
||||
Variant out;
|
||||
create_native_shader_(handle_, &code, &out);
|
||||
return out;
|
||||
}
|
||||
|
||||
// Releases a backend-native shader handle.
|
||||
void DynamicRenderer::destroy_native_shader(Variant shader)
|
||||
{
|
||||
destroy_native_shader_(handle_, &shader);
|
||||
}
|
||||
|
||||
// Uploads CPU pixel data into a backend texture through the dynamic ABI.
|
||||
void DynamicRenderer::upload_to_texture(const Variant &handle,
|
||||
const VideoParams ¶ms,
|
||||
const void *data, int linesize)
|
||||
{
|
||||
upload_to_texture_(handle_, &handle, ¶ms, data, linesize);
|
||||
}
|
||||
|
||||
// Downloads backend texture data into a caller-provided CPU buffer.
|
||||
void DynamicRenderer::download_from_texture(const Variant &handle,
|
||||
const VideoParams ¶ms, void *data,
|
||||
int linesize)
|
||||
{
|
||||
download_from_texture_(handle_, &handle, ¶ms, data, linesize);
|
||||
}
|
||||
|
||||
// Waits for backend work to become visible to subsequent CPU or GPU consumers.
|
||||
void DynamicRenderer::flush()
|
||||
{
|
||||
flush_(handle_);
|
||||
}
|
||||
|
||||
// Reads a single pixel through the backend-provided readback hook.
|
||||
Color DynamicRenderer::get_pixel_from_texture(Texture *texture, const PointF &pt)
|
||||
{
|
||||
Color out;
|
||||
get_pixel_from_texture_(handle_, texture, &pt, &out);
|
||||
return out;
|
||||
}
|
||||
|
||||
// Exposes the wrapped OpenGL context when the backend is OpenGL; Vulkan returns
|
||||
// null so callers can avoid GL-only paths.
|
||||
OpenGLContext *DynamicRenderer::open_gl_context() const
|
||||
{
|
||||
return opengl_context_ && handle_ ?
|
||||
static_cast<OpenGLContext *>(opengl_context_(handle_)) :
|
||||
nullptr;
|
||||
}
|
||||
|
||||
// Reports the effective backend after any load-time fallback has completed.
|
||||
bool DynamicRenderer::is_open_gl() const
|
||||
{
|
||||
return backend_ == "opengl";
|
||||
}
|
||||
|
||||
bool DynamicRenderer::is_vulkan() const
|
||||
{
|
||||
return backend_ == "vulkan";
|
||||
}
|
||||
|
||||
// Dispatches a shader blit to the loaded backend.
|
||||
void DynamicRenderer::blit(Variant shader, AcceleratedJob &job,
|
||||
Texture *destination, VideoParams destination_params,
|
||||
bool clear_destination)
|
||||
{
|
||||
blit_(handle_, &shader, &job, destination, &destination_params,
|
||||
clear_destination);
|
||||
}
|
||||
|
||||
// Allocates a backend-native texture and wraps its opaque handle in Variant.
|
||||
Variant DynamicRenderer::create_native_texture(int width, int height, int depth,
|
||||
PixelFormat format,
|
||||
int channel_count,
|
||||
const void *data, int linesize)
|
||||
{
|
||||
Variant out;
|
||||
create_native_texture_(handle_, width, height, depth, format, channel_count,
|
||||
data, linesize, &out);
|
||||
return out;
|
||||
}
|
||||
|
||||
// Releases a backend-native texture handle.
|
||||
void DynamicRenderer::destroy_native_texture(Variant texture)
|
||||
{
|
||||
destroy_native_texture_(handle_, &texture);
|
||||
}
|
||||
|
||||
// Releases renderer-owned backend resources before the backend object itself is
|
||||
// destroyed.
|
||||
void DynamicRenderer::destroy_internal()
|
||||
{
|
||||
if (handle_) {
|
||||
destroy_internal_(handle_);
|
||||
}
|
||||
}
|
||||
|
||||
// Exposes OFX OpenGL output binding through the dynamic backend when supported.
|
||||
void DynamicRenderer::attach_output_texture(Texture *texture)
|
||||
{
|
||||
if (attach_output_texture_ && texture) {
|
||||
Variant id = texture->id();
|
||||
attach_output_texture_(handle_, &id);
|
||||
}
|
||||
}
|
||||
|
||||
// Clears any OFX output texture binding owned by the backend.
|
||||
void DynamicRenderer::detach_output_texture()
|
||||
{
|
||||
if (detach_output_texture_) {
|
||||
detach_output_texture_(handle_);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
#ifndef OAK_DYNAMICRENDERER_H
|
||||
#define OAK_DYNAMICRENDERER_H
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "dynlib.h"
|
||||
#include "renderbackend_c.h"
|
||||
#include "../opengl/openglcontextprovider.h"
|
||||
#include "../renderer.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
// C++ Renderer adapter that loads an Oak render backend shared library and
|
||||
// forwards Renderer calls through the backend's C ABI.
|
||||
class DynamicRenderer : public Renderer, public OpenGLContextProvider {
|
||||
public:
|
||||
// Stores the requested backend name; Load() may change it after fallback.
|
||||
explicit DynamicRenderer(const std::string &backend);
|
||||
// Destroys backend resources and unloads the dynamic library.
|
||||
virtual ~DynamicRenderer() override;
|
||||
|
||||
using Renderer::blit;
|
||||
|
||||
// Loads the backend library, resolves C ABI symbols, and creates the handle.
|
||||
bool load();
|
||||
// Initializes an OpenGL backend with a caller-owned viewer context.
|
||||
bool init_with_open_gl_context(OpenGLContext *context);
|
||||
// Retrieves backend metadata through the optional info entry point.
|
||||
bool get_backend_info(OakRenderBackendInfo *out_info) const;
|
||||
// Returns the effective backend after any load-time fallback.
|
||||
std::string backend_name() const
|
||||
{
|
||||
return backend_;
|
||||
}
|
||||
|
||||
// Initializes the backend using its default device/context path.
|
||||
virtual bool init() override;
|
||||
// Runs backend post-destroy cleanup.
|
||||
virtual void post_destroy() override;
|
||||
// Runs backend post-init setup.
|
||||
virtual void post_init() override;
|
||||
// Clears either a native texture destination or the backend output target.
|
||||
virtual void clear_destination(Texture *texture = nullptr, double r = 0.0,
|
||||
double g = 0.0, double b = 0.0,
|
||||
double a = 0.0) override;
|
||||
// Creates a native shader through the dynamic backend.
|
||||
virtual Variant create_native_shader(ShaderCode code) override;
|
||||
// Destroys a native shader through the dynamic backend.
|
||||
virtual void destroy_native_shader(Variant shader) override;
|
||||
// Uploads CPU pixels to a backend texture.
|
||||
virtual void upload_to_texture(const Variant &handle,
|
||||
const VideoParams ¶ms, const void *data,
|
||||
int linesize) override;
|
||||
// Downloads backend texture pixels to CPU memory.
|
||||
virtual void download_from_texture(const Variant &handle,
|
||||
const VideoParams ¶ms, void *data,
|
||||
int linesize) override;
|
||||
// Waits for backend work to complete.
|
||||
virtual void flush() override;
|
||||
// Reads one pixel from a backend texture.
|
||||
virtual Color get_pixel_from_texture(Texture *texture,
|
||||
const PointF &pt) override;
|
||||
// Returns the wrapped OpenGL context for OpenGL backends.
|
||||
virtual OpenGLContext *open_gl_context() const override;
|
||||
|
||||
// Reports whether the effective backend is OpenGL.
|
||||
virtual bool is_open_gl() const override;
|
||||
// Reports whether the effective backend is Vulkan.
|
||||
virtual bool is_vulkan() const override;
|
||||
|
||||
// Attaches a texture for OFX OpenGL output when supported.
|
||||
virtual void attach_output_texture(Texture *texture) override;
|
||||
|
||||
// Detaches any OFX output texture binding when supported.
|
||||
virtual void detach_output_texture() override;
|
||||
|
||||
protected:
|
||||
// Dispatches a shader blit through the dynamic backend.
|
||||
virtual void blit(Variant shader, AcceleratedJob &job,
|
||||
Texture *destination, VideoParams destination_params,
|
||||
bool clear_destination) override;
|
||||
// Allocates a native texture through the dynamic backend.
|
||||
virtual Variant create_native_texture(int width, int height, int depth,
|
||||
PixelFormat format, int channel_count,
|
||||
const void *data = nullptr,
|
||||
int linesize = 0) override;
|
||||
// Releases a native texture through the dynamic backend.
|
||||
virtual void destroy_native_texture(Variant texture) override;
|
||||
// Releases backend-owned renderer resources.
|
||||
virtual void destroy_internal() override;
|
||||
|
||||
private:
|
||||
// Resolves required backend C ABI symbols.
|
||||
bool resolve_functions();
|
||||
// Replaces a failed Vulkan backend with OpenGL.
|
||||
bool fallback_to_open_gl();
|
||||
// Clears all cached function pointers.
|
||||
void reset_functions();
|
||||
// Resolves the private backend library path.
|
||||
std::string library_filename() const;
|
||||
|
||||
std::string backend_;
|
||||
DynLib library_;
|
||||
OakRenderBackendHandle handle_ = nullptr;
|
||||
|
||||
OakBackendCreateFn create_ = nullptr;
|
||||
OakBackendDestroyFn destroy_ = nullptr;
|
||||
OakBackendGetInfoFn get_info_ = nullptr;
|
||||
OakBackendIsAvailableFn is_available_ = nullptr;
|
||||
OakBackendInitFn init_ = nullptr;
|
||||
OakBackendInitWithContextFn init_with_context_ = nullptr;
|
||||
OakBackendPostInitFn post_init_ = nullptr;
|
||||
OakBackendPostDestroyFn post_destroy_ = nullptr;
|
||||
OakBackendDestroyInternalFn destroy_internal_ = nullptr;
|
||||
OakBackendClearDestinationFn clear_destination_ = nullptr;
|
||||
OakBackendCreateNativeTextureFn create_native_texture_ = nullptr;
|
||||
OakBackendDestroyNativeTextureFn destroy_native_texture_ = nullptr;
|
||||
OakBackendCreateNativeShaderFn create_native_shader_ = nullptr;
|
||||
OakBackendDestroyNativeShaderFn destroy_native_shader_ = nullptr;
|
||||
OakBackendUploadToTextureFn upload_to_texture_ = nullptr;
|
||||
OakBackendDownloadFromTextureFn download_from_texture_ = nullptr;
|
||||
OakBackendFlushFn flush_ = nullptr;
|
||||
OakBackendGetPixelFromTextureFn get_pixel_from_texture_ = nullptr;
|
||||
OakBackendBlitFn blit_ = nullptr;
|
||||
OakBackendAttachOutputTextureFn attach_output_texture_ = nullptr;
|
||||
OakBackendDetachOutputTextureFn detach_output_texture_ = nullptr;
|
||||
OakBackendOpenGLContextFn opengl_context_ = nullptr;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_DYNAMICRENDERER_H
|
||||
@@ -0,0 +1,122 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak 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 OAK_DYNLIB_H
|
||||
#define OAK_DYNLIB_H
|
||||
|
||||
// Minimal QLibrary replacement for loading render backend shared libraries.
|
||||
|
||||
#include <string>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <dlfcn.h>
|
||||
#endif
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class DynLib {
|
||||
public:
|
||||
DynLib() = default;
|
||||
|
||||
~DynLib()
|
||||
{
|
||||
unload();
|
||||
}
|
||||
|
||||
DynLib(const DynLib &) = delete;
|
||||
DynLib &operator=(const DynLib &) = delete;
|
||||
|
||||
void set_file_name(const std::string &path)
|
||||
{
|
||||
unload();
|
||||
path_ = path;
|
||||
}
|
||||
|
||||
const std::string &file_name() const
|
||||
{
|
||||
return path_;
|
||||
}
|
||||
|
||||
bool load()
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
handle_ = LoadLibraryA(path_.c_str());
|
||||
if (!handle_) {
|
||||
error_string_ = "LoadLibrary failed";
|
||||
}
|
||||
#else
|
||||
handle_ = dlopen(path_.c_str(), RTLD_NOW | RTLD_LOCAL);
|
||||
if (!handle_) {
|
||||
const char *err = dlerror();
|
||||
error_string_ = err ? err : "dlopen failed";
|
||||
}
|
||||
#endif
|
||||
return handle_ != nullptr;
|
||||
}
|
||||
|
||||
bool unload()
|
||||
{
|
||||
if (!handle_) {
|
||||
return true;
|
||||
}
|
||||
#if defined(_WIN32)
|
||||
const bool ok = FreeLibrary(HMODULE(handle_)) != 0;
|
||||
#else
|
||||
const bool ok = dlclose(handle_) == 0;
|
||||
#endif
|
||||
handle_ = nullptr;
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool is_loaded() const
|
||||
{
|
||||
return handle_ != nullptr;
|
||||
}
|
||||
|
||||
void *resolve(const char *symbol)
|
||||
{
|
||||
if (!handle_) {
|
||||
return nullptr;
|
||||
}
|
||||
#if defined(_WIN32)
|
||||
return reinterpret_cast<void *>(
|
||||
GetProcAddress(HMODULE(handle_), symbol));
|
||||
#else
|
||||
return dlsym(handle_, symbol);
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string error_string() const
|
||||
{
|
||||
return error_string_;
|
||||
}
|
||||
|
||||
private:
|
||||
std::string path_;
|
||||
void *handle_ = nullptr;
|
||||
std::string error_string_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_DYNLIB_H
|
||||
@@ -0,0 +1,121 @@
|
||||
#ifndef OAK_RENDERBACKEND_C_H
|
||||
#define OAK_RENDERBACKEND_C_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#define OAK_RENDER_BACKEND_EXPORT extern "C" __declspec(dllexport)
|
||||
#else
|
||||
#define OAK_RENDER_BACKEND_EXPORT \
|
||||
extern "C" __attribute__((visibility("default")))
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Opaque pointer to the backend-owned C++ renderer object. */
|
||||
typedef void *OakRenderBackendHandle;
|
||||
|
||||
/* Identifies the concrete backend behind a dynamically loaded library. */
|
||||
enum OakRenderBackendKind {
|
||||
oak_render_backend_unknown = 0,
|
||||
oak_render_backend_opengl = 1,
|
||||
oak_render_backend_vulkan = 2
|
||||
};
|
||||
|
||||
/* Capability bits advertised by a backend through oak_renderer_get_info(). */
|
||||
enum OakRenderBackendCapability {
|
||||
oak_render_backend_cap_textures = 1ULL << 0,
|
||||
oak_render_backend_cap_shaders = 1ULL << 1,
|
||||
oak_render_backend_cap_blit = 1ULL << 2,
|
||||
oak_render_backend_cap_readback = 1ULL << 3,
|
||||
oak_render_backend_cap_viewer_context = 1ULL << 4,
|
||||
oak_render_backend_cap_instance = 1ULL << 5,
|
||||
oak_render_backend_cap_device = 1ULL << 6
|
||||
};
|
||||
|
||||
/* Static and runtime metadata returned by the backend. */
|
||||
struct OakRenderBackendInfo {
|
||||
uint32_t abi_version;
|
||||
uint32_t kind;
|
||||
uint64_t capabilities;
|
||||
const char *name;
|
||||
const char *status;
|
||||
};
|
||||
|
||||
/* Creates a backend renderer object. */
|
||||
typedef OakRenderBackendHandle (*OakBackendCreateFn)(void *parent);
|
||||
/* Destroys a backend renderer object created by OakBackendCreateFn. */
|
||||
typedef void (*OakBackendDestroyFn)(OakRenderBackendHandle handle);
|
||||
/* Queries backend metadata and capability bits. */
|
||||
typedef bool (*OakBackendGetInfoFn)(OakRenderBackendHandle handle,
|
||||
struct OakRenderBackendInfo *out_info);
|
||||
/* Checks whether the backend can run on the current machine. */
|
||||
typedef bool (*OakBackendIsAvailableFn)(OakRenderBackendHandle handle);
|
||||
/* Initializes backend-owned device/context resources. */
|
||||
typedef bool (*OakBackendInitFn)(OakRenderBackendHandle handle);
|
||||
/* Initializes the backend against a caller-supplied GL context when applicable. */
|
||||
typedef void (*OakBackendInitWithContextFn)(OakRenderBackendHandle handle,
|
||||
void *context);
|
||||
/* Runs backend post-initialization after the device/context exists. */
|
||||
typedef void (*OakBackendPostInitFn)(OakRenderBackendHandle handle);
|
||||
/* Runs backend post-destroy cleanup before the library unloads. */
|
||||
typedef void (*OakBackendPostDestroyFn)(OakRenderBackendHandle handle);
|
||||
/* Destroys renderer-owned native resources. */
|
||||
typedef void (*OakBackendDestroyInternalFn)(OakRenderBackendHandle handle);
|
||||
/* Clears a texture destination or implicit output target. */
|
||||
typedef void (*OakBackendClearDestinationFn)(OakRenderBackendHandle handle,
|
||||
void *texture, double r, double g,
|
||||
double b, double a);
|
||||
/* Creates a native texture and writes a QVariant-compatible handle. */
|
||||
typedef void (*OakBackendCreateNativeTextureFn)(
|
||||
OakRenderBackendHandle handle, int width, int height, int depth, int format,
|
||||
int channel_count, const void *data, int linesize, void *out_variant);
|
||||
/* Destroys a native texture represented by a QVariant-compatible handle. */
|
||||
typedef void (*OakBackendDestroyNativeTextureFn)(OakRenderBackendHandle handle,
|
||||
const void *variant);
|
||||
/* Creates a native shader and writes a QVariant-compatible handle. */
|
||||
typedef void (*OakBackendCreateNativeShaderFn)(OakRenderBackendHandle handle,
|
||||
const void *shader_code,
|
||||
void *out_variant);
|
||||
/* Destroys a native shader represented by a QVariant-compatible handle. */
|
||||
typedef void (*OakBackendDestroyNativeShaderFn)(OakRenderBackendHandle handle,
|
||||
const void *variant);
|
||||
/* Uploads CPU pixel data to a native texture. */
|
||||
typedef void (*OakBackendUploadToTextureFn)(OakRenderBackendHandle handle,
|
||||
const void *variant,
|
||||
const void *video_params,
|
||||
const void *data, int linesize);
|
||||
/* Downloads native texture pixels into caller-owned CPU memory. */
|
||||
typedef void (*OakBackendDownloadFromTextureFn)(OakRenderBackendHandle handle,
|
||||
const void *variant,
|
||||
const void *video_params,
|
||||
void *data, int linesize);
|
||||
/* Waits for backend work that must be visible to later operations. */
|
||||
typedef void (*OakBackendFlushFn)(OakRenderBackendHandle handle);
|
||||
/* Reads one pixel from a texture. */
|
||||
typedef void (*OakBackendGetPixelFromTextureFn)(OakRenderBackendHandle handle,
|
||||
void *texture,
|
||||
const void *point,
|
||||
void *out_color);
|
||||
/* Executes a shader blit job. */
|
||||
typedef void (*OakBackendBlitFn)(OakRenderBackendHandle handle,
|
||||
const void *shader, void *job,
|
||||
void *destination,
|
||||
const void *destination_params,
|
||||
bool clear_destination);
|
||||
/* Attaches an output texture for OFX OpenGL rendering when supported. */
|
||||
typedef void (*OakBackendAttachOutputTextureFn)(OakRenderBackendHandle handle,
|
||||
const void *texture_id);
|
||||
/* Detaches an OFX output texture when supported. */
|
||||
typedef void (*OakBackendDetachOutputTextureFn)(OakRenderBackendHandle handle);
|
||||
/* Returns the backend OpenGL context, or null for non-OpenGL backends. */
|
||||
typedef void *(*OakBackendOpenGLContextFn)(OakRenderBackendHandle handle);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // OAK_RENDERBACKEND_C_H
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Oak Video Editor - Non-Linear Video Editor
|
||||
* Copyright (C) 2025 Olive CE 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 OAK_CANCELATOM_H
|
||||
#define OAK_CANCELATOM_H
|
||||
|
||||
#include <mutex>
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class CancelAtom {
|
||||
public:
|
||||
CancelAtom()
|
||||
: cancelled_(false)
|
||||
, heard_(false)
|
||||
{
|
||||
}
|
||||
|
||||
bool is_cancelled()
|
||||
{
|
||||
std::lock_guard<std::mutex> locker(mutex_);
|
||||
if (cancelled_) {
|
||||
heard_ = true;
|
||||
}
|
||||
return cancelled_;
|
||||
}
|
||||
|
||||
void cancel()
|
||||
{
|
||||
std::lock_guard<std::mutex> locker(mutex_);
|
||||
cancelled_ = true;
|
||||
}
|
||||
|
||||
bool heard_cancel()
|
||||
{
|
||||
std::lock_guard<std::mutex> locker(mutex_);
|
||||
return heard_;
|
||||
}
|
||||
|
||||
private:
|
||||
std::mutex mutex_;
|
||||
|
||||
bool cancelled_;
|
||||
|
||||
bool heard_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_CANCELATOM_H
|
||||
@@ -0,0 +1,247 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "renderer.h"
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
#include "filefunctions.h"
|
||||
#include "node.h"
|
||||
#include "render/colorprocessor.h"
|
||||
#include "render/job/colortransformjob.h"
|
||||
#include "render/job/shaderjob.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
// Replaces the first "%1" marker, mirroring the QString::arg() call the
|
||||
// shader template substitution below used before de-Qt.
|
||||
std::string arg1(const std::string &fmt, const std::string &arg)
|
||||
{
|
||||
std::string result = fmt;
|
||||
std::string::size_type pos = result.find("%1");
|
||||
if (pos != std::string::npos) {
|
||||
result.replace(pos, 2, arg);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
bool Renderer::get_color_context(const ColorTransformJob &color_job,
|
||||
Renderer::ColorContext *ctx)
|
||||
{
|
||||
std::unique_lock<std::mutex> locker(color_cache_mutex_);
|
||||
|
||||
ColorContext &color_ctx = *ctx;
|
||||
|
||||
std::string proc_id = color_job.id();
|
||||
|
||||
if (color_cache_.count(proc_id)) {
|
||||
color_ctx = color_cache_.at(proc_id);
|
||||
return true;
|
||||
} else {
|
||||
locker.unlock();
|
||||
|
||||
// Create shader description
|
||||
std::string ocio_func_name;
|
||||
if (color_job.get_function_name().empty()) {
|
||||
ocio_func_name = "OCIODisplay";
|
||||
} else {
|
||||
ocio_func_name = color_job.get_function_name();
|
||||
}
|
||||
auto shader_desc = ocio::GpuShaderDesc::CreateShaderDesc();
|
||||
shader_desc->setLanguage(ocio::GPU_LANGUAGE_GLSL_ES_3_0);
|
||||
shader_desc->setFunctionName(ocio_func_name.c_str());
|
||||
shader_desc->setResourcePrefix("ocio_");
|
||||
|
||||
// Generate shader
|
||||
color_job.get_color_processor()
|
||||
->get_processor()
|
||||
->getDefaultGPUProcessor()
|
||||
->extractGpuShaderInfo(shader_desc);
|
||||
|
||||
ShaderCode code;
|
||||
if (const Node *shader_src = color_job.custom_shader_source()) {
|
||||
// Use shader code from associated node
|
||||
code = shader_src->get_shader_code(
|
||||
{ color_job.custom_shader_id(), shader_desc->getShaderText() });
|
||||
} else {
|
||||
// Generate shader code using OCIO stub and our auto-generated name
|
||||
code = FileFunctions::read_file_as_string(
|
||||
":/shaders/colormanage.frag");
|
||||
code.set_frag_code(
|
||||
arg1(code.frag_code(), shader_desc->getShaderText()));
|
||||
}
|
||||
|
||||
// Try to compile shader
|
||||
color_ctx.compiled_shader = create_native_shader(code);
|
||||
|
||||
if (color_ctx.compiled_shader.is_null()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
color_ctx.lut3d_textures.resize(shader_desc->getNum3DTextures());
|
||||
for (unsigned int i = 0; i < shader_desc->getNum3DTextures(); i++) {
|
||||
const char *tex_name = nullptr;
|
||||
const char *sampler_name = nullptr;
|
||||
unsigned int edge_len = 0;
|
||||
ocio::Interpolation interpolation = ocio::INTERP_LINEAR;
|
||||
|
||||
shader_desc->get3DTexture(i, tex_name, sampler_name, edge_len,
|
||||
interpolation);
|
||||
|
||||
if (!tex_name || !*tex_name || !sampler_name || !*sampler_name ||
|
||||
!edge_len) {
|
||||
fprintf(stderr, "3D LUT texture data is corrupted\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
const float *values = nullptr;
|
||||
shader_desc->get3DTextureValues(i, values);
|
||||
if (!values) {
|
||||
fprintf(stderr, "3D LUT texture values are missing\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allocate 3D LUT
|
||||
color_ctx.lut3d_textures[i].texture = create_texture(
|
||||
VideoParams(edge_len, edge_len, edge_len, PixelFormat::f32,
|
||||
VideoParams::k_rgb_channel_count),
|
||||
values);
|
||||
color_ctx.lut3d_textures[i].name = sampler_name;
|
||||
color_ctx.lut3d_textures[i].interpolation =
|
||||
(interpolation == ocio::INTERP_NEAREST) ? Texture::k_nearest :
|
||||
Texture::k_linear;
|
||||
}
|
||||
|
||||
color_ctx.lut1d_textures.resize(shader_desc->getNumTextures());
|
||||
for (unsigned int i = 0; i < shader_desc->getNumTextures(); i++) {
|
||||
const char *tex_name = nullptr;
|
||||
const char *sampler_name = nullptr;
|
||||
unsigned int width = 0, height = 0;
|
||||
ocio::GpuShaderDesc::TextureType channel =
|
||||
ocio::GpuShaderDesc::TEXTURE_RGB_CHANNEL;
|
||||
ocio::Interpolation interpolation = ocio::INTERP_LINEAR;
|
||||
#if OCIO_VERSION_MAJOR > 2 || \
|
||||
(OCIO_VERSION_MAJOR == 2 && OCIO_VERSION_MINOR >= 3)
|
||||
ocio::GpuShaderDesc::TextureDimensions dimensions =
|
||||
ocio::GpuShaderDesc::TEXTURE_2D;
|
||||
shader_desc->getTexture(i, tex_name, sampler_name, width, height,
|
||||
channel, dimensions, interpolation);
|
||||
#else
|
||||
shader_desc->getTexture(i, tex_name, sampler_name, width, height,
|
||||
channel, interpolation);
|
||||
#endif
|
||||
|
||||
if (!tex_name || !*tex_name || !sampler_name || !*sampler_name ||
|
||||
!width) {
|
||||
fprintf(stderr, "1D LUT texture data is corrupted\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
const float *values = nullptr;
|
||||
shader_desc->getTextureValues(i, values);
|
||||
if (!values) {
|
||||
fprintf(stderr, "1D LUT texture values are missing\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allocate 1D LUT
|
||||
int lut_channels =
|
||||
(channel == ocio::GpuShaderDesc::TEXTURE_RED_CHANNEL) ?
|
||||
1 :
|
||||
VideoParams::k_rgb_channel_count;
|
||||
VideoParams lut_params(width, height, PixelFormat::f32,
|
||||
lut_channels);
|
||||
color_ctx.lut1d_textures[i].texture =
|
||||
create_texture(lut_params, values);
|
||||
color_ctx.lut1d_textures[i].name = sampler_name;
|
||||
color_ctx.lut1d_textures[i].interpolation =
|
||||
(interpolation == ocio::INTERP_NEAREST) ? Texture::k_nearest :
|
||||
Texture::k_linear;
|
||||
}
|
||||
|
||||
locker.lock();
|
||||
color_cache_.insert({ proc_id, color_ctx });
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void Renderer::blit_color_managed(const ColorTransformJob &color_job,
|
||||
Texture *destination, const VideoParams ¶ms)
|
||||
{
|
||||
ColorContext color_ctx;
|
||||
if (!get_color_context(color_job, &color_ctx)) {
|
||||
ShaderJob fallback_job;
|
||||
fallback_job.insert("ove_maintex",
|
||||
color_job.get_input_texture());
|
||||
fallback_job.insert("ove_mvpmat",
|
||||
NodeValue(NodeValue::k_matrix,
|
||||
color_job.get_transform_matrix()));
|
||||
|
||||
if (destination) {
|
||||
blit_to_texture(get_default_shader(), fallback_job, destination,
|
||||
color_job.is_clear_destination_enabled());
|
||||
} else {
|
||||
blit(get_default_shader(), fallback_job, params,
|
||||
color_job.is_clear_destination_enabled());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
ShaderJob job;
|
||||
job.insert("ove_maintex", color_job.get_input_texture());
|
||||
job.insert("ove_mvpmat",
|
||||
NodeValue(NodeValue::k_matrix, color_job.get_transform_matrix()));
|
||||
job.insert("ove_cropmatrix",
|
||||
NodeValue(NodeValue::k_matrix,
|
||||
color_job.get_crop_matrix().inverted()));
|
||||
job.insert("ove_maintex_alpha",
|
||||
NodeValue(NodeValue::k_int,
|
||||
int(color_job.get_input_alpha_association())));
|
||||
job.insert("ove_force_opaque",
|
||||
NodeValue(NodeValue::k_boolean, color_job.get_force_opaque()));
|
||||
job.insert(color_job.get_values());
|
||||
|
||||
for (const ColorContext::LUT &l : color_ctx.lut3d_textures) {
|
||||
job.insert(l.name, NodeValue(NodeValue::k_texture,
|
||||
Variant::from_value(l.texture)));
|
||||
job.set_interpolation(l.name, l.interpolation);
|
||||
}
|
||||
for (const ColorContext::LUT &l : color_ctx.lut1d_textures) {
|
||||
job.insert(l.name, NodeValue(NodeValue::k_texture,
|
||||
Variant::from_value(l.texture)));
|
||||
job.set_interpolation(l.name, l.interpolation);
|
||||
}
|
||||
|
||||
if (destination) {
|
||||
blit_to_texture(color_ctx.compiled_shader, job, destination,
|
||||
color_job.is_clear_destination_enabled());
|
||||
} else {
|
||||
blit(color_ctx.compiled_shader, job, params,
|
||||
color_job.is_clear_destination_enabled());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "colorprocessor.h"
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
#include "color/colormanager/colormanager.h"
|
||||
#include "define.h"
|
||||
#include "ocioutils.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
ColorProcessor::ColorProcessor(ColorManager *config, const std::string &input,
|
||||
const ColorTransform &transform,
|
||||
Direction direction)
|
||||
{
|
||||
processor_ = nullptr;
|
||||
cpu_processor_ = nullptr;
|
||||
|
||||
try {
|
||||
// Resolve role names (e.g. "scene_linear") to canonical colorspace names
|
||||
// so they can be passed to getProcessor()/DisplayViewTransform.
|
||||
std::string resolved_input = input;
|
||||
ocio::ConstConfigRcPtr ocio_config = config->get_config();
|
||||
if (ocio_config && ocio_config->hasRole(input.c_str())) {
|
||||
resolved_input = ocio_config->getCanonicalName(input.c_str());
|
||||
}
|
||||
|
||||
const std::string &output = (transform.output().empty()) ?
|
||||
config->get_default_display() :
|
||||
transform.output();
|
||||
|
||||
if (transform.is_display()) {
|
||||
const std::string &view = (transform.view().empty()) ?
|
||||
config->get_default_view(output) :
|
||||
transform.view();
|
||||
|
||||
auto display_transform = ocio::DisplayViewTransform::Create();
|
||||
|
||||
display_transform->setSrc(resolved_input.c_str());
|
||||
display_transform->setDisplay(output.c_str());
|
||||
display_transform->setView(view.c_str());
|
||||
display_transform->setDirection(direction == k_normal ?
|
||||
ocio::TRANSFORM_DIR_FORWARD :
|
||||
ocio::TRANSFORM_DIR_INVERSE);
|
||||
|
||||
if (transform.look().empty()) {
|
||||
processor_ = ocio_config->getProcessor(display_transform);
|
||||
} else {
|
||||
auto group = ocio::GroupTransform::Create();
|
||||
|
||||
const char *out_cs =
|
||||
ocio::LookTransform::GetLooksResultColorSpace(
|
||||
ocio_config, ocio_config->getCurrentContext(),
|
||||
transform.look().c_str());
|
||||
|
||||
auto lt = ocio::LookTransform::Create();
|
||||
lt->setSrc(resolved_input.c_str());
|
||||
lt->setDst(out_cs);
|
||||
lt->setLooks(transform.look().c_str());
|
||||
lt->setSkipColorSpaceConversion(false);
|
||||
group->appendTransform(lt);
|
||||
|
||||
display_transform->setSrc(out_cs);
|
||||
group->appendTransform(display_transform);
|
||||
|
||||
processor_ = ocio_config->getProcessor(group);
|
||||
}
|
||||
|
||||
} else {
|
||||
if (direction == k_normal) {
|
||||
processor_ = ocio_config->getProcessor(resolved_input.c_str(),
|
||||
output.c_str());
|
||||
} else {
|
||||
processor_ = ocio_config->getProcessor(output.c_str(),
|
||||
resolved_input.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
if (processor_) {
|
||||
cpu_processor_ = processor_->getDefaultCPUProcessor();
|
||||
}
|
||||
} catch (ocio::Exception &e) {
|
||||
fprintf(stderr, "ColorProcessor exception: %s\n", e.what());
|
||||
}
|
||||
}
|
||||
|
||||
ColorProcessor::ColorProcessor(ocio::ConstProcessorRcPtr processor)
|
||||
{
|
||||
processor_ = processor;
|
||||
cpu_processor_ = processor_ ? processor_->getDefaultCPUProcessor() :
|
||||
nullptr;
|
||||
}
|
||||
|
||||
void ColorProcessor::convert_frame(Frame *f)
|
||||
{
|
||||
if (!cpu_processor_) {
|
||||
return;
|
||||
}
|
||||
|
||||
ocio::BitDepth ocio_bit_depth =
|
||||
OCIOUtils::get_ocio_bit_depth_from_pixel_format(f->format());
|
||||
|
||||
if (ocio_bit_depth == ocio::BIT_DEPTH_UNKNOWN) {
|
||||
fprintf(stderr, "Tried to color convert frame with no format\n");
|
||||
return;
|
||||
}
|
||||
|
||||
ocio::PackedImageDesc img(f->data(), f->width(), f->height(),
|
||||
f->channel_count(), ocio_bit_depth,
|
||||
ocio::AutoStride, ocio::AutoStride,
|
||||
f->linesize_bytes());
|
||||
|
||||
cpu_processor_->apply(img);
|
||||
}
|
||||
|
||||
Color ColorProcessor::convert_color(const Color &in)
|
||||
{
|
||||
if (!cpu_processor_) {
|
||||
return in;
|
||||
}
|
||||
|
||||
// I've been bamboozled
|
||||
float c[4] = { float(in.red()), float(in.green()), float(in.blue()),
|
||||
float(in.alpha()) };
|
||||
|
||||
cpu_processor_->applyRGBA(c);
|
||||
|
||||
return Color(c[0], c[1], c[2], c[3]);
|
||||
}
|
||||
|
||||
ColorProcessorPtr ColorProcessor::create(ColorManager *config,
|
||||
const std::string &input,
|
||||
const ColorTransform &transform,
|
||||
Direction direction)
|
||||
{
|
||||
return std::make_shared<ColorProcessor>(config, input, transform,
|
||||
direction);
|
||||
}
|
||||
|
||||
ColorProcessorPtr ColorProcessor::create(ocio::ConstProcessorRcPtr processor)
|
||||
{
|
||||
return std::make_shared<ColorProcessor>(processor);
|
||||
}
|
||||
|
||||
ocio::ConstProcessorRcPtr ColorProcessor::get_processor()
|
||||
{
|
||||
return processor_;
|
||||
}
|
||||
|
||||
void ColorProcessor::convert_frame(FramePtr f)
|
||||
{
|
||||
convert_frame(f.get());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_COLORPROCESSOR_H
|
||||
#define OAK_COLORPROCESSOR_H
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "codec/frame.h"
|
||||
#include "colortransform.h"
|
||||
#include "define.h"
|
||||
#include "ocioutils.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class ColorManager;
|
||||
|
||||
class ColorProcessor;
|
||||
using ColorProcessorPtr = std::shared_ptr<ColorProcessor>;
|
||||
|
||||
class ColorProcessor {
|
||||
public:
|
||||
enum Direction { k_normal, k_inverse };
|
||||
|
||||
ColorProcessor(ColorManager *config, const std::string &input,
|
||||
const ColorTransform &dest_space,
|
||||
Direction direction = k_normal);
|
||||
ColorProcessor(ocio::ConstProcessorRcPtr processor);
|
||||
|
||||
DISABLE_COPY_MOVE(ColorProcessor)
|
||||
|
||||
static ColorProcessorPtr create(ColorManager *config,
|
||||
const std::string &input,
|
||||
const ColorTransform &dest_space,
|
||||
Direction direction = k_normal);
|
||||
static ColorProcessorPtr create(ocio::ConstProcessorRcPtr processor);
|
||||
|
||||
ocio::ConstProcessorRcPtr get_processor();
|
||||
|
||||
void convert_frame(FramePtr f);
|
||||
void convert_frame(Frame *f);
|
||||
|
||||
Color convert_color(const Color &in);
|
||||
|
||||
const char *id() const
|
||||
{
|
||||
return processor_->getCacheID();
|
||||
}
|
||||
|
||||
private:
|
||||
ocio::ConstProcessorRcPtr processor_;
|
||||
|
||||
ocio::ConstCPUProcessorRcPtr cpu_processor_;
|
||||
};
|
||||
|
||||
using ColorProcessorChain = std::vector<ColorProcessorPtr>;
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_COLORPROCESSOR_H
|
||||
@@ -0,0 +1,37 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_COLORPROCESSORCACHE_H
|
||||
#define OAK_COLORPROCESSORCACHE_H
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
#include "colorprocessor.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
using ColorProcessorCache = std::map<std::string, ColorProcessorPtr>;
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_COLORPROCESSORCACHE_H
|
||||
@@ -0,0 +1,561 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "diskmanager.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
#include "config/config.h"
|
||||
#include "coreengine.h"
|
||||
#include "filefunctions.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
DiskManager *DiskManager::instance_ = nullptr;
|
||||
|
||||
DiskManager::ShowDiskCacheSettingsHandler
|
||||
DiskManager::show_disk_cache_settings_handler_;
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
int64_t current_msecs_since_epoch()
|
||||
{
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch())
|
||||
.count();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
DiskManager::DiskManager()
|
||||
{
|
||||
// Add default cache location
|
||||
std::ifstream default_disk_cache_file(get_default_disk_cache_config_file(),
|
||||
std::ios::binary);
|
||||
if (default_disk_cache_file.is_open()) {
|
||||
std::stringstream ss;
|
||||
ss << default_disk_cache_file.rdbuf();
|
||||
std::string default_dir = ss.str();
|
||||
|
||||
if (!default_dir.empty()) {
|
||||
if (FileFunctions::directory_is_valid(default_dir)) {
|
||||
get_open_folder(default_dir);
|
||||
} else {
|
||||
// The UI warning (QMessageBox) moved to the app layer; the
|
||||
// engine falls back to the default cache location.
|
||||
fprintf(stderr,
|
||||
"Disk Cache Error: Unable to set custom application disk "
|
||||
"cache. Using default instead.\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no custom default was loaded, load default
|
||||
if (open_folders_.empty()) {
|
||||
get_open_folder(get_default_disk_cache_path());
|
||||
}
|
||||
|
||||
std::string disk_cache_index_path =
|
||||
(std::filesystem::path(FileFunctions::get_configuration_location()) /
|
||||
"diskcache2")
|
||||
.string();
|
||||
|
||||
std::ifstream disk_cache_index(disk_cache_index_path);
|
||||
if (disk_cache_index.is_open()) {
|
||||
std::string line;
|
||||
while (std::getline(disk_cache_index, line)) {
|
||||
get_open_folder(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DiskManager::~DiskManager()
|
||||
{
|
||||
std::ofstream default_disk_cache_file(get_default_disk_cache_config_file(),
|
||||
std::ios::binary | std::ios::trunc);
|
||||
if (default_disk_cache_file.is_open()) {
|
||||
if (get_default_disk_cache_path() != get_default_cache_path()) {
|
||||
default_disk_cache_file << get_default_cache_path();
|
||||
}
|
||||
}
|
||||
|
||||
// DiskCacheFolder children used to be deleted via QObject parentship
|
||||
for (DiskCacheFolder *f : open_folders_) {
|
||||
delete f;
|
||||
}
|
||||
open_folders_.clear();
|
||||
}
|
||||
|
||||
void DiskManager::create_instance()
|
||||
{
|
||||
instance_ = new DiskManager();
|
||||
}
|
||||
|
||||
void DiskManager::destroy_instance()
|
||||
{
|
||||
delete instance_;
|
||||
instance_ = nullptr;
|
||||
}
|
||||
|
||||
DiskManager *DiskManager::instance()
|
||||
{
|
||||
// Lazy self-create: the Qt app called create_instance() at startup, but
|
||||
// library consumers (oaknode standalone tests) may reach instance()
|
||||
// without any facade having run. Matches FrameManager callers' tolerance
|
||||
// for a missing instance by guaranteeing one exists instead.
|
||||
if (!instance_) {
|
||||
create_instance();
|
||||
}
|
||||
return instance_;
|
||||
}
|
||||
|
||||
void DiskManager::accessed(const std::string &cache_folder,
|
||||
const std::string &filename)
|
||||
{
|
||||
DiskCacheFolder *f = get_open_folder(cache_folder);
|
||||
|
||||
f->accessed(filename);
|
||||
}
|
||||
|
||||
void DiskManager::created_file(const std::string &cache_folder,
|
||||
const std::string &filename)
|
||||
{
|
||||
DiskCacheFolder *f = get_open_folder(cache_folder);
|
||||
|
||||
f->created_file(filename);
|
||||
}
|
||||
|
||||
void DiskManager::delete_specific_file(const std::string &filename)
|
||||
{
|
||||
for (DiskCacheFolder *f : open_folders_) {
|
||||
f->delete_specific_file(filename);
|
||||
}
|
||||
}
|
||||
|
||||
bool DiskManager::clear_disk_cache(const std::string &cache_folder)
|
||||
{
|
||||
DiskCacheFolder *f = get_open_folder(cache_folder);
|
||||
|
||||
return f->clear_cache();
|
||||
}
|
||||
|
||||
DiskCacheFolder *DiskManager::get_open_folder(const std::string &path)
|
||||
{
|
||||
// If path is empty, this must mean default
|
||||
if (path.empty()) {
|
||||
return get_default_cache_folder();
|
||||
}
|
||||
|
||||
// See if we have an existing path with this name
|
||||
for (DiskCacheFolder *f : open_folders_) {
|
||||
if (f->get_path() == path) {
|
||||
return f;
|
||||
}
|
||||
}
|
||||
|
||||
// We must have to open this folder
|
||||
DiskCacheFolder *f = new DiskCacheFolder(path);
|
||||
f->add_deleted_frame_handler(
|
||||
[this](const std::string &p, const std::string &fn) {
|
||||
emit_deleted_frame(p, fn);
|
||||
});
|
||||
open_folders_.push_back(f);
|
||||
|
||||
return f;
|
||||
}
|
||||
|
||||
std::string DiskManager::get_default_disk_cache_config_file()
|
||||
{
|
||||
return (std::filesystem::path(FileFunctions::get_configuration_location()) /
|
||||
"defaultdiskcache")
|
||||
.string();
|
||||
}
|
||||
|
||||
std::string DiskManager::get_default_disk_cache_path()
|
||||
{
|
||||
// QStandardPaths::AppLocalDataLocation equivalent: the configuration
|
||||
// location is the app data root on all platforms.
|
||||
return (std::filesystem::path(FileFunctions::get_configuration_location()) /
|
||||
"mediacache")
|
||||
.string();
|
||||
}
|
||||
|
||||
void DiskManager::set_show_disk_cache_settings_handler(
|
||||
ShowDiskCacheSettingsHandler handler)
|
||||
{
|
||||
show_disk_cache_settings_handler_ = std::move(handler);
|
||||
}
|
||||
|
||||
void DiskManager::show_disk_cache_settings_dialog(DiskCacheFolder *folder)
|
||||
{
|
||||
if (show_disk_cache_settings_handler_) {
|
||||
show_disk_cache_settings_handler_(folder);
|
||||
return;
|
||||
}
|
||||
|
||||
fprintf(stderr,
|
||||
"No disk cache settings dialog handler registered, skipping\n");
|
||||
}
|
||||
|
||||
void DiskManager::show_disk_cache_settings_dialog(const std::string &path)
|
||||
{
|
||||
if (!FileFunctions::directory_is_valid(path)) {
|
||||
// The UI error dialog (QMessageBox) moved to the app layer
|
||||
fprintf(stderr,
|
||||
"Disk Cache Error: Failed to open disk cache at \"%s\". Try a "
|
||||
"different folder.\n",
|
||||
path.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
DiskCacheFolder *folder = get_open_folder(path);
|
||||
|
||||
show_disk_cache_settings_dialog(folder);
|
||||
}
|
||||
|
||||
size_t DiskManager::add_deleted_frame_handler(DeletedFrameHandler handler)
|
||||
{
|
||||
size_t id = next_handler_id_++;
|
||||
deleted_frame_handlers_[id] = std::move(handler);
|
||||
return id;
|
||||
}
|
||||
|
||||
void DiskManager::remove_deleted_frame_handler(size_t id)
|
||||
{
|
||||
deleted_frame_handlers_.erase(id);
|
||||
}
|
||||
|
||||
size_t DiskManager::add_invalidate_project_handler(
|
||||
InvalidateProjectHandler handler)
|
||||
{
|
||||
size_t id = next_handler_id_++;
|
||||
invalidate_project_handlers_[id] = std::move(handler);
|
||||
return id;
|
||||
}
|
||||
|
||||
void DiskManager::remove_invalidate_project_handler(size_t id)
|
||||
{
|
||||
invalidate_project_handlers_.erase(id);
|
||||
}
|
||||
|
||||
void DiskManager::emit_deleted_frame(const std::string &path,
|
||||
const std::string &filename)
|
||||
{
|
||||
for (const auto &e : deleted_frame_handlers_) {
|
||||
e.second(path, filename);
|
||||
}
|
||||
}
|
||||
|
||||
void DiskManager::emit_invalidate_project(Project *p)
|
||||
{
|
||||
for (const auto &e : invalidate_project_handlers_) {
|
||||
e.second(p);
|
||||
}
|
||||
}
|
||||
|
||||
DiskCacheFolder::DiskCacheFolder(const std::string &path)
|
||||
{
|
||||
set_path(path);
|
||||
|
||||
// QTimer replacement: periodic index save on a background thread. The
|
||||
// timer used to fire in DiskManager's (GUI) thread; cross-thread callers
|
||||
// reached the folder through queued QMetaObject invocations.
|
||||
int interval = OAK_CONFIG("DiskCacheSaveInterval").toInt();
|
||||
if (interval <= 0) {
|
||||
// Config default (10000 ms); the transition config stub returns 0
|
||||
interval = 10000;
|
||||
}
|
||||
save_thread_stop_ = false;
|
||||
save_thread_ = std::thread([this, interval]() {
|
||||
int64_t elapsed = 0;
|
||||
while (!save_thread_stop_) {
|
||||
int64_t step = std::min<int64_t>(50, interval - elapsed);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(step));
|
||||
if (save_thread_stop_) {
|
||||
break;
|
||||
}
|
||||
elapsed += step;
|
||||
if (elapsed >= interval) {
|
||||
elapsed = 0;
|
||||
save_disk_cache_index();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
DiskCacheFolder::~DiskCacheFolder()
|
||||
{
|
||||
save_thread_stop_ = true;
|
||||
if (save_thread_.joinable()) {
|
||||
save_thread_.join();
|
||||
}
|
||||
|
||||
close_cache_folder();
|
||||
}
|
||||
|
||||
bool DiskCacheFolder::clear_cache()
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(data_mutex_);
|
||||
|
||||
bool deleted_files = true;
|
||||
|
||||
auto i = disk_data_.begin();
|
||||
|
||||
while (i != disk_data_.end()) {
|
||||
// We return a false result if any of the files fail to delete, but still try to delete as many as we can
|
||||
std::string filename = i->first;
|
||||
|
||||
std::error_code ec;
|
||||
bool removed = std::filesystem::remove(filename, ec);
|
||||
std::error_code ec2;
|
||||
if (removed || !std::filesystem::exists(filename, ec2)) {
|
||||
emit_deleted_frame(path_, filename);
|
||||
i = disk_data_.erase(i);
|
||||
} else {
|
||||
fprintf(stderr, "Failed to delete %s\n", filename.c_str());
|
||||
deleted_files = false;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
return deleted_files;
|
||||
}
|
||||
|
||||
void DiskCacheFolder::accessed(const std::string &filename)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(data_mutex_);
|
||||
|
||||
if (!disk_data_.count(filename)) {
|
||||
return;
|
||||
}
|
||||
|
||||
disk_data_[filename].access_time = current_msecs_since_epoch();
|
||||
}
|
||||
|
||||
void DiskCacheFolder::created_file(const std::string &filename)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(data_mutex_);
|
||||
|
||||
std::error_code ec;
|
||||
int64_t file_size = int64_t(std::filesystem::file_size(filename, ec));
|
||||
if (ec) {
|
||||
file_size = 0;
|
||||
}
|
||||
|
||||
disk_data_.insert({ filename, { file_size, current_msecs_since_epoch() } });
|
||||
|
||||
consumption_ += file_size;
|
||||
|
||||
while (consumption_ > limit_) {
|
||||
delete_least_recent();
|
||||
}
|
||||
}
|
||||
|
||||
void DiskCacheFolder::set_path(const std::string &path)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(data_mutex_);
|
||||
|
||||
// If this is currently set to a folder, close it out now
|
||||
close_cache_folder();
|
||||
|
||||
// Signal that disk cache is gone
|
||||
if (!disk_data_.empty()) {
|
||||
for (auto it = disk_data_.cbegin(); it != disk_data_.cend(); it++) {
|
||||
emit_deleted_frame(path_, it->first);
|
||||
}
|
||||
disk_data_.clear();
|
||||
}
|
||||
|
||||
// Set defaults
|
||||
clear_on_close_ = false;
|
||||
consumption_ = 0;
|
||||
limit_ = 21474836480; // Default to 20 GB
|
||||
|
||||
// Set path
|
||||
path_ = path;
|
||||
|
||||
// Attempt to load existing index file from path
|
||||
FileFunctions::directory_is_valid(path_);
|
||||
|
||||
index_path_ = (std::filesystem::path(path_) / "index").string();
|
||||
|
||||
// Try to load any current cache index from file
|
||||
std::FILE *cache_index_file = std::fopen(index_path_.c_str(), "rb");
|
||||
|
||||
if (cache_index_file) {
|
||||
BinaryStreamReader ds(cache_index_file);
|
||||
|
||||
ds >> limit_;
|
||||
ds >> clear_on_close_;
|
||||
|
||||
while (!ds.at_end()) {
|
||||
std::string filename;
|
||||
HashTime h;
|
||||
|
||||
ds >> filename;
|
||||
ds >> h.file_size;
|
||||
ds >> h.access_time;
|
||||
|
||||
std::error_code ec;
|
||||
if (std::filesystem::exists(filename, ec)) {
|
||||
consumption_ += h.file_size;
|
||||
disk_data_.insert({ filename, h });
|
||||
}
|
||||
}
|
||||
|
||||
std::fclose(cache_index_file);
|
||||
}
|
||||
}
|
||||
|
||||
bool DiskCacheFolder::delete_file_internal(
|
||||
std::map<std::string, HashTime>::iterator hash_to_delete)
|
||||
{
|
||||
// Cache HashTime object
|
||||
std::string filename = hash_to_delete->first;
|
||||
HashTime ht = hash_to_delete->second;
|
||||
|
||||
// Remove from disk
|
||||
std::error_code ec;
|
||||
bool removed = std::filesystem::remove(filename, ec);
|
||||
std::error_code ec2;
|
||||
bool exists = std::filesystem::exists(filename, ec2);
|
||||
|
||||
if (!exists || removed) {
|
||||
// Remove from internal map
|
||||
disk_data_.erase(hash_to_delete);
|
||||
|
||||
// Reduce consumption
|
||||
consumption_ -= ht.file_size;
|
||||
|
||||
emit_deleted_frame(path_, filename);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool DiskCacheFolder::delete_specific_file(const std::string &f)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(data_mutex_);
|
||||
|
||||
for (auto it = disk_data_.begin(); it != disk_data_.end(); it++) {
|
||||
if (it->first == f) {
|
||||
// Break out of this loop, assuming we'll only have one instance_ of each filename
|
||||
return delete_file_internal(it);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool DiskCacheFolder::delete_least_recent()
|
||||
{
|
||||
auto hash_to_delete = disk_data_.begin();
|
||||
|
||||
if (disk_data_.begin() != disk_data_.end()) {
|
||||
for (auto it = std::next(disk_data_.begin()); it != disk_data_.end(); it++) {
|
||||
if (it->second.access_time < hash_to_delete->second.access_time) {
|
||||
hash_to_delete = it;
|
||||
}
|
||||
}
|
||||
|
||||
bool e = delete_file_internal(hash_to_delete);
|
||||
|
||||
if (e) {
|
||||
EngineCore::instance()->warn_cache_full();
|
||||
}
|
||||
|
||||
return e;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void DiskCacheFolder::close_cache_folder()
|
||||
{
|
||||
if (path_.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (clear_on_close_) {
|
||||
// If we're not moving to new and we're set to clear on close, clear now or else it'll never
|
||||
// get cleared later
|
||||
clear_cache();
|
||||
}
|
||||
|
||||
// Save current cache index
|
||||
save_disk_cache_index();
|
||||
}
|
||||
|
||||
void DiskCacheFolder::save_disk_cache_index()
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(data_mutex_);
|
||||
|
||||
std::FILE *cache_index_file = std::fopen(index_path_.c_str(), "wb");
|
||||
|
||||
if (cache_index_file) {
|
||||
BinaryStreamWriter ds(cache_index_file);
|
||||
|
||||
ds << limit_;
|
||||
ds << clear_on_close_;
|
||||
|
||||
for (auto it = disk_data_.cbegin(); it != disk_data_.cend(); it++) {
|
||||
const HashTime &ht = it->second;
|
||||
|
||||
ds << it->first;
|
||||
ds << ht.file_size;
|
||||
ds << ht.access_time;
|
||||
}
|
||||
|
||||
std::fclose(cache_index_file);
|
||||
} else {
|
||||
fprintf(stderr, "Failed to write cache index: %s\n", index_path_.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
size_t DiskCacheFolder::add_deleted_frame_handler(DeletedFrameHandler handler)
|
||||
{
|
||||
size_t id = next_handler_id_++;
|
||||
deleted_frame_handlers_[id] = std::move(handler);
|
||||
return id;
|
||||
}
|
||||
|
||||
void DiskCacheFolder::remove_deleted_frame_handler(size_t id)
|
||||
{
|
||||
deleted_frame_handlers_.erase(id);
|
||||
}
|
||||
|
||||
void DiskCacheFolder::emit_deleted_frame(const std::string &path,
|
||||
const std::string &filename)
|
||||
{
|
||||
for (const auto &e : deleted_frame_handlers_) {
|
||||
e.second(path, filename);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_DISKMANAGER_H
|
||||
#define OAK_DISKMANAGER_H
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "binarystream.h"
|
||||
#include "define.h"
|
||||
#include "project.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class DiskCacheFolder {
|
||||
public:
|
||||
DiskCacheFolder(const std::string &path);
|
||||
|
||||
~DiskCacheFolder();
|
||||
|
||||
bool clear_cache();
|
||||
|
||||
void accessed(const std::string &filename);
|
||||
|
||||
void created_file(const std::string &filename);
|
||||
|
||||
const std::string &get_path() const
|
||||
{
|
||||
return path_;
|
||||
}
|
||||
|
||||
void set_path(const std::string &path);
|
||||
|
||||
int64_t get_limit() const
|
||||
{
|
||||
return limit_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Bytes currently consumed by tracked files in this folder.
|
||||
*
|
||||
* Non-const because the counter is guarded by the (non-mutable)
|
||||
* data mutex. Exposed for the oakrender C ABI
|
||||
* (oakrender_disk_cache_size(), M7 §2.4).
|
||||
*/
|
||||
int64_t get_consumption()
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> locker(data_mutex_);
|
||||
return consumption_;
|
||||
}
|
||||
|
||||
bool get_clear_on_close() const
|
||||
{
|
||||
return clear_on_close_;
|
||||
}
|
||||
|
||||
void set_limit(int64_t l)
|
||||
{
|
||||
limit_ = l;
|
||||
}
|
||||
|
||||
void set_clear_on_close(bool e)
|
||||
{
|
||||
clear_on_close_ = e;
|
||||
}
|
||||
|
||||
bool delete_specific_file(const std::string &f);
|
||||
|
||||
// Explicit handler list replacing the `deleted_frame` signal
|
||||
using DeletedFrameHandler =
|
||||
std::function<void(const std::string &path, const std::string &filename)>;
|
||||
size_t add_deleted_frame_handler(DeletedFrameHandler handler);
|
||||
void remove_deleted_frame_handler(size_t id);
|
||||
|
||||
private:
|
||||
struct HashTime {
|
||||
int64_t file_size;
|
||||
int64_t access_time;
|
||||
};
|
||||
|
||||
bool delete_file_internal(std::map<std::string, HashTime>::iterator hash_to_delete);
|
||||
|
||||
bool delete_least_recent();
|
||||
|
||||
void close_cache_folder();
|
||||
|
||||
void emit_deleted_frame(const std::string &path, const std::string &filename);
|
||||
|
||||
std::string path_;
|
||||
|
||||
std::string index_path_;
|
||||
|
||||
std::map<std::string, HashTime> disk_data_;
|
||||
|
||||
int64_t consumption_;
|
||||
|
||||
int64_t limit_;
|
||||
|
||||
bool clear_on_close_;
|
||||
|
||||
// Guards disk_data_/consumption_. The QObject version was serialized by
|
||||
// thread affinity (queued QMetaObject calls + GUI-thread timer); with
|
||||
// direct cross-thread calls and a background save thread, a mutex takes
|
||||
// that role.
|
||||
std::recursive_mutex data_mutex_;
|
||||
|
||||
// QTimer replacement: periodic save of the disk cache index on a
|
||||
// background thread (the timer used to fire in the GUI thread)
|
||||
std::thread save_thread_;
|
||||
std::atomic<bool> save_thread_stop_;
|
||||
|
||||
std::map<size_t, DeletedFrameHandler> deleted_frame_handlers_;
|
||||
size_t next_handler_id_ = 1;
|
||||
|
||||
// Formerly a QTimer timeout slot
|
||||
void save_disk_cache_index();
|
||||
};
|
||||
|
||||
class DiskManager {
|
||||
public:
|
||||
static void create_instance();
|
||||
|
||||
static void destroy_instance();
|
||||
|
||||
static DiskManager *instance();
|
||||
|
||||
bool clear_disk_cache(const std::string &cache_folder);
|
||||
|
||||
DiskCacheFolder *get_default_cache_folder() const
|
||||
{
|
||||
// The first folder will always be the default
|
||||
return open_folders_.front();
|
||||
}
|
||||
|
||||
const std::string &get_default_cache_path() const
|
||||
{
|
||||
return get_default_cache_folder()->get_path();
|
||||
}
|
||||
|
||||
DiskCacheFolder *get_open_folder(const std::string &path);
|
||||
|
||||
const std::vector<DiskCacheFolder *> &get_open_folders() const
|
||||
{
|
||||
return open_folders_;
|
||||
}
|
||||
|
||||
static std::string get_default_disk_cache_config_file();
|
||||
|
||||
static std::string get_default_disk_cache_path();
|
||||
|
||||
/**
|
||||
* @brief Handler showing the disk cache settings dialog for a folder
|
||||
*
|
||||
* Registered by the UI layer (e.g. a DiskCacheDialog-based
|
||||
* implementation), since the engine cannot show dialogs itself. Without
|
||||
* a handler, the request is logged and skipped.
|
||||
*/
|
||||
using ShowDiskCacheSettingsHandler =
|
||||
std::function<void(DiskCacheFolder *folder)>;
|
||||
|
||||
static void set_show_disk_cache_settings_handler(
|
||||
ShowDiskCacheSettingsHandler handler);
|
||||
|
||||
void show_disk_cache_settings_dialog(DiskCacheFolder *folder);
|
||||
void show_disk_cache_settings_dialog(const std::string &path);
|
||||
|
||||
// Formerly slots invoked cross-thread via QMetaObject; now direct calls.
|
||||
void accessed(const std::string &cache_folder, const std::string &filename);
|
||||
|
||||
void created_file(const std::string &cache_folder, const std::string &filename);
|
||||
|
||||
void delete_specific_file(const std::string &filename);
|
||||
|
||||
// Explicit handler lists replacing the `deleted_frame` /
|
||||
// `invalidate_project` signals (subscribers: FrameHashCache et al.)
|
||||
using DeletedFrameHandler = DiskCacheFolder::DeletedFrameHandler;
|
||||
size_t add_deleted_frame_handler(DeletedFrameHandler handler);
|
||||
void remove_deleted_frame_handler(size_t id);
|
||||
|
||||
using InvalidateProjectHandler = std::function<void(Project *p)>;
|
||||
size_t add_invalidate_project_handler(InvalidateProjectHandler handler);
|
||||
void remove_invalidate_project_handler(size_t id);
|
||||
|
||||
void emit_deleted_frame(const std::string &path, const std::string &filename);
|
||||
void emit_invalidate_project(Project *p);
|
||||
|
||||
private:
|
||||
DiskManager();
|
||||
|
||||
~DiskManager();
|
||||
|
||||
static DiskManager *instance_;
|
||||
|
||||
static ShowDiskCacheSettingsHandler show_disk_cache_settings_handler_;
|
||||
|
||||
std::vector<DiskCacheFolder *> open_folders_;
|
||||
|
||||
std::map<size_t, DeletedFrameHandler> deleted_frame_handlers_;
|
||||
std::map<size_t, InvalidateProjectHandler> invalidate_project_handlers_;
|
||||
size_t next_handler_id_ = 1;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_DISKMANAGER_H
|
||||
@@ -0,0 +1,526 @@
|
||||
/*** Olive - Non-Linear Video Editor Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "framehashcache.h"
|
||||
|
||||
#include <OpenEXR/ImfFloatAttribute.h>
|
||||
#include <OpenEXR/ImfFrameBuffer.h>
|
||||
#include <OpenEXR/ImfHeader.h>
|
||||
#include <OpenEXR/ImfInputFile.h>
|
||||
#include <OpenEXR/ImfIntAttribute.h>
|
||||
#include <OpenEXR/ImfOutputFile.h>
|
||||
#include <OpenEXR/ImfChannelList.h>
|
||||
#include <OpenImageIO/imageio.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
|
||||
#include "codec/frame.h"
|
||||
#include "diskmanager.h"
|
||||
#include "filefunctions.h"
|
||||
#include "oiioutils.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
#define super PlaybackCache
|
||||
|
||||
FrameHashCache::FrameHashCache(Node *parent)
|
||||
: super(parent)
|
||||
, deleted_frame_handler_id_(0)
|
||||
, invalidate_project_handler_id_(0)
|
||||
{
|
||||
if (DiskManager::instance()) {
|
||||
deleted_frame_handler_id_ = DiskManager::instance()->add_deleted_frame_handler(
|
||||
[this](const std::string &path, const std::string &filename) {
|
||||
hash_deleted(path, filename);
|
||||
});
|
||||
invalidate_project_handler_id_ =
|
||||
DiskManager::instance()->add_invalidate_project_handler(
|
||||
[this](Project *p) { project_invalidated(p); });
|
||||
}
|
||||
}
|
||||
|
||||
FrameHashCache::~FrameHashCache()
|
||||
{
|
||||
// QObject used to auto-disconnect on destruction; unregister explicitly.
|
||||
if (DiskManager::instance()) {
|
||||
if (deleted_frame_handler_id_) {
|
||||
DiskManager::instance()->remove_deleted_frame_handler(
|
||||
deleted_frame_handler_id_);
|
||||
}
|
||||
if (invalidate_project_handler_id_) {
|
||||
DiskManager::instance()->remove_invalidate_project_handler(
|
||||
invalidate_project_handler_id_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FrameHashCache::set_timebase(const Rational &tb)
|
||||
{
|
||||
timebase_ = tb;
|
||||
}
|
||||
|
||||
void FrameHashCache::validate_timestamp(const int64_t &ts)
|
||||
{
|
||||
TimeRange frame_range(to_time(ts), to_time(ts + 1));
|
||||
validate(frame_range);
|
||||
}
|
||||
|
||||
void FrameHashCache::validate_time(const Rational &time)
|
||||
{
|
||||
validate(TimeRange(time, time + timebase_));
|
||||
}
|
||||
|
||||
std::string FrameHashCache::get_valid_cache_filename(const Rational &time) const
|
||||
{
|
||||
if (is_frame_cached(time)) {
|
||||
return cache_path_name(time);
|
||||
} else if (!get_passthroughs().empty()) {
|
||||
for (const Passthrough &p : get_passthroughs()) {
|
||||
if (p.contains(time)) {
|
||||
return cache_path_name(get_cache_directory(), p.cache, time,
|
||||
timebase_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return std::string();
|
||||
}
|
||||
|
||||
bool FrameHashCache::save_cache_frame(const int64_t &time, FramePtr frame) const
|
||||
{
|
||||
return save_cache_frame(get_cache_directory(), get_uuid(), time, frame);
|
||||
}
|
||||
|
||||
bool FrameHashCache::save_cache_frame(const std::string &cache_path,
|
||||
const std::string &uuid,
|
||||
const int64_t &time, FramePtr frame)
|
||||
{
|
||||
if (cache_path.empty()) {
|
||||
fprintf(stderr, "Failed to save cache frame with empty path\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string fn = cache_path_name(cache_path, uuid, time);
|
||||
|
||||
bool ret = save_cache_frame(fn, frame);
|
||||
|
||||
// Register frame with the disk manager
|
||||
if (ret) {
|
||||
// Was a queued cross-thread QMetaObject::invokeMethod; now a direct call
|
||||
DiskManager::instance()->created_file(cache_path, fn);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool FrameHashCache::save_cache_frame(const std::string &cache_path,
|
||||
const std::string &uuid,
|
||||
const Rational &time, const Rational &tb,
|
||||
FramePtr frame)
|
||||
{
|
||||
if (cache_path.empty()) {
|
||||
fprintf(stderr, "Failed to save cache frame with empty path\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string fn = cache_path_name(cache_path, uuid, time, tb);
|
||||
|
||||
bool ret = save_cache_frame(fn, frame);
|
||||
|
||||
// Register frame with the disk manager
|
||||
if (ret) {
|
||||
// Was a queued cross-thread QMetaObject::invokeMethod; now a direct call
|
||||
DiskManager::instance()->created_file(cache_path, fn);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
FramePtr FrameHashCache::load_cache_frame(const std::string &cache_path,
|
||||
const std::string &uuid,
|
||||
const int64_t &time)
|
||||
{
|
||||
// Minor optimization, we store frames currently being saved just in case something tries to load
|
||||
// while we're saving. This should *occasionally* optimize and also prevent scenarios where
|
||||
// we try to load a frame that's half way through being saved.
|
||||
std::string filename = cache_path_name(cache_path, uuid, time);
|
||||
|
||||
if (cache_path.empty()) {
|
||||
fprintf(stderr, "Failed to load cache frame with empty path\n");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return load_cache_frame(filename);
|
||||
}
|
||||
|
||||
FramePtr FrameHashCache::load_cache_frame(const int64_t &hash) const
|
||||
{
|
||||
return load_cache_frame(get_cache_directory(), get_uuid(), hash);
|
||||
}
|
||||
|
||||
FramePtr FrameHashCache::load_cache_frame(const std::string &fn)
|
||||
{
|
||||
FramePtr frame = nullptr;
|
||||
|
||||
std::error_code ec;
|
||||
if (!fn.empty() && std::filesystem::exists(fn, ec)) {
|
||||
try {
|
||||
Imf::InputFile file(fn.c_str(), 0);
|
||||
|
||||
Imath::Box2i dw = file.header().dataWindow();
|
||||
Imf::PixelType pix_type =
|
||||
file.header().channels().begin().channel().type;
|
||||
int width = dw.max.x - dw.min.x + 1;
|
||||
int height = dw.max.y - dw.min.y + 1;
|
||||
bool has_alpha = file.header().channels().findChannel("A");
|
||||
|
||||
int div = std::max(1, static_cast<const Imf::IntAttribute &>(
|
||||
file.header()["oliveDivider"])
|
||||
.value());
|
||||
|
||||
PixelFormat image_format;
|
||||
if (pix_type == Imf::HALF) {
|
||||
image_format = PixelFormat::f16;
|
||||
} else {
|
||||
image_format = PixelFormat::f32;
|
||||
}
|
||||
|
||||
int channel_count = has_alpha ? VideoParams::k_rgba_channel_count :
|
||||
VideoParams::k_rgb_channel_count;
|
||||
|
||||
frame = Frame::create();
|
||||
frame->set_video_params(VideoParams(
|
||||
width * div, height * div, image_format, channel_count,
|
||||
Rational::from_double(file.header().pixelAspectRatio()),
|
||||
VideoParams::k_interlace_none, div));
|
||||
|
||||
frame->allocate();
|
||||
|
||||
int bpc = VideoParams::get_bytes_per_channel(image_format);
|
||||
|
||||
size_t xs = channel_count * bpc;
|
||||
size_t ys = frame->linesize_bytes();
|
||||
|
||||
Imf::FrameBuffer framebuffer;
|
||||
framebuffer.insert("R",
|
||||
Imf::Slice(pix_type, frame->data(), xs, ys));
|
||||
framebuffer.insert("G", Imf::Slice(pix_type, frame->data() + bpc,
|
||||
xs, ys));
|
||||
framebuffer.insert(
|
||||
"B", Imf::Slice(pix_type, frame->data() + 2 * bpc, xs, ys));
|
||||
if (has_alpha) {
|
||||
framebuffer.insert(
|
||||
"A", Imf::Slice(pix_type, frame->data() + 3 * bpc, xs, ys));
|
||||
}
|
||||
|
||||
file.setFrameBuffer(framebuffer);
|
||||
|
||||
file.readPixels(dw.min.y, dw.max.y);
|
||||
} catch (const std::exception &e) {
|
||||
// Not an EXR, maybe it's a JPEG?
|
||||
std::unique_ptr<OIIO::ImageInput> in = OIIO::ImageInput::open(fn);
|
||||
|
||||
if (in) {
|
||||
// FIXME: Hardcoded
|
||||
const int div = 1;
|
||||
const PixelFormat image_format = PixelFormat::u8;
|
||||
const int channel_count = 4;
|
||||
const Rational par(1, 1);
|
||||
|
||||
const OIIO::ImageSpec &spec = in->spec();
|
||||
const int src_channels = spec.nchannels;
|
||||
|
||||
// Read native channels as u8, then expand to RGBA with opaque
|
||||
// alpha (what QImage::convertTo(Format_RGBA8888_Premultiplied)
|
||||
// did; with alpha=255 premultiplication is the identity).
|
||||
std::vector<unsigned char> src(spec.width * spec.height *
|
||||
src_channels);
|
||||
if (in->read_image(0, 0, 0, src_channels, OIIO::TypeDesc::UINT8,
|
||||
src.data())) {
|
||||
frame = Frame::create();
|
||||
frame->set_video_params(VideoParams(
|
||||
spec.width * div, spec.height * div, image_format,
|
||||
channel_count, par, VideoParams::k_interlace_none,
|
||||
div));
|
||||
|
||||
frame->allocate();
|
||||
|
||||
size_t src_linesize = size_t(spec.width) * src_channels;
|
||||
for (int i = 0; i < spec.height; i++) {
|
||||
const unsigned char *src_row =
|
||||
src.data() + src_linesize * i;
|
||||
char *dst_row =
|
||||
frame->data() + frame->linesize_bytes() * i;
|
||||
for (int x = 0; x < spec.width; x++) {
|
||||
char *dst_px = dst_row + x * channel_count;
|
||||
const unsigned char *src_px =
|
||||
src_row + x * src_channels;
|
||||
for (int c = 0; c < 3; c++) {
|
||||
dst_px[c] = char(c < src_channels ? src_px[c] : 0);
|
||||
}
|
||||
dst_px[3] = char(0xFF);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
in->close();
|
||||
}
|
||||
|
||||
if (!frame) {
|
||||
fprintf(stderr, "Failed to read cache frame: %s\n", e.what());
|
||||
|
||||
// Clear frame to signal that nothing was loaded
|
||||
frame = nullptr;
|
||||
|
||||
// Assume this frame is corrupt in some way and delete it
|
||||
// (was a queued QMetaObject::invokeMethod; now a direct call)
|
||||
DiskManager::instance()->delete_specific_file(fn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
void FrameHashCache::set_passthrough(PlaybackCache *cache)
|
||||
{
|
||||
super::set_passthrough(cache);
|
||||
set_timebase(static_cast<FrameHashCache *>(cache)->get_timebase());
|
||||
}
|
||||
|
||||
void FrameHashCache::LoadStateEvent(BinaryStreamReader &stream)
|
||||
{
|
||||
uint32_t version;
|
||||
int32_t num, den;
|
||||
|
||||
stream >> version;
|
||||
|
||||
switch (version) {
|
||||
case 1:
|
||||
stream >> num;
|
||||
stream >> den;
|
||||
timebase_ = Rational(num, den);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void FrameHashCache::SaveStateEvent(BinaryStreamWriter &stream)
|
||||
{
|
||||
uint32_t version = 1;
|
||||
|
||||
stream << version;
|
||||
|
||||
stream << int32_t(timebase_.numerator());
|
||||
stream << int32_t(timebase_.denominator());
|
||||
}
|
||||
|
||||
Rational FrameHashCache::to_time(const int64_t &ts) const
|
||||
{
|
||||
return Timecode::timestamp_to_time(ts, timebase_);
|
||||
}
|
||||
|
||||
int64_t FrameHashCache::to_timestamp(const Rational &ts,
|
||||
Timecode::Rounding rounding) const
|
||||
{
|
||||
return Timecode::time_to_timestamp(ts, timebase_, rounding);
|
||||
}
|
||||
|
||||
void FrameHashCache::hash_deleted(const std::string &path,
|
||||
const std::string &filename)
|
||||
{
|
||||
std::string cache_dir = get_cache_directory();
|
||||
if (cache_dir.empty() || path != cache_dir) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::filesystem::path info(filename);
|
||||
if (get_uuid() != info.parent_path().filename().string()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int64_t timestamp = strtoll(info.filename().string().c_str(), nullptr, 10);
|
||||
invalidate(TimeRange(to_time(timestamp), to_time(timestamp + 1)));
|
||||
}
|
||||
|
||||
void FrameHashCache::project_invalidated(Project *p)
|
||||
{
|
||||
if (get_project() == p) {
|
||||
invalidate_all();
|
||||
}
|
||||
}
|
||||
|
||||
std::string FrameHashCache::cache_path_name(const int64_t &time) const
|
||||
{
|
||||
return cache_path_name(get_cache_directory(), get_uuid(), time);
|
||||
}
|
||||
|
||||
std::string FrameHashCache::cache_path_name(const Rational &time) const
|
||||
{
|
||||
return cache_path_name(get_cache_directory(), get_uuid(), time, timebase_);
|
||||
}
|
||||
|
||||
std::string FrameHashCache::cache_path_name(const std::string &cache_path,
|
||||
const std::string &cache_id,
|
||||
const int64_t &time)
|
||||
{
|
||||
std::string filename =
|
||||
(std::filesystem::path(get_this_cache_directory(cache_path, cache_id)) /
|
||||
std::to_string(time))
|
||||
.string();
|
||||
|
||||
// Register that in some way this hash has been accessed
|
||||
if (DiskManager::instance()) {
|
||||
// Was a queued cross-thread QMetaObject::invokeMethod; now a direct call
|
||||
DiskManager::instance()->accessed(cache_path, filename);
|
||||
}
|
||||
|
||||
return filename;
|
||||
}
|
||||
|
||||
std::string FrameHashCache::cache_path_name(const std::string &cache_path,
|
||||
const std::string &cache_id,
|
||||
const Rational &time,
|
||||
const Rational &tb)
|
||||
{
|
||||
return cache_path_name(cache_path, cache_id,
|
||||
Timecode::time_to_timestamp(time, tb,
|
||||
Timecode::k_round));
|
||||
}
|
||||
|
||||
bool FrameHashCache::save_cache_frame(const std::string &filename,
|
||||
const FramePtr frame)
|
||||
{
|
||||
// Ensure directory is created
|
||||
std::filesystem::path cache_dir =
|
||||
std::filesystem::path(filename).parent_path();
|
||||
if (!FileFunctions::directory_is_valid(cache_dir.string())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (VideoParams::format_is_float(frame->format())) {
|
||||
// Floating point types are stored in EXR
|
||||
Imf::PixelType pix_type;
|
||||
|
||||
if (frame->format() == PixelFormat::f16) {
|
||||
pix_type = Imf::HALF;
|
||||
} else {
|
||||
pix_type = Imf::FLOAT;
|
||||
}
|
||||
|
||||
Imf::Header header(frame->width(), frame->height());
|
||||
header.channels().insert("R", Imf::Channel(pix_type));
|
||||
header.channels().insert("G", Imf::Channel(pix_type));
|
||||
header.channels().insert("B", Imf::Channel(pix_type));
|
||||
if (frame->channel_count() == VideoParams::k_rgba_channel_count) {
|
||||
header.channels().insert("A", Imf::Channel(pix_type));
|
||||
}
|
||||
|
||||
header.compression() = Imf::DWAA_COMPRESSION;
|
||||
header.insert("dwaCompressionLevel", Imf::FloatAttribute(200.0f));
|
||||
header.pixelAspectRatio() =
|
||||
frame->video_params().pixel_aspect_ratio().to_double();
|
||||
|
||||
header.insert("oliveDivider",
|
||||
Imf::IntAttribute(frame->video_params().divider()));
|
||||
|
||||
try {
|
||||
Imf::OutputFile out(filename.c_str(), header, 0);
|
||||
|
||||
int bpc = VideoParams::get_bytes_per_channel(frame->format());
|
||||
|
||||
size_t xs = frame->channel_count() * bpc;
|
||||
size_t ys = frame->linesize_bytes();
|
||||
|
||||
Imf::FrameBuffer framebuffer;
|
||||
framebuffer.insert("R",
|
||||
Imf::Slice(pix_type, frame->data(), xs, ys));
|
||||
framebuffer.insert("G", Imf::Slice(pix_type, frame->data() + bpc,
|
||||
xs, ys));
|
||||
framebuffer.insert(
|
||||
"B", Imf::Slice(pix_type, frame->data() + 2 * bpc, xs, ys));
|
||||
if (frame->channel_count() == VideoParams::k_rgba_channel_count) {
|
||||
framebuffer.insert(
|
||||
"A", Imf::Slice(pix_type, frame->data() + 3 * bpc, xs, ys));
|
||||
}
|
||||
out.setFrameBuffer(framebuffer);
|
||||
|
||||
out.writePixels(frame->height());
|
||||
|
||||
return true;
|
||||
} catch (const std::exception &e) {
|
||||
fprintf(stderr, "Failed to write cache frame: %s\n", e.what());
|
||||
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// Integer types are stored as JPEG via OIIO (was QImage). The JPEG
|
||||
// writer drops the alpha channel, as Qt's JPEG handler did.
|
||||
OIIO::TypeDesc base_type = OIIO::TypeDesc::UNKNOWN;
|
||||
|
||||
switch (frame->format()) {
|
||||
case PixelFormat::u8:
|
||||
if (frame->channel_count() == VideoParams::k_rgba_channel_count ||
|
||||
frame->channel_count() == VideoParams::k_rgb_channel_count) {
|
||||
base_type = OIIO::TypeDesc::UINT8;
|
||||
}
|
||||
break;
|
||||
case PixelFormat::u10:
|
||||
break;
|
||||
case PixelFormat::u16:
|
||||
if (frame->channel_count() == VideoParams::k_rgba_channel_count) {
|
||||
base_type = OIIO::TypeDesc::UINT16;
|
||||
}
|
||||
break;
|
||||
case PixelFormat::f16:
|
||||
case PixelFormat::f32:
|
||||
case PixelFormat::count:
|
||||
case PixelFormat::invalid:
|
||||
break;
|
||||
}
|
||||
|
||||
if (base_type == OIIO::TypeDesc::UNKNOWN) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::unique_ptr<OIIO::ImageOutput> out =
|
||||
OIIO::ImageOutput::create(filename);
|
||||
if (!out) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int bpc = VideoParams::get_bytes_per_channel(frame->format());
|
||||
OIIO::ImageSpec spec(frame->width(), frame->height(),
|
||||
frame->channel_count(), base_type);
|
||||
|
||||
if (!out->open(filename, spec)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ok = out->write_image(
|
||||
base_type, frame->const_data(), frame->channel_count() * bpc,
|
||||
frame->linesize_bytes());
|
||||
ok = out->close() && ok;
|
||||
|
||||
return ok;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_VIDEORENDERFRAMECACHE_H
|
||||
#define OAK_VIDEORENDERFRAMECACHE_H
|
||||
|
||||
#include "codec/frame.h"
|
||||
#include "playbackcache.h"
|
||||
#include "videoparams.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class FrameHashCache : public PlaybackCache {
|
||||
public:
|
||||
FrameHashCache(Node *parent = nullptr);
|
||||
|
||||
virtual ~FrameHashCache() override;
|
||||
|
||||
const Rational &get_timebase() const
|
||||
{
|
||||
return timebase_;
|
||||
}
|
||||
|
||||
void set_timebase(const Rational &tb);
|
||||
|
||||
void validate_timestamp(const int64_t &ts);
|
||||
void validate_time(const Rational &time);
|
||||
|
||||
bool is_frame_cached(const Rational &time) const
|
||||
{
|
||||
return get_validated_ranges().contains(time);
|
||||
}
|
||||
|
||||
std::string get_valid_cache_filename(const Rational &time) const;
|
||||
|
||||
static bool save_cache_frame(const std::string &filename, FramePtr frame);
|
||||
bool save_cache_frame(const int64_t &time, FramePtr frame) const;
|
||||
static bool save_cache_frame(const std::string &cache_path,
|
||||
const std::string &uuid, const int64_t &time,
|
||||
FramePtr frame);
|
||||
static bool save_cache_frame(const std::string &cache_path,
|
||||
const std::string &uuid, const Rational &time,
|
||||
const Rational &tb, FramePtr frame);
|
||||
static FramePtr load_cache_frame(const std::string &cache_path,
|
||||
const std::string &uuid,
|
||||
const int64_t &time);
|
||||
FramePtr load_cache_frame(const int64_t &time) const;
|
||||
static FramePtr load_cache_frame(const std::string &fn);
|
||||
|
||||
virtual void set_passthrough(PlaybackCache *cache) override;
|
||||
|
||||
// Formerly slots connected to DiskManager's `deleted_frame` /
|
||||
// `invalidate_project` signals; registered as explicit handlers now and
|
||||
// still public so the facade can re-wire if needed.
|
||||
void hash_deleted(const std::string &path, const std::string &filename);
|
||||
|
||||
void project_invalidated(Project *p);
|
||||
|
||||
protected:
|
||||
virtual void LoadStateEvent(BinaryStreamReader &stream) override;
|
||||
virtual void SaveStateEvent(BinaryStreamWriter &stream) override;
|
||||
|
||||
private:
|
||||
Rational to_time(const int64_t &ts) const;
|
||||
int64_t to_timestamp(const Rational &ts,
|
||||
Timecode::Rounding rounding = Timecode::k_round) const;
|
||||
|
||||
/**
|
||||
* @brief Return the path of the cached image at this time
|
||||
*/
|
||||
std::string cache_path_name(const int64_t &time) const;
|
||||
std::string cache_path_name(const Rational &time) const;
|
||||
|
||||
static std::string cache_path_name(const std::string &cache_path,
|
||||
const std::string &cache_id,
|
||||
const int64_t &time);
|
||||
static std::string cache_path_name(const std::string &cache_path,
|
||||
const std::string &cache_id,
|
||||
const Rational &time, const Rational &tb);
|
||||
|
||||
Rational timebase_;
|
||||
|
||||
// DiskManager handler registration ids (0 = not registered)
|
||||
size_t deleted_frame_handler_id_;
|
||||
size_t invalidate_project_handler_id_;
|
||||
};
|
||||
|
||||
class ThumbnailCache : public FrameHashCache {
|
||||
public:
|
||||
ThumbnailCache(Node *parent = nullptr)
|
||||
: FrameHashCache(parent)
|
||||
{
|
||||
set_timebase(Rational(1, 10));
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_VIDEORENDERFRAMECACHE_H
|
||||
@@ -0,0 +1,152 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "framemanager.h"
|
||||
|
||||
#include <chrono>
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
FrameManager *FrameManager::instance_ = nullptr;
|
||||
const int FrameManager::k_frame_lifetime = 5000;
|
||||
|
||||
static int64_t current_msecs_since_epoch()
|
||||
{
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch())
|
||||
.count();
|
||||
}
|
||||
|
||||
void FrameManager::create_instance()
|
||||
{
|
||||
instance_ = new FrameManager();
|
||||
}
|
||||
|
||||
void FrameManager::destroy_instance()
|
||||
{
|
||||
delete instance_;
|
||||
instance_ = nullptr;
|
||||
}
|
||||
|
||||
FrameManager *FrameManager::instance()
|
||||
{
|
||||
return instance_;
|
||||
}
|
||||
|
||||
char *FrameManager::allocate(int size)
|
||||
{
|
||||
if (instance()) {
|
||||
return instance()->allocate_from_pool(size);
|
||||
} else {
|
||||
return new char[size];
|
||||
}
|
||||
}
|
||||
|
||||
void FrameManager::deallocate(int size, char *buffer)
|
||||
{
|
||||
if (instance()) {
|
||||
instance()->deallocate_to_pool(size, buffer);
|
||||
} else {
|
||||
delete[] buffer;
|
||||
}
|
||||
}
|
||||
|
||||
FrameManager::FrameManager()
|
||||
: gc_thread_stop_(false)
|
||||
{
|
||||
// Replaces the QTimer that fired garbage_collection() every
|
||||
// k_frame_lifetime ms
|
||||
gc_thread_ = std::thread([this]() {
|
||||
while (!gc_thread_stop_.load()) {
|
||||
std::this_thread::sleep_for(
|
||||
std::chrono::milliseconds(k_frame_lifetime));
|
||||
if (gc_thread_stop_.load()) {
|
||||
break;
|
||||
}
|
||||
garbage_collection();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
char *FrameManager::allocate_from_pool(int size)
|
||||
{
|
||||
std::lock_guard<std::mutex> locker(mutex_);
|
||||
|
||||
std::list<Buffer> &buffer_list = pool_[size];
|
||||
char *buf = nullptr;
|
||||
|
||||
if (buffer_list.empty()) {
|
||||
buf = new char[size];
|
||||
} else {
|
||||
// Take this buffer from the list
|
||||
buf = buffer_list.front().data;
|
||||
buffer_list.pop_front();
|
||||
}
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
void FrameManager::deallocate_to_pool(int size, char *buffer)
|
||||
{
|
||||
std::lock_guard<std::mutex> locker(mutex_);
|
||||
|
||||
std::list<Buffer> &buffer_list = pool_[size];
|
||||
|
||||
buffer_list.push_back({ current_msecs_since_epoch(), buffer });
|
||||
}
|
||||
|
||||
void FrameManager::garbage_collection()
|
||||
{
|
||||
std::lock_guard<std::mutex> locker(mutex_);
|
||||
|
||||
int64_t min_life = current_msecs_since_epoch() - k_frame_lifetime;
|
||||
|
||||
for (auto it = pool_.begin(); it != pool_.end(); it++) {
|
||||
std::list<Buffer> &list = it->second;
|
||||
|
||||
while (list.size() > 0 && list.front().time < min_life) {
|
||||
delete[] list.front().data;
|
||||
list.pop_front();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FrameManager::~FrameManager()
|
||||
{
|
||||
gc_thread_stop_.store(true);
|
||||
if (gc_thread_.joinable()) {
|
||||
gc_thread_.join();
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> locker(mutex_);
|
||||
|
||||
for (auto it = pool_.begin(); it != pool_.end(); it++) {
|
||||
std::list<Buffer> &list = it->second;
|
||||
for (auto jt = list.begin(); jt != list.end(); jt++) {
|
||||
delete[](*jt).data;
|
||||
}
|
||||
}
|
||||
|
||||
pool_.clear();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_FRAMEMANAGER_H
|
||||
#define OAK_FRAMEMANAGER_H
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class FrameManager {
|
||||
public:
|
||||
static void create_instance();
|
||||
|
||||
static void destroy_instance();
|
||||
|
||||
static FrameManager *instance();
|
||||
|
||||
static char *allocate(int size);
|
||||
|
||||
static void deallocate(int size, char *buffer);
|
||||
|
||||
private:
|
||||
FrameManager();
|
||||
|
||||
~FrameManager();
|
||||
|
||||
FrameManager(const FrameManager &) = delete;
|
||||
FrameManager &operator=(const FrameManager &) = delete;
|
||||
|
||||
/**
|
||||
* @brief Allocate buffer
|
||||
*
|
||||
* Caller takes ownership of buffer and can delete it if they want. It can also be returned to
|
||||
* the manager with Deallocate and potentially be re-used later.
|
||||
*
|
||||
* Thread-safe.
|
||||
*/
|
||||
char *allocate_from_pool(int size);
|
||||
|
||||
/**
|
||||
* @brief Deallocate buffer
|
||||
*
|
||||
* Manager will take ownership and buffer will stay allocated for some time in case it can be
|
||||
* re-used.
|
||||
*
|
||||
* Thread-safe.
|
||||
*/
|
||||
void deallocate_to_pool(int size, char *buffer);
|
||||
|
||||
static FrameManager *instance_;
|
||||
|
||||
static const int k_frame_lifetime;
|
||||
|
||||
struct Buffer {
|
||||
int64_t time;
|
||||
char *data;
|
||||
};
|
||||
|
||||
std::map<int, std::list<Buffer>> pool_;
|
||||
|
||||
std::mutex mutex_;
|
||||
|
||||
// QTimer replacement: periodic garbage collection on a background
|
||||
// thread (the timer used to fire in the GUI thread)
|
||||
std::thread gc_thread_;
|
||||
std::atomic<bool> gc_thread_stop_;
|
||||
|
||||
// Formerly a QTimer timeout slot
|
||||
void garbage_collection();
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_FRAMEMANAGER_H
|
||||
@@ -0,0 +1,59 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "renderer.h"
|
||||
|
||||
#include "filefunctions.h"
|
||||
#include "value.h"
|
||||
#include "render/job/shaderjob.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
TexturePtr Renderer::interlace_texture(TexturePtr top, TexturePtr bottom,
|
||||
const VideoParams ¶ms)
|
||||
{
|
||||
color_cache_mutex_.lock();
|
||||
if (interlace_texture_.is_null()) {
|
||||
interlace_texture_ =
|
||||
create_native_shader(ShaderCode(FileFunctions::read_file_as_string(
|
||||
":/shaders/interlace.frag")));
|
||||
}
|
||||
color_cache_mutex_.unlock();
|
||||
|
||||
ShaderJob job;
|
||||
job.insert("top_tex_in",
|
||||
NodeValue(NodeValue::k_texture, Variant::from_value(top)));
|
||||
job.insert("bottom_tex_in",
|
||||
NodeValue(NodeValue::k_texture, Variant::from_value(bottom)));
|
||||
job.insert("resolution_in",
|
||||
NodeValue(NodeValue::k_vec2,
|
||||
Vector2D(params.effective_width(),
|
||||
params.effective_height())));
|
||||
|
||||
TexturePtr output = create_texture(params);
|
||||
|
||||
blit_to_texture(interlace_texture_, job, output.get());
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak 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 OAK_IPC_FRAMESLOTPOOL_H
|
||||
#define OAK_IPC_FRAMESLOTPOOL_H
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "oakengine/ipc.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
namespace ipc
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Per-slot metadata describing the frame currently occupying a slot.
|
||||
*
|
||||
* Trivially-copyable POD that lives in shared memory alongside the pixel data, part of the
|
||||
* version-1 wire protocol with the render worker. This is the C ABI oak_frame_slot_meta struct,
|
||||
* aliased so the shared-memory layout is defined exactly once, in oakengine/ipc.h.
|
||||
*/
|
||||
typedef oak_frame_slot_meta FrameSlotMeta;
|
||||
|
||||
/**
|
||||
* @brief A fixed-size pool of equal-sized frame slots in shared memory, with lock-free hand-off.
|
||||
*
|
||||
* Consumer-side wrapper over the liboakengine C ABI: the object only holds an opaque
|
||||
* OakFrameSlotPool handle and forwards every call across the C boundary. The public API is
|
||||
* unchanged from the original implementation; see oakengine/ipc.h for the protocol description.
|
||||
*
|
||||
* One pool models a single direction of frame flow (e.g. worker -> main for rendered output, or
|
||||
* main -> worker for decoded input). The pool does NOT own the memory; it is constructed over a
|
||||
* SharedMemoryRegion mapping. Use bytes_needed() to size that region.
|
||||
*/
|
||||
class FrameSlotPool {
|
||||
public:
|
||||
FrameSlotPool() = default;
|
||||
|
||||
FrameSlotPool(const FrameSlotPool &rhs)
|
||||
: handle_(oakengine_ipc_framepool_copy(rhs.handle_))
|
||||
{
|
||||
}
|
||||
|
||||
FrameSlotPool(FrameSlotPool &&rhs) noexcept
|
||||
: handle_(rhs.handle_)
|
||||
{
|
||||
rhs.handle_ = nullptr;
|
||||
}
|
||||
|
||||
~FrameSlotPool()
|
||||
{
|
||||
oakengine_ipc_framepool_free(handle_);
|
||||
}
|
||||
|
||||
FrameSlotPool &operator=(const FrameSlotPool &rhs)
|
||||
{
|
||||
if (this != &rhs) {
|
||||
oakengine_ipc_framepool_free(handle_);
|
||||
handle_ = oakengine_ipc_framepool_copy(rhs.handle_);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
FrameSlotPool &operator=(FrameSlotPool &&rhs) noexcept
|
||||
{
|
||||
if (this != &rhs) {
|
||||
oakengine_ipc_framepool_free(handle_);
|
||||
handle_ = rhs.handle_;
|
||||
rhs.handle_ = nullptr;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Total bytes a region must provide to back a pool of `slot_count` x `slot_data_bytes`.
|
||||
*/
|
||||
static size_t bytes_needed(uint32_t slot_count, size_t slot_data_bytes)
|
||||
{
|
||||
return oakengine_ipc_framepool_bytes_needed(slot_count, slot_data_bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Lay out and initialize a brand-new pool over `mem` (owner side, once).
|
||||
*
|
||||
* Initializes both rings, seeds the free ring with every slot index, and zeroes metadata.
|
||||
* `mem` must provide at least bytes_needed(slot_count, slot_data_bytes) bytes.
|
||||
*/
|
||||
static FrameSlotPool create(void *mem, uint32_t slot_count,
|
||||
size_t slot_data_bytes)
|
||||
{
|
||||
return from_handle(oakengine_ipc_framepool_create(mem, slot_count,
|
||||
slot_data_bytes));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Map an existing, already-initialized pool (peer side).
|
||||
*
|
||||
* Reads slot_count/slot_data_bytes from the in-memory header written by create().
|
||||
*/
|
||||
static FrameSlotPool attach(void *mem)
|
||||
{
|
||||
return from_handle(oakengine_ipc_framepool_attach(mem));
|
||||
}
|
||||
|
||||
bool is_valid() const
|
||||
{
|
||||
return oakengine_ipc_framepool_is_valid(handle_) != 0;
|
||||
}
|
||||
|
||||
uint32_t slot_count() const
|
||||
{
|
||||
return oakengine_ipc_framepool_slot_count(handle_);
|
||||
}
|
||||
|
||||
size_t slot_data_bytes() const
|
||||
{
|
||||
return oakengine_ipc_framepool_slot_data_bytes(handle_);
|
||||
}
|
||||
|
||||
// ---- Filler side ----
|
||||
|
||||
/**
|
||||
* @brief Take ownership of a free slot. Returns false (and leaves *index untouched) if none free.
|
||||
*/
|
||||
bool acquire(uint32_t *index)
|
||||
{
|
||||
return oakengine_ipc_framepool_acquire(handle_, index) != 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Pointer to a slot's pixel data block (slot_data_bytes available).
|
||||
*/
|
||||
void *slot_data(uint32_t index)
|
||||
{
|
||||
return oakengine_ipc_framepool_slot_data(handle_, index);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Mutable metadata for a slot. Filler writes this before publish().
|
||||
*/
|
||||
FrameSlotMeta *meta(uint32_t index)
|
||||
{
|
||||
return oakengine_ipc_framepool_meta(handle_, index);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Publish a filled slot to the drainer. Must follow a successful acquire() of `index`.
|
||||
*/
|
||||
bool publish(uint32_t index)
|
||||
{
|
||||
return oakengine_ipc_framepool_publish(handle_, index) != 0;
|
||||
}
|
||||
|
||||
// ---- Drainer side ----
|
||||
|
||||
/**
|
||||
* @brief Take the next published slot. Returns false if nothing is ready.
|
||||
*/
|
||||
bool consume(uint32_t *index)
|
||||
{
|
||||
return oakengine_ipc_framepool_consume(handle_, index) != 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Return a consumed slot to the free pool for reuse. Must follow consume() of `index`.
|
||||
*/
|
||||
bool release(uint32_t index)
|
||||
{
|
||||
return oakengine_ipc_framepool_release(handle_, index) != 0;
|
||||
}
|
||||
|
||||
const FrameSlotMeta *meta(uint32_t index) const
|
||||
{
|
||||
return oakengine_ipc_framepool_meta_const(handle_, index);
|
||||
}
|
||||
|
||||
const void *slot_data(uint32_t index) const
|
||||
{
|
||||
return oakengine_ipc_framepool_slot_data_const(handle_, index);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The wrapped C handle, for cross-type wrappers and direct C API use
|
||||
*/
|
||||
OakFrameSlotPool *handle() const
|
||||
{
|
||||
return handle_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Wraps an owned C handle (takes ownership)
|
||||
*/
|
||||
static FrameSlotPool from_handle(OakFrameSlotPool *handle)
|
||||
{
|
||||
return FrameSlotPool(handle);
|
||||
}
|
||||
|
||||
private:
|
||||
explicit FrameSlotPool(OakFrameSlotPool *handle)
|
||||
: handle_(handle)
|
||||
{
|
||||
}
|
||||
|
||||
OakFrameSlotPool *handle_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace ipc
|
||||
} // namespace olive
|
||||
|
||||
#endif // OAK_IPC_FRAMESLOTPOOL_H
|
||||
@@ -0,0 +1,380 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak 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 OAK_IPC_IPCMESSAGE_H
|
||||
#define OAK_IPC_IPCMESSAGE_H
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "oakengine/ipc.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
namespace ipc
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Control-plane protocol exchanged over stdio between main and render worker.
|
||||
*
|
||||
* The wire format is NDJSON: one compact JSON object per line, terminated by '\n'. This is
|
||||
* deliberately human-readable so the channel can be inspected live with `tee`/`cat` and test
|
||||
* messages can be injected by hand. The stdio channel carries only low-frequency control traffic;
|
||||
* bulk pixel data travels through the shared-memory FrameSlotPool, and the (potentially large)
|
||||
* serialized node graph travels via a temporary file referenced by path.
|
||||
*
|
||||
* Consumer-side wrapper over the liboakengine C ABI: the typed builders/parsers below convert
|
||||
* through the oakengine_ipc_*_to_json/parse functions, so the JSON field names and the wire
|
||||
* format are defined exactly once, inside the library, and stay in lockstep with the worker.
|
||||
* A message "object" on this side is simply the compact JSON text (`JsonMessage`); no Qt JSON
|
||||
* types are involved anymore.
|
||||
*
|
||||
* Every message object has a "type" string field. Directionality (M = main, W = worker):
|
||||
* "handshake" M<->W Negotiate protocol version and announce shared-memory key/geometry.
|
||||
* "load_graph" M ->W Path to a temporary file holding the serialized node graph.
|
||||
* "render_frame" M ->W Request a frame: node uuid, time, video params.
|
||||
* "frame_ready" W ->M A rendered frame is published; carries the output-slot index + ticket.
|
||||
* "cancel" M ->W Abandon an in-flight ticket by id.
|
||||
* "graph_update" M ->W (Reserved, Phase 6) Incremental graph mutation, mirrors ProjectCopier.
|
||||
* "shutdown" M ->W Finish current work and exit cleanly.
|
||||
* "error" W ->M Worker-side failure report (human-readable "message" field).
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief One control-plane message as compact JSON object text (no trailing newline).
|
||||
*/
|
||||
using JsonMessage = std::string;
|
||||
|
||||
namespace msgtype
|
||||
{
|
||||
constexpr const char *k_handshake = OAKENGINE_IPC_MSGTYPE_HANDSHAKE;
|
||||
constexpr const char *k_load_graph = OAKENGINE_IPC_MSGTYPE_LOAD_GRAPH;
|
||||
constexpr const char *k_render_frame = OAKENGINE_IPC_MSGTYPE_RENDER_FRAME;
|
||||
constexpr const char *k_frame_ready = OAKENGINE_IPC_MSGTYPE_FRAME_READY;
|
||||
constexpr const char *k_cancel = OAKENGINE_IPC_MSGTYPE_CANCEL;
|
||||
constexpr const char *k_graph_update = OAKENGINE_IPC_MSGTYPE_GRAPH_UPDATE;
|
||||
constexpr const char *k_shutdown = OAKENGINE_IPC_MSGTYPE_SHUTDOWN;
|
||||
constexpr const char *k_error = OAKENGINE_IPC_MSGTYPE_ERROR;
|
||||
} // namespace msgtype
|
||||
|
||||
namespace detail
|
||||
{
|
||||
|
||||
inline void copy_str(const std::string &s, char *dst, size_t cap)
|
||||
{
|
||||
const size_t n = std::min(s.size(), cap - 1);
|
||||
memcpy(dst, s.data(), n);
|
||||
dst[n] = '\0';
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Run a C to_json function (buf/size convention) and return the compact JSON text.
|
||||
*/
|
||||
template <typename F> std::string via_c_json(F &&to_json)
|
||||
{
|
||||
const int size = to_json(nullptr, 0);
|
||||
std::string buf(size + 1, '\0');
|
||||
to_json(buf.data(), size + 1);
|
||||
buf.resize(size);
|
||||
return buf;
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
/**
|
||||
* @brief Write one NDJSON message line to `device`.
|
||||
*
|
||||
* Appends '\n' to the compact JSON text and writes the whole line in one call. Returns true only
|
||||
* if the full line was written. `Device` is anything with a
|
||||
* `write(const char *, int64_t)`-shaped method (QProcess during the transition, a plain pipe
|
||||
* wrapper later).
|
||||
*/
|
||||
template <typename Device>
|
||||
bool write_message(Device *device, const JsonMessage &obj)
|
||||
{
|
||||
std::string line = obj;
|
||||
line.push_back('\n');
|
||||
return device->write(line.data(), int64_t(line.size())) ==
|
||||
int64_t(line.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Pull one complete NDJSON line out of `buffer`.
|
||||
*
|
||||
* If `buffer` contains at least one '\n', the leading line is removed, validated, and returned
|
||||
* via `out` (true). If no complete line is buffered yet, leaves `buffer` untouched and returns
|
||||
* false. Malformed lines are skipped (removed) and reported via `*ok = false` so the reader can
|
||||
* log and continue rather than wedge. Supports the typical "append bytes as they arrive, then
|
||||
* drain complete lines" reader loop on a pipe.
|
||||
*
|
||||
* Validation is delegated to oakengine_ipc_message_type(): a line counts as well-formed when it
|
||||
* is a JSON object carrying a recognized "type" field. (The Qt original accepted any syntactically
|
||||
* valid JSON object here; the per-type from_json() parsers still reject wrong-type lines, so the
|
||||
* only behavioral difference is that valid-JSON-but-unknown-type lines are now reported as
|
||||
* malformed at this layer.)
|
||||
*/
|
||||
inline bool read_message(std::string *buffer, JsonMessage *out,
|
||||
bool *ok = nullptr)
|
||||
{
|
||||
while (true) {
|
||||
const std::string::size_type newline = buffer->find('\n');
|
||||
if (newline == std::string::npos) {
|
||||
// No complete line buffered yet.
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string line = buffer->substr(0, newline);
|
||||
buffer->erase(0, newline + 1);
|
||||
|
||||
// Skip blank lines silently (e.g. a stray newline) without flagging an error.
|
||||
const std::string::size_type first_non_space =
|
||||
line.find_first_not_of(" \t\r");
|
||||
if (first_non_space == std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (oakengine_ipc_message_type(line.c_str()) ==
|
||||
OAK_IPC_MSGTYPE_UNKNOWN) {
|
||||
if (ok) {
|
||||
*ok = false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
*out = std::move(line);
|
||||
if (ok) {
|
||||
*ok = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Typed message builders / parsers -------------------------------------------------------
|
||||
//
|
||||
// Thin wrappers that convert each struct to/from the C ABI POD form and let the library build or
|
||||
// read the JSON, keeping field names in one place so main and worker agree. Fields use plain JSON
|
||||
// numbers/strings; 64-bit ids are stored as JSON numbers (doubles exactly represent integers up
|
||||
// to 2^53, ample for our counters).
|
||||
|
||||
struct HandshakeMsg {
|
||||
int protocol_version = 0;
|
||||
std::string shm_key; ///< Worker->main output shared-memory segment key.
|
||||
std::string
|
||||
input_shm_key; ///< Main->worker input shared-memory segment key (optional).
|
||||
int input_slots = 0; ///< Number of main->worker input frame slots.
|
||||
int output_slots = 0; ///< Number of worker->main output frame slots.
|
||||
int64_t slot_data_bytes = 0; ///< Per-output-slot pixel block size.
|
||||
int64_t input_slot_data_bytes = 0; ///< Per-input-slot pixel block size.
|
||||
|
||||
JsonMessage to_json() const
|
||||
{
|
||||
oak_ipc_handshake c;
|
||||
c.protocol_version = protocol_version;
|
||||
detail::copy_str(shm_key, c.shm_key, sizeof(c.shm_key));
|
||||
detail::copy_str(input_shm_key, c.input_shm_key,
|
||||
sizeof(c.input_shm_key));
|
||||
c.input_slots = input_slots;
|
||||
c.output_slots = output_slots;
|
||||
c.slot_data_bytes = slot_data_bytes;
|
||||
c.input_slot_data_bytes = input_slot_data_bytes;
|
||||
return detail::via_c_json([&](char *buf, int size) {
|
||||
return oakengine_ipc_handshake_to_json(&c, buf, size);
|
||||
});
|
||||
}
|
||||
|
||||
static bool from_json(const JsonMessage &o, HandshakeMsg *out)
|
||||
{
|
||||
oak_ipc_handshake c;
|
||||
if (!oakengine_ipc_handshake_parse(o.c_str(), &c)) {
|
||||
return false;
|
||||
}
|
||||
out->protocol_version = c.protocol_version;
|
||||
out->shm_key = c.shm_key;
|
||||
out->input_shm_key = c.input_shm_key;
|
||||
out->input_slots = c.input_slots;
|
||||
out->output_slots = c.output_slots;
|
||||
out->slot_data_bytes = c.slot_data_bytes;
|
||||
out->input_slot_data_bytes = c.input_slot_data_bytes;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
struct RenderFrameMsg {
|
||||
int64_t ticket_id =
|
||||
0; ///< Correlates this request with the eventual frame_ready.
|
||||
std::string
|
||||
node_uuid; ///< Output/viewer node to render, by stable uuid in the loaded graph.
|
||||
int64_t time_num = 0;
|
||||
int64_t time_den = 1;
|
||||
int width = 0; ///< Forced output size (0 = use graph default).
|
||||
int height = 0;
|
||||
int format = -1; ///< Forced PixelFormat::Format (-1 = default/INVALID).
|
||||
int channel_count = 0; ///< 0 = default.
|
||||
int mode = 0; ///< RenderMode::Mode.
|
||||
int input_slot =
|
||||
-1; ///< Optional main->worker decoded input slot for footage nodes.
|
||||
std::vector<int>
|
||||
input_slots; ///< Optional ordered decoded input slots for footage nodes.
|
||||
|
||||
// Output color transform to apply before returning the frame. When empty,
|
||||
// the worker returns the image in the project's reference space.
|
||||
bool has_color_transform = false;
|
||||
bool color_is_display = false;
|
||||
std::string color_output;
|
||||
std::string color_view;
|
||||
std::string color_look;
|
||||
|
||||
JsonMessage to_json() const
|
||||
{
|
||||
oak_ipc_render_frame c;
|
||||
c.ticket_id = ticket_id;
|
||||
detail::copy_str(node_uuid, c.node_uuid, sizeof(c.node_uuid));
|
||||
c.time_num = time_num;
|
||||
c.time_den = time_den;
|
||||
c.width = width;
|
||||
c.height = height;
|
||||
c.format = format;
|
||||
c.channel_count = channel_count;
|
||||
c.mode = mode;
|
||||
c.input_slot = input_slot;
|
||||
c.input_slot_count = std::min(int(input_slots.size()),
|
||||
OAK_IPC_INPUT_SLOTS_CAP);
|
||||
for (int i = 0; i < c.input_slot_count; i++) {
|
||||
c.input_slots[i] = input_slots.at(i);
|
||||
}
|
||||
c.has_color_transform = has_color_transform ? 1 : 0;
|
||||
c.color_is_display = color_is_display ? 1 : 0;
|
||||
detail::copy_str(color_output, c.color_output,
|
||||
sizeof(c.color_output));
|
||||
detail::copy_str(color_view, c.color_view, sizeof(c.color_view));
|
||||
detail::copy_str(color_look, c.color_look, sizeof(c.color_look));
|
||||
return detail::via_c_json([&](char *buf, int size) {
|
||||
return oakengine_ipc_render_frame_to_json(&c, buf, size);
|
||||
});
|
||||
}
|
||||
|
||||
static bool from_json(const JsonMessage &o, RenderFrameMsg *out)
|
||||
{
|
||||
oak_ipc_render_frame c;
|
||||
if (!oakengine_ipc_render_frame_parse(o.c_str(), &c)) {
|
||||
return false;
|
||||
}
|
||||
out->ticket_id = c.ticket_id;
|
||||
out->node_uuid = c.node_uuid;
|
||||
out->time_num = c.time_num;
|
||||
out->time_den = c.time_den;
|
||||
out->width = c.width;
|
||||
out->height = c.height;
|
||||
out->format = c.format;
|
||||
out->channel_count = c.channel_count;
|
||||
out->mode = c.mode;
|
||||
out->input_slot = c.input_slot;
|
||||
out->input_slots.clear();
|
||||
for (int i = 0; i < c.input_slot_count; i++) {
|
||||
out->input_slots.push_back(c.input_slots[i]);
|
||||
}
|
||||
out->has_color_transform = c.has_color_transform != 0;
|
||||
out->color_is_display = c.color_is_display != 0;
|
||||
out->color_output = c.color_output;
|
||||
out->color_view = c.color_view;
|
||||
out->color_look = c.color_look;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
struct FrameReadyMsg {
|
||||
int64_t ticket_id = 0;
|
||||
int output_slot = 0; ///< Index into the worker->main output FrameSlotPool.
|
||||
|
||||
JsonMessage to_json() const
|
||||
{
|
||||
oak_ipc_frame_ready c;
|
||||
c.ticket_id = ticket_id;
|
||||
c.output_slot = output_slot;
|
||||
return detail::via_c_json([&](char *buf, int size) {
|
||||
return oakengine_ipc_frame_ready_to_json(&c, buf, size);
|
||||
});
|
||||
}
|
||||
|
||||
static bool from_json(const JsonMessage &o, FrameReadyMsg *out)
|
||||
{
|
||||
oak_ipc_frame_ready c;
|
||||
if (!oakengine_ipc_frame_ready_parse(o.c_str(), &c)) {
|
||||
return false;
|
||||
}
|
||||
out->ticket_id = c.ticket_id;
|
||||
out->output_slot = c.output_slot;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
struct CancelMsg {
|
||||
int64_t ticket_id = 0;
|
||||
|
||||
JsonMessage to_json() const
|
||||
{
|
||||
oak_ipc_cancel c;
|
||||
c.ticket_id = ticket_id;
|
||||
return detail::via_c_json([&](char *buf, int size) {
|
||||
return oakengine_ipc_cancel_to_json(&c, buf, size);
|
||||
});
|
||||
}
|
||||
|
||||
static bool from_json(const JsonMessage &o, CancelMsg *out)
|
||||
{
|
||||
oak_ipc_cancel c;
|
||||
if (!oakengine_ipc_cancel_parse(o.c_str(), &c)) {
|
||||
return false;
|
||||
}
|
||||
out->ticket_id = c.ticket_id;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
struct LoadGraphMsg {
|
||||
std::string path; ///< Temporary file holding the serialized node graph.
|
||||
|
||||
JsonMessage to_json() const
|
||||
{
|
||||
oak_ipc_load_graph c;
|
||||
detail::copy_str(path, c.path, sizeof(c.path));
|
||||
return detail::via_c_json([&](char *buf, int size) {
|
||||
return oakengine_ipc_load_graph_to_json(&c, buf, size);
|
||||
});
|
||||
}
|
||||
|
||||
static bool from_json(const JsonMessage &o, LoadGraphMsg *out)
|
||||
{
|
||||
oak_ipc_load_graph c;
|
||||
if (!oakengine_ipc_load_graph_parse(o.c_str(), &c)) {
|
||||
return false;
|
||||
}
|
||||
out->path = c.path;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace ipc
|
||||
} // namespace olive
|
||||
|
||||
#endif // OAK_IPC_IPCMESSAGE_H
|
||||
@@ -0,0 +1,171 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak 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 OAK_IPC_SHAREDMEMORYREGION_H
|
||||
#define OAK_IPC_SHAREDMEMORYREGION_H
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include "oakengine/ipc.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
namespace ipc
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief A named, fixed-size shared memory segment mapped into the process address space.
|
||||
*
|
||||
* Consumer-side wrapper over the liboakengine C ABI: the object only holds an opaque
|
||||
* OakSharedMemoryRegion handle and forwards every call across the C boundary. The public API is
|
||||
* unchanged from the original implementation (QString -> std::string notwithstanding).
|
||||
*
|
||||
* One process open()s the segment with k_create (owner); the peer process open()s it by the same
|
||||
* key with k_attach. The mapping is a raw contiguous byte range accessible via data() — the IPC
|
||||
* ring buffers and frame slot pools are laid out inside it. Nothing here is locked;
|
||||
* synchronization is entirely the caller's responsibility via the lock-free structures placed in
|
||||
* the mapping.
|
||||
*/
|
||||
class SharedMemoryRegion {
|
||||
public:
|
||||
enum Mode {
|
||||
/// Create (and own) the segment. Fails if it already exists; unlinks on destruction.
|
||||
k_create = OAK_IPC_SHM_MODE_CREATE,
|
||||
/// Attach to a segment created by the peer. Does not unlink on destruction.
|
||||
k_attach = OAK_IPC_SHM_MODE_ATTACH
|
||||
};
|
||||
|
||||
SharedMemoryRegion()
|
||||
: handle_(oakengine_ipc_shm_create())
|
||||
{
|
||||
}
|
||||
|
||||
~SharedMemoryRegion()
|
||||
{
|
||||
oakengine_ipc_shm_free(handle_);
|
||||
}
|
||||
|
||||
SharedMemoryRegion(const SharedMemoryRegion &) = delete;
|
||||
SharedMemoryRegion &operator=(const SharedMemoryRegion &) = delete;
|
||||
|
||||
/**
|
||||
* @brief Open the segment identified by `key` with the given `size` in bytes.
|
||||
*
|
||||
* `key` is a short identifier (no leading slash needed; the platform prefix is added internally).
|
||||
* Returns true on success. On failure, error() carries a human-readable reason.
|
||||
*/
|
||||
bool open(const std::string &key, size_t size, Mode mode)
|
||||
{
|
||||
const bool ok = oakengine_ipc_shm_open(
|
||||
handle_, key.c_str(), size,
|
||||
static_cast<oak_ipc_shm_mode>(mode)) != 0;
|
||||
refresh_caches();
|
||||
return ok;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Unmap and (if owner) unlink the segment. Called automatically by the destructor.
|
||||
*/
|
||||
void close()
|
||||
{
|
||||
oakengine_ipc_shm_close(handle_);
|
||||
}
|
||||
|
||||
bool is_valid() const
|
||||
{
|
||||
return oakengine_ipc_shm_is_valid(handle_) != 0;
|
||||
}
|
||||
|
||||
void *data() const
|
||||
{
|
||||
return oakengine_ipc_shm_data(handle_);
|
||||
}
|
||||
|
||||
size_t size() const
|
||||
{
|
||||
return oakengine_ipc_shm_size(handle_);
|
||||
}
|
||||
|
||||
const std::string &key() const
|
||||
{
|
||||
return key_;
|
||||
}
|
||||
|
||||
const std::string &error() const
|
||||
{
|
||||
return error_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build a unique segment key for a worker, e.g. "olive-rw-<pid>-<index>".
|
||||
*
|
||||
* Centralized so the owner and the spawned worker agree on the same name.
|
||||
*/
|
||||
static std::string make_key(int64_t owner_pid, int worker_index)
|
||||
{
|
||||
const int size = oakengine_ipc_shm_make_key(owner_pid, worker_index,
|
||||
nullptr, 0);
|
||||
std::string buf(size + 1, '\0');
|
||||
oakengine_ipc_shm_make_key(owner_pid, worker_index, buf.data(),
|
||||
size + 1);
|
||||
buf.resize(size);
|
||||
return buf;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The wrapped C handle, for cross-type wrappers and direct C API use
|
||||
*/
|
||||
OakSharedMemoryRegion *handle() const
|
||||
{
|
||||
return handle_;
|
||||
}
|
||||
|
||||
private:
|
||||
static std::string query_string(int (*query)(const OakSharedMemoryRegion *,
|
||||
char *, int),
|
||||
const OakSharedMemoryRegion *handle)
|
||||
{
|
||||
const int size = query(handle, nullptr, 0);
|
||||
if (size <= 0) {
|
||||
return std::string();
|
||||
}
|
||||
std::string buf(size + 1, '\0');
|
||||
query(handle, buf.data(), size + 1);
|
||||
buf.resize(size);
|
||||
return buf;
|
||||
}
|
||||
|
||||
void refresh_caches()
|
||||
{
|
||||
key_ = query_string(oakengine_ipc_shm_key, handle_);
|
||||
error_ = query_string(oakengine_ipc_shm_error, handle_);
|
||||
}
|
||||
|
||||
OakSharedMemoryRegion *handle_;
|
||||
std::string key_;
|
||||
std::string error_;
|
||||
};
|
||||
|
||||
} // namespace ipc
|
||||
} // namespace olive
|
||||
|
||||
#endif // OAK_IPC_SHAREDMEMORYREGION_H
|
||||
@@ -0,0 +1,27 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "acceleratedjob.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_ACCELERATEDJOB_H
|
||||
#define OAK_ACCELERATEDJOB_H
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "param.h"
|
||||
#include "valuedatabase.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class AcceleratedJob {
|
||||
public:
|
||||
AcceleratedJob() = default;
|
||||
|
||||
virtual ~AcceleratedJob()
|
||||
{
|
||||
}
|
||||
|
||||
virtual NodeValue get(const std::string &input) const
|
||||
{
|
||||
auto it = value_map_.find(input);
|
||||
return it == value_map_.end() ? NodeValue() : it->second;
|
||||
}
|
||||
|
||||
virtual void insert(const std::string &input, const NodeValueRow &row)
|
||||
{
|
||||
// QHash::value() semantics: a missing key yields a default NodeValue
|
||||
auto it = row.find(input);
|
||||
value_map_[input] = it == row.end() ? NodeValue() : it->second;
|
||||
}
|
||||
|
||||
virtual void insert(const std::string &input, const NodeValue &value)
|
||||
{
|
||||
value_map_[input] = value;
|
||||
}
|
||||
|
||||
virtual void insert(const NodeValueRow &row)
|
||||
{
|
||||
value_map_.insert(row.begin(), row.end());
|
||||
}
|
||||
|
||||
virtual const NodeValueRow &get_values() const
|
||||
{
|
||||
return value_map_;
|
||||
}
|
||||
virtual NodeValueRow &get_values()
|
||||
{
|
||||
return value_map_;
|
||||
}
|
||||
|
||||
protected:
|
||||
NodeValueRow value_map_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_ACCELERATEDJOB_H
|
||||
@@ -0,0 +1,67 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_CACHEJOB_H
|
||||
#define OAK_CACHEJOB_H
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "value.h"
|
||||
#include "acceleratedjob.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class CacheJob : public AcceleratedJob {
|
||||
public:
|
||||
CacheJob() = default;
|
||||
CacheJob(const std::string &filename, const NodeValue &fallback = NodeValue())
|
||||
{
|
||||
filename_ = filename;
|
||||
}
|
||||
|
||||
const std::string &get_filename() const
|
||||
{
|
||||
return filename_;
|
||||
}
|
||||
void set_filename(const std::string &s)
|
||||
{
|
||||
filename_ = s;
|
||||
}
|
||||
|
||||
const NodeValue &get_fallback() const
|
||||
{
|
||||
return fallback_;
|
||||
}
|
||||
void set_fallback(const NodeValue &val)
|
||||
{
|
||||
fallback_ = val;
|
||||
}
|
||||
|
||||
private:
|
||||
std::string filename_;
|
||||
|
||||
NodeValue fallback_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_CACHEJOB_H
|
||||
@@ -0,0 +1,186 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_COLORTRANSFORMJOB_H
|
||||
#define OAK_COLORTRANSFORMJOB_H
|
||||
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
|
||||
#include "acceleratedjob.h"
|
||||
#include "alphaassoc.h"
|
||||
#include "colorprocessor.h"
|
||||
#include "mathtypes.h"
|
||||
#include "texture.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class Node;
|
||||
|
||||
class ColorTransformJob : public AcceleratedJob {
|
||||
public:
|
||||
ColorTransformJob()
|
||||
{
|
||||
processor_ = nullptr;
|
||||
custom_shader_src_ = nullptr;
|
||||
input_alpha_association_ = k_alpha_none;
|
||||
clear_destination_ = true;
|
||||
force_opaque_ = false;
|
||||
}
|
||||
|
||||
ColorTransformJob(const NodeValueRow &row)
|
||||
: ColorTransformJob()
|
||||
{
|
||||
insert(row);
|
||||
}
|
||||
|
||||
std::string id() const
|
||||
{
|
||||
if (id_.empty()) {
|
||||
return processor_->id();
|
||||
} else {
|
||||
return id_;
|
||||
}
|
||||
}
|
||||
|
||||
void set_override_id(const std::string &id)
|
||||
{
|
||||
id_ = id;
|
||||
}
|
||||
|
||||
const NodeValue &get_input_texture() const
|
||||
{
|
||||
return input_texture_;
|
||||
}
|
||||
void set_input_texture(const NodeValue &tex)
|
||||
{
|
||||
input_texture_ = tex;
|
||||
}
|
||||
void set_input_texture(TexturePtr tex)
|
||||
{
|
||||
assert(!tex->is_dummy());
|
||||
input_texture_ = NodeValue(NodeValue::k_texture, tex);
|
||||
}
|
||||
|
||||
ColorProcessorPtr get_color_processor() const
|
||||
{
|
||||
return processor_;
|
||||
}
|
||||
void set_color_processor(ColorProcessorPtr p)
|
||||
{
|
||||
processor_ = p;
|
||||
}
|
||||
|
||||
const AlphaAssociated &get_input_alpha_association() const
|
||||
{
|
||||
return input_alpha_association_;
|
||||
}
|
||||
void set_input_alpha_association(const AlphaAssociated &e)
|
||||
{
|
||||
input_alpha_association_ = e;
|
||||
}
|
||||
|
||||
const Node *custom_shader_source() const
|
||||
{
|
||||
return custom_shader_src_;
|
||||
}
|
||||
const std::string &custom_shader_id() const
|
||||
{
|
||||
return custom_shader_id_;
|
||||
}
|
||||
void set_needs_custom_shader(const Node *node,
|
||||
const std::string &id = std::string())
|
||||
{
|
||||
custom_shader_src_ = node;
|
||||
custom_shader_id_ = id;
|
||||
}
|
||||
|
||||
bool is_clear_destination_enabled() const
|
||||
{
|
||||
return clear_destination_;
|
||||
}
|
||||
void set_clear_destination_enabled(bool e)
|
||||
{
|
||||
clear_destination_ = e;
|
||||
}
|
||||
|
||||
const Matrix4x4 &get_transform_matrix() const
|
||||
{
|
||||
return matrix_;
|
||||
}
|
||||
void set_transform_matrix(const Matrix4x4 &m)
|
||||
{
|
||||
matrix_ = m;
|
||||
}
|
||||
|
||||
const Matrix4x4 &get_crop_matrix() const
|
||||
{
|
||||
return crop_matrix_;
|
||||
}
|
||||
void set_crop_matrix(const Matrix4x4 &m)
|
||||
{
|
||||
crop_matrix_ = m;
|
||||
}
|
||||
|
||||
const std::string &get_function_name() const
|
||||
{
|
||||
return function_name_;
|
||||
}
|
||||
void set_function_name(const std::string &function_name = std::string())
|
||||
{
|
||||
function_name_ = function_name;
|
||||
};
|
||||
|
||||
bool get_force_opaque() const
|
||||
{
|
||||
return force_opaque_;
|
||||
}
|
||||
void set_force_opaque(bool e)
|
||||
{
|
||||
force_opaque_ = e;
|
||||
}
|
||||
|
||||
private:
|
||||
ColorProcessorPtr processor_;
|
||||
std::string id_;
|
||||
|
||||
NodeValue input_texture_;
|
||||
|
||||
const Node *custom_shader_src_;
|
||||
std::string custom_shader_id_;
|
||||
|
||||
AlphaAssociated input_alpha_association_;
|
||||
|
||||
bool clear_destination_;
|
||||
|
||||
Matrix4x4 matrix_;
|
||||
|
||||
Matrix4x4 crop_matrix_;
|
||||
|
||||
std::string function_name_;
|
||||
|
||||
bool force_opaque_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_COLORTRANSFORMJOB_H
|
||||
@@ -0,0 +1,199 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_FOOTAGEJOB_H
|
||||
#define OAK_FOOTAGEJOB_H
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
#include "acceleratedjob.h"
|
||||
#include "loopmode.h"
|
||||
#include "rendermodes.h"
|
||||
#include "output/track/track.h"
|
||||
#include "project/footage/footage.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class FootageJob : public AcceleratedJob {
|
||||
public:
|
||||
FootageJob()
|
||||
: type_(Track::k_none)
|
||||
{
|
||||
}
|
||||
|
||||
FootageJob(const TimeRange &time, const std::string &decoder,
|
||||
const std::string &filename, Track::Type type,
|
||||
const Rational &length, LoopMode loop_mode)
|
||||
: time_(time)
|
||||
, decoder_(decoder)
|
||||
, filename_(filename)
|
||||
, type_(type)
|
||||
, length_(length)
|
||||
, loop_mode_(loop_mode)
|
||||
{
|
||||
}
|
||||
|
||||
const std::string &decoder() const
|
||||
{
|
||||
return decoder_;
|
||||
}
|
||||
|
||||
const std::string &filename() const
|
||||
{
|
||||
return filename_;
|
||||
}
|
||||
|
||||
bool has_proxy() const
|
||||
{
|
||||
return has_proxy_;
|
||||
}
|
||||
|
||||
const std::string &proxy_filename() const
|
||||
{
|
||||
return proxy_filename_;
|
||||
}
|
||||
|
||||
const std::string &proxy_decoder() const
|
||||
{
|
||||
return proxy_decoder_;
|
||||
}
|
||||
|
||||
int proxy_stream_index() const
|
||||
{
|
||||
return proxy_stream_index_;
|
||||
}
|
||||
|
||||
void set_proxy(const std::string &filename, const std::string &decoder,
|
||||
int stream_index)
|
||||
{
|
||||
proxy_filename_ = filename;
|
||||
proxy_decoder_ = decoder;
|
||||
proxy_stream_index_ = stream_index;
|
||||
has_proxy_ = !filename.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Whether decoding for the given render mode should use the proxy
|
||||
*
|
||||
* Proxies are a preview accelerator only: offline (realtime preview)
|
||||
* renders may decode from them, online (export/master) renders must
|
||||
* always decode the original media. The proxy file must also still
|
||||
* exist on disk, otherwise decoding falls back to the original.
|
||||
*/
|
||||
bool should_use_proxy(RenderMode::Mode mode) const
|
||||
{
|
||||
std::error_code ec;
|
||||
return mode == RenderMode::k_offline && has_proxy() &&
|
||||
std::filesystem::exists(proxy_filename_, ec);
|
||||
}
|
||||
|
||||
Track::Type type() const
|
||||
{
|
||||
return type_;
|
||||
}
|
||||
|
||||
const VideoParams &video_params() const
|
||||
{
|
||||
return video_params_;
|
||||
}
|
||||
|
||||
void set_video_params(const VideoParams &p)
|
||||
{
|
||||
video_params_ = p;
|
||||
}
|
||||
|
||||
const AudioParams &audio_params() const
|
||||
{
|
||||
return audio_params_;
|
||||
}
|
||||
|
||||
void set_audio_params(const AudioParams &p)
|
||||
{
|
||||
audio_params_ = p;
|
||||
}
|
||||
|
||||
const std::string &cache_path() const
|
||||
{
|
||||
return cache_path_;
|
||||
}
|
||||
|
||||
void set_cache_path(const std::string &p)
|
||||
{
|
||||
cache_path_ = p;
|
||||
}
|
||||
|
||||
const Rational &length() const
|
||||
{
|
||||
return length_;
|
||||
}
|
||||
|
||||
void set_length(const Rational &length)
|
||||
{
|
||||
length_ = length;
|
||||
}
|
||||
|
||||
const TimeRange &time() const
|
||||
{
|
||||
return time_;
|
||||
}
|
||||
|
||||
LoopMode loop_mode() const
|
||||
{
|
||||
return loop_mode_;
|
||||
}
|
||||
void set_loop_mode(LoopMode m)
|
||||
{
|
||||
loop_mode_ = m;
|
||||
}
|
||||
|
||||
private:
|
||||
TimeRange time_;
|
||||
|
||||
std::string decoder_;
|
||||
|
||||
std::string filename_;
|
||||
|
||||
bool has_proxy_ = false;
|
||||
|
||||
std::string proxy_filename_;
|
||||
|
||||
std::string proxy_decoder_;
|
||||
|
||||
int proxy_stream_index_ = -1;
|
||||
|
||||
Track::Type type_;
|
||||
|
||||
VideoParams video_params_;
|
||||
|
||||
AudioParams audio_params_;
|
||||
|
||||
std::string cache_path_;
|
||||
|
||||
Rational length_;
|
||||
|
||||
LoopMode loop_mode_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_FOOTAGEJOB_H
|
||||
@@ -0,0 +1,43 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_GENERATEJOB_H
|
||||
#define OAK_GENERATEJOB_H
|
||||
|
||||
#include "acceleratedjob.h"
|
||||
#include "codec/frame.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class GenerateJob : public AcceleratedJob {
|
||||
public:
|
||||
GenerateJob() = default;
|
||||
GenerateJob(const NodeValueRow &row)
|
||||
: GenerateJob()
|
||||
{
|
||||
insert(row);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_GENERATEJOB_H
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Oak Video Editor - Non-Linear Video Editor
|
||||
* Copyright (C) 2025 Olive CE 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 "pluginjob.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
namespace plugin
|
||||
{
|
||||
} // plugin
|
||||
} // olive
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Oak Video Editor - Non-Linear Video Editor
|
||||
* Copyright (C) 2025 Olive CE 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 OAK_PLUGINJOB_H
|
||||
#define OAK_PLUGINJOB_H
|
||||
|
||||
#include "acceleratedjob.h"
|
||||
#include "ofxhImageEffect.h"
|
||||
#include "plugins/plugin.h"
|
||||
#include "olive/core/util/rational.h"
|
||||
|
||||
#include <any>
|
||||
#include <chrono>
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
namespace olive
|
||||
{
|
||||
namespace plugin
|
||||
{
|
||||
|
||||
class PluginJob : public AcceleratedJob {
|
||||
public:
|
||||
explicit PluginJob(const OFX::Host::ImageEffect::Instance *plugin_instance,
|
||||
const PluginNode *node, NodeValueRow row,
|
||||
const olive::core::Rational &time)
|
||||
: AcceleratedJob()
|
||||
, time_seconds_(time.to_double())
|
||||
{
|
||||
this->pluginInstance_ = plugin_instance;
|
||||
this->node_ = node;
|
||||
insert(row);
|
||||
}
|
||||
explicit PluginJob(const OFX::Host::ImageEffect::Instance *plugin_instance,
|
||||
const PluginNode *node, NodeValueRow row)
|
||||
: PluginJob(plugin_instance, node, row, olive::core::Rational(0))
|
||||
{
|
||||
}
|
||||
|
||||
PluginNode *node() const
|
||||
{
|
||||
return const_cast<PluginNode *>(node_);
|
||||
}
|
||||
|
||||
OFX::Host::ImageEffect::Instance *plugin_instance()
|
||||
{
|
||||
return const_cast<OFX::Host::ImageEffect::Instance *>(pluginInstance_);
|
||||
}
|
||||
|
||||
double time_seconds() const
|
||||
{
|
||||
return time_seconds_;
|
||||
}
|
||||
|
||||
private:
|
||||
const OFX::Host::ImageEffect::Instance *pluginInstance_ = nullptr;
|
||||
|
||||
std::map<OfxTime, std::map<std::string, std::any>> paramsOnTime_;
|
||||
|
||||
std::map<std::string, std::any> params_;
|
||||
|
||||
const PluginNode *node_ = nullptr;
|
||||
double time_seconds_ = 0.0;
|
||||
};
|
||||
|
||||
} // plugin
|
||||
} // olive
|
||||
|
||||
#endif //OAK_PLUGINJOB_H
|
||||
@@ -0,0 +1,75 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_SAMPLEJOB_H
|
||||
#define OAK_SAMPLEJOB_H
|
||||
|
||||
#include "acceleratedjob.h"
|
||||
#include "olive/core/util/timerange.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
using core::TimeRange;
|
||||
|
||||
class SampleJob : public AcceleratedJob {
|
||||
public:
|
||||
SampleJob()
|
||||
{
|
||||
}
|
||||
|
||||
SampleJob(const TimeRange &time, const NodeValue &value)
|
||||
{
|
||||
samples_ = value.to_samples();
|
||||
time_ = time;
|
||||
}
|
||||
|
||||
SampleJob(const TimeRange &time, const std::string &from,
|
||||
const NodeValueRow &row)
|
||||
{
|
||||
samples_ = row.at(from).to_samples();
|
||||
time_ = time;
|
||||
}
|
||||
|
||||
const SampleBuffer &samples() const
|
||||
{
|
||||
return samples_;
|
||||
}
|
||||
|
||||
bool has_samples() const
|
||||
{
|
||||
return samples_.is_allocated();
|
||||
}
|
||||
|
||||
const TimeRange &time() const
|
||||
{
|
||||
return time_;
|
||||
}
|
||||
|
||||
private:
|
||||
SampleBuffer samples_;
|
||||
|
||||
TimeRange time_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_SAMPLEJOB_H
|
||||
@@ -0,0 +1,126 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_SHADERJOB_H
|
||||
#define OAK_SHADERJOB_H
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "acceleratedjob.h"
|
||||
#include "texture.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class ShaderJob : public AcceleratedJob {
|
||||
public:
|
||||
ShaderJob()
|
||||
{
|
||||
iterations_ = 1;
|
||||
}
|
||||
|
||||
ShaderJob(const NodeValueRow &row)
|
||||
: ShaderJob()
|
||||
{
|
||||
insert(row);
|
||||
}
|
||||
|
||||
const std::string &get_shader_id() const
|
||||
{
|
||||
return shader_id_;
|
||||
}
|
||||
|
||||
void set_shader_id(const std::string &id)
|
||||
{
|
||||
shader_id_ = id;
|
||||
}
|
||||
|
||||
void set_iterations(int iterations, const NodeInput &iterative_input)
|
||||
{
|
||||
set_iterations(iterations, iterative_input.input());
|
||||
}
|
||||
|
||||
void set_iterations(int iterations, const std::string &iterative_input)
|
||||
{
|
||||
iterations_ = iterations;
|
||||
iterative_input_ = iterative_input;
|
||||
}
|
||||
|
||||
int get_iteration_count() const
|
||||
{
|
||||
return iterations_;
|
||||
}
|
||||
|
||||
const std::string &get_iterative_input() const
|
||||
{
|
||||
return iterative_input_;
|
||||
}
|
||||
|
||||
Texture::Interpolation get_interpolation(const std::string &id) const
|
||||
{
|
||||
auto it = interpolation_.find(id);
|
||||
return it == interpolation_.end() ? Texture::k_default_interpolation :
|
||||
it->second;
|
||||
}
|
||||
|
||||
const std::map<std::string, Texture::Interpolation> &
|
||||
get_interpolation_map() const
|
||||
{
|
||||
return interpolation_;
|
||||
}
|
||||
|
||||
void set_interpolation(const NodeInput &input, Texture::Interpolation interp)
|
||||
{
|
||||
interpolation_[input.input()] = interp;
|
||||
}
|
||||
|
||||
void set_interpolation(const std::string &id, Texture::Interpolation interp)
|
||||
{
|
||||
interpolation_[id] = interp;
|
||||
}
|
||||
|
||||
void set_vertex_coordinates(const std::vector<float> &vertex_coords)
|
||||
{
|
||||
vertex_overrides_ = vertex_coords;
|
||||
}
|
||||
|
||||
const std::vector<float> &get_vertex_coordinates()
|
||||
{
|
||||
return vertex_overrides_;
|
||||
}
|
||||
|
||||
private:
|
||||
std::string shader_id_;
|
||||
|
||||
int iterations_;
|
||||
|
||||
std::string iterative_input_;
|
||||
|
||||
std::map<std::string, Texture::Interpolation> interpolation_;
|
||||
|
||||
std::vector<float> vertex_overrides_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_SHADERJOB_H
|
||||
@@ -0,0 +1,151 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak 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 "lutlibrary.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <filesystem>
|
||||
|
||||
#include "config/config.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
std::string trim(const std::string &s)
|
||||
{
|
||||
const char *ws = " \t\n\r";
|
||||
const std::string::size_type first = s.find_first_not_of(ws);
|
||||
if (first == std::string::npos) {
|
||||
return std::string();
|
||||
}
|
||||
const std::string::size_type last = s.find_last_not_of(ws);
|
||||
return s.substr(first, last - first + 1);
|
||||
}
|
||||
|
||||
std::vector<std::string> split_skip_empty(const std::string &s, char sep)
|
||||
{
|
||||
std::vector<std::string> parts;
|
||||
std::string::size_type start = 0;
|
||||
while (true) {
|
||||
const std::string::size_type pos = s.find(sep, start);
|
||||
const std::string part =
|
||||
s.substr(start, pos == std::string::npos ? pos : pos - start);
|
||||
if (!part.empty()) {
|
||||
parts.push_back(part);
|
||||
}
|
||||
if (pos == std::string::npos) {
|
||||
break;
|
||||
}
|
||||
start = pos + 1;
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<std::string> &LUTLibrary::supported_extensions()
|
||||
{
|
||||
// LUT formats OCIO FileTransform can load
|
||||
static const std::vector<std::string> extensions = {
|
||||
"cube", "3dl", "spi1d",
|
||||
"spi3d", "spimtx", "csp",
|
||||
"clf", "ctf", "cub",
|
||||
};
|
||||
return extensions;
|
||||
}
|
||||
|
||||
bool LUTLibrary::is_supported_extension(const std::string &suffix)
|
||||
{
|
||||
std::string s = suffix;
|
||||
if (!s.empty() && s.front() == '.') {
|
||||
s.erase(0, 1);
|
||||
}
|
||||
|
||||
std::transform(s.begin(), s.end(), s.begin(),
|
||||
[](unsigned char c) { return std::tolower(c); });
|
||||
|
||||
const std::vector<std::string> &exts = supported_extensions();
|
||||
return std::find(exts.begin(), exts.end(), s) != exts.end();
|
||||
}
|
||||
|
||||
std::vector<std::string> LUTLibrary::get_directories()
|
||||
{
|
||||
const std::string serialized = OAK_CONFIG("LUTLibraryPaths").toString();
|
||||
|
||||
std::vector<std::string> dirs = split_skip_empty(serialized, ';');
|
||||
for (std::string &dir : dirs) {
|
||||
// QDir::fromNativeSeparators is a no-op off Windows, so only the
|
||||
// trim from the original code remains here.
|
||||
dir = trim(dir);
|
||||
}
|
||||
return dirs;
|
||||
}
|
||||
|
||||
void LUTLibrary::set_directories(const std::vector<std::string> &dirs)
|
||||
{
|
||||
std::vector<std::string> cleaned;
|
||||
for (const std::string &dir : dirs) {
|
||||
const std::string trimmed = trim(dir);
|
||||
if (!trimmed.empty() &&
|
||||
std::find(cleaned.begin(), cleaned.end(), trimmed) ==
|
||||
cleaned.end()) {
|
||||
cleaned.push_back(trimmed);
|
||||
}
|
||||
}
|
||||
|
||||
std::string joined;
|
||||
for (size_t i = 0; i < cleaned.size(); i++) {
|
||||
if (i > 0) {
|
||||
joined += ';';
|
||||
}
|
||||
joined += cleaned[i];
|
||||
}
|
||||
|
||||
Config::current()["LUTLibraryPaths"] = joined;
|
||||
}
|
||||
|
||||
std::vector<std::string> LUTLibrary::get_lut_files()
|
||||
{
|
||||
std::vector<std::string> files;
|
||||
|
||||
for (const std::string &dir : get_directories()) {
|
||||
std::error_code ec;
|
||||
std::filesystem::recursive_directory_iterator it(
|
||||
dir, std::filesystem::directory_options::none, ec);
|
||||
const std::filesystem::recursive_directory_iterator end;
|
||||
for (; !ec && it != end; it.increment(ec)) {
|
||||
if (!it->is_regular_file(ec)) {
|
||||
continue;
|
||||
}
|
||||
// Mirrors the original QDir name filters "*.cube" / "*.3dl"
|
||||
// (case-sensitive).
|
||||
const std::string ext = it->path().extension().string();
|
||||
if (ext == ".cube" || ext == ".3dl") {
|
||||
files.push_back(it->path().string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak 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 OAK_LUTLIBRARY_H
|
||||
#define OAK_LUTLIBRARY_H
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief A global, user-configurable library of LUT files
|
||||
*
|
||||
* The library is a list of directories (stored in the application config
|
||||
* under "LUTLibraryPaths") that are scanned for supported LUT files. LUT
|
||||
* nodes can offer the library contents as quick picks instead of forcing
|
||||
* the user to browse for a file path on every node.
|
||||
*/
|
||||
class LUTLibrary {
|
||||
public:
|
||||
/**
|
||||
* @brief All LUT file extensions supported by the library
|
||||
*
|
||||
* Extensions OCIO FileTransform can load, lowercase, without the dot.
|
||||
*/
|
||||
static const std::vector<std::string> &supported_extensions();
|
||||
|
||||
/**
|
||||
* @brief Returns true if the given file suffix is a supported LUT
|
||||
* extension (case-insensitive, leading dot tolerated)
|
||||
*/
|
||||
static bool is_supported_extension(const std::string &suffix);
|
||||
|
||||
/**
|
||||
* @brief The directories that make up the LUT library
|
||||
*/
|
||||
static std::vector<std::string> get_directories();
|
||||
|
||||
/**
|
||||
* @brief Replaces the LUT library directories and saves them to the
|
||||
* application config
|
||||
*/
|
||||
static void set_directories(const std::vector<std::string> &dirs);
|
||||
|
||||
/**
|
||||
* @brief All supported LUT files found under the library directories
|
||||
*
|
||||
* Directories are scanned recursively. Files in earlier directories
|
||||
* are listed first.
|
||||
*/
|
||||
static std::vector<std::string> get_lut_files();
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_LUTLIBRARY_H
|
||||
@@ -0,0 +1,25 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
// ManagedColor has moved to application code
|
||||
// (app/widget/manageddisplay/colorprocessorhandle.h) as part of the C ABI
|
||||
// migration. This translation unit is intentionally left empty (the file is
|
||||
// kept so the existing build rules keep working).
|
||||
@@ -0,0 +1,31 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_MANAGEDCOLOR_H
|
||||
#define OAK_MANAGEDCOLOR_H
|
||||
|
||||
// ManagedColor has moved to application code
|
||||
// (app/widget/manageddisplay/colorprocessorhandle.h) as part of the C ABI
|
||||
// migration: it is a pure UI value type that the engine never uses. This
|
||||
// header is intentionally left empty (the file is kept so the existing
|
||||
// build rules keep working) and must not be included by new code.
|
||||
|
||||
#endif // OAK_MANAGEDCOLOR_H
|
||||
Executable
+401
@@ -0,0 +1,401 @@
|
||||
# Film Emulsion-like configuration for
|
||||
# Blender. Crafted by Troy James Sobotka with
|
||||
# special thanks, feedback, and knowledge from Guillermo
|
||||
# Espertino, Claudio Rocha, Bassam Kurdali, Eugenio
|
||||
# Pignataro, Henri Hebeisen, Jason Clarke,
|
||||
# Haarm-Peter Duiker, Thomas Mansencal, Andrew
|
||||
# Price, Nick Shaw, and Timothy
|
||||
# Lottes.
|
||||
|
||||
ocio_profile_version: 2
|
||||
|
||||
search_path: "luts:looks"
|
||||
strictparsing: true
|
||||
luma: [0.2126, 0.7152, 0.0722]
|
||||
|
||||
description: A filmlike dynamic range encoding set for Blender
|
||||
|
||||
roles:
|
||||
default: Rec.709 OETF
|
||||
reference: Linear
|
||||
scene_linear: Linear
|
||||
data: Non-Colour Data
|
||||
compositing_log: Filmic Log Encoding
|
||||
color_timing: Filmic Log Encoding
|
||||
default_byte: sRGB OETF
|
||||
default_float: Linear
|
||||
default_sequencer: sRGB OETF
|
||||
color_picking: sRGB OETF
|
||||
texture_paint: sRGB OETF
|
||||
matte_paint: Filmic Log Encoding
|
||||
cie_xyz_d65_interchange: CIE-XYZ D65
|
||||
|
||||
displays:
|
||||
sRGB:
|
||||
- !<View> {name: sRGB OETF, colorspace: sRGB OETF}
|
||||
- !<View> {name: Non-Colour Data, colorspace: Non-Colour Data}
|
||||
- !<View> {name: Linear Raw, colorspace: Linear}
|
||||
- !<View> {name: Filmic Log Encoding Base, colorspace: Filmic Log Encoding}
|
||||
BT.1886:
|
||||
- !<View> {name: BT.1886 EOTF, colorspace: BT.1886 EOTF}
|
||||
- !<View> {name: Non-Colour Data, colorspace: Non-Colour Data}
|
||||
- !<View> {name: Linear Raw, colorspace: Linear}
|
||||
- !<View> {name: Filmic Log Encoding Base, colorspace: BT.1886 Filmic Log Encoding}
|
||||
Apple Display P3:
|
||||
- !<View> {name: sRGB OETF, colorspace: AppleP3 sRGB OETF}
|
||||
- !<View> {name: Non-Colour Data, colorspace: Non-Colour Data}
|
||||
- !<View> {name: Linear Raw, colorspace: Linear}
|
||||
- !<View> {name: Filmic Log Encoding Base, colorspace: AppleP3 Filmic Log Encoding}
|
||||
|
||||
active_displays: [sRGB, BT.1886, Apple Display P3, None]
|
||||
#active_views: [Filmic Log Encoding Base, sRGB OETF, Non-Colour Data, Linear Raw, No View]
|
||||
|
||||
inactive_colorspaces: [CIE-XYZ D65]
|
||||
|
||||
colorspaces:
|
||||
- !<ColorSpace>
|
||||
name: Linear
|
||||
family:
|
||||
equalitygroup:
|
||||
bitdepth: 32f
|
||||
description: |
|
||||
ITU BT.709 primaries based scene referred linear space.
|
||||
isdata: false
|
||||
allocation: lg2
|
||||
allocationvars: [-12.4739311883, 12.5260688117, 0.00392156862]
|
||||
|
||||
- !<ColorSpace>
|
||||
name: CIE-XYZ D65
|
||||
family: display
|
||||
equalitygroup: ""
|
||||
bitdepth: 32f
|
||||
description: |
|
||||
Linear CIE XYZ space with D65 white point
|
||||
isdata: false
|
||||
allocation: lg2
|
||||
allocationvars: [-12.4739311883, 12.5260688117, 0.00392156862]
|
||||
to_reference: !<GroupTransform>
|
||||
children:
|
||||
- !<MatrixTransform> {matrix: [0.4124, 0.3576, 0.1805, 0, 0.2126, 0.7152, 0.0722, 0, 0.0193, 0.1192, 0.9505, 0, 0, 0, 0, 1], direction: inverse}
|
||||
|
||||
- !<ColorSpace>
|
||||
name: Filmic Log Encoding
|
||||
family:
|
||||
equalitygroup:
|
||||
bitdepth: 32f
|
||||
description: |
|
||||
Log based filmic shaper with 16.5 stops of latitude, and 25 stops of dynamic range.
|
||||
isdata: false
|
||||
allocation: lg2
|
||||
allocationvars: [-12.4739311883, 12.5260688117, 0.00392156862]
|
||||
from_reference: !<GroupTransform>
|
||||
children:
|
||||
- !<AllocationTransform> {allocation: lg2, vars: [-12.4739311883, 12.5260688117, 0.00392156862]}
|
||||
- !<FileTransform> {src: desat65cube.spi3d, interpolation: best}
|
||||
- !<AllocationTransform> {allocation: uniform, vars: [0, 0.66]}
|
||||
to_reference: !<AllocationTransform> {allocation: lg2, vars: [-12.4739311883, 4.02606881167, 0.00392156862], direction: inverse}
|
||||
|
||||
- !<ColorSpace>
|
||||
name: sRGB OETF
|
||||
family:
|
||||
equalitygroup:
|
||||
bitdepth: 32f
|
||||
description: |
|
||||
sRGB specification display referred Optical-Electro Transfer Function.
|
||||
isdata: false
|
||||
allocation: uniform
|
||||
allocationvars: [0.0, 1.0]
|
||||
to_reference: !<FileTransform> {src: sRGB_OETF_to_Linear.spi1d, interpolation: linear}
|
||||
|
||||
- !<ColorSpace>
|
||||
name: Apple DCI-P3 D65
|
||||
family: display
|
||||
equalitygroup: ""
|
||||
bitdepth: 32f
|
||||
isdata: false
|
||||
allocation: lg2
|
||||
allocationvars: [-12.4739311883, 12.5260688117, 0.00392156862]
|
||||
to_reference: !<GroupTransform>
|
||||
children:
|
||||
- !<MatrixTransform> {matrix: [0.515121, 0.291977, 0.157104, 0, 0.241196, 0.692245, 0.0665741, 0, -0.00105286, 0.0418854, 0.784073, 0, 0, 0, 0, 1]}
|
||||
- !<MatrixTransform> {matrix: [1.04788, 0.0229187, -0.0502014, 0, 0.0295868, 0.990479, -0.0170593, 0, -0.00923157, 0.0150757, 0.751678, 0, 0, 0, 0, 1], direction: inverse}
|
||||
- !<MatrixTransform> {matrix: [0.412391, 0.357584, 0.180481, 0, 0.212639, 0.715169, 0.0721923, 0, 0.0193308, 0.119195, 0.950532, 0, 0, 0, 0, 1], direction: inverse}
|
||||
|
||||
- !<ColorSpace>
|
||||
name: AppleP3 sRGB OETF
|
||||
family:
|
||||
equalitygroup:
|
||||
bitdepth: 32f
|
||||
description: |
|
||||
sRGB specification display referred Optical-Electro Transfer Function with Apple DCI-P3 primaries.
|
||||
isdata: false
|
||||
allocation: uniform
|
||||
allocationvars: [0.0, 1.0]
|
||||
to_reference: !<GroupTransform>
|
||||
children:
|
||||
- !<FileTransform> {src: sRGB_OETF_to_Linear.spi1d, interpolation: linear}
|
||||
- !<ColorSpaceTransform> {src: Apple DCI-P3 D65, dst: Linear}
|
||||
|
||||
- !<ColorSpace>
|
||||
name: BT.1886 EOTF
|
||||
family:
|
||||
equalitygroup:
|
||||
bitdepth: 32f
|
||||
description: |
|
||||
BT.1886 specification display referred Electro-Optical Transfer Function with REC.709 primaries.
|
||||
isdata: false
|
||||
allocation: uniform
|
||||
allocationvars: [0.0, 1.0]
|
||||
to_reference: !<ExponentTransform> {value: [2.4, 2.4, 2.4, 1.0]}
|
||||
|
||||
- !<ColorSpace>
|
||||
name: AppleP3 Filmic Log Encoding
|
||||
family:
|
||||
equalitygroup:
|
||||
bitdepth: 32f
|
||||
description: |
|
||||
Log based filmic shaper with 16.5 stops of latitude, and 25 stops of dynamic range with Apple P3 primaries.
|
||||
isdata: false
|
||||
allocation: lg2
|
||||
allocationvars: [-12.4739311883, 12.5260688117, 0.00392156862]
|
||||
from_reference: !<GroupTransform>
|
||||
children:
|
||||
- !<ColorSpaceTransform> {src: Linear, dst: Filmic Log Encoding}
|
||||
- !<ExponentTransform> {value: [2.2, 2.2, 2.2, 1.0]}
|
||||
- !<ColorSpaceTransform> {src: Linear, dst: Apple DCI-P3 D65}
|
||||
- !<ExponentTransform> {value: [2.2, 2.2, 2.2, 1.0], direction: inverse}
|
||||
to_reference: !<GroupTransform>
|
||||
children:
|
||||
- !<ExponentTransform> {value: [2.2, 2.2, 2.2, 1.0]}
|
||||
- !<ColorSpaceTransform> {src: Apple DCI-P3 D65, dst: Linear}
|
||||
- !<ExponentTransform> {value: [2.2, 2.2, 2.2, 1.0], direction: inverse}
|
||||
- !<AllocationTransform> {allocation: lg2, vars: [-12.4739311883, 4.02606881167, 0.00392156862], direction: inverse}
|
||||
|
||||
- !<ColorSpace>
|
||||
name: BT.1886 Filmic Log Encoding
|
||||
family:
|
||||
equalitygroup:
|
||||
bitdepth: 32f
|
||||
description: |
|
||||
Log based filmic shaper with 16.5 stops of latitude, and 25 stops of dynamic range with REC.709 primaries.
|
||||
isdata: false
|
||||
allocation: lg2
|
||||
allocationvars: [-12.4739311883, 12.5260688117, 0.00392156862]
|
||||
from_reference: !<GroupTransform>
|
||||
children:
|
||||
- !<ColorSpaceTransform> {src: Linear, dst: Filmic Log Encoding}
|
||||
- !<ExponentTransform> {value: [2.2, 2.2, 2.2, 1.0]}
|
||||
- !<ExponentTransform> {value: [2.4, 2.4, 2.4, 1.0], direction: inverse}
|
||||
to_reference: !<GroupTransform>
|
||||
children:
|
||||
- !<ExponentTransform> {value: [2.4, 2.4, 2.4, 1.0]}
|
||||
- !<ExponentTransform> {value: [2.2, 2.2, 2.2, 1.0], direction: inverse}
|
||||
- !<AllocationTransform> {allocation: lg2, vars: [-12.4739311883, 4.02606881167, 0.00392156862], direction: inverse}
|
||||
|
||||
- !<ColorSpace>
|
||||
name: Fuji F-Log OETF
|
||||
family: Camera Footage
|
||||
equalitygroup: ""
|
||||
bitdepth: 32f
|
||||
description: |
|
||||
Fuji F-Log transfer function
|
||||
isdata: false
|
||||
allocation: uniform
|
||||
allocationvars: [0, 1]
|
||||
to_reference: !<FileTransform> {src: F-Log_to_Linear.spi1d, interpolation: linear}
|
||||
|
||||
- !<ColorSpace>
|
||||
name: Fuji F-Log F-Gamut
|
||||
family: ""
|
||||
equalitygroup: ""
|
||||
bitdepth: 32f
|
||||
description: |
|
||||
Fuji F-Log / F-Gamut
|
||||
isdata: false
|
||||
allocation: uniform
|
||||
allocationvars: [0, 1]
|
||||
to_reference: !<GroupTransform>
|
||||
children:
|
||||
- !<ColorSpaceTransform> {src: Fuji F-Log OETF, dst: Linear}
|
||||
- !<MatrixTransform> {matrix: [0.636958048000, 0.144616904000, 0.168880975000, 0.000000000000, 0.262700212000, 0.677998072000, 0.059301716500, 0.000000000000, 4.994106570E-17, 0.028072693000, 1.060985060000, 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, 1.000000000000]}
|
||||
- !<MatrixTransform> {matrix: [0.412390800000, 0.357584340000, 0.180480790000, 0.000000000000, 0.212639010000, 0.715168680000, 0.072192320000, 0.000000000000, 0.019330820000, 0.119194780000, 0.950532150000, 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, 1.000000000000], direction: inverse}
|
||||
|
||||
- !<ColorSpace>
|
||||
name: Panasonic V-Log V-Gamut
|
||||
family: Camera Footage
|
||||
equalitygroup: ""
|
||||
bitdepth: 32f
|
||||
description: |
|
||||
Panasonic V-Log / V-Gamut
|
||||
isdata: false
|
||||
allocation: uniform
|
||||
allocationvars: [0, 1]
|
||||
to_reference: !<GroupTransform>
|
||||
children:
|
||||
- !<FileTransform> {src: V-Log_to_linear.spi1d, interpolation: linear}
|
||||
- !<MatrixTransform> {matrix: [1.806576, -0.695697, -0.110879, 0, -0.170090 , 1.305955, -0.135865, 0, -0.025206, -0.154468, 1.179674, 0, 0, 0, 0, 1]}
|
||||
|
||||
- !<ColorSpace>
|
||||
name: Arri Wide Gamut / LogC EI 800
|
||||
family: Camera Footage
|
||||
equalitygroup: ""
|
||||
bitdepth: 32f
|
||||
description: |
|
||||
Panasonic V-Log / V-Gamut
|
||||
isdata: false
|
||||
allocation: uniform
|
||||
allocationvars: [0, 1]
|
||||
to_reference: !<GroupTransform>
|
||||
children:
|
||||
- !<FileTransform> {src: V3_LogC_800_to_linear.spi1d, interpolation: linear}
|
||||
- !<MatrixTransform> {matrix: [1.617523, -0.537287, -0.080237, 0, -0.070573, 1.334613, -0.26404, 0, -0.021102, -0.226954, 1.248056, 0, 0, 0, 0, 1]}
|
||||
|
||||
- !<ColorSpace>
|
||||
name: Arri Wide Gamut / LogC EI 400
|
||||
family: Camera Footage
|
||||
equalitygroup: ""
|
||||
bitdepth: 32f
|
||||
description: |
|
||||
Panasonic V-Log / V-Gamut
|
||||
isdata: false
|
||||
allocation: uniform
|
||||
allocationvars: [0, 1]
|
||||
to_reference: !<GroupTransform>
|
||||
children:
|
||||
- !<FileTransform> {src: V3_LogC_400_to_linear.spi1d, interpolation: linear}
|
||||
- !<MatrixTransform> {matrix: [1.617523, -0.537287, -0.080237, 0, -0.070573, 1.334613, -0.26404, 0, -0.021102, -0.226954, 1.248056, 0, 0, 0, 0, 1]}
|
||||
|
||||
- !<ColorSpace>
|
||||
name: Arri Wide Gamut 4/ LogC4
|
||||
family: Camera Footage
|
||||
equalitygroup: ""
|
||||
bitdepth: 32f
|
||||
description: |
|
||||
Arri Wide Gamut 4 LogC4 input
|
||||
isdata: false
|
||||
allocation: uniform
|
||||
allocationvars: [0, 1]
|
||||
to_reference: !<GroupTransform>
|
||||
children:
|
||||
- !<LogCameraTransform> {log_side_slope: 0.0647954196341293, log_side_offset: -0.295908392682586, lin_side_slope: 2231.82630906769, lin_side_offset: 64, lin_side_break: -0.0180569961199113, direction: inverse}
|
||||
- !<MatrixTransform> {matrix: [1.893123, -0.780882, -0.112242, 0, -0.205700, 1.340257, -0.134557, 0, -0.012706, -0.152185, 1.164891, 0, 0, 0, 0, 1]}
|
||||
|
||||
- !<ColorSpace>
|
||||
name: Blackmagic Film Wide Gamut (Gen 5)
|
||||
family: Camera Footage
|
||||
equalitygroup: ""
|
||||
bitdepth: 32f
|
||||
description: |
|
||||
Blackmagic Film Wide Gamut (Gen 5)
|
||||
isdata: false
|
||||
allocation: uniform
|
||||
allocationvars: [0, 1]
|
||||
to_reference: !<GroupTransform>
|
||||
children:
|
||||
- !<FileTransform> {src: Blackmagic_FilmWideGamut_Gen5_to_linear.spi1d, interpolation: linear}
|
||||
- !<MatrixTransform> {matrix: [0.606530, 0.220408, 0.123479, 0, 0.267989, 0.832731, -0.100720, 0, -0.029442, -0.086611, 1.204861, 0, 0, 0, 0, 1]}
|
||||
- !<ColorSpaceTransform> {src: CIE-XYZ D65, dst: reference}
|
||||
|
||||
- !<ColorSpace>
|
||||
name: Rec.709 OETF
|
||||
family: Camera Footage
|
||||
equalitygroup: ""
|
||||
bitdepth: 32f
|
||||
description: |
|
||||
Rec.709 OETF
|
||||
isdata: false
|
||||
allocation: uniform
|
||||
allocationvars: [0, 1]
|
||||
to_reference: !<GroupTransform>
|
||||
children:
|
||||
- !<FileTransform> {src: rec709_to_linear.spi1d, interpolation: linear}
|
||||
|
||||
- !<ColorSpace>
|
||||
name: Rec.601 OETF (NTSC)
|
||||
family:
|
||||
equalitygroup:
|
||||
bitdepth: 32f
|
||||
description: |
|
||||
Rec.601 Optical-Electro Transfer Function.
|
||||
isdata: false
|
||||
allocation: uniform
|
||||
allocationvars: [0.0, 1.0]
|
||||
to_reference: !<GroupTransform>
|
||||
children:
|
||||
- !<MatrixTransform> {matrix: [0.939542, 0.050181, 0.010277, 0, 0.017772, 0.965793, 0.016435, 0, -0.001622, -0.004370, 1.005991, 0, 0, 0, 0, 1]}
|
||||
- !<ColorSpaceTransform> {src: Rec.709 OETF, dst: Linear}
|
||||
|
||||
- !<ColorSpace>
|
||||
name: Rec.601 OETF (PAL)
|
||||
family:
|
||||
equalitygroup:
|
||||
bitdepth: 32f
|
||||
description: |
|
||||
Rec.601 Optical-Electro Transfer Function.
|
||||
isdata: false
|
||||
allocation: uniform
|
||||
allocationvars: [0.0, 1.0]
|
||||
to_reference: !<GroupTransform>
|
||||
children:
|
||||
- !<MatrixTransform> {matrix: [1.044043, -0.044043, 0.000000, 0, 0.000000, 1.000000, -0.000000, 0, -0.000000, 0.011793, 0.988207, 0, 0, 0, 0, 1]}
|
||||
- !<ColorSpaceTransform> {src: Rec.709 OETF, dst: Linear}
|
||||
|
||||
- !<ColorSpace>
|
||||
name: Non-Colour Data
|
||||
family:
|
||||
description: |
|
||||
Transform to flag data as non-colour, strictly data, and avoid OCIO colour specific transforms.
|
||||
equalitygroup:
|
||||
bitdepth: 32f
|
||||
isdata: true
|
||||
allocation: uniform
|
||||
allocationvars: [0, 1]
|
||||
|
||||
looks:
|
||||
- !<Look>
|
||||
name: Greyscale
|
||||
process_space: Filmic Log Encoding
|
||||
transform: !<MatrixTransform> {matrix: [0.2126729, 0.7151521, 0.0721750, 0, 0.2126729, 0.7151521, 0.0721750, 0, 0.2126729, 0.7151521, 0.0721750, 0, 0, 0, 0, 1]}
|
||||
|
||||
- !<Look>
|
||||
name: False Colour
|
||||
process_space: Filmic Log Encoding
|
||||
transform: !<GroupTransform>
|
||||
children:
|
||||
- !<MatrixTransform> {matrix: [0.2126729, 0.7151521, 0.0721750, 0, 0.2126729, 0.7151521, 0.0721750, 0, 0.2126729, 0.7151521, 0.0721750, 0, 0, 0, 0, 1]}
|
||||
- !<FileTransform> {src: Filmic_False_Colour.spi3d, interpolation: best}
|
||||
|
||||
- !<Look>
|
||||
name: Very High Contrast
|
||||
process_space: Filmic Log Encoding
|
||||
transform: !<FileTransform> {src: Filmic_to_1.20_1-00.spi1d, interpolation: linear}
|
||||
|
||||
- !<Look>
|
||||
name: High Contrast
|
||||
process_space: Filmic Log Encoding
|
||||
transform: !<FileTransform> {src: Filmic_to_0.99_1-0075.spi1d, interpolation: linear}
|
||||
|
||||
- !<Look>
|
||||
name: Medium High Contrast
|
||||
process_space: Filmic Log Encoding
|
||||
transform: !<FileTransform> {src: Filmic_to_0-85_1-011.spi1d, interpolation: best}
|
||||
|
||||
- !<Look>
|
||||
name: Base Contrast
|
||||
process_space: Filmic Log Encoding
|
||||
transform: !<FileTransform> {src: Filmic_to_0-70_1-03.spi1d, interpolation: linear}
|
||||
|
||||
- !<Look>
|
||||
name: Medium Low Contrast
|
||||
process_space: Filmic Log Encoding
|
||||
transform: !<FileTransform> {src: Filmic_to_0-60_1-04.spi1d, interpolation: linear}
|
||||
|
||||
- !<Look>
|
||||
name: Low Contrast
|
||||
process_space: Filmic Log Encoding
|
||||
transform: !<FileTransform> {src: Filmic_to_0-48_1-09.spi1d, interpolation: linear}
|
||||
|
||||
- !<Look>
|
||||
name: Very Low Contrast
|
||||
process_space: Filmic Log Encoding
|
||||
transform: !<FileTransform> {src: Filmic_to_0-35_1-30.spi1d, interpolation: linear}
|
||||
+274628
File diff suppressed because it is too large
Load Diff
+4102
File diff suppressed because it is too large
Load Diff
+4102
File diff suppressed because it is too large
Load Diff
+4102
File diff suppressed because it is too large
Load Diff
+4102
File diff suppressed because it is too large
Load Diff
+4102
File diff suppressed because it is too large
Load Diff
+4102
File diff suppressed because it is too large
Load Diff
+4102
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+4102
File diff suppressed because it is too large
Load Diff
+4102
File diff suppressed because it is too large
Load Diff
+65542
File diff suppressed because it is too large
Load Diff
+65542
File diff suppressed because it is too large
Load Diff
+274628
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+4102
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
<RCC>
|
||||
<qresource prefix="/ocioconf">
|
||||
@QRC_BODY@
|
||||
</qresource>
|
||||
</RCC>
|
||||
@@ -0,0 +1,239 @@
|
||||
#include "render/backend/renderbackend_c.h"
|
||||
|
||||
#include "mathtypes.h"
|
||||
#include "variant.h"
|
||||
|
||||
#include "render/job/acceleratedjob.h"
|
||||
#include "render/opengl/openglrenderer.h"
|
||||
#include "render/shadercode.h"
|
||||
#include "render/texture.h"
|
||||
#include "videoparams.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
class BackendOpenGLRenderer : public olive::OpenGLRenderer {
|
||||
public:
|
||||
using olive::OpenGLRenderer::OpenGLRenderer;
|
||||
using olive::OpenGLRenderer::blit;
|
||||
using olive::OpenGLRenderer::create_native_texture;
|
||||
using olive::OpenGLRenderer::destroy_internal;
|
||||
using olive::OpenGLRenderer::destroy_native_texture;
|
||||
using olive::OpenGLRenderer::attach_texture_as_destination;
|
||||
using olive::OpenGLRenderer::detach_texture_as_destination;
|
||||
};
|
||||
|
||||
// Converts the opaque C ABI handle back to the C++ renderer used internally.
|
||||
BackendOpenGLRenderer *renderer(OakRenderBackendHandle handle)
|
||||
{
|
||||
return static_cast<BackendOpenGLRenderer *>(handle);
|
||||
}
|
||||
|
||||
// Interprets ABI Variant payloads without copying; both modules are built
|
||||
// against the same C++ ABI in this first-generation dynamic backend.
|
||||
const olive::Variant &variant_ref(const void *variant)
|
||||
{
|
||||
return *static_cast<const olive::Variant *>(variant);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Creates the backend object and returns it as an opaque C handle. The
|
||||
// QObject-style parent argument is retained for ABI parity and ignored.
|
||||
OAK_RENDER_BACKEND_EXPORT OakRenderBackendHandle
|
||||
oak_renderer_create(void *parent)
|
||||
{
|
||||
(void) parent;
|
||||
return new BackendOpenGLRenderer();
|
||||
}
|
||||
|
||||
// Destroys the opaque backend object created by oak_renderer_create().
|
||||
OAK_RENDER_BACKEND_EXPORT void
|
||||
oak_renderer_destroy(OakRenderBackendHandle handle)
|
||||
{
|
||||
delete renderer(handle);
|
||||
}
|
||||
|
||||
// Reports static OpenGL backend capabilities to the adapter.
|
||||
OAK_RENDER_BACKEND_EXPORT bool
|
||||
oak_renderer_get_info(OakRenderBackendHandle handle,
|
||||
OakRenderBackendInfo *out_info)
|
||||
{
|
||||
if (!handle || !out_info) {
|
||||
return false;
|
||||
}
|
||||
out_info->abi_version = 1;
|
||||
out_info->kind = oak_render_backend_opengl;
|
||||
out_info->capabilities =
|
||||
oak_render_backend_cap_textures | oak_render_backend_cap_shaders |
|
||||
oak_render_backend_cap_blit | oak_render_backend_cap_readback |
|
||||
oak_render_backend_cap_viewer_context;
|
||||
out_info->name = "opengl";
|
||||
out_info->status = "available";
|
||||
return true;
|
||||
}
|
||||
|
||||
// OpenGL availability is context-dependent, so object creation is the minimum
|
||||
// availability signal for this backend.
|
||||
OAK_RENDER_BACKEND_EXPORT bool
|
||||
oak_renderer_is_available(OakRenderBackendHandle handle)
|
||||
{
|
||||
return handle != nullptr;
|
||||
}
|
||||
|
||||
// Initializes an offscreen OpenGL context for non-viewer users.
|
||||
OAK_RENDER_BACKEND_EXPORT bool oak_renderer_init(OakRenderBackendHandle handle)
|
||||
{
|
||||
return renderer(handle)->init();
|
||||
}
|
||||
|
||||
// Initializes the backend against a caller-owned viewer OpenGL context.
|
||||
// `context` is an olive::OpenGLContext * adopted from the app layer.
|
||||
OAK_RENDER_BACKEND_EXPORT void
|
||||
oak_renderer_init_with_context(OakRenderBackendHandle handle, void *context)
|
||||
{
|
||||
renderer(handle)->init(static_cast<olive::OpenGLContext *>(context));
|
||||
}
|
||||
|
||||
// Runs renderer post-initialization once the GL context is available.
|
||||
OAK_RENDER_BACKEND_EXPORT void
|
||||
oak_renderer_post_init(OakRenderBackendHandle handle)
|
||||
{
|
||||
renderer(handle)->post_init();
|
||||
}
|
||||
|
||||
// Releases post-init OpenGL surface/context state.
|
||||
OAK_RENDER_BACKEND_EXPORT void
|
||||
oak_renderer_post_destroy(OakRenderBackendHandle handle)
|
||||
{
|
||||
renderer(handle)->post_destroy();
|
||||
}
|
||||
|
||||
// Releases renderer-owned GL resources before object destruction.
|
||||
OAK_RENDER_BACKEND_EXPORT void
|
||||
oak_renderer_destroy_internal(OakRenderBackendHandle handle)
|
||||
{
|
||||
renderer(handle)->destroy_internal();
|
||||
}
|
||||
|
||||
// Clears either the widget framebuffer or a texture destination.
|
||||
OAK_RENDER_BACKEND_EXPORT void
|
||||
oak_renderer_clear_destination(OakRenderBackendHandle handle, void *texture,
|
||||
double r, double g, double b, double a)
|
||||
{
|
||||
renderer(handle)->clear_destination(static_cast<olive::Texture *>(texture),
|
||||
r, g, b, a);
|
||||
}
|
||||
|
||||
// Creates an OpenGL texture and writes its Variant handle to out_variant.
|
||||
OAK_RENDER_BACKEND_EXPORT void oak_renderer_create_native_texture(
|
||||
OakRenderBackendHandle handle, int width, int height, int depth, int format,
|
||||
int channel_count, const void *data, int linesize, void *out_variant)
|
||||
{
|
||||
*static_cast<olive::Variant *>(out_variant) =
|
||||
renderer(handle)->create_native_texture(
|
||||
width, height, depth,
|
||||
static_cast<olive::PixelFormat::Format>(format), channel_count,
|
||||
data, linesize);
|
||||
}
|
||||
|
||||
// Destroys an OpenGL texture represented by a Variant handle.
|
||||
OAK_RENDER_BACKEND_EXPORT void
|
||||
oak_renderer_destroy_native_texture(OakRenderBackendHandle handle,
|
||||
const void *variant)
|
||||
{
|
||||
renderer(handle)->destroy_native_texture(variant_ref(variant));
|
||||
}
|
||||
|
||||
// Compiles an OpenGL shader program and returns its Variant handle.
|
||||
OAK_RENDER_BACKEND_EXPORT void
|
||||
oak_renderer_create_native_shader(OakRenderBackendHandle handle,
|
||||
const void *shader_code, void *out_variant)
|
||||
{
|
||||
*static_cast<olive::Variant *>(out_variant) =
|
||||
renderer(handle)->create_native_shader(
|
||||
*static_cast<const olive::ShaderCode *>(shader_code));
|
||||
}
|
||||
|
||||
// Destroys an OpenGL shader program represented by a Variant handle.
|
||||
OAK_RENDER_BACKEND_EXPORT void
|
||||
oak_renderer_destroy_native_shader(OakRenderBackendHandle handle,
|
||||
const void *variant)
|
||||
{
|
||||
renderer(handle)->destroy_native_shader(variant_ref(variant));
|
||||
}
|
||||
|
||||
// Uploads CPU pixel data into an OpenGL texture.
|
||||
OAK_RENDER_BACKEND_EXPORT void
|
||||
oak_renderer_upload_to_texture(OakRenderBackendHandle handle,
|
||||
const void *variant, const void *video_params,
|
||||
const void *data, int linesize)
|
||||
{
|
||||
renderer(handle)->upload_to_texture(
|
||||
variant_ref(variant),
|
||||
*static_cast<const olive::VideoParams *>(video_params), data, linesize);
|
||||
}
|
||||
|
||||
// Reads an OpenGL texture back to CPU memory.
|
||||
OAK_RENDER_BACKEND_EXPORT void oak_renderer_download_from_texture(
|
||||
OakRenderBackendHandle handle, const void *variant,
|
||||
const void *video_params, void *data, int linesize)
|
||||
{
|
||||
renderer(handle)->download_from_texture(
|
||||
variant_ref(variant),
|
||||
*static_cast<const olive::VideoParams *>(video_params), data, linesize);
|
||||
}
|
||||
|
||||
// Flushes/waits for pending OpenGL work as required by the renderer.
|
||||
OAK_RENDER_BACKEND_EXPORT void oak_renderer_flush(OakRenderBackendHandle handle)
|
||||
{
|
||||
renderer(handle)->flush();
|
||||
}
|
||||
|
||||
// Reads one pixel from an OpenGL texture.
|
||||
OAK_RENDER_BACKEND_EXPORT void
|
||||
oak_renderer_get_pixel_from_texture(OakRenderBackendHandle handle,
|
||||
void *texture, const void *point,
|
||||
void *out_color)
|
||||
{
|
||||
*static_cast<olive::Color *>(out_color) =
|
||||
renderer(handle)->get_pixel_from_texture(
|
||||
static_cast<olive::Texture *>(texture),
|
||||
*static_cast<const olive::PointF *>(point));
|
||||
}
|
||||
|
||||
// Executes a shader blit through the wrapped C++ OpenGL renderer.
|
||||
OAK_RENDER_BACKEND_EXPORT void oak_renderer_blit(OakRenderBackendHandle handle,
|
||||
const void *shader, void *job,
|
||||
void *destination,
|
||||
const void *destination_params,
|
||||
bool clear_destination)
|
||||
{
|
||||
renderer(handle)->blit(
|
||||
variant_ref(shader), *static_cast<olive::AcceleratedJob *>(job),
|
||||
static_cast<olive::Texture *>(destination),
|
||||
*static_cast<const olive::VideoParams *>(destination_params),
|
||||
clear_destination);
|
||||
}
|
||||
|
||||
// Exposes the wrapped OpenGL context for GL-specific integrations.
|
||||
OAK_RENDER_BACKEND_EXPORT void *
|
||||
oak_renderer_opengl_context(OakRenderBackendHandle handle)
|
||||
{
|
||||
return renderer(handle)->context();
|
||||
}
|
||||
|
||||
// Binds an output texture for OFX OpenGL rendering.
|
||||
OAK_RENDER_BACKEND_EXPORT void
|
||||
oak_renderer_attach_output_texture(OakRenderBackendHandle handle,
|
||||
const void *texture_id)
|
||||
{
|
||||
renderer(handle)->attach_texture_as_destination(variant_ref(texture_id));
|
||||
}
|
||||
|
||||
// Detaches any OFX OpenGL output texture binding.
|
||||
OAK_RENDER_BACKEND_EXPORT void
|
||||
oak_renderer_detach_output_texture(OakRenderBackendHandle handle)
|
||||
{
|
||||
renderer(handle)->detach_texture_as_destination();
|
||||
}
|
||||
@@ -0,0 +1,427 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "openglcontext.h"
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
#if defined(__APPLE__)
|
||||
#include <OpenGL/OpenGL.h>
|
||||
#elif defined(_WIN32)
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <EGL/egl.h>
|
||||
#endif
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
#if defined(__APPLE__)
|
||||
|
||||
static CGLPixelFormatObj create_cgl_pixel_format()
|
||||
{
|
||||
CGLPixelFormatAttribute attrs[] = {
|
||||
kCGLPFAOpenGLProfile,
|
||||
static_cast<CGLPixelFormatAttribute>(kCGLOGLPVersion_3_2_Core),
|
||||
kCGLPFAAccelerated,
|
||||
static_cast<CGLPixelFormatAttribute>(0),
|
||||
};
|
||||
CGLPixelFormatObj pix = nullptr;
|
||||
GLint npix = 0;
|
||||
if (CGLChoosePixelFormat(attrs, &pix, &npix) != kCGLNoError || !pix) {
|
||||
return nullptr;
|
||||
}
|
||||
return pix;
|
||||
}
|
||||
|
||||
OpenGLContext *OpenGLContext::create_offscreen(const OpenGLContext *share)
|
||||
{
|
||||
CGLPixelFormatObj pix = create_cgl_pixel_format();
|
||||
if (!pix) {
|
||||
fprintf(stderr, "OpenGLContext: failed to choose CGL pixel format\n");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
CGLContextObj share_ctx =
|
||||
share ? static_cast<CGLContextObj>(share->native_context_) : nullptr;
|
||||
CGLContextObj ctx = nullptr;
|
||||
if (CGLCreateContext(pix, share_ctx, &ctx) != kCGLNoError || !ctx) {
|
||||
CGLReleasePixelFormat(pix);
|
||||
fprintf(stderr, "OpenGLContext: failed to create CGL context\n");
|
||||
return nullptr;
|
||||
}
|
||||
CGLReleasePixelFormat(pix);
|
||||
|
||||
OpenGLContext *self = new OpenGLContext();
|
||||
self->owned_ = true;
|
||||
self->valid_ = true;
|
||||
self->major_version_ = 3;
|
||||
self->native_context_ = ctx;
|
||||
self->owner_thread_ = std::this_thread::get_id();
|
||||
return self;
|
||||
}
|
||||
|
||||
OpenGLContext *OpenGLContext::adopt_external(void *native_context,
|
||||
void *native_surface)
|
||||
{
|
||||
if (!native_context) {
|
||||
return nullptr;
|
||||
}
|
||||
OpenGLContext *self = new OpenGLContext();
|
||||
self->owned_ = false;
|
||||
self->valid_ = true;
|
||||
self->native_context_ = native_context;
|
||||
self->native_surface_ = native_surface;
|
||||
self->owner_thread_ = std::this_thread::get_id();
|
||||
return self;
|
||||
}
|
||||
|
||||
void OpenGLContext::destroy_native()
|
||||
{
|
||||
if (owned_ && native_context_) {
|
||||
CGLSetCurrentContext(nullptr);
|
||||
CGLReleaseContext(static_cast<CGLContextObj>(native_context_));
|
||||
}
|
||||
native_context_ = nullptr;
|
||||
}
|
||||
|
||||
bool OpenGLContext::make_current()
|
||||
{
|
||||
CGLContextObj ctx = static_cast<CGLContextObj>(native_context_);
|
||||
if (!ctx) {
|
||||
return false;
|
||||
}
|
||||
if (!owned_) {
|
||||
// External contexts are made current by their owner (app layer).
|
||||
return CGLGetCurrentContext() == ctx;
|
||||
}
|
||||
return CGLSetCurrentContext(ctx) == kCGLNoError;
|
||||
}
|
||||
|
||||
bool OpenGLContext::is_current() const
|
||||
{
|
||||
return native_context_ &&
|
||||
CGLGetCurrentContext() == static_cast<CGLContextObj>(native_context_);
|
||||
}
|
||||
|
||||
bool OpenGLContext::resolve_functions(OpenGLFunctions *out) const
|
||||
{
|
||||
return resolve_open_gl_functions(out, nullptr);
|
||||
}
|
||||
|
||||
#elif defined(_WIN32)
|
||||
|
||||
// WGL requires a current HDC to create an enhanced context; a hidden window
|
||||
// provides one for offscreen rendering.
|
||||
typedef HGLRC(WINAPI *PFN_wglCreateContextAttribsARB)(HDC, HGLRC, const int *);
|
||||
#ifndef WGL_CONTEXT_MAJOR_VERSION_ARB
|
||||
#define WGL_CONTEXT_MAJOR_VERSION_ARB 0x2091
|
||||
#define WGL_CONTEXT_MINOR_VERSION_ARB 0x2092
|
||||
#define WGL_CONTEXT_PROFILE_MASK_ARB 0x9126
|
||||
#define WGL_CONTEXT_CORE_PROFILE_BIT_ARB 0x00000001
|
||||
#endif
|
||||
|
||||
static void *wgl_get_proc(const char *name)
|
||||
{
|
||||
void *p = reinterpret_cast<void *>(wglGetProcAddress(name));
|
||||
if (!p) {
|
||||
static HMODULE gl_module = LoadLibraryA("opengl32.dll");
|
||||
p = reinterpret_cast<void *>(GetProcAddress(gl_module, name));
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
static LRESULT CALLBACK oak_wgl_wnd_proc(HWND hwnd, UINT msg, WPARAM wp,
|
||||
LPARAM lp)
|
||||
{
|
||||
return DefWindowProc(hwnd, msg, wp, lp);
|
||||
}
|
||||
|
||||
OpenGLContext *OpenGLContext::create_offscreen(const OpenGLContext *share)
|
||||
{
|
||||
static ATOM wnd_class = 0;
|
||||
if (!wnd_class) {
|
||||
WNDCLASSA wc = {};
|
||||
wc.lpfnWndProc = oak_wgl_wnd_proc;
|
||||
wc.hInstance = GetModuleHandle(nullptr);
|
||||
wc.lpszClassName = "OakOpenGLOffscreen";
|
||||
wnd_class = RegisterClassA(&wc);
|
||||
}
|
||||
HWND hwnd = CreateWindowExA(0, "OakOpenGLOffscreen", "", 0, 0, 0, 1, 1,
|
||||
nullptr, nullptr, GetModuleHandle(nullptr),
|
||||
nullptr);
|
||||
if (!hwnd) {
|
||||
return nullptr;
|
||||
}
|
||||
HDC hdc = GetDC(hwnd);
|
||||
|
||||
PIXELFORMATDESCRIPTOR pfd = {};
|
||||
pfd.nSize = sizeof(pfd);
|
||||
pfd.nVersion = 1;
|
||||
pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
|
||||
pfd.iPixelType = PFD_TYPE_RGBA;
|
||||
pfd.cColorBits = 32;
|
||||
pfd.iLayerType = PFD_MAIN_PLANE;
|
||||
int pf = ChoosePixelFormat(hdc, &pfd);
|
||||
if (!pf || !SetPixelFormat(hdc, pf, &pfd)) {
|
||||
DestroyWindow(hwnd);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
HGLRC bootstrap = wglCreateContext(hdc);
|
||||
if (!bootstrap) {
|
||||
DestroyWindow(hwnd);
|
||||
return nullptr;
|
||||
}
|
||||
wglMakeCurrent(hdc, bootstrap);
|
||||
|
||||
HGLRC ctx = bootstrap;
|
||||
auto create_attribs = reinterpret_cast<PFN_wglCreateContextAttribsARB>(
|
||||
wgl_get_proc("wglCreateContextAttribsARB"));
|
||||
if (create_attribs) {
|
||||
const int attrs[] = {
|
||||
WGL_CONTEXT_MAJOR_VERSION_ARB, 3,
|
||||
WGL_CONTEXT_MINOR_VERSION_ARB, 2,
|
||||
WGL_CONTEXT_PROFILE_MASK_ARB, WGL_CONTEXT_CORE_PROFILE_BIT_ARB,
|
||||
0,
|
||||
};
|
||||
HGLRC share_ctx =
|
||||
share ? static_cast<HGLRC>(share->native_context_) : nullptr;
|
||||
HGLRC modern = create_attribs(hdc, share_ctx, attrs);
|
||||
if (modern) {
|
||||
wglMakeCurrent(nullptr, nullptr);
|
||||
wglDeleteContext(bootstrap);
|
||||
ctx = modern;
|
||||
}
|
||||
} else if (share && share->native_context_) {
|
||||
wglShareLists(static_cast<HGLRC>(share->native_context_), ctx);
|
||||
}
|
||||
wglMakeCurrent(nullptr, nullptr);
|
||||
|
||||
OpenGLContext *self = new OpenGLContext();
|
||||
self->owned_ = true;
|
||||
self->valid_ = true;
|
||||
self->major_version_ = 3;
|
||||
self->native_context_ = ctx;
|
||||
self->native_surface_ = hdc;
|
||||
self->native_window_ = hwnd;
|
||||
self->owner_thread_ = std::this_thread::get_id();
|
||||
return self;
|
||||
}
|
||||
|
||||
OpenGLContext *OpenGLContext::adopt_external(void *native_context,
|
||||
void *native_surface)
|
||||
{
|
||||
if (!native_context) {
|
||||
return nullptr;
|
||||
}
|
||||
OpenGLContext *self = new OpenGLContext();
|
||||
self->owned_ = false;
|
||||
self->valid_ = true;
|
||||
self->native_context_ = native_context;
|
||||
self->native_surface_ = native_surface;
|
||||
self->owner_thread_ = std::this_thread::get_id();
|
||||
return self;
|
||||
}
|
||||
|
||||
void OpenGLContext::destroy_native()
|
||||
{
|
||||
if (owned_ && native_context_) {
|
||||
wglMakeCurrent(nullptr, nullptr);
|
||||
wglDeleteContext(static_cast<HGLRC>(native_context_));
|
||||
if (native_window_) {
|
||||
DestroyWindow(static_cast<HWND>(native_window_));
|
||||
}
|
||||
}
|
||||
native_context_ = nullptr;
|
||||
native_window_ = nullptr;
|
||||
}
|
||||
|
||||
bool OpenGLContext::make_current()
|
||||
{
|
||||
if (!native_context_) {
|
||||
return false;
|
||||
}
|
||||
if (!owned_) {
|
||||
return wglGetCurrentContext() ==
|
||||
static_cast<HGLRC>(native_context_);
|
||||
}
|
||||
return wglMakeCurrent(static_cast<HDC>(native_surface_),
|
||||
static_cast<HGLRC>(native_context_)) == TRUE;
|
||||
}
|
||||
|
||||
bool OpenGLContext::is_current() const
|
||||
{
|
||||
return native_context_ &&
|
||||
wglGetCurrentContext() == static_cast<HGLRC>(native_context_);
|
||||
}
|
||||
|
||||
bool OpenGLContext::resolve_functions(OpenGLFunctions *out) const
|
||||
{
|
||||
return resolve_open_gl_functions(out, wgl_get_proc);
|
||||
}
|
||||
|
||||
#else // Linux: EGL + pbuffer surface
|
||||
|
||||
static void *egl_get_proc(const char *name)
|
||||
{
|
||||
return reinterpret_cast<void *>(eglGetProcAddress(name));
|
||||
}
|
||||
|
||||
OpenGLContext *OpenGLContext::create_offscreen(const OpenGLContext *share)
|
||||
{
|
||||
EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
|
||||
if (display == EGL_NO_DISPLAY || !eglInitialize(display, nullptr, nullptr)) {
|
||||
fprintf(stderr, "OpenGLContext: failed to initialize EGL\n");
|
||||
return nullptr;
|
||||
}
|
||||
eglBindAPI(EGL_OPENGL_API);
|
||||
|
||||
const EGLint config_attrs[] = {
|
||||
EGL_SURFACE_TYPE, EGL_PBUFFER_BIT,
|
||||
EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT,
|
||||
EGL_NONE,
|
||||
};
|
||||
EGLConfig config = nullptr;
|
||||
EGLint num_configs = 0;
|
||||
if (!eglChooseConfig(display, config_attrs, &config, 1, &num_configs) ||
|
||||
num_configs < 1) {
|
||||
eglTerminate(display);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const EGLint pbuffer_attrs[] = {
|
||||
EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE,
|
||||
};
|
||||
EGLSurface surface =
|
||||
eglCreatePbufferSurface(display, config, pbuffer_attrs);
|
||||
if (surface == EGL_NO_SURFACE) {
|
||||
eglTerminate(display);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const EGLint ctx_attrs[] = {
|
||||
EGL_CONTEXT_MAJOR_VERSION, 3,
|
||||
EGL_CONTEXT_MINOR_VERSION, 2,
|
||||
EGL_CONTEXT_OPENGL_PROFILE_MASK, EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT,
|
||||
EGL_NONE,
|
||||
};
|
||||
EGLContext share_ctx = share ?
|
||||
static_cast<EGLContext>(share->native_context_) :
|
||||
EGL_NO_CONTEXT;
|
||||
EGLContext ctx =
|
||||
eglCreateContext(display, config, share_ctx, ctx_attrs);
|
||||
if (ctx == EGL_NO_CONTEXT) {
|
||||
// Fall back to a default-version context on drivers without 3.2 core.
|
||||
ctx = eglCreateContext(display, config, share_ctx, nullptr);
|
||||
}
|
||||
if (ctx == EGL_NO_CONTEXT) {
|
||||
eglDestroySurface(display, surface);
|
||||
eglTerminate(display);
|
||||
fprintf(stderr, "OpenGLContext: failed to create EGL context\n");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
OpenGLContext *self = new OpenGLContext();
|
||||
self->owned_ = true;
|
||||
self->valid_ = true;
|
||||
self->major_version_ = 3;
|
||||
self->native_context_ = ctx;
|
||||
self->native_surface_ = surface;
|
||||
self->native_display_ = display;
|
||||
self->owner_thread_ = std::this_thread::get_id();
|
||||
return self;
|
||||
}
|
||||
|
||||
OpenGLContext *OpenGLContext::adopt_external(void *native_context,
|
||||
void *native_surface)
|
||||
{
|
||||
if (!native_context) {
|
||||
return nullptr;
|
||||
}
|
||||
OpenGLContext *self = new OpenGLContext();
|
||||
self->owned_ = false;
|
||||
self->valid_ = true;
|
||||
self->native_context_ = native_context;
|
||||
self->native_surface_ = native_surface;
|
||||
self->native_display_ = eglGetCurrentDisplay();
|
||||
self->owner_thread_ = std::this_thread::get_id();
|
||||
return self;
|
||||
}
|
||||
|
||||
void OpenGLContext::destroy_native()
|
||||
{
|
||||
if (owned_ && native_context_) {
|
||||
EGLDisplay display = static_cast<EGLDisplay>(native_display_);
|
||||
eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE,
|
||||
EGL_NO_CONTEXT);
|
||||
eglDestroyContext(display, static_cast<EGLContext>(native_context_));
|
||||
if (native_surface_) {
|
||||
eglDestroySurface(display,
|
||||
static_cast<EGLSurface>(native_surface_));
|
||||
}
|
||||
}
|
||||
native_context_ = nullptr;
|
||||
native_surface_ = nullptr;
|
||||
}
|
||||
|
||||
bool OpenGLContext::make_current()
|
||||
{
|
||||
if (!native_context_) {
|
||||
return false;
|
||||
}
|
||||
if (!owned_) {
|
||||
return eglGetCurrentContext() ==
|
||||
static_cast<EGLContext>(native_context_);
|
||||
}
|
||||
return eglMakeCurrent(static_cast<EGLDisplay>(native_display_),
|
||||
static_cast<EGLSurface>(native_surface_),
|
||||
static_cast<EGLSurface>(native_surface_),
|
||||
static_cast<EGLContext>(native_context_)) == EGL_TRUE;
|
||||
}
|
||||
|
||||
bool OpenGLContext::is_current() const
|
||||
{
|
||||
return native_context_ &&
|
||||
eglGetCurrentContext() == static_cast<EGLContext>(native_context_);
|
||||
}
|
||||
|
||||
bool OpenGLContext::resolve_functions(OpenGLFunctions *out) const
|
||||
{
|
||||
return resolve_open_gl_functions(out, egl_get_proc);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
OpenGLContext::~OpenGLContext()
|
||||
{
|
||||
destroy_native();
|
||||
}
|
||||
|
||||
bool OpenGLContext::is_valid() const
|
||||
{
|
||||
return valid_;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_OPENGLCONTEXT_H
|
||||
#define OAK_OPENGLCONTEXT_H
|
||||
|
||||
// De-Qt replacement for QOpenGLContext/QOffscreenSurface.
|
||||
//
|
||||
// A thin abstraction over the platform-native GL context APIs:
|
||||
// - macOS: CGL (offscreen contexts need no drawable for FBO rendering)
|
||||
// - Linux: EGL with a pbuffer surface
|
||||
// - Windows: WGL with a hidden window providing the HDC
|
||||
//
|
||||
// The renderer only ever renders into FBOs, so no on-screen surface/window
|
||||
// integration lives here; presenting to a window remains an app-layer
|
||||
// responsibility (the app hands an externally-owned context to
|
||||
// OpenGLRenderer::init(OpenGLContext *) via adopt_external()).
|
||||
|
||||
#include <thread>
|
||||
|
||||
#include "openglfunctions.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class OpenGLContext {
|
||||
public:
|
||||
OpenGLContext(const OpenGLContext &) = delete;
|
||||
OpenGLContext &operator=(const OpenGLContext &) = delete;
|
||||
~OpenGLContext();
|
||||
|
||||
// Creates an owned offscreen context. `share` may be null; when given, the
|
||||
// new context joins the share group of that context (mirrors the former
|
||||
// QOpenGLContext::globalShareContext() behavior when the app passes its
|
||||
// global context down).
|
||||
static OpenGLContext *create_offscreen(const OpenGLContext *share = nullptr);
|
||||
|
||||
// Wraps an externally owned native context (e.g. a viewer context created
|
||||
// by the app layer). Non-owning: the caller guarantees the native context
|
||||
// outlives this wrapper and calls set_external_invalidated() (or simply
|
||||
// stops using the renderer) before destroying it — there is no QPointer
|
||||
// auto-nulling anymore.
|
||||
// native_context: CGLContextObj / EGLContext / HGLRC
|
||||
// native_surface: EGLSurface / HDC (unused on macOS, may be null)
|
||||
static OpenGLContext *adopt_external(void *native_context,
|
||||
void *native_surface = nullptr);
|
||||
|
||||
bool is_valid() const;
|
||||
bool is_owned() const
|
||||
{
|
||||
return owned_;
|
||||
}
|
||||
|
||||
// Makes this context current on the calling thread. For external contexts
|
||||
// this is a no-op that only reports whether the context is already current
|
||||
// (the app layer owns make-current for its own contexts).
|
||||
bool make_current();
|
||||
bool is_current() const;
|
||||
|
||||
// Desktop GL port: always false. Kept so shader preamble logic keeps its
|
||||
// original shape.
|
||||
bool is_open_gles() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// GL context major version where known (owned contexts), else 0.
|
||||
int major_version() const
|
||||
{
|
||||
return major_version_;
|
||||
}
|
||||
|
||||
// The thread this context is bound to (mirrors QOpenGLContext::thread()).
|
||||
// Owned contexts are bound at creation; rebind with set_owner_thread().
|
||||
std::thread::id owner_thread() const
|
||||
{
|
||||
return owner_thread_;
|
||||
}
|
||||
void set_owner_thread(std::thread::id id)
|
||||
{
|
||||
owner_thread_ = id;
|
||||
}
|
||||
|
||||
// Framebuffer to bind when no texture destination is attached (mirrors
|
||||
// QOpenGLContext::defaultFramebufferObject() for viewer contexts). Zero for
|
||||
// offscreen contexts; the app layer sets it for external viewer contexts.
|
||||
unsigned int default_framebuffer() const
|
||||
{
|
||||
return default_framebuffer_;
|
||||
}
|
||||
void set_default_framebuffer(unsigned int fbo)
|
||||
{
|
||||
default_framebuffer_ = fbo;
|
||||
}
|
||||
|
||||
// Platform native handles (CGLContextObj / EGLContext / HGLRC; null on
|
||||
// failure). Exposed for GL-specific integrations (OFX, debugging).
|
||||
void *native_context() const
|
||||
{
|
||||
return native_context_;
|
||||
}
|
||||
void *native_surface() const
|
||||
{
|
||||
return native_surface_;
|
||||
}
|
||||
|
||||
// Resolves this context's GL entry points into `out` (context must be
|
||||
// current or resolvable without a current context on the platform).
|
||||
bool resolve_functions(OpenGLFunctions *out) const;
|
||||
|
||||
private:
|
||||
OpenGLContext() = default;
|
||||
void destroy_native();
|
||||
|
||||
bool owned_ = false;
|
||||
bool valid_ = false;
|
||||
int major_version_ = 0;
|
||||
unsigned int default_framebuffer_ = 0;
|
||||
std::thread::id owner_thread_;
|
||||
void *native_context_ = nullptr;
|
||||
void *native_surface_ = nullptr;
|
||||
// Platform bookkeeping (EGLDisplay / HWND etc.), opaque here.
|
||||
[[maybe_unused]] void *native_display_ = nullptr;
|
||||
[[maybe_unused]] void *native_window_ = nullptr;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_OPENGLCONTEXT_H
|
||||
@@ -0,0 +1,17 @@
|
||||
#ifndef OAK_OPENGLCONTEXTPROVIDER_H
|
||||
#define OAK_OPENGLCONTEXTPROVIDER_H
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class OpenGLContext;
|
||||
|
||||
class OpenGLContextProvider {
|
||||
public:
|
||||
virtual ~OpenGLContextProvider() = default;
|
||||
virtual OpenGLContext *open_gl_context() const = 0;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_OPENGLCONTEXTPROVIDER_H
|
||||
@@ -0,0 +1,164 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "openglfunctions.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
bool resolve_open_gl_functions(OpenGLFunctions *f,
|
||||
void *(*get_proc)(const char *))
|
||||
{
|
||||
if (!f) {
|
||||
return false;
|
||||
}
|
||||
|
||||
#if defined(__APPLE__)
|
||||
// The OpenGL framework exports every GL 3.x core entry point as a real
|
||||
// symbol, so bind directly instead of going through a loader.
|
||||
(void) get_proc;
|
||||
f->glGetError = &::glGetError;
|
||||
f->glGenTextures = &::glGenTextures;
|
||||
f->glDeleteTextures = &::glDeleteTextures;
|
||||
f->glIsTexture = &::glIsTexture;
|
||||
f->glPixelStorei = &::glPixelStorei;
|
||||
f->glGetIntegerv = &::glGetIntegerv;
|
||||
f->glBindTexture = &::glBindTexture;
|
||||
f->glTexImage2D = &::glTexImage2D;
|
||||
f->glTexImage3D = &::glTexImage3D;
|
||||
f->glTexSubImage2D = &::glTexSubImage2D;
|
||||
f->glTexSubImage3D = &::glTexSubImage3D;
|
||||
f->glTexParameteri = &::glTexParameteri;
|
||||
f->glGenerateMipmap = &::glGenerateMipmap;
|
||||
f->glCreateProgram = &::glCreateProgram;
|
||||
f->glAttachShader = &::glAttachShader;
|
||||
f->glLinkProgram = &::glLinkProgram;
|
||||
f->glGetProgramiv = &::glGetProgramiv;
|
||||
f->glDeleteProgram = &::glDeleteProgram;
|
||||
f->glCreateShader = &::glCreateShader;
|
||||
f->glShaderSource = &::glShaderSource;
|
||||
f->glCompileShader = &::glCompileShader;
|
||||
f->glGetShaderiv = &::glGetShaderiv;
|
||||
f->glGetShaderInfoLog = &::glGetShaderInfoLog;
|
||||
f->glDeleteShader = &::glDeleteShader;
|
||||
f->glUseProgram = &::glUseProgram;
|
||||
f->glGetUniformLocation = &::glGetUniformLocation;
|
||||
f->glUniform1i = &::glUniform1i;
|
||||
f->glUniform1f = &::glUniform1f;
|
||||
f->glUniform4f = &::glUniform4f;
|
||||
f->glUniform2fv = &::glUniform2fv;
|
||||
f->glUniform3fv = &::glUniform3fv;
|
||||
f->glUniform4fv = &::glUniform4fv;
|
||||
f->glUniformMatrix4fv = &::glUniformMatrix4fv;
|
||||
f->glActiveTexture = &::glActiveTexture;
|
||||
f->glViewport = &::glViewport;
|
||||
f->glGetAttribLocation = &::glGetAttribLocation;
|
||||
f->glEnableVertexAttribArray = &::glEnableVertexAttribArray;
|
||||
f->glVertexAttribPointer = &::glVertexAttribPointer;
|
||||
f->glDrawArrays = &::glDrawArrays;
|
||||
f->glGenFramebuffers = &::glGenFramebuffers;
|
||||
f->glDeleteFramebuffers = &::glDeleteFramebuffers;
|
||||
f->glBindFramebuffer = &::glBindFramebuffer;
|
||||
f->glFramebufferTexture2D = &::glFramebufferTexture2D;
|
||||
f->glCheckFramebufferStatus = &::glCheckFramebufferStatus;
|
||||
f->glFinish = &::glFinish;
|
||||
f->glFlush = &::glFlush;
|
||||
f->glReadPixels = &::glReadPixels;
|
||||
f->glClearColor = &::glClearColor;
|
||||
f->glClear = &::glClear;
|
||||
f->glGenVertexArrays = &::glGenVertexArrays;
|
||||
f->glDeleteVertexArrays = &::glDeleteVertexArrays;
|
||||
f->glBindVertexArray = &::glBindVertexArray;
|
||||
f->glGenBuffers = &::glGenBuffers;
|
||||
f->glDeleteBuffers = &::glDeleteBuffers;
|
||||
f->glBindBuffer = &::glBindBuffer;
|
||||
f->glBufferData = &::glBufferData;
|
||||
return true;
|
||||
#else
|
||||
if (!get_proc) {
|
||||
return false;
|
||||
}
|
||||
bool ok = true;
|
||||
#define OAK_GL_RESOLVE(member) \
|
||||
f->member = reinterpret_cast<decltype(f->member)>(get_proc(#member)); \
|
||||
ok = ok && (f->member != nullptr)
|
||||
OAK_GL_RESOLVE(glGetError);
|
||||
OAK_GL_RESOLVE(glGenTextures);
|
||||
OAK_GL_RESOLVE(glDeleteTextures);
|
||||
OAK_GL_RESOLVE(glIsTexture);
|
||||
OAK_GL_RESOLVE(glPixelStorei);
|
||||
OAK_GL_RESOLVE(glGetIntegerv);
|
||||
OAK_GL_RESOLVE(glBindTexture);
|
||||
OAK_GL_RESOLVE(glTexImage2D);
|
||||
OAK_GL_RESOLVE(glTexImage3D);
|
||||
OAK_GL_RESOLVE(glTexSubImage2D);
|
||||
OAK_GL_RESOLVE(glTexSubImage3D);
|
||||
OAK_GL_RESOLVE(glTexParameteri);
|
||||
OAK_GL_RESOLVE(glGenerateMipmap);
|
||||
OAK_GL_RESOLVE(glCreateProgram);
|
||||
OAK_GL_RESOLVE(glAttachShader);
|
||||
OAK_GL_RESOLVE(glLinkProgram);
|
||||
OAK_GL_RESOLVE(glGetProgramiv);
|
||||
OAK_GL_RESOLVE(glDeleteProgram);
|
||||
OAK_GL_RESOLVE(glCreateShader);
|
||||
OAK_GL_RESOLVE(glShaderSource);
|
||||
OAK_GL_RESOLVE(glCompileShader);
|
||||
OAK_GL_RESOLVE(glGetShaderiv);
|
||||
OAK_GL_RESOLVE(glGetShaderInfoLog);
|
||||
OAK_GL_RESOLVE(glDeleteShader);
|
||||
OAK_GL_RESOLVE(glUseProgram);
|
||||
OAK_GL_RESOLVE(glGetUniformLocation);
|
||||
OAK_GL_RESOLVE(glUniform1i);
|
||||
OAK_GL_RESOLVE(glUniform1f);
|
||||
OAK_GL_RESOLVE(glUniform4f);
|
||||
OAK_GL_RESOLVE(glUniform2fv);
|
||||
OAK_GL_RESOLVE(glUniform3fv);
|
||||
OAK_GL_RESOLVE(glUniform4fv);
|
||||
OAK_GL_RESOLVE(glUniformMatrix4fv);
|
||||
OAK_GL_RESOLVE(glActiveTexture);
|
||||
OAK_GL_RESOLVE(glViewport);
|
||||
OAK_GL_RESOLVE(glGetAttribLocation);
|
||||
OAK_GL_RESOLVE(glEnableVertexAttribArray);
|
||||
OAK_GL_RESOLVE(glVertexAttribPointer);
|
||||
OAK_GL_RESOLVE(glDrawArrays);
|
||||
OAK_GL_RESOLVE(glGenFramebuffers);
|
||||
OAK_GL_RESOLVE(glDeleteFramebuffers);
|
||||
OAK_GL_RESOLVE(glBindFramebuffer);
|
||||
OAK_GL_RESOLVE(glFramebufferTexture2D);
|
||||
OAK_GL_RESOLVE(glCheckFramebufferStatus);
|
||||
OAK_GL_RESOLVE(glFinish);
|
||||
OAK_GL_RESOLVE(glFlush);
|
||||
OAK_GL_RESOLVE(glReadPixels);
|
||||
OAK_GL_RESOLVE(glClearColor);
|
||||
OAK_GL_RESOLVE(glClear);
|
||||
OAK_GL_RESOLVE(glGenVertexArrays);
|
||||
OAK_GL_RESOLVE(glDeleteVertexArrays);
|
||||
OAK_GL_RESOLVE(glBindVertexArray);
|
||||
OAK_GL_RESOLVE(glGenBuffers);
|
||||
OAK_GL_RESOLVE(glDeleteBuffers);
|
||||
OAK_GL_RESOLVE(glBindBuffer);
|
||||
OAK_GL_RESOLVE(glBufferData);
|
||||
#undef OAK_GL_RESOLVE
|
||||
return ok;
|
||||
#endif
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_OPENGLFUNCTIONS_H
|
||||
#define OAK_OPENGLFUNCTIONS_H
|
||||
|
||||
// De-Qt replacement for QOpenGLFunctions/QOpenGLExtraFunctions.
|
||||
//
|
||||
// GL types/enums come from the system GL headers; entry points are stored as
|
||||
// function pointer members so existing call sites of the form
|
||||
// `functions_->glFoo(...)` keep their exact shape. On Apple the pointers are
|
||||
// bound directly to the OpenGL framework symbols; elsewhere they are resolved
|
||||
// through the platform get-proc-address hook supplied by OpenGLContext.
|
||||
|
||||
#if defined(__APPLE__)
|
||||
#define GL_SILENCE_DEPRECATION
|
||||
#include <OpenGL/gl3.h>
|
||||
#else
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glext.h>
|
||||
#endif
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
struct OpenGLFunctions {
|
||||
GLenum (*glGetError)();
|
||||
void (*glGenTextures)(GLsizei, GLuint *);
|
||||
void (*glDeleteTextures)(GLsizei, const GLuint *);
|
||||
GLboolean (*glIsTexture)(GLuint);
|
||||
void (*glPixelStorei)(GLenum, GLint);
|
||||
void (*glGetIntegerv)(GLenum, GLint *);
|
||||
void (*glBindTexture)(GLenum, GLuint);
|
||||
void (*glTexImage2D)(GLenum, GLint, GLint, GLsizei, GLsizei, GLint, GLenum,
|
||||
GLenum, const void *);
|
||||
void (*glTexImage3D)(GLenum, GLint, GLint, GLsizei, GLsizei, GLsizei, GLint,
|
||||
GLenum, GLenum, const void *);
|
||||
void (*glTexSubImage2D)(GLenum, GLint, GLint, GLint, GLsizei, GLsizei,
|
||||
GLenum, GLenum, const void *);
|
||||
void (*glTexSubImage3D)(GLenum, GLint, GLint, GLint, GLint, GLsizei,
|
||||
GLsizei, GLsizei, GLenum, GLenum, const void *);
|
||||
void (*glTexParameteri)(GLenum, GLenum, GLint);
|
||||
void (*glGenerateMipmap)(GLenum);
|
||||
GLuint (*glCreateProgram)();
|
||||
void (*glAttachShader)(GLuint, GLuint);
|
||||
void (*glLinkProgram)(GLuint);
|
||||
void (*glGetProgramiv)(GLuint, GLenum, GLint *);
|
||||
void (*glDeleteProgram)(GLuint);
|
||||
GLuint (*glCreateShader)(GLenum);
|
||||
void (*glShaderSource)(GLuint, GLsizei, const char *const *, const GLint *);
|
||||
void (*glCompileShader)(GLuint);
|
||||
void (*glGetShaderiv)(GLuint, GLenum, GLint *);
|
||||
void (*glGetShaderInfoLog)(GLuint, GLsizei, GLsizei *, char *);
|
||||
void (*glDeleteShader)(GLuint);
|
||||
void (*glUseProgram)(GLuint);
|
||||
GLint (*glGetUniformLocation)(GLuint, const char *);
|
||||
void (*glUniform1i)(GLint, GLint);
|
||||
void (*glUniform1f)(GLint, GLfloat);
|
||||
void (*glUniform4f)(GLint, GLfloat, GLfloat, GLfloat, GLfloat);
|
||||
void (*glUniform2fv)(GLint, GLsizei, const GLfloat *);
|
||||
void (*glUniform3fv)(GLint, GLsizei, const GLfloat *);
|
||||
void (*glUniform4fv)(GLint, GLsizei, const GLfloat *);
|
||||
void (*glUniformMatrix4fv)(GLint, GLsizei, GLboolean, const GLfloat *);
|
||||
void (*glActiveTexture)(GLenum);
|
||||
void (*glViewport)(GLint, GLint, GLsizei, GLsizei);
|
||||
GLint (*glGetAttribLocation)(GLuint, const char *);
|
||||
void (*glEnableVertexAttribArray)(GLuint);
|
||||
void (*glVertexAttribPointer)(GLuint, GLint, GLenum, GLboolean, GLsizei,
|
||||
const void *);
|
||||
void (*glDrawArrays)(GLenum, GLint, GLsizei);
|
||||
void (*glGenFramebuffers)(GLsizei, GLuint *);
|
||||
void (*glDeleteFramebuffers)(GLsizei, const GLuint *);
|
||||
void (*glBindFramebuffer)(GLenum, GLuint);
|
||||
void (*glFramebufferTexture2D)(GLenum, GLenum, GLenum, GLuint, GLint);
|
||||
GLenum (*glCheckFramebufferStatus)(GLenum);
|
||||
void (*glFinish)();
|
||||
void (*glFlush)();
|
||||
void (*glReadPixels)(GLint, GLint, GLsizei, GLsizei, GLenum, GLenum,
|
||||
void *);
|
||||
void (*glClearColor)(GLfloat, GLfloat, GLfloat, GLfloat);
|
||||
void (*glClear)(GLbitfield);
|
||||
void (*glGenVertexArrays)(GLsizei, GLuint *);
|
||||
void (*glDeleteVertexArrays)(GLsizei, const GLuint *);
|
||||
void (*glBindVertexArray)(GLuint);
|
||||
void (*glGenBuffers)(GLsizei, GLuint *);
|
||||
void (*glDeleteBuffers)(GLsizei, const GLuint *);
|
||||
void (*glBindBuffer)(GLenum, GLuint);
|
||||
void (*glBufferData)(GLenum, GLsizeiptr, const void *, GLenum);
|
||||
};
|
||||
|
||||
// Resolves every entry point. When `get_proc` is null (Apple), pointers are
|
||||
// bound directly to the linked OpenGL framework symbols; otherwise each entry
|
||||
// is fetched through the platform get-proc-address callback. Returns false if
|
||||
// any entry point could not be resolved.
|
||||
bool resolve_open_gl_functions(OpenGLFunctions *functions,
|
||||
void *(*get_proc)(const char *name));
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_OPENGLFUNCTIONS_H
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,162 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_OPENGLRENDERER_H
|
||||
#define OAK_OPENGLRENDERER_H
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
#include "render/opengl/openglcontext.h"
|
||||
#include "render/opengl/openglcontextprovider.h"
|
||||
#include "render/opengl/openglfunctions.h"
|
||||
#include "render/renderer.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class OpenGLRenderer : public Renderer, public OpenGLContextProvider {
|
||||
public:
|
||||
OpenGLRenderer();
|
||||
|
||||
virtual ~OpenGLRenderer() override;
|
||||
|
||||
void init(OpenGLContext *existing_ctx);
|
||||
|
||||
virtual bool init() override;
|
||||
|
||||
virtual void post_destroy() override;
|
||||
|
||||
virtual void post_init() override;
|
||||
|
||||
virtual void clear_destination(olive::Texture *texture = nullptr,
|
||||
double r = 0.0, double g = 0.0,
|
||||
double b = 0.0, double a = 0.0) override;
|
||||
|
||||
virtual Variant create_native_shader(olive::ShaderCode code) override;
|
||||
|
||||
virtual void destroy_native_shader(Variant shader) override;
|
||||
|
||||
virtual void upload_to_texture(const Variant &handle,
|
||||
const VideoParams ¶ms, const void *data,
|
||||
int linesize) override;
|
||||
|
||||
virtual void download_from_texture(const Variant &handle,
|
||||
const VideoParams ¶ms, void *data,
|
||||
int linesize) override;
|
||||
|
||||
virtual void flush() override;
|
||||
|
||||
virtual Color get_pixel_from_texture(olive::Texture *texture,
|
||||
const PointF &pt) override;
|
||||
|
||||
OpenGLContext *context() const
|
||||
{
|
||||
return context_;
|
||||
}
|
||||
|
||||
virtual OpenGLContext *open_gl_context() const override
|
||||
{
|
||||
return context();
|
||||
}
|
||||
|
||||
virtual bool is_open_gl() const override
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual void attach_output_texture(olive::Texture *texture) override;
|
||||
|
||||
virtual void detach_output_texture() override;
|
||||
|
||||
bool ensure_context_current(const char *caller);
|
||||
|
||||
protected:
|
||||
virtual void blit(Variant shader, olive::AcceleratedJob &job,
|
||||
olive::Texture *destination,
|
||||
olive::VideoParams destination_params,
|
||||
bool clear_destination) override;
|
||||
|
||||
virtual Variant create_native_texture(int width, int height, int depth,
|
||||
PixelFormat format, int channel_count,
|
||||
const void *data = nullptr,
|
||||
int linesize = 0) override;
|
||||
|
||||
virtual void destroy_native_texture(Variant texture) override;
|
||||
|
||||
virtual void destroy_internal() override;
|
||||
|
||||
void attach_texture_as_destination(const Variant &texture);
|
||||
|
||||
void detach_texture_as_destination();
|
||||
|
||||
private:
|
||||
static GLint get_internal_format(PixelFormat format, int channel_layout);
|
||||
|
||||
static GLenum get_pixel_type(PixelFormat format);
|
||||
|
||||
static GLenum get_pixel_format(int channel_count);
|
||||
|
||||
void prepare_input_texture(GLenum target, Texture::Interpolation interp);
|
||||
|
||||
void clear_destination_internal(double r = 0.0, double g = 0.0,
|
||||
double b = 0.0, double a = 0.0);
|
||||
|
||||
GLuint compile_shader(GLenum type, const std::string &code);
|
||||
|
||||
// Viewer contexts are owned by the app layer and may be destroyed before
|
||||
// this renderer (e.g. when a viewer widget tears down its context). There
|
||||
// is no QPointer auto-nulling anymore: the app must call init(nullptr)
|
||||
// (or destroy the renderer) before tearing down an external context.
|
||||
OpenGLContext *context_;
|
||||
|
||||
// True when context_ was created by init() and is owned by this renderer.
|
||||
bool context_owned_;
|
||||
|
||||
OpenGLFunctions functions_;
|
||||
|
||||
bool functions_resolved_;
|
||||
|
||||
GLuint framebuffer_;
|
||||
|
||||
struct TextureCacheKey {
|
||||
int width;
|
||||
int height;
|
||||
int depth;
|
||||
PixelFormat format;
|
||||
int channel_count;
|
||||
|
||||
bool operator==(const TextureCacheKey &rhs) const
|
||||
{
|
||||
return width == rhs.width && height == rhs.height &&
|
||||
depth == rhs.depth && format == rhs.format &&
|
||||
channel_count == rhs.channel_count;
|
||||
}
|
||||
};
|
||||
|
||||
std::map<GLuint, TextureCacheKey> texture_params_;
|
||||
|
||||
static const int k_texture_cache_max_size;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_OPENGLRENDERER_H
|
||||
@@ -0,0 +1,93 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak 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 OAK_RENDER_PATHS_H
|
||||
#define OAK_RENDER_PATHS_H
|
||||
|
||||
// Qt-free replacements for QCoreApplication::applicationDirPath(),
|
||||
// applicationPid() and QDir::tempPath() used by the render core.
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <filesystem>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#if defined(__APPLE__)
|
||||
#include <mach-o/dyld.h>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
inline std::string application_dir_path()
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
char buf[MAX_PATH];
|
||||
const DWORD n = GetModuleFileNameA(nullptr, buf, MAX_PATH);
|
||||
if (n == 0) {
|
||||
return std::string();
|
||||
}
|
||||
return std::filesystem::path(std::string(buf, n)).parent_path().string();
|
||||
#elif defined(__APPLE__)
|
||||
uint32_t size = 0;
|
||||
_NSGetExecutablePath(nullptr, &size);
|
||||
std::string buf(size, '\0');
|
||||
if (_NSGetExecutablePath(buf.data(), &size) != 0) {
|
||||
return std::string();
|
||||
}
|
||||
std::error_code ec;
|
||||
const std::string resolved =
|
||||
std::filesystem::weakly_canonical(buf.c_str(), ec).string();
|
||||
const std::string &use = ec ? buf : resolved;
|
||||
return std::filesystem::path(use).parent_path().string();
|
||||
#else
|
||||
char buf[4096];
|
||||
const ssize_t n = readlink("/proc/self/exe", buf, sizeof(buf) - 1);
|
||||
if (n <= 0) {
|
||||
return std::string();
|
||||
}
|
||||
buf[n] = '\0';
|
||||
return std::filesystem::path(buf).parent_path().string();
|
||||
#endif
|
||||
}
|
||||
|
||||
inline int64_t application_pid()
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
return int64_t(GetCurrentProcessId());
|
||||
#else
|
||||
return int64_t(getpid());
|
||||
#endif
|
||||
}
|
||||
|
||||
inline std::string temp_dir_path()
|
||||
{
|
||||
std::error_code ec;
|
||||
const std::string p = std::filesystem::temp_directory_path(ec).string();
|
||||
return ec ? std::string("/tmp") : p;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_RENDER_PATHS_H
|
||||
@@ -0,0 +1,352 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "playbackcache.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <random>
|
||||
|
||||
#include "diskmanager.h"
|
||||
#include "filefunctions.h"
|
||||
#include "output/viewer/viewer.h"
|
||||
#include "project.h"
|
||||
#include "project/sequence/sequence.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// QUuid::createUuid() replacement: canonical "{8-4-4-4-12}" lowercase text
|
||||
// with version 4 and variant bits set (same text format QUuid::toString()
|
||||
// produced, keeping project files and cache directories compatible).
|
||||
std::string create_uuid_text()
|
||||
{
|
||||
static std::mt19937_64 rng(std::random_device{}());
|
||||
|
||||
uint8_t bytes[16];
|
||||
for (int i = 0; i < 16; i += 8) {
|
||||
uint64_t v = rng();
|
||||
for (int j = 0; j < 8; j++) {
|
||||
bytes[i + j] = uint8_t(v >> (j * 8));
|
||||
}
|
||||
}
|
||||
|
||||
// Version 4, variant 1 (same as QUuid::createUuid)
|
||||
bytes[6] = (bytes[6] & 0x0F) | 0x40;
|
||||
bytes[8] = (bytes[8] & 0x3F) | 0x80;
|
||||
|
||||
static const char k_hex[] = "0123456789abcdef";
|
||||
std::string out;
|
||||
out.reserve(38);
|
||||
out += '{';
|
||||
for (int i = 0; i < 16; i++) {
|
||||
if (i == 4 || i == 6 || i == 8 || i == 10) {
|
||||
out += '-';
|
||||
}
|
||||
out += k_hex[bytes[i] >> 4];
|
||||
out += k_hex[bytes[i] & 0xF];
|
||||
}
|
||||
out += '}';
|
||||
return out;
|
||||
}
|
||||
|
||||
// QFileInfo(f).lastModified().toMSecsSinceEpoch() replacement
|
||||
int64_t modification_time_msecs(const std::string &path)
|
||||
{
|
||||
std::error_code ec;
|
||||
auto t = std::filesystem::last_write_time(path, ec);
|
||||
if (ec) {
|
||||
return 0;
|
||||
}
|
||||
auto sys = std::chrono::time_point_cast<std::chrono::milliseconds>(
|
||||
t - std::filesystem::file_time_type::clock::now() +
|
||||
std::chrono::system_clock::now());
|
||||
return sys.time_since_epoch().count();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void PlaybackCache::invalidate(const TimeRange &r)
|
||||
{
|
||||
if (r.in() == r.out()) {
|
||||
fprintf(stderr, "Tried to invalidate zero-length range\n");
|
||||
return;
|
||||
}
|
||||
|
||||
validated_.remove(r);
|
||||
|
||||
if (!passthroughs_.empty()) {
|
||||
TimeRangeList::util_remove(&passthroughs_, r);
|
||||
}
|
||||
|
||||
InvalidateEvent(r);
|
||||
|
||||
// Was `emit invalidated(r)`
|
||||
if (invalidated_callback_) {
|
||||
invalidated_callback_(r);
|
||||
}
|
||||
|
||||
if (saving_enabled_) {
|
||||
save_state();
|
||||
}
|
||||
}
|
||||
|
||||
std::string PlaybackCache::get_this_cache_directory() const
|
||||
{
|
||||
return get_this_cache_directory(get_cache_directory(), get_uuid());
|
||||
}
|
||||
|
||||
std::string
|
||||
PlaybackCache::get_this_cache_directory(const std::string &cache_path,
|
||||
const std::string &cache_id)
|
||||
{
|
||||
return (std::filesystem::path(cache_path) / cache_id).string();
|
||||
}
|
||||
|
||||
void PlaybackCache::load_state()
|
||||
{
|
||||
std::string cache_dir = get_this_cache_directory();
|
||||
std::string state_path = (std::filesystem::path(cache_dir) / "state").string();
|
||||
|
||||
std::error_code ec;
|
||||
if (!std::filesystem::exists(state_path, ec)) {
|
||||
// No state exists, assume nothing valid
|
||||
validated_.clear();
|
||||
passthroughs_.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
int64_t file_time = modification_time_msecs(state_path);
|
||||
std::FILE *f = file_time > last_loaded_state_ ? std::fopen(state_path.c_str(), "rb") :
|
||||
nullptr;
|
||||
if (f) {
|
||||
BinaryStreamReader s(f);
|
||||
|
||||
uint32_t version;
|
||||
s >> version;
|
||||
|
||||
LoadStateEvent(s);
|
||||
|
||||
switch (version) {
|
||||
case 1: {
|
||||
int32_t valid_count, pass_count;
|
||||
|
||||
s >> valid_count;
|
||||
for (int32_t i = 0; i < valid_count; i++) {
|
||||
int32_t in_num, in_den, out_num, out_den;
|
||||
|
||||
s >> in_num;
|
||||
s >> in_den;
|
||||
s >> out_num;
|
||||
s >> out_den;
|
||||
|
||||
validated_.insert(TimeRange(Rational(in_num, in_den),
|
||||
Rational(out_num, out_den)));
|
||||
}
|
||||
|
||||
s >> pass_count;
|
||||
for (int32_t i = 0; i < pass_count; i++) {
|
||||
int32_t in_num, in_den, out_num, out_den;
|
||||
|
||||
s >> in_num;
|
||||
s >> in_den;
|
||||
s >> out_num;
|
||||
s >> out_den;
|
||||
|
||||
Passthrough p = TimeRange(Rational(in_num, in_den),
|
||||
Rational(out_num, out_den));
|
||||
p.cache = s.read_uuid_text();
|
||||
passthroughs_.push_back(p);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
std::fclose(f);
|
||||
|
||||
last_loaded_state_ = file_time;
|
||||
}
|
||||
}
|
||||
|
||||
void PlaybackCache::save_state()
|
||||
{
|
||||
if (!DiskManager::instance()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string cache_dir = get_this_cache_directory();
|
||||
std::string state_path = (std::filesystem::path(cache_dir) / "state").string();
|
||||
if (validated_.isEmpty() && passthroughs_.empty()) {
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(state_path, ec);
|
||||
} else {
|
||||
if (FileFunctions::directory_is_valid(cache_dir)) {
|
||||
std::FILE *f = std::fopen(state_path.c_str(), "wb");
|
||||
if (f) {
|
||||
BinaryStreamWriter s(f);
|
||||
|
||||
uint32_t version = 1;
|
||||
s << version;
|
||||
|
||||
SaveStateEvent(s);
|
||||
|
||||
// Using "int" for backwards compatibility with when we used QVector, could potentially overflow
|
||||
s << int32_t(validated_.size());
|
||||
|
||||
for (const TimeRange &r : validated_) {
|
||||
s << int32_t(r.in().numerator());
|
||||
s << int32_t(r.in().denominator());
|
||||
s << int32_t(r.out().numerator());
|
||||
s << int32_t(r.out().denominator());
|
||||
}
|
||||
|
||||
// Using "int" for backwards compatibility with when we used QVector, could potentially overflow
|
||||
s << int32_t(passthroughs_.size());
|
||||
|
||||
for (const Passthrough &p : passthroughs_) {
|
||||
s << int32_t(p.in().numerator());
|
||||
s << int32_t(p.in().denominator());
|
||||
s << int32_t(p.out().numerator());
|
||||
s << int32_t(p.out().denominator());
|
||||
s.write_uuid_text(p.cache);
|
||||
}
|
||||
|
||||
std::fclose(f);
|
||||
|
||||
last_loaded_state_ = modification_time_msecs(state_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PlaybackCache::set_passthrough(PlaybackCache *cache)
|
||||
{
|
||||
for (const TimeRange &r : cache->get_validated_ranges()) {
|
||||
Passthrough p = r;
|
||||
p.cache = cache->get_uuid();
|
||||
passthroughs_.push_back(p);
|
||||
}
|
||||
|
||||
passthroughs_.insert(passthroughs_.end(), cache->get_passthroughs().begin(),
|
||||
cache->get_passthroughs().end());
|
||||
|
||||
if (saving_enabled_) {
|
||||
save_state();
|
||||
}
|
||||
}
|
||||
|
||||
void PlaybackCache::invalidate_all()
|
||||
{
|
||||
invalidate(TimeRange(0, RATIONAL_MAX));
|
||||
}
|
||||
|
||||
void PlaybackCache::request(ViewerOutput *context, const TimeRange &r)
|
||||
{
|
||||
request_context_ = context;
|
||||
requested_.insert(r);
|
||||
|
||||
if (requested_callback_) {
|
||||
requested_callback_(request_context_, r);
|
||||
}
|
||||
}
|
||||
|
||||
void PlaybackCache::validate(const TimeRange &r, bool signal)
|
||||
{
|
||||
validated_.insert(r);
|
||||
|
||||
// Was `emit validated(r)` (suppressed when signal == false)
|
||||
if (signal && validated_callback_) {
|
||||
validated_callback_(r);
|
||||
}
|
||||
|
||||
if (saving_enabled_) {
|
||||
save_state();
|
||||
}
|
||||
}
|
||||
|
||||
void PlaybackCache::InvalidateEvent(const TimeRange &)
|
||||
{
|
||||
}
|
||||
|
||||
Project *PlaybackCache::get_project() const
|
||||
{
|
||||
return Project::get_project_from_object(parent_);
|
||||
}
|
||||
|
||||
PlaybackCache::PlaybackCache(Node *parent)
|
||||
: saving_enabled_(true)
|
||||
, last_loaded_state_(0)
|
||||
, parent_(parent)
|
||||
{
|
||||
uuid_ = create_uuid_text();
|
||||
}
|
||||
|
||||
void PlaybackCache::set_uuid(const std::string &u)
|
||||
{
|
||||
uuid_ = u;
|
||||
|
||||
load_state();
|
||||
}
|
||||
|
||||
TimeRangeList PlaybackCache::get_invalidated_ranges(TimeRange intersecting) const
|
||||
{
|
||||
TimeRangeList invalidated;
|
||||
|
||||
// Prevent TimeRange from being below 0, some other behavior in Olive relies on this behavior
|
||||
// and it seemed reasonable to have safety code in here
|
||||
intersecting.set_out(std::max(Rational(0), intersecting.out()));
|
||||
intersecting.set_in(std::max(Rational(0), intersecting.in()));
|
||||
|
||||
invalidated.insert(intersecting);
|
||||
|
||||
for (const TimeRange &range : validated_) {
|
||||
invalidated.remove(range);
|
||||
}
|
||||
|
||||
for (const TimeRange &range : passthroughs_) {
|
||||
invalidated.remove(range);
|
||||
}
|
||||
|
||||
return invalidated;
|
||||
}
|
||||
|
||||
bool PlaybackCache::has_invalidated_ranges(const TimeRange &intersecting) const
|
||||
{
|
||||
return !validated_.contains(intersecting);
|
||||
}
|
||||
|
||||
std::string PlaybackCache::get_cache_directory() const
|
||||
{
|
||||
Project *project = get_project();
|
||||
|
||||
if (project) {
|
||||
return project->cache_path();
|
||||
} else {
|
||||
return DiskManager::instance()->get_default_cache_path();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_PLAYBACKCACHE_H
|
||||
#define OAK_PLAYBACKCACHE_H
|
||||
|
||||
#include <olive/core/core.h>
|
||||
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "binarystream.h"
|
||||
#include "common/jobtime.h"
|
||||
|
||||
using namespace olive::core;
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class Node;
|
||||
class Project;
|
||||
class ViewerOutput;
|
||||
|
||||
class PlaybackCache {
|
||||
public:
|
||||
PlaybackCache(Node *parent = nullptr);
|
||||
|
||||
virtual ~PlaybackCache() = default;
|
||||
|
||||
// Cache UUIDs are stored as canonical QUuid text ("{8-4-4-4-12}", lowercase
|
||||
// hex) so project files stay compatible.
|
||||
const std::string &get_uuid() const
|
||||
{
|
||||
return uuid_;
|
||||
}
|
||||
void set_uuid(const std::string &u);
|
||||
|
||||
TimeRangeList get_invalidated_ranges(TimeRange intersecting) const;
|
||||
TimeRangeList get_invalidated_ranges(const Rational &length) const
|
||||
{
|
||||
return get_invalidated_ranges(TimeRange(0, length));
|
||||
}
|
||||
|
||||
bool has_invalidated_ranges(const TimeRange &intersecting) const;
|
||||
bool has_invalidated_ranges(const Rational &length) const
|
||||
{
|
||||
return has_invalidated_ranges(TimeRange(0, length));
|
||||
}
|
||||
|
||||
std::string get_cache_directory() const;
|
||||
|
||||
void invalidate(const TimeRange &r);
|
||||
|
||||
bool has_validated_ranges() const
|
||||
{
|
||||
return !validated_.isEmpty();
|
||||
}
|
||||
const TimeRangeList &get_validated_ranges() const
|
||||
{
|
||||
return validated_;
|
||||
}
|
||||
|
||||
Node *parent() const
|
||||
{
|
||||
return parent_;
|
||||
}
|
||||
|
||||
std::string get_this_cache_directory() const;
|
||||
static std::string get_this_cache_directory(const std::string &cache_path,
|
||||
const std::string &cache_id);
|
||||
|
||||
void load_state();
|
||||
void save_state();
|
||||
|
||||
// Formerly QFontMetrics(QFont()).height() / 4 with the default application
|
||||
// font. The font metric moved to the app layer; the engine reports a fixed
|
||||
// indicator height (M7: oakrender_cache_indicator_height is a constant
|
||||
// query).
|
||||
static int get_cache_indicator_height()
|
||||
{
|
||||
return 4;
|
||||
}
|
||||
|
||||
bool is_saving_enabled() const
|
||||
{
|
||||
return saving_enabled_;
|
||||
}
|
||||
void set_saving_enabled(bool e)
|
||||
{
|
||||
saving_enabled_ = e;
|
||||
}
|
||||
|
||||
virtual void set_passthrough(PlaybackCache *cache);
|
||||
|
||||
std::mutex &mutex()
|
||||
{
|
||||
return mutex_;
|
||||
}
|
||||
|
||||
class Passthrough : public TimeRange {
|
||||
public:
|
||||
Passthrough(const TimeRange &r)
|
||||
: TimeRange(r)
|
||||
{
|
||||
}
|
||||
|
||||
// UUID text of the cache this range passes through to
|
||||
std::string cache;
|
||||
};
|
||||
|
||||
const std::vector<Passthrough> &get_passthroughs() const
|
||||
{
|
||||
return passthroughs_;
|
||||
}
|
||||
|
||||
void clear_request_range(const TimeRange &r)
|
||||
{
|
||||
requested_.remove(r);
|
||||
}
|
||||
|
||||
// Explicit callbacks replacing the Qt signals. PreviewAutoCacher (render
|
||||
// module) registers for `requested`/`cancel_all`; cross-layer
|
||||
// (`invalidated`/`validated`) notification is re-emitted by the facade
|
||||
// after the triggering command (M7 cache.h: no cache events held
|
||||
// cross-layer).
|
||||
using InvalidatedCallback = std::function<void(const TimeRange &r)>;
|
||||
using ValidatedCallback = std::function<void(const TimeRange &r)>;
|
||||
using RequestedCallback =
|
||||
std::function<void(ViewerOutput *context, const TimeRange &r)>;
|
||||
using CancelAllCallback = std::function<void()>;
|
||||
|
||||
void set_invalidated_callback(InvalidatedCallback callback)
|
||||
{
|
||||
invalidated_callback_ = std::move(callback);
|
||||
}
|
||||
void set_validated_callback(ValidatedCallback callback)
|
||||
{
|
||||
validated_callback_ = std::move(callback);
|
||||
}
|
||||
void set_requested_callback(RequestedCallback callback)
|
||||
{
|
||||
requested_callback_ = std::move(callback);
|
||||
}
|
||||
void set_cancel_all_callback(CancelAllCallback callback)
|
||||
{
|
||||
cancel_all_callback_ = std::move(callback);
|
||||
}
|
||||
|
||||
// Fires the cancel_all callback (was `emit cancel_all()` at call sites in
|
||||
// node/app code; now called by whoever owns that action).
|
||||
void emit_cancel_all()
|
||||
{
|
||||
if (cancel_all_callback_) {
|
||||
cancel_all_callback_();
|
||||
}
|
||||
}
|
||||
|
||||
void resignal_requests()
|
||||
{
|
||||
// Iterate over a copy: handlers may call clear_request_range() and
|
||||
// mutate requested_ while we're iterating it.
|
||||
const TimeRangeList requests = requested_;
|
||||
for (const TimeRange &r : requests) {
|
||||
if (requested_callback_) {
|
||||
requested_callback_(request_context_, r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Formerly slots; now called directly by whoever owns the notification
|
||||
// (facade / PreviewAutoCacher wave).
|
||||
void invalidate_all();
|
||||
|
||||
void request(ViewerOutput *context, const TimeRange &r);
|
||||
|
||||
protected:
|
||||
void validate(const TimeRange &r, bool signal = true);
|
||||
|
||||
virtual void InvalidateEvent(const TimeRange &range);
|
||||
|
||||
virtual void LoadStateEvent(BinaryStreamReader &stream)
|
||||
{
|
||||
(void) stream;
|
||||
}
|
||||
|
||||
virtual void SaveStateEvent(BinaryStreamWriter &stream)
|
||||
{
|
||||
(void) stream;
|
||||
}
|
||||
|
||||
Project *get_project() const;
|
||||
|
||||
private:
|
||||
TimeRangeList validated_;
|
||||
|
||||
TimeRangeList requested_;
|
||||
ViewerOutput *request_context_;
|
||||
|
||||
std::string uuid_;
|
||||
|
||||
bool saving_enabled_;
|
||||
|
||||
std::mutex mutex_;
|
||||
|
||||
std::vector<Passthrough> passthroughs_;
|
||||
|
||||
int64_t last_loaded_state_;
|
||||
|
||||
Node *parent_;
|
||||
|
||||
InvalidatedCallback invalidated_callback_;
|
||||
ValidatedCallback validated_callback_;
|
||||
RequestedCallback requested_callback_;
|
||||
CancelAllCallback cancel_all_callback_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_PLAYBACKCACHE_H
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Oak Video Editor - Non-Linear Video Editor
|
||||
* Copyright (C) 2025 Olive CE 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
//
|
||||
// Created by mikesolar on 25-10-19.
|
||||
//
|
||||
|
||||
#ifndef OAK_PLUGINRENDERER_H
|
||||
#define OAK_PLUGINRENDERER_H
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
#include "render/renderer.h"
|
||||
#include "render/job/pluginjob.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
namespace plugin
|
||||
{
|
||||
namespace detail
|
||||
{
|
||||
// 作用:将字节行跨度转换为像素跨度,便于纹理读写。
|
||||
// Purpose: Convert byte stride to pixel stride for texture I/O.
|
||||
int bytes_to_pixels(int byte_linesize, const olive::VideoParams ¶ms);
|
||||
}
|
||||
// 作用:OFX 插件渲染器,负责 CPU/GL 路径下的插件调用和纹理桥接。
|
||||
// Purpose: OFX plugin renderer that drives CPU/GL render paths and texture bridging.
|
||||
//
|
||||
// 不再继承 OpenGLRenderer,而是持有一个通用的 Renderer 指针。这样当主渲染器
|
||||
// 是 Vulkan 或动态加载的后端时,插件仍可通过 CPU readback/upload 路径工作;
|
||||
// 仅当底层渲染器真正支持 OpenGL 时才走 OFX OpenGL 渲染路径。
|
||||
class PluginRenderer {
|
||||
public:
|
||||
// 渲染线程内发生的用户可见插件错误(原实现弹 QMessageBox 并 undo)。
|
||||
// UI 交互属 app 层:facade 安装此回调把消息投递到 GUI 线程;
|
||||
// 默认 nullptr = 仅写日志(同原 QCoreApplication::instance()==null 的行为)。
|
||||
// Purpose: user-visible plugin error hook (was QMessageBox + undo). Installed
|
||||
// by the facade; nullptr means log-only.
|
||||
using ErrorCallback = std::function<void(const std::string &message)>;
|
||||
|
||||
explicit PluginRenderer(olive::Renderer *renderer)
|
||||
: renderer_(renderer)
|
||||
{
|
||||
}
|
||||
virtual ~PluginRenderer()
|
||||
{
|
||||
}
|
||||
|
||||
olive::Renderer *renderer() const
|
||||
{
|
||||
return renderer_;
|
||||
}
|
||||
|
||||
void set_error_callback(ErrorCallback cb)
|
||||
{
|
||||
error_callback_ = std::move(cb);
|
||||
}
|
||||
const ErrorCallback &error_callback() const
|
||||
{
|
||||
return error_callback_;
|
||||
}
|
||||
|
||||
// 作用:将目标纹理绑定为插件输出。
|
||||
// Purpose: Attach destination texture as OFX output.
|
||||
void attach_output_texture(olive::TexturePtr texture);
|
||||
// 作用:解除目标纹理绑定。
|
||||
// Purpose: Detach destination texture binding.
|
||||
void detach_output_texture();
|
||||
// 作用:执行插件渲染流程(参数配置、输入/输出、调用渲染动作)。
|
||||
// Purpose: Execute plugin render flow (params, inputs/outputs, render actions).
|
||||
void render_plugin(TexturePtr src, olive::plugin::PluginJob &job,
|
||||
olive::TexturePtr destination,
|
||||
olive::VideoParams destination_params,
|
||||
bool clear_destination, bool interactive);
|
||||
|
||||
private:
|
||||
olive::Renderer *renderer_;
|
||||
|
||||
ErrorCallback error_callback_;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#endif //OAK_PLUGINRENDERER_H
|
||||
@@ -0,0 +1,96 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "previewaudiodevice.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
PreviewAudioDevice::PreviewAudioDevice()
|
||||
: bytes_per_frame_(0)
|
||||
, notify_interval_(0)
|
||||
, bytes_read_(0)
|
||||
{
|
||||
}
|
||||
|
||||
PreviewAudioDevice::~PreviewAudioDevice() = default;
|
||||
|
||||
void PreviewAudioDevice::set_params(const core::AudioParams ¶ms)
|
||||
{
|
||||
set_bytes_per_frame(params.samples_to_bytes(1));
|
||||
}
|
||||
|
||||
int64_t PreviewAudioDevice::read(char *data, int64_t max_size)
|
||||
{
|
||||
bool notify = false;
|
||||
int64_t copy_length;
|
||||
{
|
||||
std::lock_guard<std::mutex> locker(lock_);
|
||||
|
||||
copy_length = std::min(max_size, int64_t(buffer_.size()));
|
||||
|
||||
if (copy_length) {
|
||||
int64_t new_bytes_read = bytes_read_ + copy_length;
|
||||
|
||||
if (notify_interval_ > 0 && notify_callback_) {
|
||||
if ((bytes_read_ / notify_interval_) !=
|
||||
(new_bytes_read / notify_interval_)) {
|
||||
notify = true;
|
||||
}
|
||||
}
|
||||
|
||||
bytes_read_ = new_bytes_read;
|
||||
|
||||
memcpy(data, buffer_.data(), copy_length);
|
||||
buffer_.erase(buffer_.begin(), buffer_.begin() + copy_length);
|
||||
}
|
||||
}
|
||||
|
||||
// Fired outside the lock (see set_notify_callback())
|
||||
if (notify) {
|
||||
notify_callback_();
|
||||
}
|
||||
|
||||
return copy_length;
|
||||
}
|
||||
|
||||
int64_t PreviewAudioDevice::write(const char *data, int64_t length)
|
||||
{
|
||||
std::lock_guard<std::mutex> locker(lock_);
|
||||
|
||||
buffer_.insert(buffer_.end(), data, data + length);
|
||||
|
||||
return length;
|
||||
}
|
||||
|
||||
void PreviewAudioDevice::clear()
|
||||
{
|
||||
std::lock_guard<std::mutex> locker(lock_);
|
||||
|
||||
buffer_.clear();
|
||||
bytes_read_ = 0;
|
||||
output_frames_consumed_.store(0);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_PREVIEWAUDIODEVICE_H
|
||||
#define OAK_PREVIEWAUDIODEVICE_H
|
||||
|
||||
#include <olive/core/render/audioparams.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Pull-style sample buffer fed to the audio output callback
|
||||
*
|
||||
* Formerly a QIODevice subclass consumed by QAudioOutput. Now a plain class:
|
||||
* the audio backend (PortAudio, see engine/audio AudioManager) pulls samples
|
||||
* through read() from its stream callback and the render side pushes samples
|
||||
* through write(). The callback-driven pull semantics are unchanged.
|
||||
*/
|
||||
class PreviewAudioDevice {
|
||||
public:
|
||||
PreviewAudioDevice();
|
||||
|
||||
virtual ~PreviewAudioDevice();
|
||||
|
||||
/**
|
||||
* @brief Read up to `max_size` bytes from the queued buffer
|
||||
*
|
||||
* Called from the audio output callback. Returns the number of bytes
|
||||
* actually copied (0 when the buffer is empty, i.e. underrun).
|
||||
*/
|
||||
int64_t read(char *data, int64_t max_size);
|
||||
|
||||
/**
|
||||
* @brief Append `length` bytes to the queued buffer
|
||||
*/
|
||||
int64_t write(const char *data, int64_t length);
|
||||
|
||||
// Derives the frame size from the audio format (bytes per sample per
|
||||
// channel * channel count). Until params are set, bytes_per_frame()
|
||||
// reports 0, i.e. "unknown".
|
||||
void set_params(const core::AudioParams ¶ms);
|
||||
|
||||
int bytes_per_frame() const
|
||||
{
|
||||
return bytes_per_frame_;
|
||||
}
|
||||
|
||||
void set_bytes_per_frame(int b)
|
||||
{
|
||||
bytes_per_frame_ = b;
|
||||
}
|
||||
|
||||
void set_notify_interval(int64_t i)
|
||||
{
|
||||
notify_interval_ = i;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Install the callback fired when a notify interval boundary is crossed
|
||||
*
|
||||
* Replaces the former `notify` signal. The callback is invoked from read(),
|
||||
* i.e. from the audio output callback thread, AFTER the internal lock has
|
||||
* been released (the Qt version emitted while holding the lock; receivers
|
||||
* lived on another thread so it was effectively queued). The callback must
|
||||
* therefore be thread-safe and must not call back into this device.
|
||||
*/
|
||||
void set_notify_callback(std::function<void()> callback)
|
||||
{
|
||||
std::lock_guard<std::mutex> locker(lock_);
|
||||
notify_callback_ = std::move(callback);
|
||||
}
|
||||
|
||||
void clear();
|
||||
|
||||
/**
|
||||
* @brief Frames consumed by the audio output callback
|
||||
*
|
||||
* Counted in the callback itself so underrun (zero-filled) frames are
|
||||
* included, making the value usable as a playback clock.
|
||||
*/
|
||||
void add_output_frames(int64_t frame_count)
|
||||
{
|
||||
output_frames_consumed_.fetch_add(frame_count);
|
||||
}
|
||||
|
||||
int64_t output_frames_consumed() const
|
||||
{
|
||||
return output_frames_consumed_.load();
|
||||
}
|
||||
|
||||
void reset_output_frames()
|
||||
{
|
||||
output_frames_consumed_.store(0);
|
||||
}
|
||||
|
||||
private:
|
||||
std::mutex lock_;
|
||||
|
||||
std::vector<char> buffer_;
|
||||
|
||||
int bytes_per_frame_;
|
||||
|
||||
int64_t notify_interval_;
|
||||
|
||||
int64_t bytes_read_;
|
||||
|
||||
std::function<void()> notify_callback_;
|
||||
|
||||
std::atomic<int64_t> output_frames_consumed_{0};
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_PREVIEWAUDIODEVICE_H
|
||||
@@ -0,0 +1,915 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "previewautocacher.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
|
||||
#include "audioplaybackcache.h"
|
||||
#include "audiowaveformcache.h"
|
||||
#include "diskmanager.h"
|
||||
#include "framehashcache.h"
|
||||
#include "inputdragger.h"
|
||||
#include "input/multicam/multicamnode.h"
|
||||
#include "playbackcache.h"
|
||||
#include "project.h"
|
||||
#include "qtutils.h"
|
||||
#include "rendermanager.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
PreviewAutoCacher::PreviewAutoCacher()
|
||||
: project_(nullptr)
|
||||
, use_custom_range_(false)
|
||||
, pause_renders_(false)
|
||||
, pause_thumbnails_(false)
|
||||
, single_frame_render_(nullptr)
|
||||
, delayed_requeue_pending_(false)
|
||||
, copied_color_manager_(nullptr)
|
||||
, multicam_(nullptr)
|
||||
, ignore_cache_requests_(false)
|
||||
{
|
||||
copier_ = std::make_unique<ProjectCopier>();
|
||||
copier_->set_added_node_handler(
|
||||
[this](Node *n) { connect_to_node_cache(n); });
|
||||
copier_->set_removed_node_handler(
|
||||
[this](Node *n) { disconnect_from_node_cache(n); });
|
||||
|
||||
// Set defaults
|
||||
set_playhead(0);
|
||||
|
||||
// Wait a certain amount of time before requeuing when we receive an invalidate signal.
|
||||
// (Formerly a single-shot QTimer; now an explicit pending flag, see header.)
|
||||
requeue_delay_ms_ = OAK_CONFIG("AutoCacheDelay").toInt();
|
||||
|
||||
// Conform notifications: the facade (codec wave) calls conform_finished()
|
||||
// when ConformManager reports a ready conform.
|
||||
}
|
||||
|
||||
PreviewAutoCacher::~PreviewAutoCacher()
|
||||
{
|
||||
// Ensure everything is cleaned up appropriately
|
||||
set_project(nullptr);
|
||||
}
|
||||
|
||||
RenderTicketPtr PreviewAutoCacher::get_single_frame(ViewerOutput *viewer,
|
||||
const Rational &t, bool dry)
|
||||
{
|
||||
return get_single_frame(viewer->get_connected_texture_output(), viewer, t, dry);
|
||||
}
|
||||
|
||||
RenderTicketPtr PreviewAutoCacher::get_single_frame(Node *n, ViewerOutput *viewer,
|
||||
const Rational &t, bool dry)
|
||||
{
|
||||
// If we have a single frame render queued (but not yet sent to the RenderManager), cancel it now
|
||||
cancel_queued_single_frame_render();
|
||||
|
||||
// Create a new single frame render ticket
|
||||
auto sfr = std::make_shared<RenderTicket>();
|
||||
sfr->start();
|
||||
sfr->set_property("time", Variant::from_value(t));
|
||||
sfr->set_property("dry", dry);
|
||||
sfr->set_property("node", Variant::from_value(QtUtils::ptr_to_value(n)));
|
||||
sfr->set_property("viewer", Variant::from_value(QtUtils::ptr_to_value(viewer)));
|
||||
|
||||
// Queue it and try to render
|
||||
single_frame_render_ = sfr;
|
||||
try_render();
|
||||
|
||||
return sfr;
|
||||
}
|
||||
|
||||
RenderTicketPtr PreviewAutoCacher::get_range_of_audio(ViewerOutput *viewer,
|
||||
TimeRange range)
|
||||
{
|
||||
Node *copy = copier_->get_copy(viewer->get_connected_sample_output());
|
||||
return render_audio(copy, viewer, range, nullptr);
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::clear_single_frame_renders()
|
||||
{
|
||||
// Snapshot the watchers before doing anything that might synchronously
|
||||
// delete them (finishing a ticket runs video_rendered(), which deletes the
|
||||
// watcher and removes it from the map). A watcher pointer is only
|
||||
// dereferenced while it is still a key of the map, which is exactly the
|
||||
// liveness criterion (deletion always goes through map removal). This
|
||||
// replaces the former QPointer guarding.
|
||||
std::vector<RenderTicketWatcher *> watchers;
|
||||
for (auto it = video_immediate_passthroughs_.cbegin();
|
||||
it != video_immediate_passthroughs_.cend(); it++) {
|
||||
watchers.push_back(it->first);
|
||||
}
|
||||
|
||||
for (RenderTicketWatcher *w : watchers) {
|
||||
if (!video_immediate_passthroughs_.count(w)) {
|
||||
// Already finished (and deleted) by an earlier iteration
|
||||
continue;
|
||||
}
|
||||
|
||||
// Keep already-running workers alive: cancelling an in-flight render
|
||||
// forces the worker process to be torn down, which defeats the process
|
||||
// pool. Frames that finish late are simply ignored by the viewer.
|
||||
if (w->is_running()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
RenderTicketPtr ticket = w->get_ticket();
|
||||
w->cancel();
|
||||
RenderManager::instance()->remove_ticket(ticket);
|
||||
ticket->finish();
|
||||
}
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::clear_single_frame_renders_that_arent_running()
|
||||
{
|
||||
std::vector<RenderTicketWatcher *> watchers;
|
||||
for (auto it = video_immediate_passthroughs_.cbegin();
|
||||
it != video_immediate_passthroughs_.cend(); it++) {
|
||||
watchers.push_back(it->first);
|
||||
}
|
||||
|
||||
for (RenderTicketWatcher *w : watchers) {
|
||||
if (!video_immediate_passthroughs_.count(w) || w->is_running()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
RenderTicketPtr ticket = w->get_ticket();
|
||||
w->cancel();
|
||||
RenderManager::instance()->remove_ticket(ticket);
|
||||
ticket->finish();
|
||||
}
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::video_invalidated_from_cache(PlaybackCache *cache,
|
||||
ViewerOutput *context,
|
||||
const TimeRange &range)
|
||||
{
|
||||
cache->clear_request_range(range);
|
||||
|
||||
video_invalidated_from_node(context, cache, range);
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::audio_invalidated_from_cache(PlaybackCache *cache,
|
||||
ViewerOutput *context,
|
||||
const TimeRange &range)
|
||||
{
|
||||
cache->clear_request_range(range);
|
||||
|
||||
audio_invalidated_from_node(context, cache, range);
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::cancel_for_cache(PlaybackCache *cache)
|
||||
{
|
||||
if (dynamic_cast<FrameHashCache *>(cache) ||
|
||||
dynamic_cast<ThumbnailCache *>(cache)) {
|
||||
for (auto it = pending_video_jobs_.begin();
|
||||
it != pending_video_jobs_.end();) {
|
||||
if ((*it).cache == cache) {
|
||||
it = pending_video_jobs_.erase(it);
|
||||
} else {
|
||||
it++;
|
||||
}
|
||||
}
|
||||
} else if (dynamic_cast<AudioPlaybackCache *>(cache) ||
|
||||
dynamic_cast<AudioWaveformCache *>(cache)) {
|
||||
for (auto it = pending_audio_jobs_.begin();
|
||||
it != pending_audio_jobs_.end();) {
|
||||
if ((*it).cache == cache) {
|
||||
it = pending_audio_jobs_.erase(it);
|
||||
} else {
|
||||
it++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::audio_rendered(RenderTicketWatcher *watcher)
|
||||
{
|
||||
// If the task list doesn't contain this watcher, presumably it was cleared as a result of a
|
||||
// viewer switch, so we'll completely ignore this watcher
|
||||
auto task_it = std::find(running_audio_tasks_.begin(),
|
||||
running_audio_tasks_.end(), watcher);
|
||||
if (task_it != running_audio_tasks_.end()) {
|
||||
running_audio_tasks_.erase(task_it);
|
||||
|
||||
// Assume that a "result" is a fully completed image and a non-result is a cancelled ticket
|
||||
TimeRange range = watcher->property("time").value<TimeRange>();
|
||||
Node *node = copier_->get_original(QtUtils::value_to_ptr<Node>(
|
||||
uintptr_t(watcher->property("node").to_u_long_long())));
|
||||
|
||||
if (watcher->has_result() && node) {
|
||||
if (PlaybackCache *cache = QtUtils::value_to_ptr<PlaybackCache>(
|
||||
uintptr_t(watcher->property("cache").to_u_long_long()))) {
|
||||
AudioCacheData &d = audio_cache_data_[cache];
|
||||
|
||||
JobTime watcher_job_time =
|
||||
watcher->property("job").value<JobTime>();
|
||||
|
||||
TimeRangeList valid_ranges =
|
||||
d.job_tracker.getCurrentSubRanges(range, watcher_job_time);
|
||||
|
||||
AudioVisualWaveform waveform =
|
||||
watcher->get_ticket()
|
||||
->property("waveform")
|
||||
.value<AudioVisualWaveform>();
|
||||
|
||||
SampleBuffer buf = watcher->get().value<SampleBuffer>();
|
||||
|
||||
bool incomplete =
|
||||
watcher->get_ticket()->property("incomplete").to_bool();
|
||||
|
||||
if (AudioPlaybackCache *pcm =
|
||||
dynamic_cast<AudioPlaybackCache *>(cache)) {
|
||||
// WritePCM is tolerant to its buffer being null, it will just write silence instead
|
||||
pcm->set_parameters(buf.audio_params());
|
||||
pcm->write_pcm(range, valid_ranges,
|
||||
watcher->get().value<SampleBuffer>());
|
||||
} else if (AudioWaveformCache *wave =
|
||||
dynamic_cast<AudioWaveformCache *>(cache)) {
|
||||
wave->set_parameters(buf.audio_params());
|
||||
if (!incomplete) {
|
||||
wave->write_waveform(range, valid_ranges, &waveform);
|
||||
}
|
||||
}
|
||||
|
||||
if (incomplete) {
|
||||
if (last_conform_task_ > watcher_job_time) {
|
||||
// Requeue now
|
||||
cache->invalidate(range);
|
||||
} else {
|
||||
// Wait for conform
|
||||
d.needs_conform.insert(range);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Continue rendering
|
||||
try_render();
|
||||
}
|
||||
|
||||
delete watcher;
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::video_rendered(RenderTicketWatcher *watcher)
|
||||
{
|
||||
const StringList bad_cache_names =
|
||||
watcher->get_ticket()->property("badcache").to_string_list();
|
||||
if (!bad_cache_names.empty()) {
|
||||
for (const std::string &fn : bad_cache_names) {
|
||||
DiskManager::instance()->delete_specific_file(fn);
|
||||
}
|
||||
}
|
||||
|
||||
// Process passthroughs no matter what, if the viewer was switched, the passthrough map would be
|
||||
// cleared anyway
|
||||
std::vector<RenderTicketPtr> tickets;
|
||||
{
|
||||
auto it = video_immediate_passthroughs_.find(watcher);
|
||||
if (it != video_immediate_passthroughs_.end()) {
|
||||
tickets = std::move(it->second);
|
||||
video_immediate_passthroughs_.erase(it);
|
||||
}
|
||||
}
|
||||
for (RenderTicketPtr &t : tickets) {
|
||||
if (watcher->has_result()) {
|
||||
t->set_property("multicam_output",
|
||||
watcher->get_ticket()->property("multicam_output"));
|
||||
t->finish(watcher->get());
|
||||
} else {
|
||||
t->finish();
|
||||
}
|
||||
}
|
||||
|
||||
// If the task list doesn't contain this watcher, presumably it was cleared as a result of a
|
||||
// viewer switch, so we'll completely ignore this watcher
|
||||
auto task_it = std::find(running_video_tasks_.begin(),
|
||||
running_video_tasks_.end(), watcher);
|
||||
if (task_it != running_video_tasks_.end()) {
|
||||
running_video_tasks_.erase(task_it);
|
||||
|
||||
// Assume that a "result" is a fully completed image and a non-result is a cancelled ticket
|
||||
if (watcher->has_result()) {
|
||||
if (watcher->get_ticket()->property("cached").to_bool()) {
|
||||
if (FrameHashCache *cache = QtUtils::value_to_ptr<FrameHashCache>(
|
||||
uintptr_t(watcher->property("cache").to_u_long_long()))) {
|
||||
Rational time = watcher->property("time").value<Rational>();
|
||||
JobTime job = watcher->property("job").value<JobTime>();
|
||||
|
||||
auto data_it = video_cache_data_.find(cache);
|
||||
if (data_it != video_cache_data_.end() &&
|
||||
data_it->second.job_tracker.isCurrent(time, job)) {
|
||||
cache->validate_time(time);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Continue rendering
|
||||
try_render();
|
||||
}
|
||||
|
||||
delete watcher;
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::connect_to_node_cache(Node *node)
|
||||
{
|
||||
if (ignore_cache_requests_) {
|
||||
return;
|
||||
}
|
||||
|
||||
PlaybackCache *video_cache = node->video_frame_cache();
|
||||
PlaybackCache *thumb_cache = node->thumbnail_cache();
|
||||
PlaybackCache *audio_cache = node->audio_playback_cache();
|
||||
PlaybackCache *wave_cache = node->waveform_cache();
|
||||
|
||||
video_cache->set_requested_callback(
|
||||
[this, video_cache](ViewerOutput *context, const TimeRange &range) {
|
||||
video_invalidated_from_cache(video_cache, context, range);
|
||||
});
|
||||
|
||||
thumb_cache->set_requested_callback(
|
||||
[this, thumb_cache](ViewerOutput *context, const TimeRange &range) {
|
||||
video_invalidated_from_cache(thumb_cache, context, range);
|
||||
});
|
||||
|
||||
audio_cache->set_requested_callback(
|
||||
[this, audio_cache](ViewerOutput *context, const TimeRange &range) {
|
||||
audio_invalidated_from_cache(audio_cache, context, range);
|
||||
});
|
||||
|
||||
wave_cache->set_requested_callback(
|
||||
[this, wave_cache](ViewerOutput *context, const TimeRange &range) {
|
||||
audio_invalidated_from_cache(wave_cache, context, range);
|
||||
});
|
||||
|
||||
video_cache->set_cancel_all_callback(
|
||||
[this, video_cache]() { cancel_for_cache(video_cache); });
|
||||
|
||||
audio_cache->set_cancel_all_callback(
|
||||
[this, audio_cache]() { cancel_for_cache(audio_cache); });
|
||||
|
||||
node->video_frame_cache()->resignal_requests();
|
||||
node->thumbnail_cache()->resignal_requests();
|
||||
node->audio_playback_cache()->resignal_requests();
|
||||
node->waveform_cache()->resignal_requests();
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::disconnect_from_node_cache(Node *node)
|
||||
{
|
||||
node->video_frame_cache()->set_requested_callback(nullptr);
|
||||
node->thumbnail_cache()->set_requested_callback(nullptr);
|
||||
node->audio_playback_cache()->set_requested_callback(nullptr);
|
||||
node->waveform_cache()->set_requested_callback(nullptr);
|
||||
|
||||
node->video_frame_cache()->set_cancel_all_callback(nullptr);
|
||||
node->audio_playback_cache()->set_cancel_all_callback(nullptr);
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::cancel_queued_single_frame_render()
|
||||
{
|
||||
if (single_frame_render_) {
|
||||
// Signal that this ticket was cancelled with no value
|
||||
single_frame_render_->finish();
|
||||
single_frame_render_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::start_caching_range(const TimeRange &range,
|
||||
TimeRangeList *range_list,
|
||||
RenderJobTracker *tracker)
|
||||
{
|
||||
range_list->insert(range);
|
||||
tracker->insert(range, copier_->get_graph_change_time());
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::start_caching_video_range(ViewerOutput *context,
|
||||
PlaybackCache *cache,
|
||||
const TimeRange &range)
|
||||
{
|
||||
Node *node = cache->parent();
|
||||
Rational using_tb;
|
||||
if (ThumbnailCache *thumbs = dynamic_cast<ThumbnailCache *>(cache)) {
|
||||
using_tb = thumbs->get_timebase();
|
||||
} else {
|
||||
using_tb = context->get_video_params().frame_rate_as_time_base();
|
||||
}
|
||||
|
||||
cache->clear_request_range(range);
|
||||
|
||||
TimeRangeListFrameIterator iterator({ range }, using_tb);
|
||||
pending_video_jobs_.push_back({ node, context, cache, range, iterator });
|
||||
video_cache_data_[cache].job_tracker.insert(
|
||||
TimeRange(iterator.snap(range.in()), range.out()),
|
||||
copier_->get_graph_change_time());
|
||||
try_render();
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::start_caching_audio_range(ViewerOutput *context,
|
||||
PlaybackCache *cache,
|
||||
const TimeRange &range)
|
||||
{
|
||||
Node *node = cache->parent();
|
||||
|
||||
cache->clear_request_range(range);
|
||||
|
||||
pending_audio_jobs_.push_back({ node, context, cache, range });
|
||||
AudioCacheData &data = audio_cache_data_[cache];
|
||||
data.context = context;
|
||||
data.job_tracker.insert(range, copier_->get_graph_change_time());
|
||||
try_render();
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::video_invalidated_from_node(ViewerOutput *context,
|
||||
PlaybackCache *cache,
|
||||
const TimeRange &range)
|
||||
{
|
||||
// Ignore render requests if no video is present
|
||||
if (!context || !context->get_video_params().is_valid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Stop any current render tasks because a) they might be out of date now anyway, and b) we
|
||||
// want to dedicate all our rendering power to realtime feedback for the user
|
||||
//CancelVideoTasks(node);
|
||||
|
||||
cache->clear_request_range(range);
|
||||
|
||||
// If auto-cache is enabled and a slider is not being dragged, queue up to hash these frames
|
||||
if (!NodeInputDragger::is_input_being_dragged()) {
|
||||
start_caching_video_range(context, cache, range);
|
||||
}
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::audio_invalidated_from_node(ViewerOutput *context,
|
||||
PlaybackCache *cache,
|
||||
const TimeRange &range)
|
||||
{
|
||||
// Ignore render requests if no video is present
|
||||
if (!context || !context->get_audio_params().is_valid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We don't stop rendering audio because currently there's no system of requeuing audio if it's
|
||||
// cancelled, so some areas may end up unrendered forever
|
||||
// ClearAudioQueue();
|
||||
|
||||
cache->clear_request_range(range);
|
||||
|
||||
// If we're auto-caching audio or require realtime waveforms, we'll have to render this
|
||||
start_caching_audio_range(context, cache, range);
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::set_playhead(const Rational &playhead)
|
||||
{
|
||||
cache_range_ =
|
||||
TimeRange(playhead - OAK_CONFIG("DiskCacheBehind").value<Rational>(),
|
||||
playhead + OAK_CONFIG("DiskCacheAhead").value<Rational>());
|
||||
|
||||
try_render();
|
||||
}
|
||||
|
||||
template <typename T> void cancel_tasks(const T &task_list, bool and_wait)
|
||||
{
|
||||
for (auto it = task_list.cbegin(); it != task_list.cend(); it++) {
|
||||
// Signal that the ticket should not be finished
|
||||
(*it)->cancel();
|
||||
}
|
||||
|
||||
if (and_wait) {
|
||||
// Wait for each ticket to finish
|
||||
for (auto it = task_list.cbegin(); it != task_list.cend(); it++) {
|
||||
(*it)->wait_for_finished();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::cancel_video_tasks(bool and_wait_for_them_to_finish)
|
||||
{
|
||||
cancel_tasks(running_video_tasks_, and_wait_for_them_to_finish);
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::cancel_audio_tasks(bool and_wait_for_them_to_finish)
|
||||
{
|
||||
cancel_tasks(running_audio_tasks_, and_wait_for_them_to_finish);
|
||||
}
|
||||
|
||||
bool PreviewAutoCacher::is_rendering_custom_range() const
|
||||
{
|
||||
if (!use_custom_range_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const VideoJob &job : pending_video_jobs_) {
|
||||
if (job.range == custom_autocache_range_ && job.iterator.has_next()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::set_renders_paused(bool e)
|
||||
{
|
||||
pause_renders_ = e;
|
||||
if (!e) {
|
||||
try_render();
|
||||
}
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::set_thumbnails_paused(bool e)
|
||||
{
|
||||
pause_thumbnails_ = e;
|
||||
if (!e) {
|
||||
try_render();
|
||||
}
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::try_render()
|
||||
{
|
||||
delayed_requeue_pending_ = false;
|
||||
|
||||
if (copier_->has_updates_in_queue()) {
|
||||
// Check if we have jobs running in other threads that shouldn't be interrupted right now
|
||||
// NOTE: We don't check for downloads because, while they run in another thread, they don't
|
||||
// require any access to the graph and therefore don't risk race conditions.
|
||||
if (!running_audio_tasks_.empty() ||
|
||||
!running_video_tasks_.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// No jobs are active, we can process the update queue
|
||||
copier_->process_update_queue();
|
||||
}
|
||||
|
||||
if (single_frame_render_) {
|
||||
// Make an explicit copy of the render ticket here - it seems that on some systems it can be set
|
||||
// to NULL before we're done with it...
|
||||
RenderTicketPtr t = single_frame_render_;
|
||||
single_frame_render_ = nullptr;
|
||||
|
||||
// Check if already caching this
|
||||
Node *n = QtUtils::value_to_ptr<Node>(
|
||||
uintptr_t(t->property("node").to_u_long_long()));
|
||||
Node *copy = copier_->get_copy(n);
|
||||
|
||||
if (copy) {
|
||||
RenderTicketWatcher *watcher = render_frame(
|
||||
copy,
|
||||
QtUtils::value_to_ptr<ViewerOutput>(
|
||||
uintptr_t(t->property("viewer").to_u_long_long())),
|
||||
t->property("time").value<Rational>(), nullptr,
|
||||
t->property("dry").to_bool());
|
||||
if (watcher) {
|
||||
video_immediate_passthroughs_[watcher].push_back(t);
|
||||
}
|
||||
} else {
|
||||
fprintf(stderr,
|
||||
"Failed to find copied node for SFR ticket, requeueing\n");
|
||||
single_frame_render_ = t;
|
||||
delayed_requeue_pending_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!pause_renders_) {
|
||||
// Completely arbitrary number. I don't know what's optimal for this yet.
|
||||
const int max_tasks = 4;
|
||||
|
||||
// Handle video tasks
|
||||
if (!pause_thumbnails_) {
|
||||
while (!pending_video_jobs_.empty()) {
|
||||
VideoJob &d = pending_video_jobs_.front();
|
||||
|
||||
if (Node *copy = copier_->get_copy(d.node)) {
|
||||
// Queue next frames
|
||||
Rational t;
|
||||
while (running_video_tasks_.size() < size_t(max_tasks) &&
|
||||
d.iterator.get_next(&t)) {
|
||||
render_frame(copy, d.context, t, d.cache, false);
|
||||
|
||||
if (cache_progress_callback_) {
|
||||
cache_progress_callback_(
|
||||
double(d.iterator.frame_index()) /
|
||||
double(d.iterator.size()));
|
||||
}
|
||||
|
||||
if (!d.iterator.has_next() &&
|
||||
stop_cache_proxy_tasks_callback_) {
|
||||
stop_cache_proxy_tasks_callback_();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fprintf(stderr,
|
||||
"Failed to find node copy for video job, retrying\n");
|
||||
delayed_requeue_pending_ = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (d.iterator.has_next()) {
|
||||
break;
|
||||
} else {
|
||||
pending_video_jobs_.pop_front();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle audio tasks
|
||||
while (!pending_audio_jobs_.empty() &&
|
||||
running_audio_tasks_.size() < size_t(max_tasks)) {
|
||||
AudioJob &d = pending_audio_jobs_.front();
|
||||
|
||||
bool pop = true;
|
||||
|
||||
// Start job
|
||||
if (Node *copy = copier_->get_copy(d.node)) {
|
||||
TimeRange &queued_range = d.range;
|
||||
TimeRange use_range = queued_range;
|
||||
|
||||
if (dynamic_cast<AudioWaveformCache *>(d.cache)) {
|
||||
Rational new_out = std::min(
|
||||
use_range.in() +
|
||||
AudioVisualWaveform::k_minimum_sample_rate.flipped(),
|
||||
use_range.out());
|
||||
|
||||
if (new_out != use_range.out()) {
|
||||
use_range.set_out(new_out);
|
||||
queued_range.set_in(new_out);
|
||||
pop = false;
|
||||
}
|
||||
}
|
||||
|
||||
render_audio(copy, d.context, use_range, d.cache);
|
||||
} else {
|
||||
fprintf(stderr,
|
||||
"Failed to find node copy for audio job, retrying\n");
|
||||
pop = false;
|
||||
delayed_requeue_pending_ = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (pop) {
|
||||
pending_audio_jobs_.pop_front();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RenderTicketWatcher *PreviewAutoCacher::render_frame(Node *node,
|
||||
ViewerOutput *context,
|
||||
const Rational &time,
|
||||
PlaybackCache *cache,
|
||||
bool dry)
|
||||
{
|
||||
RenderTicketWatcher *watcher = new RenderTicketWatcher();
|
||||
watcher->set_property("job",
|
||||
Variant::from_value(copier_->get_last_update_time()));
|
||||
watcher->set_property("cache",
|
||||
Variant::from_value(QtUtils::ptr_to_value(cache)));
|
||||
watcher->set_property("time", Variant::from_value(time));
|
||||
watcher->set_finished_callback(
|
||||
[this](RenderTicketWatcher *w) { video_rendered(w); });
|
||||
|
||||
running_video_tasks_.push_back(watcher);
|
||||
|
||||
RenderManager::RenderVideoParams rvp(node, context->get_video_params(),
|
||||
context->get_audio_params(), time,
|
||||
copied_color_manager_,
|
||||
RenderMode::k_offline);
|
||||
|
||||
if (FrameHashCache *frame_cache = dynamic_cast<FrameHashCache *>(cache)) {
|
||||
if (ThumbnailCache *wave_cache =
|
||||
dynamic_cast<ThumbnailCache *>(cache)) {
|
||||
(void) wave_cache;
|
||||
rvp.video_params.set_divider(
|
||||
VideoParams::get_divider_for_target_resolution(
|
||||
rvp.video_params.width(), rvp.video_params.height(), 160,
|
||||
120));
|
||||
rvp.force_format = PixelFormat::f32;
|
||||
rvp.force_channel_count = VideoParams::k_rgba_channel_count;
|
||||
} else {
|
||||
frame_cache->set_timebase(
|
||||
context->get_video_params().frame_rate_as_time_base());
|
||||
}
|
||||
|
||||
rvp.add_cache(frame_cache);
|
||||
} else {
|
||||
// Preview/display frames are rendered at reduced precision to cut the
|
||||
// GPU->CPU readback and IPC transfer bandwidth. The internal render
|
||||
// pipeline stays F32/ACEScg; the final preview copy is packed 10-bit
|
||||
// RGBA (4 bytes/pixel) to preserve 10-bit panel precision while halving
|
||||
// bandwidth compared to F16.
|
||||
rvp.force_format = PixelFormat::u10;
|
||||
rvp.force_channel_count = VideoParams::k_rgba_channel_count;
|
||||
}
|
||||
|
||||
// Video playback frames are rendered out-of-process. GPU textures cannot be
|
||||
// shared across worker processes (or across independent Vulkan instances),
|
||||
// so we always request CPU frames.
|
||||
rvp.return_type = dry ? RenderManager::k_null : RenderManager::k_frame;
|
||||
|
||||
// Allow using cached images for this render job
|
||||
rvp.use_cache = true;
|
||||
|
||||
// Multicam
|
||||
rvp.multicam = copier_->get_copy(multicam_);
|
||||
|
||||
watcher->set_ticket(RenderManager::instance()->render_frame(rvp));
|
||||
|
||||
// If the ticket finished synchronously, video_rendered has already deleted the
|
||||
// watcher. The caller must not use this pointer in that case.
|
||||
if (std::find(running_video_tasks_.begin(), running_video_tasks_.end(),
|
||||
watcher) == running_video_tasks_.end()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return watcher;
|
||||
}
|
||||
|
||||
RenderTicketPtr PreviewAutoCacher::render_audio(Node *node,
|
||||
ViewerOutput *context,
|
||||
const TimeRange &r,
|
||||
PlaybackCache *cache)
|
||||
{
|
||||
RenderTicketWatcher *watcher = new RenderTicketWatcher();
|
||||
watcher->set_property("job",
|
||||
Variant::from_value(copier_->get_last_update_time()));
|
||||
watcher->set_property("node",
|
||||
Variant::from_value(QtUtils::ptr_to_value(node)));
|
||||
watcher->set_property("cache",
|
||||
Variant::from_value(QtUtils::ptr_to_value(cache)));
|
||||
watcher->set_property("time", Variant::from_value(r));
|
||||
watcher->set_finished_callback(
|
||||
[this](RenderTicketWatcher *w) { audio_rendered(w); });
|
||||
running_audio_tasks_.push_back(watcher);
|
||||
|
||||
AudioParams p = context->get_audio_params();
|
||||
const bool invalid_params =
|
||||
(p.sample_rate() <= 0 || p.channel_count() <= 0);
|
||||
if (invalid_params) {
|
||||
AudioParams fallback(
|
||||
OAK_CONFIG("DefaultSequenceAudioFrequency").toInt(),
|
||||
OAK_CONFIG("DefaultSequenceAudioLayout").toULongLong(),
|
||||
ViewerOutput::k_default_sample_format);
|
||||
p = fallback;
|
||||
}
|
||||
p.set_format(ViewerOutput::k_default_sample_format);
|
||||
|
||||
RenderManager::RenderAudioParams rap(node, r, p, RenderMode::k_offline);
|
||||
|
||||
rap.generate_waveforms = dynamic_cast<AudioWaveformCache *>(cache);
|
||||
rap.clamp = false;
|
||||
|
||||
RenderTicketPtr ticket = RenderManager::instance()->render_audio(rap);
|
||||
watcher->set_ticket(ticket);
|
||||
return ticket;
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::conform_finished()
|
||||
{
|
||||
// Got an audio conform, requeue all the audio currently needing a conform
|
||||
last_conform_task_.acquire();
|
||||
|
||||
for (auto it = audio_cache_data_.begin(); it != audio_cache_data_.end();
|
||||
it++) {
|
||||
if (!it->first || !it->second.context) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const TimeRange &range : it->second.needs_conform) {
|
||||
it->first->request(it->second.context, range);
|
||||
}
|
||||
it->second.needs_conform.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::cache_proxy_task_cancelled()
|
||||
{
|
||||
pending_video_jobs_.clear();
|
||||
|
||||
try_render();
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::force_cache_range(ViewerOutput *context,
|
||||
const TimeRange &range)
|
||||
{
|
||||
use_custom_range_ = true;
|
||||
custom_autocache_range_ = range;
|
||||
|
||||
// Re-hash these frames and start rendering
|
||||
start_caching_video_range(context, context->video_frame_cache(), range);
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::project_destroyed()
|
||||
{
|
||||
// If the project dies while we're still using it (e.g. shutdown order:
|
||||
// project freed before the RenderManager), drop all state that
|
||||
// references its nodes/caches without touching them. Otherwise the
|
||||
// next set_project(nullptr) would clear callbacks on dead caches.
|
||||
project_ = nullptr;
|
||||
delayed_requeue_pending_ = false;
|
||||
single_frame_render_ = nullptr;
|
||||
video_immediate_passthroughs_.clear();
|
||||
pending_video_jobs_.clear();
|
||||
pending_audio_jobs_.clear();
|
||||
video_cache_data_.clear();
|
||||
audio_cache_data_.clear();
|
||||
multicam_ = nullptr;
|
||||
// The copier's own destroyed-guard nulls its original_; this just
|
||||
// clears its copy maps without touching the dead project.
|
||||
copier_->set_project(nullptr);
|
||||
}
|
||||
|
||||
void PreviewAutoCacher::set_project(Project *project)
|
||||
{
|
||||
if (project_ == project) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (project_) {
|
||||
// We must wait for any jobs to finish because they'll be using our copied graph and we're
|
||||
// about to destroy it
|
||||
|
||||
// Stop requeue if it's pending
|
||||
delayed_requeue_pending_ = false;
|
||||
|
||||
// Handle video rendering tasks
|
||||
if (!running_video_tasks_.empty()) {
|
||||
// Cancel any video tasks and wait for them to finish
|
||||
cancel_video_tasks(true);
|
||||
running_video_tasks_.clear();
|
||||
}
|
||||
|
||||
// Handle audio rendering tasks
|
||||
if (!running_audio_tasks_.empty()) {
|
||||
// Cancel any audio tasks and wait for them to finish
|
||||
cancel_audio_tasks(true);
|
||||
running_audio_tasks_.clear();
|
||||
}
|
||||
|
||||
// Clear any single frame render that might be queued
|
||||
cancel_queued_single_frame_render();
|
||||
|
||||
// Not interested in video passthroughs anymore
|
||||
video_immediate_passthroughs_.clear();
|
||||
|
||||
// Disconnect from all node cache's
|
||||
for (auto it = copier_->get_node_map().cbegin();
|
||||
it != copier_->get_node_map().cend(); it++) {
|
||||
disconnect_from_node_cache(it->first);
|
||||
}
|
||||
|
||||
// Delete all of our copied nodes
|
||||
copier_->set_project(nullptr);
|
||||
|
||||
// Ensure all cache data is cleared
|
||||
video_cache_data_.clear();
|
||||
audio_cache_data_.clear();
|
||||
|
||||
// Clear multicam reference
|
||||
multicam_ = nullptr;
|
||||
}
|
||||
|
||||
project_ = project;
|
||||
|
||||
if (project_) {
|
||||
// NOTE: the facade must call project_destroyed() if the Project is
|
||||
// destroyed while set (replaces the former Project::destroyed
|
||||
// connection).
|
||||
|
||||
// Copy graph (this should always be a Project)
|
||||
set_renders_paused(true);
|
||||
|
||||
copier_->set_project(project_);
|
||||
|
||||
for (size_t i = 0; i < project_->nodes().size(); i++) {
|
||||
project_->nodes().at(i)->ConnectedToPreviewEvent();
|
||||
}
|
||||
|
||||
// Find copied viewer node
|
||||
copied_color_manager_ = copier_->get_copied_project()->color_manager();
|
||||
|
||||
set_renders_paused(false);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_AUTOCACHER_H
|
||||
#define OAK_AUTOCACHER_H
|
||||
|
||||
#include <functional>
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "color/colormanager/colormanager.h"
|
||||
#include "config/config.h"
|
||||
#include "group/group.h"
|
||||
#include "node.h"
|
||||
#include "output/viewer/viewer.h"
|
||||
#include "project.h"
|
||||
#include "projectcopier.h"
|
||||
#include "renderjobtracker.h"
|
||||
#include "renderticket.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class MultiCamNode;
|
||||
|
||||
/**
|
||||
* @brief Manager for dynamically caching a sequence in the background
|
||||
*
|
||||
* Intended to be used with a Viewer to dynamically cache parts of a sequence based on the playhead.
|
||||
*
|
||||
* De-Qt notes:
|
||||
* - No longer a QObject. The ProjectCopier is owned via std::unique_ptr.
|
||||
* - The former signals (stop_cache_proxy_tasks, signal_cache_proxy_task_progress)
|
||||
* are gone; the facade re-emits progress via oakengine_event if needed.
|
||||
* - The former single-shot QTimer requeue delay is replaced by an explicit
|
||||
* pending flag: when a node copy is not ready yet, try_render() sets
|
||||
* delayed_requeue_pending_ and the facade is expected to call try_render()
|
||||
* again after requeue_delay_ms() milliseconds (single-threaded semantics
|
||||
* preserved; no worker thread calls into the graph).
|
||||
*/
|
||||
class PreviewAutoCacher {
|
||||
public:
|
||||
PreviewAutoCacher();
|
||||
|
||||
virtual ~PreviewAutoCacher();
|
||||
|
||||
RenderTicketPtr get_single_frame(ViewerOutput *viewer, const Rational &t,
|
||||
bool dry = false);
|
||||
RenderTicketPtr get_single_frame(Node *n, ViewerOutput *viewer,
|
||||
const Rational &t, bool dry = false);
|
||||
|
||||
RenderTicketPtr get_range_of_audio(ViewerOutput *viewer, TimeRange range);
|
||||
|
||||
void clear_single_frame_renders();
|
||||
void clear_single_frame_renders_that_arent_running();
|
||||
|
||||
/**
|
||||
* @brief Set the viewer node to auto-cache
|
||||
*/
|
||||
void set_project(Project *project);
|
||||
|
||||
/**
|
||||
* @brief Force a certain range to be cached
|
||||
*
|
||||
* Usually, PreviewAutoCacher caches a user-defined range around the playhead, however there are
|
||||
* times they may want certain non-playhead-related time ranges to be cached (i.e. entire sequence
|
||||
* or in/out range), so that can be set here.
|
||||
*/
|
||||
void force_cache_range(ViewerOutput *context, const TimeRange &range);
|
||||
|
||||
/**
|
||||
* @brief Updates the range of frames to auto-cache
|
||||
*/
|
||||
void set_playhead(const Rational &playhead);
|
||||
|
||||
/**
|
||||
* @brief Call cancel on all currently running video tasks
|
||||
*
|
||||
* Signalling cancel to a video task indicates that we're no longer interested in its end result.
|
||||
* This does not end all video tasks immediately, the RenderManager will do what it can to speed
|
||||
* up finishing the task. The RenderManager will also return "no result", which can be checked
|
||||
* with watcher->HasResult.
|
||||
*/
|
||||
void cancel_video_tasks(bool and_wait_for_them_to_finish = false);
|
||||
void cancel_audio_tasks(bool and_wait_for_them_to_finish = false);
|
||||
|
||||
bool is_rendering_custom_range() const;
|
||||
|
||||
void set_renders_paused(bool e);
|
||||
void set_thumbnails_paused(bool e);
|
||||
|
||||
void set_multicam_node(MultiCamNode *n)
|
||||
{
|
||||
multicam_ = n;
|
||||
}
|
||||
|
||||
void set_ignore_cache_requests(bool e)
|
||||
{
|
||||
ignore_cache_requests_ = e;
|
||||
}
|
||||
|
||||
void set_display_color_processor(ColorProcessorPtr processor)
|
||||
{
|
||||
display_color_processor_ = processor;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Progress/throttling notifications formerly emitted as signals
|
||||
*
|
||||
* Module-internal explicit callbacks (wired by RenderManager / the facade).
|
||||
* cache_progress receives iterator fractions in [0,1];
|
||||
* stop_cache_proxy_tasks fires when a forced range finishes queueing.
|
||||
*/
|
||||
void set_cache_progress_callback(std::function<void(double)> cb)
|
||||
{
|
||||
cache_progress_callback_ = std::move(cb);
|
||||
}
|
||||
|
||||
void set_stop_cache_proxy_tasks_callback(std::function<void()> cb)
|
||||
{
|
||||
stop_cache_proxy_tasks_callback_ = std::move(cb);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Handler for when the RenderManager has returned rendered audio
|
||||
*
|
||||
* Formerly a private slot connected to RenderTicketWatcher::finished; now
|
||||
* registered as the watcher's explicit finished callback.
|
||||
*/
|
||||
void audio_rendered(RenderTicketWatcher *watcher);
|
||||
|
||||
/**
|
||||
* @brief Handler for when the RenderManager has returned rendered video frames
|
||||
*/
|
||||
void video_rendered(RenderTicketWatcher *watcher);
|
||||
|
||||
/**
|
||||
* @brief Handler for a completed audio conform
|
||||
*
|
||||
* Formerly connected to ConformManager::conform_ready; the facade (or the
|
||||
* codec wave) must call this when a conform finishes.
|
||||
*/
|
||||
void conform_finished();
|
||||
|
||||
/**
|
||||
* @brief Drop all state referencing the current (about-to-be-destroyed) project
|
||||
*
|
||||
* Replaces the Qt `Project::destroyed` connection. The facade owns Project
|
||||
* lifetime events and must call this before the Project is freed.
|
||||
*/
|
||||
void project_destroyed();
|
||||
|
||||
/**
|
||||
* @brief Generic function called whenever the frames to render need to be (re)queued
|
||||
*/
|
||||
void try_render();
|
||||
|
||||
/**
|
||||
* @brief Whether try_render() wants to be called again after requeue_delay()
|
||||
*
|
||||
* Replaces the single-shot delayed_requeue_timer_ (see class comment).
|
||||
*/
|
||||
bool delayed_requeue_pending() const
|
||||
{
|
||||
return delayed_requeue_pending_;
|
||||
}
|
||||
|
||||
void cancel_delayed_requeue()
|
||||
{
|
||||
delayed_requeue_pending_ = false;
|
||||
}
|
||||
|
||||
int requeue_delay_ms() const
|
||||
{
|
||||
return requeue_delay_ms_;
|
||||
}
|
||||
|
||||
private:
|
||||
RenderTicketWatcher *render_frame(Node *node, ViewerOutput *context,
|
||||
const Rational &time, PlaybackCache *cache,
|
||||
bool dry);
|
||||
|
||||
RenderTicketPtr render_audio(Node *node, ViewerOutput *context,
|
||||
const TimeRange &range, PlaybackCache *cache);
|
||||
|
||||
void connect_to_node_cache(Node *node);
|
||||
void disconnect_from_node_cache(Node *node);
|
||||
|
||||
void cancel_queued_single_frame_render();
|
||||
|
||||
void start_caching_range(const TimeRange &range, TimeRangeList *range_list,
|
||||
RenderJobTracker *tracker);
|
||||
void start_caching_video_range(ViewerOutput *context, PlaybackCache *cache,
|
||||
const TimeRange &range);
|
||||
void start_caching_audio_range(ViewerOutput *context, PlaybackCache *cache,
|
||||
const TimeRange &range);
|
||||
|
||||
void video_invalidated_from_node(ViewerOutput *context, PlaybackCache *cache,
|
||||
const olive::TimeRange &range);
|
||||
void audio_invalidated_from_node(ViewerOutput *context, PlaybackCache *cache,
|
||||
const olive::TimeRange &range);
|
||||
|
||||
/**
|
||||
* @brief Handler for when a cache reports a video change over a time range
|
||||
*
|
||||
* Formerly a slot using sender(); the cache is now passed explicitly by the
|
||||
* PlaybackCache requested callback.
|
||||
*/
|
||||
void video_invalidated_from_cache(PlaybackCache *cache,
|
||||
ViewerOutput *context,
|
||||
const olive::TimeRange &range);
|
||||
|
||||
/**
|
||||
* @brief Handler for when a cache reports an audio change over a time range
|
||||
*/
|
||||
void audio_invalidated_from_cache(PlaybackCache *cache,
|
||||
ViewerOutput *context,
|
||||
const olive::TimeRange &range);
|
||||
|
||||
void cancel_for_cache(PlaybackCache *cache);
|
||||
|
||||
void cache_proxy_task_cancelled();
|
||||
|
||||
Project *project_;
|
||||
|
||||
std::unique_ptr<ProjectCopier> copier_;
|
||||
|
||||
TimeRange cache_range_;
|
||||
|
||||
bool use_custom_range_;
|
||||
TimeRange custom_autocache_range_;
|
||||
|
||||
bool pause_renders_;
|
||||
bool pause_thumbnails_;
|
||||
|
||||
RenderTicketPtr single_frame_render_;
|
||||
std::map<RenderTicketWatcher *, std::vector<RenderTicketPtr>>
|
||||
video_immediate_passthroughs_;
|
||||
|
||||
// Replaces the single-shot QTimer: when a copied node is not available yet,
|
||||
// try_render() sets this flag and the facade re-calls try_render() after
|
||||
// requeue_delay_.
|
||||
bool delayed_requeue_pending_;
|
||||
int requeue_delay_ms_;
|
||||
|
||||
JobTime last_conform_task_;
|
||||
|
||||
std::vector<RenderTicketWatcher *> running_video_tasks_;
|
||||
std::vector<RenderTicketWatcher *> running_audio_tasks_;
|
||||
|
||||
ColorManager *copied_color_manager_;
|
||||
|
||||
struct VideoJob {
|
||||
Node *node;
|
||||
ViewerOutput *context;
|
||||
PlaybackCache *cache;
|
||||
TimeRange range;
|
||||
TimeRangeListFrameIterator iterator;
|
||||
};
|
||||
|
||||
struct VideoCacheData {
|
||||
RenderJobTracker job_tracker;
|
||||
};
|
||||
|
||||
struct AudioJob {
|
||||
Node *node;
|
||||
ViewerOutput *context;
|
||||
PlaybackCache *cache;
|
||||
TimeRange range;
|
||||
};
|
||||
|
||||
struct AudioCacheData {
|
||||
RenderJobTracker job_tracker;
|
||||
TimeRangeList needs_conform;
|
||||
ViewerOutput *context = nullptr;
|
||||
};
|
||||
|
||||
std::list<VideoJob> pending_video_jobs_;
|
||||
std::list<AudioJob> pending_audio_jobs_;
|
||||
|
||||
std::map<PlaybackCache *, VideoCacheData> video_cache_data_;
|
||||
std::map<PlaybackCache *, AudioCacheData> audio_cache_data_;
|
||||
|
||||
ColorProcessorPtr display_color_processor_;
|
||||
|
||||
std::function<void(double)> cache_progress_callback_;
|
||||
std::function<void()> stop_cache_proxy_tasks_callback_;
|
||||
|
||||
MultiCamNode *multicam_;
|
||||
|
||||
bool ignore_cache_requests_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_AUTOCACHER_H
|
||||
@@ -0,0 +1,377 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "projectcopier.h"
|
||||
|
||||
#include "group/group.h"
|
||||
#include "project/footage/footage.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
ProjectCopier::ProjectCopier()
|
||||
{
|
||||
original_ = nullptr;
|
||||
copy_ = new Project();
|
||||
}
|
||||
|
||||
void ProjectCopier::set_project(Project *project)
|
||||
{
|
||||
if (original_) {
|
||||
// Clear current project
|
||||
for (Node *n : created_nodes_) {
|
||||
copy_->remove_node(n);
|
||||
delete n;
|
||||
}
|
||||
created_nodes_.clear();
|
||||
copy_map_.clear();
|
||||
graph_update_queue_.clear();
|
||||
|
||||
// The Project signal connections (node_added etc.) were removed with
|
||||
// QObject; there is nothing to disconnect, the facade stops calling
|
||||
// the queue_* entry points when it detaches this copier.
|
||||
}
|
||||
|
||||
original_ = project;
|
||||
|
||||
if (original_) {
|
||||
// NOTE: the QObject `destroyed` connection that nulled `original_` is
|
||||
// gone. The owner (RenderManager/PreviewAutoCacher via the facade)
|
||||
// must call set_project(nullptr) before destroying the project.
|
||||
|
||||
// Add all nodes
|
||||
for (size_t i = 0; i < copy_->nodes().size(); i++) {
|
||||
insert_into_copy_map(original_->nodes().at(i), copy_->nodes().at(i));
|
||||
}
|
||||
|
||||
for (size_t i = copy_->nodes().size(); i < original_->nodes().size();
|
||||
i++) {
|
||||
do_node_add(original_->nodes().at(i));
|
||||
}
|
||||
|
||||
// Add all connections
|
||||
for (Node *node : original_->nodes()) {
|
||||
for (auto it = node->input_connections().cbegin();
|
||||
it != node->input_connections().cend(); it++) {
|
||||
do_edge_add(it->second, it->first);
|
||||
}
|
||||
}
|
||||
|
||||
// Copy project settings
|
||||
Project::copy_settings(original_, copy_);
|
||||
|
||||
// The copied project is only used as an in-memory render proxy. Mark it so
|
||||
// downstream code (e.g. RenderWorkerPool) knows it is safe to reset its
|
||||
// modified flag after serializing a snapshot. (Was the QObject dynamic
|
||||
// property "_oak_render_proxy"; stored as a project setting now. Set
|
||||
// after copy_settings() because that replaces the whole settings map.)
|
||||
copy_->set_setting("_oak_render_proxy", "1");
|
||||
|
||||
// Ensure graph change value is just before the sync value
|
||||
update_graph_change_value();
|
||||
update_last_synced_value();
|
||||
|
||||
// The Project signal connections for future node additions/deletions
|
||||
// were removed with QObject; the facade calls the queue_* entry
|
||||
// points directly.
|
||||
}
|
||||
}
|
||||
|
||||
void ProjectCopier::process_update_queue()
|
||||
{
|
||||
bool copy_changed = false;
|
||||
|
||||
// Iterate everything that happened to the graph and do the same thing on our end
|
||||
while (!graph_update_queue_.empty()) {
|
||||
QueuedJob job = graph_update_queue_.front();
|
||||
graph_update_queue_.pop_front();
|
||||
copy_changed = true;
|
||||
|
||||
switch (job.type) {
|
||||
case QueuedJob::k_node_added:
|
||||
do_node_add(job.node);
|
||||
break;
|
||||
case QueuedJob::k_node_removed:
|
||||
do_node_remove(job.node);
|
||||
break;
|
||||
case QueuedJob::k_edge_added:
|
||||
do_edge_add(job.output, job.input);
|
||||
break;
|
||||
case QueuedJob::k_edge_removed:
|
||||
do_edge_remove(job.output, job.input);
|
||||
break;
|
||||
case QueuedJob::k_value_changed:
|
||||
do_value_change(job.input);
|
||||
break;
|
||||
case QueuedJob::k_value_hint_changed:
|
||||
do_value_hint_change(job.input);
|
||||
break;
|
||||
case QueuedJob::k_project_setting_changed:
|
||||
do_project_setting_change(job.key, job.value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// The copied project is not saved, so its modified flag is only used by the
|
||||
// render worker pool to decide whether the serialized graph snapshot is stale.
|
||||
// Mark it modified whenever the copy has actually changed.
|
||||
if (copy_changed) {
|
||||
copy_->set_modified(true);
|
||||
}
|
||||
|
||||
// Indicate that we have synchronized to this point, which is compared with the graph change
|
||||
// time to see if our copied graph is up to date
|
||||
update_last_synced_value();
|
||||
}
|
||||
|
||||
void ProjectCopier::do_node_add(Node *node)
|
||||
{
|
||||
if (dynamic_cast<NodeGroup *>(node)) {
|
||||
// Group nodes are just dummy nodes, no need to copy them
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy node
|
||||
Node *copy = node->copy();
|
||||
|
||||
// Add to project (takes ownership, was QObject parentship)
|
||||
copy_->add_node(copy);
|
||||
|
||||
// Disable caches for copy
|
||||
copy->set_caches_enabled(false);
|
||||
|
||||
// Copy cache UUIDs
|
||||
copy->copy_cache_uuids_from(node);
|
||||
|
||||
// Insert into map
|
||||
insert_into_copy_map(node, copy);
|
||||
|
||||
// Keep track of our nodes
|
||||
created_nodes_.push_back(copy);
|
||||
}
|
||||
|
||||
void ProjectCopier::do_node_remove(Node *node)
|
||||
{
|
||||
// Find our copy and remove it
|
||||
Node *copy = nullptr;
|
||||
auto it = copy_map_.find(node);
|
||||
if (it != copy_map_.end()) {
|
||||
copy = it->second;
|
||||
copy_map_.erase(it);
|
||||
}
|
||||
|
||||
// Disconnect from node's caches
|
||||
if (removed_node_handler_) {
|
||||
removed_node_handler_(node);
|
||||
}
|
||||
|
||||
// Remove from created list
|
||||
auto created_it =
|
||||
std::find(created_nodes_.begin(), created_nodes_.end(), copy);
|
||||
if (created_it != created_nodes_.end()) {
|
||||
created_nodes_.erase(created_it);
|
||||
}
|
||||
|
||||
// Detach from the owning project, then delete it
|
||||
if (copy) {
|
||||
copy_->remove_node(copy);
|
||||
}
|
||||
delete copy;
|
||||
}
|
||||
|
||||
void ProjectCopier::do_edge_add(Node *output, const NodeInput &input)
|
||||
{
|
||||
// Create same connection with our copied graph
|
||||
Node *our_output = get_copy(output);
|
||||
Node *our_input = get_copy(input.node());
|
||||
|
||||
Node::connect_edge(our_output,
|
||||
NodeInput(our_input, input.input(), input.element()));
|
||||
}
|
||||
|
||||
void ProjectCopier::do_edge_remove(Node *output, const NodeInput &input)
|
||||
{
|
||||
// Remove same connection with our copied graph
|
||||
Node *our_output = get_copy(output);
|
||||
Node *our_input = get_copy(input.node());
|
||||
|
||||
Node::disconnect_edge(our_output,
|
||||
NodeInput(our_input, input.input(), input.element()));
|
||||
}
|
||||
|
||||
void ProjectCopier::do_value_change(const NodeInput &input)
|
||||
{
|
||||
if (dynamic_cast<NodeGroup *>(input.node())) {
|
||||
// Group nodes are just dummy nodes, no need to copy them
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy all values to our graph
|
||||
Node *our_input = get_copy(input.node());
|
||||
Node::copy_values_of_element(input.node(), our_input, input.input(),
|
||||
input.element());
|
||||
}
|
||||
|
||||
void ProjectCopier::do_value_hint_change(const NodeInput &input)
|
||||
{
|
||||
if (dynamic_cast<NodeGroup *>(input.node())) {
|
||||
// Group nodes are just dummy nodes, no need to copy them
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy value hint to our graph
|
||||
Node *our_input = get_copy(input.node());
|
||||
Node::ValueHint hint =
|
||||
input.node()->get_value_hint_for_input(input.input(), input.element());
|
||||
our_input->set_value_hint_for_input(input.input(), hint, input.element());
|
||||
}
|
||||
|
||||
void ProjectCopier::do_project_setting_change(const std::string &key,
|
||||
const std::string &value)
|
||||
{
|
||||
copy_->set_setting(key, value);
|
||||
}
|
||||
|
||||
void ProjectCopier::insert_into_copy_map(Node *node, Node *copy)
|
||||
{
|
||||
// Insert into map
|
||||
copy_map_.insert({ node, copy });
|
||||
|
||||
// Copy parameters
|
||||
Node::copy_inputs(node, copy, false);
|
||||
|
||||
// Sync Footage proxy state (which is not stored as a Node input)
|
||||
if (Footage *src_footage = dynamic_cast<Footage *>(node)) {
|
||||
if (dynamic_cast<Footage *>(copy)) {
|
||||
// The Footage::proxy_settings_changed signal was removed with
|
||||
// QObject; the facade calls sync_footage_proxy_settings() when
|
||||
// proxy settings change.
|
||||
sync_footage_proxy_settings(src_footage);
|
||||
}
|
||||
}
|
||||
|
||||
// Connect to node's cache
|
||||
if (added_node_handler_) {
|
||||
added_node_handler_(node);
|
||||
}
|
||||
}
|
||||
|
||||
void ProjectCopier::sync_footage_proxy_settings(Footage *source)
|
||||
{
|
||||
Footage *copy = get_copy(source);
|
||||
if (!copy) {
|
||||
fprintf(stderr,
|
||||
"ProjectCopier::SyncFootageProxySettings: no copy for %s\n",
|
||||
source->filename().c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
fprintf(stderr, "ProjectCopier::SyncFootageProxySettings: %s enabled=%d->%d "
|
||||
"state=%s\n",
|
||||
source->filename().c_str(), source->proxy_enabled(),
|
||||
copy->proxy_enabled(),
|
||||
ProxyManager::proxy_state_to_string(source->proxy_state()).c_str());
|
||||
|
||||
copy->set_proxy(source->proxy_path(), source->proxy_state(),
|
||||
source->proxy_video_stream_index(),
|
||||
source->proxy_preset_version(), source->proxy_enabled());
|
||||
|
||||
if (Project *cp = copy->project()) {
|
||||
cp->set_modified(true);
|
||||
}
|
||||
}
|
||||
|
||||
void ProjectCopier::queue_node_add(Node *node)
|
||||
{
|
||||
graph_update_queue_.push_back(
|
||||
{ QueuedJob::k_node_added, node, NodeInput(), nullptr, std::string(),
|
||||
std::string() });
|
||||
update_graph_change_value();
|
||||
}
|
||||
|
||||
void ProjectCopier::queue_node_remove(Node *node)
|
||||
{
|
||||
graph_update_queue_.push_back(
|
||||
{ QueuedJob::k_node_removed, node, NodeInput(), nullptr, std::string(),
|
||||
std::string() });
|
||||
update_graph_change_value();
|
||||
}
|
||||
|
||||
void ProjectCopier::queue_edge_add(Node *output, const NodeInput &input)
|
||||
{
|
||||
graph_update_queue_.push_back(
|
||||
{ QueuedJob::k_edge_added, nullptr, input, output, std::string(),
|
||||
std::string() });
|
||||
update_graph_change_value();
|
||||
}
|
||||
|
||||
void ProjectCopier::queue_edge_remove(Node *output, const NodeInput &input)
|
||||
{
|
||||
graph_update_queue_.push_back(
|
||||
{ QueuedJob::k_edge_removed, nullptr, input, output, std::string(),
|
||||
std::string() });
|
||||
update_graph_change_value();
|
||||
}
|
||||
|
||||
void ProjectCopier::queue_value_change(const NodeInput &input)
|
||||
{
|
||||
/*for (auto it = graph_update_queue_.begin(); it != graph_update_queue_.end(); ) {
|
||||
if (it->type == QueuedJob::kValueChanged && it->input == input) {
|
||||
it = graph_update_queue_.erase(it);
|
||||
} else {
|
||||
it++;
|
||||
}
|
||||
}*/
|
||||
|
||||
graph_update_queue_.push_back(
|
||||
{ QueuedJob::k_value_changed, nullptr, input, nullptr, std::string(),
|
||||
std::string() });
|
||||
update_graph_change_value();
|
||||
}
|
||||
|
||||
void ProjectCopier::queue_value_hint_change(const NodeInput &input)
|
||||
{
|
||||
graph_update_queue_.push_back(
|
||||
{ QueuedJob::k_value_hint_changed, nullptr, input, nullptr,
|
||||
std::string(), std::string() });
|
||||
update_graph_change_value();
|
||||
}
|
||||
|
||||
void ProjectCopier::queue_project_setting_change(const std::string &key,
|
||||
const std::string &value)
|
||||
{
|
||||
graph_update_queue_.push_back(
|
||||
{ QueuedJob::k_project_setting_changed, nullptr, NodeInput(), nullptr,
|
||||
key, value });
|
||||
update_graph_change_value();
|
||||
}
|
||||
|
||||
void ProjectCopier::update_graph_change_value()
|
||||
{
|
||||
graph_changed_time_.acquire();
|
||||
}
|
||||
|
||||
void ProjectCopier::update_last_synced_value()
|
||||
{
|
||||
last_update_time_.acquire();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_PROJECTCOPIER_H
|
||||
#define OAK_PROJECTCOPIER_H
|
||||
|
||||
#include <functional>
|
||||
#include <list>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "common/jobtime.h"
|
||||
#include "project.h"
|
||||
#include "project/footage/footage.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class ProjectCopier {
|
||||
public:
|
||||
ProjectCopier();
|
||||
|
||||
~ProjectCopier()
|
||||
{
|
||||
delete copy_;
|
||||
}
|
||||
|
||||
void set_project(Project *project);
|
||||
|
||||
template <typename T> T *get_copy(T *original)
|
||||
{
|
||||
auto it = copy_map_.find(original);
|
||||
return it != copy_map_.end() ? static_cast<T *>(it->second) : nullptr;
|
||||
}
|
||||
|
||||
template <typename T> T *get_original(T *copy)
|
||||
{
|
||||
for (const auto &e : copy_map_) {
|
||||
if (e.second == copy) {
|
||||
return static_cast<T *>(e.first);
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Project *get_copied_project() const
|
||||
{
|
||||
return copy_;
|
||||
}
|
||||
|
||||
const std::unordered_map<Node *, Node *> &get_node_map() const
|
||||
{
|
||||
return copy_map_;
|
||||
}
|
||||
|
||||
const JobTime &get_graph_change_time() const
|
||||
{
|
||||
return graph_changed_time_;
|
||||
}
|
||||
const JobTime &get_last_update_time() const
|
||||
{
|
||||
return last_update_time_;
|
||||
}
|
||||
|
||||
bool has_updates_in_queue() const
|
||||
{
|
||||
return !graph_update_queue_.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Process all changes to internal NodeGraph copy
|
||||
*
|
||||
* PreviewAutoCacher staggers updates to its internal NodeGraph copy, only applying them when the
|
||||
* RenderManager is not reading from it. This function is called when such an opportunity arises.
|
||||
*/
|
||||
void process_update_queue();
|
||||
|
||||
// Explicit intra-module callbacks replacing the `added_node` /
|
||||
// `removed_node` signals (subscriber: PreviewAutoCacher)
|
||||
void set_added_node_handler(std::function<void(Node *)> handler)
|
||||
{
|
||||
added_node_handler_ = std::move(handler);
|
||||
}
|
||||
void set_removed_node_handler(std::function<void(Node *)> handler)
|
||||
{
|
||||
removed_node_handler_ = std::move(handler);
|
||||
}
|
||||
|
||||
// Formerly slots connected to Project's (now removed) signals. Kept as
|
||||
// public methods; the facade / project wave wires the notifications to
|
||||
// these entry points.
|
||||
void queue_node_add(Node *node);
|
||||
|
||||
void queue_node_remove(Node *node);
|
||||
|
||||
void queue_edge_add(Node *output, const NodeInput &input);
|
||||
|
||||
void queue_edge_remove(Node *output, const NodeInput &input);
|
||||
|
||||
void queue_value_change(const NodeInput &input);
|
||||
|
||||
void queue_value_hint_change(const NodeInput &input);
|
||||
|
||||
void queue_project_setting_change(const std::string &key,
|
||||
const std::string &value);
|
||||
|
||||
// Formerly connected to Footage::proxy_settings_changed (signal removed);
|
||||
// must now be called by whoever owns that notification (facade wave).
|
||||
void sync_footage_proxy_settings(Footage *source);
|
||||
|
||||
private:
|
||||
void do_node_add(Node *node);
|
||||
void do_node_remove(Node *node);
|
||||
void do_edge_add(Node *output, const NodeInput &input);
|
||||
void do_edge_remove(Node *output, const NodeInput &input);
|
||||
void do_value_change(const NodeInput &input);
|
||||
void do_value_hint_change(const NodeInput &input);
|
||||
void do_project_setting_change(const std::string &key,
|
||||
const std::string &value);
|
||||
|
||||
void insert_into_copy_map(Node *node, Node *copy);
|
||||
|
||||
void update_graph_change_value();
|
||||
void update_last_synced_value();
|
||||
|
||||
Project *original_;
|
||||
Project *copy_;
|
||||
|
||||
class QueuedJob {
|
||||
public:
|
||||
enum Type {
|
||||
k_node_added,
|
||||
k_node_removed,
|
||||
k_edge_added,
|
||||
k_edge_removed,
|
||||
k_value_changed,
|
||||
k_value_hint_changed,
|
||||
k_project_setting_changed
|
||||
};
|
||||
|
||||
Type type;
|
||||
Node *node;
|
||||
NodeInput input;
|
||||
Node *output;
|
||||
|
||||
std::string key;
|
||||
std::string value;
|
||||
};
|
||||
|
||||
std::list<QueuedJob> graph_update_queue_;
|
||||
std::unordered_map<Node *, Node *> copy_map_;
|
||||
std::vector<Node *> created_nodes_;
|
||||
|
||||
JobTime graph_changed_time_;
|
||||
JobTime last_update_time_;
|
||||
|
||||
std::function<void(Node *)> added_node_handler_;
|
||||
std::function<void(Node *)> removed_node_handler_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_PROJECTCOPIER_H
|
||||
@@ -0,0 +1,57 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_RENDERCACHE_H
|
||||
#define OAK_RENDERCACHE_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
#include "codec/decoder.h"
|
||||
#include "variant.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
template <typename K, typename V> class RenderCache : public std::map<K, V> {
|
||||
public:
|
||||
std::mutex &mutex()
|
||||
{
|
||||
return mutex_;
|
||||
}
|
||||
|
||||
private:
|
||||
std::mutex mutex_;
|
||||
};
|
||||
|
||||
struct DecoderPair {
|
||||
DecoderPtr decoder = nullptr;
|
||||
int64_t last_modified = 0;
|
||||
};
|
||||
|
||||
using DecoderCache = RenderCache<Decoder::CodecStream, DecoderPair>;
|
||||
using ShaderCache = RenderCache<std::string, Variant>;
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_RENDERCACHE_H
|
||||
@@ -0,0 +1,197 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "renderer.h"
|
||||
|
||||
#include <chrono>
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
static int64_t current_msecs_since_epoch()
|
||||
{
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch())
|
||||
.count();
|
||||
}
|
||||
|
||||
Renderer::Renderer()
|
||||
: lifetime_(std::make_shared<RendererLifetime>())
|
||||
, owner_thread_(std::this_thread::get_id())
|
||||
{
|
||||
}
|
||||
|
||||
Renderer::~Renderer()
|
||||
{
|
||||
destroyed_ = true;
|
||||
if (lifetime_) {
|
||||
lifetime_->alive = false;
|
||||
}
|
||||
}
|
||||
|
||||
TexturePtr Renderer::create_texture(const VideoParams ¶ms, const void *data,
|
||||
int linesize)
|
||||
{
|
||||
Variant v;
|
||||
|
||||
if (use_texture_cache) {
|
||||
std::lock_guard<std::mutex> locker(texture_cache_lock_);
|
||||
for (auto it = texture_cache_.begin(); it != texture_cache_.end();
|
||||
it++) {
|
||||
if (it->width == params.effective_width() &&
|
||||
it->height == params.effective_height() &&
|
||||
it->depth == params.effective_depth() &&
|
||||
it->format == params.format() &&
|
||||
it->channel_count == params.channel_count()) {
|
||||
v = it->handle;
|
||||
texture_cache_.erase(it);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (v.is_null()) {
|
||||
v = create_native_texture(params.effective_width(),
|
||||
params.effective_height(),
|
||||
params.effective_depth(), params.format(),
|
||||
params.channel_count(), data, linesize);
|
||||
} else if (data) {
|
||||
upload_to_texture(v, params, data, linesize);
|
||||
} else {
|
||||
this->flush();
|
||||
}
|
||||
|
||||
return create_texture_from_native_handle(v, params);
|
||||
}
|
||||
|
||||
void Renderer::destroy_texture(Texture *texture)
|
||||
{
|
||||
if (destroyed_) {
|
||||
return;
|
||||
}
|
||||
if (use_texture_cache) {
|
||||
// HACK: Dirty, dirty hack. OpenGL uses "contexts" to store all of its data, and each context
|
||||
// can only be used by the thread that created it. However there are also "shared contexts"
|
||||
// where assets from one context can be used in another. We use shared contexts so that
|
||||
// textures rendered in the background can be displayed on the screen, travelling from
|
||||
// a background thread to the main UI thread. However, when that texture is destroyed, it
|
||||
// comes back here to be placed in the texture cache. But that leads to a race condition
|
||||
// because it will call the background thread's renderer in the main thread. Since all
|
||||
// assets are shared, we could technically just get the texture to call "destroy" in the
|
||||
// viewer's renderer instance, but that would mean all textures would end up stranded
|
||||
// there unusable by the background renderer, negating the very advantage of the texture
|
||||
// cache in the first place. Therefore, we simply allow the thread calling to happen, and
|
||||
// use mutexes to prevent race conditions.
|
||||
//
|
||||
// Presumably Vulkan would not have this issue because it allows for application-wide
|
||||
// instances and multithreading.
|
||||
texture_cache_lock_.lock();
|
||||
texture_cache_.push_back(
|
||||
{ texture->params().effective_width(),
|
||||
texture->params().effective_height(),
|
||||
texture->params().effective_depth(), texture->params().format(),
|
||||
texture->params().channel_count(), texture->id(),
|
||||
current_msecs_since_epoch() });
|
||||
texture_cache_lock_.unlock();
|
||||
|
||||
if (called_on_owner_thread()) {
|
||||
clear_old_textures();
|
||||
}
|
||||
} else {
|
||||
destroy_native_texture(texture->id());
|
||||
}
|
||||
}
|
||||
|
||||
Variant Renderer::get_default_shader()
|
||||
{
|
||||
std::lock_guard<std::mutex> locker(color_cache_mutex_);
|
||||
|
||||
if (default_shader_.is_null()) {
|
||||
default_shader_ = create_native_shader(ShaderCode());
|
||||
}
|
||||
|
||||
return default_shader_;
|
||||
}
|
||||
|
||||
void Renderer::destroy()
|
||||
{
|
||||
if (!default_shader_.is_null()) {
|
||||
destroy_native_shader(default_shader_);
|
||||
default_shader_ = Variant();
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> locker(color_cache_mutex_);
|
||||
|
||||
// Destroy the cached native shaders explicitly. The LUT textures are
|
||||
// TexturePtrs whose destructors call DestroyTexture(), so the cache must
|
||||
// be cleared while the renderer is still alive for those to be honored.
|
||||
for (auto it = color_cache_.begin(); it != color_cache_.end(); it++) {
|
||||
if (!it->second.compiled_shader.is_null()) {
|
||||
destroy_native_shader(it->second.compiled_shader);
|
||||
}
|
||||
}
|
||||
color_cache_.clear();
|
||||
}
|
||||
|
||||
if (!interlace_texture_.is_null()) {
|
||||
destroy_native_shader(interlace_texture_);
|
||||
interlace_texture_ = Variant();
|
||||
}
|
||||
|
||||
for (auto it = texture_cache_.begin(); it != texture_cache_.end(); it++) {
|
||||
destroy_native_texture(it->handle);
|
||||
}
|
||||
texture_cache_.clear();
|
||||
|
||||
destroyed_ = true;
|
||||
if (lifetime_) {
|
||||
lifetime_->alive = false;
|
||||
}
|
||||
|
||||
destroy_internal();
|
||||
}
|
||||
|
||||
TexturePtr Renderer::create_texture_from_native_handle(const Variant &v,
|
||||
const VideoParams ¶ms)
|
||||
{
|
||||
if (v.is_null()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return std::make_shared<Texture>(this, v, params, lifetime_);
|
||||
}
|
||||
|
||||
void Renderer::clear_old_textures()
|
||||
{
|
||||
std::lock_guard<std::mutex> locker(texture_cache_lock_);
|
||||
|
||||
for (auto it = texture_cache_.begin(); it != texture_cache_.end();) {
|
||||
if (it->accessed < current_msecs_since_epoch() - max_texture_life) {
|
||||
destroy_native_texture(it->handle);
|
||||
it = texture_cache_.erase(it);
|
||||
} else {
|
||||
it++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace olive
|
||||
@@ -0,0 +1,245 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_RENDERCONTEXT_H
|
||||
#define OAK_RENDERCONTEXT_H
|
||||
|
||||
#include <atomic>
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "define.h"
|
||||
#include "mathtypes.h"
|
||||
#include "shadercode.h"
|
||||
#include "variant.h"
|
||||
#include "videoparams.h"
|
||||
#include "texture.h"
|
||||
#include "olive/core/util/color.h"
|
||||
|
||||
// Forward declarations to keep the render core header lightweight
|
||||
namespace olive
|
||||
{
|
||||
class ColorTransformJob;
|
||||
class Node;
|
||||
}
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class ShaderJob;
|
||||
|
||||
class Renderer {
|
||||
public:
|
||||
Renderer();
|
||||
virtual ~Renderer();
|
||||
|
||||
virtual bool init() = 0;
|
||||
|
||||
TexturePtr create_texture(const VideoParams ¶ms,
|
||||
const void *data = nullptr, int linesize = 0);
|
||||
|
||||
void destroy_texture(Texture *texture);
|
||||
|
||||
virtual void blit_to_texture(Variant shader, olive::AcceleratedJob &job,
|
||||
olive::Texture *destination,
|
||||
bool clear_destination = true)
|
||||
{
|
||||
blit(shader, job, destination, destination->params(),
|
||||
clear_destination);
|
||||
}
|
||||
|
||||
void blit(Variant shader, olive::AcceleratedJob &job,
|
||||
olive::VideoParams params, bool clear_destination = true)
|
||||
{
|
||||
blit(shader, job, nullptr, params, clear_destination);
|
||||
}
|
||||
|
||||
void blit_color_managed(const ColorTransformJob &color_job,
|
||||
Texture *destination, const VideoParams ¶ms);
|
||||
void blit_color_managed(const ColorTransformJob &job, Texture *destination)
|
||||
{
|
||||
blit_color_managed(job, destination, destination->params());
|
||||
}
|
||||
void blit_color_managed(const ColorTransformJob &job,
|
||||
const VideoParams ¶ms)
|
||||
{
|
||||
blit_color_managed(job, nullptr, params);
|
||||
}
|
||||
|
||||
TexturePtr interlace_texture(TexturePtr top, TexturePtr bottom,
|
||||
const VideoParams ¶ms);
|
||||
|
||||
Variant get_default_shader();
|
||||
|
||||
void destroy();
|
||||
|
||||
virtual void post_destroy() = 0;
|
||||
|
||||
virtual void post_init() = 0;
|
||||
|
||||
virtual void clear_destination(olive::Texture *texture = nullptr,
|
||||
double r = 0.0, double g = 0.0,
|
||||
double b = 0.0, double a = 1.0) = 0;
|
||||
|
||||
virtual Variant create_native_shader(olive::ShaderCode code) = 0;
|
||||
|
||||
virtual void destroy_native_shader(Variant shader) = 0;
|
||||
|
||||
virtual void upload_to_texture(const Variant &handle,
|
||||
const VideoParams ¶ms, const void *data,
|
||||
int linesize) = 0;
|
||||
|
||||
virtual void download_from_texture(const Variant &handle,
|
||||
const VideoParams ¶ms, void *data,
|
||||
int linesize) = 0;
|
||||
|
||||
virtual void flush() = 0;
|
||||
|
||||
virtual Color get_pixel_from_texture(olive::Texture *texture,
|
||||
const PointF &pt) = 0;
|
||||
std::shared_ptr<RendererLifetime> get_lifetime() const
|
||||
{
|
||||
return lifetime_;
|
||||
}
|
||||
|
||||
virtual bool is_open_gl() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
virtual bool is_vulkan() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Attach a texture as the current output destination for OFX plugin
|
||||
* OpenGL rendering.
|
||||
*
|
||||
* Default implementation is a no-op. OpenGL-based renderers override this
|
||||
* to bind the texture as a framebuffer render target.
|
||||
*/
|
||||
virtual void attach_output_texture(olive::Texture *texture)
|
||||
{
|
||||
(void)texture;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Detach the current OFX plugin OpenGL output texture.
|
||||
*
|
||||
* Default implementation is a no-op.
|
||||
*/
|
||||
virtual void detach_output_texture()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Thread-affinity bookkeeping, replacing QObject::thread()
|
||||
*
|
||||
* A Renderer starts out owned by its creating thread (like a QObject);
|
||||
* RenderThread re-assigns ownership when it adopts the renderer, the way
|
||||
* moveToThread() used to.
|
||||
*/
|
||||
void set_owner_thread_to_current()
|
||||
{
|
||||
owner_thread_ = std::this_thread::get_id();
|
||||
}
|
||||
|
||||
void clear_owner_thread()
|
||||
{
|
||||
owner_thread_ = std::thread::id();
|
||||
}
|
||||
|
||||
bool called_on_owner_thread() const
|
||||
{
|
||||
return std::this_thread::get_id() == owner_thread_;
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual void blit(Variant shader, olive::AcceleratedJob &job,
|
||||
olive::Texture *destination,
|
||||
olive::VideoParams destination_params,
|
||||
bool clear_destination) = 0;
|
||||
virtual Variant create_native_texture(int width, int height, int depth,
|
||||
PixelFormat format, int channel_count,
|
||||
const void *data = nullptr,
|
||||
int linesize = 0) = 0;
|
||||
|
||||
virtual void destroy_native_texture(Variant texture) = 0;
|
||||
|
||||
virtual void destroy_internal() = 0;
|
||||
|
||||
private:
|
||||
std::atomic<bool> destroyed_{ false };
|
||||
std::shared_ptr<RendererLifetime> lifetime_;
|
||||
std::thread::id owner_thread_;
|
||||
struct ColorContext {
|
||||
struct LUT {
|
||||
TexturePtr texture;
|
||||
Texture::Interpolation interpolation;
|
||||
std::string name;
|
||||
};
|
||||
|
||||
Variant compiled_shader;
|
||||
std::vector<LUT> lut3d_textures;
|
||||
std::vector<LUT> lut1d_textures;
|
||||
};
|
||||
|
||||
TexturePtr create_texture_from_native_handle(const Variant &v,
|
||||
const VideoParams ¶ms);
|
||||
|
||||
bool get_color_context(const ColorTransformJob &color_job, ColorContext *ctx);
|
||||
|
||||
void clear_old_textures();
|
||||
|
||||
std::map<std::string, ColorContext> color_cache_;
|
||||
|
||||
struct CachedTexture {
|
||||
int width;
|
||||
int height;
|
||||
int depth;
|
||||
PixelFormat format;
|
||||
int channel_count;
|
||||
Variant handle;
|
||||
int64_t accessed;
|
||||
};
|
||||
|
||||
static const int max_texture_life = 5000;
|
||||
static const bool use_texture_cache = true;
|
||||
std::list<CachedTexture> texture_cache_;
|
||||
|
||||
std::mutex color_cache_mutex_;
|
||||
|
||||
Variant default_shader_;
|
||||
|
||||
Variant interlace_texture_;
|
||||
|
||||
std::mutex texture_cache_lock_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_RENDERCONTEXT_H
|
||||
@@ -0,0 +1,75 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "renderjobtracker.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
void RenderJobTracker::insert(const TimeRange &range, JobTime job_time)
|
||||
{
|
||||
// First remove any ranges that overlap this one (code copied from TimeRangeList::remove)
|
||||
TimeRangeList::util_remove(&jobs_, range);
|
||||
|
||||
// Now append the job
|
||||
TimeRangeWithJob job(range, job_time);
|
||||
jobs_.push_back(job);
|
||||
}
|
||||
|
||||
void RenderJobTracker::insert(const TimeRangeList &ranges, JobTime job_time)
|
||||
{
|
||||
for (const TimeRange &r : ranges) {
|
||||
insert(r, job_time);
|
||||
}
|
||||
}
|
||||
|
||||
void RenderJobTracker::clear()
|
||||
{
|
||||
jobs_.clear();
|
||||
}
|
||||
|
||||
bool RenderJobTracker::isCurrent(const Rational &time, JobTime job_time) const
|
||||
{
|
||||
for (auto it = jobs_.crbegin(); it != jobs_.crend(); it++) {
|
||||
if (it->contains(time)) {
|
||||
return job_time >= it->get_job_time();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
TimeRangeList
|
||||
RenderJobTracker::getCurrentSubRanges(const TimeRange &range,
|
||||
const JobTime &job_time) const
|
||||
{
|
||||
TimeRangeList current_ranges;
|
||||
|
||||
for (auto it = jobs_.crbegin(); it != jobs_.crend(); it++) {
|
||||
if (job_time >= it->get_job_time() && it->overlaps_with(range)) {
|
||||
current_ranges.insert(it->intersected(range));
|
||||
}
|
||||
}
|
||||
|
||||
return current_ranges;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_RENDERJOBTRACKER_H
|
||||
#define OAK_RENDERJOBTRACKER_H
|
||||
|
||||
#include <olive/core/core.h>
|
||||
|
||||
#include "common/jobtime.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
using namespace core;
|
||||
|
||||
class RenderJobTracker {
|
||||
public:
|
||||
RenderJobTracker() = default;
|
||||
|
||||
void insert(const TimeRange &range, JobTime job_time);
|
||||
void insert(const TimeRangeList &ranges, JobTime job_time);
|
||||
|
||||
void clear();
|
||||
|
||||
bool isCurrent(const Rational &time, JobTime job_time) const;
|
||||
|
||||
TimeRangeList getCurrentSubRanges(const TimeRange &range,
|
||||
const JobTime &job_time) const;
|
||||
|
||||
private:
|
||||
class TimeRangeWithJob : public TimeRange {
|
||||
public:
|
||||
TimeRangeWithJob() = default;
|
||||
TimeRangeWithJob(const TimeRange &range, const JobTime &job_time)
|
||||
{
|
||||
set_range(range.in(), range.out());
|
||||
job_time_ = job_time;
|
||||
}
|
||||
|
||||
JobTime get_job_time() const
|
||||
{
|
||||
return job_time_;
|
||||
}
|
||||
void set_job_time(JobTime jt)
|
||||
{
|
||||
job_time_ = jt;
|
||||
}
|
||||
|
||||
private:
|
||||
JobTime job_time_;
|
||||
};
|
||||
|
||||
std::vector<TimeRangeWithJob> jobs_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_RENDERJOBTRACKER_H
|
||||
@@ -0,0 +1,465 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "rendermanager.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
|
||||
#include "config/config.h"
|
||||
#ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
|
||||
#include "backend/dynamicrenderer.h"
|
||||
#endif
|
||||
#include "opengl/openglrenderer.h"
|
||||
#include "renderprocessor.h"
|
||||
#include "renderworkerpool.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
RenderManager *RenderManager::instance_ = nullptr;
|
||||
const Rational RenderManager::k_dry_run_interval = Rational(10);
|
||||
|
||||
static int64_t current_msecs_since_epoch()
|
||||
{
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch())
|
||||
.count();
|
||||
}
|
||||
|
||||
static std::string to_lower(std::string s)
|
||||
{
|
||||
std::transform(s.begin(), s.end(), s.begin(),
|
||||
[](unsigned char c) { return char(std::tolower(c)); });
|
||||
return s;
|
||||
}
|
||||
|
||||
RenderManager::Backend RenderManager::backend_from_string(const std::string &backend)
|
||||
{
|
||||
const std::string lower = to_lower(backend);
|
||||
if (lower == "vulkan") {
|
||||
return k_vulkan;
|
||||
}
|
||||
|
||||
if (lower == "multiprocess") {
|
||||
return k_multi_process;
|
||||
}
|
||||
|
||||
if (lower == "dummy") {
|
||||
return k_dummy;
|
||||
}
|
||||
|
||||
return k_open_gl;
|
||||
}
|
||||
|
||||
std::string RenderManager::backend_to_string(Backend backend)
|
||||
{
|
||||
switch (backend) {
|
||||
case k_open_gl:
|
||||
return "opengl";
|
||||
case k_vulkan:
|
||||
return "vulkan";
|
||||
case k_multi_process:
|
||||
return "multiprocess";
|
||||
case k_dummy:
|
||||
return "dummy";
|
||||
}
|
||||
|
||||
return "opengl";
|
||||
}
|
||||
|
||||
RenderManager::RenderManager()
|
||||
: backend_(backend_from_string(OAK_CONFIG("GraphicsBackend").toString()))
|
||||
, requested_backend_(backend_)
|
||||
, aggressive_gc_(0)
|
||||
, worker_pool_(nullptr)
|
||||
{
|
||||
if (backend_ == k_vulkan) {
|
||||
#ifndef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
|
||||
fprintf(stderr,
|
||||
"Vulkan backend requested but dynamic render backend is not "
|
||||
"enabled. Falling back to OpenGL.\n");
|
||||
// NOTE: the Qt original assigned the misspelled `kOpenGL` here, which
|
||||
// could never have compiled in this branch; k_open_gl is the intent.
|
||||
backend_ = k_open_gl;
|
||||
#endif
|
||||
}
|
||||
|
||||
if (backend_ == k_open_gl || backend_ == k_vulkan) {
|
||||
#ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
|
||||
auto *dynamic_renderer =
|
||||
new DynamicRenderer(backend_to_string(requested_backend_));
|
||||
if (!dynamic_renderer->load()) {
|
||||
fprintf(stderr,
|
||||
"Failed to load dynamic render backend %s, falling back to "
|
||||
"OpenGL\n",
|
||||
backend_to_string(requested_backend_).c_str());
|
||||
delete dynamic_renderer;
|
||||
backend_ = k_open_gl;
|
||||
context_ = new OpenGLRenderer();
|
||||
} else {
|
||||
context_ = dynamic_renderer;
|
||||
// DynamicRenderer may internally fall back (e.g. Vulkan -> OpenGL).
|
||||
// Synchronize RenderManager's view of the actual runtime backend.
|
||||
Backend actual_backend =
|
||||
backend_from_string(dynamic_renderer->backend_name());
|
||||
if (actual_backend != backend_) {
|
||||
fprintf(stderr,
|
||||
"Dynamic render backend fell back from %s to %s\n",
|
||||
backend_to_string(backend_).c_str(),
|
||||
backend_to_string(actual_backend).c_str());
|
||||
backend_ = actual_backend;
|
||||
}
|
||||
}
|
||||
#else
|
||||
context_ = new OpenGLRenderer();
|
||||
#endif
|
||||
decoder_cache_ = new DecoderCache();
|
||||
shader_cache_ = new ShaderCache();
|
||||
} else {
|
||||
fprintf(stderr, "Tried to initialize unknown graphics backend\n");
|
||||
context_ = nullptr;
|
||||
decoder_cache_ = nullptr;
|
||||
}
|
||||
|
||||
if (context_) {
|
||||
dry_run_thread_ = create_thread();
|
||||
audio_thread_ = create_thread();
|
||||
|
||||
waveform_threads_.resize(std::thread::hardware_concurrency());
|
||||
for (size_t i = 0; i < waveform_threads_.size(); i++) {
|
||||
waveform_threads_[i] = create_thread();
|
||||
}
|
||||
|
||||
auto_cacher_ = new PreviewAutoCacher();
|
||||
|
||||
worker_pool_ = new RenderWorkerPool(decoder_cache_,
|
||||
backend_to_string(requested_backend_));
|
||||
worker_pool_->start();
|
||||
backend_ = k_multi_process;
|
||||
}
|
||||
|
||||
decoder_clear_interval_ms_ = k_decoder_maximum_inactivity;
|
||||
decoder_clear_thread_ = std::thread([this]() { decoder_clear_loop(); });
|
||||
}
|
||||
|
||||
RenderManager::~RenderManager()
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> locker(decoder_clear_mutex_);
|
||||
decoder_clear_stopping_ = true;
|
||||
}
|
||||
decoder_clear_cv_.notify_all();
|
||||
if (decoder_clear_thread_.joinable()) {
|
||||
decoder_clear_thread_.join();
|
||||
}
|
||||
|
||||
if (context_) {
|
||||
if (worker_pool_) {
|
||||
worker_pool_->shutdown();
|
||||
delete worker_pool_;
|
||||
worker_pool_ = nullptr;
|
||||
}
|
||||
|
||||
delete shader_cache_;
|
||||
delete decoder_cache_;
|
||||
|
||||
for (RenderThread *rt : render_threads_) {
|
||||
rt->quit();
|
||||
rt->wait();
|
||||
delete rt;
|
||||
}
|
||||
|
||||
context_->post_destroy();
|
||||
delete context_;
|
||||
}
|
||||
}
|
||||
|
||||
void RenderManager::decoder_clear_loop()
|
||||
{
|
||||
std::unique_lock<std::mutex> locker(decoder_clear_mutex_);
|
||||
while (!decoder_clear_stopping_) {
|
||||
decoder_clear_cv_.wait_for(
|
||||
locker, std::chrono::milliseconds(decoder_clear_interval_ms_.load()));
|
||||
if (decoder_clear_stopping_) {
|
||||
break;
|
||||
}
|
||||
locker.unlock();
|
||||
clear_old_decoders();
|
||||
locker.lock();
|
||||
}
|
||||
}
|
||||
|
||||
RenderThread *RenderManager::create_thread(Renderer *renderer)
|
||||
{
|
||||
auto t = new RenderThread(renderer, decoder_cache_, shader_cache_);
|
||||
render_threads_.push_back(t);
|
||||
t->start();
|
||||
return t;
|
||||
}
|
||||
|
||||
RenderTicketPtr RenderManager::render_frame(const RenderVideoParams ¶ms)
|
||||
{
|
||||
// Create ticket
|
||||
RenderTicketPtr ticket = std::make_shared<RenderTicket>();
|
||||
|
||||
ticket->set_property("node", Variant::from_value(params.node));
|
||||
ticket->set_property("time", Variant::from_value(params.time));
|
||||
ticket->set_property("size", Variant::from_value(params.force_size));
|
||||
ticket->set_property("matrix", Variant::from_value(params.force_matrix));
|
||||
ticket->set_property("format", int64_t(params.force_format));
|
||||
ticket->set_property("usecache", params.use_cache);
|
||||
ticket->set_property("channelcount", int64_t(params.force_channel_count));
|
||||
ticket->set_property("mode", int64_t(params.mode));
|
||||
ticket->set_property("type", int64_t(k_type_video));
|
||||
ticket->set_property("colormanager",
|
||||
Variant::from_value(params.color_manager));
|
||||
ticket->set_property("coloroutput",
|
||||
Variant::from_value(params.force_color_output));
|
||||
ticket->set_property("colortransform",
|
||||
Variant::from_value(params.force_color_transform));
|
||||
assert(params.video_params.is_valid());
|
||||
ticket->set_property("vparam", Variant::from_value(params.video_params));
|
||||
ticket->set_property("aparam", Variant::from_value(params.audio_params));
|
||||
ticket->set_property("return", int64_t(params.return_type));
|
||||
ticket->set_property("cache", params.cache_dir);
|
||||
ticket->set_property("cachetimebase",
|
||||
Variant::from_value(params.cache_timebase));
|
||||
ticket->set_property("cacheid", Variant::from_value(params.cache_id));
|
||||
ticket->set_property("multicam", Variant::from_value(params.multicam));
|
||||
|
||||
// Video frames are always rendered by the worker pool. GPU textures cannot
|
||||
// be shared across the process boundary (or across independent Vulkan
|
||||
// instances), so texture-return requests are downgraded to CPU frames.
|
||||
RenderVideoParams worker_params = params;
|
||||
if (worker_params.return_type == ReturnType::k_texture) {
|
||||
worker_params.return_type = ReturnType::k_frame;
|
||||
}
|
||||
|
||||
if (worker_params.return_type == ReturnType::k_null) {
|
||||
if (dry_run_thread_) {
|
||||
dry_run_thread_->add_ticket(ticket);
|
||||
} else {
|
||||
// No render threads (e.g. dummy backend), finish without a result
|
||||
ticket->finish();
|
||||
}
|
||||
} else if (worker_pool_ &&
|
||||
worker_pool_->submit_frame(ticket, worker_params)) {
|
||||
return ticket;
|
||||
} else {
|
||||
fprintf(stderr,
|
||||
"RenderManager: worker pool unavailable, finishing ticket "
|
||||
"without result\n");
|
||||
ticket->finish();
|
||||
}
|
||||
|
||||
return ticket;
|
||||
}
|
||||
|
||||
RenderTicketPtr RenderManager::render_audio(const RenderAudioParams ¶ms)
|
||||
{
|
||||
// Create ticket
|
||||
RenderTicketPtr ticket = std::make_shared<RenderTicket>();
|
||||
|
||||
ticket->set_property("node", Variant::from_value(params.node));
|
||||
ticket->set_property("time", Variant::from_value(params.range));
|
||||
ticket->set_property("type", int64_t(k_type_audio));
|
||||
ticket->set_property("enablewaveforms", params.generate_waveforms);
|
||||
ticket->set_property("clamp", params.clamp);
|
||||
ticket->set_property("aparam", Variant::from_value(params.audio_params));
|
||||
ticket->set_property("mode", int64_t(params.mode));
|
||||
|
||||
if (params.generate_waveforms && !waveform_threads_.empty()) {
|
||||
size_t thread_index = last_waveform_thread_ % waveform_threads_.size();
|
||||
RenderThread *thread = waveform_threads_[thread_index];
|
||||
thread->add_ticket(ticket);
|
||||
last_waveform_thread_++;
|
||||
} else if (audio_thread_) {
|
||||
audio_thread_->add_ticket(ticket);
|
||||
} else {
|
||||
// No render threads (e.g. dummy backend), finish without a result
|
||||
ticket->finish();
|
||||
}
|
||||
|
||||
return ticket;
|
||||
}
|
||||
|
||||
bool RenderManager::remove_ticket(RenderTicketPtr ticket)
|
||||
{
|
||||
if (worker_pool_ && worker_pool_->remove_ticket(ticket)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (RenderThread *rt : render_threads_) {
|
||||
if (rt->remove_ticket(ticket)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void RenderManager::set_aggressive_garbage_collection(bool enabled)
|
||||
{
|
||||
aggressive_gc_ += enabled ? 1 : -1;
|
||||
|
||||
// Clamp at zero so unbalanced disable calls can't drive the counter negative
|
||||
if (aggressive_gc_ < 0) {
|
||||
aggressive_gc_ = 0;
|
||||
}
|
||||
|
||||
if (aggressive_gc_ > 0) {
|
||||
decoder_clear_interval_ms_ = k_decoder_maximum_inactivity_aggressive;
|
||||
} else {
|
||||
decoder_clear_interval_ms_ = k_decoder_maximum_inactivity;
|
||||
}
|
||||
}
|
||||
|
||||
void RenderManager::clear_old_decoders()
|
||||
{
|
||||
if (!decoder_cache_) {
|
||||
// No decoder cache exists on backends without a renderer (e.g. dummy)
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> locker(decoder_cache_->mutex());
|
||||
|
||||
int64_t min_age =
|
||||
current_msecs_since_epoch() - k_decoder_maximum_inactivity;
|
||||
|
||||
for (auto it = decoder_cache_->begin(); it != decoder_cache_->end();) {
|
||||
DecoderPair decoder = it->second;
|
||||
|
||||
if (decoder.decoder->get_last_accessed_time() < min_age) {
|
||||
decoder.decoder->close();
|
||||
it = decoder_cache_->erase(it);
|
||||
} else {
|
||||
it++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RenderThread::RenderThread(Renderer *renderer, DecoderCache *decoder_cache,
|
||||
ShaderCache *shader_cache)
|
||||
: cancelled_(false)
|
||||
, context_(renderer)
|
||||
, decoder_cache_(decoder_cache)
|
||||
, shader_cache_(shader_cache)
|
||||
{
|
||||
if (context_) {
|
||||
context_->init();
|
||||
}
|
||||
}
|
||||
|
||||
RenderThread::~RenderThread()
|
||||
{
|
||||
quit();
|
||||
wait();
|
||||
}
|
||||
|
||||
void RenderThread::start()
|
||||
{
|
||||
thread_ = std::thread([this]() { run(); });
|
||||
}
|
||||
|
||||
void RenderThread::add_ticket(RenderTicketPtr ticket)
|
||||
{
|
||||
std::lock_guard<std::mutex> locker(mutex_);
|
||||
queue_.push_back(ticket);
|
||||
wait_.notify_one();
|
||||
}
|
||||
|
||||
bool RenderThread::remove_ticket(RenderTicketPtr ticket)
|
||||
{
|
||||
std::lock_guard<std::mutex> locker(mutex_);
|
||||
|
||||
auto it = std::find(queue_.begin(), queue_.end(), ticket);
|
||||
if (it == queue_.end()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
queue_.erase(it);
|
||||
return true;
|
||||
}
|
||||
|
||||
void RenderThread::quit()
|
||||
{
|
||||
std::lock_guard<std::mutex> locker(mutex_);
|
||||
cancelled_ = true;
|
||||
wait_.notify_one();
|
||||
}
|
||||
|
||||
void RenderThread::wait()
|
||||
{
|
||||
if (thread_.joinable()) {
|
||||
thread_.join();
|
||||
}
|
||||
}
|
||||
|
||||
void RenderThread::run()
|
||||
{
|
||||
if (context_) {
|
||||
context_->post_init();
|
||||
// Replaces moveToThread(this): the renderer now belongs to this thread.
|
||||
context_->set_owner_thread_to_current();
|
||||
}
|
||||
|
||||
std::unique_lock<std::mutex> locker(mutex_);
|
||||
|
||||
while (!cancelled_) {
|
||||
if (queue_.empty()) {
|
||||
wait_.wait(locker);
|
||||
}
|
||||
|
||||
if (cancelled_) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!queue_.empty()) {
|
||||
RenderTicketPtr ticket = queue_.front();
|
||||
queue_.pop_front();
|
||||
|
||||
locker.unlock();
|
||||
|
||||
// Setup the ticket for ::Process
|
||||
ticket->start();
|
||||
|
||||
if (ticket->is_cancelled()) {
|
||||
ticket->finish();
|
||||
} else {
|
||||
RenderProcessor::process(ticket, context_, decoder_cache_,
|
||||
shader_cache_);
|
||||
}
|
||||
|
||||
locker.lock();
|
||||
}
|
||||
}
|
||||
|
||||
if (context_) {
|
||||
context_->destroy();
|
||||
// Replaces moveToThread back to the creating thread.
|
||||
context_->clear_owner_thread();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_RENDERBACKEND_H
|
||||
#define OAK_RENDERBACKEND_H
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <list>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "config/config.h"
|
||||
#include "colorprocessorcache.h"
|
||||
#include "output/viewer/viewer.h"
|
||||
#include "project.h"
|
||||
#include "traverser.h"
|
||||
#include "previewautocacher.h"
|
||||
#include "renderer.h"
|
||||
#include "colortransform.h"
|
||||
#include "renderticket.h"
|
||||
#include "rendercache.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class RenderThread {
|
||||
public:
|
||||
RenderThread(Renderer *renderer, DecoderCache *decoder_cache,
|
||||
ShaderCache *shader_cache);
|
||||
~RenderThread();
|
||||
|
||||
void start();
|
||||
|
||||
void add_ticket(RenderTicketPtr ticket);
|
||||
|
||||
bool remove_ticket(RenderTicketPtr ticket);
|
||||
|
||||
void quit();
|
||||
|
||||
void wait();
|
||||
|
||||
private:
|
||||
void run();
|
||||
|
||||
std::mutex mutex_;
|
||||
|
||||
std::condition_variable wait_;
|
||||
|
||||
std::list<RenderTicketPtr> queue_;
|
||||
|
||||
bool cancelled_;
|
||||
|
||||
std::thread thread_;
|
||||
|
||||
Renderer *context_ = nullptr;
|
||||
|
||||
DecoderCache *decoder_cache_ = nullptr;
|
||||
|
||||
ShaderCache *shader_cache_ = nullptr;
|
||||
};
|
||||
|
||||
class RenderWorkerPool;
|
||||
|
||||
class RenderManager {
|
||||
public:
|
||||
enum Backend {
|
||||
/// Graphics acceleration provided by OpenGL
|
||||
k_open_gl,
|
||||
|
||||
/// Vulkan requested by the user. Falls back to OpenGL until VulkanRenderer is implemented.
|
||||
k_vulkan,
|
||||
|
||||
/// Video frames are rendered by an external oak-render-worker process.
|
||||
k_multi_process,
|
||||
|
||||
/// No graphics rendering - used to test core threading logic
|
||||
k_dummy
|
||||
};
|
||||
|
||||
static void create_instance()
|
||||
{
|
||||
instance_ = new RenderManager();
|
||||
}
|
||||
|
||||
static void destroy_instance()
|
||||
{
|
||||
delete instance_;
|
||||
instance_ = nullptr;
|
||||
}
|
||||
|
||||
static RenderManager *instance()
|
||||
{
|
||||
return instance_;
|
||||
}
|
||||
|
||||
enum ReturnType { k_texture, k_frame, k_null };
|
||||
|
||||
struct RenderVideoParams {
|
||||
RenderVideoParams(Node *n, const VideoParams &vparam,
|
||||
const AudioParams &aparam, const Rational &t,
|
||||
ColorManager *colorman, RenderMode::Mode m)
|
||||
{
|
||||
node = n;
|
||||
video_params = vparam;
|
||||
audio_params = aparam;
|
||||
time = t;
|
||||
color_manager = colorman;
|
||||
use_cache = false;
|
||||
return_type = k_frame;
|
||||
force_format = PixelFormat::invalid;
|
||||
force_color_output = nullptr;
|
||||
force_color_transform = ColorTransform();
|
||||
force_size = FrameSize(0, 0);
|
||||
force_channel_count = 0;
|
||||
mode = m;
|
||||
multicam = nullptr;
|
||||
}
|
||||
|
||||
void add_cache(FrameHashCache *cache)
|
||||
{
|
||||
cache_dir = cache->get_cache_directory();
|
||||
cache_timebase = cache->get_timebase();
|
||||
cache_id = cache->get_uuid();
|
||||
}
|
||||
|
||||
Node *node;
|
||||
VideoParams video_params;
|
||||
AudioParams audio_params;
|
||||
Rational time;
|
||||
ColorManager *color_manager;
|
||||
bool use_cache;
|
||||
ReturnType return_type;
|
||||
RenderMode::Mode mode;
|
||||
MultiCamNode *multicam;
|
||||
|
||||
std::string cache_dir;
|
||||
Rational cache_timebase;
|
||||
std::string cache_id;
|
||||
|
||||
FrameSize force_size;
|
||||
int force_channel_count;
|
||||
Matrix4x4 force_matrix;
|
||||
PixelFormat force_format;
|
||||
ColorProcessorPtr force_color_output;
|
||||
ColorTransform force_color_transform;
|
||||
};
|
||||
|
||||
static const Rational k_dry_run_interval;
|
||||
|
||||
/**
|
||||
* @brief Asynchronously generate a frame at a given time
|
||||
*
|
||||
* The ticket from this function will return a FramePtr - the rendered frame in reference color
|
||||
* space.
|
||||
*
|
||||
* This function is thread-safe.
|
||||
*/
|
||||
RenderTicketPtr render_frame(const RenderVideoParams ¶ms);
|
||||
|
||||
struct RenderAudioParams {
|
||||
RenderAudioParams(Node *n, const TimeRange &time,
|
||||
const AudioParams &aparam, RenderMode::Mode m)
|
||||
{
|
||||
node = n;
|
||||
range = time;
|
||||
audio_params = aparam;
|
||||
generate_waveforms = false;
|
||||
clamp = true;
|
||||
mode = m;
|
||||
}
|
||||
|
||||
Node *node;
|
||||
TimeRange range;
|
||||
AudioParams audio_params;
|
||||
bool generate_waveforms;
|
||||
bool clamp;
|
||||
RenderMode::Mode mode;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Asynchronously generate a chunk of audio
|
||||
*
|
||||
* The ticket from this function will return a SampleBufferPtr - the rendered audio.
|
||||
*
|
||||
* This function is thread-safe.
|
||||
*/
|
||||
RenderTicketPtr render_audio(const RenderAudioParams ¶ms);
|
||||
|
||||
bool remove_ticket(RenderTicketPtr ticket);
|
||||
|
||||
enum TicketType { k_type_video, k_type_audio };
|
||||
|
||||
Backend backend() const
|
||||
{
|
||||
return backend_;
|
||||
}
|
||||
|
||||
Backend requested_backend() const
|
||||
{
|
||||
return requested_backend_;
|
||||
}
|
||||
|
||||
static Backend backend_from_string(const std::string &backend);
|
||||
static std::string backend_to_string(Backend backend);
|
||||
|
||||
PreviewAutoCacher *get_cacher() const
|
||||
{
|
||||
return auto_cacher_;
|
||||
}
|
||||
|
||||
void set_project(Project *p)
|
||||
{
|
||||
auto_cacher_->set_project(p);
|
||||
}
|
||||
|
||||
void set_aggressive_garbage_collection(bool enabled);
|
||||
|
||||
private:
|
||||
RenderManager();
|
||||
|
||||
virtual ~RenderManager();
|
||||
|
||||
RenderThread *create_thread(Renderer *renderer = nullptr);
|
||||
|
||||
void clear_old_decoders();
|
||||
|
||||
void decoder_clear_loop();
|
||||
|
||||
static RenderManager *instance_;
|
||||
|
||||
Renderer *context_ = nullptr;
|
||||
|
||||
Backend backend_;
|
||||
Backend requested_backend_;
|
||||
|
||||
DecoderCache *decoder_cache_ = nullptr;
|
||||
|
||||
ShaderCache *shader_cache_ = nullptr;
|
||||
|
||||
static constexpr auto k_decoder_maximum_inactivity_aggressive = 1000;
|
||||
static constexpr auto k_decoder_maximum_inactivity = 5000;
|
||||
|
||||
int aggressive_gc_ = 0;
|
||||
|
||||
// Periodic decoder GC, replacing the QTimer. The interval is read
|
||||
// atomically by decoder_clear_loop().
|
||||
std::thread decoder_clear_thread_;
|
||||
std::mutex decoder_clear_mutex_;
|
||||
std::condition_variable decoder_clear_cv_;
|
||||
bool decoder_clear_stopping_ = false;
|
||||
std::atomic<int> decoder_clear_interval_ms_{ k_decoder_maximum_inactivity };
|
||||
|
||||
RenderThread *dry_run_thread_ = nullptr;
|
||||
RenderThread *audio_thread_ = nullptr;
|
||||
|
||||
std::vector<RenderThread *> waveform_threads_;
|
||||
size_t last_waveform_thread_ = 0;
|
||||
|
||||
std::list<RenderThread *> render_threads_;
|
||||
|
||||
PreviewAutoCacher *auto_cacher_ = nullptr;
|
||||
|
||||
RenderWorkerPool *worker_pool_ = nullptr;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_RENDERBACKEND_H
|
||||
@@ -0,0 +1,89 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_RENDERMODE_H
|
||||
#define OAK_RENDERMODE_H
|
||||
|
||||
#include "define.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class RenderMode {
|
||||
public:
|
||||
/**
|
||||
* @brief The primary different "modes" the renderer can function in
|
||||
*/
|
||||
enum Mode {
|
||||
/**
|
||||
* This render is for realtime preview ONLY and does not need to be "perfect". Nodes can use lower-accuracy functions
|
||||
* to save performance when possible.
|
||||
*/
|
||||
k_offline,
|
||||
|
||||
/**
|
||||
* This render is some sort of export or master copy and Nodes should take time/bandwidth/system resources to produce
|
||||
* a higher accuracy version.
|
||||
*/
|
||||
k_online
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Integer frame size, replacing QSize at the render ticket/params
|
||||
* boundary
|
||||
*/
|
||||
class FrameSize {
|
||||
public:
|
||||
FrameSize() = default;
|
||||
FrameSize(int w, int h)
|
||||
: width_(w)
|
||||
, height_(h)
|
||||
{
|
||||
}
|
||||
|
||||
bool is_null() const
|
||||
{
|
||||
return width_ == 0 && height_ == 0;
|
||||
}
|
||||
|
||||
int width() const
|
||||
{
|
||||
return width_;
|
||||
}
|
||||
int height() const
|
||||
{
|
||||
return height_;
|
||||
}
|
||||
|
||||
bool operator==(const FrameSize &rhs) const
|
||||
{
|
||||
return width_ == rhs.width_ && height_ == rhs.height_;
|
||||
}
|
||||
|
||||
private:
|
||||
int width_ = 0;
|
||||
int height_ = 0;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_RENDERMODE_H
|
||||
@@ -0,0 +1,920 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "renderprocessor.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
|
||||
#include "block/clip/clip.h"
|
||||
#include "block/transition/transition.h"
|
||||
#include "project.h"
|
||||
#include "rendermanager.h"
|
||||
#include "framehashcache.h"
|
||||
#include "plugin/pluginrenderer.h"
|
||||
#include "plugins/plugin.h"
|
||||
#include "ipc/frameslotpool.h"
|
||||
#include "texturehandle.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
#define super NodeTraverser
|
||||
|
||||
static int64_t file_last_modified_ms(const std::string &path)
|
||||
{
|
||||
std::error_code ec;
|
||||
const auto t = std::filesystem::last_write_time(path, ec);
|
||||
if (ec) {
|
||||
return 0;
|
||||
}
|
||||
// file clock -> system_clock conversion (QFileInfo::lastModified equivalent)
|
||||
const auto sys = std::chrono::time_point_cast<std::chrono::milliseconds>(
|
||||
t - decltype(t)::clock::now() + std::chrono::system_clock::now());
|
||||
return sys.time_since_epoch().count();
|
||||
}
|
||||
|
||||
RenderProcessor::RenderProcessor(RenderTicketPtr ticket, Renderer *render_ctx,
|
||||
DecoderCache *decoder_cache,
|
||||
ShaderCache *shader_cache)
|
||||
: ticket_(ticket)
|
||||
, render_ctx_(render_ctx)
|
||||
, decoder_cache_(decoder_cache)
|
||||
, shader_cache_(shader_cache)
|
||||
{
|
||||
}
|
||||
|
||||
TexturePtr RenderProcessor::generate_texture(const Rational &time,
|
||||
const Rational &frame_length)
|
||||
{
|
||||
TimeRange range = TimeRange(time, time + frame_length);
|
||||
|
||||
NodeValueTable table;
|
||||
if (Node *node = ticket_->property("node").value<Node *>()) {
|
||||
table = generate_table(node, range);
|
||||
}
|
||||
|
||||
NodeValue tex_val = table.get(NodeValue::k_texture);
|
||||
|
||||
resolve_jobs(tex_val);
|
||||
|
||||
return tex_val.to_texture();
|
||||
}
|
||||
|
||||
FramePtr RenderProcessor::generate_frame(TexturePtr texture,
|
||||
const Rational &time)
|
||||
{
|
||||
// Set up output frame parameters
|
||||
VideoParams frame_params = get_cache_video_params();
|
||||
|
||||
FrameSize frame_size = ticket_->property("size").value<FrameSize>();
|
||||
if (!frame_size.is_null()) {
|
||||
frame_params.set_width(frame_size.width());
|
||||
frame_params.set_height(frame_size.height());
|
||||
}
|
||||
|
||||
PixelFormat frame_format =
|
||||
static_cast<PixelFormat::Format>(ticket_->property("format").to_int());
|
||||
if (frame_format != PixelFormat::invalid) {
|
||||
frame_params.set_format(frame_format);
|
||||
}
|
||||
|
||||
int force_channel_count = ticket_->property("channelcount").to_int();
|
||||
if (force_channel_count != 0) {
|
||||
frame_params.set_channel_count(force_channel_count);
|
||||
} else {
|
||||
frame_params.set_channel_count(texture ?
|
||||
texture->channel_count() :
|
||||
VideoParams::k_rgba_channel_count);
|
||||
}
|
||||
|
||||
FramePtr frame = Frame::create();
|
||||
frame->set_timestamp(time);
|
||||
frame->set_video_params(frame_params);
|
||||
frame->allocate();
|
||||
|
||||
if (!texture) {
|
||||
// Blank frame out
|
||||
memset(frame->data(), 0, frame->allocated_size());
|
||||
} else {
|
||||
// Dump texture contents to frame
|
||||
ColorProcessorPtr output_color_transform =
|
||||
ticket_->property("coloroutput").value<ColorProcessorPtr>();
|
||||
const VideoParams &tex_params = texture->params();
|
||||
|
||||
if (output_color_transform) {
|
||||
TexturePtr transform_tex = render_ctx_->create_texture(tex_params);
|
||||
ColorTransformJob job;
|
||||
|
||||
job.set_color_processor(output_color_transform);
|
||||
job.set_input_texture(texture);
|
||||
job.set_input_alpha_association(
|
||||
OAK_CONFIG("ReassocLinToNonLin").toBool() ? k_alpha_associated :
|
||||
k_alpha_none);
|
||||
|
||||
render_ctx_->blit_color_managed(job, transform_tex.get());
|
||||
|
||||
texture = transform_tex;
|
||||
}
|
||||
|
||||
if (tex_params.effective_width() != frame_params.effective_width() ||
|
||||
tex_params.effective_height() != frame_params.effective_height() ||
|
||||
tex_params.format() != frame_params.format()) {
|
||||
TexturePtr blit_tex = render_ctx_->create_texture(frame_params);
|
||||
|
||||
Matrix4x4 matrix = ticket_->property("matrix").value<Matrix4x4>();
|
||||
|
||||
// No color transform, just blit
|
||||
ShaderJob job;
|
||||
job.insert("ove_maintex",
|
||||
NodeValue(NodeValue::k_texture,
|
||||
Variant::from_value(texture)));
|
||||
job.insert("ove_mvpmat",
|
||||
NodeValue(NodeValue::k_matrix, matrix));
|
||||
|
||||
render_ctx_->blit_to_texture(render_ctx_->get_default_shader(), job,
|
||||
blit_tex.get());
|
||||
|
||||
// Replace texture that we're going to download in the next step
|
||||
texture = blit_tex;
|
||||
}
|
||||
|
||||
render_ctx_->download_from_texture(texture->id(), texture->params(),
|
||||
frame->data(),
|
||||
frame->linesize_pixels());
|
||||
if (output_color_transform) {
|
||||
VideoParams display_params = frame->video_params();
|
||||
display_params.set_colorspace(std::string("display:") +
|
||||
output_color_transform->id());
|
||||
frame->set_video_params(display_params);
|
||||
}
|
||||
}
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
void RenderProcessor::run()
|
||||
{
|
||||
// Depending on the render ticket type, start a job
|
||||
RenderManager::TicketType type =
|
||||
RenderManager::TicketType(ticket_->property("type").to_int());
|
||||
|
||||
set_cancel_pointer(ticket_->get_cancel_atom());
|
||||
|
||||
VideoParams params = ticket_->property("vparam").value<VideoParams>();
|
||||
params.set_format(PixelFormat::f32);
|
||||
set_cache_video_params(params);
|
||||
set_cache_audio_params(ticket_->property("aparam").value<AudioParams>());
|
||||
|
||||
if (is_cancelled()) {
|
||||
ticket_->finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// if is a plugin
|
||||
/*Node *node=ticket_->property("node").value<Node*>();
|
||||
if (node && node->getPlugin()) {
|
||||
std::shared_ptr<OFX::Host::ImageEffect::ImageEffectPlugin> plugin
|
||||
= node->getPlugin();
|
||||
std::unique_ptr<OFX::Host::ImageEffect::Instance> instance(plugin->createInstance(kOfxImageEffectContextFilter, NULL));
|
||||
|
||||
|
||||
}
|
||||
*/
|
||||
|
||||
switch (type) {
|
||||
case RenderManager::k_type_video: {
|
||||
Rational time = ticket_->property("time").value<Rational>();
|
||||
|
||||
Rational frame_length = get_cache_video_params().frame_rate_as_time_base();
|
||||
if (get_cache_video_params().interlacing() !=
|
||||
VideoParams::k_interlace_none) {
|
||||
frame_length /= 2;
|
||||
}
|
||||
|
||||
TexturePtr texture = generate_texture(time, frame_length);
|
||||
|
||||
if (!render_ctx_) {
|
||||
ticket_->finish();
|
||||
} else {
|
||||
if (get_cache_video_params().interlacing() !=
|
||||
VideoParams::k_interlace_none) {
|
||||
// Get next between frame and interlace it
|
||||
TexturePtr top = texture;
|
||||
TexturePtr bottom =
|
||||
generate_texture(time + frame_length, frame_length);
|
||||
|
||||
if (get_cache_video_params().interlacing() ==
|
||||
VideoParams::k_interlaced_bottom_first) {
|
||||
std::swap(top, bottom);
|
||||
}
|
||||
|
||||
texture = render_ctx_->interlace_texture(top, bottom,
|
||||
get_cache_video_params());
|
||||
}
|
||||
|
||||
if (heard_cancel()) {
|
||||
// Finish cancelled ticket with nothing since we can't guarantee the frame we generated
|
||||
// is actually "complete
|
||||
ticket_->finish();
|
||||
} else {
|
||||
FramePtr frame;
|
||||
std::string cache = ticket_->property("cache").to_string();
|
||||
RenderManager::ReturnType return_type =
|
||||
RenderManager::ReturnType(
|
||||
ticket_->property("return").to_int());
|
||||
|
||||
if (return_type == RenderManager::k_frame || !cache.empty()) {
|
||||
// Convert to CPU frame
|
||||
frame = generate_frame(texture, time);
|
||||
|
||||
// Save to cache if requested
|
||||
if (!cache.empty()) {
|
||||
Rational timebase =
|
||||
ticket_->property("cachetimebase").value<Rational>();
|
||||
std::string uuid =
|
||||
ticket_->property("cacheid").value<std::string>();
|
||||
bool cache_result = FrameHashCache::save_cache_frame(
|
||||
cache, uuid, time, timebase, frame);
|
||||
ticket_->set_property("cached", cache_result);
|
||||
}
|
||||
}
|
||||
|
||||
if (return_type == RenderManager::k_texture) {
|
||||
// Return GPU texture
|
||||
if (!texture) {
|
||||
texture =
|
||||
render_ctx_->create_texture(get_cache_video_params());
|
||||
render_ctx_->clear_destination(texture.get());
|
||||
}
|
||||
|
||||
render_ctx_->flush();
|
||||
ticket_->finish(Variant::from_value(texture));
|
||||
} else {
|
||||
ticket_->finish(Variant::from_value(frame));
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case RenderManager::k_type_audio: {
|
||||
TimeRange time = ticket_->property("time").value<TimeRange>();
|
||||
|
||||
NodeValueTable table;
|
||||
if (Node *node = ticket_->property("node").value<Node *>()) {
|
||||
table = generate_table(node, time);
|
||||
}
|
||||
|
||||
NodeValue sample_val = table.get(NodeValue::k_samples);
|
||||
|
||||
resolve_jobs(sample_val);
|
||||
|
||||
SampleBuffer samples = sample_val.to_samples();
|
||||
if (samples.is_allocated()) {
|
||||
if (ticket_->property("clamp").to_bool() && !is_cancelled()) {
|
||||
samples.clamp();
|
||||
}
|
||||
|
||||
if (ticket_->property("enablewaveforms").to_bool() &&
|
||||
!is_cancelled()) {
|
||||
AudioVisualWaveform vis;
|
||||
vis.set_channel_count(samples.audio_params().channel_count());
|
||||
vis.overwrite_samples(samples,
|
||||
samples.audio_params().sample_rate());
|
||||
ticket_->set_property("waveform", Variant::from_value(vis));
|
||||
}
|
||||
}
|
||||
|
||||
if (heard_cancel()) {
|
||||
ticket_->finish();
|
||||
} else {
|
||||
ticket_->finish(Variant::from_value(samples));
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// Fail
|
||||
ticket_->finish();
|
||||
}
|
||||
}
|
||||
|
||||
DecoderPtr
|
||||
RenderProcessor::resolve_decoder_from_input(const std::string &decoder_id,
|
||||
const Decoder::CodecStream &stream)
|
||||
{
|
||||
if (!stream.is_valid()) {
|
||||
fprintf(stderr, "Attempted to resolve the decoder of a null stream\n");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (!decoder_cache_) {
|
||||
fprintf(stderr, "Cannot resolve decoder for %s without a decoder cache\n",
|
||||
stream.filename().c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::unique_lock<std::mutex> locker(decoder_cache_->mutex());
|
||||
|
||||
// std::map-based cache: a missing stream yields a default DecoderPair
|
||||
auto cache_it = decoder_cache_->find(stream);
|
||||
DecoderPair decoder =
|
||||
cache_it == decoder_cache_->end() ? DecoderPair() : cache_it->second;
|
||||
|
||||
int64_t file_last_modified = file_last_modified_ms(stream.filename());
|
||||
|
||||
DecoderPtr dec = nullptr;
|
||||
|
||||
if (decoder.decoder && decoder.last_modified == file_last_modified) {
|
||||
dec = decoder.decoder;
|
||||
} else {
|
||||
// No decoder
|
||||
decoder.decoder = dec = Decoder::create_from_id(decoder_id);
|
||||
decoder.last_modified = file_last_modified;
|
||||
decoder_cache_->insert_or_assign(stream, decoder);
|
||||
locker.unlock();
|
||||
|
||||
if (!dec->open(stream)) {
|
||||
fprintf(stderr, "Failed to open decoder for %s::%d\n",
|
||||
stream.filename().c_str(), stream.stream());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (!render_ctx_) {
|
||||
// Assume dry run and increment access time
|
||||
decoder.decoder->increment_access_time(
|
||||
RenderManager::k_dry_run_interval.to_double() * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
return dec;
|
||||
}
|
||||
|
||||
NodeValueDatabase RenderProcessor::generate_database(const Node *node,
|
||||
const TimeRange &range)
|
||||
{
|
||||
NodeValueDatabase db = super::generate_database(node, range);
|
||||
|
||||
if (const MultiCamNode *multicam =
|
||||
dynamic_cast<const MultiCamNode *>(node)) {
|
||||
if (ticket_->property("multicam").value<MultiCamNode *>() == multicam) {
|
||||
int sz = multicam->get_source_count();
|
||||
std::vector<void *> multicam_tex(sz);
|
||||
for (int i = 0; i < sz; i++) {
|
||||
NodeValueTable t =
|
||||
generate_table(multicam->get_connected_render_output(
|
||||
multicam->k_sources_input, i),
|
||||
range, multicam);
|
||||
NodeValue val = generate_row_value_element(
|
||||
multicam, multicam->k_sources_input, i, &t, range);
|
||||
resolve_jobs(val);
|
||||
|
||||
TexturePtr tp = val.to_texture();
|
||||
// Store as opaque retained handle for the C ABI app layer
|
||||
multicam_tex[i] = oakrender_internal_wrap_texture(tp);
|
||||
}
|
||||
ticket_->set_property("multicam_output",
|
||||
Variant::from_value(multicam_tex));
|
||||
}
|
||||
}
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
void RenderProcessor::process(RenderTicketPtr ticket, Renderer *render_ctx,
|
||||
DecoderCache *decoder_cache,
|
||||
ShaderCache *shader_cache)
|
||||
{
|
||||
RenderProcessor p(ticket, render_ctx, decoder_cache, shader_cache);
|
||||
p.run();
|
||||
}
|
||||
|
||||
void RenderProcessor::process_video_footage(TexturePtr destination,
|
||||
const FootageJob *stream,
|
||||
const Rational &input_time)
|
||||
{
|
||||
if (RenderManager::TicketType(ticket_->property("type").to_int()) !=
|
||||
RenderManager::k_type_video) {
|
||||
// Video cannot contribute to audio, so we do nothing here
|
||||
return;
|
||||
}
|
||||
|
||||
// Check the still frame cache. On large frames such as high resolution still images, uploading
|
||||
// and color managing them for every frame is a waste of time, so we implement a small cache here
|
||||
// to optimize such a situation
|
||||
VideoParams stream_data = stream->video_params();
|
||||
|
||||
ColorManager *color_manager =
|
||||
ticket_->property("colormanager").value<ColorManager *>();
|
||||
|
||||
std::string using_colorspace = stream_data.colorspace();
|
||||
|
||||
if (using_colorspace.empty() && color_manager) {
|
||||
using_colorspace = color_manager->get_default_input_color_space();
|
||||
}
|
||||
|
||||
if (using_colorspace.empty()) {
|
||||
fprintf(stderr,
|
||||
"RenderProcessor ProcessVideoFootage: no input colorspace "
|
||||
"available\n");
|
||||
}
|
||||
|
||||
auto blit_color_managed = [&](const TexturePtr &unmanaged_texture,
|
||||
const VideoParams &texture_params) {
|
||||
if (!render_ctx_ || !unmanaged_texture || is_cancelled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We convert to our rendering pixel format, since that will always be float-based which
|
||||
// is necessary for correct color conversion
|
||||
ColorProcessorPtr processor =
|
||||
ColorProcessor::create(color_manager, using_colorspace,
|
||||
color_manager->get_reference_color_space());
|
||||
|
||||
ColorTransformJob job;
|
||||
job.set_color_processor(processor);
|
||||
job.set_input_texture(unmanaged_texture);
|
||||
|
||||
if (texture_params.channel_count() != VideoParams::k_rgba_channel_count ||
|
||||
texture_params.colorspace() ==
|
||||
color_manager->get_reference_color_space()) {
|
||||
job.set_input_alpha_association(k_alpha_none);
|
||||
} else if (texture_params.premultiplied_alpha()) {
|
||||
job.set_input_alpha_association(k_alpha_associated);
|
||||
} else {
|
||||
job.set_input_alpha_association(k_alpha_unassociated);
|
||||
}
|
||||
|
||||
render_ctx_->blit_color_managed(job, destination.get());
|
||||
// macOS TBDR: ensure tile writeback completes before the texture
|
||||
// is read back in a potentially different shared OpenGL context.
|
||||
render_ctx_->flush();
|
||||
};
|
||||
|
||||
auto *input_pool =
|
||||
ticket_->property("ipc_input_pool").value<ipc::FrameSlotPool *>();
|
||||
int input_slot = -1;
|
||||
const std::vector<int> input_slots =
|
||||
ticket_->property("ipc_input_slots").value<std::vector<int>>();
|
||||
if (!input_slots.empty()) {
|
||||
const Variant cursor_value = ticket_->property("ipc_input_slot_cursor");
|
||||
const int cursor = !cursor_value.is_null() ? cursor_value.to_int() : 0;
|
||||
if (cursor >= 0 && cursor < int(input_slots.size())) {
|
||||
input_slot = input_slots[size_t(cursor)];
|
||||
ticket_->set_property("ipc_input_slot_cursor", int64_t(cursor + 1));
|
||||
}
|
||||
} else {
|
||||
const Variant input_slot_value = ticket_->property("ipc_input_slot");
|
||||
input_slot = !input_slot_value.is_null() ? input_slot_value.to_int() : -1;
|
||||
}
|
||||
if (render_ctx_ && input_pool && input_slot >= 0) {
|
||||
if (input_slot >= int(input_pool->slot_count())) {
|
||||
fprintf(stderr,
|
||||
"RenderProcessor received out-of-range IPC input frame slot "
|
||||
"%d\n",
|
||||
input_slot);
|
||||
return;
|
||||
}
|
||||
|
||||
const ipc::FrameSlotMeta *meta = input_pool->meta(uint32_t(input_slot));
|
||||
if (meta && meta->width > 0 && meta->height > 0 &&
|
||||
meta->data_size > 0 &&
|
||||
meta->data_size <= int(input_pool->slot_data_bytes())) {
|
||||
VideoParams input_params = stream_data;
|
||||
input_params.set_width(meta->width);
|
||||
input_params.set_height(meta->height);
|
||||
input_params.set_format(PixelFormat::Format(meta->format));
|
||||
input_params.set_channel_count(meta->channel_count);
|
||||
// The decoder may leave depth at 0 for 2D frames, but the renderer
|
||||
// needs depth >= 1 to compute image size and upload the texture.
|
||||
if (input_params.depth() <= 0) {
|
||||
input_params.set_depth(1);
|
||||
}
|
||||
|
||||
// Prefer the colorspace that the main process used when decoding this
|
||||
// frame. The FootageJob reconstructed in the worker may have stale or
|
||||
// empty colorspace if the project snapshot was saved before stream
|
||||
// metadata was fully resolved.
|
||||
const std::string ipc_colorspace(meta->colorspace);
|
||||
if (!ipc_colorspace.empty()) {
|
||||
input_params.set_colorspace(ipc_colorspace);
|
||||
using_colorspace = ipc_colorspace;
|
||||
}
|
||||
|
||||
const int bytes_per_pixel = input_params.get_bytes_per_pixel();
|
||||
const int linesize_pixels = bytes_per_pixel > 0 ?
|
||||
meta->linesize / bytes_per_pixel :
|
||||
input_params.effective_width();
|
||||
|
||||
const void *slot_data = input_pool->slot_data(uint32_t(input_slot));
|
||||
TexturePtr unmanaged_texture = render_ctx_->create_texture(
|
||||
input_params, slot_data, linesize_pixels);
|
||||
|
||||
blit_color_managed(unmanaged_texture, input_params);
|
||||
return;
|
||||
}
|
||||
fprintf(stderr,
|
||||
"RenderProcessor received invalid IPC input frame slot %d\n",
|
||||
input_slot);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!decoder_cache_) {
|
||||
fprintf(stderr,
|
||||
"RenderProcessor has no decoder cache or IPC input frame for %s\n",
|
||||
stream->filename().c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
const bool use_proxy = stream->should_use_proxy(
|
||||
static_cast<RenderMode::Mode>(ticket_->property("mode").to_int()));
|
||||
const std::string decode_filename = use_proxy ? stream->proxy_filename() :
|
||||
stream->filename();
|
||||
const std::string decoder_id = use_proxy ? stream->proxy_decoder() :
|
||||
stream->decoder();
|
||||
const int stream_index = use_proxy ? stream->proxy_stream_index() :
|
||||
stream_data.stream_index();
|
||||
|
||||
Decoder::CodecStream default_codec_stream(decode_filename, stream_index,
|
||||
get_current_block());
|
||||
|
||||
DecoderPtr decoder = nullptr;
|
||||
|
||||
switch (stream_data.video_type()) {
|
||||
case VideoParams::k_video_type_video:
|
||||
case VideoParams::k_video_type_still:
|
||||
decoder = resolve_decoder_from_input(decoder_id, default_codec_stream);
|
||||
break;
|
||||
case VideoParams::k_video_type_image_sequence: {
|
||||
if (render_ctx_) {
|
||||
// Since image sequences involve multiple files, we don't engage the decoder cache
|
||||
decoder = Decoder::create_from_id(decoder_id);
|
||||
|
||||
std::string frame_filename;
|
||||
|
||||
int64_t frame_number =
|
||||
stream_data.get_time_in_timebase_units(input_time);
|
||||
frame_filename = Decoder::transform_image_sequence_file_name(
|
||||
decode_filename, frame_number);
|
||||
|
||||
// Decoder will close automatically since it's a stream_ptr
|
||||
decoder->open(Decoder::CodecStream(frame_filename, stream_index,
|
||||
get_current_block()));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (decoder && render_ctx_) {
|
||||
Decoder::RetrieveVideoParams p;
|
||||
p.divider = stream->video_params().divider();
|
||||
p.maximum_format = destination->format();
|
||||
|
||||
if (!is_cancelled()) {
|
||||
VideoParams tex_params = stream->video_params();
|
||||
|
||||
if (tex_params.is_valid()) {
|
||||
TexturePtr unmanaged_texture;
|
||||
|
||||
p.renderer = render_ctx_;
|
||||
p.time =
|
||||
(stream_data.video_type() == VideoParams::k_video_type_video) ?
|
||||
input_time :
|
||||
Decoder::k_any_timecode;
|
||||
p.cancelled = get_cancel_pointer();
|
||||
p.force_range = stream_data.color_range();
|
||||
p.src_interlacing = stream_data.interlacing();
|
||||
|
||||
unmanaged_texture = decoder->retrieve_video(p);
|
||||
|
||||
if (!is_cancelled() && unmanaged_texture) {
|
||||
blit_color_managed(unmanaged_texture, stream_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RenderProcessor::process_audio_footage(SampleBuffer &destination,
|
||||
const FootageJob *stream,
|
||||
const TimeRange &input_time)
|
||||
{
|
||||
// The worker process has no decoder cache and does not decode audio. Bail
|
||||
// out gracefully rather than letting ResolveDecoderFromInput crash.
|
||||
if (!decoder_cache_) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Mirror the video path: use the proxy (when enabled, ready, and containing
|
||||
// audio) for offline renders only, never for export
|
||||
const bool use_proxy = stream->should_use_proxy(
|
||||
static_cast<RenderMode::Mode>(ticket_->property("mode").to_int()));
|
||||
const std::string decode_filename = use_proxy ? stream->proxy_filename() :
|
||||
stream->filename();
|
||||
const std::string decoder_id = use_proxy ? stream->proxy_decoder() :
|
||||
stream->decoder();
|
||||
const int stream_index = use_proxy ?
|
||||
stream->proxy_stream_index() :
|
||||
stream->audio_params().stream_index();
|
||||
|
||||
DecoderPtr decoder = resolve_decoder_from_input(
|
||||
decoder_id,
|
||||
Decoder::CodecStream(decode_filename, stream_index, nullptr));
|
||||
|
||||
if (decoder) {
|
||||
const AudioParams &audio_params = get_cache_audio_params();
|
||||
|
||||
Decoder::RetrieveAudioStatus status = decoder->retrieve_audio(
|
||||
destination, input_time, audio_params, stream->cache_path(),
|
||||
loop_mode(),
|
||||
static_cast<RenderMode::Mode>(ticket_->property("mode").to_int()));
|
||||
|
||||
if (status == Decoder::k_waiting_for_conform) {
|
||||
ticket_->set_property("incomplete", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RenderProcessor::process_shader(TexturePtr destination, const Node *node,
|
||||
const ShaderJob *job)
|
||||
{
|
||||
if (!render_ctx_) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string full_shader_id = node->id() + ":" + job->get_shader_id();
|
||||
|
||||
std::unique_lock<std::mutex> locker(shader_cache_->mutex());
|
||||
|
||||
Variant shader;
|
||||
{
|
||||
auto it = shader_cache_->find(full_shader_id);
|
||||
if (it != shader_cache_->end()) {
|
||||
shader = it->second;
|
||||
}
|
||||
}
|
||||
|
||||
if (shader.is_null()) {
|
||||
// Since we have shader code, compile it now
|
||||
shader = render_ctx_->create_native_shader(
|
||||
node->get_shader_code(job->get_shader_id()));
|
||||
|
||||
if (shader.is_null()) {
|
||||
// Couldn't find or build the shader required
|
||||
return;
|
||||
}
|
||||
|
||||
shader_cache_->insert_or_assign(full_shader_id, shader);
|
||||
}
|
||||
|
||||
locker.unlock();
|
||||
|
||||
// Run shader
|
||||
render_ctx_->blit_to_texture(shader, const_cast<ShaderJob &>(*job),
|
||||
destination.get());
|
||||
}
|
||||
|
||||
void RenderProcessor::process_samples(SampleBuffer &destination,
|
||||
const Node *node, const TimeRange &range,
|
||||
const SampleJob &job)
|
||||
{
|
||||
if (!job.samples().is_allocated()) {
|
||||
return;
|
||||
}
|
||||
|
||||
NodeValueRow value_db;
|
||||
|
||||
const AudioParams &audio_params = get_cache_audio_params();
|
||||
|
||||
for (size_t i = 0; i < job.samples().sample_count(); i++) {
|
||||
// Calculate the exact Rational time at this sample
|
||||
double sample_to_second =
|
||||
static_cast<double>(i) /
|
||||
static_cast<double>(audio_params.sample_rate());
|
||||
|
||||
Rational this_sample_time =
|
||||
Rational::from_double(range.in().to_double() + sample_to_second);
|
||||
|
||||
// Update all non-sample and non-footage inputs
|
||||
for (auto j = job.get_values().cbegin(); j != job.get_values().cend();
|
||||
j++) {
|
||||
TimeRange r = TimeRange(this_sample_time, this_sample_time);
|
||||
NodeValueTable value = process_input(node, j->first, r);
|
||||
|
||||
value_db[j->first] = generate_row_value(node, j->first, &value, r);
|
||||
}
|
||||
|
||||
node->process_samples(value_db, job.samples(), destination, i);
|
||||
}
|
||||
}
|
||||
|
||||
void RenderProcessor::process_color_transform(TexturePtr destination,
|
||||
const Node *node,
|
||||
const ColorTransformJob *job)
|
||||
{
|
||||
if (!render_ctx_) {
|
||||
return;
|
||||
}
|
||||
|
||||
render_ctx_->blit_color_managed(*job, destination.get());
|
||||
}
|
||||
|
||||
void RenderProcessor::process_frame_generation(TexturePtr destination,
|
||||
const Node *node,
|
||||
const GenerateJob *job)
|
||||
{
|
||||
if (!render_ctx_) {
|
||||
return;
|
||||
}
|
||||
|
||||
FramePtr frame = Frame::create();
|
||||
|
||||
frame->set_video_params(destination->params());
|
||||
frame->allocate();
|
||||
|
||||
node->generate_frame(frame, *job);
|
||||
|
||||
destination->upload(frame->data(), frame->linesize_pixels());
|
||||
}
|
||||
|
||||
TexturePtr RenderProcessor::process_plugin_job(TexturePtr texture,
|
||||
TexturePtr destination,
|
||||
const Node *node)
|
||||
{
|
||||
(void)node;
|
||||
|
||||
if (!render_ctx_ || !texture || !destination) {
|
||||
return destination;
|
||||
}
|
||||
|
||||
auto *plugin_job = dynamic_cast<plugin::PluginJob *>(texture->job());
|
||||
if (!plugin_job) {
|
||||
return destination;
|
||||
}
|
||||
|
||||
plugin::PluginRenderer plugin_renderer(render_ctx_);
|
||||
if (!plugin_renderer.renderer()) {
|
||||
return destination;
|
||||
}
|
||||
|
||||
NodeValueRow &values = plugin_job->get_values();
|
||||
|
||||
// QHash::value() semantics: a missing key yields a default NodeValue
|
||||
auto find_value = [](const NodeValueRow &row,
|
||||
const std::string &key) -> NodeValue {
|
||||
auto it = row.find(key);
|
||||
return it == row.end() ? NodeValue() : it->second;
|
||||
};
|
||||
|
||||
auto is_usable_texture = [](const TexturePtr &tex) {
|
||||
if (!tex) {
|
||||
return false;
|
||||
}
|
||||
if (!tex->is_dummy() && tex->renderer()) {
|
||||
return true;
|
||||
}
|
||||
AVFramePtr frame = tex->frame();
|
||||
return frame && frame->data(0);
|
||||
};
|
||||
|
||||
TexturePtr src = nullptr;
|
||||
std::string effect_input_id;
|
||||
if (plugin_job->node()) {
|
||||
effect_input_id = plugin_job->node()->get_effect_input_id();
|
||||
}
|
||||
if (!effect_input_id.empty()) {
|
||||
if (TexturePtr effect_tex = find_value(values, effect_input_id).to_texture();
|
||||
is_usable_texture(effect_tex)) {
|
||||
src = effect_tex;
|
||||
}
|
||||
}
|
||||
if (!src) {
|
||||
const std::string source_key(kOfxImageEffectSimpleSourceClipName);
|
||||
if (TexturePtr source_tex = find_value(values, source_key).to_texture();
|
||||
is_usable_texture(source_tex)) {
|
||||
src = source_tex;
|
||||
} else if (TexturePtr effect_tex =
|
||||
find_value(values, plugin::k_texture_input).to_texture();
|
||||
is_usable_texture(effect_tex)) {
|
||||
src = effect_tex;
|
||||
}
|
||||
}
|
||||
if (!src) {
|
||||
for (auto it = values.cbegin(); it != values.cend(); ++it) {
|
||||
if (it->second.type() == NodeValue::k_texture) {
|
||||
if (TexturePtr any_tex = it->second.to_texture();
|
||||
is_usable_texture(any_tex)) {
|
||||
src = any_tex;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
plugin_renderer.render_plugin(src, *plugin_job, destination,
|
||||
destination->params(), true, false);
|
||||
|
||||
return destination;
|
||||
}
|
||||
|
||||
TexturePtr RenderProcessor::process_video_cache_job(const CacheJob *val)
|
||||
{
|
||||
FramePtr frame = FrameHashCache::load_cache_frame(val->get_filename());
|
||||
if (frame) {
|
||||
// Auto-detect and discard black/empty cached frames (macOS TBDR artifact)
|
||||
bool all_black = true;
|
||||
if (frame->data() && frame->allocated_size() > 0) {
|
||||
const uint8_t *pixels =
|
||||
reinterpret_cast<const uint8_t *>(frame->data());
|
||||
size_t alloc_size = static_cast<size_t>(frame->allocated_size());
|
||||
size_t check_bytes = std::min(alloc_size, size_t(4096));
|
||||
for (size_t i = 0; i < check_bytes; ++i) {
|
||||
if (pixels[i] != 0) {
|
||||
all_black = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (all_black) {
|
||||
fprintf(stderr,
|
||||
"[CACHE] Discarding black cached frame: %s time=%f "
|
||||
"size=%lld\n",
|
||||
val->get_filename().c_str(), frame->timestamp().to_double(),
|
||||
(long long)frame->allocated_size());
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(val->get_filename(), ec);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
TexturePtr tex = create_texture(frame->video_params());
|
||||
if (tex) {
|
||||
tex->upload(frame->data(), frame->linesize_pixels());
|
||||
return tex;
|
||||
}
|
||||
} else {
|
||||
StringList s = ticket_->property("badcache").to_string_list();
|
||||
s.push_back(val->get_filename());
|
||||
ticket_->set_property("badcache", Variant::from_value(s));
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
TexturePtr RenderProcessor::create_texture(const VideoParams &p)
|
||||
{
|
||||
if (render_ctx_) {
|
||||
return render_ctx_->create_texture(p);
|
||||
} else {
|
||||
return super::create_texture(p);
|
||||
}
|
||||
}
|
||||
|
||||
void RenderProcessor::convert_to_reference_space(TexturePtr destination,
|
||||
TexturePtr source,
|
||||
const std::string &input_cs)
|
||||
{
|
||||
if (!render_ctx_) {
|
||||
return;
|
||||
}
|
||||
|
||||
ColorManager *color_manager =
|
||||
ticket_->property("colormanager").value<ColorManager *>();
|
||||
ColorProcessorPtr cp = ColorProcessor::create(
|
||||
color_manager, input_cs, color_manager->get_reference_color_space());
|
||||
|
||||
ColorTransformJob ctj;
|
||||
|
||||
ctj.set_color_processor(cp);
|
||||
ctj.set_input_texture(source);
|
||||
ctj.set_input_alpha_association(k_alpha_associated);
|
||||
|
||||
render_ctx_->blit_color_managed(ctj, destination.get());
|
||||
}
|
||||
|
||||
bool RenderProcessor::use_cache() const
|
||||
{
|
||||
return static_cast<RenderMode::Mode>(ticket_->property("mode").to_int()) ==
|
||||
RenderMode::k_offline;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_RENDERPROCESSOR_H
|
||||
#define OAK_RENDERPROCESSOR_H
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "audio/audiovisualwaveform.h"
|
||||
#include "block/clip/clip.h"
|
||||
#include "traverser.h"
|
||||
#include "renderer.h"
|
||||
#include "rendercache.h"
|
||||
#include "renderticket.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
namespace plugin
|
||||
{
|
||||
class PluginRenderer;
|
||||
}
|
||||
|
||||
class RenderProcessor : public NodeTraverser {
|
||||
public:
|
||||
virtual NodeValueDatabase generate_database(const Node *node,
|
||||
const TimeRange &range) override;
|
||||
|
||||
static void process(RenderTicketPtr ticket, Renderer *render_ctx,
|
||||
DecoderCache *decoder_cache, ShaderCache *shader_cache);
|
||||
|
||||
struct RenderedWaveform {
|
||||
const ClipBlock *block;
|
||||
AudioVisualWaveform waveform;
|
||||
TimeRange range;
|
||||
bool silence;
|
||||
};
|
||||
|
||||
protected:
|
||||
virtual void process_video_footage(TexturePtr destination,
|
||||
const FootageJob *stream,
|
||||
const Rational &input_time) override;
|
||||
|
||||
virtual void process_audio_footage(SampleBuffer &destination,
|
||||
const FootageJob *stream,
|
||||
const TimeRange &input_time) override;
|
||||
|
||||
virtual void process_shader(TexturePtr destination, const Node *node,
|
||||
const ShaderJob *job) override;
|
||||
|
||||
virtual void process_samples(SampleBuffer &destination, const Node *node,
|
||||
const TimeRange &range,
|
||||
const SampleJob &job) override;
|
||||
|
||||
virtual void process_color_transform(TexturePtr destination, const Node *node,
|
||||
const ColorTransformJob *job) override;
|
||||
|
||||
virtual void process_frame_generation(TexturePtr destination,
|
||||
const Node *node,
|
||||
const GenerateJob *job) override;
|
||||
|
||||
virtual TexturePtr process_plugin_job(TexturePtr texture,
|
||||
TexturePtr destination,
|
||||
const Node *node) override;
|
||||
|
||||
virtual TexturePtr process_video_cache_job(const CacheJob *val) override;
|
||||
|
||||
virtual TexturePtr create_texture(const VideoParams &p) override;
|
||||
|
||||
virtual SampleBuffer create_sample_buffer(const AudioParams ¶ms,
|
||||
int sample_count) override
|
||||
{
|
||||
return SampleBuffer(params, sample_count);
|
||||
}
|
||||
|
||||
virtual void convert_to_reference_space(TexturePtr destination,
|
||||
TexturePtr source,
|
||||
const std::string &input_cs) override;
|
||||
|
||||
virtual bool use_cache() const override;
|
||||
|
||||
private:
|
||||
RenderProcessor(RenderTicketPtr ticket, Renderer *render_ctx,
|
||||
DecoderCache *decoder_cache, ShaderCache *shader_cache);
|
||||
|
||||
TexturePtr generate_texture(const Rational &time,
|
||||
const Rational &frame_length);
|
||||
|
||||
FramePtr generate_frame(TexturePtr texture, const Rational &time);
|
||||
|
||||
void run();
|
||||
|
||||
DecoderPtr resolve_decoder_from_input(const std::string &decoder_id,
|
||||
const Decoder::CodecStream &stream);
|
||||
|
||||
RenderTicketPtr ticket_;
|
||||
|
||||
Renderer *render_ctx_;
|
||||
|
||||
std::unique_ptr<olive::plugin::PluginRenderer> plugin_renderer_;
|
||||
|
||||
DecoderCache *decoder_cache_;
|
||||
|
||||
ShaderCache *shader_cache_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_RENDERPROCESSOR_H
|
||||
@@ -0,0 +1,216 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "renderticket.h"
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
RenderTicket::RenderTicket()
|
||||
: is_running_(false)
|
||||
, has_result_(false)
|
||||
, finish_count_(0)
|
||||
{
|
||||
}
|
||||
|
||||
void RenderTicket::start()
|
||||
{
|
||||
std::lock_guard<std::mutex> locker(lock_);
|
||||
|
||||
is_running_ = true;
|
||||
has_result_ = false;
|
||||
result_ = Variant();
|
||||
}
|
||||
|
||||
void RenderTicket::finish()
|
||||
{
|
||||
finish_internal(false, Variant());
|
||||
}
|
||||
|
||||
void RenderTicket::finish(Variant result)
|
||||
{
|
||||
finish_internal(true, result);
|
||||
}
|
||||
|
||||
Variant RenderTicket::get()
|
||||
{
|
||||
wait_for_finished();
|
||||
|
||||
// We don't have to mutex around this because there is no way to write to `result_` after
|
||||
// the ticket has finished and the above function blocks the calling thread until it is finished
|
||||
return result_;
|
||||
}
|
||||
|
||||
void RenderTicket::wait_for_finished()
|
||||
{
|
||||
std::unique_lock<std::mutex> locker(lock_);
|
||||
|
||||
if (is_running_) {
|
||||
wait_.wait(locker);
|
||||
}
|
||||
}
|
||||
|
||||
bool RenderTicket::is_running(bool lock)
|
||||
{
|
||||
if (lock) {
|
||||
lock_.lock();
|
||||
}
|
||||
|
||||
bool running = is_running_;
|
||||
|
||||
if (lock) {
|
||||
lock_.unlock();
|
||||
}
|
||||
|
||||
return running;
|
||||
}
|
||||
|
||||
int RenderTicket::get_finish_count(bool lock)
|
||||
{
|
||||
if (lock) {
|
||||
lock_.lock();
|
||||
}
|
||||
|
||||
int count = finish_count_;
|
||||
|
||||
if (lock) {
|
||||
lock_.unlock();
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
bool RenderTicket::has_result()
|
||||
{
|
||||
std::lock_guard<std::mutex> locker(lock_);
|
||||
|
||||
return has_result_;
|
||||
}
|
||||
|
||||
void RenderTicket::finish_internal(bool has_result, Variant result)
|
||||
{
|
||||
std::unique_lock<std::mutex> locker(lock_);
|
||||
|
||||
if (!is_running_) {
|
||||
fprintf(stderr, "Tried to finish ticket that wasn't running\n");
|
||||
} else {
|
||||
is_running_ = false;
|
||||
has_result_ = has_result;
|
||||
result_ = result;
|
||||
finish_count_++;
|
||||
|
||||
std::function<void()> callback = finished_callback_;
|
||||
|
||||
wait_.notify_all();
|
||||
|
||||
locker.unlock();
|
||||
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RenderTicketWatcher::RenderTicketWatcher()
|
||||
: ticket_(nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
void RenderTicketWatcher::set_ticket(RenderTicketPtr ticket)
|
||||
{
|
||||
if (ticket_) {
|
||||
fprintf(stderr, "Tried to set a ticket on a RenderTicketWatcher twice\n");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ticket) {
|
||||
fprintf(stderr, "Tried to set a null ticket on a RenderTicketWatcher\n");
|
||||
return;
|
||||
}
|
||||
|
||||
ticket_ = ticket;
|
||||
|
||||
ticket->set_finished_callback([this]() { ticket_finished(); });
|
||||
|
||||
// Lock ticket so we can query if it's already finished by the time this code runs
|
||||
std::lock_guard<std::mutex> locker(*ticket->lock());
|
||||
|
||||
if (!ticket_->is_running(false) && ticket_->get_finish_count(false) > 0) {
|
||||
// Ticket has already finished before. The Qt code re-notified
|
||||
// asynchronously through the event loop (Qt::QueuedConnection) so the
|
||||
// caller could receive the watcher pointer first; with no event loop
|
||||
// there is no deferred delivery, so the caller must observe the state
|
||||
// through is_running()/has_result()/get() instead. The facade/app
|
||||
// layer re-creates the deferred notification if it needs one.
|
||||
}
|
||||
}
|
||||
|
||||
bool RenderTicketWatcher::is_running()
|
||||
{
|
||||
if (ticket_) {
|
||||
return ticket_->is_running();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void RenderTicketWatcher::wait_for_finished()
|
||||
{
|
||||
if (ticket_) {
|
||||
ticket_->wait_for_finished();
|
||||
}
|
||||
}
|
||||
|
||||
Variant RenderTicketWatcher::get()
|
||||
{
|
||||
if (ticket_) {
|
||||
return ticket_->get();
|
||||
} else {
|
||||
return Variant();
|
||||
}
|
||||
}
|
||||
|
||||
bool RenderTicketWatcher::has_result()
|
||||
{
|
||||
if (ticket_) {
|
||||
return ticket_->has_result();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void RenderTicketWatcher::cancel()
|
||||
{
|
||||
if (ticket_) {
|
||||
ticket_->cancel();
|
||||
}
|
||||
}
|
||||
|
||||
void RenderTicketWatcher::ticket_finished()
|
||||
{
|
||||
if (finished_callback_) {
|
||||
finished_callback_(this);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_RENDERTICKET_H
|
||||
#define OAK_RENDERTICKET_H
|
||||
|
||||
#include <condition_variable>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
#include "common/cancelableobject.h"
|
||||
#include "variant.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class RenderTicket : public CancelableObject {
|
||||
public:
|
||||
RenderTicket();
|
||||
|
||||
virtual ~RenderTicket() override = default;
|
||||
|
||||
/**
|
||||
* @brief Get the ticket's current state
|
||||
*
|
||||
* This function is thread safe, unless `lock` is set to false. Then the caller has responsibility
|
||||
* of locking the mutex before and unlocking after this function is called.
|
||||
*/
|
||||
bool is_running(bool lock = true);
|
||||
|
||||
/**
|
||||
* @brief Determine how many times ticket has been finished
|
||||
*
|
||||
* This function is thread safe, unless `lock` is set to false. Then the caller has responsibility
|
||||
* of locking the mutex before and unlocking after this function is called.
|
||||
*/
|
||||
int get_finish_count(bool lock = true);
|
||||
|
||||
/**
|
||||
* @brief Check if this ticket has a result
|
||||
*
|
||||
* If this ticket is running, this will always return false.
|
||||
*/
|
||||
bool has_result();
|
||||
|
||||
/**
|
||||
* @brief Get value, if any
|
||||
*/
|
||||
Variant get();
|
||||
|
||||
/**
|
||||
* @brief Wait for ticket to be finished
|
||||
*
|
||||
* If this ticket is not running, this function returns immediately.
|
||||
*/
|
||||
void wait_for_finished();
|
||||
|
||||
/**
|
||||
* @brief Access this ticket's mutex
|
||||
*
|
||||
* Use if you're doing several operations on a ticket and need to ensure thread safety while
|
||||
* doing so. Most of the time this isn't necessary since all functions are thread safe by default.
|
||||
*/
|
||||
std::mutex *lock()
|
||||
{
|
||||
return &lock_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Signal to the ticket that it is running
|
||||
*
|
||||
* If any value is set, it is cleared.
|
||||
*/
|
||||
void start();
|
||||
|
||||
/**
|
||||
* @brief Finish ticket with no value
|
||||
*
|
||||
* Sets ticket to no longer running and assume it has received no result.
|
||||
*/
|
||||
void finish();
|
||||
|
||||
/**
|
||||
* @brief Finish ticket with value
|
||||
*
|
||||
* Sets ticket to no longer running and provide a value generated by the operation requested.
|
||||
*/
|
||||
void finish(Variant result);
|
||||
|
||||
/**
|
||||
* @brief Set the callback invoked when finish has been called by any means
|
||||
* (either cancelled or with a result)
|
||||
*
|
||||
* Replaces the former `finished` signal. The callback runs on the thread
|
||||
* that called finish(), after the ticket lock has been released.
|
||||
*/
|
||||
void set_finished_callback(std::function<void()> cb)
|
||||
{
|
||||
std::lock_guard<std::mutex> locker(lock_);
|
||||
finished_callback_ = std::move(cb);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Dynamic property bag, replacing QObject::setProperty/property
|
||||
*
|
||||
* RenderManager packs render parameters onto the ticket and
|
||||
* RenderProcessor/RenderWorkerPool read them back. A missing name returns
|
||||
* a null Variant (QVariant-invalid semantics).
|
||||
*/
|
||||
void set_property(const std::string &name, const Variant &value)
|
||||
{
|
||||
std::lock_guard<std::mutex> locker(lock_);
|
||||
properties_[name] = value;
|
||||
}
|
||||
|
||||
Variant property(const std::string &name) const
|
||||
{
|
||||
std::lock_guard<std::mutex> locker(lock_);
|
||||
auto it = properties_.find(name);
|
||||
return it == properties_.end() ? Variant() : it->second;
|
||||
}
|
||||
|
||||
private:
|
||||
void finish_internal(bool has_result, Variant result);
|
||||
|
||||
bool is_running_;
|
||||
|
||||
Variant result_;
|
||||
|
||||
bool has_result_;
|
||||
|
||||
int finish_count_;
|
||||
|
||||
// mutable: property() is const but still serializes against set_property()
|
||||
mutable std::mutex lock_;
|
||||
|
||||
std::condition_variable wait_;
|
||||
|
||||
std::function<void()> finished_callback_;
|
||||
|
||||
std::map<std::string, Variant> properties_;
|
||||
};
|
||||
|
||||
using RenderTicketPtr = std::shared_ptr<RenderTicket>;
|
||||
|
||||
class RenderTicketWatcher {
|
||||
public:
|
||||
RenderTicketWatcher();
|
||||
|
||||
RenderTicketPtr get_ticket() const
|
||||
{
|
||||
return ticket_;
|
||||
}
|
||||
|
||||
void set_ticket(RenderTicketPtr ticket);
|
||||
|
||||
bool is_running();
|
||||
|
||||
void wait_for_finished();
|
||||
|
||||
Variant get();
|
||||
|
||||
bool has_result();
|
||||
|
||||
void cancel();
|
||||
|
||||
/**
|
||||
* @brief Set the callback replacing the former finished(watcher) signal
|
||||
*/
|
||||
void set_finished_callback(std::function<void(RenderTicketWatcher *)> cb)
|
||||
{
|
||||
finished_callback_ = std::move(cb);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Dynamic property bag, replacing QObject::setProperty/property
|
||||
*
|
||||
* PreviewAutoCacher packs job/cache/time/node onto the watcher before the
|
||||
* ticket exists, so the bag lives on the watcher itself (as the QObject
|
||||
* original did). A missing name returns a null Variant.
|
||||
*/
|
||||
void set_property(const std::string &name, const Variant &value)
|
||||
{
|
||||
std::lock_guard<std::mutex> locker(property_lock_);
|
||||
properties_[name] = value;
|
||||
}
|
||||
|
||||
Variant property(const std::string &name) const
|
||||
{
|
||||
std::lock_guard<std::mutex> locker(property_lock_);
|
||||
auto it = properties_.find(name);
|
||||
return it == properties_.end() ? Variant() : it->second;
|
||||
}
|
||||
|
||||
private:
|
||||
RenderTicketPtr ticket_;
|
||||
|
||||
std::function<void(RenderTicketWatcher *)> finished_callback_;
|
||||
|
||||
mutable std::mutex property_lock_;
|
||||
std::map<std::string, Variant> properties_;
|
||||
|
||||
void ticket_finished();
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_RENDERTICKET_H
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,165 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak 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 OAK_RENDERWORKERPOOL_H
|
||||
#define OAK_RENDERWORKERPOOL_H
|
||||
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "codec/frame.h"
|
||||
#include "rendermanager.h"
|
||||
#include "workerprocess.h"
|
||||
#include "project/serializer/serializer.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
// The worker IPC shared-memory/frame-slot wrappers live in ipc/ (namespace
|
||||
// olive::ipc). Only forward-declared here; PooledWorker (which holds IPC
|
||||
// objects by value) is defined in the .cpp for the same reason.
|
||||
namespace ipc
|
||||
{
|
||||
class FrameSlotPool;
|
||||
}
|
||||
|
||||
class Project;
|
||||
|
||||
class RenderWorkerPool {
|
||||
public:
|
||||
explicit RenderWorkerPool(DecoderCache *decoder_cache,
|
||||
const std::string &gpu_backend);
|
||||
~RenderWorkerPool();
|
||||
|
||||
void start();
|
||||
|
||||
bool submit_frame(RenderTicketPtr ticket,
|
||||
const RenderManager::RenderVideoParams ¶ms);
|
||||
|
||||
bool remove_ticket(RenderTicketPtr ticket);
|
||||
|
||||
void shutdown();
|
||||
|
||||
private:
|
||||
struct Job {
|
||||
Job(RenderTicketPtr t, const RenderManager::RenderVideoParams &p)
|
||||
: ticket(t)
|
||||
, params(p)
|
||||
{
|
||||
}
|
||||
|
||||
RenderTicketPtr ticket;
|
||||
RenderManager::RenderVideoParams params;
|
||||
std::string graph_path;
|
||||
std::string node_token;
|
||||
std::vector<FramePtr> input_frames;
|
||||
};
|
||||
|
||||
enum class JobResult {
|
||||
k_finished,
|
||||
k_retryable_failure,
|
||||
k_fatal_failure,
|
||||
k_cancelled
|
||||
};
|
||||
|
||||
struct ActiveJob {
|
||||
RenderTicketPtr ticket;
|
||||
int64_t process_id = 0;
|
||||
int64_t ticket_id = 0;
|
||||
};
|
||||
|
||||
// Defined in the .cpp: holds the per-worker IPC shared-memory regions and
|
||||
// frame slot pools by value.
|
||||
struct PooledWorker;
|
||||
|
||||
struct CachedGraph {
|
||||
std::string path;
|
||||
};
|
||||
|
||||
void run();
|
||||
|
||||
bool prepare_job(RenderTicketPtr ticket,
|
||||
const RenderManager::RenderVideoParams ¶ms, Job *job);
|
||||
bool write_graph_snapshot(Project *project, std::string *path);
|
||||
bool is_supported(const RenderManager::RenderVideoParams ¶ms) const;
|
||||
|
||||
void worker_loop(int worker_index,
|
||||
std::vector<std::unique_ptr<PooledWorker>> *local_pool);
|
||||
void process_job(const Job &job, int worker_index,
|
||||
std::vector<std::unique_ptr<PooledWorker>> *local_pool);
|
||||
JobResult process_job_attempt(const Job &job, int worker_index,
|
||||
int attempt_index, PooledWorker *worker);
|
||||
void finish_with_frame(RenderTicketPtr ticket,
|
||||
const ipc::FrameSlotPool &pool, uint32_t slot);
|
||||
void cleanup_graph_file(const std::string &path);
|
||||
void add_graph_path_ref(const std::string &path);
|
||||
void add_graph_path_ref_locked(const std::string &path);
|
||||
void release_graph_path_ref(const std::string &path);
|
||||
void release_graph_path_ref_locked(const std::string &path);
|
||||
void set_graph_path_cached(const std::string &path, bool cached);
|
||||
void set_graph_path_cached_locked(const std::string &path, bool cached);
|
||||
void cancel_active_process(int64_t process_id);
|
||||
void set_active_worker(int worker_index, RenderTicketPtr ticket,
|
||||
WorkerProcess *worker, int64_t ticket_id);
|
||||
void clear_active_worker(int worker_index, int64_t process_id);
|
||||
int worker_count() const;
|
||||
|
||||
std::unique_ptr<PooledWorker>
|
||||
acquire_worker(std::vector<std::unique_ptr<PooledWorker>> *local_pool,
|
||||
const std::string &graph_path);
|
||||
void return_worker(std::vector<std::unique_ptr<PooledWorker>> *local_pool,
|
||||
std::unique_ptr<PooledWorker> worker, bool keep_alive);
|
||||
void shutdown_worker(PooledWorker *worker);
|
||||
void
|
||||
shutdown_local_pool(std::vector<std::unique_ptr<PooledWorker>> *local_pool);
|
||||
void clear_graph_cache();
|
||||
|
||||
DecoderCache *decoder_cache_;
|
||||
std::string gpu_backend_;
|
||||
std::mutex mutex_;
|
||||
std::condition_variable wait_;
|
||||
std::deque<Job> queue_;
|
||||
bool stopping_ = false;
|
||||
std::vector<ActiveJob> active_jobs_;
|
||||
std::map<std::string, CachedGraph> graph_cache_;
|
||||
std::map<std::string, int> graph_path_ref_count_;
|
||||
std::set<std::string> cached_graph_paths_;
|
||||
|
||||
std::thread thread_;
|
||||
|
||||
static constexpr uint32_t k_output_slots = 2;
|
||||
static constexpr int k_max_attempts = 2;
|
||||
static constexpr int k_max_width = 4096;
|
||||
static constexpr int k_max_height = 2160;
|
||||
static constexpr int k_worker_idle_timeout_ms = 30000;
|
||||
static constexpr int k_worker_max_uses = 100;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_RENDERWORKERPOOL_H
|
||||
@@ -0,0 +1,67 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_SHADERCODE_H
|
||||
#define OAK_SHADERCODE_H
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "filefunctions.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class ShaderCode {
|
||||
public:
|
||||
ShaderCode(const std::string &frag_code = std::string(),
|
||||
const std::string &vert_code = std::string())
|
||||
: frag_code_(frag_code)
|
||||
, vert_code_(vert_code)
|
||||
{
|
||||
}
|
||||
|
||||
const std::string &frag_code() const
|
||||
{
|
||||
return frag_code_;
|
||||
}
|
||||
void set_frag_code(const std::string &f)
|
||||
{
|
||||
frag_code_ = f;
|
||||
}
|
||||
|
||||
const std::string &vert_code() const
|
||||
{
|
||||
return vert_code_;
|
||||
}
|
||||
void set_vert_code(const std::string &v)
|
||||
{
|
||||
vert_code_ = v;
|
||||
}
|
||||
|
||||
private:
|
||||
std::string frag_code_;
|
||||
|
||||
std::string vert_code_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_SHADERCODE_H
|
||||
@@ -0,0 +1,59 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "texture.h"
|
||||
|
||||
#include "render/job/acceleratedjob.h"
|
||||
#include "renderer.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
const Texture::Interpolation Texture::k_default_interpolation =
|
||||
Texture::k_mipmapped_linear;
|
||||
|
||||
Texture::~Texture()
|
||||
{
|
||||
if (is_renderer_alive()) {
|
||||
renderer_->destroy_texture(this);
|
||||
}
|
||||
|
||||
if (job_) {
|
||||
delete job_;
|
||||
}
|
||||
}
|
||||
|
||||
void Texture::upload(void *data, int linesize)
|
||||
{
|
||||
if (is_renderer_alive()) {
|
||||
renderer_->upload_to_texture(this->id(), this->params(), data, linesize);
|
||||
}
|
||||
}
|
||||
|
||||
void Texture::download(void *data, int linesize)
|
||||
{
|
||||
if (is_renderer_alive()) {
|
||||
renderer_->download_from_texture(this->id(), this->params(), data,
|
||||
linesize);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user