From 7fd79e61205238764bdace31435eb63b902081d4 Mon Sep 17 00:00:00 2001 From: mikael-lovqvists-claude-agent Date: Sat, 28 Mar 2026 21:30:28 +0000 Subject: [PATCH] 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 --- architecture.md | 48 ++++++- dev/cli/Makefile | 14 +- dev/cli/xorg_cli.c | 125 +++++++++++++++++ include/xorg.h | 24 ++++ src/modules/xorg/xorg.c | 256 ++++++++++++++++++++++++++++------- src/modules/xorg/xorg_stub.c | 2 + 6 files changed, 413 insertions(+), 56 deletions(-) create mode 100644 dev/cli/xorg_cli.c diff --git a/architecture.md b/architecture.md index 1def3ea..31c0c89 100644 --- a/architecture.md +++ b/architecture.md @@ -396,7 +396,18 @@ The module can act as a video sink by creating a window and rendering the latest - Displays the most recently received frame — driven by the low-latency output mode of the relay; never buffers for completeness - Forwards keyboard and mouse events back upstream as `INPUT_EVENT` protocol messages, enabling remote control use cases -Scale and crop are applied in the renderer — the incoming frame is stretched or letterboxed to fill the window. This allows a high-resolution source (Pi camera, screen grab) to be displayed scaled-down on a different machine. +Scale and crop are applied in the renderer. Four display modes are supported (selected per viewer): + +| Mode | Behaviour | +|---|---| +| `STRETCH` | Fill window, ignore aspect ratio | +| `FIT` | Largest rect that fits, preserve aspect, black bars | +| `FILL` | Scale to cover, preserve aspect, crop edges | +| `1:1` | Native pixel size, no scaling; excess cropped | + +Each mode combines with an anchor (`CENTER` or `TOP_LEFT`) that controls placement when the frame does not fill the window exactly. + +This allows a high-resolution source (Pi camera, screen grab) to be displayed scaled-down on a different machine, or viewed at native resolution with panning. This makes it the display-side counterpart of the V4L2 capture source: a frame grabbed from a camera on a Pi can be viewed on any machine in the network running a viewer sink node, with the relay handling the path and delivery mode. @@ -417,6 +428,41 @@ For **raw pixel formats** (BGRA, YUV planar from the wire): uploaded directly wi This keeps CPU load minimal — the only CPU work for MJPEG is Huffman decode and DCT, which libjpeg-turbo runs with SIMD. All color conversion and scaling is on the GPU. +#### Text overlays (future) + +Two tiers are planned, implemented in order: + +**Tier 1 — bitmap font atlas (initial)** + +A build-time script (Python Pillow) renders glyphs from a TTF font into a packed PNG atlas and emits a metadata file (JSON or generated C header) with per-glyph UV rects and advance widths. At runtime the atlas is uploaded as a `GL_RGBA` texture and each character is rendered as a small quad, alpha-blended over the frame. Simple skyline packing keeps the atlas compact. + +The generator lives in `tools/gen_font_atlas/` and runs as part of `make build`. Sufficient for ASCII overlays: timestamps, stream labels, debug info. + +**Tier 2 — HarfBuzz + FreeType (later)** + +A proper runtime font stack for full typography: correct shaping, kerning, ligatures, bidirectional text, non-Latin scripts. Added as a feature flag with its own runtime deps alongside the blit path. + +When Tier 2 is implemented, the Pillow build dependency may be replaced by a purpose-built atlas generator (removing the Python dep entirely), if the blit path is still useful alongside the full shaping path. + +#### Render loop + +The viewer is driven by incoming frames rather than a fixed-rate loop. The intended pattern for callers: + +```c +while (xorg_viewer_poll(v)) { + if (new_frame_available()) { + xorg_viewer_push_yuv420(v, ...); /* upload + render */ + } + /* no new frame → no redundant GPU work */ +} +``` + +`xorg_viewer_poll` calls `glfwPollEvents` which dispatches input and resize events. A `framebuffer_size_callback` registered on the window calls `render()` synchronously during the resize, so the image tracks the window edge without a one-frame lag. This avoids both a busy render loop and the latency of waiting for the next poll iteration. + +For a static image (test tool, paused stream), `glfwWaitEventsTimeout(interval)` is a better substitute for `glfwPollEvents` — it sleeps until an event arrives or the timeout expires, eliminating idle CPU usage. + +Threading note: the GL context must be used from the thread that created it. In the video node, incoming frames arrive on a network receive thread. A frame queue between the receive thread and the render thread (which owns the GL context) is the correct model — the render thread drains the queue each poll iteration rather than having the network thread call push functions directly. + #### Renderer: Vulkan (future alternative) A Vulkan renderer is planned as an alternative to the OpenGL one. GLFW's surface creation API is renderer-agnostic, so the window management and input handling code is shared. Only the renderer backend changes. diff --git a/dev/cli/Makefile b/dev/cli/Makefile index 6e0c9b4..5f16916 100644 --- a/dev/cli/Makefile +++ b/dev/cli/Makefile @@ -11,6 +11,7 @@ DISCOVERY_OBJ = $(BUILD)/discovery/discovery.o CONFIG_OBJ = $(BUILD)/config/config.o PROTOCOL_OBJ = $(BUILD)/protocol/protocol.o TEST_IMAGE_OBJ = $(BUILD)/test_image/test_image.o +XORG_OBJ = $(BUILD)/xorg/xorg.o CLI_SRCS = \ media_ctrl_cli.c \ @@ -20,7 +21,8 @@ CLI_SRCS = \ config_cli.c \ protocol_cli.c \ query_cli.c \ - test_image_cli.c + test_image_cli.c \ + xorg_cli.c CLI_OBJS = $(CLI_SRCS:%.c=$(CLI_BUILD)/%.o) @@ -34,7 +36,8 @@ all: \ $(CLI_BUILD)/config_cli \ $(CLI_BUILD)/protocol_cli \ $(CLI_BUILD)/query_cli \ - $(CLI_BUILD)/test_image_cli + $(CLI_BUILD)/test_image_cli \ + $(CLI_BUILD)/xorg_cli # Module objects delegate to their sub-makes. $(COMMON_OBJ): ; $(MAKE) -C $(ROOT)/src/modules/common @@ -46,6 +49,7 @@ $(DISCOVERY_OBJ): ; $(MAKE) -C $(ROOT)/src/modules/discovery $(CONFIG_OBJ): ; $(MAKE) -C $(ROOT)/src/modules/config $(PROTOCOL_OBJ): ; $(MAKE) -C $(ROOT)/src/modules/protocol $(TEST_IMAGE_OBJ): ; $(MAKE) -C $(ROOT)/src/modules/test_image +$(XORG_OBJ): ; $(MAKE) -C $(ROOT)/src/modules/xorg # Compile each CLI source to its own .o (generates .d alongside). $(CLI_BUILD)/%.o: %.c | $(CLI_BUILD) @@ -76,6 +80,9 @@ $(CLI_BUILD)/query_cli: $(CLI_BUILD)/query_cli.o $(COMMON_OBJ) $(SERIAL_OBJ) $(T $(CLI_BUILD)/test_image_cli: $(CLI_BUILD)/test_image_cli.o $(TEST_IMAGE_OBJ) $(CC) $(CFLAGS) -o $@ $^ +$(CLI_BUILD)/xorg_cli: $(CLI_BUILD)/xorg_cli.o $(TEST_IMAGE_OBJ) $(XORG_OBJ) + $(CC) $(CFLAGS) -o $@ $^ $(PKG_LDFLAGS) + $(CLI_BUILD): mkdir -p $@ @@ -90,6 +97,7 @@ clean: $(CLI_BUILD)/config_cli \ $(CLI_BUILD)/protocol_cli \ $(CLI_BUILD)/query_cli \ - $(CLI_BUILD)/test_image_cli + $(CLI_BUILD)/test_image_cli \ + $(CLI_BUILD)/xorg_cli -include $(CLI_OBJS:%.o=%.d) diff --git a/dev/cli/xorg_cli.c b/dev/cli/xorg_cli.c new file mode 100644 index 0000000..593bec7 --- /dev/null +++ b/dev/cli/xorg_cli.c @@ -0,0 +1,125 @@ +#include +#include +#include + +#include "test_image.h" +#include "xorg.h" + +static void usage(void) +{ + fprintf(stderr, + "usage: xorg_cli [--pattern bars|ramp|grid]\n" + " [--width N] [--height N]\n" + " [--format yuv420|bgra]\n" + " [--scale stretch|fit|fill|1:1]\n" + " [--anchor center|topleft]\n" + " [--x N] [--y N]\n" + "\n" + "Opens a window and renders a test image using the xorg viewer sink.\n" + "Q or Escape closes the window.\n" + "\n" + "defaults: bars 1280x720 yuv420 stretch center at 0,0\n"); +} + +int main(int argc, char **argv) +{ + Test_Pattern pattern = TEST_PATTERN_BARS; + Test_Fmt fmt = TEST_FMT_YUV420; + Xorg_Scale scale = XORG_SCALE_STRETCH; + Xorg_Anchor anchor = XORG_ANCHOR_CENTER; + int width = 1280; + int height = 720; + int win_x = 0; + int win_y = 0; + + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "--pattern") == 0 && i + 1 < argc) { + i++; + if (strcmp(argv[i], "bars") == 0) { pattern = TEST_PATTERN_BARS; } + else if (strcmp(argv[i], "ramp") == 0) { pattern = TEST_PATTERN_RAMP; } + else if (strcmp(argv[i], "grid") == 0) { pattern = TEST_PATTERN_GRID; } + else { fprintf(stderr, "unknown pattern: %s\n", argv[i]); usage(); return 1; } + } else if (strcmp(argv[i], "--width") == 0 && i + 1 < argc) { + width = atoi(argv[++i]); + } else if (strcmp(argv[i], "--height") == 0 && i + 1 < argc) { + height = atoi(argv[++i]); + } else if (strcmp(argv[i], "--x") == 0 && i + 1 < argc) { + win_x = atoi(argv[++i]); + } else if (strcmp(argv[i], "--y") == 0 && i + 1 < argc) { + win_y = atoi(argv[++i]); + } else if (strcmp(argv[i], "--scale") == 0 && i + 1 < argc) { + i++; + if (strcmp(argv[i], "stretch") == 0) { scale = XORG_SCALE_STRETCH; } + else if (strcmp(argv[i], "fit") == 0) { scale = XORG_SCALE_FIT; } + else if (strcmp(argv[i], "fill") == 0) { scale = XORG_SCALE_FILL; } + else if (strcmp(argv[i], "1:1") == 0) { scale = XORG_SCALE_1_1; } + else { fprintf(stderr, "unknown scale: %s\n", argv[i]); usage(); return 1; } + } else if (strcmp(argv[i], "--anchor") == 0 && i + 1 < argc) { + i++; + if (strcmp(argv[i], "center") == 0) { anchor = XORG_ANCHOR_CENTER; } + else if (strcmp(argv[i], "topleft") == 0) { anchor = XORG_ANCHOR_TOP_LEFT; } + else { fprintf(stderr, "unknown anchor: %s\n", argv[i]); usage(); return 1; } + } else if (strcmp(argv[i], "--format") == 0 && i + 1 < argc) { + i++; + if (strcmp(argv[i], "yuv420") == 0) { fmt = TEST_FMT_YUV420; } + else if (strcmp(argv[i], "bgra") == 0) { fmt = TEST_FMT_BGRA; } + else { fprintf(stderr, "unknown format: %s\n", argv[i]); usage(); return 1; } + } else { + usage(); return 1; + } + } + + if (!xorg_available()) { + fprintf(stderr, "xorg_cli: built without HAVE_GLFW — viewer not available\n"); + return 1; + } + + if (width < 2 || height < 2) { + fprintf(stderr, "xorg_cli: width and height must be >= 2\n"); + return 1; + } + + Test_Frame *f = test_image_alloc(width, height, fmt); + if (!f) { + fprintf(stderr, "xorg_cli: allocation failed\n"); + return 1; + } + test_image_generate(f, pattern); + + const char *pat_name = pattern == TEST_PATTERN_BARS ? "bars" + : pattern == TEST_PATTERN_RAMP ? "ramp" + : "grid"; + const char *fmt_name = fmt == TEST_FMT_YUV420 ? "yuv420" : "bgra"; + const char *scale_name = scale == XORG_SCALE_STRETCH ? "stretch" + : scale == XORG_SCALE_FIT ? "fit" + : scale == XORG_SCALE_FILL ? "fill" + : "1:1"; + const char *anchor_name = anchor == XORG_ANCHOR_CENTER ? "center" : "topleft"; + + printf("opening %dx%d %s %s scale=%s anchor=%s at (%d,%d)\n", + width, height, fmt_name, pat_name, scale_name, anchor_name, win_x, win_y); + + Xorg_Viewer *v = xorg_viewer_open(win_x, win_y, width, height, "xorg_cli"); + xorg_viewer_set_scale(v, scale); + xorg_viewer_set_anchor(v, anchor); + if (!v) { + fprintf(stderr, "xorg_cli: failed to open viewer window\n"); + test_image_free(f); + return 1; + } + + if (fmt == TEST_FMT_YUV420) { + xorg_viewer_push_yuv420(v, + f->plane[0], f->plane[1], f->plane[2], + f->width, f->height); + } else { + xorg_viewer_push_bgra(v, f->plane[0], f->width, f->height); + } + + test_image_free(f); + + while (xorg_viewer_poll(v)) { /* wait for window close */ } + + xorg_viewer_close(v); + return 0; +} diff --git a/include/xorg.h b/include/xorg.h index 7436005..ebfb098 100644 --- a/include/xorg.h +++ b/include/xorg.h @@ -6,6 +6,26 @@ typedef struct Xorg_Viewer Xorg_Viewer; +/* + * How the frame is scaled to fit the window. + * SCALE_STRETCH is the only implemented mode; others are reserved. + */ +typedef enum Xorg_Scale { + XORG_SCALE_STRETCH, /* fill window, ignore aspect ratio (default) */ + XORG_SCALE_FIT, /* largest rect that fits, preserve aspect, black bars */ + XORG_SCALE_FILL, /* smallest rect that covers, preserve aspect, crop edges */ + XORG_SCALE_1_1, /* native pixel size, no scaling */ +} Xorg_Scale; + +/* + * Where the frame is positioned within the window. + * Used with XORG_SCALE_FIT, XORG_SCALE_FILL, and XORG_SCALE_1_1. + */ +typedef enum Xorg_Anchor { + XORG_ANCHOR_CENTER, /* center frame in window (default) */ + XORG_ANCHOR_TOP_LEFT, /* align frame to top-left corner */ +} Xorg_Anchor; + /* Returns false when compiled without HAVE_GLFW. */ bool xorg_available(void); @@ -32,6 +52,10 @@ bool xorg_viewer_push_bgra(Xorg_Viewer *v, bool xorg_viewer_push_mjpeg(Xorg_Viewer *v, const uint8_t *data, size_t size); +/* Change scale/anchor at any time; takes effect on the next render. */ +void xorg_viewer_set_scale(Xorg_Viewer *v, Xorg_Scale scale); +void xorg_viewer_set_anchor(Xorg_Viewer *v, Xorg_Anchor anchor); + /* * Process pending window events. * Returns false when the user has closed the window. diff --git a/src/modules/xorg/xorg.c b/src/modules/xorg/xorg.c index c4415ce..0cb40ab 100644 --- a/src/modules/xorg/xorg.c +++ b/src/modules/xorg/xorg.c @@ -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) diff --git a/src/modules/xorg/xorg_stub.c b/src/modules/xorg/xorg_stub.c index a5fc619..c534ebf 100644 --- a/src/modules/xorg/xorg_stub.c +++ b/src/modules/xorg/xorg_stub.c @@ -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; }