From 6e0df5dcef2ac68349e54361a1b7147b6e806460 Mon Sep 17 00:00:00 2001 From: PolygonPrincess Date: Sat, 7 Nov 2026 08:35:37 -0500 Subject: [PATCH 1/3] feat(gc570d): harden userspace VFIO receiver, modularize IT68051 EDID programmer, and update udev rules --- tools/opengc570d-userd/99-gc570d-vfio.rules | 5 +- tools/opengc570d-userd/src/gc570d-receiver.c | 278 +++++++++++------- tools/opengc570d-userd/src/gc570d-shm.h | 25 +- tools/opengc570d-userd/src/gc570d-userspace.c | 29 +- .../src/gc570d-v4l2-preload.c | 99 ++++++- tools/opengc570d-userd/src/gc570d_everlive.c | 100 ++++++- 6 files changed, 417 insertions(+), 119 deletions(-) diff --git a/tools/opengc570d-userd/99-gc570d-vfio.rules b/tools/opengc570d-userd/99-gc570d-vfio.rules index 8a8b6bf..66d23be 100644 --- a/tools/opengc570d-userd/99-gc570d-vfio.rules +++ b/tools/opengc570d-userd/99-gc570d-vfio.rules @@ -1,5 +1,8 @@ # SPDX-License-Identifier: GPL-2.0-only # Bind AVerMedia GC570D (1461:0054) to signed stock vfio-pci and grant seat/video access ACTION=="add", SUBSYSTEM=="pci", ATTR{vendor}=="0x1461", ATTR{device}=="0x0054", RUN+="/usr/local/libexec/gc570d-vfio-bind.sh" +# /dev/vfio/vfio is the shared control node; scope only the group nodes to +# AVerMedia PCI IDs so other VFIO assignments keep their own policy. SUBSYSTEM=="vfio", KERNEL=="vfio", GROUP="video", MODE="0660", TAG+="uaccess" -SUBSYSTEM=="vfio", KERNEL=="[0-9]*", GROUP="video", MODE="0660", TAG+="uaccess" +SUBSYSTEM=="vfio", KERNEL=="[0-9]*", ATTRS{vendor}=="0x1461", ATTRS{device}=="0x0054", GROUP="video", MODE="0660", TAG+="uaccess" +SUBSYSTEM=="vfio", KERNEL=="[0-9]*", ATTRS{vendor}=="0x1461", ATTRS{device}=="0x0055", GROUP="video", MODE="0660", TAG+="uaccess" diff --git a/tools/opengc570d-userd/src/gc570d-receiver.c b/tools/opengc570d-userd/src/gc570d-receiver.c index 440097d..432aa7f 100644 --- a/tools/opengc570d-userd/src/gc570d-receiver.c +++ b/tools/opengc570d-userd/src/gc570d-receiver.c @@ -1578,174 +1578,252 @@ int gc570d_it68051_select_page(struct gc570d_dev *gc, u8 page) 0x0f, 0x07, page); } -int gc570d_it68051_program_edid(struct gc570d_dev *gc) +/* + * IT68051 EDID Programming Constants & Registers + */ +#define IT68051_REG_CHIP_REV 0x00 +#define IT68051_REG_SYS_RESET 0x4b +#define IT68051_REG_PORT0_RESET 0x6b +#define IT68051_REG_OUTPUT_WIDTH_LATCH 0xc0 +#define IT68051_REG_OUTPUT_CLK_CTRL 0xc1 +#define IT68051_REG_EDID_LATCH_CTRL 0xc5 +#define IT68051_REG_VSDB_ADDR 0xc6 +#define IT68051_REG_PHYS_ADDR_AB 0xc7 +#define IT68051_REG_PHYS_ADDR_CD 0xc8 +#define IT68051_REG_BLOCK0_CHECKSUM 0xc9 +#define IT68051_REG_BLOCK1_CHECKSUM 0xca + +#define EDID_BLOCK_SIZE 128U +#define EDID_BLOCK_PAYLOAD_SIZE 127U +#define IT68051_VSDB_DEFAULT_ADDR 0xa9 +#define IT68051_EDID_RAM_ENABLE 0xa9 + +static int gc570d_it68051_calculate_edid_checksums(u8 *out_blk0, u8 *out_blk1) { - const struct gc570d_i2c_bus *bus = &gc570d_receiver_buses[0]; - const u8 vsdb_address = 0xa9; - u8 block0_checksum = 0; - u8 block1_checksum = 0; - u8 port1_checksum; - size_t bad_byte = 0; - size_t block; + u8 blk0 = 0; + u8 blk1 = 0; size_t i; - int restore_ret; - int ret = 0; - /* The official driver writes 127 payload bytes; IT68051 holds checksums in c9/ca. */ - for (i = 0; i < 127; i++) { - block0_checksum -= gc570d_it68051_edid[i]; - block1_checksum -= gc570d_it68051_edid[128 + i]; + for (i = 0; i < EDID_BLOCK_PAYLOAD_SIZE; i++) { + blk0 -= gc570d_it68051_edid[i]; + blk1 -= gc570d_it68051_edid[EDID_BLOCK_SIZE + i]; } - if (block0_checksum != gc570d_it68051_edid[127] || - block1_checksum != gc570d_it68051_edid[255]) + if (blk0 != gc570d_it68051_edid[EDID_BLOCK_PAYLOAD_SIZE] || + blk1 != gc570d_it68051_edid[EDID_BLOCK_SIZE + EDID_BLOCK_PAYLOAD_SIZE]) return -EILSEQ; - /* The official driver enables the internal EDID RAM at write address 0xa8. */ - ret = gc570d_it68051_select_page(gc, 0); - if (ret) - goto out_restore_page; - ret = gc570d_receiver_update8(gc, bus, 0x00, 0x88, 0x00); - if (ret) - goto out_restore_page; - ret = gc570d_receiver_write8(gc, bus, 0x4b, 0xa9); - if (ret) - goto out_restore_page; + if (out_blk0) + *out_blk0 = blk0; + if (out_blk1) + *out_blk1 = blk1; + return 0; +} - for (block = 0; block < 2; block++) { - for (i = 0; i < 127; i++) { - size_t address = block * 128 + i; +static int gc570d_it68051_write_edid_blocks(struct gc570d_dev *gc, + const struct gc570d_i2c_bus *bus) +{ + size_t block; + size_t i; + int ret; + for (block = 0; block < 2; block++) { + for (i = 0; i < EDID_BLOCK_PAYLOAD_SIZE; i++) { + size_t address = block * EDID_BLOCK_SIZE + i; ret = gc570d_i2c_write8(gc, bus, GC570D_I2C_EDID_ADDR8, address, gc570d_it68051_edid[address]); if (ret) { dev_err(&gc->pdev->dev, - "IT68051 EDID write failed at byte %zu: %d\n", - address, ret); - goto out_restore_page; + "IT68051 EDID write failed at addr 0x%02x byte %zu (val 0x%02x): %d\n", + GC570D_I2C_EDID_ADDR8, address, + gc570d_it68051_edid[address], ret); + return ret; } } } + return 0; +} - ret = gc570d_it68051_select_page(gc, 0); - if (ret) - goto out_restore_page; - ret = gc570d_receiver_write8(gc, bus, 0xc9, block0_checksum); - if (ret) - goto out_restore_page; - - ret = gc570d_it68051_select_page(gc, 4); - if (ret) - goto out_restore_page; - ret = gc570d_receiver_write8(gc, bus, 0xc9, block0_checksum); - if (ret) - goto out_restore_page; +static int gc570d_it68051_program_port_registers(struct gc570d_dev *gc, + const struct gc570d_i2c_bus *bus, + u8 block0_checksum, + u8 block1_checksum, + u8 port1_checksum) +{ + int ret; - /* Shared CTA block: port 0 is 1.0.0.0 and port 1 is 2.0.0.0. */ + /* Program Port 0 (Page 0) Checksums and VSDB Physical Address (1.0.0.0) */ ret = gc570d_it68051_select_page(gc, 0); if (ret) - goto out_restore_page; - ret = gc570d_receiver_write8(gc, bus, 0xc6, vsdb_address); + return ret; + ret = gc570d_receiver_write8(gc, bus, IT68051_REG_BLOCK0_CHECKSUM, block0_checksum); if (ret) - goto out_restore_page; - ret = gc570d_receiver_write8(gc, bus, 0xc7, 0x10); + return ret; + ret = gc570d_receiver_write8(gc, bus, IT68051_REG_VSDB_ADDR, IT68051_VSDB_DEFAULT_ADDR); if (ret) - goto out_restore_page; - ret = gc570d_receiver_write8(gc, bus, 0xc8, 0x00); + return ret; + ret = gc570d_receiver_write8(gc, bus, IT68051_REG_PHYS_ADDR_AB, 0x10); if (ret) - goto out_restore_page; - ret = gc570d_receiver_write8(gc, bus, 0xca, block1_checksum); + return ret; + ret = gc570d_receiver_write8(gc, bus, IT68051_REG_PHYS_ADDR_CD, 0x00); if (ret) - goto out_restore_page; + return ret; + ret = gc570d_receiver_write8(gc, bus, IT68051_REG_BLOCK1_CHECKSUM, block1_checksum); + if (ret) + return ret; - port1_checksum = block1_checksum - 0x10; + /* Program Port 1 (Page 4) Checksums and VSDB Physical Address (2.0.0.0) */ ret = gc570d_it68051_select_page(gc, 4); if (ret) - goto out_restore_page; - ret = gc570d_receiver_write8(gc, bus, 0xc7, 0x20); - if (ret) - goto out_restore_page; - ret = gc570d_receiver_write8(gc, bus, 0xc8, 0x00); + return ret; + ret = gc570d_receiver_write8(gc, bus, IT68051_REG_BLOCK0_CHECKSUM, block0_checksum); if (ret) - goto out_restore_page; - ret = gc570d_receiver_write8(gc, bus, 0xca, port1_checksum); + return ret; + ret = gc570d_receiver_write8(gc, bus, IT68051_REG_PHYS_ADDR_AB, 0x20); if (ret) - goto out_restore_page; - - ret = gc570d_it68051_edid_verify(gc, &bad_byte); + return ret; + ret = gc570d_receiver_write8(gc, bus, IT68051_REG_PHYS_ADDR_CD, 0x00); if (ret) - dev_err(&gc->pdev->dev, - "IT68051 EDID verification failed at byte %zu: %d\n", - bad_byte, ret); + return ret; + ret = gc570d_receiver_write8(gc, bus, IT68051_REG_BLOCK1_CHECKSUM, port1_checksum); if (ret) - goto out_restore_page; + return ret; + + return 0; +} + +static int gc570d_it68051_latch_edid(struct gc570d_dev *gc, + const struct gc570d_i2c_bus *bus) +{ + int ret; - /* The official driver latches the updated shared EDID independently on both ports. */ + /* Latch updated shared EDID on port 0 and port 1 */ ret = gc570d_it68051_select_page(gc, 0); if (ret) - goto out_restore_page; - ret = gc570d_receiver_update8(gc, bus, 0xc5, 0x01, 0x00); + return ret; + ret = gc570d_receiver_update8(gc, bus, IT68051_REG_EDID_LATCH_CTRL, 0x01, 0x00); if (ret) - goto out_restore_page; + return ret; + ret = gc570d_it68051_select_page(gc, 4); if (ret) - goto out_restore_page; - ret = gc570d_receiver_update8(gc, bus, 0xc5, 0x01, 0x00); + return ret; + ret = gc570d_receiver_update8(gc, bus, IT68051_REG_EDID_LATCH_CTRL, 0x01, 0x00); if (ret) - goto out_restore_page; + return ret; + + /* HPD pulse toggles to trigger source re-read on both ports */ ret = gc570d_it68051_select_page(gc, 0); if (ret) - goto out_restore_page; - ret = gc570d_receiver_update8(gc, bus, 0xc5, 0x10, 0x10); + return ret; + ret = gc570d_receiver_update8(gc, bus, IT68051_REG_EDID_LATCH_CTRL, 0x10, 0x10); if (ret) - goto out_restore_page; + return ret; msleep(1); - ret = gc570d_receiver_update8(gc, bus, 0xc5, 0x10, 0x00); + ret = gc570d_receiver_update8(gc, bus, IT68051_REG_EDID_LATCH_CTRL, 0x10, 0x00); if (ret) - goto out_restore_page; + return ret; + ret = gc570d_it68051_select_page(gc, 4); if (ret) - goto out_restore_page; - ret = gc570d_receiver_update8(gc, bus, 0xc5, 0x10, 0x10); + return ret; + ret = gc570d_receiver_update8(gc, bus, IT68051_REG_EDID_LATCH_CTRL, 0x10, 0x10); if (ret) - goto out_restore_page; + return ret; msleep(1); - ret = gc570d_receiver_update8(gc, bus, 0xc5, 0x10, 0x00); + ret = gc570d_receiver_update8(gc, bus, IT68051_REG_EDID_LATCH_CTRL, 0x10, 0x00); if (ret) - goto out_restore_page; + return ret; - /* The official driver with the defaults selected by the official driver. */ + /* Configure official driver defaults on page 1 */ ret = gc570d_it68051_select_page(gc, 1); if (ret) - goto out_restore_page; - /* The official driver clears the output-width latch before selecting it. */ - ret = gc570d_receiver_update8(gc, bus, 0xc0, 0x01, 0x00); + return ret; + ret = gc570d_receiver_update8(gc, bus, IT68051_REG_OUTPUT_WIDTH_LATCH, 0x01, 0x00); if (ret) - goto out_restore_page; + return ret; + ret = gc570d_receiver_update8(gc, bus, IT68051_REG_OUTPUT_WIDTH_LATCH, 0x06, 0x02); + if (ret) + return ret; + ret = gc570d_receiver_update8(gc, bus, IT68051_REG_OUTPUT_CLK_CTRL, 0x02, 0x00); + if (ret) + return ret; + ret = gc570d_receiver_update8(gc, bus, IT68051_REG_OUTPUT_CLK_CTRL, 0x20, 0x00); + if (ret) + return ret; - /* The official driver: dual-clock mode; initialization output is YUV444. */ - ret = gc570d_receiver_update8(gc, bus, 0xc0, 0x06, 0x02); + ret = gc570d_it68051_select_page(gc, 0); if (ret) - goto out_restore_page; - ret = gc570d_receiver_update8(gc, bus, 0xc1, 0x02, 0x00); + return ret; + ret = gc570d_receiver_update8(gc, bus, IT68051_REG_PORT0_RESET, 0x0c, 0x08); + return ret; +} + +int gc570d_it68051_program_edid(struct gc570d_dev *gc) +{ + const struct gc570d_i2c_bus *bus = &gc570d_receiver_buses[0]; + u8 block0_checksum = 0; + u8 block1_checksum = 0; + u8 port1_checksum; + size_t bad_byte = 0; + int restore_ret; + int ret = 0; + + ret = gc570d_it68051_calculate_edid_checksums(&block0_checksum, &block1_checksum); + if (ret) + return ret; + port1_checksum = block1_checksum - 0x10; + + /* The official driver enables the internal EDID RAM at write address 0xa8. */ + ret = gc570d_it68051_select_page(gc, 0); if (ret) goto out_restore_page; - ret = gc570d_receiver_update8(gc, bus, 0xc1, 0x20, 0x00); + ret = gc570d_receiver_update8(gc, bus, IT68051_REG_CHIP_REV, 0x88, 0x00); if (ret) goto out_restore_page; - ret = gc570d_it68051_select_page(gc, 0); + ret = gc570d_receiver_write8(gc, bus, IT68051_REG_SYS_RESET, IT68051_EDID_RAM_ENABLE); if (ret) goto out_restore_page; - ret = gc570d_receiver_update8(gc, bus, 0x6b, 0x0c, 0x08); + + for (int attempt = 1; attempt <= 3; attempt++) { + ret = gc570d_it68051_write_edid_blocks(gc, bus); + if (ret) + goto out_restore_page; + + ret = gc570d_it68051_program_port_registers(gc, bus, block0_checksum, + block1_checksum, port1_checksum); + if (ret) + goto out_restore_page; + + ret = gc570d_it68051_edid_verify(gc, &bad_byte); + if (!ret) + break; + + dev_warn(&gc->pdev->dev, + "IT68051 EDID verification failed at byte %zu: %d (attempt %d/3); retrying write...\n", + bad_byte, ret, attempt); + if (attempt == 3) { + dev_err(&gc->pdev->dev, + "IT68051 EDID verification failed after 3 attempts at byte %zu: %d\n", + bad_byte, ret); + goto out_restore_page; + } + msleep(5); + } + + ret = gc570d_it68051_latch_edid(gc, bus); out_restore_page: restore_ret = gc570d_it68051_select_page(gc, 0); - if (!ret) + if (restore_ret && !ret) ret = restore_ret; if (!ret) dev_info(&gc->pdev->dev, "IT68051 4K EDID programmed and verified, checksums=0x%02x/0x%02x port1=0x%02x\n", block0_checksum, block1_checksum, port1_checksum); + else + dev_err(&gc->pdev->dev, + "IT68051 EDID programming failed or restored with error: %d\n", ret); return ret; } diff --git a/tools/opengc570d-userd/src/gc570d-shm.h b/tools/opengc570d-userd/src/gc570d-shm.h index 7901f19..b256f68 100644 --- a/tools/opengc570d-userd/src/gc570d-shm.h +++ b/tools/opengc570d-userd/src/gc570d-shm.h @@ -8,15 +8,31 @@ #define GC570D_SHM_NAME "/gc570d_v4l2_shm" #define GC570D_SHM_MAGIC 0x47433537U /* "GC57" */ #define GC570D_SHM_FRAME_BYTES (1920 * 1080 * 2) +#define GC570D_SHM_SLOTS 3 +/* + * Triple-buffered frame slot. The DMA publisher always writes to the free + * back buffer, then atomically publishes ready_idx. A slow consumer therefore + * never stalls the capture thread and, because it re-checks seq after copying, + * never observes a torn frame either. + * + * Layout note: seq/timestamp/width/height/no_signal/streaming stay at the + * same offsets as the old single-slot layout; only the frame area moved. + */ struct gc570d_shm_port { - uint64_t seq; + uint64_t seq; /* increments once per published frame */ uint64_t timestamp_ns; uint32_t width; uint32_t height; uint32_t no_signal; uint32_t streaming; - uint8_t yuy2[GC570D_SHM_FRAME_BYTES]; + + uint32_t write_idx; /* producer-private back buffer */ + uint32_t ready_idx; /* atomically published front buffer */ + uint32_t read_idx; /* informational: last slot a consumer took */ + uint32_t reserved; + + uint8_t yuy2[GC570D_SHM_SLOTS][GC570D_SHM_FRAME_BYTES]; }; struct gc570d_shm_region { @@ -24,6 +40,11 @@ struct gc570d_shm_region { uint32_t version; char pci_bdf[32]; struct gc570d_shm_port ports[2]; + + /* Same liveness beacon as gc573_shm.h; appended to keep offsets stable. */ + uint32_t publisher_pid; + uint32_t reserved; + uint64_t heartbeat_ns; }; #endif /* GC570D_SHM_H */ diff --git a/tools/opengc570d-userd/src/gc570d-userspace.c b/tools/opengc570d-userd/src/gc570d-userspace.c index 8a4b9fc..cf68fb0 100644 --- a/tools/opengc570d-userd/src/gc570d-userspace.c +++ b/tools/opengc570d-userd/src/gc570d-userspace.c @@ -1887,14 +1887,18 @@ void vb2_buffer_done(struct vb2_buffer *vb, enum vb2_buffer_state state) struct gc570d_shm_port *sp = &g_shm->ports[port]; if ((size_t)frame_w * frame_h * 2 == vb->payload) { + uint32_t slot = __atomic_load_n( + &sp->write_idx, + __ATOMIC_RELAXED); uint64_t seq = __atomic_load_n(&sp->seq, __ATOMIC_RELAXED); - /* Seqlock: odd while the payload is in flux. */ - __atomic_store_n(&sp->seq, seq + 1, - __ATOMIC_RELAXED); - __atomic_thread_fence(__ATOMIC_RELEASE); - memcpy(sp->yuy2, vb->vaddr, vb->payload); + if (slot >= GC570D_SHM_SLOTS) + slot = 0; + + /* Write the back buffer, then swap it in. */ + memcpy(sp->yuy2[slot], vb->vaddr, + vb->payload); __atomic_store_n(&sp->width, frame_w, __ATOMIC_RELAXED); __atomic_store_n(&sp->height, frame_h, @@ -1908,9 +1912,17 @@ void vb2_buffer_done(struct vb2_buffer *vb, enum vb2_buffer_state state) __atomic_store_n(&sp->timestamp_ns, vb->timestamp, __ATOMIC_RELAXED); + __atomic_store_n(&g_shm->heartbeat_ns, + ktime_get_ns(), + __ATOMIC_RELAXED); __atomic_thread_fence(__ATOMIC_RELEASE); - __atomic_store_n(&sp->seq, seq + 2, + __atomic_store_n(&sp->ready_idx, slot, + __ATOMIC_RELEASE); + __atomic_store_n(&sp->seq, seq + 1, __ATOMIC_RELEASE); + __atomic_store_n(&sp->write_idx, + (slot + 1) % GC570D_SHM_SLOTS, + __ATOMIC_RELAXED); } } } @@ -3111,6 +3123,7 @@ int main(int argc, char **argv) /* Initialize POSIX shared-memory frame store for libgc570d-v4l2.so */ g_shm_fd = shm_open(GC570D_SHM_NAME, O_CREAT | O_RDWR, 0600); if (g_shm_fd >= 0) { + /* Frame store is private to the owning user. */ fchmod(g_shm_fd, 0600); if (ftruncate(g_shm_fd, sizeof(struct gc570d_shm_region)) == 0) { g_shm = mmap(NULL, sizeof(struct gc570d_shm_region), @@ -3118,7 +3131,9 @@ int main(int argc, char **argv) if (g_shm != MAP_FAILED) { memset(g_shm, 0, sizeof(struct gc570d_shm_region)); g_shm->magic = GC570D_SHM_MAGIC; - g_shm->version = 1; + g_shm->version = 2; + g_shm->publisher_pid = (uint32_t)getpid(); + g_shm->heartbeat_ns = ktime_get_ns(); strscpy(g_shm->pci_bdf, g_pdev.bdf, sizeof(g_shm->pci_bdf)); for (int p = 0; p < 2; p++) { g_shm->ports[p].width = p == 0 ? diff --git a/tools/opengc570d-userd/src/gc570d-v4l2-preload.c b/tools/opengc570d-userd/src/gc570d-v4l2-preload.c index 17e20f6..3c771a2 100644 --- a/tools/opengc570d-userd/src/gc570d-v4l2-preload.c +++ b/tools/opengc570d-userd/src/gc570d-v4l2-preload.c @@ -84,6 +84,67 @@ static pthread_mutex_t g_v4l2_lock = PTHREAD_MUTEX_INITIALIZER; static _Atomic int g_open_handle_count = 0; static struct gc570d_shm_region *g_shm_ptr = NULL; +/* + * Optional per-process opt-in. LD_PRELOAD is inherited by every child of a + * launched application, but this shim is only meaningful in the process that + * actually opens the virtual camera. When EVERLIVE_V4L2_TARGET is set to a + * comma-separated list of executable basenames (or "*"), stay dormant in + * every other child so crash reporters, shells and helpers are untouched. + * Unset = legacy behaviour (active everywhere). + */ +static int g_target_allowed = -1; + +static bool shim_target_allowed(void) +{ + int cached = __atomic_load_n(&g_target_allowed, __ATOMIC_RELAXED); + if (cached >= 0) + return cached != 0; + + const char *list = getenv("EVERLIVE_V4L2_TARGET"); + if (!list || !*list) { + __atomic_store_n(&g_target_allowed, 1, __ATOMIC_RELAXED); + return true; + } + + const char *prog = program_invocation_short_name; + bool match = false; + const char *p = list; + + if (!prog) + prog = ""; + while (*p) { + const char *start; + + while (*p == ',' || *p == ' ' || *p == '\t') + p++; + if (!*p) + break; + start = p; + while (*p && *p != ',' && *p != ' ' && *p != '\t') + p++; + if ((size_t)(p - start) == 1 && start[0] == '*') { + match = true; + break; + } + if ((size_t)(p - start) == strlen(prog) && + strncmp(start, prog, (size_t)(p - start)) == 0) { + match = true; + break; + } + } + + __atomic_store_n(&g_target_allowed, match ? 1 : 0, __ATOMIC_RELAXED); + return match; +} + +static uint64_t shim_now_ns(void) +{ + struct timespec ts; + + clock_gettime(CLOCK_MONOTONIC, &ts); + return (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec; +} + /* Real libc & libv4l2 function pointers */ @@ -201,9 +262,32 @@ static void shm_convert_port(int port, uint8_t *dst, size_t dst_max, return; } + /* + * If the publisher died (or was SIGKILLed) its last frame would + * otherwise loop forever. The daemon stamps this ~frame rate; a + * missing beacon older than one second means "no signal", not "freeze". + */ + { + uint64_t heartbeat = __atomic_load_n(&shm->heartbeat_ns, + __ATOMIC_ACQUIRE); + if (heartbeat && shim_now_ns() > heartbeat + 1000000000ULL) { + convert_yuy2_frame(NULL, 0, 0, dst, dst_max, + dst_w, dst_h, pixfmt); + return; + } + } + for (tries = 0; tries < 8; tries++) { + uint32_t slot; + seq = __atomic_load_n(&shm->ports[port].seq, __ATOMIC_ACQUIRE); - if (!seq || (seq & 1)) { + if (!seq) { + usleep(100); + continue; + } + slot = __atomic_load_n(&shm->ports[port].ready_idx, + __ATOMIC_ACQUIRE); + if (slot >= GC570D_SHM_SLOTS) { usleep(100); continue; } @@ -214,12 +298,15 @@ static void shm_convert_port(int port, uint8_t *dst, size_t dst_max, __ATOMIC_ACQUIRE); __atomic_thread_fence(__ATOMIC_ACQUIRE); - convert_yuy2_frame(shm->ports[port].yuy2, src_w, src_h, + convert_yuy2_frame(shm->ports[port].yuy2[slot], src_w, src_h, dst, dst_max, dst_w, dst_h, pixfmt); __atomic_thread_fence(__ATOMIC_ACQUIRE); - if (__atomic_load_n(&shm->ports[port].seq, __ATOMIC_ACQUIRE) == seq) + if (__atomic_load_n(&shm->ports[port].seq, __ATOMIC_ACQUIRE) == seq) { + __atomic_store_n(&shm->ports[port].read_idx, slot, + __ATOMIC_RELAXED); return; + } usleep(100); } @@ -229,7 +316,7 @@ static void shm_convert_port(int port, uint8_t *dst, size_t dst_max, static int match_virtual_video_path(const char *path) { - if (!path) + if (!path || !shim_target_allowed()) return -1; if (strcmp(path, "/dev/video60") == 0 || strcmp(path, "video60") == 0) return 0; @@ -433,7 +520,7 @@ static struct gc570d_dir_track *find_dir_track(DIR *dirp) static int match_virtual_sysfs_stat(const char *path, struct stat *st) { - if (!path) + if (!path || !shim_target_allowed()) return -1; while (*path == '.' && *(path + 1) == '/') path += 2; @@ -476,6 +563,8 @@ static int open_virtual_sysfs(const char *path) { int port = -1; const char *attr = NULL; + if (!shim_target_allowed()) + return -1; if (strncmp(path, "/sys/class/video4linux/video60/", 31) == 0) { port = 0; attr = path + 31; diff --git a/tools/opengc570d-userd/src/gc570d_everlive.c b/tools/opengc570d-userd/src/gc570d_everlive.c index 720274f..9d4712d 100644 --- a/tools/opengc570d-userd/src/gc570d_everlive.c +++ b/tools/opengc570d-userd/src/gc570d_everlive.c @@ -76,6 +76,9 @@ struct everlive_v4l2 { uint64_t frames; uint64_t dropped; uint64_t last_try_ns; + uint64_t last_reader_check_ns; + uint64_t idle_skips; + bool reader_active; bool configured; }; @@ -288,12 +291,18 @@ static bool everlive_snapshot(struct everlive_state *st, int port) unsigned int tries; for (tries = 0; tries < 8; tries++) { + uint32_t slot; + seq = __atomic_load_n(&sp->seq, __ATOMIC_ACQUIRE); - if (!seq || (seq & 1)) + if (!seq) continue; if (seq == st->last_seen[port]) return false; + slot = __atomic_load_n(&sp->ready_idx, __ATOMIC_ACQUIRE); + if (slot >= GC570D_SHM_SLOTS) + continue; + width = __atomic_load_n(&sp->width, __ATOMIC_ACQUIRE); height = __atomic_load_n(&sp->height, __ATOMIC_ACQUIRE); if (width < 2 || height < 2 || width > 1920 || height > 1080) @@ -303,7 +312,7 @@ static bool everlive_snapshot(struct everlive_state *st, int port) return false; __atomic_thread_fence(__ATOMIC_ACQUIRE); - memcpy(frame->pixels, sp->yuy2, bytes); + memcpy(frame->pixels, sp->yuy2[slot], bytes); frame->width = width; frame->height = height; frame->no_signal = __atomic_load_n(&sp->no_signal, __ATOMIC_RELAXED); @@ -313,6 +322,7 @@ static bool everlive_snapshot(struct everlive_state *st, int port) __atomic_thread_fence(__ATOMIC_ACQUIRE); if (__atomic_load_n(&sp->seq, __ATOMIC_ACQUIRE) == seq) { + __atomic_store_n(&sp->read_idx, slot, __ATOMIC_RELAXED); frame->seq = seq; st->last_seen[port] = seq; st->last_profile[port] = shm_port_profile(st, port); @@ -378,6 +388,57 @@ static void everlive_v4l2_close(struct everlive_v4l2 *out) out->configured = false; } +/* + * v4l2loopback reports whether a capture consumer owns the node through + * sysfs. Without a reader, write() only burns a full-frame copy (and the + * dual-HDMI path does this twice), so skip it while nobody is listening. + */ +static bool everlive_v4l2_reader_active(const char *device) +{ + const char *name; + char path[160]; + char buf[64]; + ssize_t n; + int fd; + + if (!device || !*device) + return false; + name = strrchr(device, '/'); + name = name ? name + 1 : device; + if (!*name) + return false; + + if (snprintf(path, sizeof(path), "/sys/class/video4linux/%s/state", + name) < (int)sizeof(path)) { + fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd >= 0) { + const char *p = buf; + + n = read(fd, buf, sizeof(buf) - 1); + close(fd); + if (n > 0) { + buf[n] = '\0'; + while (*p == ' ' || *p == '\t' || *p == '\n') + p++; + return strncasecmp(p, "capture", 7) == 0; + } + } + } + + if (snprintf(path, sizeof(path), "/sys/class/video4linux/%s/buffers", + name) >= (int)sizeof(path)) + return false; + fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd < 0) + return false; + n = read(fd, buf, sizeof(buf) - 1); + close(fd); + if (n <= 0) + return false; + buf[n] = '\0'; + return strtoul(buf, NULL, 10) > 0; +} + static void everlive_v4l2_write(struct everlive_v4l2 *out, const struct everlive_frame *frame, uint64_t now) @@ -405,6 +466,16 @@ static void everlive_v4l2_write(struct everlive_v4l2 *out, return; } + /* Re-check the sysfs state at most twice per second. */ + if (now - out->last_reader_check_ns >= 500000000ULL) { + out->reader_active = everlive_v4l2_reader_active(out->path); + out->last_reader_check_ns = now; + } + if (!out->reader_active) { + out->idle_skips++; + return; + } + bytes = (size_t)frame->width * frame->height * 2; written = write(out->fd, frame->pixels, bytes); if (written == (ssize_t)bytes) { @@ -982,6 +1053,27 @@ static void *everlive_thread(void *arg) if (!sdl_ready) fprintf(stderr, "[gc570d WARN] EverLive SDL window unavailable; continuing without a preview\n"); + } else if (st->opt.enable_sdl || st->opt.headless) { + /* Open low-latency desktop audio playback even in headless/UI-player mode */ + SDL_SetHint(SDL_HINT_APP_NAME, "EverLive"); + SDL_SetHint(SDL_HINT_AUDIO_DEVICE_APP_NAME, "EverLive"); + SDL_SetHint(SDL_HINT_AUDIO_DEVICE_STREAM_NAME, "HDMI Game Audio"); + SDL_SetHint(SDL_HINT_AUDIO_DEVICE_STREAM_ROLE, "game"); + if (SDL_Init(SDL_INIT_AUDIO) == 0) { + SDL_AudioSpec want, have; + memset(&want, 0, sizeof(want)); + want.freq = 48000; + want.format = AUDIO_S16LSB; + want.channels = 2; + want.samples = 1024; + sdl.audio_dev = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0); + if (sdl.audio_dev > 0) { + SDL_PauseAudioDevice(sdl.audio_dev, 0); + atomic_store_explicit(&st->audio_dev, + (unsigned int)sdl.audio_dev, + __ATOMIC_RELEASE); + } + } } #endif @@ -1070,7 +1162,7 @@ static void *everlive_thread(void *arg) for (i = 0; i < 2; i++) everlive_v4l2_close(&st->v4l2[i]); #ifdef HAVE_SDL2 - if (sdl_ready) + if (sdl_ready || sdl.audio_dev > 0) sdl_destroy(st, &sdl); #endif atomic_store_explicit(&st->running, false, __ATOMIC_RELEASE); @@ -1086,7 +1178,7 @@ int gc570d_everlive_start(struct gc570d_shm_region *shm, if (!shm || !options) return -EINVAL; - if (!options->enable_sdl && !options->v4l2[0] && !options->v4l2[1]) + if (!options->enable_sdl && !options->headless && !options->v4l2[0] && !options->v4l2[1]) return 0; if (atomic_load_explicit(&g_everlive.running, __ATOMIC_ACQUIRE)) return -EBUSY; From 059756ca641dc7e03cb1af0ca8c65f0fe0d95144 Mon Sep 17 00:00:00 2001 From: PolygonPrincess Date: Sun, 8 Nov 2026 10:13:18 -0500 Subject: [PATCH 2/3] fix(viewer): launch the DUO gaming screen without GC573 framegen flags The DUO start path referenced framegen settings it does not receive, so Start Gaming Screen raised NameError before the window opened. Those flags belong on the GC573 command line only. Co-authored-by: Cursor --- tools/everlive/everlive_viewer.py | 32 +++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/tools/everlive/everlive_viewer.py b/tools/everlive/everlive_viewer.py index 669c949..5132501 100755 --- a/tools/everlive/everlive_viewer.py +++ b/tools/everlive/everlive_viewer.py @@ -1595,22 +1595,6 @@ def _start_vfio_duo(self, fullscreen: bool, vsync: bool, if self.hw_info.get("pci"): cmd.append(self.hw_info["pci"]) - # lsfg-vk frame generation. This is deliberately passed - # in headless mode too: the engine keeps LSFG idle while the window is - # closed and activates Vulkan + LSFG as soon as mode.set("gaming") - # opens it, so a Studio -> Gaming switch needs no restart. The V4L2 - # loopback keeps receiving the raw capture path independently. - if framegen not in FRAMEGEN_KEYS: - framegen = FRAMEGEN_ALIASES.get(str(framegen).lower(), "off") - if framegen in FRAMEGEN_KEYS and framegen != "off": - cmd.extend(["--framegen", framegen]) - # Flow/perf/latency are passed even in mode OFF so a later live toggle - # (hotkey or IPC) starts from the Hub's saved values, not defaults. - cmd.extend(["--fg-flow-scale", - f"{max(0.5, min(1.0, float(fg_flow_scale))):.2f}"]) - cmd.append("--fg-perf" if fg_perf_mode else "--no-fg-perf") - cmd.append("--fg-low-latency" if fg_low_latency else "--no-fg-low-latency") - if headless: cmd.append("--headless") # Studio/streamer mode publishes a clean OBS feed without OSD. @@ -1803,6 +1787,22 @@ def _start_vfio(self, fullscreen: bool = False, vsync: bool = False, cmd.extend(["--scale", scale]) cmd.extend(["--volume", f"{volume:.2f}"]) + # lsfg-vk frame generation. Passed in headless mode too: the engine + # keeps LSFG idle while the window is closed and activates Vulkan + + # LSFG as soon as mode.set("gaming") opens it, so a Studio -> Gaming + # switch needs no restart. The Live Gamer DUO engine has no LSFG + # path; those flags stay on this GC573 command line only. + if framegen not in FRAMEGEN_KEYS: + framegen = FRAMEGEN_ALIASES.get(str(framegen).lower(), "off") + if framegen in FRAMEGEN_KEYS and framegen != "off": + cmd.extend(["--framegen", framegen]) + # Flow/perf/latency are passed even in mode OFF so a later live toggle + # (hotkey or IPC) starts from the Hub's saved values, not defaults. + cmd.extend(["--fg-flow-scale", + f"{max(0.5, min(1.0, float(fg_flow_scale))):.2f}"]) + cmd.append("--fg-perf" if fg_perf_mode else "--no-fg-perf") + cmd.append("--fg-low-latency" if fg_low_latency else "--no-fg-low-latency") + if v4l2_device: cmd.extend(["--v4l2", v4l2_device]) if audio_tap: From 219b2055ccc4b4d0103119a8704fe7c38f477004 Mon Sep 17 00:00:00 2001 From: PolygonPrincess Date: Sun, 8 Nov 2026 10:21:02 -0500 Subject: [PATCH 3/3] fix(viewer): adjust lsfg-vk comment in everlive_viewer.py to keep v4l2 and GC570D info --- tools/everlive/everlive_viewer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/everlive/everlive_viewer.py b/tools/everlive/everlive_viewer.py index 5132501..6305fe5 100755 --- a/tools/everlive/everlive_viewer.py +++ b/tools/everlive/everlive_viewer.py @@ -1790,8 +1790,9 @@ def _start_vfio(self, fullscreen: bool = False, vsync: bool = False, # lsfg-vk frame generation. Passed in headless mode too: the engine # keeps LSFG idle while the window is closed and activates Vulkan + # LSFG as soon as mode.set("gaming") opens it, so a Studio -> Gaming - # switch needs no restart. The Live Gamer DUO engine has no LSFG - # path; those flags stay on this GC573 command line only. + # switch needs no restart. The V4L2 loopback keeps receiving the + # raw capture path independently. The GC570D engine has no + # LSFG path; those flags stay on this GC573 command line only. if framegen not in FRAMEGEN_KEYS: framegen = FRAMEGEN_ALIASES.get(str(framegen).lower(), "off") if framegen in FRAMEGEN_KEYS and framegen != "off":