Some checks failed
akbasic CI Build / cmake_build (push) Failing after 3m22s
akbasic CI Build / sanitizers (push) Failing after 4m43s
akbasic CI Build / coverage (push) Failing after 3m43s
akbasic CI Build / akgl_build (push) Failing after 4m48s
akbasic CI Build / mutation_test (push) Failing after 3m31s
Co-authored-by: Andrew Kesterson <andrew@aklabs.net>
298 lines
12 KiB
C
298 lines
12 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 not drawn unless a font is named.** A screenshot in the
|
|
* graphics chapter is of the drawing, and a `READY` in the corner is noise in a
|
|
* figure about `CIRCLE`; leaving the layer out also means no font has to be
|
|
* found, which keeps this runnable from a build tree with no assets resolved.
|
|
*
|
|
* A figure of a program whose *output is text* needs the opposite, and
|
|
* docs/17-tutorial-breakout.md is one: that game's wall, HUD and messages are
|
|
* characters in the grid, so a picture without the text layer is a picture of
|
|
* two sprites on a black field. Naming a font as the fifth argument swaps the
|
|
* stdio sink for the akgl one and renders the grid before the sprites, in the
|
|
* same order src/frontend_akgl.c uses. The fence attribute that asks for it is
|
|
* `text=1`; tools/docs_screenshots.sh turns that into this argument.
|
|
*
|
|
* The sink is the akgl one *alone* rather than a tee, so a program's output
|
|
* lands in the picture rather than on stdout -- which is what the caller reads
|
|
* to decide a figure failed. Nothing is lost: `screenshot=` blocks are also run
|
|
* by tests/docs_examples.sh, which is where a raised error is caught.
|
|
*/
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include <SDL3/SDL.h>
|
|
#include <SDL3_image/SDL_image.h>
|
|
#include <SDL3_ttf/SDL_ttf.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/frontend.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;
|
|
/** @brief The text grid, and the font behind it. Only used when one is named. */
|
|
static akbasic_AkglSink GRIDSTATE;
|
|
static TTF_Font *FONT = NULL;
|
|
static akbasic_GraphicsBackend GRAPHICS;
|
|
static akbasic_AkglGraphics GRAPHICSSTATE;
|
|
static akbasic_SpriteBackend SPRITES;
|
|
static akbasic_AkglSprites SPRITESSTATE;
|
|
static akbasic_UiBackend UI;
|
|
static akbasic_AkglUi UISTATE;
|
|
/*
|
|
* 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] [font.ttf] [grid]\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"
|
|
"\n"
|
|
" Naming a font lends the program a UI device, because the widgets\n"
|
|
" draw text and cannot come up without one. Passing 1 for `grid` as\n"
|
|
" well draws the text layer, for a figure of a program whose output\n"
|
|
" is characters rather than drawing.\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 |