165 lines
5.4 KiB
C
165 lines
5.4 KiB
C
|
|
/**
|
||
|
|
* @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 <stdio.h>
|
||
|
|
#include <stdlib.h>
|
||
|
|
#include <string.h>
|
||
|
|
|
||
|
|
#include <SDL3/SDL.h>
|
||
|
|
#include <SDL3_image/SDL_image.h>
|
||
|
|
|
||
|
|
#include <akerror.h>
|
||
|
|
|
||
|
|
#include <docs_prelude.h>
|
||
|
|
|
||
|
|
/** @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 <output.png> [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;
|
||
|
|
}
|