Draw into the whole window, not its top-left 320x200

The graphics verbs documented a coordinate transform that did not exist. With
SCALE off a coordinate went straight to akgl_draw_* as a pixel address, so an
800x600 window drew a C128 listing into its corner and left the rest unused --
while the chapter said coordinates were 320x200 and stretching to fit was the
host's business.

akbasic_GraphicsBackend gains a size entry point, require_graphics() asks it
before every verb that draws so a resized window is honoured between two
statements, and 320x200 becomes the fallback for a backend that leaves it NULL.
It is the record's one optional member, so a host written against the old header
keeps the behaviour it had.

SCALE now maps onto the device, and RGR(1)/RGR(2) report the drawing surface so
a program can use a window whose size it did not choose. RGR(0) is BASIC 7.0's
own field, the GRAPHIC mode.

SCALE also mapped xmax onto the width rather than onto the last pixel, so
SCALE 1, 319, 199 followed by DRAW 1, 319, 199 drew nothing at all -- one pixel
past the surface. Fixed in the same line, because it is what makes "SCALE gives
a C128 listing the whole window" true rather than nearly true.

The akgl test renders against a 128x128 target, deliberately smaller than the
old constants: a SCALE still dividing by them misses it entirely rather than
landing somewhere plausible.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-01 07:35:20 -04:00
parent 23ccb66f69
commit 5c5bf63356
15 changed files with 445 additions and 49 deletions

67
TODO.md
View File

@@ -800,13 +800,43 @@ behaviour to port. They matter to somebody typing in a listing out of a C128 man
17. **`GRAPHIC` records its mode but honours only one consequence of it.** 7.0's five modes
differ in bitmap resolution and in whether the bottom of the screen stays text. Neither
means anything against a host's renderer, whose size the interpreter does not know and
does not own. The mode is stored, an out-of-range one is refused because that is a typo
worth catching, and the one observable rule is that mode 0 is text.
means anything against a host's renderer, whose size the interpreter does not own. The
mode is stored, an out-of-range one is refused because that is a typo worth catching, and
the one observable rule is that mode 0 is text. The mode is readable as `RGR(0)`, which
is 7.0's whole `RGR` and the one field of ours that is.
18. **Coordinates reaching a backend are BASIC's 320x200 space, and scaling to the window is
the host's job.** The interpreter cannot ask the renderer how big it is without owning it.
`SCALE` maps user coordinates onto that space; with `SCALE` off they pass through.
18. **A coordinate reaching a backend is a device pixel, and the whole of the host's window
is reachable from BASIC.** This used to read "coordinates are BASIC's 320x200 space and
scaling to the window is the host's job", on the reasoning that the interpreter cannot
ask the renderer how big it is without owning it. **That reasoning was wrong twice over**,
which is what §8 item 9 came to say.
It was wrong about the mechanism: not owning a thing is no bar to *asking* it, and the
backend record is how everything else here asks. `akbasic_GraphicsBackend` grew a `size`
entry point, `require_graphics()` calls it before every verb that draws -- so a resized
window is honoured between two statements rather than at attach -- and 320x200 became
the fallback for a backend that leaves `size` NULL. It is the record's one optional
entry point, so a host written against the old header keeps working and gets exactly the
behaviour it used to get.
And it was wrong about the description: nothing ever stretched anything. With `SCALE`
off a coordinate was passed straight to `akgl_draw_*` as a pixel address, so an 800x600
window drew a C128 listing into its top-left 320x200 corner and left the rest unused.
The documentation described a coordinate transform that did not exist.
**What changed observably** is `SCALE`, which now maps user coordinates onto the device
rather than onto the constants, and `RGR(1)` / `RGR(2)`, which report the drawing
surface's width and height so a program can use a window whose size it did not choose.
A C128 listing draws in the corner as it always did; `SCALE 1, 319, 199` gives it the
whole window.
**One fix rode along, in the same line of code and too small to defer.** `SCALE` mapped
`xmax` onto the *width*, which put the user space's far corner one pixel past the
surface: `SCALE 1, 319, 199` then `DRAW 1, 319, 199` drew nothing at all. It maps onto
the last pixel now, which is what makes that sentence above true rather than nearly
true. `tests/graphics_verbs.c` and `tests/akgl_backends.c`, the latter against a
128x128 target chosen because it is *smaller* than the old constants -- a `SCALE` still
dividing by them misses it entirely rather than landing somewhere plausible.
19. **`PLAY` does not block.** On a C128 `PLAY` holds the program until the last note ends.
§1.6 forbids that outright, so `PLAY` parses the string into a fixed queue and returns;
@@ -1158,11 +1188,19 @@ deviations from the reference's *program*: `main.go` and the SDL half of
last one in the source until one of them runs. The scan is textual on purpose: parsing every
line up front would raise on lines the program would never have reached.
33. **Sprite coordinates are BASIC's 320×200, not the VIC-II's 0511 by 0255.** `MOVSPR` takes
33. **Sprite coordinates are device pixels, not the VIC-II's 0511 by 0255.** `MOVSPR` takes
the same coordinate space `DRAW` does. A C128's sprite coordinates are the raster's, offset
so that (24, 50) is the top-left of the visible screen; reproducing that would give the
language two coordinate systems and make `MOVSPR 1, 0, 0` put a sprite off-screen. Consistent
with deviation 18.
with deviation 18, which is where "the same space `DRAW` uses" is defined — and it moved,
so this one moved with it.
**`SCALE` deliberately does not apply to sprites.** It is a graphics-verb transform and
the sprite verbs do not call it, which was true before deviation 18 was rewritten and is
worth writing down now that the two spaces can differ: with `SCALE 1, 319, 199` on, a
`DRAW` at 160,100 and a `MOVSPR` to 160,100 land in different places. Arguably they
should agree; not changed here because it is a decision about what `SCALE` means rather
than a slip, and it wants its own commit.
34. **`MOVSPR`'s speed unit is ours.** BASIC 7.0 documents speed 015 with 15 fastest and says
nothing about what a unit is worth. Here one unit is
@@ -1757,9 +1795,18 @@ What remains, in priority order:
almost everything links against it), which is why CI runs `src/symtab.c` instead and the
whole-tree run is a local `cmake --build build --target mutation`.
9. Use the actual full SDL window for graphics operation width
9. ~~**Use the actual full SDL window for graphics operations.**~~ **Done**, and the premise
turned out to be half wrong in a way worth keeping. The documentation did say coordinates
are 320x200 whatever the window is — but nothing ever stretched them: with `SCALE` off a
coordinate went straight to `akgl_draw_*` as a pixel address, so an 800x600 window drew a
listing into its top-left corner and left the rest unused. The described transform did not
exist.
The current documentation states that graphics operations always use a 320x200 space regardless of the window's size. These verbs should be able to access the entirety of the SDL window to which they are bound.
`akbasic_GraphicsBackend` grew an optional `size` entry point, `require_graphics()` asks it
before every verb that draws so a resized window is honoured between statements, `SCALE`
maps onto the answer instead of onto the constants, and `RGR(1)` / `RGR(2)` let a program
read the size. 320x200 is now the fallback for a backend that will not answer. Deviation 18
in §5 has the whole of it, including the off-by-one it fixed on the way.
11. Error codes are undefined for the user