Cursor effects, buffer cross-fades and inline video each ran on their own Lisp timer, and each timer presented on its own. With everything active that stacked presents well above the refresh rate, which is exactly the pressure under which radeonsi was caught presenting stale swapchain buffers, and the blocking swaps starved the other timers (inline video dropped from 30fps to ~20fps with its tick slipping to 49ms). Replace the three timers with a single pump. gpu.el now runs one repeating timer that calls the new gpu-pump-tick primitive: the driver advances the cursor physics, the fade clock and the video upload together and presents AT MOST ONCE per tick, under the same 12ms last-swap throttle as before. The pump paces itself: 60Hz while a fade runs, 30Hz otherwise, gone when nothing needs it. A skipped subsystem loses nothing -- the fade and the video are composited by every present, whoever issues it. The old per-subsystem entry points (gpu-anim-tick, gpu-transition-tick, gpu-video-tick) remain for compatibility; gpu-video-tick now presents only when a fresh frame arrived or the rect moved, under the same throttle, instead of unconditionally. On Metal the pump consolidates the same two timers; the animator already coalesces presents, so the change there is one deterministic tick (video frame pull, then animator step) with a real elapsed dt. Measured on the AMD iGPU test box (1920x1080, Cinnamon): video tick median 33.0ms / p90 33.3ms (was 49ms), a 30fps file plays at 29.5fps, and worst-case contention (video + cursor effects + 12 cross-fades) peaks at 8 presents per 100ms window with a 16.1ms median gap. GL_LOG_PRESENT now also traces the pump decisions and the per-tick video upload state.
3124 lines
118 KiB
C
3124 lines
118 KiB
C
/* glterm.c --- OpenGL (EGL + OpenGL ES) gfx driver for GNU Emacs.
|
|
Copyright (C) 2026 Free Software Foundation, Inc.
|
|
|
|
This file is part of GNU Emacs.
|
|
|
|
GNU Emacs is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or (at
|
|
your option) any later version.
|
|
|
|
GNU Emacs is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
|
This is the GNU/Linux counterpart of mtlterm.m: the cross-platform
|
|
drawing policy lives in gfxterm.c and renders exclusively through the
|
|
`struct gfx_driver' vtable (gfxdrv.h); this file implements that vtable
|
|
on top of EGL + OpenGL ES 3, with the glyph atlas rasterized by
|
|
FreeType (reached through the cairo scaled font of the ftcr backend).
|
|
|
|
The reference for every op is the matching mtl_drv_* in mtlterm.m. The
|
|
render target is a frame-sized RGBA8 texture in an FBO (the "static
|
|
texture": its content survives across cycles -- LOAD, not clear). All
|
|
drawing happens off screen into that FBO; gl_capture_frame reads it back
|
|
for the pixel-parity harness. On-screen present (gl_present_to_window)
|
|
blits the FBO to the frame's X window through an EGL window surface and
|
|
swaps, on the same deferred-present schedule as the Metal driver: the
|
|
EGL display is brought up on the X11 platform when an X connection
|
|
exists (window surfaces need it) and falls back to surfaceless only for
|
|
a frameless batch run.
|
|
|
|
Grayscale glyphs go through an R8 coverage atlas; color glyphs (emoji)
|
|
are rasterized through cairo (which scales the bitmap strike to the
|
|
laid-out size) into premultiplied RGBA textures, drawn with the image
|
|
program -- see gl_color_glyph_get.
|
|
|
|
Two structural optimizations shape the draw path. Glyphs and solid
|
|
rectangles share one submission-ordered vertex batch (the rects sample
|
|
a white block reserved in the atlas) whose quads are clipped on the
|
|
CPU at queue time, so a whole redraw flushes as a handful of draw
|
|
calls; see gl_batch_append. And the present blits only what the back
|
|
buffer is actually missing, derived from EGL_EXT_buffer_age plus a
|
|
per-swap record of dirty regions, handing the compositor the damaged
|
|
boxes through eglSwapBuffersWithDamage; see gl_present_to_window. */
|
|
|
|
#include <config.h>
|
|
|
|
#ifdef HAVE_GFX_GL
|
|
|
|
#include <math.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
#include <EGL/egl.h>
|
|
#include <EGL/eglext.h>
|
|
#include <GLES3/gl3.h>
|
|
|
|
#ifdef HAVE_GSTREAMER
|
|
#include <gst/gst.h>
|
|
#include <gst/app/gstappsink.h>
|
|
#include <gst/video/video.h>
|
|
#endif
|
|
|
|
#include <cairo.h>
|
|
#include <cairo-ft.h>
|
|
#include <ft2build.h>
|
|
#include FT_FREETYPE_H
|
|
|
|
#include "lisp.h"
|
|
#include "dispextern.h"
|
|
#include "frame.h"
|
|
#include "blockinput.h"
|
|
#include "termchar.h"
|
|
#include "window.h"
|
|
#include "character.h"
|
|
#include "font.h"
|
|
#include "ftfont.h"
|
|
#include "composite.h"
|
|
#include "xterm.h"
|
|
#include "gfxdrv.h"
|
|
#include "glterm.h"
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Color helper: the policy hands us `unsigned long' face pixels. On a
|
|
TrueColor X visual (the only kind in practice) the pixel packs the
|
|
channels; we treat it as 0x00RRGGBB, matching the gfxdrv.h contract.
|
|
TODO: decode through the frame's visual masks for exotic visuals. */
|
|
static void
|
|
gl_unpack_color (unsigned long c, float rgba[4])
|
|
{
|
|
rgba[0] = ((c >> 16) & 0xff) / 255.0f;
|
|
rgba[1] = ((c >> 8) & 0xff) / 255.0f;
|
|
rgba[2] = (c & 0xff) / 255.0f;
|
|
rgba[3] = 1.0f;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* GLES3 shaders (ports of the MSL in mtlterm.m). Top-left-origin pixel
|
|
space -> NDC; the glyph shader applies the pow(coverage, 0.82) weight
|
|
that matches the native (cairo) text ink. */
|
|
|
|
static const char *VS_RECT =
|
|
"#version 300 es\n"
|
|
"layout(location=0) in vec2 a_pos;\n"
|
|
"layout(location=2) in vec4 a_color;\n"
|
|
"uniform vec2 u_size;\n"
|
|
"out vec4 v_color;\n"
|
|
"void main(){\n"
|
|
" vec2 n = vec2((a_pos.x/u_size.x)*2.0-1.0, 1.0-(a_pos.y/u_size.y)*2.0);\n"
|
|
" gl_Position = vec4(n,0.0,1.0); v_color=a_color; }\n";
|
|
static const char *FS_RECT =
|
|
"#version 300 es\n"
|
|
"precision mediump float;\n"
|
|
"in vec4 v_color; out vec4 o;\n"
|
|
"void main(){ o = v_color; }\n";
|
|
|
|
static const char *VS_GLYPH =
|
|
"#version 300 es\n"
|
|
"layout(location=0) in vec2 a_pos;\n"
|
|
"layout(location=1) in vec2 a_uv;\n"
|
|
"layout(location=2) in vec4 a_color;\n"
|
|
"uniform vec2 u_size;\n"
|
|
/* highp UVs: a 2048px atlas needs more than fp16's ~10-bit mantissa to
|
|
address its texels exactly on strict-mediump GPUs. */
|
|
"out highp vec2 v_uv; out vec4 v_color;\n"
|
|
"void main(){\n"
|
|
" vec2 n = vec2((a_pos.x/u_size.x)*2.0-1.0, 1.0-(a_pos.y/u_size.y)*2.0);\n"
|
|
" gl_Position = vec4(n,0.0,1.0); v_uv=a_uv; v_color=a_color; }\n";
|
|
static const char *FS_GLYPH =
|
|
"#version 300 es\n"
|
|
"precision mediump float;\n"
|
|
"in highp vec2 v_uv; in vec4 v_color; out vec4 o;\n"
|
|
"uniform sampler2D u_atlas;\n"
|
|
"void main(){\n"
|
|
" float cov = texture(u_atlas, v_uv).r;\n"
|
|
" cov = pow(cov, 0.82);\n"
|
|
" o = vec4(v_color.rgb, v_color.a*cov); }\n";
|
|
|
|
/* RGBA textured quad (images / video frames). */
|
|
static const char *VS_IMAGE =
|
|
"#version 300 es\n"
|
|
"layout(location=0) in vec2 a_pos;\n"
|
|
"layout(location=1) in vec2 a_uv;\n"
|
|
"uniform vec2 u_size;\n"
|
|
"out highp vec2 v_uv;\n"
|
|
"void main(){\n"
|
|
" vec2 n = vec2((a_pos.x/u_size.x)*2.0-1.0, 1.0-(a_pos.y/u_size.y)*2.0);\n"
|
|
" gl_Position = vec4(n,0.0,1.0); v_uv=a_uv; }\n";
|
|
/* The image texture holds PREMULTIPLIED alpha (from a cairo ARGB32 surface);
|
|
scaling all four channels by u_alpha keeps it premultiplied, and the caller
|
|
composites it with glBlendFunc (GL_ONE, GL_ONE_MINUS_SRC_ALPHA), which is
|
|
exactly cairo's OVER operator. */
|
|
static const char *FS_IMAGE =
|
|
"#version 300 es\n"
|
|
"precision mediump float;\n"
|
|
"in highp vec2 v_uv; out vec4 o;\n"
|
|
"uniform sampler2D u_tex; uniform float u_alpha;\n"
|
|
"void main(){ o = texture(u_tex,v_uv) * u_alpha; }\n";
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Global GL objects (one process-wide context, like Metal's g_device). */
|
|
|
|
#define GL_ATLAS_W 2048
|
|
#define GL_ATLAS_H 2048
|
|
|
|
static bool g_gl_ready = false;
|
|
static EGLDisplay g_dpy = EGL_NO_DISPLAY;
|
|
static EGLContext g_ctx = EGL_NO_CONTEXT;
|
|
static EGLConfig g_cfg;
|
|
/* True when g_dpy is an X11-platform display (window surfaces are usable
|
|
for on-screen present); false for the surfaceless fallback. */
|
|
static bool g_dpy_is_x11 = false;
|
|
/* Partial present support. EGL_EXT_buffer_age reports how many swaps ago
|
|
the current back buffer was last presented, which tells exactly which
|
|
frames' changes it is missing; the present then blits only the union of
|
|
those dirty regions. Unlike EGL_BUFFER_PRESERVED (which some compositing
|
|
window managers advertise but do not honor), the age is what the driver
|
|
actually guarantees about its own buffer rotation, so this is correct
|
|
under any compositor. eglSwapBuffersWithDamage additionally hands the
|
|
compositor the damaged box so it recomposites only that band. */
|
|
static bool g_has_buffer_age = false;
|
|
static PFNEGLSWAPBUFFERSWITHDAMAGEKHRPROC g_swap_damage = NULL;
|
|
/* The draw/read surface currently bound to g_ctx, so we never re-issue an
|
|
eglMakeCurrent that would not change anything. FBO rendering works under
|
|
any bound surface, so the window surface is kept current across frames
|
|
instead of switching to surfaceless and back on every present. */
|
|
static EGLSurface g_bound_surf = EGL_NO_SURFACE;
|
|
static bool g_bound_known = false;
|
|
|
|
/* Make SURF current on g_ctx (draw == read == SURF; EGL_NO_SURFACE means
|
|
surfaceless). No-op when SURF is already bound. */
|
|
static bool
|
|
gl_bind_surface (EGLSurface surf)
|
|
{
|
|
if (g_bound_known && surf == g_bound_surf) return true;
|
|
if (!eglMakeCurrent (g_dpy, surf, surf, g_ctx)) return false;
|
|
g_bound_surf = surf;
|
|
g_bound_known = true;
|
|
return true;
|
|
}
|
|
|
|
static GLuint g_prog_rect, g_prog_glyph, g_prog_image;
|
|
static GLint g_u_rect_size, g_u_glyph_size, g_u_glyph_atlas;
|
|
static GLint g_u_image_size, g_u_image_tex, g_u_image_alpha;
|
|
static GLuint g_vbo;
|
|
|
|
static GLuint g_atlas; /* R8 coverage atlas */
|
|
static int g_atlas_next_x, g_atlas_next_y, g_atlas_row_h;
|
|
|
|
/* Batched grayscale glyphs. Every glyph from the coverage atlas shares
|
|
g_atlas and g_prog_glyph, so instead of one draw call per glyph we
|
|
accumulate their quads here and emit a single glDrawArrays. The batch
|
|
is flushed (gl_flush_glyph_batch) whenever the GL state that follows
|
|
would differ: a clip/scissor change, any non-glyph primitive, or the
|
|
end of the render cycle. All glyphs in a flush therefore share one
|
|
scissor (each glyph string sets its clip once, then draws its glyphs),
|
|
so the output is identical to drawing them one by one. */
|
|
#define GL_GLYPH_VERT_FLOATS 8 /* x,y,u,v,r,g,b,a */
|
|
static float *g_glyph_batch; /* GL_GLYPH_VERT_FLOATS per vertex */
|
|
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]);
|
|
struct gl_video;
|
|
#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);
|
|
static bool gl_video_pump (struct gl_video *v);
|
|
#endif
|
|
|
|
/* Cursor animation overlay + note_cursor. Config globals are defined with
|
|
the animation types (after struct gl_anim, which holds the enum). */
|
|
static void gl_anim_overlay (struct gl_frame_data *fd, int sw, int sh);
|
|
static bool gl_anim_overlay_active (struct gl_frame_data *fd);
|
|
static bool gl_drv_note_cursor (struct frame *f, int x, int y, int w, int h,
|
|
unsigned long color);
|
|
|
|
/* Render scale. The FBO and every glyph/image/bitmap is rasterized and
|
|
drawn at physical pixels = logical * g_atlas_scale.
|
|
|
|
On X11 there is NO backing-scale indirection like the macOS Retina
|
|
drawable: an Emacs frame's X window is sized to FRAME_PIXEL_WIDTH and
|
|
Emacs lays out its text area in device pixels regardless of the GTK
|
|
scale factor (a 14px font stays 14px, the char cell stays 8px, with
|
|
GDK_SCALE=1 or 2 alike). HiDPI sharpness on X is obtained the Emacs
|
|
way, with a higher Xft.dpi / larger font: the cairo pixel_size grows,
|
|
so the atlas is already rasterized big and crisp at scale 1. Keying
|
|
the FBO off the GTK scale factor (xg_get_scale) would oversize it
|
|
relative to the real window surface, forcing a downscale on present
|
|
(verified: GDK_SCALE=2 -> xg_get_scale 2, but the X window stays
|
|
640x400, not 1280x800). So the default scale is 1.
|
|
|
|
GL_SCALE is an explicit knob: values > 1 render everything at N x and
|
|
the present blit downsamples to the window (a supersampling AA pass),
|
|
and it is the path a future compositor-aware scale would feed. The
|
|
atlas stores physical-size bitmaps, so a scale change flushes both
|
|
glyph caches. */
|
|
static double g_atlas_scale = 1.0;
|
|
|
|
static void gl_flush_glyph_caches (void);
|
|
|
|
/* Resolve the render scale for frame F: the GL_SCALE override, else 1. */
|
|
static double
|
|
gl_frame_scale (struct frame *f)
|
|
{
|
|
const char *env = getenv ("GL_SCALE");
|
|
if (env && *env)
|
|
{
|
|
double s = atof (env);
|
|
if (s >= 1.0) return s;
|
|
}
|
|
(void) f;
|
|
return 1.0;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Glyph cache: key (font ptr mixed with glyph id) -> struct gfx_glyph.
|
|
Open-addressing table with linear probing. */
|
|
|
|
#define GL_GLYPH_CAP 8192
|
|
static struct gfx_glyph g_glyphs[GL_GLYPH_CAP];
|
|
static int g_glyph_count;
|
|
|
|
static struct gfx_glyph *
|
|
glyph_cache_lookup (unsigned long long key)
|
|
{
|
|
unsigned h = (unsigned) (key % GL_GLYPH_CAP);
|
|
for (int i = 0; i < GL_GLYPH_CAP; i++)
|
|
{
|
|
struct gfx_glyph *g = &g_glyphs[(h + i) % GL_GLYPH_CAP];
|
|
if (!g->valid) return NULL;
|
|
if (g->cache_key == key) return g;
|
|
}
|
|
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 */
|
|
gl_atlas_reset ();
|
|
unsigned h = (unsigned) (key % GL_GLYPH_CAP);
|
|
for (int i = 0; i < GL_GLYPH_CAP; i++)
|
|
{
|
|
struct gfx_glyph *g = &g_glyphs[(h + i) % GL_GLYPH_CAP];
|
|
if (!g->valid)
|
|
{
|
|
memset (g, 0, sizeof *g);
|
|
g->cache_key = key;
|
|
g->valid = true;
|
|
g_glyph_count++;
|
|
return g;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Cursor animation (the MtlAnimator analogue): spring glide, comet trail,
|
|
and particle bursts (sonicboom / ripple / pixiedust), composited over
|
|
the frame in the present. Opt-in: off unless gpu.el turns it on, so the
|
|
headless parity harness (which enables only the backend) sees a plain
|
|
static cursor. */
|
|
|
|
enum gl_cursor_mode
|
|
{
|
|
GL_CURSOR_BLOCK = 0, GL_CURSOR_SPRING = 1, GL_CURSOR_TORPEDO = 2,
|
|
GL_CURSOR_SONICBOOM = 3, GL_CURSOR_RIPPLE = 4, GL_CURSOR_PIXIEDUST = 5,
|
|
GL_CURSOR_HOLLOW = 6, GL_CURSOR_BEAM = 7,
|
|
};
|
|
|
|
#define GL_TRAIL_LEN 40
|
|
#define GL_MAX_PARTICLES 80
|
|
#define GL_TRAIL_LIFETIME 0.40f
|
|
#define GL_SPRING_OMEGA 8.0f /* settles in ~150ms, like Metal */
|
|
|
|
struct gl_particle { float x, y, vx, vy, age, size; unsigned long color; };
|
|
|
|
struct gl_anim
|
|
{
|
|
float tx, ty, tw, th; /* Emacs cursor target, logical px */
|
|
unsigned long color; /* real frame cursor color */
|
|
float sx, svx, sy, svy; /* spring position + velocity, per axis */
|
|
bool hidden; /* blink-off phase */
|
|
bool have_target; /* a cursor was placed at least once */
|
|
/* Comet trail (torpedo): ring buffer of recent positions + their age. */
|
|
float trail_x[GL_TRAIL_LEN], trail_y[GL_TRAIL_LEN], trail_age[GL_TRAIL_LEN];
|
|
int trail_head, trail_count;
|
|
struct gl_particle particles[GL_MAX_PARTICLES];
|
|
int n_particles;
|
|
};
|
|
|
|
/* Cursor animation config (the g_mtl_* analogues). Animations are opt-in
|
|
(gpu.el flips g_gl_animations_enabled on); the default mode is sonicboom
|
|
so that, once enabled, the GPU flag ships a cursor effect out of the box.
|
|
g_gl_cursor_suppress is set per-command so typing does not spray effects. */
|
|
static int g_gl_cursor_mode = GL_CURSOR_SONICBOOM;
|
|
static bool g_gl_animations_enabled = false;
|
|
static int g_gl_trail_len = 20;
|
|
static bool g_gl_cursor_suppress = false;
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Dirty-region tracking for the partial present. A single union box
|
|
degenerates as soon as two distant regions change in one frame (the
|
|
edited row at the top plus the mode line at the bottom span almost the
|
|
whole frame), so the set keeps up to GL_DIRTY_MAX disjoint boxes in FBO
|
|
pixel coordinates (bottom-left origin) and merges on overlap/overflow. */
|
|
|
|
#define GL_DIRTY_MAX 8
|
|
|
|
struct gl_dirty_set
|
|
{
|
|
bool all; /* everything changed: boxes irrelevant */
|
|
int n;
|
|
int b[GL_DIRTY_MAX][4]; /* x0, y0, x1, y1 (exclusive) */
|
|
};
|
|
|
|
static void
|
|
gl_dirty_clear (struct gl_dirty_set *d)
|
|
{
|
|
d->all = false;
|
|
d->n = 0;
|
|
}
|
|
|
|
/* Add a box, growing an existing one when they touch (within SLACK px,
|
|
so the per-glyph marks of one row coalesce into a single box) and
|
|
folding into the closest box on overflow. */
|
|
static void
|
|
gl_dirty_add (struct gl_dirty_set *d, int x0, int y0, int x1, int y1)
|
|
{
|
|
const int SLACK = 8;
|
|
if (d->all || x0 >= x1 || y0 >= y1)
|
|
return;
|
|
for (int i = 0; i < d->n; i++)
|
|
{
|
|
int *b = d->b[i];
|
|
if (x0 <= b[2] + SLACK && x1 >= b[0] - SLACK
|
|
&& y0 <= b[3] + SLACK && y1 >= b[1] - SLACK)
|
|
{
|
|
if (x0 < b[0]) b[0] = x0;
|
|
if (y0 < b[1]) b[1] = y0;
|
|
if (x1 > b[2]) b[2] = x1;
|
|
if (y1 > b[3]) b[3] = y1;
|
|
return;
|
|
}
|
|
}
|
|
if (d->n < GL_DIRTY_MAX)
|
|
{
|
|
int *b = d->b[d->n++];
|
|
b[0] = x0; b[1] = y0; b[2] = x1; b[3] = y1;
|
|
return;
|
|
}
|
|
/* Full: fold into the box whose union grows the least. */
|
|
int best = 0;
|
|
long best_growth = -1;
|
|
for (int i = 0; i < d->n; i++)
|
|
{
|
|
int *b = d->b[i];
|
|
long ux0 = min (b[0], x0), uy0 = min (b[1], y0);
|
|
long ux1 = max (b[2], x1), uy1 = max (b[3], y1);
|
|
long growth = (ux1 - ux0) * (uy1 - uy0)
|
|
- (long) (b[2] - b[0]) * (b[3] - b[1]);
|
|
if (best_growth < 0 || growth < best_growth)
|
|
{ best_growth = growth; best = i; }
|
|
}
|
|
int *b = d->b[best];
|
|
if (x0 < b[0]) b[0] = x0;
|
|
if (y0 < b[1]) b[1] = y0;
|
|
if (x1 > b[2]) b[2] = x1;
|
|
if (y1 > b[3]) b[3] = y1;
|
|
}
|
|
|
|
/* Union SRC into DST (used to repair an aged back buffer). */
|
|
static void
|
|
gl_dirty_union (struct gl_dirty_set *dst, const struct gl_dirty_set *src)
|
|
{
|
|
if (src->all)
|
|
{ dst->all = true; return; }
|
|
for (int i = 0; i < src->n && !dst->all; i++)
|
|
gl_dirty_add (dst, src->b[i][0], src->b[i][1], src->b[i][2], src->b[i][3]);
|
|
}
|
|
|
|
/* Per-frame GL state (the MtlFrameData analogue). Associated with a
|
|
`struct frame *' through a small process-wide map. */
|
|
|
|
struct gl_frame_data
|
|
{
|
|
struct frame *f;
|
|
GLuint fbo, tex; /* static texture render target */
|
|
GLuint scratch_fbo, scratch_tex;
|
|
int w, h; /* texture size in physical pixels */
|
|
double scale;
|
|
bool in_cycle;
|
|
bool needs_present;
|
|
EGLSurface surf; /* on-screen window surface, or EGL_NO_SURFACE */
|
|
unsigned long surf_win; /* X window `surf' was created for (0 = none) */
|
|
int surf_w, surf_h; /* cached surface size (re-queried on resize) */
|
|
bool clip_on;
|
|
int clip_x, clip_y, clip_w, clip_h; /* top-left logical */
|
|
/* FBO changes accumulated since the last swap (bottom-left pixel
|
|
coords). The present consumes it: after the swap it is recorded in
|
|
swap_dirty and cleared. */
|
|
struct gl_dirty_set dirty;
|
|
/* What each of the last GL_SWAP_RING swaps changed, newest at
|
|
(swap_head - 1); `all' also marks overlay frames (their pixels live
|
|
outside the FBO). Combined with the back buffer's age this yields
|
|
exactly the region an aged buffer is missing. */
|
|
#define GL_SWAP_RING 8
|
|
struct gl_dirty_set swap_dirty[GL_SWAP_RING];
|
|
int swap_head;
|
|
/* Buffer-switch cross-fade: a snapshot of the previous frame fades out
|
|
over the new content while trans_dur > 0 (see gl_transition_start). */
|
|
GLuint trans_tex;
|
|
int trans_w, trans_h;
|
|
double trans_start, trans_dur;
|
|
/* When the last successful swap happened. The animation pumps (cursor
|
|
effects, cross-fade) throttle on it: their ticks interleave with
|
|
redisplay's own presents, and burst-presenting the surface well above
|
|
the refresh rate makes Mesa juggle extra swapchain buffers -- under
|
|
which radeonsi has been seen presenting a stale one (an ancient frame
|
|
flashing for one vblank). Time-based animations lose nothing by
|
|
skipping a tick that lands right after a present. */
|
|
double last_swap;
|
|
/* When gl_pump_tick last ran, for real-elapsed animation steps. */
|
|
double last_pump;
|
|
/* Inline video, or NULL. Composited over the FBO blit on every present
|
|
(see gl_present_to_window); the MtlVideoPlayer analogue. */
|
|
struct gl_video *video;
|
|
/* Cursor animation overlay state. */
|
|
struct gl_anim anim;
|
|
};
|
|
|
|
#define GL_MAX_FRAMES 64
|
|
static struct gl_frame_data *g_frames[GL_MAX_FRAMES];
|
|
static struct gl_frame_data *g_cur; /* frame of the open cycle */
|
|
|
|
/* The lookup runs once per drawing op (per glyph at the worst), so keep
|
|
the last hit: redisplay works one frame at a time and the slot scan
|
|
only happens on a frame switch. */
|
|
static struct gl_frame_data *g_fd_mru;
|
|
|
|
static struct gl_frame_data *
|
|
gl_get_frame_data (struct frame *f)
|
|
{
|
|
if (g_fd_mru && g_fd_mru->f == f)
|
|
return g_fd_mru;
|
|
for (int i = 0; i < GL_MAX_FRAMES; i++)
|
|
if (g_frames[i] && g_frames[i]->f == f)
|
|
return g_fd_mru = g_frames[i];
|
|
return NULL;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* GL helpers. */
|
|
|
|
static GLuint
|
|
gl_compile (GLenum type, const char *src)
|
|
{
|
|
GLuint s = glCreateShader (type);
|
|
glShaderSource (s, 1, &src, NULL);
|
|
glCompileShader (s);
|
|
GLint ok = 0;
|
|
glGetShaderiv (s, GL_COMPILE_STATUS, &ok);
|
|
if (!ok)
|
|
{
|
|
char log[1024];
|
|
glGetShaderInfoLog (s, sizeof log, NULL, log);
|
|
fprintf (stderr, "glterm: shader compile failed: %s\n", log);
|
|
}
|
|
return s;
|
|
}
|
|
|
|
static GLuint
|
|
gl_program (const char *vs, const char *fs)
|
|
{
|
|
GLuint p = glCreateProgram ();
|
|
glAttachShader (p, gl_compile (GL_VERTEX_SHADER, vs));
|
|
glAttachShader (p, gl_compile (GL_FRAGMENT_SHADER, fs));
|
|
glLinkProgram (p);
|
|
GLint ok = 0;
|
|
glGetProgramiv (p, GL_LINK_STATUS, &ok);
|
|
if (!ok)
|
|
{
|
|
char log[1024];
|
|
glGetProgramInfoLog (p, sizeof log, NULL, log);
|
|
fprintf (stderr, "glterm: program link failed: %s\n", log);
|
|
}
|
|
return p;
|
|
}
|
|
|
|
/* Bring up the process-wide EGL surfaceless context and the shared GL
|
|
objects. Returns false on failure (the caller keeps the platform
|
|
backend). */
|
|
static bool
|
|
gl_global_init (void)
|
|
{
|
|
if (g_gl_ready) return true;
|
|
|
|
PFNEGLGETPLATFORMDISPLAYEXTPROC get_dpy =
|
|
(PFNEGLGETPLATFORMDISPLAYEXTPROC)
|
|
eglGetProcAddress ("eglGetPlatformDisplayEXT");
|
|
|
|
/* Prefer an X11-platform display: it renders to FBOs exactly like the
|
|
surfaceless one (so the headless capture path is unchanged), but it
|
|
can also create window surfaces, which is what on-screen present
|
|
needs. Fall back to surfaceless only when there is no X
|
|
connection (a pure batch run with no frame). */
|
|
Display *xdpy = x_display_list ? x_display_list->display : NULL;
|
|
/* GL_FORCE_SURFACELESS keeps the surfaceless path even with an X
|
|
connection: on a DRI3-less X server (e.g. Xvfb) the X11 platform falls
|
|
back to llvmpipe, whereas surfaceless still reaches the real GPU. Use
|
|
it for the headless GPU benchmark; on-screen present is then a no-op
|
|
(the FBO capture is unaffected). */
|
|
if (getenv ("GL_FORCE_SURFACELESS"))
|
|
xdpy = NULL;
|
|
if (xdpy && get_dpy)
|
|
{
|
|
g_dpy = get_dpy (EGL_PLATFORM_X11_KHR, xdpy, NULL);
|
|
if (g_dpy != EGL_NO_DISPLAY)
|
|
g_dpy_is_x11 = true;
|
|
}
|
|
if (g_dpy == EGL_NO_DISPLAY && xdpy)
|
|
{
|
|
g_dpy = eglGetDisplay ((EGLNativeDisplayType) xdpy);
|
|
if (g_dpy != EGL_NO_DISPLAY)
|
|
g_dpy_is_x11 = true;
|
|
}
|
|
if (g_dpy == EGL_NO_DISPLAY && get_dpy)
|
|
g_dpy = get_dpy (EGL_PLATFORM_SURFACELESS_MESA, EGL_DEFAULT_DISPLAY, NULL);
|
|
if (g_dpy == EGL_NO_DISPLAY)
|
|
g_dpy = eglGetDisplay (EGL_DEFAULT_DISPLAY);
|
|
if (g_dpy == EGL_NO_DISPLAY) return false;
|
|
|
|
EGLint maj, min;
|
|
if (!eglInitialize (g_dpy, &maj, &min)) return false;
|
|
if (!eglBindAPI (EGL_OPENGL_ES_API)) return false;
|
|
|
|
EGLint cfg_attr[] = {
|
|
EGL_SURFACE_TYPE, g_dpy_is_x11 ? (EGL_WINDOW_BIT | EGL_PBUFFER_BIT)
|
|
: EGL_PBUFFER_BIT,
|
|
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT,
|
|
EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8,
|
|
EGL_NONE
|
|
};
|
|
EGLint n = 0;
|
|
if (!eglChooseConfig (g_dpy, cfg_attr, &g_cfg, 1, &n) || n < 1)
|
|
return false;
|
|
|
|
EGLint ctx_attr[] = { EGL_CONTEXT_MAJOR_VERSION, 3, EGL_NONE };
|
|
g_ctx = eglCreateContext (g_dpy, g_cfg, EGL_NO_CONTEXT, ctx_attr);
|
|
if (g_ctx == EGL_NO_CONTEXT) return false;
|
|
if (!gl_bind_surface (EGL_NO_SURFACE))
|
|
return false;
|
|
|
|
/* Partial-present support (see the comment at g_has_buffer_age). */
|
|
{
|
|
const char *ext = eglQueryString (g_dpy, EGL_EXTENSIONS);
|
|
if (ext && strstr (ext, "EGL_EXT_buffer_age"))
|
|
g_has_buffer_age = true;
|
|
if (ext && strstr (ext, "EGL_KHR_swap_buffers_with_damage"))
|
|
g_swap_damage = (PFNEGLSWAPBUFFERSWITHDAMAGEKHRPROC)
|
|
eglGetProcAddress ("eglSwapBuffersWithDamageKHR");
|
|
else if (ext && strstr (ext, "EGL_EXT_swap_buffers_with_damage"))
|
|
g_swap_damage = (PFNEGLSWAPBUFFERSWITHDAMAGEKHRPROC)
|
|
eglGetProcAddress ("eglSwapBuffersWithDamageEXT");
|
|
}
|
|
|
|
g_prog_rect = gl_program (VS_RECT, FS_RECT);
|
|
g_prog_glyph = gl_program (VS_GLYPH, FS_GLYPH);
|
|
g_prog_image = gl_program (VS_IMAGE, FS_IMAGE);
|
|
g_u_rect_size = glGetUniformLocation (g_prog_rect, "u_size");
|
|
g_u_glyph_size = glGetUniformLocation (g_prog_glyph, "u_size");
|
|
g_u_glyph_atlas = glGetUniformLocation (g_prog_glyph, "u_atlas");
|
|
g_u_image_size = glGetUniformLocation (g_prog_image, "u_size");
|
|
g_u_image_tex = glGetUniformLocation (g_prog_image, "u_tex");
|
|
g_u_image_alpha = glGetUniformLocation (g_prog_image, "u_alpha");
|
|
|
|
glGenBuffers (1, &g_vbo);
|
|
|
|
glGenTextures (1, &g_atlas);
|
|
glBindTexture (GL_TEXTURE_2D, g_atlas);
|
|
glTexImage2D (GL_TEXTURE_2D, 0, GL_R8, GL_ATLAS_W, GL_ATLAS_H, 0,
|
|
GL_RED, GL_UNSIGNED_BYTE, NULL);
|
|
glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
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);
|
|
glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
|
|
|
g_gl_ready = true;
|
|
return true;
|
|
}
|
|
|
|
/* Ensure FD's static texture matches (w,h); (re)create + clear if not. */
|
|
static void
|
|
gl_ensure_target (struct gl_frame_data *fd, int w, int h)
|
|
{
|
|
if (w < 1) w = 1;
|
|
if (h < 1) h = 1;
|
|
if (fd->tex && fd->w == w && fd->h == h) return;
|
|
|
|
if (!fd->tex) glGenTextures (1, &fd->tex);
|
|
glBindTexture (GL_TEXTURE_2D, fd->tex);
|
|
glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA,
|
|
GL_UNSIGNED_BYTE, NULL);
|
|
glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
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);
|
|
|
|
if (!fd->scratch_tex) glGenTextures (1, &fd->scratch_tex);
|
|
glBindTexture (GL_TEXTURE_2D, fd->scratch_tex);
|
|
glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA,
|
|
GL_UNSIGNED_BYTE, NULL);
|
|
glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
|
glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
|
|
|
if (!fd->fbo) glGenFramebuffers (1, &fd->fbo);
|
|
glBindFramebuffer (GL_FRAMEBUFFER, fd->fbo);
|
|
glFramebufferTexture2D (GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
|
|
GL_TEXTURE_2D, fd->tex, 0);
|
|
if (!fd->scratch_fbo) glGenFramebuffers (1, &fd->scratch_fbo);
|
|
glBindFramebuffer (GL_FRAMEBUFFER, fd->scratch_fbo);
|
|
glFramebufferTexture2D (GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
|
|
GL_TEXTURE_2D, fd->scratch_tex, 0);
|
|
|
|
fd->w = w;
|
|
fd->h = h;
|
|
|
|
/* Brand-new/resized target: clear to the frame background and present
|
|
the whole frame this cycle (the snapshot underneath is stale). */
|
|
fd->dirty.all = true;
|
|
glBindFramebuffer (GL_FRAMEBUFFER, fd->fbo);
|
|
glViewport (0, 0, w, h);
|
|
float bg[4];
|
|
gl_unpack_color (FRAME_BACKGROUND_PIXEL (fd->f), bg);
|
|
glDisable (GL_SCISSOR_TEST);
|
|
glClearColor (bg[0], bg[1], bg[2], 1.0f);
|
|
glClear (GL_COLOR_BUFFER_BIT);
|
|
}
|
|
|
|
/* Mark a top-left logical rect (x,y,w,h) as changed: converted to FBO
|
|
pixel coords (bottom-left), clamped to the target, and added to the
|
|
frame's dirty set. The present then blits only those boxes. */
|
|
static void
|
|
gl_mark_dirty (struct gl_frame_data *fd, double x, double y,
|
|
double w, double h)
|
|
{
|
|
if (fd->dirty.all || w <= 0 || h <= 0) return;
|
|
double s = fd->scale;
|
|
int px0 = (int) floor (x * s);
|
|
int px1 = (int) ceil ((x + w) * s);
|
|
int py_top = (int) floor (y * s);
|
|
int py_bot = (int) ceil ((y + h) * s);
|
|
/* Flip Y: top-left logical -> bottom-left FBO. */
|
|
int fy0 = fd->h - py_bot;
|
|
int fy1 = fd->h - py_top;
|
|
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);
|
|
}
|
|
|
|
/* 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_scissor_apply_now (struct gl_frame_data *fd)
|
|
{
|
|
if (!fd->clip_on)
|
|
{
|
|
glDisable (GL_SCISSOR_TEST);
|
|
return;
|
|
}
|
|
double s = fd->scale;
|
|
int x = (int) (fd->clip_x * s);
|
|
int y = (int) (fd->clip_y * s);
|
|
int w = (int) (fd->clip_w * s);
|
|
int h = (int) (fd->clip_h * s);
|
|
glEnable (GL_SCISSOR_TEST);
|
|
glScissor (x, fd->h - (y + h), w, h);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Glyph rasterization: FreeType (via the ftcr backend's cairo scaled
|
|
font) into the R8 atlas. Mirrors mtl_rasterize_glyph_id; FreeType's
|
|
bitmap_left/bitmap_top already are the contract's bearing_x/bearing_y. */
|
|
|
|
static FT_Face
|
|
gl_lock_face (struct font *font, cairo_scaled_font_t **out_sf)
|
|
{
|
|
if (!font) return NULL;
|
|
/* Only the cairo/FreeType font drivers carry a `cr_scaled_font' at the
|
|
`struct font_info' layout we cast to. Emacs can fall back to a core
|
|
X11 font (xfont_driver) for glyphs no cairo font covers (e.g. some
|
|
CJK when no scalable CJK font matches); its `struct font' is a
|
|
different (xfont_info) layout, so reading cr_scaled_font there yields
|
|
garbage and crashes cairo_ft_scaled_font_lock_face. Guard the cast.
|
|
Such glyphs cannot go through our atlas; the caller skips them rather
|
|
than crashing. */
|
|
if (font->driver != &ftcrfont_driver
|
|
&& font->driver != &ftcrhbfont_driver)
|
|
return NULL;
|
|
struct font_info *fi = (struct font_info *) font;
|
|
cairo_scaled_font_t *sf = fi->cr_scaled_font;
|
|
if (!sf) return NULL;
|
|
*out_sf = sf;
|
|
FT_Face face = cairo_ft_scaled_font_lock_face (sf);
|
|
return face;
|
|
}
|
|
|
|
static struct gfx_glyph *
|
|
gl_rasterize_glyph (struct font *font, unsigned int glyph_id,
|
|
unsigned long long key)
|
|
{
|
|
cairo_scaled_font_t *sf = NULL;
|
|
FT_Face face = gl_lock_face (font, &sf);
|
|
if (!face) return NULL;
|
|
|
|
/* HiDPI: re-hint and rasterize at physical resolution so the atlas
|
|
bitmap is crisp at the (physical) draw site. cairo activates its
|
|
own FT size on the next lock, so overriding the ppem here is safe.
|
|
The bitmap, bearings and box come out in physical pixels (what the
|
|
draw path expects); the advance is converted back to logical. */
|
|
double s = g_atlas_scale;
|
|
if (s != 1.0 && face->size)
|
|
{
|
|
FT_UInt xp = (FT_UInt) lround (face->size->metrics.x_ppem * s);
|
|
FT_UInt yp = (FT_UInt) lround (face->size->metrics.y_ppem * s);
|
|
if (xp > 0 && yp > 0)
|
|
FT_Set_Pixel_Sizes (face, xp, yp);
|
|
}
|
|
|
|
if (FT_Load_Glyph (face, glyph_id, FT_LOAD_RENDER))
|
|
{
|
|
cairo_ft_scaled_font_unlock_face (sf);
|
|
return NULL;
|
|
}
|
|
FT_GlyphSlot g = face->glyph;
|
|
int bw = g->bitmap.width, bh = g->bitmap.rows;
|
|
float adv = (g->advance.x / 64.0f) / (float) s;
|
|
int bx = g->bitmap_left, by = g->bitmap_top;
|
|
|
|
struct gfx_glyph *e;
|
|
if (bw <= 0 || bh <= 0)
|
|
{
|
|
cairo_ft_scaled_font_unlock_face (sf);
|
|
e = glyph_cache_insert (key);
|
|
if (!e) return NULL;
|
|
e->width = e->height = 0;
|
|
e->advance_x = adv;
|
|
return e;
|
|
}
|
|
|
|
/* Atlas row packing. */
|
|
if (g_atlas_next_x + bw > GL_ATLAS_W)
|
|
{
|
|
g_atlas_next_x = 0;
|
|
g_atlas_next_y += g_atlas_row_h + 1;
|
|
g_atlas_row_h = 0;
|
|
}
|
|
if (g_atlas_next_y + bh > GL_ATLAS_H)
|
|
/* 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);
|
|
glTexSubImage2D (GL_TEXTURE_2D, 0, g_atlas_next_x, g_atlas_next_y,
|
|
bw, bh, GL_RED, GL_UNSIGNED_BYTE, g->bitmap.buffer);
|
|
|
|
e = glyph_cache_insert (key);
|
|
if (!e) { cairo_ft_scaled_font_unlock_face (sf); return NULL; }
|
|
e->atlas_x = g_atlas_next_x;
|
|
e->atlas_y = g_atlas_next_y;
|
|
e->width = bw;
|
|
e->height = bh;
|
|
e->bearing_x = bx;
|
|
e->bearing_y = by; /* distance from box top to baseline */
|
|
e->advance_x = adv;
|
|
|
|
g_atlas_next_x += bw + 1;
|
|
if (bh > g_atlas_row_h) g_atlas_row_h = bh;
|
|
|
|
cairo_ft_scaled_font_unlock_face (sf);
|
|
return e;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Vtable ops. */
|
|
|
|
static bool
|
|
gl_drv_frame_ready (struct frame *f)
|
|
{
|
|
return gl_get_frame_data (f) != NULL;
|
|
}
|
|
|
|
static void
|
|
gl_drv_begin_frame (struct frame *f)
|
|
{
|
|
struct gl_frame_data *fd = gl_get_frame_data (f);
|
|
if (!fd) return;
|
|
/* Render the FBO under whatever surface is already current (the window
|
|
surface persists across frames); only bind if nothing is current yet.
|
|
This drops the per-frame surfaceless<->window make-current churn. */
|
|
if (!g_bound_known) gl_bind_surface (EGL_NO_SURFACE);
|
|
double sc = gl_frame_scale (f);
|
|
if (sc != g_atlas_scale)
|
|
{
|
|
/* The atlas holds physical-size bitmaps; a scale change makes the
|
|
cached glyphs the wrong size, so drop both caches. */
|
|
g_atlas_scale = sc;
|
|
gl_flush_glyph_caches ();
|
|
}
|
|
fd->scale = sc;
|
|
if (getenv ("GL_LOG_SCALE"))
|
|
fprintf (stderr, "[glscale] scale=%.2f logical=%dx%d physical=%dx%d\n",
|
|
sc, FRAME_PIXEL_WIDTH (f), FRAME_PIXEL_HEIGHT (f),
|
|
(int) (FRAME_PIXEL_WIDTH (f) * sc),
|
|
(int) (FRAME_PIXEL_HEIGHT (f) * sc));
|
|
int w = (int) (FRAME_PIXEL_WIDTH (f) * fd->scale);
|
|
int h = (int) (FRAME_PIXEL_HEIGHT (f) * fd->scale);
|
|
/* The dirty box is NOT reset here: it accumulates "changes since the
|
|
last swap" across cycles (a deferred present spans several) and the
|
|
present consumes it. gl_ensure_target sets dirty.all when it clears
|
|
a new or resized target, which must be presented whole. */
|
|
gl_ensure_target (fd, w, h); /* LOAD: clears only if new/resized */
|
|
glBindFramebuffer (GL_FRAMEBUFFER, fd->fbo);
|
|
glViewport (0, 0, fd->w, fd->h);
|
|
fd->clip_on = false;
|
|
glDisable (GL_SCISSOR_TEST);
|
|
/* Start the cycle with an empty glyph batch. */
|
|
g_glyph_batch_verts = 0;
|
|
g_glyph_batch_fd = NULL;
|
|
fd->in_cycle = true;
|
|
g_cur = fd;
|
|
}
|
|
|
|
/* Monotonic seconds, for the buffer-switch cross-fade timing. */
|
|
static double
|
|
gl_now (void)
|
|
{
|
|
struct timespec ts;
|
|
clock_gettime (CLOCK_MONOTONIC, &ts);
|
|
return (double) ts.tv_sec + (double) ts.tv_nsec / 1e9;
|
|
}
|
|
|
|
/* Blit the finished static texture to the frame's on-screen X window and
|
|
swap. A no-op on the surfaceless fallback display (no window surfaces);
|
|
the FBO still holds the frame for gl_capture_frame either way. */
|
|
static void
|
|
gl_present_to_window (struct gl_frame_data *fd)
|
|
{
|
|
if (!g_dpy_is_x11) return;
|
|
struct frame *f = fd->f;
|
|
if (!f || !FRAME_X_P (f)) return;
|
|
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)
|
|
{
|
|
if (fd->surf != EGL_NO_SURFACE)
|
|
{
|
|
if (g_bound_known && g_bound_surf == fd->surf)
|
|
{ eglMakeCurrent (g_dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, g_ctx);
|
|
g_bound_surf = EGL_NO_SURFACE; }
|
|
eglDestroySurface (g_dpy, fd->surf);
|
|
}
|
|
fd->surf = eglCreateWindowSurface (g_dpy, g_cfg,
|
|
(EGLNativeWindowType) win, NULL);
|
|
fd->surf_win = (unsigned long) win;
|
|
if (fd->surf == EGL_NO_SURFACE)
|
|
return; /* present unavailable; FBO still has the frame */
|
|
/* A fresh surface has no usable swap history: poison the ring so the
|
|
age-based repair falls back to full blits until it refills. */
|
|
for (int i = 0; i < GL_SWAP_RING; i++)
|
|
{ gl_dirty_clear (&fd->swap_dirty[i]); fd->swap_dirty[i].all = true; }
|
|
fd->surf_w = fd->surf_h = -1; /* size unknown: query below */
|
|
}
|
|
|
|
if (!gl_bind_surface (fd->surf))
|
|
return;
|
|
|
|
/* A window-manager resize reaches the server and reallocates the EGL
|
|
buffers asynchronously; Mesa can hand us a pre-resize size AND a
|
|
pre-resize buffer age for a present that actually lands on a fresh
|
|
(zero-filled) buffer -- a partial blit then leaves visible garbage
|
|
for a frame (caught live by GL_VERIFY_PRESENT: 41% of the buffer
|
|
wrong on a maximize, with age=2). The client cannot win that race
|
|
beforehand, but right after the swap Mesa HAS validated the new
|
|
geometry, so: present, re-query the size, and if it changed under
|
|
us, immediately present again in full at the true size. Bounded to
|
|
one retry; the steady state pays one cached-state query per swap. */
|
|
int size_retry = 0;
|
|
retry_present:;
|
|
|
|
/* Swap interval. Vsync on by default: the blocking swap is the tear
|
|
protection on a bare X server, and on radeonsi it also keeps the
|
|
swapchain rotation tight -- running interval 0 under a compositor
|
|
was tried (the compositor already guarantees tear-free output) and
|
|
the loose buffer rotation brought back the stale-frame flashes the
|
|
12ms present throttle had fixed (an ancient pre-resize buffer shown
|
|
for one vblank during fades). Swap blocking no longer starves the
|
|
Lisp timers either, now that the unified animation pump issues at
|
|
most one present per tick instead of one per subsystem.
|
|
GL_NO_VSYNC=1 frees the swap from the vblank (throughput
|
|
benchmarks); GL_FORCE_VSYNC=1 reserved for symmetry. */
|
|
{
|
|
static int vsync = -1;
|
|
if (vsync == -1)
|
|
vsync = getenv ("GL_NO_VSYNC") ? 0 : 1;
|
|
eglSwapInterval (g_dpy, vsync);
|
|
}
|
|
|
|
/* The video / cursor-effect / cross-fade overlays paint straight onto
|
|
the back buffer after the blit, so their pixels are not in the FBO:
|
|
any present involving an overlay blits and records full. An idle
|
|
burst-mode cursor (no live particles) emits nothing and does not
|
|
count, so plain typing under the default effect still goes partial. */
|
|
bool overlay = (fd->trans_dur > 0 || gl_anim_overlay_active (fd));
|
|
#ifdef HAVE_GSTREAMER
|
|
overlay = overlay || fd->video != NULL;
|
|
#endif
|
|
|
|
/* Partial presents (blit only what the aged back buffer is missing,
|
|
hand the compositor damage rectangles) are OPT-IN, experimental:
|
|
GL_PARTIAL_PRESENT=1. They buy ~10% on workloads that already run
|
|
above a thousand frames per second, and they cost correctness fights
|
|
on three asynchronous fronts at once -- the driver's buffer rotation
|
|
(ages observed lying across reallocations), the window manager's
|
|
resizes, and the compositor's damage-driven texture updates. The
|
|
default present is the robust one: blit the whole frame, swap with
|
|
full damage. A full blit of a laptop-sized frame is a fraction of a
|
|
millisecond of pure GPU copy; stability is worth far more. */
|
|
static int partial_enabled = -1, log_present = -1;
|
|
if (partial_enabled == -1)
|
|
{
|
|
partial_enabled = (getenv ("GL_PARTIAL_PRESENT")
|
|
&& !getenv ("GL_NO_DAMAGE")) ? 1 : 0;
|
|
log_present = getenv ("GL_LOG_PRESENT") ? 1 : 0;
|
|
}
|
|
static unsigned long present_seq;
|
|
present_seq++;
|
|
|
|
/* Buffer age: how many swaps ago this back buffer was last presented
|
|
(0 = new/reallocated/unknown). Queried after make-current, before
|
|
any rendering to the buffer, as the extension requires. Always
|
|
queried, not just on partial-eligible presents: an age of 0 is also
|
|
how we learn the buffer was REALLOCATED, which is what happens when
|
|
the window manager resizes the window before Emacs has processed the
|
|
ConfigureNotify (the FBO still has the old size). */
|
|
EGLint age = 0;
|
|
if (g_has_buffer_age && partial_enabled)
|
|
eglQuerySurface (g_dpy, fd->surf, EGL_BUFFER_AGE_EXT, &age);
|
|
|
|
/* Surface size. Re-queried when the cache disagrees with the FBO (a
|
|
resize Emacs has already processed), when the buffer age says this
|
|
buffer is fresh (a resize Emacs has NOT processed yet reallocates
|
|
the buffers; blitting the stale size would leave undefined -- often
|
|
black -- bands beside old content until the next redisplay), and on
|
|
every present bound for a full blit anyway (overlay frames, whole-
|
|
frame changes, no age extension): those are the rare paths, and a
|
|
full blit with a stale size would not cover a just-resized window.
|
|
Only the steady partial path trusts the cache, and there age >= 1
|
|
vouches that the buffer (hence its size) is unchanged. */
|
|
if (fd->surf_w != fd->w || fd->surf_h != fd->h
|
|
|| age == 0 || overlay || fd->dirty.all
|
|
|| !g_has_buffer_age || !partial_enabled)
|
|
{
|
|
EGLint qw = fd->w, qh = fd->h;
|
|
eglQuerySurface (g_dpy, fd->surf, EGL_WIDTH, &qw);
|
|
eglQuerySurface (g_dpy, fd->surf, EGL_HEIGHT, &qh);
|
|
fd->surf_w = qw;
|
|
fd->surf_h = qh;
|
|
}
|
|
EGLint sw = fd->surf_w, sh = fd->surf_h;
|
|
|
|
/* A partial repair is only meaningful against a buffer whose content
|
|
and size the age vouches for. */
|
|
if (overlay || fd->dirty.all || sw != fd->w || sh != fd->h)
|
|
age = 0;
|
|
|
|
/* Repair set: what this back buffer is missing = the changes since the
|
|
last swap plus everything the (age - 1) swaps in between wrote.
|
|
Unknown age, a too-old buffer, or a full-frame slot in the chain
|
|
falls back to the always-correct full blit. */
|
|
struct gl_dirty_set repair = fd->dirty;
|
|
bool full = true;
|
|
if (age > 0 && age <= GL_SWAP_RING)
|
|
{
|
|
full = false;
|
|
for (int i = 0; i < age - 1 && !repair.all; i++)
|
|
gl_dirty_union (&repair,
|
|
&fd->swap_dirty[(fd->swap_head - 1 - i + GL_SWAP_RING)
|
|
% GL_SWAP_RING]);
|
|
full = repair.all;
|
|
}
|
|
|
|
if (log_present)
|
|
fprintf (stderr, "[glpresent] #%lu t=%.4f fbo=%dx%d window=%dx%d age=%d %s"
|
|
" boxes=%d trans=%.3f cycle=%d\n",
|
|
present_seq, gl_now (), fd->w, fd->h, sw, sh, (int) age,
|
|
full ? "full" : "partial", full ? 1 : repair.n,
|
|
fd->trans_dur > 0 ? gl_now () - fd->trans_start : -1.0,
|
|
(int) fd->in_cycle);
|
|
|
|
/* Both the FBO and the window's default framebuffer use GL bottom-left
|
|
origin, so a straight (unflipped) blit lands right-side-up on screen.
|
|
gl_capture_frame, which targets a top-left PPM, flips; this does not. */
|
|
glBindFramebuffer (GL_READ_FRAMEBUFFER, fd->fbo);
|
|
glBindFramebuffer (GL_DRAW_FRAMEBUFFER, 0);
|
|
glDisable (GL_SCISSOR_TEST);
|
|
if (full)
|
|
glBlitFramebuffer (0, 0, fd->w, fd->h, 0, 0, sw, sh,
|
|
GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
|
else
|
|
for (int i = 0; i < repair.n; i++)
|
|
{
|
|
const int *b = repair.b[i];
|
|
glBlitFramebuffer (b[0], b[1], b[2], b[3], b[0], b[1], b[2], b[3],
|
|
GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
|
}
|
|
/* repair.n == 0: nothing changed since this buffer was shown; just swap. */
|
|
|
|
/* GL_VERIFY_PRESENT=1: read the repaired back buffer and the FBO back
|
|
and compare, BEFORE the overlays (which legitimately diverge). Any
|
|
mismatch is a partial-repair hole -- the exact source of "stale or
|
|
black rectangle" artifacts -- caught at the present that produced it,
|
|
with its age and box count. Debug-only: two full-frame readbacks per
|
|
present. */
|
|
{
|
|
static int verify = -1;
|
|
if (verify == -1) verify = getenv ("GL_VERIFY_PRESENT") ? 1 : 0;
|
|
if (verify && sw == fd->w && sh == fd->h)
|
|
{
|
|
size_t n = (size_t) fd->w * fd->h * 4;
|
|
unsigned char *back = malloc (n), *fbop = malloc (n);
|
|
if (back && fbop)
|
|
{
|
|
glBindFramebuffer (GL_READ_FRAMEBUFFER, 0);
|
|
glReadPixels (0, 0, fd->w, fd->h, GL_RGBA, GL_UNSIGNED_BYTE, back);
|
|
glBindFramebuffer (GL_READ_FRAMEBUFFER, fd->fbo);
|
|
glReadPixels (0, 0, fd->w, fd->h, GL_RGBA, GL_UNSIGNED_BYTE, fbop);
|
|
size_t bad = 0;
|
|
for (size_t i = 0; i < n; i += 4)
|
|
if (back[i] != fbop[i] || back[i+1] != fbop[i+1]
|
|
|| back[i+2] != fbop[i+2])
|
|
bad++;
|
|
if (bad)
|
|
fprintf (stderr,
|
|
"[glverify] MISMATCH %zu px (age=%d %s boxes=%d)\n",
|
|
bad, (int) age, full ? "full" : "partial",
|
|
full ? 1 : repair.n);
|
|
}
|
|
free (back);
|
|
free (fbop);
|
|
glBindFramebuffer (GL_READ_FRAMEBUFFER, fd->fbo);
|
|
}
|
|
}
|
|
|
|
#ifdef HAVE_GSTREAMER
|
|
/* Inline video overlay: draw the latest decoded frame over the static
|
|
content at its rect, clipped to the window interior, so redisplay can
|
|
keep treating the placeholder area as ordinary buffer background.
|
|
Mirrors MtlFrameData -compositeToScreen. */
|
|
gl_video_overlay (fd, sw, sh);
|
|
#endif
|
|
|
|
/* Cursor animation overlay (spring/trail/particles). Drawn over the
|
|
static content, like the Metal compositor's animation pass. */
|
|
if (g_gl_animations_enabled)
|
|
gl_anim_overlay (fd, sw, sh);
|
|
|
|
/* Buffer-switch cross-fade: draw the previous-frame snapshot over the
|
|
just-blitted new content at a fading alpha. The snapshot was copied
|
|
from the FBO (bottom-left), so the quad samples it with v flipped to
|
|
match the default framebuffer. */
|
|
if (fd->trans_dur > 0 && fd->trans_tex
|
|
&& fd->trans_w == fd->w && fd->trans_h == fd->h)
|
|
{
|
|
double p = (gl_now () - fd->trans_start) / fd->trans_dur;
|
|
if (p >= 1.0)
|
|
fd->trans_dur = 0; /* fade complete */
|
|
else
|
|
{
|
|
float a = (float) (1.0 - p);
|
|
a = a * a * (3.0f - 2.0f * a); /* smoothstep fade-out */
|
|
float fw = (float) sw, fh = (float) sh;
|
|
float v[] = {
|
|
0,0, 0,1, fw,0, 1,1, fw,fh, 1,0,
|
|
0,0, 0,1, fw,fh, 1,0, 0,fh, 0,0,
|
|
};
|
|
glViewport (0, 0, sw, sh);
|
|
glUseProgram (g_prog_image);
|
|
glUniform2f (g_u_image_size, fw, fh);
|
|
glActiveTexture (GL_TEXTURE0);
|
|
glBindTexture (GL_TEXTURE_2D, fd->trans_tex);
|
|
glUniform1i (g_u_image_tex, 0);
|
|
glUniform1f (g_u_image_alpha, a);
|
|
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, 16, (void *) 0);
|
|
glEnableVertexAttribArray (1);
|
|
glVertexAttribPointer (1, 2, GL_FLOAT, GL_FALSE, 16, (void *) 8);
|
|
glBlendFunc (GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
|
|
glDrawArrays (GL_TRIANGLES, 0, 6);
|
|
glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
|
}
|
|
}
|
|
|
|
/* Swap, handing the compositor the regions that differ from what is on
|
|
screen (the current frame's changes, not the age repairs: those match
|
|
the screen already). The compositor then recomposites only those
|
|
bands. Zero rects means full damage, which is also the fallback
|
|
without the extension. GL_PRESENT_FINISH=1 drains the GPU first
|
|
(diagnostic knob for present-order races in the GL stack). */
|
|
{
|
|
static int finish = -1;
|
|
if (finish == -1) finish = getenv ("GL_PRESENT_FINISH") ? 1 : 0;
|
|
if (finish) glFinish ();
|
|
}
|
|
EGLBoolean swapped;
|
|
if (partial_enabled && g_swap_damage && !overlay && !fd->dirty.all
|
|
&& fd->dirty.n > 0 && sw == fd->w && sh == fd->h)
|
|
{
|
|
EGLint rects[GL_DIRTY_MAX * 4];
|
|
for (int i = 0; i < fd->dirty.n; i++)
|
|
{
|
|
const int *b = fd->dirty.b[i];
|
|
rects[i * 4 + 0] = b[0];
|
|
rects[i * 4 + 1] = b[1];
|
|
rects[i * 4 + 2] = b[2] - b[0];
|
|
rects[i * 4 + 3] = b[3] - b[1];
|
|
}
|
|
swapped = g_swap_damage (g_dpy, fd->surf, rects, fd->dirty.n);
|
|
}
|
|
else
|
|
swapped = eglSwapBuffers (g_dpy, fd->surf);
|
|
|
|
/* A failed swap presented nothing: keep the dirty set accumulating and
|
|
leave the ring alone, or it would desynchronize from the driver's
|
|
buffer rotation and future repairs would index the wrong slots
|
|
(stale content on screen). The next present retries. */
|
|
if (!swapped)
|
|
{
|
|
glBindFramebuffer (GL_FRAMEBUFFER, fd->fbo);
|
|
return;
|
|
}
|
|
|
|
/* Record what this swap changed, so future presents can repair an aged
|
|
back buffer, and start a fresh dirty set: everything accumulated so
|
|
far is on screen now. Overlay pixels live outside the FBO, so an
|
|
overlay frame is recorded as full. */
|
|
fd->swap_dirty[fd->swap_head] = fd->dirty;
|
|
if (overlay)
|
|
fd->swap_dirty[fd->swap_head].all = true;
|
|
fd->swap_head = (fd->swap_head + 1) % GL_SWAP_RING;
|
|
gl_dirty_clear (&fd->dirty);
|
|
fd->last_swap = gl_now ();
|
|
|
|
/* Post-swap size recheck (see the comment at retry_present): if the
|
|
surface turns out to have resized under this present, what just went
|
|
on screen is partially undefined -- republish everything at the true
|
|
size right now instead of leaving the artifact up until the next
|
|
redisplay. */
|
|
{
|
|
EGLint qw = sw, qh = sh;
|
|
eglQuerySurface (g_dpy, fd->surf, EGL_WIDTH, &qw);
|
|
eglQuerySurface (g_dpy, fd->surf, EGL_HEIGHT, &qh);
|
|
if ((qw != sw || qh != sh) && size_retry++ == 0)
|
|
{
|
|
fd->surf_w = qw;
|
|
fd->surf_h = qh;
|
|
fd->dirty.all = true;
|
|
goto retry_present;
|
|
}
|
|
}
|
|
|
|
/* Keep the window surface current (FBO rendering does not care which
|
|
surface is bound), so the next frame needs no make-current at all. */
|
|
glBindFramebuffer (GL_FRAMEBUFFER, fd->fbo);
|
|
}
|
|
|
|
static void
|
|
gl_drv_end_frame (struct frame *f, bool present_p)
|
|
{
|
|
struct gl_frame_data *fd = gl_get_frame_data (f);
|
|
if (!fd) return;
|
|
gl_flush_glyph_batch (); /* drain any quads left from the last string */
|
|
fd->in_cycle = false;
|
|
g_cur = NULL;
|
|
if (present_p)
|
|
{
|
|
/* The swap in the present flushes; no explicit glFlush needed. */
|
|
gl_present_to_window (fd);
|
|
fd->needs_present = false;
|
|
}
|
|
else
|
|
{
|
|
/* Deferred: start the GPU on the frame now so the work overlaps
|
|
the wait until flush_display presents it. */
|
|
glFlush ();
|
|
fd->needs_present = true;
|
|
}
|
|
}
|
|
|
|
static void
|
|
gl_drv_present (struct frame *f)
|
|
{
|
|
struct gl_frame_data *fd = gl_get_frame_data (f);
|
|
if (!fd) return;
|
|
gl_present_to_window (fd);
|
|
fd->needs_present = false;
|
|
}
|
|
|
|
static bool
|
|
gl_drv_in_cycle (struct frame *f)
|
|
{
|
|
struct gl_frame_data *fd = gl_get_frame_data (f);
|
|
return fd && fd->in_cycle;
|
|
}
|
|
|
|
static bool
|
|
gl_drv_pending_present (struct frame *f)
|
|
{
|
|
struct gl_frame_data *fd = gl_get_frame_data (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)
|
|
{
|
|
struct gl_frame_data *fd = gl_get_frame_data (s->f);
|
|
if (!fd) return;
|
|
NativeRectangle r;
|
|
get_glyph_string_clip_rect (s, &r);
|
|
fd->clip_on = true;
|
|
fd->clip_x = r.x;
|
|
fd->clip_y = r.y;
|
|
fd->clip_w = r.width;
|
|
fd->clip_h = r.height;
|
|
}
|
|
|
|
static void
|
|
gl_drv_clear_clip (struct frame *f)
|
|
{
|
|
struct gl_frame_data *fd = gl_get_frame_data (f);
|
|
if (!fd) return;
|
|
fd->clip_on = false;
|
|
}
|
|
|
|
static void
|
|
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 || 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_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
|
|
gl_drv_copy_region (struct frame *f, int x, int y, int w, int h,
|
|
int dst_x, int dst_y)
|
|
{
|
|
struct gl_frame_data *fd = gl_get_frame_data (f);
|
|
if (!fd || w <= 0 || h <= 0) return;
|
|
gl_flush_glyph_batch (); /* the blit must see already-drawn glyphs */
|
|
gl_mark_dirty (fd, dst_x, dst_y, w, h);
|
|
if (getenv ("GL_LOG_SCROLL"))
|
|
fprintf (stderr, "[glscroll] copy %dx%d from (%d,%d) to (%d,%d)\n",
|
|
w, h, x, y, dst_x, dst_y);
|
|
double s = fd->scale;
|
|
int sx = (int) (x * s), sy = (int) (y * s);
|
|
int dx = (int) (dst_x * s), dy = (int) (dst_y * s);
|
|
int sw = (int) (w * s), sh = (int) (h * s);
|
|
/* Clamp the move to the FBO. During a resize race Emacs can ask for a
|
|
scroll computed against the old frame size; a blit whose SOURCE rect
|
|
leaves the read buffer writes UNDEFINED pixels (on radeonsi: recycled
|
|
VRAM, i.e. fragments of old frames) into the destination. Trim both
|
|
rects by the same amount so the copy stays a pure translation. */
|
|
{
|
|
int lo = sx < dx ? sx : dx; /* leftmost edge of either rect */
|
|
if (lo < 0) { sx -= lo; dx -= lo; sw += lo; }
|
|
lo = sy < dy ? sy : dy;
|
|
if (lo < 0) { sy -= lo; dy -= lo; sh += lo; }
|
|
int hi = sx > dx ? sx : dx; /* rightmost start of either rect */
|
|
if (hi + sw > fd->w) sw = fd->w - hi;
|
|
hi = sy > dy ? sy : dy;
|
|
if (hi + sh > fd->h) sh = fd->h - hi;
|
|
if (sw <= 0 || sh <= 0) return;
|
|
}
|
|
int sy0 = fd->h - (sy + sh), dy0 = fd->h - (dy + sh);
|
|
glDisable (GL_SCISSOR_TEST);
|
|
if (dx >= sx + sw || sx >= dx + sw || dy0 >= sy0 + sh || sy0 >= dy0 + sh)
|
|
{
|
|
/* Source and destination are disjoint (page scrolls, large jumps):
|
|
one direct blit within the FBO is legal and halves the bandwidth.
|
|
Only OVERLAPPING blits are undefined in ES 3. */
|
|
glBindFramebuffer (GL_READ_FRAMEBUFFER, fd->fbo);
|
|
glBindFramebuffer (GL_DRAW_FRAMEBUFFER, fd->fbo);
|
|
glBlitFramebuffer (sx, sy0, sx + sw, sy0 + sh,
|
|
dx, dy0, dx + sw, dy0 + sh,
|
|
GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
|
}
|
|
else
|
|
{
|
|
/* Overlapping move (single-line scrolls): bounce through the
|
|
scratch target so the copy is safe. */
|
|
glBindFramebuffer (GL_READ_FRAMEBUFFER, fd->fbo);
|
|
glBindFramebuffer (GL_DRAW_FRAMEBUFFER, fd->scratch_fbo);
|
|
glBlitFramebuffer (sx, sy0, sx + sw, sy0 + sh,
|
|
sx, sy0, sx + sw, sy0 + sh,
|
|
GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
|
glBindFramebuffer (GL_READ_FRAMEBUFFER, fd->scratch_fbo);
|
|
glBindFramebuffer (GL_DRAW_FRAMEBUFFER, fd->fbo);
|
|
glBlitFramebuffer (sx, sy0, sx + sw, sy0 + sh,
|
|
dx, dy0, dx + sw, dy0 + sh,
|
|
GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
|
}
|
|
glBindFramebuffer (GL_FRAMEBUFFER, fd->fbo);
|
|
}
|
|
|
|
static bool
|
|
gl_drv_font_ready_p (struct font *font)
|
|
{
|
|
if (!font) return false;
|
|
struct font_info *fi = (struct font_info *) font;
|
|
return fi->cr_scaled_font != NULL;
|
|
}
|
|
|
|
static struct gfx_glyph *
|
|
gl_drv_get_glyph (struct font *font, unsigned int glyph_id)
|
|
{
|
|
if (!font || glyph_id == 0) return NULL;
|
|
unsigned long long key =
|
|
((unsigned long long) (uintptr_t) font * 6364136223846793005ULL)
|
|
^ (unsigned long long) glyph_id;
|
|
struct gfx_glyph *e = glyph_cache_lookup (key);
|
|
if (e) return e;
|
|
return gl_rasterize_glyph (font, glyph_id, key);
|
|
}
|
|
|
|
/* 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);
|
|
glBindTexture (GL_TEXTURE_2D, g_atlas);
|
|
glUniform1i (g_u_glyph_atlas, 0);
|
|
glBindBuffer (GL_ARRAY_BUFFER, g_vbo);
|
|
glBufferData (GL_ARRAY_BUFFER,
|
|
(GLsizeiptr) g_glyph_batch_verts * GL_GLYPH_VERT_FLOATS
|
|
* sizeof (float),
|
|
g_glyph_batch, GL_STREAM_DRAW);
|
|
glEnableVertexAttribArray (0);
|
|
glVertexAttribPointer (0, 2, GL_FLOAT, GL_FALSE, 32, (void *) 0);
|
|
glEnableVertexAttribArray (1);
|
|
glVertexAttribPointer (1, 2, GL_FLOAT, GL_FALSE, 32, (void *) 8);
|
|
glEnableVertexAttribArray (2);
|
|
glVertexAttribPointer (2, 4, GL_FLOAT, GL_FALSE, 32, (void *) 16);
|
|
glDrawArrays (GL_TRIANGLES, 0, g_glyph_batch_verts);
|
|
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_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])
|
|
{
|
|
if (x0 >= x1 || y0 >= y1)
|
|
return; /* degenerate quad: nothing to draw */
|
|
|
|
/* 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;
|
|
|
|
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; }
|
|
}
|
|
|
|
/* 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;
|
|
float *p = realloc (g_glyph_batch,
|
|
(size_t) cap * GL_GLYPH_VERT_FLOATS * sizeof (float));
|
|
if (!p) { gl_flush_glyph_batch (); return; }
|
|
g_glyph_batch = p;
|
|
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},
|
|
};
|
|
memcpy (g_glyph_batch + (size_t) g_glyph_batch_verts * GL_GLYPH_VERT_FLOATS,
|
|
quad, sizeof quad);
|
|
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,
|
|
float alpha);
|
|
|
|
static bool
|
|
gl_drv_color_font_p (struct font *font)
|
|
{
|
|
if (!font) return false;
|
|
cairo_scaled_font_t *sf = NULL;
|
|
FT_Face face = gl_lock_face (font, &sf);
|
|
if (!face) return false;
|
|
bool color = FT_HAS_COLOR (face);
|
|
cairo_ft_scaled_font_unlock_face (sf);
|
|
return color;
|
|
}
|
|
|
|
/* ---- Color glyphs (emoji): cairo-rasterized -> RGBA texture ----
|
|
Color fonts (Noto Color Emoji: CBDT/CBLC bitmap strikes) can't go
|
|
through the R8 coverage atlas. Rasterizing them with raw FreeType would
|
|
yield the native strike (e.g. 136x128 px), NOT the size Emacs laid out;
|
|
cairo's scaled font already carries the pixel_size matrix and scales the
|
|
strike for us (the same path the X/cairo display uses), so we draw the
|
|
glyph through cairo into an ARGB32 surface and upload that as a
|
|
premultiplied RGBA texture, exactly like gl_drv_image_texture. The draw
|
|
box and bearings come from cairo_scaled_font_glyph_extents, which are in
|
|
the laid-out (logical) pixel size. Mirrors mtl_color_glyph. */
|
|
|
|
struct gl_color_glyph
|
|
{
|
|
unsigned long long key;
|
|
GLuint tex;
|
|
int w, h; /* draw box, logical pixels */
|
|
int bearing_x, bearing_y; /* pen -> box left; baseline -> box top */
|
|
float advance_x;
|
|
bool valid;
|
|
};
|
|
#define GL_CGLYPH_CAP 512
|
|
static struct gl_color_glyph g_cglyphs[GL_CGLYPH_CAP];
|
|
static int g_cglyph_count;
|
|
|
|
/* Drop both glyph caches (used on a backing-scale change): the grayscale
|
|
atlas and the color-glyph textures both hold physical-size bitmaps. */
|
|
static void
|
|
gl_flush_glyph_caches (void)
|
|
{
|
|
gl_atlas_reset ();
|
|
for (int i = 0; i < GL_CGLYPH_CAP; i++)
|
|
if (g_cglyphs[i].valid)
|
|
glDeleteTextures (1, &g_cglyphs[i].tex);
|
|
memset (g_cglyphs, 0, sizeof g_cglyphs);
|
|
g_cglyph_count = 0;
|
|
}
|
|
|
|
static struct gl_color_glyph *
|
|
gl_color_glyph_lookup (unsigned long long key)
|
|
{
|
|
unsigned h = (unsigned) (key % GL_CGLYPH_CAP);
|
|
for (int i = 0; i < GL_CGLYPH_CAP; i++)
|
|
{
|
|
struct gl_color_glyph *c = &g_cglyphs[(h + i) % GL_CGLYPH_CAP];
|
|
if (!c->valid) return NULL;
|
|
if (c->key == key) return c;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static struct gl_color_glyph *
|
|
gl_color_glyph_get (struct font *font, unsigned int glyph_id)
|
|
{
|
|
if (!font || glyph_id == 0) return NULL;
|
|
unsigned long long key =
|
|
((unsigned long long) (uintptr_t) font * 6364136223846793005ULL)
|
|
^ (unsigned long long) glyph_id;
|
|
struct gl_color_glyph *c = gl_color_glyph_lookup (key);
|
|
if (c) return c;
|
|
|
|
struct font_info *fi = (struct font_info *) font;
|
|
cairo_scaled_font_t *sf = fi->cr_scaled_font;
|
|
if (!sf) return NULL;
|
|
|
|
/* Laid-out metrics at the font's pixel_size (cairo scales the strike). */
|
|
cairo_glyph_t cg = { glyph_id, 0, 0 };
|
|
cairo_text_extents_t ext;
|
|
cairo_scaled_font_glyph_extents (sf, &cg, 1, &ext);
|
|
if (ext.width <= 0 || ext.height <= 0) return NULL;
|
|
|
|
int bw = (int) ceil (ext.width) + 2;
|
|
int bh = (int) ceil (ext.height) + 2;
|
|
int bx = (int) floor (ext.x_bearing); /* pen -> box left (logical) */
|
|
int by = (int) floor (-ext.y_bearing); /* baseline -> box top (logical) */
|
|
|
|
if (getenv ("GL_LOG_CGLYPH"))
|
|
fprintf (stderr, "[cglyph] gid=%u box=%dx%d bear=%d,%d adv=%.1f\n",
|
|
glyph_id, bw, bh, bx, by, (double) ext.x_advance);
|
|
|
|
/* HiDPI: the draw box and bearings stay logical (gl_drv_draw_texture
|
|
maps logical -> FBO by * scale), but the texture is rasterized at
|
|
physical resolution so the emoji is crisp. At scale 1 this is the
|
|
proven 1x path (sf2 == sf, physical dims == logical). */
|
|
double s = g_atlas_scale;
|
|
int pw = (int) ceil (bw * s);
|
|
int ph = (int) ceil (bh * s);
|
|
cairo_scaled_font_t *sf2 = sf;
|
|
if (s != 1.0)
|
|
{
|
|
cairo_font_face_t *ff = cairo_scaled_font_get_font_face (sf);
|
|
cairo_matrix_t fm, ctm;
|
|
cairo_scaled_font_get_font_matrix (sf, &fm);
|
|
cairo_scaled_font_get_ctm (sf, &ctm);
|
|
cairo_font_options_t *opts = cairo_font_options_create ();
|
|
cairo_scaled_font_get_font_options (sf, opts);
|
|
cairo_matrix_scale (&fm, s, s);
|
|
sf2 = cairo_scaled_font_create (ff, &fm, &ctm, opts);
|
|
cairo_font_options_destroy (opts);
|
|
if (!sf2 || cairo_scaled_font_status (sf2) != CAIRO_STATUS_SUCCESS)
|
|
{ if (sf2) cairo_scaled_font_destroy (sf2); return NULL; }
|
|
}
|
|
|
|
/* Render the glyph through cairo at the box origin (-x_bearing, -y_bearing
|
|
+ 1px pad, in physical pixels), so the bbox lands inside the surface.
|
|
cairo paints the embedded color for CBDT/COLR glyphs. */
|
|
cairo_surface_t *surf = cairo_image_surface_create (CAIRO_FORMAT_ARGB32,
|
|
pw, ph);
|
|
if (cairo_surface_status (surf) != CAIRO_STATUS_SUCCESS)
|
|
{
|
|
cairo_surface_destroy (surf);
|
|
if (sf2 != sf) cairo_scaled_font_destroy (sf2);
|
|
return NULL;
|
|
}
|
|
cairo_t *cr = cairo_create (surf);
|
|
cairo_set_scaled_font (cr, sf2);
|
|
cairo_set_source_rgb (cr, 0, 0, 0);
|
|
cairo_glyph_t place = { glyph_id, (-bx + 1) * s, (by + 1) * s };
|
|
cairo_show_glyphs (cr, &place, 1);
|
|
cairo_destroy (cr);
|
|
cairo_surface_flush (surf);
|
|
if (sf2 != sf) cairo_scaled_font_destroy (sf2);
|
|
|
|
unsigned char *data = cairo_image_surface_get_data (surf);
|
|
int stride = cairo_image_surface_get_stride (surf);
|
|
if (!data) { cairo_surface_destroy (surf); return NULL; }
|
|
|
|
GLuint tex;
|
|
glGenTextures (1, &tex);
|
|
glBindTexture (GL_TEXTURE_2D, tex);
|
|
glPixelStorei (GL_UNPACK_ALIGNMENT, 4);
|
|
glPixelStorei (GL_UNPACK_ROW_LENGTH, stride / 4);
|
|
glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA8, pw, ph, 0, GL_RGBA,
|
|
GL_UNSIGNED_BYTE, data);
|
|
glPixelStorei (GL_UNPACK_ROW_LENGTH, 0);
|
|
/* cairo ARGB32 is little-endian B,G,R,A: swap R and B for straight RGBA. */
|
|
glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_BLUE);
|
|
glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, GL_RED);
|
|
glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
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);
|
|
cairo_surface_destroy (surf);
|
|
|
|
if (g_cglyph_count * 4 >= GL_CGLYPH_CAP * 3)
|
|
{
|
|
for (int i = 0; i < GL_CGLYPH_CAP; i++)
|
|
if (g_cglyphs[i].valid) glDeleteTextures (1, &g_cglyphs[i].tex);
|
|
memset (g_cglyphs, 0, sizeof g_cglyphs);
|
|
g_cglyph_count = 0;
|
|
}
|
|
unsigned hh = (unsigned) (key % GL_CGLYPH_CAP);
|
|
for (int i = 0; i < GL_CGLYPH_CAP; i++)
|
|
{
|
|
struct gl_color_glyph *e = &g_cglyphs[(hh + i) % GL_CGLYPH_CAP];
|
|
if (!e->valid)
|
|
{
|
|
e->valid = true; e->key = key; e->tex = tex;
|
|
/* The 1px pad shifts the bbox down-right by 1; fold it into the
|
|
bearings so the on-screen origin still lands correctly. */
|
|
e->w = bw; e->h = bh;
|
|
e->bearing_x = bx - 1; e->bearing_y = by + 1;
|
|
e->advance_x = (float) ext.x_advance;
|
|
g_cglyph_count++;
|
|
return e;
|
|
}
|
|
}
|
|
glDeleteTextures (1, &tex);
|
|
return NULL;
|
|
}
|
|
|
|
static float
|
|
gl_drv_draw_color_glyph (struct frame *f, struct font *font,
|
|
unsigned int glyph_id, float x, float ybase)
|
|
{
|
|
struct gl_color_glyph *c = gl_color_glyph_get (font, glyph_id);
|
|
if (!c) return 0.0f;
|
|
/* Metrics are logical pixels; gl_drv_draw_texture maps logical -> FBO. */
|
|
gl_drv_draw_texture (f, (void *) (uintptr_t) c->tex,
|
|
x + c->bearing_x, ybase - c->bearing_y,
|
|
(float) c->w, (float) c->h,
|
|
0.0f, 0.0f, 1.0f, 1.0f, 1.0f);
|
|
return c->advance_x;
|
|
}
|
|
|
|
static void
|
|
gl_drv_warm_glyph_cache (struct frame *f)
|
|
{
|
|
struct face *face = FACE_FROM_ID_OR_NULL (f, DEFAULT_FACE_ID);
|
|
if (!face || !face->font) return;
|
|
struct font *font = face->font;
|
|
if (!font->driver || !font->driver->encode_char) return;
|
|
for (int c = 32; c < 127; c++)
|
|
{
|
|
unsigned int gid = font->driver->encode_char (font, c);
|
|
if (gid != FONT_INVALID_CODE && gid != 0)
|
|
gl_drv_get_glyph (font, gid);
|
|
}
|
|
}
|
|
|
|
/* Image texture cache: keyed by the `struct image *' pointer, VALIDATED
|
|
by the image's spec hash and display size. Pointer identity alone is
|
|
not enough: Emacs's image cache frees evicted images and the allocator
|
|
can hand the same address to a different image later -- a stale hit
|
|
would draw the old picture (a long-gone dashboard banner, say) in the
|
|
new image's place. The spec hash pins the entry to its image. */
|
|
#define GL_IMG_CAP 256
|
|
struct gl_img_entry
|
|
{
|
|
struct image *img;
|
|
EMACS_UINT hash;
|
|
GLuint tex;
|
|
int w, h;
|
|
};
|
|
static struct gl_img_entry g_imgs[GL_IMG_CAP];
|
|
|
|
static struct gl_img_entry *
|
|
gl_img_slot (struct image *img)
|
|
{
|
|
struct gl_img_entry *free_slot = NULL;
|
|
for (int i = 0; i < GL_IMG_CAP; i++)
|
|
{
|
|
if (g_imgs[i].img == img) return &g_imgs[i];
|
|
if (!free_slot && g_imgs[i].img == NULL) free_slot = &g_imgs[i];
|
|
}
|
|
return free_slot;
|
|
}
|
|
|
|
/* Rasterize IMG through cairo at its display size (honoring the pattern
|
|
matrix that image.c set for :scale / :rotation / flips, exactly like
|
|
x_cr_draw_image), upload it to a GL texture, and cache it. The cairo
|
|
surface is premultiplied ARGB32; we upload it as RGBA8 with a B<->R
|
|
texture swizzle so the sampler returns straight RGBA. Returns the GL
|
|
texture name as an opaque handle; W and H receive the texture size. */
|
|
static void *
|
|
gl_drv_image_texture (struct frame *f, struct image *img, int *w, int *h)
|
|
{
|
|
if (w) *w = 0;
|
|
if (h) *h = 0;
|
|
if (!img) return NULL;
|
|
|
|
cairo_pattern_t *pat = (cairo_pattern_t *) img->cr_data;
|
|
if (!pat || cairo_pattern_get_type (pat) != CAIRO_PATTERN_TYPE_SURFACE)
|
|
return NULL;
|
|
|
|
int W = img->width > 0 ? img->width : 1;
|
|
int H = img->height > 0 ? img->height : 1;
|
|
|
|
struct gl_img_entry *e = gl_img_slot (img);
|
|
if (e && e->img == img && e->hash == img->hash
|
|
&& e->tex && e->w == W && e->h == H)
|
|
{
|
|
if (w) *w = W;
|
|
if (h) *h = H;
|
|
return (void *) (uintptr_t) e->tex;
|
|
}
|
|
|
|
/* Paint the prepared pattern over an opaque-free ARGB32 surface, the
|
|
same primitive x_cr_draw_image uses (set_source + fill), with dest
|
|
and src both at the origin so the full display-size image lands in
|
|
the surface. No background fill: the policy already painted the cell
|
|
background, and we composite this over it with OVER. */
|
|
cairo_surface_t *surf =
|
|
cairo_image_surface_create (CAIRO_FORMAT_ARGB32, W, H);
|
|
if (cairo_surface_status (surf) != CAIRO_STATUS_SUCCESS)
|
|
{ cairo_surface_destroy (surf); return NULL; }
|
|
cairo_t *cr = cairo_create (surf);
|
|
cairo_rectangle (cr, 0, 0, W, H);
|
|
cairo_set_source (cr, pat);
|
|
cairo_fill (cr);
|
|
cairo_destroy (cr);
|
|
cairo_surface_flush (surf);
|
|
|
|
unsigned char *data = cairo_image_surface_get_data (surf);
|
|
int stride = cairo_image_surface_get_stride (surf);
|
|
if (!data) { cairo_surface_destroy (surf); return NULL; }
|
|
|
|
GLuint tex = (e && e->tex) ? e->tex : 0;
|
|
if (!tex) glGenTextures (1, &tex);
|
|
glBindTexture (GL_TEXTURE_2D, tex);
|
|
glPixelStorei (GL_UNPACK_ALIGNMENT, 4);
|
|
glPixelStorei (GL_UNPACK_ROW_LENGTH, stride / 4);
|
|
glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA8, W, H, 0, GL_RGBA,
|
|
GL_UNSIGNED_BYTE, data);
|
|
glPixelStorei (GL_UNPACK_ROW_LENGTH, 0);
|
|
/* cairo ARGB32 is little-endian B,G,R,A in memory: swap R and B so the
|
|
sampler returns straight RGBA. */
|
|
glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_BLUE);
|
|
glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, GL_RED);
|
|
/* Filtering: match the cairo pattern (BEST -> linear, NEAREST). */
|
|
GLint filt = (cairo_pattern_get_filter (pat) == CAIRO_FILTER_NEAREST)
|
|
? GL_NEAREST : GL_LINEAR;
|
|
glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filt);
|
|
glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filt);
|
|
glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
|
|
cairo_surface_destroy (surf);
|
|
|
|
if (e)
|
|
{ e->img = img; e->hash = img->hash; e->tex = tex; e->w = W; e->h = H; }
|
|
|
|
(void) f;
|
|
if (w) *w = W;
|
|
if (h) *h = H;
|
|
return (void *) (uintptr_t) tex;
|
|
}
|
|
|
|
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, float alpha)
|
|
{
|
|
struct gl_frame_data *fd = gl_get_frame_data (f);
|
|
if (!fd || !texture) return;
|
|
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;
|
|
float v[] = {
|
|
x0,y0, u0,v0, x1,y0, u1,v0, x1,y1, u1,v1,
|
|
x0,y0, u0,v0, x1,y1, u1,v1, x0,y1, u0,v1,
|
|
};
|
|
glUseProgram (g_prog_image);
|
|
glUniform2f (g_u_image_size, (float) fd->w, (float) fd->h);
|
|
glActiveTexture (GL_TEXTURE0);
|
|
glBindTexture (GL_TEXTURE_2D, (GLuint) (uintptr_t) texture);
|
|
glUniform1i (g_u_image_tex, 0);
|
|
glUniform1f (g_u_image_alpha, alpha);
|
|
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, 16, (void *) 0);
|
|
glEnableVertexAttribArray (1);
|
|
glVertexAttribPointer (1, 2, GL_FLOAT, GL_FALSE, 16, (void *) 8);
|
|
/* The texture is premultiplied; composite with cairo's OVER operator,
|
|
then restore the straight-alpha blend the glyph/rect paths expect. */
|
|
glBlendFunc (GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
|
|
glDrawArrays (GL_TRIANGLES, 0, 6);
|
|
glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
|
}
|
|
|
|
static void
|
|
gl_drv_draw_bitmap (struct frame *f, unsigned short *bits, int dh,
|
|
int bw, int wd, int h, int x, int y,
|
|
unsigned long color)
|
|
{
|
|
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 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);
|
|
|
|
/* The bitmap as an R8 coverage texture, cached: a fringe indicator is
|
|
redrawn every time its row updates, with the same handful of
|
|
patterns over and over, so re-uploading per call is pure waste.
|
|
Keyed by an FNV-1a hash of the visible rows plus the box size (a
|
|
64-bit hash over a couple dozen distinct patterns; collisions are
|
|
not a practical concern). The coverage is color-independent (the
|
|
color rides on the vertices), so one texture serves every face.
|
|
|
|
Bit order: the X/cairo backend writes the row straight into a
|
|
CAIRO_FORMAT_A1 surface, where on a little-endian host pixel x is
|
|
bit x (LSB-first) -- the mirror of the macOS/NS driver's MSB-first
|
|
order. Drawn with the glyph program (1-bit coverage, so the gamma
|
|
is a no-op) and NEAREST sampling for hard edges. */
|
|
unsigned long long hash = 1469598103934665603ULL;
|
|
for (int r = 0; r < h; r++)
|
|
{
|
|
hash ^= (unsigned long long) bits[dh + r];
|
|
hash *= 1099511628211ULL;
|
|
}
|
|
hash ^= ((unsigned long long) wd << 32) ^ (unsigned long long) h;
|
|
|
|
#define GL_BITMAP_CAP 64
|
|
static struct { unsigned long long hash; GLuint tex; } cache[GL_BITMAP_CAP];
|
|
static int cache_next;
|
|
GLuint tex = 0;
|
|
for (int i = 0; i < GL_BITMAP_CAP; i++)
|
|
if (cache[i].tex && cache[i].hash == hash)
|
|
{ tex = cache[i].tex; break; }
|
|
if (!tex)
|
|
{
|
|
unsigned char *buf = calloc ((size_t) wd * h, 1);
|
|
if (!buf) return;
|
|
for (int r = 0; r < h; r++)
|
|
{
|
|
unsigned short row = bits[dh + r];
|
|
for (int c = 0; c < wd; c++)
|
|
if ((row >> c) & 1)
|
|
buf[r * wd + c] = 0xFF;
|
|
}
|
|
glGenTextures (1, &tex);
|
|
glBindTexture (GL_TEXTURE_2D, tex);
|
|
glPixelStorei (GL_UNPACK_ALIGNMENT, 1);
|
|
glTexImage2D (GL_TEXTURE_2D, 0, GL_R8, wd, h, 0, GL_RED,
|
|
GL_UNSIGNED_BYTE, buf);
|
|
glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
|
glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
|
glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
free (buf);
|
|
if (cache[cache_next].tex)
|
|
glDeleteTextures (1, &cache[cache_next].tex);
|
|
cache[cache_next].hash = hash;
|
|
cache[cache_next].tex = tex;
|
|
cache_next = (cache_next + 1) % GL_BITMAP_CAP;
|
|
}
|
|
|
|
float s = (float) fd->scale;
|
|
float x0 = x * s, y0 = y * s, x1 = (x + wd) * s, y1 = (y + h) * s;
|
|
float rgba[4];
|
|
gl_unpack_color (color, rgba);
|
|
float rr = rgba[0], gg = rgba[1], bb = rgba[2], a = rgba[3];
|
|
float v[] = {
|
|
x0,y0, 0,0, rr,gg,bb,a, x1,y0, 1,0, rr,gg,bb,a, x1,y1, 1,1, rr,gg,bb,a,
|
|
x0,y0, 0,0, rr,gg,bb,a, x1,y1, 1,1, rr,gg,bb,a, x0,y1, 0,1, rr,gg,bb,a,
|
|
};
|
|
glUseProgram (g_prog_glyph);
|
|
glUniform2f (g_u_glyph_size, (float) fd->w, (float) fd->h);
|
|
glActiveTexture (GL_TEXTURE0);
|
|
glBindTexture (GL_TEXTURE_2D, tex);
|
|
glUniform1i (g_u_glyph_atlas, 0);
|
|
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 (1);
|
|
glVertexAttribPointer (1, 2, GL_FLOAT, GL_FALSE, 32, (void *) 8);
|
|
glEnableVertexAttribArray (2);
|
|
glVertexAttribPointer (2, 4, GL_FLOAT, GL_FALSE, 32, (void *) 16);
|
|
glDrawArrays (GL_TRIANGLES, 0, 6);
|
|
}
|
|
|
|
/* Relief shading. Port of x_alloc_lighter_color (xterm.c): scale the
|
|
base RGB by FACTOR, and for colors darker than the boost limit add an
|
|
extra additive term so dark backgrounds still get a visible relief.
|
|
Working in 16-bit per channel like the X path, then back to 8-bit.
|
|
This matches vanilla X far better than a flat lighten/darken blend,
|
|
which is what the gfxterm fallback would otherwise use. */
|
|
#define GL_RELIEF_DARK_BOOST_LIMIT 48000
|
|
|
|
static unsigned long
|
|
gl_relief_shade (unsigned long base, double factor, int delta)
|
|
{
|
|
long r = ((base >> 16) & 0xff) * 257;
|
|
long g = ((base >> 8) & 0xff) * 257;
|
|
long b = (base & 0xff) * 257;
|
|
long nr = min (0xffff, (long) (factor * r));
|
|
long ng = min (0xffff, (long) (factor * g));
|
|
long nb = min (0xffff, (long) (factor * b));
|
|
long bright = (2 * r + 3 * g + b) / 6;
|
|
if (bright < GL_RELIEF_DARK_BOOST_LIMIT)
|
|
{
|
|
double dimness = 1 - (double) bright / GL_RELIEF_DARK_BOOST_LIMIT;
|
|
int min_delta = (int) (delta * dimness * factor / 2);
|
|
if (factor < 1)
|
|
{
|
|
nr = max (0, nr - min_delta);
|
|
ng = max (0, ng - min_delta);
|
|
nb = max (0, nb - min_delta);
|
|
}
|
|
else
|
|
{
|
|
nr = min (0xffff, nr + min_delta);
|
|
ng = min (0xffff, ng + min_delta);
|
|
nb = min (0xffff, nb + min_delta);
|
|
}
|
|
}
|
|
/* x_alloc_lighter_color's "same color as before" path: when scaling
|
|
leaves the channel unchanged, fall back to an additive delta. */
|
|
if (nr == r && ng == g && nb == b)
|
|
{
|
|
nr = min (0xffff, r + delta);
|
|
ng = min (0xffff, g + delta);
|
|
nb = min (0xffff, b + delta);
|
|
}
|
|
return (((unsigned long) (nr >> 8)) << 16)
|
|
| (((unsigned long) (ng >> 8)) << 8)
|
|
| (unsigned long) (nb >> 8);
|
|
}
|
|
|
|
/* Appearance-aware relief colors, mirroring x_setup_relief_colors: the
|
|
light relief uses factor 1.2 / delta 0x8000, the dark one 0.6 / 0x4000,
|
|
over the face background (or the box color / image background). */
|
|
static void
|
|
gl_drv_relief_colors (struct glyph_string *s, unsigned long *light,
|
|
unsigned long *dark)
|
|
{
|
|
unsigned long base;
|
|
if (s->face->use_box_color_for_shadows_p)
|
|
base = s->face->box_color;
|
|
else if (s->first_glyph->type == IMAGE_GLYPH && s->img && s->img->pixmap
|
|
&& !IMAGE_BACKGROUND_TRANSPARENT (s->img, s->f, 0))
|
|
base = IMAGE_BACKGROUND (s->img, s->f, 0);
|
|
else
|
|
base = s->face->background;
|
|
*light = gl_relief_shade (base, 1.2, 0x8000);
|
|
*dark = gl_relief_shade (base, 0.6, 0x4000);
|
|
}
|
|
|
|
static unsigned long
|
|
gl_drv_frame_foreground (struct frame *f)
|
|
{
|
|
return FRAME_FOREGROUND_PIXEL (f);
|
|
}
|
|
|
|
static unsigned long
|
|
gl_drv_frame_background (struct frame *f)
|
|
{
|
|
return FRAME_BACKGROUND_PIXEL (f);
|
|
}
|
|
|
|
static unsigned long
|
|
gl_drv_cursor_color (struct frame *f)
|
|
{
|
|
return FRAME_X_OUTPUT (f)->cursor_pixel;
|
|
}
|
|
|
|
static struct gfx_driver gl_gfx_driver =
|
|
{
|
|
.name = "opengl",
|
|
.frame_ready = gl_drv_frame_ready,
|
|
.begin_frame = gl_drv_begin_frame,
|
|
.end_frame = gl_drv_end_frame,
|
|
.present = gl_drv_present,
|
|
.in_cycle = gl_drv_in_cycle,
|
|
.pending_present = gl_drv_pending_present,
|
|
.clip_to_glyph_string = gl_drv_clip_to_glyph_string,
|
|
.clear_clip = gl_drv_clear_clip,
|
|
.fill_rect = gl_drv_fill_rect,
|
|
.copy_region = gl_drv_copy_region,
|
|
.font_ready_p = gl_drv_font_ready_p,
|
|
.get_glyph = gl_drv_get_glyph,
|
|
.draw_glyph = gl_drv_draw_glyph,
|
|
.color_font_p = gl_drv_color_font_p,
|
|
.draw_color_glyph = gl_drv_draw_color_glyph,
|
|
.warm_glyph_cache = gl_drv_warm_glyph_cache,
|
|
.image_texture = gl_drv_image_texture,
|
|
.draw_texture = gl_drv_draw_texture,
|
|
.draw_bitmap = gl_drv_draw_bitmap,
|
|
.relief_colors = gl_drv_relief_colors,
|
|
.frame_foreground = gl_drv_frame_foreground,
|
|
.frame_background = gl_drv_frame_background,
|
|
.cursor_color = gl_drv_cursor_color,
|
|
.note_cursor = gl_drv_note_cursor,
|
|
};
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Cursor animation overlay (the MtlAnimator port). Mirrors mtlterm.m:
|
|
spring physics, comet trail and particle bursts, advanced by a Lisp
|
|
timer (gpu-anim-tick) because EGL has no display link, and composited
|
|
over the FBO in the present (gl_anim_overlay). */
|
|
|
|
/* Critically-damped spring step (analytic, dt-stable). Mirrors Metal's
|
|
spring_update. */
|
|
static void
|
|
gl_spring_step (float *pos, float *vel, float target, float dt)
|
|
{
|
|
float omega = GL_SPRING_OMEGA;
|
|
float c1 = *pos - target;
|
|
float c2 = *vel + omega * c1;
|
|
float e = expf (-omega * dt);
|
|
*pos = target + (c1 + c2 * dt) * e;
|
|
*vel = (c2 - omega * (c1 + c2 * dt)) * e;
|
|
}
|
|
|
|
/* Spawn a radial burst at (px,py) for the particle modes. */
|
|
static void
|
|
gl_anim_spawn (struct gl_anim *a, float px, float py)
|
|
{
|
|
int count = (g_gl_cursor_mode == GL_CURSOR_PIXIEDUST) ? 12 : 3;
|
|
for (int i = 0; i < count && a->n_particles < GL_MAX_PARTICLES; i++)
|
|
{
|
|
float angle = (g_gl_cursor_mode == GL_CURSOR_PIXIEDUST)
|
|
? (float) i * 2.399f /* golden angle */
|
|
: (float) i * (2.0f * (float) M_PI / (float) count);
|
|
float speed = (g_gl_cursor_mode == GL_CURSOR_SONICBOOM) ? 60.0f : 40.0f;
|
|
struct gl_particle *p = &a->particles[a->n_particles++];
|
|
p->x = px; p->y = py;
|
|
p->vx = cosf (angle) * speed;
|
|
p->vy = sinf (angle) * speed;
|
|
p->age = 0.0f;
|
|
p->size = (g_gl_cursor_mode == GL_CURSOR_SONICBOOM) ? 6.0f : 3.0f;
|
|
p->color = a->color ? a->color : 0x88C0D0;
|
|
}
|
|
}
|
|
|
|
/* note_cursor vtable op: the policy hands us the cursor rect + color every
|
|
time it would draw the cursor. We record the target, spawn effects on a
|
|
jump, and (for body-animated modes) tell the policy to skip the static
|
|
cursor. w/h <= 0 is the blink-off phase. */
|
|
static bool
|
|
gl_drv_note_cursor (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 || !g_gl_animations_enabled || g_gl_cursor_mode == GL_CURSOR_BLOCK)
|
|
return false;
|
|
struct gl_anim *a = &fd->anim;
|
|
|
|
if (w <= 0 || h <= 0)
|
|
{
|
|
if (!a->hidden)
|
|
{
|
|
a->hidden = true;
|
|
if (!fd->in_cycle) { gl_present_to_window (fd); fd->needs_present = false; }
|
|
}
|
|
return true;
|
|
}
|
|
|
|
float fx = (float) x, fy = (float) y;
|
|
float dx = fx - a->tx, dy = fy - a->ty;
|
|
|
|
if (!a->have_target) /* first placement: snap */
|
|
{ a->sx = fx; a->sy = fy; a->svx = a->svy = 0; a->have_target = true; }
|
|
|
|
/* Burst on a significant jump (not while typing). */
|
|
if (!g_gl_cursor_suppress
|
|
&& (fabsf (dx) > (float) w * 1.5f || fabsf (dy) > (float) h * 1.5f)
|
|
&& (g_gl_cursor_mode == GL_CURSOR_SONICBOOM
|
|
|| g_gl_cursor_mode == GL_CURSOR_RIPPLE
|
|
|| g_gl_cursor_mode == GL_CURSOR_PIXIEDUST))
|
|
{
|
|
gl_anim_spawn (a, a->tx + w / 2.0f, a->ty + h / 2.0f);
|
|
if (getenv ("GL_LOG_ANIM"))
|
|
fprintf (stderr, "[anim] spawn at %.0f,%.0f n=%d mode=%d\n",
|
|
(double) a->tx, (double) a->ty, a->n_particles,
|
|
g_gl_cursor_mode);
|
|
}
|
|
|
|
/* Push a trail sample (torpedo). */
|
|
if (!g_gl_cursor_suppress && g_gl_cursor_mode == GL_CURSOR_TORPEDO
|
|
&& (fabsf (dx) > 1 || fabsf (dy) > 1))
|
|
{
|
|
int slot = (a->trail_head + a->trail_count) % GL_TRAIL_LEN;
|
|
a->trail_x[slot] = a->tx; a->trail_y[slot] = a->ty;
|
|
a->trail_age[slot] = 0.0f;
|
|
if (a->trail_count < g_gl_trail_len) a->trail_count++;
|
|
else a->trail_head = (a->trail_head + 1) % GL_TRAIL_LEN;
|
|
}
|
|
|
|
a->hidden = false;
|
|
a->color = color;
|
|
a->tx = fx; a->ty = fy; a->tw = (float) w; a->th = (float) h;
|
|
|
|
/* Cursor-only motion takes redisplay's fast path (no render cycle), so
|
|
present now or the overlay would lag at its old position. */
|
|
if (!fd->in_cycle) { gl_present_to_window (fd); fd->needs_present = false; }
|
|
|
|
/* Only the body-animated modes draw the cursor in the overlay; the burst
|
|
modes keep the policy's static (inverted-glyph) cursor underneath. */
|
|
return (g_gl_cursor_mode == GL_CURSOR_SPRING
|
|
|| g_gl_cursor_mode == GL_CURSOR_TORPEDO
|
|
|| g_gl_cursor_mode == GL_CURSOR_HOLLOW
|
|
|| g_gl_cursor_mode == GL_CURSOR_BEAM);
|
|
}
|
|
|
|
/* Advance every cursor animation by DT; true when something moved and
|
|
the overlay needs a new present. Pure physics: presenting is the
|
|
caller's job (gl_pump_tick / gl_anim_tick). */
|
|
static bool
|
|
gl_anim_step (struct frame *f, struct gl_frame_data *fd, double dt)
|
|
{
|
|
struct gl_anim *a = &fd->anim;
|
|
bool moved = false;
|
|
|
|
/* Mirror the blink phase (the engine toggles cursor_off_p without
|
|
reaching the rif). */
|
|
if (WINDOWP (f->selected_window))
|
|
{
|
|
bool hidden = XWINDOW (f->selected_window)->cursor_off_p;
|
|
if (hidden != a->hidden) { a->hidden = hidden; moved = true; }
|
|
}
|
|
|
|
if (g_gl_cursor_mode == GL_CURSOR_SPRING && a->have_target)
|
|
{
|
|
gl_spring_step (&a->sx, &a->svx, a->tx, (float) dt);
|
|
gl_spring_step (&a->sy, &a->svy, a->ty, (float) dt);
|
|
if (fabsf (a->sx - a->tx) > 0.5f || fabsf (a->sy - a->ty) > 0.5f)
|
|
moved = true;
|
|
}
|
|
|
|
if (g_gl_cursor_mode == GL_CURSOR_TORPEDO && a->trail_count > 0)
|
|
{
|
|
for (int i = 0; i < a->trail_count; i++)
|
|
a->trail_age[(a->trail_head + i) % GL_TRAIL_LEN] += (float) dt;
|
|
while (a->trail_count > 0
|
|
&& a->trail_age[a->trail_head] >= GL_TRAIL_LIFETIME)
|
|
{ a->trail_head = (a->trail_head + 1) % GL_TRAIL_LEN; a->trail_count--; }
|
|
if (a->trail_count > 0) moved = true;
|
|
}
|
|
|
|
if (a->n_particles > 0)
|
|
{
|
|
moved = true;
|
|
int alive = 0;
|
|
for (int i = 0; i < a->n_particles; i++)
|
|
{
|
|
struct gl_particle *p = &a->particles[i];
|
|
p->age += (float) dt / 0.6f;
|
|
if (p->age >= 1.0f) continue;
|
|
p->x += p->vx * (float) dt; p->y += p->vy * (float) dt;
|
|
p->vx *= 0.92f; p->vy *= 0.92f;
|
|
a->particles[alive++] = *p;
|
|
}
|
|
a->n_particles = alive;
|
|
}
|
|
|
|
return moved;
|
|
}
|
|
|
|
/* Compatibility entry of the old per-subsystem pump (gpu-anim-tick).
|
|
New code goes through gl_pump_tick, which steps everything and
|
|
presents once. */
|
|
bool
|
|
gl_anim_tick (struct frame *f, double dt)
|
|
{
|
|
struct gl_frame_data *fd = gl_get_frame_data (f);
|
|
if (!fd) return g_gl_animations_enabled;
|
|
/* Present only when something moved AND redisplay has not just swapped:
|
|
piling animation presents on top of redisplay's own floods the
|
|
swapchain (see last_swap in gl_frame_data). The skipped motion still
|
|
reaches the screen with the next tick or present. */
|
|
if (gl_anim_step (f, fd, dt)
|
|
&& !fd->in_cycle && gl_now () - fd->last_swap >= 0.012)
|
|
{ gl_present_to_window (fd); fd->needs_present = false; }
|
|
return g_gl_animations_enabled;
|
|
}
|
|
|
|
/* Single animation pump (gpu-pump-tick). Advance the cursor effects,
|
|
the buffer cross-fade and the inline video together, then present AT
|
|
MOST ONCE. gpu.el drives every continuous animation through this one
|
|
entry point: when each subsystem presented from its own timer the
|
|
surface saw bursts well above the refresh rate, which is exactly the
|
|
pressure under which radeonsi was caught presenting a stale buffer
|
|
(see last_swap). A skipped subsystem loses nothing -- the fade and
|
|
the video are composited by EVERY present, whoever issues it.
|
|
Returns the GL_PUMP_* mask of subsystems that still need pumping so
|
|
the Lisp timer can re-pace itself or stop. */
|
|
int
|
|
gl_pump_tick (struct frame *f)
|
|
{
|
|
struct gl_frame_data *fd = gl_get_frame_data (f);
|
|
if (!fd)
|
|
return g_gl_animations_enabled ? GL_PUMP_ANIM : 0;
|
|
|
|
double now = gl_now ();
|
|
double dt = fd->last_pump > 0 ? now - fd->last_pump : 1.0 / 30;
|
|
if (dt > 0.1) dt = 0.1; /* a stalled timer must not teleport physics */
|
|
fd->last_pump = now;
|
|
|
|
bool need = g_gl_animations_enabled && gl_anim_step (f, fd, dt);
|
|
bool fade = fd->trans_dur > 0;
|
|
need |= fade;
|
|
#ifdef HAVE_GSTREAMER
|
|
if (fd->video)
|
|
need |= gl_video_pump (fd->video);
|
|
#endif
|
|
|
|
if (getenv ("GL_LOG_PRESENT"))
|
|
fprintf (stderr, "[glpump] t=%.4f need=%d fade=%d in_cycle=%d gap=%.3f\n",
|
|
now, need, fade, fd->in_cycle, now - fd->last_swap);
|
|
|
|
if (need && !fd->in_cycle && now - fd->last_swap >= 0.012)
|
|
{ gl_present_to_window (fd); fd->needs_present = false; }
|
|
|
|
return (g_gl_animations_enabled ? GL_PUMP_ANIM : 0)
|
|
| (fade ? GL_PUMP_FADE : 0)
|
|
| (fd->video ? GL_PUMP_VIDEO : 0);
|
|
}
|
|
|
|
/* Append a solid colored quad (pixel coords, top-left origin) to a CPU
|
|
buffer of rect-shader vertices: {x,y, r,g,b,a}. */
|
|
static void
|
|
gl_anim_quad (float *v, int *n, float x0, float y0, float x1, float y1,
|
|
float r, float g, float b, float al)
|
|
{
|
|
float q[6][6] = {
|
|
{x0,y0,r,g,b,al}, {x1,y0,r,g,b,al}, {x0,y1,r,g,b,al},
|
|
{x1,y0,r,g,b,al}, {x1,y1,r,g,b,al}, {x0,y1,r,g,b,al},
|
|
};
|
|
memcpy (v + (*n) * 6, q, sizeof q);
|
|
*n += 6;
|
|
}
|
|
|
|
/* True when gl_anim_overlay would emit at least one quad right now. The
|
|
present uses this to decide whether the frame needs a full blit (overlay
|
|
pixels live only in the back buffer, not the FBO); an idle burst-mode
|
|
cursor emits nothing, so ordinary typing still presents partially.
|
|
Mirrors the emission conditions in gl_anim_overlay below. */
|
|
static bool
|
|
gl_anim_overlay_active (struct gl_frame_data *fd)
|
|
{
|
|
if (!g_gl_animations_enabled)
|
|
return false;
|
|
struct gl_anim *a = &fd->anim;
|
|
if (a->n_particles > 0)
|
|
return true;
|
|
if (a->hidden)
|
|
return false;
|
|
if (g_gl_cursor_mode == GL_CURSOR_TORPEDO && a->trail_count > 0)
|
|
return true;
|
|
return (g_gl_cursor_mode == GL_CURSOR_SPRING
|
|
|| g_gl_cursor_mode == GL_CURSOR_TORPEDO
|
|
|| g_gl_cursor_mode == GL_CURSOR_HOLLOW
|
|
|| g_gl_cursor_mode == GL_CURSOR_BEAM);
|
|
}
|
|
|
|
/* Composite the cursor effects over the on-screen framebuffer. Called from
|
|
gl_present_to_window with the window surface current. Builds one vertex
|
|
buffer of colored quads (trail + body + particles) and draws it with the
|
|
rect shader, alpha-blended. */
|
|
static void
|
|
gl_anim_overlay (struct gl_frame_data *fd, int sw, int sh)
|
|
{
|
|
struct gl_anim *a = &fd->anim;
|
|
double s = fd->scale;
|
|
float crgba[4];
|
|
gl_unpack_color (a->color ? a->color : 0x88C0D0, crgba);
|
|
float cr = crgba[0], cg = crgba[1], cb = crgba[2];
|
|
|
|
if (!gl_anim_overlay_active (fd))
|
|
return;
|
|
|
|
/* Worst case: trail (GL_TRAIL_LEN) + body + particles, 6 verts each. */
|
|
static float verts[(GL_TRAIL_LEN + 1 + GL_MAX_PARTICLES) * 6 * 6];
|
|
int n = 0;
|
|
|
|
float cw = a->tw * (float) s, ch = a->th * (float) s;
|
|
|
|
if (g_gl_cursor_mode == GL_CURSOR_TORPEDO && a->trail_count > 0 && !a->hidden)
|
|
{
|
|
int tlen = a->trail_count < g_gl_trail_len ? a->trail_count : g_gl_trail_len;
|
|
for (int i = 0; i < tlen; i++)
|
|
{
|
|
int slot = (a->trail_head + i) % GL_TRAIL_LEN;
|
|
float pos = (float) (i + 1) / (float) tlen;
|
|
float frac = a->trail_age[slot] / GL_TRAIL_LIFETIME;
|
|
if (frac > 1.0f) frac = 1.0f;
|
|
float life = 1.0f - frac;
|
|
float alpha = pos * life * life * 0.5f;
|
|
float scale = 0.35f + 0.55f * pos;
|
|
float tx = a->trail_x[slot] * (float) s, ty = a->trail_y[slot] * (float) s;
|
|
float iw = cw * scale, ih = ch * scale;
|
|
float ox = tx + (cw - iw) * 0.5f, oy = ty + (ch - ih) * 0.5f;
|
|
gl_anim_quad (verts, &n, ox, oy, ox + iw, oy + ih, cr, cg, cb, alpha);
|
|
}
|
|
}
|
|
|
|
if (!a->hidden
|
|
&& (g_gl_cursor_mode == GL_CURSOR_SPRING || g_gl_cursor_mode == GL_CURSOR_TORPEDO
|
|
|| g_gl_cursor_mode == GL_CURSOR_HOLLOW || g_gl_cursor_mode == GL_CURSOR_BEAM))
|
|
{
|
|
float cx = (g_gl_cursor_mode == GL_CURSOR_SPRING ? a->sx : a->tx) * (float) s;
|
|
float cy = (g_gl_cursor_mode == GL_CURSOR_SPRING ? a->sy : a->ty) * (float) s;
|
|
if (g_gl_cursor_mode == GL_CURSOR_BEAM)
|
|
cw = 2.0f * (float) s; /* thin bar */
|
|
if (g_gl_cursor_mode == GL_CURSOR_HOLLOW)
|
|
{
|
|
float t = (float) s; /* 1px outline */
|
|
gl_anim_quad (verts, &n, cx, cy, cx + cw, cy + t, cr, cg, cb, 1);
|
|
gl_anim_quad (verts, &n, cx, cy + ch - t, cx + cw, cy + ch, cr, cg, cb, 1);
|
|
gl_anim_quad (verts, &n, cx, cy, cx + t, cy + ch, cr, cg, cb, 1);
|
|
gl_anim_quad (verts, &n, cx + cw - t, cy, cx + cw, cy + ch, cr, cg, cb, 1);
|
|
}
|
|
else
|
|
gl_anim_quad (verts, &n, cx, cy, cx + cw, cy + ch, cr, cg, cb, 1);
|
|
}
|
|
|
|
for (int i = 0; i < a->n_particles; i++)
|
|
{
|
|
struct gl_particle *p = &a->particles[i];
|
|
float al = (1.0f - p->age) * (1.0f - p->age);
|
|
float sz = p->size * (1.0f - p->age * 0.5f) * (float) s;
|
|
float rgba[4]; gl_unpack_color (p->color, rgba);
|
|
float px = p->x * (float) s, py = p->y * (float) s;
|
|
gl_anim_quad (verts, &n, px - sz / 2, py - sz / 2, px + sz / 2, py + sz / 2,
|
|
rgba[0], rgba[1], rgba[2], al);
|
|
}
|
|
|
|
if (getenv ("GL_LOG_ANIM") && n > 0)
|
|
fprintf (stderr, "[anim] overlay quads=%d parts=%d trail=%d hidden=%d\n",
|
|
n / 6, a->n_particles, a->trail_count, (int) a->hidden);
|
|
if (n == 0) return;
|
|
|
|
glViewport (0, 0, sw, sh);
|
|
glUseProgram (g_prog_rect);
|
|
glUniform2f (g_u_rect_size, (float) sw, (float) sh);
|
|
glBindBuffer (GL_ARRAY_BUFFER, g_vbo);
|
|
glBufferData (GL_ARRAY_BUFFER, (GLsizeiptr) n * 6 * sizeof (float),
|
|
verts, GL_STREAM_DRAW);
|
|
glEnableVertexAttribArray (0);
|
|
glVertexAttribPointer (0, 2, GL_FLOAT, GL_FALSE, 24, (void *) 0);
|
|
glEnableVertexAttribArray (2);
|
|
glVertexAttribPointer (2, 4, GL_FLOAT, GL_FALSE, 24, (void *) 8);
|
|
glEnable (GL_BLEND);
|
|
glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
|
glDrawArrays (GL_TRIANGLES, 0, n);
|
|
}
|
|
|
|
/* Public config setters used by glfns.c. */
|
|
void gl_cursor_set_mode (int mode) { g_gl_cursor_mode = mode; }
|
|
void gl_cursor_set_trail_len (int len)
|
|
{ g_gl_trail_len = len < 1 ? 1 : (len > GL_TRAIL_LEN ? GL_TRAIL_LEN : len); }
|
|
void gl_cursor_set_suppress (bool s) { g_gl_cursor_suppress = s; }
|
|
int gl_cursor_get_mode (void) { return g_gl_cursor_mode; }
|
|
bool gl_animations_get_enabled (void) { return g_gl_animations_enabled; }
|
|
void
|
|
gl_animations_set_enabled (bool on)
|
|
{
|
|
g_gl_animations_enabled = on;
|
|
if (!on) /* clear in-flight effects */
|
|
for (int i = 0; i < GL_MAX_FRAMES; i++)
|
|
if (g_frames[i])
|
|
{
|
|
g_frames[i]->anim.n_particles = 0;
|
|
g_frames[i]->anim.trail_count = 0;
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Terminal/rif hookup. Like mtl_patch_terminal_rif: copy the X rif and
|
|
override ONLY the pixel-drawing functions with the neutral policy in
|
|
gfxterm.c, capturing the original X implementations as the fallback for
|
|
frames the GPU backend is not enabled on. Replacing the whole rif would
|
|
break frame/face realization (produce_glyphs, frame_parm_handlers...). */
|
|
|
|
static struct redisplay_interface *gl_x_rif_copy = NULL;
|
|
|
|
/* update_end hook for GPU frames. Runs the neutral policy (gfx_update_end,
|
|
which closes the cycle and presents), then clears mouse_face_defer the way
|
|
x_update_end does. dispnew.c sets that flag at the start of every window
|
|
update so note_mouse_highlight defers mid-update; every window-system
|
|
backend's update_end clears it again. gfx_update_end only forwards to the
|
|
captured fallback when the GPU is OFF, so on a GPU frame the flag was never
|
|
cleared: after the first redisplay (the blink-cursor timer, once focused)
|
|
it stayed set and note_mouse_highlight returned early forever, so hovering
|
|
a link stopped highlighting after the first one. The macro needs the X
|
|
display headers, so this lives here, not in the neutral gfxterm.c. */
|
|
static void
|
|
gl_update_end (struct frame *f)
|
|
{
|
|
gfx_update_end (f);
|
|
if (gl_get_frame_data (f))
|
|
MOUSE_HL_INFO (f)->mouse_face_defer = false;
|
|
}
|
|
|
|
/* frame_up_to_date hook for GPU frames. Runs the neutral policy
|
|
(gfx_frame_up_to_date, which presents the finished frame), then re-asserts
|
|
the mouse-face highlight at the last pointer position the way
|
|
XTframe_up_to_date does with FRAME_MOUSE_UPDATE. A redisplay -- notably the
|
|
blink-cursor timer once the frame has focus -- hides the mouse face;
|
|
without this it is never redrawn, so hovering a link stops highlighting
|
|
after the first time. The macro lives here (not the neutral gfxterm.c)
|
|
because it needs the X display headers. note_mouse_highlight only repaints
|
|
when the highlighted region actually changed, so this is cheap; its draws
|
|
land outside the update cycle (deferred present), so flush them. */
|
|
static void
|
|
gl_frame_up_to_date (struct frame *f)
|
|
{
|
|
gfx_frame_up_to_date (f);
|
|
if (gl_get_frame_data (f))
|
|
{
|
|
FRAME_MOUSE_UPDATE (f);
|
|
struct gl_frame_data *fd = gl_get_frame_data (f);
|
|
if (fd && fd->needs_present)
|
|
{
|
|
gl_present_to_window (fd);
|
|
fd->needs_present = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
gl_patch_terminal_rif (struct frame *f)
|
|
{
|
|
struct terminal *term = FRAME_TERMINAL (f);
|
|
if (!term || !term->rif) return;
|
|
|
|
if (!gl_x_rif_copy)
|
|
{
|
|
gfx_fallback.rif = term->rif;
|
|
gl_x_rif_copy = xmalloc (sizeof (struct redisplay_interface));
|
|
*gl_x_rif_copy = *term->rif; /* keep all X functions as baseline */
|
|
|
|
gl_x_rif_copy->scroll_run_hook = gfx_scroll_run;
|
|
gl_x_rif_copy->after_update_window_line_hook = gfx_after_update_window_line;
|
|
gl_x_rif_copy->flush_display = gfx_flush_display;
|
|
gl_x_rif_copy->draw_fringe_bitmap = gfx_draw_fringe_bitmap;
|
|
gl_x_rif_copy->define_fringe_bitmap = gfx_define_fringe_bitmap;
|
|
gl_x_rif_copy->destroy_fringe_bitmap = gfx_destroy_fringe_bitmap;
|
|
gl_x_rif_copy->compute_glyph_string_overhangs = gfx_compute_glyph_string_overhangs;
|
|
gl_x_rif_copy->draw_glyph_string = gfx_draw_glyph_string;
|
|
gl_x_rif_copy->clear_frame_area = gfx_clear_frame_area;
|
|
gl_x_rif_copy->clear_under_internal_border = gfx_clear_under_internal_border;
|
|
gl_x_rif_copy->draw_window_cursor = gfx_draw_window_cursor;
|
|
gl_x_rif_copy->draw_vertical_window_border = gfx_draw_vertical_window_border;
|
|
gl_x_rif_copy->draw_window_divider = gfx_draw_window_divider;
|
|
gl_x_rif_copy->shift_glyphs_for_insert = gfx_shift_glyphs_for_insert;
|
|
}
|
|
|
|
term->rif = gl_x_rif_copy;
|
|
|
|
if (term->update_begin_hook != gfx_update_begin)
|
|
{
|
|
gfx_fallback.update_begin = term->update_begin_hook;
|
|
gfx_fallback.update_end = term->update_end_hook;
|
|
gfx_fallback.clear_frame = term->clear_frame_hook;
|
|
gfx_fallback.frame_up_to_date = term->frame_up_to_date_hook;
|
|
}
|
|
term->update_begin_hook = gfx_update_begin;
|
|
term->update_end_hook = gl_update_end;
|
|
term->clear_frame_hook = gfx_clear_frame;
|
|
term->frame_up_to_date_hook = gl_frame_up_to_date;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Public entry points (called from glfns.c / the X terminal hookup). */
|
|
|
|
/* Bring up the EGL context if needed; return true when the GL backend is
|
|
usable (used by gpu-backend-p without touching any frame). */
|
|
bool
|
|
gl_backend_available (void)
|
|
{
|
|
return gl_global_init ();
|
|
}
|
|
|
|
/* Enable the GL backend on frame F: bring up the context, allocate the
|
|
per-frame data, and publish the driver. Returns false on failure. */
|
|
bool
|
|
gl_enable_for_frame (struct frame *f)
|
|
{
|
|
if (!gl_global_init ()) return false;
|
|
if (gl_get_frame_data (f)) return true;
|
|
|
|
int slot = -1;
|
|
for (int i = 0; i < GL_MAX_FRAMES; i++)
|
|
if (!g_frames[i]) { slot = i; break; }
|
|
if (slot < 0) return false;
|
|
|
|
struct gl_frame_data *fd = calloc (1, sizeof *fd);
|
|
if (!fd) return false;
|
|
fd->f = f;
|
|
fd->scale = g_atlas_scale;
|
|
g_frames[slot] = fd;
|
|
|
|
gfx_drv = &gl_gfx_driver;
|
|
return true;
|
|
}
|
|
|
|
/* Read the static texture back as RGBA8, top-left origin (the
|
|
gpu-capture-frame analogue). Caller frees *out. */
|
|
bool
|
|
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;
|
|
glBindFramebuffer (GL_FRAMEBUFFER, fd->fbo);
|
|
glDisable (GL_SCISSOR_TEST);
|
|
glReadPixels (0, 0, W, H, GL_RGBA, GL_UNSIGNED_BYTE, buf);
|
|
/* Flip to top-left origin. */
|
|
unsigned char *flip = malloc ((size_t) W * H * 4);
|
|
if (!flip) { free (buf); return false; }
|
|
for (int row = 0; row < H; row++)
|
|
memcpy (flip + (size_t) row * W * 4,
|
|
buf + (size_t) (H - 1 - row) * W * 4, (size_t) W * 4);
|
|
free (buf);
|
|
*w = W; *h = H; *out = flip;
|
|
return true;
|
|
}
|
|
|
|
/* Snapshot the current FBO (the still-displayed previous frame) into the
|
|
transition texture and arm the cross-fade. Called from Lisp on
|
|
pre-redisplay-functions, before redisplay paints the new buffer, so the
|
|
FBO still holds the old content. Mirrors mtl_transition_start. */
|
|
bool
|
|
gl_transition_start (struct frame *f, double duration)
|
|
{
|
|
struct gl_frame_data *fd = gl_get_frame_data (f);
|
|
if (!fd || !fd->fbo || fd->in_cycle || duration <= 0) return false;
|
|
|
|
if (!fd->trans_tex || fd->trans_w != fd->w || fd->trans_h != fd->h)
|
|
{
|
|
if (!fd->trans_tex) glGenTextures (1, &fd->trans_tex);
|
|
glBindTexture (GL_TEXTURE_2D, fd->trans_tex);
|
|
glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA8, fd->w, fd->h, 0, GL_RGBA,
|
|
GL_UNSIGNED_BYTE, NULL);
|
|
glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
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);
|
|
fd->trans_w = fd->w;
|
|
fd->trans_h = fd->h;
|
|
}
|
|
glBindFramebuffer (GL_FRAMEBUFFER, fd->fbo);
|
|
glBindTexture (GL_TEXTURE_2D, fd->trans_tex);
|
|
glCopyTexSubImage2D (GL_TEXTURE_2D, 0, 0, 0, 0, 0, fd->w, fd->h);
|
|
|
|
fd->trans_start = gl_now ();
|
|
fd->trans_dur = duration;
|
|
if (getenv ("GL_LOG_PRESENT"))
|
|
fprintf (stderr, "[gltrans] arm t=%.4f dur=%.0fms fbo=%dx%d\n",
|
|
fd->trans_start, duration * 1000, fd->w, fd->h);
|
|
return true;
|
|
}
|
|
|
|
/* Re-present the on-screen window so the cross-fade advances while the
|
|
event loop is idle (Emacs does not drive a timer for us). Driven by a
|
|
Lisp 30fps timer; returns true while the fade is still running. */
|
|
bool
|
|
gl_transition_tick (struct frame *f)
|
|
{
|
|
struct gl_frame_data *fd = gl_get_frame_data (f);
|
|
if (!fd || fd->trans_dur <= 0) return false;
|
|
/* The fade is drawn on EVERY present while it runs, so a tick that
|
|
lands right after a redisplay present has nothing to add; skipping
|
|
it keeps the present rate at the refresh rate instead of flooding
|
|
the swapchain (see last_swap in gl_frame_data). */
|
|
if (gl_now () - fd->last_swap >= 0.012)
|
|
gl_present_to_window (fd);
|
|
return fd->trans_dur > 0;
|
|
}
|
|
|
|
void
|
|
gl_free_frame_data (struct frame *f)
|
|
{
|
|
for (int i = 0; i < GL_MAX_FRAMES; i++)
|
|
if (g_frames[i] && g_frames[i]->f == f)
|
|
{
|
|
struct gl_frame_data *fd = g_frames[i];
|
|
if (g_fd_mru == fd)
|
|
g_fd_mru = NULL;
|
|
if (g_glyph_batch_fd == fd)
|
|
{ g_glyph_batch_fd = NULL; g_glyph_batch_verts = 0; }
|
|
if (fd->surf != EGL_NO_SURFACE)
|
|
{
|
|
if (g_bound_known && g_bound_surf == fd->surf)
|
|
{ eglMakeCurrent (g_dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, g_ctx);
|
|
g_bound_surf = EGL_NO_SURFACE; }
|
|
eglDestroySurface (g_dpy, fd->surf);
|
|
}
|
|
if (fd->fbo) glDeleteFramebuffers (1, &fd->fbo);
|
|
if (fd->scratch_fbo) glDeleteFramebuffers (1, &fd->scratch_fbo);
|
|
if (fd->tex) glDeleteTextures (1, &fd->tex);
|
|
if (fd->scratch_tex) glDeleteTextures (1, &fd->scratch_tex);
|
|
if (fd->trans_tex) glDeleteTextures (1, &fd->trans_tex);
|
|
#ifdef HAVE_GSTREAMER
|
|
gl_video_free (fd);
|
|
#endif
|
|
free (fd);
|
|
g_frames[i] = NULL;
|
|
gfx_free_frame_state (f);
|
|
return;
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Inline video (gpu-video-*). GStreamer decodes the file into RGBA
|
|
frames; gl_video_tick uploads the newest frame to a texture and the
|
|
present composites it over the FBO blit at the video rect, clipped to
|
|
the window interior. This is the OpenGL analogue of the Metal driver's
|
|
MtlVideoPlayer (AVFoundation -> CVMetalTextureCache). Audio is handled
|
|
by playbin's own audio sink. */
|
|
|
|
#ifdef HAVE_GSTREAMER
|
|
|
|
struct gl_video
|
|
{
|
|
GstElement *pipeline; /* playbin */
|
|
GstElement *appsink; /* RGBA video sink we pull frames from */
|
|
GLuint tex; /* uploaded RGBA frame, or 0 */
|
|
int vw, vh; /* natural frame size in pixels */
|
|
int rx, ry, rw, rh; /* draw rect, frame-relative logical px */
|
|
int cx, cy, cw, ch; /* clip rect, logical px; cw<=0 means none */
|
|
bool loop;
|
|
bool have_frame;
|
|
unsigned tex_serial; /* bumped on every uploaded frame */
|
|
bool moved; /* rect/clip changed since the last pump */
|
|
};
|
|
|
|
static bool g_gst_inited = false;
|
|
|
|
/* Pull the most recent decoded frame (non-blocking) and upload it to the
|
|
video texture. No-op when no new sample is ready. */
|
|
static void
|
|
gl_video_upload_latest (struct gl_video *v)
|
|
{
|
|
if (!v || !v->appsink) return;
|
|
GstSample *sample =
|
|
gst_app_sink_try_pull_sample (GST_APP_SINK (v->appsink), 0);
|
|
if (!sample) return;
|
|
|
|
GstCaps *caps = gst_sample_get_caps (sample);
|
|
GstBuffer *buf = gst_sample_get_buffer (sample);
|
|
GstVideoInfo info;
|
|
if (caps && buf && gst_video_info_from_caps (&info, caps))
|
|
{
|
|
GstVideoFrame vf;
|
|
if (gst_video_frame_map (&vf, &info, buf, GST_MAP_READ))
|
|
{
|
|
int w = GST_VIDEO_FRAME_WIDTH (&vf);
|
|
int h = GST_VIDEO_FRAME_HEIGHT (&vf);
|
|
int stride = GST_VIDEO_FRAME_PLANE_STRIDE (&vf, 0);
|
|
const guint8 *data = GST_VIDEO_FRAME_PLANE_DATA (&vf, 0);
|
|
if (!v->tex)
|
|
{
|
|
glGenTextures (1, &v->tex);
|
|
glBindTexture (GL_TEXTURE_2D, v->tex);
|
|
glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
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);
|
|
v->vw = -1; /* force a fresh allocation below */
|
|
}
|
|
glBindTexture (GL_TEXTURE_2D, v->tex);
|
|
/* RGBA rows are 4-byte aligned; set the unpack row length so a
|
|
padded stride still uploads correctly. */
|
|
glPixelStorei (GL_UNPACK_ALIGNMENT, 4);
|
|
glPixelStorei (GL_UNPACK_ROW_LENGTH, stride / 4);
|
|
if (w != v->vw || h != v->vh)
|
|
{
|
|
glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0,
|
|
GL_RGBA, GL_UNSIGNED_BYTE, data);
|
|
v->vw = w; v->vh = h;
|
|
}
|
|
else
|
|
glTexSubImage2D (GL_TEXTURE_2D, 0, 0, 0, w, h,
|
|
GL_RGBA, GL_UNSIGNED_BYTE, data);
|
|
glPixelStorei (GL_UNPACK_ROW_LENGTH, 0);
|
|
v->have_frame = true;
|
|
v->tex_serial++;
|
|
gst_video_frame_unmap (&vf);
|
|
}
|
|
}
|
|
gst_sample_unref (sample);
|
|
}
|
|
|
|
/* Composite the current video frame over the on-screen default framebuffer
|
|
at its rect, clipped to its window. Called from gl_present_to_window
|
|
with the window surface current (sw x sh = surface size). */
|
|
static void
|
|
gl_video_overlay (struct gl_frame_data *fd, int sw, int sh)
|
|
{
|
|
struct gl_video *v = fd->video;
|
|
if (!v || !v->have_frame || !v->tex || v->rw <= 0 || v->rh <= 0) return;
|
|
double s = fd->scale;
|
|
|
|
bool clip = (v->cw > 0 && v->ch > 0);
|
|
if (clip)
|
|
{
|
|
int x0 = (int) lround (v->cx * s);
|
|
int x1 = (int) lround ((v->cx + v->cw) * s);
|
|
int ytop = (int) lround (v->cy * s);
|
|
int ybot = (int) lround ((v->cy + v->ch) * s);
|
|
if (x0 < 0) x0 = 0;
|
|
if (x1 > sw) x1 = sw;
|
|
if (ytop < 0) ytop = 0;
|
|
if (ybot > sh) ybot = sh;
|
|
int scw = x1 - x0, sch = ybot - ytop;
|
|
if (scw <= 0 || sch <= 0) return; /* fully scrolled away */
|
|
glEnable (GL_SCISSOR_TEST);
|
|
glScissor (x0, sh - ybot, scw, sch); /* default fb: bottom-left */
|
|
}
|
|
else
|
|
glDisable (GL_SCISSOR_TEST);
|
|
|
|
float x0 = (float) (v->rx * s), y0 = (float) (v->ry * s);
|
|
float x1 = (float) ((v->rx + v->rw) * s), y1 = (float) ((v->ry + v->rh) * s);
|
|
/* a_pos in top-left logical->physical px (VS_IMAGE flips to NDC); the RGBA
|
|
frame is top-row-first, so texcoords are NOT v-flipped. */
|
|
float verts[] = {
|
|
x0,y0, 0,0, x1,y0, 1,0, x0,y1, 0,1,
|
|
x1,y0, 1,0, x1,y1, 1,1, x0,y1, 0,1,
|
|
};
|
|
glViewport (0, 0, sw, sh);
|
|
glUseProgram (g_prog_image);
|
|
glUniform2f (g_u_image_size, (float) sw, (float) sh);
|
|
glActiveTexture (GL_TEXTURE0);
|
|
glBindTexture (GL_TEXTURE_2D, v->tex);
|
|
glUniform1i (g_u_image_tex, 0);
|
|
glUniform1f (g_u_image_alpha, 1.0f);
|
|
glBindBuffer (GL_ARRAY_BUFFER, g_vbo);
|
|
glBufferData (GL_ARRAY_BUFFER, sizeof verts, verts, GL_STREAM_DRAW);
|
|
glEnableVertexAttribArray (0);
|
|
glVertexAttribPointer (0, 2, GL_FLOAT, GL_FALSE, 16, (void *) 0);
|
|
glEnableVertexAttribArray (1);
|
|
glVertexAttribPointer (1, 2, GL_FLOAT, GL_FALSE, 16, (void *) 8);
|
|
glDrawArrays (GL_TRIANGLES, 0, 6);
|
|
if (clip) glDisable (GL_SCISSOR_TEST);
|
|
}
|
|
|
|
static void
|
|
gl_video_free (struct gl_frame_data *fd)
|
|
{
|
|
struct gl_video *v = fd->video;
|
|
if (!v) return;
|
|
if (v->pipeline)
|
|
{
|
|
gst_element_set_state (v->pipeline, GST_STATE_NULL);
|
|
gst_object_unref (v->pipeline);
|
|
}
|
|
if (v->tex) glDeleteTextures (1, &v->tex);
|
|
free (v);
|
|
fd->video = NULL;
|
|
}
|
|
|
|
/* Restart at EOS when looping; harmless otherwise. Polls the pipeline bus
|
|
without blocking. */
|
|
static void
|
|
gl_video_pump_bus (struct gl_video *v)
|
|
{
|
|
GstBus *bus = gst_element_get_bus (v->pipeline);
|
|
if (!bus) return;
|
|
GstMessage *msg;
|
|
while ((msg = gst_bus_pop_filtered (bus,
|
|
GST_MESSAGE_EOS | GST_MESSAGE_ERROR)) != NULL)
|
|
{
|
|
if (GST_MESSAGE_TYPE (msg) == GST_MESSAGE_EOS && v->loop)
|
|
gst_element_seek_simple (v->pipeline, GST_FORMAT_TIME,
|
|
GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT,
|
|
0);
|
|
gst_message_unref (msg);
|
|
}
|
|
gst_object_unref (bus);
|
|
}
|
|
|
|
bool
|
|
gl_video_open (struct frame *f, const char *path, int x, int y,
|
|
int w, int h, bool loop)
|
|
{
|
|
struct gl_frame_data *fd = gl_get_frame_data (f);
|
|
if (!fd) return false;
|
|
if (!g_gst_inited) { gst_init (NULL, NULL); g_gst_inited = true; }
|
|
|
|
if (fd->video) gl_video_free (fd);
|
|
|
|
GstElement *pipeline = gst_element_factory_make ("playbin", NULL);
|
|
GstElement *appsink = gst_element_factory_make ("appsink", NULL);
|
|
if (!pipeline || !appsink)
|
|
{
|
|
if (pipeline) gst_object_unref (pipeline);
|
|
if (appsink) gst_object_unref (appsink);
|
|
return false;
|
|
}
|
|
|
|
GstCaps *caps = gst_caps_new_simple ("video/x-raw", "format",
|
|
G_TYPE_STRING, "RGBA", NULL);
|
|
g_object_set (appsink, "caps", caps, "sync", TRUE,
|
|
"max-buffers", 1, "drop", TRUE, NULL);
|
|
gst_caps_unref (caps);
|
|
|
|
gchar *uri = gst_filename_to_uri (path, NULL);
|
|
if (!uri) { gst_object_unref (pipeline); gst_object_unref (appsink); return false; }
|
|
/* playbin takes ownership of the sink via the video-sink property. */
|
|
g_object_set (pipeline, "uri", uri, "video-sink", appsink, NULL);
|
|
g_free (uri);
|
|
|
|
struct gl_video *v = calloc (1, sizeof *v);
|
|
if (!v) { gst_object_unref (pipeline); return false; }
|
|
v->pipeline = pipeline;
|
|
v->appsink = appsink; /* owned by playbin; do not unref separately */
|
|
v->rx = x; v->ry = y; v->rw = w; v->rh = h;
|
|
v->vw = v->vh = -1;
|
|
v->loop = loop;
|
|
fd->video = v;
|
|
|
|
if (gst_element_set_state (pipeline, GST_STATE_PLAYING)
|
|
== GST_STATE_CHANGE_FAILURE)
|
|
{
|
|
gl_video_free (fd);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
gl_video_close (struct frame *f)
|
|
{
|
|
struct gl_frame_data *fd = gl_get_frame_data (f);
|
|
if (!fd || !fd->video) return false;
|
|
gl_video_free (fd);
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
gl_video_set_paused (struct frame *f, bool paused)
|
|
{
|
|
struct gl_frame_data *fd = gl_get_frame_data (f);
|
|
if (!fd || !fd->video) return false;
|
|
gst_element_set_state (fd->video->pipeline,
|
|
paused ? GST_STATE_PAUSED : GST_STATE_PLAYING);
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
gl_video_set_rect (struct frame *f, int x, int y, int w, int h)
|
|
{
|
|
struct gl_frame_data *fd = gl_get_frame_data (f);
|
|
if (!fd || !fd->video) return false;
|
|
struct gl_video *v = fd->video;
|
|
if (v->rx != x || v->ry != y || v->rw != w || v->rh != h)
|
|
v->moved = true;
|
|
v->rx = x; v->ry = y;
|
|
v->rw = w; v->rh = h;
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
gl_video_set_clip (struct frame *f, int x, int y, int w, int h)
|
|
{
|
|
struct gl_frame_data *fd = gl_get_frame_data (f);
|
|
if (!fd || !fd->video) return false;
|
|
struct gl_video *v = fd->video;
|
|
if (v->cx != x || v->cy != y || v->cw != w || v->ch != h)
|
|
v->moved = true;
|
|
v->cx = x; v->cy = y;
|
|
v->cw = w; v->ch = h;
|
|
return true;
|
|
}
|
|
|
|
/* Pull pending bus messages and upload the newest decoded frame. True
|
|
when the on-screen video needs a present: a fresh frame arrived or
|
|
the rect/clip moved since the last pump. */
|
|
static bool
|
|
gl_video_pump (struct gl_video *v)
|
|
{
|
|
gl_video_pump_bus (v);
|
|
unsigned before = v->tex_serial;
|
|
gl_video_upload_latest (v);
|
|
bool fresh = v->tex_serial != before;
|
|
if (getenv ("GL_LOG_PRESENT"))
|
|
fprintf (stderr, "[glvideo] tick t=%.4f %s\n", gl_now (),
|
|
fresh ? "new-frame" : "no-sample");
|
|
bool need = fresh || v->moved;
|
|
v->moved = false;
|
|
return need;
|
|
}
|
|
|
|
bool
|
|
gl_video_tick (struct frame *f)
|
|
{
|
|
struct gl_frame_data *fd = gl_get_frame_data (f);
|
|
if (!fd || !fd->video) return false;
|
|
/* Present only what changed, under the same global throttle as the
|
|
other animation sources (see last_swap): a paused video issues no
|
|
presents, and a tick landing right after another present defers to
|
|
it -- every present composites the video texture anyway. */
|
|
if (gl_video_pump (fd->video)
|
|
&& !fd->in_cycle && gl_now () - fd->last_swap >= 0.012)
|
|
{ gl_present_to_window (fd); fd->needs_present = false; }
|
|
return true;
|
|
}
|
|
|
|
double
|
|
gl_video_duration (struct frame *f)
|
|
{
|
|
struct gl_frame_data *fd = gl_get_frame_data (f);
|
|
if (!fd || !fd->video) return -1.0;
|
|
gint64 dur = 0;
|
|
if (gst_element_query_duration (fd->video->pipeline, GST_FORMAT_TIME, &dur)
|
|
&& dur > 0)
|
|
return (double) dur / GST_SECOND;
|
|
return -1.0;
|
|
}
|
|
|
|
double
|
|
gl_video_position (struct frame *f)
|
|
{
|
|
struct gl_frame_data *fd = gl_get_frame_data (f);
|
|
if (!fd || !fd->video) return -1.0;
|
|
gint64 pos = 0;
|
|
if (gst_element_query_position (fd->video->pipeline, GST_FORMAT_TIME, &pos)
|
|
&& pos >= 0)
|
|
return (double) pos / GST_SECOND;
|
|
return -1.0;
|
|
}
|
|
|
|
bool
|
|
gl_video_seek (struct frame *f, double seconds)
|
|
{
|
|
struct gl_frame_data *fd = gl_get_frame_data (f);
|
|
if (!fd || !fd->video) return false;
|
|
if (seconds < 0) seconds = 0;
|
|
gst_element_seek_simple (fd->video->pipeline, GST_FORMAT_TIME,
|
|
GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT,
|
|
(gint64) (seconds * GST_SECOND));
|
|
return true;
|
|
}
|
|
|
|
int
|
|
gl_video_playing (struct frame *f)
|
|
{
|
|
struct gl_frame_data *fd = gl_get_frame_data (f);
|
|
if (!fd || !fd->video) return -1;
|
|
GstState st = GST_STATE_NULL;
|
|
gst_element_get_state (fd->video->pipeline, &st, NULL, 0);
|
|
return st == GST_STATE_PLAYING ? 1 : 0;
|
|
}
|
|
|
|
bool
|
|
gl_video_size (struct frame *f, double *w, double *h)
|
|
{
|
|
struct gl_frame_data *fd = gl_get_frame_data (f);
|
|
if (!fd || !fd->video || fd->video->vw <= 0) return false;
|
|
*w = fd->video->vw; *h = fd->video->vh;
|
|
return true;
|
|
}
|
|
|
|
#endif /* HAVE_GSTREAMER */
|
|
|
|
#endif /* HAVE_GFX_GL */
|