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>
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -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
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
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.
|

|
||||||
|
|
||||||
|
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
|

|
||||||
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
|

|
||||||
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
|
||||||
```
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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
|
||||||
```
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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 —
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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
@@ -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
|
After Width: | Height: | Size: 1.5 KiB |
BIN
docs/images/circle.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
docs/images/draw.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
docs/images/paint.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
docs/images/scale.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
docs/images/shapes.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
docs/images/sprite-collision.png
Normal file
|
After Width: | Height: | Size: 739 B |
BIN
docs/images/sprites.png
Normal file
|
After Width: | Height: | Size: 904 B |
@@ -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
@@ -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
@@ -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;
|
||||||
|
}
|
||||||