Collide sprites with rectangles that are not sprites
Some checks failed
akbasic CI Build / cmake_build (push) Failing after 3m19s
akbasic CI Build / sanitizers (push) Failing after 4m33s
akbasic CI Build / coverage (push) Failing after 3m41s
akbasic CI Build / akgl_build (push) Failing after 21s
akbasic CI Build / mutation_test (push) Failing after 3m29s
Some checks failed
akbasic CI Build / cmake_build (push) Failing after 3m19s
akbasic CI Build / sanitizers (push) Failing after 4m33s
akbasic CI Build / coverage (push) Failing after 3m41s
akbasic CI Build / akgl_build (push) Failing after 21s
akbasic CI Build / mutation_test (push) Failing after 3m29s
`SOLID id, x1, y1, x2, y2` registers static collision geometry; `SOLID id` retires one and a bare `SOLID` retires them all, the way `TRAP`, `COLLISION` and `DCLOSE` all read absence. `COLLISION 2` and `BUMP(2)` stop being refused and mean *sprite met static geometry*. **This is the thing eight sprite slots made impossible.** A wall of bricks wants sixty, so until now a program could only collide with one by doing the arithmetic itself against its own array -- which is exactly what both breakout listings do, at about two hundred lines between them. A rectangle costs no sprite slot. The id is the **program's own number**, 1 to 64, not a minted handle. That is the whole trick for "which brick did I hit": the id comes back out again, so a wall built as `SOLID I#, ...` maps onto `B#(I#)` with no lookup, and retiring a broken brick is `SOLID I#`. `COLLISION 2` was refused with "sprite-to-background collision needs the screen read back every frame", which was true of the question a C128 asks -- a sprite against the bitmap's set pixels. `SOLID` gives this interpreter a background made of rectangles instead, which is the same question in a form it can answer. Same move `SPRSAV` made when it learned to take an image path. `AKBASIC_INTERRUPT_BACKGROUND` has been sitting in the interrupt table commented "COLLISION 2 -- sprite met background; refused" the whole time. Its accumulator is separate, so a sprite hitting a wall never sets a bit in `BUMP(1)`. **There is no `akgl_CollisionWorld` here, and that is deliberate.** libakgl's uniform grid keeps its cell heads, cell size and origin in file-scope statics, so it is one index per process -- and `akgl_collision_world_init()` ends in a `reset()` that memsets those heads *and* calls `akgl_heap_init_collision_cells()`. An interpreter embedded in a game with its own collision world would have destroyed every registration that game had made, on the first `SOLID` a script ran. So the geometry is indexed by an ordinary array here and pairs go straight to `akgl_collision_test()`, which needs no world. At sixty-four rectangles that is the right answer anyway; libakgl's own numbers put a naive sweep at 0.7% of a frame at sixty-four objects. **The scan now short-circuits when nothing has moved**, and that is what makes any of it affordable. Its inputs are the sprites' boxes, which slots are collidable, and the static geometry; if none changed the answer cannot have. A frame runs one full scan and 255 cached ones. Eight sprites against sixty-four rectangles is five hundred and twelve tests -- fine once a frame, ruinous 256 times. The benchmark was rewritten to say which path it is timing, because with the cache in place a loop that only calls the scan measures the short circuit and nothing else. Breakout now costs 590.6 ns for its one full scan plus 255 cached at 40.0, which is 10.8 us against a 1.19 ms frame -- **0.91%, less than the 2.0% it cost before any of this work**, with static geometry and contacts added on top. `NEW` retires the rectangles, where it cannot undefine a sprite pattern: there *is* an entry point for this one, so leaving them would be a choice, and the wrong one -- a rectangle is invisible, so one left behind by a deleted program is an unexplainable collision in the next. `CLR` leaves them alone. `tests/sprite_verbs.c` gains the whole second path against the mock and its `COLLISION 2` case is rewritten: it pinned the refusal, and now pins that type 2 arms its own handler without disturbing type 1's. `tests/akgl_backends.c` gains the end-to-end version, including a full sixty-four-rectangle wall so the proxy budget is exercised at its ceiling and the pool has to come back intact, and the sixty-fifth refused by name. A bare `SOLID` needed `akbasic_parse_optional_arglist` rather than `akbasic_parse_arglist`, which `DCLOSE` already uses for the same shape. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL
This commit is contained in:
@@ -411,33 +411,49 @@ that have not moved. Is that worth changing to a per-frame cadence?
|
||||
|
||||
Measured at `RelWithDebInfo`, scale 10, best of 5:
|
||||
|
||||
| Row | ns/op | as 256 scans of a 1.20 ms frame |
|
||||
|---|---|---|
|
||||
| `spr_collisions`, 0 sprites | 16.6 | 0.35% |
|
||||
| `spr_collisions`, 2 sprites, spread out | 29.6 | 0.63% |
|
||||
| `spr_collisions`, 4 sprites, spread out | 37.4 | 0.80% |
|
||||
| `spr_collisions`, 8 sprites, spread out | 54.9 | 1.2% |
|
||||
| **`spr_collisions`, breakout's own layout** | **211.8** | **4.5%** |
|
||||
| `spr_collisions`, 8 sprites, all overlapping | 800.4 | 17% |
|
||||
| one rendered frame, 8 sprites + full text grid | 1,203,923 | — |
|
||||
| Row | ns/op |
|
||||
|---|---|
|
||||
| full scan, 0 sprites | 59.2 |
|
||||
| full scan, 2 sprites, spread out | 153.3 |
|
||||
| full scan, 4 sprites, spread out | 229.0 |
|
||||
| full scan, 8 sprites, spread out | 366.4 |
|
||||
| **full scan, breakout's own layout** | **590.6** |
|
||||
| full scan, 8 sprites, all overlapping | 1145.4 |
|
||||
| **cached scan, nothing moved** | **40.0** |
|
||||
| one rendered frame, 8 sprites + full text grid | 1,191,947 |
|
||||
|
||||
**The answer is no.** Even the row that matters most — the arrangement
|
||||
`examples/breakout/sprites/breakout.bas` actually has, which is the most demanding program
|
||||
here — is 4.5% of a frame, and it only reaches that because two of its eight sprites *are the
|
||||
screen*: a captured HUD strip and a captured play field, so the field's box covers everything
|
||||
and the bounding-box reject can never throw those pairs out. The frame is dominated by the text
|
||||
layer repainting every row it owns, which `TODO.md` already records as the real rendering cost.
|
||||
**Two paths, and which one a call takes is the whole story.** The scan short-circuits when no
|
||||
sprite has moved and no static geometry has changed since the last one — its inputs are exactly
|
||||
those, so if none changed the answer cannot have. A frame runs **one** full scan and 255 cached
|
||||
ones. The `full scan` rows nudge a sprite before each call to defeat that, so they measure a
|
||||
scan that actually ran; they therefore include a `MOVSPR`-equivalent, which is honest for
|
||||
comparison because a real frame pays that too.
|
||||
|
||||
So the per-step cadence stays. It is what makes a collision report describe where the sprites
|
||||
have just been moved to rather than where they were (`src/runtime.c`, above the service call),
|
||||
and buying a few percent of a frame is not worth changing when a handler fires for every program
|
||||
that already works. Recorded here rather than argued again.
|
||||
So for `examples/breakout/sprites/breakout.bas`, the most demanding program here:
|
||||
|
||||
590.6 + (255 × 40.0) = 10.8 µs against a 1.19 ms frame — 0.91%
|
||||
|
||||
**That is less than it cost before any of this work**, when it was 96.3 ns unconditionally on
|
||||
all 256 steps, or 24.7 µs, or 2.0% — and it now includes static geometry and produces contacts.
|
||||
The short circuit is what pays for both: eight sprites against sixty-four rectangles is five
|
||||
hundred and twelve tests, fine once a frame and ruinous 256 times.
|
||||
|
||||
**So the per-step cadence stays**, and the question of moving it to per-frame is answered "no"
|
||||
twice over. It is what makes a collision report describe where the sprites have just been moved
|
||||
to rather than where they were (`src/runtime.c`, above the service call); it changes when a
|
||||
handler fires for every program that already works; and the cost it was supposed to save is now
|
||||
under one percent of a frame. Recorded here rather than argued again.
|
||||
|
||||
**Read the two synthetic extremes as a bracket, not as an answer.** "Spread out" is every pair
|
||||
rejected on four comparisons; "all overlapping" is every pair going through to the narrowphase,
|
||||
which is a state no real program sits in. Real programs land between them, and the breakout row
|
||||
is there so the question "between them *where*" has an answer.
|
||||
|
||||
Breakout reaches the high end of that bracket for a reason worth knowing: two of its eight
|
||||
sprites *are the screen*, a captured HUD strip and a captured play field, so the field's box
|
||||
covers everything and the bounding-box reject can never throw those pairs out. That is
|
||||
`TODO.md` §9 item 9, and fixing it would take this row down as a side effect.
|
||||
|
||||
**Read a benchmark as a gap between two rows of the same run, not as an absolute.** libakgl's
|
||||
`PERFORMANCE.md` records a whole laptop reading 15% high on a later run, including rows nothing
|
||||
had touched.
|
||||
|
||||
@@ -204,6 +204,60 @@ also stops it colliding, and is what you want when it should not be seen either.
|
||||
`RSPHIT(n, f)` reads it back, in `SPRHIT`'s own argument order — 0 the kind, then 1 to 4
|
||||
for the two corners. It needs no sprite device, the way `RSPPOS` does not.
|
||||
|
||||
## Colliding with things that are not sprites
|
||||
|
||||
There are eight sprites. A wall of bricks wants sixty, and until now the only way to
|
||||
collide with one was to do the arithmetic yourself against your own array.
|
||||
|
||||
`SOLID` registers a rectangle the interpreter will collide sprites against, and it costs
|
||||
no sprite slot:
|
||||
|
||||
```basic norun
|
||||
SOLID id, x1, y1, x2, y2 register or replace rectangle id
|
||||
SOLID id retire it
|
||||
SOLID retire them all
|
||||
```
|
||||
|
||||
`id` is 1 to 64 and it is **your own number**. That is the whole trick for "which brick did
|
||||
I hit": the id comes back out again, so a wall built as `SOLID I#, ...` maps straight onto
|
||||
`B#(I#)` with no lookup. Retiring a broken brick is the same verb, `SOLID I#`.
|
||||
|
||||
Rectangles are in window pixels — the coordinates `BOX` draws in and `MOVSPR` positions in,
|
||||
with `SCALE` not applying, exactly as for a sprite. So a brick drawn with
|
||||
`BOX 1, X#, Y#, X# + 24, Y# + 10` is registered with the same four expressions.
|
||||
|
||||
The bare form retiring everything is what a level change wants, and it follows `TRAP`,
|
||||
`COLLISION` and `DCLOSE`, which all read absence as "off".
|
||||
|
||||
**`COLLISION 2` is how you hear about it.** On a C128 type 2 is a sprite against the set
|
||||
pixels of the bitmap screen; here it is a sprite against the rectangles `SOLID` registered,
|
||||
which is the same question in a form this interpreter can answer. `BUMP(2)` is its mask,
|
||||
and it is a **separate** accumulator from `BUMP(1)` — a sprite hitting a wall never sets a
|
||||
bit in the sprite-to-sprite mask, so a program that only cares about one of them is not
|
||||
disturbed by the other.
|
||||
|
||||
```basic norun
|
||||
10 COLLISION 2, WALL
|
||||
20 FOR I# = 1 TO 60
|
||||
30 SOLID I#, BX#(I#), BY#(I#), BX#(I#) + 24, BY#(I#) + 10
|
||||
40 NEXT I#
|
||||
...
|
||||
100 LABEL WALL
|
||||
110 M# = BUMP(2)
|
||||
120 IF (M# AND 1) = 0 THEN RETURN
|
||||
130 REM ... the ball hit something ...
|
||||
140 RETURN
|
||||
```
|
||||
|
||||
Sixty-four is the ceiling, and it is a real budget rather than a round number: these come
|
||||
out of a pool of collision proxies shared with whatever game this interpreter is embedded
|
||||
in, and eight sprites plus sixty-four rectangles is most of akbasic's share of it. The
|
||||
sixty-fifth is refused by name.
|
||||
|
||||
`NEW` retires them all. A rectangle is invisible, so one left behind by a deleted program
|
||||
would be an unexplainable collision in the next; `CLR` leaves them alone, because it clears
|
||||
variables and a rectangle is not one.
|
||||
|
||||
## Reading state back
|
||||
|
||||
| Function | Gives |
|
||||
|
||||
@@ -22,7 +22,7 @@ for the reasoning in each case.
|
||||
| `CIRCLE` | `CIRCLE src, x, y, rx, ry [,...]` | Draw an ellipse, arc or polygon. |
|
||||
| `CLR` | `CLR` | Drop every variable and function, keeping the program. |
|
||||
| `COLLECT` | `COLLECT` | **Refused.** Validates a disk's block allocation map. |
|
||||
| `COLLISION` | `COLLISION 1 [,target]` | Call a subroutine when sprites collide. No target disarms it. |
|
||||
| `COLLISION` | `COLLISION type [,target]` | Call a subroutine when sprites collide. No target disarms it. |
|
||||
| `COLOR` | `COLOR src, index` | Bind a colour source to a palette index, 1 to 16. |
|
||||
| `CONCAT` | `CONCAT "a", "b"` | Append file `a` to file `b`. |
|
||||
| `CONT` | `CONT` | Resume a program stopped by `STOP`. |
|
||||
@@ -90,6 +90,7 @@ for the reasoning in each case.
|
||||
| `SCNCLR` | `SCNCLR` | Clear the text screen. |
|
||||
| `SCRATCH` | `SCRATCH "name"` | Delete a file. |
|
||||
| `SLEEP` | `SLEEP seconds` | Pause. Holds the program, not the host. |
|
||||
| `SOLID` | `SOLID [id [,x1, y1, x2, y2]]` | Register static collision geometry a sprite can hit. No rectangle retires it; no arguments retires them all. See Chapter 8. |
|
||||
| `SOUND` | `SOUND v, freq, dur [,...]` | Play a tone on a voice. Does not block. |
|
||||
| `SPRCOLOR` | `SPRCOLOR [c1] [,c2]` | Set the two shared multicolour registers. |
|
||||
| `SPRHIT` | `SPRHIT n, kind [,x1, y1, x2, y2]` | Give a sprite a collision shape. Kind 0 none, 1 box, 2 circle, 3 or 4 capsule. No rectangle fits the frame. See Chapter 8. |
|
||||
|
||||
@@ -177,7 +177,12 @@ interpreter's error code, which bears no relation to a Commodore error number. P
|
||||
narrows that to a box, a circle or a capsule. None of them is pixel-exact.
|
||||
- **`SPRHIT` and `RSPHIT` are an addition.** BASIC 7.0 has `COLLISION` and `BUMP` and
|
||||
nothing else, and nothing about them changes an existing program.
|
||||
- **Only collision type 1 exists.** Types 2 and 3 are refused by name.
|
||||
- **Collision types 1 and 2 exist.** Type 2 means the rectangles `SOLID` registered, not a
|
||||
screen read back — a C128 collides a sprite against the bitmap's set pixels and this
|
||||
cannot, so the question is asked against geometry instead. Type 3 is still refused;
|
||||
there is no light pen.
|
||||
- **`SOLID` is an addition.** BASIC 7.0 has no static collision geometry at all, and it is
|
||||
what lets a program collide with something that is not one of the eight sprites.
|
||||
- **Priority and multicolour are recorded but not drawn.**
|
||||
- **`SPRDEF` is out of scope.**
|
||||
|
||||
|
||||
@@ -288,6 +288,51 @@ typedef struct
|
||||
float32_t shapex2[AKBASIC_MAX_SPRITES];
|
||||
float32_t shapey2[AKBASIC_MAX_SPRITES];
|
||||
bool shapeexplicit[AKBASIC_MAX_SPRITES];
|
||||
|
||||
/**
|
||||
* SOLID's static geometry: a proxy and a shape per registered rectangle.
|
||||
*
|
||||
* **Not registered with any partitioner, and there is no
|
||||
* `akgl_CollisionWorld` here at all.** libakgl's uniform grid keeps its cell
|
||||
* heads, its cell size and its origin in file-scope statics
|
||||
* (`deps/libakgl/src/collision_grid.c`), so it is one index per process --
|
||||
* and `akgl_collision_world_init()` ends in a `reset()` that memsets those
|
||||
* heads *and* calls `akgl_heap_init_collision_cells()`. An interpreter
|
||||
* embedded in a game that has its own collision world would destroy every
|
||||
* registration that game had made, on the first `SOLID` a script ran.
|
||||
*
|
||||
* So the geometry is indexed here, by an ordinary array, and pairs go
|
||||
* straight to `akgl_collision_test()` -- which takes two positioned proxies
|
||||
* and needs no world. At sixty-four rectangles that is the right answer
|
||||
* anyway: libakgl's own measurements put a naive all-pairs sweep at 0.7% of
|
||||
* a frame at sixty-four objects, and a broad phase is bookkeeping that only
|
||||
* repays itself well above that.
|
||||
*/
|
||||
akgl_CollisionProxy *solidproxies[AKBASIC_MAX_SOLIDS];
|
||||
akgl_CollisionShape solidshapes[AKBASIC_MAX_SOLIDS];
|
||||
SDL_FRect solidbox[AKBASIC_MAX_SOLIDS];
|
||||
bool solidactive[AKBASIC_MAX_SOLIDS];
|
||||
|
||||
/**
|
||||
* What the last scan saw, so an unchanged scan can answer without redoing
|
||||
* the work.
|
||||
*
|
||||
* The scan runs at the top of every interpreter step -- up to 256 times a
|
||||
* rendered frame -- and its inputs are the sprites' boxes, which sprite
|
||||
* slots are collidable, and the static geometry. A sprite moves at most once
|
||||
* in that time. If none of those changed, the answer cannot have changed
|
||||
* either, and eight rectangle comparisons say so far more cheaply than
|
||||
* recomputing it.
|
||||
*
|
||||
* This is what makes static geometry affordable at all: eight sprites
|
||||
* against sixty-four rectangles is five hundred and twelve tests, which is
|
||||
* fine once a frame and is not fine two hundred and fifty-six times.
|
||||
*/
|
||||
SDL_FRect lastbox[AKBASIC_MAX_SPRITES];
|
||||
bool lastlive[AKBASIC_MAX_SPRITES];
|
||||
uint16_t lastmask;
|
||||
uint16_t lastsolidmask;
|
||||
bool lastvalid;
|
||||
} akbasic_AkglSprites;
|
||||
|
||||
/**
|
||||
|
||||
@@ -52,6 +52,19 @@
|
||||
/** @brief One past the last kind, for the range check. */
|
||||
#define AKBASIC_SHAPE_LAST 5
|
||||
|
||||
/**
|
||||
* @brief Static collision rectangles a program can register with SOLID.
|
||||
*
|
||||
* Sixty-four, which is the smallest power of two that holds a sixty-brick wall
|
||||
* with headroom. The ceiling matters because these come out of libakgl's
|
||||
* collision proxy pool, which is shared with whatever host this interpreter is
|
||||
* embedded in: eight sprites plus sixty-four solids is seventy-two of
|
||||
* #AKGL_MAX_HEAP_COLLISION_PROXY, and the rest is the host's.
|
||||
*/
|
||||
#ifndef AKBASIC_MAX_SOLIDS
|
||||
#define AKBASIC_MAX_SOLIDS 64
|
||||
#endif
|
||||
|
||||
/** @brief Width of a Commodore sprite in pixels. */
|
||||
#define AKBASIC_SPRITE_WIDTH 24
|
||||
/** @brief Height of a Commodore sprite in pixels. */
|
||||
@@ -121,6 +134,23 @@ typedef struct
|
||||
bool shapeexplicit;
|
||||
} akbasic_Sprite;
|
||||
|
||||
/**
|
||||
* @brief One rectangle registered by SOLID, as BASIC sees it.
|
||||
*
|
||||
* The id is the array index plus one and is the program's own number, not a
|
||||
* minted handle. A brick in a sixty-brick wall has a natural index; making the
|
||||
* program carry one back out of the interpreter would be inventing a second
|
||||
* numbering for something that already has one.
|
||||
*/
|
||||
typedef struct
|
||||
{
|
||||
bool active;
|
||||
double x1;
|
||||
double y1;
|
||||
double x2;
|
||||
double y2;
|
||||
} akbasic_Solid;
|
||||
|
||||
/**
|
||||
* @brief The sprite verbs' own state, which lives on the runtime.
|
||||
*
|
||||
@@ -133,9 +163,11 @@ typedef struct
|
||||
typedef struct
|
||||
{
|
||||
akbasic_Sprite sprites[AKBASIC_MAX_SPRITES];
|
||||
akbasic_Solid solids[AKBASIC_MAX_SOLIDS];
|
||||
int sharedcolor1; /* SPRCOLOR's two multicolour registers, 1-16 */
|
||||
int sharedcolor2;
|
||||
uint16_t bumped; /* bit n-1 set when sprite n has collided */
|
||||
uint16_t bumpedsolid; /* and when it has met static geometry */
|
||||
int64_t lastservicems; /* when MOVSPR's continuous motion last moved */
|
||||
} akbasic_SpriteState;
|
||||
|
||||
@@ -226,6 +258,30 @@ typedef struct akbasic_SpriteBackend
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*shape)(struct akbasic_SpriteBackend *self, int n, int kind,
|
||||
double x1, double y1, double x2, double y2);
|
||||
/**
|
||||
* Register, replace or retire static collision geometry @p id.
|
||||
*
|
||||
* @p define false retires it and the rectangle is ignored. Rectangles are in
|
||||
* window pixels -- the coordinates `BOX` draws in and `MOVSPR` positions in,
|
||||
* with `SCALE` not applying, exactly as for a sprite.
|
||||
*
|
||||
* Optional, like `shape`. A backend that withholds it makes `SOLID` refuse
|
||||
* by name.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*solid)(struct akbasic_SpriteBackend *self, int id, bool define,
|
||||
double x1, double y1, double x2, double y2);
|
||||
/**
|
||||
* Report which sprites are overlapping static geometry.
|
||||
*
|
||||
* Bit n-1 is set when sprite n overlaps at least one rectangle `solid`
|
||||
* registered. Answered from the same pass as `collisions`, so the two are
|
||||
* always about the same instant whichever order they are called in.
|
||||
*
|
||||
* Optional. A runtime whose backend withholds it reports no static
|
||||
* collisions rather than refusing, exactly as one with no backend at all
|
||||
* reports none -- a step is not a statement and has no line to blame.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*solids)(struct akbasic_SpriteBackend *self, uint16_t *mask);
|
||||
} akbasic_SpriteBackend;
|
||||
|
||||
/**
|
||||
|
||||
@@ -123,6 +123,18 @@ akerr_ErrorContext *akbasic_cmd_new(akbasic_Runtime *obj, akbasic_ASTLeaf *expr,
|
||||
PASS(errctx, obj->sprites->show(obj->sprites, (int)i + 1, false));
|
||||
}
|
||||
}
|
||||
/*
|
||||
* **Static geometry does go back to the device**, where a sprite pattern
|
||||
* cannot. There is an entry point that retires one, so leaving them would be
|
||||
* a choice rather than a limitation -- and the wrong choice: a wall is
|
||||
* invisible, so one left behind by a deleted program is an unexplainable
|
||||
* collision in the next.
|
||||
*/
|
||||
if ( obj->sprites != NULL && obj->sprites->solid != NULL ) {
|
||||
for ( i = 0; i < AKBASIC_MAX_SOLIDS; i++ ) {
|
||||
PASS(errctx, obj->sprites->solid(obj->sprites, (int)i + 1, false, 0.0, 0.0, 0.0, 0.0));
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* The line counter goes home too. Without this a NEW typed at the REPL
|
||||
|
||||
@@ -498,6 +498,7 @@ akerr_ErrorContext *akbasic_cmd_collision(akbasic_Runtime *obj, akbasic_ASTLeaf
|
||||
akbasic_ASTLeaf *handler = NULL;
|
||||
akbasic_Value *value = NULL;
|
||||
int type = 0;
|
||||
int source = AKBASIC_INTERRUPT_SPRITE;
|
||||
|
||||
(void)lval; (void)rval;
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
||||
@@ -513,22 +514,23 @@ akerr_ErrorContext *akbasic_cmd_collision(akbasic_Runtime *obj, akbasic_ASTLeaf
|
||||
FAIL_ZERO_RETURN(errctx, (type >= 1 && type <= 3), AKBASIC_ERR_BOUNDS,
|
||||
"COLLISION: type %d is outside 1..3", type);
|
||||
/*
|
||||
* Type 1 only. Sprite-to-background would need the whole render target read
|
||||
* back and compared against each sprite every frame, and a light pen has no
|
||||
* meaning on a machine with no light pen; both are refused by name rather
|
||||
* than silently accepted and never fired. TODO.md section 5.
|
||||
* Types 1 and 2 both exist now. A C128's type 2 is a sprite against the set
|
||||
* pixels of the bitmap screen, which was refused here because reading the
|
||||
* render target back every frame is not affordable -- but SOLID gives this
|
||||
* interpreter a background made of rectangles instead of pixels, and that is
|
||||
* the same question asked in a form it can answer. The same move SPRSAV made
|
||||
* when it learned to take an image path.
|
||||
*
|
||||
* Type 3 stays refused. There is still no light pen.
|
||||
*/
|
||||
FAIL_ZERO_RETURN(errctx, (type == 1), AKBASIC_ERR_DEVICE,
|
||||
"COLLISION type %d is not implemented: %s",
|
||||
type,
|
||||
(type == 2
|
||||
? "sprite-to-background collision needs the screen read back every frame"
|
||||
: "there is no light pen"));
|
||||
FAIL_ZERO_RETURN(errctx, (type == 1 || type == 2), AKBASIC_ERR_DEVICE,
|
||||
"COLLISION type %d is not implemented: there is no light pen", type);
|
||||
source = (type == 2 ? AKBASIC_INTERRUPT_BACKGROUND : AKBASIC_INTERRUPT_SPRITE);
|
||||
|
||||
handler = arg->next;
|
||||
if ( handler == NULL ) {
|
||||
/* No target disarms it, which is how a C128 turns a handler off. */
|
||||
PASS(errctx, akbasic_runtime_disarm_interrupt(obj, AKBASIC_INTERRUPT_SPRITE));
|
||||
PASS(errctx, akbasic_runtime_disarm_interrupt(obj, source));
|
||||
SUCCEED_TRUE(obj, dest);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
@@ -544,7 +546,7 @@ akerr_ErrorContext *akbasic_cmd_collision(akbasic_Runtime *obj, akbasic_ASTLeaf
|
||||
*/
|
||||
if ( handler->leaftype == AKBASIC_LEAF_IDENTIFIER &&
|
||||
akbasic_leaf_first_subscript(handler) == NULL ) {
|
||||
PASS(errctx, akbasic_runtime_arm_interrupt(obj, AKBASIC_INTERRUPT_SPRITE,
|
||||
PASS(errctx, akbasic_runtime_arm_interrupt(obj, source,
|
||||
0, handler->identifier));
|
||||
SUCCEED_TRUE(obj, dest);
|
||||
SUCCEED_RETURN(errctx);
|
||||
@@ -553,7 +555,7 @@ akerr_ErrorContext *akbasic_cmd_collision(akbasic_Runtime *obj, akbasic_ASTLeaf
|
||||
PASS(errctx, akbasic_runtime_evaluate(obj, handler, &value));
|
||||
FAIL_NONZERO_RETURN(errctx, (value->valuetype == AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
|
||||
"COLLISION expected a line number or a label");
|
||||
PASS(errctx, akbasic_runtime_arm_interrupt(obj, AKBASIC_INTERRUPT_SPRITE,
|
||||
PASS(errctx, akbasic_runtime_arm_interrupt(obj, source,
|
||||
value->intval, NULL));
|
||||
SUCCEED_TRUE(obj, dest);
|
||||
SUCCEED_RETURN(errctx);
|
||||
@@ -563,6 +565,7 @@ akerr_ErrorContext *akbasic_collision_service(akbasic_Runtime *obj)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
uint16_t mask = 0;
|
||||
uint16_t solidmask = 0;
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER,
|
||||
"NULL runtime in collision_service");
|
||||
@@ -570,6 +573,19 @@ akerr_ErrorContext *akbasic_collision_service(akbasic_Runtime *obj)
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
PASS(errctx, obj->sprites->collisions(obj->sprites, &mask));
|
||||
/*
|
||||
* Static geometry is a second question with a second accumulator and a
|
||||
* second handler, answered from the same scan the device just ran. A backend
|
||||
* that cannot do it withholds the entry point and reports nothing, the same
|
||||
* way a runtime with no backend at all does.
|
||||
*/
|
||||
if ( obj->sprites->solids != NULL ) {
|
||||
PASS(errctx, obj->sprites->solids(obj->sprites, &solidmask));
|
||||
}
|
||||
if ( solidmask != 0 ) {
|
||||
obj->sprite_state.bumpedsolid |= solidmask;
|
||||
PASS(errctx, akbasic_runtime_raise_interrupt(obj, AKBASIC_INTERRUPT_BACKGROUND));
|
||||
}
|
||||
if ( mask == 0 ) {
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
@@ -644,8 +660,8 @@ akerr_ErrorContext *akbasic_fn_bump(akbasic_Runtime *obj, akbasic_ASTLeaf *expr,
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
||||
"NULL argument in BUMP");
|
||||
PASS(errctx, nth_number(obj, expr, "BUMP", 0, &type));
|
||||
FAIL_ZERO_RETURN(errctx, (type == 1), AKBASIC_ERR_DEVICE,
|
||||
"BUMP type %" PRId64 " is not implemented; only sprite-to-sprite collision is",
|
||||
FAIL_ZERO_RETURN(errctx, (type == 1 || type == 2), AKBASIC_ERR_DEVICE,
|
||||
"BUMP type %" PRId64 " is not implemented; sprite-to-sprite is 1 and static geometry is 2",
|
||||
type);
|
||||
|
||||
/*
|
||||
@@ -653,8 +669,13 @@ akerr_ErrorContext *akbasic_fn_bump(akbasic_Runtime *obj, akbasic_ASTLeaf *expr,
|
||||
* answerable at all: without it a program that polls in a loop would see the
|
||||
* same collision forever.
|
||||
*/
|
||||
reported = obj->sprite_state.bumped;
|
||||
obj->sprite_state.bumped = 0;
|
||||
if ( type == 2 ) {
|
||||
reported = obj->sprite_state.bumpedsolid;
|
||||
obj->sprite_state.bumpedsolid = 0;
|
||||
} else {
|
||||
reported = obj->sprite_state.bumped;
|
||||
obj->sprite_state.bumped = 0;
|
||||
}
|
||||
PASS(errctx, integer_result(obj, (int64_t)reported, dest));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
@@ -706,6 +727,68 @@ static akerr_ErrorContext *float_result(akbasic_Runtime *obj, double value, akba
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------- SOLID -- */
|
||||
|
||||
akerr_ErrorContext *akbasic_cmd_solid(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_Solid *solid = NULL;
|
||||
double args[5];
|
||||
int count = 0;
|
||||
int id = 0;
|
||||
int i = 0;
|
||||
|
||||
(void)lval; (void)rval;
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
||||
"NULL argument in SOLID");
|
||||
PASS(errctx, require_sprites(obj, "SOLID"));
|
||||
FAIL_ZERO_RETURN(errctx, (obj->sprites->solid != NULL), AKBASIC_ERR_DEVICE,
|
||||
"SOLID needs a sprite device that can carry static collision geometry, and this one cannot");
|
||||
PASS(errctx, akbasic_args_numbers(obj, expr, "SOLID", args, 5, &count));
|
||||
FAIL_ZERO_RETURN(errctx, (count == 0 || count == 1 || count == 5), AKBASIC_ERR_SYNTAX,
|
||||
"SOLID takes nothing, an id, or an id and four corners, not %d arguments",
|
||||
count);
|
||||
|
||||
/*
|
||||
* No arguments clears the lot, which is how `TRAP`, `COLLISION` and `DCLOSE`
|
||||
* all read absence -- and is the verb a level change wants.
|
||||
*/
|
||||
if ( count == 0 ) {
|
||||
for ( i = 0; i < AKBASIC_MAX_SOLIDS; i++ ) {
|
||||
if ( !obj->sprite_state.solids[i].active ) {
|
||||
continue;
|
||||
}
|
||||
memset(&obj->sprite_state.solids[i], 0, sizeof(obj->sprite_state.solids[i]));
|
||||
/* PASS rather than CATCH: this is a loop. */
|
||||
PASS(errctx, obj->sprites->solid(obj->sprites, i + 1, false, 0.0, 0.0, 0.0, 0.0));
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
id = (int)args[0];
|
||||
FAIL_ZERO_RETURN(errctx, (id >= 1 && id <= AKBASIC_MAX_SOLIDS), AKBASIC_ERR_BOUNDS,
|
||||
"SOLID: %d is outside 1..%d", id, AKBASIC_MAX_SOLIDS);
|
||||
solid = &obj->sprite_state.solids[id - 1];
|
||||
|
||||
if ( count == 1 ) {
|
||||
memset(solid, 0, sizeof(*solid));
|
||||
PASS(errctx, obj->sprites->solid(obj->sprites, id, false, 0.0, 0.0, 0.0, 0.0));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (args[3] > args[1] && args[4] > args[2]), AKBASIC_ERR_VALUE,
|
||||
"SOLID: (%g, %g) to (%g, %g) is not a rectangle with a positive width and height",
|
||||
args[1], args[2], args[3], args[4]);
|
||||
solid->active = true;
|
||||
solid->x1 = args[1];
|
||||
solid->y1 = args[2];
|
||||
solid->x2 = args[3];
|
||||
solid->y2 = args[4];
|
||||
PASS(errctx, obj->sprites->solid(obj->sprites, id, true,
|
||||
solid->x1, solid->y1, solid->x2, solid->y2));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- SPRHIT -- */
|
||||
|
||||
akerr_ErrorContext *akbasic_cmd_sprhit(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||||
|
||||
@@ -625,10 +625,18 @@ static akerr_ErrorContext AKERR_NOIGNORE *sync_proxy(akbasic_AkglSprites *state,
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *spr_collisions(akbasic_SpriteBackend *self, uint16_t *mask)
|
||||
/**
|
||||
* @brief Run the scan if anything has changed, and leave both masks on the state.
|
||||
*
|
||||
* One worker behind two entry points, because both masks come out of the same
|
||||
* pass and computing them separately would be computing the sprite boxes twice
|
||||
* for one answer. `spr_collisions()` reports the sprite-against-sprite mask and
|
||||
* `spr_solids()` the sprite-against-static one; either may be called first, and
|
||||
* the cache below is what makes that true rather than merely usually true.
|
||||
*/
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *run_scan(akbasic_AkglSprites *state)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_AkglSprites *state = NULL;
|
||||
akgl_Contact contact;
|
||||
SDL_FRect box[AKBASIC_MAX_SPRITES];
|
||||
bool live[AKBASIC_MAX_SPRITES];
|
||||
@@ -637,19 +645,52 @@ static akerr_ErrorContext *spr_collisions(akbasic_SpriteBackend *self, uint16_t
|
||||
int i = 0;
|
||||
int j = 0;
|
||||
|
||||
PASS(errctx, state_of(self, &state));
|
||||
FAIL_ZERO_RETURN(errctx, (mask != NULL), AKERR_NULLPOINTER, "NULL mask in collisions");
|
||||
*mask = 0;
|
||||
|
||||
for ( i = 0; i < AKBASIC_MAX_SPRITES; i++ ) {
|
||||
live[i] = slot_collidable(state, i);
|
||||
synced[i] = false;
|
||||
memset(&box[i], 0, sizeof(box[i]));
|
||||
if ( !live[i] ) {
|
||||
continue;
|
||||
}
|
||||
collision_box(state, i, &box[i]);
|
||||
}
|
||||
|
||||
/*
|
||||
* **Nothing moved, so nothing can have changed.**
|
||||
*
|
||||
* The scan's inputs are exactly these boxes and which slots are collidable;
|
||||
* static geometry invalidates the cache where it is registered. So if all
|
||||
* eight match what the last scan saw, the answer is the answer it gave, and
|
||||
* eight rectangle comparisons are far cheaper than recomputing it.
|
||||
*
|
||||
* This is what makes static geometry affordable. The scan runs at the top of
|
||||
* every interpreter step -- up to 256 times a rendered frame -- and eight
|
||||
* sprites against sixty-four rectangles is five hundred and twelve tests.
|
||||
* That is fine once a frame and ruinous 256 times, and a sprite moves at
|
||||
* most once in that window.
|
||||
*
|
||||
* It changes nothing a program can observe: the same mask comes back, so
|
||||
* BUMP accumulates the same bits and the interrupt is raised on the same
|
||||
* steps.
|
||||
*/
|
||||
if ( state->lastvalid ) {
|
||||
bool same = true;
|
||||
|
||||
for ( i = 0; i < AKBASIC_MAX_SPRITES; i++ ) {
|
||||
if ( live[i] != state->lastlive[i]
|
||||
|| box[i].x != state->lastbox[i].x || box[i].y != state->lastbox[i].y
|
||||
|| box[i].w != state->lastbox[i].w || box[i].h != state->lastbox[i].h ) {
|
||||
same = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ( same ) {
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
}
|
||||
state->lastmask = 0;
|
||||
state->lastsolidmask = 0;
|
||||
|
||||
/*
|
||||
* **Two phases, and the split is the whole performance story.**
|
||||
*
|
||||
@@ -713,11 +754,143 @@ static akerr_ErrorContext *spr_collisions(akbasic_SpriteBackend *self, uint16_t
|
||||
PASS(errctx, akgl_collision_test(state->proxies[i], state->proxies[j],
|
||||
AKGL_COLLISION_TEST_PLANAR, &contact, &hit));
|
||||
if ( hit ) {
|
||||
*mask |= (uint16_t)(1u << i);
|
||||
*mask |= (uint16_t)(1u << j);
|
||||
state->lastmask |= (uint16_t)(1u << i);
|
||||
state->lastmask |= (uint16_t)(1u << j);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Every live sprite against every registered rectangle, same shape: reject
|
||||
* on the boxes, and only then ask the library. A wall is motionless, so its
|
||||
* proxy was synced when SOLID registered it and there is nothing to refresh
|
||||
* here.
|
||||
*/
|
||||
for ( i = 0; i < AKBASIC_MAX_SPRITES; i++ ) {
|
||||
if ( !live[i] ) {
|
||||
continue;
|
||||
}
|
||||
for ( j = 0; j < AKBASIC_MAX_SOLIDS; j++ ) {
|
||||
if ( !state->solidactive[j] ) {
|
||||
continue;
|
||||
}
|
||||
if ( !(box[i].x < state->solidbox[j].x + state->solidbox[j].w
|
||||
&& state->solidbox[j].x < box[i].x + box[i].w
|
||||
&& box[i].y < state->solidbox[j].y + state->solidbox[j].h
|
||||
&& state->solidbox[j].y < box[i].y + box[i].h) ) {
|
||||
continue;
|
||||
}
|
||||
if ( !synced[i] ) {
|
||||
PASS(errctx, sync_proxy(state, i, &box[i]));
|
||||
synced[i] = true;
|
||||
}
|
||||
hit = false;
|
||||
PASS(errctx, akgl_collision_test(state->proxies[i], state->solidproxies[j],
|
||||
AKGL_COLLISION_TEST_PLANAR, &contact, &hit));
|
||||
if ( hit ) {
|
||||
state->lastsolidmask |= (uint16_t)(1u << i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for ( i = 0; i < AKBASIC_MAX_SPRITES; i++ ) {
|
||||
state->lastbox[i] = box[i];
|
||||
state->lastlive[i] = live[i];
|
||||
}
|
||||
state->lastvalid = true;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Register, replace or retire static rectangle @p id.
|
||||
*
|
||||
* A proxy with a **NULL owner**: there is no actor behind a wall, and libakgl's
|
||||
* proxy carries the owner only so a resolver can push something. Nothing here
|
||||
* resolves anything, so the field stays empty and the shape is what matters.
|
||||
*
|
||||
* Layers are the other half. A wall sits on #AKGL_COLLISION_LAYER_STATIC and
|
||||
* responds to nothing, which is the asymmetry libakgl's masks exist for: the
|
||||
* sprite's own `collidemask` includes STATIC, so a sprite finds a wall and two
|
||||
* walls never test against each other. Sixty-four motionless rectangles
|
||||
* therefore cost nothing per scan beyond the boxes a sprite is compared to.
|
||||
*/
|
||||
static akerr_ErrorContext *spr_solid(akbasic_SpriteBackend *self, int id, bool define,
|
||||
double x1, double y1, double x2, double y2)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_AkglSprites *state = NULL;
|
||||
SDL_FRect body;
|
||||
int i = 0;
|
||||
|
||||
PASS(errctx, state_of(self, &state));
|
||||
FAIL_ZERO_RETURN(errctx, (id >= 1 && id <= AKBASIC_MAX_SOLIDS), AKBASIC_ERR_BOUNDS,
|
||||
"Static shape %d is outside 1..%d", id, AKBASIC_MAX_SOLIDS);
|
||||
i = id - 1;
|
||||
|
||||
/*
|
||||
* Anything that changes the geometry invalidates the cached answer. Cheapest
|
||||
* possible correctness: the scan's own change detection watches the sprites,
|
||||
* and this is the one input it cannot see moving.
|
||||
*/
|
||||
state->lastvalid = false;
|
||||
|
||||
if ( !define ) {
|
||||
if ( state->solidproxies[i] != NULL ) {
|
||||
PASS(errctx, akgl_heap_release_collision_proxy(state->solidproxies[i]));
|
||||
state->solidproxies[i] = NULL;
|
||||
}
|
||||
state->solidactive[i] = false;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
body.x = 0.0f;
|
||||
body.y = 0.0f;
|
||||
body.w = (float32_t)(x2 - x1);
|
||||
body.h = (float32_t)(y2 - y1);
|
||||
PASS(errctx, akgl_collision_shape_box(&state->solidshapes[i], &body, 0.0f));
|
||||
state->solidshapes[i].flags |= AKGL_COLLISION_FLAG_STATIC;
|
||||
state->solidshapes[i].layermask = AKGL_COLLISION_LAYER_STATIC;
|
||||
state->solidshapes[i].collidemask = AKGL_COLLISION_LAYER_NONE;
|
||||
|
||||
if ( state->solidproxies[i] == NULL ) {
|
||||
/* Adjacent, with nothing between: the slot is free until initialize takes it. */
|
||||
PASS(errctx, akgl_heap_next_collision_proxy(&state->solidproxies[i]));
|
||||
PASS(errctx, akgl_collision_proxy_initialize(state->solidproxies[i], NULL,
|
||||
&state->solidshapes[i],
|
||||
(float32_t)x1, (float32_t)y1, 0.0f));
|
||||
} else {
|
||||
PASS(errctx, akgl_collision_proxy_sync(state->solidproxies[i], &state->solidshapes[i],
|
||||
(float32_t)x1, (float32_t)y1, 0.0f));
|
||||
}
|
||||
state->solidbox[i].x = (float32_t)x1;
|
||||
state->solidbox[i].y = (float32_t)y1;
|
||||
state->solidbox[i].w = body.w;
|
||||
state->solidbox[i].h = body.h;
|
||||
state->solidactive[i] = true;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *spr_collisions(akbasic_SpriteBackend *self, uint16_t *mask)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_AkglSprites *state = NULL;
|
||||
|
||||
PASS(errctx, state_of(self, &state));
|
||||
FAIL_ZERO_RETURN(errctx, (mask != NULL), AKERR_NULLPOINTER, "NULL mask in collisions");
|
||||
PASS(errctx, run_scan(state));
|
||||
*mask = state->lastmask;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *spr_solids(akbasic_SpriteBackend *self, uint16_t *mask)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_AkglSprites *state = NULL;
|
||||
|
||||
PASS(errctx, state_of(self, &state));
|
||||
FAIL_ZERO_RETURN(errctx, (mask != NULL), AKERR_NULLPOINTER, "NULL mask in solids");
|
||||
PASS(errctx, run_scan(state));
|
||||
*mask = state->lastsolidmask;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
@@ -784,6 +957,8 @@ akerr_ErrorContext *akbasic_sprite_init_akgl(akbasic_SpriteBackend *obj, akbasic
|
||||
obj->shared_colors = spr_shared_colors;
|
||||
obj->collisions = spr_collisions;
|
||||
obj->shape = spr_shape;
|
||||
obj->solid = spr_solid;
|
||||
obj->solids = spr_solids;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
@@ -831,4 +1006,10 @@ void akbasic_sprite_akgl_shutdown(akbasic_SpriteBackend *obj)
|
||||
state->proxies[i] = NULL;
|
||||
}
|
||||
}
|
||||
for ( i = 0; i < AKBASIC_MAX_SOLIDS; i++ ) {
|
||||
if ( state->solidproxies[i] != NULL ) {
|
||||
IGNORE(akgl_heap_release_collision_proxy(state->solidproxies[i]));
|
||||
state->solidproxies[i] = NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,6 +154,7 @@ static const akbasic_Verb VERBS[] = {
|
||||
{ "SHR", AKBASIC_TOK_FUNCTION, 2, NULL, akbasic_fn_shr },
|
||||
{ "SIN", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_sin },
|
||||
{ "SLEEP", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_sleep },
|
||||
{ "SOLID", AKBASIC_TOK_COMMAND, -1, akbasic_parse_optional_arglist, akbasic_cmd_solid },
|
||||
{ "SOUND", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_sound },
|
||||
{ "SPC", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_spc },
|
||||
{ "SPRCOLOR", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_sprcolor },
|
||||
|
||||
@@ -90,6 +90,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_on(struct akbasic_Runtime *obj, a
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_collision(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_movspr(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_sprcolor(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_solid(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_sprhit(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_sprite(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_sprsav(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||
|
||||
@@ -844,6 +844,99 @@ static akerr_ErrorContext AKERR_NOIGNORE *test_sprite_shapes(void)
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/** @brief Run a program and report the static-geometry mask. */
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *solidmask_for(const char *source, uint16_t *dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
PASS(errctx, start_runtime(source));
|
||||
TEST_REQUIRE_STR(OUTPUT, "");
|
||||
PASS(errctx, SPRITES.solids(&SPRITES, dest));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief SOLID puts a collidable rectangle on the field without spending a sprite.
|
||||
*
|
||||
* This is the point of the whole exercise. A brick is not a sprite -- there are
|
||||
* eight and a wall wants sixty -- so until now a program could only collide with
|
||||
* one by doing the arithmetic itself against its own array. Now it registers a
|
||||
* rectangle and the interpreter answers.
|
||||
*/
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *test_static_geometry(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
uint16_t mask = 0;
|
||||
|
||||
/* A 24x21 sprite at (100, 100) squarely inside a rectangle. */
|
||||
PASS(errctx, solidmask_for("10 DIM P#(63)\n"
|
||||
"20 FOR I# = 0 TO 62\n"
|
||||
"30 P#(I#) = 255\n"
|
||||
"40 NEXT I#\n"
|
||||
"50 SPRSAV P#, 1\n"
|
||||
"60 SPRITE 1, 1\n"
|
||||
"70 SOLID 7, 90, 90, 140, 140\n"
|
||||
"80 MOVSPR 1, 100, 100\n", &mask));
|
||||
TEST_REQUIRE_INT(mask, 0x01);
|
||||
stop_runtime();
|
||||
|
||||
/* Moved clear of it, nothing. */
|
||||
PASS(errctx, solidmask_for("10 DIM P#(63)\n"
|
||||
"20 FOR I# = 0 TO 62\n"
|
||||
"30 P#(I#) = 255\n"
|
||||
"40 NEXT I#\n"
|
||||
"50 SPRSAV P#, 1\n"
|
||||
"60 SPRITE 1, 1\n"
|
||||
"70 SOLID 7, 90, 90, 140, 140\n"
|
||||
"80 MOVSPR 1, 200, 200\n", &mask));
|
||||
TEST_REQUIRE_INT(mask, 0);
|
||||
stop_runtime();
|
||||
|
||||
/*
|
||||
* Retired, and the sprite has not moved. This is the assertion that catches
|
||||
* a proxy released without its registration going with it -- the shape would
|
||||
* still be found and the mask would still come back set.
|
||||
*/
|
||||
PASS(errctx, solidmask_for("10 DIM P#(63)\n"
|
||||
"20 FOR I# = 0 TO 62\n"
|
||||
"30 P#(I#) = 255\n"
|
||||
"40 NEXT I#\n"
|
||||
"50 SPRSAV P#, 1\n"
|
||||
"60 SPRITE 1, 1\n"
|
||||
"70 SOLID 7, 90, 90, 140, 140\n"
|
||||
"80 MOVSPR 1, 100, 100\n"
|
||||
"90 SOLID 7\n", &mask));
|
||||
TEST_REQUIRE_INT(mask, 0);
|
||||
stop_runtime();
|
||||
|
||||
/*
|
||||
* A full wall registered, walked into, and cleared -- sixty-four of them, so
|
||||
* the proxy budget is exercised at its ceiling and the pool has to come back
|
||||
* intact afterwards or the next case fails.
|
||||
*/
|
||||
PASS(errctx, solidmask_for("10 DIM P#(63)\n"
|
||||
"20 FOR I# = 0 TO 62\n"
|
||||
"30 P#(I#) = 255\n"
|
||||
"40 NEXT I#\n"
|
||||
"50 SPRSAV P#, 1\n"
|
||||
"60 SPRITE 1, 1\n"
|
||||
"70 FOR I# = 1 TO 64\n"
|
||||
"80 X# = I# * 30\n"
|
||||
"90 SOLID I#, X#, 0, X# + 20, 20\n"
|
||||
"100 NEXT I#\n"
|
||||
"110 MOVSPR 1, 300, 5\n", &mask));
|
||||
TEST_REQUIRE_INT(mask, 0x01);
|
||||
stop_runtime();
|
||||
|
||||
/* And the sixty-fifth is refused by name rather than exhausting the pool. */
|
||||
PASS(errctx, start_runtime("10 SOLID 65, 0, 0, 10, 10\n"));
|
||||
TEST_REQUIRE(strstr(OUTPUT, "outside 1..64") != NULL,
|
||||
"SOLID 65 should be refused, got \"%s\"", OUTPUT);
|
||||
stop_runtime();
|
||||
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
@@ -913,6 +1006,7 @@ int main(void)
|
||||
CATCH(errctx, test_collision_pairs());
|
||||
CATCH(errctx, test_collision_cross_shape());
|
||||
CATCH(errctx, test_sprite_shapes());
|
||||
CATCH(errctx, test_static_geometry());
|
||||
} CLEANUP {
|
||||
if ( font != NULL ) {
|
||||
TTF_CloseFont(font);
|
||||
|
||||
@@ -121,6 +121,26 @@ static void sprite_program(char *dest, size_t size, int n, bool stacked)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Move sprite 1 a pixel, then scan. One full scan, no cache hit.
|
||||
*
|
||||
* The scan short-circuits when no sprite has moved since the last one, which is
|
||||
* what 255 of every 256 calls a frame do -- so a loop that only calls the scan
|
||||
* measures the short circuit and nothing else. Nudging a sprite first is what
|
||||
* makes a row mean "a scan that actually ran".
|
||||
*/
|
||||
static float32_t bench_nudge = 0.0f;
|
||||
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *move_and_scan(uint16_t *mask)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
bench_nudge = (bench_nudge > 0.5f ? 0.0f : 1.0f);
|
||||
PASS(errctx, SPRITES.move(&SPRITES, 1, 20.0 + (double)bench_nudge, 20.0));
|
||||
PASS(errctx, SPRITES.collisions(&SPRITES, mask));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The arrangement examples/breakout/sprites/breakout.bas actually has.
|
||||
*
|
||||
@@ -162,7 +182,17 @@ static akerr_ErrorContext AKERR_NOIGNORE *bench_breakout_shaped(void)
|
||||
"200 MOVSPR 7, 600, 400\n"
|
||||
"210 MOVSPR 8, 200, 150\n"));
|
||||
|
||||
bench_start("spr_collisions, breakout's own layout", "call", 0.0);
|
||||
bench_start("full scan, breakout's own layout", "call", 0.0);
|
||||
BENCH_LOOP(inner, i, iterations, move_and_scan(&mask));
|
||||
bench_stop((uint64_t)iterations);
|
||||
PASS(errctx, inner);
|
||||
|
||||
/*
|
||||
* And the other path, on the same population: what the other 255 calls a
|
||||
* frame cost once the first has done the work. This row is the reason the
|
||||
* per-step cadence is affordable at all.
|
||||
*/
|
||||
bench_start("cached scan, nothing moved", "call", 0.0);
|
||||
BENCH_LOOP(inner, i, iterations, SPRITES.collisions(&SPRITES, &mask));
|
||||
bench_stop((uint64_t)iterations);
|
||||
PASS(errctx, inner);
|
||||
@@ -184,10 +214,10 @@ static akerr_ErrorContext AKERR_NOIGNORE *bench_scan(int n, bool stacked)
|
||||
sprite_program(source, sizeof(source), n, stacked);
|
||||
PASS(errctx, load(source));
|
||||
|
||||
snprintf(name, sizeof(name), "spr_collisions, %d sprites, %s", n,
|
||||
snprintf(name, sizeof(name), "full scan, %d sprites, %s", n,
|
||||
(stacked ? "all overlapping" : "spread out"));
|
||||
bench_start(name, "call", 0.0);
|
||||
BENCH_LOOP(inner, i, iterations, SPRITES.collisions(&SPRITES, &mask));
|
||||
BENCH_LOOP(inner, i, iterations, move_and_scan(&mask));
|
||||
bench_stop((uint64_t)iterations);
|
||||
PASS(errctx, inner);
|
||||
SUCCEED_RETURN(errctx);
|
||||
|
||||
@@ -56,6 +56,7 @@ typedef struct
|
||||
|
||||
/* Sprites */
|
||||
uint16_t collisions; /* what the next collisions() call reports */
|
||||
uint16_t solidcollisions; /* and what the next solids() call reports */
|
||||
int patternbytes[AKBASIC_MAX_SPRITES]; /* bytes the last define() carried, per sprite */
|
||||
} akbasic_MockDevice;
|
||||
|
||||
@@ -428,6 +429,40 @@ static akerr_ErrorContext *mock_spr_collisions(akbasic_SpriteBackend *self, uint
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *mock_spr_solids(akbasic_SpriteBackend *self, uint16_t *mask)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
(void)self;
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (mask != NULL), AKERR_NULLPOINTER, "NULL mask in solids");
|
||||
*mask = MOCK.solidcollisions;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *mock_spr_shape(akbasic_SpriteBackend *self, int n, int kind,
|
||||
double x1, double y1, double x2, double y2)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
(void)self;
|
||||
|
||||
mock_log("sprhit %d,%d,%g,%g,%g,%g\n", n, kind, x1, y1, x2, y2);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *mock_spr_solid(akbasic_SpriteBackend *self, int id, bool define,
|
||||
double x1, double y1, double x2, double y2)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
(void)self;
|
||||
|
||||
if ( !define ) {
|
||||
mock_log("solid %d off\n", id);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
mock_log("solid %d,%g,%g,%g,%g\n", id, x1, y1, x2, y2);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- fixture -- */
|
||||
|
||||
/** @brief Reset the recorder and populate all three vtables. */
|
||||
@@ -485,6 +520,9 @@ static void mock_devices_init(void)
|
||||
MOCK_SPRITES.configure = mock_spr_configure;
|
||||
MOCK_SPRITES.shared_colors = mock_spr_shared_colors;
|
||||
MOCK_SPRITES.collisions = mock_spr_collisions;
|
||||
MOCK_SPRITES.solids = mock_spr_solids;
|
||||
MOCK_SPRITES.shape = mock_spr_shape;
|
||||
MOCK_SPRITES.solid = mock_spr_solid;
|
||||
}
|
||||
|
||||
/** @brief Set what the next collisions() call will report. Bit n-1 is sprite n. */
|
||||
@@ -494,6 +532,13 @@ static void mock_set_collisions(uint16_t mask)
|
||||
MOCK.collisions = mask;
|
||||
}
|
||||
|
||||
/** @brief Set what the next solids() call will report. Bit n-1 is sprite n. */
|
||||
__attribute__((unused))
|
||||
static void mock_set_solid_collisions(uint16_t mask)
|
||||
{
|
||||
MOCK.solidcollisions = mask;
|
||||
}
|
||||
|
||||
/** @brief Prime the input backend with keystrokes GET will drain in order. */
|
||||
__attribute__((unused))
|
||||
static void mock_push_keys(const char *keys)
|
||||
|
||||
@@ -350,9 +350,32 @@ static void test_collision_arming(void)
|
||||
"COLLISION with no target should disarm");
|
||||
harness_stop();
|
||||
|
||||
TEST_REQUIRE_OK(run_program("10 COLLISION 2, 100\n"));
|
||||
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "read back every frame") != NULL,
|
||||
"COLLISION 2 should be refused with a reason, got \"%s\"", HARNESS_OUTPUT);
|
||||
/*
|
||||
* Type 2 arms a *second* handler, on its own interrupt source, and leaves
|
||||
* type 1's alone. It used to be refused -- a C128's type 2 is a sprite
|
||||
* against the bitmap screen's set pixels, which needs the render target read
|
||||
* back every frame -- and SOLID is what turned it into a question this
|
||||
* interpreter can answer, against rectangles instead of pixels.
|
||||
*/
|
||||
TEST_REQUIRE_OK(run_program("10 COLLISION 1, 100\n"
|
||||
"20 COLLISION 2, 200\n"));
|
||||
TEST_REQUIRE_STR(HARNESS_OUTPUT, "");
|
||||
TEST_REQUIRE(HARNESS_RUNTIME.interrupts[AKBASIC_INTERRUPT_SPRITE].armed,
|
||||
"COLLISION 1 should still have armed the sprite interrupt");
|
||||
TEST_REQUIRE_INT(HARNESS_RUNTIME.interrupts[AKBASIC_INTERRUPT_SPRITE].line, 100);
|
||||
TEST_REQUIRE(HARNESS_RUNTIME.interrupts[AKBASIC_INTERRUPT_BACKGROUND].armed,
|
||||
"COLLISION 2 should have armed the background interrupt");
|
||||
TEST_REQUIRE_INT(HARNESS_RUNTIME.interrupts[AKBASIC_INTERRUPT_BACKGROUND].line, 200);
|
||||
harness_stop();
|
||||
|
||||
/* And disarming one leaves the other armed. */
|
||||
TEST_REQUIRE_OK(run_program("10 COLLISION 1, 100\n"
|
||||
"20 COLLISION 2, 200\n"
|
||||
"30 COLLISION 2\n"));
|
||||
TEST_REQUIRE(HARNESS_RUNTIME.interrupts[AKBASIC_INTERRUPT_SPRITE].armed,
|
||||
"disarming type 2 should not have disarmed type 1");
|
||||
TEST_REQUIRE(!HARNESS_RUNTIME.interrupts[AKBASIC_INTERRUPT_BACKGROUND].armed,
|
||||
"COLLISION 2 with no target should disarm");
|
||||
harness_stop();
|
||||
|
||||
TEST_REQUIRE_OK(run_program("10 COLLISION 3, 100\n"));
|
||||
@@ -407,6 +430,70 @@ static void test_collision_fires(void)
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief SOLID registers static geometry, and COLLISION 2 fires on meeting it.
|
||||
*
|
||||
* The whole second path, against the mock: the device is told about a rectangle,
|
||||
* the scan reports a sprite touching one, the service accumulates it on its own
|
||||
* accumulator and raises its own interrupt, and BUMP(2) reports and clears
|
||||
* without disturbing BUMP(1).
|
||||
*/
|
||||
static void test_solid_geometry(void)
|
||||
{
|
||||
TEST_REQUIRE_OK(harness_start(NULL));
|
||||
mock_devices_init();
|
||||
TEST_REQUIRE_OK(akbasic_runtime_set_devices(&HARNESS_RUNTIME, NULL, NULL, NULL,
|
||||
&MOCK_SPRITES));
|
||||
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME,
|
||||
"1 COLLISION 2, 5\n"
|
||||
"2 SOLID 7, 100, 100, 140, 120\n"
|
||||
"3 PRINT \"MAIN\"\n"
|
||||
"4 GOTO 3\n"
|
||||
"5 PRINT \"WALL \" + BUMP(2)\n"
|
||||
"6 PRINT \"SPRITES \" + BUMP(1)\n"
|
||||
"7 RETURN\n"));
|
||||
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
||||
|
||||
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 3));
|
||||
TEST_REQUIRE(strstr(MOCK.log, "solid 7,100,100,140,120") != NULL,
|
||||
"SOLID should have reached the device, log was \"%s\"", MOCK.log);
|
||||
|
||||
/* Sprite 1 meets a wall; nothing meets another sprite. */
|
||||
mock_set_solid_collisions(0x01);
|
||||
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 2));
|
||||
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "WALL 1") != NULL,
|
||||
"the type 2 handler should have reported sprite 1, got \"%s\"", HARNESS_OUTPUT);
|
||||
|
||||
mock_set_solid_collisions(0);
|
||||
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 2));
|
||||
/* The two accumulators are separate: BUMP(1) saw nothing. */
|
||||
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "SPRITES 0") != NULL,
|
||||
"a static collision should not set BUMP(1), got \"%s\"", HARNESS_OUTPUT);
|
||||
TEST_REQUIRE_INT(HARNESS_RUNTIME.sprite_state.bumpedsolid, 0);
|
||||
harness_stop();
|
||||
|
||||
/* SOLID with no arguments retires every one of them. */
|
||||
TEST_REQUIRE_OK(run_program("10 SOLID 3, 0, 0, 10, 10\n"
|
||||
"20 SOLID 4, 20, 20, 30, 30\n"
|
||||
"30 SOLID\n"));
|
||||
TEST_REQUIRE(strstr(MOCK.log, "solid 3 off") != NULL,
|
||||
"bare SOLID should have retired 3, log was \"%s\"", MOCK.log);
|
||||
TEST_REQUIRE(strstr(MOCK.log, "solid 4 off") != NULL,
|
||||
"bare SOLID should have retired 4, log was \"%s\"", MOCK.log);
|
||||
harness_stop();
|
||||
|
||||
/* Out of range, and a rectangle that is not one. */
|
||||
TEST_REQUIRE_OK(run_program("10 SOLID 999, 0, 0, 10, 10\n"));
|
||||
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "outside 1..") != NULL,
|
||||
"SOLID 999 should be refused, got \"%s\"", HARNESS_OUTPUT);
|
||||
harness_stop();
|
||||
|
||||
TEST_REQUIRE_OK(run_program("10 SOLID 1, 10, 10, 10, 20\n"));
|
||||
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "not a rectangle") != NULL,
|
||||
"a zero-width SOLID should be refused, got \"%s\"", HARNESS_OUTPUT);
|
||||
harness_stop();
|
||||
}
|
||||
|
||||
/** @brief The readbacks answer from interpreter state, so they need no device. */
|
||||
static void test_readbacks(void)
|
||||
{
|
||||
@@ -518,6 +605,7 @@ int main(void)
|
||||
test_sprsav_refuses_to_write();
|
||||
test_collision_arming();
|
||||
test_collision_fires();
|
||||
test_solid_geometry();
|
||||
test_readbacks();
|
||||
test_no_device();
|
||||
test_new_clears_sprites();
|
||||
|
||||
Reference in New Issue
Block a user