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)
8.2 KiB
ghostty-renderer — R1 (OpenGL backend, shaders, shadertoy, size, cursor)
Crate lint: unsafe_code = "deny"; the only #[allow(unsafe_code)] item is opengl::gl
(thin safe wrapper over glow, every unsafe block has a // SAFETY: comment; the
module doc states the shared invariants). The rest of the crate is safe code.
Deps: glow 0.18, naga 30 (features glsl-in, glsl-out, wgsl-in — see below),
bytemuck (derive), ghostty-font, log, thiserror; dev: khronos-egl (dynamic).
ghostty-config / ghostty-terminal deps removed.
Module map (Zig -> Rust, crates/ghostty-renderer/src/)
| Zig | Rust |
|---|---|
pkg/opengl/* (needed subset) |
opengl/gl.rs (Gl = Arc<glow::Context> handle, GL enums, Error) |
opengl/buffer.zig |
opengl/buffer.rs (Buffer<T: Pod>, Options) |
opengl/Texture.zig, Sampler.zig, Target.zig |
texture.rs, sampler.rs, target.rs (RAII: Drop deletes the GL object) |
opengl/Pipeline.zig |
pipeline.rs (Vertex trait with ATTRIBUTES replaces comptime reflection) |
opengl/RenderPass.zig |
render_pass.rs (RenderPass, Step, Draw, Attachment) |
opengl/Frame.zig |
frame.rs (Frame::complete(sync) -> Health) |
opengl/shaders.zig |
opengl/shaders.rs (Shaders, Uniforms, CellText, Image, BgImage, process_includes) |
OpenGL.zig |
opengl/mod.rs (OpenGL) |
shaders/*.glsl |
crates/ghostty-renderer/shaders/ (verbatim copies, include_str!) |
shadertoy.zig |
shadertoy.rs |
size.zig, cursor.zig, backend.zig (+ Health from renderer.zig), message.zig, Options.zig, math.zig (ortho2d) |
size.rs, cursor.rs, backend.rs, message.rs, options.rs, math.rs |
API overview
// apprt (GTK GLArea realize, context current) supplies the loader:
let mut api = OpenGL::init(AlphaBlending::Native); // or ::with_gl(blending, gl)
api.prepare_context(|name| gtk_get_proc_address(name))?; // loads glow, checks >= 4.3,
// enables DEBUG_OUTPUT + FRAMEBUFFER_SRGB
let shaders = api.init_shaders(&custom_glsl_strings)?; // bg_color, cell_bg, cell_text, image, bg_image, post_pipelines
let target = Arc::new(api.init_target(w, h)?); // sRGB renderbuffer if blending.is_linear()
let frame = api.begin_frame(target.clone());
let mut pass = frame.render_pass(&[Attachment { target: AttachmentTarget::Target(&target), clear_color: Some([0.;4]) }]);
pass.step(&Step { pipeline: &shaders.cell_bg, uniforms: Some(ubo.raw()),
buffers: &[None, Some(bg_cells.raw())], // [0] = vertex buffer, [1..] = SSBO at that index
textures: &[], samplers: &[], draw: Draw::new(Primitive::Triangle, 3) });
pass.complete();
let health = frame.complete(true); // glFinish, glGetError, present (blit to default FB)
- Uniform buffer is bound at binding 1, SSBOs at their slice index, textures/samplers at their slice index (same as Zig).
- GPU structs (
bytemuck::Pod, layouts asserted in tests):Uniforms144 B std140 (packbools/padding_extendwithBools::to_bits/PaddingExtend::to_bits),CellText32 B,Image48 B,BgImage8 B (BgImageInfo::to_bits),shadertoy::Uniforms4496 B. Explicitpad*fields reproduce Zigextern struct/align()padding (set to 0; use..Default::default()). - Atlas upload:
api.init_atlas_texture(&atlas)creates theGL_TEXTURE_RECTANGLEtexture (dataNone); upload withTexture::replace_region(0,0,size,size,&atlas.data)and trackatlas.modified/resized. - Textures:
texture_options(),image_texture_options(fmt, srgb),sampler_options(),*_buffer_options()mirror OpenGL.zig. - Threading hooks:
surface_init(loader),display_realized(loader),finalize_surface_init,thread_enter/exit,draw_frame_start/end(GTK semantics preserved: no-ops except context prepare). The GL context must be current on whichever thread calls GL. presentkeepsArc<Target>aslast_target(Zig copied the struct); callclear_last_target()when the target is replaced.- Viewport is NOT set by the renderer (GLArea sets it;
surface_size()reads GL_VIEWPORT) — a surfaceless EGL context has none.
shadertoy (glslang + SPIR-V + spirv-cross replaced by naga)
compile_shadertoy(user_src, errlog) -> Compiled then glsl_from_compiled(&Compiled) -> String (GLSL 430 core), or
load_from_file(s) (4 MB limit, optional/required path list PathItem; Target::Msl -> Error::Unsupported).
glsl_from_shader still returns prefix + "\n\n" + src verbatim. Errors: Error::GlslangFailed (parse; ShaderLog.info has naga's
rendered diagnostics, warn-logged like Zig), ValidationFailed, SpvcFailed.
naga's GLSL frontend only takes a Vulkan-flavoured subset, so adapt_for_naga rewrites the prefix (the .glsl files are untouched):
#version 430->460; uniform dropped from block members; layout(location=0) in vec4 gl_FragCoord; removed (naga builtin);
sampler2D iChannel0 -> texture2D+sampler pair with #define iChannel0 sampler2D(tex, smp) (so texture(iChannel0, uv) and
texture2D keep working); layout(set=0,binding=1,std140) on Globals; the mainImage prototype and main() move after the user code
(naga rejects forward declarations). Output keeps layout(std140, binding = 1) uniform ... and layout(binding = 0) uniform sampler2D,
so the Ghostty binding scheme works; verified by compiling+linking converted crt/focus shaders on Mesa. Limitations: user shaders that pass
sampler2D as a function parameter, or define helpers after mainImage that it calls (invalid GLSL anyway), will fail.
naga needs the wgsl-in feature only because naga 30.0.1's front::interpolator is cfg-gated on spv-in/wgsl-in (glsl-in alone does not compile).
Tests
Unit 30 + integration 3 (tests/egl_headless.rs), all passing.
- size.zig 4/4 ported (
padding_balanced_on_zero,grid_size_update_exact,grid_size_update_rounding,coordinate_conversion) + 3 added. - cursor.zig 4/4 ported but drive a locally defined
RenderStateCursorinstead of Terminal/RenderState (TODO(integration)); + password/lock test. - shadertoy.zig:
spirv,spirv invalid,shadertoy to glslported (+ focus shader, file loading, helper-order tests);shadertoy to mslskipped (Metal out of scope; replaced by an Unsupported test). - Layout tests: std140 offsets of
Uniforms/shadertoy::Uniforms,CellText=32,Image=48,BgImage=8, bool bit values vs common.glsl, include processing. tests/egl_headless.rs: surfaceless Mesa EGL 4.3 core context (khronos-egldynamic,EGL_PLATFORM_SURFACELESS_MESA); builds all 5 pipelines + 2 converted shadertoy post pipelines, renders bg_color into aTargetand reads back the exact color, runs cell_bg/cell_text/image/bg_image/post passes with no GL error, buffer growth, shader compile-error reporting. If no EGL display/context is available the tests printSKIP:and return (they do run here).
Deviations / notes
Buffer::init_fill: Zig setslen = data.len * sizeof(T)(bug); we store the element count.Pipelinedrop also deletes its FBO and VAO (Zig only destroyed the program).Frameno longer holds the renderer;completereturnsHealthand the caller callsframeCompleted.shadertoy::Uniformsfield order follows Zig (selection_background_colorbeforeselection_foreground_color) while the GLSL block declaresiSelectionForegroundColorbeforeiSelectionBackgroundColor— upstream quirk preserved, flagged for review.- Allocator-failure paths and comptime pipeline-collection type building are replaced by plain structs/
Result.
TODO(integration) / deferred
- Deferred per assignment: generic.zig, cell.zig, image.zig, link.zig, row.zig, State.zig, Thread.zig, Overlay.zig.
opengl::AlphaBlending(config),cursor::RenderStateCursor/TerminalCursorStyle(terminal),size::CellCountInt(=u16),shadertoy::PathItem(configRepeatablePath),message::{ThreadDerivedConfig, RendererDerivedConfig, Flattened, SharedGridKey}stubs,options::Optionslackssurface_mailbox/rt_surface/thread;Message::init_change_configtakes pre-built derived configs.- Metal/WebGL (
Backend::Metal/Webglvariants exist only for parity with backend.zig).