Files
akbasic/src/sink_akgl.c

677 lines
24 KiB
C
Raw Normal View History

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>
2026-07-31 08:46:53 -04:00
/**
* @file sink_akgl.c
* @brief The libakgl-backed text sink: where PRINT goes when a host is drawing.
*
* Implements the section 1.5 vtable over akgl_text_measure() and
* akgl_text_rendertextat(). Owns the cursor, the wrap and the scroll --
* everything in the reference's basicruntime_graphics.go except Write and
* Println, which are the sink interface itself.
*
* The measurement call is new in libakgl 42b60f7 and is the reason this file can
* exist at all: the reference derives its character grid from
* font.SizeUTF8("A") (basicruntime.go:96) and there was no akgl_text_*
* equivalent until then.
*/
#include <ctype.h>
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>
2026-07-31 08:46:53 -04:00
#include <stdio.h>
#include <string.h>
#include <akerror.h>
#include <akgl/controller.h>
Erase what the text layer drew: three GUI bugs, one cause The sink painted glyphs on top of whatever was already on the renderer and never erased anything, and the host deliberately does not clear the frame so a DRAW survives. The grid was always correct; the screen kept the previous frame. That surfaced as three separate-looking faults: - a backspaced character stayed on screen, including across a line wrap - a scroll left the old rows behind, "papered over with garbage" - the editing cursor smeared an underscore along every column it passed through, which read as an underline under the text being typed render() now fills each row it is about to draw, and each row that has emptied since it last drew, with the background first. Row by row rather than one clear over the whole area: the text layer is authoritative over the rows it occupies and leaves every other pixel alone, so a picture behind the text loses only the strips the text uses instead of all of it. echo_line() also truncates rows that hold nothing but the spaces its own erase pass wrote, which is what backspacing back across a wrap leaves behind. A row of spaces is text as far as render() is concerned, so it would have been repainted forever and kept erasing whatever was under it. The cursor glyph is removed outright, as asked. The smearing was the erase bug rather than the cursor, so one could come back and behave -- a block would read better than an underscore. Four pixel-level tests, because grid assertions could never have caught any of this. Each was checked by reverting the fix and watching it fail; two of them were vacuous when first written and are noted as such where they are fixed. The real-keyboard test also now rubs out a character and scrolls forty lines. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 15:03:37 -04:00
#include <akgl/draw.h>
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>
2026-07-31 08:46:53 -04:00
#include <akgl/error.h>
#include <akgl/text.h>
#include <akbasic/akgl.h>
#include <akbasic/error.h>
/** @brief Rows of scrollback the sink keeps. Matches the `text` array in akgl.h. */
#define SINK_MAX_ROWS 64
/** @brief Bytes per row, including the terminator. */
#define SINK_MAX_COLUMNS 256
/** @brief Scroll the grid up by one row, dropping the top one. */
static void scroll(akbasic_AkglSink *state)
{
int row = 0;
for ( row = 0; row < SINK_MAX_ROWS - 1; row++ ) {
memcpy(state->text[row], state->text[row + 1], SINK_MAX_COLUMNS);
}
memset(state->text[SINK_MAX_ROWS - 1], 0, SINK_MAX_COLUMNS);
if ( state->cursorrow > 0 ) {
state->cursorrow -= 1;
}
/*
* A line being typed is anchored to a row, and that row just moved. Follow
* it, or a backspace erases somebody else's text. Clamped at zero: a line
* long enough to scroll its own start off the top redraws from the top row,
* which is cosmetically wrong and is the only place this can be seen.
*/
if ( state->editing ) {
state->editrow -= 1;
if ( state->editrow < 0 ) {
state->editrow = 0;
}
}
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>
2026-07-31 08:46:53 -04:00
}
/** @brief Move to the start of the next row, scrolling if that runs off the end. */
static void newline(akbasic_AkglSink *state)
{
state->cursorcol = 0;
state->cursorrow += 1;
if ( state->cursorrow >= state->rows || state->cursorrow >= SINK_MAX_ROWS ) {
scroll(state);
}
}
/**
* @brief Put one character at the cursor, wrapping and scrolling as needed.
*
* Wrapping happens here, on the character grid, rather than being left to
* SDL_ttf's own wraplength. The cursor has to end up somewhere definite: a
* program that PRINTs a long string and then PRINTs again expects the second one
* to start on the row after the first one ended, and only the code that placed
* the characters knows which row that is.
*/
static void putchar_at(akbasic_AkglSink *state, char c)
{
if ( c == '\n' ) {
newline(state);
return;
}
if ( state->cursorcol >= state->columns || state->cursorcol >= SINK_MAX_COLUMNS - 1 ) {
newline(state);
}
state->text[state->cursorrow][state->cursorcol] = c;
state->cursorcol += 1;
state->text[state->cursorrow][state->cursorcol] = '\0';
}
static akerr_ErrorContext *sink_write(akbasic_TextSink *self, const char *text)
{
PREPARE_ERROR(errctx);
akbasic_AkglSink *state = NULL;
size_t i = 0;
FAIL_ZERO_RETURN(errctx, (self != NULL && text != NULL), AKERR_NULLPOINTER,
"NULL argument in akgl sink write");
state = (akbasic_AkglSink *)self->self;
FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER,
"akgl sink has no state");
for ( i = 0; text[i] != '\0'; i++ ) {
putchar_at(state, text[i]);
}
SUCCEED_RETURN(errctx);
}
static akerr_ErrorContext *sink_writeln(akbasic_TextSink *self, const char *text)
{
PREPARE_ERROR(errctx);
akbasic_AkglSink *state = NULL;
PASS(errctx, sink_write(self, text));
state = (akbasic_AkglSink *)self->self;
newline(state);
SUCCEED_RETURN(errctx);
}
/**
* @brief Redraw the line being typed, erasing whatever a longer one left behind.
*
* Two passes over the grid rather than one clever one. The first blanks what is
* already drawn and the second draws the current text, which leaves the cursor
* exactly where the text ends without anybody having to reproduce putchar_at's
* wrapping arithmetic a second time. Nothing is rendered here -- the grid is
* memory, and the host draws it when it draws its frame -- so the cost is a
* couple of memcpy-sized loops per keystroke.
*/
static void echo_line(akbasic_AkglSink *state)
{
int i = 0;
Erase what the text layer drew: three GUI bugs, one cause The sink painted glyphs on top of whatever was already on the renderer and never erased anything, and the host deliberately does not clear the frame so a DRAW survives. The grid was always correct; the screen kept the previous frame. That surfaced as three separate-looking faults: - a backspaced character stayed on screen, including across a line wrap - a scroll left the old rows behind, "papered over with garbage" - the editing cursor smeared an underscore along every column it passed through, which read as an underline under the text being typed render() now fills each row it is about to draw, and each row that has emptied since it last drew, with the background first. Row by row rather than one clear over the whole area: the text layer is authoritative over the rows it occupies and leaves every other pixel alone, so a picture behind the text loses only the strips the text uses instead of all of it. echo_line() also truncates rows that hold nothing but the spaces its own erase pass wrote, which is what backspacing back across a wrap leaves behind. A row of spaces is text as far as render() is concerned, so it would have been repainted forever and kept erasing whatever was under it. The cursor glyph is removed outright, as asked. The smearing was the erase bug rather than the cursor, so one could come back and behave -- a block would read better than an underscore. Four pixel-level tests, because grid assertions could never have caught any of this. Each was checked by reverting the fix and watching it fail; two of them were vacuous when first written and are noted as such where they are fixed. The real-keyboard test also now rubs out a character and scrolls forty lines. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 15:03:37 -04:00
int erasedto = 0;
int endedat = 0;
state->cursorrow = state->editrow;
state->cursorcol = state->editcol;
for ( i = 0; i < state->echolen; i++ ) {
putchar_at(state, ' ');
}
Erase what the text layer drew: three GUI bugs, one cause The sink painted glyphs on top of whatever was already on the renderer and never erased anything, and the host deliberately does not clear the frame so a DRAW survives. The grid was always correct; the screen kept the previous frame. That surfaced as three separate-looking faults: - a backspaced character stayed on screen, including across a line wrap - a scroll left the old rows behind, "papered over with garbage" - the editing cursor smeared an underscore along every column it passed through, which read as an underline under the text being typed render() now fills each row it is about to draw, and each row that has emptied since it last drew, with the background first. Row by row rather than one clear over the whole area: the text layer is authoritative over the rows it occupies and leaves every other pixel alone, so a picture behind the text loses only the strips the text uses instead of all of it. echo_line() also truncates rows that hold nothing but the spaces its own erase pass wrote, which is what backspacing back across a wrap leaves behind. A row of spaces is text as far as render() is concerned, so it would have been repainted forever and kept erasing whatever was under it. The cursor glyph is removed outright, as asked. The smearing was the erase bug rather than the cursor, so one could come back and behave -- a block would read better than an underscore. Four pixel-level tests, because grid assertions could never have caught any of this. Each was checked by reverting the fix and watching it fail; two of them were vacuous when first written and are noted as such where they are fixed. The real-keyboard test also now rubs out a character and scrolls forty lines. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 15:03:37 -04:00
erasedto = state->cursorrow;
state->cursorrow = state->editrow;
state->cursorcol = state->editcol;
for ( i = 0; i < state->editlen; i++ ) {
putchar_at(state, state->editline[i]);
}
Erase what the text layer drew: three GUI bugs, one cause The sink painted glyphs on top of whatever was already on the renderer and never erased anything, and the host deliberately does not clear the frame so a DRAW survives. The grid was always correct; the screen kept the previous frame. That surfaced as three separate-looking faults: - a backspaced character stayed on screen, including across a line wrap - a scroll left the old rows behind, "papered over with garbage" - the editing cursor smeared an underscore along every column it passed through, which read as an underline under the text being typed render() now fills each row it is about to draw, and each row that has emptied since it last drew, with the background first. Row by row rather than one clear over the whole area: the text layer is authoritative over the rows it occupies and leaves every other pixel alone, so a picture behind the text loses only the strips the text uses instead of all of it. echo_line() also truncates rows that hold nothing but the spaces its own erase pass wrote, which is what backspacing back across a wrap leaves behind. A row of spaces is text as far as render() is concerned, so it would have been repainted forever and kept erasing whatever was under it. The cursor glyph is removed outright, as asked. The smearing was the erase bug rather than the cursor, so one could come back and behave -- a block would read better than an underscore. Four pixel-level tests, because grid assertions could never have caught any of this. Each was checked by reverting the fix and watching it fail; two of them were vacuous when first written and are noted as such where they are fixed. The real-keyboard test also now rubs out a character and scrolls forty lines. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 15:03:37 -04:00
endedat = state->cursorrow;
state->echolen = state->editlen;
Erase what the text layer drew: three GUI bugs, one cause The sink painted glyphs on top of whatever was already on the renderer and never erased anything, and the host deliberately does not clear the frame so a DRAW survives. The grid was always correct; the screen kept the previous frame. That surfaced as three separate-looking faults: - a backspaced character stayed on screen, including across a line wrap - a scroll left the old rows behind, "papered over with garbage" - the editing cursor smeared an underscore along every column it passed through, which read as an underline under the text being typed render() now fills each row it is about to draw, and each row that has emptied since it last drew, with the background first. Row by row rather than one clear over the whole area: the text layer is authoritative over the rows it occupies and leaves every other pixel alone, so a picture behind the text loses only the strips the text uses instead of all of it. echo_line() also truncates rows that hold nothing but the spaces its own erase pass wrote, which is what backspacing back across a wrap leaves behind. A row of spaces is text as far as render() is concerned, so it would have been repainted forever and kept erasing whatever was under it. The cursor glyph is removed outright, as asked. The smearing was the erase bug rather than the cursor, so one could come back and behave -- a block would read better than an underscore. Four pixel-level tests, because grid assertions could never have caught any of this. Each was checked by reverting the fix and watching it fail; two of them were vacuous when first written and are noted as such where they are fixed. The real-keyboard test also now rubs out a character and scrolls forty lines. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 15:03:37 -04:00
/*
* A line that got shorter can leave whole rows holding nothing but the
* spaces the erase pass just wrote -- which is what backspacing back across
* a wrap does to the row below. Truncate them, so such a row reports itself
* *empty* rather than merely blank-looking.
*
* That distinction is load-bearing rather than tidiness: render() decides
* what to erase and redraw from whether a row has text, and a row of spaces
* is text. Left alone it would be repainted forever, and anything the
* program had drawn underneath it would stay erased.
*/
for ( i = endedat + 1; i <= erasedto && i < SINK_MAX_ROWS; i++ ) {
state->text[i][0] = '\0';
}
}
/** @brief Append one byte to the line being typed, if there is room for it. */
static void edit_append(akbasic_AkglSink *state, char c)
{
if ( state->editlen >= (int)sizeof(state->editline) - 1 ) {
/* Full. Dropped silently, exactly as a C128's 80-character limit does. */
return;
}
state->editline[state->editlen] = c;
state->editlen += 1;
state->editline[state->editlen] = '\0';
}
/**
* @brief Fold one keystroke into the line being typed.
*
* **The composed text is preferred over the keycode wherever there is any**, and
* that is the whole reason libakgl 0.3.0's akgl_Keystroke exists. A keycode
* cannot express a shifted character, a keyboard layout, a compose key or a dead
* key; SDL has already worked all of that out by the time the ring sees it, and
* `text` is the answer. Before 0.3.0 this took a bare keycode, folded letters to
* upper case, and could not type a double quote -- which meant a BASIC string
* literal could not be typed at the window at all.
*
* The keycode is still what identifies the editing keys, because Return,
* Backspace and Escape are keys rather than characters and several of them
* compose to text SDL would otherwise hand straight through.
*/
static void edit_key(akbasic_AkglSink *state, const akgl_Keystroke *key, bool *submitted)
{
size_t i = 0;
if ( key->key == SDLK_RETURN || key->key == SDLK_KP_ENTER ||
key->key == '\r' || key->key == '\n' ) {
*submitted = true;
return;
}
if ( key->key == SDLK_BACKSPACE || key->key == '\b' || key->key == 0x7f ) {
if ( state->editlen > 0 ) {
/*
* One *byte* at a time, which is wrong for a multi-byte character and
* is left that way deliberately: every character a BASIC program can
* hold is one byte (section 1.2's inline string), so a multi-byte
* character cannot survive being submitted anyway. Erasing what was
* accepted is consistent with that.
*/
state->editlen -= 1;
state->editline[state->editlen] = '\0';
echo_line(state);
}
return;
}
if ( key->key == SDLK_ESCAPE || key->key == 0x1b ) {
state->editlen = 0;
state->editline[0] = '\0';
echo_line(state);
return;
}
if ( key->text[0] != '\0' ) {
for ( i = 0; key->text[i] != '\0' && i < sizeof(key->text); i++ ) {
/*
* Printable ASCII only. 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.
*/
if ( (unsigned char)key->text[i] >= 0x20 && (unsigned char)key->text[i] < 0x7f ) {
edit_append(state, key->text[i]);
}
}
echo_line(state);
return;
}
/*
* No composed text, but a printable keycode: type it anyway.
*
* **This fallback is why the editor still works on a host that never called
* SDL_StartTextInput().** SDL emits no SDL_EVENT_TEXT_INPUT until text input
* is started, so without it every keystroke arrives here with an empty
* `text` -- and treating that as "not a character" makes the entire keyboard
* dead, silently, which is exactly what happened once. A worse keyboard is a
* great deal better than no keyboard.
*
* Upper case, because that is all a bare keycode can offer and it is what a
* C128 does. When text input *is* running a printable key always carries
* text, so this never fires and lower case survives.
*/
if ( key->key >= 0x20 && key->key < 0x7f ) {
edit_append(state, (char)toupper((unsigned char)key->key));
echo_line(state);
return;
}
/*
* Neither: a cursor key, a function key or a bare modifier. Not an editing
* command here -- a script's own GET loop is what wants those.
*/
}
/**
* @brief Collect keystrokes until a line is submitted or the host stops.
*
* Its own function because it is a loop: CATCH and the _BREAK macros expand to a
* C break, which inside a loop would escape only the loop and leave the rest of
* an ATTEMPT running with an error pending. PASS only in here, and the caller
* wraps this one call in the ATTEMPT that owns the cleanup.
*/
static akerr_ErrorContext AKERR_NOIGNORE *edit_loop(akbasic_AkglSink *state, bool *eof)
{
PREPARE_ERROR(errctx);
akgl_Keystroke key;
bool submitted = false;
bool available = false;
bool running = true;
while ( !submitted ) {
/*
* poll_keystroke rather than poll_key: the same ring, with the modifier
* state and the composed text still attached. poll_key is the reduced
* view, and it is what GET and GETKEY still use -- a script asking "was
* the up arrow pressed" wants a keycode, not the empty string.
*/
PASS(errctx, akgl_controller_poll_keystroke(&key, &available));
if ( available ) {
edit_key(state, &key, &submitted);
continue;
}
/*
* Nothing waiting: hand the frame back to the host, which pumps the
* events that fill the ring this loop is reading. Skipping the pump
* while keys are available is what keeps a paste or a fast typist from
* costing one frame per character.
*/
PASS(errctx, state->pump(state->pumpself, &running));
if ( !running ) {
*eof = true;
SUCCEED_RETURN(errctx);
}
}
SUCCEED_RETURN(errctx);
}
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>
2026-07-31 08:46:53 -04:00
static akerr_ErrorContext *sink_readline(akbasic_TextSink *self, char *dest, size_t len, bool *eof)
{
PREPARE_ERROR(errctx);
akbasic_AkglSink *state = NULL;
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>
2026-07-31 08:46:53 -04:00
FAIL_ZERO_RETURN(errctx, (self != NULL && dest != NULL && eof != NULL), AKERR_NULLPOINTER,
"NULL argument in akgl sink readline");
FAIL_ZERO_RETURN(errctx, (len > 1), AKBASIC_ERR_BOUNDS,
"Read buffer of %zu bytes is too small", len);
state = (akbasic_AkglSink *)self->self;
FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER,
"akgl sink has no state");
*eof = false;
dest[0] = '\0';
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>
2026-07-31 08:46:53 -04:00
/*
* No pump means no host loop to borrow, and a sink that sat on the keyboard
* without one would deadlock the process on its first INPUT. Report end of
* input instead -- the contract sink.h states, and what INPUT already
* handles.
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>
2026-07-31 08:46:53 -04:00
*/
if ( state->pump == NULL ) {
*eof = true;
SUCCEED_RETURN(errctx);
}
state->editing = true;
state->editrow = state->cursorrow;
state->editcol = state->cursorcol;
state->editlen = 0;
state->echolen = 0;
state->editline[0] = '\0';
ATTEMPT {
CATCH(errctx, edit_loop(state, eof));
} CLEANUP {
/* Whatever happened, stop drawing a cursor over a line nobody is typing. */
state->editing = false;
} PROCESS(errctx) {
} FINISH(errctx, true);
if ( *eof ) {
SUCCEED_RETURN(errctx);
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>
2026-07-31 08:46:53 -04:00
}
strncpy(dest, state->editline, len - 1);
dest[len - 1] = '\0';
newline(state);
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>
2026-07-31 08:46:53 -04:00
SUCCEED_RETURN(errctx);
}
static akerr_ErrorContext *sink_clear(akbasic_TextSink *self)
{
PREPARE_ERROR(errctx);
akbasic_AkglSink *state = NULL;
FAIL_ZERO_RETURN(errctx, (self != NULL), AKERR_NULLPOINTER,
"NULL sink in akgl sink clear");
state = (akbasic_AkglSink *)self->self;
FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER,
"akgl sink has no state");
memset(state->text, 0, sizeof(state->text));
state->cursorcol = 0;
state->cursorrow = 0;
SUCCEED_RETURN(errctx);
}
Finish the language: every remaining verb group, and the defects that blocked them Closes groups A, C, D, E, F, H and J of TODO.md section 4, plus RESTORE and RENUMBER, and closes section 6 -- all seventeen reference defects. Seven of those turned out to have been fixed or never ported and nobody had written it down; the audit records the evidence for each. Two of the seventeen were real. math_plus mutated its left operand when the operand was mutable, so A# + 1 could modify A#; it was gated on FOR/NEXT coverage because NEXT relied on the mutation, so tests/for_next.c came first and NEXT now writes the counter back itself. And the binary operators summed both numeric fields of their right operand, which no BASIC program can reach -- that one needed a test written against the value API. Writing the tests turned up eight defects nobody had listed. Seven are fixed: IF A = 2 THEN was a parse error; only == worked IF ... AND ... was a parse error, because a condition parsed as one relation IF A = 1 OR B = 2 THEN was silently always false, and so was IF A THEN EXIT before any NEXT restarted the program and exhausted the variable pool READ never found a DATA line above it, and swallowed the lines between PRINT 2 + 2 at the prompt was filed as program text instead of answering a short read discarded its bytes, so COPY produced empty files every verb taking an argument list said "peek() returned nil token!" on none The eighth is not fixed and cannot be quietly: a FOR whose step overshoots runs its body one extra time, and FOR I = 1 TO 1 runs it zero times. The two errors cancel for a step of 1, which is why neither was noticed. Correcting them changes the expected output of a checked-in acceptance file, and tests/reference/README.md forbids editing one to suit this interpreter. It is tests/for_semantics.c in AKBASIC_KNOWN_FAILING_TESTS, asserting the correct contract, and TODO.md items 19 and 20. Sprites are real libakgl actors with a renderfunc of their own, because akgl_actor_render draws every sprite square and an actor has no per-axis scale. Both are filed upstream. SPRSAV takes an image file, an SSHAPE handle or a 63-element integer array -- a string here cannot hold a zero byte. Verbs that need hardware that does not exist are refused by name with the reason rather than faked: SYS, HEADER, COLLECT, BACKUP, BOOT, FILTER, and DIRECTORY, which is refused for a missing libakstdlib wrapper filed upstream. 94 tests in the default build, 93 with SDL, 94 under ASan and UBSan, doxygen clean. The Go acceptance corpus stayed green throughout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 21:50:37 -04:00
/**
* @brief Put the cursor at a character cell, for CHAR.
*
* Clamped rather than refused: a program that asks for a column past the edge of
* a window it cannot measure has made an ordinary mistake, and a C128 clamps too.
*/
static akerr_ErrorContext *sink_moveto(akbasic_TextSink *self, int col, int row)
{
PREPARE_ERROR(errctx);
akbasic_AkglSink *state = NULL;
FAIL_ZERO_RETURN(errctx, (self != NULL), AKERR_NULLPOINTER, "NULL sink in moveto");
state = (akbasic_AkglSink *)self->self;
FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER, "akgl sink has no state");
if ( col < 0 ) { col = 0; }
if ( row < 0 ) { row = 0; }
if ( col >= state->columns ) { col = state->columns - 1; }
if ( row >= state->rows ) { row = state->rows - 1; }
state->cursorcol = col;
state->cursorrow = row;
SUCCEED_RETURN(errctx);
}
/**
* @brief Constrain the text area to a rectangle of cells, for WINDOW.
*
* The sink already drives everything -- wrap, scroll, cursor -- from `x`, `y`,
* `columns` and `rows`, so a window is those four fields and nothing else. The
* cell size does not change, which is what keeps a windowed program's text the
* same size as an unwindowed one's.
*
* The full-screen geometry is remembered so a later WINDOW can grow back out;
* without it each call could only ever shrink.
*/
Let a program ask how big the text grid is `RGR(1)` and `RGR(2)` gave the window in pixels and nothing gave columns, rows or the cell size -- so anything placing a character *and* a sprite at the same spot had to hardcode a number measured by hand against whatever font the host loaded. The Breakout in examples/ does exactly that, `CW# = 16`, and it is the one thing in that listing that breaks on a different font or window. **`RWINDOW` is BASIC 7.0's own answer and had never been implemented here.** `RWINDOW(0)` is the current text window's rows and `RWINDOW(1)` its columns. `RWINDOW(2)` reports a C128's 40 or 80 column screen mode, and this interpreter has neither -- refused by name, because answering 0 would be a plausible lie, which is worse than a refusal that says why. The cell size in pixels is `RGR(3)` and `RGR(4)`, beside the surface's own dimensions rather than on `RWINDOW`. Two reasons: a cell size is a fact about the surface, and `RWINDOW` reports the *window*, so dividing `RGR(1)` by a column count stops being right the moment a program calls `WINDOW`. Both read a new optional `grid` entry point on `akbasic_TextSink` -- columns, rows, cell width, cell height -- implemented by the akgl sink and forwarded by the tee, in the shape `moveto` and `window` already had. NULL everywhere else, so both verbs refuse by name against a sink with no grid. `akbasic_sink_init_ stdio()` clears it for the same reason it now clears the other two. Measured on the standalone build: `RGR(3)` answers 16 and `RWINDOW` answers 50 columns by 37 rows -- the three numbers the Breakout listing had written out as constants -- and `RWINDOW` follows a `WINDOW` call while `RGR(3)` does not. tests/console_verbs.c drives the answers through a stand-in sink with a grid, since the harness sink is stdio and has none; tests/graphics_verbs.c covers the new `RGR` fields, their refusal, and the moved range bound. The `c excerpt=` block in docs/10-embedding.md moves with the header, which is `docs_examples` doing its job. TODO.md section 6 item 31's second half, struck. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 00:05:17 -04:00
/**
* @brief Report the character grid, for RWINDOW and RGR.
*
* The four numbers a program cannot otherwise learn. Columns and rows are the
* *current* window rather than the whole screen, which is what RWINDOW means on
* a C128 and what a program placing text actually needs; the cell size is
* unaffected by windowing, which is what keeps a windowed program's text the
* same size as an unwindowed one's.
*/
static akerr_ErrorContext *sink_grid(akbasic_TextSink *self, int *columns, int *rows, int *cellw, int *cellh)
{
PREPARE_ERROR(errctx);
akbasic_AkglSink *state = NULL;
FAIL_ZERO_RETURN(errctx, (self != NULL), AKERR_NULLPOINTER, "NULL sink in grid");
FAIL_ZERO_RETURN(errctx, (columns != NULL && rows != NULL && cellw != NULL && cellh != NULL),
AKERR_NULLPOINTER, "NULL destination in grid");
state = (akbasic_AkglSink *)self->self;
FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER, "akgl sink has no state");
FAIL_ZERO_RETURN(errctx, (state->cellw > 0 && state->cellh > 0), AKBASIC_ERR_STATE,
"The sink has no character grid to measure");
*columns = state->columns;
*rows = state->rows;
*cellw = state->cellw;
*cellh = state->cellh;
SUCCEED_RETURN(errctx);
}
Finish the language: every remaining verb group, and the defects that blocked them Closes groups A, C, D, E, F, H and J of TODO.md section 4, plus RESTORE and RENUMBER, and closes section 6 -- all seventeen reference defects. Seven of those turned out to have been fixed or never ported and nobody had written it down; the audit records the evidence for each. Two of the seventeen were real. math_plus mutated its left operand when the operand was mutable, so A# + 1 could modify A#; it was gated on FOR/NEXT coverage because NEXT relied on the mutation, so tests/for_next.c came first and NEXT now writes the counter back itself. And the binary operators summed both numeric fields of their right operand, which no BASIC program can reach -- that one needed a test written against the value API. Writing the tests turned up eight defects nobody had listed. Seven are fixed: IF A = 2 THEN was a parse error; only == worked IF ... AND ... was a parse error, because a condition parsed as one relation IF A = 1 OR B = 2 THEN was silently always false, and so was IF A THEN EXIT before any NEXT restarted the program and exhausted the variable pool READ never found a DATA line above it, and swallowed the lines between PRINT 2 + 2 at the prompt was filed as program text instead of answering a short read discarded its bytes, so COPY produced empty files every verb taking an argument list said "peek() returned nil token!" on none The eighth is not fixed and cannot be quietly: a FOR whose step overshoots runs its body one extra time, and FOR I = 1 TO 1 runs it zero times. The two errors cancel for a step of 1, which is why neither was noticed. Correcting them changes the expected output of a checked-in acceptance file, and tests/reference/README.md forbids editing one to suit this interpreter. It is tests/for_semantics.c in AKBASIC_KNOWN_FAILING_TESTS, asserting the correct contract, and TODO.md items 19 and 20. Sprites are real libakgl actors with a renderfunc of their own, because akgl_actor_render draws every sprite square and an actor has no per-axis scale. Both are filed upstream. SPRSAV takes an image file, an SSHAPE handle or a 63-element integer array -- a string here cannot hold a zero byte. Verbs that need hardware that does not exist are refused by name with the reason rather than faked: SYS, HEADER, COLLECT, BACKUP, BOOT, FILTER, and DIRECTORY, which is refused for a missing libakstdlib wrapper filed upstream. 94 tests in the default build, 93 with SDL, 94 under ASan and UBSan, doxygen clean. The Go acceptance corpus stayed green throughout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 21:50:37 -04:00
static akerr_ErrorContext *sink_window(akbasic_TextSink *self, int left, int top, int right, int bottom)
{
PREPARE_ERROR(errctx);
akbasic_AkglSink *state = NULL;
int maxcols = 0;
int maxrows = 0;
FAIL_ZERO_RETURN(errctx, (self != NULL), AKERR_NULLPOINTER, "NULL sink in window");
state = (akbasic_AkglSink *)self->self;
FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER, "akgl sink has no state");
FAIL_ZERO_RETURN(errctx, (state->cellw > 0 && state->cellh > 0), AKBASIC_ERR_STATE,
"The sink has no character grid to window");
maxcols = state->fullwidth / state->cellw;
maxrows = state->fullheight / state->cellh;
if ( left < 0 ) { left = 0; }
if ( top < 0 ) { top = 0; }
if ( right >= maxcols ) { right = maxcols - 1; }
if ( bottom >= maxrows ) { bottom = maxrows - 1; }
FAIL_ZERO_RETURN(errctx, (right >= left && bottom >= top), AKBASIC_ERR_VALUE,
"WINDOW asks for no cells at all");
state->x = state->fullx + (left * state->cellw);
state->y = state->fully + (top * state->cellh);
state->columns = (right - left) + 1;
state->rows = (bottom - top) + 1;
state->width = state->columns * state->cellw;
state->height = state->rows * state->cellh;
state->cursorcol = 0;
state->cursorrow = 0;
SUCCEED_RETURN(errctx);
}
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>
2026-07-31 08:46:53 -04:00
akerr_ErrorContext *akbasic_sink_init_akgl(akbasic_TextSink *obj, akbasic_AkglSink *state, akgl_RenderBackend *renderer, TTF_Font *font, int w, int h)
{
PREPARE_ERROR(errctx);
int cellw = 0;
int cellh = 0;
FAIL_ZERO_RETURN(errctx, (obj != NULL && state != NULL), AKERR_NULLPOINTER,
"NULL argument in sink_init_akgl");
FAIL_ZERO_RETURN(errctx, (renderer != NULL), AKERR_NULLPOINTER,
"NULL renderer in sink_init_akgl: the host creates it, not the sink");
FAIL_ZERO_RETURN(errctx, (font != NULL), AKERR_NULLPOINTER,
"NULL font in sink_init_akgl");
/*
* Before anything else in libakgl. akgl_game_init() would have done it, but
* an embedded interpreter drives subsystems directly and never calls that --
* and a code raised before this runs carries no name into its stack trace.
* Idempotent, so a host that already called it loses nothing.
*/
PASS(errctx, akgl_error_init());
/*
* The character grid, measured rather than assumed. Direct equivalent of the
* reference's font.SizeUTF8("A"), and it needs no renderer -- which is why
* the sink can size itself before a frame has ever been drawn.
*/
PASS(errctx, akgl_text_measure(font, "A", &cellw, &cellh));
FAIL_ZERO_RETURN(errctx, (cellw > 0 && cellh > 0), AKBASIC_ERR_VALUE,
"Font measures a %dx%d character cell, which cannot be a grid",
cellw, cellh);
FAIL_ZERO_RETURN(errctx, (w >= cellw && h >= cellh), AKBASIC_ERR_BOUNDS,
"A %dx%d text area has no room for a %dx%d character",
w, h, cellw, cellh);
memset(state, 0, sizeof(*state));
state->renderer = renderer;
state->font = font;
state->color.r = 0xff;
state->color.g = 0xff;
state->color.b = 0xff;
state->color.a = 0xff;
Erase what the text layer drew: three GUI bugs, one cause The sink painted glyphs on top of whatever was already on the renderer and never erased anything, and the host deliberately does not clear the frame so a DRAW survives. The grid was always correct; the screen kept the previous frame. That surfaced as three separate-looking faults: - a backspaced character stayed on screen, including across a line wrap - a scroll left the old rows behind, "papered over with garbage" - the editing cursor smeared an underscore along every column it passed through, which read as an underline under the text being typed render() now fills each row it is about to draw, and each row that has emptied since it last drew, with the background first. Row by row rather than one clear over the whole area: the text layer is authoritative over the rows it occupies and leaves every other pixel alone, so a picture behind the text loses only the strips the text uses instead of all of it. echo_line() also truncates rows that hold nothing but the spaces its own erase pass wrote, which is what backspacing back across a wrap leaves behind. A row of spaces is text as far as render() is concerned, so it would have been repainted forever and kept erasing whatever was under it. The cursor glyph is removed outright, as asked. The smearing was the erase bug rather than the cursor, so one could come back and behave -- a block would read better than an underscore. Four pixel-level tests, because grid assertions could never have caught any of this. Each was checked by reverting the fix and watching it fail; two of them were vacuous when first written and are noted as such where they are fixed. The real-keyboard test also now rubs out a character and scrolls forty lines. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 15:03:37 -04:00
/* Opaque, not transparent: the text layer owns the rows it draws. */
Own every pixel each frame, and bring the cursor back as a blinking block The blinking backspace was not the backspace. The sink skipped rows it believed were already clear, tracked in a drawn[] array, which assumes a frame inherits the frame before it. SDL_RenderPresent swaps buffers, so a frame inherits the one two back: a row erased once is clean in one buffer and dirty in the other, and presenting alternates between them. The first character of a backspaced wrap flickered forever. It reproduces on X11 and never under the dummy driver or a software renderer, which is why the suite stayed green through two rounds of fixing it. The sink now repaints every row it owns every frame -- a frame either owns every pixel it presents or inherits pixels it cannot reason about. Confirmed on real hardware with the reported input: the wrapped tail's row reads zero across five successive captures. The cursor returns as a blinking block, half a second per cycle, off SDL's clock. It sits in the cell after the text rather than under it, which is what made the underscore unreadable, and it wraps to the next row when a line exactly fills one -- where the next character actually lands. The new regression test paints stale pixels by hand, which is what a swapped-in buffer hands back, so the case is covered without real hardware. Every new assertion was checked by reverting the fix and watching it fail. Recorded honestly in TODO.md section 5: the same buffer swap means the graphics verbs were never reliable across frames either. A one-shot DRAW lands in one buffer and the next present shows the other. Making that work needs a persistent surface, which is its own commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 15:23:41 -04:00
state->cursorperiodms = AKBASIC_SINK_CURSOR_BLINK_MS;
Erase what the text layer drew: three GUI bugs, one cause The sink painted glyphs on top of whatever was already on the renderer and never erased anything, and the host deliberately does not clear the frame so a DRAW survives. The grid was always correct; the screen kept the previous frame. That surfaced as three separate-looking faults: - a backspaced character stayed on screen, including across a line wrap - a scroll left the old rows behind, "papered over with garbage" - the editing cursor smeared an underscore along every column it passed through, which read as an underline under the text being typed render() now fills each row it is about to draw, and each row that has emptied since it last drew, with the background first. Row by row rather than one clear over the whole area: the text layer is authoritative over the rows it occupies and leaves every other pixel alone, so a picture behind the text loses only the strips the text uses instead of all of it. echo_line() also truncates rows that hold nothing but the spaces its own erase pass wrote, which is what backspacing back across a wrap leaves behind. A row of spaces is text as far as render() is concerned, so it would have been repainted forever and kept erasing whatever was under it. The cursor glyph is removed outright, as asked. The smearing was the erase bug rather than the cursor, so one could come back and behave -- a block would read better than an underscore. Four pixel-level tests, because grid assertions could never have caught any of this. Each was checked by reverting the fix and watching it fail; two of them were vacuous when first written and are noted as such where they are fixed. The real-keyboard test also now rubs out a character and scrolls forty lines. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 15:03:37 -04:00
state->background.r = 0x00;
state->background.g = 0x00;
state->background.b = 0x00;
state->background.a = 0xff;
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>
2026-07-31 08:46:53 -04:00
state->x = 0;
state->y = 0;
state->width = w;
state->height = h;
Finish the language: every remaining verb group, and the defects that blocked them Closes groups A, C, D, E, F, H and J of TODO.md section 4, plus RESTORE and RENUMBER, and closes section 6 -- all seventeen reference defects. Seven of those turned out to have been fixed or never ported and nobody had written it down; the audit records the evidence for each. Two of the seventeen were real. math_plus mutated its left operand when the operand was mutable, so A# + 1 could modify A#; it was gated on FOR/NEXT coverage because NEXT relied on the mutation, so tests/for_next.c came first and NEXT now writes the counter back itself. And the binary operators summed both numeric fields of their right operand, which no BASIC program can reach -- that one needed a test written against the value API. Writing the tests turned up eight defects nobody had listed. Seven are fixed: IF A = 2 THEN was a parse error; only == worked IF ... AND ... was a parse error, because a condition parsed as one relation IF A = 1 OR B = 2 THEN was silently always false, and so was IF A THEN EXIT before any NEXT restarted the program and exhausted the variable pool READ never found a DATA line above it, and swallowed the lines between PRINT 2 + 2 at the prompt was filed as program text instead of answering a short read discarded its bytes, so COPY produced empty files every verb taking an argument list said "peek() returned nil token!" on none The eighth is not fixed and cannot be quietly: a FOR whose step overshoots runs its body one extra time, and FOR I = 1 TO 1 runs it zero times. The two errors cancel for a step of 1, which is why neither was noticed. Correcting them changes the expected output of a checked-in acceptance file, and tests/reference/README.md forbids editing one to suit this interpreter. It is tests/for_semantics.c in AKBASIC_KNOWN_FAILING_TESTS, asserting the correct contract, and TODO.md items 19 and 20. Sprites are real libakgl actors with a renderfunc of their own, because akgl_actor_render draws every sprite square and an actor has no per-axis scale. Both are filed upstream. SPRSAV takes an image file, an SSHAPE handle or a 63-element integer array -- a string here cannot hold a zero byte. Verbs that need hardware that does not exist are refused by name with the reason rather than faked: SYS, HEADER, COLLECT, BACKUP, BOOT, FILTER, and DIRECTORY, which is refused for a missing libakstdlib wrapper filed upstream. 94 tests in the default build, 93 with SDL, 94 under ASan and UBSan, doxygen clean. The Go acceptance corpus stayed green throughout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 21:50:37 -04:00
/* Remembered so WINDOW can grow back out to the whole area. */
state->fullx = state->x;
state->fully = state->y;
state->fullwidth = w;
state->fullheight = h;
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>
2026-07-31 08:46:53 -04:00
state->cellw = cellw;
state->cellh = cellh;
state->columns = w / cellw;
state->rows = h / cellh;
if ( state->columns > SINK_MAX_COLUMNS - 1 ) {
state->columns = SINK_MAX_COLUMNS - 1;
}
if ( state->rows > SINK_MAX_ROWS ) {
state->rows = SINK_MAX_ROWS;
}
obj->self = state;
obj->write = sink_write;
obj->writeln = sink_writeln;
obj->readline = sink_readline;
obj->clear = sink_clear;
Finish the language: every remaining verb group, and the defects that blocked them Closes groups A, C, D, E, F, H and J of TODO.md section 4, plus RESTORE and RENUMBER, and closes section 6 -- all seventeen reference defects. Seven of those turned out to have been fixed or never ported and nobody had written it down; the audit records the evidence for each. Two of the seventeen were real. math_plus mutated its left operand when the operand was mutable, so A# + 1 could modify A#; it was gated on FOR/NEXT coverage because NEXT relied on the mutation, so tests/for_next.c came first and NEXT now writes the counter back itself. And the binary operators summed both numeric fields of their right operand, which no BASIC program can reach -- that one needed a test written against the value API. Writing the tests turned up eight defects nobody had listed. Seven are fixed: IF A = 2 THEN was a parse error; only == worked IF ... AND ... was a parse error, because a condition parsed as one relation IF A = 1 OR B = 2 THEN was silently always false, and so was IF A THEN EXIT before any NEXT restarted the program and exhausted the variable pool READ never found a DATA line above it, and swallowed the lines between PRINT 2 + 2 at the prompt was filed as program text instead of answering a short read discarded its bytes, so COPY produced empty files every verb taking an argument list said "peek() returned nil token!" on none The eighth is not fixed and cannot be quietly: a FOR whose step overshoots runs its body one extra time, and FOR I = 1 TO 1 runs it zero times. The two errors cancel for a step of 1, which is why neither was noticed. Correcting them changes the expected output of a checked-in acceptance file, and tests/reference/README.md forbids editing one to suit this interpreter. It is tests/for_semantics.c in AKBASIC_KNOWN_FAILING_TESTS, asserting the correct contract, and TODO.md items 19 and 20. Sprites are real libakgl actors with a renderfunc of their own, because akgl_actor_render draws every sprite square and an actor has no per-axis scale. Both are filed upstream. SPRSAV takes an image file, an SSHAPE handle or a 63-element integer array -- a string here cannot hold a zero byte. Verbs that need hardware that does not exist are refused by name with the reason rather than faked: SYS, HEADER, COLLECT, BACKUP, BOOT, FILTER, and DIRECTORY, which is refused for a missing libakstdlib wrapper filed upstream. 94 tests in the default build, 93 with SDL, 94 under ASan and UBSan, doxygen clean. The Go acceptance corpus stayed green throughout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 21:50:37 -04:00
obj->moveto = sink_moveto;
obj->window = sink_window;
Let a program ask how big the text grid is `RGR(1)` and `RGR(2)` gave the window in pixels and nothing gave columns, rows or the cell size -- so anything placing a character *and* a sprite at the same spot had to hardcode a number measured by hand against whatever font the host loaded. The Breakout in examples/ does exactly that, `CW# = 16`, and it is the one thing in that listing that breaks on a different font or window. **`RWINDOW` is BASIC 7.0's own answer and had never been implemented here.** `RWINDOW(0)` is the current text window's rows and `RWINDOW(1)` its columns. `RWINDOW(2)` reports a C128's 40 or 80 column screen mode, and this interpreter has neither -- refused by name, because answering 0 would be a plausible lie, which is worse than a refusal that says why. The cell size in pixels is `RGR(3)` and `RGR(4)`, beside the surface's own dimensions rather than on `RWINDOW`. Two reasons: a cell size is a fact about the surface, and `RWINDOW` reports the *window*, so dividing `RGR(1)` by a column count stops being right the moment a program calls `WINDOW`. Both read a new optional `grid` entry point on `akbasic_TextSink` -- columns, rows, cell width, cell height -- implemented by the akgl sink and forwarded by the tee, in the shape `moveto` and `window` already had. NULL everywhere else, so both verbs refuse by name against a sink with no grid. `akbasic_sink_init_ stdio()` clears it for the same reason it now clears the other two. Measured on the standalone build: `RGR(3)` answers 16 and `RWINDOW` answers 50 columns by 37 rows -- the three numbers the Breakout listing had written out as constants -- and `RWINDOW` follows a `WINDOW` call while `RGR(3)` does not. tests/console_verbs.c drives the answers through a stand-in sink with a grid, since the harness sink is stdio and has none; tests/graphics_verbs.c covers the new `RGR` fields, their refusal, and the moved range bound. The `c excerpt=` block in docs/10-embedding.md moves with the header, which is `docs_examples` doing its job. TODO.md section 6 item 31's second half, struck. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 00:05:17 -04:00
obj->grid = sink_grid;
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>
2026-07-31 08:46:53 -04:00
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_sink_akgl_render(akbasic_TextSink *obj)
{
PREPARE_ERROR(errctx);
akbasic_AkglSink *state = NULL;
int row = 0;
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER,
"NULL sink in sink_akgl_render");
state = (akbasic_AkglSink *)obj->self;
FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER,
"akgl sink has no state");
/*
Own every pixel each frame, and bring the cursor back as a blinking block The blinking backspace was not the backspace. The sink skipped rows it believed were already clear, tracked in a drawn[] array, which assumes a frame inherits the frame before it. SDL_RenderPresent swaps buffers, so a frame inherits the one two back: a row erased once is clean in one buffer and dirty in the other, and presenting alternates between them. The first character of a backspaced wrap flickered forever. It reproduces on X11 and never under the dummy driver or a software renderer, which is why the suite stayed green through two rounds of fixing it. The sink now repaints every row it owns every frame -- a frame either owns every pixel it presents or inherits pixels it cannot reason about. Confirmed on real hardware with the reported input: the wrapped tail's row reads zero across five successive captures. The cursor returns as a blinking block, half a second per cycle, off SDL's clock. It sits in the cell after the text rather than under it, which is what made the underscore unreadable, and it wraps to the next row when a line exactly fills one -- where the next character actually lands. The new regression test paints stale pixels by hand, which is what a swapped-in buffer hands back, so the case is covered without real hardware. Every new assertion was checked by reverting the fix and watching it fail. Recorded honestly in TODO.md section 5: the same buffer swap means the graphics verbs were never reliable across frames either. A one-shot DRAW lands in one buffer and the next present shows the other. Making that work needs a persistent surface, which is its own commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 15:23:41 -04:00
* **Repaint every row of the text area, every frame.** Not just the rows
* that changed -- that was tried, with a `drawn[]` array marking which rows
* had carried glyphs, and it is wrong on real hardware.
Erase what the text layer drew: three GUI bugs, one cause The sink painted glyphs on top of whatever was already on the renderer and never erased anything, and the host deliberately does not clear the frame so a DRAW survives. The grid was always correct; the screen kept the previous frame. That surfaced as three separate-looking faults: - a backspaced character stayed on screen, including across a line wrap - a scroll left the old rows behind, "papered over with garbage" - the editing cursor smeared an underscore along every column it passed through, which read as an underline under the text being typed render() now fills each row it is about to draw, and each row that has emptied since it last drew, with the background first. Row by row rather than one clear over the whole area: the text layer is authoritative over the rows it occupies and leaves every other pixel alone, so a picture behind the text loses only the strips the text uses instead of all of it. echo_line() also truncates rows that hold nothing but the spaces its own erase pass wrote, which is what backspacing back across a wrap leaves behind. A row of spaces is text as far as render() is concerned, so it would have been repainted forever and kept erasing whatever was under it. The cursor glyph is removed outright, as asked. The smearing was the erase bug rather than the cursor, so one could come back and behave -- a block would read better than an underscore. Four pixel-level tests, because grid assertions could never have caught any of this. Each was checked by reverting the fix and watching it fail; two of them were vacuous when first written and are noted as such where they are fixed. The real-keyboard test also now rubs out a character and scrolls forty lines. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 15:03:37 -04:00
*
Own every pixel each frame, and bring the cursor back as a blinking block The blinking backspace was not the backspace. The sink skipped rows it believed were already clear, tracked in a drawn[] array, which assumes a frame inherits the frame before it. SDL_RenderPresent swaps buffers, so a frame inherits the one two back: a row erased once is clean in one buffer and dirty in the other, and presenting alternates between them. The first character of a backspaced wrap flickered forever. It reproduces on X11 and never under the dummy driver or a software renderer, which is why the suite stayed green through two rounds of fixing it. The sink now repaints every row it owns every frame -- a frame either owns every pixel it presents or inherits pixels it cannot reason about. Confirmed on real hardware with the reported input: the wrapped tail's row reads zero across five successive captures. The cursor returns as a blinking block, half a second per cycle, off SDL's clock. It sits in the cell after the text rather than under it, which is what made the underscore unreadable, and it wraps to the next row when a line exactly fills one -- where the next character actually lands. The new regression test paints stale pixels by hand, which is what a swapped-in buffer hands back, so the case is covered without real hardware. Every new assertion was checked by reverting the fix and watching it fail. Recorded honestly in TODO.md section 5: the same buffer swap means the graphics verbs were never reliable across frames either. A one-shot DRAW lands in one buffer and the next present shows the other. Making that work needs a persistent surface, which is its own commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 15:23:41 -04:00
* SDL_RenderPresent swaps buffers. What a frame inherits is not the frame
* before it, it is the frame *two* before it, and on some backends it is
* undefined. So a row erased once is erased in one buffer and still dirty in
* the other, and presenting alternates between them: the row blinks in and
* out forever. That is exactly what a backspace across a line wrap looked
* like -- the first character of the wrapped tail flickering after the rest
* had gone. It reproduces on X11 and never under the dummy driver or a
* software renderer, which is why the suite was green.
Erase what the text layer drew: three GUI bugs, one cause The sink painted glyphs on top of whatever was already on the renderer and never erased anything, and the host deliberately does not clear the frame so a DRAW survives. The grid was always correct; the screen kept the previous frame. That surfaced as three separate-looking faults: - a backspaced character stayed on screen, including across a line wrap - a scroll left the old rows behind, "papered over with garbage" - the editing cursor smeared an underscore along every column it passed through, which read as an underline under the text being typed render() now fills each row it is about to draw, and each row that has emptied since it last drew, with the background first. Row by row rather than one clear over the whole area: the text layer is authoritative over the rows it occupies and leaves every other pixel alone, so a picture behind the text loses only the strips the text uses instead of all of it. echo_line() also truncates rows that hold nothing but the spaces its own erase pass wrote, which is what backspacing back across a wrap leaves behind. A row of spaces is text as far as render() is concerned, so it would have been repainted forever and kept erasing whatever was under it. The cursor glyph is removed outright, as asked. The smearing was the erase bug rather than the cursor, so one could come back and behave -- a block would read better than an underscore. Four pixel-level tests, because grid assertions could never have caught any of this. Each was checked by reverting the fix and watching it fail; two of them were vacuous when first written and are noted as such where they are fixed. The real-keyboard test also now rubs out a character and scrolls forty lines. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 15:03:37 -04:00
*
Own every pixel each frame, and bring the cursor back as a blinking block The blinking backspace was not the backspace. The sink skipped rows it believed were already clear, tracked in a drawn[] array, which assumes a frame inherits the frame before it. SDL_RenderPresent swaps buffers, so a frame inherits the one two back: a row erased once is clean in one buffer and dirty in the other, and presenting alternates between them. The first character of a backspaced wrap flickered forever. It reproduces on X11 and never under the dummy driver or a software renderer, which is why the suite stayed green through two rounds of fixing it. The sink now repaints every row it owns every frame -- a frame either owns every pixel it presents or inherits pixels it cannot reason about. Confirmed on real hardware with the reported input: the wrapped tail's row reads zero across five successive captures. The cursor returns as a blinking block, half a second per cycle, off SDL's clock. It sits in the cell after the text rather than under it, which is what made the underscore unreadable, and it wraps to the next row when a line exactly fills one -- where the next character actually lands. The new regression test paints stale pixels by hand, which is what a swapped-in buffer hands back, so the case is covered without real hardware. Every new assertion was checked by reverting the fix and watching it fail. Recorded honestly in TODO.md section 5: the same buffer swap means the graphics verbs were never reliable across frames either. A one-shot DRAW lands in one buffer and the next present shows the other. Making that work needs a persistent surface, which is its own commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 15:23:41 -04:00
* There is no cheaper correct answer available here. A frame either owns
* every pixel it presents or it inherits pixels it cannot reason about.
*
* Row by row rather than one fill over the whole area, still: the two are
* equivalent in cost here and the row loop keeps the text layer's ownership
* of exactly `rows * cellh` pixels obvious, which matters when the area is
* smaller than the window.
Erase what the text layer drew: three GUI bugs, one cause The sink painted glyphs on top of whatever was already on the renderer and never erased anything, and the host deliberately does not clear the frame so a DRAW survives. The grid was always correct; the screen kept the previous frame. That surfaced as three separate-looking faults: - a backspaced character stayed on screen, including across a line wrap - a scroll left the old rows behind, "papered over with garbage" - the editing cursor smeared an underscore along every column it passed through, which read as an underline under the text being typed render() now fills each row it is about to draw, and each row that has emptied since it last drew, with the background first. Row by row rather than one clear over the whole area: the text layer is authoritative over the rows it occupies and leaves every other pixel alone, so a picture behind the text loses only the strips the text uses instead of all of it. echo_line() also truncates rows that hold nothing but the spaces its own erase pass wrote, which is what backspacing back across a wrap leaves behind. A row of spaces is text as far as render() is concerned, so it would have been repainted forever and kept erasing whatever was under it. The cursor glyph is removed outright, as asked. The smearing was the erase bug rather than the cursor, so one could come back and behave -- a block would read better than an underscore. Four pixel-level tests, because grid assertions could never have caught any of this. Each was checked by reverting the fix and watching it fail; two of them were vacuous when first written and are noted as such where they are fixed. The real-keyboard test also now rubs out a character and scrolls forty lines. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 15:03:37 -04:00
*
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>
2026-07-31 08:46:53 -04:00
* A loop, so no CATCH and no _BREAK macros: they expand to a C break, which
* would escape the loop with an error still pending. PASS only.
*
* Wrapping is off at the draw -- a zero wraplength -- because the grid above
* already decided where every line ends. Letting SDL_ttf wrap again would
* put characters somewhere the cursor does not think they are.
*/
for ( row = 0; row < state->rows; row++ ) {
Erase what the text layer drew: three GUI bugs, one cause The sink painted glyphs on top of whatever was already on the renderer and never erased anything, and the host deliberately does not clear the frame so a DRAW survives. The grid was always correct; the screen kept the previous frame. That surfaced as three separate-looking faults: - a backspaced character stayed on screen, including across a line wrap - a scroll left the old rows behind, "papered over with garbage" - the editing cursor smeared an underscore along every column it passed through, which read as an underline under the text being typed render() now fills each row it is about to draw, and each row that has emptied since it last drew, with the background first. Row by row rather than one clear over the whole area: the text layer is authoritative over the rows it occupies and leaves every other pixel alone, so a picture behind the text loses only the strips the text uses instead of all of it. echo_line() also truncates rows that hold nothing but the spaces its own erase pass wrote, which is what backspacing back across a wrap leaves behind. A row of spaces is text as far as render() is concerned, so it would have been repainted forever and kept erasing whatever was under it. The cursor glyph is removed outright, as asked. The smearing was the erase bug rather than the cursor, so one could come back and behave -- a block would read better than an underscore. Four pixel-level tests, because grid assertions could never have caught any of this. Each was checked by reverting the fix and watching it fail; two of them were vacuous when first written and are noted as such where they are fixed. The real-keyboard test also now rubs out a character and scrolls forty lines. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 15:03:37 -04:00
SDL_FRect cells;
cells.x = (float)state->x;
cells.y = (float)(state->y + (row * state->cellh));
cells.w = (float)state->width;
cells.h = (float)state->cellh;
PASS(errctx, akgl_draw_filled_rect(state->renderer, &cells, state->background));
Own every pixel each frame, and bring the cursor back as a blinking block The blinking backspace was not the backspace. The sink skipped rows it believed were already clear, tracked in a drawn[] array, which assumes a frame inherits the frame before it. SDL_RenderPresent swaps buffers, so a frame inherits the one two back: a row erased once is clean in one buffer and dirty in the other, and presenting alternates between them. The first character of a backspaced wrap flickered forever. It reproduces on X11 and never under the dummy driver or a software renderer, which is why the suite stayed green through two rounds of fixing it. The sink now repaints every row it owns every frame -- a frame either owns every pixel it presents or inherits pixels it cannot reason about. Confirmed on real hardware with the reported input: the wrapped tail's row reads zero across five successive captures. The cursor returns as a blinking block, half a second per cycle, off SDL's clock. It sits in the cell after the text rather than under it, which is what made the underscore unreadable, and it wraps to the next row when a line exactly fills one -- where the next character actually lands. The new regression test paints stale pixels by hand, which is what a swapped-in buffer hands back, so the case is covered without real hardware. Every new assertion was checked by reverting the fix and watching it fail. Recorded honestly in TODO.md section 5: the same buffer swap means the graphics verbs were never reliable across frames either. A one-shot DRAW lands in one buffer and the next present shows the other. Making that work needs a persistent surface, which is its own commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 15:23:41 -04:00
if ( state->text[row][0] == '\0' ) {
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>
2026-07-31 08:46:53 -04:00
continue;
}
PASS(errctx, akgl_text_rendertextat(state->font, state->text[row],
state->color, 0,
state->x,
state->y + (row * state->cellh)));
}
/*
Own every pixel each frame, and bring the cursor back as a blinking block The blinking backspace was not the backspace. The sink skipped rows it believed were already clear, tracked in a drawn[] array, which assumes a frame inherits the frame before it. SDL_RenderPresent swaps buffers, so a frame inherits the one two back: a row erased once is clean in one buffer and dirty in the other, and presenting alternates between them. The first character of a backspaced wrap flickered forever. It reproduces on X11 and never under the dummy driver or a software renderer, which is why the suite stayed green through two rounds of fixing it. The sink now repaints every row it owns every frame -- a frame either owns every pixel it presents or inherits pixels it cannot reason about. Confirmed on real hardware with the reported input: the wrapped tail's row reads zero across five successive captures. The cursor returns as a blinking block, half a second per cycle, off SDL's clock. It sits in the cell after the text rather than under it, which is what made the underscore unreadable, and it wraps to the next row when a line exactly fills one -- where the next character actually lands. The new regression test paints stale pixels by hand, which is what a swapped-in buffer hands back, so the case is covered without real hardware. Every new assertion was checked by reverting the fix and watching it fail. Recorded honestly in TODO.md section 5: the same buffer swap means the graphics verbs were never reliable across frames either. A one-shot DRAW lands in one buffer and the next present shows the other. Making that work needs a persistent surface, which is its own commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 15:23:41 -04:00
* The cursor: a filled block at the cursor cell, blinking.
*
* A block rather than the reference's underscore glyph (drawCursor,
* basicruntime_graphics.go:33). The underscore sat *under* the text being
* typed and made it hard to read, which is what got it removed; a block
* occupies the cell after the text instead of the space beneath it.
*
* The clock is SDL's rather than the host's. Everywhere else in this
* interpreter the host owns the clock, because the library owns no loop and
* must not block -- but this is an adaptor that already links SDL, the
* blink is cosmetic, and threading a timestamp through the sink interface to
* animate a cursor would be a poor trade. `cursorperiodms` of zero holds it
* solid, which is what makes a frame deterministic for a test.
*/
Own every pixel each frame, and bring the cursor back as a blinking block The blinking backspace was not the backspace. The sink skipped rows it believed were already clear, tracked in a drawn[] array, which assumes a frame inherits the frame before it. SDL_RenderPresent swaps buffers, so a frame inherits the one two back: a row erased once is clean in one buffer and dirty in the other, and presenting alternates between them. The first character of a backspaced wrap flickered forever. It reproduces on X11 and never under the dummy driver or a software renderer, which is why the suite stayed green through two rounds of fixing it. The sink now repaints every row it owns every frame -- a frame either owns every pixel it presents or inherits pixels it cannot reason about. Confirmed on real hardware with the reported input: the wrapped tail's row reads zero across five successive captures. The cursor returns as a blinking block, half a second per cycle, off SDL's clock. It sits in the cell after the text rather than under it, which is what made the underscore unreadable, and it wraps to the next row when a line exactly fills one -- where the next character actually lands. The new regression test paints stale pixels by hand, which is what a swapped-in buffer hands back, so the case is covered without real hardware. Every new assertion was checked by reverting the fix and watching it fail. Recorded honestly in TODO.md section 5: the same buffer swap means the graphics verbs were never reliable across frames either. A one-shot DRAW lands in one buffer and the next present shows the other. Making that work needs a persistent surface, which is its own commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 15:23:41 -04:00
if ( state->editing ) {
SDL_FRect cell;
bool visible = true;
if ( state->cursorperiodms > 0 ) {
visible = ((SDL_GetTicks() % state->cursorperiodms) <
(state->cursorperiodms / 2));
}
if ( visible ) {
int curcol = state->cursorcol;
int currow = state->cursorrow;
/*
* A line that exactly fills a row leaves the cursor one past the last
* column, because putchar_at only wraps when the *next* character
* arrives. Drawn there it would be off the right edge and invisible.
* Show it where the next character will actually land instead, which
* is also where a C128 puts it.
*/
if ( curcol >= state->columns ) {
curcol = 0;
currow += 1;
}
if ( currow < state->rows ) {
cell.x = (float)(state->x + (curcol * state->cellw));
cell.y = (float)(state->y + (currow * state->cellh));
cell.w = (float)state->cellw;
cell.h = (float)state->cellh;
PASS(errctx, akgl_draw_filled_rect(state->renderer, &cell, state->color));
}
}
}
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_sink_akgl_set_pump(akbasic_TextSink *obj, akbasic_AkglPump pump, void *self)
{
PREPARE_ERROR(errctx);
akbasic_AkglSink *state = NULL;
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER,
"NULL sink in sink_akgl_set_pump");
state = (akbasic_AkglSink *)obj->self;
FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER,
"akgl sink has no state");
state->pump = pump;
state->pumpself = self;
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>
2026-07-31 08:46:53 -04:00
SUCCEED_RETURN(errctx);
}