refactor(codec): de-Qt oakcodec and wrap it in a pure C ABI; switch common handles to refcounted value structs

- oakcodec: de-Qt all 20 sources (QThread decode loop -> std::thread,
  QObject/signals -> callbacks), pure C ABI in include/codec with
  refcounted neutral handles (OakFrame/OakDecoder/OakEncoder),
  framemanager moved in from render, frame_to_buffer/buffer_to_frame
  moved in from oakcommon oiioutils, codec->task via submit callback
  (M8 will register), all cross-module calls go through the other
  side's C API, -fvisibility=hidden + OAKCODEC_API
- oakcommon: handles become refcounted value structs
  {ctx, addref, release, abi_version} (FFmpeg-style), pass-by-value
  signatures, free() as release wrapper; init_from_native/get_native
  for copyable value objects; OakCommonXxx renamed to OakXxx
- oakcommon: add logging (log_debug/info/warning/critical with level
  filtering and sink injection) + printf-style oakcommon_log C wrapper
- oakrender: add CancelAtom C API family; complete
  oakrender_color_processor_convert_frame; fix get_processor() missing
  definition and OCIO env var lookup
- tests: oakcommon 174, oaknode 96, oakrender 42, oakcodec 18, all
  green in their standalone builds
This commit is contained in:
2026-08-06 18:50:07 +08:00
parent edbd3913af
commit 3d004c081b
127 changed files with 13760 additions and 1222 deletions
+1
View File
@@ -1,6 +1,7 @@
target_sources(oakrender PRIVATE
renderer.cpp
cache.cpp
cancelatom.cpp
color.cpp
manager.cpp
)
+135
View File
@@ -0,0 +1,135 @@
/***
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/cancelatom.h"
#include <atomic>
#include <cstdint>
#include "alivecount.h"
#include "cancelatom.h"
namespace
{
/**
* @brief Heap box behind every OakCancelAtom's ctx pointer.
*
* Holds the wrapped CancelAtom plus its atomic reference count. addref
* and release are emitted in this translation unit so the function
* pointers stored in a handle always run code from the DLL that created
* the object.
*/
struct CancelAtomBox {
olive::CancelAtom impl;
std::atomic<uint32_t> refs;
CancelAtomBox()
: refs(1)
{
}
};
CancelAtomBox *box(OakCancelAtom atom)
{
return static_cast<CancelAtomBox *>(atom.ctx);
}
olive::CancelAtom *impl(OakCancelAtom atom)
{
auto *b = box(atom);
return b ? &b->impl : nullptr;
}
/**
* @brief Handle addref thunk: atomically increments the count.
*/
void cancel_atom_addref(void *ctx)
{
auto *b = static_cast<CancelAtomBox *>(ctx);
if (b)
b->refs.fetch_add(1, std::memory_order_relaxed);
}
/**
* @brief Handle release thunk: decrements the count, destroys at zero.
*/
void cancel_atom_release(void *ctx)
{
auto *b = static_cast<CancelAtomBox *>(ctx);
if (b && b->refs.fetch_sub(1, std::memory_order_acq_rel) == 1) {
delete b;
oakrender_c_api::alive_dec();
}
}
} // namespace
OakCancelAtom oakrender_cancelatom_init(void)
{
OakCancelAtom h = {};
try {
h.ctx = new CancelAtomBox();
} catch (...) {
h.ctx = nullptr;
}
h.addref = &cancel_atom_addref;
h.release = &cancel_atom_release;
h.abi_version = OAKRENDER_ABI_VERSION;
if (h.ctx)
oakrender_c_api::alive_inc();
return h;
}
void oakrender_cancelatom_free(OakCancelAtom *atom)
{
if (!atom || !atom->ctx || !atom->release)
return;
atom->release(atom->ctx);
atom->ctx = nullptr;
}
int oakrender_cancelatom_cancel(OakCancelAtom atom)
{
auto *c = impl(atom);
if (!c)
return OAKRENDER_E_INVALID;
c->cancel();
return OAKRENDER_OK;
}
int oakrender_cancelatom_is_cancelled(OakCancelAtom atom, int *cancelled)
{
auto *c = impl(atom);
if (!c || !cancelled)
return OAKRENDER_E_INVALID;
*cancelled = c->is_cancelled() ? 1 : 0;
return OAKRENDER_OK;
}
int oakrender_cancelatom_heard_cancel(OakCancelAtom atom, int *heard)
{
auto *c = impl(atom);
if (!c || !heard)
return OAKRENDER_E_INVALID;
*heard = c->heard_cancel() ? 1 : 0;
return OAKRENDER_OK;
}
+30 -10
View File
@@ -30,6 +30,7 @@
#include "color/colormanager/colormanager.h"
#include "filefunctions.h"
#include <OpenColorIO/OpenColorIO.h>
namespace
{
@@ -57,7 +58,7 @@ OakColorProcessor *oakrender_color_processor_create(const char *src_space,
return nullptr;
}
try {
ocio::ConstConfigRcPtr config = olive::ColorManager::get_default_config();
OCIO_NAMESPACE::ConstConfigRcPtr config = olive::ColorManager::get_default_config();
if (!config) {
return nullptr;
}
@@ -69,17 +70,17 @@ OakColorProcessor *oakrender_color_processor_create(const char *src_space,
src = config->getCanonicalName(src_space);
}
// OCIO failures are non-fatal (matching the C++ behavior): the
// OCIO_NAMESPACE 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;
OCIO_NAMESPACE::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 &) {
} catch (OCIO_NAMESPACE::Exception &) {
processor = nullptr;
}
@@ -169,7 +170,7 @@ int oakrender_color_manager_display_transform(const char *display,
return OAKRENDER_E_INVALID;
}
try {
ocio::ConstConfigRcPtr config = olive::ColorManager::get_default_config();
OCIO_NAMESPACE::ConstConfigRcPtr config = olive::ColorManager::get_default_config();
if (!config) {
return OAKRENDER_E_STATE;
}
@@ -197,25 +198,44 @@ int oakrender_color_manager_display_transform(const char *display,
}
// Source = the config's reference colorspace (role lookup).
ocio::ConstColorSpaceRcPtr ref_cs =
config->getColorSpace(ocio::ROLE_REFERENCE);
OCIO_NAMESPACE::ConstColorSpaceRcPtr ref_cs =
config->getColorSpace(OCIO_NAMESPACE::ROLE_REFERENCE);
if (!ref_cs) {
return OAKRENDER_E_STATE;
}
auto dvt = ocio::DisplayViewTransform::Create();
auto dvt = OCIO_NAMESPACE::DisplayViewTransform::Create();
dvt->setSrc(ref_cs->getName());
dvt->setDisplay(display);
dvt->setView(view);
ocio::ConstProcessorRcPtr processor = config->getProcessor(dvt);
OCIO_NAMESPACE::ConstProcessorRcPtr processor = config->getProcessor(dvt);
if (!processor) {
return OAKRENDER_E_NOT_FOUND;
}
return write_string(processor->getCacheID(), buf, n);
} catch (ocio::Exception &) {
} catch (OCIO_NAMESPACE::Exception &) {
return OAKRENDER_E_NOT_FOUND;
} catch (...) {
return OAKRENDER_E_FAILED;
}
}
int oakrender_color_processor_convert_frame(OakColorProcessor *processor,
OakCodecFrame *frame)
{
if (!processor || !processor->ptr || !frame || !frame->ptr) {
return OAKRENDER_E_INVALID;
}
try {
// In-place: ColorProcessor::convert_frame() applies the CPU
// processor to the frame's pixel buffer through an
// OCIO::PackedImageDesc view. A processor whose underlying OCIO
// processor is null (creation failure was non-fatal) is a
// pass-through and still reports success, mirroring the C++ API.
processor->ptr->convert_frame(frame->ptr);
return OAKRENDER_OK;
} catch (...) {
return OAKRENDER_E_FAILED;
}
}
+2 -2
View File
@@ -30,9 +30,9 @@ 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(oakgl2 SHARED opengl/openglbackend_c.cpp)
add_library(oakvulkan SHARED vulkan/vulkanbackend_c.cpp)
foreach(backend oakgl oakvulkan)
foreach(backend oakgl2 oakvulkan)
target_link_libraries(${backend} PRIVATE oakrender)
endforeach()
+5 -4
View File
@@ -134,6 +134,11 @@ void ColorProcessor::convert_frame(Frame *f)
cpu_processor_->apply(img);
}
ocio::ConstProcessorRcPtr ColorProcessor::get_processor()
{
return processor_;
}
Color ColorProcessor::convert_color(const Color &in)
{
if (!cpu_processor_) {
@@ -163,10 +168,6 @@ 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)
{
-152
View File
@@ -1,152 +0,0 @@
/***
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();
}
}
-99
View File
@@ -1,99 +0,0 @@
/***
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
+6 -4
View File
@@ -116,10 +116,12 @@ endif()
# Symbols of the not-yet-split engine modules (codec/audio/task/config/
# pluginSupport/...) dangle by design. The backend libraries resolve most
# symbols from liboakrender at load time and dangle the same way.
foreach(t oakrender oakgl oakvulkan)
target_link_options(${t} PRIVATE
"-undefined" "dynamic_lookup"
)
foreach(t oakrender oakgl oakgl2 oakvulkan)
if(TARGET ${t})
target_link_options(${t} PRIVATE
"-undefined" "dynamic_lookup"
)
endif()
endforeach()
target_link_libraries(oakrender PRIVATE
+1
View File
@@ -17,6 +17,7 @@ endif()
add_executable(oakrender-gtest
cache_test.cpp
cancelatom_test.cpp
color_test.cpp
manager_test.cpp
renderer_test.cpp
+136
View File
@@ -0,0 +1,136 @@
/***
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/>.
***/
// Same-dir quoted include would hit the src/node/transition/render/
// bridge header (olive::CancelAtom) first on this build's include path;
// reference the public header relative to this file instead.
#include "../../../include/render/cancelatom.h"
#include <gtest/gtest.h>
#include "render/cache.h" /* oakrender_debug_alive_count */
TEST(OakCancelAtomTest, InitFree)
{
const int alive_before = oakrender_debug_alive_count();
OakCancelAtom atom = oakrender_cancelatom_init();
ASSERT_NE(atom.ctx, nullptr);
EXPECT_NE(atom.addref, nullptr);
EXPECT_NE(atom.release, nullptr);
EXPECT_EQ(atom.abi_version, OAKRENDER_ABI_VERSION);
EXPECT_EQ(oakrender_debug_alive_count(), alive_before + 1);
oakrender_cancelatom_free(&atom);
EXPECT_EQ(atom.ctx, nullptr);
EXPECT_EQ(oakrender_debug_alive_count(), alive_before);
// NULL and empty handles are no-ops
oakrender_cancelatom_free(nullptr);
oakrender_cancelatom_free(&atom);
EXPECT_EQ(oakrender_debug_alive_count(), alive_before);
}
TEST(OakCancelAtomTest, CancelStateMachine)
{
OakCancelAtom atom = oakrender_cancelatom_init();
ASSERT_NE(atom.ctx, nullptr);
int flag = -1;
EXPECT_EQ(oakrender_cancelatom_is_cancelled(atom, &flag), OAKRENDER_OK);
EXPECT_EQ(flag, 0);
EXPECT_EQ(oakrender_cancelatom_cancel(atom), OAKRENDER_OK);
// Cancel must not be heard until a consumer reads the flag
int heard = -1;
EXPECT_EQ(oakrender_cancelatom_heard_cancel(atom, &heard), OAKRENDER_OK);
EXPECT_EQ(heard, 0);
EXPECT_EQ(oakrender_cancelatom_is_cancelled(atom, &flag), OAKRENDER_OK);
EXPECT_EQ(flag, 1);
EXPECT_EQ(oakrender_cancelatom_heard_cancel(atom, &heard), OAKRENDER_OK);
EXPECT_EQ(heard, 1);
oakrender_cancelatom_free(&atom);
}
TEST(OakCancelAtomTest, InvalidArgs)
{
OakCancelAtom empty = {};
int flag = 7;
EXPECT_EQ(oakrender_cancelatom_cancel(empty), OAKRENDER_E_INVALID);
EXPECT_EQ(oakrender_cancelatom_is_cancelled(empty, &flag),
OAKRENDER_E_INVALID);
EXPECT_EQ(flag, 7);
EXPECT_EQ(oakrender_cancelatom_heard_cancel(empty, &flag),
OAKRENDER_E_INVALID);
EXPECT_EQ(flag, 7);
OakCancelAtom atom = oakrender_cancelatom_init();
ASSERT_NE(atom.ctx, nullptr);
EXPECT_EQ(oakrender_cancelatom_is_cancelled(atom, nullptr),
OAKRENDER_E_INVALID);
EXPECT_EQ(oakrender_cancelatom_heard_cancel(atom, nullptr),
OAKRENDER_E_INVALID);
oakrender_cancelatom_free(&atom);
}
TEST(OakCancelAtomTest, AddrefReleaseCountSemantics)
{
const int alive_before = oakrender_debug_alive_count();
OakCancelAtom atom = oakrender_cancelatom_init();
ASSERT_NE(atom.ctx, nullptr);
// Copy the struct and take an extra reference; both copies share the
// same underlying object and cancel state
OakCancelAtom copy = atom;
copy.addref(copy.ctx);
EXPECT_EQ(oakrender_cancelatom_cancel(atom), OAKRENDER_OK);
int flag = 0;
EXPECT_EQ(oakrender_cancelatom_is_cancelled(copy, &flag), OAKRENDER_OK);
EXPECT_EQ(flag, 1);
// Releasing one reference keeps the object alive for the other
atom.release(atom.ctx);
EXPECT_EQ(oakrender_debug_alive_count(), alive_before + 1);
flag = 0;
EXPECT_EQ(oakrender_cancelatom_is_cancelled(copy, &flag), OAKRENDER_OK);
EXPECT_EQ(flag, 1);
// The final reference destroys the object
oakrender_cancelatom_free(&copy);
EXPECT_EQ(copy.ctx, nullptr);
EXPECT_EQ(oakrender_debug_alive_count(), alive_before);
}
TEST(OakCancelAtomTest, AddrefReleaseNullCtxIsSafe)
{
// NULL ctx must not crash the thunks
OakCancelAtom atom = oakrender_cancelatom_init();
ASSERT_NE(atom.ctx, nullptr);
atom.addref(nullptr);
atom.release(nullptr);
oakrender_cancelatom_free(&atom);
}