From 1878af92911c6cdaab9ec6a56d4046ee88df82a6 Mon Sep 17 00:00:00 2001 From: Jonathan Steffan Date: Sat, 1 Aug 2026 13:45:35 -0600 Subject: [PATCH 1/3] src/driver_usbmon.c: gzip_cookie_seek: fix pointer type on 32-bit targets glibc's cookie_seek_function_t is always int (*)(void *cookie, __off64_t *pos, int whence) regardless of _FILE_OFFSET_BITS. gzip_cookie_seek() declared its position argument as z_off_t *, which zlib defines as off_t. On LP64 targets off_t and __off64_t are both long, so the mismatch is invisible; on 32-bit targets such as i686, off_t is a 32-bit long while __off64_t is long long, and initialising cookie_io_functions_t::seek fails to compile: src/driver_usbmon.c:796:99: error: initialization of 'int (*)(void *, __off64_t *, int)' from incompatible pointer type 'int (*)(void *, off_t *, int)' [-Wincompatible-pointer-types] CMakeLists.txt passes -Werror=incompatible-pointer-types, and GCC 14 promotes that warning to an error by default, so this breaks the 32-bit build outright. Use off64_t and gzseek64() so the type matches on every architecture. The file already defines _GNU_SOURCE ahead of every include, which implies _LARGEFILE64_SOURCE, so both are available. While here, follow the fopencookie(3) contract: the seek callback must return 0 on success and -1 on error, storing the resulting offset in *pos. The old code returned the new offset directly and never wrote back to *pos, so fseek() and ftell() on a compressed playback stream misbehaved on every architecture. Also make the function static, matching its sibling callbacks. Assisted-by: Claude Code:claude-sonnet-4-6 --- src/driver_usbmon.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/driver_usbmon.c b/src/driver_usbmon.c index a476afe7..0858e5c8 100644 --- a/src/driver_usbmon.c +++ b/src/driver_usbmon.c @@ -790,7 +790,14 @@ static int gzip_cookie_close(void *cookie) { return gzclose((gzFile)cookie); } static ssize_t gzip_cookie_read(void *cookie, char *buf, size_t nbytes) { return gzread((gzFile)cookie, buf, nbytes); } -int gzip_cookie_seek(void *cookie, z_off_t *pos, int __w) { return gzseek((gzFile)cookie, *pos, __w); } +static int gzip_cookie_seek(void *cookie, off64_t *pos, int whence) { + z_off64_t r = gzseek64((gzFile)cookie, (z_off64_t)*pos, whence); + if (r < 0) + return -1; + + *pos = (off64_t)r; + return 0; +} cookie_io_functions_t gzip_cookie = { .close = gzip_cookie_close, .write = gzip_cookie_write, .read = gzip_cookie_read, .seek = gzip_cookie_seek}; From 9b61a8131a84a2acd1849a6ee17a46c2597394a0 Mon Sep 17 00:00:00 2001 From: Jonathan Steffan Date: Sat, 1 Aug 2026 15:55:15 -0600 Subject: [PATCH 2/3] src/driver_playback.c: parse_and_run_imu: parse IMU timecode as unsigned parse_and_run_imu() reads the IMU sample's timecode into a plain int via sscanf(..., "%d", &timecode, ...). Every other timecode field in the .rec.gz replay format (light, sweep, sync) is typed as survive_timecode (uint32_t) and parsed with %u; only the IMU path uses a signed int and %d. The recorded IMU timecode is a free-running 32-bit hardware counter, so it routinely exceeds INT32_MAX for the back half of any recording longer than about 45 seconds (2^31 ticks / 48MHz). On LP64 targets glibc's %d conversion happens to round-trip these large values because its internal accumulator is a 64-bit long; the out-of-range value is narrowed into the destination int by a bit-preserving truncation that reads back correctly once passed on as a uint32_t. On ILP32 targets such as i686, long is 32 bits -- the same width as int -- so the strtol-style overflow handling inside %d parsing saturates at LONG_MAX instead, and every IMU timecode above INT32_MAX is clamped to exactly 2147483647. That freezes SurviveSensorActivations::last_imu at 2147483647 for the rest of playback. SurviveSensorActivations_long_timecode_light() in src/survive_sensor_activations.c uses last_imu as the reference clock for the workaround that corrects the known lightcap/IMU FPGA clock desync in multiples of 2^28 ticks. With last_imu stuck, that correction miscomputes and yanks the light timecode backward by 5 * 2^28 ticks (~5.58s), which trips assert(tracker->model.t - t < 1); in survive_kalman_tracker_report_state() and aborts the process. This is the cause of the compare-test-index, drone, haagh-tracking, stool-test2, tracker-bad-tracking and tracker-throw-compare replay tests failing on i686 while passing on x86_64 from the same snapshot. Parse the field the way every other timecode is parsed: survive_timecode with %u. Assisted-by: Claude Code:claude-sonnet-4-6 --- src/driver_playback.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/driver_playback.c b/src/driver_playback.c index afe64206..0a17b08d 100644 --- a/src/driver_playback.c +++ b/src/driver_playback.c @@ -222,7 +222,7 @@ static int parse_and_run_imu(const char *line, SurvivePlaybackData *driver, bool return 0; char dev[10]; - int timecode = 0; + survive_timecode timecode = 0; FLT accelgyro[9] = { 0 }; int mask; int id; @@ -231,7 +231,7 @@ static int parse_and_run_imu(const char *line, SurvivePlaybackData *driver, bool char i_char = 0; int rr = sscanf(line, - "%s %c %d %d " FLT_sformat " " FLT_sformat " " FLT_sformat " " FLT_sformat " " FLT_sformat + "%s %c %d %u " FLT_sformat " " FLT_sformat " " FLT_sformat " " FLT_sformat " " FLT_sformat " " FLT_sformat " " FLT_sformat " " FLT_sformat " " FLT_sformat "%d", dev, &i_char, &mask, &timecode, &accelgyro[0], &accelgyro[1], &accelgyro[2], &accelgyro[3], &accelgyro[4], &accelgyro[5], &accelgyro[6], &accelgyro[7], &accelgyro[8], &id); From cc3c47aedfbc5abf10f5cddbc47039abd700a341 Mon Sep 17 00:00:00 2001 From: Jonathan Steffan Date: Sat, 1 Aug 2026 15:55:24 -0600 Subject: [PATCH 3/3] src/survive_sensor_activations.c: SurviveSensorActivations_long_timecode_light: use llabs() on int64_t time_sync_error is an int64_t, but labs() takes a long, which is 32 bits on ILP32 targets such as i686. The argument is truncated there before the magnitude test against 48000000, so a desync large enough to need correcting can be missed (or a small one spuriously flagged) depending on which bits survive the narrowing. This did not reproduce a test failure on its own -- DIV_ROUND_CLOSEST() below already operates on the untruncated 64-bit value, so once the branch is taken the correction itself is computed correctly -- but it is the same class of ILP32-vs-LP64 bug as the IMU timecode parse and sits directly next to the code that consumes it. Assisted-by: Claude Code:claude-sonnet-4-6 --- src/survive_sensor_activations.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/survive_sensor_activations.c b/src/survive_sensor_activations.c index e5d18e16..bdc3ee2f 100644 --- a/src/survive_sensor_activations.c +++ b/src/survive_sensor_activations.c @@ -454,7 +454,7 @@ SURVIVE_EXPORT survive_long_timecode SurviveSensorActivations_long_timecode_ligh use that as a basis. It's worth noting that I've never seen a system develop this while running; it would likely cause some chaos if it did since it'd kick the kalman out of sorts. ***/ - if (self->last_imu != 0 && labs(time_sync_error) > 48000000) { + if (self->last_imu != 0 && llabs(time_sync_error) > 48000000) { int64_t offset = 0x10000000; int scale = DIV_ROUND_CLOSEST(time_sync_error, offset); initial_time -= offset * scale;