Compare commits

2 Commits

Author SHA1 Message Date
3342f2b569 Make GRAPHIC select the text plane
Some checks failed
akbasic CI Build / cmake_build (push) Failing after 3m22s
akbasic CI Build / sanitizers (push) Failing after 4m43s
akbasic CI Build / coverage (push) Failing after 3m43s
akbasic CI Build / akgl_build (push) Failing after 4m48s
akbasic CI Build / mutation_test (push) Failing after 3m31s
Co-authored-by: Andrew Kesterson <andrew@aklabs.net>
2026-08-03 08:08:34 -04:00
c5d13f00f6 Repoint the TODO.md citations that no longer resolve
Some checks failed
akbasic CI Build / cmake_build (push) Failing after 4m7s
akbasic CI Build / coverage (push) Failing after 3m33s
akbasic CI Build / akgl_build (push) Failing after 4m39s
akbasic CI Build / mutation_test (push) Failing after 3m32s
akbasic CI Build / sanitizers (push) Failing after 14m32s
Its own SS0-SS9 structure survived the move to the tracker, so 133 citations
still resolve and are left alone. Thirteen did not.

Six named another repository's TODO.md by a section number: libakstdlib 1.6,
2.2.2 and 2.3, and libakgl's file. Those repositories dropped their numbering
entirely, so each now names UPGRADING.md, an issue, or the tracker.

Three cited 'TODO.md section 12', which has never existed here -- the defect
list is SS6. TODO.md itself caught two others of that class earlier. The
CMakeLists comment carried a stale premise with it: eleven defects
'deliberately reproduced and not yet fixed' stopped being the rule when SS0.1
retired the fidelity constraint.

Four told a reader to record work in TODO.md; the scanner defect is issue #4,
the audio_tables mutation gap is #25, and the UI gaps are libakgl #79 and #80.

Verified: cmake --build build && ctest --test-dir build, 112/112.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-08-02 22:01:28 -04:00
24 changed files with 364 additions and 98 deletions

View File

@@ -282,7 +282,7 @@ jobs:
# avoid the file: almost every survivor is in akbasic_audio_state_init, # avoid the file: almost every survivor is in akbasic_audio_state_init,
# where nothing asserts that a freshly initialised audio state is actually # where nothing asserts that a freshly initialised audio state is actually
# zeroed and defaulted -- the same gap this job's own history records # zeroed and defaulted -- the same gap this job's own history records
# closing for src/symtab.c. Recorded in TODO.md; add the file back when it # closing for src/symtab.c. That is issue #25; add the file back when it
# is closed. # is closed.
# #
# It was 73.1% before writing this job. The run's own findings closed the # It was 73.1% before writing this job. The run's own findings closed the
@@ -297,7 +297,7 @@ jobs:
# #
# The 21 remaining survivors are listed in the published report. Most are # The 21 remaining survivors are listed in the published report. Most are
# ICR mutants on loop and accumulator initialisers that a stronger # ICR mutants on loop and accumulator initialisers that a stronger
# placement assertion would catch. Recorded in TODO.md. # placement assertion would catch. Issue #25.
- name: mutation testing - name: mutation testing
run: | run: |
python3 scripts/mutation_test.py \ python3 scripts/mutation_test.py \

View File

