diff --git a/.gitignore b/.gitignore index 204a3b2..a2fce26 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ build/ .cache/ *.AppImage squashfs-root/ + +buildpi/ +rpi-ffmpeg/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 058760c..8b5678e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,11 +8,19 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") SET(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/lib") SET(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") +set(CMAKE_C_STANDARD 23) -add_subdirectory(lib) +OPTION(VANILLA_BUILD_DESKTOP "Build Qt app for Linux desktop" ON) +OPTION(VANILLA_BUILD_RPI "Build SDL2 app for Raspberry Pi" OFF) +OPTION(VANILLA_BUILD_TESTS "Build unit tests for Vanilla" OFF) +add_subdirectory(lib) if (NOT ANDROID) add_subdirectory(pipe) endif() - -add_subdirectory(app) +if (VANILLA_BUILD_DESKTOP) + add_subdirectory(app) +endif() +if (VANILLA_BUILD_RPI) + add_subdirectory(rpi) +endif() diff --git a/app/backend.cpp b/app/backend.cpp index 428cc01..cd5d82f 100644 --- a/app/backend.cpp +++ b/app/backend.cpp @@ -14,20 +14,22 @@ #include #include -void vanillaEventHandler(void *context, int type, const char *data, size_t dataLength) -{ - Backend *backend = static_cast(context); - - switch (type) { - case VANILLA_EVENT_VIDEO: - emit backend->videoAvailable(QByteArray(data, dataLength)); - break; - case VANILLA_EVENT_AUDIO: - emit backend->audioAvailable(QByteArray(data, dataLength)); - break; - case VANILLA_EVENT_VIBRATE: - emit backend->vibrate(*data); - break; +void Backend::vanillaEventHandler() +{ + vanilla_event_t event; + + while (vanilla_wait_event(&event)) { + switch (event.type) { + case VANILLA_EVENT_VIDEO: + emit videoAvailable(QByteArray((const char *) event.data, event.size)); + break; + case VANILLA_EVENT_AUDIO: + emit audioAvailable(QByteArray((const char *) event.data, event.size)); + break; + case VANILLA_EVENT_VIBRATE: + emit vibrate(*event.data); + break; + } } } @@ -70,12 +72,13 @@ int Backend::syncInternal(uint16_t code) void Backend::connectToConsole() { - QtConcurrent::run(&Backend::connectInternal, this); + connectInternal(); + QtConcurrent::run(&Backend::vanillaEventHandler, this); } int Backend::connectInternal() { - return vanilla_start(vanillaEventHandler, this); + return vanilla_start(0); } void Backend::updateTouch(int x, int y) @@ -175,7 +178,7 @@ int BackendViaInternalPipe::syncInternal(uint16_t code) int BackendViaInternalPipe::connectInternal() { - return vanilla_start_udp(vanillaEventHandler, this, QHostAddress(QHostAddress::LocalHost).toIPv4Address()); + return vanilla_start(QHostAddress(QHostAddress::LocalHost).toIPv4Address()); } BackendViaExternalPipe::BackendViaExternalPipe(const QHostAddress &udpServer, QObject *parent) : Backend(parent) @@ -190,5 +193,5 @@ int BackendViaExternalPipe::syncInternal(uint16_t code) int BackendViaExternalPipe::connectInternal() { - return vanilla_start_udp(vanillaEventHandler, this, m_serverAddress.toIPv4Address()); + return vanilla_start(m_serverAddress.toIPv4Address()); } \ No newline at end of file diff --git a/app/backend.h b/app/backend.h index 7b79e49..2c036ef 100644 --- a/app/backend.h +++ b/app/backend.h @@ -75,6 +75,8 @@ public slots: private slots: void syncFutureCompleted(); + void vanillaEventHandler(); + }; class BackendViaInternalPipe : public Backend diff --git a/docker/rpi/Dockerfile b/docker/rpi/Dockerfile new file mode 100644 index 0000000..22c1dc6 --- /dev/null +++ b/docker/rpi/Dockerfile @@ -0,0 +1,7 @@ +FROM --platform=linux/arm/v6 arm32v6/alpine + +RUN apk update +RUN apk upgrade +RUN apk add build-base cmake libnl3-dev openssl-dev sdl2-dev linux-headers ffmpeg-dev eudev-dev + +CMD [ "make", "-j32", "-C", "/data/buildpi" ] diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 52c2f1c..94c9b92 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -20,3 +20,12 @@ if (NOT ANDROID) endif() install(TARGETS vanilla) + +if (VANILLA_BUILD_TESTS) + add_executable(bittest + test/bittest.c + ) + + target_link_libraries(bittest PRIVATE vanilla m) + target_include_directories(bittest PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) +endif() \ No newline at end of file diff --git a/lib/gamepad/audio.c b/lib/gamepad/audio.c index 455125a..48dcf77 100644 --- a/lib/gamepad/audio.c +++ b/lib/gamepad/audio.c @@ -31,21 +31,24 @@ typedef struct { uint32_t video_format; } AudioPacketVideoFormat; -void handle_audio_packet(vanilla_event_handler_t event_handler, void *context, char *data, size_t len) +void handle_audio_packet(gamepad_context_t *ctx, unsigned char *data, size_t len) { - for (int byte = 0; byte < len; byte++) { + // + // === IMPORTANT NOTE! === + // + // This for loop skips ap->format, ap->seq_id, and ap->timestamp to save processing. + // If you want those, you'll have to adjust this loop. + // + for (int byte = 0; byte < 4; byte++) { data[byte] = (unsigned char) reverse_bits(data[byte], 8); } AudioPacket *ap = (AudioPacket *) data; - ap->format = reverse_bits(ap->format, 3); - ap->seq_id = reverse_bits(ap->seq_id, 10); + // ap->format = reverse_bits(ap->format, 3); + // ap->seq_id = reverse_bits(ap->seq_id, 10); ap->payload_size = reverse_bits(ap->payload_size, 16); - ap->timestamp = reverse_bits(ap->timestamp, 32); - for (int byte = 0; byte < ap->payload_size; ++byte) { - ap->payload[byte] = (unsigned char) reverse_bits(ap->payload[byte], 8); - } + // ap->timestamp = reverse_bits(ap->timestamp, 32); if (ap->type == TYPE_VIDEO) { AudioPacketVideoFormat *avp = (AudioPacketVideoFormat *) ap->payload; @@ -56,22 +59,24 @@ void handle_audio_packet(vanilla_event_handler_t event_handler, void *context, c return; } - event_handler(context, VANILLA_EVENT_AUDIO, ap->payload, ap->payload_size); + if (ap->payload_size) { + push_event(ctx->event_loop, VANILLA_EVENT_AUDIO, ap->payload, ap->payload_size); + } uint8_t vibrate_val = ap->vibrate; - event_handler(context, VANILLA_EVENT_VIBRATE, &vibrate_val, sizeof(vibrate_val)); + push_event(ctx->event_loop, VANILLA_EVENT_VIBRATE, &vibrate_val, sizeof(vibrate_val)); } void *listen_audio(void *x) { - struct gamepad_thread_context *info = (struct gamepad_thread_context *) x; + gamepad_context_t *info = (gamepad_context_t *) x; unsigned char data[2048]; ssize_t size; do { size = recv(info->socket_aud, data, sizeof(data), 0); if (size > 0) { if (is_stop_code(data, size)) break; - handle_audio_packet(info->event_handler, info->context, data, size); + handle_audio_packet(info, data, size); } } while (!is_interrupted()); diff --git a/lib/gamepad/command.c b/lib/gamepad/command.c index 4475285..f542664 100644 --- a/lib/gamepad/command.c +++ b/lib/gamepad/command.c @@ -422,7 +422,7 @@ void handle_command_packet(int skt, CmdHeader *request) void *listen_command(void *x) { - struct gamepad_thread_context *info = (struct gamepad_thread_context *)x; + gamepad_context_t *info = (gamepad_context_t *)x; unsigned char data[sizeof(CmdHeader) + 2048]; ssize_t size; diff --git a/lib/gamepad/gamepad.c b/lib/gamepad/gamepad.c index c6397e8..730443e 100644 --- a/lib/gamepad/gamepad.c +++ b/lib/gamepad/gamepad.c @@ -1,3 +1,5 @@ +#define _GNU_SOURCE + #include "gamepad.h" #include @@ -29,30 +31,19 @@ uint16_t PORT_AUD; uint16_t PORT_HID; uint16_t PORT_CMD; -unsigned int reverse_bits(unsigned int b, int bit_count) -{ - unsigned int result = 0; - - for (int i = 0; i < bit_count; i++) { - result |= ((b >> i) & 1) << (bit_count - 1 -i ); - } - - return result; -} - void send_to_console(int fd, const void *data, size_t data_size, int port) { struct sockaddr_in address; address.sin_family = AF_INET; address.sin_addr.s_addr = SERVER_ADDRESS; address.sin_port = htons((uint16_t) (port - 100)); - - char ip[20]; - inet_ntop(AF_INET, &address.sin_addr, ip, sizeof(ip)); + + char ip[INET_ADDRSTRLEN]; + inet_ntop(AF_INET, &address.sin_addr, ip, INET_ADDRSTRLEN); ssize_t sent = sendto(fd, data, data_size, 0, (const struct sockaddr *) &address, sizeof(address)); if (sent == -1) { - print_info("Failed to send to Wii U socket: fd - %d; port - %d", fd, port - 100); + print_info("Failed to send to Wii U socket: address: %s, fd: %d, port: %d, errno: %i", ip, fd, port - 100, errno); } } @@ -62,13 +53,21 @@ int create_socket(int *socket_out, uint16_t port) address.sin_family = AF_INET; address.sin_addr.s_addr = INADDR_ANY; address.sin_port = htons(port); - (*socket_out) = socket(AF_INET, SOCK_DGRAM, 0); + + int skt = socket(AF_INET, SOCK_DGRAM, 0); + if (skt == -1) { + print_info("FAILED TO CREATE SOCKET FOR PORT %i", port); + return 0; + } + + (*socket_out) = skt; if (bind((*socket_out), (const struct sockaddr *) &address, sizeof(address)) == -1) { print_info("FAILED TO BIND PORT %u: %i", port, errno); return 0; } + print_info("SUCCESSFULLY BOUND SOCKET %i on PORT %i", skt, port); return 1; } @@ -145,14 +144,15 @@ int sync_internal(uint16_t code, uint32_t server_address) SERVER_ADDRESS = htonl(server_address); } - int skt; - int ret = connect_to_backend(&skt, VANILLA_PIPE_SYNC_CODE(code)); - if (ret != VANILLA_SUCCESS) { - goto exit; + int skt = 0; + int ret = VANILLA_ERROR; + if (server_address != 0) { + ret = connect_to_backend(&skt, VANILLA_PIPE_SYNC_CODE(code)); + if (ret != VANILLA_SUCCESS) { + goto exit; + } } - ret = VANILLA_ERROR; - // Wait for sync result from pipe uint32_t recv_cc; while (1) { @@ -172,13 +172,14 @@ int sync_internal(uint16_t code, uint32_t server_address) } exit_pipe: - close(skt); + if (skt) + close(skt); exit: return ret; } -int connect_as_gamepad_internal(vanilla_event_handler_t event_handler, void *context, uint32_t server_address) +int connect_as_gamepad_internal(event_loop_t *event_loop, uint32_t server_address) { clear_interrupt(); @@ -199,16 +200,17 @@ int connect_as_gamepad_internal(vanilla_event_handler_t event_handler, void *con PORT_CMD += 200; } - struct gamepad_thread_context info; - info.event_handler = event_handler; - info.context = context; + gamepad_context_t info; + info.event_loop = event_loop; int ret = VANILLA_ERROR; - int pipe_cc_skt; - ret = connect_to_backend(&pipe_cc_skt, VANILLA_PIPE_CC_CONNECT); - if (ret != VANILLA_SUCCESS) { - goto exit; + int pipe_cc_skt = 0; + if (server_address != 0) { + ret = connect_to_backend(&pipe_cc_skt, VANILLA_PIPE_CC_CONNECT); + if (ret != VANILLA_SUCCESS) { + goto exit; + } } // Open all required sockets @@ -221,9 +223,16 @@ int connect_as_gamepad_internal(vanilla_event_handler_t event_handler, void *con pthread_t video_thread, audio_thread, input_thread, msg_thread, cmd_thread; pthread_create(&video_thread, NULL, listen_video, &info); + pthread_setname_np(video_thread, "vanilla-video"); + pthread_create(&audio_thread, NULL, listen_audio, &info); + pthread_setname_np(audio_thread, "vanilla-audio"); + pthread_create(&input_thread, NULL, listen_input, &info); + pthread_setname_np(input_thread, "vanilla-input"); + pthread_create(&cmd_thread, NULL, listen_command, &info); + pthread_setname_np(cmd_thread, "vanilla-cmd"); while (1) { usleep(250 * 1000); @@ -241,7 +250,9 @@ int connect_as_gamepad_internal(vanilla_event_handler_t event_handler, void *con pthread_join(input_thread, NULL); pthread_join(cmd_thread, NULL); - send_pipe_cc(pipe_cc_skt, VANILLA_PIPE_CC_UNBIND, 0); + if (server_address != 0) { + send_pipe_cc(pipe_cc_skt, VANILLA_PIPE_CC_UNBIND, 0); + } ret = VANILLA_SUCCESS; @@ -261,7 +272,8 @@ int connect_as_gamepad_internal(vanilla_event_handler_t event_handler, void *con close(info.socket_vid); exit_pipe: - close(pipe_cc_skt); + if (pipe_cc_skt) + close(pipe_cc_skt); exit: return ret; @@ -270,4 +282,58 @@ int connect_as_gamepad_internal(vanilla_event_handler_t event_handler, void *con int is_stop_code(const char *data, size_t data_length) { return (data_length == sizeof(STOP_CODE) && !memcmp(data, &STOP_CODE, sizeof(STOP_CODE))); +} + +int push_event(event_loop_t *loop, int type, const void *data, size_t size) +{ + + pthread_mutex_lock(&loop->mutex); + + vanilla_event_t *ev = &loop->events[loop->new_index % VANILLA_MAX_EVENT_COUNT]; + + if (size <= sizeof(ev->data)) { + ev->type = type; + memcpy(ev->data, data, size); + ev->size = size; + + loop->new_index++; + + // Prevent rollover by skipping oldest event if necessary + if (loop->new_index > loop->used_index + VANILLA_MAX_EVENT_COUNT) { + print_info("SKIPPED EVENT TO PREVENT ROLLOVER (%lu > %lu + %lu)", loop->new_index, loop->used_index, VANILLA_MAX_EVENT_COUNT); + loop->used_index++; + } + + pthread_cond_broadcast(&loop->waitcond); + } else { + print_info("FAILED TO PUSH EVENT: wanted %lu, only had %lu. This is a bug, please report to developers.\n", size, sizeof(ev->data)); + } + + pthread_mutex_unlock(&loop->mutex); +} + +int get_event(event_loop_t *loop, vanilla_event_t *event, int wait) +{ + int ret = 0; + + pthread_mutex_lock(&loop->mutex); + + if (loop->active) { + if (wait) { + while (loop->active && loop->used_index == loop->new_index) { + pthread_cond_wait(&loop->waitcond, &loop->mutex); + } + } + + if (loop->active && loop->used_index < loop->new_index) { + // Output data to pointer + *event = loop->events[loop->used_index % VANILLA_MAX_EVENT_COUNT]; + loop->used_index++; + ret = 1; + } + } + + pthread_mutex_unlock(&loop->mutex); + + return ret; } \ No newline at end of file diff --git a/lib/gamepad/gamepad.h b/lib/gamepad/gamepad.h index ddcdb36..6a376f4 100644 --- a/lib/gamepad/gamepad.h +++ b/lib/gamepad/gamepad.h @@ -3,6 +3,7 @@ #include "vanilla.h" +#include #include extern uint16_t PORT_MSG; @@ -13,22 +14,32 @@ extern uint16_t PORT_CMD; struct wpa_ctrl; -struct gamepad_thread_context +#define VANILLA_MAX_EVENT_COUNT 20 +typedef struct { - vanilla_event_handler_t event_handler; - void *context; - + vanilla_event_t events[VANILLA_MAX_EVENT_COUNT]; + size_t new_index; + size_t used_index; + int active; + pthread_mutex_t mutex; + pthread_cond_t waitcond; +} event_loop_t; + +typedef struct +{ + event_loop_t *event_loop; int socket_vid; int socket_aud; int socket_hid; int socket_msg; int socket_cmd; -}; +} gamepad_context_t; int sync_internal(uint16_t code, uint32_t server_address); -int connect_as_gamepad_internal(vanilla_event_handler_t event_handler, void *context, uint32_t server_address); -unsigned int reverse_bits(unsigned int b, int bit_count); +int connect_as_gamepad_internal(event_loop_t *ctx, uint32_t server_address); void send_to_console(int fd, const void *data, size_t data_size, int port); int is_stop_code(const char *data, size_t data_length); +int push_event(event_loop_t *loop, int type, const void *data, size_t size); +int get_event(event_loop_t *loop, vanilla_event_t *event, int wait); #endif // VANILLA_GAMEPAD_H \ No newline at end of file diff --git a/lib/gamepad/input.c b/lib/gamepad/input.c index 737095a..844e75b 100644 --- a/lib/gamepad/input.c +++ b/lib/gamepad/input.c @@ -243,7 +243,7 @@ void send_input(int socket_hid) void *listen_input(void *x) { - struct gamepad_thread_context *info = (struct gamepad_thread_context *) x; + gamepad_context_t *info = (gamepad_context_t *) x; pthread_mutex_init(&button_mtx, NULL); diff --git a/lib/gamepad/video.c b/lib/gamepad/video.c index cbf3cc4..7daaf80 100644 --- a/lib/gamepad/video.c +++ b/lib/gamepad/video.c @@ -1,11 +1,14 @@ #include "video.h" +#include +#include #include #include #include #include #include #include +#include #include #include "gamepad.h" @@ -29,8 +32,15 @@ typedef struct uint8_t payload[2048]; } VideoPacket; -pthread_mutex_t video_mutex; -int idr_is_queued = 0; +static pthread_mutex_t video_mutex; +static int idr_is_queued = 0; + +#define VIDEO_PACKET_CACHE_MAX 1024 +static VideoPacket video_packet_cache[VIDEO_PACKET_CACHE_MAX]; +static size_t video_packet_cache_index = 0; +static uint8_t video_packet[100000]; + +static size_t generated_sps_params_size = 0; void request_idr() { @@ -46,26 +56,25 @@ void send_idr_request_to_console(int socket_msg) send_to_console(socket_msg, idr_request, sizeof(idr_request), PORT_MSG); } -void handle_video_packet(vanilla_event_handler_t event_handler, void *context, unsigned char *data, size_t size, int socket_msg) +void handle_video_packet(gamepad_context_t *ctx, VideoPacket *vp, size_t size, int socket_msg) { - // TODO: This is all really weird. Copied from drc-sim-c but I feel like there's probably a better way. - - for (size_t i = 0; i < size; i++) { + // + // === IMPORTANT NOTE! === + // + // This for loop skips vp->magic, vp->packet_type, and vp->timestamp to save processing. + // If you want those, you'll have to adjust this loop. + // + uint8_t *data = (uint8_t *) vp; + for (size_t i = 0; i < 4; i++) { data[i] = reverse_bits(data[i], 8); } - VideoPacket *vp = (VideoPacket *) data; - - vp->magic = reverse_bits(vp->magic, 4); - vp->packet_type = reverse_bits(vp->packet_type, 2); + // vp->magic = reverse_bits(vp->magic, 4); + // vp->packet_type = reverse_bits(vp->packet_type, 2); + // vp->timestamp = reverse_bits(vp->timestamp, 32); vp->seq_id = reverse_bits(vp->seq_id, 10); vp->payload_size = reverse_bits(vp->payload_size, 11); - vp->timestamp = reverse_bits(vp->timestamp, 32); - for (int byte = 0; byte < sizeof(vp->extended_header); ++byte) - vp->extended_header[byte] = (unsigned char) reverse_bits(vp->extended_header[byte], 8); - for (int byte = 0; byte < vp->payload_size; ++byte) - vp->payload[byte] = (unsigned char) reverse_bits(vp->payload[byte], 8); - + // Check if packet is IDR (instantaneous decoder refresh) int is_idr = 0; for (int i = 0; i < sizeof(vp->extended_header); i++) { @@ -90,8 +99,7 @@ void handle_video_packet(vanilla_event_handler_t event_handler, void *context, u } // Check if this is the beginning of the packet - static char video_segments[1024][2048]; - static size_t video_segment_size[1024]; + static VideoPacket *video_segments[1024]; static int video_packet_seq = -1; static int video_packet_seq_end = -1; @@ -116,8 +124,7 @@ void handle_video_packet(vanilla_event_handler_t event_handler, void *context, u } pthread_mutex_unlock(&video_mutex); - memcpy(video_segments[vp->seq_id], vp->payload, vp->payload_size); - video_segment_size[vp->seq_id] = vp->payload_size; + video_segments[vp->seq_id] = vp; if (vp->frame_end) { video_packet_seq_end = vp->seq_id; @@ -126,32 +133,13 @@ void handle_video_packet(vanilla_event_handler_t event_handler, void *context, u if (video_packet_seq_end != -1) { // int complete_frame = 1; if (is_streaming) { - // Combine segments - char video_packet[100000]; - size_t video_packet_size = 0; - for (int i = video_packet_seq; ; i = (i + 1) % 1024) { - memcpy(video_packet + video_packet_size, video_segments[i], video_segment_size[i]); - video_packet_size += video_segment_size[i]; - if (i == video_packet_seq_end) { - break; - } - } - // Encapsulate packet data into NAL unit static int frame_decode_num = 0; - size_t nals_sz = video_packet_size * 2; - uint8_t *nals = malloc(nals_sz); - uint8_t *nals_current = nals; + uint8_t *nals_current = video_packet + generated_sps_params_size + sizeof(VANILLA_PPS_PARAMS); + int slice_header = is_idr ? 0x25b804ff : (0x21e003ff | ((frame_decode_num & 0xff) << 13)); frame_decode_num++; - if (is_idr) { - memcpy(nals_current, VANILLA_SPS_PARAMS, sizeof(VANILLA_SPS_PARAMS)); - nals_current += sizeof(VANILLA_SPS_PARAMS); - memcpy(nals_current, VANILLA_PPS_PARAMS, sizeof(VANILLA_PPS_PARAMS)); - nals_current += sizeof(VANILLA_PPS_PARAMS); - } - // begin slice nalu uint8_t slice[] = {0x00, 0x00, 0x00, 0x01, (uint8_t) ((slice_header >> 24) & 0xff), @@ -162,23 +150,39 @@ void handle_video_packet(vanilla_event_handler_t event_handler, void *context, u memcpy(nals_current, slice, sizeof(slice)); nals_current += sizeof(slice); - // Frame - memcpy(nals_current, video_packet, 2); + // Get pointer to first packet's payload + int current_index = video_packet_seq; + uint8_t *from = video_segments[current_index]->payload; + + memcpy(nals_current, from, 2); nals_current += 2; // Escape codes - for (int byte = 2; byte < video_packet_size; ++byte) { - if (video_packet[byte] <= 3 && *(nals_current - 2) == 0 && *(nals_current - 1) == 0) { - *nals_current = 3; + int byte = 2; + while (1) { + uint8_t *data = video_segments[current_index]->payload; + size_t pkt_size = video_segments[current_index]->payload_size; + while (byte < pkt_size) { + if (data[byte] <= 3 && *(nals_current - 2) == 0 && *(nals_current - 1) == 0) { + *nals_current = 3; + nals_current++; + } + *nals_current = data[byte]; nals_current++; + byte++; } - *nals_current = video_packet[byte]; - nals_current++; - } - event_handler(context, VANILLA_EVENT_VIDEO, nals, (nals_current - nals)); + if (current_index == video_packet_seq_end) { + break; + } - free(nals); + byte = 0; + current_index = (current_index + 1) % VIDEO_PACKET_CACHE_MAX; + } + + // Skip IDR parameters if not an IDR frame + uint8_t *nals = (is_idr) ? video_packet : (video_packet + generated_sps_params_size + sizeof(VANILLA_PPS_PARAMS)); + push_event(ctx->event_loop, VANILLA_EVENT_VIDEO, nals, (nals_current - nals)); } else { // We didn't receive the complete frame so we'll skip it here } @@ -188,17 +192,35 @@ void handle_video_packet(vanilla_event_handler_t event_handler, void *context, u void *listen_video(void *x) { // Receive video - struct gamepad_thread_context *info = (struct gamepad_thread_context *) x; - unsigned char data[2048]; + gamepad_context_t *info = (gamepad_context_t *) x; ssize_t size; + // Set up IDR nals on video_packet + uint8_t *nals_current = video_packet; + + static const char *frame_start_word = "\x00\x00\x00\x01"; + memcpy(nals_current, frame_start_word, 4); + nals_current += 4; + generated_sps_params_size += 4; + + size_t generated_sps = generate_sps_params(nals_current, sizeof(video_packet)); + nals_current += generated_sps; + generated_sps_params_size += generated_sps; + + memcpy(nals_current, VANILLA_PPS_PARAMS, sizeof(VANILLA_PPS_PARAMS)); + nals_current += sizeof(VANILLA_PPS_PARAMS); + pthread_mutex_init(&video_mutex, NULL); do { - size = recv(info->socket_vid, data, sizeof(data), 0); + VideoPacket *vp = &video_packet_cache[video_packet_cache_index % VIDEO_PACKET_CACHE_MAX]; + video_packet_cache_index++; + void *data = vp; + + size = recv(info->socket_vid, data, sizeof(VideoPacket), 0); if (size > 0) { if (is_stop_code(data, size)) break; - handle_video_packet(info->event_handler, info->context, data, size, info->socket_msg); + handle_video_packet(info, vp, size, info->socket_msg); } } while (!is_interrupted()); @@ -208,3 +230,257 @@ void *listen_video(void *x) return NULL; } + +void write_bits(void *data, size_t buffer_size, size_t *bit_index, uint8_t value, size_t bit_width) +{ + const size_t size_of_byte = 8; + + assert(bit_width <= size_of_byte); + + // Calculate offsets + size_t offset = *bit_index; + uint8_t *bytes = (uint8_t *) data; + size_t byte_offset = offset / size_of_byte; + + // NOTE: If this was big endian, the value would need to be shifted to the upper most 8 bits manually + + // Shift to the right for placing into buffer + size_t local_bit_offset = offset - (byte_offset * size_of_byte); + size_t remainder = size_of_byte - local_bit_offset; + + // Put bits into the right place in the uint16 + // Promote value to a 16-bit buffer (in case it crosses an 8-bit alignment boundary) + uint16_t shifted = value << (size_of_byte - bit_width) >> local_bit_offset; + + // Clear any non-zero bits if necessary + bytes[byte_offset] = (bytes[byte_offset] >> remainder) << remainder; + + // Put bits into buffer + if (buffer_size - byte_offset > 1) { + bytes[byte_offset + 1] = 0; + uint16_t *words = (uint16_t *) (&bytes[byte_offset]); + *words |= shifted; + } else { + uint8_t shifted_byte = shifted; + // NOTE: If this was big endian, we'd need to get the upper 8 bits manually + bytes[byte_offset] |= shifted_byte; + } + + // Increment bit counter by bits + offset += bit_width; + *bit_index = offset; +} + +void write_exp_golomb(void *data, size_t buffer_size, size_t *bit_index, uint64_t value) +{ + const size_t size_of_byte = 8; + + // x + 1 + uint64_t exp_golomb_value = value + 1; + + // Count how many bits are in this byte + int leading_zeros = 0; + const size_t num_bits = sizeof(value)*size_of_byte; + for (size_t i = 0; i < num_bits; i++) { + if (exp_golomb_value & (1ULL << (num_bits - i - 1))) { + break; + } else { + leading_zeros++; + } + } + + int bit_width = ((sizeof(value) * size_of_byte) - leading_zeros); + + int exp_golomb_leading_zeros = bit_width - 1; + + for (int i = 0; i < exp_golomb_leading_zeros; i += size_of_byte) { + write_bits(data, buffer_size, bit_index, 0, MIN(size_of_byte, exp_golomb_leading_zeros - i)); + } + + int total_bytes = (bit_width / size_of_byte); + if (bit_width % size_of_byte != 0) { + total_bytes++; + } + for (int i = 0; i < total_bytes; i++) { + int write_count; + if (i == 0 && bit_width % size_of_byte != 0) { + write_count = bit_width % size_of_byte; + } else { + write_count = MIN(bit_width, size_of_byte); + } + + uint8_t byte = exp_golomb_value >> ((total_bytes - 1 - i) * size_of_byte); + + write_bits(data, buffer_size, bit_index, byte, write_count); + } +} + +size_t generate_sps_params(void *data, size_t size) +{ + // + // Reference: https://www.cardinalpeak.com/blog/the-h-264-sequence-parameter-set + // + + size_t bit_index = 0; + + // forbidden_zero_bit + write_bits(data, size, &bit_index, 0, 1); + + // nal_ref_idc = 3 (important/SPS) + write_bits(data, size, &bit_index, 3, 2); + + // nal_unit_type = 7 (SPS) + write_bits(data, size, &bit_index, 7, 5); + + // profile_idc = 100 (not sure if this is correct, seems to work) + write_bits(data, size, &bit_index, 100, 8); + + // constraint_set0_flag + write_bits(data, size, &bit_index, 0, 1); + + // constraint_set1_flag + write_bits(data, size, &bit_index, 0, 1); + + // constraint_set2_flag + write_bits(data, size, &bit_index, 0, 1); + + // constraint_set3_flag + write_bits(data, size, &bit_index, 0, 1); + + // constraint_set4_flag + write_bits(data, size, &bit_index, 0, 1); + + // constraint_set5_flag + write_bits(data, size, &bit_index, 0, 1); + + // reserved_zero_2bits + write_bits(data, size, &bit_index, 0, 2); + + // level_idc (not sure if this is correct, seems to work) + write_bits(data, size, &bit_index, 0x20, 8); + + // seq_parameter_set_id + write_exp_golomb(data, size, &bit_index, 0); + + // chroma_format_idc + write_exp_golomb(data, size, &bit_index, 1); + + // bit_depth_luma_minus8 + write_exp_golomb(data, size, &bit_index, 0); + + // bit_depth_chroma_minus8 + write_exp_golomb(data, size, &bit_index, 0); + + // qpprime_y_zero_transform_bypass_flag + write_bits(data, size, &bit_index, 0, 1); + + // seq_scaling_matrix_present_flag + write_bits(data, size, &bit_index, 0, 1); + + // log2_max_frame_num_minus4 + write_exp_golomb(data, size, &bit_index, 4); + + // pic_order_cnt_type + write_exp_golomb(data, size, &bit_index, 2); + + // max_num_ref_frames + write_exp_golomb(data, size, &bit_index, 1); + + // gaps_in_frame_num_value_allowed_flag + write_bits(data, size, &bit_index, 0, 1); + + // pic_width_in_mbs_minus1 + write_exp_golomb(data, size, &bit_index, 53); + + // pic_height_in_map_units_minus1 + write_exp_golomb(data, size, &bit_index, 29); + + // frame_mbs_only_flag + write_bits(data, size, &bit_index, 1, 1); + + // direct_8x8_inference_flag + write_bits(data, size, &bit_index, 1, 1); + + // frame_cropping_flag + write_bits(data, size, &bit_index, 1, 1); + + // frame_crop_left_offset + write_exp_golomb(data, size, &bit_index, 0); + + // frame_crop_right_offset + write_exp_golomb(data, size, &bit_index, 5); + + // frame_crop_top_offset + write_exp_golomb(data, size, &bit_index, 0); + + // frame_crop_bottom_offset + write_exp_golomb(data, size, &bit_index, 0); + + // vui_parameters_present_flag + int enable_vui = 1; + write_bits(data, size, &bit_index, enable_vui, 1); + + if (enable_vui) { + // aspect_ratio_info_present_flag + write_bits(data, size, &bit_index, 0, 1); + + // overscan_info_present_flag + write_bits(data, size, &bit_index, 0, 1); + + // video_signal_type_present_flag + write_bits(data, size, &bit_index, 0, 1); + + // chroma_loc_info_present_flag + write_bits(data, size, &bit_index, 0, 1); + + // timing_info_present_flag + write_bits(data, size, &bit_index, 0, 1); + + // nal_hrd_parameters_present_flag + write_bits(data, size, &bit_index, 0, 1); + + // vcl_hrd_parameters_present_flag + write_bits(data, size, &bit_index, 0, 1); + + // pic_struct_present_flag + write_bits(data, size, &bit_index, 0, 1); + + // bitstream_restriction_flag + write_bits(data, size, &bit_index, 1, 1); + + // bitstream_restriction: + { + // motion_vectors_over_pic_boundaries_flag + write_bits(data, size, &bit_index, 1, 1); + + // max_bytes_per_pic_denom + write_exp_golomb(data, size, &bit_index, 2); + + // max_bits_per_mb_denom + write_exp_golomb(data, size, &bit_index, 1); + + // log2_max_mv_length_horizontal + write_exp_golomb(data, size, &bit_index, 16); + + // log2_max_mv_length_vertical + write_exp_golomb(data, size, &bit_index, 16); + + // max_num_reorder_frames + write_exp_golomb(data, size, &bit_index, 0); + + // max_dec_frame_buffering + write_exp_golomb(data, size, &bit_index, 1); + } + } + + // RBSP trailing stop bit + write_bits(data, size, &bit_index, 1, 1); + + // Alignment + const int align = 8; + if (bit_index % align != 0) { + write_bits(data, size, &bit_index, 0, align - (bit_index % align)); + } + + return bit_index / align; +} \ No newline at end of file diff --git a/lib/gamepad/video.h b/lib/gamepad/video.h index cc0002e..d0e29fc 100644 --- a/lib/gamepad/video.h +++ b/lib/gamepad/video.h @@ -2,8 +2,12 @@ #define GAMEPAD_VIDEO_H #include +#include void *listen_video(void *x); void request_idr(); +size_t generate_sps_params(void *data, size_t size); +void write_bits(void *data, size_t size, size_t *bit_index, uint8_t value, size_t bit_width); +void write_exp_golomb(void *data, size_t buffer_size, size_t *bit_index, uint64_t value); #endif // GAMEPAD_VIDEO_H \ No newline at end of file diff --git a/lib/test/bittest.c b/lib/test/bittest.c new file mode 100644 index 0000000..219717d --- /dev/null +++ b/lib/test/bittest.c @@ -0,0 +1,195 @@ +/** + * Small unit test for ensuring our SPS/PPS bit writing functions are accurate + */ + +#include +#include +#include +#include + +#include "gamepad/video.h" + +int simple() +{ + uint8_t test = 0; + size_t offset = 0; + + write_bits(&test, sizeof(test), &offset, 1, 1); + + if (test == 128) { + printf("SUCCESS\n"); + return 0; + } else { + printf("FAIL (got %i)\n", test); + return 1; + } +} + +int simple_double() +{ + uint8_t test = 0; + size_t offset = 0; + + write_bits(&test, sizeof(test), &offset, 1, 1); + + offset = 7; + write_bits(&test, sizeof(test), &offset, 1, 1); + + if (test != 129) { + printf("FAIL (got %i)\n", test); + return 1; + } + + printf("SUCCESS\n"); + return 0; +} + +int simple_exp_golomb() +{ + uint8_t test = 0; + size_t index = 0; + + write_exp_golomb(&test, sizeof(test), &index, 1); + + if (test != 64) { + printf("FAIL (got %i)\n", test); + return 1; + } + + printf("SUCCESS\n"); + return 0; +} + +int complex_exp_golomb() +{ + // 00000110110 + uint16_t test = 0; + size_t index = 5; + + write_exp_golomb(&test, sizeof(test), &index, 53); + + test = __bswap_16(test); + + if (test != 0b110110) { + printf("FAIL (got %x)\n", test); + return 1; + } + + printf("SUCCESS\n"); + return 0; +} + +int complex_bit_test() +{ + const char *expected = "\x67\x64\x00\x20"; + uint8_t data[4]; + size_t bit_index = 0; + + // forbidden_zero_bit + write_bits(data, sizeof(data), &bit_index, 0, 1); + + // nal_ref_idc = 3 (important/SPS) + write_bits(data, sizeof(data), &bit_index, 3, 2); + + // nal_unit_type = 7 (SPS) + write_bits(data, sizeof(data), &bit_index, 7, 5); + + // profile_idc = 100 (not sure if this is correct, seems to work) + write_bits(data, sizeof(data), &bit_index, 100, 8); + + // constraint_set0_flag + write_bits(data, sizeof(data), &bit_index, 0, 1); + + // constraint_set1_flag + write_bits(data, sizeof(data), &bit_index, 0, 1); + + // constraint_set2_flag + write_bits(data, sizeof(data), &bit_index, 0, 1); + + // constraint_set3_flag + write_bits(data, sizeof(data), &bit_index, 0, 1); + + // constraint_set4_flag + write_bits(data, sizeof(data), &bit_index, 0, 1); + + // constraint_set5_flag + write_bits(data, sizeof(data), &bit_index, 0, 1); + + // reserved_zero_2bits + write_bits(data, sizeof(data), &bit_index, 0, 2); + + // level_idc (not sure if this is correct, seems to work) + write_bits(data, sizeof(data), &bit_index, 0x20, 8); + + for (int i = 0; i < sizeof(data); i++) { + if (i > 0) { + printf(" "); + } + printf("%X", data[i] & 0xFF); + } + printf("\n"); + + if (!memcmp(expected, data, sizeof(data))) { + printf("SUCCESS\n"); + return 0; + } else { + printf("FAIL\n"); + return 1; + } +} + +int full_test() +{ + const char *expected = "\x67\x64\x00\x20\xAC\x2B\x40\x6C\x1E\xF3\x68"; + + //uint8_t buffer[0xB]; + uint8_t buffer[0x100]; + size_t size = generate_sps_params(buffer, sizeof(buffer)); + + for (int i = 0; i < sizeof(buffer); i++) { + if (i > 0) { + printf(" "); + } + printf("%02X", buffer[i] & 0xFF); + } + printf("\n"); + printf("Size: %zu\n", size); + + /*if (!memcmp(expected, buffer, sizeof(buffer))) { + printf("SUCCESS\n"); + return 0; + } else { + printf("FAIL\n"); + return 1; + }*/ + return 0; +} + +int main() +{ + if (simple()) { + return 1; + } + + if (simple_double()) { + return 1; + } + + if (complex_bit_test()) { + return 1; + } + + if (simple_exp_golomb()) { + return 1; + } + + if (complex_exp_golomb()) { + return 1; + } + + if (full_test()) { + return 1; + } + + return 0; +} \ No newline at end of file diff --git a/lib/util.c b/lib/util.c index 3afe0b1..53c92ed 100644 --- a/lib/util.c +++ b/lib/util.c @@ -1,8 +1,10 @@ #include "util.h" +#include #include #include #include +#include #include #include "status.h" @@ -62,3 +64,32 @@ uint16_t crc16(const void *data, size_t len) return crc; } + +size_t get_millis() +{ + size_t ms; // Milliseconds + size_t s; // Seconds + struct timespec spec; + + clock_gettime(CLOCK_REALTIME, &spec); + + s = spec.tv_sec; + ms = round(spec.tv_nsec / 1.0e6); // Convert nanoseconds to milliseconds + if (ms > 999) { + s++; + ms = 0; + } + + return (s * 1000) + ms; +} + +unsigned int reverse_bits(unsigned int b, int bit_count) +{ + unsigned int result = 0; + + for (int i = 0; i < bit_count; i++) { + result |= ((b >> i) & 1) << (bit_count - 1 -i ); + } + + return result; +} \ No newline at end of file diff --git a/lib/util.h b/lib/util.h index db2bf33..8312408 100644 --- a/lib/util.h +++ b/lib/util.h @@ -19,6 +19,8 @@ int is_interrupted(); void force_interrupt(); void install_interrupt_handler(); void uninstall_interrupt_handler(); +size_t get_millis(); +unsigned int reverse_bits(unsigned int b, int bit_count); uint16_t crc16(const void* data, size_t len); diff --git a/lib/vanilla.c b/lib/vanilla.c index d33f617..543ae26 100644 --- a/lib/vanilla.c +++ b/lib/vanilla.c @@ -1,7 +1,10 @@ +#define _GNU_SOURCE + #include "vanilla.h" #include #include +#include #include #include @@ -14,29 +17,70 @@ #include "vanilla.h" pthread_mutex_t main_mutex = PTHREAD_MUTEX_INITIALIZER; +pthread_mutex_t gamepad_mutex = PTHREAD_MUTEX_INITIALIZER; +event_loop_t event_loop = {{0}, 0, 0, 0, PTHREAD_MUTEX_INITIALIZER, PTHREAD_COND_INITIALIZER}; -int vanilla_start(vanilla_event_handler_t event_handler, void *context) +struct gamepad_data_t { - if (pthread_mutex_trylock(&main_mutex) == 0) { - int r = connect_as_gamepad_internal(event_handler, context, 0); - pthread_mutex_unlock(&main_mutex); - return r; - } else { - return VANILLA_ERROR; - } + uint32_t server_address; +}; + +void *start_gamepad(void *arg) +{ + struct gamepad_data_t *data = (struct gamepad_data_t *) arg; + + pthread_mutex_lock(&event_loop.mutex); + event_loop.active = 1; + event_loop.new_index = 0; + event_loop.used_index = 0; + pthread_cond_broadcast(&event_loop.waitcond); + pthread_mutex_unlock(&event_loop.mutex); + + connect_as_gamepad_internal(&event_loop, data->server_address); + + free(data); + + pthread_mutex_lock(&event_loop.mutex); + event_loop.active = 0; + pthread_cond_broadcast(&event_loop.waitcond); + pthread_mutex_unlock(&event_loop.mutex); + + pthread_mutex_unlock(&main_mutex); + return 0; } -int vanilla_start_udp(vanilla_event_handler_t event_handler, void *context, uint32_t server_address) +int vanilla_start_internal(uint32_t server_address) { if (pthread_mutex_trylock(&main_mutex) == 0) { - int r = connect_as_gamepad_internal(event_handler, context, server_address); - pthread_mutex_unlock(&main_mutex); - return r; + pthread_t other; + + struct gamepad_data_t *data = malloc(sizeof(struct gamepad_data_t)); + data->server_address = server_address; + + // Lock event loop mutex so it can't be set to active until we're ready + pthread_mutex_lock(&event_loop.mutex); + + // Start other thread (which will set event loop to active) + pthread_create(&other, NULL, start_gamepad, data); + pthread_setname_np(other, "vanilla-gamepad"); + + // Wait for event loop to be set active before returning + while (!event_loop.active) { + pthread_cond_wait(&event_loop.waitcond, &event_loop.mutex); + } + pthread_mutex_unlock(&event_loop.mutex); + + return VANILLA_SUCCESS; } else { return VANILLA_ERROR; } } +int vanilla_start(uint32_t server_address) +{ + return vanilla_start_internal(server_address); +} + void vanilla_stop() { // Signal to all other threads to exit gracefully @@ -116,4 +160,14 @@ void vanilla_set_battery_status(int battery_status) int vanilla_sync(uint16_t code, uint32_t server_address) { return sync_internal(code, server_address); +} + +int vanilla_poll_event(vanilla_event_t *event) +{ + return get_event(&event_loop, event, 0); +} + +int vanilla_wait_event(vanilla_event_t *event) +{ + return get_event(&event_loop, event, 1); } \ No newline at end of file diff --git a/lib/vanilla.h b/lib/vanilla.h index ca908b9..4f77e93 100644 --- a/lib/vanilla.h +++ b/lib/vanilla.h @@ -60,6 +60,7 @@ enum VanillaGamepadButtons enum VanillaEvent { + VANILLA_EVENT_NONE, VANILLA_EVENT_VIDEO, VANILLA_EVENT_AUDIO, VANILLA_EVENT_VIBRATE @@ -94,19 +95,22 @@ static const uint8_t VANILLA_PPS_PARAMS[] = { 0x00, 0x00, 0x00, 0x01, 0x68, 0xee, 0x06, 0x0c, 0xe8 }; -/** - * Event handler used by caller to receive events - */ -typedef void (*vanilla_event_handler_t)(void *context, int event_type, const char *data, size_t data_size); +typedef struct +{ + int type; + uint8_t data[65536]; + size_t size; +} vanilla_event_t; /** * Start listening for gamepad commands */ -int vanilla_start(vanilla_event_handler_t event_handler, void *context); -int vanilla_start_udp(vanilla_event_handler_t event_handler, void *context, uint32_t server_address); - +int vanilla_start(uint32_t server_address); int vanilla_sync(uint16_t code, uint32_t server_address); +int vanilla_poll_event(vanilla_event_t *event); +int vanilla_wait_event(vanilla_event_t *event); + /** * Attempt to stop the current action * diff --git a/pipe/linux/CMakeLists.txt b/pipe/linux/CMakeLists.txt index b675618..0113594 100644 --- a/pipe/linux/CMakeLists.txt +++ b/pipe/linux/CMakeLists.txt @@ -18,20 +18,20 @@ add_custom_target( ) add_custom_target( wpa_supplicant_build - COMMAND make -j$$(nproc) && cp wpa_supplicant wpa_supplicant_drc + COMMAND make -j$$(nproc) WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/hostap/wpa_supplicant" - BYPRODUCTS "${CMAKE_CURRENT_SOURCE_DIR}/hostap/wpa_supplicant/wpa_supplicant_drc" + BYPRODUCTS "${CMAKE_CURRENT_SOURCE_DIR}/hostap/wpa_supplicant/wpa_supplicant" USES_TERMINAL DEPENDS wpa_supplicant_configure ) add_executable(wpa_supplicant IMPORTED) add_dependencies(wpa_supplicant wpa_supplicant_build) set_target_properties(wpa_supplicant PROPERTIES - IMPORTED_LOCATION "${CMAKE_CURRENT_SOURCE_DIR}/hostap/wpa_supplicant/wpa_supplicant_drc" + IMPORTED_LOCATION "${CMAKE_CURRENT_SOURCE_DIR}/hostap/wpa_supplicant/wpa_supplicant" ) add_custom_command( TARGET wpa_supplicant_build POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/hostap/wpa_supplicant/wpa_supplicant_drc" "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}" + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/hostap/wpa_supplicant/wpa_supplicant" "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/wpa_supplicant_drc" DEPENDS wpa_supplicant_build ) diff --git a/pipe/linux/wpa.c b/pipe/linux/wpa.c index 95e00c5..16b76a2 100644 --- a/pipe/linux/wpa.c +++ b/pipe/linux/wpa.c @@ -981,6 +981,10 @@ void *vanilla_connect_to_console(void *data) void vanilla_listen(const char *wireless_interface) { int skt = open_socket(VANILLA_PIPE_CMD_SERVER_PORT); + if (skt == -1) { + pprint("Failed to open server socket\n"); + return; + } uint32_t control_code; struct sockaddr_in addr = {0}; diff --git a/rpi/CMakeLists.txt b/rpi/CMakeLists.txt new file mode 100644 index 0000000..21747a1 --- /dev/null +++ b/rpi/CMakeLists.txt @@ -0,0 +1,31 @@ +find_package(SDL2 REQUIRED) +#find_package(SDL2_ttf REQUIRED) +find_package(FFmpeg REQUIRED COMPONENTS avformat avcodec avutil avfilter swscale) + +add_executable(vanilla-pi + drm.c + main.c +) + +install(TARGETS vanilla-pi) + +target_include_directories(vanilla-pi PRIVATE + "${CMAKE_SOURCE_DIR}/lib" + + # xf86drm.h needs this + /usr/include/libdrm +) + +target_link_libraries(vanilla-pi PRIVATE + SDL2::SDL2 +# SDL2_ttf::SDL2_ttf + vanilla + FFmpeg::avfilter + FFmpeg::avformat + FFmpeg::avcodec + FFmpeg::avutil + FFmpeg::swscale + + drm + m +) diff --git a/rpi/drm.c b/rpi/drm.c new file mode 100644 index 0000000..3d4c39d --- /dev/null +++ b/rpi/drm.c @@ -0,0 +1,269 @@ +#include "drm.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#define ALIGN(x, a) ((x) + (a - 1)) & (~(a - 1)) + +int set_tty(int mode) +{ + /*int tty_fd = open("/dev/tty0", O_RDWR); + if (tty_fd == -1) { + fprintf(stderr, "Failed to open /dev/tty\n"); + return 0; + } + if (ioctl(tty_fd, KDSETMODE, mode) < 0) { + fprintf(stderr, "Failed to set KDSETMODE: %s (%i)\n", strerror(errno), errno); + return 0; + } + close(tty_fd);*/ + + /*if (ioctl(STDIN_FILENO, KDSETMODE, mode) < 0) { + fprintf(stderr, "Failed to set KDSETMODE: %s (%i)\n", strerror(errno), errno); + return 0; + }*/ + + return 1; +} + +int initialize_drm(vanilla_drm_ctx_t *ctx) +{ + // Open DRM + ctx->fd = drmOpen("vc4", 0); + if (ctx->fd == -1) { + return 0; + } + + if (!set_tty(KD_GRAPHICS)) { + return 0; + } + + int ret = 0; + + // Find DRM output + drmModeResPtr res = drmModeGetResources(ctx->fd); + + for (int i = 0; i < res->count_connectors; i++) { + drmModeConnectorPtr c = drmModeGetConnector(ctx->fd, res->connectors[i]); + if (c->encoder_id) { + drmModeEncoderPtr enc = drmModeGetEncoder(ctx->fd, c->encoder_id); + if (enc->crtc_id) { + drmModeCrtcPtr crtc = drmModeGetCrtc(ctx->fd, enc->crtc_id); + + // Good! We can use this connector :) + ctx->crtc = crtc->crtc_id; + + for (int j = 0; j < res->count_crtcs; j++) { + if (res->crtcs[j] == crtc->crtc_id) { + ctx->crtc_index = j; + break; + } + } + + ret = 1; + + drmModeFreeCrtc(crtc); + } + drmModeFreeEncoder(enc); + } + drmModeFreeConnector(c); + } + + // Free DRM resources + drmModeFreeResources(res); + + ctx->got_plane = 0; + ctx->got_fb = 0; + ctx->got_handles = 0; + memset(ctx->handles, 0, sizeof(ctx->handles)); + + return ret; +} + +int free_drm(vanilla_drm_ctx_t *ctx) +{ + if (ctx->got_fb) { + drmModeRmFB(ctx->fd, ctx->fb_id); + } + + set_tty(KD_TEXT); + + // Close DRM + drmClose(ctx->fd); +} + +static int find_plane(const int drmfd, const int crtcidx, const uint32_t format, + uint32_t *const pplane_id) +{ + drmModePlaneResPtr planes; + drmModePlanePtr plane; + unsigned int i; + unsigned int j; + int ret = 0; + + planes = drmModeGetPlaneResources(drmfd); + if (!planes) { + fprintf(stderr, "drmModeGetPlaneResources failed: %s\n", strerror(errno)); + return -1; + } + + for (i = 0; i < planes->count_planes; ++i) { + plane = drmModeGetPlane(drmfd, planes->planes[i]); + if (!plane) { + fprintf(stderr, "drmModeGetPlane failed: %s\n", strerror(errno)); + break; + } + + if (!(plane->possible_crtcs & (1 << crtcidx))) { + drmModeFreePlane(plane); + continue; + } + + for (j = 0; j < plane->count_formats; ++j) { + if (plane->formats[j] == format) break; + } + + if (j == plane->count_formats) { + drmModeFreePlane(plane); + continue; + } + + *pplane_id = plane->plane_id; + drmModeFreePlane(plane); + break; + } + + if (i == planes->count_planes) ret = -1; + + drmModeFreePlaneResources(planes); + return ret; +} + +extern int running; +int display_drm(vanilla_drm_ctx_t *ctx, AVFrame *frame) +{ + const AVDRMFrameDescriptor *desc = (AVDRMFrameDescriptor *) frame->data[0]; + const uint32_t format = desc->layers[0].format; + + if (!ctx->got_plane) { + if (find_plane(ctx->fd, ctx->crtc_index, format, &ctx->plane_id) < 0) { + fprintf(stderr, "Failed to find plane for format: %x\n", format, DRM_FORMAT_YUV420, frame->format); + return 0; + } else { + ctx->got_plane = 1; + } + } + + /*{ + drmVBlank vbl; + vbl.request.type = DRM_VBLANK_RELATIVE; + vbl.request.sequence = 0; + while (running && drmWaitVBlank(ctx->fd, &vbl)) { + // Not sure what this does, stole it from hello_drmprime + if (errno != EINTR) { + break; + } + } + } + + if (!running) { + return 1; + }*/ + + uint32_t pitches[AV_DRM_MAX_PLANES] = {0}; + uint32_t offsets[AV_DRM_MAX_PLANES] = {0}; + uint32_t bo_handles[AV_DRM_MAX_PLANES] = {0}; + uint64_t modifiers[AV_DRM_MAX_PLANES] = {0}; + + uint32_t new_handles[AV_DRM_MAX_PLANES] = {0}; + + for (int i = 0; i < desc->nb_objects; i++) { + if (drmPrimeFDToHandle(ctx->fd, desc->objects[i].fd, &new_handles[i]) != 0) { + fprintf(stderr, "Failed to get handle from file descriptor: %s\n", strerror(errno)); + return 0; + } + } + + int n = 0; + for (int i = 0; i < desc->nb_layers; i++) { + const AVDRMLayerDescriptor *layer = &desc->layers[i]; + for (int j = 0; j < layer->nb_planes; j++) { + const AVDRMPlaneDescriptor *plane = &layer->planes[j]; + const AVDRMObjectDescriptor *obj = &desc->objects[plane->object_index]; + + pitches[n] = plane->pitch; + offsets[n] = plane->offset; + modifiers[n] = obj->format_modifier; + bo_handles[n] = new_handles[plane->object_index]; + + n++; + } + } + + /*fprintf(stderr, "%dx%d, fmt: %x, boh=%d,%d,%d,%d, pitch=%d,%d,%d,%d," + " offset=%d,%d,%d,%d, mod=%llx,%llx,%llx,%llx\n", + frame->width, + frame->height, + desc->layers[0].format, + bo_handles[0], + bo_handles[1], + bo_handles[2], + bo_handles[3], + pitches[0], + pitches[1], + pitches[2], + pitches[3], + offsets[0], + offsets[1], + offsets[2], + offsets[3], + (long long)modifiers[0], + (long long)modifiers[1], + (long long)modifiers[2], + (long long)modifiers[3] + );*/ + + uint32_t new_fb; + if (drmModeAddFB2WithModifiers(ctx->fd, + frame->width, frame->height, desc->layers[0].format, + bo_handles, pitches, offsets, modifiers, + &new_fb, DRM_MODE_FB_MODIFIERS) != 0) { + fprintf(stderr, "Failed to create framebuffer: %s\n", strerror(errno)); + return 0; + } + + if (drmModeSetPlane(ctx->fd, ctx->plane_id, ctx->crtc, new_fb, 0, + 0, 0, frame->width, frame->height, + 0, 0, frame->width << 16, frame->height << 16) != 0) { + fprintf(stderr, "Failed to set plane: %s\n", strerror(errno)); + return 0; + } + + // Free old framebuffer + if (ctx->got_fb) { + drmModeRmFB(ctx->fd, ctx->fb_id); + } + ctx->fb_id = new_fb; + ctx->got_fb = 1; + + if (ctx->got_handles) { + struct drm_gem_close gem_close = {0}; + for (int i = 0; i < desc->nb_objects; i++) { + if (ctx->handles[i]) { + gem_close.handle = ctx->handles[i]; + drmIoctl(ctx->fd, DRM_IOCTL_GEM_CLOSE, &gem_close); + ctx->handles[i] = 0; + } + } + } + memcpy(ctx->handles, new_handles, sizeof(ctx->handles)); + ctx->got_handles = 1; + + return 1; +} \ No newline at end of file diff --git a/rpi/drm.h b/rpi/drm.h new file mode 100644 index 0000000..12637a6 --- /dev/null +++ b/rpi/drm.h @@ -0,0 +1,26 @@ +#ifndef VANILLA_PI_DRM_H +#define VANILLA_PI_DRM_H + +#include +#include +#include +#include + +typedef struct { + int fd; + uint32_t crtc; + int crtc_index; + uint32_t plane_id; + int got_plane; + uint32_t fb_id; + int got_fb; + uint32_t handles[AV_DRM_MAX_PLANES]; + int got_handles; +} vanilla_drm_ctx_t; + +int set_tty(int mode); +int initialize_drm(vanilla_drm_ctx_t *ctx); +int free_drm(vanilla_drm_ctx_t *ctx); +int display_drm(vanilla_drm_ctx_t *ctx, AVFrame *frame); + +#endif // VANILLA_PI_DRM_H \ No newline at end of file diff --git a/rpi/main.c b/rpi/main.c new file mode 100644 index 0000000..12349c1 --- /dev/null +++ b/rpi/main.c @@ -0,0 +1,491 @@ +#define SCREEN_WIDTH 854 +#define SCREEN_HEIGHT 480 + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "drm.h" + +AVFrame *present_frame; +AVFrame *decoding_frame; +pthread_mutex_t decoding_mutex; +pthread_cond_t decoding_wait_cond; +AVCodecContext *video_codec_ctx; +AVCodecParserContext *video_parser; +AVPacket *video_packet; +int running = 0; + +static int button_map[SDL_CONTROLLER_BUTTON_MAX] = {-1}; +static int axis_map[SDL_CONTROLLER_AXIS_MAX] = {-1}; +static int vibrate = 0; + +// HACK: Easy macro to test between desktop and RPi (even though ARM doesn't necessarily mean RPi) +#ifdef __arm__ +#define RASPBERRY_PI +#endif + +enum AVPixelFormat get_format(AVCodecContext* ctx, const enum AVPixelFormat *pix_fmt) +{ + while (*pix_fmt != AV_PIX_FMT_NONE) { + if (*pix_fmt == AV_PIX_FMT_DRM_PRIME) { + return AV_PIX_FMT_DRM_PRIME; + } + pix_fmt++; + } + + return AV_PIX_FMT_NONE; +} + +int decode(const void *data, size_t size) +{ + int err; + + // Parse this data for packets + err = av_packet_from_data(video_packet, (uint8_t *) data, size); + if (err < 0) { + fprintf(stderr, "Failed to initialize AVPacket from data: %s (%i)\n", av_err2str(err), err); + return 0; + } + + int64_t ticks = SDL_GetTicks64(); + + // Send packet to decoder + err = avcodec_send_packet(video_codec_ctx, video_packet); + if (err < 0) { + fprintf(stderr, "Failed to send packet to decoder: %s (%i)\n", av_err2str(err), err); + return 0; + } + + int ret = 1; + + // Retrieve frame from decoder + while (1) { + err = avcodec_receive_frame(video_codec_ctx, decoding_frame); + if (err == AVERROR(EAGAIN)) { + // Decoder wants another packet before it can output a frame. Silently exit. + break; + } else if (err < 0) { + fprintf(stderr, "Failed to receive frame from decoder: %i\n", err); + ret = 0; + break; + } else if (!decoding_frame->data[0]) { + fprintf(stderr, "WE GOT A NULL DATA[0] STRAIGHT FROM THE DECODER?????\n"); + abort(); + } else if ((decoding_frame->flags & AV_FRAME_FLAG_CORRUPT) != 0) { + fprintf(stderr, "GOT A CORRUPT FRAME??????\n"); + abort(); + } else { + pthread_mutex_lock(&decoding_mutex); + + // Swap refs from decoding_frame to present_frame + av_frame_unref(present_frame); + av_frame_move_ref(present_frame, decoding_frame); + + pthread_cond_broadcast(&decoding_wait_cond); + pthread_mutex_unlock(&decoding_mutex); + } + } + + return ret; +} + +static SDL_mutex *decode_loop_mutex; +static SDL_cond *decode_loop_cond; +static int decode_loop_running = 0; +static int decode_pkt_ready = 0; +static vanilla_event_t decode_event; +int decode_loop(void *) +{ + vanilla_event_t our_pkt; + + SDL_LockMutex(decode_loop_mutex); + while (decode_loop_running) { + while (!decode_pkt_ready) { + SDL_CondWait(decode_loop_cond, decode_loop_mutex); + } + + our_pkt = decode_event; + decode_pkt_ready = 0; + + SDL_UnlockMutex(decode_loop_mutex); + + decode(our_pkt.data, our_pkt.size); + + SDL_LockMutex(decode_loop_mutex); + } + SDL_UnlockMutex(decode_loop_mutex); +} + +int run_backend(void *data) +{ + SDL_AudioSpec desired = {0}; + desired.freq = 48000; + desired.format = AUDIO_S16LSB; + desired.channels = 2; + + SDL_AudioDeviceID audio = SDL_OpenAudioDevice(NULL, 0, &desired, NULL, 0); + if (audio) { + SDL_PauseAudioDevice(audio, 0); + } else { + printf("Failed to open audio device\n"); + } + + vanilla_event_t event; + + decode_loop_mutex = SDL_CreateMutex(); + decode_loop_cond = SDL_CreateCond(); + decode_loop_running = 1; + + SDL_Thread *decode_thread = SDL_CreateThread(decode_loop, "vanilla-decode", NULL); + + while (vanilla_wait_event(&event)) { + if (event.type == VANILLA_EVENT_VIDEO) { + SDL_LockMutex(decode_loop_mutex); + decode_event = event; + decode_pkt_ready = 1; + SDL_CondBroadcast(decode_loop_cond); + SDL_UnlockMutex(decode_loop_mutex); + // decode(event.data, event.size); + } else if (event.type == VANILLA_EVENT_AUDIO) { + if (audio) { + if (SDL_QueueAudio(audio, event.data, event.size) < 0) { + printf("Failed to queue audio\n", event.size); + } + } + } else if (event.type == VANILLA_EVENT_VIBRATE) { + vibrate = event.data[0]; + } + } + + SDL_LockMutex(decode_loop_mutex); + decode_loop_running = 0; + SDL_CondBroadcast(decode_loop_cond); + SDL_UnlockMutex(decode_loop_mutex); + + SDL_WaitThread(decode_thread, NULL); + SDL_DestroyCond(decode_loop_cond); + SDL_DestroyMutex(decode_loop_mutex); + + SDL_CloseAudioDevice(audio); + + return 0; +} + +void logger(const char *s, va_list args) +{ + vfprintf(stderr, s, args); +} + +int display_loop(void *data) +{ + vanilla_drm_ctx_t *drm_ctx = (vanilla_drm_ctx_t *) data; + + Uint32 start_ticks = SDL_GetTicks(); + + #define TICK_MAX 30 + Uint32 tick_deltas[TICK_MAX]; + size_t tick_delta_index = 0; + + AVFrame *frame = av_frame_alloc(); + + pthread_mutex_lock(&decoding_mutex); + + while (running) { + while (running && !present_frame->data[0]) { + pthread_cond_wait(&decoding_wait_cond, &decoding_mutex); + } + + if (!running) { + break; + } + + // If a frame is available, present it here + av_frame_unref(frame); + av_frame_move_ref(frame, present_frame); + + pthread_mutex_unlock(&decoding_mutex); + + display_drm(drm_ctx, frame); + + // FPS counter stuff + Uint32 now = SDL_GetTicks(); + tick_deltas[tick_delta_index] = (now - start_ticks); + start_ticks = now; + tick_delta_index++; + + if (tick_delta_index == TICK_MAX) { + Uint32 total = 0; + for (int i = 0; i < TICK_MAX; i++) { + total += tick_deltas[i]; + } + + fprintf(stderr, "AVERAGE FPS: %.2f\n", 1000.0f / (total / (float) TICK_MAX)); + + tick_delta_index = 0; + } + + pthread_mutex_lock(&decoding_mutex); + } + + pthread_mutex_unlock(&decoding_mutex); + + av_frame_free(&frame); + + return 0; +} + +SDL_GameController *find_valid_controller() +{ + for (int i = 0; i < SDL_NumJoysticks(); i++) { + SDL_GameController *c = SDL_GameControllerOpen(i); + if (c) { + return c; + } + } + + return NULL; +} + +void sigint_handler(int signum) +{ + set_tty(KD_TEXT); + + printf("Received SIGINT...\n"); + running = 0; + + SDL_QuitEvent ev; + ev.type = SDL_QUIT; + ev.timestamp = SDL_GetTicks(); + SDL_PushEvent((SDL_Event *) &ev); +} + +void init_gamepad() +{ + vibrate = 0; + + button_map[SDL_CONTROLLER_BUTTON_A] = VANILLA_BTN_A; + button_map[SDL_CONTROLLER_BUTTON_B] = VANILLA_BTN_B; + button_map[SDL_CONTROLLER_BUTTON_X] = VANILLA_BTN_X; + button_map[SDL_CONTROLLER_BUTTON_Y] = VANILLA_BTN_Y; + button_map[SDL_CONTROLLER_BUTTON_BACK] = VANILLA_BTN_MINUS; + button_map[SDL_CONTROLLER_BUTTON_GUIDE] = VANILLA_BTN_HOME; + button_map[SDL_CONTROLLER_BUTTON_MISC1] = VANILLA_BTN_TV; + button_map[SDL_CONTROLLER_BUTTON_START] = VANILLA_BTN_PLUS; + button_map[SDL_CONTROLLER_BUTTON_LEFTSTICK] = VANILLA_BTN_L3; + button_map[SDL_CONTROLLER_BUTTON_RIGHTSTICK] = VANILLA_BTN_R3; + button_map[SDL_CONTROLLER_BUTTON_LEFTSHOULDER] = VANILLA_BTN_L; + button_map[SDL_CONTROLLER_BUTTON_RIGHTSHOULDER] = VANILLA_BTN_R; + button_map[SDL_CONTROLLER_BUTTON_DPAD_UP] = VANILLA_BTN_UP; + button_map[SDL_CONTROLLER_BUTTON_DPAD_DOWN] = VANILLA_BTN_DOWN; + button_map[SDL_CONTROLLER_BUTTON_DPAD_LEFT] = VANILLA_BTN_LEFT; + button_map[SDL_CONTROLLER_BUTTON_DPAD_RIGHT] = VANILLA_BTN_RIGHT; + axis_map[SDL_CONTROLLER_AXIS_LEFTX] = VANILLA_AXIS_L_X; + axis_map[SDL_CONTROLLER_AXIS_LEFTY] = VANILLA_AXIS_L_Y; + axis_map[SDL_CONTROLLER_AXIS_RIGHTX] = VANILLA_AXIS_R_X; + axis_map[SDL_CONTROLLER_AXIS_RIGHTY] = VANILLA_AXIS_R_Y; + axis_map[SDL_CONTROLLER_AXIS_TRIGGERLEFT] = VANILLA_BTN_ZL; + axis_map[SDL_CONTROLLER_AXIS_TRIGGERRIGHT] = VANILLA_BTN_ZR; +} + +int32_t pack_float(float f) +{ + int32_t x; + memcpy(&x, &f, sizeof(int32_t)); + return x; +} + +int main(int argc, const char **argv) +{ + vanilla_drm_ctx_t drm_ctx; + if (!initialize_drm(&drm_ctx)) { + fprintf(stderr, "Failed to find DRM output\n"); + return 1; + } + + // Initialize SDL2 for audio and input + if (SDL_Init(SDL_INIT_AUDIO | SDL_INIT_GAMECONTROLLER) < 0) { + fprintf(stderr, "Failed to initialize SDL: %s\n", SDL_GetError()); + return 1; + } + + // Initialize FFmpeg +#ifdef RASPBERRY_PI + const AVCodec *codec = avcodec_find_decoder_by_name("h264_v4l2m2m"); +#else + const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264); +#endif + if (!codec) { + fprintf(stderr, "No decoder was available\n"); + return 1; + } + + video_codec_ctx = avcodec_alloc_context3(codec); + if (!video_codec_ctx) { + fprintf(stderr, "Failed to allocate codec context\n"); + return 1; + } + + int ffmpeg_err; + + ffmpeg_err = av_hwdevice_ctx_create(&video_codec_ctx->hw_device_ctx, AV_HWDEVICE_TYPE_DRM, "/dev/dri/card0", NULL, 0); + if (ffmpeg_err < 0) { + fprintf(stderr, "Failed to create hwdevice context: %s (%i)\n", av_err2str(ffmpeg_err), ffmpeg_err); + return 1; + } + + // MAKE SURE WE GET DRM PRIME FRAMES BY OVERRIDING THE GET_FORMAT FUNCTION + video_codec_ctx->get_format = get_format; + + ffmpeg_err = avcodec_open2(video_codec_ctx, codec, NULL); + if (ffmpeg_err < 0) { + fprintf(stderr, "Failed to open decoder: %i\n", ffmpeg_err); + return 1; + } + + decoding_frame = av_frame_alloc(); + present_frame = av_frame_alloc(); + if (!decoding_frame || !present_frame) { + fprintf(stderr, "Failed to allocate AVFrame\n"); + return 1; + } + + video_packet = av_packet_alloc(); + if (!video_packet) { + fprintf(stderr, "Failed to create renderer: %s\n", SDL_GetError()); + return 1; + } + + video_parser = av_parser_init(codec->id); + if (!video_parser) { + fprintf(stderr, "Failed to create codec parser\n"); + exit(1); + } + + // Install logging debugger + vanilla_install_logger(logger); + + // Start Vanilla +#ifdef RASPBERRY_PI + vanilla_start(0); +#else + vanilla_start(ntohl(inet_addr("127.0.0.1"))); +#endif + + // Initialize gamepad lookup tables + init_gamepad(); + + // Create variable for background threads to run + running = 1; + signal(SIGINT, sigint_handler); + + pthread_mutex_init(&decoding_mutex, NULL); + pthread_cond_init(&decoding_wait_cond, NULL); + + // Start background threads + SDL_Thread *backend_thread = SDL_CreateThread(run_backend, "vanilla-pi-backend", NULL); + SDL_Thread *display_thread = SDL_CreateThread(display_loop, "vanilla-pi-display", &drm_ctx); + + SDL_GameController *controller = find_valid_controller(); + + while (running) { + SDL_Event event; + if (SDL_WaitEvent(&event)) { + switch (event.type) { + case SDL_QUIT: + // Exit loop + running = 0; + break; + case SDL_CONTROLLERDEVICEADDED: + // Attempt to find controller if one doesn't exist already + if (!controller) { + controller = find_valid_controller(); + } + break; + case SDL_CONTROLLERDEVICEREMOVED: + // Handle current controller being removed + if (controller && event.cdevice.which == SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(controller))) { + SDL_GameControllerClose(controller); + controller = find_valid_controller(); + } + break; + case SDL_CONTROLLERBUTTONDOWN: + case SDL_CONTROLLERBUTTONUP: + if (controller && event.cdevice.which == SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(controller))) { + int vanilla_btn = button_map[event.cbutton.button]; + if (vanilla_btn != -1) { + vanilla_set_button(vanilla_btn, event.type == SDL_CONTROLLERBUTTONDOWN ? INT16_MAX : 0); + } + } + break; + case SDL_CONTROLLERAXISMOTION: + if (controller && event.cdevice.which == SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(controller))) { + int vanilla_axis = axis_map[event.caxis.axis]; + Sint16 axis_value = event.caxis.value; + if (vanilla_axis != -1) { + vanilla_set_button(vanilla_axis, axis_value); + } + } + break; + case SDL_CONTROLLERSENSORUPDATE: + if (event.csensor.sensor == SDL_SENSOR_ACCEL) { + vanilla_set_button(VANILLA_SENSOR_ACCEL_X, pack_float(event.csensor.data[0])); + vanilla_set_button(VANILLA_SENSOR_ACCEL_Y, pack_float(event.csensor.data[1])); + vanilla_set_button(VANILLA_SENSOR_ACCEL_Z, pack_float(event.csensor.data[2])); + } else if (event.csensor.sensor == SDL_SENSOR_GYRO) { + vanilla_set_button(VANILLA_SENSOR_GYRO_PITCH, pack_float(event.csensor.data[0])); + vanilla_set_button(VANILLA_SENSOR_GYRO_YAW, pack_float(event.csensor.data[1])); + vanilla_set_button(VANILLA_SENSOR_GYRO_ROLL, pack_float(event.csensor.data[2])); + } + break; + case SDL_KEYDOWN: + if (event.key.keysym.sym == SDLK_ESCAPE) { + running = 0; + } + break; + } + + if (controller) { + uint16_t amount = vibrate ? 0xFFFF : 0; + SDL_GameControllerRumble(controller, amount, amount, 0); + } + } + } + + // Terminate background threads and wait for them to end gracefully + vanilla_stop(); + + pthread_mutex_lock(&decoding_mutex); + running = 0; + pthread_cond_broadcast(&decoding_wait_cond); + pthread_mutex_unlock(&decoding_mutex); + + SDL_WaitThread(backend_thread, NULL); + SDL_WaitThread(display_thread, NULL); + + pthread_cond_destroy(&decoding_wait_cond); + pthread_mutex_destroy(&decoding_mutex); + + av_parser_close(video_parser); + av_frame_free(&present_frame); + av_frame_free(&decoding_frame); + av_packet_free(&video_packet); + avcodec_free_context(&video_codec_ctx); + + SDL_Quit(); + + free_drm(&drm_ctx); + + return 0; +} \ No newline at end of file