glterm: one vertex stream for glyphs and fills, clipped on the CPU

Solid rectangles now join the glyph batch instead of flushing it: the
atlas reserves a 4x4 white block whose center texel gives coverage 1.0
(the gamma curve passes it unchanged), so a fill is just a quad with
constant texture coordinates in the same program, same texture and same
submission-ordered stream as the glyphs around it.  Backgrounds,
underlines, boxes and reliefs no longer split the batch, and the atlas
reset paths are centralized in gl_atlas_reset, which re-reserves the
white block.

Clipping moves from the GL scissor to queue time: each quad is clamped
against the glyph string's clip rect (the same integer bounds the
scissor used, with texture coordinates adjusted proportionally), so a
clip change no longer forces a flush either, and the batch survives
across strings and rows.  A full-frame redraw drops from one draw call
per background/glyph-run pair (hundreds) to a handful per frame.  The
non-batched primitives (image textures, fringe bitmaps) apply the
recorded clip as a real scissor right before their own draw.

Pixel parity re-verified across the text, decorations, cursor, scroll,
image/fringe and emoji batteries (byte-identical to the previous driver
in the deterministic cases), and the on-screen present still matches
the FBO exactly after a partial-present session.  Full-frame redraw
goes from 192 to 290 fps on the AMD Renoir test machine, overtaking
the cairo backend (1.15x) on that workload for the first time;
page scroll reaches 0.95x, line scroll 0.86x.
This commit is contained in:
Andros Fenollosa 2026-06-11 16:34:43 +02:00
parent 9a1674d447
commit 51d51ace75

View file

