diff --git a/TODO.md b/TODO.md index 68abf40..339b9ff 100644 --- a/TODO.md +++ b/TODO.md @@ -800,13 +800,43 @@ behaviour to port. They matter to somebody typing in a listing out of a C128 man 17. **`GRAPHIC` records its mode but honours only one consequence of it.** 7.0's five modes differ in bitmap resolution and in whether the bottom of the screen stays text. Neither - means anything against a host's renderer, whose size the interpreter does not know and - does not own. The mode is stored, an out-of-range one is refused because that is a typo - worth catching, and the one observable rule is that mode 0 is text. + means anything against a host's renderer, whose size the interpreter does not own. The + mode is stored, an out-of-range one is refused because that is a typo worth catching, and + the one observable rule is that mode 0 is text. The mode is readable as `RGR(0)`, which + is 7.0's whole `RGR` and the one field of ours that is. -18. **Coordinates reaching a backend are BASIC's 320x200 space, and scaling to the window is - the host's job.** The interpreter cannot ask the renderer how big it is without owning it. - `SCALE` maps user coordinates onto that space; with `SCALE` off they pass through. +18. **A coordinate reaching a backend is a device pixel, and the whole of the host's window + is reachable from BASIC.** This used to read "coordinates are BASIC's 320x200 space and + scaling to the window is the host's job", on the reasoning that the interpreter cannot + ask the renderer how big it is without owning it. **That reasoning was wrong twice over**, + which is what §8 item 9 came to say. + + It was wrong about the mechanism: not owning a thing is no bar to *asking* it, and the + backend record is how everything else here asks. `akbasic_GraphicsBackend` grew a `size` + entry point, `require_graphics()` calls it before every verb that draws -- so a resized + window is honoured between two statements rather than at attach -- and 320x200 became + the fallback for a backend that leaves `size` NULL. It is the record's one optional + entry point, so a host written against the old header keeps working and gets exactly the + behaviour it used to get. + + And it was wrong about the description: nothing ever stretched anything. With `SCALE` + off a coordinate was passed straight to `akgl_draw_*` as a pixel address, so an 800x600 + window drew a C128 listing into its top-left 320x200 corner and left the rest unused. + The documentation described a coordinate transform that did not exist. + + **What changed observably** is `SCALE`, which now maps user coordinates onto the device + rather than onto the constants, and `RGR(1)` / `RGR(2)`, which report the drawing + surface's width and height so a program can use a window whose size it did not choose. + A C128 listing draws in the corner as it always did; `SCALE 1, 319, 199` gives it the + whole window. + + **One fix rode along, in the same line of code and too small to defer.** `SCALE` mapped + `xmax` onto the *width*, which put the user space's far corner one pixel past the + surface: `SCALE 1, 319, 199` then `DRAW 1, 319, 199` drew nothing at all. It maps onto + the last pixel now, which is what makes that sentence above true rather than nearly + true. `tests/graphics_verbs.c` and `tests/akgl_backends.c`, the latter against a + 128x128 target chosen because it is *smaller* than the old constants -- a `SCALE` still + dividing by them misses it entirely rather than landing somewhere plausible. 19. **`PLAY` does not block.** On a C128 `PLAY` holds the program until the last note ends. §1.6 forbids that outright, so `PLAY` parses the string into a fixed queue and returns; @@ -1158,11 +1188,19 @@ deviations from the reference's *program*: `main.go` and the SDL half of last one in the source until one of them runs. The scan is textual on purpose: parsing every line up front would raise on lines the program would never have reached. -33. **Sprite coordinates are BASIC's 320×200, not the VIC-II's 0–511 by 0–255.** `MOVSPR` takes +33. **Sprite coordinates are device pixels, not the VIC-II's 0–511 by 0–255.** `MOVSPR` takes the same coordinate space `DRAW` does. A C128's sprite coordinates are the raster's, offset so that (24, 50) is the top-left of the visible screen; reproducing that would give the language two coordinate systems and make `MOVSPR 1, 0, 0` put a sprite off-screen. Consistent - with deviation 18. + with deviation 18, which is where "the same space `DRAW` uses" is defined — and it moved, + so this one moved with it. + + **`SCALE` deliberately does not apply to sprites.** It is a graphics-verb transform and + the sprite verbs do not call it, which was true before deviation 18 was rewritten and is + worth writing down now that the two spaces can differ: with `SCALE 1, 319, 199` on, a + `DRAW` at 160,100 and a `MOVSPR` to 160,100 land in different places. Arguably they + should agree; not changed here because it is a decision about what `SCALE` means rather + than a slip, and it wants its own commit. 34. **`MOVSPR`'s speed unit is ours.** BASIC 7.0 documents speed 0–15 with 15 fastest and says nothing about what a unit is worth. Here one unit is @@ -1757,9 +1795,18 @@ What remains, in priority order: almost everything links against it), which is why CI runs `src/symtab.c` instead and the whole-tree run is a local `cmake --build build --target mutation`. -9. Use the actual full SDL window for graphics operation width +9. ~~**Use the actual full SDL window for graphics operations.**~~ **Done**, and the premise + turned out to be half wrong in a way worth keeping. The documentation did say coordinates + are 320x200 whatever the window is — but nothing ever stretched them: with `SCALE` off a + coordinate went straight to `akgl_draw_*` as a pixel address, so an 800x600 window drew a + listing into its top-left corner and left the rest unused. The described transform did not + exist. -The current documentation states that graphics operations always use a 320x200 space regardless of the window's size. These verbs should be able to access the entirety of the SDL window to which they are bound. + `akbasic_GraphicsBackend` grew an optional `size` entry point, `require_graphics()` asks it + before every verb that draws so a resized window is honoured between statements, `SCALE` + maps onto the answer instead of onto the constants, and `RGR(1)` / `RGR(2)` let a program + read the size. 320x200 is now the fallback for a backend that will not answer. Deviation 18 + in §5 has the whole of it, including the off-by-one it fixed on the way. 11. Error codes are undefined for the user diff --git a/docs/06-graphics.md b/docs/06-graphics.md index 974c438..e735f2d 100644 --- a/docs/06-graphics.md +++ b/docs/06-graphics.md @@ -14,10 +14,35 @@ verb refuses by name: ## The coordinate space -Drawing coordinates are **320 by 200**, with (0, 0) at the top left — BASIC 7.0's -hi-res screen. That is true whatever size the host's window is; stretching to fit is -the host's business, not the program's. `SCALE` maps a different range onto the same -screen. +**A drawing coordinate is a pixel of the host's window**, with (0, 0) at the top left. +On the standalone interpreter's 800 by 600 window, `DRAW 1, 799, 599` lands on the +bottom-right pixel and everything in between is reachable. A game embedding the +interpreter gets whatever size its own renderer is. + +`RGR` is how a program finds out: + +```basic requires=akgl +10 PRINT "THE SCREEN IS" +20 PRINT RGR(1) +30 PRINT "BY" +40 PRINT RGR(2) +50 DRAW 1, 0, 0 TO RGR(1) - 1, RGR(2) - 1 +``` + +Subtracting one is not a wart, it is the last pixel: a window `RGR(1)` wide has +columns 0 through `RGR(1) - 1`. + +**A C128 listing assumes 320 by 200 and will draw in the top-left corner.** Give it +the whole window by naming the space it was written for: + +```basic requires=akgl +10 SCALE 1, 319, 199 +20 BOX 1, 0, 0, 319, 199 +``` + +That box is now the border of the window whatever size the window is. When no device +answers the size question at all, 320 by 200 is what the interpreter assumes — the +space a C128 listing was written for is the right thing to fall back to. ## Colour @@ -90,7 +115,18 @@ coordinates finishes. ``` Turns on user coordinates and gives their maxima. With it on, your coordinates are -mapped onto the 320 by 200 screen. `SCALE 0` turns it off. +mapped onto the drawing surface: 0 is the first pixel and the maximum you gave is the +*last* one, so `DRAW 1, 1023, 1023` above reaches the bottom-right corner rather than +missing it by a pixel. `SCALE 1` on its own uses 7.0's 1023 by 1023. `SCALE 0` turns +it off and coordinates go back to being window pixels. + +### RGR + +`RGR(0)` is the current `GRAPHIC` mode. `RGR(1)` and `RGR(2)` are the drawing +surface's width and height in pixels — those two are ours rather than 7.0's, and they +are what a program needs to use a window whose size it did not choose. All three +refuse when there is no graphics device, except `RGR(0)`, which is a mode this +interpreter recorded rather than a screen it has to go and measure. ### WIDTH diff --git a/docs/08-sprites.md b/docs/08-sprites.md index b5672ad..0819b97 100644 --- a/docs/08-sprites.md +++ b/docs/08-sprites.md @@ -84,8 +84,10 @@ the continuous form, which is a trap worth remembering. The continuous form does not move anything on the statement itself. The sprite moves as the program runs, paced by the host's clock. Speed 0 stops it. -Coordinates are the same 320 by 200 space the drawing verbs use, not the VIC-II's -raster coordinates. +Coordinates are the same window pixels the drawing verbs use, not the VIC-II's raster +coordinates — see Chapter 6, and `RGR(1)` and `RGR(2)` for how big the window is. +`SCALE` does not apply to them: a sprite is positioned in device pixels whatever the +drawing verbs are doing. ## Collision diff --git a/docs/12-function-reference.md b/docs/12-function-reference.md index bb7ddc3..075aee7 100644 --- a/docs/12-function-reference.md +++ b/docs/12-function-reference.md @@ -25,6 +25,7 @@ so a call with the wrong number is a syntax error rather than a surprise. | `POINTER` | 1 | `POINTER(V)` | The address of a variable's value. | | `POINTERVAR` | 1 | `POINTERVAR(V)` | The address of the variable structure itself, metadata included. | | `RAD` | 1 | `RAD(n)` | Degrees converted to radians. | +| `RGR` | 1 | `RGR(f)` | The `GRAPHIC` mode (0), or the drawing surface's width (1) or height (2) in pixels. | | `RIGHT` | 2 | `RIGHT(A$, n)` | The rightmost `n` characters. Clamped. | | `RSPCOLOR` | 1 | `RSPCOLOR(n)` | One of `SPRCOLOR`'s two shared registers, 1 or 2. | | `RSPPOS` | 2 | `RSPPOS(n, f)` | A sprite's x (0), y (1) or speed (2). | diff --git a/docs/13-differences.md b/docs/13-differences.md index 7f517c0..1e10af9 100644 --- a/docs/13-differences.md +++ b/docs/13-differences.md @@ -89,7 +89,10 @@ interpreter's error code, which bears no relation to a Commodore error number. P ## Graphics -- **Coordinates are always 320 by 200**, whatever the window size. +- **A coordinate is a window pixel, not one of 320 by 200.** A C128 listing therefore + draws in the top-left corner of a larger window; `SCALE 1, 319, 199` gives it the + whole window back. `RGR(1)` and `RGR(2)` report the size, and are ours rather than + 7.0's — where 7.0's `RGR` takes only field 0, the `GRAPHIC` mode. - **`SSHAPE` puts a handle in the string, not the pixels.** You can pass it to `GSHAPE` and `SPRSAV`; you cannot save it or take its `LEN`. - **`WIDTH` is emulated** by drawing parallel passes. @@ -107,7 +110,8 @@ interpreter's error code, which bears no relation to a Commodore error number. P ## Sprites -- **Coordinates are the 320 by 200 drawing space**, not the VIC-II's raster space. +- **Coordinates are window pixels**, not the VIC-II's raster space, and `SCALE` does + not apply to them. - **`SPRSAV` takes an integer array**, not a string, for the data form — a string here cannot hold a zero byte. It also takes an **image file path**, which a C128 cannot. - **A sprite loaded from a file keeps the image's own size**, not 24 by 21. diff --git a/include/akbasic/graphics.h b/include/akbasic/graphics.h index 862c89b..e866ebd 100644 --- a/include/akbasic/graphics.h +++ b/include/akbasic/graphics.h @@ -24,12 +24,18 @@ #include /** - * @brief The coordinate space the backend receives, in C128 hi-res pixels. + * @brief The coordinate space assumed when the device will not say how big it is. * - * The interpreter does not own the renderer and cannot ask how big it is, so it - * emits the coordinates the program wrote -- BASIC 7.0's 320x200 hi-res screen, - * or whatever SCALE maps onto it. Stretching that to the window is the host's - * business, the same way the host owns the window in the first place. + * A coordinate reaching the backend is a *device pixel*: `DRAW 1, 799, 599` lands + * at the bottom-right of an 800x600 window, and every pixel of that window is + * addressable from BASIC. The interpreter still does not own the renderer -- it + * asks, through akbasic_GraphicsBackend::size(), and the backend that does own + * one answers. + * + * These constants are what is assumed when nobody answers: a backend that leaves + * `size` NULL, or reports a nonsense size, gets BASIC 7.0's 320x200 hi-res + * screen. That is the right fallback rather than a guess, because it is the space + * a C128 listing was written for. */ #define AKBASIC_GRAPHICS_WIDTH 320 #define AKBASIC_GRAPHICS_HEIGHT 200 @@ -70,6 +76,8 @@ typedef struct int linewidth; /* WIDTH: 1 or 2 pixels per drawn line */ double xmax; /* user-coordinate maxima SCALE maps from */ double ymax; + int devwidth; /* what the device last said it is, in pixels; */ + int devheight; /* the AKBASIC_GRAPHICS_* fallback until then */ } akbasic_GraphicsState; /** @@ -89,6 +97,21 @@ typedef struct akbasic_GraphicsBackend { void *self; + /** + * How big the drawing surface is, in pixels. + * + * **Optional, and the only optional entry point in the record.** It was + * added after the rest, so a host that filled one of these in by hand + * before it existed leaves it NULL and keeps the 320x200 fallback rather + * than crashing; and a backend drawing somewhere with no meaningful size + * has an honest way to say so. Everything else here is required. + * + * Called before each verb draws rather than once at attach, because a + * window is resizable and a script that runs for an hour should not be + * drawing to the size the window was when it started. + */ + akerr_ErrorContext AKERR_NOIGNORE *(*size)(struct akbasic_GraphicsBackend *self, int *width, int *height); + /** Plot one pixel. */ akerr_ErrorContext AKERR_NOIGNORE *(*point)(struct akbasic_GraphicsBackend *self, double x, double y, akbasic_Color color); /** Draw a line between two points. */ diff --git a/include/akbasic/sprite.h b/include/akbasic/sprite.h index e1c048c..cb8e0a2 100644 --- a/include/akbasic/sprite.h +++ b/include/akbasic/sprite.h @@ -56,10 +56,10 @@ * * `MOVSPR n, angle # speed` takes 0-15 on a C128 and the reference manual does * not say what a unit is in pixels; it says 15 is fastest and 0 stops. So the - * mapping is ours: one unit is #AKBASIC_SPRITE_SPEED_PIXELS_PER_SECOND pixels - * per second in BASIC's 320x200 space, which puts speed 15 at four seconds to - * cross the screen. Recorded as a deviation in TODO.md section 5 rather than - * presented as fidelity. + * mapping is ours: one unit is #AKBASIC_SPRITE_SPEED_PIXELS_PER_SECOND device + * pixels per second, which puts speed 15 at four seconds to cross a 320-pixel + * screen and proportionally longer across a wider one. Recorded as a deviation + * in TODO.md section 5 rather than presented as fidelity. */ #define AKBASIC_SPRITE_MAX_SPEED 15 /** @brief Pixels per second one unit of MOVSPR speed is worth. See #AKBASIC_SPRITE_MAX_SPEED. */ @@ -83,7 +83,7 @@ typedef struct bool yexpand; bool multicolour; int colorindex; /* 1-16, the sprite's own colour */ - double x; /* position in BASIC's 320x200 space */ + double x; /* position in device pixels; see graphics.h */ double y; double angle; /* MOVSPR's continuous motion: degrees */ int speed; /* clockwise from vertical, and 0-15 */ @@ -159,7 +159,7 @@ typedef struct akbasic_SpriteBackend akerr_ErrorContext AKERR_NOIGNORE *(*define_file)(struct akbasic_SpriteBackend *self, int n, const char *path, const char *root); /** Show or hide sprite @p n. */ akerr_ErrorContext AKERR_NOIGNORE *(*show)(struct akbasic_SpriteBackend *self, int n, bool visible); - /** Put sprite @p n at (@p x, @p y) in BASIC's 320x200 space. */ + /** Put sprite @p n at (@p x, @p y), in device pixels. */ akerr_ErrorContext AKERR_NOIGNORE *(*move)(struct akbasic_SpriteBackend *self, int n, double x, double y); /** * Set sprite @p n's own colour, its expansion bits and its priority. diff --git a/src/graphics_akgl.c b/src/graphics_akgl.c index 6e40235..f1f856c 100644 --- a/src/graphics_akgl.c +++ b/src/graphics_akgl.c @@ -47,6 +47,27 @@ static akerr_ErrorContext *state_of(akbasic_GraphicsBackend *self, akbasic_AkglG SUCCEED_RETURN(errctx); } +/** + * @brief How big the renderer's output is, which is the space BASIC draws into. + * + * SDL_GetCurrentRenderOutputSize rather than the window size: the two differ on + * a high-DPI display and on a renderer with a logical presentation set, and it + * is the output that a coordinate handed to akgl_draw_point actually indexes. + */ +static akerr_ErrorContext *gfx_size(akbasic_GraphicsBackend *self, int *width, int *height) +{ + PREPARE_ERROR(errctx); + akbasic_AkglGraphics *state = NULL; + + PASS(errctx, state_of(self, &state)); + FAIL_ZERO_RETURN(errctx, (width != NULL && height != NULL), AKERR_NULLPOINTER, + "NULL destination in gfx_size"); + FAIL_ZERO_RETURN(errctx, + SDL_GetCurrentRenderOutputSize(state->renderer->sdl_renderer, width, height), + AKGL_ERR_SDL, "%s", SDL_GetError()); + SUCCEED_RETURN(errctx); +} + static akerr_ErrorContext *gfx_point(akbasic_GraphicsBackend *self, double x, double y, akbasic_Color color) { PREPARE_ERROR(errctx); @@ -134,9 +155,7 @@ static akerr_ErrorContext *gfx_clear(akbasic_GraphicsBackend *self, akbasic_Colo int h = 0; PASS(errctx, state_of(self, &state)); - FAIL_ZERO_RETURN(errctx, - SDL_GetCurrentRenderOutputSize(state->renderer->sdl_renderer, &w, &h), - AKGL_ERR_SDL, "%s", SDL_GetError()); + PASS(errctx, gfx_size(self, &w, &h)); /* * A filled rectangle over the output rather than SDL_RenderClear, because @@ -231,6 +250,7 @@ akerr_ErrorContext *akbasic_graphics_init_akgl(akbasic_GraphicsBackend *obj, akb state->renderer = renderer; obj->self = state; + obj->size = gfx_size; obj->point = gfx_point; obj->line = gfx_line; obj->rect = gfx_rect; diff --git a/src/graphics_tables.c b/src/graphics_tables.c index ab458c6..cec9e9e 100644 --- a/src/graphics_tables.c +++ b/src/graphics_tables.c @@ -85,6 +85,14 @@ akerr_ErrorContext *akbasic_graphics_state_init(akbasic_GraphicsState *obj) obj->linewidth = 1; obj->xmax = (double)AKBASIC_GRAPHICS_WIDTH; obj->ymax = (double)AKBASIC_GRAPHICS_HEIGHT; + /* + * The size assumed until a device says otherwise, which require_graphics() + * asks it before every verb that draws. GRAPHIC CLR comes back through here + * and deliberately resets it: the next verb re-asks, so a stale size cannot + * outlive the device that reported it. + */ + obj->devwidth = AKBASIC_GRAPHICS_WIDTH; + obj->devheight = AKBASIC_GRAPHICS_HEIGHT; for ( source = 0; source < AKBASIC_COLOR_SOURCES; source++ ) { obj->source[source] = SOURCE_DEFAULTS[source]; } diff --git a/src/runtime_graphics.c b/src/runtime_graphics.c index 32837d7..d3d7324 100644 --- a/src/runtime_graphics.c +++ b/src/runtime_graphics.c @@ -39,40 +39,73 @@ #define CIRCLE_DEFAULT_INC 2.0 /** - * @brief Refuse politely when the host lent us no graphics device. + * @brief Refuse politely when the host lent us no graphics device, and ask its size. * * Every verb in this file opens with it. The standalone driver attaches no * backend at all, so this is the common path rather than an edge case, and it * has to name the verb -- "no graphics device" on its own tells a program author * nothing about which line to look at. + * + * It is also where the drawing surface's size is refreshed, which is why the two + * jobs are one function: the size is wanted by every verb that draws, every verb + * that draws already opens with this, and a window can be resized between two + * statements. A backend with no `size` -- it is the record's one optional entry + * point -- keeps whatever akbasic_graphics_state_init() left, which is 320x200. */ static akerr_ErrorContext *require_graphics(akbasic_Runtime *obj, const char *verb) { PREPARE_ERROR(errctx); + int width = 0; + int height = 0; FAIL_ZERO_RETURN(errctx, (obj != NULL && verb != NULL), AKERR_NULLPOINTER, "NULL argument in require_graphics"); FAIL_ZERO_RETURN(errctx, (obj->graphics != NULL), AKBASIC_ERR_DEVICE, "%s needs a graphics device and this runtime has none", verb); + if ( obj->graphics->size != NULL ) { + PASS(errctx, obj->graphics->size(obj->graphics, &width, &height)); + /* + * A device reporting nothing useful is not an error -- an SDL window + * mid-resize legitimately measures zero -- but it must not become the + * space a program draws into, because dividing by it in scale_point + * would send every scaled coordinate to infinity. + */ + if ( width > 0 && height > 0 ) { + obj->gfx.devwidth = width; + obj->gfx.devheight = height; + } + } SUCCEED_RETURN(errctx); } /** - * @brief Map a user coordinate onto the 320x200 space the backend receives. + * @brief Map a user coordinate onto the device pixels the backend receives. * - * With SCALE off this is the identity. With it on, BASIC's user coordinates run - * 0..xmax across the same screen, so the ratio is all there is to it. + * With SCALE off this is the identity, and a coordinate is a device pixel: the + * whole of the host's window is reachable from BASIC. With SCALE on, the + * program's coordinates run 0..xmax across the same surface, so the ratio is all + * there is to it -- and the surface is however big the device last said it was, + * which is what makes `SCALE 1, 319, 199` the way to run a C128 listing full + * screen. + * + * **`xmax` maps onto the last pixel, not onto the width**, which is a + * one-character difference and the whole difference between that sentence being + * true and being nearly true. Dividing by the width sends `SCALE 1, 319, 199` + * plus `DRAW 1, 319, 199` to (width, height) -- one past the bottom-right + * corner, off the surface, drawing nothing. A user maximum is a coordinate the + * program is entitled to plot, the same way 7.0's own `SCALE 1` then + * `DRAW 1, 1023, 1023` reaches the corner of a C128 screen rather than missing it. */ static void scale_point(akbasic_GraphicsState *gfx, double *x, double *y) { if ( !gfx->scaling ) { return; } - if ( gfx->xmax > 0.0 ) { - *x = (*x / gfx->xmax) * (double)AKBASIC_GRAPHICS_WIDTH; + if ( gfx->xmax > 0.0 && gfx->devwidth > 1 ) { + *x = (*x / gfx->xmax) * (double)(gfx->devwidth - 1); } - if ( gfx->ymax > 0.0 ) { - *y = (*y / gfx->ymax) * (double)AKBASIC_GRAPHICS_HEIGHT; + if ( gfx->ymax > 0.0 && gfx->devheight > 1 ) { + *y = (*y / gfx->ymax) * (double)(gfx->devheight - 1); } } @@ -249,6 +282,61 @@ akerr_ErrorContext *akbasic_cmd_scale(akbasic_Runtime *obj, akbasic_ASTLeaf *exp SUCCEED_RETURN(errctx); } +/* ----------------------------------------------------------------- RGR --- */ + +akerr_ErrorContext *akbasic_fn_rgr(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest) +{ + PREPARE_ERROR(errctx); + akbasic_Value *out = NULL; + double args[1]; + int count = 0; + int field = 0; + int64_t answer = 0; + + (void)lval; (void)rval; + FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER, + "NULL argument in RGR"); + PASS(errctx, akbasic_args_numbers(obj, expr, "RGR", args, 1, &count)); + FAIL_ZERO_RETURN(errctx, (count >= 1), AKBASIC_ERR_SYNTAX, "RGR expected a field number"); + field = (int)args[0]; + + /* + * Field 0 is BASIC 7.0's whole RGR: the current GRAPHIC mode. It answers + * from state and works with no device, because a program can ask what mode + * it set without a screen to set it on. + * + * Fields 1 and 2 are ours, and they are the half of item 9 a program can + * see: the drawing surface's real width and height in pixels, so a listing + * can fill whatever window the host opened instead of assuming 320x200. + * They go through require_graphics(), which is both the refusal when there + * is no device -- asking how big the screen is when there is no screen is a + * program bug worth reporting -- and the refresh that makes the answer + * current rather than whatever it was at attach. + */ + switch ( field ) { + case 0: + answer = (int64_t)obj->gfx.mode; + break; + case 1: + PASS(errctx, require_graphics(obj, "RGR")); + answer = (int64_t)obj->gfx.devwidth; + break; + case 2: + PASS(errctx, require_graphics(obj, "RGR")); + answer = (int64_t)obj->gfx.devheight; + break; + default: + FAIL_RETURN(errctx, AKBASIC_ERR_BOUNDS, "RGR: field %d is outside 0..2", field); + } + + PASS(errctx, akbasic_environment_new_value(obj->environment, &out)); + PASS(errctx, akbasic_value_zero(out)); + out->valuetype = AKBASIC_TYPE_INTEGER; + out->intval = answer; + *dest = out; + SUCCEED_RETURN(errctx); +} + /* ---------------------------------------------------------------- DRAW --- */ akerr_ErrorContext *akbasic_cmd_draw(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest) diff --git a/src/verbs.c b/src/verbs.c index 253312a..a7a6107 100644 --- a/src/verbs.c +++ b/src/verbs.c @@ -136,6 +136,7 @@ static const akbasic_Verb VERBS[] = { { "RESTORE", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_restore }, { "RESUME", AKBASIC_TOK_COMMAND, -1, akbasic_parse_resume, akbasic_cmd_resume }, { "RETURN", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_return }, + { "RGR", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_rgr }, { "RIGHT", AKBASIC_TOK_FUNCTION, 2, NULL, akbasic_fn_right }, { "RSPCOLOR", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_rspcolor }, { "RSPPOS", AKBASIC_TOK_FUNCTION, 2, NULL, akbasic_fn_rsppos }, diff --git a/src/verbs.h b/src/verbs.h index 5a23015..8eb75da 100644 --- a/src/verbs.h +++ b/src/verbs.h @@ -171,6 +171,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_locate(struct akbasic_Runtime *ob akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_paint(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest); akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_scale(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest); akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_sshape(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest); +akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_rgr(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest); /* Group I sound verbs -- src/runtime_audio.c */ akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_envelope(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest); diff --git a/tests/akgl_backends.c b/tests/akgl_backends.c index f6a5393..cb1df0b 100644 --- a/tests/akgl_backends.c +++ b/tests/akgl_backends.c @@ -148,6 +148,43 @@ static akerr_ErrorContext AKERR_NOIGNORE *test_draw_reaches_pixels(void) SUCCEED_RETURN(errctx); } +/** + * @brief The renderer's own size is the space BASIC draws into. + * + * The one test that puts a real SDL renderer behind item 9. The target here is + * 128x128 -- deliberately *smaller* than the 320x200 fallback -- so a `SCALE` + * that still divided by the old constants would put its far corner at 2.5 times + * the target's width and light nothing at all. + */ +static akerr_ErrorContext AKERR_NOIGNORE *test_size_is_the_renderer(void) +{ + PREPARE_ERROR(errctx); + SDL_Surface *shot = NULL; + + PASS(errctx, clear_target()); + PASS(errctx, start_runtime("10 PRINT RGR(1)\n20 PRINT RGR(2)\n")); + TEST_REQUIRE_STR(OUTPUT, "128\n128\n"); + stop_runtime(); + + PASS(errctx, clear_target()); + PASS(errctx, start_runtime("10 SCALE 1, 1000, 1000\n" + "20 DRAW 1, 1000, 1000\n")); + TEST_REQUIRE_STR(OUTPUT, ""); + + shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL); + TEST_REQUIRE(shot != NULL, "could not read the render target back"); + /* + * The far corner of the user space lands on the far corner of the target. + * The old code divided by the 320x200 constants, which would have put this + * at (400, 400) -- off a 128x128 surface entirely, lighting nothing. + */ + TEST_REQUIRE(pixel_is(shot, TARGET_SIZE - 1, TARGET_SIZE - 1, 0xff, 0xff, 0xff), + "SCALE 1,1000,1000 then DRAW 1,1000,1000 should reach the bottom-right pixel"); + SDL_DestroySurface(shot); + stop_runtime(); + SUCCEED_RETURN(errctx); +} + /** @brief A BOX outlines: its corners are lit and its middle is not. */ static akerr_ErrorContext AKERR_NOIGNORE *test_box_outlines(void) { @@ -519,6 +556,7 @@ int main(void) "Couldn't open %s: %s", AKBASIC_TEST_FONT, SDL_GetError()); CATCH(errctx, test_draw_reaches_pixels()); + CATCH(errctx, test_size_is_the_renderer()); CATCH(errctx, test_box_outlines()); CATCH(errctx, test_paint_fills_region()); CATCH(errctx, test_shape_roundtrip()); diff --git a/tests/graphics_verbs.c b/tests/graphics_verbs.c index e888e2f..f4739a5 100644 --- a/tests/graphics_verbs.c +++ b/tests/graphics_verbs.c @@ -172,22 +172,39 @@ static void test_paint(void) harness_stop(); } -/** @brief SCALE maps user coordinates onto the 320x200 space, and only then. */ +/** + * @brief SCALE maps user coordinates onto the device's own size, and only then. + * + * The mock reports 640x400, which is what every scaled expectation below is + * against. If the interpreter ever stops asking the device and goes back to the + * 320x200 constants, every one of them is out by a factor of two -- which is + * the reason the mock's size is not 320x200. + */ static void test_scale(void) { - /* Off by default: a coordinate passes through untouched. */ + /* Off by default: a coordinate is a device pixel and passes through + untouched, which is what puts the whole of the host's window in reach. */ TEST_REQUIRE_OK(run_program("10 DRAW 1, 160, 100\n")); TEST_REQUIRE_STR(MOCK.log, "point 160.0,100.0 " WHITE "\n"); harness_stop(); - /* On, with the 7.0 default maxima: the middle of the user space is the - middle of the screen. */ - TEST_REQUIRE_OK(run_program("10 SCALE 1\n20 DRAW 1, 1023, 1023\n")); - TEST_REQUIRE_STR(MOCK.log, "point 320.0,200.0 " WHITE "\n"); + /* A coordinate past the old 320x200 ceiling is not clamped, refused, or + wrapped. This is item 9 in one assertion. */ + TEST_REQUIRE_OK(run_program("10 DRAW 1, 639, 399\n")); + TEST_REQUIRE_STR(MOCK.log, "point 639.0,399.0 " WHITE "\n"); harness_stop(); - TEST_REQUIRE_OK(run_program("10 SCALE 1, 640, 400\n20 DRAW 1, 320, 200\n")); - TEST_REQUIRE_STR(MOCK.log, "point 160.0,100.0 " WHITE "\n"); + /* On, with the 7.0 default maxima: the far corner of the user space is the + far corner of the *device*, not of a 320x200 screen inside it -- and it + is the last pixel, 639, rather than one past it. */ + TEST_REQUIRE_OK(run_program("10 SCALE 1\n20 DRAW 1, 1023, 1023\n")); + TEST_REQUIRE_STR(MOCK.log, "point 639.0,399.0 " WHITE "\n"); + harness_stop(); + + /* And a C128 listing fills the window by declaring the space it was + written for, which is the migration path for every one of them. */ + TEST_REQUIRE_OK(run_program("10 SCALE 1, 319, 199\n20 DRAW 1, 319, 199\n")); + TEST_REQUIRE_STR(MOCK.log, "point 639.0,399.0 " WHITE "\n"); harness_stop(); TEST_REQUIRE_OK(run_program("10 SCALE 1, 0, 400\n")); @@ -196,6 +213,84 @@ static void test_scale(void) harness_stop(); } +/** + * @brief A backend that will not say how big it is gets the 320x200 fallback. + * + * `size` is the record's one optional entry point, so a host that filled a + * backend in before it existed has to keep working -- and what it gets is the + * space a C128 listing was written for rather than a division by zero. + */ +static void test_scale_without_a_size(void) +{ + TEST_REQUIRE_OK(harness_start(NULL)); + mock_devices_init(); + MOCK_GRAPHICS.size = NULL; + TEST_REQUIRE_OK(akbasic_runtime_set_devices(&HARNESS_RUNTIME, &MOCK_GRAPHICS, NULL, NULL, NULL)); + TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 SCALE 1\n20 DRAW 1, 1023, 1023\n")); + TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN)); + TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0)); + TEST_REQUIRE_STR(MOCK.log, "point 319.0,199.0 " WHITE "\n"); + harness_stop(); + + /* + * A device reporting nothing useful -- an SDL window mid-resize measures + * zero -- is not an error and must not become the space, because dividing + * by it would send every scaled coordinate to infinity. + */ + TEST_REQUIRE_OK(harness_start(NULL)); + mock_devices_init(); + MOCK.devwidth = 0; + MOCK.devheight = 0; + TEST_REQUIRE_OK(akbasic_runtime_set_devices(&HARNESS_RUNTIME, &MOCK_GRAPHICS, NULL, NULL, NULL)); + TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 SCALE 1\n20 DRAW 1, 1023, 1023\n")); + TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN)); + TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0)); + TEST_REQUIRE_STR(MOCK.log, "point 319.0,199.0 " WHITE "\n"); + harness_stop(); +} + +/** @brief RGR answers the mode, the width and the height, and refuses the rest. */ +static void test_rgr(void) +{ + TEST_REQUIRE_OK(run_program("10 GRAPHIC 3\n20 PRINT RGR(0)\n")); + TEST_REQUIRE_STR(HARNESS_OUTPUT, "3\n"); + harness_stop(); + + TEST_REQUIRE_OK(run_program("10 PRINT RGR(1)\n20 PRINT RGR(2)\n")); + TEST_REQUIRE_STR(HARNESS_OUTPUT, "640\n400\n"); + harness_stop(); + + /* The whole point of asking: a program can draw to the far corner without + knowing in advance what size window the host opened. */ + TEST_REQUIRE_OK(run_program("10 DRAW 1, RGR(1) - 1, RGR(2) - 1\n")); + TEST_REQUIRE_STR(MOCK.log, "point 639.0,399.0 " WHITE "\n"); + harness_stop(); + + TEST_REQUIRE_OK(run_program("10 PRINT RGR(3)\n")); + TEST_REQUIRE(strstr(HARNESS_OUTPUT, "outside 0..2") != NULL, + "RGR(3) should be refused, got \"%s\"", HARNESS_OUTPUT); + harness_stop(); + + /* + * The mode is state and answers with no device; a size is not, and asking + * for one when there is no screen is a program bug worth reporting. + */ + TEST_REQUIRE_OK(harness_start(NULL)); + TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 PRINT RGR(0)\n")); + TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN)); + TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0)); + TEST_REQUIRE_STR(HARNESS_OUTPUT, "0\n"); + harness_stop(); + + TEST_REQUIRE_OK(harness_start(NULL)); + TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 PRINT RGR(1)\n")); + TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN)); + TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0)); + TEST_REQUIRE(strstr(HARNESS_OUTPUT, "RGR needs a graphics device") != NULL, + "RGR(1) with no device should refuse, got \"%s\"", HARNESS_OUTPUT); + harness_stop(); +} + /** @brief GRAPHIC records the mode, clears on request and refuses a bad mode. */ static void test_graphic(void) { @@ -280,6 +375,8 @@ int main(void) test_circle(); test_paint(); test_scale(); + test_scale_without_a_size(); + test_rgr(); test_graphic(); test_shapes(); test_no_device(); diff --git a/tests/mockdevice.h b/tests/mockdevice.h index 92c3ce9..3a9bbc7 100644 --- a/tests/mockdevice.h +++ b/tests/mockdevice.h @@ -43,6 +43,8 @@ typedef struct /* Graphics */ int shapes; /* handles handed out so far */ bool paint_exhausts; /* when true, paint reports a partial fill */ + int devwidth; /* what size() reports; see mock_devices_init */ + int devheight; /* Audio */ bool voice_active[AKBASIC_AUDIO_VOICES]; @@ -94,6 +96,24 @@ static void mock_log(const char *format, ...) /* --------------------------------------------------------------- graphics -- */ +/** + * @brief Report the recorded surface size, and log that it was asked. + * + * Logged like every other call because *when* the interpreter asks matters: it + * re-asks before each verb that draws, deliberately, so a resized window is not + * drawn to at its old size. + */ +static akerr_ErrorContext *mock_size(akbasic_GraphicsBackend *self, int *width, int *height) +{ + PREPARE_ERROR(errctx); + (void)self; + FAIL_ZERO_RETURN(errctx, (width != NULL && height != NULL), AKERR_NULLPOINTER, + "NULL destination in mock_size"); + *width = MOCK.devwidth; + *height = MOCK.devheight; + SUCCEED_RETURN(errctx); +} + static akerr_ErrorContext *mock_point(akbasic_GraphicsBackend *self, double x, double y, akbasic_Color color) { PREPARE_ERROR(errctx); @@ -415,8 +435,18 @@ __attribute__((unused)) static void mock_devices_init(void) { memset(&MOCK, 0, sizeof(MOCK)); + /* + * 640x400 rather than 320x200, on purpose: it is exactly twice the fallback, + * so a scaled coordinate that came out right by accident -- because nothing + * consulted the device at all -- is off by a factor of two rather than + * indistinguishable. A test that wants the fallback clears MOCK_GRAPHICS.size, + * the way the SOUND test clears MOCK_AUDIO.sweep. + */ + MOCK.devwidth = 640; + MOCK.devheight = 400; MOCK_GRAPHICS.self = &MOCK; + MOCK_GRAPHICS.size = mock_size; MOCK_GRAPHICS.point = mock_point; MOCK_GRAPHICS.line = mock_line; MOCK_GRAPHICS.rect = mock_rect;