From 96f4f20d1b9f68265d23fa954de5cc680c849bc3 Mon Sep 17 00:00:00 2001 From: Adrian Pistol Date: Fri, 6 Jun 2025 20:54:20 +0200 Subject: [PATCH 1/3] out_ndi: Basic NDI Output. I have a feeling that it's 4:2:2 or 4:2:0, but it works good enough for the Night of Science, I think. --- GNUmakefile | 2 +- src/modules/out_ndi.c | 152 +++++++++++++++++++++++++++++++++++++++ src/modules/out_ndi.incs | 1 + src/modules/out_ndi.libs | 1 + 4 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 src/modules/out_ndi.c create mode 100644 src/modules/out_ndi.incs create mode 100644 src/modules/out_ndi.libs diff --git a/GNUmakefile b/GNUmakefile index d9e36e6..2c494d0 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -24,7 +24,7 @@ FLTMODS_AVAILABLE += flt_debug flt_gamma_correct flt_flip_x flt_flip_y flt_scale FLTMODS_AVAILABLE += flt_rot_90 flt_smapper flt_channel_reorder OUTMODS_AVAILABLE := out_dummy out_sdl2 out_rpi_ws2812b out_udp out_fb out_rpi_hub75 -OUTMODS_AVAILABLE += out_sf75_bi_spidev out_ansi out_pixelflut +OUTMODS_AVAILABLE += out_sf75_bi_spidev out_ansi out_pixelflut out_ndi # List of modules to compile. GFXMODS_DEFAULT := gfx_twinkle gfx_gol gfx_rainbow gfx_math_sinpi gfx_plasma diff --git a/src/modules/out_ndi.c b/src/modules/out_ndi.c new file mode 100644 index 0000000..9c81e2e --- /dev/null +++ b/src/modules/out_ndi.c @@ -0,0 +1,152 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include // NDI SDK headers + +// Matrix size +#ifndef MATRIX_X +#error Define MATRIX_X as the matrix's X size. +#endif + +#ifndef MATRIX_Y +#error Define MATRIX_Y as the matrix's Y size. +#endif + +#define FPS 60 + +static RGB *primary_buffer; +static RGB *front_buffer; +static RGB *back_buffer; +static atomic_bool keep_running; +static atomic_int front_or_back; + +static NDIlib_send_instance_t ndi_send = NULL; +static oscore_task ndi_task; + +static void* send_task(void *arg); + +int init(void) { + assert(sizeof(RGB) == 4); + + // Allocate memory for the buffers + primary_buffer = (RGB*)malloc(MATRIX_X * MATRIX_Y * sizeof(RGB)); + front_buffer = (RGB*)malloc(MATRIX_X * MATRIX_Y * sizeof(RGB)); + back_buffer = (RGB*)malloc(MATRIX_X * MATRIX_Y * sizeof(RGB)); + + atomic_init(&keep_running, true); + atomic_init(&front_or_back, true); + + // Start the NDI sending task + ndi_task = oscore_task_create("NDI Sender", send_task, NULL); + return 0; +} + +int getx(int _modno) { + return MATRIX_X; +} + +int gety(int _modno) { + return MATRIX_Y; +} + +int set(int _modno, int x, int y, RGB color) { + primary_buffer[(y * MATRIX_X + x)] = color; + return 0; +} + +RGB get(int _modno, int x, int y) { + return primary_buffer[(y * MATRIX_X + x)]; +} + +int clear(int _modno) { + // Clear the primary buffer + memset(primary_buffer, 0, MATRIX_X * MATRIX_Y * sizeof(RGB)); + return 0; +} + +int render(void) { + // Get current buffer + int current_fob = atomic_load(&front_or_back); + RGB *current_buffer = current_fob ? front_buffer : back_buffer; + + // Copy primary buffer to current buffer + memcpy(current_buffer, primary_buffer, MATRIX_X * MATRIX_Y * sizeof(RGB)); + + // Flip buffers + atomic_store(&front_or_back, !current_fob); + + return 0; +} + +oscore_time wait_until(int _modno, oscore_time desired_usec) { +#ifdef CIMODE + return desired_usec; +#else + return timers_wait_until_core(desired_usec); +#endif +} + +void wait_until_break(int _modno) { +#ifndef CIMODE + timers_wait_until_break_core(); +#endif +} + +void deinit(int _modno) { + // Stop the NDI Sender task + atomic_store(&keep_running, false); + oscore_task_join(&ndi_task); + + // Clean up resources + NDIlib_send_destroy(ndi_send); + NDIlib_destroy(); + + free(primary_buffer); + free(front_buffer); + free(back_buffer); +} + +// NDI sending function (runs in a separate task) +static void* send_task(void *arg) { + // Create the NDI sender + NDIlib_initialize(); + NDIlib_send_create_t send_create_desc = {0}; + send_create_desc.p_ndi_name = "SLED"; + send_create_desc.clock_video = false; + send_create_desc.clock_audio = false; + ndi_send = NDIlib_send_create(&send_create_desc); + + // Prepopulate frame with static info. + NDIlib_video_frame_v2_t video_frame; + video_frame.xres = MATRIX_X; + video_frame.yres = MATRIX_Y; + video_frame.line_stride_in_bytes = MATRIX_X * sizeof(RGB); + video_frame.picture_aspect_ratio = (float)MATRIX_X / (float)MATRIX_Y; + video_frame.FourCC = NDIlib_FourCC_type_RGBX; + video_frame.frame_format_type = NDIlib_frame_format_type_progressive; + video_frame.frame_rate_N = FPS * 1000; + video_frame.frame_rate_D = 1000; + + while (atomic_load(&keep_running)) { + // Get current buffer + int current_fob = atomic_load(&front_or_back); + + // Send a video frame with the actual data. + video_frame.p_data = (uint8_t*) (current_fob ? front_buffer : back_buffer); + NDIlib_send_send_video_async_v2(ndi_send, &video_frame); + + // Wait for the next frame + usleep(1000000 / FPS); + } + + // Force sync for last frame. + NDIlib_send_send_video_async_v2(ndi_send, NULL); + return NULL; +} + diff --git a/src/modules/out_ndi.incs b/src/modules/out_ndi.incs new file mode 100644 index 0000000..fd5c190 --- /dev/null +++ b/src/modules/out_ndi.incs @@ -0,0 +1 @@ +-I"$NDI_SDK_DIR/include" diff --git a/src/modules/out_ndi.libs b/src/modules/out_ndi.libs new file mode 100644 index 0000000..897f475 --- /dev/null +++ b/src/modules/out_ndi.libs @@ -0,0 +1 @@ +-L"$NDI_SDK_DIR/lib" -L/usr/local/lib -lndi From f21438b6bfdf6eb5c28eff1711a8badb8b371a05 Mon Sep 17 00:00:00 2001 From: Adrian Pistol Date: Sat, 7 Jun 2025 14:40:42 +0200 Subject: [PATCH 2/3] out_ndi: Scale output to 2x2 rendered This lets us ignore the 4:2:2 subsampling as it's quite important in our usecase. --- src/modules/out_ndi.c | 38 +++++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/src/modules/out_ndi.c b/src/modules/out_ndi.c index 9c81e2e..fc13f47 100644 --- a/src/modules/out_ndi.c +++ b/src/modules/out_ndi.c @@ -18,7 +18,8 @@ #error Define MATRIX_Y as the matrix's Y size. #endif -#define FPS 60 +#define NDI_FPS 60 +#define NDI_SCALE_FACTOR 2 // Pixels per Axis static RGB *primary_buffer; static RGB *front_buffer; @@ -30,14 +31,29 @@ static NDIlib_send_instance_t ndi_send = NULL; static oscore_task ndi_task; static void* send_task(void *arg); - + +static void upscale_buffer(const RGB *src, int w, int h, int scale, RGB *dst) { + int w2 = w * scale; + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + RGB px = src[y*w + x]; + int base = (y*scale)*w2 + x*scale; + for (int dy = 0; dy < scale; dy++) { + for (int dx = 0; dx < scale; dx++) { + dst[base + dy*w2 + dx] = px; + } + } + } + } +} + int init(void) { assert(sizeof(RGB) == 4); // Allocate memory for the buffers primary_buffer = (RGB*)malloc(MATRIX_X * MATRIX_Y * sizeof(RGB)); - front_buffer = (RGB*)malloc(MATRIX_X * MATRIX_Y * sizeof(RGB)); - back_buffer = (RGB*)malloc(MATRIX_X * MATRIX_Y * sizeof(RGB)); + front_buffer = (RGB*)malloc(MATRIX_X * MATRIX_Y * sizeof(RGB) * (2*NDI_SCALE_FACTOR)); + back_buffer = (RGB*)malloc(MATRIX_X * MATRIX_Y * sizeof(RGB) * (2*NDI_SCALE_FACTOR)); atomic_init(&keep_running, true); atomic_init(&front_or_back, true); @@ -75,8 +91,8 @@ int render(void) { int current_fob = atomic_load(&front_or_back); RGB *current_buffer = current_fob ? front_buffer : back_buffer; - // Copy primary buffer to current buffer - memcpy(current_buffer, primary_buffer, MATRIX_X * MATRIX_Y * sizeof(RGB)); + // Upscale primary buffer to current buffer + upscale_buffer(primary_buffer, MATRIX_X, MATRIX_Y, NDI_SCALE_FACTOR, current_buffer); // Flip buffers atomic_store(&front_or_back, !current_fob); @@ -124,13 +140,13 @@ static void* send_task(void *arg) { // Prepopulate frame with static info. NDIlib_video_frame_v2_t video_frame; - video_frame.xres = MATRIX_X; - video_frame.yres = MATRIX_Y; - video_frame.line_stride_in_bytes = MATRIX_X * sizeof(RGB); + video_frame.xres = MATRIX_X * NDI_SCALE_FACTOR; + video_frame.yres = MATRIX_Y * NDI_SCALE_FACTOR; + video_frame.line_stride_in_bytes = MATRIX_X * NDI_SCALE_FACTOR * sizeof(RGB); video_frame.picture_aspect_ratio = (float)MATRIX_X / (float)MATRIX_Y; video_frame.FourCC = NDIlib_FourCC_type_RGBX; video_frame.frame_format_type = NDIlib_frame_format_type_progressive; - video_frame.frame_rate_N = FPS * 1000; + video_frame.frame_rate_N = NDI_FPS * 1000; video_frame.frame_rate_D = 1000; while (atomic_load(&keep_running)) { @@ -142,7 +158,7 @@ static void* send_task(void *arg) { NDIlib_send_send_video_async_v2(ndi_send, &video_frame); // Wait for the next frame - usleep(1000000 / FPS); + usleep(1000000 / NDI_FPS); } // Force sync for last frame. From b99feef3b239f0da42248670ca523751375bbd7c Mon Sep 17 00:00:00 2001 From: Adrian Pistol Date: Sat, 7 Jun 2025 15:42:23 +0200 Subject: [PATCH 3/3] out_ndi: Actually render to alternative buffer. All the effort, none of the gain. I'm an idiot. --- src/modules/out_ndi.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/modules/out_ndi.c b/src/modules/out_ndi.c index fc13f47..ee8b1d4 100644 --- a/src/modules/out_ndi.c +++ b/src/modules/out_ndi.c @@ -89,10 +89,10 @@ int clear(int _modno) { int render(void) { // Get current buffer int current_fob = atomic_load(&front_or_back); - RGB *current_buffer = current_fob ? front_buffer : back_buffer; + RGB *alt_buffer = current_fob ? back_buffer : front_buffer; - // Upscale primary buffer to current buffer - upscale_buffer(primary_buffer, MATRIX_X, MATRIX_Y, NDI_SCALE_FACTOR, current_buffer); + // Upscale primary buffer to alternate buffer + upscale_buffer(primary_buffer, MATRIX_X, MATRIX_Y, NDI_SCALE_FACTOR, alt_buffer); // Flip buffers atomic_store(&front_or_back, !current_fob); @@ -153,7 +153,7 @@ static void* send_task(void *arg) { // Get current buffer int current_fob = atomic_load(&front_or_back); - // Send a video frame with the actual data. + // Send a video frame with the current buffer's data. video_frame.p_data = (uint8_t*) (current_fob ? front_buffer : back_buffer); NDIlib_send_send_video_async_v2(ndi_send, &video_frame);