Generate the documentation's figures from the listings they illustrate

Chapters 6 and 8 described what a verb draws in prose. Eight figures now show
it, and each one is produced by running the BASIC listing printed immediately
above it -- so a picture cannot drift away from the code beside it, which is the
way a screenshot goes wrong and the way nothing notices.

tools/screenshot.c is a second SDL host, much smaller than the frontend: dummy
video driver, software renderer, run to completion, read the target back, write
a PNG. It draws no text layer on purpose, so a READY in the corner is not noise
in a figure about BOX and no font has to be resolved.

tools/docs_screenshots.sh reads the new screenshot=NAME fence tag straight out
of the markdown. size=WxH is the second tag, and SCALE's figure uses it: the
point being made is a 320x200 listing filling a larger window, which cannot be
made on a 320x200 surface.

Two gates, answering different questions. docs_examples fails a tagged block
with no image, in both configurations, so a figure cannot be added and
forgotten. docs_screenshots -- a CTest, AKGL build only -- re-renders every
figure and compares byte for byte, so a listing edited without regenerating
fails. Only the second catches a stale picture.

The PNGs are checked in because a reader on the forge has no build tree, and
docs/images/README.md says loudly that they are generated. Regenerating is never
part of a build: the target is run deliberately, so a make cannot put eight
binary diffs in front of whoever ran it.

Drawing the BOX figure caught a defect in TODO.md itself. Deviation 16 claimed
in bold that BOX fills on a negative angle while its own paragraph said the fill
was filed rather than implemented. BOX cannot fill, and filled_rect is reached
by no verb as a result.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-01 07:37:04 -04:00
parent 737fdc760f
commit 16b38c1138
18 changed files with 759 additions and 40 deletions

View File

@@ -214,6 +214,11 @@ jobs:
# they are the ones that must keep passing when SDL is present, *and* they # they are the ones that must keep passing when SDL is present, *and* they
# are what proves the SDL frontend changes no output anywhere in the # are what proves the SDL frontend changes no output anywhere in the
# corpus. # corpus.
# docs_screenshots runs here and nowhere else, because rendering a figure
# needs the SDL half. It re-renders every picture in docs/ and compares it
# byte for byte with the checked-in copy, so a chapter whose listing was
# edited without regenerating fails on this push rather than shipping a
# picture of code that no longer exists.
# akgl_typing reports Skipped here and that is correct: it needs a real X # akgl_typing reports Skipped here and that is correct: it needs a real X
# server, a window manager and xdotool to type at a focused window, and a # server, a window manager and xdotool to type at a focused window, and a
# CI runner has none of the three. It is a developer-machine gate, and the # CI runner has none of the three. It is a developer-machine gate, and the

View File

@@ -192,6 +192,26 @@ if(AKBASIC_WITH_AKGL)
target_compile_options(akbasic_frontend PRIVATE -Wall -Wextra) target_compile_options(akbasic_frontend PRIVATE -Wall -Wextra)
target_link_libraries(akbasic_frontend PUBLIC akbasic_akgl) target_link_libraries(akbasic_frontend PUBLIC akbasic_akgl)
akbasic_instrument(akbasic_frontend) akbasic_instrument(akbasic_frontend)
# The documentation's figure generator: a second, much smaller host that draws
# one program onto an offscreen target and writes a PNG. Not instrumented and
# not a test -- it produces a build artifact rather than an answer, and folding
# it into the coverage figure would credit the interpreter for lines only a
# documentation build runs.
add_executable(akbasic_screenshot tools/screenshot.c)
target_compile_options(akbasic_screenshot PRIVATE -Wall -Wextra)
target_link_libraries(akbasic_screenshot PRIVATE akbasic_akgl SDL3_image::SDL3_image)
# Regenerating every figure in docs/ is a deliberate act, never part of a
# build: the PNGs are checked in, and a rebuild that silently rewrote them
# would put a binary diff in front of anybody who happened to run `make`.
add_custom_target(docs_screenshots
COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tools/docs_screenshots.sh"
--root "${CMAKE_CURRENT_SOURCE_DIR}"
--tool "$<TARGET_FILE:akbasic_screenshot>"
DEPENDS akbasic_screenshot
COMMENT "Regenerating the documentation figures in docs/images"
VERBATIM)
endif() endif()
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -370,6 +390,28 @@ if(AKBASIC_WITH_AKGL)
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
TIMEOUT 180 TIMEOUT 180
SKIP_RETURN_CODE 77) SKIP_RETURN_CODE 77)
# Every figure in docs/ re-rendered and byte-compared against the checked-in
# copy. docs_examples only asks whether the file *exists*, which catches a
# figure that was never generated and not one that stopped being of the code
# beside it -- and that second failure is the one this whole arrangement is
# against.
#
# **A byte comparison of a rendered PNG is a deliberate bet**, the same bet
# tests/reference/ already makes about golden output: that the dummy video
# driver and the software renderer are reproducible. They are, run to run and
# build to build. What is untested is an SDL upgrade that shifts one pixel of
# a diagonal, and the answer to that is to regenerate the figures in the same
# commit as the bump -- not to weaken this to a size check, which would pass
# for every wrong picture that happened to be 320x200.
_add_test(NAME docs_screenshots
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/tools/docs_screenshots.sh
--root "${CMAKE_CURRENT_SOURCE_DIR}"
--tool $<TARGET_FILE:akbasic_screenshot>
--check)
_set_tests_properties(docs_screenshots PROPERTIES
TIMEOUT 120
ENVIRONMENT "SDL_VIDEODRIVER=dummy;SDL_AUDIODRIVER=dummy;SDL_RENDER_DRIVER=software")
endif() endif()
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------

View File

