From a155c166865d04e7645052dd543973730175944b Mon Sep 17 00:00:00 2001 From: Scyllinice Date: Mon, 9 Nov 2026 22:10:36 -0600 Subject: [PATCH 1/5] feat(gc573): line-buffered, timestamped streamer log streamer.log is written through stdio, which buffers file output in 4 KB blocks. The Control Hub saw status lines seconds late, and the log had no timing, so slow starts could not be diagnosed. Replace stdout/stderr with fopencookie() streams after argument parsing. They are line-buffered, write straight to fd 1/2, and prefix each line with the seconds since start ([ 5.706] ...). The banner records the wall-clock start time. EVERLIVE_LOG_TIMESTAMPS=0 keeps plain lines (but still line-buffered). The Control Hub matches log lines with unanchored regexes, so the prefix does not affect it. --- tools/opengc573-userd/Makefile | 4 +- tools/opengc573-userd/gc573_user_log.c | 138 ++++++++++++++++++++ tools/opengc573-userd/gc573_user_log.h | 21 +++ tools/opengc573-userd/gc573_user_streamer.c | 6 +- 4 files changed, 166 insertions(+), 3 deletions(-) create mode 100644 tools/opengc573-userd/gc573_user_log.c create mode 100644 tools/opengc573-userd/gc573_user_log.h diff --git a/tools/opengc573-userd/Makefile b/tools/opengc573-userd/Makefile index 0c2c3a6..d45f1ce 100644 --- a/tools/opengc573-userd/Makefile +++ b/tools/opengc573-userd/Makefile @@ -18,9 +18,9 @@ PW_LIBS = $(shell pkg-config --libs libpipewire-0.3) # 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_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 +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 TARGETS = gc573_user_probe gc573_user_streamer everlive_userd gc573_user_diag -HEADERS = gc573-user-compat.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 +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) diff --git a/tools/opengc573-userd/gc573_user_log.c b/tools/opengc573-userd/gc573_user_log.c new file mode 100644 index 0000000..faa0788 --- /dev/null +++ b/tools/opengc573-userd/gc573_user_log.c @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * gc573_user_log.c — Line-buffered, timestamped stdout/stderr. + * + * The streamer's output usually goes to a file (streamer.log), where stdio + * would otherwise buffer it in 4 KB blocks: the Control Hub saw status lines + * late and the log carried no timing at all. stdout and stderr are replaced + * with fopencookie() streams that write straight to fd 1 / fd 2, adding the + * elapsed time at the start of every line. Output stays synchronous, so a + * crash loses no more than the line being written. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "gc573_user_log.h" + +struct ts_stream { + int fd; + bool line_start; +}; + +static struct timespec g_t0; +static char g_start_time[32] = ""; + +static int write_all(int fd, struct iovec *iov, int cnt) +{ + while (cnt > 0) { + ssize_t n = writev(fd, iov, cnt); + + if (n < 0) { + if (errno == EINTR) + continue; + return -1; + } + while (cnt > 0 && (size_t)n >= iov->iov_len) { + n -= (ssize_t)iov->iov_len; + iov++; + cnt--; + } + if (cnt > 0) { + iov->iov_base = (char *)iov->iov_base + n; + iov->iov_len -= (size_t)n; + } + } + return 0; +} + +static ssize_t ts_write(void *cookie, const char *buf, size_t len) +{ + struct ts_stream *s = cookie; + size_t done = 0; + + while (done < len) { + const char *nl = memchr(buf + done, '\n', len - done); + size_t chunk = nl ? (size_t)(nl - (buf + done)) + 1 : len - done; + struct iovec iov[2]; + char prefix[24]; + int cnt = 0; + + if (s->line_start) { + struct timespec now; + int64_t ms; + + clock_gettime(CLOCK_MONOTONIC, &now); + ms = (int64_t)(now.tv_sec - g_t0.tv_sec) * 1000 + + (now.tv_nsec - g_t0.tv_nsec) / 1000000; + iov[cnt].iov_base = prefix; + iov[cnt].iov_len = (size_t)snprintf(prefix, sizeof(prefix), + "[%4lld.%03lld] ", (long long)(ms / 1000), + (long long)(ms % 1000)); + cnt++; + } + iov[cnt].iov_base = (void *)(buf + done); + iov[cnt].iov_len = chunk; + cnt++; + + if (write_all(s->fd, iov, cnt) < 0) + return done ? (ssize_t)done : -1; + done += chunk; + s->line_start = nl != NULL; + } + return (ssize_t)len; +} + +static FILE *ts_open(struct ts_stream *s) +{ + cookie_io_functions_t io = { .write = ts_write }; + FILE *f = fopencookie(s, "w", io); + + if (f) + setvbuf(f, NULL, _IOLBF, 0); + return f; +} + +void gc573_user_log_init(void) +{ + static struct ts_stream out = { .fd = STDOUT_FILENO, .line_start = true }; + static struct ts_stream err = { .fd = STDERR_FILENO, .line_start = true }; + const char *env = getenv("EVERLIVE_LOG_TIMESTAMPS"); + time_t wall = time(NULL); + struct tm tm; + FILE *f; + + clock_gettime(CLOCK_MONOTONIC, &g_t0); + if (localtime_r(&wall, &tm)) + strftime(g_start_time, sizeof(g_start_time), + "%Y-%m-%d %H:%M:%S", &tm); + + fflush(stdout); + fflush(stderr); + if (env && strcmp(env, "0") == 0) { + setvbuf(stdout, NULL, _IOLBF, 0); + return; + } + + f = ts_open(&out); + if (f) + stdout = f; + else + setvbuf(stdout, NULL, _IOLBF, 0); + f = ts_open(&err); + if (f) + stderr = f; +} + +const char *gc573_user_log_start_time(void) +{ + return g_start_time; +} diff --git a/tools/opengc573-userd/gc573_user_log.h b/tools/opengc573-userd/gc573_user_log.h new file mode 100644 index 0000000..43c50d2 --- /dev/null +++ b/tools/opengc573-userd/gc573_user_log.h @@ -0,0 +1,21 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * gc573_user_log.h — Line-buffered, timestamped stdout/stderr for the + * GC573 streamer, so streamer.log shows when each step happened. + */ + +#ifndef GC573_USER_LOG_H +#define GC573_USER_LOG_H + +/** + * gc573_user_log_init - Make stdout/stderr line-buffered and, unless + * EVERLIVE_LOG_TIMESTAMPS=0, prefix every line with the time since start + * as "[ 12.345] ". Call once from main(), after argument parsing + * (so --help stays plain) and before the startup banner. + */ +void gc573_user_log_init(void); + +/** Wall-clock time gc573_user_log_init() ran, for the log banner. */ +const char *gc573_user_log_start_time(void); + +#endif /* GC573_USER_LOG_H */ diff --git a/tools/opengc573-userd/gc573_user_streamer.c b/tools/opengc573-userd/gc573_user_streamer.c index 684f065..98e0880 100644 --- a/tools/opengc573-userd/gc573_user_streamer.c +++ b/tools/opengc573-userd/gc573_user_streamer.c @@ -30,6 +30,7 @@ #include "gc573_user_audio_tap.h" #include "gc573_user_hdr.h" #include "gc573_user_framegen.h" +#include "gc573_user_log.h" #include "gc573_user_font8x8.h" #include #include @@ -5952,9 +5953,12 @@ int main(int argc, char *argv[]) g_osd_pos = OSD_POS_TR; } + gc573_user_log_init(); printf("==================================================================\n"); printf(" EverLive — Userspace VFIO Streamer (v" EVERLIVE_VERSION ")\n"); - printf("==================================================================\n\n"); + printf("==================================================================\n"); + printf("[LOG] Started %s; line prefixes are seconds since start.\n\n", + gc573_user_log_start_time()); if (g_trace_mode) printf("[TRACE] Teardown trace enabled: press T while streaming to start the phase test.\n"); From 98475d356e719ad045ee99e917f91c7c554aa101 Mon Sep 17 00:00:00 2001 From: Scyllinice Date: Mon, 9 Nov 2026 22:10:36 -0600 Subject: [PATCH 2/5] perf(gc573): poll the FPGA DDR sampler lock instead of sleeping 2 s gc573_user_bridge_wait_for_ddr() kicked the sampler, slept a fixed 2000 ms and checked once. Poll every 10 ms within the same 2 s budget, and accept the lock once it has held for 50 ms, so a transient bit right after the kick is not taken as a lock. The number of attempts and the timeout are unchanged. On the test card (GC573, RetroTINK 4K source) the sampler locks after ~1.63 s, so this saves ~0.35 s per start. The log now reports the time taken. --- tools/opengc573-userd/gc573_user_hw.c | 29 +++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/tools/opengc573-userd/gc573_user_hw.c b/tools/opengc573-userd/gc573_user_hw.c index 334a638..c4b254b 100644 --- a/tools/opengc573-userd/gc573_user_hw.c +++ b/tools/opengc573-userd/gc573_user_hw.c @@ -8,6 +8,11 @@ #include "gc573_user_hw.h" #include "gc573_user_i2c.h" +/* FPGA DDR sampler gate: budget per attempt and poll interval. */ +#define DDR_LOCK_TIMEOUT_MS 2000 +#define DDR_LOCK_POLL_MS 10 +#define DDR_LOCK_STABLE_POLLS 5 /* 50 ms of continuous lock */ + /* * Software copy of the GPIO output latch (reg 0x00040). * @@ -158,12 +163,24 @@ int gc573_user_bridge_wait_for_ddr(void *bar0) writel(0x8, base + GC573_REG_DDR_STATUS); writel(0x1, base + GC573_REG_DDR_STATUS); - msleep(2000); - status = readl(base + GC573_REG_DDR_STATUS); - if (status & BIT(0)) { - pr_info("FPGA DDR sampler locked on attempt %d (status 0x%08x)", - attempt + 1, status); - return 0; + /* + * Poll instead of sleeping the whole window: the sampler + * normally locks long before the 2 s budget runs out. Only + * accept a lock that holds for several consecutive polls, so a + * transient bit right after the kick is not mistaken for it. + */ + int stable = 0; + + for (int waited = DDR_LOCK_POLL_MS; waited <= DDR_LOCK_TIMEOUT_MS; + waited += DDR_LOCK_POLL_MS) { + msleep(DDR_LOCK_POLL_MS); + status = readl(base + GC573_REG_DDR_STATUS); + stable = (status & BIT(0)) ? stable + 1 : 0; + if (stable >= DDR_LOCK_STABLE_POLLS) { + pr_info("FPGA DDR sampler locked on attempt %d after %d ms (status 0x%08x)", + attempt + 1, waited, status); + return 0; + } } } From 6867c221c03e8f49747e06c57b6d360d3059b940 Mon Sep 17 00:00:00 2001 From: Scyllinice Date: Mon, 9 Nov 2026 22:10:46 -0600 Subject: [PATCH 3/5] perf(gc573): reuse the last clean link EQ before running auto-EQ Every HDMI 2.0 lock ran the full hardware auto-EQ on the input receiver (~0.75 s), or the RS sweep when that left errors (~2.6 s), although the source and cable rarely change between starts. gc573_rx_eq_tune() now takes the settings that left the port error-free last time (RS and DFE taps per lane). After the baseline it applies them and keeps them if one 100 ms CED window reads zero. Otherwise it continues with the normal auto-EQ path, so a stale entry costs one extra window. The result returns the final settings when the link ends clean. The streamer keeps them per receiver in memory for re-locks and in $XDG_STATE_HOME/everlive/link-eq (default ~/.local/state) across restarts. The log shows 'auto-EQ skipped (saved EQ clean)' when they were used. Hardware (GC573, RetroTINK 4K, 4K60 RGB): input EQ took 227 ms with the saved settings, against 731 ms for a fresh auto-EQ. --- tools/opengc573-userd/gc573_user_rx_eq.c | 58 +++++++++- tools/opengc573-userd/gc573_user_rx_eq.h | 23 +++- tools/opengc573-userd/gc573_user_streamer.c | 119 +++++++++++++++++++- 3 files changed, 192 insertions(+), 8 deletions(-) diff --git a/tools/opengc573-userd/gc573_user_rx_eq.c b/tools/opengc573-userd/gc573_user_rx_eq.c index 5dc8a03..ca03249 100644 --- a/tools/opengc573-userd/gc573_user_rx_eq.c +++ b/tools/opengc573-userd/gc573_user_rx_eq.c @@ -457,6 +457,25 @@ static int eq_force_zero_skew(void *bar0, const struct rx_port *p) return p->set_bank(bar0, BANK0); } +/* Force saved RS values and DFE taps (skew is left as it is). */ +static int eq_apply_state(void *bar0, const struct rx_port *p, + const struct gc573_rx_eq_state *st) +{ + for (unsigned int ch = 0; ch < CH_COUNT; ch++) { + TRY(wr(bar0, p, BANK3, (u8)(0x27 + ch), (u8)(0x80 | st->rs[ch]))); + if (st->dfe) + for (unsigned int k = 0; k < 3; k++) + TRY(wr(bar0, p, BANK3, (u8)(0x4b + ch * 3 + k), + st->taps[ch][k])); + } + if (p->dfe) { + /* 0x4b[7] shares a register with ch0's first tap: set last. */ + TRY(upd(bar0, p, BANK3, 0x4b, 0x80, st->dfe ? 0x80 : 0x00)); + TRY(upd(bar0, p, BANK3, 0x22, 0x40, st->dfe ? 0x40 : 0x00)); + } + return p->set_bank(bar0, BANK0); +} + static bool env_flag(const char *name) { const char *v = getenv(name); @@ -522,6 +541,7 @@ static int eq_auto_and_apply(void *bar0, const struct rx_port *p, } int gc573_rx_eq_tune(void *bar0, enum gc573_rx_port port, + const struct gc573_rx_eq_state *saved, struct gc573_rx_eq_result *res) { struct dfe_taps dfe[CH_COUNT][RS_LADDER_LEN]; @@ -559,7 +579,24 @@ int gc573_rx_eq_tune(void *bar0, enum gc573_rx_port port, } res->ran = true; - /* 2. Hardware auto-EQ (+ DFE where supported). */ + /* + * 2. Settings that were clean on this port last time. Source and + * cable rarely change, so this usually skips the auto-EQ entirely. + */ + if (saved && saved->valid && (!saved->dfe || p->dfe)) { + TRY(eq_apply_state(bar0, p, saved)); + TRY(ced_measure(bar0, p, CED_WINDOW_MS, &ced)); + if (ced_any_valid(&ced) && ced_total(&ced) == 0) { + res->used_saved = true; + res->ced_valid = true; + res->dfe_applied = saved->dfe; + res->state = *saved; + memcpy(best_rs, saved->rs, sizeof(best_rs)); + goto finish; + } + } + + /* 3. Hardware auto-EQ (+ DFE where supported). */ ret = eq_auto_and_apply(bar0, p, rs, dfe, &dfe_ok); if (ret < 0) return ret; @@ -574,7 +611,7 @@ int gc573_rx_eq_tune(void *bar0, enum gc573_rx_port port, best_score[ch] = ced_score(&ced, ch); /* - * 3. Errors remain: sweep RS per channel with skew forced to 0. The + * 4. Errors remain: sweep RS per channel with skew forced to 0. The * sweep is scored by CED, so it is pointless without valid counts. */ if (auto_total && ced_any_valid(&ced)) { @@ -598,7 +635,7 @@ int gc573_rx_eq_tune(void *bar0, enum gc573_rx_port port, for (unsigned int ch = 0; ch < CH_COUNT; ch++) sweep_total += best_score[ch]; - /* 4. Retrain skew around the chosen RS values. */ + /* 5. Retrain skew around the chosen RS values. */ TRY(eq_apply(bar0, p, best_rs, dfe_ok ? dfe : NULL)); ret = eq_run_skew(bar0, p); if (ret < 0) @@ -626,7 +663,20 @@ int gc573_rx_eq_tune(void *bar0, enum gc573_rx_port port, } } - /* 5. Done: power the EQ engine down and clear its IRQs. */ + /* Remember what a clean link ended on, for the next lock. */ + if (ced_any_valid(&ced) && ced_total(&ced) == 0) { + struct gc573_rx_eq_state *st = &res->state; + + st->valid = true; + st->dfe = dfe_ok; + memcpy(st->rs, best_rs, sizeof(st->rs)); + for (unsigned int ch = 0; ch < CH_COUNT; ch++) + memcpy(st->taps[ch], dfe[ch][rs_index(best_rs[ch])].tap, + sizeof(st->taps[ch])); + } + +finish: + /* 6. Done: power the EQ engine down and clear its IRQs. */ TRY(upd(bar0, p, BANK3, 0x22, 0x08, 0x00)); TRY(wr(bar0, p, BANK0, 0x07, 0xf0)); diff --git a/tools/opengc573-userd/gc573_user_rx_eq.h b/tools/opengc573-userd/gc573_user_rx_eq.h index 8b461db..65e0641 100644 --- a/tools/opengc573-userd/gc573_user_rx_eq.h +++ b/tools/opengc573-userd/gc573_user_rx_eq.h @@ -32,8 +32,21 @@ struct gc573_rx_ced { u32 window_ms; }; +/* + * EQ settings that left a link error-free, so the next lock can try them + * before running the hardware auto-EQ. Only RS and the DFE taps are kept; + * a saved state is always verified by measurement before it is trusted. + */ +struct gc573_rx_eq_state { + bool valid; + bool dfe; /* manual DFE taps enabled */ + u8 rs[GC573_RX_TMDS_CHANNELS]; + u8 taps[GC573_RX_TMDS_CHANNELS][3]; /* DFE tap registers */ +}; + struct gc573_rx_eq_result { bool ran; /* false: not HDMI 2.0, disabled, or clean */ + bool used_saved; /* the saved settings were clean; no auto-EQ */ bool ced_valid; /* CED counters reported valid counts */ bool before_valid; /* baseline window had valid counts */ bool auto_eq_done; /* hardware auto-EQ reported completion */ @@ -46,6 +59,7 @@ struct gc573_rx_eq_result { u32 errors_after; /* summed CED over the final window */ u32 ch_errors_after[GC573_RX_TMDS_CHANNELS]; u32 elapsed_ms; + struct gc573_rx_eq_state state; /* valid when the link ended clean */ }; const char *gc573_rx_port_name(enum gc573_rx_port port); @@ -76,13 +90,16 @@ void gc573_rx_ced_reset(void); * gc573_rx_eq_tune - Equalise one receiver on an HDMI 2.0 link * * Measures CED first and leaves an error-free link untouched. Otherwise - * runs the receiver's hardware auto-EQ, applies the result plus DFE taps - * and, if errors remain, sweeps the RS (equaliser - * strength) per TMDS channel and keeps the setting with the fewest errors. + * tries @saved (may be NULL) and keeps it if it measures error-free; failing + * that, runs the receiver's hardware auto-EQ, applies the result plus DFE + * taps and, if errors remain, sweeps the RS (equaliser strength) per TMDS + * channel and keeps the setting with the fewest errors. @result->state + * receives the final settings when the link ends error-free. * No-op on HDMI 1.x links or when EVERLIVE_NO_AUTO_EQ=1 is set. Blocks for * up to roughly 1.5 s; the link may briefly lose SCDT while EQ runs. */ int gc573_rx_eq_tune(void *bar0, enum gc573_rx_port port, + const struct gc573_rx_eq_state *saved, struct gc573_rx_eq_result *result); #endif /* GC573_USER_RX_EQ_H */ diff --git a/tools/opengc573-userd/gc573_user_streamer.c b/tools/opengc573-userd/gc573_user_streamer.c index 98e0880..cc2383e 100644 --- a/tools/opengc573-userd/gc573_user_streamer.c +++ b/tools/opengc573-userd/gc573_user_streamer.c @@ -46,6 +46,7 @@ #include #include #include +#include #include #include #include @@ -1121,6 +1122,112 @@ static bool wait_capture_scdt(void *bar0, unsigned int timeout_ms) return false; } +/* + * Link EQ settings that were clean last time, per receiver, kept in memory + * for re-locks and in $XDG_STATE_HOME/everlive/link-eq across restarts. + * The tuner verifies them by measurement, so a stale file only costs one + * 100 ms check before the normal auto-EQ runs. + */ +static struct gc573_rx_eq_state g_eq_saved[GC573_RX_PORT_COUNT]; +static bool g_eq_saved_loaded; +static const char *const eq_state_key[GC573_RX_PORT_COUNT] = { + [GC573_RX_INPUT] = "input", + [GC573_RX_CAPTURE] = "capture", +}; + +static bool eq_state_path(char *buf, size_t len, bool create_dir) +{ + const char *base = getenv("XDG_STATE_HOME"); + const char *home = getenv("HOME"); + char dir[PATH_MAX]; + int n; + + if (base && *base) + n = snprintf(dir, sizeof(dir), "%s/everlive", base); + else if (home && *home) + n = snprintf(dir, sizeof(dir), "%s/.local/state/everlive", home); + else + return false; + if (n < 0 || (size_t)n >= sizeof(dir)) + return false; + if (create_dir) { + /* Create each missing component (~/.local/state may not exist). */ + for (char *c = dir + 1; *c; c++) { + if (*c != '/') + continue; + *c = '\0'; + mkdir(dir, 0700); + *c = '/'; + } + mkdir(dir, 0700); + } + n = snprintf(buf, len, "%s/link-eq", dir); + return n > 0 && (size_t)n < len; +} + +static void eq_state_load(void) +{ + char path[PATH_MAX], line[256]; + FILE *f; + + g_eq_saved_loaded = true; + if (!eq_state_path(path, sizeof(path), false)) + return; + f = fopen(path, "r"); + if (!f) + return; + while (fgets(line, sizeof(line), f)) { + struct gc573_rx_eq_state st = { .valid = true }; + unsigned int rs[3], t[9], dfe; + char key[16]; + + if (sscanf(line, "%15s dfe=%u rs=%x,%x,%x taps=%x,%x,%x,%x,%x,%x,%x,%x,%x", + key, &dfe, &rs[0], &rs[1], &rs[2], &t[0], &t[1], &t[2], + &t[3], &t[4], &t[5], &t[6], &t[7], &t[8]) != 14) + continue; + st.dfe = dfe != 0; + for (int ch = 0; ch < 3; ch++) { + st.rs[ch] = (u8)(rs[ch] & 0x7f); + for (int k = 0; k < 3; k++) + st.taps[ch][k] = (u8)t[ch * 3 + k]; + } + for (int port = 0; port < GC573_RX_PORT_COUNT; port++) + if (strcmp(key, eq_state_key[port]) == 0) + g_eq_saved[port] = st; + } + fclose(f); +} + +static void eq_state_save(void) +{ + char path[PATH_MAX], tmp[PATH_MAX + 8]; + FILE *f; + + if (!eq_state_path(path, sizeof(path), true)) + return; + snprintf(tmp, sizeof(tmp), "%s.tmp", path); + f = fopen(tmp, "w"); + if (!f) + return; + fprintf(f, "# EverLive GC573 link EQ that was error-free last time (safe to delete)\n"); + for (int port = 0; port < GC573_RX_PORT_COUNT; port++) { + const struct gc573_rx_eq_state *st = &g_eq_saved[port]; + + if (!st->valid) + continue; + fprintf(f, "%s dfe=%d rs=%02x,%02x,%02x taps=%02x,%02x,%02x,%02x,%02x,%02x,%02x,%02x,%02x\n", + eq_state_key[port], st->dfe ? 1 : 0, + st->rs[0], st->rs[1], st->rs[2], + st->taps[0][0], st->taps[0][1], st->taps[0][2], + st->taps[1][0], st->taps[1][1], st->taps[1][2], + st->taps[2][0], st->taps[2][1], st->taps[2][2]); + } + if (fclose(f) == 0) + rename(tmp, path); + else + unlink(tmp); +} + /* * Equalise an HDMI 2.0 link before the timing is measured: first the card's * input (IT6664), whose errors are re-encoded and invisible downstream, then @@ -1130,14 +1237,17 @@ static bool wait_capture_scdt(void *bar0, unsigned int timeout_ms) static void tune_link_eq(void *bar0) { bool touched = false; + bool save = false; if (!gc573_rx_link_is_hdmi2(bar0)) return; + if (!g_eq_saved_loaded) + eq_state_load(); for (int port = 0; port < GC573_RX_PORT_COUNT; port++) { struct gc573_rx_eq_result eq; const char *name = gc573_rx_port_name(port); - int ret = gc573_rx_eq_tune(bar0, port, &eq); + int ret = gc573_rx_eq_tune(bar0, port, &g_eq_saved[port], &eq); char before[16], after[16]; if (ret < 0) { @@ -1154,6 +1264,10 @@ static void tune_link_eq(void *bar0) continue; } touched = true; + if (eq.state.valid && !eq.used_saved) { + g_eq_saved[port] = eq.state; + save = true; + } if (eq.before_valid) snprintf(before, sizeof(before), "%u", eq.errors_before); else @@ -1167,12 +1281,15 @@ static void tune_link_eq(void *bar0) eq.ch_errors_after[0], eq.ch_errors_after[1], eq.ch_errors_after[2], eq.rs[0], eq.rs[1], eq.rs[2], + eq.used_saved ? "skipped (saved EQ clean)" : eq.auto_eq_done ? "done" : "failed", eq.dfe_applied ? "on" : "off", !eq.swept ? "no" : eq.kept_auto ? "lost to auto-EQ" : "yes", !eq.swept ? "auto" : eq.skew_trained ? "trained" : "forced 0", eq.elapsed_ms); } + if (save) + eq_state_save(); if (!touched) return; From 24e20814dd6131f8af1c3d284c168933daccd1de Mon Sep 17 00:00:00 2001 From: Scyllinice Date: Mon, 9 Nov 2026 22:10:56 -0600 Subject: [PATCH 4/5] docs: changelog entry for faster start-up and timestamped log --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5735cab..b72f0cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - `[HDMI] TMDS errors B/G/R` line in `streamer.log` with per-lane error counts for the card input and the internal capture hop: every second while errors occur, otherwise every 10 s. - `EVERLIVE_NO_AUTO_EQ=1` disables link equalisation; `EVERLIVE_EQ_INPUT_DFE=0` keeps the input on RS-only EQ (for comparison and troubleshooting). +- `streamer.log` lines carry the seconds since the streamer started (`[ 5.706] ...`), and the log is now written line by line instead of in 4 KB blocks, so the Control Hub sees status changes immediately. `EVERLIVE_LOG_TIMESTAMPS=0` turns the prefix off. + +### Changed +- **Faster GC573 start-up:** + - The FPGA DDR sampler lock is polled instead of waited out with a fixed 2 s sleep (it locks after ~1.6 s on the test card). + - Link EQ settings that left a receiver error-free are saved in `~/.local/state/everlive/link-eq` and tried first on the next lock. One 100 ms error count confirms them, which skips the hardware auto-EQ (227 ms instead of ~730 ms on the input). Stale settings fall back to the full tune automatically. ## [1.4.0] - 2026-11-06 From c006ed5fc49307b233c0496e1c90abab6ba38c3d Mon Sep 17 00:00:00 2001 From: Scyllinice Date: Mon, 9 Nov 2026 22:39:15 -0600 Subject: [PATCH 5/5] feat(gc573): log how the saved link EQ measured The Link EQ line now has a 'saved EQ' field: 'none' when nothing was saved for the port, otherwise the error count from the saved settings' 100 ms check. This shows whether a full auto-EQ ran because the saved settings failed or because there were none. A skew retrain around the saved RS before falling back was also tested. On the card it only roughly halved the errors from bad saved settings, never rescued a start, and added ~0.3 s to the fallback, so it was dropped. Across 22 starts the saved settings passed the check every time. --- tools/opengc573-userd/gc573_user_rx_eq.c | 2 ++ tools/opengc573-userd/gc573_user_rx_eq.h | 2 ++ tools/opengc573-userd/gc573_user_streamer.c | 12 +++++++++--- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/tools/opengc573-userd/gc573_user_rx_eq.c b/tools/opengc573-userd/gc573_user_rx_eq.c index ca03249..62a2663 100644 --- a/tools/opengc573-userd/gc573_user_rx_eq.c +++ b/tools/opengc573-userd/gc573_user_rx_eq.c @@ -586,6 +586,8 @@ int gc573_rx_eq_tune(void *bar0, enum gc573_rx_port port, if (saved && saved->valid && (!saved->dfe || p->dfe)) { TRY(eq_apply_state(bar0, p, saved)); TRY(ced_measure(bar0, p, CED_WINDOW_MS, &ced)); + res->saved_tried = true; + res->saved_errors = ced_total(&ced); if (ced_any_valid(&ced) && ced_total(&ced) == 0) { res->used_saved = true; res->ced_valid = true; diff --git a/tools/opengc573-userd/gc573_user_rx_eq.h b/tools/opengc573-userd/gc573_user_rx_eq.h index 65e0641..148becd 100644 --- a/tools/opengc573-userd/gc573_user_rx_eq.h +++ b/tools/opengc573-userd/gc573_user_rx_eq.h @@ -47,6 +47,8 @@ struct gc573_rx_eq_state { struct gc573_rx_eq_result { bool ran; /* false: not HDMI 2.0, disabled, or clean */ bool used_saved; /* the saved settings were clean; no auto-EQ */ + bool saved_tried; /* saved settings were applied and measured */ + u32 saved_errors; /* CED with the saved settings */ bool ced_valid; /* CED counters reported valid counts */ bool before_valid; /* baseline window had valid counts */ bool auto_eq_done; /* hardware auto-EQ reported completion */ diff --git a/tools/opengc573-userd/gc573_user_streamer.c b/tools/opengc573-userd/gc573_user_streamer.c index cc2383e..d3c3f20 100644 --- a/tools/opengc573-userd/gc573_user_streamer.c +++ b/tools/opengc573-userd/gc573_user_streamer.c @@ -1248,7 +1248,7 @@ static void tune_link_eq(void *bar0) struct gc573_rx_eq_result eq; const char *name = gc573_rx_port_name(port); int ret = gc573_rx_eq_tune(bar0, port, &g_eq_saved[port], &eq); - char before[16], after[16]; + char before[16], after[16], saved_info[24]; if (ret < 0) { pr_warn("HDMI 2.0 link EQ (%s) failed: %d", name, ret); @@ -1268,6 +1268,11 @@ static void tune_link_eq(void *bar0) g_eq_saved[port] = eq.state; save = true; } + if (!eq.saved_tried) + snprintf(saved_info, sizeof(saved_info), "none"); + else + snprintf(saved_info, sizeof(saved_info), "%u errors", + eq.saved_errors); if (eq.before_valid) snprintf(before, sizeof(before), "%u", eq.errors_before); else @@ -1276,12 +1281,13 @@ static void tune_link_eq(void *bar0) snprintf(after, sizeof(after), "%u", eq.errors_after); else snprintf(after, sizeof(after), "n/a"); - printf("[HDMI] Link EQ %s: TMDS errors %s -> %s per 100 ms (B/G/R %u/%u/%u) | RS B/G/R 0x%02x/0x%02x/0x%02x | auto-EQ %s | DFE %s | sweep %s | skew %s | %u ms\n", + printf("[HDMI] Link EQ %s: TMDS errors %s -> %s per 100 ms (B/G/R %u/%u/%u) | RS B/G/R 0x%02x/0x%02x/0x%02x | saved EQ %s | auto-EQ %s | DFE %s | sweep %s | skew %s | %u ms\n", name, before, after, eq.ch_errors_after[0], eq.ch_errors_after[1], eq.ch_errors_after[2], eq.rs[0], eq.rs[1], eq.rs[2], - eq.used_saved ? "skipped (saved EQ clean)" : + saved_info, + eq.used_saved ? "skipped" : eq.auto_eq_done ? "done" : "failed", eq.dfe_applied ? "on" : "off", !eq.swept ? "no" : eq.kept_auto ? "lost to auto-EQ" : "yes",