Files
akbasic/tools/screenshot.c
Andrew Kesterson 3415c9aec6 Take libakgl 0.5.0 and rename every symbol it namespaced
0.5.0 gives every exported symbol the akgl_ prefix, which is the first libakgl
release to break this project's source rather than only its ABI. The soname goes
to libakgl.so.0.5 and include/akbasic/akgl.h asserts the floor.

What moved here: akgl_render_bind2d is akgl_render_2d_bind,
akgl_sprite_sheet_coords_for_frame is akgl_spritesheet_coords_for_frame, the
renderer, camera and window globals carry the prefix, and _akgl_renderer and
_akgl_camera are akgl_default_renderer and akgl_default_camera.

The renames were applied by site rather than by pattern, because renderer is also
a parameter name in src/sprite_akgl.c and a struct member throughout
src/frontend_akgl.c -- a substitution would have rewritten both without a word.
That is the same trap upstream describes hitting, and it is worth knowing that
the defect behind the rename was not cosmetic: an exported global called renderer
collided with a test's own variable, the executable's definition preempted the
library's, and every texture load in that suite failed while the suite passed.

0.5.0 also fixes libakgl defect 26, which was one of the two reasons
src/sprite_akgl.c installs its own renderfunc: akgl_actor_render took its
destination height from the sprite's width, drawing a 24x21 Commodore sprite as a
24x24 square. The renderfunc stays, because the other reason has not moved -- an
actor carries one scalar scale and SPRITE has separate x- and y-expand bits --
but the comments and TODO.md deviation 40 no longer claim a defect that is fixed.

Every documentation figure re-renders byte-identical under the new library, which
is what says the sprite and drawing paths did not move underneath them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 08:01:43 -04:00

218 lines
8.0 KiB
C

