218 lines
7.9 KiB
C
218 lines
7.9 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());
|
||
|
|
renderer = &_akgl_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,
|
||
|
|
&window, &renderer->sdl_renderer),
|
||
|
|
AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError());
|
||
|
|
PASS(errctx, akgl_render_bind2d(renderer));
|
||
|
|
PASS(errctx, akgl_heap_init());
|
||
|
|
PASS(errctx, akgl_registry_init());
|
||
|
|
camera = &_akgl_camera;
|
||
|
|
camera->x = 0.0f;
|
||
|
|
camera->y = 0.0f;
|
||
|
|
camera->w = (float32_t)w;
|
||
|
|
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(renderer->sdl_renderer, 0, 0, 0, 0xff),
|
||
|
|
AKGL_ERR_SDL, "%s", SDL_GetError());
|
||
|
|
FAIL_ZERO_RETURN(errctx, SDL_RenderClear(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, renderer));
|
||
|
|
PASS(errctx, akbasic_sprite_init_akgl(&SPRITES, &SPRITESSTATE, 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(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 ( window != NULL ) {
|
||
|
|
SDL_DestroyWindow(window);
|
||
|
|
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;
|
||
|
|
}
|