File the four things this work deliberately left alone
Nothing here is built. Each is written down so the reasoning does not have to be reconstructed by whoever picks it up. **§9 item 9: the sprite breakout spends two of its eight sprites on the screen.** It draws its play field, captures the whole 800x540 region with SSHAPE, and installs the capture as sprite 2 -- and does the same for the HUD strip as sprite 1. That reads as a silly thing to do. It is not: it is the only thing that works, and it is working around items 3 and 5 together rather than choosing anything. Item 3 says the text layer owns every row by default, and that half is now answerable since WINDOW became reachable. Item 5 is what WINDOW does not fix -- the frontend never clears and SDL double-buffers, so a drawing has to be re-issued every frame and fit inside one 256-line batch. Breakout's field is sixty GSHAPE stamps plus two BOXes plus a drawn banner; it does not fit and never will. A sprite is the one thing the interpreter redraws from its own state for nothing. What that costs is now measured rather than asserted: two of eight sprite slots, which is the root of every design compromise in that game and the reason chapter 18 opens with a budget table; **4.5% of a frame in collision alone**, because sprite 2's box covers the whole field so every moving sprite overlaps it permanently and the broad-phase reject can never throw those pairs out -- 211.8 ns a scan against 54.9 for eight sprites that do not overlap, on every one of 256 scans a frame, to collide with the backdrop; and a chapter section that exists only to teach the workaround. The fix is somewhere to draw that persists and is not a sprite -- a layer the sink composites under the text and the sprites, that a program writes once and the frontend does not discard. Nothing in libakgl 0.8.0 supplies it; there is no render-to-texture layer and `frame_start` clears. Filed rather than fixed because it is a design decision about what a frame owns. **§6 items 38-40**, the follow-ups to the collision integration: - Finishing the COLLISION/BUMP migration. The proxies are deliberately not registered with a partitioner -- a uniform grid over eight of them costs more than an all-pairs loop over 28 pairs saves. What is worth recording is the *threshold*, so that raising AKBASIC_MAX_SPRITES is a decision made with the number in front of it. - Converting both breakout listings and chapters 17 and 18 to the new verbs. About 200 of their 230 collision lines are a sprite against a BASIC array, which static collision geometry is what changes. Separate because both chapters were rewritten and validated immediately before this work, their examples are executed by ctest, and folding a listing rewrite into a library integration would make neither reviewable. - `akbasic_runtime_call_function()`. A verb cannot take a BASIC function today. The finding worth keeping is that the hard half already exists at src/runtime.c:1031 -- the multi-line DEF path already re-enters the line loop from inside expression evaluation -- so this is splitting one argument-binding loop, not building a mechanism. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL
This commit is contained in:
152
TODO.md
152
TODO.md
@@ -2214,6 +2214,98 @@ should loop — and the third is why both games are built out of `LABEL` and `GO
|
||||
`DO`", which is a rule a reader can follow. Wants `tests/for_next.c` either way, because
|
||||
the reduction above is four lines and currently nothing asserts it in either direction.
|
||||
|
||||
### Found while integrating libakgl's collision subsystem
|
||||
|
||||
`spr_collisions()` now answers through `akgl_collision_test()` and every sprite carries an
|
||||
`akgl_CollisionShape` and a pooled proxy. Three things were deliberately left for later, and
|
||||
each is here so the reasoning does not have to be reconstructed.
|
||||
|
||||
38. **Finish moving `COLLISION` and `BUMP` onto the subsystem.** What landed is the narrowphase
|
||||
and the shapes; the seam is still `collisions(self, uint16_t *mask)` and the pairing is
|
||||
still all-against-all over eight slots with a bounding-box reject in front of it.
|
||||
|
||||
That is the right shape *at eight sprites* and the benchmark says so: 54.9 ns a scan spread
|
||||
out, 211.8 ns for breakout's layout, against a 1.20 ms frame. A uniform grid over eight
|
||||
proxies costs more in per-proxy insert and move bookkeeping than an all-pairs loop over 28
|
||||
pairs saves, which is why the proxies are deliberately **not** registered with any
|
||||
partitioner — `akgl_collision_test()` takes two positioned proxies and needs no world.
|
||||
|
||||
**The threshold is what to record, not the work.** Registering the proxies and letting
|
||||
`akgl_collision_query_box()` or the partitioner's `each_pair` do the pairing becomes worth
|
||||
it somewhere above eight, and `AKBASIC_MAX_SPRITES` is a `#define`
|
||||
(`include/akbasic/sprite.h:38`). Anybody raising it should have the number in front of them
|
||||
rather than discovering the cost afterwards: re-run `tests/collision_perf.c` at the new
|
||||
ceiling and compare against the frame row. Note that libakgl's own measurement puts the
|
||||
naive all-pairs sweep at 0.7% of a frame at 64 actors and 11% at 256, so the crossover is
|
||||
well above anything a Commodore-shaped sprite budget will reach.
|
||||
|
||||
Also still open at the seam: the contact is computed and discarded. §6 item 40 and the
|
||||
BASIC surface built on it are what consume it.
|
||||
|
||||
39. **Convert the breakout listings and chapters 17 and 18 to the collision verbs.** This is the
|
||||
payoff, and it is deliberately not part of the integration commit.
|
||||
|
||||
Neither game calls `COLLISION` or `BUMP` even once today. Of roughly 230 lines of collision
|
||||
code across the two, about 19 are sprite-against-sprite; the rest is a sprite against
|
||||
numbers in a BASIC array or against bare constants, because bricks are text characters in
|
||||
one game and stamped pixels inside one big sprite in the other. Static collision geometry is
|
||||
what changes that — a brick becomes a collidable box without spending a sprite slot.
|
||||
|
||||
What goes, concretely:
|
||||
|
||||
- `examples/breakout/sprites/breakout.bas` — `BALLBRICKS` (29 lines) and `TESTCELL` (28)
|
||||
become a handler that reads which box was hit and which axis to reverse. The
|
||||
hand-written minimum-translation-axis at `:661-666` is a contact field. `BUILDLIVE` and
|
||||
`BUILDROW` stop maintaining a parallel `BRK#()` grid.
|
||||
- `examples/breakout/characters/breakout.bas` — `HITTEST` (14 lines) and the pixel-to-cell
|
||||
arithmetic go the same way, and `PADHIT`'s five-line overlap test becomes `BUMP`.
|
||||
|
||||
**The documentation cost is the reason this is separate.** Chapter 17 Step 10 and chapter 18
|
||||
Steps 9 and 13 are built around that arithmetic, both chapters' examples are executed by
|
||||
`docs_examples`, and both were rewritten and validated immediately before this work started.
|
||||
Folding a listing rewrite into a library integration would make neither reviewable.
|
||||
|
||||
Sequence it after §9 item 9 if that is ever fixed, since a game that no longer spends two
|
||||
sprites on the screen has two more to spend on collidable things, and the two rewrites would
|
||||
otherwise touch the same lines twice.
|
||||
|
||||
40. **`akbasic_runtime_call_function()`: call a BASIC function from C with bound values.**
|
||||
|
||||
A verb cannot currently take a BASIC function as an argument, and it came up while designing
|
||||
the collision query — a callback per overlapping shape is one obvious shape for that verb.
|
||||
It was rejected for the query on the use case rather than on feasibility: the motivating
|
||||
program wants one answer rather than many, a loop is the idiomatic form in this dialect, and
|
||||
the language already has a callback shaped the way a BASIC programmer expects — an interrupt
|
||||
handler taking a label, which `COLLISION` uses.
|
||||
|
||||
**The finding worth keeping is that the hard half already exists.** `src/runtime.c:1031` is
|
||||
the multi-line `DEF` path:
|
||||
|
||||
```c
|
||||
callenv->gosubReturnLine = callenv->lineno + 1;
|
||||
callenv->nextline = fndef->lineno;
|
||||
while ( obj->environment != targetenv && obj->mode == AKBASIC_MODE_RUN ) {
|
||||
PASS(errctx, akbasic_runtime_process_line_run(obj));
|
||||
}
|
||||
```
|
||||
|
||||
The interpreter already re-enters its own line loop synchronously from inside expression
|
||||
evaluation, with a scope from the pool, re-entrant since §6 item 26, and with recursion
|
||||
depth answering to `AKBASIC_MAX_ENVIRONMENTS` as a diagnosis rather than a hang.
|
||||
|
||||
What is missing is narrow. `akbasic_runtime_user_function()` takes an `akbasic_ASTLeaf *`
|
||||
call site and evaluates argument *leaves* at `:993-1006`; split that loop so the AST path
|
||||
evaluates-then-binds and a new entry point binds values it is handed. Resolving a bare word
|
||||
to a function rather than a label is `akbasic_environment_get_function()` at `:960`, and
|
||||
`COLLISION 1, BUMPED` is the precedent for a verb taking a bare word by name
|
||||
(`src/runtime_sprite.c:543-555`).
|
||||
|
||||
**One hazard, if it is ever wired to a collision query.** Do not pass the BASIC function
|
||||
through as an `akgl_CollisionVisitFunc`: it would run inside the partitioner's cell-chain
|
||||
walk, and the callback a brick loop wants removes the brick it was just told about, which is
|
||||
`partitioner.remove()` rewriting the chain the walk holds a cursor into. Collect the
|
||||
candidates during the walk, close the walk, then dispatch.
|
||||
|
||||
## 7. Filing gaps against `libakgl`
|
||||
|
||||
When phase 7 or phase 8 needs something `libakgl` does not have, **stop and file it** in
|
||||
@@ -2991,9 +3083,65 @@ reduced against `build/basic`, the stdio build, unless it says otherwise.
|
||||
routine above it that ends in a fall-through rather than an `END`, and a table of angles or
|
||||
offsets is exactly where negative numbers live. `tests/read_data.c`.
|
||||
|
||||
9. **A program that wants a picture on the screen has to spend a sprite slot on it, and
|
||||
`examples/breakout/sprites/breakout.bas` spends two of its eight on the screen itself.**
|
||||
|
||||
That listing draws its play field -- walls, sixty stamped bricks, the lettering -- captures
|
||||
the whole 800 by 540 region with `SSHAPE`, and installs the capture as sprite 2. It does the
|
||||
same for the HUD strip as sprite 1. Its own header calls them "the screen". It reads as a
|
||||
silly thing to do and it is not: it is the only thing that works, and it is working around
|
||||
the combination of item 3 and item 5 rather than choosing anything.
|
||||
|
||||
Neither defect alone would force it:
|
||||
|
||||
- **Item 3** says the text layer owns every row of the window by default, so a drawing is
|
||||
painted over before it is presented. That half is now answerable -- `WINDOW` is reachable
|
||||
since §6 item 31, so a program *can* take rows back.
|
||||
- **Item 5** is what `WINDOW` does not fix. The frontend never clears and SDL double-buffers,
|
||||
so a drawing has to be **re-issued every frame**, and a re-issue has to fit inside one
|
||||
256-line batch to survive a present. Breakout's field is sixty `GSHAPE` stamps plus two
|
||||
`BOX`es plus a stroke-drawn banner. It does not fit in one batch and never will.
|
||||
|
||||
So the program cannot draw its field every frame, and a drawing it makes once does not
|
||||
survive. A sprite is drawn from state the interpreter keeps, every frame, for nothing --
|
||||
which is exactly the property the field needs and the only place in the interpreter that
|
||||
has it.
|
||||
|
||||
**What it costs, now measured rather than asserted:**
|
||||
|
||||
- **Two of eight sprite slots.** That is the whole reason the bricks are drawn rather than
|
||||
made of artwork, why only one gem can fall at a time, and why chapter 18 opens with a
|
||||
sprite budget table. Every design compromise in that game traces back here.
|
||||
- **4.5% of a frame in collision alone.** Sprite 2's bounding box covers the entire play
|
||||
field, so every moving sprite overlaps it permanently and the broad-phase reject can never
|
||||
throw those pairs out. `tests/collision_perf.c` has the row: 211.8 ns a scan for that
|
||||
layout against 54.9 ns for eight sprites that do not overlap. The game pays it on every
|
||||
one of 256 scans a frame to collide with *the backdrop*.
|
||||
- **A whole chapter of documentation.** Chapter 18 Step 3 is called "Turn a drawing into a
|
||||
sprite" and exists solely to teach the workaround.
|
||||
|
||||
**What would fix it: somewhere to draw that persists and is not a sprite.** A drawing layer
|
||||
the sink composites beneath the text layer and the sprites, that a program writes once and
|
||||
the frontend does not discard. `GRAPHIC` already selects a mode; the natural shape is for a
|
||||
drawing mode to mean "this layer is yours and it stays" rather than "this is thrown away at
|
||||
the next present". That is `src/sink_akgl.c` and `src/frontend_akgl.c`, and it is a real
|
||||
design decision about what a frame owns rather than a patch -- which is why this is a filed
|
||||
item and not a fix.
|
||||
|
||||
Nothing in libakgl 0.8.0 supplies it. There is no render-to-texture layer and no persistent
|
||||
surface in `include/akgl/renderer.h`; `frame_start` clears the target, which is the opposite.
|
||||
If the answer turns out to want one, it is a §7 filing against `libakgl` rather than
|
||||
something to build here.
|
||||
|
||||
**Do not "fix" this by rewriting the game.** The listing is correct for the interpreter it
|
||||
was written against, and item 3's own entry says the trick "is still the right one for a
|
||||
game; it should not be the *only* way". The game changes when the interpreter gives it
|
||||
something better, and chapters 17 and 18 change with it -- see §6 item 39.
|
||||
|
||||
**A note on what this list is.** Items 1, 2, 6, 7 and 8 are defects by any reading. Item 3 is a
|
||||
design consequence nobody chose. Item 4 may be intended and is a documentation gap either way.
|
||||
Item 5 sharpens something already recorded. None of them is a complaint about the interpreter,
|
||||
design consequence nobody chose, and item 9 is what that consequence costs a real program.
|
||||
Item 4 may be intended and is a documentation gap either way. Item 5 sharpens something already
|
||||
recorded. None of them is a complaint about the interpreter,
|
||||
which was a pleasure to write a real program against — they are the kind of thing one real
|
||||
program finds and a test suite written alongside the implementation structurally cannot,
|
||||
because the suite tests the constructs the implementer had in mind and a game reaches for
|
||||
|
||||
Reference in New Issue
Block a user