@@ -3,9 +3,9 @@ cmake_minimum_required(VERSION 3.10)
# libakgl both use. It flows into the library SOVERSION and nothing else spells # libakgl both use. It flows into the library SOVERSION and nothing else spells
# a version number. # a version number.
# #
# 0.x on purpose: TODO.md section 12 records eleven defects carried over from the # 0.x on purpose: the language surface is not being promised yet. TODO.md §6
# Go reference that are deliberately reproduced and not yet fixed, so the # records the defects inherited from the Go reference and what became of each;
# language surface is not being promised yet. # the ones still open are in the issue tracker.
project(akbasic VERSION 0.1.0 LANGUAGES C) project(akbasic VERSION 0.1.0 LANGUAGES C)
# Pre-1.0 the ABI may break on a minor bump, so the soname carries MAJOR.MINOR. # Pre-1.0 the ABI may break on a minor bump, so the soname carries MAJOR.MINOR.
@@ -56,7 +56,7 @@ option(AKBASIC_SANITIZE "Build with ASan + UBSan" OFF
# coverage build collides on the `coverage` target and fails to configure at all. # coverage build collides on the `coverage` target and fails to configure at all.
# Rename the dependency's on the way past. Remove this once libakerror applies # Rename the dependency's on the way past. Remove this once libakerror applies
# the same CMAKE_SOURCE_DIR test to `coverage` that it already applies to # the same CMAKE_SOURCE_DIR test to `coverage` that it already applies to
# `mutation` -- filed in deps/libakstdlib/TODO.md section 2.3. # `mutation` -- filed as libakerror issue #15.
# #
# **Only one project in a tree may shadow add_test(), and this is that project.** # **Only one project in a tree may shadow add_test(), and this is that project.**
# CMake exposes an overridden command as `_name` and chains exactly one level: a # CMake exposes an overridden command as `_name` and chains exactly one level: a
@@ -276,7 +276,7 @@ endif()
# AKBASIC_TESTS must exit 0 # AKBASIC_TESTS must exit 0
# AKBASIC_WILL_FAIL_TESTS abort by design # AKBASIC_WILL_FAIL_TESTS abort by design
# AKBASIC_KNOWN_FAILING_TESTS assert the *correct* contract for a defect # AKBASIC_KNOWN_FAILING_TESTS assert the *correct* contract for a defect
# recorded in TODO.md and are expected to fail. # tracked as issues and are expected to fail.
# When one starts passing CTest reports # When one starts passing CTest reports
# "unexpectedly passed" -- that is the cue to move # "unexpectedly passed" -- that is the cue to move
# it into AKBASIC_TESTS along with the fix. # it into AKBASIC_TESTS along with the fix.

View File

@@ -2331,7 +2331,7 @@ gitignored is exactly what makes it invisible. **Keep build trees out of the sou
and if a coverage number looks suspiciously unchanged, `find . -name '*.gcda'` before believing and if a coverage number looks suspiciously unchanged, `find . -name '*.gcda'` before believing
it. it.
Branch coverage reads 18.0% and is not a target, for the reason `libakgl/TODO.md` and Branch coverage reads 18.0% and is not a target, for the reason `libakgl` and
`libakstdlib` both give: the akerror control-flow macros expand into large branch trees `libakstdlib` both give: the akerror control-flow macros expand into large branch trees
at every call site, most of them unreachable in normal operation. Track line and function at every call site, most of them unreachable in normal operation. Track line and function
coverage. coverage.

View File

@@ -62,22 +62,28 @@ small number that is not a colour.
### GRAPHIC ### GRAPHIC
`GRAPHIC mode` chooses a screen mode; `GRAPHIC CLR` clears it. Mode 0 is text and `GRAPHIC mode [, clear] [, split]` chooses the display composition; `GRAPHIC CLR`
refuses to draw. clears the saved graphics state. `GRAPHIC 0` displays the text plane. `GRAPHIC 1`
selects a full bitmap plane: `PRINT` still updates the retained text screen, but it is
not visible until text mode returns. `GRAPHIC 2` is the explicit split-screen form;
its optional third argument names the first text row, and its default leaves six text
rows at the bottom. `WINDOW` restricts the current text region; it does not make a
full bitmap visible.
Every picture in this chapter is generated by running the listing above it; see Every picture in this chapter is generated by running the listing above it; see
`MAINTENANCE.md` if you are editing one. `MAINTENANCE.md` if you are editing one.
### DRAW ### DRAW
```basic requires=akgl screenshot=draw ```basic requires=akgl screenshot=draw text=1
10 COLOR 1, 8 10 GRAPHIC 1, 1
20 DRAW 1, 20, 180 TO 90, 40 TO 160, 150 TO 230, 20 TO 300, 120 20 COLOR 1, 8
30 COLOR 2, 6 30 DRAW 1, 20, 180 TO 90, 40 TO 160, 150 TO 230, 20 TO 300, 120
40 DRAW 2, 20, 190 TO 300, 190 40 COLOR 2, 6
50 LOCATE 160, 100 50 DRAW 2, 20, 190 TO 300, 190
60 COLOR 3, 3 60 LOCATE 160, 100
70 DRAW 3 70 COLOR 3, 3
80 DRAW 3
``` ```
![](images/draw.png) ![](images/draw.png)
@@ -88,14 +94,15 @@ picture — plots wherever `LOCATE` left the pixel cursor.
### BOX ### BOX
```basic requires=akgl screenshot=box ```basic requires=akgl screenshot=box text=1
10 COLOR 1, 8 10 GRAPHIC 1, 1
20 BOX 1, 20, 30, 130, 140 20 COLOR 1, 8
30 COLOR 2, 6 30 BOX 1, 20, 30, 130, 140
40 BOX 2, 180, 30, 290, 140, 30 40 COLOR 2, 6
50 COLOR 3, 3 50 BOX 2, 180, 30, 290, 140, 30
60 LOCATE 300, 190 60 COLOR 3, 3
70 BOX 3, 20, 160 70 LOCATE 300, 190
80 BOX 3, 20, 160
``` ```
![](images/box.png) ![](images/box.png)
@@ -110,13 +117,14 @@ have.
### CIRCLE ### CIRCLE
```basic requires=akgl screenshot=circle ```basic requires=akgl screenshot=circle text=1
10 COLOR 1, 8 10 GRAPHIC 1, 1
20 CIRCLE 1, 80, 70, 60, 60 20 COLOR 1, 8
30 COLOR 2, 6 30 CIRCLE 1, 80, 70, 60, 60
40 CIRCLE 2, 230, 70, 75, 45 40 COLOR 2, 6
50 COLOR 3, 3 50 CIRCLE 2, 230, 70, 75, 45
60 CIRCLE 3, 160, 140, 130, 50, 90, 270 60 COLOR 3, 3
70 CIRCLE 3, 160, 140, 130, 50, 90, 270
``` ```
![](images/circle.png) ![](images/circle.png)
@@ -129,14 +137,15 @@ degree increment, and a large increment is what turns a circle into a polygon.
### PAINT ### PAINT
```basic requires=akgl screenshot=paint ```basic requires=akgl screenshot=paint text=1
10 COLOR 1, 8 10 GRAPHIC 1, 1
20 CIRCLE 1, 100, 100, 70, 70 20 COLOR 1, 8
30 BOX 1, 180, 50, 290, 150 30 CIRCLE 1, 100, 100, 70, 70
40 COLOR 2, 6 40 BOX 1, 180, 50, 290, 150
50 PAINT 2, 100, 100 50 COLOR 2, 6
60 COLOR 3, 3 60 PAINT 2, 100, 100
70 PAINT 3, 230, 100 70 COLOR 3, 3
80 PAINT 3, 230, 100
``` ```
![](images/paint.png) ![](images/paint.png)
@@ -153,13 +162,14 @@ coordinates finishes.
### SCALE ### SCALE
```basic requires=akgl screenshot=scale size=640x400 ```basic requires=akgl screenshot=scale size=640x400 text=1
10 COLOR 1, 3 10 GRAPHIC 1, 1
20 BOX 1, 0, 0, 319, 199 20 COLOR 1, 3
30 SCALE 1, 319, 199 30 BOX 1, 0, 0, 319, 199
40 COLOR 2, 6 40 SCALE 1, 319, 199
50 BOX 2, 0, 0, 319, 199 50 COLOR 2, 6
60 DRAW 2, 0, 0 TO 319, 199 60 BOX 2, 0, 0, 319, 199
70 DRAW 2, 0, 0 TO 319, 199
``` ```
![](images/scale.png) ![](images/scale.png)
@@ -212,15 +222,16 @@ parallel passes; see Chapter 13.
`SSHAPE` copies a rectangle off the screen and `GSHAPE` stamps it back: `SSHAPE` copies a rectangle off the screen and `GSHAPE` stamps it back:
```basic requires=akgl screenshot=shapes ```basic requires=akgl screenshot=shapes text=1
10 COLOR 1, 8 10 GRAPHIC 1, 1
20 CIRCLE 1, 40, 40, 30, 30 20 COLOR 1, 8
30 COLOR 2, 6 30 CIRCLE 1, 40, 40, 30, 30
40 PAINT 2, 40, 40 40 COLOR 2, 6
50 SSHAPE A$, 8, 8, 72, 72 50 PAINT 2, 40, 40
60 GSHAPE A$, 120, 20 60 SSHAPE A$, 8, 8, 72, 72
70 GSHAPE A$, 200, 60 70 GSHAPE A$, 120, 20
80 GSHAPE A$, 120, 120 80 GSHAPE A$, 200, 60
90 GSHAPE A$, 120, 120
``` ```
![](images/shapes.png) ![](images/shapes.png)
@@ -244,19 +255,11 @@ does with one, but you cannot store it or measure it.
text and the sprites, so a program draws its picture once and it is there on every frame text and the sprites, so a program draws its picture once and it is there on every frame
after. It does not have to redraw it, and it does not have to capture it into a sprite. after. It does not have to redraw it, and it does not have to capture it into a sprite.
What covers it is the text layer, which repaints every row it owns — opaque, every frame, `GRAPHIC 1` changes that order: the full bitmap owns the display, while the text layer
for a reason `akbasic_sink_akgl_render()` explains — and by default it owns the whole keeps its contents off-screen. Returning with `GRAPHIC 0` reveals what `PRINT` wrote.
window. `WINDOW` shrinks it: Use `GRAPHIC 2, clear, split` when a program deliberately needs both planes; its text
rows begin at `split`. `WINDOW` can then narrow that text region further, but it is not
```basic norun needed to make a full bitmap visible.
10 WINDOW 0, 0, 39, 1
20 GRAPHIC 1, 1
30 COLOR 1, 3
40 BOX 1, 20, 40, 300, 180
```
Two rows of text at the top, the rest of the window for drawing, and the box is still
there a thousand frames later.
**A redraw also has to fit inside one batch.** The host runs a fixed number of source **A redraw also has to fit inside one batch.** The host runs a fixed number of source
lines and then presents, and presenting throws the drawing buffer away — so a run of lines and then presents, and presenting throws the drawing buffer away — so a run of

View File

@@ -116,6 +116,7 @@ typedef struct akbasic_TextSink
akerr_ErrorContext AKERR_NOIGNORE *(*moveto)(struct akbasic_TextSink *self, int col, int row); akerr_ErrorContext AKERR_NOIGNORE *(*moveto)(struct akbasic_TextSink *self, int col, int row);
akerr_ErrorContext AKERR_NOIGNORE *(*window)(struct akbasic_TextSink *self, int left, int top, int right, int bottom); akerr_ErrorContext AKERR_NOIGNORE *(*window)(struct akbasic_TextSink *self, int left, int top, int right, int bottom);
akerr_ErrorContext AKERR_NOIGNORE *(*grid)(struct akbasic_TextSink *self, int *columns, int *rows, int *cellw, int *cellh); akerr_ErrorContext AKERR_NOIGNORE *(*grid)(struct akbasic_TextSink *self, int *columns, int *rows, int *cellw, int *cellh);
akerr_ErrorContext AKERR_NOIGNORE *(*graphic)(struct akbasic_TextSink *self, int mode, int split);
} akbasic_TextSink; } akbasic_TextSink;
``` ```
@@ -123,8 +124,8 @@ typedef struct akbasic_TextSink
supplies its own and draws into a text layer. `readline` is expected to set `*eof` rather supplies its own and draws into a text layer. `readline` is expected to set `*eof` rather
than block — that is how `INPUT` behaves sanely inside a frame. than block — that is how `INPUT` behaves sanely inside a frame.
**The last three are optional and may be NULL**, which is how `CHAR`, `WINDOW` and **The last four are optional and may be NULL**, which is how `CHAR`, `WINDOW`, `RWINDOW`
`RWINDOW` know to refuse by name rather than pretending. Supply `grid` if your text layer and `GRAPHIC` know to refuse by name rather than pretending. Supply `grid` if your text layer
has a character cell: it is the only way a script can find out how big one is, and has a character cell: it is the only way a script can find out how big one is, and
without it anything placing a character and a sprite at the same spot has to hardcode a without it anything placing a character and a sprite at the same spot has to hardcode a
number measured against your font. number measured against your font.

