/** * @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 #include #include #include #include #include #include #include /* * 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 #include #include #include #include #include #include #include #include /** @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; /* * 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 [width] [height] [font.ttf]\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 draws the text grid as well, for a figure of a\n" " program whose output 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(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, const char *fontpath) { 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()); /* * With no font: stdout, so a program that errors puts its * "? line : CLASS message" where the caller reads it. With one: the grid, * so the characters the program writes are in the picture. The file comment * explains why the two are not teed together. */ if ( fontpath == NULL ) { PASS(errctx, akbasic_sink_init_stdio(&SINK, &SINKSTATE, stdout, NULL)); } else { FAIL_ZERO_RETURN(errctx, TTF_Init(), AKGL_ERR_SDL, "Couldn't initialize SDL_ttf: %s", SDL_GetError()); FONT = TTF_OpenFont(fontpath, (float)AKBASIC_FRONTEND_FONT_SIZE); FAIL_ZERO_RETURN(errctx, (FONT != NULL), AKGL_ERR_SDL, "Couldn't open the font %s: %s", fontpath, SDL_GetError()); PASS(errctx, akbasic_sink_init_akgl(&SINK, &GRIDSTATE, akgl_renderer, FONT, w, h)); } 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. The text * grid and the sprites are not: both are redrawn from state the interpreter * keeps, which in the standalone frontend happens once a frame. There is no * frame here, so this is it -- and the order is the frontend's, grid first * and sprites over it, because that ordering is what a program written * against the real host will have assumed. */ if ( fontpath != NULL ) { PASS(errctx, akbasic_sink_akgl_render(&SINK)); } 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; const char *fontpath = NULL; if ( argc < 3 ) { usage(); return 2; } if ( argc > 3 ) { w = atoi(argv[3]); } if ( argc > 4 ) { h = atoi(argv[4]); } if ( argc > 5 && argv[5][0] != '\0' ) { fontpath = argv[5]; } 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, fontpath)); } CLEANUP { if ( FONT != NULL ) { TTF_CloseFont(FONT); FONT = NULL; TTF_Quit(); } 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; }