feat(renderer): port the OpenGL backend, shaders and size math

Ports the terminal-independent half of ZIG/src/renderer (OpenGL only):
- opengl/*: buffers, frames, pipelines, render passes, samplers,
  targets, textures and the OpenGL GraphicsAPI backend on top of glow;
  the GTK GLArea glue is replaced by a get_proc_address loader closure
- the GLSL shaders, copied verbatim and embedded; uniform/vertex structs
  as bytemuck Pod types with std140 offsets asserted in tests
- shadertoy custom shaders converted with naga (GLSL in -> validate ->
  GLSL 430 out) instead of glslang + spirv-cross
- size (screen/grid/cell/padding math), cursor style logic, backend
  health, options and message types

The crate opts into unsafe_code = "deny"; unsafe glow calls live only in
opengl/gl.rs. A headless EGL integration test builds every pipeline and
the converted shadertoy shaders on Mesa and reads back rendered pixels.

Note: Zig's shadertoy Uniforms order the selection background/foreground
colors opposite to the GLSL block (likely an upstream bug); kept as in
Zig for parity and recorded in the notes.

Deferred to the next renderer pass: generic, cell, image, link, row,
State, Thread, Overlay (need terminal render state).

Test Plan:
- cargo test -p ghostty-renderer (33 tests incl. tests/egl_headless.rs)
This commit is contained in:
ddidderr committed 2026-10-11 22:26:09 +02:00
1 parent ac0e05def1
commit c548e63bd1
35 files changed
+5477 -5

No files matched your search

@@ -0,0 +1,13 @@
#include "common.glsl"
// Must declare this output for some versions of OpenGL.
layout(location = 0) out vec4 out_FragColor;
void main() {
bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0;
out_FragColor = load_color(
unpack4u8(bg_color_packed_4u8),
use_linear_blending
);
}
@@ -0,0 +1,63 @@
#include "common.glsl"
// Position the FragCoord origin to the upper left
// so as to align with our texture's directionality.
layout(origin_upper_left) in vec4 gl_FragCoord;
layout(binding = 0) uniform sampler2D image;
flat in vec4 bg_color;
flat in vec2 offset;
flat in vec2 scale;
flat in float opacity;
flat in uint repeat;
layout(location = 0) out vec4 out_FragColor;
void main() {
bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0;
// Our texture coordinate is based on the screen position, offset by the
// dest rect origin, and scaled by the ratio between the dest rect size
// and the original texture size, which effectively scales the original
// size of the texture to the dest rect size.
vec2 tex_coord = (gl_FragCoord.xy - offset) * scale;
vec2 tex_size = textureSize(image, 0);
// If we need to repeat the texture, wrap the coordinates.
if (repeat != 0) {
tex_coord = mod(mod(tex_coord, tex_size) + tex_size, tex_size);
}
vec4 rgba;
// If we're out of bounds, we have no color,
// otherwise we sample the texture for it.
if (any(lessThan(tex_coord, vec2(0.0))) ||
any(greaterThan(tex_coord, tex_size)))
{
rgba = vec4(0.0);
} else {
// We divide by the texture size to normalize for sampling.
rgba = texture(image, tex_coord / tex_size);
if (!use_linear_blending) {
rgba = unlinearize(rgba);
}
rgba.rgb *= rgba.a;
}
// Multiply it by the configured opacity, but cap it at
// the value that will make it fully opaque relative to
// the background color alpha, so it isn't overexposed.
rgba *= min(opacity, 1.0 / bg_color.a);
// Blend it on to a fully opaque version of the background color.
rgba += max(vec4(0.0), vec4(bg_color.rgb, 1.0) * vec4(1.0 - rgba.a));
// Multiply everything by the background color alpha.
rgba *= bg_color.a;
out_FragColor = rgba;
}
@@ -0,0 +1,145 @@
#include "common.glsl"
layout(binding = 0) uniform sampler2D image;
layout(location = 0) in float in_opacity;
layout(location = 1) in uint info;
// 4 bits of info.
const uint BG_IMAGE_POSITION = 15u;
const uint BG_IMAGE_TL = 0u;
const uint BG_IMAGE_TC = 1u;
const uint BG_IMAGE_TR = 2u;
const uint BG_IMAGE_ML = 3u;
const uint BG_IMAGE_MC = 4u;
const uint BG_IMAGE_MR = 5u;
const uint BG_IMAGE_BL = 6u;
const uint BG_IMAGE_BC = 7u;
const uint BG_IMAGE_BR = 8u;
// 2 bits of info shifted 4.
const uint BG_IMAGE_FIT = 3u << 4;
const uint BG_IMAGE_CONTAIN = 0u << 4;
const uint BG_IMAGE_COVER = 1u << 4;
const uint BG_IMAGE_STRETCH = 2u << 4;
const uint BG_IMAGE_NO_FIT = 3u << 4;
// 1 bit of info shifted 6.
const uint BG_IMAGE_REPEAT = 1u << 6;
flat out vec4 bg_color;
flat out vec2 offset;
flat out vec2 scale;
flat out float opacity;
// We use a uint to pass the repeat value because
// bools aren't allowed for vertex outputs in OpenGL.
flat out uint repeat;
void main() {
bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0;
vec4 position;
position.x = (gl_VertexID == 2) ? 3.0 : -1.0;
position.y = (gl_VertexID == 0) ? -3.0 : 1.0;
position.z = 1.0;
position.w = 1.0;
// Single triangle is clipped to viewport.
//
// X <- vid == 0: (-1, -3)
// |\
// | \
// | \
// |###\
// |#+# \ `+` is (0, 0). `#`s are viewport area.
// |### \
// X------X <- vid == 2: (3, 1)
// ^
// vid == 1: (-1, 1)
gl_Position = position;
opacity = in_opacity;
repeat = info & BG_IMAGE_REPEAT;
vec2 screen_size = screen_size;
vec2 tex_size = textureSize(image, 0);
vec2 dest_size = tex_size;
switch (info & BG_IMAGE_FIT) {
// For `contain` we scale by a factor that makes the image
// width match the screen width or makes the image height
// match the screen height, whichever is smaller.
case BG_IMAGE_CONTAIN: {
float scale = min(screen_size.x / tex_size.x, screen_size.y / tex_size.y);
dest_size = tex_size * scale;
} break;
// For `cover` we scale by a factor that makes the image
// width match the screen width or makes the image height
// match the screen height, whichever is larger.
case BG_IMAGE_COVER: {
float scale = max(screen_size.x / tex_size.x, screen_size.y / tex_size.y);
dest_size = tex_size * scale;
} break;
// For `stretch` we stretch the image to the size of
// the screen without worrying about aspect ratio.
case BG_IMAGE_STRETCH: {
dest_size = screen_size;
} break;
// For `none` we just use the original texture size.
case BG_IMAGE_NO_FIT: {
dest_size = tex_size;
} break;
}
vec2 start = vec2(0.0);
vec2 mid = (screen_size - dest_size) / vec2(2.0);
vec2 end = screen_size - dest_size;
vec2 dest_offset = mid;
switch (info & BG_IMAGE_POSITION) {
case BG_IMAGE_TL: {
dest_offset = vec2(start.x, start.y);
} break;
case BG_IMAGE_TC: {
dest_offset = vec2(mid.x, start.y);
} break;
case BG_IMAGE_TR: {
dest_offset = vec2(end.x, start.y);
} break;
case BG_IMAGE_ML: {
dest_offset = vec2(start.x, mid.y);
} break;
case BG_IMAGE_MC: {
dest_offset = vec2(mid.x, mid.y);
} break;
case BG_IMAGE_MR: {
dest_offset = vec2(end.x, mid.y);
} break;
case BG_IMAGE_BL: {
dest_offset = vec2(start.x, end.y);
} break;
case BG_IMAGE_BC: {
dest_offset = vec2(mid.x, end.y);
} break;
case BG_IMAGE_BR: {
dest_offset = vec2(end.x, end.y);
} break;
}
offset = dest_offset;
scale = tex_size / dest_size;
// We load a fully opaque version of the bg color and combine it with
// the alpha separately, because we need these as separate values in
// the framgment shader.
uvec4 u_bg_color = unpack4u8(bg_color_packed_4u8);
bg_color = vec4(load_color(
uvec4(u_bg_color.rgb, 255),
use_linear_blending
).rgb, float(u_bg_color.a) / 255.0);
}
@@ -0,0 +1,61 @@
#include "common.glsl"
// Position the origin to the upper left
layout(origin_upper_left) in vec4 gl_FragCoord;
// Must declare this output for some versions of OpenGL.
layout(location = 0) out vec4 out_FragColor;
layout(binding = 1, std430) readonly buffer bg_cells {
uint cells[];
};
vec4 cell_bg() {
uvec2 grid_size = unpack2u16(grid_size_packed_2u16);
ivec2 grid_pos = ivec2(floor((gl_FragCoord.xy - grid_padding.wx) / cell_size));
bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0;
vec4 bg = vec4(0.0);
// Clamp x position, extends edge bg colors in to padding on sides.
if (grid_pos.x < 0) {
if ((padding_extend & EXTEND_LEFT) != 0) {
grid_pos.x = 0;
} else {
return bg;
}
} else if (grid_pos.x > grid_size.x - 1) {
if ((padding_extend & EXTEND_RIGHT) != 0) {
grid_pos.x = int(grid_size.x) - 1;
} else {
return bg;
}
}
// Clamp y position if we should extend, otherwise discard if out of bounds.
if (grid_pos.y < 0) {
if ((padding_extend & EXTEND_UP) != 0) {
grid_pos.y = 0;
} else {
return bg;
}
} else if (grid_pos.y > grid_size.y - 1) {
if ((padding_extend & EXTEND_DOWN) != 0) {
grid_pos.y = int(grid_size.y) - 1;
} else {
return bg;
}
}
// Load the color for the cell.
vec4 cell_color = load_color(
unpack4u8(cells[grid_pos.y * grid_size.x + grid_pos.x]),
use_linear_blending
);
return cell_color;
}
void main() {
out_FragColor = cell_bg();
}
@@ -0,0 +1,100 @@
#include "common.glsl"
layout(binding = 0) uniform sampler2DRect atlas_grayscale;
layout(binding = 1) uniform sampler2DRect atlas_color;
in CellTextVertexOut {
flat uint atlas;
flat vec4 color;
flat vec4 bg_color;
vec2 tex_coord;
} in_data;
// Values `atlas` can take.
const uint ATLAS_GRAYSCALE = 0u;
const uint ATLAS_COLOR = 1u;
// Must declare this output for some versions of OpenGL.
layout(location = 0) out vec4 out_FragColor;
void main() {
bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0;
bool use_linear_correction = (bools & USE_LINEAR_CORRECTION) != 0;
switch (in_data.atlas) {
default:
case ATLAS_GRAYSCALE:
{
// Our input color is always linear.
vec4 color = in_data.color;
// If we're not doing linear blending, then we need to
// re-apply the gamma encoding to our color manually.
//
// Since the alpha is premultiplied, we need to divide
// it out before unlinearizing and re-multiply it after.
if (!use_linear_blending) {
color.rgb /= vec3(color.a);
color = unlinearize(color);
color.rgb *= vec3(color.a);
}
// Fetch our alpha mask for this pixel.
float a = texture(atlas_grayscale, in_data.tex_coord).r;
// Linear blending weight correction corrects the alpha value to
// produce blending results which match gamma-incorrect blending.
if (use_linear_correction) {
// Short explanation of how this works:
//
// We get the luminances of the foreground and background colors,
// and then unlinearize them and perform blending on them. This
// gives us our desired luminance, which we derive our new alpha
// value from by mapping the range [bg_l, fg_l] to [0, 1], since
// our final blend will be a linear interpolation from bg to fg.
//
// This yields virtually identical results for grayscale blending,
// and very similar but non-identical results for color blending.
vec4 bg = in_data.bg_color;
float fg_l = luminance(color.rgb);
float bg_l = luminance(bg.rgb);
// To avoid numbers going haywire, we don't apply correction
// when the bg and fg luminances are within 0.001 of each other.
if (abs(fg_l - bg_l) > 0.001) {
float blend_l = linearize(unlinearize(fg_l) * a + unlinearize(bg_l) * (1.0 - a));
a = clamp((blend_l - bg_l) / (fg_l - bg_l), 0.0, 1.0);
}
}
// Multiply our whole color by the alpha mask.
// Since we use premultiplied alpha, this is
// the correct way to apply the mask.
color *= a;
out_FragColor = color;
return;
}
case ATLAS_COLOR:
{
// For now, we assume that color glyphs
// are already premultiplied linear colors.
vec4 color = texture(atlas_color, in_data.tex_coord);
// If we are doing linear blending, we can return this right away.
if (use_linear_blending) {
out_FragColor = color;
return;
}
// Otherwise we need to unlinearize the color. Since the alpha is
// premultiplied, we need to divide it out before unlinearizing.
color.rgb /= vec3(color.a);
color = unlinearize(color);
color.rgb *= vec3(color.a);
out_FragColor = color;
return;
}
}
}
@@ -0,0 +1,146 @@
#include "common.glsl"
// The position of the glyph in the texture (x, y)
layout(location = 0) in uvec2 glyph_pos;
// The size of the glyph in the texture (w, h)
layout(location = 1) in uvec2 glyph_size;
// The left and top bearings for the glyph (x, y)
layout(location = 2) in ivec2 bearings;
// The grid coordinates (x, y) where x < columns and y < rows
layout(location = 3) in uvec2 grid_pos;
// The color of the rendered text glyph.
layout(location = 4) in uvec4 color;
// Which atlas this glyph is in.
layout(location = 5) in uint atlas;
// Misc glyph properties.
layout(location = 6) in uint glyph_bools;
// Values `atlas` can take.
const uint ATLAS_GRAYSCALE = 0u;
const uint ATLAS_COLOR = 1u;
// Masks for the `glyph_bools` attribute
const uint NO_MIN_CONTRAST = 1u;
const uint IS_CURSOR_GLYPH = 2u;
out CellTextVertexOut {
flat uint atlas;
flat vec4 color;
flat vec4 bg_color;
vec2 tex_coord;
} out_data;
layout(binding = 1, std430) readonly buffer bg_cells {
uint bg_colors[];
};
void main() {
uvec2 grid_size = unpack2u16(grid_size_packed_2u16);
uvec2 cursor_pos = unpack2u16(cursor_pos_packed_2u16);
bool cursor_wide = (bools & CURSOR_WIDE) != 0;
bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0;
// Convert the grid x, y into world space x, y by accounting for cell size
vec2 cell_pos = cell_size * vec2(grid_pos);
int vid = gl_VertexID;
// We use a triangle strip with 4 vertices to render quads,
// so we determine which corner of the cell this vertex is in
// based on the vertex ID.
//
// 0 --> 1
// | .'|
// | / |
// | L |
// 2 --> 3
//
// 0 = top-left (0, 0)
// 1 = top-right (1, 0)
// 2 = bot-left (0, 1)
// 3 = bot-right (1, 1)
vec2 corner;
corner.x = float(vid == 1 || vid == 3);
corner.y = float(vid == 2 || vid == 3);
out_data.atlas = atlas;
// === Grid Cell ===
// +X
// 0,0--...->
// |
// . offset.x = bearings.x
// +Y. .|.
// . | |
// | cell_pos -> +-------+ _.
// v ._| |_. _|- offset.y = cell_size.y - bearings.y
// | | .###. | |
// | | #...# | |
// glyph_size.y -+ | ##### | |
// | | #.... | +- bearings.y
// |_| .#### | |
// | |_|
// +-------+
// |_._|
// |
// glyph_size.x
//
// In order to get the top left of the glyph, we compute an offset based on
// the bearings. The Y bearing is the distance from the bottom of the cell
// to the top of the glyph, so we subtract it from the cell height to get
// the y offset. The X bearing is the distance from the left of the cell
// to the left of the glyph, so it works as the x offset directly.
vec2 size = vec2(glyph_size);
vec2 offset = vec2(bearings);
offset.y = cell_size.y - offset.y;
// Calculate the final position of the cell which uses our glyph size
// and glyph offset to create the correct bounding box for the glyph.
cell_pos = cell_pos + size * corner + offset;
gl_Position = projection_matrix * vec4(cell_pos.x, cell_pos.y, 0.0f, 1.0f);
// Calculate the texture coordinate in pixels. This is NOT normalized
// (between 0.0 and 1.0), and does not need to be, since the texture will
// be sampled with pixel coordinate mode.
out_data.tex_coord = vec2(glyph_pos) + vec2(glyph_size) * corner;
// Get our color. We always fetch a linearized version to
// make it easier to handle minimum contrast calculations.
out_data.color = load_color(color, true);
// Get the BG color
out_data.bg_color = load_color(
unpack4u8(bg_colors[grid_pos.y * grid_size.x + grid_pos.x]),
true
);
// Blend it with the global bg color
vec4 global_bg = load_color(
unpack4u8(bg_color_packed_4u8),
true
);
out_data.bg_color += global_bg * vec4(1.0 - out_data.bg_color.a);
// If we have a minimum contrast, we need to check if we need to
// change the color of the text to ensure it has enough contrast
// with the background.
if (min_contrast > 1.0f && (glyph_bools & NO_MIN_CONTRAST) == 0) {
// Ensure our minimum contrast
out_data.color = contrasted_color(min_contrast, out_data.color, out_data.bg_color);
}
// Check if current position is under cursor (including wide cursor)
bool is_cursor_pos = ((grid_pos.x == cursor_pos.x) || (cursor_wide && (grid_pos.x == (cursor_pos.x + 1)))) && (grid_pos.y == cursor_pos.y);
// If this cell is the cursor cell, but we're not processing
// the cursor glyph itself, then we need to change the color.
if ((glyph_bools & IS_CURSOR_GLYPH) == 0 && is_cursor_pos) {
out_data.color = load_color(unpack4u8(cursor_color_packed_4u8), use_linear_blending);
}
}
@@ -0,0 +1,156 @@
#version 430 core
// These are common definitions to be shared across shaders, the first
// line of any shader that needs these should be `#include "common.glsl"`.
//
// Included in this file are:
// - The interface block for the global uniforms.
// - Functions for unpacking values.
// - Functions for working with colors.
//----------------------------------------------------------------------------//
// Global Uniforms
//----------------------------------------------------------------------------//
layout(binding = 1, std140) uniform Globals {
uniform mat4 projection_matrix;
uniform vec2 screen_size;
uniform vec2 cell_size;
uniform uint grid_size_packed_2u16;
uniform vec4 grid_padding;
uniform uint padding_extend;
uniform float min_contrast;
uniform uint cursor_pos_packed_2u16;
uniform uint cursor_color_packed_4u8;
uniform uint bg_color_packed_4u8;
uniform uint bools;
};
// Bools
const uint CURSOR_WIDE = 1u;
const uint USE_DISPLAY_P3 = 2u;
const uint USE_LINEAR_BLENDING = 4u;
const uint USE_LINEAR_CORRECTION = 8u;
// Padding extend enum
const uint EXTEND_LEFT = 1u;
const uint EXTEND_RIGHT = 2u;
const uint EXTEND_UP = 4u;
const uint EXTEND_DOWN = 8u;
//----------------------------------------------------------------------------//
// Functions for Unpacking Values
//----------------------------------------------------------------------------//
// NOTE: These unpack functions assume little-endian.
// If this ever becomes a problem... oh dear!
uvec4 unpack4u8(uint packed_value) {
return uvec4(
uint(packed_value >> 0) & uint(0xFF),
uint(packed_value >> 8) & uint(0xFF),
uint(packed_value >> 16) & uint(0xFF),
uint(packed_value >> 24) & uint(0xFF)
);
}
uvec2 unpack2u16(uint packed_value) {
return uvec2(
uint(packed_value >> 0) & uint(0xFFFF),
uint(packed_value >> 16) & uint(0xFFFF)
);
}
ivec2 unpack2i16(int packed_value) {
return ivec2(
(packed_value << 16) >> 16,
(packed_value << 0) >> 16
);
}
//----------------------------------------------------------------------------//
// Color Functions
//----------------------------------------------------------------------------//
// Compute the luminance of the provided color.
//
// Takes colors in linear RGB space. If your colors are gamma
// encoded, linearize them before using them with this function.
float luminance(vec3 color) {
return dot(color, vec3(0.2126f, 0.7152f, 0.0722f));
}
// https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
//
// Takes colors in linear RGB space. If your colors are gamma
// encoded, linearize them before using them with this function.
float contrast_ratio(vec3 color1, vec3 color2) {
float luminance1 = luminance(color1) + 0.05;
float luminance2 = luminance(color2) + 0.05;
return max(luminance1, luminance2) / min(luminance1, luminance2);
}
// Return the fg if the contrast ratio is greater than min, otherwise
// return a color that satisfies the contrast ratio. Currently, the color
// is always white or black, whichever has the highest contrast ratio.
//
// Takes colors in linear RGB space. If your colors are gamma
// encoded, linearize them before using them with this function.
vec4 contrasted_color(float min_ratio, vec4 fg, vec4 bg) {
float ratio = contrast_ratio(fg.rgb, bg.rgb);
if (ratio < min_ratio) {
float white_ratio = contrast_ratio(vec3(1.0, 1.0, 1.0), bg.rgb);
float black_ratio = contrast_ratio(vec3(0.0, 0.0, 0.0), bg.rgb);
if (white_ratio > black_ratio) {
return vec4(1.0);
} else {
return vec4(0.0, 0.0, 0.0, 1.0);
}
}
return fg;
}
// Converts a color from sRGB gamma encoding to linear.
vec4 linearize(vec4 srgb) {
bvec3 cutoff = lessThanEqual(srgb.rgb, vec3(0.04045));
vec3 higher = pow((srgb.rgb + vec3(0.055)) / vec3(1.055), vec3(2.4));
vec3 lower = srgb.rgb / vec3(12.92);
return vec4(mix(higher, lower, cutoff), srgb.a);
}
float linearize(float v) {
return v <= 0.04045 ? v / 12.92 : pow((v + 0.055) / 1.055, 2.4);
}
// Converts a color from linear to sRGB gamma encoding.
vec4 unlinearize(vec4 linear) {
bvec3 cutoff = lessThanEqual(linear.rgb, vec3(0.0031308));
vec3 higher = pow(linear.rgb, vec3(1.0 / 2.4)) * vec3(1.055) - vec3(0.055);
vec3 lower = linear.rgb * vec3(12.92);
return vec4(mix(higher, lower, cutoff), linear.a);
}
float unlinearize(float v) {
return v <= 0.0031308 ? v * 12.92 : pow(v, 1.0 / 2.4) * 1.055 - 0.055;
}
// Load a 4 byte RGBA non-premultiplied color and linearize
// and convert it as necessary depending on the provided info.
//
// `linear` controls whether the returned color is linear or gamma encoded.
vec4 load_color(
uvec4 in_color,
bool linear
) {
// 0 .. 255 -> 0.0 .. 1.0
vec4 color = vec4(in_color) / vec4(255.0f);
// Linearize if necessary.
if (linear) color = linearize(color);
// Premultiply our color by its alpha.
color.rgb *= color.a;
return color;
}
//----------------------------------------------------------------------------//
@@ -0,0 +1,24 @@
#version 330 core
void main() {
vec4 position;
position.x = (gl_VertexID == 2) ? 3.0 : -1.0;
position.y = (gl_VertexID == 0) ? -3.0 : 1.0;
position.z = 1.0;
position.w = 1.0;
// Single triangle is clipped to viewport.
//
// X <- vid == 0: (-1, -3)
// |\
// | \
// | \
// |###\
// |#+# \ `+` is (0, 0). `#`s are viewport area.
// |### \
// X------X <- vid == 2: (3, 1)
// ^
// vid == 1: (-1, 1)
gl_Position = position;
}
@@ -0,0 +1,21 @@
#include "common.glsl"
layout(binding = 0) uniform sampler2D image;
in vec2 tex_coord;
layout(location = 0) out vec4 out_FragColor;
void main() {
bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0;
vec4 rgba = texture(image, tex_coord);
if (!use_linear_blending) {
rgba = unlinearize(rgba);
}
rgba.rgb *= vec3(rgba.a);
out_FragColor = rgba;
}
@@ -0,0 +1,47 @@
#include "common.glsl"
layout(binding = 0) uniform sampler2D image;
layout(location = 0) in vec2 grid_pos;
layout(location = 1) in vec2 cell_offset;
layout(location = 2) in vec4 source_rect;
layout(location = 3) in vec2 dest_size;
out vec2 tex_coord;
void main() {
int vid = gl_VertexID;
// We use a triangle strip with 4 vertices to render quads,
// so we determine which corner of the cell this vertex is in
// based on the vertex ID.
//
// 0 --> 1
// | .'|
// | / |
// | L |
// 2 --> 3
//
// 0 = top-left (0, 0)
// 1 = top-right (1, 0)
// 2 = bot-left (0, 1)
// 3 = bot-right (1, 1)
vec2 corner;
corner.x = float(vid == 1 || vid == 3);
corner.y = float(vid == 2 || vid == 3);
// The texture coordinates start at our source x/y
// and add the width/height depending on the corner.
tex_coord = source_rect.xy;
tex_coord += source_rect.zw * corner;
// Normalize the coordinates.
tex_coord /= textureSize(image, 0);
// The position of our image starts at the top-left of the grid cell and
// adds the source rect width/height components.
vec2 image_pos = (cell_size * grid_pos) + cell_offset;
image_pos += dest_size * corner;
gl_Position = projection_matrix * vec4(image_pos.xy, 1.0, 1.0);
}