2026-07-30 01:10:31 -04:00
|
|
|
/**
|
|
|
|
|
* @file physics.c
|
|
|
|
|
* @brief Implements the physics subsystem.
|
|
|
|
|
*/
|
|
|
|
|
|
2026-05-26 11:22:45 -04:00
|
|
|
#include <math.h>
|
2026-05-26 10:36:31 -04:00
|
|
|
#include <akstdlib.h>
|
Call collide from the simulation, after the move rather than before it
The collide slot has existed unused since the backend was written. It is wired
in now, and its signature changes from (self, a1, a2) to (self, actor, dt): the
old shape had nowhere to put a normal, a depth, or a piece of map geometry, and
no answer to which of the two actors it was supposed to be resolving.
**Resolution runs after `move`.** That is the decision everything else follows
from. A resolver called from movementlogicfunc runs before gravity, drag and the
move, so it has to predict where the actor will end up -- which means
re-implementing the integrator's arithmetic in the game, and being wrong
whenever the integrator changes. examples/sidescroller/collision.c carries forty
lines of exactly that, and says so.
Only `move` is subdivided. Gravity, drag, the thrust integration and the speed
ellipse all still run once against the whole dt, and sum(subdt) is dt, so an
actor with nothing to collide with takes one sub-step of exactly dt -- `dt/1.0f`
is dt exactly in IEEE-754 -- and follows the arithmetic path it always did. That
is what lets the integrator stay untouched and every recorded physics number
stay valid, and tests/physics.c and tests/physics_sim.c pass unchanged with no
world attached.
Collision is opt-in. A backend with no collision world costs one comparison per
actor per step and does nothing else.
Also here:
- `self->gravity` was never null-checked and is dereferenced unconditionally, so
a hand-built backend with only `move` filled in crashed rather than reporting.
- Children are re-snapped in a post-pass, gated on collision being on. The
in-loop snap uses whatever position the parent had when the child's slot came
up in pool order, which is already sometimes a frame stale; that was harmless
while a parent only moved by v*dt and is not once a parent can be pushed out
of a wall mid-step.
- tests/physics.c's deliberate tripwire has been visited. It asserted that
arcade collide always raised AKERR_API, with a comment saying it existed so
that implementing collision would have to come back here. What replaces it
asserts the opt-in contract instead.
- physics_sim.c's hard-coded "%d of 7 simulations" is derived now. A literal
goes stale the first time somebody adds a simulation, which is this commit.
Three new whole-motion simulations, and the third took two attempts to make
honest:
- Landing and resting: 0.0000 px of drift over 120 steps after settling.
- Walking after landing: 107 px in a second, which is the symptom a player sees
when a resting actor is caught on the ground it is standing on.
- A fast fall not tunnelling. The first version used gravity, so the step size
grew every frame and the actor happened to land *inside* the floor rather than
stepping over it -- it passed with sub-stepping disabled and proved nothing.
It uses a constant 1200 px/s now, so every step is exactly 60 pixels against a
32-pixel window in which an overlap exists, and the samples miss it cleanly.
With sub-stepping the actor stops at y=288; without it, y=1300.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 07:05:09 -04:00
|
|
|
#include <akgl/collision.h>
|
2026-05-26 10:36:31 -04:00
|
|
|
#include <akgl/physics.h>
|
|
|
|
|
#include <akgl/actor.h>
|
|
|
|
|
#include <akgl/game.h>
|
|
|
|
|
#include <akgl/error.h>
|
|
|
|
|
#include <akgl/heap.h>
|
|
|
|
|
#include <akgl/registry.h>
|
|
|
|
|
|
Give every exported function a declaration, and check that it stays that way
Closes internal-consistency items 7 through 15. Nineteen non-static functions
were in the ABI with no declaration anywhere, so no consumer could call them
and any consumer could collide with them.
The four gamepad_handle_* functions are the ones that mattered: controller.h
declared akgl_controller_handle_button_down and three siblings that did not
exist, so anything compiled against the header alone failed to link. The
definitions carry the declared names now, which also closes Defects -> Known
and still open item 10, and their documentation moved to the header.
The rest are either declared under a "part of the internal API" block --
akgl_game_save_actors and akgl_game_load_versioncmp, which tests/game.c had to
declare for itself, plus six tilemap loader helpers the untested-loader work
wants to reach -- or static, which is what the four save iterators and
load_objectnamemap should always have been. akgl_path_relative_from is deleted:
declared nowhere, called from nowhere, never wrote its output, and leaked a
pooled string on every call, so it closes Known and still open item 4 and item
40 by ceasing to exist.
scripts/check_api_surface.sh keeps it closed. It reads the built library's
dynamic symbol table and every public header with comments stripped, and fails
on an exported akgl_* symbol that is declared nowhere. Stripping comments is
the whole point -- four of these were mentioned in controller.h prose, which is
how they went unnoticed.
The pool-size ceilings are defined once, in heap.h, so the #ifndef override
hook fires for the first time; actor.h, sprite.h and character.h were defining
the same four unconditionally from headers heap.h includes above its own guard.
tests/header_pool_override.c fails the compile if that regresses.
Also here: (void) rather than () on the twelve no-argument entry points,
AKERR_NOIGNORE only on declarations, static helpers with the akgl_ prefix
dropped, and the six parameter-name mismatches. akgl_get_json_with_default had
its two contexts swapped rather than merely misspelled -- the incoming one was
`err` and its own was `e`, which is the name reserved for an incoming one.
24/24 pass, reindent --check clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 23:44:38 -04:00
|
|
|
akerr_ErrorContext *akgl_physics_null_gravity(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt)
|
2026-05-26 10:36:31 -04:00
|
|
|
{
|
2026-07-31 23:58:28 -04:00
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, actor, AKERR_NULLPOINTER, "actor");
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
2026-05-26 10:36:31 -04:00
|
|
|
}
|
|
|
|
|
|
Call collide from the simulation, after the move rather than before it
The collide slot has existed unused since the backend was written. It is wired
in now, and its signature changes from (self, a1, a2) to (self, actor, dt): the
old shape had nowhere to put a normal, a depth, or a piece of map geometry, and
no answer to which of the two actors it was supposed to be resolving.
**Resolution runs after `move`.** That is the decision everything else follows
from. A resolver called from movementlogicfunc runs before gravity, drag and the
move, so it has to predict where the actor will end up -- which means
re-implementing the integrator's arithmetic in the game, and being wrong
whenever the integrator changes. examples/sidescroller/collision.c carries forty
lines of exactly that, and says so.
Only `move` is subdivided. Gravity, drag, the thrust integration and the speed
ellipse all still run once against the whole dt, and sum(subdt) is dt, so an
actor with nothing to collide with takes one sub-step of exactly dt -- `dt/1.0f`
is dt exactly in IEEE-754 -- and follows the arithmetic path it always did. That
is what lets the integrator stay untouched and every recorded physics number
stay valid, and tests/physics.c and tests/physics_sim.c pass unchanged with no
world attached.
Collision is opt-in. A backend with no collision world costs one comparison per
actor per step and does nothing else.
Also here:
- `self->gravity` was never null-checked and is dereferenced unconditionally, so
a hand-built backend with only `move` filled in crashed rather than reporting.
- Children are re-snapped in a post-pass, gated on collision being on. The
in-loop snap uses whatever position the parent had when the child's slot came
up in pool order, which is already sometimes a frame stale; that was harmless
while a parent only moved by v*dt and is not once a parent can be pushed out
of a wall mid-step.
- tests/physics.c's deliberate tripwire has been visited. It asserted that
arcade collide always raised AKERR_API, with a comment saying it existed so
that implementing collision would have to come back here. What replaces it
asserts the opt-in contract instead.
- physics_sim.c's hard-coded "%d of 7 simulations" is derived now. A literal
goes stale the first time somebody adds a simulation, which is this commit.
Three new whole-motion simulations, and the third took two attempts to make
honest:
- Landing and resting: 0.0000 px of drift over 120 steps after settling.
- Walking after landing: 107 px in a second, which is the symptom a player sees
when a resting actor is caught on the ground it is standing on.
- A fast fall not tunnelling. The first version used gravity, so the step size
grew every frame and the actor happened to land *inside* the floor rather than
stepping over it -- it passed with sub-stepping disabled and proved nothing.
It uses a constant 1200 px/s now, so every step is exactly 60 pixels against a
32-pixel window in which an overlap exists, and the samples miss it cleanly.
With sub-stepping the actor stops at y=288; without it, y=1300.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 07:05:09 -04:00
|
|
|
akerr_ErrorContext *akgl_physics_null_collide(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt)
|
2026-05-26 10:36:31 -04:00
|
|
|
{
|
2026-07-31 23:58:28 -04:00
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
Call collide from the simulation, after the move rather than before it
The collide slot has existed unused since the backend was written. It is wired
in now, and its signature changes from (self, a1, a2) to (self, actor, dt): the
old shape had nowhere to put a normal, a depth, or a piece of map geometry, and
no answer to which of the two actors it was supposed to be resolving.
**Resolution runs after `move`.** That is the decision everything else follows
from. A resolver called from movementlogicfunc runs before gravity, drag and the
move, so it has to predict where the actor will end up -- which means
re-implementing the integrator's arithmetic in the game, and being wrong
whenever the integrator changes. examples/sidescroller/collision.c carries forty
lines of exactly that, and says so.
Only `move` is subdivided. Gravity, drag, the thrust integration and the speed
ellipse all still run once against the whole dt, and sum(subdt) is dt, so an
actor with nothing to collide with takes one sub-step of exactly dt -- `dt/1.0f`
is dt exactly in IEEE-754 -- and follows the arithmetic path it always did. That
is what lets the integrator stay untouched and every recorded physics number
stay valid, and tests/physics.c and tests/physics_sim.c pass unchanged with no
world attached.
Collision is opt-in. A backend with no collision world costs one comparison per
actor per step and does nothing else.
Also here:
- `self->gravity` was never null-checked and is dereferenced unconditionally, so
a hand-built backend with only `move` filled in crashed rather than reporting.
- Children are re-snapped in a post-pass, gated on collision being on. The
in-loop snap uses whatever position the parent had when the child's slot came
up in pool order, which is already sometimes a frame stale; that was harmless
while a parent only moved by v*dt and is not once a parent can be pushed out
of a wall mid-step.
- tests/physics.c's deliberate tripwire has been visited. It asserted that
arcade collide always raised AKERR_API, with a comment saying it existed so
that implementing collision would have to come back here. What replaces it
asserts the opt-in contract instead.
- physics_sim.c's hard-coded "%d of 7 simulations" is derived now. A literal
goes stale the first time somebody adds a simulation, which is this commit.
Three new whole-motion simulations, and the third took two attempts to make
honest:
- Landing and resting: 0.0000 px of drift over 120 steps after settling.
- Walking after landing: 107 px in a second, which is the symptom a player sees
when a resting actor is caught on the ground it is standing on.
- A fast fall not tunnelling. The first version used gravity, so the step size
grew every frame and the actor happened to land *inside* the floor rather than
stepping over it -- it passed with sub-stepping disabled and proved nothing.
It uses a constant 1200 px/s now, so every step is exactly 60 pixels against a
32-pixel window in which an overlap exists, and the samples miss it cleanly.
With sub-stepping the actor stops at y=288; without it, y=1300.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 07:05:09 -04:00
|
|
|
FAIL_ZERO_RETURN(errctx, actor, AKERR_NULLPOINTER, "actor");
|
|
|
|
|
(void)dt;
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* Nothing collides, and that is an answer rather than a gap. A caller who
|
|
|
|
|
* wants a world with no collision in it installs this backend and gets it,
|
|
|
|
|
* without the simulation having to branch on whether collision exists.
|
|
|
|
|
*/
|
2026-07-31 23:58:28 -04:00
|
|
|
SUCCEED_RETURN(errctx);
|
2026-05-26 10:36:31 -04:00
|
|
|
}
|
|
|
|
|
|
Stop returning past CLEANUP, and validate the arguments every sibling validates
Closes internal-consistency items 16 and 17.
Ten *_RETURN macros sat inside ATTEMPT blocks, which return past CLEANUP and
skip every release in it. The one that mattered was the success path of
akgl_get_json_tilemap_property: it leaked two of the string pool's 256 entries
on every lookup that *found* what it was asked for, and a map load does that
several times per layer. tests/tilemap.c now runs each of its three paths --
found, absent, wrong type -- twice the pool size and asserts the pool is where
it started. Against the old code that test does not merely fail, it segfaults,
which is Defects item 30 seen from the outside: pool exhaustion arriving as a
NULL strncpy rather than as AKGL_ERR_HEAP.
Two of the conversions needed more than swapping the macro. In
akgl_get_json_tilemap_property a plain break would have fallen through to the
"property not found" FAIL_RETURN after FINISH, reporting a miss for something
found, so the success path sets a flag. In akgl_collide_rectangles the eight
early exits are followed by `*collide = false;`, which would have overwritten
the hit that broke out; each corner test writes the flag itself, so that line
is gone rather than moved. The same function also released its scratch string
once per loop iteration while continuing to use it, so the slot was free while
still live -- one claim now covers the whole scan.
akgl_controller_default is the other behavioural one: its SUCCEED_RETURN was
the last statement in the ATTEMPT block, so the path that falls out of FINISH
reached the closing brace of a non-void function.
scripts/check_error_protocol.py keeps both rules enforced -- a *_RETURN inside
ATTEMPT, and a return out of a HANDLE block -- as the error_protocol test.
Neither produces a compiler diagnostic and neither fails a test run until the
pool it drains is empty, which is why both have already shipped once.
For item 17: the eight typed JSON accessors that validated their container and
then wrote through dest unconditionally now check key and dest as the two
string accessors always did; the null physics backend checks its actors like
the arcade one; and akgl_render_2d_frame_start, _frame_end and _shutdown check
self, which the first two read straight through. tests/renderer.c calls all
three with NULL, which segfaulted before.
25/25 pass, memcheck clean, reindent --check clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 23:56:10 -04:00
|
|
|
akerr_ErrorContext *akgl_physics_null_move(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt)
|
2026-05-26 10:36:31 -04:00
|
|
|
{
|
2026-07-31 23:58:28 -04:00
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, actor, AKERR_NULLPOINTER, "actor");
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
2026-05-26 10:36:31 -04:00
|
|
|
}
|
|
|
|
|
|
Give every exported function a declaration, and check that it stays that way
Closes internal-consistency items 7 through 15. Nineteen non-static functions
were in the ABI with no declaration anywhere, so no consumer could call them
and any consumer could collide with them.
The four gamepad_handle_* functions are the ones that mattered: controller.h
declared akgl_controller_handle_button_down and three siblings that did not
exist, so anything compiled against the header alone failed to link. The
definitions carry the declared names now, which also closes Defects -> Known
and still open item 10, and their documentation moved to the header.
The rest are either declared under a "part of the internal API" block --
akgl_game_save_actors and akgl_game_load_versioncmp, which tests/game.c had to
declare for itself, plus six tilemap loader helpers the untested-loader work
wants to reach -- or static, which is what the four save iterators and
load_objectnamemap should always have been. akgl_path_relative_from is deleted:
declared nowhere, called from nowhere, never wrote its output, and leaked a
pooled string on every call, so it closes Known and still open item 4 and item
40 by ceasing to exist.
scripts/check_api_surface.sh keeps it closed. It reads the built library's
dynamic symbol table and every public header with comments stripped, and fails
on an exported akgl_* symbol that is declared nowhere. Stripping comments is
the whole point -- four of these were mentioned in controller.h prose, which is
how they went unnoticed.
The pool-size ceilings are defined once, in heap.h, so the #ifndef override
hook fires for the first time; actor.h, sprite.h and character.h were defining
the same four unconditionally from headers heap.h includes above its own guard.
tests/header_pool_override.c fails the compile if that regresses.
Also here: (void) rather than () on the twelve no-argument entry points,
AKERR_NOIGNORE only on declarations, static helpers with the akgl_ prefix
dropped, and the six parameter-name mismatches. akgl_get_json_with_default had
its two contexts swapped rather than merely misspelled -- the incoming one was
`err` and its own was `e`, which is the name reserved for an incoming one.
24/24 pass, reindent --check clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 23:44:38 -04:00
|
|
|
akerr_ErrorContext *akgl_physics_init_null(akgl_PhysicsBackend *self)
|
2026-05-26 10:36:31 -04:00
|
|
|
{
|
2026-07-31 23:58:28 -04:00
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
Reindent all C sources to the canonical stroustrup style
Mechanical whitespace-only change across src/, include/, tests/, and
util/, applied with scripts/reindent.sh. No behavioral change.
Most files already conformed. The genuine outliers indented at 2 columns
(json_helpers.c, util.c from akgl_rectangle_points onward, assets.c,
staticstring.c, akgl_actor_add_child, util.h, staticstring.h) or used
spaces where the canonical form is a tab (draw.c). cc-mode also re-aligns
line-continuation backslashes in multi-line macros to its default
c-backslash-column, which is required for the tree to be a fixed point of
the indenter.
Verified whitespace-only: `git diff -w` over this change is empty apart
from three trailing blank lines removed at EOF in src/heap.c,
src/registry.c, and tests/tilemap.c. The build produces no new errors and
ctest is unchanged at 13/14, with `character` still the one intentional
failure.
The tree is now a fixed point of `scripts/reindent.sh --check`.
include/akgl/SDL_GameControllerDB.h is generated and excluded.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 18:17:37 -04:00
|
|
|
|
2026-05-26 10:36:31 -04:00
|
|
|
self->gravity = akgl_physics_null_gravity;
|
|
|
|
|
self->collide = akgl_physics_null_collide;
|
|
|
|
|
self->move = akgl_physics_null_move;
|
|
|
|
|
self->simulate = akgl_physics_simulate;
|
|
|
|
|
|
Fix three arcade physics defects the unit tests could not see
tests/physics_sim.c runs the arcade backend the way a game does -- a
Mario-esque jump and fall, Zelda-style top-down walking, and a run
reversed at full speed -- and prints what the actor actually did.
tests/physics.c already checked that akgl_physics_simulate does the
arithmetic it claims. Every one of these got past it.
The first step was however long the level took to load. gravity_time was
never initialized and dt was unbounded, so a 250 ms load produced a
250 ms step: 100 px of fall where a 60 Hz frame under the same gravity
is 0.44 px, straight through whatever was underneath. Both initializers
seed the clock, and simulate bounds dt to max_timestep -- a new
physics.max_timestep property, default 0.05 s, read like gravity and
drag. Zero disables the bound. The field replaces the dead timer_gravity,
so the struct is the same size.
Releasing a vertical key cancelled gravity. The _off handlers zeroed ay,
ey, ty and vy together, and ey is where the arcade backend accumulates
gravity -- tapping down mid-jump stopped the character in the air. They
clear ax/tx (or ay/ty) and nothing else now. Velocity was never theirs
to clear either: simulate recomputes v as e + t every step.
Diagonal movement was 41% too fast. Thrust was capped per axis, so an
actor holding two directions got both caps at once and travelled their
diagonal. It is capped as a vector against the sx/sy/sz ellipse, which
also keeps a character whose horizontal and vertical top speeds differ
moving at the ratio it asked for. An axis with a zero max speed stays
out of the magnitude and is forced to zero, as the old clamp did.
Each fix was checked by reverting it and confirming the simulation goes
red: 100.1 px, vy 0.0 after a down tap, and 141% diagonal respectively.
tests/actor.c and tests/physics.c pinned the old behaviour in both
places and now assert the new contract.
Bumped to 0.6.0: akgl_PhysicsBackend changed. TODO.md records the four
things the simulations found and this does not fix -- no terminal
velocity, no deceleration on release, Euler's frame-rate dependence, and
a drag coefficient large enough to invert velocity.
26/26 ctest, memcheck clean, warning-clean at -Wall -Werror.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B8T5FAYXE8HEJqFLCYwNNc
2026-08-01 12:10:53 -04:00
|
|
|
// Set for the same reason as the arcade backend: the null backend still
|
|
|
|
|
// goes through akgl_physics_simulate, which still computes a dt and still
|
|
|
|
|
// commits velocity to position through akgl_physics_null_move.
|
|
|
|
|
self->gravity_time = SDL_GetTicksNS();
|
|
|
|
|
self->max_timestep = AKGL_PHYSICS_DEFAULT_MAX_TIMESTEP;
|
|
|
|
|
|
2026-07-31 23:58:28 -04:00
|
|
|
SUCCEED_RETURN(errctx);
|
2026-05-26 10:36:31 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
Give every exported function a declaration, and check that it stays that way
Closes internal-consistency items 7 through 15. Nineteen non-static functions
were in the ABI with no declaration anywhere, so no consumer could call them
and any consumer could collide with them.
The four gamepad_handle_* functions are the ones that mattered: controller.h
declared akgl_controller_handle_button_down and three siblings that did not
exist, so anything compiled against the header alone failed to link. The
definitions carry the declared names now, which also closes Defects -> Known
and still open item 10, and their documentation moved to the header.
The rest are either declared under a "part of the internal API" block --
akgl_game_save_actors and akgl_game_load_versioncmp, which tests/game.c had to
declare for itself, plus six tilemap loader helpers the untested-loader work
wants to reach -- or static, which is what the four save iterators and
load_objectnamemap should always have been. akgl_path_relative_from is deleted:
declared nowhere, called from nowhere, never wrote its output, and leaked a
pooled string on every call, so it closes Known and still open item 4 and item
40 by ceasing to exist.
scripts/check_api_surface.sh keeps it closed. It reads the built library's
dynamic symbol table and every public header with comments stripped, and fails
on an exported akgl_* symbol that is declared nowhere. Stripping comments is
the whole point -- four of these were mentioned in controller.h prose, which is
how they went unnoticed.
The pool-size ceilings are defined once, in heap.h, so the #ifndef override
hook fires for the first time; actor.h, sprite.h and character.h were defining
the same four unconditionally from headers heap.h includes above its own guard.
tests/header_pool_override.c fails the compile if that regresses.
Also here: (void) rather than () on the twelve no-argument entry points,
AKERR_NOIGNORE only on declarations, static helpers with the akgl_ prefix
dropped, and the six parameter-name mismatches. akgl_get_json_with_default had
its two contexts swapped rather than merely misspelled -- the incoming one was
`err` and its own was `e`, which is the name reserved for an incoming one.
24/24 pass, reindent --check clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 23:44:38 -04:00
|
|
|
akerr_ErrorContext *akgl_physics_arcade_gravity(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt)
|
2026-05-26 10:36:31 -04:00
|
|
|
{
|
2026-07-31 23:58:28 -04:00
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, actor, AKERR_NULLPOINTER, "actor");
|
2026-05-26 11:22:45 -04:00
|
|
|
|
2026-06-02 17:11:16 -04:00
|
|
|
if ( self->gravity_x != 0 ) {
|
|
|
|
|
// Assume the X origin is - (screen left)
|
|
|
|
|
actor->ex -= (self->gravity_x * dt);
|
|
|
|
|
}
|
|
|
|
|
if ( self->gravity_y != 0 ) {
|
|
|
|
|
// Assume Y origin is + (down screen)
|
|
|
|
|
actor->ey += (self->gravity_y * dt);
|
|
|
|
|
}
|
|
|
|
|
if ( self->gravity_z != 0 ) {
|
|
|
|
|
// Assume Z origin is - (behind the camera)
|
|
|
|
|
actor->ez -= (self->gravity_z * dt);
|
|
|
|
|
}
|
Reindent all C sources to the canonical stroustrup style
Mechanical whitespace-only change across src/, include/, tests/, and
util/, applied with scripts/reindent.sh. No behavioral change.
Most files already conformed. The genuine outliers indented at 2 columns
(json_helpers.c, util.c from akgl_rectangle_points onward, assets.c,
staticstring.c, akgl_actor_add_child, util.h, staticstring.h) or used
spaces where the canonical form is a tab (draw.c). cc-mode also re-aligns
line-continuation backslashes in multi-line macros to its default
c-backslash-column, which is required for the tree to be a fixed point of
the indenter.
Verified whitespace-only: `git diff -w` over this change is empty apart
from three trailing blank lines removed at EOF in src/heap.c,
src/registry.c, and tests/tilemap.c. The build produces no new errors and
ctest is unchanged at 13/14, with `character` still the one intentional
failure.
The tree is now a fixed point of `scripts/reindent.sh --check`.
include/akgl/SDL_GameControllerDB.h is generated and excluded.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 18:17:37 -04:00
|
|
|
|
2026-07-31 23:58:28 -04:00
|
|
|
SUCCEED_RETURN(errctx);
|
2026-05-26 10:36:31 -04:00
|
|
|
}
|
|
|
|
|
|
Call collide from the simulation, after the move rather than before it
The collide slot has existed unused since the backend was written. It is wired
in now, and its signature changes from (self, a1, a2) to (self, actor, dt): the
old shape had nowhere to put a normal, a depth, or a piece of map geometry, and
no answer to which of the two actors it was supposed to be resolving.
**Resolution runs after `move`.** That is the decision everything else follows
from. A resolver called from movementlogicfunc runs before gravity, drag and the
move, so it has to predict where the actor will end up -- which means
re-implementing the integrator's arithmetic in the game, and being wrong
whenever the integrator changes. examples/sidescroller/collision.c carries forty
lines of exactly that, and says so.
Only `move` is subdivided. Gravity, drag, the thrust integration and the speed
ellipse all still run once against the whole dt, and sum(subdt) is dt, so an
actor with nothing to collide with takes one sub-step of exactly dt -- `dt/1.0f`
is dt exactly in IEEE-754 -- and follows the arithmetic path it always did. That
is what lets the integrator stay untouched and every recorded physics number
stay valid, and tests/physics.c and tests/physics_sim.c pass unchanged with no
world attached.
Collision is opt-in. A backend with no collision world costs one comparison per
actor per step and does nothing else.
Also here:
- `self->gravity` was never null-checked and is dereferenced unconditionally, so
a hand-built backend with only `move` filled in crashed rather than reporting.
- Children are re-snapped in a post-pass, gated on collision being on. The
in-loop snap uses whatever position the parent had when the child's slot came
up in pool order, which is already sometimes a frame stale; that was harmless
while a parent only moved by v*dt and is not once a parent can be pushed out
of a wall mid-step.
- tests/physics.c's deliberate tripwire has been visited. It asserted that
arcade collide always raised AKERR_API, with a comment saying it existed so
that implementing collision would have to come back here. What replaces it
asserts the opt-in contract instead.
- physics_sim.c's hard-coded "%d of 7 simulations" is derived now. A literal
goes stale the first time somebody adds a simulation, which is this commit.
Three new whole-motion simulations, and the third took two attempts to make
honest:
- Landing and resting: 0.0000 px of drift over 120 steps after settling.
- Walking after landing: 107 px in a second, which is the symptom a player sees
when a resting actor is caught on the ground it is standing on.
- A fast fall not tunnelling. The first version used gravity, so the step size
grew every frame and the actor happened to land *inside* the floor rather than
stepping over it -- it passed with sub-stepping disabled and proved nothing.
It uses a constant 1200 px/s now, so every step is exactly 60 pixels against a
32-pixel window in which an overlap exists, and the samples miss it cleanly.
With sub-stepping the actor stops at y=288; without it, y=1300.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 07:05:09 -04:00
|
|
|
akerr_ErrorContext *akgl_physics_arcade_collide(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt)
|
2026-05-26 10:36:31 -04:00
|
|
|
{
|
2026-07-31 23:58:28 -04:00
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
Call collide from the simulation, after the move rather than before it
The collide slot has existed unused since the backend was written. It is wired
in now, and its signature changes from (self, a1, a2) to (self, actor, dt): the
old shape had nowhere to put a normal, a depth, or a piece of map geometry, and
no answer to which of the two actors it was supposed to be resolving.
**Resolution runs after `move`.** That is the decision everything else follows
from. A resolver called from movementlogicfunc runs before gravity, drag and the
move, so it has to predict where the actor will end up -- which means
re-implementing the integrator's arithmetic in the game, and being wrong
whenever the integrator changes. examples/sidescroller/collision.c carries forty
lines of exactly that, and says so.
Only `move` is subdivided. Gravity, drag, the thrust integration and the speed
ellipse all still run once against the whole dt, and sum(subdt) is dt, so an
actor with nothing to collide with takes one sub-step of exactly dt -- `dt/1.0f`
is dt exactly in IEEE-754 -- and follows the arithmetic path it always did. That
is what lets the integrator stay untouched and every recorded physics number
stay valid, and tests/physics.c and tests/physics_sim.c pass unchanged with no
world attached.
Collision is opt-in. A backend with no collision world costs one comparison per
actor per step and does nothing else.
Also here:
- `self->gravity` was never null-checked and is dereferenced unconditionally, so
a hand-built backend with only `move` filled in crashed rather than reporting.
- Children are re-snapped in a post-pass, gated on collision being on. The
in-loop snap uses whatever position the parent had when the child's slot came
up in pool order, which is already sometimes a frame stale; that was harmless
while a parent only moved by v*dt and is not once a parent can be pushed out
of a wall mid-step.
- tests/physics.c's deliberate tripwire has been visited. It asserted that
arcade collide always raised AKERR_API, with a comment saying it existed so
that implementing collision would have to come back here. What replaces it
asserts the opt-in contract instead.
- physics_sim.c's hard-coded "%d of 7 simulations" is derived now. A literal
goes stale the first time somebody adds a simulation, which is this commit.
Three new whole-motion simulations, and the third took two attempts to make
honest:
- Landing and resting: 0.0000 px of drift over 120 steps after settling.
- Walking after landing: 107 px in a second, which is the symptom a player sees
when a resting actor is caught on the ground it is standing on.
- A fast fall not tunnelling. The first version used gravity, so the step size
grew every frame and the actor happened to land *inside* the floor rather than
stepping over it -- it passed with sub-stepping disabled and proved nothing.
It uses a constant 1200 px/s now, so every step is exactly 60 pixels against a
32-pixel window in which an overlap exists, and the samples miss it cleanly.
With sub-stepping the actor stops at y=288; without it, y=1300.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 07:05:09 -04:00
|
|
|
FAIL_ZERO_RETURN(errctx, actor, AKERR_NULLPOINTER, "actor");
|
|
|
|
|
|
|
|
|
|
// No world means no collision, and costs one comparison. This is what makes
|
|
|
|
|
// the feature opt-in: a backend nobody attached a world to runs exactly as
|
|
|
|
|
// it did before any of this existed.
|
|
|
|
|
if ( self->collision == NULL ) {
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
PASS(errctx, akgl_collision_resolve(self->collision, actor, dt));
|
2026-07-31 23:58:28 -04:00
|
|
|
SUCCEED_RETURN(errctx);
|
2026-05-26 10:36:31 -04:00
|
|
|
}
|
|
|
|
|
|
Stop returning past CLEANUP, and validate the arguments every sibling validates
Closes internal-consistency items 16 and 17.
Ten *_RETURN macros sat inside ATTEMPT blocks, which return past CLEANUP and
skip every release in it. The one that mattered was the success path of
akgl_get_json_tilemap_property: it leaked two of the string pool's 256 entries
on every lookup that *found* what it was asked for, and a map load does that
several times per layer. tests/tilemap.c now runs each of its three paths --
found, absent, wrong type -- twice the pool size and asserts the pool is where
it started. Against the old code that test does not merely fail, it segfaults,
which is Defects item 30 seen from the outside: pool exhaustion arriving as a
NULL strncpy rather than as AKGL_ERR_HEAP.
Two of the conversions needed more than swapping the macro. In
akgl_get_json_tilemap_property a plain break would have fallen through to the
"property not found" FAIL_RETURN after FINISH, reporting a miss for something
found, so the success path sets a flag. In akgl_collide_rectangles the eight
early exits are followed by `*collide = false;`, which would have overwritten
the hit that broke out; each corner test writes the flag itself, so that line
is gone rather than moved. The same function also released its scratch string
once per loop iteration while continuing to use it, so the slot was free while
still live -- one claim now covers the whole scan.
akgl_controller_default is the other behavioural one: its SUCCEED_RETURN was
the last statement in the ATTEMPT block, so the path that falls out of FINISH
reached the closing brace of a non-void function.
scripts/check_error_protocol.py keeps both rules enforced -- a *_RETURN inside
ATTEMPT, and a return out of a HANDLE block -- as the error_protocol test.
Neither produces a compiler diagnostic and neither fails a test run until the
pool it drains is empty, which is why both have already shipped once.
For item 17: the eight typed JSON accessors that validated their container and
then wrote through dest unconditionally now check key and dest as the two
string accessors always did; the null physics backend checks its actors like
the arcade one; and akgl_render_2d_frame_start, _frame_end and _shutdown check
self, which the first two read straight through. tests/renderer.c calls all
three with NULL, which segfaulted before.
25/25 pass, memcheck clean, reindent --check clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 23:56:10 -04:00
|
|
|
akerr_ErrorContext *akgl_physics_arcade_move(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt)
|
2026-05-26 10:36:31 -04:00
|
|
|
{
|
2026-07-31 23:58:28 -04:00
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, actor, AKERR_NULLPOINTER, "actor");
|
2026-05-26 11:22:45 -04:00
|
|
|
actor->x += actor->vx * dt;
|
|
|
|
|
actor->y += actor->vy * dt;
|
Reindent all C sources to the canonical stroustrup style
Mechanical whitespace-only change across src/, include/, tests/, and
util/, applied with scripts/reindent.sh. No behavioral change.
Most files already conformed. The genuine outliers indented at 2 columns
(json_helpers.c, util.c from akgl_rectangle_points onward, assets.c,
staticstring.c, akgl_actor_add_child, util.h, staticstring.h) or used
spaces where the canonical form is a tab (draw.c). cc-mode also re-aligns
line-continuation backslashes in multi-line macros to its default
c-backslash-column, which is required for the tree to be a fixed point of
the indenter.
Verified whitespace-only: `git diff -w` over this change is empty apart
from three trailing blank lines removed at EOF in src/heap.c,
src/registry.c, and tests/tilemap.c. The build produces no new errors and
ctest is unchanged at 13/14, with `character` still the one intentional
failure.
The tree is now a fixed point of `scripts/reindent.sh --check`.
include/akgl/SDL_GameControllerDB.h is generated and excluded.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 18:17:37 -04:00
|
|
|
actor->z += actor->vz * dt;
|
2026-07-31 23:58:28 -04:00
|
|
|
SUCCEED_RETURN(errctx);
|
2026-05-26 10:36:31 -04:00
|
|
|
}
|
|
|
|
|
|
Give every exported function a declaration, and check that it stays that way
Closes internal-consistency items 7 through 15. Nineteen non-static functions
were in the ABI with no declaration anywhere, so no consumer could call them
and any consumer could collide with them.
The four gamepad_handle_* functions are the ones that mattered: controller.h
declared akgl_controller_handle_button_down and three siblings that did not
exist, so anything compiled against the header alone failed to link. The
definitions carry the declared names now, which also closes Defects -> Known
and still open item 10, and their documentation moved to the header.
The rest are either declared under a "part of the internal API" block --
akgl_game_save_actors and akgl_game_load_versioncmp, which tests/game.c had to
declare for itself, plus six tilemap loader helpers the untested-loader work
wants to reach -- or static, which is what the four save iterators and
load_objectnamemap should always have been. akgl_path_relative_from is deleted:
declared nowhere, called from nowhere, never wrote its output, and leaked a
pooled string on every call, so it closes Known and still open item 4 and item
40 by ceasing to exist.
scripts/check_api_surface.sh keeps it closed. It reads the built library's
dynamic symbol table and every public header with comments stripped, and fails
on an exported akgl_* symbol that is declared nowhere. Stripping comments is
the whole point -- four of these were mentioned in controller.h prose, which is
how they went unnoticed.
The pool-size ceilings are defined once, in heap.h, so the #ifndef override
hook fires for the first time; actor.h, sprite.h and character.h were defining
the same four unconditionally from headers heap.h includes above its own guard.
tests/header_pool_override.c fails the compile if that regresses.
Also here: (void) rather than () on the twelve no-argument entry points,
AKERR_NOIGNORE only on declarations, static helpers with the akgl_ prefix
dropped, and the six parameter-name mismatches. akgl_get_json_with_default had
its two contexts swapped rather than merely misspelled -- the incoming one was
`err` and its own was `e`, which is the name reserved for an incoming one.
24/24 pass, reindent --check clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 23:44:38 -04:00
|
|
|
akerr_ErrorContext *akgl_physics_init_arcade(akgl_PhysicsBackend *self)
|
2026-05-26 10:36:31 -04:00
|
|
|
{
|
|
|
|
|
akgl_String *tmp;
|
2026-07-31 23:58:28 -04:00
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
|
|
|
|
PASS(errctx, akgl_heap_next_string(&tmp));
|
Reindent all C sources to the canonical stroustrup style
Mechanical whitespace-only change across src/, include/, tests/, and
util/, applied with scripts/reindent.sh. No behavioral change.
Most files already conformed. The genuine outliers indented at 2 columns
(json_helpers.c, util.c from akgl_rectangle_points onward, assets.c,
staticstring.c, akgl_actor_add_child, util.h, staticstring.h) or used
spaces where the canonical form is a tab (draw.c). cc-mode also re-aligns
line-continuation backslashes in multi-line macros to its default
c-backslash-column, which is required for the tree to be a fixed point of
the indenter.
Verified whitespace-only: `git diff -w` over this change is empty apart
from three trailing blank lines removed at EOF in src/heap.c,
src/registry.c, and tests/tilemap.c. The build produces no new errors and
ctest is unchanged at 13/14, with `character` still the one intentional
failure.
The tree is now a fixed point of `scripts/reindent.sh --check`.
include/akgl/SDL_GameControllerDB.h is generated and excluded.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 18:17:37 -04:00
|
|
|
|
2026-06-02 13:15:26 -04:00
|
|
|
self->gravity = akgl_physics_arcade_gravity;
|
|
|
|
|
self->collide = akgl_physics_arcade_collide;
|
|
|
|
|
self->move = akgl_physics_arcade_move;
|
2026-05-26 10:36:31 -04:00
|
|
|
self->simulate = akgl_physics_simulate;
|
|
|
|
|
|
Fix three arcade physics defects the unit tests could not see
tests/physics_sim.c runs the arcade backend the way a game does -- a
Mario-esque jump and fall, Zelda-style top-down walking, and a run
reversed at full speed -- and prints what the actor actually did.
tests/physics.c already checked that akgl_physics_simulate does the
arithmetic it claims. Every one of these got past it.
The first step was however long the level took to load. gravity_time was
never initialized and dt was unbounded, so a 250 ms load produced a
250 ms step: 100 px of fall where a 60 Hz frame under the same gravity
is 0.44 px, straight through whatever was underneath. Both initializers
seed the clock, and simulate bounds dt to max_timestep -- a new
physics.max_timestep property, default 0.05 s, read like gravity and
drag. Zero disables the bound. The field replaces the dead timer_gravity,
so the struct is the same size.
Releasing a vertical key cancelled gravity. The _off handlers zeroed ay,
ey, ty and vy together, and ey is where the arcade backend accumulates
gravity -- tapping down mid-jump stopped the character in the air. They
clear ax/tx (or ay/ty) and nothing else now. Velocity was never theirs
to clear either: simulate recomputes v as e + t every step.
Diagonal movement was 41% too fast. Thrust was capped per axis, so an
actor holding two directions got both caps at once and travelled their
diagonal. It is capped as a vector against the sx/sy/sz ellipse, which
also keeps a character whose horizontal and vertical top speeds differ
moving at the ratio it asked for. An axis with a zero max speed stays
out of the magnitude and is forced to zero, as the old clamp did.
Each fix was checked by reverting it and confirming the simulation goes
red: 100.1 px, vy 0.0 after a down tap, and 141% diagonal respectively.
tests/actor.c and tests/physics.c pinned the old behaviour in both
places and now assert the new contract.
Bumped to 0.6.0: akgl_PhysicsBackend changed. TODO.md records the four
things the simulations found and this does not fix -- no terminal
velocity, no deceleration on release, Euler's frame-rate dependence, and
a drag coefficient large enough to invert velocity.
26/26 ctest, memcheck clean, warning-clean at -Wall -Werror.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B8T5FAYXE8HEJqFLCYwNNc
2026-08-01 12:10:53 -04:00
|
|
|
// The epoch the first step's dt is measured from. Nothing set it, so it
|
|
|
|
|
// stayed at whatever the backend's storage held -- zero, for the default
|
|
|
|
|
// backend in BSS -- and the first akgl_physics_simulate() measured dt as
|
|
|
|
|
// the entire time since SDL started. A host that spends a quarter of a
|
|
|
|
|
// second loading before its first frame got a quarter-second step: 101
|
|
|
|
|
// pixels of fall where a 60 Hz frame is 0.44.
|
|
|
|
|
self->gravity_time = SDL_GetTicksNS();
|
|
|
|
|
|
2026-05-26 10:36:31 -04:00
|
|
|
ATTEMPT {
|
2026-07-31 23:58:28 -04:00
|
|
|
CATCH(errctx, akgl_get_property("physics.gravity.x", &tmp, "0.0"));
|
|
|
|
|
CATCH(errctx, aksl_atof(tmp->data, &self->gravity_x));
|
|
|
|
|
CATCH(errctx, akgl_get_property("physics.gravity.y", &tmp, "0.0"));
|
|
|
|
|
CATCH(errctx, aksl_atof(tmp->data, &self->gravity_y));
|
|
|
|
|
CATCH(errctx, akgl_get_property("physics.gravity.z", &tmp, "0.0"));
|
|
|
|
|
CATCH(errctx, aksl_atof(tmp->data, &self->gravity_z));
|
|
|
|
|
CATCH(errctx, akgl_get_property("physics.drag.x", &tmp, "0.0"));
|
|
|
|
|
CATCH(errctx, aksl_atof(tmp->data, &self->drag_x));
|
|
|
|
|
CATCH(errctx, akgl_get_property("physics.drag.y", &tmp, "0.0"));
|
|
|
|
|
CATCH(errctx, aksl_atof(tmp->data, &self->drag_y));
|
|
|
|
|
CATCH(errctx, akgl_get_property("physics.drag.z", &tmp, "0.0"));
|
|
|
|
|
CATCH(errctx, aksl_atof(tmp->data, &self->drag_z));
|
Fix three arcade physics defects the unit tests could not see
tests/physics_sim.c runs the arcade backend the way a game does -- a
Mario-esque jump and fall, Zelda-style top-down walking, and a run
reversed at full speed -- and prints what the actor actually did.
tests/physics.c already checked that akgl_physics_simulate does the
arithmetic it claims. Every one of these got past it.
The first step was however long the level took to load. gravity_time was
never initialized and dt was unbounded, so a 250 ms load produced a
250 ms step: 100 px of fall where a 60 Hz frame under the same gravity
is 0.44 px, straight through whatever was underneath. Both initializers
seed the clock, and simulate bounds dt to max_timestep -- a new
physics.max_timestep property, default 0.05 s, read like gravity and
drag. Zero disables the bound. The field replaces the dead timer_gravity,
so the struct is the same size.
Releasing a vertical key cancelled gravity. The _off handlers zeroed ay,
ey, ty and vy together, and ey is where the arcade backend accumulates
gravity -- tapping down mid-jump stopped the character in the air. They
clear ax/tx (or ay/ty) and nothing else now. Velocity was never theirs
to clear either: simulate recomputes v as e + t every step.
Diagonal movement was 41% too fast. Thrust was capped per axis, so an
actor holding two directions got both caps at once and travelled their
diagonal. It is capped as a vector against the sx/sy/sz ellipse, which
also keeps a character whose horizontal and vertical top speeds differ
moving at the ratio it asked for. An axis with a zero max speed stays
out of the magnitude and is forced to zero, as the old clamp did.
Each fix was checked by reverting it and confirming the simulation goes
red: 100.1 px, vy 0.0 after a down tap, and 141% diagonal respectively.
tests/actor.c and tests/physics.c pinned the old behaviour in both
places and now assert the new contract.
Bumped to 0.6.0: akgl_PhysicsBackend changed. TODO.md records the four
things the simulations found and this does not fix -- no terminal
velocity, no deceleration on release, Euler's frame-rate dependence, and
a drag coefficient large enough to invert velocity.
26/26 ctest, memcheck clean, warning-clean at -Wall -Werror.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B8T5FAYXE8HEJqFLCYwNNc
2026-08-01 12:10:53 -04:00
|
|
|
CATCH(errctx, akgl_get_property("physics.max_timestep", &tmp, "0.05"));
|
|
|
|
|
CATCH(errctx, aksl_atof(tmp->data, &self->max_timestep));
|
2026-05-26 10:36:31 -04:00
|
|
|
} CLEANUP {
|
|
|
|
|
IGNORE(akgl_heap_release_string(tmp));
|
2026-07-31 23:58:28 -04:00
|
|
|
} PROCESS(errctx) {
|
|
|
|
|
} FINISH(errctx, true);
|
Reindent all C sources to the canonical stroustrup style
Mechanical whitespace-only change across src/, include/, tests/, and
util/, applied with scripts/reindent.sh. No behavioral change.
Most files already conformed. The genuine outliers indented at 2 columns
(json_helpers.c, util.c from akgl_rectangle_points onward, assets.c,
staticstring.c, akgl_actor_add_child, util.h, staticstring.h) or used
spaces where the canonical form is a tab (draw.c). cc-mode also re-aligns
line-continuation backslashes in multi-line macros to its default
c-backslash-column, which is required for the tree to be a fixed point of
the indenter.
Verified whitespace-only: `git diff -w` over this change is empty apart
from three trailing blank lines removed at EOF in src/heap.c,
src/registry.c, and tests/tilemap.c. The build produces no new errors and
ctest is unchanged at 13/14, with `character` still the one intentional
failure.
The tree is now a fixed point of `scripts/reindent.sh --check`.
include/akgl/SDL_GameControllerDB.h is generated and excluded.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 18:17:37 -04:00
|
|
|
|
2026-07-31 23:58:28 -04:00
|
|
|
SUCCEED_RETURN(errctx);
|
2026-05-26 10:36:31 -04:00
|
|
|
}
|
|
|
|
|
|
Give every exported function a declaration, and check that it stays that way
Closes internal-consistency items 7 through 15. Nineteen non-static functions
were in the ABI with no declaration anywhere, so no consumer could call them
and any consumer could collide with them.
The four gamepad_handle_* functions are the ones that mattered: controller.h
declared akgl_controller_handle_button_down and three siblings that did not
exist, so anything compiled against the header alone failed to link. The
definitions carry the declared names now, which also closes Defects -> Known
and still open item 10, and their documentation moved to the header.
The rest are either declared under a "part of the internal API" block --
akgl_game_save_actors and akgl_game_load_versioncmp, which tests/game.c had to
declare for itself, plus six tilemap loader helpers the untested-loader work
wants to reach -- or static, which is what the four save iterators and
load_objectnamemap should always have been. akgl_path_relative_from is deleted:
declared nowhere, called from nowhere, never wrote its output, and leaked a
pooled string on every call, so it closes Known and still open item 4 and item
40 by ceasing to exist.
scripts/check_api_surface.sh keeps it closed. It reads the built library's
dynamic symbol table and every public header with comments stripped, and fails
on an exported akgl_* symbol that is declared nowhere. Stripping comments is
the whole point -- four of these were mentioned in controller.h prose, which is
how they went unnoticed.
The pool-size ceilings are defined once, in heap.h, so the #ifndef override
hook fires for the first time; actor.h, sprite.h and character.h were defining
the same four unconditionally from headers heap.h includes above its own guard.
tests/header_pool_override.c fails the compile if that regresses.
Also here: (void) rather than () on the twelve no-argument entry points,
AKERR_NOIGNORE only on declarations, static helpers with the akgl_ prefix
dropped, and the six parameter-name mismatches. akgl_get_json_with_default had
its two contexts swapped rather than merely misspelled -- the incoming one was
`err` and its own was `e`, which is the name reserved for an incoming one.
24/24 pass, reindent --check clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 23:44:38 -04:00
|
|
|
akerr_ErrorContext *akgl_physics_simulate(akgl_PhysicsBackend *self, akgl_Iterator *opflags)
|
2026-05-26 10:36:31 -04:00
|
|
|
{
|
2026-07-31 23:58:28 -04:00
|
|
|
PREPARE_ERROR(errctx);
|
2026-05-26 10:36:31 -04:00
|
|
|
akgl_Iterator defflags = {
|
|
|
|
|
.flags = 0,
|
|
|
|
|
.layerid = 0
|
|
|
|
|
};
|
Add physics, heap, json_helpers, game, and actor test suites
Raise line coverage from 39.6 to 61.8 percent with four new suites and an
extension to the actor suite, and register every suite through a single
CMake list so a new test file cannot be left out of the coverage fixture.
Give the test targets a build-tree RPATH and prepend the build tree to
LD_LIBRARY_PATH for CTest, so a developer with a previously installed
libakgl.so exercises the library that was just compiled.
Fix six defects the new tests exposed:
- akgl_physics_simulate read self->gravity_time before its NULL check, so a
NULL backend crashed instead of reporting AKERR_NULLPOINTER.
- akgl_game_save transposed CLEANUP and PROCESS, which placed the fclose
inside the PROCESS switch. An ordinary save never flushed or closed its
stream and produced an empty file.
- akgl_game_save_actors wrote each name table terminator from the address of
a single char, emitting stack contents into the save file and a sentinel
the loader could not recognize.
- akgl_game_load_objectnamemap used CATCH directly inside while(1), where the
break leaves the loop rather than propagating, so a truncated name table
loaded as a successful game.
- akgl_Actor_cmhf_up_on and _down_on dereferenced actor->basechar with no
NULL check, unlike their left and right counterparts.
- akgl_actor_logic_movement checked actor twice instead of checking
actor->basechar before dereferencing it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 02:03:21 -04:00
|
|
|
SDL_Time curtime = 0;
|
|
|
|
|
float32_t dt = 0;
|
Fix three arcade physics defects the unit tests could not see
tests/physics_sim.c runs the arcade backend the way a game does -- a
Mario-esque jump and fall, Zelda-style top-down walking, and a run
reversed at full speed -- and prints what the actor actually did.
tests/physics.c already checked that akgl_physics_simulate does the
arithmetic it claims. Every one of these got past it.
The first step was however long the level took to load. gravity_time was
never initialized and dt was unbounded, so a 250 ms load produced a
250 ms step: 100 px of fall where a 60 Hz frame under the same gravity
is 0.44 px, straight through whatever was underneath. Both initializers
seed the clock, and simulate bounds dt to max_timestep -- a new
physics.max_timestep property, default 0.05 s, read like gravity and
drag. Zero disables the bound. The field replaces the dead timer_gravity,
so the struct is the same size.
Releasing a vertical key cancelled gravity. The _off handlers zeroed ay,
ey, ty and vy together, and ey is where the arcade backend accumulates
gravity -- tapping down mid-jump stopped the character in the air. They
clear ax/tx (or ay/ty) and nothing else now. Velocity was never theirs
to clear either: simulate recomputes v as e + t every step.
Diagonal movement was 41% too fast. Thrust was capped per axis, so an
actor holding two directions got both caps at once and travelled their
diagonal. It is capped as a vector against the sx/sy/sz ellipse, which
also keeps a character whose horizontal and vertical top speeds differ
moving at the ratio it asked for. An axis with a zero max speed stays
out of the magnitude and is forced to zero, as the old clamp did.
Each fix was checked by reverting it and confirming the simulation goes
red: 100.1 px, vy 0.0 after a down tap, and 141% diagonal respectively.
tests/actor.c and tests/physics.c pinned the old behaviour in both
places and now assert the new contract.
Bumped to 0.6.0: akgl_PhysicsBackend changed. TODO.md records the four
things the simulations found and this does not fix -- no terminal
velocity, no deceleration on release, Euler's frame-rate dependence, and
a drag coefficient large enough to invert velocity.
26/26 ctest, memcheck clean, warning-clean at -Wall -Werror.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B8T5FAYXE8HEJqFLCYwNNc
2026-08-01 12:10:53 -04:00
|
|
|
float32_t overshoot = 0.0f;
|
|
|
|
|
float32_t thrustscale = 0.0f;
|
Call collide from the simulation, after the move rather than before it
The collide slot has existed unused since the backend was written. It is wired
in now, and its signature changes from (self, a1, a2) to (self, actor, dt): the
old shape had nowhere to put a normal, a depth, or a piece of map geometry, and
no answer to which of the two actors it was supposed to be resolving.
**Resolution runs after `move`.** That is the decision everything else follows
from. A resolver called from movementlogicfunc runs before gravity, drag and the
move, so it has to predict where the actor will end up -- which means
re-implementing the integrator's arithmetic in the game, and being wrong
whenever the integrator changes. examples/sidescroller/collision.c carries forty
lines of exactly that, and says so.
Only `move` is subdivided. Gravity, drag, the thrust integration and the speed
ellipse all still run once against the whole dt, and sum(subdt) is dt, so an
actor with nothing to collide with takes one sub-step of exactly dt -- `dt/1.0f`
is dt exactly in IEEE-754 -- and follows the arithmetic path it always did. That
is what lets the integrator stay untouched and every recorded physics number
stay valid, and tests/physics.c and tests/physics_sim.c pass unchanged with no
world attached.
Collision is opt-in. A backend with no collision world costs one comparison per
actor per step and does nothing else.
Also here:
- `self->gravity` was never null-checked and is dereferenced unconditionally, so
a hand-built backend with only `move` filled in crashed rather than reporting.
- Children are re-snapped in a post-pass, gated on collision being on. The
in-loop snap uses whatever position the parent had when the child's slot came
up in pool order, which is already sometimes a frame stale; that was harmless
while a parent only moved by v*dt and is not once a parent can be pushed out
of a wall mid-step.
- tests/physics.c's deliberate tripwire has been visited. It asserted that
arcade collide always raised AKERR_API, with a comment saying it existed so
that implementing collision would have to come back here. What replaces it
asserts the opt-in contract instead.
- physics_sim.c's hard-coded "%d of 7 simulations" is derived now. A literal
goes stale the first time somebody adds a simulation, which is this commit.
Three new whole-motion simulations, and the third took two attempts to make
honest:
- Landing and resting: 0.0000 px of drift over 120 steps after settling.
- Walking after landing: 107 px in a second, which is the symptom a player sees
when a resting actor is caught on the ground it is standing on.
- A fast fall not tunnelling. The first version used gravity, so the step size
grew every frame and the actor happened to land *inside* the floor rather than
stepping over it -- it passed with sub-stepping disabled and proved nothing.
It uses a constant 1200 px/s now, so every step is exactly 60 pixels against a
32-pixel window in which an overlap exists, and the samples miss it cleanly.
With sub-stepping the actor stops at y=288; without it, y=1300.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 07:05:09 -04:00
|
|
|
float32_t subdt = 0.0f;
|
|
|
|
|
int substeps = 1;
|
|
|
|
|
int s = 0;
|
2026-05-26 10:36:31 -04:00
|
|
|
akgl_Actor *actor = NULL;
|
|
|
|
|
|
2026-07-31 23:58:28 -04:00
|
|
|
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, self->move, AKERR_NULLPOINTER, "self->move");
|
Call collide from the simulation, after the move rather than before it
The collide slot has existed unused since the backend was written. It is wired
in now, and its signature changes from (self, a1, a2) to (self, actor, dt): the
old shape had nowhere to put a normal, a depth, or a piece of map geometry, and
no answer to which of the two actors it was supposed to be resolving.
**Resolution runs after `move`.** That is the decision everything else follows
from. A resolver called from movementlogicfunc runs before gravity, drag and the
move, so it has to predict where the actor will end up -- which means
re-implementing the integrator's arithmetic in the game, and being wrong
whenever the integrator changes. examples/sidescroller/collision.c carries forty
lines of exactly that, and says so.
Only `move` is subdivided. Gravity, drag, the thrust integration and the speed
ellipse all still run once against the whole dt, and sum(subdt) is dt, so an
actor with nothing to collide with takes one sub-step of exactly dt -- `dt/1.0f`
is dt exactly in IEEE-754 -- and follows the arithmetic path it always did. That
is what lets the integrator stay untouched and every recorded physics number
stay valid, and tests/physics.c and tests/physics_sim.c pass unchanged with no
world attached.
Collision is opt-in. A backend with no collision world costs one comparison per
actor per step and does nothing else.
Also here:
- `self->gravity` was never null-checked and is dereferenced unconditionally, so
a hand-built backend with only `move` filled in crashed rather than reporting.
- Children are re-snapped in a post-pass, gated on collision being on. The
in-loop snap uses whatever position the parent had when the child's slot came
up in pool order, which is already sometimes a frame stale; that was harmless
while a parent only moved by v*dt and is not once a parent can be pushed out
of a wall mid-step.
- tests/physics.c's deliberate tripwire has been visited. It asserted that
arcade collide always raised AKERR_API, with a comment saying it existed so
that implementing collision would have to come back here. What replaces it
asserts the opt-in contract instead.
- physics_sim.c's hard-coded "%d of 7 simulations" is derived now. A literal
goes stale the first time somebody adds a simulation, which is this commit.
Three new whole-motion simulations, and the third took two attempts to make
honest:
- Landing and resting: 0.0000 px of drift over 120 steps after settling.
- Walking after landing: 107 px in a second, which is the symptom a player sees
when a resting actor is caught on the ground it is standing on.
- A fast fall not tunnelling. The first version used gravity, so the step size
grew every frame and the actor happened to land *inside* the floor rather than
stepping over it -- it passed with sub-stepping disabled and proved nothing.
It uses a constant 1200 px/s now, so every step is exactly 60 pixels against a
32-pixel window in which an overlap exists, and the samples miss it cleanly.
With sub-stepping the actor stops at y=288; without it, y=1300.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 07:05:09 -04:00
|
|
|
// `gravity` was never checked, and is dereferenced unconditionally below. A
|
|
|
|
|
// backend built by hand with only `move` filled in crashed here rather than
|
|
|
|
|
// reporting; `collide` is allowed to be NULL and means "nothing collides".
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, self->gravity, AKERR_NULLPOINTER, "self->gravity");
|
2026-05-26 10:36:31 -04:00
|
|
|
|
Add physics, heap, json_helpers, game, and actor test suites
Raise line coverage from 39.6 to 61.8 percent with four new suites and an
extension to the actor suite, and register every suite through a single
CMake list so a new test file cannot be left out of the coverage fixture.
Give the test targets a build-tree RPATH and prepend the build tree to
LD_LIBRARY_PATH for CTest, so a developer with a previously installed
libakgl.so exercises the library that was just compiled.
Fix six defects the new tests exposed:
- akgl_physics_simulate read self->gravity_time before its NULL check, so a
NULL backend crashed instead of reporting AKERR_NULLPOINTER.
- akgl_game_save transposed CLEANUP and PROCESS, which placed the fclose
inside the PROCESS switch. An ordinary save never flushed or closed its
stream and produced an empty file.
- akgl_game_save_actors wrote each name table terminator from the address of
a single char, emitting stack contents into the save file and a sentinel
the loader could not recognize.
- akgl_game_load_objectnamemap used CATCH directly inside while(1), where the
break leaves the loop rather than propagating, so a truncated name table
loaded as a successful game.
- akgl_Actor_cmhf_up_on and _down_on dereferenced actor->basechar with no
NULL check, unlike their left and right counterparts.
- akgl_actor_logic_movement checked actor twice instead of checking
actor->basechar before dereferencing it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 02:03:21 -04:00
|
|
|
// Reading the elapsed time requires self, so it cannot be hoisted above
|
|
|
|
|
// the null check.
|
|
|
|
|
curtime = SDL_GetTicksNS();
|
|
|
|
|
dt = (float32_t)(curtime - self->gravity_time) / (float32_t)AKGL_TIME_ONESEC_NS;
|
|
|
|
|
|
Fix three arcade physics defects the unit tests could not see
tests/physics_sim.c runs the arcade backend the way a game does -- a
Mario-esque jump and fall, Zelda-style top-down walking, and a run
reversed at full speed -- and prints what the actor actually did.
tests/physics.c already checked that akgl_physics_simulate does the
arithmetic it claims. Every one of these got past it.
The first step was however long the level took to load. gravity_time was
never initialized and dt was unbounded, so a 250 ms load produced a
250 ms step: 100 px of fall where a 60 Hz frame under the same gravity
is 0.44 px, straight through whatever was underneath. Both initializers
seed the clock, and simulate bounds dt to max_timestep -- a new
physics.max_timestep property, default 0.05 s, read like gravity and
drag. Zero disables the bound. The field replaces the dead timer_gravity,
so the struct is the same size.
Releasing a vertical key cancelled gravity. The _off handlers zeroed ay,
ey, ty and vy together, and ey is where the arcade backend accumulates
gravity -- tapping down mid-jump stopped the character in the air. They
clear ax/tx (or ay/ty) and nothing else now. Velocity was never theirs
to clear either: simulate recomputes v as e + t every step.
Diagonal movement was 41% too fast. Thrust was capped per axis, so an
actor holding two directions got both caps at once and travelled their
diagonal. It is capped as a vector against the sx/sy/sz ellipse, which
also keeps a character whose horizontal and vertical top speeds differ
moving at the ratio it asked for. An axis with a zero max speed stays
out of the magnitude and is forced to zero, as the old clamp did.
Each fix was checked by reverting it and confirming the simulation goes
red: 100.1 px, vy 0.0 after a down tap, and 141% diagonal respectively.
tests/actor.c and tests/physics.c pinned the old behaviour in both
places and now assert the new contract.
Bumped to 0.6.0: akgl_PhysicsBackend changed. TODO.md records the four
things the simulations found and this does not fix -- no terminal
velocity, no deceleration on release, Euler's frame-rate dependence, and
a drag coefficient large enough to invert velocity.
26/26 ctest, memcheck clean, warning-clean at -Wall -Werror.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B8T5FAYXE8HEJqFLCYwNNc
2026-08-01 12:10:53 -04:00
|
|
|
// Bound the step. Everything below multiplies by dt, so one enormous dt
|
|
|
|
|
// moves every actor by however far its velocity carries it over the whole
|
|
|
|
|
// stall -- a quarter-second load is a hundred pixels of fall under
|
|
|
|
|
// platformer gravity, straight through whatever was underneath. Advancing
|
|
|
|
|
// the world in slow motion through a hitch is the trade to make here.
|
|
|
|
|
//
|
|
|
|
|
// A zero or negative max_timestep disables the bound, for a caller who
|
|
|
|
|
// would rather have the real elapsed time and handle it themselves.
|
|
|
|
|
if ( (self->max_timestep > 0.0) && (dt > (float32_t)self->max_timestep) ) {
|
|
|
|
|
dt = (float32_t)self->max_timestep;
|
|
|
|
|
}
|
|
|
|
|
// A clock that went backwards is not a step. SDL_GetTicksNS is monotonic,
|
|
|
|
|
// but gravity_time is a public field and a caller can put anything in it.
|
|
|
|
|
if ( dt < 0.0f ) {
|
|
|
|
|
dt = 0.0f;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-26 10:36:31 -04:00
|
|
|
if ( opflags == NULL ) {
|
|
|
|
|
opflags = &defflags;
|
|
|
|
|
}
|
Reindent all C sources to the canonical stroustrup style
Mechanical whitespace-only change across src/, include/, tests/, and
util/, applied with scripts/reindent.sh. No behavioral change.
Most files already conformed. The genuine outliers indented at 2 columns
(json_helpers.c, util.c from akgl_rectangle_points onward, assets.c,
staticstring.c, akgl_actor_add_child, util.h, staticstring.h) or used
spaces where the canonical form is a tab (draw.c). cc-mode also re-aligns
line-continuation backslashes in multi-line macros to its default
c-backslash-column, which is required for the tree to be a fixed point of
the indenter.
Verified whitespace-only: `git diff -w` over this change is empty apart
from three trailing blank lines removed at EOF in src/heap.c,
src/registry.c, and tests/tilemap.c. The build produces no new errors and
ctest is unchanged at 13/14, with `character` still the one intentional
failure.
The tree is now a fixed point of `scripts/reindent.sh --check`.
include/akgl/SDL_GameControllerDB.h is generated and excluded.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 18:17:37 -04:00
|
|
|
|
Call collide from the simulation, after the move rather than before it
The collide slot has existed unused since the backend was written. It is wired
in now, and its signature changes from (self, a1, a2) to (self, actor, dt): the
old shape had nowhere to put a normal, a depth, or a piece of map geometry, and
no answer to which of the two actors it was supposed to be resolving.
**Resolution runs after `move`.** That is the decision everything else follows
from. A resolver called from movementlogicfunc runs before gravity, drag and the
move, so it has to predict where the actor will end up -- which means
re-implementing the integrator's arithmetic in the game, and being wrong
whenever the integrator changes. examples/sidescroller/collision.c carries forty
lines of exactly that, and says so.
Only `move` is subdivided. Gravity, drag, the thrust integration and the speed
ellipse all still run once against the whole dt, and sum(subdt) is dt, so an
actor with nothing to collide with takes one sub-step of exactly dt -- `dt/1.0f`
is dt exactly in IEEE-754 -- and follows the arithmetic path it always did. That
is what lets the integrator stay untouched and every recorded physics number
stay valid, and tests/physics.c and tests/physics_sim.c pass unchanged with no
world attached.
Collision is opt-in. A backend with no collision world costs one comparison per
actor per step and does nothing else.
Also here:
- `self->gravity` was never null-checked and is dereferenced unconditionally, so
a hand-built backend with only `move` filled in crashed rather than reporting.
- Children are re-snapped in a post-pass, gated on collision being on. The
in-loop snap uses whatever position the parent had when the child's slot came
up in pool order, which is already sometimes a frame stale; that was harmless
while a parent only moved by v*dt and is not once a parent can be pushed out
of a wall mid-step.
- tests/physics.c's deliberate tripwire has been visited. It asserted that
arcade collide always raised AKERR_API, with a comment saying it existed so
that implementing collision would have to come back here. What replaces it
asserts the opt-in contract instead.
- physics_sim.c's hard-coded "%d of 7 simulations" is derived now. A literal
goes stale the first time somebody adds a simulation, which is this commit.
Three new whole-motion simulations, and the third took two attempts to make
honest:
- Landing and resting: 0.0000 px of drift over 120 steps after settling.
- Walking after landing: 107 px in a second, which is the symptom a player sees
when a resting actor is caught on the ground it is standing on.
- A fast fall not tunnelling. The first version used gravity, so the step size
grew every frame and the actor happened to land *inside* the floor rather than
stepping over it -- it passed with sub-stepping disabled and proved nothing.
It uses a constant 1200 px/s now, so every step is exactly 60 pixels against a
32-pixel window in which an overlap exists, and the samples miss it cleanly.
With sub-stepping the actor stops at y=288; without it, y=1300.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 07:05:09 -04:00
|
|
|
// One pass to give every shaped actor a proxy and take one back from
|
|
|
|
|
// anything that lost its shape, so the loop below never has to.
|
|
|
|
|
if ( self->collision != NULL ) {
|
|
|
|
|
PASS(errctx, akgl_collision_sync_actors(self->collision));
|
|
|
|
|
}
|
|
|
|
|
|
Reindent all C sources to the canonical stroustrup style
Mechanical whitespace-only change across src/, include/, tests/, and
util/, applied with scripts/reindent.sh. No behavioral change.
Most files already conformed. The genuine outliers indented at 2 columns
(json_helpers.c, util.c from akgl_rectangle_points onward, assets.c,
staticstring.c, akgl_actor_add_child, util.h, staticstring.h) or used
spaces where the canonical form is a tab (draw.c). cc-mode also re-aligns
line-continuation backslashes in multi-line macros to its default
c-backslash-column, which is required for the tree to be a fixed point of
the indenter.
Verified whitespace-only: `git diff -w` over this change is empty apart
from three trailing blank lines removed at EOF in src/heap.c,
src/registry.c, and tests/tilemap.c. The build produces no new errors and
ctest is unchanged at 13/14, with `character` still the one intentional
failure.
The tree is now a fixed point of `scripts/reindent.sh --check`.
include/akgl/SDL_GameControllerDB.h is generated and excluded.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 18:17:37 -04:00
|
|
|
|
2026-05-26 10:36:31 -04:00
|
|
|
for ( int i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) {
|
Namespace every exported symbol, and bump to 0.5.0
Closes internal-consistency items 1 through 6, 12 and 13. Every include guard
is _AKGL_<FILE>_H_, every in-project header include is angled, and every
exported function, type and global carries the akgl_ prefix. This is an ABI
break; the soname goes to libakgl.so.0.5. TODO.md carries the full rename
table.
The renames were driven by renaming each declaration and letting the compiler
find the uses, not by pattern substitution: renderer, physics and camera are
also parameter and struct-member names, and a sed would have rewritten
map->physics and every akgl_RenderBackend *renderer parameter without a word.
Item 4 turned out not to be cosmetic. The library exported a global called
renderer and tests/character.c defined an SDL_Renderer *renderer of its own;
the executable's definition preempted the library's, akgl_sprite_load_json
read a SDL_Renderer * through an akgl_RenderBackend *, and every texture load
in that suite failed. The suite reported success anyway, because libakerror's
unhandled-error handler ends in exit(errctx->status), exit keeps only the low
byte, and AKGL_ERR_SDL is exactly 256. So character had been green while
running one of its four tests, and every suite in the tree was unable to fail
on the most common status in a library built on SDL.
Both are fixed. tests/testutil.h gains TEST_TRAP_UNHANDLED_ERRORS(), which
collapses any status a byte cannot carry onto 1, and every suite installs it.
character binds a real backend with akgl_render_2d_bind. Its fourth test then
runs for the first time and fails on a defect it has asserted all along, so
akgl_heap_release_character now walks state_sprites with
AKGL_ITERATOR_OP_RELEASE and destroys the property set before zeroing the slot
-- TODO.md Defects item 21 and half of Carried over item 1.
AKGL_TIME_ONESEC_MS said "one second in milliseconds" and held 1000000, so
akgl_game_state_lock waited roughly sixteen minutes rather than one second. It
is AKGL_TIME_ONEMS_NS now, the budget is its own named constant, and
tests/game.c holds the mutex from a second thread to assert the wait -- the
contended path had no coverage at all.
Headers are self-contained and it is enforced: AKGL_PUBLIC_HEADERS drives both
install() and a generated translation unit per header, so a header that ships
is a header that is checked. Writing that found registry.h, which used
SDL_PropertiesID in eight declarations and included no SDL header.
23/23 suites pass, memcheck is clean, reindent --check is clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 23:32:21 -04:00
|
|
|
actor = &akgl_heap_actors[i];
|
2026-05-26 10:36:31 -04:00
|
|
|
if ( actor->refcount == 0 ) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-05-26 11:22:45 -04:00
|
|
|
if ( actor->parent != NULL ) {
|
|
|
|
|
// Children don't move independently of their parents, they just have an offset
|
|
|
|
|
actor->x = actor->parent->x + actor->vx;
|
|
|
|
|
actor->y = actor->parent->y + actor->vy;
|
|
|
|
|
actor->z = actor->parent->z + actor->vz;
|
|
|
|
|
continue;
|
|
|
|
|
} else if ( actor->basechar == NULL ) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-05-26 10:36:31 -04:00
|
|
|
if ( AKGL_BITMASK_HAS(opflags->flags, AKGL_ITERATOR_OP_LAYERMASK) ) {
|
|
|
|
|
if ( actor->layer != opflags->layerid ) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
Reindent all C sources to the canonical stroustrup style
Mechanical whitespace-only change across src/, include/, tests/, and
util/, applied with scripts/reindent.sh. No behavioral change.
Most files already conformed. The genuine outliers indented at 2 columns
(json_helpers.c, util.c from akgl_rectangle_points onward, assets.c,
staticstring.c, akgl_actor_add_child, util.h, staticstring.h) or used
spaces where the canonical form is a tab (draw.c). cc-mode also re-aligns
line-continuation backslashes in multi-line macros to its default
c-backslash-column, which is required for the tree to be a fixed point of
the indenter.
Verified whitespace-only: `git diff -w` over this change is empty apart
from three trailing blank lines removed at EOF in src/heap.c,
src/registry.c, and tests/tilemap.c. The build produces no new errors and
ctest is unchanged at 13/14, with `character` still the one intentional
failure.
The tree is now a fixed point of `scripts/reindent.sh --check`.
include/akgl/SDL_GameControllerDB.h is generated and excluded.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 18:17:37 -04:00
|
|
|
|
2026-05-26 11:22:45 -04:00
|
|
|
// thrust is a function of acceleration on a given axis
|
2026-05-26 10:36:31 -04:00
|
|
|
if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_LEFT) ||
|
Reindent all C sources to the canonical stroustrup style
Mechanical whitespace-only change across src/, include/, tests/, and
util/, applied with scripts/reindent.sh. No behavioral change.
Most files already conformed. The genuine outliers indented at 2 columns
(json_helpers.c, util.c from akgl_rectangle_points onward, assets.c,
staticstring.c, akgl_actor_add_child, util.h, staticstring.h) or used
spaces where the canonical form is a tab (draw.c). cc-mode also re-aligns
line-continuation backslashes in multi-line macros to its default
c-backslash-column, which is required for the tree to be a fixed point of
the indenter.
Verified whitespace-only: `git diff -w` over this change is empty apart
from three trailing blank lines removed at EOF in src/heap.c,
src/registry.c, and tests/tilemap.c. The build produces no new errors and
ctest is unchanged at 13/14, with `character` still the one intentional
failure.
The tree is now a fixed point of `scripts/reindent.sh --check`.
include/akgl/SDL_GameControllerDB.h is generated and excluded.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 18:17:37 -04:00
|
|
|
AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_RIGHT) ) {
|
2026-05-26 11:22:45 -04:00
|
|
|
actor->tx += actor->ax * dt;
|
2026-05-26 10:36:31 -04:00
|
|
|
}
|
|
|
|
|
if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_UP) ||
|
Reindent all C sources to the canonical stroustrup style
Mechanical whitespace-only change across src/, include/, tests/, and
util/, applied with scripts/reindent.sh. No behavioral change.
Most files already conformed. The genuine outliers indented at 2 columns
(json_helpers.c, util.c from akgl_rectangle_points onward, assets.c,
staticstring.c, akgl_actor_add_child, util.h, staticstring.h) or used
spaces where the canonical form is a tab (draw.c). cc-mode also re-aligns
line-continuation backslashes in multi-line macros to its default
c-backslash-column, which is required for the tree to be a fixed point of
the indenter.
Verified whitespace-only: `git diff -w` over this change is empty apart
from three trailing blank lines removed at EOF in src/heap.c,
src/registry.c, and tests/tilemap.c. The build produces no new errors and
ctest is unchanged at 13/14, with `character` still the one intentional
failure.
The tree is now a fixed point of `scripts/reindent.sh --check`.
include/akgl/SDL_GameControllerDB.h is generated and excluded.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 18:17:37 -04:00
|
|
|
AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_DOWN) ) {
|
2026-05-26 11:22:45 -04:00
|
|
|
actor->ty += actor->ay * dt;
|
|
|
|
|
}
|
|
|
|
|
|
Fix three arcade physics defects the unit tests could not see
tests/physics_sim.c runs the arcade backend the way a game does -- a
Mario-esque jump and fall, Zelda-style top-down walking, and a run
reversed at full speed -- and prints what the actor actually did.
tests/physics.c already checked that akgl_physics_simulate does the
arithmetic it claims. Every one of these got past it.
The first step was however long the level took to load. gravity_time was
never initialized and dt was unbounded, so a 250 ms load produced a
250 ms step: 100 px of fall where a 60 Hz frame under the same gravity
is 0.44 px, straight through whatever was underneath. Both initializers
seed the clock, and simulate bounds dt to max_timestep -- a new
physics.max_timestep property, default 0.05 s, read like gravity and
drag. Zero disables the bound. The field replaces the dead timer_gravity,
so the struct is the same size.
Releasing a vertical key cancelled gravity. The _off handlers zeroed ay,
ey, ty and vy together, and ey is where the arcade backend accumulates
gravity -- tapping down mid-jump stopped the character in the air. They
clear ax/tx (or ay/ty) and nothing else now. Velocity was never theirs
to clear either: simulate recomputes v as e + t every step.
Diagonal movement was 41% too fast. Thrust was capped per axis, so an
actor holding two directions got both caps at once and travelled their
diagonal. It is capped as a vector against the sx/sy/sz ellipse, which
also keeps a character whose horizontal and vertical top speeds differ
moving at the ratio it asked for. An axis with a zero max speed stays
out of the magnitude and is forced to zero, as the old clamp did.
Each fix was checked by reverting it and confirming the simulation goes
red: 100.1 px, vy 0.0 after a down tap, and 141% diagonal respectively.
tests/actor.c and tests/physics.c pinned the old behaviour in both
places and now assert the new contract.
Bumped to 0.6.0: akgl_PhysicsBackend changed. TODO.md records the four
things the simulations found and this does not fix -- no terminal
velocity, no deceleration on release, Euler's frame-rate dependence, and
a drag coefficient large enough to invert velocity.
26/26 ctest, memcheck clean, warning-clean at -Wall -Werror.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B8T5FAYXE8HEJqFLCYwNNc
2026-08-01 12:10:53 -04:00
|
|
|
// Cap the thrust *vector* against the ellipse the character's per-axis
|
|
|
|
|
// top speeds describe, rather than each axis against its own cap.
|
|
|
|
|
//
|
|
|
|
|
// Capping the axes independently lets the corner of the box through: an
|
|
|
|
|
// actor holding two directions at once got both caps at once and
|
|
|
|
|
// travelled their diagonal, which measured 41% faster than either alone.
|
|
|
|
|
// Scaling to the ellipse keeps a character whose horizontal and vertical
|
|
|
|
|
// speeds differ moving at the ratio it asked for, and makes them equal
|
|
|
|
|
// where the speeds are.
|
|
|
|
|
//
|
|
|
|
|
// An axis with a top speed of zero cannot be thrust along at all, which
|
|
|
|
|
// is what the old per-axis clamp did with it, and it stays out of the
|
|
|
|
|
// magnitude entirely -- dividing by it would not end well.
|
|
|
|
|
overshoot = 0.0f;
|
|
|
|
|
if ( actor->sx != 0.0f ) {
|
|
|
|
|
overshoot += (actor->tx / actor->sx) * (actor->tx / actor->sx);
|
|
|
|
|
} else {
|
|
|
|
|
actor->tx = 0.0f;
|
2026-05-26 11:22:45 -04:00
|
|
|
}
|
Fix three arcade physics defects the unit tests could not see
tests/physics_sim.c runs the arcade backend the way a game does -- a
Mario-esque jump and fall, Zelda-style top-down walking, and a run
reversed at full speed -- and prints what the actor actually did.
tests/physics.c already checked that akgl_physics_simulate does the
arithmetic it claims. Every one of these got past it.
The first step was however long the level took to load. gravity_time was
never initialized and dt was unbounded, so a 250 ms load produced a
250 ms step: 100 px of fall where a 60 Hz frame under the same gravity
is 0.44 px, straight through whatever was underneath. Both initializers
seed the clock, and simulate bounds dt to max_timestep -- a new
physics.max_timestep property, default 0.05 s, read like gravity and
drag. Zero disables the bound. The field replaces the dead timer_gravity,
so the struct is the same size.
Releasing a vertical key cancelled gravity. The _off handlers zeroed ay,
ey, ty and vy together, and ey is where the arcade backend accumulates
gravity -- tapping down mid-jump stopped the character in the air. They
clear ax/tx (or ay/ty) and nothing else now. Velocity was never theirs
to clear either: simulate recomputes v as e + t every step.
Diagonal movement was 41% too fast. Thrust was capped per axis, so an
actor holding two directions got both caps at once and travelled their
diagonal. It is capped as a vector against the sx/sy/sz ellipse, which
also keeps a character whose horizontal and vertical top speeds differ
moving at the ratio it asked for. An axis with a zero max speed stays
out of the magnitude and is forced to zero, as the old clamp did.
Each fix was checked by reverting it and confirming the simulation goes
red: 100.1 px, vy 0.0 after a down tap, and 141% diagonal respectively.
tests/actor.c and tests/physics.c pinned the old behaviour in both
places and now assert the new contract.
Bumped to 0.6.0: akgl_PhysicsBackend changed. TODO.md records the four
things the simulations found and this does not fix -- no terminal
velocity, no deceleration on release, Euler's frame-rate dependence, and
a drag coefficient large enough to invert velocity.
26/26 ctest, memcheck clean, warning-clean at -Wall -Werror.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B8T5FAYXE8HEJqFLCYwNNc
2026-08-01 12:10:53 -04:00
|
|
|
if ( actor->sy != 0.0f ) {
|
|
|
|
|
overshoot += (actor->ty / actor->sy) * (actor->ty / actor->sy);
|
|
|
|
|
} else {
|
|
|
|
|
actor->ty = 0.0f;
|
2026-05-26 11:22:45 -04:00
|
|
|
}
|
Fix three arcade physics defects the unit tests could not see
tests/physics_sim.c runs the arcade backend the way a game does -- a
Mario-esque jump and fall, Zelda-style top-down walking, and a run
reversed at full speed -- and prints what the actor actually did.
tests/physics.c already checked that akgl_physics_simulate does the
arithmetic it claims. Every one of these got past it.
The first step was however long the level took to load. gravity_time was
never initialized and dt was unbounded, so a 250 ms load produced a
250 ms step: 100 px of fall where a 60 Hz frame under the same gravity
is 0.44 px, straight through whatever was underneath. Both initializers
seed the clock, and simulate bounds dt to max_timestep -- a new
physics.max_timestep property, default 0.05 s, read like gravity and
drag. Zero disables the bound. The field replaces the dead timer_gravity,
so the struct is the same size.
Releasing a vertical key cancelled gravity. The _off handlers zeroed ay,
ey, ty and vy together, and ey is where the arcade backend accumulates
gravity -- tapping down mid-jump stopped the character in the air. They
clear ax/tx (or ay/ty) and nothing else now. Velocity was never theirs
to clear either: simulate recomputes v as e + t every step.
Diagonal movement was 41% too fast. Thrust was capped per axis, so an
actor holding two directions got both caps at once and travelled their
diagonal. It is capped as a vector against the sx/sy/sz ellipse, which
also keeps a character whose horizontal and vertical top speeds differ
moving at the ratio it asked for. An axis with a zero max speed stays
out of the magnitude and is forced to zero, as the old clamp did.
Each fix was checked by reverting it and confirming the simulation goes
red: 100.1 px, vy 0.0 after a down tap, and 141% diagonal respectively.
tests/actor.c and tests/physics.c pinned the old behaviour in both
places and now assert the new contract.
Bumped to 0.6.0: akgl_PhysicsBackend changed. TODO.md records the four
things the simulations found and this does not fix -- no terminal
velocity, no deceleration on release, Euler's frame-rate dependence, and
a drag coefficient large enough to invert velocity.
26/26 ctest, memcheck clean, warning-clean at -Wall -Werror.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B8T5FAYXE8HEJqFLCYwNNc
2026-08-01 12:10:53 -04:00
|
|
|
if ( actor->sz != 0.0f ) {
|
|
|
|
|
overshoot += (actor->tz / actor->sz) * (actor->tz / actor->sz);
|
|
|
|
|
} else {
|
|
|
|
|
actor->tz = 0.0f;
|
|
|
|
|
}
|
|
|
|
|
if ( overshoot > 1.0f ) {
|
|
|
|
|
thrustscale = 1.0f / sqrtf(overshoot);
|
|
|
|
|
actor->tx *= thrustscale;
|
|
|
|
|
actor->ty *= thrustscale;
|
|
|
|
|
actor->tz *= thrustscale;
|
2026-05-26 10:36:31 -04:00
|
|
|
}
|
2026-05-26 11:22:45 -04:00
|
|
|
ATTEMPT {
|
2026-07-31 23:58:28 -04:00
|
|
|
CATCH(errctx, actor->movementlogicfunc(actor, dt));
|
|
|
|
|
PASS(errctx, self->gravity(self, actor, dt));
|
2026-05-26 11:22:45 -04:00
|
|
|
|
|
|
|
|
// Counteract velocity with atmospheric drag
|
2026-06-02 17:11:16 -04:00
|
|
|
if ( self->drag_x != 0 ) {
|
|
|
|
|
actor->ex -= actor->ex * self->drag_x * dt;
|
|
|
|
|
}
|
|
|
|
|
if ( self->drag_y != 0 ) {
|
|
|
|
|
actor->ey -= actor->ey * self->drag_y * dt;
|
|
|
|
|
}
|
|
|
|
|
if ( self->drag_z != 0 ) {
|
|
|
|
|
actor->ez -= actor->ez * self->drag_z * dt;
|
|
|
|
|
}
|
2026-05-26 11:22:45 -04:00
|
|
|
|
|
|
|
|
actor->vx = actor->ex + actor->tx;
|
|
|
|
|
actor->vy = actor->ey + actor->ty;
|
|
|
|
|
actor->vz = actor->ez + actor->tz;
|
|
|
|
|
|
Call collide from the simulation, after the move rather than before it
The collide slot has existed unused since the backend was written. It is wired
in now, and its signature changes from (self, a1, a2) to (self, actor, dt): the
old shape had nowhere to put a normal, a depth, or a piece of map geometry, and
no answer to which of the two actors it was supposed to be resolving.
**Resolution runs after `move`.** That is the decision everything else follows
from. A resolver called from movementlogicfunc runs before gravity, drag and the
move, so it has to predict where the actor will end up -- which means
re-implementing the integrator's arithmetic in the game, and being wrong
whenever the integrator changes. examples/sidescroller/collision.c carries forty
lines of exactly that, and says so.
Only `move` is subdivided. Gravity, drag, the thrust integration and the speed
ellipse all still run once against the whole dt, and sum(subdt) is dt, so an
actor with nothing to collide with takes one sub-step of exactly dt -- `dt/1.0f`
is dt exactly in IEEE-754 -- and follows the arithmetic path it always did. That
is what lets the integrator stay untouched and every recorded physics number
stay valid, and tests/physics.c and tests/physics_sim.c pass unchanged with no
world attached.
Collision is opt-in. A backend with no collision world costs one comparison per
actor per step and does nothing else.
Also here:
- `self->gravity` was never null-checked and is dereferenced unconditionally, so
a hand-built backend with only `move` filled in crashed rather than reporting.
- Children are re-snapped in a post-pass, gated on collision being on. The
in-loop snap uses whatever position the parent had when the child's slot came
up in pool order, which is already sometimes a frame stale; that was harmless
while a parent only moved by v*dt and is not once a parent can be pushed out
of a wall mid-step.
- tests/physics.c's deliberate tripwire has been visited. It asserted that
arcade collide always raised AKERR_API, with a comment saying it existed so
that implementing collision would have to come back here. What replaces it
asserts the opt-in contract instead.
- physics_sim.c's hard-coded "%d of 7 simulations" is derived now. A literal
goes stale the first time somebody adds a simulation, which is this commit.
Three new whole-motion simulations, and the third took two attempts to make
honest:
- Landing and resting: 0.0000 px of drift over 120 steps after settling.
- Walking after landing: 107 px in a second, which is the symptom a player sees
when a resting actor is caught on the ground it is standing on.
- A fast fall not tunnelling. The first version used gravity, so the step size
grew every frame and the actor happened to land *inside* the floor rather than
stepping over it -- it passed with sub-stepping disabled and proved nothing.
It uses a constant 1200 px/s now, so every step is exactly 60 pixels against a
32-pixel window in which an overlap exists, and the samples miss it cleanly.
With sub-stepping the actor stops at y=288; without it, y=1300.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 07:05:09 -04:00
|
|
|
/*
|
|
|
|
|
* Only `move` is subdivided. Gravity, drag, the thrust integration
|
|
|
|
|
* and the speed-ellipse cap above all ran once against the whole dt,
|
|
|
|
|
* and sum(subdt) is dt -- so an actor with nothing to collide with
|
|
|
|
|
* takes exactly one sub-step of exactly dt and follows the arithmetic
|
|
|
|
|
* path it always did. That is what lets the integrator stay untouched
|
|
|
|
|
* and every recorded physics number stay valid.
|
|
|
|
|
*/
|
Measure collision, and say which numbers moved for what reason
Seven new benchmark rows, budgets set from a measured full-scale run rather
than guessed, and the three existing rows the work moved re-recorded from that
same run. The all-pairs sweep stays and is relabelled `control:` -- it is the
cost a caller paid before the library had a broad phase, measured on the same
machine in the same run, which is the only honest way to read a reduction.
What the numbers say:
- The box fast path is two to seven times cheaper than the general solver, and
the disjoint case is cheaper still because the proxies' bounds reject it before
any shape arithmetic runs. 9 to 20 ns is what a tile game actually pays.
- The grid beats the tree by 2.6x on the same population, which is the argument
for the default measured here rather than cited from another engine. The tree
also rebuilds on every move and that is not in its row, so a moving scene is
worse than 2.6x.
- A grid `move` that changes nothing is 11.5 ns. That is the incremental claim
in one number.
Two rows moved on a backend with **no collision world attached**, so no
collision code runs in either, and the commit says so rather than letting the
feature take credit:
- akgl_Actor grew from 415 to 464 bytes -- a 40-byte shape, an override flag and
a proxy pointer. The step sweeps the whole pool, so that is about 3 KB more
working set per frame. The empty-pool row is unchanged at 58.7 ns, which is
what identifies the cost as per live actor rather than per slot.
- Reaching the collision check through CATCH cost more than the check. PASS and
CATCH call akerr_valid_error_address, which walks AKERR_ARRAY_ERROR, so an
early-returning function is not free when it is reached through one. The
no-collision path now routes around the machinery entirely, which took the
64-actor sweep from 2,018.7 ns back to 1,588.5. AGENTS.md records that lesson
for writing benchmarks; this is the same thing in production code.
The remainder is the struct, and it is paid whether or not a game uses
collision.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 07:13:53 -04:00
|
|
|
if ( (self->collision == NULL) || (self->collide == NULL) ) {
|
|
|
|
|
/*
|
|
|
|
|
* No collision: exactly the call this has always made, with
|
|
|
|
|
* nothing added around it. Not merely equivalent -- CATCH and
|
|
|
|
|
* PASS walk AKERR_ARRAY_ERROR to validate the context, which
|
|
|
|
|
* costs more than several of the calls being measured, so even
|
|
|
|
|
* a check that answers "no" instantly is not free if it is
|
|
|
|
|
* reached through one. Measured: routing the off path through
|
|
|
|
|
* one extra CATCH took the 64-actor sweep from 1.2 us to 2.0.
|
|
|
|
|
*/
|
|
|
|
|
PASS(errctx, self->move(self, actor, dt));
|
|
|
|
|
} else {
|
|
|
|
|
CATCH(errctx, akgl_collision_substeps(self->collision, actor, dt, &substeps));
|
|
|
|
|
subdt = dt / (float32_t)substeps;
|
|
|
|
|
for ( s = 0; s < substeps; s++ ) {
|
|
|
|
|
PASS(errctx, self->move(self, actor, subdt));
|
Call collide from the simulation, after the move rather than before it
The collide slot has existed unused since the backend was written. It is wired
in now, and its signature changes from (self, a1, a2) to (self, actor, dt): the
old shape had nowhere to put a normal, a depth, or a piece of map geometry, and
no answer to which of the two actors it was supposed to be resolving.
**Resolution runs after `move`.** That is the decision everything else follows
from. A resolver called from movementlogicfunc runs before gravity, drag and the
move, so it has to predict where the actor will end up -- which means
re-implementing the integrator's arithmetic in the game, and being wrong
whenever the integrator changes. examples/sidescroller/collision.c carries forty
lines of exactly that, and says so.
Only `move` is subdivided. Gravity, drag, the thrust integration and the speed
ellipse all still run once against the whole dt, and sum(subdt) is dt, so an
actor with nothing to collide with takes one sub-step of exactly dt -- `dt/1.0f`
is dt exactly in IEEE-754 -- and follows the arithmetic path it always did. That
is what lets the integrator stay untouched and every recorded physics number
stay valid, and tests/physics.c and tests/physics_sim.c pass unchanged with no
world attached.
Collision is opt-in. A backend with no collision world costs one comparison per
actor per step and does nothing else.
Also here:
- `self->gravity` was never null-checked and is dereferenced unconditionally, so
a hand-built backend with only `move` filled in crashed rather than reporting.
- Children are re-snapped in a post-pass, gated on collision being on. The
in-loop snap uses whatever position the parent had when the child's slot came
up in pool order, which is already sometimes a frame stale; that was harmless
while a parent only moved by v*dt and is not once a parent can be pushed out
of a wall mid-step.
- tests/physics.c's deliberate tripwire has been visited. It asserted that
arcade collide always raised AKERR_API, with a comment saying it existed so
that implementing collision would have to come back here. What replaces it
asserts the opt-in contract instead.
- physics_sim.c's hard-coded "%d of 7 simulations" is derived now. A literal
goes stale the first time somebody adds a simulation, which is this commit.
Three new whole-motion simulations, and the third took two attempts to make
honest:
- Landing and resting: 0.0000 px of drift over 120 steps after settling.
- Walking after landing: 107 px in a second, which is the symptom a player sees
when a resting actor is caught on the ground it is standing on.
- A fast fall not tunnelling. The first version used gravity, so the step size
grew every frame and the actor happened to land *inside* the floor rather than
stepping over it -- it passed with sub-stepping disabled and proved nothing.
It uses a constant 1200 px/s now, so every step is exactly 60 pixels against a
32-pixel window in which an overlap exists, and the samples miss it cleanly.
With sub-stepping the actor stops at y=288; without it, y=1300.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 07:05:09 -04:00
|
|
|
PASS(errctx, self->collide(self, actor, subdt));
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-26 11:22:45 -04:00
|
|
|
} CLEANUP {
|
2026-07-31 23:58:28 -04:00
|
|
|
} PROCESS(errctx) {
|
|
|
|
|
} HANDLE(errctx, AKGL_ERR_LOGICINTERRUPT) {
|
2026-05-26 11:22:45 -04:00
|
|
|
// noop
|
2026-07-31 23:58:28 -04:00
|
|
|
} FINISH(errctx, true);
|
2026-05-26 10:36:31 -04:00
|
|
|
}
|
Call collide from the simulation, after the move rather than before it
The collide slot has existed unused since the backend was written. It is wired
in now, and its signature changes from (self, a1, a2) to (self, actor, dt): the
old shape had nowhere to put a normal, a depth, or a piece of map geometry, and
no answer to which of the two actors it was supposed to be resolving.
**Resolution runs after `move`.** That is the decision everything else follows
from. A resolver called from movementlogicfunc runs before gravity, drag and the
move, so it has to predict where the actor will end up -- which means
re-implementing the integrator's arithmetic in the game, and being wrong
whenever the integrator changes. examples/sidescroller/collision.c carries forty
lines of exactly that, and says so.
Only `move` is subdivided. Gravity, drag, the thrust integration and the speed
ellipse all still run once against the whole dt, and sum(subdt) is dt, so an
actor with nothing to collide with takes one sub-step of exactly dt -- `dt/1.0f`
is dt exactly in IEEE-754 -- and follows the arithmetic path it always did. That
is what lets the integrator stay untouched and every recorded physics number
stay valid, and tests/physics.c and tests/physics_sim.c pass unchanged with no
world attached.
Collision is opt-in. A backend with no collision world costs one comparison per
actor per step and does nothing else.
Also here:
- `self->gravity` was never null-checked and is dereferenced unconditionally, so
a hand-built backend with only `move` filled in crashed rather than reporting.
- Children are re-snapped in a post-pass, gated on collision being on. The
in-loop snap uses whatever position the parent had when the child's slot came
up in pool order, which is already sometimes a frame stale; that was harmless
while a parent only moved by v*dt and is not once a parent can be pushed out
of a wall mid-step.
- tests/physics.c's deliberate tripwire has been visited. It asserted that
arcade collide always raised AKERR_API, with a comment saying it existed so
that implementing collision would have to come back here. What replaces it
asserts the opt-in contract instead.
- physics_sim.c's hard-coded "%d of 7 simulations" is derived now. A literal
goes stale the first time somebody adds a simulation, which is this commit.
Three new whole-motion simulations, and the third took two attempts to make
honest:
- Landing and resting: 0.0000 px of drift over 120 steps after settling.
- Walking after landing: 107 px in a second, which is the symptom a player sees
when a resting actor is caught on the ground it is standing on.
- A fast fall not tunnelling. The first version used gravity, so the step size
grew every frame and the actor happened to land *inside* the floor rather than
stepping over it -- it passed with sub-stepping disabled and proved nothing.
It uses a constant 1200 px/s now, so every step is exactly 60 pixels against a
32-pixel window in which an overlap exists, and the samples miss it cleanly.
With sub-stepping the actor stops at y=288; without it, y=1300.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 07:05:09 -04:00
|
|
|
/*
|
|
|
|
|
* Re-snap children after everything has moved, but only when collision is
|
|
|
|
|
* on. The in-loop snap uses whatever position the parent had when the
|
|
|
|
|
* child's own slot came up in pool order, which is already sometimes a
|
|
|
|
|
* frame stale -- harmless while a parent only ever moved by `v * dt`, and
|
|
|
|
|
* not harmless once a parent can also be pushed out of a wall part way
|
|
|
|
|
* through its own sub-steps. Gating it keeps a collision-free world
|
|
|
|
|
* byte-for-byte what it was.
|
|
|
|
|
*/
|
|
|
|
|
if ( self->collision != NULL ) {
|
|
|
|
|
for ( int i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) {
|
|
|
|
|
actor = &akgl_heap_actors[i];
|
|
|
|
|
if ( (actor->refcount == 0) || (actor->parent == NULL) ) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
actor->x = actor->parent->x + actor->vx;
|
|
|
|
|
actor->y = actor->parent->y + actor->vy;
|
|
|
|
|
actor->z = actor->parent->z + actor->vz;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Reindent all C sources to the canonical stroustrup style
Mechanical whitespace-only change across src/, include/, tests/, and
util/, applied with scripts/reindent.sh. No behavioral change.
Most files already conformed. The genuine outliers indented at 2 columns
(json_helpers.c, util.c from akgl_rectangle_points onward, assets.c,
staticstring.c, akgl_actor_add_child, util.h, staticstring.h) or used
spaces where the canonical form is a tab (draw.c). cc-mode also re-aligns
line-continuation backslashes in multi-line macros to its default
c-backslash-column, which is required for the tree to be a fixed point of
the indenter.
Verified whitespace-only: `git diff -w` over this change is empty apart
from three trailing blank lines removed at EOF in src/heap.c,
src/registry.c, and tests/tilemap.c. The build produces no new errors and
ctest is unchanged at 13/14, with `character` still the one intentional
failure.
The tree is now a fixed point of `scripts/reindent.sh --check`.
include/akgl/SDL_GameControllerDB.h is generated and excluded.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 18:17:37 -04:00
|
|
|
self->gravity_time = curtime;
|
2026-07-31 23:58:28 -04:00
|
|
|
SUCCEED_RETURN(errctx);
|
2026-05-26 10:36:31 -04:00
|
|
|
}
|
2026-06-02 17:11:16 -04:00
|
|
|
|
Give every exported function a declaration, and check that it stays that way
Closes internal-consistency items 7 through 15. Nineteen non-static functions
were in the ABI with no declaration anywhere, so no consumer could call them
and any consumer could collide with them.
The four gamepad_handle_* functions are the ones that mattered: controller.h
declared akgl_controller_handle_button_down and three siblings that did not
exist, so anything compiled against the header alone failed to link. The
definitions carry the declared names now, which also closes Defects -> Known
and still open item 10, and their documentation moved to the header.
The rest are either declared under a "part of the internal API" block --
akgl_game_save_actors and akgl_game_load_versioncmp, which tests/game.c had to
declare for itself, plus six tilemap loader helpers the untested-loader work
wants to reach -- or static, which is what the four save iterators and
load_objectnamemap should always have been. akgl_path_relative_from is deleted:
declared nowhere, called from nowhere, never wrote its output, and leaked a
pooled string on every call, so it closes Known and still open item 4 and item
40 by ceasing to exist.
scripts/check_api_surface.sh keeps it closed. It reads the built library's
dynamic symbol table and every public header with comments stripped, and fails
on an exported akgl_* symbol that is declared nowhere. Stripping comments is
the whole point -- four of these were mentioned in controller.h prose, which is
how they went unnoticed.
The pool-size ceilings are defined once, in heap.h, so the #ifndef override
hook fires for the first time; actor.h, sprite.h and character.h were defining
the same four unconditionally from headers heap.h includes above its own guard.
tests/header_pool_override.c fails the compile if that regresses.
Also here: (void) rather than () on the twelve no-argument entry points,
AKERR_NOIGNORE only on declarations, static helpers with the akgl_ prefix
dropped, and the six parameter-name mismatches. akgl_get_json_with_default had
its two contexts swapped rather than merely misspelled -- the incoming one was
`err` and its own was `e`, which is the name reserved for an incoming one.
24/24 pass, reindent --check clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 23:44:38 -04:00
|
|
|
akerr_ErrorContext *akgl_physics_factory(akgl_PhysicsBackend *self, akgl_String *type)
|
2026-06-02 17:11:16 -04:00
|
|
|
{
|
2026-07-31 23:58:28 -04:00
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, type, AKERR_NULLPOINTER, "type");
|
Reindent all C sources to the canonical stroustrup style
Mechanical whitespace-only change across src/, include/, tests/, and
util/, applied with scripts/reindent.sh. No behavioral change.
Most files already conformed. The genuine outliers indented at 2 columns
(json_helpers.c, util.c from akgl_rectangle_points onward, assets.c,
staticstring.c, akgl_actor_add_child, util.h, staticstring.h) or used
spaces where the canonical form is a tab (draw.c). cc-mode also re-aligns
line-continuation backslashes in multi-line macros to its default
c-backslash-column, which is required for the tree to be a fixed point of
the indenter.
Verified whitespace-only: `git diff -w` over this change is empty apart
from three trailing blank lines removed at EOF in src/heap.c,
src/registry.c, and tests/tilemap.c. The build produces no new errors and
ctest is unchanged at 13/14, with `character` still the one intentional
failure.
The tree is now a fixed point of `scripts/reindent.sh --check`.
include/akgl/SDL_GameControllerDB.h is generated and excluded.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 18:17:37 -04:00
|
|
|
|
2026-06-02 17:11:16 -04:00
|
|
|
if ( strncmp(type->data, "null", 4) == 0) {
|
2026-07-31 23:58:28 -04:00
|
|
|
PASS(errctx, akgl_physics_init_null(self));
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
2026-06-02 17:11:16 -04:00
|
|
|
}
|
|
|
|
|
if ( strncmp(type->data, "arcade", 6) == 0) {
|
2026-07-31 23:58:28 -04:00
|
|
|
PASS(errctx, akgl_physics_init_arcade(self));
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
2026-06-02 17:11:16 -04:00
|
|
|
}
|
2026-07-31 23:58:28 -04:00
|
|
|
FAIL_RETURN(errctx, AKERR_KEY, "Invalid physics engine %s", type->data);
|
2026-06-02 17:11:16 -04:00
|
|
|
}
|