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,134 @@
//! OpenGL data storage for a certain set of equal types (`opengl/buffer.zig`).
use std::marker::PhantomData;
use bytemuck::Pod;
use super::gl::{self, BufferTarget, BufferUsage, Gl, RawBuffer};
/// Options for initializing a buffer.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Options {
pub target: BufferTarget,
pub usage: BufferUsage,
}
impl Default for Options {
fn default() -> Self {
Options {
target: BufferTarget::Array,
usage: BufferUsage::DynamicDraw,
}
}
}
/// OpenGL data storage for a certain set of equal types. This is usually
/// used for vertex buffers, etc. This helpful wrapper makes it easy to
/// prealloc, shrink, grow, sync, buffers with OpenGL.
pub struct Buffer<T: Pod> {
gl: Gl,
/// Underlying GL buffer object.
handle: RawBuffer,
/// Options this buffer was allocated with.
pub opts: Options,
/// Current allocated length of the data store.
/// Note this is the number of `T`s, not the size in bytes.
pub len: usize,
_marker: PhantomData<T>,
}
impl<T: Pod> Buffer<T> {
/// The raw GL buffer handle (for passing to render pass steps).
pub fn raw(&self) -> RawBuffer {
self.handle
}
/// Initialize a buffer with the given length pre-allocated.
pub fn init(gl: &Gl, opts: Options, len: usize) -> gl::Result<Self> {
let buffer = gl.create_buffer()?;
let this = Buffer {
gl: gl.clone(),
handle: buffer,
opts,
len,
_marker: PhantomData,
};
// On error `this` is dropped, which destroys the buffer.
gl.bind_buffer(opts.target, Some(buffer));
let r = gl.buffer_data_size(opts.target, len * size_of::<T>(), opts.usage);
gl.bind_buffer(opts.target, None);
r?;
Ok(this)
}
/// Init the buffer filled with the given data.
pub fn init_fill(gl: &Gl, opts: Options, data: &[T]) -> gl::Result<Self> {
let buffer = gl.create_buffer()?;
// NOTE: Zig sets `len = data.len * @sizeOf(T)` here, which is a bug
// (len is documented as a count of `T`s); we store the element count.
let this = Buffer {
gl: gl.clone(),
handle: buffer,
opts,
len: data.len(),
_marker: PhantomData,
};
gl.bind_buffer(opts.target, Some(buffer));
let r = gl.buffer_data(opts.target, bytemuck::cast_slice(data), opts.usage);
gl.bind_buffer(opts.target, None);
r?;
Ok(this)
}
/// Sync new contents to the buffer. The data is expected to be the
/// complete contents of the buffer. If the amount of data is larger
/// than the buffer length, the buffer will be reallocated.
///
/// If the amount of data is smaller than the buffer length, the
/// remaining data in the buffer is left untouched.
pub fn sync(&mut self, data: &[T]) -> gl::Result<()> {
self.sync_from_slices(&[data]).map(|_| ())
}
/// Like `Buffer::sync` but takes data from an array of slices, rather
/// than a single slice. Returns the number of items synced.
pub fn sync_from_slices(&mut self, lists: &[&[T]]) -> gl::Result<usize> {
let target = self.opts.target;
self.gl.bind_buffer(target, Some(self.handle));
let r = self.sync_bound(lists);
self.gl.bind_buffer(target, None);
r
}
fn sync_bound(&mut self, lists: &[&[T]]) -> gl::Result<usize> {
let target = self.opts.target;
let total_len: usize = lists.iter().map(|l| l.len()).sum();
// If we need more space than our buffer has, we need to reallocate.
if total_len > self.len {
// Reallocate the buffer to hold double what we require.
self.len = total_len * 2;
self.gl
.buffer_data_size(target, self.len * size_of::<T>(), self.opts.usage)?;
}
// We can fit within the buffer so we can just replace bytes.
let mut offset = 0;
for list in lists {
self.gl
.buffer_sub_data(target, offset, bytemuck::cast_slice(list))?;
offset += size_of_val(*list);
}
Ok(total_len)
}
}
impl<T: Pod> Drop for Buffer<T> {
fn drop(&mut self) {
self.gl.delete_buffer(self.handle);
}
}