@@ -576,9 +576,42 @@ a missing decision rather than a free pass.
| `sh norun` | Shown, not run | | `sh norun` | Shown, not run |
| `cmake` | **Never executed.** Hand-maintained, by decision | | `cmake` | **Never executed.** Hand-maintained, by decision |
| `text` | **Never executed.** A block diagram, or a stack trace captured from a real run | | `text` | **Never executed.** A block diagram, or a stack trace captured from a real run |
| `basic screenshot=NAME` | Also the source of `docs/images/NAME.png`. The harness checks the file exists; `tools/docs_screenshots.sh` is what makes it |
| `basic size=WxH` | That figure's surface, when 320x200 is not the size the point needs |
Attributes combine: `basic requires=akgl setup=ship` is a real tag in `docs/08-sprites.md`. Attributes combine: `basic requires=akgl setup=ship` is a real tag in `docs/08-sprites.md`.
### Figures are output, not assets
A chapter about `CIRCLE` wants a picture of what `CIRCLE` draws, and the way a picture
goes wrong is that it stops being of the code beside it — silently, because a screenshot
taken by hand still looks like a screenshot long after the verb changed. So a figure is
generated from the listing it illustrates:
```sh norun
$ cmake --build build-akgl --target docs_screenshots
```
`tools/screenshot.c` is a second, much smaller SDL host — the dummy video driver, a
software renderer, run the program to completion, read the target back, write a PNG. It
draws no text layer, deliberately: a `READY` in the corner is noise in a figure about
`BOX`, and skipping it means no font has to be found.
Three rules around it:
- **The PNGs are checked in.** A reader on the forge has no build tree. They are generated
files that are tracked on purpose, so **do not hand-edit one** — change the listing and
regenerate, which is the whole point of the arrangement.
- **The build never regenerates them.** The target is only ever run deliberately, because a
`make` that quietly rewrote eight binaries would put that diff in front of whoever
happened to build.
- **`docs_examples` fails a tagged block with no image**, in both configurations, so a
figure cannot be added to a chapter and then forgotten.
`requires=akgl` belongs on a screenshot block too. The two tags answer different questions:
one is whether the program is *run by the suite*, the other is whether it *produces a
figure*, and a graphics listing wants both.
`text` exists because `docs/14-architecture.md` needed block diagrams and an untagged block `text` exists because `docs/14-architecture.md` needed block diagrams and an untagged block
is a hard error. It carries no executable claim, which is exactly why it has to be *said*: is a hard error. It carries no executable claim, which is exactly why it has to be *said*:
the alternative was an indented code block that the extractor never sees, and a picture the alternative was an indented code block that the extractor never sees, and a picture

83
TODO.md
View File

