Write down that a redraw has to fit inside one batch

"Drawing does not persist across frames; redraw it every frame" was the advice,
and it is not sufficient on its own. The host runs a fixed number of source
lines and then presents, and presenting discards the drawing buffer -- so a run
of drawing verbs longer than one budget is **torn**, not merely transient, and
an `SSHAPE` at the end of it captures only what was issued since the present,
over whatever the frame before left behind.

Measured against the standalone frontend's 256: after synchronising to a jiffy
edge, 220 lines of drawing survive a capture and 250 do not.

There is no fix available that does not change what a host owns -- the budget is
the host's and so is the present -- so this is documentation, in the three
places it belongs. Chapter 13 beside the note it qualifies, with the numbers.
Chapter 6 where `SSHAPE` is introduced, which is where a program meets it.
Chapter 14 from the step loop's side, naming `AKBASIC_FRONTEND_STEPS_PER_FRAME`
and explaining the one thing a script can do about it: `settime()` is called once
a frame, so `TI#` changes on the first step of a batch and nowhere else, and
spinning until it changes is the only frame synchronisation this dialect has.

**Untested, deliberately.** Reproducing it needs the real frontend, a clock and
a timing window, and a test that reproduced it would be reproducing a race.
Recorded in TODO.md rather than left looking like an oversight.

TODO.md section 9 item 5, struck as a documentation outcome.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-02 00:23:36 -04:00
parent c8b917d205
commit c3d9e91bbd
2 changed files with 24 additions and 2 deletions

View File

@@ -244,3 +244,11 @@ The graphics verbs draw straight to the renderer rather than into a display list
anything drawn is overwritten by the text layer on the next frame. A program that wants
its drawing to persist has to redraw it. This is recorded as a defect rather than a
design; see Chapter 13.
**A redraw also has to fit inside one batch.** The host runs a fixed number of source
lines and then presents, and presenting throws the drawing buffer away — so a run of
drawing verbs longer than one batch is torn rather than merely transient, and an
`SSHAPE` at the end of it captures only the part issued since the present. Watching
`TI#` change is how a program finds the boundary; Chapter 13 has the measured numbers and
[Chapter 18](18-tutorial-breakout-artwork.md#step-4-find-the-frame-boundary) has a
routine that uses them.

View File

@@ -168,8 +168,22 @@ resume there.
`akbasic_runtime_run(rt, n)` is `step()` in a `while` with a budget: at most `n` steps,
then return regardless. That bound is the only thing standing between a script containing
`10 GOTO 10` and your frame rate. `n <= 0` means unbounded, which is right for a test and
wrong for a game. The SDL frontend uses 256; the stdio driver uses 1, because it wants to
refresh the clock between steps.
wrong for a game. The SDL frontend uses 256 (`AKBASIC_FRONTEND_STEPS_PER_FRAME`); the
stdio driver uses 1, because it wants to refresh the clock between steps.
**That budget is visible to a script, and drawing is where it shows.** The frontend
presents the frame when `run()` comes back, and presenting discards the drawing buffer —
so a sequence of drawing verbs longer than one budget is cut in half by the present in
the middle of it, and an `SSHAPE` afterwards captures only what was issued since. Not a
transient artefact: a torn capture, over whatever the frame before left behind.
A script cannot read the budget, but it can *see* it: `akbasic_runtime_settime()` is
called once per frame by the host, so `TI#` changes on the first step of a batch and
nowhere else. Spinning until it changes is the only frame synchronisation this dialect
has, and it is what
[Chapter 18](18-tutorial-breakout-artwork.md#step-4-find-the-frame-boundary) is built on.
A host that gives the interpreter a larger budget makes more drawing fit; one that never
calls `settime()` takes the synchronisation away entirely.
Time comes in from outside. `akbasic_runtime_settime(rt, ms)` is how the interpreter
learns what time it is; it reads no clock, because it owns no loop and must not block. A