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
5a5fa7893b
commit
568ce784f9
35 files changed
+5477
-5
No files matched your search
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user