Files
oak-editor/crates/oak-plugin/src/gl_bridge.rs
T
Mike-Solar 244d5e860f
CI / Build & test (Windows) (push) Failing after 7s
workspace: kebab-case crates, app under crates/oak-app, shared versions
All crates take the oak-* kebab-case naming (oak-audio, oak-codec,
oak-common, oak-core, oak-ffmpeg-link, oak-node, oak-otio, oak-plugin,
oak-render, oak-storage, oak-task, oak-timeline, oak-undo), with the
lib identifiers rewritten (oakrender:: -> oak_render::, oakcore_rs:: ->
oak_core::, ...) across all 226 referencing files.

The GUI application moves from the workspace root into
crates/oak-app/: src/, build.rs (paths fixed for the new location) and
tests/ travel with it, the root Cargo.toml becomes workspace-only
([workspace] + workspace.package + profiles), and the app package
inherits the workspace version. The screenshots example becomes a
standalone crate examples/simple_player/ with its own Cargo.toml.

Every crate now inherits the single workspace version
(version.workspace = true), and the workflows' crate paths and the
build docs follow the renames.

Validated with a clean cargo check --workspace.
2026-08-22 16:58:37 +08:00

997 lines
32 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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/>.
//! GL 互操作桥(方案 B 落地:离屏 GL 上下文 + 纹理/FBO + 回读)。
//!
//! ## 背景(阶段 6a spike 结论,已落地)
//!
//! OFX 的 `OpenGLRender` 扩展要求宿主把 clip 纹理以 **GL 纹理名**
//! `kOfxImageEffectPropOpenGLTextureIndex`)递给插件,插件直接画进
//! 输出纹理。oak 的渲染后端是 wgpumacOS 为 Metal),Metal 与 GL
//! 纹理名之间没有共享命名空间(方案 A 不可行,详见 0.1 版的 spike
//! 文档)。本模块实现**方案 B**:
//!
//! ```text
//! 宿主自建原生离屏 GL 上下文(macOS = CGL
//! → 为每帧建输出 GL 纹理 + FBO(插件 render 画进附着纹理)
//! → 插件经 GL suite 取到真实纹理名(OpenGLTextureIndex
//! → render 返回后 glReadPixels 回读为 CPU 帧
//! → 按目标帧格式转换(F32/RGBA8)装帧
//! ```
//!
//! 渲染跑在 oak-worker 进程内(M15 进程隔离),GL 上下文在 worker
//! 进程内创建即可;主进程无需 GL。
//!
//! ## 上下文生命周期与线程模型
//!
//! - **进程级共享单例**`OnceLock` 惰性创建(`imp::context`),一次
//! 创建、整个进程共用(CGL 上下文可在任意线程置为 current,但不能
//! 同时)。
//! - **串行化**[`acquire`] 取全局 `Mutex` 并把上下文置为当前线程
//! current;返回 [`GlGuard`]drop 时清 current 并放锁。OFX 插件
//! 渲染已由实例锁(`Instance::render_lock`)串行化,但不同实例可在
//! 不同线程渲染——GL 侧必须再串行一次,保证同一时刻只有一条线程
//! 持有 current 上下文。
//! - **current 的跨调用保持**ofxGPURender.h 要求宿主在 Render/
//! Begin/EndSequenceRender 期间让上下文保持 current(插件在 suite
//! 调用之间直接发 GL 命令)。因此 render 驱动**一次** acquire 整个
//! render action(含 suite 回调),[`GlGuard`] 跨整个 action 持有;
//! 本模块其余函数假定上下文已 current(guard 已持有),不再加锁。
//! - **同线程可重入**[`acquire`] 在同一线程已持外层 guard 时只递增
//! 线程局部嵌套深度(上下文已 current,不重取锁);最外层 guard
//! drop 时才清 current 放锁。interact draw 的宿主测试需要"一次
//! acquire 覆盖 FBO 装配 → 插件绘制 → 回读"而 `Interact::draw`
//! 内部又要自行 acquire(宿主侧每次 action 前都要保证 current)——
//! 嵌套 acquire 免去死锁。
//! - **清理**:进程退出时 `OnceLock` 里的 [`GlContext`] drop,销毁
//! CGL 上下文与像素格式。
//!
//! ## 平台
//!
//! - **macOS(真实实现)**Core OpenGL`OpenGL.framework`
//! `#[link(name = "OpenGL", kind = "framework")]`,无需新 crate)。
//! 像素格式:3.2 core profile、offline renderer 允许(无 GPU 的无头
//! 环境可回退软件渲染)、24 位颜色 + 8 位 alpha。离屏渲染(FBO
//! 无需 drawable,不 swap buffer。
//! - **Linux(预留 EGL**`cfg(target_os = "linux")` 走 stub,全部
//! 返回失败/`false`use_opengl 决策据此回退 CPU)。接线点:
//! `imp` 换成 EGL`eglGetDisplay/eglCreateContext` + GLES3),
//! FFI 同构,GL 命令一致。
//! - **Windows(预留 WGL**:同理,stub。接线点:`wglCreateContext`
//! + `wglMakeCurrent`FFI 同构。
//!
//! ## 回读格式转换(真实正确)
//!
//! 输出 GL 纹理的内部格式按目标帧格式选择(管线 F32 → `GL_RGBA32F`
//! U8 目标 → `GL_RGBA8`)。`glReadPixels` 按纹理类型回读(`GL_FLOAT`
//! 或 `GL_UNSIGNED_BYTE`),垂直翻转(GL 原点左下 vs 帧缓冲原点左上),
//! U8 按 `v/255.0` 归一化到 F32——与 CPU 路径输出格式(F32 RGBA
//! 一致,经 `write_output_frame` 装配。
use std::sync::Arc;
use crate::image::Image;
use crate::render::VideoParams;
// ---- 对外 API(平台无关签名;实现按平台 cfg 分派)----------------------
/// GL 上下文是否可用(离屏上下文可创建)。use_opengl 决策用它当
/// "目标纹理有有效 GL 名" 的门:桥能为目标帧建出真实 GL 纹理 ⟺
/// 上下文可用。惰性创建,幂等。
pub fn gl_available() -> bool {
imp::gl_available()
}
/// 取得进程级 GL 上下文并把本线程置为 current(全局串行)。
/// 返回的 guard 在 drop 时清 current 并放锁。失败(无 GL / 上下文
/// 创建失败)→ 调用方回退 CPU。
pub fn acquire() -> crate::error::Result<GlGuard> {
imp::acquire()
}
/// 创建输出 GL 纹理(尺寸 = 目标帧,内部格式按 `params.format`
/// F32 → RGBA32FU8 → RGBA8),返回真实 GL 纹理名。
///
/// # Safety
///
/// 要求 GL 上下文已 current[`GlGuard`] 持有期间调用)。
pub fn create_output_texture(w: i32, h: i32, params: &VideoParams) -> crate::error::Result<i32> {
imp::create_output_texture(w, h, params)
}
/// 创建输入 GL 纹理(RGBA32F)并把 F32 像素数据上传,返回真实 GL
/// 纹理名。`pixels` 必须为 `w*h*4*4` 字节(紧凑 RGBA F32,行优先)。
///
/// # Safety
///
/// 要求 GL 上下文已 current[`GlGuard`] 持有期间调用)。
pub fn create_input_texture(w: i32, h: i32, pixels: &[u8]) -> crate::error::Result<i32> {
imp::create_input_texture(w, h, pixels)
}
/// 删除 GL 纹理。
///
/// # Safety
///
/// 要求 GL 上下文已 current。
pub fn delete_gl_texture(name: i32) {
imp::delete_gl_texture(name);
}
/// 创建 FBO 并把 `tex` 挂为颜色附件(绑定并校验完整性)。
/// 返回 FBO 名(当前已绑定)。失败时已清理。
///
/// # Safety
///
/// 要求 GL 上下文已 current。
pub fn create_fbo(tex: i32, w: i32, h: i32) -> crate::error::Result<i32> {
imp::create_fbo(tex, w, h)
}
/// 绑定 FBO(0 = 系统帧缓冲)。
///
/// # Safety
///
/// 要求 GL 上下文已 current。
pub fn bind_fbo(fbo: i32) {
imp::bind_fbo(fbo);
}
/// 删除 FBO。
///
/// # Safety
///
/// 要求 GL 上下文已 current。
pub fn delete_fbo(fbo: i32) {
imp::delete_fbo(fbo);
}
/// 设置视口(像素坐标)。
///
/// # Safety
///
/// 要求 GL 上下文已 current。
pub fn set_viewport(w: i32, h: i32) {
imp::set_viewport(w, h);
}
/// 回读当前绑定 FBO 并装配成 F32 RGBA 图像(垂直翻转、U8 归一化)。
/// 返回的 Image 与 CPU 路径输出格式一致,可直接交
/// [`crate::render_driver::write_output_frame`]。
///
/// # Safety
///
/// 要求 GL 上下文已 current、目标 FBO 仍绑定。
pub fn read_pixels_to_image(w: i32, h: i32, params: &VideoParams) -> crate::error::Result<Arc<Image>> {
imp::read_pixels_to_image(w, h, params)
}
/// 设置 clear 颜色(FBO 测试/失败紫屏用)。
///
/// # Safety
///
/// 要求 GL 上下文已 current。
pub fn gl_clear_color(r: f32, g: f32, b: f32, a: f32) {
imp::gl_clear_color(r, g, b, a);
}
/// 清除颜色缓冲(FBO 测试用;插件正常路径自行发 GL 命令)。
///
/// # Safety
///
/// 要求 GL 上下文已 current。
pub fn gl_clear() {
imp::gl_clear();
}
/// 当前 GL 实现的版本串(真实上下文证据;测试用)。
pub fn gl_version_string() -> Option<String> {
imp::gl_version_string()
}
/// GL 上下文句柄(持有全局锁 + 上下文 currentdrop 时清 current 放锁)。
pub struct GlGuard {
_inner: imp::GuardInner,
}
/// 当前线程是否持有 GL current(嵌套 guard 内也算;[`acquire`] 成功
/// 后为 true)。无 GL 的调用方(Draw suite 在 draw action 外被调)据此
/// 不触 GL 命令——macOS 无 current 上下文时调 GL 是未定义行为。
pub fn is_current() -> bool {
imp::is_current()
}
// ---- 平台实现 ----------------------------------------------------------
#[cfg(target_os = "macos")]
mod imp {
use super::*;
use crate::image::{BitDepth, Components};
use std::ffi::{c_char, c_void};
use std::sync::{Arc, Mutex, OnceLock};
// ---- OpenGL 常量(GL 规范值,非 OFX 宏)----
const GL_TEXTURE_2D: u32 = 0x0DE1;
const GL_RGBA: u32 = 0x1908;
const GL_UNSIGNED_BYTE: u32 = 0x1401;
const GL_FLOAT: u32 = 0x1406;
const GL_RGBA32F: u32 = 0x8814;
const GL_RGBA8: u32 = 0x8058;
const GL_TEXTURE_MIN_FILTER: u32 = 0x2801;
const GL_TEXTURE_MAG_FILTER: u32 = 0x2800;
const GL_NEAREST: u32 = 0x2600;
const GL_TEXTURE_WRAP_S: u32 = 0x2802;
const GL_TEXTURE_WRAP_T: u32 = 0x2803;
const GL_CLAMP_TO_EDGE: u32 = 0x812F;
const GL_FRAMEBUFFER: u32 = 0x8D40;
const GL_COLOR_ATTACHMENT0: u32 = 0x8CE0;
const GL_FRAMEBUFFER_COMPLETE: u32 = 0x8CD5;
const GL_COLOR_BUFFER_BIT: u32 = 0x4000;
const GL_PACK_ALIGNMENT: u32 = 0x0D05;
const GL_UNPACK_ALIGNMENT: u32 = 0x0CF5;
const GL_VERSION: u32 = 0x1F02;
const GL_NO_ERROR: u32 = 0;
// CGL 像素格式属性(CGLPixelFormatAttributeCGL.h)。
const K_CGL_PFA_OPENGL_PROFILE: i32 = 99;
const K_CGLOGLP_VERSION_3_2_CORE: i32 = 0x3200;
const K_CGL_PFA_ALLOW_OFFLINE_RENDERERS: i32 = 96;
const K_CGL_PFA_COLOR_SIZE: i32 = 8;
const K_CGL_PFA_ALPHA_SIZE: i32 = 11;
/// macOSCGL + OpenGL 的 FFI 声明(直接链接 OpenGL.framework)。
///
/// # Safety
///
/// 全部是系统 framework 导出函数;指针参数语义见各函数注释。
/// CGL* 类型用 `*mut c_void` 表示不透明对象指针(CGLContextObj /
/// CGLPixelFormatObj)。
#[link(name = "OpenGL", kind = "framework")]
unsafe extern "C" {
/// 按属性列表选像素格式(成功返回 kCGLNoError=0`npix` 是
/// CGL 要求返回的像素格式计数,本桥只取格式对象,可丢弃——
/// 但实参必须给足,否则是未定义行为)。
fn CGLChoosePixelFormat(attrs: *const i32, pix: *mut *mut c_void, npix: *mut i32) -> i32;
/// 建上下文(share = NULL 无共享组)。返回 kCGLNoError=0。
fn CGLCreateContext(pix: *mut c_void, share: *mut c_void, ctx: *mut *mut c_void) -> i32;
fn CGLDestroyPixelFormat(pix: *mut c_void);
fn CGLDestroyContext(ctx: *mut c_void);
/// 置为当前线程的 current 上下文(NULL = 清)。返回 kCGLNoError=0。
fn CGLSetCurrentContext(ctx: *mut c_void) -> i32;
fn glGenTextures(n: i32, textures: *mut u32);
fn glDeleteTextures(n: i32, textures: *const u32);
fn glBindTexture(target: u32, texture: u32);
fn glTexParameteri(target: u32, pname: u32, param: i32);
fn glPixelStorei(pname: u32, param: i32);
/// 分配/上传 2D 纹理图像。`pixels` 为 NULL 时只分配。
fn glTexImage2D(
target: u32,
level: i32,
internalformat: i32,
width: i32,
height: i32,
border: i32,
format: u32,
type_: u32,
pixels: *const c_void,
);
fn glGenFramebuffers(n: i32, framebuffers: *mut u32);
fn glDeleteFramebuffers(n: i32, framebuffers: *const u32);
fn glBindFramebuffer(target: u32, framebuffer: u32);
fn glFramebufferTexture2D(target: u32, attachment: u32, textarget: u32, texture: u32, level: i32);
fn glCheckFramebufferStatus(target: u32) -> u32;
fn glViewport(x: i32, y: i32, width: i32, height: i32);
fn glClearColor(r: f32, g: f32, b: f32, a: f32);
fn glClear(mask: u32);
fn glReadPixels(
x: i32,
y: i32,
width: i32,
height: i32,
format: u32,
type_: u32,
pixels: *mut c_void,
);
fn glGetError() -> u32;
/// 返回当前上下文的信息字符串(非空 = 上下文真实可用)。
fn glGetString(name: u32) -> *const c_char;
}
/// CGL 上下文单例(OnceLock 惰性创建;Send+Sync 见下)。
pub struct GlContext {
ctx: *mut c_void,
pixel_format: *mut c_void,
}
// SAFETY: CGL 上下文对象可在任意线程经 CGLSetCurrentContext 置为
// current,但不能同时被两条线程使用——本模块用全局 Mutex 串行化
// 一切置 current 与 GL 调用,指针只在线程间传递(Arc 共享)而从不
// 并发解引用;Drop 只在进程退出(单线程)执行。Send/Sync 因此安全。
unsafe impl Send for GlContext {}
unsafe impl Sync for GlContext {}
impl Drop for GlContext {
fn drop(&mut self) {
// SAFETY: 进程退出期单线程;先清 current 再销毁对象,顺序
// 满足 CGL 文档(context 必须先取消 current 再 destroy)。
unsafe {
CGLSetCurrentContext(std::ptr::null_mut());
if !self.ctx.is_null() {
CGLDestroyContext(self.ctx);
}
if !self.pixel_format.is_null() {
CGLDestroyPixelFormat(self.pixel_format);
}
}
}
}
static GL_CONTEXT: OnceLock<Result<Arc<GlContext>, String>> = OnceLock::new();
/// GL 全局串行锁:同一时刻只有一条线程把共享上下文置为 current。
static GL_LOCK: Mutex<()> = Mutex::new(());
fn lock() -> std::sync::MutexGuard<'static, ()> {
GL_LOCK.lock().unwrap_or_else(|e| e.into_inner())
}
fn err(msg: &str) -> crate::error::Error {
crate::error::Error::Failed(format!("gl_bridge: {msg}"))
}
/// 惰性创建进程级 CGL 上下文(幂等)。
fn context() -> crate::error::Result<&'static Arc<GlContext>> {
let res = GL_CONTEXT.get_or_init(create_context);
match res {
Ok(c) => Ok(c),
Err(e) => Err(err(&format!("CGL 上下文创建失败:{e}"))),
}
}
fn create_context() -> Result<Arc<GlContext>, String> {
// SAFETY: CGLChoosePixelFormat 把像素格式对象写入 pix;属性表
// 以 0 结尾(kCGLPFAOpenGLProfile 之后是 profile 值、kCGLPFA
// AllowOfflineRenderers 后跟 1;见 CGL.h 的 attr 配对约定)。
let attrs = [
K_CGL_PFA_OPENGL_PROFILE,
K_CGLOGLP_VERSION_3_2_CORE,
K_CGL_PFA_ALLOW_OFFLINE_RENDERERS,
1,
K_CGL_PFA_COLOR_SIZE,
24,
K_CGL_PFA_ALPHA_SIZE,
8,
0,
];
unsafe {
let mut pix: *mut c_void = std::ptr::null_mut();
// npix 是 CGL 的 out 参数(像素格式计数);本桥不需要,但
// 必须给足实参(CGL 会写它)。
let mut npix: i32 = 0;
let st = CGLChoosePixelFormat(attrs.as_ptr(), &mut pix, &mut npix);
if st != 0 || pix.is_null() {
return Err(format!("CGLChoosePixelFormat 失败:{st}"));
}
let mut ctx: *mut c_void = std::ptr::null_mut();
// share = NULL:无共享组(本进程唯一 GL 使用方,无需与其他
// 上下文共享命名空间)。
let st = CGLCreateContext(pix, std::ptr::null_mut(), &mut ctx);
if st != 0 || ctx.is_null() {
CGLDestroyPixelFormat(pix);
return Err(format!("CGLCreateContext 失败:{st}"));
}
Ok(Arc::new(GlContext { ctx, pixel_format: pix }))
}
}
/// guard 持有期:全局锁 + 上下文 current。
pub struct GuardInner {
/// 最外层 guard 持有全局锁;同线程嵌套 guard 为 None(只递增
/// 深度,见 [`acquire`])。
_lock: Option<std::sync::MutexGuard<'static, ()>>,
_ctx: Arc<GlContext>,
}
/// 当前线程的 acquire 嵌套深度。>0 = 本线程已持外层 guard(上下文
/// 已 current);嵌套 acquire 只递增深度,不重取锁。
thread_local! {
static ACQUIRE_DEPTH: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
pub fn is_current() -> bool {
ACQUIRE_DEPTH.with(|d| d.get() > 0)
}
pub fn acquire() -> crate::error::Result<GlGuard> {
let ctx = context()?.clone();
let depth = ACQUIRE_DEPTH.with(|d| d.get());
if depth > 0 {
// 嵌套:上下文已 current(外层 guard 持有期),只递增深度。
ACQUIRE_DEPTH.with(|d| d.set(depth + 1));
return Ok(GlGuard {
_inner: GuardInner {
_lock: None,
_ctx: ctx,
},
});
}
let guard = lock();
// SAFETY: ctx 是有效 CGLContextObj;置 current 后本线程可发 GL
// 命令。失败返回非 0。
let st = unsafe { CGLSetCurrentContext(ctx.ctx) };
if st != 0 {
drop(guard);
return Err(err(&format!("CGLSetCurrentContext 失败:{st}")));
}
ACQUIRE_DEPTH.with(|d| d.set(1));
Ok(GlGuard {
_inner: GuardInner {
_lock: Some(guard),
_ctx: ctx,
},
})
}
impl Drop for GuardInner {
fn drop(&mut self) {
let depth = ACQUIRE_DEPTH.with(|d| d.get());
if depth <= 1 {
// 最外层:清 currentNULL)并释放全局锁。LIFO 栈纪律
// 保证外层 drop 前所有嵌套 guard 已 drop(深度归 1),
// 故 _lock 必为 Some。
ACQUIRE_DEPTH.with(|d| d.set(0));
// SAFETY: 清 currentNULL)后放锁;见模块文档的线程模型。
unsafe {
CGLSetCurrentContext(std::ptr::null_mut());
}
drop(self._lock.take());
} else {
// 嵌套 guard drop:只递减深度,不清 current/不放锁。
ACQUIRE_DEPTH.with(|d| d.set(depth - 1));
}
}
}
pub fn gl_available() -> bool {
context().is_ok()
}
/// GL 错误检查(测试断言用;返回最近错误或 GL_NO_ERROR)。
fn check_error(what: &str) -> crate::error::Result<()> {
// SAFETY: glGetError 无参数,读取当前上下文的错误状态。
let e = unsafe { glGetError() };
if e != GL_NO_ERROR {
Err(err(&format!("{what} 失败:GL error 0x{e:x}")))
} else {
Ok(())
}
}
/// 目标帧格式 → 输出 GL 纹理格式。
/// 返回 (internal format, readback type, bytes per channel)。
fn gl_texture_format(params: &VideoParams) -> crate::error::Result<(u32, u32, usize)> {
match params.format {
f if f == crate::render::PIXEL_FORMAT_F32 => Ok((GL_RGBA32F, GL_FLOAT, 4)),
f if f == crate::render::PIXEL_FORMAT_U8 => Ok((GL_RGBA8, GL_UNSIGNED_BYTE, 1)),
f => Err(err(&format!(
"目标帧格式 {f} 不支持 GL 输出(仅 F32/U8"
))),
}
}
/// 建纹理并设采样参数(FBO 完整性要求非 mipmap 的 min filter 与
/// clamp-to-edge)。
fn new_texture(w: i32, h: i32, internal: u32) -> crate::error::Result<u32> {
if w <= 0 || h <= 0 {
return Err(err("纹理尺寸非法"));
}
unsafe {
let mut tex: u32 = 0;
glGenTextures(1, &mut tex);
if tex == 0 {
return Err(err("glGenTextures 失败"));
}
glBindTexture(GL_TEXTURE_2D, tex);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST as i32);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST as i32);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE as i32);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE as i32);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
// internalformat 是 GLenum(无符号);C 签名里 glTexImage2D
// 的 internalformat 实为 GLint——此处值 < 2^31 安全。
glTexImage2D(
GL_TEXTURE_2D,
0,
internal as i32,
w,
h,
0,
GL_RGBA,
GL_FLOAT,
std::ptr::null(),
);
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
glBindTexture(GL_TEXTURE_2D, 0);
check_error("glTexImage2D")?;
Ok(tex)
}
}
pub fn create_output_texture(
w: i32,
h: i32,
params: &VideoParams,
) -> crate::error::Result<i32> {
let (internal, _, _) = gl_texture_format(params)?;
new_texture(w, h, internal).map(|t| t as i32)
}
pub fn create_input_texture(w: i32, h: i32, pixels: &[u8]) -> crate::error::Result<i32> {
let tex = new_texture(w, h, GL_RGBA32F)?;
// 上传 F32 RGBA 像素(紧凑,行优先;先确保 unpack alignment=1)。
let expected = (w as usize) * (h as usize) * 4 * 4;
if pixels.len() != expected {
let _ = delete_gl_texture(tex as i32);
return Err(err(&format!(
"输入像素长度 {} 与 w*h*4*4={expected} 不符",
pixels.len()
)));
}
unsafe {
glBindTexture(GL_TEXTURE_2D, tex);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexImage2D(
GL_TEXTURE_2D,
0,
GL_RGBA32F as i32,
w,
h,
0,
GL_RGBA,
GL_FLOAT,
pixels.as_ptr() as *const c_void,
);
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
glBindTexture(GL_TEXTURE_2D, 0);
}
check_error("输入纹理上传")?;
Ok(tex as i32)
}
pub fn delete_gl_texture(name: i32) {
if name <= 0 {
return;
}
// SAFETY: name 是之前 glGenTextures 产出的纹理名;删除后不可
// 再用(本桥保证不再引用)。要求上下文 current。
unsafe {
let t = name as u32;
glDeleteTextures(1, &t);
}
}
pub fn create_fbo(tex: i32, _w: i32, _h: i32) -> crate::error::Result<i32> {
if tex <= 0 {
return Err(err("FBO 附着纹理无效"));
}
unsafe {
let mut fbo: u32 = 0;
glGenFramebuffers(1, &mut fbo);
if fbo == 0 {
return Err(err("glGenFramebuffers 失败"));
}
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glFramebufferTexture2D(
GL_FRAMEBUFFER,
GL_COLOR_ATTACHMENT0,
GL_TEXTURE_2D,
tex as u32,
0,
);
let status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
if status != GL_FRAMEBUFFER_COMPLETE {
glDeleteFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
return Err(err(&format!("FBO 不完整:0x{status:x}")));
}
Ok(fbo as i32)
}
}
pub fn bind_fbo(fbo: i32) {
// SAFETY: 绑定 0(系统帧缓冲)或本桥创建的 FBO 名。
unsafe {
glBindFramebuffer(GL_FRAMEBUFFER, fbo as u32);
}
}
pub fn delete_fbo(fbo: i32) {
if fbo <= 0 {
return;
}
// SAFETY: fbo 是本桥 glGenFramebuffers 的产物;删除前先解除绑定。
unsafe {
glBindFramebuffer(GL_FRAMEBUFFER, 0);
let f = fbo as u32;
glDeleteFramebuffers(1, &f);
}
}
pub fn set_viewport(w: i32, h: i32) {
// SAFETY: 视口是 GL 上下文状态,直接写。
unsafe {
glViewport(0, 0, w, h);
}
}
/// 回读原始像素(紧凑、行优先、GL 原点左下)。
fn read_pixels(w: i32, h: i32, format: u32, type_: u32, bpc: usize) -> crate::error::Result<Vec<u8>> {
let size = (w as usize) * (h as usize) * 4 * bpc;
let mut buf = vec![0u8; size];
unsafe {
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glReadPixels(0, 0, w, h, format, type_, buf.as_mut_ptr() as *mut c_void);
glPixelStorei(GL_PACK_ALIGNMENT, 4);
}
check_error("glReadPixels")?;
Ok(buf)
}
pub fn read_pixels_to_image(
w: i32,
h: i32,
params: &VideoParams,
) -> crate::error::Result<Arc<Image>> {
let (_, gl_type, bpc) = gl_texture_format(params)?;
let raw = read_pixels(w, h, GL_RGBA, gl_type, bpc)?;
let (w, h) = (w as usize, h as usize);
let mut img = Image::allocate(
BitDepth::Float,
Components::Rgba,
crate::instance::OfxRectD {
x1: 0.0,
y1: 0.0,
x2: w as f64,
y2: h as f64,
},
);
let dst = img.pixels_mut();
let src_row = w * 4 * bpc;
let dst_row = w * 4 * 4;
for y in 0..h {
// 垂直翻转:GL 读回原点在左下,帧缓冲原点在左上。
let src_y = h - 1 - y;
let src = &raw[src_y * src_row..(src_y + 1) * src_row];
let d = &mut dst[y * dst_row..(y + 1) * dst_row];
for x in 0..w {
let (r, g, b, a): (f32, f32, f32, f32) = if bpc == 4 {
let p = &src[x * 16..x * 16 + 16];
(
f32::from_ne_bytes(p[0..4].try_into().unwrap()),
f32::from_ne_bytes(p[4..8].try_into().unwrap()),
f32::from_ne_bytes(p[8..12].try_into().unwrap()),
f32::from_ne_bytes(p[12..16].try_into().unwrap()),
)
} else {
let p = &src[x * 4..x * 4 + 4];
(
p[0] as f32 / 255.0,
p[1] as f32 / 255.0,
p[2] as f32 / 255.0,
p[3] as f32 / 255.0,
)
};
let o = x * 16;
d[o..o + 4].copy_from_slice(&r.to_ne_bytes());
d[o + 4..o + 8].copy_from_slice(&g.to_ne_bytes());
d[o + 8..o + 12].copy_from_slice(&b.to_ne_bytes());
d[o + 12..o + 16].copy_from_slice(&a.to_ne_bytes());
}
}
Ok(Arc::new(img))
}
pub fn gl_clear_color(r: f32, g: f32, b: f32, a: f32) {
// SAFETY: 写当前上下文的 clear 颜色状态。
unsafe {
glClearColor(r, g, b, a);
}
}
pub fn gl_clear() {
// SAFETY: 清除当前绑定帧缓冲的颜色缓冲。
unsafe {
glClear(GL_COLOR_BUFFER_BIT);
}
}
pub fn gl_version_string() -> Option<String> {
// SAFETY: glGetString 返回当前上下文内驻字符串;拷贝成 Rust
// String 后再返回,不持指针。
unsafe {
let p = glGetString(GL_VERSION);
if p.is_null() {
None
} else {
Some(std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned())
}
}
}
}
/// 非 macOSGL 桥 stubLinux 预留 EGL、Windows 预留 WGL 的接线点;
/// 全部失败/falseuse_opengl 决策回退 CPU)。
#[cfg(not(target_os = "macos"))]
mod imp {
use super::*;
/// Linux 接线点:替换为 EGLeglGetDisplay → eglInitialize →
/// eglChooseConfig → eglCreateContext → eglMakeCurrent);Windows
/// 替换为 WGLwglCreateContext/wglMakeCurrent)。签名与 macOS
/// 实现同构。
pub struct GuardInner;
pub fn gl_available() -> bool {
false
}
pub fn is_current() -> bool {
false
}
pub fn acquire() -> crate::error::Result<GlGuard> {
Err(crate::error::Error::Failed("gl_bridge: 平台无 GL 实现(Linux/Windows stub".into()))
}
pub fn create_output_texture(_w: i32, _h: i32, _p: &VideoParams) -> crate::error::Result<i32> {
Err(crate::error::Error::Failed("gl_bridge: 平台无 GL 实现".into()))
}
pub fn create_input_texture(_w: i32, _h: i32, _p: &[u8]) -> crate::error::Result<i32> {
Err(crate::error::Error::Failed("gl_bridge: 平台无 GL 实现".into()))
}
pub fn delete_gl_texture(_name: i32) {}
pub fn create_fbo(_tex: i32, _w: i32, _h: i32) -> crate::error::Result<i32> {
Err(crate::error::Error::Failed("gl_bridge: 平台无 GL 实现".into()))
}
pub fn bind_fbo(_fbo: i32) {}
pub fn delete_fbo(_fbo: i32) {}
pub fn set_viewport(_w: i32, _h: i32) {}
pub fn read_pixels_to_image(
_w: i32,
_h: i32,
_p: &VideoParams,
) -> crate::error::Result<Arc<Image>> {
Err(crate::error::Error::Failed("gl_bridge: 平台无 GL 实现".into()))
}
pub fn gl_clear_color(_r: f32, _g: f32, _b: f32, _a: f32) {}
pub fn gl_clear() {}
pub fn gl_version_string() -> Option<String> {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::image::{BitDepth, Components};
use crate::render::VideoParams;
/// GL 不可用时 skip(无 GPU 无头环境);可用时返回 guard。
fn maybe_gl() -> Option<GlGuard> {
match acquire() {
Ok(g) => Some(g),
Err(e) => {
println!("SKIP: GL 上下文不可用:{e}");
None
}
}
}
fn f32_params(w: i32, h: i32) -> VideoParams {
let mut p = VideoParams::default();
p.width = w;
p.height = h;
p.format = crate::render::PIXEL_FORMAT_F32;
p
}
fn u8_params(w: i32, h: i32) -> VideoParams {
let mut p = VideoParams::default();
p.width = w;
p.height = h;
p.format = crate::render::PIXEL_FORMAT_U8;
p
}
/// 真实 GL 上下文证据:acquire 后能取到非空版本串。
#[test]
fn gl_context_is_real() {
let Some(_g) = maybe_gl() else {
return;
};
let v = gl_version_string();
assert!(v.is_some(), "真实 CGL 上下文应返回 GL_VERSION");
let v = v.unwrap();
// macOS 上 Apple 的 OpenGL 实现构建于 Metal 之上,GL_VERSION
// 形如 "4.1 Metal - 90.5"(无头/有头皆然)——只断言非空。
assert!(!v.trim().is_empty(), "GL_VERSION 应非空,got {v:?}");
assert!(gl_available(), "上下文可用后 gl_available 应为 true");
}
/// 上下文单例与跨线程 current 串行:两条线程轮流 acquire 都能
/// 拿到真实上下文(不崩、版本一致)。
#[test]
fn gl_context_thread_serialized() {
let mut handles = Vec::new();
for _ in 0..4 {
handles.push(std::thread::spawn(|| {
let Some(_g) = maybe_gl() else {
return;
};
let v = gl_version_string().unwrap_or_default();
assert!(!v.is_empty());
}));
}
for h in handles {
h.join().unwrap();
}
}
/// 输出纹理 + FBO + clear + 回读 round-tripF32 目标):clear 到
/// 已知颜色 → glReadPixels 读回,逐像素断言。
#[test]
fn fbo_clear_readback_f32() {
let Some(_g) = maybe_gl() else {
return;
};
let (w, h) = (4i32, 2i32);
let params = f32_params(w, h);
let tex = create_output_texture(w, h, &params).expect("输出纹理应可建");
assert!(tex > 0, "GL 纹理名必须为真实非零 GL 名,got {tex}");
let fbo = create_fbo(tex, w, h).expect("FBO 应完整");
bind_fbo(fbo);
set_viewport(w, h);
gl_clear_color(0.1, 0.2, 0.3, 1.0);
gl_clear();
let img = read_pixels_to_image(w, h, &params).expect("回读应成功");
delete_fbo(fbo);
delete_gl_texture(tex);
assert_eq!(img.components(), Components::Rgba);
assert_eq!(img.depth(), BitDepth::Float);
assert_eq!(img.bounds().x2, w as f64);
assert_eq!(img.bounds().y2, h as f64);
let px = &img.pixels()[0..16];
let px_f: Vec<f32> = px.chunks_exact(4).map(|c| f32::from_ne_bytes(c.try_into().unwrap())).collect();
assert!((px_f[0] - 0.1).abs() < 1e-6, "R={}", px_f[0]);
assert!((px_f[1] - 0.2).abs() < 1e-6, "G={}", px_f[1]);
assert!((px_f[2] - 0.3).abs() < 1e-6, "B={}", px_f[2]);
assert!((px_f[3] - 1.0).abs() < 1e-6, "A={}", px_f[3]);
// 整帧同色(clear 覆盖全 FBO)。
let all: Vec<f32> = img.pixels()[..]
.chunks_exact(4)
.map(|c| f32::from_ne_bytes(c.try_into().unwrap()))
.collect();
assert!(all.chunks(4).all(|px| (px[0] - 0.1).abs() < 1e-6), "整帧应为清屏色");
}
/// U8 目标:clear → GL_RGBA8 存储 → 回读归一化为 F32(容差内)。
#[test]
fn fbo_clear_readback_u8() {
let Some(_g) = maybe_gl() else {
return;
};
let (w, h) = (3i32, 3i32);
let params = u8_params(w, h);
let tex = create_output_texture(w, h, &params).expect("U8 输出纹理应可建");
let fbo = create_fbo(tex, w, h).expect("FBO 应完整");
bind_fbo(fbo);
set_viewport(w, h);
gl_clear_color(0.5, 0.25, 0.0, 1.0);
gl_clear();
let img = read_pixels_to_image(w, h, &params).expect("U8 回读应成功");
delete_fbo(fbo);
delete_gl_texture(tex);
let px_f: Vec<f32> = img.pixels()[0..16]
.chunks_exact(4)
.map(|c| f32::from_ne_bytes(c.try_into().unwrap()))
.collect();
// 0.5*255=127.5→128round),128/255≈0.501960.25→64→0.25098。
assert!((px_f[0] - 0.502).abs() < 0.01, "R={}", px_f[0]);
assert!((px_f[1] - 0.251).abs() < 0.01, "G={}", px_f[1]);
assert!(px_f[2].abs() < 0.01, "B={}", px_f[2]);
assert!((px_f[3] - 1.0).abs() < 0.01, "A={}", px_f[3]);
}
/// 输入纹理上传:建 RGBA32F 纹理 + 上传已知像素,读回纹理内容
/// (经 FBO 附着 + glReadPixels)与源像素一致。
#[test]
fn input_texture_upload_roundtrip() {
let Some(_g) = maybe_gl() else {
return;
};
let (w, h) = (2i32, 2i32);
let mut px = Vec::new();
for i in 0..(w * h) as usize {
for c in 0..4 {
px.extend_from_slice(&((i * 4 + c) as f32 * 0.1).to_ne_bytes());
}
}
let tex = create_input_texture(w, h, &px).expect("输入纹理应可建");
let params = f32_params(w, h);
let fbo = create_fbo(tex, w, h).expect("FBO 应完整");
bind_fbo(fbo);
let img = read_pixels_to_image(w, h, &params).expect("回读应成功");
delete_fbo(fbo);
delete_gl_texture(tex);
let got: Vec<f32> = img.pixels()[0..w as usize * h as usize * 16]
.chunks_exact(4)
.map(|c| f32::from_ne_bytes(c.try_into().unwrap()))
.collect();
let want: Vec<f32> = px
.chunks_exact(4)
.map(|c| f32::from_ne_bytes(c.try_into().unwrap()))
.collect();
// 源是逐像素相异序列:read_pixels_to_image 垂直翻转(GL 左下 →
// 帧左上)后,最终图像第 r 行 = 源第 (h-1-r) 行。
let stride = w as usize * 4;
for (i, g) in got.iter().enumerate() {
let row = i / stride;
let inv_row = (h as usize - 1 - row) as usize;
let expected = want[inv_row * stride + i % stride];
assert!(
(g - expected).abs() < 1e-6,
"pixel {i}: got {g} want {expected}"
);
}
}
/// 同线程可重入 acquire:外层 guard 持有期再 acquire 不死锁,GL
/// 调用仍可达;嵌套 guard drop 后上下文保持 current(外层仍有效),
/// 外层 drop 后清 current。模拟 interact draw 的"一次 acquire 覆盖
/// FBO 装配 → 插件绘制 → 回读"场景。
#[test]
fn acquire_is_reentrant_on_same_thread() {
let Some(_outer) = maybe_gl() else {
return;
};
// 嵌套 acquireinteract::draw 内部路径)。
let inner = acquire().expect("嵌套 acquire 应成功(同线程重入)");
let v1 = gl_version_string();
assert!(v1.is_some(), "嵌套 guard 期间 GL 应仍 current");
drop(inner);
// 外层仍持有:GL 仍 current。
let v2 = gl_version_string();
assert!(v2.is_some(), "嵌套 guard drop 后外层仍应 current");
// 再套一层(深度 2)验证深度计数不串。
{
let inner2 = acquire().expect("二次嵌套应成功");
assert!(gl_version_string().is_some());
drop(inner2);
}
}
}