diff --git a/tools/everlive/everlive_viewer.py b/tools/everlive/everlive_viewer.py index 669c949..6305fe5 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,23 @@ 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 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": + 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: 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;