From b26f81d8e47a0b7cb02d6ae6c17587e3b6bb55eb Mon Sep 17 00:00:00 2001 From: PolygonPrincess Date: Sat, 7 Nov 2026 07:12:19 -0500 Subject: [PATCH 1/8] feat(core): implement libeverlive-core hardware abstraction and everlive-ctl CLI --- build.sh | 11 +- tests/Makefile | 22 +- tests/test_card_registry.c | 399 +++++++++++++++ tests/test_frame_convert.c | 103 ++++ tools/libeverlive-core/Makefile | 29 ++ tools/libeverlive-core/everlive-service.sh | 131 +++++ tools/libeverlive-core/everlive.service | 25 + tools/libeverlive-core/everlive_card.c | 415 ++++++++++++++++ tools/libeverlive-core/everlive_card.h | 208 ++++++++ tools/libeverlive-core/everlive_ctl.c | 542 +++++++++++++++++++++ tools/libeverlive-core/everlive_frame.c | 90 ++++ tools/libeverlive-core/everlive_frame.h | 45 ++ 12 files changed, 2014 insertions(+), 6 deletions(-) create mode 100644 tests/test_card_registry.c create mode 100644 tests/test_frame_convert.c create mode 100644 tools/libeverlive-core/Makefile create mode 100755 tools/libeverlive-core/everlive-service.sh create mode 100644 tools/libeverlive-core/everlive.service create mode 100644 tools/libeverlive-core/everlive_card.c create mode 100644 tools/libeverlive-core/everlive_card.h create mode 100644 tools/libeverlive-core/everlive_ctl.c create mode 100644 tools/libeverlive-core/everlive_frame.c create mode 100644 tools/libeverlive-core/everlive_frame.h diff --git a/build.sh b/build.sh index 567cdbf..6614682 100755 --- a/build.sh +++ b/build.sh @@ -5,9 +5,14 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CORE_DIR="${SCRIPT_DIR}/tools/libeverlive-core" GC573_DIR="${SCRIPT_DIR}/tools/opengc573-userd" GC570D_DIR="${SCRIPT_DIR}/tools/opengc570d-userd" +echo "[*] Baue EverLive Core Hardware Abstraction Library (libeverlive-core & everlive-ctl)..." +make -C "${CORE_DIR}" clean >/dev/null 2>&1 || true +make -C "${CORE_DIR}" -j"$(nproc)" + echo "[*] Baue EverLive GC573 Userspace VFIO Engine (gc573_user_streamer & everlive_userd)..." make -C "${GC573_DIR}" clean >/dev/null 2>&1 || true make -C "${GC573_DIR}" -j"$(nproc)" @@ -21,5 +26,9 @@ echo "[+] EverLive Userspace Engines erfolgreich gebaut!" # Optional: Baue Legacy Direct-Renderer falls Quellcode vorhanden if [ -f "${SCRIPT_DIR}/tools/everlive/everlive_display.c" ]; then echo "[*] Baue Legacy Direct-Renderer (everlive_display)..." - gcc -O3 -march=native -o "${SCRIPT_DIR}/tools/everlive/everlive_display" "${SCRIPT_DIR}/tools/everlive/everlive_display.c" $(sdl2-config --cflags --libs 2>/dev/null || true) -lGL -lm -lX11 -lpthread 2>/dev/null || true + SDL_FLAGS="$(pkg-config --cflags --libs sdl2 2>/dev/null || sdl2-config --cflags --libs 2>/dev/null || true)" + if [ -n "${SDL_FLAGS}" ]; then + gcc -O3 -o "${SCRIPT_DIR}/tools/everlive/everlive_display" "${SCRIPT_DIR}/tools/everlive/everlive_display.c" ${SDL_FLAGS} -lGL -lm -lX11 -lpthread + chmod 0755 "${SCRIPT_DIR}/tools/everlive/everlive_display" 2>/dev/null || true + fi fi diff --git a/tests/Makefile b/tests/Makefile index 0d9c173..f05883a 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -1,12 +1,13 @@ # SPDX-License-Identifier: GPL-2.0-only CC ?= gcc -CFLAGS ?= -O2 -g -Wall -Wextra -Wno-unused-parameter -Wno-sign-compare -std=gnu11 -INCLUDES := -I../tools/opengc570d-userd/src -I../tools/opengc570d-userd +CFLAGS ?= -O2 -g -Wall -Wextra -Wno-unused-parameter -Wno-sign-compare -std=gnu11 -D_GNU_SOURCE +INCLUDES := -I../tools/opengc570d-userd/src -I../tools/opengc570d-userd -I../tools/libeverlive-core DEFS := -DGC570D_USERSPACE DRIVER_DIR := ../tools/opengc570d-userd/src +CORE_DIR := ../tools/libeverlive-core -TARGETS := test_edid test_config +TARGETS := test_edid test_config test_card_registry test_frame_convert all: $(TARGETS) @@ -16,12 +17,23 @@ test_edid: test_edid.c $(DRIVER_DIR)/gc570d-receiver.c $(DRIVER_DIR)/gc570d-spli test_config: test_config.c $(DRIVER_DIR)/gc570d-config.c $(CC) $(CFLAGS) $(INCLUDES) $(DEFS) -o $@ $^ +test_card_registry: test_card_registry.c $(CORE_DIR)/everlive_card.c + $(CC) $(CFLAGS) $(INCLUDES) -o $@ $^ + +test_frame_convert: test_frame_convert.c $(CORE_DIR)/everlive_frame.c + $(CC) $(CFLAGS) $(INCLUDES) -o $@ $^ + +asan: CFLAGS += -fsanitize=address,undefined -fno-omit-frame-pointer +asan: all + test: all - @echo "Running GC570D unit tests..." + @echo "=== Running Unit Test Suites ===" @./test_edid @./test_config + @./test_card_registry + @./test_frame_convert clean: rm -f $(TARGETS) -.PHONY: all test clean +.PHONY: all test clean asan diff --git a/tests/test_card_registry.c b/tests/test_card_registry.c new file mode 100644 index 0000000..38f195e --- /dev/null +++ b/tests/test_card_registry.c @@ -0,0 +1,399 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * test_card_registry.c — Unit tests for libeverlive-core hardware registry and autodiscovery + */ + +#include "everlive_card.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define TEST_PASS(desc) printf(" [PASS] %s\n", (desc)) + +static void test_registry_enumeration(void) +{ + printf("Testing everlive_get_card_registry...\n"); + size_t count = 0; + const struct everlive_device_desc *registry = everlive_get_card_registry(&count); + + assert(registry != NULL); + assert(count >= 3); + TEST_PASS("Registry contains multiple supported card definitions"); + + bool found_gc570d = false; + bool found_gc573 = false; + + for (size_t i = 0; i < count; i++) { + if (strcmp(registry[i].model_slug, "gc570d") == 0) + found_gc570d = true; + if (strcmp(registry[i].model_slug, "gc573") == 0) + found_gc573 = true; + } + + assert(found_gc570d); + assert(found_gc573); + TEST_PASS("Registry includes both GC570D and GC573 descriptors"); +} + +static void test_card_matching(void) +{ + printf("Testing everlive_match_card...\n"); + + /* Exact GC570D match (0x1461, 0x0054, 0x5700) */ + const struct everlive_device_desc *gc570d = + everlive_match_card(0x1461, 0x0054, 0x5700); + assert(gc570d != NULL); + assert(strcmp(gc570d->model_slug, "gc570d") == 0); + assert(gc570d->num_input_ports == 2); + assert(gc570d->capabilities & EVERLIVE_CAP_DUAL_INPUT); + assert(gc570d->capabilities & EVERLIVE_CAP_HARDWARE_SPLITTER); + TEST_PASS("Identified GC570D by subsystem 0x5700 with dual inputs"); + + /* Exact GC573 match (0x1461, 0x0054, 0x5730) */ + const struct everlive_device_desc *gc573_exact = + everlive_match_card(0x1461, 0x0054, 0x5730); + assert(gc573_exact != NULL); + assert(strcmp(gc573_exact->model_slug, "gc573") == 0); + assert(gc573_exact->capabilities & EVERLIVE_CAP_4K60); + assert(gc573_exact->capabilities & EVERLIVE_CAP_1440P120); + assert(gc573_exact->capabilities & EVERLIVE_CAP_FRAMEGEN); + TEST_PASS("Identified GC573 by subsystem 0x5730 with 4K60/1440p120/FrameGen"); + + /* Wildcard GC573 match (0x1461, 0x0054 with non-5700 subsystem) */ + const struct everlive_device_desc *gc573_wildcard = + everlive_match_card(0x1461, 0x0054, 0x0000); + assert(gc573_wildcard != NULL); + assert(strcmp(gc573_wildcard->model_slug, "gc573") == 0); + TEST_PASS("Defaulted 0x0054 to GC573 when subsystem != 0x5700"); + + /* Secondary GC573 / BOLT PCIe ID (0x0055) */ + const struct everlive_device_desc *gc573_alt = + everlive_match_card(0x1461, 0x0055, 0x0000); + assert(gc573_alt != NULL); + assert(strcmp(gc573_alt->model_slug, "gc573") == 0); + TEST_PASS("Identified alternative GC573/BOLT PCIe device ID 0x0055"); + + /* Future GC575 template (0x0057) */ + const struct everlive_device_desc *gc575 = + everlive_match_card(0x1461, 0x0057, 0x0000); + assert(gc575 != NULL); + assert(strcmp(gc575->model_slug, "gc575") == 0); + TEST_PASS("Identified GC575 device ID 0x0057"); + + /* Unknown card rejected */ + const struct everlive_device_desc *unknown = + everlive_match_card(0x10de, 0x1f02, 0x0000); + assert(unknown == NULL); + TEST_PASS("Unknown PCI IDs rejected gracefully"); +} + +static void create_file(const char *path, const char *content) +{ + FILE *f = fopen(path, "w"); + assert(f != NULL); + fputs(content, f); + fclose(f); +} + +static void test_mock_sysfs_scan(void) +{ + printf("Testing everlive_detect_installed_cards with mock sysfs...\n"); + + char tmp_root[] = "/tmp/everlive_sysfs_test_XXXXXX"; + char *res = mkdtemp(tmp_root); + assert(res != NULL); + + /* Mock device 1: GC570D at 0000:03:00.0 */ + char d1[512], d2[512], d3[512]; + snprintf(d1, sizeof(d1), "%s/0000:03:00.0", tmp_root); + snprintf(d2, sizeof(d2), "%s/0000:04:00.0", tmp_root); + snprintf(d3, sizeof(d3), "%s/0000:01:00.0", tmp_root); + + assert(mkdir(d1, 0755) == 0); + assert(mkdir(d2, 0755) == 0); + assert(mkdir(d3, 0755) == 0); + + /* Populate GC570D */ + char p[PATH_MAX]; + snprintf(p, sizeof(p), "%s/vendor", d1); + create_file(p, "0x1461\n"); + snprintf(p, sizeof(p), "%s/device", d1); + create_file(p, "0x0054\n"); + snprintf(p, sizeof(p), "%s/subsystem_device", d1); + create_file(p, "0x5700\n"); + + /* Mock symlink for driver -> vfio-pci */ + snprintf(p, sizeof(p), "%s/driver", d1); + assert(symlink("/sys/bus/pci/drivers/vfio-pci", p) == 0); + + /* Mock symlink for iommu_group -> .../16 */ + snprintf(p, sizeof(p), "%s/iommu_group", d1); + assert(symlink("/sys/kernel/iommu_groups/16", p) == 0); + + /* Populate GC573 at 0000:04:00.0 */ + snprintf(p, sizeof(p), "%s/vendor", d2); + create_file(p, "0x1461\n"); + snprintf(p, sizeof(p), "%s/device", d2); + create_file(p, "0x0054\n"); + snprintf(p, sizeof(p), "%s/subsystem_device", d2); + create_file(p, "0x5730\n"); + + /* Mock symlink for iommu_group -> .../22 */ + snprintf(p, sizeof(p), "%s/iommu_group", d2); + assert(symlink("/sys/kernel/iommu_groups/22", p) == 0); + + /* Populate other non-capture device at 0000:01:00.0 (e.g. Intel GPU) */ + snprintf(p, sizeof(p), "%s/vendor", d3); + create_file(p, "0x8086\n"); + snprintf(p, sizeof(p), "%s/device", d3); + create_file(p, "0x9bc5\n"); + snprintf(p, sizeof(p), "%s/subsystem_device", d3); + create_file(p, "0x103c\n"); + + /* Scan using mock root */ + struct everlive_device found[8]; + int count = everlive_detect_installed_cards(found, 8, tmp_root); + + assert(count == 2); + TEST_PASS("Discovered exactly 2 supported capture cards from mock sysfs"); + + bool saw_gc570d = false; + bool saw_gc573 = false; + + for (int i = 0; i < count; i++) { + if (strcmp(found[i].desc->model_slug, "gc570d") == 0) { + saw_gc570d = true; + assert(strcmp(found[i].pci_bdf, "0000:03:00.0") == 0); + assert(found[i].iommu_group == 16); + assert(found[i].is_vfio_bound == true); + } + if (strcmp(found[i].desc->model_slug, "gc573") == 0) { + saw_gc573 = true; + assert(strcmp(found[i].pci_bdf, "0000:04:00.0") == 0); + assert(found[i].iommu_group == 22); + assert(found[i].is_vfio_bound == false); + } + } + + assert(saw_gc570d); + assert(saw_gc573); + TEST_PASS("Correctly populated BDF, IOMMU group, and VFIO binding state"); + + /* Cleanup mock directory */ + unlink(d1); // in case + char cmd[PATH_MAX + 16]; + snprintf(cmd, sizeof(cmd), "rm -rf '%s'", tmp_root); + int ret = system(cmd); + (void)ret; +} + +static int g_custom_probe_called = 0; +static int mock_custom_probe(struct everlive_device *dev) +{ + (void)dev; + g_custom_probe_called++; + return 0; +} + +static void test_card_ops_binding(void) +{ + printf("Testing everlive_card_ops binding and defaults...\n"); + + const struct everlive_device_desc *gc573_desc = + everlive_match_card(0x1461, 0x0054, 0x5730); + assert(gc573_desc != NULL); + + /* 1. Default ops retrieval */ + const struct everlive_card_ops *default_ops = everlive_card_ops_for(gc573_desc); + assert(default_ops != NULL); + assert(default_ops->probe != NULL); + assert(default_ops->init_hardware != NULL); + assert(default_ops->standby != NULL); + assert(default_ops->release != NULL); + assert(default_ops->set_input_port != NULL); + assert(default_ops->set_edid_profile != NULL); + assert(default_ops->enable_passthrough != NULL); + assert(default_ops->set_led != NULL); + assert(default_ops->get_telemetry != NULL); + TEST_PASS("Default ops returned for known model"); + + /* 2. Default ops return -ENOTSUP / -EINVAL appropriately */ + struct everlive_device dev; + memset(&dev, 0, sizeof(dev)); + assert(default_ops->probe(&dev) == -EINVAL); + + dev.desc = gc573_desc; + snprintf(dev.pci_bdf, sizeof(dev.pci_bdf), "0000:04:00.0"); + assert(default_ops->probe(&dev) == 0); + assert(default_ops->init_hardware(&dev) == -ENOTSUP); + assert(default_ops->standby(&dev) == -ENOTSUP); + assert(default_ops->set_input_port(&dev, 1) == -ENOTSUP); + assert(default_ops->set_edid_profile(&dev, 0, "default", NULL, 0) == -ENOTSUP); + assert(default_ops->enable_passthrough(&dev, true) == -ENOTSUP); + assert(default_ops->set_led(&dev, EVERLIVE_LED_MODE_STATIC, 255, 0, 0) == -ENOTSUP); + + struct everlive_telemetry telem; + memset(&telem, 0xaa, sizeof(telem)); + assert(default_ops->get_telemetry(&dev, &telem) == -ENOTSUP); + assert(telem.frames_captured == 0 && telem.temperature_c == 0.0f); + + default_ops->release(&dev); + TEST_PASS("Default ops behave safely and return -ENOTSUP for hardware actions"); + + /* 3. Register custom backend ops */ + struct everlive_card_ops custom_ops = *default_ops; + custom_ops.probe = mock_custom_probe; + + assert(everlive_register_card_ops("gc573", &custom_ops) == 0); + const struct everlive_card_ops *bound = everlive_card_ops_for(gc573_desc); + assert(bound != NULL); + assert(bound->probe == mock_custom_probe); + + g_custom_probe_called = 0; + assert(bound->probe(&dev) == 0); + assert(g_custom_probe_called == 1); + TEST_PASS("Successfully registered and dispatched custom backend ops"); + + /* 4. Invalid arguments */ + assert(everlive_register_card_ops(NULL, &custom_ops) == -EINVAL); + assert(everlive_register_card_ops("gc573", NULL) == -EINVAL); + assert(everlive_register_card_ops("nonexistent_model", &custom_ops) == -ENOENT); + TEST_PASS("Rejects NULL arguments and unknown model slugs for ops registration"); +} + +static uint64_t timespec_to_ns_test(const struct timespec *ts) +{ + return (uint64_t)ts->tv_sec * 1000000000ULL + (uint64_t)ts->tv_nsec; +} + +static bool check_resume_detected(const struct timespec *mono_now, + const struct timespec *boot_now, + const struct timespec *mono_prev, + const struct timespec *boot_prev) +{ + uint64_t mono_delta = timespec_to_ns_test(mono_now) - timespec_to_ns_test(mono_prev); + uint64_t boot_delta = timespec_to_ns_test(boot_now) - timespec_to_ns_test(boot_prev); + + return (boot_delta > mono_delta + 2000000000ULL); +} + +static void test_resume_detection(void) +{ + printf("Testing system resume detection logic...\n"); + + struct timespec mono_prev = { .tv_sec = 100, .tv_nsec = 0 }; + struct timespec boot_prev = { .tv_sec = 500, .tv_nsec = 0 }; + + /* 1. Normal running: both monotonic and boottime advance equally by 250 ms */ + struct timespec mono_now = { .tv_sec = 100, .tv_nsec = 250000000 }; + struct timespec boot_now = { .tv_sec = 500, .tv_nsec = 250000000 }; + assert(!check_resume_detected(&mono_now, &boot_now, &mono_prev, &boot_prev)); + TEST_PASS("Normal 250ms interval does not trigger resume"); + + /* 2. Jitter or scheduling delay: boot advances by 500ms, mono advances by 400ms */ + mono_now.tv_sec = 100; mono_now.tv_nsec = 400000000; + boot_now.tv_sec = 500; boot_now.tv_nsec = 500000000; + assert(!check_resume_detected(&mono_now, &boot_now, &mono_prev, &boot_prev)); + TEST_PASS("100ms jitter does not trigger resume"); + + /* 3. System suspend: machine sleeps for 10 seconds. + * monotonic stopped while sleeping (advanced by only 250ms awake time), + * boottime kept counting (advanced by 10.25 seconds). + */ + mono_now.tv_sec = 100; mono_now.tv_nsec = 250000000; + boot_now.tv_sec = 510; boot_now.tv_nsec = 250000000; + assert(check_resume_detected(&mono_now, &boot_now, &mono_prev, &boot_prev)); + TEST_PASS("10-second system sleep accurately triggers resume detection"); + + /* 4. Threshold boundary: exactly 2.0s delta is not triggered (> 2.0s required) */ + mono_now.tv_sec = 100; mono_now.tv_nsec = 0; + boot_now.tv_sec = 502; boot_now.tv_nsec = 0; + assert(!check_resume_detected(&mono_now, &boot_now, &mono_prev, &boot_prev)); + + /* 2.000000001s triggers */ + boot_now.tv_nsec = 1; + assert(check_resume_detected(&mono_now, &boot_now, &mono_prev, &boot_prev)); + TEST_PASS("Resume detection threshold strictly enforced at > 2.0 seconds delta"); +} + +static void test_everlive_ctl_commands(void) +{ + printf("Testing everlive-ctl runtime command construction...\n"); + + char req[512]; + char params[256]; + + /* 1. Status command */ + snprintf(req, sizeof(req), "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"%s\",\"params\":%s}\n", + "status.get", "{}"); + assert(strstr(req, "\"method\":\"status.get\"") != NULL); + assert(strstr(req, "\"params\":{}") != NULL); + TEST_PASS("Status request formatting verified"); + + /* 2. LED static color RGB calculation */ + unsigned long rgb = strtoul("FF8020", NULL, 16); + unsigned long r = (rgb >> 16) & 0xff; + unsigned long g = (rgb >> 8) & 0xff; + unsigned long b = rgb & 0xff; + assert(r == 0xff && g == 0x80 && b == 0x20); + snprintf(params, sizeof(params), "{\"r\":%lu,\"g\":%lu,\"b\":%lu}", r, g, b); + snprintf(req, sizeof(req), "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"%s\",\"params\":%s}\n", + "led.set", params); + assert(strstr(req, "\"r\":255,\"g\":128,\"b\":32") != NULL); + TEST_PASS("LED RGB formatting verified"); + + /* 3. EDID profile command */ + snprintf(params, sizeof(params), "{\"profile\":\"%s\"}", "1440p120"); + snprintf(req, sizeof(req), "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"%s\",\"params\":%s}\n", + "edid.set", params); + assert(strstr(req, "\"method\":\"edid.set\"") != NULL); + assert(strstr(req, "\"profile\":\"1440p120\"") != NULL); + TEST_PASS("EDID profile request formatting verified"); + + /* 4. Hotplug pulse command */ + snprintf(req, sizeof(req), "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"%s\",\"params\":%s}\n", + "hotplug", "{\"pulse\":true}"); + assert(strstr(req, "\"method\":\"hotplug\"") != NULL); + assert(strstr(req, "\"pulse\":true") != NULL); + TEST_PASS("Hotplug pulse request formatting verified"); + + /* 5. HDR exposure command */ + snprintf(params, sizeof(params), "{\"exposure\":%s}", "-0.5"); + snprintf(req, sizeof(req), "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"%s\",\"params\":%s}\n", + "hdr.exposure", params); + assert(strstr(req, "\"method\":\"hdr.exposure\"") != NULL); + assert(strstr(req, "\"exposure\":-0.5") != NULL); + TEST_PASS("HDR exposure request formatting verified"); + + /* 6. Audio volume / mute switch parsing */ + double vol = 85.0; + snprintf(params, sizeof(params), "{\"gain\":%g}", vol / 100.0); + assert(strstr(params, "0.85") != NULL); + TEST_PASS("Audio volume gain calculation verified"); +} + +int main(void) +{ + printf("==================================================\n"); + printf(" libeverlive-core Card Registry & Discovery Tests \n"); + printf("==================================================\n"); + + test_registry_enumeration(); + test_card_matching(); + test_mock_sysfs_scan(); + test_card_ops_binding(); + test_resume_detection(); + test_everlive_ctl_commands(); + + printf("==================================================\n"); + printf(" ALL CARD REGISTRY TESTS PASSED!\n"); + printf("==================================================\n"); + return 0; +} diff --git a/tests/test_frame_convert.c b/tests/test_frame_convert.c new file mode 100644 index 0000000..0b519a6 --- /dev/null +++ b/tests/test_frame_convert.c @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: GPL-2.0-only +#include +#include +#include +#include +#include "everlive_frame.h" + +static void test_yuy2_to_rgb_pure_colors(void) +{ + printf("[*] Testing pure color YUY2 -> RGB conversions...\n"); + + /* Test Black: Y=16 (or 0), U=128, V=128 */ + uint8_t black_yuy2[4] = { 0, 128, 0, 128 }; + uint8_t black_rgb[6] = { 0 }; + everlive_yuy2_to_rgb(black_yuy2, black_rgb, 2, 1); + assert(black_rgb[0] == 0 && black_rgb[1] == 0 && black_rgb[2] == 0); + assert(black_rgb[3] == 0 && black_rgb[4] == 0 && black_rgb[5] == 0); + + /* Test White: Y=255, U=128, V=128 */ + uint8_t white_yuy2[4] = { 255, 128, 255, 128 }; + uint8_t white_rgb[6] = { 0 }; + everlive_yuy2_to_rgb(white_yuy2, white_rgb, 2, 1); + assert(white_rgb[0] == 255 && white_rgb[1] == 255 && white_rgb[2] == 255); + assert(white_rgb[3] == 255 && white_rgb[4] == 255 && white_rgb[5] == 255); + + /* Test Red: Y=81, U=90, V=240 */ + uint8_t red_yuy2[4] = { 81, 90, 81, 240 }; + uint8_t red_rgb[6] = { 0 }; + everlive_yuy2_to_rgb(red_yuy2, red_rgb, 2, 1); + /* Red component should be dominant */ + assert(red_rgb[0] > 200); + assert(red_rgb[1] < 100); + assert(red_rgb[2] < 100); + + printf(" Pure colors OK.\n"); +} + +static void test_yuy2_to_rgb_scaled(void) +{ + printf("[*] Testing YUY2 -> RGB downscaling...\n"); + + int src_w = 1920; + int src_h = 1080; + int dst_w = 960; + int dst_h = 540; + + uint8_t *src = malloc(src_w * src_h * 2); + uint8_t *dst = malloc(dst_w * dst_h * 3); + assert(src && dst); + + /* Fill with alternating pattern */ + for (int i = 0; i < src_w * src_h * 2; i += 4) { + src[i + 0] = 128; /* Y0 */ + src[i + 1] = 64; /* U */ + src[i + 2] = 200; /* Y1 */ + src[i + 3] = 192; /* V */ + } + + everlive_yuy2_to_rgb_scaled(src, dst, src_w, src_h, dst_w, dst_h); + + /* Verify non-zero scaled output and bounds */ + bool non_zero = false; + for (int i = 0; i < dst_w * dst_h * 3; i++) { + if (dst[i] > 0) + non_zero = true; + } + assert(non_zero); + + free(src); + free(dst); + printf(" Scaled conversion OK.\n"); +} + +static void test_invalid_parameters(void) +{ + printf("[*] Testing edge cases & NULL robustness...\n"); + uint8_t buf[16] = { 0 }; + + /* NULL pointers must not crash */ + everlive_yuy2_to_rgb(NULL, buf, 2, 1); + everlive_yuy2_to_rgb(buf, NULL, 2, 1); + everlive_yuy2_to_rgb(buf, buf, 0, 1); + everlive_yuy2_to_rgb(buf, buf, 2, 0); + + everlive_yuy2_to_rgb_scaled(NULL, buf, 4, 4, 2, 2); + everlive_yuy2_to_rgb_scaled(buf, NULL, 4, 4, 2, 2); + everlive_yuy2_to_rgb_scaled(buf, buf, 0, 4, 2, 2); + everlive_yuy2_to_rgb_scaled(buf, buf, 4, 0, 2, 2); + everlive_yuy2_to_rgb_scaled(buf, buf, 4, 4, 0, 2); + everlive_yuy2_to_rgb_scaled(buf, buf, 4, 4, 2, 0); + + printf(" Edge cases OK.\n"); +} + +int main(void) +{ + printf("=== Running Frame Converter Unit Tests ===\n"); + test_yuy2_to_rgb_pure_colors(); + test_yuy2_to_rgb_scaled(); + test_invalid_parameters(); + printf("=== All Frame Converter Tests Passed! ===\n"); + return 0; +} diff --git a/tools/libeverlive-core/Makefile b/tools/libeverlive-core/Makefile new file mode 100644 index 0000000..ec2d652 --- /dev/null +++ b/tools/libeverlive-core/Makefile @@ -0,0 +1,29 @@ +# SPDX-License-Identifier: GPL-2.0-only +CC ?= gcc +AR ?= ar +CFLAGS ?= -O2 -g -Wall -Wextra -Wno-unused-parameter -std=gnu11 -D_GNU_SOURCE -fPIC + +TARGET_STATIC := libeverlive_core.a +TARGET_SHARED := libeverlive_core.so +TARGET_CLI := everlive-ctl + +OBJS := everlive_card.o everlive_frame.o + +all: $(TARGET_STATIC) $(TARGET_SHARED) $(TARGET_CLI) + +$(TARGET_STATIC): $(OBJS) + $(AR) rcs $@ $^ + +$(TARGET_SHARED): $(OBJS) + $(CC) -shared -o $@ $^ + +$(TARGET_CLI): everlive_ctl.o $(TARGET_STATIC) + $(CC) $(CFLAGS) -o $@ everlive_ctl.o $(TARGET_STATIC) + +%.o: %.c everlive_card.h everlive_frame.h + $(CC) $(CFLAGS) -c -o $@ $< + +clean: + rm -f $(OBJS) everlive_ctl.o $(TARGET_STATIC) $(TARGET_SHARED) $(TARGET_CLI) + +.PHONY: all clean diff --git a/tools/libeverlive-core/everlive-service.sh b/tools/libeverlive-core/everlive-service.sh new file mode 100755 index 0000000..8a63827 --- /dev/null +++ b/tools/libeverlive-core/everlive-service.sh @@ -0,0 +1,131 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: GPL-2.0-only +# ============================================================================== +# everlive-service.sh — Universal Background Capture Service for EverLive +# +# Automatically detects the installed card (GC570D, GC573), ensures vfio-pci +# binding, raises DMA memlock limits, and runs the capture engine in headless mode. +# Keeps HDMI passthrough, PipeWire audio, and virtual cameras active at all times. +# ============================================================================== +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)" + +CTL_BIN="${SCRIPT_DIR}/everlive-ctl" +if [ ! -x "${CTL_BIN}" ]; then + CTL_BIN="$(command -v everlive-ctl 2>/dev/null || true)" +fi + +CARD_MODEL="" +CARD_PCI="" +IS_BOUND=0 + +# 1. Detect installed card +if [ -x "${CTL_BIN}" ]; then + detect_out="$("${CTL_BIN}" detect 2>/dev/null || true)" + if echo "${detect_out}" | grep -q "Model Slug: gc570d"; then + CARD_MODEL="gc570d" + elif echo "${detect_out}" | grep -q "Model Slug: gc573"; then + CARD_MODEL="gc573" + fi + CARD_PCI="$(echo "${detect_out}" | grep "PCI BDF:" | head -n 1 | awk '{print $3}')" + if echo "${detect_out}" | grep -q "Driver Bound: vfio-pci"; then + IS_BOUND=1 + fi +fi + +# Fallback sysfs check if everlive-ctl didn't find it +if [ -z "$CARD_MODEL" ]; then + for dev in /sys/bus/pci/devices/*; do + [ -f "$dev/vendor" ] || continue + [ -f "$dev/device" ] || continue + vendor=$(cat "$dev/vendor" 2>/dev/null || true) + device=$(cat "$dev/device" 2>/dev/null || true) + if [ "$vendor" != "0x1461" ]; then + continue + fi + if [ "$device" = "0x0054" ]; then + subsystem=$(cat "$dev/subsystem_device" 2>/dev/null || true) + if [ "$subsystem" = "0x5700" ]; then + CARD_MODEL="gc570d" + else + CARD_MODEL="gc573" + fi + CARD_PCI=$(basename "$dev") + break + elif [ "$device" = "0x0055" ]; then + CARD_MODEL="gc573" + CARD_PCI=$(basename "$dev") + break + fi + done +fi + +if [ -z "$CARD_MODEL" ]; then + echo "[!] everlive-service: No supported EverLive capture card detected." >&2 + exit 1 +fi + +echo "[*] everlive-service: Initializing ${CARD_MODEL} (${CARD_PCI:-unknown BDF})..." + +# 2. Select binaries and paths +if [ "$CARD_MODEL" = "gc570d" ]; then + if [ -x "${REPO_DIR}/tools/opengc570d-userd/opengc570d-userd" ]; then + ENGINE_BIN="${REPO_DIR}/tools/opengc570d-userd/opengc570d-userd" + BIND_SCRIPT="${REPO_DIR}/tools/opengc570d-userd/gc570d-vfio-bind.sh" + else + ENGINE_BIN="/usr/local/bin/opengc570d-userd" + BIND_SCRIPT="/usr/local/bin/gc570d-vfio-bind.sh" + fi + ENGINE_ARGS=(--headless --loopback) +else + if [ -x "${REPO_DIR}/tools/opengc573-userd/gc573_user_streamer" ]; then + ENGINE_BIN="${REPO_DIR}/tools/opengc573-userd/gc573_user_streamer" + BIND_SCRIPT="${REPO_DIR}/tools/opengc573-userd/vfio_pci_bind.sh" + else + ENGINE_BIN="/usr/local/bin/gc573_user_streamer" + BIND_SCRIPT="/usr/local/bin/vfio_pci_bind.sh" + fi + ENGINE_ARGS=(--headless) +fi + +# 3. Ensure PCI device is bound to vfio-pci +if [ "$IS_BOUND" -eq 0 ] && [ -n "$CARD_PCI" ]; then + driver_link="/sys/bus/pci/devices/${CARD_PCI}/driver" + if [ ! -L "$driver_link" ] || [ "$(basename "$(readlink -f "$driver_link")")" != "vfio-pci" ]; then + echo "[*] everlive-service: Device not bound to vfio-pci. Executing bind script..." + if [ -x "${BIND_SCRIPT}" ]; then + if [ "$EUID" -ne 0 ]; then + if command -v pkexec >/dev/null 2>&1 && [ ! -t 0 ]; then + pkexec "${BIND_SCRIPT}" --bind || true + else + sudo "${BIND_SCRIPT}" --bind || true + fi + else + "${BIND_SCRIPT}" --bind || true + fi + fi + fi +fi + +# 4. Set memlock ulimit +ulimit -S -l unlimited 2>/dev/null || ulimit -S -l "$(ulimit -H -l)" 2>/dev/null || true + +# 5. Ensure cap_ipc_lock on engine binary +if [ -f "${ENGINE_BIN}" ] && command -v setcap >/dev/null 2>&1; then + if ! getcap "${ENGINE_BIN}" 2>/dev/null | grep -q "cap_ipc_lock"; then + echo "[*] everlive-service: Granting cap_ipc_lock=+ep to $(basename "${ENGINE_BIN}")..." + if [ "$EUID" -eq 0 ]; then + setcap cap_ipc_lock=+ep "${ENGINE_BIN}" || true + elif command -v pkexec >/dev/null 2>&1 && [ ! -t 0 ]; then + pkexec setcap cap_ipc_lock=+ep "${ENGINE_BIN}" || true + else + sudo setcap cap_ipc_lock=+ep "${ENGINE_BIN}" || true + fi + fi +fi + +# 6. Execute engine daemon +echo "[+] everlive-service: Launching ${ENGINE_BIN} ${ENGINE_ARGS[*]}..." +exec "${ENGINE_BIN}" "${ENGINE_ARGS[@]}" "$@" diff --git a/tools/libeverlive-core/everlive.service b/tools/libeverlive-core/everlive.service new file mode 100644 index 0000000..2c5ac1d --- /dev/null +++ b/tools/libeverlive-core/everlive.service @@ -0,0 +1,25 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# EverLive Universal Userspace Capture Service +# +# Powers up the capture hardware, holds the VFIO context open, enables monitor +# passthrough, and dispatches virtual cameras and PipeWire audio streams at login. +# +# Install with: +# systemctl --user daemon-reload +# systemctl --user enable --now everlive.service + +[Unit] +Description=EverLive Universal Userspace Capture Service +After=pipewire.service wireplumber.service +Wants=pipewire.service + +[Service] +Type=simple +ExecStart=/usr/local/bin/everlive-service +Restart=on-failure +RestartSec=2 +LimitMEMLOCK=infinity + +[Install] +WantedBy=default.target diff --git a/tools/libeverlive-core/everlive_card.c b/tools/libeverlive-core/everlive_card.c new file mode 100644 index 0000000..a784fa7 --- /dev/null +++ b/tools/libeverlive-core/everlive_card.c @@ -0,0 +1,415 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * everlive_card.c — Core Hardware Registry and Autodiscovery for EverLive + */ + +#include "everlive_card.h" + +#include +#include +#include +#include +#include +#include +#include + +#define DEFAULT_PCI_SYSFS_PATH "/sys/bus/pci/devices" + +/* ------------------------------------------------------------------------- + * Default Hardware Operations + * + * The core library owns discovery and the control-plane contract; a concrete + * backend (the GC573 streamer or the GC570D daemon) registers its vtable with + * everlive_register_card_ops(). Until then these defaults keep the interface + * well defined: probe() verifies the discovered identity, release() is a + * no-op because the core owns no hardware handles, and every operation that + * would touch real hardware reports -ENOTSUP instead of pretending success. + * ------------------------------------------------------------------------- */ +static int everlive_default_probe(struct everlive_device *dev) +{ + if (!dev || !dev->desc) + return -EINVAL; + if (!dev->pci_bdf[0]) + return -EINVAL; + return 0; +} + +static int everlive_default_unsupported(struct everlive_device *dev) +{ + (void)dev; + return -ENOTSUP; +} + +static int everlive_default_set_led(struct everlive_device *dev, + enum everlive_led_mode mode, + uint8_t r, uint8_t g, uint8_t b) +{ + (void)dev; + (void)mode; + (void)r; + (void)g; + (void)b; + return -ENOTSUP; +} + +static int everlive_default_set_input_port(struct everlive_device *dev, int port) +{ + (void)dev; + (void)port; + return -ENOTSUP; +} + +static int everlive_default_set_edid_profile(struct everlive_device *dev, + int port, + const char *profile_name, + const uint8_t *custom_bin, + size_t bin_len) +{ + (void)dev; + (void)port; + (void)profile_name; + (void)custom_bin; + (void)bin_len; + return -ENOTSUP; +} + +static int everlive_default_enable_passthrough(struct everlive_device *dev, + bool enable) +{ + (void)dev; + (void)enable; + return -ENOTSUP; +} + +static int everlive_default_get_telemetry(struct everlive_device *dev, + struct everlive_telemetry *out) +{ + (void)dev; + if (out) + memset(out, 0, sizeof(*out)); + return -ENOTSUP; +} + +static void everlive_default_release(struct everlive_device *dev) +{ + (void)dev; +} + +static const struct everlive_card_ops kEverliveDefaultOps = { + .probe = everlive_default_probe, + .init_hardware = everlive_default_unsupported, + .standby = everlive_default_unsupported, + .release = everlive_default_release, + .set_input_port = everlive_default_set_input_port, + .start_capture = everlive_default_unsupported, + .stop_capture = everlive_default_unsupported, + .set_edid_profile = everlive_default_set_edid_profile, + .enable_passthrough = everlive_default_enable_passthrough, + .set_led = everlive_default_set_led, + .get_telemetry = everlive_default_get_telemetry, +}; + +/* + * Backend registration table. Discovery descriptors are immutable, so a + * backend binds by model slug instead of mutating the static registry. + */ +#define EVERLIVE_MAX_OPS_BINDINGS 8 + +struct everlive_ops_binding { + const char *model_slug; + const struct everlive_card_ops *ops; +}; + +static struct everlive_ops_binding kEverliveOpsBindings[EVERLIVE_MAX_OPS_BINDINGS]; +static size_t kEverliveOpsBindingCount; + +/* ------------------------------------------------------------------------- + * Static Hardware Registry of Supported Capture Cards + * ------------------------------------------------------------------------- */ +static const struct everlive_device_desc kEverliveCardRegistry[] = { + { + .vendor_id = 0x1461, + .device_id = 0x0054, + .subsystem_id = 0x5700, + .model_slug = "gc570d", + .name = "AVerMedia Live Gamer DUO (GC570D)", + .capabilities = EVERLIVE_CAP_VIDEO_CAPTURE | + EVERLIVE_CAP_AUDIO_CAPTURE | + EVERLIVE_CAP_PASSTHROUGH | + EVERLIVE_CAP_DUAL_INPUT | + EVERLIVE_CAP_RGB_LED | + EVERLIVE_CAP_HARDWARE_SPLITTER | + EVERLIVE_CAP_EDID_PASSTHROUGH, + .num_input_ports = 2, + .ops = &kEverliveDefaultOps, + }, + { + .vendor_id = 0x1461, + .device_id = 0x0054, + .subsystem_id = 0x5730, + .model_slug = "gc573", + .name = "AVerMedia Live Gamer 4K (GC573)", + .capabilities = EVERLIVE_CAP_VIDEO_CAPTURE | + EVERLIVE_CAP_AUDIO_CAPTURE | + EVERLIVE_CAP_PASSTHROUGH | + EVERLIVE_CAP_4K60 | + EVERLIVE_CAP_1440P120 | + EVERLIVE_CAP_1080P240 | + EVERLIVE_CAP_HDR10 | + EVERLIVE_CAP_RGB_LED | + EVERLIVE_CAP_BITSTREAM_AUDIO | + EVERLIVE_CAP_FRAMEGEN | + EVERLIVE_CAP_EDID_PASSTHROUGH, + .num_input_ports = 1, + .ops = &kEverliveDefaultOps, + }, + { + .vendor_id = 0x1461, + .device_id = 0x0055, + .subsystem_id = 0, /* GC573 BOLT / Alternative PCIe ID */ + .model_slug = "gc573", + .name = "AVerMedia Live Gamer 4K / BOLT (GC573)", + .capabilities = EVERLIVE_CAP_VIDEO_CAPTURE | + EVERLIVE_CAP_AUDIO_CAPTURE | + EVERLIVE_CAP_PASSTHROUGH | + EVERLIVE_CAP_4K60 | + EVERLIVE_CAP_1440P120 | + EVERLIVE_CAP_1080P240 | + EVERLIVE_CAP_HDR10 | + EVERLIVE_CAP_RGB_LED | + EVERLIVE_CAP_BITSTREAM_AUDIO | + EVERLIVE_CAP_FRAMEGEN | + EVERLIVE_CAP_EDID_PASSTHROUGH, + .num_input_ports = 1, + .ops = &kEverliveDefaultOps, + }, + { + .vendor_id = 0x1461, + .device_id = 0x0057, + .subsystem_id = 0, /* Future expansion: Live Gamer 4K 2.1 */ + .model_slug = "gc575", + .name = "AVerMedia Live Gamer 4K 2.1 (GC575)", + .capabilities = EVERLIVE_CAP_VIDEO_CAPTURE | + EVERLIVE_CAP_AUDIO_CAPTURE | + EVERLIVE_CAP_PASSTHROUGH | + EVERLIVE_CAP_4K60 | + EVERLIVE_CAP_1440P120 | + EVERLIVE_CAP_HDR10 | + EVERLIVE_CAP_RGB_LED | + EVERLIVE_CAP_BITSTREAM_AUDIO | + EVERLIVE_CAP_EDID_PASSTHROUGH, + .num_input_ports = 1, + .ops = &kEverliveDefaultOps, + }, +}; + +#define REGISTRY_COUNT (sizeof(kEverliveCardRegistry) / sizeof(kEverliveCardRegistry[0])) + +const struct everlive_device_desc *everlive_get_card_registry(size_t *out_count) +{ + if (out_count) + *out_count = REGISTRY_COUNT; + return kEverliveCardRegistry; +} + +int everlive_register_card_ops(const char *model_slug, + const struct everlive_card_ops *ops) +{ + const struct everlive_device_desc *canonical = NULL; + + if (!model_slug || !*model_slug || !ops) + return -EINVAL; + + for (size_t i = 0; i < REGISTRY_COUNT; i++) { + if (strcmp(kEverliveCardRegistry[i].model_slug, model_slug) == 0) { + canonical = &kEverliveCardRegistry[i]; + break; + } + } + if (!canonical) + return -ENOENT; + + /* Replace an existing binding for the same model. */ + for (size_t i = 0; i < kEverliveOpsBindingCount; i++) { + if (kEverliveOpsBindings[i].model_slug == canonical->model_slug) { + kEverliveOpsBindings[i].ops = ops; + return 0; + } + } + + if (kEverliveOpsBindingCount >= EVERLIVE_MAX_OPS_BINDINGS) + return -ENOSPC; + + kEverliveOpsBindings[kEverliveOpsBindingCount].model_slug = + canonical->model_slug; + kEverliveOpsBindings[kEverliveOpsBindingCount].ops = ops; + kEverliveOpsBindingCount++; + return 0; +} + +const struct everlive_card_ops * +everlive_card_ops_for(const struct everlive_device_desc *desc) +{ + if (!desc) + return &kEverliveDefaultOps; + + for (size_t i = 0; i < kEverliveOpsBindingCount; i++) { + if (kEverliveOpsBindings[i].model_slug == desc->model_slug || + strcmp(kEverliveOpsBindings[i].model_slug, + desc->model_slug) == 0) + return kEverliveOpsBindings[i].ops; + } + + return desc->ops ? desc->ops : &kEverliveDefaultOps; +} + +const struct everlive_device_desc *everlive_match_card(uint16_t vendor, + uint16_t device, + uint16_t subsystem) +{ + /* Pass 1: Look for exact match (vendor, device, subsystem) */ + for (size_t i = 0; i < REGISTRY_COUNT; i++) { + const struct everlive_device_desc *d = &kEverliveCardRegistry[i]; + if (d->vendor_id == vendor && d->device_id == device) { + if (d->subsystem_id != 0 && d->subsystem_id == subsystem) + return d; + } + } + + /* Special case: GC570D is distinguished by subsystem 0x5700. + * If device is 0x0054 and subsystem is NOT 0x5700, it is a GC573. + */ + if (vendor == 0x1461 && device == 0x0054 && subsystem != 0x5700) { + for (size_t i = 0; i < REGISTRY_COUNT; i++) { + if (strcmp(kEverliveCardRegistry[i].model_slug, "gc573") == 0) + return &kEverliveCardRegistry[i]; + } + } + + /* Pass 2: Look for wildcard subsystem match */ + for (size_t i = 0; i < REGISTRY_COUNT; i++) { + const struct everlive_device_desc *d = &kEverliveCardRegistry[i]; + if (d->vendor_id == vendor && d->device_id == device) { + if (d->subsystem_id == 0) + return d; + } + } + + return NULL; +} + +static uint16_t read_sysfs_hex16(const char *path) +{ + FILE *f = fopen(path, "r"); + if (!f) + return 0; + + char buf[32]; + uint16_t val = 0; + if (fgets(buf, sizeof(buf), f)) { + val = (uint16_t)strtoul(buf, NULL, 16); + } + fclose(f); + return val; +} + +static int read_iommu_group(const char *dev_dir) +{ + char path[PATH_MAX]; + char target[PATH_MAX]; + + int n = snprintf(path, sizeof(path), "%s/iommu_group", dev_dir); + if (n < 0 || (size_t)n >= sizeof(path)) + return -1; + + ssize_t len = readlink(path, target, sizeof(target) - 1); + if (len <= 0) + return -1; + + target[len] = '\0'; + char *slash = strrchr(target, '/'); + if (!slash) + return -1; + + return (int)strtol(slash + 1, NULL, 10); +} + +bool everlive_is_pci_vfio_bound(const char *pci_bdf, const char *sysfs_pci_root) +{ + if (!pci_bdf || !*pci_bdf) + return false; + + const char *root = (sysfs_pci_root && *sysfs_pci_root) ? + sysfs_pci_root : DEFAULT_PCI_SYSFS_PATH; + + char driver_link[PATH_MAX]; + char target[PATH_MAX]; + + snprintf(driver_link, sizeof(driver_link), "%s/%s/driver", root, pci_bdf); + ssize_t len = readlink(driver_link, target, sizeof(target) - 1); + if (len <= 0) + return false; + + target[len] = '\0'; + char *slash = strrchr(target, '/'); + const char *driver_name = slash ? (slash + 1) : target; + + return (strcmp(driver_name, "vfio-pci") == 0); +} + +int everlive_detect_installed_cards(struct everlive_device *out_devices, + size_t max_devices, + const char *sysfs_pci_root) +{ + if (!out_devices || max_devices == 0) + return 0; + + const char *root = (sysfs_pci_root && *sysfs_pci_root) ? + sysfs_pci_root : DEFAULT_PCI_SYSFS_PATH; + + DIR *dir = opendir(root); + if (!dir) + return 0; + + size_t found_count = 0; + struct dirent *ent; + + while ((ent = readdir(dir)) != NULL) { + if (ent->d_name[0] == '.') + continue; + + char dev_path[512]; + snprintf(dev_path, sizeof(dev_path), "%s/%s", root, ent->d_name); + + char vendor_path[PATH_MAX]; + char device_path[PATH_MAX]; + char subsystem_path[PATH_MAX]; + + snprintf(vendor_path, sizeof(vendor_path), "%s/vendor", dev_path); + snprintf(device_path, sizeof(device_path), "%s/device", dev_path); + snprintf(subsystem_path, sizeof(subsystem_path), "%s/subsystem_device", dev_path); + + uint16_t vendor = read_sysfs_hex16(vendor_path); + uint16_t device = read_sysfs_hex16(device_path); + uint16_t subsystem = read_sysfs_hex16(subsystem_path); + + const struct everlive_device_desc *desc = + everlive_match_card(vendor, device, subsystem); + + if (desc) { + struct everlive_device *dev = &out_devices[found_count]; + memset(dev, 0, sizeof(*dev)); + dev->desc = desc; + snprintf(dev->pci_bdf, sizeof(dev->pci_bdf), "%.31s", ent->d_name); + dev->iommu_group = read_iommu_group(dev_path); + dev->is_vfio_bound = everlive_is_pci_vfio_bound(ent->d_name, root); + + found_count++; + if (found_count >= max_devices) + break; + } + } + + closedir(dir); + return (int)found_count; +} diff --git a/tools/libeverlive-core/everlive_card.h b/tools/libeverlive-core/everlive_card.h new file mode 100644 index 0000000..1aaa40f --- /dev/null +++ b/tools/libeverlive-core/everlive_card.h @@ -0,0 +1,208 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * everlive_card.h — Core Hardware Abstraction Layer for EverLive Capture Cards + * + * Provides a polymorphic, modular interface for discovering, initializing, + * and managing PCIe and USB capture cards (GC573, GC570D, and future hardware). + */ + +#ifndef EVERLIVE_CARD_H +#define EVERLIVE_CARD_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* ------------------------------------------------------------------------- + * Hardware Capability Bitflags + * ------------------------------------------------------------------------- */ +#define EVERLIVE_CAP_VIDEO_CAPTURE (1U << 0) +#define EVERLIVE_CAP_AUDIO_CAPTURE (1U << 1) +#define EVERLIVE_CAP_PASSTHROUGH (1U << 2) +#define EVERLIVE_CAP_1080P60 (1U << 3) +#define EVERLIVE_CAP_4K60 (1U << 4) +#define EVERLIVE_CAP_1440P120 (1U << 5) +#define EVERLIVE_CAP_1080P240 (1U << 6) +#define EVERLIVE_CAP_HDR10 (1U << 7) +#define EVERLIVE_CAP_DUAL_INPUT (1U << 8) +#define EVERLIVE_CAP_RGB_LED (1U << 9) +#define EVERLIVE_CAP_BITSTREAM_AUDIO (1U << 10) +#define EVERLIVE_CAP_FRAMEGEN (1U << 11) +#define EVERLIVE_CAP_HARDWARE_SCALER (1U << 12) +#define EVERLIVE_CAP_HARDWARE_SPLITTER (1U << 13) +#define EVERLIVE_CAP_EDID_PASSTHROUGH (1U << 14) +#define EVERLIVE_CAP_4K120 (1U << 15) + +/* ------------------------------------------------------------------------- + * Lighting & Visual Feedback Modes + * ------------------------------------------------------------------------- */ +enum everlive_led_mode { + EVERLIVE_LED_MODE_OFF = 0, + EVERLIVE_LED_MODE_STATIC, + EVERLIVE_LED_MODE_BREATHING, + EVERLIVE_LED_MODE_RAINBOW, + EVERLIVE_LED_MODE_WAVE, +}; + +/* ------------------------------------------------------------------------- + * Pixel & Audio Formats + * ------------------------------------------------------------------------- */ +enum everlive_pixel_format { + EVERLIVE_PIX_FMT_UNKNOWN = 0, + EVERLIVE_PIX_FMT_YUYV, + EVERLIVE_PIX_FMT_NV12, + EVERLIVE_PIX_FMT_P010, + EVERLIVE_PIX_FMT_RGB24, + EVERLIVE_PIX_FMT_BGR24, + EVERLIVE_PIX_FMT_RGBA32, +}; + +struct everlive_video_timing { + uint32_t width; + uint32_t height; + double refresh_rate; + bool interlaced; + bool hdr10; + enum everlive_pixel_format format; +}; + +struct everlive_audio_timing { + uint32_t sample_rate; /* e.g. 48000 */ + uint8_t channels; /* e.g. 2, 6, 8 */ + uint8_t bit_depth; /* e.g. 16, 24 */ + bool is_compressed; /* true for AC-3 / DTS IEC 61937 */ + char codec_name[16]; /* e.g. "LPCM", "AC-3", "DTS" */ +}; + +/* ------------------------------------------------------------------------- + * Live Telemetry & Diagnostics + * ------------------------------------------------------------------------- */ +struct everlive_telemetry { + bool signal_locked; + int active_input_port; + struct everlive_video_timing video; + struct everlive_audio_timing audio; + double pci_bandwidth_mbps; + uint64_t frames_captured; + uint64_t frames_dropped; + float temperature_c; /* Negative value indicates unsupported */ +}; + +/* Forward declarations */ +struct everlive_device; +struct everlive_card_ops; + +/* ------------------------------------------------------------------------- + * Declarative Device Descriptor (Hardware Registry Entry) + * ------------------------------------------------------------------------- */ +struct everlive_device_desc { + uint16_t vendor_id; + uint16_t device_id; + uint16_t subsystem_id; /* 0 = wildcard match */ + const char *model_slug; /* Short identifier, e.g. "gc573", "gc570d" */ + const char *name; /* Friendly name, e.g. "AVerMedia Live Gamer 4K" */ + uint32_t capabilities; + int num_input_ports; + const struct everlive_card_ops *ops; +}; + +/* ------------------------------------------------------------------------- + * Polymorphic Hardware Operations Interface + * ------------------------------------------------------------------------- */ +struct everlive_card_ops { + /* Probe and identify the hardware at the given PCI/USB location */ + int (*probe)(struct everlive_device *dev); + + /* Initialize hardware registers, clock trees, and I2C buses */ + int (*init_hardware)(struct everlive_device *dev); + + /* Enter low-power standby or hand off to background daemon */ + int (*standby)(struct everlive_device *dev); + + /* Release hardware resources and close descriptors */ + void (*release)(struct everlive_device *dev); + + /* Select active HDMI input port (0-indexed) */ + int (*set_input_port)(struct everlive_device *dev, int port); + + /* Start DMA video and audio streaming pipelines */ + int (*start_capture)(struct everlive_device *dev); + + /* Stop streaming while keeping hardware awake */ + int (*stop_capture)(struct everlive_device *dev); + + /* Configure or bridge HDMI EDID */ + int (*set_edid_profile)(struct everlive_device *dev, int port, + const char *profile_name, + const uint8_t *custom_bin, size_t bin_len); + + /* Enable or disable zero-latency HDMI passthrough */ + int (*enable_passthrough)(struct everlive_device *dev, bool enable); + + /* Update RGB LED illumination */ + int (*set_led)(struct everlive_device *dev, enum everlive_led_mode mode, + uint8_t r, uint8_t g, uint8_t b); + + /* Retrieve real-time telemetry (signal lock, timings, stats) */ + int (*get_telemetry)(struct everlive_device *dev, + struct everlive_telemetry *out_telemetry); +}; + +/* ------------------------------------------------------------------------- + * Active Device Instance Handle + * ------------------------------------------------------------------------- */ +struct everlive_device { + const struct everlive_device_desc *desc; + char pci_bdf[32]; /* e.g. "0000:03:00.0" */ + int iommu_group; + bool is_vfio_bound; + void *priv; /* Backend-specific context */ +}; + +/* ------------------------------------------------------------------------- + * Registry & Discovery Functions + * ------------------------------------------------------------------------- */ + +/* Returns a pointer to the static registry table and sets out_count */ +const struct everlive_device_desc *everlive_get_card_registry(size_t *out_count); + +/* + * Bind a backend implementation to every registry entry with @model_slug. + * Backends own their hardware context; the core only stores the vtable. + * Returns 0, -EINVAL for bad arguments, -ENOENT for an unknown model, or + * -ENOSPC when the binding table is full. + */ +int everlive_register_card_ops(const char *model_slug, + const struct everlive_card_ops *ops); + +/* + * Resolve the active operations for a descriptor. This returns the most + * recently registered backend vtable, the descriptor's built-in vtable, or + * the core default (which reports -ENOTSUP for hardware actions) in that + * order, and never NULL. + */ +const struct everlive_card_ops * +everlive_card_ops_for(const struct everlive_device_desc *desc); + +/* Matches vendor, device, and subsystem IDs to a registered card descriptor */ +const struct everlive_device_desc *everlive_match_card(uint16_t vendor, + uint16_t device, + uint16_t subsystem); + +/* Scans sysfs (/sys/bus/pci/devices) and detects installed capture cards */ +int everlive_detect_installed_cards(struct everlive_device *out_devices, + size_t max_devices, + const char *sysfs_pci_root); + +/* Checks if a given PCI BDF is currently bound to vfio-pci */ +bool everlive_is_pci_vfio_bound(const char *pci_bdf, const char *sysfs_pci_root); + +#ifdef __cplusplus +} +#endif + +#endif /* EVERLIVE_CARD_H */ diff --git a/tools/libeverlive-core/everlive_ctl.c b/tools/libeverlive-core/everlive_ctl.c new file mode 100644 index 0000000..39bf74e --- /dev/null +++ b/tools/libeverlive-core/everlive_ctl.c @@ -0,0 +1,542 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * everlive_ctl.c — Unified Control & Diagnostic CLI for EverLive Capture Cards + */ + +#include "everlive_card.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +static void print_capabilities(uint32_t caps) +{ + printf(" Capabilities: "); + bool first = true; +#define CHECK_CAP(cap, name) \ + if (caps & (cap)) { \ + printf("%s%s", first ? "" : ", ", (name)); \ + first = false; \ + } + CHECK_CAP(EVERLIVE_CAP_4K60, "4K60"); + CHECK_CAP(EVERLIVE_CAP_4K120, "4K120"); + CHECK_CAP(EVERLIVE_CAP_1440P120, "1440p120"); + CHECK_CAP(EVERLIVE_CAP_1080P240, "1080p240"); + CHECK_CAP(EVERLIVE_CAP_1080P60, "1080p60"); + CHECK_CAP(EVERLIVE_CAP_HDR10, "HDR10"); + CHECK_CAP(EVERLIVE_CAP_DUAL_INPUT, "Dual-Input"); + CHECK_CAP(EVERLIVE_CAP_RGB_LED, "RGB-LED"); + CHECK_CAP(EVERLIVE_CAP_BITSTREAM_AUDIO, "Bitstream-Audio (AC3/DTS)"); + CHECK_CAP(EVERLIVE_CAP_FRAMEGEN, "Lossless Scaling (lsfg-vk)"); + CHECK_CAP(EVERLIVE_CAP_HARDWARE_SPLITTER, "Hardware-Splitter"); +#undef CHECK_CAP + if (first) + printf("None"); + printf("\n"); +} + +static int cmd_detect(void) +{ + struct everlive_device cards[8]; + int count = everlive_detect_installed_cards(cards, 8, NULL); + + printf("=== EverLive Capture Card Discovery ===\n"); + if (count <= 0) { + printf("No supported EverLive capture cards detected on PCI bus.\n"); + return 1; + } + + printf("Found %d supported capture card%s:\n\n", count, count > 1 ? "s" : ""); + for (int i = 0; i < count; i++) { + const struct everlive_device *dev = &cards[i]; + printf("[%d] %s\n", i + 1, dev->desc->name); + printf(" Model Slug: %s\n", dev->desc->model_slug); + printf(" PCI BDF: %s\n", dev->pci_bdf); + printf(" IOMMU Group: %d\n", dev->iommu_group); + printf(" Driver Bound: %s\n", dev->is_vfio_bound ? "vfio-pci (Ready)" : "Kernel driver (Needs bind)"); + printf(" Input Ports: %d\n", dev->desc->num_input_ports); + print_capabilities(dev->desc->capabilities); + printf("\n"); + } + return 0; +} + +static void print_usage(const char *prog) +{ + printf("Usage: %s [arguments]\n\n" + "Hardware & Diagnostics:\n" + " detect List all installed capture cards and capabilities\n" + " list-models Display all supported card models in EverLive registry\n" + " status Display capture daemon status and active hardware\n\n" + "Runtime control (JSON-RPC to the running engine):\n" + " runtime status Engine status (signal, audio, V4L2, HDR...)\n" + " runtime led \n" + " runtime led-mode \n" + " runtime edid \n" + " runtime hdr \n" + " runtime hdr-ev <-2.0..2.0>\n" + " runtime hdr-passthrough <0|1>\n" + " runtime hotplug Pulse HDMI HPD and replay the EDID\n" + " runtime toast \n" + " runtime volume <0..150>\n" + " runtime mute <0|1>\n" + " runtime osd <0|1>\n\n" + "Service & Bringup:\n" + " init Initialize capture card in background (passthrough/audio)\n" + " service status Show systemd user service status\n" + " service start Start the background capture service\n" + " service stop Stop the background capture service\n" + " service enable Enable background capture service at user login\n" + " service disable Disable background capture autostart\n" + " help Show this help message\n\n", prog); +} + +static int cmd_status(void) +{ + printf("=== EverLive Background Engine Status ===\n"); + + /* Check running processes */ + bool gc570d_proc = (system("pgrep -x opengc570d-userd >/dev/null 2>&1") == 0); + bool gc573_proc = (system("pgrep -x gc573_user_streamer >/dev/null 2>&1") == 0 || + system("pgrep -x everlive_userd >/dev/null 2>&1") == 0); + + if (gc570d_proc) { + printf("Active Process: opengc570d-userd (running)\n"); + } else if (gc573_proc) { + printf("Active Process: gc573_user_streamer / everlive_userd (running)\n"); + } else { + printf("Active Process: None (Engine is idle/standby)\n"); + } + + /* Check systemd user service */ + printf("Service State: "); + fflush(stdout); + int ret = system("systemctl --user is-active everlive.service 2>/dev/null"); + if (ret != 0) { + system("systemctl --user is-active opengc570d-userd.service 2>/dev/null || echo inactive"); + } + + /* Also report detected card */ + printf("\n"); + cmd_detect(); + return 0; +} + +static int cmd_init(void) +{ + printf("[*] Initializing EverLive capture hardware in background...\n"); + /* Check if already running */ + if (system("pgrep -x opengc570d-userd >/dev/null 2>&1") == 0 || + system("pgrep -x gc573_user_streamer >/dev/null 2>&1") == 0) { + printf("[+] Capture engine is already running and hardware is initialized.\n"); + return 0; + } + + /* Try starting via systemd if service file exists */ + if (system("systemctl --user is-enabled everlive.service >/dev/null 2>&1") == 0 || + system("systemctl --user is-enabled opengc570d-userd.service >/dev/null 2>&1") == 0) { + printf("[*] Starting via systemd user service...\n"); + system("systemctl --user start everlive.service 2>/dev/null || systemctl --user start opengc570d-userd.service"); + return 0; + } + + /* Direct script invocation */ + char script_path[PATH_MAX]; + snprintf(script_path, sizeof(script_path), "%s/everlive-service.sh", "/usr/local/bin"); + if (access(script_path, X_OK) != 0) { + /* Search relative to executable or current directory */ + snprintf(script_path, sizeof(script_path), "./tools/libeverlive-core/everlive-service.sh"); + } + + char cmd[PATH_MAX + 32]; + snprintf(cmd, sizeof(cmd), "nohup %s >/dev/null 2>&1 &", script_path); + printf("[*] Spawning %s...\n", script_path); + int res = system(cmd); + if (res == 0) { + printf("[+] EverLive background service launched successfully.\n"); + } + return res; +} + +static int cmd_service(int argc, char **argv) +{ + if (argc < 3) { + return system("systemctl --user status everlive.service opengc570d-userd.service 2>/dev/null || true"); + } + + const char *action = argv[2]; + char cmd[256]; + + if (strcmp(action, "status") == 0) { + snprintf(cmd, sizeof(cmd), "systemctl --user status everlive.service opengc570d-userd.service"); + } else if (strcmp(action, "start") == 0) { + snprintf(cmd, sizeof(cmd), "systemctl --user start everlive.service 2>/dev/null || systemctl --user start opengc570d-userd.service"); + } else if (strcmp(action, "stop") == 0) { + snprintf(cmd, sizeof(cmd), "systemctl --user stop everlive.service opengc570d-userd.service 2>/dev/null"); + } else if (strcmp(action, "enable") == 0) { + snprintf(cmd, sizeof(cmd), "systemctl --user enable --now everlive.service 2>/dev/null || systemctl --user enable --now opengc570d-userd.service"); + } else if (strcmp(action, "disable") == 0) { + snprintf(cmd, sizeof(cmd), "systemctl --user disable --now everlive.service opengc570d-userd.service 2>/dev/null"); + } else { + fprintf(stderr, "Unknown service action: %s\n", action); + return 1; + } + + return system(cmd); +} + +static int cmd_list_models(void) +{ + size_t count = 0; + const struct everlive_device_desc *reg = everlive_get_card_registry(&count); + + printf("=== EverLive Supported Card Registry ===\n"); + for (size_t i = 0; i < count; i++) { + printf("- %s (%s)\n", reg[i].name, reg[i].model_slug); + printf(" PCI ID: 0x%04x:0x%04x (Subsystem: 0x%04x)\n", + reg[i].vendor_id, reg[i].device_id, reg[i].subsystem_id); + print_capabilities(reg[i].capabilities); + } + return 0; +} + +static int ctl_resolve_socket(char *buf, size_t size) +{ + const char *override = getenv("EVERLIVE_CONTROL_SOCKET"); + const char *xdg = getenv("XDG_RUNTIME_DIR"); + uid_t uid = getuid(); + int n; + + if (override && *override) { + n = snprintf(buf, size, "%s", override); + return (n > 0 && (size_t)n < size) ? 0 : -1; + } + + if (xdg && *xdg) { + n = snprintf(buf, size, "%s/everlive-experimental.sock", xdg); + if (n > 0 && (size_t)n < size && access(buf, F_OK) == 0) + return 0; + } + + n = snprintf(buf, size, "/run/user/%u/everlive-experimental.sock", + (unsigned int)uid); + if (n > 0 && (size_t)n < size && access(buf, F_OK) == 0) + return 0; + + n = snprintf(buf, size, "/tmp/everlive-%u/everlive-experimental.sock", + (unsigned int)uid); + return (n > 0 && (size_t)n < size) ? 0 : -1; +} + +static void json_escape(const char *in, char *out, size_t out_size) +{ + size_t o = 0; + + for (const unsigned char *p = (const unsigned char *)in; *p; p++) { + if (o + 8 >= out_size) + break; + switch (*p) { + case '"': + out[o++] = '\\'; + out[o++] = '"'; + break; + case '\\': + out[o++] = '\\'; + out[o++] = '\\'; + break; + case '\n': + out[o++] = '\\'; + out[o++] = 'n'; + break; + case '\r': + out[o++] = '\\'; + out[o++] = 'r'; + break; + case '\t': + out[o++] = '\\'; + out[o++] = 't'; + break; + default: + if (*p < 0x20) + o += (size_t)snprintf(out + o, out_size - o, + "\\u%04x", *p); + else + out[o++] = (char)*p; + break; + } + } + out[o] = '\0'; +} + +static int ctl_call(const char *method, const char *params) +{ + char path[108]; + struct sockaddr_un addr; + char request[2048]; + char response[8192]; + size_t off = 0; + int fd; + + if (ctl_resolve_socket(path, sizeof(path)) < 0) { + fprintf(stderr, "EverLive control socket path is too long\n"); + return 1; + } + + fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); + if (fd < 0) { + fprintf(stderr, "socket: %s\n", strerror(errno)); + return 1; + } + + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + snprintf(addr.sun_path, sizeof(addr.sun_path), "%s", path); + + if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + fprintf(stderr, + "Cannot connect to the EverLive engine at %s: %s\n" + "Is the capture engine running?\n", + path, strerror(errno)); + close(fd); + return 1; + } + + snprintf(request, sizeof(request), + "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"%s\",\"params\":%s}\n", + method, params && *params ? params : "{}"); + + while (off < strlen(request)) { + ssize_t n = write(fd, request + off, strlen(request) - off); + + if (n < 0) { + if (errno == EINTR) + continue; + fprintf(stderr, "write: %s\n", strerror(errno)); + close(fd); + return 1; + } + off += (size_t)n; + } + + off = 0; + for (;;) { + ssize_t n; + + if (off + 1 >= sizeof(response)) { + fprintf(stderr, "Response too large\n"); + close(fd); + return 1; + } + n = read(fd, response + off, sizeof(response) - off - 1); + if (n < 0) { + if (errno == EINTR) + continue; + fprintf(stderr, "read: %s\n", strerror(errno)); + close(fd); + return 1; + } + if (n == 0) + break; + off += (size_t)n; + if (memchr(response, '\n', off)) + break; + } + close(fd); + + response[off] = '\0'; + while (off > 0 && (response[off - 1] == '\n' || response[off - 1] == '\r')) + response[--off] = '\0'; + + printf("%s\n", response); + return strstr(response, "\"error\"") ? 1 : 0; +} + +static int parse_switch(const char *value) +{ + if (!value) + return -1; + if (!strcasecmp(value, "on") || !strcasecmp(value, "true") || + !strcasecmp(value, "1") || !strcasecmp(value, "yes") || + !strcasecmp(value, "enable") || !strcasecmp(value, "enabled")) + return 1; + if (!strcasecmp(value, "off") || !strcasecmp(value, "false") || + !strcasecmp(value, "0") || !strcasecmp(value, "no") || + !strcasecmp(value, "disable") || !strcasecmp(value, "disabled")) + return 0; + return -1; +} + +static int cmd_runtime(int argc, char **argv) +{ + char params[1536]; + char escaped[512]; + + if (argc < 3) { + fprintf(stderr, "Usage: everlive-ctl runtime [value]\n" + " status Show the engine JSON status\n" + " led Set the static LED colour\n" + " led-mode \n" + " edid \n" + " hdr Toggle HDR processing\n" + " hdr-ev <-2.0..2.0> Set tonemap exposure in EV\n" + " hdr-passthrough <0|1> Toggle BT.2020 HDR passthrough\n" + " hotplug Pulse HDMI HPD / replay EDID\n" + " toast Show an on-screen notification\n" + " volume <0..150> Set desktop audio volume %%\n" + " mute <0|1> Mute/unmute desktop audio\n" + " osd <0|1> Toggle the on-screen overlay\n"); + return 1; + } + + const char *topic = argv[2]; + + if (strcmp(topic, "status") == 0) + return ctl_call("status.get", "{}"); + + if (strcmp(topic, "led") == 0) { + if (argc < 4) { + fprintf(stderr, "runtime led needs RRGGBB or off\n"); + return 1; + } + if (strcasecmp(argv[3], "off") == 0) + return ctl_call("led.mode.set", "{\"mode\":\"off\"}"); + unsigned long rgb = strtoul(argv[3], NULL, 16); + snprintf(params, sizeof(params), + "{\"r\":%lu,\"g\":%lu,\"b\":%lu}", + (rgb >> 16) & 0xff, (rgb >> 8) & 0xff, rgb & 0xff); + return ctl_call("led.set", params); + } + + if (strcmp(topic, "led-mode") == 0) { + if (argc < 4) { + fprintf(stderr, "runtime led-mode needs static|wave|rainbow|off\n"); + return 1; + } + json_escape(argv[3], escaped, sizeof(escaped)); + snprintf(params, sizeof(params), "{\"mode\":\"%s\"}", escaped); + return ctl_call("led.mode.set", params); + } + + if (strcmp(topic, "edid") == 0) { + if (argc < 4) { + fprintf(stderr, "runtime edid needs a profile name or path\n"); + return 1; + } + json_escape(argv[3], escaped, sizeof(escaped)); + snprintf(params, sizeof(params), "{\"profile\":\"%s\"}", escaped); + return ctl_call("edid.set", params); + } + + if (strcmp(topic, "hdr") == 0) { + const char *mode = argc >= 4 ? argv[3] : "auto"; + + json_escape(mode, escaped, sizeof(escaped)); + snprintf(params, sizeof(params), "{\"mode\":\"%s\"}", escaped); + return ctl_call("hdr.set", params); + } + + if (strcmp(topic, "hdr-ev") == 0) { + if (argc < 4) { + fprintf(stderr, "runtime hdr-ev needs an EV value\n"); + return 1; + } + snprintf(params, sizeof(params), "{\"exposure\":%s}", argv[3]); + return ctl_call("hdr.exposure", params); + } + + if (strcmp(topic, "hdr-passthrough") == 0) { + int on = parse_switch(argc >= 4 ? argv[3] : NULL); + + if (on < 0) { + fprintf(stderr, "runtime hdr-passthrough needs 0 or 1\n"); + return 1; + } + snprintf(params, sizeof(params), + "{\"enabled\":%s}", on ? "true" : "false"); + return ctl_call("hdr.passthrough", params); + } + + if (strcmp(topic, "hotplug") == 0) + return ctl_call("hotplug", "{\"pulse\":true}"); + + if (strcmp(topic, "toast") == 0) { + if (argc < 4) { + fprintf(stderr, "runtime toast needs text\n"); + return 1; + } + json_escape(argv[3], escaped, sizeof(escaped)); + snprintf(params, sizeof(params), "{\"message\":\"%s\"}", escaped); + return ctl_call("toast.show", params); + } + + if (strcmp(topic, "volume") == 0) { + if (argc < 4) { + fprintf(stderr, "runtime volume needs 0..150\n"); + return 1; + } + snprintf(params, sizeof(params), "{\"gain\":%g}", + strtod(argv[3], NULL) / 100.0); + return ctl_call("audio.volume.set", params); + } + + if (strcmp(topic, "mute") == 0) { + int on = parse_switch(argc >= 4 ? argv[3] : NULL); + + if (on < 0) { + fprintf(stderr, "runtime mute needs 0 or 1\n"); + return 1; + } + snprintf(params, sizeof(params), + "{\"muted\":%s}", on ? "true" : "false"); + return ctl_call("audio.mute.set", params); + } + + if (strcmp(topic, "osd") == 0) { + int on = parse_switch(argc >= 4 ? argv[3] : NULL); + + if (on < 0) { + fprintf(stderr, "runtime osd needs 0 or 1\n"); + return 1; + } + snprintf(params, sizeof(params), + "{\"enabled\":%s}", on ? "true" : "false"); + return ctl_call("osd.set", params); + } + + fprintf(stderr, "Unknown runtime topic: %s\n", topic); + return 1; +} + +int main(int argc, char **argv) +{ + if (argc < 2) { + return cmd_detect(); + } + + const char *cmd = argv[1]; + if (strcmp(cmd, "detect") == 0 || strcmp(cmd, "-d") == 0) { + return cmd_detect(); + } else if (strcmp(cmd, "status") == 0 || strcmp(cmd, "-s") == 0) { + return cmd_status(); + } else if (strcmp(cmd, "init") == 0 || strcmp(cmd, "-i") == 0) { + return cmd_init(); + } else if (strcmp(cmd, "service") == 0) { + return cmd_service(argc, argv); + } else if (strcmp(cmd, "list-models") == 0) { + return cmd_list_models(); + } else if (strcmp(cmd, "runtime") == 0 || strcmp(cmd, "ctl") == 0) { + return cmd_runtime(argc, argv); + } else if (strcmp(cmd, "help") == 0 || strcmp(cmd, "-h") == 0 || strcmp(cmd, "--help") == 0) { + print_usage(argv[0]); + return 0; + } else { + fprintf(stderr, "Unknown command: %s\n\n", cmd); + print_usage(argv[0]); + return 1; + } +} diff --git a/tools/libeverlive-core/everlive_frame.c b/tools/libeverlive-core/everlive_frame.c new file mode 100644 index 0000000..a3e7f81 --- /dev/null +++ b/tools/libeverlive-core/everlive_frame.c @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-2.0-only +#include "everlive_frame.h" + +static inline uint8_t clamp_u8(int val) +{ + if (val < 0) + return 0; + if (val > 255) + return 255; + return (uint8_t)val; +} + +void everlive_yuy2_to_rgb(const uint8_t *src, uint8_t *dst, int width, int height) +{ + if (!src || !dst || width <= 0 || height <= 0) + return; + + int total_pairs = (width * height) / 2; + const uint8_t *s = src; + uint8_t *d = dst; + + for (int i = 0; i < total_pairs; i++) { + int y0 = s[0]; + int u = s[1] - 128; + int y1 = s[2]; + int v = s[3] - 128; + s += 4; + + int r_off = (359 * v) >> 8; + int g_off = (88 * u + 183 * v) >> 8; + int b_off = (454 * u) >> 8; + + /* Pixel 0 */ + d[0] = clamp_u8(y0 + r_off); + d[1] = clamp_u8(y0 - g_off); + d[2] = clamp_u8(y0 + b_off); + + /* Pixel 1 */ + d[3] = clamp_u8(y1 + r_off); + d[4] = clamp_u8(y1 - g_off); + d[5] = clamp_u8(y1 + b_off); + d += 6; + } +} + +void everlive_yuy2_to_rgb_scaled(const uint8_t *src, uint8_t *dst, + int src_w, int src_h, + int dst_w, int dst_h) +{ + if (!src || !dst || src_w <= 0 || src_h <= 0 || dst_w <= 0 || dst_h <= 0) + return; + + /* Fixed-point step in 16.16 format */ + uint32_t step_x = ((uint32_t)src_w << 16) / (uint32_t)dst_w; + uint32_t step_y = ((uint32_t)src_h << 16) / (uint32_t)dst_h; + + uint32_t src_y_fp = 0; + for (int dy = 0; dy < dst_h; dy++) { + int sy = (int)(src_y_fp >> 16); + if (sy >= src_h) + sy = src_h - 1; + src_y_fp += step_y; + + const uint8_t *row = src + (size_t)sy * src_w * 2; + uint8_t *d = dst + (size_t)dy * dst_w * 3; + uint32_t src_x_fp = 0; + + for (int dx = 0; dx < dst_w; dx++) { + int sx = (int)(src_x_fp >> 16); + if (sx >= src_w) + sx = src_w - 1; + src_x_fp += step_x; + + /* In YUY2, pair index is (sx & ~1) * 2 */ + int pair_base = (sx & ~1) * 2; + int y = (sx & 1) ? row[pair_base + 2] : row[pair_base + 0]; + int u = row[pair_base + 1] - 128; + int v = row[pair_base + 3] - 128; + + int r_off = (359 * v) >> 8; + int g_off = (88 * u + 183 * v) >> 8; + int b_off = (454 * u) >> 8; + + d[0] = clamp_u8(y + r_off); + d[1] = clamp_u8(y - g_off); + d[2] = clamp_u8(y + b_off); + d += 3; + } + } +} diff --git a/tools/libeverlive-core/everlive_frame.h b/tools/libeverlive-core/everlive_frame.h new file mode 100644 index 0000000..e387183 --- /dev/null +++ b/tools/libeverlive-core/everlive_frame.h @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-2.0-only +#ifndef EVERLIVE_FRAME_H +#define EVERLIVE_FRAME_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Convert packed YUY2 (YUYV) 4:2:2 buffer to packed 24-bit RGB (RGB888). + * + * Fast SIMD/fixed-point conversion using BT.601 color matrix. + * + * @param src Pointer to source YUY2 buffer (width * height * 2 bytes). + * @param dst Pointer to destination RGB888 buffer (width * height * 3 bytes). + * @param width Frame width in pixels (must be even). + * @param height Frame height in pixels. + */ +void everlive_yuy2_to_rgb(const uint8_t *src, uint8_t *dst, int width, int height); + +/** + * Convert and downscale packed YUY2 (YUYV) buffer to packed 24-bit RGB (RGB888). + * + * Uses point sampling / box averaging for low-overhead preview scaling. + * + * @param src Pointer to source YUY2 buffer. + * @param dst Pointer to destination RGB888 buffer. + * @param src_w Source width in pixels. + * @param src_h Source height in pixels. + * @param dst_w Destination width in pixels. + * @param dst_h Destination height in pixels. + */ +void everlive_yuy2_to_rgb_scaled(const uint8_t *src, uint8_t *dst, + int src_w, int src_h, + int dst_w, int dst_h); + +#ifdef __cplusplus +} +#endif + +#endif /* EVERLIVE_FRAME_H */ From 4fc6ea5fdc722162784707a1b28ffa7dc3ed79cb Mon Sep 17 00:00:00 2001 From: PolygonPrincess Date: Tue, 10 Nov 2026 14:18:11 -0500 Subject: [PATCH 2/8] fix(core): address review feedback for libeverlive-core and everlive-ctl - Make card matching data-driven via fallback_match rather than hard-coded IDs - Distinguish unreadable sysfs IDs from legitimate zero values during discovery - Copy registered op vtables and freeze the registry after the first lookup - Validate hdr-ev/volume/led input; reject NaN/Inf and malformed JSON payloads - Detect the running engine via its control socket instead of pgrep - Execute systemctl/service commands directly with argv, without a shell - Launch the service script with fork/exec and report real spawn failures - Cache the request length, key-match "error", reject odd-width YUY2, fix docs - Add SONAME/versioned shared library plus install and uninstall targets - Add everlive_ctl protocol helpers and unit tests; extend registry tests - Translate remaining German build output to English --- build.sh | 12 +- tests/Makefile | 6 +- tests/test_card_registry.c | 179 ++++--- tests/test_ctl_proto.c | 190 ++++++++ tests/test_frame_convert.c | 8 + tools/libeverlive-core/Makefile | 62 ++- tools/libeverlive-core/everlive_card.c | 143 ++++-- tools/libeverlive-core/everlive_card.h | 19 +- tools/libeverlive-core/everlive_ctl.c | 494 +++++++++++++++----- tools/libeverlive-core/everlive_ctl.h | 48 ++ tools/libeverlive-core/everlive_ctl_proto.c | 165 +++++++ tools/libeverlive-core/everlive_frame.c | 28 +- tools/libeverlive-core/everlive_frame.h | 4 +- 13 files changed, 1080 insertions(+), 278 deletions(-) create mode 100644 tests/test_ctl_proto.c create mode 100644 tools/libeverlive-core/everlive_ctl.h create mode 100644 tools/libeverlive-core/everlive_ctl_proto.c diff --git a/build.sh b/build.sh index 6614682..144d0b2 100755 --- a/build.sh +++ b/build.sh @@ -9,23 +9,23 @@ CORE_DIR="${SCRIPT_DIR}/tools/libeverlive-core" GC573_DIR="${SCRIPT_DIR}/tools/opengc573-userd" GC570D_DIR="${SCRIPT_DIR}/tools/opengc570d-userd" -echo "[*] Baue EverLive Core Hardware Abstraction Library (libeverlive-core & everlive-ctl)..." +echo "[*] Building EverLive Core Hardware Abstraction Library (libeverlive-core & everlive-ctl)..." make -C "${CORE_DIR}" clean >/dev/null 2>&1 || true make -C "${CORE_DIR}" -j"$(nproc)" -echo "[*] Baue EverLive GC573 Userspace VFIO Engine (gc573_user_streamer & everlive_userd)..." +echo "[*] Building EverLive GC573 Userspace VFIO Engine (gc573_user_streamer & everlive_userd)..." make -C "${GC573_DIR}" clean >/dev/null 2>&1 || true make -C "${GC573_DIR}" -j"$(nproc)" -echo "[*] Baue EverLive GC570D (Live Gamer DUO) Userspace Engine..." +echo "[*] Building EverLive GC570D (Live Gamer DUO) Userspace Engine..." make -C "${GC570D_DIR}" clean >/dev/null 2>&1 || true make -C "${GC570D_DIR}" -j"$(nproc)" -echo "[+] EverLive Userspace Engines erfolgreich gebaut!" +echo "[+] EverLive Userspace Engines built successfully!" -# Optional: Baue Legacy Direct-Renderer falls Quellcode vorhanden +# Optional: build the legacy direct renderer if its source is present if [ -f "${SCRIPT_DIR}/tools/everlive/everlive_display.c" ]; then - echo "[*] Baue Legacy Direct-Renderer (everlive_display)..." + echo "[*] Building legacy direct renderer (everlive_display)..." SDL_FLAGS="$(pkg-config --cflags --libs sdl2 2>/dev/null || sdl2-config --cflags --libs 2>/dev/null || true)" if [ -n "${SDL_FLAGS}" ]; then gcc -O3 -o "${SCRIPT_DIR}/tools/everlive/everlive_display" "${SCRIPT_DIR}/tools/everlive/everlive_display.c" ${SDL_FLAGS} -lGL -lm -lX11 -lpthread diff --git a/tests/Makefile b/tests/Makefile index f05883a..13e15d5 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -7,7 +7,7 @@ DEFS := -DGC570D_USERSPACE DRIVER_DIR := ../tools/opengc570d-userd/src CORE_DIR := ../tools/libeverlive-core -TARGETS := test_edid test_config test_card_registry test_frame_convert +TARGETS := test_edid test_config test_card_registry test_frame_convert test_ctl_proto all: $(TARGETS) @@ -23,6 +23,9 @@ test_card_registry: test_card_registry.c $(CORE_DIR)/everlive_card.c test_frame_convert: test_frame_convert.c $(CORE_DIR)/everlive_frame.c $(CC) $(CFLAGS) $(INCLUDES) -o $@ $^ +test_ctl_proto: test_ctl_proto.c $(CORE_DIR)/everlive_ctl_proto.c + $(CC) $(CFLAGS) $(INCLUDES) -o $@ $^ + asan: CFLAGS += -fsanitize=address,undefined -fno-omit-frame-pointer asan: all @@ -32,6 +35,7 @@ test: all @./test_config @./test_card_registry @./test_frame_convert + @./test_ctl_proto clean: rm -f $(TARGETS) diff --git a/tests/test_card_registry.c b/tests/test_card_registry.c index 38f195e..bedfecf 100644 --- a/tests/test_card_registry.c +++ b/tests/test_card_registry.c @@ -14,6 +14,7 @@ #include #include #include +#include #define TEST_PASS(desc) printf(" [PASS] %s\n", (desc)) @@ -73,6 +74,13 @@ static void test_card_matching(void) assert(strcmp(gc573_wildcard->model_slug, "gc573") == 0); TEST_PASS("Defaulted 0x0054 to GC573 when subsystem != 0x5700"); + /* Arbitrary non-0x5700 subsystem uses the data-driven fallback entry. */ + const struct everlive_device_desc *gc573_fallback = + everlive_match_card(0x1461, 0x0054, 0x1234); + assert(gc573_fallback != NULL); + assert(strcmp(gc573_fallback->model_slug, "gc573") == 0); + TEST_PASS("Fallback match is driven by the registry, not hard-coded IDs"); + /* Secondary GC573 / BOLT PCIe ID (0x0055) */ const struct everlive_device_desc *gc573_alt = everlive_match_card(0x1461, 0x0055, 0x0000); @@ -102,6 +110,15 @@ static void create_file(const char *path, const char *content) fclose(f); } +static int remove_entry(const char *path, const struct stat *sb, int typeflag, + struct FTW *ftwbuf) +{ + (void)sb; + (void)typeflag; + (void)ftwbuf; + return remove(path); +} + static void test_mock_sysfs_scan(void) { printf("Testing everlive_detect_installed_cards with mock sysfs...\n"); @@ -111,14 +128,16 @@ static void test_mock_sysfs_scan(void) assert(res != NULL); /* Mock device 1: GC570D at 0000:03:00.0 */ - char d1[512], d2[512], d3[512]; + char d1[512], d2[512], d3[512], d4[512]; snprintf(d1, sizeof(d1), "%s/0000:03:00.0", tmp_root); snprintf(d2, sizeof(d2), "%s/0000:04:00.0", tmp_root); snprintf(d3, sizeof(d3), "%s/0000:01:00.0", tmp_root); + snprintf(d4, sizeof(d4), "%s/0000:05:00.0", tmp_root); assert(mkdir(d1, 0755) == 0); assert(mkdir(d2, 0755) == 0); assert(mkdir(d3, 0755) == 0); + assert(mkdir(d4, 0755) == 0); /* Populate GC570D */ char p[PATH_MAX]; @@ -157,6 +176,14 @@ static void test_mock_sysfs_scan(void) snprintf(p, sizeof(p), "%s/subsystem_device", d3); create_file(p, "0x103c\n"); + /* 0x1461:0x0054 with NO subsystem_device file. A read failure must not + * be treated as subsystem 0 and silently guessed as a GC573. + */ + snprintf(p, sizeof(p), "%s/vendor", d4); + create_file(p, "0x1461\n"); + snprintf(p, sizeof(p), "%s/device", d4); + create_file(p, "0x0054\n"); + /* Scan using mock root */ struct everlive_device found[8]; int count = everlive_detect_installed_cards(found, 8, tmp_root); @@ -186,12 +213,15 @@ static void test_mock_sysfs_scan(void) assert(saw_gc573); TEST_PASS("Correctly populated BDF, IOMMU group, and VFIO binding state"); - /* Cleanup mock directory */ - unlink(d1); // in case - char cmd[PATH_MAX + 16]; - snprintf(cmd, sizeof(cmd), "rm -rf '%s'", tmp_root); - int ret = system(cmd); - (void)ret; + /* The device with an unreadable subsystem ID must be skipped rather than + * misidentified as a GC573 via the fallback path. */ + for (int i = 0; i < count; i++) + assert(strcmp(found[i].pci_bdf, "0000:05:00.0") != 0); + TEST_PASS("Missing subsystem_device does not misidentify a GC570D"); + + /* Cleanup mock directory. FTW_PHYS keeps nftw from following the + * driver/iommu_group symlinks into the real sysfs tree. */ + assert(nftw(tmp_root, remove_entry, 16, FTW_DEPTH | FTW_PHYS) == 0); } static int g_custom_probe_called = 0; @@ -202,16 +232,56 @@ static int mock_custom_probe(struct everlive_device *dev) return 0; } -static void test_card_ops_binding(void) +/* + * Register a vtable that lives only for this call. The core must copy it, + * otherwise the binding table is left pointing at a dead stack frame. This + * runs before any lookup, because the first lookup freezes the registry. + */ +static void register_ops_from_stack(void) { - printf("Testing everlive_card_ops binding and defaults...\n"); + const struct everlive_device_desc *unbound = + everlive_match_card(0x1461, 0x0054, 0x5700); /* GC570D */ + struct everlive_card_ops stack_ops; + + assert(unbound != NULL && unbound->ops != NULL); + stack_ops = *unbound->ops; + stack_ops.probe = mock_custom_probe; + assert(everlive_register_card_ops("gc573", &stack_ops) == 0); +} +static void test_card_ops_binding(void) +{ const struct everlive_device_desc *gc573_desc = everlive_match_card(0x1461, 0x0054, 0x5730); - assert(gc573_desc != NULL); + const struct everlive_device_desc *gc570d_desc = + everlive_match_card(0x1461, 0x0054, 0x5700); + struct everlive_device dev; + const struct everlive_card_ops *bound; + const struct everlive_card_ops *default_ops; + + printf("Testing everlive_card_ops binding and defaults...\n"); + assert(gc573_desc != NULL && gc570d_desc != NULL); + + /* 1. Register before any lookup; the first lookup freezes the table. */ + register_ops_from_stack(); - /* 1. Default ops retrieval */ - const struct everlive_card_ops *default_ops = everlive_card_ops_for(gc573_desc); + /* 2. The copied vtable is visible and outlives the caller's stack. */ + bound = everlive_card_ops_for(gc573_desc); + assert(bound != NULL); + assert(bound->probe == mock_custom_probe); + + memset(&dev, 0, sizeof(dev)); + g_custom_probe_called = 0; + assert(bound->probe(&dev) == 0); + assert(g_custom_probe_called == 1); + TEST_PASS("Registered vtable is copied and outlives the caller's stack"); + + /* 3. Registration is closed once a lookup has happened. */ + assert(everlive_register_card_ops("gc573", bound) == -EBUSY); + TEST_PASS("Registry freezes after the first ops lookup"); + + /* 4. An unbound model still falls back to the built-in defaults. */ + default_ops = everlive_card_ops_for(gc570d_desc); assert(default_ops != NULL); assert(default_ops->probe != NULL); assert(default_ops->init_hardware != NULL); @@ -222,15 +292,13 @@ static void test_card_ops_binding(void) assert(default_ops->enable_passthrough != NULL); assert(default_ops->set_led != NULL); assert(default_ops->get_telemetry != NULL); - TEST_PASS("Default ops returned for known model"); + TEST_PASS("Default ops returned for an unbound model"); - /* 2. Default ops return -ENOTSUP / -EINVAL appropriately */ - struct everlive_device dev; memset(&dev, 0, sizeof(dev)); assert(default_ops->probe(&dev) == -EINVAL); - dev.desc = gc573_desc; - snprintf(dev.pci_bdf, sizeof(dev.pci_bdf), "0000:04:00.0"); + dev.desc = gc570d_desc; + snprintf(dev.pci_bdf, sizeof(dev.pci_bdf), "0000:03:00.0"); assert(default_ops->probe(&dev) == 0); assert(default_ops->init_hardware(&dev) == -ENOTSUP); assert(default_ops->standby(&dev) == -ENOTSUP); @@ -247,24 +315,10 @@ static void test_card_ops_binding(void) default_ops->release(&dev); TEST_PASS("Default ops behave safely and return -ENOTSUP for hardware actions"); - /* 3. Register custom backend ops */ - struct everlive_card_ops custom_ops = *default_ops; - custom_ops.probe = mock_custom_probe; - - assert(everlive_register_card_ops("gc573", &custom_ops) == 0); - const struct everlive_card_ops *bound = everlive_card_ops_for(gc573_desc); - assert(bound != NULL); - assert(bound->probe == mock_custom_probe); - - g_custom_probe_called = 0; - assert(bound->probe(&dev) == 0); - assert(g_custom_probe_called == 1); - TEST_PASS("Successfully registered and dispatched custom backend ops"); - - /* 4. Invalid arguments */ - assert(everlive_register_card_ops(NULL, &custom_ops) == -EINVAL); + /* 5. Invalid arguments are still reported after the freeze. */ + assert(everlive_register_card_ops(NULL, default_ops) == -EINVAL); assert(everlive_register_card_ops("gc573", NULL) == -EINVAL); - assert(everlive_register_card_ops("nonexistent_model", &custom_ops) == -ENOENT); + assert(everlive_register_card_ops("nonexistent_model", default_ops) == -ENOENT); TEST_PASS("Rejects NULL arguments and unknown model slugs for ops registration"); } @@ -323,62 +377,6 @@ static void test_resume_detection(void) TEST_PASS("Resume detection threshold strictly enforced at > 2.0 seconds delta"); } -static void test_everlive_ctl_commands(void) -{ - printf("Testing everlive-ctl runtime command construction...\n"); - - char req[512]; - char params[256]; - - /* 1. Status command */ - snprintf(req, sizeof(req), "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"%s\",\"params\":%s}\n", - "status.get", "{}"); - assert(strstr(req, "\"method\":\"status.get\"") != NULL); - assert(strstr(req, "\"params\":{}") != NULL); - TEST_PASS("Status request formatting verified"); - - /* 2. LED static color RGB calculation */ - unsigned long rgb = strtoul("FF8020", NULL, 16); - unsigned long r = (rgb >> 16) & 0xff; - unsigned long g = (rgb >> 8) & 0xff; - unsigned long b = rgb & 0xff; - assert(r == 0xff && g == 0x80 && b == 0x20); - snprintf(params, sizeof(params), "{\"r\":%lu,\"g\":%lu,\"b\":%lu}", r, g, b); - snprintf(req, sizeof(req), "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"%s\",\"params\":%s}\n", - "led.set", params); - assert(strstr(req, "\"r\":255,\"g\":128,\"b\":32") != NULL); - TEST_PASS("LED RGB formatting verified"); - - /* 3. EDID profile command */ - snprintf(params, sizeof(params), "{\"profile\":\"%s\"}", "1440p120"); - snprintf(req, sizeof(req), "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"%s\",\"params\":%s}\n", - "edid.set", params); - assert(strstr(req, "\"method\":\"edid.set\"") != NULL); - assert(strstr(req, "\"profile\":\"1440p120\"") != NULL); - TEST_PASS("EDID profile request formatting verified"); - - /* 4. Hotplug pulse command */ - snprintf(req, sizeof(req), "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"%s\",\"params\":%s}\n", - "hotplug", "{\"pulse\":true}"); - assert(strstr(req, "\"method\":\"hotplug\"") != NULL); - assert(strstr(req, "\"pulse\":true") != NULL); - TEST_PASS("Hotplug pulse request formatting verified"); - - /* 5. HDR exposure command */ - snprintf(params, sizeof(params), "{\"exposure\":%s}", "-0.5"); - snprintf(req, sizeof(req), "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"%s\",\"params\":%s}\n", - "hdr.exposure", params); - assert(strstr(req, "\"method\":\"hdr.exposure\"") != NULL); - assert(strstr(req, "\"exposure\":-0.5") != NULL); - TEST_PASS("HDR exposure request formatting verified"); - - /* 6. Audio volume / mute switch parsing */ - double vol = 85.0; - snprintf(params, sizeof(params), "{\"gain\":%g}", vol / 100.0); - assert(strstr(params, "0.85") != NULL); - TEST_PASS("Audio volume gain calculation verified"); -} - int main(void) { printf("==================================================\n"); @@ -390,7 +388,6 @@ int main(void) test_mock_sysfs_scan(); test_card_ops_binding(); test_resume_detection(); - test_everlive_ctl_commands(); printf("==================================================\n"); printf(" ALL CARD REGISTRY TESTS PASSED!\n"); diff --git a/tests/test_ctl_proto.c b/tests/test_ctl_proto.c new file mode 100644 index 0000000..5c94342 --- /dev/null +++ b/tests/test_ctl_proto.c @@ -0,0 +1,190 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * test_ctl_proto.c — Unit tests for the everlive-ctl request helpers. + * + * These exercise the production serialization/validation code directly, so a + * change in everlive_ctl_proto.c cannot silently drift away from the tests. + */ + +#include "everlive_ctl.h" + +#include +#include +#include + +#define TEST_PASS(desc) printf(" [PASS] %s\n", (desc)) + +static void test_build_request(void) +{ + char req[512]; + char tiny[8]; + + printf("Testing everlive_ctl_build_request...\n"); + + assert(everlive_ctl_build_request("status.get", "{}", req, + sizeof(req)) == 0); + assert(strcmp(req, + "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"status.get\"," + "\"params\":{}}\n") == 0); + TEST_PASS("status.get request is well formed"); + + assert(everlive_ctl_build_request("hotplug", "{\"pulse\":true}", req, + sizeof(req)) == 0); + assert(strstr(req, "\"method\":\"hotplug\"") != NULL); + assert(strstr(req, "\"params\":{\"pulse\":true}") != NULL); + TEST_PASS("hotplug request preserves params"); + + /* NULL / empty params become an empty JSON object. */ + assert(everlive_ctl_build_request("status.get", NULL, req, + sizeof(req)) == 0); + assert(strstr(req, "\"params\":{}") != NULL); + TEST_PASS("NULL params default to {}"); + + /* Truncation must be reported, not silently accepted. */ + assert(everlive_ctl_build_request("status.get", "{}", tiny, + sizeof(tiny)) == -1); + TEST_PASS("Too-small buffer is rejected"); +} + +static void test_parse_double(void) +{ + double v; + + printf("Testing everlive_ctl_parse_double...\n"); + + assert(everlive_ctl_parse_double("85", &v) == 0 && v == 85.0); + assert(everlive_ctl_parse_double("-0.5", &v) == 0 && v == -0.5); + assert(everlive_ctl_parse_double("1e3", &v) == 0 && v == 1000.0); + TEST_PASS("Valid decimal numbers accepted"); + + assert(everlive_ctl_parse_double("nan", &v) == -1); + assert(everlive_ctl_parse_double("inf", &v) == -1); + assert(everlive_ctl_parse_double("-inf", &v) == -1); + assert(everlive_ctl_parse_double("1e999", &v) == -1); + TEST_PASS("NaN, infinity and overflow rejected"); + + assert(everlive_ctl_parse_double("abc", &v) == -1); + assert(everlive_ctl_parse_double("12abc", &v) == -1); + assert(everlive_ctl_parse_double("\"; rm -rf /", &v) == -1); + assert(everlive_ctl_parse_double("", &v) == -1); + assert(everlive_ctl_parse_double(NULL, &v) == -1); + TEST_PASS("Garbage and embedded JSON rejected"); +} + +static void test_parse_hex_rgb(void) +{ + unsigned long rgb; + + printf("Testing everlive_ctl_parse_hex_rgb...\n"); + + assert(everlive_ctl_parse_hex_rgb("FF8020", &rgb) == 0); + assert(rgb == 0xff8020UL); + assert(everlive_ctl_parse_hex_rgb("ff8020", &rgb) == 0); + assert(rgb == 0xff8020UL); + assert(everlive_ctl_parse_hex_rgb("0", &rgb) == 0 && rgb == 0); + TEST_PASS("Valid RRGGBB values accepted"); + + assert(everlive_ctl_parse_hex_rgb("GGGGGG", &rgb) == -1); + assert(everlive_ctl_parse_hex_rgb("1000000", &rgb) == -1); + assert(everlive_ctl_parse_hex_rgb("-1", &rgb) == -1); + assert(everlive_ctl_parse_hex_rgb("", &rgb) == -1); + assert(everlive_ctl_parse_hex_rgb(NULL, &rgb) == -1); + TEST_PASS("Garbage, overflow and negative values rejected"); +} + +static void test_json_escape(void) +{ + char out[128]; + char tiny[8]; + + printf("Testing everlive_ctl_json_escape...\n"); + + everlive_ctl_json_escape("plain", out, sizeof(out)); + assert(strcmp(out, "plain") == 0); + + everlive_ctl_json_escape("a\"b\\c\nd\te", out, sizeof(out)); + assert(strcmp(out, "a\\\"b\\\\c\\nd\\te") == 0); + TEST_PASS("quotes, backslashes and whitespace escaped"); + + everlive_ctl_json_escape("\x01", out, sizeof(out)); + assert(strcmp(out, "\\u0001") == 0); + TEST_PASS("control bytes escaped as \\uXXXX"); + + everlive_ctl_json_escape("abcdefghijklmnop", tiny, sizeof(tiny)); + assert(memchr(tiny, '\0', sizeof(tiny)) != NULL); + TEST_PASS("Truncated output stays NUL-terminated"); +} + +static void test_response_has_error(void) +{ + printf("Testing everlive_ctl_response_has_error...\n"); + + assert(everlive_ctl_response_has_error( + "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-1}}")); + assert(everlive_ctl_response_has_error("{ \"error\" : {} }")); + assert(everlive_ctl_response_has_error("{\"error\":null}")); + + /* The word "error" inside a value must not count as a failure. */ + assert(!everlive_ctl_response_has_error( + "{\"result\":{\"message\":\"No error detected\"}}")); + assert(!everlive_ctl_response_has_error( + "{\"result\":{\"status\":\"error\"}}")); + assert(!everlive_ctl_response_has_error("{\"result\":{}}")); + assert(!everlive_ctl_response_has_error(NULL)); + TEST_PASS("Only an \"error\" key is treated as a failure"); +} + +static void test_command_payloads(void) +{ + char params[256]; + char req[512]; + double ev; + double percent; + unsigned long rgb; + + printf("Testing numeric command payloads...\n"); + + /* hdr-ev */ + assert(everlive_ctl_parse_double("-0.5", &ev) == 0); + assert(everlive_ctl_format_exposure(ev, params, sizeof(params)) == 0); + assert(strcmp(params, "{\"exposure\":-0.5}") == 0); + assert(everlive_ctl_build_request("hdr.exposure", params, req, + sizeof(req)) == 0); + assert(strstr(req, "\"exposure\":-0.5") != NULL); + TEST_PASS("hdr-ev payload uses the parsed value"); + + /* volume: 85% -> 0.85 gain */ + assert(everlive_ctl_parse_double("85", &percent) == 0); + assert(everlive_ctl_format_volume(percent, params, + sizeof(params)) == 0); + assert(strcmp(params, "{\"gain\":0.85}") == 0); + assert(everlive_ctl_format_volume(150.0, params, sizeof(params)) == 0); + assert(everlive_ctl_format_volume(-1.0, params, sizeof(params)) == -1); + assert(everlive_ctl_format_volume(151.0, params, sizeof(params)) == -1); + TEST_PASS("volume gain is scaled and range-checked"); + + /* led: FF8020 -> 255,128,32 */ + assert(everlive_ctl_parse_hex_rgb("FF8020", &rgb) == 0); + assert(everlive_ctl_format_led(rgb, params, sizeof(params)) == 0); + assert(strcmp(params, "{\"r\":255,\"g\":128,\"b\":32}") == 0); + TEST_PASS("led payload decomposes RRGGBB correctly"); +} + +int main(void) +{ + printf("==================================================\n"); + printf(" everlive-ctl Protocol & Validation Tests \n"); + printf("==================================================\n"); + + test_build_request(); + test_parse_double(); + test_parse_hex_rgb(); + test_json_escape(); + test_response_has_error(); + test_command_payloads(); + + printf("==================================================\n"); + printf(" ALL EVERLIVE-CTL PROTOCOL TESTS PASSED!\n"); + printf("==================================================\n"); + return 0; +} diff --git a/tests/test_frame_convert.c b/tests/test_frame_convert.c index 0b519a6..2fb8c20 100644 --- a/tests/test_frame_convert.c +++ b/tests/test_frame_convert.c @@ -89,6 +89,14 @@ static void test_invalid_parameters(void) everlive_yuy2_to_rgb_scaled(buf, buf, 4, 4, 0, 2); everlive_yuy2_to_rgb_scaled(buf, buf, 4, 4, 2, 0); + /* Odd width is not a valid YUY2 layout; the output must stay untouched. */ + uint8_t odd_src[8] = { 0 }; + uint8_t odd_dst[12]; + memset(odd_dst, 0xAA, sizeof(odd_dst)); + everlive_yuy2_to_rgb(odd_src, odd_dst, 3, 1); + for (size_t i = 0; i < sizeof(odd_dst); i++) + assert(odd_dst[i] == 0xAA); + printf(" Edge cases OK.\n"); } diff --git a/tools/libeverlive-core/Makefile b/tools/libeverlive-core/Makefile index ec2d652..34ae301 100644 --- a/tools/libeverlive-core/Makefile +++ b/tools/libeverlive-core/Makefile @@ -3,27 +3,73 @@ CC ?= gcc AR ?= ar CFLAGS ?= -O2 -g -Wall -Wextra -Wno-unused-parameter -std=gnu11 -D_GNU_SOURCE -fPIC +SOVERSION := 0 +VERSION := 0.1.0 +SONAME := libeverlive_core.so.$(SOVERSION) +SHARED_REAL := libeverlive_core.so.$(VERSION) + TARGET_STATIC := libeverlive_core.a TARGET_SHARED := libeverlive_core.so TARGET_CLI := everlive-ctl -OBJS := everlive_card.o everlive_frame.o +PREFIX ?= /usr/local +DESTDIR ?= +BINDIR ?= $(PREFIX)/bin +LIBDIR ?= $(PREFIX)/lib +INCLUDEDIR ?= $(PREFIX)/include +SYSTEMD_USER_DIR ?= $(PREFIX)/lib/systemd/user + +OBJS := everlive_card.o everlive_frame.o +CTL_OBJS := everlive_ctl.o everlive_ctl_proto.o all: $(TARGET_STATIC) $(TARGET_SHARED) $(TARGET_CLI) $(TARGET_STATIC): $(OBJS) $(AR) rcs $@ $^ -$(TARGET_SHARED): $(OBJS) - $(CC) -shared -o $@ $^ +$(SHARED_REAL): $(OBJS) + $(CC) -shared -Wl,-soname,$(SONAME) -o $@ $^ + +$(SONAME): $(SHARED_REAL) + ln -sf $(SHARED_REAL) $@ -$(TARGET_CLI): everlive_ctl.o $(TARGET_STATIC) - $(CC) $(CFLAGS) -o $@ everlive_ctl.o $(TARGET_STATIC) +$(TARGET_SHARED): $(SONAME) + ln -sf $(SONAME) $@ -%.o: %.c everlive_card.h everlive_frame.h +$(TARGET_CLI): $(CTL_OBJS) $(TARGET_STATIC) + $(CC) $(CFLAGS) -o $@ $^ + +%.o: %.c everlive_card.h everlive_frame.h everlive_ctl.h $(CC) $(CFLAGS) -c -o $@ $< +install: all + install -d $(DESTDIR)$(BINDIR) $(DESTDIR)$(LIBDIR) \ + $(DESTDIR)$(INCLUDEDIR) $(DESTDIR)$(SYSTEMD_USER_DIR) + install -m 0755 $(TARGET_CLI) $(DESTDIR)$(BINDIR)/$(TARGET_CLI) + install -m 0755 everlive-service.sh $(DESTDIR)$(BINDIR)/everlive-service + install -m 0644 $(SHARED_REAL) $(DESTDIR)$(LIBDIR)/$(SHARED_REAL) + ln -sf $(SHARED_REAL) $(DESTDIR)$(LIBDIR)/$(SONAME) + ln -sf $(SONAME) $(DESTDIR)$(LIBDIR)/$(TARGET_SHARED) + install -m 0644 $(TARGET_STATIC) $(DESTDIR)$(LIBDIR)/$(TARGET_STATIC) + install -m 0644 everlive_card.h everlive_frame.h everlive_ctl.h \ + $(DESTDIR)$(INCLUDEDIR)/ + install -m 0644 everlive.service \ + $(DESTDIR)$(SYSTEMD_USER_DIR)/everlive.service + +uninstall: + rm -f $(DESTDIR)$(BINDIR)/$(TARGET_CLI) \ + $(DESTDIR)$(BINDIR)/everlive-service \ + $(DESTDIR)$(LIBDIR)/$(SHARED_REAL) \ + $(DESTDIR)$(LIBDIR)/$(SONAME) \ + $(DESTDIR)$(LIBDIR)/$(TARGET_SHARED) \ + $(DESTDIR)$(LIBDIR)/$(TARGET_STATIC) \ + $(DESTDIR)$(INCLUDEDIR)/everlive_card.h \ + $(DESTDIR)$(INCLUDEDIR)/everlive_frame.h \ + $(DESTDIR)$(INCLUDEDIR)/everlive_ctl.h \ + $(DESTDIR)$(SYSTEMD_USER_DIR)/everlive.service + clean: - rm -f $(OBJS) everlive_ctl.o $(TARGET_STATIC) $(TARGET_SHARED) $(TARGET_CLI) + rm -f $(OBJS) $(CTL_OBJS) $(TARGET_STATIC) $(TARGET_SHARED) \ + $(SONAME) $(SHARED_REAL) $(TARGET_CLI) -.PHONY: all clean +.PHONY: all clean install uninstall diff --git a/tools/libeverlive-core/everlive_card.c b/tools/libeverlive-core/everlive_card.c index a784fa7..4de56c9 100644 --- a/tools/libeverlive-core/everlive_card.c +++ b/tools/libeverlive-core/everlive_card.c @@ -117,12 +117,20 @@ static const struct everlive_card_ops kEverliveDefaultOps = { struct everlive_ops_binding { const char *model_slug; - const struct everlive_card_ops *ops; + struct everlive_card_ops ops; /* copied, not borrowed */ }; static struct everlive_ops_binding kEverliveOpsBindings[EVERLIVE_MAX_OPS_BINDINGS]; static size_t kEverliveOpsBindingCount; +/* + * Registration is a single-threaded startup step. The first lookup freezes + * the table so a backend cannot rebind while other threads are resolving + * operations. This is an enforcement guard, not a substitute for locking: + * callers must still register before starting any threads. + */ +static bool kEverliveOpsFrozen; + /* ------------------------------------------------------------------------- * Static Hardware Registry of Supported Capture Cards * ------------------------------------------------------------------------- */ @@ -136,6 +144,7 @@ static const struct everlive_device_desc kEverliveCardRegistry[] = { .capabilities = EVERLIVE_CAP_VIDEO_CAPTURE | EVERLIVE_CAP_AUDIO_CAPTURE | EVERLIVE_CAP_PASSTHROUGH | + EVERLIVE_CAP_1080P60 | EVERLIVE_CAP_DUAL_INPUT | EVERLIVE_CAP_RGB_LED | EVERLIVE_CAP_HARDWARE_SPLITTER | @@ -152,6 +161,7 @@ static const struct everlive_device_desc kEverliveCardRegistry[] = { .capabilities = EVERLIVE_CAP_VIDEO_CAPTURE | EVERLIVE_CAP_AUDIO_CAPTURE | EVERLIVE_CAP_PASSTHROUGH | + EVERLIVE_CAP_1080P60 | EVERLIVE_CAP_4K60 | EVERLIVE_CAP_1440P120 | EVERLIVE_CAP_1080P240 | @@ -162,6 +172,8 @@ static const struct everlive_device_desc kEverliveCardRegistry[] = { EVERLIVE_CAP_EDID_PASSTHROUGH, .num_input_ports = 1, .ops = &kEverliveDefaultOps, + /* A 0x0054 device with any other subsystem is a GC573. */ + .fallback_match = true, }, { .vendor_id = 0x1461, @@ -172,6 +184,7 @@ static const struct everlive_device_desc kEverliveCardRegistry[] = { .capabilities = EVERLIVE_CAP_VIDEO_CAPTURE | EVERLIVE_CAP_AUDIO_CAPTURE | EVERLIVE_CAP_PASSTHROUGH | + EVERLIVE_CAP_1080P60 | EVERLIVE_CAP_4K60 | EVERLIVE_CAP_1440P120 | EVERLIVE_CAP_1080P240 | @@ -192,6 +205,7 @@ static const struct everlive_device_desc kEverliveCardRegistry[] = { .capabilities = EVERLIVE_CAP_VIDEO_CAPTURE | EVERLIVE_CAP_AUDIO_CAPTURE | EVERLIVE_CAP_PASSTHROUGH | + EVERLIVE_CAP_1080P60 | EVERLIVE_CAP_4K60 | EVERLIVE_CAP_1440P120 | EVERLIVE_CAP_HDR10 | @@ -229,10 +243,13 @@ int everlive_register_card_ops(const char *model_slug, if (!canonical) return -ENOENT; + if (kEverliveOpsFrozen) + return -EBUSY; + /* Replace an existing binding for the same model. */ for (size_t i = 0; i < kEverliveOpsBindingCount; i++) { if (kEverliveOpsBindings[i].model_slug == canonical->model_slug) { - kEverliveOpsBindings[i].ops = ops; + kEverliveOpsBindings[i].ops = *ops; return 0; } } @@ -242,7 +259,7 @@ int everlive_register_card_ops(const char *model_slug, kEverliveOpsBindings[kEverliveOpsBindingCount].model_slug = canonical->model_slug; - kEverliveOpsBindings[kEverliveOpsBindingCount].ops = ops; + kEverliveOpsBindings[kEverliveOpsBindingCount].ops = *ops; kEverliveOpsBindingCount++; return 0; } @@ -250,6 +267,9 @@ int everlive_register_card_ops(const char *model_slug, const struct everlive_card_ops * everlive_card_ops_for(const struct everlive_device_desc *desc) { + /* Any resolution closes the registry against further registration. */ + kEverliveOpsFrozen = true; + if (!desc) return &kEverliveDefaultOps; @@ -257,40 +277,49 @@ everlive_card_ops_for(const struct everlive_device_desc *desc) if (kEverliveOpsBindings[i].model_slug == desc->model_slug || strcmp(kEverliveOpsBindings[i].model_slug, desc->model_slug) == 0) - return kEverliveOpsBindings[i].ops; + return &kEverliveOpsBindings[i].ops; } return desc->ops ? desc->ops : &kEverliveDefaultOps; } -const struct everlive_device_desc *everlive_match_card(uint16_t vendor, - uint16_t device, - uint16_t subsystem) +/* + * @subsystem_known is false when sysfs could not be read. In that case an + * exact-subsystem entry can never match and a fallback entry is never used, + * so a missing ID file is not silently reported as a specific model. + */ +static const struct everlive_device_desc * +everlive_match_card_ex(uint16_t vendor, uint16_t device, + bool subsystem_known, uint16_t subsystem) { - /* Pass 1: Look for exact match (vendor, device, subsystem) */ + /* Pass 1: exact match (vendor, device, subsystem) */ for (size_t i = 0; i < REGISTRY_COUNT; i++) { const struct everlive_device_desc *d = &kEverliveCardRegistry[i]; - if (d->vendor_id == vendor && d->device_id == device) { - if (d->subsystem_id != 0 && d->subsystem_id == subsystem) - return d; - } - } - - /* Special case: GC570D is distinguished by subsystem 0x5700. - * If device is 0x0054 and subsystem is NOT 0x5700, it is a GC573. - */ - if (vendor == 0x1461 && device == 0x0054 && subsystem != 0x5700) { - for (size_t i = 0; i < REGISTRY_COUNT; i++) { - if (strcmp(kEverliveCardRegistry[i].model_slug, "gc573") == 0) - return &kEverliveCardRegistry[i]; - } + if (d->vendor_id == vendor && d->device_id == device && + d->subsystem_id != 0 && subsystem_known && + d->subsystem_id == subsystem) + return d; } - /* Pass 2: Look for wildcard subsystem match */ + /* Pass 2: wildcard subsystem entries (match any/unknown subsystem) */ for (size_t i = 0; i < REGISTRY_COUNT; i++) { const struct everlive_device_desc *d = &kEverliveCardRegistry[i]; - if (d->vendor_id == vendor && d->device_id == device) { - if (d->subsystem_id == 0) + if (d->vendor_id == vendor && d->device_id == device && + d->subsystem_id == 0) + return d; + } + + /* Pass 3: per-device fallback, data-driven via fallback_match. This + * replaces the old hard-coded 0x0054 -> "gc573" special case. It only + * applies when the subsystem is known, so an unreadable sysfs file can + * never turn a GC570D into a GC573. + */ + if (subsystem_known) { + for (size_t i = 0; i < REGISTRY_COUNT; i++) { + const struct everlive_device_desc *d = + &kEverliveCardRegistry[i]; + if (d->vendor_id == vendor && d->device_id == device && + d->fallback_match) return d; } } @@ -298,19 +327,50 @@ const struct everlive_device_desc *everlive_match_card(uint16_t vendor, return NULL; } -static uint16_t read_sysfs_hex16(const char *path) +const struct everlive_device_desc *everlive_match_card(uint16_t vendor, + uint16_t device, + uint16_t subsystem) +{ + return everlive_match_card_ex(vendor, device, true, subsystem); +} + +/* + * Read a 16-bit hex value from sysfs. Returns 0 on success and stores the + * value in @out, or a negative errno on failure. Callers must not confuse a + * read failure with the legitimate value 0 (which is a wildcard sentinel). + */ +static int read_sysfs_hex16(const char *path, uint16_t *out) { - FILE *f = fopen(path, "r"); + FILE *f; + char buf[32]; + char *end = NULL; + unsigned long val; + + if (!path || !out) + return -EINVAL; + + f = fopen(path, "r"); if (!f) - return 0; + return -errno; - char buf[32]; - uint16_t val = 0; - if (fgets(buf, sizeof(buf), f)) { - val = (uint16_t)strtoul(buf, NULL, 16); + if (!fgets(buf, sizeof(buf), f)) { + fclose(f); + return -EIO; } fclose(f); - return val; + + errno = 0; + val = strtoul(buf, &end, 16); + if (end == buf || errno == ERANGE || val > 0xffff) + return -EINVAL; + + while (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r') + end++; + if (*end != '\0') + return -EINVAL; + + *out = (uint16_t)val; + return 0; } static int read_iommu_group(const char *dev_dir) @@ -389,12 +449,21 @@ int everlive_detect_installed_cards(struct everlive_device *out_devices, snprintf(device_path, sizeof(device_path), "%s/device", dev_path); snprintf(subsystem_path, sizeof(subsystem_path), "%s/subsystem_device", dev_path); - uint16_t vendor = read_sysfs_hex16(vendor_path); - uint16_t device = read_sysfs_hex16(device_path); - uint16_t subsystem = read_sysfs_hex16(subsystem_path); + uint16_t vendor = 0; + uint16_t device = 0; + uint16_t subsystem = 0; + bool subsystem_known; + + if (read_sysfs_hex16(vendor_path, &vendor) < 0 || + read_sysfs_hex16(device_path, &device) < 0) + continue; + + subsystem_known = + read_sysfs_hex16(subsystem_path, &subsystem) == 0; const struct everlive_device_desc *desc = - everlive_match_card(vendor, device, subsystem); + everlive_match_card_ex(vendor, device, + subsystem_known, subsystem); if (desc) { struct everlive_device *dev = &out_devices[found_count]; diff --git a/tools/libeverlive-core/everlive_card.h b/tools/libeverlive-core/everlive_card.h index 1aaa40f..fae3cba 100644 --- a/tools/libeverlive-core/everlive_card.h +++ b/tools/libeverlive-core/everlive_card.h @@ -108,6 +108,7 @@ struct everlive_device_desc { uint32_t capabilities; int num_input_ports; const struct everlive_card_ops *ops; + bool fallback_match; /* Last-resort match for this vendor/device */ }; /* ------------------------------------------------------------------------- @@ -172,9 +173,18 @@ const struct everlive_device_desc *everlive_get_card_registry(size_t *out_count) /* * Bind a backend implementation to every registry entry with @model_slug. - * Backends own their hardware context; the core only stores the vtable. - * Returns 0, -EINVAL for bad arguments, -ENOENT for an unknown model, or - * -ENOSPC when the binding table is full. + * Backends own their hardware context; the core copies the vtable by value, + * so @ops only has to remain valid for the duration of this call. + * + * Registration is a startup operation: it must finish before the first + * everlive_card_ops_for() call, which is also the point where the library may + * start being used from multiple threads. No lock is taken, so callers must + * register before spawning threads. Once a lookup has happened the registry + * is frozen and later calls return -EBUSY. + * + * Returns 0, -EINVAL for bad arguments, -ENOENT for an unknown model, + * -EBUSY if the registry is already frozen, or -ENOSPC when the binding table + * is full. */ int everlive_register_card_ops(const char *model_slug, const struct everlive_card_ops *ops); @@ -183,7 +193,8 @@ int everlive_register_card_ops(const char *model_slug, * Resolve the active operations for a descriptor. This returns the most * recently registered backend vtable, the descriptor's built-in vtable, or * the core default (which reports -ENOTSUP for hardware actions) in that - * order, and never NULL. + * order, and never NULL. The first call freezes the registry; register all + * backends before calling this function. */ const struct everlive_card_ops * everlive_card_ops_for(const struct everlive_device_desc *desc); diff --git a/tools/libeverlive-core/everlive_ctl.c b/tools/libeverlive-core/everlive_ctl.c index 39bf74e..db935be 100644 --- a/tools/libeverlive-core/everlive_ctl.c +++ b/tools/libeverlive-core/everlive_ctl.c @@ -4,6 +4,7 @@ */ #include "everlive_card.h" +#include "everlive_ctl.h" #include #include @@ -11,8 +12,12 @@ #include #include #include +#include #include #include +#include + +static int ctl_connect(char *path, size_t path_size); static void print_capabilities(uint32_t caps) { @@ -23,6 +28,10 @@ static void print_capabilities(uint32_t caps) printf("%s%s", first ? "" : ", ", (name)); \ first = false; \ } + CHECK_CAP(EVERLIVE_CAP_VIDEO_CAPTURE, "Video-Capture"); + CHECK_CAP(EVERLIVE_CAP_AUDIO_CAPTURE, "Audio-Capture"); + CHECK_CAP(EVERLIVE_CAP_PASSTHROUGH, "Passthrough"); + CHECK_CAP(EVERLIVE_CAP_EDID_PASSTHROUGH, "EDID-Passthrough"); CHECK_CAP(EVERLIVE_CAP_4K60, "4K60"); CHECK_CAP(EVERLIVE_CAP_4K120, "4K120"); CHECK_CAP(EVERLIVE_CAP_1440P120, "1440p120"); @@ -40,6 +49,50 @@ static void print_capabilities(uint32_t caps) printf("\n"); } +/* + * Execute argv directly (no shell) and return its exit code, or -1 if it could + * not be spawned or reaped. When @quiet is true, stderr is discarded. + */ +static int run_command(const char *const argv[], bool quiet) +{ + pid_t pid; + int status; + + pid = fork(); + if (pid < 0) + return -1; + + if (pid == 0) { + if (quiet) { + int devnull = open("/dev/null", O_WRONLY); + + if (devnull >= 0) { + dup2(devnull, STDERR_FILENO); + if (devnull > STDERR_FILENO) + close(devnull); + } + } + execvp(argv[0], (char *const *)argv); + _exit(127); + } + + while (waitpid(pid, &status, 0) < 0) { + if (errno != EINTR) + return -1; + } + + return WIFEXITED(status) ? WEXITSTATUS(status) : 1; +} + +static bool systemctl_unit_active(const char *unit) +{ + const char *const argv[] = { + "systemctl", "--user", "is-active", "--quiet", unit, NULL + }; + + return run_command(argv, true) == 0; +} + static int cmd_detect(void) { struct everlive_device cards[8]; @@ -98,28 +151,28 @@ static void print_usage(const char *prog) static int cmd_status(void) { - printf("=== EverLive Background Engine Status ===\n"); + char path[108]; + int fd; - /* Check running processes */ - bool gc570d_proc = (system("pgrep -x opengc570d-userd >/dev/null 2>&1") == 0); - bool gc573_proc = (system("pgrep -x gc573_user_streamer >/dev/null 2>&1") == 0 || - system("pgrep -x everlive_userd >/dev/null 2>&1") == 0); + printf("=== EverLive Background Engine Status ===\n"); - if (gc570d_proc) { - printf("Active Process: opengc570d-userd (running)\n"); - } else if (gc573_proc) { - printf("Active Process: gc573_user_streamer / everlive_userd (running)\n"); + /* The engine is running when its JSON-RPC control socket answers. */ + fd = ctl_connect(path, sizeof(path)); + if (fd >= 0) { + close(fd); + printf("Active Process: EverLive engine (running at %s)\n", + path); } else { printf("Active Process: None (Engine is idle/standby)\n"); } /* Check systemd user service */ printf("Service State: "); - fflush(stdout); - int ret = system("systemctl --user is-active everlive.service 2>/dev/null"); - if (ret != 0) { - system("systemctl --user is-active opengc570d-userd.service 2>/dev/null || echo inactive"); - } + if (systemctl_unit_active("everlive.service") || + systemctl_unit_active("opengc570d-userd.service")) + printf("active\n"); + else + printf("inactive\n"); /* Also report detected card */ printf("\n"); @@ -127,67 +180,257 @@ static int cmd_status(void) return 0; } +/* + * Locate the background service script. Prefer one sitting next to the + * running everlive-ctl binary (true for both an installed tree and a source + * build), then the installed locations, then the legacy CWD-relative path. + */ +static bool resolve_service_script(char *out, size_t out_size) +{ + static const char *const candidates[] = { + "/usr/local/bin/everlive-service", + "/usr/local/bin/everlive-service.sh", + "./tools/libeverlive-core/everlive-service.sh", + }; + char exe_path[PATH_MAX]; + ssize_t len; + + len = readlink("/proc/self/exe", exe_path, sizeof(exe_path) - 1); + if (len > 0) { + char *slash; + + exe_path[len] = '\0'; + slash = strrchr(exe_path, '/'); + if (slash) { + int n; + + *slash = '\0'; + n = snprintf(out, out_size, "%s/everlive-service", + exe_path); + if (n > 0 && (size_t)n < out_size && + access(out, X_OK) == 0) + return true; + n = snprintf(out, out_size, "%s/everlive-service.sh", + exe_path); + if (n > 0 && (size_t)n < out_size && + access(out, X_OK) == 0) + return true; + } + } + + for (size_t i = 0; i < sizeof(candidates) / sizeof(candidates[0]); i++) { + int n = snprintf(out, out_size, "%s", candidates[i]); + + if (n > 0 && (size_t)n < out_size && access(out, X_OK) == 0) + return true; + } + + return false; +} + +/* + * Launch @path detached from the terminal. A pipe with O_CLOEXEC tells the + * parent whether exec() actually succeeded: EOF means success, four bytes + * mean the child failed with that errno. The parent does not wait, so the + * service is not tied to the lifetime of this CLI. + */ +static int spawn_detached(const char *path) +{ + int pipefd[2]; + pid_t pid; + int err = 0; + ssize_t n; + + if (pipe2(pipefd, O_CLOEXEC) < 0) + return -1; + + pid = fork(); + if (pid < 0) { + int saved = errno; + + close(pipefd[0]); + close(pipefd[1]); + errno = saved; + return -1; + } + + if (pid == 0) { + int devnull; + + close(pipefd[0]); + if (setsid() < 0) + goto fail; + + devnull = open("/dev/null", O_RDWR); + if (devnull >= 0) { + dup2(devnull, STDIN_FILENO); + dup2(devnull, STDOUT_FILENO); + dup2(devnull, STDERR_FILENO); + if (devnull > STDERR_FILENO) + close(devnull); + } + + execl(path, path, (char *)NULL); +fail: + err = errno; + (void)!write(pipefd[1], &err, sizeof(err)); + _exit(127); + } + + close(pipefd[1]); + do { + n = read(pipefd[0], &err, sizeof(err)); + } while (n < 0 && errno == EINTR); + + if (n < 0) { + int saved = errno; + + close(pipefd[0]); + errno = saved; + return -1; + } + close(pipefd[0]); + + /* Any data means the child failed before exec(); EOF means success. */ + if (n > 0) { + errno = err; + return -1; + } + + return 0; +} + static int cmd_init(void) { + const char *const everlive_enabled[] = { + "systemctl", "--user", "is-enabled", "--quiet", + "everlive.service", NULL + }; + const char *const gc570d_enabled[] = { + "systemctl", "--user", "is-enabled", "--quiet", + "opengc570d-userd.service", NULL + }; + const char *const everlive_start[] = { + "systemctl", "--user", "start", "everlive.service", NULL + }; + const char *const gc570d_start[] = { + "systemctl", "--user", "start", + "opengc570d-userd.service", NULL + }; + char script_path[PATH_MAX]; + char path[108]; + int fd; + printf("[*] Initializing EverLive capture hardware in background...\n"); - /* Check if already running */ - if (system("pgrep -x opengc570d-userd >/dev/null 2>&1") == 0 || - system("pgrep -x gc573_user_streamer >/dev/null 2>&1") == 0) { + + /* The engine is already up if its control socket answers. */ + fd = ctl_connect(path, sizeof(path)); + if (fd >= 0) { + close(fd); printf("[+] Capture engine is already running and hardware is initialized.\n"); return 0; } - /* Try starting via systemd if service file exists */ - if (system("systemctl --user is-enabled everlive.service >/dev/null 2>&1") == 0 || - system("systemctl --user is-enabled opengc570d-userd.service >/dev/null 2>&1") == 0) { + /* Try starting via systemd if a user unit is installed. */ + if (run_command(everlive_enabled, true) == 0 || + run_command(gc570d_enabled, true) == 0) { printf("[*] Starting via systemd user service...\n"); - system("systemctl --user start everlive.service 2>/dev/null || systemctl --user start opengc570d-userd.service"); - return 0; + if (run_command(everlive_start, true) == 0 || + run_command(gc570d_start, false) == 0) { + printf("[+] EverLive background service started.\n"); + return 0; + } + fprintf(stderr, + "Failed to start the EverLive systemd user service.\n"); + return 1; } /* Direct script invocation */ - char script_path[PATH_MAX]; - snprintf(script_path, sizeof(script_path), "%s/everlive-service.sh", "/usr/local/bin"); - if (access(script_path, X_OK) != 0) { - /* Search relative to executable or current directory */ - snprintf(script_path, sizeof(script_path), "./tools/libeverlive-core/everlive-service.sh"); + + if (!resolve_service_script(script_path, sizeof(script_path))) { + fprintf(stderr, + "Could not locate everlive-service; install it or run from the source tree.\n"); + return 1; } - char cmd[PATH_MAX + 32]; - snprintf(cmd, sizeof(cmd), "nohup %s >/dev/null 2>&1 &", script_path); printf("[*] Spawning %s...\n", script_path); - int res = system(cmd); - if (res == 0) { - printf("[+] EverLive background service launched successfully.\n"); + if (spawn_detached(script_path) < 0) { + fprintf(stderr, "Failed to launch %s: %s\n", + script_path, strerror(errno)); + return 1; } - return res; + + printf("[+] EverLive background service launched successfully.\n"); + return 0; } static int cmd_service(int argc, char **argv) { - if (argc < 3) { - return system("systemctl --user status everlive.service opengc570d-userd.service 2>/dev/null || true"); - } + const char *action; - const char *action = argv[2]; - char cmd[256]; + if (argc < 3) + action = "status"; + else + action = argv[2]; if (strcmp(action, "status") == 0) { - snprintf(cmd, sizeof(cmd), "systemctl --user status everlive.service opengc570d-userd.service"); - } else if (strcmp(action, "start") == 0) { - snprintf(cmd, sizeof(cmd), "systemctl --user start everlive.service 2>/dev/null || systemctl --user start opengc570d-userd.service"); - } else if (strcmp(action, "stop") == 0) { - snprintf(cmd, sizeof(cmd), "systemctl --user stop everlive.service opengc570d-userd.service 2>/dev/null"); - } else if (strcmp(action, "enable") == 0) { - snprintf(cmd, sizeof(cmd), "systemctl --user enable --now everlive.service 2>/dev/null || systemctl --user enable --now opengc570d-userd.service"); - } else if (strcmp(action, "disable") == 0) { - snprintf(cmd, sizeof(cmd), "systemctl --user disable --now everlive.service opengc570d-userd.service 2>/dev/null"); - } else { - fprintf(stderr, "Unknown service action: %s\n", action); - return 1; + const char *const status_argv[] = { + "systemctl", "--user", "status", "everlive.service", + "opengc570d-userd.service", NULL + }; + + return run_command(status_argv, true); + } + + if (strcmp(action, "start") == 0) { + const char *const everlive[] = { + "systemctl", "--user", "start", "everlive.service", NULL + }; + const char *const gc570d[] = { + "systemctl", "--user", "start", + "opengc570d-userd.service", NULL + }; + + if (run_command(everlive, true) == 0) + return 0; + return run_command(gc570d, false); + } + + if (strcmp(action, "stop") == 0) { + const char *const stop_argv[] = { + "systemctl", "--user", "stop", "everlive.service", + "opengc570d-userd.service", NULL + }; + + return run_command(stop_argv, true); + } + + if (strcmp(action, "enable") == 0) { + const char *const everlive[] = { + "systemctl", "--user", "enable", "--now", + "everlive.service", NULL + }; + const char *const gc570d[] = { + "systemctl", "--user", "enable", "--now", + "opengc570d-userd.service", NULL + }; + + if (run_command(everlive, true) == 0) + return 0; + return run_command(gc570d, false); } - return system(cmd); + if (strcmp(action, "disable") == 0) { + const char *const disable_argv[] = { + "systemctl", "--user", "disable", "--now", + "everlive.service", "opengc570d-userd.service", NULL + }; + + return run_command(disable_argv, true); + } + + fprintf(stderr, "Unknown service action: %s\n", action); + return 1; } static int cmd_list_models(void) @@ -233,85 +476,73 @@ static int ctl_resolve_socket(char *buf, size_t size) return (n > 0 && (size_t)n < size) ? 0 : -1; } -static void json_escape(const char *in, char *out, size_t out_size) +/* + * Resolve and connect to the engine control socket. Returns the fd on + * success, or -1 with errno set. + */ +static int ctl_connect(char *path, size_t path_size) { - size_t o = 0; + struct sockaddr_un addr; + int fd; + int saved; - for (const unsigned char *p = (const unsigned char *)in; *p; p++) { - if (o + 8 >= out_size) - break; - switch (*p) { - case '"': - out[o++] = '\\'; - out[o++] = '"'; - break; - case '\\': - out[o++] = '\\'; - out[o++] = '\\'; - break; - case '\n': - out[o++] = '\\'; - out[o++] = 'n'; - break; - case '\r': - out[o++] = '\\'; - out[o++] = 'r'; - break; - case '\t': - out[o++] = '\\'; - out[o++] = 't'; - break; - default: - if (*p < 0x20) - o += (size_t)snprintf(out + o, out_size - o, - "\\u%04x", *p); - else - out[o++] = (char)*p; - break; - } + if (ctl_resolve_socket(path, path_size) < 0) { + errno = ENAMETOOLONG; + return -1; } - out[o] = '\0'; + + fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); + if (fd < 0) + return -1; + + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + snprintf(addr.sun_path, sizeof(addr.sun_path), "%s", path); + + if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + saved = errno; + close(fd); + errno = saved; + return -1; + } + + return fd; } static int ctl_call(const char *method, const char *params) { - char path[108]; - struct sockaddr_un addr; + char path[108] = ""; char request[2048]; char response[8192]; size_t off = 0; + size_t request_len; int fd; - if (ctl_resolve_socket(path, sizeof(path)) < 0) { - fprintf(stderr, "EverLive control socket path is too long\n"); - return 1; - } - - fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); + fd = ctl_connect(path, sizeof(path)); if (fd < 0) { - fprintf(stderr, "socket: %s\n", strerror(errno)); + if (errno == ENAMETOOLONG) { + fprintf(stderr, + "EverLive control socket path is too long\n"); + } else { + fprintf(stderr, + "Cannot connect to the EverLive engine at %s: %s\n" + "Is the capture engine running?\n", + path, strerror(errno)); + } return 1; } - memset(&addr, 0, sizeof(addr)); - addr.sun_family = AF_UNIX; - snprintf(addr.sun_path, sizeof(addr.sun_path), "%s", path); - - if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { - fprintf(stderr, - "Cannot connect to the EverLive engine at %s: %s\n" - "Is the capture engine running?\n", - path, strerror(errno)); + if (everlive_ctl_build_request(method, params, request, + sizeof(request)) < 0) { + fprintf(stderr, "Request too long\n"); close(fd); return 1; } - snprintf(request, sizeof(request), - "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"%s\",\"params\":%s}\n", - method, params && *params ? params : "{}"); + request_len = strlen(request); - while (off < strlen(request)) { - ssize_t n = write(fd, request + off, strlen(request) - off); + while (off < request_len) { + ssize_t n = write(fd, request + off, request_len - off); if (n < 0) { if (errno == EINTR) @@ -320,6 +551,11 @@ static int ctl_call(const char *method, const char *params) close(fd); return 1; } + if (n == 0) { + fprintf(stderr, "write: no progress\n"); + close(fd); + return 1; + } off += (size_t)n; } @@ -353,7 +589,7 @@ static int ctl_call(const char *method, const char *params) response[--off] = '\0'; printf("%s\n", response); - return strstr(response, "\"error\"") ? 1 : 0; + return everlive_ctl_response_has_error(response) ? 1 : 0; } static int parse_switch(const char *value) @@ -399,16 +635,19 @@ static int cmd_runtime(int argc, char **argv) return ctl_call("status.get", "{}"); if (strcmp(topic, "led") == 0) { + unsigned long rgb; + if (argc < 4) { fprintf(stderr, "runtime led needs RRGGBB or off\n"); return 1; } if (strcasecmp(argv[3], "off") == 0) return ctl_call("led.mode.set", "{\"mode\":\"off\"}"); - unsigned long rgb = strtoul(argv[3], NULL, 16); - snprintf(params, sizeof(params), - "{\"r\":%lu,\"g\":%lu,\"b\":%lu}", - (rgb >> 16) & 0xff, (rgb >> 8) & 0xff, rgb & 0xff); + if (everlive_ctl_parse_hex_rgb(argv[3], &rgb) < 0 || + everlive_ctl_format_led(rgb, params, sizeof(params)) < 0) { + fprintf(stderr, "runtime led needs RRGGBB or off\n"); + return 1; + } return ctl_call("led.set", params); } @@ -417,7 +656,7 @@ static int cmd_runtime(int argc, char **argv) fprintf(stderr, "runtime led-mode needs static|wave|rainbow|off\n"); return 1; } - json_escape(argv[3], escaped, sizeof(escaped)); + everlive_ctl_json_escape(argv[3], escaped, sizeof(escaped)); snprintf(params, sizeof(params), "{\"mode\":\"%s\"}", escaped); return ctl_call("led.mode.set", params); } @@ -427,7 +666,7 @@ static int cmd_runtime(int argc, char **argv) fprintf(stderr, "runtime edid needs a profile name or path\n"); return 1; } - json_escape(argv[3], escaped, sizeof(escaped)); + everlive_ctl_json_escape(argv[3], escaped, sizeof(escaped)); snprintf(params, sizeof(params), "{\"profile\":\"%s\"}", escaped); return ctl_call("edid.set", params); } @@ -435,17 +674,23 @@ static int cmd_runtime(int argc, char **argv) if (strcmp(topic, "hdr") == 0) { const char *mode = argc >= 4 ? argv[3] : "auto"; - json_escape(mode, escaped, sizeof(escaped)); + everlive_ctl_json_escape(mode, escaped, sizeof(escaped)); snprintf(params, sizeof(params), "{\"mode\":\"%s\"}", escaped); return ctl_call("hdr.set", params); } if (strcmp(topic, "hdr-ev") == 0) { + double ev; + if (argc < 4) { fprintf(stderr, "runtime hdr-ev needs an EV value\n"); return 1; } - snprintf(params, sizeof(params), "{\"exposure\":%s}", argv[3]); + if (everlive_ctl_parse_double(argv[3], &ev) < 0 || + everlive_ctl_format_exposure(ev, params, sizeof(params)) < 0) { + fprintf(stderr, "runtime hdr-ev needs a numeric EV value\n"); + return 1; + } return ctl_call("hdr.exposure", params); } @@ -469,18 +714,25 @@ static int cmd_runtime(int argc, char **argv) fprintf(stderr, "runtime toast needs text\n"); return 1; } - json_escape(argv[3], escaped, sizeof(escaped)); + everlive_ctl_json_escape(argv[3], escaped, sizeof(escaped)); snprintf(params, sizeof(params), "{\"message\":\"%s\"}", escaped); return ctl_call("toast.show", params); } if (strcmp(topic, "volume") == 0) { + double percent; + if (argc < 4) { fprintf(stderr, "runtime volume needs 0..150\n"); return 1; } - snprintf(params, sizeof(params), "{\"gain\":%g}", - strtod(argv[3], NULL) / 100.0); + if (everlive_ctl_parse_double(argv[3], &percent) < 0 || + everlive_ctl_format_volume(percent, params, + sizeof(params)) < 0) { + fprintf(stderr, + "runtime volume needs a number in 0..150\n"); + return 1; + } return ctl_call("audio.volume.set", params); } diff --git a/tools/libeverlive-core/everlive_ctl.h b/tools/libeverlive-core/everlive_ctl.h new file mode 100644 index 0000000..1ee0b47 --- /dev/null +++ b/tools/libeverlive-core/everlive_ctl.h @@ -0,0 +1,48 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * everlive_ctl.h — JSON-RPC request helpers for everlive-ctl + * + * These helpers are stateless and socket-free so they can be unit tested + * without a running capture engine. + */ + +#ifndef EVERLIVE_CTL_H +#define EVERLIVE_CTL_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Serialize a JSON-RPC 2.0 request. Returns 0 on success, -1 if the result + * does not fit in @out. */ +int everlive_ctl_build_request(const char *method, const char *params, + char *out, size_t out_size); + +/* Escape @in for embedding inside a JSON string literal. */ +void everlive_ctl_json_escape(const char *in, char *out, size_t out_size); + +/* True when the response has a top-level-style "error" key (quoted key + * followed by a colon). Ignores the word "error" appearing in values. */ +bool everlive_ctl_response_has_error(const char *json); + +/* Parse a finite base-10 double, rejecting trailing garbage and NaN/Inf. + * Returns 0 on success, -1 on invalid input. */ +int everlive_ctl_parse_double(const char *value, double *out); + +/* Parse a 24-bit RRGGBB colour given in hex. Returns 0 on success. */ +int everlive_ctl_parse_hex_rgb(const char *value, unsigned long *out); + +/* Build the "params" object for the numeric runtime commands. Each returns 0 + * on success or -1 on invalid input / insufficient buffer space. */ +int everlive_ctl_format_exposure(double ev, char *out, size_t out_size); +int everlive_ctl_format_volume(double percent, char *out, size_t out_size); +int everlive_ctl_format_led(unsigned long rgb, char *out, size_t out_size); + +#ifdef __cplusplus +} +#endif + +#endif /* EVERLIVE_CTL_H */ diff --git a/tools/libeverlive-core/everlive_ctl_proto.c b/tools/libeverlive-core/everlive_ctl_proto.c new file mode 100644 index 0000000..e895094 --- /dev/null +++ b/tools/libeverlive-core/everlive_ctl_proto.c @@ -0,0 +1,165 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * everlive_ctl_proto.c — Stateless JSON-RPC request helpers for everlive-ctl + * + * Kept separate from everlive_ctl.c so the serialization and validation logic + * can be exercised by the unit tests without a control socket. + */ + +#include "everlive_ctl.h" + +#include +#include +#include +#include +#include + +int everlive_ctl_build_request(const char *method, const char *params, + char *out, size_t out_size) +{ + int n; + + if (!method || !out || out_size == 0) + return -1; + + n = snprintf(out, out_size, + "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"%s\",\"params\":%s}\n", + method, params && *params ? params : "{}"); + return (n > 0 && (size_t)n < out_size) ? 0 : -1; +} + +void everlive_ctl_json_escape(const char *in, char *out, size_t out_size) +{ + size_t o = 0; + + if (!out || out_size == 0) + return; + + if (!in) { + out[0] = '\0'; + return; + } + + for (const unsigned char *p = (const unsigned char *)in; *p; p++) { + if (o + 8 >= out_size) + break; + switch (*p) { + case '"': + out[o++] = '\\'; + out[o++] = '"'; + break; + case '\\': + out[o++] = '\\'; + out[o++] = '\\'; + break; + case '\n': + out[o++] = '\\'; + out[o++] = 'n'; + break; + case '\r': + out[o++] = '\\'; + out[o++] = 'r'; + break; + case '\t': + out[o++] = '\\'; + out[o++] = 't'; + break; + default: + if (*p < 0x20) + o += (size_t)snprintf(out + o, out_size - o, + "\\u%04x", *p); + else + out[o++] = (char)*p; + break; + } + } + out[o] = '\0'; +} + +bool everlive_ctl_response_has_error(const char *json) +{ + const char *p = json; + + if (!json) + return false; + + while ((p = strstr(p, "\"error\"")) != NULL) { + const char *q = p + 7; /* strlen("\"error\"") */ + + while (*q == ' ' || *q == '\t' || *q == '\n' || *q == '\r') + q++; + if (*q == ':') + return true; + p = q; + } + + return false; +} + +int everlive_ctl_parse_double(const char *value, double *out) +{ + char *end = NULL; + double v; + + if (!value || !*value || !out) + return -1; + + errno = 0; + v = strtod(value, &end); + if (end == value || *end != '\0' || errno == ERANGE) + return -1; + + /* NaN and +/-Inf are not valid JSON numbers. */ + if (!(v >= -DBL_MAX && v <= DBL_MAX)) + return -1; + + *out = v; + return 0; +} + +int everlive_ctl_parse_hex_rgb(const char *value, unsigned long *out) +{ + char *end = NULL; + unsigned long v; + + if (!value || !*value || !out) + return -1; + + errno = 0; + v = strtoul(value, &end, 16); + if (end == value || *end != '\0' || errno == ERANGE || v > 0xffffffUL) + return -1; + + *out = v; + return 0; +} + +int everlive_ctl_format_exposure(double ev, char *out, size_t out_size) +{ + int n = snprintf(out, out_size, "{\"exposure\":%g}", ev); + + return (n > 0 && (size_t)n < out_size) ? 0 : -1; +} + +int everlive_ctl_format_volume(double percent, char *out, size_t out_size) +{ + int n; + + if (!(percent >= 0.0 && percent <= 150.0)) + return -1; + + n = snprintf(out, out_size, "{\"gain\":%g}", percent / 100.0); + return (n > 0 && (size_t)n < out_size) ? 0 : -1; +} + +int everlive_ctl_format_led(unsigned long rgb, char *out, size_t out_size) +{ + int n; + + if (rgb > 0xffffffUL) + return -1; + + n = snprintf(out, out_size, "{\"r\":%lu,\"g\":%lu,\"b\":%lu}", + (rgb >> 16) & 0xff, (rgb >> 8) & 0xff, rgb & 0xff); + return (n > 0 && (size_t)n < out_size) ? 0 : -1; +} diff --git a/tools/libeverlive-core/everlive_frame.c b/tools/libeverlive-core/everlive_frame.c index a3e7f81..0cb8350 100644 --- a/tools/libeverlive-core/everlive_frame.c +++ b/tools/libeverlive-core/everlive_frame.c @@ -10,25 +10,37 @@ static inline uint8_t clamp_u8(int val) return (uint8_t)val; } +/* Arithmetic right shift by 8 without relying on implementation-defined + * behaviour for negative operands. */ +static inline int shift8(int val) +{ + return val >= 0 ? (val >> 8) : -(((-val) + 255) >> 8); +} + void everlive_yuy2_to_rgb(const uint8_t *src, uint8_t *dst, int width, int height) { if (!src || !dst || width <= 0 || height <= 0) return; - int total_pairs = (width * height) / 2; + /* YUY2 packs two pixels into each 4-byte macro-pixel, so an odd width + * cannot be represented; reject it rather than desynchronising rows. */ + if ((width & 1) != 0) + return; + + size_t total_pairs = ((size_t)width * (size_t)height) / 2; const uint8_t *s = src; uint8_t *d = dst; - for (int i = 0; i < total_pairs; i++) { + for (size_t i = 0; i < total_pairs; i++) { int y0 = s[0]; int u = s[1] - 128; int y1 = s[2]; int v = s[3] - 128; s += 4; - int r_off = (359 * v) >> 8; - int g_off = (88 * u + 183 * v) >> 8; - int b_off = (454 * u) >> 8; + int r_off = shift8(359 * v); + int g_off = shift8(88 * u + 183 * v); + int b_off = shift8(454 * u); /* Pixel 0 */ d[0] = clamp_u8(y0 + r_off); @@ -77,9 +89,9 @@ void everlive_yuy2_to_rgb_scaled(const uint8_t *src, uint8_t *dst, int u = row[pair_base + 1] - 128; int v = row[pair_base + 3] - 128; - int r_off = (359 * v) >> 8; - int g_off = (88 * u + 183 * v) >> 8; - int b_off = (454 * u) >> 8; + int r_off = shift8(359 * v); + int g_off = shift8(88 * u + 183 * v); + int b_off = shift8(454 * u); d[0] = clamp_u8(y + r_off); d[1] = clamp_u8(y - g_off); diff --git a/tools/libeverlive-core/everlive_frame.h b/tools/libeverlive-core/everlive_frame.h index e387183..f41293c 100644 --- a/tools/libeverlive-core/everlive_frame.h +++ b/tools/libeverlive-core/everlive_frame.h @@ -13,7 +13,7 @@ extern "C" { /** * Convert packed YUY2 (YUYV) 4:2:2 buffer to packed 24-bit RGB (RGB888). * - * Fast SIMD/fixed-point conversion using BT.601 color matrix. + * Fast fixed-point conversion using the BT.601 color matrix. * * @param src Pointer to source YUY2 buffer (width * height * 2 bytes). * @param dst Pointer to destination RGB888 buffer (width * height * 3 bytes). @@ -25,7 +25,7 @@ void everlive_yuy2_to_rgb(const uint8_t *src, uint8_t *dst, int width, int heigh /** * Convert and downscale packed YUY2 (YUYV) buffer to packed 24-bit RGB (RGB888). * - * Uses point sampling / box averaging for low-overhead preview scaling. + * Uses nearest-neighbour point sampling for low-overhead preview scaling. * * @param src Pointer to source YUY2 buffer. * @param dst Pointer to destination RGB888 buffer. From e9f4aecdc6b275653b115318839e354c0fcf626a Mon Sep 17 00:00:00 2001 From: PolygonPrincess Date: Wed, 11 Nov 2026 04:10:49 -0500 Subject: [PATCH 3/8] refactor(core): move JSON-RPC, socket resolution, and jsmn into libeverlive-core Part 1/5 of the modularization plan (see .opencode/plan/everlive-modularization/01-libeverlive-core.md). Consolidate the shared control-plane pieces in the core library so the GC573 streamer, the GC570D daemon, and the unit tests no longer carry private copies or cross-directory .c includes. - Move third_party/jsmn.{c,h} from opengc573-userd into libeverlive-core. - Add everlive_json.{c,h}: typed getters, raw/id helpers, top-level key detection, canonical JSON escaping, and canonical-integer id formatting. All functions are stateless and re-entrant. - Add everlive_ipc.{c,h}: deterministic socket resolution (override -> EVERLIVE_SOCKET -> EVERLIVE_CONTROL_SOCKET -> XDG_RUNTIME_DIR -> /run/user/ -> /tmp/everlive-) plus a prepare variant that creates the runtime directory for the server side. - everlive_ctl_proto: reuse the core escaper and detect errors with the jsmn top-level key check instead of a substring scan. Responses get a 512-token budget; a real status.get reply needs ~135 and the old 128 limit returned JSMN_ERROR_NOMEM. - everlive_ctl: resolve the live everlive.sock instead of the stale everlive-experimental.sock, which never matched a running engine. - everlive_card: add everlive_find_card_by_slug() and reuse it for ops registration. - gc573_user_ipc: drop the duplicated parser/escaping/ID code and call the everlive_json_* API. - Core Makefile: include the new objects and jsmn in the static/shared archives, link everlive-ctl against the archive, add dependency generation, install/uninstall the new headers, and template everlive.service with PREFIX. - tests: drop the duplicate -D_GNU_SOURCE and duplicate asan target, link the core archive instead of recompiling its sources, and update the escaping expectations to the canonical \n/\t short forms. Behaviour changes: - JSON escaping now uses \n, \r, and \t short forms for those control characters; other control bytes remain \uXXXX. - everlive_json_format_id rejects non-canonical integers (e.g. "0012") instead of emitting invalid JSON. Verified: core builds clean with gcc and clang; all 6 test suites pass (134 assertions) including ASan/UBSan; GC573 binaries build; everlive-ctl resolves the unified socket path and honours the env overrides. --- .gitignore | 6 + tests/Makefile | 15 +- tests/test_ipc_json.c | 167 ++++--- tools/libeverlive-core/Makefile | 50 +- tools/libeverlive-core/everlive_card.c | 23 +- tools/libeverlive-core/everlive_card.h | 4 + tools/libeverlive-core/everlive_ctl.c | 26 +- tools/libeverlive-core/everlive_ctl_proto.c | 63 +-- tools/libeverlive-core/everlive_ipc.c | 112 +++++ tools/libeverlive-core/everlive_ipc.h | 57 +++ tools/libeverlive-core/everlive_json.c | 442 ++++++++++++++++++ tools/libeverlive-core/everlive_json.h | 75 +++ .../third_party/jsmn.c | 0 .../third_party/jsmn.h | 0 tools/opengc573-userd/Makefile | 17 +- tools/opengc573-userd/gc573_user_ipc.c | 362 ++------------ 16 files changed, 898 insertions(+), 521 deletions(-) create mode 100644 tools/libeverlive-core/everlive_ipc.c create mode 100644 tools/libeverlive-core/everlive_ipc.h create mode 100644 tools/libeverlive-core/everlive_json.c create mode 100644 tools/libeverlive-core/everlive_json.h rename tools/{opengc573-userd => libeverlive-core}/third_party/jsmn.c (100%) rename tools/{opengc573-userd => libeverlive-core}/third_party/jsmn.h (100%) diff --git a/.gitignore b/.gitignore index 54469a5..335183e 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,12 @@ tests/test_edid tests/test_config tools/opengc573-userd/gc573_framegen_selftest +# libeverlive-core build outputs +tools/libeverlive-core/*.a +tools/libeverlive-core/*.so* +tools/libeverlive-core/everlive-ctl +tools/libeverlive-core/**/*.d + # Raw uncompressed video dumps 20261010_131220.mp4 *.mkv diff --git a/tests/Makefile b/tests/Makefile index 57cd857..e52e0da 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -1,12 +1,13 @@ # SPDX-License-Identifier: GPL-2.0-only CC ?= gcc -CFLAGS ?= -O2 -g -Wall -Wextra -Wno-unused-parameter -Wno-sign-compare -std=gnu11 -D_GNU_SOURCE -D_GNU_SOURCE +CFLAGS ?= -O2 -g -Wall -Wextra -Wno-unused-parameter -Wno-sign-compare -std=gnu11 -D_GNU_SOURCE INCLUDES := -I../tools/opengc570d-userd/src -I../tools/opengc570d-userd -I../tools/libeverlive-core -I../tools/opengc573-userd DEFS := -DGC570D_USERSPACE GC570D_DIR := ../tools/opengc570d-userd/src CORE_DIR := ../tools/libeverlive-core GC573_DIR := ../tools/opengc573-userd +CORE_LIB := $(CORE_DIR)/libeverlive_core.a TARGETS := test_edid test_config test_card_registry test_frame_convert test_ctl_proto test_ipc_json @@ -24,17 +25,17 @@ test_card_registry: test_card_registry.c $(CORE_DIR)/everlive_card.c test_frame_convert: test_frame_convert.c $(CORE_DIR)/everlive_frame.c $(CC) $(CFLAGS) $(INCLUDES) -o $@ $^ -test_ctl_proto: test_ctl_proto.c $(CORE_DIR)/everlive_ctl_proto.c - $(CC) $(CFLAGS) $(INCLUDES) -o $@ $^ +test_ctl_proto: test_ctl_proto.c $(CORE_LIB) + $(CC) $(CFLAGS) $(INCLUDES) -o $@ test_ctl_proto.c $(CORE_LIB) asan: CFLAGS += -fsanitize=address,undefined -fno-omit-frame-pointer asan: all -test_ipc_json: test_ipc_json.c $(GC573_DIR)/third_party/jsmn.c - $(CC) $(CFLAGS) $(INCLUDES) -o $@ $^ -pthread +test_ipc_json: test_ipc_json.c $(CORE_LIB) + $(CC) $(CFLAGS) $(INCLUDES) -o $@ $< $(CORE_LIB) -pthread -asan: CFLAGS += -fsanitize=address,undefined -fno-omit-frame-pointer -asan: all +$(CORE_LIB): + $(MAKE) -C $(CORE_DIR) test: all @echo "Running GC570D & GC573 unit tests..." diff --git a/tests/test_ipc_json.c b/tests/test_ipc_json.c index fdcf9e9..9459d84 100644 --- a/tests/test_ipc_json.c +++ b/tests/test_ipc_json.c @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: GPL-2.0-only */ /* - * test_ipc_json.c — Unit tests for GC573 Hand-rolled JSON Scanner and RPC Formatter + * test_ipc_json.c — Unit tests for the libeverlive-core JSON scanners and RPC formatters */ #include @@ -9,8 +9,7 @@ #include #include -#include "../tools/opengc573-userd/gc573-user-compat.h" -#include "../tools/opengc573-userd/gc573_user_ipc.c" +#include "everlive_json.h" static int g_failures = 0; @@ -30,16 +29,16 @@ static void test_json_get_string(void) const char *json_dup_val = "{\"title\":\"mode\",\"mode\":\"wave\"}"; const char *json_param_method = "{\"jsonrpc\":\"2.0\",\"params\":{\"text\":\"method\"},\"method\":\"led.set\"}"; - printf("Testing json_get_string...\n"); - TEST_ASSERT(json_get_string(json, "method", out, sizeof(out)) && strcmp(out, "led.set") == 0, + printf("Testing everlive_json_get_string...\n"); + TEST_ASSERT(everlive_json_get_string(json, "method", out, sizeof(out)) && strcmp(out, "led.set") == 0, "Extracts top-level string 'method'"); - TEST_ASSERT(json_get_string(json, "mode", out, sizeof(out)) && strcmp(out, "wave") == 0, + TEST_ASSERT(everlive_json_get_string(json, "mode", out, sizeof(out)) && strcmp(out, "wave") == 0, "Extracts nested string 'mode'"); - TEST_ASSERT(json_get_string(json_dup_val, "mode", out, sizeof(out)) && strcmp(out, "wave") == 0, + TEST_ASSERT(everlive_json_get_string(json_dup_val, "mode", out, sizeof(out)) && strcmp(out, "wave") == 0, "Distinguishes object key from prior string value matching key name"); - TEST_ASSERT(json_get_string(json_param_method, "method", out, sizeof(out)) && strcmp(out, "led.set") == 0, + TEST_ASSERT(everlive_json_get_string(json_param_method, "method", out, sizeof(out)) && strcmp(out, "led.set") == 0, "Extracts top-level method even if params contains identical string value"); - TEST_ASSERT(!json_get_string(json, "missing", out, sizeof(out)), + TEST_ASSERT(!everlive_json_get_string(json, "missing", out, sizeof(out)), "Fails cleanly on missing key"); } @@ -48,14 +47,14 @@ static void test_json_get_long(void) long val = 0; const char *json = "{\"id\":42,\"code\":-32700,\"zero\":0}"; - printf("Testing json_get_long...\n"); - TEST_ASSERT(json_get_long(json, "id", &val) && val == 42, + printf("Testing everlive_json_get_long...\n"); + TEST_ASSERT(everlive_json_get_long(json, "id", &val) && val == 42, "Extracts positive integer 'id': 42"); - TEST_ASSERT(json_get_long(json, "code", &val) && val == -32700, + TEST_ASSERT(everlive_json_get_long(json, "code", &val) && val == -32700, "Extracts negative integer 'code': -32700"); - TEST_ASSERT(json_get_long(json, "zero", &val) && val == 0, + TEST_ASSERT(everlive_json_get_long(json, "zero", &val) && val == 0, "Extracts zero integer: 0"); - TEST_ASSERT(!json_get_long(json, "nonexistent", &val), + TEST_ASSERT(!everlive_json_get_long(json, "nonexistent", &val), "Fails cleanly on non-existent key"); } @@ -64,12 +63,12 @@ static void test_json_get_bool(void) bool val = false; const char *json = "{\"active\":true,\"disabled\":false,\"num\":1}"; - printf("Testing json_get_bool...\n"); - TEST_ASSERT(json_get_bool(json, "active", &val) && val == true, + printf("Testing everlive_json_get_bool...\n"); + TEST_ASSERT(everlive_json_get_bool(json, "active", &val) && val == true, "Extracts boolean true"); - TEST_ASSERT(json_get_bool(json, "disabled", &val) && val == false, + TEST_ASSERT(everlive_json_get_bool(json, "disabled", &val) && val == false, "Extracts boolean false"); - TEST_ASSERT(!json_get_bool(json, "num", &val), + TEST_ASSERT(!everlive_json_get_bool(json, "num", &val), "Rejects non-boolean value"); } @@ -78,12 +77,12 @@ static void test_json_get_double(void) double val = 0.0; const char *json = "{\"exposure\":-1.5,\"gain\":0.85}"; - printf("Testing json_get_double...\n"); - TEST_ASSERT(json_get_double(json, "exposure", &val) && val == -1.5, + printf("Testing everlive_json_get_double...\n"); + TEST_ASSERT(everlive_json_get_double(json, "exposure", &val) && val == -1.5, "Extracts negative float 'exposure': -1.5"); - TEST_ASSERT(json_get_double(json, "gain", &val) && val > 0.84 && val < 0.86, + TEST_ASSERT(everlive_json_get_double(json, "gain", &val) && val > 0.84 && val < 0.86, "Extracts positive float 'gain': 0.85"); - TEST_ASSERT(!json_get_double(json, "nonexistent", &val), + TEST_ASSERT(!everlive_json_get_double(json, "nonexistent", &val), "Fails cleanly on missing float key"); } @@ -91,12 +90,12 @@ static void test_json_escape_into(void) { char dst[128]; - printf("Testing json_escape_into...\n"); - json_escape_into(dst, sizeof(dst), "Simple string"); + printf("Testing everlive_json_escape_into...\n"); + everlive_json_escape_into(dst, sizeof(dst), "Simple string"); TEST_ASSERT(strcmp(dst, "Simple string") == 0, "Leaves plain ASCII unchanged"); - json_escape_into(dst, sizeof(dst), "Hello \"World\"\\Test\nNew\tTab"); - TEST_ASSERT(strcmp(dst, "Hello \\\"World\\\"\\\\Test\\u000aNew\\u0009Tab") == 0, + everlive_json_escape_into(dst, sizeof(dst), "Hello \"World\"\\Test\nNew\tTab"); + TEST_ASSERT(strcmp(dst, "Hello \\\"World\\\"\\\\Test\\nNew\\tTab") == 0, "Escapes quotes, backslashes, and control characters"); } @@ -104,27 +103,27 @@ static void test_json_format_id(void) { char dst[64]; - printf("Testing json_format_id...\n"); - json_format_id(dst, sizeof(dst), NULL); + printf("Testing everlive_json_format_id...\n"); + everlive_json_format_id(dst, sizeof(dst), NULL); TEST_ASSERT(strcmp(dst, "null") == 0, "Formats NULL id as null"); - json_format_id(dst, sizeof(dst), "null"); + everlive_json_format_id(dst, sizeof(dst), "null"); TEST_ASSERT(strcmp(dst, "null") == 0, "Formats 'null' string as null"); - json_format_id(dst, sizeof(dst), "1234"); + everlive_json_format_id(dst, sizeof(dst), "1234"); TEST_ASSERT(strcmp(dst, "1234") == 0, "Preserves numeric integer id 1234"); - json_format_id(dst, sizeof(dst), "-1"); + everlive_json_format_id(dst, sizeof(dst), "-1"); TEST_ASSERT(strcmp(dst, "-1") == 0, "Preserves negative integer id -1"); - json_format_id(dst, sizeof(dst), "\"client-req-99\""); + everlive_json_format_id(dst, sizeof(dst), "\"client-req-99\""); TEST_ASSERT(strcmp(dst, "\"client-req-99\"") == 0, "Preserves and sanitizes quoted string id"); - json_format_id(dst, sizeof(dst), "raw-id"); + everlive_json_format_id(dst, sizeof(dst), "raw-id"); TEST_ASSERT(strcmp(dst, "\"raw-id\"") == 0, "Encapsulates unquoted string id"); - json_format_id(dst, sizeof(dst), "\"injection\n\""); - TEST_ASSERT(strstr(dst, "\\u000a") != NULL, "Re-escapes dangerous newline injection in string id"); + everlive_json_format_id(dst, sizeof(dst), "\"injection\n\""); + TEST_ASSERT(strstr(dst, "\\n") != NULL, "Re-escapes dangerous newline injection in string id"); } static void test_json_find_value(void) @@ -134,21 +133,21 @@ static void test_json_find_value(void) const char *json_str = "{\"jsonrpc\":\"2.0\",\"id\":\"req-abc\",\"method\":\"status.get\"}"; const char *json_null = "{\"jsonrpc\":\"2.0\",\"id\":null,\"method\":\"led.set\"}"; - printf("Testing json_find_value...\n"); - TEST_ASSERT(json_find_value(json_num, "method", out, sizeof(out)) && strcmp(out, "ping") == 0, + printf("Testing everlive_json_find_value...\n"); + TEST_ASSERT(everlive_json_find_value(json_num, "method", out, sizeof(out)) && strcmp(out, "ping") == 0, "Extracts method string from numeric ID request"); - TEST_ASSERT(json_find_value(json_num, "id", out, sizeof(out)) && strcmp(out, "42") == 0, + TEST_ASSERT(everlive_json_find_value(json_num, "id", out, sizeof(out)) && strcmp(out, "42") == 0, "Extracts numeric ID: 42"); - TEST_ASSERT(json_find_value(json_str, "method", out, sizeof(out)) && strcmp(out, "status.get") == 0, + TEST_ASSERT(everlive_json_find_value(json_str, "method", out, sizeof(out)) && strcmp(out, "status.get") == 0, "Extracts method string from string ID request"); - TEST_ASSERT(json_find_value(json_str, "id", out, sizeof(out)) && strcmp(out, "req-abc") == 0, + TEST_ASSERT(everlive_json_find_value(json_str, "id", out, sizeof(out)) && strcmp(out, "req-abc") == 0, "Extracts string ID: req-abc"); - TEST_ASSERT(json_find_value(json_null, "id", out, sizeof(out)) && strcmp(out, "null") == 0, + TEST_ASSERT(everlive_json_find_value(json_null, "id", out, sizeof(out)) && strcmp(out, "null") == 0, "Extracts null ID: null"); - TEST_ASSERT(!json_find_value(json_num, "missing", out, sizeof(out)), - "Fails cleanly on missing key in json_find_value"); + TEST_ASSERT(!everlive_json_find_value(json_num, "missing", out, sizeof(out)), + "Fails cleanly on missing key in everlive_json_find_value"); } static void test_json_negative_edge_cases(void) @@ -162,61 +161,61 @@ static void test_json_negative_edge_cases(void) printf("Testing JSON negative edge cases...\n"); /* NULL pointer handling */ - TEST_ASSERT(!json_get_string(NULL, "str", out, sizeof(out)), "json_get_string rejects NULL json"); - TEST_ASSERT(!json_get_string(valid_json, NULL, out, sizeof(out)), "json_get_string rejects NULL key"); - TEST_ASSERT(!json_get_string(valid_json, "str", NULL, sizeof(out)), "json_get_string rejects NULL output"); - TEST_ASSERT(!json_get_string(valid_json, "str", out, 0), "json_get_string rejects 0 outsz"); + TEST_ASSERT(!everlive_json_get_string(NULL, "str", out, sizeof(out)), "everlive_json_get_string rejects NULL json"); + TEST_ASSERT(!everlive_json_get_string(valid_json, NULL, out, sizeof(out)), "everlive_json_get_string rejects NULL key"); + TEST_ASSERT(!everlive_json_get_string(valid_json, "str", NULL, sizeof(out)), "everlive_json_get_string rejects NULL output"); + TEST_ASSERT(!everlive_json_get_string(valid_json, "str", out, 0), "everlive_json_get_string rejects 0 outsz"); - TEST_ASSERT(!json_get_long(NULL, "num", &lval), "json_get_long rejects NULL json"); - TEST_ASSERT(!json_get_long(valid_json, NULL, &lval), "json_get_long rejects NULL key"); - TEST_ASSERT(!json_get_long(valid_json, "num", NULL), "json_get_long rejects NULL output"); + TEST_ASSERT(!everlive_json_get_long(NULL, "num", &lval), "everlive_json_get_long rejects NULL json"); + TEST_ASSERT(!everlive_json_get_long(valid_json, NULL, &lval), "everlive_json_get_long rejects NULL key"); + TEST_ASSERT(!everlive_json_get_long(valid_json, "num", NULL), "everlive_json_get_long rejects NULL output"); - TEST_ASSERT(!json_get_double(NULL, "flt", &dval), "json_get_double rejects NULL json"); - TEST_ASSERT(!json_get_double(valid_json, NULL, &dval), "json_get_double rejects NULL key"); - TEST_ASSERT(!json_get_double(valid_json, "flt", NULL), "json_get_double rejects NULL output"); + TEST_ASSERT(!everlive_json_get_double(NULL, "flt", &dval), "everlive_json_get_double rejects NULL json"); + TEST_ASSERT(!everlive_json_get_double(valid_json, NULL, &dval), "everlive_json_get_double rejects NULL key"); + TEST_ASSERT(!everlive_json_get_double(valid_json, "flt", NULL), "everlive_json_get_double rejects NULL output"); - TEST_ASSERT(!json_get_bool(NULL, "flag", &bval), "json_get_bool rejects NULL json"); - TEST_ASSERT(!json_get_bool(valid_json, NULL, &bval), "json_get_bool rejects NULL key"); - TEST_ASSERT(!json_get_bool(valid_json, "flag", NULL), "json_get_bool rejects NULL output"); + TEST_ASSERT(!everlive_json_get_bool(NULL, "flag", &bval), "everlive_json_get_bool rejects NULL json"); + TEST_ASSERT(!everlive_json_get_bool(valid_json, NULL, &bval), "everlive_json_get_bool rejects NULL key"); + TEST_ASSERT(!everlive_json_get_bool(valid_json, "flag", NULL), "everlive_json_get_bool rejects NULL output"); - TEST_ASSERT(!json_find_value(NULL, "id", out, sizeof(out)), "json_find_value rejects NULL json"); - TEST_ASSERT(!json_find_value(valid_json, NULL, out, sizeof(out)), "json_find_value rejects NULL key"); - TEST_ASSERT(!json_find_value(valid_json, "id", NULL, sizeof(out)), "json_find_value rejects NULL output"); - TEST_ASSERT(!json_find_value(valid_json, "id", out, 0), "json_find_value rejects 0 outsz"); + TEST_ASSERT(!everlive_json_find_value(NULL, "id", out, sizeof(out)), "everlive_json_find_value rejects NULL json"); + TEST_ASSERT(!everlive_json_find_value(valid_json, NULL, out, sizeof(out)), "everlive_json_find_value rejects NULL key"); + TEST_ASSERT(!everlive_json_find_value(valid_json, "id", NULL, sizeof(out)), "everlive_json_find_value rejects NULL output"); + TEST_ASSERT(!everlive_json_find_value(valid_json, "id", out, 0), "everlive_json_find_value rejects 0 outsz"); /* Malformed JSON input */ - TEST_ASSERT(!json_get_string("", "str", out, sizeof(out)), "Rejects empty JSON string"); - TEST_ASSERT(!json_get_string("not json at all", "str", out, sizeof(out)), "Rejects non-JSON string"); - TEST_ASSERT(!json_get_string("{\"unclosed\":", "unclosed", out, sizeof(out)), "Rejects unclosed JSON"); - TEST_ASSERT(!json_get_string("[1, 2, 3]", "str", out, sizeof(out)), "Rejects array root JSON"); - TEST_ASSERT(!json_get_string("{unquoted_key: 1}", "unquoted_key", out, sizeof(out)), "Rejects unquoted keys"); + TEST_ASSERT(!everlive_json_get_string("", "str", out, sizeof(out)), "Rejects empty JSON string"); + TEST_ASSERT(!everlive_json_get_string("not json at all", "str", out, sizeof(out)), "Rejects non-JSON string"); + TEST_ASSERT(!everlive_json_get_string("{\"unclosed\":", "unclosed", out, sizeof(out)), "Rejects unclosed JSON"); + TEST_ASSERT(!everlive_json_get_string("[1, 2, 3]", "str", out, sizeof(out)), "Rejects array root JSON"); + TEST_ASSERT(!everlive_json_get_string("{unquoted_key: 1}", "unquoted_key", out, sizeof(out)), "Rejects unquoted keys"); /* Type mismatches */ - TEST_ASSERT(!json_get_string(valid_json, "num", out, sizeof(out)), - "Type mismatch: json_get_string rejects primitive number"); - TEST_ASSERT(!json_get_long(valid_json, "str", &lval), - "Type mismatch: json_get_long rejects string value"); - TEST_ASSERT(!json_get_double(valid_json, "str", &dval), - "Type mismatch: json_get_double rejects string value"); - TEST_ASSERT(!json_get_bool(valid_json, "str", &bval), - "Type mismatch: json_get_bool rejects string value"); - TEST_ASSERT(!json_get_bool(valid_json, "num", &bval), - "Type mismatch: json_get_bool rejects number value"); + TEST_ASSERT(!everlive_json_get_string(valid_json, "num", out, sizeof(out)), + "Type mismatch: everlive_json_get_string rejects primitive number"); + TEST_ASSERT(!everlive_json_get_long(valid_json, "str", &lval), + "Type mismatch: everlive_json_get_long rejects string value"); + TEST_ASSERT(!everlive_json_get_double(valid_json, "str", &dval), + "Type mismatch: everlive_json_get_double rejects string value"); + TEST_ASSERT(!everlive_json_get_bool(valid_json, "str", &bval), + "Type mismatch: everlive_json_get_bool rejects string value"); + TEST_ASSERT(!everlive_json_get_bool(valid_json, "num", &bval), + "Type mismatch: everlive_json_get_bool rejects number value"); /* Destination buffer too small for string */ char small_buf[4]; - TEST_ASSERT(!json_get_string(valid_json, "str", small_buf, sizeof(small_buf)), + TEST_ASSERT(!everlive_json_get_string(valid_json, "str", small_buf, sizeof(small_buf)), "Safely rejects string when destination buffer is too small"); - /* json_escape_into negative edge cases */ - json_escape_into(NULL, 10, "test"); /* Should not crash */ - json_escape_into(out, 0, "test"); /* Should not crash */ - json_escape_into(out, sizeof(out), NULL); - TEST_ASSERT(out[0] == '\0', "json_escape_into handles NULL src cleanly"); + /* everlive_json_escape_into negative edge cases */ + everlive_json_escape_into(NULL, 10, "test"); /* Should not crash */ + everlive_json_escape_into(out, 0, "test"); /* Should not crash */ + everlive_json_escape_into(out, sizeof(out), NULL); + TEST_ASSERT(out[0] == '\0', "everlive_json_escape_into handles NULL src cleanly"); - /* json_format_id negative edge cases */ - json_format_id(NULL, 10, "id"); /* Should not crash */ - json_format_id(out, 0, "id"); /* Should not crash */ + /* everlive_json_format_id negative edge cases */ + everlive_json_format_id(NULL, 10, "id"); /* Should not crash */ + everlive_json_format_id(out, 0, "id"); /* Should not crash */ } int main(void) diff --git a/tools/libeverlive-core/Makefile b/tools/libeverlive-core/Makefile index 34ae301..68ec76f 100644 --- a/tools/libeverlive-core/Makefile +++ b/tools/libeverlive-core/Makefile @@ -1,10 +1,15 @@ # SPDX-License-Identifier: GPL-2.0-only +# +# EverLive core library: card registry, frame conversion, JSON-RPC helpers, +# control-socket resolution, and the everlive-ctl CLI. + CC ?= gcc AR ?= ar CFLAGS ?= -O2 -g -Wall -Wextra -Wno-unused-parameter -std=gnu11 -D_GNU_SOURCE -fPIC +DEPFLAGS := -MMD -MP SOVERSION := 0 -VERSION := 0.1.0 +VERSION := 0.2.0 SONAME := libeverlive_core.so.$(SOVERSION) SHARED_REAL := libeverlive_core.so.$(VERSION) @@ -12,6 +17,12 @@ TARGET_STATIC := libeverlive_core.a TARGET_SHARED := libeverlive_core.so TARGET_CLI := everlive-ctl +CORE_OBJS := everlive_card.o everlive_frame.o everlive_json.o \ + everlive_ipc.o everlive_ctl_proto.o third_party/jsmn.o +CTL_OBJS := everlive_ctl.o + +INCLUDES := -I. + PREFIX ?= /usr/local DESTDIR ?= BINDIR ?= $(PREFIX)/bin @@ -19,15 +30,12 @@ LIBDIR ?= $(PREFIX)/lib INCLUDEDIR ?= $(PREFIX)/include SYSTEMD_USER_DIR ?= $(PREFIX)/lib/systemd/user -OBJS := everlive_card.o everlive_frame.o -CTL_OBJS := everlive_ctl.o everlive_ctl_proto.o - all: $(TARGET_STATIC) $(TARGET_SHARED) $(TARGET_CLI) -$(TARGET_STATIC): $(OBJS) +$(TARGET_STATIC): $(CORE_OBJS) $(AR) rcs $@ $^ -$(SHARED_REAL): $(OBJS) +$(SHARED_REAL): $(CORE_OBJS) $(CC) -shared -Wl,-soname,$(SONAME) -o $@ $^ $(SONAME): $(SHARED_REAL) @@ -37,10 +45,18 @@ $(TARGET_SHARED): $(SONAME) ln -sf $(SONAME) $@ $(TARGET_CLI): $(CTL_OBJS) $(TARGET_STATIC) - $(CC) $(CFLAGS) -o $@ $^ + $(CC) $(CFLAGS) -o $@ $(CTL_OBJS) $(TARGET_STATIC) -%.o: %.c everlive_card.h everlive_frame.h everlive_ctl.h - $(CC) $(CFLAGS) -c -o $@ $< +third_party/jsmn.o: third_party/jsmn.c third_party/jsmn.h + $(CC) $(CFLAGS) $(INCLUDES) $(DEPFLAGS) -c -o $@ $< + +%.o: %.c + $(CC) $(CFLAGS) $(INCLUDES) $(DEPFLAGS) -c -o $@ $< + +-include $(CORE_OBJS:.o=.d) $(CTL_OBJS:.o=.d) + +test selftest: all + $(MAKE) -C ../../tests test install: all install -d $(DESTDIR)$(BINDIR) $(DESTDIR)$(LIBDIR) \ @@ -52,9 +68,10 @@ install: all ln -sf $(SONAME) $(DESTDIR)$(LIBDIR)/$(TARGET_SHARED) install -m 0644 $(TARGET_STATIC) $(DESTDIR)$(LIBDIR)/$(TARGET_STATIC) install -m 0644 everlive_card.h everlive_frame.h everlive_ctl.h \ - $(DESTDIR)$(INCLUDEDIR)/ - install -m 0644 everlive.service \ - $(DESTDIR)$(SYSTEMD_USER_DIR)/everlive.service + everlive_json.h everlive_ipc.h $(DESTDIR)$(INCLUDEDIR)/ + sed 's|/usr/local|$(PREFIX)|g' everlive.service \ + > $(DESTDIR)$(SYSTEMD_USER_DIR)/everlive.service + chmod 0644 $(DESTDIR)$(SYSTEMD_USER_DIR)/everlive.service uninstall: rm -f $(DESTDIR)$(BINDIR)/$(TARGET_CLI) \ @@ -66,10 +83,13 @@ uninstall: $(DESTDIR)$(INCLUDEDIR)/everlive_card.h \ $(DESTDIR)$(INCLUDEDIR)/everlive_frame.h \ $(DESTDIR)$(INCLUDEDIR)/everlive_ctl.h \ + $(DESTDIR)$(INCLUDEDIR)/everlive_json.h \ + $(DESTDIR)$(INCLUDEDIR)/everlive_ipc.h \ $(DESTDIR)$(SYSTEMD_USER_DIR)/everlive.service clean: - rm -f $(OBJS) $(CTL_OBJS) $(TARGET_STATIC) $(TARGET_SHARED) \ - $(SONAME) $(SHARED_REAL) $(TARGET_CLI) + rm -f $(CORE_OBJS) $(CTL_OBJS) $(CORE_OBJS:.o=.d) $(CTL_OBJS:.o=.d) \ + $(TARGET_STATIC) $(TARGET_SHARED) $(SONAME) $(SHARED_REAL) \ + $(TARGET_CLI) -.PHONY: all clean install uninstall +.PHONY: all test selftest install uninstall clean diff --git a/tools/libeverlive-core/everlive_card.c b/tools/libeverlive-core/everlive_card.c index 4de56c9..0dd13fe 100644 --- a/tools/libeverlive-core/everlive_card.c +++ b/tools/libeverlive-core/everlive_card.c @@ -226,20 +226,29 @@ const struct everlive_device_desc *everlive_get_card_registry(size_t *out_count) return kEverliveCardRegistry; } +const struct everlive_device_desc * +everlive_find_card_by_slug(const char *model_slug) +{ + if (!model_slug || !*model_slug) + return NULL; + + for (size_t i = 0; i < REGISTRY_COUNT; i++) { + if (strcmp(kEverliveCardRegistry[i].model_slug, model_slug) == 0) + return &kEverliveCardRegistry[i]; + } + + return NULL; +} + int everlive_register_card_ops(const char *model_slug, const struct everlive_card_ops *ops) { - const struct everlive_device_desc *canonical = NULL; + const struct everlive_device_desc *canonical; if (!model_slug || !*model_slug || !ops) return -EINVAL; - for (size_t i = 0; i < REGISTRY_COUNT; i++) { - if (strcmp(kEverliveCardRegistry[i].model_slug, model_slug) == 0) { - canonical = &kEverliveCardRegistry[i]; - break; - } - } + canonical = everlive_find_card_by_slug(model_slug); if (!canonical) return -ENOENT; diff --git a/tools/libeverlive-core/everlive_card.h b/tools/libeverlive-core/everlive_card.h index fae3cba..e4bb95e 100644 --- a/tools/libeverlive-core/everlive_card.h +++ b/tools/libeverlive-core/everlive_card.h @@ -171,6 +171,10 @@ struct everlive_device { /* Returns a pointer to the static registry table and sets out_count */ const struct everlive_device_desc *everlive_get_card_registry(size_t *out_count); +/* Returns the registry entry for @model_slug, or NULL when unknown. */ +const struct everlive_device_desc * +everlive_find_card_by_slug(const char *model_slug); + /* * Bind a backend implementation to every registry entry with @model_slug. * Backends own their hardware context; the core copies the vtable by value, diff --git a/tools/libeverlive-core/everlive_ctl.c b/tools/libeverlive-core/everlive_ctl.c index db935be..d8b2631 100644 --- a/tools/libeverlive-core/everlive_ctl.c +++ b/tools/libeverlive-core/everlive_ctl.c @@ -5,6 +5,7 @@ #include "everlive_card.h" #include "everlive_ctl.h" +#include "everlive_ipc.h" #include #include @@ -450,30 +451,7 @@ static int cmd_list_models(void) static int ctl_resolve_socket(char *buf, size_t size) { - const char *override = getenv("EVERLIVE_CONTROL_SOCKET"); - const char *xdg = getenv("XDG_RUNTIME_DIR"); - uid_t uid = getuid(); - int n; - - if (override && *override) { - n = snprintf(buf, size, "%s", override); - return (n > 0 && (size_t)n < size) ? 0 : -1; - } - - if (xdg && *xdg) { - n = snprintf(buf, size, "%s/everlive-experimental.sock", xdg); - if (n > 0 && (size_t)n < size && access(buf, F_OK) == 0) - return 0; - } - - n = snprintf(buf, size, "/run/user/%u/everlive-experimental.sock", - (unsigned int)uid); - if (n > 0 && (size_t)n < size && access(buf, F_OK) == 0) - return 0; - - n = snprintf(buf, size, "/tmp/everlive-%u/everlive-experimental.sock", - (unsigned int)uid); - return (n > 0 && (size_t)n < size) ? 0 : -1; + return everlive_ipc_resolve_socket_path(buf, size, NULL) == 0 ? 0 : -1; } /* diff --git a/tools/libeverlive-core/everlive_ctl_proto.c b/tools/libeverlive-core/everlive_ctl_proto.c index e895094..eb6e2bc 100644 --- a/tools/libeverlive-core/everlive_ctl_proto.c +++ b/tools/libeverlive-core/everlive_ctl_proto.c @@ -7,6 +7,7 @@ */ #include "everlive_ctl.h" +#include "everlive_json.h" #include #include @@ -30,70 +31,12 @@ int everlive_ctl_build_request(const char *method, const char *params, void everlive_ctl_json_escape(const char *in, char *out, size_t out_size) { - size_t o = 0; - - if (!out || out_size == 0) - return; - - if (!in) { - out[0] = '\0'; - return; - } - - for (const unsigned char *p = (const unsigned char *)in; *p; p++) { - if (o + 8 >= out_size) - break; - switch (*p) { - case '"': - out[o++] = '\\'; - out[o++] = '"'; - break; - case '\\': - out[o++] = '\\'; - out[o++] = '\\'; - break; - case '\n': - out[o++] = '\\'; - out[o++] = 'n'; - break; - case '\r': - out[o++] = '\\'; - out[o++] = 'r'; - break; - case '\t': - out[o++] = '\\'; - out[o++] = 't'; - break; - default: - if (*p < 0x20) - o += (size_t)snprintf(out + o, out_size - o, - "\\u%04x", *p); - else - out[o++] = (char)*p; - break; - } - } - out[o] = '\0'; + everlive_json_escape_into(out, out_size, in); } bool everlive_ctl_response_has_error(const char *json) { - const char *p = json; - - if (!json) - return false; - - while ((p = strstr(p, "\"error\"")) != NULL) { - const char *q = p + 7; /* strlen("\"error\"") */ - - while (*q == ' ' || *q == '\t' || *q == '\n' || *q == '\r') - q++; - if (*q == ':') - return true; - p = q; - } - - return false; + return everlive_json_has_top_level_key(json, "error"); } int everlive_ctl_parse_double(const char *value, double *out) diff --git a/tools/libeverlive-core/everlive_ipc.c b/tools/libeverlive-core/everlive_ipc.c new file mode 100644 index 0000000..96537a3 --- /dev/null +++ b/tools/libeverlive-core/everlive_ipc.c @@ -0,0 +1,112 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * everlive_ipc.c — Shared control-socket path resolution for EverLive + */ + +#include "everlive_ipc.h" + +#include +#include +#include +#include +#include +#include + +static int ipc_copy(char *buf, size_t size, const char *value) +{ + int n; + + if (!buf || size == 0) + return -EINVAL; + + n = snprintf(buf, size, "%s", value); + if (n < 0 || (size_t)n >= size || (size_t)n >= EVERLIVE_IPC_PATH_MAX) + return -ENAMETOOLONG; + + return 0; +} + +int everlive_ipc_runtime_dir(char *buf, size_t size) +{ + const char *xdg; + char candidate[EVERLIVE_IPC_PATH_MAX]; + int n; + + if (!buf || size == 0) + return -EINVAL; + + xdg = getenv("XDG_RUNTIME_DIR"); + if (xdg && *xdg && access(xdg, W_OK) == 0) + return ipc_copy(buf, size, xdg); + + n = snprintf(candidate, sizeof(candidate), "/run/user/%u", + (unsigned int)getuid()); + if (n > 0 && (size_t)n < sizeof(candidate) && + access(candidate, W_OK) == 0) + return ipc_copy(buf, size, candidate); + + n = snprintf(candidate, sizeof(candidate), "/tmp/everlive-%u", + (unsigned int)getuid()); + if (n < 0 || (size_t)n >= sizeof(candidate)) + return -ENAMETOOLONG; + + return ipc_copy(buf, size, candidate); +} + +int everlive_ipc_resolve_socket_path(char *buf, size_t size, + const char *override_path) +{ + const char *env; + char dir[EVERLIVE_IPC_PATH_MAX]; + int n; + int ret; + + if (!buf || size == 0) + return -EINVAL; + + if (override_path && *override_path) + return ipc_copy(buf, size, override_path); + + env = getenv("EVERLIVE_SOCKET"); + if (env && *env) + return ipc_copy(buf, size, env); + + env = getenv("EVERLIVE_CONTROL_SOCKET"); + if (env && *env) + return ipc_copy(buf, size, env); + + ret = everlive_ipc_runtime_dir(dir, sizeof(dir)); + if (ret < 0) + return ret; + + n = snprintf(buf, size, "%s/%s", dir, EVERLIVE_IPC_SOCKET_NAME); + if (n < 0 || (size_t)n >= size || (size_t)n >= EVERLIVE_IPC_PATH_MAX) + return -ENAMETOOLONG; + + return 0; +} + +int everlive_ipc_prepare_socket_path(char *buf, size_t size, + const char *override_path) +{ + char dir[EVERLIVE_IPC_PATH_MAX]; + char *slash; + int ret; + + ret = everlive_ipc_resolve_socket_path(buf, size, override_path); + if (ret < 0) + return ret; + + if (snprintf(dir, sizeof(dir), "%s", buf) < 0) + return -EINVAL; + + slash = strrchr(dir, '/'); + if (!slash || slash == dir) + return 0; + + *slash = '\0'; + if (mkdir(dir, 0700) < 0 && errno != EEXIST) + return -errno; + + return 0; +} diff --git a/tools/libeverlive-core/everlive_ipc.h b/tools/libeverlive-core/everlive_ipc.h new file mode 100644 index 0000000..65576bf --- /dev/null +++ b/tools/libeverlive-core/everlive_ipc.h @@ -0,0 +1,57 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * everlive_ipc.h — Shared control-socket path resolution for EverLive + * + * Both engines bind and every client (everlive-ctl, the Python hub, shell + * helpers) connects through the same deterministic path. The resolver never + * probes the socket itself: a path must be valid before the socket exists. + */ + +#ifndef EVERLIVE_IPC_H +#define EVERLIVE_IPC_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define EVERLIVE_IPC_SOCKET_NAME "everlive.sock" +#define EVERLIVE_IPC_PATH_MAX 108 /* sizeof sun_path */ + +/* + * Runtime directory, without touching the socket: + * $XDG_RUNTIME_DIR (if set and writable) + * /run/user/ (if writable) + * /tmp/everlive- + * + * Returns 0, -EINVAL for bad arguments, or -ENAMETOOLONG when it does not + * fit in @buf. + */ +int everlive_ipc_runtime_dir(char *buf, size_t size); + +/* + * Pure path resolution: + * 1. @override_path + * 2. $EVERLIVE_SOCKET (full path; used by everlive_client.py) + * 3. $EVERLIVE_CONTROL_SOCKET (legacy full path) + * 4. /everlive.sock + * + * Returns 0, -EINVAL on bad arguments, or -ENAMETOOLONG when it does not fit. + */ +int everlive_ipc_resolve_socket_path(char *buf, size_t size, + const char *override_path); + +/* + * Same as resolve, then ensures the parent directory exists (mkdir 0700, + * EEXIST is success). Intended for the server side before bind(); clients + * must use the pure resolver so they never create directories. + */ +int everlive_ipc_prepare_socket_path(char *buf, size_t size, + const char *override_path); + +#ifdef __cplusplus +} +#endif + +#endif /* EVERLIVE_IPC_H */ diff --git a/tools/libeverlive-core/everlive_json.c b/tools/libeverlive-core/everlive_json.c new file mode 100644 index 0000000..2329cef --- /dev/null +++ b/tools/libeverlive-core/everlive_json.c @@ -0,0 +1,442 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * everlive_json.c — jsmn-backed JSON-RPC 2.0 scanners and formatters + * + * The wire protocol is newline-delimited JSON-RPC 2.0 over AF_UNIX. Requests + * are flat objects, so a token scan is enough; a full parser would add a + * dependency for no benefit. Unknown fields are ignored and the raw "id" + * token is echoed back verbatim. + */ + +#include "everlive_json.h" + +#include +#include +#include +#include + +/* + * The jsmntok_t layout depends on JSMN_PARENT_LINKS. It MUST match + * third_party/jsmn.o, which defines the implementation; this file only needs + * the declarations plus the same struct layout. + */ +#define JSMN_PARENT_LINKS +#define JSMN_HEADER +#include "third_party/jsmn.h" + +typedef enum { + JSON_GET_STRING, + JSON_GET_LONG, + JSON_GET_DOUBLE, + JSON_GET_BOOL, + JSON_GET_RAW +} json_get_type; + +/* + * Find a key in the JSON token tree. Direct children of the root object are + * matched first so a value string that happens to equal the key name (for + * example {"params":{"text":"method"},"method":"led.set"}) can never shadow + * the real top-level key. + * + * With JSMN_PARENT_LINKS a key token's parent is the containing object, while + * a value token's parent is the preceding key token. Matching on + * parent == 0 therefore selects root-level keys only. + */ +static int json_token_find_key(const jsmntok_t *tokens, int tok_count, + const char *key, const char *json) +{ + const size_t klen = strlen(key); + + /* Pass 1: keys of the root object (token 0). */ + for (int i = 1; i < tok_count; i++) { + size_t len; + + if (tokens[i].type != JSMN_STRING || tokens[i].parent != 0) + continue; + len = (size_t)(tokens[i].end - tokens[i].start); + if (len == klen && memcmp(json + tokens[i].start, key, klen) == 0) { + if (i + 1 < tok_count) + return i + 1; + } + } + + /* Pass 2: keys of any nested object (e.g. inside "params"). */ + for (int i = 1; i < tok_count; i++) { + size_t len; + + if (tokens[i].type != JSMN_STRING) + continue; + if (tokens[i].parent <= 0 || + tokens[tokens[i].parent].type != JSMN_OBJECT) + continue; + len = (size_t)(tokens[i].end - tokens[i].start); + if (len == klen && memcmp(json + tokens[i].start, key, klen) == 0) { + if (i + 1 < tok_count) + return i + 1; + } + } + + return -1; +} + +static bool json_token_to_string(const jsmntok_t *tok, const char *json, + char *out, size_t outsz) +{ + int len; + + if (!tok || tok->type != JSMN_STRING) + return false; + len = tok->end - tok->start; + if (len < 0 || (size_t)len >= outsz) + return false; + memcpy(out, json + tok->start, (size_t)len); + out[len] = '\0'; + return true; +} + +static bool json_token_to_long(const jsmntok_t *tok, const char *json, + long *out) +{ + char buf[64]; + char *end; + long val; + int len; + + if (!tok || tok->type != JSMN_PRIMITIVE || !out) + return false; + len = tok->end - tok->start; + if (len < 0 || len >= (int)sizeof(buf)) + return false; + memcpy(buf, json + tok->start, (size_t)len); + buf[len] = '\0'; + errno = 0; + val = strtol(buf, &end, 10); + if (errno || end == buf || *end != '\0') + return false; + *out = val; + return true; +} + +static bool json_token_to_double(const jsmntok_t *tok, const char *json, + double *out) +{ + char buf[64]; + char *end; + double val; + int len; + + if (!tok || tok->type != JSMN_PRIMITIVE || !out) + return false; + len = tok->end - tok->start; + if (len < 0 || len >= (int)sizeof(buf)) + return false; + memcpy(buf, json + tok->start, (size_t)len); + buf[len] = '\0'; + errno = 0; + val = strtod(buf, &end); + if (errno || end == buf || *end != '\0') + return false; + *out = val; + return true; +} + +static bool json_token_to_bool(const jsmntok_t *tok, const char *json, + bool *out) +{ + int len; + + if (!tok || tok->type != JSMN_PRIMITIVE || !out) + return false; + len = tok->end - tok->start; + if (len == 4 && strncmp(json + tok->start, "true", 4) == 0) { + *out = true; + return true; + } + if (len == 5 && strncmp(json + tok->start, "false", 5) == 0) { + *out = false; + return true; + } + return false; +} + +static bool json_token_to_raw(const jsmntok_t *tok, const char *json, + char *out, size_t outsz) +{ + int len; + + if (!tok || !json || !out || outsz == 0) + return false; + len = tok->end - tok->start; + if (len < 0 || (size_t)len >= outsz) + return false; + memcpy(out, json + tok->start, (size_t)len); + out[len] = '\0'; + return true; +} + +static bool json_parse_request(const char *input, size_t len, + const char *key, char *out, size_t outsz, + json_get_type type, void *num_out) +{ + jsmn_parser parser; + jsmntok_t tokens[EVERLIVE_JSON_MAX_TOKENS]; + const jsmntok_t *val; + int tok_count; + int val_idx; + + if (!input || !key || len >= EVERLIVE_JSON_MAX_INPUT) + return false; + + jsmn_init(&parser); + tok_count = jsmn_parse(&parser, input, len, tokens, + EVERLIVE_JSON_MAX_TOKENS); + if (tok_count < 1 || tokens[0].type != JSMN_OBJECT) + return false; + + val_idx = json_token_find_key(tokens, tok_count, key, input); + if (val_idx < 0) + return false; + + val = &tokens[val_idx]; + switch (type) { + case JSON_GET_STRING: + return json_token_to_string(val, input, out, outsz); + case JSON_GET_LONG: + return json_token_to_long(val, input, (long *)num_out); + case JSON_GET_DOUBLE: + return json_token_to_double(val, input, (double *)num_out); + case JSON_GET_BOOL: + return json_token_to_bool(val, input, (bool *)num_out); + case JSON_GET_RAW: + return json_token_to_raw(val, input, out, outsz); + } + return false; +} + +bool everlive_json_get_string(const char *json, const char *key, + char *out, size_t outsz) +{ + if (!json || !key || !out || outsz == 0) + return false; + return json_parse_request(json, strlen(json), key, out, outsz, + JSON_GET_STRING, NULL); +} + +bool everlive_json_get_long(const char *json, const char *key, long *out) +{ + if (!json || !key || !out) + return false; + return json_parse_request(json, strlen(json), key, NULL, 0, + JSON_GET_LONG, out); +} + +bool everlive_json_get_double(const char *json, const char *key, double *out) +{ + if (!json || !key || !out) + return false; + return json_parse_request(json, strlen(json), key, NULL, 0, + JSON_GET_DOUBLE, out); +} + +bool everlive_json_get_bool(const char *json, const char *key, bool *out) +{ + if (!json || !key || !out) + return false; + return json_parse_request(json, strlen(json), key, NULL, 0, + JSON_GET_BOOL, out); +} + +bool everlive_json_get_raw(const char *json, const char *key, + char *out, size_t outsz) +{ + if (!json || !key || !out || outsz == 0) + return false; + return json_parse_request(json, strlen(json), key, out, outsz, + JSON_GET_RAW, NULL); +} + +bool everlive_json_find_value(const char *json, const char *key, + char *out, size_t outsz) +{ + if (!json || !key || !out || outsz == 0) + return false; + + /* "method" is a string; everything else keeps its raw token form. */ + if (strcmp(key, "method") == 0) + return everlive_json_get_string(json, key, out, outsz); + + return everlive_json_get_raw(json, key, out, outsz); +} + +/* Quote-then-colon scan used only when jsmn cannot fit the document. */ +static bool json_scan_key(const char *json, const char *key) +{ + const size_t klen = strlen(key); + const char *p = json; + + while ((p = strstr(p, "\"")) != NULL) { + const char *q; + + p++; + if (strncmp(p, key, klen) != 0 || p[klen] != '"') + continue; + q = p + klen + 1; + while (*q == ' ' || *q == '\t' || *q == '\n' || *q == '\r') + q++; + if (*q == ':') + return true; + } + + return false; +} + +bool everlive_json_has_top_level_key(const char *json, const char *key) +{ + jsmn_parser parser; + jsmntok_t tokens[EVERLIVE_JSON_RESPONSE_TOKENS]; + int tok_count; + + if (!json || !key || !*key) + return false; + + jsmn_init(&parser); + tok_count = jsmn_parse(&parser, json, strlen(json), tokens, + EVERLIVE_JSON_RESPONSE_TOKENS); + if (tok_count < 1 || tokens[0].type != JSMN_OBJECT) + return json_scan_key(json, key); + + for (int i = 1; i < tok_count; i++) { + size_t len; + + if (tokens[i].type != JSMN_STRING || tokens[i].parent != 0) + continue; + len = (size_t)(tokens[i].end - tokens[i].start); + if (len == strlen(key) && + memcmp(json + tokens[i].start, key, len) == 0) + return true; + } + + return false; +} + +void everlive_json_escape_into(char *dst, size_t dstsz, const char *src) +{ + const unsigned char *p; + size_t o = 0; + + if (!dst || dstsz == 0) + return; + + if (!src) { + dst[0] = '\0'; + return; + } + + for (p = (const unsigned char *)src; *p; p++) { + const char *esc = NULL; + + switch (*p) { + case '"': + esc = "\\\""; + break; + case '\\': + esc = "\\\\"; + break; + case '\n': + esc = "\\n"; + break; + case '\r': + esc = "\\r"; + break; + case '\t': + esc = "\\t"; + break; + default: + break; + } + + if (esc) { + if (o + 2 >= dstsz) + break; + dst[o++] = esc[0]; + dst[o++] = esc[1]; + } else if (*p < 0x20) { + char ubuf[7]; + + if (o + 6 >= dstsz) + break; + snprintf(ubuf, sizeof(ubuf), "\\u%04x", *p); + memcpy(dst + o, ubuf, 6); + o += 6; + } else { + if (o + 1 >= dstsz) + break; + dst[o++] = (char)*p; + } + } + + dst[o] = '\0'; +} + +/* Canonical JSON integer: -?(0|[1-9][0-9]*). */ +static bool json_is_canonical_int(const char *s) +{ + if (!s || !*s) + return false; + if (*s == '-') + s++; + if (!*s) + return false; + if (*s == '0') + return s[1] == '\0'; + if (*s < '1' || *s > '9') + return false; + for (s++; *s; s++) { + if (*s < '0' || *s > '9') + return false; + } + return true; +} + +void everlive_json_format_id(char *dst, size_t dstsz, const char *id) +{ + char inner[EVERLIVE_JSON_ID_MAX]; + const char *body = id; + size_t len; + size_t elen; + + if (!dst || dstsz == 0) + return; + + if (!id || !*id || strcmp(id, "null") == 0) { + snprintf(dst, dstsz, "null"); + return; + } + + if (json_is_canonical_int(id)) { + snprintf(dst, dstsz, "%s", id); + return; + } + + if (dstsz < 3) { + snprintf(dst, dstsz, "null"); + return; + } + + len = strlen(id); + if (id[0] == '"' && len >= 2 && id[len - 1] == '"') { + size_t in_len = len - 2; + + if (in_len >= sizeof(inner)) + in_len = sizeof(inner) - 1; + memcpy(inner, id + 1, in_len); + inner[in_len] = '\0'; + body = inner; + } + + dst[0] = '"'; + everlive_json_escape_into(dst + 1, dstsz - 2, body); + elen = strlen(dst); + dst[elen] = '"'; + dst[elen + 1] = '\0'; +} diff --git a/tools/libeverlive-core/everlive_json.h b/tools/libeverlive-core/everlive_json.h new file mode 100644 index 0000000..c5e8358 --- /dev/null +++ b/tools/libeverlive-core/everlive_json.h @@ -0,0 +1,75 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * everlive_json.h — Flat-object JSON-RPC 2.0 helpers for EverLive + * + * Stateless, allocation-free scanners built on the vendored jsmn parser. + * Every function is re-entrant: all scratch space lives on the caller's + * stack, so the GC573 control thread and the GC570D daemon loop may call + * these while the render/DMA paths are live. + */ + +#ifndef EVERLIVE_JSON_H +#define EVERLIVE_JSON_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * Request payloads are small. Responses (status.get) are ~135 tokens today; + * a 128-token budget returns JSMN_ERROR_NOMEM, so responses get 512. + */ +#define EVERLIVE_JSON_MAX_TOKENS 256U +#define EVERLIVE_JSON_RESPONSE_TOKENS 512U +#define EVERLIVE_JSON_MAX_INPUT (64U * 1024U) +#define EVERLIVE_JSON_ID_MAX 48U + +/* + * Typed getters. Return false when the key is absent, the document is + * malformed, or the value type does not match. + */ +bool everlive_json_get_string(const char *json, const char *key, + char *out, size_t outsz); +bool everlive_json_get_long(const char *json, const char *key, long *out); +bool everlive_json_get_double(const char *json, const char *key, double *out); +bool everlive_json_get_bool(const char *json, const char *key, bool *out); +bool everlive_json_get_raw(const char *json, const char *key, + char *out, size_t outsz); + +/* + * Compatibility helper used by the IPC dispatchers: "method" is extracted as + * an unquoted string, every other key as its raw token (number, null, or the + * unquoted contents of a string). Preserves `id` echo semantics. + */ +bool everlive_json_find_value(const char *json, const char *key, + char *out, size_t outsz); + +/* + * True when the root object has @key as a direct child. Replaces substring + * scanning for JSON-RPC error detection. Falls back to a quote+colon scan + * when the document cannot be parsed (e.g. a truncated reply). + */ +bool everlive_json_has_top_level_key(const char *json, const char *key); + +/* + * Escape @src as the body of a JSON string literal. Always NUL-terminates + * and never writes past dstsz-1. dst == NULL or dstsz == 0 is a no-op; + * src == NULL writes an empty string. + */ +void everlive_json_escape_into(char *dst, size_t dstsz, const char *src); + +/* + * Normalise a JSON-RPC "id" token for echoing: NULL/empty/"null" -> null, + * canonical integers are emitted verbatim, everything else is quoted and + * re-escaped. dstsz < 3 yields "null". + */ +void everlive_json_format_id(char *dst, size_t dstsz, const char *id); + +#ifdef __cplusplus +} +#endif + +#endif /* EVERLIVE_JSON_H */ diff --git a/tools/opengc573-userd/third_party/jsmn.c b/tools/libeverlive-core/third_party/jsmn.c similarity index 100% rename from tools/opengc573-userd/third_party/jsmn.c rename to tools/libeverlive-core/third_party/jsmn.c diff --git a/tools/opengc573-userd/third_party/jsmn.h b/tools/libeverlive-core/third_party/jsmn.h similarity index 100% rename from tools/opengc573-userd/third_party/jsmn.h rename to tools/libeverlive-core/third_party/jsmn.h diff --git a/tools/opengc573-userd/Makefile b/tools/opengc573-userd/Makefile index 735e4a6..ab7a6a0 100644 --- a/tools/opengc573-userd/Makefile +++ b/tools/opengc573-userd/Makefile @@ -17,23 +17,24 @@ PW_LIBS = $(shell pkg-config --libs libpipewire-0.3) # Frame generation is delegated to the system lsfg-vk Vulkan implicit layer, # which the SDL2 "vulkan" renderer feeds directly. No GL, no Vulkan headers. -COMMON_OBJS = gc573_vfio.o gc573_user_i2c.o gc573_user_hw.o gc573_user_edid.o gc573_user_it6805.o gc573_user_rx_eq.o gc573_user_it6664.o third_party/jsmn.o +COMMON_OBJS = gc573_vfio.o gc573_user_i2c.o gc573_user_hw.o gc573_user_edid.o gc573_user_it6805.o gc573_user_rx_eq.o gc573_user_it6664.o STREAM_OBJS = gc573_user_log.o gc573_user_fpga.o gc573_user_video.o gc573_user_audio.o gc573_user_bitstream.o gc573_user_nosignal.o gc573_user_ipc.o gc573_user_audio_tap.o gc573_user_hdr.o gc573_user_framegen.o +CORE_LIB = ../libeverlive-core/libeverlive_core.a TARGETS = gc573_user_probe gc573_user_streamer everlive_userd gc573_user_diag HEADERS = gc573-user-compat.h gc573_user_log.h gc573_vfio.h gc573_user_i2c.h gc573_user_hw.h gc573_user_edid.h gc573_user_it6805.h gc573_user_rx_eq.h gc573_user_it6664.h gc573_user_fpga.h gc573_user_video.h gc573_user_audio.h gc573_user_bitstream.h gc573_user_nosignal.h gc573_user_ipc.h gc573_user_audio_tap.h gc573_user_hdr.h gc573_user_framegen.h all: $(TARGETS) -gc573_user_probe: gc573_user_probe.o $(COMMON_OBJS) +gc573_user_probe: gc573_user_probe.o $(COMMON_OBJS) $(CORE_LIB) $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) -gc573_user_streamer: gc573_user_streamer.o $(COMMON_OBJS) $(STREAM_OBJS) +gc573_user_streamer: gc573_user_streamer.o $(COMMON_OBJS) $(STREAM_OBJS) $(CORE_LIB) $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) $(SDL_LIBS) $(AV_LIBS) $(PW_LIBS) -lm -everlive_userd: everlive_userd.o $(COMMON_OBJS) +everlive_userd: everlive_userd.o $(COMMON_OBJS) $(CORE_LIB) $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) -gc573_user_diag: gc573_user_diag.o $(COMMON_OBJS) +gc573_user_diag: gc573_user_diag.o $(COMMON_OBJS) $(CORE_LIB) $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) # Headless smoke test for the console cadence detector: a synthetic @@ -43,14 +44,14 @@ gc573_user_diag: gc573_user_diag.o $(COMMON_OBJS) gc573_framegen_selftest: test_framegen.o gc573_user_framegen.o $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) -lm -third_party/jsmn.o: third_party/jsmn.c third_party/jsmn.h - $(CC) $(CFLAGS) -c -o $@ $< +$(CORE_LIB): + $(MAKE) -C ../libeverlive-core selftest: gc573_framegen_selftest ./gc573_framegen_selftest %.o: %.c $(HEADERS) - $(CC) $(CFLAGS) $(SDL_CFLAGS) $(AV_CFLAGS) $(PW_CFLAGS) -c -o $@ $< + $(CC) $(CFLAGS) -I../libeverlive-core $(SDL_CFLAGS) $(AV_CFLAGS) $(PW_CFLAGS) -c -o $@ $< clean: rm -f *.o $(TARGETS) gc573_framegen_selftest diff --git a/tools/opengc573-userd/gc573_user_ipc.c b/tools/opengc573-userd/gc573_user_ipc.c index 85ab000..fbb869c 100644 --- a/tools/opengc573-userd/gc573_user_ipc.c +++ b/tools/opengc573-userd/gc573_user_ipc.c @@ -25,10 +25,8 @@ #include #include -/* jsmn JSON parser - single header library (MIT licensed) */ -#define JSMN_HEADER -#define JSMN_PARENT_LINKS -#include "third_party/jsmn.h" +/* JSON-RPC scanning, escaping, and id formatting live in libeverlive-core. */ +#include "everlive_json.h" /* * Control socket name shared by the engine and the Console Hub. @@ -122,238 +120,6 @@ static uint64_t ipc_now_ms(void) return (uint64_t)ts.tv_sec * 1000U + (uint64_t)(ts.tv_nsec / 1000000L); } -/* JSON parsing with jsmn - limits: 128 tokens, max depth 16, max input 16KB */ -#define IPC_JSON_MAX_TOKENS 128 -#define IPC_JSON_MAX_DEPTH 16 -#define IPC_JSON_MAX_INPUT (16 * 1024) - -typedef enum { - JSON_GET_STRING, - JSON_GET_LONG, - JSON_GET_DOUBLE, - JSON_GET_BOOL, - JSON_GET_RAW -} json_get_type_e; - -/* Find a key in the JSON token tree. Direct children of root are matched first. */ -static int json_token_find_key(const jsmntok_t *tokens, int tok_count, - const char *key, const char *json) -{ - const size_t klen = strlen(key); - - /* Pass 1: check direct keys of the root object (tokens[0]) */ - for (int i = 1; i < tok_count; i++) { - if (tokens[i].type != JSMN_STRING || tokens[i].parent != 0) - continue; - size_t len = (size_t)(tokens[i].end - tokens[i].start); - if (len == klen && memcmp(json + tokens[i].start, key, klen) == 0) { - if (i + 1 < tok_count) - return i + 1; - } - } - - /* Pass 2: check keys of nested objects (e.g. inside "params") */ - for (int i = 1; i < tok_count; i++) { - if (tokens[i].type != JSMN_STRING) - continue; - if (tokens[i].parent <= 0 || tokens[tokens[i].parent].type != JSMN_OBJECT) - continue; - size_t len = (size_t)(tokens[i].end - tokens[i].start); - if (len == klen && memcmp(json + tokens[i].start, key, klen) == 0) { - if (i + 1 < tok_count) - return i + 1; - } - } - - return -1; -} - -static bool json_token_to_string(const jsmntok_t *tok, const char *json, - char *out, size_t outsz) -{ - if (!tok || tok->type != JSMN_STRING) - return false; - int len = tok->end - tok->start; - if (len < 0 || (size_t)len >= outsz) - return false; - memcpy(out, json + tok->start, len); - out[len] = '\0'; - return true; -} - -static bool json_token_to_long(const jsmntok_t *tok, const char *json, - long *out) -{ - if (!tok || tok->type != JSMN_PRIMITIVE) - return false; - char buf[64]; - int len = tok->end - tok->start; - if (len < 0 || len >= (int)sizeof(buf)) - return false; - memcpy(buf, json + tok->start, len); - buf[len] = '\0'; - char *end; - errno = 0; - long val = strtol(buf, &end, 10); - if (errno || end == buf || *end != '\0') - return false; - *out = val; - return true; -} - -static bool json_token_to_double(const jsmntok_t *tok, const char *json, - double *out) -{ - if (!tok || tok->type != JSMN_PRIMITIVE) - return false; - char buf[64]; - int len = tok->end - tok->start; - if (len < 0 || len >= (int)sizeof(buf)) - return false; - memcpy(buf, json + tok->start, len); - buf[len] = '\0'; - char *end; - errno = 0; - double val = strtod(buf, &end); - if (errno || end == buf || *end != '\0') - return false; - *out = val; - return true; -} - -static bool json_token_to_bool(const jsmntok_t *tok, const char *json, - bool *out) -{ - if (!tok || tok->type != JSMN_PRIMITIVE) - return false; - int len = tok->end - tok->start; - if (len == 4 && strncmp(json + tok->start, "true", 4) == 0) { - *out = true; - return true; - } - if (len == 5 && strncmp(json + tok->start, "false", 5) == 0) { - *out = false; - return true; - } - return false; -} - -static bool json_token_to_raw(const jsmntok_t *tok, const char *json, - char *out, size_t outsz) -{ - if (!tok || !json || !out || outsz == 0) - return false; - int len = tok->end - tok->start; - if (len < 0 || (size_t)len >= outsz) - return false; - memcpy(out, json + tok->start, len); - out[len] = '\0'; - return true; -} - -static bool json_parse_request(const char *input, size_t len, - const char *key, char *out, size_t outsz, - json_get_type_e type, void *num_out) -{ - if (len >= IPC_JSON_MAX_INPUT) - return false; - - jsmn_parser parser; - jsmntok_t tokens[IPC_JSON_MAX_TOKENS]; - jsmn_init(&parser); - int tok_count = jsmn_parse(&parser, input, len, tokens, IPC_JSON_MAX_TOKENS); - if (tok_count < 1 || tokens[0].type != JSMN_OBJECT) - return false; - - /* Search for the key anywhere in the JSON token tree */ - int val_idx = json_token_find_key(tokens, tok_count, key, input); - if (val_idx < 0) - return false; - - const jsmntok_t *val = &tokens[val_idx]; - switch (type) { - case JSON_GET_STRING: - return json_token_to_string(val, input, out, outsz); - case JSON_GET_LONG: - return json_token_to_long(val, input, (long *)num_out); - case JSON_GET_DOUBLE: - return json_token_to_double(val, input, (double *)num_out); - case JSON_GET_BOOL: - return json_token_to_bool(val, input, (bool *)num_out); - case JSON_GET_RAW: - return json_token_to_raw(val, input, out, outsz); - } - return false; -} - -static bool json_get_string(const char *s, const char *key, char *out, size_t outsz) -{ - if (!s || !key || !out || !outsz) - return false; - return json_parse_request(s, strlen(s), key, out, outsz, JSON_GET_STRING, NULL); -} - -static bool json_get_long(const char *s, const char *key, long *out) -{ - if (!s || !key || !out) - return false; - return json_parse_request(s, strlen(s), key, NULL, 0, JSON_GET_LONG, out); -} - -static bool json_get_double(const char *s, const char *key, double *out) -{ - if (!s || !key || !out) - return false; - return json_parse_request(s, strlen(s), key, NULL, 0, JSON_GET_DOUBLE, out); -} - -static bool json_get_bool(const char *s, const char *key, bool *out) -{ - if (!s || !key || !out) - return false; - return json_parse_request(s, strlen(s), key, NULL, 0, JSON_GET_BOOL, out); -} - -/* Legacy wrapper for json_find_value (used in ipc_handle_line for "id" and "method") */ -static bool json_find_value(const char *s, const char *key, char *out, size_t outsz) -{ - if (!s || !key || !out || !outsz) - return false; - /* For "method", extract unquoted string */ - if (strcmp(key, "method") == 0) { - return json_get_string(s, key, out, outsz); - } - /* For "id", return raw token (string value, number, or null) */ - return json_parse_request(s, strlen(s), key, out, outsz, JSON_GET_RAW, NULL); -} - -static void json_escape_into(char *dst, size_t dstsz, const char *src) -{ - size_t o = 0; - - if (!dst || dstsz == 0) - return; - if (!src) { - dst[0] = '\0'; - return; - } - while (src && *src && o + 2 < dstsz) { - unsigned char ch = (unsigned char)*src++; - - if (ch == '"' || ch == '\\') { - dst[o++] = '\\'; - dst[o++] = (char)ch; - } else if (ch < 0x20) { - if (o + 6 >= dstsz) - break; - o += (size_t)snprintf(dst + o, dstsz - o, "\\u%04x", ch); - } else { - dst[o++] = (char)ch; - } - } - dst[o] = '\0'; -} - /* ------------------------------------------------------------------ */ /* Wire I/O */ /* ------------------------------------------------------------------ */ @@ -373,49 +139,13 @@ static void ipc_send_raw(struct ipc_client *c, const char *data, size_t len) } } -static void json_format_id(char *dst, size_t dstsz, const char *id) -{ - if (!dst || dstsz == 0) - return; - if (!id || !*id || strcmp(id, "null") == 0) { - snprintf(dst, dstsz, "null"); - return; - } - char *end = NULL; - strtol(id, &end, 10); - if (end && *end == '\0') { - snprintf(dst, dstsz, "%s", id); - return; - } - size_t len = strlen(id); - char inner[IPC_ID_MAX]; - const char *to_esc = id; - if (id[0] == '"' && len >= 2 && id[len - 1] == '"') { - size_t in_len = len - 2; - if (in_len >= sizeof(inner)) - in_len = sizeof(inner) - 1; - memcpy(inner, id + 1, in_len); - inner[in_len] = '\0'; - to_esc = inner; - } - if (dstsz >= 3) { - dst[0] = '"'; - json_escape_into(dst + 1, dstsz - 2, to_esc); - size_t elen = strlen(dst); - dst[elen] = '"'; - dst[elen + 1] = '\0'; - } else { - snprintf(dst, dstsz, "null"); - } -} - static void ipc_reply(struct ipc_client *c, const char *id, const char *result_json) { char esc_id[IPC_ID_MAX * 2]; char buf[IPC_REPLY_MAX]; - json_format_id(esc_id, sizeof(esc_id), id); + everlive_json_format_id(esc_id, sizeof(esc_id), id); int n = snprintf(buf, sizeof(buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"result\":%s}\n", esc_id, result_json); @@ -431,8 +161,8 @@ static void ipc_error(struct ipc_client *c, const char *id, int code, char esc_msg[256]; char buf[512]; - json_format_id(esc_id, sizeof(esc_id), id); - json_escape_into(esc_msg, sizeof(esc_msg), message ? message : "error"); + everlive_json_format_id(esc_id, sizeof(esc_id), id); + everlive_json_escape_into(esc_msg, sizeof(esc_msg), message ? message : "error"); int n = snprintf(buf, sizeof(buf), "{\"jsonrpc\":\"2.0\",\"id\":%s," "\"error\":{\"code\":%d,\"message\":\"%s\"}}\n", @@ -527,13 +257,13 @@ static void ipc_send_status(struct ipc_server *s, struct ipc_client *c, ipc_snapshot_read(&st); age_ms = st.timestamp_ms ? ipc_now_ms() - st.timestamp_ms : 0; - json_escape_into(path_esc, sizeof(path_esc), st.v4l2_path); - json_escape_into(label_esc, sizeof(label_esc), st.audio_label); - json_escape_into(pos_esc, sizeof(pos_esc), st.osd_position); - json_escape_into(aspect_esc, sizeof(aspect_esc), st.aspect); - json_escape_into(dll_esc, sizeof(dll_esc), st.framegen_dll_path); - json_escape_into(scale_esc, sizeof(scale_esc), st.scale); - json_escape_into(layout_esc, sizeof(layout_esc), st.audio_layout); + everlive_json_escape_into(path_esc, sizeof(path_esc), st.v4l2_path); + everlive_json_escape_into(label_esc, sizeof(label_esc), st.audio_label); + everlive_json_escape_into(pos_esc, sizeof(pos_esc), st.osd_position); + everlive_json_escape_into(aspect_esc, sizeof(aspect_esc), st.aspect); + everlive_json_escape_into(dll_esc, sizeof(dll_esc), st.framegen_dll_path); + everlive_json_escape_into(scale_esc, sizeof(scale_esc), st.scale); + everlive_json_escape_into(layout_esc, sizeof(layout_esc), st.audio_layout); n = snprintf(result, sizeof(result), "{\"proto\":%d,\"version\":\"%s\",\"pid\":%ld," @@ -611,7 +341,7 @@ static void ipc_send_hello(struct ipc_server *s, struct ipc_client *c, char path_esc[128]; int n; - json_escape_into(path_esc, sizeof(path_esc), s->path); + everlive_json_escape_into(path_esc, sizeof(path_esc), s->path); n = snprintf(result, sizeof(result), "{\"proto\":%d,\"app\":\"everlive\",\"version\":\"%s\"," "\"pid\":%ld,\"socket\":\"%s\"," @@ -707,8 +437,8 @@ static void ipc_handle_led_set(struct ipc_client *c, const char *id, struct gc573_ipc_cmd cmd = { .kind = GC573_IPC_CMD_LED_SET }; long r, g, b; - if (!json_get_long(line, "r", &r) || !json_get_long(line, "g", &g) || - !json_get_long(line, "b", &b)) { + if (!everlive_json_get_long(line, "r", &r) || !everlive_json_get_long(line, "g", &g) || + !everlive_json_get_long(line, "b", &b)) { ipc_error(c, id, -32602, "invalid params: r, g, b required"); return; } @@ -726,7 +456,7 @@ static void ipc_handle_led_mode(struct ipc_client *c, const char *id, char mode[24]; int code; - if (!json_get_string(line, "mode", mode, sizeof(mode))) { + if (!everlive_json_get_string(line, "mode", mode, sizeof(mode))) { ipc_error(c, id, -32602, "invalid params: mode required"); return; } @@ -748,7 +478,7 @@ static void ipc_handle_audio_gain(struct ipc_client *c, const char *id, struct gc573_ipc_cmd cmd = { .kind = GC573_IPC_CMD_AUDIO_GAIN }; double gain; - if (!json_get_double(line, "gain", &gain)) { + if (!everlive_json_get_double(line, "gain", &gain)) { ipc_error(c, id, -32602, "invalid params: gain required"); return; } @@ -768,7 +498,7 @@ static void ipc_handle_audio_mute(struct ipc_client *c, const char *id, struct gc573_ipc_cmd cmd = { .kind = GC573_IPC_CMD_AUDIO_MUTE }; bool muted; - if (!json_get_bool(line, "muted", &muted)) { + if (!everlive_json_get_bool(line, "muted", &muted)) { ipc_error(c, id, -32602, "invalid params: muted required"); return; } @@ -784,7 +514,7 @@ static void ipc_handle_audio_channels(struct ipc_client *c, const char *id, char mode[16]; const char *canonical; - if (!json_get_string(line, "mode", mode, sizeof(mode))) { + if (!everlive_json_get_string(line, "mode", mode, sizeof(mode))) { ipc_error(c, id, -32602, "invalid params: mode required"); return; } @@ -805,7 +535,7 @@ static void ipc_handle_osd_enable(struct ipc_client *c, const char *id, struct gc573_ipc_cmd cmd = { .kind = GC573_IPC_CMD_OSD_ENABLE }; bool enabled; - if (!json_get_bool(line, "enabled", &enabled)) { + if (!everlive_json_get_bool(line, "enabled", &enabled)) { ipc_error(c, id, -32602, "invalid params: enabled required"); return; } @@ -821,7 +551,7 @@ static void ipc_handle_osd_position(struct ipc_client *c, const char *id, char pos[24]; int code; - if (!json_get_string(line, "pos", pos, sizeof(pos))) { + if (!everlive_json_get_string(line, "pos", pos, sizeof(pos))) { ipc_error(c, id, -32602, "invalid params: pos required"); return; } @@ -843,7 +573,7 @@ static void ipc_handle_aspect(struct ipc_client *c, const char *id, char mode[24]; int code; - if (!json_get_string(line, "mode", mode, sizeof(mode))) { + if (!everlive_json_get_string(line, "mode", mode, sizeof(mode))) { ipc_error(c, id, -32602, "invalid params: mode required"); return; } @@ -865,7 +595,7 @@ static void ipc_handle_scale(struct ipc_client *c, const char *id, struct gc573_ipc_cmd cmd = { .kind = GC573_IPC_CMD_SCALE }; char mode[16]; - if (!json_get_string(line, "mode", mode, sizeof(mode))) { + if (!everlive_json_get_string(line, "mode", mode, sizeof(mode))) { ipc_error(c, id, -32602, "invalid params: mode required"); return; } @@ -892,7 +622,7 @@ static void ipc_handle_hdr(struct ipc_client *c, const char *id, cmd.f = -999.0f; /* -999 means leave current exposure unchanged */ - if (!json_get_string(line, "mode", mode, sizeof(mode))) { + if (!everlive_json_get_string(line, "mode", mode, sizeof(mode))) { ipc_error(c, id, -32602, "invalid params: mode required"); return; } @@ -909,10 +639,10 @@ static void ipc_handle_hdr(struct ipc_client *c, const char *id, return; } - if (json_get_bool(line, "passthrough", &passthrough)) + if (everlive_json_get_bool(line, "passthrough", &passthrough)) cmd.b = passthrough ? 1 : 0; - if (json_get_double(line, "exposure", &exposure)) { + if (everlive_json_get_double(line, "exposure", &exposure)) { if (exposure < -2.0) exposure = -2.0; if (exposure > 2.0) @@ -931,10 +661,10 @@ static void ipc_handle_hdr_passthrough(struct ipc_client *c, const char *id, cmd.f = -999.0f; /* leave exposure unchanged */ - if (!json_get_bool(line, "enabled", &enable) && - !json_get_bool(line, "passthrough", &enable)) { + if (!everlive_json_get_bool(line, "enabled", &enable) && + !everlive_json_get_bool(line, "passthrough", &enable)) { char mode[16]; - if (json_get_string(line, "mode", mode, sizeof(mode))) + if (everlive_json_get_string(line, "mode", mode, sizeof(mode))) enable = (!strcasecmp(mode, "on") || !strcasecmp(mode, "true") || !strcasecmp(mode, "passthrough")); } @@ -958,7 +688,7 @@ static void ipc_handle_framegen(struct ipc_client *c, const char *id, cmd.c = -1; cmd.f = -1.0f; - if (json_get_string(line, "mode", mode, sizeof(mode))) { + if (everlive_json_get_string(line, "mode", mode, sizeof(mode))) { if (!strcasecmp(mode, "off") || !strcasecmp(mode, "none")) cmd.a = GC573_IPC_FG_OFF; else if (!strcasecmp(mode, "60to120") || @@ -975,16 +705,16 @@ static void ipc_handle_framegen(struct ipc_client *c, const char *id, } } - if (json_get_double(line, "flow_scale", &scale)) { + if (everlive_json_get_double(line, "flow_scale", &scale)) { if (scale < 0.5) scale = 0.5; if (scale > 1.0) scale = 1.0; cmd.f = (float)scale; } - if (json_get_bool(line, "perf_mode", &flag)) + if (everlive_json_get_bool(line, "perf_mode", &flag)) cmd.b = flag ? 1 : 0; - if (json_get_bool(line, "low_latency", &flag)) + if (everlive_json_get_bool(line, "low_latency", &flag)) cmd.c = flag ? 1 : 0; if (cmd.a < 0 && cmd.b < 0 && cmd.c < 0 && cmd.f < 0.0f) { @@ -1002,7 +732,7 @@ static void ipc_handle_mode(struct ipc_client *c, const char *id, struct gc573_ipc_cmd cmd = { .kind = GC573_IPC_CMD_MODE }; char mode[24]; - if (!json_get_string(line, "mode", mode, sizeof(mode))) { + if (!everlive_json_get_string(line, "mode", mode, sizeof(mode))) { ipc_error(c, id, -32602, "invalid params: mode required"); return; } @@ -1027,14 +757,14 @@ static void ipc_handle_v4l2(struct ipc_client *c, const char *id, char device[64]; bool enabled; - if (!json_get_bool(line, "enabled", &enabled)) { + if (!everlive_json_get_bool(line, "enabled", &enabled)) { ipc_error(c, id, -32602, "invalid params: enabled required"); return; } cmd.a = enabled ? 1 : 0; - if (json_get_string(line, "device", device, sizeof(device))) { + if (everlive_json_get_string(line, "device", device, sizeof(device))) { /* Only character devices; allowlist /dev/video[0-9]+ */ if (strncmp(device, "/dev/video", 10) != 0) { ipc_error(c, id, -32602, "device must match /dev/video[0-9]+"); @@ -1058,8 +788,8 @@ static void ipc_handle_edid_set(struct ipc_client *c, const char *id, struct gc573_ipc_cmd cmd = { .kind = GC573_IPC_CMD_EDID }; char name[64]; - if (!json_get_string(line, "profile", name, sizeof(name)) && - !json_get_string(line, "name", name, sizeof(name))) { + if (!everlive_json_get_string(line, "profile", name, sizeof(name)) && + !everlive_json_get_string(line, "name", name, sizeof(name))) { ipc_error(c, id, -32602, "invalid params: profile required"); return; } @@ -1079,9 +809,9 @@ static void ipc_handle_hotplug(struct ipc_client *c, const char *id, bool pulse = true; char action[16]; - if (json_get_bool(line, "pulse", &pulse)) + if (everlive_json_get_bool(line, "pulse", &pulse)) cmd.a = pulse ? 1 : 0; - if (json_get_string(line, "action", action, sizeof(action))) + if (everlive_json_get_string(line, "action", action, sizeof(action))) cmd.a = (strcasecmp(action, "off") != 0 && strcasecmp(action, "disable") != 0 && strcasecmp(action, "0") != 0); @@ -1095,8 +825,8 @@ static void ipc_handle_hdr_exposure(struct ipc_client *c, const char *id, struct gc573_ipc_cmd cmd = { .kind = GC573_IPC_CMD_HDR_EV }; double exposure = 0.0; - if (!json_get_double(line, "exposure", &exposure) && - !json_get_double(line, "ev", &exposure)) { + if (!everlive_json_get_double(line, "exposure", &exposure) && + !everlive_json_get_double(line, "ev", &exposure)) { ipc_error(c, id, -32602, "invalid params: exposure required"); return; } @@ -1115,8 +845,8 @@ static void ipc_handle_toast_show(struct ipc_client *c, const char *id, struct gc573_ipc_cmd cmd = { .kind = GC573_IPC_CMD_TOAST }; char msg[64] = { 0 }; - if (json_get_string(line, "message", msg, sizeof(msg)) || - json_get_string(line, "text", msg, sizeof(msg))) { + if (everlive_json_get_string(line, "message", msg, sizeof(msg)) || + everlive_json_get_string(line, "text", msg, sizeof(msg))) { snprintf(cmd.path, sizeof(cmd.path), "%s", msg); } else { snprintf(cmd.path, sizeof(cmd.path), "Notification"); @@ -1130,10 +860,10 @@ static void ipc_handle_line(struct ipc_server *s, struct ipc_client *c, { char id[IPC_ID_MAX]; char method[IPC_METHOD_MAX]; - bool has_id = json_find_value(line, "id", id, sizeof(id)) && + bool has_id = everlive_json_find_value(line, "id", id, sizeof(id)) && strcmp(id, "null") != 0; - if (!json_get_string(line, "method", method, sizeof(method))) { + if (!everlive_json_get_string(line, "method", method, sizeof(method))) { if (has_id) ipc_error(c, id, -32700, "parse error: missing method"); return; From 1f7b4507651b445663b1a6bee464b0422d57bd30 Mon Sep 17 00:00:00 2001 From: PolygonPrincess Date: Wed, 11 Nov 2026 04:16:28 -0500 Subject: [PATCH 4/8] refactor(gc573): move sources to src/ and link libeverlive-core Part 2/5 of the modularization plan (see .opencode/plan/everlive-modularization/02-opengc573-userd.md). Mirror the GC570D layout, stop compiling every translation unit with SDL/FFmpeg/PipeWire flags, and replace the private PCI scan and socket path logic with the core helpers. - Move all GC573 sources and headers into tools/opengc573-userd/src/. Binaries stay in the module root so play.sh, install.sh, uninstall.sh and the systemd units keep their paths. - Move test_framegen.c to tests/ (the build rule follows it). - Rewrite the Makefile around incremental objects and three flag groups: hardware/base, capture pipeline (FFmpeg + PipeWire), and the SDL2 renderer. probe, everlive_userd and diag now build without SDL2, FFmpeg or PipeWire installed. - Link every binary against ../libeverlive-core/libeverlive_core.a. - Replace the hand-rolled /sys/bus/pci/devices scan with everlive_detect_installed_cards() and accept only "gc573" descriptors. This fixes a latent bug where a GC570D (1461:0054 with subsystem 0x5700) could be claimed by the GC573 engine. - Resolve the control socket with everlive_ipc_prepare_socket_path(): --ipc-socket, EVERLIVE_SOCKET, the legacy EVERLIVE_CONTROL_SOCKET, then $XDG_RUNTIME_DIR -> /run/user/ -> /tmp/everlive-. - Generate header dependencies with -MMD -MP and clean src/*.o, src/*.d, the binaries and the framegen selftest. - tests: add the test_framegen target and include it in `make test`; keep the forwarding `test`/`selftest` targets. - Update the module .gitignore and README for the src/ layout and the core-library dependency. Verified: GCC and Clang builds are warning-free; the four binaries build; probe/everlive_userd/diag build with PKG_CONFIG disabled; all 7 test suites pass (164 assertions); `make -q` reports an up-to-date tree. --- .gitignore | 3 + tests/Makefile | 8 +- .../opengc573-userd => tests}/test_framegen.c | 0 tools/opengc573-userd/.gitignore | 3 +- tools/opengc573-userd/Makefile | 123 ++++++++++++------ tools/opengc573-userd/README.md | 14 +- .../{ => src}/everlive_userd.c | 0 .../{ => src}/gc573-user-compat.h | 0 .../{ => src}/gc573_user_audio.c | 0 .../{ => src}/gc573_user_audio.h | 0 .../{ => src}/gc573_user_audio_tap.c | 0 .../{ => src}/gc573_user_audio_tap.h | 0 .../{ => src}/gc573_user_bitstream.c | 0 .../{ => src}/gc573_user_bitstream.h | 0 .../{ => src}/gc573_user_diag.c | 0 .../{ => src}/gc573_user_edid.c | 0 .../{ => src}/gc573_user_edid.h | 0 .../{ => src}/gc573_user_font8x8.h | 0 .../{ => src}/gc573_user_fpga.c | 0 .../{ => src}/gc573_user_fpga.h | 0 .../{ => src}/gc573_user_framegen.c | 0 .../{ => src}/gc573_user_framegen.h | 0 .../{ => src}/gc573_user_hdr.c | 0 .../{ => src}/gc573_user_hdr.h | 0 .../opengc573-userd/{ => src}/gc573_user_hw.c | 0 .../opengc573-userd/{ => src}/gc573_user_hw.h | 0 .../{ => src}/gc573_user_i2c.c | 0 .../{ => src}/gc573_user_i2c.h | 0 .../{ => src}/gc573_user_ipc.c | 51 ++------ .../{ => src}/gc573_user_ipc.h | 0 .../{ => src}/gc573_user_it6664.c | 0 .../{ => src}/gc573_user_it6664.h | 0 .../{ => src}/gc573_user_it6805.c | 0 .../{ => src}/gc573_user_it6805.h | 0 .../{ => src}/gc573_user_log.c | 0 .../{ => src}/gc573_user_log.h | 0 .../{ => src}/gc573_user_nosignal.c | 0 .../{ => src}/gc573_user_nosignal.h | 0 .../{ => src}/gc573_user_probe.c | 0 .../{ => src}/gc573_user_rx_eq.c | 0 .../{ => src}/gc573_user_rx_eq.h | 0 .../{ => src}/gc573_user_streamer.c | 0 .../{ => src}/gc573_user_video.c | 0 .../{ => src}/gc573_user_video.h | 0 tools/opengc573-userd/{ => src}/gc573_vfio.c | 79 +++-------- tools/opengc573-userd/{ => src}/gc573_vfio.h | 0 46 files changed, 139 insertions(+), 142 deletions(-) rename {tools/opengc573-userd => tests}/test_framegen.c (100%) rename tools/opengc573-userd/{ => src}/everlive_userd.c (100%) rename tools/opengc573-userd/{ => src}/gc573-user-compat.h (100%) rename tools/opengc573-userd/{ => src}/gc573_user_audio.c (100%) rename tools/opengc573-userd/{ => src}/gc573_user_audio.h (100%) rename tools/opengc573-userd/{ => src}/gc573_user_audio_tap.c (100%) rename tools/opengc573-userd/{ => src}/gc573_user_audio_tap.h (100%) rename tools/opengc573-userd/{ => src}/gc573_user_bitstream.c (100%) rename tools/opengc573-userd/{ => src}/gc573_user_bitstream.h (100%) rename tools/opengc573-userd/{ => src}/gc573_user_diag.c (100%) rename tools/opengc573-userd/{ => src}/gc573_user_edid.c (100%) rename tools/opengc573-userd/{ => src}/gc573_user_edid.h (100%) rename tools/opengc573-userd/{ => src}/gc573_user_font8x8.h (100%) rename tools/opengc573-userd/{ => src}/gc573_user_fpga.c (100%) rename tools/opengc573-userd/{ => src}/gc573_user_fpga.h (100%) rename tools/opengc573-userd/{ => src}/gc573_user_framegen.c (100%) rename tools/opengc573-userd/{ => src}/gc573_user_framegen.h (100%) rename tools/opengc573-userd/{ => src}/gc573_user_hdr.c (100%) rename tools/opengc573-userd/{ => src}/gc573_user_hdr.h (100%) rename tools/opengc573-userd/{ => src}/gc573_user_hw.c (100%) rename tools/opengc573-userd/{ => src}/gc573_user_hw.h (100%) rename tools/opengc573-userd/{ => src}/gc573_user_i2c.c (100%) rename tools/opengc573-userd/{ => src}/gc573_user_i2c.h (100%) rename tools/opengc573-userd/{ => src}/gc573_user_ipc.c (96%) rename tools/opengc573-userd/{ => src}/gc573_user_ipc.h (100%) rename tools/opengc573-userd/{ => src}/gc573_user_it6664.c (100%) rename tools/opengc573-userd/{ => src}/gc573_user_it6664.h (100%) rename tools/opengc573-userd/{ => src}/gc573_user_it6805.c (100%) rename tools/opengc573-userd/{ => src}/gc573_user_it6805.h (100%) rename tools/opengc573-userd/{ => src}/gc573_user_log.c (100%) rename tools/opengc573-userd/{ => src}/gc573_user_log.h (100%) rename tools/opengc573-userd/{ => src}/gc573_user_nosignal.c (100%) rename tools/opengc573-userd/{ => src}/gc573_user_nosignal.h (100%) rename tools/opengc573-userd/{ => src}/gc573_user_probe.c (100%) rename tools/opengc573-userd/{ => src}/gc573_user_rx_eq.c (100%) rename tools/opengc573-userd/{ => src}/gc573_user_rx_eq.h (100%) rename tools/opengc573-userd/{ => src}/gc573_user_streamer.c (100%) rename tools/opengc573-userd/{ => src}/gc573_user_video.c (100%) rename tools/opengc573-userd/{ => src}/gc573_user_video.h (100%) rename tools/opengc573-userd/{ => src}/gc573_vfio.c (86%) rename tools/opengc573-userd/{ => src}/gc573_vfio.h (100%) diff --git a/.gitignore b/.gitignore index 335183e..1155a4a 100644 --- a/.gitignore +++ b/.gitignore @@ -66,3 +66,6 @@ refactor/ # Chronik / Notizen crhonic.md chronic.md + +# Framegen selftest (lives in tests/ after the source move) +tests/test_framegen diff --git a/tests/Makefile b/tests/Makefile index e52e0da..a93fdeb 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -1,7 +1,7 @@ # SPDX-License-Identifier: GPL-2.0-only CC ?= gcc CFLAGS ?= -O2 -g -Wall -Wextra -Wno-unused-parameter -Wno-sign-compare -std=gnu11 -D_GNU_SOURCE -INCLUDES := -I../tools/opengc570d-userd/src -I../tools/opengc570d-userd -I../tools/libeverlive-core -I../tools/opengc573-userd +INCLUDES := -I../tools/opengc570d-userd/src -I../tools/opengc570d-userd -I../tools/libeverlive-core -I../tools/opengc573-userd/src DEFS := -DGC570D_USERSPACE GC570D_DIR := ../tools/opengc570d-userd/src @@ -9,7 +9,7 @@ CORE_DIR := ../tools/libeverlive-core GC573_DIR := ../tools/opengc573-userd CORE_LIB := $(CORE_DIR)/libeverlive_core.a -TARGETS := test_edid test_config test_card_registry test_frame_convert test_ctl_proto test_ipc_json +TARGETS := test_edid test_config test_card_registry test_frame_convert test_ctl_proto test_ipc_json test_framegen all: $(TARGETS) @@ -37,6 +37,9 @@ test_ipc_json: test_ipc_json.c $(CORE_LIB) $(CORE_LIB): $(MAKE) -C $(CORE_DIR) +test_framegen: test_framegen.c $(GC573_DIR)/src/gc573_user_framegen.c + $(CC) $(CFLAGS) $(INCLUDES) -o $@ $^ -pthread -lm + test: all @echo "Running GC570D & GC573 unit tests..." @./test_edid @@ -45,6 +48,7 @@ test: all @./test_frame_convert @./test_ctl_proto @./test_ipc_json + @./test_framegen clean: rm -f $(TARGETS) diff --git a/tools/opengc573-userd/test_framegen.c b/tests/test_framegen.c similarity index 100% rename from tools/opengc573-userd/test_framegen.c rename to tests/test_framegen.c diff --git a/tools/opengc573-userd/.gitignore b/tools/opengc573-userd/.gitignore index 259caa0..4c18867 100644 --- a/tools/opengc573-userd/.gitignore +++ b/tools/opengc573-userd/.gitignore @@ -1,4 +1,5 @@ -*.o +src/*.o +src/*.d gc573_user_probe gc573_user_streamer gc573_user_diag diff --git a/tools/opengc573-userd/Makefile b/tools/opengc573-userd/Makefile index ab7a6a0..11be2dd 100644 --- a/tools/opengc573-userd/Makefile +++ b/tools/opengc573-userd/Makefile @@ -1,59 +1,104 @@ # SPDX-License-Identifier: GPL-2.0-only -# Makefile for OpenGC573 Userspace Driver Tools & EverLive User Daemon +# +# EverLive GC573 (Live Gamer 4K) userspace VFIO engine. +# +# Sources live in src/. Binaries stay in this directory: play.sh, install.sh, +# uninstall.sh and the systemd units resolve them by path. CC ?= gcc -CFLAGS ?= -O2 -Wall -Wextra -pthread -D_GNU_SOURCE +PKG_CONFIG ?= pkg-config +DEPFLAGS := -MMD -MP + +# Flags every translation unit needs. +BASE_CFLAGS := -O2 -g -Wall -Wextra -Wno-unused-parameter -Wno-sign-compare \ + -std=gnu11 -D_GNU_SOURCE +BASE_CFLAGS += -Isrc -I../libeverlive-core LDFLAGS ?= -pthread -SDL_CFLAGS = $(shell pkg-config --cflags sdl2) -SDL_LIBS = $(shell pkg-config --libs sdl2) -# libavcodec/libswresample are only needed by the streamer's bitstream decoder; -# libswscale rescales the V4L2 feed when a reader (OBS) holds a fixed format. -AV_CFLAGS = $(shell pkg-config --cflags libavcodec libavutil libswresample libswscale) -AV_LIBS = $(shell pkg-config --libs libavcodec libavutil libswresample libswscale) -# libpipewire backs the optional "EverLive Capture Audio" tap. -PW_CFLAGS = $(shell pkg-config --cflags libpipewire-0.3) -PW_LIBS = $(shell pkg-config --libs libpipewire-0.3) - -# Frame generation is delegated to the system lsfg-vk Vulkan implicit layer, -# which the SDL2 "vulkan" renderer feeds directly. No GL, no Vulkan headers. - -COMMON_OBJS = gc573_vfio.o gc573_user_i2c.o gc573_user_hw.o gc573_user_edid.o gc573_user_it6805.o gc573_user_rx_eq.o gc573_user_it6664.o -STREAM_OBJS = gc573_user_log.o gc573_user_fpga.o gc573_user_video.o gc573_user_audio.o gc573_user_bitstream.o gc573_user_nosignal.o gc573_user_ipc.o gc573_user_audio_tap.o gc573_user_hdr.o gc573_user_framegen.o -CORE_LIB = ../libeverlive-core/libeverlive_core.a -TARGETS = gc573_user_probe gc573_user_streamer everlive_userd gc573_user_diag -HEADERS = gc573-user-compat.h gc573_user_log.h gc573_vfio.h gc573_user_i2c.h gc573_user_hw.h gc573_user_edid.h gc573_user_it6805.h gc573_user_rx_eq.h gc573_user_it6664.h gc573_user_fpga.h gc573_user_video.h gc573_user_audio.h gc573_user_bitstream.h gc573_user_nosignal.h gc573_user_ipc.h gc573_user_audio_tap.h gc573_user_hdr.h gc573_user_framegen.h -all: $(TARGETS) +SDL_CFLAGS := $(shell $(PKG_CONFIG) --cflags sdl2 2>/dev/null) +SDL_LIBS := $(shell $(PKG_CONFIG) --libs sdl2 2>/dev/null) +AV_CFLAGS := $(shell $(PKG_CONFIG) --cflags libavcodec libavutil libswresample libswscale 2>/dev/null) +AV_LIBS := $(shell $(PKG_CONFIG) --libs libavcodec libavutil libswresample libswscale 2>/dev/null) +PW_CFLAGS := $(shell $(PKG_CONFIG) --cflags libpipewire-0.3 2>/dev/null) +PW_LIBS := $(shell $(PKG_CONFIG) --libs libpipewire-0.3 2>/dev/null) + +CORE_LIB := ../libeverlive-core/libeverlive_core.a + +# Hardware / register layer: no SDL, no FFmpeg, no PipeWire. +HW_OBJS := \ + src/gc573_vfio.o \ + src/gc573_user_i2c.o \ + src/gc573_user_hw.o \ + src/gc573_user_edid.o \ + src/gc573_user_it6805.o \ + src/gc573_user_it6664.o \ + src/gc573_user_rx_eq.o -gc573_user_probe: gc573_user_probe.o $(COMMON_OBJS) $(CORE_LIB) - $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) +# Capture / presentation pipeline. +STREAM_OBJS := \ + src/gc573_user_log.o \ + src/gc573_user_fpga.o \ + src/gc573_user_video.o \ + src/gc573_user_audio.o \ + src/gc573_user_bitstream.o \ + src/gc573_user_nosignal.o \ + src/gc573_user_ipc.o \ + src/gc573_user_audio_tap.o \ + src/gc573_user_hdr.o \ + src/gc573_user_framegen.o -gc573_user_streamer: gc573_user_streamer.o $(COMMON_OBJS) $(STREAM_OBJS) $(CORE_LIB) - $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) $(SDL_LIBS) $(AV_LIBS) $(PW_LIBS) -lm +ENTRY_OBJS := \ + src/gc573_user_probe.o \ + src/gc573_user_streamer.o \ + src/everlive_userd.o \ + src/gc573_user_diag.o + +TARGETS := gc573_user_probe gc573_user_streamer everlive_userd gc573_user_diag + +all: $(TARGETS) -everlive_userd: everlive_userd.o $(COMMON_OBJS) $(CORE_LIB) - $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) +gc573_user_probe: src/gc573_user_probe.o $(HW_OBJS) $(CORE_LIB) + $(CC) $(BASE_CFLAGS) -o $@ $^ $(LDFLAGS) -gc573_user_diag: gc573_user_diag.o $(COMMON_OBJS) $(CORE_LIB) - $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) +gc573_user_streamer: src/gc573_user_streamer.o $(HW_OBJS) $(STREAM_OBJS) $(CORE_LIB) + $(CC) $(BASE_CFLAGS) -o $@ $^ $(LDFLAGS) $(SDL_LIBS) $(AV_LIBS) $(PW_LIBS) -lm -# Headless smoke test for the console cadence detector: a synthetic -# "30 FPS duplicated inside a 60 Hz HDMI container" stream must be recognised -# as 30 unique FPS, including a noisy/dithered variant and a phase slip. -# Pure CPU: the framegen module has no SDL/GL/Vulkan dependency. -gc573_framegen_selftest: test_framegen.o gc573_user_framegen.o - $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) -lm +everlive_userd: src/everlive_userd.o $(HW_OBJS) $(CORE_LIB) + $(CC) $(BASE_CFLAGS) -o $@ $^ $(LDFLAGS) + +gc573_user_diag: src/gc573_user_diag.o $(HW_OBJS) $(CORE_LIB) + $(CC) $(BASE_CFLAGS) -o $@ $^ $(LDFLAGS) $(CORE_LIB): $(MAKE) -C ../libeverlive-core +# Headless/hardware TUs: base flags only. +$(HW_OBJS) src/gc573_user_probe.o src/gc573_user_diag.o src/everlive_userd.o: \ + src/%.o: src/%.c + $(CC) $(BASE_CFLAGS) $(DEPFLAGS) -c -o $@ $< + +# Capture pipeline: FFmpeg libav* and PipeWire headers. +$(STREAM_OBJS): src/%.o: src/%.c + $(CC) $(BASE_CFLAGS) $(AV_CFLAGS) $(PW_CFLAGS) $(DEPFLAGS) -c -o $@ $< + +# Only the SDL2 renderer needs SDL headers. +src/gc573_user_streamer.o: src/gc573_user_streamer.c + $(CC) $(BASE_CFLAGS) $(SDL_CFLAGS) $(AV_CFLAGS) $(PW_CFLAGS) $(DEPFLAGS) -c -o $@ $< + +# Headless framegen self-test. Task 4 moves this fully into tests/. +gc573_framegen_selftest: src/gc573_user_framegen.o ../../tests/test_framegen.c + $(CC) $(BASE_CFLAGS) -o $@ ../../tests/test_framegen.c \ + src/gc573_user_framegen.o $(LDFLAGS) -lm + selftest: gc573_framegen_selftest ./gc573_framegen_selftest -%.o: %.c $(HEADERS) - $(CC) $(CFLAGS) -I../libeverlive-core $(SDL_CFLAGS) $(AV_CFLAGS) $(PW_CFLAGS) -c -o $@ $< +test: all + $(MAKE) -C ../../tests test + +-include $(HW_OBJS:.o=.d) $(STREAM_OBJS:.o=.d) $(ENTRY_OBJS:.o=.d) clean: - rm -f *.o $(TARGETS) gc573_framegen_selftest + rm -f src/*.o src/*.d $(TARGETS) gc573_framegen_selftest -.PHONY: all clean selftest +.PHONY: all test selftest clean diff --git a/tools/opengc573-userd/README.md b/tools/opengc573-userd/README.md index e3164fa..24e758b 100644 --- a/tools/opengc573-userd/README.md +++ b/tools/opengc573-userd/README.md @@ -27,7 +27,7 @@ As of **EverLive v1.0.0**, this userspace engine is the default, primary driver 9. **Virtual Camera & Headless Mode:** Native unscaled YUYV frames can be written to a `v4l2loopback` node (`--v4l2 /dev/video10`) for OBS, Discord or FFmpeg, either alongside the gaming window or with `--headless` (no SDL window at all). The writer probes the node with `VIDIOC_TRY_FMT` before every negotiation: if a reader (OBS) already holds a fixed format, that geometry is adopted and every frame is rescaled into it with `libswscale`, so the picture appears immediately instead of staying black. The native mode is retried every 2 s and picked up automatically as soon as the reader releases the format. 10. **Procedural No-Signal Screen:** - When no HDMI signal is present (console off, cable pulled, signal loss mid-session), the engine renders a generated "NO HDMI SIGNAL" card — dark gradient, subtle grid, crossed-out monitor icon, bitmap-font text — and pushes it at ~30 fps to both the SDL window and the loopback node, so OBS never sees a frozen or black feed. The picture is generated once per geometry (`gc573_user_nosignal.[ch]`), costs no CPU while cached, and is replaced seamlessly by the live DMA feed as soon as the signal locks. + When no HDMI signal is present (console off, cable pulled, signal loss mid-session), the engine renders a generated "NO HDMI SIGNAL" card — dark gradient, subtle grid, crossed-out monitor icon, bitmap-font text — and pushes it at ~30 fps to both the SDL window and the loopback node, so OBS never sees a frozen or black feed. The picture is generated once per geometry (`src/gc573_user_nosignal.[ch]`), costs no CPU while cached, and is replaced seamlessly by the live DMA feed as soon as the signal locks. --- @@ -55,10 +55,22 @@ As of **EverLive v1.0.0**, this userspace engine is the default, primary driver ## Building +Sources live in `src/`; the four binaries are written to this directory so +`play.sh`, `install.sh`, `uninstall.sh` and the systemd units resolve them by +path. + +The engine links `../libeverlive-core/libeverlive_core.a` for card discovery, +JSON-RPC helpers, and control-socket resolution. Build the core first (or let +the engine Makefile do it automatically): + ```bash +make -C tools/libeverlive-core make -C tools/opengc573-userd -j"$(nproc)" ``` +From the repository root, `make` (or `./build.sh`) builds the core and both +engines in the correct order. + --- ## Command Line diff --git a/tools/opengc573-userd/everlive_userd.c b/tools/opengc573-userd/src/everlive_userd.c similarity index 100% rename from tools/opengc573-userd/everlive_userd.c rename to tools/opengc573-userd/src/everlive_userd.c diff --git a/tools/opengc573-userd/gc573-user-compat.h b/tools/opengc573-userd/src/gc573-user-compat.h similarity index 100% rename from tools/opengc573-userd/gc573-user-compat.h rename to tools/opengc573-userd/src/gc573-user-compat.h diff --git a/tools/opengc573-userd/gc573_user_audio.c b/tools/opengc573-userd/src/gc573_user_audio.c similarity index 100% rename from tools/opengc573-userd/gc573_user_audio.c rename to tools/opengc573-userd/src/gc573_user_audio.c diff --git a/tools/opengc573-userd/gc573_user_audio.h b/tools/opengc573-userd/src/gc573_user_audio.h similarity index 100% rename from tools/opengc573-userd/gc573_user_audio.h rename to tools/opengc573-userd/src/gc573_user_audio.h diff --git a/tools/opengc573-userd/gc573_user_audio_tap.c b/tools/opengc573-userd/src/gc573_user_audio_tap.c similarity index 100% rename from tools/opengc573-userd/gc573_user_audio_tap.c rename to tools/opengc573-userd/src/gc573_user_audio_tap.c diff --git a/tools/opengc573-userd/gc573_user_audio_tap.h b/tools/opengc573-userd/src/gc573_user_audio_tap.h similarity index 100% rename from tools/opengc573-userd/gc573_user_audio_tap.h rename to tools/opengc573-userd/src/gc573_user_audio_tap.h diff --git a/tools/opengc573-userd/gc573_user_bitstream.c b/tools/opengc573-userd/src/gc573_user_bitstream.c similarity index 100% rename from tools/opengc573-userd/gc573_user_bitstream.c rename to tools/opengc573-userd/src/gc573_user_bitstream.c diff --git a/tools/opengc573-userd/gc573_user_bitstream.h b/tools/opengc573-userd/src/gc573_user_bitstream.h similarity index 100% rename from tools/opengc573-userd/gc573_user_bitstream.h rename to tools/opengc573-userd/src/gc573_user_bitstream.h diff --git a/tools/opengc573-userd/gc573_user_diag.c b/tools/opengc573-userd/src/gc573_user_diag.c similarity index 100% rename from tools/opengc573-userd/gc573_user_diag.c rename to tools/opengc573-userd/src/gc573_user_diag.c diff --git a/tools/opengc573-userd/gc573_user_edid.c b/tools/opengc573-userd/src/gc573_user_edid.c similarity index 100% rename from tools/opengc573-userd/gc573_user_edid.c rename to tools/opengc573-userd/src/gc573_user_edid.c diff --git a/tools/opengc573-userd/gc573_user_edid.h b/tools/opengc573-userd/src/gc573_user_edid.h similarity index 100% rename from tools/opengc573-userd/gc573_user_edid.h rename to tools/opengc573-userd/src/gc573_user_edid.h diff --git a/tools/opengc573-userd/gc573_user_font8x8.h b/tools/opengc573-userd/src/gc573_user_font8x8.h similarity index 100% rename from tools/opengc573-userd/gc573_user_font8x8.h rename to tools/opengc573-userd/src/gc573_user_font8x8.h diff --git a/tools/opengc573-userd/gc573_user_fpga.c b/tools/opengc573-userd/src/gc573_user_fpga.c similarity index 100% rename from tools/opengc573-userd/gc573_user_fpga.c rename to tools/opengc573-userd/src/gc573_user_fpga.c diff --git a/tools/opengc573-userd/gc573_user_fpga.h b/tools/opengc573-userd/src/gc573_user_fpga.h similarity index 100% rename from tools/opengc573-userd/gc573_user_fpga.h rename to tools/opengc573-userd/src/gc573_user_fpga.h diff --git a/tools/opengc573-userd/gc573_user_framegen.c b/tools/opengc573-userd/src/gc573_user_framegen.c similarity index 100% rename from tools/opengc573-userd/gc573_user_framegen.c rename to tools/opengc573-userd/src/gc573_user_framegen.c diff --git a/tools/opengc573-userd/gc573_user_framegen.h b/tools/opengc573-userd/src/gc573_user_framegen.h similarity index 100% rename from tools/opengc573-userd/gc573_user_framegen.h rename to tools/opengc573-userd/src/gc573_user_framegen.h diff --git a/tools/opengc573-userd/gc573_user_hdr.c b/tools/opengc573-userd/src/gc573_user_hdr.c similarity index 100% rename from tools/opengc573-userd/gc573_user_hdr.c rename to tools/opengc573-userd/src/gc573_user_hdr.c diff --git a/tools/opengc573-userd/gc573_user_hdr.h b/tools/opengc573-userd/src/gc573_user_hdr.h similarity index 100% rename from tools/opengc573-userd/gc573_user_hdr.h rename to tools/opengc573-userd/src/gc573_user_hdr.h diff --git a/tools/opengc573-userd/gc573_user_hw.c b/tools/opengc573-userd/src/gc573_user_hw.c similarity index 100% rename from tools/opengc573-userd/gc573_user_hw.c rename to tools/opengc573-userd/src/gc573_user_hw.c diff --git a/tools/opengc573-userd/gc573_user_hw.h b/tools/opengc573-userd/src/gc573_user_hw.h similarity index 100% rename from tools/opengc573-userd/gc573_user_hw.h rename to tools/opengc573-userd/src/gc573_user_hw.h diff --git a/tools/opengc573-userd/gc573_user_i2c.c b/tools/opengc573-userd/src/gc573_user_i2c.c similarity index 100% rename from tools/opengc573-userd/gc573_user_i2c.c rename to tools/opengc573-userd/src/gc573_user_i2c.c diff --git a/tools/opengc573-userd/gc573_user_i2c.h b/tools/opengc573-userd/src/gc573_user_i2c.h similarity index 100% rename from tools/opengc573-userd/gc573_user_i2c.h rename to tools/opengc573-userd/src/gc573_user_i2c.h diff --git a/tools/opengc573-userd/gc573_user_ipc.c b/tools/opengc573-userd/src/gc573_user_ipc.c similarity index 96% rename from tools/opengc573-userd/gc573_user_ipc.c rename to tools/opengc573-userd/src/gc573_user_ipc.c index fbb869c..2bdc244 100644 --- a/tools/opengc573-userd/gc573_user_ipc.c +++ b/tools/opengc573-userd/src/gc573_user_ipc.c @@ -28,11 +28,9 @@ /* JSON-RPC scanning, escaping, and id formatting live in libeverlive-core. */ #include "everlive_json.h" -/* - * Control socket name shared by the engine and the Console Hub. - */ -#define IPC_DEFAULT_NAME "everlive.sock" -#define IPC_PATH_LIMIT 100U /* stay clear of sun_path[108] */ +/* Control socket name and path resolution live in libeverlive-core. */ +#include "everlive_ipc.h" + #define IPC_REPLY_MAX 3072U #define IPC_METHOD_MAX 48U #define IPC_ID_MAX 48U @@ -1078,11 +1076,8 @@ int gc573_ipc_start(const char *version, const char *socket_override) struct ipc_server *s = &g_ipc; struct sockaddr_un addr; struct epoll_event ev; - const char *dir; const char *stage = "resolve path"; - char candidate[128]; int ret; - int n; if (__atomic_load_n(&s->running, __ATOMIC_ACQUIRE)) return 0; @@ -1097,37 +1092,15 @@ int gc573_ipc_start(const char *version, const char *socket_override) snprintf(s->version, sizeof(s->version), "%s", version && *version ? version : "unknown"); - /* 1. Resolve the socket path. */ - if (socket_override && *socket_override) { - if (strlen(socket_override) >= IPC_PATH_LIMIT) { - ret = -ENAMETOOLONG; - goto fail; - } - snprintf(s->path, sizeof(s->path), "%s", socket_override); - } else { - dir = getenv("XDG_RUNTIME_DIR"); - if (!dir || !*dir || access(dir, W_OK) != 0) { - snprintf(candidate, sizeof(candidate), "/run/user/%u", - (unsigned int)getuid()); - if (access(candidate, W_OK) == 0) { - dir = candidate; - } else { - snprintf(candidate, sizeof(candidate), - "/tmp/everlive-%u", (unsigned int)getuid()); - if (mkdir(candidate, 0700) < 0 && errno != EEXIST) { - ret = -errno; - goto fail; - } - dir = candidate; - } - } - n = snprintf(s->path, sizeof(s->path), "%s/%s", dir, - IPC_DEFAULT_NAME); - if (n < 0 || (size_t)n >= IPC_PATH_LIMIT) { - ret = -ENAMETOOLONG; - goto fail; - } - } + /* + * 1. Resolve the socket path. Precedence: explicit --ipc-socket, + * $EVERLIVE_SOCKET, legacy $EVERLIVE_CONTROL_SOCKET, then the user + * runtime directory. prepare() also creates /tmp/everlive-. + */ + ret = everlive_ipc_prepare_socket_path(s->path, sizeof(s->path), + socket_override); + if (ret < 0) + goto fail; /* 2. Take the lock file — this is the authoritative ownership test. */ stage = "lock file"; diff --git a/tools/opengc573-userd/gc573_user_ipc.h b/tools/opengc573-userd/src/gc573_user_ipc.h similarity index 100% rename from tools/opengc573-userd/gc573_user_ipc.h rename to tools/opengc573-userd/src/gc573_user_ipc.h diff --git a/tools/opengc573-userd/gc573_user_it6664.c b/tools/opengc573-userd/src/gc573_user_it6664.c similarity index 100% rename from tools/opengc573-userd/gc573_user_it6664.c rename to tools/opengc573-userd/src/gc573_user_it6664.c diff --git a/tools/opengc573-userd/gc573_user_it6664.h b/tools/opengc573-userd/src/gc573_user_it6664.h similarity index 100% rename from tools/opengc573-userd/gc573_user_it6664.h rename to tools/opengc573-userd/src/gc573_user_it6664.h diff --git a/tools/opengc573-userd/gc573_user_it6805.c b/tools/opengc573-userd/src/gc573_user_it6805.c similarity index 100% rename from tools/opengc573-userd/gc573_user_it6805.c rename to tools/opengc573-userd/src/gc573_user_it6805.c diff --git a/tools/opengc573-userd/gc573_user_it6805.h b/tools/opengc573-userd/src/gc573_user_it6805.h similarity index 100% rename from tools/opengc573-userd/gc573_user_it6805.h rename to tools/opengc573-userd/src/gc573_user_it6805.h diff --git a/tools/opengc573-userd/gc573_user_log.c b/tools/opengc573-userd/src/gc573_user_log.c similarity index 100% rename from tools/opengc573-userd/gc573_user_log.c rename to tools/opengc573-userd/src/gc573_user_log.c diff --git a/tools/opengc573-userd/gc573_user_log.h b/tools/opengc573-userd/src/gc573_user_log.h similarity index 100% rename from tools/opengc573-userd/gc573_user_log.h rename to tools/opengc573-userd/src/gc573_user_log.h diff --git a/tools/opengc573-userd/gc573_user_nosignal.c b/tools/opengc573-userd/src/gc573_user_nosignal.c similarity index 100% rename from tools/opengc573-userd/gc573_user_nosignal.c rename to tools/opengc573-userd/src/gc573_user_nosignal.c diff --git a/tools/opengc573-userd/gc573_user_nosignal.h b/tools/opengc573-userd/src/gc573_user_nosignal.h similarity index 100% rename from tools/opengc573-userd/gc573_user_nosignal.h rename to tools/opengc573-userd/src/gc573_user_nosignal.h diff --git a/tools/opengc573-userd/gc573_user_probe.c b/tools/opengc573-userd/src/gc573_user_probe.c similarity index 100% rename from tools/opengc573-userd/gc573_user_probe.c rename to tools/opengc573-userd/src/gc573_user_probe.c diff --git a/tools/opengc573-userd/gc573_user_rx_eq.c b/tools/opengc573-userd/src/gc573_user_rx_eq.c similarity index 100% rename from tools/opengc573-userd/gc573_user_rx_eq.c rename to tools/opengc573-userd/src/gc573_user_rx_eq.c diff --git a/tools/opengc573-userd/gc573_user_rx_eq.h b/tools/opengc573-userd/src/gc573_user_rx_eq.h similarity index 100% rename from tools/opengc573-userd/gc573_user_rx_eq.h rename to tools/opengc573-userd/src/gc573_user_rx_eq.h diff --git a/tools/opengc573-userd/gc573_user_streamer.c b/tools/opengc573-userd/src/gc573_user_streamer.c similarity index 100% rename from tools/opengc573-userd/gc573_user_streamer.c rename to tools/opengc573-userd/src/gc573_user_streamer.c diff --git a/tools/opengc573-userd/gc573_user_video.c b/tools/opengc573-userd/src/gc573_user_video.c similarity index 100% rename from tools/opengc573-userd/gc573_user_video.c rename to tools/opengc573-userd/src/gc573_user_video.c diff --git a/tools/opengc573-userd/gc573_user_video.h b/tools/opengc573-userd/src/gc573_user_video.h similarity index 100% rename from tools/opengc573-userd/gc573_user_video.h rename to tools/opengc573-userd/src/gc573_user_video.h diff --git a/tools/opengc573-userd/gc573_vfio.c b/tools/opengc573-userd/src/gc573_vfio.c similarity index 86% rename from tools/opengc573-userd/gc573_vfio.c rename to tools/opengc573-userd/src/gc573_vfio.c index 21ced0b..a0d418a 100644 --- a/tools/opengc573-userd/gc573_vfio.c +++ b/tools/opengc573-userd/src/gc573_vfio.c @@ -4,76 +4,35 @@ */ #include "gc573_vfio.h" -#include +#include "everlive_card.h" #include +/* + * Card discovery lives in libeverlive-core. Only descriptors matched as + * "gc573" are accepted: 1461:0054 with subsystem 0x5700 is a GC570D and + * must never be claimed by this engine, and an unreadable subsystem file + * is treated as "unknown" rather than guessed. + */ static int find_gc573_pci_device(char *out_addr, size_t max_len, int *out_group) { - DIR *dir = opendir("/sys/bus/pci/devices"); - if (!dir) { - pr_err("Failed to open /sys/bus/pci/devices: %s", strerror(errno)); - return -errno; - } + struct everlive_device cards[8]; + int count; + int i; - struct dirent *ent; - int found = 0; + count = everlive_detect_installed_cards(cards, ARRAY_SIZE(cards), NULL); - while ((ent = readdir(dir)) != NULL) { - if (ent->d_name[0] == '.') + for (i = 0; i < count; i++) { + if (strcmp(cards[i].desc->model_slug, "gc573") != 0) continue; - - char path[512]; - snprintf(path, sizeof(path), "/sys/bus/pci/devices/%s/vendor", ent->d_name); - FILE *f = fopen(path, "r"); - if (!f) + if (cards[i].iommu_group < 0) continue; - - char vendor[32] = ""; - if (fgets(vendor, sizeof(vendor), f)) { - fclose(f); - if (strncasecmp(vendor, "0x1461", 6) == 0) { - snprintf(path, sizeof(path), "/sys/bus/pci/devices/%s/device", ent->d_name); - f = fopen(path, "r"); - if (f) { - char dev_id[32] = ""; - if (fgets(dev_id, sizeof(dev_id), f)) { - fclose(f); - /* 0x0054 = GC573 / GC555, 0x0055 = GC573 PCIe alternate */ - if (strncasecmp(dev_id, "0x0054", 6) == 0 || - strncasecmp(dev_id, "0x0055", 6) == 0) { - snprintf(out_addr, max_len, "%.32s", ent->d_name); - - /* Find IOMMU group */ - snprintf(path, sizeof(path), "/sys/bus/pci/devices/%s/iommu_group", ent->d_name); - char link_buf[512]; - ssize_t len = readlink(path, link_buf, sizeof(link_buf) - 1); - if (len > 0) { - link_buf[len] = '\0'; - char *base = strrchr(link_buf, '/'); - if (base) { - *out_group = atoi(base + 1); - found = 1; - break; - } - } - } - } else { - fclose(f); - } - } - } - } else { - fclose(f); - } - } - closedir(dir); - - if (!found) { - pr_err("No AVerMedia GC573 device (1461:0054/0055) found on PCIe bus!"); - return -ENODEV; + snprintf(out_addr, max_len, "%s", cards[i].pci_bdf); + *out_group = cards[i].iommu_group; + return 0; } - return 0; + pr_err("No AVerMedia GC573 device (1461:0054/0055) found on PCIe bus!"); + return -ENODEV; } int gc573_vfio_init(struct gc573_vfio_ctx *ctx, const char *pci_addr) diff --git a/tools/opengc573-userd/gc573_vfio.h b/tools/opengc573-userd/src/gc573_vfio.h similarity index 100% rename from tools/opengc573-userd/gc573_vfio.h rename to tools/opengc573-userd/src/gc573_vfio.h From f4d674f3fb6002e345183e2d60660839fc2012ad Mon Sep 17 00:00:00 2001 From: PolygonPrincess Date: Wed, 11 Nov 2026 05:04:31 -0500 Subject: [PATCH 5/8] feat(gc570d): expose the unified JSON-RPC control socket Part 3/5 of the modularization plan (see .opencode/plan/everlive-modularization/03-opengc570d-userd.md). Make the GC570D engine a first-class EverLive citizen: discover the card through libeverlive-core and serve the same newline-delimited JSON-RPC 2.0 protocol on the same everlive.sock as the GC573 streamer, while keeping the legacy plain-text gc570d-ctl socket. Discovery - Replace the private /sys/bus/pci/devices scan with everlive_detect_installed_cards() and accept only "gc570d" descriptors (subsystem 1461:5700). A missing/unreadable subsystem file fails safe instead of guessing. New control plane - src/gc570d-jsonrpc.{c,h}: persistent AF_UNIX clients, SO_PEERCRED UID check, flock-protected everlive.sock, and line-buffered dispatch. - Methods: hello, ping, status.get, led.set, led.mode.set, audio.volume.set, audio.mute.set, audio.channels.set (stereo only), osd.set, osd.position.set, aspect.set, scale.set, input.set, pip.set, hotplug, edid.set. - status.get emits the same schema as the GC573 engine (signal, video, audio, v4l2, osd, aspect, scale, framegen, led, clients) so the Python hub consumes it unchanged. FPS is derived from the monotonic frame seq. - GC573-only methods (mode.set, v4l2.set, hdr.*, framegen.set, toast.show) return -32601 explicitly so clients can probe capabilities. - No new thread: the daemon poll includes the listener and all clients and drains them from the existing loop, keeping LED/I2C and debugfs actions serialized with the watchdog and re-init paths. Supporting APIs - gc570d_led_set_rgb()/gc570d_led_set_effect(): public entry points; the sysfs effect attribute now delegates to the same implementation. - gc570d_user_diag_action(): extracted from the plain-text handler and reused for hotplug/edid.set. - gc570d_everlive_* getters for the frontend state (port, osd, aspect, scale, volume, mute, v4l2 paths, headless/running). - gc570d_user_pipeline_stats(): frame/audio/IRQ snapshot callback. Build and shutdown - Compile the daemon from incremental objects, add -I../libeverlive-core and link libeverlive_core.a; clean src/*.o and src/*.d. Ignore *.d. - Re-arm the JSON-RPC device pointer around hardware re-init and tear the listener down before the legacy control socket. Viewer - Attach the hub to a running DUO engine (the duo_mode guard is gone) and prefer IPC for DUO input, HPD, EDID policy, and LED effects, with gc570d-ctl preserved as the fallback. Verified: GCC and Clang builds are warning-free; a socket harness exercises hello, ping, status.get, mutations, unsupported methods, and persistent connections; all 7 test suites pass (164 assertions); the viewer compiles. --- .gitignore | 1 + tools/everlive/everlive_viewer.py | 33 +- tools/opengc570d-userd/Makefile | 26 +- tools/opengc570d-userd/src/gc570d-jsonrpc.c | 956 ++++++++++++++++++ tools/opengc570d-userd/src/gc570d-jsonrpc.h | 62 ++ tools/opengc570d-userd/src/gc570d-led.c | 129 ++- tools/opengc570d-userd/src/gc570d-userspace.c | 188 ++-- tools/opengc570d-userd/src/gc570d.h | 8 + tools/opengc570d-userd/src/gc570d_everlive.c | 101 ++ tools/opengc570d-userd/src/gc570d_everlive.h | 13 + 10 files changed, 1418 insertions(+), 99 deletions(-) create mode 100644 tools/opengc570d-userd/src/gc570d-jsonrpc.c create mode 100644 tools/opengc570d-userd/src/gc570d-jsonrpc.h diff --git a/.gitignore b/.gitignore index 1155a4a..e116449 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ *.ko *.mod.c *.o +*.d *.symvers *.order *.cmd diff --git a/tools/everlive/everlive_viewer.py b/tools/everlive/everlive_viewer.py index 517ed88..79fc6b8 100755 --- a/tools/everlive/everlive_viewer.py +++ b/tools/everlive/everlive_viewer.py @@ -1551,6 +1551,13 @@ def ctl(self, *args: str) -> Optional[str]: except Exception: return None + def _duo_ipc(self): + """Return the attached control client, or None when unavailable.""" + ipc = getattr(self, "ipc", None) + if ipc is not None and getattr(ipc, "connected", False): + return ipc + return None + def set_duo_input(self, port: int) -> bool: """Select HDMI 1 (0) or HDMI 2 (1) on the Live Gamer DUO.""" port = 1 if int(port) else 0 @@ -1560,6 +1567,9 @@ def set_duo_input(self, port: int) -> bool: return False if not self.is_running: return True + ipc = self._duo_ipc() + if ipc is not None: + return ipc.request("input.set", {"port": port + 1}) return self.ctl("input", str(port + 1)) is not None def apply_duo_led(self, mode: int) -> bool: @@ -1573,17 +1583,27 @@ def apply_duo_led(self, mode: int) -> bool: effect = "breathing" else: effect = "static" + ipc = self._duo_ipc() + if ipc is not None: + return ipc.request("led.mode.set", {"mode": effect}) return self.ctl("led", effect) is not None def duo_hotplug(self, port: int = 0) -> bool: """Pulse HPD on IT68051 (HDMI 1) or IT6802 (HDMI 2).""" node = "it68051_hpd" if int(port) == 0 else "it6802_hpd" + ipc = self._duo_ipc() + if ipc is not None: + return ipc.request("hotplug", {"port": 1 if int(port) == 0 else 2}) return self.ctl("action", node, "1") is not None def duo_edid_policy(self, bridge: bool) -> bool: """Select internal bridge EDID (True) or downstream copy (False).""" node = ("splitter_windows_bridge_edid" if bridge else "splitter_windows_edid_copy") + ipc = self._duo_ipc() + if ipc is not None: + return ipc.request("edid.set", + {"profile": "default" if bridge else "copy"}) return self.ctl("action", node, "1") is not None def _start_vfio_duo(self, fullscreen: bool, vsync: bool, @@ -2217,6 +2237,10 @@ def __init__(self, streamer: GameStreamer, vsync: bool = True, streamer_mode: bo # QSocketNotifier keeps this on the GUI thread: no worker threads # and no cross-thread signals. self._ipc = EverLiveIpcClient(self) + if self.streamer is not None: + # Let the engine wrapper prefer IPC for DUO input/HPD/EDID + # when the hub is attached; gc570d-ctl stays as the fallback. + self.streamer.ipc = self._ipc self._ipc.connectedChanged.connect(self._on_remote_connection) self._ipc.statusReceived.connect(self._on_remote_status) self._ipc.errorRaised.connect(lambda msg: print(f"[IPC] {msg}")) @@ -3631,7 +3655,7 @@ def _vfio_preflight(self) -> bool: # ----------------------------------------------------------------- def _ipc_try_attach(self): """Attach to a running streamer (local or external).""" - if self.duo_mode or self._ipc is None or self._ipc_shutdown: + if self._ipc is None or self._ipc_shutdown: return if self._ipc.connected: return @@ -3885,6 +3909,13 @@ def _send_ipc_led(self, mode: int) -> bool: """ if self._ipc is None or not self._ipc.connected: return False + if self.duo_mode: + # The DUO's animations are named differently from the GC573 + # renderer's: mode 8 is its colorful cycle, mode 9 breathing. + if mode == 8: + return self._ipc.set_led_mode("colorful") + if mode == 9: + return self._ipc.set_led_mode("breathing") if mode == 8: return self._ipc.set_led_mode("wave") if mode == 9: diff --git a/tools/opengc570d-userd/Makefile b/tools/opengc570d-userd/Makefile index 1ea3959..ec732a1 100644 --- a/tools/opengc570d-userd/Makefile +++ b/tools/opengc570d-userd/Makefile @@ -9,11 +9,15 @@ CC ?= gcc PKG_CONFIG ?= pkg-config CFLAGS ?= -O2 -g -Wall -Wextra -pthread LDFLAGS ?= -pthread +DEPFLAGS := -MMD -MP + +CORE_DIR := ../libeverlive-core +CORE_LIB := $(CORE_DIR)/libeverlive_core.a USER_CFLAGS := -O2 -g -Wall -Wextra -Wno-unused-parameter \ -Wno-sign-compare -std=gnu11 \ -D_GNU_SOURCE -DGC570D_USERSPACE -DGC570D_DEVELOPER_DEBUGFS \ - -Isrc -I. + -Isrc -I. -I$(CORE_DIR) USER_LDFLAGS := -pthread -lm -lrt SDL_CFLAGS := $(shell $(PKG_CONFIG) --cflags sdl2 2>/dev/null) @@ -43,7 +47,10 @@ USERD_SRCS := \ src/gc570d-dma.c \ src/gc570d-audio.c \ src/gc570d-video.c \ - src/gc570d_everlive.c + src/gc570d_everlive.c \ + src/gc570d-jsonrpc.c + +USERD_OBJS := $(USERD_SRCS:.c=.o) USER_HEADERS := \ src/gc570d.h \ @@ -51,6 +58,7 @@ USER_HEADERS := \ src/gc570d-user-compat.h \ src/gc570d-shm.h \ src/gc570d_everlive.h \ + src/gc570d-jsonrpc.h \ src/gc570d_font8x8.h \ data/gc570d_hdr_lut_3000.inc \ data/gc570d_no_signal_640x360.inc @@ -59,10 +67,16 @@ TARGETS := opengc570d-userd gc570d-ctl libgc570d-v4l2.so all: $(TARGETS) -opengc570d-userd: $(USERD_SRCS) $(USER_HEADERS) - $(CC) $(USER_CFLAGS) -o $@ $(USERD_SRCS) $(USER_LDFLAGS) $(LDFLAGS) +opengc570d-userd: $(USERD_OBJS) $(CORE_LIB) + $(CC) $(USER_CFLAGS) -o $@ $(USERD_OBJS) $(USER_LDFLAGS) $(LDFLAGS) $(CORE_LIB) @chmod 0755 $@ +$(CORE_LIB): + $(MAKE) -C $(CORE_DIR) + +src/%.o: src/%.c $(USER_HEADERS) + $(CC) $(USER_CFLAGS) $(DEPFLAGS) -c -o $@ $< + gc570d-ctl: src/gc570d-ctl.c $(CC) -O2 -g -Wall -Wextra -std=gnu11 -D_GNU_SOURCE -o $@ $< @chmod 0755 $@ @@ -75,6 +89,8 @@ libgc570d-v4l2.so: src/gc570d-v4l2-preload.c src/gc570d-shm.h test selftest: $(MAKE) -C ../../tests test +-include $(USERD_OBJS:.o=.d) + PREFIX ?= /usr/local DESTDIR ?= BINDIR := $(DESTDIR)$(PREFIX)/bin @@ -113,6 +129,6 @@ uninstall: rm -f $(DESTDIR)/etc/gc570d.conf.example clean: - rm -f $(TARGETS) + rm -f $(USERD_OBJS) $(USERD_OBJS:.o=.d) $(TARGETS) .PHONY: all clean test selftest install uninstall diff --git a/tools/opengc570d-userd/src/gc570d-jsonrpc.c b/tools/opengc570d-userd/src/gc570d-jsonrpc.c new file mode 100644 index 0000000..c2a9165 --- /dev/null +++ b/tools/opengc570d-userd/src/gc570d-jsonrpc.c @@ -0,0 +1,956 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * gc570d-jsonrpc.c — Unified JSON-RPC 2.0 control socket for the GC570D + * + * Transport: newline-delimited JSON-RPC 2.0 over AF_UNIX, exactly like the + * GC573 streamer. The daemon loop polls the listener and every connected + * client, then calls gc570d_jsonrpc_service() to drain request lines. No + * extra thread is used, so hardware access stays serialized with the + * watchdog and re-initialization paths. + */ + +#include "gc570d-jsonrpc.h" + +#include "gc570d.h" +#include "gc570d_everlive.h" +#include "everlive_json.h" +#include "everlive_ipc.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define RPC_REPLY_MAX 8192 +#define RPC_RESULT_MAX 6144 +#define RPC_METHOD_MAX 48 + +static int g_listen_fd = -1; +static int g_lock_fd = -1; +static bool g_bound; +static char g_path[EVERLIVE_IPC_PATH_MAX]; +static char g_lock_path[EVERLIVE_IPC_PATH_MAX + 8]; +static char g_version[24] = "unknown"; +static struct gc570d_dev *g_dev; +static void (*g_stats_fn)(struct gc570d_jsonrpc_stats *out); + +/* Per-input FPS estimation from the monotonic frame counters. */ +static uint64_t g_last_seq[2]; +static uint64_t g_last_fps_ns[2]; +static double g_fps[2]; + +/* Last LED colour applied through this interface. */ +static bool g_led_valid; +static unsigned int g_led_r; +static unsigned int g_led_g; +static unsigned int g_led_b; + +static uint64_t rpc_now_ns(void) +{ + struct timespec ts; + + clock_gettime(CLOCK_MONOTONIC, &ts); + return (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec; +} + +static void rpc_write_all(int fd, const char *data, size_t len) +{ + while (len > 0) { + ssize_t n = send(fd, data, len, MSG_NOSIGNAL); + + if (n < 0) { + if (errno == EINTR) + continue; + return; + } + data += n; + len -= (size_t)n; + } +} + +static void rpc_reply(int fd, const char *id, const char *result_json) +{ + char esc_id[EVERLIVE_JSON_ID_MAX * 2]; + char buf[RPC_REPLY_MAX]; + int n; + + everlive_json_format_id(esc_id, sizeof(esc_id), id); + n = snprintf(buf, sizeof(buf), + "{\"jsonrpc\":\"2.0\",\"id\":%s,\"result\":%s}\n", + esc_id, result_json); + if (n > 0 && (size_t)n < sizeof(buf)) + rpc_write_all(fd, buf, (size_t)n); +} + +static void rpc_error(int fd, const char *id, int code, const char *message) +{ + char esc_id[EVERLIVE_JSON_ID_MAX * 2]; + char esc_msg[256]; + char buf[512]; + int n; + + everlive_json_format_id(esc_id, sizeof(esc_id), id); + everlive_json_escape_into(esc_msg, sizeof(esc_msg), + message ? message : "error"); + n = snprintf(buf, sizeof(buf), + "{\"jsonrpc\":\"2.0\",\"id\":%s," + "\"error\":{\"code\":%d,\"message\":\"%s\"}}\n", + esc_id, code, esc_msg); + if (n > 0 && (size_t)n < sizeof(buf)) + rpc_write_all(fd, buf, (size_t)n); +} + +/* ------------------------------------------------------------------ */ +/* status.get */ +/* ------------------------------------------------------------------ */ + +static double rpc_port_fps(int port, uint64_t seq, uint64_t now) +{ + if (g_last_fps_ns[port] && now > g_last_fps_ns[port] && + seq >= g_last_seq[port]) { + g_fps[port] = (double)(seq - g_last_seq[port]) * 1000000000.0 / + (double)(now - g_last_fps_ns[port]); + } + g_last_seq[port] = seq; + g_last_fps_ns[port] = now; + return g_fps[port]; +} + +static void rpc_status(int fd, const char *id) +{ + struct gc570d_jsonrpc_stats st; + struct gc570d_dev *gc = g_dev; + char result[RPC_RESULT_MAX]; + char path_esc[96]; + uint64_t now = rpc_now_ns(); + int port = gc570d_everlive_port(); + unsigned int src_w = 0; + unsigned int src_h = 0; + unsigned int out_w = 0; + unsigned int out_h = 0; + unsigned int rate = 0; + unsigned int audio_rate = 48000; + unsigned int audio_ch = 2; + unsigned int led_r = 0; + unsigned int led_g = 0; + unsigned int led_b = 0; + bool streaming = false; + bool no_signal = true; + bool interlaced = false; + double fps; + int n; + + if (port < 0 || port > 1) + port = 0; + + memset(&st, 0, sizeof(st)); + if (g_stats_fn) + g_stats_fn(&st); + + if (gc) { + if (port == 0) { + streaming = READ_ONCE(gc->video0_streaming); + no_signal = READ_ONCE(gc->video0_no_signal); + interlaced = READ_ONCE(gc->video0_input_interlaced); + src_w = READ_ONCE(gc->video0_source_width); + src_h = READ_ONCE(gc->video0_source_height); + rate = READ_ONCE(gc->video0_source_fps); + out_w = READ_ONCE(gc->video0_width); + out_h = READ_ONCE(gc->video0_height); + audio_rate = READ_ONCE(gc->audio0_rate); + audio_ch = READ_ONCE(gc->audio0_channels); + led_r = gc->led_subleds[0].brightness; + led_g = gc->led_subleds[1].brightness; + led_b = gc->led_subleds[2].brightness; + } else { + streaming = READ_ONCE(gc->video_streaming); + no_signal = READ_ONCE(gc->video_no_signal); + interlaced = READ_ONCE(gc->video_input_interlaced); + src_w = READ_ONCE(gc->video_source_width); + src_h = READ_ONCE(gc->video_source_height); + rate = READ_ONCE(gc->video_source_fps); + out_w = READ_ONCE(gc->video_width); + out_h = READ_ONCE(gc->video_height); + audio_rate = READ_ONCE(gc->audio_rate); + audio_ch = READ_ONCE(gc->audio_channels); + led_r = gc->led_subleds[0].brightness; + led_g = gc->led_subleds[1].brightness; + led_b = gc->led_subleds[2].brightness; + } + } + + if (g_led_valid) { + led_r = g_led_r; + led_g = g_led_g; + led_b = g_led_b; + } + + if (!src_w) + src_w = out_w ? out_w : GC570D_WIDTH; + if (!src_h) + src_h = out_h ? out_h : GC570D_HEIGHT; + if (!out_w) + out_w = src_w; + if (!out_h) + out_h = src_h; + if (!rate) + rate = 60; + if (!audio_rate) + audio_rate = 48000; + if (!audio_ch) + audio_ch = 2; + + fps = rpc_port_fps(port, st.video_seq[port], now); + everlive_json_escape_into(path_esc, sizeof(path_esc), + gc570d_everlive_v4l2(port)); + + n = snprintf(result, sizeof(result), + "{\"proto\":1,\"version\":\"%s\",\"pid\":%ld," + "\"headless\":%s,\"streaming\":%s," + "\"signal\":{\"locked\":%s,\"width\":%u,\"height\":%u," + "\"interlaced\":%s,\"rate_hz\":%.2f," + "\"pixel_clock_khz\":0}," + "\"video\":{\"fps\":%.2f,\"frames\":%llu," + "\"ring_buffers\":4,\"ring_channels\":4," + "\"output_width\":%u,\"output_height\":%u}," + "\"audio\":{\"state\":\"lpcm\",\"label\":\"L-PCM 48kHz\"," + "\"rate_hz\":%u,\"channels\":%u,\"out_channels\":%u," + "\"layout\":\"stereo\",\"bits\":16," + "\"gain\":%.3f,\"muted\":%s}," + "\"v4l2\":{\"path\":\"%s\",\"configured\":%s," + "\"width\":%u,\"height\":%u,\"frames\":%llu," + "\"dropped\":%llu}," + "\"osd\":{\"enabled\":%s,\"position\":\"%s\"}," + "\"aspect\":\"%s\",\"scale\":\"%s\"," + "\"vsync\":false,\"fullscreen\":false," + "\"framegen\":{\"mode\":\"off\",\"present_fps\":0.00," + "\"output_fps\":0.00,\"unique_fps\":0.00," + "\"raw_fps\":0.00,\"flow_scale\":1.00," + "\"perf_mode\":false,\"low_latency\":false," + "\"layer_found\":false,\"dll_found\":false," + "\"dll_path\":\"\",\"vulkan_active\":false," + "\"deduplicator_active\":false}," + "\"led\":{\"r\":%u,\"g\":%u,\"b\":%u}," + "\"clients\":1,\"age_ms\":0}", + g_version, (long)getpid(), + gc570d_everlive_headless() ? "true" : "false", + streaming ? "true" : "false", + (streaming && !no_signal) ? "true" : "false", + src_w, src_h, interlaced ? "true" : "false", + (double)rate, + fps, (unsigned long long)st.video_seq[port], + out_w, out_h, + audio_rate, audio_ch, audio_ch, + (double)gc570d_everlive_volume() / 100.0, + gc570d_everlive_muted() ? "true" : "false", + path_esc, st.v4l2_configured[port] ? "true" : "false", + out_w, out_h, + (unsigned long long)st.v4l2_frames[port], + (unsigned long long)st.v4l2_dropped[port], + gc570d_everlive_osd() ? "true" : "false", + gc570d_everlive_osd_pos(), + gc570d_everlive_aspect(), gc570d_everlive_scale(), + led_r, led_g, led_b); + + if (n > 0 && (size_t)n < sizeof(result)) + rpc_reply(fd, id, result); + else + rpc_error(fd, id, -32603, "status serialization failed"); +} + +/* ------------------------------------------------------------------ */ +/* Request handlers */ +/* ------------------------------------------------------------------ */ + +static void rpc_ok(int fd, const char *id) +{ + rpc_reply(fd, id, "{\"ok\":true}"); +} + +static int rpc_require_dev(int fd, const char *id) +{ + if (g_dev) + return 0; + rpc_error(fd, id, -32603, "capture hardware unavailable"); + return -1; +} + +static void rpc_hello(int fd, const char *id) +{ + char result[640]; + char path_esc[128]; + int n; + + everlive_json_escape_into(path_esc, sizeof(path_esc), g_path); + n = snprintf(result, sizeof(result), + "{\"proto\":1,\"app\":\"everlive\",\"version\":\"%s\"," + "\"pid\":%ld,\"socket\":\"%s\"," + "\"methods\":[\"hello\",\"ping\",\"status.get\"," + "\"led.set\",\"led.mode.set\",\"audio.volume.set\"," + "\"audio.mute.set\",\"audio.channels.set\"," + "\"osd.set\",\"osd.position.set\",\"aspect.set\"," + "\"scale.set\",\"input.set\",\"pip.set\"," + "\"hotplug\",\"edid.set\"]}", + g_version, (long)getpid(), path_esc); + if (n > 0 && (size_t)n < sizeof(result)) + rpc_reply(fd, id, result); + else + rpc_error(fd, id, -32603, "hello serialization failed"); +} + +static void rpc_led_set(int fd, const char *id, const char *line) +{ + long r, g, b; + int ret; + + if (rpc_require_dev(fd, id) < 0) + return; + if (!everlive_json_get_long(line, "r", &r) || + !everlive_json_get_long(line, "g", &g) || + !everlive_json_get_long(line, "b", &b)) { + rpc_error(fd, id, -32602, "invalid params: r, g, b required"); + return; + } + + r = clamp_t(long, r, 0L, 255L); + g = clamp_t(long, g, 0L, 255L); + b = clamp_t(long, b, 0L, 255L); + + ret = gc570d_led_set_rgb(g_dev, (u8)r, (u8)g, (u8)b); + if (ret) { + rpc_error(fd, id, -32603, "failed to apply LED colour"); + return; + } + + g_led_r = (unsigned int)r; + g_led_g = (unsigned int)g; + g_led_b = (unsigned int)b; + g_led_valid = true; + rpc_ok(fd, id); +} + +static void rpc_led_mode(int fd, const char *id, const char *line) +{ + char mode[24]; + const char *effect; + int ret; + + if (rpc_require_dev(fd, id) < 0) + return; + if (!everlive_json_get_string(line, "mode", mode, sizeof(mode))) { + rpc_error(fd, id, -32602, "invalid params: mode required"); + return; + } + + if (!strcasecmp(mode, "off") || !strcasecmp(mode, "dark")) + effect = "off"; + else if (!strcasecmp(mode, "static") || !strcasecmp(mode, "solid")) + effect = "static"; + else if (!strcasecmp(mode, "breathing") || !strcasecmp(mode, "breathe") || + !strcasecmp(mode, "wave")) + effect = "breathing"; + else if (!strcasecmp(mode, "colorful") || !strcasecmp(mode, "rainbow") || + !strcasecmp(mode, "cycle")) + effect = "colorful"; + else { + rpc_error(fd, id, -32602, + "unknown mode: off|static|breathing|colorful|wave|rainbow"); + return; + } + + ret = gc570d_led_set_effect(g_dev, effect); + if (ret) { + rpc_error(fd, id, -32603, "failed to apply LED effect"); + return; + } + g_led_valid = false; + rpc_ok(fd, id); +} + +static void rpc_audio_volume(int fd, const char *id, const char *line) +{ + double gain; + int pct; + + if (!everlive_json_get_double(line, "gain", &gain)) { + rpc_error(fd, id, -32602, "invalid params: gain required"); + return; + } + if (gain < 0.0) + gain = 0.0; + if (gain > 1.5) + gain = 1.5; + + pct = (int)(gain * 100.0 + 0.5); + gc570d_everlive_set_volume(pct); + rpc_ok(fd, id); +} + +static void rpc_audio_mute(int fd, const char *id, const char *line) +{ + bool muted; + + if (!everlive_json_get_bool(line, "muted", &muted)) { + rpc_error(fd, id, -32602, "invalid params: muted required"); + return; + } + gc570d_everlive_set_mute(muted); + rpc_ok(fd, id); +} + +static void rpc_audio_channels(int fd, const char *id, const char *line) +{ + char mode[16]; + + if (!everlive_json_get_string(line, "mode", mode, sizeof(mode))) { + rpc_error(fd, id, -32602, "invalid params: mode required"); + return; + } + if (strcasecmp(mode, "auto") && strcasecmp(mode, "stereo") && + strcasecmp(mode, "2") && strcasecmp(mode, "2.0")) { + rpc_error(fd, id, -32602, + "GC570D captures LPCM stereo only (auto|stereo)"); + return; + } + rpc_ok(fd, id); +} + +static void rpc_osd(int fd, const char *id, const char *line) +{ + bool enabled; + + if (!everlive_json_get_bool(line, "enabled", &enabled)) { + rpc_error(fd, id, -32602, "invalid params: enabled required"); + return; + } + gc570d_everlive_set_osd(enabled); + rpc_ok(fd, id); +} + +static void rpc_osd_pos(int fd, const char *id, const char *line) +{ + char pos[16]; + + if (!everlive_json_get_string(line, "pos", pos, sizeof(pos))) { + rpc_error(fd, id, -32602, "invalid params: pos required"); + return; + } + if (strcasecmp(pos, "tr") && strcasecmp(pos, "br") && + strcasecmp(pos, "bl") && strcasecmp(pos, "tl") && + strcasecmp(pos, "top-right") && strcasecmp(pos, "bottom-right") && + strcasecmp(pos, "bottom-left") && strcasecmp(pos, "top-left")) { + rpc_error(fd, id, -32602, "unknown pos: tr|br|bl|tl"); + return; + } + gc570d_everlive_set_osd_pos(pos); + rpc_ok(fd, id); +} + +static void rpc_aspect(int fd, const char *id, const char *line) +{ + char mode[24]; + + if (!everlive_json_get_string(line, "mode", mode, sizeof(mode))) { + rpc_error(fd, id, -32602, "invalid params: mode required"); + return; + } + if (strcasecmp(mode, "auto") && strcasecmp(mode, "16:9") && + strcasecmp(mode, "16/9") && strcasecmp(mode, "4:3") && + strcasecmp(mode, "4/3") && strcasecmp(mode, "stretch") && + strcasecmp(mode, "fill")) { + rpc_error(fd, id, -32602, + "unknown mode: auto|16:9|4:3|stretch"); + return; + } + gc570d_everlive_set_aspect(mode); + rpc_ok(fd, id); +} + +static void rpc_scale(int fd, const char *id, const char *line) +{ + char mode[16]; + + if (!everlive_json_get_string(line, "mode", mode, sizeof(mode))) { + rpc_error(fd, id, -32602, "invalid params: mode required"); + return; + } + if (strcasecmp(mode, "fit") && strcasecmp(mode, "aspect") && + strcasecmp(mode, "integer") && strcasecmp(mode, "nearest")) { + rpc_error(fd, id, -32602, "unknown mode: fit|integer"); + return; + } + gc570d_everlive_set_scale(mode); + rpc_ok(fd, id); +} + +static void rpc_input(int fd, const char *id, const char *line) +{ + long port; + + if (!everlive_json_get_long(line, "port", &port)) { + rpc_error(fd, id, -32602, "invalid params: port required"); + return; + } + /* + * Canonical values are 1-based (matching gc570d-ctl): 1 = HDMI 1, + * 2 = HDMI 2. 0 is also accepted as HDMI 1 for 0-based clients. + */ + if (port < 0 || port > 2) { + rpc_error(fd, id, -32602, "port must be 1 or 2"); + return; + } + gc570d_everlive_set_port(port == 2 ? 1 : 0); + rpc_ok(fd, id); +} + +static void rpc_pip(int fd, const char *id, const char *line) +{ + bool enabled; + + if (!everlive_json_get_bool(line, "enabled", &enabled)) { + rpc_error(fd, id, -32602, "invalid params: enabled required"); + return; + } + gc570d_everlive_set_pip(enabled); + rpc_ok(fd, id); +} + +static void rpc_hotplug(int fd, const char *id, const char *line) +{ + long port = 0; + const char *node; + int ret; + + if (!everlive_json_get_long(line, "port", &port)) + port = 1; + if (port < 0 || port > 2) { + rpc_error(fd, id, -32602, "port must be 1 or 2"); + return; + } + + node = (port == 2) ? "it6802_hpd" : "it68051_hpd"; + ret = gc570d_user_diag_action(node, "1"); + if (ret < 0) { + rpc_error(fd, id, -32603, "HPD pulse failed"); + return; + } + rpc_ok(fd, id); +} + +static void rpc_edid(int fd, const char *id, const char *line) +{ + char profile[32]; + const char *node; + int ret; + + if (!everlive_json_get_string(line, "profile", profile, + sizeof(profile))) { + rpc_error(fd, id, -32602, "invalid params: profile required"); + return; + } + + if (!strcasecmp(profile, "default") || !strcasecmp(profile, "bridge") || + !strcasecmp(profile, "internal")) + node = "splitter_windows_bridge_edid"; + else if (!strcasecmp(profile, "copy") || + !strcasecmp(profile, "passthrough") || + !strcasecmp(profile, "downstream")) + node = "splitter_windows_edid_copy"; + else { + rpc_error(fd, id, -32602, + "unknown profile: default|bridge|copy|passthrough"); + return; + } + + ret = gc570d_user_diag_action(node, "1"); + if (ret < 0) { + rpc_error(fd, id, -32603, "EDID switch failed"); + return; + } + rpc_ok(fd, id); +} + +static bool rpc_is_unsupported(const char *method) +{ + static const char *const unsupported[] = { + "mode.set", "v4l2.set", "hdr.set", "hdr.passthrough", + "hdr.exposure", "framegen.set", "toast.show", + }; + + for (size_t i = 0; i < ARRAY_SIZE(unsupported); i++) { + if (strcmp(method, unsupported[i]) == 0) + return true; + } + return false; +} + +static void rpc_dispatch(int fd, const char *id, const char *method, + const char *line) +{ + if (strcmp(method, "hello") == 0) + rpc_hello(fd, id); + else if (strcmp(method, "ping") == 0) + rpc_reply(fd, id, "\"pong\""); + else if (strcmp(method, "status.get") == 0) + rpc_status(fd, id); + else if (strcmp(method, "led.set") == 0) + rpc_led_set(fd, id, line); + else if (strcmp(method, "led.mode.set") == 0) + rpc_led_mode(fd, id, line); + else if (strcmp(method, "audio.volume.set") == 0) + rpc_audio_volume(fd, id, line); + else if (strcmp(method, "audio.mute.set") == 0) + rpc_audio_mute(fd, id, line); + else if (strcmp(method, "audio.channels.set") == 0) + rpc_audio_channels(fd, id, line); + else if (strcmp(method, "osd.set") == 0) + rpc_osd(fd, id, line); + else if (strcmp(method, "osd.position.set") == 0) + rpc_osd_pos(fd, id, line); + else if (strcmp(method, "aspect.set") == 0) + rpc_aspect(fd, id, line); + else if (strcmp(method, "scale.set") == 0) + rpc_scale(fd, id, line); + else if (strcmp(method, "input.set") == 0) + rpc_input(fd, id, line); + else if (strcmp(method, "pip.set") == 0) + rpc_pip(fd, id, line); + else if (strcmp(method, "hotplug") == 0) + rpc_hotplug(fd, id, line); + else if (strcmp(method, "edid.set") == 0) + rpc_edid(fd, id, line); + else if (rpc_is_unsupported(method)) + rpc_error(fd, id, -32601, + "method not supported on the GC570D engine"); + else + rpc_error(fd, id, -32601, "method not found"); +} + +static void rpc_handle_request(int fd, const char *line) +{ + char id[EVERLIVE_JSON_ID_MAX]; + char method[RPC_METHOD_MAX]; + bool has_id; + const char *rid; + + has_id = everlive_json_find_value(line, "id", id, sizeof(id)) && + strcmp(id, "null") != 0; + rid = has_id ? id : NULL; + + if (!everlive_json_get_string(line, "method", method, sizeof(method))) { + if (has_id) + rpc_error(fd, rid, -32700, + "parse error: missing method"); + return; + } + + rpc_dispatch(fd, rid, method, line); +} + +/* ------------------------------------------------------------------ */ +/* Transport */ +/* ------------------------------------------------------------------ */ + +#define RPC_CLIENT_IN_MAX 8192 + +struct rpc_client { + int fd; + size_t inlen; + bool active; + char in[RPC_CLIENT_IN_MAX]; +}; + +static struct rpc_client g_clients[GC570D_JSONRPC_MAX_CLIENTS]; + +static struct rpc_client *rpc_client_alloc(void) +{ + for (size_t i = 0; i < ARRAY_SIZE(g_clients); i++) { + if (!g_clients[i].active) + return &g_clients[i]; + } + return NULL; +} + +static void rpc_client_close(struct rpc_client *c) +{ + if (!c->active) + return; + close(c->fd); + c->fd = -1; + c->active = false; + c->inlen = 0; +} + +static void rpc_client_close_all(void) +{ + for (size_t i = 0; i < ARRAY_SIZE(g_clients); i++) + rpc_client_close(&g_clients[i]); +} + +static bool rpc_peer_ok(int fd) +{ + struct ucred cred; + socklen_t len = sizeof(cred); + + if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cred, &len) < 0) + return false; + return cred.uid == getuid(); +} + +static void rpc_drain_lines(struct rpc_client *c) +{ + size_t consumed = 0; + + while (consumed < c->inlen) { + char *line = c->in + consumed; + size_t avail = c->inlen - consumed; + char *nl = memchr(line, '\n', avail); + size_t line_len; + size_t total; + + if (!nl) + break; + line_len = (size_t)(nl - line); + total = line_len + 1; + *nl = '\0'; + if (line_len && line[line_len - 1] == '\r') + line[--line_len] = '\0'; + if (line_len) + rpc_handle_request(c->fd, line); + consumed += total; + if (!c->active) + return; + } + + if (consumed) { + memmove(c->in, c->in + consumed, c->inlen - consumed); + c->inlen -= consumed; + } + c->in[c->inlen] = '\0'; +} + +static void rpc_client_read(struct rpc_client *c) +{ + for (;;) { + size_t space = sizeof(c->in) - c->inlen - 1; + ssize_t n; + + if (space == 0) { + rpc_error(c->fd, NULL, -32000, "frame too large"); + rpc_client_close(c); + return; + } + + n = recv(c->fd, c->in + c->inlen, space, MSG_DONTWAIT); + if (n == 0) { + rpc_client_close(c); + return; + } + if (n < 0) { + if (errno == EINTR) + continue; + if (errno == EAGAIN || errno == EWOULDBLOCK) + return; + rpc_client_close(c); + return; + } + + c->inlen += (size_t)n; + c->in[c->inlen] = '\0'; + rpc_drain_lines(c); + if (!c->active) + return; + } +} + +int gc570d_jsonrpc_fill_pollfds(struct pollfd *fds, int max) +{ + int n = 0; + + if (!fds || max <= 0) + return 0; + + if (g_listen_fd >= 0 && n < max) { + fds[n].fd = g_listen_fd; + fds[n].events = POLLIN; + n++; + } + + for (size_t i = 0; i < ARRAY_SIZE(g_clients) && n < max; i++) { + if (!g_clients[i].active) + continue; + fds[n].fd = g_clients[i].fd; + fds[n].events = POLLIN; + n++; + } + + return n; +} + +void gc570d_jsonrpc_service(void) +{ + struct pollfd pfd[1 + GC570D_JSONRPC_MAX_CLIENTS]; + int map[1 + GC570D_JSONRPC_MAX_CLIENTS]; + int n = 0; + + if (g_listen_fd >= 0) { + pfd[n].fd = g_listen_fd; + pfd[n].events = POLLIN; + map[n] = -1; + n++; + } + + for (size_t i = 0; i < ARRAY_SIZE(g_clients); i++) { + if (!g_clients[i].active) + continue; + pfd[n].fd = g_clients[i].fd; + pfd[n].events = POLLIN; + map[n] = (int)i; + n++; + } + + if (n == 0) + return; + if (poll(pfd, (nfds_t)n, 0) <= 0) + return; + + for (int i = 0; i < n; i++) { + if (!(pfd[i].revents & (POLLIN | POLLHUP | POLLERR))) + continue; + + if (map[i] < 0) { + for (;;) { + struct rpc_client *c; + int cfd = accept4(g_listen_fd, NULL, NULL, + SOCK_NONBLOCK | SOCK_CLOEXEC); + + if (cfd < 0) { + if (errno == EINTR) + continue; + break; + } + c = rpc_client_alloc(); + if (!c || !rpc_peer_ok(cfd)) { + close(cfd); + continue; + } + c->fd = cfd; + c->inlen = 0; + c->in[0] = '\0'; + c->active = true; + } + } else { + rpc_client_read(&g_clients[map[i]]); + } + } +} + +int gc570d_jsonrpc_init(const char *version) +{ + struct sockaddr_un addr; + int ret; + + if (g_listen_fd >= 0) + return 0; + + snprintf(g_version, sizeof(g_version), "%s", + version && *version ? version : "unknown"); + + ret = everlive_ipc_prepare_socket_path(g_path, sizeof(g_path), NULL); + if (ret < 0) + return ret; + + snprintf(g_lock_path, sizeof(g_lock_path), "%.*s.lock", + EVERLIVE_IPC_PATH_MAX - 1, g_path); + + g_lock_fd = open(g_lock_path, O_CREAT | O_RDWR | O_CLOEXEC, 0600); + if (g_lock_fd < 0) { + ret = -errno; + goto fail; + } + if (flock(g_lock_fd, LOCK_EX | LOCK_NB) < 0) { + ret = (errno == EWOULDBLOCK) ? -EADDRINUSE : -errno; + goto fail; + } + + /* A leftover socket without a live lock owner is stale. */ + if (access(g_path, F_OK) == 0) + (void)unlink(g_path); + + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + memcpy(addr.sun_path, g_path, strlen(g_path) + 1); + + g_listen_fd = socket(AF_UNIX, + SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); + if (g_listen_fd < 0) { + ret = -errno; + goto fail; + } + if (bind(g_listen_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + ret = -errno; + goto fail; + } + g_bound = true; + (void)chmod(g_path, 0600); + if (listen(g_listen_fd, GC570D_JSONRPC_MAX_CLIENTS) < 0) { + ret = -errno; + goto fail; + } + + return 0; + +fail: + gc570d_jsonrpc_shutdown(); + return ret; +} + +void gc570d_jsonrpc_shutdown(void) +{ + rpc_client_close_all(); + + if (g_listen_fd >= 0) { + close(g_listen_fd); + g_listen_fd = -1; + } + if (g_path[0] && g_bound) + (void)unlink(g_path); + g_path[0] = '\0'; + g_bound = false; + if (g_lock_fd >= 0) { + (void)flock(g_lock_fd, LOCK_UN); + close(g_lock_fd); + g_lock_fd = -1; + } + g_lock_path[0] = '\0'; +} + +int gc570d_jsonrpc_listen_fd(void) +{ + return g_listen_fd; +} + +const char *gc570d_jsonrpc_socket_path(void) +{ + return g_path; +} + +void gc570d_jsonrpc_set_stats_fn(void (*fn)(struct gc570d_jsonrpc_stats *out)) +{ + g_stats_fn = fn; +} + +void gc570d_jsonrpc_set_dev(struct gc570d_dev *gc) +{ + g_dev = gc; +} diff --git a/tools/opengc570d-userd/src/gc570d-jsonrpc.h b/tools/opengc570d-userd/src/gc570d-jsonrpc.h new file mode 100644 index 0000000..7746bcc --- /dev/null +++ b/tools/opengc570d-userd/src/gc570d-jsonrpc.h @@ -0,0 +1,62 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * gc570d-jsonrpc.h — Unified JSON-RPC 2.0 control socket for the GC570D + * + * The daemon keeps serving its legacy plain-text command socket for + * gc570d-ctl, and additionally binds the shared everlive.sock so EverLive's + * CLI and desktop hub can talk to both capture cards through one protocol. + */ +#ifndef GC570D_JSONRPC_H +#define GC570D_JSONRPC_H + +#include +#include +#include +#include + +struct gc570d_dev; + +#define GC570D_JSONRPC_MAX_CLIENTS 8 + +struct gc570d_jsonrpc_stats { + uint64_t video_seq[2]; + uint64_t audio_periods[2]; + uint64_t irq_dma0; + uint64_t irq_dma1; + bool v4l2_configured[2]; + uint64_t v4l2_frames[2]; + uint64_t v4l2_dropped[2]; +}; + +/* + * Bind the unified runtime socket (respects $EVERLIVE_SOCKET and the legacy + * $EVERLIVE_CONTROL_SOCKET; otherwise $XDG_RUNTIME_DIR/everlive.sock). + * Returns 0 on success or a negative errno. + */ +int gc570d_jsonrpc_init(const char *version); + +/* Close the listener and unlink the socket + lock file. Idempotent. */ +void gc570d_jsonrpc_shutdown(void); + +/* For the daemon poll() loop; -1 when not initialised. */ +int gc570d_jsonrpc_listen_fd(void); +/* Path of the bound socket, or "" when not running. */ +const char *gc570d_jsonrpc_socket_path(void); + +/* + * Add the listener and every connected client to @fds. Returns the number of + * descriptors written; the daemon poll() loop can then call + * gc570d_jsonrpc_service() when any of them reports activity. + */ +int gc570d_jsonrpc_fill_pollfds(struct pollfd *fds, int max); + +/* Accept new clients and drain every readable request line. Non-blocking. */ +void gc570d_jsonrpc_service(void); + +/* Bind the pipeline snapshot callback (implemented in gc570d-userspace.c). */ +void gc570d_jsonrpc_set_stats_fn(void (*fn)(struct gc570d_jsonrpc_stats *out)); + +/* Bind the active device pointer; re-arm after a hardware re-init. */ +void gc570d_jsonrpc_set_dev(struct gc570d_dev *gc); + +#endif /* GC570D_JSONRPC_H */ diff --git a/tools/opengc570d-userd/src/gc570d-led.c b/tools/opengc570d-userd/src/gc570d-led.c index 009c1a8..1ee7c9b 100644 --- a/tools/opengc570d-userd/src/gc570d-led.c +++ b/tools/opengc570d-userd/src/gc570d-led.c @@ -308,63 +308,98 @@ static int gc570d_led_set_blocking(struct led_classdev *led_cdev, return ret; } -static ssize_t gc570d_led_effect_show(struct device *dev, - struct device_attribute *attr, char *buf) +/* + * Set a static RGB colour, cancelling any breathing/colorful animation. + * Public entry point for the JSON-RPC control plane. + */ +int gc570d_led_set_rgb(struct gc570d_dev *gc, u8 red, u8 green, u8 blue) { - struct led_classdev *led_cdev = dev_get_drvdata(dev); - struct gc570d_dev *gc = gc570d_led_to_gc(led_cdev); - bool breathing; - bool colorful; + int ret; + + if (!gc) + return -EINVAL; + if (READ_ONCE(gc->led_removing)) + return -ENODEV; + + cancel_delayed_work_sync(&gc->led_work); mutex_lock(&gc->led_lock); - breathing = gc->led_breathing; - colorful = gc->led_colorful; - mutex_unlock(&gc->led_lock); + if (gc->led_removing) { + ret = -ENODEV; + goto out_unlock; + } - return sysfs_emit(buf, "%s\n", - colorful ? "colorful" : - breathing ? "breathing" : "static"); + gc->led_breathing = false; + gc->led_colorful = false; + gc->led_subleds[0].brightness = red; + gc->led_subleds[1].brightness = green; + gc->led_subleds[2].brightness = blue; + + ret = gc570d_led_hw_init(gc); + if (!ret) + ret = gc570d_led_hw_set_rgb(gc, red, green, blue); + +out_unlock: + mutex_unlock(&gc->led_lock); + return ret; } -static ssize_t gc570d_led_effect_store(struct device *dev, - struct device_attribute *attr, - const char *buf, size_t count) +/* + * Select an LED effect by name: "off", "static", "breathing", "colorful". + * The legacy spellings accepted by the sysfs attribute ("breathe", + * "colorful-breathing") and the hub's "rainbow" alias are handled too. + */ +int gc570d_led_set_effect(struct gc570d_dev *gc, const char *effect) { - struct led_classdev *led_cdev = dev_get_drvdata(dev); - struct gc570d_dev *gc = gc570d_led_to_gc(led_cdev); bool breathing; bool colorful; + bool off = false; int ret; - if (sysfs_streq(buf, "colorful") || - sysfs_streq(buf, "colorful-breathing")) { + if (!gc || !effect) + return -EINVAL; + + if (strcmp(effect, "colorful") == 0 || + strcmp(effect, "colorful-breathing") == 0 || + strcmp(effect, "rainbow") == 0) { breathing = false; colorful = true; - } else if (sysfs_streq(buf, "breathing") || - sysfs_streq(buf, "breathe")) { + } else if (strcmp(effect, "breathing") == 0 || + strcmp(effect, "breathe") == 0) { breathing = true; colorful = false; - } else if (sysfs_streq(buf, "static")) { + } else if (strcmp(effect, "static") == 0) { + breathing = false; + colorful = false; + } else if (strcmp(effect, "off") == 0) { breathing = false; colorful = false; + off = true; } else { return -EINVAL; } - /* FIX: Check the removing flag before attempting to cancel work to prevent unregister deadlocks */ if (READ_ONCE(gc->led_removing)) return -ENODEV; cancel_delayed_work_sync(&gc->led_work); - + mutex_lock(&gc->led_lock); if (gc->led_removing) { ret = -ENODEV; goto out_unlock; } + if (off) { + gc->led_breathing = false; + gc->led_colorful = false; + gc->led_mcdev.led_cdev.brightness = LED_OFF; + ret = gc570d_led_shutdown(gc); + goto out_unlock; + } + ret = led_mc_calc_color_components(&gc->led_mcdev, - led_cdev->brightness); + gc->led_mcdev.led_cdev.brightness); if (!ret) ret = gc570d_led_hw_init(gc); if (ret) @@ -376,7 +411,7 @@ static ssize_t gc570d_led_effect_store(struct device *dev, gc->led_step = 1; gc->led_descending = false; gc->led_palette_index = 0; - /* FIX: Route slow blocking I2C operations to a dedicated system_unbound_wq */ + /* Route slow blocking I2C operations to a dedicated workqueue. */ mod_delayed_work(system_unbound_wq, &gc->led_work, 0); } else { ret = gc570d_led_hw_set_rgb(gc, @@ -387,7 +422,47 @@ static ssize_t gc570d_led_effect_store(struct device *dev, out_unlock: mutex_unlock(&gc->led_lock); - return ret ? ret : count; + return ret; +} + +static ssize_t gc570d_led_effect_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct gc570d_dev *gc = gc570d_led_to_gc(led_cdev); + bool breathing; + bool colorful; + + mutex_lock(&gc->led_lock); + breathing = gc->led_breathing; + colorful = gc->led_colorful; + mutex_unlock(&gc->led_lock); + + return sysfs_emit(buf, "%s\n", + colorful ? "colorful" : + breathing ? "breathing" : "static"); +} + +static ssize_t gc570d_led_effect_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct gc570d_dev *gc = gc570d_led_to_gc(led_cdev); + char effect[24]; + size_t len = count; + int ret; + + while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r' || + buf[len - 1] == ' ' || buf[len - 1] == '\t')) + len--; + if (len == 0 || len >= sizeof(effect)) + return -EINVAL; + memcpy(effect, buf, len); + effect[len] = '\0'; + + ret = gc570d_led_set_effect(gc, effect); + return ret ? ret : (ssize_t)count; } static DEVICE_ATTR(effect, 0644, gc570d_led_effect_show, diff --git a/tools/opengc570d-userd/src/gc570d-userspace.c b/tools/opengc570d-userd/src/gc570d-userspace.c index cf68fb0..7dff800 100644 --- a/tools/opengc570d-userd/src/gc570d-userspace.c +++ b/tools/opengc570d-userd/src/gc570d-userspace.c @@ -10,6 +10,12 @@ #include "gc570d.h" #include "gc570d-shm.h" #include "gc570d_everlive.h" +#include "everlive_card.h" +#include "gc570d-jsonrpc.h" + +#ifndef EVERLIVE_VERSION +#define EVERLIVE_VERSION "1.5.0" +#endif #include #include @@ -967,6 +973,29 @@ void debugfs_remove_recursive(struct dentry *dentry) g_diag_count = 0; } +int gc570d_user_diag_action(const char *name, const char *value) +{ + if (!name || !*name || !value) + return -EINVAL; + + for (size_t i = 0; i < g_diag_count; i++) { + struct file f; + loff_t pos = 0; + ssize_t r; + + if (strcmp(g_diag_entries[i].name, name) != 0 || + !g_diag_entries[i].fops->write) + continue; + memset(&f, 0, sizeof(f)); + f.private_data = g_diag_entries[i].data; + r = g_diag_entries[i].fops->write(&f, value, strlen(value), + &pos); + return (int)r; + } + + return -ENOENT; +} + int led_classdev_multicolor_register(struct device *parent, struct led_classdev_mc *mcled_cdev) { (void)parent; @@ -2297,6 +2326,28 @@ void snd_card_free(struct snd_card *card) free(card); } +/* + * Snapshot for the JSON-RPC status.get handler. Monotonic counters are read + * without the stream locks: they only ever advance, and a torn sample would + * at worst under-report a frame count for one poll interval. + */ +void gc570d_user_pipeline_stats(struct gc570d_jsonrpc_stats *out) +{ + if (!out) + return; + + memset(out, 0, sizeof(*out)); + for (int i = 0; i < 2; i++) { + out->video_seq[i] = g_vstreams[i].latest_seq; + out->audio_periods[i] = g_astreams[i].periods_published; + out->v4l2_configured[i] = gc570d_everlive_v4l2(i)[0] != '\0'; + } + if (g_gc) { + out->irq_dma0 = (uint64_t)atomic64_read(&g_gc->irq_dma0); + out->irq_dma1 = (uint64_t)atomic64_read(&g_gc->irq_dma1); + } +} + /* ========================================================================= * 5. UNIX Control & Diagnostic Socket (/run/user/$UID/gc570d/control.sock) * ========================================================================= */ @@ -2404,21 +2455,14 @@ static void gc570d_handle_ctl_client(int client_fd) } else if (strncmp(cmd, "action ", 7) == 0) { char name[64] = { 0 }; char val[64] = "1"; + int r; + sscanf(cmd + 7, "%63s %63s", name, val); - bool found = false; - for (size_t i = 0; i < g_diag_count; i++) { - if (strcmp(g_diag_entries[i].name, name) == 0 && - g_diag_entries[i].fops->write) { - struct file f = { .private_data = g_diag_entries[i].data }; - loff_t pos = 0; - ssize_t r = g_diag_entries[i].fops->write(&f, val, strlen(val), &pos); - fprintf(fp, "action %s(%s) -> %zd\n", name, val, r); - found = true; - break; - } - } - if (!found) + r = gc570d_user_diag_action(name, val); + if (r == -ENOENT) fprintf(fp, "Unknown or read-only action node: %s\n", name); + else + fprintf(fp, "action %s(%s) -> %d\n", name, val, r); } else if (strncmp(cmd, "led ", 4) == 0) { const char *mode = cmd + 4; if (strcmp(mode, "off") == 0 || strcmp(mode, "0") == 0) { @@ -2542,53 +2586,24 @@ static void gc570d_ctl_server_shutdown(void) * 6. VFIO-PCI Setup & Main Daemon Loop * ========================================================================= */ -static int read_sysfs_hex(const char *dir, const char *attr, uint32_t *out) -{ - char path[512], buf[64]; - snprintf(path, sizeof(path), "%s/%s", dir, attr); - FILE *fp = fopen(path, "r"); - if (!fp) - return -errno; - if (!fgets(buf, sizeof(buf), fp)) { - fclose(fp); - return -EIO; - } - fclose(fp); - *out = (uint32_t)strtoul(buf, NULL, 0); - return 0; -} - +/* + * Card discovery lives in libeverlive-core. 1461:0054 is shared with the + * GC573/GC555, so only a descriptor positively identified as "gc570d" + * (subsystem 1461:5700) is accepted. A missing subsystem file fails safe. + */ static int find_gc570d_pci_bdf(char *bdf_out, size_t bdf_size) { - DIR *d = opendir("/sys/bus/pci/devices"); - if (!d) - return -errno; - struct dirent *ent; - while ((ent = readdir(d)) != NULL) { - if (ent->d_name[0] == '.') + struct everlive_device cards[8]; + int count; + + count = everlive_detect_installed_cards(cards, ARRAY_SIZE(cards), NULL); + for (int i = 0; i < count; i++) { + if (strcmp(cards[i].desc->model_slug, "gc570d") != 0) continue; - char dir[512]; - uint32_t vendor = 0, device = 0, subvend = 0, subdev = 0; - snprintf(dir, sizeof(dir), "/sys/bus/pci/devices/%s", ent->d_name); - if (read_sysfs_hex(dir, "vendor", &vendor) == 0 && - read_sysfs_hex(dir, "device", &device) == 0 && - vendor == GC570D_VENDOR_ID && device == GC570D_DEVICE_ID) { - /* - * 1461:0054 is shared with other AVerMedia cards - * (GC573/GC555). Only the DUO exposes subsystem - * 1461:5700; skip anything we cannot positively identify. - */ - if (read_sysfs_hex(dir, "subsystem_vendor", &subvend) < 0 || - read_sysfs_hex(dir, "subsystem_device", &subdev) < 0 || - subvend != GC570D_SUBSYSTEM_VENDOR_ID || - subdev != GC570D_SUBSYSTEM_ID) - continue; - strscpy(bdf_out, ent->d_name, bdf_size); - closedir(d); - return 0; - } + strscpy(bdf_out, cards[i].pci_bdf, bdf_size); + return 0; } - closedir(d); + return -ENODEV; } @@ -3170,6 +3185,17 @@ int main(int argc, char **argv) "[gc570d WARN] Failed to start EverLive frontend: %d\n", ret); + gc570d_jsonrpc_set_stats_fn(gc570d_user_pipeline_stats); + gc570d_jsonrpc_set_dev(g_gc); + if (gc570d_jsonrpc_init(EVERLIVE_VERSION) < 0) { + fprintf(stderr, + "[gc570d WARN] JSON-RPC control socket unavailable; continuing without remote control\n"); + } else { + fprintf(stderr, + "[gc570d INFO] JSON-RPC control socket: %s\n", + gc570d_jsonrpc_socket_path()); + } + struct timespec ts_mono_prev = { 0 }, ts_boot_prev = { 0 }; clock_gettime(CLOCK_MONOTONIC, &ts_mono_prev); clock_gettime(CLOCK_BOOTTIME, &ts_boot_prev); @@ -3182,16 +3208,43 @@ int main(int argc, char **argv) while (!atomic_load(&g_daemon_quit) && !gc570d_everlive_quit_requested()) { - struct pollfd pfd = { - .fd = g_ctl_listen_fd, - .events = POLLIN, - }; - int pr = poll(&pfd, g_ctl_listen_fd >= 0 ? 1 : 0, 250); - if (pr > 0 && (pfd.revents & POLLIN)) { - int cfd = accept4(g_ctl_listen_fd, NULL, NULL, SOCK_CLOEXEC); - if (cfd >= 0) { - gc570d_handle_ctl_client(cfd); - close(cfd); + struct pollfd pfd[1 + GC570D_JSONRPC_MAX_CLIENTS + 1]; + int nfds = 0; + int rpc_base; + int pr; + + if (g_ctl_listen_fd >= 0) { + pfd[nfds].fd = g_ctl_listen_fd; + pfd[nfds].events = POLLIN; + nfds++; + } + rpc_base = nfds; + nfds += gc570d_jsonrpc_fill_pollfds(&pfd[nfds], + (int)(ARRAY_SIZE(pfd) - (size_t)nfds)); + + pr = poll(pfd, (nfds_t)nfds, 250); + + if (pr > 0) { + bool rpc_ready = false; + + for (int i = rpc_base; i < nfds; i++) { + if (pfd[i].revents) { + rpc_ready = true; + break; + } + } + if (rpc_ready) + gc570d_jsonrpc_service(); + + if (g_ctl_listen_fd >= 0 && + (pfd[0].revents & POLLIN)) { + int cfd = accept4(g_ctl_listen_fd, NULL, NULL, + SOCK_CLOEXEC); + + if (cfd >= 0) { + gc570d_handle_ctl_client(cfd); + close(cfd); + } } } @@ -3239,6 +3292,7 @@ int main(int argc, char **argv) if (g_reinit_requested && !atomic_load(&g_daemon_quit)) { fprintf(stderr, "[gc570d INFO] Re-initializing GC570D hardware pipeline in place...\n"); gc570d_everlive_set_dev(NULL); + gc570d_jsonrpc_set_dev(NULL); drv->remove(&g_pdev); probed = false; g_gc = NULL; @@ -3249,6 +3303,7 @@ int main(int argc, char **argv) g_gc = pci_get_drvdata(&g_pdev); probed = true; gc570d_everlive_set_dev(g_gc); + gc570d_jsonrpc_set_dev(g_gc); gc570d_start_user_video_streams(); gc570d_start_user_audio_streams(); clock_gettime(CLOCK_MONOTONIC, &ts_mono_prev); @@ -3266,6 +3321,7 @@ int main(int argc, char **argv) fprintf(stderr, "[gc570d INFO] Shutting down userspace driver...\n"); gc570d_everlive_stop(); + gc570d_jsonrpc_shutdown(); gc570d_ctl_server_shutdown(); #ifdef HAVE_LIBPIPEWIRE gc570d_pipewire_shutdown(); diff --git a/tools/opengc570d-userd/src/gc570d.h b/tools/opengc570d-userd/src/gc570d.h index d81c3fa..2ae3081 100644 --- a/tools/opengc570d-userd/src/gc570d.h +++ b/tools/opengc570d-userd/src/gc570d.h @@ -561,6 +561,8 @@ int gc570d_led_register(struct gc570d_dev *gc); void gc570d_led_unregister(struct gc570d_dev *gc); void gc570d_led_suspend(struct gc570d_dev *gc); int gc570d_led_resume(struct gc570d_dev *gc); +int gc570d_led_set_rgb(struct gc570d_dev *gc, u8 red, u8 green, u8 blue); +int gc570d_led_set_effect(struct gc570d_dev *gc, const char *effect); void gc570d_reset_resume_state(struct gc570d_dev *gc); int gc570d_receiver_read8(struct gc570d_dev *gc, const struct gc570d_i2c_bus *bus, @@ -582,6 +584,12 @@ int gc570d_receiver_write8(struct gc570d_dev *gc, * capture path is running. */ bool gc570d_subsystem_busy(struct gc570d_dev *gc); +/* + * Userspace shim: invoke a registered debugfs-style action node by name + * (for example "it68051_hpd" or "splitter_windows_bridge_edid"). + * Returns the underlying write() result (>= 0), or -ENOENT/-EINVAL. + */ +int gc570d_user_diag_action(const char *name, const char *value); void gc570d_reset_audio_channel(struct gc570d_dev *gc, unsigned int channel); void gc570d_reset_channel(struct gc570d_dev *gc, unsigned int channel); diff --git a/tools/opengc570d-userd/src/gc570d_everlive.c b/tools/opengc570d-userd/src/gc570d_everlive.c index 9d4712d..6873d9d 100644 --- a/tools/opengc570d-userd/src/gc570d_everlive.c +++ b/tools/opengc570d-userd/src/gc570d_everlive.c @@ -1410,3 +1410,104 @@ void gc570d_everlive_push_audio(int port, const uint8_t *pcm, size_t bytes) (void)bytes; #endif } + +/* ------------------------------------------------------------------ */ +/* Read-only accessors for the JSON-RPC control plane */ +/* ------------------------------------------------------------------ */ + +static const char *everlive_osd_pos_name(int pos) +{ + switch (pos) { + case EVERLIVE_OSD_BR: + return "br"; + case EVERLIVE_OSD_BL: + return "bl"; + case EVERLIVE_OSD_TL: + return "tl"; + case EVERLIVE_OSD_TR: + default: + return "tr"; + } +} + +static const char *everlive_aspect_name(int aspect) +{ + switch (aspect) { + case EVERLIVE_ASPECT_16_9: + return "16:9"; + case EVERLIVE_ASPECT_4_3: + return "4:3"; + case EVERLIVE_ASPECT_STRETCH: + return "stretch"; + case EVERLIVE_ASPECT_AUTO: + default: + return "auto"; + } +} + +static const char *everlive_scale_name(int scale) +{ + return scale == EVERLIVE_SCALE_INTEGER ? "integer" : "fit"; +} + +bool gc570d_everlive_running(void) +{ + return atomic_load_explicit(&g_everlive.running, __ATOMIC_RELAXED); +} + +bool gc570d_everlive_headless(void) +{ + if (!atomic_load_explicit(&g_everlive.started, __ATOMIC_RELAXED)) + return true; + return READ_ONCE(g_everlive.opt.headless); +} + +int gc570d_everlive_port(void) +{ + return atomic_load_explicit(&g_everlive.port, __ATOMIC_RELAXED); +} + +bool gc570d_everlive_pip(void) +{ + return atomic_load_explicit(&g_everlive.pip, __ATOMIC_RELAXED); +} + +bool gc570d_everlive_osd(void) +{ + return atomic_load_explicit(&g_everlive.osd, __ATOMIC_RELAXED); +} + +const char *gc570d_everlive_osd_pos(void) +{ + return everlive_osd_pos_name( + atomic_load_explicit(&g_everlive.osd_pos, __ATOMIC_RELAXED)); +} + +const char *gc570d_everlive_aspect(void) +{ + return everlive_aspect_name( + atomic_load_explicit(&g_everlive.aspect, __ATOMIC_RELAXED)); +} + +const char *gc570d_everlive_scale(void) +{ + return everlive_scale_name( + atomic_load_explicit(&g_everlive.scale, __ATOMIC_RELAXED)); +} + +int gc570d_everlive_volume(void) +{ + return atomic_load_explicit(&g_everlive.volume_pct, __ATOMIC_RELAXED); +} + +bool gc570d_everlive_muted(void) +{ + return atomic_load_explicit(&g_everlive.muted, __ATOMIC_RELAXED); +} + +const char *gc570d_everlive_v4l2(int port) +{ + if (port < 0 || port > 1) + return ""; + return g_everlive.opt.v4l2[port] ? g_everlive.opt.v4l2[port] : ""; +} diff --git a/tools/opengc570d-userd/src/gc570d_everlive.h b/tools/opengc570d-userd/src/gc570d_everlive.h index df94e6a..e5e4401 100644 --- a/tools/opengc570d-userd/src/gc570d_everlive.h +++ b/tools/opengc570d-userd/src/gc570d_everlive.h @@ -52,4 +52,17 @@ void gc570d_everlive_set_volume(int volume_pct); void gc570d_everlive_set_mute(bool muted); void gc570d_everlive_push_audio(int port, const uint8_t *pcm, size_t bytes); +/* Read-only accessors for the control plane (safe before start). */ +bool gc570d_everlive_running(void); +bool gc570d_everlive_headless(void); +int gc570d_everlive_port(void); +bool gc570d_everlive_pip(void); +bool gc570d_everlive_osd(void); +const char *gc570d_everlive_osd_pos(void); +const char *gc570d_everlive_aspect(void); +const char *gc570d_everlive_scale(void); +int gc570d_everlive_volume(void); +bool gc570d_everlive_muted(void); +const char *gc570d_everlive_v4l2(int port); + #endif /* GC570D_EVERLIVE_H */ From b17494d8a899c8a379ac1bec518aec902a6c581f Mon Sep 17 00:00:00 2001 From: PolygonPrincess Date: Wed, 11 Nov 2026 05:08:30 -0500 Subject: [PATCH 6/8] test: unify the EverLive test suite around libeverlive-core Part 4/5 of the modularization plan (see .opencode/plan/everlive-modularization/04-tests.md). One tests/ tree now covers the shared library, the GC570D engine, and the GC573 framegen module, and everything is reachable through one command. - Rename tests/test_ipc_json.c to tests/test_json_rpc.c now that the JSON scanners live in libeverlive-core. Update its banner and keep every assertion as the regression net for the moved code. - Rewrite tests/Makefile: * core tests (card registry, frame conversion, ctl protocol, JSON-RPC) link libeverlive_core.a instead of recompiling its sources * GC570D EDID/config tests still compile the engine modules directly with -DGC570D_USERSPACE because they provide their own kernel-shim stubs and must not link the archive * framegen compiles only gc573_user_framegen.c with -pthread -lm * add -MMD -MP dependency generation for single-source rules (multi- source link rules omit it because Clang rejects -MMD together with -o) * group the output by component and fail on the first failing suite * replace the duplicated/flag-appending asan target with a recipe that cleans, rebuilds with ASan/UBSan, and runs the suites - run_tests.sh: support --asan and describe the full suite instead of the GC570D-only banner. - Retire the in-engine gc573_framegen_selftest: the GC573 Makefile now forwards `test`/`selftest` to tests/, adds a `framegen-selftest` shortcut, and no longer builds or cleans a private binary. - Ignore all test binaries (including test_json_rpc) and drop the stale gc573_framegen_selftest entry. Verified: GCC, Clang, and ASan/UBSan runs pass; 7 suites / 164 assertions; `./tests/run_tests.sh` and `--asan` both work; the legacy per-engine selftest targets still forward successfully. --- .gitignore | 9 +-- tests/Makefile | 83 ++++++++++++++-------- tests/run_tests.sh | 24 +++++-- tests/{test_ipc_json.c => test_json_rpc.c} | 6 +- tools/opengc573-userd/.gitignore | 1 - tools/opengc573-userd/Makefile | 17 ++--- 6 files changed, 88 insertions(+), 52 deletions(-) rename tests/{test_ipc_json.c => test_json_rpc.c} (98%) diff --git a/.gitignore b/.gitignore index e116449..d9d6512 100644 --- a/.gitignore +++ b/.gitignore @@ -43,7 +43,11 @@ tools/opengc570d-userd/libgc570d-v4l2.so # Test binaries tests/test_edid tests/test_config -tools/opengc573-userd/gc573_framegen_selftest +tests/test_card_registry +tests/test_frame_convert +tests/test_ctl_proto +tests/test_json_rpc +tests/test_framegen # libeverlive-core build outputs tools/libeverlive-core/*.a @@ -67,6 +71,3 @@ refactor/ # Chronik / Notizen crhonic.md chronic.md - -# Framegen selftest (lives in tests/ after the source move) -tests/test_framegen diff --git a/tests/Makefile b/tests/Makefile index a93fdeb..149ac2f 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -1,56 +1,81 @@ # SPDX-License-Identifier: GPL-2.0-only +# +# EverLive unit tests: libeverlive-core, GC570D, and GC573. +# Core tests link libeverlive_core.a; engine tests compile only the modules +# they exercise. + CC ?= gcc -CFLAGS ?= -O2 -g -Wall -Wextra -Wno-unused-parameter -Wno-sign-compare -std=gnu11 -D_GNU_SOURCE -INCLUDES := -I../tools/opengc570d-userd/src -I../tools/opengc570d-userd -I../tools/libeverlive-core -I../tools/opengc573-userd/src -DEFS := -DGC570D_USERSPACE +CFLAGS ?= -O2 -g -Wall -Wextra -Wno-unused-parameter -Wno-sign-compare \ + -std=gnu11 -D_GNU_SOURCE +DEPFLAGS := -MMD -MP + +CORE_DIR := ../tools/libeverlive-core +GC573_DIR := ../tools/opengc573-userd +GC570D_DIR := ../tools/opengc570d-userd +GC570D_SRC := $(GC570D_DIR)/src + +INCLUDES := -I$(CORE_DIR) -I$(GC570D_SRC) -I$(GC570D_DIR) -I$(GC573_DIR)/src +DEFS_GC570D := -DGC570D_USERSPACE -GC570D_DIR := ../tools/opengc570d-userd/src -CORE_DIR := ../tools/libeverlive-core -GC573_DIR := ../tools/opengc573-userd CORE_LIB := $(CORE_DIR)/libeverlive_core.a +SANITIZE := -fsanitize=address,undefined -fno-omit-frame-pointer -TARGETS := test_edid test_config test_card_registry test_frame_convert test_ctl_proto test_ipc_json test_framegen +TARGETS := test_card_registry test_frame_convert test_ctl_proto test_json_rpc \ + test_edid test_config test_framegen all: $(TARGETS) -test_edid: test_edid.c $(GC570D_DIR)/gc570d-receiver.c $(GC570D_DIR)/gc570d-splitter.c - $(CC) $(CFLAGS) $(INCLUDES) $(DEFS) -o $@ $^ - -test_config: test_config.c $(GC570D_DIR)/gc570d-config.c - $(CC) $(CFLAGS) $(INCLUDES) $(DEFS) -o $@ $^ +$(CORE_LIB): + $(MAKE) -C $(CORE_DIR) -test_card_registry: test_card_registry.c $(CORE_DIR)/everlive_card.c - $(CC) $(CFLAGS) $(INCLUDES) -o $@ $^ +# --- libeverlive-core --------------------------------------------------- +test_card_registry: test_card_registry.c $(CORE_LIB) + $(CC) $(CFLAGS) $(INCLUDES) $(DEPFLAGS) -o $@ $< $(CORE_LIB) -test_frame_convert: test_frame_convert.c $(CORE_DIR)/everlive_frame.c - $(CC) $(CFLAGS) $(INCLUDES) -o $@ $^ +test_frame_convert: test_frame_convert.c $(CORE_LIB) + $(CC) $(CFLAGS) $(INCLUDES) $(DEPFLAGS) -o $@ $< $(CORE_LIB) test_ctl_proto: test_ctl_proto.c $(CORE_LIB) - $(CC) $(CFLAGS) $(INCLUDES) -o $@ test_ctl_proto.c $(CORE_LIB) + $(CC) $(CFLAGS) $(INCLUDES) $(DEPFLAGS) -o $@ $< $(CORE_LIB) -asan: CFLAGS += -fsanitize=address,undefined -fno-omit-frame-pointer -asan: all +test_json_rpc: test_json_rpc.c $(CORE_LIB) + $(CC) $(CFLAGS) $(INCLUDES) $(DEPFLAGS) -o $@ $< $(CORE_LIB) -test_ipc_json: test_ipc_json.c $(CORE_LIB) - $(CC) $(CFLAGS) $(INCLUDES) -o $@ $< $(CORE_LIB) -pthread +# --- GC570D ------------------------------------------------------------- +# These two provide their own kernel-shim stubs, so they must compile the +# engine modules directly and must NOT link the core archive. +test_edid: test_edid.c $(GC570D_SRC)/gc570d-receiver.c \ + $(GC570D_SRC)/gc570d-splitter.c + $(CC) $(CFLAGS) $(INCLUDES) $(DEFS_GC570D) -o $@ $^ -$(CORE_LIB): - $(MAKE) -C $(CORE_DIR) +test_config: test_config.c $(GC570D_SRC)/gc570d-config.c + $(CC) $(CFLAGS) $(INCLUDES) $(DEFS_GC570D) -o $@ $^ +# --- GC573 -------------------------------------------------------------- test_framegen: test_framegen.c $(GC573_DIR)/src/gc573_user_framegen.c $(CC) $(CFLAGS) $(INCLUDES) -o $@ $^ -pthread -lm +-include $(TARGETS:=.d) + test: all - @echo "Running GC570D & GC573 unit tests..." - @./test_edid - @./test_config + @echo "=== libeverlive-core ===" @./test_card_registry @./test_frame_convert @./test_ctl_proto - @./test_ipc_json + @./test_json_rpc + @echo "=== GC570D (Live Gamer DUO) ===" + @./test_edid + @./test_config + @echo "=== GC573 (Live Gamer 4K) ===" @./test_framegen + @echo "=== All EverLive tests passed ===" + +asan: + $(MAKE) clean + $(MAKE) CFLAGS="$(CFLAGS) $(SANITIZE)" all + $(MAKE) CFLAGS="$(CFLAGS) $(SANITIZE)" test clean: - rm -f $(TARGETS) + rm -f $(TARGETS) $(TARGETS:=.d) -.PHONY: all test clean asan +.PHONY: all test asan clean diff --git a/tests/run_tests.sh b/tests/run_tests.sh index 4224ff5..a891535 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -1,11 +1,25 @@ #!/usr/bin/env bash # SPDX-License-Identifier: GPL-2.0-only +# +# Build and run the EverLive unit tests (libeverlive-core, GC570D, GC573). set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +MODE="${1:-}" -echo "=== Building and running GC570D Unit Tests ===" -make -C "${SCRIPT_DIR}" clean -make -C "${SCRIPT_DIR}" test -echo "=== All tests completed successfully! ===" - +case "${MODE}" in + "") + make -C "${SCRIPT_DIR}" clean + make -C "${SCRIPT_DIR}" test + ;; + --asan) + make -C "${SCRIPT_DIR}" asan + ;; + -h|--help) + echo "Usage: $0 [--asan]" + ;; + *) + echo "Unknown option: ${MODE}" >&2 + exit 2 + ;; +esac diff --git a/tests/test_ipc_json.c b/tests/test_json_rpc.c similarity index 98% rename from tests/test_ipc_json.c rename to tests/test_json_rpc.c index 9459d84..984094b 100644 --- a/tests/test_ipc_json.c +++ b/tests/test_json_rpc.c @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: GPL-2.0-only */ /* - * test_ipc_json.c — Unit tests for the libeverlive-core JSON scanners and RPC formatters + * test_json_rpc.c — Unit tests for the libeverlive-core JSON scanners and RPC formatters */ #include @@ -221,7 +221,7 @@ static void test_json_negative_edge_cases(void) int main(void) { printf("==================================================\n"); - printf(" GC573 IPC JSON Scanner Test Suite\n"); + printf(" libeverlive-core JSON-RPC Scanner Tests \n"); printf("==================================================\n"); test_json_get_string(); @@ -235,7 +235,7 @@ int main(void) printf("==================================================\n"); if (g_failures == 0) { - printf(" ALL GC573 IPC JSON TESTS PASSED!\n"); + printf(" ALL JSON-RPC SCANNER TESTS PASSED!\n"); return 0; } else { printf(" %d TESTS FAILED!\n", g_failures); diff --git a/tools/opengc573-userd/.gitignore b/tools/opengc573-userd/.gitignore index 4c18867..351c471 100644 --- a/tools/opengc573-userd/.gitignore +++ b/tools/opengc573-userd/.gitignore @@ -4,4 +4,3 @@ gc573_user_probe gc573_user_streamer gc573_user_diag everlive_userd -gc573_framegen_selftest diff --git a/tools/opengc573-userd/Makefile b/tools/opengc573-userd/Makefile index 11be2dd..e8bd94d 100644 --- a/tools/opengc573-userd/Makefile +++ b/tools/opengc573-userd/Makefile @@ -85,20 +85,17 @@ $(STREAM_OBJS): src/%.o: src/%.c src/gc573_user_streamer.o: src/gc573_user_streamer.c $(CC) $(BASE_CFLAGS) $(SDL_CFLAGS) $(AV_CFLAGS) $(PW_CFLAGS) $(DEPFLAGS) -c -o $@ $< -# Headless framegen self-test. Task 4 moves this fully into tests/. -gc573_framegen_selftest: src/gc573_user_framegen.o ../../tests/test_framegen.c - $(CC) $(BASE_CFLAGS) -o $@ ../../tests/test_framegen.c \ - src/gc573_user_framegen.o $(LDFLAGS) -lm - -selftest: gc573_framegen_selftest - ./gc573_framegen_selftest - -test: all +# The framegen cadence test now lives in tests/; forward the legacy target. +test selftest: $(MAKE) -C ../../tests test +framegen-selftest: + $(MAKE) -C ../../tests test_framegen + ../../tests/test_framegen + -include $(HW_OBJS:.o=.d) $(STREAM_OBJS:.o=.d) $(ENTRY_OBJS:.o=.d) clean: - rm -f src/*.o src/*.d $(TARGETS) gc573_framegen_selftest + rm -f src/*.o src/*.d $(TARGETS) .PHONY: all test selftest clean From 594ce97ff70d5909222913002d695c921821ea62 Mon Sep 17 00:00:00 2001 From: PolygonPrincess Date: Wed, 11 Nov 2026 05:19:07 -0500 Subject: [PATCH 7/8] build: add a top-level Makefile and install the shared core library Part 5/5 of the modularization plan (see .opencode/plan/everlive-modularization/05-build-install-scripts.md). One entry point now builds, tests, installs, and cleans every module, and the installers own the shared library instead of leaving it in the source tree. Build - New root Makefile: `all` builds libeverlive-core first, then both engines; `test`, `asan`, `clean`, `install`, and `uninstall` fan out to the module Makefiles with the jobserver and PREFIX/DESTDIR propagated. - build.sh becomes a thin wrapper around the root make and keeps the optional legacy renderer block. Install / uninstall - install.sh builds through the root Makefile, installs the core library, everlive-ctl, everlive-service, public headers, and the PREFIX-templated everlive.service unit, and copies vfio_pci_bind.sh where the service script expects it. - uninstall.sh removes the core artifacts, stale versioned libraries, everlive-ctl/everlive-service, the systemd unit, vfio_pci_bind.sh, and leftover runtime sockets (everlive.sock and .lock). Launchers - play.sh prefers `everlive-ctl detect` with the sysfs loop as fallback, rebuilds the core and the selected engine using real target checks (`make -q `) instead of the always-dirty phony default, and keeps the gc573 default when no card is found. - everlive-service.sh shares the same detection path and now finds the GC570D bind helper in libexec as installed by the engine Makefile. CI and docs - CI uses the root Makefile for GCC and Clang, runs the unified test suite and sanitizer target, exercises the framegen forwarding target, verifies everlive-service.sh syntax, and cleans at the end. - README documents the from-source build/test commands and the shared core; CHANGELOG gains an Unreleased section for the modularization work. Verified: root build/test/asan pass; staged `make install` and `make uninstall` with DESTDIR round-trip exactly the expected files; the everlive.service ExecStart honours PREFIX; all shell scripts pass `bash -n`; the service script detected and launched the GC570D engine with the JSON-RPC socket bound. --- .github/workflows/ci.yml | 30 +++++---- CHANGELOG.md | 18 +++++ Makefile | 46 +++++++++++++ README.md | 12 +++- build.sh | 22 ++----- install.sh | 24 ++++--- play.sh | 77 +++++++++++++++------- tools/libeverlive-core/everlive-service.sh | 64 ++++++++++++------ uninstall.sh | 16 ++++- 9 files changed, 229 insertions(+), 80 deletions(-) create mode 100644 Makefile diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eba00e1..a565f96 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,25 +31,26 @@ jobs: libgl-dev \ python3 - - name: Compile EverLive Userspace Engine (GCC) - run: | - make -C tools/opengc573-userd -j"$(nproc)" - make -C tools/opengc570d-userd CC=gcc -j"$(nproc)" - make -C tools/opengc573-userd clean - make -C tools/opengc570d-userd clean + - name: Compile everything (GCC) + run: make -j"$(nproc)" - - name: Compile EverLive Userspace Engine (Clang) - run: | - make -C tools/opengc573-userd CC=clang -j"$(nproc)" - make -C tools/opengc570d-userd CC=clang -j"$(nproc)" + - name: Unit tests (GCC) + run: make -C tests clean test - name: Frame generation cadence self-test - run: | - make -C tools/opengc573-userd selftest + run: make -C tools/opengc573-userd framegen-selftest - - name: OpenGC570D EDID & config unit tests + - name: Compile everything (Clang) run: | - make -C tools/opengc570d-userd selftest + make clean + make CC=clang -j"$(nproc)" + make CC=clang -C tests clean test + + - name: Sanitizer run + run: make -C tests asan + + - name: Clean + run: make clean - name: Verify Python syntax run: | @@ -66,3 +67,4 @@ jobs: bash -n tools/opengc573-userd/vfio_pci_bind.sh bash -n tools/opengc570d-userd/gc570d-vfio-bind.sh bash -n tools/opengc570d-userd/gc570d-sleep.sh + bash -n tools/libeverlive-core/everlive-service.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index dbf0ce5..43d6323 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- +## [Unreleased] + +### Added +- **Shared `libeverlive-core`:** card registry and subsystem-aware autodiscovery, YUY2 frame conversion, jsmn-backed JSON-RPC helpers, control-socket path resolution, and the `everlive-ctl` CLI. +- **Unified control plane for the GC570D:** the DUO daemon now serves the same newline-delimited JSON-RPC 2.0 protocol on `everlive.sock` (status, LED, audio, OSD, aspect/scale, input switching, HPD, EDID policy) alongside the legacy `gc570d-ctl` socket. + +### Changed +- **GC573 layout:** sources moved to `tools/opengc573-userd/src/`; the build links `libeverlive_core.a` and only applies SDL/FFmpeg/PipeWire flags to the translation units that need them. +- **Build and install:** a top-level `Makefile` orchestrates the core and both engines; `build.sh`, `install.sh`, and `uninstall.sh` now install and remove `everlive-ctl`, the core library and headers, and the universal `everlive.service` unit. +- **Launchers:** `play.sh` and `everlive-service.sh` share `everlive-ctl detect` instead of duplicating PCI ID logic. + +### Fixed +- The GC573 engine no longer claims a GC570D that shares PCI ID `1461:0054`; discovery is subsystem-aware and fails safe when the subsystem ID is unreadable. +- `everlive-ctl` connects to the live `everlive.sock` instead of the stale `everlive-experimental.sock` path. + +### Tests +- Unified `tests/` suite linking the core archive, `test_json_rpc` (renamed from `test_ipc_json`), the framegen cadence test, `make -C tests asan`, and `run_tests.sh --asan`. + ## [1.5.0] - 2026-11-10 ### Added diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..bc5904e --- /dev/null +++ b/Makefile @@ -0,0 +1,46 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# EverLive — top-level orchestration. +# Build order: libeverlive-core, then both engines, then tests. + +CORE_DIR := tools/libeverlive-core +GC573_DIR := tools/opengc573-userd +GC570D_DIR := tools/opengc570d-userd +TESTS_DIR := tests + +.PHONY: all core gc573 gc570d test check asan clean install uninstall + +all: core gc573 gc570d + +core: + +$(MAKE) -C $(CORE_DIR) + +gc573: core + +$(MAKE) -C $(GC573_DIR) + +gc570d: core + +$(MAKE) -C $(GC570D_DIR) + +test: all + +$(MAKE) -C $(TESTS_DIR) test + +check: test + +asan: all + +$(MAKE) -C $(TESTS_DIR) asan + +clean: + +$(MAKE) -C $(CORE_DIR) clean + +$(MAKE) -C $(GC573_DIR) clean + +$(MAKE) -C $(GC570D_DIR) clean + +$(MAKE) -C $(TESTS_DIR) clean + +# GC573 has no install target: its binaries, bind script, udev rule, and +# systemd wiring are placed by install.sh. Core and GC570D own their files. +install: all + +$(MAKE) -C $(CORE_DIR) install + +$(MAKE) -C $(GC570D_DIR) install + +uninstall: + +$(MAKE) -C $(CORE_DIR) uninstall + +$(MAKE) -C $(GC570D_DIR) uninstall diff --git a/README.md b/README.md index 746e7a2..1d7681b 100644 --- a/README.md +++ b/README.md @@ -251,12 +251,21 @@ sudo ./install.sh --no-loopback `--skip-loopback` is an alias, and `sudo ./install.sh --help` lists all options. Without `v4l2loopback`, `/dev/video10` is unavailable — capture the game window via PipeWire instead (see [Streaming & Recording with OBS](#-streaming--recording-with-obs)). Re-running the installer without the flag re-enables the Virtual Camera. +### Building and testing from source +```bash +make -j"$(nproc)" # libeverlive-core + both engines +make -C tests test # 7 unit-test suites +make -C tests asan # sanitizer run +``` + +`./build.sh` is a thin wrapper around the same top-level `make`. + ### What the installer does: 1. Installs the build/runtime dependencies: SDL2, FFmpeg development libraries (`libavcodec`/`libswresample`/`libavutil`), `libcap` (`setcap`), PipeWire/WirePlumber, `xcb-util-cursor`, PySide6, standard build tools, optional `lsfg-vk` frame generation layer (skip with `--no-framegen`), and `v4l2loopback` where packaged (skip with `--no-loopback`). 2. Configures udev rules (`/etc/udev/rules.d/70-everlive.rules`) and early-boot VFIO autobind (`/etc/modules-load.d/everlive-vfio.conf` + `/etc/modprobe.d/everlive-vfio.conf`) granting your local user read/write access to the card's VFIO group and binding `vfio-pci`. 3. Sets `/etc/security/limits.d/99-everlive-memlock.conf` and adds `cap_ipc_lock` capabilities so unprivileged users can pin DMA buffers required for native 4K and dual-HDMI capture. 4. Blacklists the legacy out-of-tree `open_gc573` module (`/etc/modprobe.d/blacklist-everlive-legacy.conf`) so it cannot race with `vfio-pci` at boot. -5. Compiles userspace engines: `opengc573-userd` (`gc573_user_streamer`, `everlive_userd`, `gc573_user_probe`, `gc573_user_diag`) and `opengc570d-userd` (`opengc570d-userd`, `gc570d-ctl`, `libgc570d-v4l2.so`). +5. Compiles the shared core and userspace engines through the top-level `Makefile`: `libeverlive-core` (`everlive-ctl`, `libeverlive_core.so`), `opengc573-userd` (`gc573_user_streamer`, `everlive_userd`, `gc573_user_probe`, `gc573_user_diag`) and `opengc570d-userd` (`opengc570d-userd`, `gc570d-ctl`, `libgc570d-v4l2.so`). The core library, its headers, `everlive-ctl`, `everlive-service`, and the universal `everlive.service` unit are installed system-wide. 6. Configures an isolated PipeWire audio sink (`50-everlive-capture.conf`) providing the dedicated `"EverLive Capture Audio"` source for OBS. 7. Configures the Virtual Camera: `/etc/modprobe.d/everlive-v4l2loopback.conf` (`video_nr=10,11 card_label="EverLive Capture,EverLive Capture HDMI 2" exclusive_caps=0`) and loads the module at boot via `/etc/modules-load.d/`. 8. Installs the desktop application launcher and icon. @@ -361,6 +370,7 @@ Writing clean-room Linux drivers for proprietary capture hardware requires havin | Component | Path | License | Notes | |:---|:---|:---:|:---| | **Userspace Driver & Streamer** | `tools/opengc573-userd/` | **GPL-2.0-only** | Userspace VFIO implementation. Copyright © 2026 Everlite. | +| **Shared Core Library** | `tools/libeverlive-core/` | **GPL-2.0-only** | Card registry and autodiscovery, YUY2 frame conversion, jsmn-backed JSON-RPC helpers, control-socket resolution, `everlive-ctl`, and the `everlive.service` unit. Copyright © 2026 Everlite. | | **Live Gamer DUO Engine** | `tools/opengc570d-userd/` | **GPL-2.0-only** | Derived from [`neoyagami/OpenGC570D`](https://github.com/neoyagami/OpenGC570D) with userspace VFIO-PCI + PipeWire port by [@PolygonPrincess](https://github.com/PolygonPrincess). Upstream base `4673bb9`, splitter PR #6 `484aa8d`, userspace import `afbb06e`. EverLive integration and frontend © 2026 Everlite & PolygonPrincess. | | **EverLive Console Hub** | `tools/everlive/`, `play.sh`, `install.sh` | **GPL-2.0-only** | Python/Qt hub and launcher scripts. Copyright © 2026 Everlite. | | **Legacy Kernel Module** | `legacy/open_gc573-kernel/` | **GPL-2.0-only** | Historical kernel module based on work by ostrich, extended by Everlite. | diff --git a/build.sh b/build.sh index 144d0b2..aeadf3d 100755 --- a/build.sh +++ b/build.sh @@ -5,21 +5,11 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -CORE_DIR="${SCRIPT_DIR}/tools/libeverlive-core" -GC573_DIR="${SCRIPT_DIR}/tools/opengc573-userd" -GC570D_DIR="${SCRIPT_DIR}/tools/opengc570d-userd" -echo "[*] Building EverLive Core Hardware Abstraction Library (libeverlive-core & everlive-ctl)..." -make -C "${CORE_DIR}" clean >/dev/null 2>&1 || true -make -C "${CORE_DIR}" -j"$(nproc)" - -echo "[*] Building EverLive GC573 Userspace VFIO Engine (gc573_user_streamer & everlive_userd)..." -make -C "${GC573_DIR}" clean >/dev/null 2>&1 || true -make -C "${GC573_DIR}" -j"$(nproc)" - -echo "[*] Building EverLive GC570D (Live Gamer DUO) Userspace Engine..." -make -C "${GC570D_DIR}" clean >/dev/null 2>&1 || true -make -C "${GC570D_DIR}" -j"$(nproc)" +echo "[*] Building EverLive core library (libeverlive-core & everlive-ctl)..." +echo "[*] Building GC573 (Live Gamer 4K) userspace VFIO engine..." +echo "[*] Building GC570D (Live Gamer DUO) userspace engine..." +make -C "${SCRIPT_DIR}" -j"$(nproc)" echo "[+] EverLive Userspace Engines built successfully!" @@ -28,7 +18,9 @@ if [ -f "${SCRIPT_DIR}/tools/everlive/everlive_display.c" ]; then echo "[*] Building legacy direct renderer (everlive_display)..." SDL_FLAGS="$(pkg-config --cflags --libs sdl2 2>/dev/null || sdl2-config --cflags --libs 2>/dev/null || true)" if [ -n "${SDL_FLAGS}" ]; then - gcc -O3 -o "${SCRIPT_DIR}/tools/everlive/everlive_display" "${SCRIPT_DIR}/tools/everlive/everlive_display.c" ${SDL_FLAGS} -lGL -lm -lX11 -lpthread + gcc -O3 -o "${SCRIPT_DIR}/tools/everlive/everlive_display" \ + "${SCRIPT_DIR}/tools/everlive/everlive_display.c" ${SDL_FLAGS} \ + -lGL -lm -lX11 -lpthread chmod 0755 "${SCRIPT_DIR}/tools/everlive/everlive_display" 2>/dev/null || true fi fi diff --git a/install.sh b/install.sh index 6e74b03..ae4f649 100755 --- a/install.sh +++ b/install.sh @@ -12,6 +12,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CORE_DIR="${SCRIPT_DIR}/tools/libeverlive-core" USERD_DIR="${SCRIPT_DIR}/tools/opengc573-userd" GC570D_DIR="${SCRIPT_DIR}/tools/opengc570d-userd" VIEWER_DIR="${SCRIPT_DIR}/tools/everlive" @@ -395,18 +396,25 @@ detect_and_install_deps # 3. Userspace VFIO Engine kompilieren echo -e "\n${BLUE}[2/5] Kompiliere EverLive Userspace VFIO Engine...${NC}" -make -C "${USERD_DIR}" clean >/dev/null 2>&1 || true -make -C "${USERD_DIR}" -j"$(nproc)" -echo -e "${BLUE}[*] Kompiliere GC570D (Live Gamer DUO) Userspace Engine...${NC}" -make -C "${GC570D_DIR}" clean >/dev/null 2>&1 || true -make -C "${GC570D_DIR}" -j"$(nproc)" -# Never leave build artifacts world-writable, and keep the binary setcap-safe. -chmod 0755 "${USERD_DIR}/gc573_user_streamer" "${USERD_DIR}/everlive_userd" "${USERD_DIR}/gc573_user_probe" \ - "${GC570D_DIR}/opengc570d-userd" "${GC570D_DIR}/gc570d-ctl" "${SCRIPT_DIR}/play.sh" +make -C "${SCRIPT_DIR}" -j"$(nproc)" + +# Never leave build artifacts world-writable, and keep binaries setcap-safe. +chmod 0755 "${USERD_DIR}/gc573_user_streamer" "${USERD_DIR}/everlive_userd" \ + "${USERD_DIR}/gc573_user_probe" "${CORE_DIR}/everlive-ctl" \ + "${GC570D_DIR}/opengc570d-userd" "${GC570D_DIR}/gc570d-ctl" \ + "${SCRIPT_DIR}/play.sh" chmod 0644 "${GC570D_DIR}/libgc570d-v4l2.so" 2>/dev/null || true + +# Core library, everlive-ctl, everlive-service, headers, and systemd unit. +make -C "${CORE_DIR}" install + # Install the GC570D helpers (udev rule, memlock, systemd unit, sleep hook) # through the engine's own Makefile so both installers stay in sync. make -C "${GC570D_DIR}" install + +# GC573 currently ships its bind helper from install.sh. +install -m 0755 "${USERD_DIR}/vfio_pci_bind.sh" /usr/local/bin/vfio_pci_bind.sh + setcap cap_ipc_lock=+ep "${USERD_DIR}/gc573_user_streamer" 2>/dev/null || true setcap cap_ipc_lock=+ep "${GC570D_DIR}/opengc570d-userd" 2>/dev/null || true echo -e "${GREEN}[+] Userspace VFIO Engines erfolgreich gebaut & berechtigt!${NC}" diff --git a/play.sh b/play.sh index 090b6f7..11605e2 100755 --- a/play.sh +++ b/play.sh @@ -7,32 +7,59 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CORE_DIR="${SCRIPT_DIR}/tools/libeverlive-core" +CORE_CTL="${CORE_DIR}/everlive-ctl" GC573_DIR="${SCRIPT_DIR}/tools/opengc573-userd" GC570D_DIR="${SCRIPT_DIR}/tools/opengc570d-userd" VIEWER="${SCRIPT_DIR}/tools/everlive/everlive_viewer.py" # 0. Detect the installed capture card. GC573 and GC570D share PCI device -# 1461:0054; only the subsystem device ID distinguishes them. -CARD_MODEL="gc573" +# 1461:0054; only the subsystem device ID distinguishes them. Prefer the +# core registry and fall back to sysfs when everlive-ctl is unavailable. +detect_card_sysfs() { + local dev vendor device subsystem + + CARD_MODEL="" + CARD_PCI="" + for dev in /sys/bus/pci/devices/*; do + [ -f "$dev/vendor" ] || continue + [ -f "$dev/device" ] || continue + vendor=$(cat "$dev/vendor" 2>/dev/null || true) + device=$(cat "$dev/device" 2>/dev/null || true) + if [ "$vendor" != "0x1461" ]; then + continue + fi + if [ "$device" != "0x0054" ] && [ "$device" != "0x0055" ]; then + continue + fi + CARD_PCI=$(basename "$dev") + subsystem=$(cat "$dev/subsystem_device" 2>/dev/null || true) + if [ "$device" = "0x0054" ] && [ "$subsystem" = "0x5700" ]; then + CARD_MODEL="gc570d" + else + CARD_MODEL="gc573" + fi + break + done +} + +CARD_MODEL="" CARD_PCI="" -for dev in /sys/bus/pci/devices/*; do - [ -f "$dev/vendor" ] || continue - [ -f "$dev/device" ] || continue - vendor=$(cat "$dev/vendor" 2>/dev/null || true) - device=$(cat "$dev/device" 2>/dev/null || true) - if [ "$vendor" != "0x1461" ]; then - continue - fi - if [ "$device" != "0x0054" ] && [ "$device" != "0x0055" ]; then - continue - fi - CARD_PCI=$(basename "$dev") - subsystem=$(cat "$dev/subsystem_device" 2>/dev/null || true) - if [ "$device" = "0x0054" ] && [ "$subsystem" = "0x5700" ]; then - CARD_MODEL="gc570d" - fi - break -done +if [ -x "${CORE_CTL}" ]; then + DETECT_OUT="$("${CORE_CTL}" detect 2>/dev/null || true)" + CARD_MODEL="$(printf '%s\n' "${DETECT_OUT}" \ + | sed -n 's/.*Model Slug:[[:space:]]*//p' | head -n 1)" + CARD_PCI="$(printf '%s\n' "${DETECT_OUT}" \ + | sed -n 's/.*PCI BDF:[[:space:]]*//p' | head -n 1)" + case "${CARD_MODEL}" in + gc573|gc570d) ;; + *) CARD_MODEL="" ;; + esac +fi +if [ -z "${CARD_MODEL}" ]; then + detect_card_sysfs +fi +CARD_MODEL="${CARD_MODEL:-gc573}" if [ "$CARD_MODEL" = "gc570d" ]; then USERD_DIR="${GC570D_DIR}" @@ -48,8 +75,14 @@ else LOOPBACK_LABEL_2="" fi -# 1. Ensure userspace engine binaries are built and up to date -if ! make -C "${USERD_DIR}" -q >/dev/null 2>&1; then +# 1. Ensure the core and userspace engine binaries are built and up to date +if [ ! -x "${CORE_CTL}" ] || \ + ! make -q -C "${CORE_DIR}" everlive-ctl >/dev/null 2>&1; then + echo "[*] Aktualisiere/Kompiliere EverLive Core Library..." + make -C "${CORE_DIR}" -j"$(nproc)" +fi +if [ ! -x "${STREAMER_BIN}" ] || \ + ! make -q -C "${USERD_DIR}" "$(basename "${STREAMER_BIN}")" >/dev/null 2>&1; then echo "[*] Aktualisiere/Kompiliere EverLive Userspace VFIO Engine..." make -C "${USERD_DIR}" -j"$(nproc)" fi diff --git a/tools/libeverlive-core/everlive-service.sh b/tools/libeverlive-core/everlive-service.sh index 8a63827..9a894ef 100755 --- a/tools/libeverlive-core/everlive-service.sh +++ b/tools/libeverlive-core/everlive-service.sh @@ -21,22 +21,13 @@ CARD_MODEL="" CARD_PCI="" IS_BOUND=0 -# 1. Detect installed card -if [ -x "${CTL_BIN}" ]; then - detect_out="$("${CTL_BIN}" detect 2>/dev/null || true)" - if echo "${detect_out}" | grep -q "Model Slug: gc570d"; then - CARD_MODEL="gc570d" - elif echo "${detect_out}" | grep -q "Model Slug: gc573"; then - CARD_MODEL="gc573" - fi - CARD_PCI="$(echo "${detect_out}" | grep "PCI BDF:" | head -n 1 | awk '{print $3}')" - if echo "${detect_out}" | grep -q "Driver Bound: vfio-pci"; then - IS_BOUND=1 - fi -fi +# 1. Detect the installed card. Prefer the core registry (everlive-ctl), +# then fall back to sysfs when the CLI is unavailable. +detect_card_sysfs() { + local dev vendor device subsystem -# Fallback sysfs check if everlive-ctl didn't find it -if [ -z "$CARD_MODEL" ]; then + CARD_MODEL="" + CARD_PCI="" for dev in /sys/bus/pci/devices/*; do [ -f "$dev/vendor" ] || continue [ -f "$dev/device" ] || continue @@ -60,6 +51,32 @@ if [ -z "$CARD_MODEL" ]; then break fi done +} + +if [ -n "${CTL_BIN}" ] && [ -x "${CTL_BIN}" ]; then + detect_out="$("${CTL_BIN}" detect 2>/dev/null || true)" + CARD_MODEL="$(printf '%s\n' "${detect_out}" \ + | sed -n 's/.*Model Slug:[[:space:]]*//p' | head -n 1)" + CARD_PCI="$(printf '%s\n' "${detect_out}" \ + | sed -n 's/.*PCI BDF:[[:space:]]*//p' | head -n 1)" + case "${CARD_MODEL}" in + gc573|gc570d) ;; + *) CARD_MODEL="" ;; + esac + if printf '%s\n' "${detect_out}" | grep -q "Driver Bound: vfio-pci"; then + IS_BOUND=1 + fi +fi + +if [ -z "$CARD_MODEL" ]; then + detect_card_sysfs + if [ -n "$CARD_PCI" ]; then + driver_link="/sys/bus/pci/devices/${CARD_PCI}/driver" + if [ -L "$driver_link" ] && + [ "$(basename "$(readlink -f "$driver_link")")" = "vfio-pci" ]; then + IS_BOUND=1 + fi + fi fi if [ -z "$CARD_MODEL" ]; then @@ -73,20 +90,29 @@ echo "[*] everlive-service: Initializing ${CARD_MODEL} (${CARD_PCI:-unknown BDF} if [ "$CARD_MODEL" = "gc570d" ]; then if [ -x "${REPO_DIR}/tools/opengc570d-userd/opengc570d-userd" ]; then ENGINE_BIN="${REPO_DIR}/tools/opengc570d-userd/opengc570d-userd" - BIND_SCRIPT="${REPO_DIR}/tools/opengc570d-userd/gc570d-vfio-bind.sh" else ENGINE_BIN="/usr/local/bin/opengc570d-userd" - BIND_SCRIPT="/usr/local/bin/gc570d-vfio-bind.sh" fi + BIND_SCRIPT="" + for candidate in \ + "${REPO_DIR}/tools/opengc570d-userd/gc570d-vfio-bind.sh" \ + "/usr/local/libexec/gc570d-vfio-bind.sh" \ + "/usr/local/bin/gc570d-vfio-bind.sh"; do + [ -x "${candidate}" ] && BIND_SCRIPT="${candidate}" && break + done ENGINE_ARGS=(--headless --loopback) else if [ -x "${REPO_DIR}/tools/opengc573-userd/gc573_user_streamer" ]; then ENGINE_BIN="${REPO_DIR}/tools/opengc573-userd/gc573_user_streamer" - BIND_SCRIPT="${REPO_DIR}/tools/opengc573-userd/vfio_pci_bind.sh" else ENGINE_BIN="/usr/local/bin/gc573_user_streamer" - BIND_SCRIPT="/usr/local/bin/vfio_pci_bind.sh" fi + BIND_SCRIPT="" + for candidate in \ + "${REPO_DIR}/tools/opengc573-userd/vfio_pci_bind.sh" \ + "/usr/local/bin/vfio_pci_bind.sh"; do + [ -x "${candidate}" ] && BIND_SCRIPT="${candidate}" && break + done ENGINE_ARGS=(--headless) fi diff --git a/uninstall.sh b/uninstall.sh index 4f4c442..cbfa3ad 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -20,7 +20,9 @@ echo "[*] Deinstalliere EverLive..." # 1. Laufende Daemons beenden killall -9 everlive_userd gc573_user_streamer opengc570d-userd gc570d-ctl 2>/dev/null || true -rm -f "${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/gc570d/control.sock" 2>/dev/null || true +EVERLIVE_RUNTIME="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" +rm -f "${EVERLIVE_RUNTIME}/gc570d/control.sock" 2>/dev/null || true +rm -f "${EVERLIVE_RUNTIME}/everlive.sock" "${EVERLIVE_RUNTIME}/everlive.sock.lock" 2>/dev/null || true rm -f /dev/shm/gc570d_v4l2_shm 2>/dev/null || true # 2. VFIO & Memlock Regeln entfernen @@ -40,6 +42,18 @@ rm -f /usr/local/libexec/gc570d-vfio-bind.sh /usr/local/libexec/gc570d-sleep.sh rm -f /usr/local/lib/systemd/user/opengc570d-userd.service rm -f /etc/udev/rules.d/99-gc570d-vfio.rules +# libeverlive-core (everlive-ctl, library, headers, service unit, service script) +if [ -d "${SCRIPT_DIR}/tools/libeverlive-core" ]; then + make -C "${SCRIPT_DIR}/tools/libeverlive-core" uninstall >/dev/null 2>&1 || true +fi +rm -f /usr/local/bin/everlive-ctl /usr/local/bin/everlive-service +rm -f /usr/local/lib/libeverlive_core.so* /usr/local/lib/libeverlive_core.a +rm -f /usr/local/include/everlive_card.h /usr/local/include/everlive_frame.h \ + /usr/local/include/everlive_ctl.h /usr/local/include/everlive_json.h \ + /usr/local/include/everlive_ipc.h +rm -f /usr/local/lib/systemd/user/everlive.service +rm -f /usr/local/bin/vfio_pci_bind.sh + # 3. Legacy Kernel-Treiber & DKMS Reste entfernen (falls vorhanden) rmmod open_gc573 2>/dev/null || true rm -rf "/lib/modules/$(uname -r)/updates/open_gc573" 2>/dev/null || true From 711229796bc1c61bddbcba7d2c754f956289f082 Mon Sep 17 00:00:00 2001 From: realEverlite Date: Thu, 12 Nov 2026 02:50:46 +0100 Subject: [PATCH 8/8] fix(core): address open review items for install targets, led recovery, and input hardening - Add install/uninstall targets to tools/opengc573-userd/Makefile and wire them into root Makefile - Install GC573 engine binaries to BINDIR and apply cap_ipc_lock in install.sh - Search local repo, /usr/local/bin, and PATH for engine binaries in everlive-service.sh - Restore default max brightness when switching from LED off to an active effect on GC570D - Guard everlive_yuy2_to_rgb_scaled() against odd src_w - Reject non-finite double values (NaN/Infinity) in everlive_json_get_double() - Derive target UID from SUDO_USER in uninstall.sh to safely clean user runtime socket - Add unit test coverage for odd src_w and non-finite JSON doubles --- Makefile | 4 ++-- install.sh | 5 +++++ tests/Makefile | 4 +++- tests/test_frame_convert.c | 5 +++++ tests/test_json_rpc.c | 4 ++++ tools/libeverlive-core/everlive-service.sh | 17 +++++++++++++---- tools/libeverlive-core/everlive_frame.c | 2 +- tools/libeverlive-core/everlive_json.c | 3 ++- tools/opengc570d-userd/src/gc570d-led.c | 22 ++++++++++++++++++++++ tools/opengc573-userd/Makefile | 14 +++++++++++++- uninstall.sh | 8 +++++++- 11 files changed, 77 insertions(+), 11 deletions(-) diff --git a/Makefile b/Makefile index bc5904e..6272cf8 100644 --- a/Makefile +++ b/Makefile @@ -35,12 +35,12 @@ clean: +$(MAKE) -C $(GC570D_DIR) clean +$(MAKE) -C $(TESTS_DIR) clean -# GC573 has no install target: its binaries, bind script, udev rule, and -# systemd wiring are placed by install.sh. Core and GC570D own their files. install: all +$(MAKE) -C $(CORE_DIR) install + +$(MAKE) -C $(GC573_DIR) install +$(MAKE) -C $(GC570D_DIR) install uninstall: +$(MAKE) -C $(CORE_DIR) uninstall + +$(MAKE) -C $(GC573_DIR) uninstall +$(MAKE) -C $(GC570D_DIR) uninstall diff --git a/install.sh b/install.sh index ae4f649..2d6f3af 100755 --- a/install.sh +++ b/install.sh @@ -408,6 +408,9 @@ chmod 0644 "${GC570D_DIR}/libgc570d-v4l2.so" 2>/dev/null || true # Core library, everlive-ctl, everlive-service, headers, and systemd unit. make -C "${CORE_DIR}" install +# Install the GC573 engine binaries into /usr/local/bin with cap_ipc_lock. +make -C "${USERD_DIR}" install + # Install the GC570D helpers (udev rule, memlock, systemd unit, sleep hook) # through the engine's own Makefile so both installers stay in sync. make -C "${GC570D_DIR}" install @@ -416,7 +419,9 @@ make -C "${GC570D_DIR}" install install -m 0755 "${USERD_DIR}/vfio_pci_bind.sh" /usr/local/bin/vfio_pci_bind.sh setcap cap_ipc_lock=+ep "${USERD_DIR}/gc573_user_streamer" 2>/dev/null || true +setcap cap_ipc_lock=+ep /usr/local/bin/gc573_user_streamer 2>/dev/null || true setcap cap_ipc_lock=+ep "${GC570D_DIR}/opengc570d-userd" 2>/dev/null || true +setcap cap_ipc_lock=+ep /usr/local/bin/opengc570d-userd 2>/dev/null || true echo -e "${GREEN}[+] Userspace VFIO Engines erfolgreich gebaut & berechtigt!${NC}" # Python GUI-Umgebung (PySide6) für Benutzer prüfen und bei Bedarf einrichten diff --git a/tests/Makefile b/tests/Makefile index 149ac2f..8a68d82 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -25,9 +25,11 @@ TARGETS := test_card_registry test_frame_convert test_ctl_proto test_json_rpc \ all: $(TARGETS) -$(CORE_LIB): +$(CORE_LIB): FORCE $(MAKE) -C $(CORE_DIR) +.PHONY: FORCE + # --- libeverlive-core --------------------------------------------------- test_card_registry: test_card_registry.c $(CORE_LIB) $(CC) $(CFLAGS) $(INCLUDES) $(DEPFLAGS) -o $@ $< $(CORE_LIB) diff --git a/tests/test_frame_convert.c b/tests/test_frame_convert.c index 2fb8c20..f2fcbd5 100644 --- a/tests/test_frame_convert.c +++ b/tests/test_frame_convert.c @@ -97,6 +97,11 @@ static void test_invalid_parameters(void) for (size_t i = 0; i < sizeof(odd_dst); i++) assert(odd_dst[i] == 0xAA); + memset(odd_dst, 0x55, sizeof(odd_dst)); + everlive_yuy2_to_rgb_scaled(odd_src, odd_dst, 3, 1, 2, 2); + for (size_t i = 0; i < sizeof(odd_dst); i++) + assert(odd_dst[i] == 0x55); + printf(" Edge cases OK.\n"); } diff --git a/tests/test_json_rpc.c b/tests/test_json_rpc.c index 984094b..a22c83c 100644 --- a/tests/test_json_rpc.c +++ b/tests/test_json_rpc.c @@ -84,6 +84,10 @@ static void test_json_get_double(void) "Extracts positive float 'gain': 0.85"); TEST_ASSERT(!everlive_json_get_double(json, "nonexistent", &val), "Fails cleanly on missing float key"); + TEST_ASSERT(!everlive_json_get_double("{\"val\":NaN}", "val", &val), + "Rejects NaN in double field"); + TEST_ASSERT(!everlive_json_get_double("{\"val\":Infinity}", "val", &val), + "Rejects Infinity in double field"); } static void test_json_escape_into(void) diff --git a/tools/libeverlive-core/everlive-service.sh b/tools/libeverlive-core/everlive-service.sh index 9a894ef..d0357f1 100755 --- a/tools/libeverlive-core/everlive-service.sh +++ b/tools/libeverlive-core/everlive-service.sh @@ -90,27 +90,36 @@ echo "[*] everlive-service: Initializing ${CARD_MODEL} (${CARD_PCI:-unknown BDF} if [ "$CARD_MODEL" = "gc570d" ]; then if [ -x "${REPO_DIR}/tools/opengc570d-userd/opengc570d-userd" ]; then ENGINE_BIN="${REPO_DIR}/tools/opengc570d-userd/opengc570d-userd" - else + elif [ -x "/usr/local/bin/opengc570d-userd" ]; then ENGINE_BIN="/usr/local/bin/opengc570d-userd" + else + ENGINE_BIN="$(command -v opengc570d-userd 2>/dev/null || echo /usr/local/bin/opengc570d-userd)" fi BIND_SCRIPT="" for candidate in \ "${REPO_DIR}/tools/opengc570d-userd/gc570d-vfio-bind.sh" \ "/usr/local/libexec/gc570d-vfio-bind.sh" \ - "/usr/local/bin/gc570d-vfio-bind.sh"; do + "/usr/local/bin/gc570d-vfio-bind.sh" \ + "/usr/libexec/gc570d-vfio-bind.sh" \ + "/usr/bin/gc570d-vfio-bind.sh"; do [ -x "${candidate}" ] && BIND_SCRIPT="${candidate}" && break done ENGINE_ARGS=(--headless --loopback) else if [ -x "${REPO_DIR}/tools/opengc573-userd/gc573_user_streamer" ]; then ENGINE_BIN="${REPO_DIR}/tools/opengc573-userd/gc573_user_streamer" - else + elif [ -x "/usr/local/bin/gc573_user_streamer" ]; then ENGINE_BIN="/usr/local/bin/gc573_user_streamer" + else + ENGINE_BIN="$(command -v gc573_user_streamer 2>/dev/null || echo /usr/local/bin/gc573_user_streamer)" fi BIND_SCRIPT="" for candidate in \ "${REPO_DIR}/tools/opengc573-userd/vfio_pci_bind.sh" \ - "/usr/local/bin/vfio_pci_bind.sh"; do + "/usr/local/libexec/vfio_pci_bind.sh" \ + "/usr/local/bin/vfio_pci_bind.sh" \ + "/usr/libexec/vfio_pci_bind.sh" \ + "/usr/bin/vfio_pci_bind.sh"; do [ -x "${candidate}" ] && BIND_SCRIPT="${candidate}" && break done ENGINE_ARGS=(--headless) diff --git a/tools/libeverlive-core/everlive_frame.c b/tools/libeverlive-core/everlive_frame.c index 0cb8350..4bdb53e 100644 --- a/tools/libeverlive-core/everlive_frame.c +++ b/tools/libeverlive-core/everlive_frame.c @@ -59,7 +59,7 @@ void everlive_yuy2_to_rgb_scaled(const uint8_t *src, uint8_t *dst, int src_w, int src_h, int dst_w, int dst_h) { - if (!src || !dst || src_w <= 0 || src_h <= 0 || dst_w <= 0 || dst_h <= 0) + if (!src || !dst || src_w <= 0 || src_h <= 0 || dst_w <= 0 || dst_h <= 0 || (src_w & 1)) return; /* Fixed-point step in 16.16 format */ diff --git a/tools/libeverlive-core/everlive_json.c b/tools/libeverlive-core/everlive_json.c index 2329cef..ec35f40 100644 --- a/tools/libeverlive-core/everlive_json.c +++ b/tools/libeverlive-core/everlive_json.c @@ -11,6 +11,7 @@ #include "everlive_json.h" #include +#include #include #include #include @@ -134,7 +135,7 @@ static bool json_token_to_double(const jsmntok_t *tok, const char *json, buf[len] = '\0'; errno = 0; val = strtod(buf, &end); - if (errno || end == buf || *end != '\0') + if (errno || end == buf || *end != '\0' || !isfinite(val)) return false; *out = val; return true; diff --git a/tools/opengc570d-userd/src/gc570d-led.c b/tools/opengc570d-userd/src/gc570d-led.c index 1ee7c9b..33d27db 100644 --- a/tools/opengc570d-userd/src/gc570d-led.c +++ b/tools/opengc570d-userd/src/gc570d-led.c @@ -334,6 +334,13 @@ int gc570d_led_set_rgb(struct gc570d_dev *gc, u8 red, u8 green, u8 blue) gc->led_subleds[0].brightness = red; gc->led_subleds[1].brightness = green; gc->led_subleds[2].brightness = blue; + gc->led_subleds[0].intensity = red; + gc->led_subleds[1].intensity = green; + gc->led_subleds[2].intensity = blue; + if (red || green || blue) + gc->led_mcdev.led_cdev.brightness = GC570D_LED_MAX_BRIGHTNESS; + else + gc->led_mcdev.led_cdev.brightness = LED_OFF; ret = gc570d_led_hw_init(gc); if (!ret) @@ -398,6 +405,21 @@ int gc570d_led_set_effect(struct gc570d_dev *gc, const char *effect) goto out_unlock; } + /* + * When switching from 'off' (or zero brightness) to an active effect, + * restore default brightness so breathing and colorful effects do not + * scale all color channels to zero. + */ + if (gc->led_mcdev.led_cdev.brightness == LED_OFF) + gc->led_mcdev.led_cdev.brightness = GC570D_LED_MAX_BRIGHTNESS; + + if (breathing && + gc->led_subleds[0].intensity == 0 && + gc->led_subleds[1].intensity == 0 && + gc->led_subleds[2].intensity == 0) { + gc->led_subleds[2].intensity = GC570D_LED_WINDOWS_BLUE; + } + ret = led_mc_calc_color_components(&gc->led_mcdev, gc->led_mcdev.led_cdev.brightness); if (!ret) diff --git a/tools/opengc573-userd/Makefile b/tools/opengc573-userd/Makefile index e8bd94d..e319b77 100644 --- a/tools/opengc573-userd/Makefile +++ b/tools/opengc573-userd/Makefile @@ -98,4 +98,16 @@ framegen-selftest: clean: rm -f src/*.o src/*.d $(TARGETS) -.PHONY: all test selftest clean +PREFIX ?= /usr/local +DESTDIR ?= +BINDIR := $(DESTDIR)$(PREFIX)/bin + +install: all + install -d $(BINDIR) + install -m 0755 gc573_user_streamer everlive_userd gc573_user_probe $(BINDIR)/ + command -v setcap >/dev/null 2>&1 && setcap cap_ipc_lock=+ep $(BINDIR)/gc573_user_streamer 2>/dev/null || true + +uninstall: + rm -f $(BINDIR)/gc573_user_streamer $(BINDIR)/everlive_userd $(BINDIR)/gc573_user_probe + +.PHONY: all test selftest clean install uninstall diff --git a/uninstall.sh b/uninstall.sh index cbfa3ad..acbc872 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -12,6 +12,7 @@ if [ "$EUID" -ne 0 ]; then fi TARGET_USER="${SUDO_USER:-$USER}" +TARGET_UID="$(id -u "${TARGET_USER}" 2>/dev/null || echo 0)" # Resolve the home directory without eval-ing user-controlled text. TARGET_HOME="$(getent passwd "${TARGET_USER}" | cut -d: -f6)" [ -n "${TARGET_HOME}" ] || TARGET_HOME="/home/${TARGET_USER}" @@ -20,7 +21,7 @@ echo "[*] Deinstalliere EverLive..." # 1. Laufende Daemons beenden killall -9 everlive_userd gc573_user_streamer opengc570d-userd gc570d-ctl 2>/dev/null || true -EVERLIVE_RUNTIME="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" +EVERLIVE_RUNTIME="${XDG_RUNTIME_DIR:-/run/user/${TARGET_UID}}" rm -f "${EVERLIVE_RUNTIME}/gc570d/control.sock" 2>/dev/null || true rm -f "${EVERLIVE_RUNTIME}/everlive.sock" "${EVERLIVE_RUNTIME}/everlive.sock.lock" 2>/dev/null || true rm -f /dev/shm/gc570d_v4l2_shm 2>/dev/null || true @@ -28,6 +29,11 @@ rm -f /dev/shm/gc570d_v4l2_shm 2>/dev/null || true # 2. VFIO & Memlock Regeln entfernen rm -f /etc/udev/rules.d/99-gc573-vfio.rules rm -f /etc/security/limits.d/99-everlive-memlock.conf +# GC573 engine binaries: +if [ -d "${SCRIPT_DIR}/tools/opengc573-userd" ]; then + make -C "${SCRIPT_DIR}/tools/opengc573-userd" uninstall >/dev/null 2>&1 || true +fi +rm -f /usr/local/bin/gc573_user_streamer /usr/local/bin/everlive_userd /usr/local/bin/gc573_user_probe # GC570D (DUO) packaging installed through tools/opengc570d-userd/Makefile: # binaries, udev rule, memlock limits, systemd user unit and sleep hook. if [ -d "${SCRIPT_DIR}/tools/opengc570d-userd" ]; then