/**
* @file screenshot.c
* @brief Run one BASIC program against an offscreen renderer and save a PNG.
*
* The documentation's graphics and sprite chapters describe what a verb draws,
* and a sentence is a poor way to describe a picture. This is what turns the
* listing in a chapter into the image beside it -- tools/docs_screenshots.sh
* extracts the listing from the markdown and hands it here, so the picture is
* generated from the exact program the reader is looking at rather than from a
* copy that can drift away from it.
*
* **It is a host, like src/frontend_akgl.c**, and it is deliberately a separate
* one: the frontend opens a real window, pumps events and never stops until the
* script quits, none of which a build step wants. What this does instead is the
* shortest path to pixels -- the dummy video driver, a software renderer, run to
* completion, read the target back, write it out. deps/libakgl/tests/draw.c and
* tests/akgl_backends.c set that pattern; this is the third user of it.
*
* **The text layer is deliberately not drawn.** A screenshot in the graphics
* chapter is of the drawing, and a `READY` in the corner is noise in a figure
* about `CIRCLE`. It also means no font has to be found, which keeps this
* runnable from a build tree with no assets resolved.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
#include <akerror.h>
#include <akgl/error.h>
/*
* game.h for the `renderer` and `camera` globals: akgl_actor_render() reads
* both without taking either as an argument, so a host that never calls
* akgl_game_init() -- which is every host here, because owning the game loop is
* what goal 3 forbids -- populates them itself. Same three lines as
* src/frontend_akgl.c and tests/akgl_backends.c, and each says so.
*/
#include <akgl/game.h>
#include <akgl/heap.h>
#include <akgl/registry.h>
#include <akgl/renderer.h>
#include <akbasic/akgl.h>
#include <akbasic/error.h>
#include <akbasic/runtime.h>
#include <akbasic/sink.h>
/** @brief The interpreter carries every pool it owns, so it does not fit on a stack. */
static akbasic_Runtime RUNTIME;
static akbasic_TextSink SINK;
static akbasic_StdioSink SINKSTATE;
static akbasic_GraphicsBackend GRAPHICS;
static akbasic_AkglGraphics GRAPHICSSTATE;
static akbasic_SpriteBackend SPRITES;
static akbasic_AkglSprites SPRITESSTATE;
/*
* No `window` of our own: akgl/game.h declares one as an unprefixed extern --
* libakgl's TODO calls that a defect and it is -- so a static here shadows it
* and the file will not compile. Writing the global is what a host does anyway.
*/
/** @brief The whole program, read in one go. Longer than any figure's listing. */
static char SOURCE[65536];
static void usage(void)
{
fprintf(stderr,
"usage: akbasic_screenshot <program.bas> <output.png> [width] [height]\n"
"\n"
" Runs the program against an offscreen renderer of the given size\n"
" (default 320x200) and writes what it drew as a PNG.\n");
}
/** @brief Slurp the program. A figure's listing is small; a partial read is not tolerated. */
static akerr_ErrorContext AKERR_NOIGNORE *read_program(const char *path)
{
PREPARE_ERROR(errctx);
FILE *in = NULL;
size_t got = 0;
memset(SOURCE, 0, sizeof(SOURCE));
in = fopen(path, "r");
FAIL_ZERO_RETURN(errctx, (in != NULL), AKERR_IO, "could not open %s", path);
got = fread(SOURCE, 1, sizeof(SOURCE) - 1, in);
fclose(in);
FAIL_ZERO_RETURN(errctx, (got > 0), AKERR_IO, "%s is empty", path);
FAIL_ZERO_RETURN(errctx, (got < sizeof(SOURCE) - 1), AKERR_OUTOFBOUNDS,
"%s is longer than %zu bytes", path, sizeof(SOURCE) - 1);
SUCCEED_RETURN(errctx);
}
/**
* @brief Everything between SDL being up and the pixels being readable.
*
* Split out so the ATTEMPT in main() has one thing to CATCH and the SDL
* teardown has one place to happen.
*/
static akerr_ErrorContext AKERR_NOIGNORE *draw_program(const char *outpath, int w, int h)
{
PREPARE_ERROR(errctx);
SDL_Surface *shot = NULL;
PASS(errctx, akgl_error_init());
akgl_renderer = &akgl_default_renderer;
FAIL_ZERO_RETURN(errctx, SDL_Init(SDL_INIT_VIDEO), AKGL_ERR_SDL,
"Couldn't initialize SDL: %s", SDL_GetError());
FAIL_ZERO_RETURN(errctx,
SDL_CreateWindowAndRenderer("net/aklabs/akbasic/screenshot", w, h, 0,
&akgl_window, &akgl_renderer->sdl_renderer),
AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError());
PASS(errctx, akgl_render_2d_bind(akgl_renderer));
PASS(errctx, akgl_heap_init());
PASS(errctx, akgl_registry_init());
akgl_camera = &akgl_default_camera;
akgl_camera->x = 0.0f;
akgl_camera->y = 0.0f;
akgl_camera->w = (float32_t)w;
akgl_camera->h = (float32_t)h;
/*
* Opaque black, so a figure has a defined background rather than whatever
* the driver left in the buffer. GRAPHIC's own clear is a filled rectangle
* over the output for a reason src/graphics_akgl.c explains -- clearing is
* the host's prerogative -- and this is the host doing it.
*/
FAIL_ZERO_RETURN(errctx, SDL_SetRenderDrawColor(akgl_renderer->sdl_renderer, 0, 0, 0, 0xff),
AKGL_ERR_SDL, "%s", SDL_GetError());
FAIL_ZERO_RETURN(errctx, SDL_RenderClear(akgl_renderer->sdl_renderer),
AKGL_ERR_SDL, "%s", SDL_GetError());
/* stdout: a program that errors puts its "? line : CLASS message" there,
which is what the caller shows when a figure comes out wrong. */
PASS(errctx, akbasic_sink_init_stdio(&SINK, &SINKSTATE, stdout, NULL));
PASS(errctx, akbasic_runtime_init(&RUNTIME, &SINK));
PASS(errctx, akbasic_graphics_init_akgl(&GRAPHICS, &GRAPHICSSTATE, akgl_renderer));
PASS(errctx, akbasic_sprite_init_akgl(&SPRITES, &SPRITESSTATE, akgl_renderer, &GRAPHICSSTATE));
PASS(errctx, akbasic_runtime_set_devices(&RUNTIME, &GRAPHICS, NULL, NULL, &SPRITES));
PASS(errctx, akbasic_runtime_load(&RUNTIME, SOURCE));
PASS(errctx, akbasic_runtime_start(&RUNTIME, AKBASIC_MODE_RUN));
PASS(errctx, akbasic_runtime_run(&RUNTIME, 0));
/*
* The drawing verbs are immediate and are already on the target. Sprites are
* not: they are libakgl actors and reach the target through a render pass,
* which in the standalone frontend is part of a frame. There is no frame
* here, so this is it.
*/
PASS(errctx, akbasic_sprite_akgl_render(&SPRITES));
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
FAIL_ZERO_RETURN(errctx, (shot != NULL), AKGL_ERR_SDL,
"Couldn't read the target back: %s", SDL_GetError());
if ( !IMG_SavePNG(shot, outpath) ) {
SDL_DestroySurface(shot);
FAIL_RETURN(errctx, AKGL_ERR_SDL, "Couldn't write %s: %s", outpath, SDL_GetError());
}
SDL_DestroySurface(shot);
SUCCEED_RETURN(errctx);
}
int main(int argc, char **argv)
{
PREPARE_ERROR(errctx);
int w = 320;
int h = 200;
if ( argc < 3 ) {
usage();
return 2;
}
if ( argc > 3 ) {
w = atoi(argv[3]);
}
if ( argc > 4 ) {
h = atoi(argv[4]);
}
if ( w <= 0 || h <= 0 ) {
usage();
return 2;
}
/*
* Set here rather than left to the environment so the tool answers the same
* on a developer's desktop as it does in CI. A figure regenerated over a
* real GPU could differ by a pixel of antialiasing and turn every rebuild
* into a diff.
*/
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software");
ATTEMPT {
CATCH(errctx, read_program(argv[1]));
CATCH(errctx, draw_program(argv[2], w, h));
} CLEANUP {
if ( akgl_window != NULL ) {
SDL_DestroyWindow(akgl_window);
akgl_window = NULL;
}
SDL_Quit();
} PROCESS(errctx) {
} HANDLE_DEFAULT(errctx) {
LOG_ERROR_WITH_MESSAGE(errctx, "could not draw the screenshot");
return 1;
/*
* FINISH_NORETURN rather than FINISH, matching src/main.c and
* tests/akgl_backends.c: FINISH expands a `return __err_context` that
* this int-returning function cannot compile even where the branch is
* unreachable.
*/
} FINISH_NORETURN(errctx);
return 0;
}