diff --git a/CMakeLists.txt b/CMakeLists.txt index 6526707..e835503 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/examples/grid_record.c b/examples/grid_record.c index 6ff3df8..5793a08 100644 --- a/examples/grid_record.c +++ b/examples/grid_record.c @@ -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 = { diff --git a/include/cuframes_composer/overlay.h b/include/cuframes_composer/overlay.h index faaf94c..f10ab7f 100644 --- a/include/cuframes_composer/overlay.h +++ b/include/cuframes_composer/overlay.h @@ -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. В рамках одного diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5d6638a..3674ef7 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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. diff --git a/src/nvenc.c b/src/nvenc.c index 0de93b4..b606a2e 100644 --- a/src/nvenc.c +++ b/src/nvenc.c @@ -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; diff --git a/src/overlay.c b/src/overlay.c index d94eba6..c602994 100644 --- a/src/overlay.c +++ b/src/overlay.c @@ -19,6 +19,9 @@ #include #include +#include +#include FT_FREETYPE_H +#include #include #include #include @@ -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; }