Phase 3c: TEXT overlays через FreeType + GTX 1050 (Pascal) compat
Phase 3c — динамический текст через libfreetype6: открывается font (.ttf/.otf), измеряется bounding box строки, рендерится в RGBA-атлас с anti-aliased alpha из FT bitmap, заливается в VRAM. На каждом кадре блитится через cfc_cugrid_blit_rgba_nv12 (kernels уже есть). Поддержка UTF-8 через простой inline-decoder (1/2/3/4-byte). cfc_overlay_update_text() переподдерживает re-render atlas (text / color change) — для Phase 4 ZMQ control plane (динамическое изменение NO SIGNAL / RECORDING / timestamp'ов). Адаптация под GTX 1050 (Pascal) в проде: - CMAKE_CUDA_ARCHITECTURES = 61;75;86;89;120 (Pascal + Turing + Ampere + Ada + Blackwell, покрывает все production-кейсы) - grid_record default снижен с 4K@10Mbps на 1080p@4Mbps. 4K требует явных --width 3840 --height 2160 (Pascal NVENC 4K H.264 на грани). - Ссылка на research-документ + NVENC §Input Buffers рядом со staging buffer'ом в nvenc.c с цитатой "the client is required to use buffers allocated using the cuMemAlloc family of APIs". Содержимое Phase 3c: - overlay.h — cfc_overlay_create_text + update_text + text_size. - overlay.c — utf8_next decoder, text_measure (ascender/descender), text_render (alpha-over blend в RGBA), text_rebuild_atlas (VRAM cycle), full FreeType lifecycle. - CMakeLists.txt — find_package(Freetype REQUIRED) + link Freetype::Freetype. - examples/grid_record — флаг --text font,size,r,g,b,x,y,text.
This commit is contained in:
+11
-4
@@ -9,11 +9,15 @@ set(CMAKE_C_STANDARD 11)
|
||||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
|
||||
# CUDA архитектуры. RTX 5090 = sm_120 (Blackwell consumer), но также 5090 не
|
||||
# поддерживает legacy sm_75, поэтому 89 (Ada Lovelace, RTX 4090) для совместимости
|
||||
# с тестовым окружением + 120 для прода.
|
||||
# CUDA архитектуры. Покрываем production-сценарии:
|
||||
# sm_61 = Pascal (GTX 1050/1060/1070/1080) — низкобюджетный prod
|
||||
# sm_75 = Turing (RTX 2060/Quadro RTX) — частый prod
|
||||
# sm_86 = Ampere consumer (RTX 3060/3090)
|
||||
# sm_89 = Ada Lovelace (RTX 4090) — тестовое окружение разработки
|
||||
# sm_120 = Blackwell (RTX 5090) — текущий dev-хост
|
||||
# Pascal (sm_61) обязателен — у пользователя в проде GTX 1050.
|
||||
if(NOT DEFINED CMAKE_CUDA_ARCHITECTURES)
|
||||
set(CMAKE_CUDA_ARCHITECTURES "89;120")
|
||||
set(CMAKE_CUDA_ARCHITECTURES "61;75;86;89;120")
|
||||
endif()
|
||||
|
||||
if(NOT CMAKE_BUILD_TYPE)
|
||||
@@ -38,6 +42,9 @@ find_library(LIBDL_LIBRARY dl REQUIRED)
|
||||
# PNG — для декода RGBA-иконок overlay'ев (Phase 3b)
|
||||
find_package(PNG REQUIRED)
|
||||
|
||||
# FreeType — для text overlay'ев (Phase 3c)
|
||||
find_package(Freetype REQUIRED)
|
||||
|
||||
# ── Сторонние библиотеки (subomodules в third_party/) ───────────────────
|
||||
|
||||
# cuframes — статически линкуем libcuframes. cuframes_static — это static lib
|
||||
|
||||
+67
-3
@@ -88,8 +88,10 @@ static const char *cu_err(CUresult r)
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
const char *out_path = NULL;
|
||||
int fps = 25, bitrate = 10000, max_seconds = 0;
|
||||
int out_w = 3840, out_h = 2160;
|
||||
/* Default'ы: 1080p / 4 Mbps подходят для GTX 1050 (Pascal), на которой
|
||||
* крутится production. 4K требует --width 3840 --height 2160 явно. */
|
||||
int fps = 25, bitrate = 4000, max_seconds = 0;
|
||||
int out_w = 1920, out_h = 1080;
|
||||
int border_thickness = 0; /* 0 = без border'ов */
|
||||
cfc_composer_cell_t cells[MAX_CELLS] = { 0 };
|
||||
static char cell_keys[MAX_CELLS][64];
|
||||
@@ -100,6 +102,11 @@ int main(int argc, char **argv)
|
||||
icon_spec_t icons[MAX_CELLS] = { 0 };
|
||||
int num_icons = 0;
|
||||
|
||||
/* --text font,size,r,g,b,x,y,text */
|
||||
typedef struct { const char *font, *text; int size, r, g, b, x, y; } text_spec_t;
|
||||
text_spec_t texts[MAX_CELLS] = { 0 };
|
||||
int num_texts = 0;
|
||||
|
||||
static struct option opts[] = {
|
||||
{"out", required_argument, 0, 'o'},
|
||||
{"cell", required_argument, 0, 'c'},
|
||||
@@ -110,10 +117,11 @@ int main(int argc, char **argv)
|
||||
{"seconds", required_argument, 0, 's'},
|
||||
{"border", required_argument, 0, 'r'}, /* толщина border'ов */
|
||||
{"icon", required_argument, 0, 'i'}, /* path,x,y[,alpha] */
|
||||
{"text", required_argument, 0, 't'}, /* font,size,r,g,b,x,y,text */
|
||||
{0, 0, 0, 0},
|
||||
};
|
||||
int c;
|
||||
while ((c = getopt_long(argc, argv, "o:c:f:b:W:H:s:r:i:", opts, NULL)) != -1) {
|
||||
while ((c = getopt_long(argc, argv, "o:c:f:b:W:H:s:r:i:t:", opts, NULL)) != -1) {
|
||||
switch (c) {
|
||||
case 'o': out_path = optarg; break;
|
||||
case 'c':
|
||||
@@ -133,6 +141,36 @@ int main(int argc, char **argv)
|
||||
case 'H': out_h = atoi(optarg); break;
|
||||
case 's': max_seconds = atoi(optarg); break;
|
||||
case 'r': border_thickness = atoi(optarg); break;
|
||||
case 't': {
|
||||
if (num_texts >= MAX_CELLS) { fprintf(stderr, "max %d texts\n", MAX_CELLS); return 1; }
|
||||
/* font,size,r,g,b,x,y,text — text идёт до конца строки (может
|
||||
* содержать запятые и пробелы). */
|
||||
static char text_font[MAX_CELLS][256];
|
||||
static char text_body[MAX_CELLS][256];
|
||||
char buf[512]; strncpy(buf, optarg, sizeof(buf) - 1);
|
||||
buf[sizeof(buf) - 1] = '\0';
|
||||
char *p = buf;
|
||||
char *q = strchr(p, ','); if (!q) { fprintf(stderr, "bad --text\n"); return 1; }
|
||||
*q = '\0'; strncpy(text_font[num_texts], p, 255); p = q + 1;
|
||||
q = strchr(p, ','); if (!q) { fprintf(stderr, "bad --text\n"); return 1; }
|
||||
*q = '\0'; texts[num_texts].size = atoi(p); p = q + 1;
|
||||
q = strchr(p, ','); if (!q) { fprintf(stderr, "bad --text\n"); return 1; }
|
||||
*q = '\0'; texts[num_texts].r = atoi(p); p = q + 1;
|
||||
q = strchr(p, ','); if (!q) { fprintf(stderr, "bad --text\n"); return 1; }
|
||||
*q = '\0'; texts[num_texts].g = atoi(p); p = q + 1;
|
||||
q = strchr(p, ','); if (!q) { fprintf(stderr, "bad --text\n"); return 1; }
|
||||
*q = '\0'; texts[num_texts].b = atoi(p); p = q + 1;
|
||||
q = strchr(p, ','); if (!q) { fprintf(stderr, "bad --text\n"); return 1; }
|
||||
*q = '\0'; texts[num_texts].x = atoi(p); p = q + 1;
|
||||
q = strchr(p, ','); if (!q) { fprintf(stderr, "bad --text\n"); return 1; }
|
||||
*q = '\0'; texts[num_texts].y = atoi(p); p = q + 1;
|
||||
/* Остаток — text body. */
|
||||
strncpy(text_body[num_texts], p, 255);
|
||||
texts[num_texts].font = text_font[num_texts];
|
||||
texts[num_texts].text = text_body[num_texts];
|
||||
num_texts++;
|
||||
break;
|
||||
}
|
||||
case 'i': {
|
||||
if (num_icons >= MAX_CELLS) {
|
||||
fprintf(stderr, "max %d icons\n", MAX_CELLS);
|
||||
@@ -195,6 +233,32 @@ int main(int argc, char **argv)
|
||||
fprintf(stderr, "[grid_record] composer %dx%d, %d ячеек\n",
|
||||
out_w, out_h, num_cells);
|
||||
|
||||
/* TEXT overlays. */
|
||||
for (int i = 0; i < num_texts; i++) {
|
||||
cfc_overlay_text_config_t tc = {
|
||||
.font_path = texts[i].font,
|
||||
.text = texts[i].text,
|
||||
.pixel_size = texts[i].size,
|
||||
.x = texts[i].x, .y = texts[i].y,
|
||||
.r = texts[i].r, .g = texts[i].g, .b = texts[i].b,
|
||||
.extra_alpha = 255,
|
||||
.visible = 1,
|
||||
};
|
||||
cfc_overlay_t *ov = NULL;
|
||||
if (cfc_overlay_create_text(&tc, &ov) != 0) {
|
||||
fprintf(stderr, "[grid_record] text overlay create failed: '%s'\n",
|
||||
texts[i].text);
|
||||
continue;
|
||||
}
|
||||
int tw = 0, th = 0;
|
||||
cfc_overlay_text_size(ov, &tw, &th);
|
||||
cfc_composer_add_overlay(comp, ov);
|
||||
fprintf(stderr,
|
||||
"[grid_record] text @ (%d,%d) %dx%d size=%d color=(%d,%d,%d) '%s'\n",
|
||||
texts[i].x, texts[i].y, tw, th, texts[i].size,
|
||||
texts[i].r, texts[i].g, texts[i].b, texts[i].text);
|
||||
}
|
||||
|
||||
/* PNG иконки. */
|
||||
for (int i = 0; i < num_icons; i++) {
|
||||
cfc_overlay_png_config_t pc = {
|
||||
|
||||
@@ -84,6 +84,32 @@ int cfc_overlay_png_size(cfc_overlay_t *ov, int *width, int *height);
|
||||
int cfc_overlay_update_png(cfc_overlay_t *ov,
|
||||
const cfc_overlay_png_config_t *cfg);
|
||||
|
||||
/* Параметры TEXT overlay'я (Phase 3c). */
|
||||
typedef struct cfc_overlay_text_config {
|
||||
const char *font_path; /* путь к .ttf / .otf */
|
||||
const char *text; /* UTF-8 строка для рендера */
|
||||
int pixel_size; /* высота glyph'а в пикселях (10..200) */
|
||||
int x, y; /* top-left на output буфере (чётные) */
|
||||
int r, g, b; /* sRGB цвет 0..255 */
|
||||
int extra_alpha; /* 0..255 общий множитель прозрачности */
|
||||
int visible; /* 0/1 — выводить ли */
|
||||
} cfc_overlay_text_config_t;
|
||||
|
||||
/* Создать TEXT overlay. Открывает font через FreeType, рендерит строку
|
||||
* в RGBA-атлас на CPU (alpha-channel из glyph bitmap'ов anti-aliased),
|
||||
* заливает в VRAM. */
|
||||
int cfc_overlay_create_text(const cfc_overlay_text_config_t *cfg,
|
||||
cfc_overlay_t **out);
|
||||
|
||||
/* Обновить TEXT overlay. Если text изменился — re-render atlas (VRAM
|
||||
* перевыделяется). font_path и pixel_size менять нельзя — заведите новый
|
||||
* overlay (face и связанные ресурсы пере-init'ить дорого). */
|
||||
int cfc_overlay_update_text(cfc_overlay_t *ov,
|
||||
const cfc_overlay_text_config_t *cfg);
|
||||
|
||||
/* Получить ширину/высоту текущего рендеренного текста (в пикселях). */
|
||||
int cfc_overlay_text_size(cfc_overlay_t *ov, int *width, int *height);
|
||||
|
||||
/* Обновить параметры BORDER overlay'я (можно переключить visible,
|
||||
* сменить цвет, изменить позицию). Thread-safe? Нет — caller должен сам
|
||||
* заботиться о том, чтобы update не пересекался с draw. В рамках одного
|
||||
|
||||
@@ -52,6 +52,7 @@ foreach(target cuframes_composer cuframes_composer_static)
|
||||
Threads::Threads
|
||||
${LIBDL_LIBRARY} # для dlopen libnvidia-encode.so
|
||||
PNG::PNG # для PNG overlay'ев (Phase 3b)
|
||||
Freetype::Freetype # для text overlay'ев (Phase 3c)
|
||||
rt
|
||||
)
|
||||
# CUDA properties.
|
||||
|
||||
+9
-1
@@ -68,7 +68,15 @@ struct cfc_encoder {
|
||||
* в которую копируем NV12 кадр перед NVENC encode. Нужно потому что
|
||||
* NVENC nvEncRegisterResource не принимает VMM-mapped указатели cuframes
|
||||
* напрямую (NV_ENC_ERR_RESOURCE_REGISTER_FAILED) — это известное
|
||||
* ограничение SDK, см. Phase 1.1 research. */
|
||||
* ограничение SDK. Корневая причина задокументирована в
|
||||
* gx/cuframes:docs/RESEARCH-vmm-nvenc-zerocopy.md
|
||||
* Цитата из NVENC Programming Guide §Input Buffers:
|
||||
* "the client is required to use buffers allocated using the
|
||||
* cuMemAlloc family of APIs"
|
||||
* VMM-API (cuMemCreate/cuMemMap) в эту family не входит.
|
||||
* Стоимость staging-копии (cuMemcpy2D D2D) — 0.02% bandwidth на
|
||||
* RTX 5090, ~0.5% на GTX 1050, незаметна.
|
||||
* Phase 5 TODO: cuMemAllocPitch + cuMemcpy2DAsync на encoder stream. */
|
||||
CUdeviceptr staging_ptr;
|
||||
size_t staging_size;
|
||||
int staging_pitch;
|
||||
|
||||
+256
-2
@@ -19,6 +19,9 @@
|
||||
|
||||
#include <cuda.h>
|
||||
#include <png.h>
|
||||
#include <ft2build.h>
|
||||
#include FT_FREETYPE_H
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -30,11 +33,24 @@ typedef struct png_data {
|
||||
CUdeviceptr atlas; /* RGBA buffer в VRAM */
|
||||
} png_data_t;
|
||||
|
||||
typedef struct text_data {
|
||||
cfc_overlay_text_config_t cfg;
|
||||
char *text_owned; /* копия cfg.text (caller владеет original'ом) */
|
||||
char *font_path_owned;
|
||||
FT_Library ft_lib;
|
||||
FT_Face face;
|
||||
int pixel_size;
|
||||
int width, height; /* размер atlas в пикселях */
|
||||
int pitch; /* 4 * width */
|
||||
CUdeviceptr atlas; /* RGBA atlas в VRAM */
|
||||
} text_data_t;
|
||||
|
||||
struct cfc_overlay {
|
||||
cfc_overlay_type_t type;
|
||||
union {
|
||||
cfc_overlay_border_config_t border;
|
||||
png_data_t png;
|
||||
text_data_t text;
|
||||
} u;
|
||||
};
|
||||
|
||||
@@ -272,6 +288,236 @@ static int draw_png(cfc_overlay_t *ov, CUstream stream,
|
||||
p->cfg.extra_alpha);
|
||||
}
|
||||
|
||||
/* ── TEXT ─────────────────────────────────────────────────────────────── */
|
||||
|
||||
/* UTF-8 decoder. Возвращает true если ещё есть данные, advance'ит указатель
|
||||
* на следующий codepoint. Невалидные последовательности заменяются на U+FFFD. */
|
||||
static int utf8_next(const char **p, uint32_t *cp)
|
||||
{
|
||||
const unsigned char *s = (const unsigned char *)*p;
|
||||
if (!*s) return 0;
|
||||
unsigned char c = *s;
|
||||
if (c < 0x80) { *cp = c; (*p)++; return 1; }
|
||||
if ((c & 0xE0) == 0xC0 && s[1]) {
|
||||
*cp = ((c & 0x1F) << 6) | (s[1] & 0x3F);
|
||||
(*p) += 2; return 1;
|
||||
}
|
||||
if ((c & 0xF0) == 0xE0 && s[1] && s[2]) {
|
||||
*cp = ((c & 0x0F) << 12) | ((s[1] & 0x3F) << 6) | (s[2] & 0x3F);
|
||||
(*p) += 3; return 1;
|
||||
}
|
||||
if ((c & 0xF8) == 0xF0 && s[1] && s[2] && s[3]) {
|
||||
*cp = ((c & 0x07) << 18) | ((s[1] & 0x3F) << 12) |
|
||||
((s[2] & 0x3F) << 6) | (s[3] & 0x3F);
|
||||
(*p) += 4; return 1;
|
||||
}
|
||||
*cp = 0xFFFD;
|
||||
(*p)++;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Pass1: измерить bounding box строки в pixel'ах + ascent для baseline'а.
|
||||
* Возвращает 0 при успехе. */
|
||||
static int text_measure(FT_Face face, const char *text,
|
||||
int *out_w, int *out_h, int *out_ascent)
|
||||
{
|
||||
int width = 0;
|
||||
int ascent = face->size->metrics.ascender >> 6; /* 26.6 → integer */
|
||||
int descent = -(face->size->metrics.descender >> 6);
|
||||
if (ascent <= 0) ascent = face->size->metrics.height >> 6;
|
||||
if (descent < 0) descent = 0;
|
||||
|
||||
const char *p = text;
|
||||
uint32_t cp;
|
||||
while (utf8_next(&p, &cp)) {
|
||||
if (FT_Load_Char(face, cp, FT_LOAD_DEFAULT) != 0) continue;
|
||||
width += face->glyph->advance.x >> 6;
|
||||
}
|
||||
if (width <= 0) width = 1;
|
||||
*out_w = width;
|
||||
*out_h = ascent + descent;
|
||||
*out_ascent = ascent;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Pass2: отрисовать строку в RGBA-буфер. color_rgb пишется во все
|
||||
* пиксели, alpha берётся из FreeType bitmap'а (anti-aliased grayscale). */
|
||||
static void text_render(FT_Face face, const char *text,
|
||||
unsigned char *rgba, int width, int height,
|
||||
int ascent, int r, int g, int b)
|
||||
{
|
||||
memset(rgba, 0, (size_t)width * height * 4);
|
||||
|
||||
int pen_x = 0;
|
||||
const char *p = text;
|
||||
uint32_t cp;
|
||||
while (utf8_next(&p, &cp)) {
|
||||
if (FT_Load_Char(face, cp, FT_LOAD_RENDER) != 0) continue;
|
||||
FT_Bitmap *bm = &face->glyph->bitmap;
|
||||
int bx = face->glyph->bitmap_left;
|
||||
int by = ascent - face->glyph->bitmap_top;
|
||||
for (unsigned int gy = 0; gy < bm->rows; gy++) {
|
||||
int dy = by + (int)gy;
|
||||
if (dy < 0 || dy >= height) continue;
|
||||
for (unsigned int gx = 0; gx < bm->width; gx++) {
|
||||
int dx = pen_x + bx + (int)gx;
|
||||
if (dx < 0 || dx >= width) continue;
|
||||
unsigned char a = bm->buffer[gy * bm->pitch + gx];
|
||||
if (a == 0) continue;
|
||||
unsigned char *dst = rgba + ((size_t)dy * width + dx) * 4;
|
||||
/* Simple over-blend; в Phase 3 без gamma correction. */
|
||||
int ca = dst[3];
|
||||
int new_a = a + (ca * (255 - a)) / 255;
|
||||
if (new_a > 0) {
|
||||
dst[0] = (unsigned char)((r * a + dst[0] * ca * (255 - a) / 255) / new_a);
|
||||
dst[1] = (unsigned char)((g * a + dst[1] * ca * (255 - a) / 255) / new_a);
|
||||
dst[2] = (unsigned char)((b * a + dst[2] * ca * (255 - a) / 255) / new_a);
|
||||
dst[3] = (unsigned char)new_a;
|
||||
}
|
||||
}
|
||||
}
|
||||
pen_x += face->glyph->advance.x >> 6;
|
||||
}
|
||||
}
|
||||
|
||||
/* Заново рендерить atlas из face + cfg.text. Освобождает старый VRAM
|
||||
* (если был) и аллоцирует новый. */
|
||||
static int text_rebuild_atlas(text_data_t *td)
|
||||
{
|
||||
int w = 0, h = 0, ascent = 0;
|
||||
if (text_measure(td->face, td->text_owned, &w, &h, &ascent) != 0) return -1;
|
||||
if (w <= 0 || h <= 0) return -1;
|
||||
|
||||
unsigned char *cpu = calloc((size_t)w * h, 4);
|
||||
if (!cpu) return -1;
|
||||
text_render(td->face, td->text_owned, cpu, w, h, ascent,
|
||||
td->cfg.r, td->cfg.g, td->cfg.b);
|
||||
|
||||
/* Free old VRAM. */
|
||||
if (td->atlas) {
|
||||
cuMemFree(td->atlas);
|
||||
td->atlas = 0;
|
||||
}
|
||||
CUresult cr = cuMemAlloc(&td->atlas, (size_t)w * h * 4);
|
||||
if (cr != CUDA_SUCCESS) { free(cpu); return -1; }
|
||||
cr = cuMemcpyHtoD(td->atlas, cpu, (size_t)w * h * 4);
|
||||
free(cpu);
|
||||
if (cr != CUDA_SUCCESS) {
|
||||
cuMemFree(td->atlas); td->atlas = 0; return -1;
|
||||
}
|
||||
td->width = w;
|
||||
td->height = h;
|
||||
td->pitch = w * 4;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cfc_overlay_create_text(const cfc_overlay_text_config_t *cfg,
|
||||
cfc_overlay_t **out)
|
||||
{
|
||||
if (!cfg || !cfg->font_path || !cfg->text || !out) return -1;
|
||||
if (cfg->pixel_size < 4) return -1;
|
||||
|
||||
cfc_overlay_t *ov = calloc(1, sizeof(*ov));
|
||||
if (!ov) return -1;
|
||||
ov->type = CFC_OVERLAY_TEXT;
|
||||
text_data_t *td = &ov->u.text;
|
||||
td->cfg = *cfg;
|
||||
if (td->cfg.extra_alpha == 0) td->cfg.extra_alpha = 255;
|
||||
td->text_owned = strdup(cfg->text);
|
||||
td->font_path_owned = strdup(cfg->font_path);
|
||||
td->pixel_size = cfg->pixel_size;
|
||||
td->cfg.text = td->text_owned;
|
||||
td->cfg.font_path = td->font_path_owned;
|
||||
if (!td->text_owned || !td->font_path_owned) goto fail;
|
||||
|
||||
if (FT_Init_FreeType(&td->ft_lib) != 0) {
|
||||
fprintf(stderr, "[cfc/overlay/text] FT_Init_FreeType failed\n");
|
||||
goto fail;
|
||||
}
|
||||
if (FT_New_Face(td->ft_lib, td->font_path_owned, 0, &td->face) != 0) {
|
||||
fprintf(stderr, "[cfc/overlay/text] FT_New_Face(%s) failed\n",
|
||||
td->font_path_owned);
|
||||
goto fail_lib;
|
||||
}
|
||||
if (FT_Set_Pixel_Sizes(td->face, 0, td->pixel_size) != 0) {
|
||||
fprintf(stderr, "[cfc/overlay/text] FT_Set_Pixel_Sizes(%d) failed\n",
|
||||
td->pixel_size);
|
||||
goto fail_face;
|
||||
}
|
||||
if (text_rebuild_atlas(td) != 0) goto fail_face;
|
||||
|
||||
*out = ov;
|
||||
return 0;
|
||||
|
||||
fail_face:
|
||||
FT_Done_Face(td->face);
|
||||
fail_lib:
|
||||
FT_Done_FreeType(td->ft_lib);
|
||||
fail:
|
||||
free(td->text_owned);
|
||||
free(td->font_path_owned);
|
||||
free(ov);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int cfc_overlay_update_text(cfc_overlay_t *ov,
|
||||
const cfc_overlay_text_config_t *cfg)
|
||||
{
|
||||
if (!ov || !cfg) return -1;
|
||||
if (ov->type != CFC_OVERLAY_TEXT) return -1;
|
||||
text_data_t *td = &ov->u.text;
|
||||
|
||||
/* Сравним нужно ли re-render: text / r / g / b изменились. */
|
||||
int need_rebuild = 0;
|
||||
if (cfg->text && strcmp(cfg->text, td->text_owned) != 0) {
|
||||
free(td->text_owned);
|
||||
td->text_owned = strdup(cfg->text);
|
||||
if (!td->text_owned) return -1;
|
||||
td->cfg.text = td->text_owned;
|
||||
need_rebuild = 1;
|
||||
}
|
||||
if (cfg->r != td->cfg.r || cfg->g != td->cfg.g || cfg->b != td->cfg.b) {
|
||||
td->cfg.r = cfg->r; td->cfg.g = cfg->g; td->cfg.b = cfg->b;
|
||||
need_rebuild = 1;
|
||||
}
|
||||
td->cfg.x = cfg->x;
|
||||
td->cfg.y = cfg->y;
|
||||
td->cfg.extra_alpha = cfg->extra_alpha ? cfg->extra_alpha : 255;
|
||||
td->cfg.visible = cfg->visible;
|
||||
|
||||
if (need_rebuild) return text_rebuild_atlas(td);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cfc_overlay_text_size(cfc_overlay_t *ov, int *width, int *height)
|
||||
{
|
||||
if (!ov || ov->type != CFC_OVERLAY_TEXT) return -1;
|
||||
if (width) *width = ov->u.text.width;
|
||||
if (height) *height = ov->u.text.height;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int draw_text(cfc_overlay_t *ov, CUstream stream,
|
||||
CUdeviceptr dst_y, int pitch_y,
|
||||
CUdeviceptr dst_uv, int pitch_uv,
|
||||
int frame_w, int frame_h)
|
||||
{
|
||||
const text_data_t *t = &ov->u.text;
|
||||
if (!t->cfg.visible) return 0;
|
||||
if (t->cfg.extra_alpha <= 0) return 0;
|
||||
if (!t->atlas) return 0;
|
||||
if (t->cfg.x >= frame_w || t->cfg.y >= frame_h) return 0;
|
||||
|
||||
int x = t->cfg.x & ~1;
|
||||
int y = t->cfg.y & ~1;
|
||||
return cfc_cugrid_blit_rgba_nv12(
|
||||
stream,
|
||||
dst_y, pitch_y, dst_uv, pitch_uv,
|
||||
x, y,
|
||||
t->atlas, t->width, t->height, t->pitch,
|
||||
t->cfg.extra_alpha);
|
||||
}
|
||||
|
||||
/* ── Public dispatch ─────────────────────────────────────────────────── */
|
||||
|
||||
int cfc_overlay_draw(cfc_overlay_t *ov,
|
||||
@@ -289,8 +535,8 @@ int cfc_overlay_draw(cfc_overlay_t *ov,
|
||||
return draw_png(ov, stream, dst_y, pitch_y, dst_uv, pitch_uv,
|
||||
frame_w, frame_h);
|
||||
case CFC_OVERLAY_TEXT:
|
||||
/* Phase 3c — пока no-op. */
|
||||
return 0;
|
||||
return draw_text(ov, stream, dst_y, pitch_y, dst_uv, pitch_uv,
|
||||
frame_w, frame_h);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
@@ -301,6 +547,14 @@ int cfc_overlay_destroy(cfc_overlay_t *ov)
|
||||
if (ov->type == CFC_OVERLAY_PNG && ov->u.png.atlas) {
|
||||
cuMemFree(ov->u.png.atlas);
|
||||
}
|
||||
if (ov->type == CFC_OVERLAY_TEXT) {
|
||||
text_data_t *td = &ov->u.text;
|
||||
if (td->atlas) cuMemFree(td->atlas);
|
||||
if (td->face) FT_Done_Face(td->face);
|
||||
if (td->ft_lib) FT_Done_FreeType(td->ft_lib);
|
||||
free(td->text_owned);
|
||||
free(td->font_path_owned);
|
||||
}
|
||||
free(ov);
|
||||
return 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user