Draw into the whole window, not its top-left 320x200
The graphics verbs documented a coordinate transform that did not exist. With SCALE off a coordinate went straight to akgl_draw_* as a pixel address, so an 800x600 window drew a C128 listing into its corner and left the rest unused -- while the chapter said coordinates were 320x200 and stretching to fit was the host's business. akbasic_GraphicsBackend gains a size entry point, require_graphics() asks it before every verb that draws so a resized window is honoured between two statements, and 320x200 becomes the fallback for a backend that leaves it NULL. It is the record's one optional member, so a host written against the old header keeps the behaviour it had. SCALE now maps onto the device, and RGR(1)/RGR(2) report the drawing surface so a program can use a window whose size it did not choose. RGR(0) is BASIC 7.0's own field, the GRAPHIC mode. SCALE also mapped xmax onto the width rather than onto the last pixel, so SCALE 1, 319, 199 followed by DRAW 1, 319, 199 drew nothing at all -- one pixel past the surface. Fixed in the same line, because it is what makes "SCALE gives a C128 listing the whole window" true rather than nearly true. The akgl test renders against a 128x128 target, deliberately smaller than the old constants: a SCALE still dividing by them misses it entirely rather than landing somewhere plausible. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
67
TODO.md
67
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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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). |
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -24,12 +24,18 @@
|
||||
#include <akbasic/types.h>
|
||||
|
||||
/**
|
||||
* @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. */
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user