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

+844
View File
@@ -0,0 +1,844 @@
//! Thin safe wrapper over `glow` (the Rust equivalent of Ghostty's
//! `pkg/opengl`).
//!
//! This is the only module in the crate that contains `unsafe` code: every
//! `glow::HasContext` method is an `unsafe fn` because GL calls are only valid
//! with a current context on the calling thread. The invariants are
//! documented once here:
//!
//! * A [`Gl`] is only constructed from a context the caller asserts is (or
//! will be) current on the thread that uses it (`Gl::from_loader`).
//! * All object handles are created from, and only ever passed back to, the
//! same `Gl`.
//! * Pointer-carrying calls (`buffer_data`, `tex_image_2d`, ...) are only given
//! Rust slices, so lengths are always valid for the pointed memory.
use std::ffi::c_void;
use std::sync::Arc;
use glow::HasContext;
/// The GL error enum (`pkg/opengl/errors.zig`), plus shader build errors.
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum Error {
#[error("GL_INVALID_ENUM")]
InvalidEnum,
#[error("GL_INVALID_VALUE")]
InvalidValue,
#[error("GL_INVALID_OPERATION")]
InvalidOperation,
#[error("GL_INVALID_FRAMEBUFFER_OPERATION")]
InvalidFramebufferOperation,
#[error("GL_OUT_OF_MEMORY")]
OutOfMemory,
#[error("unknown GL error {0:#x}")]
Unknown(u32),
/// Shader compilation or program link failure, with the driver info log.
#[error("shader compile/link failed: {0}")]
CompileFailed(String),
/// Creating an object failed (glGen*/glCreate* returned an error).
#[error("failed to create GL object: {0}")]
CreateFailed(String),
}
pub type Result<T, E = Error> = std::result::Result<T, E>;
/// Handle to a loaded GL context. Cheap to clone (shared).
#[derive(Clone)]
pub struct Gl {
ctx: Arc<glow::Context>,
}
impl std::fmt::Debug for Gl {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Gl").finish_non_exhaustive()
}
}
// Re-exported raw handle types.
pub use glow::{
Buffer as RawBuffer, Framebuffer as RawFramebuffer, Program as RawProgram,
Renderbuffer as RawRenderbuffer, Sampler as RawSampler, Texture as RawTexture,
VertexArray as RawVertexArray,
};
/// Enum for buffer binding targets (`gl.Buffer.Target`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum BufferTarget {
Array = glow::ARRAY_BUFFER,
ElementArray = glow::ELEMENT_ARRAY_BUFFER,
Uniform = glow::UNIFORM_BUFFER,
Storage = glow::SHADER_STORAGE_BUFFER,
}
/// Buffer usage hints (`gl.Buffer.Usage`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum BufferUsage {
StreamDraw = glow::STREAM_DRAW,
StreamRead = glow::STREAM_READ,
StreamCopy = glow::STREAM_COPY,
StaticDraw = glow::STATIC_DRAW,
StaticRead = glow::STATIC_READ,
StaticCopy = glow::STATIC_COPY,
DynamicDraw = glow::DYNAMIC_DRAW,
DynamicRead = glow::DYNAMIC_READ,
DynamicCopy = glow::DYNAMIC_COPY,
}
/// Texture binding targets (`gl.Texture.Target`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum TextureTarget {
D1 = glow::TEXTURE_1D,
D2 = glow::TEXTURE_2D,
D3 = glow::TEXTURE_3D,
D1Array = glow::TEXTURE_1D_ARRAY,
D2Array = glow::TEXTURE_2D_ARRAY,
Rectangle = glow::TEXTURE_RECTANGLE,
CubeMap = glow::TEXTURE_CUBE_MAP,
Buffer = glow::TEXTURE_BUFFER,
D2Multisample = glow::TEXTURE_2D_MULTISAMPLE,
D2MultisampleArray = glow::TEXTURE_2D_MULTISAMPLE_ARRAY,
}
/// Internal format enum for texture images (`gl.Texture.InternalFormat`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum InternalFormat {
Red = glow::RED,
Rgb = glow::RGB8,
Rgba = glow::RGBA8,
Srgb = glow::SRGB8,
Srgba = glow::SRGB8_ALPHA8,
RgbaCompressed = glow::COMPRESSED_RGBA_BPTC_UNORM,
SrgbaCompressed = glow::COMPRESSED_SRGB_ALPHA_BPTC_UNORM,
}
/// Format for texture images (`gl.Texture.Format`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum PixelFormat {
Red = glow::RED,
Rgb = glow::RGB,
Rgba = glow::RGBA,
Bgra = glow::BGRA,
}
/// Minification filter for textures.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum MinFilter {
Nearest = glow::NEAREST as i32,
Linear = glow::LINEAR as i32,
NearestMipmapNearest = glow::NEAREST_MIPMAP_NEAREST as i32,
LinearMipmapNearest = glow::LINEAR_MIPMAP_NEAREST as i32,
NearestMipmapLinear = glow::NEAREST_MIPMAP_LINEAR as i32,
LinearMipmapLinear = glow::LINEAR_MIPMAP_LINEAR as i32,
}
/// Magnification filter for textures.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum MagFilter {
Nearest = glow::NEAREST as i32,
Linear = glow::LINEAR as i32,
}
/// Texture coordinate wrapping mode.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum Wrap {
ClampToEdge = glow::CLAMP_TO_EDGE as i32,
ClampToBorder = glow::CLAMP_TO_BORDER as i32,
MirroredRepeat = glow::MIRRORED_REPEAT as i32,
Repeat = glow::REPEAT as i32,
}
/// Primitive types we support drawing (same subset as Metal).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum Primitive {
Point = glow::POINTS,
Line = glow::LINES,
LineStrip = glow::LINE_STRIP,
Triangle = glow::TRIANGLES,
TriangleStrip = glow::TRIANGLE_STRIP,
}
/// Framebuffer binding targets.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum FramebufferTarget {
Framebuffer = glow::FRAMEBUFFER,
Draw = glow::DRAW_FRAMEBUFFER,
Read = glow::READ_FRAMEBUFFER,
}
/// Framebuffer attachment points.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum Attachment {
Color0 = glow::COLOR_ATTACHMENT0,
Depth = glow::DEPTH_ATTACHMENT,
Stencil = glow::STENCIL_ATTACHMENT,
DepthStencil = glow::DEPTH_STENCIL_ATTACHMENT,
}
/// A previous framebuffer binding, restored by [`Gl::unbind_framebuffer`].
#[derive(Debug, Clone, Copy)]
pub struct FramebufferBinding {
pub target: FramebufferTarget,
previous: Option<RawFramebuffer>,
}
/// Component type of a vertex attribute.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AttribType {
U8,
U16,
U32,
I8,
I16,
I32,
F16,
F32,
F64,
}
impl Gl {
/// Load GL function pointers with `loader` (e.g. `eglGetProcAddress`, or
/// for GTK's GLArea the global loader) and wrap the resulting context.
///
/// `loader` receives GL symbol names and returns the function address or
/// null. The GL context must be current on this thread and must stay
/// current (on whichever thread uses this `Gl`) while it is used.
pub fn from_loader<F>(mut loader: F) -> Gl
where
F: FnMut(&str) -> *const c_void,
{
// SAFETY: glow documents that the loader must return valid function
// pointers for the current context (or null); the caller supplies a
// real GL loader. The returned context is only used with that context
// current.
let ctx = unsafe { glow::Context::from_loader_function(|s| loader(s)) };
Gl { ctx: Arc::new(ctx) }
}
/// Like [`Gl::from_loader`] but also installs Ghostty's debug-message
/// callback (`glDebugMessageCallback`) on the context.
pub fn from_loader_with_debug<F>(mut loader: F) -> Gl
where
F: FnMut(&str) -> *const c_void,
{
// SAFETY: see `from_loader`.
let mut ctx = unsafe { glow::Context::from_loader_function(|s| loader(s)) };
// SAFETY: the context is current (caller contract); the callback is a
// plain Rust closure with no captured state.
unsafe {
ctx.debug_message_callback(debug_message);
}
Gl { ctx: Arc::new(ctx) }
}
/// The (major, minor) of the loaded GL context.
pub fn version(&self) -> (u32, u32) {
let v = self.ctx.version();
(v.major, v.minor)
}
/// Raw `glow` context, for operations the wrapper doesn't cover.
pub fn raw(&self) -> &glow::Context {
&self.ctx
}
/// `glGetError`: the error (if any) from the last OpenGL operation.
pub fn get_error(&self) -> Result<()> {
// SAFETY: see module docs.
let e = unsafe { self.ctx.get_error() };
match e {
glow::NO_ERROR => Ok(()),
glow::INVALID_ENUM => Err(Error::InvalidEnum),
glow::INVALID_VALUE => Err(Error::InvalidValue),
glow::INVALID_OPERATION => Err(Error::InvalidOperation),
glow::INVALID_FRAMEBUFFER_OPERATION => Err(Error::InvalidFramebufferOperation),
glow::OUT_OF_MEMORY => Err(Error::OutOfMemory),
other => Err(Error::Unknown(other)),
}
}
/// Enable a capability (`glEnable`).
pub fn enable(&self, cap: u32) -> Result<()> {
// SAFETY: see module docs.
unsafe { self.ctx.enable(cap) };
self.get_error()
}
/// Disable a capability (`glDisable`).
pub fn disable(&self, cap: u32) -> Result<()> {
// SAFETY: see module docs.
unsafe { self.ctx.disable(cap) };
self.get_error()
}
pub fn blend_func(&self, src: u32, dst: u32) -> Result<()> {
// SAFETY: see module docs.
unsafe { self.ctx.blend_func(src, dst) };
self.get_error()
}
pub fn viewport(&self, x: i32, y: i32, w: i32, h: i32) -> Result<()> {
// SAFETY: see module docs.
unsafe { self.ctx.viewport(x, y, w, h) };
self.get_error()
}
/// Returns the current viewport `[x, y, w, h]`.
pub fn get_viewport(&self) -> [i32; 4] {
let mut out = [0i32; 4];
// SAFETY: GL_VIEWPORT writes exactly four ints into the slice.
unsafe { self.ctx.get_parameter_i32_slice(glow::VIEWPORT, &mut out) };
out
}
pub fn pixel_store_i32(&self, mode: u32, value: i32) -> Result<()> {
// SAFETY: see module docs.
unsafe { self.ctx.pixel_store_i32(mode, value) };
self.get_error()
}
pub fn clear_color(&self, r: f32, g: f32, b: f32, a: f32) {
// SAFETY: see module docs.
unsafe { self.ctx.clear_color(r, g, b, a) };
}
pub fn clear(&self, mask: u32) {
// SAFETY: see module docs.
unsafe { self.ctx.clear(mask) };
}
pub fn finish(&self) {
// SAFETY: see module docs.
unsafe { self.ctx.finish() };
}
pub fn flush(&self) {
// SAFETY: see module docs.
unsafe { self.ctx.flush() };
}
pub fn draw_arrays_instanced(
&self,
mode: Primitive,
first: i32,
count: i32,
instances: i32,
) -> Result<()> {
// SAFETY: see module docs; vertex data comes from bound GL buffers.
unsafe {
self.ctx
.draw_arrays_instanced(mode as u32, first, count, instances);
}
self.get_error()
}
/// `glBlitFramebuffer` with the currently bound read/draw framebuffers.
#[allow(clippy::too_many_arguments)]
pub fn blit_framebuffer(
&self,
src: (i32, i32, i32, i32),
dst: (i32, i32, i32, i32),
mask: u32,
filter: u32,
) {
// SAFETY: see module docs.
unsafe {
self.ctx.blit_framebuffer(
src.0, src.1, src.2, src.3, dst.0, dst.1, dst.2, dst.3, mask, filter,
);
}
}
/// Read RGBA8 pixels from the currently bound read framebuffer.
pub fn read_pixels_rgba8(&self, x: i32, y: i32, w: i32, h: i32) -> Vec<u8> {
let mut out = vec![0u8; (w.max(0) as usize) * (h.max(0) as usize) * 4];
// SAFETY: `out` is exactly w*h*4 bytes, which is what RGBA/UNSIGNED_BYTE
// reads at the default pack alignment of 4 (rows are a multiple of 4).
unsafe {
self.ctx.read_pixels(
x,
y,
w,
h,
glow::RGBA,
glow::UNSIGNED_BYTE,
glow::PixelPackData::Slice(Some(&mut out)),
);
}
out
}
// ------------------------------------------------------------ buffers
pub fn create_buffer(&self) -> Result<RawBuffer> {
// SAFETY: see module docs.
unsafe { self.ctx.create_buffer() }.map_err(Error::CreateFailed)
}
pub fn delete_buffer(&self, b: RawBuffer) {
// SAFETY: see module docs.
unsafe { self.ctx.delete_buffer(b) };
}
pub fn bind_buffer(&self, target: BufferTarget, b: Option<RawBuffer>) {
// SAFETY: see module docs.
unsafe { self.ctx.bind_buffer(target as u32, b) };
}
pub fn bind_buffer_base(&self, target: BufferTarget, idx: u32, b: RawBuffer) -> Result<()> {
// SAFETY: see module docs.
unsafe { self.ctx.bind_buffer_base(target as u32, idx, Some(b)) };
self.get_error()
}
/// `glBufferData` with a null pointer: allocate `size` bytes.
pub fn buffer_data_size(
&self,
target: BufferTarget,
size: usize,
usage: BufferUsage,
) -> Result<()> {
// SAFETY: see module docs.
unsafe {
self.ctx
.buffer_data_size(target as u32, size as i32, usage as u32);
};
self.get_error()
}
pub fn buffer_data(&self, target: BufferTarget, data: &[u8], usage: BufferUsage) -> Result<()> {
// SAFETY: slice pointer/len are valid.
unsafe {
self.ctx
.buffer_data_u8_slice(target as u32, data, usage as u32);
};
self.get_error()
}
pub fn buffer_sub_data(&self, target: BufferTarget, offset: usize, data: &[u8]) -> Result<()> {
// SAFETY: slice pointer/len are valid.
unsafe {
self.ctx
.buffer_sub_data_u8_slice(target as u32, offset as i32, data);
}
self.get_error()
}
// ----------------------------------------------------------- programs
/// Create a program from vertex and fragment shader source: compile both,
/// attach and link (`Program.createVF`).
pub fn create_program_vf(&self, vsrc: &str, fsrc: &str) -> Result<RawProgram> {
let gl = &*self.ctx;
let compile = |ty: u32, src: &str| -> Result<glow::Shader> {
// SAFETY: see module docs.
unsafe {
let s = gl.create_shader(ty).map_err(Error::CreateFailed)?;
gl.shader_source(s, src);
gl.compile_shader(s);
if gl.get_shader_compile_status(s) {
return Ok(s);
}
let log = gl.get_shader_info_log(s);
log::error!("shader compilation failure id={s:?} message={log}");
gl.delete_shader(s);
Err(Error::CompileFailed(log))
}
};
let vs = compile(glow::VERTEX_SHADER, vsrc)?;
let fs = match compile(glow::FRAGMENT_SHADER, fsrc) {
Ok(fs) => fs,
Err(e) => {
// SAFETY: see module docs.
unsafe { gl.delete_shader(vs) };
return Err(e);
}
};
// SAFETY: see module docs.
unsafe {
let result = (|| {
let p = gl.create_program().map_err(Error::CreateFailed)?;
gl.attach_shader(p, vs);
gl.attach_shader(p, fs);
gl.link_program(p);
if gl.get_program_link_status(p) {
return Ok(p);
}
let log = gl.get_program_info_log(p);
log::error!("program link failure id={p:?} message={log}");
gl.delete_program(p);
Err(Error::CompileFailed(log))
})();
gl.delete_shader(vs);
gl.delete_shader(fs);
result
}
}
pub fn use_program(&self, p: Option<RawProgram>) -> Result<()> {
// SAFETY: see module docs.
unsafe { self.ctx.use_program(p) };
self.get_error()
}
pub fn delete_program(&self, p: RawProgram) {
// SAFETY: see module docs.
unsafe { self.ctx.delete_program(p) };
}
// ------------------------------------------------------ vertex arrays
pub fn create_vertex_array(&self) -> Result<RawVertexArray> {
// SAFETY: see module docs.
unsafe { self.ctx.create_vertex_array() }.map_err(Error::CreateFailed)
}
pub fn delete_vertex_array(&self, v: RawVertexArray) {
// SAFETY: see module docs.
unsafe { self.ctx.delete_vertex_array(v) };
}
pub fn bind_vertex_array(&self, v: Option<RawVertexArray>) -> Result<()> {
// SAFETY: see module docs.
unsafe { self.ctx.bind_vertex_array(v) };
self.get_error()
}
/// Configure attribute `idx` on the currently bound VAO (`VertexArray.Binding`
/// enableAttribArray + attributeBinding + bindingDivisor + attribute*Format).
pub fn setup_vertex_attrib(
&self,
idx: u32,
binding: u32,
divisor: u32,
size: i32,
ty: AttribType,
offset: u32,
) -> Result<()> {
let gl = &*self.ctx;
// SAFETY: see module docs.
unsafe {
gl.enable_vertex_attrib_array(idx);
self.get_error()?;
gl.vertex_attrib_binding(idx, binding);
self.get_error()?;
gl.vertex_binding_divisor(binding, divisor);
self.get_error()?;
match ty {
AttribType::U8 => {
gl.vertex_attrib_format_i32(idx, size, glow::UNSIGNED_BYTE, offset);
}
AttribType::U16 => {
gl.vertex_attrib_format_i32(idx, size, glow::UNSIGNED_SHORT, offset);
}
AttribType::U32 => {
gl.vertex_attrib_format_i32(idx, size, glow::UNSIGNED_INT, offset);
}
AttribType::I8 => gl.vertex_attrib_format_i32(idx, size, glow::BYTE, offset),
AttribType::I16 => gl.vertex_attrib_format_i32(idx, size, glow::SHORT, offset),
AttribType::I32 => gl.vertex_attrib_format_i32(idx, size, glow::INT, offset),
AttribType::F16 => {
gl.vertex_attrib_format_f32(idx, size, glow::HALF_FLOAT, false, offset);
}
AttribType::F32 => {
gl.vertex_attrib_format_f32(idx, size, glow::FLOAT, false, offset);
}
AttribType::F64 => gl.vertex_attrib_format_f64(idx, size, glow::DOUBLE, offset),
}
}
self.get_error()
}
/// `glBindVertexBuffer` on the currently bound VAO.
pub fn bind_vertex_buffer(
&self,
idx: u32,
b: RawBuffer,
offset: i32,
stride: i32,
) -> Result<()> {
// SAFETY: see module docs.
unsafe { self.ctx.bind_vertex_buffer(idx, Some(b), offset, stride) };
self.get_error()
}
// ------------------------------------------------------- framebuffers
pub fn create_framebuffer(&self) -> Result<RawFramebuffer> {
// SAFETY: see module docs.
unsafe { self.ctx.create_framebuffer() }.map_err(Error::CreateFailed)
}
pub fn delete_framebuffer(&self, f: RawFramebuffer) {
// SAFETY: see module docs.
unsafe { self.ctx.delete_framebuffer(f) };
}
/// Bind a framebuffer, remembering what was bound before (the default
/// framebuffer isn't always zero, so it's queried at runtime).
pub fn bind_framebuffer(
&self,
f: RawFramebuffer,
target: FramebufferTarget,
) -> FramebufferBinding {
// SAFETY: see module docs.
unsafe {
let current = self.ctx.get_parameter_i32(glow::FRAMEBUFFER_BINDING);
self.ctx.bind_framebuffer(target as u32, Some(f));
FramebufferBinding {
target,
previous: u32::try_from(current)
.ok()
.and_then(std::num::NonZeroU32::new)
.map(glow::NativeFramebuffer),
}
}
}
/// Restore the framebuffer binding that was current before `bind_framebuffer`.
pub fn unbind_framebuffer(&self, b: FramebufferBinding) {
// SAFETY: see module docs.
unsafe { self.ctx.bind_framebuffer(b.target as u32, b.previous) };
}
pub fn framebuffer_texture_2d(
&self,
b: &FramebufferBinding,
attachment: Attachment,
textarget: TextureTarget,
texture: RawTexture,
level: i32,
) -> Result<()> {
// SAFETY: see module docs.
unsafe {
self.ctx.framebuffer_texture_2d(
b.target as u32,
attachment as u32,
textarget as u32,
Some(texture),
level,
);
}
self.get_error()
}
pub fn framebuffer_renderbuffer(
&self,
b: &FramebufferBinding,
attachment: Attachment,
rb: RawRenderbuffer,
) -> Result<()> {
// SAFETY: see module docs.
unsafe {
self.ctx.framebuffer_renderbuffer(
b.target as u32,
attachment as u32,
glow::RENDERBUFFER,
Some(rb),
);
}
self.get_error()
}
/// Returns true if the currently bound framebuffer (at `target`) is complete.
pub fn framebuffer_complete(&self, target: FramebufferTarget) -> bool {
// SAFETY: see module docs.
unsafe { self.ctx.check_framebuffer_status(target as u32) == glow::FRAMEBUFFER_COMPLETE }
}
pub fn create_renderbuffer(&self) -> Result<RawRenderbuffer> {
// SAFETY: see module docs.
unsafe { self.ctx.create_renderbuffer() }.map_err(Error::CreateFailed)
}
pub fn delete_renderbuffer(&self, r: RawRenderbuffer) {
// SAFETY: see module docs.
unsafe { self.ctx.delete_renderbuffer(r) };
}
pub fn bind_renderbuffer(&self, r: Option<RawRenderbuffer>) {
// SAFETY: see module docs.
unsafe { self.ctx.bind_renderbuffer(glow::RENDERBUFFER, r) };
}
/// `glRenderbufferStorage` on the currently bound renderbuffer.
pub fn renderbuffer_storage(&self, format: InternalFormat, w: i32, h: i32) -> Result<()> {
// SAFETY: see module docs.
unsafe {
self.ctx
.renderbuffer_storage(glow::RENDERBUFFER, format as u32, w, h);
};
self.get_error()
}
// ----------------------------------------------------------- textures
pub fn create_texture(&self) -> Result<RawTexture> {
// SAFETY: see module docs.
unsafe { self.ctx.create_texture() }.map_err(Error::CreateFailed)
}
pub fn delete_texture(&self, t: RawTexture) {
// SAFETY: see module docs.
unsafe { self.ctx.delete_texture(t) };
}
pub fn bind_texture(&self, target: TextureTarget, t: Option<RawTexture>) -> Result<()> {
// SAFETY: see module docs.
unsafe { self.ctx.bind_texture(target as u32, t) };
self.get_error()
}
/// `glActiveTexture(GL_TEXTURE0 + index)`.
pub fn active_texture(&self, index: u32) -> Result<()> {
// SAFETY: see module docs.
unsafe { self.ctx.active_texture(glow::TEXTURE0 + index) };
self.get_error()
}
pub fn tex_parameter_i32(&self, target: TextureTarget, name: u32, value: i32) -> Result<()> {
// SAFETY: see module docs.
unsafe { self.ctx.tex_parameter_i32(target as u32, name, value) };
self.get_error()
}
#[allow(clippy::too_many_arguments)]
/// `glTexImage2D` (level, border 0, `GL_UNSIGNED_BYTE`) on the bound texture.
pub fn tex_image_2d(
&self,
target: TextureTarget,
level: i32,
internal: InternalFormat,
width: i32,
height: i32,
format: PixelFormat,
data: Option<&[u8]>,
) -> Result<()> {
// SAFETY: the slice (if any) is a valid byte slice; as in Ghostty the
// caller is responsible for it being at least width*height*bpp bytes.
unsafe {
self.ctx.tex_image_2d(
target as u32,
level,
internal as i32,
width,
height,
0,
format as u32,
glow::UNSIGNED_BYTE,
glow::PixelUnpackData::Slice(data),
);
}
self.get_error()
}
#[allow(clippy::too_many_arguments)]
/// `glTexSubImage2D` (`GL_UNSIGNED_BYTE`) on the bound texture.
pub fn tex_sub_image_2d(
&self,
target: TextureTarget,
level: i32,
x: i32,
y: i32,
width: i32,
height: i32,
format: PixelFormat,
data: &[u8],
) -> Result<()> {
// SAFETY: see `tex_image_2d`.
unsafe {
self.ctx.tex_sub_image_2d(
target as u32,
level,
x,
y,
width,
height,
format as u32,
glow::UNSIGNED_BYTE,
glow::PixelUnpackData::Slice(Some(data)),
);
}
self.get_error()
}
// ----------------------------------------------------------- samplers
pub fn create_sampler(&self) -> Result<RawSampler> {
// SAFETY: see module docs.
unsafe { self.ctx.create_sampler() }.map_err(Error::CreateFailed)
}
pub fn delete_sampler(&self, s: RawSampler) {
// SAFETY: see module docs.
unsafe { self.ctx.delete_sampler(s) };
}
pub fn sampler_parameter_i32(&self, s: RawSampler, name: u32, value: i32) -> Result<()> {
// SAFETY: see module docs.
unsafe { self.ctx.sampler_parameter_i32(s, name, value) };
self.get_error()
}
pub fn bind_sampler(&self, unit: u32, s: RawSampler) -> Result<()> {
// SAFETY: see module docs.
unsafe { self.ctx.bind_sampler(unit, Some(s)) };
self.get_error()
}
}
/// Parameter names used by the textures/samplers (`gl.Texture.Parameter`).
pub mod param {
pub const MIN_FILTER: u32 = glow::TEXTURE_MIN_FILTER;
pub const MAG_FILTER: u32 = glow::TEXTURE_MAG_FILTER;
pub const WRAP_S: u32 = glow::TEXTURE_WRAP_S;
pub const WRAP_T: u32 = glow::TEXTURE_WRAP_T;
}
/// The GL debug message callback (`glDebugMessageCallback` in OpenGL.zig).
fn debug_message(src: u32, typ: u32, id: u32, severity: u32, msg: &str) {
let src_str = match src {
glow::DEBUG_SOURCE_API => "OpenGL API",
glow::DEBUG_SOURCE_WINDOW_SYSTEM => "Window System",
glow::DEBUG_SOURCE_SHADER_COMPILER => "Shader Compiler",
glow::DEBUG_SOURCE_THIRD_PARTY => "Third Party",
glow::DEBUG_SOURCE_APPLICATION => "User",
glow::DEBUG_SOURCE_OTHER => "Other",
_ => "Unknown",
};
let typ_str = match typ {
glow::DEBUG_TYPE_ERROR => "Error",
glow::DEBUG_TYPE_DEPRECATED_BEHAVIOR => "Deprecated Behavior",
glow::DEBUG_TYPE_UNDEFINED_BEHAVIOR => "Undefined Behavior",
glow::DEBUG_TYPE_PORTABILITY => "Portability Issue",
glow::DEBUG_TYPE_PERFORMANCE => "Performance Issue",
glow::DEBUG_TYPE_MARKER => "Marker",
glow::DEBUG_TYPE_PUSH_GROUP => "Group Push",
glow::DEBUG_TYPE_POP_GROUP => "Group Pop",
glow::DEBUG_TYPE_OTHER => "Other",
_ => "Unknown",
};
match severity {
glow::DEBUG_SEVERITY_HIGH => log::error!("[{id}] ({src_str}: {typ_str}) {msg}"),
glow::DEBUG_SEVERITY_MEDIUM => log::warn!("[{id}] ({src_str}: {typ_str}) {msg}"),
glow::DEBUG_SEVERITY_LOW => log::info!("[{id}] ({src_str}: {typ_str}) {msg}"),
glow::DEBUG_SEVERITY_NOTIFICATION => {
log::debug!("[{id}] ({src_str}: {typ_str}) {msg}");
}
_ => log::warn!("UNKNOWN SEVERITY [{id}] ({src_str}: {typ_str}) {msg}"),
}
}