View File

@@ -458,7 +458,7 @@ Three consequences follow, and all three are things people report as bugs:
`akbasic_Value` carries its string **inline**, not behind a pointer, so a copy is a struct `akbasic_Value` carries its string **inline**, not behind a pointer, so a copy is a struct
assignment with no allocator, no refcount and no lifetime question. It costs 256 bytes per assignment with no allocator, no refcount and no lifetime question. It costs 256 bytes per
value; that is the trade, and it is recorded in `TODO.md`. value; that is the trade, and it is recorded in `TODO.md` §6.
Type comes from the identifier's suffix and nothing else: `A#` integer, `A%` float, `A$` Type comes from the identifier's suffix and nothing else: `A#` integer, `A%` float, `A$`
string, and a bare name with no suffix is a *label*. That is `akbasic_leaf_identifier_type()`, string, and a bare name with no suffix is a *label*. That is `akbasic_leaf_identifier_type()`,
@@ -596,7 +596,7 @@ script's mistake into an error line are wrapped around *parsing* and *interpreta
not around *scanning*. A line with more than 32 tokens therefore escapes as an not around *scanning*. A line with more than 32 tokens therefore escapes as an
interpreter error — the driver prints a stack trace and exits 1, and an embedding host is interpreter error — the driver prints a stack trace and exits 1, and an embedding host is
handed a context for what is really a script's mistake. It is the same shape as the handed a context for what is really a script's mistake. It is the same shape as the
`VERIFY` defect above, on a path that fix did not cover. Filed in `TODO.md`; until it is `VERIFY` defect above, on a path that fix did not cover. That is issue #4; until it is
closed, a host that cannot tolerate that should refuse over-long lines itself. closed, a host that cannot tolerate that should refuse over-long lines itself.
## Devices, and how a capability is withheld ## Devices, and how a capability is withheld
@@ -808,7 +808,7 @@ by name when the pointer is `NULL`**, so an older host or a different backend ge
error rather than silence. error rather than silence.
If `libakgl` cannot supply what the verb needs, **do not work around it here**. File it in If `libakgl` cannot supply what the verb needs, **do not work around it here**. File it in
`deps/libakgl/TODO.md`: what the BASIC verb requires, what the `akgl_*` entry point should `libakgl`'s issue tracker: what the BASIC verb requires, what the `akgl_*` entry point should
look like, and what tests would cover it. Four gaps have gone upstream that way and all look like, and what tests would cover it. Four gaps have gone upstream that way and all
four landed. four landed.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 739 B

After

Width:  |  Height:  |  Size: 735 B

View File

@@ -189,6 +189,10 @@ typedef struct
int columns; /* the character grid the cell size works out to */ int columns; /* the character grid the cell size works out to */
int rows; int rows;
/* GRAPHIC chooses whether this text plane is visible and where it begins. */
int graphicmode;
int texttop; /* first screen row the current text region owns */
int cursorcol; int cursorcol;
int cursorrow; int cursorrow;

View File

