Take the tutorial figures out of the games themselves
Both examples gain --screenshot PATH and --screenshot-frame N. The capture sits between the world being drawn and the frame being presented, because SDL_RenderPresent is where the target stops being readable, and it works under the dummy video driver and the software renderer like the rest of the headless path does. `cmake --build build --target docs_game_figures` regenerates docs/images/sidescroller.png and docs/images/jrpg.png by running each game to a chosen frame. Same contract as docs_screenshots: deliberate, never part of a build, because the PNGs are tracked. There is no --check counterpart, and the target says why -- the sidescroller drives its physics from the wall clock, so a byte comparison would fail for reasons that have nothing to do with the documentation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
This commit is contained in:
@@ -20,6 +20,7 @@
|
||||
#include <string.h>
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3_image/SDL_image.h>
|
||||
#include <SDL3_ttf/SDL_ttf.h>
|
||||
#include <akerror.h>
|
||||
#include <akstdlib.h>
|
||||
@@ -68,6 +69,10 @@ static const jrpg_ScriptStep JRPG_DEMO_SCRIPT[] = {
|
||||
#define JRPG_DEMO_STEPS (sizeof(JRPG_DEMO_SCRIPT) / sizeof(JRPG_DEMO_SCRIPT[0]))
|
||||
|
||||
static long frame_limit = 0;
|
||||
|
||||
/** @brief Where `--screenshot` writes, and on which frame. NULL means never. */
|
||||
static char *shotpath = NULL;
|
||||
static long shotframe = 0;
|
||||
static bool demo = false;
|
||||
static bool running = true;
|
||||
static int exitstatus = 0;
|
||||
@@ -116,8 +121,23 @@ static akerr_ErrorContext *parse_args(int argc, char *argv[])
|
||||
i += 1;
|
||||
PASS(errctx, aksl_atoi(argv[i], &frames));
|
||||
frame_limit = frames;
|
||||
} else if ( strcmp(argv[i], "--screenshot") == 0 ) {
|
||||
if ( (i + 1) >= argc ) {
|
||||
FAIL_RETURN(errctx, AKERR_VALUE, "--screenshot needs a path");
|
||||
}
|
||||
i += 1;
|
||||
shotpath = argv[i];
|
||||
} else if ( strcmp(argv[i], "--screenshot-frame") == 0 ) {
|
||||
if ( (i + 1) >= argc ) {
|
||||
FAIL_RETURN(errctx, AKERR_VALUE, "--screenshot-frame needs a number");
|
||||
}
|
||||
i += 1;
|
||||
PASS(errctx, aksl_atoi(argv[i], &frames));
|
||||
shotframe = frames;
|
||||
} else {
|
||||
FAIL_RETURN(errctx, AKERR_VALUE, "usage: jrpg [--frames N] [--demo]");
|
||||
FAIL_RETURN(errctx, AKERR_VALUE,
|
||||
"usage: jrpg [--frames N] [--demo]"
|
||||
" [--screenshot PATH] [--screenshot-frame N]");
|
||||
}
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
@@ -230,6 +250,36 @@ static akerr_ErrorContext *demo_step(long frameno)
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Read the render target back and write it out as a PNG.
|
||||
*
|
||||
* Called between the box being drawn and the frame being presented, because
|
||||
* SDL_RenderPresent is where the target stops being readable. This is how the
|
||||
* figure in chapter 21 is generated: it is output from this program rather than
|
||||
* a picture somebody took once, so it cannot show a version of the game that no
|
||||
* longer exists.
|
||||
*/
|
||||
static akerr_ErrorContext *save_screenshot(char *path)
|
||||
{
|
||||
SDL_Surface *shot = NULL;
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, path, AKERR_NULLPOINTER, "path");
|
||||
|
||||
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
|
||||
FAIL_ZERO_RETURN(errctx, shot, AKGL_ERR_SDL, "SDL_RenderReadPixels: %s", SDL_GetError());
|
||||
|
||||
ATTEMPT {
|
||||
FAIL_ZERO_BREAK(errctx, IMG_SavePNG(shot, path), AKGL_ERR_SDL,
|
||||
"IMG_SavePNG(%s): %s", path, SDL_GetError());
|
||||
} CLEANUP {
|
||||
SDL_DestroySurface(shot);
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SDL_Log("Wrote %s", path);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief One frame: events, camera, the library's own update, the box, present.
|
||||
*
|
||||
@@ -276,6 +326,9 @@ static akerr_ErrorContext *frame(long frameno)
|
||||
// on top of a frame that did not finish. Treat a failed frame as terminal.
|
||||
PASS(errctx, akgl_game_update(&opflags));
|
||||
PASS(errctx, jrpg_textbox_draw());
|
||||
if ( (shotpath != NULL) && (frameno == shotframe) ) {
|
||||
PASS(errctx, save_screenshot(shotpath));
|
||||
}
|
||||
PASS(errctx, akgl_renderer->frame_end(akgl_renderer));
|
||||
|
||||
SUCCEED_RETURN(errctx);
|
||||
|
||||
Reference in New Issue
Block a user