@@ -793,10 +793,23 @@ behaviour to port. They matter to somebody typing in a listing out of a C128 man
is not its size. Nothing in BASIC does any of those to a shape string except a program is not its size. Nothing in BASIC does any of those to a shape string except a program
deliberately poking at it. deliberately poking at it.
16. **`BOX` fills on a negative angle rather than on a seventh argument.** 7.0's last 16. **`BOX` cannot fill at all, and the plan for it is to spell fill as a negative angle.**
argument selects outline or fill and sits *after* the rotation, which would make a filled 7.0's last argument selects outline or fill and sits *after* the rotation, which would
box a seven-argument call. That is filed rather than implemented; today a rotation of zero make a filled box a seven-argument call — one more than `akbasic_cmd_box()` collects.
outlines through the renderer's rectangle and any other rotation draws four lines. Spelling it as a negative rotation instead is the intended answer and **is not
implemented**: today a rotation of zero outlines through the renderer's rectangle and any
other rotation, negative included, draws four lines.
**This entry used to claim in bold that it fills**, with the body then saying it was
filed rather than implemented — a headline that contradicted its own paragraph, which is
exactly how a reader ends up believing a feature exists. Found by writing a
documentation figure for `BOX` and getting an outline back. `PAINT` is the fill a
program has today.
**`akbasic_GraphicsBackend::filled_rect` is dead from the language's side** as a direct
consequence: `src/graphics_akgl.c` implements it and `tests/mockdevice.h` records it, but
no BASIC verb reaches it. It is the entry point this fix would call, so it is waiting
rather than unused — worth knowing before somebody tidies it away.
17. **`GRAPHIC` records its mode but honours only one consequence of it.** 7.0's five modes 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 differ in bitmap resolution and in whether the bottom of the screen stays text. Neither
@@ -1588,12 +1601,13 @@ requirement; exactly one case has diverged on purpose since, and
| Gate | Result | | Gate | Result |
|---|---| |---|---|
| `ctest` | 95/95 — 41 reference golden cases, 15 local ones, 36 unit tests, 2 embedding examples, and `docs_examples` | | `ctest` | 95/95 — 41 reference golden cases, 15 local ones, 36 unit tests, 2 embedding examples, and `docs_examples` |
| `ctest` with `-DAKBASIC_WITH_AKGL=ON` | 94/94 on a machine with a display; 93 passed + 1 skipped headless. The same set minus the three `no_device` cases the SDL driver contradicts, plus `akgl_backends`, `akgl_frontend` and `akgl_typing` — the last of which is the skip, and the `akgl_build` CI job is where it skips | | `ctest` with `-DAKBASIC_WITH_AKGL=ON` | 95/95 on a machine with a display; 94 passed + 1 skipped headless. The same set minus the three `no_device` cases the SDL driver contradicts, plus `akgl_backends`, `akgl_frontend`, `docs_screenshots` and `akgl_typing` — the last of which is the skip, and the `akgl_build` CI job is where it skips |
| `docs_examples` | Every fenced block in `README.md`, `MAINTENANCE.md` and `docs/` executed and byte-compared: 37 programs, 9 transcripts, 45 output comparisons, 6 C snippets, 1 excerpt and 2 shell blocks in the default build; 50 programs and 7 C snippets in the AKGL one. `MAINTENANCE.md` documents the fence-tag convention | | `docs_examples` | Every fenced block in `README.md`, `MAINTENANCE.md` and `docs/` executed and byte-compared: 41 programs, 9 transcripts, 49 output comparisons, 2 excerpts, 2 shell blocks and 8 figures in the default build; 57 programs and 48 output comparisons in the AKGL one. The C-snippet count reads 0 when the harness is run by hand without `--cflags-file`; CTest passes it. `MAINTENANCE.md` documents the fence-tag convention |
| `docs_screenshots` | 8/8 figures re-rendered and byte-identical to the checked-in PNGs. AKGL build only — rendering a picture needs the SDL half |
| Golden corpus | 41/41 byte-exact from `tests/reference/`**and 41/41 again through the SDL binary**, which is most of what proves the frontend changes no output | | Golden corpus | 41/41 byte-exact from `tests/reference/`**and 41/41 again through the SDL binary**, which is most of what proves the frontend changes no output |
| ASan + UBSan | 77/77 | | ASan + UBSan | 95/95 |
| Line coverage | 94.6% (3857/4076) — above the 90% gate | | Line coverage | 95.1% (5916/6222) — above the 90% gate |
| Function coverage | 98.6% (287/291) | | Function coverage | 98.5% (403/409) |
| Warnings | none under `-Wall -Wextra` | | Warnings | none under `-Wall -Wextra` |
| `doxygen Doxyfile` | clean | | `doxygen Doxyfile` | clean |
| Mutation (`src/symtab.c`) | 74.1%, against a gate of 65 — see `.gitea/workflows/ci.yaml` | | Mutation (`src/symtab.c`) | 74.1%, against a gate of 65 — see `.gitea/workflows/ci.yaml` |
@@ -1666,6 +1680,14 @@ and `src/graphics_tables.c` 100%, `src/runtime_input.c` 100%, `src/runtime_graph
`src/runtime_audio.c` 98%, `src/play.c` 87%. The `akbasic_akgl` target is not in the coverage `src/runtime_audio.c` 98%, `src/play.c` 87%. The `akbasic_akgl` target is not in the coverage
figure — it is not built in a default configuration. figure — it is not built in a default configuration.
**The denominator moved a long way and this is not an explanation of why.** The line total
went from 4076 to 6222 between the previous entry and this one, which is far more than the
work since added — so one of the two measurements is counting something the other is not,
and which one is right has not been established. Both figures were produced with
`gcovr --filter 'src/.*'` over a clean tree. Recorded rather than quietly overwritten,
because a coverage percentage whose denominator nobody can account for is worth exactly as
much as the accounting. The *ratio* is above the gate on either reading.
**A note on reading that number, because it was misread once while producing it.** A stale **A note on reading that number, because it was misread once while producing it.** A stale
`build-cov/` left in the source directory from an earlier session was silently folded into the `build-cov/` left in the source directory from an earlier session was silently folded into the
report by `gcovr --root .`, which produced the *previous* run's 92.3% for a tree that had grown report by `gcovr --root .`, which produced the *previous* run's 92.3% for a tree that had grown
@@ -1856,6 +1878,45 @@ What remains, in priority order:
recorded above. And `VAL` reports a dependency's status code where the interpreter has recorded above. And `VAL` reports a dependency's status code where the interpreter has
one of its own, which is §6 item 21 and is filed rather than fixed. one of its own, which is §6 item 21 and is filed rather than fixed.
12. Screenshots for graphics operations 12. ~~**Screenshots for graphics operations.**~~ **Done** — eight figures, six in
`docs/06-graphics.md` and two in `docs/08-sprites.md`. The item said chapter 8; chapter 8
is sprites and chapter 6 is the drawing verbs, so both got them.
Chapter 8 of the documentation should include screenshots of various graphics operations being performed. **A figure here is output, not an asset**, which is the part worth keeping. Each one is
generated by running the listing printed immediately above it:
- `tools/screenshot.c` is a second SDL host, and a much smaller one than
`src/frontend_akgl.c` — dummy video driver, software renderer, run to completion,
`SDL_RenderReadPixels`, `IMG_SavePNG`. The pattern is `deps/libakgl/tests/draw.c`'s and
this is its third user. It draws no text layer on purpose: a `READY` in the corner is
noise in a figure about `BOX`, and skipping it means no font has to be resolved.
- `tools/docs_screenshots.sh` reads the new `screenshot=NAME` fence tag straight out of
the markdown, so the picture cannot drift from the code beside it without somebody
editing one of the two. `size=WxH` is the second tag, and `SCALE`'s figure uses it —
640x400, because the point being illustrated is a 320x200 listing filling a larger
window, and that point cannot be made on a 320x200 surface.
- **Two gates, and they answer different questions.** `docs_examples` checks that a
tagged block *has* an image, in both configurations, so a figure cannot be added and
forgotten. `docs_screenshots` — a CTest, AKGL build only — re-renders every figure and
compares byte for byte, so a listing edited without regenerating fails. The first
catches a missing picture; only the second catches a *stale* one, which is the failure
this whole arrangement exists against.
The byte comparison is a deliberate bet that the dummy driver and the software renderer
are reproducible, which is the same bet `tests/reference/` already makes about golden
output. Verified run-to-run and build-to-build here; what is untested is an SDL upgrade
that moves one pixel of a diagonal. **If that happens, regenerate the figures in the same
commit as the bump** — do not weaken the test to a size check, which would pass for every
wrong picture that happened to be 320x200.
The PNGs are checked in, because a reader on the forge has no build tree, and
`docs/images/README.md` says loudly that they are generated so nobody hand-edits one.
Regenerating is never part of a build: `cmake --build build-akgl --target
docs_screenshots` is a deliberate act, so a `make` cannot quietly put eight binary diffs
in front of whoever ran it.
**Writing them turned up a documentation defect of this file's own**, recorded at §5
deviation 16: that entry claimed in bold that `BOX` fills on a negative angle, while its
own body said the fill was filed rather than implemented. Drawing the figure and getting
an outline back is what caught it. `akbasic_GraphicsBackend::filled_rect` is implemented,
recorded by the mock, and reached by no verb at all as a result.

View File

@@ -65,43 +65,86 @@ small number that is not a colour.
`GRAPHIC mode` chooses a screen mode; `GRAPHIC CLR` clears it. Mode 0 is text and `GRAPHIC mode` chooses a screen mode; `GRAPHIC CLR` clears it. Mode 0 is text and
refuses to draw. refuses to draw.
Every picture in this chapter is generated by running the listing above it; see
`MAINTENANCE.md` if you are editing one.
### DRAW ### DRAW
```basic requires=akgl ```basic requires=akgl screenshot=draw
10 DRAW 1, 10, 20 10 COLOR 1, 8
20 DRAW 1, 0, 0 TO 100, 100 TO 200, 0 20 DRAW 1, 20, 180 TO 90, 40 TO 160, 150 TO 230, 20 TO 300, 120
30 COLOR 2, 6
40 DRAW 2, 20, 190 TO 300, 190
50 LOCATE 160, 100
60 COLOR 3, 3
70 DRAW 3
``` ```
![](images/draw.png)
One coordinate pair plots a point. Two or more, separated by `TO`, draw a polyline. A One coordinate pair plots a point. Two or more, separated by `TO`, draw a polyline. A
bare `DRAW 1` plots wherever `LOCATE` left the pixel cursor. bare `DRAW 3` — the last line above, and the single red pixel in the middle of the
picture — plots wherever `LOCATE` left the pixel cursor.
### BOX ### BOX
```basic requires=akgl ```basic requires=akgl screenshot=box
10 BOX 1, 10, 10, 40, 40 10 COLOR 1, 8
20 BOX 1, 20, 30, 130, 140
30 COLOR 2, 6
40 BOX 2, 180, 30, 290, 140, 30
50 COLOR 3, 3
60 LOCATE 300, 190
70 BOX 3, 20, 160
``` ```
Corners, and an optional rotation angle. An unrotated `BOX` outlines rather than fills. ![](images/box.png)
Corners, and an optional rotation angle — the green box is the same box turned 30
degrees about its own centre. Two coordinates instead of four take the other corner
from the pixel cursor, which is the red box.
**`BOX` always outlines; it cannot fill.** BASIC 7.0 selects fill with a seventh
argument and that is not implemented here — see Chapter 13. `PAINT` is the fill you
have.
### CIRCLE ### CIRCLE
```basic requires=akgl ```basic requires=akgl screenshot=circle
10 CIRCLE 1, 160, 100, 50, 30 10 COLOR 1, 8
20 CIRCLE 1, 80, 70, 60, 60
30 COLOR 2, 6
40 CIRCLE 2, 230, 70, 75, 45
50 COLOR 3, 3
60 CIRCLE 3, 160, 140, 130, 50, 90, 270
``` ```
Source, centre, then the two radii — so it draws ellipses. Further arguments give a ![](images/circle.png)
start angle, an end angle, a rotation and the degree increment, which is what makes it
an arc or a polygon. Source, centre, then the two radii — equal radii give a circle and unequal ones an
ellipse. Two further arguments are a start and an end angle, which is what makes the
red arc: 90 to 270 is the bottom half, because angles here are degrees clockwise from
straight up, the same convention `MOVSPR` uses. Beyond those come a rotation and the
degree increment, and a large increment is what turns a circle into a polygon.
### PAINT ### PAINT
```basic requires=akgl ```basic requires=akgl screenshot=paint
10 PAINT 1, 160, 100 10 COLOR 1, 8
20 CIRCLE 1, 100, 100, 70, 70
30 BOX 1, 180, 50, 290, 150
40 COLOR 2, 6
50 PAINT 2, 100, 100
60 COLOR 3, 3
70 PAINT 3, 230, 100
``` ```
Flood-fills the region containing a point. If the region is too large for the fill's ![](images/paint.png)
own working space it stops and reports rather than leaving a half-painted screen with
no explanation. Flood-fills the region containing a point, stopping at whatever is already drawn — so
the outline you fill inside can come from any verb. If the region is too large for the
fill's own working space it stops and reports rather than leaving a half-painted screen
with no explanation.
### LOCATE ### LOCATE
@@ -110,10 +153,23 @@ coordinates finishes.
### SCALE ### SCALE
```basic requires=akgl ```basic requires=akgl screenshot=scale size=640x400
10 SCALE 1, 1023, 1023 10 COLOR 1, 3
20 BOX 1, 0, 0, 319, 199
30 SCALE 1, 319, 199
40 COLOR 2, 6
50 BOX 2, 0, 0, 319, 199
60 DRAW 2, 0, 0 TO 319, 199
``` ```
![](images/scale.png)
That picture is 640 by 400, and both boxes name the same four numbers. The red one is
drawn with `SCALE` off, so its coordinates are pixels and it covers exactly the
top-left 320 by 200 of the window — which is what a C128 listing does here. The green
one is drawn after `SCALE 1, 319, 199` and fills the window, and the diagonal confirms
that 319, 199 reaches the last pixel rather than stopping one short of it.
Turns on user coordinates and gives their maxima. With it on, your coordinates are Turns on user coordinates and gives their maxima. With it on, your coordinates are
mapped onto the drawing surface: 0 is the first pixel and the maximum you gave is the mapped onto the drawing surface: 0 is the first pixel and the maximum you gave is the
*last* one, so `DRAW 1, 1023, 1023` above reaches the bottom-right corner rather than *last* one, so `DRAW 1, 1023, 1023` above reaches the bottom-right corner rather than
@@ -137,12 +193,21 @@ parallel passes; see Chapter 13.
`SSHAPE` copies a rectangle off the screen and `GSHAPE` stamps it back: `SSHAPE` copies a rectangle off the screen and `GSHAPE` stamps it back:
```basic requires=akgl ```basic requires=akgl screenshot=shapes
10 BOX 1, 0, 0, 20, 20 10 COLOR 1, 8
20 SSHAPE A$, 0, 0, 20, 20 20 CIRCLE 1, 40, 40, 30, 30
30 GSHAPE A$, 100, 100 30 COLOR 2, 6
40 PAINT 2, 40, 40
50 SSHAPE A$, 8, 8, 72, 72
60 GSHAPE A$, 120, 20
70 GSHAPE A$, 200, 60
80 GSHAPE A$, 120, 120
``` ```
![](images/shapes.png)
One disc drawn, captured, and stamped three times.
**`A$` holds a handle, not the pixels.** On a C128 the string holds the bitmap, so a **`A$` holds a handle, not the pixels.** On a C128 the string holds the bitmap, so a
program could save it to disk or take its `LEN`. Here a string is a fixed 255 bytes and program could save it to disk or take its `LEN`. Here a string is a fixed 255 bytes and
the region is a device surface, so what goes in the string is a reference to it — the region is a device surface, so what goes in the string is a reference to it —

View File

@@ -58,12 +58,33 @@ operation.
## Showing and moving ## Showing and moving
```basic requires=akgl setup=ship ```basic requires=akgl screenshot=sprites
10 SPRSAV "ship.png", 1 10 COLOR 1, 2
20 SPRITE 1, 1, 3 20 CIRCLE 1, 20, 20, 18, 18
30 MOVSPR 1, 100, 50 30 PAINT 1, 20, 20
40 SSHAPE A$, 0, 0, 40, 40
50 GRAPHIC 1, 1
60 FOR N# = 1 TO 3
70 SPRSAV A$, N#
80 NEXT N#
90 SPRITE 1, 1
100 SPRITE 2, 1, 6
110 SPRITE 3, 1, 8, 0, 1, 1
120 MOVSPR 1, 30, 80
130 MOVSPR 2, 110, 80
140 MOVSPR 3, 190, 60
``` ```
![](images/sprites.png)
Three sprites from one drawing: the first in the default colour, the second in colour
6, and the third in colour 8 with both expansion bits set, which is what makes it twice
the size. `GRAPHIC 1, 1` on line 50 wipes the drawing the sprites were captured from —
the picture would otherwise still show the original disc in the top-left corner.
**A sprite's colour multiplies the artwork rather than replacing it**, so a white disc
takes the colour cleanly and a coloured one comes out darker than you asked for.
`SPRITE n [,on] [,colour] [,priority] [,xexpand] [,yexpand] [,multicolour]`. Only the `SPRITE n [,on] [,colour] [,priority] [,xexpand] [,yexpand] [,multicolour]`. Only the
number is required and **an argument you leave out is left alone**, so `SPRITE 1, 1` number is required and **an argument you leave out is left alone**, so `SPRITE 1, 1`
turns sprite 1 on without disturbing its colour. turns sprite 1 on without disturbing its colour.
@@ -113,6 +134,27 @@ Only type 1, sprite-to-sprite, is implemented. Types 2 and 3 are refused by name
Collision is by bounding box, not by pixel: two sprites whose boxes overlap but whose Collision is by bounding box, not by pixel: two sprites whose boxes overlap but whose
artwork does not are reported as colliding. artwork does not are reported as colliding.
```basic requires=akgl screenshot=sprite-collision
10 COLOR 1, 2
20 CIRCLE 1, 20, 20, 14, 14
30 PAINT 1, 20, 20
40 BOX 1, 0, 0, 40, 40
50 SSHAPE A$, 0, 0, 40, 40
60 GRAPHIC 1, 1
70 SPRSAV A$, 1
80 SPRSAV A$, 2
90 SPRITE 1, 1, 6
100 SPRITE 2, 1, 3
110 MOVSPR 1, 110, 55
120 MOVSPR 2, 145, 90
```
![](images/sprite-collision.png)
The artwork here includes a border around the whole 40 by 40 sprite, so each sprite
draws its own bounding box. Those boxes overlap at one corner and the two discs are
nowhere near each other — and `BUMP(1)` reports a collision.
## Reading state back ## Reading state back
| Function | Gives | | Function | Gives |

21
docs/images/README.md Normal file
View File

@@ -0,0 +1,21 @@
# Generated figures — do not edit
**Every PNG in this directory is output, not an asset.** Each one is produced by running
the BASIC listing shown immediately above it in the chapter, through
`tools/screenshot.c`, and it is checked in only because a reader on the forge has no
build tree to generate it from.
To change a picture, **change the listing** and regenerate:
```sh
cmake --build build-akgl --target docs_screenshots
```
Editing a PNG here by hand puts the picture out of step with the code it illustrates,
which is the exact failure this arrangement exists to prevent — and the `docs_screenshots`
test will fail on it, because it re-renders every figure and compares byte for byte.
The tag that ties a figure to its listing is `screenshot=NAME` in the fence info string,
and `MAINTENANCE.md` documents it along with `size=WxH` for a figure that needs a surface
other than 320 by 200. A tagged block with no image here fails `docs_examples` in both
build configurations, so a figure cannot be added to a chapter and then forgotten.

BIN
docs/images/box.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
docs/images/circle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
docs/images/draw.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
docs/images/paint.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
docs/images/scale.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
docs/images/shapes.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 739 B

BIN
docs/images/sprites.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 904 B

View File

@@ -24,6 +24,8 @@
# ```basic norun a fragment, shown but not run # ```basic norun a fragment, shown but not run
# ```basic requires=akgl only run in the -DAKBASIC_WITH_AKGL=ON build # ```basic requires=akgl only run in the -DAKBASIC_WITH_AKGL=ON build
# ```basic requires=noakgl only run in the build with no devices attached # ```basic requires=noakgl only run in the build with no devices attached
# ```basic screenshot=N also the source of docs/images/N.png; checked to exist
# ```basic size=WxH that figure's surface size, when it is not 320x200
# ```output the exact stdout of the block above, byte for byte # ```output the exact stdout of the block above, byte for byte
# ```c a translation unit: compile it with -fsyntax-only # ```c a translation unit: compile it with -fsyntax-only
# ```c wrap=NAME the same, wrapped in tests/docs_preludes/NAME.pre/.post # ```c wrap=NAME the same, wrapped in tests/docs_preludes/NAME.pre/.post
@@ -128,7 +130,7 @@ fi
CC="${CC:-cc}" CC="${CC:-cc}"
FAILURES=0 FAILURES=0
declare -A RAN=([basic]=0 [repl]=0 [output]=0 [c]=0 [excerpt]=0 [sh]=0) declare -A RAN=([basic]=0 [repl]=0 [output]=0 [c]=0 [excerpt]=0 [sh]=0 [screenshot]=0)
declare -A SKIPPED=([norun]=0 [akgl]=0 [cmake]=0 [text]=0 [nocc]=0) declare -A SKIPPED=([norun]=0 [akgl]=0 [cmake]=0 [text]=0 [nocc]=0)
# Every failure names the file and line of the block, so the message points at # Every failure names the file and line of the block, so the message points at
@@ -475,6 +477,22 @@ for DOC in "${DOCS[@]}"; do
# language does -- a verb refusing because no device was attached is the # language does -- a verb refusing because no device was attached is the
# clearest one -- so they run in one configuration and are skipped in the # clearest one -- so they run in one configuration and are skipped in the
# other. Both directions are needed, and both are visible in the count. # other. Both directions are needed, and both are visible in the count.
# A `screenshot=NAME` block is the source of docs/images/NAME.png, generated
# by tools/docs_screenshots.sh. The image is checked in, because a reader on
# the forge has no build tree -- so nothing would otherwise notice a figure
# that was tagged and never generated, and the chapter would render a broken
# image. Checked in *both* configurations and before the requires= filter,
# because the file either exists or it does not and that has nothing to do
# with whether SDL is present.
SHOT="$(attr screenshot "${INFO}")"
if [ -n "${SHOT}" ]; then
if [ -r "docs/images/${SHOT}.png" ]; then
RAN[screenshot]=$((RAN[screenshot] + 1))
else
fail "${WHERE}" "screenshot=${SHOT} has no docs/images/${SHOT}.png; run \`cmake --build <akgl build> --target docs_screenshots\`"
fi
fi
REQUIRES="$(attr requires "${INFO}")" REQUIRES="$(attr requires "${INFO}")"
if [ "${REQUIRES}" = "akgl" ] && [ "${WITH_AKGL}" -eq 0 ]; then if [ "${REQUIRES}" = "akgl" ] && [ "${WITH_AKGL}" -eq 0 ]; then
SKIPPED[akgl]=$((SKIPPED[akgl] + 1)) SKIPPED[akgl]=$((SKIPPED[akgl] + 1))
@@ -522,7 +540,7 @@ done
# The count is part of the result, not decoration. A harness that passes because # The count is part of the result, not decoration. A harness that passes because
# a chapter stopped matching the extractor looks exactly like a harness that # a chapter stopped matching the extractor looks exactly like a harness that
# passes because the documentation is correct, and this is what tells them apart. # passes because the documentation is correct, and this is what tells them apart.
echo "ran: ${RAN[basic]} programs, ${RAN[repl]} transcripts, ${RAN[output]} output comparisons, ${RAN[c]} C snippets, ${RAN[excerpt]} excerpts, ${RAN[sh]} shell blocks" echo "ran: ${RAN[basic]} programs, ${RAN[repl]} transcripts, ${RAN[output]} output comparisons, ${RAN[c]} C snippets, ${RAN[excerpt]} excerpts, ${RAN[sh]} shell blocks, ${RAN[screenshot]} figures"
echo "skipped: ${SKIPPED[norun]} norun, ${SKIPPED[akgl]} needing akgl, ${SKIPPED[cmake]} cmake, ${SKIPPED[text]} text, ${SKIPPED[nocc]} C (no compiler flags given)" echo "skipped: ${SKIPPED[norun]} norun, ${SKIPPED[akgl]} needing akgl, ${SKIPPED[cmake]} cmake, ${SKIPPED[text]} text, ${SKIPPED[nocc]} C (no compiler flags given)"
if [ "${FAILURES}" -eq 0 ]; then if [ "${FAILURES}" -eq 0 ]; then

