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:
@@ -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
|
||||
$<TARGET_FILE:sidescroller>
|
||||
--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
|
||||
$<TARGET_FILE:jrpg>
|
||||
--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
|
||||
|
||||
@@ -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**
|
||||
|
||||
@@ -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
|
||||
|
||||
BIN
docs/images/jrpg.png
Normal file
BIN
docs/images/jrpg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.7 KiB |
BIN
docs/images/sidescroller.png
Normal file
BIN
docs/images/sidescroller.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
@@ -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);
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include <string.h>
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3_image/SDL_image.h>
|
||||
#include <SDL3_mixer/SDL_mixer.h>
|
||||
#include <SDL3_ttf/SDL_ttf.h>
|
||||
|
||||
@@ -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]"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user