From 8ae7ef6d564ffae9d79e88080381bbd24327c24a Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 8 Jun 2026 15:02:49 +0000 Subject: [PATCH 01/16] new_api: import st20 video session API (compiles+links clean on main) Import the additive unified session API subsystem from new_api_draft: - lib/src/new_api/ (mt_session*, video tx/rx, buffer, event, common) - include/mtl_session_api.h public header - app/sample/new_api/ samples - meson wiring (subdir new_api, header install, sample executables) Video ST20 only; audio/anc/st22 vtables are extern-declared but unused. Verified: zero pipeline-symbol usage; builds clean against main with no source changes (libmtl.so + 4 new samples link OK). --- app/meson.build | 29 + app/sample/meson.build | 6 + .../new_api/rx_video_lib_owned_sample.c | 264 +++++ .../new_api/rx_video_user_owned_sample.c | 326 +++++++ .../new_api/tx_video_lib_owned_sample.c | 276 ++++++ .../new_api/tx_video_user_owned_sample.c | 378 ++++++++ include/meson.build | 3 +- include/mtl_session_api.h | 807 +++++++++++++++ lib/src/meson.build | 1 + lib/src/new_api/meson.build | 13 + lib/src/new_api/mt_session.c | 621 ++++++++++++ lib/src/new_api/mt_session.h | 493 ++++++++++ lib/src/new_api/mt_session_buffer.c | 323 ++++++ lib/src/new_api/mt_session_event.c | 110 +++ lib/src/new_api/mt_session_video_common.c | 242 +++++ lib/src/new_api/mt_session_video_common.h | 173 ++++ lib/src/new_api/mt_session_video_rx.c | 918 ++++++++++++++++++ lib/src/new_api/mt_session_video_tx.c | 880 +++++++++++++++++ 18 files changed, 5862 insertions(+), 1 deletion(-) create mode 100644 app/sample/new_api/rx_video_lib_owned_sample.c create mode 100644 app/sample/new_api/rx_video_user_owned_sample.c create mode 100644 app/sample/new_api/tx_video_lib_owned_sample.c create mode 100644 app/sample/new_api/tx_video_user_owned_sample.c create mode 100644 include/mtl_session_api.h create mode 100644 lib/src/new_api/meson.build create mode 100644 lib/src/new_api/mt_session.c create mode 100644 lib/src/new_api/mt_session.h create mode 100644 lib/src/new_api/mt_session_buffer.c create mode 100644 lib/src/new_api/mt_session_event.c create mode 100644 lib/src/new_api/mt_session_video_common.c create mode 100644 lib/src/new_api/mt_session_video_common.h create mode 100644 lib/src/new_api/mt_session_video_rx.c create mode 100644 lib/src/new_api/mt_session_video_tx.c diff --git a/app/meson.build b/app/meson.build index d6ba0c517..8ff8373ee 100644 --- a/app/meson.build +++ b/app/meson.build @@ -441,6 +441,35 @@ executable('RxSt20CombinedRedundantSample', redundant_rx_st20_combined_sample_so dependencies: [asan_dep, mtl, libpthread, ws2_32_dep, mman_dep] ) +# New unified API samples +executable('NewApiTxVideoLibOwned', new_api_tx_video_lib_owned_sources, + c_args : app_c_args, + link_args: app_ld_args, + # asan should be always the first dep + dependencies: [asan_dep, mtl, libpthread, ws2_32_dep, mman_dep] +) + +executable('NewApiRxVideoLibOwned', new_api_rx_video_lib_owned_sources, + c_args : app_c_args, + link_args: app_ld_args, + # asan should be always the first dep + dependencies: [asan_dep, mtl, libpthread, ws2_32_dep, mman_dep] +) + +executable('NewApiTxVideoUserOwned', new_api_tx_video_user_owned_sources, + c_args : app_c_args, + link_args: app_ld_args, + # asan should be always the first dep + dependencies: [asan_dep, mtl, libpthread, ws2_32_dep, mman_dep] +) + +executable('NewApiRxVideoUserOwned', new_api_rx_video_user_owned_sources, + c_args : app_c_args, + link_args: app_ld_args, + # asan should be always the first dep + dependencies: [asan_dep, mtl, libpthread, ws2_32_dep, mman_dep] +) + # Dma sample app executable('DmaSample', dma_sample_sources, c_args : app_c_args, diff --git a/app/sample/meson.build b/app/sample/meson.build index ff9f3c29b..bbc2d2646 100644 --- a/app/sample/meson.build +++ b/app/sample/meson.build @@ -50,6 +50,12 @@ dma_sample_sources = files('dma/dma_sample.c', 'sample_util.c') redundant_rx_st20_combined_sample_sources = files('experimental/rx_st20_redundant_combined_sample.c', 'sample_util.c') +# new unified API +new_api_tx_video_lib_owned_sources = files('new_api/tx_video_lib_owned_sample.c', 'sample_util.c') +new_api_rx_video_lib_owned_sources = files('new_api/rx_video_lib_owned_sample.c', 'sample_util.c') +new_api_tx_video_user_owned_sources = files('new_api/tx_video_user_owned_sample.c', 'sample_util.c') +new_api_rx_video_user_owned_sources = files('new_api/rx_video_user_owned_sample.c', 'sample_util.c') + # gpu direct gpu_direct_tx_sample_sources = files('gpu_direct/tx_st20_pipeline_gpu_direct.c', 'sample_util.c') gpu_direct_rx_sample_sources = files('gpu_direct/rx_st20_pipeline_gpu_direct.c', 'sample_util.c') \ No newline at end of file diff --git a/app/sample/new_api/rx_video_lib_owned_sample.c b/app/sample/new_api/rx_video_lib_owned_sample.c new file mode 100644 index 000000000..86c647c62 --- /dev/null +++ b/app/sample/new_api/rx_video_lib_owned_sample.c @@ -0,0 +1,264 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2024 Intel Corporation + */ + +/** + * @file rx_video_lib_owned_sample.c + * + * New unified API sample: RX video with library-owned buffers. + * Library manages buffer allocation. App uses buffer_get/put loop. + * + * Usage: + * ./NewApiRxVideoLibOwned --p_port 0000:4b:01.1 --p_sip 192.168.96.3 \ + * --p_rx_ip 239.168.85.20 --udp_port 20000 + */ + +#include "../sample_util.h" + +#include + +struct rx_sample_ctx { + int idx; + mtl_session_t* session; + + bool stop; + pthread_t frame_thread; + + int fb_recv; + size_t frame_size; + + /* Optional: dump received frames to file */ + int dst_fd; + uint8_t* dst_begin; + uint8_t* dst_end; + uint8_t* dst_cursor; + int fb_cnt; +}; + +static int rx_open_dest(struct rx_sample_ctx* s, const char* file) { + int fd, ret, idx = s->idx; + off_t f_size; + int fb_cnt = 3; + + fd = st_open_mode(file, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR); + if (fd < 0) { + err("%s(%d), open %s fail\n", __func__, idx, file); + return -EIO; + } + + f_size = fb_cnt * s->frame_size; + ret = ftruncate(fd, f_size); + if (ret < 0) { + err("%s(%d), ftruncate %s fail\n", __func__, idx, file); + close(fd); + return -EIO; + } + + uint8_t* m = mmap(NULL, f_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (MAP_FAILED == m) { + err("%s(%d), mmap %s fail\n", __func__, idx, file); + close(fd); + return -EIO; + } + + s->dst_begin = m; + s->dst_cursor = m; + s->dst_end = m + f_size; + s->dst_fd = fd; + info("%s(%d), save %d framebuffers to file %s(%p,%" PRIu64 ")\n", __func__, idx, + fb_cnt, file, m, f_size); + return 0; +} + +static void rx_close_dest(struct rx_sample_ctx* s) { + if (s->dst_begin) { + munmap(s->dst_begin, s->dst_end - s->dst_begin); + s->dst_begin = NULL; + } + if (s->dst_fd >= 0) { + close(s->dst_fd); + s->dst_fd = -1; + } +} + +static void rx_consume_frame(struct rx_sample_ctx* s, mtl_buffer_t* buf) { + s->fb_recv++; + if (s->dst_fd < 0) return; /* no dump */ + + if (s->dst_cursor + s->frame_size > s->dst_end) s->dst_cursor = s->dst_begin; + mtl_memcpy(s->dst_cursor, buf->data, s->frame_size); + s->dst_cursor += s->frame_size; +} + +static void* rx_frame_thread(void* arg) { + struct rx_sample_ctx* s = arg; + mtl_session_t* session = s->session; + mtl_buffer_t* buf = NULL; + int ret; + + info("%s(%d), start\n", __func__, s->idx); + while (!s->stop) { + ret = mtl_session_buffer_get(session, &buf, 1000); + if (ret == -EAGAIN) { + info("%s(%d), session stopped\n", __func__, s->idx); + break; + } + if (ret == -ETIMEDOUT) { + continue; + } + if (ret < 0) { + err("%s(%d), buffer_get error: %d\n", __func__, s->idx, ret); + break; + } + + if (buf->flags & MTL_BUF_FLAG_INCOMPLETE) { + dbg("%s(%d), incomplete frame\n", __func__, s->idx); + } + + rx_consume_frame(s, buf); + + ret = mtl_session_buffer_put(session, buf); + if (ret < 0) { + err("%s(%d), buffer_put error: %d\n", __func__, s->idx, ret); + break; + } + + if (s->fb_recv % 100 == 0) + info("%s(%d), received %d frames\n", __func__, s->idx, s->fb_recv); + } + info("%s(%d), stop, received %d frames\n", __func__, s->idx, s->fb_recv); + + return NULL; +} + +int main(int argc, char** argv) { + struct st_sample_context ctx; + int ret; + + memset(&ctx, 0, sizeof(ctx)); + ret = rx_sample_parse_args(&ctx, argc, argv); + if (ret < 0) return ret; + + ctx.param.flags |= MTL_FLAG_DEV_AUTO_START_STOP; + ctx.st = mtl_init(&ctx.param); + if (!ctx.st) { + err("%s, mtl_init fail\n", __func__); + return -EIO; + } + + uint32_t session_num = ctx.sessions; + struct rx_sample_ctx* app[session_num]; + + for (int i = 0; i < session_num; i++) { + app[i] = malloc(sizeof(struct rx_sample_ctx)); + if (!app[i]) { + err("%s(%d), app context malloc fail\n", __func__, i); + ret = -ENOMEM; + goto error; + } + memset(app[i], 0, sizeof(struct rx_sample_ctx)); + app[i]->idx = i; + app[i]->stop = false; + app[i]->dst_fd = -1; + app[i]->fb_cnt = ctx.framebuff_cnt; + + /* Configure unified session */ + mtl_video_config_t config; + memset(&config, 0, sizeof(config)); + config.base.direction = MTL_SESSION_RX; + config.base.ownership = MTL_BUFFER_LIBRARY_OWNED; + config.base.num_buffers = ctx.framebuff_cnt; + config.base.name = "new_api_rx_lib"; + config.base.flags = MTL_SESSION_FLAG_BLOCK_GET; + + /* Port config */ + config.rx_port.num_port = ctx.param.num_ports; + memcpy(config.rx_port.ip_addr[MTL_SESSION_PORT_P], ctx.rx_ip_addr[MTL_PORT_P], + MTL_IP_ADDR_LEN); + snprintf(config.rx_port.port[MTL_SESSION_PORT_P], MTL_PORT_MAX_LEN, "%s", + ctx.param.port[MTL_PORT_P]); + config.rx_port.udp_port[MTL_SESSION_PORT_P] = ctx.udp_port + i * 2; + if (config.rx_port.num_port > 1) { + memcpy(config.rx_port.ip_addr[MTL_SESSION_PORT_R], ctx.rx_ip_addr[MTL_PORT_R], + MTL_IP_ADDR_LEN); + snprintf(config.rx_port.port[MTL_SESSION_PORT_R], MTL_PORT_MAX_LEN, "%s", + ctx.param.port[MTL_PORT_R]); + config.rx_port.udp_port[MTL_SESSION_PORT_R] = ctx.udp_port + i * 2; + } + if (ctx.multi_inc_addr) { + config.rx_port.udp_port[MTL_SESSION_PORT_P] = ctx.udp_port; + config.rx_port.ip_addr[MTL_SESSION_PORT_P][3] += i; + } + config.rx_port.payload_type = ctx.payload_type; + + /* Video format */ + config.width = ctx.width; + config.height = ctx.height; + config.fps = ctx.fps; + config.interlaced = ctx.interlaced; + config.frame_fmt = ctx.output_fmt; + config.transport_fmt = ctx.fmt; + + ret = mtl_video_session_create(ctx.st, &config, &app[i]->session); + if (ret < 0) { + err("%s(%d), session create fail: %d\n", __func__, i, ret); + goto error; + } + + app[i]->frame_size = mtl_session_get_frame_size(app[i]->session); + info("%s(%d), frame_size %" PRId64 "\n", __func__, i, app[i]->frame_size); + + if (ctx.rx_dump) { + ret = rx_open_dest(app[i], ctx.rx_url); + if (ret < 0) goto error; + } + + ret = mtl_session_start(app[i]->session); + if (ret < 0) { + err("%s(%d), session start fail: %d\n", __func__, i, ret); + goto error; + } + + ret = pthread_create(&app[i]->frame_thread, NULL, rx_frame_thread, app[i]); + if (ret < 0) { + err("%s(%d), thread create fail: %d\n", __func__, i, ret); + ret = -EIO; + goto error; + } + } + + while (!ctx.exit) { + sleep(1); + } + + /* Stop */ + for (int i = 0; i < session_num; i++) { + app[i]->stop = true; + if (app[i]->session) mtl_session_stop(app[i]->session); + pthread_join(app[i]->frame_thread, NULL); + info("%s(%d), received frames %d\n", __func__, i, app[i]->fb_recv); + rx_close_dest(app[i]); + } + + /* Check result */ + for (int i = 0; i < session_num; i++) { + if (app[i]->fb_recv <= 0) { + err("%s(%d), error, no received frames %d\n", __func__, i, app[i]->fb_recv); + ret = -EIO; + } + } + +error: + for (int i = 0; i < session_num; i++) { + if (app[i]) { + if (app[i]->session) mtl_session_destroy(app[i]->session); + free(app[i]); + } + } + + if (ctx.st) { + mtl_uninit(ctx.st); + ctx.st = NULL; + } + return ret; +} diff --git a/app/sample/new_api/rx_video_user_owned_sample.c b/app/sample/new_api/rx_video_user_owned_sample.c new file mode 100644 index 000000000..8495bb0a1 --- /dev/null +++ b/app/sample/new_api/rx_video_user_owned_sample.c @@ -0,0 +1,326 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2024 Intel Corporation + */ + +/** + * @file rx_video_user_owned_sample.c + * + * New unified API sample: RX video with user-owned buffers (zero-copy). + * + * Demonstrates true zero-copy to file: the output file is mmap'd and its + * pages are posted directly to the library as receive buffers. Received + * data lands in the file-backed memory with no memcpy. + * + * Flow: + * 1. mmap output file (pre-allocated) + * 2. mem_register() — register the mmap'd region for DMA + * 3. buffer_post() — provide file-backed pages as receive buffers + * 4. event_poll() — wait for MTL_EVENT_BUFFER_READY, re-post + * + * Usage: + * ./NewApiRxVideoUserOwned --p_port 0000:af:01.1 --p_sip 192.168.96.3 \ + * --p_rx_ip 239.168.85.20 --udp_port 20000 --rx_dump + */ + +#include "../sample_util.h" + +#include + +#define USER_BUF_CNT 4 + +/* Application's buffer tracking */ +typedef struct { + void* data; + size_t size; + int id; +} app_buffer_t; + +struct rx_user_sample_ctx { + mtl_handle st; + int idx; + mtl_session_t* session; + + bool stop; + pthread_t worker_thread; + + int fb_recv; + size_t frame_size; + + /* File-backed mmap'd region — serves as both receive buffers AND output file. + * The library writes directly into these pages — zero copy to disk. */ + int dst_fd; + uint8_t* dst_begin; + size_t dst_size; + int dst_frame_cnt; + + /* DMA handle for the mmap'd region */ + mtl_dma_mem_t* dma_handle; + + /* Per-buffer tracking (points into the mmap'd file, no copy) */ + app_buffer_t buffers[USER_BUF_CNT]; +}; + +/** + * Open (or create) the output file and mmap it. + * The mmap'd pages ARE the receive buffers — zero copy. + */ +static int rx_open_dest(struct rx_user_sample_ctx* s, const char* file) { + int fd, ret, idx = s->idx; + + s->dst_frame_cnt = USER_BUF_CNT; + s->dst_size = s->dst_frame_cnt * s->frame_size; + + fd = st_open_mode(file, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR); + if (fd < 0) { + err("%s(%d), open %s fail\n", __func__, idx, file); + return -EIO; + } + + ret = ftruncate(fd, s->dst_size); + if (ret < 0) { + err("%s(%d), ftruncate %s fail\n", __func__, idx, file); + close(fd); + return -EIO; + } + + uint8_t* m = mmap(NULL, s->dst_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (MAP_FAILED == m) { + err("%s(%d), mmap %s fail\n", __func__, idx, file); + close(fd); + return -EIO; + } + + s->dst_begin = m; + s->dst_fd = fd; + info("%s(%d), mmap'd %s: %d frames, %" PRIu64 + " bytes (zero-copy receive target)\n", + __func__, idx, file, s->dst_frame_cnt, s->dst_size); + return 0; +} + +static void rx_close_dest(struct rx_user_sample_ctx* s) { + if (s->dst_begin) { + msync(s->dst_begin, s->dst_size, MS_SYNC); + munmap(s->dst_begin, s->dst_size); + s->dst_begin = NULL; + } + if (s->dst_fd >= 0) { + close(s->dst_fd); + s->dst_fd = -1; + } +} + +/* Worker thread: polls events and re-posts buffers */ +static void* rx_worker_thread(void* arg) { + struct rx_user_sample_ctx* s = arg; + mtl_event_t event; + int ret; + + info("%s(%d), start\n", __func__, s->idx); + while (!s->stop) { + ret = mtl_session_event_poll(s->session, &event, 1000); + if (ret == -EAGAIN) { + info("%s(%d), session stopped\n", __func__, s->idx); + break; + } + if (ret == -ETIMEDOUT) { + continue; + } + if (ret < 0) { + err("%s(%d), event_poll error: %d\n", __func__, s->idx, ret); + break; + } + + if (event.type == MTL_EVENT_BUFFER_READY) { + app_buffer_t* buf = (app_buffer_t*)event.ctx; + if (buf) { + s->fb_recv++; + dbg("%s(%d), frame received in buffer %d (already in file, zero-copy)\n", + __func__, s->idx, buf->id); + + /* Data is already in the file-backed mmap — nothing to copy. + * Just re-post the buffer for the next frame. */ + ret = mtl_session_buffer_post(s->session, buf->data, buf->size, buf); + if (ret < 0) { + err("%s(%d), failed to repost buffer %d: %d\n", __func__, s->idx, buf->id, + ret); + } + + if (s->fb_recv % 100 == 0) + info("%s(%d), received %d frames (zero-copy to file)\n", __func__, s->idx, + s->fb_recv); + } + } else if (event.type == MTL_EVENT_ERROR) { + err("%s(%d), error event: %d\n", __func__, s->idx, event.status); + } + } + info("%s(%d), stop, received %d frames\n", __func__, s->idx, s->fb_recv); + + return NULL; +} + +int main(int argc, char** argv) { + struct st_sample_context ctx; + int ret; + + memset(&ctx, 0, sizeof(ctx)); + ret = rx_sample_parse_args(&ctx, argc, argv); + if (ret < 0) return ret; + + /* Default: always dump to file (that's the point of this zero-copy sample) */ + if (!ctx.rx_dump) { + ctx.rx_dump = true; + info("rx_dump enabled by default for zero-copy sample\n"); + } + + ctx.param.flags |= MTL_FLAG_DEV_AUTO_START_STOP; + ctx.st = mtl_init(&ctx.param); + if (!ctx.st) { + err("%s, mtl_init fail\n", __func__); + return -EIO; + } + + uint32_t session_num = ctx.sessions; + struct rx_user_sample_ctx* app[session_num]; + + for (int i = 0; i < session_num; i++) { + app[i] = malloc(sizeof(struct rx_user_sample_ctx)); + if (!app[i]) { + err("%s(%d), app context malloc fail\n", __func__, i); + ret = -ENOMEM; + goto error; + } + memset(app[i], 0, sizeof(struct rx_user_sample_ctx)); + app[i]->st = ctx.st; + app[i]->idx = i; + app[i]->stop = false; + app[i]->dst_fd = -1; + + /* Configure unified session */ + mtl_video_config_t config; + memset(&config, 0, sizeof(config)); + config.base.direction = MTL_SESSION_RX; + config.base.ownership = MTL_BUFFER_USER_OWNED; + config.base.num_buffers = USER_BUF_CNT; + config.base.name = "new_api_rx_user"; + + /* Port config */ + config.rx_port.num_port = ctx.param.num_ports; + memcpy(config.rx_port.ip_addr[MTL_SESSION_PORT_P], ctx.rx_ip_addr[MTL_PORT_P], + MTL_IP_ADDR_LEN); + snprintf(config.rx_port.port[MTL_SESSION_PORT_P], MTL_PORT_MAX_LEN, "%s", + ctx.param.port[MTL_PORT_P]); + config.rx_port.udp_port[MTL_SESSION_PORT_P] = ctx.udp_port + i * 2; + if (config.rx_port.num_port > 1) { + memcpy(config.rx_port.ip_addr[MTL_SESSION_PORT_R], ctx.rx_ip_addr[MTL_PORT_R], + MTL_IP_ADDR_LEN); + snprintf(config.rx_port.port[MTL_SESSION_PORT_R], MTL_PORT_MAX_LEN, "%s", + ctx.param.port[MTL_PORT_R]); + config.rx_port.udp_port[MTL_SESSION_PORT_R] = ctx.udp_port + i * 2; + } + if (ctx.multi_inc_addr) { + config.rx_port.udp_port[MTL_SESSION_PORT_P] = ctx.udp_port; + config.rx_port.ip_addr[MTL_SESSION_PORT_P][3] += i; + } + config.rx_port.payload_type = ctx.payload_type; + + /* Video format */ + config.width = ctx.width; + config.height = ctx.height; + config.fps = ctx.fps; + config.interlaced = ctx.interlaced; + config.frame_fmt = ctx.output_fmt; + config.transport_fmt = ctx.fmt; + + ret = mtl_video_session_create(ctx.st, &config, &app[i]->session); + if (ret < 0) { + err("%s(%d), session create fail: %d\n", __func__, i, ret); + goto error; + } + + app[i]->frame_size = mtl_session_get_frame_size(app[i]->session); + info("%s(%d), frame_size %" PRId64 "\n", __func__, i, app[i]->frame_size); + + /* mmap output file — these pages ARE the receive buffers (zero-copy) */ + ret = rx_open_dest(app[i], ctx.rx_url); + if (ret < 0) { + err("%s(%d), open dest fail\n", __func__, i); + goto error; + } + + /* Register the file-backed mmap region for DMA */ + ret = mtl_session_mem_register(app[i]->session, app[i]->dst_begin, app[i]->dst_size, + &app[i]->dma_handle); + if (ret < 0) { + err("%s(%d), mem_register fail: %d\n", __func__, i, ret); + goto error; + } + + /* Set up buffer tracking — each buffer points into the mmap'd file. + * Received data lands directly in the file, zero copy. */ + for (int j = 0; j < USER_BUF_CNT; j++) { + app[i]->buffers[j].data = app[i]->dst_begin + j * app[i]->frame_size; + app[i]->buffers[j].size = app[i]->frame_size; + app[i]->buffers[j].id = j; + + /* Pre-post buffer so library can start receiving into it */ + ret = mtl_session_buffer_post(app[i]->session, app[i]->buffers[j].data, + app[i]->buffers[j].size, &app[i]->buffers[j]); + if (ret < 0) { + err("%s(%d), failed to pre-post buffer %d: %d\n", __func__, i, j, ret); + goto error; + } + } + + ret = mtl_session_start(app[i]->session); + if (ret < 0) { + err("%s(%d), session start fail: %d\n", __func__, i, ret); + goto error; + } + + ret = pthread_create(&app[i]->worker_thread, NULL, rx_worker_thread, app[i]); + if (ret < 0) { + err("%s(%d), thread create fail: %d\n", __func__, i, ret); + ret = -EIO; + goto error; + } + } + + while (!ctx.exit) { + sleep(1); + } + + /* Stop */ + for (int i = 0; i < session_num; i++) { + app[i]->stop = true; + if (app[i]->session) mtl_session_stop(app[i]->session); + pthread_join(app[i]->worker_thread, NULL); + info("%s(%d), received %d frames (zero-copy to file)\n", __func__, i, + app[i]->fb_recv); + } + + /* Check result */ + for (int i = 0; i < session_num; i++) { + if (app[i]->fb_recv <= 0) { + err("%s(%d), error, no received frames %d\n", __func__, i, app[i]->fb_recv); + ret = -EIO; + } + } + +error: + for (int i = 0; i < session_num; i++) { + if (app[i]) { + if (app[i]->dma_handle) + mtl_session_mem_unregister(app[i]->session, app[i]->dma_handle); + rx_close_dest(app[i]); + if (app[i]->session) mtl_session_destroy(app[i]->session); + free(app[i]); + } + } + + if (ctx.st) { + mtl_uninit(ctx.st); + ctx.st = NULL; + } + return ret; +} diff --git a/app/sample/new_api/tx_video_lib_owned_sample.c b/app/sample/new_api/tx_video_lib_owned_sample.c new file mode 100644 index 000000000..1820e14d6 --- /dev/null +++ b/app/sample/new_api/tx_video_lib_owned_sample.c @@ -0,0 +1,276 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2024 Intel Corporation + */ + +/** + * @file tx_video_lib_owned_sample.c + * + * New unified API sample: TX video with library-owned buffers. + * Library manages buffer allocation. App uses buffer_get/put loop. + * + * Usage: + * ./NewApiTxVideoLibOwned --p_port 0000:4b:01.0 --p_sip 192.168.96.2 \ + * --p_tx_ip 239.168.85.20 --udp_port 20000 + */ + +#include "../sample_util.h" + +#include + +struct tx_sample_ctx { + mtl_handle st; + int idx; + mtl_session_t* session; + + bool stop; + pthread_t frame_thread; + + int fb_send; + size_t frame_size; + + /* Source data (from file or generated) */ + uint8_t* source_begin; + uint8_t* source_end; + uint8_t* frame_cursor; +}; + +static int tx_open_source(struct tx_sample_ctx* s, char* file) { + int fd = -EIO; + struct stat i; + int frame_cnt = 2; + uint8_t* m = NULL; + size_t fbs_size = s->frame_size * frame_cnt; + + fd = st_open(file, O_RDONLY); + if (fd < 0) { + info("%s, open %s fail, will use generated pattern\n", __func__, file); + goto init_fb; + } + + if (fstat(fd, &i) < 0) { + err("%s, fstat %s fail\n", __func__, file); + close(fd); + return -EIO; + } + if (i.st_size < s->frame_size) { + err("%s, %s file size smaller than a frame %" PRIu64 "\n", __func__, file, + s->frame_size); + close(fd); + return -EIO; + } + m = mmap(NULL, i.st_size, PROT_READ, MAP_SHARED, fd, 0); + if (MAP_FAILED == m) { + err("%s, mmap %s fail\n", __func__, file); + close(fd); + return -EIO; + } + frame_cnt = i.st_size / s->frame_size; + fbs_size = frame_cnt * s->frame_size; + info("%s, tx_url %s frame_cnt %d\n", __func__, file, frame_cnt); + +init_fb: + s->source_begin = mtl_hp_zmalloc(s->st, fbs_size, MTL_PORT_P); + if (!s->source_begin) { + err("%s, source malloc on hugepage fail\n", __func__); + if (m) munmap(m, i.st_size); + if (fd >= 0) close(fd); + return -EIO; + } + s->frame_cursor = s->source_begin; + if (m) { + mtl_memcpy(s->source_begin, m, fbs_size); + munmap(m, i.st_size); + } else { + /* Fill with test pattern */ + memset(s->source_begin, 0x80, fbs_size); + } + s->source_end = s->source_begin + fbs_size; + + if (fd >= 0) close(fd); + return 0; +} + +static void tx_close_source(struct tx_sample_ctx* s) { + if (s->source_begin) { + mtl_hp_free(s->st, s->source_begin); + s->source_begin = NULL; + } +} + +static void* tx_frame_thread(void* arg) { + struct tx_sample_ctx* s = arg; + mtl_session_t* session = s->session; + mtl_buffer_t* buf = NULL; + int ret; + + info("%s(%d), start\n", __func__, s->idx); + while (!s->stop) { + ret = mtl_session_buffer_get(session, &buf, 1000); + if (ret == -EAGAIN) { + info("%s(%d), session stopped\n", __func__, s->idx); + break; + } + if (ret == -ETIMEDOUT) { + continue; + } + if (ret < 0) { + err("%s(%d), buffer_get error: %d\n", __func__, s->idx, ret); + break; + } + + /* Fill buffer with source data */ + if (s->source_begin) { + mtl_memcpy(buf->data, s->frame_cursor, s->frame_size); + s->frame_cursor += s->frame_size; + if (s->frame_cursor + s->frame_size > s->source_end) + s->frame_cursor = s->source_begin; + } + + ret = mtl_session_buffer_put(session, buf); + if (ret < 0) { + err("%s(%d), buffer_put error: %d\n", __func__, s->idx, ret); + break; + } + + s->fb_send++; + if (s->fb_send % 100 == 0) + info("%s(%d), sent %d frames\n", __func__, s->idx, s->fb_send); + } + info("%s(%d), stop, sent %d frames\n", __func__, s->idx, s->fb_send); + + return NULL; +} + +int main(int argc, char** argv) { + struct st_sample_context ctx; + int ret; + + memset(&ctx, 0, sizeof(ctx)); + ret = tx_sample_parse_args(&ctx, argc, argv); + if (ret < 0) return ret; + + ctx.param.flags |= MTL_FLAG_DEV_AUTO_START_STOP; + ctx.st = mtl_init(&ctx.param); + if (!ctx.st) { + err("%s, mtl_init fail\n", __func__); + return -EIO; + } + + uint32_t session_num = ctx.sessions; + struct tx_sample_ctx* app[session_num]; + + for (int i = 0; i < session_num; i++) { + app[i] = malloc(sizeof(struct tx_sample_ctx)); + if (!app[i]) { + err("%s(%d), app context malloc fail\n", __func__, i); + ret = -ENOMEM; + goto error; + } + memset(app[i], 0, sizeof(struct tx_sample_ctx)); + app[i]->st = ctx.st; + app[i]->idx = i; + app[i]->stop = false; + + /* Configure unified session */ + mtl_video_config_t config; + memset(&config, 0, sizeof(config)); + config.base.direction = MTL_SESSION_TX; + config.base.ownership = MTL_BUFFER_LIBRARY_OWNED; + config.base.num_buffers = ctx.framebuff_cnt; + config.base.name = "new_api_tx_lib"; + config.base.flags = MTL_SESSION_FLAG_BLOCK_GET; + + /* Port config */ + config.tx_port.num_port = ctx.param.num_ports; + memcpy(config.tx_port.dip_addr[MTL_SESSION_PORT_P], ctx.tx_dip_addr[MTL_PORT_P], + MTL_IP_ADDR_LEN); + snprintf(config.tx_port.port[MTL_SESSION_PORT_P], MTL_PORT_MAX_LEN, "%s", + ctx.param.port[MTL_PORT_P]); + config.tx_port.udp_port[MTL_SESSION_PORT_P] = ctx.udp_port + i * 2; + if (config.tx_port.num_port > 1) { + memcpy(config.tx_port.dip_addr[MTL_SESSION_PORT_R], ctx.tx_dip_addr[MTL_PORT_R], + MTL_IP_ADDR_LEN); + snprintf(config.tx_port.port[MTL_SESSION_PORT_R], MTL_PORT_MAX_LEN, "%s", + ctx.param.port[MTL_PORT_R]); + config.tx_port.udp_port[MTL_SESSION_PORT_R] = ctx.udp_port + i * 2; + } + if (ctx.multi_inc_addr) { + config.tx_port.udp_port[MTL_SESSION_PORT_P] = ctx.udp_port; + config.tx_port.dip_addr[MTL_SESSION_PORT_P][3] += i; + } + config.tx_port.payload_type = ctx.payload_type; + + /* Video format */ + config.width = ctx.width; + config.height = ctx.height; + config.fps = ctx.fps; + config.interlaced = ctx.interlaced; + config.frame_fmt = ctx.input_fmt; + config.transport_fmt = ctx.fmt; + config.packing = ctx.packing; + config.pacing = ST21_PACING_NARROW; + + ret = mtl_video_session_create(ctx.st, &config, &app[i]->session); + if (ret < 0) { + err("%s(%d), session create fail: %d\n", __func__, i, ret); + goto error; + } + + app[i]->frame_size = mtl_session_get_frame_size(app[i]->session); + info("%s(%d), frame_size %" PRId64 "\n", __func__, i, app[i]->frame_size); + + ret = tx_open_source(app[i], ctx.tx_url); + if (ret < 0) { + err("%s(%d), open source fail\n", __func__, i); + goto error; + } + + ret = mtl_session_start(app[i]->session); + if (ret < 0) { + err("%s(%d), session start fail: %d\n", __func__, i, ret); + goto error; + } + + ret = pthread_create(&app[i]->frame_thread, NULL, tx_frame_thread, app[i]); + if (ret < 0) { + err("%s(%d), thread create fail: %d\n", __func__, i, ret); + ret = -EIO; + goto error; + } + } + + while (!ctx.exit) { + sleep(1); + } + + /* Stop */ + for (int i = 0; i < session_num; i++) { + app[i]->stop = true; + if (app[i]->session) mtl_session_stop(app[i]->session); + pthread_join(app[i]->frame_thread, NULL); + info("%s(%d), sent frames %d\n", __func__, i, app[i]->fb_send); + tx_close_source(app[i]); + } + + /* Check result */ + for (int i = 0; i < session_num; i++) { + if (app[i]->fb_send <= 0) { + err("%s(%d), error, no sent frames %d\n", __func__, i, app[i]->fb_send); + ret = -EIO; + } + } + +error: + for (int i = 0; i < session_num; i++) { + if (app[i]) { + if (app[i]->session) mtl_session_destroy(app[i]->session); + free(app[i]); + } + } + + if (ctx.st) { + mtl_uninit(ctx.st); + ctx.st = NULL; + } + return ret; +} diff --git a/app/sample/new_api/tx_video_user_owned_sample.c b/app/sample/new_api/tx_video_user_owned_sample.c new file mode 100644 index 000000000..e85833976 --- /dev/null +++ b/app/sample/new_api/tx_video_user_owned_sample.c @@ -0,0 +1,378 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2024 Intel Corporation + */ + +/** + * @file tx_video_user_owned_sample.c + * + * New unified API sample: TX video with user-owned buffers (zero-copy). + * + * Demonstrates true zero-copy: the source file is mmap'd and its pages + * are posted directly to the library for DMA transmission — no memcpy. + * + * Flow: + * 1. mmap source file + * 2. mem_register() — register the mmap'd region for DMA + * 3. buffer_post() — submit file-backed pages for transmission + * 4. event_poll() — wait for MTL_EVENT_BUFFER_DONE to re-post + * + * Usage: + * ./NewApiTxVideoUserOwned --p_port 0000:4b:01.0 --p_sip 192.168.96.2 \ + * --p_tx_ip 239.168.85.20 --udp_port 20000 --tx_url source.yuv + */ + +#include "../sample_util.h" + +#include + +#define USER_BUF_CNT 4 + +/* Application's buffer tracking */ +typedef struct { + void* data; + size_t size; + int id; + volatile int in_use; /* 1 = submitted to library */ +} app_buffer_t; + +struct tx_user_sample_ctx { + mtl_handle st; + int idx; + mtl_session_t* session; + + bool stop; + pthread_t producer_thread; + pthread_t event_thread; + + int fb_send; + int fb_done; + size_t frame_size; + + /* Source file mmap'd directly — serves as both source data AND transmit buffers */ + int src_fd; + uint8_t* src_begin; + size_t src_size; + int src_frame_cnt; + + /* DMA handle for the mmap'd region */ + mtl_dma_mem_t* dma_handle; + + /* Per-buffer tracking (points into the mmap'd region, no copy) */ + app_buffer_t buffers[USER_BUF_CNT]; +}; + +/** + * Open source file and mmap it. If no file, allocate hugepage with test pattern. + * The mmap'd region is used directly as transmit buffers — zero copy. + */ +static int tx_open_source(struct tx_user_sample_ctx* s, char* file) { + struct stat st; + int fd; + + s->src_fd = -1; + + fd = st_open(file, O_RDONLY); + if (fd < 0) { + info("%s, open %s fail, will use hugepage with test pattern\n", __func__, file); + goto fallback_hugepage; + } + + if (fstat(fd, &st) < 0) { + err("%s, fstat %s fail\n", __func__, file); + close(fd); + return -EIO; + } + if ((size_t)st.st_size < s->frame_size) { + err("%s, %s file size %" PRId64 " < frame_size %" PRIu64 "\n", __func__, file, + (int64_t)st.st_size, s->frame_size); + close(fd); + return -EIO; + } + + s->src_begin = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd, 0); + if (s->src_begin == MAP_FAILED) { + err("%s, mmap %s fail\n", __func__, file); + close(fd); + return -EIO; + } + + s->src_fd = fd; + s->src_frame_cnt = st.st_size / s->frame_size; + /* Trim to frame-aligned size */ + s->src_size = s->src_frame_cnt * s->frame_size; + info("%s, mmap'd %s: %d frames, %" PRIu64 " bytes (zero-copy source)\n", __func__, + file, s->src_frame_cnt, s->src_size); + return 0; + +fallback_hugepage: + /* No file — allocate hugepage memory with a test pattern */ + s->src_frame_cnt = USER_BUF_CNT; + s->src_size = s->src_frame_cnt * s->frame_size; + s->src_begin = mtl_hp_zmalloc(s->st, s->src_size, MTL_PORT_P); + if (!s->src_begin) { + err("%s, hugepage malloc fail\n", __func__); + return -ENOMEM; + } + memset(s->src_begin, 0x80, s->src_size); + info("%s, using hugepage test pattern: %d frames, %" PRIu64 " bytes\n", __func__, + s->src_frame_cnt, s->src_size); + if (fd >= 0) close(fd); + return 0; +} + +static void tx_close_source(struct tx_user_sample_ctx* s) { + if (s->src_begin) { + if (s->src_fd >= 0) { + munmap(s->src_begin, s->src_size); + } else { + mtl_hp_free(s->st, s->src_begin); + } + s->src_begin = NULL; + } + if (s->src_fd >= 0) { + close(s->src_fd); + s->src_fd = -1; + } +} + +/** + * Producer thread: posts buffers pointing directly into the mmap'd source file. + * No memcpy — the DMA engine reads from the file-backed pages. + */ +static void* tx_producer_thread(void* arg) { + struct tx_user_sample_ctx* s = arg; + int next_buf = 0; + int ret; + + info("%s(%d), start\n", __func__, s->idx); + while (!s->stop) { + app_buffer_t* buf = &s->buffers[next_buf]; + + /* Wait for buffer to be free */ + while (buf->in_use && !s->stop) { + usleep(1000); + } + if (s->stop) break; + + /* Post buffer for transmission (data already points into mmap'd file) */ + buf->in_use = 1; + ret = mtl_session_buffer_post(s->session, buf->data, buf->size, buf); + if (ret < 0) { + err("%s(%d), buffer_post failed: %d\n", __func__, s->idx, ret); + buf->in_use = 0; + if (ret == -EAGAIN) break; /* Session stopped */ + break; + } + + s->fb_send++; + next_buf = (next_buf + 1) % USER_BUF_CNT; + + if (s->fb_send % 100 == 0) + info("%s(%d), posted %d frames (zero-copy)\n", __func__, s->idx, s->fb_send); + } + info("%s(%d), stop, posted %d frames\n", __func__, s->idx, s->fb_send); + + return NULL; +} + +/* Event thread: handles completion events */ +static void* tx_event_thread(void* arg) { + struct tx_user_sample_ctx* s = arg; + mtl_event_t event; + int ret; + + info("%s(%d), start\n", __func__, s->idx); + while (!s->stop) { + ret = mtl_session_event_poll(s->session, &event, 100); + if (ret == -EAGAIN) { + info("%s(%d), session stopped\n", __func__, s->idx); + break; + } + if (ret == -ETIMEDOUT) { + continue; + } + if (ret < 0) { + err("%s(%d), event_poll error: %d\n", __func__, s->idx, ret); + break; + } + + if (event.type == MTL_EVENT_BUFFER_DONE) { + app_buffer_t* buf = (app_buffer_t*)event.ctx; + if (buf) { + buf->in_use = 0; + s->fb_done++; + dbg("%s(%d), buffer %d done, total %d\n", __func__, s->idx, buf->id, + s->fb_done); + } + } else if (event.type == MTL_EVENT_ERROR) { + err("%s(%d), error event: %d\n", __func__, s->idx, event.status); + } + } + info("%s(%d), stop, completed %d frames\n", __func__, s->idx, s->fb_done); + + return NULL; +} + +int main(int argc, char** argv) { + struct st_sample_context ctx; + int ret; + + memset(&ctx, 0, sizeof(ctx)); + ret = tx_sample_parse_args(&ctx, argc, argv); + if (ret < 0) return ret; + + ctx.param.flags |= MTL_FLAG_DEV_AUTO_START_STOP; + ctx.st = mtl_init(&ctx.param); + if (!ctx.st) { + err("%s, mtl_init fail\n", __func__); + return -EIO; + } + + uint32_t session_num = ctx.sessions; + struct tx_user_sample_ctx* app[session_num]; + + for (int i = 0; i < session_num; i++) { + app[i] = malloc(sizeof(struct tx_user_sample_ctx)); + if (!app[i]) { + err("%s(%d), app context malloc fail\n", __func__, i); + ret = -ENOMEM; + goto error; + } + memset(app[i], 0, sizeof(struct tx_user_sample_ctx)); + app[i]->st = ctx.st; + app[i]->idx = i; + app[i]->stop = false; + app[i]->src_fd = -1; + + /* Configure unified session */ + mtl_video_config_t config; + memset(&config, 0, sizeof(config)); + config.base.direction = MTL_SESSION_TX; + config.base.ownership = MTL_BUFFER_USER_OWNED; + config.base.num_buffers = USER_BUF_CNT; + config.base.name = "new_api_tx_user"; + + /* Port config */ + config.tx_port.num_port = ctx.param.num_ports; + memcpy(config.tx_port.dip_addr[MTL_SESSION_PORT_P], ctx.tx_dip_addr[MTL_PORT_P], + MTL_IP_ADDR_LEN); + snprintf(config.tx_port.port[MTL_SESSION_PORT_P], MTL_PORT_MAX_LEN, "%s", + ctx.param.port[MTL_PORT_P]); + config.tx_port.udp_port[MTL_SESSION_PORT_P] = ctx.udp_port + i * 2; + if (config.tx_port.num_port > 1) { + memcpy(config.tx_port.dip_addr[MTL_SESSION_PORT_R], ctx.tx_dip_addr[MTL_PORT_R], + MTL_IP_ADDR_LEN); + snprintf(config.tx_port.port[MTL_SESSION_PORT_R], MTL_PORT_MAX_LEN, "%s", + ctx.param.port[MTL_PORT_R]); + config.tx_port.udp_port[MTL_SESSION_PORT_R] = ctx.udp_port + i * 2; + } + if (ctx.multi_inc_addr) { + config.tx_port.udp_port[MTL_SESSION_PORT_P] = ctx.udp_port; + config.tx_port.dip_addr[MTL_SESSION_PORT_P][3] += i; + } + config.tx_port.payload_type = ctx.payload_type; + + /* Video format */ + config.width = ctx.width; + config.height = ctx.height; + config.fps = ctx.fps; + config.interlaced = ctx.interlaced; + config.frame_fmt = ctx.input_fmt; + config.transport_fmt = ctx.fmt; + config.packing = ctx.packing; + config.pacing = ST21_PACING_NARROW; + + ret = mtl_video_session_create(ctx.st, &config, &app[i]->session); + if (ret < 0) { + err("%s(%d), session create fail: %d\n", __func__, i, ret); + goto error; + } + + app[i]->frame_size = mtl_session_get_frame_size(app[i]->session); + info("%s(%d), frame_size %" PRId64 "\n", __func__, i, app[i]->frame_size); + + /* Open source: mmap file directly (zero-copy) or hugepage fallback */ + ret = tx_open_source(app[i], ctx.tx_url); + if (ret < 0) { + err("%s(%d), open source fail\n", __func__, i); + goto error; + } + + /* Register the mmap'd/hugepage source region for DMA */ + ret = mtl_session_mem_register(app[i]->session, app[i]->src_begin, app[i]->src_size, + &app[i]->dma_handle); + if (ret < 0) { + err("%s(%d), mem_register fail: %d\n", __func__, i, ret); + goto error; + } + + /* Set up buffer tracking — each buffer points directly into the source region. + * Buffers cycle through the source frames with no copy. */ + for (int j = 0; j < USER_BUF_CNT; j++) { + int frame_idx = j % app[i]->src_frame_cnt; + app[i]->buffers[j].data = app[i]->src_begin + frame_idx * app[i]->frame_size; + app[i]->buffers[j].size = app[i]->frame_size; + app[i]->buffers[j].id = j; + app[i]->buffers[j].in_use = 0; + } + + ret = mtl_session_start(app[i]->session); + if (ret < 0) { + err("%s(%d), session start fail: %d\n", __func__, i, ret); + goto error; + } + + /* Start worker threads */ + ret = pthread_create(&app[i]->event_thread, NULL, tx_event_thread, app[i]); + if (ret < 0) { + err("%s(%d), event thread create fail: %d\n", __func__, i, ret); + ret = -EIO; + goto error; + } + ret = pthread_create(&app[i]->producer_thread, NULL, tx_producer_thread, app[i]); + if (ret < 0) { + err("%s(%d), producer thread create fail: %d\n", __func__, i, ret); + ret = -EIO; + goto error; + } + } + + while (!ctx.exit) { + sleep(1); + } + + /* Stop */ + for (int i = 0; i < session_num; i++) { + app[i]->stop = true; + if (app[i]->session) mtl_session_stop(app[i]->session); + pthread_join(app[i]->producer_thread, NULL); + pthread_join(app[i]->event_thread, NULL); + info("%s(%d), sent %d frames, completed %d (zero-copy)\n", __func__, i, + app[i]->fb_send, app[i]->fb_done); + } + + /* Check result */ + for (int i = 0; i < session_num; i++) { + if (app[i]->fb_send <= 0) { + err("%s(%d), error, no sent frames %d\n", __func__, i, app[i]->fb_send); + ret = -EIO; + } + } + +error: + for (int i = 0; i < session_num; i++) { + if (app[i]) { + if (app[i]->dma_handle) + mtl_session_mem_unregister(app[i]->session, app[i]->dma_handle); + tx_close_source(app[i]); + if (app[i]->session) mtl_session_destroy(app[i]->session); + free(app[i]); + } + } + + if (ctx.st) { + mtl_uninit(ctx.st); + ctx.st = NULL; + } + return ret; +} diff --git a/include/meson.build b/include/meson.build index 9d67d8609..88a6ac030 100644 --- a/include/meson.build +++ b/include/meson.build @@ -10,7 +10,8 @@ mtl_deprecated_header_files = files( mtl_header_files = files('mtl_api.h', 'st_api.h', 'st_convert_api.h', 'st_convert_internal.h', 'st_pipeline_api.h', 'st20_api.h', 'st30_api.h', 'st40_api.h', 'st41_api.h', 'mtl_lcore_shm_api.h', - 'mtl_sch_api.h', 'st30_pipeline_api.h', 'st40_pipeline_api.h') + mtl_deprecated_header_files + 'mtl_sch_api.h', 'st30_pipeline_api.h', 'st40_pipeline_api.h', + 'mtl_session_api.h') + mtl_deprecated_header_files if is_windows mtl_header_files += files('deprecated/mudp_win.h') diff --git a/include/mtl_session_api.h b/include/mtl_session_api.h new file mode 100644 index 000000000..d0be430e6 --- /dev/null +++ b/include/mtl_session_api.h @@ -0,0 +1,807 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2024 Intel Corporation + */ + +/** + * @file mtl_session_api.h + * + * Unified Polymorphic Session API for Media Transport Library (MTL). + * + * DESIGN GOAL - REDUCE CODE REPETITION: + * ===================================== + * + * Currently, MTL has separate APIs for each media type: + * - st20p_tx_get_frame() / st20p_tx_put_frame() + * - st22p_tx_get_frame() / st22p_tx_put_frame() + * - st30p_tx_get_frame() / st30p_tx_put_frame() + * - st40p_tx_get_frame() / st40p_tx_put_frame() + * + * This leads to code duplication in both the library and applications. + * + * THE POLYMORPHIC SOLUTION: + * ========================= + * + * 1. ONE session type: mtl_session_t (wraps st20p/st22p/st30p/st40p internally) + * + * 2. CREATION is type-specific (different configs needed): + * - mtl_video_session_create(mt, &video_config, &session) + * - mtl_audio_session_create(mt, &audio_config, &session) + * - mtl_ancillary_session_create(mt, &anc_config, &session) + * + * 3. ALL OTHER OPERATIONS ARE IDENTICAL - same function for any media: + * - mtl_session_buffer_get(session, &buffer, timeout) + * - mtl_session_buffer_put(session, buffer) + * - mtl_session_event_poll(session, &event, timeout) + * - mtl_session_start/stop/destroy(session) + * + * BENEFITS: + * - Applications can write generic media handling code + * - Library can share implementation across media types + * - Simpler API to learn - same pattern everywhere + * - Easier testing - one test framework for all session types + * + * INTERNAL IMPLEMENTATION: + * - mtl_session_t contains vtable pointer + wrapped session handle + * - vtable dispatches to appropriate st20/st22/st30/st40 functions + * - No performance penalty - vtable dispatch is just one indirect call + */ + +#ifndef _MTL_SESSION_API_HEAD_H_ +#define _MTL_SESSION_API_HEAD_H_ + +#include "mtl_api.h" +#include "st_api.h" +#include "st20_api.h" +#include "st30_api.h" +#include "st_pipeline_api.h" + +#if defined(__cplusplus) +extern "C" { +#endif + +/************************************************************************* + * Core Types + *************************************************************************/ + +/** Session direction */ +typedef enum { + MTL_SESSION_TX = 0, /**< Transmitter */ + MTL_SESSION_RX = 1, /**< Receiver */ +} mtl_session_dir_t; + +/** Media type (for querying session type) */ +typedef enum { + MTL_TYPE_VIDEO = 0, /**< Video (ST20/ST22) */ + MTL_TYPE_AUDIO = 1, /**< Audio (ST30) */ + MTL_TYPE_ANCILLARY = 2, /**< Ancillary (ST40) */ + MTL_TYPE_FASTMETA = 3, /**< Fast Metadata (ST41) */ +} mtl_media_type_t; + +/** Buffer ownership model */ +typedef enum { + MTL_BUFFER_LIBRARY_OWNED = 0, /**< Library manages buffers (default) */ + MTL_BUFFER_USER_OWNED = 1, /**< App provides buffers (zero-copy) */ +} mtl_buffer_ownership_t; + +/** Video processing mode */ +typedef enum { + MTL_VIDEO_MODE_FRAME = 0, /**< Frame-level: full frames only */ + MTL_VIDEO_MODE_SLICE = 1, /**< Slice-level: line-by-line for ultra-low latency */ +} mtl_video_mode_t; + +/** Event types for polling */ +typedef enum { + MTL_EVENT_NONE = 0, + MTL_EVENT_BUFFER_READY = 1, /**< Buffer ready (RX: has data, TX: available) */ + MTL_EVENT_BUFFER_DONE = 2, /**< Buffer processing complete */ + MTL_EVENT_ERROR = 3, /**< Error occurred */ + MTL_EVENT_VSYNC = 4, /**< Vertical sync (epoch boundary) */ + MTL_EVENT_FRAME_LATE = 5, /**< TX: frame missed its epoch */ + MTL_EVENT_FORMAT_DETECTED = 6, /**< RX: video format auto-detected */ + MTL_EVENT_TIMING_REPORT = 7, /**< RX: timing parser result (periodic) */ + MTL_EVENT_SLICE_READY = 8, /**< Slice mode: lines ready (RX) or need more (TX) */ +} mtl_event_type_t; + +/** Frame/buffer status */ +typedef enum { + MTL_FRAME_STATUS_COMPLETE = 0, /**< Complete frame received */ + MTL_FRAME_STATUS_INCOMPLETE = 1, /**< Missing packets */ + MTL_FRAME_STATUS_CORRUPTED = 2, /**< Detected corruption */ +} mtl_frame_status_t; + +/** Session flags (used in base config flags field) */ +#define MTL_SESSION_FLAG_EXT_BUFFER (1 << 0) /**< Use external/user-owned buffers */ +#define MTL_SESSION_FLAG_USER_PACING (1 << 1) /**< User controls TX pacing */ +#define MTL_SESSION_FLAG_USER_TIMESTAMP (1 << 2) /**< User provides timestamps */ +#define MTL_SESSION_FLAG_ENABLE_VSYNC (1 << 3) /**< Enable VSYNC events */ +#define MTL_SESSION_FLAG_ENABLE_RTCP (1 << 4) /**< Enable RTCP feedback */ +#define MTL_SESSION_FLAG_FORCE_NUMA (1 << 5) /**< Force NUMA socket affinity */ +#define MTL_SESSION_FLAG_DATA_PATH_ONLY (1 << 6) /**< App manages flow rules */ +#define MTL_SESSION_FLAG_RECEIVE_INCOMPLETE_FRAME (1 << 7) /**< Accept incomplete frames */ +#define MTL_SESSION_FLAG_DMA_OFFLOAD (1 << 8) /**< DMA copy offload */ +#define MTL_SESSION_FLAG_HDR_SPLIT (1 << 9) /**< Header split mode */ +#define MTL_SESSION_FLAG_BLOCK_GET (1 << 10) /**< Blocking buffer get mode */ +#define MTL_SESSION_FLAG_USER_P_MAC (1 << 11) /**< TX: User-provided primary MAC */ +#define MTL_SESSION_FLAG_USER_R_MAC (1 << 12) /**< TX: User-provided redundant MAC */ +#define MTL_SESSION_FLAG_EXACT_USER_PACING (1 << 13) /**< TX: Exact user pacing */ +#define MTL_SESSION_FLAG_RTP_TIMESTAMP_EPOCH (1 << 14) /**< TX: RTP timestamp epoch mode */ +#define MTL_SESSION_FLAG_DISABLE_BULK (1 << 15) /**< TX: Disable bulk enqueue */ +#define MTL_SESSION_FLAG_STATIC_PAD_P (1 << 16) /**< TX: Static padding for primary */ +#define MTL_SESSION_FLAG_USE_MULTI_THREADS (1 << 17) /**< RX: Multi-thread processing */ + +/************************************************************************* + * Opaque Handles + *************************************************************************/ + +/** + * Unified session handle - THE POLYMORPHIC TYPE + * Works for video, audio, ancillary, metadata - all the same handle type! + */ +typedef struct mtl_session mtl_session_t; + +/** DMA memory handle for user-owned buffers */ +typedef struct mtl_dma_mem mtl_dma_mem_t; + +/************************************************************************* + * Buffer Structure - UNIFIED FOR ALL MEDIA TYPES + *************************************************************************/ + +/** + * Unified buffer returned by mtl_session_buffer_get(). + * Same structure for video, audio, ancillary - polymorphic design. + * + * For basic usage, just use: data, size, timestamp + * For type-specific fields, use the union after checking media type. + */ +typedef struct mtl_buffer { + /* Common fields - sufficient for most use cases */ + void* data; /**< Buffer data pointer */ + mtl_iova_t iova; /**< DMA address (if applicable) */ + size_t size; /**< Total buffer size */ + size_t data_size; /**< Valid data size (may be < size) */ + uint64_t timestamp; /**< Presentation timestamp (TAI ns) */ + uint64_t epoch; /**< Epoch info for the frame */ + uint32_t rtp_timestamp; /**< RTP timestamp */ + uint32_t flags; /**< Buffer flags */ + mtl_frame_status_t status; /**< Frame completeness status */ + void* priv; /**< Library private - DO NOT TOUCH */ + void* user_data; /**< Application context (opaque from ext_frame) */ + + /** User metadata (TX: set before put, RX: read after get) */ + void* user_meta; /**< User metadata pointer */ + size_t user_meta_size; /**< User metadata size in bytes */ + enum st10_timestamp_fmt tfmt; /**< Timestamp format (TAI, MEDIA_CLK, etc.) */ + + /* Type-specific extended fields (optional to use) */ + union { + struct { + void* planes[ST_MAX_PLANES]; /**< Plane pointers for planar formats */ + size_t linesize[ST_MAX_PLANES]; /**< Linesize (stride) per plane */ + uint32_t width; + uint32_t height; + enum st_frame_fmt fmt; /**< Frame format (st_frame_fmt) */ + bool interlaced; /**< Interlaced mode */ + bool second_field; /**< Second field for interlaced */ + uint32_t pkts_total; /**< Total packets expected */ + uint32_t pkts_recv[2]; /**< Packets received per port */ + } video; + + struct { + uint32_t samples; /**< Number of samples */ + uint16_t channels; + enum st30_fmt fmt; /**< Audio format */ + enum st30_sampling sampling; + enum st30_ptime ptime; + size_t frame_recv_size; /**< Actual received size */ + } audio; + + struct { + uint16_t line_number; + uint8_t did, sdid; + uint32_t meta_num; /**< Number of ANC packets in frame */ + bool second_field; /**< For interlaced */ + } ancillary; + + uint8_t _reserved[96]; + }; +} mtl_buffer_t; + +/** Buffer flags */ +#define MTL_BUF_FLAG_EXT (1 << 0) /**< External (user-owned) buffer */ +#define MTL_BUF_FLAG_INCOMPLETE (1 << 1) /**< Incomplete frame (RX) */ + +/************************************************************************* + * Event Structure + *************************************************************************/ + +/** Event from mtl_session_event_poll() */ +typedef struct mtl_event { + mtl_event_type_t type; + int status; /**< Error code if type == MTL_EVENT_ERROR */ + uint64_t timestamp; /**< Event timestamp (TAI ns) */ + void* ctx; /**< Buffer ptr (lib-owned) or user ctx (user-owned) */ + + /* Event-specific data */ + union { + /** For MTL_EVENT_VSYNC */ + struct { + uint64_t epoch; /**< Current epoch */ + uint64_t ptp_time; /**< PTP time at vsync */ + } vsync; + + /** For MTL_EVENT_FRAME_LATE */ + struct { + uint64_t epoch_skipped; /**< The epoch that was missed */ + } frame_late; + + /** For MTL_EVENT_FORMAT_DETECTED (video auto-detect) */ + struct { + uint32_t width; + uint32_t height; + enum st_fps fps; + enum st20_packing packing; + bool interlaced; + } format_detected; + + /** For MTL_EVENT_TIMING_REPORT */ + struct { + enum st_rx_tp_compliant compliant; + int32_t vrx_max, vrx_min; + int32_t ipt_max, ipt_min; /**< Inter-packet time (ns) */ + int32_t latency; /**< ns */ + uint32_t pkts_cnt; + } timing; + + /** For MTL_EVENT_SLICE_READY (slice mode) */ + struct { + uint16_t lines_ready; /**< RX: lines received so far */ + uint16_t lines_total; /**< Total lines in frame */ + void* buffer; /**< Current frame buffer */ + } slice; + + /** For MTL_EVENT_ERROR */ + struct { + int code; /**< Error code */ + } error; + + /** For MTL_EVENT_BUFFER_READY / MTL_EVENT_BUFFER_DONE */ + struct { + struct mtl_buffer* buf; /**< Related buffer */ + } buffer; + + uint8_t _reserved[64]; + }; +} mtl_event_t; + +/************************************************************************* + * Configuration Structures + *************************************************************************/ + +/** + * Base configuration - embedded in all type-specific configs. + * Contains fields common to all media types. + */ +typedef struct mtl_session_base_config { + mtl_session_dir_t direction; /**< TX or RX */ + mtl_buffer_ownership_t ownership; /**< Who owns buffers */ + uint16_t num_buffers; /**< Buffer count */ + const char* name; /**< Session name (debug) */ + void* priv; /**< App context for callbacks */ + uint32_t flags; /**< Session flags (MTL_SESSION_FLAG_*) */ + + /** NUMA socket to use (-1 for auto based on NIC) */ + int socket_id; + + /** + * Optional callbacks (alternative to polling). + * NOTE: callbacks run from library thread, must be non-blocking! + */ + int (*notify_buffer_ready)(void* priv); + int (*notify_event)(void* priv, mtl_event_t* event); + + /** + * For user-owned/ext_frame mode: query callback to get external frame. + * Library calls this when it needs a buffer. + * Return 0 and fill ext_frame on success. + */ + int (*query_ext_frame)(void* priv, struct st_ext_frame* ext_frame, + struct mtl_buffer* frame_meta); +} mtl_session_base_config_t; + +/** + * Video session configuration. + * For ST20 (uncompressed) and ST22 (compressed) video. + */ +typedef struct mtl_video_config { + mtl_session_base_config_t base; + + /* Network - use existing MTL port structures */ + union { + struct st_tx_port tx_port; /**< For TX sessions */ + struct st_rx_port rx_port; /**< For RX sessions */ + }; + + /* Video format */ + uint32_t width; + uint32_t height; + enum st_fps fps; + bool interlaced; + enum st_frame_fmt frame_fmt; /**< App pixel format */ + enum st20_fmt transport_fmt; /**< Wire format */ + + /* Pacing/packing (TX) */ + enum st21_pacing pacing; + enum st20_packing packing; + uint32_t linesize; /**< Line stride, 0 = no padding */ + + /************************************************************************* + * Slice Mode (ultra-low latency) + *************************************************************************/ + + /** + * Video processing mode: FRAME (default) or SLICE. + * + * SLICE mode enables line-by-line processing for ultra-low latency: + * - TX: Use mtl_session_slice_ready() to signal lines are ready + * - RX: Get MTL_EVENT_SLICE_READY events as lines arrive + * + * Note: Always enable RECEIVE_INCOMPLETE_FRAME flag with slice mode. + */ + mtl_video_mode_t mode; + + /** + * Slice mode TX only: callback when lib needs to know ready lines. + * Return the number of lines ready for transmission. + * Non-blocking, called from library thread. + */ + int (*query_lines_ready)(void* priv, uint16_t frame_idx, uint16_t* lines_ready); + + /************************************************************************* + * ST22 Compression / Plugins + *************************************************************************/ + + /** Enable ST22 compressed video (requires codec plugin) */ + bool compressed; + + /** ST22 codec type (JPEGXS, H264, H265, etc.) */ + enum st22_codec codec; + + /** Target codestream size for ST22 (CBR mode) */ + size_t codestream_size; + + /** + * Plugin device preference (CPU, GPU, FPGA, AUTO). + * Library selects appropriate registered plugin. + */ + enum st_plugin_device plugin_device; + + /** Encode quality vs speed tradeoff */ + enum st22_quality_mode quality; + + /** Number of codec threads (0 = auto) */ + uint32_t codec_thread_cnt; + + /************************************************************************* + * Advanced Options + *************************************************************************/ + + /** + * TX only: User-provided destination MAC addresses. + * Used when MTL_SESSION_FLAG_USER_P_MAC / MTL_SESSION_FLAG_USER_R_MAC is set. + */ + uint8_t tx_dst_mac[MTL_SESSION_PORT_MAX][MTL_MAC_ADDR_LEN]; + + /** TX only: start VRX value for pacing (0 = library default) */ + uint32_t start_vrx; + + /** TX only: pad interval for pacing (0 = library default) */ + uint32_t pad_interval; + + /** TX only: RTP timestamp delta in microseconds (0 = auto) */ + int32_t rtp_timestamp_delta_us; + + /** RX only: burst size for packet receive (0 = default) */ + uint32_t rx_burst_size; + + /** + * RX only: Enable timing parser analysis. + * Results delivered via MTL_EVENT_TIMING_REPORT events. + */ + bool enable_timing_parser; + + /** + * RX only: Enable auto-detect of video format. + * If enabled, width/height/fps can be left as 0. + * Format detection delivered via MTL_EVENT_FORMAT_DETECTED. + */ + bool enable_auto_detect; + +} mtl_video_config_t; + +/** + * Audio session configuration (ST30). + */ +typedef struct mtl_audio_config { + mtl_session_base_config_t base; + + union { + struct st_tx_port tx_port; + struct st_rx_port rx_port; + }; + + enum st30_fmt fmt; + uint16_t channels; + enum st30_sampling sampling; + enum st30_ptime ptime; + uint32_t framebuff_size; + + /** TX pacing method */ + enum st30_tx_pacing_way pacing_way; + + /** Enable timing parser (RX) - results via MTL_EVENT_TIMING_REPORT */ + bool enable_timing_parser; + +} mtl_audio_config_t; + +/** + * Ancillary session configuration (ST40). + */ +typedef struct mtl_ancillary_config { + mtl_session_base_config_t base; + + union { + struct st_tx_port tx_port; + struct st_rx_port rx_port; + }; + + enum st_fps fps; + bool interlaced; + uint32_t framebuff_size; + +} mtl_ancillary_config_t; + +/************************************************************************* + * Session Creation - TYPE-SPECIFIC (only part that differs) + *************************************************************************/ + +/** + * Create a video session (ST20 or ST22). + * After creation, use generic mtl_session_* functions. + */ +int mtl_video_session_create(mtl_handle mt, const mtl_video_config_t* config, + mtl_session_t** session); + +/** + * Create an audio session (ST30). + */ +int mtl_audio_session_create(mtl_handle mt, const mtl_audio_config_t* config, + mtl_session_t** session); + +/** + * Create an ancillary session (ST40). + */ +int mtl_ancillary_session_create(mtl_handle mt, const mtl_ancillary_config_t* config, + mtl_session_t** session); + +/************************************************************************* + * Session Operations - POLYMORPHIC (same for ALL media types!) + * + * This is the key API: same functions work for video, audio, ancillary. + * No need for separate st20p_tx_get_frame, st30p_tx_get_frame, etc. + *************************************************************************/ + +/** + * Start session processing. + * @return 0 on success, negative errno on error. + */ +int mtl_session_start(mtl_session_t* session); + +/** + * Stop session. + * + * After this call: + * - Session enters "stopped" state + * - mtl_session_buffer_get() returns -EAGAIN immediately (no blocking) + * - mtl_session_event_poll() returns -EAGAIN immediately (no blocking) + * - Application threads can detect -EAGAIN, check their stop flag, exit cleanly + * + * Can be restarted with mtl_session_start() (clears stopped state). + * Thread-safe: can be called from any thread (signal handler, main thread, etc.) + * + * Typical shutdown sequence: + * app->stop = true; // Your app flag + * mtl_session_stop(session); // Make buffer_get() return -EAGAIN + * pthread_join(app->worker, NULL); // Wait for worker to exit + * mtl_session_destroy(session); // Now safe to destroy + * + * @param session Session handle + * @return 0 on success, negative errno on error. + */ +int mtl_session_stop(mtl_session_t* session); + +/** + * Check if session is stopped. + * + * @param session Session handle + * @return true if stop() was called (and start() not called after), false otherwise + */ +bool mtl_session_is_stopped(mtl_session_t* session); + +/** + * Destroy session and free all resources. + * + * PRECONDITION: All application threads must have stopped using this session. + * Call mtl_session_stop() and join your threads first. + * + * @param session Session handle (invalid after return) + * @return 0 on success, negative errno on error + */ +int mtl_session_destroy(mtl_session_t* session); + +/** + * Get media type of session. + */ +mtl_media_type_t mtl_session_get_type(mtl_session_t* session); + +/************************************************************************* + * Buffer Operations - POLYMORPHIC (same for ALL media types!) + *************************************************************************/ + +/** + * Get buffer from session (library-owned mode). + * + * For TX: returns empty buffer to fill with data + * For RX: returns buffer containing received data + * + * Works the same whether session is video, audio, or ancillary! + * + * @param session Any session type (video/audio/ancillary) + * @param buffer Output buffer pointer + * @param timeout_ms Timeout (0 = non-blocking) + * @return 0 success, -ETIMEDOUT, -EAGAIN if stopped, or other negative errno + */ +int mtl_session_buffer_get(mtl_session_t* session, mtl_buffer_t** buffer, + uint32_t timeout_ms); + +/** + * Return buffer to session (library-owned mode). + * + * For TX: submits filled buffer for transmission + * For RX: returns processed buffer for reuse + * + * @param session Any session type + * @param buffer Buffer from mtl_session_buffer_get() + */ +int mtl_session_buffer_put(mtl_session_t* session, mtl_buffer_t* buffer); + +/** + * Post user-owned buffer (zero-copy mode). + * + * For TX: submits user buffer for transmission + * For RX: provides user buffer to receive into + * + * Buffer must be from registered memory region. + * Completion via mtl_session_event_poll(). + * + * @param session Any session type + * @param data Buffer data pointer (from registered region) + * @param size Buffer size + * @param user_ctx Returned in completion event + */ +int mtl_session_buffer_post(mtl_session_t* session, void* data, size_t size, + void* user_ctx); + +/** + * Flush pending buffers. + */ +int mtl_session_buffer_flush(mtl_session_t* session, uint32_t timeout_ms); + +/************************************************************************* + * Memory Registration (for user-owned/zero-copy mode) + *************************************************************************/ + +/** + * Register memory region for DMA. + * Required before posting buffers from this region. + */ +int mtl_session_mem_register(mtl_session_t* session, void* addr, size_t size, + mtl_dma_mem_t** handle); + +/** + * Unregister memory region. + */ +int mtl_session_mem_unregister(mtl_session_t* session, mtl_dma_mem_t* handle); + +/************************************************************************* + * Event Polling - POLYMORPHIC + *************************************************************************/ + +/** + * Poll for events. + * + * @param session Any session type + * @param event Output event + * @param timeout_ms Timeout (0 = non-blocking) + * @return 0 if event available, -ETIMEDOUT, -EAGAIN if stopped, or error + */ +int mtl_session_event_poll(mtl_session_t* session, mtl_event_t* event, + uint32_t timeout_ms); + +/************************************************************************* + * Statistics - POLYMORPHIC + *************************************************************************/ + +typedef struct mtl_session_stats { + uint64_t buffers_processed; + uint64_t bytes_processed; + uint64_t buffers_dropped; + uint32_t buffers_free; + uint32_t buffers_in_use; + /* TX specific */ + uint64_t epochs_missed; /**< Frames that missed their epoch */ + /* RX specific */ + uint64_t pkts_received; + uint64_t pkts_redundant; /**< Redundant path packets */ +} mtl_session_stats_t; + +int mtl_session_stats_get(mtl_session_t* session, mtl_session_stats_t* stats); +int mtl_session_stats_reset(mtl_session_t* session); + +/** + * Get the frame (buffer) size for the session in bytes. + * For TX, this is the transport frame size. For RX with conversion, + * this is the converted output frame size. + * + * @param session Session handle + * @return Frame size in bytes, or 0 on error + */ +size_t mtl_session_get_frame_size(mtl_session_t* session); + +/** + * Get detailed per-port IO statistics for the session. + * Wraps the underlying st20_tx/rx_get_session_stats(). + * The stats struct is type-specific (pass appropriate struct). + * + * @param session Session handle + * @param stats Output buffer (cast to st20_tx_user_stats or st20_rx_user_stats) + * @param stats_size Size of the stats buffer (for validation) + * @return 0 on success, negative errno on error + */ +int mtl_session_io_stats_get(mtl_session_t* session, void* stats, size_t stats_size); + +/** + * Reset per-port IO statistics. + * @return 0 on success, negative errno on error + */ +int mtl_session_io_stats_reset(mtl_session_t* session); + +/** + * Trigger pcap dump for an RX session (debug tool). + * + * @param session RX session handle + * @param max_dump_packets Maximum number of packets to dump + * @param sync If true, block until dump is complete + * @param meta Optional output metadata (file paths, dumped counts). NULL if not needed. + * @return 0 on success, negative errno on error + */ +int mtl_session_pcap_dump(mtl_session_t* session, uint32_t max_dump_packets, + bool sync, struct st_pcap_dump_meta* meta); + +/************************************************************************* + * Online Session Updates + *************************************************************************/ + +/** + * Update TX session destination (for stream switching). + * Allows changing destination IP/port without recreating session. + */ +int mtl_session_update_destination(mtl_session_t* session, + const struct st_tx_dest_info* dst); + +/** + * Update RX session source (for stream switching). + * Allows changing source filter without recreating session. + */ +int mtl_session_update_source(mtl_session_t* session, + const struct st_rx_source_info* src); + +/************************************************************************* + * Slice-Level API (ultra-low latency video) + * + * These functions are only valid for video sessions with + * mode = MTL_VIDEO_MODE_SLICE. + *************************************************************************/ + +/** + * TX Slice Mode: Notify library that lines are ready for transmission. + * + * In slice mode, application fills frame buffer line-by-line and + * calls this function to signal progress. Library transmits lines + * as they become available, achieving sub-frame latency. + * + * @param session Video TX session with mode=MTL_VIDEO_MODE_SLICE + * @param buffer Current frame buffer (from buffer_get) + * @param lines_ready Number of lines now ready (cumulative from top) + * @return 0 on success, -EINVAL if not slice mode + */ +int mtl_session_slice_ready(mtl_session_t* session, mtl_buffer_t* buffer, + uint16_t lines_ready); + +/** + * RX Slice Mode: Query how many lines have been received. + * + * Alternative to event-driven: application can poll for line progress. + * Useful when processing each line as it arrives. + * + * @param session Video RX session with mode=MTL_VIDEO_MODE_SLICE + * @param buffer Current frame buffer + * @param[out] lines_ready Number of lines received so far + * @return 0 on success, -EINVAL if not slice mode + */ +int mtl_session_slice_query(mtl_session_t* session, mtl_buffer_t* buffer, + uint16_t* lines_ready); + +/************************************************************************* + * Plugin Information + *************************************************************************/ + +/** + * Plugin capability info (returned by mtl_session_get_plugin_info) + */ +typedef struct mtl_plugin_info { + char name[64]; /**< Plugin name */ + char version[32]; /**< Plugin version string */ + enum st_plugin_device device; /**< CPU, GPU, FPGA */ + enum st22_codec codec; /**< Codec type (for ST22) */ + bool supports_interlaced; /**< Can handle interlaced */ + uint32_t max_threads; /**< Max codec threads */ +} mtl_plugin_info_t; + +/** + * Get info about the plugin used by this session. + * + * Only valid for ST22 compressed video sessions. + * + * @param session Video session using codec plugin + * @param[out] info Plugin information + * @return 0 on success, -ENOENT if no plugin, -EINVAL if not applicable + */ +int mtl_session_get_plugin_info(mtl_session_t* session, mtl_plugin_info_t* info); + +/************************************************************************* + * Queue Meta (for DATA_PATH_ONLY mode) + *************************************************************************/ + +/** + * Get queue metadata for DATA_PATH_ONLY mode. + * Application manages flow rules when this mode is enabled. + */ +int mtl_session_get_queue_meta(mtl_session_t* session, struct st_queue_meta* meta); + +/************************************************************************* + * Event FD for epoll/select integration + *************************************************************************/ + +/** + * Get file descriptor for event notification. + * Can be used with epoll/select to wait for events. + * @return fd >= 0 on success, negative errno on error + */ +int mtl_session_get_event_fd(mtl_session_t* session); + +/************************************************************************* + * Blocking behavior configuration + *************************************************************************/ + +/** + * Set timeout for blocking buffer_get operations. + * Only applies when BLOCK_GET flag is set. + * @param timeout_us Timeout in microseconds + */ +int mtl_session_set_block_timeout(mtl_session_t* session, uint64_t timeout_us); + +#if defined(__cplusplus) +} +#endif + +#endif /* _MTL_SESSION_API_HEAD_H_ */ diff --git a/lib/src/meson.build b/lib/src/meson.build index fc8440d78..f76bc37bb 100644 --- a/lib/src/meson.build +++ b/lib/src/meson.build @@ -33,4 +33,5 @@ subdir('datapath') subdir('dev') subdir('st2110') subdir('deprecated/udp') +subdir('new_api') diff --git a/lib/src/new_api/meson.build b/lib/src/new_api/meson.build new file mode 100644 index 000000000..b6a676e92 --- /dev/null +++ b/lib/src/new_api/meson.build @@ -0,0 +1,13 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright 2022 Intel Corporation + +sources += files( + 'mt_session.c', + 'mt_session.h', + 'mt_session_buffer.c', + 'mt_session_event.c', + 'mt_session_video_common.c', + 'mt_session_video_common.h', + 'mt_session_video_tx.c', + 'mt_session_video_rx.c', +) diff --git a/lib/src/new_api/mt_session.c b/lib/src/new_api/mt_session.c new file mode 100644 index 000000000..fb6a2065c --- /dev/null +++ b/lib/src/new_api/mt_session.c @@ -0,0 +1,621 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2024 Intel Corporation + */ + +/** + * @file mt_session.c + * + * Core polymorphic dispatch layer for the unified session API. + * This file implements the public mtl_session_* functions that dispatch + * to type-specific implementations via the vtable. + */ + +#include "mt_session.h" + +#include + +#include "../mt_log.h" +#include "../mt_mem.h" + +/************************************************************************* + * Session Allocation / Deallocation + *************************************************************************/ + +struct mtl_session_impl* mtl_session_alloc(struct mtl_main_impl* impl, int socket_id) { + struct mtl_session_impl* s; + + s = mt_rte_zmalloc_socket(sizeof(*s), socket_id); + if (!s) { + err("%s, failed to alloc session memory\n", __func__); + return NULL; + } + + s->parent = impl; + s->socket_id = socket_id; + __atomic_store_n(&s->state, MTL_SESSION_STATE_CREATED, __ATOMIC_RELAXED); + __atomic_store_n(&s->stopped, 0, __ATOMIC_RELAXED); + s->event_fd = -1; + + return s; +} + +void mtl_session_free(struct mtl_session_impl* s) { + if (!s) return; + + mtl_session_events_uinit(s); + mtl_session_user_buf_uinit(s); + mtl_session_buffers_uinit(s); + + s->magic = 0; /* Invalidate handle */ + mt_rte_free(s); +} + +/************************************************************************* + * Session Creation - Type-Specific Entry Points + *************************************************************************/ + +int mtl_video_session_create(mtl_handle mt, const mtl_video_config_t* config, + mtl_session_t** session) { + struct mtl_main_impl* impl = mt; + struct mtl_session_impl* s; + int ret; + + if (!mt || !config || !session) { + err("%s, invalid args\n", __func__); + return -EINVAL; + } + + int socket_id = config->base.socket_id; + if (socket_id < 0) socket_id = mt_socket_id(impl, MTL_PORT_P); + + s = mtl_session_alloc(impl, socket_id); + if (!s) return -ENOMEM; + + s->type = MTL_TYPE_VIDEO; + s->direction = config->base.direction; + s->ownership = config->base.ownership; + s->flags = config->base.flags; + s->notify_buffer_ready = config->base.notify_buffer_ready; + s->notify_priv = config->base.priv; + s->video.compressed = config->compressed; + s->video.mode = config->mode; + + if (config->base.name) { + snprintf(s->name, ST_MAX_NAME_LEN, "%s", config->base.name); + } + + /* Initialize event queue */ + ret = mtl_session_events_init(s); + if (ret < 0) { + err("%s, events init failed: %d\n", __func__, ret); + mtl_session_free(s); + return ret; + } + + /* Initialize type-specific session */ + if (config->base.direction == MTL_SESSION_TX) { + s->magic = MTL_SESSION_MAGIC_VIDEO_TX; + s->vt = &mtl_video_tx_vtable; + ret = mtl_video_tx_session_init(s, impl, config); + } else { + s->magic = MTL_SESSION_MAGIC_VIDEO_RX; + s->vt = &mtl_video_rx_vtable; + ret = mtl_video_rx_session_init(s, impl, config); + } + + if (ret < 0) { + err("%s, session init failed: %d\n", __func__, ret); + mtl_session_free(s); + return ret; + } + + /* Initialize buffer wrappers */ + if (config->base.num_buffers > 0) { + ret = mtl_session_buffers_init(s, config->base.num_buffers); + if (ret < 0) { + err("%s, buffers init failed: %d\n", __func__, ret); + if (config->base.direction == MTL_SESSION_TX) + mtl_video_tx_session_uinit(s); + else + mtl_video_rx_session_uinit(s); + mtl_session_free(s); + return ret; + } + } + + info("%s(%s), created %s video %s session\n", __func__, s->name, + config->compressed ? "ST22" : "ST20", + config->base.direction == MTL_SESSION_TX ? "TX" : "RX"); + + *session = MTL_SESSION_PUB(s); + return 0; +} + +int mtl_audio_session_create(mtl_handle mt, const mtl_audio_config_t* config, + mtl_session_t** session) { + /* Audio session creation - to be implemented */ + (void)mt; + (void)config; + (void)session; + err("%s, not yet implemented\n", __func__); + return -ENOTSUP; +} + +int mtl_ancillary_session_create(mtl_handle mt, const mtl_ancillary_config_t* config, + mtl_session_t** session) { + /* Ancillary session creation - to be implemented */ + (void)mt; + (void)config; + (void)session; + err("%s, not yet implemented\n", __func__); + return -ENOTSUP; +} + +/************************************************************************* + * Session Lifecycle - Polymorphic + *************************************************************************/ + +int mtl_session_start(mtl_session_t* session) { + struct mtl_session_impl* s = MTL_SESSION_IMPL(session); + + if (!s || !MTL_SESSION_VALID(s)) { + err("%s, invalid session handle\n", __func__); + return -EINVAL; + } + + if (__atomic_load_n(&s->state, __ATOMIC_ACQUIRE) == MTL_SESSION_STATE_STARTED) { + return 0; /* Already started */ + } + + mtl_session_clear_stopped(s); + + int ret = 0; + if (s->vt && s->vt->start) { + ret = s->vt->start(s); + } + + if (ret == 0) { + info("%s(%s), session started\n", __func__, s->name); + } + + return ret; +} + +int mtl_session_stop(mtl_session_t* session) { + struct mtl_session_impl* s = MTL_SESSION_IMPL(session); + + if (!s || !MTL_SESSION_VALID(s)) { + return -EINVAL; + } + + /* Set stopped flag - this is safe from signal handlers */ + mtl_session_set_stopped(s); + + /* Call type-specific stop if available */ + if (s->vt && s->vt->stop) { + s->vt->stop(s); + } + + dbg("%s(%s), session stopped\n", __func__, s->name); + return 0; +} + +bool mtl_session_is_stopped(mtl_session_t* session) { + struct mtl_session_impl* s = MTL_SESSION_IMPL(session); + + if (!s || !MTL_SESSION_VALID(s)) return true; + + return mtl_session_check_stopped(s); +} + +int mtl_session_destroy(mtl_session_t* session) { + struct mtl_session_impl* s = MTL_SESSION_IMPL(session); + + if (!s || !MTL_SESSION_VALID(s)) { + err("%s, invalid session handle\n", __func__); + return -EINVAL; + } + + info("%s(%s), destroying session\n", __func__, s->name); + + /* Call type-specific destroy */ + if (s->vt && s->vt->destroy) { + s->vt->destroy(s); + } + + mtl_session_free(s); + return 0; +} + +mtl_media_type_t mtl_session_get_type(mtl_session_t* session) { + struct mtl_session_impl* s = MTL_SESSION_IMPL(session); + + if (!s || !MTL_SESSION_VALID(s)) return MTL_TYPE_VIDEO; /* Default */ + + return s->type; +} + +/************************************************************************* + * Buffer Operations - Polymorphic + *************************************************************************/ + +int mtl_session_buffer_get(mtl_session_t* session, mtl_buffer_t** buffer, + uint32_t timeout_ms) { + struct mtl_session_impl* s = MTL_SESSION_IMPL(session); + + if (!s || !MTL_SESSION_VALID(s) || !buffer) { + return -EINVAL; + } + + /* Check stopped flag first - fast path for shutdown */ + if (mtl_session_check_stopped(s)) { + return -EAGAIN; + } + + if (!s->vt || !s->vt->buffer_get) { + return -ENOTSUP; + } + + return s->vt->buffer_get(s, buffer, timeout_ms); +} + +int mtl_session_buffer_put(mtl_session_t* session, mtl_buffer_t* buffer) { + struct mtl_session_impl* s = MTL_SESSION_IMPL(session); + + if (!s || !MTL_SESSION_VALID(s) || !buffer) { + return -EINVAL; + } + + if (!s->vt || !s->vt->buffer_put) { + return -ENOTSUP; + } + + return s->vt->buffer_put(s, buffer); +} + +int mtl_session_buffer_post(mtl_session_t* session, void* data, size_t size, + void* user_ctx) { + struct mtl_session_impl* s = MTL_SESSION_IMPL(session); + + if (!s || !MTL_SESSION_VALID(s) || !data) { + return -EINVAL; + } + + if (mtl_session_check_stopped(s)) { + return -EAGAIN; + } + + if (!s->vt || !s->vt->buffer_post) { + return -ENOTSUP; + } + + return s->vt->buffer_post(s, data, size, user_ctx); +} + +int mtl_session_buffer_flush(mtl_session_t* session, uint32_t timeout_ms) { + struct mtl_session_impl* s = MTL_SESSION_IMPL(session); + + if (!s || !MTL_SESSION_VALID(s)) { + return -EINVAL; + } + + if (!s->vt || !s->vt->buffer_flush) { + return -ENOTSUP; + } + + return s->vt->buffer_flush(s, timeout_ms); +} + +/************************************************************************* + * Memory Registration + *************************************************************************/ + +int mtl_session_mem_register(mtl_session_t* session, void* addr, size_t size, + mtl_dma_mem_t** handle) { + struct mtl_session_impl* s = MTL_SESSION_IMPL(session); + + if (!s || !MTL_SESSION_VALID(s) || !addr || !handle) { + return -EINVAL; + } + + if (!s->vt || !s->vt->mem_register) { + return -ENOTSUP; + } + + return s->vt->mem_register(s, addr, size, handle); +} + +int mtl_session_mem_unregister(mtl_session_t* session, mtl_dma_mem_t* handle) { + struct mtl_session_impl* s = MTL_SESSION_IMPL(session); + + if (!s || !MTL_SESSION_VALID(s) || !handle) { + return -EINVAL; + } + + if (!s->vt || !s->vt->mem_unregister) { + return -ENOTSUP; + } + + return s->vt->mem_unregister(s, handle); +} + +/************************************************************************* + * Event Polling + *************************************************************************/ + +int mtl_session_event_poll(mtl_session_t* session, mtl_event_t* event, + uint32_t timeout_ms) { + struct mtl_session_impl* s = MTL_SESSION_IMPL(session); + + if (!s || !MTL_SESSION_VALID(s) || !event) { + return -EINVAL; + } + + /* Check stopped flag first */ + if (mtl_session_check_stopped(s)) { + return -EAGAIN; + } + + if (!s->vt || !s->vt->event_poll) { + /* Default: try to dequeue from event ring */ + if (s->event_ring) { + void* obj = NULL; + if (rte_ring_dequeue(s->event_ring, &obj) == 0 && obj) { + mtl_event_t* ev = (mtl_event_t*)obj; + *event = *ev; + mt_rte_free(ev); + return 0; + } + } + return -ETIMEDOUT; + } + + return s->vt->event_poll(s, event, timeout_ms); +} + +int mtl_session_get_event_fd(mtl_session_t* session) { + struct mtl_session_impl* s = MTL_SESSION_IMPL(session); + + if (!s || !MTL_SESSION_VALID(s)) { + return -EINVAL; + } + + if (s->vt && s->vt->get_event_fd) { + return s->vt->get_event_fd(s); + } + + return s->event_fd; +} + +/************************************************************************* + * Statistics + *************************************************************************/ + +int mtl_session_stats_get(mtl_session_t* session, mtl_session_stats_t* stats) { + struct mtl_session_impl* s = MTL_SESSION_IMPL(session); + + if (!s || !MTL_SESSION_VALID(s) || !stats) { + return -EINVAL; + } + + if (s->vt && s->vt->stats_get) { + return s->vt->stats_get(s, stats); + } + + /* Default: return cached stats (atomic reads, relaxed ordering) */ + stats->buffers_processed = + __atomic_load_n(&s->stats.buffers_processed, __ATOMIC_RELAXED); + stats->bytes_processed = + __atomic_load_n(&s->stats.bytes_processed, __ATOMIC_RELAXED); + stats->buffers_dropped = + __atomic_load_n(&s->stats.buffers_dropped, __ATOMIC_RELAXED); + stats->epochs_missed = + __atomic_load_n(&s->stats.epochs_missed, __ATOMIC_RELAXED); + stats->buffers_free = 0; + stats->buffers_in_use = 0; + return 0; +} + +int mtl_session_stats_reset(mtl_session_t* session) { + struct mtl_session_impl* s = MTL_SESSION_IMPL(session); + + if (!s || !MTL_SESSION_VALID(s)) { + return -EINVAL; + } + + if (s->vt && s->vt->stats_reset) { + return s->vt->stats_reset(s); + } + + __atomic_store_n(&s->stats.buffers_processed, 0, __ATOMIC_RELAXED); + __atomic_store_n(&s->stats.bytes_processed, 0, __ATOMIC_RELAXED); + __atomic_store_n(&s->stats.buffers_dropped, 0, __ATOMIC_RELAXED); + __atomic_store_n(&s->stats.epochs_missed, 0, __ATOMIC_RELAXED); + return 0; +} + +size_t mtl_session_get_frame_size(mtl_session_t* session) { + struct mtl_session_impl* s = MTL_SESSION_IMPL(session); + + if (!s || !MTL_SESSION_VALID(s)) { + return 0; + } + + if (!s->vt || !s->vt->get_frame_size) { + return 0; + } + + return s->vt->get_frame_size(s); +} + +int mtl_session_io_stats_get(mtl_session_t* session, void* stats, size_t stats_size) { + struct mtl_session_impl* s = MTL_SESSION_IMPL(session); + + if (!s || !MTL_SESSION_VALID(s) || !stats || !stats_size) { + return -EINVAL; + } + + if (!s->vt || !s->vt->io_stats_get) { + return -ENOTSUP; + } + + return s->vt->io_stats_get(s, stats, stats_size); +} + +int mtl_session_io_stats_reset(mtl_session_t* session) { + struct mtl_session_impl* s = MTL_SESSION_IMPL(session); + + if (!s || !MTL_SESSION_VALID(s)) { + return -EINVAL; + } + + if (!s->vt || !s->vt->io_stats_reset) { + return -ENOTSUP; + } + + return s->vt->io_stats_reset(s); +} + +int mtl_session_pcap_dump(mtl_session_t* session, uint32_t max_dump_packets, + bool sync, struct st_pcap_dump_meta* meta) { + struct mtl_session_impl* s = MTL_SESSION_IMPL(session); + + if (!s || !MTL_SESSION_VALID(s)) { + return -EINVAL; + } + + if (!s->vt || !s->vt->pcap_dump) { + return -ENOTSUP; + } + + return s->vt->pcap_dump(s, max_dump_packets, sync, meta); +} + +/************************************************************************* + * Online Updates + *************************************************************************/ + +int mtl_session_update_destination(mtl_session_t* session, + const struct st_tx_dest_info* dst) { + struct mtl_session_impl* s = MTL_SESSION_IMPL(session); + + if (!s || !MTL_SESSION_VALID(s) || !dst) { + return -EINVAL; + } + + if (s->direction != MTL_SESSION_TX) { + err("%s(%s), not a TX session\n", __func__, s->name); + return -EINVAL; + } + + if (!s->vt || !s->vt->update_destination) { + return -ENOTSUP; + } + + return s->vt->update_destination(s, dst); +} + +int mtl_session_update_source(mtl_session_t* session, + const struct st_rx_source_info* src) { + struct mtl_session_impl* s = MTL_SESSION_IMPL(session); + + if (!s || !MTL_SESSION_VALID(s) || !src) { + return -EINVAL; + } + + if (s->direction != MTL_SESSION_RX) { + err("%s(%s), not an RX session\n", __func__, s->name); + return -EINVAL; + } + + if (!s->vt || !s->vt->update_source) { + return -ENOTSUP; + } + + return s->vt->update_source(s, src); +} + +/************************************************************************* + * Slice Mode + *************************************************************************/ + +int mtl_session_slice_ready(mtl_session_t* session, mtl_buffer_t* buffer, + uint16_t lines_ready) { + struct mtl_session_impl* s = MTL_SESSION_IMPL(session); + + if (!s || !MTL_SESSION_VALID(s) || !buffer) { + return -EINVAL; + } + + if (!s->vt || !s->vt->slice_ready) { + return -ENOTSUP; + } + + return s->vt->slice_ready(s, buffer, lines_ready); +} + +int mtl_session_slice_query(mtl_session_t* session, mtl_buffer_t* buffer, + uint16_t* lines_ready) { + struct mtl_session_impl* s = MTL_SESSION_IMPL(session); + + if (!s || !MTL_SESSION_VALID(s) || !buffer || !lines_ready) { + return -EINVAL; + } + + if (!s->vt || !s->vt->slice_query) { + return -ENOTSUP; + } + + return s->vt->slice_query(s, buffer, lines_ready); +} + +/************************************************************************* + * Plugin Info + *************************************************************************/ + +int mtl_session_get_plugin_info(mtl_session_t* session, mtl_plugin_info_t* info) { + struct mtl_session_impl* s = MTL_SESSION_IMPL(session); + + if (!s || !MTL_SESSION_VALID(s) || !info) { + return -EINVAL; + } + + if (!s->vt || !s->vt->get_plugin_info) { + return -ENOTSUP; + } + + return s->vt->get_plugin_info(s, info); +} + +/************************************************************************* + * Queue Meta + *************************************************************************/ + +int mtl_session_get_queue_meta(mtl_session_t* session, struct st_queue_meta* meta) { + struct mtl_session_impl* s = MTL_SESSION_IMPL(session); + + if (!s || !MTL_SESSION_VALID(s) || !meta) { + return -EINVAL; + } + + if (!s->vt || !s->vt->get_queue_meta) { + return -ENOTSUP; + } + + return s->vt->get_queue_meta(s, meta); +} + +/************************************************************************* + * Block Timeout + *************************************************************************/ + +int mtl_session_set_block_timeout(mtl_session_t* session, uint64_t timeout_us) { + struct mtl_session_impl* s = MTL_SESSION_IMPL(session); + + if (!s || !MTL_SESSION_VALID(s)) { + return -EINVAL; + } + + (void)timeout_us; /* TODO: implement when block mode is needed */ + return 0; +} diff --git a/lib/src/new_api/mt_session.h b/lib/src/new_api/mt_session.h new file mode 100644 index 000000000..ef1023b60 --- /dev/null +++ b/lib/src/new_api/mt_session.h @@ -0,0 +1,493 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2024 Intel Corporation + */ + +/** + * @file mt_session.h + * + * Internal definitions for unified session API implementation. + * NOT part of public API - for library implementation only. + * + * Architecture Note: + * ----------------- + * This new unified API wraps the low-level session structures: + * - st_tx_video_session_impl / st_rx_video_session_impl (from st_header.h) + * - st_frame_trans (the actual frame buffer structure) + * + * The pipeline layer (st20p_*, st_frame, etc.) is kept for backward compatibility + * but new code should use the unified session API. + */ + +#ifndef _MT_LIB_SESSION_H_ +#define _MT_LIB_SESSION_H_ + +#include "mtl_session_api.h" + +/* Internal MTL headers */ +#include "../mt_main.h" +#include "../st2110/st_header.h" + +#include +#include + +#if defined(__cplusplus) +extern "C" { +#endif + +/************************************************************************* + * Forward Declarations + *************************************************************************/ + +struct mtl_session_impl; +struct mtl_buffer_impl; + +/************************************************************************* + * VTable - Polymorphic Dispatch + * + * Each media type (video/audio/ancillary) implements these functions. + * The unified API dispatches through this table. + *************************************************************************/ + +typedef struct mtl_session_vtable { + /* Lifecycle */ + int (*start)(struct mtl_session_impl* s); + int (*stop)(struct mtl_session_impl* s); + void (*destroy)(struct mtl_session_impl* s); + + /* Buffer operations */ + int (*buffer_get)(struct mtl_session_impl* s, mtl_buffer_t** buf, uint32_t timeout_ms); + int (*buffer_put)(struct mtl_session_impl* s, mtl_buffer_t* buf); + int (*buffer_post)(struct mtl_session_impl* s, void* data, size_t size, void* ctx); + int (*buffer_flush)(struct mtl_session_impl* s, uint32_t timeout_ms); + + /* Memory registration */ + int (*mem_register)(struct mtl_session_impl* s, void* addr, size_t size, + mtl_dma_mem_t** handle); + int (*mem_unregister)(struct mtl_session_impl* s, mtl_dma_mem_t* handle); + + /* Events */ + int (*event_poll)(struct mtl_session_impl* s, mtl_event_t* event, uint32_t timeout_ms); + int (*get_event_fd)(struct mtl_session_impl* s); + + /* Stats */ + int (*stats_get)(struct mtl_session_impl* s, mtl_session_stats_t* stats); + int (*stats_reset)(struct mtl_session_impl* s); + + /* Frame size query */ + size_t (*get_frame_size)(struct mtl_session_impl* s); + + /* IO stats (per-port detailed stats) */ + int (*io_stats_get)(struct mtl_session_impl* s, void* stats, size_t stats_size); + int (*io_stats_reset)(struct mtl_session_impl* s); + + /* Pcap dump (RX only) */ + int (*pcap_dump)(struct mtl_session_impl* s, uint32_t max_pkts, bool sync, + struct st_pcap_dump_meta* meta); + + /* Online updates */ + int (*update_destination)(struct mtl_session_impl* s, + const struct st_tx_dest_info* dst); + int (*update_source)(struct mtl_session_impl* s, const struct st_rx_source_info* src); + + /* Slice mode (video only, NULL for audio/ancillary) */ + int (*slice_ready)(struct mtl_session_impl* s, mtl_buffer_t* buf, uint16_t lines); + int (*slice_query)(struct mtl_session_impl* s, mtl_buffer_t* buf, uint16_t* lines); + + /* Plugin info (ST22 only, NULL otherwise) */ + int (*get_plugin_info)(struct mtl_session_impl* s, mtl_plugin_info_t* info); + + /* Queue meta (for DATA_PATH_ONLY) */ + int (*get_queue_meta)(struct mtl_session_impl* s, struct st_queue_meta* meta); + +} mtl_session_vtable_t; + +/************************************************************************* + * Session State + *************************************************************************/ + +typedef enum mtl_session_state { + MTL_SESSION_STATE_CREATED = 0, + MTL_SESSION_STATE_STARTED, + MTL_SESSION_STATE_STOPPED, /**< stop() called - buffer_get returns -EAGAIN */ + MTL_SESSION_STATE_ERROR, +} mtl_session_state_t; + +/************************************************************************* + * Internal Buffer Implementation + * + * Wraps st_frame_trans (the actual frame buffer from st_header.h) + *************************************************************************/ + +struct mtl_buffer_impl { + /* Public view (returned to user) */ + mtl_buffer_t pub; + + /* Internal linkage */ + struct mtl_session_impl* session; + uint32_t idx; /**< Buffer index in pool */ + + /* + * The ACTUAL frame buffer from low-level session. + * st_frame_trans contains: addr, iova, refcnt, flags, metadata, etc. + * This is NOT st_frame from pipeline - that's the old API. + */ + struct st_frame_trans* frame_trans; + + /* For user-owned mode */ + void* user_ctx; /**< User context for completion */ + bool user_owned; /**< true if app-owned buffer */ +}; + +/************************************************************************* + * User-Owned Buffer Entry (for buffer_post ring) + * + * When app calls mtl_session_buffer_post(), we queue this entry. + * TX: picked up by get_next_frame → st20_tx_set_ext_frame() + * RX: picked up by query_ext_frame callback + *************************************************************************/ + +struct mtl_user_buffer_entry { + void* data; /**< User buffer virtual address */ + mtl_iova_t iova; /**< DMA-mapped IOVA of user buffer */ + size_t size; /**< Buffer size */ + void* user_ctx; /**< User context returned in completion event */ +}; + +/************************************************************************* + * DMA Memory Registration Handle + * + * Wraps user memory that has been DMA-mapped for zero-copy I/O. + *************************************************************************/ + +struct mtl_dma_mem_impl { + struct mtl_main_impl* parent; /**< MTL instance */ + void* addr; /**< User-provided virtual address */ + size_t size; /**< Size of the registered region */ + mtl_iova_t iova; /**< DMA-mapped IOVA base address */ + bool hp_mapped; /**< true if mapped via hugepage allocator */ +}; + +/************************************************************************* + * Internal Session Implementation + * + * Contains pointer to ACTUAL low-level session impl from st_header.h. + *************************************************************************/ + +struct mtl_session_impl { + /* VTable for polymorphic dispatch - MUST BE FIRST */ + const mtl_session_vtable_t* vt; + + /* Type identification */ + uint32_t magic; /**< Magic number for validation */ + mtl_media_type_t type; /**< VIDEO, AUDIO, ANCILLARY */ + mtl_session_dir_t direction; /**< TX or RX */ + + /* Parent context */ + struct mtl_main_impl* parent; /**< MTL instance (internal type) */ + int idx; /**< Session index (for logging) */ + int socket_id; /**< NUMA socket */ + + /* + * Session state — accessed with C11 __atomic builtins. + * No lock needed; state transitions pair with the stopped flag. + */ + mtl_session_state_t state; + + /** + * Atomic stopped flag — the primary cross-thread signal. + * Set by stop(), checked by buffer_get/event_poll to return -EAGAIN. + * + * Memory ordering rationale: + * store (__ATOMIC_RELEASE): all prior stores (state, data) are visible + * before stopped is observed by other threads. + * load (__ATOMIC_ACQUIRE): subsequent reads in the checking thread see + * all stores that happened before the set. + */ + int stopped; + + /* Configuration (copied from create) */ + char name[ST_MAX_NAME_LEN]; + uint32_t flags; + mtl_buffer_ownership_t ownership; + + /* + * Pointer to the ACTUAL low-level session implementation. + * These are the real session structs from st_header.h that contain + * all the frame management, pacing, stats, etc. + */ + union { + /* Video - direct to low-level impl */ + struct st_tx_video_session_impl* video_tx; + struct st_rx_video_session_impl* video_rx; + + /* Audio - direct to low-level impl */ + struct st_tx_audio_session_impl* audio_tx; + struct st_rx_audio_session_impl* audio_rx; + + /* Ancillary - direct to low-level impl */ + struct st_tx_ancillary_session_impl* anc_tx; + struct st_rx_ancillary_session_impl* anc_rx; + } inner; + + /* + * Frame buffer management. + * For library-owned mode, we manage mtl_buffer_impl wrappers. + * The actual frame memory is in inner->st20_frames (st_frame_trans array). + * + * Thread safety: completely lock-free. + * - TX: atomic CAS on per-frame state (enum tx_frame_state) provides + * mutual exclusion — only the CAS winner owns a given frame. + * - RX: multi-consumer rte_ring ensures safe concurrent dequeue. + * Buffer wrapper assignment is race-free because each frame_idx maps + * 1:1 to a unique buffer_impl slot (buffer_count >= frame_count). + */ + uint32_t buffer_count; + struct mtl_buffer_impl* buffers; /**< Buffer wrapper pool */ + + /* Event queue */ + struct rte_ring* event_ring; /**< Pending events */ + int event_fd; /**< For epoll integration */ + + /* + * User-owned buffer management (MTL_BUFFER_USER_OWNED mode). + * + * TX: app posts buffers via buffer_post() → queued in user_buf_ring. + * get_next_frame picks them up, calls st20_tx_set_ext_frame(). + * notify_frame_done fires → MTL_EVENT_BUFFER_DONE with user_ctx. + * + * RX: app posts buffers via buffer_post() → queued in user_buf_ring. + * query_ext_frame callback dequeues and provides to library. + * notify_frame_ready → MTL_EVENT_BUFFER_READY with user_ctx. + * + * Thread safety: rte_ring is lock-free SPSC/MPSC/MPMC. + * user_buf_ctx array is indexed by frame_idx (1:1 mapping, no lock). + */ + struct rte_ring* user_buf_ring; /**< Pending user buffers to post */ + void** user_buf_ctx; /**< Per-frame user_ctx, indexed by frame_idx */ + uint16_t user_buf_ctx_cnt; /**< Size of user_buf_ctx array */ + + /* DMA memory registrations (for user-owned buffers) */ + struct mtl_dma_mem_impl* dma_registrations[8]; /**< Up to 8 registered regions */ + uint8_t dma_registration_cnt; + + /* + * Statistics — aggregated view of inner session stats. + * Thread safety: individual counter fields are accessed with __atomic builtins. + * Increments use __ATOMIC_RELAXED (no ordering needed for counters). + * Reads/resets also use __ATOMIC_RELAXED (approximate snapshot is fine). + * No lock needed — each field is independently atomic. + */ + mtl_session_stats_t stats; + + /* Callbacks (optional, for low-latency notification) */ + int (*notify_buffer_ready)(void* priv); + void* notify_priv; + + /* + * Type-specific cached config. + * Actual config is in the inner session impl. + */ + union { + struct { + bool compressed; /**< ST22 mode */ + mtl_video_mode_t mode; /**< FRAME or SLICE */ + enum st_frame_fmt frame_fmt; /**< App pixel format (may differ from transport) */ + bool derive; /**< true if frame_fmt == transport_fmt (no conversion) */ + } video; + struct { + uint32_t channels; + } audio; + }; +}; + +/************************************************************************* + * Magic Numbers for Handle Validation + *************************************************************************/ + +#define MTL_SESSION_MAGIC_VIDEO_TX 0x4D564458 /* "MVTX" */ +#define MTL_SESSION_MAGIC_VIDEO_RX 0x4D565258 /* "MVRX" */ +#define MTL_SESSION_MAGIC_AUDIO_TX 0x4D415458 /* "MATX" */ +#define MTL_SESSION_MAGIC_AUDIO_RX 0x4D415258 /* "MARX" */ +#define MTL_SESSION_MAGIC_ANC_TX 0x4D4E5458 /* "MNTX" */ +#define MTL_SESSION_MAGIC_ANC_RX 0x4D4E5258 /* "MNRX" */ + +/************************************************************************* + * Internal Helper Macros + *************************************************************************/ + +/** Validate session handle */ +#define MTL_SESSION_VALID(s) \ + ((s) && ((s)->magic == MTL_SESSION_MAGIC_VIDEO_TX || \ + (s)->magic == MTL_SESSION_MAGIC_VIDEO_RX || \ + (s)->magic == MTL_SESSION_MAGIC_AUDIO_TX || \ + (s)->magic == MTL_SESSION_MAGIC_AUDIO_RX || \ + (s)->magic == MTL_SESSION_MAGIC_ANC_TX || (s)->magic == MTL_SESSION_MAGIC_ANC_RX)) + +/** Get implementation from public handle */ +#define MTL_SESSION_IMPL(pub) ((struct mtl_session_impl*)(pub)) + +/** Get public handle from implementation */ +#define MTL_SESSION_PUB(impl) ((mtl_session_t*)(impl)) + +/** Get buffer implementation from public handle */ +#define MTL_BUFFER_IMPL(_pub) \ + ((struct mtl_buffer_impl*)((char*)(_pub) - offsetof(struct mtl_buffer_impl, pub))) + +/************************************************************************* + * Internal Functions - Video Session + * + * These create/init the actual st_tx/rx_video_session_impl and + * attach to the session manager, similar to current st20_tx_create(). + *************************************************************************/ + +/** Create video TX session */ +int mtl_video_tx_session_init(struct mtl_session_impl* s, struct mtl_main_impl* impl, + const mtl_video_config_t* config); + +/** Create video RX session */ +int mtl_video_rx_session_init(struct mtl_session_impl* s, struct mtl_main_impl* impl, + const mtl_video_config_t* config); + +/** Cleanup video TX session */ +void mtl_video_tx_session_uinit(struct mtl_session_impl* s); + +/** Cleanup video RX session */ +void mtl_video_rx_session_uinit(struct mtl_session_impl* s); + +/** Video session vtables */ +extern const mtl_session_vtable_t mtl_video_tx_vtable; +extern const mtl_session_vtable_t mtl_video_rx_vtable; + +/************************************************************************* + * Internal Functions - Audio Session (stub for future) + *************************************************************************/ + +/** Audio session vtables */ +extern const mtl_session_vtable_t mtl_audio_tx_vtable; +extern const mtl_session_vtable_t mtl_audio_rx_vtable; + +/************************************************************************* + * Internal Functions - Ancillary Session (stub for future) + *************************************************************************/ + +/** Ancillary session vtables */ +extern const mtl_session_vtable_t mtl_ancillary_tx_vtable; +extern const mtl_session_vtable_t mtl_ancillary_rx_vtable; + +/************************************************************************* + * Internal Utilities + *************************************************************************/ + +/** Allocate session structure */ +struct mtl_session_impl* mtl_session_alloc(struct mtl_main_impl* impl, int socket_id); + +/** Free session structure */ +void mtl_session_free(struct mtl_session_impl* s); + +/** Initialize buffer wrapper pool (library-owned mode) */ +int mtl_session_buffers_init(struct mtl_session_impl* s, uint32_t count); + +/** Cleanup buffer wrapper pool */ +void mtl_session_buffers_uinit(struct mtl_session_impl* s); + +/** Initialize event ring */ +int mtl_session_events_init(struct mtl_session_impl* s); + +/** Cleanup event ring */ +void mtl_session_events_uinit(struct mtl_session_impl* s); + +/** Post event to session */ +int mtl_session_event_post(struct mtl_session_impl* s, const mtl_event_t* event); + +/************************************************************************* + * User-Owned Buffer Helpers + *************************************************************************/ + +/** Initialize user-owned buffer ring and per-frame context array */ +int mtl_session_user_buf_init(struct mtl_session_impl* s, uint16_t frame_cnt); + +/** Cleanup user-owned buffer resources */ +void mtl_session_user_buf_uinit(struct mtl_session_impl* s); + +/** Enqueue a user buffer entry into the pending ring */ +int mtl_session_user_buf_enqueue(struct mtl_session_impl* s, void* data, + mtl_iova_t iova, size_t size, void* user_ctx); + +/** Dequeue a user buffer entry from the pending ring */ +int mtl_session_user_buf_dequeue(struct mtl_session_impl* s, + struct mtl_user_buffer_entry* entry); + +/** Look up IOVA for a user virtual address from registered DMA regions */ +mtl_iova_t mtl_session_lookup_iova(struct mtl_session_impl* s, void* addr, size_t size); + +/** + * Populate mtl_buffer public fields from st_frame_trans. + * Called when getting a buffer to fill the user-visible fields. + */ +void mtl_buffer_fill_from_frame_trans(struct mtl_buffer_impl* b, + struct st_frame_trans* ft, + mtl_media_type_t type); + +/** + * Get a free st_frame_trans from the session's frame pool. + * Uses refcnt == 0 to find free frame, then increments refcnt. + */ +struct st_frame_trans* mtl_session_get_frame_trans(struct mtl_session_impl* s); + +/** + * Release st_frame_trans back to pool (decrement refcnt). + */ +void mtl_session_put_frame_trans(struct st_frame_trans* ft); + +/************************************************************************* + * Stop/Start Helpers + * + * stop() sets stopped=true, buffer_get/event_poll return -EAGAIN + * start() clears stopped, blocking calls work again + *************************************************************************/ + +/** + * Check if session is stopped (thread-safe, acquire semantics). + * Call this at the start of any blocking operation. + */ +static inline bool mtl_session_check_stopped(struct mtl_session_impl* s) { + return __atomic_load_n(&s->stopped, __ATOMIC_ACQUIRE) != 0; +} + +/** + * Set stopped flag (thread-safe). Called by mtl_session_stop(). + * Uses release semantics so all prior stores are visible. + */ +static inline void mtl_session_set_stopped(struct mtl_session_impl* s) { + /* Store state first (relaxed), then stopped with release. + * The release on stopped ensures that the state store is visible + * to any thread that observes stopped == 1 via acquire load. */ + __atomic_store_n(&s->state, MTL_SESSION_STATE_STOPPED, __ATOMIC_RELAXED); + __atomic_store_n(&s->stopped, 1, __ATOMIC_RELEASE); +} + +/** + * Clear stopped flag (thread-safe). Called by mtl_session_start(). + */ +static inline void mtl_session_clear_stopped(struct mtl_session_impl* s) { + __atomic_store_n(&s->state, MTL_SESSION_STATE_STARTED, __ATOMIC_RELAXED); + __atomic_store_n(&s->stopped, 0, __ATOMIC_RELEASE); +} + +/************************************************************************* + * Logging Helpers + *************************************************************************/ + +#define MTL_SESSION_LOG(level, s, fmt, ...) \ + level("%s(%d), " fmt, __func__, (s)->idx, ##__VA_ARGS__) + +#define MTL_SESSION_DBG(s, fmt, ...) MTL_SESSION_LOG(dbg, s, fmt, ##__VA_ARGS__) +#define MTL_SESSION_INFO(s, fmt, ...) MTL_SESSION_LOG(info, s, fmt, ##__VA_ARGS__) +#define MTL_SESSION_WARN(s, fmt, ...) MTL_SESSION_LOG(warn, s, fmt, ##__VA_ARGS__) +#define MTL_SESSION_ERR(s, fmt, ...) MTL_SESSION_LOG(err, s, fmt, ##__VA_ARGS__) + +#if defined(__cplusplus) +} +#endif + +#endif /* _MT_LIB_SESSION_H_ */ diff --git a/lib/src/new_api/mt_session_buffer.c b/lib/src/new_api/mt_session_buffer.c new file mode 100644 index 000000000..88be4ca5a --- /dev/null +++ b/lib/src/new_api/mt_session_buffer.c @@ -0,0 +1,323 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2024 Intel Corporation + */ + +/** + * @file mt_session_buffer.c + * + * Buffer wrapper implementation for the unified session API. + * Wraps st_frame_trans as mtl_buffer_impl with public mtl_buffer_t view. + */ + +#include "mt_session.h" + +#include + +#include "../mt_log.h" +#include "../mt_mem.h" + +/************************************************************************* + * Buffer Pool Management + *************************************************************************/ + +int mtl_session_buffers_init(struct mtl_session_impl* s, uint32_t count) { + struct mtl_buffer_impl* buffers; + + if (!count) return 0; + + buffers = mt_rte_zmalloc_socket(sizeof(*buffers) * count, s->socket_id); + if (!buffers) { + err("%s(%s), failed to alloc %u buffer wrappers\n", __func__, s->name, count); + return -ENOMEM; + } + + for (uint32_t i = 0; i < count; i++) { + buffers[i].session = s; + buffers[i].idx = i; + buffers[i].frame_trans = NULL; + buffers[i].user_ctx = NULL; + buffers[i].user_owned = false; + buffers[i].pub.priv = &buffers[i]; /* Link back to impl */ + } + + s->buffers = buffers; + s->buffer_count = count; + + dbg("%s(%s), initialized %u buffer wrappers\n", __func__, s->name, count); + return 0; +} + +void mtl_session_buffers_uinit(struct mtl_session_impl* s) { + if (s->buffers) { + mt_rte_free(s->buffers); + s->buffers = NULL; + } + s->buffer_count = 0; +} + +/************************************************************************* + * Buffer Fill from st_frame_trans + *************************************************************************/ + +void mtl_buffer_fill_from_frame_trans(struct mtl_buffer_impl* b, + struct st_frame_trans* ft, + mtl_media_type_t type) { + mtl_buffer_t* pub = &b->pub; + + b->frame_trans = ft; + + /* Common fields */ + pub->data = ft->addr; + pub->iova = ft->iova; + pub->priv = b; + pub->user_data = ft->user_meta; + pub->flags = 0; + + if (ft->flags & ST_FT_FLAG_EXT) { + pub->flags |= MTL_BUF_FLAG_EXT; + } + + /* Fill type-specific fields from frame metadata */ + switch (type) { + case MTL_TYPE_VIDEO: { + struct mtl_session_impl* s = b->session; + if (s->direction == MTL_SESSION_TX) { + struct st20_tx_frame_meta* meta = &ft->tv_meta; + pub->timestamp = meta->tfmt == ST10_TIMESTAMP_FMT_TAI ? meta->timestamp : 0; + pub->rtp_timestamp = meta->rtp_timestamp; + pub->epoch = meta->epoch; + pub->status = MTL_FRAME_STATUS_COMPLETE; + } else { + struct st20_rx_frame_meta* meta = &ft->rv_meta; + pub->timestamp = meta->tfmt == ST10_TIMESTAMP_FMT_TAI ? meta->timestamp : 0; + pub->rtp_timestamp = meta->rtp_timestamp; + pub->epoch = meta->timestamp_first_pkt; /* RX meta has no epoch, use first pkt ts */ + + /* Frame status from metadata */ + if (meta->status == ST_FRAME_STATUS_COMPLETE) + pub->status = MTL_FRAME_STATUS_COMPLETE; + else + pub->status = MTL_FRAME_STATUS_INCOMPLETE; + + if (pub->status != MTL_FRAME_STATUS_COMPLETE) { + pub->flags |= MTL_BUF_FLAG_INCOMPLETE; + } + + /* RX video-specific extended fields */ + pub->video.pkts_total = meta->pkts_total; + pub->video.pkts_recv[0] = meta->pkts_recv[0]; + if (MTL_SESSION_PORT_MAX > 1) pub->video.pkts_recv[1] = meta->pkts_recv[1]; + } + break; + } + + case MTL_TYPE_AUDIO: { + struct mtl_session_impl* s = b->session; + if (s->direction == MTL_SESSION_TX) { + struct st30_tx_frame_meta* meta = &ft->ta_meta; + pub->rtp_timestamp = meta->rtp_timestamp; + pub->epoch = meta->epoch; + } else { + struct st30_rx_frame_meta* meta = &ft->ra_meta; + pub->rtp_timestamp = meta->rtp_timestamp; + pub->timestamp = + meta->tfmt == ST10_TIMESTAMP_FMT_TAI ? meta->timestamp : 0; + } + break; + } + + case MTL_TYPE_ANCILLARY: { + /* Ancillary basic fill */ + pub->timestamp = 0; + pub->rtp_timestamp = 0; + break; + } + + default: + break; + } +} + +/************************************************************************* + * Frame Trans Pool Helpers + *************************************************************************/ + +struct st_frame_trans* mtl_session_get_frame_trans(struct mtl_session_impl* s) { + struct st_frame_trans* frames = NULL; + uint32_t count = 0; + + /* Get frame array from inner session */ + switch (s->type) { + case MTL_TYPE_VIDEO: + if (s->direction == MTL_SESSION_TX && s->inner.video_tx) { + frames = s->inner.video_tx->st20_frames; + count = s->inner.video_tx->st20_frames_cnt; + } else if (s->direction == MTL_SESSION_RX && s->inner.video_rx) { + frames = s->inner.video_rx->st20_frames; + count = s->inner.video_rx->st20_frames_cnt; + } + break; + default: + err("%s(%s), unsupported type %d\n", __func__, s->name, s->type); + return NULL; + } + + if (!frames || !count) { + err("%s(%s), no frames available\n", __func__, s->name); + return NULL; + } + + /* Find a free frame (refcnt == 0) */ + for (uint32_t i = 0; i < count; i++) { + if (rte_atomic32_read(&frames[i].refcnt) == 0) { + rte_atomic32_inc(&frames[i].refcnt); + return &frames[i]; + } + } + + return NULL; /* No free frames */ +} + +void mtl_session_put_frame_trans(struct st_frame_trans* ft) { + if (ft) { + rte_atomic32_dec(&ft->refcnt); + } +} + +/************************************************************************* + * User-Owned Buffer Management + *************************************************************************/ + +#define MTL_USER_BUF_RING_SIZE 32 /* Must be power of 2 */ + +int mtl_session_user_buf_init(struct mtl_session_impl* s, uint16_t frame_cnt) { + char ring_name[RTE_RING_NAMESIZE]; + + snprintf(ring_name, sizeof(ring_name), "mtl_ub_%p", s); + + s->user_buf_ring = + rte_ring_create(ring_name, MTL_USER_BUF_RING_SIZE, s->socket_id, 0); + if (!s->user_buf_ring) { + err("%s(%s), failed to create user buffer ring\n", __func__, s->name); + return -ENOMEM; + } + + s->user_buf_ctx = + mt_rte_zmalloc_socket(sizeof(void*) * frame_cnt, s->socket_id); + if (!s->user_buf_ctx) { + err("%s(%s), failed to alloc user_buf_ctx array\n", __func__, s->name); + rte_ring_free(s->user_buf_ring); + s->user_buf_ring = NULL; + return -ENOMEM; + } + s->user_buf_ctx_cnt = frame_cnt; + + dbg("%s(%s), initialized user buffer ring, frame_cnt %u\n", __func__, s->name, + frame_cnt); + return 0; +} + +void mtl_session_user_buf_uinit(struct mtl_session_impl* s) { + /* Drain and free any remaining entries in the ring */ + if (s->user_buf_ring) { + void* obj = NULL; + while (rte_ring_dequeue(s->user_buf_ring, &obj) == 0 && obj) { + mt_rte_free(obj); + obj = NULL; + } + rte_ring_free(s->user_buf_ring); + s->user_buf_ring = NULL; + } + + if (s->user_buf_ctx) { + mt_rte_free(s->user_buf_ctx); + s->user_buf_ctx = NULL; + } + s->user_buf_ctx_cnt = 0; + + /* Free DMA registrations */ + for (uint8_t i = 0; i < s->dma_registration_cnt; i++) { + if (s->dma_registrations[i]) { + mt_rte_free(s->dma_registrations[i]); + s->dma_registrations[i] = NULL; + } + } + s->dma_registration_cnt = 0; +} + +int mtl_session_user_buf_enqueue(struct mtl_session_impl* s, void* data, + mtl_iova_t iova, size_t size, void* user_ctx) { + if (!s->user_buf_ring) return -EINVAL; + + struct mtl_user_buffer_entry* entry = + mt_rte_zmalloc_socket(sizeof(*entry), s->socket_id); + if (!entry) { + err("%s(%s), failed to alloc user buffer entry\n", __func__, s->name); + return -ENOMEM; + } + + entry->data = data; + entry->iova = iova; + entry->size = size; + entry->user_ctx = user_ctx; + + if (rte_ring_enqueue(s->user_buf_ring, entry) != 0) { + mt_rte_free(entry); + dbg("%s(%s), user buffer ring full\n", __func__, s->name); + return -ENOSPC; + } + + return 0; +} + +int mtl_session_user_buf_dequeue(struct mtl_session_impl* s, + struct mtl_user_buffer_entry* entry) { + if (!s->user_buf_ring) return -EINVAL; + + void* obj = NULL; + if (rte_ring_dequeue(s->user_buf_ring, &obj) != 0 || !obj) { + return -EAGAIN; + } + + struct mtl_user_buffer_entry* queued = (struct mtl_user_buffer_entry*)obj; + *entry = *queued; + mt_rte_free(queued); + return 0; +} + +mtl_iova_t mtl_session_lookup_iova(struct mtl_session_impl* s, void* addr, + size_t size) { + /* Search registered DMA memory regions */ + for (uint8_t i = 0; i < s->dma_registration_cnt; i++) { + struct mtl_dma_mem_impl* reg = s->dma_registrations[i]; + if (!reg) continue; + + uintptr_t region_start = (uintptr_t)reg->addr; + uintptr_t region_end = region_start + reg->size; + uintptr_t buf_start = (uintptr_t)addr; + uintptr_t buf_end = buf_start + size; + + if (buf_start >= region_start && buf_end <= region_end) { + /* Buffer is within this registered region */ + size_t offset = buf_start - region_start; + return reg->iova + offset; + } + } + + /* Fallback: try direct IOVA lookup via DPDK */ + mtl_iova_t iova = rte_mem_virt2iova(addr); + if (iova != RTE_BAD_IOVA && iova != 0) { + return iova; + } + + /* Try hugepage lookup if parent available */ + if (s->parent) { + iova = mtl_hp_virt2iova(s->parent, addr); + if (iova != MTL_BAD_IOVA && iova != 0) { + return iova; + } + } + + err("%s(%s), failed to find IOVA for addr %p\n", __func__, s->name, addr); + return MTL_BAD_IOVA; +} diff --git a/lib/src/new_api/mt_session_event.c b/lib/src/new_api/mt_session_event.c new file mode 100644 index 000000000..d2a66279c --- /dev/null +++ b/lib/src/new_api/mt_session_event.c @@ -0,0 +1,110 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2024 Intel Corporation + */ + +/** + * @file mt_session_event.c + * + * Event queue implementation for the unified session API. + * Uses rte_ring for lock-free event queuing from callbacks to poll(). + */ + +#include "mt_session.h" + +#include +#include +#include + +#include "../mt_log.h" +#include "../mt_mem.h" + +#define MTL_EVENT_RING_SIZE 64 /* Must be power of 2 */ + +/************************************************************************* + * Event Queue Lifecycle + *************************************************************************/ + +int mtl_session_events_init(struct mtl_session_impl* s) { + char ring_name[RTE_RING_NAMESIZE]; + + snprintf(ring_name, sizeof(ring_name), "mtl_ev_%p", s); + + s->event_ring = + rte_ring_create(ring_name, MTL_EVENT_RING_SIZE, s->socket_id, 0); + if (!s->event_ring) { + err("%s(%s), failed to create event ring\n", __func__, s->name); + return -ENOMEM; + } + + /* Create eventfd for epoll/select integration */ + s->event_fd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC); + if (s->event_fd < 0) { + /* Not fatal - event_fd is optional */ + dbg("%s(%s), eventfd creation failed (optional)\n", __func__, s->name); + s->event_fd = -1; + } + + dbg("%s(%s), event queue initialized\n", __func__, s->name); + return 0; +} + +void mtl_session_events_uinit(struct mtl_session_impl* s) { + /* Drain and free any remaining events */ + if (s->event_ring) { + void* obj = NULL; + while (rte_ring_dequeue(s->event_ring, &obj) == 0 && obj) { + mt_rte_free(obj); + obj = NULL; + } + rte_ring_free(s->event_ring); + s->event_ring = NULL; + } + + if (s->event_fd >= 0) { + close(s->event_fd); + s->event_fd = -1; + } +} + +/************************************************************************* + * Event Posting (called from callbacks / library threads) + *************************************************************************/ + +int mtl_session_event_post(struct mtl_session_impl* s, const mtl_event_t* event) { + mtl_event_t* ev_copy; + + if (!s->event_ring) { + return -EINVAL; + } + + /* Allocate a copy of the event for the ring */ + ev_copy = mt_rte_zmalloc_socket(sizeof(*ev_copy), s->socket_id); + if (!ev_copy) { + dbg("%s(%s), failed to alloc event copy\n", __func__, s->name); + return -ENOMEM; + } + + *ev_copy = *event; + + if (rte_ring_enqueue(s->event_ring, ev_copy) != 0) { + /* Ring full - drop event */ + mt_rte_free(ev_copy); + dbg("%s(%s), event ring full, dropping event type %d\n", __func__, s->name, + event->type); + return -ENOSPC; + } + + /* Signal eventfd if available */ + if (s->event_fd >= 0) { + uint64_t val = 1; + ssize_t n = write(s->event_fd, &val, sizeof(val)); + (void)n; /* Ignore write failures on non-blocking fd */ + } + + /* Call optional callback */ + if (event->type == MTL_EVENT_BUFFER_READY && s->notify_buffer_ready) { + s->notify_buffer_ready(s->notify_priv); + } + + return 0; +} diff --git a/lib/src/new_api/mt_session_video_common.c b/lib/src/new_api/mt_session_video_common.c new file mode 100644 index 000000000..7f230744c --- /dev/null +++ b/lib/src/new_api/mt_session_video_common.c @@ -0,0 +1,242 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2024 Intel Corporation + */ + +/** + * @file mt_session_video_common.c + * + * Shared implementation for video TX and RX sessions. + * Contains format conversion, event polling, stats, and deadline helpers + * that are identical or near-identical between TX and RX. + */ + +#include "mt_session_video_common.h" + +#include +#include +#include + +#include "../mt_log.h" +#include "../mt_mem.h" + +/************************************************************************* + * Format Conversion Context + *************************************************************************/ + +int video_convert_ctx_init(struct video_convert_ctx* cvt, + const mtl_video_config_t* config, bool is_tx) { + cvt->width = config->width; + cvt->height = config->height; + cvt->interlaced = config->interlaced; + cvt->frame_fmt = config->frame_fmt; + cvt->transport_fmt = config->transport_fmt; + cvt->app_bufs = NULL; + cvt->app_bufs_cnt = 0; + + /* Check if app format matches transport format (no conversion needed) */ + cvt->derive = st_frame_fmt_equal_transport(config->frame_fmt, config->transport_fmt); + + if (cvt->derive) return 0; + + /* Conversion needed: validate transport format */ + enum st_frame_fmt transport_frame_fmt = + st_frame_fmt_from_transport(config->transport_fmt); + if (transport_frame_fmt == ST_FRAME_FMT_MAX) { + err("%s(%s), unsupported transport_fmt %d\n", __func__, config->base.name, + config->transport_fmt); + return -EINVAL; + } + + /* Look up converter: direction depends on TX vs RX */ + enum st_frame_fmt src_fmt = is_tx ? config->frame_fmt : transport_frame_fmt; + enum st_frame_fmt dst_fmt = is_tx ? transport_frame_fmt : config->frame_fmt; + int ret = st_frame_get_converter(src_fmt, dst_fmt, &cvt->converter); + if (ret < 0) { + err("%s(%s), no converter from %s to %s\n", __func__, config->base.name, + st_frame_fmt_name(src_fmt), st_frame_fmt_name(dst_fmt)); + return ret; + } + + /* Calculate app-side frame size */ + cvt->app_frame_size = + st_frame_size(config->frame_fmt, config->width, config->height, + config->interlaced); + if (!cvt->app_frame_size) { + err("%s(%s), failed to get frame size for fmt %s\n", __func__, config->base.name, + st_frame_fmt_name(config->frame_fmt)); + return -EINVAL; + } + + info("%s(%s), conversion enabled: %s %s %s, app_frame_size %zu\n", __func__, + config->base.name, st_frame_fmt_name(src_fmt), is_tx ? "->" : "<-", + st_frame_fmt_name(dst_fmt), cvt->app_frame_size); + + return 0; +} + +int video_convert_bufs_alloc(struct video_convert_ctx* cvt, uint16_t fb_cnt, + int socket_id) { + if (cvt->derive || fb_cnt == 0) return 0; + + cvt->app_bufs = mt_rte_zmalloc_socket(sizeof(void*) * fb_cnt, socket_id); + if (!cvt->app_bufs) { + err("%s, failed to alloc app_bufs array (%u entries)\n", __func__, fb_cnt); + return -ENOMEM; + } + cvt->app_bufs_cnt = fb_cnt; + + for (uint16_t i = 0; i < fb_cnt; i++) { + cvt->app_bufs[i] = mt_rte_zmalloc_socket(cvt->app_frame_size, socket_id); + if (!cvt->app_bufs[i]) { + err("%s, failed to alloc app_buf[%u], size %zu\n", __func__, i, + cvt->app_frame_size); + /* Cleanup already-allocated buffers */ + for (uint16_t j = 0; j < i; j++) { + mt_rte_free(cvt->app_bufs[j]); + cvt->app_bufs[j] = NULL; + } + mt_rte_free(cvt->app_bufs); + cvt->app_bufs = NULL; + cvt->app_bufs_cnt = 0; + return -ENOMEM; + } + } + + info("%s, allocated %u conversion buffers, %zu bytes each\n", __func__, fb_cnt, + cvt->app_frame_size); + return 0; +} + +void video_convert_bufs_free(struct video_convert_ctx* cvt) { + if (!cvt->app_bufs) return; + + for (uint16_t i = 0; i < cvt->app_bufs_cnt; i++) { + if (cvt->app_bufs[i]) { + mt_rte_free(cvt->app_bufs[i]); + cvt->app_bufs[i] = NULL; + } + } + mt_rte_free(cvt->app_bufs); + cvt->app_bufs = NULL; + cvt->app_bufs_cnt = 0; +} + +/************************************************************************* + * Frame Conversion + *************************************************************************/ + +int video_convert_frame(struct video_convert_ctx* cvt, void* src_data, + mtl_iova_t src_iova, size_t src_size, void* dst_data, + mtl_iova_t dst_iova, size_t dst_size, bool is_tx) { + struct st_frame src_frame; + struct st_frame dst_frame; + + memset(&src_frame, 0, sizeof(src_frame)); + memset(&dst_frame, 0, sizeof(dst_frame)); + + if (is_tx) { + /* TX: app format → transport format */ + src_frame.fmt = cvt->frame_fmt; + dst_frame.fmt = st_frame_fmt_from_transport(cvt->transport_fmt); + } else { + /* RX: transport format → app format */ + src_frame.fmt = st_frame_fmt_from_transport(cvt->transport_fmt); + dst_frame.fmt = cvt->frame_fmt; + } + + /* Common fields for both frames */ + src_frame.width = cvt->width; + src_frame.height = cvt->height; + src_frame.interlaced = cvt->interlaced; + src_frame.buffer_size = src_size; + src_frame.data_size = src_size; + st_frame_init_plane_single_src(&src_frame, src_data, src_iova); + + dst_frame.width = cvt->width; + dst_frame.height = cvt->height; + dst_frame.interlaced = cvt->interlaced; + dst_frame.buffer_size = dst_size; + dst_frame.data_size = dst_size; + st_frame_init_plane_single_src(&dst_frame, dst_data, dst_iova); + + int ret = cvt->converter.convert_func(&src_frame, &dst_frame); + if (ret < 0) { + err("%s, conversion failed %d, %s -> %s\n", __func__, ret, + st_frame_fmt_name(src_frame.fmt), st_frame_fmt_name(dst_frame.fmt)); + } + return ret; +} + +/************************************************************************* + * Shared Event Poll + *************************************************************************/ + +int video_session_event_poll(struct mtl_session_impl* s, mtl_event_t* event, + uint32_t timeout_ms) { + if (mtl_session_check_stopped(s)) { + return -EAGAIN; + } + + /* Non-blocking dequeue attempt */ + if (s->event_ring) { + void* obj = NULL; + if (rte_ring_dequeue(s->event_ring, &obj) == 0 && obj) { + *event = *(mtl_event_t*)obj; + mt_rte_free(obj); + return 0; + } + } + + if (timeout_ms == 0) { + return -ETIMEDOUT; + } + + /* Poll with timeout */ + uint64_t deadline_ns = video_calc_deadline_ns(timeout_ms); + + while (!mtl_session_check_stopped(s)) { + void* obj = NULL; + if (s->event_ring && rte_ring_dequeue(s->event_ring, &obj) == 0 && obj) { + *event = *(mtl_event_t*)obj; + mt_rte_free(obj); + return 0; + } + + usleep(100); + + if (video_deadline_reached(deadline_ns)) return -ETIMEDOUT; + } + + return -EAGAIN; +} + +/************************************************************************* + * Shared Stats Reset + *************************************************************************/ + +int video_session_stats_reset(struct mtl_session_impl* s) { + __atomic_store_n(&s->stats.buffers_processed, 0, __ATOMIC_RELAXED); + __atomic_store_n(&s->stats.bytes_processed, 0, __ATOMIC_RELAXED); + __atomic_store_n(&s->stats.buffers_dropped, 0, __ATOMIC_RELAXED); + __atomic_store_n(&s->stats.epochs_missed, 0, __ATOMIC_RELAXED); + return 0; +} + +/************************************************************************* + * Shared Vsync Callback + *************************************************************************/ + +int video_session_notify_event(void* priv, enum st_event ev, void* args) { + struct mtl_session_impl* s = priv; + + if (ev == ST_EVENT_VSYNC && args) { + struct st10_vsync_meta* vsync = args; + mtl_event_t event = {0}; + event.type = MTL_EVENT_VSYNC; + event.vsync.epoch = vsync->epoch; + event.vsync.ptp_time = vsync->ptp; + mtl_session_event_post(s, &event); + } + + return 0; +} diff --git a/lib/src/new_api/mt_session_video_common.h b/lib/src/new_api/mt_session_video_common.h new file mode 100644 index 000000000..ae97dae77 --- /dev/null +++ b/lib/src/new_api/mt_session_video_common.h @@ -0,0 +1,173 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2024 Intel Corporation + */ + +/** + * @file mt_session_video_common.h + * + * Shared helpers for video TX and RX session implementations. + * Reduces code duplication between mt_session_video_tx.c and + * mt_session_video_rx.c. + */ + +#ifndef _MT_SESSION_VIDEO_COMMON_H_ +#define _MT_SESSION_VIDEO_COMMON_H_ + +#include "mt_session.h" + +#include "../st2110/st_convert.h" +#include "../st2110/st_fmt.h" + +#if defined(__cplusplus) +extern "C" { +#endif + +/************************************************************************* + * Shared Format Conversion Context + * + * Common fields used by both video_tx_ctx and video_rx_ctx for + * format conversion between app pixel format and transport format. + *************************************************************************/ + +struct video_convert_ctx { + bool derive; /**< true if no conversion needed */ + enum st_frame_fmt frame_fmt; /**< app pixel format */ + enum st20_fmt transport_fmt; /**< wire format */ + struct st_frame_converter converter; /**< cached converter function */ + size_t app_frame_size; /**< frame size in app pixel format */ + size_t transport_frame_size; /**< frame size in transport format */ + uint32_t width; + uint32_t height; + bool interlaced; + + /** + * Per-framebuffer app-format buffers. + * - TX: source buffers (app writes, then converted to transport on put) + * - RX: destination buffers (transport converted to app on get) + * Only allocated when !derive (conversion needed). + */ + void** app_bufs; + uint16_t app_bufs_cnt; +}; + +/************************************************************************* + * Initialization / Teardown + *************************************************************************/ + +/** + * Initialize format conversion context. + * Determines if conversion is needed and looks up the converter. + * + * @param cvt Conversion context to initialize. + * @param config Video configuration. + * @param is_tx true for TX (app→transport), false for RX (transport→app). + * @return 0 on success, negative errno on failure. + */ +int video_convert_ctx_init(struct video_convert_ctx* cvt, + const mtl_video_config_t* config, bool is_tx); + +/** + * Allocate per-framebuffer app-format conversion buffers. + * + * @param cvt Initialized conversion context. + * @param fb_cnt Number of framebuffers. + * @param socket_id NUMA socket for allocation. + * @return 0 on success, negative errno on failure. + */ +int video_convert_bufs_alloc(struct video_convert_ctx* cvt, uint16_t fb_cnt, + int socket_id); + +/** + * Free per-framebuffer app-format conversion buffers. + */ +void video_convert_bufs_free(struct video_convert_ctx* cvt); + +/************************************************************************* + * Frame Conversion + *************************************************************************/ + +/** + * Perform frame format conversion. + * Builds st_frame descriptors and calls the cached converter. + * + * @param cvt Conversion context. + * @param src_data Source buffer data pointer. + * @param src_iova Source buffer IOVA (0 if not applicable). + * @param src_size Source buffer size. + * @param dst_data Destination buffer data pointer. + * @param dst_iova Destination buffer IOVA (0 if not applicable). + * @param dst_size Destination buffer size. + * @param is_tx true for TX (app→transport), false for RX (transport→app). + * @return 0 on success, negative errno on failure. + */ +int video_convert_frame(struct video_convert_ctx* cvt, void* src_data, + mtl_iova_t src_iova, size_t src_size, void* dst_data, + mtl_iova_t dst_iova, size_t dst_size, bool is_tx); + +/************************************************************************* + * Shared Event Poll (identical for TX and RX) + *************************************************************************/ + +/** + * Generic event poll implementation shared by video TX and RX. + * Dequeues events from the session's event ring with optional timeout. + */ +int video_session_event_poll(struct mtl_session_impl* s, mtl_event_t* event, + uint32_t timeout_ms); + +/************************************************************************* + * Shared Stats (identical for TX and RX) + *************************************************************************/ + +/** + * Reset session statistics (shared implementation). + */ +int video_session_stats_reset(struct mtl_session_impl* s); + +/************************************************************************* + * Shared Vsync Callback + *************************************************************************/ + +/** + * Common notify_event callback for vsync events. + * Used identically by both TX and RX. + * + * @param priv Pointer to mtl_session_impl (set as session wrapper's priv). + * @param ev Event type from low-level library. + * @param args Event arguments. + */ +int video_session_notify_event(void* priv, enum st_event ev, void* args); + +/************************************************************************* + * Deadline Helpers + *************************************************************************/ + +/** + * Calculate an absolute deadline in nanoseconds from a relative timeout. + * Returns 0 if timeout_ms == 0 (non-blocking). + */ +static inline uint64_t video_calc_deadline_ns(uint32_t timeout_ms) { + if (timeout_ms == 0) return 0; + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (uint64_t)ts.tv_sec * 1000000000ULL + ts.tv_nsec + + (uint64_t)timeout_ms * 1000000ULL; +} + +/** + * Check if the deadline has been reached. + * Returns true if the current time is past the deadline. + */ +static inline bool video_deadline_reached(uint64_t deadline_ns) { + if (deadline_ns == 0) return true; /* Non-blocking mode */ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + uint64_t now = (uint64_t)ts.tv_sec * 1000000000ULL + ts.tv_nsec; + return now >= deadline_ns; +} + +#if defined(__cplusplus) +} +#endif + +#endif /* _MT_SESSION_VIDEO_COMMON_H_ */ diff --git a/lib/src/new_api/mt_session_video_rx.c b/lib/src/new_api/mt_session_video_rx.c new file mode 100644 index 000000000..68fe07587 --- /dev/null +++ b/lib/src/new_api/mt_session_video_rx.c @@ -0,0 +1,918 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2024 Intel Corporation + */ + +/** + * @file mt_session_video_rx.c + * + * Video RX session implementation for the unified session API. + * Wraps st20_rx_create/free and translates between mtl_video_config_t + * and st20_rx_ops. + */ + +#include "mt_session_video_common.h" + +#include "../mt_log.h" +#include "../mt_mem.h" + +/************************************************************************* + * Callback Context + *************************************************************************/ + +struct video_rx_ctx { + struct mtl_session_impl* session; + st20_rx_handle handle; /**< low-level RX handle */ + struct video_convert_ctx convert; /**< shared format conversion context */ + + /** Lock-free ring to queue received frames for buffer_get() */ + struct rte_ring* ready_ring; + + /* User ext_frame callback (if any) */ + int (*user_query_ext_frame)(void* priv, struct st_ext_frame* ext_frame, + struct mtl_buffer* frame_meta); + void* user_priv; +}; + +/************************************************************************* + * Internal Helpers + *************************************************************************/ + +/** Get the video_rx_ctx from session (caller must ensure session is valid). */ +static inline struct video_rx_ctx* rx_ctx_from_session(struct mtl_session_impl* s) { + return s->inner.video_rx->ops.priv; +} + +/************************************************************************* + * ST20 RX Callbacks → Unified Event Queue / Ready Ring + *************************************************************************/ + +/** + * Save received frame metadata into st_frame_trans for later retrieval. + * The meta pointer from the callback is transient, so we must copy now. + */ +static void rx_save_frame_metadata(struct st_rx_video_session_impl* rx_impl, + void* frame, struct st20_rx_frame_meta* meta) { + if (!meta || !rx_impl) return; + + for (uint16_t i = 0; i < rx_impl->st20_frames_cnt; i++) { + if (rx_impl->st20_frames[i].addr == frame) { + rx_impl->st20_frames[i].rv_meta = *meta; + return; + } + } +} + +/** + * Enqueue a received frame pointer onto the ready ring. + * Returns 0 on success, or drops the frame and updates stats on failure. + */ +static int rx_enqueue_frame(struct video_rx_ctx* ctx, void* frame) { + if (!ctx->ready_ring) return -EINVAL; + + if (rte_ring_enqueue(ctx->ready_ring, frame) != 0) { + struct mtl_session_impl* s = ctx->session; + dbg("%s(%s), ready ring full, dropping frame\n", __func__, s->name); + st20_rx_put_framebuff(ctx->handle, frame); + + __atomic_add_fetch(&s->stats.buffers_dropped, 1, __ATOMIC_RELAXED); + return -ENOSPC; + } + return 0; +} + +/** + * Post a buffer-ready event with optional timestamp from RX metadata. + */ +static void rx_post_buffer_ready_event(struct mtl_session_impl* s, + struct st20_rx_frame_meta* meta, + void* user_ctx) { + mtl_event_t event = {0}; + event.type = MTL_EVENT_BUFFER_READY; + event.ctx = user_ctx; + if (meta) { + event.timestamp = meta->tfmt == ST10_TIMESTAMP_FMT_TAI ? meta->timestamp : 0; + } + mtl_session_event_post(s, &event); +} + +/** + * notify_frame_ready callback - library delivered a received frame. + * Thread context: library datapath thread. Must be non-blocking. + * + * For user-owned mode: saves the user_ctx (from ext_frame opaque) per frame_idx, + * so that buffer_get or event_poll can return it to the app. + */ +static int video_rx_notify_frame_ready(void* priv, void* frame, + struct st20_rx_frame_meta* meta) { + struct video_rx_ctx* ctx = priv; + struct mtl_session_impl* s = ctx->session; + + rx_save_frame_metadata(s->inner.video_rx, frame, meta); + + /* + * User-owned mode via buffer_post (no explicit query_ext_frame): + * The library receives into its own internal framebuffers. + * Here we convert/copy into the user's buffer, return the library frame, + * and post an event carrying the user context. + */ + if (s->ownership == MTL_BUFFER_USER_OWNED && !ctx->user_query_ext_frame) { + struct mtl_user_buffer_entry entry; + if (mtl_session_user_buf_dequeue(s, &entry) != 0) { + dbg("%s(%s), no user buffer for received frame, dropping\n", __func__, s->name); + st20_rx_put_framebuff(ctx->handle, frame); + __atomic_add_fetch(&s->stats.buffers_dropped, 1, __ATOMIC_RELAXED); + return 0; + } + + /* Convert transport → app format, or copy if derive */ + struct video_convert_ctx* cvt = &ctx->convert; + if (cvt->derive) { + size_t copy_len = cvt->transport_frame_size; + if (copy_len > entry.size) copy_len = entry.size; + mtl_memcpy(entry.data, frame, copy_len); + } else { + int ret = video_convert_frame(cvt, frame, 0, cvt->transport_frame_size, + entry.data, entry.iova, cvt->app_frame_size, + false /* RX */); + if (ret < 0) { + err("%s(%s), conversion failed: %d\n", __func__, s->name, ret); + mtl_session_user_buf_enqueue(s, entry.data, entry.iova, entry.size, + entry.user_ctx); + st20_rx_put_framebuff(ctx->handle, frame); + return 0; + } + } + + /* Return library frame immediately */ + st20_rx_put_framebuff(ctx->handle, frame); + + /* Post event with user context */ + rx_post_buffer_ready_event(s, meta, entry.user_ctx); + __atomic_add_fetch(&s->stats.buffers_processed, 1, __ATOMIC_RELAXED); + return 0; + } + + /* User-owned mode with explicit query_ext_frame: save opaque user_ctx per frame */ + if (s->ownership == MTL_BUFFER_USER_OWNED && s->user_buf_ctx) { + struct st_rx_video_session_impl* rx_impl = s->inner.video_rx; + for (uint16_t i = 0; i < rx_impl->st20_frames_cnt; i++) { + if (rx_impl->st20_frames[i].addr == frame) { + if (i < s->user_buf_ctx_cnt) { + s->user_buf_ctx[i] = rx_impl->st20_frames[i].user_meta; + } + break; + } + } + } + + if (rx_enqueue_frame(ctx, frame) == 0) { + rx_post_buffer_ready_event(s, meta, NULL); + } + + return 0; +} + +/** + * notify_detected callback - video format auto-detected. + */ +static int video_rx_notify_detected(void* priv, const struct st20_detect_meta* meta, + struct st20_detect_reply* reply) { + struct video_rx_ctx* ctx = priv; + struct mtl_session_impl* s = ctx->session; + + if (!meta) return -EINVAL; + + mtl_event_t event = {0}; + event.type = MTL_EVENT_FORMAT_DETECTED; + event.format_detected.width = meta->width; + event.format_detected.height = meta->height; + event.format_detected.fps = meta->fps; + event.format_detected.packing = meta->packing; + event.format_detected.interlaced = meta->interlaced; + mtl_session_event_post(s, &event); + + (void)reply; /* Accept detected format with default reply */ + return 0; +} + +/** + * Wrapper for query_ext_frame: translates st20_ext_frame to st_ext_frame. + * + * In user-owned mode without an explicit query_ext_frame callback from the app, + * this implementation dequeues from the user_buf_ring (populated by buffer_post). + */ +static int video_rx_query_ext_frame_wrapper(void* priv, + struct st20_ext_frame* st20_ext, + struct st20_rx_frame_meta* meta) { + struct video_rx_ctx* ctx = priv; + struct mtl_session_impl* s = ctx->session; + + /* If app provided its own query_ext_frame callback, use it */ + if (ctx->user_query_ext_frame) { + struct st_ext_frame ext = {0}; + mtl_buffer_t buf = {0}; + buf.video.width = meta->width; + buf.video.height = meta->height; + buf.size = meta->frame_total_size; + + int ret = ctx->user_query_ext_frame(ctx->user_priv, &ext, &buf); + if (ret < 0) return ret; + + st20_ext->buf_addr = ext.addr[0]; + st20_ext->buf_iova = ext.iova[0]; + st20_ext->buf_len = ext.size; + st20_ext->opaque = ext.opaque; + return 0; + } + + /* User-owned mode via buffer_post(): dequeue from ring */ + struct mtl_user_buffer_entry entry; + int ret = mtl_session_user_buf_dequeue(s, &entry); + if (ret < 0) { + dbg("%s(%s), no user buffer available for ext_frame\n", __func__, s->name); + return -EAGAIN; + } + + st20_ext->buf_addr = entry.data; + st20_ext->buf_iova = entry.iova; + st20_ext->buf_len = entry.size; + st20_ext->opaque = entry.user_ctx; + + return 0; +} + +/************************************************************************* + * Buffer Get/Put Helpers + *************************************************************************/ + +/** + * Find the st_frame_trans matching a frame address. + * Returns the frame_trans and sets *frame_idx, or NULL if not found. + */ +static struct st_frame_trans* rx_find_frame_trans( + struct st_rx_video_session_impl* rx_impl, void* frame, uint16_t* frame_idx) { + for (uint16_t i = 0; i < rx_impl->st20_frames_cnt; i++) { + if (rx_impl->st20_frames[i].addr == frame) { + *frame_idx = i; + return &rx_impl->st20_frames[i]; + } + } + return NULL; +} + +/** + * Fill buffer status and timestamp fields from RX frame metadata. + */ +static void rx_fill_buffer_status(mtl_buffer_t* pub, struct st20_rx_frame_meta* meta) { + pub->rtp_timestamp = meta->rtp_timestamp; + pub->tfmt = meta->tfmt; + pub->timestamp = meta->timestamp; + + if (meta->status == ST_FRAME_STATUS_COMPLETE || + meta->status == ST_FRAME_STATUS_RECONSTRUCTED) { + pub->status = MTL_FRAME_STATUS_COMPLETE; + } else { + pub->status = MTL_FRAME_STATUS_INCOMPLETE; + pub->flags |= MTL_BUF_FLAG_INCOMPLETE; + } +} + +/** + * Fill video-specific fields in the buffer from RX metadata. + */ +static void rx_fill_buffer_video_fields(mtl_buffer_t* pub, + struct st20_rx_frame_meta* meta, + struct video_rx_ctx* ctx) { + pub->video.width = meta->width; + pub->video.height = meta->height; + pub->video.pkts_total = meta->pkts_total; + pub->video.pkts_recv[0] = meta->pkts_recv[0]; + if (MTL_SESSION_PORT_MAX > 1) + pub->video.pkts_recv[1] = meta->pkts_recv[1]; + pub->video.interlaced = ctx->convert.interlaced; + pub->video.second_field = meta->second_field; +} + +/** + * Fill user metadata pass-through fields from frame_trans. + */ +static void rx_fill_user_metadata(mtl_buffer_t* pub, struct st_frame_trans* ft) { + if (ft->user_meta && ft->user_meta_data_size > 0) { + pub->user_meta = ft->user_meta; + pub->user_meta_size = ft->user_meta_data_size; + } +} + +/** + * Perform format conversion for a received frame (transport → app format). + * On success, sets pub->data/size to the converted buffer. + * On failure, returns the transport frame to the library. + */ +static int rx_convert_and_fill_buffer(struct video_rx_ctx* ctx, + struct st_frame_trans* ft, + uint16_t frame_idx, + mtl_buffer_t* pub) { + struct video_convert_ctx* cvt = &ctx->convert; + + if (cvt->derive || !cvt->app_bufs || frame_idx >= cvt->app_bufs_cnt || + !cvt->app_bufs[frame_idx]) { + /* Derive mode or missing buffer - give transport buffer directly */ + pub->data = ft->addr; + pub->iova = ft->iova; + pub->size = cvt->transport_frame_size; + pub->data_size = ft->rv_meta.frame_recv_size > 0 ? ft->rv_meta.frame_recv_size + : cvt->transport_frame_size; + pub->video.fmt = st_frame_fmt_from_transport(cvt->transport_fmt); + return 0; + } + + /* Convert transport frame → app format */ + int ret = video_convert_frame(cvt, ft->addr, ft->iova, cvt->transport_frame_size, + cvt->app_bufs[frame_idx], 0, cvt->app_frame_size, + false /* is_tx=false, RX direction */); + if (ret < 0) { + st20_rx_put_framebuff(ctx->handle, ft->addr); + return ret; + } + + pub->data = cvt->app_bufs[frame_idx]; + pub->iova = 0; + pub->size = cvt->app_frame_size; + pub->data_size = cvt->app_frame_size; + pub->video.fmt = cvt->frame_fmt; + return 0; +} + +/** + * Try to dequeue one received frame and populate the buffer. + * Returns 0 on success with *buf set, or negative errno. + * + * Thread safety: lock-free. The ready_ring supports multi-consumer dequeue. + * Each dequeued frame has a unique frame_idx mapping to a unique buffer wrapper. + */ +static int rx_try_dequeue_frame(struct mtl_session_impl* s, mtl_buffer_t** buf) { + struct st_rx_video_session_impl* rx_impl = s->inner.video_rx; + struct video_rx_ctx* ctx = rx_ctx_from_session(s); + void* frame = NULL; + + if (!ctx->ready_ring || rte_ring_dequeue(ctx->ready_ring, &frame) != 0 || !frame) { + return -EAGAIN; + } + + /* Find the frame_trans for this address */ + uint16_t frame_idx = 0; + struct st_frame_trans* ft = rx_find_frame_trans(rx_impl, frame, &frame_idx); + if (!ft) { + err("%s(%s), frame addr %p not found\n", __func__, s->name, frame); + return -EIO; + } + + /* Fill buffer wrapper (under buffer_lock) */ + struct mtl_buffer_impl* b = &s->buffers[frame_idx % s->buffer_count]; + b->frame_trans = ft; + b->idx = frame_idx; + + mtl_buffer_t* pub = &b->pub; + memset(pub, 0, sizeof(*pub)); + pub->priv = b; + pub->flags = 0; + + struct st20_rx_frame_meta* meta = &ft->rv_meta; + rx_fill_buffer_status(pub, meta); + + /* Convert or pass through the frame data */ + int ret = rx_convert_and_fill_buffer(ctx, ft, frame_idx, pub); + if (ret < 0) { + b->frame_trans = NULL; + return ret; + } + + rx_fill_buffer_video_fields(pub, meta, ctx); + rx_fill_user_metadata(pub, ft); + + /* For user-owned mode: attach user_ctx to buffer */ + if (s->ownership == MTL_BUFFER_USER_OWNED && s->user_buf_ctx && + frame_idx < s->user_buf_ctx_cnt) { + pub->user_data = s->user_buf_ctx[frame_idx]; + b->user_ctx = s->user_buf_ctx[frame_idx]; + b->user_owned = true; + s->user_buf_ctx[frame_idx] = NULL; + } + + /* Update stats (lock-free, relaxed ordering for counters) */ + __atomic_add_fetch(&s->stats.buffers_processed, 1, __ATOMIC_RELAXED); + __atomic_add_fetch(&s->stats.bytes_processed, pub->data_size, __ATOMIC_RELAXED); + + *buf = pub; + return 0; +} + +/************************************************************************* + * VTable Implementation + *************************************************************************/ + +static int video_rx_start(struct mtl_session_impl* s) { + (void)s; + return 0; +} + +static int video_rx_stop(struct mtl_session_impl* s) { + (void)s; + return 0; +} + +static void video_rx_destroy(struct mtl_session_impl* s) { + struct video_rx_ctx* ctx = NULL; + + if (s->inner.video_rx) { + ctx = s->inner.video_rx->ops.priv; + } + + /* Drain ready ring and return frames to library before freeing */ + if (ctx && ctx->ready_ring && ctx->handle) { + void* frame = NULL; + while (rte_ring_dequeue(ctx->ready_ring, &frame) == 0 && frame) { + st20_rx_put_framebuff(ctx->handle, frame); + } + } + + /* Free the low-level session */ + if (ctx && ctx->handle) { + st20_rx_free(ctx->handle); + ctx->handle = NULL; + } + + s->inner.video_rx = NULL; + + /* Clean up user-owned buffer resources */ + mtl_session_user_buf_uinit(s); + + if (ctx) { + if (ctx->ready_ring) { + rte_ring_free(ctx->ready_ring); + ctx->ready_ring = NULL; + } + video_convert_bufs_free(&ctx->convert); + mt_rte_free(ctx); + } +} + +static int video_rx_buffer_get(struct mtl_session_impl* s, mtl_buffer_t** buf, + uint32_t timeout_ms) { + uint64_t deadline_ns = video_calc_deadline_ns(timeout_ms); + + do { + if (mtl_session_check_stopped(s)) return -EAGAIN; + + int ret = rx_try_dequeue_frame(s, buf); + + if (ret == 0) return 0; + if (ret != -EAGAIN) return ret; /* Real error */ + + /* No frame available */ + if (timeout_ms == 0) return -ETIMEDOUT; + + usleep(100); + + if (video_deadline_reached(deadline_ns)) return -ETIMEDOUT; + } while (1); +} + +static int video_rx_buffer_put(struct mtl_session_impl* s, mtl_buffer_t* buf) { + struct video_rx_ctx* ctx = rx_ctx_from_session(s); + struct mtl_buffer_impl* b = MTL_BUFFER_IMPL(buf); + + if (!b || !b->frame_trans) return -EINVAL; + + /* Return frame to the low-level library (thread-safe via st20_rx_put_framebuff) */ + int ret = st20_rx_put_framebuff(ctx->handle, b->frame_trans->addr); + + b->frame_trans = NULL; + b->user_ctx = NULL; + b->user_owned = false; + + return ret; +} + +/************************************************************************* + * User-Owned Buffer Operations (RX) + *************************************************************************/ + +/** + * Post a user-owned buffer for receiving (zero-copy mode). + * + * Looks up IOVA from registered DMA regions, then enqueues the buffer. + * The query_ext_frame callback will dequeue it when the library needs a buffer. + * Received data is signaled via MTL_EVENT_BUFFER_READY with user_ctx. + */ +static int video_rx_buffer_post(struct mtl_session_impl* s, void* data, + size_t size, void* user_ctx) { + if (s->ownership != MTL_BUFFER_USER_OWNED) { + err("%s(%s), buffer_post only valid in USER_OWNED mode\n", __func__, s->name); + return -EINVAL; + } + + /* Look up IOVA for the user buffer */ + mtl_iova_t iova = mtl_session_lookup_iova(s, data, size); + if (iova == MTL_BAD_IOVA) { + err("%s(%s), failed to get IOVA for buffer %p (not registered?)\n", + __func__, s->name, data); + return -EINVAL; + } + + return mtl_session_user_buf_enqueue(s, data, iova, size, user_ctx); +} + +/** + * Register a memory region for DMA access (user-owned mode). + * After registration, buffers from this region can be passed to buffer_post(). + */ +static int video_rx_mem_register(struct mtl_session_impl* s, void* addr, + size_t size, mtl_dma_mem_t** handle) { + if (s->dma_registration_cnt >= 8) { + err("%s(%s), too many DMA registrations (max 8)\n", __func__, s->name); + return -ENOSPC; + } + + struct mtl_dma_mem_impl* reg = + mt_rte_zmalloc_socket(sizeof(*reg), s->socket_id); + if (!reg) return -ENOMEM; + + reg->parent = s->parent; + reg->addr = addr; + reg->size = size; + + /* Try to get IOVA mapping */ + reg->iova = rte_mem_virt2iova(addr); + if (reg->iova == RTE_BAD_IOVA || reg->iova == 0) { + reg->iova = mtl_hp_virt2iova(s->parent, addr); + if (reg->iova == MTL_BAD_IOVA || reg->iova == 0) { + warn("%s(%s), could not get IOVA for region %p, will try per-buffer lookup\n", + __func__, s->name, addr); + reg->iova = 0; + reg->hp_mapped = false; + } else { + reg->hp_mapped = true; + } + } + + s->dma_registrations[s->dma_registration_cnt++] = reg; + + info("%s(%s), registered DMA region %p, size %zu, iova 0x%" PRIx64 "\n", + __func__, s->name, addr, size, reg->iova); + + *handle = (mtl_dma_mem_t*)reg; + return 0; +} + +/** + * Unregister a previously registered DMA memory region. + */ +static int video_rx_mem_unregister(struct mtl_session_impl* s, + mtl_dma_mem_t* handle) { + struct mtl_dma_mem_impl* reg = (struct mtl_dma_mem_impl*)handle; + + for (uint8_t i = 0; i < s->dma_registration_cnt; i++) { + if (s->dma_registrations[i] == reg) { + info("%s(%s), unregistered DMA region %p\n", __func__, s->name, reg->addr); + mt_rte_free(reg); + for (uint8_t j = i; j < s->dma_registration_cnt - 1; j++) { + s->dma_registrations[j] = s->dma_registrations[j + 1]; + } + s->dma_registrations[--s->dma_registration_cnt] = NULL; + return 0; + } + } + + err("%s(%s), DMA handle not found\n", __func__, s->name); + return -EINVAL; +} + +static int video_rx_stats_get(struct mtl_session_impl* s, + mtl_session_stats_t* stats) { + /* Read stats atomically — no lock needed */ + stats->buffers_processed = + __atomic_load_n(&s->stats.buffers_processed, __ATOMIC_RELAXED); + stats->bytes_processed = + __atomic_load_n(&s->stats.bytes_processed, __ATOMIC_RELAXED); + stats->buffers_dropped = + __atomic_load_n(&s->stats.buffers_dropped, __ATOMIC_RELAXED); + stats->epochs_missed = + __atomic_load_n(&s->stats.epochs_missed, __ATOMIC_RELAXED); + + struct st_rx_video_session_impl* rx_impl = s->inner.video_rx; + if (rx_impl) { + uint32_t free_cnt = 0; + for (int i = 0; i < rx_impl->st20_frames_cnt; i++) { + if (rte_atomic32_read(&rx_impl->st20_frames[i].refcnt) == 0) free_cnt++; + } + stats->buffers_free = free_cnt; + stats->buffers_in_use = rx_impl->st20_frames_cnt - free_cnt; + } else { + stats->buffers_free = 0; + stats->buffers_in_use = 0; + } + + return 0; +} + +static int video_rx_update_source(struct mtl_session_impl* s, + const struct st_rx_source_info* src) { + struct video_rx_ctx* ctx = rx_ctx_from_session(s); + if (ctx && ctx->handle) { + return st20_rx_update_source(ctx->handle, (struct st_rx_source_info*)src); + } + return -EINVAL; +} + +static size_t video_rx_get_frame_size(struct mtl_session_impl* s) { + struct video_rx_ctx* ctx = rx_ctx_from_session(s); + if (!ctx) return 0; + return ctx->convert.derive ? ctx->convert.transport_frame_size + : ctx->convert.app_frame_size; +} + +static int video_rx_io_stats_get(struct mtl_session_impl* s, void* stats, + size_t stats_size) { + struct video_rx_ctx* ctx = rx_ctx_from_session(s); + if (!ctx || !ctx->handle) return -EINVAL; + if (stats_size < sizeof(struct st20_rx_user_stats)) return -EINVAL; + return st20_rx_get_session_stats(ctx->handle, (struct st20_rx_user_stats*)stats); +} + +static int video_rx_io_stats_reset(struct mtl_session_impl* s) { + struct video_rx_ctx* ctx = rx_ctx_from_session(s); + if (!ctx || !ctx->handle) return -EINVAL; + return st20_rx_reset_session_stats(ctx->handle); +} + +static int video_rx_pcap_dump(struct mtl_session_impl* s, uint32_t max_pkts, + bool sync, struct st_pcap_dump_meta* meta) { + struct video_rx_ctx* ctx = rx_ctx_from_session(s); + if (!ctx || !ctx->handle) return -EINVAL; + return st20_rx_pcapng_dump(ctx->handle, max_pkts, sync, meta); +} + +static int video_rx_slice_query(struct mtl_session_impl* s, mtl_buffer_t* buf, + uint16_t* lines) { + (void)s; + (void)buf; + (void)lines; + /* TODO: Implement slice query using internal slot line counters */ + return -ENOTSUP; +} + +/************************************************************************* + * Video RX VTable + *************************************************************************/ + +const mtl_session_vtable_t mtl_video_rx_vtable = { + .start = video_rx_start, + .stop = video_rx_stop, + .destroy = video_rx_destroy, + .buffer_get = video_rx_buffer_get, + .buffer_put = video_rx_buffer_put, + .buffer_post = video_rx_buffer_post, + .buffer_flush = NULL, + .mem_register = video_rx_mem_register, + .mem_unregister = video_rx_mem_unregister, + .event_poll = video_session_event_poll, /* shared implementation */ + .get_event_fd = NULL, + .stats_get = video_rx_stats_get, + .stats_reset = video_session_stats_reset, /* shared implementation */ + .get_frame_size = video_rx_get_frame_size, + .io_stats_get = video_rx_io_stats_get, + .io_stats_reset = video_rx_io_stats_reset, + .pcap_dump = video_rx_pcap_dump, + .update_destination = NULL, + .update_source = video_rx_update_source, + .slice_ready = NULL, + .slice_query = video_rx_slice_query, + .get_plugin_info = NULL, + .get_queue_meta = NULL, +}; + +/************************************************************************* + * Session Initialization - Helpers + *************************************************************************/ + +/** + * Create the ready ring for received frame queuing. + */ +static int rx_create_ready_ring(struct video_rx_ctx* ctx, struct mtl_session_impl* s) { + char ring_name[RTE_RING_NAMESIZE]; + snprintf(ring_name, sizeof(ring_name), "mtl_rx_%p", s); + + ctx->ready_ring = + rte_ring_create(ring_name, 32, s->socket_id, RING_F_SP_ENQ); + if (!ctx->ready_ring) { + err("%s(%s), failed to create ready ring\n", __func__, s->name); + return -ENOMEM; + } + return 0; +} + +/** + * Populate st20_rx_ops port fields from mtl_video_config_t. + */ +static void rx_fill_port_config(struct st20_rx_ops* ops, + const mtl_video_config_t* config) { + memcpy(ops->port, config->rx_port.port, sizeof(ops->port)); + memcpy(ops->ip_addr, config->rx_port.ip_addr, sizeof(ops->ip_addr)); + ops->num_port = config->rx_port.num_port; + if (ops->num_port == 0) ops->num_port = 1; + memcpy(ops->udp_port, config->rx_port.udp_port, sizeof(ops->udp_port)); + ops->payload_type = config->rx_port.payload_type; + ops->ssrc = config->rx_port.ssrc; + memcpy(ops->mcast_sip_addr, config->rx_port.mcast_sip_addr, + sizeof(ops->mcast_sip_addr)); +} + +/** + * Populate st20_rx_ops video format fields from mtl_video_config_t. + */ +static void rx_fill_video_format(struct st20_rx_ops* ops, + const mtl_video_config_t* config) { + ops->width = config->width; + ops->height = config->height; + ops->fps = config->fps; + ops->interlaced = config->interlaced; + ops->fmt = config->transport_fmt; + ops->packing = config->packing; + ops->linesize = config->linesize; +} + +/** + * Map unified session flags to st20_rx flags and set callbacks. + */ +static void rx_apply_session_flags(struct st20_rx_ops* ops, + const mtl_video_config_t* config, + struct video_rx_ctx* ctx) { + /* Auto-detect */ + if (config->enable_auto_detect) { + ops->flags |= ST20_RX_FLAG_AUTO_DETECT; + ops->notify_detected = video_rx_notify_detected; + } + + /* Vsync events - use shared callback via session pointer */ + if (config->base.flags & MTL_SESSION_FLAG_ENABLE_VSYNC) { + ops->notify_event = video_session_notify_event; + } + + /* User-owned ext_frame mode: only when app provides an explicit callback. + * The default buffer_post() path uses library's internal framebuffers and + * converts/copies into user buffers in notify_frame_ready. */ + if (config->base.ownership == MTL_BUFFER_USER_OWNED && + config->base.query_ext_frame) { + ops->query_ext_frame = video_rx_query_ext_frame_wrapper; + ops->flags |= ST20_RX_FLAG_RECEIVE_INCOMPLETE_FRAME; + ctx->user_query_ext_frame = config->base.query_ext_frame; + ctx->user_priv = config->base.priv; + } + + /* Individual flag mappings */ + if (config->base.flags & MTL_SESSION_FLAG_RECEIVE_INCOMPLETE_FRAME) + ops->flags |= ST20_RX_FLAG_RECEIVE_INCOMPLETE_FRAME; + if (config->base.flags & MTL_SESSION_FLAG_DMA_OFFLOAD) + ops->flags |= ST20_RX_FLAG_DMA_OFFLOAD; + if (config->base.flags & MTL_SESSION_FLAG_DATA_PATH_ONLY) + ops->flags |= ST20_RX_FLAG_DATA_PATH_ONLY; + if (config->base.flags & MTL_SESSION_FLAG_HDR_SPLIT) + ops->flags |= ST20_RX_FLAG_HDR_SPLIT; + if (config->base.flags & MTL_SESSION_FLAG_ENABLE_RTCP) + ops->flags |= ST20_RX_FLAG_ENABLE_RTCP; + if (config->base.flags & MTL_SESSION_FLAG_FORCE_NUMA) + ops->socket_id = config->base.socket_id; + if (config->base.flags & MTL_SESSION_FLAG_USE_MULTI_THREADS) + ops->flags |= ST20_RX_FLAG_USE_MULTI_THREADS; + if (config->enable_timing_parser) + ops->flags |= ST20_RX_FLAG_TIMING_PARSER_STAT; + + /* Advanced RX options */ + if (config->rx_burst_size) ops->rx_burst_size = config->rx_burst_size; +} + +/** + * Cleanup all resources on init failure. + */ +static void rx_cleanup_on_failure(struct video_rx_ctx* ctx) { + if (ctx->handle) { + st20_rx_free(ctx->handle); + ctx->handle = NULL; + } + video_convert_bufs_free(&ctx->convert); + if (ctx->ready_ring) { + rte_ring_free(ctx->ready_ring); + ctx->ready_ring = NULL; + } + mt_rte_free(ctx); +} + +/************************************************************************* + * Session Initialization + *************************************************************************/ + +int mtl_video_rx_session_init(struct mtl_session_impl* s, struct mtl_main_impl* impl, + const mtl_video_config_t* config) { + int ret; + + /* Allocate callback context */ + struct video_rx_ctx* ctx = mt_rte_zmalloc_socket(sizeof(*ctx), s->socket_id); + if (!ctx) { + err("%s, failed to alloc ctx\n", __func__); + return -ENOMEM; + } + ctx->session = s; + + /* Initialize format conversion (shared helper) */ + ret = video_convert_ctx_init(&ctx->convert, config, false /* RX */); + if (ret < 0) { + mt_rte_free(ctx); + return ret; + } + s->video.frame_fmt = ctx->convert.frame_fmt; + s->video.derive = ctx->convert.derive; + + /* Create frame queuing ring */ + ret = rx_create_ready_ring(ctx, s); + if (ret < 0) { + mt_rte_free(ctx); + return ret; + } + + /* Build st20_rx_ops from config */ + struct st20_rx_ops ops; + memset(&ops, 0, sizeof(ops)); + + rx_fill_port_config(&ops, config); + rx_fill_video_format(&ops, config); + + ops.name = config->base.name; + ops.priv = ctx; + ops.framebuff_cnt = config->base.num_buffers; + if (ops.framebuff_cnt < 2) ops.framebuff_cnt = 2; + + /* Mode: frame vs slice */ + if (config->mode == MTL_VIDEO_MODE_SLICE) { + ops.type = ST20_TYPE_SLICE_LEVEL; + ops.slice_lines = config->height / 4; + } else { + ops.type = ST20_TYPE_FRAME_LEVEL; + } + + ops.notify_frame_ready = video_rx_notify_frame_ready; + + rx_apply_session_flags(&ops, config, ctx); + + /* Create the low-level RX session */ + st20_rx_handle handle = st20_rx_create(impl, &ops); + if (!handle) { + err("%s(%s), st20_rx_create failed\n", __func__, s->name); + rx_cleanup_on_failure(ctx); + return -EIO; + } + + ctx->handle = handle; + ctx->convert.transport_frame_size = st20_rx_get_framebuffer_size(handle); + + /* Link inner session implementation */ + struct st_rx_video_session_handle_impl* handle_impl = + (struct st_rx_video_session_handle_impl*)handle; + s->inner.video_rx = handle_impl->impl; + s->idx = s->inner.video_rx->idx; + + /* Allocate conversion buffers if needed (shared helper) */ + if (!ctx->convert.derive) { + ret = video_convert_bufs_alloc(&ctx->convert, s->inner.video_rx->st20_frames_cnt, + s->socket_id); + if (ret < 0) { + s->inner.video_rx = NULL; + rx_cleanup_on_failure(ctx); + return ret; + } + } + + /* Initialize user-owned buffer management if needed */ + if (s->ownership == MTL_BUFFER_USER_OWNED) { + ret = mtl_session_user_buf_init(s, s->inner.video_rx->st20_frames_cnt); + if (ret < 0) { + err("%s(%s), user_buf_init failed: %d\n", __func__, s->name, ret); + s->inner.video_rx = NULL; + rx_cleanup_on_failure(ctx); + return ret; + } + } + + info("%s(%d), transport fmt %s, output fmt %s, frame_size %zu, fb_cnt %u, derive %d%s\n", + __func__, s->idx, st20_fmt_name(config->transport_fmt), + st_frame_fmt_name(config->frame_fmt), + ctx->convert.transport_frame_size, ops.framebuff_cnt, + ctx->convert.derive, + s->ownership == MTL_BUFFER_USER_OWNED ? ", user-owned" : ""); + + return 0; +} + +void mtl_video_rx_session_uinit(struct mtl_session_impl* s) { + video_rx_destroy(s); +} diff --git a/lib/src/new_api/mt_session_video_tx.c b/lib/src/new_api/mt_session_video_tx.c new file mode 100644 index 000000000..f11993c2e --- /dev/null +++ b/lib/src/new_api/mt_session_video_tx.c @@ -0,0 +1,880 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2024 Intel Corporation + */ + +/** + * @file mt_session_video_tx.c + * + * Video TX session implementation for the unified session API. + * Wraps st20_tx_create/free and translates between mtl_video_config_t + * and st20_tx_ops. + */ + +#include "mt_session_video_common.h" + +#include "../mt_log.h" +#include "../mt_mem.h" + +/************************************************************************* + * TX Frame State Machine + * + * Tracks the app-facing lifecycle of each framebuffer, separate from + * the low-level library's internal refcnt. + * + * FREE → APP_OWNED → READY → TRANSMITTING → FREE + * (get) (put) (get_next_frame) (frame_done) + * + * Thread safety: lock-free using C11 __atomic operations. + * - tx_try_claim_frame: CAS (FREE → APP_OWNED) with __ATOMIC_ACQ_REL + * - buffer_put: atomic store (APP_OWNED → READY) with __ATOMIC_RELEASE + * - get_next_frame: CAS (READY → TRANSMITTING) with __ATOMIC_ACQUIRE + * - frame_done: atomic store (TRANSMITTING → FREE) with __ATOMIC_RELEASE + * + * This forms an acquire-release chain ensuring frame data visibility: + * frame_done(RELEASE:FREE) → try_claim(ACQUIRE:APP_OWNED) → + * buffer_put(RELEASE:READY) → get_next_frame(ACQUIRE:TRANSMITTING) → ... + *************************************************************************/ + +enum tx_frame_state { + TX_FRAME_FREE = 0, /**< Available for buffer_get */ + TX_FRAME_APP_OWNED = 1, /**< App is filling it (between get and put) */ + TX_FRAME_READY = 2, /**< App called put, awaiting get_next_frame */ + TX_FRAME_TRANSMITTING = 3 /**< Library picked it for transmission */ +}; + +/************************************************************************* + * Callback Context + *************************************************************************/ + +struct video_tx_ctx { + struct mtl_session_impl* session; + st20_tx_handle handle; /**< low-level TX handle */ + struct video_convert_ctx convert; /**< shared format conversion context */ + + /** Per-frame state tracking (protected by session->buffer_lock) */ + enum tx_frame_state* frame_state; + uint16_t frame_cnt; + + /* User slice callback (if any) */ + int (*user_query_lines_ready)(void* priv, uint16_t frame_idx, uint16_t* lines_ready); + void* user_priv; +}; + +/************************************************************************* + * Internal Helpers + *************************************************************************/ + +/** Get the video_tx_ctx from session (caller must ensure session is valid). */ +static inline struct video_tx_ctx* tx_ctx_from_session(struct mtl_session_impl* s) { + return s->inner.video_tx->ops.priv; +} + +/************************************************************************* + * ST20 TX Callbacks → Unified Event Queue + * + * These run on library datapath threads. They acquire buffer_lock + * briefly for frame_state transitions. + *************************************************************************/ + +/** + * get_next_frame callback - library asks which frame to transmit next. + * Scans for a frame in READY state and transitions it to TRANSMITTING. + * + * For user-owned mode: also checks the user_buf_ring for posted buffers + * and sets ext_frame on a free frame slot before marking it READY. + */ +static int video_tx_get_next_frame(void* priv, uint16_t* next_frame_idx, + struct st20_tx_frame_meta* meta) { + struct video_tx_ctx* ctx = priv; + struct mtl_session_impl* s = ctx->session; + struct st_tx_video_session_impl* tx_impl = s->inner.video_tx; + (void)meta; + + if (!tx_impl || !tx_impl->st20_frames) return -EIO; + if (!ctx->frame_state) return -EAGAIN; /* init not yet complete */ + + /* User-owned mode: check for posted buffers and bind to free frame slots */ + if (s->ownership == MTL_BUFFER_USER_OWNED) { + struct mtl_user_buffer_entry entry; + while (mtl_session_user_buf_dequeue(s, &entry) == 0) { + /* Find a free frame slot to bind this user buffer */ + bool bound = false; + for (uint16_t i = 0; i < tx_impl->st20_frames_cnt; i++) { + enum tx_frame_state expected = TX_FRAME_FREE; + if (__atomic_compare_exchange_n(&ctx->frame_state[i], &expected, + TX_FRAME_APP_OWNED, false, + __ATOMIC_ACQ_REL, __ATOMIC_RELAXED)) { + int ret; + + if (ctx->convert.derive) { + /* Formats match — true zero-copy via ext_frame. + * st20_tx sends the user buffer directly; no conversion. */ + struct st20_ext_frame ext = {0}; + ext.buf_addr = entry.data; + ext.buf_iova = entry.iova; + ext.buf_len = entry.size; + ext.opaque = entry.user_ctx; + + ret = st20_tx_set_ext_frame(ctx->handle, i, &ext); + if (ret < 0) { + err("%s(%s), st20_tx_set_ext_frame failed for slot %u: %d\n", + __func__, s->name, i, ret); + __atomic_store_n(&ctx->frame_state[i], TX_FRAME_FREE, __ATOMIC_RELEASE); + continue; + } + } else { + /* Format conversion needed: convert user data (app format) into + * the library's own framebuffer (transport format). + * st20_tx will transmit the correctly-formatted framebuffer. */ + ret = video_convert_frame( + &ctx->convert, entry.data, entry.iova, entry.size, + tx_impl->st20_frames[i].addr, tx_impl->st20_frames[i].iova, + ctx->convert.transport_frame_size, true /* TX: app→transport */); + if (ret < 0) { + err("%s(%s), format conversion failed for slot %u: %d\n", + __func__, s->name, i, ret); + __atomic_store_n(&ctx->frame_state[i], TX_FRAME_FREE, __ATOMIC_RELEASE); + continue; + } + } + + /* Save user context for completion event */ + if (s->user_buf_ctx && i < s->user_buf_ctx_cnt) { + s->user_buf_ctx[i] = entry.user_ctx; + } + + /* Mark ready for transmission */ + __atomic_store_n(&ctx->frame_state[i], TX_FRAME_READY, __ATOMIC_RELEASE); + bound = true; + break; + } + } + if (!bound) { + dbg("%s(%s), no free frame slot for user buffer, requeueing\n", + __func__, s->name); + /* Re-enqueue - no slot free yet */ + mtl_session_user_buf_enqueue(s, entry.data, entry.iova, entry.size, + entry.user_ctx); + break; + } + } + } + + for (uint16_t i = 0; i < tx_impl->st20_frames_cnt; i++) { + enum tx_frame_state expected = TX_FRAME_READY; + if (__atomic_compare_exchange_n(&ctx->frame_state[i], &expected, + TX_FRAME_TRANSMITTING, false, + __ATOMIC_ACQUIRE, __ATOMIC_RELAXED)) { + *next_frame_idx = i; + rte_atomic32_set(&tx_impl->st20_frames[i].refcnt, 0); + return 0; + } + } + + return -EBUSY; +} + +/** + * notify_frame_done callback - transmission complete, release frame. + * Transitions frame from TRANSMITTING → FREE. + * For user-owned mode: includes user_ctx in the completion event. + */ +static int video_tx_notify_frame_done(void* priv, uint16_t frame_idx, + struct st20_tx_frame_meta* meta) { + struct video_tx_ctx* ctx = priv; + struct mtl_session_impl* s = ctx->session; + struct st_tx_video_session_impl* tx_impl = s->inner.video_tx; + + if (frame_idx >= tx_impl->st20_frames_cnt) return -EINVAL; + + /* Retrieve user context before clearing frame state */ + void* user_ctx = NULL; + if (s->ownership == MTL_BUFFER_USER_OWNED && s->user_buf_ctx && + frame_idx < s->user_buf_ctx_cnt) { + user_ctx = s->user_buf_ctx[frame_idx]; + s->user_buf_ctx[frame_idx] = NULL; + } + + __atomic_store_n(&ctx->frame_state[frame_idx], TX_FRAME_FREE, __ATOMIC_RELEASE); + + /* Update stats (lock-free, relaxed ordering for counters) */ + __atomic_add_fetch(&s->stats.buffers_processed, 1, __ATOMIC_RELAXED); + __atomic_add_fetch(&s->stats.bytes_processed, ctx->convert.transport_frame_size, + __ATOMIC_RELAXED); + + /* Post completion event */ + mtl_event_t event = {0}; + event.type = MTL_EVENT_BUFFER_DONE; + event.timestamp = meta ? meta->epoch : 0; + event.ctx = user_ctx; /* User context for user-owned mode */ + mtl_session_event_post(s, &event); + + return 0; +} + +/** + * notify_frame_late callback - frame missed its epoch. + */ +static int video_tx_notify_frame_late(void* priv, uint64_t epoch_skipped) { + struct video_tx_ctx* ctx = priv; + struct mtl_session_impl* s = ctx->session; + + __atomic_add_fetch(&s->stats.epochs_missed, 1, __ATOMIC_RELAXED); + + mtl_event_t event = {0}; + event.type = MTL_EVENT_FRAME_LATE; + event.frame_late.epoch_skipped = epoch_skipped; + mtl_session_event_post(s, &event); + + return 0; +} + +/** + * Wrapper for query_frame_lines_ready (slice mode). + */ +static int video_tx_query_lines_ready_wrapper(void* priv, uint16_t frame_idx, + struct st20_tx_slice_meta* meta) { + struct video_tx_ctx* ctx = priv; + if (!ctx->user_query_lines_ready) return -ENOTSUP; + + uint16_t lines_ready = 0; + int ret = ctx->user_query_lines_ready(ctx->user_priv, frame_idx, &lines_ready); + if (ret == 0) { + meta->lines_ready = lines_ready; + } + return ret; +} + +/************************************************************************* + * Buffer Get/Put Helpers + *************************************************************************/ + +/** + * Fill buffer data pointers for the app. + * In conversion mode, gives the app-format source buffer. + * In derive mode, gives the transport framebuffer directly. + */ +static void tx_fill_buffer_data(mtl_buffer_t* pub, struct video_tx_ctx* ctx, + struct st_tx_video_session_impl* tx_impl, + uint16_t frame_idx) { + struct video_convert_ctx* cvt = &ctx->convert; + + if (!cvt->derive && cvt->app_bufs && frame_idx < cvt->app_bufs_cnt && + cvt->app_bufs[frame_idx]) { + /* Conversion mode: give app the source buffer (app pixel format) */ + pub->data = cvt->app_bufs[frame_idx]; + pub->iova = 0; + pub->size = cvt->app_frame_size; + pub->data_size = cvt->app_frame_size; + pub->video.fmt = cvt->frame_fmt; + } else { + /* Derive mode: give app the transport framebuffer directly */ + pub->data = tx_impl->st20_frames[frame_idx].addr; + pub->iova = tx_impl->st20_frames[frame_idx].iova; + pub->size = cvt->transport_frame_size; + pub->data_size = cvt->transport_frame_size; + pub->video.fmt = st_frame_fmt_from_transport(cvt->transport_fmt); + } + + pub->video.width = cvt->width; + pub->video.height = cvt->height; +} + +/** + * Try to find a free frame and claim it for the app. + * Returns 0 on success with *buf set, or -EAGAIN if no frame free. + * + * Thread safety: lock-free. Uses atomic CAS to claim exclusive ownership. + * Multiple threads can call this concurrently; only one CAS succeeds per frame. + */ +static int tx_try_claim_frame(struct mtl_session_impl* s, mtl_buffer_t** buf) { + struct st_tx_video_session_impl* tx_impl = s->inner.video_tx; + struct video_tx_ctx* ctx = tx_ctx_from_session(s); + + for (uint16_t i = 0; i < tx_impl->st20_frames_cnt; i++) { + if (rte_atomic32_read(&tx_impl->st20_frames[i].refcnt) != 0) continue; + + enum tx_frame_state expected = TX_FRAME_FREE; + if (__atomic_compare_exchange_n(&ctx->frame_state[i], &expected, + TX_FRAME_APP_OWNED, false, + __ATOMIC_ACQ_REL, __ATOMIC_RELAXED)) { + /* Claimed this frame for the app */ + + struct mtl_buffer_impl* b = &s->buffers[i % s->buffer_count]; + b->frame_trans = &tx_impl->st20_frames[i]; + b->idx = i; + + mtl_buffer_t* pub = &b->pub; + memset(pub, 0, sizeof(*pub)); + pub->priv = b; + pub->flags = 0; + pub->status = MTL_FRAME_STATUS_COMPLETE; + + tx_fill_buffer_data(pub, ctx, tx_impl, i); + + *buf = pub; + return 0; + } + } + + return -EAGAIN; +} + +/** + * Perform format conversion on buffer_put (app format → transport). + * Returns 0 on success, negative errno on failure. + */ +static int tx_convert_on_put(struct video_tx_ctx* ctx, struct mtl_buffer_impl* b) { + struct video_convert_ctx* cvt = &ctx->convert; + + if (cvt->derive || !cvt->app_bufs || b->idx >= cvt->app_bufs_cnt || + !cvt->app_bufs[b->idx]) { + return 0; /* No conversion needed */ + } + + return video_convert_frame(cvt, cvt->app_bufs[b->idx], 0, cvt->app_frame_size, + b->frame_trans->addr, b->frame_trans->iova, + cvt->transport_frame_size, true /* TX direction */); +} + +/** + * Pass user metadata and timestamp from the buffer to the frame_trans. + */ +static void tx_apply_buffer_metadata(mtl_buffer_t* buf, struct st_frame_trans* ft) { + if (buf->user_meta && buf->user_meta_size > 0) { + ft->tv_meta.user_meta = buf->user_meta; + ft->tv_meta.user_meta_size = buf->user_meta_size; + } else { + ft->tv_meta.user_meta = NULL; + ft->tv_meta.user_meta_size = 0; + } + + if (buf->timestamp) { + ft->tv_meta.timestamp = buf->timestamp; + ft->tv_meta.tfmt = buf->tfmt; + } +} + +/************************************************************************* + * VTable Implementation + *************************************************************************/ + +static int video_tx_start(struct mtl_session_impl* s) { + (void)s; + return 0; +} + +static int video_tx_stop(struct mtl_session_impl* s) { + (void)s; + return 0; +} + +static void video_tx_destroy(struct mtl_session_impl* s) { + struct video_tx_ctx* ctx = NULL; + + if (s->inner.video_tx) { + ctx = s->inner.video_tx->ops.priv; + } + + /* Free the low-level session */ + if (ctx && ctx->handle) { + st20_tx_free(ctx->handle); + ctx->handle = NULL; + } + + s->inner.video_tx = NULL; + + /* Clean up user-owned buffer resources */ + mtl_session_user_buf_uinit(s); + + if (ctx) { + if (ctx->frame_state) { + mt_rte_free(ctx->frame_state); + ctx->frame_state = NULL; + } + video_convert_bufs_free(&ctx->convert); + mt_rte_free(ctx); + } +} + +static int video_tx_buffer_get(struct mtl_session_impl* s, mtl_buffer_t** buf, + uint32_t timeout_ms) { + uint64_t deadline_ns = video_calc_deadline_ns(timeout_ms); + + do { + if (mtl_session_check_stopped(s)) return -EAGAIN; + + int ret = tx_try_claim_frame(s, buf); + + if (ret == 0) return 0; + + /* No free frame - check timeout */ + if (timeout_ms == 0) return -ETIMEDOUT; + + usleep(100); + + if (video_deadline_reached(deadline_ns)) return -ETIMEDOUT; + } while (1); +} + +static int video_tx_buffer_put(struct mtl_session_impl* s, mtl_buffer_t* buf) { + struct video_tx_ctx* ctx = tx_ctx_from_session(s); + struct mtl_buffer_impl* b = MTL_BUFFER_IMPL(buf); + + if (!b || !b->frame_trans) return -EINVAL; + + /* Perform format conversion if needed (app → transport) */ + int ret = tx_convert_on_put(ctx, b); + if (ret < 0) { + __atomic_store_n(&ctx->frame_state[b->idx], TX_FRAME_FREE, __ATOMIC_RELEASE); + return ret; + } + + /* Apply metadata to the low-level frame */ + tx_apply_buffer_metadata(buf, b->frame_trans); + + /* Mark frame as ready for transmission (atomic release ensures data visibility) */ + __atomic_store_n(&ctx->frame_state[b->idx], TX_FRAME_READY, __ATOMIC_RELEASE); + + return 0; +} + +/************************************************************************* + * User-Owned Buffer Operations (TX) + *************************************************************************/ + +/** + * Post a user-owned buffer for transmission (zero-copy mode). + * + * Looks up IOVA from registered DMA regions, then enqueues the buffer. + * The get_next_frame callback will bind it to a frame slot and transmit. + * Completion is signaled via MTL_EVENT_BUFFER_DONE with user_ctx. + */ +static int video_tx_buffer_post(struct mtl_session_impl* s, void* data, + size_t size, void* user_ctx) { + if (s->ownership != MTL_BUFFER_USER_OWNED) { + err("%s(%s), buffer_post only valid in USER_OWNED mode\n", __func__, s->name); + return -EINVAL; + } + + /* Look up IOVA for the user buffer */ + mtl_iova_t iova = mtl_session_lookup_iova(s, data, size); + if (iova == MTL_BAD_IOVA) { + err("%s(%s), failed to get IOVA for buffer %p (not registered?)\n", + __func__, s->name, data); + return -EINVAL; + } + + return mtl_session_user_buf_enqueue(s, data, iova, size, user_ctx); +} + +/** + * Register a memory region for DMA access (user-owned mode). + * After registration, buffers from this region can be passed to buffer_post(). + */ +static int video_tx_mem_register(struct mtl_session_impl* s, void* addr, + size_t size, mtl_dma_mem_t** handle) { + if (s->dma_registration_cnt >= 8) { + err("%s(%s), too many DMA registrations (max 8)\n", __func__, s->name); + return -ENOSPC; + } + + struct mtl_dma_mem_impl* reg = + mt_rte_zmalloc_socket(sizeof(*reg), s->socket_id); + if (!reg) return -ENOMEM; + + reg->parent = s->parent; + reg->addr = addr; + reg->size = size; + + /* Try to get IOVA mapping */ + reg->iova = rte_mem_virt2iova(addr); + if (reg->iova == RTE_BAD_IOVA || reg->iova == 0) { + /* Try hugepage mapping */ + reg->iova = mtl_hp_virt2iova(s->parent, addr); + if (reg->iova == MTL_BAD_IOVA || reg->iova == 0) { + /* Memory might be from a custom allocator - try to use it anyway. + * The IOVA lookup will try rte_mem_virt2iova per-buffer later. */ + warn("%s(%s), could not get IOVA for region %p, will try per-buffer lookup\n", + __func__, s->name, addr); + reg->iova = 0; + reg->hp_mapped = false; + } else { + reg->hp_mapped = true; + } + } + + s->dma_registrations[s->dma_registration_cnt++] = reg; + + info("%s(%s), registered DMA region %p, size %zu, iova 0x%" PRIx64 "\n", + __func__, s->name, addr, size, reg->iova); + + *handle = (mtl_dma_mem_t*)reg; + return 0; +} + +/** + * Unregister a previously registered DMA memory region. + */ +static int video_tx_mem_unregister(struct mtl_session_impl* s, + mtl_dma_mem_t* handle) { + struct mtl_dma_mem_impl* reg = (struct mtl_dma_mem_impl*)handle; + + for (uint8_t i = 0; i < s->dma_registration_cnt; i++) { + if (s->dma_registrations[i] == reg) { + info("%s(%s), unregistered DMA region %p\n", __func__, s->name, reg->addr); + mt_rte_free(reg); + /* Shift remaining entries */ + for (uint8_t j = i; j < s->dma_registration_cnt - 1; j++) { + s->dma_registrations[j] = s->dma_registrations[j + 1]; + } + s->dma_registrations[--s->dma_registration_cnt] = NULL; + return 0; + } + } + + err("%s(%s), DMA handle not found\n", __func__, s->name); + return -EINVAL; +} + +static int video_tx_stats_get(struct mtl_session_impl* s, + mtl_session_stats_t* stats) { + /* Read stats atomically — no lock needed, no deadlock possible */ + stats->buffers_processed = + __atomic_load_n(&s->stats.buffers_processed, __ATOMIC_RELAXED); + stats->bytes_processed = + __atomic_load_n(&s->stats.bytes_processed, __ATOMIC_RELAXED); + stats->buffers_dropped = + __atomic_load_n(&s->stats.buffers_dropped, __ATOMIC_RELAXED); + stats->epochs_missed = + __atomic_load_n(&s->stats.epochs_missed, __ATOMIC_RELAXED); + + struct st_tx_video_session_impl* tx_impl = s->inner.video_tx; + struct video_tx_ctx* ctx = tx_impl ? tx_impl->ops.priv : NULL; + if (tx_impl && ctx && ctx->frame_state) { + uint32_t free_cnt = 0; + for (uint16_t i = 0; i < ctx->frame_cnt; i++) { + if (__atomic_load_n(&ctx->frame_state[i], __ATOMIC_RELAXED) == TX_FRAME_FREE) + free_cnt++; + } + stats->buffers_free = free_cnt; + stats->buffers_in_use = ctx->frame_cnt - free_cnt; + } else { + stats->buffers_free = 0; + stats->buffers_in_use = 0; + } + + return 0; +} + +static int video_tx_update_destination(struct mtl_session_impl* s, + const struct st_tx_dest_info* dst) { + struct video_tx_ctx* ctx = tx_ctx_from_session(s); + if (ctx && ctx->handle) { + return st20_tx_update_destination(ctx->handle, (struct st_tx_dest_info*)dst); + } + return -EINVAL; +} + +static size_t video_tx_get_frame_size(struct mtl_session_impl* s) { + struct video_tx_ctx* ctx = tx_ctx_from_session(s); + if (!ctx) return 0; + return ctx->convert.derive ? ctx->convert.transport_frame_size + : ctx->convert.app_frame_size; +} + +static int video_tx_io_stats_get(struct mtl_session_impl* s, void* stats, + size_t stats_size) { + struct video_tx_ctx* ctx = tx_ctx_from_session(s); + if (!ctx || !ctx->handle) return -EINVAL; + if (stats_size < sizeof(struct st20_tx_user_stats)) return -EINVAL; + return st20_tx_get_session_stats(ctx->handle, (struct st20_tx_user_stats*)stats); +} + +static int video_tx_io_stats_reset(struct mtl_session_impl* s) { + struct video_tx_ctx* ctx = tx_ctx_from_session(s); + if (!ctx || !ctx->handle) return -EINVAL; + return st20_tx_reset_session_stats(ctx->handle); +} + +static int video_tx_slice_ready(struct mtl_session_impl* s, mtl_buffer_t* buf, + uint16_t lines) { + (void)s; + (void)buf; + (void)lines; + return 0; +} + +/************************************************************************* + * Video TX VTable + *************************************************************************/ + +const mtl_session_vtable_t mtl_video_tx_vtable = { + .start = video_tx_start, + .stop = video_tx_stop, + .destroy = video_tx_destroy, + .buffer_get = video_tx_buffer_get, + .buffer_put = video_tx_buffer_put, + .buffer_post = video_tx_buffer_post, + .buffer_flush = NULL, + .mem_register = video_tx_mem_register, + .mem_unregister = video_tx_mem_unregister, + .event_poll = video_session_event_poll, /* shared implementation */ + .get_event_fd = NULL, + .stats_get = video_tx_stats_get, + .stats_reset = video_session_stats_reset, /* shared implementation */ + .get_frame_size = video_tx_get_frame_size, + .io_stats_get = video_tx_io_stats_get, + .io_stats_reset = video_tx_io_stats_reset, + .pcap_dump = NULL, + .update_destination = video_tx_update_destination, + .update_source = NULL, + .slice_ready = video_tx_slice_ready, + .slice_query = NULL, + .get_plugin_info = NULL, + .get_queue_meta = NULL, +}; + +/************************************************************************* + * Session Initialization - Helpers + *************************************************************************/ + +/** + * Populate st20_tx_ops port fields from mtl_video_config_t. + */ +static void tx_fill_port_config(struct st20_tx_ops* ops, + const mtl_video_config_t* config) { + memcpy(ops->port, config->tx_port.port, sizeof(ops->port)); + memcpy(ops->dip_addr, config->tx_port.dip_addr, sizeof(ops->dip_addr)); + ops->num_port = config->tx_port.num_port; + if (ops->num_port == 0) ops->num_port = 1; + memcpy(ops->udp_port, config->tx_port.udp_port, sizeof(ops->udp_port)); + ops->payload_type = config->tx_port.payload_type; + ops->ssrc = config->tx_port.ssrc; + memcpy(ops->udp_src_port, config->tx_port.udp_src_port, sizeof(ops->udp_src_port)); +} + +/** + * Populate st20_tx_ops video format fields from mtl_video_config_t. + */ +static void tx_fill_video_format(struct st20_tx_ops* ops, + const mtl_video_config_t* config) { + ops->width = config->width; + ops->height = config->height; + ops->fps = config->fps; + ops->interlaced = config->interlaced; + ops->fmt = config->transport_fmt; + ops->packing = config->packing; + ops->pacing = config->pacing; + ops->linesize = config->linesize; +} + +/** + * Map unified session flags to st20_tx flags and set callbacks. + */ +static void tx_apply_session_flags(struct st20_tx_ops* ops, + const mtl_video_config_t* config, + struct video_tx_ctx* ctx) { + /* Vsync events - use shared callback */ + if (config->base.flags & MTL_SESSION_FLAG_ENABLE_VSYNC) { + ops->notify_event = video_session_notify_event; + ops->flags |= ST20_TX_FLAG_ENABLE_VSYNC; + } + + /* Buffer ownership flags: + * Only use ext_frame when formats match (derive) — true zero-copy. + * When conversion is needed (!derive), we must convert app→transport into + * the library's own framebuffers, so ext_frame cannot be used. */ + if (config->base.ownership == MTL_BUFFER_USER_OWNED && ctx->convert.derive) + ops->flags |= ST20_TX_FLAG_EXT_FRAME; + + /* Individual flag mappings */ + if (config->base.flags & MTL_SESSION_FLAG_USER_PACING) + ops->flags |= ST20_TX_FLAG_USER_PACING; + if (config->base.flags & MTL_SESSION_FLAG_USER_TIMESTAMP) + ops->flags |= ST20_TX_FLAG_USER_TIMESTAMP; + if (config->base.flags & MTL_SESSION_FLAG_ENABLE_RTCP) + ops->flags |= ST20_TX_FLAG_ENABLE_RTCP; + if (config->base.flags & MTL_SESSION_FLAG_FORCE_NUMA) { + ops->flags |= ST20_TX_FLAG_FORCE_NUMA; + ops->socket_id = config->base.socket_id; + } + if (config->base.flags & MTL_SESSION_FLAG_USER_P_MAC) { + ops->flags |= ST20_TX_FLAG_USER_P_MAC; + memcpy(ops->tx_dst_mac[MTL_SESSION_PORT_P], config->tx_dst_mac[MTL_SESSION_PORT_P], + MTL_MAC_ADDR_LEN); + } + if (config->base.flags & MTL_SESSION_FLAG_USER_R_MAC) { + ops->flags |= ST20_TX_FLAG_USER_R_MAC; + memcpy(ops->tx_dst_mac[MTL_SESSION_PORT_R], config->tx_dst_mac[MTL_SESSION_PORT_R], + MTL_MAC_ADDR_LEN); + } + if (config->base.flags & MTL_SESSION_FLAG_EXACT_USER_PACING) + ops->flags |= ST20_TX_FLAG_EXACT_USER_PACING; + if (config->base.flags & MTL_SESSION_FLAG_RTP_TIMESTAMP_EPOCH) + ops->flags |= ST20_TX_FLAG_RTP_TIMESTAMP_EPOCH; + if (config->base.flags & MTL_SESSION_FLAG_DISABLE_BULK) + ops->flags |= ST20_TX_FLAG_DISABLE_BULK; + if (config->base.flags & MTL_SESSION_FLAG_STATIC_PAD_P) + ops->flags |= ST20_TX_FLAG_ENABLE_STATIC_PAD_P; + + /* Advanced TX options */ + if (config->start_vrx) ops->start_vrx = config->start_vrx; + if (config->pad_interval) ops->pad_interval = config->pad_interval; + if (config->rtp_timestamp_delta_us) + ops->rtp_timestamp_delta_us = config->rtp_timestamp_delta_us; + + /* Slice mode */ + (void)ctx; /* ctx used for slice callback setup below */ +} + +/** + * Allocate the per-frame state tracking array. + * All frames start in TX_FRAME_FREE state. + */ +static int tx_alloc_frame_state(struct video_tx_ctx* ctx, uint16_t fb_cnt, + int socket_id) { + ctx->frame_state = + mt_rte_zmalloc_socket(sizeof(enum tx_frame_state) * fb_cnt, socket_id); + if (!ctx->frame_state) { + err("%s, failed to alloc frame_state array (%u entries)\n", __func__, fb_cnt); + return -ENOMEM; + } + ctx->frame_cnt = fb_cnt; + for (uint16_t i = 0; i < fb_cnt; i++) { + ctx->frame_state[i] = TX_FRAME_FREE; + } + return 0; +} + +/** + * Cleanup all resources on init failure. + */ +static void tx_cleanup_on_failure(struct video_tx_ctx* ctx) { + if (ctx->handle) { + st20_tx_free(ctx->handle); + ctx->handle = NULL; + } + if (ctx->frame_state) { + mt_rte_free(ctx->frame_state); + ctx->frame_state = NULL; + } + video_convert_bufs_free(&ctx->convert); + mt_rte_free(ctx); +} + +/************************************************************************* + * Session Initialization + *************************************************************************/ + +int mtl_video_tx_session_init(struct mtl_session_impl* s, struct mtl_main_impl* impl, + const mtl_video_config_t* config) { + int ret; + + /* Allocate callback context */ + struct video_tx_ctx* ctx = mt_rte_zmalloc_socket(sizeof(*ctx), s->socket_id); + if (!ctx) { + err("%s, failed to alloc ctx\n", __func__); + return -ENOMEM; + } + ctx->session = s; + + /* Initialize format conversion (shared helper) */ + ret = video_convert_ctx_init(&ctx->convert, config, true /* TX */); + if (ret < 0) { + mt_rte_free(ctx); + return ret; + } + s->video.frame_fmt = ctx->convert.frame_fmt; + s->video.derive = ctx->convert.derive; + + /* Build st20_tx_ops from config */ + struct st20_tx_ops ops; + memset(&ops, 0, sizeof(ops)); + + tx_fill_port_config(&ops, config); + tx_fill_video_format(&ops, config); + + ops.name = config->base.name; + ops.priv = ctx; + ops.framebuff_cnt = config->base.num_buffers; + if (ops.framebuff_cnt < 2) ops.framebuff_cnt = 2; + + /* Mode: frame vs slice */ + if (config->mode == MTL_VIDEO_MODE_SLICE) { + ops.type = ST20_TYPE_SLICE_LEVEL; + if (config->query_lines_ready) { + ctx->user_query_lines_ready = config->query_lines_ready; + ctx->user_priv = config->base.priv; + ops.query_frame_lines_ready = video_tx_query_lines_ready_wrapper; + } + } else { + ops.type = ST20_TYPE_FRAME_LEVEL; + } + + /* Core TX callbacks */ + ops.get_next_frame = video_tx_get_next_frame; + ops.notify_frame_done = video_tx_notify_frame_done; + ops.notify_frame_late = video_tx_notify_frame_late; + + tx_apply_session_flags(&ops, config, ctx); + + /* Allocate per-frame state tracking BEFORE st20_tx_create, because the + * scheduler may call video_tx_get_next_frame as soon as the handle exists. */ + uint16_t fb_cnt = ops.framebuff_cnt; + ret = tx_alloc_frame_state(ctx, fb_cnt, s->socket_id); + if (ret < 0) { + video_convert_bufs_free(&ctx->convert); + mt_rte_free(ctx); + return ret; + } + + /* Initialize user-owned buffer management before create too */ + if (s->ownership == MTL_BUFFER_USER_OWNED) { + ret = mtl_session_user_buf_init(s, fb_cnt); + if (ret < 0) { + err("%s(%s), user_buf_init failed: %d\n", __func__, s->name, ret); + tx_cleanup_on_failure(ctx); + return ret; + } + } + + /* Create the low-level TX session */ + st20_tx_handle handle = st20_tx_create(impl, &ops); + if (!handle) { + err("%s(%s), st20_tx_create failed\n", __func__, s->name); + tx_cleanup_on_failure(ctx); + return -EIO; + } + + ctx->handle = handle; + ctx->convert.transport_frame_size = st20_tx_get_framebuffer_size(handle); + + /* Link inner session implementation */ + struct st_tx_video_session_handle_impl* handle_impl = + (struct st_tx_video_session_handle_impl*)handle; + s->inner.video_tx = handle_impl->impl; + s->idx = s->inner.video_tx->idx; + + /* Allocate conversion buffers if needed (shared helper) */ + if (!ctx->convert.derive) { + ret = video_convert_bufs_alloc(&ctx->convert, fb_cnt, s->socket_id); + if (ret < 0) { + s->inner.video_tx = NULL; + tx_cleanup_on_failure(ctx); + return ret; + } + } + + info("%s(%d), transport fmt %s, input fmt: %s, frame_size %zu, fb_cnt %u, derive %d\n", + __func__, s->idx, st20_fmt_name(config->transport_fmt), + st_frame_fmt_name(config->frame_fmt), + ctx->convert.transport_frame_size, ops.framebuff_cnt, + ctx->convert.derive); + + return 0; +} + +void mtl_video_tx_session_uinit(struct mtl_session_impl* s) { + video_tx_destroy(s); +} From c67d3a1e12c0e76511954df788e4f66524947629 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 8 Jun 2026 15:51:09 +0000 Subject: [PATCH 02/16] new_api: migrate RxTxApp st20p tx/rx to unified session API Re-apply the new_api_draft migration of the st20p RxTxApp paths on top of main, routing frame get/put/create/destroy through mtl_session_* instead of st20p_*. Preserve main's per-frame pkts modulo-gated dbg logging and the app_rx_st20p_result wire-stats reconciliation block (ported to mtl_session_io_stats_get). app_base.h st20p tx/rx structs now hold mtl_session_t* session. Deferred: ST20P_TX_FLAG_DROP_WHEN_LATE has no new-API flag yet (feature gap). --- tests/tools/RxTxApp/src/app_base.h | 5 +- tests/tools/RxTxApp/src/rx_st20p_app.c | 171 +++++++++++----------- tests/tools/RxTxApp/src/tx_st20p_app.c | 194 ++++++++++++------------- 3 files changed, 181 insertions(+), 189 deletions(-) diff --git a/tests/tools/RxTxApp/src/app_base.h b/tests/tools/RxTxApp/src/app_base.h index 4d666aa0b..25c6e42fc 100644 --- a/tests/tools/RxTxApp/src/app_base.h +++ b/tests/tools/RxTxApp/src/app_base.h @@ -10,6 +10,7 @@ #endif #include #include +#include #include #include #include @@ -570,7 +571,7 @@ struct st_app_tx_st20p_session { struct st_app_context* ctx; int idx; - st20p_tx_handle handle; + mtl_session_t* session; /* unified session handle */ mtl_handle st; int framebuff_cnt; int st20p_frame_size; @@ -602,7 +603,7 @@ struct st_app_tx_st20p_session { struct st_app_rx_st20p_session { struct st_app_context* ctx; int idx; - st20p_rx_handle handle; + mtl_session_t* session; /* unified session handle */ mtl_handle st; int framebuff_cnt; int st20p_frame_size; diff --git a/tests/tools/RxTxApp/src/rx_st20p_app.c b/tests/tools/RxTxApp/src/rx_st20p_app.c index c96664bb6..7ae51384d 100644 --- a/tests/tools/RxTxApp/src/rx_st20p_app.c +++ b/tests/tools/RxTxApp/src/rx_st20p_app.c @@ -5,7 +5,7 @@ #include "rx_st20p_app.h" static void app_rx_st20p_consume_frame(struct st_app_rx_st20p_session* s, - struct st_frame* frame) { + mtl_buffer_t* buf) { struct st_display* d = s->display; int idx = s->idx; @@ -18,7 +18,7 @@ static void app_rx_st20p_consume_frame(struct st_app_rx_st20p_session* s, __func__, idx, s->rx_file_bytes_written, max_size); s->rx_file_size_limit_reached = true; } else { - if (!fwrite(frame->addr[0], 1, s->st20p_frame_size, s->st20p_destination_file)) { + if (!fwrite(buf->data, 1, s->st20p_frame_size, s->st20p_destination_file)) { err("%s(%d), failed to write frame to file %s\n", __func__, idx, s->st20p_destination_url); } else { @@ -29,29 +29,29 @@ static void app_rx_st20p_consume_frame(struct st_app_rx_st20p_session* s, if (s->num_port > 1) { dbg("%s(%d): pkts_total %u, pkts per port P %u R %u\n", __func__, idx, - frame->pkts_total, frame->pkts_recv[MTL_SESSION_PORT_P], - frame->pkts_recv[MTL_SESSION_PORT_R]); + buf->video.pkts_total, buf->video.pkts_recv[MTL_SESSION_PORT_P], + buf->video.pkts_recv[MTL_SESSION_PORT_R]); if (s->stat_frame_total_received % 60 == 0 && - frame->pkts_recv[MTL_SESSION_PORT_P] < (frame->pkts_total / 2)) + buf->video.pkts_recv[MTL_SESSION_PORT_P] < (buf->video.pkts_total / 2)) dbg("%s(%d): P port only receive %u pkts while total pkts is %u\n", __func__, idx, - frame->pkts_recv[MTL_SESSION_PORT_P], frame->pkts_total); + buf->video.pkts_recv[MTL_SESSION_PORT_P], buf->video.pkts_total); if (s->stat_frame_total_received % 60 && - frame->pkts_recv[MTL_SESSION_PORT_R] < (frame->pkts_total / 2)) + buf->video.pkts_recv[MTL_SESSION_PORT_R] < (buf->video.pkts_total / 2)) dbg("%s(%d): R port only receive %u pkts while total pkts is %u\n", __func__, idx, - frame->pkts_recv[MTL_SESSION_PORT_R], frame->pkts_total); + buf->video.pkts_recv[MTL_SESSION_PORT_R], buf->video.pkts_total); } - if (frame->interlaced) { - dbg("%s(%d), %s field\n", __func__, s->idx, frame->second_field ? "second" : "first"); + if (buf->video.interlaced) { + dbg("%s(%d), %s field\n", __func__, s->idx, + buf->video.second_field ? "second" : "first"); } if (d && d->front_frame) { if (st_pthread_mutex_trylock(&d->display_frame_mutex) == 0) { - if (frame->fmt == ST_FRAME_FMT_YUV422RFC4175PG2BE10) { - st20_rfc4175_422be10_to_422le8(frame->addr[0], d->front_frame, s->width, - s->height); - } else if (frame->fmt == ST_FRAME_FMT_UYVY) { - mtl_memcpy(d->front_frame, frame->addr[0], d->front_frame_size); + if (buf->video.fmt == ST_FRAME_FMT_YUV422RFC4175PG2BE10) { + st20_rfc4175_422be10_to_422le8(buf->data, d->front_frame, s->width, s->height); + } else if (buf->video.fmt == ST_FRAME_FMT_UYVY) { + mtl_memcpy(d->front_frame, buf->data, d->front_frame_size); } else { st_pthread_mutex_unlock(&d->display_frame_mutex); return; @@ -66,15 +66,15 @@ static void app_rx_st20p_consume_frame(struct st_app_rx_st20p_session* s, static void* app_rx_st20p_frame_thread(void* arg) { struct st_app_rx_st20p_session* s = arg; - struct st_frame* frame; + mtl_buffer_t* buf; uint8_t shas[SHA256_DIGEST_LENGTH]; int idx = s->idx; info("%s(%d), start\n", __func__, s->idx); while (!s->st20p_app_thread_stop) { - frame = st20p_rx_get_frame(s->handle); - if (!frame) { /* no ready frame */ - warn("%s(%d), get frame time out\n", __func__, s->idx); + int ret = mtl_session_buffer_get(s->session, &buf, -1); /* blocking */ + if (ret < 0) { /* no ready buffer or stopped */ + warn("%s(%d), get buffer time out\n", __func__, s->idx); /* track consecutive timeouts for auto_stop */ if (s->ctx && s->ctx->auto_stop && s->rx_started) { s->rx_timeout_cnt++; @@ -101,27 +101,27 @@ static void* app_rx_st20p_frame_thread(void* arg) { uint64_t ptp_ns = mtl_ptp_read_time(s->st); uint32_t sampling_rate = 90 * 1000; - if (frame->tfmt == ST10_TIMESTAMP_FMT_MEDIA_CLK) { + if (buf->tfmt == ST10_TIMESTAMP_FMT_MEDIA_CLK) { uint32_t latency_media_clk = - st10_tai_to_media_clk(ptp_ns, sampling_rate) - frame->timestamp; + st10_tai_to_media_clk(ptp_ns, sampling_rate) - buf->timestamp; latency_ns = st10_media_clk_to_ns(latency_media_clk, sampling_rate); } else { - latency_ns = ptp_ns - frame->timestamp; + latency_ns = ptp_ns - buf->timestamp; } dbg("%s, latency_us %" PRIu64 "\n", __func__, latency_ns / 1000); s->stat_latency_us_sum += latency_ns / 1000; } - app_rx_st20p_consume_frame(s, frame); + app_rx_st20p_consume_frame(s, buf); if (s->sha_check) { - if (frame->user_meta_size != sizeof(shas)) { + if (buf->user_meta_size != sizeof(shas)) { err("%s(%d), invalid user meta size %" PRId64 "\n", __func__, idx, - frame->user_meta_size); + buf->user_meta_size); } else { - st_sha256((unsigned char*)frame->addr[0], st_frame_plane_size(frame, 0), shas); - if (memcmp(shas, frame->user_meta, sizeof(shas))) { - err("%s(%d), sha check fail for frame %p\n", __func__, idx, frame->addr); - st_sha_dump("user meta sha:", frame->user_meta); + st_sha256((unsigned char*)buf->data, buf->data_size, shas); + if (memcmp(shas, buf->user_meta, sizeof(shas))) { + err("%s(%d), sha check fail for frame %p\n", __func__, idx, buf->data); + st_sha_dump("user meta sha:", buf->user_meta); st_sha_dump("frame sha:", shas); } } @@ -130,7 +130,7 @@ static void* app_rx_st20p_frame_thread(void* arg) { if (!s->stat_frame_first_rx_time) s->stat_frame_first_rx_time = st_app_get_monotonic_time(); s->stat_frame_last_rx_time = st_app_get_monotonic_time(); - st20p_rx_put_frame(s->handle, frame); + mtl_session_buffer_put(s->session, buf); } info("%s(%d), stop\n", __func__, s->idx); @@ -166,14 +166,14 @@ static int app_rx_st20p_uinit(struct st_app_rx_st20p_session* s) { if (s->st20p_app_thread_stop) { /* wake up the thread */ info("%s(%d), wait app thread stop\n", __func__, idx); - if (s->handle) st20p_rx_wake_block(s->handle); + if (s->session) mtl_session_stop(s->session); if (s->st20p_app_thread) pthread_join(s->st20p_app_thread, NULL); } - if (s->handle) { - ret = st20p_rx_free(s->handle); - if (ret < 0) err("%s(%d), st20_rx_free fail %d\n", __func__, idx, ret); - s->handle = NULL; + if (s->session) { + ret = mtl_session_destroy(s->session); + if (ret < 0) err("%s(%d), mtl_session_destroy fail %d\n", __func__, idx, ret); + s->session = NULL; } if (s->st20p_destination_file) { @@ -192,10 +192,10 @@ static int app_rx_st20p_io_stat(struct st_app_rx_st20p_session* s) { int ret; struct st20_rx_user_stats stats; - if (!s->handle) return 0; + if (!s->session) return 0; for (uint8_t port = 0; port < s->num_port; port++) { - ret = st20p_rx_get_session_stats(s->handle, &stats); + ret = mtl_session_io_stats_get(s->session, &stats, sizeof(stats)); if (ret < 0) return ret; tx_rate_m = (double)stats.common.port[port].bytes * 8 / time_sec / MTL_STAT_M_UNIT; @@ -203,7 +203,7 @@ static int app_rx_st20p_io_stat(struct st_app_rx_st20p_session* s) { info("%s(%d,%u), rx %f Mb/s fps %f\n", __func__, idx, port, tx_rate_m, fps); } - st20p_rx_reset_session_stats(s->handle); + mtl_session_io_stats_reset(s->session); s->last_stat_time_ns = cur_time; return 0; @@ -213,44 +213,46 @@ static int app_rx_st20p_init(struct st_app_context* ctx, struct st_json_st20p_session* st20p, struct st_app_rx_st20p_session* s) { int idx = s->idx, ret; - struct st20p_rx_ops ops; + mtl_video_config_t config; char name[32]; - st20p_rx_handle handle; - memset(&ops, 0, sizeof(ops)); + mtl_session_t* session = NULL; + memset(&config, 0, sizeof(config)); s->ctx = ctx; s->last_stat_time_ns = st_app_get_monotonic_time(); s->sha_check = ctx->video_sha_check; snprintf(name, 32, "app_rx_st20p_%d", idx); - ops.name = name; - ops.priv = s; - ops.port.num_port = st20p ? st20p->base.num_inf : ctx->para.num_ports; - memcpy(ops.port.ip_addr[MTL_SESSION_PORT_P], + config.base.name = name; + config.base.priv = s; + config.base.direction = MTL_SESSION_RX; + config.rx_port.num_port = st20p ? st20p->base.num_inf : ctx->para.num_ports; + memcpy(config.rx_port.ip_addr[MTL_SESSION_PORT_P], st20p ? st_json_ip(ctx, &st20p->base, MTL_SESSION_PORT_P) : ctx->rx_ip_addr[MTL_PORT_P], MTL_IP_ADDR_LEN); memcpy( - ops.port.mcast_sip_addr[MTL_SESSION_PORT_P], + config.rx_port.mcast_sip_addr[MTL_SESSION_PORT_P], st20p ? st20p->base.mcast_src_ip[MTL_PORT_P] : ctx->rx_mcast_sip_addr[MTL_PORT_P], MTL_IP_ADDR_LEN); snprintf( - ops.port.port[MTL_SESSION_PORT_P], MTL_PORT_MAX_LEN, "%s", + config.rx_port.port[MTL_SESSION_PORT_P], MTL_PORT_MAX_LEN, "%s", st20p ? st20p->base.inf[MTL_SESSION_PORT_P]->name : ctx->para.port[MTL_PORT_P]); - ops.port.udp_port[MTL_SESSION_PORT_P] = st20p ? st20p->base.udp_port : (10000 + s->idx); - if (ops.port.num_port > 1) { - memcpy(ops.port.ip_addr[MTL_SESSION_PORT_R], + config.rx_port.udp_port[MTL_SESSION_PORT_P] = + st20p ? st20p->base.udp_port : (10000 + s->idx); + if (config.rx_port.num_port > 1) { + memcpy(config.rx_port.ip_addr[MTL_SESSION_PORT_R], st20p ? st_json_ip(ctx, &st20p->base, MTL_SESSION_PORT_R) : ctx->rx_ip_addr[MTL_PORT_R], MTL_IP_ADDR_LEN); memcpy( - ops.port.mcast_sip_addr[MTL_SESSION_PORT_R], + config.rx_port.mcast_sip_addr[MTL_SESSION_PORT_R], st20p ? st20p->base.mcast_src_ip[MTL_PORT_R] : ctx->rx_mcast_sip_addr[MTL_PORT_R], MTL_IP_ADDR_LEN); snprintf( - ops.port.port[MTL_SESSION_PORT_R], MTL_PORT_MAX_LEN, "%s", + config.rx_port.port[MTL_SESSION_PORT_R], MTL_PORT_MAX_LEN, "%s", st20p ? st20p->base.inf[MTL_SESSION_PORT_R]->name : ctx->para.port[MTL_PORT_R]); - ops.port.udp_port[MTL_SESSION_PORT_R] = + config.rx_port.udp_port[MTL_SESSION_PORT_R] = st20p ? st20p->base.udp_port : (10000 + s->idx); } @@ -266,37 +268,38 @@ static int app_rx_st20p_init(struct st_app_context* ctx, } } - ops.width = st20p ? st20p->info.width : 1920; - ops.height = st20p ? st20p->info.height : 1080; - ops.fps = st20p ? st20p->info.fps : ST_FPS_P59_94; - ops.interlaced = st20p ? st20p->info.interlaced : false; - ops.output_fmt = st20p ? st20p->info.format : ST_FRAME_FMT_YUV422RFC4175PG2BE10; - ops.transport_fmt = st20p ? st20p->info.transport_format : ST20_FMT_YUV_422_10BIT; - ops.port.payload_type = st20p ? st20p->base.payload_type : ST_APP_PAYLOAD_TYPE_VIDEO; - ops.device = st20p ? st20p->info.device : ST_PLUGIN_DEVICE_AUTO; - ops.flags |= ST20P_RX_FLAG_BLOCK_GET; - ops.rx_burst_size = ctx->rx_burst_size; - ops.framebuff_cnt = s->framebuff_cnt; + config.width = st20p ? st20p->info.width : 1920; + config.height = st20p ? st20p->info.height : 1080; + config.fps = st20p ? st20p->info.fps : ST_FPS_P59_94; + config.interlaced = st20p ? st20p->info.interlaced : false; + config.frame_fmt = st20p ? st20p->info.format : ST_FRAME_FMT_YUV422RFC4175PG2BE10; + config.transport_fmt = st20p ? st20p->info.transport_format : ST20_FMT_YUV_422_10BIT; + config.rx_port.payload_type = + st20p ? st20p->base.payload_type : ST_APP_PAYLOAD_TYPE_VIDEO; + config.plugin_device = st20p ? st20p->info.device : ST_PLUGIN_DEVICE_AUTO; + config.base.flags |= MTL_SESSION_FLAG_BLOCK_GET; + config.rx_burst_size = ctx->rx_burst_size; + config.base.num_buffers = s->framebuff_cnt; /* always try to enable DMA offload */ - ops.flags |= ST20P_RX_FLAG_DMA_OFFLOAD; - if (st20p && st20p->enable_rtcp) ops.flags |= ST20P_RX_FLAG_ENABLE_RTCP; - if (ctx->enable_timing_parser) ops.flags |= ST20P_RX_FLAG_TIMING_PARSER_STAT; - if (ctx->rx_video_multi_thread) ops.flags |= ST20P_RX_FLAG_USE_MULTI_THREADS; - if (ctx->enable_hdr_split) ops.flags |= ST20P_RX_FLAG_HDR_SPLIT; + config.base.flags |= MTL_SESSION_FLAG_DMA_OFFLOAD; + if (st20p && st20p->enable_rtcp) config.base.flags |= MTL_SESSION_FLAG_ENABLE_RTCP; + if (ctx->enable_timing_parser) config.enable_timing_parser = true; + if (ctx->rx_video_multi_thread) config.base.flags |= MTL_SESSION_FLAG_USE_MULTI_THREADS; + if (ctx->enable_hdr_split) config.base.flags |= MTL_SESSION_FLAG_HDR_SPLIT; if (ctx->force_rx_video_numa >= 0) { - ops.flags |= ST20P_RX_FLAG_FORCE_NUMA; - ops.socket_id = ctx->force_rx_video_numa; + config.base.flags |= MTL_SESSION_FLAG_FORCE_NUMA; + config.base.socket_id = ctx->force_rx_video_numa; } - s->width = ops.width; - s->height = ops.height; - if (ops.interlaced) { + s->width = config.width; + s->height = config.height; + if (config.interlaced) { s->height >>= 1; } - s->num_port = ops.port.num_port; + s->num_port = config.rx_port.num_port; s->pcapng_max_pkts = ctx->pcapng_max_pkts; - s->expect_fps = st_frame_rate(ops.fps); + s->expect_fps = st_frame_rate(config.fps); if ((st20p && st20p->display) || ctx->rx_display) { struct st_display* d = st_app_zmalloc(sizeof(struct st_display)); @@ -311,15 +314,15 @@ static int app_rx_st20p_init(struct st_app_context* ctx, s->measure_latency = st20p ? st20p->measure_latency : true; - handle = st20p_rx_create(ctx->st, &ops); - if (!handle) { - err("%s(%d), st20_rx_create fail\n", __func__, idx); + ret = mtl_video_session_create(ctx->st, &config, &session); + if (ret < 0) { + err("%s(%d), mtl_video_session_create fail %d\n", __func__, idx, ret); app_rx_st20p_uinit(s); return -EIO; } - s->handle = handle; + s->session = session; - s->st20p_frame_size = st20p_rx_frame_size(handle); + s->st20p_frame_size = mtl_session_get_frame_size(session); ret = app_rx_st20p_init_frame_thread(s); if (ret < 0) { @@ -374,9 +377,9 @@ static int app_rx_st20p_result(struct st_app_rx_st20p_session* s) { * and classify the cause from already-published stats so the user can tell * whether frames were dropped due to packet loss, protocol drops, slow * consumer, etc. All numbers come from existing counters; we only print. */ - if (s->handle) { + if (s->session) { struct st20_rx_user_stats wire = {0}; - if (st20p_rx_get_session_stats(s->handle, &wire) == 0) { + if (mtl_session_io_stats_get(s->session, &wire, sizeof(wire)) == 0) { bool wire_trouble = wire.common.stat_pkts_unrecovered || wire.common.stat_lost_packets || wire.stat_frames_incomplete || wire.stat_pkts_idx_dropped || wire.stat_pkts_offset_dropped || @@ -419,7 +422,7 @@ static int app_rx_st20p_result(struct st_app_rx_st20p_session* s) { static int app_rx_st20p_pcap(struct st_app_rx_st20p_session* s) { if (s->pcapng_max_pkts) - st20p_rx_pcapng_dump(s->handle, s->pcapng_max_pkts, false, NULL); + mtl_session_pcap_dump(s->session, s->pcapng_max_pkts, false, NULL); return 0; } diff --git a/tests/tools/RxTxApp/src/tx_st20p_app.c b/tests/tools/RxTxApp/src/tx_st20p_app.c index f2b18c1b3..767477310 100644 --- a/tests/tools/RxTxApp/src/tx_st20p_app.c +++ b/tests/tools/RxTxApp/src/tx_st20p_app.c @@ -5,16 +5,15 @@ #include "tx_st20p_app.h" static void app_tx_st20p_display_frame(struct st_app_tx_st20p_session* s, - struct st_frame* frame) { + mtl_buffer_t* buf) { struct st_display* d = s->display; if (d && d->front_frame) { if (st_pthread_mutex_trylock(&d->display_frame_mutex) == 0) { - if (frame->fmt == ST_FRAME_FMT_YUV422RFC4175PG2BE10) { - st20_rfc4175_422be10_to_422le8(frame->addr[0], d->front_frame, s->width, - s->height); - } else if (frame->fmt == ST_FRAME_FMT_UYVY) { - mtl_memcpy(d->front_frame, frame->addr[0], d->front_frame_size); + if (buf->video.fmt == ST_FRAME_FMT_YUV422RFC4175PG2BE10) { + st20_rfc4175_422be10_to_422le8(buf->data, d->front_frame, s->width, s->height); + } else if (buf->video.fmt == ST_FRAME_FMT_UYVY) { + mtl_memcpy(d->front_frame, buf->data, d->front_frame_size); } else { st_pthread_mutex_unlock(&d->display_frame_mutex); return; @@ -27,26 +26,12 @@ static void app_tx_st20p_display_frame(struct st_app_tx_st20p_session* s, } } -static int app_tx_st20p_notify_event(void* priv, enum st_event event, void* args) { - struct st_app_tx_st20p_session* s = priv; - if (event == ST_EVENT_VSYNC) { - struct st10_vsync_meta* meta = args; - info("%s(%d), epoch %" PRIu64 "\n", __func__, s->idx, meta->epoch); - } else if (event == ST_EVENT_FATAL_ERROR) { - err("%s(%d), ST_EVENT_FATAL_ERROR\n", __func__, s->idx); - /* add a exist routine */ - } else if (event == ST_EVENT_RECOVERY_ERROR) { - info("%s(%d), ST_EVENT_RECOVERY_ERROR\n", __func__, s->idx); - } - return 0; -} - -static void app_tx_st20p_build_frame(struct st_app_tx_st20p_session* s, - struct st_frame* frame, size_t frame_size) { +static void app_tx_st20p_build_frame(struct st_app_tx_st20p_session* s, mtl_buffer_t* buf, + size_t frame_size) { uint8_t* src = s->st20p_frame_cursor; if (!s->ctx->tx_copy_once || !s->st20p_frames_copied) { - mtl_memcpy(frame->addr[0], src, frame_size); + mtl_memcpy(buf->data, src, frame_size); } /* point to next frame */ s->st20p_frame_cursor += frame_size; @@ -60,14 +45,14 @@ static void app_tx_st20p_build_frame(struct st_app_tx_st20p_session* s, } } - app_tx_st20p_display_frame(s, frame); + app_tx_st20p_display_frame(s, buf); } static void* app_tx_st20p_frame_thread(void* arg) { struct st_app_tx_st20p_session* s = arg; - st20p_tx_handle handle = s->handle; + mtl_session_t* session = s->session; int idx = s->idx; - struct st_frame* frame; + mtl_buffer_t* buf; uint8_t shas[SHA256_DIGEST_LENGTH]; double frame_time; @@ -81,28 +66,28 @@ static void* app_tx_st20p_frame_thread(void* arg) { break; } - frame = st20p_tx_get_frame(handle); - if (!frame) { /* no ready frame */ - warn("%s(%d), get frame time out\n", __func__, s->idx); + int ret = mtl_session_buffer_get(session, &buf, -1); /* blocking */ + if (ret < 0) { /* no ready buffer or stopped */ + warn("%s(%d), get buffer time out\n", __func__, s->idx); continue; } - app_tx_st20p_build_frame(s, frame, s->st20p_frame_size); + app_tx_st20p_build_frame(s, buf, s->st20p_frame_size); if (s->sha_check) { - st_sha256((unsigned char*)frame->addr[0], st_frame_plane_size(frame, 0), shas); - frame->user_meta = shas; - frame->user_meta_size = sizeof(shas); + st_sha256((unsigned char*)buf->data, s->st20p_frame_size, shas); + buf->user_meta = shas; + buf->user_meta_size = sizeof(shas); } if (s->user_time) { bool restart_base_time = !s->local_tai_base_time; - frame->timestamp = st_app_user_time(s->ctx, s->user_time, s->frame_num, frame_time, - restart_base_time); - frame->tfmt = ST10_TIMESTAMP_FMT_TAI; + buf->timestamp = st_app_user_time(s->ctx, s->user_time, s->frame_num, frame_time, + restart_base_time); + buf->tfmt = ST10_TIMESTAMP_FMT_TAI; s->frame_num++; s->local_tai_base_time = s->user_time->base_tai_time; } - st20p_tx_put_frame(handle, frame); + mtl_session_buffer_put(session, buf); } info("%s(%d), stop\n", __func__, idx); @@ -177,7 +162,7 @@ static void app_tx_st20p_stop_source(struct st_app_tx_st20p_session* s) { s->st20p_app_thread_stop = true; if (s->st20p_app_thread) { info("%s(%d), wait app thread stop\n", __func__, s->idx); - if (s->handle) st20p_tx_wake_block(s->handle); + if (s->session) mtl_session_stop(s->session); pthread_join(s->st20p_app_thread, NULL); s->st20p_app_thread = 0; } @@ -201,10 +186,10 @@ static int app_tx_st20p_handle_free(struct st_app_tx_st20p_session* s) { int ret; int idx = s->idx; - if (s->handle) { - ret = st20p_tx_free(s->handle); - if (ret < 0) err("%s(%d), st20p_tx_free fail %d\n", __func__, idx, ret); - s->handle = NULL; + if (s->session) { + ret = mtl_session_destroy(s->session); + if (ret < 0) err("%s(%d), mtl_session_destroy fail %d\n", __func__, idx, ret); + s->session = NULL; } return 0; @@ -231,17 +216,17 @@ static int app_tx_st20p_io_stat(struct st_app_tx_st20p_session* s) { int ret; struct st20_tx_user_stats stats; - if (!s->handle) return 0; + if (!s->session) return 0; for (uint8_t port = 0; port < s->num_port; port++) { - ret = st20p_tx_get_session_stats(s->handle, &stats); + ret = mtl_session_io_stats_get(s->session, &stats, sizeof(stats)); if (ret < 0) return ret; tx_rate_m = (double)stats.common.port[port].bytes * 8 / time_sec / MTL_STAT_M_UNIT; fps = (double)stats.common.port[port].frames / time_sec; info("%s(%d,%u), tx %f Mb/s fps %f\n", __func__, idx, port, tx_rate_m, fps); } - st20p_tx_reset_session_stats(s->handle); + mtl_session_io_stats_reset(s->session); s->last_stat_time_ns = cur_time; return 0; @@ -250,75 +235,81 @@ static int app_tx_st20p_io_stat(struct st_app_tx_st20p_session* s) { static int app_tx_st20p_init(struct st_app_context* ctx, st_json_st20p_session_t* st20p, struct st_app_tx_st20p_session* s) { int idx = s->idx, ret; - struct st20p_tx_ops ops; + mtl_video_config_t config; char name[32]; - st20p_tx_handle handle; - memset(&ops, 0, sizeof(ops)); + mtl_session_t* session; + memset(&config, 0, sizeof(config)); s->ctx = ctx; s->last_stat_time_ns = st_app_get_monotonic_time(); s->sha_check = ctx->video_sha_check; snprintf(name, 32, "app_tx_st20p_%d", idx); - ops.name = name; - ops.priv = s; - ops.port.num_port = st20p ? st20p->base.num_inf : ctx->para.num_ports; - memcpy(ops.port.dip_addr[MTL_SESSION_PORT_P], + config.base.name = name; + config.base.priv = s; + config.base.direction = MTL_SESSION_TX; + + /* Port config */ + config.tx_port.num_port = st20p ? st20p->base.num_inf : ctx->para.num_ports; + memcpy(config.tx_port.dip_addr[MTL_SESSION_PORT_P], st20p ? st_json_ip(ctx, &st20p->base, MTL_SESSION_PORT_P) : ctx->tx_dip_addr[MTL_PORT_P], MTL_IP_ADDR_LEN); snprintf( - ops.port.port[MTL_SESSION_PORT_P], MTL_PORT_MAX_LEN, "%s", + config.tx_port.port[MTL_SESSION_PORT_P], MTL_PORT_MAX_LEN, "%s", st20p ? st20p->base.inf[MTL_SESSION_PORT_P]->name : ctx->para.port[MTL_PORT_P]); - ops.port.udp_port[MTL_SESSION_PORT_P] = st20p ? st20p->base.udp_port : (10000 + s->idx); + config.tx_port.udp_port[MTL_SESSION_PORT_P] = + st20p ? st20p->base.udp_port : (10000 + s->idx); if (ctx->has_tx_dst_mac[MTL_PORT_P]) { - memcpy(&ops.tx_dst_mac[MTL_SESSION_PORT_P][0], ctx->tx_dst_mac[MTL_PORT_P], + memcpy(&config.tx_dst_mac[MTL_SESSION_PORT_P][0], ctx->tx_dst_mac[MTL_PORT_P], MTL_MAC_ADDR_LEN); - ops.flags |= ST20P_TX_FLAG_USER_P_MAC; + config.base.flags |= MTL_SESSION_FLAG_USER_P_MAC; } - if (ops.port.num_port > 1) { - memcpy(ops.port.dip_addr[MTL_SESSION_PORT_R], + if (config.tx_port.num_port > 1) { + memcpy(config.tx_port.dip_addr[MTL_SESSION_PORT_R], st20p ? st_json_ip(ctx, &st20p->base, MTL_SESSION_PORT_R) : ctx->tx_dip_addr[MTL_PORT_R], MTL_IP_ADDR_LEN); snprintf( - ops.port.port[MTL_SESSION_PORT_R], MTL_PORT_MAX_LEN, "%s", + config.tx_port.port[MTL_SESSION_PORT_R], MTL_PORT_MAX_LEN, "%s", st20p ? st20p->base.inf[MTL_SESSION_PORT_R]->name : ctx->para.port[MTL_PORT_R]); - ops.port.udp_port[MTL_SESSION_PORT_R] = + config.tx_port.udp_port[MTL_SESSION_PORT_R] = st20p ? st20p->base.udp_port : (10000 + s->idx); if (ctx->has_tx_dst_mac[MTL_PORT_R]) { - memcpy(&ops.tx_dst_mac[MTL_SESSION_PORT_R][0], ctx->tx_dst_mac[MTL_PORT_R], + memcpy(&config.tx_dst_mac[MTL_SESSION_PORT_R][0], ctx->tx_dst_mac[MTL_PORT_R], MTL_MAC_ADDR_LEN); - ops.flags |= ST20P_TX_FLAG_USER_R_MAC; + config.base.flags |= MTL_SESSION_FLAG_USER_R_MAC; } } - ops.port.payload_type = st20p ? st20p->base.payload_type : ST_APP_PAYLOAD_TYPE_VIDEO; - ops.width = st20p ? st20p->info.width : 1920; - ops.height = st20p ? st20p->info.height : 1080; - ops.fps = st20p ? st20p->info.fps : ST_FPS_P59_94; - ops.interlaced = st20p ? st20p->info.interlaced : false; - ops.input_fmt = st20p ? st20p->info.format : ST_FRAME_FMT_YUV422RFC4175PG2BE10; - ops.transport_pacing = st20p ? st20p->info.transport_pacing : ST21_PACING_NARROW; + config.tx_port.payload_type = + st20p ? st20p->base.payload_type : ST_APP_PAYLOAD_TYPE_VIDEO; + + /* Video format */ + config.width = st20p ? st20p->info.width : 1920; + config.height = st20p ? st20p->info.height : 1080; + config.fps = st20p ? st20p->info.fps : ST_FPS_P59_94; + config.interlaced = st20p ? st20p->info.interlaced : false; + config.frame_fmt = st20p ? st20p->info.format : ST_FRAME_FMT_YUV422RFC4175PG2BE10; + config.pacing = st20p ? st20p->info.transport_pacing : ST21_PACING_NARROW; if (ctx->tx_pacing_type) /* override if args has pacing defined */ - ops.transport_pacing = ctx->tx_pacing_type; - ops.transport_packing = st20p ? st20p->info.transport_packing : ST20_PACKING_BPM; - ops.transport_fmt = st20p ? st20p->info.transport_format : ST20_FMT_YUV_422_10BIT; - ops.device = st20p ? st20p->info.device : ST_PLUGIN_DEVICE_AUTO; - ops.framebuff_cnt = 2; - ops.flags |= ST20P_TX_FLAG_BLOCK_GET; - ops.start_vrx = ctx->tx_start_vrx; - ops.pad_interval = ctx->tx_pad_interval; - ops.rtp_timestamp_delta_us = ctx->tx_ts_delta_us; - ops.notify_event = app_tx_st20p_notify_event; - if (ctx->tx_static_pad) ops.flags |= ST20P_TX_FLAG_ENABLE_STATIC_PAD_P; - if (st20p && st20p->enable_rtcp) ops.flags |= ST20P_TX_FLAG_ENABLE_RTCP; + config.pacing = ctx->tx_pacing_type; + config.packing = st20p ? st20p->info.transport_packing : ST20_PACKING_BPM; + config.transport_fmt = st20p ? st20p->info.transport_format : ST20_FMT_YUV_422_10BIT; + config.plugin_device = st20p ? st20p->info.device : ST_PLUGIN_DEVICE_AUTO; + config.base.num_buffers = 2; + config.base.flags |= MTL_SESSION_FLAG_BLOCK_GET; + config.start_vrx = ctx->tx_start_vrx; + config.pad_interval = ctx->tx_pad_interval; + config.rtp_timestamp_delta_us = ctx->tx_ts_delta_us; + if (ctx->tx_static_pad) config.base.flags |= MTL_SESSION_FLAG_STATIC_PAD_P; + if (st20p && st20p->enable_rtcp) config.base.flags |= MTL_SESSION_FLAG_ENABLE_RTCP; if (st20p && (st20p->user_timestamp || st20p->user_pacing)) { if (st20p->user_pacing) { - ops.flags |= ST20P_TX_FLAG_USER_PACING; + config.base.flags |= MTL_SESSION_FLAG_USER_PACING; } if (st20p->user_timestamp) { - ops.flags |= ST20P_TX_FLAG_USER_TIMESTAMP; + config.base.flags |= MTL_SESSION_FLAG_USER_TIMESTAMP; } /* use global user time */ s->user_time = &ctx->user_time; @@ -327,42 +318,39 @@ static int app_tx_st20p_init(struct st_app_context* ctx, st_json_st20p_session_t } if (st20p && st20p->exact_user_pacing) { - /* should be active only with user_pacing */ - ops.flags |= ST20P_TX_FLAG_EXACT_USER_PACING; + config.base.flags |= MTL_SESSION_FLAG_EXACT_USER_PACING; } - if (st20p && st20p->drop_when_late) ops.flags |= ST20P_TX_FLAG_DROP_WHEN_LATE; - - if (ctx->tx_exact_user_pacing) ops.flags |= ST20P_TX_FLAG_EXACT_USER_PACING; - if (ctx->tx_ts_epoch) ops.flags |= ST20P_TX_FLAG_RTP_TIMESTAMP_EPOCH; - if (ctx->tx_no_bulk) ops.flags |= ST20P_TX_FLAG_DISABLE_BULK; + if (ctx->tx_exact_user_pacing) config.base.flags |= MTL_SESSION_FLAG_EXACT_USER_PACING; + if (ctx->tx_ts_epoch) config.base.flags |= MTL_SESSION_FLAG_RTP_TIMESTAMP_EPOCH; + if (ctx->tx_no_bulk) config.base.flags |= MTL_SESSION_FLAG_DISABLE_BULK; if (ctx->force_tx_video_numa >= 0) { - ops.flags |= ST20P_TX_FLAG_FORCE_NUMA; - ops.socket_id = ctx->force_tx_video_numa; + config.base.flags |= MTL_SESSION_FLAG_FORCE_NUMA; + config.base.socket_id = ctx->force_tx_video_numa; } - s->width = ops.width; - s->height = ops.height; - if (ops.interlaced) { + s->width = config.width; + s->height = config.height; + if (config.interlaced) { s->height >>= 1; } - s->num_port = ops.port.num_port; + s->num_port = config.tx_port.num_port; memcpy(s->st20p_source_url, st20p ? st20p->info.st20p_url : ctx->tx_st20p_url, ST_APP_URL_MAX_LEN); s->st = ctx->st; - s->expect_fps = st_frame_rate(ops.fps); + s->expect_fps = st_frame_rate(config.fps); - s->framebuff_cnt = ops.framebuff_cnt; + s->framebuff_cnt = config.base.num_buffers; s->st20p_source_fd = -1; - handle = st20p_tx_create(ctx->st, &ops); - if (!handle) { - err("%s(%d), st20p_tx_create fail\n", __func__, idx); + ret = mtl_video_session_create(ctx->st, &config, &session); + if (ret < 0) { + err("%s(%d), mtl_video_session_create fail %d\n", __func__, idx, ret); app_tx_st20p_uinit(s); return -EIO; } - s->handle = handle; - s->st20p_frame_size = st20p_tx_frame_size(handle); + s->session = session; + s->st20p_frame_size = mtl_session_get_frame_size(session); ret = app_tx_st20p_open_source(s); if (ret < 0) { From 633b123573f5581ffb741af87d8f86beaa256a54 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 9 Jun 2026 07:41:08 +0000 Subject: [PATCH 03/16] test: mirror st20 RX frame-count contract for new_api session Add tests/unit/new_api/ harness + suite (St20NewApiRxTest) mirroring the old-API pipeline suite (St20PipelineRxTest) so both the pipeline and the unified-session RX paths have parallel coverage of the same producer/consumer contract. Both suites pass (12/12); full UnitTest 268/268. Characterization tests of existing imported code (Gate-2 failing-first exempt). New-API adaptations vs pipeline: busy atomic -> removed (none in new_api); corrupted counter -> incomplete-delivery assertion; stats overlay -> abstract stats_get vs passthrough io_stats. --- tests/unit/meson.build | 2 + tests/unit/new_api/st20_rx_harness.c | 237 +++++++++++++++++++++++++++ tests/unit/new_api/st20_rx_harness.h | 76 +++++++++ tests/unit/new_api/st20_rx_test.cpp | 219 +++++++++++++++++++++++++ 4 files changed, 534 insertions(+) create mode 100644 tests/unit/new_api/st20_rx_harness.c create mode 100644 tests/unit/new_api/st20_rx_harness.h create mode 100644 tests/unit/new_api/st20_rx_test.cpp diff --git a/tests/unit/meson.build b/tests/unit/meson.build index 4f1091b81..36a5cae17 100644 --- a/tests/unit/meson.build +++ b/tests/unit/meson.build @@ -47,6 +47,8 @@ unit_sources = [ 'pipeline/st40p_test.cpp', 'pipeline/st20p_harness.c', 'pipeline/st20p_test.cpp', + 'new_api/st20_rx_harness.c', + 'new_api/st20_rx_test.cpp', 'main.cpp', ] diff --git a/tests/unit/new_api/st20_rx_harness.c b/tests/unit/new_api/st20_rx_harness.c new file mode 100644 index 000000000..b17d364b4 --- /dev/null +++ b/tests/unit/new_api/st20_rx_harness.c @@ -0,0 +1,237 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2026 Intel Corporation + * + * C harness for the NEW-API (unified session) video RX frame-count tests. + * + * Drives video_rx_notify_frame_ready() directly with a synthetic + * st20_rx_frame_meta — bypasses the transport session entirely. The + * unified-session counters under test (buffers_processed / buffers_dropped) + * are about producer/consumer flow, not packet semantics, so transport + * realism is not required. + * + * convert.derive=true so buffer_get takes the passthrough branch + * (pub->data = frame_trans->addr) and never touches a converter, mirroring + * how st20p_harness sets derive=true. + */ + +#include +#include + +/* + * Include the production RX .c so the static datapath/buffer helpers are + * reachable and our hand-built session is the one they operate on. Disable + * USDT to avoid linker references to probe semaphores. + */ +#undef MTL_HAS_USDT +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-variable" +#include "new_api/mt_session_video_rx.c" +#pragma GCC diagnostic pop + +#include "common/ut_common.h" + +/* + * Stubs for the transport libmtl symbols the RX .c calls on the paths we + * exercise. The real implementations dereference a live st20_rx handle (HW / + * a real session); ours own a fake handle, so --allow-multiple-definition lets + * these win. + */ + +int st20_rx_put_framebuff(st20_rx_handle handle, void* frame) { + (void)handle; + (void)frame; /* transport-side framebuf release is a no-op for the harness */ + return 0; +} + +int st20_rx_get_session_stats(st20_rx_handle handle, struct st20_rx_user_stats* stats) { + (void)handle; + /* io_stats_get is pure passthrough; return zeroed transport stats so the + * test can show it is a distinct surface from the abstract counters. */ + if (stats) memset(stats, 0, sizeof(*stats)); + return 0; +} + +int st20_rx_reset_session_stats(st20_rx_handle handle) { + (void)handle; + return 0; +} + +/* ── opaque context ───────────────────────────────────────────────────── */ + +struct ut20rx_ctx { + struct mtl_session_impl s; + struct st_rx_video_session_impl rx_impl; + struct video_rx_ctx vctx; + struct st_frame_trans* frames; + uint8_t* frame_storage; + int framebuff_cnt; + uint32_t enq_seq; /* advanced only on a successful enqueue */ +}; + +#include "new_api/st20_rx_harness.h" + +#define UT20RX_FRAME_STRIDE 64 /* per-frame sentinel storage, never read */ + +/* ── init ─────────────────────────────────────────────────────────────── */ + +int ut20rx_init(void) { + return ut_eal_init(); +} + +/* ── context create / destroy ─────────────────────────────────────────── */ + +ut20rx_ctx* ut20rx_ctx_create(int framebuff_cnt) { + static uint32_t ring_seq; + + ut20rx_ctx* ctx = calloc(1, sizeof(*ctx)); + if (!ctx) return NULL; + ctx->framebuff_cnt = framebuff_cnt; + + ctx->frames = calloc(framebuff_cnt, sizeof(struct st_frame_trans)); + ctx->frame_storage = calloc(framebuff_cnt, UT20RX_FRAME_STRIDE); + if (!ctx->frames || !ctx->frame_storage) { + free(ctx->frames); + free(ctx->frame_storage); + free(ctx); + return NULL; + } + for (int i = 0; i < framebuff_cnt; i++) { + ctx->frames[i].idx = i; + ctx->frames[i].addr = &ctx->frame_storage[i * UT20RX_FRAME_STRIDE]; + rte_atomic32_set(&ctx->frames[i].refcnt, 0); + } + + struct st_rx_video_session_impl* rx = &ctx->rx_impl; + rx->idx = 0; + rx->socket_id = rte_socket_id(); + rx->st20_frames = ctx->frames; + rx->st20_frames_cnt = framebuff_cnt; + + struct mtl_session_impl* s = &ctx->s; + s->vt = &mtl_video_rx_vtable; + s->magic = MTL_SESSION_MAGIC_VIDEO_RX; + s->type = MTL_TYPE_VIDEO; + s->direction = MTL_SESSION_RX; + s->socket_id = rte_socket_id(); + s->ownership = MTL_BUFFER_LIBRARY_OWNED; + s->stopped = 0; + s->event_ring = NULL; /* events are best-effort; post just returns -EINVAL */ + s->event_fd = -1; + s->inner.video_rx = rx; + snprintf(s->name, sizeof(s->name), "ut_new_rx"); + + s->buffer_count = framebuff_cnt; + s->buffers = calloc(framebuff_cnt, sizeof(struct mtl_buffer_impl)); + if (!s->buffers) { + free(ctx->frames); + free(ctx->frame_storage); + free(ctx); + return NULL; + } + for (int i = 0; i < framebuff_cnt; i++) { + s->buffers[i].session = s; + s->buffers[i].idx = i; + } + + struct video_rx_ctx* v = &ctx->vctx; + v->session = s; + v->handle = (st20_rx_handle)(uintptr_t)0x1; /* non-NULL sentinel */ + v->convert.derive = true; + v->convert.transport_frame_size = 1; + + char ring_name[RTE_RING_NAMESIZE]; + snprintf(ring_name, sizeof(ring_name), "utnrx_%u", ring_seq++); + /* EXACT_SZ: usable capacity == framebuff_cnt → deterministic overflow. */ + v->ready_ring = rte_ring_create(ring_name, framebuff_cnt, s->socket_id, + RING_F_SP_ENQ | RING_F_SC_DEQ | RING_F_EXACT_SZ); + if (!v->ready_ring) { + free(s->buffers); + free(ctx->frames); + free(ctx->frame_storage); + free(ctx); + return NULL; + } + + rx->ops.priv = v; /* rx_ctx_from_session() reads this */ + + return ctx; +} + +void ut20rx_ctx_destroy(ut20rx_ctx* ctx) { + if (!ctx) return; + if (ctx->vctx.ready_ring) rte_ring_free(ctx->vctx.ready_ring); + free(ctx->s.buffers); + free(ctx->frames); + free(ctx->frame_storage); + free(ctx); +} + +/* ── inject one frame via video_rx_notify_frame_ready ─────────────────── */ + +int ut20rx_inject_frame(ut20rx_ctx* ctx, enum st_frame_status status, + uint32_t timestamp) { + /* Pick the frame address from the cyclic pool. enq_seq advances only on a + * successful enqueue, so the live (undrained) window of the FIFO ready_ring + * always maps to distinct frame_trans slots. */ + void* frame = ctx->frames[ctx->enq_seq % ctx->framebuff_cnt].addr; + + struct st20_rx_frame_meta meta; + memset(&meta, 0, sizeof(meta)); + meta.status = status; + meta.timestamp = timestamp; + meta.rtp_timestamp = timestamp; + meta.frame_total_size = 1; + meta.frame_recv_size = 1; + meta.pkts_total = 1; + + uint64_t dropped_before = ctx->s.stats.buffers_dropped; + video_rx_notify_frame_ready(&ctx->vctx, frame, &meta); + + if (ctx->s.stats.buffers_dropped != dropped_before) { + /* ready_ring was full — frame returned to library, counted as dropped. */ + return -ENOSPC; + } + ctx->enq_seq++; + return 0; +} + +/* ── buffer get/put ───────────────────────────────────────────────────── */ + +mtl_buffer_t* ut20rx_buffer_get(ut20rx_ctx* ctx) { + mtl_buffer_t* buf = NULL; + int ret = mtl_video_rx_vtable.buffer_get(&ctx->s, &buf, 0 /* non-blocking */); + if (ret != 0) return NULL; + return buf; +} + +int ut20rx_buffer_put(ut20rx_ctx* ctx, mtl_buffer_t* buf) { + return mtl_video_rx_vtable.buffer_put(&ctx->s, buf); +} + +/* ── stat accessors ───────────────────────────────────────────────────── */ + +uint64_t ut20rx_buffers_processed(const ut20rx_ctx* ctx) { + return ctx->s.stats.buffers_processed; +} + +uint64_t ut20rx_buffers_dropped(const ut20rx_ctx* ctx) { + return ctx->s.stats.buffers_dropped; +} + +uint64_t ut20rx_bytes_processed(const ut20rx_ctx* ctx) { + return ctx->s.stats.bytes_processed; +} + +/* ── vtable stats surfaces ────────────────────────────────────────────── */ + +int ut20rx_stats_get(ut20rx_ctx* ctx, mtl_session_stats_t* stats) { + return mtl_video_rx_vtable.stats_get(&ctx->s, stats); +} + +int ut20rx_io_stats_get(ut20rx_ctx* ctx, struct st20_rx_user_stats* stats) { + return mtl_video_rx_vtable.io_stats_get(&ctx->s, stats, sizeof(*stats)); +} + +int ut20rx_reset_stats(ut20rx_ctx* ctx) { + return mtl_video_rx_vtable.stats_reset(&ctx->s); +} diff --git a/tests/unit/new_api/st20_rx_harness.h b/tests/unit/new_api/st20_rx_harness.h new file mode 100644 index 000000000..c112749fc --- /dev/null +++ b/tests/unit/new_api/st20_rx_harness.h @@ -0,0 +1,76 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2026 Intel Corporation + * + * C header for the NEW-API (unified session) video RX frame-count tests. + * + * Mirrors tests/unit/pipeline/st20p_harness.h, but pins the unified-session + * RX contract instead of the pipeline one: + * - s->stats.buffers_processed bumps in video_rx_buffer_get() (app consumes), + * NOT when the datapath enqueues a frame. + * - s->stats.buffers_dropped bumps in video_rx_notify_frame_ready() when the + * ready_ring is full (back-pressure; the frame is returned to the library). + * - a non-COMPLETE frame is still delivered by buffer_get with + * status MTL_FRAME_STATUS_INCOMPLETE and the MTL_BUF_FLAG_INCOMPLETE flag. + * + * Drives video_rx_notify_frame_ready() directly with synthetic meta — no + * transport session is stitched in. The unified-session counters are about + * producer/consumer accounting; transport realism is not required. + */ + +#ifndef _ST20_NEW_API_RX_HARNESS_H_ +#define _ST20_NEW_API_RX_HARNESS_H_ + +#include +#include + +#include "mtl_session_api.h" +#include "st20_api.h" +#include "st_api.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct ut20rx_ctx ut20rx_ctx; + +int ut20rx_init(void); + +/** Create an RX test context whose ready_ring holds exactly framebuff_cnt + * frames (so overflow → buffers_dropped is deterministic). */ +ut20rx_ctx* ut20rx_ctx_create(int framebuff_cnt); +void ut20rx_ctx_destroy(ut20rx_ctx* ctx); + +/** Inject one synthetic frame as if the datapath just received it. + * status is an enum st_frame_status (COMPLETE / CORRUPTED / ...). + * Returns 0 when enqueued onto the ready_ring, or -ENOSPC when the ring was + * full (drives the buffers_dropped path). */ +int ut20rx_inject_frame(ut20rx_ctx* ctx, enum st_frame_status status, uint32_t timestamp); + +/** Wraps video_rx_buffer_get() with timeout_ms=0 (non-blocking). */ +mtl_buffer_t* ut20rx_buffer_get(ut20rx_ctx* ctx); + +/** Wraps video_rx_buffer_put(). */ +int ut20rx_buffer_put(ut20rx_ctx* ctx, mtl_buffer_t* buf); + +/* ── stat accessors (raw s->stats fields) ─────────────────────────────── */ + +uint64_t ut20rx_buffers_processed(const ut20rx_ctx* ctx); +uint64_t ut20rx_buffers_dropped(const ut20rx_ctx* ctx); +uint64_t ut20rx_bytes_processed(const ut20rx_ctx* ctx); + +/* ── vtable stats surfaces ────────────────────────────────────────────── */ + +/** Wraps the abstract stats_get vtable entry (video_rx_stats_get). */ +int ut20rx_stats_get(ut20rx_ctx* ctx, mtl_session_stats_t* stats); + +/** Wraps the passthrough io_stats_get vtable entry (video_rx_io_stats_get). */ +int ut20rx_io_stats_get(ut20rx_ctx* ctx, struct st20_rx_user_stats* stats); + +/** Wraps the shared stats_reset vtable entry (video_session_stats_reset). */ +int ut20rx_reset_stats(ut20rx_ctx* ctx); + +#ifdef __cplusplus +} +#endif + +#endif /* _ST20_NEW_API_RX_HARNESS_H_ */ diff --git a/tests/unit/new_api/st20_rx_test.cpp b/tests/unit/new_api/st20_rx_test.cpp new file mode 100644 index 000000000..541fccbec --- /dev/null +++ b/tests/unit/new_api/st20_rx_test.cpp @@ -0,0 +1,219 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2026 Intel Corporation + * + * ST20 NEW-API (unified session) RX frame-count contract tests. + * + * Parallel copy of tests/unit/pipeline/st20p_test.cpp, adapted to the unified + * session API in lib/src/new_api. Old → new mapping: + * + * pipeline stat_frames_received ≙ s->stats.buffers_processed + * (bumped in buffer_get, NOT at datapath ingress). + * pipeline stat_frames_dropped ≙ s->stats.buffers_dropped + * (bumped in notify_frame_ready when the ready_ring is full). + * pipeline stat_busy → no equivalent. The unified API has no + * separate busy atomic; back-pressure is counted solely by buffers_dropped, + * which bumps 1:1 with each ready_ring overflow. + * pipeline stat_frames_corrupted → no equivalent. A non-COMPLETE frame is + * still delivered by buffer_get with status MTL_FRAME_STATUS_INCOMPLETE and + * the MTL_BUF_FLAG_INCOMPLETE flag, and still counts as processed. + * pipeline get_session_stats overlay → io_stats_get is pure transport + * passthrough; the abstract counters live on stats_get instead. + */ + +#include + +#include "new_api/st20_rx_harness.h" + +class St20NewApiRxTest : public ::testing::Test { + protected: + ut20rx_ctx* ctx_ = nullptr; + + void SetUp() override { + ASSERT_EQ(ut20rx_init(), 0) << "EAL init failed"; + ctx_ = ut20rx_ctx_create(/*framebuff_cnt=*/3); + ASSERT_NE(ctx_, nullptr); + } + + void TearDown() override { + ut20rx_ctx_destroy(ctx_); + ctx_ = nullptr; + } + + int inject_complete(uint32_t ts) { + return ut20rx_inject_frame(ctx_, ST_FRAME_STATUS_COMPLETE, ts); + } + int inject_corrupted(uint32_t ts) { + return ut20rx_inject_frame(ctx_, ST_FRAME_STATUS_CORRUPTED, ts); + } + mtl_buffer_t* get_buffer() { + return ut20rx_buffer_get(ctx_); + } + int put_buffer(mtl_buffer_t* b) { + return ut20rx_buffer_put(ctx_, b); + } + + uint64_t buffers_processed() { + return ut20rx_buffers_processed(ctx_); + } + uint64_t buffers_dropped() { + return ut20rx_buffers_dropped(ctx_); + } +}; + +/* The unified session counts a buffer as "processed" only when the application + * calls buffer_get. Inject N frames but consume only M < N → counter == M. + * (pipeline analog: FramesReceivedOnlyOnGetFrame.) */ +TEST_F(St20NewApiRxTest, BuffersProcessedOnlyOnBufferGet) { + ASSERT_EQ(inject_complete(1000), 0); + ASSERT_EQ(inject_complete(2000), 0); + ASSERT_EQ(inject_complete(3000), 0); + + EXPECT_EQ(buffers_processed(), 0u) + << "notify_frame_ready must not bump buffers_processed — only buffer_get does"; + + mtl_buffer_t* b = get_buffer(); + ASSERT_NE(b, nullptr); + EXPECT_EQ(buffers_processed(), 1u); + EXPECT_EQ(put_buffer(b), 0); + + /* second consume */ + b = get_buffer(); + ASSERT_NE(b, nullptr); + EXPECT_EQ(buffers_processed(), 2u); + EXPECT_EQ(put_buffer(b), 0); + + /* third frame still pending in the ready_ring — not yet counted */ + EXPECT_EQ(buffers_processed(), 2u); +} + +/* When the ready_ring is full (no buffer_get draining), every further + * notify_frame_ready must bump buffers_dropped 1:1 with the overflow event. + * (pipeline analog: FramesDroppedWhenFramebufsFull. The pipeline's + * stat_busy 1:1 invariant is dropped — the unified API has no busy atomic.) */ +TEST_F(St20NewApiRxTest, BuffersDroppedWhenReadyRingFull) { + /* fill all 3 ready_ring slots */ + ASSERT_EQ(inject_complete(1000), 0); + ASSERT_EQ(inject_complete(2000), 0); + ASSERT_EQ(inject_complete(3000), 0); + EXPECT_EQ(buffers_dropped(), 0u); + + /* further frames have nowhere to go */ + EXPECT_EQ(inject_complete(4000), -ENOSPC); + EXPECT_EQ(inject_complete(5000), -ENOSPC); + EXPECT_EQ(inject_complete(6000), -ENOSPC); + + EXPECT_EQ(buffers_dropped(), 3u); + EXPECT_EQ(buffers_processed(), 0u) << "dropped frames must not be counted as processed"; +} + +/* A non-COMPLETE (CORRUPTED) frame is still delivered to the application. + * Unlike the pipeline (which has a dedicated stat_frames_corrupted), the + * unified API surfaces corruption only through buffer status/flags: the buffer + * carries MTL_FRAME_STATUS_INCOMPLETE + MTL_BUF_FLAG_INCOMPLETE and still + * counts toward buffers_processed. COMPLETE frames stay COMPLETE/no-flag. + * (pipeline analog: CorruptedDeliveredAndCounted.) */ +TEST_F(St20NewApiRxTest, IncompleteDeliveredWithStatusFlag) { + ASSERT_EQ(inject_complete(1000), 0); + ASSERT_EQ(inject_corrupted(2000), 0); + ASSERT_EQ(inject_complete(3000), 0); + + mtl_buffer_t* b0 = get_buffer(); + ASSERT_NE(b0, nullptr); + EXPECT_EQ(b0->status, MTL_FRAME_STATUS_COMPLETE); + EXPECT_EQ(b0->flags & MTL_BUF_FLAG_INCOMPLETE, 0u); + EXPECT_EQ(put_buffer(b0), 0); + + mtl_buffer_t* b1 = get_buffer(); + ASSERT_NE(b1, nullptr); + EXPECT_EQ(b1->status, MTL_FRAME_STATUS_INCOMPLETE) + << "a non-COMPLETE frame must still be delivered, marked incomplete"; + EXPECT_NE(b1->flags & MTL_BUF_FLAG_INCOMPLETE, 0u); + EXPECT_EQ(put_buffer(b1), 0); + + mtl_buffer_t* b2 = get_buffer(); + ASSERT_NE(b2, nullptr); + EXPECT_EQ(b2->status, MTL_FRAME_STATUS_COMPLETE); + EXPECT_EQ(put_buffer(b2), 0); + + EXPECT_EQ(buffers_processed(), 3u) + << "every delivered frame, complete or not, counts as processed"; +} + +/* stats_reset clears every cumulative unified-session counter. + * (pipeline analog: ResetClearsAllPipelineCounters.) */ +TEST_F(St20NewApiRxTest, ResetClearsAllCounters) { + ASSERT_EQ(inject_complete(1000), 0); + ASSERT_EQ(inject_corrupted(2000), 0); + mtl_buffer_t* b1 = get_buffer(); + ASSERT_NE(b1, nullptr); + EXPECT_EQ(put_buffer(b1), 0); + mtl_buffer_t* b2 = get_buffer(); + ASSERT_NE(b2, nullptr); + EXPECT_EQ(put_buffer(b2), 0); + + /* fill up and overflow to exercise buffers_dropped */ + ASSERT_EQ(inject_complete(3000), 0); + ASSERT_EQ(inject_complete(4000), 0); + ASSERT_EQ(inject_complete(5000), 0); + EXPECT_EQ(inject_complete(6000), -ENOSPC); + + ASSERT_GT(buffers_processed(), 0u); + ASSERT_GT(buffers_dropped(), 0u); + ASSERT_GT(ut20rx_bytes_processed(ctx_), 0u); + + EXPECT_EQ(ut20rx_reset_stats(ctx_), 0); + + EXPECT_EQ(buffers_processed(), 0u); + EXPECT_EQ(buffers_dropped(), 0u); + EXPECT_EQ(ut20rx_bytes_processed(ctx_), 0u); +} + +/* The unified API exposes two distinct stats surfaces: + * - stats_get → the abstract counters this suite drives. + * - io_stats_get → a pure passthrough of the transport's own stats. + * Unlike the pipeline (which OVERLAYS the frame counters onto the transport + * stats), the two surfaces are independent here. Assert that stats_get matches + * what the test drove, while the passthrough io_stats returns the (stubbed) + * zeroed transport stats. (pipeline analog: GetSessionStatsOverlay.) */ +TEST_F(St20NewApiRxTest, StatsGetVsIoStatsPassthrough) { + ASSERT_EQ(inject_complete(1000), 0); + ASSERT_EQ(inject_corrupted(2000), 0); + ASSERT_EQ(inject_complete(3000), 0); + for (int i = 0; i < 3; i++) { + mtl_buffer_t* b = get_buffer(); + ASSERT_NE(b, nullptr); + EXPECT_EQ(put_buffer(b), 0); + } + /* fill + one overflow */ + ASSERT_EQ(inject_complete(4000), 0); + ASSERT_EQ(inject_complete(5000), 0); + ASSERT_EQ(inject_complete(6000), 0); + EXPECT_EQ(inject_complete(7000), -ENOSPC); + + mtl_session_stats_t abstract{}; + ASSERT_EQ(ut20rx_stats_get(ctx_, &abstract), 0); + EXPECT_EQ(abstract.buffers_processed, buffers_processed()); + EXPECT_EQ(abstract.buffers_dropped, buffers_dropped()); + + struct st20_rx_user_stats io {}; + ASSERT_EQ(ut20rx_io_stats_get(ctx_, &io), 0); + EXPECT_EQ(io.common.stat_frames_received, 0u) + << "io_stats_get is transport passthrough, not an overlay of the " + "abstract counters"; +} + +/* Invariant: buffers_dropped advances monotonically, exactly once per + * ready_ring overflow. Replaces the pipeline's BusyEqualsDroppedInvariant — + * the unified API has no busy atomic, so the 1:1 relationship is between the + * overflow event and buffers_dropped itself. */ +TEST_F(St20NewApiRxTest, DroppedMonotonicWithOverflow) { + /* fill */ + for (int i = 0; i < 3; i++) ASSERT_EQ(inject_complete(1000 + i), 0); + /* overflow burst */ + for (uint64_t i = 0; i < 5; i++) { + EXPECT_EQ(inject_complete(2000 + i), -ENOSPC); + EXPECT_EQ(buffers_dropped(), i + 1) + << "after overflow " << (i + 1) + << ": buffers_dropped must bump exactly once per event"; + } +} From daf7a167bf8c448d98a033f3708c9b0eb27db3f5 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 9 Jun 2026 08:35:35 +0000 Subject: [PATCH 04/16] new_api: address review on RxTxApp st20p migration - tx: emit warn() when st20p drop_when_late is set, so the unsupported unified-session option is an observable no-op instead of a silent one (reviewer MAJOR). - rx: hash s->st20p_frame_size in sha_check to match the TX side, instead of buf->data_size, so partial-recv / multi-plane frames stay symmetric (reviewer MINOR). --- tests/tools/RxTxApp/src/rx_st20p_app.c | 2 +- tests/tools/RxTxApp/src/tx_st20p_app.c | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/tools/RxTxApp/src/rx_st20p_app.c b/tests/tools/RxTxApp/src/rx_st20p_app.c index 7ae51384d..eca289cb9 100644 --- a/tests/tools/RxTxApp/src/rx_st20p_app.c +++ b/tests/tools/RxTxApp/src/rx_st20p_app.c @@ -118,7 +118,7 @@ static void* app_rx_st20p_frame_thread(void* arg) { err("%s(%d), invalid user meta size %" PRId64 "\n", __func__, idx, buf->user_meta_size); } else { - st_sha256((unsigned char*)buf->data, buf->data_size, shas); + st_sha256((unsigned char*)buf->data, s->st20p_frame_size, shas); if (memcmp(shas, buf->user_meta, sizeof(shas))) { err("%s(%d), sha check fail for frame %p\n", __func__, idx, buf->data); st_sha_dump("user meta sha:", buf->user_meta); diff --git a/tests/tools/RxTxApp/src/tx_st20p_app.c b/tests/tools/RxTxApp/src/tx_st20p_app.c index 767477310..0d37bd3e0 100644 --- a/tests/tools/RxTxApp/src/tx_st20p_app.c +++ b/tests/tools/RxTxApp/src/tx_st20p_app.c @@ -321,6 +321,10 @@ static int app_tx_st20p_init(struct st_app_context* ctx, st_json_st20p_session_t config.base.flags |= MTL_SESSION_FLAG_EXACT_USER_PACING; } + if (st20p && st20p->drop_when_late) + warn("%s(%d), drop_when_late not supported on unified session path, ignored\n", + __func__, idx); + if (ctx->tx_exact_user_pacing) config.base.flags |= MTL_SESSION_FLAG_EXACT_USER_PACING; if (ctx->tx_ts_epoch) config.base.flags |= MTL_SESSION_FLAG_RTP_TIMESTAMP_EPOCH; if (ctx->tx_no_bulk) config.base.flags |= MTL_SESSION_FLAG_DISABLE_BULK; From a693810cb54faca7c7296934593f8e0363b46087 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 9 Jun 2026 10:03:42 +0000 Subject: [PATCH 05/16] new_api: st20 TX drop-when-late + TX unit parity suite Add MTL_SESSION_FLAG_DROP_WHEN_LATE to the new unified session API for st20 video TX. Raw st20_api.h has no ST20_TX_FLAG_DROP_WHEN_LATE, so the late-drop is enforced in video_tx_get_next_frame: when the flag and USER_PACING are set, a READY frame whose TAI timestamp is more than one frame period in the past is recycled to FREE instead of transmitted, a buffers_dropped stat is bumped, and an MTL_EVENT_FRAME_LATE event is posted. Drops per call are capped at MT_VIDEO_TX_DROP_MAX_BATCH, matching the pipeline's ST_TX_DROP_MAX_BATCH, so a backlog cannot stall the datapath thread. The per-frame TAI is already stored in st20_frames[].tv_meta by tx_apply_buffer_metadata, so no new plumbing is needed. Add a new-API TX unit harness/test pair mirroring the existing RX harness, and an old-API st20 pipeline TX harness/test mirror so the two TX paths are pinned by parallel suites (lifecycle, user_meta passthrough, derive no-convert, stats surfaces, non-blocking get, framebuff count, and the three drop-when-late cases). Both suites pass under UnitTest. --- include/mtl_session_api.h | 1 + lib/src/new_api/mt_session_video_tx.c | 34 +++ tests/unit/meson.build | 4 + tests/unit/new_api/st20_tx_harness.c | 298 +++++++++++++++++++++++++ tests/unit/new_api/st20_tx_harness.h | 94 ++++++++ tests/unit/new_api/st20_tx_test.cpp | 251 +++++++++++++++++++++ tests/unit/pipeline/st20p_tx_harness.c | 230 +++++++++++++++++++ tests/unit/pipeline/st20p_tx_harness.h | 77 +++++++ tests/unit/pipeline/st20p_tx_test.cpp | 240 ++++++++++++++++++++ 9 files changed, 1229 insertions(+) create mode 100644 tests/unit/new_api/st20_tx_harness.c create mode 100644 tests/unit/new_api/st20_tx_harness.h create mode 100644 tests/unit/new_api/st20_tx_test.cpp create mode 100644 tests/unit/pipeline/st20p_tx_harness.c create mode 100644 tests/unit/pipeline/st20p_tx_harness.h create mode 100644 tests/unit/pipeline/st20p_tx_test.cpp diff --git a/include/mtl_session_api.h b/include/mtl_session_api.h index d0be430e6..b011b1a67 100644 --- a/include/mtl_session_api.h +++ b/include/mtl_session_api.h @@ -128,6 +128,7 @@ typedef enum { #define MTL_SESSION_FLAG_DISABLE_BULK (1 << 15) /**< TX: Disable bulk enqueue */ #define MTL_SESSION_FLAG_STATIC_PAD_P (1 << 16) /**< TX: Static padding for primary */ #define MTL_SESSION_FLAG_USE_MULTI_THREADS (1 << 17) /**< RX: Multi-thread processing */ +#define MTL_SESSION_FLAG_DROP_WHEN_LATE (1 << 18) /**< TX: Drop frame if late (requires USER_PACING) */ /************************************************************************* * Opaque Handles diff --git a/lib/src/new_api/mt_session_video_tx.c b/lib/src/new_api/mt_session_video_tx.c index f11993c2e..eead5441c 100644 --- a/lib/src/new_api/mt_session_video_tx.c +++ b/lib/src/new_api/mt_session_video_tx.c @@ -15,6 +15,10 @@ #include "../mt_log.h" #include "../mt_mem.h" +/* Cap on frames dropped per get_next_frame, mirroring the pipeline's + * ST_TX_DROP_MAX_BATCH, so a backlog cannot stall the datapath thread. */ +#define MT_VIDEO_TX_DROP_MAX_BATCH (80) + /************************************************************************* * TX Frame State Machine * @@ -54,6 +58,8 @@ struct video_tx_ctx { /** Per-frame state tracking (protected by session->buffer_lock) */ enum tx_frame_state* frame_state; uint16_t frame_cnt; + bool drop_when_late; /**< drop frames past their TX window (needs USER_PACING) */ + enum st_fps fps; /**< frame rate, for the late-drop grace window */ /* User slice callback (if any) */ int (*user_query_lines_ready)(void* priv, uint16_t frame_idx, uint16_t* lines_ready); @@ -69,6 +75,19 @@ static inline struct video_tx_ctx* tx_ctx_from_session(struct mtl_session_impl* return s->inner.video_tx->ops.priv; } +static bool tx_frame_is_late(struct video_tx_ctx* ctx, + struct st_tx_video_session_impl* tx_impl, + uint16_t idx) { + struct mtl_session_impl* s = ctx->session; + if (!(s->flags & MTL_SESSION_FLAG_USER_PACING)) return false; + struct st20_tx_frame_meta* meta = &tx_impl->st20_frames[idx].tv_meta; + if (meta->tfmt != ST10_TIMESTAMP_FMT_TAI) return false; + uint64_t frame_tai = meta->timestamp; + uint64_t cur_tai = mt_get_ptp_time(s->parent, MTL_PORT_P); + uint64_t frame_period_ns = (uint64_t)((double)NS_PER_S / st_frame_rate(ctx->fps)); + return cur_tai >= frame_tai + frame_period_ns; +} + /************************************************************************* * ST20 TX Callbacks → Unified Event Queue * @@ -160,11 +179,23 @@ static int video_tx_get_next_frame(void* priv, uint16_t* next_frame_idx, } } + uint16_t drop_cnt = 0; for (uint16_t i = 0; i < tx_impl->st20_frames_cnt; i++) { enum tx_frame_state expected = TX_FRAME_READY; if (__atomic_compare_exchange_n(&ctx->frame_state[i], &expected, TX_FRAME_TRANSMITTING, false, __ATOMIC_ACQUIRE, __ATOMIC_RELAXED)) { + if (ctx->drop_when_late && drop_cnt < MT_VIDEO_TX_DROP_MAX_BATCH && + tx_frame_is_late(ctx, tx_impl, i)) { + __atomic_add_fetch(&s->stats.buffers_dropped, 1, __ATOMIC_RELAXED); + mtl_event_t event = {0}; + event.type = MTL_EVENT_FRAME_LATE; + event.frame_late.epoch_skipped = 0; + mtl_session_event_post(s, &event); + __atomic_store_n(&ctx->frame_state[i], TX_FRAME_FREE, __ATOMIC_RELEASE); + drop_cnt++; + continue; + } *next_frame_idx = i; rte_atomic32_set(&tx_impl->st20_frames[i].refcnt, 0); return 0; @@ -717,6 +748,9 @@ static void tx_apply_session_flags(struct st20_tx_ops* ops, ops->flags |= ST20_TX_FLAG_DISABLE_BULK; if (config->base.flags & MTL_SESSION_FLAG_STATIC_PAD_P) ops->flags |= ST20_TX_FLAG_ENABLE_STATIC_PAD_P; + /* DROP_WHEN_LATE has no raw st20 equivalent; act on it in get_next_frame. */ + ctx->fps = config->fps; + ctx->drop_when_late = config->base.flags & MTL_SESSION_FLAG_DROP_WHEN_LATE; /* Advanced TX options */ if (config->start_vrx) ops->start_vrx = config->start_vrx; diff --git a/tests/unit/meson.build b/tests/unit/meson.build index 36a5cae17..9a08c9afb 100644 --- a/tests/unit/meson.build +++ b/tests/unit/meson.build @@ -47,8 +47,12 @@ unit_sources = [ 'pipeline/st40p_test.cpp', 'pipeline/st20p_harness.c', 'pipeline/st20p_test.cpp', + 'pipeline/st20p_tx_harness.c', + 'pipeline/st20p_tx_test.cpp', 'new_api/st20_rx_harness.c', 'new_api/st20_rx_test.cpp', + 'new_api/st20_tx_harness.c', + 'new_api/st20_tx_test.cpp', 'main.cpp', ] diff --git a/tests/unit/new_api/st20_tx_harness.c b/tests/unit/new_api/st20_tx_harness.c new file mode 100644 index 000000000..00a8e6c3e --- /dev/null +++ b/tests/unit/new_api/st20_tx_harness.c @@ -0,0 +1,298 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2026 Intel Corporation + * + * C harness for the NEW-API (unified session) video TX tests. + * + * Includes the production TX .c so the static datapath/buffer helpers and the + * get_next_frame / frame_done callbacks are reachable and operate on our + * hand-built session. convert.derive=true so buffer_get/put take the + * passthrough branch (pub->data = frame_trans->addr) and never touch a + * converter, mirroring how st20_rx_harness sets derive=true. + */ + +#include +#include + +#undef MTL_HAS_USDT +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-variable" +#include "new_api/mt_session_video_tx.c" +#pragma GCC diagnostic pop + +#include "common/ut_common.h" + +/* + * Stubs for the transport libmtl symbols the TX .c calls on the paths we + * exercise. The real implementations dereference a live st20_tx handle; ours + * owns a fake handle, so --allow-multiple-definition lets these win. + */ + +int st20_tx_set_ext_frame(st20_tx_handle handle, uint16_t idx, + struct st20_ext_frame* ext_frame) { + (void)handle; + (void)idx; + (void)ext_frame; + return 0; +} + +int st20_tx_get_session_stats(st20_tx_handle handle, struct st20_tx_user_stats* stats) { + (void)handle; + /* io_stats_get is pure passthrough; return zeroed transport stats so the + * test can show it is a distinct surface from the abstract counters. */ + if (stats) memset(stats, 0, sizeof(*stats)); + return 0; +} + +int st20_tx_reset_session_stats(st20_tx_handle handle) { + (void)handle; + return 0; +} + +/* ── opaque context ───────────────────────────────────────────────────── */ + +struct ut20tx_ctx { + struct mtl_session_impl s; + struct st_tx_video_session_impl tx_impl; + struct video_tx_ctx vctx; + struct mtl_main_impl impl; + struct st_frame_trans* frames; + uint8_t* frame_storage; + int framebuff_cnt; +}; + +#include "new_api/st20_tx_harness.h" + +#define UT20TX_FRAME_STRIDE 64 /* per-frame sentinel storage */ + +/* ── ptp stub (returns the test-controlled wall clock) ─────────────────── */ + +static uint64_t ut20tx_ptp_time(struct mtl_main_impl* impl, enum mtl_port port) { + (void)port; + return impl->ptp_usync; +} + +/* ── init ─────────────────────────────────────────────────────────────── */ + +int ut20tx_init(void) { + return ut_eal_init(); +} + +/* ── context create / destroy ─────────────────────────────────────────── */ + +ut20tx_ctx* ut20tx_ctx_create(int framebuff_cnt) { + static uint32_t ring_seq; + + ut20tx_ctx* ctx = calloc(1, sizeof(*ctx)); + if (!ctx) return NULL; + ctx->framebuff_cnt = framebuff_cnt; + + ctx->frames = calloc(framebuff_cnt, sizeof(struct st_frame_trans)); + ctx->frame_storage = calloc(framebuff_cnt, UT20TX_FRAME_STRIDE); + if (!ctx->frames || !ctx->frame_storage) { + free(ctx->frames); + free(ctx->frame_storage); + free(ctx); + return NULL; + } + for (int i = 0; i < framebuff_cnt; i++) { + ctx->frames[i].idx = i; + ctx->frames[i].addr = &ctx->frame_storage[i * UT20TX_FRAME_STRIDE]; + rte_atomic32_set(&ctx->frames[i].refcnt, 0); + } + + ctx->impl.type = MT_HANDLE_MAIN; + ctx->impl.ptp_usync = 0; + for (int i = 0; i < MTL_PORT_MAX; i++) { + ctx->impl.inf[i].parent = &ctx->impl; + ctx->impl.inf[i].port = i; + ctx->impl.inf[i].ptp_get_time_fn = ut20tx_ptp_time; + } + + struct st_tx_video_session_impl* tx = &ctx->tx_impl; + tx->idx = 0; + tx->socket_id = rte_socket_id(); + tx->st20_frames = ctx->frames; + tx->st20_frames_cnt = framebuff_cnt; + + struct mtl_session_impl* s = &ctx->s; + s->vt = &mtl_video_tx_vtable; + s->magic = MTL_SESSION_MAGIC_VIDEO_TX; + s->type = MTL_TYPE_VIDEO; + s->direction = MTL_SESSION_TX; + s->parent = &ctx->impl; + s->socket_id = rte_socket_id(); + s->ownership = MTL_BUFFER_LIBRARY_OWNED; + s->flags = 0; + s->stopped = 0; + s->event_fd = -1; + s->inner.video_tx = tx; + snprintf(s->name, sizeof(s->name), "ut_new_tx"); + + s->buffer_count = framebuff_cnt; + s->buffers = calloc(framebuff_cnt, sizeof(struct mtl_buffer_impl)); + if (!s->buffers) { + free(ctx->frames); + free(ctx->frame_storage); + free(ctx); + return NULL; + } + for (int i = 0; i < framebuff_cnt; i++) { + s->buffers[i].session = s; + s->buffers[i].idx = i; + } + + char ring_name[RTE_RING_NAMESIZE]; + snprintf(ring_name, sizeof(ring_name), "utntx_%u", ring_seq++); + s->event_ring = + rte_ring_create(ring_name, 64, s->socket_id, RING_F_SP_ENQ | RING_F_SC_DEQ); + if (!s->event_ring) { + free(s->buffers); + free(ctx->frames); + free(ctx->frame_storage); + free(ctx); + return NULL; + } + + struct video_tx_ctx* v = &ctx->vctx; + v->session = s; + v->handle = (st20_tx_handle)(uintptr_t)0x1; /* non-NULL sentinel */ + v->convert.derive = true; + v->convert.transport_fmt = ST20_FMT_YUV_422_10BIT; + v->convert.transport_frame_size = UT20TX_FRAME_STRIDE; + v->convert.width = 1920; + v->convert.height = 1080; + v->frame_state = calloc(framebuff_cnt, sizeof(enum tx_frame_state)); + if (!v->frame_state) { + rte_ring_free(s->event_ring); + free(s->buffers); + free(ctx->frames); + free(ctx->frame_storage); + free(ctx); + return NULL; + } + v->frame_cnt = framebuff_cnt; + for (int i = 0; i < framebuff_cnt; i++) v->frame_state[i] = TX_FRAME_FREE; + v->fps = ST_FPS_P59_94; + v->drop_when_late = false; + + tx->ops.priv = v; /* tx_ctx_from_session() reads this */ + + return ctx; +} + +void ut20tx_ctx_destroy(ut20tx_ctx* ctx) { + if (!ctx) return; + if (ctx->s.event_ring) rte_ring_free(ctx->s.event_ring); + free(ctx->vctx.frame_state); + free(ctx->s.buffers); + free(ctx->frames); + free(ctx->frame_storage); + free(ctx); +} + +/* ── controls ─────────────────────────────────────────────────────────── */ + +void ut20tx_set_drop_when_late(ut20tx_ctx* ctx, bool on) { + ctx->vctx.drop_when_late = on; +} + +void ut20tx_set_user_pacing(ut20tx_ctx* ctx, bool on) { + if (on) + ctx->s.flags |= MTL_SESSION_FLAG_USER_PACING; + else + ctx->s.flags &= ~(uint32_t)MTL_SESSION_FLAG_USER_PACING; +} + +void ut20tx_set_fps(ut20tx_ctx* ctx, enum st_fps fps) { + ctx->vctx.fps = fps; +} + +void ut20tx_set_ptp_now(ut20tx_ctx* ctx, uint64_t ns) { + ctx->impl.ptp_usync = ns; +} + +/* ── state machine drivers ────────────────────────────────────────────── */ + +mtl_buffer_t* ut20tx_buffer_get(ut20tx_ctx* ctx) { + mtl_buffer_t* buf = NULL; + int ret = mtl_video_tx_vtable.buffer_get(&ctx->s, &buf, 0 /* non-blocking */); + if (ret != 0) return NULL; + return buf; +} + +void ut20tx_buffer_set_timestamp(mtl_buffer_t* buf, uint64_t tai_ns) { + buf->timestamp = tai_ns; + buf->tfmt = ST10_TIMESTAMP_FMT_TAI; +} + +void ut20tx_buffer_set_user_meta(mtl_buffer_t* buf, void* meta, size_t size) { + buf->user_meta = meta; + buf->user_meta_size = size; +} + +int ut20tx_buffer_put(ut20tx_ctx* ctx, mtl_buffer_t* buf) { + return mtl_video_tx_vtable.buffer_put(&ctx->s, buf); +} + +int ut20tx_get_next_frame(ut20tx_ctx* ctx, uint16_t* idx) { + struct st20_tx_frame_meta meta; + memset(&meta, 0, sizeof(meta)); + uint16_t local = 0; + int ret = video_tx_get_next_frame(&ctx->vctx, &local, &meta); + if (ret == 0 && idx) *idx = local; + return ret; +} + +int ut20tx_frame_done(ut20tx_ctx* ctx, uint16_t idx) { + struct st20_tx_frame_meta meta; + memset(&meta, 0, sizeof(meta)); + return video_tx_notify_frame_done(&ctx->vctx, idx, &meta); +} + +/* ── inspection ───────────────────────────────────────────────────────── */ + +int ut20tx_frame_state(ut20tx_ctx* ctx, uint16_t idx) { + return (int)ctx->vctx.frame_state[idx]; +} + +const void* ut20tx_frame_user_meta(ut20tx_ctx* ctx, uint16_t idx) { + return ctx->frames[idx].tv_meta.user_meta; +} + +void* ut20tx_frame_addr(ut20tx_ctx* ctx, uint16_t idx) { + return ctx->frames[idx].addr; +} + +/* ── stat accessors ───────────────────────────────────────────────────── */ + +uint64_t ut20tx_buffers_processed(const ut20tx_ctx* ctx) { + return ctx->s.stats.buffers_processed; +} + +uint64_t ut20tx_buffers_dropped(const ut20tx_ctx* ctx) { + return ctx->s.stats.buffers_dropped; +} + +uint64_t ut20tx_bytes_processed(const ut20tx_ctx* ctx) { + return ctx->s.stats.bytes_processed; +} + +/* ── vtable stats surfaces ────────────────────────────────────────────── */ + +int ut20tx_stats_get(ut20tx_ctx* ctx, mtl_session_stats_t* stats) { + return mtl_video_tx_vtable.stats_get(&ctx->s, stats); +} + +int ut20tx_io_stats_get(ut20tx_ctx* ctx, struct st20_tx_user_stats* stats) { + return mtl_video_tx_vtable.io_stats_get(&ctx->s, stats, sizeof(*stats)); +} + +int ut20tx_reset_stats(ut20tx_ctx* ctx) { + return mtl_video_tx_vtable.stats_reset(&ctx->s); +} + +/* ── event drain ──────────────────────────────────────────────────────── */ + +int ut20tx_poll_event(ut20tx_ctx* ctx, mtl_event_t* ev) { + return mtl_video_tx_vtable.event_poll(&ctx->s, ev, 0 /* non-blocking */); +} diff --git a/tests/unit/new_api/st20_tx_harness.h b/tests/unit/new_api/st20_tx_harness.h new file mode 100644 index 000000000..b36c8401d --- /dev/null +++ b/tests/unit/new_api/st20_tx_harness.h @@ -0,0 +1,94 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2026 Intel Corporation + * + * C header for the NEW-API (unified session) video TX tests. + * + * Drives the FREE -> APP_OWNED -> READY -> TRANSMITTING -> FREE frame state + * machine of lib/src/new_api/mt_session_video_tx.c directly, with no NIC, no + * transport session and no EAL datapath. A hand-built mtl_session_impl + + * st_tx_video_session_impl + video_tx_ctx is wired so the production + * buffer_get / buffer_put / get_next_frame / frame_done callbacks operate on + * our in-memory frames. + * + * mt_get_ptp_time() is an inline that calls impl->inf[port].ptp_get_time_fn, + * so the late-drop wall clock is controlled via ut20tx_set_ptp_now() (stored + * in impl.ptp_usync and returned by the harness ptp stub) — no link override. + */ + +#ifndef _ST20_NEW_API_TX_HARNESS_H_ +#define _ST20_NEW_API_TX_HARNESS_H_ + +#include +#include + +#include "mtl_session_api.h" +#include "st20_api.h" +#include "st_api.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct ut20tx_ctx ut20tx_ctx; + +int ut20tx_init(void); + +/** Create a TX test context with framebuff_cnt library-owned frames, all FREE, + * derive=true (no conversion). */ +ut20tx_ctx* ut20tx_ctx_create(int framebuff_cnt); +void ut20tx_ctx_destroy(ut20tx_ctx* ctx); + +/* drop-when-late / pacing controls (mirror what session_init would set) */ +void ut20tx_set_drop_when_late(ut20tx_ctx* ctx, bool on); +void ut20tx_set_user_pacing(ut20tx_ctx* ctx, bool on); +void ut20tx_set_fps(ut20tx_ctx* ctx, enum st_fps fps); +/** Set the wall clock returned by mt_get_ptp_time() (TAI ns). */ +void ut20tx_set_ptp_now(ut20tx_ctx* ctx, uint64_t ns); + +/* ── frame state machine drivers ─────────────────────────────────────── */ + +/** Wraps the buffer_get vtable entry (FREE -> APP_OWNED). NULL on -ETIMEDOUT. */ +mtl_buffer_t* ut20tx_buffer_get(ut20tx_ctx* ctx); +/** Stamp a TAI timestamp onto a buffer before put (drives tv_meta passthrough). */ +void ut20tx_buffer_set_timestamp(mtl_buffer_t* buf, uint64_t tai_ns); +/** Attach user metadata to a buffer before put. */ +void ut20tx_buffer_set_user_meta(mtl_buffer_t* buf, void* meta, size_t size); +/** Wraps the buffer_put vtable entry (APP_OWNED -> READY). */ +int ut20tx_buffer_put(ut20tx_ctx* ctx, mtl_buffer_t* buf); +/** Wraps video_tx_get_next_frame (READY -> TRANSMITTING). 0 + *idx, or -EBUSY. */ +int ut20tx_get_next_frame(ut20tx_ctx* ctx, uint16_t* idx); +/** Wraps video_tx_notify_frame_done (TRANSMITTING -> FREE). */ +int ut20tx_frame_done(ut20tx_ctx* ctx, uint16_t idx); + +/* ── state / metadata inspection ─────────────────────────────────────── */ + +/** Returns the enum tx_frame_state of frame idx (0=FREE,1=APP_OWNED,2=READY, + * 3=TRANSMITTING). */ +int ut20tx_frame_state(ut20tx_ctx* ctx, uint16_t idx); +/** Returns the tv_meta.user_meta pointer recorded on frame idx by buffer_put. */ +const void* ut20tx_frame_user_meta(ut20tx_ctx* ctx, uint16_t idx); +/** Returns the transport framebuffer address of frame idx. */ +void* ut20tx_frame_addr(ut20tx_ctx* ctx, uint16_t idx); + +/* ── stat accessors (raw s->stats fields) ────────────────────────────── */ + +uint64_t ut20tx_buffers_processed(const ut20tx_ctx* ctx); +uint64_t ut20tx_buffers_dropped(const ut20tx_ctx* ctx); +uint64_t ut20tx_bytes_processed(const ut20tx_ctx* ctx); + +/* ── vtable stats surfaces ────────────────────────────────────────────── */ + +int ut20tx_stats_get(ut20tx_ctx* ctx, mtl_session_stats_t* stats); +int ut20tx_io_stats_get(ut20tx_ctx* ctx, struct st20_tx_user_stats* stats); +int ut20tx_reset_stats(ut20tx_ctx* ctx); + +/* ── event drain ──────────────────────────────────────────────────────── */ + +/** Non-blocking event_poll wrapper. 0 + *ev on an event, -ETIMEDOUT if none. */ +int ut20tx_poll_event(ut20tx_ctx* ctx, mtl_event_t* ev); + +#ifdef __cplusplus +} +#endif + +#endif /* _ST20_NEW_API_TX_HARNESS_H_ */ diff --git a/tests/unit/new_api/st20_tx_test.cpp b/tests/unit/new_api/st20_tx_test.cpp new file mode 100644 index 000000000..cb7a04dc4 --- /dev/null +++ b/tests/unit/new_api/st20_tx_test.cpp @@ -0,0 +1,251 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2026 Intel Corporation + * + * ST20 NEW-API (unified session) TX contract tests. + * + * Parallel TX copy of tests/unit/new_api/st20_rx_test.cpp / the pipeline TX + * suite. Drives the FREE -> APP_OWNED -> READY -> TRANSMITTING -> FREE frame + * state machine directly through the production vtable callbacks. + * + * DropWhenLate* pin MTL_SESSION_FLAG_DROP_WHEN_LATE, which has no raw st20 + * equivalent and is implemented inside video_tx_get_next_frame: a READY frame + * whose TAI timestamp has fallen more than one frame period behind the PTP + * wall clock is recycled to FREE, counted in buffers_dropped, and surfaced via + * an MTL_EVENT_FRAME_LATE event — mirroring pipeline tx_st20p_if_frame_late. + */ + +#include + +#include "new_api/st20_tx_harness.h" + +/* P59_94: one frame period is ~16.683 ms. A TAI base of 1 s keeps both the + * "on time" and "late" wall clocks positive and unambiguous. */ +static constexpr uint64_t kFrameTai = 1000000000ULL; +static constexpr uint64_t kPeriodNs = 16683350ULL; /* ~1e9 / 59.94 */ + +class St20NewApiTxTest : public ::testing::Test { + protected: + ut20tx_ctx* ctx_ = nullptr; + + void SetUp() override { + ASSERT_EQ(ut20tx_init(), 0) << "EAL init failed"; + ctx_ = ut20tx_ctx_create(/*framebuff_cnt=*/3); + ASSERT_NE(ctx_, nullptr); + } + + void TearDown() override { + ut20tx_ctx_destroy(ctx_); + ctx_ = nullptr; + } + + mtl_buffer_t* get() { + return ut20tx_buffer_get(ctx_); + } + int put(mtl_buffer_t* b) { + return ut20tx_buffer_put(ctx_, b); + } + uint64_t dropped() { + return ut20tx_buffers_dropped(ctx_); + } + uint64_t processed() { + return ut20tx_buffers_processed(ctx_); + } + int state(uint16_t i) { + return ut20tx_frame_state(ctx_, i); + } + + /* Put one READY frame stamped at kFrameTai, then arm drop-when-late. */ + void put_stamped_frame(uint64_t tai) { + mtl_buffer_t* b = get(); + ASSERT_NE(b, nullptr); + ut20tx_buffer_set_timestamp(b, tai); + ASSERT_EQ(put(b), 0); + } +}; + +/* enum tx_frame_state mirror (private to the .c). */ +enum { kFree = 0, kAppOwned = 1, kReady = 2, kTransmitting = 3 }; + +/* DropWhenLate ON + USER_PACING: a READY frame whose TAI is a full period in + * the past must NOT be selected. The slot returns to FREE, buffers_dropped + * bumps once, and an MTL_EVENT_FRAME_LATE event is posted. */ +TEST_F(St20NewApiTxTest, DropWhenLateDropsLateFrame) { + ut20tx_set_drop_when_late(ctx_, true); + ut20tx_set_user_pacing(ctx_, true); + ut20tx_set_fps(ctx_, ST_FPS_P59_94); + + put_stamped_frame(kFrameTai); + ASSERT_EQ(state(0), kReady); + + /* wall clock well past the frame's TX window (> one frame period late) */ + ut20tx_set_ptp_now(ctx_, kFrameTai + 2 * kPeriodNs); + + uint16_t idx = 0xffff; + EXPECT_EQ(ut20tx_get_next_frame(ctx_, &idx), -EBUSY) + << "a late frame must not be selected for transmission"; + + EXPECT_EQ(state(0), kFree) << "dropped frame must be recycled to FREE"; + EXPECT_EQ(dropped(), 1u); + EXPECT_EQ(processed(), 0u) << "a dropped frame is not transmitted"; + + mtl_event_t ev; + ASSERT_EQ(ut20tx_poll_event(ctx_, &ev), 0) << "a FRAME_LATE event must be posted"; + EXPECT_EQ(ev.type, MTL_EVENT_FRAME_LATE); +} + +/* DropWhenLate ON + USER_PACING but the frame is still within its window: + * it is selected and transmitted normally, nothing is dropped. */ +TEST_F(St20NewApiTxTest, DropWhenLateKeepsOnTimeFrame) { + ut20tx_set_drop_when_late(ctx_, true); + ut20tx_set_user_pacing(ctx_, true); + ut20tx_set_fps(ctx_, ST_FPS_P59_94); + + put_stamped_frame(kFrameTai); + ASSERT_EQ(state(0), kReady); + + /* within one frame period — on time */ + ut20tx_set_ptp_now(ctx_, kFrameTai + kPeriodNs / 2); + + uint16_t idx = 0xffff; + EXPECT_EQ(ut20tx_get_next_frame(ctx_, &idx), 0); + EXPECT_EQ(idx, 0u); + EXPECT_EQ(state(0), kTransmitting); + EXPECT_EQ(dropped(), 0u); + + mtl_event_t ev; + EXPECT_EQ(ut20tx_poll_event(ctx_, &ev), -ETIMEDOUT) << "no late event expected"; +} + +/* DropWhenLate ON but USER_PACING absent: drop-when-late requires user pacing, + * so the late check is skipped and the (late) frame is transmitted normally. */ +TEST_F(St20NewApiTxTest, DropWhenLateIgnoredWithoutUserPacing) { + ut20tx_set_drop_when_late(ctx_, true); + ut20tx_set_user_pacing(ctx_, false); + ut20tx_set_fps(ctx_, ST_FPS_P59_94); + + put_stamped_frame(kFrameTai); + ASSERT_EQ(state(0), kReady); + + ut20tx_set_ptp_now(ctx_, kFrameTai + 100 * kPeriodNs); + + uint16_t idx = 0xffff; + EXPECT_EQ(ut20tx_get_next_frame(ctx_, &idx), 0) + << "without USER_PACING the late frame must still be transmitted"; + EXPECT_EQ(idx, 0u); + EXPECT_EQ(state(0), kTransmitting); + EXPECT_EQ(dropped(), 0u); +} + +/* Full FREE -> APP_OWNED -> READY -> TRANSMITTING -> FREE walk via the public + * callbacks. buffers_processed bumps on frame_done (transmission complete), + * not on get or put. */ +TEST_F(St20NewApiTxTest, FrameLifecycleGetPutTransmit) { + mtl_buffer_t* b = get(); + ASSERT_NE(b, nullptr); + EXPECT_EQ(state(0), kAppOwned); + EXPECT_EQ(processed(), 0u); + + EXPECT_EQ(put(b), 0); + EXPECT_EQ(state(0), kReady); + EXPECT_EQ(processed(), 0u); + + uint16_t idx = 0xffff; + ASSERT_EQ(ut20tx_get_next_frame(ctx_, &idx), 0); + EXPECT_EQ(idx, 0u); + EXPECT_EQ(state(0), kTransmitting); + EXPECT_EQ(processed(), 0u) << "not yet done — still on the wire"; + + ASSERT_EQ(ut20tx_frame_done(ctx_, 0), 0); + EXPECT_EQ(state(0), kFree); + EXPECT_EQ(processed(), 1u) << "buffers_processed bumps on frame_done"; +} + +/* buffer_put threads the buffer's user_meta pointer/size into the frame slot's + * tv_meta, so the transport sends it alongside the frame. */ +TEST_F(St20NewApiTxTest, UserMetaPassthroughOnPut) { + static uint8_t meta_blob[16] = {0xAB}; + + mtl_buffer_t* b = get(); + ASSERT_NE(b, nullptr); + ut20tx_buffer_set_user_meta(b, meta_blob, sizeof(meta_blob)); + ASSERT_EQ(put(b), 0); + + EXPECT_EQ(ut20tx_frame_user_meta(ctx_, 0), meta_blob) + << "put must record the user_meta pointer on the frame's tv_meta"; +} + +/* In derive mode (transport fmt == app fmt) buffer_get hands the app the + * transport framebuffer directly, so the app writes straight into the wire + * buffer and put is a pure handoff with no copy. (The conversion copy lives on + * the !derive branch of buffer_put.) */ +TEST_F(St20NewApiTxTest, ConvertOnPutAppToTransport) { + mtl_buffer_t* b = get(); + ASSERT_NE(b, nullptr); + EXPECT_EQ(b->data, ut20tx_frame_addr(ctx_, 0)) + << "derive mode: app buffer IS the transport framebuffer (zero-copy)"; + + /* app fills the buffer; in derive mode this lands directly on the wire */ + memset(b->data, 0x5A, 16); + ASSERT_EQ(put(b), 0); + EXPECT_EQ(((uint8_t*)ut20tx_frame_addr(ctx_, 0))[0], 0x5A) + << "no conversion copy in derive mode — data is already in place"; +} + +/* Two distinct stats surfaces: stats_get returns the abstract unified counters + * the test drove; io_stats_get is a pure passthrough of the transport's own + * (stubbed, zeroed) stats. They are independent, not an overlay. */ +TEST_F(St20NewApiTxTest, StatsGetVsIoStatsPassthrough) { + for (int i = 0; i < 3; i++) { + mtl_buffer_t* b = get(); + ASSERT_NE(b, nullptr); + ASSERT_EQ(put(b), 0); + uint16_t idx = 0xffff; + ASSERT_EQ(ut20tx_get_next_frame(ctx_, &idx), 0); + ASSERT_EQ(ut20tx_frame_done(ctx_, idx), 0); + } + + mtl_session_stats_t abstract{}; + ASSERT_EQ(ut20tx_stats_get(ctx_, &abstract), 0); + EXPECT_EQ(abstract.buffers_processed, processed()); + EXPECT_EQ(abstract.buffers_processed, 3u); + + struct st20_tx_user_stats io {}; + ASSERT_EQ(ut20tx_io_stats_get(ctx_, &io), 0); + EXPECT_EQ(io.common.stat_frames_sent, 0u) + << "io_stats_get is transport passthrough, not an overlay of the " + "abstract counters"; +} + +/* buffer_get with timeout_ms=0 returns NULL (-ETIMEDOUT) once every frame is + * claimed — it never blocks and never invents a slot. */ +TEST_F(St20NewApiTxTest, BufferGetTimeoutSemantics) { + mtl_buffer_t* held[3]; + for (int i = 0; i < 3; i++) { + held[i] = get(); + ASSERT_NE(held[i], nullptr); + } + /* all 3 frames are APP_OWNED — none FREE */ + EXPECT_EQ(get(), nullptr) << "non-blocking get must return NULL when full"; + + /* releasing one (put + transmit + done) frees a slot again */ + ASSERT_EQ(put(held[0]), 0); + uint16_t idx = 0xffff; + ASSERT_EQ(ut20tx_get_next_frame(ctx_, &idx), 0); + ASSERT_EQ(ut20tx_frame_done(ctx_, idx), 0); + EXPECT_NE(get(), nullptr) << "a freed slot is claimable again"; +} + +/* The unified TX honors the requested framebuffer count with no max clamp: + * every one of the N frames is an independently claimable slot. */ +TEST_F(St20NewApiTxTest, FramebuffCntNoTxClamp) { + ut20tx_ctx_destroy(ctx_); + ctx_ = ut20tx_ctx_create(/*framebuff_cnt=*/8); + ASSERT_NE(ctx_, nullptr); + + for (int i = 0; i < 8; i++) { + mtl_buffer_t* b = get(); + ASSERT_NE(b, nullptr) << "frame " << i << " must be claimable (no clamp)"; + EXPECT_EQ(state((uint16_t)i), kAppOwned); + } + EXPECT_EQ(get(), nullptr) << "exactly 8 slots, no more"; +} diff --git a/tests/unit/pipeline/st20p_tx_harness.c b/tests/unit/pipeline/st20p_tx_harness.c new file mode 100644 index 000000000..5da942b68 --- /dev/null +++ b/tests/unit/pipeline/st20p_tx_harness.c @@ -0,0 +1,230 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2026 Intel Corporation + * + * C harness for ST20p (video) pipeline-layer TX unit tests. + * + * Includes the production pipeline TX .c so the static tx_st20p_next_frame / + * tx_st20p_frame_done / tx_st20p_if_frame_late are reachable and the public + * st20p_tx_* entry points see our hand-built st20p_tx_ctx. derive=true so + * put_frame goes straight to CONVERTED and the converter is never invoked. + * Disable USDT to avoid linker references to probe semaphores. + */ + +#include +#include + +#undef MTL_HAS_USDT +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-variable" +#pragma GCC diagnostic ignored "-Wunused-but-set-variable" +#include "st2110/pipeline/st20_pipeline_tx.c" +#pragma GCC diagnostic pop + +#include "common/ut_common.h" + +/* + * Stubs for the transport libmtl symbols the pipeline TX calls on the paths we + * exercise (get_session_stats / reset). Our transport handle is a fake, so + * --allow-multiple-definition lets ours win. + */ + +int st20_tx_get_session_stats(st20_tx_handle handle, struct st20_tx_user_stats* stats) { + (void)handle; + /* Return zeroed transport stats; the pipeline overlays its own frame-level + * counters on top, which is exactly what we want to test. */ + if (stats) memset(stats, 0, sizeof(*stats)); + return 0; +} + +int st20_tx_reset_session_stats(st20_tx_handle handle) { + (void)handle; + return 0; +} + +/* ── opaque context ───────────────────────────────────────────────────── */ + +#define UT20PTX_USER_META_SIZE 64 + +struct ut20ptx_ctx { + struct mtl_main_impl impl; + struct st20p_tx_ctx pipeline; + struct st20p_tx_frame* framebuffs; + int framebuff_cnt; + uint32_t late_cnt; +}; + +#include "pipeline/st20p_tx_harness.h" + +/* ── ptp stub (returns the test-controlled wall clock) ─────────────────── */ + +static uint64_t ut20ptx_ptp_time(struct mtl_main_impl* impl, enum mtl_port port) { + (void)port; + return impl->ptp_usync; +} + +/* ── notify_frame_late callback (counts drop-when-late events) ──────────── */ + +static int ut20ptx_notify_late(void* priv, uint64_t epoch_skipped) { + (void)epoch_skipped; + ut20ptx_ctx* ctx = priv; + ctx->late_cnt++; + return 0; +} + +/* ── init ─────────────────────────────────────────────────────────────── */ + +int ut20ptx_init(void) { + return ut_eal_init(); +} + +/* ── context create / destroy ─────────────────────────────────────────── */ + +ut20ptx_ctx* ut20ptx_ctx_create(int framebuff_cnt) { + ut20ptx_ctx* ctx = calloc(1, sizeof(*ctx)); + if (!ctx) return NULL; + ctx->framebuff_cnt = framebuff_cnt; + + ctx->impl.type = MT_HANDLE_MAIN; + ctx->impl.ptp_usync = 0; + for (int i = 0; i < MTL_PORT_MAX; i++) { + ctx->impl.inf[i].parent = &ctx->impl; + ctx->impl.inf[i].port = i; + ctx->impl.inf[i].ptp_get_time_fn = ut20ptx_ptp_time; + } + + ctx->framebuffs = calloc(framebuff_cnt, sizeof(struct st20p_tx_frame)); + if (!ctx->framebuffs) { + free(ctx); + return NULL; + } + for (int i = 0; i < framebuff_cnt; i++) { + ctx->framebuffs[i].stat = ST20P_TX_FRAME_FREE; + ctx->framebuffs[i].idx = i; + /* derive=true: get/put operate on dst; put reads frame->priv to recover + * the owning framebuff. */ + ctx->framebuffs[i].src.priv = &ctx->framebuffs[i]; + ctx->framebuffs[i].dst.priv = &ctx->framebuffs[i]; + ctx->framebuffs[i].user_meta = calloc(1, UT20PTX_USER_META_SIZE); + ctx->framebuffs[i].user_meta_buffer_size = UT20PTX_USER_META_SIZE; + if (!ctx->framebuffs[i].user_meta) { + for (int j = 0; j < i; j++) free(ctx->framebuffs[j].user_meta); + free(ctx->framebuffs); + free(ctx); + return NULL; + } + } + + struct st20p_tx_ctx* p = &ctx->pipeline; + p->impl = &ctx->impl; + p->idx = 0; + p->socket_id = rte_socket_id(); + p->type = MT_ST20_HANDLE_PIPELINE_TX; + p->framebuff_cnt = framebuff_cnt; + p->framebuffs = ctx->framebuffs; + p->derive = true; + p->ready = true; + p->block_get = false; + p->transport = (st20_tx_handle)(uintptr_t)0x1; + p->ops.fps = ST_FPS_P59_94; + p->ops.notify_frame_late = ut20ptx_notify_late; + p->ops.priv = ctx; + + if (pthread_mutex_init(&p->lock, NULL) != 0) { + for (int i = 0; i < framebuff_cnt; i++) free(ctx->framebuffs[i].user_meta); + free(ctx->framebuffs); + free(ctx); + return NULL; + } + + return ctx; +} + +void ut20ptx_ctx_destroy(ut20ptx_ctx* ctx) { + if (!ctx) return; + pthread_mutex_destroy(&ctx->pipeline.lock); + for (int i = 0; i < ctx->framebuff_cnt; i++) free(ctx->framebuffs[i].user_meta); + free(ctx->framebuffs); + free(ctx); +} + +/* ── controls ─────────────────────────────────────────────────────────── */ + +void ut20ptx_set_drop_when_late(ut20ptx_ctx* ctx, bool on) { + if (on) + ctx->pipeline.ops.flags |= ST20P_TX_FLAG_DROP_WHEN_LATE; + else + ctx->pipeline.ops.flags &= ~(uint32_t)ST20P_TX_FLAG_DROP_WHEN_LATE; +} + +void ut20ptx_set_user_pacing(ut20ptx_ctx* ctx, bool on) { + if (on) + ctx->pipeline.ops.flags |= ST20P_TX_FLAG_USER_PACING; + else + ctx->pipeline.ops.flags &= ~(uint32_t)ST20P_TX_FLAG_USER_PACING; +} + +void ut20ptx_set_fps(ut20ptx_ctx* ctx, enum st_fps fps) { + ctx->pipeline.ops.fps = fps; +} + +void ut20ptx_set_ptp_now(ut20ptx_ctx* ctx, uint64_t ns) { + ctx->impl.ptp_usync = ns; +} + +/* ── frame state machine drivers ──────────────────────────────────────── */ + +struct st_frame* ut20ptx_get_frame(ut20ptx_ctx* ctx) { + return st20p_tx_get_frame(&ctx->pipeline); +} + +int ut20ptx_put_frame(ut20ptx_ctx* ctx, struct st_frame* frame) { + return st20p_tx_put_frame(&ctx->pipeline, frame); +} + +int ut20ptx_next_frame(ut20ptx_ctx* ctx, uint16_t* idx, struct st20_tx_frame_meta* meta) { + struct st20_tx_frame_meta local; + memset(&local, 0, sizeof(local)); + uint16_t local_idx = 0; + int ret = tx_st20p_next_frame(&ctx->pipeline, &local_idx, &local); + if (ret == 0) { + if (idx) *idx = local_idx; + if (meta) *meta = local; + } + return ret; +} + +int ut20ptx_frame_done(ut20ptx_ctx* ctx, uint16_t idx) { + struct st20_tx_frame_meta meta; + memset(&meta, 0, sizeof(meta)); + return tx_st20p_frame_done(&ctx->pipeline, idx, &meta); +} + +/* ── inspection ───────────────────────────────────────────────────────── */ + +int ut20ptx_frame_stat(ut20ptx_ctx* ctx, uint16_t idx) { + return (int)ctx->framebuffs[idx].stat; +} + +uint32_t ut20ptx_notify_late_cnt(const ut20ptx_ctx* ctx) { + return ctx->late_cnt; +} + +/* ── stat accessors ───────────────────────────────────────────────────── */ + +uint64_t ut20ptx_stat_frames_dropped(const ut20ptx_ctx* ctx) { + return ctx->pipeline.stat_frames_dropped; +} + +uint64_t ut20ptx_stat_frames_sent(const ut20ptx_ctx* ctx) { + return ctx->pipeline.stat_frames_sent; +} + +/* ── public-API wrappers ──────────────────────────────────────────────── */ + +int ut20ptx_get_session_stats(ut20ptx_ctx* ctx, struct st20_tx_user_stats* stats) { + return st20p_tx_get_session_stats(&ctx->pipeline, stats); +} + +int ut20ptx_reset_session_stats(ut20ptx_ctx* ctx) { + return st20p_tx_reset_session_stats(&ctx->pipeline); +} diff --git a/tests/unit/pipeline/st20p_tx_harness.h b/tests/unit/pipeline/st20p_tx_harness.h new file mode 100644 index 000000000..f3ff80e32 --- /dev/null +++ b/tests/unit/pipeline/st20p_tx_harness.h @@ -0,0 +1,77 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2026 Intel Corporation + * + * C header for ST20p (video) pipeline-layer TX unit tests. + * + * Parallel TX mirror of tests/unit/pipeline/st20p_harness.h (RX). Drives the + * pipeline TX frame state machine of lib/src/st2110/pipeline/st20_pipeline_tx.c + * directly through st20p_tx_get_frame / st20p_tx_put_frame and the static + * tx_st20p_next_frame / tx_st20p_frame_done callbacks, with no transport + * session stitched in. derive=true so the converter path is bypassed. + * + * mt_get_ptp_time() is controlled via ut20ptx_set_ptp_now() (stored in + * impl.ptp_usync and returned by the harness ptp stub), so the late-drop + * predicate tx_st20p_if_frame_late can be exercised deterministically. + */ + +#ifndef _ST20P_PIPELINE_TX_HARNESS_H_ +#define _ST20P_PIPELINE_TX_HARNESS_H_ + +#include +#include + +#include "mtl_api.h" +#include "st20_api.h" +#include "st_pipeline_api.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct ut20ptx_ctx ut20ptx_ctx; + +int ut20ptx_init(void); + +ut20ptx_ctx* ut20ptx_ctx_create(int framebuff_cnt); +void ut20ptx_ctx_destroy(ut20ptx_ctx* ctx); + +/* drop-when-late / pacing controls */ +void ut20ptx_set_drop_when_late(ut20ptx_ctx* ctx, bool on); +void ut20ptx_set_user_pacing(ut20ptx_ctx* ctx, bool on); +void ut20ptx_set_fps(ut20ptx_ctx* ctx, enum st_fps fps); +void ut20ptx_set_ptp_now(ut20ptx_ctx* ctx, uint64_t ns); + +/* ── frame state machine drivers ─────────────────────────────────────── */ + +/** Wraps st20p_tx_get_frame (FREE -> IN_USER). NULL when no free frame. */ +struct st_frame* ut20ptx_get_frame(ut20ptx_ctx* ctx); +/** Wraps st20p_tx_put_frame (IN_USER -> CONVERTED in derive mode). */ +int ut20ptx_put_frame(ut20ptx_ctx* ctx, struct st_frame* frame); +/** Wraps tx_st20p_next_frame (CONVERTED -> IN_TRANSMITTING, or drop). 0 + *idx + * + fills *meta, or -EBUSY. */ +int ut20ptx_next_frame(ut20ptx_ctx* ctx, uint16_t* idx, struct st20_tx_frame_meta* meta); +/** Wraps tx_st20p_frame_done (IN_TRANSMITTING -> FREE). */ +int ut20ptx_frame_done(ut20ptx_ctx* ctx, uint16_t idx); + +/* ── inspection ───────────────────────────────────────────────────────── */ + +/** Returns enum st20p_tx_frame_status of framebuff idx. */ +int ut20ptx_frame_stat(ut20ptx_ctx* ctx, uint16_t idx); +/** Count of ops.notify_frame_late() callbacks fired (drop-when-late events). */ +uint32_t ut20ptx_notify_late_cnt(const ut20ptx_ctx* ctx); + +/* ── stat accessors ───────────────────────────────────────────────────── */ + +uint64_t ut20ptx_stat_frames_dropped(const ut20ptx_ctx* ctx); +uint64_t ut20ptx_stat_frames_sent(const ut20ptx_ctx* ctx); + +/* ── public-API wrappers ──────────────────────────────────────────────── */ + +int ut20ptx_get_session_stats(ut20ptx_ctx* ctx, struct st20_tx_user_stats* stats); +int ut20ptx_reset_session_stats(ut20ptx_ctx* ctx); + +#ifdef __cplusplus +} +#endif + +#endif /* _ST20P_PIPELINE_TX_HARNESS_H_ */ diff --git a/tests/unit/pipeline/st20p_tx_test.cpp b/tests/unit/pipeline/st20p_tx_test.cpp new file mode 100644 index 000000000..d7cce8f40 --- /dev/null +++ b/tests/unit/pipeline/st20p_tx_test.cpp @@ -0,0 +1,240 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright(c) 2026 Intel Corporation + * + * ST20p (video) pipeline-layer TX contract tests. + * + * Old-API mirror of tests/unit/new_api/st20_tx_test.cpp. Drives the pipeline + * TX state machine FREE -> IN_USER -> CONVERTED -> IN_TRANSMITTING -> FREE + * through st20p_tx_get_frame / put_frame and the tx_st20p_next_frame / + * frame_done callbacks. The two suites are parallel: the new-API DropWhenLate* + * cases pin the replicated late-drop logic in video_tx_get_next_frame; these + * pin the original tx_st20p_if_frame_late they were modelled on. + */ + +#include + +#include "pipeline/st20p_tx_harness.h" + +/* P59_94: one frame period is ~16.683 ms. */ +static constexpr uint64_t kFrameTai = 1000000000ULL; +static constexpr uint64_t kPeriodNs = 16683350ULL; /* ~1e9 / 59.94 */ + +/* enum st20p_tx_frame_status mirror (header is pipeline-internal). */ +enum { + kFree = 0, + kReady = 1, + kInConverting = 2, + kConverted = 3, + kDropped = 4, + kInUser = 5, + kInTransmitting = 6, +}; + +class St20PipelineTxTest : public ::testing::Test { + protected: + ut20ptx_ctx* ctx_ = nullptr; + + void SetUp() override { + ASSERT_EQ(ut20ptx_init(), 0) << "EAL init failed"; + ctx_ = ut20ptx_ctx_create(/*framebuff_cnt=*/3); + ASSERT_NE(ctx_, nullptr); + } + + void TearDown() override { + ut20ptx_ctx_destroy(ctx_); + ctx_ = nullptr; + } + + struct st_frame* get() { + return ut20ptx_get_frame(ctx_); + } + int put(struct st_frame* f) { + return ut20ptx_put_frame(ctx_, f); + } + uint64_t dropped() { + return ut20ptx_stat_frames_dropped(ctx_); + } + uint64_t sent() { + return ut20ptx_stat_frames_sent(ctx_); + } + int stat(uint16_t i) { + return ut20ptx_frame_stat(ctx_, i); + } + + void put_stamped_frame(uint64_t tai) { + struct st_frame* f = get(); + ASSERT_NE(f, nullptr); + f->tfmt = ST10_TIMESTAMP_FMT_TAI; + f->timestamp = tai; + ASSERT_EQ(put(f), 0); + } +}; + +/* DROP_WHEN_LATE + USER_PACING: a CONVERTED frame a full period in the past is + * dropped by tx_st20p_next_frame (recycled, not transmitted). stat_frames_dropped + * bumps once and notify_frame_late fires. */ +TEST_F(St20PipelineTxTest, DropWhenLateDropsLateFrame) { + ut20ptx_set_drop_when_late(ctx_, true); + ut20ptx_set_user_pacing(ctx_, true); + ut20ptx_set_fps(ctx_, ST_FPS_P59_94); + + put_stamped_frame(kFrameTai); + ASSERT_EQ(stat(0), kConverted); + + ut20ptx_set_ptp_now(ctx_, kFrameTai + 2 * kPeriodNs); + + uint16_t idx = 0xffff; + EXPECT_EQ(ut20ptx_next_frame(ctx_, &idx, nullptr), -EBUSY) + << "a late frame must not be selected for transmission"; + + EXPECT_EQ(stat(0), kFree) << "dropped frame must be recycled to FREE"; + EXPECT_EQ(dropped(), 1u); + EXPECT_EQ(ut20ptx_notify_late_cnt(ctx_), 1u) << "notify_frame_late must fire"; +} + +/* On-time CONVERTED frame is selected and transmitted normally. */ +TEST_F(St20PipelineTxTest, DropWhenLateKeepsOnTimeFrame) { + ut20ptx_set_drop_when_late(ctx_, true); + ut20ptx_set_user_pacing(ctx_, true); + ut20ptx_set_fps(ctx_, ST_FPS_P59_94); + + put_stamped_frame(kFrameTai); + ASSERT_EQ(stat(0), kConverted); + + ut20ptx_set_ptp_now(ctx_, kFrameTai + kPeriodNs / 2); + + uint16_t idx = 0xffff; + EXPECT_EQ(ut20ptx_next_frame(ctx_, &idx, nullptr), 0); + EXPECT_EQ(idx, 0u); + EXPECT_EQ(stat(0), kInTransmitting); + EXPECT_EQ(dropped(), 0u); + EXPECT_EQ(ut20ptx_notify_late_cnt(ctx_), 0u); +} + +/* DROP_WHEN_LATE set but USER_PACING absent: late check is skipped (both flags + * required), so the late frame is still transmitted. */ +TEST_F(St20PipelineTxTest, DropWhenLateIgnoredWithoutUserPacing) { + ut20ptx_set_drop_when_late(ctx_, true); + ut20ptx_set_user_pacing(ctx_, false); + ut20ptx_set_fps(ctx_, ST_FPS_P59_94); + + put_stamped_frame(kFrameTai); + ASSERT_EQ(stat(0), kConverted); + + ut20ptx_set_ptp_now(ctx_, kFrameTai + 100 * kPeriodNs); + + uint16_t idx = 0xffff; + EXPECT_EQ(ut20ptx_next_frame(ctx_, &idx, nullptr), 0) + << "without USER_PACING the late frame must still be transmitted"; + EXPECT_EQ(idx, 0u); + EXPECT_EQ(stat(0), kInTransmitting); + EXPECT_EQ(dropped(), 0u); +} + +/* Full FREE -> IN_USER -> CONVERTED -> IN_TRANSMITTING -> FREE walk. + * stat_frames_sent bumps on frame_done (transmission complete). */ +TEST_F(St20PipelineTxTest, FrameLifecycleGetPutTransmit) { + struct st_frame* f = get(); + ASSERT_NE(f, nullptr); + EXPECT_EQ(stat(0), kInUser); + EXPECT_EQ(sent(), 0u); + + EXPECT_EQ(put(f), 0); + EXPECT_EQ(stat(0), kConverted); + + uint16_t idx = 0xffff; + ASSERT_EQ(ut20ptx_next_frame(ctx_, &idx, nullptr), 0); + EXPECT_EQ(idx, 0u); + EXPECT_EQ(stat(0), kInTransmitting); + EXPECT_EQ(sent(), 0u) << "not yet done — still on the wire"; + + ASSERT_EQ(ut20ptx_frame_done(ctx_, 0), 0); + EXPECT_EQ(stat(0), kFree); + EXPECT_EQ(sent(), 1u) << "stat_frames_sent bumps on frame_done"; +} + +/* put_frame copies the app's user_meta into the framebuff, and next_frame + * threads it into the st20_tx_frame_meta handed to the transport. */ +TEST_F(St20PipelineTxTest, UserMetaPassthroughOnPut) { + static const uint8_t blob[8] = {1, 2, 3, 4, 5, 6, 7, 8}; + + ut20ptx_set_user_pacing(ctx_, true); + + struct st_frame* f = get(); + ASSERT_NE(f, nullptr); + f->user_meta = blob; + f->user_meta_size = sizeof(blob); + ASSERT_EQ(put(f), 0); + + uint16_t idx = 0xffff; + struct st20_tx_frame_meta meta {}; + ASSERT_EQ(ut20ptx_next_frame(ctx_, &idx, &meta), 0); + ASSERT_NE(meta.user_meta, nullptr); + EXPECT_EQ(meta.user_meta_size, sizeof(blob)); + EXPECT_EQ(memcmp(meta.user_meta, blob, sizeof(blob)), 0) + << "user_meta must survive put -> next_frame intact"; +} + +/* In derive mode (input fmt == transport fmt) put_frame takes the no-convert + * branch straight to CONVERTED — the converter is never invoked. (The copy + * lives on the internal_converter / !derive branch.) */ +TEST_F(St20PipelineTxTest, ConvertOnPutAppToTransport) { + struct st_frame* f = get(); + ASSERT_NE(f, nullptr); + EXPECT_EQ(stat(0), kInUser); + + ASSERT_EQ(put(f), 0); + EXPECT_EQ(stat(0), kConverted) + << "derive mode: put goes straight to CONVERTED with no conversion copy"; +} + +/* Pipeline get_session_stats OVERLAYS its frame counters onto the transport + * stats — the opposite of the new API, where stats_get and io_stats_get are + * independent surfaces. After sending 3 frames the overlay reports 3 sent. */ +TEST_F(St20PipelineTxTest, StatsGetVsIoStatsPassthrough) { + for (int i = 0; i < 3; i++) { + struct st_frame* f = get(); + ASSERT_NE(f, nullptr); + ASSERT_EQ(put(f), 0); + uint16_t idx = 0xffff; + ASSERT_EQ(ut20ptx_next_frame(ctx_, &idx, nullptr), 0); + ASSERT_EQ(ut20ptx_frame_done(ctx_, idx), 0); + } + EXPECT_EQ(sent(), 3u); + + struct st20_tx_user_stats stats {}; + ASSERT_EQ(ut20ptx_get_session_stats(ctx_, &stats), 0); + EXPECT_EQ(stats.common.stat_frames_sent, 3u) + << "pipeline overlays stat_frames_sent onto the transport stats"; +} + +/* get_frame with block_get=false returns NULL once every framebuffer is + * claimed (IN_USER) — it never blocks. Releasing one frees a slot again. */ +TEST_F(St20PipelineTxTest, BufferGetTimeoutSemantics) { + struct st_frame* held[3]; + for (int i = 0; i < 3; i++) { + held[i] = get(); + ASSERT_NE(held[i], nullptr); + } + EXPECT_EQ(get(), nullptr) << "non-blocking get must return NULL when full"; + + ASSERT_EQ(put(held[0]), 0); + uint16_t idx = 0xffff; + ASSERT_EQ(ut20ptx_next_frame(ctx_, &idx, nullptr), 0); + ASSERT_EQ(ut20ptx_frame_done(ctx_, idx), 0); + EXPECT_NE(get(), nullptr) << "a freed slot is claimable again"; +} + +/* Every requested framebuffer is an independently claimable slot. */ +TEST_F(St20PipelineTxTest, FramebuffCntNoTxClamp) { + ut20ptx_ctx_destroy(ctx_); + ctx_ = ut20ptx_ctx_create(/*framebuff_cnt=*/8); + ASSERT_NE(ctx_, nullptr); + + for (int i = 0; i < 8; i++) { + struct st_frame* f = get(); + ASSERT_NE(f, nullptr) << "frame " << i << " must be claimable"; + EXPECT_EQ(stat((uint16_t)i), kInUser); + } + EXPECT_EQ(get(), nullptr) << "exactly 8 slots, no more"; +} From f23158c130bb3f651046f5a8b245cc9c067f1151 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 9 Jun 2026 10:28:26 +0000 Subject: [PATCH 06/16] new_api: st20 RX unit parity suite (Phase 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expand the new-API (unified session) st20 video RX unit suite from the 6 stats-only cases to cover every mapped RX feature, and mirror the feature cases on the old st20 pipeline RX suite so the two stay parallel. Test-only change; no lib edits. New St20NewApiRxTest cases (characterization of existing production code in lib/src/new_api/mt_session_video_rx.c, driven via the harness #include of the production .c — no NIC/EAL device): - StatusCompleteVsReconstructed: COMPLETE and RECONSTRUCTED both collapse onto MTL_FRAME_STATUS_COMPLETE; CORRUPTED -> INCOMPLETE + flag. - ConvertTransportToApp: derive is zero-copy (no converter); transport!=app runs the (stubbed) converter and the buffer carries the app fmt/size. - UserMetaPassthrough: frame_trans user_meta aliases into buf->user_meta. - VideoMetaFieldsFilled: pkts_total/pkts_recv[]/second_field/dimensions. - RtpTimestampAndTfmt: rtp_timestamp/tfmt/timestamp forwarded from meta. - FramebuffCntClampedToTwo: drives the real mtl_video_rx_session_init clamp via a stubbed st20_rx_create that captures ops.framebuff_cnt and fails. - BlockGetTimeoutSemantics: timeout_ms=0 on an empty ready_ring is a bounded poll returning -ETIMEDOUT, never a condvar block. - BuffersProcessedSiteCrossMode: pins divergence #4 — library-owned bumps buffers_processed at buffer_get, user-owned-post at notify_frame_ready. - AutoDetectPostsFormatEvent: video_rx_notify_detected posts MTL_EVENT_FORMAT_DETECTED with the detected geometry. - ExtFrameQueryWrapperBinds: mem_register + buffer_post + the query_ext_frame wrapper bind a posted user buffer to a transport ext_frame slot. Harness (st20_rx_harness.{c,h}) gains: caller-supplied-meta injection, a recorded video_convert_frame stub, an st20_rx_create capture stub for the clamp, an event ring + poll wrapper, user-owned-post setup, and mem_register/ buffer_post/query_ext_frame wrappers. Mirrored on St20PipelineRxTest (where a real old-pipeline analog exists): StatusReconstructedPassthrough (raw status, no collapse), RtpTimestampAndTfmt, VideoMetaFieldsFilled (pkts_total/second_field; pipeline copies no pkts_recv), and UserMetaPassthrough (value-copied into the framebuf user_meta buffer). The pipeline harness now injects caller-supplied meta and allocates per-framebuf user_meta scratch. Deferred to Phase 4 (integration), noted in the test file: - TimingParserPostsReport: MTL_EVENT_TIMING_REPORT is posted nowhere in lib/src/new_api; the timing-parser->event bridge is not yet wired, so there is no unit-tier production path to exercise. All 300 UnitTest cases pass (26 across the two RX suites); clang-format-14 and build.sh clean. --- tests/unit/new_api/st20_rx_harness.c | 168 +++++++++++++++++++- tests/unit/new_api/st20_rx_harness.h | 45 ++++++ tests/unit/new_api/st20_rx_test.cpp | 224 +++++++++++++++++++++++++++ tests/unit/pipeline/st20p_harness.c | 27 +++- tests/unit/pipeline/st20p_harness.h | 5 + tests/unit/pipeline/st20p_test.cpp | 85 ++++++++++ 6 files changed, 541 insertions(+), 13 deletions(-) diff --git a/tests/unit/new_api/st20_rx_harness.c b/tests/unit/new_api/st20_rx_harness.c index b17d364b4..6f8d8c83e 100644 --- a/tests/unit/new_api/st20_rx_harness.c +++ b/tests/unit/new_api/st20_rx_harness.c @@ -56,16 +56,53 @@ int st20_rx_reset_session_stats(st20_rx_handle handle) { return 0; } +/* + * Stub the format converter (defined in a separate TU, mt_session_video_common.c). + * Records each invocation so the convert-vs-derive contract can be asserted, and + * copies a byte so the destination buffer is observably written. + */ +static int ut20rx_convert_calls_g; + +int video_convert_frame(struct video_convert_ctx* cvt, void* src_data, + mtl_iova_t src_iova, size_t src_size, void* dst_data, + mtl_iova_t dst_iova, size_t dst_size, bool is_tx) { + (void)cvt; + (void)src_iova; + (void)dst_iova; + (void)is_tx; + ut20rx_convert_calls_g++; + if (dst_data && src_data && dst_size && src_size) { + *(uint8_t*)dst_data = *(const uint8_t*)src_data; + } + return 0; +} + +/* + * Stub st20_rx_create so the framebuff_cnt clamp in mtl_video_rx_session_init is + * reachable without a NIC: it captures the clamped ops.framebuff_cnt and returns + * NULL, making init bail out before any transport handle is dereferenced. + */ +static uint16_t ut20rx_captured_framebuff_cnt; + +st20_rx_handle st20_rx_create(mtl_handle mt, struct st20_rx_ops* ops) { + (void)mt; + ut20rx_captured_framebuff_cnt = ops ? ops->framebuff_cnt : 0; + return NULL; +} + /* ── opaque context ───────────────────────────────────────────────────── */ struct ut20rx_ctx { struct mtl_session_impl s; struct st_rx_video_session_impl rx_impl; struct video_rx_ctx vctx; + struct mtl_main_impl impl; struct st_frame_trans* frames; uint8_t* frame_storage; + void* app_buf_slots[64]; /* per-slot convert destinations (!derive) */ int framebuff_cnt; uint32_t enq_seq; /* advanced only on a successful enqueue */ + bool user_owned; /* user_buf ring initialized → uinit at destroy */ }; #include "new_api/st20_rx_harness.h" @@ -83,6 +120,8 @@ int ut20rx_init(void) { ut20rx_ctx* ut20rx_ctx_create(int framebuff_cnt) { static uint32_t ring_seq; + ut20rx_convert_calls_g = 0; + ut20rx_ctx* ctx = calloc(1, sizeof(*ctx)); if (!ctx) return NULL; ctx->framebuff_cnt = framebuff_cnt; @@ -115,11 +154,13 @@ ut20rx_ctx* ut20rx_ctx_create(int framebuff_cnt) { s->socket_id = rte_socket_id(); s->ownership = MTL_BUFFER_LIBRARY_OWNED; s->stopped = 0; - s->event_ring = NULL; /* events are best-effort; post just returns -EINVAL */ s->event_fd = -1; s->inner.video_rx = rx; snprintf(s->name, sizeof(s->name), "ut_new_rx"); + ctx->impl.type = MT_HANDLE_MAIN; + s->parent = &ctx->impl; + s->buffer_count = framebuff_cnt; s->buffers = calloc(framebuff_cnt, sizeof(struct mtl_buffer_impl)); if (!s->buffers) { @@ -152,6 +193,18 @@ ut20rx_ctx* ut20rx_ctx_create(int framebuff_cnt) { return NULL; } + snprintf(ring_name, sizeof(ring_name), "utnrxe_%u", ring_seq++); + s->event_ring = + rte_ring_create(ring_name, 64, s->socket_id, RING_F_SP_ENQ | RING_F_SC_DEQ); + if (!s->event_ring) { + rte_ring_free(v->ready_ring); + free(s->buffers); + free(ctx->frames); + free(ctx->frame_storage); + free(ctx); + return NULL; + } + rx->ops.priv = v; /* rx_ctx_from_session() reads this */ return ctx; @@ -159,6 +212,15 @@ ut20rx_ctx* ut20rx_ctx_create(int framebuff_cnt) { void ut20rx_ctx_destroy(ut20rx_ctx* ctx) { if (!ctx) return; + if (ctx->user_owned) mtl_session_user_buf_uinit(&ctx->s); + if (ctx->s.event_ring) { + void* obj = NULL; + while (rte_ring_dequeue(ctx->s.event_ring, &obj) == 0 && obj) { + mt_rte_free(obj); + obj = NULL; + } + rte_ring_free(ctx->s.event_ring); + } if (ctx->vctx.ready_ring) rte_ring_free(ctx->vctx.ready_ring); free(ctx->s.buffers); free(ctx->frames); @@ -170,11 +232,6 @@ void ut20rx_ctx_destroy(ut20rx_ctx* ctx) { int ut20rx_inject_frame(ut20rx_ctx* ctx, enum st_frame_status status, uint32_t timestamp) { - /* Pick the frame address from the cyclic pool. enq_seq advances only on a - * successful enqueue, so the live (undrained) window of the FIFO ready_ring - * always maps to distinct frame_trans slots. */ - void* frame = ctx->frames[ctx->enq_seq % ctx->framebuff_cnt].addr; - struct st20_rx_frame_meta meta; memset(&meta, 0, sizeof(meta)); meta.status = status; @@ -183,9 +240,18 @@ int ut20rx_inject_frame(ut20rx_ctx* ctx, enum st_frame_status status, meta.frame_total_size = 1; meta.frame_recv_size = 1; meta.pkts_total = 1; + return ut20rx_inject_meta(ctx, &meta); +} +int ut20rx_inject_meta(ut20rx_ctx* ctx, const struct st20_rx_frame_meta* meta) { + /* Pick the frame address from the cyclic pool. enq_seq advances only on a + * successful enqueue, so the live (undrained) window of the FIFO ready_ring + * always maps to distinct frame_trans slots. */ + void* frame = ctx->frames[ctx->enq_seq % ctx->framebuff_cnt].addr; + + struct st20_rx_frame_meta local = *meta; uint64_t dropped_before = ctx->s.stats.buffers_dropped; - video_rx_notify_frame_ready(&ctx->vctx, frame, &meta); + video_rx_notify_frame_ready(&ctx->vctx, frame, &local); if (ctx->s.stats.buffers_dropped != dropped_before) { /* ready_ring was full — frame returned to library, counted as dropped. */ @@ -195,6 +261,94 @@ int ut20rx_inject_frame(ut20rx_ctx* ctx, enum st_frame_status status, return 0; } +void ut20rx_set_frame_user_meta(ut20rx_ctx* ctx, int idx, void* meta, size_t size) { + ctx->frames[idx].user_meta = meta; + ctx->frames[idx].user_meta_data_size = size; +} + +void ut20rx_enable_convert(ut20rx_ctx* ctx, enum st_frame_fmt app_fmt, void* app_buf, + size_t app_size) { + struct video_convert_ctx* cvt = &ctx->vctx.convert; + cvt->derive = false; + cvt->frame_fmt = app_fmt; + cvt->app_frame_size = app_size; + for (int i = 0; i < ctx->framebuff_cnt && i < 64; i++) ctx->app_buf_slots[i] = app_buf; + cvt->app_bufs = ctx->app_buf_slots; + cvt->app_bufs_cnt = ctx->framebuff_cnt; +} + +int ut20rx_convert_calls(const ut20rx_ctx* ctx) { + (void)ctx; + return ut20rx_convert_calls_g; +} + +int ut20rx_notify_detected(ut20rx_ctx* ctx, uint32_t width, uint32_t height, + enum st_fps fps, enum st20_packing packing, bool interlaced) { + struct st20_detect_meta meta; + memset(&meta, 0, sizeof(meta)); + meta.width = width; + meta.height = height; + meta.fps = fps; + meta.packing = packing; + meta.interlaced = interlaced; + struct st20_detect_reply reply; + memset(&reply, 0, sizeof(reply)); + return video_rx_notify_detected(&ctx->vctx, &meta, &reply); +} + +int ut20rx_poll_event(ut20rx_ctx* ctx, mtl_event_t* event) { + return mtl_video_rx_vtable.event_poll(&ctx->s, event, 0 /* non-blocking */); +} + +int ut20rx_enable_user_owned_post(ut20rx_ctx* ctx) { + ctx->s.ownership = MTL_BUFFER_USER_OWNED; + ctx->vctx.user_query_ext_frame = NULL; + int ret = mtl_session_user_buf_init(&ctx->s, ctx->framebuff_cnt); + if (ret == 0) ctx->user_owned = true; + return ret; +} + +int ut20rx_mem_register(ut20rx_ctx* ctx, void* addr, size_t size) { + mtl_dma_mem_t* handle = NULL; + return mtl_video_rx_vtable.mem_register(&ctx->s, addr, size, &handle); +} + +int ut20rx_post_user_buffer(ut20rx_ctx* ctx, void* data, size_t size, void* user_ctx) { + return mtl_video_rx_vtable.buffer_post(&ctx->s, data, size, user_ctx); +} + +int ut20rx_query_ext_frame(ut20rx_ctx* ctx, struct st20_ext_frame* out, + struct st20_rx_frame_meta* meta) { + return video_rx_query_ext_frame_wrapper(&ctx->vctx, out, meta); +} + +uint16_t ut20rx_clamp_framebuff_cnt(uint32_t requested) { + struct mtl_session_impl s; + memset(&s, 0, sizeof(s)); + s.socket_id = rte_socket_id(); + s.ownership = MTL_BUFFER_LIBRARY_OWNED; + snprintf(s.name, sizeof(s.name), "ut_clamp"); + + struct mtl_main_impl impl; + memset(&impl, 0, sizeof(impl)); + impl.type = MT_HANDLE_MAIN; + + mtl_video_config_t cfg; + memset(&cfg, 0, sizeof(cfg)); + cfg.base.name = "ut_clamp"; + cfg.base.num_buffers = requested; + cfg.width = 1920; + cfg.height = 1080; + cfg.fps = ST_FPS_P59_94; + /* derive pair so video_convert_ctx_init returns early (no converter lookup) */ + cfg.transport_fmt = ST20_FMT_YUV_422_10BIT; + cfg.frame_fmt = ST_FRAME_FMT_YUV422RFC4175PG2BE10; + + ut20rx_captured_framebuff_cnt = 0; + (void)mtl_video_rx_session_init(&s, &impl, &cfg); + return ut20rx_captured_framebuff_cnt; +} + /* ── buffer get/put ───────────────────────────────────────────────────── */ mtl_buffer_t* ut20rx_buffer_get(ut20rx_ctx* ctx) { diff --git a/tests/unit/new_api/st20_rx_harness.h b/tests/unit/new_api/st20_rx_harness.h index c112749fc..5301cf72e 100644 --- a/tests/unit/new_api/st20_rx_harness.h +++ b/tests/unit/new_api/st20_rx_harness.h @@ -46,6 +46,51 @@ void ut20rx_ctx_destroy(ut20rx_ctx* ctx); * full (drives the buffers_dropped path). */ int ut20rx_inject_frame(ut20rx_ctx* ctx, enum st_frame_status status, uint32_t timestamp); +/** Inject one frame with caller-supplied metadata. The harness picks the + * cyclic frame slot; the caller owns every meta field (status, tfmt, + * rtp_timestamp, pkts_total, pkts_recv[], second_field, ...). Same return + * contract as ut20rx_inject_frame. */ +int ut20rx_inject_meta(ut20rx_ctx* ctx, const struct st20_rx_frame_meta* meta); + +/** Attach user metadata to a frame slot before injecting it, exercising the + * rx_fill_user_metadata pass-through (frame_trans->user_meta → buf->user_meta). */ +void ut20rx_set_frame_user_meta(ut20rx_ctx* ctx, int idx, void* meta, size_t size); + +/** Switch out of derive mode so buffer_get runs rx_convert_and_fill_buffer. + * Provides one app-format destination buffer for every frame slot. */ +void ut20rx_enable_convert(ut20rx_ctx* ctx, enum st_frame_fmt app_fmt, void* app_buf, + size_t app_size); + +/** Count of times the stubbed converter ran since ctx_create (0 in derive mode). */ +int ut20rx_convert_calls(const ut20rx_ctx* ctx); + +/** Drive video_rx_notify_detected() directly (auto-detect format event). */ +int ut20rx_notify_detected(ut20rx_ctx* ctx, uint32_t width, uint32_t height, + enum st_fps fps, enum st20_packing packing, bool interlaced); + +/** Non-blocking drain of one session event (shared video_session_event_poll). */ +int ut20rx_poll_event(ut20rx_ctx* ctx, mtl_event_t* event); + +/** Reconfigure the session for USER_OWNED buffer-post mode (no app + * query_ext_frame). Returns 0 on success. */ +int ut20rx_enable_user_owned_post(ut20rx_ctx* ctx); + +/** Wraps the mem_register vtable entry (user-owned zero-copy registration). */ +int ut20rx_mem_register(ut20rx_ctx* ctx, void* addr, size_t size); + +/** Wraps the buffer_post vtable entry (post a user buffer for receiving). */ +int ut20rx_post_user_buffer(ut20rx_ctx* ctx, void* data, size_t size, void* user_ctx); + +/** Drive the query_ext_frame wrapper directly; on success *out binds to a + * posted user buffer slot. */ +int ut20rx_query_ext_frame(ut20rx_ctx* ctx, struct st20_ext_frame* out, + struct st20_rx_frame_meta* meta); + +/** Drive the framebuff_cnt < 2 clamp through mtl_video_rx_session_init without a + * NIC: st20_rx_create is stubbed to capture ops.framebuff_cnt and fail, so init + * returns before any transport dereference. Returns the clamped framebuff_cnt. */ +uint16_t ut20rx_clamp_framebuff_cnt(uint32_t requested); + /** Wraps video_rx_buffer_get() with timeout_ms=0 (non-blocking). */ mtl_buffer_t* ut20rx_buffer_get(ut20rx_ctx* ctx); diff --git a/tests/unit/new_api/st20_rx_test.cpp b/tests/unit/new_api/st20_rx_test.cpp index 541fccbec..307438b88 100644 --- a/tests/unit/new_api/st20_rx_test.cpp +++ b/tests/unit/new_api/st20_rx_test.cpp @@ -217,3 +217,227 @@ TEST_F(St20NewApiRxTest, DroppedMonotonicWithOverflow) { << ": buffers_dropped must bump exactly once per event"; } } + +/* rx_fill_buffer_status collapses both COMPLETE and RECONSTRUCTED transport + * statuses onto MTL_FRAME_STATUS_COMPLETE (no INCOMPLETE flag); any other + * status (CORRUPTED) surfaces MTL_FRAME_STATUS_INCOMPLETE + the flag. This is a + * deliberate divergence from the pipeline, which passes the raw status through + * (RECONSTRUCTED stays RECONSTRUCTED — see St20PipelineRxTest). */ +TEST_F(St20NewApiRxTest, StatusCompleteVsReconstructed) { + ASSERT_EQ(ut20rx_inject_frame(ctx_, ST_FRAME_STATUS_COMPLETE, 1000), 0); + ASSERT_EQ(ut20rx_inject_frame(ctx_, ST_FRAME_STATUS_RECONSTRUCTED, 2000), 0); + ASSERT_EQ(ut20rx_inject_frame(ctx_, ST_FRAME_STATUS_CORRUPTED, 3000), 0); + + mtl_buffer_t* b0 = get_buffer(); + ASSERT_NE(b0, nullptr); + EXPECT_EQ(b0->status, MTL_FRAME_STATUS_COMPLETE); + EXPECT_EQ(b0->flags & MTL_BUF_FLAG_INCOMPLETE, 0u); + EXPECT_EQ(put_buffer(b0), 0); + + mtl_buffer_t* b1 = get_buffer(); + ASSERT_NE(b1, nullptr); + EXPECT_EQ(b1->status, MTL_FRAME_STATUS_COMPLETE) + << "RECONSTRUCTED must collapse onto COMPLETE in the unified API"; + EXPECT_EQ(b1->flags & MTL_BUF_FLAG_INCOMPLETE, 0u); + EXPECT_EQ(put_buffer(b1), 0); + + mtl_buffer_t* b2 = get_buffer(); + ASSERT_NE(b2, nullptr); + EXPECT_EQ(b2->status, MTL_FRAME_STATUS_INCOMPLETE); + EXPECT_NE(b2->flags & MTL_BUF_FLAG_INCOMPLETE, 0u); + EXPECT_EQ(put_buffer(b2), 0); +} + +/* rx_convert_and_fill_buffer: derive mode hands the app the transport + * framebuffer with no converter call; a transport-fmt != app-fmt session runs + * the converter and the buffer carries the app fmt/size. */ +TEST_F(St20NewApiRxTest, ConvertTransportToApp) { + /* derive (default): no converter, zero-copy transport buffer */ + ASSERT_EQ(inject_complete(1000), 0); + mtl_buffer_t* d = get_buffer(); + ASSERT_NE(d, nullptr); + EXPECT_EQ(ut20rx_convert_calls(ctx_), 0) << "derive mode must not convert"; + EXPECT_EQ(put_buffer(d), 0); + + /* fresh ctx switched into convert mode */ + ut20rx_ctx_destroy(ctx_); + ctx_ = ut20rx_ctx_create(/*framebuff_cnt=*/3); + ASSERT_NE(ctx_, nullptr); + + static uint8_t app_buf[64]; + ut20rx_enable_convert(ctx_, ST_FRAME_FMT_YUV422PLANAR10LE, app_buf, sizeof(app_buf)); + + ASSERT_EQ(inject_complete(2000), 0); + mtl_buffer_t* c = get_buffer(); + ASSERT_NE(c, nullptr); + EXPECT_EQ(ut20rx_convert_calls(ctx_), 1) << "convert mode must run the converter once"; + EXPECT_EQ(c->video.fmt, ST_FRAME_FMT_YUV422PLANAR10LE) + << "converted buffer must carry the app fmt"; + EXPECT_EQ(c->data, app_buf) << "converted buffer must point at the app destination"; + EXPECT_EQ(c->size, sizeof(app_buf)); + EXPECT_EQ(put_buffer(c), 0); +} + +/* rx_fill_user_metadata copies the frame_trans user_meta pointer/size into the + * delivered buffer (RX pass-through). */ +TEST_F(St20NewApiRxTest, UserMetaPassthrough) { + static uint8_t meta_blob[16] = {0xDE, 0xAD, 0xBE, 0xEF}; + ut20rx_set_frame_user_meta(ctx_, /*idx=*/0, meta_blob, sizeof(meta_blob)); + + ASSERT_EQ(inject_complete(1000), 0); + mtl_buffer_t* b = get_buffer(); + ASSERT_NE(b, nullptr); + EXPECT_EQ(b->user_meta, meta_blob); + EXPECT_EQ(b->user_meta_size, sizeof(meta_blob)); + EXPECT_EQ(put_buffer(b), 0); +} + +/* rx_fill_buffer_video_fields reflects the synthetic st20_rx_frame_meta into the + * buffer's video sub-struct (pkts_total/pkts_recv/second_field/dimensions); + * interlaced is carried from the convert ctx (false by default here). */ +TEST_F(St20NewApiRxTest, VideoMetaFieldsFilled) { + struct st20_rx_frame_meta meta {}; + meta.status = ST_FRAME_STATUS_COMPLETE; + meta.timestamp = 4242; + meta.frame_total_size = 1; + meta.frame_recv_size = 1; + meta.width = 1920; + meta.height = 1080; + meta.pkts_total = 100; + meta.pkts_recv[0] = 90; + meta.pkts_recv[1] = 80; + meta.second_field = true; + ASSERT_EQ(ut20rx_inject_meta(ctx_, &meta), 0); + + mtl_buffer_t* b = get_buffer(); + ASSERT_NE(b, nullptr); + EXPECT_EQ(b->video.width, 1920u); + EXPECT_EQ(b->video.height, 1080u); + EXPECT_EQ(b->video.pkts_total, 100u); + EXPECT_EQ(b->video.pkts_recv[0], 90u); + EXPECT_EQ(b->video.pkts_recv[1], 80u); + EXPECT_TRUE(b->video.second_field); + EXPECT_FALSE(b->video.interlaced) << "interlaced is convert-ctx driven, false here"; + EXPECT_EQ(put_buffer(b), 0); +} + +/* rx_fill_buffer_status forwards rtp_timestamp/tfmt/timestamp from the meta. */ +TEST_F(St20NewApiRxTest, RtpTimestampAndTfmt) { + struct st20_rx_frame_meta meta {}; + meta.status = ST_FRAME_STATUS_COMPLETE; + meta.frame_total_size = 1; + meta.frame_recv_size = 1; + meta.pkts_total = 1; + meta.rtp_timestamp = 0xCAFEu; + meta.tfmt = ST10_TIMESTAMP_FMT_TAI; + meta.timestamp = 0x123456789ULL; + ASSERT_EQ(ut20rx_inject_meta(ctx_, &meta), 0); + + mtl_buffer_t* b = get_buffer(); + ASSERT_NE(b, nullptr); + EXPECT_EQ(b->rtp_timestamp, 0xCAFEu); + EXPECT_EQ(b->tfmt, ST10_TIMESTAMP_FMT_TAI); + EXPECT_EQ(b->timestamp, 0x123456789ULL); + EXPECT_EQ(put_buffer(b), 0); +} + +/* mtl_video_rx_session_init clamps a requested framebuff_cnt < 2 up to 2 (the + * pipeline does the same — mirrored in St20PipelineRxTest). Counts >= 2 pass + * through unchanged. */ +TEST_F(St20NewApiRxTest, FramebuffCntClampedToTwo) { + EXPECT_EQ(ut20rx_clamp_framebuff_cnt(0), 2u); + EXPECT_EQ(ut20rx_clamp_framebuff_cnt(1), 2u); + EXPECT_EQ(ut20rx_clamp_framebuff_cnt(2), 2u); + EXPECT_EQ(ut20rx_clamp_framebuff_cnt(5), 5u); +} + +/* video_rx_buffer_get(timeout_ms=0) on an empty ready_ring returns -ETIMEDOUT + * immediately (the harness wrapper surfaces that as NULL); a queued frame is + * returned. Divergence from the pipeline: the unified RX get is a bounded poll, + * not a condvar wait — timeout_ms=0 never blocks. */ +TEST_F(St20NewApiRxTest, BlockGetTimeoutSemantics) { + EXPECT_EQ(get_buffer(), nullptr) << "empty ready_ring + timeout 0 must not block"; + + ASSERT_EQ(inject_complete(1000), 0); + mtl_buffer_t* b = get_buffer(); + ASSERT_NE(b, nullptr) << "a queued frame is returned immediately"; + EXPECT_EQ(put_buffer(b), 0); + + EXPECT_EQ(get_buffer(), nullptr) << "ring drained again → -ETIMEDOUT"; +} + +/* Characterization of divergence #4: the buffers_processed bump site depends on + * ownership. LIBRARY_OWNED bumps in buffer_get (app consumes); USER_OWNED via + * buffer_post bumps in notify_frame_ready (the lib copies into the user buffer + * and returns the frame immediately, before any buffer_get). Exactly once per + * delivered frame in each mode. */ +TEST_F(St20NewApiRxTest, BuffersProcessedSiteCrossMode) { + /* LIBRARY_OWNED: no bump at ingress, one bump at buffer_get */ + ASSERT_EQ(inject_complete(1000), 0); + EXPECT_EQ(buffers_processed(), 0u) << "library-owned: ingress must not bump"; + mtl_buffer_t* b = get_buffer(); + ASSERT_NE(b, nullptr); + EXPECT_EQ(buffers_processed(), 1u) << "library-owned: buffer_get bumps once"; + EXPECT_EQ(put_buffer(b), 0); + + /* USER_OWNED post: the bump moves to ingress (notify_frame_ready) */ + ut20rx_ctx_destroy(ctx_); + ctx_ = ut20rx_ctx_create(/*framebuff_cnt=*/3); + ASSERT_NE(ctx_, nullptr); + ASSERT_EQ(ut20rx_enable_user_owned_post(ctx_), 0); + + static uint8_t user_buf[64]; + ASSERT_EQ(ut20rx_mem_register(ctx_, user_buf, sizeof(user_buf)), 0); + ASSERT_EQ(ut20rx_post_user_buffer(ctx_, user_buf, sizeof(user_buf), (void*)0x77), 0); + + ASSERT_EQ(inject_complete(2000), 0); + EXPECT_EQ(buffers_processed(), 1u) + << "user-owned post: notify_frame_ready bumps once, no buffer_get needed"; +} + +/* video_rx_notify_detected posts MTL_EVENT_FORMAT_DETECTED carrying the detected + * geometry. Reachable without a NIC by driving the static callback directly. */ +TEST_F(St20NewApiRxTest, AutoDetectPostsFormatEvent) { + ASSERT_EQ( + ut20rx_notify_detected(ctx_, 1920, 1080, ST_FPS_P59_94, ST20_PACKING_BPM, false), + 0); + + mtl_event_t ev{}; + ASSERT_EQ(ut20rx_poll_event(ctx_, &ev), 0) << "a FORMAT_DETECTED event must be posted"; + EXPECT_EQ(ev.type, MTL_EVENT_FORMAT_DETECTED); + EXPECT_EQ(ev.format_detected.width, 1920u); + EXPECT_EQ(ev.format_detected.height, 1080u); +} + +/* integration-only: MTL_EVENT_TIMING_REPORT (case 10, TimingParserPostsReport) + * is not posted anywhere in lib/src/new_api — the timing-parser report path is + * not yet wired into the unified session, so there is no production code to + * exercise at the unit tier. Route to Phase 4 once the parser → event bridge + * lands. */ + +/* The query_ext_frame wrapper (USER_OWNED, no app callback) binds a buffer + * posted via buffer_post to the next transport ext_frame slot: addr/len/opaque + * come straight off the posted user buffer. Exercises mem_register + buffer_post + * + the wrapper end to end. */ +TEST_F(St20NewApiRxTest, ExtFrameQueryWrapperBinds) { + ut20rx_ctx_destroy(ctx_); + ctx_ = ut20rx_ctx_create(/*framebuff_cnt=*/3); + ASSERT_NE(ctx_, nullptr); + ASSERT_EQ(ut20rx_enable_user_owned_post(ctx_), 0); + + static uint8_t user_buf[128]; + ASSERT_EQ(ut20rx_mem_register(ctx_, user_buf, sizeof(user_buf)), 0); + ASSERT_EQ(ut20rx_post_user_buffer(ctx_, user_buf, sizeof(user_buf), (void*)0x99), 0); + + struct st20_rx_frame_meta meta {}; + meta.width = 1920; + meta.height = 1080; + meta.frame_total_size = sizeof(user_buf); + + struct st20_ext_frame ext {}; + ASSERT_EQ(ut20rx_query_ext_frame(ctx_, &ext, &meta), 0) + << "a posted user buffer must be available to bind"; + EXPECT_EQ(ext.buf_addr, user_buf); + EXPECT_EQ(ext.buf_len, sizeof(user_buf)); + EXPECT_EQ(ext.opaque, (void*)0x99); +} diff --git a/tests/unit/pipeline/st20p_harness.c b/tests/unit/pipeline/st20p_harness.c index 863fc511b..e86df29b6 100644 --- a/tests/unit/pipeline/st20p_harness.c +++ b/tests/unit/pipeline/st20p_harness.c @@ -67,6 +67,8 @@ struct ut20p_ctx { #include "pipeline/st20p_harness.h" +#define UT20P_USER_META_SIZE 64 /* per-framebuf user_meta scratch */ + /* ── init ─────────────────────────────────────────────────────────────── */ int ut20p_init(void) { @@ -95,6 +97,16 @@ ut20p_ctx* ut20p_ctx_create(int framebuff_cnt) { * `dst = src`, so setting src.priv is sufficient. */ ctx->framebuffs[i].src.priv = &ctx->framebuffs[i]; ctx->framebuffs[i].dst.priv = &ctx->framebuffs[i]; + /* mirrors production user_meta alloc so frame_ready can copy meta->user_meta + * and get_frame can surface it. */ + ctx->framebuffs[i].user_meta_buffer_size = UT20P_USER_META_SIZE; + ctx->framebuffs[i].user_meta = calloc(1, UT20P_USER_META_SIZE); + if (!ctx->framebuffs[i].user_meta) { + for (int j = 0; j < i; j++) free(ctx->framebuffs[j].user_meta); + free(ctx->framebuffs); + free(ctx); + return NULL; + } } struct st20p_rx_ctx* p = &ctx->pipeline; @@ -124,6 +136,7 @@ ut20p_ctx* ut20p_ctx_create(int framebuff_cnt) { void ut20p_ctx_destroy(ut20p_ctx* ctx) { if (!ctx) return; pthread_mutex_destroy(&ctx->pipeline.lock); + for (int i = 0; i < ctx->framebuff_cnt; i++) free(ctx->framebuffs[i].user_meta); free(ctx->framebuffs); free(ctx); } @@ -131,11 +144,6 @@ void ut20p_ctx_destroy(ut20p_ctx* ctx) { /* ── inject one frame via rx_st20p_frame_ready ────────────────────────── */ int ut20p_inject_frame(ut20p_ctx* ctx, enum st_frame_status status, uint32_t timestamp) { - /* Stack-allocated synthetic meta — frame_ready copies what it needs - * into the framebuf. The frame addr only needs to be a stable - * non-NULL pointer; with derive=true it is never dereferenced by the - * pipeline before delivery, and our st20_rx_put_framebuff stub - * ignores it on release. */ struct st20_rx_frame_meta meta; memset(&meta, 0, sizeof(meta)); meta.status = status; @@ -144,9 +152,16 @@ int ut20p_inject_frame(ut20p_ctx* ctx, enum st_frame_status status, uint32_t tim meta.frame_total_size = 1; meta.frame_recv_size = 1; meta.pkts_total = 1; + return ut20p_inject_meta(ctx, &meta); +} +int ut20p_inject_meta(ut20p_ctx* ctx, const struct st20_rx_frame_meta* meta) { + /* The frame addr only needs to be a stable non-NULL pointer; with + * derive=true it is never dereferenced by the pipeline before delivery, and + * our st20_rx_put_framebuff stub ignores it on release. */ static uint8_t dummy_frame_storage; /* address-stable sentinel */ - return rx_st20p_frame_ready(&ctx->pipeline, &dummy_frame_storage, &meta); + struct st20_rx_frame_meta local = *meta; + return rx_st20p_frame_ready(&ctx->pipeline, &dummy_frame_storage, &local); } /* ── frame get/put ────────────────────────────────────────────────────── */ diff --git a/tests/unit/pipeline/st20p_harness.h b/tests/unit/pipeline/st20p_harness.h index 6d0d9f9e5..2a75e3dfd 100644 --- a/tests/unit/pipeline/st20p_harness.h +++ b/tests/unit/pipeline/st20p_harness.h @@ -44,6 +44,11 @@ void ut20p_ctx_destroy(ut20p_ctx* ctx); * stat_frames_dropped path). */ int ut20p_inject_frame(ut20p_ctx* ctx, enum st_frame_status status, uint32_t timestamp); +/** Inject one frame with caller-supplied metadata (status, tfmt, rtp_timestamp, + * pkts_total, second_field, user_meta, ...). Same return contract as + * ut20p_inject_frame. */ +int ut20p_inject_meta(ut20p_ctx* ctx, const struct st20_rx_frame_meta* meta); + /** Wraps st20p_rx_get_frame(). */ struct st_frame* ut20p_get_frame(ut20p_ctx* ctx); diff --git a/tests/unit/pipeline/st20p_test.cpp b/tests/unit/pipeline/st20p_test.cpp index 6ec26671e..92a508ad7 100644 --- a/tests/unit/pipeline/st20p_test.cpp +++ b/tests/unit/pipeline/st20p_test.cpp @@ -13,6 +13,8 @@ #include +#include + #include "pipeline/st20p_harness.h" class St20PipelineRxTest : public ::testing::Test { @@ -188,3 +190,86 @@ TEST_F(St20PipelineRxTest, BusyEqualsDroppedInvariant) { << "after drop " << (i + 1) << ": stat_busy and frames_dropped diverged"; } } + +/* Pipeline passes the transport frame status through unchanged: a RECONSTRUCTED + * frame is delivered as RECONSTRUCTED (not collapsed to COMPLETE). This is the + * old-pipeline analog of the new-API St20NewApiRxTest.StatusCompleteVsReconstructed, + * which DOES collapse RECONSTRUCTED onto MTL_FRAME_STATUS_COMPLETE — the two + * suites pin opposite, deliberate mappings. */ +TEST_F(St20PipelineRxTest, StatusReconstructedPassthrough) { + ASSERT_EQ(ut20p_inject_frame(ctx_, ST_FRAME_STATUS_COMPLETE, 1000), 0); + ASSERT_EQ(ut20p_inject_frame(ctx_, ST_FRAME_STATUS_RECONSTRUCTED, 2000), 0); + + struct st_frame* f0 = get_frame(); + ASSERT_NE(f0, nullptr); + EXPECT_EQ(f0->status, ST_FRAME_STATUS_COMPLETE); + EXPECT_EQ(put_frame(f0), 0); + + struct st_frame* f1 = get_frame(); + ASSERT_NE(f1, nullptr); + EXPECT_EQ(f1->status, ST_FRAME_STATUS_RECONSTRUCTED) + << "pipeline forwards the raw transport status, no COMPLETE collapse"; + EXPECT_EQ(put_frame(f1), 0); +} + +/* rx_st20p_frame_ready forwards rtp_timestamp/tfmt/timestamp from the meta onto + * the delivered st_frame (analog of St20NewApiRxTest.RtpTimestampAndTfmt). */ +TEST_F(St20PipelineRxTest, RtpTimestampAndTfmt) { + struct st20_rx_frame_meta meta {}; + meta.status = ST_FRAME_STATUS_COMPLETE; + meta.frame_total_size = 1; + meta.frame_recv_size = 1; + meta.pkts_total = 1; + meta.rtp_timestamp = 0xCAFEu; + meta.tfmt = ST10_TIMESTAMP_FMT_TAI; + meta.timestamp = 0x123456789ULL; + ASSERT_EQ(ut20p_inject_meta(ctx_, &meta), 0); + + struct st_frame* f = get_frame(); + ASSERT_NE(f, nullptr); + EXPECT_EQ(f->rtp_timestamp, 0xCAFEu); + EXPECT_EQ(f->tfmt, ST10_TIMESTAMP_FMT_TAI); + EXPECT_EQ(f->timestamp, 0x123456789ULL); + EXPECT_EQ(put_frame(f), 0); +} + +/* rx_st20p_frame_ready forwards pkts_total/second_field onto the st_frame. + * Note the divergence from the new API: the pipeline does NOT copy pkts_recv[] + * (only pkts_total), whereas rx_fill_buffer_video_fields fills pkts_recv too. */ +TEST_F(St20PipelineRxTest, VideoMetaFieldsFilled) { + struct st20_rx_frame_meta meta {}; + meta.status = ST_FRAME_STATUS_COMPLETE; + meta.frame_total_size = 1; + meta.frame_recv_size = 1; + meta.pkts_total = 100; + meta.second_field = true; + ASSERT_EQ(ut20p_inject_meta(ctx_, &meta), 0); + + struct st_frame* f = get_frame(); + ASSERT_NE(f, nullptr); + EXPECT_EQ(f->pkts_total, 100u); + EXPECT_TRUE(f->second_field); + EXPECT_EQ(put_frame(f), 0); +} + +/* rx_st20p_frame_ready copies meta->user_meta into the framebuf's user_meta + * buffer and get_frame surfaces it (analog of St20NewApiRxTest.UserMetaPassthrough; + * the pipeline copies by value into its own buffer rather than aliasing). */ +TEST_F(St20PipelineRxTest, UserMetaPassthrough) { + static const uint8_t blob[8] = {0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03, 0x04}; + struct st20_rx_frame_meta meta {}; + meta.status = ST_FRAME_STATUS_COMPLETE; + meta.frame_total_size = 1; + meta.frame_recv_size = 1; + meta.pkts_total = 1; + meta.user_meta = blob; + meta.user_meta_size = sizeof(blob); + ASSERT_EQ(ut20p_inject_meta(ctx_, &meta), 0); + + struct st_frame* f = get_frame(); + ASSERT_NE(f, nullptr); + ASSERT_NE(f->user_meta, nullptr); + EXPECT_EQ(f->user_meta_size, sizeof(blob)); + EXPECT_EQ(memcmp(f->user_meta, blob, sizeof(blob)), 0); + EXPECT_EQ(put_frame(f), 0); +} From 06a19330738bacd8788416481db5a4d0da08e0ba Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 9 Jun 2026 10:51:30 +0000 Subject: [PATCH 07/16] new_api: fix drop-when-late TAI gate + user-owned completion (review) BLOCKER 1: tx_frame_is_late treated an unstamped slot (timestamp==0, tfmt==0==TAI) as infinitely late and silently dropped it. Gate the late check on timestamp != 0 so only frames the user actually stamped with TAI are eligible. BLOCKER 2: the drop path recycled the slot and posted only FRAME_LATE, so in MTL_BUFFER_USER_OWNED mode the external buffer (s->user_buf_ctx[i]) was never returned and leaked. Mirror video_tx_notify_frame_done: retrieve and clear user_buf_ctx[i] and post MTL_EVENT_BUFFER_DONE with user_ctx in addition to FRAME_LATE. Remove the unreachable MT_VIDEO_TX_DROP_MAX_BATCH cap (the for-loop is already bounded by the framebuffer count; tripping it would have transmitted a late frame) and the redundant frame_late.epoch_skipped=0. --- lib/src/new_api/mt_session_video_tx.c | 25 ++++++----- tests/unit/new_api/st20_tx_harness.c | 19 ++++++++ tests/unit/new_api/st20_tx_harness.h | 11 +++++ tests/unit/new_api/st20_tx_test.cpp | 65 +++++++++++++++++++++++++++ 4 files changed, 110 insertions(+), 10 deletions(-) diff --git a/lib/src/new_api/mt_session_video_tx.c b/lib/src/new_api/mt_session_video_tx.c index eead5441c..66155bc9a 100644 --- a/lib/src/new_api/mt_session_video_tx.c +++ b/lib/src/new_api/mt_session_video_tx.c @@ -15,10 +15,6 @@ #include "../mt_log.h" #include "../mt_mem.h" -/* Cap on frames dropped per get_next_frame, mirroring the pipeline's - * ST_TX_DROP_MAX_BATCH, so a backlog cannot stall the datapath thread. */ -#define MT_VIDEO_TX_DROP_MAX_BATCH (80) - /************************************************************************* * TX Frame State Machine * @@ -81,7 +77,8 @@ static bool tx_frame_is_late(struct video_tx_ctx* ctx, struct mtl_session_impl* s = ctx->session; if (!(s->flags & MTL_SESSION_FLAG_USER_PACING)) return false; struct st20_tx_frame_meta* meta = &tx_impl->st20_frames[idx].tv_meta; - if (meta->tfmt != ST10_TIMESTAMP_FMT_TAI) return false; + /* timestamp==0 marks an unstamped slot (tfmt==0 also == TAI), never late */ + if (meta->timestamp == 0 || meta->tfmt != ST10_TIMESTAMP_FMT_TAI) return false; uint64_t frame_tai = meta->timestamp; uint64_t cur_tai = mt_get_ptp_time(s->parent, MTL_PORT_P); uint64_t frame_period_ns = (uint64_t)((double)NS_PER_S / st_frame_rate(ctx->fps)); @@ -179,21 +176,29 @@ static int video_tx_get_next_frame(void* priv, uint16_t* next_frame_idx, } } - uint16_t drop_cnt = 0; for (uint16_t i = 0; i < tx_impl->st20_frames_cnt; i++) { enum tx_frame_state expected = TX_FRAME_READY; if (__atomic_compare_exchange_n(&ctx->frame_state[i], &expected, TX_FRAME_TRANSMITTING, false, __ATOMIC_ACQUIRE, __ATOMIC_RELAXED)) { - if (ctx->drop_when_late && drop_cnt < MT_VIDEO_TX_DROP_MAX_BATCH && - tx_frame_is_late(ctx, tx_impl, i)) { + if (ctx->drop_when_late && tx_frame_is_late(ctx, tx_impl, i)) { __atomic_add_fetch(&s->stats.buffers_dropped, 1, __ATOMIC_RELAXED); + if (s->ownership == MTL_BUFFER_USER_OWNED) { + /* Return the external buffer to the app, mirroring frame_done. */ + void* user_ctx = NULL; + if (s->user_buf_ctx && i < s->user_buf_ctx_cnt) { + user_ctx = s->user_buf_ctx[i]; + s->user_buf_ctx[i] = NULL; + } + mtl_event_t done = {0}; + done.type = MTL_EVENT_BUFFER_DONE; + done.ctx = user_ctx; + mtl_session_event_post(s, &done); + } mtl_event_t event = {0}; event.type = MTL_EVENT_FRAME_LATE; - event.frame_late.epoch_skipped = 0; mtl_session_event_post(s, &event); __atomic_store_n(&ctx->frame_state[i], TX_FRAME_FREE, __ATOMIC_RELEASE); - drop_cnt++; continue; } *next_frame_idx = i; diff --git a/tests/unit/new_api/st20_tx_harness.c b/tests/unit/new_api/st20_tx_harness.c index 00a8e6c3e..1581bbbb7 100644 --- a/tests/unit/new_api/st20_tx_harness.c +++ b/tests/unit/new_api/st20_tx_harness.c @@ -182,6 +182,7 @@ ut20tx_ctx* ut20tx_ctx_create(int framebuff_cnt) { void ut20tx_ctx_destroy(ut20tx_ctx* ctx) { if (!ctx) return; + mtl_session_user_buf_uinit(&ctx->s); if (ctx->s.event_ring) rte_ring_free(ctx->s.event_ring); free(ctx->vctx.frame_state); free(ctx->s.buffers); @@ -211,6 +212,24 @@ void ut20tx_set_ptp_now(ut20tx_ctx* ctx, uint64_t ns) { ctx->impl.ptp_usync = ns; } +int ut20tx_set_user_owned(ut20tx_ctx* ctx) { + ctx->s.ownership = MTL_BUFFER_USER_OWNED; + return mtl_session_user_buf_init(&ctx->s, ctx->framebuff_cnt); +} + +int ut20tx_post_user_buffer(ut20tx_ctx* ctx, void* data, void* user_ctx) { + return mtl_session_user_buf_enqueue(&ctx->s, data, 0, UT20TX_FRAME_STRIDE, user_ctx); +} + +void ut20tx_frame_set_timestamp(ut20tx_ctx* ctx, uint16_t idx, uint64_t tai_ns) { + ctx->frames[idx].tv_meta.timestamp = tai_ns; + ctx->frames[idx].tv_meta.tfmt = ST10_TIMESTAMP_FMT_TAI; +} + +void* ut20tx_user_buf_ctx(ut20tx_ctx* ctx, uint16_t idx) { + return ctx->s.user_buf_ctx ? ctx->s.user_buf_ctx[idx] : NULL; +} + /* ── state machine drivers ────────────────────────────────────────────── */ mtl_buffer_t* ut20tx_buffer_get(ut20tx_ctx* ctx) { diff --git a/tests/unit/new_api/st20_tx_harness.h b/tests/unit/new_api/st20_tx_harness.h index b36c8401d..6945469f5 100644 --- a/tests/unit/new_api/st20_tx_harness.h +++ b/tests/unit/new_api/st20_tx_harness.h @@ -45,6 +45,17 @@ void ut20tx_set_fps(ut20tx_ctx* ctx, enum st_fps fps); /** Set the wall clock returned by mt_get_ptp_time() (TAI ns). */ void ut20tx_set_ptp_now(ut20tx_ctx* ctx, uint64_t ns); +/* ── user-owned mode drivers ─────────────────────────────────────────── */ + +/** Switch the session to MTL_BUFFER_USER_OWNED and init the user-buf ring. */ +int ut20tx_set_user_owned(ut20tx_ctx* ctx); +/** Post an external user buffer (queued for get_next_frame to bind). */ +int ut20tx_post_user_buffer(ut20tx_ctx* ctx, void* data, void* user_ctx); +/** Stamp a frame slot's tv_meta with a TAI timestamp, bypassing buffer_put. */ +void ut20tx_frame_set_timestamp(ut20tx_ctx* ctx, uint16_t idx, uint64_t tai_ns); +/** Read the per-frame user_ctx slot (user-owned completion bookkeeping). */ +void* ut20tx_user_buf_ctx(ut20tx_ctx* ctx, uint16_t idx); + /* ── frame state machine drivers ─────────────────────────────────────── */ /** Wraps the buffer_get vtable entry (FREE -> APP_OWNED). NULL on -ETIMEDOUT. */ diff --git a/tests/unit/new_api/st20_tx_test.cpp b/tests/unit/new_api/st20_tx_test.cpp index cb7a04dc4..512a7f857 100644 --- a/tests/unit/new_api/st20_tx_test.cpp +++ b/tests/unit/new_api/st20_tx_test.cpp @@ -136,6 +136,71 @@ TEST_F(St20NewApiTxTest, DropWhenLateIgnoredWithoutUserPacing) { EXPECT_EQ(dropped(), 0u); } +/* DropWhenLate ON + USER_PACING but the frame was never stamped with a TAI + * timestamp (timestamp==0/tfmt==0). The late gate must treat an unstamped slot + * as not-late regardless of the wall clock, otherwise every unstamped frame + * looks infinitely late and is silently dropped. */ +TEST_F(St20NewApiTxTest, DropWhenLateIgnoresUnstampedFrame) { + ut20tx_set_drop_when_late(ctx_, true); + ut20tx_set_user_pacing(ctx_, true); + ut20tx_set_fps(ctx_, ST_FPS_P59_94); + + /* put a READY frame WITHOUT stamping any TAI timestamp on it */ + mtl_buffer_t* b = get(); + ASSERT_NE(b, nullptr); + ASSERT_EQ(put(b), 0); + ASSERT_EQ(state(0), kReady); + + /* wall clock far in the future: a timestamp==0 slot would look infinitely + * late under a naive gate, but an unstamped frame must NOT be dropped. */ + ut20tx_set_ptp_now(ctx_, kFrameTai + 1000 * kPeriodNs); + + uint16_t idx = 0xffff; + EXPECT_EQ(ut20tx_get_next_frame(ctx_, &idx), 0) + << "an unstamped frame must be transmitted, not dropped"; + EXPECT_EQ(idx, 0u); + EXPECT_EQ(state(0), kTransmitting); + EXPECT_EQ(dropped(), 0u); +} + +/* DropWhenLate ON + USER_PACING in MTL_BUFFER_USER_OWNED mode: when a posted + * external buffer is dropped as late, the app must still reclaim it. The drop + * path must deliver MTL_EVENT_BUFFER_DONE carrying the exact user_ctx and clear + * the per-frame ctx slot, otherwise the external buffer leaks. */ +TEST_F(St20NewApiTxTest, DropWhenLateUserOwnedReturnsBuffer) { + ut20tx_set_drop_when_late(ctx_, true); + ut20tx_set_user_pacing(ctx_, true); + ut20tx_set_fps(ctx_, ST_FPS_P59_94); + ASSERT_EQ(ut20tx_set_user_owned(ctx_), 0); + + int user_data = 0; + void* user_ctx = &user_data; + + /* stamp slot 0 so the bound user buffer is eligible for the late check */ + ut20tx_frame_set_timestamp(ctx_, 0, kFrameTai); + ut20tx_set_ptp_now(ctx_, kFrameTai + 2 * kPeriodNs); + + ASSERT_EQ(ut20tx_post_user_buffer(ctx_, &user_data, user_ctx), 0); + + uint16_t idx = 0xffff; + EXPECT_EQ(ut20tx_get_next_frame(ctx_, &idx), -EBUSY) + << "the late user buffer must be dropped, not transmitted"; + EXPECT_EQ(state(0), kFree); + EXPECT_EQ(dropped(), 1u); + + bool got_done = false; + mtl_event_t ev; + while (ut20tx_poll_event(ctx_, &ev) == 0) { + if (ev.type == MTL_EVENT_BUFFER_DONE) { + EXPECT_EQ(ev.ctx, user_ctx); + got_done = true; + } + } + EXPECT_TRUE(got_done) << "user-owned drop must return the buffer to the app"; + EXPECT_EQ(ut20tx_user_buf_ctx(ctx_, 0), nullptr) + << "the per-frame user_ctx slot must be cleared, not leaked"; +} + /* Full FREE -> APP_OWNED -> READY -> TRANSMITTING -> FREE walk via the public * callbacks. buffers_processed bumps on frame_done (transmission complete), * not on get or put. */ From 0bbbc920d9f7ea8d8f1706b0e8ff21e35e164930 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 9 Jun 2026 11:06:00 +0000 Subject: [PATCH 08/16] new_api: fix drop-when-late slot-reuse false drop (review r2) --- lib/src/new_api/mt_session_video_tx.c | 10 +++++++ tests/unit/new_api/st20_tx_test.cpp | 41 +++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/lib/src/new_api/mt_session_video_tx.c b/lib/src/new_api/mt_session_video_tx.c index 66155bc9a..a92bd3cff 100644 --- a/lib/src/new_api/mt_session_video_tx.c +++ b/lib/src/new_api/mt_session_video_tx.c @@ -198,6 +198,8 @@ static int video_tx_get_next_frame(void* priv, uint16_t* next_frame_idx, mtl_event_t event = {0}; event.type = MTL_EVENT_FRAME_LATE; mtl_session_event_post(s, &event); + tx_impl->st20_frames[i].tv_meta.timestamp = 0; + tx_impl->st20_frames[i].tv_meta.tfmt = 0; __atomic_store_n(&ctx->frame_state[i], TX_FRAME_FREE, __ATOMIC_RELEASE); continue; } @@ -231,6 +233,10 @@ static int video_tx_notify_frame_done(void* priv, uint16_t frame_idx, s->user_buf_ctx[frame_idx] = NULL; } + /* Drop the low-level pacing stamp so a reused slot starts unstamped. */ + tx_impl->st20_frames[frame_idx].tv_meta.timestamp = 0; + tx_impl->st20_frames[frame_idx].tv_meta.tfmt = 0; + __atomic_store_n(&ctx->frame_state[frame_idx], TX_FRAME_FREE, __ATOMIC_RELEASE); /* Update stats (lock-free, relaxed ordering for counters) */ @@ -388,6 +394,10 @@ static void tx_apply_buffer_metadata(mtl_buffer_t* buf, struct st_frame_trans* f if (buf->timestamp) { ft->tv_meta.timestamp = buf->timestamp; ft->tv_meta.tfmt = buf->tfmt; + } else { + /* Clear any stale low-level stamp so an unstamped frame is never late. */ + ft->tv_meta.timestamp = 0; + ft->tv_meta.tfmt = 0; } } diff --git a/tests/unit/new_api/st20_tx_test.cpp b/tests/unit/new_api/st20_tx_test.cpp index 512a7f857..f22c16d52 100644 --- a/tests/unit/new_api/st20_tx_test.cpp +++ b/tests/unit/new_api/st20_tx_test.cpp @@ -163,6 +163,47 @@ TEST_F(St20NewApiTxTest, DropWhenLateIgnoresUnstampedFrame) { EXPECT_EQ(dropped(), 0u); } +/* DropWhenLate ON + USER_PACING, library-owned slot reuse: the low-level TX + * pacing path stamps st20_frames[idx].tv_meta with a non-zero PAST cursor on + * every transmitted frame (st_tx_video_session.c). When the app later reuses + * that same slot with an UNSTAMPED buffer, the stale stamp must NOT be mistaken + * for an app deadline — the frame must transmit, not drop. */ +TEST_F(St20NewApiTxTest, DropWhenLateUnstampedSurvivesSlotReuse) { + ut20tx_set_drop_when_late(ctx_, true); + ut20tx_set_user_pacing(ctx_, true); + ut20tx_set_fps(ctx_, ST_FPS_P59_94); + + /* First use: drive an unstamped frame through slot 0 to TRANSMITTING. */ + mtl_buffer_t* b = get(); + ASSERT_NE(b, nullptr); + ASSERT_EQ(put(b), 0); + uint16_t idx = 0xffff; + ASSERT_EQ(ut20tx_get_next_frame(ctx_, &idx), 0); + ASSERT_EQ(idx, 0u); + + /* Mimic the low-level pacing path stamping tv_meta with a past TAI cursor. */ + ut20tx_frame_set_timestamp(ctx_, idx, kFrameTai); + + /* Transmission completes: the slot returns to FREE. */ + ASSERT_EQ(ut20tx_frame_done(ctx_, idx), 0); + ASSERT_EQ(state(0), kFree); + + /* Reuse the slot with an UNSTAMPED buffer while the wall clock is far past + * the stale stamp. */ + ut20tx_set_ptp_now(ctx_, kFrameTai + 1000 * kPeriodNs); + mtl_buffer_t* b2 = get(); + ASSERT_NE(b2, nullptr); + ASSERT_EQ(put(b2), 0); + + uint16_t idx2 = 0xffff; + EXPECT_EQ(ut20tx_get_next_frame(ctx_, &idx2), 0) + << "a reused slot with an unstamped buffer must transmit, not drop on a " + "stale low-level stamp"; + EXPECT_EQ(idx2, 0u); + EXPECT_EQ(state(0), kTransmitting); + EXPECT_EQ(dropped(), 0u); +} + /* DropWhenLate ON + USER_PACING in MTL_BUFFER_USER_OWNED mode: when a posted * external buffer is dropped as late, the app must still reclaim it. The drop * path must deliver MTL_EVENT_BUFFER_DONE carrying the exact user_ctx and clear From 3371a5d52284052b236fc3aecd6b9cce67136307 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 9 Jun 2026 11:23:50 +0000 Subject: [PATCH 09/16] test: add discriminating user-owned drop-when-late slot-reuse cases (review) --- lib/src/new_api/mt_session_video_tx.c | 1 + tests/unit/new_api/st20_tx_test.cpp | 78 +++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/lib/src/new_api/mt_session_video_tx.c b/lib/src/new_api/mt_session_video_tx.c index a92bd3cff..be40be3f1 100644 --- a/lib/src/new_api/mt_session_video_tx.c +++ b/lib/src/new_api/mt_session_video_tx.c @@ -198,6 +198,7 @@ static int video_tx_get_next_frame(void* priv, uint16_t* next_frame_idx, mtl_event_t event = {0}; event.type = MTL_EVENT_FRAME_LATE; mtl_session_event_post(s, &event); + /* Drop the low-level pacing stamp so a reused slot starts unstamped. */ tx_impl->st20_frames[i].tv_meta.timestamp = 0; tx_impl->st20_frames[i].tv_meta.tfmt = 0; __atomic_store_n(&ctx->frame_state[i], TX_FRAME_FREE, __ATOMIC_RELEASE); diff --git a/tests/unit/new_api/st20_tx_test.cpp b/tests/unit/new_api/st20_tx_test.cpp index f22c16d52..62847d425 100644 --- a/tests/unit/new_api/st20_tx_test.cpp +++ b/tests/unit/new_api/st20_tx_test.cpp @@ -204,6 +204,84 @@ TEST_F(St20NewApiTxTest, DropWhenLateUnstampedSurvivesSlotReuse) { EXPECT_EQ(dropped(), 0u); } +/* DropWhenLate ON + USER_PACING, USER_OWNED slot reuse after a completed + * transmit. User-owned bind never runs tx_apply_buffer_metadata, so only + * notify_frame_done (clear A) can drop the transmit-time pacing stamp. A slot + * stamped during transmit must start unstamped when the app rebinds it with a + * fresh buffer, otherwise the stale past stamp false-drops it. Reverting clear A + * alone makes this fail — clear B cannot mask it. */ +TEST_F(St20NewApiTxTest, DropWhenLateUserOwnedSurvivesSlotReuseAfterTransmit) { + ut20tx_set_drop_when_late(ctx_, true); + ut20tx_set_user_pacing(ctx_, true); + ut20tx_set_fps(ctx_, ST_FPS_P59_94); + ASSERT_EQ(ut20tx_set_user_owned(ctx_), 0); + + int data1 = 0, uctx1 = 0; + ASSERT_EQ(ut20tx_post_user_buffer(ctx_, &data1, &uctx1), 0); + + /* First use: an unstamped user buffer binds slot 0 and transmits. */ + uint16_t idx = 0xffff; + ASSERT_EQ(ut20tx_get_next_frame(ctx_, &idx), 0); + ASSERT_EQ(idx, 0u); + ASSERT_EQ(state(0), kTransmitting); + + /* Mimic the low-level pacing path stamping tv_meta with a past TAI cursor. */ + ut20tx_frame_set_timestamp(ctx_, idx, kFrameTai); + + /* Transmission completes: notify_frame_done (clear A) drops that stamp. */ + ASSERT_EQ(ut20tx_frame_done(ctx_, idx), 0); + ASSERT_EQ(state(0), kFree); + + /* Rebind the same slot with a fresh unstamped buffer, wall clock far past. */ + ut20tx_set_ptp_now(ctx_, kFrameTai + 1000 * kPeriodNs); + int data2 = 0, uctx2 = 0; + ASSERT_EQ(ut20tx_post_user_buffer(ctx_, &data2, &uctx2), 0); + + uint16_t idx2 = 0xffff; + EXPECT_EQ(ut20tx_get_next_frame(ctx_, &idx2), 0) + << "a user-owned reused slot must transmit, not drop on a stale " + "transmit-time pacing stamp"; + EXPECT_EQ(idx2, 0u); + EXPECT_EQ(state(0), kTransmitting); + EXPECT_EQ(dropped(), 0u); +} + +/* DropWhenLate ON + USER_PACING, USER_OWNED slot reuse after a drop. When a + * stamped buffer is dropped as late, the get_next_frame drop path itself + * (clear C) must clear the slot's tv_meta, otherwise the next fresh buffer that + * rebinds the slot inherits the stale past stamp and is false-dropped too. + * Reverting clear C alone makes this fail. */ +TEST_F(St20NewApiTxTest, DropWhenLateUserOwnedSurvivesSlotReuseAfterDrop) { + ut20tx_set_drop_when_late(ctx_, true); + ut20tx_set_user_pacing(ctx_, true); + ut20tx_set_fps(ctx_, ST_FPS_P59_94); + ASSERT_EQ(ut20tx_set_user_owned(ctx_), 0); + + /* Arm a drop: stamp slot 0 past TAI, wall clock far ahead. The bound buffer + * inherits the slot stamp — user-owned never re-stamps tv_meta. */ + ut20tx_frame_set_timestamp(ctx_, 0, kFrameTai); + ut20tx_set_ptp_now(ctx_, kFrameTai + 1000 * kPeriodNs); + + int data1 = 0, uctx1 = 0; + ASSERT_EQ(ut20tx_post_user_buffer(ctx_, &data1, &uctx1), 0); + uint16_t idx = 0xffff; + EXPECT_EQ(ut20tx_get_next_frame(ctx_, &idx), -EBUSY) + << "the stamped late user buffer must be dropped"; + EXPECT_EQ(state(0), kFree); + EXPECT_EQ(dropped(), 1u); + + /* Rebind the same slot with a fresh unstamped buffer. */ + int data2 = 0, uctx2 = 0; + ASSERT_EQ(ut20tx_post_user_buffer(ctx_, &data2, &uctx2), 0); + uint16_t idx2 = 0xffff; + EXPECT_EQ(ut20tx_get_next_frame(ctx_, &idx2), 0) + << "a user-owned reused slot must transmit, not drop on a stale " + "drop-path pacing stamp"; + EXPECT_EQ(idx2, 0u); + EXPECT_EQ(state(0), kTransmitting); + EXPECT_EQ(dropped(), 1u); +} + /* DropWhenLate ON + USER_PACING in MTL_BUFFER_USER_OWNED mode: when a posted * external buffer is dropped as late, the app must still reclaim it. The drop * path must deliver MTL_EVENT_BUFFER_DONE carrying the exact user_ctx and clear From 1f4e8ddb73faad62ce67576feae3ef86c351ab91 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 9 Jun 2026 11:28:49 +0000 Subject: [PATCH 10/16] new_api: wire RxTxApp st20p drop_when_late to MTL_SESSION_FLAG_DROP_WHEN_LATE The flag now exists in the unified session API, so replace the placeholder warn() no-op (added when the flag was absent) with the real flag mapping, making drop-when-late reachable end-to-end through RxTxApp. --- tests/tools/RxTxApp/src/tx_st20p_app.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/tools/RxTxApp/src/tx_st20p_app.c b/tests/tools/RxTxApp/src/tx_st20p_app.c index 0d37bd3e0..fa1e01b67 100644 --- a/tests/tools/RxTxApp/src/tx_st20p_app.c +++ b/tests/tools/RxTxApp/src/tx_st20p_app.c @@ -322,8 +322,7 @@ static int app_tx_st20p_init(struct st_app_context* ctx, st_json_st20p_session_t } if (st20p && st20p->drop_when_late) - warn("%s(%d), drop_when_late not supported on unified session path, ignored\n", - __func__, idx); + config.base.flags |= MTL_SESSION_FLAG_DROP_WHEN_LATE; if (ctx->tx_exact_user_pacing) config.base.flags |= MTL_SESSION_FLAG_EXACT_USER_PACING; if (ctx->tx_ts_epoch) config.base.flags |= MTL_SESSION_FLAG_RTP_TIMESTAMP_EPOCH; From 1f2fb61df4fbfbf73f2df711acd58bc5b91257c6 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 9 Jun 2026 12:41:45 +0000 Subject: [PATCH 11/16] docs(.github): record build/format/verify gotchas from new_api session - mtl-build skill: add the Unit Tests build+run section (exact binary path build_unit/tests/unit/UnitTest), fix Format guidance to prefer formatting only touched files over the whole-tree format-coding.sh churn, and add the broken-jobserver + stale-.ninja_deps phantom-crash traps to Common Build Errors. - copilot-instructions: orchestrator should independently re-run build + full unit suite after a Develop handoff; reported crashes are often stale-build artifacts, not regressions. --- .github/copilot-instructions.md | 7 +++++- .github/skills/mtl-build/SKILL.md | 39 +++++++++++++++++++++++++++---- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index ec2a3e1a0..8f2713084 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -72,4 +72,9 @@ decision to commit once Reviewer returns. If Reviewer raises BLOCKERs the user re-invokes the Develop agent with them, and Gates 2–4 run again for the fix. This is why Gates 5 and 6 are the only truly enforceable checks — they involve independent agents producing independent evidence; Gates 0–4 rely on the Develop agent following -its own checklist. +its own checklist. Because Gates 0–4 are self-reported, after a Develop handoff the +orchestrator should independently re-run `./build.sh` (or MCP `build_mtl`) and the +**full** unit suite rather than trusting a "builds clean" / "test crashes" report +verbatim — reported crashes are frequently stale-build artifacts (root-owned +`.ninja_deps`, a broken user-local `ninja`) that vanish on a clean rebuild, not real +regressions. diff --git a/.github/skills/mtl-build/SKILL.md b/.github/skills/mtl-build/SKILL.md index 77e06e5fe..ec4c7439d 100644 --- a/.github/skills/mtl-build/SKILL.md +++ b/.github/skills/mtl-build/SKILL.md @@ -17,13 +17,41 @@ Run from the repository root: ## Format Code +`clang-format-14` is required (`sudo apt install clang-format-14`). **CI rejects +improperly formatted code.** + ```bash +# Narrow change (PREFERRED): format ONLY the files you edited +clang-format-14 -i --style=file path/to/changed1.c path/to/changed2.h + +# Whole-tree formatter — use only for a sweeping change ./format-coding.sh ``` -- Requires `clang-format-14` (install via `sudo apt install clang-format-14`) -- **CI rejects improperly formatted code** — always run before committing -- Formats all C/C++ source files in the repository +- `format-coding.sh` reformats the **entire** repository. If any subtree has + pre-existing format drift, it rewrites files you never touched and produces a + noisy, hard-to-review diff. +- **After any format step run `git diff --name-only`** and keep only your own + files' changes — `git checkout -- ` to revert the churn. +- Confirm your touched files are clang-clean before committing; that is all CI + checks on the diff. + +## Unit Tests (NOT built by `./build.sh`) + +Unit gtests in `tests/unit/` need a separate build flag — `./build.sh` does not +build them. + +```bash +meson setup build_unit -Denable_unit_tests=true # one-time (or: meson configure build_unit -Denable_unit_tests=true) +ninja -C build_unit +./build_unit/tests/unit/UnitTest # NOTE the /unit/ path segment +./build_unit/tests/unit/UnitTest --gtest_filter='St20NewApiTxTest.*' # one suite +``` + +- The binary is `build_unit/tests/unit/UnitTest` — **not** `build_unit/tests/UnitTest`. +- ASan is preloaded; any leak fails the whole run. +- Re-run the **full** `UnitTest` (not just your filter) before declaring green — + ordering/teardown leaks only surface across the full suite. ## Verification Checklist @@ -33,7 +61,8 @@ After building, verify: - `build/lib/libmtl.so` — shared library - `build/tests/KahawaiTest` — integration test binary - `build/app/RxTxApp` — reference application -3. Run `./format-coding.sh` and check for any formatting changes +3. Your touched files are clang-clean (see Format Code) and `git diff --name-only` + shows no unrelated churn ## Common Build Errors @@ -49,3 +78,5 @@ After building, verify: | DPDK not found / wrong version | Build DPDK first: see [build docs](../../doc/build.md) | | Stale build directory | `rm -rf build && ./build.sh` or use MCP tool `mtl_clean_rebuild` | | Permission errors in build/ | `sudo rm -rf build && ./build.sh` | +| Build exits early / `jobserver` warnings | A broken ninja jobserver (often a user-local `~/.local/bin/ninja` ahead on `PATH`). Invoke the system binary explicitly: `/usr/bin/ninja -C build_unit`. For the lib `build/` dir, prefer MCP `build_mtl` / `mtl_clean_rebuild`. | +| Phantom test crash / segfault that does **not** reproduce on a clean tree | Stale or root-owned `build*/.ninja_deps` from a prior privileged build. Clean-rebuild (`rm -rf build_unit && meson setup build_unit -Denable_unit_tests=true`) and re-verify before trusting any reported crash. | From 9f176cf83b2eb0ff34c41f5dcf8ac6982fd7f53a Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 10 Jun 2026 07:04:07 +0000 Subject: [PATCH 12/16] new_api: fix st20 crash/correctness defects (Phase 1) Three unit-provable crash/correctness fixes in the new unified session API: - B5: size the buffer wrapper pool from the actual low-level frame count (forced >= 2), not the raw num_buffers. With num_buffers 0/1 the hot-path `buffers[idx % buffer_count]` divided by zero or aliased frames. Create now always calls mtl_session_init_buffers(), sized via mtl_session_video_frame_count(). - B6: video_tx_slice_ready() returned a fake success (0) for an unimplemented mode; return -ENOTSUP to match the RX twin and the dispatcher convention. - RX-ext: external-frame completion stored the library's internal user_meta as the user ctx; store the app's ext-frame opaque (meta->opaque) instead. Adds four RED-before/GREEN-after unit tests pinning each fix (BufferPoolSizeFollowsFrameCount rx+tx, SliceReadyUnsupported, ExtFrameCompletionCarriesAppOpaque). Full unit suite 309/309. --- lib/src/new_api/mt_session.c | 22 ++++++------ lib/src/new_api/mt_session.h | 8 +++++ lib/src/new_api/mt_session_buffer.c | 10 ++++++ lib/src/new_api/mt_session_video_rx.c | 2 +- lib/src/new_api/mt_session_video_tx.c | 2 +- tests/unit/new_api/st20_rx_harness.c | 38 +++++++++++++++++++- tests/unit/new_api/st20_rx_harness.h | 12 +++++++ tests/unit/new_api/st20_rx_test.cpp | 52 +++++++++++++++++++++++++++ tests/unit/new_api/st20_tx_harness.c | 30 +++++++++++++++- tests/unit/new_api/st20_tx_harness.h | 15 ++++++++ tests/unit/new_api/st20_tx_test.cpp | 34 ++++++++++++++++++ 11 files changed, 209 insertions(+), 16 deletions(-) diff --git a/lib/src/new_api/mt_session.c b/lib/src/new_api/mt_session.c index fb6a2065c..47d46d622 100644 --- a/lib/src/new_api/mt_session.c +++ b/lib/src/new_api/mt_session.c @@ -109,18 +109,16 @@ int mtl_video_session_create(mtl_handle mt, const mtl_video_config_t* config, return ret; } - /* Initialize buffer wrappers */ - if (config->base.num_buffers > 0) { - ret = mtl_session_buffers_init(s, config->base.num_buffers); - if (ret < 0) { - err("%s, buffers init failed: %d\n", __func__, ret); - if (config->base.direction == MTL_SESSION_TX) - mtl_video_tx_session_uinit(s); - else - mtl_video_rx_session_uinit(s); - mtl_session_free(s); - return ret; - } + /* Initialize buffer wrappers, one per low-level frame (forced >= 2) */ + ret = mtl_session_init_buffers(s); + if (ret < 0) { + err("%s, buffers init failed: %d\n", __func__, ret); + if (config->base.direction == MTL_SESSION_TX) + mtl_video_tx_session_uinit(s); + else + mtl_video_rx_session_uinit(s); + mtl_session_free(s); + return ret; } info("%s(%s), created %s video %s session\n", __func__, s->name, diff --git a/lib/src/new_api/mt_session.h b/lib/src/new_api/mt_session.h index ef1023b60..4aa6df167 100644 --- a/lib/src/new_api/mt_session.h +++ b/lib/src/new_api/mt_session.h @@ -390,6 +390,14 @@ int mtl_session_buffers_init(struct mtl_session_impl* s, uint32_t count); /** Cleanup buffer wrapper pool */ void mtl_session_buffers_uinit(struct mtl_session_impl* s); +/** Number of low-level transport frames allocated for a video session (the + * create path forces this >= 2). The wrapper pool must be sized to this, not the + * raw num_buffers, else the hot path % buffer_count divides by zero or aliases. */ +uint32_t mtl_session_video_frame_count(struct mtl_session_impl* s); + +/** Size the buffer wrapper pool to the actual frame count (create path). */ +int mtl_session_init_buffers(struct mtl_session_impl* s); + /** Initialize event ring */ int mtl_session_events_init(struct mtl_session_impl* s); diff --git a/lib/src/new_api/mt_session_buffer.c b/lib/src/new_api/mt_session_buffer.c index 88be4ca5a..1c31f6137 100644 --- a/lib/src/new_api/mt_session_buffer.c +++ b/lib/src/new_api/mt_session_buffer.c @@ -55,6 +55,16 @@ void mtl_session_buffers_uinit(struct mtl_session_impl* s) { s->buffer_count = 0; } +uint32_t mtl_session_video_frame_count(struct mtl_session_impl* s) { + if (s->direction == MTL_SESSION_TX) + return s->inner.video_tx ? s->inner.video_tx->st20_frames_cnt : 0; + return s->inner.video_rx ? s->inner.video_rx->st20_frames_cnt : 0; +} + +int mtl_session_init_buffers(struct mtl_session_impl* s) { + return mtl_session_buffers_init(s, mtl_session_video_frame_count(s)); +} + /************************************************************************* * Buffer Fill from st_frame_trans *************************************************************************/ diff --git a/lib/src/new_api/mt_session_video_rx.c b/lib/src/new_api/mt_session_video_rx.c index 68fe07587..06e3317cb 100644 --- a/lib/src/new_api/mt_session_video_rx.c +++ b/lib/src/new_api/mt_session_video_rx.c @@ -158,7 +158,7 @@ static int video_rx_notify_frame_ready(void* priv, void* frame, for (uint16_t i = 0; i < rx_impl->st20_frames_cnt; i++) { if (rx_impl->st20_frames[i].addr == frame) { if (i < s->user_buf_ctx_cnt) { - s->user_buf_ctx[i] = rx_impl->st20_frames[i].user_meta; + s->user_buf_ctx[i] = meta->opaque; } break; } diff --git a/lib/src/new_api/mt_session_video_tx.c b/lib/src/new_api/mt_session_video_tx.c index be40be3f1..687b7d957 100644 --- a/lib/src/new_api/mt_session_video_tx.c +++ b/lib/src/new_api/mt_session_video_tx.c @@ -649,7 +649,7 @@ static int video_tx_slice_ready(struct mtl_session_impl* s, mtl_buffer_t* buf, (void)s; (void)buf; (void)lines; - return 0; + return -ENOTSUP; } /************************************************************************* diff --git a/tests/unit/new_api/st20_rx_harness.c b/tests/unit/new_api/st20_rx_harness.c index 6f8d8c83e..48e304241 100644 --- a/tests/unit/new_api/st20_rx_harness.c +++ b/tests/unit/new_api/st20_rx_harness.c @@ -103,6 +103,7 @@ struct ut20rx_ctx { int framebuff_cnt; uint32_t enq_seq; /* advanced only on a successful enqueue */ bool user_owned; /* user_buf ring initialized → uinit at destroy */ + bool buffers_rte; /* wrapper pool came from mtl_session_init_buffers (rte) */ }; #include "new_api/st20_rx_harness.h" @@ -222,7 +223,10 @@ void ut20rx_ctx_destroy(ut20rx_ctx* ctx) { rte_ring_free(ctx->s.event_ring); } if (ctx->vctx.ready_ring) rte_ring_free(ctx->vctx.ready_ring); - free(ctx->s.buffers); + if (ctx->buffers_rte) + mtl_session_buffers_uinit(&ctx->s); + else + free(ctx->s.buffers); free(ctx->frames); free(ctx->frame_storage); free(ctx); @@ -308,6 +312,38 @@ int ut20rx_enable_user_owned_post(ut20rx_ctx* ctx) { return ret; } +static int ut20rx_stub_query_ext(void* priv, struct st_ext_frame* ext, + struct mtl_buffer* frame_meta) { + (void)priv; + (void)ext; + (void)frame_meta; + return 0; +} + +int ut20rx_enable_user_owned_query_ext(ut20rx_ctx* ctx) { + ctx->s.ownership = MTL_BUFFER_USER_OWNED; + ctx->vctx.user_query_ext_frame = ut20rx_stub_query_ext; + int ret = mtl_session_user_buf_init(&ctx->s, ctx->framebuff_cnt); + if (ret == 0) ctx->user_owned = true; + return ret; +} + +uint32_t ut20rx_frame_count(ut20rx_ctx* ctx) { + return mtl_session_video_frame_count(&ctx->s); +} + +uint32_t ut20rx_buffer_count(ut20rx_ctx* ctx) { + return ctx->s.buffer_count; +} + +int ut20rx_init_buffers(ut20rx_ctx* ctx) { + free(ctx->s.buffers); /* drop the libc pool ut20rx_ctx_create allocated */ + ctx->s.buffers = NULL; + ctx->s.buffer_count = 0; + ctx->buffers_rte = true; + return mtl_session_init_buffers(&ctx->s); +} + int ut20rx_mem_register(ut20rx_ctx* ctx, void* addr, size_t size) { mtl_dma_mem_t* handle = NULL; return mtl_video_rx_vtable.mem_register(&ctx->s, addr, size, &handle); diff --git a/tests/unit/new_api/st20_rx_harness.h b/tests/unit/new_api/st20_rx_harness.h index 5301cf72e..79502a5c4 100644 --- a/tests/unit/new_api/st20_rx_harness.h +++ b/tests/unit/new_api/st20_rx_harness.h @@ -75,6 +75,18 @@ int ut20rx_poll_event(ut20rx_ctx* ctx, mtl_event_t* event); * query_ext_frame). Returns 0 on success. */ int ut20rx_enable_user_owned_post(ut20rx_ctx* ctx); +/** Reconfigure for USER_OWNED with an explicit (app) query_ext_frame callback, + * so notify_frame_ready takes the per-frame opaque-saving branch. */ +int ut20rx_enable_user_owned_query_ext(ut20rx_ctx* ctx); + +/** Number of low-level frames (mtl_session_video_frame_count). */ +uint32_t ut20rx_frame_count(ut20rx_ctx* ctx); +/** Current wrapper pool size (s->buffer_count). */ +uint32_t ut20rx_buffer_count(ut20rx_ctx* ctx); +/** Drop the wrapper pool, then rebuild it via the production create-path + * helper (mtl_session_init_buffers). Returns its result. */ +int ut20rx_init_buffers(ut20rx_ctx* ctx); + /** Wraps the mem_register vtable entry (user-owned zero-copy registration). */ int ut20rx_mem_register(ut20rx_ctx* ctx, void* addr, size_t size); diff --git a/tests/unit/new_api/st20_rx_test.cpp b/tests/unit/new_api/st20_rx_test.cpp index 307438b88..4db69d3b5 100644 --- a/tests/unit/new_api/st20_rx_test.cpp +++ b/tests/unit/new_api/st20_rx_test.cpp @@ -22,6 +22,8 @@ #include +#include + #include "new_api/st20_rx_harness.h" class St20NewApiRxTest : public ::testing::Test { @@ -441,3 +443,53 @@ TEST_F(St20NewApiRxTest, ExtFrameQueryWrapperBinds) { EXPECT_EQ(ext.buf_len, sizeof(user_buf)); EXPECT_EQ(ext.opaque, (void*)0x99); } + +/* In USER_OWNED mode with an explicit app query_ext_frame, the transport carries + * the app's ext opaque on meta->opaque. The unified session must surface that + * opaque as the buffer completion ctx (buf->user_data), NOT the library's + * internal user_meta scratch buffer. */ +TEST_F(St20NewApiRxTest, ExtFrameCompletionCarriesAppOpaque) { + ASSERT_EQ(ut20rx_enable_user_owned_query_ext(ctx_), 0); + + /* A distinct non-NULL user_meta proves the completion ctx is the app opaque, + * not the internal user_meta pointer. */ + ut20rx_set_frame_user_meta(ctx_, 0, (void*)0xDEAD, 8); + + struct st20_rx_frame_meta meta {}; + meta.status = ST_FRAME_STATUS_COMPLETE; + meta.frame_total_size = 1; + meta.frame_recv_size = 1; + meta.pkts_total = 1; + meta.opaque = (void*)0xABCD; /* the app ext opaque */ + ASSERT_EQ(ut20rx_inject_meta(ctx_, &meta), 0); + + mtl_buffer_t* b = get_buffer(); + ASSERT_NE(b, nullptr); + EXPECT_EQ(b->user_data, (void*)0xABCD) + << "completion ctx must be the app ext opaque, not the lib user_meta"; + EXPECT_EQ(put_buffer(b), 0); +} + +/* The wrapper pool must be sized from the actual (bumped, >= 2) frame count, not + * the raw num_buffers. The create path forces st20_frames_cnt >= 2, so a + * num_buffers of 0 or 1 must still yield buffer_count == frame_count: otherwise + * the hot path s->buffers[i % buffer_count] divides by zero (0) or aliases two + * frames onto one wrapper (1). */ +TEST_F(St20NewApiRxTest, BufferPoolSizeFollowsFrameCount) { + const uint32_t fc = ut20rx_frame_count(ctx_); + ASSERT_GE(fc, 2u); + + ASSERT_EQ(ut20rx_init_buffers(ctx_), 0); + ASSERT_EQ(ut20rx_buffer_count(ctx_), fc) + << "wrapper pool must match the frame count (no % 0, no aliasing)"; + + std::set seen; + for (uint32_t i = 0; i < fc; i++) { + ASSERT_EQ(inject_complete(1000 + i), 0); + mtl_buffer_t* b = get_buffer(); + ASSERT_NE(b, nullptr); + EXPECT_TRUE(seen.insert(b).second) + << "frame " << i << " must map to a distinct wrapper"; + EXPECT_EQ(put_buffer(b), 0); + } +} diff --git a/tests/unit/new_api/st20_tx_harness.c b/tests/unit/new_api/st20_tx_harness.c index 1581bbbb7..d8c4762f5 100644 --- a/tests/unit/new_api/st20_tx_harness.c +++ b/tests/unit/new_api/st20_tx_harness.c @@ -58,6 +58,7 @@ struct ut20tx_ctx { struct st_frame_trans* frames; uint8_t* frame_storage; int framebuff_cnt; + bool buffers_rte; /* wrapper pool came from mtl_session_init_buffers (rte) */ }; #include "new_api/st20_tx_harness.h" @@ -185,7 +186,10 @@ void ut20tx_ctx_destroy(ut20tx_ctx* ctx) { mtl_session_user_buf_uinit(&ctx->s); if (ctx->s.event_ring) rte_ring_free(ctx->s.event_ring); free(ctx->vctx.frame_state); - free(ctx->s.buffers); + if (ctx->buffers_rte) + mtl_session_buffers_uinit(&ctx->s); + else + free(ctx->s.buffers); free(ctx->frames); free(ctx->frame_storage); free(ctx); @@ -315,3 +319,27 @@ int ut20tx_reset_stats(ut20tx_ctx* ctx) { int ut20tx_poll_event(ut20tx_ctx* ctx, mtl_event_t* ev) { return mtl_video_tx_vtable.event_poll(&ctx->s, ev, 0 /* non-blocking */); } + +/* ── slice op ─────────────────────────────────────────────────────────── */ + +int ut20tx_slice_ready(ut20tx_ctx* ctx, mtl_buffer_t* buf, uint16_t lines) { + return mtl_video_tx_vtable.slice_ready(&ctx->s, buf, lines); +} + +/* ── buffer pool sizing ───────────────────────────────────────────────── */ + +uint32_t ut20tx_frame_count(ut20tx_ctx* ctx) { + return mtl_session_video_frame_count(&ctx->s); +} + +uint32_t ut20tx_buffer_count(ut20tx_ctx* ctx) { + return ctx->s.buffer_count; +} + +int ut20tx_init_buffers(ut20tx_ctx* ctx) { + free(ctx->s.buffers); /* drop the libc pool ut20tx_ctx_create allocated */ + ctx->s.buffers = NULL; + ctx->s.buffer_count = 0; + ctx->buffers_rte = true; + return mtl_session_init_buffers(&ctx->s); +} diff --git a/tests/unit/new_api/st20_tx_harness.h b/tests/unit/new_api/st20_tx_harness.h index 6945469f5..61953014a 100644 --- a/tests/unit/new_api/st20_tx_harness.h +++ b/tests/unit/new_api/st20_tx_harness.h @@ -98,6 +98,21 @@ int ut20tx_reset_stats(ut20tx_ctx* ctx); /** Non-blocking event_poll wrapper. 0 + *ev on an event, -ETIMEDOUT if none. */ int ut20tx_poll_event(ut20tx_ctx* ctx, mtl_event_t* ev); +/* ── slice op ─────────────────────────────────────────────────────────── */ + +/** Wraps the slice_ready vtable entry (slice TX is not implemented). */ +int ut20tx_slice_ready(ut20tx_ctx* ctx, mtl_buffer_t* buf, uint16_t lines); + +/* ── buffer pool sizing ───────────────────────────────────────────────── */ + +/** Number of low-level frames (mtl_session_video_frame_count). */ +uint32_t ut20tx_frame_count(ut20tx_ctx* ctx); +/** Current wrapper pool size (s->buffer_count). */ +uint32_t ut20tx_buffer_count(ut20tx_ctx* ctx); +/** Drop the wrapper pool, then rebuild it via the production create-path + * helper (mtl_session_init_buffers). Returns its result. */ +int ut20tx_init_buffers(ut20tx_ctx* ctx); + #ifdef __cplusplus } #endif diff --git a/tests/unit/new_api/st20_tx_test.cpp b/tests/unit/new_api/st20_tx_test.cpp index 62847d425..e59669ddd 100644 --- a/tests/unit/new_api/st20_tx_test.cpp +++ b/tests/unit/new_api/st20_tx_test.cpp @@ -16,6 +16,9 @@ #include +#include +#include + #include "new_api/st20_tx_harness.h" /* P59_94: one frame period is ~16.683 ms. A TAI base of 1 s keeps both the @@ -433,3 +436,34 @@ TEST_F(St20NewApiTxTest, FramebuffCntNoTxClamp) { } EXPECT_EQ(get(), nullptr) << "exactly 8 slots, no more"; } + +/* Slice TX is not implemented in the unified session. slice_ready must report + * -ENOTSUP (mirroring the RX slice_query twin), never fake success by + * returning 0 and silently dropping the submitted lines. */ +TEST_F(St20NewApiTxTest, SliceReadyUnsupported) { + mtl_buffer_t* b = get(); + ASSERT_NE(b, nullptr); + EXPECT_EQ(ut20tx_slice_ready(ctx_, b, 10), -ENOTSUP) + << "slice TX is unimplemented; it must not claim success"; +} + +/* The wrapper pool must be sized from the actual (bumped, >= 2) frame count, not + * the raw num_buffers. A num_buffers of 0 or 1 must still yield + * buffer_count == frame_count, else the hot path s->buffers[i % buffer_count] + * divides by zero (0) or aliases two frames onto one wrapper (1). */ +TEST_F(St20NewApiTxTest, BufferPoolSizeFollowsFrameCount) { + const uint32_t fc = ut20tx_frame_count(ctx_); + ASSERT_GE(fc, 2u); + + ASSERT_EQ(ut20tx_init_buffers(ctx_), 0); + ASSERT_EQ(ut20tx_buffer_count(ctx_), fc) + << "wrapper pool must match the frame count (no % 0, no aliasing)"; + + std::set seen; + for (uint32_t i = 0; i < fc; i++) { + mtl_buffer_t* b = get(); + ASSERT_NE(b, nullptr) << "frame " << i << " must be claimable"; + EXPECT_TRUE(seen.insert(b).second) + << "frame " << i << " must map to a distinct wrapper"; + } +} From 80d5b7aeee4964580fae9633931082b3c261998a Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 10 Jun 2026 08:11:18 +0000 Subject: [PATCH 13/16] new_api: value-backed event queue + blocking consumer (Phase 2.1) Redesign the event system to honor the producer/consumer model: - Producer (tasklet via notify_* callbacks) now appends WITHOUT allocating or blocking: replace the malloc-per-post pointer ring with a value-backed rte_ring_create_elem(sizeof(mtl_event_t), RING_F_SC_DEQ). On a full ring it drops and bumps events_dropped (atomic), never waits. This removes the per-event mt_rte_zmalloc/mt_rte_free from tasklet context. - Consumer (app thread) now BLOCKS on the eventfd via poll() honoring timeout_ms instead of a usleep(100) busy-wait. timeout==0 -> -ETIMEDOUT, stop() -> -EAGAIN (distinct, P7/P27), with the sub-ms wait rounded up to avoid a poll(...,0) spin. - stop() now signals the eventfd (mtl_session_events_signal, async-signal-safe non-blocking write) so a consumer already blocked in poll() wakes promptly and returns -EAGAIN, instead of stalling for the full timeout. - Implement get_event_fd on both video vtables; drop the now-dead pointer-mode fallback dequeue in the dispatcher. Scope: event mechanism only. The BUFFER_DONE-per-frame flood, user-owned conversion, and tasklet re-enqueue malloc are addressed in later Phase 2 steps. Unit: 320/320 (11 new event/stop tests incl. a discriminating blocked-poll stop-wakeup concurrency test). Reviewer: APPROVE. --- lib/src/new_api/mt_session.c | 34 +++------ lib/src/new_api/mt_session.h | 8 +- lib/src/new_api/mt_session_event.c | 60 +++++++-------- lib/src/new_api/mt_session_video_common.c | 51 ++++++++----- lib/src/new_api/mt_session_video_common.h | 24 ++++-- lib/src/new_api/mt_session_video_rx.c | 2 +- lib/src/new_api/mt_session_video_tx.c | 2 +- tests/unit/new_api/st20_rx_harness.c | 40 ++++++---- tests/unit/new_api/st20_rx_harness.h | 13 ++++ tests/unit/new_api/st20_rx_test.cpp | 85 +++++++++++++++++++++ tests/unit/new_api/st20_tx_harness.c | 38 +++++++--- tests/unit/new_api/st20_tx_harness.h | 13 ++++ tests/unit/new_api/st20_tx_test.cpp | 92 +++++++++++++++++++++++ 13 files changed, 356 insertions(+), 106 deletions(-) diff --git a/lib/src/new_api/mt_session.c b/lib/src/new_api/mt_session.c index 47d46d622..a90acd9af 100644 --- a/lib/src/new_api/mt_session.c +++ b/lib/src/new_api/mt_session.c @@ -189,6 +189,9 @@ int mtl_session_stop(mtl_session_t* session) { /* Set stopped flag - this is safe from signal handlers */ mtl_session_set_stopped(s); + /* Wake any consumer already blocked in event_poll so it re-checks stopped */ + mtl_session_events_signal(s); + /* Call type-specific stop if available */ if (s->vt && s->vt->stop) { s->vt->stop(s); @@ -355,17 +358,7 @@ int mtl_session_event_poll(mtl_session_t* session, mtl_event_t* event, } if (!s->vt || !s->vt->event_poll) { - /* Default: try to dequeue from event ring */ - if (s->event_ring) { - void* obj = NULL; - if (rte_ring_dequeue(s->event_ring, &obj) == 0 && obj) { - mtl_event_t* ev = (mtl_event_t*)obj; - *event = *ev; - mt_rte_free(ev); - return 0; - } - } - return -ETIMEDOUT; + return -ENOSYS; } return s->vt->event_poll(s, event, timeout_ms); @@ -378,11 +371,11 @@ int mtl_session_get_event_fd(mtl_session_t* session) { return -EINVAL; } - if (s->vt && s->vt->get_event_fd) { - return s->vt->get_event_fd(s); + if (!s->vt || !s->vt->get_event_fd) { + return -ENOSYS; } - return s->event_fd; + return s->vt->get_event_fd(s); } /************************************************************************* @@ -403,12 +396,9 @@ int mtl_session_stats_get(mtl_session_t* session, mtl_session_stats_t* stats) { /* Default: return cached stats (atomic reads, relaxed ordering) */ stats->buffers_processed = __atomic_load_n(&s->stats.buffers_processed, __ATOMIC_RELAXED); - stats->bytes_processed = - __atomic_load_n(&s->stats.bytes_processed, __ATOMIC_RELAXED); - stats->buffers_dropped = - __atomic_load_n(&s->stats.buffers_dropped, __ATOMIC_RELAXED); - stats->epochs_missed = - __atomic_load_n(&s->stats.epochs_missed, __ATOMIC_RELAXED); + stats->bytes_processed = __atomic_load_n(&s->stats.bytes_processed, __ATOMIC_RELAXED); + stats->buffers_dropped = __atomic_load_n(&s->stats.buffers_dropped, __ATOMIC_RELAXED); + stats->epochs_missed = __atomic_load_n(&s->stats.epochs_missed, __ATOMIC_RELAXED); stats->buffers_free = 0; stats->buffers_in_use = 0; return 0; @@ -474,8 +464,8 @@ int mtl_session_io_stats_reset(mtl_session_t* session) { return s->vt->io_stats_reset(s); } -int mtl_session_pcap_dump(mtl_session_t* session, uint32_t max_dump_packets, - bool sync, struct st_pcap_dump_meta* meta) { +int mtl_session_pcap_dump(mtl_session_t* session, uint32_t max_dump_packets, bool sync, + struct st_pcap_dump_meta* meta) { struct mtl_session_impl* s = MTL_SESSION_IMPL(session); if (!s || !MTL_SESSION_VALID(s)) { diff --git a/lib/src/new_api/mt_session.h b/lib/src/new_api/mt_session.h index 4aa6df167..e395e0046 100644 --- a/lib/src/new_api/mt_session.h +++ b/lib/src/new_api/mt_session.h @@ -245,8 +245,9 @@ struct mtl_session_impl { struct mtl_buffer_impl* buffers; /**< Buffer wrapper pool */ /* Event queue */ - struct rte_ring* event_ring; /**< Pending events */ - int event_fd; /**< For epoll integration */ + struct rte_ring* event_ring; /**< Pending events (value-backed) */ + int event_fd; /**< Wakeup fd for the blocking consumer */ + uint64_t events_dropped; /**< Events dropped on a full ring (producer) */ /* * User-owned buffer management (MTL_BUFFER_USER_OWNED mode). @@ -407,6 +408,9 @@ void mtl_session_events_uinit(struct mtl_session_impl* s); /** Post event to session */ int mtl_session_event_post(struct mtl_session_impl* s, const mtl_event_t* event); +/** Wake a consumer blocked in event_poll (non-blocking eventfd bump). */ +void mtl_session_events_signal(struct mtl_session_impl* s); + /************************************************************************* * User-Owned Buffer Helpers *************************************************************************/ diff --git a/lib/src/new_api/mt_session_event.c b/lib/src/new_api/mt_session_event.c index d2a66279c..fcb9d63d7 100644 --- a/lib/src/new_api/mt_session_event.c +++ b/lib/src/new_api/mt_session_event.c @@ -9,14 +9,12 @@ * Uses rte_ring for lock-free event queuing from callbacks to poll(). */ -#include "mt_session.h" - #include #include #include #include "../mt_log.h" -#include "../mt_mem.h" +#include "mt_session.h" #define MTL_EVENT_RING_SIZE 64 /* Must be power of 2 */ @@ -29,14 +27,18 @@ int mtl_session_events_init(struct mtl_session_impl* s) { snprintf(ring_name, sizeof(ring_name), "mtl_ev_%p", s); - s->event_ring = - rte_ring_create(ring_name, MTL_EVENT_RING_SIZE, s->socket_id, 0); + /* Value-backed ring: events are copied in, so the producer (tasklet) never + * allocates. Single consumer (the app thread) -> RING_F_SC_DEQ; the producer + * stays MP-safe (notify_frame_* and the vsync callback are distinct + * contexts), so no RING_F_SP_ENQ. */ + s->event_ring = rte_ring_create_elem(ring_name, sizeof(mtl_event_t), + MTL_EVENT_RING_SIZE, s->socket_id, RING_F_SC_DEQ); if (!s->event_ring) { err("%s(%s), failed to create event ring\n", __func__, s->name); return -ENOMEM; } - /* Create eventfd for epoll/select integration */ + /* Wakeup fd so the consumer can block instead of spin. */ s->event_fd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC); if (s->event_fd < 0) { /* Not fatal - event_fd is optional */ @@ -44,18 +46,14 @@ int mtl_session_events_init(struct mtl_session_impl* s) { s->event_fd = -1; } + s->events_dropped = 0; + dbg("%s(%s), event queue initialized\n", __func__, s->name); return 0; } void mtl_session_events_uinit(struct mtl_session_impl* s) { - /* Drain and free any remaining events */ if (s->event_ring) { - void* obj = NULL; - while (rte_ring_dequeue(s->event_ring, &obj) == 0 && obj) { - mt_rte_free(obj); - obj = NULL; - } rte_ring_free(s->event_ring); s->event_ring = NULL; } @@ -70,36 +68,34 @@ void mtl_session_events_uinit(struct mtl_session_impl* s) { * Event Posting (called from callbacks / library threads) *************************************************************************/ -int mtl_session_event_post(struct mtl_session_impl* s, const mtl_event_t* event) { - mtl_event_t* ev_copy; +void mtl_session_events_signal(struct mtl_session_impl* s) { + /* Non-blocking 8-byte counter bump to wake a blocked consumer. The fd is + * O_NONBLOCK so this never blocks (ignore EAGAIN); write() is async-signal- + * safe, so the stop path may call this from a signal handler. */ + if (s->event_fd >= 0) { + uint64_t val = 1; + ssize_t n = write(s->event_fd, &val, sizeof(val)); + (void)n; + } +} +int mtl_session_event_post(struct mtl_session_impl* s, const mtl_event_t* event) { if (!s->event_ring) { return -EINVAL; } - /* Allocate a copy of the event for the ring */ - ev_copy = mt_rte_zmalloc_socket(sizeof(*ev_copy), s->socket_id); - if (!ev_copy) { - dbg("%s(%s), failed to alloc event copy\n", __func__, s->name); - return -ENOMEM; - } - - *ev_copy = *event; - - if (rte_ring_enqueue(s->event_ring, ev_copy) != 0) { - /* Ring full - drop event */ - mt_rte_free(ev_copy); + /* Producer (tasklet) path: copy the event in by value - no alloc, no block. + * On a full ring drop and count rather than wait. */ + if (rte_ring_enqueue_elem(s->event_ring, (void*)event, sizeof(*event)) != 0) { + __atomic_fetch_add(&s->events_dropped, 1, __ATOMIC_RELAXED); dbg("%s(%s), event ring full, dropping event type %d\n", __func__, s->name, event->type); return -ENOSPC; } - /* Signal eventfd if available */ - if (s->event_fd >= 0) { - uint64_t val = 1; - ssize_t n = write(s->event_fd, &val, sizeof(val)); - (void)n; /* Ignore write failures on non-blocking fd */ - } + /* Non-blocking 8-byte counter bump to wake a blocked consumer; the fd is + * O_NONBLOCK so this cannot block the tasklet (ignore EAGAIN). */ + mtl_session_events_signal(s); /* Call optional callback */ if (event->type == MTL_EVENT_BUFFER_READY && s->notify_buffer_ready) { diff --git a/lib/src/new_api/mt_session_video_common.c b/lib/src/new_api/mt_session_video_common.c index 7f230744c..cfecdd441 100644 --- a/lib/src/new_api/mt_session_video_common.c +++ b/lib/src/new_api/mt_session_video_common.c @@ -13,6 +13,7 @@ #include "mt_session_video_common.h" #include +#include #include #include @@ -59,8 +60,7 @@ int video_convert_ctx_init(struct video_convert_ctx* cvt, /* Calculate app-side frame size */ cvt->app_frame_size = - st_frame_size(config->frame_fmt, config->width, config->height, - config->interlaced); + st_frame_size(config->frame_fmt, config->width, config->height, config->interlaced); if (!cvt->app_frame_size) { err("%s(%s), failed to get frame size for fmt %s\n", __func__, config->base.name, st_frame_fmt_name(config->frame_fmt)); @@ -173,43 +173,60 @@ int video_convert_frame(struct video_convert_ctx* cvt, void* src_data, int video_session_event_poll(struct mtl_session_impl* s, mtl_event_t* event, uint32_t timeout_ms) { + /* Single-consumer contract: the event ring is RING_F_SC_DEQ, so only one + * thread may call event_poll on a session at a time. */ if (mtl_session_check_stopped(s)) { return -EAGAIN; } - /* Non-blocking dequeue attempt */ - if (s->event_ring) { - void* obj = NULL; - if (rte_ring_dequeue(s->event_ring, &obj) == 0 && obj) { - *event = *(mtl_event_t*)obj; - mt_rte_free(obj); - return 0; - } + /* Non-blocking value dequeue attempt */ + if (s->event_ring && rte_ring_dequeue_elem(s->event_ring, event, sizeof(*event)) == 0) { + return 0; } if (timeout_ms == 0) { return -ETIMEDOUT; } - /* Poll with timeout */ uint64_t deadline_ns = video_calc_deadline_ns(timeout_ms); + /* Consumer (app thread) may block: wait on the eventfd up to the deadline, + * waking as soon as the producer posts. */ while (!mtl_session_check_stopped(s)) { - void* obj = NULL; - if (s->event_ring && rte_ring_dequeue(s->event_ring, &obj) == 0 && obj) { - *event = *(mtl_event_t*)obj; - mt_rte_free(obj); + if (s->event_ring && + rte_ring_dequeue_elem(s->event_ring, event, sizeof(*event)) == 0) { return 0; } - usleep(100); - if (video_deadline_reached(deadline_ns)) return -ETIMEDOUT; + + if (s->event_fd >= 0) { + uint64_t now_ns = video_now_ns(); + /* Round the remaining time up to whole milliseconds: truncation to 0 + * while time remains would turn poll() into a busy-wait. */ + int wait_ms = 0; + if (deadline_ns > now_ns) { + wait_ms = (int)((deadline_ns - now_ns + 999999ULL) / 1000000ULL); + } + struct pollfd pfd = {.fd = s->event_fd, .events = POLLIN}; + int pret = poll(&pfd, 1, wait_ms); + if (pret > 0 && (pfd.revents & POLLIN)) { + uint64_t cnt; + ssize_t n = read(s->event_fd, &cnt, sizeof(cnt)); + (void)n; + } + } else { + usleep(100); + } } return -EAGAIN; } +int video_session_get_event_fd(struct mtl_session_impl* s) { + return s->event_fd; +} + /************************************************************************* * Shared Stats Reset *************************************************************************/ diff --git a/lib/src/new_api/mt_session_video_common.h b/lib/src/new_api/mt_session_video_common.h index ae97dae77..35f1cfd2e 100644 --- a/lib/src/new_api/mt_session_video_common.h +++ b/lib/src/new_api/mt_session_video_common.h @@ -115,6 +115,11 @@ int video_convert_frame(struct video_convert_ctx* cvt, void* src_data, int video_session_event_poll(struct mtl_session_impl* s, mtl_event_t* event, uint32_t timeout_ms); +/** + * Return the session's event wakeup fd for app epoll/select integration. + */ +int video_session_get_event_fd(struct mtl_session_impl* s); + /************************************************************************* * Shared Stats (identical for TX and RX) *************************************************************************/ @@ -142,16 +147,22 @@ int video_session_notify_event(void* priv, enum st_event ev, void* args); * Deadline Helpers *************************************************************************/ +/** + * Current CLOCK_MONOTONIC time in nanoseconds. + */ +static inline uint64_t video_now_ns(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (uint64_t)ts.tv_sec * 1000000000ULL + ts.tv_nsec; +} + /** * Calculate an absolute deadline in nanoseconds from a relative timeout. * Returns 0 if timeout_ms == 0 (non-blocking). */ static inline uint64_t video_calc_deadline_ns(uint32_t timeout_ms) { if (timeout_ms == 0) return 0; - struct timespec ts; - clock_gettime(CLOCK_MONOTONIC, &ts); - return (uint64_t)ts.tv_sec * 1000000000ULL + ts.tv_nsec + - (uint64_t)timeout_ms * 1000000ULL; + return video_now_ns() + (uint64_t)timeout_ms * 1000000ULL; } /** @@ -160,10 +171,7 @@ static inline uint64_t video_calc_deadline_ns(uint32_t timeout_ms) { */ static inline bool video_deadline_reached(uint64_t deadline_ns) { if (deadline_ns == 0) return true; /* Non-blocking mode */ - struct timespec ts; - clock_gettime(CLOCK_MONOTONIC, &ts); - uint64_t now = (uint64_t)ts.tv_sec * 1000000000ULL + ts.tv_nsec; - return now >= deadline_ns; + return video_now_ns() >= deadline_ns; } #if defined(__cplusplus) diff --git a/lib/src/new_api/mt_session_video_rx.c b/lib/src/new_api/mt_session_video_rx.c index 06e3317cb..31a3d9c73 100644 --- a/lib/src/new_api/mt_session_video_rx.c +++ b/lib/src/new_api/mt_session_video_rx.c @@ -677,7 +677,7 @@ const mtl_session_vtable_t mtl_video_rx_vtable = { .mem_register = video_rx_mem_register, .mem_unregister = video_rx_mem_unregister, .event_poll = video_session_event_poll, /* shared implementation */ - .get_event_fd = NULL, + .get_event_fd = video_session_get_event_fd, /* shared implementation */ .stats_get = video_rx_stats_get, .stats_reset = video_session_stats_reset, /* shared implementation */ .get_frame_size = video_rx_get_frame_size, diff --git a/lib/src/new_api/mt_session_video_tx.c b/lib/src/new_api/mt_session_video_tx.c index 687b7d957..74c2e246a 100644 --- a/lib/src/new_api/mt_session_video_tx.c +++ b/lib/src/new_api/mt_session_video_tx.c @@ -667,7 +667,7 @@ const mtl_session_vtable_t mtl_video_tx_vtable = { .mem_register = video_tx_mem_register, .mem_unregister = video_tx_mem_unregister, .event_poll = video_session_event_poll, /* shared implementation */ - .get_event_fd = NULL, + .get_event_fd = video_session_get_event_fd, /* shared implementation */ .stats_get = video_tx_stats_get, .stats_reset = video_session_stats_reset, /* shared implementation */ .get_frame_size = video_tx_get_frame_size, diff --git a/tests/unit/new_api/st20_rx_harness.c b/tests/unit/new_api/st20_rx_harness.c index 48e304241..ac3b02b86 100644 --- a/tests/unit/new_api/st20_rx_harness.c +++ b/tests/unit/new_api/st20_rx_harness.c @@ -155,7 +155,6 @@ ut20rx_ctx* ut20rx_ctx_create(int framebuff_cnt) { s->socket_id = rte_socket_id(); s->ownership = MTL_BUFFER_LIBRARY_OWNED; s->stopped = 0; - s->event_fd = -1; s->inner.video_rx = rx; snprintf(s->name, sizeof(s->name), "ut_new_rx"); @@ -194,10 +193,7 @@ ut20rx_ctx* ut20rx_ctx_create(int framebuff_cnt) { return NULL; } - snprintf(ring_name, sizeof(ring_name), "utnrxe_%u", ring_seq++); - s->event_ring = - rte_ring_create(ring_name, 64, s->socket_id, RING_F_SP_ENQ | RING_F_SC_DEQ); - if (!s->event_ring) { + if (mtl_session_events_init(s) < 0) { rte_ring_free(v->ready_ring); free(s->buffers); free(ctx->frames); @@ -214,14 +210,7 @@ ut20rx_ctx* ut20rx_ctx_create(int framebuff_cnt) { void ut20rx_ctx_destroy(ut20rx_ctx* ctx) { if (!ctx) return; if (ctx->user_owned) mtl_session_user_buf_uinit(&ctx->s); - if (ctx->s.event_ring) { - void* obj = NULL; - while (rte_ring_dequeue(ctx->s.event_ring, &obj) == 0 && obj) { - mt_rte_free(obj); - obj = NULL; - } - rte_ring_free(ctx->s.event_ring); - } + mtl_session_events_uinit(&ctx->s); if (ctx->vctx.ready_ring) rte_ring_free(ctx->vctx.ready_ring); if (ctx->buffers_rte) mtl_session_buffers_uinit(&ctx->s); @@ -304,6 +293,31 @@ int ut20rx_poll_event(ut20rx_ctx* ctx, mtl_event_t* event) { return mtl_video_rx_vtable.event_poll(&ctx->s, event, 0 /* non-blocking */); } +int ut20rx_poll_event_timeout(ut20rx_ctx* ctx, mtl_event_t* event, uint32_t timeout_ms) { + return mtl_video_rx_vtable.event_poll(&ctx->s, event, timeout_ms); +} + +int ut20rx_post_event(ut20rx_ctx* ctx, const mtl_event_t* event) { + return mtl_session_event_post(&ctx->s, event); +} + +uint64_t ut20rx_events_dropped(const ut20rx_ctx* ctx) { + return ctx->s.events_dropped; +} + +int ut20rx_get_event_fd(ut20rx_ctx* ctx) { + if (!ctx->s.vt->get_event_fd) return -ENOSYS; + return ctx->s.vt->get_event_fd(&ctx->s); +} + +void ut20rx_set_stopped(ut20rx_ctx* ctx) { + mtl_session_set_stopped(&ctx->s); +} + +void ut20rx_stop(ut20rx_ctx* ctx) { + mtl_session_stop((mtl_session_t*)&ctx->s); +} + int ut20rx_enable_user_owned_post(ut20rx_ctx* ctx) { ctx->s.ownership = MTL_BUFFER_USER_OWNED; ctx->vctx.user_query_ext_frame = NULL; diff --git a/tests/unit/new_api/st20_rx_harness.h b/tests/unit/new_api/st20_rx_harness.h index 79502a5c4..f1fbfac80 100644 --- a/tests/unit/new_api/st20_rx_harness.h +++ b/tests/unit/new_api/st20_rx_harness.h @@ -70,6 +70,19 @@ int ut20rx_notify_detected(ut20rx_ctx* ctx, uint32_t width, uint32_t height, /** Non-blocking drain of one session event (shared video_session_event_poll). */ int ut20rx_poll_event(ut20rx_ctx* ctx, mtl_event_t* event); +/** event_poll wrapper with an explicit timeout (drives the blocking path). */ +int ut20rx_poll_event_timeout(ut20rx_ctx* ctx, mtl_event_t* event, uint32_t timeout_ms); +/** Post an event through the producer path (mtl_session_event_post). */ +int ut20rx_post_event(ut20rx_ctx* ctx, const mtl_event_t* event); +/** Read the producer-side dropped-event counter (s->events_dropped). */ +uint64_t ut20rx_events_dropped(const ut20rx_ctx* ctx); +/** Wraps the get_event_fd vtable slot; -ENOSYS if the slot is NULL. */ +int ut20rx_get_event_fd(ut20rx_ctx* ctx); +/** Set the session stopped flag (drives the poll -EAGAIN path). */ +void ut20rx_set_stopped(ut20rx_ctx* ctx); +/** Drive the full production stop path (sets stopped + signals the eventfd to + * wake a consumer already blocked in event_poll). */ +void ut20rx_stop(ut20rx_ctx* ctx); /** Reconfigure the session for USER_OWNED buffer-post mode (no app * query_ext_frame). Returns 0 on success. */ diff --git a/tests/unit/new_api/st20_rx_test.cpp b/tests/unit/new_api/st20_rx_test.cpp index 4db69d3b5..f42452f1d 100644 --- a/tests/unit/new_api/st20_rx_test.cpp +++ b/tests/unit/new_api/st20_rx_test.cpp @@ -22,7 +22,11 @@ #include +#include +#include +#include #include +#include #include "new_api/st20_rx_harness.h" @@ -493,3 +497,84 @@ TEST_F(St20NewApiRxTest, BufferPoolSizeFollowsFrameCount) { EXPECT_EQ(put_buffer(b), 0); } } + +/* ── event-system mechanism (producer-nonblocking / consumer-blocking) ─── */ + +/* Producer overflow on the shared event queue: surplus posts are dropped and + * counted, never allocated/blocked. */ +TEST_F(St20NewApiRxTest, EventPostDropsOnFullBumpsCounter) { + mtl_event_t ev = {}; + ev.type = MTL_EVENT_BUFFER_READY; + + const int kPosts = 256; + int accepted = 0, dropped = 0; + for (int i = 0; i < kPosts; i++) { + ev.ctx = reinterpret_cast(static_cast(i)); + int ret = ut20rx_post_event(ctx_, &ev); + if (ret == 0) + accepted++; + else + dropped++; + } + + EXPECT_GT(dropped, 0) << "posting past ring capacity must drop the surplus"; + EXPECT_EQ(accepted + dropped, kPosts); + EXPECT_EQ(ut20rx_events_dropped(ctx_), static_cast(dropped)) + << "every dropped post must bump the producer drop counter"; +} + +/* Value round-trip across the shared post/poll path on an RX session. */ +TEST_F(St20NewApiRxTest, EventPostPollValueRoundTrip) { + mtl_event_t in = {}; + in.type = MTL_EVENT_FORMAT_DETECTED; + in.status = 7; + in.timestamp = 123456789ULL; + in.ctx = reinterpret_cast(static_cast(0x5A5A)); + + ASSERT_EQ(ut20rx_post_event(ctx_, &in), 0); + + mtl_event_t out = {}; + ASSERT_EQ(ut20rx_poll_event(ctx_, &out), 0); + EXPECT_EQ(out.type, in.type); + EXPECT_EQ(out.status, in.status); + EXPECT_EQ(out.timestamp, in.timestamp); + EXPECT_EQ(out.ctx, in.ctx); +} + +/* Pre-check path: stopped before poll() is entered, so the top-of-function + * check returns -EAGAIN without ever blocking. */ +TEST_F(St20NewApiRxTest, EventPollPreStopReturnsEagain) { + ut20rx_set_stopped(ctx_); + mtl_event_t ev; + EXPECT_EQ(ut20rx_poll_event_timeout(ctx_, &ev, 1000), -EAGAIN); +} + +/* Missed-wakeup path: a consumer already blocked in poll() must wake promptly + * when another thread calls stop(), returning -EAGAIN well under the timeout + * rather than waiting out the full timeout_ms. */ +TEST_F(St20NewApiRxTest, EventPollStopWakesBlockedPoll) { + std::atomic entered{false}; + std::atomic ret{1}; + std::thread consumer([&] { + mtl_event_t ev; + entered.store(true, std::memory_order_release); + ret.store(ut20rx_poll_event_timeout(ctx_, &ev, 5000), std::memory_order_release); + }); + + while (!entered.load(std::memory_order_acquire)) std::this_thread::yield(); + + auto t0 = std::chrono::steady_clock::now(); + ut20rx_stop(ctx_); + consumer.join(); + auto elapsed_ms = std::chrono::duration_cast( + std::chrono::steady_clock::now() - t0) + .count(); + + EXPECT_EQ(ret.load(std::memory_order_acquire), -EAGAIN); + EXPECT_LT(elapsed_ms, 1000) << "stop() must wake the blocked poll promptly"; +} + +/* get_event_fd is wired and returns a valid (>= 0) wakeup fd. */ +TEST_F(St20NewApiRxTest, EventFdValid) { + EXPECT_GE(ut20rx_get_event_fd(ctx_), 0); +} diff --git a/tests/unit/new_api/st20_tx_harness.c b/tests/unit/new_api/st20_tx_harness.c index d8c4762f5..350dd4f27 100644 --- a/tests/unit/new_api/st20_tx_harness.c +++ b/tests/unit/new_api/st20_tx_harness.c @@ -81,8 +81,6 @@ int ut20tx_init(void) { /* ── context create / destroy ─────────────────────────────────────────── */ ut20tx_ctx* ut20tx_ctx_create(int framebuff_cnt) { - static uint32_t ring_seq; - ut20tx_ctx* ctx = calloc(1, sizeof(*ctx)); if (!ctx) return NULL; ctx->framebuff_cnt = framebuff_cnt; @@ -125,7 +123,6 @@ ut20tx_ctx* ut20tx_ctx_create(int framebuff_cnt) { s->ownership = MTL_BUFFER_LIBRARY_OWNED; s->flags = 0; s->stopped = 0; - s->event_fd = -1; s->inner.video_tx = tx; snprintf(s->name, sizeof(s->name), "ut_new_tx"); @@ -142,11 +139,7 @@ ut20tx_ctx* ut20tx_ctx_create(int framebuff_cnt) { s->buffers[i].idx = i; } - char ring_name[RTE_RING_NAMESIZE]; - snprintf(ring_name, sizeof(ring_name), "utntx_%u", ring_seq++); - s->event_ring = - rte_ring_create(ring_name, 64, s->socket_id, RING_F_SP_ENQ | RING_F_SC_DEQ); - if (!s->event_ring) { + if (mtl_session_events_init(s) < 0) { free(s->buffers); free(ctx->frames); free(ctx->frame_storage); @@ -164,7 +157,7 @@ ut20tx_ctx* ut20tx_ctx_create(int framebuff_cnt) { v->convert.height = 1080; v->frame_state = calloc(framebuff_cnt, sizeof(enum tx_frame_state)); if (!v->frame_state) { - rte_ring_free(s->event_ring); + mtl_session_events_uinit(s); free(s->buffers); free(ctx->frames); free(ctx->frame_storage); @@ -184,7 +177,7 @@ ut20tx_ctx* ut20tx_ctx_create(int framebuff_cnt) { void ut20tx_ctx_destroy(ut20tx_ctx* ctx) { if (!ctx) return; mtl_session_user_buf_uinit(&ctx->s); - if (ctx->s.event_ring) rte_ring_free(ctx->s.event_ring); + mtl_session_events_uinit(&ctx->s); free(ctx->vctx.frame_state); if (ctx->buffers_rte) mtl_session_buffers_uinit(&ctx->s); @@ -320,6 +313,31 @@ int ut20tx_poll_event(ut20tx_ctx* ctx, mtl_event_t* ev) { return mtl_video_tx_vtable.event_poll(&ctx->s, ev, 0 /* non-blocking */); } +int ut20tx_poll_event_timeout(ut20tx_ctx* ctx, mtl_event_t* ev, uint32_t timeout_ms) { + return mtl_video_tx_vtable.event_poll(&ctx->s, ev, timeout_ms); +} + +int ut20tx_post_event(ut20tx_ctx* ctx, const mtl_event_t* ev) { + return mtl_session_event_post(&ctx->s, ev); +} + +uint64_t ut20tx_events_dropped(const ut20tx_ctx* ctx) { + return ctx->s.events_dropped; +} + +int ut20tx_get_event_fd(ut20tx_ctx* ctx) { + if (!ctx->s.vt->get_event_fd) return -ENOSYS; + return ctx->s.vt->get_event_fd(&ctx->s); +} + +void ut20tx_set_stopped(ut20tx_ctx* ctx) { + mtl_session_set_stopped(&ctx->s); +} + +void ut20tx_stop(ut20tx_ctx* ctx) { + mtl_session_stop((mtl_session_t*)&ctx->s); +} + /* ── slice op ─────────────────────────────────────────────────────────── */ int ut20tx_slice_ready(ut20tx_ctx* ctx, mtl_buffer_t* buf, uint16_t lines) { diff --git a/tests/unit/new_api/st20_tx_harness.h b/tests/unit/new_api/st20_tx_harness.h index 61953014a..ccb904e60 100644 --- a/tests/unit/new_api/st20_tx_harness.h +++ b/tests/unit/new_api/st20_tx_harness.h @@ -97,6 +97,19 @@ int ut20tx_reset_stats(ut20tx_ctx* ctx); /** Non-blocking event_poll wrapper. 0 + *ev on an event, -ETIMEDOUT if none. */ int ut20tx_poll_event(ut20tx_ctx* ctx, mtl_event_t* ev); +/** event_poll wrapper with an explicit timeout (drives the blocking path). */ +int ut20tx_poll_event_timeout(ut20tx_ctx* ctx, mtl_event_t* ev, uint32_t timeout_ms); +/** Post an event through the producer path (mtl_session_event_post). */ +int ut20tx_post_event(ut20tx_ctx* ctx, const mtl_event_t* ev); +/** Read the producer-side dropped-event counter (s->events_dropped). */ +uint64_t ut20tx_events_dropped(const ut20tx_ctx* ctx); +/** Wraps the get_event_fd vtable slot; -ENOSYS if the slot is NULL. */ +int ut20tx_get_event_fd(ut20tx_ctx* ctx); +/** Set the session stopped flag (drives the poll -EAGAIN path). */ +void ut20tx_set_stopped(ut20tx_ctx* ctx); +/** Drive the full production stop path (sets stopped + signals the eventfd to + * wake a consumer already blocked in event_poll). */ +void ut20tx_stop(ut20tx_ctx* ctx); /* ── slice op ─────────────────────────────────────────────────────────── */ diff --git a/tests/unit/new_api/st20_tx_test.cpp b/tests/unit/new_api/st20_tx_test.cpp index e59669ddd..29b760f39 100644 --- a/tests/unit/new_api/st20_tx_test.cpp +++ b/tests/unit/new_api/st20_tx_test.cpp @@ -16,8 +16,11 @@ #include +#include #include +#include #include +#include #include "new_api/st20_tx_harness.h" @@ -467,3 +470,92 @@ TEST_F(St20NewApiTxTest, BufferPoolSizeFollowsFrameCount) { << "frame " << i << " must map to a distinct wrapper"; } } + +/* ── event-system mechanism (producer-nonblocking / consumer-blocking) ─── */ + +/* Producer overflow: posting far past the ring capacity must never block or + * crash; the surplus is dropped and counted in s->events_dropped. Proves the + * post path drops-on-full instead of allocating/growing. */ +TEST_F(St20NewApiTxTest, EventPostDropsOnFullBumpsCounter) { + mtl_event_t ev = {}; + ev.type = MTL_EVENT_BUFFER_DONE; + + const int kPosts = 256; + int accepted = 0, dropped = 0; + for (int i = 0; i < kPosts; i++) { + ev.ctx = reinterpret_cast(static_cast(i)); + int ret = ut20tx_post_event(ctx_, &ev); + if (ret == 0) + accepted++; + else + dropped++; + } + + EXPECT_GT(dropped, 0) << "posting past ring capacity must drop the surplus"; + EXPECT_EQ(accepted + dropped, kPosts); + EXPECT_EQ(ut20tx_events_dropped(ctx_), static_cast(dropped)) + << "every dropped post must bump the producer drop counter"; +} + +/* Value round-trip: the queue stores events by value (copy-in), so a polled + * event reproduces the posted type/ctx/status/timestamp — not a freed pointer. */ +TEST_F(St20NewApiTxTest, EventPostPollValueRoundTrip) { + mtl_event_t in = {}; + in.type = MTL_EVENT_FRAME_LATE; + in.status = -42; + in.timestamp = kFrameTai; + in.ctx = reinterpret_cast(static_cast(0xABCD)); + + ASSERT_EQ(ut20tx_post_event(ctx_, &in), 0); + + mtl_event_t out = {}; + ASSERT_EQ(ut20tx_poll_event(ctx_, &out), 0); + EXPECT_EQ(out.type, in.type); + EXPECT_EQ(out.status, in.status); + EXPECT_EQ(out.timestamp, in.timestamp); + EXPECT_EQ(out.ctx, in.ctx); +} + +/* timeout_ms == 0 on an empty queue is non-blocking and returns -ETIMEDOUT. */ +TEST_F(St20NewApiTxTest, EventPollTimeoutZeroOnEmpty) { + mtl_event_t ev; + EXPECT_EQ(ut20tx_poll_event(ctx_, &ev), -ETIMEDOUT); +} + +/* Pre-check path: stopped before poll() is entered, so the top-of-function + * check returns -EAGAIN without ever blocking. */ +TEST_F(St20NewApiTxTest, EventPollPreStopReturnsEagain) { + ut20tx_set_stopped(ctx_); + mtl_event_t ev; + EXPECT_EQ(ut20tx_poll_event_timeout(ctx_, &ev, 1000), -EAGAIN); +} + +/* Missed-wakeup path: a consumer already blocked in poll() must wake promptly + * when another thread calls stop(), returning -EAGAIN well under the timeout + * rather than waiting out the full timeout_ms. */ +TEST_F(St20NewApiTxTest, EventPollStopWakesBlockedPoll) { + std::atomic entered{false}; + std::atomic ret{1}; + std::thread consumer([&] { + mtl_event_t ev; + entered.store(true, std::memory_order_release); + ret.store(ut20tx_poll_event_timeout(ctx_, &ev, 5000), std::memory_order_release); + }); + + while (!entered.load(std::memory_order_acquire)) std::this_thread::yield(); + + auto t0 = std::chrono::steady_clock::now(); + ut20tx_stop(ctx_); + consumer.join(); + auto elapsed_ms = std::chrono::duration_cast( + std::chrono::steady_clock::now() - t0) + .count(); + + EXPECT_EQ(ret.load(std::memory_order_acquire), -EAGAIN); + EXPECT_LT(elapsed_ms, 1000) << "stop() must wake the blocked poll promptly"; +} + +/* get_event_fd is wired and returns a valid (>= 0) wakeup fd. */ +TEST_F(St20NewApiTxTest, EventFdValid) { + EXPECT_GE(ut20tx_get_event_fd(ctx_), 0); +} From b0e24975580a871c5059dc131cd3403c00a7cd71 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 10 Jun 2026 08:30:01 +0000 Subject: [PATCH 14/16] new_api: gate per-frame BUFFER_DONE to user-owned TX (Phase 2.2, B2) In library-owned TX mode a completed transmission returns the frame slot to the FREE pool implicitly and the app reuses it via the next buffer_get(); it does not consume a per-frame completion event. video_tx_notify_frame_done was nonetheless posting MTL_EVENT_BUFFER_DONE on every transmitted frame, flooding the 64-entry value-backed event ring (nothing drains it at frame rate) and dropping real events. Gate the BUFFER_DONE post on s->ownership == MTL_BUFFER_USER_OWNED, mirroring the existing drop_when_late path. Stats updates and the TX_FRAME_FREE store stay unconditional; FRAME_LATE and RX are untouched. BUFFER_DONE.ctx (user_ctx) is only meaningful for user-owned external buffers anyway. Unit: 322/322. New tests: LibraryOwnedFrameDonePostsNoEvent (fails without fix), UserOwnedFrameDonePostsBufferDone (distinct data/ctx sentinels assert ctx round-trip). Reviewer: APPROVE. --- lib/src/new_api/mt_session_video_tx.c | 12 +++++--- tests/unit/new_api/st20_tx_test.cpp | 44 +++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/lib/src/new_api/mt_session_video_tx.c b/lib/src/new_api/mt_session_video_tx.c index 74c2e246a..e36db2463 100644 --- a/lib/src/new_api/mt_session_video_tx.c +++ b/lib/src/new_api/mt_session_video_tx.c @@ -246,11 +246,13 @@ static int video_tx_notify_frame_done(void* priv, uint16_t frame_idx, __ATOMIC_RELAXED); /* Post completion event */ - mtl_event_t event = {0}; - event.type = MTL_EVENT_BUFFER_DONE; - event.timestamp = meta ? meta->epoch : 0; - event.ctx = user_ctx; /* User context for user-owned mode */ - mtl_session_event_post(s, &event); + if (s->ownership == MTL_BUFFER_USER_OWNED) { + mtl_event_t event = {0}; + event.type = MTL_EVENT_BUFFER_DONE; + event.timestamp = meta ? meta->epoch : 0; + event.ctx = user_ctx; /* User context for user-owned mode */ + mtl_session_event_post(s, &event); + } return 0; } diff --git a/tests/unit/new_api/st20_tx_test.cpp b/tests/unit/new_api/st20_tx_test.cpp index 29b760f39..45bca01f5 100644 --- a/tests/unit/new_api/st20_tx_test.cpp +++ b/tests/unit/new_api/st20_tx_test.cpp @@ -350,6 +350,50 @@ TEST_F(St20NewApiTxTest, FrameLifecycleGetPutTransmit) { EXPECT_EQ(processed(), 1u) << "buffers_processed bumps on frame_done"; } +/* Library-owned TX: a completed transmission returns the slot to FREE + * implicitly and the app reuses it via the next buffer_get. It does NOT consume + * a per-frame completion event, so notify_frame_done must NOT post BUFFER_DONE + * — a per-frame flood would overflow the value-backed event ring. */ +TEST_F(St20NewApiTxTest, LibraryOwnedFrameDonePostsNoEvent) { + mtl_buffer_t* b = get(); + ASSERT_NE(b, nullptr); + ASSERT_EQ(put(b), 0); + uint16_t idx = 0xffff; + ASSERT_EQ(ut20tx_get_next_frame(ctx_, &idx), 0); + + ASSERT_EQ(ut20tx_frame_done(ctx_, idx), 0); + EXPECT_EQ(processed(), 1u); + + mtl_event_t ev; + EXPECT_EQ(ut20tx_poll_event_timeout(ctx_, &ev, 0), -ETIMEDOUT) + << "library-owned frame_done must not post a per-frame BUFFER_DONE event"; +} + +/* User-owned TX: notify_frame_done returns the external buffer to the app via + * MTL_EVENT_BUFFER_DONE carrying the exact registered user_ctx. */ +TEST_F(St20NewApiTxTest, UserOwnedFrameDonePostsBufferDone) { + ASSERT_EQ(ut20tx_set_user_owned(ctx_), 0); + + /* Distinct objects for data and ctx so the assertion can only be satisfied by + * the registered user_ctx, not by the buffer data/IOVA pointer. */ + int user_data = 0; + int ctx_sentinel = 0; + void* user_ctx = &ctx_sentinel; + ASSERT_EQ(ut20tx_post_user_buffer(ctx_, &user_data, user_ctx), 0); + + uint16_t idx = 0xffff; + ASSERT_EQ(ut20tx_get_next_frame(ctx_, &idx), 0); + ASSERT_EQ(state(0), kTransmitting); + + ASSERT_EQ(ut20tx_frame_done(ctx_, idx), 0); + + mtl_event_t ev; + ASSERT_EQ(ut20tx_poll_event_timeout(ctx_, &ev, 0), 0) + << "user-owned frame_done must post a BUFFER_DONE event"; + EXPECT_EQ(ev.type, MTL_EVENT_BUFFER_DONE); + EXPECT_EQ(ev.ctx, user_ctx); +} + /* buffer_put threads the buffer's user_meta pointer/size into the frame slot's * tv_meta, so the transport sends it alongside the frame. */ TEST_F(St20NewApiTxTest, UserMetaPassthroughOnPut) { From b400c5ece5e83f1eca31f6b72fe156e79f1d1b5c Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 10 Jun 2026 09:19:52 +0000 Subject: [PATCH 15/16] new_api: move user-owned bind/convert off the tasklet; value-backed user-buf ring (Phase 2.3, B3+B4) User-owned (zero-copy) TX previously did, inside the pacing tasklet (video_tx_get_next_frame): - a full-frame video_convert_frame for non-derive (format-mismatch) buffers, - per-buffer mt_rte_zmalloc/free for the user-buf ring entries (B3), - a re-enqueue malloc when no slot was free. Both are data-plane violations (heavy work + allocation in the tasklet). B3: make user_buf_ring value-backed via rte_ring_create_elem(sizeof(entry)); enqueue/dequeue by value, no per-buffer malloc/free. The ring is now touched only by the app thread (buffer_post / uinit). B4: move slot binding + conversion onto the app thread (buffer_post): - video_tx_claim_free_slot: CAS FREE->APP_OWNED. - video_tx_fill_slot: derive(st20_tx_set_ext_frame) or non-derive (video_convert_frame into the slot framebuffer), store user_ctx, then RELEASE-publish READY; on hard error release the slot to FREE. - buffer_post drains the backlog claim-then-pop (a slot is reserved before the head is dequeued, so the backlog is never reordered = strict FIFO at the bind stage); a new buffer goes to the tail when a backlog exists. - get_next_frame is now a pure READY->TRANSMITTING scan: no convert, no enqueue/dequeue, no malloc. Safety: app thread claims a slot exclusively (APP_OWNED) before st20_tx_set_ext_frame (which only touches that slot, refcnt==0) and before RELEASE-publishing READY; the tasklet ACQUIRE-claims READY->TRANSMITTING, so the converted framebuffer and user_ctx are fully visible. Completion contract: buffer_post returning 0 yields exactly one async BUFFER_DONE; a negative return yields none. Backlog entries (already accepted with a 0 return) get a BUFFER_DONE on hard bind error so the app is not stranded. Unit: 325/325 (new UserOwnedConvertHappensOnPostNotTasklet, UserOwnedValueRingRoundTrip, UserOwnedPostBackpressureDefersAndBinds - the last defers 3 entries, frees one slot, and asserts the OLDEST binds first; fails against the prior head-rotation logic). Reviewer: APPROVE. Known follow-up (Phase 4 lifecycle): no buffer_flush yet, so a backlog is drained only by subsequent buffer_post; documented in the buffer_post contract. --- lib/src/new_api/mt_session_buffer.c | 34 +--- lib/src/new_api/mt_session_video_tx.c | 252 +++++++++++++++----------- tests/unit/new_api/st20_tx_harness.c | 44 ++++- tests/unit/new_api/st20_tx_harness.h | 12 +- tests/unit/new_api/st20_tx_test.cpp | 76 ++++++++ 5 files changed, 279 insertions(+), 139 deletions(-) diff --git a/lib/src/new_api/mt_session_buffer.c b/lib/src/new_api/mt_session_buffer.c index 1c31f6137..c2cca84d5 100644 --- a/lib/src/new_api/mt_session_buffer.c +++ b/lib/src/new_api/mt_session_buffer.c @@ -205,8 +205,9 @@ int mtl_session_user_buf_init(struct mtl_session_impl* s, uint16_t frame_cnt) { snprintf(ring_name, sizeof(ring_name), "mtl_ub_%p", s); - s->user_buf_ring = - rte_ring_create(ring_name, MTL_USER_BUF_RING_SIZE, s->socket_id, 0); + s->user_buf_ring = rte_ring_create_elem( + ring_name, sizeof(struct mtl_user_buffer_entry), MTL_USER_BUF_RING_SIZE, + s->socket_id, 0); if (!s->user_buf_ring) { err("%s(%s), failed to create user buffer ring\n", __func__, s->name); return -ENOMEM; @@ -228,13 +229,7 @@ int mtl_session_user_buf_init(struct mtl_session_impl* s, uint16_t frame_cnt) { } void mtl_session_user_buf_uinit(struct mtl_session_impl* s) { - /* Drain and free any remaining entries in the ring */ if (s->user_buf_ring) { - void* obj = NULL; - while (rte_ring_dequeue(s->user_buf_ring, &obj) == 0 && obj) { - mt_rte_free(obj); - obj = NULL; - } rte_ring_free(s->user_buf_ring); s->user_buf_ring = NULL; } @@ -259,20 +254,10 @@ int mtl_session_user_buf_enqueue(struct mtl_session_impl* s, void* data, mtl_iova_t iova, size_t size, void* user_ctx) { if (!s->user_buf_ring) return -EINVAL; - struct mtl_user_buffer_entry* entry = - mt_rte_zmalloc_socket(sizeof(*entry), s->socket_id); - if (!entry) { - err("%s(%s), failed to alloc user buffer entry\n", __func__, s->name); - return -ENOMEM; - } + struct mtl_user_buffer_entry e = { + .data = data, .iova = iova, .size = size, .user_ctx = user_ctx}; - entry->data = data; - entry->iova = iova; - entry->size = size; - entry->user_ctx = user_ctx; - - if (rte_ring_enqueue(s->user_buf_ring, entry) != 0) { - mt_rte_free(entry); + if (rte_ring_enqueue_elem(s->user_buf_ring, &e, sizeof(e)) != 0) { dbg("%s(%s), user buffer ring full\n", __func__, s->name); return -ENOSPC; } @@ -284,14 +269,9 @@ int mtl_session_user_buf_dequeue(struct mtl_session_impl* s, struct mtl_user_buffer_entry* entry) { if (!s->user_buf_ring) return -EINVAL; - void* obj = NULL; - if (rte_ring_dequeue(s->user_buf_ring, &obj) != 0 || !obj) { + if (rte_ring_dequeue_elem(s->user_buf_ring, entry, sizeof(*entry)) != 0) { return -EAGAIN; } - - struct mtl_user_buffer_entry* queued = (struct mtl_user_buffer_entry*)obj; - *entry = *queued; - mt_rte_free(queued); return 0; } diff --git a/lib/src/new_api/mt_session_video_tx.c b/lib/src/new_api/mt_session_video_tx.c index e36db2463..768644294 100644 --- a/lib/src/new_api/mt_session_video_tx.c +++ b/lib/src/new_api/mt_session_video_tx.c @@ -10,10 +10,9 @@ * and st20_tx_ops. */ -#include "mt_session_video_common.h" - #include "../mt_log.h" #include "../mt_mem.h" +#include "mt_session_video_common.h" /************************************************************************* * TX Frame State Machine @@ -48,7 +47,7 @@ enum tx_frame_state { struct video_tx_ctx { struct mtl_session_impl* session; - st20_tx_handle handle; /**< low-level TX handle */ + st20_tx_handle handle; /**< low-level TX handle */ struct video_convert_ctx convert; /**< shared format conversion context */ /** Per-frame state tracking (protected by session->buffer_lock) */ @@ -72,8 +71,7 @@ static inline struct video_tx_ctx* tx_ctx_from_session(struct mtl_session_impl* } static bool tx_frame_is_late(struct video_tx_ctx* ctx, - struct st_tx_video_session_impl* tx_impl, - uint16_t idx) { + struct st_tx_video_session_impl* tx_impl, uint16_t idx) { struct mtl_session_impl* s = ctx->session; if (!(s->flags & MTL_SESSION_FLAG_USER_PACING)) return false; struct st20_tx_frame_meta* meta = &tx_impl->st20_frames[idx].tv_meta; @@ -92,12 +90,78 @@ static bool tx_frame_is_late(struct video_tx_ctx* ctx, * briefly for frame_state transitions. *************************************************************************/ +/** + * Claim a FREE frame slot for the app thread to fill. + * + * Scans frame_state[] and CAS FREE→APP_OWNED so the slot is reserved before any + * buffer is dequeued. Claiming first lets the drain pop the backlog only when a + * slot is guaranteed, so a failed bind never has to rotate the ring head. + * + * @return claimed slot index, or -EAGAIN if no slot is free. + */ +static int video_tx_claim_free_slot(struct mtl_session_impl* s, + struct video_tx_ctx* ctx) { + struct st_tx_video_session_impl* tx_impl = s->inner.video_tx; + + for (uint16_t i = 0; i < tx_impl->st20_frames_cnt; i++) { + enum tx_frame_state expected = TX_FRAME_FREE; + if (__atomic_compare_exchange_n(&ctx->frame_state[i], &expected, TX_FRAME_APP_OWNED, + false, __ATOMIC_ACQ_REL, __ATOMIC_RELAXED)) + return i; + } + + return -EAGAIN; +} + +/** + * Fill a claimed (APP_OWNED) slot with a posted user buffer and publish READY. + * + * Derive mode points st20_tx at the external buffer (zero-copy); non-derive + * converts the app-format data into the slot's transport framebuffer. On hard + * error the slot is returned to FREE so it is not leaked as APP_OWNED. + * + * @return 0 if bound, negative errno on bind error. + */ +static int video_tx_fill_slot(struct mtl_session_impl* s, struct video_tx_ctx* ctx, + uint16_t idx, const struct mtl_user_buffer_entry* e) { + struct st_tx_video_session_impl* tx_impl = s->inner.video_tx; + + int ret; + if (ctx->convert.derive) { + struct st20_ext_frame ext = {0}; + ext.buf_addr = e->data; + ext.buf_iova = e->iova; + ext.buf_len = e->size; + ext.opaque = e->user_ctx; + ret = st20_tx_set_ext_frame(ctx->handle, idx, &ext); + } else { + ret = video_convert_frame( + &ctx->convert, e->data, e->iova, e->size, tx_impl->st20_frames[idx].addr, + tx_impl->st20_frames[idx].iova, ctx->convert.transport_frame_size, true); + } + if (ret < 0) { + err("%s(%s), bind failed for slot %u: %d\n", __func__, s->name, idx, ret); + __atomic_store_n(&ctx->frame_state[idx], TX_FRAME_FREE, __ATOMIC_RELEASE); + return ret; + } + + if (s->user_buf_ctx && idx < s->user_buf_ctx_cnt) s->user_buf_ctx[idx] = e->user_ctx; + + __atomic_store_n(&ctx->frame_state[idx], TX_FRAME_READY, __ATOMIC_RELEASE); + return 0; +} + +/** Return a buffer to the app on hard bind error so it is not lost. */ +static void video_tx_post_buffer_done(struct mtl_session_impl* s, void* user_ctx) { + mtl_event_t ev = {0}; + ev.type = MTL_EVENT_BUFFER_DONE; + ev.ctx = user_ctx; + mtl_session_event_post(s, &ev); +} + /** * get_next_frame callback - library asks which frame to transmit next. * Scans for a frame in READY state and transitions it to TRANSMITTING. - * - * For user-owned mode: also checks the user_buf_ring for posted buffers - * and sets ext_frame on a free frame slot before marking it READY. */ static int video_tx_get_next_frame(void* priv, uint16_t* next_frame_idx, struct st20_tx_frame_meta* meta) { @@ -109,78 +173,11 @@ static int video_tx_get_next_frame(void* priv, uint16_t* next_frame_idx, if (!tx_impl || !tx_impl->st20_frames) return -EIO; if (!ctx->frame_state) return -EAGAIN; /* init not yet complete */ - /* User-owned mode: check for posted buffers and bind to free frame slots */ - if (s->ownership == MTL_BUFFER_USER_OWNED) { - struct mtl_user_buffer_entry entry; - while (mtl_session_user_buf_dequeue(s, &entry) == 0) { - /* Find a free frame slot to bind this user buffer */ - bool bound = false; - for (uint16_t i = 0; i < tx_impl->st20_frames_cnt; i++) { - enum tx_frame_state expected = TX_FRAME_FREE; - if (__atomic_compare_exchange_n(&ctx->frame_state[i], &expected, - TX_FRAME_APP_OWNED, false, - __ATOMIC_ACQ_REL, __ATOMIC_RELAXED)) { - int ret; - - if (ctx->convert.derive) { - /* Formats match — true zero-copy via ext_frame. - * st20_tx sends the user buffer directly; no conversion. */ - struct st20_ext_frame ext = {0}; - ext.buf_addr = entry.data; - ext.buf_iova = entry.iova; - ext.buf_len = entry.size; - ext.opaque = entry.user_ctx; - - ret = st20_tx_set_ext_frame(ctx->handle, i, &ext); - if (ret < 0) { - err("%s(%s), st20_tx_set_ext_frame failed for slot %u: %d\n", - __func__, s->name, i, ret); - __atomic_store_n(&ctx->frame_state[i], TX_FRAME_FREE, __ATOMIC_RELEASE); - continue; - } - } else { - /* Format conversion needed: convert user data (app format) into - * the library's own framebuffer (transport format). - * st20_tx will transmit the correctly-formatted framebuffer. */ - ret = video_convert_frame( - &ctx->convert, entry.data, entry.iova, entry.size, - tx_impl->st20_frames[i].addr, tx_impl->st20_frames[i].iova, - ctx->convert.transport_frame_size, true /* TX: app→transport */); - if (ret < 0) { - err("%s(%s), format conversion failed for slot %u: %d\n", - __func__, s->name, i, ret); - __atomic_store_n(&ctx->frame_state[i], TX_FRAME_FREE, __ATOMIC_RELEASE); - continue; - } - } - - /* Save user context for completion event */ - if (s->user_buf_ctx && i < s->user_buf_ctx_cnt) { - s->user_buf_ctx[i] = entry.user_ctx; - } - - /* Mark ready for transmission */ - __atomic_store_n(&ctx->frame_state[i], TX_FRAME_READY, __ATOMIC_RELEASE); - bound = true; - break; - } - } - if (!bound) { - dbg("%s(%s), no free frame slot for user buffer, requeueing\n", - __func__, s->name); - /* Re-enqueue - no slot free yet */ - mtl_session_user_buf_enqueue(s, entry.data, entry.iova, entry.size, - entry.user_ctx); - break; - } - } - } - for (uint16_t i = 0; i < tx_impl->st20_frames_cnt; i++) { enum tx_frame_state expected = TX_FRAME_READY; if (__atomic_compare_exchange_n(&ctx->frame_state[i], &expected, - TX_FRAME_TRANSMITTING, false, - __ATOMIC_ACQUIRE, __ATOMIC_RELAXED)) { + TX_FRAME_TRANSMITTING, false, __ATOMIC_ACQUIRE, + __ATOMIC_RELAXED)) { if (ctx->drop_when_late && tx_frame_is_late(ctx, tx_impl, i)) { __atomic_add_fetch(&s->stats.buffers_dropped, 1, __ATOMIC_RELAXED); if (s->ownership == MTL_BUFFER_USER_OWNED) { @@ -340,9 +337,8 @@ static int tx_try_claim_frame(struct mtl_session_impl* s, mtl_buffer_t** buf) { if (rte_atomic32_read(&tx_impl->st20_frames[i].refcnt) != 0) continue; enum tx_frame_state expected = TX_FRAME_FREE; - if (__atomic_compare_exchange_n(&ctx->frame_state[i], &expected, - TX_FRAME_APP_OWNED, false, - __ATOMIC_ACQ_REL, __ATOMIC_RELAXED)) { + if (__atomic_compare_exchange_n(&ctx->frame_state[i], &expected, TX_FRAME_APP_OWNED, + false, __ATOMIC_ACQ_REL, __ATOMIC_RELAXED)) { /* Claimed this frame for the app */ struct mtl_buffer_impl* b = &s->buffers[i % s->buffer_count]; @@ -495,41 +491,87 @@ static int video_tx_buffer_put(struct mtl_session_impl* s, mtl_buffer_t* buf) { /** * Post a user-owned buffer for transmission (zero-copy mode). * - * Looks up IOVA from registered DMA regions, then enqueues the buffer. - * The get_next_frame callback will bind it to a frame slot and transmit. + * Binds the buffer to a free frame slot on this (app) thread: derive mode + * points st20_tx at the external buffer, non-derive converts it into the slot's + * transport framebuffer. Buffers posted while every slot is busy are deferred + * to the user-buf ring and bound, oldest-first, by a later post. The bind stage + * is strict-FIFO; the final transmit order is the get_next_frame slot policy. * Completion is signaled via MTL_EVENT_BUFFER_DONE with user_ctx. + * + * Single-producer: must be called from one app thread per session, matching the + * buffer API contract. The pacing tasklet never touches the user-buf ring, so + * rte_ring_empty() is a reliable backlog check on this thread. + * + * Completion contract: a 0 return means the buffer was accepted and yields + * exactly one asynchronous MTL_EVENT_BUFFER_DONE; a negative return means the + * buffer was rejected synchronously and produces no completion event. Entries + * already accepted into the backlog are drained only by subsequent + * buffer_post() calls, so the app must keep posting until all completions + * arrive (or stop the session) to avoid stranding a backlogged buffer. */ -static int video_tx_buffer_post(struct mtl_session_impl* s, void* data, - size_t size, void* user_ctx) { +static int video_tx_buffer_post(struct mtl_session_impl* s, void* data, size_t size, + void* user_ctx) { if (s->ownership != MTL_BUFFER_USER_OWNED) { err("%s(%s), buffer_post only valid in USER_OWNED mode\n", __func__, s->name); return -EINVAL; } - /* Look up IOVA for the user buffer */ + struct video_tx_ctx* ctx = tx_ctx_from_session(s); + + /* Drain the backlog claim-then-pop: a slot is reserved before the head is + * dequeued, so the ring order is never disturbed when no slot is free. */ + for (;;) { + int idx = video_tx_claim_free_slot(s, ctx); + if (idx < 0) break; + + struct mtl_user_buffer_entry deferred; + if (mtl_session_user_buf_dequeue(s, &deferred) != 0) { + __atomic_store_n(&ctx->frame_state[idx], TX_FRAME_FREE, __ATOMIC_RELEASE); + break; + } + + if (video_tx_fill_slot(s, ctx, idx, &deferred) < 0) + video_tx_post_buffer_done(s, deferred.user_ctx); + } + mtl_iova_t iova = mtl_session_lookup_iova(s, data, size); if (iova == MTL_BAD_IOVA) { - err("%s(%s), failed to get IOVA for buffer %p (not registered?)\n", - __func__, s->name, data); + err("%s(%s), failed to get IOVA for buffer %p (not registered?)\n", __func__, s->name, + data); return -EINVAL; } - return mtl_session_user_buf_enqueue(s, data, iova, size, user_ctx); + /* A non-empty backlog outranks the new buffer: enqueue it to the tail. */ + if (!rte_ring_empty(s->user_buf_ring)) + return mtl_session_user_buf_enqueue(s, data, iova, size, user_ctx); + + int idx = video_tx_claim_free_slot(s, ctx); + if (idx < 0) return mtl_session_user_buf_enqueue(s, data, iova, size, user_ctx); + + struct mtl_user_buffer_entry e = { + .data = data, .iova = iova, .size = size, .user_ctx = user_ctx}; + /* Direct bind of the new buffer: a hard error is reported synchronously via + * the negative return (video_tx_fill_slot already released the slot to FREE), + * so do not also post a BUFFER_DONE - that would give the app two completions + * for one buffer. Only backlog entries, already accepted with a 0 return, are + * completed asynchronously on error above. */ + int ret = video_tx_fill_slot(s, ctx, idx, &e); + if (ret < 0) return ret; + return 0; } /** * Register a memory region for DMA access (user-owned mode). * After registration, buffers from this region can be passed to buffer_post(). */ -static int video_tx_mem_register(struct mtl_session_impl* s, void* addr, - size_t size, mtl_dma_mem_t** handle) { +static int video_tx_mem_register(struct mtl_session_impl* s, void* addr, size_t size, + mtl_dma_mem_t** handle) { if (s->dma_registration_cnt >= 8) { err("%s(%s), too many DMA registrations (max 8)\n", __func__, s->name); return -ENOSPC; } - struct mtl_dma_mem_impl* reg = - mt_rte_zmalloc_socket(sizeof(*reg), s->socket_id); + struct mtl_dma_mem_impl* reg = mt_rte_zmalloc_socket(sizeof(*reg), s->socket_id); if (!reg) return -ENOMEM; reg->parent = s->parent; @@ -555,8 +597,8 @@ static int video_tx_mem_register(struct mtl_session_impl* s, void* addr, s->dma_registrations[s->dma_registration_cnt++] = reg; - info("%s(%s), registered DMA region %p, size %zu, iova 0x%" PRIx64 "\n", - __func__, s->name, addr, size, reg->iova); + info("%s(%s), registered DMA region %p, size %zu, iova 0x%" PRIx64 "\n", __func__, + s->name, addr, size, reg->iova); *handle = (mtl_dma_mem_t*)reg; return 0; @@ -565,8 +607,7 @@ static int video_tx_mem_register(struct mtl_session_impl* s, void* addr, /** * Unregister a previously registered DMA memory region. */ -static int video_tx_mem_unregister(struct mtl_session_impl* s, - mtl_dma_mem_t* handle) { +static int video_tx_mem_unregister(struct mtl_session_impl* s, mtl_dma_mem_t* handle) { struct mtl_dma_mem_impl* reg = (struct mtl_dma_mem_impl*)handle; for (uint8_t i = 0; i < s->dma_registration_cnt; i++) { @@ -586,17 +627,13 @@ static int video_tx_mem_unregister(struct mtl_session_impl* s, return -EINVAL; } -static int video_tx_stats_get(struct mtl_session_impl* s, - mtl_session_stats_t* stats) { +static int video_tx_stats_get(struct mtl_session_impl* s, mtl_session_stats_t* stats) { /* Read stats atomically — no lock needed, no deadlock possible */ stats->buffers_processed = __atomic_load_n(&s->stats.buffers_processed, __ATOMIC_RELAXED); - stats->bytes_processed = - __atomic_load_n(&s->stats.bytes_processed, __ATOMIC_RELAXED); - stats->buffers_dropped = - __atomic_load_n(&s->stats.buffers_dropped, __ATOMIC_RELAXED); - stats->epochs_missed = - __atomic_load_n(&s->stats.epochs_missed, __ATOMIC_RELAXED); + stats->bytes_processed = __atomic_load_n(&s->stats.bytes_processed, __ATOMIC_RELAXED); + stats->buffers_dropped = __atomic_load_n(&s->stats.buffers_dropped, __ATOMIC_RELAXED); + stats->epochs_missed = __atomic_load_n(&s->stats.epochs_missed, __ATOMIC_RELAXED); struct st_tx_video_session_impl* tx_impl = s->inner.video_tx; struct video_tx_ctx* ctx = tx_impl ? tx_impl->ops.priv : NULL; @@ -668,7 +705,7 @@ const mtl_session_vtable_t mtl_video_tx_vtable = { .buffer_flush = NULL, .mem_register = video_tx_mem_register, .mem_unregister = video_tx_mem_unregister, - .event_poll = video_session_event_poll, /* shared implementation */ + .event_poll = video_session_event_poll, /* shared implementation */ .get_event_fd = video_session_get_event_fd, /* shared implementation */ .stats_get = video_tx_stats_get, .stats_reset = video_session_stats_reset, /* shared implementation */ @@ -920,9 +957,8 @@ int mtl_video_tx_session_init(struct mtl_session_impl* s, struct mtl_main_impl* info("%s(%d), transport fmt %s, input fmt: %s, frame_size %zu, fb_cnt %u, derive %d\n", __func__, s->idx, st20_fmt_name(config->transport_fmt), - st_frame_fmt_name(config->frame_fmt), - ctx->convert.transport_frame_size, ops.framebuff_cnt, - ctx->convert.derive); + st_frame_fmt_name(config->frame_fmt), ctx->convert.transport_frame_size, + ops.framebuff_cnt, ctx->convert.derive); return 0; } diff --git a/tests/unit/new_api/st20_tx_harness.c b/tests/unit/new_api/st20_tx_harness.c index 350dd4f27..db69fadb8 100644 --- a/tests/unit/new_api/st20_tx_harness.c +++ b/tests/unit/new_api/st20_tx_harness.c @@ -210,12 +210,50 @@ void ut20tx_set_ptp_now(ut20tx_ctx* ctx, uint64_t ns) { } int ut20tx_set_user_owned(ut20tx_ctx* ctx) { - ctx->s.ownership = MTL_BUFFER_USER_OWNED; - return mtl_session_user_buf_init(&ctx->s, ctx->framebuff_cnt); + struct mtl_session_impl* s = &ctx->s; + s->ownership = MTL_BUFFER_USER_OWNED; + + /* Catch-all DMA registration so video_tx_buffer_post's IOVA lookup resolves + * any stack buffer the tests post (iova == virtual address; never DMA'd). */ + struct mtl_dma_mem_impl* reg = mt_rte_zmalloc_socket(sizeof(*reg), s->socket_id); + if (!reg) return -ENOMEM; + reg->addr = NULL; + reg->size = (size_t)-1; + reg->iova = 0; + s->dma_registrations[0] = reg; + s->dma_registration_cnt = 1; + + return mtl_session_user_buf_init(s, ctx->framebuff_cnt); +} + +void ut20tx_set_user_convert(ut20tx_ctx* ctx) { + ctx->vctx.convert.derive = false; + ctx->vctx.convert.frame_fmt = ST_FRAME_FMT_YUV422PLANAR10LE; + ctx->vctx.convert.transport_fmt = ST20_FMT_YUV_422_10BIT; } int ut20tx_post_user_buffer(ut20tx_ctx* ctx, void* data, void* user_ctx) { - return mtl_session_user_buf_enqueue(&ctx->s, data, 0, UT20TX_FRAME_STRIDE, user_ctx); + return mtl_video_tx_vtable.buffer_post(&ctx->s, data, UT20TX_FRAME_STRIDE, + user_ctx); +} + +int ut20tx_user_buf_roundtrip(ut20tx_ctx* ctx, void* data, mtl_iova_t iova, + size_t size, void* user_ctx, void** out_data, + mtl_iova_t* out_iova, size_t* out_size, + void** out_ctx) { + int ret = mtl_session_user_buf_enqueue(&ctx->s, data, iova, size, user_ctx); + if (ret != 0) return ret; + + struct mtl_user_buffer_entry e; + memset(&e, 0, sizeof(e)); + ret = mtl_session_user_buf_dequeue(&ctx->s, &e); + if (ret != 0) return ret; + + if (out_data) *out_data = e.data; + if (out_iova) *out_iova = e.iova; + if (out_size) *out_size = e.size; + if (out_ctx) *out_ctx = e.user_ctx; + return 0; } void ut20tx_frame_set_timestamp(ut20tx_ctx* ctx, uint16_t idx, uint64_t tai_ns) { diff --git a/tests/unit/new_api/st20_tx_harness.h b/tests/unit/new_api/st20_tx_harness.h index ccb904e60..2cb014cb2 100644 --- a/tests/unit/new_api/st20_tx_harness.h +++ b/tests/unit/new_api/st20_tx_harness.h @@ -49,8 +49,18 @@ void ut20tx_set_ptp_now(ut20tx_ctx* ctx, uint64_t ns); /** Switch the session to MTL_BUFFER_USER_OWNED and init the user-buf ring. */ int ut20tx_set_user_owned(ut20tx_ctx* ctx); -/** Post an external user buffer (queued for get_next_frame to bind). */ +/** Drive the production buffer_post path: binds the buffer on the app thread. */ int ut20tx_post_user_buffer(ut20tx_ctx* ctx, void* data, void* user_ctx); +/** Switch the user-owned convert ctx to non-derive, so buffer_post takes the + * format-conversion branch (the shared video_convert_frame stub copies the + * first source byte to the destination slot). */ +void ut20tx_set_user_convert(ut20tx_ctx* ctx); +/** Round-trip one entry through the value-backed user-buf ring, copying the + * dequeued fields to the out-params. Returns 0 on success. */ +int ut20tx_user_buf_roundtrip(ut20tx_ctx* ctx, void* data, mtl_iova_t iova, + size_t size, void* user_ctx, void** out_data, + mtl_iova_t* out_iova, size_t* out_size, + void** out_ctx); /** Stamp a frame slot's tv_meta with a TAI timestamp, bypassing buffer_put. */ void ut20tx_frame_set_timestamp(ut20tx_ctx* ctx, uint16_t idx, uint64_t tai_ns); /** Read the per-frame user_ctx slot (user-owned completion bookkeeping). */ diff --git a/tests/unit/new_api/st20_tx_test.cpp b/tests/unit/new_api/st20_tx_test.cpp index 45bca01f5..9b88021e4 100644 --- a/tests/unit/new_api/st20_tx_test.cpp +++ b/tests/unit/new_api/st20_tx_test.cpp @@ -394,6 +394,82 @@ TEST_F(St20NewApiTxTest, UserOwnedFrameDonePostsBufferDone) { EXPECT_EQ(ev.ctx, user_ctx); } +/* B4: the full-frame pixel conversion of a user-owned, non-derive buffer must + * run on the app thread inside buffer_post, NOT inside the pacing tasklet. After + * post and BEFORE any get_next_frame call, the destination slot is already + * converted and READY. (The shared video_convert_frame stub copies the first + * source byte to the slot, so the sentinel proves the convert branch ran.) */ +TEST_F(St20NewApiTxTest, UserOwnedConvertHappensOnPostNotTasklet) { + ASSERT_EQ(ut20tx_set_user_owned(ctx_), 0); + ut20tx_set_user_convert(ctx_); + + uint8_t user_data[8] = {0xCC}; + int ctx_sentinel = 0; + ASSERT_EQ(ut20tx_post_user_buffer(ctx_, user_data, &ctx_sentinel), 0); + + EXPECT_EQ(state(0), kReady) + << "post must leave the bound slot READY before any get_next_frame"; + EXPECT_EQ(((uint8_t*)ut20tx_frame_addr(ctx_, 0))[0], 0xCC) + << "conversion must run on the post (app) thread, not the tasklet"; +} + +/* B3: user-buf entries round-trip through the value-backed ring by value, so the + * producer never allocates. The dequeued fields must match what was enqueued. */ +TEST_F(St20NewApiTxTest, UserOwnedValueRingRoundTrip) { + ASSERT_EQ(ut20tx_set_user_owned(ctx_), 0); + + int data = 0; + int sentinel = 0; + void* out_data = nullptr; + mtl_iova_t out_iova = 0; + size_t out_size = 0; + void* out_ctx = nullptr; + ASSERT_EQ(ut20tx_user_buf_roundtrip(ctx_, &data, 0xdead, 64, &sentinel, &out_data, + &out_iova, &out_size, &out_ctx), + 0); + + EXPECT_EQ(out_data, &data); + EXPECT_EQ(out_iova, 0xdeadu); + EXPECT_EQ(out_size, 64u); + EXPECT_EQ(out_ctx, &sentinel); +} + +/* B4 backpressure: with all N slots busy and several buffers backlogged, a post + * that cannot bind must leave the backlog in strict submission order. When one + * slot later frees, the OLDEST deferred buffer binds it — proving the drain + * never rotates the ring head. Multiple residents at the no-slot moment are + * required: a single-entry backlog cannot expose head rotation. */ +TEST_F(St20NewApiTxTest, UserOwnedPostBackpressureDefersAndBinds) { + ASSERT_EQ(ut20tx_set_user_owned(ctx_), 0); + + int data[7] = {0}; + int sentinel[7] = {0}; + + /* Three slots: posts 0..2 bind, posts 3..5 are deferred (backlog [3,4,5]). + * Each deferred post drains first while every slot is busy; a pop-then-fail + * drain would rotate the head out of order before the backlog even settles. */ + for (int i = 0; i < 6; i++) + ASSERT_EQ(ut20tx_post_user_buffer(ctx_, &data[i], &sentinel[i]), 0) + << "post must never block; excess buffers are deferred"; + EXPECT_EQ(state(0), kReady); + EXPECT_EQ(state(1), kReady); + EXPECT_EQ(state(2), kReady); + + /* Free exactly ONE slot. */ + uint16_t idx = 0xffff; + ASSERT_EQ(ut20tx_get_next_frame(ctx_, &idx), 0); + ASSERT_EQ(ut20tx_frame_done(ctx_, idx), 0); + ASSERT_EQ(state(idx), kFree); + + /* A later post drains the backlog into the freed slot. The oldest deferred + * buffer (data[3]) must bind it, not a newer one. */ + ASSERT_EQ(ut20tx_post_user_buffer(ctx_, &data[6], &sentinel[6]), 0); + EXPECT_EQ(state(idx), kReady); + EXPECT_EQ(ut20tx_user_buf_ctx(ctx_, idx), &sentinel[3]) + << "oldest deferred buffer must bind first (strict FIFO at bind stage)"; + EXPECT_EQ(dropped(), 0u); +} + /* buffer_put threads the buffer's user_meta pointer/size into the frame slot's * tv_meta, so the transport sends it alongside the frame. */ TEST_F(St20NewApiTxTest, UserMetaPassthroughOnPut) { From 14a1f80ccc6ec8d2ae021a2baad8e054564dc000 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 10 Jun 2026 12:11:13 +0000 Subject: [PATCH 16/16] doc: add new unified session API documentation Bring the new_API docs into the repo and align them with the current post-Phase-2 implementation: - CURRENT_STATE.md: rewritten to lib/src/new_api/ paths, value-backed event ring + eventfd poll, frame-state model, implemented buffer_post/ mem_register, completion contract, and the -ENOTSUP/TODO list. - GRACEFUL_SHUTDOWN.md: guard-centric teardown. destroy() is the single safe primitive (validated generation-tagged handle + refcount drain + eventfd wake); stop() is optional for SIGINT quiesce and pause/resume. - List-of-changes.md: fixed dangling links, annotated auto-start divergence. - samples/: reference the real app/sample/new_api samples; slice/ST22 diagrams marked not-yet-implemented. Docs describe the target guard design; the guard itself is not yet wired into mt_session.c. --- doc/new_API/CURRENT_STATE.md | 371 ++++++++++++++++++++ doc/new_API/GRACEFUL_SHUTDOWN.md | 319 +++++++++++++++++ doc/new_API/List-of-changes.md | 572 +++++++++++++++++++++++++++++++ doc/new_API/samples/README.md | 167 +++++++++ doc/new_API/samples/diagrams.md | 347 +++++++++++++++++++ 5 files changed, 1776 insertions(+) create mode 100644 doc/new_API/CURRENT_STATE.md create mode 100644 doc/new_API/GRACEFUL_SHUTDOWN.md create mode 100644 doc/new_API/List-of-changes.md create mode 100644 doc/new_API/samples/README.md create mode 100644 doc/new_API/samples/diagrams.md diff --git a/doc/new_API/CURRENT_STATE.md b/doc/new_API/CURRENT_STATE.md new file mode 100644 index 000000000..8b2d06013 --- /dev/null +++ b/doc/new_API/CURRENT_STATE.md @@ -0,0 +1,371 @@ +# MTL Unified Session API — Current Implementation State + +**Last updated:** 2026-06-10 +**Branch:** `new-api-rebased` +**Scope of this revision:** brought in line with the code after the Phase 1 +(crash/correctness) and Phase 2 (data-plane / tasklet-purity) hardening work. +See [NEW_API_PHASE2_HANDOFF.md](../../NEW_API_PHASE2_HANDOFF.md) for the change +log and [NEW_API_REVIEW.md](../../NEW_API_REVIEW.md) for the architectural review +that drove it. + +> **Status in one line:** ST20 (uncompressed video) TX **and** RX are +> implemented and unit-tested (library-owned and user-owned/zero-copy). Audio +> (ST30), ancillary (ST40) and ST22 compressed video are **not implemented** +> (`-ENOTSUP`). A hardware (VF) integration gate for the Phase 2 data-plane +> changes is still pending. + +--- + +## Table of Contents + +1. [Overview](#overview) +2. [Source Layout](#source-layout) +3. [Architecture](#architecture) +4. [What Works Today](#what-works) +5. [How To Use the API](#how-to-use) +6. [User-Owned (Zero-Copy) Path](#user-owned) +7. [Event Queue](#event-queue) +8. [Not Implemented / TODO](#todo) +9. [Samples](#samples) +10. [Build & Test](#build) + +--- + +## 1. Overview + +The unified session API wraps the existing low-level `st20_tx/rx_create()` +sessions behind a single **polymorphic** `mtl_session_t` handle. The goal is one +buffer/event API that is identical regardless of media type, replacing the four +per-media pipeline APIs (`st20p_*`, `st22p_*`, `st30p_*`, `st40p_*`). + +**Key principle:** the new API sits *on top of* the existing low-level API. It +does not replace the internal session manager, scheduler, or data path. It: + +- translates `mtl_video_config_t` → `st20_tx_ops` / `st20_rx_ops`, +- wraps `st_frame_trans` as `mtl_buffer_t`, +- bridges the ST20 transport callbacks → a unified `mtl_event_t` queue, +- manages frame lifecycle through an explicit per-frame state machine. + +Dispatch is **vtable-based**: `mtl_session_buffer_get/put/post()`, +`mtl_session_event_poll()`, etc. call through `s->vt->...`, so the same public +function works for any media type that defines a vtable. + +The public header is shipped at [include/mtl_session_api.h](../../include/mtl_session_api.h). + +--- + +## 2. Source Layout + +All new-API library code now lives under **`lib/src/new_api/`** (it was moved out +of `lib/src/` and `lib/src/st2110/` during the rebase). + +| File | Purpose | +|------|---------| +| [lib/src/new_api/mt_session.h](../../lib/src/new_api/mt_session.h) | Internal types: `mtl_session_impl`, `mtl_buffer_impl`, the vtable, magic numbers, the TX frame-state enum, helper decls | +| [lib/src/new_api/mt_session.c](../../lib/src/new_api/mt_session.c) | Polymorphic dispatch for all `mtl_session_*()` public functions; create/start/stop/destroy | +| [lib/src/new_api/mt_session_buffer.c](../../lib/src/new_api/mt_session_buffer.c) | Buffer wrapper pool; `st_frame_trans` ↔ `mtl_buffer_t`; value-backed user-buffer ring | +| [lib/src/new_api/mt_session_event.c](../../lib/src/new_api/mt_session_event.c) | Event queue: value-backed `rte_ring` + `eventfd` wakeup; drop-and-count on overflow | +| [lib/src/new_api/mt_session_video_common.h](../../lib/src/new_api/mt_session_video_common.h) | Shared TX/RX video helpers (event poll, deadlines, conversion ctx) | +| [lib/src/new_api/mt_session_video_common.c](../../lib/src/new_api/mt_session_video_common.c) | Shared implementations: blocking `event_poll`, format-conversion helper, stats | +| [lib/src/new_api/mt_session_video_tx.c](../../lib/src/new_api/mt_session_video_tx.c) | Video TX: wraps `st20_tx_create`; library- and user-owned buffer paths; pacing-tasklet callbacks | +| [lib/src/new_api/mt_session_video_rx.c](../../lib/src/new_api/mt_session_video_rx.c) | Video RX: wraps `st20_rx_create`; ready-ring delivery; format auto-detect | + +Unit tests and harnesses live under +[tests/unit/new_api/](../../tests/unit/new_api/) (`st20_tx_*`, `st20_rx_*`). + +Samples live under [app/sample/new_api/](../../app/sample/new_api/) — see +[§9](#samples). + +--- + +## 3. Architecture + +``` +┌──────────────────────────────────────────────────────────┐ +│ User Application │ +│ mtl_video_session_create() → buffer_get()/buffer_post() │ +│ → buffer_put() / event_poll() → stop() → destroy() │ +└────────────────────────┬─────────────────────────────────┘ + │ public API: mtl_session_api.h +┌────────────────────────┴─────────────────────────────────┐ +│ mt_session.c (dispatch layer) │ +│ Validates magic → selects vtable → dispatches │ +└────────────────────────┬─────────────────────────────────┘ + │ vtable (mtl_session_vtable) + ┌──────────────┼──────────────┐ + ┌─────┴─────┐ ┌──────┴────┐ ┌──────┴──────┐ + │ video_tx │ │ video_rx │ │ audio/anc │ + │ vtable │ │ vtable │ │ (NOT IMPL.) │ + └─────┬─────┘ └─────┬─────┘ └─────────────┘ + │ │ + ┌─────┴─────┐ ┌─────┴─────┐ + │st20_tx_ │ │st20_rx_ │ + │create() │ │create() │ + └───────────┘ └───────────┘ +``` + +### Magic numbers (runtime validation) + +| Magic | Type | Notes | +|-------|------|-------| +| `0x4D564458` | Video TX | comment says "MVTX" but the bytes spell "MVDX" — a benign typo (the value is internally consistent); slated for cleanup in Phase 5 | +| `0x4D565258` | Video RX | "MVRX" | +| `0x4D415458` | Audio TX | "MATX" | +| `0x4D415258` | Audio RX | "MARX" | +| `0x4D4E5458` | Ancillary TX | "MNTX" | +| `0x4D4E5258` | Ancillary RX | "MNRX" | + +--- + +## 4. What Works Today + +| Feature | TX | RX | Notes | +|---------|----|----|-------| +| Video session create/destroy | ✅ | ✅ | `mtl_video_config_t` → `st20_tx/rx_ops` → `st20_tx/rx_create` | +| Library-owned `buffer_get`/`buffer_put` | ✅ | ✅ | TX: fill & submit; RX: receive & release | +| User-owned `buffer_post` (zero-copy) | ✅ | ✅ | `mem_register` + `buffer_post`; completion via `BUFFER_DONE` event | +| `mem_register` / `mem_unregister` | ✅ | ✅ | Up to 8 DMA regions per session | +| Format conversion (frame_fmt ↔ transport_fmt) | ✅ | ✅ | Auto-detected; *derive* (zero-copy) when formats match. TX user-owned conversion runs on the app thread (not the tasklet). | +| Event queue + `event_poll` | ✅ | ✅ | Value-backed ring + `eventfd`; blocking with timeout (see [§7](#event-queue)) | +| `get_event_fd` (epoll integration) | ✅ | ✅ | Returns the level-triggered `eventfd` | +| Start / stop / is_stopped | ✅ | ✅ | `stop()` is reversible and signals the eventfd to wake a blocked consumer | +| Statistics (`stats_get`) | ✅ | ✅ | processed / dropped / bytes / epochs-missed / free / in-use | +| Per-port IO stats | ✅ | ✅ | wraps `st20_tx/rx_get_session_stats` | +| Online destination update (TX) | ✅ | — | `st20_tx_update_destination` | +| Online source update (RX) | — | ✅ | `st20_rx_update_source` | +| VSYNC events | ✅ | ✅ | when `MTL_SESSION_FLAG_ENABLE_VSYNC` set | +| Frame-late notification (TX) | ✅ | — | posts `MTL_EVENT_FRAME_LATE` | +| `drop_when_late` (TX) | ✅ | — | `MTL_SESSION_FLAG_DROP_WHEN_LATE` (requires user pacing) | +| Format auto-detect (RX) | — | ✅ | posts `MTL_EVENT_FORMAT_DETECTED` | +| Frame size query | ✅ | ✅ | `mtl_session_get_frame_size` (app-visible size) | +| Pcap dump (RX) | — | ✅ | `st20_rx_pcapng_dump` | + +### Completion-event contract (user-owned TX) + +`mtl_session_buffer_post()` returning **0** means the buffer was accepted and +will produce **exactly one** asynchronous `MTL_EVENT_BUFFER_DONE` (carrying the +`user_ctx` you passed). A **negative** return means the buffer was rejected +synchronously and produces **no** completion event. In **library-owned** mode no +per-frame `BUFFER_DONE` is posted — a transmitted frame returns to the free pool +implicitly and is reused by the next `buffer_get()`. + +--- + +## 5. How To Use the API + +The canonical, **compiling** usage is in [app/sample/new_api/](../../app/sample/new_api/). +A minimal library-owned TX loop looks like this: + +```c +#include + +mtl_video_config_t config; +memset(&config, 0, sizeof(config)); +config.base.direction = MTL_SESSION_TX; +config.base.ownership = MTL_BUFFER_LIBRARY_OWNED; +config.base.num_buffers = 3; +config.base.name = "my_tx"; +config.base.flags = MTL_SESSION_FLAG_BLOCK_GET; /* block in buffer_get */ + +config.tx_port = /* st_tx_port: ports, dip_addr, udp_port, payload_type */; +config.width = 1920; config.height = 1080; config.fps = ST_FPS_P59_94; +config.frame_fmt = ST_FRAME_FMT_YUV422PLANAR10LE; /* app pixel format */ +config.transport_fmt = ST20_FMT_YUV_422_10BIT; /* wire format */ +config.pacing = ST21_PACING_NARROW; + +mtl_session_t* session = NULL; +int ret = mtl_video_session_create(mt_handle, &config, &session); +if (ret < 0) { /* error */ } + +size_t frame_size = mtl_session_get_frame_size(session); +mtl_session_start(session); + +while (running) { + mtl_buffer_t* buf = NULL; + ret = mtl_session_buffer_get(session, &buf, 1000 /* ms */); + if (ret == -EAGAIN) break; /* session stopped */ + if (ret == -ETIMEDOUT) continue; /* no buffer within timeout */ + if (ret < 0) break; /* error */ + + memcpy(buf->data, my_frame, frame_size); + mtl_session_buffer_put(session, buf); +} + +mtl_session_stop(session); +mtl_session_destroy(session); +``` + +RX is symmetric: `buffer_get()` returns a received frame, you read +`buf->data`/`buf->status`/`buf->timestamp`, then `buffer_put()` returns it to the +library. + +### Return-value conventions + +| Value | Meaning | +|-------|---------| +| `0` | Success | +| `-ETIMEDOUT` | Timed out with no buffer/event (normal — retry) | +| `-EAGAIN` | Session stopped — exit the loop cleanly | +| `-ENOSPC` | User-owned ring full (TX `buffer_post`) | +| `-EINVAL` / `-ENOMEM` / `-EIO` | Bad arg / OOM / I/O error | + +> **Auto-start note.** The current implementation **auto-starts** the data path +> at create time; `mtl_session_start()` clears the *stopped* flag and is the +> documented way to resume after `stop()`. An option to *initialize without +> starting* is planned (see [§8](#todo)) but not yet present, so do not rely on +> "no frames flow until start()" today. + +--- + +## 6. User-Owned (Zero-Copy) Path + +For zero-copy TX/RX the app owns the memory and hands page pointers to the +library: + +```c +config.base.ownership = MTL_BUFFER_USER_OWNED; + +/* 1. register a DMA-capable region (mmap'd file, hugepage, etc.) */ +mtl_dma_mem_t* dma = NULL; +mtl_session_mem_register(session, region, region_size, &dma); + +/* 2. post buffers that point into the registered region (no copy) */ +mtl_session_buffer_post(session, frame_ptr, frame_size, my_ctx); + +/* 3. reclaim on completion */ +mtl_event_t ev; +if (mtl_session_event_poll(session, &ev, 100) == 0 && + ev.type == MTL_EVENT_BUFFER_DONE) { + my_ctx_t* done = ev.ctx; /* the user_ctx passed to buffer_post */ + /* buffer is free to reuse */ +} + +/* 4. cleanup */ +mtl_session_mem_unregister(session, dma); +``` + +### TX data-plane behavior (post-Phase 2) + +- The posted buffer is bound to a free transmit slot and converted (if + `frame_fmt != transport_fmt`) **on the calling app thread**, inside + `buffer_post()`. The pacing tasklet only picks an already-READY slot — it does + no conversion, allocation, or ring traffic. +- The user-buffer ring is **value-backed** (no per-post malloc). +- If no transmit slot is free, the buffer is deferred in submission order + (strict FIFO at the bind stage) and bound by a later `buffer_post()`. There is + currently **no `buffer_flush()`**, so an app must keep posting (or stop the + session) to drain a backlog — see [§8](#todo). + +A complete, runnable example is +[app/sample/new_api/tx_video_user_owned_sample.c](../../app/sample/new_api/tx_video_user_owned_sample.c), +which mmaps a source file and DMA-transmits its pages with no `memcpy`. + +--- + +## 7. Event Queue + +The event queue was redesigned in Phase 2.1 to respect the producer/consumer +split (the tasklet producer must never block or allocate; the app-thread +consumer may block): + +- **Storage:** a **value-backed** `rte_ring` (`rte_ring_create_elem`, + single-consumer dequeue). Events are copied into the ring by value — no + per-event heap allocation. +- **Producer** (transport callbacks, tasklet context): `event_post` enqueues by + value and never blocks. On a full ring it **drops** the event and bumps an + atomic `events_dropped` counter, returning `-ENOSPC`. +- **Consumer** (`mtl_session_event_poll`, app thread): blocks on the `eventfd` + via `poll()` honoring `timeout_ms`. `timeout == 0` is non-blocking + (`-ETIMEDOUT` when empty); a stopped session returns `-EAGAIN`. +- **Wakeup:** a level-triggered `eventfd`. `mtl_session_stop()` signals it so a + consumer already blocked in `poll()` wakes promptly and returns `-EAGAIN` + instead of waiting out the timeout. +- **Epoll integration:** `mtl_session_get_event_fd()` returns the `eventfd` for + apps that drive their own `epoll`/`poll` loop. + +Event types actually posted by the library today: `BUFFER_READY` (RX), +`BUFFER_DONE` (user-owned TX), `VSYNC`, `FRAME_LATE` (TX), `FORMAT_DETECTED` +(RX). `MTL_EVENT_ERROR`, `MTL_EVENT_TIMING_REPORT`, and `MTL_EVENT_SLICE_READY` +are defined in the header but **not yet emitted**. + +> `events_dropped` is currently internal only; it is not yet surfaced through the +> public stats API (planned — see [§8](#todo)). + +--- + +## 8. Not Implemented / TODO + +### Not implemented (returns `-ENOTSUP` or absent) + +| Item | Where | Notes | +|------|-------|-------| +| Audio (ST30) sessions | `mtl_audio_session_create` | `-ENOTSUP`; no `mt_session_audio_*` | +| Ancillary (ST40) sessions | `mtl_ancillary_session_create` | `-ENOTSUP` | +| Audio/anc vtables | `mt_session.h` | declared `extern`, **never defined** — calling create for these media types must stay gated | +| ST22 compressed video | video config `compressed` path | encoder/decoder plugin wiring not present | +| Slice mode (`slice_ready` / `slice_query`) | video TX/RX | `-ENOTSUP` | +| `buffer_flush` | vtable slot | `NULL`; user-owned backlog drains only on the next `buffer_post` | +| `get_plugin_info` / `get_queue_meta` | vtable slots | `NULL` | +| `set_block_timeout` | `mt_session.c` | not reconciled with per-call `timeout_ms` | + +### Planned next (Phase 4 — design decisions already taken) + +- **Optional no-auto-start.** Keep auto-start as the default but add a typed + config field to initialize *without* starting; then `start()` becomes a real + gate on the data path. +- **Drop the `MTL_SESSION_FLAG_*` bitmask** in favor of typed config fields. +- **Remove dead `mtl_event_t` union members** (`error{int code}`, + `buffer{mtl_buffer* buf}`) — never written by the library. +- **Add `buffer_flush`** for user-owned backlog drain at stream end. +- **Surface `events_dropped`** through the stats API. + +See [NEW_API_PHASE2_HANDOFF.md](../../NEW_API_PHASE2_HANDOFF.md) §3 for the full +remaining plan, and [List-of-changes.md](List-of-changes.md) for the original +design intent (note: some of that intent — e.g. "no auto-start" — is not yet the +implemented behavior and is annotated there). + +--- + +## 9. Samples + +Real, compiling, meson-wired samples live in +[app/sample/new_api/](../../app/sample/new_api/). They are built as part of the +normal app build (executable names in parentheses): + +| Sample | Pattern | +|--------|---------| +| [tx_video_lib_owned_sample.c](../../app/sample/new_api/tx_video_lib_owned_sample.c) | TX, library-owned `buffer_get`/`put` loop (`NewApiTxVideoLibOwned`) | +| [rx_video_lib_owned_sample.c](../../app/sample/new_api/rx_video_lib_owned_sample.c) | RX, library-owned `buffer_get`/`put` loop (`NewApiRxVideoLibOwned`) | +| [tx_video_user_owned_sample.c](../../app/sample/new_api/tx_video_user_owned_sample.c) | TX, user-owned zero-copy: `mem_register` + `buffer_post` + completion events (`NewApiTxVideoUserOwned`) | +| [rx_video_user_owned_sample.c](../../app/sample/new_api/rx_video_user_owned_sample.c) | RX, user-owned zero-copy (`NewApiRxVideoUserOwned`) | + +The conceptual walkthroughs and diagrams in +[samples/README.md](samples/README.md) and [samples/diagrams.md](samples/diagrams.md) +illustrate the same patterns. (Slice-mode and ST22 diagrams there describe the +*intended* API for features that are not yet implemented — see [§8](#todo).) + +For graceful shutdown, see [GRACEFUL_SHUTDOWN.md](GRACEFUL_SHUTDOWN.md). + +--- + +## 10. Build & Test + +```bash +# Library (canonical) +./build.sh + +# Unit tests (no NIC required) +meson configure build_unit -Denable_unit_tests=true +ninja -C build_unit +./build_unit/tests/unit/UnitTest --gtest_filter='St20NewApi*' + +# Samples are produced by the normal app build as +# NewApiTxVideoLibOwned / NewApiRxVideoLibOwned / +# NewApiTxVideoUserOwned / NewApiRxVideoUserOwned +``` + +The new-API sources compile into `libmtl.so`; no separate library is produced. + +> Host-specific build quirks (which `ninja` to use, the `clang-format-14` +> whole-file churn trap on these headers, stale-deps false crashes) are recorded +> in the repo's `.github` build notes. diff --git a/doc/new_API/GRACEFUL_SHUTDOWN.md b/doc/new_API/GRACEFUL_SHUTDOWN.md new file mode 100644 index 000000000..f865e84c8 --- /dev/null +++ b/doc/new_API/GRACEFUL_SHUTDOWN.md @@ -0,0 +1,319 @@ +# Shutting Down MTL Sessions + +> **TL;DR** — `mtl_session_destroy()` is the single, safe teardown call. It is +> thread-safe, idempotent against concurrent/double calls, and it **wakes and +> drains** any of your threads blocked in `buffer_get()` / `event_poll()` before +> freeing anything. You do **not** need to call `stop()` first, and you do +> **not** need to `join()` first for safety. `stop()` is an *optional* tool for +> pause/resume and for quiescing from a signal handler — see +> [When to use stop()](#when-stop). + +--- + +## The Lifecycle Guard + +Safety is enforced **inside the library**, not by application discipline. Every +public `mtl_session_*()` entry point is protected by a lock-free handle guard — +the same `mt_handle_guard.h` mechanism the low-level `st20_*` API already uses — +plus a validated handle that is **always safe to dereference**. Together these +make it **impossible to segfault** from any sequence of API calls, including +misuse. + +The guard carries, per session: + +- `_Atomic uint32_t lc_refcnt` — number of public-API callers currently inside + the session. +- `_Atomic uint32_t lc_destroying` — set once, via a single-winner CAS, when + teardown begins. +- a generation tag, so a handle that names a since-destroyed (or never-valid) + session is **detected and rejected**, never dereferenced as live memory. + +### How a call is protected + +``` +int mtl_session_(handle, ...) { + s = resolve(handle); // validate WITHOUT trusting caller memory + if (!s) return -EINVAL; // bad / stale / destroyed handle → error + if (acquire(&s->lc_refcnt, + &s->lc_destroying) < 0) // tearing down → refuse cleanly + return -EAGAIN; + ... do the work ... + release(&s->lc_refcnt); // every exit path releases + return ret; +} +``` + +`resolve()` validates the handle against the library's own session table; it +**never reads through a raw application pointer**, so even a garbage or +already-freed handle yields `-EINVAL` instead of a fault. + +### How destroy is protected + +``` +int mtl_session_destroy(handle) { + s = resolve(handle); + if (!s) return -EINVAL; // double-destroy / stale → clean error + if (begin_destroy(&s->lc_destroying)) // CAS 0 → 1; loser: + return -EBUSY; // another thread already won + signal_eventfd(s); // wake any blocked buffer_get/event_poll + drain(&s->lc_refcnt); // spin until refcnt == 0 + ... inner teardown (st20_*_free) ... // tasklet already detached under lock + retire(handle); // bump generation; slot stays mapped +} +``` + +This closes the classic use-after-free race by construction: + +``` +Thread A (worker) Thread B (shutdown) +───────────────── ─────────────────── +buffer_get(s, &b, 1000) + guard: refcnt 0 → 1 + ... blocked in poll() ... + mtl_session_destroy(s) + CAS destroying 0 → 1 (winner) + signal eventfd ───────► poll() returns + guard: refcnt 1 → 0 ◄────────── drain: wait refcnt → 0 + returns -EAGAIN inner teardown + retire +``` + +### Safety guarantees + +Because of the guard and the validated handle, **no API call sequence can crash**: + +- **Destroy while a worker is blocked** → the worker is woken and drained first. +- **Double `destroy()` / concurrent `destroy()`** → the CAS picks one winner; the + losers return `-EBUSY` and free nothing. +- **A call that races teardown** → returns `-EAGAIN` instead of touching memory + mid-free. +- **A call on an already-destroyed handle** → the generation tag no longer + matches, so the call returns `-EINVAL`. The memory is **not** dereferenced as a + live session. +- **A call with a garbage / never-valid handle** → rejected by validation, + returns `-EINVAL`. + +> In short: every public function either does the work or returns a negative +> errno. None of them can fault on a bad, stale, or concurrently-destroyed +> handle. + +--- + +## The Normal Shutdown Pattern + +`destroy()` is sufficient on its own. The only thing you owe your *own* worker +threads is a way to leave their loops — they must treat `-EAGAIN` as "exit": + +```c +/* Your worker thread */ +void* my_worker(void* arg) { + my_ctx_t* ctx = arg; + mtl_buffer_t* buf; + + while (!ctx->stop) { + int ret = mtl_session_buffer_get(ctx->session, &buf, 1000); + if (ret == -EAGAIN) break; /* session tearing down — exit */ + if (ret == -ETIMEDOUT) continue; /* no buffer in time — retry */ + if (ret < 0) break; /* error */ + + process(buf); + mtl_session_buffer_put(ctx->session, buf); + } + return NULL; +} + +/* Shutdown */ +void shutdown(my_app_t* app) { + app->ctx.stop = true; /* ask your worker to exit */ + pthread_join(app->worker_thread, NULL);/* optional: wait for it */ + mtl_session_destroy(app->session); /* safe: wakes + drains */ +} +``` + +Notes: + +- The `pthread_join()` is **optional and for your benefit**, not for safety. If + your worker is still inside `buffer_get()` when you call `destroy()`, the guard + wakes and drains it — no crash. Joining first simply gives you deterministic + control over when your own thread stops touching application state. +- You may call `destroy()` from any thread. + +### Multiple sessions + +Just destroy them. Order does not matter for safety. + +```c +for (int i = 0; i < app->n; i++) mtl_session_destroy(app->sessions[i]); +mtl_uninit(app->mt); +``` + +--- + +## When to use stop() + +`stop()` is **not** required for a safe shutdown. It exists for two specific +situations where `destroy()` is the wrong tool: + +### 1. Signal handlers (SIGINT / SIGTERM) + +You **cannot** call `mtl_session_destroy()` from a signal handler — it tears down +DPDK resources and is not async-signal-safe. `mtl_session_stop()`, by contrast, +only sets a flag and performs a single non-blocking 8-byte `write()` to the +eventfd, both of which *are* async-signal-safe. So the idiomatic Ctrl+C pattern +is: **quiesce in the handler with `stop()`, do the heavy `destroy()` back in +normal context.** + +```c +static volatile sig_atomic_t g_running = 1; +static app_t* g_app = NULL; /* set before installing the handler */ + +static void on_sigint(int sig) { + (void)sig; + g_running = 0; + /* stop() is async-signal-safe: wakes blocked workers so they exit promptly */ + if (g_app) + for (int i = 0; i < g_app->n; i++) mtl_session_stop(g_app->sessions[i]); +} + +int main(...) { + signal(SIGINT, on_sigint); + /* ... create + start sessions, spawn workers ... */ + + while (g_running) pause(); /* or your main loop */ + + /* Back in normal context — safe to tear down */ + for (int i = 0; i < g_app->n; i++) { + pthread_join(g_app->workers[i], NULL); + mtl_session_destroy(g_app->sessions[i]); + } +} +``` + +Calling `stop()` here makes the blocked `buffer_get(timeout=1000)` in each worker +return `-EAGAIN` immediately instead of waiting out its timeout, so shutdown feels +instant. (`destroy()` would also wake them — but you can't call it from the +handler, which is the whole point.) + +### 2. Pause / resume + +`stop()` is reversible. If you want to suspend a session and later resume it +*without* tearing it down and recreating it, use `stop()` / `start()`: + +```c +mtl_session_stop(session); /* buffer_get/event_poll return -EAGAIN */ +/* ... session idle, still fully allocated ... */ +mtl_session_start(session); /* resume normal operation */ +``` + +If you are not pausing and not handling signals, you never need `stop()`. + +--- + +## What stop() and destroy() do **not** do: flush + +Neither `stop()` nor `destroy()` guarantees that frames you already submitted are +fully transmitted/received before the session closes. `destroy()` tears down +promptly and may drop in-flight or queued frames; `stop()` only sets the stop +flag. + +If you need a "transmit everything I submitted, then close" guarantee (e.g. to +avoid truncating a recording or a stream), that is a distinct **`buffer_flush()`** +operation. It is **not yet implemented** — see +[CURRENT_STATE.md §8](CURRENT_STATE.md#todo). Until it exists, applications +that care about completeness must drain at the application level (stop producing, +wait until your outstanding `BUFFER_DONE` count matches your posted count) before +calling `destroy()`. + +--- + +## API Reference + +### mtl_session_destroy() + +```c +int mtl_session_destroy(mtl_session_t* session); +``` + +The safe teardown primitive. + +- Thread-safe; may be called from any thread. +- Idempotent against races: concurrent/duplicate calls have one winner; others + return `-EBUSY` and free nothing. +- Wakes any caller blocked in `buffer_get()` / `event_poll()` and drains all + in-flight public-API callers before freeing. +- **You do not need to call `stop()` or `join()` first for safety.** +- After it returns, the handle is retired: any later call on it returns + `-EINVAL` — it will **not** crash. + +### mtl_session_stop() + +```c +int mtl_session_stop(mtl_session_t* session); +``` + +Optional. Set the session to the stopped state and wake blocked callers. + +- Async-signal-safe — callable from a SIGINT/SIGTERM handler. +- After this, `buffer_get()` / `event_poll()` return `-EAGAIN` immediately. +- Reversible: `mtl_session_start()` resumes normal operation. +- Does **not** free anything and does **not** flush in-flight frames. + +### mtl_session_start() + +```c +int mtl_session_start(mtl_session_t* session); +``` + +Clear the stopped state and resume. Use with `stop()` for pause/resume. + +### mtl_session_is_stopped() + +```c +bool mtl_session_is_stopped(mtl_session_t* session); +``` + +Returns whether the session is currently in the stopped state. + +--- + +## Common Questions + +**Do I have to call `stop()` before `destroy()`?** +No. `destroy()` is self-sufficient. Call `stop()` only for signal-handler +quiesce or pause/resume. + +**Is it safe to `destroy()` while my worker is blocked in `buffer_get()`?** +Yes. The worker is woken (returns `-EAGAIN`) and drained before any memory is +freed. + +**Is it safe to call `destroy()` twice, or from two threads?** +Yes. One call wins; the others return `-EBUSY`. Nothing is freed twice. + +**What happens if I use the handle after `destroy()` returns?** +The call returns `-EINVAL`. The handle is validated against the library's session +table (with a generation tag), so a retired handle is detected and rejected — it +is never dereferenced as live memory and **cannot crash**. + +**How do I make sure all my frames went out before closing?** +Drain at the application level today (match `BUFFER_DONE` count to posted count), +then `destroy()`. A library `buffer_flush()` is planned but not yet available. + +--- + +## Design Rationale + +The lifecycle guard puts the *safety* guarantee in the library, where it belongs, +rather than relying on the application to perform a precise `stop → join → +destroy` dance. This follows the lead of the low-level `st20_*` API, which +already guards its handles with the same mechanism. + +The guarantee is deliberately stronger than "don't crash if you follow the +rules": a **validated, generation-tagged handle** means the library never trusts +a raw application pointer, so *no* call sequence — double destroy, use after +destroy, garbage handle, or a race against teardown — can fault. The worst an +application can do is get a negative errno back. + +`stop()` is deliberately kept as a small, single-purpose, async-signal-safe +primitive — it sets a flag and wakes waiters, nothing more. It is *not* overloaded +into the destroy path, so there is no "mixed-mode" teardown function with flags. +Applications that need pause/resume or signal-driven quiesce get exactly that +primitive; everyone else just calls `destroy()`. diff --git a/doc/new_API/List-of-changes.md b/doc/new_API/List-of-changes.md new file mode 100644 index 000000000..604d0cd14 --- /dev/null +++ b/doc/new_API/List-of-changes.md @@ -0,0 +1,572 @@ +# Unified Polymorphic Session API - Design Document + +> **Status:** this is the original **design intent**. Most of it is implemented +> for ST20 video (see [CURRENT_STATE.md](CURRENT_STATE.md) for the authoritative +> feature matrix), but a few design statements below have **not** been realized +> yet and are annotated inline (e.g. "no auto-start"). Treat +> [CURRENT_STATE.md](CURRENT_STATE.md) as the source of truth for what the code +> does today. + +## Overview + +This document describes the new unified session API design. For related documentation: +- **[../../include/mtl_session_api.h](../../include/mtl_session_api.h)** - Public API header (shipped) +- **[CURRENT_STATE.md](CURRENT_STATE.md)** - What is actually implemented today +- **[GRACEFUL_SHUTDOWN.md](GRACEFUL_SHUTDOWN.md)** - Thread-safe shutdown patterns (CRITICAL!) +- **[samples/](samples/)** - Usage examples (point to [app/sample/new_api/](../../app/sample/new_api/)) + +## Problem Statement + +Current MTL has separate APIs for each media type, causing code duplication: + +```c +// Current API - repetitive functions for each media type +st20p_tx_get_frame() / st20p_tx_put_frame() +st22p_tx_get_frame() / st22p_tx_put_frame() +st30p_tx_get_frame() / st30p_tx_put_frame() +st40p_tx_get_frame() / st40p_tx_put_frame() +``` + +This leads to: +- Duplicated code in library implementation +- Duplicated code in applications +- Harder to learn API (many similar but different functions) +- Harder to test (separate tests for each type) + +--- + +## The Polymorphic Solution + +### Key Design Principle + +**ONE session type for ALL media. Same operations everywhere. Only creation differs.** + +```c +// NEW API - one set of functions for everything +mtl_session_buffer_get(session, &buffer, timeout); // Works for video, audio, ancillary! +mtl_session_buffer_put(session, buffer); // Same function, any media type +``` + +### API Structure + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ TYPE-SPECIFIC CREATION │ +│ mtl_video_session_create() - Creates video session │ +│ mtl_audio_session_create() - Creates audio session │ +│ mtl_ancillary_session_create() - Creates ancillary session │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ UNIFIED mtl_session_t HANDLE │ +│ (opaque - works for any media type) │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ POLYMORPHIC OPERATIONS │ +│ mtl_session_start(session) │ +│ mtl_session_stop(session) │ +│ mtl_session_buffer_get(session, &buf, timeout) │ +│ mtl_session_buffer_put(session, buf) │ +│ mtl_session_event_poll(session, &event, timeout) │ +│ mtl_session_destroy(session) │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Core Changes from Current API + +### 1. Removal of "frame" and "rtp" modes + +- Pipeline mode becomes the only API +- Simplifies API surface significantly +- Migration path: pipeline already wraps frame/rtp internally + +### 2. Polymorphic Session Classes + +- Unified `mtl_session_t` for all media types +- VTable-based dispatch internally (no performance penalty) +- Type-safe configuration structs per media type + +### 3. Event Polling Replaces Callbacks + +| Old (Callbacks) | New (Polling) | +|----------------|---------------| +| `notify_frame_available` callback | `mtl_session_event_poll()` | +| Called from library thread | Application controls timing | +| Hard to integrate with event loops | Easy epoll/select integration | + +Optional callbacks still available for low-latency use cases. + +### 4. Clear Buffer Ownership Model + +**Library-owned** (simple): +```c +mtl_session_buffer_get(session, &buf, timeout); +// Use buf->data +mtl_session_buffer_put(session, buf); +``` + +**User-owned** (zero-copy): +```c +mtl_session_mem_register(session, my_memory, size, &handle); +mtl_session_buffer_post(session, my_data, size, my_ctx); +// Wait for completion via event_poll +``` + +### 5. Explicit Session Lifecycle + +``` +CREATED ──start()──► RUNNING ──stop()──► STOPPED ──destroy()──► DESTROYED +``` + +- No auto-start on create (predictable behavior) + +> **⚠ Not yet true in the current build.** The implementation **auto-starts** the +> data path at create time; `start()` clears the *stopped* flag (it is the +> resume-after-`stop()` operation). An opt-in "initialize without starting" mode +> is planned — see [CURRENT_STATE.md §8](CURRENT_STATE.md#todo). +- `stop()` signals stop, blocking calls return `-EAGAIN` +- Application controls shutdown timing (see [GRACEFUL_SHUTDOWN.md](GRACEFUL_SHUTDOWN.md)) + +--- + +## Unified Buffer Structure + +```c +typedef struct mtl_buffer { + /* Common fields - sufficient for 90% of use cases */ + void* data; // Buffer data pointer + size_t size; // Total buffer size + size_t data_size; // Valid data size + uint64_t timestamp; // Presentation timestamp + + /* Type-specific fields (optional) */ + union { + struct { uint32_t width, height; ... } video; + struct { uint32_t samples, channels; ... } audio; + struct { uint16_t line_number; ... } ancillary; + }; +} mtl_buffer_t; +``` + +Same buffer structure for all media types. Generic code uses common fields; type-specific code can access the union. + +--- + +## Internal Implementation + +**IMPORTANT ARCHITECTURE CHANGE**: The new API does NOT wrap the pipeline API. +It REPLACES the pipeline API and directly uses the low-level session structures. + +``` +OLD ARCHITECTURE: +┌─────────────────────────┐ +│ Application │ +│ (uses st20p_* API) │ +└──────────┬──────────────┘ + ↓ +┌─────────────────────────┐ +│ Pipeline Layer │ ← WILL BE REMOVED +│ (st_pipeline_api.h) │ +│ st20p_tx_*, st_frame │ +└──────────┬──────────────┘ + ↓ +┌─────────────────────────┐ +│ Low-Level Session │ ← STAYS +│ (st_header.h) │ +│ st_tx_video_session_impl, st_frame_trans +└─────────────────────────┘ + +NEW ARCHITECTURE: +┌─────────────────────────┐ +│ Application │ +│ (uses mtl_session_* API) +└──────────┬──────────────┘ + ↓ +┌─────────────────────────┐ +│ NEW Unified API │ ← THIS IS THE NEW LAYER +│ (mtl_session_api.h) │ +│ mtl_session_t, mtl_buffer_t +└──────────┬──────────────┘ + ↓ +┌─────────────────────────┐ +│ Low-Level Session │ ← STAYS (same as before) +│ (st_header.h) │ +│ st_tx_video_session_impl, st_frame_trans +└─────────────────────────┘ +``` + +The internal session structure directly points to the low-level impl: + +```c +struct mtl_session_impl { + const mtl_session_vtable_t* vt; // Dispatch table + + union { + /* Direct pointers to low-level session impl */ + struct st_tx_video_session_impl* video_tx; + struct st_rx_video_session_impl* video_rx; + struct st_tx_audio_session_impl* audio_tx; + struct st_rx_audio_session_impl* audio_rx; + } inner; +}; +``` + +**Zero overhead**: One indirect call through vtable, same as current function pointer callbacks. + +--- + +## Benefits Summary + +| Aspect | Current API | New Unified API | +|--------|-------------|-----------------| +| Functions to learn | 40+ (10 per media type) | ~15 total | +| App code duplication | High (separate handlers) | Low (generic handlers) | +| Library code duplication | High | Medium (shared via vtable) | +| Testing | Separate tests per type | One test framework | +| Adding new media type | Copy-paste all functions | Add vtable + create fn | + +--- + +## Files in this Directory + +### API Design +| File | Description | +|------|-------------| +| [List-of-changes.md](List-of-changes.md) | This document - API changes summary | +| [../../include/mtl_session_api.h](../../include/mtl_session_api.h) | **Public API** (shipped in `include/`) | +| [../../lib/src/new_api/mt_session.h](../../lib/src/new_api/mt_session.h) | **Internal** types/vtable (in `lib/src/new_api/`) | +| [CURRENT_STATE.md](CURRENT_STATE.md) | Authoritative implemented-feature matrix | +| [GRACEFUL_SHUTDOWN.md](GRACEFUL_SHUTDOWN.md) | Shutdown design rationale | + +### Sample Code +The real, compiling samples live in [app/sample/new_api/](../../app/sample/new_api/) +and cover the implemented ST20 video paths (library- and user-owned, TX and RX). + +| Sample | Pattern | +|--------|----------| +| [tx_video_lib_owned_sample.c](../../app/sample/new_api/tx_video_lib_owned_sample.c) | TX, library-owned (`NewApiTxVideoLibOwned`) | +| [rx_video_lib_owned_sample.c](../../app/sample/new_api/rx_video_lib_owned_sample.c) | RX, library-owned (`NewApiRxVideoLibOwned`) | +| [tx_video_user_owned_sample.c](../../app/sample/new_api/tx_video_user_owned_sample.c) | TX, zero-copy (`NewApiTxVideoUserOwned`) | +| [rx_video_user_owned_sample.c](../../app/sample/new_api/rx_video_user_owned_sample.c) | RX, zero-copy (`NewApiRxVideoUserOwned`) | + +> Slice-mode and ST22 samples are not provided (those features are `-ENOTSUP`). + +See [samples/README.md](samples/README.md) for detailed usage patterns. + +--- + +## API Wrapping Feasibility - Video Session (ST20/ST22) + +The new unified API **replaces** the pipeline API and **directly wraps** the low-level +session structures from `st_header.h`. This section documents what from `st20_api.h` +can be wrapped and why. + +### Low-Level Structures We Wrap + +| Low-Level (stays) | New Unified API | +|-------------------|-----------------| +| `st_tx_video_session_impl` | `mtl_session_impl.inner.video_tx` | +| `st_rx_video_session_impl` | `mtl_session_impl.inner.video_rx` | +| `st_frame_trans` | `mtl_buffer_impl.frame_trans` | +| `st20_tx_ops` callbacks | Converted to polling events | +| `st20_rx_ops` callbacks | Converted to polling events | + +### TX Operations Mapping (st20_tx_* → mtl_session_*) + +| Current `st20_api.h` Function | New Unified API | Notes | +|------------------------------|-----------------|-------| +| `st20_tx_create()` | `mtl_video_session_create()` | Config translation | +| `st20_tx_free()` | `mtl_session_destroy()` | Polymorphic | +| `st20_tx_get_framebuffer()` | `mtl_session_buffer_get()` | Get by index → get next free | +| `st20_tx_update_destination()` | `mtl_session_update_destination()` | Direct wrap | +| `st20_tx_set_ext_frame()` | Via `query_ext_frame` callback | Unified ext frame model | +| `st20_tx_get_framebuffer_size()` | Via `mtl_buffer_t.size` | Part of buffer struct | +| `st20_tx_get_framebuffer_count()` | Via config | Set at creation | +| `st20_tx_get_pacing_params()` | Could add if needed | Low priority | +| `st20_tx_get_session_stats()` | `mtl_session_stats_get()` | Polymorphic | +| `st20_tx_reset_session_stats()` | `mtl_session_stats_reset()` | Polymorphic | +| `st20_tx_get_sch_idx()` | Internal only | Not exposed | +| `st20_tx_get_mbuf()` / `st20_tx_put_mbuf()` | **NOT WRAPPED** | RTP-level only | + +### RX Operations Mapping (st20_rx_* → mtl_session_*) + +| Current `st20_api.h` Function | New Unified API | Notes | +|------------------------------|-----------------|-------| +| `st20_rx_create()` | `mtl_video_session_create()` | Config translation | +| `st20_rx_free()` | `mtl_session_destroy()` | Polymorphic | +| `st20_rx_put_framebuff()` | `mtl_session_buffer_put()` | Polymorphic | +| `st20_rx_update_source()` | `mtl_session_update_source()` | Direct wrap | +| `st20_rx_get_queue_meta()` | `mtl_session_get_queue_meta()` | For DATA_PATH_ONLY | +| `st20_rx_timing_parser_critical()` | Config + events | Via TIMING_REPORT event | +| `st20_rx_get_session_stats()` | `mtl_session_stats_get()` | Polymorphic | +| `st20_rx_reset_session_stats()` | `mtl_session_stats_reset()` | Polymorphic | +| `st20_rx_pcapng_dump()` | Could add if needed | Debug feature | +| `st20_rx_get_mbuf()` / `st20_rx_put_mbuf()` | **NOT WRAPPED** | RTP-level only | + +### Callback → Event Mapping + +| `st20_tx_ops` / `st20_rx_ops` Callback | New Event Type | Notes | +|---------------------------------------|----------------|-------| +| `get_next_frame()` | Internal | Library manages frame selection | +| `notify_frame_done()` | `MTL_EVENT_BUFFER_DONE` | TX completion | +| `notify_frame_ready()` | `MTL_EVENT_BUFFER_READY` | RX frame available | +| `notify_frame_late()` | `MTL_EVENT_FRAME_LATE` | TX missed epoch | +| `notify_event(VSYNC)` | `MTL_EVENT_VSYNC` | Epoch boundary | +| `notify_detected()` | `MTL_EVENT_FORMAT_DETECTED` | Auto-detect result | +| `notify_slice_ready()` | `MTL_EVENT_SLICE_READY` | Slice mode RX | +| `query_frame_lines_ready()` | Via config callback | Slice mode TX | +| `query_ext_frame()` | Via base config callback | User-owned buffers | +| `uframe_pg_callback()` | **NOT SUPPORTED** | Rare use case | +| `notify_rtp_done()` / `notify_rtp_ready()` | **NOT SUPPORTED** | RTP-level only | + +### Flags Mapping + +| `ST20_TX_FLAG_*` / `ST20_RX_FLAG_*` | New `mtl_video_config_t` | Notes | +|------------------------------------|--------------------------|-------| +| `ST20_TX_FLAG_EXT_FRAME` | `ownership = USER_OWNED` | Unified model | +| `ST20_TX_FLAG_USER_PACING` | Via `flags` | Preserved | +| `ST20_TX_FLAG_USER_TIMESTAMP` | Via `flags` | Preserved | +| `ST20_TX_FLAG_ENABLE_VSYNC` | Via `flags` | Enables VSYNC events | +| `ST20_TX_FLAG_ENABLE_RTCP` | Via `flags` | Preserved | +| `ST20_TX_FLAG_FORCE_NUMA` | Via `socket_id` config | Explicit NUMA | +| `ST20_RX_FLAG_DATA_PATH_ONLY` | Via `flags` | App manages flow rules | +| `ST20_RX_FLAG_RECEIVE_INCOMPLETE_FRAME` | Via `flags` | Required for slice | +| `ST20_RX_FLAG_DMA_OFFLOAD` | Via `flags` | DMA copy offload | +| `ST20_RX_FLAG_AUTO_DETECT` | `enable_auto_detect` config | Explicit | +| `ST20_RX_FLAG_TIMING_PARSER_*` | `enable_timing_parser` config | Explicit | +| `ST20_RX_FLAG_HDR_SPLIT` | Via `flags` | Preserved | + +### What is NOT Wrapped (by design) + +| Excluded Feature | Reason | +|-----------------|--------| +| `ST20_TYPE_RTP_LEVEL` | Too low-level; use frame mode | +| `st20_tx_get_mbuf()` / mbuf API | RTP-level only | +| `uframe_pg_callback` | Very rare use case | +| `st20_pgroup` utilities | Helper functions, not session API | +| Pixel format conversion APIs | Separate utility, not session | + +### What IS Supported (new features) + +| Feature | New API Support | +|---------|----------------| +| **Slice-level (video)** | `mode = MTL_VIDEO_MODE_SLICE`, `mtl_session_slice_*()` | +| **ST22 Plugins** | `plugin_device`, `codec`, `codec_thread_cnt` in config | +| **Plugin info query** | `mtl_session_get_plugin_info()` | +| **Event FD for epoll** | `mtl_session_get_event_fd()` | +| **Explicit start/stop** | `mtl_session_start()` / `mtl_session_stop()` | + +--- + +## Slice-Level Support (Ultra-Low Latency) + +Slice-level mode enables sub-frame latency by processing video line-by-line. + +### Configuration + +```c +mtl_video_config_t config = { + // ... standard fields ... + .mode = MTL_VIDEO_MODE_SLICE, // Enable slice mode + + // TX only: callback for library to query ready lines + .query_lines_ready = my_query_lines_callback, +}; +``` + +### TX Workflow + +```c +mtl_buffer_t* buf; +mtl_session_buffer_get(session, &buf, timeout); + +// Fill lines progressively +for (int line = 0; line < height; line++) { + fill_line(buf->data, line); + // Notify library: lines 0..line are ready + mtl_session_slice_ready(session, buf, line + 1); +} + +mtl_session_buffer_put(session, buf); // Complete frame +``` + +### RX Workflow (Event-Driven) + +```c +mtl_event_t event; +while (mtl_session_event_poll(session, &event, timeout) == 0) { + if (event.type == MTL_EVENT_SLICE_READY) { + // Process newly received lines + process_lines(event.slice.buffer, + last_lines, + event.slice.lines_ready); + last_lines = event.slice.lines_ready; + } + if (event.type == MTL_EVENT_BUFFER_READY) { + // Full frame complete + mtl_session_buffer_put(session, event.buffer.buf); + } +} +``` + +### RX Workflow (Polling) + +```c +mtl_buffer_t* buf; +mtl_session_buffer_get(session, &buf, timeout); + +uint16_t lines = 0; +while (lines < height) { + uint16_t new_lines; + mtl_session_slice_query(session, buf, &new_lines); + if (new_lines > lines) { + process_lines(buf->data, lines, new_lines); + lines = new_lines; + } +} + +mtl_session_buffer_put(session, buf); +``` + +--- + +## Plugin Support (ST22 Codecs) + +ST22 compressed video uses plugins for encoding/decoding (JPEGXS, H264, etc.). + +### Configuration + +```c +mtl_video_config_t config = { + .compressed = true, // Enable ST22 + .codec = ST22_CODEC_JPEGXS, // Codec type + .plugin_device = ST_PLUGIN_DEVICE_AUTO, // Let library choose + .quality = ST22_QUALITY_MODE_SPEED, // Speed vs quality + .codec_thread_cnt = 4, // Parallel encode threads + .codestream_size = ..., // Target bitrate for CBR +}; +``` + +### Querying Plugin Information + +```c +mtl_plugin_info_t info; +if (mtl_session_get_plugin_info(session, &info) == 0) { + printf("Using plugin: %s v%s (%s)\n", + info.name, info.version, + info.device == ST_PLUGIN_DEVICE_GPU ? "GPU" : "CPU"); +} +``` + +### Note on Plugin Registration + +Plugin registration (`st22_encoder_register()`, etc.) remains a separate API at the `mtl_handle` level. Sessions simply specify which plugin device to use; the library selects an appropriate registered plugin. + +### Events Supported + +| Event | Source in Current API | +|-------|----------------------| +| `MTL_EVENT_BUFFER_READY` | `notify_frame_available` | +| `MTL_EVENT_BUFFER_DONE` | User-owned buffer completion | +| `MTL_EVENT_ERROR` | Various error conditions | +| `MTL_EVENT_VSYNC` | `notify_event(ST_EVENT_VSYNC)` | +| `MTL_EVENT_FRAME_LATE` | `notify_frame_late` callback | +| `MTL_EVENT_FORMAT_DETECTED` | `notify_detected` (RX auto-detect) | +| `MTL_EVENT_TIMING_REPORT` | `notify_timing_parser_result` | + +--- + +## Migration from Current API + +```c +// OLD: Separate creation and operations per media type +st20p_tx_handle h = st20p_tx_create(mt, &ops); +struct st_frame* f = st20p_tx_get_frame(h); +st20p_tx_put_frame(h, f); +st20p_tx_free(h); + +// NEW: Unified operations, type-specific creation only +mtl_session_t* s; +mtl_video_session_create(mt, &config, &s); +mtl_session_start(s); + +mtl_buffer_t* b; +while (frame_count < MAX_FRAMES) { + if (mtl_session_buffer_get(s, &b, 1000) == 0) { + // use b->data + mtl_session_buffer_put(s, b); + frame_count++; + } +} + +mtl_session_stop(s); +mtl_session_destroy(s); +``` + +For signal handler shutdown pattern, see [GRACEFUL_SHUTDOWN.md](GRACEFUL_SHUTDOWN.md) +and the user-owned samples in [app/sample/new_api/](../../app/sample/new_api/). + +--- + +## Conclusion: Video Session Wrapping Feasibility + +> The table below is the original **feasibility** assessment ("can this be +> wrapped?"). For what is **actually implemented today**, see +> [CURRENT_STATE.md](CURRENT_STATE.md). In short: frame-based TX/RX (library- and +> user-owned) and statistics are implemented for ST20 video; slice mode, ST22 +> plugins, and audio/ancillary are **not yet implemented** (`-ENOTSUP`). + +### ✅ FULLY FEASIBLE + +The new unified API can fully wrap the video session functionality: + +| Aspect | Status | Notes | +|--------|--------|-------| +| **Frame-based TX** | ✅ Full | `buffer_get/put` maps to `get_next_frame/put_frame` | +| **Frame-based RX** | ✅ Full | `buffer_get/put` maps to `get_frame/put_frame` | +| **Slice mode TX** | ✅ Full | `slice_ready()` maps to `notify_frame_lines_ready` | +| **Slice mode RX** | ✅ Full | `slice_query()` + events map to `query_frame_lines_ready` + `notify_slice_ready` | +| **Library-owned buffers** | ✅ Full | Default mode, internal alloc | +| **App-owned buffers** | ✅ Full | `MTL_SESSION_FLAG_EXT_BUFFER` + callback | +| **All video formats** | ✅ Full | Pass-through `st20_fmt` enum | +| **All timing/pacing** | ✅ Full | Config fields map 1:1 | +| **All events** | ✅ Full | Callbacks → event queue | +| **Statistics** | ✅ Full | `get_stats()` polymorphic | +| **ST22 plugins** | ✅ Full | Codec config fields | + +### Why This Works + +1. **Low-level API is stable**: `st_tx_video_session_impl` and `st_frame_trans` provide all needed functionality +2. **No functionality loss**: Every callback has an event equivalent +3. **Polymorphic dispatch**: VTable allows type-specific implementation without user-visible complexity +4. **Direct access**: Wrap low-level, not pipeline - no double-wrapping overhead + +### What the New API Provides Over Low-Level + +| Feature | Low-Level | New API | +|---------|-----------|---------| +| Polymorphic operations | ❌ Per-type functions | ✅ Same functions for all types | +| Event system | Callbacks | Unified event queue + poll | +| Buffer abstraction | `st_frame_trans*` raw | `mtl_buffer_t` with metadata | +| Error reporting | Scattered | Centralized with events | +| API surface | ~50 functions per type | ~15 unified functions | + +--- + +## Next Steps + +1. Review [../../include/mtl_session_api.h](../../include/mtl_session_api.h) for the complete public API +2. Review [../../lib/src/new_api/mt_session.h](../../lib/src/new_api/mt_session.h) for the implementation structure +3. Review [GRACEFUL_SHUTDOWN.md](GRACEFUL_SHUTDOWN.md) for shutdown patterns +4. Study the real samples in order: + - [tx_video_lib_owned_sample.c](../../app/sample/new_api/tx_video_lib_owned_sample.c) → basic TX + - [rx_video_lib_owned_sample.c](../../app/sample/new_api/rx_video_lib_owned_sample.c) → basic RX + - [tx_video_user_owned_sample.c](../../app/sample/new_api/tx_video_user_owned_sample.c) → zero-copy TX + shutdown +5. Track remaining work via [CURRENT_STATE.md §8](CURRENT_STATE.md#todo) +6. (Done) Video session wrapper implemented as the first type +7. (TODO) Add audio and ancillary session wrappers \ No newline at end of file diff --git a/doc/new_API/samples/README.md b/doc/new_API/samples/README.md new file mode 100644 index 000000000..0ba7bfbc3 --- /dev/null +++ b/doc/new_API/samples/README.md @@ -0,0 +1,167 @@ +# New Unified Session API - Samples + +Sample code demonstrating the new unified polymorphic session API for MTL. + +> **Note:** the real, compiling samples live in +> [app/sample/new_api/](../../../app/sample/new_api/) and are built by the normal +> app build. The snippets in this file are conceptual walkthroughs of the same +> patterns. For an up-to-date feature matrix see +> [../CURRENT_STATE.md](../CURRENT_STATE.md). + +## Sample Organization + +Four real samples cover the implemented ST20 video paths (library-owned and +user-owned / zero-copy), for both TX and RX. Executable names are in +parentheses. + +| Sample | Description | +|--------|-------------| +| [tx_video_lib_owned_sample.c](../../../app/sample/new_api/tx_video_lib_owned_sample.c) | TX, library-owned `buffer_get`/`put` loop (`NewApiTxVideoLibOwned`) | +| [rx_video_lib_owned_sample.c](../../../app/sample/new_api/rx_video_lib_owned_sample.c) | RX, library-owned `buffer_get`/`put` loop (`NewApiRxVideoLibOwned`) | +| [tx_video_user_owned_sample.c](../../../app/sample/new_api/tx_video_user_owned_sample.c) | TX, user-owned zero-copy: `mem_register` + `buffer_post` + completion events (`NewApiTxVideoUserOwned`) | +| [rx_video_user_owned_sample.c](../../../app/sample/new_api/rx_video_user_owned_sample.c) | RX, user-owned zero-copy (`NewApiRxVideoUserOwned`) | + +> Slice mode, ST22 compressed video, and audio/ancillary samples are **not** +> provided because those features are not yet implemented (`-ENOTSUP`). See +> [../CURRENT_STATE.md §8](../CURRENT_STATE.md#todo). + +The user-owned samples each run a producer thread (posting buffers) and an event +thread (reaping `MTL_EVENT_BUFFER_DONE`), demonstrating proper graceful shutdown +via `mtl_session_stop()` before `pthread_join()` — see +[../GRACEFUL_SHUTDOWN.md](../GRACEFUL_SHUTDOWN.md). + +## Buffer Ownership Models + +### Library-Owned (Recommended for most use cases) +```c +mtl_video_config_t config = { + .base = { + .ownership = MTL_BUFFER_LIBRARY_OWNED, + .num_buffers = 3, + }, + ... +}; + +// Simple: get buffer → fill/process → put +mtl_session_buffer_get(session, &buffer, timeout); +// use buffer->data +mtl_session_buffer_put(session, buffer); +``` + +### User-Owned (Zero-Copy) +```c +mtl_video_config_t config = { + .base = { + .ownership = MTL_BUFFER_USER_OWNED, + .num_buffers = NUM_BUFFERS, + }, + ... +}; + +// Register memory region +mtl_session_mem_register(session, my_buffers, size, &dma_handle); + +// Post buffers to library +mtl_session_buffer_post(session, buf->data, buf->size, user_ctx); + +// Poll for completion +mtl_session_event_poll(session, &event, timeout); +if (event.type == MTL_EVENT_BUFFER_DONE) { + // Buffer can be reused +} +``` + +## Session Lifecycle + +``` +create() → start() → [use] → stop() → destroy() +``` + +1. **create()**: Allocate resources, validate config +2. **start()**: Clear the *stopped* state and (re)enable the data path. Note: the + current build auto-starts at create time, so `start()` is primarily the + resume-after-`stop()` operation (see [../CURRENT_STATE.md §8](../CURRENT_STATE.md#todo)). +3. **use**: buffer_get/put loop or event_poll loop +4. **stop()**: Signal stop; blocking calls return -EAGAIN and a blocked + `event_poll()` is woken via the eventfd +5. **destroy()**: Free all resources + +## Common Patterns + +### Simple TX Loop +```c +while (frame_count < MAX_FRAMES) { + err = mtl_session_buffer_get(session, &buffer, 1000); + if (err == -ETIMEDOUT) continue; + if (err < 0) break; + + // Fill buffer + memset(buffer->data, 0x80, buffer->data_size); + + mtl_session_buffer_put(session, buffer); + frame_count++; +} +``` + +### Simple RX Loop +```c +while (frame_count < MAX_FRAMES) { + err = mtl_session_buffer_get(session, &buffer, 1000); + if (err == -ETIMEDOUT) continue; + if (err < 0) break; + + // Process buffer->data + + mtl_session_buffer_put(session, buffer); + frame_count++; +} +``` + +### Event-Driven (User-Owned) +```c +while (running) { + err = mtl_session_event_poll(session, &event, 1000); + if (err == -EAGAIN) break; // session stopped + if (err == -ETIMEDOUT) continue; + if (err < 0) break; + + switch (event.type) { + case MTL_EVENT_BUFFER_READY: + // RX: buffer has received data + break; + case MTL_EVENT_BUFFER_DONE: + // TX user-owned: event.ctx is the user_ctx passed to buffer_post(); + // the buffer is now free to reuse + break; + /* MTL_EVENT_SLICE_READY is defined but not yet emitted (slice mode TODO) */ + } +} +``` + +> The event queue blocks on a level-triggered `eventfd`; `mtl_session_get_event_fd()` +> returns it for `epoll`/`select` integration. A blocked `event_poll()` is woken +> immediately by `mtl_session_stop()`. + +## Error Handling + +| Return Value | Meaning | +|--------------|---------| +| 0 | Success | +| -ETIMEDOUT | Operation timed out (normal, retry) | +| -EAGAIN | Session stopped (exit gracefully) | +| -ENOSPC | User-owned TX ring full (`buffer_post`) | +| -EINVAL | Invalid parameter | +| -ENOMEM | Out of memory | +| -EIO | I/O error | + +## Building + +The four samples above are built by the normal app build (`./build.sh`) and +appear as `NewApiTxVideoLibOwned`, `NewApiRxVideoLibOwned`, +`NewApiTxVideoUserOwned`, and `NewApiRxVideoUserOwned`. + +To build your own application: + +1. `#include ` +2. Link against `libmtl` +3. `mtl_init()` before creating sessions; `mtl_uninit()` after destroying them diff --git a/doc/new_API/samples/diagrams.md b/doc/new_API/samples/diagrams.md new file mode 100644 index 000000000..0e7ff692c --- /dev/null +++ b/doc/new_API/samples/diagrams.md @@ -0,0 +1,347 @@ +# Sequence Diagrams for Unified Session API + +Visual representation of the session API patterns. See [README.md](README.md) for +code samples and [../CURRENT_STATE.md](../CURRENT_STATE.md) for the feature +matrix. The real samples referenced below live in +[app/sample/new_api/](../../../app/sample/new_api/). + +> Diagrams marked **(NOT YET IMPLEMENTED)** describe the *intended* API for +> features that are not present in the current build (slice mode, ST22). They +> are kept as design references. + +## Library-Owned RX Flow + +```mermaid +sequenceDiagram + participant App as Application + participant Session as mtl_session_t + participant Network as Network I/O + + Note over App,Network: Library-Owned Receiver (rx_video_lib_owned_sample.c) + + App->>Session: mtl_video_session_create(mt, &config, &session) + Session->>Session: Allocate frame buffers + Session->>Network: Setup receiver + Session-->>App: Return session handle + + App->>Session: mtl_session_start(session) + Session->>Network: Start receiving + + loop frame_count < MAX_FRAMES + App->>Session: mtl_session_buffer_get(session, &buffer, timeout) + Session->>Network: Wait for frame + Network-->>Session: Frame received + Session-->>App: Return mtl_buffer_t* + + Note over App: Process buffer->data + + App->>Session: mtl_session_buffer_put(session, buffer) + Session->>Network: Return buffer to receive queue + end + + App->>Session: mtl_session_stop(session) + App->>Session: mtl_session_destroy(session) +``` + +## Library-Owned TX Flow + +```mermaid +sequenceDiagram + participant App as Application + participant Session as mtl_session_t + participant Network as Network I/O + + Note over App,Network: Library-Owned Transmitter (tx_video_lib_owned_sample.c) + + App->>Session: mtl_video_session_create(mt, &config, &session) + Session->>Session: Allocate frame buffers + Session->>Network: Setup transmitter + Session-->>App: Return session handle + + App->>Session: mtl_session_start(session) + + loop frame_count < MAX_FRAMES + App->>Session: mtl_session_buffer_get(session, &buffer, timeout) + Session-->>App: Return empty buffer + + Note over App: Fill buffer->data with frame + + App->>Session: mtl_session_buffer_put(session, buffer) + Session->>Network: Transmit frame + Network-->>Session: Transmission complete + Session->>Session: Mark buffer available + end + + App->>Session: mtl_session_stop(session) + App->>Session: mtl_session_destroy(session) +``` + +## User-Owned RX Flow (Zero-Copy) + +```mermaid +sequenceDiagram + participant App as Application + participant Session as mtl_session_t + participant DMA as DMA Manager + participant Network as Network I/O + + Note over App,Network: User-Owned Receiver (rx_video_user_owned_sample.c) + + App->>Session: mtl_video_session_create(mt, &config, &session) + Session-->>App: Return session handle + + App->>App: aligned_alloc() for buffers + App->>Session: mtl_session_mem_register(session, memory, size, &handle) + Session->>DMA: Register for DMA access + Session-->>App: Return DMA handle + + loop Post all buffers + App->>Session: mtl_session_buffer_post(session, buf->data, size, ctx) + Session->>Network: Add buffer to receive queue + end + + App->>Session: mtl_session_start(session) + + loop frame_count < MAX_FRAMES + App->>Session: mtl_session_event_poll(session, &event, timeout) + Network-->>Session: Frame received + Session-->>App: MTL_EVENT_BUFFER_READY + context + + Note over App: Process data at ctx->data + + App->>Session: mtl_session_buffer_post(session, ctx->data, size, ctx) + Session->>Network: Repost buffer + end + + App->>Session: mtl_session_mem_unregister(session, handle) + App->>Session: mtl_session_stop(session) + App->>Session: mtl_session_destroy(session) +``` + +## User-Owned TX Flow (Zero-Copy) + +```mermaid +sequenceDiagram + participant Producer as Producer Thread + participant EventHandler as Event Thread + participant Session as mtl_session_t + participant Network as Network I/O + + Note over Producer,Network: User-Owned Transmitter (tx_video_user_owned_sample.c) + + Note over Session: Setup: create session, register memory, start + + loop frames_sent < MAX_FRAMES + Producer->>Producer: Wait for free buffer + Note over Producer: Fill buffer with frame data + Producer->>Session: mtl_session_buffer_post(session, data, size, ctx) + Session->>Network: Queue for transmission + end + + loop frames_completed < MAX_FRAMES + EventHandler->>Session: mtl_session_event_poll(session, &event, timeout) + Network-->>Session: Transmission complete + Session-->>EventHandler: MTL_EVENT_BUFFER_DONE + context + EventHandler->>EventHandler: Mark buffer as free + end + + Note over Session: Cleanup: stop, unregister, destroy +``` + +## Polymorphic Session - Same API for All Media Types + +```mermaid +flowchart TB + subgraph Creation ["Type-Specific Creation"] + VC[mtl_video_session_create] + AC[mtl_audio_session_create] + NC[mtl_ancillary_session_create] + end + + subgraph Session ["Unified mtl_session_t"] + S[session handle] + end + + subgraph Operations ["Polymorphic Operations
(Same for ALL media types)"] + START[mtl_session_start] + GET[mtl_session_buffer_get] + PUT[mtl_session_buffer_put] + POST[mtl_session_buffer_post] + POLL[mtl_session_event_poll] + STOP[mtl_session_stop] + DESTROY[mtl_session_destroy] + end + + VC --> S + AC --> S + NC --> S + + S --> START + S --> GET + S --> PUT + S --> POST + S --> POLL + S --> STOP + S --> DESTROY +``` + +## Slice-Level TX Flow (Ultra-Low Latency) — NOT YET IMPLEMENTED + +> `slice_ready`/`slice_query` currently return `-ENOTSUP`. Design reference only. + +```mermaid +sequenceDiagram + participant App as Application + participant Session as mtl_session_t + participant Network as Network I/O + + Note over App,Network: Slice Mode TX (design reference) + + App->>Session: mtl_video_session_create(config with mode=SLICE) + Session-->>App: Return session handle + + App->>Session: mtl_session_start(session) + + loop Frame Loop + App->>Session: mtl_session_buffer_get(session, &buffer, timeout) + Session-->>App: Return empty buffer + + loop Line by Line + Note over App: Fill line N with video data + App->>Session: mtl_session_slice_ready(session, buffer, lines=N+1) + Session->>Network: Transmit line N immediately + Note over Network: Wire latency ~1 line time! + end + + App->>Session: mtl_session_buffer_put(session, buffer) + Note over Session: Frame complete + end +``` + +## Slice-Level RX Flow (Ultra-Low Latency) — NOT YET IMPLEMENTED + +> `MTL_EVENT_SLICE_READY` is defined but not emitted. Design reference only. + +```mermaid +sequenceDiagram + participant Network as Network I/O + participant Session as mtl_session_t + participant App as Application + + Note over Network,App: Slice Mode RX (design reference) + + App->>Session: mtl_video_session_create(config with mode=SLICE) + App->>Session: mtl_session_start(session) + + loop Frame Reception + App->>Session: mtl_session_buffer_get(session, &buffer, timeout) + + loop As Packets Arrive + Network-->>Session: RTP packets (lines 0-N) + Session-->>App: MTL_EVENT_SLICE_READY (lines_ready=N+1) + Note over App: Process lines immediately + Note over App: Don't wait for full frame! + end + + Network-->>Session: Final packets + Session-->>App: MTL_EVENT_BUFFER_READY (frame complete) + App->>Session: mtl_session_buffer_put(session, buffer) + end +``` + +## ST22 Plugin Flow — NOT YET IMPLEMENTED + +> ST22 compressed-video plugin wiring is not present. Design reference only. + +```mermaid +sequenceDiagram + participant App as Application + participant Session as mtl_session_t + participant Plugin as JPEGXS Plugin + participant Network as Network I/O + + Note over App,Network: ST22 TX (design reference) + + Note over Plugin: Plugin registered at mtl_init time + + App->>Session: mtl_video_session_create(config with compressed=true) + Session->>Plugin: Create encoder context + Session-->>App: Return session handle + + App->>Session: mtl_session_get_plugin_info(&info) + Session-->>App: Plugin name, version, device type + + App->>Session: mtl_session_start(session) + + loop frame_count < MAX_FRAMES + App->>Session: mtl_session_buffer_get(session, &buffer, timeout) + Session-->>App: Return buffer for RAW video + + Note over App: Fill with uncompressed frame + + App->>Session: mtl_session_buffer_put(session, buffer) + Session->>Plugin: Encode frame (JPEGXS) + Plugin-->>Session: Compressed codestream + Session->>Network: Transmit ST22 packets + end + + App->>Session: mtl_session_stop(session) + App->>Session: mtl_session_destroy(session) +``` + +## Signal Handler Shutdown Flow + +```mermaid +sequenceDiagram + participant Signal as Signal Handler + participant Main as Main Thread + participant Worker as Worker Thread + participant Session as mtl_session_t + + Note over Signal,Session: Graceful Shutdown (see GRACEFUL_SHUTDOWN.md) + + Main->>Session: mtl_video_session_create() + Main->>Session: mtl_session_start() + Main->>Worker: pthread_create(worker_thread) + + loop Worker running + Worker->>Session: mtl_session_buffer_get(timeout=1000) + Session-->>Worker: buffer or -ETIMEDOUT + Note over Worker: Process frame if received + end + + Note over Signal: User presses Ctrl+C + Signal->>Signal: g_running = 0 + Signal->>Session: mtl_session_stop(session) + Note over Session: Set stopping flag + + Worker->>Session: mtl_session_buffer_get() + Session-->>Worker: -EAGAIN (session stopped) + Worker->>Worker: Exit loop cleanly + Worker-->>Main: Thread returns + + Main->>Main: pthread_join(worker) + Main->>Session: mtl_session_destroy(session) + Note over Main: Shutdown complete +``` + +## Session Lifecycle States + +```mermaid +stateDiagram-v2 + [*] --> CREATED: mtl_video_session_create() + CREATED --> RUNNING: mtl_session_start() + RUNNING --> STOPPED: mtl_session_stop() + STOPPED --> [*]: mtl_session_destroy() + + note right of RUNNING + buffer_get/put work normally + event_poll returns events + end note + + note right of STOPPED + buffer_get returns -EAGAIN + event_poll returns -EAGAIN + Safe to destroy + end note +``` \ No newline at end of file