Wire the sink and the three devices to libakgl

src/sink_akgl.c, src/graphics_akgl.c, src/audio_akgl.c and src/input_akgl.c, in
the akbasic_akgl target, which is the only thing here that links SDL.
-DAKBASIC_WITH_AKGL=ON had never been configured in this repository before, and
it now builds and passes.

The sink is what section 3 has been waiting on. Its character grid comes from
akgl_text_measure(font, "A", &w, &h), the direct equivalent of the reference's
font.SizeUTF8("A") and the call that did not exist until 42b60f7. Wrapping is
done on the character grid rather than by handing SDL_ttf a wraplength, because
the cursor has to land somewhere definite: a program that PRINTs a long string
and then PRINTs again expects the second to start on the row after the first
ended, and only the code that placed the characters knows which row that is.

tests/akgl_backends.c draws into a 128x128 software renderer under the dummy
video driver and reads the pixels back -- the pattern deps/libakgl/tests/draw.c
established, which needs no display and no offscreen harness. It asserts the
seam rather than libakgl's own behaviour: a BASIC line in, a lit pixel of the
right colour out.

Four things in libakgl had to be worked around to get here. All four are
commented at their site with "filed upstream" and recorded in TODO.md section 3:

- An embedded libakgl requires SDL, SDL_image, SDL_mixer, SDL_ttf and jansson to
  be *installed*. It builds its own vendored copies only when it is top-level,
  and they are sitting right there in deps/libakgl/deps. Every lookup is guarded
  with if(NOT TARGET ...), so this adds those five subdirectories before
  add_subdirectory(deps/libakgl) -- the same trick and the same ordering
  requirement akerror::akerror and akstdlib::akstdlib already need.
- akgl/controller.h does not compile on its own: it declares handlers taking an
  akgl_Actor * and includes nothing that declares the type.
- There is no way to attach a 2D backend to a renderer you already have.
  akgl_render_init2d() installs the vtable but also creates its own window and
  writes the camera global, so it belongs to the akgl_game_init() path -- which
  is exactly the path an embedding host is not on. The test assigns the six
  pointers by hand.
- akgl_text_rendertextat() segfaults on a backend whose vtable is empty; it
  reaches through renderer->draw_texture without checking it. Same class of
  defect 42b60f7's own commit added a draw test for.

The sink's readline reports end of input rather than reading: a drawn text layer
is not a source of lines, and INPUT through one wants a line editor built on the
keystroke ring. EOF rather than an error is the contract sink.h states, so INPUT
already handles it. That editor is the next piece of work there.

70/70 core ctest with no SDL on the include path, 71/71 with the akgl suite,
clean under -Wall -Wextra, doxygen clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 08:46:53 -04:00
parent 83185bafa8
commit 4fc763efb3
9 changed files with 1402 additions and 43 deletions

115
TODO.md
View File

