diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d9677e..c4f4b25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,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 diff --git a/tools/opengc573-userd/Makefile b/tools/opengc573-userd/Makefile index 0557ca6..735e4a6 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 third_party/jsmn.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_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; + } } } 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_rx_eq.c b/tools/opengc573-userd/gc573_user_rx_eq.c index 5dc8a03..62a2663 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,26 @@ 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)); + 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; + 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 +613,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 +637,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 +665,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..148becd 100644 --- a/tools/opengc573-userd/gc573_user_rx_eq.h +++ b/tools/opengc573-userd/gc573_user_rx_eq.h @@ -32,8 +32,23 @@ 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 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 */ @@ -46,6 +61,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 +92,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 0e54050..5c511f4 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 @@ -45,6 +46,7 @@ #include #include #include +#include #include #include #include @@ -1120,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 @@ -1129,15 +1237,18 @@ 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); - char before[16], after[16]; + int ret = gc573_rx_eq_tune(bar0, port, &g_eq_saved[port], &eq); + char before[16], after[16], saved_info[24]; if (ret < 0) { pr_warn("HDMI 2.0 link EQ (%s) failed: %d", name, ret); @@ -1153,6 +1264,15 @@ 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.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 @@ -1161,17 +1281,21 @@ 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], + 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", !eq.swept ? "auto" : eq.skew_trained ? "trained" : "forced 0", eq.elapsed_ms); } + if (save) + eq_state_save(); if (!touched) return; @@ -6053,9 +6177,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");