/** * @file util.h * @brief Axis-aligned collision tests, path resolution, and two test-only image helpers. * * The grab bag. Three unrelated groups live here: rectangle/point overlap for * the physics backend, path resolution for the asset loaders, and a pair of * pixel-comparison routines that exist only so tests can assert on what was * actually drawn. * * All the geometry here is axis-aligned and treats edges as touching: a point * exactly on a boundary is inside. There is no rotation support and no * separating-axis test. */ #ifndef _AKGL_UTIL_H_ #define _AKGL_UTIL_H_ #include #include #include #include /** @brief An integer point. Carries a `z` the collision routines do not use. */ typedef struct akgl_Point { int x; /**< Horizontal position, in whatever space the caller is working in. */ int y; /**< Vertical position. */ int z; /**< Depth. Never written by akgl_rectangle_points and never read by the collision tests. */ } akgl_Point; /** * @brief The four corners of an axis-aligned rectangle, precomputed. * * akgl_collide_rectangles works corner by corner rather than by comparing edge * spans, so it wants the corners as points. akgl_rectangle_points derives one of * these from an `SDL_FRect`. */ typedef struct akgl_RectanglePoints { akgl_Point topleft; /**< (x, y). */ akgl_Point topright; /**< (x + w, y). */ akgl_Point bottomleft; /**< (x, y + h). */ akgl_Point bottomright; /**< (x + w, y + h). */ } akgl_RectanglePoints; /** * @brief Expand a rectangle into its four corner points. * * Coordinates are truncated from `float` to `int` on the way in, so a rectangle * at x = 10.9 has its corners at 10. That is deliberate for tile-grid work and * wrong for sub-pixel work; callers needing the latter should not round-trip * through this. * * @param dest Receives the corners. Required. * @param rect The rectangle, in any coordinate space. Required. `w` and `h` are * taken as extents from `x`/`y`, so a negative one produces a * rectangle whose "bottom right" is above and left of its "top * left" -- which every test here then reports as empty. * @return `NULL` on success, otherwise an error context owned by the caller. * @throws AKERR_NULLPOINTER If @p dest or @p rect is `NULL`. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_rectangle_points(akgl_RectanglePoints *dest, SDL_FRect *rect); /** * @brief Test whether a point falls inside a rectangle, edges included. * * Compares against `topleft` and `bottomright` only, so it assumes @p r is * well-formed -- the two corners actually being the minimum and maximum. `z` is * ignored on both sides: this is a 2D test. * * @param p The point to test. Required. * @param r The rectangle, as corners from akgl_rectangle_points. Required. * @param collide Receives `true` when the point is inside or exactly on an edge, * `false` otherwise. Required -- the return value is the error * context. Not written on any failure path. * @return `NULL` on success, otherwise an error context owned by the caller. * @throws AKERR_NULLPOINTER If @p p, @p r, or @p collide is `NULL`. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_collide_point_rectangle(akgl_Point *p, akgl_RectanglePoints *r, bool *collide); /** * @brief Test whether two rectangles overlap, edges included. * * Tests all eight corners -- each rectangle's four against the other -- and * stops at the first hit. Checking both directions is what catches the case * where one rectangle is entirely inside the other and so has no corner within * its neighbour. * * @param r1 First rectangle. Required. * @param r2 Second rectangle. Required. Order does not matter. * @param collide Receives `true` on any overlap or shared edge, `false` * otherwise. Required. * @return `NULL` on success, otherwise an error context owned by the caller. * @throws AKERR_NULLPOINTER If @p r1, @p r2, or @p collide is `NULL`. * * @note A corner-containment test misses the one arrangement where two * rectangles overlap in a cross without either enclosing a corner of the * other -- a tall thin rectangle crossing a short wide one. Both are * reported as not colliding. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_collide_rectangles(SDL_FRect *r1, SDL_FRect *r2, bool *collide); /** * @brief Resolve an asset path, trying the working directory before the given root. * * Asset files name their neighbours relatively -- a sprite definition names its * spritesheet, a tilemap names its tilesets -- and "relative" has to mean * relative to the file doing the naming, not to wherever the game was launched * from. So this tries @p path against the process working directory first, and * only if that does not exist joins it onto @p root and resolves that. Either * way the result is absolute, with symlinks and `..` folded out. * * @param root Directory to fall back to, normally `dirname` of the file that * contained @p path. Required, even when unused. * @param path The path to resolve, relative or absolute. Required. * @param dest Receives the resolved absolute path. Required, and must already be * a claimed pool string -- this writes into it, it does not claim * one for you. * @return `NULL` on success, otherwise an error context owned by the caller. * @throws AKERR_NULLPOINTER If @p root, @p path, or @p dest is `NULL`. * @throws AKERR_OUTOFBOUNDS If `root + "/" + path` would not fit in * #AKGL_MAX_STRING_LENGTH. * @throws ENOENT If neither spelling names an existing file. Any other `errno` * `realpath(3)` can raise -- EACCES on an unsearchable directory, * ELOOP, ENOTDIR -- propagates the same way. * @throws AKGL_ERR_HEAP If the string pool is exhausted. * * @note The fallback path -- the common one, since most asset references are * relative to their own file rather than to the working directory -- * returns straight out of the ENOENT handler and so never releases the * error context it was handling. Each such call consumes one slot of * libakerror's fixed 128-entry context array for the life of the process. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_path_relative(char *root, char *path, akgl_String *dest); // These are REALLY slow routines that are only useful in testing harnesses /** * @brief Assert that two surfaces hold byte-identical pixels. * * A `memcmp` over the raw pixel buffer, so it is exact: one differing byte in * one pixel is a failure. Meant for test harnesses asserting on rendered output, * not for anything on a frame path. * * @param s1 First surface. Required. Its `pitch * h` is what determines how many * bytes are compared. * @param s2 Second surface. Required. * @return `NULL` when the pixels match, otherwise an error context owned by the * caller. "Not equal" is reported as an error, not as an out-param. * @throws AKERR_NULLPOINTER If @p s1 or @p s2 is `NULL`, or either has no * pixel buffer. * @throws AKERR_VALUE If the surfaces differ in size, pitch, or pixel format, * or if their pixels differ. * * Dimensions, pitch and pixel format are compared first, and a difference in * any of them is reported as a mismatch. Until 0.5.0 they were not, so a * smaller @p s2 was read past its end instead -- benign in practice and * immediately fatal under a memory checker. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_compare_sdl_surfaces(SDL_Surface *s1, SDL_Surface *s2); /** * @brief Draw two textures in turn, read the framebuffer back after each, and compare. * * The test-harness counterpart to akgl_compare_sdl_surfaces: it answers "do * these two textures *render* the same", which is not the same question as "are * these two textures identical", because the renderer's scaling and blending sit * in between. Both are drawn into the same rectangle against a cleared target. * * @param t1 First texture. Required. * @param t2 Second texture. Required. * @param x Left edge of the region, in pixels. Used for the source * rectangle, the destination, and the readback alike. * @param y Top edge of the region. * @param w Width of the region. * @param h Height of the region. * @param writeout Optional filename for a PNG of the *first* render, written * under `SDL_GetBasePath()`. `NULL` skips it. This is a * debugging aid -- when an image assertion fails, this is how * you see what was actually drawn. * @return `NULL` when the two renders match, otherwise an error context owned by * the caller. * @throws AKERR_NULLPOINTER If @p t1 or @p t2 is `NULL`. * @throws AKGL_ERR_SDL If the framebuffer cannot be read back. * @throws AKERR_IO If @p writeout is given and the PNG cannot be written. * @throws AKERR_VALUE If the two renders differ. * @throws AKGL_ERR_HEAP If the string pool is exhausted. * * @note Until 0.5.0 both passes drew @p t1, so this always reported a match and * every image assertion built on it asserted nothing. It draws @p t2 on * the second pass now. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_render_and_compare(SDL_Texture *t1, SDL_Texture *t2, int x, int y, int w, int h, char *writeout); #endif // _AKGL_UTIL_H_