diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-04-04 19:22:04 +0300 |
|---|---|---|
| committer | Can Celik <ogulcancelik@gmail.com> | 2026-04-05 01:23:06 +0300 |
| commit | 41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch) | |
| tree | 3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/renderer/shaders | |
| parent | 55c138263e190a1e79d4b997bfb9397306c0ec1f (diff) | |
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/renderer/shaders')
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! |
