From 16b38c113817f89330f8bd70e183683183103090 Mon Sep 17 00:00:00 2001 From: Andrew Kesterson Date: Sat, 1 Aug 2026 07:37:04 -0400 Subject: [PATCH] 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) --- .gitea/workflows/ci.yaml | 5 + CMakeLists.txt | 42 ++++++ MAINTENANCE.md | 33 +++++ TODO.md | 83 ++++++++++-- docs/06-graphics.md | 111 ++++++++++++---- docs/08-sprites.md | 50 ++++++- docs/images/README.md | 21 +++ docs/images/box.png | Bin 0 -> 1557 bytes docs/images/circle.png | Bin 0 -> 1813 bytes docs/images/draw.png | Bin 0 -> 1468 bytes docs/images/paint.png | Bin 0 -> 1232 bytes docs/images/scale.png | Bin 0 -> 3207 bytes docs/images/shapes.png | Bin 0 -> 1728 bytes docs/images/sprite-collision.png | Bin 0 -> 739 bytes docs/images/sprites.png | Bin 0 -> 904 bytes tests/docs_examples.sh | 22 +++- tools/docs_screenshots.sh | 215 ++++++++++++++++++++++++++++++ tools/screenshot.c | 217 +++++++++++++++++++++++++++++++ 18 files changed, 759 insertions(+), 40 deletions(-) create mode 100644 docs/images/README.md create mode 100644 docs/images/box.png create mode 100644 docs/images/circle.png create mode 100644 docs/images/draw.png create mode 100644 docs/images/paint.png create mode 100644 docs/images/scale.png create mode 100644 docs/images/shapes.png create mode 100644 docs/images/sprite-collision.png create mode 100644 docs/images/sprites.png create mode 100755 tools/docs_screenshots.sh create mode 100644 tools/screenshot.c diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 007ce07..fc104ff 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -214,6 +214,11 @@ jobs: # 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 # 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 # 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 diff --git a/CMakeLists.txt b/CMakeLists.txt index a57c0a5..b3a2278 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -192,6 +192,26 @@ if(AKBASIC_WITH_AKGL) target_compile_options(akbasic_frontend PRIVATE -Wall -Wextra) target_link_libraries(akbasic_frontend PUBLIC akbasic_akgl) 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 "$" + DEPENDS akbasic_screenshot + COMMENT "Regenerating the documentation figures in docs/images" + VERBATIM) endif() # --------------------------------------------------------------------------- @@ -370,6 +390,28 @@ if(AKBASIC_WITH_AKGL) WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" TIMEOUT 180 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 $ + --check) + _set_tests_properties(docs_screenshots PROPERTIES + TIMEOUT 120 + ENVIRONMENT "SDL_VIDEODRIVER=dummy;SDL_AUDIODRIVER=dummy;SDL_RENDER_DRIVER=software") endif() # --------------------------------------------------------------------------- diff --git a/MAINTENANCE.md b/MAINTENANCE.md index 046f03e..9e2fa12 100644 --- a/MAINTENANCE.md +++ b/MAINTENANCE.md @@ -576,9 +576,42 @@ a missing decision rather than a free pass. | `sh norun` | Shown, not run | | `cmake` | **Never executed.** Hand-maintained, by decision | | `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`. +### 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 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 diff --git a/TODO.md b/TODO.md index 37ec7e6..d3fedb5 100644 --- a/TODO.md +++ b/TODO.md @@ -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 deliberately poking at it. -16. **`BOX` fills on a negative angle rather than on a seventh argument.** 7.0's last - argument selects outline or fill and sits *after* the rotation, which would make a filled - box a seven-argument call. That is filed rather than implemented; today a rotation of zero - outlines through the renderer's rectangle and any other rotation draws four lines. +16. **`BOX` cannot fill at all, and the plan for it is to spell fill as a negative angle.** + 7.0's last argument selects outline or fill and sits *after* the rotation, which would + make a filled box a seven-argument call — one more than `akbasic_cmd_box()` collects. + 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 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 | |---|---| | `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 | -| `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 | +| `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: 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 | -| ASan + UBSan | 77/77 | -| Line coverage | 94.6% (3857/4076) — above the 90% gate | -| Function coverage | 98.6% (287/291) | +| ASan + UBSan | 95/95 | +| Line coverage | 95.1% (5916/6222) — above the 90% gate | +| Function coverage | 98.5% (403/409) | | Warnings | none under `-Wall -Wextra` | | `doxygen Doxyfile` | clean | | 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 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 `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 @@ -1856,6 +1878,45 @@ What remains, in priority order: 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. -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. diff --git a/docs/06-graphics.md b/docs/06-graphics.md index e735f2d..e26d11f 100644 --- a/docs/06-graphics.md +++ b/docs/06-graphics.md @@ -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 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 -```basic requires=akgl -10 DRAW 1, 10, 20 -20 DRAW 1, 0, 0 TO 100, 100 TO 200, 0 +```basic requires=akgl screenshot=draw +10 COLOR 1, 8 +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 -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 -```basic requires=akgl -10 BOX 1, 10, 10, 40, 40 +```basic requires=akgl screenshot=box +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 -```basic requires=akgl -10 CIRCLE 1, 160, 100, 50, 30 +```basic requires=akgl screenshot=circle +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 -start angle, an end angle, a rotation and the degree increment, which is what makes it -an arc or a polygon. +![](images/circle.png) + +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 -```basic requires=akgl -10 PAINT 1, 160, 100 +```basic requires=akgl screenshot=paint +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 -own working space it stops and reports rather than leaving a half-painted screen with -no explanation. +![](images/paint.png) + +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 @@ -110,10 +153,23 @@ coordinates finishes. ### SCALE -```basic requires=akgl -10 SCALE 1, 1023, 1023 +```basic requires=akgl screenshot=scale size=640x400 +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 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 @@ -137,12 +193,21 @@ parallel passes; see Chapter 13. `SSHAPE` copies a rectangle off the screen and `GSHAPE` stamps it back: -```basic requires=akgl -10 BOX 1, 0, 0, 20, 20 -20 SSHAPE A$, 0, 0, 20, 20 -30 GSHAPE A$, 100, 100 +```basic requires=akgl screenshot=shapes +10 COLOR 1, 8 +20 CIRCLE 1, 40, 40, 30, 30 +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 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 — diff --git a/docs/08-sprites.md b/docs/08-sprites.md index 0819b97..a3e3fcc 100644 --- a/docs/08-sprites.md +++ b/docs/08-sprites.md @@ -58,12 +58,33 @@ operation. ## Showing and moving -```basic requires=akgl setup=ship -10 SPRSAV "ship.png", 1 -20 SPRITE 1, 1, 3 -30 MOVSPR 1, 100, 50 +```basic requires=akgl screenshot=sprites +10 COLOR 1, 2 +20 CIRCLE 1, 20, 20, 18, 18 +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 number is required and **an argument you leave out is left alone**, so `SPRITE 1, 1` 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 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 | Function | Gives | diff --git a/docs/images/README.md b/docs/images/README.md new file mode 100644 index 0000000..79abb9e --- /dev/null +++ b/docs/images/README.md @@ -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. diff --git a/docs/images/box.png b/docs/images/box.png new file mode 100644 index 0000000000000000000000000000000000000000..e27c926e63affcc23d1ffb206d72a2de33fa9140 GIT binary patch literal 1557 zcmb_c`!~}K7@ujF)+v;}!$?^eWnbN1n_H&2%Y8|<*`iYFMHkem(P?JQhBs|*m{`cY zIY}2oXcQG8U9?;hjfh#U^}eff-v8izet6FFJm>R#KF@Q`bDlImU$-^d2HFq^WQ~Wr zYXAhI=B$d_wV*1mA62~qfxrelT=xbg53Q%WkIB5*;9)VIZHt)Ue)8KuxV3dgkIw4G zCeRQBWqS^yWm8^rF}vWA45>#Sk34X<6?kW8gh4W+T!wG43(}i5rGZ9SrcN{je$>NP zn7woEOZ(bOKfb$_pRt}&jPS?Nr(-WVO6(=X>fux`X5cok0_fQ-^Tx|l$6-xJ z`5LdKJt1w>K(5&HNpR>`^@_d(!kU|%!}V~N90Nnz5)|oTnlW2?K)vN&%=?a>iK2AT zM%%6neLA!nFyy6kH%3OUv9fvQP0#)Os#=%c=KhU~=H7@Z<2v_n#IBKFGZy0$(_kio z@3)WLkh6BR0p|4$|CmQ`9MoGXqt0W6B3!c%R(e9Rvk766m||sfSx(O#3Xq{&xbO50 z&SDxxRb-UMHDh`L46V~AkvXf z-GBp3KnZAEgkuk%9{CW+NX%c#qs%IJ7+r36w|~X^vFc<~{-f`X(9W>kFRy4}&Elq2 zsKvF)u&WS)K7L9F?OZEAw+;T%3FB!?baNB0c%p7c&&%8xZ+GX6)vN+iP!G;)W94;N zHKx1fv=Ca4Q$DIP8#C#H{T3Ke^f#>&JyqfRBNg^)YD`F_inHEAiPuzPb{ohXUa;Z2Bv(Fhl3NG80=fB}&p`2w)NxbsOL@?lll$ zeuggWtZyB5Z18m89qh{t=~n46dDBQio;|=P-Nn`XP30gT9=(HfoLxmCYs7P6OHpTp z`=eL;7@qA_cKV}+J{yh<#N^6FcEmylN>NorgIo)o0mjOQPnfjrz@iedzA?jc;8-T; z+a>Mm|L65)68V%@y7;6d~k_b8l@lFbsVHMT8?*sxsvV&02zW=iPy0IX(B-MH``a*7Y&y$F-c%iD^D01Fk80t~DEPmU^M5QTe%kB)_l_ZcZ=3|^Zl8QJ R^O;{YkA1$bRW8(&e*wt~(un{7 literal 0 HcmV?d00001 diff --git a/docs/images/circle.png b/docs/images/circle.png new file mode 100644 index 0000000000000000000000000000000000000000..1f16528e62a18daba15d829a7196ec20aaf0efb4 GIT binary patch literal 1813 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 literal 0 HcmV?d00001 diff --git a/docs/images/scale.png b/docs/images/scale.png new file mode 100644 index 0000000000000000000000000000000000000000..5bd8ecd3913083fe66dc0bead01bafc189113b0c GIT binary patch literal 3207 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 E|6y9{Q2+n{ literal 0 HcmV?d00001 diff --git a/docs/images/shapes.png b/docs/images/shapes.png new file mode 100644 index 0000000000000000000000000000000000000000..3bfe2f4522f34a17d36fd29298f2e863a910e2bd GIT binary patch literal 1728 zcmY*aeK-?pAKvC;<`_jdO9!N$zlpB-2mokb^7Yz(gdtoUyMOKl$xth(XAlV|NTZ7B zQC5c6{0Td{ireT?#!-{OppTsQbY+{%V8TFUdIsZM!BvBHdv%@9!@jrXZrXz(WoOyu z#-ei8j{s5~vzDApFzA$(!B}bRxXpibQ-|IH6Z#6T%Zaw#6g_8~9TTD(%z(^EUI(ZN zpF((3gL2g4f1|Th*=JrsX+S$mBF16y1f|f5m(&Yp6g(G~{X||F7s3?ZiT~ANOiL-} zmj133?&3|(Y3tWKSkmC=A)S|`zXjJHlTl{NwXjE13dE;|6|^TR7#o0_niEgIFz}=C zk!Wh``q_0mGV@X6WFH`gbg{X7h+i(A9^FUY?i!PFsU3{z^b6@}Tc%@+%A0>iR$Vg1 zE-taE3-*sFXuD#*w&p*s&ReW!vD3P!aZ0lWFBL{aRNqr%ACE#mx>GN#$b7oo8uGsL zl&$;mCxzN}Xb*!JR6DITkoF$9v=dIR#qqm-=eM{UP54~wmv>QJKzbPvs^HA3eD7qa zCKmrT+rk5f2kk_XRhT$~Orlt;pH9C0DT1tWhfCFT8(J{!9{7vs4dDe)jO+84yh^t2NT3>@39vYub4gWDP zCr2g;8a@Dg645%Jj0iYGGd0m=q9xaeBw}rtktsHYC*r$1c^)9kK*Fl@0hF6_Bllet zDP^+zYgb&`BzQ@Php(ts5%b(<3+>Ocgv|SQHm|R} zGgDGwV`0y)oTmh?3gi`*r<6*m(Y*U`46v%X>-&4ep*y)(RwqSc3XRw?JED} z#>lmM>CVlRl&>(I>VC8F#O)#T>H}-MpSsN0QBF7&mB-5-O)}QcDPAvpq&>!Te7Jf} z_p#G8kPg&inqc!^KGzUIGTFFe8D!*4fm_Ht4~<_!-V6b zr#DY0y3C@f4%2_+^KXrpQy}pZlxlJ7g6+8F6;ffgg6-&8cf<2t(&2=k2LGlWVsDK6 zvPsLk-x0CZA-t9|Q-FK$FZqnWKp(irLQ2AZc*N$RVenn$K6Px}TvK3+=%LQ7=;LGe9@MC-ID$AXP6Y#_nZAyr`%}Ph z^zKyFw)txP$bXs&Ot|i&1SQ5)V846c@WHLXN8JvAV{#q)2P5BB)R?~rZYmTd8%E}k z_DNOS-}rF4DOq~T&?Yz#$rn*Z*>p1?vUC}8@^CKB&M_G_hUfS$j?{tF?s65TvuIHd z2iir?x1pHq{b}s9mGu_PWx!Bm*%SdJEc7-{Fq~*&{ipQ=Ncc+@6z9p2-M*XM38C6y zlunlcF}Dvchac(?HNvnzXPb>WvF<|yWs>C^Th3zVr*YFT{KrYKBneHewTS2&CdsS_ z9Jt6Y5#Dy!q@=_i`bndH4TjL2C+AOa&4{UtOiir+lXW$jCj88&z<&MSS-pOQ`dR%o z606is;fM8rs?~099G$K>QnIC2%bTBNscE8Xq=tRssZ~=Drr(+Cmh1Q_)!2dm_tgKN ze**WYBw^NcVr_5sTeXn4M2~+7K$OS6OMv%_pq(gRo~V8goUW_(U^fpyQ7N}hXMsEx#@7z8DAPWl7-x#* zXE902U`xr|e^G}b+@w_uQN1fL;PilrwIV@>8Xg}-TE2ki)P-3|Z^#kAYBH;J_?gAk tD?M8~yP3ba>b5nW*j@bP*Z8u2wWe{07Vg2Rol$P3e7ys_sy!or_!DnXARhn# literal 0 HcmV?d00001 diff --git a/docs/images/sprite-collision.png b/docs/images/sprite-collision.png new file mode 100644 index 0000000000000000000000000000000000000000..f46db1594513d2e8bb1613b784988f8e02db21d0 GIT binary patch literal 739 zcmeAS@N?(olHy`uVBq!ia0y~yU~~YoPjD~+NwKu`e;62;R(ZNOhE&XXdv~MnVF!t} zL;Z7~S~#g8RqvW$pfKr)4}3 z?3Fb(|7YiR(r0t)yY$_+=lr^IJXmQN=Z?uYHM3@LGQCSoY>|4V&lDFs`H#sNN2ay} zrnvCKQ!gu>v14>LU|hfA=d4V@XW}3>9JL!Bf27XX{;^KTo>QU;iYi>sz7N04@VOi6 zjO$kmE@%G-nzDYKXT__h3XI=37}$UPB+giFWbO3T$nn5moBR9cL@lnG`Rp{$-deMl zm$eGwrtMLGl7D~Z^P(@TS2o_w{k3LJf4RBrxlQjMe$72mIWvJ!?7zEDhvW5>+A^L2T5`#!lIIO@wkpJ&=-&K+x#pB=k@T*GpTVwfY) zsO5HFKTgq@(y)5w+(m7|oI9qp3T9dKHN4K4G{0>#2hcA&I@+|CyIORBJfWSjFDY>1O4Sd?iNlFCTuDW%a(|eR`TF6~*J-8(aUy>+C!Vw{oVFyHOA*#Zd$nH1SV9 V+j(h?=NBiCIi9Y5F6*2UngBqm62t%i literal 0 HcmV?d00001 diff --git a/docs/images/sprites.png b/docs/images/sprites.png new file mode 100644 index 0000000000000000000000000000000000000000..8c2746886c6b88c8006f1547427190342744ffd8 GIT binary patch literal 904 zcmeAS@N?(olHy`uVBq!ia0y~yU~~YoPjD~+NwKu`e;62;{XJbALn`LHy?Ze5c7RB0 z;zUo^mkQoo3G<~iR7^McGe%xw)V1V3E8xp>Fk$cScY^n~f2-JI`T6xpF&%E8Ddd8l za^v632jctvzwh32{(HWCdFA|n`_BAhIB;CkVDj70yVqrQALHUVC^Cc7E&RLb@zood zoDEo)omlf~-!lo*ump}}2Y9Zp-5dQ<7Q()=b8qxcD0||iduxyIX&bN@Cvc=45I7?_ zW3KAFy6v0%4gn3^ckUm<=}kJzFTXr{{mvbk=SdIWJ^Xg|`k!APcR!dsA@5Uz{>*pZ zroJ@Zm$K`2_$d~M7Zs!!q<@4*d2l*Wm=BhbcviOYPd%*RmhO9>?szCzyXUaSC6>_miggKfIf~Iq_d$xu{iMj_vxT$9va4-1=pr z-~Q;o{)hIlw5NyS;*AfC)3!4ztIUC9gawyJnNL_q&9R8U`*PtE6W?DG$Bi_A_oyE*rt*-O_=|F{2O)KsyG?;A_w-Ww(4%)NeE zAv6ZRHx6excg;t z!Ks(!rauc0oz9+PclYX3Q@=TSU!!dJYwXTlep%9Ubl;1bIpzG@#ZI%E{FVE0;C1c9 zub&L(XRA-~wBbyjTNm!j%@cE|L2(AqU+D>KUY4Bd^XkIEspX+nxyAOq+N^B}Ts{-? zWc~8r1A}k|lkvedQu2#;MT6Cxbi4ke;_BTXP$Dwf6yDn(dsmm0ZNaI>mut7LpQ0X= gKyI35U}*ZosAGOX^ylAIbs$4LUHx3vIVCg!0Mr9`kN^Mx literal 0 HcmV?d00001 diff --git a/tests/docs_examples.sh b/tests/docs_examples.sh index 0bd5bb7..65a8171 100755 --- a/tests/docs_examples.sh +++ b/tests/docs_examples.sh @@ -24,6 +24,8 @@ # ```basic norun a fragment, shown but not run # ```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 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 # ```c a translation unit: compile it with -fsyntax-only # ```c wrap=NAME the same, wrapped in tests/docs_preludes/NAME.pre/.post @@ -128,7 +130,7 @@ fi CC="${CC:-cc}" 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) # 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 # 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. + # 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 --target docs_screenshots\`" + fi + fi + REQUIRES="$(attr requires "${INFO}")" if [ "${REQUIRES}" = "akgl" ] && [ "${WITH_AKGL}" -eq 0 ]; then SKIPPED[akgl]=$((SKIPPED[akgl] + 1)) @@ -522,7 +540,7 @@ done # 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 # 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)" if [ "${FAILURES}" -eq 0 ]; then diff --git a/tools/docs_screenshots.sh b/tools/docs_screenshots.sh new file mode 100755 index 0000000..5b60969 --- /dev/null +++ b/tools/docs_screenshots.sh @@ -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 --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}" diff --git a/tools/screenshot.c b/tools/screenshot.c new file mode 100644 index 0000000..6fd2451 --- /dev/null +++ b/tools/screenshot.c @@ -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 +#include +#include + +#include +#include + +#include + +#include +/* + * 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 +#include +#include +#include + +#include +#include +#include +#include + +/** @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 [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; +}