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:
2026-08-02 09:51:17 -04:00
parent f06c53110d
commit 40198cbc07

152
TODO.md
View File

@@ -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 `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. 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` ## 7. Filing gaps against `libakgl`
When phase 7 or phase 8 needs something `libakgl` does not have, **stop and file it** in 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 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`. 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 **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. design consequence nobody chose, and item 9 is what that consequence costs a real program.
Item 5 sharpens something already recorded. None of them is a complaint about the interpreter, 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 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, 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 because the suite tests the constructs the implementer had in mind and a game reaches for