/* ------------------------------------------------------------------------------ Licensing information can be found at the end of the file. ------------------------------------------------------------------------------ cute_spritebatch.h - v1.08 To create implementation (the function definitions) #define SPRITEBATCH_IMPLEMENTATION in *one* C/CPP file (translation unit) that includes this file SUMMARY: This header implements a 2D sprite batcher by tracking different textures within a rolling atlas cache. Over time atlases are decayed and recreated when textures stop being used. This header is useful for batching sprites at run-time. This avoids the need to compile texture atlases as a pre-process step, letting the game load images up individually, dramatically simplifying art pipelines. MORE DETAILS: `spritebatch_push` is used to push sprite instances into a buffer. Rendering sprites works by calling `spritebatch_flush`. `spritebatch_flush` will use a user-supplied callback to report sprite batches. This callback is of type `submit_batch_fn`. The batches are reported as an array of `spritebatch_sprite_t` sprites, and can be further sorted by the user (for example to sort by depth). Sprites in a batch share the same texture handle (either from the same base image, or from the same internal atlas). cute_spritebatch does not know anything about how to generate texture handles, or destroy them. As such, the user must supply two callbacks for creating handles and destroying them. These can be simple wrappers around, for example, `glGenTextures` and `glDeleteTextures`. Finally, cute_spritebatch will periodically need access to pixels from images. These pixels are used to generate textures, or to build atlases (which in turn generate a texture). cute_spritebatch does not need to know much about your images, other than the pixel stride. The user supplies callback of type `get_pixels_fn`, which lets cute_spritebatch retreive the pixels associated with a particular image. The pixels can be stored in RAM and handed to cute_spritebatch whenever requested, or the pixels can be fetched directly from disk and handed to cute_spritebatch. It doesn't matter to cute_spritebatch. Since `get_pixels_fn` can be called from `spritebatch_flush` it is recommended to avoid file i/o within the `get_pixels_fn` callback, and instead try to already have pixels ready in RAM. The `spritebatch_defrag` function performs atlas creation and texture management. It should be called periodically. It can be called once per game tick (once per render), or optionally called at a different frequency (once every N game ticks). PROS AND CONS: PROS - Texture atlases are completely hidden behind an api. The api in this header can easily be implemented with different backend sprite batchers. For example on some platforms bindless textures can be utilized in order to avoid texture atlases entirely! Code using this API can have the backend implementation swapped without requiring any user code to change. - Sprites are batched in an effective manner to dramatically reduce draw call counts. - Supporting hotswapping or live-reloading of images can be trivialized due to moving atlas creation out of the art-pipeline and into the run-time. - Since atlases are built at run-time and continually maintained, images are guaranteed to be drawn at the same time on-screen as their atlas neighbors. This is typically not the case for atlas preprocessors, as a *guess* must be made to try and organize images together in atlases that need to be drawn at roughly the same time. CONS - Performance hits in the `spritebatch_defrag` function, and a little as well in the `spritebatch_flush` function. Extra run-time memory usage for bookkeeping, which implies a RAM hit as well as more things to clog the CPU cache. - If each texture comes from a separate image on-disk, opening individual files on disk can be very slow. For example on Windows just performing permissions and related work to open a file is time-consuming. This can be mitigated by moving assets into a single larger file, for example a .zip archive and read from using a file io abstraction like PHYSFS. - For large numbers of separate images, some file abstraction is necessary to avoid a large performance hit on opening/closing many individual files. This problem is *not* solved by cute_spritebatch.h, and instead should be solved by some separate file abstraction system. PHYSFS is a good example of a solid file io abstraction. EXAMPLE USAGE: spritebatch_config_t config; spritebatch_set_default_config(&config); config.batch_callback = my_report_batches_function; config.get_pixels_callback = my_get_pixels_function; config.generate_texture_callback = my_make_texture_handle_function; config.delete_texture_callback = my_destroy_texture_handle_function; spritebatch_t batcher; spritebatch_init(&batcher, &config); while (game_is_running) { for (int i = 0; i < sprite_count; ++i) spritebatch_push( &batcher, sprites[i].image_id, sprites[i].image_width_in_pixels, sprites[i].image_height_in_pixels, sprites[i].position_x, sprites[i].poxition_y, sprites[i].scale_x, sprites[i].scale_y, sprites[i].cos_rotation_angle, sprites[i].sin_rotation_angle ); spritebatch_tick(&batcher); spritebatch_defrag(&batcher); spritebatch_flush(&batcher); } CUSTOMIZATION: The following macros can be defined before including this header with the SPRITEBATCH_IMPLEMENTATION symbol defined, in order to customize the internal behavior of cute_spritebatch.h. Search this header to find how each macro is defined and used. Note that MALLOC/FREE functions can optionally take a context parameter for custom allocation. SPRITEBATCH_MALLOC SPRITEBATCH_MEMCPY SPRITEBATCH_MEMSET SPRITEBATCH_MEMMOVE SPRITEBATCH_ASSERT SPRITEBATCH_ATLAS_FLIP_Y_AXIS_FOR_UV SPRITEBATCH_ATLAS_EMPTY_COLOR SPRITEBATCH_LOG Revision history: 0.01 (11/20/2017) experimental release 1.00 (04/14/2018) initial release 1.01 (05/07/2018) modification for easier file embedding 1.02 (02/03/2019) moved def of spritebatch_t for easier embedding, inverted get pixels callback to let users have an easier time with memory management, added support for pixel padding along the edges of all textures (useful for certain shader effects) 1.03 (08/18/2020) refactored `spritebatch_push` so that sprites can have userdata 1.04 (08/20/2021) qsort -> mergesort to avoid sort bugs, optional override `sprites_sorter_fn` sorting routines provided by Kariem, added new function `spritebatch_prefetch` 1.05 (12/10/2022) added `SPRITEBATCH_SPRITE_GEOMETRY`, a way to put custom geom- etry in sprites. Added `spritebatch_register_premade_atlas`, a way to inject premade atlases into spritebatch 1.06 (03/24/2023) added `spritebatch_invalidate`, useful for updating pixels NOW 1.07 (10/01/2024) Removed public `sort_bits` since it's not actually useful, and reused it internally to implement stable sorting (bugfix). 1.08 (02/01/2026) Replaced external hashtable.h with simpler internal spritebatch_map_t, fixed memory context bugs in spritebatch_term and get_pixels. */ /* Contributors: Kariem 1.04 - Optional sorter function `sprites_sorter_fn` Kariem 1.05 - Initial work on premade atlases */ #ifndef SPRITEBATCH_H #ifndef SPRITEBATCH_U64 #define SPRITEBATCH_U64 unsigned long long #endif // SPRITEBATCH_U64 typedef struct spritebatch_t spritebatch_t; typedef struct spritebatch_config_t spritebatch_config_t; typedef struct spritebatch_sprite_t spritebatch_sprite_t; // This define is *completely optional*. It lets you override the default layout of // data passed through the sprite batcher. By default it contains the minimal info // needed for a quad (position, scale, rotation, etc.). You can override this macro // to define your own geometry for batching. For example, you could have a union to // support both quads and individual triangles, as opposed to just quads by default. // // IMPROTANT NOTE (ignore unless you override this macro): // Just note you'll have to calculate your own uv coordinates after each batch is // reported. You should also be careful about the scale of your geometry if you opt // to use `atlas_use_border_pixels`, as you'll have to account for border pixels // included in the reported uvs. Search `SPRITEBATCH_SPRITE_GEOMETRY_DEFAULT` for // more an example on how to do this yourself. #ifndef SPRITEBATCH_SPRITE_GEOMETRY typedef struct spritebatch_primitive_t { float x, y; // x and y position float sx, sy; // scale on x and y axis float c, s; // cosine and sine (represents cos(angle) and sin(angle)) } spritebatch_primitive_t; # define SPRITEBATCH_SPRITE_GEOMETRY spritebatch_primitive_t // Used to automatically scale reported sprites according to border pixels. # define SPRITEBATCH_SPRITE_GEOMETRY_DEFAULT #endif // Sprites will be pushed into the spritebatch with this struct. All the fields // should be set before calling `spritebatch_push`, though `texture_id` and // `sort_bits` can simply be set to zero. // // After sprites are pushed onto the spritebatch via `spritebatch_push`, they will // be sorted, `texture_id` is assigned to a generated atlas, and handed back to you // via the `submit_batch_fn` callback. struct spritebatch_sprite_t { // `image_id` must be a unique identifier for the image a sprite references. // You must set this value! SPRITEBATCH_U64 image_id; // `texture_id` can be set to zero. This value will be overwritten with a valid // texture id before batches are reported back to you. This id will map to an // atlas created internally. For premade atlases you must set this yourself. SPRITEBATCH_U64 texture_id; // For internal use. Will get overwritten. int sort_bits; // Contains all of the sprite's geometry. By default this is just a scale + // translation + rotation. However, you can overload this macro to use your own // geometry definition. See comments at this macro definition above for more info. SPRITEBATCH_SPRITE_GEOMETRY geom; int w, h; // width and height of this sprite's image in pixels float minx, miny; // u coordinate - this will be overwritten, except for premade sprites float maxx, maxy; // v coordinate - this will be overwritten, except for premade sprites // This is a *completely optional* feature. You can insert your own user data // struct into each sprite. It is *never* touched internally, and simply handed // back to you later. #ifdef SPRITEBATCH_SPRITE_USERDATA SPRITEBATCH_SPRITE_USERDATA udata; #endif // SPRITEBATCH_SPRITE_USERDATA }; // Pushes a sprite onto an internal buffer. Does no other logic. void spritebatch_push(spritebatch_t* sb, spritebatch_sprite_t sprite); // Ensures the image associated with your unique `image_id` is loaded up into spritebatch. This // function pretends to draw a sprite referencing `image_id` but doesn't actually do any // drawing at all. Use this function as an optimization to pre-load images you know will be // drawn very soon, e.g. prefetch all ten images within a single animation just as it starts // playing. void spritebatch_prefetch(spritebatch_t* sb, SPRITEBATCH_U64 image_id, int w, int h); // Useful for re-uploading pixels to the GPU. // Invalidates the internal cache for a specific sprite. If this sprite resides in a texture atlas // the entire atlas is recompiled. If the sprite resides in the lonely buffer only the individual // texture gets recreated. You may want to beef up `lonely_buffer_count_till_flush` in the config // `spritebatch_config_t` if you want to invalidate sprites often -- this can help prevent constantly // invalidating internal atlases and recompiling them, and instead get your dynamic textures into the // lonely buffer. void spritebatch_invalidate(spritebatch_t* sb, SPRITEBATCH_U64 image_id); // If a match for `image_id` is found, the texture id and uv coordinates are looked up and returned // as a sprite instance. This is sometimes useful to render sprites through an external mechanism, // such as Dear ImGui. The return result will be valid until the next call to `spritebatch_defrag`. spritebatch_sprite_t spritebatch_fetch(spritebatch_t* sb, SPRITEBATCH_U64 image_id, int w, int h); // Increments internal timestamps on all textures, for use in `spritebatch_defrag`. void spritebatch_tick(spritebatch_t* sb); // Sorts the internal sprites and flushes the buffer built by `spritebatch_push`. Will call // the `submit_batch_fn` function for each batch of sprites and return them as an array. Any `image_id` // within the `spritebatch_push` buffer that do not yet have a texture handle will request pixels // from the image via `get_pixels_fn` and request a texture handle via `generate_texture_handle_fn`. // Returns the number of batches created and submitted. int spritebatch_flush(spritebatch_t* sb); // Clears buffers from `spritebatch_flush` in case you need to bail on rendering for any reason, such // as working with deferred contexts. void spritebatch_clear(spritebatch_t* sb); // All textures created so far by `spritebatch_flush` will be considered as candidates for creating // new internal texture atlases. Internal texture atlases compress images together inside of one // texture to dramatically reduce draw calls. When an atlas is created, the most recently used `image_id` // instances are prioritized, to ensure atlases are filled with images all drawn at the same time. // As some textures cease to draw on screen, they "decay" over time. Once enough images in an atlas // decay, the atlas is removed, and any "live" images in the atlas are used to create new atlases. // Can be called every 1/N times `spritebatch_flush` is called. int spritebatch_defrag(spritebatch_t* sb); int spritebatch_init(spritebatch_t* sb, spritebatch_config_t* config, void* udata); void spritebatch_term(spritebatch_t* sb); typedef struct spritebatch_premade_sprite_t { // `image_id` must be a unique identifier for the image a sprite references. This id is *not* local // to a particular atlas, and instead is global to the entire spritebatch. SPRITEBATCH_U64 image_id; int w, h; // width and height of this sprite's image in pixels float minx, miny; // u coordinate in the premade atlas float maxx, maxy; // v coordinate in the premade atlas } spritebatch_premade_sprite_t; // Registers a premade atlas into the sprite batcher. This function is provided here mostly for // convenience. Sometimes you may already have a pre-created atlas and want a simple way to draw all // sprites through spritebatch. // // In terms of performance, premade atlases provide another way to tune the performance of batching. // If you know ahead of time it's better to inject a premade atlas into the spritebatch, instead of // using `spritebatch_push`, this can be a good option. For example, this makes sense for text // rendering systems that create their own font texture atlas. Understanding the performance impact // can become a lot simpler than flooding `spritebatch_push` with a lot of unique glyphs used briefly. void spritebatch_register_premade_atlas(spritebatch_t* sb, SPRITEBATCH_U64 texture_id, int w, int h, int sprite_count, spritebatch_premade_sprite_t* sprites); // Sprite batches are submit via synchronous callback back to the user. This function is called // from inside `spritebatch_flush`. Each time `submit_batch_fn` is called an array of sprites // is handed to the user. The sprites are intended to be further sorted by the user as desired // (for example, additional sorting based on depth). `w` and `h` are the width/height, respectively, // of the texture the batch of sprites resides upon. w/h can be useful for knowing texture dim- // ensions, which is needed to know texel size or other measurements. typedef void (submit_batch_fn)(spritebatch_sprite_t* sprites, int count, int texture_w, int texture_h, void* udata); // cute_spritebatch.h needs to know how to get the pixels of an image, generate textures handles (for // example glGenTextures for OpenGL), and destroy texture handles. These functions are all called // from within the `spritebatch_defrag` function, and sometimes from `spritebatch_flush`. // Called when the pixels are needed from the user. `image_id` maps to a unique image, and is *not* // related to `texture_id` at all. `buffer` must be filled in with `bytes_to_fill` number of bytes. // The user is assumed to know the width/height of the image, and can optionally verify that // `bytes_to_fill` matches the user's w * h * stride for this particular image. typedef void (get_pixels_fn)(SPRITEBATCH_U64 image_id, void* buffer, int bytes_to_fill, void* udata); // Called with a new texture handle is needed. This will happen whenever a new atlas is created, // and whenever new `image_id`s first appear to cute_spritebatch, and have yet to find their way // into an appropriate atlas. typedef SPRITEBATCH_U64 (generate_texture_handle_fn)(void* pixels, int w, int h, void* udata); // Called whenever a texture handle is ready to be free'd up. This happens whenever a particular image // or a particular atlas has not been used for a while, and is ready to be released. typedef void (destroy_texture_handle_fn)(SPRITEBATCH_U64 texture_id, void* udata); // (Optional) If the user provides this callback, cute_spritebatch will call it to sort all of sprites before submit_batch // callback is called. The intention of sorting is to minimize the submit_batch calls. cute_spritebatch // provides its own internal sorting function which will be used if the user does not provide this callback. // // Example using std::sort (C++) - Please note the lambda needs to be a non-capturing one. // // config.sprites_sorter_callback = [](spritebatch_sprite_t* sprites, int count) // { // std::sort(sprites, sprites + count, // [](const spritebatch_sprite_t& a, const spritebatch_sprite_t& b) { // return a.texture_id < b.texture_id; // }); // }; typedef void (sprites_sorter_fn)(spritebatch_sprite_t* sprites, int count); // Sets all function pointers originally defined in the `config` struct when calling `spritebatch_init`. // Useful if DLL's are reloaded, or swapped, etc. void spritebatch_reset_function_ptrs(spritebatch_t* sb, submit_batch_fn* batch_callback, get_pixels_fn* get_pixels_callback, generate_texture_handle_fn* generate_texture_callback, destroy_texture_handle_fn* delete_texture_callback, sprites_sorter_fn* sprites_sorter_callback); // Initializes a set of good default paramaters. The users must still set // the four callbacks inside of `config`. void spritebatch_set_default_config(spritebatch_config_t* config); struct spritebatch_config_t { int pixel_stride; int atlas_width_in_pixels; int atlas_height_in_pixels; int atlas_use_border_pixels; int ticks_to_decay_texture; // number of ticks it takes for a texture handle to be destroyed via `destroy_texture_handle_fn` int lonely_buffer_count_till_flush; // Number of unique textures allowed to persist that are not a part of an atlas yet, each one allowed is another draw call. // These are called "lonely textures", since they don't belong to any atlas yet. Set this to 0 if you want all textures to be // immediately put into atlases. Setting a higher number, like 64, will buffer up 64 unique textures (which means up to an // additional 64 draw calls) before flushing them into atlases. Too low of a lonely buffer count combined with a low tick // to decay rate will cause performance problems where atlases are constantly created and immedately destroyed -- you have // been warned! Use `SPRITEBATCH_LOG` to gain some insight on what's going on inside the spritebatch when tuning these settings. float ratio_to_decay_atlas; // from 0 to 1, once ratio is less than `ratio_to_decay_atlas`, flush active textures in atlas to lonely buffer float ratio_to_merge_atlases; // from 0 to 0.5, attempts to merge atlases with some ratio of empty space submit_batch_fn* batch_callback; get_pixels_fn* get_pixels_callback; generate_texture_handle_fn* generate_texture_callback; destroy_texture_handle_fn* delete_texture_callback; sprites_sorter_fn* sprites_sorter_callback; // (Optional) void* allocator_context; }; #define SPRITEBATCH_H #endif #if !defined(SPRITE_BATCH_INTERNAL_H) // spritebatch_map_t - A simple hash map for SPRITEBATCH_U64 keys to fixed-size values. // Uses open addressing with linear probing and maintains a dense item array for fast iteration. typedef struct spritebatch_map_slot_t { unsigned key_hash; int item_index; int base_count; } spritebatch_map_slot_t; typedef struct spritebatch_map_t { void* mem_ctx; int count; int item_size; spritebatch_map_slot_t* slots; int slot_capacity; SPRITEBATCH_U64* keys; int* item_slots; void* items; int item_capacity; } spritebatch_map_t; typedef struct spritebatch_internal_sprite_t { SPRITEBATCH_U64 image_id; int sort_bits; SPRITEBATCH_SPRITE_GEOMETRY geom; int w, h; // w/h of image in pixels float premade_minx, premade_miny; // u coordinate for premade float premade_maxx, premade_maxy; // v coordinate for premade #ifdef SPRITEBATCH_SPRITE_USERDATA SPRITEBATCH_SPRITE_USERDATA udata; #endif // SPRITEBATCH_SPRITE_USERDATA } spritebatch_internal_sprite_t; typedef struct spritebatch_internal_texture_t { int timestamp; int w, h; float minx, miny; float maxx, maxy; SPRITEBATCH_U64 image_id; } spritebatch_internal_texture_t; typedef struct spritebatch_internal_atlas_t { SPRITEBATCH_U64 texture_id; float volume_ratio; spritebatch_map_t sprites_to_textures; struct spritebatch_internal_atlas_t* next; struct spritebatch_internal_atlas_t* prev; } spritebatch_internal_atlas_t; typedef struct spritebatch_internal_lonely_texture_t { int timestamp; int w, h; SPRITEBATCH_U64 image_id; SPRITEBATCH_U64 texture_id; } spritebatch_internal_lonely_texture_t; typedef struct spritebatch_internal_premade_sprite_t { int atlas_w, atlas_h; float minx, miny; float maxx, maxy; SPRITEBATCH_U64 texture_id; } spritebatch_internal_premade_sprite_t; struct spritebatch_t { int input_count; int input_capacity; spritebatch_internal_sprite_t* input_buffer; int sprite_count; int sprite_capacity; spritebatch_sprite_t* sprites; spritebatch_sprite_t* sprites_scratch; int key_buffer_count; int key_buffer_capacity; SPRITEBATCH_U64* key_buffer; int pixel_buffer_size; // number of pixels void* pixel_buffer; spritebatch_map_t sprites_to_premades; spritebatch_map_t sprites_to_lonely_textures; spritebatch_map_t sprites_to_atlases; spritebatch_internal_atlas_t* atlases; int sort_id; int pixel_stride; int atlas_width_in_pixels; int atlas_height_in_pixels; int atlas_use_border_pixels; int ticks_to_decay_texture; int lonely_buffer_count_till_flush; int lonely_buffer_count_till_decay; float ratio_to_decay_atlas; float ratio_to_merge_atlases; submit_batch_fn* batch_callback; get_pixels_fn* get_pixels_callback; generate_texture_handle_fn* generate_texture_callback; destroy_texture_handle_fn* delete_texture_callback; sprites_sorter_fn* sprites_sorter_callback; void* mem_ctx; void* udata; }; #ifndef _CRT_SECURE_NO_WARNINGS #define _CRT_SECURE_NO_WARNINGS #endif #ifndef _CRT_NONSTDC_NO_DEPRECATE #define _CRT_NONSTDC_NO_DEPRECATE #endif #ifndef SPRITEBATCH_MALLOC #include #define SPRITEBATCH_MALLOC(size, ctx) malloc(size) #define SPRITEBATCH_FREE(ptr, ctx) free(ptr) #endif #ifndef SPRITEBATCH_MEMCPY #include #define SPRITEBATCH_MEMCPY(dst, src, n) memcpy(dst, src, n) #endif #ifndef SPRITEBATCH_MEMSET #include #define SPRITEBATCH_MEMSET(ptr, val, n) memset(ptr, val, n) #endif #ifndef SPRITEBATCH_MEMMOVE #include #define SPRITEBATCH_MEMMOVE(dst, src, n) memmove(dst, src, n) #endif #ifndef SPRITEBATCH_ASSERT #include #define SPRITEBATCH_ASSERT(condition) assert(condition) #endif // flips output uv coordinate's y. Can be useful to "flip image on load" #ifndef SPRITEBATCH_ATLAS_FLIP_Y_AXIS_FOR_UV #define SPRITEBATCH_ATLAS_FLIP_Y_AXIS_FOR_UV 1 #endif // flips output uv coordinate's y. Can be useful to "flip image on load" #ifndef SPRITEBATCH_LONELY_FLIP_Y_AXIS_FOR_UV #define SPRITEBATCH_LONELY_FLIP_Y_AXIS_FOR_UV 1 #endif #ifndef SPRITEBATCH_ATLAS_EMPTY_COLOR #define SPRITEBATCH_ATLAS_EMPTY_COLOR 0x00000000 #endif #ifndef SPRITEBATCH_LOG #if 0 #define SPRITEBATCH_LOG printf #else #define SPRITEBATCH_LOG(...) #endif #endif #define SPRITE_BATCH_INTERNAL_H #endif #ifdef SPRITEBATCH_IMPLEMENTATION #ifndef SPRITEBATCH_IMPLEMENTATION_ONCE #define SPRITEBATCH_IMPLEMENTATION_ONCE // spritebatch_map implementation static unsigned spritebatch_map_pow2ceil(unsigned v) { --v; v |= v >> 1; v |= v >> 2; v |= v >> 4; v |= v >> 8; v |= v >> 16; ++v; v += (v == 0); return v; } static unsigned spritebatch_map_hash(SPRITEBATCH_U64 key) { key = (~key) + (key << 18); key = key ^ (key >> 31); key = key * 21; key = key ^ (key >> 11); key = key + (key << 6); key = key ^ (key >> 22); SPRITEBATCH_ASSERT(key); return (unsigned)key; } static void spritebatch_map_init(spritebatch_map_t* map, int item_size, int initial_capacity, void* mem_ctx) { initial_capacity = (int)spritebatch_map_pow2ceil(initial_capacity >= 0 ? (unsigned)initial_capacity : 32U); map->mem_ctx = mem_ctx; map->count = 0; map->item_size = item_size; map->slot_capacity = (int)spritebatch_map_pow2ceil((unsigned)(initial_capacity + initial_capacity / 2)); int slots_size = (int)(map->slot_capacity * sizeof(*map->slots)); map->slots = (spritebatch_map_slot_t*)SPRITEBATCH_MALLOC(slots_size, mem_ctx); SPRITEBATCH_ASSERT(map->slots); SPRITEBATCH_MEMSET(map->slots, 0, slots_size); map->item_capacity = (int)spritebatch_map_pow2ceil((unsigned)initial_capacity); map->keys = (SPRITEBATCH_U64*)SPRITEBATCH_MALLOC(map->item_capacity * (sizeof(*map->keys) + sizeof(*map->item_slots) + map->item_size), mem_ctx); SPRITEBATCH_ASSERT(map->keys); map->item_slots = (int*)(map->keys + map->item_capacity); map->items = (void*)(map->item_slots + map->item_capacity); } static void spritebatch_map_term(spritebatch_map_t* map) { SPRITEBATCH_FREE(map->keys, map->mem_ctx); SPRITEBATCH_FREE(map->slots, map->mem_ctx); } static int spritebatch_map_find_slot(spritebatch_map_t const* map, SPRITEBATCH_U64 key) { int slot_mask = map->slot_capacity - 1; unsigned hash = spritebatch_map_hash(key); int base_slot = (int)(hash & (unsigned)slot_mask); int base_count = map->slots[base_slot].base_count; int slot = base_slot; while (base_count > 0) { unsigned slot_hash = map->slots[slot].key_hash; if (slot_hash) { int slot_base = (int)(slot_hash & (unsigned)slot_mask); if (slot_base == base_slot) { --base_count; if (slot_hash == hash && map->keys[map->slots[slot].item_index] == key) return slot; } } slot = (slot + 1) & slot_mask; } return -1; } static void spritebatch_map_expand_slots(spritebatch_map_t* map) { int old_capacity = map->slot_capacity; spritebatch_map_slot_t* old_slots = map->slots; map->slot_capacity *= 2; int slot_mask = map->slot_capacity - 1; int size = (int)(map->slot_capacity * sizeof(*map->slots)); map->slots = (spritebatch_map_slot_t*)SPRITEBATCH_MALLOC(size, map->mem_ctx); SPRITEBATCH_ASSERT(map->slots); SPRITEBATCH_MEMSET(map->slots, 0, size); for (int i = 0; i < old_capacity; ++i) { unsigned hash = old_slots[i].key_hash; if (hash) { int base_slot = (int)(hash & (unsigned)slot_mask); int slot = base_slot; while (map->slots[slot].key_hash) slot = (slot + 1) & slot_mask; map->slots[slot].key_hash = hash; int item_index = old_slots[i].item_index; map->slots[slot].item_index = item_index; map->item_slots[item_index] = slot; ++map->slots[base_slot].base_count; } } SPRITEBATCH_FREE(old_slots, map->mem_ctx); } static void spritebatch_map_expand_items(spritebatch_map_t* map) { map->item_capacity *= 2; SPRITEBATCH_U64* new_keys = (SPRITEBATCH_U64*)SPRITEBATCH_MALLOC( map->item_capacity * (sizeof(*map->keys) + sizeof(*map->item_slots) + map->item_size), map->mem_ctx); SPRITEBATCH_ASSERT(new_keys); int* new_item_slots = (int*)(new_keys + map->item_capacity); void* new_items = (void*)(new_item_slots + map->item_capacity); SPRITEBATCH_MEMCPY(new_keys, map->keys, map->count * sizeof(*map->keys)); SPRITEBATCH_MEMCPY(new_item_slots, map->item_slots, map->count * sizeof(*map->item_slots)); SPRITEBATCH_MEMCPY(new_items, map->items, (size_t)map->count * map->item_size); SPRITEBATCH_FREE(map->keys, map->mem_ctx); map->keys = new_keys; map->item_slots = new_item_slots; map->items = new_items; } static void* spritebatch_map_insert(spritebatch_map_t* map, SPRITEBATCH_U64 key, void const* item) { SPRITEBATCH_ASSERT(spritebatch_map_find_slot(map, key) < 0); if (map->count >= (map->slot_capacity - map->slot_capacity / 3)) spritebatch_map_expand_slots(map); int slot_mask = map->slot_capacity - 1; unsigned hash = spritebatch_map_hash(key); int base_slot = (int)(hash & (unsigned)slot_mask); int base_count = map->slots[base_slot].base_count; int slot = base_slot; int first_free = slot; while (base_count) { unsigned slot_hash = map->slots[slot].key_hash; if (slot_hash == 0 && map->slots[first_free].key_hash != 0) first_free = slot; int slot_base = (int)(slot_hash & (unsigned)slot_mask); if (slot_base == base_slot) --base_count; slot = (slot + 1) & slot_mask; } slot = first_free; while (map->slots[slot].key_hash) slot = (slot + 1) & slot_mask; if (map->count >= map->item_capacity) spritebatch_map_expand_items(map); map->slots[slot].key_hash = hash; map->slots[slot].item_index = map->count; ++map->slots[base_slot].base_count; void* dest = (void*)((uintptr_t)map->items + map->count * map->item_size); SPRITEBATCH_MEMCPY(dest, item, map->item_size); map->keys[map->count] = key; map->item_slots[map->count] = slot; ++map->count; return dest; } static void spritebatch_map_remove(spritebatch_map_t* map, SPRITEBATCH_U64 key) { int slot = spritebatch_map_find_slot(map, key); SPRITEBATCH_ASSERT(slot >= 0); int slot_mask = map->slot_capacity - 1; unsigned hash = map->slots[slot].key_hash; int base_slot = (int)(hash & (unsigned)slot_mask); --map->slots[base_slot].base_count; map->slots[slot].key_hash = 0; int index = map->slots[slot].item_index; int last_index = map->count - 1; if (index != last_index) { map->keys[index] = map->keys[last_index]; map->item_slots[index] = map->item_slots[last_index]; void* dst = (void*)((uintptr_t)map->items + index * map->item_size); void* src = (void*)((uintptr_t)map->items + last_index * map->item_size); SPRITEBATCH_MEMCPY(dst, src, map->item_size); map->slots[map->item_slots[last_index]].item_index = index; } --map->count; } static void spritebatch_map_clear(spritebatch_map_t* map) { map->count = 0; SPRITEBATCH_MEMSET(map->slots, 0, map->slot_capacity * sizeof(*map->slots)); } static void* spritebatch_map_find(spritebatch_map_t const* map, SPRITEBATCH_U64 key) { int slot = spritebatch_map_find_slot(map, key); if (slot < 0) return 0; int index = map->slots[slot].item_index; return (void*)((uintptr_t)map->items + index * map->item_size); } static int spritebatch_map_count(spritebatch_map_t const* map) { return map->count; } static void* spritebatch_map_items(spritebatch_map_t const* map) { return map->items; } static void spritebatch_map_swap(spritebatch_map_t* map, int index_a, int index_b) { if (index_a < 0 || index_a >= map->count || index_b < 0 || index_b >= map->count) return; int slot_a = map->item_slots[index_a]; int slot_b = map->item_slots[index_b]; map->item_slots[index_a] = slot_b; map->item_slots[index_b] = slot_a; SPRITEBATCH_U64 temp_key = map->keys[index_a]; map->keys[index_a] = map->keys[index_b]; map->keys[index_b] = temp_key; void* item_a = (void*)((uintptr_t)map->items + index_a * map->item_size); void* item_b = (void*)((uintptr_t)map->items + index_b * map->item_size); // Use stack buffer for swap (item_size is typically small) char swap_temp[256]; SPRITEBATCH_ASSERT(map->item_size <= (int)sizeof(swap_temp)); SPRITEBATCH_MEMCPY(swap_temp, item_a, map->item_size); SPRITEBATCH_MEMCPY(item_a, item_b, map->item_size); SPRITEBATCH_MEMCPY(item_b, swap_temp, map->item_size); map->slots[slot_a].item_index = index_b; map->slots[slot_b].item_index = index_a; } #include bool sprite_batch_internal_use_scratch_buffer(spritebatch_t* sb) { return sb->sprites_sorter_callback == 0; } int spritebatch_init(spritebatch_t* sb, spritebatch_config_t* config, void* udata) { // read config params if (!config | !sb) return 1; sb->sort_id = 0; sb->pixel_stride = config->pixel_stride; sb->atlas_width_in_pixels = config->atlas_width_in_pixels; sb->atlas_height_in_pixels = config->atlas_height_in_pixels; sb->atlas_use_border_pixels = config->atlas_use_border_pixels; sb->ticks_to_decay_texture = config->ticks_to_decay_texture; sb->lonely_buffer_count_till_flush = config->lonely_buffer_count_till_flush; sb->lonely_buffer_count_till_decay = sb->lonely_buffer_count_till_flush / 2; if (sb->lonely_buffer_count_till_decay <= 0) sb->lonely_buffer_count_till_decay = 1; sb->ratio_to_decay_atlas = config->ratio_to_decay_atlas; sb->ratio_to_merge_atlases = config->ratio_to_merge_atlases; sb->batch_callback = config->batch_callback; sb->get_pixels_callback = config->get_pixels_callback; sb->generate_texture_callback = config->generate_texture_callback; sb->delete_texture_callback = config->delete_texture_callback; sb->sprites_sorter_callback = config->sprites_sorter_callback; sb->mem_ctx = config->allocator_context; sb->udata = udata; if (sb->atlas_width_in_pixels < 1 || sb->atlas_height_in_pixels < 1) return 1; if (sb->ticks_to_decay_texture < 1) return 1; if (sb->ratio_to_decay_atlas < 0 || sb->ratio_to_decay_atlas > 1.0f) return 1; if (sb->ratio_to_merge_atlases < 0 || sb->ratio_to_merge_atlases > 0.5f) return 1; if (!sb->batch_callback) return 1; if (!sb->get_pixels_callback) return 1; if (!sb->generate_texture_callback) return 1; if (!sb->delete_texture_callback) return 1; // initialize input buffer sb->input_count = 0; sb->input_capacity = 1024; sb->input_buffer = (spritebatch_internal_sprite_t*)SPRITEBATCH_MALLOC(sizeof(spritebatch_internal_sprite_t) * sb->input_capacity, sb->mem_ctx); if (!sb->input_buffer) return 1; // initialize sprite buffer sb->sprite_count = 0; sb->sprite_capacity = 1024; sb->sprites = (spritebatch_sprite_t*)SPRITEBATCH_MALLOC(sizeof(spritebatch_sprite_t) * sb->sprite_capacity, sb->mem_ctx); sb->sprites_scratch = 0; if (sprite_batch_internal_use_scratch_buffer(sb)) { sb->sprites_scratch = (spritebatch_sprite_t*)SPRITEBATCH_MALLOC(sizeof(spritebatch_sprite_t) * sb->sprite_capacity, sb->mem_ctx); } if (!sb->sprites) return 1; if (sprite_batch_internal_use_scratch_buffer(sb)) { if (!sb->sprites_scratch) return 1; } // initialize key buffer (for marking hash table entries for deletion) sb->key_buffer_count = 0; sb->key_buffer_capacity = 1024; sb->key_buffer = (SPRITEBATCH_U64*)SPRITEBATCH_MALLOC(sizeof(SPRITEBATCH_U64) * sb->key_buffer_capacity, sb->mem_ctx); // initialize pixel buffer for grabbing pixel data from the user as needed sb->pixel_buffer_size = 1024; sb->pixel_buffer = SPRITEBATCH_MALLOC(sb->pixel_buffer_size * sb->pixel_stride, sb->mem_ctx); // setup tables spritebatch_map_init(&sb->sprites_to_lonely_textures, sizeof(spritebatch_internal_lonely_texture_t), 1024, sb->mem_ctx); spritebatch_map_init(&sb->sprites_to_premades, sizeof(spritebatch_internal_premade_sprite_t), 1024 * 10, sb->mem_ctx); spritebatch_map_init(&sb->sprites_to_atlases, sizeof(spritebatch_internal_atlas_t*), 16, sb->mem_ctx); sb->atlases = 0; return 0; } void spritebatch_term(spritebatch_t* sb) { SPRITEBATCH_FREE(sb->input_buffer, sb->mem_ctx); SPRITEBATCH_FREE(sb->sprites, sb->mem_ctx); if (sb->sprites_scratch) { SPRITEBATCH_FREE(sb->sprites_scratch, sb->mem_ctx); } SPRITEBATCH_FREE(sb->key_buffer, sb->mem_ctx); SPRITEBATCH_FREE(sb->pixel_buffer, sb->mem_ctx); spritebatch_map_term(&sb->sprites_to_lonely_textures); spritebatch_map_term(&sb->sprites_to_premades); spritebatch_map_term(&sb->sprites_to_atlases); if (sb->atlases) { spritebatch_internal_atlas_t* atlas = sb->atlases; spritebatch_internal_atlas_t* sentinel = sb->atlases; do { spritebatch_map_term(&atlas->sprites_to_textures); spritebatch_internal_atlas_t* next = atlas->next; SPRITEBATCH_FREE(atlas, sb->mem_ctx); atlas = next; } while (atlas != sentinel); } SPRITEBATCH_MEMSET(sb, 0, sizeof(spritebatch_t)); } void spritebatch_reset_function_ptrs(spritebatch_t* sb, submit_batch_fn* batch_callback, get_pixels_fn* get_pixels_callback, generate_texture_handle_fn* generate_texture_callback, destroy_texture_handle_fn* delete_texture_callback, sprites_sorter_fn* sprites_sorter_callback) { sb->batch_callback = batch_callback; sb->get_pixels_callback = get_pixels_callback; sb->generate_texture_callback = generate_texture_callback; sb->delete_texture_callback = delete_texture_callback; sb->sprites_sorter_callback = sprites_sorter_callback; } void spritebatch_set_default_config(spritebatch_config_t* config) { config->pixel_stride = sizeof(char) * 4; config->atlas_width_in_pixels = 2048; config->atlas_height_in_pixels = 2048; config->atlas_use_border_pixels = 0; config->ticks_to_decay_texture = 60 * 30; config->lonely_buffer_count_till_flush = 64; config->ratio_to_decay_atlas = 0.5f; config->ratio_to_merge_atlases = 0.25f; config->batch_callback = 0; config->generate_texture_callback = 0; config->delete_texture_callback = 0; config->sprites_sorter_callback = 0; config->allocator_context = 0; } #define SPRITEBATCH_CHECK_BUFFER_GROW(ctx, count, capacity, data, type) \ do { \ if (ctx->count == ctx->capacity) \ { \ int new_capacity = ctx->capacity * 2; \ void* new_data = SPRITEBATCH_MALLOC(sizeof(type) * new_capacity, ctx->mem_ctx); \ if (!new_data) return 0; \ SPRITEBATCH_MEMCPY(new_data, ctx->data, sizeof(type) * ctx->count); \ SPRITEBATCH_FREE(ctx->data, ctx->mem_ctx); \ ctx->data = (type*)new_data; \ ctx->capacity = new_capacity; \ } \ } while (0) int spritebatch_internal_fill_internal_sprite(spritebatch_t* sb, spritebatch_sprite_t sprite, spritebatch_internal_sprite_t* out) { SPRITEBATCH_ASSERT(sprite.w <= sb->atlas_width_in_pixels); SPRITEBATCH_ASSERT(sprite.h <= sb->atlas_height_in_pixels); SPRITEBATCH_CHECK_BUFFER_GROW(sb, input_count, input_capacity, input_buffer, spritebatch_internal_sprite_t); out->image_id = sprite.image_id; out->sort_bits = sb->sort_id++; out->geom = sprite.geom; out->w = sprite.w; out->h = sprite.h; #ifdef SPRITEBATCH_SPRITE_GEOMETRY_DEFAULT out->geom.sx = sprite.geom.sx + (sb->atlas_use_border_pixels ? (sprite.geom.sx / (float)sprite.w) * 2.0f : 0); out->geom.sy = sprite.geom.sy + (sb->atlas_use_border_pixels ? (sprite.geom.sy / (float)sprite.h) * 2.0f : 0); #endif out->premade_minx = sprite.minx; out->premade_miny = sprite.miny; out->premade_maxx = sprite.maxx; out->premade_maxy = sprite.maxy; #ifdef SPRITEBATCH_SPRITE_USERDATA out->udata = sprite.udata; #endif return 1; } void spritebatch_internal_append_sprite(spritebatch_t* sb, spritebatch_internal_sprite_t sprite) { sb->input_buffer[sb->input_count++] = sprite; } void spritebatch_push(spritebatch_t* sb, spritebatch_sprite_t sprite) { spritebatch_internal_sprite_t sprite_out; spritebatch_internal_fill_internal_sprite(sb, sprite, &sprite_out); sb->input_buffer[sb->input_count++] = sprite_out; } void spritebatch_register_premade_atlas(spritebatch_t* sb, SPRITEBATCH_U64 texture_id, int w, int h, int sprite_count, spritebatch_premade_sprite_t* sprites) { for (int i = 0; i < sprite_count; ++i) { spritebatch_internal_premade_sprite_t premade; premade.atlas_w = w; premade.atlas_h = h; premade.minx = sprites[i].minx; premade.miny = sprites[i].miny; premade.maxx = sprites[i].maxx; premade.maxy = sprites[i].maxy; premade.texture_id = texture_id; void* find = spritebatch_map_find(&sb->sprites_to_premades, sprites[i].image_id); if (find) { SPRITEBATCH_MEMCPY(find, &premade, sizeof(premade)); } else { spritebatch_map_insert(&sb->sprites_to_premades, sprites[i].image_id, &premade); } } } int spritebatch_internal_lonely_sprite(spritebatch_t* sb, SPRITEBATCH_U64 image_id, int w, int h, spritebatch_sprite_t* sprite_out, int skip_missing_textures); spritebatch_internal_premade_sprite_t* spritebatch_internal_premade_sprite(spritebatch_t* sb, SPRITEBATCH_U64 image_id, spritebatch_sprite_t* sprite_out); void spritebatch_prefetch(spritebatch_t* sb, SPRITEBATCH_U64 image_id, int w, int h) { spritebatch_internal_premade_sprite_t* premade = spritebatch_internal_premade_sprite(sb, image_id, NULL); if(!premade) { void* atlas_ptr = spritebatch_map_find(&sb->sprites_to_atlases, image_id); if (!atlas_ptr) spritebatch_internal_lonely_sprite(sb, image_id, w, h, NULL, 0); } } spritebatch_sprite_t spritebatch_fetch(spritebatch_t* sb, SPRITEBATCH_U64 image_id, int w, int h) { spritebatch_sprite_t s; SPRITEBATCH_MEMSET(&s, 0, sizeof(s)); spritebatch_internal_premade_sprite_t* premade = (spritebatch_internal_premade_sprite_t*)spritebatch_map_find(&sb->sprites_to_premades, image_id); if(!premade) { void* atlas_ptr = spritebatch_map_find(&sb->sprites_to_atlases, image_id); if (atlas_ptr) { spritebatch_internal_atlas_t* atlas = *(spritebatch_internal_atlas_t**)atlas_ptr; s.texture_id = atlas->texture_id; spritebatch_internal_texture_t* tex = (spritebatch_internal_texture_t*)spritebatch_map_find(&atlas->sprites_to_textures, image_id); if (tex) { s.maxx = tex->maxx; s.maxy = tex->maxy; s.minx = tex->minx; s.miny = tex->miny; } } else { spritebatch_internal_lonely_sprite(sb, image_id, w, h, &s, 0); } } else { spritebatch_internal_premade_sprite(sb, image_id, &s); } return s; } static int spritebatch_internal_sprite_less_than_or_equal(spritebatch_sprite_t* a, spritebatch_sprite_t* b) { if (a->sort_bits < b->sort_bits) return 1; return a->texture_id <= b->texture_id; } void spritebatch_internal_merge_sort_iteration(spritebatch_sprite_t* a, int lo, int split, int hi, spritebatch_sprite_t* b) { int i = lo, j = split; for (int k = lo; k < hi; k++) { if (i < split && (j >= hi || spritebatch_internal_sprite_less_than_or_equal(a + i, a + j))) { b[k] = a[i]; i = i + 1; } else { b[k] = a[j]; j = j + 1; } } } void spritebatch_internal_merge_sort_recurse(spritebatch_sprite_t* b, int lo, int hi, spritebatch_sprite_t* a) { if (hi - lo <= 1) return; int split = (lo + hi) / 2; spritebatch_internal_merge_sort_recurse(a, lo, split, b); spritebatch_internal_merge_sort_recurse(a, split, hi, b); spritebatch_internal_merge_sort_iteration(b, lo, split, hi, a); } void spritebatch_internal_merge_sort(spritebatch_sprite_t* a, spritebatch_sprite_t* b, int n) { SPRITEBATCH_MEMCPY(b, a, sizeof(spritebatch_sprite_t) * n); spritebatch_internal_merge_sort_recurse(b, 0, n, a); } void spritebatch_internal_sort_sprites(spritebatch_t* sb) { if (sb->sprites_sorter_callback) sb->sprites_sorter_callback(sb->sprites, sb->sprite_count); else spritebatch_internal_merge_sort(sb->sprites, sb->sprites_scratch, sb->sprite_count); } static inline void spritebatch_internal_get_pixels(spritebatch_t* sb, SPRITEBATCH_U64 image_id, int w, int h) { int size = sb->atlas_use_border_pixels ? sb->pixel_stride * (w + 2) * (h + 2) : sb->pixel_stride * w * h; if (size > sb->pixel_buffer_size) { SPRITEBATCH_FREE(sb->pixel_buffer, sb->mem_ctx); sb->pixel_buffer_size = size; sb->pixel_buffer = SPRITEBATCH_MALLOC(sb->pixel_buffer_size, sb->mem_ctx); if (!sb->pixel_buffer) return; } SPRITEBATCH_MEMSET(sb->pixel_buffer, 0, size); int size_from_user = sb->pixel_stride * w * h; sb->get_pixels_callback(image_id, sb->pixel_buffer, size_from_user, sb->udata); if (sb->atlas_use_border_pixels) { // Expand image from top-left corner, offset by (1, 1). int w0 = w; int h0 = h; w += 2; h += 2; char* buffer = (char*)sb->pixel_buffer; int dst_row_stride = w * sb->pixel_stride; int src_row_stride = w0 * sb->pixel_stride; int src_row_offset = sb->pixel_stride; for (int i = 0; i < h - 2; ++i) { char* src_row = buffer + (h0 - i - 1) * src_row_stride; char* dst_row = buffer + (h - i - 2) * dst_row_stride + src_row_offset; SPRITEBATCH_MEMMOVE(dst_row, src_row, src_row_stride); } // Clear the border pixels. int pixel_stride = sb->pixel_stride; SPRITEBATCH_MEMSET(buffer, 0, dst_row_stride); for (int i = 1; i < h - 1; ++i) { SPRITEBATCH_MEMSET(buffer + i * dst_row_stride, 0, pixel_stride); SPRITEBATCH_MEMSET(buffer + i * dst_row_stride + src_row_stride + src_row_offset, 0, pixel_stride); } SPRITEBATCH_MEMSET(buffer + (h - 1) * dst_row_stride, 0, dst_row_stride); } } static inline SPRITEBATCH_U64 spritebatch_internal_generate_texture_handle(spritebatch_t* sb, SPRITEBATCH_U64 image_id, int w, int h) { spritebatch_internal_get_pixels(sb, image_id, w, h); if (sb->atlas_use_border_pixels) { w += 2; h += 2; } return sb->generate_texture_callback(sb->pixel_buffer, w, h, sb->udata); } spritebatch_internal_lonely_texture_t* spritebatch_internal_lonelybuffer_push(spritebatch_t* sb, SPRITEBATCH_U64 image_id, int w, int h, int make_tex) { spritebatch_internal_lonely_texture_t texture; texture.timestamp = 0; texture.w = w; texture.h = h; texture.image_id = image_id; texture.texture_id = make_tex ? spritebatch_internal_generate_texture_handle(sb, image_id, w, h) : ~0; return (spritebatch_internal_lonely_texture_t*)spritebatch_map_insert(&sb->sprites_to_lonely_textures, image_id, &texture); } int spritebatch_internal_lonely_sprite(spritebatch_t* sb, SPRITEBATCH_U64 image_id, int w, int h, spritebatch_sprite_t* sprite_out, int skip_missing_textures) { spritebatch_internal_lonely_texture_t* tex = (spritebatch_internal_lonely_texture_t*)spritebatch_map_find(&sb->sprites_to_lonely_textures, image_id); if (skip_missing_textures) { if (!tex) spritebatch_internal_lonelybuffer_push(sb, image_id, w, h, 0); return 1; } else { if (!tex) tex = spritebatch_internal_lonelybuffer_push(sb, image_id, w, h, 1); else if (tex->texture_id == ~0) tex->texture_id = spritebatch_internal_generate_texture_handle(sb, image_id, w, h); tex->timestamp = 0; if (sprite_out) { sprite_out->texture_id = tex->texture_id; sprite_out->minx = sprite_out->miny = 0; sprite_out->maxx = sprite_out->maxy = 1.0f; if (SPRITEBATCH_LONELY_FLIP_Y_AXIS_FOR_UV) { float tmp = sprite_out->miny; sprite_out->miny = sprite_out->maxy; sprite_out->maxy = tmp; } } return 0; } } spritebatch_internal_premade_sprite_t* spritebatch_internal_premade_sprite(spritebatch_t* sb, SPRITEBATCH_U64 image_id, spritebatch_sprite_t* sprite_out) { spritebatch_internal_premade_sprite_t* tex = (spritebatch_internal_premade_sprite_t*)spritebatch_map_find(&sb->sprites_to_premades, image_id); if (!tex) return NULL; SPRITEBATCH_ASSERT(tex->texture_id != ~0); if (sprite_out) { sprite_out->texture_id = tex->texture_id; } return tex; } int spritebatch_internal_push_sprite(spritebatch_t* sb, spritebatch_internal_sprite_t* s, int skip_missing_textures) { int skipped_tex = 0; spritebatch_sprite_t sprite; sprite.image_id = s->image_id; sprite.sort_bits = s->sort_bits; sprite.geom = s->geom; sprite.w = s->w; sprite.h = s->h; sprite.minx = s->premade_minx; sprite.miny = s->premade_miny; sprite.maxx = s->premade_maxx; sprite.maxy = s->premade_maxy; #ifdef SPRITEBATCH_SPRITE_USERDATA sprite.udata = s->udata; #endif spritebatch_internal_premade_sprite_t* premade = spritebatch_internal_premade_sprite(sb, s->image_id, &sprite); if(!premade) { void* atlas_ptr = spritebatch_map_find(&sb->sprites_to_atlases, s->image_id); if (atlas_ptr) { spritebatch_internal_atlas_t* atlas = *(spritebatch_internal_atlas_t**)atlas_ptr; sprite.texture_id = atlas->texture_id; spritebatch_internal_texture_t* tex = (spritebatch_internal_texture_t*)spritebatch_map_find(&atlas->sprites_to_textures, s->image_id); SPRITEBATCH_ASSERT(tex); tex->timestamp = 0; sprite.w = tex->w; sprite.h = tex->h; // Previously the uvs were overwritten here directly with the sprite batch texture UVs // now it expects the user code to send in local texture UVs. // Default sprites should pass in minx and miny as 0 and maxx and maxy as 1 to draw the full // texture, values above will creep into other textures of the atlas, between 0-1 will draw a portion // and below 0 will again creep into other parts of the atlas. // Example use case is to draw 9 slice sprites to be able to draw each slice with specific UVs. float dx = tex->maxx - tex->minx; float dy = tex->maxy - tex->miny; sprite.minx = dx * sprite.minx + tex->minx; sprite.miny = dy * sprite.miny + tex->miny; sprite.maxx = dx * sprite.maxx + tex->minx; sprite.maxy = dy * sprite.maxy + tex->miny; } else skipped_tex = spritebatch_internal_lonely_sprite(sb, s->image_id, s->w, s->h, &sprite, skip_missing_textures); } else { sprite.texture_id = premade->texture_id; } if (!skipped_tex) { if (sb->sprite_count >= sb->sprite_capacity) { int new_capacity = sb->sprite_capacity * 2; void* new_data = SPRITEBATCH_MALLOC(sizeof(spritebatch_sprite_t) * new_capacity, sb->mem_ctx); if (!new_data) return 0; SPRITEBATCH_MEMCPY(new_data, sb->sprites, sizeof(spritebatch_sprite_t) * sb->sprite_count); SPRITEBATCH_FREE(sb->sprites, sb->mem_ctx); sb->sprites = (spritebatch_sprite_t*)new_data; sb->sprite_capacity = new_capacity; if (sb->sprites_scratch) { SPRITEBATCH_FREE(sb->sprites_scratch, sb->mem_ctx); } if (sprite_batch_internal_use_scratch_buffer(sb)) { sb->sprites_scratch = (spritebatch_sprite_t*)SPRITEBATCH_MALLOC(sizeof(spritebatch_sprite_t) * new_capacity, sb->mem_ctx); } } sb->sprites[sb->sprite_count++] = sprite; } return skipped_tex; } void spritebatch_internal_process_input(spritebatch_t* sb, int skip_missing_textures) { int skipped_index = 0; for (int i = 0; i < sb->input_count; ++i) { spritebatch_internal_sprite_t* s = sb->input_buffer + i; int skipped = spritebatch_internal_push_sprite(sb, s, skip_missing_textures); if (skip_missing_textures && skipped) sb->input_buffer[skipped_index++] = *s; } sb->input_count = skipped_index; } void spritebatch_tick(spritebatch_t* sb) { spritebatch_internal_atlas_t* atlas = sb->atlases; if (atlas) { spritebatch_internal_atlas_t* sentinel = atlas; do { int texture_count = spritebatch_map_count(&atlas->sprites_to_textures); spritebatch_internal_texture_t* textures = (spritebatch_internal_texture_t*)spritebatch_map_items(&atlas->sprites_to_textures); for (int i = 0; i < texture_count; ++i) textures[i].timestamp += 1; atlas = atlas->next; } while (atlas != sentinel); } int texture_count = spritebatch_map_count(&sb->sprites_to_lonely_textures); spritebatch_internal_lonely_texture_t* lonely_textures = (spritebatch_internal_lonely_texture_t*)spritebatch_map_items(&sb->sprites_to_lonely_textures); for (int i = 0; i < texture_count; ++i) lonely_textures[i].timestamp += 1; } int spritebatch_flush(spritebatch_t* sb) { // process input buffer, make any necessary lonely textures // convert user sprites to internal format // lookup uv coordinates spritebatch_internal_process_input(sb, 0); // patchup any missing lonely textures that may have come from atlases decaying and whatnot int texture_count = spritebatch_map_count(&sb->sprites_to_lonely_textures); spritebatch_internal_lonely_texture_t* lonely_textures = (spritebatch_internal_lonely_texture_t*)spritebatch_map_items(&sb->sprites_to_lonely_textures); for (int i = 0; i < texture_count; ++i) { spritebatch_internal_lonely_texture_t* lonely = lonely_textures + i; if (lonely->texture_id == ~0) { lonely->texture_id = spritebatch_internal_generate_texture_handle(sb, lonely->image_id, lonely->w, lonely->h); } } // Reset each flush, since it's only used to id sprites on a per-sort basis. sb->sort_id = 0; // sort internal sprite buffer and submit batches spritebatch_internal_sort_sprites(sb); int min = 0; int max = 0; int done = !sb->sprite_count; int count = 0; while (!done) { SPRITEBATCH_U64 id = sb->sprites[min].texture_id; SPRITEBATCH_U64 image_id = sb->sprites[min].image_id; while (1) { if (max == sb->sprite_count) { done = 1; break; } if (id != sb->sprites[max].texture_id) break; ++max; } int batch_count = max - min; if (batch_count) { int w, h; spritebatch_internal_premade_sprite_t* premade = (spritebatch_internal_premade_sprite_t*)spritebatch_map_find(&sb->sprites_to_premades, image_id); if (!premade) { void* atlas_ptr = spritebatch_map_find(&sb->sprites_to_atlases, image_id); if (atlas_ptr) { w = sb->atlas_width_in_pixels; h = sb->atlas_height_in_pixels; } else { spritebatch_internal_lonely_texture_t* tex = (spritebatch_internal_lonely_texture_t*)spritebatch_map_find(&sb->sprites_to_lonely_textures, image_id); SPRITEBATCH_ASSERT(tex); w = tex->w; h = tex->h; if (sb->atlas_use_border_pixels) { w += 2; h += 2; } } } else { w = premade->atlas_w; h = premade->atlas_h; } sb->batch_callback(sb->sprites + min, batch_count, w, h, sb->udata); ++count; } min = max; } sb->sprite_count = 0; if (count > 1) { SPRITEBATCH_LOG("Flushed %d batches.\n", count); } return count; } void spritebatch_clear(spritebatch_t* sb) { sb->input_count = 0; sb->sprite_count = 0; } typedef struct { int x; int y; } spritebatch_v2_t; typedef struct { int img_index; spritebatch_v2_t size; spritebatch_v2_t min; spritebatch_v2_t max; int fit; } spritebatch_internal_integer_image_t; static spritebatch_v2_t spritebatch_v2(int x, int y) { spritebatch_v2_t v; v.x = x; v.y = y; return v; } static spritebatch_v2_t spritebatch_sub(spritebatch_v2_t a, spritebatch_v2_t b) { spritebatch_v2_t v; v.x = a.x - b.x; v.y = a.y - b.y; return v; } static spritebatch_v2_t spritebatch_add(spritebatch_v2_t a, spritebatch_v2_t b) { spritebatch_v2_t v; v.x = a.x + b.x; v.y = a.y + b.y; return v; } typedef struct { spritebatch_v2_t size; spritebatch_v2_t min; spritebatch_v2_t max; } spritebatch_internal_atlas_node_t; static spritebatch_internal_atlas_node_t* spritebatch_best_fit(int sp, int w, int h, spritebatch_internal_atlas_node_t* nodes) { int best_volume = INT_MAX; spritebatch_internal_atlas_node_t *best_node = 0; int img_volume = w * h; for ( int i = 0; i < sp; ++i ) { spritebatch_internal_atlas_node_t *node = nodes + i; int can_contain = node->size.x >= w && node->size.y >= h; if ( can_contain ) { int node_volume = node->size.x * node->size.y; if ( node_volume == img_volume ) return node; if ( node_volume < best_volume ) { best_volume = node_volume; best_node = node; } } } return best_node; } static int spritebatch_internal_image_less_than_or_equal(spritebatch_internal_integer_image_t* a, spritebatch_internal_integer_image_t* b) { int perimeterA = 2 * (a->size.x + a->size.y); int perimeterB = 2 * (b->size.x + b->size.y); return perimeterB <= perimeterA; } void spritebatch_internal_image_merge_sort_iteration(spritebatch_internal_integer_image_t* a, int lo, int split, int hi, spritebatch_internal_integer_image_t* b) { int i = lo, j = split; for (int k = lo; k < hi; k++) { if (i < split && (j >= hi || spritebatch_internal_image_less_than_or_equal(a + i, a + j))) { b[k] = a[i]; i = i + 1; } else { b[k] = a[j]; j = j + 1; } } } void spritebatch_internal_image_merge_sort_recurse(spritebatch_internal_integer_image_t* b, int lo, int hi, spritebatch_internal_integer_image_t* a) { if (hi - lo <= 1) return; int split = (lo + hi) / 2; spritebatch_internal_image_merge_sort_recurse(a, lo, split, b); spritebatch_internal_image_merge_sort_recurse(a, split, hi, b); spritebatch_internal_image_merge_sort_iteration(b, lo, split, hi, a); } void spritebatch_internal_image_merge_sort(spritebatch_internal_integer_image_t* a, spritebatch_internal_integer_image_t* b, int n) { SPRITEBATCH_MEMCPY(b, a, sizeof(spritebatch_internal_integer_image_t) * n); spritebatch_internal_image_merge_sort_recurse(b, 0, n, a); } typedef struct spritebatch_internal_atlas_image_t { int img_index; // index into the `imgs` array int w, h; // pixel w/h of original image float minx, miny; // u coordinate float maxx, maxy; // v coordinate int fit; // non-zero if image fit and was placed into the atlas } spritebatch_internal_atlas_image_t; #define SPRITEBATCH_CHECK( X, Y ) do { if ( !(X) ) { SPRITEBATCH_LOG(Y); goto sb_err; } } while ( 0 ) void spritebatch_make_atlas(spritebatch_t* sb, spritebatch_internal_atlas_t* atlas_out, const spritebatch_internal_lonely_texture_t* imgs, int img_count) { float iw, ih; int atlas_image_size, atlas_stride, sp; void* atlas_pixels = 0; int atlas_node_capacity = img_count * 2; spritebatch_internal_integer_image_t* images = 0; spritebatch_internal_integer_image_t* images_scratch = 0; spritebatch_internal_atlas_node_t* nodes = 0; int pixel_stride = sb->pixel_stride; int atlas_width = sb->atlas_width_in_pixels; int atlas_height = sb->atlas_height_in_pixels; float volume_used = 0; images = (spritebatch_internal_integer_image_t*)SPRITEBATCH_MALLOC(sizeof(spritebatch_internal_integer_image_t) * img_count, sb->mem_ctx); images_scratch = (spritebatch_internal_integer_image_t*)SPRITEBATCH_MALLOC(sizeof(spritebatch_internal_integer_image_t) * img_count, sb->mem_ctx); nodes = (spritebatch_internal_atlas_node_t*)SPRITEBATCH_MALLOC(sizeof(spritebatch_internal_atlas_node_t) * atlas_node_capacity, sb->mem_ctx); SPRITEBATCH_CHECK(images, "out of mem"); SPRITEBATCH_CHECK(nodes, "out of mem"); for (int i = 0; i < img_count; ++i) { const spritebatch_internal_lonely_texture_t* img = imgs + i; spritebatch_internal_integer_image_t* image = images + i; image->fit = 0; image->size = sb->atlas_use_border_pixels ? spritebatch_v2(img->w + 2, img->h + 2) : spritebatch_v2(img->w, img->h); image->img_index = i; } // Sort PNGs from largest to smallest spritebatch_internal_image_merge_sort(images, images_scratch, img_count); // stack pointer, the stack is the nodes array which we will // allocate nodes from as necessary. sp = 1; nodes[0].min = spritebatch_v2(0, 0); nodes[0].max = spritebatch_v2(atlas_width, atlas_height); nodes[0].size = spritebatch_v2(atlas_width, atlas_height); // Nodes represent empty space in the atlas. Placing a texture into the // atlas involves splitting a node into two smaller pieces (or, if a // perfect fit is found, deleting the node). for (int i = 0; i < img_count; ++i) { spritebatch_internal_integer_image_t* image = images + i; int width = image->size.x; int height = image->size.y; spritebatch_internal_atlas_node_t *best_fit = spritebatch_best_fit(sp, width, height, nodes); if (!best_fit) { image->fit = 0; continue; } image->min = best_fit->min; image->max = spritebatch_add(image->min, image->size); if (best_fit->size.x == width && best_fit->size.y == height) { spritebatch_internal_atlas_node_t* last_node = nodes + --sp; *best_fit = *last_node; image->fit = 1; continue; } image->fit = 1; if (sp == atlas_node_capacity) { int new_capacity = atlas_node_capacity * 2; spritebatch_internal_atlas_node_t* new_nodes = (spritebatch_internal_atlas_node_t*)SPRITEBATCH_MALLOC(sizeof(spritebatch_internal_atlas_node_t) * new_capacity, mem_ctx); SPRITEBATCH_CHECK(new_nodes, "out of mem"); memcpy(new_nodes, nodes, sizeof(spritebatch_internal_atlas_node_t) * sp); SPRITEBATCH_FREE(nodes, mem_ctx); nodes = new_nodes; atlas_node_capacity = new_capacity; } spritebatch_internal_atlas_node_t* new_node = nodes + sp++; new_node->min = best_fit->min; // Split bestFit along x or y, whichever minimizes // fragmentation of empty space spritebatch_v2_t d = spritebatch_sub(best_fit->size, spritebatch_v2(width, height)); if (d.x < d.y) { new_node->size.x = d.x; new_node->size.y = height; new_node->min.x += width; best_fit->size.y = d.y; best_fit->min.y += height; } else { new_node->size.x = width; new_node->size.y = d.y; new_node->min.y += height; best_fit->size.x = d.x; best_fit->min.x += width; } new_node->max = spritebatch_add(new_node->min, new_node->size); } // Write the final atlas image, use SPRITEBATCH_ATLAS_EMPTY_COLOR as base color atlas_stride = atlas_width * pixel_stride; atlas_image_size = atlas_width * atlas_height * pixel_stride; atlas_pixels = SPRITEBATCH_MALLOC(atlas_image_size, mem_ctx); SPRITEBATCH_CHECK(atlas_image_size, "out of mem"); SPRITEBATCH_MEMSET(atlas_pixels, SPRITEBATCH_ATLAS_EMPTY_COLOR, atlas_image_size); for (int i = 0; i < img_count; ++i) { spritebatch_internal_integer_image_t* image = images + i; if (image->fit) { const spritebatch_internal_lonely_texture_t* img = imgs + image->img_index; spritebatch_internal_get_pixels(sb, img->image_id, img->w, img->h); char* pixels = (char*)sb->pixel_buffer; spritebatch_v2_t min = image->min; spritebatch_v2_t max = image->max; int atlas_offset = min.x * pixel_stride; int tex_stride = image->size.x * pixel_stride; for (int row = min.y, y = 0; row < max.y; ++row, ++y) { void* row_ptr = (char*)atlas_pixels + (row * atlas_stride + atlas_offset); SPRITEBATCH_MEMCPY(row_ptr, pixels + y * tex_stride, tex_stride); } } } spritebatch_map_init(&atlas_out->sprites_to_textures, sizeof(spritebatch_internal_texture_t), img_count, sb->mem_ctx); atlas_out->texture_id = sb->generate_texture_callback(atlas_pixels, atlas_width, atlas_height, sb->udata); iw = 1.0f / (float)(atlas_width); ih = 1.0f / (float)(atlas_height); for (int i = 0; i < img_count; ++i) { spritebatch_internal_integer_image_t* img = images + i; if (img->fit) { spritebatch_v2_t min = img->min; spritebatch_v2_t max = img->max; volume_used += img->size.x * img->size.y; float min_x = (float)min.x * iw; float min_y = (float)min.y * ih; float max_x = (float)max.x * iw; float max_y = (float)max.y * ih; // flip image on y axis if (SPRITEBATCH_ATLAS_FLIP_Y_AXIS_FOR_UV) { float tmp = min_y; min_y = max_y; max_y = tmp; } spritebatch_internal_texture_t texture; texture.w = img->size.x; texture.h = img->size.y; texture.timestamp = 0; texture.minx = min_x; texture.miny = min_y; texture.maxx = max_x; texture.maxy = max_y; SPRITEBATCH_ASSERT(!(img->size.x < 0)); SPRITEBATCH_ASSERT(!(img->size.y < 0)); SPRITEBATCH_ASSERT(!(min_x < 0)); SPRITEBATCH_ASSERT(!(max_x < 0)); SPRITEBATCH_ASSERT(!(min_y < 0)); SPRITEBATCH_ASSERT(!(max_y < 0)); texture.image_id = imgs[img->img_index].image_id; spritebatch_map_insert(&atlas_out->sprites_to_textures, texture.image_id, &texture); } } // Need to adjust atlas_width and atlas_height in config params, as none of the images for this // atlas actually fit inside of the atlas! Either adjust the config, or stop sending giant images // to the sprite batcher. SPRITEBATCH_ASSERT(volume_used > 0); atlas_out->volume_ratio = volume_used / (atlas_width * atlas_height); sb_err: // no specific error handling needed here (yet) SPRITEBATCH_FREE(atlas_pixels, mem_ctx); SPRITEBATCH_FREE(nodes, mem_ctx); SPRITEBATCH_FREE(images, mem_ctx); SPRITEBATCH_FREE(images_scratch, mem_ctx); return; } static int spritebatch_internal_lonely_pred(spritebatch_internal_lonely_texture_t* a, spritebatch_internal_lonely_texture_t* b) { return a->timestamp < b->timestamp; } static void spritebatch_internal_qsort_lonely(spritebatch_map_t* lonely_table, spritebatch_internal_lonely_texture_t* items, int count) { if (count <= 1) return; spritebatch_internal_lonely_texture_t pivot = items[count - 1]; int low = 0; for (int i = 0; i < count - 1; ++i) { if (spritebatch_internal_lonely_pred(items + i, &pivot)) { spritebatch_map_swap(lonely_table, i, low); low++; } } spritebatch_map_swap(lonely_table, low, count - 1); spritebatch_internal_qsort_lonely(lonely_table, items, low); spritebatch_internal_qsort_lonely(lonely_table, items + low + 1, count - 1 - low); } int spritebatch_internal_buffer_key(spritebatch_t* sb, SPRITEBATCH_U64 key) { SPRITEBATCH_CHECK_BUFFER_GROW(sb, key_buffer_count, key_buffer_capacity, key_buffer, SPRITEBATCH_U64); sb->key_buffer[sb->key_buffer_count++] = key; return 0; } void spritebatch_internal_remove_table_entries(spritebatch_t* sb, spritebatch_map_t* table) { for (int i = 0; i < sb->key_buffer_count; ++i) spritebatch_map_remove(table, sb->key_buffer[i]); sb->key_buffer_count = 0; } void spritebatch_internal_flush_atlas(spritebatch_t* sb, spritebatch_internal_atlas_t* atlas, spritebatch_internal_atlas_t** sentinel, spritebatch_internal_atlas_t** next) { int ticks_to_decay_texture = sb->ticks_to_decay_texture; int texture_count = spritebatch_map_count(&atlas->sprites_to_textures); spritebatch_internal_texture_t* textures = (spritebatch_internal_texture_t*)spritebatch_map_items(&atlas->sprites_to_textures); for (int i = 0; i < texture_count; ++i) { spritebatch_internal_texture_t* atlas_texture = textures + i; if (atlas_texture->timestamp < ticks_to_decay_texture) { int w = atlas_texture->w; int h = atlas_texture->h; if (sb->atlas_use_border_pixels) { w -= 2; h -= 2; } spritebatch_internal_lonely_texture_t* lonely_texture = spritebatch_internal_lonelybuffer_push(sb, atlas_texture->image_id, w, h, 0); lonely_texture->timestamp = atlas_texture->timestamp; } spritebatch_map_remove(&sb->sprites_to_atlases, atlas_texture->image_id); } if (sb->atlases == atlas) { if (atlas->next == atlas) sb->atlases = 0; else sb->atlases = atlas->prev; } // handle loop end conditions if sentinel was removed from the chain if (sentinel && next) { if (*sentinel == atlas) { SPRITEBATCH_LOG("\t\tsentinel was also atlas: %p\n", *sentinel); if ((*next)->next != *sentinel) { SPRITEBATCH_LOG("\t\t*next = (*next)->next : %p = (*next)->%p\n", *next, (*next)->next); *next = (*next)->next; } SPRITEBATCH_LOG("\t\t*sentinel = *next : %p = %p\n", *sentinel, *next); *sentinel = *next; } } atlas->next->prev = atlas->prev; atlas->prev->next = atlas->next; spritebatch_map_term(&atlas->sprites_to_textures); sb->delete_texture_callback(atlas->texture_id, sb->udata); SPRITEBATCH_FREE(atlas, sb->mem_ctx); } void spritebatch_invalidate(spritebatch_t* sb, SPRITEBATCH_U64 image_id) { void* atlas_ptr = spritebatch_map_find(&sb->sprites_to_atlases, image_id); if (atlas_ptr) { spritebatch_internal_atlas_t* atlas = *(spritebatch_internal_atlas_t**)atlas_ptr; spritebatch_internal_flush_atlas(sb, atlas, 0, 0); } if (spritebatch_map_find(&sb->sprites_to_lonely_textures, image_id)) { spritebatch_map_remove(&sb->sprites_to_lonely_textures, image_id); } } void spritebatch_internal_log_chain(spritebatch_internal_atlas_t* atlas) { if (atlas) { spritebatch_internal_atlas_t* sentinel = atlas; SPRITEBATCH_LOG("sentinel: %p\n", sentinel); do { spritebatch_internal_atlas_t* next = atlas->next; SPRITEBATCH_LOG("\tatlas %p\n", atlas); atlas = next; } while (atlas != sentinel); } } int spritebatch_defrag(spritebatch_t* sb) { // remove decayed atlases and flush them to the lonely buffer // only flush textures that are not decayed int ticks_to_decay_texture = sb->ticks_to_decay_texture; float ratio_to_decay_atlas = sb->ratio_to_decay_atlas; spritebatch_internal_atlas_t* atlas = sb->atlases; if (atlas) { spritebatch_internal_log_chain(atlas); spritebatch_internal_atlas_t* sentinel = atlas; do { spritebatch_internal_atlas_t* next = atlas->next; int texture_count = spritebatch_map_count(&atlas->sprites_to_textures); spritebatch_internal_texture_t* textures = (spritebatch_internal_texture_t*)spritebatch_map_items(&atlas->sprites_to_textures); int decayed_texture_count = 0; for (int i = 0; i < texture_count; ++i) if (textures[i].timestamp >= ticks_to_decay_texture) decayed_texture_count++; float ratio; if (!decayed_texture_count) ratio = 0; else ratio = (float)texture_count / (float)decayed_texture_count; if (ratio > ratio_to_decay_atlas) { SPRITEBATCH_LOG("flushed atlas %p\n", atlas); spritebatch_internal_flush_atlas(sb, atlas, &sentinel, &next); } atlas = next; } while (atlas != sentinel); } // merge mostly empty atlases float ratio_to_merge_atlases = sb->ratio_to_merge_atlases; atlas = sb->atlases; if (atlas) { int sp = 0; spritebatch_internal_atlas_t* merge_stack[2]; spritebatch_internal_atlas_t* sentinel = atlas; do { spritebatch_internal_atlas_t* next = atlas->next; SPRITEBATCH_ASSERT(sp >= 0 && sp <= 2); if (sp == 2) { SPRITEBATCH_LOG("merged 2 atlases\n"); spritebatch_internal_flush_atlas(sb, merge_stack[0], &sentinel, &next); spritebatch_internal_flush_atlas(sb, merge_stack[1], &sentinel, &next); sp = 0; } float ratio = atlas->volume_ratio; if (ratio < ratio_to_merge_atlases) merge_stack[sp++] = atlas; atlas = next; } while (atlas != sentinel); if (sp == 2) { SPRITEBATCH_LOG("merged 2 atlases (out of loop)\n"); spritebatch_internal_flush_atlas(sb, merge_stack[0], 0, 0); spritebatch_internal_flush_atlas(sb, merge_stack[1], 0, 0); } } // remove decayed textures from the lonely buffer int lonely_buffer_count_till_decay = sb->lonely_buffer_count_till_decay; int lonely_count = spritebatch_map_count(&sb->sprites_to_lonely_textures); spritebatch_internal_lonely_texture_t* lonely_textures = (spritebatch_internal_lonely_texture_t*)spritebatch_map_items(&sb->sprites_to_lonely_textures); if (lonely_count >= lonely_buffer_count_till_decay) { spritebatch_internal_qsort_lonely(&sb->sprites_to_lonely_textures, lonely_textures, lonely_count); int index = 0; while (1) { if (index == lonely_count) break; if (lonely_textures[index].timestamp >= ticks_to_decay_texture) break; ++index; } for (int i = index; i < lonely_count; ++i) { SPRITEBATCH_U64 texture_id = lonely_textures[i].texture_id; if (texture_id != ~0) sb->delete_texture_callback(texture_id, sb->udata); spritebatch_internal_buffer_key(sb, lonely_textures[i].image_id); SPRITEBATCH_LOG("lonely texture decayed\n"); } spritebatch_internal_remove_table_entries(sb, &sb->sprites_to_lonely_textures); lonely_count -= lonely_count - index; SPRITEBATCH_ASSERT(lonely_count == spritebatch_map_count(&sb->sprites_to_lonely_textures)); } // process input, but don't make textures just yet spritebatch_internal_process_input(sb, 1); lonely_count = spritebatch_map_count(&sb->sprites_to_lonely_textures); // while greater than lonely_buffer_count_till_flush elements in lonely buffer // grab lonely_buffer_count_till_flush of them and make an atlas int lonely_buffer_count_till_flush = sb->lonely_buffer_count_till_flush; int stuck = 0; while (lonely_count > lonely_buffer_count_till_flush && !stuck) { atlas = (spritebatch_internal_atlas_t*)SPRITEBATCH_MALLOC(sizeof(spritebatch_internal_atlas_t), sb->mem_ctx); if (sb->atlases) { atlas->prev = sb->atlases; atlas->next = sb->atlases->next; sb->atlases->next->prev = atlas; sb->atlases->next = atlas; } else { atlas->next = atlas; atlas->prev = atlas; sb->atlases = atlas; } spritebatch_make_atlas(sb, atlas, lonely_textures, lonely_count); SPRITEBATCH_LOG("making atlas\n"); int tex_count_in_atlas = spritebatch_map_count(&atlas->sprites_to_textures); if (tex_count_in_atlas != lonely_count) { int hit_count = 0; for (int i = 0; i < lonely_count; ++i) { SPRITEBATCH_U64 key = lonely_textures[i].image_id; if (spritebatch_map_find(&atlas->sprites_to_textures, key)) { spritebatch_internal_buffer_key(sb, key); SPRITEBATCH_U64 texture_id = lonely_textures[i].texture_id; if (texture_id != ~0) sb->delete_texture_callback(texture_id, sb->udata); spritebatch_map_insert(&sb->sprites_to_atlases, key, &atlas); SPRITEBATCH_LOG("removing lonely texture for atlas%s\n", texture_id != ~0 ? "" : " (tex was ~0)" ); } else { hit_count++; SPRITEBATCH_ASSERT(lonely_textures[i].w <= sb->atlas_width_in_pixels); SPRITEBATCH_ASSERT(lonely_textures[i].h <= sb->atlas_height_in_pixels); } } spritebatch_internal_remove_table_entries(sb, &sb->sprites_to_lonely_textures); lonely_count = spritebatch_map_count(&sb->sprites_to_lonely_textures); if (!hit_count) { // TODO // handle case where none fit in atlas stuck = 1; SPRITEBATCH_ASSERT(0); } } else { for (int i = 0; i < lonely_count; ++i) { SPRITEBATCH_U64 key = lonely_textures[i].image_id; SPRITEBATCH_U64 texture_id = lonely_textures[i].texture_id; if (texture_id != ~0) sb->delete_texture_callback(texture_id, sb->udata); spritebatch_map_insert(&sb->sprites_to_atlases, key, &atlas); SPRITEBATCH_LOG("(fast path) removing lonely texture for atlas%s\n", texture_id != ~0 ? "" : " (tex was ~0)" ); } spritebatch_map_clear(&sb->sprites_to_lonely_textures); lonely_count = 0; break; } } return 1; } #endif // SPRITEBATCH_IMPLEMENTATION_ONCE #endif // SPRITEBATCH_IMPLEMENTATION /* ------------------------------------------------------------------------------ This software is available under 2 licenses - you may choose the one you like. ------------------------------------------------------------------------------ ALTERNATIVE A - zlib license Copyright (c) 2026 Randy Gaul https://randygaul.github.io/ This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. ------------------------------------------------------------------------------ ALTERNATIVE B - Public Domain (www.unlicense.org) This is free and unencumbered software released into the public domain. Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ------------------------------------------------------------------------------ */