aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/renderer/shaders
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-04-04 19:22:04 +0300
committerCan Celik <ogulcancelik@gmail.com>2026-04-05 01:23:06 +0300
commit41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch)
tree3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/renderer/shaders
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (diff)
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/renderer/shaders')
-rw-r--r--vendor/libghostty-vt/src/renderer/shaders/glsl/bg_color.f.glsl13
-rw-r--r--vendor/libghostty-vt/src/renderer/shaders/glsl/bg_image.f.glsl63
-rw-r--r--vendor/libghostty-vt/src/renderer/shaders/glsl/bg_image.v.glsl145
-rw-r--r--vendor/libghostty-vt/src/renderer/shaders/glsl/cell_bg.f.glsl61
-rw-r--r--vendor/libghostty-vt/src/renderer/shaders/glsl/cell_text.f.glsl100
-rw-r--r--vendor/libghostty-vt/src/renderer/shaders/glsl/cell_text.v.glsl146
-rw-r--r--vendor/libghostty-vt/src/renderer/shaders/glsl/common.glsl156
-rw-r--r--vendor/libghostty-vt/src/renderer/shaders/glsl/full_screen.v.glsl24
-rw-r--r--vendor/libghostty-vt/src/renderer/shaders/glsl/image.f.glsl21
-rw-r--r--vendor/libghostty-vt/src/renderer/shaders/glsl/image.v.glsl47
-rw-r--r--vendor/libghostty-vt/src/renderer/shaders/shaders.metal853
-rw-r--r--vendor/libghostty-vt/src/renderer/shaders/shadertoy_prefix.glsl52
-rw-r--r--vendor/libghostty-vt/src/renderer/shaders/test_shadertoy_crt.glsl59
-rw-r--r--vendor/libghostty-vt/src/renderer/shaders/test_shadertoy_focus.glsl41
-rw-r--r--vendor/libghostty-vt/src/renderer/shaders/test_shadertoy_invalid.glsl12
15 files changed, 1793 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/renderer/shaders/glsl/bg_color.f.glsl b/vendor/libghostty-vt/src/renderer/shaders/glsl/bg_color.f.glsl
new file mode 100644
index 00000000..616c44b8
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/shaders/glsl/bg_color.f.glsl
@@ -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
+ );
+}
diff --git a/vendor/libghostty-vt/src/renderer/shaders/glsl/bg_image.f.glsl b/vendor/libghostty-vt/src/renderer/shaders/glsl/bg_image.f.glsl
new file mode 100644
index 00000000..ee1195ef
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/shaders/glsl/bg_image.f.glsl
@@ -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;
+}
diff --git a/vendor/libghostty-vt/src/renderer/shaders/glsl/bg_image.v.glsl b/vendor/libghostty-vt/src/renderer/shaders/glsl/bg_image.v.glsl
new file mode 100644
index 00000000..d55aa174
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/shaders/glsl/bg_image.v.glsl
@@ -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);
+}
diff --git a/vendor/libghostty-vt/src/renderer/shaders/glsl/cell_bg.f.glsl b/vendor/libghostty-vt/src/renderer/shaders/glsl/cell_bg.f.glsl
new file mode 100644
index 00000000..fa48c673
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/shaders/glsl/cell_bg.f.glsl
@@ -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();
+}
diff --git a/vendor/libghostty-vt/src/renderer/shaders/glsl/cell_text.f.glsl b/vendor/libghostty-vt/src/renderer/shaders/glsl/cell_text.f.glsl
new file mode 100644
index 00000000..176efcbd
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/shaders/glsl/cell_text.f.glsl
@@ -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;
+ }
+ }
+}
diff --git a/vendor/libghostty-vt/src/renderer/shaders/glsl/cell_text.v.glsl b/vendor/libghostty-vt/src/renderer/shaders/glsl/cell_text.v.glsl
new file mode 100644
index 00000000..7e38e2f0
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/shaders/glsl/cell_text.v.glsl
@@ -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);
+ }
+}
diff --git a/vendor/libghostty-vt/src/renderer/shaders/glsl/common.glsl b/vendor/libghostty-vt/src/renderer/shaders/glsl/common.glsl
new file mode 100644
index 00000000..1a7ea03a
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/shaders/glsl/common.glsl
@@ -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;
+}
+
+//----------------------------------------------------------------------------//
diff --git a/vendor/libghostty-vt/src/renderer/shaders/glsl/full_screen.v.glsl b/vendor/libghostty-vt/src/renderer/shaders/glsl/full_screen.v.glsl
new file mode 100644
index 00000000..b89cedfa
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/shaders/glsl/full_screen.v.glsl
@@ -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;
+}
diff --git a/vendor/libghostty-vt/src/renderer/shaders/glsl/image.f.glsl b/vendor/libghostty-vt/src/renderer/shaders/glsl/image.f.glsl
new file mode 100644
index 00000000..4f89d7a7
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/shaders/glsl/image.f.glsl
@@ -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;
+}
diff --git a/vendor/libghostty-vt/src/renderer/shaders/glsl/image.v.glsl b/vendor/libghostty-vt/src/renderer/shaders/glsl/image.v.glsl
new file mode 100644
index 00000000..779fae32
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/shaders/glsl/image.v.glsl
@@ -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);
+}
diff --git a/vendor/libghostty-vt/src/renderer/shaders/shaders.metal b/vendor/libghostty-vt/src/renderer/shaders/shaders.metal
new file mode 100644
index 00000000..4e02b633
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/shaders/shaders.metal
@@ -0,0 +1,853 @@
+#include <metal_stdlib>
+
+using namespace metal;
+
+enum Padding : uint8_t {
+ EXTEND_LEFT = 1u,
+ EXTEND_RIGHT = 2u,
+ EXTEND_UP = 4u,
+ EXTEND_DOWN = 8u,
+};
+
+struct Uniforms {
+ float4x4 projection_matrix;
+ float2 screen_size;
+ float2 cell_size;
+ ushort2 grid_size;
+ float4 grid_padding;
+ uint8_t padding_extend;
+ float min_contrast;
+ ushort2 cursor_pos;
+ uchar4 cursor_color;
+ uchar4 bg_color;
+ bool cursor_wide;
+ bool use_display_p3;
+ bool use_linear_blending;
+ bool use_linear_correction;
+};
+
+//-------------------------------------------------------------------
+// Color Functions
+//-------------------------------------------------------------------
+#pragma mark - Colors
+
+// D50-adapted sRGB to XYZ conversion matrix.
+// http://www.brucelindbloom.com/Eqn_RGB_XYZ_Matrix.html
+constant float3x3 sRGB_XYZ = transpose(float3x3(
+ 0.4360747, 0.3850649, 0.1430804,
+ 0.2225045, 0.7168786, 0.0606169,
+ 0.0139322, 0.0971045, 0.7141733
+));
+// XYZ to Display P3 conversion matrix.
+// http://endavid.com/index.php?entry=79
+constant float3x3 XYZ_DP3 = transpose(float3x3(
+ 2.40414768,-0.99010704,-0.39759019,
+ -0.84239098, 1.79905954, 0.01597023,
+ 0.04838763,-0.09752546, 1.27393636
+));
+// By composing the two above matrices we get
+// our sRGB to Display P3 conversion matrix.
+constant float3x3 sRGB_DP3 = XYZ_DP3 * sRGB_XYZ;
+
+// Converts a color in linear sRGB to linear Display P3
+//
+// TODO: The color matrix should probably be computed
+// dynamically and passed as a uniform, rather
+// than being hard coded above.
+float3 srgb_to_display_p3(float3 srgb) {
+ return sRGB_DP3 * srgb;
+}
+
+// Converts a color from sRGB gamma encoding to linear.
+float4 linearize(float4 srgb) {
+ bool3 cutoff = srgb.rgb <= 0.04045;
+ float3 lower = srgb.rgb / 12.92;
+ float3 higher = pow((srgb.rgb + 0.055) / 1.055, 2.4);
+ srgb.rgb = mix(higher, lower, float3(cutoff));
+
+ return srgb;
+}
+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.
+float4 unlinearize(float4 linear) {
+ bool3 cutoff = linear.rgb <= 0.0031308;
+ float3 lower = linear.rgb * 12.92;
+ float3 higher = pow(linear.rgb, 1.0 / 2.4) * 1.055 - 0.055;
+ linear.rgb = mix(higher, lower, float3(cutoff));
+
+ return linear;
+}
+float unlinearize(float v) {
+ return v <= 0.0031308 ? v * 12.92 : pow(v, 1.0 / 2.4) * 1.055 - 0.055;
+}
+
+// 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(float3 color) {
+ return dot(color, float3(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(float3 color1, float3 color2) {
+ float l1 = luminance(color1);
+ float l2 = luminance(color2);
+ return (max(l1, l2) + 0.05f) / (min(l1, l2) + 0.05f);
+}
+
+// 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.
+float4 contrasted_color(float min, float4 fg, float4 bg) {
+ float ratio = contrast_ratio(fg.rgb, bg.rgb);
+ if (ratio < min) {
+ float white_ratio = contrast_ratio(float3(1.0f), bg.rgb);
+ float black_ratio = contrast_ratio(float3(0.0f), bg.rgb);
+ if (white_ratio > black_ratio) {
+ return float4(1.0f);
+ } else {
+ return float4(0.0f, 0.0f, 0.0f, 1.0f);
+ }
+ }
+
+ return fg;
+}
+
+// Load a 4 byte RGBA non-premultiplied color and linearize
+// and convert it as necessary depending on the provided info.
+//
+// Returns a color in the Display P3 color space.
+//
+// If `display_p3` is true, then the provided color is assumed to
+// already be in the Display P3 color space, otherwise it's treated
+// as an sRGB color and is appropriately converted to Display P3.
+//
+// `linear` controls whether the returned color is linear or gamma encoded.
+float4 load_color(
+ uchar4 in_color,
+ bool display_p3,
+ bool linear
+) {
+ // 0 .. 255 -> 0.0 .. 1.0
+ float4 color = float4(in_color) / 255.0f;
+
+ // If our color is already in Display P3 and
+ // we aren't doing linear blending, then we
+ // already have the correct color here and
+ // can premultiply and return it.
+ if (display_p3 && !linear) {
+ color.rgb *= color.a;
+ return color;
+ }
+
+ // The color is in either the sRGB or Display P3 color space,
+ // so in either case, it's a color space which uses the sRGB
+ // transfer function, so we can use one function in order to
+ // linearize it in either case.
+ //
+ // Even if we aren't doing linear blending, the color
+ // needs to be in linear space to convert color spaces.
+ color = linearize(color);
+
+ // If we're *NOT* using display P3 colors, then we're dealing
+ // with an sRGB color, in which case we need to convert it in
+ // to the Display P3 color space, since our output is always
+ // Display P3.
+ if (!display_p3) {
+ color.rgb = srgb_to_display_p3(color.rgb);
+ }
+
+ // If we're not doing linear blending, then we need to
+ // unlinearize after doing the color space conversion.
+ if (!linear) {
+ color = unlinearize(color);
+ }
+
+ // Premultiply our color by its alpha.
+ color.rgb *= color.a;
+
+ return color;
+}
+
+//-------------------------------------------------------------------
+// Full Screen Vertex Shader
+//-------------------------------------------------------------------
+#pragma mark - Full Screen Vertex Shader
+
+struct FullScreenVertexOut {
+ float4 position [[position]];
+};
+
+vertex FullScreenVertexOut full_screen_vertex(
+ uint vid [[vertex_id]]
+) {
+ FullScreenVertexOut out;
+
+ float4 position;
+ position.x = (vid == 2) ? 3.0 : -1.0;
+ position.y = (vid == 0) ? -3.0 : 1.0;
+ position.zw = 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)
+
+ out.position = position;
+
+ return out;
+}
+
+//-------------------------------------------------------------------
+// Background Color Shader
+//-------------------------------------------------------------------
+#pragma mark - BG Color Shader
+
+fragment float4 bg_color_fragment(
+ FullScreenVertexOut in [[stage_in]],
+ constant Uniforms& uniforms [[buffer(1)]]
+) {
+ return load_color(
+ uniforms.bg_color,
+ uniforms.use_display_p3,
+ uniforms.use_linear_blending
+ );
+}
+
+//-------------------------------------------------------------------
+// Background Image Shader
+//-------------------------------------------------------------------
+#pragma mark - BG Image Shader
+
+struct BgImageVertexIn {
+ float opacity [[attribute(0)]];
+ uint8_t info [[attribute(1)]];
+};
+
+enum BgImagePosition : uint8_t {
+ // 4 bits of info.
+ BG_IMAGE_POSITION = 15u,
+
+ BG_IMAGE_TL = 0u,
+ BG_IMAGE_TC = 1u,
+ BG_IMAGE_TR = 2u,
+ BG_IMAGE_ML = 3u,
+ BG_IMAGE_MC = 4u,
+ BG_IMAGE_MR = 5u,
+ BG_IMAGE_BL = 6u,
+ BG_IMAGE_BC = 7u,
+ BG_IMAGE_BR = 8u,
+};
+
+enum BgImageFit : uint8_t {
+ // 2 bits of info shifted 4.
+ BG_IMAGE_FIT = 3u << 4,
+
+ BG_IMAGE_CONTAIN = 0u << 4,
+ BG_IMAGE_COVER = 1u << 4,
+ BG_IMAGE_STRETCH = 2u << 4,
+ BG_IMAGE_NO_FIT = 3u << 4,
+};
+
+enum BgImageRepeat : uint8_t {
+ // 1 bit of info shifted 6.
+ BG_IMAGE_REPEAT = 1u << 6,
+};
+
+struct BgImageVertexOut {
+ float4 position [[position]];
+ float4 bg_color [[flat]];
+ float2 offset [[flat]];
+ float2 scale [[flat]];
+ float opacity [[flat]];
+ bool repeat [[flat]];
+};
+
+vertex BgImageVertexOut bg_image_vertex(
+ uint vid [[vertex_id]],
+ BgImageVertexIn in [[stage_in]],
+ texture2d<float> image [[texture(0)]],
+ constant Uniforms& uniforms [[buffer(1)]]
+) {
+ BgImageVertexOut out;
+
+ float4 position;
+ position.x = (vid == 2) ? 3.0 : -1.0;
+ position.y = (vid == 0) ? -3.0 : 1.0;
+ position.zw = 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)
+
+ out.position = position;
+
+ out.opacity = in.opacity;
+
+ out.repeat = (in.info & BG_IMAGE_REPEAT) == BG_IMAGE_REPEAT;
+
+ float2 screen_size = uniforms.screen_size;
+ float2 tex_size = float2(image.get_width(), image.get_height());
+
+ float2 dest_size = tex_size;
+ switch (in.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;
+ }
+
+ float2 start = float2(0.0);
+ float2 mid = (screen_size - dest_size) / 2;
+ float2 end = screen_size - dest_size;
+
+ float2 dest_offset = mid;
+ switch (in.info & BG_IMAGE_POSITION) {
+ case BG_IMAGE_TL: {
+ dest_offset = float2(start.x, start.y);
+ } break;
+ case BG_IMAGE_TC: {
+ dest_offset = float2(mid.x, start.y);
+ } break;
+ case BG_IMAGE_TR: {
+ dest_offset = float2(end.x, start.y);
+ } break;
+ case BG_IMAGE_ML: {
+ dest_offset = float2(start.x, mid.y);
+ } break;
+ case BG_IMAGE_MC: {
+ dest_offset = float2(mid.x, mid.y);
+ } break;
+ case BG_IMAGE_MR: {
+ dest_offset = float2(end.x, mid.y);
+ } break;
+ case BG_IMAGE_BL: {
+ dest_offset = float2(start.x, end.y);
+ } break;
+ case BG_IMAGE_BC: {
+ dest_offset = float2(mid.x, end.y);
+ } break;
+ case BG_IMAGE_BR: {
+ dest_offset = float2(end.x, end.y);
+ } break;
+ }
+
+ out.offset = dest_offset;
+ out.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.
+ out.bg_color = float4(load_color(
+ uchar4(uniforms.bg_color.rgb, 255),
+ uniforms.use_display_p3,
+ uniforms.use_linear_blending
+ ).rgb, float(uniforms.bg_color.a) / 255.0);
+
+ return out;
+}
+
+fragment float4 bg_image_fragment(
+ BgImageVertexOut in [[stage_in]],
+ texture2d<float> image [[texture(0)]],
+ constant Uniforms& uniforms [[buffer(1)]]
+) {
+ constexpr sampler textureSampler(
+ coord::pixel,
+ address::clamp_to_zero,
+ filter::linear
+ );
+
+ // 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.
+ float2 tex_coord = (in.position.xy - in.offset) * in.scale;
+
+ // If we need to repeat the texture, wrap the coordinates.
+ if (in.repeat) {
+ float2 tex_size = float2(image.get_width(), image.get_height());
+
+ tex_coord = fmod(fmod(tex_coord, tex_size) + tex_size, tex_size);
+ }
+
+ float4 rgba = image.sample(textureSampler, tex_coord);
+
+ if (!uniforms.use_linear_blending) {
+ rgba = unlinearize(rgba);
+ }
+
+ // Premultiply the bg image.
+ 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(in.opacity, 1.0 / in.bg_color.a);
+
+ // Blend it on to a fully opaque version of the background color.
+ rgba += max(float4(0.0), float4(in.bg_color.rgb, 1.0) * (1.0 - rgba.a));
+
+ // Multiply everything by the background color alpha.
+ rgba *= in.bg_color.a;
+
+ return rgba;
+}
+
+//-------------------------------------------------------------------
+// Cell Background Shader
+//-------------------------------------------------------------------
+#pragma mark - Cell BG Shader
+
+fragment float4 cell_bg_fragment(
+ FullScreenVertexOut in [[stage_in]],
+ constant Uniforms& uniforms [[buffer(1)]],
+ constant uchar4 *cells [[buffer(2)]]
+) {
+ int2 grid_pos = int2(floor((in.position.xy - uniforms.grid_padding.wx) / uniforms.cell_size));
+
+ float4 bg = float4(0.0);
+
+ // Clamp x position, extends edge bg colors in to padding on sides.
+ if (grid_pos.x < 0) {
+ if (uniforms.padding_extend & EXTEND_LEFT) {
+ grid_pos.x = 0;
+ } else {
+ return bg;
+ }
+ } else if (grid_pos.x > uniforms.grid_size.x - 1) {
+ if (uniforms.padding_extend & EXTEND_RIGHT) {
+ grid_pos.x = uniforms.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 (uniforms.padding_extend & EXTEND_UP) {
+ grid_pos.y = 0;
+ } else {
+ return bg;
+ }
+ } else if (grid_pos.y > uniforms.grid_size.y - 1) {
+ if (uniforms.padding_extend & EXTEND_DOWN) {
+ grid_pos.y = uniforms.grid_size.y - 1;
+ } else {
+ return bg;
+ }
+ }
+
+ // Load the color for the cell.
+ uchar4 cell_color = cells[grid_pos.y * uniforms.grid_size.x + grid_pos.x];
+
+ // Convert the color and return it.
+ //
+ // TODO: It might be a good idea to do a pass before this
+ // to convert all of the bg colors, so we don't waste
+ // a bunch of work converting the cell color in every
+ // fragment of each cell. It's not the most epxensive
+ // operation, but it is still wasted work.
+ return load_color(
+ cell_color,
+ uniforms.use_display_p3,
+ uniforms.use_linear_blending
+ );
+}
+
+//-------------------------------------------------------------------
+// Cell Text Shader
+//-------------------------------------------------------------------
+#pragma mark - Cell Text Shader
+
+enum CellTextAtlas : uint8_t {
+ ATLAS_GRAYSCALE = 0u,
+ ATLAS_COLOR = 1u,
+};
+
+// We use a packed struct of bools for misc properties of the glyph.
+enum CellTextBools : uint8_t {
+ // Don't apply min contrast to this glyph.
+ NO_MIN_CONTRAST = 1u,
+ // This is the cursor glyph.
+ IS_CURSOR_GLYPH = 2u,
+};
+
+struct CellTextVertexIn {
+ // The position of the glyph in the texture (x, y)
+ uint2 glyph_pos [[attribute(0)]];
+
+ // The size of the glyph in the texture (w, h)
+ uint2 glyph_size [[attribute(1)]];
+
+ // The left and top bearings for the glyph (x, y)
+ int2 bearings [[attribute(2)]];
+
+ // The grid coordinates (x, y) where x < columns and y < rows
+ ushort2 grid_pos [[attribute(3)]];
+
+ // The color of the rendered text glyph.
+ uchar4 color [[attribute(4)]];
+
+ // Which atlas to sample for our glyph.
+ uint8_t atlas [[attribute(5)]];
+
+ // Misc properties of the glyph.
+ uint8_t bools [[attribute(6)]];
+};
+
+struct CellTextVertexOut {
+ float4 position [[position]];
+ uint8_t atlas [[flat]];
+ float4 color [[flat]];
+ float4 bg_color [[flat]];
+ float2 tex_coord;
+};
+
+vertex CellTextVertexOut cell_text_vertex(
+ uint vid [[vertex_id]],
+ CellTextVertexIn in [[stage_in]],
+ constant Uniforms& uniforms [[buffer(1)]],
+ constant uchar4 *bg_colors [[buffer(2)]]
+) {
+ // Convert the grid x, y into world space x, y by accounting for cell size
+ float2 cell_pos = uniforms.cell_size * float2(in.grid_pos);
+
+ // 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)
+ float2 corner;
+ corner.x = float(vid == 1 || vid == 3);
+ corner.y = float(vid == 2 || vid == 3);
+
+ CellTextVertexOut out;
+ out.atlas = in.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.
+
+ float2 size = float2(in.glyph_size);
+ float2 offset = float2(in.bearings);
+
+ offset.y = uniforms.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;
+ out.position =
+ uniforms.projection_matrix * float4(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.tex_coord = float2(in.glyph_pos) + float2(in.glyph_size) * corner;
+
+ // Get our color. We always fetch a linearized version to
+ // make it easier to handle minimum contrast calculations.
+ out.color = load_color(
+ in.color,
+ uniforms.use_display_p3,
+ true
+ );
+
+ // Get the BG color
+ out.bg_color = load_color(
+ bg_colors[in.grid_pos.y * uniforms.grid_size.x + in.grid_pos.x],
+ uniforms.use_display_p3,
+ true
+ );
+ // Blend it with the global bg color
+ float4 global_bg = load_color(
+ uniforms.bg_color,
+ uniforms.use_display_p3,
+ true
+ );
+ out.bg_color += global_bg * (1.0 - out.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 (uniforms.min_contrast > 1.0f && (in.bools & NO_MIN_CONTRAST) == 0) {
+ // Ensure our minimum contrast
+ out.color = contrasted_color(uniforms.min_contrast, out.color, out.bg_color);
+ }
+
+ // Check if current position is under cursor (including wide cursor)
+ bool is_cursor_pos = (
+ in.grid_pos.x == uniforms.cursor_pos.x ||
+ uniforms.cursor_wide &&
+ in.grid_pos.x == uniforms.cursor_pos.x + 1
+ ) && in.grid_pos.y == uniforms.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 ((in.bools & IS_CURSOR_GLYPH) == 0 && is_cursor_pos) {
+ out.color = load_color(
+ uniforms.cursor_color,
+ uniforms.use_display_p3,
+ true
+ );
+ }
+
+ return out;
+}
+
+fragment float4 cell_text_fragment(
+ CellTextVertexOut in [[stage_in]],
+ texture2d<float> textureGrayscale [[texture(0)]],
+ texture2d<float> textureColor [[texture(1)]],
+ constant Uniforms& uniforms [[buffer(1)]]
+) {
+ constexpr sampler textureSampler(
+ coord::pixel,
+ address::clamp_to_edge,
+ filter::nearest
+ );
+
+ switch (in.atlas) {
+ default:
+ case ATLAS_GRAYSCALE: {
+ // Our input color is always linear.
+ float4 color = in.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 (!uniforms.use_linear_blending) {
+ color.rgb /= color.a;
+ color = unlinearize(color);
+ color.rgb *= color.a;
+ }
+
+ // Fetch our alpha mask for this pixel.
+ float a = textureGrayscale.sample(textureSampler, in.tex_coord).r;
+
+ // Linear blending weight correction corrects the alpha value to
+ // produce blending results which match gamma-incorrect blending.
+ if (uniforms.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.
+ float4 bg = in.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;
+
+ return color;
+ }
+
+ case ATLAS_COLOR: {
+ // For now, we assume that color glyphs
+ // are already premultiplied linear colors.
+ float4 color = textureColor.sample(textureSampler, in.tex_coord);
+
+ // If we're doing linear blending, we can return this right away.
+ if (uniforms.use_linear_blending) {
+ return color;
+ }
+
+ // Otherwise we need to unlinearize the color. Since the alpha is
+ // premultiplied, we need to divide it out before unlinearizing.
+ color.rgb /= color.a;
+ color = unlinearize(color);
+ color.rgb *= color.a;
+
+ return color;
+ }
+ }
+}
+//-------------------------------------------------------------------
+// Image Shader
+//-------------------------------------------------------------------
+#pragma mark - Image Shader
+
+struct ImageVertexIn {
+ // The grid coordinates (x, y) where x < columns and y < rows where
+ // the image will be rendered. It will be rendered from the top left.
+ float2 grid_pos [[attribute(0)]];
+
+ // Offset in pixels from the top-left of the cell to make the top-left
+ // corner of the image.
+ float2 cell_offset [[attribute(1)]];
+
+ // The source rectangle of the texture to sample from.
+ float4 source_rect [[attribute(2)]];
+
+ // The final width/height of the image in pixels.
+ float2 dest_size [[attribute(3)]];
+};
+
+struct ImageVertexOut {
+ float4 position [[position]];
+ float2 tex_coord;
+};
+
+vertex ImageVertexOut image_vertex(
+ uint vid [[vertex_id]],
+ ImageVertexIn in [[stage_in]],
+ texture2d<uint> image [[texture(0)]],
+ constant Uniforms& uniforms [[buffer(1)]]
+) {
+ // 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)
+ float2 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.
+ //
+ // We don't need to normalize because we use pixel addressing for our sampler.
+ float2 tex_coord = in.source_rect.xy;
+ tex_coord += in.source_rect.zw * corner;
+
+ ImageVertexOut out;
+
+ // The position of our image starts at the top-left of the grid cell and
+ // adds the source rect width/height components.
+ float2 image_pos = (uniforms.cell_size * in.grid_pos) + in.cell_offset;
+ image_pos += in.dest_size * corner;
+
+ out.position =
+ uniforms.projection_matrix * float4(image_pos.x, image_pos.y, 0.0f, 1.0f);
+ out.tex_coord = tex_coord;
+ return out;
+}
+
+fragment float4 image_fragment(
+ ImageVertexOut in [[stage_in]],
+ texture2d<float> image [[texture(0)]],
+ constant Uniforms& uniforms [[buffer(1)]]
+) {
+ constexpr sampler textureSampler(
+ coord::pixel,
+ address::clamp_to_edge,
+ filter::linear
+ );
+
+ float4 rgba = image.sample(textureSampler, in.tex_coord);
+
+ if (!uniforms.use_linear_blending) {
+ rgba = unlinearize(rgba);
+ }
+
+ rgba.rgb *= rgba.a;
+
+ return rgba;
+}
+
diff --git a/vendor/libghostty-vt/src/renderer/shaders/shadertoy_prefix.glsl b/vendor/libghostty-vt/src/renderer/shaders/shadertoy_prefix.glsl
new file mode 100644
index 00000000..4b6d091b
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/shaders/shadertoy_prefix.glsl
@@ -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); }
diff --git a/vendor/libghostty-vt/src/renderer/shaders/test_shadertoy_crt.glsl b/vendor/libghostty-vt/src/renderer/shaders/test_shadertoy_crt.glsl
new file mode 100644
index 00000000..b8d6dbb4
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/shaders/test_shadertoy_crt.glsl
@@ -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);
+}
diff --git a/vendor/libghostty-vt/src/renderer/shaders/test_shadertoy_focus.glsl b/vendor/libghostty-vt/src/renderer/shaders/test_shadertoy_focus.glsl
new file mode 100644
index 00000000..9fc2304d
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/shaders/test_shadertoy_focus.glsl
@@ -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);
+}
diff --git a/vendor/libghostty-vt/src/renderer/shaders/test_shadertoy_invalid.glsl b/vendor/libghostty-vt/src/renderer/shaders/test_shadertoy_invalid.glsl
new file mode 100644
index 00000000..1cff1c7c
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/shaders/test_shadertoy_invalid.glsl
@@ -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!