Benchmark the boundary and close the cold read's tutorial gaps
The interop test now ends with a measured comparison: 24,000 formation updates through the script boundary against a line-for-line C translation of the same state machine. 881 us against 0.01 us per call on this machine, quoted verbatim in the new chapter 21 Step 11 with the architectural decisions it prices. A Haiku-class cold read of the chapters produced a build whose failures were all mechanical -- invented include paths, never-shown sink statics, guessed status codes and character names. The chapters now carry the include lists, the script.c statics, the status-code roster, the sprite/character table, the full CMake recipe and the explosion spawn's HANDLE example, so none of those have to be guessed again. Co-authored-by: andrew <andrew@aklabs.net> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XiGgpHuXUm2mR4Wzndw3dc
This commit is contained in:
@@ -304,7 +304,7 @@ if(AKBASIC_BUILD_EXAMPLES AND AKBASIC_WITH_AKGL)
|
|||||||
target_compile_definitions(akbasic_example_galaga_interop PRIVATE
|
target_compile_definitions(akbasic_example_galaga_interop PRIVATE
|
||||||
GALAGA_SCRIPT_PATH="${CMAKE_CURRENT_SOURCE_DIR}/examples/galaga/galaga.bas")
|
GALAGA_SCRIPT_PATH="${CMAKE_CURRENT_SOURCE_DIR}/examples/galaga/galaga.bas")
|
||||||
target_link_libraries(akbasic_example_galaga_interop PRIVATE akbasic akgl
|
target_link_libraries(akbasic_example_galaga_interop PRIVATE akbasic akgl
|
||||||
SDL3::SDL3)
|
SDL3::SDL3 m)
|
||||||
akbasic_instrument(akbasic_example_galaga_interop)
|
akbasic_instrument(akbasic_example_galaga_interop)
|
||||||
_add_test(NAME example_galaga_interop COMMAND akbasic_example_galaga_interop)
|
_add_test(NAME example_galaga_interop COMMAND akbasic_example_galaga_interop)
|
||||||
_set_tests_properties(example_galaga_interop PROPERTIES TIMEOUT 120)
|
_set_tests_properties(example_galaga_interop PROPERTIES TIMEOUT 120)
|
||||||
|
|||||||
@@ -125,7 +125,41 @@ writes `x` and `y` directly is the mover, and in this game that will be BASIC.
|
|||||||
**Error handling is the house protocol.** Every function returns
|
**Error handling is the house protocol.** Every function returns
|
||||||
`akerr_ErrorContext *`, `PASS` propagates, `ATTEMPT`/`CATCH`/`CLEANUP` brackets
|
`akerr_ErrorContext *`, `PASS` propagates, `ATTEMPT`/`CATCH`/`CLEANUP` brackets
|
||||||
anything that must unwind. libakgl's docs/04-errors.md teaches it; this chapter
|
anything that must unwind. libakgl's docs/04-errors.md teaches it; this chapter
|
||||||
just uses it.
|
just uses it. The status codes this game raises are `AKERR_NULLPOINTER`,
|
||||||
|
`AKERR_VALUE`, `AKERR_KEY`, `AKERR_IO`, `AKERR_OUTOFBOUNDS`, `AKGL_ERR_SDL`
|
||||||
|
and `AKGL_ERR_HEAP` — there is no code this tutorial invents.
|
||||||
|
|
||||||
|
The includes the engine files draw on, so nothing later has to be guessed —
|
||||||
|
the SDL satellites use their own prefixes (`SDL3_ttf/SDL_ttf.h`, not
|
||||||
|
`SDL3/SDL_ttf.h`):
|
||||||
|
|
||||||
|
```c wrap=galagatypes requires=akgl
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
#include <SDL3_image/SDL_image.h>
|
||||||
|
#include <SDL3_ttf/SDL_ttf.h>
|
||||||
|
|
||||||
|
#include <akerror.h>
|
||||||
|
#include <akstdlib.h>
|
||||||
|
|
||||||
|
#include <akgl/actor.h>
|
||||||
|
#include <akgl/character.h>
|
||||||
|
#include <akgl/controller.h>
|
||||||
|
#include <akgl/draw.h>
|
||||||
|
#include <akgl/error.h>
|
||||||
|
#include <akgl/game.h>
|
||||||
|
#include <akgl/heap.h>
|
||||||
|
#include <akgl/physics.h>
|
||||||
|
#include <akgl/registry.h>
|
||||||
|
#include <akgl/renderer.h>
|
||||||
|
#include <akgl/sprite.h>
|
||||||
|
#include <akgl/text.h>
|
||||||
|
#include <akgl/ui.h>
|
||||||
|
#include <akgl/util.h>
|
||||||
|
```
|
||||||
|
|
||||||
The frame loop is the standard bracket, with one addition you will meet in
|
The frame loop is the standard bracket, with one addition you will meet in
|
||||||
Step 6 — for now, events in, world drawn, frame out:
|
Step 6 — for now, events in, world drawn, frame out:
|
||||||
@@ -198,6 +232,20 @@ actor state words to **sprites** (libakgl docs/10 and 12). Both are JSON; load
|
|||||||
sprites first, because a character names its sprites and a character loaded
|
sprites first, because a character names its sprites and a character loaded
|
||||||
first fails on the first name it cannot find.
|
first fails on the first name it cannot find.
|
||||||
|
|
||||||
|
These are the names, so the loading lists and every
|
||||||
|
`akgl_actor_set_character()` call in both chapters agree — each `sprite_*.json`
|
||||||
|
and `character_*.json` lives in `assets/`:
|
||||||
|
|
||||||
|
| Character | Sprite(s) it maps | Worn by |
|
||||||
|
|---|---|---|
|
||||||
|
| `galaga_player` | `galaga_player` | the ship |
|
||||||
|
| `galaga_bee` | `galaga_bee` | bees |
|
||||||
|
| `galaga_butterfly` | `galaga_butterfly` | butterflies |
|
||||||
|
| `galaga_boss` | `galaga_boss`, and `galaga_boss_hurt` on state bit 13 | bosses |
|
||||||
|
| `galaga_playershot` | `galaga_playershot` | the ship's shots |
|
||||||
|
| `galaga_enemyshot` | `galaga_enemyshot` | enemy shots |
|
||||||
|
| `galaga_boom` | `galaga_boom` | explosions |
|
||||||
|
|
||||||
The spawn is four decisions after the two boilerplate calls:
|
The spawn is four decisions after the two boilerplate calls:
|
||||||
|
|
||||||
```c wrap=galagagame requires=akgl
|
```c wrap=galagagame requires=akgl
|
||||||
@@ -338,18 +386,84 @@ Four conventions worth keeping:
|
|||||||
Give the enemy shots the same shape falling downward, and the ship a sweep over
|
Give the enemy shots the same shape falling downward, and the ship a sweep over
|
||||||
both — `examples/galaga/player.c` has all three loops.
|
both — `examples/galaga/player.c` has all three loops.
|
||||||
|
|
||||||
|
Explosions are the fourth actor kind, and they carry the one place this game
|
||||||
|
*absorbs* an error instead of propagating it. `HANDLE` names the status it
|
||||||
|
forgives; everything else still travels:
|
||||||
|
|
||||||
|
```c wrap=galagagame requires=akgl
|
||||||
|
static float BOOM_TTL[AKGL_MAX_HEAP_ACTOR];
|
||||||
|
static uint32_t BOOM_SERIAL = 0;
|
||||||
|
|
||||||
|
static akerr_ErrorContext *boom_update(akgl_Actor *obj)
|
||||||
|
{
|
||||||
|
ptrdiff_t slot = 0;
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj");
|
||||||
|
slot = obj - akgl_heap_actors;
|
||||||
|
BOOM_TTL[slot] -= galaga_game.dt;
|
||||||
|
if ( BOOM_TTL[slot] <= 0.0f ) {
|
||||||
|
PASS(errctx, akgl_heap_release_actor(obj));
|
||||||
|
}
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *galaga_boom_spawn(float x, float y)
|
||||||
|
{
|
||||||
|
akgl_Actor *boom = NULL;
|
||||||
|
char name[32];
|
||||||
|
int count = 0;
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
ATTEMPT {
|
||||||
|
CATCH(errctx, akgl_heap_next_actor(&boom));
|
||||||
|
BOOM_SERIAL += 1;
|
||||||
|
CATCH(errctx, aksl_snprintf(&count, name, sizeof(name), "boom%u", BOOM_SERIAL));
|
||||||
|
CATCH(errctx, akgl_actor_initialize(boom, name));
|
||||||
|
CATCH(errctx, akgl_actor_set_character(boom, "galaga_boom"));
|
||||||
|
boom->updatefunc = &boom_update;
|
||||||
|
boom->movement_controls_face = false;
|
||||||
|
boom->state = AKGL_ACTOR_STATE_ALIVE;
|
||||||
|
boom->visible = true;
|
||||||
|
boom->x = x;
|
||||||
|
boom->y = y;
|
||||||
|
BOOM_TTL[boom - akgl_heap_actors] = 0.25f;
|
||||||
|
} CLEANUP {
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} HANDLE(errctx, AKGL_ERR_HEAP) {
|
||||||
|
/* Explosions are decoration. When the heap is momentarily full the
|
||||||
|
* right outcome is no explosion, not a dead frame. */
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Step 5: Boot the interpreter
|
## Step 5: Boot the interpreter
|
||||||
|
|
||||||
**Goal: the engine calls a BASIC function and prints its answer.**
|
**Goal: the engine calls a BASIC function and prints its answer.**
|
||||||
|
|
||||||
Everything so far was libakgl. Now link the interpreter into the same
|
Everything so far was libakgl. Now link the interpreter into the same
|
||||||
executable. In CMake:
|
executable. The whole CMake recipe, inside an akbasic checkout with
|
||||||
|
`AKBASIC_WITH_AKGL=ON`:
|
||||||
|
|
||||||
```cmake
|
```cmake
|
||||||
target_link_libraries(akbasic_example_galaga PRIVATE akbasic akgl
|
add_executable(mygalaga
|
||||||
|
main.c
|
||||||
|
script.c
|
||||||
|
enemies.c
|
||||||
|
player.c)
|
||||||
|
target_compile_options(mygalaga PRIVATE -Wall -Wextra)
|
||||||
|
target_compile_definitions(mygalaga PRIVATE
|
||||||
|
GALAGA_ASSET_DIR="${CMAKE_CURRENT_SOURCE_DIR}/assets"
|
||||||
|
GALAGA_SCRIPT_PATH="${CMAKE_CURRENT_SOURCE_DIR}/galaga.bas"
|
||||||
|
GALAGA_FONT_PATH="${CMAKE_CURRENT_SOURCE_DIR}/assets/fonts/C64_Pro_Mono-STYLE.ttf")
|
||||||
|
target_link_libraries(mygalaga PRIVATE akbasic akgl
|
||||||
SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::SDL3_image)
|
SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::SDL3_image)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The three baked-in paths are what let the program launch from any working
|
||||||
|
directory; `--assets` and `--script` flags can override them at runtime.
|
||||||
|
|
||||||
Link `akbasic` — the interpreter only. Not `akbasic_akgl` (the device backends
|
Link `akbasic` — the interpreter only. Not `akbasic_akgl` (the device backends
|
||||||
that let a script draw), and not `akbasic_frontend` (the standalone program's
|
that let a script draw), and not `akbasic_frontend` (the standalone program's
|
||||||
host). This game lends the script **no devices at all**: the scripts compute,
|
host). This game lends the script **no devices at all**: the scripts compute,
|
||||||
@@ -360,7 +474,25 @@ declines to use.
|
|||||||
|
|
||||||
The boot is the embedding host from Chapter 10, adapted to a script that only
|
The boot is the embedding host from Chapter 10, adapted to a script that only
|
||||||
defines. Keep every line that touches the interpreter in one file — the
|
defines. Keep every line that touches the interpreter in one file — the
|
||||||
example's `script.c` — so the boundary stays a place rather than a habit:
|
example's `script.c` — so the boundary stays a place rather than a habit. That
|
||||||
|
file's interpreter-facing includes and statics, exactly:
|
||||||
|
|
||||||
|
```c wrap=galagatypes requires=akgl
|
||||||
|
#include <akbasic/environment.h>
|
||||||
|
#include <akbasic/error.h>
|
||||||
|
#include <akbasic/host.h>
|
||||||
|
#include <akbasic/runtime.h>
|
||||||
|
#include <akbasic/sink.h>
|
||||||
|
|
||||||
|
/* Static because an akbasic_Runtime is far too big for a stack frame --
|
||||||
|
* 2.40 MiB on this branch. */
|
||||||
|
static akbasic_Runtime SCRIPT;
|
||||||
|
static akbasic_TextSink SINK;
|
||||||
|
static akbasic_StdioSink SINKSTATE;
|
||||||
|
static char SOURCE[16384];
|
||||||
|
```
|
||||||
|
|
||||||
|
The boot itself:
|
||||||
|
|
||||||
```c wrap=galagacalls requires=akgl
|
```c wrap=galagacalls requires=akgl
|
||||||
CATCH(errctx, akbasic_error_register());
|
CATCH(errctx, akbasic_error_register());
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ whole development loop; the engine never rebuilds.
|
|||||||
the game, and make it do that
|
the game, and make it do that
|
||||||
- **[Step 10](#step-10-prove-it)** — prove the boundary with a test that links
|
- **[Step 10](#step-10-prove-it)** — prove the boundary with a test that links
|
||||||
the real files
|
the real files
|
||||||
|
- **[Step 11](#step-11-the-cost-measured)** — measure what thinking in BASIC
|
||||||
|
costs, against the same logic in C
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -507,6 +509,56 @@ the readout tells you how each brain did:
|
|||||||
galaga: 3000 frames, screen 2, score 2350, alive 0, kills bee 20 bfly 15 boss 1, shots bee 1 bfly 1 boss 1, script errors 0
|
galaga: 3000 frames, screen 2, score 2350, alive 0, kills bee 20 bfly 15 boss 1, shots bee 1 bfly 1 boss 1, script errors 0
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Step 11: The cost, measured
|
||||||
|
|
||||||
|
**Goal: the real price of the boundary, in numbers, next to the same logic in C.**
|
||||||
|
|
||||||
|
The interop test binary ends with a benchmark: 24,000 formation-hold updates —
|
||||||
|
forty enemies at sixty frames a second for ten seconds — once through
|
||||||
|
`galaga_script_update_enemy()` and once through a line-for-line C translation
|
||||||
|
of `UPDATEBEE` with its helpers inlined. Same guard, same branches, same
|
||||||
|
arithmetic; the difference is the interpreter. On this repository's build
|
||||||
|
machine (a two-core VM, the interpreter built `-O2`):
|
||||||
|
|
||||||
|
```text
|
||||||
|
benchmark: 24000 formation-hold updates, dt 0.016
|
||||||
|
BASIC through the boundary: 21.147 s 881.11 us/call 35.245 ms per 40-enemy frame
|
||||||
|
the same logic in C: 0.000 s 0.01 us/call 0.001 ms per 40-enemy frame
|
||||||
|
ratio: 61022x
|
||||||
|
```
|
||||||
|
|
||||||
|
The facts, without decoration:
|
||||||
|
|
||||||
|
- **A BASIC-driven update costs about four orders of magnitude more than the
|
||||||
|
same logic compiled.** The C translation of the whole state machine costs
|
||||||
|
tens of *nano*seconds; the scripted call costs high hundreds of
|
||||||
|
*micro*seconds.
|
||||||
|
- **The cost is per line executed, not per call.** The interpreter scans and
|
||||||
|
parses each body line from source text on every call; a 3-line body measured
|
||||||
|
~148 us on this class of machine, and this ~15-line body measures ~881 us.
|
||||||
|
Body length is the knob.
|
||||||
|
- **At this cost, forty thinking enemies spend ~35 ms per frame on this
|
||||||
|
hardware** — more than two 60 Hz frames. The shipped example visibly runs
|
||||||
|
below 60 fps on this machine while the whole wave is alive, and exactly at
|
||||||
|
its frame pace once the wave thins. A faster machine moves the numbers, not
|
||||||
|
the shape.
|
||||||
|
|
||||||
|
This is the measured version of decisions the chapters already made on
|
||||||
|
architectural grounds. Bullets, collision and the starfield are C
|
||||||
|
([Chapter 20](20-tutorial-galaga.md), Steps 2 and 4) — at two shots and forty
|
||||||
|
tests a frame, scripting them would multiply the call count for things that
|
||||||
|
decide nothing. The fire decision is one flag rather than a per-bullet
|
||||||
|
callback (Step 6): the script's call budget is bounded by the enemy count and
|
||||||
|
nothing else. C owns the formation and the spawn timing (Step 8), so zero
|
||||||
|
calls happen for enemies that do not exist yet. And the 36 KiB function slots
|
||||||
|
and 2.40 MiB runtime (Step 5) are the memory half of the same bill.
|
||||||
|
|
||||||
|
What the cost buys is the previous ten steps: behavior as data, edited and
|
||||||
|
swapped without a compiler. Whether ~900 us per thinking entity per frame is
|
||||||
|
acceptable is a per-project decision — fewer thinkers, shorter bodies, or a
|
||||||
|
lower think rate (every Nth frame) are the standard levers, and all three are
|
||||||
|
host-side choices this architecture leaves open.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Where to go from here: more waves are rows in the table; a new enemy kind is
|
Where to go from here: more waves are rows in the table; a new enemy kind is
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
#include <akerror.h>
|
#include <akerror.h>
|
||||||
|
|
||||||
@@ -115,12 +116,144 @@ static akerr_ErrorContext *run_claims(void)
|
|||||||
SUCCEED_RETURN(errctx);
|
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)
|
int main(void)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
CATCH(errctx, run_claims());
|
CATCH(errctx, run_claims());
|
||||||
|
CATCH(errctx, run_benchmark());
|
||||||
} CLEANUP {
|
} CLEANUP {
|
||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
} HANDLE_DEFAULT(errctx) {
|
} HANDLE_DEFAULT(errctx) {
|
||||||
|
|||||||
Reference in New Issue
Block a user