From 70d94aaf7b39e2cd7ad0c69544a01598025b2ae4 Mon Sep 17 00:00:00 2001 From: Jaro Marval Date: Wed, 30 Sep 2026 11:51:52 -0500 Subject: [PATCH 01/13] Seeding the OSD defaults before parsing so absent keys keep them --- src/app_config.c | 3 ++- src/region.c | 21 +++++++++++---------- src/region.h | 1 + 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/app_config.c b/src/app_config.c index ed828771..177ffc6c 100644 --- a/src/app_config.c +++ b/src/app_config.c @@ -380,6 +380,7 @@ enum ConfigError app_config_parse(void) { parse_bool(&ini, "mdns", "enable", &app_config.mdns_enable); + region_defaults(); parse_bool(&ini, "osd", "enable", &app_config.osd_enable); if (app_config.osd_enable) { for (char i = 0; i < MAX_OSD; i++) { @@ -408,7 +409,7 @@ enum ConfigError app_config_parse(void) { parse_int(&ini, "osd", param, 0, USHRT_MAX, &osds[i].outl); sprintf(param, "reg%d_thick", i); parse_double(&ini, "osd", param, 0, UCHAR_MAX, &osds[i].thick); - osds[i].updt = 1; + osds[i].updt = !EMPTY(osds[i].text) || !EMPTY(osds[i].img); } } diff --git a/src/region.c b/src/region.c index 23785663..9ae577ef 100644 --- a/src/region.c +++ b/src/region.c @@ -326,18 +326,9 @@ int region_prepare_bitmap(char *path, hal_bitmap *bitmap) { return EXIT_SUCCESS; } -void *region_thread(void) { - switch (plat) { -#if defined(__ARM_PCS_VFP) - case HAL_PLATFORM_I6: i6_region_init(); break; - case HAL_PLATFORM_I6C: i6c_region_init(); break; - case HAL_PLATFORM_M6: m6_region_init(); break; -#endif - } - +void region_defaults(void) { for (char id = 0; id < MAX_OSD; id++) { - if (!EMPTY(osds[id].text) || !EMPTY(osds[id].img)) continue; osds[id].hand = -1; osds[id].color = DEF_COLOR; osds[id].opal = DEF_OPAL; @@ -351,6 +342,16 @@ void *region_thread(void) { osds[id].text[0] = '\0'; osds[id].img[0] = '\0'; } +} + +void *region_thread(void) { + switch (plat) { +#if defined(__ARM_PCS_VFP) + case HAL_PLATFORM_I6: i6_region_init(); break; + case HAL_PLATFORM_I6C: i6c_region_init(); break; + case HAL_PLATFORM_M6: m6_region_init(); break; +#endif + } while (keepRunning) { for (char id = 0; id < MAX_OSD; id++) { diff --git a/src/region.h b/src/region.h index 50a3148b..bcfc0e5e 100644 --- a/src/region.h +++ b/src/region.h @@ -73,5 +73,6 @@ typedef struct { extern osd osds[MAX_OSD]; extern char timefmt[64]; +void region_defaults(void); int region_start(); void region_stop(); From 3a171ff8027126323ac76b60a1621463e82e41e7 Mon Sep 17 00:00:00 2001 From: Jaro Marval Date: Wed, 30 Sep 2026 11:54:54 -0500 Subject: [PATCH 02/13] Reporting i6 region attach and bitmap failures, attaching only once --- src/hal/star/i6_hal.c | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/hal/star/i6_hal.c b/src/hal/star/i6_hal.c index 1c48fd0c..f4450c22 100644 --- a/src/hal/star/i6_hal.c +++ b/src/hal/star/i6_hal.c @@ -381,7 +381,7 @@ void i6_pipeline_destroy(void) int i6_region_create(char handle, hal_rect rect, short opacity) { - int ret; + int ret, attach = 0; i6_sys_bind dest = { .module = 0, .device = _i6_vpe_dev, .channel = _i6_vpe_chn }; @@ -412,9 +412,10 @@ int i6_region_create(char handle, hal_rect rect, short opacity) return ret; } - if (i6_rgn.fnGetChannelConfig(handle, &dest, &attribCurr)) + if (i6_rgn.fnGetChannelConfig(handle, &dest, &attribCurr)) { HAL_INFO("i6_rgn", "Attaching region %d...\n", handle); - else if (attribCurr.point.x != rect.x || attribCurr.point.y != rect.y || + attach = 1; + } else if (attribCurr.point.x != rect.x || attribCurr.point.y != rect.y || attribCurr.osd.bgFgAlpha[1] != opacity) { HAL_INFO("i6_rgn", "Parameters are different, reattaching " "region %d...\n", handle); @@ -423,8 +424,12 @@ int i6_region_create(char handle, hal_rect rect, short opacity) dest.port = i; i6_rgn.fnDetachChannel(handle, &dest); } + attach = 1; } + if (!attach) + return EXIT_SUCCESS; + memset(&attrib, 0, sizeof(attrib)); attrib.show = 1; attrib.point.x = rect.x; @@ -434,13 +439,19 @@ int i6_region_create(char handle, hal_rect rect, short opacity) attrib.osd.bgFgAlpha[0] = 0; attrib.osd.bgFgAlpha[1] = opacity; + ret = EXIT_SUCCESS; for (char i = 0; i < I6_VENC_CHN_NUM; i++) { if (!i6_state[i].enable) continue; dest.port = i; - i6_rgn.fnAttachChannel(handle, &dest, &attrib); + int err = i6_rgn.fnAttachChannel(handle, &dest, &attrib); + if (err) { + HAL_DANGER("i6_rgn", "Attaching region %d to VPE port %d failed " + "with %#x!\n", handle, i, err); + ret = err; + } } - return EXIT_SUCCESS; + return ret; } void i6_region_deinit(void) @@ -472,7 +483,13 @@ int i6_region_setbitmap(int handle, hal_bitmap *bitmap) i6_rgn_bmp nativeBmp = { .data = bitmap->data, .pixFmt = I6_RGN_PIXFMT_ARGB1555, .size.height = bitmap->dim.height, .size.width = bitmap->dim.width }; - return i6_rgn.fnSetBitmap(handle, &nativeBmp); + int ret = i6_rgn.fnSetBitmap(handle, &nativeBmp); + if (ret) + HAL_DANGER("i6_rgn", "Setting the bitmap of region %d (%ux%u) " + "failed with %#x!\n", handle, nativeBmp.size.width, + nativeBmp.size.height, ret); + + return ret; } int i6_sensor_exposure(unsigned int micros) From 047b2a089847c65ac010fa96c104658e5dcbb39a Mon Sep 17 00:00:00 2001 From: Jaro Marval Date: Wed, 30 Sep 2026 12:21:15 -0500 Subject: [PATCH 03/13] Keeping i6 region canvases stable so the OSD stops churning the MMA heap A clock rendered with a proportional font changes its width every few seconds and the region got destroyed and recreated each time, freeing and reallocating its MMA blocks between the VPE frame buffers. The canvas is now rounded up to 32x16 and only grows, and the bitmap is padded with transparent pixels to its size. --- src/hal/star/i6_hal.c | 59 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 51 insertions(+), 8 deletions(-) diff --git a/src/hal/star/i6_hal.c b/src/hal/star/i6_hal.c index f4450c22..f33037c3 100644 --- a/src/hal/star/i6_hal.c +++ b/src/hal/star/i6_hal.c @@ -379,6 +379,14 @@ void i6_pipeline_destroy(void) i6_snr.fnDisable(_i6_snr_index); } +// A text rendered with a proportional font, like a clock, changes its width +// from one second to the next. Recreating the region each time frees and +// reallocates its MMA blocks between the VPE frame buffers, which cycle +// through the heap, until a port buffer no longer fits and frames drop. +// The canvas is rounded up and only grows, the bitmap is padded to it +#define I6_RGN_CANVAS_W 32 +#define I6_RGN_CANVAS_H 16 + int i6_region_create(char handle, hal_rect rect, short opacity) { int ret, attach = 0; @@ -390,18 +398,19 @@ int i6_region_create(char handle, hal_rect rect, short opacity) region.type = I6_RGN_TYPE_OSD; region.pixFmt = I6_RGN_PIXFMT_ARGB1555; - region.size.width = rect.width; - region.size.height = rect.height; + region.size.width = (rect.width + I6_RGN_CANVAS_W - 1) & ~(I6_RGN_CANVAS_W - 1); + region.size.height = (rect.height + I6_RGN_CANVAS_H - 1) & ~(I6_RGN_CANVAS_H - 1); if (i6_rgn.fnGetRegionConfig(handle, ®ionCurr)) { - HAL_INFO("i6_rgn", "Creating region %d...\n", handle); + HAL_INFO("i6_rgn", "Creating region %d (%ux%u)...\n", handle, + region.size.width, region.size.height); if (ret = i6_rgn.fnCreateRegion(handle, ®ion)) return ret; } else if (regionCurr.type != region.type || regionCurr.size.height != region.size.height || - regionCurr.size.width != region.size.width) { + regionCurr.size.width < region.size.width) { HAL_INFO("i6_rgn", "Parameters are different, recreating " - "region %d...\n", handle); + "region %d (%ux%u)...\n", handle, region.size.width, region.size.height); for (char i = 0; i < I6_VENC_CHN_NUM; i++) { if (!i6_state[i].enable) continue; dest.port = i; @@ -480,15 +489,49 @@ void i6_region_init(void) int i6_region_setbitmap(int handle, hal_bitmap *bitmap) { - i6_rgn_bmp nativeBmp = { .data = bitmap->data, .pixFmt = I6_RGN_PIXFMT_ARGB1555, - .size.height = bitmap->dim.height, .size.width = bitmap->dim.width }; + int ret; + i6_rgn_cnf region; + + if (ret = i6_rgn.fnGetRegionConfig(handle, ®ion)) { + HAL_DANGER("i6_rgn", "Reading the size of region %d failed " + "with %#x!\n", handle, ret); + return ret; + } + if (bitmap->dim.width > region.size.width || + bitmap->dim.height > region.size.height) { + HAL_DANGER("i6_rgn", "Bitmap %ux%u exceeds the %ux%u canvas " + "of region %d!\n", bitmap->dim.width, bitmap->dim.height, + region.size.width, region.size.height, handle); + return EXIT_FAILURE; + } - int ret = i6_rgn.fnSetBitmap(handle, &nativeBmp); + // ARGB1555 zero is a transparent pixel, the margin stays invisible + unsigned short *canvas = bitmap->data; + if (bitmap->dim.width != region.size.width || + bitmap->dim.height != region.size.height) { + canvas = calloc(region.size.width * region.size.height, sizeof(*canvas)); + if (!canvas) { + HAL_DANGER("i6_rgn", "Allocating the canvas of region %d failed!\n", handle); + return EXIT_FAILURE; + } + for (unsigned int y = 0; y < bitmap->dim.height; y++) + memcpy(canvas + y * region.size.width, + (unsigned short*)bitmap->data + y * bitmap->dim.width, + bitmap->dim.width * sizeof(*canvas)); + } + + i6_rgn_bmp nativeBmp = { .data = canvas, .pixFmt = I6_RGN_PIXFMT_ARGB1555, + .size.height = region.size.height, .size.width = region.size.width }; + + ret = i6_rgn.fnSetBitmap(handle, &nativeBmp); if (ret) HAL_DANGER("i6_rgn", "Setting the bitmap of region %d (%ux%u) " "failed with %#x!\n", handle, nativeBmp.size.width, nativeBmp.size.height, ret); + if (canvas != bitmap->data) + free(canvas); + return ret; } From 50f78c60466000a85929fef273c2f72d9b262438 Mon Sep 17 00:00:00 2001 From: Jaro Marval Date: Wed, 30 Sep 2026 13:02:44 -0500 Subject: [PATCH 04/13] Reserving i6 OSD canvases before the video pipeline Made from the region thread, a canvas lands in the MMA gap the encoder's frame buffers are taken from and returned to, and on SSC323 the main stream then drops to 13 fps with 3DNR on. Created once at startup, with a quarter more width than the text needs, it sits below them: 20 fps with OSD, 3DNR, JPEG snapshots and motion detection. --- src/hal/star/i6_hal.c | 24 +++++++++++++++++ src/hal/star/i6_hal.h | 1 + src/media.c | 3 +++ src/region.c | 60 +++++++++++++++++++++++++++++++------------ src/region.h | 1 + 5 files changed, 73 insertions(+), 16 deletions(-) diff --git a/src/hal/star/i6_hal.c b/src/hal/star/i6_hal.c index f33037c3..8a76ac09 100644 --- a/src/hal/star/i6_hal.c +++ b/src/hal/star/i6_hal.c @@ -463,9 +463,12 @@ int i6_region_create(char handle, hal_rect rect, short opacity) return ret; } +static char _i6_rgn_ready = 0; + void i6_region_deinit(void) { i6_rgn.fnDeinit(); + _i6_rgn_ready = 0; } void i6_region_destroy(char handle) @@ -483,8 +486,29 @@ void i6_region_destroy(char handle) void i6_region_init(void) { + if (_i6_rgn_ready) return; + i6_rgn_pal palette = {{{0, 0, 0, 0}}}; i6_rgn.fnInit(&palette); + _i6_rgn_ready = 1; +} + +int i6_region_prepare(char handle, short width, short height) +{ + i6_rgn_cnf region; + + i6_region_init(); + + memset(®ion, 0, sizeof(region)); + region.type = I6_RGN_TYPE_OSD; + region.pixFmt = I6_RGN_PIXFMT_ARGB1555; + // A quarter more than the text needs, so a clock or a counter can grow + region.size.width = (width + width / 4 + I6_RGN_CANVAS_W - 1) & ~(I6_RGN_CANVAS_W - 1); + region.size.height = (height + I6_RGN_CANVAS_H - 1) & ~(I6_RGN_CANVAS_H - 1); + + HAL_INFO("i6_rgn", "Reserving region %d (%ux%u)...\n", handle, + region.size.width, region.size.height); + return i6_rgn.fnCreateRegion(handle, ®ion); } int i6_region_setbitmap(int handle, hal_bitmap *bitmap) diff --git a/src/hal/star/i6_hal.h b/src/hal/star/i6_hal.h index 75e83c48..55b03645 100644 --- a/src/hal/star/i6_hal.h +++ b/src/hal/star/i6_hal.h @@ -42,6 +42,7 @@ int i6_region_create(char handle, hal_rect rect, short opacity); void i6_region_deinit(void); void i6_region_destroy(char handle); void i6_region_init(void); +int i6_region_prepare(char handle, short width, short height); int i6_region_setbitmap(int handle, hal_bitmap *bitmap); int i6_sensor_exposure(unsigned int micros); diff --git a/src/media.c b/src/media.c index e9cb510c..558ec411 100644 --- a/src/media.c +++ b/src/media.c @@ -1,4 +1,5 @@ #include "media.h" +#include "region.h" char audioOn = 0, udpOn = 0; pthread_mutex_t aencMtx, chnMtx, mp4Mtx; @@ -874,6 +875,8 @@ int sdk_start(void) { HAL_ERROR("media", "System initialization failed with %#x!\n%s\n", ret, errstr(ret)); + region_prepare(); + if (app_config.audio_enable) { ret = media_audio_enable(); if (ret) diff --git a/src/region.c b/src/region.c index 9ae577ef..3e7f5146 100644 --- a/src/region.c +++ b/src/region.c @@ -344,6 +344,47 @@ void region_defaults(void) { } } +static int region_font(const char *name, char *path) { + char *dirs[] = { + ".", + "/oem/usr/share", + "/usr/local/share/fonts", + "/usr/share/fonts/truetype", + "/usr/share/fonts", + NULL}; + + for (char **dir = dirs; *dir; dir++) { + sprintf(path, "%s/%s.ttf", *dir, name); + if (!access(path, F_OK)) return EXIT_SUCCESS; + sprintf(path, "%s/%s2.ttf", *dir, name); + if (!access(path, F_OK)) return EXIT_SUCCESS; + } + + return EXIT_FAILURE; +} + +// On infinity6 the text canvases have to exist before the video pipeline: made +// later, they land in the MMA gaps the encoder's frame buffers come and go +// from, and the main stream loses a third of its frames +void region_prepare(void) { + if (!app_config.osd_enable || plat != HAL_PLATFORM_I6) return; + +#if defined(__ARM_PCS_VFP) + for (char id = 0; id < MAX_OSD; id++) { + char font[256], out[80] = {0}; + + if (EMPTY(osds[id].text) || region_font(osds[id].font, font)) continue; + + strncpy(out, osds[id].text, sizeof(out) - 1); + region_fill_formatted(out); + hal_bitmap bitmap = text_create_rendered(font, osds[id].size, out, osds[id].color, + osds[id].outl, osds[id].thick); + i6_region_prepare(id, bitmap.dim.width, bitmap.dim.height); + free(bitmap.data); + } +#endif +} + void *region_thread(void) { switch (plat) { #if defined(__ARM_PCS_VFP) @@ -367,23 +408,10 @@ void *region_thread(void) { if (osds[id].updt) { char font[256]; - char *dirs[] = { - ".", - "/oem/usr/share", - "/usr/local/share/fonts", - "/usr/share/fonts/truetype", - "/usr/share/fonts", - NULL}; - char **dir = dirs; - while (*dir) { - sprintf(font, "%s/%s.ttf", *dir, osds[id].font); - if (!access(font, F_OK)) goto found_font; - sprintf(font, "%s/%s2.ttf", *dir++, osds[id].font); - if (!access(font, F_OK)) goto found_font; + if (region_font(osds[id].font, font)) { + HAL_DANGER("region", "Font \"%s\" not found!\n", osds[id].font); + continue; } - HAL_DANGER("region", "Font \"%s\" not found!\n", osds[id].font); - continue; -found_font:; hal_bitmap bitmap = text_create_rendered(font, osds[id].size, out, osds[id].color, osds[id].outl, osds[id].thick); hal_rect rect = { .height = bitmap.dim.height, .width = bitmap.dim.width, diff --git a/src/region.h b/src/region.h index bcfc0e5e..d5e9ddac 100644 --- a/src/region.h +++ b/src/region.h @@ -74,5 +74,6 @@ extern osd osds[MAX_OSD]; extern char timefmt[64]; void region_defaults(void); +void region_prepare(void); int region_start(); void region_stop(); From eb75159fbb117b2ef7c1d43fbe29f17b63fac00b Mon Sep 17 00:00:00 2001 From: Jaro Marval Date: Wed, 30 Sep 2026 13:28:53 -0500 Subject: [PATCH 05/13] Accepting single-quoted YAML values, as yaml-cli writes them --- src/hal/config.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/hal/config.c b/src/hal/config.c index f5f13e14..57b82d02 100644 --- a/src/hal/config.c +++ b/src/hal/config.c @@ -103,7 +103,8 @@ enum ConfigError parse_param_value_n( } param_value[res] = 0; - if (res >= 2 && param_value[0] == '"' && param_value[res - 1] == '"') { + if (res >= 2 && (param_value[0] == '"' || param_value[0] == '\'') && + param_value[res - 1] == param_value[0]) { memmove(param_value, param_value + 1, res - 2); param_value[res - 2] = '\0'; res -= 2; From 5143c4af819b95b77cf8dd0dc8e2f33dac50c858 Mon Sep 17 00:00:00 2001 From: Jaro Marval Date: Wed, 30 Sep 2026 13:28:53 -0500 Subject: [PATCH 06/13] Adding OSD background boxes and horizontally centered regions regX_bgcolor fills the text canvas with a color, with a small margin on either side, so white text stays readable over bright walls; it is also set and reported through /api/osd/X as bgcolor=#RRGGBB or none. A regX_posx of -1 centers the region on the main stream. --- doc/config.md | 3 ++- src/app_config.c | 5 ++++- src/region.c | 17 +++++++++++++---- src/region.h | 2 +- src/server.c | 12 ++++++++++-- src/text.c | 10 ++++++---- src/text.h | 4 ++-- 7 files changed, 38 insertions(+), 15 deletions(-) diff --git a/doc/config.md b/doc/config.md index cab0b10b..9e749129 100644 --- a/doc/config.md +++ b/doc/config.md @@ -96,12 +96,13 @@ This document describes the fields that can be found within a configuration file - **regX_text**: Text displayed in OSD region X. - **regX_font**: Font used for text in OSD region X. - **regX_opal**: Opacity of OSD region X. -- **regX_posx**: X position of OSD region X. +- **regX_posx**: X position of OSD region X; `-1` centers it on the main stream. - **regX_posy**: Y position of OSD region X. - **regX_size**: Size of the text or image in OSD region X. - **regX_color**: Color of the text or image in OSD region X. - **regX_outl**: Outline color of the text in OSD region X. - **regX_thick**: Thickness of the text outline in OSD region X. +- **regX_bgcolor**: Background of the text in OSD region X, in the same 15-bit format as the colors with the top bit set to show it: `0x8000` is an opaque black box, `0` none (default: `0`). Through `/api/osd/X`, `bgcolor=#RRGGBB` or `none`. ## JPEG section diff --git a/src/app_config.c b/src/app_config.c index 177ffc6c..c23d18e5 100644 --- a/src/app_config.c +++ b/src/app_config.c @@ -180,6 +180,7 @@ int app_config_save(void) { fprintf(file, " reg%d_size: %.1f\n", i, osds[i].size); fprintf(file, " reg%d_color: %#04x\n", i, osds[i].color); fprintf(file, " reg%d_outl: %#04x\n", i, osds[i].outl); + fprintf(file, " reg%d_bgcolor: %#04x\n", i, osds[i].bgcolor); fprintf(file, " reg%d_thick: %.1f\n", i, osds[i].thick); } @@ -396,7 +397,7 @@ enum ConfigError app_config_parse(void) { err = parse_int(&ini, "osd", param, 0, UCHAR_MAX, &val); if (err == CONFIG_OK) osds[i].opal = (unsigned char)val; sprintf(param, "reg%d_posx", i); - err = parse_int(&ini, "osd", param, 0, SHRT_MAX, &val); + err = parse_int(&ini, "osd", param, -1, SHRT_MAX, &val); if (err == CONFIG_OK) osds[i].posx = (short)val; sprintf(param, "reg%d_posy", i); err = parse_int(&ini, "osd", param, 0, SHRT_MAX, &val); @@ -407,6 +408,8 @@ enum ConfigError app_config_parse(void) { parse_int(&ini, "osd", param, 0, USHRT_MAX, &osds[i].color); sprintf(param, "reg%d_outl", i); parse_int(&ini, "osd", param, 0, USHRT_MAX, &osds[i].outl); + sprintf(param, "reg%d_bgcolor", i); + parse_int(&ini, "osd", param, 0, USHRT_MAX, &osds[i].bgcolor); sprintf(param, "reg%d_thick", i); parse_double(&ini, "osd", param, 0, UCHAR_MAX, &osds[i].thick); osds[i].updt = !EMPTY(osds[i].text) || !EMPTY(osds[i].img); diff --git a/src/region.c b/src/region.c index 3e7f5146..2493fb6c 100644 --- a/src/region.c +++ b/src/region.c @@ -336,6 +336,7 @@ void region_defaults(void) { osds[id].posx = DEF_POSX; osds[id].posy = DEF_POSY + (DEF_SIZE * 3 / 2) * id; osds[id].outl = DEF_OUTL; + osds[id].bgcolor = 0; osds[id].thick = DEF_THICK; osds[id].updt = 0; strncpy(osds[id].font, DEF_FONT, sizeof(osds[id].font) - 1); @@ -344,6 +345,14 @@ void region_defaults(void) { } } +// A negative X centers the region on the main stream +static short region_posx(char id, short width) { + if (osds[id].posx >= 0) return osds[id].posx; + + short frame = app_config.mp4_enable ? app_config.mp4_width : app_config.mjpeg_width; + return MAX(frame - width, 0) / 2 & ~1; +} + static int region_font(const char *name, char *path) { char *dirs[] = { ".", @@ -378,7 +387,7 @@ void region_prepare(void) { strncpy(out, osds[id].text, sizeof(out) - 1); region_fill_formatted(out); hal_bitmap bitmap = text_create_rendered(font, osds[id].size, out, osds[id].color, - osds[id].outl, osds[id].thick); + osds[id].outl, osds[id].thick, osds[id].bgcolor); i6_region_prepare(id, bitmap.dim.width, bitmap.dim.height); free(bitmap.data); } @@ -413,9 +422,9 @@ void *region_thread(void) { continue; } hal_bitmap bitmap = text_create_rendered(font, osds[id].size, out, osds[id].color, - osds[id].outl, osds[id].thick); + osds[id].outl, osds[id].thick, osds[id].bgcolor); hal_rect rect = { .height = bitmap.dim.height, .width = bitmap.dim.width, - .x = osds[id].posx, .y = osds[id].posy }; + .x = region_posx(id, bitmap.dim.width), .y = osds[id].posy }; switch (plat) { #if defined(__ARM_PCS_VFP) case HAL_PLATFORM_I6: @@ -482,7 +491,7 @@ void *region_thread(void) { if (!ret) { hal_rect rect = { .height = bitmap.dim.height, .width = bitmap.dim.width, - .x = osds[id].posx, .y = osds[id].posy }; + .x = region_posx(id, bitmap.dim.width), .y = osds[id].posy }; switch (plat) { #if defined(__ARM_PCS_VFP) case HAL_PLATFORM_I6: diff --git a/src/region.h b/src/region.h index d5e9ddac..bb0dd5ff 100644 --- a/src/region.h +++ b/src/region.h @@ -66,7 +66,7 @@ typedef struct { char font[32]; char text[80]; char img[64]; - int outl; + int outl, bgcolor; double thick; } osd; diff --git a/src/server.c b/src/server.c index 56c06497..11ea4ee1 100644 --- a/src/server.c +++ b/src/server.c @@ -1454,12 +1454,20 @@ void respond_request(http_request_t *req) { if (remain == value) continue; osds[id].thick = result; } + else if (EQUALS(key, "bgcolor")) + osds[id].bgcolor = EQUALS(value, "none") ? 0 : color_parse(value); } osds[id].updt = 1; } int color = (((osds[id].color >> 10) & 0x1F) * 255 / 31) << 16 | (((osds[id].color >> 5) & 0x1F) * 255 / 31) << 8 | ((osds[id].color & 0x1F) * 255 / 31); + char bgcolor[8] = "none"; + if (osds[id].bgcolor & 0x8000) + sprintf(bgcolor, "#%06x", + (((osds[id].bgcolor >> 10) & 0x1F) * 255 / 31) << 16 | + (((osds[id].bgcolor >> 5) & 0x1F) * 255 / 31) << 8 | + ((osds[id].bgcolor & 0x1F) * 255 / 31)); respLen = sprintf(response, "HTTP/1.1 200 OK\r\n" "Content-Type: application/json;charset=UTF-8\r\n" @@ -1467,10 +1475,10 @@ void respond_request(http_request_t *req) { "\r\n" "{\"id\":%d,\"color\":\"#%x\",\"opal\":%d,\"pos\":[%d,%d]," "\"font\":\"%s\",\"size\":%.1f,\"text\":\"%s\",\"img\":\"%s\"," - "\"outl\":\"#%x\",\"thick\":%.1f}", + "\"outl\":\"#%x\",\"thick\":%.1f,\"bgcolor\":\"%s\"}", id, color, osds[id].opal, osds[id].posx, osds[id].posy, osds[id].font, osds[id].size, osds[id].text, osds[id].img, - osds[id].outl, osds[id].thick); + osds[id].outl, osds[id].thick, bgcolor); send_and_close(req->clntFd, response, respLen); return; } diff --git a/src/text.c b/src/text.c index a4d190ae..9d6da595 100644 --- a/src/text.c +++ b/src/text.c @@ -172,18 +172,20 @@ void text_dim_rendered(double *margin, double *height, double *width, const char } hal_bitmap text_create_rendered(const char *font, double size, const char *text, - int color, int outline, double thick) + int color, int outline, double thick, int background) { text_load_font(&sft, font, size, &lmtx); double margin, height, width; text_dim_rendered(&margin, &height, &width, text); - text_new_rendered(&canvas, (CEILING(width) + 3) & ~3, CEILING(height), 0); + // A box around the text gets some room on either side + int pad = background ? CEILING(size / 4) : 0; + text_new_rendered(&canvas, (CEILING(width) + 2 * pad + 3) & ~3, CEILING(height), background); unsigned cps[strlen(text) + 1]; int n = utf8_to_utf32(text, cps, strlen(text) + 1); - double x = margin; + double x = margin + pad; double y = margin + lmtx.ascender + lmtx.lineGap; SFT_Glyph ogid = 0; for (int k = 0; k < n; k++) @@ -191,7 +193,7 @@ hal_bitmap text_create_rendered(const char *font, double size, const char *text, if (cps[k] == '\\' && cps[k + 1] == 'n') { k++; - x = margin; + x = margin + pad; y += lmtx.ascender - lmtx.descender + lmtx.lineGap; ogid = 0; continue; diff --git a/src/text.h b/src/text.h index 8ad294c9..a4f65425 100644 --- a/src/text.h +++ b/src/text.h @@ -5,5 +5,5 @@ #include "hal/support.h" #include "lib/schrift.h" -hal_bitmap text_create_rendered(const char *font, double size, const char *text, - int color, int outline, double thick); \ No newline at end of file +hal_bitmap text_create_rendered(const char *font, double size, const char *text, + int color, int outline, double thick, int background); \ No newline at end of file From 3493dd09708670294e282d66e900b31b72957a7b Mon Sep 17 00:00:00 2001 From: Jaro Marval Date: Wed, 30 Sep 2026 15:00:55 -0500 Subject: [PATCH 07/13] Documenting OSD background boxes and centered regions --- doc/endpoints.md | 6 ++++-- doc/overlays.md | 6 ++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/doc/endpoints.md b/doc/endpoints.md index 2e9c9fd4..3fa27f7d 100644 --- a/doc/endpoints.md +++ b/doc/endpoints.md @@ -241,10 +241,11 @@ Configures text or image overlays by their ID (0-9 at the moment). | GET | `color` | Font color (hex format, RGB555 format) | | GET | `opal` | Opacity level (0-255) | | GET | `pos` | Position on main stream \[x,y\] | -| GET | `posx` | X coordinate (write-only) | +| GET | `posx` | X coordinate, -1 to center (write-only) | | GET | `posy` | Y coordinate (write-only) | | GET | `outl` | Outline color (hex format, RGB555 format) | | GET | `thick` | Outline thickness (0 to disable) | +| GET | `bgcolor` | Background box color (hex format), `none` for none | | POST | `file` | Bitmap or PNG image to upload (replaces text) | **Response** @@ -258,7 +259,8 @@ Configures text or image overlays by their ID (0-9 at the moment). "size": 15.0, "text": "Backyard (%T)", "outl": "#8000", - "thick": 0.0 + "thick": 0.0, + "bgcolor": "none" } ``` diff --git a/doc/overlays.md b/doc/overlays.md index 3e366dac..758cf4d0 100644 --- a/doc/overlays.md +++ b/doc/overlays.md @@ -20,6 +20,12 @@ curl http://192.168.1.17/api/osd/1?color=%23FFFF&outl=%238000&thick=1.0 ``` N.B. Hashtags have to be espaced with %23 in curl URL syntaxes +A box behind the text keeps it readable over bright scenes, and a negative X centers a region: +``` +curl "http://192.168.1.17/api/osd/1?text=Backyard&bgcolor=%23000000&posx=-1&posy=0" +curl http://192.168.1.17/api/osd/1?bgcolor=none +``` + Empty strings are used to clear the regions: ``` curl http://192.168.1.17/api/osd/1?text= From 42957932a828b0bac6f9710237ea64b6a909cbc3 Mon Sep 17 00:00:00 2001 From: Jaro Marval Date: Wed, 30 Sep 2026 16:41:18 -0500 Subject: [PATCH 08/13] Setting the alpha bit on #RGB colors, which fill the OSD background box --- src/hal/tools.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hal/tools.c b/src/hal/tools.c index 83ec44af..c5d4fd93 100644 --- a/src/hal/tools.c +++ b/src/hal/tools.c @@ -97,7 +97,7 @@ int color_parse(const char *str) { int b = hex_to_int(str[3]); if (r >= 0 && g >= 0 && b >= 0) - return (1 << 16) | (r << 11) | (g << 6) | (b << 1); + return (1 << 15) | (r << 11) | (g << 6) | (b << 1); } // 8-bit hex format "#RRGGBB" From c3a2fd13a61fdfc0d88c5469b5deb428cce465af Mon Sep 17 00:00:00 2001 From: Jaro Marval Date: Wed, 30 Sep 2026 16:41:29 -0500 Subject: [PATCH 09/13] Ignoring bgcolor values that are neither a hex color nor none --- doc/config.md | 2 +- src/server.c | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/doc/config.md b/doc/config.md index 9e749129..3aafca42 100644 --- a/doc/config.md +++ b/doc/config.md @@ -102,7 +102,7 @@ This document describes the fields that can be found within a configuration file - **regX_color**: Color of the text or image in OSD region X. - **regX_outl**: Outline color of the text in OSD region X. - **regX_thick**: Thickness of the text outline in OSD region X. -- **regX_bgcolor**: Background of the text in OSD region X, in the same 15-bit format as the colors with the top bit set to show it: `0x8000` is an opaque black box, `0` none (default: `0`). Through `/api/osd/X`, `bgcolor=#RRGGBB` or `none`. +- **regX_bgcolor**: Background of the text in OSD region X, in the same 15-bit format as the colors with the top bit set to show it: `0x8000` is an opaque black box, `0` none (default: `0`). Through `/api/osd/X`, `bgcolor=#RGB`, `#RRGGBB` or `none`, other values leave it unchanged. ## JPEG section diff --git a/src/server.c b/src/server.c index 11ea4ee1..a7d98a5e 100644 --- a/src/server.c +++ b/src/server.c @@ -1454,8 +1454,14 @@ void respond_request(http_request_t *req) { if (remain == value) continue; osds[id].thick = result; } - else if (EQUALS(key, "bgcolor")) - osds[id].bgcolor = EQUALS(value, "none") ? 0 : color_parse(value); + else if (EQUALS(key, "bgcolor")) { + size_t len = strlen(value); + if (EMPTY(value) || EQUALS(value, "none") || EQUALS(value, "0")) + osds[id].bgcolor = 0; + else if (value[0] == '#' && (len == 4 || len == 7) && + strspn(value + 1, "0123456789abcdefABCDEF") == len - 1) + osds[id].bgcolor = color_parse(value); + } } osds[id].updt = 1; } From 3f87539daa74e11fa844089d2dcb8919df555dfc Mon Sep 17 00:00:00 2001 From: Jaro Marval Date: Wed, 30 Sep 2026 16:41:50 -0500 Subject: [PATCH 10/13] Attaching i6 regions per VPE port, so ports enabled later get them --- src/hal/star/i6_hal.c | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/src/hal/star/i6_hal.c b/src/hal/star/i6_hal.c index 8a76ac09..9c9383ec 100644 --- a/src/hal/star/i6_hal.c +++ b/src/hal/star/i6_hal.c @@ -389,7 +389,7 @@ void i6_pipeline_destroy(void) int i6_region_create(char handle, hal_rect rect, short opacity) { - int ret, attach = 0; + int ret; i6_sys_bind dest = { .module = 0, .device = _i6_vpe_dev, .channel = _i6_vpe_chn }; @@ -421,24 +421,6 @@ int i6_region_create(char handle, hal_rect rect, short opacity) return ret; } - if (i6_rgn.fnGetChannelConfig(handle, &dest, &attribCurr)) { - HAL_INFO("i6_rgn", "Attaching region %d...\n", handle); - attach = 1; - } else if (attribCurr.point.x != rect.x || attribCurr.point.y != rect.y || - attribCurr.osd.bgFgAlpha[1] != opacity) { - HAL_INFO("i6_rgn", "Parameters are different, reattaching " - "region %d...\n", handle); - for (char i = 0; i < I6_VENC_CHN_NUM; i++) { - if (!i6_state[i].enable) continue; - dest.port = i; - i6_rgn.fnDetachChannel(handle, &dest); - } - attach = 1; - } - - if (!attach) - return EXIT_SUCCESS; - memset(&attrib, 0, sizeof(attrib)); attrib.show = 1; attrib.point.x = rect.x; @@ -448,10 +430,21 @@ int i6_region_create(char handle, hal_rect rect, short opacity) attrib.osd.bgFgAlpha[0] = 0; attrib.osd.bgFgAlpha[1] = opacity; + // Each port is checked on its own, so one enabled after the region was + // attached still gets it ret = EXIT_SUCCESS; for (char i = 0; i < I6_VENC_CHN_NUM; i++) { if (!i6_state[i].enable) continue; dest.port = i; + if (!i6_rgn.fnGetChannelConfig(handle, &dest, &attribCurr)) { + if (attribCurr.point.x == rect.x && attribCurr.point.y == rect.y && + attribCurr.osd.bgFgAlpha[1] == opacity) + continue; + HAL_INFO("i6_rgn", "Parameters are different, reattaching " + "region %d to VPE port %d...\n", handle, i); + i6_rgn.fnDetachChannel(handle, &dest); + } else + HAL_INFO("i6_rgn", "Attaching region %d to VPE port %d...\n", handle, i); int err = i6_rgn.fnAttachChannel(handle, &dest, &attrib); if (err) { HAL_DANGER("i6_rgn", "Attaching region %d to VPE port %d failed " From 9eee9157fabb714c4a747b4d71e3a4add0c6e8bd Mon Sep 17 00:00:00 2001 From: Jaro Marval Date: Wed, 30 Sep 2026 16:42:02 -0500 Subject: [PATCH 11/13] Rounding the width a region is centered by, so a clock stays put --- src/region.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/region.c b/src/region.c index 2493fb6c..6d69b800 100644 --- a/src/region.c +++ b/src/region.c @@ -345,12 +345,13 @@ void region_defaults(void) { } } -// A negative X centers the region on the main stream +// A negative X centers the region on the main stream, the width is rounded +// so a clock does not move, and get reattached, every second static short region_posx(char id, short width) { if (osds[id].posx >= 0) return osds[id].posx; short frame = app_config.mp4_enable ? app_config.mp4_width : app_config.mjpeg_width; - return MAX(frame - width, 0) / 2 & ~1; + return MAX(frame - ((width + 31) & ~31), 0) / 2 & ~1; } static int region_font(const char *name, char *path) { From 99b5dcae19ede688bf9ef8e2ff9c231759d81dcc Mon Sep 17 00:00:00 2001 From: Jaro Marval Date: Wed, 30 Sep 2026 16:43:42 -0500 Subject: [PATCH 12/13] Keeping padded i6 canvases within the frame's right edge --- src/hal/star/i6_hal.c | 21 ++++++++++++++++----- src/hal/star/i6_hal.h | 4 ++-- src/region.c | 14 +++++++++----- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/hal/star/i6_hal.c b/src/hal/star/i6_hal.c index 9c9383ec..f46ad487 100644 --- a/src/hal/star/i6_hal.c +++ b/src/hal/star/i6_hal.c @@ -387,7 +387,15 @@ void i6_pipeline_destroy(void) #define I6_RGN_CANVAS_W 32 #define I6_RGN_CANVAS_H 16 -int i6_region_create(char handle, hal_rect rect, short opacity) +// The padding stays within the room left to the frame's right edge, a +// bitmap that does not fit there on its own keeps its exact width +static unsigned int i6_region_width(unsigned int canvas, short width, short room) +{ + if (room < width) room = width; + return MIN(canvas, (unsigned int)room); +} + +int i6_region_create(char handle, hal_rect rect, short opacity, short room) { int ret; @@ -398,7 +406,8 @@ int i6_region_create(char handle, hal_rect rect, short opacity) region.type = I6_RGN_TYPE_OSD; region.pixFmt = I6_RGN_PIXFMT_ARGB1555; - region.size.width = (rect.width + I6_RGN_CANVAS_W - 1) & ~(I6_RGN_CANVAS_W - 1); + region.size.width = i6_region_width( + (rect.width + I6_RGN_CANVAS_W - 1) & ~(I6_RGN_CANVAS_W - 1), rect.width, room); region.size.height = (rect.height + I6_RGN_CANVAS_H - 1) & ~(I6_RGN_CANVAS_H - 1); if (i6_rgn.fnGetRegionConfig(handle, ®ionCurr)) { @@ -408,7 +417,8 @@ int i6_region_create(char handle, hal_rect rect, short opacity) return ret; } else if (regionCurr.type != region.type || regionCurr.size.height != region.size.height || - regionCurr.size.width < region.size.width) { + regionCurr.size.width < region.size.width || + regionCurr.size.width > i6_region_width(regionCurr.size.width, rect.width, room)) { HAL_INFO("i6_rgn", "Parameters are different, recreating " "region %d (%ux%u)...\n", handle, region.size.width, region.size.height); for (char i = 0; i < I6_VENC_CHN_NUM; i++) { @@ -486,7 +496,7 @@ void i6_region_init(void) _i6_rgn_ready = 1; } -int i6_region_prepare(char handle, short width, short height) +int i6_region_prepare(char handle, short width, short height, short room) { i6_rgn_cnf region; @@ -496,7 +506,8 @@ int i6_region_prepare(char handle, short width, short height) region.type = I6_RGN_TYPE_OSD; region.pixFmt = I6_RGN_PIXFMT_ARGB1555; // A quarter more than the text needs, so a clock or a counter can grow - region.size.width = (width + width / 4 + I6_RGN_CANVAS_W - 1) & ~(I6_RGN_CANVAS_W - 1); + region.size.width = i6_region_width( + (width + width / 4 + I6_RGN_CANVAS_W - 1) & ~(I6_RGN_CANVAS_W - 1), width, room); region.size.height = (height + I6_RGN_CANVAS_H - 1) & ~(I6_RGN_CANVAS_H - 1); HAL_INFO("i6_rgn", "Reserving region %d (%ux%u)...\n", handle, diff --git a/src/hal/star/i6_hal.h b/src/hal/star/i6_hal.h index 55b03645..abae584e 100644 --- a/src/hal/star/i6_hal.h +++ b/src/hal/star/i6_hal.h @@ -38,11 +38,11 @@ int i6_config_load(char *path); int i6_pipeline_create(char index, short width, short height, char mirror, char flip, char framerate); void i6_pipeline_destroy(void); -int i6_region_create(char handle, hal_rect rect, short opacity); +int i6_region_create(char handle, hal_rect rect, short opacity, short room); void i6_region_deinit(void); void i6_region_destroy(char handle); void i6_region_init(void); -int i6_region_prepare(char handle, short width, short height); +int i6_region_prepare(char handle, short width, short height, short room); int i6_region_setbitmap(int handle, hal_bitmap *bitmap); int i6_sensor_exposure(unsigned int micros); diff --git a/src/region.c b/src/region.c index 6d69b800..bf115c42 100644 --- a/src/region.c +++ b/src/region.c @@ -345,13 +345,16 @@ void region_defaults(void) { } } +static short region_frame(void) { + return app_config.mp4_enable ? app_config.mp4_width : app_config.mjpeg_width; +} + // A negative X centers the region on the main stream, the width is rounded // so a clock does not move, and get reattached, every second static short region_posx(char id, short width) { if (osds[id].posx >= 0) return osds[id].posx; - short frame = app_config.mp4_enable ? app_config.mp4_width : app_config.mjpeg_width; - return MAX(frame - ((width + 31) & ~31), 0) / 2 & ~1; + return MAX(region_frame() - ((width + 31) & ~31), 0) / 2 & ~1; } static int region_font(const char *name, char *path) { @@ -389,7 +392,8 @@ void region_prepare(void) { region_fill_formatted(out); hal_bitmap bitmap = text_create_rendered(font, osds[id].size, out, osds[id].color, osds[id].outl, osds[id].thick, osds[id].bgcolor); - i6_region_prepare(id, bitmap.dim.width, bitmap.dim.height); + i6_region_prepare(id, bitmap.dim.width, bitmap.dim.height, + region_frame() - region_posx(id, bitmap.dim.width)); free(bitmap.data); } #endif @@ -429,7 +433,7 @@ void *region_thread(void) { switch (plat) { #if defined(__ARM_PCS_VFP) case HAL_PLATFORM_I6: - i6_region_create(id, rect, osds[id].opal); + i6_region_create(id, rect, osds[id].opal, region_frame() - rect.x); i6_region_setbitmap(id, &bitmap); break; case HAL_PLATFORM_I6C: @@ -496,7 +500,7 @@ void *region_thread(void) { switch (plat) { #if defined(__ARM_PCS_VFP) case HAL_PLATFORM_I6: - i6_region_create(id, rect, osds[id].opal); + i6_region_create(id, rect, osds[id].opal, region_frame() - rect.x); i6_region_setbitmap(id, &bitmap); break; case HAL_PLATFORM_I6C: From e16a0bdd048f0ab044ac7c5cda8ca6b634fbce3c Mon Sep 17 00:00:00 2001 From: Jaro Marval Date: Wed, 30 Sep 2026 16:50:02 -0500 Subject: [PATCH 13/13] Leaving the YUYV snapshot port out of i6 regions on SSC32x --- src/hal/star/i6_hal.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/hal/star/i6_hal.c b/src/hal/star/i6_hal.c index f46ad487..6b79d33c 100644 --- a/src/hal/star/i6_hal.c +++ b/src/hal/star/i6_hal.c @@ -445,6 +445,9 @@ int i6_region_create(char handle, hal_rect rect, short opacity, short room) ret = EXIT_SUCCESS; for (char i = 0; i < I6_VENC_CHN_NUM; i++) { if (!i6_state[i].enable) continue; + // On SSC32x the snapshot port is YUYV, where a region comes out + // stretched with every glyph doubled + if (series == 0xEF && i6_state[i].payload == HAL_VIDCODEC_JPG) continue; dest.port = i; if (!i6_rgn.fnGetChannelConfig(handle, &dest, &attribCurr)) { if (attribCurr.point.x == rect.x && attribCurr.point.y == rect.y &&