From 40198cbc07999262fcd0e26f942353c564536061 Mon Sep 17 00:00:00 2001 From: Andrew Kesterson Date: Sun, 2 Aug 2026 09:51:17 -0400 Subject: [PATCH] File the four things this work deliberately left alone MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL --- TODO.md | 152 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 150 insertions(+), 2 deletions(-) diff --git a/TODO.md b/TODO.md index b6b3b14..d8b32b1 100644 --- a/TODO.md +++ b/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