/** * @file interop_test.c * @brief Round-trip test for the galaga boundary, hoststruct.c-style. * * Links the real script.c and the real galaga.bas -- not copies -- so this * fails the moment the boundary and the script disagree. The four claims it * pins: * * 1. The script writes the engine's actor memory: a formation enemy's sway * lands in akgl_Actor.x with no marshalling step. * 2. The outbox works: a diving enemy above the player raises FIRE# and the * C side reads it. * 3. The boss flips actor state bit 13 at one hit point -- the boundary * crossed engine-ward. * 4. Sustained calling holds: 24000 calls through the per-call * akbasic_environment_zero() regime, the load a 40-enemy wave puts on * the runtime in ten seconds. * * Exit status equals the number of failed claims. */ #include #include #include #include #include #include #include #include #include "galaga.h" #ifndef GALAGA_SCRIPT_PATH #define GALAGA_SCRIPT_PATH "galaga.bas" #endif /* script.c reads these; main.c usually defines them. This test is the host. */ galaga_Game galaga_game; galaga_Shared galaga_shared; static int FAILURES = 0; #define CLAIM(__cond, __text) \ if ( !(__cond) ) { \ fprintf(stderr, "FAILED: %s\n", __text); \ FAILURES += 1; \ } else { \ printf("ok: %s\n", __text); \ } static akerr_ErrorContext *run_claims(void) { galaga_Enemy enemy; akgl_Actor actor; int i = 0; PREPARE_ERROR(errctx); PASS(errctx, galaga_script_boot((char *)GALAGA_SCRIPT_PATH)); /* --- 1: formation sway lands in the actor ---------------------------- */ memset(&enemy, 0, sizeof(enemy)); memset(&actor, 0, sizeof(actor)); enemy.kind = GALAGA_ENEMY_BEE; enemy.state = GALAGA_ES_FORMATION; enemy.homex = 400.0f; enemy.homey = 300.0f; enemy.hp = 1; actor.x = 0.0f; actor.y = 0.0f; PASS(errctx, galaga_script_update_enemy(&enemy, &actor, 0.016f)); CLAIM((fabsf(actor.x - enemy.homex) <= 16.5f) && (actor.y == enemy.homey), "a formation bee's sway is written into akgl_Actor.x/y by the script"); /* --- 2: the fire outbox ---------------------------------------------- */ memset(&enemy, 0, sizeof(enemy)); memset(&actor, 0, sizeof(actor)); enemy.kind = GALAGA_ENEMY_BEE; enemy.state = GALAGA_ES_DIVING; enemy.rnd = 0.0f; /* 0.0 < DT% * 1.5: always willing */ actor.x = 600.0f; actor.y = 200.0f; galaga_shared.playerx = 610.0f; /* just off the shot's column */ galaga_shared.playery = 860.0f; /* well below */ PASS(errctx, galaga_script_update_enemy(&enemy, &actor, 0.016f)); CLAIM(enemy.fire == 1, "a diving bee above the player raises FIRE# for the engine to consume"); /* --- 3: the boss's hurt bit ------------------------------------------ */ memset(&enemy, 0, sizeof(enemy)); memset(&actor, 0, sizeof(actor)); enemy.kind = GALAGA_ENEMY_BOSS; enemy.state = GALAGA_ES_FORMATION; enemy.homex = 500.0f; enemy.homey = 120.0f; enemy.hp = 1; actor.state = AKGL_ACTOR_STATE_ALIVE; PASS(errctx, galaga_script_update_enemy(&enemy, &actor, 0.016f)); CLAIM((actor.state & AKGL_ACTOR_STATE_UNDEFINED_13) != 0, "a boss at one hit point raises actor state bit 13 from BASIC"); /* --- 4: sustained calling --------------------------------------------- */ memset(&enemy, 0, sizeof(enemy)); memset(&actor, 0, sizeof(actor)); enemy.kind = GALAGA_ENEMY_BEE; enemy.state = GALAGA_ES_FORMATION; enemy.homex = 400.0f; enemy.homey = 300.0f; for ( i = 0; i < 24000; i++ ) { enemy.rnd = 0.9f; /* never dive: keep the state put */ PASS(errctx, galaga_script_update_enemy(&enemy, &actor, 0.016f)); } CLAIM(galaga_game.script_errors == 0, "24000 calls survive the per-call akbasic_environment_zero() regime"); SUCCEED_RETURN(errctx); } /* ------------------------------------------------------------ benchmark --- */ /** * @brief UPDATEBEE's state machine, translated line for line into C. * * The native comparator for the benchmark below: the same guard, the same * three branches, the same arithmetic as galaga.bas's UPDATEBEE with its * helpers inlined. Nothing is simplified, so the timing difference is the * interpreter's, not the algorithm's. */ static void native_updatebee(galaga_Enemy *enemy, akgl_Actor *actor, float dt) { float dx = 0.0f; float dy = 0.0f; float k = 0.0f; int32_t s = 0; enemy->t += dt; if ( enemy->t < 0.0f ) { return; } s = enemy->state; if ( (s & GALAGA_ES_ENTERING) != 0 ) { dx = enemy->homex - actor->x; dy = enemy->homey - actor->y; k = dt * 4.5f; if ( k > 1.0f ) { k = 1.0f; } actor->x += dx * k + sinf(enemy->t * 6.0f) * 90.0f * dt; actor->y += dy * k; if ( fabsf(dx) < 3.0f && fabsf(dy) < 3.0f ) { enemy->state = GALAGA_ES_FORMATION; enemy->t = 0.0f; } } if ( (s & GALAGA_ES_FORMATION) != 0 ) { actor->x = enemy->homex + sinf(enemy->t * 1.7f) * 16.0f; actor->y = enemy->homey; if ( enemy->rnd < dt * 0.04f ) { enemy->state = GALAGA_ES_DIVING; enemy->t = 0.0f; } } if ( (s & GALAGA_ES_DIVING) != 0 ) { actor->y += (enemy->t * 150.0f + 260.0f) * dt; actor->x += sinf(enemy->t * 4.0f) * 130.0f * dt; dx = galaga_shared.playerx - actor->x; if ( dx > 220.0f ) { dx = 220.0f; } if ( dx < -220.0f ) { dx = -220.0f; } actor->x += dx * 0.2f * dt; if ( actor->y > 1040.0f ) { actor->y = -90.0f; enemy->state = GALAGA_ES_ENTERING; enemy->t = 0.0f; } dx = galaga_shared.playerx - actor->x; if ( fabsf(dx) <= 140.0f && actor->y <= galaga_shared.playery && enemy->rnd < dt * 1.5f ) { enemy->fire = 1; } } } static double seconds_since(const struct timespec *t0) { struct timespec t1; clock_gettime(CLOCK_MONOTONIC, &t1); return (double)(t1.tv_sec - t0->tv_sec) + (double)(t1.tv_nsec - t0->tv_nsec) / 1e9; } /** * @brief The cost of thinking in BASIC, measured against the same logic in C. * * Both loops run the identical formation-hold workload, 24,000 calls -- forty * enemies at sixty frames a second for ten seconds. Informational: nothing * asserts on the timing, because CI machines vary; the numbers print so the * tutorial can quote a real measurement. */ static akerr_ErrorContext *run_benchmark(void) { galaga_Enemy enemy; akgl_Actor actor; struct timespec t0; double basic_s = 0.0; double native_s = 0.0; int i = 0; PREPARE_ERROR(errctx); memset(&enemy, 0, sizeof(enemy)); memset(&actor, 0, sizeof(actor)); enemy.kind = GALAGA_ENEMY_BEE; enemy.state = GALAGA_ES_FORMATION; enemy.homex = 400.0f; enemy.homey = 300.0f; enemy.rnd = 0.9f; clock_gettime(CLOCK_MONOTONIC, &t0); for ( i = 0; i < 24000; i++ ) { PASS(errctx, galaga_script_update_enemy(&enemy, &actor, 0.016f)); } basic_s = seconds_since(&t0); memset(&enemy, 0, sizeof(enemy)); memset(&actor, 0, sizeof(actor)); enemy.kind = GALAGA_ENEMY_BEE; enemy.state = GALAGA_ES_FORMATION; enemy.homex = 400.0f; enemy.homey = 300.0f; enemy.rnd = 0.9f; clock_gettime(CLOCK_MONOTONIC, &t0); for ( i = 0; i < 24000; i++ ) { native_updatebee(&enemy, &actor, 0.016f); } native_s = seconds_since(&t0); printf("benchmark: 24000 formation-hold updates, dt 0.016\n"); printf(" BASIC through the boundary: %8.3f s %7.2f us/call %6.3f ms per 40-enemy frame\n", basic_s, basic_s / 24000.0 * 1e6, basic_s / 24000.0 * 40.0 * 1e3); printf(" the same logic in C: %8.3f s %7.2f us/call %6.3f ms per 40-enemy frame\n", native_s, native_s / 24000.0 * 1e6, native_s / 24000.0 * 40.0 * 1e3); printf(" ratio: %.0fx\n", basic_s / native_s); SUCCEED_RETURN(errctx); } int main(void) { PREPARE_ERROR(errctx); ATTEMPT { CATCH(errctx, run_claims()); CATCH(errctx, run_benchmark()); } CLEANUP { } PROCESS(errctx) { } HANDLE_DEFAULT(errctx) { LOG_ERROR_WITH_MESSAGE(errctx, "the interop test could not run"); FAILURES += 1; } FINISH_NORETURN(errctx); return FAILURES; }