Wire the sink and the three devices to libakgl
src/sink_akgl.c, src/graphics_akgl.c, src/audio_akgl.c and src/input_akgl.c, in
the akbasic_akgl target, which is the only thing here that links SDL.
-DAKBASIC_WITH_AKGL=ON had never been configured in this repository before, and
it now builds and passes.
The sink is what section 3 has been waiting on. Its character grid comes from
akgl_text_measure(font, "A", &w, &h), the direct equivalent of the reference's
font.SizeUTF8("A") and the call that did not exist until 42b60f7. Wrapping is
done on the character grid rather than by handing SDL_ttf a wraplength, because
the cursor has to land somewhere definite: a program that PRINTs a long string
and then PRINTs again expects the second to start on the row after the first
ended, and only the code that placed the characters knows which row that is.
tests/akgl_backends.c draws into a 128x128 software renderer under the dummy
video driver and reads the pixels back -- the pattern deps/libakgl/tests/draw.c
established, which needs no display and no offscreen harness. It asserts the
seam rather than libakgl's own behaviour: a BASIC line in, a lit pixel of the
right colour out.
Four things in libakgl had to be worked around to get here. All four are
commented at their site with "filed upstream" and recorded in TODO.md section 3:
- An embedded libakgl requires SDL, SDL_image, SDL_mixer, SDL_ttf and jansson to
be *installed*. It builds its own vendored copies only when it is top-level,
and they are sitting right there in deps/libakgl/deps. Every lookup is guarded
with if(NOT TARGET ...), so this adds those five subdirectories before
add_subdirectory(deps/libakgl) -- the same trick and the same ordering
requirement akerror::akerror and akstdlib::akstdlib already need.
- akgl/controller.h does not compile on its own: it declares handlers taking an
akgl_Actor * and includes nothing that declares the type.
- There is no way to attach a 2D backend to a renderer you already have.
akgl_render_init2d() installs the vtable but also creates its own window and
writes the camera global, so it belongs to the akgl_game_init() path -- which
is exactly the path an embedding host is not on. The test assigns the six
pointers by hand.
- akgl_text_rendertextat() segfaults on a backend whose vtable is empty; it
reaches through renderer->draw_texture without checking it. Same class of
defect 42b60f7's own commit added a draw test for.
The sink's readline reports end of input rather than reading: a drawn text layer
is not a source of lines, and INPUT through one wants a line editor built on the
keystroke ring. EOF rather than an error is the contract sink.h states, so INPUT
already handles it. That editor is the next piece of work there.
70/70 core ctest with no SDL on the include path, 71/71 with the akgl suite,
clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 08:46:53 -04:00
|
|
|
/**
|
|
|
|
|
* @file graphics_akgl.c
|
|
|
|
|
* @brief Wires the graphics backend record to libakgl's immediate-mode drawing.
|
|
|
|
|
*
|
|
|
|
|
* A thin adaptor and deliberately so: the interesting decisions about what a
|
|
|
|
|
* BASIC graphics verb means live in src/runtime_graphics.c, and everything here
|
|
|
|
|
* is coordinate and colour conversion plus the shape pool that SSHAPE hands out
|
|
|
|
|
* handles into.
|
|
|
|
|
*
|
|
|
|
|
* The akgl_draw_* family is new in libakgl 42b60f7. Every one of its entry
|
|
|
|
|
* points takes the akgl_RenderBackend the host already initialized rather than
|
|
|
|
|
* reaching for a global, which is exactly what goal 3 needs.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
#include <string.h>
|
|
|
|
|
|
|
|
|
|
#include <akerror.h>
|
|
|
|
|
|
|
|
|
|
#include <akgl/draw.h>
|
|
|
|
|
#include <akgl/error.h>
|
|
|
|
|
|
|
|
|
|
#include <akbasic/akgl.h>
|
|
|
|
|
#include <akbasic/error.h>
|
|
|
|
|
|
|
|
|
|
/** @brief The colour conversion, which is the whole impedance mismatch. */
|
|
|
|
|
static SDL_Color to_sdl(akbasic_Color color)
|
|
|
|
|
{
|
|
|
|
|
SDL_Color out;
|
|
|
|
|
|
|
|
|
|
out.r = color.r;
|
|
|
|
|
out.g = color.g;
|
|
|
|
|
out.b = color.b;
|
|
|
|
|
out.a = color.a;
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @brief Recover the backend's own state, or say that it has none. */
|
|
|
|
|
static akerr_ErrorContext *state_of(akbasic_GraphicsBackend *self, akbasic_AkglGraphics **dest)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (self != NULL && dest != NULL), AKERR_NULLPOINTER,
|
|
|
|
|
"NULL argument in akgl graphics backend");
|
|
|
|
|
*dest = (akbasic_AkglGraphics *)self->self;
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (*dest != NULL), AKERR_NULLPOINTER,
|
|
|
|
|
"akgl graphics backend has no state");
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
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>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-08-01 07:35:20 -04:00
|
|
|
/**
|
|
|
|
|
* @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);
|
|
|
|
|
}
|
|
|
|
|
|
Wire the sink and the three devices to libakgl
src/sink_akgl.c, src/graphics_akgl.c, src/audio_akgl.c and src/input_akgl.c, in
the akbasic_akgl target, which is the only thing here that links SDL.
-DAKBASIC_WITH_AKGL=ON had never been configured in this repository before, and
it now builds and passes.
The sink is what section 3 has been waiting on. Its character grid comes from
akgl_text_measure(font, "A", &w, &h), the direct equivalent of the reference's
font.SizeUTF8("A") and the call that did not exist until 42b60f7. Wrapping is
done on the character grid rather than by handing SDL_ttf a wraplength, because
the cursor has to land somewhere definite: a program that PRINTs a long string
and then PRINTs again expects the second to start on the row after the first
ended, and only the code that placed the characters knows which row that is.
tests/akgl_backends.c draws into a 128x128 software renderer under the dummy
video driver and reads the pixels back -- the pattern deps/libakgl/tests/draw.c
established, which needs no display and no offscreen harness. It asserts the
seam rather than libakgl's own behaviour: a BASIC line in, a lit pixel of the
right colour out.
Four things in libakgl had to be worked around to get here. All four are
commented at their site with "filed upstream" and recorded in TODO.md section 3:
- An embedded libakgl requires SDL, SDL_image, SDL_mixer, SDL_ttf and jansson to
be *installed*. It builds its own vendored copies only when it is top-level,
and they are sitting right there in deps/libakgl/deps. Every lookup is guarded
with if(NOT TARGET ...), so this adds those five subdirectories before
add_subdirectory(deps/libakgl) -- the same trick and the same ordering
requirement akerror::akerror and akstdlib::akstdlib already need.
- akgl/controller.h does not compile on its own: it declares handlers taking an
akgl_Actor * and includes nothing that declares the type.
- There is no way to attach a 2D backend to a renderer you already have.
akgl_render_init2d() installs the vtable but also creates its own window and
writes the camera global, so it belongs to the akgl_game_init() path -- which
is exactly the path an embedding host is not on. The test assigns the six
pointers by hand.
- akgl_text_rendertextat() segfaults on a backend whose vtable is empty; it
reaches through renderer->draw_texture without checking it. Same class of
defect 42b60f7's own commit added a draw test for.
The sink's readline reports end of input rather than reading: a drawn text layer
is not a source of lines, and INPUT through one wants a line editor built on the
keystroke ring. EOF rather than an error is the contract sink.h states, so INPUT
already handles it. That editor is the next piece of work there.
70/70 core ctest with no SDL on the include path, 71/71 with the akgl suite,
clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 08:46:53 -04:00
|
|
|
static akerr_ErrorContext *gfx_point(akbasic_GraphicsBackend *self, double x, double y, akbasic_Color color)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_AkglGraphics *state = NULL;
|
|
|
|
|
|
|
|
|
|
PASS(errctx, state_of(self, &state));
|
|
|
|
|
PASS(errctx, akgl_draw_point(state->renderer, (float32_t)x, (float32_t)y, to_sdl(color)));
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static akerr_ErrorContext *gfx_line(akbasic_GraphicsBackend *self, double x1, double y1, double x2, double y2, akbasic_Color color)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_AkglGraphics *state = NULL;
|
|
|
|
|
|
|
|
|
|
PASS(errctx, state_of(self, &state));
|
|
|
|
|
PASS(errctx, akgl_draw_line(state->renderer, (float32_t)x1, (float32_t)y1,
|
|
|
|
|
(float32_t)x2, (float32_t)y2, to_sdl(color)));
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @brief Turn two opposite corners into the rect SDL wants.
|
|
|
|
|
*
|
|
|
|
|
* BASIC gives two corners in whatever order the program felt like; SDL_FRect is
|
|
|
|
|
* an origin and a size and does nothing useful with a negative one.
|
|
|
|
|
*/
|
|
|
|
|
static SDL_FRect corners_to_rect(double x1, double y1, double x2, double y2)
|
|
|
|
|
{
|
|
|
|
|
SDL_FRect rect;
|
|
|
|
|
|
|
|
|
|
rect.x = (float32_t)((x1 < x2) ? x1 : x2);
|
|
|
|
|
rect.y = (float32_t)((y1 < y2) ? y1 : y2);
|
|
|
|
|
rect.w = (float32_t)((x1 < x2) ? (x2 - x1) : (x1 - x2));
|
|
|
|
|
rect.h = (float32_t)((y1 < y2) ? (y2 - y1) : (y1 - y2));
|
|
|
|
|
return rect;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static akerr_ErrorContext *gfx_rect(akbasic_GraphicsBackend *self, double x1, double y1, double x2, double y2, akbasic_Color color)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_AkglGraphics *state = NULL;
|
|
|
|
|
SDL_FRect rect;
|
|
|
|
|
|
|
|
|
|
PASS(errctx, state_of(self, &state));
|
|
|
|
|
rect = corners_to_rect(x1, y1, x2, y2);
|
|
|
|
|
PASS(errctx, akgl_draw_rect(state->renderer, &rect, to_sdl(color)));
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static akerr_ErrorContext *gfx_filled_rect(akbasic_GraphicsBackend *self, double x1, double y1, double x2, double y2, akbasic_Color color)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_AkglGraphics *state = NULL;
|
|
|
|
|
SDL_FRect rect;
|
|
|
|
|
|
|
|
|
|
PASS(errctx, state_of(self, &state));
|
|
|
|
|
rect = corners_to_rect(x1, y1, x2, y2);
|
|
|
|
|
PASS(errctx, akgl_draw_filled_rect(state->renderer, &rect, to_sdl(color)));
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static akerr_ErrorContext *gfx_paint(akbasic_GraphicsBackend *self, int x, int y, akbasic_Color color)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_AkglGraphics *state = NULL;
|
|
|
|
|
|
|
|
|
|
PASS(errctx, state_of(self, &state));
|
|
|
|
|
/*
|
|
|
|
|
* AKERR_OUTOFBOUNDS out of here means the fill exhausted its span stack and
|
|
|
|
|
* left the region partly filled. It is passed straight through: PAINT is
|
|
|
|
|
* where that is turned into something a BASIC program can see, because PAINT
|
|
|
|
|
* is the only thing that knows a program is watching.
|
|
|
|
|
*/
|
|
|
|
|
PASS(errctx, akgl_draw_flood_fill(state->renderer, x, y, to_sdl(color)));
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static akerr_ErrorContext *gfx_clear(akbasic_GraphicsBackend *self, akbasic_Color color)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_AkglGraphics *state = NULL;
|
|
|
|
|
SDL_FRect rect;
|
|
|
|
|
int w = 0;
|
|
|
|
|
int h = 0;
|
|
|
|
|
|
|
|
|
|
PASS(errctx, state_of(self, &state));
|
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>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-08-01 07:35:20 -04:00
|
|
|
PASS(errctx, gfx_size(self, &w, &h));
|
Wire the sink and the three devices to libakgl
src/sink_akgl.c, src/graphics_akgl.c, src/audio_akgl.c and src/input_akgl.c, in
the akbasic_akgl target, which is the only thing here that links SDL.
-DAKBASIC_WITH_AKGL=ON had never been configured in this repository before, and
it now builds and passes.
The sink is what section 3 has been waiting on. Its character grid comes from
akgl_text_measure(font, "A", &w, &h), the direct equivalent of the reference's
font.SizeUTF8("A") and the call that did not exist until 42b60f7. Wrapping is
done on the character grid rather than by handing SDL_ttf a wraplength, because
the cursor has to land somewhere definite: a program that PRINTs a long string
and then PRINTs again expects the second to start on the row after the first
ended, and only the code that placed the characters knows which row that is.
tests/akgl_backends.c draws into a 128x128 software renderer under the dummy
video driver and reads the pixels back -- the pattern deps/libakgl/tests/draw.c
established, which needs no display and no offscreen harness. It asserts the
seam rather than libakgl's own behaviour: a BASIC line in, a lit pixel of the
right colour out.
Four things in libakgl had to be worked around to get here. All four are
commented at their site with "filed upstream" and recorded in TODO.md section 3:
- An embedded libakgl requires SDL, SDL_image, SDL_mixer, SDL_ttf and jansson to
be *installed*. It builds its own vendored copies only when it is top-level,
and they are sitting right there in deps/libakgl/deps. Every lookup is guarded
with if(NOT TARGET ...), so this adds those five subdirectories before
add_subdirectory(deps/libakgl) -- the same trick and the same ordering
requirement akerror::akerror and akstdlib::akstdlib already need.
- akgl/controller.h does not compile on its own: it declares handlers taking an
akgl_Actor * and includes nothing that declares the type.
- There is no way to attach a 2D backend to a renderer you already have.
akgl_render_init2d() installs the vtable but also creates its own window and
writes the camera global, so it belongs to the akgl_game_init() path -- which
is exactly the path an embedding host is not on. The test assigns the six
pointers by hand.
- akgl_text_rendertextat() segfaults on a backend whose vtable is empty; it
reaches through renderer->draw_texture without checking it. Same class of
defect 42b60f7's own commit added a draw test for.
The sink's readline reports end of input rather than reading: a drawn text layer
is not a source of lines, and INPUT through one wants a line editor built on the
keystroke ring. EOF rather than an error is the contract sink.h states, so INPUT
already handles it. That editor is the next piece of work there.
70/70 core ctest with no SDL on the include path, 71/71 with the akgl suite,
clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 08:46:53 -04:00
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* A filled rectangle over the output rather than SDL_RenderClear, because
|
|
|
|
|
* clearing is the host's prerogative: a game that draws a background and
|
|
|
|
|
* then hands the renderer to a script does not expect GRAPHIC to wipe it to
|
|
|
|
|
* a colour of the script's choosing outside the area the script owns. This
|
|
|
|
|
* covers exactly the render output and nothing beyond it.
|
|
|
|
|
*/
|
|
|
|
|
rect.x = 0.0f;
|
|
|
|
|
rect.y = 0.0f;
|
|
|
|
|
rect.w = (float32_t)w;
|
|
|
|
|
rect.h = (float32_t)h;
|
|
|
|
|
PASS(errctx, akgl_draw_filled_rect(state->renderer, &rect, to_sdl(color)));
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static akerr_ErrorContext *gfx_save_shape(akbasic_GraphicsBackend *self, int x1, int y1, int x2, int y2, int *handle)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_AkglGraphics *state = NULL;
|
|
|
|
|
SDL_Rect region;
|
|
|
|
|
|
|
|
|
|
PASS(errctx, state_of(self, &state));
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (handle != NULL), AKERR_NULLPOINTER,
|
|
|
|
|
"NULL handle in save_shape");
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (state->shapecount < AKBASIC_AKGL_MAX_SHAPES),
|
|
|
|
|
AKBASIC_ERR_DEVICE,
|
|
|
|
|
"SSHAPE pool is full at %d saved regions; GRAPHIC CLR releases them",
|
|
|
|
|
AKBASIC_AKGL_MAX_SHAPES);
|
|
|
|
|
|
|
|
|
|
region.x = (x1 < x2) ? x1 : x2;
|
|
|
|
|
region.y = (y1 < y2) ? y1 : y2;
|
|
|
|
|
region.w = (x1 < x2) ? (x2 - x1) : (x1 - x2);
|
|
|
|
|
region.h = (y1 < y2) ? (y2 - y1) : (y1 - y2);
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* NULL through dest asks akgl_draw_copy_region to allocate the surface. That
|
|
|
|
|
* is the one allocation in this file and it belongs to the pool below, which
|
|
|
|
|
* is bounded -- so it is a fixed ceiling on memory, not an open one.
|
|
|
|
|
*/
|
|
|
|
|
state->shapes[state->shapecount] = NULL;
|
|
|
|
|
PASS(errctx, akgl_draw_copy_region(state->renderer, ®ion,
|
|
|
|
|
&state->shapes[state->shapecount]));
|
|
|
|
|
*handle = state->shapecount;
|
|
|
|
|
state->shapecount += 1;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static akerr_ErrorContext *gfx_paste_shape(akbasic_GraphicsBackend *self, int handle, double x, double y)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_AkglGraphics *state = NULL;
|
|
|
|
|
|
|
|
|
|
PASS(errctx, state_of(self, &state));
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (handle >= 0 && handle < state->shapecount),
|
|
|
|
|
AKBASIC_ERR_BOUNDS, "No saved shape %d", handle);
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (state->shapes[handle] != NULL), AKBASIC_ERR_STATE,
|
|
|
|
|
"Saved shape %d has been released", handle);
|
|
|
|
|
PASS(errctx, akgl_draw_paste_region(state->renderer, state->shapes[handle],
|
|
|
|
|
(float32_t)x, (float32_t)y));
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static akerr_ErrorContext *gfx_free_shapes(akbasic_GraphicsBackend *self)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_AkglGraphics *state = NULL;
|
|
|
|
|
int i = 0;
|
|
|
|
|
|
|
|
|
|
PASS(errctx, state_of(self, &state));
|
|
|
|
|
for ( i = 0; i < state->shapecount; i++ ) {
|
|
|
|
|
if ( state->shapes[i] != NULL ) {
|
|
|
|
|
SDL_DestroySurface(state->shapes[i]);
|
|
|
|
|
state->shapes[i] = NULL;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
state->shapecount = 0;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_graphics_init_akgl(akbasic_GraphicsBackend *obj, akbasic_AkglGraphics *state, akgl_RenderBackend *renderer)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && state != NULL), AKERR_NULLPOINTER,
|
|
|
|
|
"NULL argument in graphics_init_akgl");
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (renderer != NULL), AKERR_NULLPOINTER,
|
|
|
|
|
"NULL renderer in graphics_init_akgl: the host creates it, not this");
|
|
|
|
|
PASS(errctx, akgl_error_init());
|
|
|
|
|
|
|
|
|
|
memset(state, 0, sizeof(*state));
|
|
|
|
|
state->renderer = renderer;
|
|
|
|
|
|
|
|
|
|
obj->self = state;
|
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>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-08-01 07:35:20 -04:00
|
|
|
obj->size = gfx_size;
|
Wire the sink and the three devices to libakgl
src/sink_akgl.c, src/graphics_akgl.c, src/audio_akgl.c and src/input_akgl.c, in
the akbasic_akgl target, which is the only thing here that links SDL.
-DAKBASIC_WITH_AKGL=ON had never been configured in this repository before, and
it now builds and passes.
The sink is what section 3 has been waiting on. Its character grid comes from
akgl_text_measure(font, "A", &w, &h), the direct equivalent of the reference's
font.SizeUTF8("A") and the call that did not exist until 42b60f7. Wrapping is
done on the character grid rather than by handing SDL_ttf a wraplength, because
the cursor has to land somewhere definite: a program that PRINTs a long string
and then PRINTs again expects the second to start on the row after the first
ended, and only the code that placed the characters knows which row that is.
tests/akgl_backends.c draws into a 128x128 software renderer under the dummy
video driver and reads the pixels back -- the pattern deps/libakgl/tests/draw.c
established, which needs no display and no offscreen harness. It asserts the
seam rather than libakgl's own behaviour: a BASIC line in, a lit pixel of the
right colour out.
Four things in libakgl had to be worked around to get here. All four are
commented at their site with "filed upstream" and recorded in TODO.md section 3:
- An embedded libakgl requires SDL, SDL_image, SDL_mixer, SDL_ttf and jansson to
be *installed*. It builds its own vendored copies only when it is top-level,
and they are sitting right there in deps/libakgl/deps. Every lookup is guarded
with if(NOT TARGET ...), so this adds those five subdirectories before
add_subdirectory(deps/libakgl) -- the same trick and the same ordering
requirement akerror::akerror and akstdlib::akstdlib already need.
- akgl/controller.h does not compile on its own: it declares handlers taking an
akgl_Actor * and includes nothing that declares the type.
- There is no way to attach a 2D backend to a renderer you already have.
akgl_render_init2d() installs the vtable but also creates its own window and
writes the camera global, so it belongs to the akgl_game_init() path -- which
is exactly the path an embedding host is not on. The test assigns the six
pointers by hand.
- akgl_text_rendertextat() segfaults on a backend whose vtable is empty; it
reaches through renderer->draw_texture without checking it. Same class of
defect 42b60f7's own commit added a draw test for.
The sink's readline reports end of input rather than reading: a drawn text layer
is not a source of lines, and INPUT through one wants a line editor built on the
keystroke ring. EOF rather than an error is the contract sink.h states, so INPUT
already handles it. That editor is the next piece of work there.
70/70 core ctest with no SDL on the include path, 71/71 with the akgl suite,
clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 08:46:53 -04:00
|
|
|
obj->point = gfx_point;
|
|
|
|
|
obj->line = gfx_line;
|
|
|
|
|
obj->rect = gfx_rect;
|
|
|
|
|
obj->filled_rect = gfx_filled_rect;
|
|
|
|
|
obj->paint = gfx_paint;
|
|
|
|
|
obj->clear = gfx_clear;
|
|
|
|
|
obj->save_shape = gfx_save_shape;
|
|
|
|
|
obj->paste_shape = gfx_paste_shape;
|
|
|
|
|
obj->free_shapes = gfx_free_shapes;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
Keep what a program draws, instead of making it a sprite
A drawing lasted exactly one frame. The verbs are immediate, they went to the
back buffer, SDL double-buffers and the frontend never clears -- so the only way
to keep a picture was to capture it with `SSHAPE` and install it as a sprite,
which is what `examples/breakout/sprites/breakout.bas` spends two of its eight
sprites doing. That was TODO.md section 9 item 9.
The drawing verbs now render into a layer texture the frame composites under the
text and the sprites. Draw once; it is there on every frame after.
**Bracketed around the step phase, not around each verb.** One pair of
`SDL_SetRenderTarget` calls a frame instead of one per `DRAW`, and it is also
what makes `SSHAPE` read back what the program has just drawn rather than
whatever the last frame left.
**The layer is transparent where nothing was drawn.** It covers the whole window
and composites underneath, so an opaque one would black out the frame the moment
a program issued a single `DRAW`. And a fresh SDL target texture's contents are
undefined, so it is cleared on creation -- skipping that puts uninitialised
memory under the first frame's text and looks like a driver bug rather than a
missing memset.
**The line editor forced a wrinkle worth naming.** `akbasic_frontend_akgl_pump()`
is called from two places with different answers to "is a render target current":
the frame loop calls it between steps, and the sink's editor calls it from
*inside* a step, borrowing a frame while it waits for a typed line. SDL refuses
to present while a target is current, so the pump ends the layer, presents, and
puts it back only if it was the one that ended it. `akgl_frontend` caught this --
it drives a REPL session, and it failed with "You can't present on a render
target" the first time the brackets went in.
This does not make a drawing *visible* on its own. The text layer still repaints
every row it owns, opaque, every frame, and by default it owns the whole window;
`WINDOW` shrinks it and that half was already fixed. The two together are what a
picture needed, and the tests assert both -- a pixel still there a frame later
with nothing redrawn, and a pixel below a shrunk text area surviving the text
repaint. The second assertion wipes to a non-black colour first, because against
black it could not tell a transparent layer from an opaque one.
The tests found two of their own bugs on the way: `stop_runtime()` was not
tearing the graphics backend down, so re-initialising it dropped a live texture
on the floor; and a first draft called `begin()` before `start_runtime()`, which
re-inits the backend, so the assertion read back off an orphaned render target
and passed while proving nothing.
Chapters 6 and 13 stop saying a drawing has to be redrawn every frame, because it
does not. The batch-boundary tear stays documented -- it bites an `SSHAPE`
capture, which matters much less now that capturing is not the only way to keep a
picture.
Both games still run clean. 111 with akgl, 110 without.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-08-02 10:46:10 -04:00
|
|
|
|
|
|
|
|
/* ----------------------------------------------------------- the layer -- */
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @brief Make sure the layer texture exists and is the size of the output.
|
|
|
|
|
*
|
|
|
|
|
* Created on demand rather than at init, because it is the size of the window
|
|
|
|
|
* and a program that never draws should not pay for one. Recreated if the
|
|
|
|
|
* output has since changed size, which is what a resized window looks like from
|
|
|
|
|
* here.
|
|
|
|
|
*/
|
|
|
|
|
static akerr_ErrorContext AKERR_NOIGNORE *ensure_layer(akbasic_AkglGraphics *state)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
SDL_Texture *previous = NULL;
|
|
|
|
|
int w = 0;
|
|
|
|
|
int h = 0;
|
|
|
|
|
float texw = 0.0f;
|
|
|
|
|
float texh = 0.0f;
|
|
|
|
|
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, SDL_GetCurrentRenderOutputSize(state->renderer->sdl_renderer, &w, &h),
|
|
|
|
|
AKGL_ERR_SDL, "Couldn't measure the render output: %s", SDL_GetError());
|
|
|
|
|
if ( state->layer != NULL ) {
|
|
|
|
|
if ( SDL_GetTextureSize(state->layer, &texw, &texh)
|
|
|
|
|
&& (int)texw == w && (int)texh == h ) {
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
SDL_DestroyTexture(state->layer);
|
|
|
|
|
state->layer = NULL;
|
|
|
|
|
}
|
|
|
|
|
state->layer = SDL_CreateTexture(state->renderer->sdl_renderer,
|
|
|
|
|
SDL_PIXELFORMAT_RGBA8888,
|
|
|
|
|
SDL_TEXTUREACCESS_TARGET, w, h);
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, (state->layer != NULL), AKGL_ERR_SDL,
|
|
|
|
|
"Couldn't create the drawing layer: %s", SDL_GetError());
|
|
|
|
|
/*
|
|
|
|
|
* Blended, and cleared to fully transparent. The layer covers the whole
|
|
|
|
|
* window and is composited *under* the text and the sprites, so anywhere the
|
|
|
|
|
* program has not drawn has to let what is behind it through -- otherwise a
|
|
|
|
|
* single DRAW would black out the entire frame.
|
|
|
|
|
*/
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, SDL_SetTextureBlendMode(state->layer, SDL_BLENDMODE_BLEND),
|
|
|
|
|
AKGL_ERR_SDL, "%s", SDL_GetError());
|
|
|
|
|
/*
|
|
|
|
|
* **And clear it, because a fresh target texture's contents are undefined.**
|
|
|
|
|
* Skipping this puts whatever was in that memory on the screen under the
|
|
|
|
|
* first frame's text, which is the kind of defect that looks like a driver
|
|
|
|
|
* bug and is not.
|
|
|
|
|
*/
|
|
|
|
|
previous = SDL_GetRenderTarget(state->renderer->sdl_renderer);
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, SDL_SetRenderTarget(state->renderer->sdl_renderer, state->layer),
|
|
|
|
|
AKGL_ERR_SDL, "%s", SDL_GetError());
|
|
|
|
|
if ( !SDL_SetRenderDrawColor(state->renderer->sdl_renderer, 0, 0, 0, 0)
|
|
|
|
|
|| !SDL_RenderClear(state->renderer->sdl_renderer) ) {
|
|
|
|
|
/* Best effort: we are already failing, and leaving the target on the
|
|
|
|
|
layer would make the next unrelated draw land in the wrong place. */
|
|
|
|
|
(void)SDL_SetRenderTarget(state->renderer->sdl_renderer, previous);
|
|
|
|
|
FAIL_RETURN(errctx, AKGL_ERR_SDL, "Couldn't clear the drawing layer: %s", SDL_GetError());
|
|
|
|
|
}
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, SDL_SetRenderTarget(state->renderer->sdl_renderer, previous),
|
|
|
|
|
AKGL_ERR_SDL, "%s", SDL_GetError());
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_graphics_akgl_begin(akbasic_GraphicsBackend *obj)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_AkglGraphics *state = NULL;
|
|
|
|
|
|
|
|
|
|
if ( obj == NULL || obj->self == NULL ) {
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
state = (akbasic_AkglGraphics *)obj->self;
|
|
|
|
|
PASS(errctx, ensure_layer(state));
|
|
|
|
|
state->savedtarget = SDL_GetRenderTarget(state->renderer->sdl_renderer);
|
|
|
|
|
FAIL_ZERO_RETURN(errctx, SDL_SetRenderTarget(state->renderer->sdl_renderer, state->layer),
|
|
|
|
|
AKGL_ERR_SDL, "Couldn't make the drawing layer current: %s", SDL_GetError());
|
|
|
|
|
state->layeractive = true;
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool akbasic_graphics_akgl_layer_active(akbasic_GraphicsBackend *obj)
|
|
|
|
|
{
|
|
|
|
|
if ( obj == NULL || obj->self == NULL ) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return ((akbasic_AkglGraphics *)obj->self)->layeractive;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_graphics_akgl_end(akbasic_GraphicsBackend *obj)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_AkglGraphics *state = NULL;
|
|
|
|
|
|
|
|
|
|
if ( obj == NULL || obj->self == NULL ) {
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
state = (akbasic_AkglGraphics *)obj->self;
|
|
|
|
|
if ( !state->layeractive ) {
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
state->layeractive = false;
|
|
|
|
|
FAIL_ZERO_RETURN(errctx,
|
|
|
|
|
SDL_SetRenderTarget(state->renderer->sdl_renderer, state->savedtarget),
|
|
|
|
|
AKGL_ERR_SDL, "Couldn't restore the render target: %s", SDL_GetError());
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
akerr_ErrorContext *akbasic_graphics_akgl_render(akbasic_GraphicsBackend *obj)
|
|
|
|
|
{
|
|
|
|
|
PREPARE_ERROR(errctx);
|
|
|
|
|
akbasic_AkglGraphics *state = NULL;
|
|
|
|
|
|
|
|
|
|
if ( obj == NULL || obj->self == NULL ) {
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
state = (akbasic_AkglGraphics *)obj->self;
|
|
|
|
|
/* Nothing drawn, nothing to composite: a text-only program pays nothing. */
|
|
|
|
|
if ( state->layer == NULL ) {
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
FAIL_ZERO_RETURN(errctx,
|
|
|
|
|
SDL_RenderTexture(state->renderer->sdl_renderer, state->layer, NULL, NULL),
|
|
|
|
|
AKGL_ERR_SDL, "Couldn't draw the drawing layer: %s", SDL_GetError());
|
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void akbasic_graphics_akgl_shutdown(akbasic_GraphicsBackend *obj)
|
|
|
|
|
{
|
|
|
|
|
akbasic_AkglGraphics *state = NULL;
|
|
|
|
|
|
|
|
|
|
if ( obj == NULL || obj->self == NULL ) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
state = (akbasic_AkglGraphics *)obj->self;
|
|
|
|
|
if ( state->layer != NULL ) {
|
|
|
|
|
SDL_DestroyTexture(state->layer);
|
|
|
|
|
state->layer = NULL;
|
|
|
|
|
}
|
|
|
|
|
}
|