Record the akbasic API gaps as resolved and bump to 0.3.0
All ten items under "API gaps blocking akbasic" are closed. Items 7, 9 and 10 add public symbols and item 9 adds three fields to akgl_AudioVoice, so the version and the soname go with them -- an 0.2 consumer cannot be handed this library and told it is compatible. Also refreshes the coverage numbers against a fresh run (79.6% line, 87.2% function, all 21 suites), records the mutation smoke runs over the two files that changed most, and files one new defect the text tests turned up: akgl_text_rendertextat refuses the empty string while akgl_text_measure accepts it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.10)
|
||||
# The single source of truth for the library version. It drives the generated
|
||||
# include/akgl/version.h, the shared library's VERSION and SOVERSION, and the
|
||||
# Version field in akgl.pc. Bump it here and nowhere else.
|
||||
project(akgl VERSION 0.2.0 LANGUAGES C)
|
||||
project(akgl VERSION 0.3.0 LANGUAGES C)
|
||||
|
||||
include(CTest)
|
||||
option(AKGL_COVERAGE "Instrument libakgl and generate coverage reports with CTest" OFF)
|
||||
|
||||
278
TODO.md
278
TODO.md
@@ -379,30 +379,36 @@ ctest --test-dir build-coverage --output-on-failure
|
||||
|
||||
Reports land in `build-coverage/coverage/` (`index.html`, `coverage.xml`).
|
||||
|
||||
**Line coverage 77.2%, function coverage 83.1%** (2035/2637 lines), up from a
|
||||
39.6% / 44.3% baseline. All 19 suites pass. (An earlier revision of this file
|
||||
recorded `character` as an intentionally failing suite; it passes now, and
|
||||
nothing in `tests/character.c` or `src/character.c` has changed since — the fix
|
||||
came from elsewhere in the tree and this note was never updated.)
|
||||
**Line coverage 79.6%, function coverage 87.2%** (2189/2749 lines, 164/188
|
||||
functions), up from 77.2% / 83.1% and from a 39.6% / 44.3% baseline. All 21
|
||||
suites pass. (An earlier revision of this file recorded `character` as an
|
||||
intentionally failing suite; it passes now, and nothing in `tests/character.c`
|
||||
or `src/character.c` has changed since — the fix came from elsewhere in the tree
|
||||
and this note was never updated.)
|
||||
|
||||
Note the trap described in "Known and still open" item 13 while regenerating
|
||||
these numbers: a coverage tree rebuilt after a source edit fails `coverage_reset`
|
||||
with `GCOV returncode was 5`. `find build-coverage -name '*.gcda' -delete` clears
|
||||
it.
|
||||
|
||||
| File | Lines | Functions |
|
||||
|---|---|---|
|
||||
| `src/actor.c` | 205/258 (79%) | 16/18 |
|
||||
| `src/actor.c` | 205/258 (80%) | 16/18 |
|
||||
| `src/assets.c` | 0/21 (0%) | 0/1 |
|
||||
| `src/audio.c` | 189/207 (91%) | 19/20 |
|
||||
| `src/audio.c` | 229/248 (92%) | 22/23 |
|
||||
| `src/character.c` | 104/118 (88%) | 6/7 |
|
||||
| `src/controller.c` | 247/270 (91%) | 12/12 |
|
||||
| `src/controller.c` | 300/329 (91%) | 16/16 |
|
||||
| `src/draw.c` | 253/267 (95%) | 11/12 |
|
||||
| `src/error.c` | 9/9 (100%) | 1/1 |
|
||||
| `src/game.c` | 124/231 (54%) | 10/15 |
|
||||
| `src/game.c` | 128/235 (54%) | 12/17 |
|
||||
| `src/heap.c` | 111/111 (100%) | 12/12 |
|
||||
| `src/json_helpers.c` | 111/111 (100%) | 11/11 |
|
||||
| `src/physics.c` | 140/140 (100%) | 10/10 |
|
||||
| `src/registry.c` | 76/102 (75%) | 11/12 |
|
||||
| `src/renderer.c` | 7/70 (10%) | 1/7 |
|
||||
| `src/registry.c` | 76/102 (74%) | 11/12 |
|
||||
| `src/renderer.c` | 42/75 (56%) | 7/8 |
|
||||
| `src/sprite.c` | 93/101 (92%) | 5/5 |
|
||||
| `src/staticstring.c` | 16/17 (94%) | 2/2 |
|
||||
| `src/text.c` | 26/45 (58%) | 3/4 |
|
||||
| `src/text.c` | 48/48 (100%) | 4/4 |
|
||||
| `src/tilemap.c` | 201/426 (47%) | 10/20 |
|
||||
| `src/util.c` | 121/131 (92%) | 7/8 |
|
||||
| `src/version.c` | 2/2 (100%) | 1/1 |
|
||||
@@ -423,7 +429,7 @@ scores):
|
||||
|---|---|---|
|
||||
| `src/draw.c` | 90% | Deleting the `FAIL_ZERO_BREAK` on `SDL_CreateTextureFromSurface` in `akgl_draw_flood_fill` |
|
||||
| `src/audio.c` | 75% | Deleting `SUCCEED_RETURN` from the static `check_voice`; deleting `spec.freq` before opening a device |
|
||||
| `src/text.c` | 50% | Three in `akgl_text_rendertextat`, which has no test yet, plus one `SUCCEED_RETURN` deletion |
|
||||
| `src/text.c` | 50% | Three in `akgl_text_rendertextat`, which had no test yet, plus one `SUCCEED_RETURN` deletion |
|
||||
|
||||
The first pass over `src/audio.c` scored 50% and named two real gaps, both of
|
||||
which are now tested: nothing asserted that an *unconfigured* voice is audible
|
||||
@@ -434,11 +440,31 @@ scored 70% first and named one: the circle was only checked at its four axis
|
||||
points, which a mis-signed octant reflection survives, so it now checks that
|
||||
every plotted pixel has a mirror in the other three quadrants.
|
||||
|
||||
Re-run as a smoke check after the akbasic API work, again at 10 sampled
|
||||
mutants each:
|
||||
|
||||
| File | Score | Surviving mutants |
|
||||
|---|---|---|
|
||||
| `src/controller.c` | 40% | Three `SDL_Log` deletions; `keybuffer_head`'s initialiser; `count > 0` in `keybuffer_attach_text`; the `mod` a text-only entry is given |
|
||||
| `src/audio.c` | 60% | The `break` on the last `switch` case; `errctx->handled` in the device callback; `ensure_voices()` in the mixer; the mixer's voice loop bound |
|
||||
|
||||
Only one of those was a real gap and it is now asserted: nothing checked that
|
||||
a text-only ring entry reports no modifiers. Of the rest, three are equivalent
|
||||
mutants rather than misses — a ring buffer whose head starts at 1 behaves
|
||||
identically, `count > 0` cannot be false where it is checked, and deleting the
|
||||
`break` on a `switch`'s last case changes nothing — and the mixer's `v <=`
|
||||
bound reads one voice past a zeroed table, which is undefined rather than
|
||||
observable. `SDL_Log` deletions are unobservable by construction.
|
||||
|
||||
What is left is honestly untestable from here. Deleting a `SUCCEED_RETURN`
|
||||
leaves a non-void function falling off its end, which is undefined rather than
|
||||
observably wrong, and the surviving SDL branches are allocation failures the
|
||||
suite has no way to provoke. The `src/text.c` survivors go away with the
|
||||
offscreen harness.
|
||||
suite has no way to provoke. The three `src/text.c` survivors in
|
||||
`akgl_text_rendertextat` are gone: it is tested now, against a software
|
||||
renderer, and deleting any of its three backend checks fails the suite. That
|
||||
was not free — the first draft of the test used a made-up `SDL_Renderer`
|
||||
pointer, which SDL refuses on its own, so the deleted-check mutant survived it.
|
||||
A live renderer was what made the check observable.
|
||||
|
||||
### Suites
|
||||
|
||||
@@ -472,45 +498,60 @@ Done:
|
||||
facing, movement logic, the animation frame state machine, `akgl_actor_update`,
|
||||
and character/sprite binding lookups. 80%.
|
||||
- `tests/audio.c` — every waveform, the ADSR envelope stage by stage, gate
|
||||
expiry and release, voice summing and clamping, the master level, and device
|
||||
open/shutdown under the dummy driver. 91%.
|
||||
expiry and release, the frequency sweep frame by frame, voice summing and
|
||||
clamping, the master level, and device open/shutdown under the dummy driver.
|
||||
92%.
|
||||
- `tests/draw.c` — every primitive against a 64x64 software renderer with the
|
||||
pixels read back, including flood-fill containment, save/paste roundtrip, and
|
||||
that drawing restores the renderer's draw color. 95%.
|
||||
- `tests/text.c` — font loading into the registry and both measurement entry
|
||||
points against a monospaced fixture font. 58%; the rest of `src/text.c` is
|
||||
`akgl_text_rendertextat`, which needs the harness.
|
||||
- `tests/text.c` — font loading into the registry, both measurement entry points
|
||||
against a monospaced fixture font, and drawing through a bound backend over a
|
||||
software renderer, including every way the backend can be unusable. 100%.
|
||||
- `tests/renderer.c` — the 2D vtable binding, frame start and end, both
|
||||
`draw_texture` paths, the refusals a bound-but-unrendered backend gives, and
|
||||
the `draw_mesh` stub. 56%; `akgl_render_init2d` and `akgl_render_2d_draw_world`
|
||||
are what is left, and both want the harness.
|
||||
- `tests/headers.c` — that `akgl/controller.h` compiles as the first include in
|
||||
a translation unit. The assertion is the compile; `main()` only has to return
|
||||
zero.
|
||||
|
||||
## Remaining work
|
||||
|
||||
### Needs the offscreen renderer harness
|
||||
|
||||
`src/renderer.c` (63 lines), `akgl_text_rendertextat` in `src/text.c` (19),
|
||||
`akgl_draw_background` in `src/draw.c` (13), `src/assets.c` (21),
|
||||
`akgl_render_init2d` and `akgl_render_2d_draw_world` in `src/renderer.c` (33
|
||||
lines), `akgl_draw_background` in `src/draw.c` (13), `src/assets.c` (21),
|
||||
`akgl_actor_render`/`actor_visible` in `src/actor.c` (53), and the drawing half
|
||||
of `src/tilemap.c` all need a live `renderer` global.
|
||||
of `src/tilemap.c` all need a live `renderer` global, a window, or the world
|
||||
globals.
|
||||
|
||||
`akgl_text_rendertextat` was on this list and is not any more: `tests/text.c`
|
||||
builds a software renderer and binds a backend to it with `akgl_render_bind2d`
|
||||
in nine lines, which is enough for anything that only needs *a* renderer rather
|
||||
than the whole world. `tests/renderer.c` and `tests/draw.c` do the same. The
|
||||
harness is still wanted, but for what is left it is a convenience rather than
|
||||
the blocker it was.
|
||||
|
||||
Build `tests/harness.c` / `tests/harness.h` with `akgl_test_init_headless()` and
|
||||
`akgl_test_shutdown_headless()`: set the dummy video and audio drivers,
|
||||
`SDL_Init()`, `akgl_heap_init()`, `akgl_registry_init()`, create a software
|
||||
`SDL_CreateWindowAndRenderer`, and point the global `renderer` at it. Four
|
||||
`SDL_CreateWindowAndRenderer`, and point the global `renderer` at it. Seven
|
||||
existing tests hand-roll this today (`tests/sprite.c:194`, `tests/character.c:200`,
|
||||
`tests/tilemap.c:421`, `tests/charviewer.c:42`); collapse them onto the shared
|
||||
harness in the same change. The new suites set the driver hints inline and need
|
||||
no window, so they are unaffected.
|
||||
`tests/tilemap.c:421`, `tests/charviewer.c:42`, plus `tests/draw.c`,
|
||||
`tests/renderer.c` and `tests/text.c`); collapse them onto the shared harness in
|
||||
the same change.
|
||||
|
||||
Then:
|
||||
|
||||
- **`tests/renderer.c`** — `akgl_render_init2d` populating all six function
|
||||
pointers and the camera; frame start/end against a NULL `sdl_renderer`; the
|
||||
rotated `draw_texture` path including `angle != 0` with a NULL center; the
|
||||
`draw_mesh` "not implemented" stub; and `draw_world` layer ordering. Note
|
||||
`defflags` at `src/renderer.c:113` is uninitialized until the `if` body runs.
|
||||
- **`tests/renderer.c` extensions** — `akgl_render_init2d` populating the camera
|
||||
from the property registry, and `draw_world` layer ordering. `tests/renderer.c`
|
||||
exists and covers everything that does not need a world or the registry: the
|
||||
vtable binding, frame start/end against a NULL `sdl_renderer`, both
|
||||
`draw_texture` paths including `angle != 0` with a NULL center, and the
|
||||
`draw_mesh` stub. Note `defflags` at `src/renderer.c:113` is uninitialized
|
||||
until the `if` body runs.
|
||||
- **`tests/assets.c`** — BGM loading into `AKGL_REGISTRY_MUSIC` under the dummy
|
||||
audio driver.
|
||||
- **`tests/text.c` extensions** — text rendering with wrap on and off.
|
||||
`tests/text.c` exists and covers font loading and measurement; only
|
||||
`akgl_text_rendertextat` is left.
|
||||
- **`tests/draw.c` extensions** — `akgl_draw_background` at zero, negative and
|
||||
oversized dimensions. `tests/draw.c` exists and covers every other primitive
|
||||
against a software renderer; `akgl_draw_background` is the one function in the
|
||||
@@ -857,6 +898,26 @@ without coming here first. Ordered by blast radius.
|
||||
Fix: one word. Touches `src/actor.c:276`. Worth a test that renders a
|
||||
deliberately non-square sprite and asserts on the destination rectangle.
|
||||
|
||||
### Found while closing the akbasic API gaps
|
||||
|
||||
27. **`akgl_text_rendertextat` refuses the empty string, and `akgl_text_measure` accepts it.**
|
||||
SDL_ttf returns `NULL` with "Text has zero width" from both `TTF_RenderText_Blended` and
|
||||
`TTF_RenderText_Blended_Wrapped` for `""`, so `src/text.c:56`'s `FAIL_ZERO_RETURN` on the
|
||||
surface reports `AKERR_NULLPOINTER` for what a caller means as "draw nothing". Verified
|
||||
directly against SDL_ttf 3.x with the fixture font.
|
||||
|
||||
`akgl_text_measure("")` is documented as legal and returns 0 wide by one line high, which
|
||||
is what a cursor sitting on an empty line needs, so the two halves of the same header
|
||||
disagree about the same string. The functional consequence is a caller that draws a line of
|
||||
text which may be empty — a line editor, a `PRINT` of an empty string, a HUD field that has
|
||||
not been filled in yet — has to test for it before every call or handle a failure that
|
||||
means nothing went wrong.
|
||||
|
||||
**Fix:** return success without rasterizing when `text[0] == '\0'`, matching the measure
|
||||
side, and say so in the header. Touches `akgl_text_rendertextat` only. `tests/text.c` has
|
||||
the case written and deliberately not asserted; it would become a `TEST_EXPECT_OK`. Until
|
||||
then the header carries the wart as a `@note` pointing here.
|
||||
|
||||
## Build notes
|
||||
|
||||
The vendored SDL satellite libraries are built into per-project subdirectories
|
||||
@@ -872,26 +933,32 @@ test target, and prepends the build tree to `LD_LIBRARY_PATH` for the CTest run.
|
||||
`akbasic` (the C port of the BASIC interpreter, `source.starfort.tech/andrew/akbasic`) is
|
||||
being built to link into `libakgl` as a scripting engine for game authors.
|
||||
|
||||
**Items 1 through 4 are all resolved, and `akbasic` has since consumed every one of them.**
|
||||
Its `libakgl`-backed text sink, its graphics backend, its sound backend and its input backend
|
||||
are written and tested, and the BASIC 7.0 graphics verbs (`GRAPHIC`, `COLOR`, `DRAW`, `BOX`,
|
||||
`CIRCLE`, `PAINT`, `SCALE`, `SSHAPE`, `GSHAPE`, `LOCATE`), the sound verbs (`SOUND`,
|
||||
`ENVELOPE`, `VOL`, `PLAY`, `TEMPO`) and the console input verbs (`GET`, `GETKEY`, `SCNCLR`)
|
||||
all work against them.
|
||||
**All ten items are resolved.** Items 1 through 4 landed first and `akbasic` has since
|
||||
consumed every one of them: its `libakgl`-backed text sink, its graphics backend, its sound
|
||||
backend and its input backend are written and tested, and the BASIC 7.0 graphics verbs
|
||||
(`GRAPHIC`, `COLOR`, `DRAW`, `BOX`, `CIRCLE`, `PAINT`, `SCALE`, `SSHAPE`, `GSHAPE`, `LOCATE`),
|
||||
the sound verbs (`SOUND`, `ENVELOPE`, `VOL`, `PLAY`, `TEMPO`) and the console input verbs
|
||||
(`GET`, `GETKEY`, `SCNCLR`) all work against them.
|
||||
|
||||
Doing that turned up **items 5 through 9**, filed below. Four are things `akbasic` had to work
|
||||
around to build at all; each workaround is commented at its site over there with the words
|
||||
"filed upstream", so they can be found and deleted when these land.
|
||||
Doing that turned up **items 5 through 9**. Four were things `akbasic` had to work around to
|
||||
build at all; each workaround is commented at its site over there with the words "filed
|
||||
upstream". **Those workarounds can now be deleted** — including the five `add_subdirectory`
|
||||
lines for the vendored SDL projects and the hand-assigned render vtable in
|
||||
`tests/akgl_backends.c`.
|
||||
|
||||
Building the *standalone* SDL frontend on top of all that — a real window, a real event pump
|
||||
and a line editor — turned up **item 10**, and re-confirmed items 6 and 7: both workarounds
|
||||
had to be written a second time, in `src/frontend_akgl.c`, because a host that owns a window is
|
||||
exactly the caller they inconvenience.
|
||||
|
||||
These are filed here rather than worked around in `akbasic` because growing `libakgl` to serve
|
||||
These were filed here rather than worked around in `akbasic` because growing `libakgl` to serve
|
||||
a consumer is the wanted outcome. Each entry says what the BASIC verb needs, what the
|
||||
`akgl_*` entry point should look like, and what would cover it.
|
||||
|
||||
Items 7, 9 and 10 add public symbols and item 9 adds three fields to `akgl_AudioVoice`, so the
|
||||
project version goes to **0.3.0** and the soname with it — an 0.2 consumer cannot be handed
|
||||
this library and told it is compatible.
|
||||
|
||||
1. **No way to measure rendered text.** `include/akgl/text.h` exposes `akgl_text_loadfont()`
|
||||
and `akgl_text_rendertextat()`, and nothing that reports how large a string will be in a
|
||||
given font. A terminal-style text surface cannot be built on that: a cursor needs the
|
||||
@@ -1047,6 +1114,28 @@ a consumer is the wanted outcome. Each entry says what the BASIC verb needs, wha
|
||||
`if(NOT TARGET ...)` so a consumer that has already declared them wins. The suppression of
|
||||
their CTest registration should move with them.
|
||||
|
||||
**Resolved.** The vendored block no longer sits behind
|
||||
`CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR`. An
|
||||
`akgl_add_vendored_dependency(<target> <dir>)` macro adds each of the seven submodules when
|
||||
nothing has already declared that target *and* the submodule is actually checked out; the
|
||||
`find_package` lookups for anything left over run afterwards, unchanged, so a checkout
|
||||
without submodules still resolves against the system. A macro rather than a function
|
||||
because `add_subdirectory()` inside a function runs in that function's variable scope.
|
||||
|
||||
The CTest suppression moved with them, and is lifted again before this project registers
|
||||
its own suites, so an embedding consumer's `add_test()` still reaches CTest. Two smaller
|
||||
consequences of the move: `find_package(PkgConfig)` now runs only when something has to
|
||||
come from the system (a fully vendored build needs no pkg-config of its own, though
|
||||
`deps/SDL_mixer` asks for it separately), and the build-tree RPATH block keys on whether
|
||||
anything was vendored rather than on being the top-level project, so an embedded build's
|
||||
tests can also find the satellite libraries.
|
||||
|
||||
Verified by configuring and building a scratch consumer that embeds this repository with
|
||||
`CMAKE_DISABLE_FIND_PACKAGE_SDL3`, `_SDL3_image`, `_SDL3_mixer`, `_SDL3_ttf`, `_akerror`,
|
||||
`_akstdlib` and `_jansson` all set, so any reliance on an installed copy would have failed
|
||||
the configure. It configures, builds and links. **The five `add_subdirectory` lines in
|
||||
`akbasic`'s `CMakeLists.txt` can be deleted.**
|
||||
|
||||
6. **`include/akgl/controller.h` does not compile on its own.** Lines 35, 36 and 41 declare
|
||||
handler function pointers taking an `akgl_Actor *`, and the header includes only
|
||||
`SDL3/SDL.h`, `akerror.h` and `types.h` — none of which declares that type. Any translation
|
||||
@@ -1058,6 +1147,17 @@ a consumer is the wanted outcome. Each entry says what the BASIC verb needs, wha
|
||||
`struct akgl_Actor` if the include order makes that circular. A one-line test that includes
|
||||
only `akgl/controller.h` would have caught it and would keep catching it.
|
||||
|
||||
**Resolved.** `controller.h` includes `<akgl/actor.h>`. Not a forward declaration:
|
||||
`akgl_Actor` is a typedef of a named struct, and repeating a typedef is C11, not C99. The
|
||||
include closes no cycle — `actor.h` reaches only `types.h` and `character.h`, neither of
|
||||
which knows about the controller.
|
||||
|
||||
`tests/headers.c` is the test, and it is a whole suite for one `#include` on purpose: a
|
||||
second `#include` after the first proves nothing about the second, because by then the
|
||||
first has dragged its dependencies in. Covering another header means another file shaped
|
||||
like that one. It also stopped being true that `tests/controller.c` has to include
|
||||
`akgl/actor.h` first, and the comment there saying so is gone.
|
||||
|
||||
7. **There is no way to attach a 2D backend to a renderer the caller already has.**
|
||||
`akgl_render_init2d()` (`src/renderer.c:17`) does two separable things: it creates a window
|
||||
and an `SDL_Renderer` from the `game.screenwidth`/`game.screenheight` properties and writes
|
||||
@@ -1073,6 +1173,21 @@ a consumer is the wanted outcome. Each entry says what the BASIC verb needs, wha
|
||||
and nothing else, and have `akgl_render_init2d()` call it after it has made its window.
|
||||
`tests/renderer.c` could then cover the vtable half without a display at all.
|
||||
|
||||
**Resolved.** `akgl_render_bind2d(akgl_RenderBackend *self)` installs the six pointers and
|
||||
returns; `akgl_render_init2d()` calls it once the window and the camera exist. It
|
||||
deliberately does *not* touch `self->sdl_renderer`, which is the whole point — a host that
|
||||
already owns one keeps it, and a host that does not gets a backend whose entry points all
|
||||
report `AKERR_NULLPOINTER` instead of crashing.
|
||||
|
||||
`tests/renderer.c` is new and needs no display: it covers the binding (including that the
|
||||
caller's `SDL_Renderer` survives it, and that binding a backend without one is legal),
|
||||
frame start and end and both `draw_texture` paths against a software renderer under the
|
||||
dummy video driver, the refusals a bound-but-unrendered backend gives, and the `draw_mesh`
|
||||
"not implemented" stub. `src/renderer.c` goes from 10% line coverage to 56%; what is left
|
||||
is `akgl_render_init2d` itself and `akgl_render_2d_draw_world`, both of which want the
|
||||
offscreen harness and the world globals. **`akbasic`'s hand-assigned vtable in
|
||||
`tests/akgl_backends.c` can be deleted.**
|
||||
|
||||
8. **`akgl_text_rendertextat()` dereferences an uninitialised backend vtable.**
|
||||
`src/text.c` calls `renderer->draw_texture(renderer, ...)` with no NULL check on either
|
||||
`renderer` or the function pointer, so a backend that has an `SDL_Renderer` but has not been
|
||||
@@ -1086,6 +1201,24 @@ a consumer is the wanted outcome. Each entry says what the BASIC verb needs, wha
|
||||
`renderer->draw_texture`, reporting `AKGL_ERR_SDL` or `AKERR_NULLPOINTER` as the draw
|
||||
entry points do. Test alongside the existing `tests/draw.c` backend-without-a-renderer case.
|
||||
|
||||
**Resolved.** All three are checked, with `AKERR_NULLPOINTER` to match the draw entry
|
||||
points, and they are checked *before* anything is rasterized rather than after: refusing
|
||||
early costs nothing and leaks nothing, where refusing after the rasterize would hit the
|
||||
leak the `@note` on this function already describes.
|
||||
|
||||
`tests/text.c` grew a software renderer under the dummy video driver — bound with the new
|
||||
`akgl_render_bind2d`, which is what made this cheap — and covers all three refusals plus
|
||||
the successful draw, wrapped and unwrapped. `src/text.c` goes from 58% to **100%** line
|
||||
coverage, and the three `akgl_text_rendertextat` mutants the mutation run left surviving
|
||||
are dead. The third case is the one that matters: with a *live* `SDL_Renderer` behind a
|
||||
backend that was never bound, the old code got all the way to a NULL function pointer.
|
||||
Checking it against a made-up renderer pointer, as the first draft of the test did, is
|
||||
worth nothing — SDL refuses the bogus handle and the call fails for the wrong reason, which
|
||||
a deleted-check mutant survives.
|
||||
|
||||
Found while writing that test and filed below as item 27: SDL_ttf refuses the empty string
|
||||
on both rasterizing paths, so drawing an empty line is an error while measuring one is not.
|
||||
|
||||
9. **`SOUND`'s frequency sweep has no `akgl_audio_*` equivalent.** 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 — a siren, a laser, the whole reason
|
||||
@@ -1104,6 +1237,36 @@ a consumer is the wanted outcome. Each entry says what the BASIC verb needs, wha
|
||||
hand and assert the frame at which the pitch has moved, exactly as `tests/audio.c` already
|
||||
does for the envelope stages.
|
||||
|
||||
**Resolved.** `akgl_audio_sweep(voice, from_hz, to_hz, step_hz, ms)` is in
|
||||
`include/akgl/audio.h`, with the step taken in `akgl_audio_mix()` off the mixer's own frame
|
||||
counter. Decisions worth knowing:
|
||||
|
||||
- **One step every 1/60 second** (`AKGL_AUDIO_SWEEP_TICK_HZ`), because that is the rate the
|
||||
machine this vocabulary comes from advanced its sweep at, and its tunes are written for
|
||||
it. It divides 44100 exactly, so a step boundary always lands on a whole frame — which is
|
||||
what lets the suite assert the exact frame the pitch moves on rather than a tolerance.
|
||||
- **Direction comes from the two frequencies, not from the sign of the step.** `step_hz`
|
||||
stays positive; `to_hz` below `from_hz` sweeps down. A negative step is refused rather
|
||||
than silently meaning something.
|
||||
- **It arrives and holds.** The last step is clamped to `to_hz` rather than overshooting,
|
||||
and equal frequencies are a legal held tone rather than an error, so a caller computing
|
||||
its own limits does not have to special-case them.
|
||||
- **`akgl_audio_tone` is now the same start path with a step of 0**, which is also what
|
||||
makes a voice reused for a plain tone stop sweeping. That is one function, `start_note`,
|
||||
rather than two copies of the same six assignments.
|
||||
- **A swept voice accumulates its phase** instead of deriving it from the frame counter.
|
||||
Deriving assumes a constant frequency; under a sweep it jumps the waveform at every step,
|
||||
which is an audible click. The drift the derived form exists to avoid is not something a
|
||||
note that is changing pitch anyway can be said to suffer from.
|
||||
|
||||
`tests/audio.c` drives the mixer by hand: one tick's frames do not move the pitch, the next
|
||||
frame does, both directions clamp at their target, a gate shorter than the sweep cuts it off
|
||||
part way, and a plain tone on a swept voice stays put. `src/audio.c` holds at 92%.
|
||||
|
||||
Still missing for a complete `SOUND`: the oscillating third direction (C128 `dir` 2), which
|
||||
is a re-issue on arrival rather than a third kind of sweep, and `FILTER`, `TEMPO` and the
|
||||
`PLAY` note-string parser as before.
|
||||
|
||||
10. **The keystroke ring carries a keycode and nothing else, so Shift is invisible.**
|
||||
`akgl_controller_handle_event()` (`src/controller.c:104-105`) pushes `event->key.key` into
|
||||
the ring and drops the rest of the event, and `akgl_controller_poll_key(int *keycode, bool
|
||||
@@ -1146,6 +1309,31 @@ a consumer is the wanted outcome. Each entry says what the BASIC verb needs, wha
|
||||
`akgl_controller_handle_event()` and assert both the keycode and the composed character
|
||||
come back, mirroring what `tests/controller.c` already does for the plain ring.
|
||||
|
||||
**Resolved.** `akgl_Keystroke` (keycode, `SDL_Keymod`, and eight bytes of UTF-8) and
|
||||
`akgl_controller_poll_keystroke(akgl_Keystroke *dest, bool *available)` are in
|
||||
`include/akgl/controller.h`; the ring now holds those instead of bare keycodes.
|
||||
`akgl_controller_poll_key()` is untouched from its caller's side. Decisions worth knowing:
|
||||
|
||||
- **The text is attached to the press that is still waiting for it**, tracked by a flag
|
||||
rather than by "whatever entry is newest". Without the flag, a press dropped by a full
|
||||
buffer hands its text to some older key that happens to be sitting at the end of the
|
||||
ring — which is wrong in exactly the case where things are already going badly.
|
||||
- **Text with no press behind it is buffered on its own**, with a keycode of 0. An input
|
||||
method commit and a character finished by a dead key have no key press of their own, and
|
||||
a line editor wants the character regardless. `akgl_controller_poll_key()` discards those
|
||||
on the way past rather than reporting a keystroke with no key.
|
||||
- **Oversized text is cut on a code point boundary.** An IME can commit several characters
|
||||
at once and an entry holds one; truncating on a byte boundary would leave a partial UTF-8
|
||||
sequence that nothing downstream can render.
|
||||
- **SDL only sends `SDL_EVENT_TEXT_INPUT` while text input is started**, so a host that
|
||||
wants `text` populated calls `SDL_StartTextInput()` on its window. Without it `key` and
|
||||
`mod` still arrive. That is documented on the function rather than left to be discovered.
|
||||
|
||||
`tests/controller.c` covers the shifted-key case end to end (the `"` that started this),
|
||||
a non-printing key composing to nothing, a text-only entry through both pollers, text that
|
||||
outlived its own press, the code-point-boundary truncation, and both NULL arguments.
|
||||
`src/controller.c` holds at 91%.
|
||||
|
||||
## Carried over
|
||||
|
||||
1. **Make character-to-sprite state bindings release their references symmetrically.**
|
||||
|
||||
Reference in New Issue
Block a user