/** * @file docs_screenshot.c * @brief Bring libakgl up against an offscreen target, call one documented frame, save a PNG. * * The drawing, sprite and tilemap chapters describe what a call puts on the * screen, 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, compiles it against this file, and * runs the result -- so the figure is generated from the exact code the reader * is looking at rather than from a copy that can drift away from it. * * **It is a host**, and deliberately a small one: `akgl_game_init()` opens a * real window, owns the frame loop and does not stop until the game 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, one frame, read the * target back, write it out. `tests/sprite.c` and `tests/draw.c` already set * that pattern. * * The frame itself is `docs_frame()`, supplied by the chapter's block through * `tests/docs_preludes/akglframe.pre`. It is declared in `docs_prelude.h` so the * host and the prelude cannot disagree about the signature. */ #include #include #include #include #include #include #include /** @brief Set in HANDLE_DEFAULT and read after FINISH; see the note in main(). */ static int failed = 0; static void usage(void) { fprintf(stderr, "usage: akgl_docs_screenshot [width] [height]\n" "\n" " Calls docs_frame() against an offscreen renderer of the given size\n" " (default 320x240) and writes what it drew as a PNG.\n"); } /** * @brief Everything between SDL being up and the pixels being on disk. * * 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_frame(const char *outpath, int w, int h) { PREPARE_ERROR(errctx); SDL_Surface *shot = NULL; FAIL_ZERO_RETURN(errctx, outpath, AKERR_NULLPOINTER, "Received null output path"); 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/libakgl/docs_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. Clearing is the host's prerogative -- the * library's draw calls put things on the target and do not own what is * underneath them -- 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()); PASS(errctx, docs_frame()); 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 = 240; if ( argc < 2 ) { usage(); return 2; } if ( argc > 2 ) { w = atoi(argv[2]); } if ( argc > 3 ) { h = atoi(argv[3]); } 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 binary diff. */ SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy"); SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software"); SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy"); ATTEMPT { CATCH(errctx, draw_frame(argv[1], w, h)); } CLEANUP { 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 figure"); /* * A flag rather than a `return`: FINISH ends with RELEASE_ERROR, and * leaving a HANDLE block early means the context never goes back to * AKERR_ARRAY_ERROR -- one leaked slot per call, and AGENTS.md documents * where that ends up. */ failed = 1; /* * FINISH_NORETURN rather than FINISH: FINISH expands a * `return __err_context` that an int-returning function cannot compile, * even where the branch is unreachable. */ } FINISH_NORETURN(errctx); return failed; }