refactor(task,render): RenderTask family over a new oakrender ticket C ABI

- oakrender: new ticket family (render_frame/render_audio with finished
  callback, result frame/samples access, cancel/wait), project copier
  C API, cache get_invalidated_ranges, manager set_aggressive_gc
- oaknode: node_copy_inputs, set_value_hint_track, viewer params
  setters, video frame cache borrowed outlet, project copy_settings
- oakcodec: encoder desired pixel format, export format extension,
  encoding generate_matrix, custom range in encoding params POD
- oaktask: RenderTask orchestrates oakrender tickets (no Qt threads/
  watchers), PreCacheTask and ExportTask rewritten over the oaknode/
  oakrender/oakcodec C ABIs; frames cross from oakrender to oakcodec
  handles by pixel copy (different control blocks)
- the two-step texture/download render path collapses into single-step
  (tickets always yield frames); subtitle sidecar kept
- tests: 105 in build-oaktask (export/precache factory paths,
  construction, conform end-to-end); regressions all green
This commit is contained in:
2026-08-07 14:40:12 +08:00
parent 6ded1d2d63
commit 40a336276f
29 changed files with 2548 additions and 726 deletions
+2
View File
@@ -2,6 +2,8 @@ target_sources(oakrender PRIVATE
renderer.cpp
cache.cpp
cancelatom.cpp
ticket.cpp
copier.cpp
color.cpp
manager.cpp
)
+34
View File
@@ -206,3 +206,37 @@ void oakrender_frame_cache_save(OakRenderCache *cache, const char *path,
} catch (...) {
}
}
int oakrender_cache_get_invalidated_ranges(OakRenderCache *cache,
int64_t in_num, int64_t in_den, int64_t out_num, int64_t out_den,
int64_t *ranges, int max_ranges)
{
if (!cache || max_ranges < 0) {
return OAKRENDER_E_INVALID;
}
try {
olive::core::TimeRangeList list = impl(cache)->get_invalidated_ranges(
olive::core::TimeRange(
olive::core::Rational(int(in_num), int(in_den)),
olive::core::Rational(int(out_num), int(out_den))));
int count = int(list.size());
if (ranges) {
int written = 0;
for (const olive::core::TimeRange &r : list) {
if (written >= max_ranges) {
break;
}
ranges[written * 4 + 0] = r.in().numerator();
ranges[written * 4 + 1] = r.in().denominator();
ranges[written * 4 + 2] = r.out().numerator();
ranges[written * 4 + 3] = r.out().denominator();
written++;
}
}
return count;
} catch (...) {
return OAKRENDER_E_FAILED;
}
}
+85
View File
@@ -0,0 +1,85 @@
/***
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 "render/copier.h"
#include <new>
#include "../src/projectcopier.h"
namespace
{
olive::ProjectCopier *impl(OakRenderProjectCopier *h)
{
return reinterpret_cast<olive::ProjectCopier *>(h);
}
} // namespace
OakRenderProjectCopier *oakrender_project_copier_create(void)
{
try {
return reinterpret_cast<OakRenderProjectCopier *>(
new olive::ProjectCopier());
} catch (...) {
return NULL;
}
}
void oakrender_project_copier_free(OakRenderProjectCopier *copier)
{
delete impl(copier);
}
int oakrender_project_copier_set_project(OakRenderProjectCopier *copier,
OakNodeProject *project)
{
if (!copier || !project) {
return OAKRENDER_E_INVALID;
}
try {
impl(copier)->set_project(
reinterpret_cast<olive::Project *>(project));
return OAKRENDER_OK;
} catch (...) {
return OAKRENDER_E_FAILED;
}
}
OakNodeNode *oakrender_project_copier_get_copy(
OakRenderProjectCopier *copier, OakNodeNode *original)
{
if (!copier || !original) {
return NULL;
}
return reinterpret_cast<OakNodeNode *>(
impl(copier)->get_copy(reinterpret_cast<olive::Node *>(original)));
}
OakNodeProject *oakrender_project_copier_get_copied_project(
OakRenderProjectCopier *copier)
{
if (!copier) {
return NULL;
}
return reinterpret_cast<OakNodeProject *>(
impl(copier)->get_copied_project());
}
+350
View File
@@ -0,0 +1,350 @@
/***
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 "render/ticket.h"
#include <new>
#include "../src/colorprocessor.h"
#include "../src/framehashcache.h"
#include "../src/rendermanager.h"
#include "../src/renderticket.h"
#include "internalhandles.h"
namespace
{
struct TicketHandle {
olive::RenderTicketWatcher *watcher;
olive::RenderManager::TicketType type;
olive::core::Rational time;
olive::core::TimeRange range;
};
TicketHandle *impl(OakRenderTicket *t)
{
return reinterpret_cast<TicketHandle *>(t);
}
OakRenderTicket *wrap(olive::RenderTicketWatcher *w,
olive::RenderManager::TicketType type)
{
if (!w) {
return NULL;
}
TicketHandle *h = new (std::nothrow) TicketHandle{
w, type, olive::core::Rational(), olive::core::TimeRange()
};
if (!h) {
delete w;
return NULL;
}
return reinterpret_cast<OakRenderTicket *>(h);
}
olive::Node *to_node(OakNodeNode *n)
{
return reinterpret_cast<olive::Node *>(n);
}
} // namespace
OakRenderTicket *oakrender_ticket_render_frame(
const oakrender_video_ticket_params *params,
oakrender_ticket_finished_fn cb, void *userdata)
{
if (!params || !params->output_node) {
return NULL;
}
olive::RenderManager *manager = olive::RenderManager::instance();
if (!manager) {
return NULL;
}
try {
const olive::VideoParams *vp =
params->video_params.ctx
? oakcommon_videoparams_get_native(params->video_params)
: nullptr;
if (!vp) {
return NULL;
}
olive::RenderManager::RenderVideoParams rvp(
to_node(params->output_node), *vp,
params->audio_params
? olive::AudioParams::from_handle(
oakcore_audioparams_copy(params->audio_params))
: olive::AudioParams(),
olive::core::Rational(int(params->time_num),
int(params->time_den)),
reinterpret_cast<olive::ColorManager *>(params->color_manager),
static_cast<olive::RenderMode::Mode>(params->mode));
rvp.force_size =
olive::FrameSize(params->force_width, params->force_height);
if (params->has_force_matrix) {
olive::Matrix4x4 matrix;
for (int row = 0; row < 4; row++) {
for (int col = 0; col < 4; col++) {
matrix(row, col) =
float(params->force_matrix[row * 4 + col]);
}
}
rvp.force_matrix = matrix;
}
rvp.force_format = static_cast<olive::core::PixelFormat::Format>(
params->force_format);
rvp.force_channel_count = params->force_channel_count;
rvp.force_color_output =
params->force_color_output ? params->force_color_output->ptr
: nullptr;
if (params->force_color_transform.ctx) {
const olive::ColorTransform *ct =
oakcommon_colortransform_get_native(
params->force_color_transform);
if (ct) {
rvp.force_color_transform = *ct;
}
}
if (params->cache) {
rvp.add_cache(reinterpret_cast<olive::FrameHashCache *>(
params->cache));
}
auto *watcher = new olive::RenderTicketWatcher();
watcher->set_property("type", olive::Variant::from_value(
int(olive::RenderManager::k_type_video)));
watcher->set_property(
"time", olive::Variant::from_value(olive::core::Rational(
int(params->time_num), int(params->time_den))));
OakRenderTicket *handle =
wrap(watcher, olive::RenderManager::k_type_video);
if (!handle) {
return NULL;
}
impl(handle)->time = olive::core::Rational(
int(params->time_num), int(params->time_den));
if (cb) {
watcher->set_finished_callback(
[handle, cb, userdata](olive::RenderTicketWatcher *) {
cb(handle, userdata);
});
}
watcher->set_ticket(manager->render_frame(rvp));
return handle;
} catch (...) {
return NULL;
}
}
OakRenderTicket *oakrender_ticket_render_audio(
OakNodeNode *output_node, int64_t in_num, int64_t in_den,
int64_t out_num, int64_t out_den, const OakAudioParams *params,
int mode, oakrender_ticket_finished_fn cb, void *userdata)
{
if (!output_node || !params) {
return NULL;
}
olive::RenderManager *manager = olive::RenderManager::instance();
if (!manager) {
return NULL;
}
try {
olive::core::Rational range_in((int)in_num, (int)in_den);
olive::core::Rational range_out((int)out_num, (int)out_den);
olive::core::TimeRange range(range_in, range_out);
olive::RenderManager::RenderAudioParams rap(
to_node(output_node), range,
olive::AudioParams::from_handle(
oakcore_audioparams_copy(params)),
static_cast<olive::RenderMode::Mode>(mode));
auto *watcher = new olive::RenderTicketWatcher();
watcher->set_property("type", olive::Variant::from_value(
int(olive::RenderManager::k_type_audio)));
watcher->set_property("range", olive::Variant::from_value(range));
OakRenderTicket *handle =
wrap(watcher, olive::RenderManager::k_type_audio);
if (!handle) {
return NULL;
}
impl(handle)->range = range;
if (cb) {
watcher->set_finished_callback(
[handle, cb, userdata](olive::RenderTicketWatcher *) {
cb(handle, userdata);
});
}
watcher->set_ticket(manager->render_audio(rap));
return handle;
} catch (...) {
return NULL;
}
}
int oakrender_ticket_is_finished(OakRenderTicket *ticket)
{
if (!ticket) {
return OAKRENDER_E_INVALID;
}
return impl(ticket)->watcher->is_running() ? 0 : 1;
}
int oakrender_ticket_wait(OakRenderTicket *ticket)
{
if (!ticket) {
return OAKRENDER_E_INVALID;
}
impl(ticket)->watcher->wait_for_finished();
return OAKRENDER_OK;
}
int oakrender_ticket_cancel(OakRenderTicket *ticket)
{
if (!ticket) {
return OAKRENDER_E_INVALID;
}
impl(ticket)->watcher->cancel();
return OAKRENDER_OK;
}
int oakrender_ticket_get_type(OakRenderTicket *ticket)
{
if (!ticket) {
return OAKRENDER_E_INVALID;
}
return int(impl(ticket)->type);
}
int oakrender_ticket_get_time(OakRenderTicket *ticket, int64_t *out_num,
int64_t *out_den)
{
if (!ticket || !out_num || !out_den) {
return OAKRENDER_E_INVALID;
}
*out_num = impl(ticket)->time.numerator();
*out_den = impl(ticket)->time.denominator();
return OAKRENDER_OK;
}
int oakrender_ticket_get_range(OakRenderTicket *ticket, int64_t *in_num,
int64_t *in_den, int64_t *out_num,
int64_t *out_den)
{
if (!ticket || !in_num || !in_den || !out_num || !out_den) {
return OAKRENDER_E_INVALID;
}
*in_num = impl(ticket)->range.in().numerator();
*in_den = impl(ticket)->range.in().denominator();
*out_num = impl(ticket)->range.out().numerator();
*out_den = impl(ticket)->range.out().denominator();
return OAKRENDER_OK;
}
int oakrender_ticket_get_frame(OakRenderTicket *ticket, OakCodecFrame **out)
{
if (!ticket || !out) {
return OAKRENDER_E_INVALID;
}
*out = NULL;
TicketHandle *h = impl(ticket);
if (h->watcher->is_running()) {
return OAKRENDER_E_STATE;
}
olive::Variant result = h->watcher->get();
if (!result.can_convert<olive::FramePtr>()) {
return OAKRENDER_E_FAILED;
}
olive::FramePtr frame = result.value<olive::FramePtr>();
if (!frame) {
return OAKRENDER_E_FAILED;
}
OakCodecFrame *handle = new (std::nothrow) OakCodecFrame;
if (!handle) {
return OAKRENDER_E_NOMEM;
}
handle->ptr = frame;
*out = handle;
return OAKRENDER_OK;
}
int oakrender_ticket_get_samples(OakRenderTicket *ticket,
OakSampleBuffer **out)
{
if (!ticket || !out) {
return OAKRENDER_E_INVALID;
}
*out = NULL;
TicketHandle *h = impl(ticket);
if (h->watcher->is_running()) {
return OAKRENDER_E_STATE;
}
olive::Variant result = h->watcher->get();
if (!result.can_convert<olive::SampleBuffer>()) {
return OAKRENDER_E_FAILED;
}
olive::SampleBuffer samples = result.value<olive::SampleBuffer>();
*out = oakcore_samplebuffer_copy(samples.handle());
return *out ? OAKRENDER_OK : OAKRENDER_E_NOMEM;
}
void oakrender_ticket_free(OakRenderTicket *ticket)
{
if (!ticket) {
return;
}
TicketHandle *h = impl(ticket);
if (h->watcher->is_running()) {
h->watcher->cancel();
h->watcher->wait_for_finished();
}
delete h->watcher;
delete h;
}
int oakrender_manager_set_aggressive_gc(int enabled)
{
olive::RenderManager *manager = olive::RenderManager::instance();
if (!manager) {
return OAKRENDER_E_STATE;
}
manager->set_aggressive_garbage_collection(enabled != 0);
return OAKRENDER_OK;
}
+1
View File
@@ -18,6 +18,7 @@ endif()
add_executable(oakrender-gtest
cache_test.cpp
cancelatom_test.cpp
ticket_test.cpp
color_test.cpp
manager_test.cpp
renderer_test.cpp
+57
View File
@@ -0,0 +1,57 @@
/***
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 <gtest/gtest.h>
#include "render/ticket.h"
TEST(OakRenderTicket, NullAndInvalidArgs)
{
EXPECT_EQ(oakrender_ticket_render_frame(nullptr, nullptr, nullptr),
nullptr);
oakrender_video_ticket_params params = {};
EXPECT_EQ(oakrender_ticket_render_frame(&params, nullptr, nullptr),
nullptr);
EXPECT_EQ(oakrender_ticket_is_finished(nullptr), OAKRENDER_E_INVALID);
EXPECT_EQ(oakrender_ticket_wait(nullptr), OAKRENDER_E_INVALID);
EXPECT_EQ(oakrender_ticket_cancel(nullptr), OAKRENDER_E_INVALID);
EXPECT_EQ(oakrender_ticket_get_type(nullptr), OAKRENDER_E_INVALID);
EXPECT_EQ(oakrender_ticket_get_time(nullptr, nullptr, nullptr),
OAKRENDER_E_INVALID);
OakCodecFrame *frame = nullptr;
EXPECT_EQ(oakrender_ticket_get_frame(nullptr, &frame),
OAKRENDER_E_INVALID);
OakSampleBuffer *samples = nullptr;
EXPECT_EQ(oakrender_ticket_get_samples(nullptr, &samples),
OAKRENDER_E_INVALID);
oakrender_ticket_free(nullptr); // no-op
}
TEST(OakRenderTicket, AggressiveGcRequiresManager)
{
// The standalone test binary never initializes the render manager
int result = oakrender_manager_set_aggressive_gc(1);
EXPECT_TRUE(result == OAKRENDER_OK || result == OAKRENDER_E_STATE);
}