Consume libakgl 0.3.0: every workaround deleted, two capabilities gained
0.3.0 closed all ten API gaps this port had filed. The four workarounds go with them: the CMake block that declared libakgl's vendored dependencies by hand, the akgl/actor.h include in three files, and the six vtable pointers assigned by hand in two more, now akgl_render_bind2d(). Two gaps were capabilities rather than inconveniences, and both are now real: The line editor takes the composed UTF-8 text the ring carries in preference to the keycode, so shifted characters, keyboard layouts, compose keys and dead keys all work. A double quote can be typed, which means a BASIC string literal can be typed -- the sharp end of the old limitation. Letters are no longer folded to upper case. SOUND's dir/min/step reach akgl_audio_sweep instead of being refused. dir 3 sweeps once rather than oscillating and TODO.md section 5 says so. A backend with no sweep still refuses the swept note and plays the held one. The adaptors now carry an AKGL_VERSION_AT_LEAST(0, 3, 0) floor, verified by temporarily demanding 0.4.0 and watching it fire. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
191
TODO.md
191
TODO.md
@@ -535,50 +535,49 @@ path is proved only through SDL's own event queue in `tests/akgl_frontend.c` —
|
||||
same queue a real keyboard delivers into, but not the same as a window manager giving the
|
||||
window focus. Somebody should type at it once.
|
||||
|
||||
### Five things that had to be worked around to get there
|
||||
### The five things that had to be worked around — **all gone**
|
||||
|
||||
All five are `libakgl`'s and all five 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.
|
||||
Every one was filed against `libakgl` and every one is resolved in its 0.3.0. What is left here
|
||||
is the note that they existed, because the pattern is the point: file it upstream, comment the
|
||||
workaround at its site with the words "filed upstream", delete it when it lands.
|
||||
|
||||
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.
|
||||
| Was | Resolved by |
|
||||
|---|---|
|
||||
| An embedded `libakgl` required its vendored SDL, SDL_image, SDL_mixer, SDL_ttf and jansson to be *installed*, so our `CMakeLists.txt` declared all five by hand first | It adds them whether or not it is top-level |
|
||||
| `akgl/controller.h` did not compile on its own — it used `akgl_Actor *` and included nothing that declared it | It includes `<akgl/actor.h>` |
|
||||
| There was no way to bind a 2D backend to a renderer you already had, so the six vtable pointers were assigned by hand in two files | `akgl_render_bind2d()` |
|
||||
| `akgl_text_rendertextat()` dereferenced an unset `draw_texture`, so the first `PRINT` through an unbound backend segfaulted | It checks, and reports `AKERR_NULLPOINTER` like the draw entry points |
|
||||
| `SOUND`'s frequency sweep had no equivalent and was refused | `akgl_audio_sweep()` |
|
||||
|
||||
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.
|
||||
**0.3.0 also arrived with a regression that this repository is the one that found**, because it
|
||||
is the only consumer that embeds `libakgl` alongside other projects: the commit that made the
|
||||
vendored dependencies unconditional also made its `add_test()` shadow unconditional, and CMake
|
||||
chains command overrides exactly one level deep. Two projects in one tree cannot both shadow
|
||||
`add_test` — the second rebinds `_add_test` to the first and the builtin becomes unreachable to
|
||||
everybody, so our own registrations recursed to CMake's depth limit. Fixed upstream by putting
|
||||
the top-level guard back; filed as `libakgl` defect 27, with the two-line CMake program that
|
||||
demonstrates the one-level limit.
|
||||
|
||||
### Still missing from the sink
|
||||
|
||||
Nothing that blocks a verb. The line editor exists (see above), so `INPUT` and the REPL work in
|
||||
the window. Two limits are worth stating because a program can reach both:
|
||||
Nothing that blocks a verb, and as of `libakgl` 0.3.0 nothing that blocks a *character* either.
|
||||
The ring now carries the composed UTF-8 text SDL worked out from each keystroke, so the editor
|
||||
takes that in preference to the keycode and shifted characters, keyboard layouts, compose keys
|
||||
and dead keys all work. **A double quote can be typed, so a BASIC string literal can be typed**,
|
||||
which was the sharp end of the old limitation — it used to mean a program with a string in it
|
||||
had to be passed on the command line. Letters are no longer folded to upper case, because there
|
||||
is no longer any reason to.
|
||||
|
||||
One limit remains, and it is a choice rather than a gap:
|
||||
|
||||
- **No shifted character can be typed.** The keystroke ring carries a keycode and no modifier
|
||||
state, so `"`, `!`, `(`, `)`, `:` and `;` are unreachable and a string literal cannot be typed
|
||||
at the window at all. Letters are folded to upper case, which is a C128 doing what a C128 does
|
||||
rather than a workaround, but punctuation has no such excuse. Filed upstream as `libakgl`
|
||||
item 10; until it lands, the way to run a program with a string literal in it is to pass the
|
||||
file on the command line, which is unaffected.
|
||||
- **No cursor movement within a line.** Backspace and escape only. The arrow keys stay in the
|
||||
ring for a script's own `GET` loop, which is the more valuable use of them.
|
||||
|
||||
A non-ASCII character is accepted by the keyboard and then dropped rather than stored, which is
|
||||
§1.2 rather than the editor: the grid is a byte per cell and a value's string is a fixed 256
|
||||
bytes, so a multi-byte character has nowhere to go. Dropping it is honest where storing half of
|
||||
it is not.
|
||||
|
||||
`WINDOW` and `KEY` in group E want a text-window rectangle and a function-key table on top of
|
||||
this, which is ordinary work here rather than anything blocked.
|
||||
|
||||
@@ -757,13 +756,28 @@ behaviour to port. They matter to somebody typing in a listing out of a C128 man
|
||||
`step()` is called. Audible, deterministic, and never a hang, which is the right way for
|
||||
that to fail.
|
||||
|
||||
21. **`PLAY`'s `M` (measure) is a no-op and `SOUND`'s frequency sweep is refused.** `M`
|
||||
synchronises the three voices on a C128; with one sequential queue there is nothing to
|
||||
synchronise, and a listing full of them should still play, so it is accepted and ignored.
|
||||
The sweep is the opposite call: `SOUND`'s `dir`/`min`/`step` arguments have no
|
||||
`akgl_audio_*` equivalent, and the only way to fake one is to re-issue tones from
|
||||
`step()` — which would tie audible pitch to how often the host happens to call us, a tune
|
||||
that changes key with the frame rate. Filed upstream as `akgl_audio_sweep`; see §7.
|
||||
21. **`PLAY`'s `M` (measure) is a no-op, and `SOUND`'s sweep runs once rather than
|
||||
oscillating.** `M` synchronises the three voices on a C128; with one sequential queue there
|
||||
is nothing to synchronise, and a listing full of them should still play, so it is accepted
|
||||
and ignored.
|
||||
|
||||
The sweep *used* to be refused outright — there was no `akgl_audio_*` equivalent and faking
|
||||
one would have tied audible pitch to how often the host calls us. `libakgl` 0.3.0 added
|
||||
`akgl_audio_sweep()` and `SOUND voice, freq, dur, dir, min, step` now reaches it. What does
|
||||
not survive is `dir` 3, "oscillate": `akgl_audio_sweep` runs one pass between two endpoints,
|
||||
and a real oscillation needs the mixer to turn around at them. **What this changes for a
|
||||
program:** a `dir` 3 siren rises or falls once instead of warbling. `dir` 1 and 2 are exact,
|
||||
and so is the direction — both the SID and `akgl_audio_sweep` take it from which endpoint is
|
||||
higher, so a `min` above the starting frequency rises whatever `dir` says.
|
||||
|
||||
`step` is converted as a *delta* rather than a position: it is a register increment, and the
|
||||
register-to-hertz table maps positions, so it goes across as the distance between register 0
|
||||
and register `step`. A step that converts to zero would never arrive and is raised to one
|
||||
hertz, which is the smallest move that still gets there.
|
||||
|
||||
A backend whose record has no `sweep` — a host written against `libakgl` 0.2.0 — still
|
||||
refuses a swept note with `AKBASIC_ERR_DEVICE` and plays a held one normally.
|
||||
`tests/audio_verbs.c` asserts both halves.
|
||||
|
||||
22. **`TEMPO`'s constant is a calibration choice, not a transcription.** BASIC 7.0 documents
|
||||
`TEMPO` as a relative duration from 1 to 255 defaulting to 8, and does not publish what a
|
||||
@@ -1031,8 +1045,9 @@ When phase 7 or phase 8 needs something `libakgl` does not have, **stop and file
|
||||
the `akgl_*` entry point should look like, and what tests would cover it. Growing `libakgl` to
|
||||
serve the interpreter is a wanted outcome, not a detour.
|
||||
|
||||
**All four gaps filed so far are now closed upstream**, at `libakgl` 42b60f7. Nothing here is
|
||||
blocked on `libakgl` any more; what remains is akbasic-side work.
|
||||
**All ten gaps filed so far are closed upstream**, as of `libakgl` 0.3.0. Nothing here is
|
||||
blocked on `libakgl`, and this repository carries no `libakgl` workarounds at all — the four
|
||||
§3 used to list are deleted.
|
||||
|
||||
| Was blocking | Closed by |
|
||||
|---|---|
|
||||
@@ -1040,70 +1055,42 @@ blocked on `libakgl` any more; what remains is akbasic-side work.
|
||||
| Immediate-mode drawing (§4 group G) | `akgl_draw_point`/`_line`/`_rect`/`_filled_rect`/`_circle`/`_flood_fill`/`_copy_region`/`_paste_region` |
|
||||
| Audio (§4 group I) | `akgl_audio_init`/`_tone`/`_envelope`/`_waveform`/`_volume`/`_stop`/`_voice_active`/`_mix` |
|
||||
| Console input (§4 group E) | `akgl_controller_poll_key`, `akgl_controller_flush_keys` |
|
||||
| Vendored dependencies when embedded | added unconditionally, 0.3.0 |
|
||||
| `controller.h` not self-contained | includes `<akgl/actor.h>`, 0.3.0 |
|
||||
| Binding a 2D backend to an existing renderer | `akgl_render_bind2d()`, 0.3.0 |
|
||||
| `akgl_text_rendertextat()` on an unbound backend | checked, 0.3.0 |
|
||||
| `SOUND`'s frequency sweep (§5 item 21) | `akgl_audio_sweep()`, 0.3.0 |
|
||||
| The line editor could not see Shift (§3) | `akgl_Keystroke` + `akgl_controller_poll_keystroke()`, 0.3.0 |
|
||||
|
||||
Two notes for whoever picks up §3 next. The draw calls take an `akgl_RenderBackend *` as their
|
||||
first argument rather than reaching for a global renderer, which fits the rule that the
|
||||
interpreter draws through whatever renderer the host already initialized. And the audio API is
|
||||
a synthesised-voice one — `akgl_audio_tone(voice, hz, ms)` with a separate ADSR envelope —
|
||||
which is the shape `PLAY` and `ENVELOPE` need, rather than the sample playback SDL3_mixer
|
||||
would have given.
|
||||
Three notes for whoever files the next one. The draw calls take an `akgl_RenderBackend *` as
|
||||
their first argument rather than reaching for a global renderer, which fits the rule that the
|
||||
interpreter draws through whatever renderer the host already initialized. The audio API is a
|
||||
synthesised-voice one — `akgl_audio_tone(voice, hz, ms)` with a separate ADSR envelope — which
|
||||
is the shape `PLAY` and `ENVELOPE` need, rather than the sample playback SDL3_mixer would have
|
||||
given. And `akgl_controller_poll_key()` survives alongside the keystroke form on purpose: `GET`
|
||||
and `GETKEY` still use it, because a script asking "was the up arrow pressed" wants a keycode
|
||||
and would get the empty string from the other one.
|
||||
|
||||
**`FILTER` is the one verb still refused**, and it is not filed as a gap because upstream said
|
||||
plainly what it would take: `akgl_audio_*` synthesises raw waveforms and mixes them, there is no
|
||||
filter stage to configure, and SDL3 supplies no primitive to build one from. Until an
|
||||
`akgl_audio_filter()` exists, `FILTER` is accepted by the parser and refused at execution with
|
||||
`AKBASIC_ERR_DEVICE` rather than silently ignored — a program that asks for a low-pass and gets
|
||||
an unfiltered square wave has been lied to.
|
||||
|
||||
**One thing went the other way**, and it is worth recording because it is the only defect this
|
||||
project has ever sent upstream in a release it also consumed. 0.3.0's vendored-dependency fix
|
||||
also made `libakgl`'s `add_test()` shadow unconditional. CMake chains command overrides exactly
|
||||
one level deep, so two projects in one tree cannot both shadow it: the second rebinds
|
||||
`_add_test` to the first and the builtin becomes unreachable to everyone, and this repository's
|
||||
own test registrations recursed until CMake stopped at depth 1000. `akbasic` is the only
|
||||
consumer that embeds `libakgl` next to other projects, so it is the only place this could show
|
||||
up. Fixed upstream by restoring the top-level guard and filed as `libakgl` defect 27.
|
||||
|
||||
Building against any of it needs `-DAKBASIC_WITH_AKGL=ON`, which pulls in `libakgl`'s own
|
||||
submodules (SDL and friends). Those are not initialized in a default clone; `git submodule
|
||||
update --init --recursive` gets them.
|
||||
|
||||
**Seven items are filed upstream and outstanding**, at `deps/libakgl/TODO.md` items 5-10 of "API
|
||||
gaps blocking akbasic" and item 14 of "Defects". Four are workarounds this repository carries
|
||||
today, each commented at its site with the words "filed upstream" so it can be found and
|
||||
deleted: the embedded-dependency CMake trick in our `CMakeLists.txt`, the `akgl/actor.h`
|
||||
include in `src/input_akgl.c` and `src/sink_akgl.c`, and the hand-populated vtable and its
|
||||
NULL-deref hazard in `tests/akgl_backends.c` and `src/frontend_akgl.c`. §3 describes all four.
|
||||
The fifth is `SOUND`'s frequency sweep, the sixth is the missing version bump recorded in §8,
|
||||
and the seventh is new:
|
||||
|
||||
- **The keystroke ring carries a keycode and no modifier state**, so the line editor cannot see
|
||||
Shift. Every shifted character — `"`, `!`, `(`, `)`, `:`, `;` — is unreachable, which means a
|
||||
string literal cannot be typed at the window at all, and lower case is unreachable too.
|
||||
Letters are folded to upper case, which is what a C128 does and is therefore the right answer
|
||||
for letters rather than merely the available one; punctuation has no such excuse. It is not
|
||||
fixable from here: by the time a caller could ask `SDL_GetModState()` the key is long
|
||||
released, and decoupling reads from events is the entire point of a ring. The wanted API is
|
||||
an `akgl_controller_poll_keystroke()` returning keycode, modifiers and the composed UTF-8
|
||||
text SDL already computes from `SDL_EVENT_TEXT_INPUT` — which is also the only way a keyboard
|
||||
layout, a compose key or a dead key can ever work. Filed as item 10, alongside the existing
|
||||
`akgl_controller_poll_key()`, which stays exactly as it is: a game asking "was the up arrow
|
||||
pressed" wants precisely what it already gets.
|
||||
|
||||
**One further gap is outstanding, and upstream named it itself.** The commit that added the audio API
|
||||
says plainly what it does not cover: *"Still missing for a complete BASIC sound vocabulary:
|
||||
`FILTER` (SDL3 has no filter primitive; this would need writing), `TEMPO` and the `PLAY`
|
||||
note-string parser, both of which belong in the interpreter rather than here."*
|
||||
|
||||
- **`TEMPO` and the `PLAY` parser are ours**, not a gap. They are group I work in this
|
||||
repository and nothing upstream is waiting on.
|
||||
- **`FILTER` is a real gap.** BASIC 7.0's `FILTER freq, lp, bp, hp, res` sets the SID's
|
||||
filter cutoff, the three band-pass switches and the resonance. `akgl_audio_*` synthesises
|
||||
raw waveforms and mixes them; there is no filter stage to configure and SDL3 supplies no
|
||||
primitive to build one from. Until an `akgl_audio_filter()` exists, `FILTER` is accepted by
|
||||
the parser and refused at execution with `AKBASIC_ERR_DEVICE` rather than silently ignored —
|
||||
a program that asks for a low-pass and gets an unfiltered square wave has been lied to.
|
||||
|
||||
A second gap turned up while implementing group I, and is filed alongside it:
|
||||
|
||||
- **`SOUND` has no frequency sweep.** BASIC 7.0's `SOUND voice, freq, dur, dir, min, step`
|
||||
ramps the pitch from `freq` toward `min` in `step` increments per tick, in the direction
|
||||
`dir` selects. `akgl_audio_tone(voice, hz, ms)` holds one pitch for one duration and there
|
||||
is no entry point that changes pitch over the life of a note. The wanted API is roughly
|
||||
`akgl_audio_sweep(int voice, float32_t from_hz, float32_t to_hz, float32_t step_hz, uint32_t ms)`,
|
||||
advanced on the mixer's own frame counter — the same place the phase is already derived
|
||||
from, and the reason it belongs there rather than here. Faking it in the interpreter means
|
||||
re-issuing tones from `akbasic_runtime_step()`, which ties audible pitch to how often a host
|
||||
calls us: a tune that changes key with the frame rate. Refused with `AKBASIC_ERR_DEVICE`
|
||||
until it lands. Tests would drive `akgl_audio_mix` by hand and assert the frame at which the
|
||||
pitch has moved, exactly as `tests/audio.c` already does for the envelope.
|
||||
|
||||
When the next gap turns up, file it there rather than working around it here.
|
||||
|
||||
---
|
||||
|
||||
## 8. Status
|
||||
@@ -1150,7 +1137,7 @@ Dependency baseline:
|
||||
|---|---|---|
|
||||
| `deps/libakerror` | 1.0.0 | Private ownership-enforced status registry. akbasic reserves 512–767 in `akbasic_error_register()`. Does not namespace its `coverage` target when embedded — worked around in our `CMakeLists.txt`. |
|
||||
| `deps/libakstdlib` | 0.2.0 | soname `libakstdlib.so.0.2`. `AKSL_VERSION_CHECK()` asserted in `tests/version_check.c`. This release fixed all six confirmed defects the port was working around — see §1.9, where the bans are now lifted. |
|
||||
| `deps/libakgl` | 0.2.0 | soname `libakgl.so.0.2`. Owns status codes 256–260. 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. It requires `libakstdlib` 0.2, which is why the two moved together. |
|
||||
| `deps/libakgl` | 0.3.0 | soname `libakgl.so.0.3`. Owns status codes 256–260. 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. **0.3.0 closed every API gap this port had filed** — see §7 — so the four workarounds §3 used to list are gone. |
|
||||
|
||||
**The `libakgl` requirement used to be pinned by commit, and no longer is.** 42b60f7 added 22
|
||||
public symbols across four headers and left `project(akgl VERSION 0.1.0)` and the
|
||||
|
||||
Reference in New Issue
Block a user