215
tools/docs_screenshots.sh Executable file
View File

@@ -0,0 +1,215 @@
#!/bin/bash
#
# Regenerate every figure in the documentation from the listing shown beside it.
#
# A chapter that says "CIRCLE draws ellipses" and shows a listing wants a picture
# of what that listing draws, and the failure mode of a picture is that it stops
# being of the code next to it. Nothing tells you: a screenshot taken by hand in
# 2026 still looks like a screenshot in 2027, long after the verb it illustrates
# has changed.
#
# So the figure is not an asset, it is *output*. A block tagged
#
# ```basic requires=akgl screenshot=circle
#
# is run by tools/screenshot.c and written to docs/images/circle.png, and the
# chapter shows that file. Regenerate and the picture follows the code. The
# generated PNGs are checked in on purpose -- a reader on the forge has no build
# tree -- and tests/docs_examples.sh fails if a tagged block has no image, so a
# new figure cannot be forgotten.
#
# **This is not run by the build.** `cmake --build build-akgl --target
# docs_screenshots` is deliberate; see the target's comment in CMakeLists.txt.
#
# Exit status is the number of figures that failed, the house convention.
set -u
ROOT=""
TOOL=""
CHECK=0
FAILURES=0
usage()
{
cat >&2 <<'EOF'
usage: docs_screenshots.sh --root DIR --tool PATH [--check] [FILE...]
--root DIR repository root; images are written to DIR/docs/images
--tool PATH the built akbasic_screenshot
--check render to a scratch directory and compare, changing nothing
FILE... which documents to regenerate (default: docs/*.md)
EOF
exit 2
}
while [ $# -gt 0 ]; do
case "$1" in
--root) ROOT="$2"; shift 2 ;;
--tool) TOOL="$2"; shift 2 ;;
--check) CHECK=1; shift ;;
--help|-h) usage ;;
--*) echo "unknown option $1" >&2; usage ;;
*) break ;;
esac
done
[ -n "${ROOT}" ] || usage
[ -n "${TOOL}" ] || usage
[ -x "${TOOL}" ] || { echo "FAIL: no screenshot tool at ${TOOL}" >&2; exit 2; }
# Absolute, because the loop below cd's into a sandbox to run each program --
# a listing that loads an asset resolves it relative to its own directory.
case "${TOOL}" in
/*) ;;
*) TOOL="${PWD}/${TOOL}" ;;
esac
cd "${ROOT}" || exit 2
ROOT="${PWD}"
DOCS=("$@")
if [ ${#DOCS[@]} -eq 0 ]; then
DOCS=(docs/*.md)
fi
IMAGES="${ROOT}/docs/images"
mkdir -p "${IMAGES}" || exit 2
WORK="$(mktemp -d)"
trap 'rm -rf "${WORK}"' EXIT
# --check renders somewhere else entirely and compares, so a run that finds a
# stale figure does not also fix it. A test that repairs what it is measuring
# passes the second time for the wrong reason.
if [ "${CHECK}" -eq 1 ]; then
OUTDIR="${WORK}/rendered"
mkdir -p "${OUTDIR}" || exit 2
else
OUTDIR="${IMAGES}"
fi
# The value of attribute $1 in an info string $2, or empty. Same shape as the
# `attr` in tests/docs_examples.sh, and deliberately so -- the two read the same
# fence tags and a second dialect of them would be a bug waiting to happen.
attr()
{
local name="$1" info="$2" word
for word in ${info}; do
case "${word}" in
"${name}"=*) echo "${word#*=}"; return 0 ;;
esac
done
echo ""
}
# One figure: write the listing to a file, run it, keep the PNG.
render()
{
local name="$1" body="$2" size="$3" where="$4" setup="$5"
local w=320 h=200 out="${OUTDIR}/${name}.png" log=""
if [ -n "${size}" ]; then
w="${size%x*}"
h="${size#*x}"
fi
# The same setup scripts tests/docs_examples.sh uses, run in the same place
# relative to the program. A listing that loads `ship.png` needs one whether
# it is being checked or being photographed.
if [ -n "${setup}" ]; then
if [ ! -x "${ROOT}/tests/docs_setups/${setup}.sh" ]; then
echo "FAIL ${where}: no setup script ${setup}.sh" >&2
FAILURES=$((FAILURES + 1))
return
fi
( cd "${WORK}" && "${ROOT}/tests/docs_setups/${setup}.sh" ) || {
echo "FAIL ${where}: setup ${setup} failed" >&2
FAILURES=$((FAILURES + 1))
return
}
fi
printf '%s' "${body}" > "${WORK}/${name}.bas"
# **stdout only.** The tool puts the interpreter's sink on stdout and libakgl
# logs its registry chatter -- "Actor akbasic:actor:1 initialized" -- to
# stderr, so merging the two would make every sprite figure look like a
# failing program. stderr is shown when the tool itself refuses, which is
# when it is worth reading.
if ! log="$(cd "${WORK}" && "${TOOL}" "${name}.bas" "${out}" "${w}" "${h}" \
2>"${WORK}/${name}.err")"; then
echo "FAIL ${where}: ${name} did not render" >&2
cat "${WORK}/${name}.err" >&2
FAILURES=$((FAILURES + 1))
return
fi
# A program that raised prints its error line and still exits zero, because
# a BASIC error is the script's and not the host's. For a figure that is
# still a failure: an image of a blank screen is worse than no image.
if [ -n "${log}" ]; then
echo "FAIL ${where}: ${name} rendered, but the program reported:" >&2
echo "${log}" >&2
FAILURES=$((FAILURES + 1))
return
fi
if [ "${CHECK}" -eq 1 ]; then
if [ ! -r "${IMAGES}/${name}.png" ]; then
echo "FAIL ${where}: docs/images/${name}.png does not exist" >&2
FAILURES=$((FAILURES + 1))
return
fi
if ! cmp -s "${out}" "${IMAGES}/${name}.png"; then
echo "FAIL ${where}: docs/images/${name}.png is not what that listing draws" >&2
echo " regenerate with: cmake --build <akgl build> --target docs_screenshots" >&2
FAILURES=$((FAILURES + 1))
return
fi
echo " ${name}.png matches"
return
fi
echo " ${name}.png (${w}x${h})"
}
for DOC in "${DOCS[@]}"; do
[ -r "${DOC}" ] || { echo "FAIL: no document at \"${DOC}\"" >&2; exit 2; }
# Walk the file collecting fenced blocks. Only `screenshot=` ones are kept;
# everything else is somebody else's problem, and docs_examples.sh is that
# somebody.
IN_BLOCK=0
INFO=""
BODY=""
START=0
LINENO=0
while IFS= read -r LINE; do
LINENO=$((LINENO + 1))
case "${LINE}" in
'```'*)
if [ "${IN_BLOCK}" -eq 0 ]; then
IN_BLOCK=1
INFO="${LINE#'```'}"
BODY=""
START="${LINENO}"
else
IN_BLOCK=0
NAME="$(attr screenshot "${INFO}")"
if [ -n "${NAME}" ]; then
render "${NAME}" "${BODY}" "$(attr size "${INFO}")" \
"${DOC}:${START}" "$(attr setup "${INFO}")"
fi
fi
;;
*)
if [ "${IN_BLOCK}" -eq 1 ]; then
BODY="${BODY}${LINE}"$'\n'
fi
;;
esac
done < "${DOC}"
done
if [ "${FAILURES}" -ne 0 ]; then
echo "${FAILURES} figure(s) failed" >&2
fi
exit "${FAILURES}"

217
tools/screenshot.c Normal file
View File

@@ -0,0 +1,217 @@
/**
* @file screenshot.c
* @brief Run one BASIC program against an offscreen renderer and save a PNG.
*
* The documentation's graphics and sprite chapters describe what a verb draws,
* and a sentence is a poor way to describe a picture. This is what turns the
* listing in a chapter into the image beside it -- tools/docs_screenshots.sh
* extracts the listing from the markdown and hands it here, so the picture is
* generated from the exact program the reader is looking at rather than from a
* copy that can drift away from it.
*
* **It is a host, like src/frontend_akgl.c**, and it is deliberately a separate
* one: the frontend opens a real window, pumps events and never stops until the
* script quits, none of which a build step wants. What this does instead is the
* shortest path to pixels -- the dummy video driver, a software renderer, run to
* completion, read the target back, write it out. deps/libakgl/tests/draw.c and
* tests/akgl_backends.c set that pattern; this is the third user of it.
*
* **The text layer is deliberately not drawn.** A screenshot in the graphics
* chapter is of the drawing, and a `READY` in the corner is noise in a figure
* about `CIRCLE`. It also means no font has to be found, which keeps this
* runnable from a build tree with no assets resolved.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
#include <akerror.h>
#include <akgl/error.h>
/*
* game.h for the `renderer` and `camera` globals: akgl_actor_render() reads
* both without taking either as an argument, so a host that never calls
* akgl_game_init() -- which is every host here, because owning the game loop is
* what goal 3 forbids -- populates them itself. Same three lines as
* src/frontend_akgl.c and tests/akgl_backends.c, and each says so.
*/
#include <akgl/game.h>
#include <akgl/heap.h>
#include <akgl/registry.h>
#include <akgl/renderer.h>
#include <akbasic/akgl.h>
#include <akbasic/error.h>
#include <akbasic/runtime.h>
#include <akbasic/sink.h>
/** @brief The interpreter carries every pool it owns, so it does not fit on a stack. */
static akbasic_Runtime RUNTIME;
static akbasic_TextSink SINK;
static akbasic_StdioSink SINKSTATE;
static akbasic_GraphicsBackend GRAPHICS;
static akbasic_AkglGraphics GRAPHICSSTATE;
static akbasic_SpriteBackend SPRITES;
static akbasic_AkglSprites SPRITESSTATE;
/*
* No `window` of our own: akgl/game.h declares one as an unprefixed extern --
* libakgl's TODO calls that a defect and it is -- so a static here shadows it
* and the file will not compile. Writing the global is what a host does anyway.
*/
/** @brief The whole program, read in one go. Longer than any figure's listing. */
static char SOURCE[65536];
static void usage(void)
{
fprintf(stderr,
"usage: akbasic_screenshot <program.bas> <output.png> [width] [height]\n"
"\n"
" Runs the program against an offscreen renderer of the given size\n"
" (default 320x200) and writes what it drew as a PNG.\n");
}
/** @brief Slurp the program. A figure's listing is small; a partial read is not tolerated. */
static akerr_ErrorContext AKERR_NOIGNORE *read_program(const char *path)
{
PREPARE_ERROR(errctx);
FILE *in = NULL;
size_t got = 0;
memset(SOURCE, 0, sizeof(SOURCE));
in = fopen(path, "r");
FAIL_ZERO_RETURN(errctx, (in != NULL), AKERR_IO, "could not open %s", path);
got = fread(SOURCE, 1, sizeof(SOURCE) - 1, in);
fclose(in);
FAIL_ZERO_RETURN(errctx, (got > 0), AKERR_IO, "%s is empty", path);
FAIL_ZERO_RETURN(errctx, (got < sizeof(SOURCE) - 1), AKERR_OUTOFBOUNDS,
"%s is longer than %zu bytes", path, sizeof(SOURCE) - 1);
SUCCEED_RETURN(errctx);
}
/**
* @brief Everything between SDL being up and the pixels being readable.
*
* Split out so the ATTEMPT in main() has one thing to CATCH and the SDL
* teardown has one place to happen.
*/
static akerr_ErrorContext AKERR_NOIGNORE *draw_program(const char *outpath, int w, int h)
{
PREPARE_ERROR(errctx);
SDL_Surface *shot = NULL;
PASS(errctx, akgl_error_init());
renderer = &_akgl_renderer;
FAIL_ZERO_RETURN(errctx, SDL_Init(SDL_INIT_VIDEO), AKGL_ERR_SDL,
"Couldn't initialize SDL: %s", SDL_GetError());
FAIL_ZERO_RETURN(errctx,
SDL_CreateWindowAndRenderer("net/aklabs/akbasic/screenshot", w, h, 0,
&window, &renderer->sdl_renderer),
AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError());
PASS(errctx, akgl_render_bind2d(renderer));
PASS(errctx, akgl_heap_init());
PASS(errctx, akgl_registry_init());
camera = &_akgl_camera;
camera->x = 0.0f;
camera->y = 0.0f;
camera->w = (float32_t)w;
camera->h = (float32_t)h;
/*
* Opaque black, so a figure has a defined background rather than whatever
* the driver left in the buffer. GRAPHIC's own clear is a filled rectangle
* over the output for a reason src/graphics_akgl.c explains -- clearing is
* the host's prerogative -- and this is the host doing it.
*/
FAIL_ZERO_RETURN(errctx, SDL_SetRenderDrawColor(renderer->sdl_renderer, 0, 0, 0, 0xff),
AKGL_ERR_SDL, "%s", SDL_GetError());
FAIL_ZERO_RETURN(errctx, SDL_RenderClear(renderer->sdl_renderer),
AKGL_ERR_SDL, "%s", SDL_GetError());
/* stdout: a program that errors puts its "? line : CLASS message" there,
which is what the caller shows when a figure comes out wrong. */
PASS(errctx, akbasic_sink_init_stdio(&SINK, &SINKSTATE, stdout, NULL));
PASS(errctx, akbasic_runtime_init(&RUNTIME, &SINK));
PASS(errctx, akbasic_graphics_init_akgl(&GRAPHICS, &GRAPHICSSTATE, renderer));
PASS(errctx, akbasic_sprite_init_akgl(&SPRITES, &SPRITESSTATE, renderer, &GRAPHICSSTATE));
PASS(errctx, akbasic_runtime_set_devices(&RUNTIME, &GRAPHICS, NULL, NULL, &SPRITES));
PASS(errctx, akbasic_runtime_load(&RUNTIME, SOURCE));
PASS(errctx, akbasic_runtime_start(&RUNTIME, AKBASIC_MODE_RUN));
PASS(errctx, akbasic_runtime_run(&RUNTIME, 0));
/*
* The drawing verbs are immediate and are already on the target. Sprites are
* not: they are libakgl actors and reach the target through a render pass,
* which in the standalone frontend is part of a frame. There is no frame
* here, so this is it.
*/
PASS(errctx, akbasic_sprite_akgl_render(&SPRITES));
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
FAIL_ZERO_RETURN(errctx, (shot != NULL), AKGL_ERR_SDL,
"Couldn't read the target back: %s", SDL_GetError());
if ( !IMG_SavePNG(shot, outpath) ) {
SDL_DestroySurface(shot);
FAIL_RETURN(errctx, AKGL_ERR_SDL, "Couldn't write %s: %s", outpath, SDL_GetError());
}
SDL_DestroySurface(shot);
SUCCEED_RETURN(errctx);
}
int main(int argc, char **argv)
{
PREPARE_ERROR(errctx);
int w = 320;
int h = 200;
if ( argc < 3 ) {
usage();
return 2;
}
if ( argc > 3 ) {
w = atoi(argv[3]);
}
if ( argc > 4 ) {
h = atoi(argv[4]);
}
if ( w <= 0 || h <= 0 ) {
usage();
return 2;
}
/*
* Set here rather than left to the environment so the tool answers the same
* on a developer's desktop as it does in CI. A figure regenerated over a
* real GPU could differ by a pixel of antialiasing and turn every rebuild
* into a diff.
*/
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software");
ATTEMPT {
CATCH(errctx, read_program(argv[1]));
CATCH(errctx, draw_program(argv[2], w, h));
} CLEANUP {
if ( window != NULL ) {
SDL_DestroyWindow(window);
window = NULL;
}
SDL_Quit();
} PROCESS(errctx) {
} HANDLE_DEFAULT(errctx) {
LOG_ERROR_WITH_MESSAGE(errctx, "could not draw the screenshot");
return 1;
/*
* FINISH_NORETURN rather than FINISH, matching src/main.c and
* tests/akgl_backends.c: FINISH expands a `return __err_context` that
* this int-returning function cannot compile even where the branch is
* unreachable.
*/
} FINISH_NORETURN(errctx);
return 0;
}