feat: xorg viewer scale modes, resize fix, arch notes
Scale modes (STRETCH/FIT/FILL/1:1) with CENTER/TOP_LEFT anchor: - UV crop via u_uv_scale/u_uv_offset uniforms in vertex shader - glViewport sub-rect + glClear for FIT and 1:1 modes - xorg_viewer_set_scale() / xorg_viewer_set_anchor() setters - Stub implementations for both Resize fix: glfwSetWindowUserPointer + framebuffer_size_callback calls render() synchronously during resize so image tracks window edge immediately. Forward declaration added to fix implicit decl error. Q/Escape close the window via key_callback. xorg_cli: --scale and --anchor arguments added. architecture.md: - Scale mode table and anchor docs in Frame Viewer Sink section - Render loop design note: frame-driven not timer-driven, resize callback rationale, threading note (GL context ownership, frame queue) - Text overlay section: tier 1 bitmap atlas (Pillow build tool, skyline packing, quad rendering), tier 2 HarfBuzz+FreeType, migration path Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,17 +16,20 @@
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/*
|
||||
* Full-screen quad via gl_VertexID — no VBO needed.
|
||||
* UV is flipped on Y so that image row 0 appears at the top of the window
|
||||
* (OpenGL texture origin is bottom-left; image data is stored top-row-first).
|
||||
* Full-screen quad via gl_VertexID.
|
||||
* u_uv_scale / u_uv_offset let render() apply UV crop for FILL and 1:1 modes.
|
||||
* UV Y is flipped: image row 0 appears at the top of the window.
|
||||
*/
|
||||
static const char *VERT_SRC =
|
||||
"#version 330 core\n"
|
||||
"out vec2 v_uv;\n"
|
||||
"uniform vec2 u_uv_scale;\n"
|
||||
"uniform vec2 u_uv_offset;\n"
|
||||
"void main() {\n"
|
||||
" float x = float(gl_VertexID & 1) * 2.0 - 1.0;\n"
|
||||
" float y = float((gl_VertexID >> 1) & 1) * 2.0 - 1.0;\n"
|
||||
" v_uv = vec2((x + 1.0) * 0.5, (1.0 - y) * 0.5);\n"
|
||||
" vec2 uv = vec2((x + 1.0) * 0.5, (1.0 - y) * 0.5);\n"
|
||||
" v_uv = uv * u_uv_scale + u_uv_offset;\n"
|
||||
" gl_Position = vec4(x, y, 0.0, 1.0);\n"
|
||||
"}\n";
|
||||
|
||||
@@ -62,17 +65,26 @@ static const char *FRAG_RGB_SRC =
|
||||
/* Viewer state */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
typedef enum { MODE_NONE, MODE_YUV, MODE_RGB } Render_Mode;
|
||||
|
||||
struct Xorg_Viewer {
|
||||
GLFWwindow *window;
|
||||
GLFWwindow *window;
|
||||
|
||||
GLuint prog_yuv;
|
||||
GLint u_tex_y, u_tex_cb, u_tex_cr;
|
||||
GLint u_uv_scale_yuv, u_uv_offset_yuv;
|
||||
|
||||
GLuint prog_rgb;
|
||||
GLint u_tex_rgb;
|
||||
GLint u_uv_scale_rgb, u_uv_offset_rgb;
|
||||
|
||||
GLuint vao;
|
||||
GLuint tex[4]; /* 0=Y 1=Cb 2=Cr 3=BGRA/RGB */
|
||||
GLuint vao;
|
||||
GLuint tex[4]; /* 0=Y 1=Cb 2=Cr 3=BGRA/RGB */
|
||||
Render_Mode mode;
|
||||
|
||||
Xorg_Scale scale;
|
||||
Xorg_Anchor anchor;
|
||||
int frame_w, frame_h;
|
||||
|
||||
#ifdef HAVE_TURBOJPEG
|
||||
tjhandle tj;
|
||||
@@ -133,8 +145,27 @@ static GLuint link_program(const char *frag_src)
|
||||
/* Public API */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static void render(Xorg_Viewer *v); /* forward declaration */
|
||||
|
||||
bool xorg_available(void) { return true; }
|
||||
|
||||
static void key_callback(GLFWwindow *window, int key, int scancode,
|
||||
int action, int mods)
|
||||
{
|
||||
(void)scancode; (void)mods;
|
||||
if (action == GLFW_PRESS &&
|
||||
(key == GLFW_KEY_ESCAPE || key == GLFW_KEY_Q)) {
|
||||
glfwSetWindowShouldClose(window, GLFW_TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
static void framebuffer_size_callback(GLFWwindow *window, int width, int height)
|
||||
{
|
||||
(void)width; (void)height;
|
||||
Xorg_Viewer *v = glfwGetWindowUserPointer(window);
|
||||
if (v) { render(v); }
|
||||
}
|
||||
|
||||
Xorg_Viewer *xorg_viewer_open(int x, int y, int width, int height,
|
||||
const char *title)
|
||||
{
|
||||
@@ -154,8 +185,9 @@ Xorg_Viewer *xorg_viewer_open(int x, int y, int width, int height,
|
||||
return NULL;
|
||||
}
|
||||
glfwSetWindowPos(win, x, y);
|
||||
glfwSetKeyCallback(win, key_callback);
|
||||
glfwMakeContextCurrent(win);
|
||||
glfwSwapInterval(1); /* vsync */
|
||||
glfwSwapInterval(1);
|
||||
|
||||
glewExperimental = GL_TRUE;
|
||||
if (glewInit() != GLEW_OK) {
|
||||
@@ -172,6 +204,10 @@ Xorg_Viewer *xorg_viewer_open(int x, int y, int width, int height,
|
||||
return NULL;
|
||||
}
|
||||
v->window = win;
|
||||
v->scale = XORG_SCALE_STRETCH;
|
||||
v->anchor = XORG_ANCHOR_CENTER;
|
||||
glfwSetWindowUserPointer(win, v);
|
||||
glfwSetFramebufferSizeCallback(win, framebuffer_size_callback);
|
||||
|
||||
v->prog_yuv = link_program(FRAG_YUV_SRC);
|
||||
v->prog_rgb = link_program(FRAG_RGB_SRC);
|
||||
@@ -180,10 +216,15 @@ Xorg_Viewer *xorg_viewer_open(int x, int y, int width, int height,
|
||||
return NULL;
|
||||
}
|
||||
|
||||
v->u_tex_y = glGetUniformLocation(v->prog_yuv, "u_tex_y");
|
||||
v->u_tex_cb = glGetUniformLocation(v->prog_yuv, "u_tex_cb");
|
||||
v->u_tex_cr = glGetUniformLocation(v->prog_yuv, "u_tex_cr");
|
||||
v->u_tex_rgb = glGetUniformLocation(v->prog_rgb, "u_tex_rgb");
|
||||
v->u_tex_y = glGetUniformLocation(v->prog_yuv, "u_tex_y");
|
||||
v->u_tex_cb = glGetUniformLocation(v->prog_yuv, "u_tex_cb");
|
||||
v->u_tex_cr = glGetUniformLocation(v->prog_yuv, "u_tex_cr");
|
||||
v->u_uv_scale_yuv = glGetUniformLocation(v->prog_yuv, "u_uv_scale");
|
||||
v->u_uv_offset_yuv= glGetUniformLocation(v->prog_yuv, "u_uv_offset");
|
||||
|
||||
v->u_tex_rgb = glGetUniformLocation(v->prog_rgb, "u_tex_rgb");
|
||||
v->u_uv_scale_rgb = glGetUniformLocation(v->prog_rgb, "u_uv_scale");
|
||||
v->u_uv_offset_rgb= glGetUniformLocation(v->prog_rgb, "u_uv_offset");
|
||||
|
||||
glGenVertexArrays(1, &v->vao);
|
||||
glGenTextures(4, v->tex);
|
||||
@@ -209,8 +250,144 @@ Xorg_Viewer *xorg_viewer_open(int x, int y, int width, int height,
|
||||
return v;
|
||||
}
|
||||
|
||||
void xorg_viewer_set_scale(Xorg_Viewer *v, Xorg_Scale scale)
|
||||
{
|
||||
if (v) { v->scale = scale; }
|
||||
}
|
||||
|
||||
void xorg_viewer_set_anchor(Xorg_Viewer *v, Xorg_Anchor anchor)
|
||||
{
|
||||
if (v) { v->anchor = anchor; }
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Internal: upload YUV planes and render */
|
||||
/* Internal: compute layout and render from existing textures */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static void render(Xorg_Viewer *v)
|
||||
{
|
||||
if (v->mode == MODE_NONE) { return; }
|
||||
|
||||
int fb_w, fb_h;
|
||||
glfwGetFramebufferSize(v->window, &fb_w, &fb_h);
|
||||
|
||||
int vp_x = 0, vp_y = 0, vp_w = fb_w, vp_h = fb_h;
|
||||
float uv_sx = 1.0f, uv_sy = 1.0f;
|
||||
float uv_ox = 0.0f, uv_oy = 0.0f;
|
||||
|
||||
int fw = v->frame_w, fh = v->frame_h;
|
||||
|
||||
if (fw > 0 && fh > 0 && v->scale != XORG_SCALE_STRETCH) {
|
||||
float fa = (float)fw / fh;
|
||||
float wa = (float)fb_w / fb_h;
|
||||
|
||||
switch (v->scale) {
|
||||
|
||||
case XORG_SCALE_FIT:
|
||||
/* Largest rect that fits; black bars fill the rest. */
|
||||
if (fa > wa) {
|
||||
vp_w = fb_w;
|
||||
vp_h = (int)(fb_w / fa);
|
||||
} else {
|
||||
vp_h = fb_h;
|
||||
vp_w = (int)(fb_h * fa);
|
||||
}
|
||||
vp_x = (v->anchor == XORG_ANCHOR_CENTER) ? (fb_w - vp_w) / 2 : 0;
|
||||
vp_y = (v->anchor == XORG_ANCHOR_CENTER) ? (fb_h - vp_h) / 2
|
||||
: fb_h - vp_h;
|
||||
break;
|
||||
|
||||
case XORG_SCALE_FILL:
|
||||
/*
|
||||
* Scale to cover the window; crop the overflowing axis.
|
||||
* UV range is narrowed on the cropped axis.
|
||||
*/
|
||||
if (fa > wa) {
|
||||
/* Frame wider than window: fit height, crop width. */
|
||||
uv_sx = wa / fa;
|
||||
uv_ox = (v->anchor == XORG_ANCHOR_CENTER)
|
||||
? (1.0f - uv_sx) * 0.5f : 0.0f;
|
||||
} else {
|
||||
/* Frame taller than window: fit width, crop height. */
|
||||
uv_sy = fa / wa;
|
||||
uv_oy = (v->anchor == XORG_ANCHOR_CENTER)
|
||||
? (1.0f - uv_sy) * 0.5f : 0.0f;
|
||||
}
|
||||
break;
|
||||
|
||||
case XORG_SCALE_1_1:
|
||||
/*
|
||||
* One frame pixel = one screen pixel.
|
||||
* If frame is larger than the window, the excess is cropped.
|
||||
*/
|
||||
vp_w = fw < fb_w ? fw : fb_w;
|
||||
vp_h = fh < fb_h ? fh : fb_h;
|
||||
|
||||
if (v->anchor == XORG_ANCHOR_CENTER) {
|
||||
vp_x = (fb_w - fw) / 2;
|
||||
vp_y = (fb_h - fh) / 2;
|
||||
/* If frame overflows, crop from centre via UV offset. */
|
||||
if (vp_x < 0) {
|
||||
uv_ox = (float)(-vp_x) / fw;
|
||||
uv_sx = (float)vp_w / fw;
|
||||
vp_x = 0;
|
||||
}
|
||||
if (vp_y < 0) {
|
||||
uv_oy = (float)(-vp_y) / fh;
|
||||
uv_sy = (float)vp_h / fh;
|
||||
vp_y = 0;
|
||||
}
|
||||
} else {
|
||||
/* Top-left anchor: show top-left portion of frame. */
|
||||
vp_x = 0;
|
||||
vp_y = fb_h - vp_h;
|
||||
uv_sx = (float)vp_w / fw;
|
||||
uv_sy = (float)vp_h / fh;
|
||||
}
|
||||
break;
|
||||
|
||||
case XORG_SCALE_STRETCH:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glViewport(vp_x, vp_y, vp_w, vp_h);
|
||||
|
||||
GLuint prog = (v->mode == MODE_YUV) ? v->prog_yuv : v->prog_rgb;
|
||||
GLint u_uv_scale = (v->mode == MODE_YUV) ? v->u_uv_scale_yuv : v->u_uv_scale_rgb;
|
||||
GLint u_uv_off = (v->mode == MODE_YUV) ? v->u_uv_offset_yuv : v->u_uv_offset_rgb;
|
||||
|
||||
glUseProgram(prog);
|
||||
glUniform2f(u_uv_scale, uv_sx, uv_sy);
|
||||
glUniform2f(u_uv_off, uv_ox, uv_oy);
|
||||
|
||||
if (v->mode == MODE_YUV) {
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, v->tex[0]);
|
||||
glUniform1i(v->u_tex_y, 0);
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
glBindTexture(GL_TEXTURE_2D, v->tex[1]);
|
||||
glUniform1i(v->u_tex_cb, 1);
|
||||
glActiveTexture(GL_TEXTURE2);
|
||||
glBindTexture(GL_TEXTURE_2D, v->tex[2]);
|
||||
glUniform1i(v->u_tex_cr, 2);
|
||||
} else {
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, v->tex[3]);
|
||||
glUniform1i(v->u_tex_rgb, 0);
|
||||
}
|
||||
|
||||
glBindVertexArray(v->vao);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glBindVertexArray(0);
|
||||
|
||||
glfwSwapBuffers(v->window);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Internal: upload YUV planes */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static void upload_yuv(Xorg_Viewer *v,
|
||||
@@ -234,29 +411,8 @@ static void upload_yuv(Xorg_Viewer *v,
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
int fb_w, fb_h;
|
||||
glfwGetFramebufferSize(v->window, &fb_w, &fb_h);
|
||||
glViewport(0, 0, fb_w, fb_h);
|
||||
|
||||
glUseProgram(v->prog_yuv);
|
||||
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, v->tex[0]);
|
||||
glUniform1i(v->u_tex_y, 0);
|
||||
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
glBindTexture(GL_TEXTURE_2D, v->tex[1]);
|
||||
glUniform1i(v->u_tex_cb, 1);
|
||||
|
||||
glActiveTexture(GL_TEXTURE2);
|
||||
glBindTexture(GL_TEXTURE_2D, v->tex[2]);
|
||||
glUniform1i(v->u_tex_cr, 2);
|
||||
|
||||
glBindVertexArray(v->vao);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glBindVertexArray(0);
|
||||
|
||||
glfwSwapBuffers(v->window);
|
||||
v->mode = MODE_YUV;
|
||||
render(v);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
@@ -268,6 +424,8 @@ bool xorg_viewer_push_yuv420(Xorg_Viewer *v,
|
||||
int width, int height)
|
||||
{
|
||||
if (!v) { return false; }
|
||||
v->frame_w = width;
|
||||
v->frame_h = height;
|
||||
upload_yuv(v, y, width, height, cb, width / 2, height / 2, cr);
|
||||
return true;
|
||||
}
|
||||
@@ -277,25 +435,16 @@ bool xorg_viewer_push_bgra(Xorg_Viewer *v,
|
||||
{
|
||||
if (!v) { return false; }
|
||||
|
||||
v->frame_w = width;
|
||||
v->frame_h = height;
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, v->tex[3]);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0,
|
||||
GL_BGRA, GL_UNSIGNED_BYTE, data);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
int fb_w, fb_h;
|
||||
glfwGetFramebufferSize(v->window, &fb_w, &fb_h);
|
||||
glViewport(0, 0, fb_w, fb_h);
|
||||
|
||||
glUseProgram(v->prog_rgb);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, v->tex[3]);
|
||||
glUniform1i(v->u_tex_rgb, 0);
|
||||
|
||||
glBindVertexArray(v->vao);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glBindVertexArray(0);
|
||||
|
||||
glfwSwapBuffers(v->window);
|
||||
v->mode = MODE_RGB;
|
||||
render(v);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -315,7 +464,6 @@ bool xorg_viewer_push_mjpeg(Xorg_Viewer *v,
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Reallocate if dimensions or subsampling changed. */
|
||||
if (w != v->tj_width || h != v->tj_height || subsamp != v->tj_subsamp) {
|
||||
free(v->yuv_buf);
|
||||
v->yuv_buf = malloc(tjBufSizeYUV2(w, 1, h, subsamp));
|
||||
@@ -342,6 +490,8 @@ bool xorg_viewer_push_mjpeg(Xorg_Viewer *v,
|
||||
return false;
|
||||
}
|
||||
|
||||
v->frame_w = w;
|
||||
v->frame_h = h;
|
||||
upload_yuv(v, planes[0], y_w, y_h, planes[1], c_w, c_h, planes[2]);
|
||||
return true;
|
||||
#endif
|
||||
@@ -355,7 +505,9 @@ bool xorg_viewer_poll(Xorg_Viewer *v)
|
||||
{
|
||||
if (!v || glfwWindowShouldClose(v->window)) { return false; }
|
||||
glfwPollEvents();
|
||||
return !glfwWindowShouldClose(v->window);
|
||||
if (glfwWindowShouldClose(v->window)) { return false; }
|
||||
render(v);
|
||||
return true;
|
||||
}
|
||||
|
||||
void xorg_viewer_close(Xorg_Viewer *v)
|
||||
|
||||
@@ -32,5 +32,7 @@ bool xorg_viewer_push_mjpeg(Xorg_Viewer *v,
|
||||
return false;
|
||||
}
|
||||
|
||||
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_close(Xorg_Viewer *v) { (void)v; }
|
||||
|
||||
Reference in New Issue
Block a user