You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

393 lines
12 KiB

7 months ago
// cam_enc_bench.c : V4L2 캡처 + TurboJPEG 인코딩 벤치마크 (WS/추론 없음)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <signal.h>
#include <errno.h>
#include <pthread.h>
#include <linux/videodev2.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <time.h>
#include <unistd.h>
#include <stdint.h>
#include <turbojpeg.h>
#include "v4l2_interface.h"
#include "nc_utils.h"
// -----------------------------
// 설정
// -----------------------------
#define VIS0_MAX_CH (0)
#define VIS1_MAX_CH (1)
#define VIDEO_MAX_CH (VIS0_MAX_CH + VIS1_MAX_CH)
#define VIDEO_BUFFER_NUM (3)
#define VIDEO_WIDTH (1920)
#define VIDEO_HEIGHT (1080)
// 인코딩 워커 설정
#define ENC_WORKER_NUM (4)
#define ENC_QUEUE_SIZE (16)
// -----------------------------
// 전역
// -----------------------------
static volatile int g_running = 1;
st_nc_v4l2_config v4l2_config[VIDEO_MAX_CH];
// -----------------------------
// 인코딩 큐 구조체
// -----------------------------
typedef struct {
int valid;
int ch;
int w, h;
int stride;
size_t buf_size;
uint8_t *data; // RGB24 복사 버퍼
} enc_job_t;
static enc_job_t g_enc_queue[ENC_QUEUE_SIZE];
static int g_enc_head = 0;
static int g_enc_tail = 0;
static pthread_mutex_t g_enc_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t g_enc_cond = PTHREAD_COND_INITIALIZER;
// 간단 통계용
static unsigned long g_enc_frame_count = 0;
static pthread_mutex_t g_stat_mutex = PTHREAD_MUTEX_INITIALIZER;
static struct timespec g_fps_last_ts = {0, 0};
static unsigned long g_fps_last_cnt = 0;
// -----------------------------
// Ctrl+C 핸들러
// -----------------------------
static void on_sigint(int sig) {
(void)sig;
g_running = 0;
// 워커들이 cond wait에서 깨어나도록 브로드캐스트
pthread_mutex_lock(&g_enc_mutex);
pthread_cond_broadcast(&g_enc_cond);
pthread_mutex_unlock(&g_enc_mutex);
}
// -----------------------------
// 채널 설정
// -----------------------------
static void set_v4l2_config(void)
{
// VIS0 영역 (현재 VIS0_MAX_CH = 0 이라서 실제론 없지만 구조 유지)
for (int i = 0; i < VIS0_MAX_CH; i++) {
v4l2_config[i].video_buf.video_device_num = CNN_DEVICE_NUM(VISION0) + i;
v4l2_config[i].video_buf.video_fd = -1;
v4l2_config[i].dma_mode = INTERLEAVE;
v4l2_config[i].img_process = MODE_DS;
v4l2_config[i].pixformat = V4L2_PIX_FMT_RGB24;
v4l2_config[i].crop_x_start = 0;
v4l2_config[i].crop_y_start = 0;
v4l2_config[i].crop_width = 0;
v4l2_config[i].crop_height = 0;
v4l2_config[i].ds_width = VIDEO_WIDTH;
v4l2_config[i].ds_height = VIDEO_HEIGHT;
}
// VIS1 영역 (실제 1ch)
for (int j = VIS0_MAX_CH; j < VIDEO_MAX_CH; j++) {
v4l2_config[j].video_buf.video_device_num = CNN_DEVICE_NUM(VISION1) + j;
v4l2_config[j].video_buf.video_fd = -1;
v4l2_config[j].dma_mode = INTERLEAVE;
v4l2_config[j].img_process = MODE_DS;
v4l2_config[j].pixformat = V4L2_PIX_FMT_RGB24;
v4l2_config[j].crop_x_start = 0;
v4l2_config[j].crop_y_start = 0;
v4l2_config[j].crop_width = 0;
v4l2_config[j].crop_height = 0;
v4l2_config[j].ds_width = VIDEO_WIDTH;
v4l2_config[j].ds_height = VIDEO_HEIGHT;
}
}
// -----------------------------
// V4L2 초기화
// -----------------------------
static int v4l2_initialize(void)
{
for (int i = 0; i < VIDEO_MAX_CH; i++) {
v4l2_config[i].video_buf.video_fd =
nc_v4l2_open(v4l2_config[i].video_buf.video_device_num, true);
if (v4l2_config[i].video_buf.video_fd == errno) {
fprintf(stderr, "[error] nc_v4l2_open() failure (ch=%d)\n", i);
return -1;
}
if (nc_v4l2_init_device_and_stream_on(&v4l2_config[i], VIDEO_BUFFER_NUM) < 0) {
fprintf(stderr, "[error] nc_v4l2_init_device_and_stream_on() failure (ch=%d)\n", i);
return -1;
}
}
nc_v4l2_show_user_config(&v4l2_config[0], VIDEO_MAX_CH);
return 0;
}
// -----------------------------
// 인코딩 큐 enqueue
// -----------------------------
static void enc_enqueue(int ch, int w, int h, int stride, const void *src)
{
size_t bytes = (size_t)stride * (size_t)h;
pthread_mutex_lock(&g_enc_mutex);
int next_tail = (g_enc_tail + 1) % ENC_QUEUE_SIZE;
if (next_tail == g_enc_head) {
// 큐가 가득 찼으면 가장 오래된 것 하나 drop
g_enc_head = (g_enc_head + 1) % ENC_QUEUE_SIZE;
}
enc_job_t *job = &g_enc_queue[g_enc_tail];
if (!job->data || job->buf_size < bytes) {
free(job->data);
job->data = (uint8_t*)malloc(bytes);
job->buf_size = bytes;
}
memcpy(job->data, src, bytes);
job->ch = ch;
job->w = w;
job->h = h;
job->stride = stride;
job->valid = 1;
g_enc_tail = next_tail;
pthread_cond_signal(&g_enc_cond);
pthread_mutex_unlock(&g_enc_mutex);
}
// -----------------------------
// 인코딩 워커 스레드
// -----------------------------
static void* enc_worker_thread(void *arg)
{
intptr_t wid = (intptr_t)arg;
tjhandle tj = tjInitCompress();
if (!tj) {
fprintf(stderr, "[ENC%ld] tjInitCompress failed: %s\n",
(long)wid, tjGetErrorStr());
return NULL;
}
while (g_running) {
enc_job_t job;
memset(&job, 0, sizeof(job));
// 큐에서 job 하나 가져오기
pthread_mutex_lock(&g_enc_mutex);
while (g_enc_head == g_enc_tail && g_running) {
pthread_cond_wait(&g_enc_cond, &g_enc_mutex);
}
if (!g_running) {
pthread_mutex_unlock(&g_enc_mutex);
break;
}
job = g_enc_queue[g_enc_head]; // 구조체 복사 (data 포인터 공유)
g_enc_head = (g_enc_head + 1) % ENC_QUEUE_SIZE;
pthread_mutex_unlock(&g_enc_mutex);
if (!job.valid || !job.data) continue;
unsigned char *jpegBuf = NULL;
unsigned long jpegSize = 0;
struct timespec t0, t1;
clock_gettime(CLOCK_MONOTONIC, &t0);
int rc = tjCompress2(
tj,
job.data,
job.w,
job.stride,
job.h,
TJPF_RGB, // V4L2_PIX_FMT_RGB24 기준
&jpegBuf,
&jpegSize,
TJSAMP_420,
45, // quality
TJFLAG_FASTDCT
);
clock_gettime(CLOCK_MONOTONIC, &t1);
if (rc != 0) {
fprintf(stderr, "[ENC%ld] tjCompress2 error: %s\n",
(long)wid, tjGetErrorStr());
if (jpegBuf) tjFree(jpegBuf);
continue;
}
// double sec_diff = (double)(t1.tv_sec - t0.tv_sec);
// double nsec_diff = (double)(t1.tv_nsec - t0.tv_nsec);
// double ms = sec_diff * 1000.0 +
// nsec_diff / 1e6;
pthread_mutex_lock(&g_stat_mutex);
g_enc_frame_count++;
unsigned long n = g_enc_frame_count;
// ---- FPS 계산 ----
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
if (g_fps_last_ts.tv_sec == 0 && g_fps_last_ts.tv_nsec == 0) {
// 첫 호출 초기화
g_fps_last_ts = now;
g_fps_last_cnt = n;
} else {
double sec = (double)(now.tv_sec - g_fps_last_ts.tv_sec) +
(double)(now.tv_nsec - g_fps_last_ts.tv_nsec) / 1e9;
if (sec >= 1.0) { // 1초 이상 지났을 때만 출력
unsigned long diff = n - g_fps_last_cnt;
double fps = (double)diff / sec;
printf("[ENC_STAT] total=%lu fps=%.2f\n", n, fps);
g_fps_last_ts = now;
g_fps_last_cnt = n;
}
}
pthread_mutex_unlock(&g_stat_mutex);
// // 간단 로그
// printf("[ENC%ld] frame=%lu ch=%d w=%d h=%d size=%lu bytes time=%.3f ms\n",
// (long)wid, n, job.ch, job.w, job.h,
// (unsigned long)jpegSize, ms);
tjFree(jpegBuf);
}
tjDestroy(tj);
return NULL;
}
// -----------------------------
// 메인
// -----------------------------
int main(int argc, char** argv)
{
(void)argc; (void)argv;
// SIGINT 핸들링
struct sigaction sa;
sa.sa_handler = on_sigint;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESETHAND;
sigaction(SIGINT, &sa, NULL);
// 채널/디바이스 설정
memset(v4l2_config, 0, sizeof(v4l2_config));
set_v4l2_config();
// V4L2 시작
if (v4l2_initialize() < 0) {
fprintf(stderr, "v4l2_initialize failed\n");
return -1;
}
// 인코더 워커 스레드 시작
pthread_t enc_threads[ENC_WORKER_NUM];
for (int i = 0; i < ENC_WORKER_NUM; i++) {
pthread_create(&enc_threads[i], NULL,
enc_worker_thread, (void*)(intptr_t)i);
}
printf("[APP] capture + TurboJPEG encode only (no WS, no CNN)\n");
printf("[APP] press Ctrl+C to stop\n");
// 메인 루프: 캡처 → 인코딩 큐 enqueue → QBUF
while (g_running) {
for (int ch = 0; ch < VIDEO_MAX_CH; ch++) {
if (v4l2_config[ch].video_buf.video_fd < 0) continue;
struct v4l2_buffer video_buf;
CLEAR(video_buf);
// DQBUF (프레임 수신)
if (nc_v4l2_dequeue_buffer(v4l2_config[ch].video_buf.video_fd, &video_buf) == -1) {
// non-fatal
continue;
}
// 프레임 포인터
void* frame_ptr = v4l2_config[ch].video_buf.buffers[video_buf.index].start;
// RGB24 기준으로 인코딩 큐에 넣기
int w = VIDEO_WIDTH;
int h = VIDEO_HEIGHT;
int stride = w * 3;
enc_enqueue(ch, w, h, stride, frame_ptr);
static struct timespec last_ts = {0, 0};
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
static unsigned long last_cnt = 0;
if (last_ts.tv_sec != 0) {
double sec = (double)(now.tv_sec - last_ts.tv_sec) +
(double)(now.tv_nsec - last_ts.tv_nsec) / 1e9;
if (sec >= 1.0) {
unsigned long cur_cnt = g_enc_frame_count;
double fps = (double)(cur_cnt - last_cnt) / sec;
printf("[ENC_STAT] fps=%.2f\n", fps);
last_cnt = cur_cnt;
last_ts = now;
}
} else {
last_ts = now;
}
// QBUF (재사용)
if (nc_v4l2_queue_buffer(v4l2_config[ch].video_buf.video_fd, video_buf.index) == -1) {
fprintf(stderr, "Error VIDIOC_QBUF buffer %d (ch=%d)\n",
video_buf.index, ch);
}
}
// 너무 바쁘면 살짝 양보 (원하면 줄이거나 제거 가능)
// usleep(1000);
}
// 종료 처리: 스트림 오프 & 닫기
for (int i = 0; i < VIDEO_MAX_CH; i++) {
if (v4l2_config[i].video_buf.video_fd >= 0) {
close(v4l2_config[i].video_buf.video_fd);
v4l2_config[i].video_buf.video_fd = -1;
}
}
// 워커 종료 대기 (굳이 안 해도 프로세스 종료되지만 깔끔하게)
for (int i = 0; i < ENC_WORKER_NUM; i++) {
pthread_join(enc_threads[i], NULL);
}
// 큐 버퍼 free
for (int i = 0; i < ENC_QUEUE_SIZE; i++) {
free(g_enc_queue[i].data);
g_enc_queue[i].data = NULL;
g_enc_queue[i].buf_size = 0;
}
return 0;
}