fix(zerocopy/egl): construction can fail late, so teardown must be structural
Three unwind holes in the EGL side, all of the same shape — fallible construction over raw handles with no Drop to unwind through — and all retried per frame, so a sustained failure (VRAM pressure is the realistic one) leaked unboundedly: - The GlBlit/Nv12Blit/Yuv444Blit constructors interleave GL-object creation with fallible steps (FBO completeness, CUDA registration, pool allocation). A GlNameGuard now owns every bare GL name until the final struct exists; on unwind it deletes them AFTER the RegisteredTexture locals unregister (declaration order), preserving the unregister-before-delete invariant. gl.rs gets the same treatment for its compile paths: the vertex shader on a fragment-compile failure, the program on a link failure. (R1) - EglImporter::new leaked the DRM render-node fd and the whole gbm_device on every '?' after their creation (most realistically cuda::context() failing on a host where EGL comes up but CUDA does not). Both now live in a GbmDevice with its own Drop: destroy the device, then close the fd. (R2) - EglImporter's manual Drop destroyed the gbm device and closed the fd BEFORE field drops ran the blit destructors, which then called cuGraphicsUnregisterResource/glDeleteTextures against a dead native display — the stale-driver-state class this path once crashed on. The Drop impl is gone; teardown is now purely field order (blits and bridge first, GbmDevice last), and the blit SAFETY comments cite that order instead of the previously-false claim. (R4) (R1/R2/R4 from design/pf-zerocopy-sweep-handoff.md.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@
|
|||||||
use super::cuda::{self, DeviceBuffer};
|
use super::cuda::{self, DeviceBuffer};
|
||||||
use anyhow::{ensure, Context as _, Result};
|
use anyhow::{ensure, Context as _, Result};
|
||||||
use khronos_egl as egl;
|
use khronos_egl as egl;
|
||||||
|
use std::os::fd::{AsRawFd as _, FromRawFd as _};
|
||||||
use std::os::raw::{c_int, c_void};
|
use std::os::raw::{c_int, c_void};
|
||||||
|
|
||||||
// EGL_EXT_image_dma_buf_import / _modifiers + platform enums (not defined by khronos-egl).
|
// EGL_EXT_image_dma_buf_import / _modifiers + platform enums (not defined by khronos-egl).
|
||||||
@@ -90,6 +91,75 @@ fn nvidia_render_node_in(
|
|||||||
.map(|node| dri.join(node))
|
.map(|node| dri.join(node))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Bare GL names created mid-constructor, deleted on unwind if the constructor fails before its
|
||||||
|
/// struct (whose `Drop` then owns deletion) exists. The blit constructors interleave GL-object
|
||||||
|
/// creation with fallible steps (FBO-completeness checks, CUDA registration, pool allocation) and
|
||||||
|
/// are retried per frame — without this, a late failure leaked every name, unbounded, under
|
||||||
|
/// exactly the VRAM pressure that causes such failures. `defuse()` hands ownership to the built
|
||||||
|
/// struct. `RegisteredTexture` locals unwind themselves (their own `Drop`), and they drop before
|
||||||
|
/// this guard (declared after it), preserving the unregister-before-delete order.
|
||||||
|
#[derive(Default)]
|
||||||
|
struct GlNameGuard {
|
||||||
|
textures: Vec<u32>,
|
||||||
|
fbos: Vec<u32>,
|
||||||
|
vaos: Vec<u32>,
|
||||||
|
programs: Vec<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GlNameGuard {
|
||||||
|
/// Construction succeeded — the final struct's `Drop` owns the names from here.
|
||||||
|
fn defuse(mut self) {
|
||||||
|
self.textures.clear();
|
||||||
|
self.fbos.clear();
|
||||||
|
self.vaos.clear();
|
||||||
|
self.programs.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for GlNameGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// SAFETY: every name held was created by the guarded constructor on the GL context still
|
||||||
|
// current on this thread (constructors run and unwind on the single capture thread). Each
|
||||||
|
// `glDelete*` gets a count of 1 and a pointer to one live element; names reaching this
|
||||||
|
// `Drop` were never handed to a final struct (`defuse` clears the Vecs), so each is
|
||||||
|
// deleted exactly once.
|
||||||
|
unsafe {
|
||||||
|
for t in &self.textures {
|
||||||
|
glDeleteTextures(1, t);
|
||||||
|
}
|
||||||
|
for f in &self.fbos {
|
||||||
|
glDeleteFramebuffers(1, f);
|
||||||
|
}
|
||||||
|
for v in &self.vaos {
|
||||||
|
glDeleteVertexArrays(1, v);
|
||||||
|
}
|
||||||
|
for &p in &self.programs {
|
||||||
|
glDeleteProgram(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The GBM device and the DRM render-node fd it borrows, owned together so every exit path —
|
||||||
|
/// including each `?` in `EglImporter::new` after their creation (most realistically a host where
|
||||||
|
/// EGL comes up but CUDA does not) — releases both, in order: destroy the device, then close the
|
||||||
|
/// fd (`OwnedFd`'s drop). Also what makes `EglImporter` teardown ordering structural: the importer
|
||||||
|
/// has no `Drop` of its own, so its fields drop in declaration order — GL/CUDA objects first, this
|
||||||
|
/// device (and the display it backs) last.
|
||||||
|
struct GbmDevice {
|
||||||
|
raw: *mut c_void,
|
||||||
|
_fd: std::os::fd::OwnedFd,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for GbmDevice {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// SAFETY: `raw` is the non-null `gbm_device*` from `gbm_create_device` (null-checked at
|
||||||
|
// construction), owned exclusively by this struct and destroyed exactly once here — before
|
||||||
|
// `_fd` (the render-node fd the device borrows) closes via its own drop.
|
||||||
|
unsafe { gbm_device_destroy(self.raw) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Per-size GL machinery to blit a dmabuf EGLImage into a CUDA-registrable `GL_RGBA8` texture.
|
/// Per-size GL machinery to blit a dmabuf EGLImage into a CUDA-registrable `GL_RGBA8` texture.
|
||||||
struct GlBlit {
|
struct GlBlit {
|
||||||
program: u32,
|
program: u32,
|
||||||
@@ -109,14 +179,21 @@ struct GlBlit {
|
|||||||
|
|
||||||
impl GlBlit {
|
impl GlBlit {
|
||||||
unsafe fn new(width: u32, height: u32) -> Result<GlBlit> {
|
unsafe fn new(width: u32, height: u32) -> Result<GlBlit> {
|
||||||
|
// Declared before every GL name so it drops LAST on unwind — after the CUDA registration
|
||||||
|
// (declared below) has unregistered itself.
|
||||||
|
let mut guard = GlNameGuard::default();
|
||||||
let program = compile_program()?;
|
let program = compile_program()?;
|
||||||
|
guard.programs.push(program);
|
||||||
let mut vao = 0u32;
|
let mut vao = 0u32;
|
||||||
glGenVertexArrays(1, &mut vao); // core profile needs a bound VAO for glDrawArrays
|
glGenVertexArrays(1, &mut vao); // core profile needs a bound VAO for glDrawArrays
|
||||||
|
guard.vaos.push(vao);
|
||||||
let mut fbo = 0u32;
|
let mut fbo = 0u32;
|
||||||
glGenFramebuffers(1, &mut fbo);
|
glGenFramebuffers(1, &mut fbo);
|
||||||
|
guard.fbos.push(fbo);
|
||||||
|
|
||||||
let mut dst_tex = 0u32;
|
let mut dst_tex = 0u32;
|
||||||
glGenTextures(1, &mut dst_tex);
|
glGenTextures(1, &mut dst_tex);
|
||||||
|
guard.textures.push(dst_tex);
|
||||||
glBindTexture(GL_TEXTURE_2D, dst_tex);
|
glBindTexture(GL_TEXTURE_2D, dst_tex);
|
||||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, width as c_int, height as c_int);
|
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, width as c_int, height as c_int);
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
@@ -124,6 +201,7 @@ impl GlBlit {
|
|||||||
|
|
||||||
let mut src_tex = 0u32;
|
let mut src_tex = 0u32;
|
||||||
glGenTextures(1, &mut src_tex);
|
glGenTextures(1, &mut src_tex);
|
||||||
|
guard.textures.push(src_tex);
|
||||||
glBindTexture(GL_TEXTURE_2D, src_tex);
|
glBindTexture(GL_TEXTURE_2D, src_tex);
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||||
@@ -148,6 +226,7 @@ impl GlBlit {
|
|||||||
// current (the caller makes it current before constructing the blit).
|
// current (the caller makes it current before constructing the blit).
|
||||||
let registered = cuda::RegisteredTexture::register_gl(dst_tex)?;
|
let registered = cuda::RegisteredTexture::register_gl(dst_tex)?;
|
||||||
let pool = cuda::BufferPool::new(width, height)?;
|
let pool = cuda::BufferPool::new(width, height)?;
|
||||||
|
guard.defuse();
|
||||||
Ok(GlBlit {
|
Ok(GlBlit {
|
||||||
program,
|
program,
|
||||||
vao,
|
vao,
|
||||||
@@ -193,10 +272,12 @@ impl Drop for GlBlit {
|
|||||||
// its GL objects leaked on every size change and on importer teardown.
|
// its GL objects leaked on every size change and on importer teardown.
|
||||||
self.registered.release();
|
self.registered.release();
|
||||||
// SAFETY: these GL names were all created by THIS `GlBlit` in `GlBlit::new` on the current
|
// SAFETY: these GL names were all created by THIS `GlBlit` in `GlBlit::new` on the current
|
||||||
// GL context, still current here (the owning `EglImporter` drops on its single capture
|
// GL context, still current here (the owning `EglImporter` — which never releases the
|
||||||
// thread and never releases the context). Each `glDelete*` gets a count of 1 and a `&u32`
|
// context — drops on its single capture thread and has no `Drop` of its own, so this blit
|
||||||
// to one live field; the symbols dispatch through libGL to the driver for the current
|
// field drops before the `GbmDevice` backing the display). Each `glDelete*` gets a count
|
||||||
// context. Each name is deleted exactly once, after its CUDA registration was released.
|
// of 1 and a `&u32` to one live field; the symbols dispatch through libGL to the driver
|
||||||
|
// for the current context. Each name is deleted exactly once, after its CUDA registration
|
||||||
|
// was released.
|
||||||
unsafe {
|
unsafe {
|
||||||
glDeleteTextures(1, &self.dst_tex);
|
glDeleteTextures(1, &self.dst_tex);
|
||||||
glDeleteTextures(1, &self.src_tex);
|
glDeleteTextures(1, &self.src_tex);
|
||||||
@@ -242,17 +323,25 @@ impl Nv12Blit {
|
|||||||
width % 2 == 0 && height % 2 == 0,
|
width % 2 == 0 && height % 2 == 0,
|
||||||
"NV12 convert needs even dimensions (got {width}x{height})"
|
"NV12 convert needs even dimensions (got {width}x{height})"
|
||||||
);
|
);
|
||||||
|
// Declared before every GL name so it drops LAST on unwind — after the CUDA registrations
|
||||||
|
// (declared below) have unregistered themselves.
|
||||||
|
let mut guard = GlNameGuard::default();
|
||||||
let y_program = compile_program_with(FRAG_Y_SRC)?;
|
let y_program = compile_program_with(FRAG_Y_SRC)?;
|
||||||
|
guard.programs.push(y_program);
|
||||||
let uv_program = compile_program_with(FRAG_UV_SRC)?;
|
let uv_program = compile_program_with(FRAG_UV_SRC)?;
|
||||||
|
guard.programs.push(uv_program);
|
||||||
let mut vao = 0u32;
|
let mut vao = 0u32;
|
||||||
glGenVertexArrays(1, &mut vao);
|
glGenVertexArrays(1, &mut vao);
|
||||||
|
guard.vaos.push(vao);
|
||||||
let mut fbos = [0u32; 2];
|
let mut fbos = [0u32; 2];
|
||||||
glGenFramebuffers(2, fbos.as_mut_ptr());
|
glGenFramebuffers(2, fbos.as_mut_ptr());
|
||||||
|
guard.fbos.extend_from_slice(&fbos);
|
||||||
let (y_fbo, uv_fbo) = (fbos[0], fbos[1]);
|
let (y_fbo, uv_fbo) = (fbos[0], fbos[1]);
|
||||||
|
|
||||||
// Luma target: GL_R8 at full resolution.
|
// Luma target: GL_R8 at full resolution.
|
||||||
let mut y_tex = 0u32;
|
let mut y_tex = 0u32;
|
||||||
glGenTextures(1, &mut y_tex);
|
glGenTextures(1, &mut y_tex);
|
||||||
|
guard.textures.push(y_tex);
|
||||||
glBindTexture(GL_TEXTURE_2D, y_tex);
|
glBindTexture(GL_TEXTURE_2D, y_tex);
|
||||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_R8, width as c_int, height as c_int);
|
glTexStorage2D(GL_TEXTURE_2D, 1, GL_R8, width as c_int, height as c_int);
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
@@ -261,6 +350,7 @@ impl Nv12Blit {
|
|||||||
// Chroma target: GL_RG8 at half resolution (R=U, G=V).
|
// Chroma target: GL_RG8 at half resolution (R=U, G=V).
|
||||||
let mut uv_tex = 0u32;
|
let mut uv_tex = 0u32;
|
||||||
glGenTextures(1, &mut uv_tex);
|
glGenTextures(1, &mut uv_tex);
|
||||||
|
guard.textures.push(uv_tex);
|
||||||
glBindTexture(GL_TEXTURE_2D, uv_tex);
|
glBindTexture(GL_TEXTURE_2D, uv_tex);
|
||||||
glTexStorage2D(
|
glTexStorage2D(
|
||||||
GL_TEXTURE_2D,
|
GL_TEXTURE_2D,
|
||||||
@@ -275,6 +365,7 @@ impl Nv12Blit {
|
|||||||
// Source: GL_LINEAR so the half-res UV pass averages the 2×2 chroma footprint.
|
// Source: GL_LINEAR so the half-res UV pass averages the 2×2 chroma footprint.
|
||||||
let mut src_tex = 0u32;
|
let mut src_tex = 0u32;
|
||||||
glGenTextures(1, &mut src_tex);
|
glGenTextures(1, &mut src_tex);
|
||||||
|
guard.textures.push(src_tex);
|
||||||
glBindTexture(GL_TEXTURE_2D, src_tex);
|
glBindTexture(GL_TEXTURE_2D, src_tex);
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||||
@@ -294,6 +385,7 @@ impl Nv12Blit {
|
|||||||
let y_registered = cuda::RegisteredTexture::register_gl(y_tex)?;
|
let y_registered = cuda::RegisteredTexture::register_gl(y_tex)?;
|
||||||
let uv_registered = cuda::RegisteredTexture::register_gl(uv_tex)?;
|
let uv_registered = cuda::RegisteredTexture::register_gl(uv_tex)?;
|
||||||
let pool = cuda::BufferPool::new_nv12(width, height)?;
|
let pool = cuda::BufferPool::new_nv12(width, height)?;
|
||||||
|
guard.defuse();
|
||||||
Ok(Nv12Blit {
|
Ok(Nv12Blit {
|
||||||
y_program,
|
y_program,
|
||||||
uv_program,
|
uv_program,
|
||||||
@@ -362,8 +454,9 @@ impl Drop for Nv12Blit {
|
|||||||
self.uv_registered.release();
|
self.uv_registered.release();
|
||||||
// SAFETY: these GL names (textures/FBOs/VAO/programs) were all created by THIS `Nv12Blit`
|
// SAFETY: these GL names (textures/FBOs/VAO/programs) were all created by THIS `Nv12Blit`
|
||||||
// in `Nv12Blit::new` on the current GL context, which is still current because the owning
|
// in `Nv12Blit::new` on the current GL context, which is still current because the owning
|
||||||
// `EglImporter` is dropped on its single capture thread (fields drop before
|
// `EglImporter` (which never releases the context) drops on its single capture thread and
|
||||||
// `EglImporter::drop`, which never releases the context). `glDelete*` takes a count + a
|
// has no `Drop` of its own — its fields drop in declaration order, this blit before the
|
||||||
|
// `GbmDevice` backing the display. `glDelete*` takes a count + a
|
||||||
// pointer to that many names: `&self.y_tex`/`&self.vao` are `&u32` to one live field (n=1);
|
// pointer to that many names: `&self.y_tex`/`&self.vao` are `&u32` to one live field (n=1);
|
||||||
// `[self.y_fbo, self.uv_fbo].as_ptr()` points at a 2-element temporary that lives for the
|
// `[self.y_fbo, self.uv_fbo].as_ptr()` points at a 2-element temporary that lives for the
|
||||||
// whole `glDeleteFramebuffers` call (n=2 matches). The symbols dispatch through libGL
|
// whole `glDeleteFramebuffers` call (n=2 matches). The symbols dispatch through libGL
|
||||||
@@ -411,17 +504,23 @@ impl Yuv444Blit {
|
|||||||
);
|
);
|
||||||
let full_range = std::env::var("PUNKTFUNK_444_FULLRANGE").is_ok_and(|v| v.trim() == "1");
|
let full_range = std::env::var("PUNKTFUNK_444_FULLRANGE").is_ok_and(|v| v.trim() == "1");
|
||||||
let (y_src, u_src, v_src) = yuv444_frag_sources(full_range);
|
let (y_src, u_src, v_src) = yuv444_frag_sources(full_range);
|
||||||
let programs = [
|
// Declared before every GL name so it drops LAST on unwind — after the CUDA registrations
|
||||||
compile_program_with(&y_src)?,
|
// (declared below) have unregistered themselves.
|
||||||
compile_program_with(&u_src)?,
|
let mut guard = GlNameGuard::default();
|
||||||
compile_program_with(&v_src)?,
|
let mut programs = [0u32; 3];
|
||||||
];
|
for (p, src) in programs.iter_mut().zip([&y_src, &u_src, &v_src]) {
|
||||||
|
*p = compile_program_with(src)?;
|
||||||
|
guard.programs.push(*p);
|
||||||
|
}
|
||||||
let mut vao = 0u32;
|
let mut vao = 0u32;
|
||||||
glGenVertexArrays(1, &mut vao);
|
glGenVertexArrays(1, &mut vao);
|
||||||
|
guard.vaos.push(vao);
|
||||||
let mut fbos = [0u32; 3];
|
let mut fbos = [0u32; 3];
|
||||||
glGenFramebuffers(3, fbos.as_mut_ptr());
|
glGenFramebuffers(3, fbos.as_mut_ptr());
|
||||||
|
guard.fbos.extend_from_slice(&fbos);
|
||||||
let mut texs = [0u32; 3];
|
let mut texs = [0u32; 3];
|
||||||
glGenTextures(3, texs.as_mut_ptr());
|
glGenTextures(3, texs.as_mut_ptr());
|
||||||
|
guard.textures.extend_from_slice(&texs);
|
||||||
for &tex in &texs {
|
for &tex in &texs {
|
||||||
glBindTexture(GL_TEXTURE_2D, tex);
|
glBindTexture(GL_TEXTURE_2D, tex);
|
||||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_R8, width as c_int, height as c_int);
|
glTexStorage2D(GL_TEXTURE_2D, 1, GL_R8, width as c_int, height as c_int);
|
||||||
@@ -432,6 +531,7 @@ impl Yuv444Blit {
|
|||||||
// the Nv12Blit source setup.
|
// the Nv12Blit source setup.
|
||||||
let mut src_tex = 0u32;
|
let mut src_tex = 0u32;
|
||||||
glGenTextures(1, &mut src_tex);
|
glGenTextures(1, &mut src_tex);
|
||||||
|
guard.textures.push(src_tex);
|
||||||
glBindTexture(GL_TEXTURE_2D, src_tex);
|
glBindTexture(GL_TEXTURE_2D, src_tex);
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||||
@@ -452,6 +552,7 @@ impl Yuv444Blit {
|
|||||||
cuda::RegisteredTexture::register_gl(texs[2])?,
|
cuda::RegisteredTexture::register_gl(texs[2])?,
|
||||||
];
|
];
|
||||||
let pool = cuda::BufferPool::new_yuv444(width, height)?;
|
let pool = cuda::BufferPool::new_yuv444(width, height)?;
|
||||||
|
guard.defuse();
|
||||||
if full_range {
|
if full_range {
|
||||||
tracing::info!("YUV444 zero-copy convert: FULL range (PUNKTFUNK_444_FULLRANGE=1)");
|
tracing::info!("YUV444 zero-copy convert: FULL range (PUNKTFUNK_444_FULLRANGE=1)");
|
||||||
}
|
}
|
||||||
@@ -502,10 +603,11 @@ impl Drop for Yuv444Blit {
|
|||||||
r.release();
|
r.release();
|
||||||
}
|
}
|
||||||
// SAFETY: these GL names were all created by THIS `Yuv444Blit` in `new` on the current GL
|
// SAFETY: these GL names were all created by THIS `Yuv444Blit` in `new` on the current GL
|
||||||
// context (still current — the owning `EglImporter` drops on its single capture thread).
|
// context (still current — the owning `EglImporter` drops on its single capture thread,
|
||||||
// Each `glDelete*` takes a count + a pointer to that many names; the arrays are live
|
// and having no `Drop` of its own, this blit field drops before the `GbmDevice` backing
|
||||||
// fields (or a live temporary for the whole call). Each name is deleted exactly once,
|
// the display). Each `glDelete*` takes a count + a pointer to that many names; the arrays
|
||||||
// after its CUDA registration was released above.
|
// are live fields (or a live temporary for the whole call). Each name is deleted exactly
|
||||||
|
// once, after its CUDA registration was released above.
|
||||||
unsafe {
|
unsafe {
|
||||||
glDeleteTextures(3, self.texs.as_ptr());
|
glDeleteTextures(3, self.texs.as_ptr());
|
||||||
glDeleteTextures(1, &self.src_tex);
|
glDeleteTextures(1, &self.src_tex);
|
||||||
@@ -563,8 +665,11 @@ pub struct EglImporter {
|
|||||||
/// NV12 twin of [`linear_pool`](Self::linear_pool) for the bridge's compute-CSC output
|
/// NV12 twin of [`linear_pool`](Self::linear_pool) for the bridge's compute-CSC output
|
||||||
/// (T2.5b) — separate pools because a session may fall back RGB mid-stream.
|
/// (T2.5b) — separate pools because a session may fall back RGB mid-stream.
|
||||||
linear_nv12_pool: Option<cuda::BufferPool>,
|
linear_nv12_pool: Option<cuda::BufferPool>,
|
||||||
gbm: *mut c_void,
|
/// Declared LAST deliberately: with no `Drop` impl on `EglImporter`, fields drop in
|
||||||
render_fd: c_int,
|
/// declaration order, so every GL blit / CUDA registration / Vulkan bridge above releases its
|
||||||
|
/// driver objects BEFORE the gbm device backing the EGLDisplay (and its fd) go away — the
|
||||||
|
/// stale-driver-state class the blit destructors' ordering comments cite.
|
||||||
|
_gbm: GbmDevice,
|
||||||
}
|
}
|
||||||
|
|
||||||
// SAFETY: `EglImporter` owns thread-affine handles — an EGLDisplay/contexts made current on one
|
// SAFETY: `EglImporter` owns thread-affine handles — an EGLDisplay/contexts made current on one
|
||||||
@@ -593,19 +698,25 @@ impl EglImporter {
|
|||||||
// retains the pointer nor writes through it, so there is no aliasing or lifetime hazard.
|
// retains the pointer nor writes through it, so there is no aliasing or lifetime hazard.
|
||||||
let render_fd = unsafe { libc::open(path.as_ptr(), libc::O_RDWR | libc::O_CLOEXEC) };
|
let render_fd = unsafe { libc::open(path.as_ptr(), libc::O_RDWR | libc::O_CLOEXEC) };
|
||||||
ensure!(render_fd >= 0, "open {} for GBM", node.display());
|
ensure!(render_fd >= 0, "open {} for GBM", node.display());
|
||||||
// SAFETY: `render_fd` is the live DRM render-node fd just returned by `open` and checked
|
// SAFETY: `open` just returned this fd (checked `>= 0`) and nothing else owns it, so
|
||||||
// `>= 0`. `gbm_create_device` (libgbm, linked above) builds a `gbm_device` over that fd and
|
// `OwnedFd` takes sole ownership — every exit path below (including each `?`) now closes
|
||||||
// returns a `*mut gbm_device` (or null); it borrows but does not take ownership of the fd,
|
// it exactly once, after the gbm device borrowing it is destroyed (`GbmDevice`'s drop
|
||||||
// which `EglImporter` keeps open and closes only in `Drop` after `gbm_device_destroy`. No
|
// order).
|
||||||
// Rust-owned memory is passed, so there is nothing to alias.
|
let render_fd = unsafe { std::os::fd::OwnedFd::from_raw_fd(render_fd) };
|
||||||
let gbm = unsafe { gbm_create_device(render_fd) };
|
// SAFETY: `render_fd` is the live DRM render-node fd just opened. `gbm_create_device`
|
||||||
if gbm.is_null() {
|
// (libgbm, linked above) builds a `gbm_device` over that fd and returns a
|
||||||
// SAFETY: reached only when `gbm_create_device` failed (null) — the fd was not consumed
|
// `*mut gbm_device` (or null); it borrows but does not take ownership of the fd, which
|
||||||
// and no `EglImporter` exists yet to close it again, so this `close` runs exactly once on
|
// `GbmDevice` keeps open until after `gbm_device_destroy`. No Rust-owned memory is
|
||||||
// the live `render_fd`, releasing it before the error return. No double-close.
|
// passed, so there is nothing to alias.
|
||||||
unsafe { libc::close(render_fd) };
|
let raw_gbm = unsafe { gbm_create_device(render_fd.as_raw_fd()) };
|
||||||
|
if raw_gbm.is_null() {
|
||||||
|
// `render_fd` closes via its own drop.
|
||||||
anyhow::bail!("gbm_create_device failed on {}", node.display());
|
anyhow::bail!("gbm_create_device failed on {}", node.display());
|
||||||
}
|
}
|
||||||
|
let gbm = GbmDevice {
|
||||||
|
raw: raw_gbm,
|
||||||
|
_fd: render_fd,
|
||||||
|
};
|
||||||
|
|
||||||
// SAFETY: `Egl::load_required` dlopens the system libEGL and binds its entry points,
|
// SAFETY: `Egl::load_required` dlopens the system libEGL and binds its entry points,
|
||||||
// trusting that libEGL (libglvnd) is a genuine EGL 1.5 implementation whose core symbols
|
// trusting that libEGL (libglvnd) is a genuine EGL 1.5 implementation whose core symbols
|
||||||
@@ -613,16 +724,16 @@ impl EglImporter {
|
|||||||
// returned instance is afterwards used only through the safe `khronos_egl` wrappers.
|
// returned instance is afterwards used only through the safe `khronos_egl` wrappers.
|
||||||
let egl: Egl =
|
let egl: Egl =
|
||||||
unsafe { Egl::load_required() }.context("load libEGL (EGL 1.5 dynamic instance)")?;
|
unsafe { Egl::load_required() }.context("load libEGL (EGL 1.5 dynamic instance)")?;
|
||||||
// SAFETY: `gbm` is the non-null `gbm_device*` created just above (checked), and
|
// SAFETY: `gbm.raw` is the non-null `gbm_device*` created just above (checked), and
|
||||||
// `EGL_PLATFORM_GBM_KHR` is exactly the platform enum that pairs with a GBM device as the
|
// `EGL_PLATFORM_GBM_KHR` is exactly the platform enum that pairs with a GBM device as the
|
||||||
// native-display handle, so the `gbm as NativeDisplayType` cast hands EGL a valid native
|
// native-display handle, so the `gbm.raw as NativeDisplayType` cast hands EGL a valid native
|
||||||
// display for the requested platform. `&[egl::ATTRIB_NONE]` is a properly terminated, empty
|
// display for the requested platform. `&[egl::ATTRIB_NONE]` is a properly terminated, empty
|
||||||
// attribute array borrowed for this synchronous call; EGL only reads it and returns an
|
// attribute array borrowed for this synchronous call; EGL only reads it and returns an
|
||||||
// `EGLDisplay`, retaining no pointer into Rust memory.
|
// `EGLDisplay`, retaining no pointer into Rust memory.
|
||||||
let display = unsafe {
|
let display = unsafe {
|
||||||
egl.get_platform_display(
|
egl.get_platform_display(
|
||||||
EGL_PLATFORM_GBM_KHR,
|
EGL_PLATFORM_GBM_KHR,
|
||||||
gbm as egl::NativeDisplayType,
|
gbm.raw as egl::NativeDisplayType,
|
||||||
&[egl::ATTRIB_NONE],
|
&[egl::ATTRIB_NONE],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -736,8 +847,7 @@ impl EglImporter {
|
|||||||
vk: None,
|
vk: None,
|
||||||
linear_pool: None,
|
linear_pool: None,
|
||||||
linear_nv12_pool: None,
|
linear_nv12_pool: None,
|
||||||
gbm,
|
_gbm: gbm,
|
||||||
render_fd,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1155,23 +1265,11 @@ impl EglImporter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for EglImporter {
|
// No `Drop` impl on `EglImporter` — deliberately. `Drop::drop` runs BEFORE field drops, so the
|
||||||
fn drop(&mut self) {
|
// old impl destroyed the gbm device and closed the render-node fd while the blits' destructors
|
||||||
if !self.gbm.is_null() {
|
// (which call `cuGraphicsUnregisterResource`/`glDeleteTextures` into the driver) still had to
|
||||||
// SAFETY: `self.gbm` is the non-null `gbm_device*` from `gbm_create_device` in `new`
|
// run. With teardown expressed purely as field order (blits and bridge first, `GbmDevice` last —
|
||||||
// (checked non-null here), owned exclusively by this `EglImporter` and destroyed exactly
|
// see the field comments), the driver objects always release against a live native display.
|
||||||
// once (in `Drop`). It is freed BEFORE `render_fd` is closed below — the correct order,
|
|
||||||
// since the device borrowed that fd for its lifetime.
|
|
||||||
unsafe { gbm_device_destroy(self.gbm) };
|
|
||||||
}
|
|
||||||
if self.render_fd >= 0 {
|
|
||||||
// SAFETY: `self.render_fd` is the fd `open` returned in `new` (checked `>= 0`), owned
|
|
||||||
// exclusively by this `EglImporter`; this `close` runs exactly once, after the gbm device
|
|
||||||
// that borrowed it has been destroyed. No double-close or use-after-close.
|
|
||||||
unsafe { libc::close(self.render_fd) };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|||||||
@@ -166,9 +166,22 @@ pub(crate) unsafe fn compile_shader(kind: u32, src: &[u8]) -> Result<u32> {
|
|||||||
/// Compile+link the fullscreen-triangle program with fragment source `frag` and bind its `image`
|
/// Compile+link the fullscreen-triangle program with fragment source `frag` and bind its `image`
|
||||||
/// sampler to texture unit 0.
|
/// sampler to texture unit 0.
|
||||||
pub(crate) unsafe fn compile_program_with(frag: &[u8]) -> Result<u32> {
|
pub(crate) unsafe fn compile_program_with(frag: &[u8]) -> Result<u32> {
|
||||||
|
// Callers retry per frame, so every error path below must delete what it created — a leak
|
||||||
|
// here is unbounded, not one-shot.
|
||||||
let vs = compile_shader(GL_VERTEX_SHADER, VERT_SRC)?;
|
let vs = compile_shader(GL_VERTEX_SHADER, VERT_SRC)?;
|
||||||
let fs = compile_shader(GL_FRAGMENT_SHADER, frag)?;
|
let fs = match compile_shader(GL_FRAGMENT_SHADER, frag) {
|
||||||
|
Ok(fs) => fs,
|
||||||
|
Err(e) => {
|
||||||
|
glDeleteShader(vs);
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
let prog = glCreateProgram();
|
let prog = glCreateProgram();
|
||||||
|
if prog == 0 {
|
||||||
|
glDeleteShader(vs);
|
||||||
|
glDeleteShader(fs);
|
||||||
|
bail!("glCreateProgram failed");
|
||||||
|
}
|
||||||
glAttachShader(prog, vs);
|
glAttachShader(prog, vs);
|
||||||
glAttachShader(prog, fs);
|
glAttachShader(prog, fs);
|
||||||
glLinkProgram(prog);
|
glLinkProgram(prog);
|
||||||
@@ -176,7 +189,10 @@ pub(crate) unsafe fn compile_program_with(frag: &[u8]) -> Result<u32> {
|
|||||||
glDeleteShader(fs);
|
glDeleteShader(fs);
|
||||||
let mut ok: c_int = 0;
|
let mut ok: c_int = 0;
|
||||||
glGetProgramiv(prog, GL_LINK_STATUS, &mut ok);
|
glGetProgramiv(prog, GL_LINK_STATUS, &mut ok);
|
||||||
ensure!(ok != 0, "GL program link failed");
|
if ok == 0 {
|
||||||
|
glDeleteProgram(prog);
|
||||||
|
bail!("GL program link failed");
|
||||||
|
}
|
||||||
glUseProgram(prog);
|
glUseProgram(prog);
|
||||||
let loc = glGetUniformLocation(prog, c"image".as_ptr());
|
let loc = glGetUniformLocation(prog, c"image".as_ptr());
|
||||||
if loc >= 0 {
|
if loc >= 0 {
|
||||||
|
|||||||
Reference in New Issue
Block a user