From 3342f2b56995397a5763bfeb2018e5e26492abcb Mon Sep 17 00:00:00 2001 From: Tachikoma Date: Mon, 3 Aug 2026 08:08:34 -0400 Subject: [PATCH] Make GRAPHIC select the text plane Co-authored-by: Andrew Kesterson --- docs/06-graphics.md | 127 ++++++++++++++++--------------- docs/10-embedding.md | 5 +- docs/images/sprite-collision.png | Bin 739 -> 735 bytes include/akbasic/akgl.h | 4 + include/akbasic/sink.h | 14 ++++ src/runtime_graphics.c | 25 ++++-- src/sink_akgl.c | 84 +++++++++++++++++++- src/sink_stdio.c | 1 + src/sink_tee.c | 21 +++++ tests/akgl_backends.c | 73 ++++++++++++++++++ tests/graphics_verbs.c | 31 ++++++++ tests/sink_tee.c | 26 +++++++ tools/screenshot.c | 13 ++-- 13 files changed, 345 insertions(+), 79 deletions(-) diff --git a/docs/06-graphics.md b/docs/06-graphics.md index 357dd91..634456d 100644 --- a/docs/06-graphics.md +++ b/docs/06-graphics.md @@ -62,22 +62,28 @@ small number that is not a colour. ### GRAPHIC -`GRAPHIC mode` chooses a screen mode; `GRAPHIC CLR` clears it. Mode 0 is text and -refuses to draw. +`GRAPHIC mode [, clear] [, split]` chooses the display composition; `GRAPHIC CLR` +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 `MAINTENANCE.md` if you are editing one. ### DRAW -```basic requires=akgl screenshot=draw -10 COLOR 1, 8 -20 DRAW 1, 20, 180 TO 90, 40 TO 160, 150 TO 230, 20 TO 300, 120 -30 COLOR 2, 6 -40 DRAW 2, 20, 190 TO 300, 190 -50 LOCATE 160, 100 -60 COLOR 3, 3 -70 DRAW 3 +```basic requires=akgl screenshot=draw text=1 +10 GRAPHIC 1, 1 +20 COLOR 1, 8 +30 DRAW 1, 20, 180 TO 90, 40 TO 160, 150 TO 230, 20 TO 300, 120 +40 COLOR 2, 6 +50 DRAW 2, 20, 190 TO 300, 190 +60 LOCATE 160, 100 +70 COLOR 3, 3 +80 DRAW 3 ``` ![](images/draw.png) @@ -88,14 +94,15 @@ picture — plots wherever `LOCATE` left the pixel cursor. ### BOX -```basic requires=akgl screenshot=box -10 COLOR 1, 8 -20 BOX 1, 20, 30, 130, 140 -30 COLOR 2, 6 -40 BOX 2, 180, 30, 290, 140, 30 -50 COLOR 3, 3 -60 LOCATE 300, 190 -70 BOX 3, 20, 160 +```basic requires=akgl screenshot=box text=1 +10 GRAPHIC 1, 1 +20 COLOR 1, 8 +30 BOX 1, 20, 30, 130, 140 +40 COLOR 2, 6 +50 BOX 2, 180, 30, 290, 140, 30 +60 COLOR 3, 3 +70 LOCATE 300, 190 +80 BOX 3, 20, 160 ``` ![](images/box.png) @@ -110,13 +117,14 @@ have. ### CIRCLE -```basic requires=akgl screenshot=circle -10 COLOR 1, 8 -20 CIRCLE 1, 80, 70, 60, 60 -30 COLOR 2, 6 -40 CIRCLE 2, 230, 70, 75, 45 -50 COLOR 3, 3 -60 CIRCLE 3, 160, 140, 130, 50, 90, 270 +```basic requires=akgl screenshot=circle text=1 +10 GRAPHIC 1, 1 +20 COLOR 1, 8 +30 CIRCLE 1, 80, 70, 60, 60 +40 COLOR 2, 6 +50 CIRCLE 2, 230, 70, 75, 45 +60 COLOR 3, 3 +70 CIRCLE 3, 160, 140, 130, 50, 90, 270 ``` ![](images/circle.png) @@ -129,14 +137,15 @@ degree increment, and a large increment is what turns a circle into a polygon. ### PAINT -```basic requires=akgl screenshot=paint -10 COLOR 1, 8 -20 CIRCLE 1, 100, 100, 70, 70 -30 BOX 1, 180, 50, 290, 150 -40 COLOR 2, 6 -50 PAINT 2, 100, 100 -60 COLOR 3, 3 -70 PAINT 3, 230, 100 +```basic requires=akgl screenshot=paint text=1 +10 GRAPHIC 1, 1 +20 COLOR 1, 8 +30 CIRCLE 1, 100, 100, 70, 70 +40 BOX 1, 180, 50, 290, 150 +50 COLOR 2, 6 +60 PAINT 2, 100, 100 +70 COLOR 3, 3 +80 PAINT 3, 230, 100 ``` ![](images/paint.png) @@ -153,13 +162,14 @@ coordinates finishes. ### SCALE -```basic requires=akgl screenshot=scale size=640x400 -10 COLOR 1, 3 -20 BOX 1, 0, 0, 319, 199 -30 SCALE 1, 319, 199 -40 COLOR 2, 6 -50 BOX 2, 0, 0, 319, 199 -60 DRAW 2, 0, 0 TO 319, 199 +```basic requires=akgl screenshot=scale size=640x400 text=1 +10 GRAPHIC 1, 1 +20 COLOR 1, 3 +30 BOX 1, 0, 0, 319, 199 +40 SCALE 1, 319, 199 +50 COLOR 2, 6 +60 BOX 2, 0, 0, 319, 199 +70 DRAW 2, 0, 0 TO 319, 199 ``` ![](images/scale.png) @@ -212,15 +222,16 @@ parallel passes; see Chapter 13. `SSHAPE` copies a rectangle off the screen and `GSHAPE` stamps it back: -```basic requires=akgl screenshot=shapes -10 COLOR 1, 8 -20 CIRCLE 1, 40, 40, 30, 30 -30 COLOR 2, 6 -40 PAINT 2, 40, 40 -50 SSHAPE A$, 8, 8, 72, 72 -60 GSHAPE A$, 120, 20 -70 GSHAPE A$, 200, 60 -80 GSHAPE A$, 120, 120 +```basic requires=akgl screenshot=shapes text=1 +10 GRAPHIC 1, 1 +20 COLOR 1, 8 +30 CIRCLE 1, 40, 40, 30, 30 +40 COLOR 2, 6 +50 PAINT 2, 40, 40 +60 SSHAPE A$, 8, 8, 72, 72 +70 GSHAPE A$, 120, 20 +80 GSHAPE A$, 200, 60 +90 GSHAPE A$, 120, 120 ``` ![](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 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, -for a reason `akbasic_sink_akgl_render()` explains — and by default it owns the whole -window. `WINDOW` shrinks it: - -```basic norun -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. +`GRAPHIC 1` changes that order: the full bitmap owns the display, while the text layer +keeps its contents off-screen. Returning with `GRAPHIC 0` reveals what `PRINT` wrote. +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 +needed to make a full bitmap visible. **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 diff --git a/docs/10-embedding.md b/docs/10-embedding.md index 6913778..236e1e4 100644 --- a/docs/10-embedding.md +++ b/docs/10-embedding.md @@ -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 *(*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 *(*graphic)(struct akbasic_TextSink *self, int mode, int split); } 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 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 -`RWINDOW` know to refuse by name rather than pretending. Supply `grid` if your text layer +**The last four are optional and may be NULL**, which is how `CHAR`, `WINDOW`, `RWINDOW` +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 without it anything placing a character and a sprite at the same spot has to hardcode a number measured against your font. diff --git a/docs/images/sprite-collision.png b/docs/images/sprite-collision.png index f46db1594513d2e8bb1613b784988f8e02db21d0..a16a07afd17423551eb31cc24d6be2158407494d 100644 GIT binary patch literal 735 zcmeAS@N?(olHy`uVBq!ia0y~yU~~YoPjD~+NwKu`e;62;mU+53hE&XXd-tI4YX_0G zK!^28Wm6_FZCcLbwScwkW-x>5hQ<85D!5fdCMKwUt2p-m>6}uP>d#Nl`8@gYj16cG ziD1E&+ZW9Pj{n;rU3B;Q@$HJ&TZ7tIY+gk-_wJCcWj;{D)Rq82|K08%kTR87U}eTv;CWOf3F^2@8Tm|puRcyih(f8ox_JXOwZ)A?AfLEGjqmf z7M_DFd)Bp0zAW%;N`vBzhVUyjX;O#&C?s&G!O;VQa~mzx9vIB?e>4N627-Vp_h&Jm z;fFdSF8r|T`me^zK?XluIejz8qCZzo=d;{7`G(`_X%@!spMU)*%k6!YI?LSn%eSjb z!dD;Irc>#6;{HD4^KV~BuSl-H@pa~$^Zegs*A=~b`8WAQ>B9EB!yhajD2k_A%Op7( zFghD7zo1(hbzxtP)$XPc=WUC>9k`vsByKJ{ulZ>Sliuq|Gk^Y-RBbQn2)*$6@ZYW4 zaoSG;nAWA0bxHZDJO~nVTk+6>Y29MaI;nF?K)*dulJfI&w&(zPM?1sLnD?OwJVZf^ zXEx6x1X<2h&hzg=gvJA|S?|Mh8-h6`nmB=IM@L(C-0SBGAY&_DomB$b@@)A%pe<=- zEw6l49yHCnXTNT3;NFFMW-eT_UTtp_>$cRB#ZLde?UnF;vH$1^UAF@QK45f4vqZ(+ gi^LRysXXL^yxf=W4^ATc{(~evUHx3vIVCg!08V2XGXMYp literal 739 zcmeAS@N?(olHy`uVBq!ia0y~yU~~YoPjD~+NwKu`e;62;R(ZNOhE&XXdv~MnVF!t} zL;Z7~S~#g8RqvW$pfKr)4}3 z?3Fb(|7YiR(r0t)yY$_+=lr^IJXmQN=Z?uYHM3@LGQCSoY>|4V&lDFs`H#sNN2ay} zrnvCKQ!gu>v14>LU|hfA=d4V@XW}3>9JL!Bf27XX{;^KTo>QU;iYi>sz7N04@VOi6 zjO$kmE@%G-nzDYKXT__h3XI=37}$UPB+giFWbO3T$nn5moBR9cL@lnG`Rp{$-deMl zm$eGwrtMLGl7D~Z^P(@TS2o_w{k3LJf4RBrxlQjMe$72mIWvJ!?7zEDhvW5>+A^L2T5`#!lIIO@wkpJ&=-&K+x#pB=k@T*GpTVwfY) zsO5HFKTgq@(y)5w+(m7|oI9qp3T9dKHN4K4G{0>#2hcA&I@+|CyIORBJfWSjFDY>1O4Sd?iNlFCTuDW%a(|eR`TF6~*J-8(aUy>+C!Vw{oVFyHOA*#Zd$nH1SV9 V+j(h?=NBiCIi9Y5F6*2UngBqm62t%i diff --git a/include/akbasic/akgl.h b/include/akbasic/akgl.h index 2c23ef6..53b205d 100644 --- a/include/akbasic/akgl.h +++ b/include/akbasic/akgl.h @@ -189,6 +189,10 @@ typedef struct int columns; /* the character grid the cell size works out to */ 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 cursorrow; diff --git a/include/akbasic/sink.h b/include/akbasic/sink.h index 09b129d..bcb6ad1 100644 --- a/include/akbasic/sink.h +++ b/include/akbasic/sink.h @@ -82,6 +82,20 @@ typedef struct akbasic_TextSink * @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); + /** + * 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; /** @brief State for the stdio-backed sink. */ diff --git a/src/runtime_graphics.c b/src/runtime_graphics.c index 8ab5d97..d2eaf17 100644 --- a/src/runtime_graphics.c +++ b/src/runtime_graphics.c @@ -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) { PREPARE_ERROR(errctx); - double args[2]; + double args[3]; int count = 0; int mode = 0; + int split = -1; akbasic_Color background; (void)lval; (void)rval; 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"); mode = (int)args[0]; /* * 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 - * anything against a host's renderer -- so the mode is recorded and only its - * one observable consequence is honoured: mode 0 is text, and text mode does - * not draw. Refusing an out-of-range mode still matters, because that is a - * typo the program author can fix. + * and in whether the bottom of the screen stays text. Resolution belongs to + * the graphics backend, while the latter is a text-sink decision: an AKGL + * sink hides its text plane for a full bitmap and moves it below the split. + * A stream has no plane, so it simply leaves that optional entry point NULL. */ FAIL_ZERO_RETURN(errctx, (mode >= 0 && mode <= 5), AKBASIC_ERR_BOUNDS, "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. */ PASS(errctx, obj->graphics->free_shapes(obj->graphics)); 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_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; + if ( obj->sink != NULL && obj->sink->graphic != NULL ) { + PASS(errctx, obj->sink->graphic(obj->sink, mode, split)); + } if ( count >= 2 && args[1] != 0.0 ) { PASS(errctx, akbasic_graphics_source_color(&obj->gfx, 0, &background)); PASS(errctx, obj->graphics->clear(obj->graphics, background)); diff --git a/src/sink_akgl.c b/src/sink_akgl.c index 76503d1..0d3e2a7 100644 --- a/src/sink_akgl.c +++ b/src/sink_akgl.c @@ -475,7 +475,7 @@ static akerr_ErrorContext *sink_window(akbasic_TextSink *self, int left, int top "The sink has no character grid to window"); maxcols = state->fullwidth / state->cellw; - maxrows = state->fullheight / state->cellh; + maxrows = (state->fullheight / state->cellh) - state->texttop; if ( left < 0 ) { left = 0; } if ( top < 0 ) { top = 0; } 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"); 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->rows = (bottom - top) + 1; 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); } +/** + * @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) { PREPARE_ERROR(errctx); @@ -554,6 +626,8 @@ akerr_ErrorContext *akbasic_sink_init_akgl(akbasic_TextSink *obj, akbasic_AkglSi state->cellh = cellh; state->columns = w / cellw; state->rows = h / cellh; + state->graphicmode = 0; + state->texttop = 0; if ( 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->window = sink_window; obj->grid = sink_grid; + obj->graphic = sink_graphic; SUCCEED_RETURN(errctx); } @@ -584,6 +659,11 @@ akerr_ErrorContext *akbasic_sink_akgl_render(akbasic_TextSink *obj) FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER, "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 * that changed -- that was tried, with a `drawn[]` array marking which rows diff --git a/src/sink_stdio.c b/src/sink_stdio.c index 70c968d..3dffee7 100644 --- a/src/sink_stdio.c +++ b/src/sink_stdio.c @@ -109,5 +109,6 @@ akerr_ErrorContext *akbasic_sink_init_stdio(akbasic_TextSink *obj, akbasic_Stdio obj->moveto = NULL; obj->window = NULL; obj->grid = NULL; + obj->graphic = NULL; SUCCEED_RETURN(errctx); } diff --git a/src/sink_tee.c b/src/sink_tee.c index dc6211b..8d01ad0 100644 --- a/src/sink_tee.c +++ b/src/sink_tee.c @@ -143,6 +143,22 @@ static akerr_ErrorContext *tee_window(akbasic_TextSink *self, int left, int top, 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. * @@ -202,6 +218,7 @@ akerr_ErrorContext *akbasic_sink_init_tee(akbasic_TextSink *obj, akbasic_TeeSink obj->moveto = NULL; obj->window = NULL; obj->grid = NULL; + obj->graphic = NULL; /* * Offered only when a half can actually do it, so CHAR's refusal against a * 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) ) { obj->grid = tee_grid; } + if ( (primary != NULL && primary->graphic != NULL) || + (mirror != NULL && mirror->graphic != NULL) ) { + obj->graphic = tee_graphic; + } SUCCEED_RETURN(errctx); } diff --git a/tests/akgl_backends.c b/tests/akgl_backends.c index 698df9d..11ebfcd 100644 --- a/tests/akgl_backends.c +++ b/tests/akgl_backends.c @@ -1173,6 +1173,78 @@ static akerr_ErrorContext AKERR_NOIGNORE *test_drawing_layer_persists(void) 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. * @@ -1339,6 +1411,7 @@ int main(void) CATCH(errctx, test_static_geometry()); CATCH(errctx, test_contact_geometry()); CATCH(errctx, test_drawing_layer_persists()); + CATCH(errctx, test_graphic_text_composition()); } CLEANUP { if ( font != NULL ) { TTF_CloseFont(font); diff --git a/tests/graphics_verbs.c b/tests/graphics_verbs.c index 51019fb..78ecbf0 100644 --- a/tests/graphics_verbs.c +++ b/tests/graphics_verbs.c @@ -34,6 +34,22 @@ static akerr_ErrorContext AKERR_NOIGNORE *run_program(const char *source) 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. */ #define WHITE "#ffffff" /** @brief The red of palette index 3. */ @@ -323,6 +339,21 @@ static void test_graphic(void) TEST_REQUIRE_STR(MOCK.log, ""); 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. */ TEST_REQUIRE_OK(run_program("10 COLOR 1, 3\n20 GRAPHIC 5\n")); TEST_REQUIRE_STR(MOCK.log, "freeshapes\n"); diff --git a/tests/sink_tee.c b/tests/sink_tee.c index d2ecd06..203f744 100644 --- a/tests/sink_tee.c +++ b/tests/sink_tee.c @@ -47,6 +47,9 @@ static akerr_ErrorContext AKERR_NOIGNORE *broken_clear(akbasic_TextSink *self) static int MOVETOS = 0; static int WINDOWS = 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) { @@ -70,6 +73,17 @@ static akerr_ErrorContext AKERR_NOIGNORE *counting_window(akbasic_TextSink *self 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) { akbasic_TextSink primary; @@ -163,11 +177,15 @@ int main(void) gridded.clear = broken_clear; gridded.moveto = counting_moveto; gridded.window = counting_window; + gridded.grid = NULL; + gridded.graphic = counting_graphic; MOVETOS = 0; WINDOWS = 0; + GRAPHICS = 0; 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.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.window(&tee, 0, 20, 39, 24)); TEST_REQUIRE_INT(MOVETOS, 1); @@ -176,6 +194,10 @@ int main(void) TEST_REQUIRE_INT(WINDOW_ARGS[1], 20); TEST_REQUIRE_INT(WINDOW_ARGS[2], 39); 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. */ WINDOWS = 0; @@ -193,6 +215,10 @@ int main(void) broken.writeln = broken_write; broken.readline = NULL; 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_STATUS(tee.write(&tee, "X"), AKERR_IO); TEST_REQUIRE_STATUS(tee.writeln(&tee, "X"), AKERR_IO); diff --git a/tools/screenshot.c b/tools/screenshot.c index c5ff434..f2769b0 100644 --- a/tools/screenshot.c +++ b/tools/screenshot.c @@ -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_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_graphics_akgl_end(&GRAPHICS)); /* - * The drawing verbs are immediate and are already on the target. The text - * grid and the sprites are not: both are redrawn from state the interpreter - * keeps, which in the standalone frontend happens once a frame. There is no - * 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. + * The drawing layer, text grid and sprites all redraw from retained state. + * Keep this in the frontend's order so a documentation image exercises the + * same GRAPHIC composition users run rather than a convenient direct path. */ + PASS(errctx, akbasic_graphics_akgl_render(&GRAPHICS)); if ( grid ) { PASS(errctx, akbasic_sink_akgl_render(&SINK)); }