joeylib2/tests/host/millisHost.c

134 lines
5 KiB
C

// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
// See LICENSE.md for license information.
// SPDX-License-Identifier: MIT
//
// millisHost.c - Host-cc harness for jlpGenericMillisElapsed (the
// IIgs/Amiga millisecond clock). Drives simulated frame-count sequences,
// including the 16-bit wrap at 65536 frames, and asserts the millisecond
// clock never runs backward (claudeDocs/PERF-AUDIT.md finding #23: it once
// jumped back every 65536 frames); and steps it a few frames at a time,
// the per-frame path and past it the divide, checking it exact at every
// step.
//
// Built by scripts/check-millis.sh as the blank port, whose platform block
// declares JL_HAS_FRAME_COUNT and JL_HAS_FRAME_HZ, so port.h routes
// jlpFrameCount/jlpFrameHz to the mock implementations below instead of
// the generic stubs.
#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include "port.h"
static uint16_t gMockFrame = 0;
static uint16_t gMockHz = 60;
// Prototypes (harness-local, alphabetical).
static bool runScenario(const char *name, uint16_t hz, const uint32_t *frames, int count);
static bool runSteps(const char *name, uint16_t hz);
uint16_t jlpFrameCount(void) {
return gMockFrame;
}
uint16_t jlpFrameHz(void) {
return gMockHz;
}
// Walk an absolute-frame sequence (values may exceed 65535 to express
// real elapsed frames; the mock truncates to uint16_t exactly like real
// hardware counters wrap). Asserts millis is non-decreasing and lands
// within 1 frame-period of the true elapsed time at the end.
static bool runScenario(const char *name, uint16_t hz, const uint32_t *frames, int count) {
uint32_t prevMs;
uint32_t ms;
uint32_t expectMs;
uint32_t tolerance;
bool ok;
int i;
gMockHz = hz;
ok = true;
prevMs = 0;
ms = 0;
for (i = 0; i < count; i++) {
gMockFrame = (uint16_t)frames[i];
ms = jlpGenericMillisElapsed();
if (ms < prevMs) {
printf(" %s: BACKWARD at step %d (frame %lu): %lu ms -> %lu ms\n", name, i, (unsigned long)frames[i], (unsigned long)prevMs, (unsigned long)ms);
ok = false;
}
prevMs = ms;
}
expectMs = (uint32_t)(((uint64_t)frames[count - 1] * 1000u) / hz);
tolerance = (uint32_t)(1000u / hz + 1u);
if (ok && (ms + tolerance < expectMs || ms > expectMs + tolerance)) {
printf(" %s: DRIFT: got %lu ms, expected ~%lu ms (+/- %lu)\n", name, (unsigned long)ms, (unsigned long)expectMs, (unsigned long)tolerance);
ok = false;
}
printf(" %s: %s\n", name, ok ? "PASS" : "FAIL");
return ok;
}
// Step the clock by 1 to STEP_DELTAS frames in turn, a few past the
// per-frame path into the divide, and check it at every step against the
// exact elapsed milliseconds: whole at 50 Hz, the thirds carried at 60.
#define STEP_DELTAS 10
#define STEP_COUNT 2000
#define STEP_BASE 1000u
static bool runSteps(const char *name, uint16_t hz) {
uint32_t elapsed;
uint32_t ms;
uint32_t expectMs;
bool ok;
int i;
gMockHz = hz;
gMockFrame = (uint16_t)STEP_BASE;
ok = jlpGenericMillisElapsed() == 0u;
elapsed = 0u;
for (i = 0; i < STEP_COUNT && ok; i++) {
elapsed = elapsed + (uint32_t)(1 + i % STEP_DELTAS);
gMockFrame = (uint16_t)(STEP_BASE + elapsed);
ms = jlpGenericMillisElapsed();
expectMs = (uint32_t)(((uint64_t)elapsed * 1000u) / hz);
if (ms != expectMs) {
printf(" %s: step %d (%lu frames): %lu ms, expected %lu ms\n", name, i, (unsigned long)elapsed, (unsigned long)ms, (unsigned long)expectMs);
ok = false;
}
}
printf(" %s: %s\n", name, ok ? "PASS" : "FAIL");
return ok;
}
int main(void) {
// Sequences are ABSOLUTE frame counts; steps stay under 65535 frames
// apart (the documented poll-at-least-once-per-wrap requirement).
static const uint32_t simple60[] = { 0, 60, 600, 3600, 36000 };
static const uint32_t wrap60[] = { 0, 30000, 65000, 70000, 131000, 190000, 200000 };
static const uint32_t wrap50[] = { 0, 40000, 65500, 66000, 131000 };
bool ok = true;
printf("millisHost: jlpGenericMillisElapsed scenarios\n");
// Scenario order alternates Hz deliberately: the accumulator's
// statics persist across scenarios, and a frame-rate change is the
// documented re-base point -- alternating gives each scenario a
// fresh epoch instead of inheriting the previous one's baseline.
ok &= runScenario("simple-60hz", 60, simple60, (int)(sizeof(simple60) / sizeof(simple60[0])));
ok &= runScenario("wrap-50hz", 50, wrap50, (int)(sizeof(wrap50) / sizeof(wrap50[0])));
ok &= runScenario("wrap-60hz", 60, wrap60, (int)(sizeof(wrap60) / sizeof(wrap60[0])));
ok &= runSteps("steps-50hz", 50);
ok &= runSteps("steps-60hz", 60);
if (!ok) {
printf("millisHost: FAIL\n");
return 1;
}
printf("millisHost: PASS\n");
return 0;
}