feat(renderer): port the OpenGL backend, shaders and size math

Ports the terminal-independent half of ZIG/src/renderer (OpenGL only):
- opengl/*: buffers, frames, pipelines, render passes, samplers,
  targets, textures and the OpenGL GraphicsAPI backend on top of glow;
  the GTK GLArea glue is replaced by a get_proc_address loader closure
- the GLSL shaders, copied verbatim and embedded; uniform/vertex structs
  as bytemuck Pod types with std140 offsets asserted in tests
- shadertoy custom shaders converted with naga (GLSL in -> validate ->
  GLSL 430 out) instead of glslang + spirv-cross
- size (screen/grid/cell/padding math), cursor style logic, backend
  health, options and message types

The crate opts into unsafe_code = "deny"; unsafe glow calls live only in
opengl/gl.rs. A headless EGL integration test builds every pipeline and
the converted shadertoy shaders on Mesa and reads back rendered pixels.

Note: Zig's shadertoy Uniforms order the selection background/foreground
colors opposite to the GLSL block (likely an upstream bug); kept as in
Zig for parity and recorded in the notes.

Deferred to the next renderer pass: generic, cell, image, link, row,
State, Thread, Overlay (need terminal render state).

Test Plan:
- cargo test -p ghostty-renderer (33 tests incl. tests/egl_headless.rs)
This commit is contained in:
ddidderr committed 2026-10-11 22:26:09 +02:00
1 parent ac0e05def1
commit c548e63bd1
35 files changed
+5477 -5

No files matched your search

@@ -0,0 +1,160 @@
//! Wrapper for handling render pipelines (`opengl/Pipeline.zig`).
use super::gl::{
self, AttribType, FramebufferTarget, Gl, RawFramebuffer, RawProgram, RawVertexArray,
};
/// Vertex step function.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum StepFunction {
Constant,
#[default]
PerVertex,
PerInstance,
}
/// Options for initializing a render pipeline.
#[derive(Debug, Clone)]
pub struct Options<'a> {
/// GLSL source of the vertex function
pub vertex_fn: &'a str,
/// GLSL source of the fragment function
pub fragment_fn: &'a str,
/// Vertex step function
pub step_fn: StepFunction,
/// Whether to enable blending.
pub blending_enabled: bool,
}
impl<'a> Options<'a> {
pub fn new(vertex_fn: &'a str, fragment_fn: &'a str) -> Self {
Options {
vertex_fn,
fragment_fn,
step_fn: StepFunction::PerVertex,
blending_enabled: true,
}
}
}
/// One vertex attribute of a vertex struct (what Zig derives by comptime
/// reflection over the struct fields in `autoAttribute`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Attribute {
/// Number of components (1 for scalars, N for `[T; N]`).
pub size: i32,
/// Component type.
pub ty: AttribType,
/// Byte offset within the vertex struct.
pub offset: u32,
}
/// A vertex struct whose fields map 1:1 to vertex attribute locations
/// 0, 1, 2, ... in declaration order.
pub trait Vertex: bytemuck::Pod {
const ATTRIBUTES: &'static [Attribute];
}
pub struct Pipeline {
gl: Gl,
pub program: RawProgram,
pub fbo: RawFramebuffer,
pub vao: RawVertexArray,
pub stride: usize,
pub blending_enabled: bool,
}
impl Pipeline {
/// Build a pipeline with vertex attributes derived from `V`
/// (`Pipeline.init(VertexAttributes, opts)`).
pub fn init<V: Vertex>(gl: &Gl, opts: &Options<'_>) -> gl::Result<Pipeline> {
Self::init_impl(gl, Some((V::ATTRIBUTES, size_of::<V>())), opts)
}
/// Build a pipeline without any vertex attributes
/// (`Pipeline.init(null, opts)`).
pub fn init_no_attributes(gl: &Gl, opts: &Options<'_>) -> gl::Result<Pipeline> {
Self::init_impl(gl, None, opts)
}
fn init_impl(
gl: &Gl,
attrs: Option<(&[Attribute], usize)>,
opts: &Options<'_>,
) -> gl::Result<Pipeline> {
// Load and compile our shaders.
let program = gl.create_program_vf(opts.vertex_fn, opts.fragment_fn)?;
let fbo = match gl.create_framebuffer() {
Ok(f) => f,
Err(e) => {
gl.delete_program(program);
return Err(e);
}
};
let vao = match gl.create_vertex_array() {
Ok(v) => v,
Err(e) => {
gl.delete_framebuffer(fbo);
gl.delete_program(program);
return Err(e);
}
};
// From here on, dropping `this` releases everything on error.
let this = Pipeline {
gl: gl.clone(),
program,
fbo,
vao,
stride: attrs.map_or(0, |(_, stride)| stride),
blending_enabled: opts.blending_enabled,
};
let r = (|| -> gl::Result<()> {
gl.use_program(Some(program))?;
let fbobind = gl.bind_framebuffer(fbo, FramebufferTarget::Framebuffer);
let r = (|| {
gl.bind_vertex_array(Some(vao))?;
let r = match attrs {
Some((attrs, _)) => auto_attribute(gl, attrs, opts.step_fn),
None => Ok(()),
};
let _ = gl.bind_vertex_array(None);
r
})();
gl.unbind_framebuffer(fbobind);
let _ = gl.use_program(None);
r
})();
r?;
Ok(this)
}
}
impl Drop for Pipeline {
fn drop(&mut self) {
self.gl.delete_program(self.program);
self.gl.delete_framebuffer(self.fbo);
self.gl.delete_vertex_array(self.vao);
}
}
fn auto_attribute(gl: &Gl, attrs: &[Attribute], step_fn: StepFunction) -> gl::Result<()> {
let divisor: u32 = match step_fn {
StepFunction::PerVertex => 0,
StepFunction::PerInstance => 1,
StepFunction::Constant => u32::MAX,
};
for (i, a) in attrs.iter().enumerate() {
gl.setup_vertex_attrib(i as u32, 0, divisor, a.size, a.ty, a.offset)?;
}
Ok(())
}