/** * @file renderer.c * @brief Unit tests for the 2D render backend's vtable and its entry points. * * None of this needs a display. akgl_render_2d_bind() installs the backend's * methods and nothing else, so it can be checked against a backend that has no * SDL_Renderer at all; the entry points that do draw are checked against a * software renderer under the dummy video driver, the same way tests/draw.c * does it. * * akgl_render_2d_init() is not covered here: it creates a window from the * property registry and writes the `camera` global, which is what the offscreen * harness described in TODO.md exists to make testable. */ #include #include #include #include #include #include #include "testutil.h" /** @brief Width and height of the offscreen target the drawing tests use. */ #define TEST_TARGET_SIZE 32 /** @brief A backend bound to a live software renderer, built by main(). */ static akgl_RenderBackend bound; akerr_ErrorContext *test_render_bind2d(void) { PREPARE_ERROR(errctx); akgl_RenderBackend backend; ATTEMPT { // The state a host is in when it owns its own window: a zeroed backend // with somebody else's SDL_Renderer in it. Binding must fill in the six // methods and leave that renderer alone -- creating a second window is // precisely what an embedded interpreter must not do. memset(&backend, 0x00, sizeof(akgl_RenderBackend)); backend.sdl_renderer = (SDL_Renderer *)&bound; TEST_EXPECT_OK(errctx, akgl_render_2d_bind(&backend), "binding the 2D backend"); TEST_ASSERT(errctx, backend.sdl_renderer == (SDL_Renderer *)&bound, "binding replaced the caller's SDL_Renderer"); TEST_ASSERT(errctx, backend.shutdown == &akgl_render_2d_shutdown, "binding did not install shutdown"); TEST_ASSERT(errctx, backend.frame_start == &akgl_render_2d_frame_start, "binding did not install frame_start"); TEST_ASSERT(errctx, backend.frame_end == &akgl_render_2d_frame_end, "binding did not install frame_end"); TEST_ASSERT(errctx, backend.draw_texture == &akgl_render_2d_draw_texture, "binding did not install draw_texture"); TEST_ASSERT(errctx, backend.draw_mesh == &akgl_render_2d_draw_mesh, "binding did not install draw_mesh"); TEST_ASSERT(errctx, backend.draw_world == &akgl_render_2d_draw_world, "binding did not install draw_world"); // Binding a backend that has no renderer yet is legal -- the two halves // are separable in both directions -- and leaves it NULL rather than // inventing one. memset(&backend, 0x00, sizeof(akgl_RenderBackend)); TEST_EXPECT_OK(errctx, akgl_render_2d_bind(&backend), "binding a backend with no SDL renderer"); TEST_ASSERT(errctx, backend.sdl_renderer == NULL, "binding invented an SDL_Renderer"); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_render_2d_bind(NULL), "binding a NULL backend"); } CLEANUP { } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); } akerr_ErrorContext *test_render_backend_without_a_renderer(void) { PREPARE_ERROR(errctx); akgl_RenderBackend empty; ATTEMPT { // Bound but never given an SDL_Renderer. Every entry point that draws // has to report that rather than dereference it. memset(&empty, 0x00, sizeof(akgl_RenderBackend)); CATCH(errctx, akgl_render_2d_bind(&empty)); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, empty.frame_start(&empty), "starting a frame on a backend with no renderer"); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, empty.frame_end(&empty), "ending a frame on a backend with no renderer"); // Shutdown is the one that has nothing to release yet, so it succeeds. TEST_EXPECT_OK(errctx, empty.shutdown(&empty), "shutting down an unused backend"); } CLEANUP { } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); } akerr_ErrorContext *test_render_frames_and_textures(void) { PREPARE_ERROR(errctx); SDL_Surface *surface = NULL; SDL_Texture *texture = NULL; SDL_FRect dest; SDL_FPoint center; ATTEMPT { dest.x = 0.0f; dest.y = 0.0f; dest.w = 8.0f; dest.h = 8.0f; center.x = 4.0f; center.y = 4.0f; surface = SDL_CreateSurface(8, 8, SDL_PIXELFORMAT_RGBA32); FAIL_ZERO_BREAK(errctx, surface, AKGL_ERR_SDL, "%s", SDL_GetError()); texture = SDL_CreateTextureFromSurface(bound.sdl_renderer, surface); FAIL_ZERO_BREAK(errctx, texture, AKGL_ERR_SDL, "%s", SDL_GetError()); TEST_EXPECT_OK(errctx, bound.frame_start(&bound), "starting a frame"); TEST_EXPECT_OK(errctx, bound.draw_texture(&bound, texture, NULL, &dest, 0, NULL, SDL_FLIP_NONE), "blitting a texture unrotated"); // A non-zero angle takes the rotated path, which is the one that // consults the pivot and the flip mode. TEST_EXPECT_OK(errctx, bound.draw_texture(&bound, texture, NULL, &dest, 90.0, ¢er, SDL_FLIP_HORIZONTAL), "blitting a texture rotated"); TEST_EXPECT_OK(errctx, bound.frame_end(&bound), "ending a frame"); // A rotation with no pivot is refused: SDL's "NULL means the centre of // dest" convention is not offered here, so a NULL is a caller mistake // rather than a default. TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, bound.draw_texture(&bound, texture, NULL, &dest, 90.0, NULL, SDL_FLIP_NONE), "blitting rotated with no pivot"); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_render_2d_draw_texture(NULL, texture, NULL, &dest, 0, NULL, SDL_FLIP_NONE), "blitting through a NULL backend"); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, bound.draw_texture(&bound, NULL, NULL, &dest, 0, NULL, SDL_FLIP_NONE), "blitting a NULL texture"); } CLEANUP { if ( texture != NULL ) { SDL_DestroyTexture(texture); } if ( surface != NULL ) { SDL_DestroySurface(surface); } } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); } akerr_ErrorContext *test_render_unimplemented_and_guards(void) { PREPARE_ERROR(errctx); ATTEMPT { // The 3D hook is not a stub that quietly does nothing. It refuses, so a // caller that reaches it finds out on the first frame. TEST_EXPECT_STATUS(errctx, AKERR_API, bound.draw_mesh(&bound), "drawing a mesh through the 2D backend"); TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_render_2d_draw_world(NULL, NULL), "drawing the world through a NULL backend"); // Shutting the 2D backend down releases nothing today, and says so. TEST_EXPECT_OK(errctx, bound.shutdown(&bound), "shutting the backend down"); } CLEANUP { } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); } int main(void) { PREPARE_ERROR(errctx); SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy"); SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy"); SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software"); ATTEMPT { CATCH(errctx, akgl_error_init()); TEST_TRAP_UNHANDLED_ERRORS(); memset(&bound, 0x00, sizeof(akgl_RenderBackend)); FAIL_ZERO_BREAK( errctx, SDL_Init(SDL_INIT_VIDEO), AKGL_ERR_SDL, "Couldn't initialize SDL: %s", SDL_GetError()); FAIL_ZERO_BREAK( errctx, SDL_CreateWindowAndRenderer( "net/aklabs/libakgl/test_renderer", TEST_TARGET_SIZE, TEST_TARGET_SIZE, 0, &akgl_window, &bound.sdl_renderer), AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError()); // The host owns the window; libakgl only ever binds to it. This is the // arrangement akgl_render_2d_bind exists for. CATCH(errctx, akgl_render_2d_bind(&bound)); CATCH(errctx, test_render_bind2d()); CATCH(errctx, test_render_backend_without_a_renderer()); CATCH(errctx, test_render_frames_and_textures()); CATCH(errctx, test_render_unimplemented_and_guards()); } CLEANUP { SDL_Quit(); } PROCESS(errctx) { } FINISH_NORETURN(errctx); }