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:
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
//! Wrapper for handling render frames (`opengl/Frame.zig`).
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::OpenGL;
|
||||
use super::render_pass::{Attachment, Options as RenderPassOptions, RenderPass};
|
||||
use super::target::Target;
|
||||
use crate::backend::Health;
|
||||
|
||||
/// Options for beginning a frame.
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct Options {}
|
||||
|
||||
/// A frame being encoded.
|
||||
///
|
||||
/// Upstream holds `*Renderer` and calls `renderer.frameCompleted(health)`
|
||||
/// after presenting; here the frame borrows the graphics API and
|
||||
/// [`Frame::complete`] returns the [`Health`] for the renderer to report.
|
||||
pub struct Frame<'a> {
|
||||
api: &'a mut OpenGL,
|
||||
target: Arc<Target>,
|
||||
}
|
||||
|
||||
impl<'a> Frame<'a> {
|
||||
/// Begin encoding a frame.
|
||||
///
|
||||
/// The target is presented via the provided API when completed.
|
||||
pub fn begin(_opts: Options, api: &'a mut OpenGL, target: Arc<Target>) -> Frame<'a> {
|
||||
Frame { api, target }
|
||||
}
|
||||
|
||||
/// Add a render pass to this frame with the provided attachments.
|
||||
/// Returns a RenderPass which allows render steps to be added.
|
||||
pub fn render_pass<'b>(&self, attachments: &'b [Attachment<'b>]) -> RenderPass<'b> {
|
||||
let gl = self.api.gl().expect("OpenGL context not initialized");
|
||||
RenderPass::begin(gl, RenderPassOptions { attachments })
|
||||
}
|
||||
|
||||
/// Complete this frame and present the target.
|
||||
///
|
||||
/// If `sync` is true, this will block until the frame is presented.
|
||||
///
|
||||
/// NOTE: For OpenGL, `sync` is ignored and we always block.
|
||||
pub fn complete(self, _sync: bool) -> Health {
|
||||
let Some(gl) = self.api.gl().cloned() else {
|
||||
return Health::Unhealthy;
|
||||
};
|
||||
gl.finish();
|
||||
|
||||
// If there are any GL errors, consider the frame unhealthy.
|
||||
let health = if gl.get_error().is_ok() {
|
||||
Health::Healthy
|
||||
} else {
|
||||
Health::Unhealthy
|
||||
};
|
||||
|
||||
// If the frame is healthy, present it.
|
||||
if health == Health::Healthy
|
||||
&& let Err(err) = self.api.present(&self.target)
|
||||
{
|
||||
log::error!("Failed to present render target: err={err}");
|
||||
}
|
||||
|
||||
// Report the health to the renderer.
|
||||
health
|
||||
}
|
||||
}
|
||||
@@ -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}"),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,412 @@
|
||||
//! Graphics API wrapper for OpenGL (`renderer/OpenGL.zig` and `renderer/opengl/*`).
|
||||
//!
|
||||
//! The context itself is owned by the apprt (a `GtkGLArea` for GTK). The
|
||||
//! renderer never creates a context: it is given a `get_proc_address` loader
|
||||
//! closure (see [`OpenGL::prepare_context`]) by the apprt once the context is
|
||||
//! current, and everything afterwards goes through the [`Gl`] handle.
|
||||
|
||||
pub mod buffer;
|
||||
pub mod frame;
|
||||
#[allow(unsafe_code)]
|
||||
pub mod gl;
|
||||
pub mod pipeline;
|
||||
pub mod render_pass;
|
||||
pub mod sampler;
|
||||
pub mod shaders;
|
||||
pub mod target;
|
||||
pub mod texture;
|
||||
|
||||
use std::ffi::c_void;
|
||||
use std::sync::Arc;
|
||||
|
||||
use ghostty_font::atlas::{Atlas, Format as AtlasFormat};
|
||||
|
||||
pub use self::buffer::Buffer;
|
||||
pub use self::frame::Frame;
|
||||
pub use self::gl::Gl;
|
||||
use self::gl::{
|
||||
BufferTarget, BufferUsage, FramebufferTarget, InternalFormat, MagFilter, MinFilter,
|
||||
PixelFormat, TextureTarget, Wrap,
|
||||
};
|
||||
pub use self::pipeline::Pipeline;
|
||||
pub use self::render_pass::RenderPass;
|
||||
pub use self::sampler::Sampler;
|
||||
pub use self::target::Target;
|
||||
pub use self::texture::Texture;
|
||||
use crate::shadertoy;
|
||||
|
||||
pub type GraphicsApi = OpenGL;
|
||||
|
||||
/// Custom shaders are converted to GLSL for OpenGL.
|
||||
pub const CUSTOM_SHADER_TARGET: shadertoy::Target = shadertoy::Target::Glsl;
|
||||
/// The fragCoord for OpenGL shaders is +Y = up.
|
||||
pub const CUSTOM_SHADER_Y_IS_DOWN: bool = false;
|
||||
|
||||
/// Because OpenGL's frame completion is always
|
||||
/// sync, we have no need for multi-buffering.
|
||||
pub const SWAP_CHAIN_COUNT: usize = 1;
|
||||
|
||||
/// We require at least OpenGL 4.3
|
||||
pub const MIN_VERSION_MAJOR: u32 = 4;
|
||||
pub const MIN_VERSION_MINOR: u32 = 3;
|
||||
|
||||
/// `config.AlphaBlending`.
|
||||
// TODO(integration): replace with ghostty_config's AlphaBlending.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum AlphaBlending {
|
||||
#[default]
|
||||
Native,
|
||||
Linear,
|
||||
LinearCorrected,
|
||||
}
|
||||
|
||||
impl AlphaBlending {
|
||||
pub fn is_linear(self) -> bool {
|
||||
match self {
|
||||
AlphaBlending::Native => false,
|
||||
AlphaBlending::Linear | AlphaBlending::LinearCorrected => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error(
|
||||
"OpenGL version is too old, Ghostty requires OpenGL {MIN_VERSION_MAJOR}.{MIN_VERSION_MINOR} (got {0}.{1})"
|
||||
)]
|
||||
OpenGLOutdated(u32, u32),
|
||||
#[error("OpenGL context has not been prepared yet")]
|
||||
NoContext,
|
||||
#[error(transparent)]
|
||||
Gl(#[from] gl::Error),
|
||||
#[error(transparent)]
|
||||
Texture(#[from] texture::Error),
|
||||
}
|
||||
|
||||
/// The OpenGL graphics API backend.
|
||||
pub struct OpenGL {
|
||||
/// Loaded GL context; `None` until [`OpenGL::prepare_context`] (apprt
|
||||
/// supplies the loader once the GL context exists).
|
||||
gl: Option<Gl>,
|
||||
|
||||
/// Alpha blending mode
|
||||
pub blending: AlphaBlending,
|
||||
|
||||
/// The most recently presented target, in case we need to present it again.
|
||||
last_target: Option<Arc<Target>>,
|
||||
}
|
||||
|
||||
/// Pixel format for image texture options.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ImageTextureFormat {
|
||||
/// 1 byte per pixel grayscale.
|
||||
Gray,
|
||||
/// 4 bytes per pixel RGBA.
|
||||
Rgba,
|
||||
/// 4 bytes per pixel BGRA.
|
||||
Bgra,
|
||||
}
|
||||
|
||||
impl ImageTextureFormat {
|
||||
fn to_pixel_format(self) -> PixelFormat {
|
||||
match self {
|
||||
ImageTextureFormat::Gray => PixelFormat::Red,
|
||||
ImageTextureFormat::Rgba => PixelFormat::Rgba,
|
||||
ImageTextureFormat::Bgra => PixelFormat::Bgra,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenGL {
|
||||
/// Create the backend. `OpenGL.init`; the GL context is attached later
|
||||
/// with [`OpenGL::prepare_context`] (GTK: from the GLArea `realize`).
|
||||
pub fn init(blending: AlphaBlending) -> OpenGL {
|
||||
OpenGL {
|
||||
gl: None,
|
||||
blending,
|
||||
last_target: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create the backend around an already loaded context (used by tests and
|
||||
/// by apprts that load GL themselves).
|
||||
pub fn with_gl(blending: AlphaBlending, gl: Gl) -> OpenGL {
|
||||
OpenGL {
|
||||
gl: Some(gl),
|
||||
blending,
|
||||
last_target: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The loaded GL handle, if the context has been prepared.
|
||||
pub fn gl(&self) -> Option<&Gl> {
|
||||
self.gl.as_ref()
|
||||
}
|
||||
|
||||
fn gl_or_err(&self) -> Result<&Gl, Error> {
|
||||
self.gl.as_ref().ok_or(Error::NoContext)
|
||||
}
|
||||
|
||||
/// Prepares the provided GL context, loading it with `get_proc_address`
|
||||
/// (`prepareContext`). The context must be current on the calling thread.
|
||||
pub fn prepare_context<F>(&mut self, get_proc_address: F) -> Result<(), Error>
|
||||
where
|
||||
F: FnMut(&str) -> *const c_void,
|
||||
{
|
||||
let gl = Gl::from_loader_with_debug(get_proc_address);
|
||||
let (major, minor) = gl.version();
|
||||
log::info!("loaded OpenGL {major}.{minor}");
|
||||
|
||||
// Need to check version before trying to enable it
|
||||
if major < MIN_VERSION_MAJOR || (major == MIN_VERSION_MAJOR && minor < MIN_VERSION_MINOR) {
|
||||
log::warn!(
|
||||
"OpenGL version is too old. Ghostty requires OpenGL {MIN_VERSION_MAJOR}.{MIN_VERSION_MINOR}"
|
||||
);
|
||||
return Err(Error::OpenGLOutdated(major, minor));
|
||||
}
|
||||
|
||||
// Enable debug output for the context.
|
||||
gl.enable(glow::DEBUG_OUTPUT)?;
|
||||
|
||||
// Enable SRGB framebuffer for linear blending support.
|
||||
gl.enable(glow::FRAMEBUFFER_SRGB)?;
|
||||
|
||||
self.gl = Some(gl);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This is called early right after surface creation.
|
||||
///
|
||||
/// GTK uses a global OpenGL context, so the apprt passes its loader here.
|
||||
pub fn surface_init<F>(&mut self, get_proc_address: F) -> Result<(), Error>
|
||||
where
|
||||
F: FnMut(&str) -> *const c_void,
|
||||
{
|
||||
self.prepare_context(get_proc_address)
|
||||
}
|
||||
|
||||
/// This is called just prior to spinning up the renderer
|
||||
/// thread for final main thread setup requirements.
|
||||
pub fn finalize_surface_init(&self) {}
|
||||
|
||||
/// Callback called by the renderer thread when it begins.
|
||||
///
|
||||
/// GTK doesn't support threaded OpenGL operations as far as I can
|
||||
/// tell, so we use the renderer thread to setup all the state
|
||||
/// but then do the actual draws and texture syncs and all that
|
||||
/// on the main thread. As such, we don't do anything here.
|
||||
pub fn thread_enter(&self) {}
|
||||
|
||||
/// Callback called by the renderer thread when it exits.
|
||||
///
|
||||
/// We don't need to do any unloading for GTK because we may
|
||||
/// be sharing the global bindings with other windows.
|
||||
pub fn thread_exit(&self) {}
|
||||
|
||||
/// Called by the apprt when the display (GLArea) is (re)realized.
|
||||
pub fn display_realized<F>(&mut self, get_proc_address: F)
|
||||
where
|
||||
F: FnMut(&str) -> *const c_void,
|
||||
{
|
||||
if let Err(err) = self.prepare_context(get_proc_address) {
|
||||
log::warn!("Error preparing GL context in displayRealized, err={err}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Actions taken before doing anything in `drawFrame`.
|
||||
///
|
||||
/// Right now there's nothing we need to do for OpenGL.
|
||||
pub fn draw_frame_start(&mut self) {}
|
||||
|
||||
/// Actions taken after `drawFrame` is done.
|
||||
///
|
||||
/// Right now there's nothing we need to do for OpenGL.
|
||||
pub fn draw_frame_end(&mut self) {}
|
||||
|
||||
/// Build the shader pipelines, including the post-processing pipelines
|
||||
/// for `custom_shaders` (GLSL produced by [`shadertoy::load_from_files`]).
|
||||
pub fn init_shaders<S: AsRef<str>>(
|
||||
&self,
|
||||
custom_shaders: &[S],
|
||||
) -> Result<shaders::Shaders, Error> {
|
||||
Ok(shaders::Shaders::init(self.gl_or_err()?, custom_shaders)?)
|
||||
}
|
||||
|
||||
/// Get the current size of the runtime surface: `(width, height)`.
|
||||
pub fn surface_size(&self) -> Result<(u32, u32), Error> {
|
||||
let viewport = self.gl_or_err()?.get_viewport();
|
||||
Ok((viewport[2] as u32, viewport[3] as u32))
|
||||
}
|
||||
|
||||
/// Initialize a new render target which can be presented by this API.
|
||||
pub fn init_target(&self, width: usize, height: usize) -> Result<Target, Error> {
|
||||
Ok(Target::init(
|
||||
self.gl_or_err()?,
|
||||
target::Options {
|
||||
internal_format: if self.blending.is_linear() {
|
||||
InternalFormat::Srgba
|
||||
} else {
|
||||
InternalFormat::Rgba
|
||||
},
|
||||
width,
|
||||
height,
|
||||
},
|
||||
)?)
|
||||
}
|
||||
|
||||
/// Present the provided target.
|
||||
pub fn present(&mut self, target: &Arc<Target>) -> Result<(), Error> {
|
||||
let gl = self.gl_or_err()?.clone();
|
||||
|
||||
// In order to present a target we blit it to the default framebuffer.
|
||||
|
||||
// We disable GL_FRAMEBUFFER_SRGB while doing this blit, otherwise the
|
||||
// values may be linearized as they're copied, but even though the draw
|
||||
// framebuffer has a linear internal format, the values in it should be
|
||||
// sRGB, not linear!
|
||||
gl.disable(glow::FRAMEBUFFER_SRGB)?;
|
||||
|
||||
// Bind the target for reading.
|
||||
let fbobind = gl.bind_framebuffer(target.framebuffer(), FramebufferTarget::Read);
|
||||
|
||||
// Blit
|
||||
let (w, h) = (target.width as i32, target.height as i32);
|
||||
gl.blit_framebuffer(
|
||||
(0, 0, w, h),
|
||||
(0, 0, w, h),
|
||||
glow::COLOR_BUFFER_BIT,
|
||||
glow::NEAREST,
|
||||
);
|
||||
|
||||
gl.unbind_framebuffer(fbobind);
|
||||
if let Err(err) = gl.enable(glow::FRAMEBUFFER_SRGB) {
|
||||
log::error!("Error re-enabling GL_FRAMEBUFFER_SRGB, err={err}");
|
||||
}
|
||||
|
||||
// Keep track of this target in case we need to repeat it.
|
||||
self.last_target = Some(Arc::clone(target));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Present the last presented target again.
|
||||
pub fn present_last_target(&mut self) -> Result<(), Error> {
|
||||
if let Some(target) = self.last_target.clone() {
|
||||
self.present(&target)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Drop the remembered last-presented target (call when the target it
|
||||
/// refers to is being replaced, e.g. on resize).
|
||||
pub fn clear_last_target(&mut self) {
|
||||
self.last_target = None;
|
||||
}
|
||||
|
||||
/// Returns the options to use when constructing buffers.
|
||||
pub fn buffer_options(&self) -> buffer::Options {
|
||||
buffer::Options {
|
||||
target: BufferTarget::Array,
|
||||
usage: BufferUsage::DynamicDraw,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn instance_buffer_options(&self) -> buffer::Options {
|
||||
self.buffer_options()
|
||||
}
|
||||
pub fn uniform_buffer_options(&self) -> buffer::Options {
|
||||
self.buffer_options()
|
||||
}
|
||||
pub fn fg_buffer_options(&self) -> buffer::Options {
|
||||
self.buffer_options()
|
||||
}
|
||||
pub fn bg_buffer_options(&self) -> buffer::Options {
|
||||
self.buffer_options()
|
||||
}
|
||||
pub fn image_buffer_options(&self) -> buffer::Options {
|
||||
self.buffer_options()
|
||||
}
|
||||
pub fn bg_image_buffer_options(&self) -> buffer::Options {
|
||||
self.buffer_options()
|
||||
}
|
||||
|
||||
/// Returns the options to use when constructing textures.
|
||||
pub fn texture_options(&self) -> texture::Options {
|
||||
texture::Options {
|
||||
format: PixelFormat::Rgba,
|
||||
internal_format: InternalFormat::Srgba,
|
||||
target: TextureTarget::D2,
|
||||
min_filter: MinFilter::Linear,
|
||||
mag_filter: MagFilter::Linear,
|
||||
wrap_s: Wrap::ClampToEdge,
|
||||
wrap_t: Wrap::ClampToEdge,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the options to use when constructing samplers.
|
||||
pub fn sampler_options(&self) -> sampler::Options {
|
||||
sampler::Options {
|
||||
min_filter: MinFilter::Linear,
|
||||
mag_filter: MagFilter::Linear,
|
||||
wrap_s: Wrap::ClampToEdge,
|
||||
wrap_t: Wrap::ClampToEdge,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the options to use when constructing textures for images.
|
||||
pub fn image_texture_options(
|
||||
&self,
|
||||
format: ImageTextureFormat,
|
||||
srgb: bool,
|
||||
) -> texture::Options {
|
||||
texture::Options {
|
||||
format: format.to_pixel_format(),
|
||||
internal_format: if srgb {
|
||||
InternalFormat::Srgba
|
||||
} else {
|
||||
InternalFormat::Rgba
|
||||
},
|
||||
target: TextureTarget::D2,
|
||||
// TODO: Generate mipmaps for image textures and use
|
||||
// linear_mipmap_linear filtering so that they
|
||||
// look good even when scaled way down.
|
||||
min_filter: MinFilter::Linear,
|
||||
mag_filter: MagFilter::Linear,
|
||||
// TODO: Separate out background image options, use
|
||||
// repeating coordinate modes so we don't have
|
||||
// to do the modulus in the shader.
|
||||
wrap_s: Wrap::ClampToEdge,
|
||||
wrap_t: Wrap::ClampToEdge,
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes a Texture suitable for the provided font atlas.
|
||||
pub fn init_atlas_texture(&self, atlas: &Atlas) -> Result<Texture, Error> {
|
||||
let (format, internal_format) = match atlas.format {
|
||||
AtlasFormat::Grayscale => (PixelFormat::Red, InternalFormat::Red),
|
||||
AtlasFormat::Bgra => (PixelFormat::Bgra, InternalFormat::Srgba),
|
||||
AtlasFormat::Bgr => panic!("unsupported atlas format for OpenGL texture"),
|
||||
};
|
||||
|
||||
Ok(Texture::init(
|
||||
self.gl_or_err().map_err(|_| texture::Error::OpenGLFailed)?,
|
||||
texture::Options {
|
||||
format,
|
||||
internal_format,
|
||||
target: TextureTarget::Rectangle,
|
||||
min_filter: MinFilter::Nearest,
|
||||
mag_filter: MagFilter::Nearest,
|
||||
wrap_s: Wrap::ClampToEdge,
|
||||
wrap_t: Wrap::ClampToEdge,
|
||||
},
|
||||
atlas.size as usize,
|
||||
atlas.size as usize,
|
||||
None,
|
||||
)?)
|
||||
}
|
||||
|
||||
/// Begin a frame. The target is presented via this API when the frame is
|
||||
/// completed (see [`Frame::complete`]).
|
||||
pub fn begin_frame(&mut self, target: Arc<Target>) -> Frame<'_> {
|
||||
Frame::begin(frame::Options::default(), self, target)
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
//! Wrapper for handling render passes (`opengl/RenderPass.zig`).
|
||||
use super::gl::{
|
||||
Attachment as GlAttachment, BufferTarget, FramebufferTarget, Gl, Primitive, RawBuffer,
|
||||
};
|
||||
use super::pipeline::Pipeline;
|
||||
use super::sampler::Sampler;
|
||||
use super::target::Target;
|
||||
use super::texture::Texture;
|
||||
|
||||
/// What a color attachment renders into.
|
||||
pub enum AttachmentTarget<'a> {
|
||||
Texture(&'a Texture),
|
||||
Target(&'a Target),
|
||||
}
|
||||
|
||||
/// Describes a color attachment.
|
||||
pub struct Attachment<'a> {
|
||||
pub target: AttachmentTarget<'a>,
|
||||
pub clear_color: Option<[f32; 4]>,
|
||||
}
|
||||
|
||||
/// Options for beginning a render pass.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Options<'a> {
|
||||
/// Color attachments for this render pass.
|
||||
pub attachments: &'a [Attachment<'a>],
|
||||
}
|
||||
|
||||
/// Describes the draw call for a step.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Draw {
|
||||
pub ty: Primitive,
|
||||
pub vertex_count: usize,
|
||||
pub instance_count: usize,
|
||||
}
|
||||
|
||||
impl Draw {
|
||||
pub fn new(ty: Primitive, vertex_count: usize) -> Self {
|
||||
Draw {
|
||||
ty,
|
||||
vertex_count,
|
||||
instance_count: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes a step in a render pass.
|
||||
pub struct Step<'a> {
|
||||
pub pipeline: &'a Pipeline,
|
||||
pub uniforms: Option<RawBuffer>,
|
||||
pub buffers: &'a [Option<RawBuffer>],
|
||||
pub textures: &'a [Option<&'a Texture>],
|
||||
pub samplers: &'a [Option<&'a Sampler>],
|
||||
pub draw: Draw,
|
||||
}
|
||||
|
||||
pub struct RenderPass<'a> {
|
||||
gl: Gl,
|
||||
attachments: &'a [Attachment<'a>],
|
||||
step_number: usize,
|
||||
}
|
||||
|
||||
impl<'a> RenderPass<'a> {
|
||||
/// Begin a render pass.
|
||||
pub fn begin(gl: &Gl, opts: Options<'a>) -> RenderPass<'a> {
|
||||
RenderPass {
|
||||
gl: gl.clone(),
|
||||
attachments: opts.attachments,
|
||||
step_number: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a step to this render pass.
|
||||
///
|
||||
/// TODO: Errors are silently ignored in this function, maybe they shouldn't be?
|
||||
/// (Same as upstream; failures are logged at debug level here.)
|
||||
pub fn step(&mut self, s: &Step<'_>) {
|
||||
if s.draw.instance_count == 0 {
|
||||
return;
|
||||
}
|
||||
if self.step_(s).is_none() {
|
||||
log::debug!("render pass step failed");
|
||||
}
|
||||
self.step_number += 1;
|
||||
}
|
||||
|
||||
fn step_(&mut self, s: &Step<'_>) -> Option<()> {
|
||||
let gl = self.gl.clone();
|
||||
|
||||
gl.use_program(Some(s.pipeline.program)).ok()?;
|
||||
// Unbinding on all exit paths.
|
||||
let r = (|| {
|
||||
gl.bind_vertex_array(Some(s.pipeline.vao)).ok()?;
|
||||
let r = self.step_with_vao(s);
|
||||
let _ = gl.bind_vertex_array(None);
|
||||
r
|
||||
})();
|
||||
let _ = gl.use_program(None);
|
||||
r
|
||||
}
|
||||
|
||||
fn step_with_vao(&mut self, s: &Step<'_>) -> Option<()> {
|
||||
let gl = self.gl.clone();
|
||||
|
||||
let fbobind = match &self.attachments[0].target {
|
||||
AttachmentTarget::Target(t) => {
|
||||
gl.bind_framebuffer(t.framebuffer(), FramebufferTarget::Framebuffer)
|
||||
}
|
||||
AttachmentTarget::Texture(t) => {
|
||||
let fbobind = gl.bind_framebuffer(s.pipeline.fbo, FramebufferTarget::Framebuffer);
|
||||
if gl
|
||||
.framebuffer_texture_2d(&fbobind, GlAttachment::Color0, t.target, t.raw(), 0)
|
||||
.is_err()
|
||||
{
|
||||
gl.unbind_framebuffer(fbobind);
|
||||
return None;
|
||||
}
|
||||
fbobind
|
||||
}
|
||||
};
|
||||
|
||||
let r = self.step_with_fbo(s);
|
||||
self.gl.unbind_framebuffer(fbobind);
|
||||
r
|
||||
}
|
||||
|
||||
fn step_with_fbo(&mut self, s: &Step<'_>) -> Option<()> {
|
||||
let gl = &self.gl;
|
||||
|
||||
// If we have a clear color and this is the
|
||||
// first step in the pass, go ahead and clear.
|
||||
if self.step_number == 0
|
||||
&& let Some(c) = self.attachments[0].clear_color
|
||||
{
|
||||
gl.clear_color(c[0], c[1], c[2], c[3]);
|
||||
gl.clear(glow::COLOR_BUFFER_BIT);
|
||||
}
|
||||
|
||||
// Bind the uniform buffer we bind at index 1 to align with Metal.
|
||||
if let Some(ubo) = s.uniforms {
|
||||
gl.bind_buffer_base(BufferTarget::Uniform, 1, ubo).ok()?;
|
||||
}
|
||||
|
||||
// Bind relevant texture units.
|
||||
for (i, t) in s.textures.iter().enumerate() {
|
||||
if let Some(tex) = t {
|
||||
gl.active_texture(i as u32).ok()?;
|
||||
gl.bind_texture(tex.target, Some(tex.raw())).ok()?;
|
||||
}
|
||||
}
|
||||
|
||||
// Bind relevant samplers.
|
||||
for (i, sampler) in s.samplers.iter().enumerate() {
|
||||
if let Some(sampler) = sampler {
|
||||
gl.bind_sampler(i as u32, sampler.raw()).ok()?;
|
||||
}
|
||||
}
|
||||
|
||||
// Bind 0th buffer as the vertex buffer,
|
||||
// and bind the rest as storage buffers.
|
||||
if !s.buffers.is_empty() {
|
||||
if let Some(vbo) = s.buffers[0] {
|
||||
gl.bind_vertex_buffer(0, vbo, 0, s.pipeline.stride as i32)
|
||||
.ok()?;
|
||||
}
|
||||
|
||||
for (i, b) in s.buffers.iter().enumerate().skip(1) {
|
||||
if let Some(buf) = b {
|
||||
gl.bind_buffer_base(BufferTarget::Storage, i as u32, *buf)
|
||||
.ok()?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if s.pipeline.blending_enabled {
|
||||
gl.enable(glow::BLEND).ok()?;
|
||||
gl.blend_func(glow::ONE, glow::ONE_MINUS_SRC_ALPHA).ok()?;
|
||||
} else {
|
||||
gl.disable(glow::BLEND).ok()?;
|
||||
}
|
||||
|
||||
gl.draw_arrays_instanced(
|
||||
s.draw.ty,
|
||||
0,
|
||||
s.draw.vertex_count as i32,
|
||||
s.draw.instance_count as i32,
|
||||
)
|
||||
.ok()
|
||||
}
|
||||
|
||||
/// Complete this render pass.
|
||||
/// This struct can no longer be used after calling this.
|
||||
pub fn complete(self) {
|
||||
self.gl.flush();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
//! Wrapper for handling samplers (`opengl/Sampler.zig`).
|
||||
use super::gl::{self, Gl, MagFilter, MinFilter, RawSampler, Wrap};
|
||||
|
||||
/// Options for initializing a sampler.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Options {
|
||||
pub min_filter: MinFilter,
|
||||
pub mag_filter: MagFilter,
|
||||
pub wrap_s: Wrap,
|
||||
pub wrap_t: Wrap,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// An OpenGL API call failed.
|
||||
#[error("OpenGL call failed")]
|
||||
OpenGLFailed,
|
||||
}
|
||||
|
||||
pub struct Sampler {
|
||||
gl: Gl,
|
||||
sampler: RawSampler,
|
||||
}
|
||||
|
||||
impl Sampler {
|
||||
/// The raw GL sampler handle.
|
||||
pub fn raw(&self) -> RawSampler {
|
||||
self.sampler
|
||||
}
|
||||
|
||||
/// Initialize a sampler
|
||||
pub fn init(gl: &Gl, opts: Options) -> Result<Sampler, Error> {
|
||||
let sampler = gl.create_sampler().map_err(|_| Error::OpenGLFailed)?;
|
||||
// Dropped (and destroyed) on error.
|
||||
let this = Sampler {
|
||||
gl: gl.clone(),
|
||||
sampler,
|
||||
};
|
||||
let p = |name, v| gl.sampler_parameter_i32(sampler, name, v);
|
||||
(|| -> gl::Result<()> {
|
||||
p(gl::param::WRAP_S, opts.wrap_s as i32)?;
|
||||
p(gl::param::WRAP_T, opts.wrap_t as i32)?;
|
||||
p(gl::param::MIN_FILTER, opts.min_filter as i32)?;
|
||||
p(gl::param::MAG_FILTER, opts.mag_filter as i32)
|
||||
})()
|
||||
.map_err(|_| Error::OpenGLFailed)?;
|
||||
Ok(this)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Sampler {
|
||||
fn drop(&mut self) {
|
||||
self.gl.delete_sampler(self.sampler);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,572 @@
|
||||
//! Shader pipelines and GPU data layouts for OpenGL (`opengl/shaders.zig`).
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
|
||||
use super::gl::{self, AttribType, Gl};
|
||||
use super::pipeline::{Attribute, Options as PipelineOptions, Pipeline, StepFunction, Vertex};
|
||||
use crate::math::Mat;
|
||||
|
||||
/// `shaders/glsl/common.glsl`, the only `#include`d file.
|
||||
const COMMON_GLSL: &str = include_str!("../../shaders/glsl/common.glsl");
|
||||
const FULL_SCREEN_V: &str = include_str!("../../shaders/glsl/full_screen.v.glsl");
|
||||
const BG_COLOR_F: &str = include_str!("../../shaders/glsl/bg_color.f.glsl");
|
||||
const CELL_BG_F: &str = include_str!("../../shaders/glsl/cell_bg.f.glsl");
|
||||
const CELL_TEXT_V: &str = include_str!("../../shaders/glsl/cell_text.v.glsl");
|
||||
const CELL_TEXT_F: &str = include_str!("../../shaders/glsl/cell_text.f.glsl");
|
||||
const IMAGE_V: &str = include_str!("../../shaders/glsl/image.v.glsl");
|
||||
const IMAGE_F: &str = include_str!("../../shaders/glsl/image.f.glsl");
|
||||
const BG_IMAGE_V: &str = include_str!("../../shaders/glsl/bg_image.v.glsl");
|
||||
const BG_IMAGE_F: &str = include_str!("../../shaders/glsl/bg_image.f.glsl");
|
||||
|
||||
/// This contains the state for the shaders used by the OpenGL renderer.
|
||||
pub struct Shaders {
|
||||
pub bg_color: Pipeline,
|
||||
pub cell_bg: Pipeline,
|
||||
pub cell_text: Pipeline,
|
||||
pub image: Pipeline,
|
||||
pub bg_image: Pipeline,
|
||||
|
||||
/// Custom shaders to run against the final drawable texture. This
|
||||
/// can be used to apply a lot of effects. Each shader is run in sequence
|
||||
/// against the output of the previous shader.
|
||||
pub post_pipelines: Vec<Pipeline>,
|
||||
}
|
||||
|
||||
impl Shaders {
|
||||
/// Initialize our shader set.
|
||||
///
|
||||
/// "post_shaders" is an optional list of postprocess shaders to run
|
||||
/// against the final drawable texture. This is an array of shader source
|
||||
/// code, not file paths.
|
||||
pub fn init<S: AsRef<str>>(gl: &Gl, post_shaders: &[S]) -> gl::Result<Shaders> {
|
||||
let full_screen_v = process_includes(FULL_SCREEN_V);
|
||||
let cell_text_v = process_includes(CELL_TEXT_V);
|
||||
let image_v = process_includes(IMAGE_V);
|
||||
let bg_image_v = process_includes(BG_IMAGE_V);
|
||||
|
||||
let bg_color = Pipeline::init_no_attributes(
|
||||
gl,
|
||||
&PipelineOptions {
|
||||
blending_enabled: false,
|
||||
..PipelineOptions::new(&full_screen_v, &process_includes(BG_COLOR_F))
|
||||
},
|
||||
)?;
|
||||
let cell_bg = Pipeline::init_no_attributes(
|
||||
gl,
|
||||
&PipelineOptions {
|
||||
blending_enabled: true,
|
||||
..PipelineOptions::new(&full_screen_v, &process_includes(CELL_BG_F))
|
||||
},
|
||||
)?;
|
||||
let cell_text = Pipeline::init::<CellText>(
|
||||
gl,
|
||||
&PipelineOptions {
|
||||
step_fn: StepFunction::PerInstance,
|
||||
blending_enabled: true,
|
||||
..PipelineOptions::new(&cell_text_v, &process_includes(CELL_TEXT_F))
|
||||
},
|
||||
)?;
|
||||
let image = Pipeline::init::<Image>(
|
||||
gl,
|
||||
&PipelineOptions {
|
||||
step_fn: StepFunction::PerInstance,
|
||||
blending_enabled: true,
|
||||
..PipelineOptions::new(&image_v, &process_includes(IMAGE_F))
|
||||
},
|
||||
)?;
|
||||
let bg_image = Pipeline::init::<BgImage>(
|
||||
gl,
|
||||
&PipelineOptions {
|
||||
step_fn: StepFunction::PerInstance,
|
||||
blending_enabled: true,
|
||||
..PipelineOptions::new(&bg_image_v, &process_includes(BG_IMAGE_F))
|
||||
},
|
||||
)?;
|
||||
|
||||
let post_pipelines = init_post_pipelines(gl, post_shaders).unwrap_or_else(|err| {
|
||||
// If an error happens while building postprocess shaders we
|
||||
// want to just not use any postprocess shaders since we don't
|
||||
// want to block Ghostty from working.
|
||||
log::warn!("error initializing postprocess shaders err={err}");
|
||||
Vec::new()
|
||||
});
|
||||
|
||||
Ok(Shaders {
|
||||
bg_color,
|
||||
cell_bg,
|
||||
cell_text,
|
||||
image,
|
||||
bg_image,
|
||||
post_pipelines,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize our custom shader pipelines. The shaders argument is a
|
||||
/// set of shader source code, not file paths.
|
||||
fn init_post_pipelines<S: AsRef<str>>(gl: &Gl, shaders: &[S]) -> gl::Result<Vec<Pipeline>> {
|
||||
// If we have no shaders, do nothing. Otherwise if any error happens
|
||||
// the already-built pipelines are dropped (destroyed).
|
||||
shaders
|
||||
.iter()
|
||||
.map(|source| init_post_pipeline(gl, source.as_ref()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Initialize a single custom shader pipeline from shader source.
|
||||
fn init_post_pipeline(gl: &Gl, data: &str) -> gl::Result<Pipeline> {
|
||||
let vertex = process_includes(FULL_SCREEN_V);
|
||||
Pipeline::init_no_attributes(gl, &PipelineOptions::new(&vertex, data))
|
||||
}
|
||||
|
||||
/// Process `#include "file"` directives in an embedded shader source.
|
||||
///
|
||||
/// Like upstream this is intentionally simple: it makes a bunch of
|
||||
/// assumptions about things being well formed and file names not containing
|
||||
/// quote marks, and the only file that may be included is `common.glsl`
|
||||
/// (included files are not themselves processed).
|
||||
pub fn process_includes(contents: &str) -> String {
|
||||
let mut i = 0;
|
||||
while i < contents.len() {
|
||||
if contents[i..].starts_with("#include") {
|
||||
assert!(
|
||||
contents[i..].starts_with("#include \""),
|
||||
"malformed #include directive"
|
||||
);
|
||||
let start = i + "#include \"".len();
|
||||
let end = start
|
||||
+ contents[start..]
|
||||
.find('"')
|
||||
.expect("unterminated #include directive");
|
||||
let name = &contents[start..end];
|
||||
let included = match name {
|
||||
"common.glsl" => COMMON_GLSL,
|
||||
other => panic!("unknown shader include {other:?}"),
|
||||
};
|
||||
return format!(
|
||||
"{}{}{}",
|
||||
&contents[..i],
|
||||
included,
|
||||
process_includes(&contents[end + 1..])
|
||||
);
|
||||
}
|
||||
if let Some(j) = contents[i..].find("\n#") {
|
||||
i += j + 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
contents.to_owned()
|
||||
}
|
||||
|
||||
/// The uniforms that are passed to our shaders (std140 layout, 144 bytes).
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default, Pod, Zeroable)]
|
||||
pub struct Uniforms {
|
||||
/// The projection matrix for turning world coordinates to normalized.
|
||||
/// This is calculated based on the size of the screen.
|
||||
pub projection_matrix: Mat,
|
||||
|
||||
/// Size of the screen (render target) in pixels.
|
||||
pub screen_size: [f32; 2],
|
||||
|
||||
/// Size of a single cell in pixels, unscaled.
|
||||
pub cell_size: [f32; 2],
|
||||
|
||||
/// Size of the grid in columns and rows.
|
||||
pub grid_size: [u16; 2],
|
||||
|
||||
// Explicit padding so that `grid_padding` is 16-byte aligned (std140).
|
||||
pub pad0: [u8; 12],
|
||||
|
||||
/// The padding around the terminal grid in pixels. In order:
|
||||
/// top, right, bottom, left.
|
||||
pub grid_padding: [f32; 4],
|
||||
|
||||
/// Bit mask defining which directions to
|
||||
/// extend cell colors in to the padding.
|
||||
/// Order, LSB first: left, right, up, down. See [`PaddingExtend`].
|
||||
pub padding_extend: u32,
|
||||
|
||||
/// The minimum contrast ratio for text. The contrast ratio is calculated
|
||||
/// according to the WCAG 2.0 spec.
|
||||
pub min_contrast: f32,
|
||||
|
||||
/// The cursor position and color.
|
||||
pub cursor_pos: [u16; 2],
|
||||
pub cursor_color: [u8; 4],
|
||||
|
||||
/// The background color for the whole surface.
|
||||
pub bg_color: [u8; 4],
|
||||
|
||||
/// Various booleans, in a packed bitfield for space efficiency. See [`Bools`].
|
||||
pub bools: u32,
|
||||
|
||||
// Tail padding to the struct's 16-byte alignment (extern struct size).
|
||||
pub pad1: [u8; 8],
|
||||
}
|
||||
|
||||
/// The `bools` bitfield of [`Uniforms`] (a `packed struct(u32)` upstream).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct Bools {
|
||||
/// Whether the cursor is 2 cells wide.
|
||||
pub cursor_wide: bool,
|
||||
|
||||
/// Indicates that colors provided to the shader are already in
|
||||
/// the P3 color space, so they don't need to be converted from
|
||||
/// sRGB.
|
||||
pub use_display_p3: bool,
|
||||
|
||||
/// Indicates that the color attachments for the shaders have
|
||||
/// an `*_srgb` pixel format, which means the shaders need to
|
||||
/// output linear RGB colors rather than gamma encoded colors,
|
||||
/// since blending will be performed in linear space and then
|
||||
/// the GPU itself will re-encode the colors for storage.
|
||||
pub use_linear_blending: bool,
|
||||
|
||||
/// Enables a weight correction step that makes text rendered
|
||||
/// with linear alpha blending have a similar apparent weight
|
||||
/// (thickness) to gamma-incorrect blending.
|
||||
pub use_linear_correction: bool,
|
||||
}
|
||||
|
||||
impl Bools {
|
||||
pub const CURSOR_WIDE: u32 = 1;
|
||||
pub const USE_DISPLAY_P3: u32 = 2;
|
||||
pub const USE_LINEAR_BLENDING: u32 = 4;
|
||||
pub const USE_LINEAR_CORRECTION: u32 = 8;
|
||||
|
||||
pub fn to_bits(self) -> u32 {
|
||||
u32::from(self.cursor_wide)
|
||||
| u32::from(self.use_display_p3) << 1
|
||||
| u32::from(self.use_linear_blending) << 2
|
||||
| u32::from(self.use_linear_correction) << 3
|
||||
}
|
||||
|
||||
pub fn from_bits(bits: u32) -> Self {
|
||||
Bools {
|
||||
cursor_wide: bits & Self::CURSOR_WIDE != 0,
|
||||
use_display_p3: bits & Self::USE_DISPLAY_P3 != 0,
|
||||
use_linear_blending: bits & Self::USE_LINEAR_BLENDING != 0,
|
||||
use_linear_correction: bits & Self::USE_LINEAR_CORRECTION != 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The `padding_extend` bitfield of [`Uniforms`] (a `packed struct(u32)` upstream).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct PaddingExtend {
|
||||
pub left: bool,
|
||||
pub right: bool,
|
||||
pub up: bool,
|
||||
pub down: bool,
|
||||
}
|
||||
|
||||
impl PaddingExtend {
|
||||
pub fn to_bits(self) -> u32 {
|
||||
u32::from(self.left)
|
||||
| u32::from(self.right) << 1
|
||||
| u32::from(self.up) << 2
|
||||
| u32::from(self.down) << 3
|
||||
}
|
||||
|
||||
pub fn from_bits(bits: u32) -> Self {
|
||||
PaddingExtend {
|
||||
left: bits & 1 != 0,
|
||||
right: bits & 2 != 0,
|
||||
up: bits & 4 != 0,
|
||||
down: bits & 8 != 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Which atlas a glyph lives in (`CellText.Atlas`).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum CellTextAtlas {
|
||||
Grayscale = 0,
|
||||
Color = 1,
|
||||
}
|
||||
|
||||
/// This is a single parameter for the terminal cell shader (32 bytes).
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Pod, Zeroable)]
|
||||
pub struct CellText {
|
||||
pub glyph_pos: [u32; 2],
|
||||
pub glyph_size: [u32; 2],
|
||||
pub bearings: [i16; 2],
|
||||
pub grid_pos: [u16; 2],
|
||||
pub color: [u8; 4],
|
||||
/// A [`CellTextAtlas`] value.
|
||||
pub atlas: u8,
|
||||
/// Bit 0: `no_min_contrast`, bit 1: `is_cursor_glyph`. See the consts below.
|
||||
pub bools: u8,
|
||||
pub pad: [u8; 2],
|
||||
}
|
||||
|
||||
impl CellText {
|
||||
pub const NO_MIN_CONTRAST: u8 = 1;
|
||||
pub const IS_CURSOR_GLYPH: u8 = 2;
|
||||
|
||||
pub fn atlas(&self) -> CellTextAtlas {
|
||||
if self.atlas == 0 {
|
||||
CellTextAtlas::Grayscale
|
||||
} else {
|
||||
CellTextAtlas::Color
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_atlas(&mut self, a: CellTextAtlas) {
|
||||
self.atlas = a as u8;
|
||||
}
|
||||
}
|
||||
|
||||
impl Vertex for CellText {
|
||||
const ATTRIBUTES: &'static [Attribute] = &[
|
||||
Attribute {
|
||||
size: 2,
|
||||
ty: AttribType::U32,
|
||||
offset: core::mem::offset_of!(CellText, glyph_pos) as u32,
|
||||
},
|
||||
Attribute {
|
||||
size: 2,
|
||||
ty: AttribType::U32,
|
||||
offset: core::mem::offset_of!(CellText, glyph_size) as u32,
|
||||
},
|
||||
Attribute {
|
||||
size: 2,
|
||||
ty: AttribType::I16,
|
||||
offset: core::mem::offset_of!(CellText, bearings) as u32,
|
||||
},
|
||||
Attribute {
|
||||
size: 2,
|
||||
ty: AttribType::U16,
|
||||
offset: core::mem::offset_of!(CellText, grid_pos) as u32,
|
||||
},
|
||||
Attribute {
|
||||
size: 4,
|
||||
ty: AttribType::U8,
|
||||
offset: core::mem::offset_of!(CellText, color) as u32,
|
||||
},
|
||||
Attribute {
|
||||
size: 1,
|
||||
ty: AttribType::U8,
|
||||
offset: core::mem::offset_of!(CellText, atlas) as u32,
|
||||
},
|
||||
Attribute {
|
||||
size: 1,
|
||||
ty: AttribType::U8,
|
||||
offset: core::mem::offset_of!(CellText, bools) as u32,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/// This is a single parameter for the cell bg shader.
|
||||
pub type CellBg = [u8; 4];
|
||||
|
||||
/// Single parameter for the image shader. See shader for field details.
|
||||
/// (48 bytes: the Zig extern struct is 16-byte aligned.)
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default, Pod, Zeroable)]
|
||||
pub struct Image {
|
||||
pub grid_pos: [f32; 2],
|
||||
pub cell_offset: [f32; 2],
|
||||
pub source_rect: [f32; 4],
|
||||
pub dest_size: [f32; 2],
|
||||
pub pad: [f32; 2],
|
||||
}
|
||||
|
||||
impl Vertex for Image {
|
||||
const ATTRIBUTES: &'static [Attribute] = &[
|
||||
Attribute {
|
||||
size: 2,
|
||||
ty: AttribType::F32,
|
||||
offset: core::mem::offset_of!(Image, grid_pos) as u32,
|
||||
},
|
||||
Attribute {
|
||||
size: 2,
|
||||
ty: AttribType::F32,
|
||||
offset: core::mem::offset_of!(Image, cell_offset) as u32,
|
||||
},
|
||||
Attribute {
|
||||
size: 4,
|
||||
ty: AttribType::F32,
|
||||
offset: core::mem::offset_of!(Image, source_rect) as u32,
|
||||
},
|
||||
Attribute {
|
||||
size: 2,
|
||||
ty: AttribType::F32,
|
||||
offset: core::mem::offset_of!(Image, dest_size) as u32,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/// Single parameter for the bg image shader (8 bytes).
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default, Pod, Zeroable)]
|
||||
pub struct BgImage {
|
||||
pub opacity: f32,
|
||||
/// A packed [`BgImageInfo`] byte.
|
||||
pub info: u8,
|
||||
pub pad: [u8; 3],
|
||||
}
|
||||
|
||||
/// `BgImage.Info`, a `packed struct(u8)`: position:u4, fit:u2, repeat:u1, pad:u1.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct BgImageInfo {
|
||||
pub position: BgImagePosition,
|
||||
pub fit: BgImageFit,
|
||||
pub repeat: bool,
|
||||
}
|
||||
|
||||
impl BgImageInfo {
|
||||
pub fn to_bits(self) -> u8 {
|
||||
(self.position as u8) | (self.fit as u8) << 4 | u8::from(self.repeat) << 6
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum BgImagePosition {
|
||||
Tl = 0,
|
||||
Tc = 1,
|
||||
Tr = 2,
|
||||
Ml = 3,
|
||||
Mc = 4,
|
||||
Mr = 5,
|
||||
Bl = 6,
|
||||
Bc = 7,
|
||||
Br = 8,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum BgImageFit {
|
||||
Contain = 0,
|
||||
Cover = 1,
|
||||
Stretch = 2,
|
||||
None = 3,
|
||||
}
|
||||
|
||||
impl Vertex for BgImage {
|
||||
const ATTRIBUTES: &'static [Attribute] = &[
|
||||
Attribute {
|
||||
size: 1,
|
||||
ty: AttribType::F32,
|
||||
offset: core::mem::offset_of!(BgImage, opacity) as u32,
|
||||
},
|
||||
Attribute {
|
||||
size: 1,
|
||||
ty: AttribType::U8,
|
||||
offset: core::mem::offset_of!(BgImage, info) as u32,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use core::mem::{offset_of, size_of};
|
||||
|
||||
#[test]
|
||||
fn uniforms_std140_layout() {
|
||||
// Offsets from the std140 rules applied to the `Globals` block in
|
||||
// shaders/glsl/common.glsl.
|
||||
assert_eq!(offset_of!(Uniforms, projection_matrix), 0);
|
||||
assert_eq!(offset_of!(Uniforms, screen_size), 64);
|
||||
assert_eq!(offset_of!(Uniforms, cell_size), 72);
|
||||
assert_eq!(offset_of!(Uniforms, grid_size), 80);
|
||||
assert_eq!(offset_of!(Uniforms, grid_padding), 96);
|
||||
assert_eq!(offset_of!(Uniforms, padding_extend), 112);
|
||||
assert_eq!(offset_of!(Uniforms, min_contrast), 116);
|
||||
assert_eq!(offset_of!(Uniforms, cursor_pos), 120);
|
||||
assert_eq!(offset_of!(Uniforms, cursor_color), 124);
|
||||
assert_eq!(offset_of!(Uniforms, bg_color), 128);
|
||||
assert_eq!(offset_of!(Uniforms, bools), 132);
|
||||
assert_eq!(size_of::<Uniforms>(), 144);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cell_text_layout() {
|
||||
// Minimizing the size of this struct is important,
|
||||
// so we test it in order to be aware of any changes.
|
||||
assert_eq!(size_of::<CellText>(), 32);
|
||||
assert_eq!(offset_of!(CellText, glyph_pos), 0);
|
||||
assert_eq!(offset_of!(CellText, glyph_size), 8);
|
||||
assert_eq!(offset_of!(CellText, bearings), 16);
|
||||
assert_eq!(offset_of!(CellText, grid_pos), 20);
|
||||
assert_eq!(offset_of!(CellText, color), 24);
|
||||
assert_eq!(offset_of!(CellText, atlas), 28);
|
||||
assert_eq!(offset_of!(CellText, bools), 29);
|
||||
let offsets: Vec<u32> = CellText::ATTRIBUTES.iter().map(|a| a.offset).collect();
|
||||
assert_eq!(offsets, [0, 8, 16, 20, 24, 28, 29]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_layout() {
|
||||
assert_eq!(size_of::<Image>(), 48);
|
||||
let offsets: Vec<u32> = Image::ATTRIBUTES.iter().map(|a| a.offset).collect();
|
||||
assert_eq!(offsets, [0, 8, 16, 32]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bg_image_layout() {
|
||||
assert_eq!(size_of::<BgImage>(), 8);
|
||||
let offsets: Vec<u32> = BgImage::ATTRIBUTES.iter().map(|a| a.offset).collect();
|
||||
assert_eq!(offsets, [0, 4]);
|
||||
let info = BgImageInfo {
|
||||
position: BgImagePosition::Br,
|
||||
fit: BgImageFit::None,
|
||||
repeat: true,
|
||||
};
|
||||
assert_eq!(info.to_bits(), 0b0111_1000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bools_bits_match_glsl() {
|
||||
// Mirrors the consts in common.glsl.
|
||||
assert!(COMMON_GLSL.contains("const uint CURSOR_WIDE = 1u;"));
|
||||
assert!(COMMON_GLSL.contains("const uint USE_DISPLAY_P3 = 2u;"));
|
||||
assert!(COMMON_GLSL.contains("const uint USE_LINEAR_BLENDING = 4u;"));
|
||||
assert!(COMMON_GLSL.contains("const uint USE_LINEAR_CORRECTION = 8u;"));
|
||||
let b = Bools {
|
||||
cursor_wide: true,
|
||||
use_display_p3: false,
|
||||
use_linear_blending: true,
|
||||
use_linear_correction: true,
|
||||
};
|
||||
assert_eq!(b.to_bits(), 1 | 4 | 8);
|
||||
assert_eq!(Bools::from_bits(b.to_bits()), b);
|
||||
|
||||
assert!(COMMON_GLSL.contains("const uint EXTEND_LEFT = 1u;"));
|
||||
assert!(COMMON_GLSL.contains("const uint EXTEND_DOWN = 8u;"));
|
||||
let p = PaddingExtend {
|
||||
left: true,
|
||||
right: false,
|
||||
up: false,
|
||||
down: true,
|
||||
};
|
||||
assert_eq!(p.to_bits(), 9);
|
||||
assert_eq!(PaddingExtend::from_bits(9), p);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_includes_inlines_common() {
|
||||
let out = process_includes(CELL_TEXT_V);
|
||||
// (common.glsl mentions the directive in a comment, so only check the start.)
|
||||
assert!(!out.starts_with("#include"));
|
||||
assert!(out.starts_with("#version 430 core"));
|
||||
assert!(out.contains("layout(binding = 1, std140) uniform Globals"));
|
||||
// Shaders without includes are returned verbatim.
|
||||
assert_eq!(process_includes(FULL_SCREEN_V), FULL_SCREEN_V);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_includes_only_at_line_start() {
|
||||
let src =
|
||||
"#version 430 core\n// #include \"x\"\n#include \"common.glsl\"\nvoid main() {}\n";
|
||||
let out = process_includes(src);
|
||||
assert!(out.contains("// #include \"x\""));
|
||||
assert!(out.contains("uniform Globals"));
|
||||
assert!(out.ends_with("\nvoid main() {}\n"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
//! Represents a render target (`opengl/Target.zig`).
|
||||
//!
|
||||
//! In this case, an OpenGL renderbuffer-backed framebuffer.
|
||||
use super::gl::{
|
||||
self, Attachment, FramebufferTarget, Gl, InternalFormat, RawFramebuffer, RawRenderbuffer,
|
||||
};
|
||||
|
||||
/// Options for initializing a Target
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Options {
|
||||
/// Desired width
|
||||
pub width: usize,
|
||||
/// Desired height
|
||||
pub height: usize,
|
||||
|
||||
/// Internal format for the renderbuffer.
|
||||
pub internal_format: InternalFormat,
|
||||
}
|
||||
|
||||
pub struct Target {
|
||||
gl: Gl,
|
||||
|
||||
/// The underlying framebuffer.
|
||||
framebuffer: RawFramebuffer,
|
||||
|
||||
/// The underlying renderbuffer.
|
||||
renderbuffer: RawRenderbuffer,
|
||||
|
||||
/// Current width of this target.
|
||||
pub width: usize,
|
||||
/// Current height of this target.
|
||||
pub height: usize,
|
||||
}
|
||||
|
||||
impl Target {
|
||||
pub fn framebuffer(&self) -> RawFramebuffer {
|
||||
self.framebuffer
|
||||
}
|
||||
|
||||
pub fn renderbuffer(&self) -> RawRenderbuffer {
|
||||
self.renderbuffer
|
||||
}
|
||||
|
||||
pub fn init(gl: &Gl, opts: Options) -> gl::Result<Target> {
|
||||
let rbo = gl.create_renderbuffer()?;
|
||||
gl.bind_renderbuffer(Some(rbo));
|
||||
let r =
|
||||
gl.renderbuffer_storage(opts.internal_format, opts.width as i32, opts.height as i32);
|
||||
gl.bind_renderbuffer(None);
|
||||
if let Err(e) = r {
|
||||
gl.delete_renderbuffer(rbo);
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
let fbo = match gl.create_framebuffer() {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
gl.delete_renderbuffer(rbo);
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
// Dropping `this` on error cleans up both objects.
|
||||
let this = Target {
|
||||
gl: gl.clone(),
|
||||
framebuffer: fbo,
|
||||
renderbuffer: rbo,
|
||||
width: opts.width,
|
||||
height: opts.height,
|
||||
};
|
||||
let bound = gl.bind_framebuffer(fbo, FramebufferTarget::Framebuffer);
|
||||
let r = gl.framebuffer_renderbuffer(&bound, Attachment::Color0, rbo);
|
||||
gl.unbind_framebuffer(bound);
|
||||
r?;
|
||||
Ok(this)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Target {
|
||||
fn drop(&mut self) {
|
||||
self.gl.delete_framebuffer(self.framebuffer);
|
||||
self.gl.delete_renderbuffer(self.renderbuffer);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
//! Wrapper for handling textures (`opengl/Texture.zig`).
|
||||
use super::gl::{
|
||||
self, Gl, InternalFormat, MagFilter, MinFilter, PixelFormat, RawTexture, TextureTarget, Wrap,
|
||||
};
|
||||
|
||||
/// Options for initializing a texture.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Options {
|
||||
pub format: PixelFormat,
|
||||
pub internal_format: InternalFormat,
|
||||
pub target: TextureTarget,
|
||||
pub min_filter: MinFilter,
|
||||
pub mag_filter: MagFilter,
|
||||
pub wrap_s: Wrap,
|
||||
pub wrap_t: Wrap,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// An OpenGL API call failed.
|
||||
#[error("OpenGL call failed")]
|
||||
OpenGLFailed,
|
||||
}
|
||||
|
||||
pub struct Texture {
|
||||
gl: Gl,
|
||||
handle: RawTexture,
|
||||
|
||||
/// The width of this texture.
|
||||
pub width: usize,
|
||||
/// The height of this texture.
|
||||
pub height: usize,
|
||||
|
||||
/// Format for this texture.
|
||||
pub format: PixelFormat,
|
||||
|
||||
/// Target for this texture.
|
||||
pub target: TextureTarget,
|
||||
}
|
||||
|
||||
impl Texture {
|
||||
/// The raw GL texture handle.
|
||||
pub fn raw(&self) -> RawTexture {
|
||||
self.handle
|
||||
}
|
||||
|
||||
/// Initialize a texture
|
||||
pub fn init(
|
||||
gl: &Gl,
|
||||
opts: Options,
|
||||
width: usize,
|
||||
height: usize,
|
||||
data: Option<&[u8]>,
|
||||
) -> Result<Texture, Error> {
|
||||
let texture = gl.create_texture().map_err(|_| Error::OpenGLFailed)?;
|
||||
// On any error below, `this` is dropped and the texture destroyed.
|
||||
let this = Texture {
|
||||
gl: gl.clone(),
|
||||
handle: texture,
|
||||
width,
|
||||
height,
|
||||
format: opts.format,
|
||||
target: opts.target,
|
||||
};
|
||||
|
||||
let r = (|| -> gl::Result<()> {
|
||||
gl.bind_texture(opts.target, Some(texture))?;
|
||||
gl.tex_parameter_i32(opts.target, gl::param::WRAP_S, opts.wrap_s as i32)?;
|
||||
gl.tex_parameter_i32(opts.target, gl::param::WRAP_T, opts.wrap_t as i32)?;
|
||||
gl.tex_parameter_i32(opts.target, gl::param::MIN_FILTER, opts.min_filter as i32)?;
|
||||
gl.tex_parameter_i32(opts.target, gl::param::MAG_FILTER, opts.mag_filter as i32)?;
|
||||
gl.tex_image_2d(
|
||||
opts.target,
|
||||
0,
|
||||
opts.internal_format,
|
||||
width as i32,
|
||||
height as i32,
|
||||
opts.format,
|
||||
data,
|
||||
)
|
||||
})();
|
||||
let _ = gl.bind_texture(opts.target, None);
|
||||
r.map_err(|_| Error::OpenGLFailed)?;
|
||||
Ok(this)
|
||||
}
|
||||
|
||||
/// Replace a region of the texture with the provided data.
|
||||
///
|
||||
/// Does NOT check the dimensions of the data to ensure correctness.
|
||||
pub fn replace_region(
|
||||
&self,
|
||||
x: usize,
|
||||
y: usize,
|
||||
width: usize,
|
||||
height: usize,
|
||||
data: &[u8],
|
||||
) -> Result<(), Error> {
|
||||
self.gl
|
||||
.bind_texture(self.target, Some(self.handle))
|
||||
.map_err(|_| Error::OpenGLFailed)?;
|
||||
let r = self.gl.tex_sub_image_2d(
|
||||
self.target,
|
||||
0,
|
||||
x as i32,
|
||||
y as i32,
|
||||
width as i32,
|
||||
height as i32,
|
||||
self.format,
|
||||
data,
|
||||
);
|
||||
let _ = self.gl.bind_texture(self.target, None);
|
||||
r.map_err(|_| Error::OpenGLFailed)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Texture {
|
||||
fn drop(&mut self) {
|
||||
self.gl.delete_texture(self.handle);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user