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:
@@ -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
|
- 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
|
- 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.
|
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.
|
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)
|
#### 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.
|
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.
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ DISCOVERY_OBJ = $(BUILD)/discovery/discovery.o
|
|||||||
CONFIG_OBJ = $(BUILD)/config/config.o
|
CONFIG_OBJ = $(BUILD)/config/config.o
|
||||||
PROTOCOL_OBJ = $(BUILD)/protocol/protocol.o
|
PROTOCOL_OBJ = $(BUILD)/protocol/protocol.o
|
||||||
TEST_IMAGE_OBJ = $(BUILD)/test_image/test_image.o
|
TEST_IMAGE_OBJ = $(BUILD)/test_image/test_image.o
|
||||||
|
XORG_OBJ = $(BUILD)/xorg/xorg.o
|
||||||
|
|
||||||
CLI_SRCS = \
|
CLI_SRCS = \
|
||||||
media_ctrl_cli.c \
|
media_ctrl_cli.c \
|
||||||
@@ -20,7 +21,8 @@ CLI_SRCS = \
|
|||||||
config_cli.c \
|
config_cli.c \
|
||||||
protocol_cli.c \
|
protocol_cli.c \
|
||||||
query_cli.c \
|
query_cli.c \
|
||||||
test_image_cli.c
|
test_image_cli.c \
|
||||||
|
xorg_cli.c
|
||||||
|
|
||||||
CLI_OBJS = $(CLI_SRCS:%.c=$(CLI_BUILD)/%.o)
|
CLI_OBJS = $(CLI_SRCS:%.c=$(CLI_BUILD)/%.o)
|
||||||
|
|
||||||
@@ -34,7 +36,8 @@ all: \
|
|||||||
$(CLI_BUILD)/config_cli \
|
$(CLI_BUILD)/config_cli \
|
||||||
$(CLI_BUILD)/protocol_cli \
|
$(CLI_BUILD)/protocol_cli \
|
||||||
$(CLI_BUILD)/query_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.
|
# Module objects delegate to their sub-makes.
|
||||||
$(COMMON_OBJ): ; $(MAKE) -C $(ROOT)/src/modules/common
|
$(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
|
$(CONFIG_OBJ): ; $(MAKE) -C $(ROOT)/src/modules/config
|
||||||
$(PROTOCOL_OBJ): ; $(MAKE) -C $(ROOT)/src/modules/protocol
|
$(PROTOCOL_OBJ): ; $(MAKE) -C $(ROOT)/src/modules/protocol
|
||||||
$(TEST_IMAGE_OBJ): ; $(MAKE) -C $(ROOT)/src/modules/test_image
|
$(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).
|
# Compile each CLI source to its own .o (generates .d alongside).
|
||||||
$(CLI_BUILD)/%.o: %.c | $(CLI_BUILD)
|
$(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)
|
$(CLI_BUILD)/test_image_cli: $(CLI_BUILD)/test_image_cli.o $(TEST_IMAGE_OBJ)
|
||||||
$(CC) $(CFLAGS) -o $@ $^
|
$(CC) $(CFLAGS) -o $@ $^
|
||||||
|
|
||||||
|
$(CLI_BUILD)/xorg_cli: $(CLI_BUILD)/xorg_cli.o $(TEST_IMAGE_OBJ) $(XORG_OBJ)
|
||||||
|
$(CC) $(CFLAGS) -o $@ $^ $(PKG_LDFLAGS)
|
||||||
|
|
||||||
$(CLI_BUILD):
|
$(CLI_BUILD):
|
||||||
mkdir -p $@
|
mkdir -p $@
|
||||||
|
|
||||||
@@ -90,6 +97,7 @@ clean:
|
|||||||
$(CLI_BUILD)/config_cli \
|
$(CLI_BUILD)/config_cli \
|
||||||
$(CLI_BUILD)/protocol_cli \
|
$(CLI_BUILD)/protocol_cli \
|
||||||
$(CLI_BUILD)/query_cli \
|
$(CLI_BUILD)/query_cli \
|
||||||
$(CLI_BUILD)/test_image_cli
|
$(CLI_BUILD)/test_image_cli \
|
||||||
|
$(CLI_BUILD)/xorg_cli
|
||||||
|
|
||||||
-include $(CLI_OBJS:%.o=%.d)
|
-include $(CLI_OBJS:%.o=%.d)
|
||||||
|
|||||||
125
dev/cli/xorg_cli.c
Normal file
125
dev/cli/xorg_cli.c
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#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;
|
||||||
|
}
|
||||||
@@ -6,6 +6,26 @@
|
|||||||
|
|
||||||
typedef struct Xorg_Viewer Xorg_Viewer;
|
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. */
|
/* Returns false when compiled without HAVE_GLFW. */
|
||||||
bool xorg_available(void);
|
bool xorg_available(void);
|
||||||
|
|
||||||
@@ -32,6 +52,10 @@ bool xorg_viewer_push_bgra(Xorg_Viewer *v,
|
|||||||
bool xorg_viewer_push_mjpeg(Xorg_Viewer *v,
|
bool xorg_viewer_push_mjpeg(Xorg_Viewer *v,
|
||||||
const uint8_t *data, size_t size);
|
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.
|
* Process pending window events.
|
||||||
* Returns false when the user has closed the window.
|
* Returns false when the user has closed the window.
|
||||||
|
|||||||
@@ -16,17 +16,20 @@
|
|||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Full-screen quad via gl_VertexID — no VBO needed.
|
* Full-screen quad via gl_VertexID.
|
||||||
* UV is flipped on Y so that image row 0 appears at the top of the window
|
* u_uv_scale / u_uv_offset let render() apply UV crop for FILL and 1:1 modes.
|
||||||
* (OpenGL texture origin is bottom-left; image data is stored top-row-first).
|
* UV Y is flipped: image row 0 appears at the top of the window.
|
||||||
*/
|
*/
|
||||||
static const char *VERT_SRC =
|
static const char *VERT_SRC =
|
||||||
"#version 330 core\n"
|
"#version 330 core\n"
|
||||||
"out vec2 v_uv;\n"
|
"out vec2 v_uv;\n"
|
||||||
|
"uniform vec2 u_uv_scale;\n"
|
||||||
|
"uniform vec2 u_uv_offset;\n"
|
||||||
"void main() {\n"
|
"void main() {\n"
|
||||||
" float x = float(gl_VertexID & 1) * 2.0 - 1.0;\n"
|
" float x = float(gl_VertexID & 1) * 2.0 - 1.0;\n"
|
||||||
" float y = float((gl_VertexID >> 1) & 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"
|
" gl_Position = vec4(x, y, 0.0, 1.0);\n"
|
||||||
"}\n";
|
"}\n";
|
||||||
|
|
||||||
@@ -62,17 +65,26 @@ static const char *FRAG_RGB_SRC =
|
|||||||
/* Viewer state */
|
/* Viewer state */
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
typedef enum { MODE_NONE, MODE_YUV, MODE_RGB } Render_Mode;
|
||||||
|
|
||||||
struct Xorg_Viewer {
|
struct Xorg_Viewer {
|
||||||
GLFWwindow *window;
|
GLFWwindow *window;
|
||||||
|
|
||||||
GLuint prog_yuv;
|
GLuint prog_yuv;
|
||||||
GLint u_tex_y, u_tex_cb, u_tex_cr;
|
GLint u_tex_y, u_tex_cb, u_tex_cr;
|
||||||
|
GLint u_uv_scale_yuv, u_uv_offset_yuv;
|
||||||
|
|
||||||
GLuint prog_rgb;
|
GLuint prog_rgb;
|
||||||
GLint u_tex_rgb;
|
GLint u_tex_rgb;
|
||||||
|
GLint u_uv_scale_rgb, u_uv_offset_rgb;
|
||||||
|
|
||||||
GLuint vao;
|
GLuint vao;
|
||||||
GLuint tex[4]; /* 0=Y 1=Cb 2=Cr 3=BGRA/RGB */
|
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
|
#ifdef HAVE_TURBOJPEG
|
||||||
tjhandle tj;
|
tjhandle tj;
|
||||||
@@ -133,8 +145,27 @@ static GLuint link_program(const char *frag_src)
|
|||||||
/* Public API */
|
/* Public API */
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
static void render(Xorg_Viewer *v); /* forward declaration */
|
||||||
|
|
||||||
bool xorg_available(void) { return true; }
|
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,
|
Xorg_Viewer *xorg_viewer_open(int x, int y, int width, int height,
|
||||||
const char *title)
|
const char *title)
|
||||||
{
|
{
|
||||||
@@ -154,8 +185,9 @@ Xorg_Viewer *xorg_viewer_open(int x, int y, int width, int height,
|
|||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
glfwSetWindowPos(win, x, y);
|
glfwSetWindowPos(win, x, y);
|
||||||
|
glfwSetKeyCallback(win, key_callback);
|
||||||
glfwMakeContextCurrent(win);
|
glfwMakeContextCurrent(win);
|
||||||
glfwSwapInterval(1); /* vsync */
|
glfwSwapInterval(1);
|
||||||
|
|
||||||
glewExperimental = GL_TRUE;
|
glewExperimental = GL_TRUE;
|
||||||
if (glewInit() != GLEW_OK) {
|
if (glewInit() != GLEW_OK) {
|
||||||
@@ -172,6 +204,10 @@ Xorg_Viewer *xorg_viewer_open(int x, int y, int width, int height,
|
|||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
v->window = win;
|
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_yuv = link_program(FRAG_YUV_SRC);
|
||||||
v->prog_rgb = link_program(FRAG_RGB_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;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
v->u_tex_y = glGetUniformLocation(v->prog_yuv, "u_tex_y");
|
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_cb = glGetUniformLocation(v->prog_yuv, "u_tex_cb");
|
||||||
v->u_tex_cr = glGetUniformLocation(v->prog_yuv, "u_tex_cr");
|
v->u_tex_cr = glGetUniformLocation(v->prog_yuv, "u_tex_cr");
|
||||||
v->u_tex_rgb = glGetUniformLocation(v->prog_rgb, "u_tex_rgb");
|
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);
|
glGenVertexArrays(1, &v->vao);
|
||||||
glGenTextures(4, v->tex);
|
glGenTextures(4, v->tex);
|
||||||
@@ -209,8 +250,144 @@ Xorg_Viewer *xorg_viewer_open(int x, int y, int width, int height,
|
|||||||
return v;
|
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,
|
static void upload_yuv(Xorg_Viewer *v,
|
||||||
@@ -234,29 +411,8 @@ static void upload_yuv(Xorg_Viewer *v,
|
|||||||
|
|
||||||
glBindTexture(GL_TEXTURE_2D, 0);
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
|
||||||
int fb_w, fb_h;
|
v->mode = MODE_YUV;
|
||||||
glfwGetFramebufferSize(v->window, &fb_w, &fb_h);
|
render(v);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
@@ -268,6 +424,8 @@ bool xorg_viewer_push_yuv420(Xorg_Viewer *v,
|
|||||||
int width, int height)
|
int width, int height)
|
||||||
{
|
{
|
||||||
if (!v) { return false; }
|
if (!v) { return false; }
|
||||||
|
v->frame_w = width;
|
||||||
|
v->frame_h = height;
|
||||||
upload_yuv(v, y, width, height, cb, width / 2, height / 2, cr);
|
upload_yuv(v, y, width, height, cb, width / 2, height / 2, cr);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -277,25 +435,16 @@ bool xorg_viewer_push_bgra(Xorg_Viewer *v,
|
|||||||
{
|
{
|
||||||
if (!v) { return false; }
|
if (!v) { return false; }
|
||||||
|
|
||||||
|
v->frame_w = width;
|
||||||
|
v->frame_h = height;
|
||||||
|
|
||||||
glBindTexture(GL_TEXTURE_2D, v->tex[3]);
|
glBindTexture(GL_TEXTURE_2D, v->tex[3]);
|
||||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0,
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0,
|
||||||
GL_BGRA, GL_UNSIGNED_BYTE, data);
|
GL_BGRA, GL_UNSIGNED_BYTE, data);
|
||||||
glBindTexture(GL_TEXTURE_2D, 0);
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
|
||||||
int fb_w, fb_h;
|
v->mode = MODE_RGB;
|
||||||
glfwGetFramebufferSize(v->window, &fb_w, &fb_h);
|
render(v);
|
||||||
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);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,7 +464,6 @@ bool xorg_viewer_push_mjpeg(Xorg_Viewer *v,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Reallocate if dimensions or subsampling changed. */
|
|
||||||
if (w != v->tj_width || h != v->tj_height || subsamp != v->tj_subsamp) {
|
if (w != v->tj_width || h != v->tj_height || subsamp != v->tj_subsamp) {
|
||||||
free(v->yuv_buf);
|
free(v->yuv_buf);
|
||||||
v->yuv_buf = malloc(tjBufSizeYUV2(w, 1, h, subsamp));
|
v->yuv_buf = malloc(tjBufSizeYUV2(w, 1, h, subsamp));
|
||||||
@@ -342,6 +490,8 @@ bool xorg_viewer_push_mjpeg(Xorg_Viewer *v,
|
|||||||
return false;
|
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]);
|
upload_yuv(v, planes[0], y_w, y_h, planes[1], c_w, c_h, planes[2]);
|
||||||
return true;
|
return true;
|
||||||
#endif
|
#endif
|
||||||
@@ -355,7 +505,9 @@ bool xorg_viewer_poll(Xorg_Viewer *v)
|
|||||||
{
|
{
|
||||||
if (!v || glfwWindowShouldClose(v->window)) { return false; }
|
if (!v || glfwWindowShouldClose(v->window)) { return false; }
|
||||||
glfwPollEvents();
|
glfwPollEvents();
|
||||||
return !glfwWindowShouldClose(v->window);
|
if (glfwWindowShouldClose(v->window)) { return false; }
|
||||||
|
render(v);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void xorg_viewer_close(Xorg_Viewer *v)
|
void xorg_viewer_close(Xorg_Viewer *v)
|
||||||
|
|||||||
@@ -32,5 +32,7 @@ bool xorg_viewer_push_mjpeg(Xorg_Viewer *v,
|
|||||||
return false;
|
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; }
|
bool xorg_viewer_poll(Xorg_Viewer *v) { (void)v; return false; }
|
||||||
void xorg_viewer_close(Xorg_Viewer *v) { (void)v; }
|
void xorg_viewer_close(Xorg_Viewer *v) { (void)v; }
|
||||||
|
|||||||
Reference in New Issue
Block a user