@ -217,6 +217,9 @@ static int g_glyph_batch_verts; /* vertices queued */
static int g_glyph_batch_cap; /* capacity in vertices */
static struct gl_frame_data *g_glyph_batch_fd; /* frame the quads target */
static void gl_flush_glyph_batch (void);
static void gl_batch_append (struct gl_frame_data *fd, float x0, float y0,
float x1, float y1, float u0, float v0,
float u1, float v1, const float rgba[4]);
#ifdef HAVE_GSTREAMER
static void gl_video_overlay (struct gl_frame_data *fd, int sw, int sh);
static void gl_video_free (struct gl_frame_data *fd);
@ -288,15 +291,40 @@ glyph_cache_lookup (unsigned long long key)
return NULL;
}
/* Reset the atlas packing and the glyph table, then re-reserve the 4x4
white block at (0,0) that batched solid rects sample (the rect quads
share the glyph program: a coverage of 1.0 passes the gamma curve
unchanged, so one program and one texture cover glyphs and fills and
the two never split the batch). Any queued quads still reference the
old layout, so they are flushed first. */
static void
gl_atlas_reset (void)
{
gl_flush_glyph_batch ();
memset (g_glyphs, 0, sizeof g_glyphs);
g_glyph_count = 0;
static const unsigned char white[16] = {
255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255,
};
glBindTexture (GL_TEXTURE_2D, g_atlas);
glPixelStorei (GL_UNPACK_ALIGNMENT, 1);
glTexSubImage2D (GL_TEXTURE_2D, 0, 0, 0, 4, 4, GL_RED,
GL_UNSIGNED_BYTE, white);
g_atlas_next_x = 5;
g_atlas_next_y = 0;
g_atlas_row_h = 4;
}
/* Texture coordinates of the white block's center texel. */
#define GL_WHITE_U (2.0f / GL_ATLAS_W)
#define GL_WHITE_V (2.0f / GL_ATLAS_H)
static struct gfx_glyph *
glyph_cache_insert (unsigned long long key)
{
if (g_glyph_count * 4 >= GL_GLYPH_CAP * 3) /* >75% full: reset atlas */
{
memset (g_glyphs, 0, sizeof g_glyphs);
g_glyph_count = 0;
g_atlas_next_x = g_atlas_next_y = g_atlas_row_h = 0;
}
gl_atlas_reset ();
unsigned h = (unsigned) (key % GL_GLYPH_CAP);
for (int i = 0; i < GL_GLYPH_CAP; i++)
{
@ -626,6 +654,7 @@ gl_global_init (void)
glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
gl_atlas_reset (); /* reserve the white block for rect quads */
glPixelStorei (GL_UNPACK_ALIGNMENT, 1);
glEnable (GL_BLEND);
@ -706,13 +735,13 @@ gl_mark_dirty (struct gl_frame_data *fd, double x, double y,
gl_dirty_add (&fd->dirty, px0, fy0, px1, fy1);
}
/* Scissor in the FBO's bottom-left space, from a top-left logical rect. */
/* Apply FD's recorded clip rect as the GL scissor, in the FBO's
bottom-left space. Only the non-batched primitives (image textures,
fringe bitmaps) call this, right before their draw; batched quads are
clipped on the CPU at queue time instead (gl_batch_append). */
static void
gl_apply_scissor (struct gl_frame_data *fd)
gl_scissor_apply_now (struct gl_frame_data *fd)
{
/* Queued glyphs were drawn under the current scissor; emit them before
it changes. */
gl_flush_glyph_batch ();
if (!fd->clip_on)
{
glDisable (GL_SCISSOR_TEST);
@ -806,15 +835,9 @@ gl_rasterize_glyph (struct font *font, unsigned int glyph_id,
g_atlas_row_h = 0;
}
if (g_atlas_next_y + bh > GL_ATLAS_H)
{
/* The atlas is full and about to be repacked from the top, which
overwrites cells that queued glyph quads still reference. Emit
the pending batch first so it samples the old layout. */
gl_flush_glyph_batch ();
g_atlas_next_x = g_atlas_next_y = g_atlas_row_h = 0;
memset (g_glyphs, 0, sizeof g_glyphs);
g_glyph_count = 0;
}
/* Atlas full: repack from the top (gl_atlas_reset flushes the queued
quads that still reference the old layout). */
gl_atlas_reset ();
glBindTexture (GL_TEXTURE_2D, g_atlas);
glPixelStorei (GL_UNPACK_ALIGNMENT, 1);
@ -838,32 +861,6 @@ gl_rasterize_glyph (struct font *font, unsigned int glyph_id,
return e;
}
/* ------------------------------------------------------------------ */
/* Small immediate-mode draw helpers. */
static void
gl_draw_rect_px (struct gl_frame_data *fd, float x, float y, float w,
float h, const float rgba[4])
{
gl_mark_dirty (fd, x, y, w, h);
float s = (float) fd->scale;
float x0 = x * s, y0 = y * s, x1 = (x + w) * s, y1 = (y + h) * s;
float r = rgba[0], gg = rgba[1], b = rgba[2], a = rgba[3];
float v[] = {
x0,y0, 0,0, r,gg,b,a, x1,y0, 0,0, r,gg,b,a, x1,y1, 0,0, r,gg,b,a,
x0,y0, 0,0, r,gg,b,a, x1,y1, 0,0, r,gg,b,a, x0,y1, 0,0, r,gg,b,a,
};
glUseProgram (g_prog_rect);
glUniform2f (g_u_rect_size, (float) fd->w, (float) fd->h);
glBindBuffer (GL_ARRAY_BUFFER, g_vbo);
glBufferData (GL_ARRAY_BUFFER, sizeof v, v, GL_STREAM_DRAW);
glEnableVertexAttribArray (0);
glVertexAttribPointer (0, 2, GL_FLOAT, GL_FALSE, 32, (void *) 0);
glEnableVertexAttribArray (2);
glVertexAttribPointer (2, 4, GL_FLOAT, GL_FALSE, 32, (void *) 16);
glDrawArrays (GL_TRIANGLES, 0, 6);
}
/* ------------------------------------------------------------------ */
/* Vtable ops. */
@ -935,6 +932,11 @@ gl_present_to_window (struct gl_frame_data *fd)
Window win = FRAME_X_WINDOW (f);
if (!win) return;
/* Quads queued for this frame target its FBO; emit them before the
draw framebuffer switches to the window. */
if (g_glyph_batch_fd == fd)
gl_flush_glyph_batch ();
/* Create (or recreate, if the window id changed) the window surface. */
if (fd->surf == EGL_NO_SURFACE || fd->surf_win != (unsigned long) win)
{
@ -1168,6 +1170,11 @@ gl_drv_pending_present (struct frame *f)
return fd && fd->needs_present;
}
/* The clip ops only record state: batched quads are clipped on the CPU
at queue time (gl_batch_append), and the non-batched primitives apply
the recorded rect as a real scissor right before they draw
(gl_scissor_apply_now). Neither needs the batch flushed. */
static void
gl_drv_clip_to_glyph_string (struct glyph_string *s)
{
@ -1180,7 +1187,6 @@ gl_drv_clip_to_glyph_string (struct glyph_string *s)
fd->clip_y = r.y;
fd->clip_w = r.width;
fd->clip_h = r.height;
gl_apply_scissor (fd);
}
static void
@ -1188,9 +1194,7 @@ gl_drv_clear_clip (struct frame *f)
{
struct gl_frame_data *fd = gl_get_frame_data (f);
if (!fd) return;
gl_flush_glyph_batch (); /* glyphs queued under the current clip */
fd->clip_on = false;
glDisable (GL_SCISSOR_TEST);
}
static void
@ -1198,11 +1202,15 @@ gl_drv_fill_rect (struct frame *f, int x, int y, int w, int h,
unsigned long color)
{
struct gl_frame_data *fd = gl_get_frame_data (f);
if (!fd) return;
gl_flush_glyph_batch (); /* keep submission order vs queued glyphs */
if (!fd || w <= 0 || h <= 0) return;
/* A solid rect is a quad sampling the atlas' white block: coverage 1.0
passes the gamma curve unchanged, so it joins the glyph batch with
no program switch and no flush. */
float s = (float) fd->scale;
float rgba[4];
gl_unpack_color (color, rgba);
gl_draw_rect_px (fd, x, y, w, h, rgba);
gl_batch_append (fd, x * s, y * s, (x + w) * s, (y + h) * s,
GL_WHITE_U, GL_WHITE_V, GL_WHITE_U, GL_WHITE_V, rgba);
}
static void
@ -1234,7 +1242,6 @@ gl_drv_copy_region (struct frame *f, int x, int y, int w, int h,
dx, dy0, dx + sw, dy0 + sh,
GL_COLOR_BUFFER_BIT, GL_NEAREST);
glBindFramebuffer (GL_FRAMEBUFFER, fd->fbo);
gl_apply_scissor (fd);
}
static bool
@ -1257,15 +1264,18 @@ gl_drv_get_glyph (struct font *font, unsigned int glyph_id)
return gl_rasterize_glyph (font, glyph_id, key);
}
/* Emit the queued glyph quads as a single draw call (or nothing when the
batch is empty). The scissor and FBO in effect now are the ones that
were active while these glyphs were queued, since every state change
flushes first. */
/* Emit the queued quads -- glyphs and solid rects share the program, the
atlas and one submission-ordered vertex stream -- as a single draw
call. Every quad was clipped on the CPU against the clip rect in
effect when it was queued (gl_batch_append), so the draw itself runs
scissor-free; the non-batched primitives re-apply the scissor
themselves (gl_scissor_apply_now). */
static void
gl_flush_glyph_batch (void)
{
if (g_glyph_batch_verts == 0 || !g_glyph_batch_fd) return;
struct gl_frame_data *fd = g_glyph_batch_fd;
glDisable (GL_SCISSOR_TEST); /* quads are pre-clipped */
glUseProgram (g_prog_glyph);
glUniform2f (g_u_glyph_size, (float) fd->w, (float) fd->h);
glActiveTexture (GL_TEXTURE0);
@ -1286,35 +1296,52 @@ gl_flush_glyph_batch (void)
g_glyph_batch_verts = 0;
}
/* Queue one textured quad (physical pixel coords, top-left origin) into
the shared batch. The quad is clipped here on the CPU against FD's
current clip rect -- computed with the same integer casts the GL
scissor used, so the pixel result is identical -- with the texture
coordinates adjusted proportionally. Clipping at queue time is what
lets the batch run scissor-free and survive across glyph strings: a
full-frame redraw becomes a handful of draw calls. */
static void
gl_drv_draw_glyph (struct frame *f, struct gfx_glyph *g,
float x, float ybase, unsigned long color)
gl_batch_append (struct gl_frame_data *fd, float x0, float y0,
float x1, float y1, float u0, float v0,
float u1, float v1, const float rgba[4])
{
struct gl_frame_data *fd = gl_get_frame_data (f);
if (!fd || !g || g->width <= 0 || g->height <= 0) return;
/* A batch targets one frame's FBO; if the frame changed, flush first. */
if (g_glyph_batch_fd && g_glyph_batch_fd != fd)
gl_flush_glyph_batch ();
g_glyph_batch_fd = fd;
float s = (float) fd->scale;
/* Mark the glyph's logical box dirty (bearings/size are physical px). */
gl_mark_dirty (fd, x + g->bearing_x / s, ybase - g->bearing_y / s,
g->width / s, g->height / s);
/* Stored metrics are physical pixels; draw site is logical, so scale. */
float x0 = (x + g->bearing_x / s) * s;
float y0 = (ybase - g->bearing_y / s) * s;
float x1 = x0 + g->width;
float y1 = y0 + g->height;
float u0 = (float) g->atlas_x / GL_ATLAS_W;
float v0 = (float) g->atlas_y / GL_ATLAS_H;
float u1 = (float) (g->atlas_x + g->width) / GL_ATLAS_W;
float v1 = (float) (g->atlas_y + g->height) / GL_ATLAS_H;
float rgba[4];
gl_unpack_color (color, rgba);
float r = rgba[0], gg = rgba[1], b = rgba[2], a = rgba[3];
if (fd->clip_on)
{
double s = fd->scale;
float cx0 = (float) (int) (fd->clip_x * s);
float cy0 = (float) (int) (fd->clip_y * s);
float cx1 = cx0 + (float) (int) (fd->clip_w * s);
float cy1 = cy0 + (float) (int) (fd->clip_h * s);
if (x0 >= cx1 || x1 <= cx0 || y0 >= cy1 || y1 <= cy0)
return;
float du = (u1 - u0) / (x1 - x0), dv = (v1 - v0) / (y1 - y0);
if (x0 < cx0) { u0 += du * (cx0 - x0); x0 = cx0; }
if (x1 > cx1) { u1 -= du * (x1 - cx1); x1 = cx1; }
if (y0 < cy0) { v0 += dv * (cy0 - y0); y0 = cy0; }
if (y1 > cy1) { v1 -= dv * (y1 - cy1); y1 = cy1; }
}
/* Grow the CPU-side vertex buffer if needed (6 vertices per glyph). */
/* Mark the clipped box dirty (top-left physical -> bottom-left FBO). */
{
int px0 = (int) floorf (x0), px1 = (int) ceilf (x1);
int fy0 = fd->h - (int) ceilf (y1);
int fy1 = fd->h - (int) floorf (y0);
if (px0 < 0) px0 = 0;
if (fy0 < 0) fy0 = 0;
if (px1 > fd->w) px1 = fd->w;
if (fy1 > fd->h) fy1 = fd->h;
gl_dirty_add (&fd->dirty, px0, fy0, px1, fy1);
}
/* Grow the CPU-side vertex buffer if needed (6 vertices per quad). */
if (g_glyph_batch_verts + 6 > g_glyph_batch_cap)
{
int cap = g_glyph_batch_cap ? g_glyph_batch_cap * 2 : 4096;
@ -1325,6 +1352,7 @@ gl_drv_draw_glyph (struct frame *f, struct gfx_glyph *g,
g_glyph_batch_cap = cap;
}
float r = rgba[0], gg = rgba[1], b = rgba[2], a = rgba[3];
float quad[6][GL_GLYPH_VERT_FLOATS] = {
{x0,y0, u0,v0, r,gg,b,a}, {x1,y0, u1,v0, r,gg,b,a}, {x1,y1, u1,v1, r,gg,b,a},
{x0,y0, u0,v0, r,gg,b,a}, {x1,y1, u1,v1, r,gg,b,a}, {x0,y1, u0,v1, r,gg,b,a},
@ -1334,6 +1362,26 @@ gl_drv_draw_glyph (struct frame *f, struct gfx_glyph *g,
g_glyph_batch_verts += 6;
}
static void
gl_drv_draw_glyph (struct frame *f, struct gfx_glyph *g,
float x, float ybase, unsigned long color)
{
struct gl_frame_data *fd = gl_get_frame_data (f);
if (!fd || !g || g->width <= 0 || g->height <= 0) return;
/* Stored metrics are physical pixels; the draw site is logical. */
float s = (float) fd->scale;
float x0 = x * s + g->bearing_x;
float y0 = ybase * s - g->bearing_y;
float rgba[4];
gl_unpack_color (color, rgba);
gl_batch_append (fd, x0, y0, x0 + g->width, y0 + g->height,
(float) g->atlas_x / GL_ATLAS_W,
(float) g->atlas_y / GL_ATLAS_H,
(float) (g->atlas_x + g->width) / GL_ATLAS_W,
(float) (g->atlas_y + g->height) / GL_ATLAS_H,
rgba);
}
static void gl_drv_draw_texture (struct frame *f, void *texture,
float x, float y, float w, float h,
float u0, float v0, float u1, float v1,
@ -1380,9 +1428,7 @@ static int g_cglyph_count;
static void
gl_flush_glyph_caches (void)
{
memset (g_glyphs, 0, sizeof g_glyphs);
g_glyph_count = 0;
g_atlas_next_x = g_atlas_next_y = g_atlas_row_h = 0;
gl_atlas_reset ();
for (int i = 0; i < GL_CGLYPH_CAP; i++)
if (g_cglyphs[i].valid)
glDeleteTextures (1, &g_cglyphs[i].tex);
@ -1657,7 +1703,8 @@ gl_drv_draw_texture (struct frame *f, void *texture,
{
struct gl_frame_data *fd = gl_get_frame_data (f);
if (!fd || !texture) return;
gl_flush_glyph_batch (); /* keep submission order vs queued glyphs */
gl_flush_glyph_batch (); /* keep submission order vs queued quads */
gl_scissor_apply_now (fd); /* non-batched: needs the real scissor */
gl_mark_dirty (fd, x, y, w, h);
float s = (float) fd->scale;
float x0 = x * s, y0 = y * s, x1 = (x + w) * s, y1 = (y + h) * s;
@ -1692,7 +1739,8 @@ gl_drv_draw_bitmap (struct frame *f, unsigned short *bits, int dh,
struct gl_frame_data *fd = gl_get_frame_data (f);
(void) bw;
if (!fd || !bits || wd <= 0 || h <= 0) return;
gl_flush_glyph_batch (); /* binds its own R8 texture; drain glyphs first */
gl_flush_glyph_batch (); /* binds its own R8 texture; drain quads first */
gl_scissor_apply_now (fd); /* non-batched: needs the real scissor */
if (wd > 16) wd = 16; /* a fringe bitmap row is an unsigned short */
gl_mark_dirty (fd, x, y, wd, h);
@ -2318,6 +2366,8 @@ gl_capture_frame (struct frame *f, int *w, int *h, unsigned char **out)
{
struct gl_frame_data *fd = gl_get_frame_data (f);
if (!fd || !fd->tex) return false;
if (g_glyph_batch_fd == fd)
gl_flush_glyph_batch (); /* the FBO must hold every queued quad */
int W = fd->w, H = fd->h;
unsigned char *buf = malloc ((size_t) W * H * 4);
if (!buf) return false;