134 lines
5 KiB
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;
|
|
}
|