@@ -82,6 +82,20 @@ typedef struct akbasic_TextSink
* @return `NULL` on success, otherwise an error context owned by the caller. * @return `NULL` on success, otherwise an error context owned by the caller.
*/ */
akerr_ErrorContext AKERR_NOIGNORE *(*grid)(struct akbasic_TextSink *self, int *columns, int *rows, int *cellw, int *cellh); akerr_ErrorContext AKERR_NOIGNORE *(*grid)(struct akbasic_TextSink *self, int *columns, int *rows, int *cellw, int *cellh);
/**
* Select how this sink's text plane participates in a GRAPHIC display mode.
*
* This is optional: a stream has no display plane to hide or split, so the
* stdio sink leaves it NULL. A graphical sink receives the mode after the
* runtime has validated it. @p split is the first text row for a split mode,
* or -1 when BASIC used the C128 default.
*
* @param self The sink.
* @param mode BASIC 7.0 GRAPHIC mode.
* @param split First text row for a split screen, or -1 for the default.
* @return `NULL` on success, otherwise an error context owned by the caller.
*/
akerr_ErrorContext AKERR_NOIGNORE *(*graphic)(struct akbasic_TextSink *self, int mode, int split);
} akbasic_TextSink; } akbasic_TextSink;
/** @brief State for the stdio-backed sink. */ /** @brief State for the stdio-backed sink. */

View File

