feat: xorg text overlays, font atlas generator, v4l2_view_cli

- tools/gen_font_atlas: Python/Pillow build tool — skyline packs DejaVu
  Sans glyphs 32-255 into a grayscale atlas, emits build/gen/font_atlas.h
  with pixel data and Font_Glyph[256] metrics table
- xorg: bitmap font atlas text overlay rendering (GL_R8 atlas texture,
  alpha-blended glyph quads, dark background rect per overlay)
- xorg: add xorg_viewer_set_overlay_text / clear_overlays API
- xorg: add xorg_viewer_handle_events for streaming use (events only,
  no redundant render)
- xorg_cli: show today's date as white text overlay
- v4l2_view_cli: new tool — V4L2 capture with format auto-selection
  (highest FPS then largest resolution), MJPEG/YUYV, measured FPS overlay
- docs: update README, planning, architecture to reflect current status

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-28 22:13:59 +00:00
parent 7fd79e6120
commit 611376dbc1
11 changed files with 1204 additions and 46 deletions

View File

@@ -2,12 +2,20 @@ ROOT := $(abspath ../../..)
include $(ROOT)/common.mk
MODULE_BUILD = $(BUILD)/xorg
GEN_DIR = $(BUILD)/gen
ATLAS_H = $(GEN_DIR)/font_atlas.h
ATLAS_PNG = $(GEN_DIR)/font_atlas.png
GEN_SCRIPT = $(ROOT)/tools/gen_font_atlas/gen_font_atlas.py
# Select real implementation when glfw feature is enabled, stub otherwise.
ifeq ($(filter glfw,$(FEATURES)),glfw)
SRC = xorg.c
SRC = xorg.c
DEPS = $(ATLAS_H)
EXTRA_CFLAGS = -I$(GEN_DIR)
else
SRC = xorg_stub.c
SRC = xorg_stub.c
DEPS =
EXTRA_CFLAGS =
endif
OBJ = $(MODULE_BUILD)/xorg.o
@@ -16,12 +24,18 @@ OBJ = $(MODULE_BUILD)/xorg.o
all: $(OBJ)
$(OBJ): $(SRC) | $(MODULE_BUILD)
$(CC) $(CFLAGS) $(DEPFLAGS) -c -o $@ $<
$(OBJ): $(SRC) $(DEPS) | $(MODULE_BUILD)
$(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(DEPFLAGS) -c -o $@ $<
$(ATLAS_H) $(ATLAS_PNG): $(GEN_SCRIPT) | $(GEN_DIR)
python3 $< --out-header $(ATLAS_H) --out-png $(ATLAS_PNG)
$(MODULE_BUILD):
mkdir -p $@
$(GEN_DIR):
mkdir -p $@
clean:
rm -f $(OBJ) $(MODULE_BUILD)/xorg.d

View File

@@ -10,9 +10,10 @@
#endif
#include "xorg.h"
#include "font_atlas.h" /* generated: font_glyphs[], font_atlas_pixels[], FONT_ATLAS_W/H */
/* ------------------------------------------------------------------ */
/* Shader sources */
/* Shader sources — video */
/* ------------------------------------------------------------------ */
/*
@@ -62,14 +63,99 @@ static const char *FRAG_RGB_SRC =
"}\n";
/* ------------------------------------------------------------------ */
/* Viewer state */
/* Shader sources — solid rect */
/* ------------------------------------------------------------------ */
/*
* Draws a screen-space axis-aligned rect using gl_VertexID (no VBO).
* u_rect = (x0, y0, x1, y1) in window pixels.
*/
static const char *VERT_RECT_SRC =
"#version 330 core\n"
"uniform vec4 u_rect;\n"
"uniform vec2 u_fb_size;\n"
"void main() {\n"
" vec2 corners[4] = vec2[4](\n"
" vec2(u_rect.x, u_rect.y),\n"
" vec2(u_rect.x, u_rect.w),\n"
" vec2(u_rect.z, u_rect.y),\n"
" vec2(u_rect.z, u_rect.w)\n"
" );\n"
" vec2 pos = corners[gl_VertexID];\n"
" vec2 ndc = (pos / u_fb_size) * 2.0 - 1.0;\n"
" ndc.y = -ndc.y;\n"
" gl_Position = vec4(ndc, 0.0, 1.0);\n"
"}\n";
static const char *FRAG_RECT_SRC =
"#version 330 core\n"
"out vec4 out_color;\n"
"uniform vec4 u_color;\n"
"void main() {\n"
" out_color = u_color;\n"
"}\n";
/* ------------------------------------------------------------------ */
/* Shader sources — text overlay */
/* ------------------------------------------------------------------ */
/*
* Screen-space vertex shader: a_pos in window pixels (top-left = 0,0),
* converted to NDC. a_uv is the atlas UV coordinate.
*/
static const char *VERT_TEXT_SRC =
"#version 330 core\n"
"layout(location = 0) in vec2 a_pos;\n"
"layout(location = 1) in vec2 a_uv;\n"
"out vec2 v_uv;\n"
"uniform vec2 u_fb_size;\n"
"void main() {\n"
" vec2 ndc = (a_pos / u_fb_size) * 2.0 - 1.0;\n"
" ndc.y = -ndc.y;\n"
" gl_Position = vec4(ndc, 0.0, 1.0);\n"
" v_uv = a_uv;\n"
"}\n";
/*
* Fragment shader: samples the atlas (GL_R8, grayscale) and uses the
* texel value as alpha, blended with the per-overlay colour.
*/
static const char *FRAG_TEXT_SRC =
"#version 330 core\n"
"in vec2 v_uv;\n"
"out vec4 out_color;\n"
"uniform sampler2D u_text_atlas;\n"
"uniform vec3 u_text_color;\n"
"void main() {\n"
" float a = texture(u_text_atlas, v_uv).r;\n"
" out_color = vec4(u_text_color, a);\n"
"}\n";
/* ------------------------------------------------------------------ */
/* Constants and types */
/* ------------------------------------------------------------------ */
#define MAX_OVERLAYS 8
#define MAX_OVERLAY_CHARS 256
/*
* Each glyph quad = 6 vertices × 4 floats (x, y, u, v).
* Maximum vertex buffer covers all overlays at full text length.
*/
#define MAX_TEXT_VERTS (MAX_OVERLAYS * MAX_OVERLAY_CHARS * 6 * 4)
typedef struct {
char text[MAX_OVERLAY_CHARS];
int x, y; /* top-left of text block in window pixels */
float r, g, b;
} Overlay;
typedef enum { MODE_NONE, MODE_YUV, MODE_RGB } Render_Mode;
struct Xorg_Viewer {
GLFWwindow *window;
/* Video programs */
GLuint prog_yuv;
GLint u_tex_y, u_tex_cb, u_tex_cr;
GLint u_uv_scale_yuv, u_uv_offset_yuv;
@@ -86,6 +172,24 @@ struct Xorg_Viewer {
Xorg_Anchor anchor;
int frame_w, frame_h;
/* Solid rect (overlay background) */
GLuint prog_rect;
GLint u_rect_loc;
GLint u_rect_fb_size_loc;
GLint u_rect_color_loc;
/* Text overlay */
GLuint prog_text;
GLint u_fb_size_loc;
GLint u_text_atlas_loc;
GLint u_text_color_loc;
GLuint tex_atlas;
GLuint vao_text;
GLuint vbo_text;
Overlay overlays[MAX_OVERLAYS];
int n_overlays;
#ifdef HAVE_TURBOJPEG
tjhandle tj;
uint8_t *yuv_buf;
@@ -114,9 +218,9 @@ static GLuint compile_shader(GLenum type, const char *src)
return s;
}
static GLuint link_program(const char *frag_src)
static GLuint link_program(const char *vert_src, const char *frag_src)
{
GLuint vs = compile_shader(GL_VERTEX_SHADER, VERT_SRC);
GLuint vs = compile_shader(GL_VERTEX_SHADER, vert_src);
GLuint fs = compile_shader(GL_FRAGMENT_SHADER, frag_src);
if (!vs || !fs) {
glDeleteShader(vs);
@@ -145,7 +249,8 @@ static GLuint link_program(const char *frag_src)
/* Public API */
/* ------------------------------------------------------------------ */
static void render(Xorg_Viewer *v); /* forward declaration */
static void render(Xorg_Viewer *v); /* forward declarations */
static void draw_text_overlays(Xorg_Viewer *v, int fb_w, int fb_h);
bool xorg_available(void) { return true; }
@@ -166,6 +271,58 @@ static void framebuffer_size_callback(GLFWwindow *window, int width, int height)
if (v) { render(v); }
}
/* Initialise text rendering resources — called from xorg_viewer_open. */
static bool init_text_rendering(Xorg_Viewer *v)
{
v->prog_rect = link_program(VERT_RECT_SRC, FRAG_RECT_SRC);
if (!v->prog_rect) { return false; }
v->u_rect_loc = glGetUniformLocation(v->prog_rect, "u_rect");
v->u_rect_fb_size_loc = glGetUniformLocation(v->prog_rect, "u_fb_size");
v->u_rect_color_loc = glGetUniformLocation(v->prog_rect, "u_color");
v->prog_text = link_program(VERT_TEXT_SRC, FRAG_TEXT_SRC);
if (!v->prog_text) { return false; }
v->u_fb_size_loc = glGetUniformLocation(v->prog_text, "u_fb_size");
v->u_text_atlas_loc = glGetUniformLocation(v->prog_text, "u_text_atlas");
v->u_text_color_loc = glGetUniformLocation(v->prog_text, "u_text_color");
/* Upload atlas texture (grayscale GL_R8). */
glGenTextures(1, &v->tex_atlas);
glBindTexture(GL_TEXTURE_2D, v->tex_atlas);
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);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexImage2D(GL_TEXTURE_2D, 0, GL_R8,
FONT_ATLAS_W, FONT_ATLAS_H, 0,
GL_RED, GL_UNSIGNED_BYTE, font_atlas_pixels);
glBindTexture(GL_TEXTURE_2D, 0);
/* VAO + dynamic VBO for glyph quads. */
glGenVertexArrays(1, &v->vao_text);
glGenBuffers(1, &v->vbo_text);
glBindVertexArray(v->vao_text);
glBindBuffer(GL_ARRAY_BUFFER, v->vbo_text);
glBufferData(GL_ARRAY_BUFFER,
MAX_TEXT_VERTS * sizeof(float), NULL, GL_DYNAMIC_DRAW);
/* layout(location=0): vec2 a_pos, layout(location=1): vec2 a_uv */
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE,
4 * sizeof(float), (void *)0);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE,
4 * sizeof(float), (void *)(2 * sizeof(float)));
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
return true;
}
Xorg_Viewer *xorg_viewer_open(int x, int y, int width, int height,
const char *title)
{
@@ -209,8 +366,8 @@ Xorg_Viewer *xorg_viewer_open(int x, int y, int width, int height,
glfwSetWindowUserPointer(win, v);
glfwSetFramebufferSizeCallback(win, framebuffer_size_callback);
v->prog_yuv = link_program(FRAG_YUV_SRC);
v->prog_rgb = link_program(FRAG_RGB_SRC);
v->prog_yuv = link_program(VERT_SRC, FRAG_YUV_SRC);
v->prog_rgb = link_program(VERT_SRC, FRAG_RGB_SRC);
if (!v->prog_yuv || !v->prog_rgb) {
xorg_viewer_close(v);
return NULL;
@@ -238,6 +395,11 @@ Xorg_Viewer *xorg_viewer_open(int x, int y, int width, int height,
}
glBindTexture(GL_TEXTURE_2D, 0);
if (!init_text_rendering(v)) {
xorg_viewer_close(v);
return NULL;
}
#ifdef HAVE_TURBOJPEG
v->tj = tjInitDecompress();
if (!v->tj) {
@@ -260,6 +422,144 @@ void xorg_viewer_set_anchor(Xorg_Viewer *v, Xorg_Anchor anchor)
if (v) { v->anchor = anchor; }
}
void xorg_viewer_set_overlay_text(Xorg_Viewer *v, int idx, int x, int y,
const char *text, float r, float g, float b)
{
if (!v || idx < 0 || idx >= MAX_OVERLAYS) { return; }
Overlay *o = &v->overlays[idx];
strncpy(o->text, text, MAX_OVERLAY_CHARS - 1);
o->text[MAX_OVERLAY_CHARS - 1] = '\0';
o->x = x;
o->y = y;
o->r = r;
o->g = g;
o->b = b;
if (idx >= v->n_overlays) { v->n_overlays = idx + 1; }
}
void xorg_viewer_clear_overlays(Xorg_Viewer *v)
{
if (v) { v->n_overlays = 0; }
}
/* ------------------------------------------------------------------ */
/* Internal: build and draw text quads for all overlays */
/* ------------------------------------------------------------------ */
static void draw_text_overlays(Xorg_Viewer *v, int fb_w, int fb_h)
{
if (v->n_overlays == 0 || !v->prog_text) { return; }
/*
* Vertex layout: (x, y, u, uv) — 4 floats per vertex, 6 verts per glyph.
* Declared static to keep it off the stack (2MB+ otherwise).
*/
static float verts[MAX_TEXT_VERTS];
/* Reset to full-window viewport for overlay drawing. */
glViewport(0, 0, fb_w, fb_h);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, v->tex_atlas);
#define OVERLAY_MARGIN 4
for (int oi = 0; oi < v->n_overlays; oi++) {
const Overlay *o = &v->overlays[oi];
if (o->text[0] == '\0') { continue; }
/* Measure text to compute bounding box for the background rect. */
int text_w = 0, max_ascent = 0, max_descent = 0;
for (const char *p = o->text; *p; p++) {
unsigned char cp = (unsigned char)*p;
if (cp < 32) { continue; }
const Font_Glyph *g = &font_glyphs[cp];
text_w += g->advance;
if (g->bearing_y > max_ascent) { max_ascent = g->bearing_y; }
if (g->h > 0) {
int desc = g->h - g->bearing_y;
if (desc > max_descent) { max_descent = desc; }
}
}
int text_h = max_ascent + max_descent;
int max_bearing_y = max_ascent;
/* Draw semi-transparent dark background rect. */
float rx0 = (float)(o->x - OVERLAY_MARGIN);
float ry0 = (float)(o->y - OVERLAY_MARGIN);
float rx1 = (float)(o->x + text_w + OVERLAY_MARGIN);
float ry1 = (float)(o->y + text_h + OVERLAY_MARGIN);
glUseProgram(v->prog_rect);
glUniform2f(v->u_rect_fb_size_loc, (float)fb_w, (float)fb_h);
glUniform4f(v->u_rect_loc, rx0, ry0, rx1, ry1);
glUniform4f(v->u_rect_color_loc, 0.0f, 0.0f, 0.0f, 0.55f);
glBindVertexArray(v->vao);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
int baseline_y = o->y + max_bearing_y;
/* Switch back to text program + VBO for glyph quads. */
glUseProgram(v->prog_text);
glUniform2f(v->u_fb_size_loc, (float)fb_w, (float)fb_h);
glUniform1i(v->u_text_atlas_loc, 0);
glBindVertexArray(v->vao_text);
glBindBuffer(GL_ARRAY_BUFFER, v->vbo_text);
/* Build quads for this overlay. */
int n_verts = 0;
int cursor_x = o->x;
for (const char *p = o->text; *p; p++) {
unsigned char cp = (unsigned char)*p;
if (cp < 32) { continue; }
const Font_Glyph *g = &font_glyphs[cp];
/* Advance even for non-printing glyphs (e.g. space). */
if (g->w > 0 && g->h > 0) {
/* Screen-space quad corners (top-left origin). */
float sx0 = (float)(cursor_x + g->bearing_x);
float sy0 = (float)(baseline_y - g->bearing_y);
float sx1 = sx0 + (float)g->w;
float sy1 = sy0 + (float)g->h;
/* Atlas UV corners. */
float u0 = (float)g->x / FONT_ATLAS_W;
float u1 = (float)(g->x + g->w) / FONT_ATLAS_W;
float v0 = (float)g->y / FONT_ATLAS_H;
float v1 = (float)(g->y + g->h) / FONT_ATLAS_H;
if (n_verts + 6 * 4 > MAX_TEXT_VERTS) { break; }
float *d = verts + n_verts;
/* tri 0 */
d[ 0]=sx0; d[ 1]=sy0; d[ 2]=u0; d[ 3]=v0;
d[ 4]=sx0; d[ 5]=sy1; d[ 6]=u0; d[ 7]=v1;
d[ 8]=sx1; d[ 9]=sy0; d[10]=u1; d[11]=v0;
/* tri 1 */
d[12]=sx1; d[13]=sy0; d[14]=u1; d[15]=v0;
d[16]=sx0; d[17]=sy1; d[18]=u0; d[19]=v1;
d[20]=sx1; d[21]=sy1; d[22]=u1; d[23]=v1;
n_verts += 24;
}
cursor_x += g->advance;
}
if (n_verts == 0) { continue; }
glBufferSubData(GL_ARRAY_BUFFER, 0, n_verts * sizeof(float), verts);
glUniform3f(v->u_text_color_loc, o->r, o->g, o->b);
glDrawArrays(GL_TRIANGLES, 0, n_verts / 4);
}
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
glDisable(GL_BLEND);
}
/* ------------------------------------------------------------------ */
/* Internal: compute layout and render from existing textures */
/* ------------------------------------------------------------------ */
@@ -383,6 +683,9 @@ static void render(Xorg_Viewer *v)
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glBindVertexArray(0);
/* Draw text overlays on top, then present. */
draw_text_overlays(v, fb_w, fb_h);
glfwSwapBuffers(v->window);
}
@@ -510,6 +813,13 @@ bool xorg_viewer_poll(Xorg_Viewer *v)
return true;
}
bool xorg_viewer_handle_events(Xorg_Viewer *v)
{
if (!v || glfwWindowShouldClose(v->window)) { return false; }
glfwPollEvents();
return !glfwWindowShouldClose(v->window);
}
void xorg_viewer_close(Xorg_Viewer *v)
{
if (!v) { return; }
@@ -517,10 +827,15 @@ void xorg_viewer_close(Xorg_Viewer *v)
if (v->tj) { tjDestroy(v->tj); }
free(v->yuv_buf);
#endif
if (v->vao) { glDeleteVertexArrays(1, &v->vao); }
if (v->tex[0]) { glDeleteTextures(4, v->tex); }
if (v->prog_yuv) { glDeleteProgram(v->prog_yuv); }
if (v->prog_rgb) { glDeleteProgram(v->prog_rgb); }
if (v->vao_text) { glDeleteVertexArrays(1, &v->vao_text); }
if (v->vbo_text) { glDeleteBuffers(1, &v->vbo_text); }
if (v->tex_atlas) { glDeleteTextures(1, &v->tex_atlas); }
if (v->prog_text) { glDeleteProgram(v->prog_text); }
if (v->prog_rect) { glDeleteProgram(v->prog_rect); }
if (v->vao) { glDeleteVertexArrays(1, &v->vao); }
if (v->tex[0]) { glDeleteTextures(4, v->tex); }
if (v->prog_yuv) { glDeleteProgram(v->prog_yuv); }
if (v->prog_rgb) { glDeleteProgram(v->prog_rgb); }
if (v->window) {
glfwDestroyWindow(v->window);
glfwTerminate();

View File

@@ -34,5 +34,12 @@ bool xorg_viewer_push_mjpeg(Xorg_Viewer *v,
void xorg_viewer_set_scale(Xorg_Viewer *v, Xorg_Scale scale) { (void)v; (void)scale; }
void xorg_viewer_set_anchor(Xorg_Viewer *v, Xorg_Anchor anchor) { (void)v; (void)anchor; }
bool xorg_viewer_poll(Xorg_Viewer *v) { (void)v; return false; }
void xorg_viewer_set_overlay_text(Xorg_Viewer *v, int idx, int x, int y,
const char *text, float r, float g, float b)
{
(void)v; (void)idx; (void)x; (void)y; (void)text; (void)r; (void)g; (void)b;
}
void xorg_viewer_clear_overlays(Xorg_Viewer *v) { (void)v; }
bool xorg_viewer_poll(Xorg_Viewer *v) { (void)v; return false; }
bool xorg_viewer_handle_events(Xorg_Viewer *v) { (void)v; return false; }
void xorg_viewer_close(Xorg_Viewer *v) { (void)v; }