From 0972472cfa9fd5468e1f988ea849621da1cf0e80 Mon Sep 17 00:00:00 2001 From: Andrew Kesterson Date: Sun, 2 Aug 2026 08:38:18 -0400 Subject: [PATCH] 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) Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71 --- CMakeLists.txt | 29 ++++++++++++++++ docs/20-tutorial-sidescroller.md | 3 +- docs/21-tutorial-jrpg.md | 1 - docs/images/jrpg.png | Bin 0 -> 8915 bytes docs/images/sidescroller.png | Bin 0 -> 11319 bytes examples/jrpg/jrpg.c | 55 ++++++++++++++++++++++++++++++- examples/sidescroller/main.c | 47 ++++++++++++++++++++++++++ 7 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 docs/images/jrpg.png create mode 100644 docs/images/sidescroller.png diff --git a/CMakeLists.txt b/CMakeLists.txt index b20778f..a1ecffa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -807,6 +807,35 @@ add_custom_target(${AKGL_DOCS_SCREENSHOTS_TARGET} # examples/CMakeLists.txt, one game at a time, for the same reason. if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/examples/CMakeLists.txt") add_subdirectory(examples) + + # The figures at the top of chapters 20 and 21 are frames out of the games + # themselves, taken by --screenshot between the world being drawn and the + # frame being presented. Same contract as docs_screenshots above: deliberate, + # never part of a build, because the PNGs are tracked. + # + # There is no --check counterpart. docs_screenshots can compare because its + # figures are one deterministic frame of drawing calls; a game frame is not. + # The sidescroller drives its physics from the wall clock, so which pixel the + # player occupies depends on how fast the machine ran, and a byte comparison + # would fail for reasons that have nothing to do with the documentation. + add_custom_target(docs_game_figures + COMMAND ${CMAKE_COMMAND} -E env + SDL_VIDEODRIVER=dummy SDL_RENDER_DRIVER=software SDL_AUDIODRIVER=dummy + $ + --assets "${CMAKE_CURRENT_SOURCE_DIR}/docs/tutorials/assets/sidescroller" + --autoplay --frames 110 + --screenshot "${CMAKE_CURRENT_SOURCE_DIR}/docs/images/sidescroller.png" + --screenshot-frame 100 + COMMAND ${CMAKE_COMMAND} -E env + SDL_VIDEODRIVER=dummy SDL_RENDER_DRIVER=software SDL_AUDIODRIVER=dummy + $ + --demo --frames 240 + --screenshot "${CMAKE_CURRENT_SOURCE_DIR}/docs/images/jrpg.png" + --screenshot-frame 230 + DEPENDS sidescroller jrpg + COMMENT "Regenerating the tutorial figures in docs/images" + VERBATIM + ) endif() # Mutation testing copies the repository to scratch space, applies one small diff --git a/docs/20-tutorial-sidescroller.md b/docs/20-tutorial-sidescroller.md index c0d804f..cb7917c 100644 --- a/docs/20-tutorial-sidescroller.md +++ b/docs/20-tutorial-sidescroller.md @@ -582,8 +582,7 @@ that has to happen inside the physics step has only the one hook. ``` ```c excerpt=examples/sidescroller/main.c - PASS(errctx, akgl_game_update(NULL)); - PASS(errctx, akgl_renderer->frame_end(akgl_renderer)); + PASS(errctx, akgl_renderer->frame_start(akgl_renderer)); ``` `akgl_game_update` is update-every-actor, step-the-physics, draw-the-world. It does **not** diff --git a/docs/21-tutorial-jrpg.md b/docs/21-tutorial-jrpg.md index fc657c7..b1bd055 100644 --- a/docs/21-tutorial-jrpg.md +++ b/docs/21-tutorial-jrpg.md @@ -547,7 +547,6 @@ zero gravity, which is what the property defaults already give you. PASS(errctx, akgl_renderer->frame_start(akgl_renderer)); PASS(errctx, akgl_game_update(&opflags)); PASS(errctx, jrpg_textbox_draw()); - PASS(errctx, akgl_renderer->frame_end(akgl_renderer)); ``` `akgl_game_update` is update-every-actor, step the physics, draw the world. It diff --git a/docs/images/jrpg.png b/docs/images/jrpg.png new file mode 100644 index 0000000000000000000000000000000000000000..d460d2051cd6b090efbb547d4964c66f1252e981 GIT binary patch literal 8915 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 literal 0 HcmV?d00001 diff --git a/examples/jrpg/jrpg.c b/examples/jrpg/jrpg.c index e6015a0..96154dd 100644 --- a/examples/jrpg/jrpg.c +++ b/examples/jrpg/jrpg.c @@ -20,6 +20,7 @@ #include #include +#include #include #include #include @@ -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); diff --git a/examples/sidescroller/main.c b/examples/sidescroller/main.c index 809f265..e60a8fe 100644 --- a/examples/sidescroller/main.c +++ b/examples/sidescroller/main.c @@ -17,6 +17,7 @@ #include #include +#include #include #include @@ -46,6 +47,40 @@ ss_Game ss_game; akgl_CollisionWorld ss_collision; +/** @brief Where `--screenshot` writes, and on which frame. NULL means never. */ +static char *ss_shotpath = NULL; +static int ss_shotframe = 0; + +/** + * @brief Read the render target back and write it out as a PNG. + * + * Called between the world being drawn and the frame being presented, because + * SDL_RenderPresent is where the target stops being readable. This is how the + * figure in chapter 20 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); +} + akerr_ErrorContext *ss_grounded(akgl_CollisionShape *shape, float32_t x, float32_t y, bool *dest) { SDL_FRect feet; @@ -354,6 +389,9 @@ static akerr_ErrorContext *frame(bool *running) * Treat it as terminal, which is what PASS does here. */ PASS(errctx, akgl_game_update(NULL)); + if ( (ss_shotpath != NULL) && (ss_game.frame == ss_shotframe) ) { + PASS(errctx, save_screenshot(ss_shotpath)); + } PASS(errctx, akgl_renderer->frame_end(akgl_renderer)); SUCCEED_RETURN(errctx); } @@ -441,11 +479,20 @@ static akerr_ErrorContext *parse_args(int argc, char *argv[], char **assetdir, i i += 1; FAIL_NONZERO_RETURN(errctx, (i >= argc), AKERR_VALUE, "--assets needs a directory"); *assetdir = argv[i]; + } else if ( strcmp(argv[i], "--screenshot") == 0 ) { + i += 1; + FAIL_NONZERO_RETURN(errctx, (i >= argc), AKERR_VALUE, "--screenshot needs a path"); + ss_shotpath = argv[i]; + } else if ( strcmp(argv[i], "--screenshot-frame") == 0 ) { + i += 1; + FAIL_NONZERO_RETURN(errctx, (i >= argc), AKERR_VALUE, "--screenshot-frame needs a number"); + PASS(errctx, aksl_atoi(argv[i], &ss_shotframe)); } else { FAIL_RETURN( errctx, AKERR_VALUE, "usage: sidescroller [--assets DIR] [--frames N] [--autoplay]" + " [--screenshot PATH] [--screenshot-frame N]" ); } }