@@ -369,44 +369,72 @@ such ceiling because it called `make()`.
---
## 3. Remaining work: the akgl text sink
## 3. The akgl-backed sink and devices — **done**
**Unblocked as of `libakgl` 42b60f7** — the text-measurement call this waited on now exists.
`-DAKBASIC_WITH_AKGL=ON` builds and its suite passes. Four adaptors in the `akbasic_akgl`
target, which is the only thing here that links SDL:
**`src/sink_akgl.c`.** Implement the §1.5 vtable against `akgl_text_loadfont`,
`akgl_text_rendertextat` and `akgl_text_measure`. Owns the cursor, the wrap, and the scroll —
everything in `basicruntime_graphics.go` except `Write`/`Println`, which are now the sink
interface itself.
| File | Backs |
|---|---|
| `src/sink_akgl.c` | the §1.5 text sink, over `akgl_text_measure` and `akgl_text_rendertextat` |
| `src/graphics_akgl.c` | `akbasic_GraphicsBackend`, over `akgl_draw_*`, and the SSHAPE surface pool |
| `src/audio_akgl.c` | `akbasic_AudioBackend`, over `akgl_audio_*` |
| `src/input_akgl.c` | `akbasic_InputBackend`, over `akgl_controller_poll_key` |
The character grid comes from `akgl_text_measure(font, "A", &w, &h)`, which is the direct
equivalent of the `font.SizeUTF8("A")` at `basicruntime.go:96` that the reference derives
`maxCharsW`/`maxCharsH` from. For wrapping, `akgl_text_measure_wrapped` takes the same
`wraplength` argument `akgl_text_rendertextat` already does, so the measurement and the draw
cannot disagree about where a line breaks.
The character grid comes from `akgl_text_measure(font, "A", &w, &h)` the direct equivalent of
the `font.SizeUTF8("A")` at `basicruntime.go:96`, and the call this was blocked on until
42b60f7. Wrapping is done on the character grid rather than by handing SDL_ttf a `wraplength`,
because the cursor has to land somewhere definite: a program that `PRINT`s a long string and
then `PRINT`s again expects the second to start on the row after the first ended, and only the
code that placed the characters knows which row that is.
**The interpreter does not own the window, the renderer, or the game loop.** The sink draws
through whatever renderer the host already initialized. `akbasic_sink_init_akgl()` takes the
renderer; it does not create one.
**The interpreter owns no window, renderer or event loop.** Every initializer takes something
the host already made. Each calls `akgl_error_init()` first: `akgl_game_init()` would have,
but we drive subsystems directly and never call it, and a code raised before that registration
carries no name into its stack trace. It is idempotent.
**Call `akgl_error_init()` before anything else in `libakgl`.** New in `libakgl` 0.1.0
(`deps/libakgl/src/error.c`): it reserves the 256260 status band and registers a name for
each `AKGL_ERR_*` code. `akgl_game_init()` calls it first, but we drive subsystems directly and
never call `akgl_game_init()`, so it is ours to call. Skip it and every `AKGL_ERR_*` that
reaches a stack trace prints "Unknown Error" — which is exactly the defect the upstream commit
found in `game.c`, where an SDL failure five lines ahead of the old registration site was
guaranteed to be unnamed. It is idempotent, so calling it from both `akbasic_sink_init_akgl()`
and a host that already did is harmless.
`PASS` its result; a range collision is an initialization failure, not a warning.
*Acceptance:* `tests/akgl_backends.c`, a 128x128 software renderer under the dummy video
driver read back with `SDL_RenderReadPixels` — the pattern `deps/libakgl/tests/draw.c`
established, which needs no display and no offscreen harness. Registered as the `akgl_backends`
CTest case, and only when `AKBASIC_WITH_AKGL` is on.
**Still to check before starting:** `-DAKBASIC_WITH_AKGL=ON` has never been configured in this
repository, because until now there was nothing to build against. It needs `libakgl`'s own
submodules present (`git submodule update --init --recursive` pulls SDL and friends), and the
`akbasic_akgl` target in `CMakeLists.txt` has therefore never been compiled. Expect to fix
something there on the first attempt.
### Four things that had to be worked around to get there
*Acceptance:* `tests/sink_akgl.c` against the offscreen renderer harness described in
`deps/libakgl/TODO.md` under "Remaining work". If that harness still does not exist, a
known-failing test plus a `libakgl` TODO entry saying so.
All four are `libakgl`'s and all four are filed in `deps/libakgl/TODO.md`. Each workaround is
commented at its site with the words "filed upstream" so it can be found and deleted later.
1. **An embedded `libakgl` demands its dependencies be *installed*.** It builds its vendored
SDL, SDL_image, SDL_mixer, SDL_ttf and jansson only when it is top-level; embedded, it goes
down a `find_package` path and fails on a machine that has none of them — while the
submodules sit right there in `deps/libakgl/deps`. Every lookup is guarded with
`if(NOT TARGET ...)`, so our `CMakeLists.txt` adds those five subdirectories *before*
`add_subdirectory(deps/libakgl)` and the vendored copies get used after all. Same trick and
same ordering requirement `akerror::akerror` and `akstdlib::akstdlib` already need.
2. **`akgl/controller.h` does not compile on its own.** It declares two handler function
pointers taking an `akgl_Actor *` and includes nothing that declares the type.
`src/input_akgl.c` includes `akgl/actor.h` ahead of it.
3. **There is no way to attach a 2D backend to a renderer you already have.**
`akgl_render_init2d()` installs the six vtable pointers, but it also creates its own window
from the game properties and writes to the `camera` global, so it belongs to the
`akgl_game_init()` path — which is exactly the path an embedding host is not on.
`tests/akgl_backends.c` assigns the six pointers by hand. What is wanted upstream is the
vtable half of `init2d` on its own.
4. **`akgl_text_rendertextat()` segfaults on a backend whose vtable is empty.** It reaches
through `renderer->draw_texture` without checking it, so the first `PRINT` through the sink
dereferences NULL. That is the same class of defect 42b60f7's own commit added a draw test
for — "a backend that exists but was never given an `SDL_Renderer`" — and the text path
still has it.
### Still missing from the sink
`readline` reports end of input rather than reading anything: a drawn text layer is not a
source of lines, and `INPUT` through a graphics sink wants a line editor built on the keystroke
ring. EOF rather than an error is the contract `sink.h` states, so `INPUT` handles it already.
That editor is the next piece of work here, and it is what `WINDOW` and `KEY` in group E want
as well.
---
@@ -781,7 +809,7 @@ Dependency baseline:
|---|---|---|
| `deps/libakerror` | 1.0.0 | Private ownership-enforced status registry. akbasic reserves 512767 in `akbasic_error_register()`. Does not namespace its `coverage` target when embedded — worked around in our `CMakeLists.txt`. |
| `deps/libakstdlib` | 0.1.0 | soname `libakstdlib.so.0.1`. `AKSL_VERSION_CHECK()` asserted in `tests/version_check.c`. Its `aksl_ato*` family is banned here — see §1.9. |
| `deps/libakgl` | 0.1.0, **pinned at commit 42b60f7** | Migrated to the 1.0.0 registry and owns 256260. Not yet linked: `AKBASIC_WITH_AKGL` defaults OFF and `src/sink_akgl.c` does not exist. The version number cannot carry this requirement — see below. |
| `deps/libakgl` | 0.1.0, **pinned at commit 42b60f7** | Migrated to the 1.0.0 registry and owns 256260. Linked and tested under `-DAKBASIC_WITH_AKGL=ON`, which still defaults OFF so the core library and its whole suite build on a machine with no SDL. The version number cannot carry this requirement — see below. |
**The `libakgl` requirement is pinned by commit, not by version, and that is a defect
upstream.** 42b60f7 added 22 public symbols across four headers — `akgl_draw_*` (8),
@@ -823,16 +851,21 @@ keeping:
What remains, in priority order:
1. **§3 — the akgl text sink.** No longer blocked: `libakgl` 42b60f7 landed
`akgl_text_measure`, so this is now ordinary work. Note that `-DAKBASIC_WITH_AKGL=ON` has
never been configured here, so the `akbasic_akgl` target is unproven.
2. **§4 — the language completion work queue.** Nothing in it is blocked on `libakgl` any more:
groups G, I and part of E were, and `libakgl` 42b60f7 closed all three. Multiple statements
per line (the `COLON` token exists and nothing consumes it) should still come first, because
`DO`/`LOOP` reads badly without it.
3. **§6 items 1217** — the defects the port uncovered. Item 12 is the one that produces a
1. **§4 — the language completion work queue.** Groups G and I and the `GET`/`GETKEY`/`SCNCLR`
part of E are done. Of what is left, **multiple statements per line** should come first:
the `COLON` token exists and nothing consumes it, and `DO`/`LOOP` in group A reads badly
without it. Then groups A, B, D, F and J, none of which need anything from `libakgl`.
2. **A line editor for the akgl sink.** `readline` reports EOF, so `INPUT` does not work
through a drawn text layer. It wants the keystroke ring plus a cursor, and it is also what
`WINDOW` and `KEY` in group E need. See §3.
3. **CI does not cover `-DAKBASIC_WITH_AKGL=ON`.** Doing so needs `submodules: recursive`,
which pulls SDL, SDL_image, SDL_mixer, SDL_ttf and jansson — a long build on a job the other
four do not need. Deliberately deferred, not forgotten: the akgl target is built and tested
locally with the two commands in §3, and it will rot silently until somebody decides that
cost is worth paying.
4. **§6 items 1217** — the defects the port uncovered. Item 12 is the one that produces a
*wrong answer* rather than a refused one, and should be fixed first.
4. **Mutation survivors in `src/value.c`.** The harness is in place (`scripts/mutation_test.py`,
5. **Mutation survivors in `src/value.c`.** The harness is in place (`scripts/mutation_test.py`,
the `mutation` CMake target, and a CI job on `src/convert.c`), and a partial run over
`src/value.c` turned up test gaps worth closing. These are *not* equivalent mutants; each
is a real bug of that shape the suite would not notice: