diff --git a/docs/zh/plans/riir/M7-oakrender.md b/docs/zh/plans/riir/M7-oakrender.md
index b42998aea..c88ddc370 100644
--- a/docs/zh/plans/riir/M7-oakrender.md
+++ b/docs/zh/plans/riir/M7-oakrender.md
@@ -126,3 +126,99 @@ nm -D oaknode 构建产物 | grep -c " U _ZN5olive.*render" # 必须 0
```
node→render 的 47 次引用此时应全部经 oakrender C ABI(M3 遗留的
"暂不断链"在此结清)。
+
+## 实施现状(2026-08-05)
+
+M7 已落地:oakrender 整库编译通过、纯 C ABI 与测试就位、独立构建
+全绿(37 个用例:33 通过 + 4 个 GTEST_SKIP,0 失败),oaknode 回归
+96/96 不变红。以下为与上文计划的实际差异。
+
+### 最终目录结构
+
+- `src/render/src/` — 去 Qt 化 C++ 实现(`olive::` 命名空间),target
+ `oakrender`(SHARED)+ 动态后端 `oakgl`/`oakvulkan`(各自由
+ `*backend_c.cpp` 单个 TU 编成,`oak_renderer_*` C ABI,运行时由
+ DynamicRenderer dlopen);平铺结构,`src/render/src` 为 include 根。
+- `src/render/c_api/` — 纯 C ABI 包装(renderer/cache/color/manager
+ 共 4 个 .cpp + 内部头 `alivecount.h`/`internalhandles.h`),经
+ `target_sources` 合并进 `oakrender`,不单独成库。
+- `src/render/tests/` — gtest,单一 target `oakrender-gtest`
+ (cache/color/manager/renderer 4 个 _test.cpp),
+ `gtest_discover_tests`(`DISCOVERY_MODE PRE_TEST`)。
+- `include/render/`(仓库根)— 公共 C 头:`error.h` + renderer.h
+ (display/纹理/帧/blit + 后端管理)、cache.h(PlaybackCache/
+ FrameHashCache + `oakrender_debug_alive_count()`)、color.h
+ (ColorProcessor + ColorManager 静态函数)、manager.h
+ (RenderManager/PreviewAutoCacher/DiskManager + 异步帧请求)。
+- `src/render/standalone/CMakeLists.txt` — 独立构建 driver(见下)。
+- `src/render/transition/` — 过渡 stub/桥接头(render/ 前缀的桥接头
+ 转发到 `src/render/src` 真身;codec/audio/task/config/pluginSupport
+ 为 stub),供 oaknode 与 oakrender 共用(须置于
+ `src/node/transition` 之前)。
+
+### 独立构建与测试
+
+```sh
+cmake -S src/render/standalone -B build-oakrender
+cmake --build build-oakrender -j
+ctest --test-dir build-oakrender --output-on-failure
+```
+
+测试数字:oakrender **37/37**(33 通过,4 个 skip:GL 相关的
+renderer init/纹理生命周期、blit_color_managed 2 例按 §4 的
+GTEST_SKIP 模式;manager_init、request_frame 实跑 2 例因
+RenderManager 构造读取 config 悬置符号而无法在 standalone 跑,
+可跑的 E_STATE/E_INVALID/E_NOT_FOUND 路径已覆盖,含用 oaknode
+factory 真建 ViewerOutput 验证)。oaknode 回归 **96/96**。
+OCIO 配置照 oaknode 做法经 ctest `ENVIRONMENT OCIO=` 注入
+`engine/render/ocioconf/config.ocio`;color 测试对
+sRGB OETF→Linear 做已知值抽样断言(0.5 → 0.214041,容差 1e-3)。
+oakrender-gtest 照 oaknode-gtest 配方 `-Wl,-force_load` libOfxHost,
+并复用 `src/node/standalone/oakengine_ipc_shim.cpp` 垫
+oakengine_ipc_* 悬置符号。
+
+### 实际依赖与链接形态
+
+- Oak 内部:oaknode、oakcommon、oakundo、olivecore、ffmpeg_bridge
+ (真实 target,`add_subdirectory` 引入)。
+- 第三方:OpenColorIO、OpenImageIO、Imath/OpenEXR(头)、OpenGL/
+ CoreVideo/Metal/QuartzCore framework、GTest(仅测试)。
+- liboakrender + liboakgl/liboakvulkan 均以 `-undefined dynamic_lookup`
+ 悬置 codec/audio/task/pluginSupport/config 等未拆分模块符号;
+ 后端库在加载时从 liboakrender 解析大部分符号。
+- 帧缓存 save/load 往返与 `codec_frame_set/get_params` 的存储断言
+ 留待 M5(oakcodec):`codec/frame.h` 仍是 transition stub,Frame
+ 无法 allocate,本批只覆盖错误路径(save 不崩、load 未命中返回
+ E_NOT_FOUND)。
+
+### 与计划的主要差异
+
+- **§5 闭环的实现方式:oaknode 直接链接 oakrender 的 C++ 库,而不
+ 是经 oakrender C ABI。** `src/node/transition/render/*` 已从 stub
+ 改为桥接头(转发到 `src/render/src` 真身),oaknode 源里的
+ `#include "render/..."` 原样保留、解析到真身类;oaknode-gtest
+ 链接 liboakrender,node→render 引用在链接期由 C++ 符号直接结清。
+ 这是 02 §4 裁决 A 的变通执行(裁决原文要求"统一改成经 oakrender
+ C ABI"):OakRenderCache/ColorProcessor 等的纯 C ABI 已在
+ `include/render/` 就位供后续消费者(Rust 化)使用,但 oaknode
+ 自身未改写为 C ABI 调用方。
+- §5 的 grep/nm 判据按此口径重新表述(实测值,2026-08-05):
+ - `grep -rn '#include "render/' src/node/src | wc -l` = **38**
+ (非 0;全部经桥接头指向真身,无 stub 残留语义);
+ - `nm` 口径改为「liboaknode 的 `U __ZN5olive*` 未解析符号
+ 共 **56** 个,**0 个悬置**」:25 个由 liboakrender 解析
+ (PlaybackCache/FrameHashCache/DiskManager/RenderManager/
+ ColorProcessor/PreviewAutoCacher/LUTLibrary 等),其余 31 个由
+ oakcommon(VideoParams/XmlStream*/SubtitleParams/QtUtils)、
+ oakundo(UndoCommand/UndoStack/MultiUndoCommand vtable)解析。
+- 公共头位于 `include/render/`(非 §1 的 `include/oakrender/`),
+ 函数前缀 `oakrender_`,与 oaknode/oakcommon 的既有契约一致
+ (§1 目标形态中的 `oakrender/include/oakrender/` 未采用)。
+- C API 命名照 R7-A §A.2 的 display.h 重写版改前缀
+ `oakrender_display_*`/`oakrender_codec_frame_*`;`OakCodecFrame`
+ 在 renderer.h 定义(M5 未落地,按 §2.2 注释在 oakrender 侧先写)。
+- 已知坑(已解,记录给后续模块):`src/render/transition/render/`
+ 桥接头占用 `"render/renderer.h"` 等拼写,与公共头
+ `include/render/renderer.h` 撞名——公共头内部互相同目录
+ quoted include,c_api 源用相对路径包含公共头,测试目标
+ `BEFORE PRIVATE` 把 `include/` 提到最前。
diff --git a/docs/zh/plans/riir/notes.md b/docs/zh/plans/riir/notes.md
index d10a36d03..26c63ec7b 100644
--- a/docs/zh/plans/riir/notes.md
+++ b/docs/zh/plans/riir/notes.md
@@ -213,3 +213,36 @@ ColorManager)去Qt化过程中的删除与语义变化,迁移调用方时需
见 `src/node/DEQT.md` §7(如 `Sequence::update_track_cache()`、
`Footage::check_footage()/default_color_space_changed()/
proxy_ready()/proxy_finished()`、5 条 invalidate_from_keyframe_*)。
+
+## oakrender 去Qt化的删除与语义变更(2026-08-05)
+
+- `RenderTicket::finished` 不再跨线程补发:回调在调用 finish() 的
+ 线程上触发;未决状态由 facade 自查 `is_running()`。
+- PreviewAutoCacher 延迟重排队改显式标志:单次 QTimer 重排队删除,
+ `try_render()` 置 `delayed_requeue_pending_`,facade 在
+ `requeue_delay_ms()` 后重调 `try_render()`。
+- DiskCacheFolder 周期存盘由 GUI 线程 QTimer 改后台线程,数据保护
+ 用 `std::recursive_mutex`(原靠 QObject 线程亲和串行化)。
+- PlaybackCache 的 Qt 信号改单回调注册:`set_invalidated_callback`/
+ `set_validated_callback`/`set_requested_callback`/
+ `set_cancel_all_callback`;跨层 invalidated/validated 通知由 facade
+ 在命令后重发(M7 §2.2:oakrender 不持有上层回调)。
+- `DiskManager::instance()` 惰性自建(原 app 启动时显式
+ create_instance;库消费者不经 facade 也能用)。
+- FrameHashCache 帧缓存 JPEG 读写 QImage→OIIO/OpenEXR。
+- AudioWaveformCache/PlaybackCache 的 QPainter 绘制归 app 层;
+ 缓存指示条高度固化为常量 4(`oakrender_cache_indicator_height()`,
+ 原 `QFontMetrics(QFont()).height()/4`)。
+- 离屏 GL 上下文共享组需 app 显式传入(原 QOpenGLContext
+ globalShareContext 隐式共享)。
+- OpenGLContext/Vulkan 上下文抽象层(OpenGLContextProvider 等)替代
+ QOpenGLContext 直用。
+- WorkerProcess 仅 POSIX 实现(Windows 占位未做)。
+- worker IPC 的 NDJSON 协议未变,workerjson 为本地(去 Qt)实现。
+- `:/shaders` qrc 资源缺口:着色器源码的 Qt 资源路径在库形态下
+ 不可用,需 app 侧提供文件映射。
+- 删除 ipc/ipcmessage.cpp 等 3 个过期 .cpp(对应头已自包含 inline,
+ .cpp 内容失效)。
+- displayinternal.h 分层违规(render→src/capi)并入 texturehandle.h。
+- Renderer 线程亲和由 QObject::thread() 改显式 owner thread
+ (`set_owner_thread_to_current()`/`called_on_owner_thread()`)。
diff --git a/include/render/cache.h b/include/render/cache.h
new file mode 100644
index 000000000..655a2f3ef
--- /dev/null
+++ b/include/render/cache.h
@@ -0,0 +1,146 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAK_EDITOR_RENDER_CACHE_H
+#define OAK_EDITOR_RENDER_CACHE_H
+
+#include
+
+// Same-dir quoted includes: inside this build the engine-style spelling
+// "render/renderer.h" resolves to the transition bridge headers, so the
+// public headers reference each other relative to their own directory.
+#include "error.h"
+#include "renderer.h" /* OakCodecFrame */
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @file cache.h
+ * @brief C ABI for the oakrender playback/frame-hash caches
+ * (olive::PlaybackCache / olive::FrameHashCache), M7 §2.2.
+ *
+ * An OakRenderCache IS a reinterpreted olive::FrameHashCache (created
+ * without a parent node), no wrapper allocation. Handles from
+ * oakrender_cache_create() are owned by the caller and must be released
+ * with oakrender_cache_free().
+ *
+ * All timestamps are int64 frame numbers in the cache's timebase (see
+ * oakrender_cache_set_timebase()); a cache without a valid timebase
+ * treats timestamps as whole seconds.
+ *
+ * No cache events cross the boundary (M7 §2.2, 2026-08 revision):
+ * invalidate/validate are triggered by and known to the caller; the
+ * facade re-emits notifications after the triggering command.
+ */
+typedef struct OakRenderCache OakRenderCache;
+
+/**
+ * @brief Create a detached frame hash cache (no parent node, no
+ * timebase). Owned by the caller.
+ *
+ * @return Cache handle, or NULL on allocation failure.
+ */
+OakRenderCache *oakrender_cache_create(void);
+
+/** @brief Destroy a cache created by oakrender_cache_create(). NULL-safe. */
+void oakrender_cache_free(OakRenderCache *cache);
+
+/**
+ * @brief Set the frame timebase used to interpret all timestamps of this
+ * cache (FrameHashCache::set_timebase()).
+ *
+ * @return OAKRENDER_OK, or OAKRENDER_E_INVALID for NULL cache or
+ * non-positive num/den.
+ */
+int oakrender_cache_set_timebase(OakRenderCache *cache, int num, int den);
+
+/**
+ * @brief Set the cache UUID used in on-disk frame cache filenames
+ * (PlaybackCache::set_uuid()).
+ *
+ * @return OAKRENDER_OK or OAKRENDER_E_INVALID.
+ */
+int oakrender_cache_set_uuid(OakRenderCache *cache, const char *uuid);
+
+/**
+ * @brief Mark the timestamp range [in_ts, out_ts) invalidated
+ * (PlaybackCache::invalidate()). NULL cache is a no-op.
+ */
+void oakrender_cache_invalidate(OakRenderCache *cache, int64_t in_ts,
+ int64_t out_ts);
+
+/**
+ * @brief Mark the timestamp range [in_ts, out_ts) validated
+ * (PlaybackCache::validate()). NULL cache is a no-op.
+ */
+void oakrender_cache_validate(OakRenderCache *cache, int64_t in_ts,
+ int64_t out_ts);
+
+/**
+ * @brief 1 when the cache holds any validated range
+ * (PlaybackCache::has_validated_ranges()), 0 otherwise / on NULL.
+ */
+int oakrender_cache_has_validated_ranges(const OakRenderCache *cache);
+
+/**
+ * @brief Timeline cache indicator height in pixels
+ * (PlaybackCache::get_cache_indicator_height()). Constant query.
+ */
+int oakrender_cache_indicator_height(void);
+
+/**
+ * @brief Load a cached frame from disk
+ * (FrameHashCache::load_cache_frame(cache_path, uuid, ts)).
+ *
+ * @param path Cache directory (e.g. oakrender_disk_cache_path()).
+ * @param uuid Cache UUID of the producing node.
+ * @param out_frame Receives an owned frame handle (release with
+ * oakrender_codec_frame_free()).
+ *
+ * @return OAKRENDER_OK, OAKRENDER_E_INVALID (NULL argument), or
+ * OAKRENDER_E_NOT_FOUND (no cached frame at `ts` / undecodable).
+ */
+int oakrender_frame_cache_load(OakRenderCache *cache, const char *path,
+ const char *uuid, int64_t ts,
+ OakCodecFrame **out_frame);
+
+/**
+ * @brief Save a frame to the disk cache under the cache's timebase and
+ * the frame's own timestamp (FrameHashCache::save_cache_frame()).
+ * NULL arguments are a no-op.
+ */
+void oakrender_frame_cache_save(OakRenderCache *cache, const char *path,
+ const char *uuid, const OakCodecFrame *frame);
+
+/* ---- Debug --------------------------------------------------------------- */
+
+/**
+ * @brief Number of live oakrender-owned objects (caches, textures,
+ * frames, color processors) for leak assertions in tests.
+ */
+int oakrender_debug_alive_count(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif //OAK_EDITOR_RENDER_CACHE_H
diff --git a/include/render/color.h b/include/render/color.h
new file mode 100644
index 000000000..a1721e6b7
--- /dev/null
+++ b/include/render/color.h
@@ -0,0 +1,138 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAK_EDITOR_RENDER_COLOR_H
+#define OAK_EDITOR_RENDER_COLOR_H
+
+#include "error.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @file color.h
+ * @brief C ABI for oakrender color processing (olive::ColorProcessor) and
+ * the process-wide default OCIO config (olive::ColorManager
+ * statics), M7 §2.3.
+ *
+ * An OakColorProcessor IS a wrapper allocation holding a
+ * ColorProcessorPtr (ColorProcessor is shared_ptr-managed); release with
+ * oakrender_color_processor_free(). NULL is accepted by every function
+ * and yields a no-op / OAKRENDER_E_INVALID.
+ *
+ * Processors are built against the process-wide default OCIO config
+ * (olive::ColorManager::get_default_config()): the $OCIO config when the
+ * environment variable is set, otherwise the config extracted to the
+ * user configuration location. oakrender_color_manager_set_up_default_config()
+ * (re)builds it.
+ */
+
+/** Direction values for oakrender_color_processor_create(). */
+enum {
+ OAKRENDER_COLOR_DIRECTION_NORMAL = 0,
+ OAKRENDER_COLOR_DIRECTION_INVERSE = 1
+};
+
+typedef struct OakColorProcessor OakColorProcessor;
+
+/**
+ * @brief Create a colorspace-to-colorspace processor on the default
+ * OCIO config.
+ *
+ * @param src_space Source colorspace name (role names are resolved).
+ * @param dst_transform Destination colorspace / output transform name.
+ * @param direction OAKRENDER_COLOR_DIRECTION_NORMAL (src -> dst) or
+ * OAKRENDER_COLOR_DIRECTION_INVERSE (dst -> src).
+ *
+ * OCIO failures are non-fatal (matching the C++ behavior): the handle is
+ * still returned but oakrender_color_processor_is_valid() reports 0 and
+ * conversions are pass-through.
+ *
+ * @return Processor handle, or NULL for NULL/empty strings, an unknown
+ * direction, no default config, or allocation failure.
+ */
+OakColorProcessor *oakrender_color_processor_create(const char *src_space,
+ const char *dst_transform,
+ int direction);
+
+/** @brief Release a processor handle. NULL-safe no-op. */
+void oakrender_color_processor_free(OakColorProcessor *processor);
+
+/**
+ * @brief 1 when the processor holds a valid OCIO processor
+ * (ColorProcessor::get_processor() != null), 0 otherwise / on NULL.
+ */
+int oakrender_color_processor_is_valid(const OakColorProcessor *processor);
+
+/**
+ * @brief Convert a single RGBA color (ColorProcessor::convert_color()).
+ * On an invalid processor the input is copied through.
+ *
+ * @return OAKRENDER_OK, or OAKRENDER_E_INVALID for NULL arguments.
+ */
+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);
+
+/* ---- ColorManager statics ------------------------------------------------- */
+
+/**
+ * @brief (Re)build the process-wide default OCIO config
+ * (ColorManager::set_up_default_config()).
+ *
+ * @return OAKRENDER_OK, or OAKRENDER_E_FAILED when no config could be
+ * created.
+ */
+int oakrender_color_manager_set_up_default_config(void);
+
+/**
+ * @brief Describe the active default config: the $OCIO path when set,
+ * otherwise the extracted default config's path. Two-stage string
+ * getter: returns the required buffer size including NUL; pass
+ * buf == NULL or too small a buffer to query the size.
+ *
+ * @return Required size (non-negative), or OAKRENDER_E_STATE when no
+ * default config exists.
+ */
+int oakrender_color_manager_get_config(char *buf, int n);
+
+/**
+ * @brief OCIO cache id of the display/view transform of the active
+ * default config, computed from the config's reference colorspace
+ * (a stable identifier usable as a conversion cache key).
+ *
+ * Two-stage string getter (same convention as
+ * oakrender_color_manager_get_config()).
+ *
+ * @return Required size (non-negative), OAKRENDER_E_INVALID (NULL/empty
+ * display or view), OAKRENDER_E_STATE (no default config), or
+ * OAKRENDER_E_NOT_FOUND (unknown display/view).
+ */
+int oakrender_color_manager_display_transform(const char *display,
+ const char *view, char *buf,
+ int n);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif //OAK_EDITOR_RENDER_COLOR_H
diff --git a/include/render/error.h b/include/render/error.h
new file mode 100644
index 000000000..87e309fcc
--- /dev/null
+++ b/include/render/error.h
@@ -0,0 +1,39 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAK_EDITOR_RENDER_ERROR_H
+#define OAK_EDITOR_RENDER_ERROR_H
+
+/**
+ * @brief Status and error codes shared by all oakrender C API families.
+ *
+ * Return-code convention (mirrors include/node/error.h):
+ * 0 (OAKRENDER_OK) on success, a negative OAKRENDER_E_* error code on
+ * failure. String getters return the required buffer size in bytes
+ * (including the terminating NUL) as a non-negative value instead.
+ */
+#define OAKRENDER_OK 0 /**< Success. */
+#define OAKRENDER_E_INVALID (-1) /**< NULL handle or invalid argument. */
+#define OAKRENDER_E_STATE (-2) /**< Call not valid in the current state. */
+#define OAKRENDER_E_FAILED (-3) /**< The underlying operation failed. */
+#define OAKRENDER_E_NOT_FOUND (-4) /**< Index out of range / entry not found. */
+#define OAKRENDER_E_NOMEM (-5) /**< Allocation failed. */
+
+#endif //OAK_EDITOR_RENDER_ERROR_H
diff --git a/include/render/manager.h b/include/render/manager.h
new file mode 100644
index 000000000..69e29a6f8
--- /dev/null
+++ b/include/render/manager.h
@@ -0,0 +1,157 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAK_EDITOR_RENDER_MANAGER_H
+#define OAK_EDITOR_RENDER_MANAGER_H
+
+#include
+
+// See cache.h for why these are same-dir relative includes.
+#include "cache.h" /* OakCodecFrame */
+#include "color.h" /* OakColorProcessor */
+#include "error.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @file manager.h
+ * @brief C ABI for the oakrender render manager / preview auto-cacher /
+ * disk cache singletons (olive::RenderManager,
+ * olive::PreviewAutoCacher, olive::DiskManager), M7 §2.4.
+ *
+ * The render manager is a process-wide singleton gated by
+ * oakrender_manager_init() / oakrender_manager_shutdown(). Functions
+ * that need it return OAKRENDER_E_STATE when it is not up.
+ *
+ * The frame request callback is the asynchronous command return channel
+ * (M7 §2.2 note): it fires on a render worker thread, possibly after
+ * cancellation. The delivered OakCodecFrame is owned by the callback
+ * recipient (release with oakrender_codec_frame_free()); a NULL frame
+ * signals "no result" (cancelled or failed). Beyond this callback there
+ * are no event subscription interfaces.
+ */
+
+/**
+ * @brief Borrowed node handle (olive::Node) from the oaknode ABI,
+ * re-declared here so this header is self-contained.
+ */
+typedef struct OakNodeNode OakNodeNode;
+
+/**
+ * @brief Create the RenderManager singleton (spawns render/audio
+ * threads, loads the configured backend).
+ *
+ * @return OAKRENDER_OK, OAKRENDER_E_STATE (already initialized), or
+ * OAKRENDER_E_FAILED.
+ */
+int oakrender_manager_init(void);
+
+/**
+ * @brief Destroy the RenderManager singleton. No-op when not
+ * initialized.
+ */
+void oakrender_manager_shutdown(void);
+
+/**
+ * @brief Completion callback of an asynchronous frame request.
+ *
+ * @param frame Owned frame handle, or NULL when the request finished
+ * without a result (cancelled/failed).
+ * @param ts The request's timestamp, passed back verbatim.
+ */
+typedef void (*oakrender_frame_ready_fn)(OakCodecFrame *frame, int64_t ts,
+ void *userdata);
+
+/**
+ * @brief Asynchronously render one frame of `viewer` at `ts`
+ * (PreviewAutoCacher::get_single_frame()).
+ *
+ * `ts` is a frame number in the viewer node's video timebase (a whole
+ * second count when the viewer carries no valid timebase). The
+ * completion is delivered through `cb`; until then the request can be
+ * cancelled with oakrender_cancel_request().
+ *
+ * @return A positive request id, or a negative OAKRENDER_E_* code
+ * (OAKRENDER_E_INVALID for NULL viewer/callback,
+ * OAKRENDER_E_STATE when the manager is not initialized,
+ * OAKRENDER_E_FAILED when no ticket could be created).
+ */
+int64_t oakrender_request_frame(OakNodeNode *viewer, int64_t ts,
+ oakrender_frame_ready_fn cb, void *userdata);
+
+/**
+ * @brief Cancel a pending frame request. The callback still fires with a
+ * NULL frame.
+ *
+ * @return OAKRENDER_OK, or OAKRENDER_E_NOT_FOUND for an unknown id.
+ */
+int oakrender_cancel_request(int64_t request_id);
+
+/**
+ * @brief Set the multicam node on the manager's auto-cacher
+ * (PreviewAutoCacher::set_multicam_node()). `multicam_or_NULL` is a
+ * borrowed oaknode handle to a MultiCamNode (NULL to clear).
+ *
+ * @return OAKRENDER_OK or OAKRENDER_E_STATE.
+ */
+int oakrender_set_cacher_multicam(OakNodeNode *multicam_or_NULL);
+
+/**
+ * @brief Set the display color processor on the manager's auto-cacher
+ * (PreviewAutoCacher::set_display_color_processor()). Borrowed handle,
+ * NULL to clear.
+ *
+ * @return OAKRENDER_OK or OAKRENDER_E_STATE.
+ */
+int oakrender_set_display_color_processor(OakColorProcessor *p_or_NULL);
+
+/* ---- Disk cache (olive::DiskManager) -------------------------------------- */
+
+/**
+ * @brief The default disk cache directory
+ * (DiskManager::get_default_disk_cache_path()). Two-stage string getter:
+ * returns the required buffer size including NUL; pass buf == NULL or
+ * too small a buffer to query the size. Does not require the manager.
+ */
+int oakrender_disk_cache_path(char *buf, int n);
+
+/**
+ * @brief Bytes currently consumed by the default disk cache folder.
+ * Lazily creates the DiskManager singleton on first use.
+ *
+ * @return Consumption in bytes (>= 0), or OAKRENDER_E_FAILED.
+ */
+int64_t oakrender_disk_cache_size(void);
+
+/**
+ * @brief Clear the default disk cache folder
+ * (DiskManager::clear_disk_cache()).
+ *
+ * @return OAKRENDER_OK or OAKRENDER_E_FAILED.
+ */
+int oakrender_disk_cache_clear(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif //OAK_EDITOR_RENDER_MANAGER_H
diff --git a/include/render/renderer.h b/include/render/renderer.h
new file mode 100644
index 000000000..2fb7ab5dd
--- /dev/null
+++ b/include/render/renderer.h
@@ -0,0 +1,286 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAK_EDITOR_RENDER_RENDERER_H
+#define OAK_EDITOR_RENDER_RENDERER_H
+
+#include
+
+#include "error.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @file renderer.h
+ * @brief C ABI for the oakrender display renderer (olive::Renderer) —
+ * renderer/texture/frame/blit families plus backend management.
+ *
+ * Signatures follow the R7-A display.h rewrite
+ * (docs/zh/plans/completed/r7-pure-abi-plan.md §A.2) with the
+ * oakrender_ prefix (M7 §2.1).
+ *
+ * Ownership protocol: textures and frames are opaque handles pointing to
+ * oakrender-heap control blocks (internally holding std::shared_ptr;
+ * invisible to the ABI). Ownership transfers via explicit retain/free.
+ * Every retain must be paired with exactly one free. NULL is accepted by
+ * every function and yields a no-op / zero result / OAKRENDER_E_INVALID.
+ *
+ * Cross-thread handoff (§A.3): the producing side retains before
+ * publishing a handle into a shared slot; the consuming side frees the
+ * handle it replaced. The side holding the slot when it is torn down
+ * frees the remaining handle.
+ *
+ * Handles:
+ * - OakRenderRenderer IS a reinterpreted olive::Renderer (no wrapper).
+ * - OakRenderTexture / OakCodecFrame are refcounted control blocks.
+ * - `gl_context` is an opaque borrowed olive::OpenGLContext* (or NULL
+ * to let the backend create its own offscreen surface).
+ */
+
+/**
+ * @brief POD mirror of olive::VideoParams' user-facing fields.
+ *
+ * Same layout and field semantics as oak_video_params
+ * (engine/include/oakengine/videoparams.h): `time_base_*` is the frame
+ * duration (frame rate flipped), `format` an olive::PixelFormat::Format
+ * value, `interlacing` an olive::VideoParams::Interlacing value,
+ * `color_range` an olive::VideoParams::ColorRange value. The video
+ * channel count is an engine-internal constant and not exposed.
+ */
+typedef struct oakrender_video_params {
+ int width;
+ int height;
+ int time_base_num; /**< Frame duration numerator (e.g. 1001/30000 s). */
+ int time_base_den;
+ int format; /**< olive::PixelFormat::Format. */
+ int pixel_aspect_num;
+ int pixel_aspect_den;
+ int interlacing; /**< olive::VideoParams::Interlacing. */
+ int color_range; /**< olive::VideoParams::ColorRange. */
+ int divider; /**< Preview resolution divider (1 = full). */
+ int video_type; /**< olive::VideoParams::Type (0 = video). */
+ int premultiplied_alpha; /**< 0/1. */
+} oakrender_video_params;
+
+typedef struct OakRenderRenderer OakRenderRenderer;
+typedef struct OakRenderTexture OakRenderTexture;
+
+/**
+ * @brief Opaque CPU frame handle (refcounted control block around an
+ * olive::FramePtr). Declared here so the cache family (render/cache.h)
+ * can use the same type; the frame functions live in this header.
+ * Named OakCodecFrame per the M7 §2.2 contract; the oakcodec wave (M5)
+ * adopts the same handle.
+ */
+typedef struct OakCodecFrame OakCodecFrame;
+
+/**
+ * @brief Flattened POD of olive::ColorTransformJob for the display blit
+ * path. `matrix`/`crop_matrix` are column-major 4x4; an all-zero matrix
+ * means identity.
+ */
+typedef struct oakrender_color_transform_job {
+ const void *processor; /**< OakColorProcessor* (borrowed), may be NULL. */
+ void *input_texture; /**< OakRenderTexture* (borrowed, not retained). */
+ int input_alpha_association; /**< 0=none, 1=associated. */
+ int clear_destination; /**< 0/1. */
+ int force_opaque; /**< 0/1. */
+ float matrix[16];
+ float crop_matrix[16];
+} oakrender_color_transform_job;
+
+/* ---- Renderer lifecycle -------------------------------------------------- */
+
+/**
+ * @brief Create a renderer on the named dynamic backend ("opengl",
+ * "vulkan"; olive::DynamicRenderer). Loads the backend shared library;
+ * falls back per DynamicRenderer rules.
+ *
+ * @return Renderer handle, or NULL on NULL/empty backend id, load
+ * failure, or allocation failure.
+ */
+OakRenderRenderer *oakrender_display_renderer_create_dynamic(
+ const char *backend_id);
+
+/**
+ * @brief Create an OpenGL renderer (olive::OpenGLRenderer). The renderer
+ * is not initialized; call oakrender_display_renderer_init() before use.
+ *
+ * @return Renderer handle, or NULL on allocation failure.
+ */
+OakRenderRenderer *oakrender_display_renderer_create_opengl(void);
+
+/**
+ * @brief Initialize a renderer. `gl_context` is a borrowed opaque
+ * olive::OpenGLContext*, or NULL to use the backend's default
+ * device/context path (Renderer::init()).
+ *
+ * @return OAKRENDER_OK, OAKRENDER_E_INVALID (NULL renderer), or
+ * OAKRENDER_E_FAILED (backend init failed).
+ */
+int oakrender_display_renderer_init(OakRenderRenderer *renderer,
+ void *gl_context);
+
+/**
+ * @brief Destroy a renderer (Renderer::destroy() + delete). NULL-safe
+ * no-op.
+ */
+void oakrender_display_renderer_destroy(OakRenderRenderer *renderer);
+
+/* ---- Renderer queries ---------------------------------------------------- */
+
+/** @brief 1 when the renderer is OpenGL-based, 0 otherwise / on NULL. */
+int oakrender_display_renderer_is_open_gl(const OakRenderRenderer *renderer);
+
+/** @brief 1 when the renderer is Vulkan-based, 0 otherwise / on NULL. */
+int oakrender_display_renderer_is_vulkan(const OakRenderRenderer *renderer);
+
+/* ---- Texture handle (opaque, refcounted) --------------------------------- */
+
+/**
+ * @brief Create a GPU texture on `renderer`.
+ *
+ * @param pixels Initial pixel data, or NULL for an uninitialized texture.
+ * @param linesize Stride of `pixels` in bytes (0 when pixels is NULL).
+ * @return New texture handle (refcount=1), or NULL on invalid arguments /
+ * allocation failure.
+ */
+OakRenderTexture *oakrender_display_texture_create(
+ OakRenderRenderer *renderer, const oakrender_video_params *params,
+ const void *pixels, int linesize);
+
+/** @brief Increment refcount, return the same handle. NULL-safe. */
+OakRenderTexture *oakrender_display_texture_retain(OakRenderTexture *texture);
+
+/** @brief Decrement refcount; frees at zero. NULL-safe. */
+void oakrender_display_texture_free(OakRenderTexture *texture);
+
+int oakrender_display_texture_upload(OakRenderTexture *texture,
+ const void *pixels, int linesize);
+
+int oakrender_display_texture_download(OakRenderTexture *texture, void *pixels,
+ int linesize);
+
+/* ---- Texture queries ----------------------------------------------------- */
+
+int oakrender_display_texture_get_params(const OakRenderTexture *texture,
+ oakrender_video_params *out);
+
+/** @brief Native texture id (0 on NULL or a dummy/id-less texture). */
+int oakrender_display_texture_id(const OakRenderTexture *texture);
+
+/* ---- Frame handle (opaque, refcounted) ----------------------------------- */
+
+/** @brief Create an empty CPU frame. Returns handle (refcount=1). */
+OakCodecFrame *oakrender_codec_frame_create(void);
+
+/** @brief Increment refcount, return the same handle. NULL-safe. */
+OakCodecFrame *oakrender_codec_frame_retain(OakCodecFrame *frame);
+
+/** @brief Decrement refcount; frees at zero. NULL-safe. */
+void oakrender_codec_frame_free(OakCodecFrame *frame);
+
+int oakrender_codec_frame_set_video_params(
+ OakCodecFrame *frame, const oakrender_video_params *params);
+
+int oakrender_codec_frame_get_params(const OakCodecFrame *frame,
+ oakrender_video_params *out);
+
+/**
+ * @brief Allocate the pixel buffer per the frame's video params
+ * (Frame::allocate()).
+ *
+ * @return OAKRENDER_OK, OAKRENDER_E_INVALID (NULL frame), or
+ * OAKRENDER_E_FAILED (invalid params / allocation failed).
+ */
+int oakrender_codec_frame_allocate(OakCodecFrame *frame);
+
+/** @brief Borrowed pixel data pointer (valid until free). */
+void *oakrender_codec_frame_data(OakCodecFrame *frame);
+
+/** @brief Borrowed const pixel data pointer. */
+const void *oakrender_codec_frame_const_data(const OakCodecFrame *frame);
+
+/** @brief Line stride in bytes. */
+int oakrender_codec_frame_linesize_bytes(const OakCodecFrame *frame);
+
+/** @brief 1 when the pixel buffer is allocated, 0 otherwise / on NULL. */
+int oakrender_codec_frame_is_allocated(const OakCodecFrame *frame);
+
+/* ---- Color-managed blit -------------------------------------------------- */
+
+/**
+ * @brief Blit a color-managed image through the OCIO pipeline
+ * (Renderer::blit_color_managed()).
+ *
+ * @param dst_texture Destination texture handle, or NULL for the current
+ * output target.
+ * @param params Destination video params, or NULL to use dst_texture's.
+ */
+int oakrender_display_renderer_blit_color_managed(
+ OakRenderRenderer *renderer, const oakrender_color_transform_job *job,
+ OakRenderTexture *dst_texture, const oakrender_video_params *params);
+
+/* ---- 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);
+
+/* ---- Backend management (M7 §2.1) ---------------------------------------- */
+
+/**
+ * @brief Number of known render backends (olive::RenderManager::Backend:
+ * opengl, vulkan, multiprocess, dummy).
+ */
+int oakrender_backend_count(void);
+
+/**
+ * @brief Id string of the `i`-th backend ("opengl", ...). Two-stage
+ * string getter: returns the required buffer size including NUL; pass
+ * buf == NULL or too small a buffer to query the size.
+ *
+ * @return Required size (non-negative), or OAKRENDER_E_NOT_FOUND when
+ * `i` is out of range.
+ */
+int oakrender_backend_id_at(int i, char *buf, int n);
+
+/**
+ * @brief Record the requested backend id (applied to the RenderManager
+ * instance when one exists).
+ *
+ * @return OAKRENDER_OK, or OAKRENDER_E_INVALID for a NULL/unknown id.
+ */
+int oakrender_set_backend(const char *backend_id);
+
+/**
+ * @brief The effective backend: the RenderManager instance's backend when
+ * an instance exists, otherwise the requested backend. Two-stage string
+ * getter (same convention as oakrender_backend_id_at()).
+ */
+int oakrender_current_backend(char *buf, int n);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif //OAK_EDITOR_RENDER_RENDERER_H
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index c0819a57e..df31c126e 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -1,2 +1,3 @@
add_subdirectory(common)
add_subdirectory(undo)add_subdirectory(node)
+add_subdirectory(render)
diff --git a/src/node/standalone/CMakeLists.txt b/src/node/standalone/CMakeLists.txt
index a7efbb99e..78b42bcf4 100644
--- a/src/node/standalone/CMakeLists.txt
+++ b/src/node/standalone/CMakeLists.txt
@@ -74,8 +74,14 @@ add_subdirectory(${OAK_REPO_ROOT}/src/node ${CMAKE_BINARY_DIR}/node)
# The transition stub dir must precede ${OAK_REPO_ROOT}/include so that e.g.
# "common/current.h" resolves to the transitional forwarder, not the stale
# public mirror in include/common.
+# M7 收尾:src/node/transition/render/* 已改为桥接头,转发到
+# src/render/src 真身;因此 oaknode 需要 src/render/src 在 include 路径上,
+# 且 src/render/transition 必须置于 src/node/transition 之前(两侧共用
+# render 的扩展 stub,契约一致)。
target_include_directories(oaknode BEFORE PUBLIC
+ ${OAK_REPO_ROOT}/src/render/transition
${OAK_REPO_ROOT}/src/node/transition
+ ${OAK_REPO_ROOT}/src/render/src
)
target_include_directories(oaknode PUBLIC
@@ -89,3 +95,55 @@ target_include_directories(oaknode PUBLIC
target_link_options(oaknode PRIVATE
"-undefined" "dynamic_lookup"
)
+
+# M7 收尾:oaknode 的 render/ 引用现在解析到 src/render/src 的真身类,
+# 其成员函数(如 PlaybackCache::InvalidateEvent)定义在 liboakrender 里。
+# 把 oakrender 构建进来并链进测试二进制;未拆分模块符号同样悬置。
+set(BUILD_TESTS OFF)
+add_subdirectory(${OAK_REPO_ROOT}/src/render/src ${CMAKE_BINARY_DIR}/render)
+set(BUILD_TESTS ON)
+
+target_include_directories(oakrender BEFORE PUBLIC
+ ${OAK_REPO_ROOT}/src/render/transition
+ ${OAK_REPO_ROOT}/src/node/transition
+)
+
+target_include_directories(oakrender PUBLIC
+ ${OAK_REPO_ROOT}/engine/include
+ ${OAK_REPO_ROOT}/third_party/openfx/HostSupport/include
+ /opt/homebrew/include
+ /opt/homebrew/include/Imath
+ /opt/homebrew/include/OpenEXR
+)
+
+foreach(t oakrender oakgl oakvulkan)
+ target_link_options(${t} PRIVATE
+ "-undefined" "dynamic_lookup"
+ )
+endforeach()
+
+target_link_libraries(oakrender PRIVATE
+ oaknode
+ oakcommon
+ oakundo
+ olivecore
+ ffmpeg_bridge
+ ${OCIO_LIBRARIES}
+ ${OIIO_LIBRARIES}
+ "-framework OpenGL"
+ "-framework CoreVideo"
+ "-framework Metal"
+ "-framework QuartzCore"
+)
+
+target_link_libraries(oaknode-gtest PRIVATE oakrender)
+
+# liboakrender references the oakengine_ipc_* C ABI (worker IPC) via
+# dynamic_lookup; the real implementation (engine/src/capi/ipc.cpp) is still
+# Qt-based, so the test binary links an inert shim instead.
+target_sources(oaknode-gtest PRIVATE
+ ${CMAKE_CURRENT_SOURCE_DIR}/oakengine_ipc_shim.cpp
+)
+target_include_directories(oaknode-gtest PRIVATE
+ ${OAK_REPO_ROOT}/engine/include
+)
diff --git a/src/node/standalone/oakengine_ipc_shim.cpp b/src/node/standalone/oakengine_ipc_shim.cpp
new file mode 100644
index 000000000..0a5c61217
--- /dev/null
+++ b/src/node/standalone/oakengine_ipc_shim.cpp
@@ -0,0 +1,174 @@
+// 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.
+//
+// Test-only inert shim for the oakengine_ipc_* C ABI (real implementation:
+// engine/src/capi/ipc.cpp, still Qt-based and not buildable here).
+// liboakrender references these symbols via -undefined dynamic_lookup; the
+// oaknode test binary must provide them in the flat namespace so dyld can
+// bind liboakrender at startup. The node-level tests never drive the render
+// worker IPC paths, so every entry point fails/inert-safe: open returns 0,
+// pools report invalid, acquire/consume report empty.
+
+#include "oakengine/ipc.h"
+
+#include
+#include
+
+struct OakSharedMemoryRegion {
+ int unused;
+};
+
+struct OakFrameSlotPool {
+ int unused;
+};
+
+extern "C" {
+
+OakSharedMemoryRegion *oakengine_ipc_shm_create(void)
+{
+ return new OakSharedMemoryRegion{};
+}
+
+void oakengine_ipc_shm_free(OakSharedMemoryRegion *self)
+{
+ delete self;
+}
+
+int oakengine_ipc_shm_open(OakSharedMemoryRegion *, const char *, size_t,
+ oak_ipc_shm_mode)
+{
+ return 0;
+}
+
+void oakengine_ipc_shm_close(OakSharedMemoryRegion *) {}
+
+int oakengine_ipc_shm_is_valid(const OakSharedMemoryRegion *)
+{
+ return 0;
+}
+
+void *oakengine_ipc_shm_data(OakSharedMemoryRegion *)
+{
+ return nullptr;
+}
+
+size_t oakengine_ipc_shm_size(const OakSharedMemoryRegion *)
+{
+ return 0;
+}
+
+int oakengine_ipc_shm_key(const OakSharedMemoryRegion *, char *buf,
+ int buf_size)
+{
+ if (buf && buf_size > 0) {
+ buf[0] = '\0';
+ }
+ return 0;
+}
+
+int oakengine_ipc_shm_error(const OakSharedMemoryRegion *, char *buf,
+ int buf_size)
+{
+ static const char k_msg[] = "oakengine_ipc test shim: IPC unavailable";
+ if (buf && buf_size > 0) {
+ std::strncpy(buf, k_msg, size_t(buf_size) - 1);
+ buf[buf_size - 1] = '\0';
+ }
+ return int(sizeof(k_msg) - 1);
+}
+
+int oakengine_ipc_shm_make_key(int64_t owner_pid, int worker_index, char *buf,
+ int buf_size)
+{
+ if (buf && buf_size > 0) {
+ std::snprintf(buf, size_t(buf_size), "olive-rw-%lld-%d",
+ (long long) owner_pid, worker_index);
+ }
+ return 0;
+}
+
+size_t oakengine_ipc_framepool_bytes_needed(uint32_t, size_t)
+{
+ return 0;
+}
+
+OakFrameSlotPool *oakengine_ipc_framepool_create(void *, uint32_t, size_t)
+{
+ return nullptr;
+}
+
+OakFrameSlotPool *oakengine_ipc_framepool_attach(void *)
+{
+ return nullptr;
+}
+
+OakFrameSlotPool *oakengine_ipc_framepool_copy(const OakFrameSlotPool *)
+{
+ return nullptr;
+}
+
+void oakengine_ipc_framepool_free(OakFrameSlotPool *) {}
+
+int oakengine_ipc_framepool_is_valid(const OakFrameSlotPool *)
+{
+ return 0;
+}
+
+uint32_t oakengine_ipc_framepool_slot_count(const OakFrameSlotPool *)
+{
+ return 0;
+}
+
+size_t oakengine_ipc_framepool_slot_data_bytes(const OakFrameSlotPool *)
+{
+ return 0;
+}
+
+int oakengine_ipc_framepool_acquire(OakFrameSlotPool *, uint32_t *)
+{
+ return 0;
+}
+
+void *oakengine_ipc_framepool_slot_data(OakFrameSlotPool *, uint32_t)
+{
+ return nullptr;
+}
+
+const void *oakengine_ipc_framepool_slot_data_const(const OakFrameSlotPool *,
+ uint32_t)
+{
+ return nullptr;
+}
+
+oak_frame_slot_meta *oakengine_ipc_framepool_meta(OakFrameSlotPool *, uint32_t)
+{
+ return nullptr;
+}
+
+const oak_frame_slot_meta *
+oakengine_ipc_framepool_meta_const(const OakFrameSlotPool *, uint32_t)
+{
+ return nullptr;
+}
+
+int oakengine_ipc_framepool_publish(OakFrameSlotPool *, uint32_t)
+{
+ return 0;
+}
+
+int oakengine_ipc_framepool_consume(OakFrameSlotPool *, uint32_t *)
+{
+ return 0;
+}
+
+int oakengine_ipc_framepool_release(OakFrameSlotPool *, uint32_t)
+{
+ return 0;
+}
+
+} // extern "C"
diff --git a/src/node/transition/binarystream.h b/src/node/transition/binarystream.h
new file mode 100644
index 000000000..dc3e961b2
--- /dev/null
+++ b/src/node/transition/binarystream.h
@@ -0,0 +1,299 @@
+#pragma once
+// Transitional: QDataStream replacement for the render disk-cache state and
+// index files. Mirrors QDataStream's default (big-endian) wire format for the
+// subset of types those files use, so existing cache files stay readable:
+// quint32/qint32/qint64 -> big-endian bytes
+// bool -> quint8
+// QString -> quint32 byte length + UTF-16BE code units
+// QUuid -> 16 raw bytes in RFC 4122 order
+// Reads past EOF yield zeroed values (QDataStream::ReadPastEnd semantics).
+
+#include
+#include
+#include
+
+namespace olive
+{
+
+namespace binarystream_detail
+{
+
+inline void append_utf16be(std::string &out, uint32_t cp)
+{
+ if (cp >= 0x10000) {
+ cp -= 0x10000;
+ uint16_t hi = 0xD800 + (cp >> 10);
+ uint16_t lo = 0xDC00 + (cp & 0x3FF);
+ out += char(hi >> 8);
+ out += char(hi & 0xFF);
+ out += char(lo >> 8);
+ out += char(lo & 0xFF);
+ } else {
+ out += char(cp >> 8);
+ out += char(cp & 0xFF);
+ }
+}
+
+inline std::string utf8_to_utf16be(const std::string &s)
+{
+ std::string out;
+ size_t i = 0;
+ while (i < s.size()) {
+ unsigned char c = s[i];
+ uint32_t cp;
+ size_t extra;
+ if (c < 0x80) {
+ cp = c;
+ extra = 0;
+ } else if ((c & 0xE0) == 0xC0) {
+ cp = c & 0x1F;
+ extra = 1;
+ } else if ((c & 0xF0) == 0xE0) {
+ cp = c & 0x0F;
+ extra = 2;
+ } else if ((c & 0xF8) == 0xF0) {
+ cp = c & 0x07;
+ extra = 3;
+ } else {
+ // Invalid byte, emit U+FFFD like Qt's UTF-8 handling
+ cp = 0xFFFD;
+ extra = 0;
+ }
+ for (size_t j = 0; j < extra; j++) {
+ if (i + 1 < s.size()) {
+ cp = (cp << 6) | (uint8_t(s[i + 1]) & 0x3F);
+ i++;
+ }
+ }
+ i++;
+ append_utf16be(out, cp);
+ }
+ return out;
+}
+
+inline void append_utf8(std::string &out, uint32_t cp)
+{
+ if (cp < 0x80) {
+ out += char(cp);
+ } else if (cp < 0x800) {
+ out += char(0xC0 | (cp >> 6));
+ out += char(0x80 | (cp & 0x3F));
+ } else if (cp < 0x10000) {
+ out += char(0xE0 | (cp >> 12));
+ out += char(0x80 | ((cp >> 6) & 0x3F));
+ out += char(0x80 | (cp & 0x3F));
+ } else {
+ out += char(0xF0 | (cp >> 18));
+ out += char(0x80 | ((cp >> 12) & 0x3F));
+ out += char(0x80 | ((cp >> 6) & 0x3F));
+ out += char(0x80 | (cp & 0x3F));
+ }
+}
+
+inline std::string utf16be_to_utf8(const std::string &be)
+{
+ std::string out;
+ size_t units = be.size() / 2;
+ for (size_t i = 0; i < units; i++) {
+ uint16_t u = (uint8_t(be[i * 2]) << 8) | uint8_t(be[i * 2 + 1]);
+ uint32_t cp = u;
+ if (u >= 0xD800 && u < 0xDC00 && i + 1 < units) {
+ uint16_t lo = (uint8_t(be[i * 2 + 2]) << 8) |
+ uint8_t(be[i * 2 + 3]);
+ if (lo >= 0xDC00 && lo < 0xE000) {
+ cp = 0x10000 + ((uint32_t(u) - 0xD800) << 10) + (lo - 0xDC00);
+ i++;
+ }
+ }
+ append_utf8(out, cp);
+ }
+ return out;
+}
+
+inline int hex_digit(char c)
+{
+ if (c >= '0' && c <= '9') {
+ return c - '0';
+ }
+ if (c >= 'a' && c <= 'f') {
+ return c - 'a' + 10;
+ }
+ if (c >= 'A' && c <= 'F') {
+ return c - 'A' + 10;
+ }
+ return 0;
+}
+
+} // namespace binarystream_detail
+
+class BinaryStreamWriter {
+public:
+ explicit BinaryStreamWriter(std::FILE *f)
+ : f_(f)
+ {
+ }
+
+ BinaryStreamWriter &operator<<(uint32_t v)
+ {
+ write_be(v, 4);
+ return *this;
+ }
+
+ BinaryStreamWriter &operator<<(int32_t v)
+ {
+ write_be(uint32_t(v), 4);
+ return *this;
+ }
+
+ BinaryStreamWriter &operator<<(int64_t v)
+ {
+ write_be(uint64_t(v), 8);
+ return *this;
+ }
+
+ BinaryStreamWriter &operator<<(bool v)
+ {
+ write_be(v ? 1 : 0, 1);
+ return *this;
+ }
+
+ // QString: quint32 byte length + UTF-16BE data
+ BinaryStreamWriter &operator<<(const std::string &v)
+ {
+ std::string be = binarystream_detail::utf8_to_utf16be(v);
+ write_be(uint32_t(be.size()), 4);
+ if (!be.empty()) {
+ std::fwrite(be.data(), 1, be.size(), f_);
+ }
+ return *this;
+ }
+
+ // QUuid: 16 raw bytes in RFC 4122 order. Accepts the canonical text form
+ // "{8-4-4-4-12}" (braces optional), which is what cache uuids are stored as.
+ void write_uuid_text(const std::string &uuid_text)
+ {
+ uint8_t bytes[16] = { 0 };
+ int nibble = 0;
+ for (char c : uuid_text) {
+ if (c == '{' || c == '}' || c == '-') {
+ continue;
+ }
+ if (nibble >= 32) {
+ break;
+ }
+ int d = binarystream_detail::hex_digit(c);
+ if (nibble % 2 == 0) {
+ bytes[nibble / 2] = uint8_t(d << 4);
+ } else {
+ bytes[nibble / 2] |= uint8_t(d);
+ }
+ nibble++;
+ }
+ std::fwrite(bytes, 1, 16, f_);
+ }
+
+private:
+ void write_be(uint64_t v, int bytes)
+ {
+ uint8_t b[8];
+ for (int i = 0; i < bytes; i++) {
+ b[i] = uint8_t(v >> ((bytes - 1 - i) * 8));
+ }
+ std::fwrite(b, 1, bytes, f_);
+ }
+
+ std::FILE *f_;
+};
+
+class BinaryStreamReader {
+public:
+ explicit BinaryStreamReader(std::FILE *f)
+ : f_(f)
+ {
+ }
+
+ BinaryStreamReader &operator>>(uint32_t &v)
+ {
+ v = uint32_t(read_be(4));
+ return *this;
+ }
+
+ BinaryStreamReader &operator>>(int32_t &v)
+ {
+ v = int32_t(read_be(4));
+ return *this;
+ }
+
+ BinaryStreamReader &operator>>(int64_t &v)
+ {
+ v = int64_t(read_be(8));
+ return *this;
+ }
+
+ BinaryStreamReader &operator>>(bool &v)
+ {
+ v = read_be(1) != 0;
+ return *this;
+ }
+
+ BinaryStreamReader &operator>>(std::string &v)
+ {
+ uint32_t len = uint32_t(read_be(4));
+ v.clear();
+ if (len == 0xFFFFFFFF || len == 0) {
+ return *this;
+ }
+ std::string be(len, '\0');
+ size_t got = std::fread(&be[0], 1, len, f_);
+ be.resize(got);
+ v = binarystream_detail::utf16be_to_utf8(be);
+ return *this;
+ }
+
+ // QUuid: 16 raw bytes -> canonical "{8-4-4-4-12}" lowercase text
+ std::string read_uuid_text()
+ {
+ uint8_t bytes[16] = { 0 };
+ std::fread(bytes, 1, 16, f_);
+
+ 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;
+ }
+
+ bool at_end() const
+ {
+ int c = std::fgetc(f_);
+ if (c == EOF) {
+ return true;
+ }
+ std::ungetc(c, f_);
+ return false;
+ }
+
+private:
+ uint64_t read_be(int bytes)
+ {
+ uint8_t b[8] = { 0 };
+ std::fread(b, 1, bytes, f_);
+ uint64_t v = 0;
+ for (int i = 0; i < bytes; i++) {
+ v = (v << 8) | b[i];
+ }
+ return v;
+ }
+
+ std::FILE *f_;
+};
+
+}
diff --git a/src/node/transition/common/avframeptr.h b/src/node/transition/common/avframeptr.h
new file mode 100644
index 000000000..eccb31423
--- /dev/null
+++ b/src/node/transition/common/avframeptr.h
@@ -0,0 +1,140 @@
+// Transitional copy of engine/common/avframeptr.h(本身已无 Qt,待下沉 oakcommon)。只增不删。
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAK_AVFRAMEPTR_H
+#define OAK_AVFRAMEPTR_H
+
+#include
+
+#include
+
+#include
+
+namespace olive
+{
+
+/**
+ * @brief C++ adapter around the ffmpeg_bridge frame handle
+ *
+ * Mirrors the AVFrame field access the codebase used to perform directly,
+ * but every operation goes through the pure C bridge API so the editor
+ * never touches FFmpeg itself. The underlying frame object always lives
+ * inside the bridge library.
+ */
+class AVFrame {
+public:
+ AVFrame() :
+ handle_(fb_frame_alloc())
+ {
+ }
+
+ explicit AVFrame(FBFrame *handle) :
+ handle_(handle)
+ {
+ }
+
+ ~AVFrame()
+ {
+ if (handle_) {
+ fb_frame_free(&handle_);
+ }
+ }
+
+ AVFrame(const AVFrame &) = delete;
+ AVFrame &operator=(const AVFrame &) = delete;
+
+ FBFrame *handle() const { return handle_; }
+
+ int width() const { return fb_frame_get_width(handle_); }
+ void set_width(int w) { fb_frame_set_width(handle_, w); }
+ int height() const { return fb_frame_get_height(handle_); }
+ void set_height(int h) { fb_frame_set_height(handle_, h); }
+ int format() const { return fb_frame_get_format(handle_); }
+ void set_format(int f) { fb_frame_set_format(handle_, f); }
+ int64_t pts() const { return fb_frame_get_pts(handle_); }
+ void set_pts(int64_t p) { fb_frame_set_pts(handle_, p); }
+ int64_t best_effort_timestamp() const
+ {
+ return fb_frame_get_best_effort_timestamp(handle_);
+ }
+ int nb_samples() const { return fb_frame_get_nb_samples(handle_); }
+ void set_nb_samples(int n) { fb_frame_set_nb_samples(handle_, n); }
+ int sample_rate() const { return fb_frame_get_sample_rate(handle_); }
+ void set_sample_rate(int r) { fb_frame_set_sample_rate(handle_, r); }
+ int color_range() const { return fb_frame_get_color_range(handle_); }
+ void set_color_range(int r) { fb_frame_set_color_range(handle_, r); }
+ int colorspace() const { return fb_frame_get_colorspace(handle_); }
+ void set_colorspace(int cs) { fb_frame_set_colorspace(handle_, cs); }
+ uint64_t channel_layout_mask() const
+ {
+ return fb_frame_get_channel_layout_mask(handle_);
+ }
+ void set_channel_layout_mask(uint64_t m)
+ {
+ fb_frame_set_channel_layout_mask(handle_, m);
+ }
+
+ bool is_hw() const { return fb_frame_is_hw(handle_) != 0; }
+ int hw_transfer_data(const AVFrame *src)
+ {
+ return fb_frame_hw_transfer_data(handle_, src->handle_);
+ }
+ int get_buffer(int align) { return fb_frame_get_buffer(handle_, align); }
+ int make_writable() { return fb_frame_make_writable(handle_); }
+
+ uint8_t *data(int plane) { return fb_frame_get_data(handle_, plane); }
+ const uint8_t *data(int plane) const
+ {
+ return fb_frame_get_data_const(handle_, plane);
+ }
+ void set_data(int plane, uint8_t *d)
+ {
+ fb_frame_set_data(handle_, plane, d);
+ }
+ int linesize(int plane) const
+ {
+ return fb_frame_get_linesize(handle_, plane);
+ }
+ void set_linesize(int plane, int l)
+ {
+ fb_frame_set_linesize(handle_, plane, l);
+ }
+
+private:
+ FBFrame *handle_;
+};
+
+using AVFramePtr = std::shared_ptr;
+
+inline AVFramePtr create_av_frame_ptr(FBFrame *f)
+{
+ return std::make_shared(f);
+}
+
+inline AVFramePtr create_av_frame_ptr()
+{
+ return std::make_shared();
+}
+
+}
+
+#endif // OAK_AVFRAMEPTR_H
diff --git a/src/node/transition/common/jobtime.h b/src/node/transition/common/jobtime.h
new file mode 100644
index 000000000..ff0069549
--- /dev/null
+++ b/src/node/transition/common/jobtime.h
@@ -0,0 +1,34 @@
+#pragma once
+// Transitional: engine/common/jobtime.h de-Qt target form. Original kept a
+// process-global mutex-protected counter; the atomic below preserves the same
+// monotonic-unique semantics. oakcommon has no jobtime.h yet (M-common folds
+// it in); renderjobtracker.h includes "common/jobtime.h".
+#include
+#include
+
+namespace olive {
+
+class JobTime {
+public:
+ JobTime() { acquire(); }
+
+ void acquire()
+ {
+ static std::atomic index{ 0 };
+ value_ = index.fetch_add(1, std::memory_order_relaxed);
+ }
+
+ uint64_t value() const { return value_; }
+
+ bool operator==(const JobTime &rhs) const { return value_ == rhs.value_; }
+ bool operator!=(const JobTime &rhs) const { return value_ != rhs.value_; }
+ bool operator<(const JobTime &rhs) const { return value_ < rhs.value_; }
+ bool operator>(const JobTime &rhs) const { return value_ > rhs.value_; }
+ bool operator<=(const JobTime &rhs) const { return value_ <= rhs.value_; }
+ bool operator>=(const JobTime &rhs) const { return value_ >= rhs.value_; }
+
+private:
+ uint64_t value_;
+};
+
+}
diff --git a/src/node/transition/render/audioplaybackcache.h b/src/node/transition/render/audioplaybackcache.h
index 2743a9bee..a5f17d501 100644
--- a/src/node/transition/render/audioplaybackcache.h
+++ b/src/node/transition/render/audioplaybackcache.h
@@ -1,14 +1,4 @@
#pragma once
-// Syntax-check stub only (not in repo).
-#include
-#include "olive/core/util/timerange.h"
-#include "olive/core/render/audioparams.h"
-namespace olive { using core::AudioParams; }
-#include "render/playbackcache.h"
-namespace olive {
-class Node;
-class AudioPlaybackCache : public PlaybackCache {
-public:
- template explicit AudioPlaybackCache(T *) : PlaybackCache(this) {}
-};
-}
+// Bridge: routed to the real de-Qt oakrender header (src/render/src/audioplaybackcache.h,
+// M7) so oaknode and oakrender see the same class definition.
+#include "../../../render/src/audioplaybackcache.h"
diff --git a/src/node/transition/render/audiowaveformcache.h b/src/node/transition/render/audiowaveformcache.h
index 33d9fa62c..2b57e1511 100644
--- a/src/node/transition/render/audiowaveformcache.h
+++ b/src/node/transition/render/audiowaveformcache.h
@@ -1,14 +1,4 @@
#pragma once
-// Syntax-check stub only (not in repo).
-#include
-#include
-#include "olive/core/util/timerange.h"
-#include "render/playbackcache.h"
-namespace olive {
-class Node;
-class AudioWaveformCache : public PlaybackCache {
-public:
- template explicit AudioWaveformCache(T *) : PlaybackCache(this) {}
- void set_saving_enabled(bool) {}
-};
-}
+// Bridge: routed to the real de-Qt oakrender header (src/render/src/audiowaveformcache.h,
+// M7) so oaknode and oakrender see the same class definition.
+#include "../../../render/src/audiowaveformcache.h"
diff --git a/src/node/transition/render/cancelatom.h b/src/node/transition/render/cancelatom.h
index bfb592a31..bd1b9b184 100644
--- a/src/node/transition/render/cancelatom.h
+++ b/src/node/transition/render/cancelatom.h
@@ -1,8 +1,4 @@
#pragma once
-namespace olive {
-class CancelAtom {
-public:
- bool is_cancelled() const { return false; }
- bool heard_cancel() const { return false; }
-};
-}
+// Bridge: routed to the real de-Qt oakrender header (src/render/src/cancelatom.h,
+// M7) so oaknode and oakrender see the same class definition.
+#include "../../../render/src/cancelatom.h"
diff --git a/src/node/transition/render/colorprocessor.h b/src/node/transition/render/colorprocessor.h
index 0d8238a95..25508ff9f 100644
--- a/src/node/transition/render/colorprocessor.h
+++ b/src/node/transition/render/colorprocessor.h
@@ -1,24 +1,4 @@
#pragma once
-#include
-#include
-#include "ocioutils.h"
-#include "colortransform.h"
-namespace olive {
-class ColorManager;
-class ColorProcessor;
-using ColorProcessorPtr = std::shared_ptr;
-class ColorProcessor {
-public:
- enum Direction { k_normal, k_inverse };
- static ColorProcessorPtr create(ColorManager *, const std::string &,
- const ColorTransform &,
- Direction = k_normal)
- {
- return nullptr;
- }
- static ColorProcessorPtr create(ocio::ConstProcessorRcPtr)
- {
- return nullptr;
- }
-};
-}
+// Bridge: routed to the real de-Qt oakrender header (src/render/src/colorprocessor.h,
+// M7) so oaknode and oakrender see the same class definition.
+#include "../../../render/src/colorprocessor.h"
diff --git a/src/node/transition/render/diskmanager.h b/src/node/transition/render/diskmanager.h
index 40c8ab52e..c5c05f373 100644
--- a/src/node/transition/render/diskmanager.h
+++ b/src/node/transition/render/diskmanager.h
@@ -1,9 +1,4 @@
#pragma once
-#include
-namespace olive {
-class DiskManager {
-public:
- static DiskManager *instance() { static DiskManager d; return &d; }
- std::string get_default_cache_path() const { return std::string(); }
-};
-}
+// Bridge: routed to the real de-Qt oakrender header (src/render/src/diskmanager.h,
+// M7) so oaknode and oakrender see the same class definition.
+#include "../../../render/src/diskmanager.h"
diff --git a/src/node/transition/render/framehashcache.h b/src/node/transition/render/framehashcache.h
index e036de065..059944717 100644
--- a/src/node/transition/render/framehashcache.h
+++ b/src/node/transition/render/framehashcache.h
@@ -1,32 +1,4 @@
#pragma once
-// Transitional stub for engine/render/framehashcache.h (still Qt-based).
-// Only the surface oaknode uses. M7 replaces this with the real oakrender
-// boundary.
-#include
-#include
-#include "olive/core/util/rational.h"
-#include "olive/core/util/timerange.h"
-#include "render/playbackcache.h"
-namespace olive {
-class Node;
-class FrameHashCache : public PlaybackCache {
-public:
- template explicit FrameHashCache(T *) : PlaybackCache(this) {}
- const core::Rational &get_timebase() const { return timebase_; }
- void set_timebase(const core::Rational &t) { timebase_ = t; }
- std::mutex &mutex() { return mutex_; }
- void load_state() {}
- std::string get_valid_cache_filename(const core::Rational &) const
- {
- return std::string();
- }
-
-private:
- core::Rational timebase_;
- std::mutex mutex_;
-};
-class ThumbnailCache : public FrameHashCache {
-public:
- template explicit ThumbnailCache(T *) : FrameHashCache(this) {}
-};
-}
+// Bridge: routed to the real de-Qt oakrender header (src/render/src/framehashcache.h,
+// M7) so oaknode and oakrender see the same class definition.
+#include "../../../render/src/framehashcache.h"
diff --git a/src/node/transition/render/job/acceleratedjob.h b/src/node/transition/render/job/acceleratedjob.h
index 1f8163f20..d32be0f9d 100644
--- a/src/node/transition/render/job/acceleratedjob.h
+++ b/src/node/transition/render/job/acceleratedjob.h
@@ -1,23 +1,4 @@
#pragma once
-#include "param.h"
-#include "value.h"
-namespace olive {
-class AcceleratedJob {
-public:
- virtual ~AcceleratedJob() = default;
- virtual void insert(const std::string &input, const NodeValueRow &row)
- {
- (void) input; (void) row;
- }
- virtual void insert(const std::string &input, const NodeValue &value)
- {
- (void) input; (void) value;
- }
- virtual void insert(const NodeValueRow &row)
- {
- (void) row;
- }
- virtual const NodeValueRow &get_values() const { static NodeValueRow r; return r; }
- virtual NodeValueRow &get_values() { static NodeValueRow r; return r; }
-};
-}
+// Bridge: routed to the real de-Qt oakrender header (src/render/src/job/acceleratedjob.h,
+// M7) so oaknode and oakrender see the same class definition.
+#include "../../../../render/src/job/acceleratedjob.h"
diff --git a/src/node/transition/render/job/cachejob.h b/src/node/transition/render/job/cachejob.h
index da02cd13a..5da31b590 100644
--- a/src/node/transition/render/job/cachejob.h
+++ b/src/node/transition/render/job/cachejob.h
@@ -1,10 +1,4 @@
#pragma once
-#include
-#include "render/job/acceleratedjob.h"
-namespace olive {
-class CacheJob : public AcceleratedJob {
-public:
- CacheJob(const std::string &, const NodeValue &) {}
- TexturePtr get_fallback() const { return nullptr; }
-};
-}
+// Bridge: routed to the real de-Qt oakrender header (src/render/src/job/cachejob.h,
+// M7) so oaknode and oakrender see the same class definition.
+#include "../../../../render/src/job/cachejob.h"
diff --git a/src/node/transition/render/job/colortransformjob.h b/src/node/transition/render/job/colortransformjob.h
index 7094c4b7f..259af680b 100644
--- a/src/node/transition/render/job/colortransformjob.h
+++ b/src/node/transition/render/job/colortransformjob.h
@@ -1,16 +1,4 @@
#pragma once
-#include "render/job/acceleratedjob.h"
-#include "render/colorprocessor.h"
-namespace olive {
-class Node;
-class ColorTransformJob : public AcceleratedJob {
-public:
- ColorTransformJob() = default;
- ColorTransformJob(const NodeValueRow &) {}
- NodeValue get_input_texture() const { return NodeValue(); }
- void set_input_texture(const NodeValue &) {}
- void set_color_processor(ColorProcessorPtr) {}
- void set_needs_custom_shader(const Node *) {}
- void set_function_name(const std::string &) {}
-};
-}
+// Bridge: routed to the real de-Qt oakrender header (src/render/src/job/colortransformjob.h,
+// M7) so oaknode and oakrender see the same class definition.
+#include "../../../../render/src/job/colortransformjob.h"
diff --git a/src/node/transition/render/job/footagejob.h b/src/node/transition/render/job/footagejob.h
index c555b7777..a3ba2dae6 100644
--- a/src/node/transition/render/job/footagejob.h
+++ b/src/node/transition/render/job/footagejob.h
@@ -1,27 +1,4 @@
#pragma once
-#include
-#include "render/job/acceleratedjob.h"
-#include "videoparams.h"
-#include "olive/core/render/audioparams.h"
-namespace olive { using core::AudioParams; }
-#include "loopmode.h"
-#include "olive/core/util/timerange.h"
-#include "output/track/track.h"
-namespace olive {
-class FootageJob : public AcceleratedJob {
-public:
- FootageJob() {}
- FootageJob(const core::TimeRange &, const std::string &,
- const std::string &, Track::Type, const core::Rational &,
- LoopMode) {}
- void set_proxy(const std::string &, const std::string &, int) {}
- void set_video_params(const VideoParams &) {}
- void set_audio_params(const AudioParams &) {}
- void set_cache_path(const std::string &) {}
- core::TimeRange time() const { return core::TimeRange(); }
- int loop_mode_as_int() const { return 0; }
- LoopMode loop_mode() const { return LoopMode::k_loop_mode_off; }
- core::Rational length() const { return core::Rational(); }
- const VideoParams &video_params() const { static VideoParams p; return p; }
-};
-}
+// Bridge: routed to the real de-Qt oakrender header (src/render/src/job/footagejob.h,
+// M7) so oaknode and oakrender see the same class definition.
+#include "../../../../render/src/job/footagejob.h"
diff --git a/src/node/transition/render/job/generatejob.h b/src/node/transition/render/job/generatejob.h
index 4d0772519..88b9b71ed 100644
--- a/src/node/transition/render/job/generatejob.h
+++ b/src/node/transition/render/job/generatejob.h
@@ -1,11 +1,4 @@
#pragma once
-#include "render/job/acceleratedjob.h"
-#include "value.h"
-namespace olive {
-class GenerateJob : public AcceleratedJob {
-public:
- GenerateJob() {}
- explicit GenerateJob(const NodeValueRow &) {}
- NodeValue get(const std::string &) const { return NodeValue(); }
-};
-}
+// Bridge: routed to the real de-Qt oakrender header (src/render/src/job/generatejob.h,
+// M7) so oaknode and oakrender see the same class definition.
+#include "../../../../render/src/job/generatejob.h"
diff --git a/src/node/transition/render/job/pluginjob.h b/src/node/transition/render/job/pluginjob.h
index 51b1b2972..d566ef774 100644
--- a/src/node/transition/render/job/pluginjob.h
+++ b/src/node/transition/render/job/pluginjob.h
@@ -1,16 +1,4 @@
#pragma once
-#include "render/job/acceleratedjob.h"
-#include "olive/core/util/rational.h"
-namespace olive {
-class Node;
-namespace plugin {
-class PluginJob : public AcceleratedJob {
-public:
- template
- PluginJob(InstanceT *, const Node *, const NodeValueRow &,
- const core::Rational &)
- {
- }
-};
-}
-}
+// Bridge: routed to the real de-Qt oakrender header (src/render/src/job/pluginjob.h,
+// M7) so oaknode and oakrender see the same class definition.
+#include "../../../../render/src/job/pluginjob.h"
diff --git a/src/node/transition/render/job/samplejob.h b/src/node/transition/render/job/samplejob.h
index 5a15d4bb9..031723d9d 100644
--- a/src/node/transition/render/job/samplejob.h
+++ b/src/node/transition/render/job/samplejob.h
@@ -1,31 +1,4 @@
#pragma once
-#include
-#include "value.h"
-#include "render/samplebuffer.h"
-#include "olive/core/util/timerange.h"
-namespace olive {
-class SampleJob {
-public:
- SampleJob() = default;
- SampleJob(const core::TimeRange &time, const NodeValue &value)
- {
- (void) time; (void) value;
- }
- SampleJob(const core::TimeRange &time, const std::string &from,
- const NodeValueRow &values)
- {
- (void) time; (void) from; (void) values;
- }
- void insert(const std::string &input, const NodeValueRow &row)
- {
- (void) input; (void) row;
- }
- void insert(const std::string &input, const NodeValue &value)
- {
- (void) input; (void) value;
- }
- bool operator==(const SampleJob &) const { return true; }
- const SampleBuffer &samples() const { static SampleBuffer b; return b; }
- core::TimeRange time() const { return core::TimeRange(); }
-};
-}
+// Bridge: routed to the real de-Qt oakrender header (src/render/src/job/samplejob.h,
+// M7) so oaknode and oakrender see the same class definition.
+#include "../../../../render/src/job/samplejob.h"
diff --git a/src/node/transition/render/job/shaderjob.h b/src/node/transition/render/job/shaderjob.h
index 4fe6b4c67..8026e061f 100644
--- a/src/node/transition/render/job/shaderjob.h
+++ b/src/node/transition/render/job/shaderjob.h
@@ -1,25 +1,4 @@
#pragma once
-#include
-#include
-#include "render/job/acceleratedjob.h"
-namespace olive {
-class ShaderJob : public AcceleratedJob {
-public:
- ShaderJob() = default;
- ShaderJob(const NodeValueRow &row) { (void) row; }
- NodeValue get(const std::string &id) const { (void) id; return NodeValue(); }
- void set_shader_id(const std::string &id) { (void) id; }
- void set_iterations(int iterations, const std::string &iterative_input)
- {
- (void) iterations; (void) iterative_input;
- }
- void set_interpolation(const std::string &id, int mode)
- {
- (void) id; (void) mode;
- }
- void set_vertex_coordinates(const std::vector &coords)
- {
- (void) coords;
- }
-};
-}
+// Bridge: routed to the real de-Qt oakrender header (src/render/src/job/shaderjob.h,
+// M7) so oaknode and oakrender see the same class definition.
+#include "../../../../render/src/job/shaderjob.h"
diff --git a/src/node/transition/render/lutlibrary.h b/src/node/transition/render/lutlibrary.h
index b92a9ae1c..73f704cd0 100644
--- a/src/node/transition/render/lutlibrary.h
+++ b/src/node/transition/render/lutlibrary.h
@@ -1,15 +1,4 @@
#pragma once
-// Transitional stub for engine/render/lutlibrary.h (still Qt-based). Only the
-// surface oaknode uses. M7 replaces this with the real oakrender boundary.
-#include "variant.h"
-namespace olive {
-class LUTLibrary {
-public:
- static const StringList &supported_extensions()
- {
- static const StringList e;
- return e;
- }
- static bool is_supported_extension(const std::string &) { return false; }
-};
-}
+// Bridge: routed to the real de-Qt oakrender header (src/render/src/lutlibrary.h,
+// M7) so oaknode and oakrender see the same class definition.
+#include "../../../render/src/lutlibrary.h"
diff --git a/src/node/transition/render/playbackcache.h b/src/node/transition/render/playbackcache.h
index 9e3da13d5..2c93fef7d 100644
--- a/src/node/transition/render/playbackcache.h
+++ b/src/node/transition/render/playbackcache.h
@@ -1,45 +1,4 @@
#pragma once
-// Syntax-check stub only (not in repo). Minimal de-Qt shape of engine PlaybackCache.
-#include
-#include
-#include "olive/core/util/timerange.h"
-namespace olive {
-class Node;
-class ViewerOutput;
-class PlaybackCache {
-public:
- template explicit PlaybackCache(T *) {}
- virtual ~PlaybackCache() {}
- void set_uuid(const std::string &) {}
- std::string get_uuid() const { return {}; }
- core::TimeRangeList get_invalidated_ranges(core::TimeRange intersecting) const
- {
- (void) intersecting;
- return {};
- }
- core::TimeRangeList get_invalidated_ranges(const core::Rational &length) const
- {
- return get_invalidated_ranges(core::TimeRange(0, length));
- }
- void invalidate(const core::TimeRange &) {}
- void invalidate_all() {}
- void request(ViewerOutput *, const core::TimeRange &) {}
- virtual void set_passthrough(PlaybackCache *) {}
- class Passthrough : public core::TimeRange {
- public:
- Passthrough(const core::TimeRange &r, PlaybackCache *cache = nullptr)
- : core::TimeRange(r)
- , cache_(cache)
- {
- }
- PlaybackCache *cache() const { return cache_; }
-
- private:
- PlaybackCache *cache_;
- };
- const std::vector &get_passthroughs() const { return passthroughs_; }
-
-private:
- std::vector passthroughs_;
-};
-}
+// Bridge: routed to the real de-Qt oakrender header (src/render/src/playbackcache.h,
+// M7) so oaknode and oakrender see the same class definition.
+#include "../../../render/src/playbackcache.h"
diff --git a/src/node/transition/render/previewautocacher.h b/src/node/transition/render/previewautocacher.h
index 3f67a6205..c6c48acc3 100644
--- a/src/node/transition/render/previewautocacher.h
+++ b/src/node/transition/render/previewautocacher.h
@@ -1,12 +1,4 @@
#pragma once
-// Transitional stub for engine/render/previewautocacher.h (still Qt-based).
-// Only the surface oaknode uses. M7 replaces this with the real oakrender
-// boundary.
-namespace olive {
-class PreviewAutoCacher {
-public:
- void cancel_video_tasks(bool) {}
- void cancel_audio_tasks(bool) {}
- void cancel_tasks(bool) {}
-};
-}
+// Bridge: routed to the real de-Qt oakrender header (src/render/src/previewautocacher.h,
+// M7) so oaknode and oakrender see the same class definition.
+#include "../../../render/src/previewautocacher.h"
diff --git a/src/node/transition/render/rendermanager.h b/src/node/transition/render/rendermanager.h
index 7b1b28d2f..82f0f5629 100644
--- a/src/node/transition/render/rendermanager.h
+++ b/src/node/transition/render/rendermanager.h
@@ -1,12 +1,4 @@
#pragma once
-// Transitional stub for engine/render/rendermanager.h (still Qt-based). Only
-// the surface oaknode uses. M7 replaces this with the real oakrender
-// boundary.
-namespace olive {
-class PreviewAutoCacher;
-class RenderManager {
-public:
- static RenderManager *instance() { return nullptr; }
- PreviewAutoCacher *get_cacher() { return nullptr; }
-};
-}
+// Bridge: routed to the real de-Qt oakrender header (src/render/src/rendermanager.h,
+// M7) so oaknode and oakrender see the same class definition.
+#include "../../../render/src/rendermanager.h"
diff --git a/src/node/transition/render/shadercode.h b/src/node/transition/render/shadercode.h
index a2d71f2b4..ebeb68ecc 100644
--- a/src/node/transition/render/shadercode.h
+++ b/src/node/transition/render/shadercode.h
@@ -1,9 +1,4 @@
#pragma once
-#include
-#include "filefunctions.h"
-namespace olive {
-class ShaderCode {
-public:
- ShaderCode(const std::string &frag = std::string(), const std::string &vert = std::string()) {}
-};
-}
+// Bridge: routed to the real de-Qt oakrender header (src/render/src/shadercode.h,
+// M7) so oaknode and oakrender see the same class definition.
+#include "../../../render/src/shadercode.h"
diff --git a/src/node/transition/render/texture.h b/src/node/transition/render/texture.h
index 24e96392c..ec500a4c0 100644
--- a/src/node/transition/render/texture.h
+++ b/src/node/transition/render/texture.h
@@ -1,26 +1,7 @@
#pragma once
-#include
-#include "videoparams.h"
+// Bridge: routed to the real de-Qt oakrender header (src/render/src/texture.h,
+// M7) so oaknode and oakrender see the same class definition.
+// samplebuffer.h is included first because oaknode's value.h relied on the
+// old transition texture.h pulling it in transitively.
#include "render/samplebuffer.h"
-#include "olive/core/render/audioparams.h"
-namespace olive { using core::AudioParams; }
-namespace olive {
-class AcceleratedJob;
-class CacheJob;
-class Texture;
-using TexturePtr = std::shared_ptr;
-class Texture {
-public:
- enum Interpolation { k_nearest, k_linear, k_mipmapped_linear };
- Texture(const VideoParams &) {}
- int width() const { return 0; }
- int height() const { return 0; }
- int channel_count() const { return 0; }
- core::Rational pixel_aspect_ratio() const { return core::Rational(); }
- Vector2D virtual_resolution() const { return Vector2D(); }
- AcceleratedJob *job() { return nullptr; }
- template static TexturePtr job(const VideoParams &, const T &) { return nullptr; }
- const VideoParams ¶ms() const { static VideoParams p; return p; }
- template TexturePtr to_job(const T &) { return nullptr; }
-};
-}
+#include "../../../render/src/texture.h"
diff --git a/src/render/CMakeLists.txt b/src/render/CMakeLists.txt
new file mode 100644
index 000000000..f41570121
--- /dev/null
+++ b/src/render/CMakeLists.txt
@@ -0,0 +1,6 @@
+add_subdirectory(src)
+add_subdirectory(c_api)
+
+if(BUILD_TESTS)
+ add_subdirectory(tests)
+endif()
diff --git a/src/render/c_api/CMakeLists.txt b/src/render/c_api/CMakeLists.txt
new file mode 100644
index 000000000..489178911
--- /dev/null
+++ b/src/render/c_api/CMakeLists.txt
@@ -0,0 +1,6 @@
+target_sources(oakrender PRIVATE
+ renderer.cpp
+ cache.cpp
+ color.cpp
+ manager.cpp
+)
diff --git a/src/render/c_api/alivecount.h b/src/render/c_api/alivecount.h
new file mode 100644
index 000000000..9a46d683e
--- /dev/null
+++ b/src/render/c_api/alivecount.h
@@ -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 .
+
+***/
+
+#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
diff --git a/src/render/c_api/cache.cpp b/src/render/c_api/cache.cpp
new file mode 100644
index 000000000..4c8927adb
--- /dev/null
+++ b/src/render/c_api/cache.cpp
@@ -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 .
+
+***/
+
+#include "../../../include/render/cache.h"
+
+#include
+#include
+
+#include "alivecount.h"
+#include "internalhandles.h"
+
+#include "framehashcache.h"
+#include "playbackcache.h"
+
+namespace
+{
+
+std::atomic 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(c);
+}
+
+const OakRenderCacheImpl *impl(const OakRenderCache *c)
+{
+ return reinterpret_cast(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(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 (...) {
+ }
+}
diff --git a/src/render/c_api/color.cpp b/src/render/c_api/color.cpp
new file mode 100644
index 000000000..90e89f501
--- /dev/null
+++ b/src/render/c_api/color.cpp
@@ -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 .
+
+***/
+
+#include "../../../include/render/color.h"
+
+#include
+#include
+#include
+#include
+
+#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;
+ }
+}
diff --git a/src/render/c_api/internalhandles.h b/src/render/c_api/internalhandles.h
new file mode 100644
index 000000000..71911f17c
--- /dev/null
+++ b/src/render/c_api/internalhandles.h
@@ -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 .
+
+***/
+
+#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
+
+#include "codec/frame.h"
+#include "colorprocessor.h"
+#include "texture.h"
+
+struct OakRenderTexture {
+ olive::TexturePtr ptr;
+ std::atomic refcount{ 1 };
+};
+
+struct OakCodecFrame {
+ olive::FramePtr ptr;
+ std::atomic refcount{ 1 };
+};
+
+struct OakColorProcessor {
+ olive::ColorProcessorPtr ptr;
+};
+
+#endif //OAK_EDITOR_RENDER_INTERNALHANDLES_H
diff --git a/src/render/c_api/manager.cpp b/src/render/c_api/manager.cpp
new file mode 100644
index 000000000..1e7af5037
--- /dev/null
+++ b/src/render/c_api/manager.cpp
@@ -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 .
+
+***/
+
+#include "../../../include/render/manager.h"
+
+#include
+#include
+#include