// 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 .
//! GL 互操作桥(方案 B 落地:离屏 GL 上下文 + 纹理/FBO + 回读)。
//!
//! ## 背景(阶段 6a spike 结论,已落地)
//!
//! OFX 的 `OpenGLRender` 扩展要求宿主把 clip 纹理以 **GL 纹理名**
//! (`kOfxImageEffectPropOpenGLTextureIndex`)递给插件,插件直接画进
//! 输出纹理。oak 的渲染后端是 wgpu(macOS 为 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 {
imp::acquire()
}
/// 创建输出 GL 纹理(尺寸 = 目标帧,内部格式按 `params.format`:
/// F32 → RGBA32F,U8 → RGBA8),返回真实 GL 纹理名。
///
/// # Safety
///
/// 要求 GL 上下文已 current([`GlGuard`] 持有期间调用)。
pub fn create_output_texture(w: i32, h: i32, params: &VideoParams) -> crate::error::Result {
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 {
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 {
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> {
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 {
imp::gl_version_string()
}
/// GL 上下文句柄(持有全局锁 + 上下文 current;drop 时清 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 像素格式属性(CGLPixelFormatAttribute,CGL.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;
/// macOS:CGL + 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, 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> {
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, 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>,
_ctx: Arc,
}
/// 当前线程的 acquire 嵌套深度。>0 = 本线程已持外层 guard(上下文
/// 已 current);嵌套 acquire 只递增深度,不重取锁。
thread_local! {
static ACQUIRE_DEPTH: std::cell::Cell = const { std::cell::Cell::new(0) };
}
pub fn is_current() -> bool {
ACQUIRE_DEPTH.with(|d| d.get() > 0)
}
pub fn acquire() -> crate::error::Result {
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 {
// 最外层:清 current(NULL)并释放全局锁。LIFO 栈纪律
// 保证外层 drop 前所有嵌套 guard 已 drop(深度归 1),
// 故 _lock 必为 Some。
ACQUIRE_DEPTH.with(|d| d.set(0));
// SAFETY: 清 current(NULL)后放锁;见模块文档的线程模型。
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 {
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 {
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 {
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 {
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> {
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> {
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 {
// 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())
}
}
}
}
/// 非 macOS:GL 桥 stub(Linux 预留 EGL、Windows 预留 WGL 的接线点;
/// 全部失败/false,use_opengl 决策回退 CPU)。
#[cfg(not(target_os = "macos"))]
mod imp {
use super::*;
/// Linux 接线点:替换为 EGL(eglGetDisplay → eglInitialize →
/// eglChooseConfig → eglCreateContext → eglMakeCurrent);Windows
/// 替换为 WGL(wglCreateContext/wglMakeCurrent)。签名与 macOS
/// 实现同构。
pub struct GuardInner;
pub fn gl_available() -> bool {
false
}
pub fn is_current() -> bool {
false
}
pub fn acquire() -> crate::error::Result {
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 {
Err(crate::error::Error::Failed("gl_bridge: 平台无 GL 实现".into()))
}
pub fn create_input_texture(_w: i32, _h: i32, _p: &[u8]) -> crate::error::Result {
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 {
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> {
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 {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::image::{BitDepth, Components};
use crate::render::VideoParams;
/// GL 不可用时 skip(无 GPU 无头环境);可用时返回 guard。
fn maybe_gl() -> Option {
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-trip(F32 目标):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, ¶ms).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, ¶ms).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 = 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 = 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, ¶ms).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, ¶ms).expect("U8 回读应成功");
delete_fbo(fbo);
delete_gl_texture(tex);
let px_f: Vec = img.pixels()[0..16]
.chunks_exact(4)
.map(|c| f32::from_ne_bytes(c.try_into().unwrap()))
.collect();
// 0.5*255=127.5→128(round),128/255≈0.50196;0.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, ¶ms).expect("回读应成功");
delete_fbo(fbo);
delete_gl_texture(tex);
let got: Vec = 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 = 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;
};
// 嵌套 acquire(interact::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);
}
}
}