Files
akbasic/examples/galaga/interop_test.c

266 lines
8.1 KiB
C
Raw Permalink Normal View History

/**
* @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 <math.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <akerror.h>
#include <akgl/actor.h>
#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;
}