Ports the macOS-specific Zig code behind cfg(target_os = "macos"): - renderer: a GraphicsApi trait with generic::Renderer<G>; Metal backend (all renderer/metal/* files, shaders.metal verbatim, runtime library compile, IOSurface layer, CVDisplayLink), shadertoy -> MSL via naga; OpenGL remains the backend elsewhere. - font: CoreText face, shaper and discovery with build.rs backend selection (coretext default; coretext-freetype / coretext-harfbuzz / coretext-noshape features), no fontconfig/freetype/harfbuzz on Apple. - input: KeymapDarwin via UCKeyTranslate, option-as-alt, mac keycodes. - os: macos, mach, cf_release_thread, NSProcessInfo args and macOS branches; config: macOS defaults, keybinds, Application Support paths. - termio/core: login(1) wrapping, openpty/execvpe, Darwin child exit handling, os_log logging. Compile-checked for aarch64/x86_64-apple-darwin (clippy -D warnings clean) using the cross setup in docs/rewrite/notes/macos-M1.md; cannot run here. The embedded runtime/C API (embedded.zig) is the next step. Also includes the cross-platform integration edits shared by the crash, verification, Windows and macOS work (Cargo target-gating, thread crash-context hooks, apprt-gtk surface/IME refactor, termio/os cfg gates), a cfg fix so ghostty-config builds on wasm32, and an allow for the intentional recursion in the crash overflow test. Test Plan: - cargo test --workspace (3224 passed) - cargo clippy --workspace --all-targets -- -D warnings - cargo check --target aarch64-apple-darwin -p ghostty-renderer (with the documented cross env)
428 lines
14 KiB
Rust
428 lines
14 KiB
Rust
//! 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 api;
|
|
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,
|
|
sync::{Arc, Mutex, PoisonError},
|
|
};
|
|
|
|
use ghostty_font::atlas::{Atlas, Format as AtlasFormat};
|
|
|
|
use self::gl::{
|
|
BufferTarget,
|
|
BufferUsage,
|
|
FramebufferTarget,
|
|
InternalFormat,
|
|
MagFilter,
|
|
MinFilter,
|
|
PixelFormat,
|
|
TextureTarget,
|
|
Wrap,
|
|
};
|
|
pub use self::{
|
|
api::GlFrame,
|
|
buffer::Buffer,
|
|
frame::Frame,
|
|
gl::Gl,
|
|
pipeline::Pipeline,
|
|
render_pass::RenderPass,
|
|
sampler::Sampler,
|
|
target::Target,
|
|
texture::Texture,
|
|
};
|
|
use crate::shadertoy;
|
|
|
|
/// 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`.
|
|
pub use ghostty_config::types_b::AlphaBlending;
|
|
|
|
#[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.
|
|
/// Shared with in-flight [`Frame`]s, which record what they present.
|
|
last_target: LastTarget,
|
|
}
|
|
|
|
/// Slot holding the most recently presented target.
|
|
pub type LastTarget = Arc<Mutex<Option<Arc<Target>>>>;
|
|
|
|
/// Present `target` by blitting it to the default framebuffer, remembering it
|
|
/// in `last_target` in case we need to repeat it.
|
|
fn present_target(gl: &Gl, last_target: &LastTarget, target: &Arc<Target>) -> Result<(), Error> {
|
|
// 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.
|
|
*last_target.lock().unwrap_or_else(PoisonError::into_inner) = Some(Arc::clone(target));
|
|
Ok(())
|
|
}
|
|
|
|
pub use crate::graphics_api::ImageTextureFormat;
|
|
|
|
/// The GL pixel format of an [`ImageTextureFormat`].
|
|
fn to_pixel_format(format: ImageTextureFormat) -> PixelFormat {
|
|
match format {
|
|
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: LastTarget::default(),
|
|
}
|
|
}
|
|
|
|
/// 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: LastTarget::default(),
|
|
}
|
|
}
|
|
|
|
/// 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(&self, target: &Arc<Target>) -> Result<(), Error> {
|
|
present_target(self.gl_or_err()?, &self.last_target, target)
|
|
}
|
|
|
|
/// Present the last presented target again.
|
|
pub fn present_last_target(&self) -> Result<(), Error> {
|
|
let last = self
|
|
.last_target
|
|
.lock()
|
|
.unwrap_or_else(PoisonError::into_inner)
|
|
.clone();
|
|
if let Some(target) = last {
|
|
self.present(&target)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// The most recently presented target, if any (also set when the blit to
|
|
/// the default framebuffer is a no-op, e.g. on a surfaceless context).
|
|
pub fn last_target(&self) -> Option<Arc<Target>> {
|
|
self.last_target
|
|
.lock()
|
|
.unwrap_or_else(PoisonError::into_inner)
|
|
.clone()
|
|
}
|
|
|
|
/// 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(&self) {
|
|
*self
|
|
.last_target
|
|
.lock()
|
|
.unwrap_or_else(PoisonError::into_inner) = None;
|
|
}
|
|
|
|
/// The slot [`Frame`]s record the target they present in.
|
|
pub(crate) fn last_target_slot(&self) -> LastTarget {
|
|
Arc::clone(&self.last_target)
|
|
}
|
|
|
|
/// 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: to_pixel_format(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(&self, target: Arc<Target>) -> Frame {
|
|
Frame::begin(frame::Options::default(), self, target)
|
|
}
|
|
}
|