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
@@ -5,9 +5,42 @@ edition.workspace = true
|
|||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ghostty-config.workspace = true
|
bytemuck = { version = "1.25.2", features = ["derive"] }
|
||||||
ghostty-font.workspace = true
|
ghostty-font.workspace = true
|
||||||
ghostty-terminal.workspace = true
|
glow = "0.18.0"
|
||||||
|
khronos-egl = { version = "6.0.0", features = ["dynamic"] }
|
||||||
|
log = "0.4.34"
|
||||||
|
naga = { version = "30.0.1", features = ["glsl-in", "glsl-out", "wgsl-in"] }
|
||||||
|
thiserror = "2.0.21"
|
||||||
|
|
||||||
[lints]
|
[lints.rust]
|
||||||
workspace = true
|
unsafe_code = "deny"
|
||||||
|
|
||||||
|
[lints.clippy]
|
||||||
|
pedantic = { level = "warn", priority = -1 }
|
||||||
|
todo = "warn"
|
||||||
|
unwrap_used = "warn"
|
||||||
|
# Porting a low-level terminal emulator involves a lot of intentional
|
||||||
|
# numeric casts and long state-machine functions; these pedantic lints are
|
||||||
|
# noise for this codebase.
|
||||||
|
cast_possible_truncation = "allow"
|
||||||
|
cast_possible_wrap = "allow"
|
||||||
|
cast_sign_loss = "allow"
|
||||||
|
cast_precision_loss = "allow"
|
||||||
|
cast_lossless = "allow"
|
||||||
|
module_name_repetitions = "allow"
|
||||||
|
too_many_lines = "allow"
|
||||||
|
missing_errors_doc = "allow"
|
||||||
|
missing_panics_doc = "allow"
|
||||||
|
similar_names = "allow"
|
||||||
|
struct_excessive_bools = "allow"
|
||||||
|
fn_params_excessive_bools = "allow"
|
||||||
|
match_same_arms = "allow"
|
||||||
|
unreadable_literal = "allow"
|
||||||
|
many_single_char_names = "allow"
|
||||||
|
doc_markdown = "allow"
|
||||||
|
must_use_candidate = "allow"
|
||||||
|
return_self_not_must_use = "allow"
|
||||||
|
used_underscore_binding = "allow"
|
||||||
|
large_stack_arrays = "allow"
|
||||||
|
inline_always = "allow"
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#include "common.glsl"
|
||||||
|
|
||||||
|
// Must declare this output for some versions of OpenGL.
|
||||||
|
layout(location = 0) out vec4 out_FragColor;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0;
|
||||||
|
|
||||||
|
out_FragColor = load_color(
|
||||||
|
unpack4u8(bg_color_packed_4u8),
|
||||||
|
use_linear_blending
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
#include "common.glsl"
|
||||||
|
|
||||||
|
// Position the FragCoord origin to the upper left
|
||||||
|
// so as to align with our texture's directionality.
|
||||||
|
layout(origin_upper_left) in vec4 gl_FragCoord;
|
||||||
|
|
||||||
|
layout(binding = 0) uniform sampler2D image;
|
||||||
|
|
||||||
|
flat in vec4 bg_color;
|
||||||
|
flat in vec2 offset;
|
||||||
|
flat in vec2 scale;
|
||||||
|
flat in float opacity;
|
||||||
|
flat in uint repeat;
|
||||||
|
|
||||||
|
layout(location = 0) out vec4 out_FragColor;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0;
|
||||||
|
|
||||||
|
// Our texture coordinate is based on the screen position, offset by the
|
||||||
|
// dest rect origin, and scaled by the ratio between the dest rect size
|
||||||
|
// and the original texture size, which effectively scales the original
|
||||||
|
// size of the texture to the dest rect size.
|
||||||
|
vec2 tex_coord = (gl_FragCoord.xy - offset) * scale;
|
||||||
|
|
||||||
|
vec2 tex_size = textureSize(image, 0);
|
||||||
|
|
||||||
|
// If we need to repeat the texture, wrap the coordinates.
|
||||||
|
if (repeat != 0) {
|
||||||
|
tex_coord = mod(mod(tex_coord, tex_size) + tex_size, tex_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec4 rgba;
|
||||||
|
// If we're out of bounds, we have no color,
|
||||||
|
// otherwise we sample the texture for it.
|
||||||
|
if (any(lessThan(tex_coord, vec2(0.0))) ||
|
||||||
|
any(greaterThan(tex_coord, tex_size)))
|
||||||
|
{
|
||||||
|
rgba = vec4(0.0);
|
||||||
|
} else {
|
||||||
|
// We divide by the texture size to normalize for sampling.
|
||||||
|
rgba = texture(image, tex_coord / tex_size);
|
||||||
|
|
||||||
|
if (!use_linear_blending) {
|
||||||
|
rgba = unlinearize(rgba);
|
||||||
|
}
|
||||||
|
|
||||||
|
rgba.rgb *= rgba.a;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiply it by the configured opacity, but cap it at
|
||||||
|
// the value that will make it fully opaque relative to
|
||||||
|
// the background color alpha, so it isn't overexposed.
|
||||||
|
rgba *= min(opacity, 1.0 / bg_color.a);
|
||||||
|
|
||||||
|
// Blend it on to a fully opaque version of the background color.
|
||||||
|
rgba += max(vec4(0.0), vec4(bg_color.rgb, 1.0) * vec4(1.0 - rgba.a));
|
||||||
|
|
||||||
|
// Multiply everything by the background color alpha.
|
||||||
|
rgba *= bg_color.a;
|
||||||
|
|
||||||
|
out_FragColor = rgba;
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
#include "common.glsl"
|
||||||
|
|
||||||
|
layout(binding = 0) uniform sampler2D image;
|
||||||
|
|
||||||
|
layout(location = 0) in float in_opacity;
|
||||||
|
layout(location = 1) in uint info;
|
||||||
|
|
||||||
|
// 4 bits of info.
|
||||||
|
const uint BG_IMAGE_POSITION = 15u;
|
||||||
|
const uint BG_IMAGE_TL = 0u;
|
||||||
|
const uint BG_IMAGE_TC = 1u;
|
||||||
|
const uint BG_IMAGE_TR = 2u;
|
||||||
|
const uint BG_IMAGE_ML = 3u;
|
||||||
|
const uint BG_IMAGE_MC = 4u;
|
||||||
|
const uint BG_IMAGE_MR = 5u;
|
||||||
|
const uint BG_IMAGE_BL = 6u;
|
||||||
|
const uint BG_IMAGE_BC = 7u;
|
||||||
|
const uint BG_IMAGE_BR = 8u;
|
||||||
|
|
||||||
|
// 2 bits of info shifted 4.
|
||||||
|
const uint BG_IMAGE_FIT = 3u << 4;
|
||||||
|
const uint BG_IMAGE_CONTAIN = 0u << 4;
|
||||||
|
const uint BG_IMAGE_COVER = 1u << 4;
|
||||||
|
const uint BG_IMAGE_STRETCH = 2u << 4;
|
||||||
|
const uint BG_IMAGE_NO_FIT = 3u << 4;
|
||||||
|
|
||||||
|
// 1 bit of info shifted 6.
|
||||||
|
const uint BG_IMAGE_REPEAT = 1u << 6;
|
||||||
|
|
||||||
|
flat out vec4 bg_color;
|
||||||
|
flat out vec2 offset;
|
||||||
|
flat out vec2 scale;
|
||||||
|
flat out float opacity;
|
||||||
|
// We use a uint to pass the repeat value because
|
||||||
|
// bools aren't allowed for vertex outputs in OpenGL.
|
||||||
|
flat out uint repeat;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0;
|
||||||
|
|
||||||
|
vec4 position;
|
||||||
|
position.x = (gl_VertexID == 2) ? 3.0 : -1.0;
|
||||||
|
position.y = (gl_VertexID == 0) ? -3.0 : 1.0;
|
||||||
|
position.z = 1.0;
|
||||||
|
position.w = 1.0;
|
||||||
|
|
||||||
|
// Single triangle is clipped to viewport.
|
||||||
|
//
|
||||||
|
// X <- vid == 0: (-1, -3)
|
||||||
|
// |\
|
||||||
|
// | \
|
||||||
|
// | \
|
||||||
|
// |###\
|
||||||
|
// |#+# \ `+` is (0, 0). `#`s are viewport area.
|
||||||
|
// |### \
|
||||||
|
// X------X <- vid == 2: (3, 1)
|
||||||
|
// ^
|
||||||
|
// vid == 1: (-1, 1)
|
||||||
|
|
||||||
|
gl_Position = position;
|
||||||
|
|
||||||
|
opacity = in_opacity;
|
||||||
|
|
||||||
|
repeat = info & BG_IMAGE_REPEAT;
|
||||||
|
|
||||||
|
vec2 screen_size = screen_size;
|
||||||
|
vec2 tex_size = textureSize(image, 0);
|
||||||
|
|
||||||
|
vec2 dest_size = tex_size;
|
||||||
|
switch (info & BG_IMAGE_FIT) {
|
||||||
|
// For `contain` we scale by a factor that makes the image
|
||||||
|
// width match the screen width or makes the image height
|
||||||
|
// match the screen height, whichever is smaller.
|
||||||
|
case BG_IMAGE_CONTAIN: {
|
||||||
|
float scale = min(screen_size.x / tex_size.x, screen_size.y / tex_size.y);
|
||||||
|
dest_size = tex_size * scale;
|
||||||
|
} break;
|
||||||
|
|
||||||
|
// For `cover` we scale by a factor that makes the image
|
||||||
|
// width match the screen width or makes the image height
|
||||||
|
// match the screen height, whichever is larger.
|
||||||
|
case BG_IMAGE_COVER: {
|
||||||
|
float scale = max(screen_size.x / tex_size.x, screen_size.y / tex_size.y);
|
||||||
|
dest_size = tex_size * scale;
|
||||||
|
} break;
|
||||||
|
|
||||||
|
// For `stretch` we stretch the image to the size of
|
||||||
|
// the screen without worrying about aspect ratio.
|
||||||
|
case BG_IMAGE_STRETCH: {
|
||||||
|
dest_size = screen_size;
|
||||||
|
} break;
|
||||||
|
|
||||||
|
// For `none` we just use the original texture size.
|
||||||
|
case BG_IMAGE_NO_FIT: {
|
||||||
|
dest_size = tex_size;
|
||||||
|
} break;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec2 start = vec2(0.0);
|
||||||
|
vec2 mid = (screen_size - dest_size) / vec2(2.0);
|
||||||
|
vec2 end = screen_size - dest_size;
|
||||||
|
|
||||||
|
vec2 dest_offset = mid;
|
||||||
|
switch (info & BG_IMAGE_POSITION) {
|
||||||
|
case BG_IMAGE_TL: {
|
||||||
|
dest_offset = vec2(start.x, start.y);
|
||||||
|
} break;
|
||||||
|
case BG_IMAGE_TC: {
|
||||||
|
dest_offset = vec2(mid.x, start.y);
|
||||||
|
} break;
|
||||||
|
case BG_IMAGE_TR: {
|
||||||
|
dest_offset = vec2(end.x, start.y);
|
||||||
|
} break;
|
||||||
|
case BG_IMAGE_ML: {
|
||||||
|
dest_offset = vec2(start.x, mid.y);
|
||||||
|
} break;
|
||||||
|
case BG_IMAGE_MC: {
|
||||||
|
dest_offset = vec2(mid.x, mid.y);
|
||||||
|
} break;
|
||||||
|
case BG_IMAGE_MR: {
|
||||||
|
dest_offset = vec2(end.x, mid.y);
|
||||||
|
} break;
|
||||||
|
case BG_IMAGE_BL: {
|
||||||
|
dest_offset = vec2(start.x, end.y);
|
||||||
|
} break;
|
||||||
|
case BG_IMAGE_BC: {
|
||||||
|
dest_offset = vec2(mid.x, end.y);
|
||||||
|
} break;
|
||||||
|
case BG_IMAGE_BR: {
|
||||||
|
dest_offset = vec2(end.x, end.y);
|
||||||
|
} break;
|
||||||
|
}
|
||||||
|
|
||||||
|
offset = dest_offset;
|
||||||
|
scale = tex_size / dest_size;
|
||||||
|
|
||||||
|
// We load a fully opaque version of the bg color and combine it with
|
||||||
|
// the alpha separately, because we need these as separate values in
|
||||||
|
// the framgment shader.
|
||||||
|
uvec4 u_bg_color = unpack4u8(bg_color_packed_4u8);
|
||||||
|
bg_color = vec4(load_color(
|
||||||
|
uvec4(u_bg_color.rgb, 255),
|
||||||
|
use_linear_blending
|
||||||
|
).rgb, float(u_bg_color.a) / 255.0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
#include "common.glsl"
|
||||||
|
|
||||||
|
// Position the origin to the upper left
|
||||||
|
layout(origin_upper_left) in vec4 gl_FragCoord;
|
||||||
|
|
||||||
|
// Must declare this output for some versions of OpenGL.
|
||||||
|
layout(location = 0) out vec4 out_FragColor;
|
||||||
|
|
||||||
|
layout(binding = 1, std430) readonly buffer bg_cells {
|
||||||
|
uint cells[];
|
||||||
|
};
|
||||||
|
|
||||||
|
vec4 cell_bg() {
|
||||||
|
uvec2 grid_size = unpack2u16(grid_size_packed_2u16);
|
||||||
|
ivec2 grid_pos = ivec2(floor((gl_FragCoord.xy - grid_padding.wx) / cell_size));
|
||||||
|
bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0;
|
||||||
|
|
||||||
|
vec4 bg = vec4(0.0);
|
||||||
|
|
||||||
|
// Clamp x position, extends edge bg colors in to padding on sides.
|
||||||
|
if (grid_pos.x < 0) {
|
||||||
|
if ((padding_extend & EXTEND_LEFT) != 0) {
|
||||||
|
grid_pos.x = 0;
|
||||||
|
} else {
|
||||||
|
return bg;
|
||||||
|
}
|
||||||
|
} else if (grid_pos.x > grid_size.x - 1) {
|
||||||
|
if ((padding_extend & EXTEND_RIGHT) != 0) {
|
||||||
|
grid_pos.x = int(grid_size.x) - 1;
|
||||||
|
} else {
|
||||||
|
return bg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clamp y position if we should extend, otherwise discard if out of bounds.
|
||||||
|
if (grid_pos.y < 0) {
|
||||||
|
if ((padding_extend & EXTEND_UP) != 0) {
|
||||||
|
grid_pos.y = 0;
|
||||||
|
} else {
|
||||||
|
return bg;
|
||||||
|
}
|
||||||
|
} else if (grid_pos.y > grid_size.y - 1) {
|
||||||
|
if ((padding_extend & EXTEND_DOWN) != 0) {
|
||||||
|
grid_pos.y = int(grid_size.y) - 1;
|
||||||
|
} else {
|
||||||
|
return bg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the color for the cell.
|
||||||
|
vec4 cell_color = load_color(
|
||||||
|
unpack4u8(cells[grid_pos.y * grid_size.x + grid_pos.x]),
|
||||||
|
use_linear_blending
|
||||||
|
);
|
||||||
|
|
||||||
|
return cell_color;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
out_FragColor = cell_bg();
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
#include "common.glsl"
|
||||||
|
|
||||||
|
layout(binding = 0) uniform sampler2DRect atlas_grayscale;
|
||||||
|
layout(binding = 1) uniform sampler2DRect atlas_color;
|
||||||
|
|
||||||
|
in CellTextVertexOut {
|
||||||
|
flat uint atlas;
|
||||||
|
flat vec4 color;
|
||||||
|
flat vec4 bg_color;
|
||||||
|
vec2 tex_coord;
|
||||||
|
} in_data;
|
||||||
|
|
||||||
|
// Values `atlas` can take.
|
||||||
|
const uint ATLAS_GRAYSCALE = 0u;
|
||||||
|
const uint ATLAS_COLOR = 1u;
|
||||||
|
|
||||||
|
// Must declare this output for some versions of OpenGL.
|
||||||
|
layout(location = 0) out vec4 out_FragColor;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0;
|
||||||
|
bool use_linear_correction = (bools & USE_LINEAR_CORRECTION) != 0;
|
||||||
|
|
||||||
|
switch (in_data.atlas) {
|
||||||
|
default:
|
||||||
|
case ATLAS_GRAYSCALE:
|
||||||
|
{
|
||||||
|
// Our input color is always linear.
|
||||||
|
vec4 color = in_data.color;
|
||||||
|
|
||||||
|
// If we're not doing linear blending, then we need to
|
||||||
|
// re-apply the gamma encoding to our color manually.
|
||||||
|
//
|
||||||
|
// Since the alpha is premultiplied, we need to divide
|
||||||
|
// it out before unlinearizing and re-multiply it after.
|
||||||
|
if (!use_linear_blending) {
|
||||||
|
color.rgb /= vec3(color.a);
|
||||||
|
color = unlinearize(color);
|
||||||
|
color.rgb *= vec3(color.a);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch our alpha mask for this pixel.
|
||||||
|
float a = texture(atlas_grayscale, in_data.tex_coord).r;
|
||||||
|
|
||||||
|
// Linear blending weight correction corrects the alpha value to
|
||||||
|
// produce blending results which match gamma-incorrect blending.
|
||||||
|
if (use_linear_correction) {
|
||||||
|
// Short explanation of how this works:
|
||||||
|
//
|
||||||
|
// We get the luminances of the foreground and background colors,
|
||||||
|
// and then unlinearize them and perform blending on them. This
|
||||||
|
// gives us our desired luminance, which we derive our new alpha
|
||||||
|
// value from by mapping the range [bg_l, fg_l] to [0, 1], since
|
||||||
|
// our final blend will be a linear interpolation from bg to fg.
|
||||||
|
//
|
||||||
|
// This yields virtually identical results for grayscale blending,
|
||||||
|
// and very similar but non-identical results for color blending.
|
||||||
|
vec4 bg = in_data.bg_color;
|
||||||
|
float fg_l = luminance(color.rgb);
|
||||||
|
float bg_l = luminance(bg.rgb);
|
||||||
|
// To avoid numbers going haywire, we don't apply correction
|
||||||
|
// when the bg and fg luminances are within 0.001 of each other.
|
||||||
|
if (abs(fg_l - bg_l) > 0.001) {
|
||||||
|
float blend_l = linearize(unlinearize(fg_l) * a + unlinearize(bg_l) * (1.0 - a));
|
||||||
|
a = clamp((blend_l - bg_l) / (fg_l - bg_l), 0.0, 1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiply our whole color by the alpha mask.
|
||||||
|
// Since we use premultiplied alpha, this is
|
||||||
|
// the correct way to apply the mask.
|
||||||
|
color *= a;
|
||||||
|
|
||||||
|
out_FragColor = color;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
case ATLAS_COLOR:
|
||||||
|
{
|
||||||
|
// For now, we assume that color glyphs
|
||||||
|
// are already premultiplied linear colors.
|
||||||
|
vec4 color = texture(atlas_color, in_data.tex_coord);
|
||||||
|
|
||||||
|
// If we are doing linear blending, we can return this right away.
|
||||||
|
if (use_linear_blending) {
|
||||||
|
out_FragColor = color;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise we need to unlinearize the color. Since the alpha is
|
||||||
|
// premultiplied, we need to divide it out before unlinearizing.
|
||||||
|
color.rgb /= vec3(color.a);
|
||||||
|
color = unlinearize(color);
|
||||||
|
color.rgb *= vec3(color.a);
|
||||||
|
|
||||||
|
out_FragColor = color;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
#include "common.glsl"
|
||||||
|
|
||||||
|
// The position of the glyph in the texture (x, y)
|
||||||
|
layout(location = 0) in uvec2 glyph_pos;
|
||||||
|
|
||||||
|
// The size of the glyph in the texture (w, h)
|
||||||
|
layout(location = 1) in uvec2 glyph_size;
|
||||||
|
|
||||||
|
// The left and top bearings for the glyph (x, y)
|
||||||
|
layout(location = 2) in ivec2 bearings;
|
||||||
|
|
||||||
|
// The grid coordinates (x, y) where x < columns and y < rows
|
||||||
|
layout(location = 3) in uvec2 grid_pos;
|
||||||
|
|
||||||
|
// The color of the rendered text glyph.
|
||||||
|
layout(location = 4) in uvec4 color;
|
||||||
|
|
||||||
|
// Which atlas this glyph is in.
|
||||||
|
layout(location = 5) in uint atlas;
|
||||||
|
|
||||||
|
// Misc glyph properties.
|
||||||
|
layout(location = 6) in uint glyph_bools;
|
||||||
|
|
||||||
|
// Values `atlas` can take.
|
||||||
|
const uint ATLAS_GRAYSCALE = 0u;
|
||||||
|
const uint ATLAS_COLOR = 1u;
|
||||||
|
|
||||||
|
// Masks for the `glyph_bools` attribute
|
||||||
|
const uint NO_MIN_CONTRAST = 1u;
|
||||||
|
const uint IS_CURSOR_GLYPH = 2u;
|
||||||
|
|
||||||
|
out CellTextVertexOut {
|
||||||
|
flat uint atlas;
|
||||||
|
flat vec4 color;
|
||||||
|
flat vec4 bg_color;
|
||||||
|
vec2 tex_coord;
|
||||||
|
} out_data;
|
||||||
|
|
||||||
|
layout(binding = 1, std430) readonly buffer bg_cells {
|
||||||
|
uint bg_colors[];
|
||||||
|
};
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
uvec2 grid_size = unpack2u16(grid_size_packed_2u16);
|
||||||
|
uvec2 cursor_pos = unpack2u16(cursor_pos_packed_2u16);
|
||||||
|
bool cursor_wide = (bools & CURSOR_WIDE) != 0;
|
||||||
|
bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0;
|
||||||
|
|
||||||
|
// Convert the grid x, y into world space x, y by accounting for cell size
|
||||||
|
vec2 cell_pos = cell_size * vec2(grid_pos);
|
||||||
|
|
||||||
|
int vid = gl_VertexID;
|
||||||
|
|
||||||
|
// We use a triangle strip with 4 vertices to render quads,
|
||||||
|
// so we determine which corner of the cell this vertex is in
|
||||||
|
// based on the vertex ID.
|
||||||
|
//
|
||||||
|
// 0 --> 1
|
||||||
|
// | .'|
|
||||||
|
// | / |
|
||||||
|
// | L |
|
||||||
|
// 2 --> 3
|
||||||
|
//
|
||||||
|
// 0 = top-left (0, 0)
|
||||||
|
// 1 = top-right (1, 0)
|
||||||
|
// 2 = bot-left (0, 1)
|
||||||
|
// 3 = bot-right (1, 1)
|
||||||
|
vec2 corner;
|
||||||
|
corner.x = float(vid == 1 || vid == 3);
|
||||||
|
corner.y = float(vid == 2 || vid == 3);
|
||||||
|
|
||||||
|
out_data.atlas = atlas;
|
||||||
|
|
||||||
|
// === Grid Cell ===
|
||||||
|
// +X
|
||||||
|
// 0,0--...->
|
||||||
|
// |
|
||||||
|
// . offset.x = bearings.x
|
||||||
|
// +Y. .|.
|
||||||
|
// . | |
|
||||||
|
// | cell_pos -> +-------+ _.
|
||||||
|
// v ._| |_. _|- offset.y = cell_size.y - bearings.y
|
||||||
|
// | | .###. | |
|
||||||
|
// | | #...# | |
|
||||||
|
// glyph_size.y -+ | ##### | |
|
||||||
|
// | | #.... | +- bearings.y
|
||||||
|
// |_| .#### | |
|
||||||
|
// | |_|
|
||||||
|
// +-------+
|
||||||
|
// |_._|
|
||||||
|
// |
|
||||||
|
// glyph_size.x
|
||||||
|
//
|
||||||
|
// In order to get the top left of the glyph, we compute an offset based on
|
||||||
|
// the bearings. The Y bearing is the distance from the bottom of the cell
|
||||||
|
// to the top of the glyph, so we subtract it from the cell height to get
|
||||||
|
// the y offset. The X bearing is the distance from the left of the cell
|
||||||
|
// to the left of the glyph, so it works as the x offset directly.
|
||||||
|
|
||||||
|
vec2 size = vec2(glyph_size);
|
||||||
|
vec2 offset = vec2(bearings);
|
||||||
|
|
||||||
|
offset.y = cell_size.y - offset.y;
|
||||||
|
|
||||||
|
// Calculate the final position of the cell which uses our glyph size
|
||||||
|
// and glyph offset to create the correct bounding box for the glyph.
|
||||||
|
cell_pos = cell_pos + size * corner + offset;
|
||||||
|
gl_Position = projection_matrix * vec4(cell_pos.x, cell_pos.y, 0.0f, 1.0f);
|
||||||
|
|
||||||
|
// Calculate the texture coordinate in pixels. This is NOT normalized
|
||||||
|
// (between 0.0 and 1.0), and does not need to be, since the texture will
|
||||||
|
// be sampled with pixel coordinate mode.
|
||||||
|
out_data.tex_coord = vec2(glyph_pos) + vec2(glyph_size) * corner;
|
||||||
|
|
||||||
|
// Get our color. We always fetch a linearized version to
|
||||||
|
// make it easier to handle minimum contrast calculations.
|
||||||
|
out_data.color = load_color(color, true);
|
||||||
|
// Get the BG color
|
||||||
|
out_data.bg_color = load_color(
|
||||||
|
unpack4u8(bg_colors[grid_pos.y * grid_size.x + grid_pos.x]),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
// Blend it with the global bg color
|
||||||
|
vec4 global_bg = load_color(
|
||||||
|
unpack4u8(bg_color_packed_4u8),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
out_data.bg_color += global_bg * vec4(1.0 - out_data.bg_color.a);
|
||||||
|
|
||||||
|
// If we have a minimum contrast, we need to check if we need to
|
||||||
|
// change the color of the text to ensure it has enough contrast
|
||||||
|
// with the background.
|
||||||
|
if (min_contrast > 1.0f && (glyph_bools & NO_MIN_CONTRAST) == 0) {
|
||||||
|
// Ensure our minimum contrast
|
||||||
|
out_data.color = contrasted_color(min_contrast, out_data.color, out_data.bg_color);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if current position is under cursor (including wide cursor)
|
||||||
|
bool is_cursor_pos = ((grid_pos.x == cursor_pos.x) || (cursor_wide && (grid_pos.x == (cursor_pos.x + 1)))) && (grid_pos.y == cursor_pos.y);
|
||||||
|
|
||||||
|
// If this cell is the cursor cell, but we're not processing
|
||||||
|
// the cursor glyph itself, then we need to change the color.
|
||||||
|
if ((glyph_bools & IS_CURSOR_GLYPH) == 0 && is_cursor_pos) {
|
||||||
|
out_data.color = load_color(unpack4u8(cursor_color_packed_4u8), use_linear_blending);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
#version 430 core
|
||||||
|
|
||||||
|
// These are common definitions to be shared across shaders, the first
|
||||||
|
// line of any shader that needs these should be `#include "common.glsl"`.
|
||||||
|
//
|
||||||
|
// Included in this file are:
|
||||||
|
// - The interface block for the global uniforms.
|
||||||
|
// - Functions for unpacking values.
|
||||||
|
// - Functions for working with colors.
|
||||||
|
|
||||||
|
//----------------------------------------------------------------------------//
|
||||||
|
// Global Uniforms
|
||||||
|
//----------------------------------------------------------------------------//
|
||||||
|
layout(binding = 1, std140) uniform Globals {
|
||||||
|
uniform mat4 projection_matrix;
|
||||||
|
uniform vec2 screen_size;
|
||||||
|
uniform vec2 cell_size;
|
||||||
|
uniform uint grid_size_packed_2u16;
|
||||||
|
uniform vec4 grid_padding;
|
||||||
|
uniform uint padding_extend;
|
||||||
|
uniform float min_contrast;
|
||||||
|
uniform uint cursor_pos_packed_2u16;
|
||||||
|
uniform uint cursor_color_packed_4u8;
|
||||||
|
uniform uint bg_color_packed_4u8;
|
||||||
|
uniform uint bools;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Bools
|
||||||
|
const uint CURSOR_WIDE = 1u;
|
||||||
|
const uint USE_DISPLAY_P3 = 2u;
|
||||||
|
const uint USE_LINEAR_BLENDING = 4u;
|
||||||
|
const uint USE_LINEAR_CORRECTION = 8u;
|
||||||
|
|
||||||
|
// Padding extend enum
|
||||||
|
const uint EXTEND_LEFT = 1u;
|
||||||
|
const uint EXTEND_RIGHT = 2u;
|
||||||
|
const uint EXTEND_UP = 4u;
|
||||||
|
const uint EXTEND_DOWN = 8u;
|
||||||
|
|
||||||
|
//----------------------------------------------------------------------------//
|
||||||
|
// Functions for Unpacking Values
|
||||||
|
//----------------------------------------------------------------------------//
|
||||||
|
// NOTE: These unpack functions assume little-endian.
|
||||||
|
// If this ever becomes a problem... oh dear!
|
||||||
|
|
||||||
|
uvec4 unpack4u8(uint packed_value) {
|
||||||
|
return uvec4(
|
||||||
|
uint(packed_value >> 0) & uint(0xFF),
|
||||||
|
uint(packed_value >> 8) & uint(0xFF),
|
||||||
|
uint(packed_value >> 16) & uint(0xFF),
|
||||||
|
uint(packed_value >> 24) & uint(0xFF)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
uvec2 unpack2u16(uint packed_value) {
|
||||||
|
return uvec2(
|
||||||
|
uint(packed_value >> 0) & uint(0xFFFF),
|
||||||
|
uint(packed_value >> 16) & uint(0xFFFF)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ivec2 unpack2i16(int packed_value) {
|
||||||
|
return ivec2(
|
||||||
|
(packed_value << 16) >> 16,
|
||||||
|
(packed_value << 0) >> 16
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
//----------------------------------------------------------------------------//
|
||||||
|
// Color Functions
|
||||||
|
//----------------------------------------------------------------------------//
|
||||||
|
|
||||||
|
// Compute the luminance of the provided color.
|
||||||
|
//
|
||||||
|
// Takes colors in linear RGB space. If your colors are gamma
|
||||||
|
// encoded, linearize them before using them with this function.
|
||||||
|
float luminance(vec3 color) {
|
||||||
|
return dot(color, vec3(0.2126f, 0.7152f, 0.0722f));
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
|
||||||
|
//
|
||||||
|
// Takes colors in linear RGB space. If your colors are gamma
|
||||||
|
// encoded, linearize them before using them with this function.
|
||||||
|
float contrast_ratio(vec3 color1, vec3 color2) {
|
||||||
|
float luminance1 = luminance(color1) + 0.05;
|
||||||
|
float luminance2 = luminance(color2) + 0.05;
|
||||||
|
return max(luminance1, luminance2) / min(luminance1, luminance2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the fg if the contrast ratio is greater than min, otherwise
|
||||||
|
// return a color that satisfies the contrast ratio. Currently, the color
|
||||||
|
// is always white or black, whichever has the highest contrast ratio.
|
||||||
|
//
|
||||||
|
// Takes colors in linear RGB space. If your colors are gamma
|
||||||
|
// encoded, linearize them before using them with this function.
|
||||||
|
vec4 contrasted_color(float min_ratio, vec4 fg, vec4 bg) {
|
||||||
|
float ratio = contrast_ratio(fg.rgb, bg.rgb);
|
||||||
|
if (ratio < min_ratio) {
|
||||||
|
float white_ratio = contrast_ratio(vec3(1.0, 1.0, 1.0), bg.rgb);
|
||||||
|
float black_ratio = contrast_ratio(vec3(0.0, 0.0, 0.0), bg.rgb);
|
||||||
|
if (white_ratio > black_ratio) {
|
||||||
|
return vec4(1.0);
|
||||||
|
} else {
|
||||||
|
return vec4(0.0, 0.0, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fg;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converts a color from sRGB gamma encoding to linear.
|
||||||
|
vec4 linearize(vec4 srgb) {
|
||||||
|
bvec3 cutoff = lessThanEqual(srgb.rgb, vec3(0.04045));
|
||||||
|
vec3 higher = pow((srgb.rgb + vec3(0.055)) / vec3(1.055), vec3(2.4));
|
||||||
|
vec3 lower = srgb.rgb / vec3(12.92);
|
||||||
|
|
||||||
|
return vec4(mix(higher, lower, cutoff), srgb.a);
|
||||||
|
}
|
||||||
|
float linearize(float v) {
|
||||||
|
return v <= 0.04045 ? v / 12.92 : pow((v + 0.055) / 1.055, 2.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converts a color from linear to sRGB gamma encoding.
|
||||||
|
vec4 unlinearize(vec4 linear) {
|
||||||
|
bvec3 cutoff = lessThanEqual(linear.rgb, vec3(0.0031308));
|
||||||
|
vec3 higher = pow(linear.rgb, vec3(1.0 / 2.4)) * vec3(1.055) - vec3(0.055);
|
||||||
|
vec3 lower = linear.rgb * vec3(12.92);
|
||||||
|
|
||||||
|
return vec4(mix(higher, lower, cutoff), linear.a);
|
||||||
|
}
|
||||||
|
float unlinearize(float v) {
|
||||||
|
return v <= 0.0031308 ? v * 12.92 : pow(v, 1.0 / 2.4) * 1.055 - 0.055;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load a 4 byte RGBA non-premultiplied color and linearize
|
||||||
|
// and convert it as necessary depending on the provided info.
|
||||||
|
//
|
||||||
|
// `linear` controls whether the returned color is linear or gamma encoded.
|
||||||
|
vec4 load_color(
|
||||||
|
uvec4 in_color,
|
||||||
|
bool linear
|
||||||
|
) {
|
||||||
|
// 0 .. 255 -> 0.0 .. 1.0
|
||||||
|
vec4 color = vec4(in_color) / vec4(255.0f);
|
||||||
|
|
||||||
|
// Linearize if necessary.
|
||||||
|
if (linear) color = linearize(color);
|
||||||
|
|
||||||
|
// Premultiply our color by its alpha.
|
||||||
|
color.rgb *= color.a;
|
||||||
|
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
//----------------------------------------------------------------------------//
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
#version 330 core
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec4 position;
|
||||||
|
position.x = (gl_VertexID == 2) ? 3.0 : -1.0;
|
||||||
|
position.y = (gl_VertexID == 0) ? -3.0 : 1.0;
|
||||||
|
position.z = 1.0;
|
||||||
|
position.w = 1.0;
|
||||||
|
|
||||||
|
// Single triangle is clipped to viewport.
|
||||||
|
//
|
||||||
|
// X <- vid == 0: (-1, -3)
|
||||||
|
// |\
|
||||||
|
// | \
|
||||||
|
// | \
|
||||||
|
// |###\
|
||||||
|
// |#+# \ `+` is (0, 0). `#`s are viewport area.
|
||||||
|
// |### \
|
||||||
|
// X------X <- vid == 2: (3, 1)
|
||||||
|
// ^
|
||||||
|
// vid == 1: (-1, 1)
|
||||||
|
|
||||||
|
gl_Position = position;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
#include "common.glsl"
|
||||||
|
|
||||||
|
layout(binding = 0) uniform sampler2D image;
|
||||||
|
|
||||||
|
in vec2 tex_coord;
|
||||||
|
|
||||||
|
layout(location = 0) out vec4 out_FragColor;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0;
|
||||||
|
|
||||||
|
vec4 rgba = texture(image, tex_coord);
|
||||||
|
|
||||||
|
if (!use_linear_blending) {
|
||||||
|
rgba = unlinearize(rgba);
|
||||||
|
}
|
||||||
|
|
||||||
|
rgba.rgb *= vec3(rgba.a);
|
||||||
|
|
||||||
|
out_FragColor = rgba;
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
#include "common.glsl"
|
||||||
|
|
||||||
|
layout(binding = 0) uniform sampler2D image;
|
||||||
|
|
||||||
|
layout(location = 0) in vec2 grid_pos;
|
||||||
|
layout(location = 1) in vec2 cell_offset;
|
||||||
|
layout(location = 2) in vec4 source_rect;
|
||||||
|
layout(location = 3) in vec2 dest_size;
|
||||||
|
|
||||||
|
out vec2 tex_coord;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
int vid = gl_VertexID;
|
||||||
|
|
||||||
|
// We use a triangle strip with 4 vertices to render quads,
|
||||||
|
// so we determine which corner of the cell this vertex is in
|
||||||
|
// based on the vertex ID.
|
||||||
|
//
|
||||||
|
// 0 --> 1
|
||||||
|
// | .'|
|
||||||
|
// | / |
|
||||||
|
// | L |
|
||||||
|
// 2 --> 3
|
||||||
|
//
|
||||||
|
// 0 = top-left (0, 0)
|
||||||
|
// 1 = top-right (1, 0)
|
||||||
|
// 2 = bot-left (0, 1)
|
||||||
|
// 3 = bot-right (1, 1)
|
||||||
|
vec2 corner;
|
||||||
|
corner.x = float(vid == 1 || vid == 3);
|
||||||
|
corner.y = float(vid == 2 || vid == 3);
|
||||||
|
|
||||||
|
// The texture coordinates start at our source x/y
|
||||||
|
// and add the width/height depending on the corner.
|
||||||
|
tex_coord = source_rect.xy;
|
||||||
|
tex_coord += source_rect.zw * corner;
|
||||||
|
|
||||||
|
// Normalize the coordinates.
|
||||||
|
tex_coord /= textureSize(image, 0);
|
||||||
|
|
||||||
|
// The position of our image starts at the top-left of the grid cell and
|
||||||
|
// adds the source rect width/height components.
|
||||||
|
vec2 image_pos = (cell_size * grid_pos) + cell_offset;
|
||||||
|
image_pos += dest_size * corner;
|
||||||
|
|
||||||
|
gl_Position = projection_matrix * vec4(image_pos.xy, 1.0, 1.0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
#version 430 core
|
||||||
|
|
||||||
|
layout(binding = 1, std140) uniform Globals {
|
||||||
|
uniform vec3 iResolution;
|
||||||
|
uniform float iTime;
|
||||||
|
uniform float iTimeDelta;
|
||||||
|
uniform float iFrameRate;
|
||||||
|
uniform int iFrame;
|
||||||
|
uniform float iChannelTime[4];
|
||||||
|
uniform vec3 iChannelResolution[4];
|
||||||
|
uniform vec4 iMouse;
|
||||||
|
uniform vec4 iDate;
|
||||||
|
uniform float iSampleRate;
|
||||||
|
uniform vec4 iCurrentCursor;
|
||||||
|
uniform vec4 iPreviousCursor;
|
||||||
|
uniform vec4 iCurrentCursorColor;
|
||||||
|
uniform vec4 iPreviousCursorColor;
|
||||||
|
uniform int iCurrentCursorStyle;
|
||||||
|
uniform int iPreviousCursorStyle;
|
||||||
|
uniform int iCursorVisible;
|
||||||
|
uniform float iTimeCursorChange;
|
||||||
|
uniform float iTimeFocus;
|
||||||
|
uniform int iFocus;
|
||||||
|
uniform vec3 iPalette[256];
|
||||||
|
uniform vec3 iBackgroundColor;
|
||||||
|
uniform vec3 iForegroundColor;
|
||||||
|
uniform vec3 iCursorColor;
|
||||||
|
uniform vec3 iCursorText;
|
||||||
|
uniform vec3 iSelectionForegroundColor;
|
||||||
|
uniform vec3 iSelectionBackgroundColor;
|
||||||
|
};
|
||||||
|
|
||||||
|
#define CURSORSTYLE_BLOCK 0
|
||||||
|
#define CURSORSTYLE_BLOCK_HOLLOW 1
|
||||||
|
#define CURSORSTYLE_BAR 2
|
||||||
|
#define CURSORSTYLE_UNDERLINE 3
|
||||||
|
#define CURSORSTYLE_LOCK 4
|
||||||
|
|
||||||
|
layout(binding = 0) uniform sampler2D iChannel0;
|
||||||
|
|
||||||
|
// These are unused currently by Ghostty:
|
||||||
|
// layout(binding = 1) uniform sampler2D iChannel1;
|
||||||
|
// layout(binding = 2) uniform sampler2D iChannel2;
|
||||||
|
// layout(binding = 3) uniform sampler2D iChannel3;
|
||||||
|
|
||||||
|
layout(location = 0) in vec4 gl_FragCoord;
|
||||||
|
layout(location = 0) out vec4 _fragColor;
|
||||||
|
|
||||||
|
#define texture2D texture
|
||||||
|
|
||||||
|
void mainImage( out vec4 fragColor, in vec2 fragCoord );
|
||||||
|
void main() { mainImage (_fragColor, gl_FragCoord.xy); }
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
// This shader is NOT BUILD INTO Ghostty. This is only here for unit tests.
|
||||||
|
|
||||||
|
// Loosely based on postprocessing shader by inigo quilez, License Creative
|
||||||
|
// Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.
|
||||||
|
|
||||||
|
vec2 curve(vec2 uv)
|
||||||
|
{
|
||||||
|
uv = (uv - 0.5) * 2.0;
|
||||||
|
uv *= 1.1;
|
||||||
|
uv.x *= 1.0 + pow((abs(uv.y) / 5.0), 2.0);
|
||||||
|
uv.y *= 1.0 + pow((abs(uv.x) / 4.0), 2.0);
|
||||||
|
uv = (uv / 2.0) + 0.5;
|
||||||
|
uv = uv *0.92 + 0.04;
|
||||||
|
return uv;
|
||||||
|
}
|
||||||
|
void mainImage( out vec4 fragColor, in vec2 fragCoord )
|
||||||
|
{
|
||||||
|
vec2 q = fragCoord.xy / iResolution.xy;
|
||||||
|
vec2 uv = q;
|
||||||
|
uv = curve( uv );
|
||||||
|
vec3 oricol = texture( iChannel0, vec2(q.x,q.y) ).xyz;
|
||||||
|
vec3 col;
|
||||||
|
float x = sin(0.3*iTime+uv.y*21.0)*sin(0.7*iTime+uv.y*29.0)*sin(0.3+0.33*iTime+uv.y*31.0)*0.0017;
|
||||||
|
|
||||||
|
col.r = texture(iChannel0,vec2(x+uv.x+0.001,uv.y+0.001)).x+0.05;
|
||||||
|
col.g = texture(iChannel0,vec2(x+uv.x+0.000,uv.y-0.002)).y+0.05;
|
||||||
|
col.b = texture(iChannel0,vec2(x+uv.x-0.002,uv.y+0.000)).z+0.05;
|
||||||
|
col.r += 0.08*texture(iChannel0,0.75*vec2(x+0.025, -0.027)+vec2(uv.x+0.001,uv.y+0.001)).x;
|
||||||
|
col.g += 0.05*texture(iChannel0,0.75*vec2(x+-0.022, -0.02)+vec2(uv.x+0.000,uv.y-0.002)).y;
|
||||||
|
col.b += 0.08*texture(iChannel0,0.75*vec2(x+-0.02, -0.018)+vec2(uv.x-0.002,uv.y+0.000)).z;
|
||||||
|
|
||||||
|
col = clamp(col*0.6+0.4*col*col*1.0,0.0,1.0);
|
||||||
|
|
||||||
|
float vig = (0.0 + 1.0*16.0*uv.x*uv.y*(1.0-uv.x)*(1.0-uv.y));
|
||||||
|
col *= vec3(pow(vig,0.3));
|
||||||
|
|
||||||
|
col *= vec3(0.95,1.05,0.95);
|
||||||
|
col *= 2.8;
|
||||||
|
|
||||||
|
float scans = clamp( 0.35+0.35*sin(3.5*iTime+uv.y*iResolution.y*1.5), 0.0, 1.0);
|
||||||
|
|
||||||
|
float s = pow(scans,1.7);
|
||||||
|
col = col*vec3( 0.4+0.7*s) ;
|
||||||
|
|
||||||
|
col *= 1.0+0.01*sin(110.0*iTime);
|
||||||
|
if (uv.x < 0.0 || uv.x > 1.0)
|
||||||
|
col *= 0.0;
|
||||||
|
if (uv.y < 0.0 || uv.y > 1.0)
|
||||||
|
col *= 0.0;
|
||||||
|
|
||||||
|
col*=1.0-0.65*vec3(clamp((mod(fragCoord.x, 2.0)-1.0)*2.0,0.0,1.0));
|
||||||
|
|
||||||
|
float comp = smoothstep( 0.1, 0.9, sin(iTime) );
|
||||||
|
|
||||||
|
// Remove the next line to stop cross-fade between original and postprocess
|
||||||
|
// col = mix( col, oricol, comp );
|
||||||
|
|
||||||
|
fragColor = vec4(col,1.0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
// Test shader for iTimeFocus and iFocus
|
||||||
|
// Shows border when focused, green fade that restarts on each focus gain
|
||||||
|
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
||||||
|
vec2 uv = fragCoord / iResolution.xy;
|
||||||
|
|
||||||
|
// Sample the terminal content
|
||||||
|
vec4 terminal = texture2D(iChannel0, uv);
|
||||||
|
vec3 color = terminal.rgb;
|
||||||
|
|
||||||
|
if (iFocus > 0) {
|
||||||
|
// FOCUSED: Add border and fading green overlay
|
||||||
|
|
||||||
|
// Calculate time since focus was gained
|
||||||
|
float timeSinceFocus = iTime - iTimeFocus;
|
||||||
|
|
||||||
|
// Green fade: starts at 1.0 (full green), fades to 0.0 over 3 seconds
|
||||||
|
float fadeOut = max(0.0, 1.0 - (timeSinceFocus / 3.0));
|
||||||
|
|
||||||
|
// Add green overlay that fades out
|
||||||
|
color = mix(color, vec3(0.0, 1.0, 0.0), fadeOut * 0.4);
|
||||||
|
|
||||||
|
// Add border (5 pixels)
|
||||||
|
float borderSize = 5.0;
|
||||||
|
vec2 pixelCoord = fragCoord;
|
||||||
|
bool isBorder = pixelCoord.x < borderSize ||
|
||||||
|
pixelCoord.x > iResolution.x - borderSize ||
|
||||||
|
pixelCoord.y < borderSize ||
|
||||||
|
pixelCoord.y > iResolution.y - borderSize;
|
||||||
|
|
||||||
|
if (isBorder) {
|
||||||
|
// Bright cyan border that pulses subtly
|
||||||
|
float pulse = sin(timeSinceFocus * 2.0) * 0.1 + 0.9;
|
||||||
|
color = vec3(0.0, 1.0, 1.0) * pulse;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// UNFOCUSED: Solid red overlay (no border)
|
||||||
|
color = mix(color, vec3(1.0, 0.0, 0.0), 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
fragColor = vec4(color, 1.0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
vec2 curve(vec2 uv)
|
||||||
|
{
|
||||||
|
uv = (uv - 0.5) * 2.0;
|
||||||
|
uv *= 1.1;
|
||||||
|
uv.x *= 1.0 + pow((abs(uv.y) / 5.0), 2.0);
|
||||||
|
uv.y *= 1.0 + pow((abs(uv.x) / 4.0), 2.0);
|
||||||
|
uv = (uv / 2.0) + 0.5;
|
||||||
|
uv = uv *0.92 + 0.04;
|
||||||
|
return uv;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Missing mainImage!
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
//! Renderer backend selection (`renderer/backend.zig`).
|
||||||
|
|
||||||
|
/// Possible implementations, used for build options.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Backend {
|
||||||
|
Opengl,
|
||||||
|
Metal,
|
||||||
|
Webgl,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Backend {
|
||||||
|
/// The default backend for the target. Metal/WebGL are out of scope for
|
||||||
|
/// the Rust port (Linux only), so this is always OpenGL on non-Darwin.
|
||||||
|
pub fn default_for_target() -> Backend {
|
||||||
|
if cfg!(target_vendor = "apple") {
|
||||||
|
Backend::Metal
|
||||||
|
} else {
|
||||||
|
Backend::Opengl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The health status of a renderer (`renderer.Health`).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[repr(C)]
|
||||||
|
pub enum Health {
|
||||||
|
Healthy,
|
||||||
|
Unhealthy,
|
||||||
|
}
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
//! Cursor style decision logic (`renderer/cursor.zig`).
|
||||||
|
|
||||||
|
/// Terminal cursor styles (`terminal.CursorStyle`).
|
||||||
|
// TODO(integration): replace with ghostty_terminal's CursorStyle.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub enum TerminalCursorStyle {
|
||||||
|
Bar,
|
||||||
|
#[default]
|
||||||
|
Block,
|
||||||
|
BlockHollow,
|
||||||
|
Underline,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The subset of `terminal.RenderState.cursor` that the cursor style
|
||||||
|
/// decision needs.
|
||||||
|
// TODO(integration): replace with `&ghostty_terminal::RenderState` (use its
|
||||||
|
// `cursor` field) once the terminal crate is a dependency.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct RenderStateCursor {
|
||||||
|
/// Viewport position of the cursor (`None` if it isn't in the viewport).
|
||||||
|
pub viewport: Option<(u16, u16)>,
|
||||||
|
/// Whether the terminal is at a password prompt.
|
||||||
|
pub password_input: bool,
|
||||||
|
/// Cursor visibility via terminal mode.
|
||||||
|
pub visible: bool,
|
||||||
|
/// Whether the cursor is blinking.
|
||||||
|
pub blinking: bool,
|
||||||
|
/// The visual style requested by the terminal.
|
||||||
|
pub visual_style: TerminalCursorStyle,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Available cursor styles for drawing that renderers must support.
|
||||||
|
/// This is a superset of terminal cursor styles since the renderer supports
|
||||||
|
/// some additional cursor states such as the hollow block.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Style {
|
||||||
|
// Typical cursor input styles
|
||||||
|
Block,
|
||||||
|
BlockHollow,
|
||||||
|
Bar,
|
||||||
|
Underline,
|
||||||
|
|
||||||
|
// Special cursor styles
|
||||||
|
Lock,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Style {
|
||||||
|
/// Create a cursor style from the terminal style request.
|
||||||
|
pub fn from_terminal(term: TerminalCursorStyle) -> Style {
|
||||||
|
match term {
|
||||||
|
TerminalCursorStyle::Bar => Style::Bar,
|
||||||
|
TerminalCursorStyle::Block => Style::Block,
|
||||||
|
TerminalCursorStyle::BlockHollow => Style::BlockHollow,
|
||||||
|
TerminalCursorStyle::Underline => Style::Underline,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub struct StyleOptions {
|
||||||
|
pub preedit: bool,
|
||||||
|
pub focused: bool,
|
||||||
|
pub blink_visible: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the cursor style to use for the current render state or None
|
||||||
|
/// if a cursor should not be rendered at all.
|
||||||
|
pub fn style(state: &RenderStateCursor, opts: StyleOptions) -> Option<Style> {
|
||||||
|
// Note the order of conditionals below is important. It represents
|
||||||
|
// a priority system of how we determine what state overrides cursor
|
||||||
|
// visibility and style.
|
||||||
|
|
||||||
|
// The cursor must be visible in the viewport to be rendered.
|
||||||
|
state.viewport?;
|
||||||
|
|
||||||
|
// If we are in preedit, then we always show the block cursor. We do
|
||||||
|
// this even if the cursor is explicitly not visible because it shows
|
||||||
|
// an important editing state to the user.
|
||||||
|
if opts.preedit {
|
||||||
|
return Some(Style::Block);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're at a password input its always a lock.
|
||||||
|
if state.password_input {
|
||||||
|
return Some(Style::Lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the cursor is explicitly not visible by terminal mode, we don't render.
|
||||||
|
if !state.visible {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're not focused, our cursor is always visible so that
|
||||||
|
// we can show the hollow box.
|
||||||
|
if !opts.focused {
|
||||||
|
return Some(Style::BlockHollow);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the cursor is blinking and our blink state is not visible,
|
||||||
|
// then we don't show the cursor.
|
||||||
|
if state.blinking && !opts.blink_visible {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, we use whatever style the terminal wants.
|
||||||
|
Some(Style::from_terminal(state.visual_style))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
// The Zig tests drive a real Terminal + RenderState; until
|
||||||
|
// ghostty-terminal is a dependency these build the cursor state directly
|
||||||
|
// with the values the Zig tests produce.
|
||||||
|
// TODO(integration): port to use Terminal/RenderState.
|
||||||
|
fn state(visible: bool, blinking: bool) -> RenderStateCursor {
|
||||||
|
RenderStateCursor {
|
||||||
|
viewport: Some((0, 0)),
|
||||||
|
password_input: false,
|
||||||
|
visible,
|
||||||
|
blinking,
|
||||||
|
visual_style: TerminalCursorStyle::Bar,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn o(preedit: bool, focused: bool, blink_visible: bool) -> StyleOptions {
|
||||||
|
StyleOptions {
|
||||||
|
preedit,
|
||||||
|
focused,
|
||||||
|
blink_visible,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cursor_default_uses_configured_style() {
|
||||||
|
let s = state(true, true);
|
||||||
|
assert_eq!(style(&s, o(false, true, true)), Some(Style::Bar));
|
||||||
|
assert_eq!(style(&s, o(false, false, true)), Some(Style::BlockHollow));
|
||||||
|
assert_eq!(style(&s, o(false, false, false)), Some(Style::BlockHollow));
|
||||||
|
assert_eq!(style(&s, o(false, true, false)), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cursor_blinking_disabled() {
|
||||||
|
let s = state(true, false);
|
||||||
|
assert_eq!(style(&s, o(false, true, true)), Some(Style::Bar));
|
||||||
|
assert_eq!(style(&s, o(false, true, false)), Some(Style::Bar));
|
||||||
|
assert_eq!(style(&s, o(false, false, true)), Some(Style::BlockHollow));
|
||||||
|
assert_eq!(style(&s, o(false, false, false)), Some(Style::BlockHollow));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cursor_explicitly_not_visible() {
|
||||||
|
let s = state(false, false);
|
||||||
|
for focused in [true, false] {
|
||||||
|
for blink in [true, false] {
|
||||||
|
assert_eq!(style(&s, o(false, focused, blink)), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cursor_always_block_with_preedit() {
|
||||||
|
let mut s = state(true, true);
|
||||||
|
for focused in [true, false] {
|
||||||
|
for blink in [true, false] {
|
||||||
|
assert_eq!(style(&s, o(true, focused, blink)), Some(Style::Block));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're scrolled though, then we don't show the cursor.
|
||||||
|
s.viewport = None;
|
||||||
|
for focused in [true, false] {
|
||||||
|
for blink in [true, false] {
|
||||||
|
assert_eq!(style(&s, o(true, focused, blink)), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cursor_password_input_is_lock() {
|
||||||
|
let mut s = state(false, true);
|
||||||
|
s.password_input = true;
|
||||||
|
assert_eq!(style(&s, o(false, true, true)), Some(Style::Lock));
|
||||||
|
assert_eq!(style(&s, o(true, true, true)), Some(Style::Block));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1,22 @@
|
|||||||
//! ghostty-renderer: see docs/rewrite/ARCHITECTURE.md for the module map.
|
//! ghostty-renderer: OpenGL renderer building blocks (see
|
||||||
|
//! docs/rewrite/ARCHITECTURE.md and docs/rewrite/notes/ghostty-renderer-R1.md).
|
||||||
|
//!
|
||||||
|
//! Terminal-independent parts of `renderer/`: the OpenGL graphics API backend
|
||||||
|
//! and its GPU object wrappers, shaders and their std140 data layouts,
|
||||||
|
//! shadertoy custom-shader conversion, size math and cursor style logic.
|
||||||
|
|
||||||
|
#![cfg_attr(test, allow(clippy::unwrap_used))]
|
||||||
|
|
||||||
|
pub mod backend;
|
||||||
|
pub mod cursor;
|
||||||
|
pub mod math;
|
||||||
|
pub mod message;
|
||||||
|
pub mod opengl;
|
||||||
|
pub mod options;
|
||||||
|
pub mod shadertoy;
|
||||||
|
pub mod size;
|
||||||
|
|
||||||
|
pub use backend::{Backend, Health};
|
||||||
|
pub use cursor::{Style as CursorStyle, style as cursor_style};
|
||||||
|
pub use opengl::OpenGL;
|
||||||
|
pub use size::{CellSize, Coordinate, GridSize, Padding, ScreenSize, Size};
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
//! Math helpers (`src/math.zig`), only what the renderer needs.
|
||||||
|
|
||||||
|
/// A 4x4 matrix as four column vectors (same layout as Zig's `[4]@Vector(4, f32)`).
|
||||||
|
pub type Mat = [[f32; 4]; 4];
|
||||||
|
|
||||||
|
/// 2D orthographic projection matrix
|
||||||
|
pub fn ortho2d(left: f32, right: f32, bottom: f32, top: f32) -> Mat {
|
||||||
|
let w = right - left;
|
||||||
|
let h = top - bottom;
|
||||||
|
[
|
||||||
|
[2.0 / w, 0.0, 0.0, 0.0],
|
||||||
|
[0.0, 2.0 / h, 0.0, 0.0],
|
||||||
|
[0.0, 0.0, -1.0, 0.0],
|
||||||
|
[-(right + left) / w, -(top + bottom) / h, 0.0, 1.0],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ortho2d_maps_corners() {
|
||||||
|
let m = ortho2d(0.0, 800.0, 600.0, 0.0);
|
||||||
|
let apply = |x: f32, y: f32| {
|
||||||
|
(
|
||||||
|
m[0][0] * x + m[1][0] * y + m[3][0],
|
||||||
|
m[0][1] * x + m[1][1] * y + m[3][1],
|
||||||
|
)
|
||||||
|
};
|
||||||
|
assert_eq!(apply(0.0, 0.0), (-1.0, 1.0));
|
||||||
|
assert_eq!(apply(800.0, 600.0), (1.0, -1.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
//! Messages that can be sent to a renderer thread (`renderer/message.zig`).
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use ghostty_font::SharedGrid;
|
||||||
|
|
||||||
|
use crate::size::Size;
|
||||||
|
|
||||||
|
/// Key identifying a font grid in the `SharedGridSet`.
|
||||||
|
// TODO(integration): replace with ghostty_font's SharedGridSet key once
|
||||||
|
// SharedGridSet is ported (it needs ghostty-config).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct SharedGridKey(pub u64);
|
||||||
|
|
||||||
|
/// The renderer-thread derived config (`renderer.Thread.DerivedConfig`).
|
||||||
|
// TODO(integration): defined with Thread.zig (deferred; needs ghostty-config).
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct ThreadDerivedConfig {}
|
||||||
|
|
||||||
|
/// The renderer implementation derived config (`Renderer.DerivedConfig`).
|
||||||
|
// TODO(integration): defined with generic.zig (deferred; needs ghostty-config).
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct RendererDerivedConfig {}
|
||||||
|
|
||||||
|
/// A flattened highlight (`terminal.highlight.Flattened`), used for search matches.
|
||||||
|
// TODO(integration): replace with ghostty_terminal::highlight::Flattened.
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||||
|
pub struct Flattened {}
|
||||||
|
|
||||||
|
/// The messages that can be sent to a renderer thread.
|
||||||
|
pub enum Message {
|
||||||
|
/// Purposely crash the renderer. This is used for testing and debugging.
|
||||||
|
/// See the "crash" binding action.
|
||||||
|
Crash,
|
||||||
|
|
||||||
|
/// A change in state in the window focus that this renderer is
|
||||||
|
/// rendering within. This is only sent when a change is detected so
|
||||||
|
/// the renderer is expected to handle all of these.
|
||||||
|
Focus(bool),
|
||||||
|
|
||||||
|
/// A change in the view occlusion state. This can be used to determine
|
||||||
|
/// if the window is visible or not. A window can be not visible (occluded)
|
||||||
|
/// and still have focus.
|
||||||
|
Visible(bool),
|
||||||
|
|
||||||
|
/// Reset the cursor blink by immediately showing the cursor then
|
||||||
|
/// restarting the timer.
|
||||||
|
ResetCursorBlink,
|
||||||
|
|
||||||
|
/// Change the font grid. This can happen for any number of reasons
|
||||||
|
/// including a font size change, family change, etc.
|
||||||
|
FontGrid(FontGridChange),
|
||||||
|
|
||||||
|
/// Changes the size. The screen size might change, padding, grid, etc.
|
||||||
|
Resize(Size),
|
||||||
|
|
||||||
|
/// The derived configuration to update the renderer with.
|
||||||
|
ChangeConfig {
|
||||||
|
thread: Box<ThreadDerivedConfig>,
|
||||||
|
r#impl: Box<RendererDerivedConfig>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Matches for the current viewport from the search thread. These happen
|
||||||
|
/// async so they may be off for a frame or two from the actually rendered
|
||||||
|
/// viewport. The renderer must handle this gracefully.
|
||||||
|
SearchViewportMatches(Vec<Flattened>),
|
||||||
|
|
||||||
|
/// The selected match from the search thread. May be None to indicate
|
||||||
|
/// no match currently.
|
||||||
|
SearchSelectedMatch(Option<Flattened>),
|
||||||
|
|
||||||
|
/// Activate or deactivate the inspector.
|
||||||
|
Inspector(bool),
|
||||||
|
|
||||||
|
/// The macOS display ID has changed for the window.
|
||||||
|
MacosDisplayId(u32),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Payload of [`Message::FontGrid`].
|
||||||
|
pub struct FontGridChange {
|
||||||
|
pub grid: Arc<SharedGrid>,
|
||||||
|
|
||||||
|
/// The key for the new grid. If adopting the new grid fails for any
|
||||||
|
/// reason, the old grid should be kept but the new key should be
|
||||||
|
/// dereferenced.
|
||||||
|
pub new_key: SharedGridKey,
|
||||||
|
|
||||||
|
/// After accepting the new grid, the old grid must be dereferenced
|
||||||
|
/// using this key.
|
||||||
|
pub old_key: SharedGridKey,
|
||||||
|
// TODO(integration): upstream also carries `set: *SharedGridSet`.
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Message {
|
||||||
|
/// Initialize a change_config message.
|
||||||
|
// TODO(integration): `initChangeConfig(alloc, config)` builds the derived
|
||||||
|
// configs from `config.Config`; add once the derived config types exist.
|
||||||
|
pub fn init_change_config(
|
||||||
|
thread: ThreadDerivedConfig,
|
||||||
|
r#impl: RendererDerivedConfig,
|
||||||
|
) -> Message {
|
||||||
|
Message::ChangeConfig {
|
||||||
|
thread: Box::new(thread),
|
||||||
|
r#impl: Box::new(r#impl),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn change_config_message_roundtrip() {
|
||||||
|
let m = Message::init_change_config(ThreadDerivedConfig {}, RendererDerivedConfig {});
|
||||||
|
assert!(matches!(m, Message::ChangeConfig { .. }));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
//! The options that are used to configure a renderer (`renderer/Options.zig`).
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use ghostty_font::SharedGrid;
|
||||||
|
|
||||||
|
use crate::size::Size;
|
||||||
|
|
||||||
|
/// Options for constructing a renderer.
|
||||||
|
///
|
||||||
|
/// `config` is the derived renderer configuration (`Renderer.DerivedConfig`).
|
||||||
|
// TODO(integration): upstream also carries `surface_mailbox`, `rt_surface`
|
||||||
|
// and `thread` (apprt / Thread.zig types). They are added when the generic
|
||||||
|
// renderer and the apprt exist; the GL loader closure replaces `rt_surface`.
|
||||||
|
pub struct Options<C> {
|
||||||
|
/// The derived configuration for this renderer implementation.
|
||||||
|
pub config: C,
|
||||||
|
|
||||||
|
/// The font grid that should be used along with the key for deref-ing.
|
||||||
|
pub font_grid: Arc<SharedGrid>,
|
||||||
|
|
||||||
|
/// The size of everything.
|
||||||
|
pub size: Size,
|
||||||
|
}
|
||||||
@@ -0,0 +1,465 @@
|
|||||||
|
//! Shadertoy-style custom shader support (`renderer/shadertoy.zig`).
|
||||||
|
//!
|
||||||
|
//! Ghostty converts user shaders (shadertoy GLSL, which only define a
|
||||||
|
//! `mainImage` function) into full GLSL for the active renderer backend. The
|
||||||
|
//! Zig implementation goes GLSL -> glslang -> SPIR-V -> spirv-cross -> target
|
||||||
|
//! GLSL/MSL. This port is pure Rust: GLSL -> `naga` (glsl-in frontend,
|
||||||
|
//! validated) -> `naga` glsl-out backend at version 430 core. See
|
||||||
|
//! `docs/rewrite/notes/ghostty-renderer-R1.md` for the (small) source
|
||||||
|
//! adaptations needed for naga's Vulkan-flavoured GLSL frontend.
|
||||||
|
|
||||||
|
use std::io::Read;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use bytemuck::{Pod, Zeroable};
|
||||||
|
use naga::back::glsl as glsl_out;
|
||||||
|
use naga::front::glsl as glsl_in;
|
||||||
|
use naga::valid::{Capabilities, ModuleInfo, ValidationFlags, Validator};
|
||||||
|
|
||||||
|
/// The shadertoy prefix (declares the uniforms and the `main` entry point).
|
||||||
|
pub const PREFIX: &str = include_str!("../shaders/shadertoy_prefix.glsl");
|
||||||
|
|
||||||
|
/// The uniform struct used for shadertoy shaders (std140, 4496 bytes).
|
||||||
|
///
|
||||||
|
/// Field order matches `shadertoy.zig`; explicit `pad*` fields reproduce the
|
||||||
|
/// padding of the Zig `extern struct` with `align(16)` fields.
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Pod, Zeroable)]
|
||||||
|
pub struct Uniforms {
|
||||||
|
pub resolution: [f32; 3],
|
||||||
|
pub time: f32,
|
||||||
|
pub time_delta: f32,
|
||||||
|
pub frame_rate: f32,
|
||||||
|
pub frame: i32,
|
||||||
|
pub pad0: [u8; 4],
|
||||||
|
pub channel_time: [[f32; 4]; 4],
|
||||||
|
pub channel_resolution: [[f32; 4]; 4],
|
||||||
|
pub mouse: [f32; 4],
|
||||||
|
pub date: [f32; 4],
|
||||||
|
pub sample_rate: f32,
|
||||||
|
pub pad1: [u8; 12],
|
||||||
|
pub current_cursor: [f32; 4],
|
||||||
|
pub previous_cursor: [f32; 4],
|
||||||
|
pub current_cursor_color: [f32; 4],
|
||||||
|
pub previous_cursor_color: [f32; 4],
|
||||||
|
pub current_cursor_style: i32,
|
||||||
|
pub previous_cursor_style: i32,
|
||||||
|
pub cursor_visible: i32,
|
||||||
|
pub cursor_change_time: f32,
|
||||||
|
pub time_focus: f32,
|
||||||
|
pub focus: i32,
|
||||||
|
pub pad2: [u8; 8],
|
||||||
|
pub palette: [[f32; 4]; 256],
|
||||||
|
pub background_color: [f32; 4],
|
||||||
|
pub foreground_color: [f32; 4],
|
||||||
|
pub cursor_color: [f32; 4],
|
||||||
|
pub cursor_text: [f32; 4],
|
||||||
|
pub selection_background_color: [f32; 4],
|
||||||
|
pub selection_foreground_color: [f32; 4],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Uniforms {
|
||||||
|
fn default() -> Self {
|
||||||
|
Zeroable::zeroed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The target to load shaders for.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Target {
|
||||||
|
Glsl,
|
||||||
|
/// Metal is out of scope for the Rust port; converting to it fails with
|
||||||
|
/// [`Error::Unsupported`].
|
||||||
|
Msl,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An entry of a `custom-shader` config path list (`RepeatablePath.Value`).
|
||||||
|
// TODO(integration): replace with ghostty_config's RepeatablePath.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum PathItem {
|
||||||
|
/// A path that may be missing (`?path`); missing files are skipped.
|
||||||
|
Optional(String),
|
||||||
|
/// A path that must exist.
|
||||||
|
Required(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("io error: {0}")]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
/// The shader source exceeds the 4MB limit.
|
||||||
|
#[error("shader file too big")]
|
||||||
|
FileTooBig,
|
||||||
|
/// GLSL parsing failed (upstream: `error.GlslangFailed`); details are in
|
||||||
|
/// the [`ShaderLog`].
|
||||||
|
#[error("GLSL compilation failed")]
|
||||||
|
GlslangFailed,
|
||||||
|
/// The parsed module failed validation (upstream: link failure).
|
||||||
|
#[error("shader validation failed")]
|
||||||
|
ValidationFailed,
|
||||||
|
/// Writing the target GLSL failed (upstream: `error.SpvcFailed`).
|
||||||
|
#[error("GLSL generation failed: {0}")]
|
||||||
|
SpvcFailed(String),
|
||||||
|
#[error("unsupported shader target")]
|
||||||
|
Unsupported,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieve errors from shader compilation (`SpirvLog`).
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||||
|
pub struct ShaderLog {
|
||||||
|
pub info: String,
|
||||||
|
pub debug: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load a set of shaders from files and convert them to the target
|
||||||
|
/// format. The shader order is preserved.
|
||||||
|
pub fn load_from_files(paths: &[PathItem], target: Target) -> Result<Vec<String>, Error> {
|
||||||
|
let mut list = Vec::new();
|
||||||
|
|
||||||
|
for item in paths {
|
||||||
|
let (path, optional) = match item {
|
||||||
|
PathItem::Optional(p) => (p, true),
|
||||||
|
PathItem::Required(p) => (p, false),
|
||||||
|
};
|
||||||
|
|
||||||
|
let shader = match load_from_file(Path::new(path), target) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(Error::Io(e)) if optional && e.kind() == std::io::ErrorKind::NotFound => {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
};
|
||||||
|
log::info!("loaded custom shader path={path}");
|
||||||
|
list.push(shader);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load a single shader from a file and convert it to the target language
|
||||||
|
/// ready to be used with renderers.
|
||||||
|
pub fn load_from_file(path: &Path, target: Target) -> Result<String, Error> {
|
||||||
|
// Read it all into memory -- we don't expect shaders to be large.
|
||||||
|
const MAX: u64 = 4 * 1024 * 1024; // 4MB
|
||||||
|
let mut src = Vec::new();
|
||||||
|
let file = std::fs::File::open(path)?;
|
||||||
|
file.take(MAX + 1).read_to_end(&mut src)?;
|
||||||
|
if src.len() as u64 > MAX {
|
||||||
|
return Err(Error::FileTooBig);
|
||||||
|
}
|
||||||
|
let src = String::from_utf8_lossy(&src);
|
||||||
|
|
||||||
|
// Convert to full GLSL, then compile.
|
||||||
|
let mut errlog = ShaderLog::default();
|
||||||
|
let compiled = match compile_shadertoy(&src, Some(&mut errlog)) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(err) => {
|
||||||
|
if !errlog.info.is_empty() || !errlog.debug.is_empty() {
|
||||||
|
log::warn!(
|
||||||
|
"spirv error path={} info={} debug={}",
|
||||||
|
path.display(),
|
||||||
|
errlog.info,
|
||||||
|
errlog.debug
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match target {
|
||||||
|
Target::Glsl => glsl_from_compiled(&compiled),
|
||||||
|
Target::Msl => Err(Error::Unsupported),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert a ShaderToy shader into valid GLSL.
|
||||||
|
///
|
||||||
|
/// ShaderToy shaders aren't full shaders, they're just implementing a
|
||||||
|
/// mainImage function and don't define any of the uniforms. This function
|
||||||
|
/// will convert the ShaderToy shader into a valid GLSL shader that can be
|
||||||
|
/// compiled and linked.
|
||||||
|
pub fn glsl_from_shader(src: &str) -> String {
|
||||||
|
let mut out = String::with_capacity(PREFIX.len() + src.len() + 2);
|
||||||
|
out.push_str(PREFIX);
|
||||||
|
out.push_str("\n\n");
|
||||||
|
out.push_str(src);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A parsed and validated shader module, ready to be converted to target GLSL.
|
||||||
|
pub struct Compiled {
|
||||||
|
module: naga::Module,
|
||||||
|
info: ModuleInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compile a ShaderToy shader (just the user's source, without the prefix)
|
||||||
|
/// to a validated module. Equivalent to `glslFromShader` + `spirvFromGlsl`.
|
||||||
|
pub fn compile_shadertoy(src: &str, errlog: Option<&mut ShaderLog>) -> Result<Compiled, Error> {
|
||||||
|
spirv_from_glsl(&glsl_from_shader(src), errlog)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compile full fragment-shader GLSL (as produced by [`glsl_from_shader`])
|
||||||
|
/// into a validated module (`spirvFromGlsl`; the SPIR-V is naga's IR).
|
||||||
|
///
|
||||||
|
/// The prefix is written for glslang's Vulkan GLSL. naga's GLSL frontend
|
||||||
|
/// accepts a stricter subset, so the well known prefix constructs are adapted
|
||||||
|
/// first (see [`adapt_for_naga`]).
|
||||||
|
pub fn spirv_from_glsl(src: &str, errlog: Option<&mut ShaderLog>) -> Result<Compiled, Error> {
|
||||||
|
let adapted = adapt_for_naga(src);
|
||||||
|
|
||||||
|
let mut frontend = glsl_in::Frontend::default();
|
||||||
|
let module = match frontend.parse(
|
||||||
|
&glsl_in::Options::from(naga::ShaderStage::Fragment),
|
||||||
|
&adapted,
|
||||||
|
) {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(e) => {
|
||||||
|
if let Some(log) = errlog {
|
||||||
|
log.info = e.emit_to_string(&adapted);
|
||||||
|
log.debug = String::new();
|
||||||
|
}
|
||||||
|
return Err(Error::GlslangFailed);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let info = match Validator::new(ValidationFlags::all(), Capabilities::all()).validate(&module) {
|
||||||
|
Ok(i) => i,
|
||||||
|
Err(e) => {
|
||||||
|
if let Some(log) = errlog {
|
||||||
|
log.info = e.emit_to_string(&adapted);
|
||||||
|
log.debug = format!("{e:?}");
|
||||||
|
}
|
||||||
|
return Err(Error::ValidationFailed);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Compiled { module, info })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rewrites the (verbatim Ghostty) shadertoy prefix constructs that naga's
|
||||||
|
/// GLSL frontend cannot parse. Source text outside the prefix is untouched
|
||||||
|
/// except that `main` is moved after the user code.
|
||||||
|
///
|
||||||
|
/// * `#version 430 core` -> `#version 460 core` (naga accepts 440/450/460).
|
||||||
|
/// * `uniform` qualifiers on interface block members are dropped.
|
||||||
|
/// * `layout(location = 0) in vec4 gl_FragCoord;` is dropped (naga knows the
|
||||||
|
/// builtin).
|
||||||
|
/// * the combined `sampler2D iChannel0` becomes a texture + sampler pair and
|
||||||
|
/// `iChannel0` a macro that combines them, so `texture(iChannel0, uv)`
|
||||||
|
/// keeps working.
|
||||||
|
/// * the `mainImage` prototype and `main` are moved after the user code
|
||||||
|
/// (naga requires callee definitions before their callers).
|
||||||
|
fn adapt_for_naga(full: &str) -> String {
|
||||||
|
let mut s = full
|
||||||
|
.replacen("#version 430 core", "#version 460 core", 1)
|
||||||
|
.replace(" uniform ", " ")
|
||||||
|
.replacen("layout(location = 0) in vec4 gl_FragCoord;", "", 1)
|
||||||
|
.replacen(
|
||||||
|
"layout(binding = 0) uniform sampler2D iChannel0;",
|
||||||
|
"layout(set = 0, binding = 0) uniform texture2D iChannel0_tex;\n\
|
||||||
|
layout(set = 0, binding = 2) uniform sampler iChannel0_smp;\n\
|
||||||
|
#define iChannel0 sampler2D(iChannel0_tex, iChannel0_smp)",
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
.replacen(
|
||||||
|
"layout(binding = 1, std140)",
|
||||||
|
"layout(set = 0, binding = 1, std140)",
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
.replacen(
|
||||||
|
"void mainImage( out vec4 fragColor, in vec2 fragCoord );\n",
|
||||||
|
"",
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
.replacen(
|
||||||
|
"void main() { mainImage (_fragColor, gl_FragCoord.xy); }",
|
||||||
|
"",
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
s.push_str("\nvoid main() { mainImage (_fragColor, gl_FragCoord.xy); }\n");
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert a compiled shader to GLSL 430 core (`glslFromSpv`).
|
||||||
|
pub fn glsl_from_compiled(compiled: &Compiled) -> Result<String, Error> {
|
||||||
|
const GLSL_VERSION: u16 = 430;
|
||||||
|
|
||||||
|
let mut options = glsl_out::Options {
|
||||||
|
version: glsl_out::Version::Desktop(GLSL_VERSION),
|
||||||
|
writer_flags: glsl_out::WriterFlags::empty(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
// Map the (set 0) resources to the GL binding points used by the
|
||||||
|
// renderer: iChannel0 texture at unit 0, the Globals block at binding 1.
|
||||||
|
for (binding, slot) in [(0u32, 0u8), (1, 1), (2, 0)] {
|
||||||
|
options
|
||||||
|
.binding_map
|
||||||
|
.insert(naga::ResourceBinding { group: 0, binding }, slot);
|
||||||
|
}
|
||||||
|
let pipeline = glsl_out::PipelineOptions {
|
||||||
|
shader_stage: naga::ShaderStage::Fragment,
|
||||||
|
entry_point: "main".into(),
|
||||||
|
multiview: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut out = String::new();
|
||||||
|
let mut writer = glsl_out::Writer::new(
|
||||||
|
&mut out,
|
||||||
|
&compiled.module,
|
||||||
|
&compiled.info,
|
||||||
|
&options,
|
||||||
|
&pipeline,
|
||||||
|
naga::proc::BoundsCheckPolicies::default(),
|
||||||
|
)
|
||||||
|
.map_err(|e| {
|
||||||
|
log::warn!("spirv-cross error message={e}");
|
||||||
|
Error::SpvcFailed(e.to_string())
|
||||||
|
})?;
|
||||||
|
writer.write().map_err(|e| {
|
||||||
|
log::warn!("spirv-cross error message={e}");
|
||||||
|
Error::SpvcFailed(e.to_string())
|
||||||
|
})?;
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
const TEST_CRT: &str = include_str!("../shaders/test_shadertoy_crt.glsl");
|
||||||
|
const TEST_FOCUS: &str = include_str!("../shaders/test_shadertoy_focus.glsl");
|
||||||
|
const TEST_INVALID: &str = include_str!("../shaders/test_shadertoy_invalid.glsl");
|
||||||
|
|
||||||
|
/// Test shader sources, for use in GL integration tests.
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub mod test_shaders {
|
||||||
|
pub const CRT: &str = super::TEST_CRT;
|
||||||
|
pub const FOCUS: &str = super::TEST_FOCUS;
|
||||||
|
pub const INVALID: &str = super::TEST_INVALID;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use core::mem::{offset_of, size_of};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spirv() {
|
||||||
|
let src = glsl_from_shader(TEST_CRT);
|
||||||
|
spirv_from_glsl(&src, None).expect("crt shader compiles");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spirv_invalid() {
|
||||||
|
let src = glsl_from_shader(TEST_INVALID);
|
||||||
|
let mut errlog = ShaderLog::default();
|
||||||
|
let err = spirv_from_glsl(&src, Some(&mut errlog))
|
||||||
|
.err()
|
||||||
|
.expect("invalid shader fails");
|
||||||
|
assert!(matches!(err, Error::GlslangFailed));
|
||||||
|
assert_ne!(errlog.info, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shadertoy_to_glsl() {
|
||||||
|
let src = glsl_from_shader(TEST_CRT);
|
||||||
|
let compiled = spirv_from_glsl(&src, None).unwrap();
|
||||||
|
let glsl = glsl_from_compiled(&compiled).unwrap();
|
||||||
|
assert!(glsl.starts_with("#version 430 core"));
|
||||||
|
assert!(glsl.contains("layout(std140, binding = 1) uniform"));
|
||||||
|
assert!(glsl.contains("layout(binding = 0) uniform sampler2D"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shadertoy_to_msl_unsupported() {
|
||||||
|
let compiled = compile_shadertoy(TEST_CRT, None).unwrap();
|
||||||
|
assert_eq!(compiled.module.entry_points.len(), 1);
|
||||||
|
let dir = std::env::temp_dir().join(format!("ghostty-shadertoy-{}", std::process::id()));
|
||||||
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
|
let path = dir.join("crt.glsl");
|
||||||
|
std::fs::write(&path, TEST_CRT).unwrap();
|
||||||
|
assert!(matches!(
|
||||||
|
load_from_file(&path, Target::Msl),
|
||||||
|
Err(Error::Unsupported)
|
||||||
|
));
|
||||||
|
std::fs::remove_dir_all(&dir).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shadertoy_focus_to_glsl() {
|
||||||
|
let compiled = compile_shadertoy(TEST_FOCUS, None).unwrap();
|
||||||
|
let glsl = glsl_from_compiled(&compiled).unwrap();
|
||||||
|
assert!(glsl.contains("iTimeFocus") || glsl.contains("_group_0_binding_1_fs"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn glsl_from_shader_is_prefix_plus_source() {
|
||||||
|
let out = glsl_from_shader("void mainImage(out vec4 c, in vec2 p) { c = vec4(0.0); }");
|
||||||
|
assert!(out.starts_with(PREFIX));
|
||||||
|
assert!(out.contains(&format!("{PREFIX}\n\nvoid mainImage")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn user_helpers_after_main_image_compile() {
|
||||||
|
let src = "void mainImage(out vec4 c, in vec2 p) { c = vec4(helper(p), 0.0, 0.0, 1.0); }\n\
|
||||||
|
float helper(vec2 p) { return p.x; }\n";
|
||||||
|
// helper is defined after use: invalid GLSL, must be rejected, not panic.
|
||||||
|
assert!(compile_shadertoy(src, None).is_err());
|
||||||
|
let src = "float helper(vec2 p) { return p.x / iResolution.x; }\n\
|
||||||
|
void mainImage(out vec4 c, in vec2 p) { c = vec4(helper(p), texture2D(iChannel0, p / iResolution.xy).g, iTime, 1.0); }\n";
|
||||||
|
compile_shadertoy(src, None).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_from_files_skips_missing_optional() {
|
||||||
|
let dir =
|
||||||
|
std::env::temp_dir().join(format!("ghostty-shadertoy-files-{}", std::process::id()));
|
||||||
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
|
let good = dir.join("crt.glsl");
|
||||||
|
std::fs::write(&good, TEST_CRT).unwrap();
|
||||||
|
let missing = dir.join("missing.glsl");
|
||||||
|
|
||||||
|
let items = [
|
||||||
|
PathItem::Optional(missing.to_string_lossy().into_owned()),
|
||||||
|
PathItem::Required(good.to_string_lossy().into_owned()),
|
||||||
|
];
|
||||||
|
let shaders = load_from_files(&items, Target::Glsl).unwrap();
|
||||||
|
assert_eq!(shaders.len(), 1);
|
||||||
|
|
||||||
|
let items = [PathItem::Required(missing.to_string_lossy().into_owned())];
|
||||||
|
assert!(matches!(
|
||||||
|
load_from_files(&items, Target::Glsl),
|
||||||
|
Err(Error::Io(_))
|
||||||
|
));
|
||||||
|
std::fs::remove_dir_all(&dir).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn uniforms_std140_layout() {
|
||||||
|
// std140 offsets of the `Globals` block in shadertoy_prefix.glsl.
|
||||||
|
assert_eq!(offset_of!(Uniforms, resolution), 0);
|
||||||
|
assert_eq!(offset_of!(Uniforms, time), 12);
|
||||||
|
assert_eq!(offset_of!(Uniforms, time_delta), 16);
|
||||||
|
assert_eq!(offset_of!(Uniforms, frame_rate), 20);
|
||||||
|
assert_eq!(offset_of!(Uniforms, frame), 24);
|
||||||
|
assert_eq!(offset_of!(Uniforms, channel_time), 32);
|
||||||
|
assert_eq!(offset_of!(Uniforms, channel_resolution), 96);
|
||||||
|
assert_eq!(offset_of!(Uniforms, mouse), 160);
|
||||||
|
assert_eq!(offset_of!(Uniforms, date), 176);
|
||||||
|
assert_eq!(offset_of!(Uniforms, sample_rate), 192);
|
||||||
|
assert_eq!(offset_of!(Uniforms, current_cursor), 208);
|
||||||
|
assert_eq!(offset_of!(Uniforms, previous_cursor), 224);
|
||||||
|
assert_eq!(offset_of!(Uniforms, current_cursor_color), 240);
|
||||||
|
assert_eq!(offset_of!(Uniforms, previous_cursor_color), 256);
|
||||||
|
assert_eq!(offset_of!(Uniforms, current_cursor_style), 272);
|
||||||
|
assert_eq!(offset_of!(Uniforms, previous_cursor_style), 276);
|
||||||
|
assert_eq!(offset_of!(Uniforms, cursor_visible), 280);
|
||||||
|
assert_eq!(offset_of!(Uniforms, cursor_change_time), 284);
|
||||||
|
assert_eq!(offset_of!(Uniforms, time_focus), 288);
|
||||||
|
assert_eq!(offset_of!(Uniforms, focus), 292);
|
||||||
|
assert_eq!(offset_of!(Uniforms, palette), 304);
|
||||||
|
assert_eq!(offset_of!(Uniforms, background_color), 4400);
|
||||||
|
assert_eq!(offset_of!(Uniforms, foreground_color), 4416);
|
||||||
|
assert_eq!(offset_of!(Uniforms, cursor_color), 4432);
|
||||||
|
assert_eq!(offset_of!(Uniforms, cursor_text), 4448);
|
||||||
|
assert_eq!(offset_of!(Uniforms, selection_background_color), 4464);
|
||||||
|
assert_eq!(offset_of!(Uniforms, selection_foreground_color), 4480);
|
||||||
|
assert_eq!(size_of::<Uniforms>(), 4496);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,498 @@
|
|||||||
|
//! Screen/grid/cell/padding size math (`renderer/size.zig`).
|
||||||
|
|
||||||
|
/// Terminal cell count integer (`terminal.size.CellCountInt`).
|
||||||
|
// TODO(integration): re-export from ghostty-terminal once it is a dependency.
|
||||||
|
pub type CellCountInt = u16;
|
||||||
|
|
||||||
|
/// All relevant sizes for a rendered terminal. These are all the sizes that
|
||||||
|
/// any functionality should need to know about the terminal in order to
|
||||||
|
/// convert between any coordinate systems.
|
||||||
|
///
|
||||||
|
/// See the individual field type documentation for more information on each
|
||||||
|
/// field. One important note is that any pixel values should already be scaled
|
||||||
|
/// to the current DPI of the screen. If the DPI changes, the sizes should be
|
||||||
|
/// recalculated and we expect this to be done by the caller.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub struct Size {
|
||||||
|
pub screen: ScreenSize,
|
||||||
|
pub cell: CellSize,
|
||||||
|
pub padding: Padding,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Size {
|
||||||
|
/// Return the grid size for this size. The grid size is calculated by
|
||||||
|
/// taking the screen size, removing padding, and dividing by the cell
|
||||||
|
/// dimensions.
|
||||||
|
pub fn grid(&self) -> GridSize {
|
||||||
|
GridSize::init(self.screen.sub_padding(self.padding), self.cell)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The size of the terminal. This is the same as the screen without
|
||||||
|
/// padding.
|
||||||
|
pub fn terminal(&self) -> ScreenSize {
|
||||||
|
self.screen.sub_padding(self.padding)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the padding to be balanced around the grid. The balanced
|
||||||
|
/// padding is calculated AFTER the explicit padding is taken
|
||||||
|
/// into account.
|
||||||
|
pub fn balance_padding(&mut self, explicit: Padding) {
|
||||||
|
// This ensure grid() does the right thing
|
||||||
|
self.padding = explicit;
|
||||||
|
|
||||||
|
// Now we can calculate the balanced padding
|
||||||
|
self.padding = Padding::balanced(self.screen, self.grid(), self.cell);
|
||||||
|
|
||||||
|
// The top/bottom padding is interesting. Subjectively, lots of padding
|
||||||
|
// at the top looks bad. So instead of always being equal (like left/right),
|
||||||
|
// we force the top padding to be at most equal to the maximum left padding,
|
||||||
|
// which is the balanced explicit horizontal padding plus half a cell width.
|
||||||
|
let max_padding_left = (explicit.left + explicit.right + self.cell.width) / 2;
|
||||||
|
let vshift = self.padding.top.saturating_sub(max_padding_left);
|
||||||
|
self.padding.top -= vshift;
|
||||||
|
self.padding.bottom += vshift;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A coordinate. This is defined as a tagged union to allow for different
|
||||||
|
/// coordinate systems to be represented.
|
||||||
|
///
|
||||||
|
/// A coordinate is only valid within the context of a stable Size value.
|
||||||
|
/// If any of the sizes in the Size struct change, the coordinate is no
|
||||||
|
/// longer valid and must be recalculated. A conversion function is provided
|
||||||
|
/// to migrate to a new Size (which may result in failure).
|
||||||
|
///
|
||||||
|
/// The coordinate systems are:
|
||||||
|
///
|
||||||
|
/// * surface: (0, 0) is the top-left of the surface (with padding). Negative
|
||||||
|
/// values are allowed and are off the surface. Likewise, values greater
|
||||||
|
/// than the surface size are off the surface. Units are pixels.
|
||||||
|
///
|
||||||
|
/// * terminal: (0, 0) is the top-left of the terminal grid. This is the
|
||||||
|
/// same as the surface but with the padding removed. Negative values and
|
||||||
|
/// values greater than the grid size are allowed and are off the terminal.
|
||||||
|
/// Units are pixels.
|
||||||
|
///
|
||||||
|
/// * grid: (0, 0) is the top-left of the grid. Units are in cells. Negative
|
||||||
|
/// values are not allowed but values greater than the grid size are
|
||||||
|
/// possible and are off the grid.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub enum Coordinate {
|
||||||
|
Surface(SurfaceCoord),
|
||||||
|
Terminal(TerminalCoord),
|
||||||
|
Grid(GridCoord),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum CoordinateTag {
|
||||||
|
Surface,
|
||||||
|
Terminal,
|
||||||
|
Grid,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct SurfaceCoord {
|
||||||
|
pub x: f64,
|
||||||
|
pub y: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct TerminalCoord {
|
||||||
|
pub x: f64,
|
||||||
|
pub y: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct GridCoord {
|
||||||
|
pub x: CellCountInt,
|
||||||
|
pub y: CellCountInt,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Coordinate {
|
||||||
|
pub fn tag(&self) -> CoordinateTag {
|
||||||
|
match self {
|
||||||
|
Coordinate::Surface(_) => CoordinateTag::Surface,
|
||||||
|
Coordinate::Terminal(_) => CoordinateTag::Terminal,
|
||||||
|
Coordinate::Grid(_) => CoordinateTag::Grid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert a coordinate to a different space within the same Size.
|
||||||
|
pub fn convert(self, to: CoordinateTag, size: Size) -> Coordinate {
|
||||||
|
// Unlikely fast-path but avoid work.
|
||||||
|
if self.tag() == to {
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
// To avoid the combinatorial explosion of conversion functions, we
|
||||||
|
// convert to the surface system first and then reconvert from there.
|
||||||
|
let surface = self.convert_to_surface(size);
|
||||||
|
|
||||||
|
match to {
|
||||||
|
CoordinateTag::Surface => Coordinate::Surface(surface),
|
||||||
|
CoordinateTag::Terminal => Coordinate::Terminal(TerminalCoord {
|
||||||
|
x: surface.x - f64::from(size.padding.left),
|
||||||
|
y: surface.y - f64::from(size.padding.top),
|
||||||
|
}),
|
||||||
|
CoordinateTag::Grid => {
|
||||||
|
// Get rid of the padding.
|
||||||
|
let Coordinate::Terminal(term) =
|
||||||
|
Coordinate::Surface(surface).convert(CoordinateTag::Terminal, size)
|
||||||
|
else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
|
||||||
|
// We need our grid to clamp
|
||||||
|
let grid = size.grid();
|
||||||
|
|
||||||
|
// Calculate the grid position.
|
||||||
|
let cell_width = f64::from(size.cell.width);
|
||||||
|
let cell_height = f64::from(size.cell.height);
|
||||||
|
let clamped_x = term.x.max(0.0);
|
||||||
|
let clamped_y = term.y.max(0.0);
|
||||||
|
let col = (clamped_x / cell_width) as CellCountInt;
|
||||||
|
let row = (clamped_y / cell_height) as CellCountInt;
|
||||||
|
let clamped_col = col.min(grid.columns - 1);
|
||||||
|
let clamped_row = row.min(grid.rows - 1);
|
||||||
|
Coordinate::Grid(GridCoord {
|
||||||
|
x: clamped_col,
|
||||||
|
y: clamped_row,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert a coordinate to the surface coordinate system.
|
||||||
|
fn convert_to_surface(self, size: Size) -> SurfaceCoord {
|
||||||
|
match self {
|
||||||
|
Coordinate::Surface(v) => v,
|
||||||
|
Coordinate::Terminal(v) => SurfaceCoord {
|
||||||
|
x: v.x + f64::from(size.padding.left),
|
||||||
|
y: v.y + f64::from(size.padding.top),
|
||||||
|
},
|
||||||
|
Coordinate::Grid(v) => {
|
||||||
|
let col = f64::from(v.x);
|
||||||
|
let row = f64::from(v.y);
|
||||||
|
let cell_width = f64::from(size.cell.width);
|
||||||
|
let cell_height = f64::from(size.cell.height);
|
||||||
|
let padding_left = f64::from(size.padding.left);
|
||||||
|
let padding_top = f64::from(size.padding.top);
|
||||||
|
SurfaceCoord {
|
||||||
|
x: col * cell_width + padding_left,
|
||||||
|
y: row * cell_height + padding_top,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The dimensions of a single "cell" in the terminal grid.
|
||||||
|
///
|
||||||
|
/// The dimensions are dependent on the current loaded set of font glyphs.
|
||||||
|
/// We calculate the width based on the widest character and the height based
|
||||||
|
/// on the height requirement for an underscore (the "lowest" -- visually --
|
||||||
|
/// character).
|
||||||
|
///
|
||||||
|
/// The units for the width and height are in world space. They have to
|
||||||
|
/// be normalized for any renderer implementation.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub struct CellSize {
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The dimensions of the screen that the grid is rendered to. This is the
|
||||||
|
/// terminal screen, so it is likely a subset of the window size. The dimensions
|
||||||
|
/// should be in pixels.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub struct ScreenSize {
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScreenSize {
|
||||||
|
/// Subtract padding from the screen size.
|
||||||
|
pub fn sub_padding(self, padding: Padding) -> ScreenSize {
|
||||||
|
ScreenSize {
|
||||||
|
width: self.width.saturating_sub(padding.left + padding.right),
|
||||||
|
height: self.height.saturating_sub(padding.top + padding.bottom),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates the amount of blank space around the grid. This is possible
|
||||||
|
/// when padding isn't balanced.
|
||||||
|
///
|
||||||
|
/// The "self" screen size here should be the unpadded screen.
|
||||||
|
pub fn blank_padding(self, padding: Padding, grid: GridSize, cell: CellSize) -> Padding {
|
||||||
|
let grid_width = u32::from(grid.columns) * cell.width;
|
||||||
|
let grid_height = u32::from(grid.rows) * cell.height;
|
||||||
|
let padded_width = grid_width + (padding.left + padding.right);
|
||||||
|
let padded_height = grid_height + (padding.top + padding.bottom);
|
||||||
|
|
||||||
|
// Note these have to use a saturating subtraction to avoid underflow
|
||||||
|
// because our padding can cause the padded sizes to be larger than
|
||||||
|
// our real screen if the screen is shrunk to a minimal size such
|
||||||
|
// as 1x1.
|
||||||
|
let leftover_width = self.width.saturating_sub(padded_width);
|
||||||
|
let leftover_height = self.height.saturating_sub(padded_height);
|
||||||
|
|
||||||
|
Padding {
|
||||||
|
top: 0,
|
||||||
|
bottom: leftover_height,
|
||||||
|
right: leftover_width,
|
||||||
|
left: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if two sizes are equal.
|
||||||
|
pub fn equals(self, other: ScreenSize) -> bool {
|
||||||
|
self == other
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The dimensions of the grid itself, in rows/columns units.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub struct GridSize {
|
||||||
|
pub columns: CellCountInt,
|
||||||
|
pub rows: CellCountInt,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GridSize {
|
||||||
|
/// Initialize a grid size based on a screen and cell size.
|
||||||
|
pub fn init(screen: ScreenSize, cell: CellSize) -> GridSize {
|
||||||
|
let mut result = GridSize::default();
|
||||||
|
result.update(screen, cell);
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the columns/rows for the grid based on the given screen and
|
||||||
|
/// cell size.
|
||||||
|
pub fn update(&mut self, screen: ScreenSize, cell: CellSize) {
|
||||||
|
let cell_width = cell.width as f32;
|
||||||
|
let cell_height = cell.height as f32;
|
||||||
|
let screen_width = screen.width as f32;
|
||||||
|
let screen_height = screen.height as f32;
|
||||||
|
let calc_cols = (screen_width / cell_width) as CellCountInt;
|
||||||
|
let calc_rows = (screen_height / cell_height) as CellCountInt;
|
||||||
|
self.columns = calc_cols.max(1);
|
||||||
|
self.rows = calc_rows.max(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if two sizes are equal.
|
||||||
|
pub fn equals(self, other: GridSize) -> bool {
|
||||||
|
self == other
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The padding to add to a screen.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub struct Padding {
|
||||||
|
pub top: u32,
|
||||||
|
pub bottom: u32,
|
||||||
|
pub right: u32,
|
||||||
|
pub left: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Padding {
|
||||||
|
/// Returns padding that balances the whitespace around the screen
|
||||||
|
/// for the given grid and cell sizes.
|
||||||
|
pub fn balanced(screen: ScreenSize, grid: GridSize, cell: CellSize) -> Padding {
|
||||||
|
// Turn our cell sizes into floats for the math
|
||||||
|
let cell_width = cell.width as f32;
|
||||||
|
let cell_height = cell.height as f32;
|
||||||
|
|
||||||
|
// The size of our full grid
|
||||||
|
let grid_width = f32::from(grid.columns) * cell_width;
|
||||||
|
let grid_height = f32::from(grid.rows) * cell_height;
|
||||||
|
|
||||||
|
// The empty space to the right of a line and bottom of the last row
|
||||||
|
let space_right = screen.width as f32 - grid_width;
|
||||||
|
let space_bot = screen.height as f32 - grid_height;
|
||||||
|
|
||||||
|
// The padding is split equally along both axes.
|
||||||
|
let padding_right = (space_right / 2.0).floor();
|
||||||
|
let padding_left = padding_right;
|
||||||
|
|
||||||
|
let padding_bot = (space_bot / 2.0).floor();
|
||||||
|
let padding_top = padding_bot;
|
||||||
|
|
||||||
|
let zero = 0.0f32;
|
||||||
|
Padding {
|
||||||
|
top: padding_top.max(zero) as u32,
|
||||||
|
bottom: padding_bot.max(zero) as u32,
|
||||||
|
right: padding_right.max(zero) as u32,
|
||||||
|
left: padding_left.max(zero) as u32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Equality test between two paddings.
|
||||||
|
pub fn eql(self, other: Padding) -> bool {
|
||||||
|
self == other
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add another padding to this one (`Padding.add`).
|
||||||
|
impl std::ops::Add for Padding {
|
||||||
|
type Output = Padding;
|
||||||
|
|
||||||
|
fn add(self, other: Padding) -> Padding {
|
||||||
|
Padding {
|
||||||
|
top: self.top + other.top,
|
||||||
|
bottom: self.bottom + other.bottom,
|
||||||
|
right: self.right + other.right,
|
||||||
|
left: self.left + other.left,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn padding_balanced_on_zero() {
|
||||||
|
// On some systems, our screen can be zero-sized for a bit, and we
|
||||||
|
// don't want to end up with negative padding.
|
||||||
|
let grid = GridSize {
|
||||||
|
columns: 100,
|
||||||
|
rows: 37,
|
||||||
|
};
|
||||||
|
let cell = CellSize {
|
||||||
|
width: 10,
|
||||||
|
height: 20,
|
||||||
|
};
|
||||||
|
let screen = ScreenSize {
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
};
|
||||||
|
let padding = Padding::balanced(screen, grid, cell);
|
||||||
|
assert_eq!(Padding::default(), padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grid_size_update_exact() {
|
||||||
|
let mut grid = GridSize::default();
|
||||||
|
grid.update(
|
||||||
|
ScreenSize {
|
||||||
|
width: 100,
|
||||||
|
height: 40,
|
||||||
|
},
|
||||||
|
CellSize {
|
||||||
|
width: 5,
|
||||||
|
height: 10,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert_eq!(20, grid.columns);
|
||||||
|
assert_eq!(4, grid.rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grid_size_update_rounding() {
|
||||||
|
let mut grid = GridSize::default();
|
||||||
|
grid.update(
|
||||||
|
ScreenSize {
|
||||||
|
width: 20,
|
||||||
|
height: 40,
|
||||||
|
},
|
||||||
|
CellSize {
|
||||||
|
width: 6,
|
||||||
|
height: 15,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert_eq!(3, grid.columns);
|
||||||
|
assert_eq!(2, grid.rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn coordinate_conversion() {
|
||||||
|
// A size for testing purposes. Purposely easy to calculate numbers.
|
||||||
|
let test_size = Size {
|
||||||
|
screen: ScreenSize {
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
},
|
||||||
|
cell: CellSize {
|
||||||
|
width: 5,
|
||||||
|
height: 10,
|
||||||
|
},
|
||||||
|
padding: Padding::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let g = |x, y| Coordinate::Grid(GridCoord { x, y });
|
||||||
|
let s = |x, y| Coordinate::Surface(SurfaceCoord { x, y });
|
||||||
|
let table = [
|
||||||
|
(g(0, 0), s(0.0, 0.0)),
|
||||||
|
(g(1, 0), s(6.0, 0.0)),
|
||||||
|
(g(1, 1), s(6.0, 10.0)),
|
||||||
|
(g(0, 0), s(-10.0, -10.0)),
|
||||||
|
(
|
||||||
|
g(test_size.grid().columns - 1, test_size.grid().rows - 1),
|
||||||
|
s(100_000.0, 100_000.0),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (expected, input) in table {
|
||||||
|
let actual = input.convert(expected.tag(), test_size);
|
||||||
|
assert_eq!(expected, actual);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn balance_padding_keeps_top_at_most_max_left() {
|
||||||
|
let mut size = Size {
|
||||||
|
screen: ScreenSize {
|
||||||
|
width: 105,
|
||||||
|
height: 200,
|
||||||
|
},
|
||||||
|
cell: CellSize {
|
||||||
|
width: 10,
|
||||||
|
height: 20,
|
||||||
|
},
|
||||||
|
padding: Padding::default(),
|
||||||
|
};
|
||||||
|
size.balance_padding(Padding::default());
|
||||||
|
// grid is 10x10 -> 100x200; leftover width 5 => left 2, right 2.
|
||||||
|
assert_eq!(size.padding.left, 2);
|
||||||
|
assert_eq!(size.padding.top, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn padding_add() {
|
||||||
|
let a = Padding {
|
||||||
|
top: 1,
|
||||||
|
bottom: 2,
|
||||||
|
right: 3,
|
||||||
|
left: 4,
|
||||||
|
};
|
||||||
|
let sum = a + a;
|
||||||
|
assert_eq!((sum.top, sum.bottom, sum.right, sum.left), (2, 4, 6, 8));
|
||||||
|
assert!(sum.eql(a + a));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn terminal_and_blank_padding() {
|
||||||
|
let size = Size {
|
||||||
|
screen: ScreenSize {
|
||||||
|
width: 110,
|
||||||
|
height: 70,
|
||||||
|
},
|
||||||
|
cell: CellSize {
|
||||||
|
width: 10,
|
||||||
|
height: 20,
|
||||||
|
},
|
||||||
|
padding: Padding {
|
||||||
|
top: 5,
|
||||||
|
bottom: 5,
|
||||||
|
left: 5,
|
||||||
|
right: 5,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let term = size.terminal();
|
||||||
|
assert_eq!((term.width, term.height), (100, 60));
|
||||||
|
let grid = size.grid();
|
||||||
|
assert_eq!((grid.columns, grid.rows), (10, 3));
|
||||||
|
let blank = term.blank_padding(size.padding, grid, size.cell);
|
||||||
|
// 100 - (100 + 10) saturates to 0; 60 - (60 + 10) saturates to 0.
|
||||||
|
assert_eq!(blank, Padding::default());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,390 @@
|
|||||||
|
//! Headless GL integration test: creates a surfaceless EGL context (Mesa),
|
||||||
|
//! builds every pipeline, compiles the converted shadertoy shaders and runs a
|
||||||
|
//! tiny render pass, reading the pixels back.
|
||||||
|
//!
|
||||||
|
//! Skipped (returns early with a message) when no EGL display / GL 4.3 core
|
||||||
|
//! context can be created, e.g. on CI without Mesa.
|
||||||
|
|
||||||
|
// The EGL loader calls are `unsafe fn`s in khronos-egl (test-only usage).
|
||||||
|
#![allow(unsafe_code, clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use std::ffi::c_void;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use ghostty_renderer::opengl::buffer;
|
||||||
|
use ghostty_renderer::opengl::gl::{Gl, Primitive};
|
||||||
|
use ghostty_renderer::opengl::render_pass::{Attachment, AttachmentTarget, Draw, Step};
|
||||||
|
use ghostty_renderer::opengl::shaders::{
|
||||||
|
BgImage, BgImageFit, BgImageInfo, BgImagePosition, CellText, Image, Uniforms,
|
||||||
|
};
|
||||||
|
use ghostty_renderer::opengl::{AlphaBlending, OpenGL};
|
||||||
|
use ghostty_renderer::shadertoy;
|
||||||
|
use khronos_egl as egl;
|
||||||
|
|
||||||
|
// EGL_PLATFORM_SURFACELESS_MESA, falling back to the default display.
|
||||||
|
const PLATFORM_SURFACELESS_MESA: egl::Enum = 0x31DD;
|
||||||
|
|
||||||
|
struct Headless {
|
||||||
|
egl: egl::DynamicInstance<egl::EGL1_5>,
|
||||||
|
display: egl::Display,
|
||||||
|
context: egl::Context,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Headless {
|
||||||
|
fn new() -> Result<Headless, String> {
|
||||||
|
// SAFETY: loads the system libEGL.
|
||||||
|
let egl = unsafe { egl::DynamicInstance::<egl::EGL1_5>::load_required() }
|
||||||
|
.map_err(|e| format!("cannot load libEGL: {e}"))?;
|
||||||
|
|
||||||
|
// SAFETY: EGL_DEFAULT_DISPLAY with an empty (terminated) attribute list.
|
||||||
|
let display = unsafe {
|
||||||
|
egl.get_platform_display(
|
||||||
|
PLATFORM_SURFACELESS_MESA,
|
||||||
|
egl::DEFAULT_DISPLAY,
|
||||||
|
&[egl::ATTRIB_NONE],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.or_else(|_| {
|
||||||
|
// SAFETY: default display handle.
|
||||||
|
unsafe { egl.get_display(egl::DEFAULT_DISPLAY) }.ok_or(egl::Error::BadDisplay)
|
||||||
|
})
|
||||||
|
.map_err(|e| format!("no EGL display: {e}"))?;
|
||||||
|
egl.initialize(display)
|
||||||
|
.map_err(|e| format!("eglInitialize: {e}"))?;
|
||||||
|
egl.bind_api(egl::OPENGL_API)
|
||||||
|
.map_err(|e| format!("eglBindAPI: {e}"))?;
|
||||||
|
|
||||||
|
let attribs = [
|
||||||
|
egl::RENDERABLE_TYPE,
|
||||||
|
egl::OPENGL_BIT,
|
||||||
|
egl::SURFACE_TYPE,
|
||||||
|
egl::PBUFFER_BIT,
|
||||||
|
egl::NONE,
|
||||||
|
];
|
||||||
|
let config = egl
|
||||||
|
.choose_first_config(display, &attribs)
|
||||||
|
.map_err(|e| format!("eglChooseConfig: {e}"))?
|
||||||
|
.ok_or("no EGL config")?;
|
||||||
|
let ctx_attribs = [
|
||||||
|
egl::CONTEXT_MAJOR_VERSION,
|
||||||
|
4,
|
||||||
|
egl::CONTEXT_MINOR_VERSION,
|
||||||
|
3,
|
||||||
|
egl::CONTEXT_OPENGL_PROFILE_MASK,
|
||||||
|
egl::CONTEXT_OPENGL_CORE_PROFILE_BIT,
|
||||||
|
egl::NONE,
|
||||||
|
];
|
||||||
|
let context = egl
|
||||||
|
.create_context(display, config, None, &ctx_attribs)
|
||||||
|
.map_err(|e| format!("eglCreateContext (GL 4.3 core): {e}"))?;
|
||||||
|
// Surfaceless: EGL_KHR_surfaceless_context.
|
||||||
|
egl.make_current(display, None, None, Some(context))
|
||||||
|
.map_err(|e| format!("eglMakeCurrent: {e}"))?;
|
||||||
|
Ok(Headless {
|
||||||
|
egl,
|
||||||
|
display,
|
||||||
|
context,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn proc_address(&self, name: &str) -> *const c_void {
|
||||||
|
self.egl
|
||||||
|
.get_proc_address(name)
|
||||||
|
.map_or(std::ptr::null(), |f| f as *const c_void)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Headless {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = self.egl.make_current(self.display, None, None, None);
|
||||||
|
let _ = self.egl.destroy_context(self.display, self.context);
|
||||||
|
let _ = self.egl.terminate(self.display);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup() -> Option<(Headless, OpenGL)> {
|
||||||
|
let h = match Headless::new() {
|
||||||
|
Ok(h) => h,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("SKIP: headless EGL unavailable: {e}");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut api = OpenGL::init(AlphaBlending::Native);
|
||||||
|
if let Err(e) = api.prepare_context(|n| h.proc_address(n)) {
|
||||||
|
eprintln!("SKIP: cannot prepare GL context: {e}");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some((h, api))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the pixel at (x, y) of an RGBA readback.
|
||||||
|
fn px(buf: &[u8], w: usize, x: usize, y: usize) -> [u8; 4] {
|
||||||
|
let i = (y * w + x) * 4;
|
||||||
|
[buf[i], buf[i + 1], buf[i + 2], buf[i + 3]]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn headless_shaders_compile_and_render() {
|
||||||
|
let Some((_h, mut api)) = setup() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let gl: Gl = api.gl().unwrap().clone();
|
||||||
|
|
||||||
|
// Convert the shadertoy test shaders to GLSL 430 and build everything.
|
||||||
|
let custom: Vec<String> = [shadertoy::test_shaders::CRT, shadertoy::test_shaders::FOCUS]
|
||||||
|
.iter()
|
||||||
|
.map(|src| {
|
||||||
|
let c = shadertoy::compile_shadertoy(src, None).unwrap();
|
||||||
|
shadertoy::glsl_from_compiled(&c).unwrap()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let shaders = api.init_shaders(&custom).expect("all pipelines compile");
|
||||||
|
assert_eq!(
|
||||||
|
shaders.post_pipelines.len(),
|
||||||
|
2,
|
||||||
|
"converted shadertoy shaders must compile and link on the driver"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Vertex layouts are accepted by the driver: no GL error pending.
|
||||||
|
gl.get_error().unwrap();
|
||||||
|
|
||||||
|
// ------- A tiny render pass: bg_color fills the target with the bg color.
|
||||||
|
let (w, h) = (16usize, 8usize);
|
||||||
|
let target = Arc::new(api.init_target(w, h).unwrap());
|
||||||
|
let uniforms = Uniforms {
|
||||||
|
projection_matrix: ghostty_renderer::math::ortho2d(0.0, w as f32, h as f32, 0.0),
|
||||||
|
screen_size: [w as f32, h as f32],
|
||||||
|
cell_size: [8.0, 8.0],
|
||||||
|
grid_size: [2, 1],
|
||||||
|
bg_color: [10, 200, 30, 255],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let ubo = buffer::Buffer::<Uniforms>::init_fill(
|
||||||
|
&gl,
|
||||||
|
buffer::Options {
|
||||||
|
target: ghostty_renderer::opengl::gl::BufferTarget::Uniform,
|
||||||
|
usage: ghostty_renderer::opengl::gl::BufferUsage::DynamicDraw,
|
||||||
|
},
|
||||||
|
&[uniforms],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// A surfaceless context has no default viewport (GTK's GLArea sets one).
|
||||||
|
gl.viewport(0, 0, w as i32, h as i32).unwrap();
|
||||||
|
assert_eq!(api.surface_size().unwrap(), (w as u32, h as u32));
|
||||||
|
|
||||||
|
let frame = api.begin_frame(Arc::clone(&target));
|
||||||
|
{
|
||||||
|
let attachments = [Attachment {
|
||||||
|
target: AttachmentTarget::Target(&target),
|
||||||
|
clear_color: Some([0.0, 0.0, 0.0, 0.0]),
|
||||||
|
}];
|
||||||
|
let mut pass = frame.render_pass(&attachments);
|
||||||
|
pass.step(&Step {
|
||||||
|
pipeline: &shaders.bg_color,
|
||||||
|
uniforms: Some(ubo.raw()),
|
||||||
|
buffers: &[],
|
||||||
|
textures: &[],
|
||||||
|
samplers: &[],
|
||||||
|
draw: Draw::new(Primitive::Triangle, 3),
|
||||||
|
});
|
||||||
|
pass.complete();
|
||||||
|
}
|
||||||
|
let health = frame.complete(true);
|
||||||
|
assert_eq!(health, ghostty_renderer::Health::Healthy);
|
||||||
|
|
||||||
|
// Read the target back.
|
||||||
|
let fbobind = gl.bind_framebuffer(
|
||||||
|
target.framebuffer(),
|
||||||
|
ghostty_renderer::opengl::gl::FramebufferTarget::Read,
|
||||||
|
);
|
||||||
|
let pixels = gl.read_pixels_rgba8(0, 0, w as i32, h as i32);
|
||||||
|
gl.unbind_framebuffer(fbobind);
|
||||||
|
gl.get_error().unwrap();
|
||||||
|
assert_eq!(px(&pixels, w, 0, 0), [10, 200, 30, 255]);
|
||||||
|
assert_eq!(px(&pixels, w, w - 1, h - 1), [10, 200, 30, 255]);
|
||||||
|
|
||||||
|
// ------- The other pipelines draw (instanced) without GL errors.
|
||||||
|
let instances = buffer::Buffer::<CellText>::init_fill(
|
||||||
|
&gl,
|
||||||
|
buffer::Options::default(),
|
||||||
|
&[CellText {
|
||||||
|
grid_pos: [0, 0],
|
||||||
|
color: [255, 255, 255, 255],
|
||||||
|
..Default::default()
|
||||||
|
}],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let images = buffer::Buffer::<Image>::init_fill(
|
||||||
|
&gl,
|
||||||
|
buffer::Options::default(),
|
||||||
|
&[Image {
|
||||||
|
source_rect: [0.0, 0.0, 1.0, 1.0],
|
||||||
|
dest_size: [4.0, 4.0],
|
||||||
|
..Default::default()
|
||||||
|
}],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let bg_images = buffer::Buffer::<BgImage>::init_fill(
|
||||||
|
&gl,
|
||||||
|
buffer::Options::default(),
|
||||||
|
&[BgImage {
|
||||||
|
opacity: 1.0,
|
||||||
|
info: BgImageInfo {
|
||||||
|
position: BgImagePosition::Mc,
|
||||||
|
fit: BgImageFit::Contain,
|
||||||
|
repeat: false,
|
||||||
|
}
|
||||||
|
.to_bits(),
|
||||||
|
pad: [0; 3],
|
||||||
|
}],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let bg_cells = buffer::Buffer::<[u8; 4]>::init_fill(
|
||||||
|
&gl,
|
||||||
|
buffer::Options {
|
||||||
|
target: ghostty_renderer::opengl::gl::BufferTarget::Storage,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
&[[1, 2, 3, 255]; 2],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let atlas = ghostty_font::atlas::Atlas::new(64, ghostty_font::atlas::Format::Grayscale);
|
||||||
|
let atlas_tex = api.init_atlas_texture(&atlas).unwrap();
|
||||||
|
let sampler = ghostty_renderer::opengl::Sampler::init(&gl, api.sampler_options()).unwrap();
|
||||||
|
let img_tex = ghostty_renderer::opengl::Texture::init(
|
||||||
|
&gl,
|
||||||
|
api.image_texture_options(ghostty_renderer::opengl::ImageTextureFormat::Rgba, true),
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
Some(&[255, 0, 0, 255]),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let attachments = [Attachment {
|
||||||
|
target: AttachmentTarget::Target(&target),
|
||||||
|
clear_color: None,
|
||||||
|
}];
|
||||||
|
let mut pass = ghostty_renderer::opengl::RenderPass::begin(
|
||||||
|
&gl,
|
||||||
|
ghostty_renderer::opengl::render_pass::Options {
|
||||||
|
attachments: &attachments,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
pass.step(&Step {
|
||||||
|
pipeline: &shaders.cell_bg,
|
||||||
|
uniforms: Some(ubo.raw()),
|
||||||
|
buffers: &[None, Some(bg_cells.raw())],
|
||||||
|
textures: &[],
|
||||||
|
samplers: &[],
|
||||||
|
draw: Draw::new(Primitive::Triangle, 3),
|
||||||
|
});
|
||||||
|
pass.step(&Step {
|
||||||
|
pipeline: &shaders.cell_text,
|
||||||
|
uniforms: Some(ubo.raw()),
|
||||||
|
buffers: &[Some(instances.raw()), Some(bg_cells.raw())],
|
||||||
|
textures: &[Some(&atlas_tex), Some(&atlas_tex)],
|
||||||
|
samplers: &[],
|
||||||
|
draw: Draw {
|
||||||
|
ty: Primitive::TriangleStrip,
|
||||||
|
vertex_count: 4,
|
||||||
|
instance_count: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
pass.step(&Step {
|
||||||
|
pipeline: &shaders.image,
|
||||||
|
uniforms: Some(ubo.raw()),
|
||||||
|
buffers: &[Some(images.raw())],
|
||||||
|
textures: &[Some(&img_tex)],
|
||||||
|
samplers: &[Some(&sampler)],
|
||||||
|
draw: Draw {
|
||||||
|
ty: Primitive::TriangleStrip,
|
||||||
|
vertex_count: 4,
|
||||||
|
instance_count: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
pass.step(&Step {
|
||||||
|
pipeline: &shaders.bg_image,
|
||||||
|
uniforms: Some(ubo.raw()),
|
||||||
|
buffers: &[Some(bg_images.raw())],
|
||||||
|
textures: &[Some(&img_tex)],
|
||||||
|
samplers: &[Some(&sampler)],
|
||||||
|
draw: Draw {
|
||||||
|
ty: Primitive::TriangleStrip,
|
||||||
|
vertex_count: 4,
|
||||||
|
instance_count: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
pass.complete();
|
||||||
|
gl.finish();
|
||||||
|
gl.get_error().unwrap();
|
||||||
|
|
||||||
|
// ------- Post pipelines render into a texture attachment.
|
||||||
|
let tex =
|
||||||
|
ghostty_renderer::opengl::Texture::init(&gl, api.texture_options(), w, h, None).unwrap();
|
||||||
|
let attachments = [Attachment {
|
||||||
|
target: AttachmentTarget::Texture(&tex),
|
||||||
|
clear_color: Some([0.0, 0.0, 0.0, 1.0]),
|
||||||
|
}];
|
||||||
|
let mut pass = ghostty_renderer::opengl::RenderPass::begin(
|
||||||
|
&gl,
|
||||||
|
ghostty_renderer::opengl::render_pass::Options {
|
||||||
|
attachments: &attachments,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let shadertoy_ubo = buffer::Buffer::<shadertoy::Uniforms>::init_fill(
|
||||||
|
&gl,
|
||||||
|
buffer::Options {
|
||||||
|
target: ghostty_renderer::opengl::gl::BufferTarget::Uniform,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
&[shadertoy::Uniforms {
|
||||||
|
resolution: [w as f32, h as f32, 1.0],
|
||||||
|
..Default::default()
|
||||||
|
}],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
for post in &shaders.post_pipelines {
|
||||||
|
pass.step(&Step {
|
||||||
|
pipeline: post,
|
||||||
|
uniforms: Some(shadertoy_ubo.raw()),
|
||||||
|
buffers: &[],
|
||||||
|
textures: &[Some(&img_tex)],
|
||||||
|
samplers: &[Some(&sampler)],
|
||||||
|
draw: Draw::new(Primitive::Triangle, 3),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
pass.complete();
|
||||||
|
gl.finish();
|
||||||
|
gl.get_error().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn headless_buffer_sync_grows() {
|
||||||
|
let Some((_h, api)) = setup() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let gl = api.gl().unwrap().clone();
|
||||||
|
let mut b = buffer::Buffer::<u32>::init(&gl, buffer::Options::default(), 2).unwrap();
|
||||||
|
b.sync(&[1, 2]).unwrap();
|
||||||
|
assert_eq!(b.len, 2);
|
||||||
|
b.sync(&[1, 2, 3]).unwrap();
|
||||||
|
assert_eq!(b.len, 6, "reallocates to double the required size");
|
||||||
|
let n = b.sync_from_slices(&[&[1, 2], &[3]]).unwrap();
|
||||||
|
assert_eq!(n, 3);
|
||||||
|
gl.get_error().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn headless_invalid_shader_reports_compile_error() {
|
||||||
|
let Some((_h, api)) = setup() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let gl = api.gl().unwrap();
|
||||||
|
let err = gl.create_program_vf("#version 430 core\nvoid main() { nope }", "x");
|
||||||
|
assert!(matches!(
|
||||||
|
err,
|
||||||
|
Err(ghostty_renderer::opengl::gl::Error::CompileFailed(_))
|
||||||
|
));
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# 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
|
||||||
|
```rust
|
||||||
|
// 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): `Uniforms` 144 B std140 (pack `bools`/`padding_extend`
|
||||||
|
with `Bools::to_bits` / `PaddingExtend::to_bits`), `CellText` 32 B, `Image` 48 B, `BgImage` 8 B (`BgImageInfo::to_bits`),
|
||||||
|
`shadertoy::Uniforms` 4496 B. Explicit `pad*` fields reproduce Zig `extern struct`/`align()` padding (set to 0; use `..Default::default()`).
|
||||||
|
* Atlas upload: `api.init_atlas_texture(&atlas)` creates the `GL_TEXTURE_RECTANGLE` texture (data `None`);
|
||||||
|
upload with `Texture::replace_region(0,0,size,size,&atlas.data)` and track `atlas.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.
|
||||||
|
* `present` keeps `Arc<Target>` as `last_target` (Zig copied the struct); call `clear_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 `RenderStateCursor` instead of Terminal/RenderState (TODO(integration)); + password/lock test.
|
||||||
|
* shadertoy.zig: `spirv`, `spirv invalid`, `shadertoy to glsl` ported (+ focus shader, file loading, helper-order tests);
|
||||||
|
`shadertoy to msl` skipped (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-egl` dynamic, `EGL_PLATFORM_SURFACELESS_MESA`);
|
||||||
|
builds all 5 pipelines + 2 converted shadertoy post pipelines, renders bg_color into a `Target` and 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 print `SKIP:` and return (they do run here).
|
||||||
|
|
||||||
|
## Deviations / notes
|
||||||
|
* `Buffer::init_fill`: Zig sets `len = data.len * sizeof(T)` (bug); we store the element count.
|
||||||
|
* `Pipeline` drop also deletes its FBO and VAO (Zig only destroyed the program).
|
||||||
|
* `Frame` no longer holds the renderer; `complete` returns `Health` and the caller calls `frameCompleted`.
|
||||||
|
* `shadertoy::Uniforms` field order follows Zig (`selection_background_color` before `selection_foreground_color`) while the GLSL
|
||||||
|
block declares `iSelectionForegroundColor` before `iSelectionBackgroundColor` — 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` (config `RepeatablePath`), `message::{ThreadDerivedConfig, RendererDerivedConfig, Flattened, SharedGridKey}` stubs,
|
||||||
|
`options::Options` lacks `surface_mailbox/rt_surface/thread`; `Message::init_change_config` takes pre-built derived configs.
|
||||||
|
* Metal/WebGL (`Backend::Metal/Webgl` variants exist only for parity with backend.zig).
|
||||||
Reference in new issue
Block a user