@@ -57,7 +57,7 @@
* These are akgl_UiAnchor's five values and its numbering, so the adaptor maps * These are akgl_UiAnchor's five values and its numbering, so the adaptor maps
* them across without a table. **There is deliberately no top-centre or * them across without a table. **There is deliberately no top-centre or
* bottom-centre**, because libakgl has neither and this repository does not edit * bottom-centre**, because libakgl has neither and this repository does not edit
* its dependencies to add one; see TODO.md. * its dependencies to add one; see libakgl issues #79 and #80.
*/ */
typedef enum typedef enum
{ {

View File

@@ -3,8 +3,8 @@
* @brief Declares the strongly-typed BASIC value and its operators. * @brief Declares the strongly-typed BASIC value and its operators.
* *
* Ported from the reference's basicvalue.go. The arithmetic is reproduced * Ported from the reference's basicvalue.go. The arithmetic is reproduced
* exactly, including the parts that look wrong -- see TODO.md section 12 for the * exactly, except where a defect has since been fixed on its own merits -- see
* catalogue and the reason they are not fixed yet. * TODO.md §6 for the catalogue and what became of each.
* *
* **The left operand decides whether an operation is done in integers or in * **The left operand decides whether an operation is done in integers or in
* floats**, and this is deliberate rather than an oversight: every operator * floats**, and this is deliberate rather than an oversight: every operator

View File

@@ -172,7 +172,7 @@ akerr_ErrorContext *akbasic_environment_stop_waiting(akbasic_Environment *obj, c
"NULL argument in stop_waiting"); "NULL argument in stop_waiting");
/* /*
* The reference ignores `command` and clears unconditionally, which lets an * The reference ignores `command` and clears unconditionally, which lets an
* inner block clear an outer block's wait (TODO.md section 12 item 3). The * inner block clear an outer block's wait (TODO.md §6 item 3). The
* argument is honoured here only to the extent of walking to the environment * argument is honoured here only to the extent of walking to the environment
* that is actually waiting for it -- clearing the wrong one outright would * that is actually waiting for it -- clearing the wrong one outright would
* change observable control flow, so the search stops at the first match and * change observable control flow, so the search stops at the first match and

View File

@@ -1056,7 +1056,7 @@ akerr_ErrorContext *akbasic_runtime_call_function(akbasic_Runtime *obj, const ch
* rule.** A multi-line function called at the REPL falls straight past this * rule.** A multi-line function called at the REPL falls straight past this
* loop and returns whatever is in the caller's return slot -- zero -- so * loop and returns whatever is in the caller's return slot -- zero -- so
* `PRINT TRIPLE(14)` answers "(UNDEFINED STRING REPRESENTATION FOR 0)" with * `PRINT TRIPLE(14)` answers "(UNDEFINED STRING REPRESENTATION FOR 0)" with
* no error and no diagnostic. Recorded as TODO.md section 6 item 41. * no error and no diagnostic. Issue #8.
* *
* Widening it to `mode != AKBASIC_MODE_QUIT` is the obvious fix and is * Widening it to `mode != AKBASIC_MODE_QUIT` is the obvious fix and is
* *wrong*: akbasic_runtime_process_line_run() does not advance a REPL-mode * *wrong*: akbasic_runtime_process_line_run() does not advance a REPL-mode

View File

@@ -617,7 +617,7 @@ akerr_ErrorContext *akbasic_cmd_dload(akbasic_Runtime *obj, akbasic_ASTLeaf *exp
/* /*
* aksl_fopen does not NULL-check pathname or mode and fopen(NULL, ...) is * aksl_fopen does not NULL-check pathname or mode and fopen(NULL, ...) is
* undefined, so the name is validated here before it is handed over -- * undefined, so the name is validated here before it is handed over --
* deps/libakstdlib/TODO.md 2.2.2. * libakstdlib's UPGRADING.md.
*/ */
PASS(errctx, filename_argument(obj, expr, filename, sizeof(filename))); PASS(errctx, filename_argument(obj, expr, filename, sizeof(filename)));

View File

@@ -389,7 +389,7 @@ akerr_ErrorContext *akbasic_cmd_directory(akbasic_Runtime *obj, akbasic_ASTLeaf
* Refused rather than half-built. Listing a directory needs opendir/readdir, * Refused rather than half-built. Listing a directory needs opendir/readdir,
* which `libakstdlib` does not wrap -- and this project's rule is that a * which `libakstdlib` does not wrap -- and this project's rule is that a
* missing capability gets filed upstream rather than worked around here * missing capability gets filed upstream rather than worked around here
* (MAINTENANCE.md). Filed in deps/libakstdlib/TODO.md. * (MAINTENANCE.md). Filed as libakstdlib issue #10.
* *
* The alternative was shelling out to `ls`, which a library has no business * The alternative was shelling out to `ls`, which a library has no business
* doing, or calling readdir directly and stepping outside the error * doing, or calling readdir directly and stepping outside the error

View File

@@ -155,24 +155,24 @@ static akerr_ErrorContext *draw_line(akbasic_Runtime *obj, double x1, double y1,
akerr_ErrorContext *akbasic_cmd_graphic(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest) akerr_ErrorContext *akbasic_cmd_graphic(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
double args[2]; double args[3];
int count = 0; int count = 0;
int mode = 0; int mode = 0;
int split = -1;
akbasic_Color background; akbasic_Color background;
(void)lval; (void)rval; (void)lval; (void)rval;
PASS(errctx, require_graphics(obj, "GRAPHIC")); PASS(errctx, require_graphics(obj, "GRAPHIC"));
PASS(errctx, akbasic_args_numbers(obj, expr, "GRAPHIC", args, 2, &count)); PASS(errctx, akbasic_args_numbers(obj, expr, "GRAPHIC", args, 3, &count));
FAIL_ZERO_RETURN(errctx, (count >= 1), AKBASIC_ERR_SYNTAX, "GRAPHIC expected a mode"); FAIL_ZERO_RETURN(errctx, (count >= 1), AKBASIC_ERR_SYNTAX, "GRAPHIC expected a mode");
mode = (int)args[0]; mode = (int)args[0];
/* /*
* BASIC 7.0 numbers five modes plus a CLR. They differ in bitmap resolution * BASIC 7.0 numbers five modes plus a CLR. They differ in bitmap resolution
* and in whether the bottom of the screen stays text, neither of which means * and in whether the bottom of the screen stays text. Resolution belongs to
* anything against a host's renderer -- so the mode is recorded and only its * the graphics backend, while the latter is a text-sink decision: an AKGL
* one observable consequence is honoured: mode 0 is text, and text mode does * sink hides its text plane for a full bitmap and moves it below the split.
* not draw. Refusing an out-of-range mode still matters, because that is a * A stream has no plane, so it simply leaves that optional entry point NULL.
* typo the program author can fix.
*/ */
FAIL_ZERO_RETURN(errctx, (mode >= 0 && mode <= 5), AKBASIC_ERR_BOUNDS, FAIL_ZERO_RETURN(errctx, (mode >= 0 && mode <= 5), AKBASIC_ERR_BOUNDS,
"GRAPHIC mode %d out of range (0 to 5)", mode); "GRAPHIC mode %d out of range (0 to 5)", mode);
@@ -180,11 +180,22 @@ akerr_ErrorContext *akbasic_cmd_graphic(akbasic_Runtime *obj, akbasic_ASTLeaf *e
/* GRAPHIC CLR: drop the saved shapes and go back to text. */ /* GRAPHIC CLR: drop the saved shapes and go back to text. */
PASS(errctx, obj->graphics->free_shapes(obj->graphics)); PASS(errctx, obj->graphics->free_shapes(obj->graphics));
PASS(errctx, akbasic_graphics_state_init(&obj->gfx)); PASS(errctx, akbasic_graphics_state_init(&obj->gfx));
if ( obj->sink != NULL && obj->sink->graphic != NULL ) {
PASS(errctx, obj->sink->graphic(obj->sink, 0, -1));
}
SUCCEED_TRUE(obj, dest); SUCCEED_TRUE(obj, dest);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
if ( count >= 3 ) {
split = (int)args[2];
FAIL_ZERO_RETURN(errctx, (split >= 0 && split <= 25), AKBASIC_ERR_BOUNDS,
"GRAPHIC split %d out of range (0 to 25)", split);
}
obj->gfx.mode = mode; obj->gfx.mode = mode;
if ( obj->sink != NULL && obj->sink->graphic != NULL ) {
PASS(errctx, obj->sink->graphic(obj->sink, mode, split));
}
if ( count >= 2 && args[1] != 0.0 ) { if ( count >= 2 && args[1] != 0.0 ) {
PASS(errctx, akbasic_graphics_source_color(&obj->gfx, 0, &background)); PASS(errctx, akbasic_graphics_source_color(&obj->gfx, 0, &background));
PASS(errctx, obj->graphics->clear(obj->graphics, background)); PASS(errctx, obj->graphics->clear(obj->graphics, background));

View File

@@ -475,7 +475,7 @@ static akerr_ErrorContext *sink_window(akbasic_TextSink *self, int left, int top
"The sink has no character grid to window"); "The sink has no character grid to window");
maxcols = state->fullwidth / state->cellw; maxcols = state->fullwidth / state->cellw;
maxrows = state->fullheight / state->cellh; maxrows = (state->fullheight / state->cellh) - state->texttop;
if ( left < 0 ) { left = 0; } if ( left < 0 ) { left = 0; }
if ( top < 0 ) { top = 0; } if ( top < 0 ) { top = 0; }
if ( right >= maxcols ) { right = maxcols - 1; } if ( right >= maxcols ) { right = maxcols - 1; }
@@ -484,7 +484,7 @@ static akerr_ErrorContext *sink_window(akbasic_TextSink *self, int left, int top
"WINDOW asks for no cells at all"); "WINDOW asks for no cells at all");
state->x = state->fullx + (left * state->cellw); state->x = state->fullx + (left * state->cellw);
state->y = state->fully + (top * state->cellh); state->y = state->fully + ((state->texttop + top) * state->cellh);
state->columns = (right - left) + 1; state->columns = (right - left) + 1;
state->rows = (bottom - top) + 1; state->rows = (bottom - top) + 1;
state->width = state->columns * state->cellw; state->width = state->columns * state->cellw;
@@ -494,6 +494,78 @@ static akerr_ErrorContext *sink_window(akbasic_TextSink *self, int left, int top
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
/**
* @brief Apply GRAPHIC's text-plane half without teaching the runtime about SDL.
*
* Modes 1 and 3 are full bitmaps, so the retained text grid is hidden. Modes 2
* and 4 have a bitmap above and text below. A C128 gives an omitted split its
* bottom six rows; an explicit zero means all text, and the host clamps a C128
* row number to however many measured cells its own window has.
*/
static akerr_ErrorContext *sink_graphic(akbasic_TextSink *self, int mode, int split)
{
PREPARE_ERROR(errctx);
akbasic_AkglSink *state = NULL;
int maxcols = 0;
int maxrows = 0;
FAIL_ZERO_RETURN(errctx, (self != NULL), AKERR_NULLPOINTER, "NULL sink in graphic");
state = (akbasic_AkglSink *)self->self;
FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER, "akgl sink has no state");
maxcols = state->fullwidth / state->cellw;
maxrows = state->fullheight / state->cellh;
state->graphicmode = mode;
if ( mode == 0 ) {
/* Text mode owns the whole screen again, not the last split's rows. */
state->texttop = 0;
state->x = state->fullx;
state->y = state->fully;
state->width = state->fullwidth;
state->height = state->fullheight;
state->columns = maxcols;
state->rows = maxrows;
if ( state->columns > SINK_MAX_COLUMNS - 1 ) {
state->columns = SINK_MAX_COLUMNS - 1;
}
if ( state->rows > SINK_MAX_ROWS ) {
state->rows = SINK_MAX_ROWS;
}
state->cursorcol = 0;
state->cursorrow = 0;
SUCCEED_RETURN(errctx);
}
if ( mode != 2 && mode != 4 ) {
SUCCEED_RETURN(errctx);
}
if ( split < 0 ) {
split = maxrows - 6;
}
if ( split < 0 ) { split = 0; }
if ( split >= maxrows ) {
/* A split below the final row is C128 all-bitmap mode, not a zero-row grid. */
state->graphicmode = 1;
SUCCEED_RETURN(errctx);
}
state->texttop = split;
state->x = state->fullx;
state->y = state->fully + (split * state->cellh);
state->width = state->fullwidth;
state->height = (maxrows - split) * state->cellh;
state->columns = maxcols;
state->rows = maxrows - split;
if ( state->columns > SINK_MAX_COLUMNS - 1 ) {
state->columns = SINK_MAX_COLUMNS - 1;
}
if ( state->rows > SINK_MAX_ROWS ) {
state->rows = SINK_MAX_ROWS;
}
state->cursorcol = 0;
state->cursorrow = 0;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_sink_init_akgl(akbasic_TextSink *obj, akbasic_AkglSink *state, akgl_RenderBackend *renderer, TTF_Font *font, int w, int h) 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); PREPARE_ERROR(errctx);
@@ -554,6 +626,8 @@ akerr_ErrorContext *akbasic_sink_init_akgl(akbasic_TextSink *obj, akbasic_AkglSi
state->cellh = cellh; state->cellh = cellh;
state->columns = w / cellw; state->columns = w / cellw;
state->rows = h / cellh; state->rows = h / cellh;
state->graphicmode = 0;
state->texttop = 0;
if ( state->columns > SINK_MAX_COLUMNS - 1 ) { if ( state->columns > SINK_MAX_COLUMNS - 1 ) {
state->columns = SINK_MAX_COLUMNS - 1; state->columns = SINK_MAX_COLUMNS - 1;
} }
@@ -569,6 +643,7 @@ akerr_ErrorContext *akbasic_sink_init_akgl(akbasic_TextSink *obj, akbasic_AkglSi
obj->moveto = sink_moveto; obj->moveto = sink_moveto;
obj->window = sink_window; obj->window = sink_window;
obj->grid = sink_grid; obj->grid = sink_grid;
obj->graphic = sink_graphic;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -584,6 +659,11 @@ akerr_ErrorContext *akbasic_sink_akgl_render(akbasic_TextSink *obj)
FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER, FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER,
"akgl sink has no state"); "akgl sink has no state");
/* A full bitmap owns every display row; the text buffer remains intact. */
if ( state->graphicmode == 1 || state->graphicmode == 3 ) {
SUCCEED_RETURN(errctx);
}
/* /*
* **Repaint every row of the text area, every frame.** Not just the rows * **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 * that changed -- that was tried, with a `drawn[]` array marking which rows

View File

@@ -109,5 +109,6 @@ akerr_ErrorContext *akbasic_sink_init_stdio(akbasic_TextSink *obj, akbasic_Stdio
obj->moveto = NULL; obj->moveto = NULL;
obj->window = NULL; obj->window = NULL;
obj->grid = NULL; obj->grid = NULL;
obj->graphic = NULL;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }

View File

@@ -143,6 +143,22 @@ static akerr_ErrorContext *tee_window(akbasic_TextSink *self, int left, int top,
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
/** @brief Forward GRAPHIC's display-mode decision to the half with a display. */
static akerr_ErrorContext *tee_graphic(akbasic_TextSink *self, int mode, int split)
{
PREPARE_ERROR(errctx);
akbasic_TeeSink *state = NULL;
PASS(errctx, tee_state(self, &state));
if ( state->primary != NULL && state->primary->graphic != NULL ) {
PASS(errctx, state->primary->graphic(state->primary, mode, split));
}
if ( state->mirror != NULL && state->mirror->graphic != NULL ) {
PASS(errctx, state->mirror->graphic(state->mirror, mode, split));
}
SUCCEED_RETURN(errctx);
}
/** /**
* @brief Report the grid of whichever half has one. * @brief Report the grid of whichever half has one.
* *
@@ -202,6 +218,7 @@ akerr_ErrorContext *akbasic_sink_init_tee(akbasic_TextSink *obj, akbasic_TeeSink
obj->moveto = NULL; obj->moveto = NULL;
obj->window = NULL; obj->window = NULL;
obj->grid = NULL; obj->grid = NULL;
obj->graphic = NULL;
/* /*
* Offered only when a half can actually do it, so CHAR's refusal against a * Offered only when a half can actually do it, so CHAR's refusal against a
* stdio-only driver still reads correctly through a tee. * stdio-only driver still reads correctly through a tee.
@@ -221,5 +238,9 @@ akerr_ErrorContext *akbasic_sink_init_tee(akbasic_TextSink *obj, akbasic_TeeSink
(mirror != NULL && mirror->grid != NULL) ) { (mirror != NULL && mirror->grid != NULL) ) {
obj->grid = tee_grid; obj->grid = tee_grid;
} }
if ( (primary != NULL && primary->graphic != NULL) ||
(mirror != NULL && mirror->graphic != NULL) ) {
obj->graphic = tee_graphic;
}
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }

View File

@@ -15,7 +15,7 @@
* Probe for `key`. On success *slot points at either the slot holding the key or * Probe for `key`. On success *slot points at either the slot holding the key or
* the first free slot it could occupy; *found says which. Uses * the first free slot it could occupy; *found says which. Uses
* aksl_strhash_djb2 rather than a private hash. That wrapper sign-extends char, * aksl_strhash_djb2 rather than a private hash. That wrapper sign-extends char,
* so a high-bit byte hashes wrong (deps/libakstdlib/TODO.md 1.6) -- harmless * so a high-bit byte hashes wrong (libakstdlib's UPGRADING.md) -- harmless
* here because BASIC identifiers are 7-bit ASCII, and it would only ever cost * here because BASIC identifiers are 7-bit ASCII, and it would only ever cost
* probe efficiency, never correctness, since the key comparison is a strcmp. * probe efficiency, never correctness, since the key comparison is a strcmp.
*/ */

View File

@@ -1173,6 +1173,78 @@ static akerr_ErrorContext AKERR_NOIGNORE *test_drawing_layer_persists(void)
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
/**
* @brief GRAPHIC selects whether the AKGL text plane covers the drawing layer.
*
* `PRINT` always writes into the retained grid. In a full bitmap mode the grid
* is simply not composited; returning to mode zero reveals it. Split mode moves
* the text region below the requested row, leaving the drawing visible above.
*/
static akerr_ErrorContext AKERR_NOIGNORE *test_graphic_text_composition(void)
{
PREPARE_ERROR(errctx);
SDL_Surface *shot = NULL;
int fullrows = 0;
PASS(errctx, akbasic_sink_init_akgl(&AKGLSINK, &AKGLSINKSTATE, akgl_renderer, font,
TARGET_SIZE, TARGET_SIZE));
PASS(errctx, akbasic_runtime_init(&RUNTIME, &AKGLSINK));
PASS(errctx, akbasic_graphics_init_akgl(&GRAPHICS, &GRAPHICSSTATE, akgl_renderer));
PASS(errctx, akbasic_runtime_set_devices(&RUNTIME, &GRAPHICS, NULL, NULL, NULL));
PASS(errctx, akbasic_runtime_load(&RUNTIME,
"10 GRAPHIC 1, 1\n"
"20 COLOR 1, 3\n"
"30 DRAW 1, 40, 40\n"
"40 PRINT \"HIDDEN\"\n"));
PASS(errctx, akbasic_runtime_start(&RUNTIME, AKBASIC_MODE_RUN));
PASS(errctx, akbasic_graphics_akgl_begin(&GRAPHICS));
PASS(errctx, akbasic_runtime_run(&RUNTIME, 0));
PASS(errctx, akbasic_graphics_akgl_end(&GRAPHICS));
TEST_REQUIRE_INT(AKGLSINKSTATE.graphicmode, 1);
TEST_REQUIRE_STR(AKGLSINKSTATE.text[0], "HIDDEN");
PASS(errctx, clear_target());
PASS(errctx, akbasic_graphics_akgl_render(&GRAPHICS));
PASS(errctx, akbasic_sink_akgl_render(&AKGLSINK));
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
TEST_REQUIRE(shot != NULL, "could not read the full-bitmap frame back");
TEST_REQUIRE(pixel_is(shot, 40, 40, 0x88, 0x39, 0x32),
"GRAPHIC 1 must show DRAW even after PRINT wrote the hidden text grid");
SDL_DestroySurface(shot);
PASS(errctx, AKGLSINK.graphic(&AKGLSINK, 0, -1));
PASS(errctx, clear_target());
PASS(errctx, akbasic_graphics_akgl_render(&GRAPHICS));
PASS(errctx, akbasic_sink_akgl_render(&AKGLSINK));
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
TEST_REQUIRE(shot != NULL, "could not read the text frame back");
TEST_REQUIRE(!pixel_is(shot, 40, 40, 0x88, 0x39, 0x32),
"GRAPHIC 0 must put the retained text plane back over the drawing");
SDL_DestroySurface(shot);
PASS(errctx, AKGLSINK.clear(&AKGLSINK));
fullrows = TARGET_SIZE / AKGLSINKSTATE.cellh;
PASS(errctx, AKGLSINK.graphic(&AKGLSINK, 2, 3));
TEST_REQUIRE_INT(AKGLSINKSTATE.y, 3 * AKGLSINKSTATE.cellh);
TEST_REQUIRE_INT(AKGLSINKSTATE.rows, fullrows - 3);
PASS(errctx, AKGLSINK.writeln(&AKGLSINK, "BOTTOM"));
PASS(errctx, clear_target());
PASS(errctx, akbasic_graphics_akgl_render(&GRAPHICS));
PASS(errctx, akbasic_sink_akgl_render(&AKGLSINK));
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
TEST_REQUIRE(shot != NULL, "could not read the split-screen frame back");
TEST_REQUIRE(pixel_is(shot, 40, 40, 0x88, 0x39, 0x32),
"GRAPHIC 2 must leave the graphics region above its text rows visible");
SDL_DestroySurface(shot);
PASS(errctx, AKGLSINK.graphic(&AKGLSINK, 0, -1));
TEST_REQUIRE_INT(AKGLSINKSTATE.texttop, 0);
TEST_REQUIRE_INT(AKGLSINKSTATE.rows, fullrows);
akbasic_graphics_akgl_shutdown(&GRAPHICS);
SUCCEED_RETURN(errctx);
}
/** /**
* @brief The contact says which way to push out and how far. * @brief The contact says which way to push out and how far.
* *
@@ -1339,6 +1411,7 @@ int main(void)
CATCH(errctx, test_static_geometry()); CATCH(errctx, test_static_geometry());
CATCH(errctx, test_contact_geometry()); CATCH(errctx, test_contact_geometry());
CATCH(errctx, test_drawing_layer_persists()); CATCH(errctx, test_drawing_layer_persists());
CATCH(errctx, test_graphic_text_composition());
} CLEANUP { } CLEANUP {
if ( font != NULL ) { if ( font != NULL ) {
TTF_CloseFont(font); TTF_CloseFont(font);

View File

@@ -34,6 +34,22 @@ static akerr_ErrorContext AKERR_NOIGNORE *run_program(const char *source)
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
/** @brief The values delivered through GRAPHIC's optional text-plane hook. */
static int GRAPHIC_CALLS = 0;
static int GRAPHIC_MODE = 0;
static int GRAPHIC_SPLIT = 0;
static akerr_ErrorContext AKERR_NOIGNORE *record_graphic(akbasic_TextSink *self, int mode, int split)
{
PREPARE_ERROR(errctx);
(void)self;
GRAPHIC_CALLS += 1;
GRAPHIC_MODE = mode;
GRAPHIC_SPLIT = split;
SUCCEED_RETURN(errctx);
}
/** @brief The white of palette index 2, which most of these draw with. */ /** @brief The white of palette index 2, which most of these draw with. */
#define WHITE "#ffffff" #define WHITE "#ffffff"
/** @brief The red of palette index 3. */ /** @brief The red of palette index 3. */
@@ -323,6 +339,21 @@ static void test_graphic(void)
TEST_REQUIRE_STR(MOCK.log, ""); TEST_REQUIRE_STR(MOCK.log, "");
harness_stop(); harness_stop();
/* The third argument reaches a graphical text sink as the first text row. */
TEST_REQUIRE_OK(harness_start(NULL));
mock_devices_init();
GRAPHIC_CALLS = 0;
HARNESS_SINK.graphic = record_graphic;
TEST_REQUIRE_OK(akbasic_runtime_set_devices(&HARNESS_RUNTIME, &MOCK_GRAPHICS,
&MOCK_AUDIO, &MOCK_INPUT, NULL));
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 GRAPHIC 2, 1, 19\n"));
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
TEST_REQUIRE_INT(GRAPHIC_CALLS, 1);
TEST_REQUIRE_INT(GRAPHIC_MODE, 2);
TEST_REQUIRE_INT(GRAPHIC_SPLIT, 19);
harness_stop();
/* GRAPHIC CLR drops the saved shapes and resets the whole state. */ /* GRAPHIC CLR drops the saved shapes and resets the whole state. */
TEST_REQUIRE_OK(run_program("10 COLOR 1, 3\n20 GRAPHIC 5\n")); TEST_REQUIRE_OK(run_program("10 COLOR 1, 3\n20 GRAPHIC 5\n"));
TEST_REQUIRE_STR(MOCK.log, "freeshapes\n"); TEST_REQUIRE_STR(MOCK.log, "freeshapes\n");

View File

@@ -47,6 +47,9 @@ static akerr_ErrorContext AKERR_NOIGNORE *broken_clear(akbasic_TextSink *self)
static int MOVETOS = 0; static int MOVETOS = 0;
static int WINDOWS = 0; static int WINDOWS = 0;
static int WINDOW_ARGS[4] = { 0, 0, 0, 0 }; static int WINDOW_ARGS[4] = { 0, 0, 0, 0 };
static int GRAPHICS = 0;
static int GRAPHIC_MODE = 0;
static int GRAPHIC_SPLIT = 0;
static akerr_ErrorContext AKERR_NOIGNORE *counting_moveto(akbasic_TextSink *self, int col, int row) static akerr_ErrorContext AKERR_NOIGNORE *counting_moveto(akbasic_TextSink *self, int col, int row)
{ {
@@ -70,6 +73,17 @@ static akerr_ErrorContext AKERR_NOIGNORE *counting_window(akbasic_TextSink *self
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
static akerr_ErrorContext AKERR_NOIGNORE *counting_graphic(akbasic_TextSink *self, int mode, int split)
{
PREPARE_ERROR(errctx);
(void)self;
GRAPHICS += 1;
GRAPHIC_MODE = mode;
GRAPHIC_SPLIT = split;
SUCCEED_RETURN(errctx);
}
int main(void) int main(void)
{ {
akbasic_TextSink primary; akbasic_TextSink primary;
@@ -163,11 +177,15 @@ int main(void)
gridded.clear = broken_clear; gridded.clear = broken_clear;
gridded.moveto = counting_moveto; gridded.moveto = counting_moveto;
gridded.window = counting_window; gridded.window = counting_window;
gridded.grid = NULL;
gridded.graphic = counting_graphic;
MOVETOS = 0; MOVETOS = 0;
WINDOWS = 0; WINDOWS = 0;
GRAPHICS = 0;
TEST_REQUIRE_OK(akbasic_sink_init_tee(&tee, &teestate, &primary, &gridded, NULL)); TEST_REQUIRE_OK(akbasic_sink_init_tee(&tee, &teestate, &primary, &gridded, NULL));
TEST_REQUIRE(tee.moveto != NULL, "a tee with a gridded half must offer moveto"); TEST_REQUIRE(tee.moveto != NULL, "a tee with a gridded half must offer moveto");
TEST_REQUIRE(tee.window != NULL, "a tee with a gridded half must offer window"); TEST_REQUIRE(tee.window != NULL, "a tee with a gridded half must offer window");
TEST_REQUIRE(tee.graphic != NULL, "a tee with a graphical half must offer graphic");
TEST_REQUIRE_OK(tee.moveto(&tee, 3, 4)); TEST_REQUIRE_OK(tee.moveto(&tee, 3, 4));
TEST_REQUIRE_OK(tee.window(&tee, 0, 20, 39, 24)); TEST_REQUIRE_OK(tee.window(&tee, 0, 20, 39, 24));
TEST_REQUIRE_INT(MOVETOS, 1); TEST_REQUIRE_INT(MOVETOS, 1);
@@ -176,6 +194,10 @@ int main(void)
TEST_REQUIRE_INT(WINDOW_ARGS[1], 20); TEST_REQUIRE_INT(WINDOW_ARGS[1], 20);
TEST_REQUIRE_INT(WINDOW_ARGS[2], 39); TEST_REQUIRE_INT(WINDOW_ARGS[2], 39);
TEST_REQUIRE_INT(WINDOW_ARGS[3], 24); TEST_REQUIRE_INT(WINDOW_ARGS[3], 24);
TEST_REQUIRE_OK(tee.graphic(&tee, 2, 19));
TEST_REQUIRE_INT(GRAPHICS, 1);
TEST_REQUIRE_INT(GRAPHIC_MODE, 2);
TEST_REQUIRE_INT(GRAPHIC_SPLIT, 19);
/* The gridded half being first is wired the same way round. */ /* The gridded half being first is wired the same way round. */
WINDOWS = 0; WINDOWS = 0;
@@ -193,6 +215,10 @@ int main(void)
broken.writeln = broken_write; broken.writeln = broken_write;
broken.readline = NULL; broken.readline = NULL;
broken.clear = broken_clear; broken.clear = broken_clear;
broken.moveto = NULL;
broken.window = NULL;
broken.grid = NULL;
broken.graphic = NULL;
TEST_REQUIRE_OK(akbasic_sink_init_tee(&tee, &teestate, &primary, &broken, NULL)); TEST_REQUIRE_OK(akbasic_sink_init_tee(&tee, &teestate, &primary, &broken, NULL));
TEST_REQUIRE_STATUS(tee.write(&tee, "X"), AKERR_IO); TEST_REQUIRE_STATUS(tee.write(&tee, "X"), AKERR_IO);
TEST_REQUIRE_STATUS(tee.writeln(&tee, "X"), AKERR_IO); TEST_REQUIRE_STATUS(tee.writeln(&tee, "X"), AKERR_IO);

View File

@@ -197,16 +197,17 @@ static akerr_ErrorContext AKERR_NOIGNORE *draw_program(const char *outpath, int
} }
PASS(errctx, akbasic_runtime_load(&RUNTIME, SOURCE)); PASS(errctx, akbasic_runtime_load(&RUNTIME, SOURCE));
PASS(errctx, akbasic_runtime_start(&RUNTIME, AKBASIC_MODE_RUN)); PASS(errctx, akbasic_runtime_start(&RUNTIME, AKBASIC_MODE_RUN));
/* The same layer bracket and composition order the standalone frontend uses. */
PASS(errctx, akbasic_graphics_akgl_begin(&GRAPHICS));
PASS(errctx, akbasic_runtime_run(&RUNTIME, 0)); PASS(errctx, akbasic_runtime_run(&RUNTIME, 0));
PASS(errctx, akbasic_graphics_akgl_end(&GRAPHICS));
/* /*
* The drawing verbs are immediate and are already on the target. The text * The drawing layer, text grid and sprites all redraw from retained state.
* grid and the sprites are not: both are redrawn from state the interpreter * Keep this in the frontend's order so a documentation image exercises the
* keeps, which in the standalone frontend happens once a frame. There is no * same GRAPHIC composition users run rather than a convenient direct path.
* frame here, so this is it -- and the order is the frontend's, grid first
* and sprites over it, because that ordering is what a program written
* against the real host will have assumed.
*/ */
PASS(errctx, akbasic_graphics_akgl_render(&GRAPHICS));
if ( grid ) { if ( grid ) {
PASS(errctx, akbasic_sink_akgl_render(&SINK)); PASS(errctx, akbasic_sink_akgl_render(&SINK));
} }