浏览代码

Update sokol_spritebatch.h

Reworked the API to use contexts. Removed internal sorting of sprites (for now, at least). Adjusted naming. Introduced push_sprite_rect which lets you create a sprite to be rendered at a specific destination rectangle.
Stuart Adams 4 年之前
父节点
当前提交
7cfc3c9e86
共有 1 个文件被更改,包括 829 次插入545 次删除
  1. 829 545
      util/sokol_spritebatch.h

+ 829 - 545
util/sokol_spritebatch.h

@@ -1,4 +1,4 @@
-#if defined(SOKOL_IMPL) && !defined(SOKOL_SPRITEBATCH_IMPL)
+#if defined(SOKOL_IMPL) && !defined(SOKOL_SPRITEBATCH_IMPL)
 #define SOKOL_SPRITEBATCH_IMPL
 #endif
 
@@ -22,70 +22,92 @@
 #endif
 #endif
 
-typedef enum sb_sprite_flags {
-    SB_FLIP_NONE = 0,
-    SB_FLIP_X    = 1 << 0,
-    SB_FLIP_Y    = 1 << 1,
-    SB_FLIP_BOTH = SB_FLIP_Y | SB_FLIP_X,
-    SB_Z_TILT    = 1<< 2
-} sb_sprite_flags;
-
-typedef enum sb_sort_mode {
-    SB_SORT_MODE_DEFERRED,
-    SB_SORT_MODE_TEXTURE,
-    SB_SORT_MODE_BACK_TO_FRONT,
-    SB_SORT_MODE_FRONT_TO_BACK
-} sb_sort_mode;
-
-typedef struct sb_float2 {
-    float x;
-    float y;
-} sb_float2;
-
-typedef struct sb_sprite_info {
-    sg_image  image;
-    float     width;
-    float     height;
-    sb_float2 position;
-    float     depth;
-    float     rotation;
-    sb_float2 origin;
-    sb_float2 scale;
-    uint32_t  flags;
-    sb_float2 source;
-    sg_color  color;
-} sb_sprite_info;
-
-typedef struct sb_desc {
-    int max_quads;
-} sb_desc;
-
-typedef struct sb_matrix {
-  float m[4][4];
-} sb_matrix;
-
-typedef struct sb_viewport {
-    int x, y, width, height;
-    bool origin_top_left;
-} sb_viewport;
-
-typedef struct sb_render_state {
-    sb_sort_mode sort_mode;
-    sg_pipeline  pipeline;
-    sb_matrix    transform_matrix;
-    sb_viewport  viewport;
-} sb_render_state;
-
-SOKOL_SPRITEBATCH_API_DECL void sb_setup(const sb_desc* desc);
-SOKOL_SPRITEBATCH_API_DECL void sb_shutdown(void);
-
-SOKOL_SPRITEBATCH_API_DECL void sb_begin(const sb_render_state* render_state);
-SOKOL_SPRITEBATCH_API_DECL void sb_sprite(const sb_sprite_info* sprite);
-SOKOL_SPRITEBATCH_API_DECL void sb_end(void);
-
-SOKOL_SPRITEBATCH_API_DECL void sb_draw(void);
-
-SOKOL_SPRITEBATCH_API_DECL void sb_premultiply_alpha(uint8_t* pixels, int pixel_count);
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+    typedef enum sbatch_sprite_flags {
+        SBATCH_FLIP_NONE = 0,
+        SBATCH_FLIP_X = 1 << 0,
+        SBATCH_FLIP_Y = 1 << 1,
+        SBATCH_FLIP_BOTH = SBATCH_FLIP_Y | SBATCH_FLIP_X
+    } sbatch_sprite_flags;
+
+    typedef struct sbatch_float2 {
+        float x;
+        float y;
+    } sbatch_float2;
+
+    typedef struct sbatch_rect {
+        float x;
+        float y;
+        float width;
+        float height;
+    } sbatch_rect;
+
+    typedef struct sbatch_sprite {
+        sg_image      image;
+        sbatch_float2 position;
+        sbatch_rect   source;
+        sg_color      color;
+        float         rotation;
+        sbatch_float2 origin;
+        sbatch_float2 scale;
+        uint32_t      flags;
+        float         depth;
+    } sbatch_sprite;
+
+    typedef struct sbatch_sprite_rect {
+        sg_image      image;
+        sbatch_rect   destination;
+        sbatch_rect   source;
+        sg_color      color;
+        float         rotation;
+        sbatch_float2 origin;
+        uint32_t      flags;
+        float         depth;
+    } sbatch_sprite_rect;
+
+    typedef struct sbatch_desc {
+        int context_pool_size;
+        sg_pixel_format color_format;
+        sg_pixel_format depth_format;
+        int sample_count;
+    } sbatch_desc;
+
+    typedef struct sbatch_context {
+        uint32_t id;
+    } sbatch_context;
+
+    enum
+    {
+        SBATCH_MAX_SPRITES = (1 << 16) / 4,
+        SBATCH_DEFAULT_SPRITES = SBATCH_MAX_SPRITES / 4
+    };
+
+    typedef struct sbatch_context_desc {
+        int canvas_width;
+        int canvas_height;
+        int max_sprites;
+        sg_pipeline pipeline;
+        const char* label;
+    } sbatch_context_desc;
+
+    SOKOL_SPRITEBATCH_API_DECL void sbatch_setup(const sbatch_desc* desc);
+    SOKOL_SPRITEBATCH_API_DECL void sbatch_shutdown(void);
+    SOKOL_SPRITEBATCH_API_DECL int sbatch_frame();
+
+    SOKOL_SPRITEBATCH_API_DECL sbatch_context sbatch_make_context(const sbatch_context_desc* desc);
+    SOKOL_SPRITEBATCH_API_DECL void sbatch_destroy_context(sbatch_context context);
+
+    SOKOL_SPRITEBATCH_API_DECL void sbatch_begin(sbatch_context context);
+    SOKOL_SPRITEBATCH_API_DECL void sbatch_push_sprite(const sbatch_sprite* sprite);
+    SOKOL_SPRITEBATCH_API_DECL void sbatch_push_sprite_rect(const sbatch_sprite_rect* sprite);
+    SOKOL_SPRITEBATCH_API_DECL void sbatch_end(void);
+
+    SOKOL_SPRITEBATCH_API_DECL void sbatch_apply_fs_uniforms(int ub_index, const sg_range* data);
+
+    SOKOL_SPRITEBATCH_API_DECL void sbatch_premultiply_alpha_rgba8(uint8_t* pixels, int pixel_count);
 
 #ifdef __cplusplus
 } /* extern "C" */
@@ -101,36 +123,50 @@ SOKOL_SPRITEBATCH_API_DECL void sb_premultiply_alpha(uint8_t* pixels, int pixel_
 #include <math.h>   /* sinf, cosf */
 
 #ifndef SOKOL_API_IMPL
-    #define SOKOL_API_IMPL
+#define SOKOL_API_IMPL
 #endif
 
 #ifndef SOKOL_ASSERT
-    #include <assert.h>
-    #define SOKOL_ASSERT(c) assert(c)
+#include <assert.h>
+#define SOKOL_ASSERT(c) assert(c)
 #endif
 
 #ifndef SOKOL_MALLOC
-    #include <stdlib.h>
-    #define SOKOL_MALLOC(s) malloc(s)
-    #define SOKOL_FREE(result) free(result)
+#include <stdlib.h>
+#define SOKOL_MALLOC(s) malloc(s)
+#define SOKOL_FREE(result) free(result)
 #endif
 
-#define _SB_IMAGE_SLOT_MASK (0xFFFF)
-#define _SB_DEFAULT(val, def) (((val) == 0) ? (def) : (val))
-#define _SB_MAX_VERTICES (1 << 16)
-#define _SB_MAX_QUADS (_SB_MAX_VERTICES / 4)
-#define _SB_MAX_INDICES (_SB_MAX_QUADS * 6)
-#define _SB_INITIAL_BATCH_CAPACITY 32
+#ifndef SOKOL_DEBUG
+#ifndef NDEBUG
+#define SOKOL_DEBUG (1)
+#endif
+#endif
 
-#ifndef SB_MAX_DEPTH
-#define SB_MAX_DEPTH 1000.0f
+#ifndef SOKOL_LOG
+#ifdef SOKOL_DEBUG
+#include <stdio.h>
+#define SOKOL_LOG(s) { SOKOL_ASSERT(s); puts(s); }
+#else
+#define SOKOL_LOG(s)
+#endif
 #endif
 
-typedef struct {
-    sg_image image;
-    int      width;
-    int      height;
-} _sb_sprite_data;
+#define _SBATCH_IMAGE_SLOT_MASK (0xFFFF)
+
+#define _SBATCH_DEFAULT(val, def) (((val) == 0) ? (def) : (val))
+
+#define _SBATCH_MAX_VERTICES (1 << 16)
+#define _SBATCH_MAX_QUADS (_SBATCH_MAX_VERTICES / 4)
+#define _SBATCH_MAX_INDICES (_SBATCH_MAX_QUADS * 6)
+
+#define _SBATCH_INITIAL_BATCH_CAPACITY (32)
+
+#define _SBATCH_INVALID_SLOT_INDEX (0)
+#define _SBATCH_SLOT_SHIFT (16)
+#define _SBATCH_MAX_POOL_SIZE (1<<_SBATCH_SLOT_SHIFT)
+#define _SBATCH_SLOT_MASK (_SBATCH_MAX_POOL_SIZE-1)
+#define _SBATCH_STRBUF_LEN (96)
 
 typedef struct {
     float    x;
@@ -139,55 +175,81 @@ typedef struct {
     float    u;
     float    v;
     uint32_t rgba;
-} _sb_vertex;
+} _sbatch_vertex;
 
 typedef struct {
-    _sb_vertex top_left;
-    _sb_vertex top_right;
-    _sb_vertex bottom_left;
-    _sb_vertex bottom_right;
-    sg_image   image;
-    uint64_t   sort_key;
-} _sb_quad;
+    uint32_t id;
+    sg_resource_state state;
+} _sbatch_slot;
 
 typedef struct {
-    _sb_sprite_data* data;
-    size_t           size;
-} _sb_sprite_pool;
+    char buf[_SBATCH_STRBUF_LEN];
+} _sbatch_str;
+
+typedef struct _sbatch_fs_uniform_state {
+    int ub_index;
+    const sg_range* data;
+} _sbatch_fs_uniform_state;
+
+typedef struct {
+    _sbatch_str label;
+    _sbatch_slot slot;
+    sbatch_context_desc desc;
+    int sprite_count;
+    _sbatch_vertex* vertices;
+    sg_image* images;
+    sg_buffer vertex_buffer;
+    sg_pipeline pipeline;
+    int update_frame_index;
+    _sbatch_fs_uniform_state fs_uniform_state;
+} _sbatch_context;
 
 typedef struct {
-    sg_image  image;
-    int       base_element;
-    int       num_elements;
-    sb_matrix matrix;
-} _sb_batch;
+    int size;
+    int queue_top;
+    uint32_t* gen_ctrs;
+    int* free_queue;
+} _sbatch_pool;
 
 typedef struct {
-    size_t     batch_size;
-    size_t     batch_capacity;
-    _sb_batch* batches;
-} _sb_batch_data;
+    _sbatch_pool pool;
+    _sbatch_context* contexts;
+} _sbatch_context_pool;
 
 typedef struct {
-    _sb_sprite_pool sprite_pool;
-    _sb_quad*       quads;
-    size_t          quad_count;
-    _sb_vertex*     vertex_buffer_data;
-    sg_buffer       vertex_buffer;
-    sg_buffer       index_buffer;
-    _sb_batch_data  batch_data;
-    sg_bindings     bindings;
-    bool            begin_called;
-    sb_render_state render_state;
-    sg_shader       default_shader;
-    sg_pipeline     default_pipeline;
-    sb_matrix       projection_matrix;
-} _sb_t;
-
-static _sb_t _sb;
+    sg_image image;
+    int      width;
+    int      height;
+    float    texel_width;
+    float    texel_height;
+} _sbatch_sprite_data;
+
+typedef struct {
+    _sbatch_sprite_data* data;
+    size_t           size;
+} _sbatch_sprite_pool;
+
+typedef struct {
+    bool begin_called;
+    _sbatch_slot slot;
+    sg_bindings bindings;
+    sg_shader shader;
+    sg_pipeline pipeline;
+    sbatch_context ctx_id;
+    _sbatch_context_pool context_pool;
+    _sbatch_sprite_pool sprite_pool;
+    sg_buffer index_buffer;
+    int frame_index;
+} _sbatch_t;
+
+typedef struct {
+    float m[4][4];
+} _sbatch_mat4x4;
+
+static _sbatch_t _sbatch;
 
 #if defined(SOKOL_D3D11)
-static const uint8_t vs_bytecode_hlsl4[884] = {
+static const uint8_t vs_bytecode[884] = {
     0x44,0x58,0x42,0x43,0x5f,0x8c,0xaf,0xe1,0x5e,0x2d,0xba,0x0e,0x85,0xba,0xeb,0xc5,
     0x0c,0x64,0x6d,0x0c,0x01,0x00,0x00,0x00,0x74,0x03,0x00,0x00,0x05,0x00,0x00,0x00,
     0x34,0x00,0x00,0x00,0xf4,0x00,0x00,0x00,0x58,0x01,0x00,0x00,0xc8,0x01,0x00,0x00,
@@ -246,7 +308,7 @@ static const uint8_t vs_bytecode_hlsl4[884] = {
     0x00,0x00,0x00,0x00,
 };
 
-static const uint8_t fs_bytecode_hlsl4[620] = {
+static const uint8_t fs_bytecode[620] = {
     0x44,0x58,0x42,0x43,0xd1,0x93,0x1f,0x1b,0x9d,0x70,0x90,0xeb,0xc2,0x7c,0x26,0x07,
     0xdf,0x52,0xda,0x49,0x01,0x00,0x00,0x00,0x6c,0x02,0x00,0x00,0x05,0x00,0x00,0x00,
     0x34,0x00,0x00,0x00,0xd4,0x00,0x00,0x00,0x20,0x01,0x00,0x00,0x54,0x01,0x00,0x00,
@@ -287,11 +349,8 @@ static const uint8_t fs_bytecode_hlsl4[620] = {
     0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
     0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
 };
-#if !defined(SOKOL_GFX_INCLUDED)
-#error "Please include sokol_gfx.h before spritebatch-sapp.glsl.h"
-#endif
 
-static inline const sg_shader_desc* spritebatch_shader_desc(sg_backend backend) {
+static const sg_shader_desc* spritebatch_shader_desc(sg_backend backend) {
     if (backend == SG_BACKEND_D3D11) {
         static sg_shader_desc desc;
         static bool valid;
@@ -303,11 +362,11 @@ static inline const sg_shader_desc* spritebatch_shader_desc(sg_backend backend)
             desc.attrs[1].sem_index = 1;
             desc.attrs[2].sem_name = "TEXCOORD";
             desc.attrs[2].sem_index = 2;
-            desc.vs.bytecode.ptr = vs_bytecode_hlsl4;
+            desc.vs.bytecode.ptr = vs_bytecode;
             desc.vs.bytecode.size = 884;
             desc.vs.entry = "main";
             desc.vs.uniform_blocks[0].size = 64;
-            desc.fs.bytecode.ptr = fs_bytecode_hlsl4;
+            desc.fs.bytecode.ptr = fs_bytecode;
             desc.fs.bytecode.size = 620;
             desc.fs.entry = "main";
             desc.fs.images[0].name = "tex";
@@ -322,531 +381,756 @@ static inline const sg_shader_desc* spritebatch_shader_desc(sg_backend backend)
 
 #endif
 
-static inline bool _sb_matrix_is_null(const sb_matrix* m) {
-    for (int y = 0; y < 4; y++) {
-        for (int x = 0; x < 4; x++) {
-            if (0.0f != m->m[y][x]) {
-                return false;
-            }
-        }
+static void _sbatch_strcpy(_sbatch_str* dst, const char* src) {
+    SOKOL_ASSERT(dst);
+    if (src) {
+#if defined(_MSC_VER)
+        strncpy_s(dst->buf, _SBATCH_STRBUF_LEN, src, (_SBATCH_STRBUF_LEN - 1));
+#else
+        strncpy(dst->buf, src, SG_IMGUI_STRBUF_LEN);
+#endif
+        dst->buf[_SBATCH_STRBUF_LEN - 1] = 0;
+    }
+    else {
+        memset(dst->buf, 0, _SBATCH_STRBUF_LEN);
     }
-    return true;
 }
 
-static inline sb_matrix _sb_matrix_identity(void) {
-    sb_matrix m = {
-        {
-            { 1.0f, 0.0f, 0.0f, 0.0f },
-            { 0.0f, 1.0f, 0.0f, 0.0f },
-            { 0.0f, 0.0f, 1.0f, 0.0f },
-            { 0.0f, 0.0f, 0.0f, 1.0f }
-        }
-    };
-    return m;
+static _sbatch_str _sbatch_make_str(const char* str) {
+    _sbatch_str res;
+    _sbatch_strcpy(&res, str);
+    return res;
 }
 
-static inline sb_matrix _sb_orthographic_off_center(float left, float right, float bottom, float top, float near, float far) {
-    sb_matrix result;
+/*=== CONTEXT POOL ===========================================================*/
+static void _sbatch_init_pool(_sbatch_pool* pool, int num) {
+    SOKOL_ASSERT(pool && (num >= 1));
+    /* slot 0 is reserved for the 'invalid id', so bump the pool size by 1 */
+    pool->size = num + 1;
+    pool->queue_top = 0;
+    /* generation counters indexable by pool slot index, slot 0 is reserved */
+    const size_t gen_ctrs_size = sizeof(uint32_t) * (size_t)pool->size;
+    pool->gen_ctrs = (uint32_t*)SOKOL_MALLOC(gen_ctrs_size);
+    SOKOL_ASSERT(pool->gen_ctrs);
+    memset(pool->gen_ctrs, 0, gen_ctrs_size);
+    /* it's not a bug to only reserve 'num' here */
+    pool->free_queue = (int*)SOKOL_MALLOC(sizeof(int) * (size_t)num);
+    SOKOL_ASSERT(pool->free_queue);
+    /* never allocate the zero-th pool item since the invalid id is 0 */
+    for (int i = pool->size - 1; i >= 1; i--) {
+        pool->free_queue[pool->queue_top++] = i;
+    }
+}
 
-    result.m[0][0] = 2.0f / (right - left);
-    result.m[0][1] = 0.0f;
-    result.m[0][2] = 0.0f;
-    result.m[0][3] = 0.0f;
+static void _sbatch_discard_pool(_sbatch_pool* pool) {
+    SOKOL_ASSERT(pool);
+    SOKOL_ASSERT(pool->free_queue);
+    SOKOL_FREE(pool->free_queue);
+    pool->free_queue = 0;
+    SOKOL_ASSERT(pool->gen_ctrs);
+    SOKOL_FREE(pool->gen_ctrs);
+    pool->gen_ctrs = 0;
+    pool->size = 0;
+    pool->queue_top = 0;
+}
 
-    result.m[1][0] = 0.0f;
-    result.m[1][1] = 2.0f / (top - bottom);
-    result.m[1][2] = 0.0f;
-    result.m[1][3] = 0.0f;
+static int _sbatch_pool_alloc_index(_sbatch_pool* pool) {
+    SOKOL_ASSERT(pool);
+    SOKOL_ASSERT(pool->free_queue);
+    if (pool->queue_top > 0) {
+        const int slot_index = pool->free_queue[--pool->queue_top];
+        SOKOL_ASSERT((slot_index > 0) && (slot_index < pool->size));
+        return slot_index;
+    }
+    /* pool exhausted */
+    return _SBATCH_INVALID_SLOT_INDEX;
+}
 
-    result.m[2][0] = 0.0f;
-    result.m[2][1] = 0.0f;
-    result.m[2][2] = 1.0f / (near - far);
-    result.m[2][3] = 0.0f;
+static void _sbatch_pool_free_index(_sbatch_pool* pool, int slot_index) {
+    SOKOL_ASSERT((slot_index > _SBATCH_INVALID_SLOT_INDEX) && (slot_index < pool->size));
+    SOKOL_ASSERT(pool);
+    SOKOL_ASSERT(pool->free_queue);
+    SOKOL_ASSERT(pool->queue_top < pool->size);
+#ifdef SOKOL_DEBUG
+    /* debug check against double-free */
+    for (int i = 0; i < pool->queue_top; i++) {
+        SOKOL_ASSERT(pool->free_queue[i] != slot_index);
+    }
+#endif
+    pool->free_queue[pool->queue_top++] = slot_index;
+    SOKOL_ASSERT(pool->queue_top <= (pool->size - 1));
+}
 
-    result.m[3][0] = (left + right) / (left - right);
-    result.m[3][1] = (bottom + top) / (bottom - top);
-    result.m[3][2] = near / (near - far);
-    result.m[3][3] = 1.0f;
+static void _sbatch_setup_context_pool(const sbatch_desc* desc) {
+    SOKOL_ASSERT(desc);
+    /* note: the pool will have an additional item, since slot 0 is reserved */
+    SOKOL_ASSERT((desc->context_pool_size > 0) && (desc->context_pool_size < _SBATCH_MAX_POOL_SIZE));
+    _sbatch_init_pool(&_sbatch.context_pool.pool, desc->context_pool_size);
+    const size_t pool_byte_size = sizeof(_sbatch_context) * (size_t)_sbatch.context_pool.pool.size;
+    _sbatch.context_pool.contexts = (_sbatch_context*)SOKOL_MALLOC(pool_byte_size);
+    SOKOL_ASSERT(_sbatch.context_pool.contexts);
+    memset(_sbatch.context_pool.contexts, 0, pool_byte_size);
+}
 
-    return result;
+static void _sbatch_discard_context_pool(void) {
+    SOKOL_ASSERT(_sbatch.context_pool.contexts);
+    SOKOL_FREE(_sbatch.context_pool.contexts);
+    _sbatch.context_pool.contexts = 0;
+    _sbatch_discard_pool(&_sbatch.context_pool.pool);
 }
 
-static inline int _sg_image_slot_index(uint32_t id) {
-    int slot_index = (int)(id & _SB_IMAGE_SLOT_MASK);
-    SOKOL_ASSERT(0 != slot_index);
+/* allocate the slot at slot_index:
+    - bump the slot's generation counter
+    - create a resource id from the generation counter and slot index
+    - set the slot's id to this id
+    - set the slot's state to ALLOC
+    - return the resource id
+*/
+static uint32_t _sbatch_slot_alloc(_sbatch_pool* pool, _sbatch_slot* slot, int slot_index) {
+    /* FIXME: add handling for an overflowing generation counter,
+       for now, just overflow (another option is to disable
+       the slot)
+    */
+    SOKOL_ASSERT(pool && pool->gen_ctrs);
+    SOKOL_ASSERT((slot_index > _SBATCH_INVALID_SLOT_INDEX) && (slot_index < pool->size));
+    SOKOL_ASSERT((slot->state == SG_RESOURCESTATE_INITIAL) && (slot->id == SG_INVALID_ID));
+    const uint32_t ctr = ++pool->gen_ctrs[slot_index];
+    slot->id = (ctr << _SBATCH_SLOT_SHIFT) | (slot_index & _SBATCH_SLOT_MASK);
+    slot->state = SG_RESOURCESTATE_ALLOC;
+    return slot->id;
+}
+
+/* extract slot index from id */
+static int _sbatch_slot_index(uint32_t id) {
+    const int slot_index = (int)(id & _SBATCH_SLOT_MASK);
+    SOKOL_ASSERT(_SBATCH_INVALID_SLOT_INDEX != slot_index);
     return slot_index;
 }
 
-static inline void _sb_init_sprite_pool(void) {
-    sg_desc sg_desc = sg_query_desc();
-    int num_slots = sg_desc.image_pool_size;
-    _sb.sprite_pool.size = (size_t)num_slots * sizeof(_sb_sprite_data);
-    _sb.sprite_pool.data = (_sb_sprite_data*)SOKOL_MALLOC(_sb.sprite_pool.size);
-    SOKOL_ASSERT(_sb.sprite_pool.data);
-    memset(_sb.sprite_pool.data, 0, _sb.sprite_pool.size);
+/* get context pointer without id-check */
+static _sbatch_context* _sbatch_context_at(uint32_t ctx_id) {
+    SOKOL_ASSERT(SG_INVALID_ID != ctx_id);
+    const int slot_index = _sbatch_slot_index(ctx_id);
+    SOKOL_ASSERT((slot_index > _SBATCH_INVALID_SLOT_INDEX) && (slot_index < _sbatch.context_pool.pool.size));
+    return &_sbatch.context_pool.contexts[slot_index];
 }
 
-static inline void _sb_init_batch_data(void) {
-    _sb.batch_data.batch_capacity = _SB_INITIAL_BATCH_CAPACITY;
-    _sb.batch_data.batches = (_sb_batch*)SOKOL_MALLOC(_SB_INITIAL_BATCH_CAPACITY * sizeof(_sb_batch));
+/* get context pointer with id-check, returns 0 if no match */
+static _sbatch_context* _sbatch_lookup_context(uint32_t ctx_id) {
+    if (SG_INVALID_ID != ctx_id) {
+        _sbatch_context* ctx = _sbatch_context_at(ctx_id);
+        if (ctx->slot.id == ctx_id) {
+            return ctx;
+        }
+    }
+    return NULL;
 }
 
-static inline void _sb_init_index_buffer(void) {
-    uint16_t* index_buffer = (uint16_t*)SOKOL_MALLOC(_SB_MAX_INDICES * sizeof(uint16_t));
-    SOKOL_ASSERT(index_buffer);
+/* make context handle from raw uint32_t id */
+static sbatch_context _sbatch_make_ctx_id(uint32_t ctx_id) {
+    sbatch_context ctx;
+    ctx.id = ctx_id;
+    return ctx;
+}
 
-    uint16_t* index_ptr = index_buffer;
-    for (uint32_t i = 0; i < _SB_MAX_QUADS; i++, index_ptr += 6) {
-        // Triangle 1
-        *(index_ptr + 0) = (uint16_t)(i * 4);
-        *(index_ptr + 1) = (uint16_t)(i * 4 + 1);
-        *(index_ptr + 2) = (uint16_t)(i * 4 + 2);
-        // Triangle 2
-        *(index_ptr + 3) = (uint16_t)(i * 4 + 1);
-        *(index_ptr + 4) = (uint16_t)(i * 4 + 3);
-        *(index_ptr + 5) = (uint16_t)(i * 4 + 2);
+static sbatch_context _sbatch_alloc_context(void) {
+    sbatch_context ctx_id;
+    const int slot_index = _sbatch_pool_alloc_index(&_sbatch.context_pool.pool);
+    if (_SBATCH_INVALID_SLOT_INDEX != slot_index) {
+        ctx_id = _sbatch_make_ctx_id(_sbatch_slot_alloc(&_sbatch.context_pool.pool, &_sbatch.context_pool.contexts[slot_index].slot, slot_index));
     }
+    else {
+        /* pool is exhausted */
+        ctx_id = _sbatch_make_ctx_id(SG_INVALID_ID);
+    }
+    return ctx_id;
+}
 
-    sg_buffer_desc index_buffer_desc;
-    memset(&index_buffer_desc, 0, sizeof(index_buffer_desc));
-    index_buffer_desc.size      = _SB_MAX_INDICES * sizeof(uint16_t);
-    index_buffer_desc.type      = SG_BUFFERTYPE_INDEXBUFFER;
-    index_buffer_desc.usage     = SG_USAGE_IMMUTABLE;
-    index_buffer_desc.label     = "sokol-spritebatch-indices";
-    index_buffer_desc.data.size = _SB_MAX_INDICES * sizeof(uint16_t);
-    index_buffer_desc.data.ptr  = index_buffer;
-    _sb.index_buffer = sg_make_buffer(&index_buffer_desc);
-    SOKOL_ASSERT(SG_INVALID_ID != _sb.index_buffer.id);
+static sbatch_context_desc _sbatch_context_desc_defaults(const sbatch_context_desc* desc) {
+    sbatch_context_desc res = *desc;
+    res.max_sprites = _SBATCH_DEFAULT(res.max_sprites, 4096);
+    res.canvas_height = _SBATCH_DEFAULT(res.canvas_height, 480);
+    res.canvas_width = _SBATCH_DEFAULT(res.canvas_width, 640);
+    return res;
+}
 
-    SOKOL_FREE(index_buffer);
+static void _sbatch_init_context(sbatch_context ctx_id, const sbatch_context_desc* in_desc) {
+    sg_push_debug_group("sokol-spritebatch");
 
-    _sb.bindings.index_buffer = _sb.index_buffer;
-}
+    SOKOL_ASSERT((ctx_id.id != SG_INVALID_ID) && in_desc);
 
-static inline void _sb_init_quad_rotated(
-    _sb_quad* quad, float x, float y, float dx, float dy,
-    float w, float h, float sin, float cos, uint32_t rgba,
-    sb_float2 top_left, sb_float2 bottom_right, float depth) {
-
-    /* TODO: Z-tilt: what the hell do we do? */
-
-    quad->top_left.x    = x + dx * cos - dy * sin;
-    quad->top_left.y    = y + dx * sin + dy * cos;
-    quad->top_left.z    = depth;
-    quad->top_left.rgba = rgba;
-    quad->top_left.u    = top_left.x;
-    quad->top_left.v    = top_left.y;
-
-    quad->top_right.x    = x + (dx + w) * cos - dy * sin;
-    quad->top_right.y    = y + (dx + w) * sin + dy * cos;
-    quad->top_right.z    = depth;
-    quad->top_right.rgba = rgba;
-    quad->top_right.u    = bottom_right.x;
-    quad->top_right.v    = top_left.y;
-
-    quad->bottom_left.x    = x + dx * cos - (dy + h) * sin;
-    quad->bottom_left.y    = y + dx * sin + (dy + h) * cos;
-    quad->bottom_left.z    = depth;
-    quad->bottom_left.rgba = rgba;
-    quad->bottom_left.u    = top_left.x;
-    quad->bottom_left.v    = bottom_right.y;
-
-    quad->bottom_right.x    = x + (dx + w) * cos - (dy + h) * sin;
-    quad->bottom_right.y    = y + (dx + w) * sin + (dy + h) * cos;
-    quad->bottom_right.z    = depth;
-    quad->bottom_right.rgba = rgba;
-    quad->bottom_right.u    = bottom_right.x;
-    quad->bottom_right.v    = bottom_right.y;
-}
-
-static inline void _sb_init_quad(
-    _sb_quad* quad, uint32_t flags, float x, float y, float w, float h, uint32_t rgba,
-    sb_float2 top_left, sb_float2 bottom_right, float depth) {
-
-    quad->top_left.x = x;
-    quad->top_left.y = y;
-    quad->top_left.z = depth;
-    quad->top_left.rgba = rgba;
-    quad->top_left.u = top_left.x;
-    quad->top_left.v = top_left.y;
-
-    quad->top_right.x = x + w;
-    quad->top_right.y = y;
-    quad->top_right.z = depth;
-    quad->top_right.rgba = rgba;
-    quad->top_right.u = bottom_right.x;
-    quad->top_right.v = top_left.y;
-
-    if ((flags & SB_Z_TILT) != SB_FLIP_NONE) {
-        // move the topmost vertices further out to enable z-tilting
-        const float angle = 0.785398f; // 45 degrees
-        const float depth = h * tanf(angle);
-        quad->top_left.z  -= depth;
-        quad->top_right.z -= depth;
-    }
+    _sbatch_context* ctx = _sbatch_lookup_context(ctx_id.id);
+    SOKOL_ASSERT(ctx);
 
-    quad->bottom_left.x    = x;
-    quad->bottom_left.y    = y + h;
-    quad->bottom_left.z    = depth;
-    quad->bottom_left.rgba = rgba;
-    quad->bottom_left.u    = top_left.x;
-    quad->bottom_left.v    = bottom_right.y;
-
-    quad->bottom_right.x    = x + w;
-    quad->bottom_right.y    = y + h;
-    quad->bottom_right.z    = depth;
-    quad->bottom_right.rgba = rgba;
-    quad->bottom_right.u    = bottom_right.x;
-    quad->bottom_right.v    = bottom_right.y;
-}
-
-static inline void _sb_init_vertex_buffer(void) {
-    sg_buffer_desc vertex_buffer_desc;
-    memset(&vertex_buffer_desc, 0, sizeof(vertex_buffer_desc));
-    vertex_buffer_desc.size = _SB_MAX_VERTICES * sizeof(_sb_vertex);
-    vertex_buffer_desc.type = SG_BUFFERTYPE_VERTEXBUFFER;
-    vertex_buffer_desc.usage = SG_USAGE_STREAM;
-    vertex_buffer_desc.label = "sokol-spritebatch-vertices";
-    _sb.vertex_buffer = sg_make_buffer(&vertex_buffer_desc);
-    SOKOL_ASSERT(SG_INVALID_ID != _sb.vertex_buffer.id);
-
-    _sb.vertex_buffer_data = (_sb_vertex*)SOKOL_MALLOC(_SB_MAX_VERTICES * sizeof(_sb_vertex));
-    SOKOL_ASSERT(_sb.vertex_buffer_data);
-
-    _sb.bindings.vertex_buffers[0] = _sb.vertex_buffer;
-}
-
-static inline uint32_t _sb_float_flip(uint32_t f) {
-    uint32_t mask = -(int32_t)(f >> 31) | 0x80000000;
-    return f ^ mask;
-}
-
-static inline uint32_t _sb_depth_to_bits(float value) {
-    // https://aras-p.info/blog/2014/01/16/rough-sorting-by-depth/
-    // Taking highest 10 bits for rough sort of positive floats.
-    // Sign is always zero, so only 9 bits in the result are used.
-    // 0.01 maps to 240; 0.1 to 247; 1.0 to 254; 10.0 to 260;
-    // 100.0 to 267; 1000.0 to 273 etc.
-    uint32_t i;
-    memcpy(&i, &value, sizeof(value));
-    i = _sb_float_flip(i);
-    return i >> 22; // take highest 10 bits
-}
-
-static inline uint64_t _sb_make_sort_key(const sb_sprite_info* sprite) {
-    switch (_sb.render_state.sort_mode) {
-    case SB_SORT_MODE_TEXTURE: {
-        return (uint64_t)sprite->image.id;
-    }
-    case SB_SORT_MODE_BACK_TO_FRONT: {
-        return (uint64_t)_sb_depth_to_bits(-sprite->depth) << 32 | sprite->image.id;
-    }
-    case SB_SORT_MODE_FRONT_TO_BACK: {
-        return (uint64_t)_sb_depth_to_bits(sprite->depth) << 32 | sprite->image.id;
-    }
+    ctx->update_frame_index = -1;
+
+    ctx->desc = _sbatch_context_desc_defaults(in_desc);
+
+    ctx->label = _sbatch_make_str(ctx->desc.label);
+
+    ctx->desc.label = NULL;
+
+    const int max_vertices = 4 * ctx->desc.max_sprites;
+    const size_t vbuf_size = (size_t)max_vertices * sizeof(_sbatch_vertex);
+
+    ctx->vertices = (_sbatch_vertex*)SOKOL_MALLOC(vbuf_size);
+    SOKOL_ASSERT(ctx->vertices);
+
+    ctx->images = (sg_image*)SOKOL_MALLOC(ctx->desc.max_sprites * sizeof(sg_image));
+    SOKOL_ASSERT(ctx->images);
+
+    ctx->pipeline.id = _SBATCH_DEFAULT(in_desc->pipeline.id, _sbatch.pipeline.id);
+
+    ctx->sprite_count = 0;
+
+    sg_buffer_desc vbuf_desc;
+    memset(&vbuf_desc, 0, sizeof(vbuf_desc));
+    vbuf_desc.size = vbuf_size;
+    vbuf_desc.type = SG_BUFFERTYPE_VERTEXBUFFER;
+    vbuf_desc.usage = SG_USAGE_STREAM;
+    vbuf_desc.label = "sokol-spritebatch-vertices";
+    ctx->vertex_buffer = sg_make_buffer(&vbuf_desc);
+    SOKOL_ASSERT(SG_INVALID_ID != ctx->vertex_buffer.id);
+
+    sg_pop_debug_group();
+}
+
+static void _sbatch_destroy_context(sbatch_context ctx_id) {
+    _sbatch_context* ctx = _sbatch_lookup_context(ctx_id.id);
+    if (ctx) {
+        ctx->sprite_count = 0;
+        if (ctx->vertices) {
+            SOKOL_FREE(ctx->vertices);
+        }
+        if (ctx->images) {
+            SOKOL_FREE(ctx->images);
+        }
+        sg_push_debug_group("sokol-spritebatch");
+        sg_destroy_buffer(ctx->vertex_buffer);
+        sg_pop_debug_group();
+        memset(ctx, 0, sizeof(*ctx));
+        _sbatch_pool_free_index(&_sbatch.context_pool.pool, _sbatch_slot_index(ctx_id.id));
     }
-    return 0;
 }
 
-static inline uint32_t _sb_pack_color_bytes(uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
-    return (uint32_t)(((uint32_t)a << 24) | ((uint32_t)b << 16) | ((uint32_t)g << 8) | r);
+static uint32_t _sbatch_pack_color_bytes(uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
+    return (uint32_t)a << 24 | (uint32_t)b << 16 | (uint32_t)g << 8 | (uint32_t)r;
 }
 
-static inline float _sb_clampf(float v, float low, float high) {
+static float _sbatch_clampf(float v, float low, float high) {
     if (v < low) {
         return low;
     }
-    else if (v > high) {
+    if (v > high) {
         return high;
     }
     return v;
 }
 
-static inline uint32_t _sb_pack_color(const sg_color* color) {
-    const uint8_t r = (uint8_t)(_sb_clampf(color->r, 0.0f, 1.0f) * 255.0f);
-    const uint8_t g = (uint8_t)(_sb_clampf(color->g, 0.0f, 1.0f) * 255.0f);
-    const uint8_t b = (uint8_t)(_sb_clampf(color->b, 0.0f, 1.0f) * 255.0f);
-    const uint8_t a = (uint8_t)(_sb_clampf(color->a, 0.0f, 1.0f) * 255.0f);
-    return _sb_pack_color_bytes(r, g, b, a);
+static uint32_t _sbatch_pack_color(const sg_color* color) {
+    const uint8_t r = (uint8_t)(_sbatch_clampf(color->r, 0.0f, 1.0f) * 255.0f);
+    const uint8_t g = (uint8_t)(_sbatch_clampf(color->g, 0.0f, 1.0f) * 255.0f);
+    const uint8_t b = (uint8_t)(_sbatch_clampf(color->b, 0.0f, 1.0f) * 255.0f);
+    const uint8_t a = (uint8_t)(_sbatch_clampf(color->a, 0.0f, 1.0f) * 255.0f);
+    return _sbatch_pack_color_bytes(r, g, b, a);
 }
 
-static inline int _sb_quad_compare(const void* a, const void* b) {
-    uint64_t key_a = ((const _sb_quad*)a)->sort_key;
-    uint64_t key_b = ((const _sb_quad*)b)->sort_key;
-    if (key_a < key_b) return -1;
-    if (key_a > key_b) return  1;
-    return 0;
-}
-
-static inline void* _sb_realloc(void* old_ptr, size_t old_size, size_t new_size) {
-    SOKOL_ASSERT((new_size > 0) && (new_size > old_size));
-    void* new_ptr = SOKOL_MALLOC(new_size);
-    SOKOL_ASSERT(new_ptr);
-    if (old_ptr) {
-        if (old_size > 0) {
-            memcpy(new_ptr, old_ptr, old_size);
-        }
-        SOKOL_FREE(old_ptr);
-    }
-    return new_ptr;
+static int _sg_image_slot_index(uint32_t id) {
+    int slot_index = (int)(id & _SBATCH_IMAGE_SLOT_MASK);
+    SOKOL_ASSERT(0 != slot_index);
+    return slot_index;
 }
 
-static inline void _sb_grow_batch_buffer(void) {
-    const size_t new_capacity = _sb.batch_data.batch_capacity * 2;
-    _sb.batch_data.batches = (_sb_batch*)_sb_realloc(_sb.batch_data.batches, _sb.batch_data.batch_capacity, new_capacity);
-    _sb.batch_data.batch_capacity = new_capacity;
+static void _sbatch_setup_sprite_pool(void) {
+    sg_desc sg_desc = sg_query_desc();
+    const int num_slots = sg_desc.image_pool_size;
+    _sbatch.sprite_pool.size = (size_t)num_slots * sizeof(_sbatch_sprite_data);
+    _sbatch.sprite_pool.data = (_sbatch_sprite_data*)SOKOL_MALLOC(_sbatch.sprite_pool.size);
+    SOKOL_ASSERT(_sbatch.sprite_pool.data);
+    memset(_sbatch.sprite_pool.data, 0, _sbatch.sprite_pool.size);
 }
 
-static inline _sb_batch* _sb_create_batch(void) {
-    if (_sb.batch_data.batch_size >= _sb.batch_data.batch_capacity) {
-        _sb_grow_batch_buffer();
-    }
-    return &_sb.batch_data.batches[_sb.batch_data.batch_size++];
+static void _sbatch_init_quad_rotated(
+    _sbatch_vertex* vertices, float x, float y, float dx, float dy,
+    float w, float h, float sin, float cos, uint32_t rgba,
+    sbatch_float2 top_left, sbatch_float2 bottom_right, float depth) {
+
+    _sbatch_vertex* top_left_vertex = vertices;
+    top_left_vertex->x = x + dx * cos - dy * sin;
+    top_left_vertex->y = y + dx * sin + dy * cos;
+    top_left_vertex->z = depth;
+    top_left_vertex->rgba = rgba;
+    top_left_vertex->u = top_left.x;
+    top_left_vertex->v = top_left.y;
+
+    _sbatch_vertex* top_right_vertex = vertices + 1;
+    top_right_vertex->x = x + (dx + w) * cos - dy * sin;
+    top_right_vertex->y = y + (dx + w) * sin + dy * cos;
+    top_right_vertex->z = depth;
+    top_right_vertex->rgba = rgba;
+    top_right_vertex->u = bottom_right.x;
+    top_right_vertex->v = top_left.y;
+
+    _sbatch_vertex* bottom_left_vertex = vertices + 2;
+    bottom_left_vertex->x = x + dx * cos - (dy + h) * sin;
+    bottom_left_vertex->y = y + dx * sin + (dy + h) * cos;
+    bottom_left_vertex->z = depth;
+    bottom_left_vertex->rgba = rgba;
+    bottom_left_vertex->u = top_left.x;
+    bottom_left_vertex->v = bottom_right.y;
+
+    _sbatch_vertex* bottom_right_vertex = vertices + 3;
+    bottom_right_vertex->x = x + (dx + w) * cos - (dy + h) * sin;
+    bottom_right_vertex->y = y + (dx + w) * sin + (dy + h) * cos;
+    bottom_right_vertex->z = depth;
+    bottom_right_vertex->rgba = rgba;
+    bottom_right_vertex->u = bottom_right.x;
+    bottom_right_vertex->v = bottom_right.y;
 }
 
-static inline void _sb_init_batch(sg_image image, int num_elements, int base_element) {
-    _sb_batch* batch    = _sb_create_batch();
-    batch->image        = image;
-    batch->num_elements = num_elements;
-    batch->base_element = base_element;
-    batch->matrix       = _sb.projection_matrix;
+static void _sbatch_init_quad(
+    _sbatch_vertex* vertices, float x, float y, float w, float h, uint32_t rgba,
+    sbatch_float2 top_left, sbatch_float2 bottom_right, float depth) {
+
+    _sbatch_vertex* top_left_vertex = vertices;
+    top_left_vertex->x = x;
+    top_left_vertex->y = y;
+    top_left_vertex->z = depth;
+    top_left_vertex->rgba = rgba;
+    top_left_vertex->u = top_left.x;
+    top_left_vertex->v = top_left.y;
+
+    _sbatch_vertex* top_right_vertex = vertices + 1;
+    top_right_vertex->x = x + w;
+    top_right_vertex->y = y;
+    top_right_vertex->z = depth;
+    top_right_vertex->rgba = rgba;
+    top_right_vertex->u = bottom_right.x;
+    top_right_vertex->v = top_left.y;
+
+    _sbatch_vertex* bottom_left_vertex = vertices + 2;
+    bottom_left_vertex->x = x;
+    bottom_left_vertex->y = y + h;
+    bottom_left_vertex->z = depth;
+    bottom_left_vertex->rgba = rgba;
+    bottom_left_vertex->u = top_left.x;
+    bottom_left_vertex->v = bottom_right.y;
+
+    _sbatch_vertex* bottom_right_vertex = vertices + 3;
+    bottom_right_vertex->x = x + w;
+    bottom_right_vertex->y = y + h;
+    bottom_right_vertex->z = depth;
+    bottom_right_vertex->rgba = rgba;
+    bottom_right_vertex->u = bottom_right.x;
+    bottom_right_vertex->v = bottom_right.y;
 }
 
-static inline void _sb_init_batches(void) {
-    int batch_size = 0;
-    int base_element = 0;
-    sg_image current_image = { SG_INVALID_ID };
+static void _sbatch_init_index_buffer(void) {
+    uint16_t* index_buffer = (uint16_t*)SOKOL_MALLOC(_SBATCH_MAX_INDICES * sizeof(uint16_t));
+    SOKOL_ASSERT(index_buffer);
 
-    for (int i = 0; i < _sb.quad_count; ++i, ++batch_size) {
-        if (_sb.quads[i].image.id != current_image.id) {
-            const int num_elements = batch_size * 6;
-            _sb_init_batch(current_image, num_elements, base_element);
-            current_image = _sb.quads[i].image;
-            batch_size = 0;
-            base_element += num_elements;
-        }
+    uint16_t* index_ptr = index_buffer;
+    for (uint32_t i = 0; i < _SBATCH_MAX_QUADS; i++, index_ptr += 6) {
+        // Triangle 1
+        *(index_ptr + 0) = (uint16_t)(i * 4);
+        *(index_ptr + 1) = (uint16_t)(i * 4 + 1);
+        *(index_ptr + 2) = (uint16_t)(i * 4 + 2);
+        // Triangle 2
+        *(index_ptr + 3) = (uint16_t)(i * 4 + 1);
+        *(index_ptr + 4) = (uint16_t)(i * 4 + 3);
+        *(index_ptr + 5) = (uint16_t)(i * 4 + 2);
     }
 
-    const int num_elements = batch_size * 6;
-    _sb_init_batch(current_image, num_elements, base_element);
+    sg_buffer_desc index_buffer_desc;
+    memset(&index_buffer_desc, 0, sizeof(index_buffer_desc));
+    index_buffer_desc.size = _SBATCH_MAX_INDICES * sizeof(uint16_t);
+    index_buffer_desc.type = SG_BUFFERTYPE_INDEXBUFFER;
+    index_buffer_desc.usage = SG_USAGE_IMMUTABLE;
+    index_buffer_desc.label = "sokol-spritebatch-indices";
+    index_buffer_desc.data.size = _SBATCH_MAX_INDICES * sizeof(uint16_t);
+    index_buffer_desc.data.ptr = index_buffer;
+    _sbatch.index_buffer = sg_make_buffer(&index_buffer_desc);
+    SOKOL_ASSERT(SG_INVALID_ID != _sbatch.index_buffer.id);
+
+    SOKOL_FREE(index_buffer);
+
+    _sbatch.bindings.index_buffer = _sbatch.index_buffer;
 }
 
-SOKOL_API_IMPL void sb_premultiply_alpha(uint8_t* pixels, int pixel_count) {
-    /*
-        http://www.realtimerendering.com/blog/gpus-prefer-premultiplication/
-        https://shawnhargreaves.com/blog/premultiplied-alpha.html
-    */
-    SOKOL_ASSERT(pixels);
-    for (int i = 0; i < pixel_count; ++i) {
-        pixels[0] = pixels[0] * pixels[3] / 255;
-        pixels[1] = pixels[1] * pixels[3] / 255;
-        pixels[2] = pixels[2] * pixels[3] / 255;
-        pixels += 4;
+static bool _sbatch_rect_is_valid(const sbatch_rect* rect) {
+    if (rect->width != 0.0f && rect->height != 0.0f) {
+        return true;
     }
+    return false;
 }
 
-SOKOL_API_IMPL void sb_setup(const sb_desc* desc) {
-    SOKOL_ASSERT(desc);
-    memset(&_sb, 0, sizeof(_sb_t));
-    _sb_init_sprite_pool();
-    _sb.quads = (_sb_quad*)SOKOL_MALLOC(_SB_MAX_QUADS * sizeof(_sb_quad));
-    SOKOL_ASSERT(_sb.quads);
-    _sb_init_vertex_buffer();
-    _sb_init_index_buffer();
-    _sb_init_batch_data();
+SOKOL_SPRITEBATCH_API_DECL int sbatch_frame() {
+    return ++_sbatch.frame_index;
+}
 
-    _sb.default_shader = sg_make_shader(spritebatch_shader_desc(sg_query_backend()));
+SOKOL_API_IMPL void sbatch_setup(const sbatch_desc* desc) {
+
+    memset(&_sbatch, 0, sizeof _sbatch);
+
+    sbatch_desc batch_desc = *desc;
+    batch_desc.context_pool_size = _SBATCH_DEFAULT(batch_desc.context_pool_size, 32);
+    _sbatch_setup_context_pool(&batch_desc);
+    _sbatch_setup_sprite_pool();
+
+    _sbatch.shader = sg_make_shader(spritebatch_shader_desc(sg_query_backend()));
 
     sg_pipeline_desc pipeline_desc;
     memset(&pipeline_desc, 0, sizeof(sg_pipeline_desc));
-    pipeline_desc.color_count                      = 1;
-    pipeline_desc.colors[0].blend.enabled          = true;
-    pipeline_desc.colors[0].blend.src_factor_rgb   = SG_BLENDFACTOR_ONE;
+    pipeline_desc.color_count = 1;
+    pipeline_desc.colors[0].blend.enabled = true;
+    pipeline_desc.colors[0].blend.src_factor_rgb = SG_BLENDFACTOR_ONE;
     pipeline_desc.colors[0].blend.src_factor_alpha = SG_BLENDFACTOR_ONE;
-    pipeline_desc.colors[0].blend.dst_factor_rgb   = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
+    pipeline_desc.colors[0].blend.dst_factor_rgb = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
     pipeline_desc.colors[0].blend.dst_factor_alpha = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
-    pipeline_desc.shader                           = _sb.default_shader;
-    pipeline_desc.index_type                       = SG_INDEXTYPE_UINT16;
-    pipeline_desc.layout.attrs[0].format           = SG_VERTEXFORMAT_FLOAT3;
-    pipeline_desc.layout.attrs[1].format           = SG_VERTEXFORMAT_FLOAT2;
-    pipeline_desc.layout.attrs[2].format           = SG_VERTEXFORMAT_UBYTE4N;
-    pipeline_desc.label                            = "spritebatch-default-pipeline";
+    pipeline_desc.shader = _sbatch.shader;
+    pipeline_desc.index_type = SG_INDEXTYPE_UINT16;
+    pipeline_desc.layout.attrs[0].format = SG_VERTEXFORMAT_FLOAT3;
+    pipeline_desc.layout.attrs[1].format = SG_VERTEXFORMAT_FLOAT2;
+    pipeline_desc.layout.attrs[2].format = SG_VERTEXFORMAT_UBYTE4N;
+    pipeline_desc.label = "spritebatch-default-pipeline";
+
+    _sbatch.pipeline = sg_make_pipeline(&pipeline_desc);
 
-    _sb.default_pipeline = sg_make_pipeline(&pipeline_desc);
+    _sbatch_init_index_buffer();
 }
 
-SOKOL_API_IMPL void sb_shutdown(void) {
-    sg_destroy_pipeline(_sb.default_pipeline);
-    sg_destroy_shader(_sb.default_shader);
-    SOKOL_FREE(_sb.batch_data.batches);
-    sg_destroy_buffer(_sb.index_buffer);
-    sg_destroy_buffer(_sb.vertex_buffer);
-    SOKOL_FREE(_sb.vertex_buffer_data);
-    SOKOL_FREE(_sb.quads);
-    SOKOL_FREE(_sb.sprite_pool.data);
+SOKOL_API_IMPL void sbatch_shutdown(void) {
+    sg_destroy_buffer(_sbatch.index_buffer);
+    sg_destroy_pipeline(_sbatch.pipeline);
+    sg_destroy_shader(_sbatch.shader);
+    _sbatch_discard_context_pool();
+    SOKOL_FREE(_sbatch.sprite_pool.data);
+}
+
+SOKOL_API_IMPL sbatch_context sbatch_make_context(const sbatch_context_desc* desc) {
+    SOKOL_ASSERT(desc);
+    const sbatch_context result = _sbatch_alloc_context();
+    _sbatch_init_context(result, desc);
+    return result;
 }
 
-SOKOL_API_IMPL void sb_begin(const sb_render_state* render_state) {
-    SOKOL_ASSERT(render_state);
-    SOKOL_ASSERT(!_sb.begin_called);
-    _sb.begin_called = true;
+SOKOL_API_IMPL void sbatch_destroy_context(sbatch_context context) {
+    SOKOL_ASSERT(context.id != SG_INVALID_ID);
+    _sbatch_destroy_context(context);
+}
 
-    _sb.render_state.viewport = render_state->viewport;
-    _sb.render_state.sort_mode = render_state->sort_mode;
-    _sb.render_state.pipeline.id
-        = _SB_DEFAULT(render_state->pipeline.id, _sb.default_pipeline.id);
+SOKOL_API_IMPL void sbatch_begin(sbatch_context context) {
+    SOKOL_ASSERT(context.id != SG_INVALID_ID);
+    _sbatch.ctx_id = context;
+    SOKOL_ASSERT(!_sbatch.begin_called);
+    _sbatch.begin_called = true;
 
-    const float width = (float)_sb.render_state.viewport.width;
-    const float height = (float)_sb.render_state.viewport.height;
+    _sbatch_context* ctx = _sbatch_context_at(_sbatch.ctx_id.id);
 
-    _sb.projection_matrix
-        = _sb_orthographic_off_center(0.0f, width, height, 0.0f, 0.0f, SB_MAX_DEPTH);
+    // a sbatch_context object can only be used in one sbatch_begin call per frame.
+    SOKOL_ASSERT(_sbatch.frame_index != ctx->update_frame_index);
+    ctx->update_frame_index = _sbatch.frame_index;
 
-    _sb.render_state.transform_matrix
-        = _sb_matrix_is_null(&render_state->transform_matrix)
-            ? _sb_matrix_identity()
-            : render_state->transform_matrix;
+    SOKOL_ASSERT(ctx);
+    _sbatch.bindings.vertex_buffers[0] = ctx->vertex_buffer;
 }
 
-SOKOL_API_IMPL void sb_sprite(const sb_sprite_info* sprite) {
-    /* TODO: maybe take an array of sprite info? */
+SOKOL_API_IMPL void sbatch_push_sprite(const sbatch_sprite* sprite) {
     SOKOL_ASSERT(sprite);
     SOKOL_ASSERT(sprite->image.id != SG_INVALID_ID);
-    SOKOL_ASSERT(_sb.quad_count < _SB_MAX_QUADS);
 
-    _sb_sprite_data* cached_sprite_data = &_sb.sprite_pool.data[_sg_image_slot_index(sprite->image.id)];
-    if (cached_sprite_data->image.id != sprite->image.id)
-    {
-        sg_image_info info = sg_query_image_info(sprite->image);
-        cached_sprite_data->height = info.height;
-        cached_sprite_data->width  = info.width;
-        cached_sprite_data->image  = sprite->image;
-    }
+    _sbatch_context* ctx = _sbatch_context_at(_sbatch.ctx_id.id);
+    SOKOL_ASSERT(ctx);
 
-    const float scale_x = _SB_DEFAULT(sprite->scale.x, 1.0f);
-    const float scale_y = _SB_DEFAULT(sprite->scale.y, 1.0f);
+    if (ctx->sprite_count < ctx->desc.max_sprites) {
 
-    const float sprite_width  = _SB_DEFAULT(sprite->width, (float)cached_sprite_data->width);
-    const float sprite_height = _SB_DEFAULT(sprite->height, (float)cached_sprite_data->height);
+        const int sprite_index = ctx->sprite_count++;
 
-    const float width  = sprite_width * scale_x;
-    const float height = sprite_height * scale_y;
+        _sbatch_sprite_data* cached_sprite_data = &_sbatch.sprite_pool.data[_sg_image_slot_index(sprite->image.id)];
 
-    const float texel_width  = (1.0f / cached_sprite_data->width);
-    const float texel_height = (1.0f / cached_sprite_data->height);
+        if (cached_sprite_data->image.id != sprite->image.id) {
+            const sg_image_info info = sg_query_image_info(sprite->image);
+            cached_sprite_data->height = info.height;
+            cached_sprite_data->width = info.width;
+            cached_sprite_data->texel_height = 1.0f / (float)info.height;
+            cached_sprite_data->texel_width = 1.0f / (float)info.width;
+            cached_sprite_data->image = sprite->image;
+        }
 
-    sb_float2 tex_coord_top_left = {
-        sprite->source.x * texel_width,
-        sprite->source.y * texel_height
-    };
+        const sbatch_float2 scale = { _SBATCH_DEFAULT(sprite->scale.x, 1.0f),  _SBATCH_DEFAULT(sprite->scale.y, 1.0f) };
+        const sbatch_float2 scaled_origin = { scale.x * sprite->origin.x, scale.y * sprite->origin.y };
+        float width, height;
+        sbatch_float2 tex_coord_top_left;
+        sbatch_float2 tex_coord_bottom_right;
+
+        if (_sbatch_rect_is_valid(&sprite->source)) {
+            width = sprite->source.width * scale.x;
+            height = sprite->source.height * scale.y;
+            tex_coord_top_left.x = sprite->source.x * cached_sprite_data->texel_width;
+            tex_coord_top_left.y = sprite->source.y * cached_sprite_data->texel_height;
+            tex_coord_bottom_right.x = (sprite->source.x + sprite->source.width) * cached_sprite_data->texel_width;
+            tex_coord_bottom_right.y = (sprite->source.y + sprite->source.height) * cached_sprite_data->texel_height;
+        } else {
+            width = (float)cached_sprite_data->width * scale.x;
+            height = (float)cached_sprite_data->height * scale.y;
+            tex_coord_top_left.x = 0.0f;
+            tex_coord_top_left.y = 0.0f;
+            tex_coord_bottom_right.x = 1.0f;
+            tex_coord_bottom_right.y = 1.0f;
+        }
 
-    sb_float2 tex_coord_bottom_right = {
-        (sprite->source.x + sprite_width)  * texel_width,
-        (sprite->source.y + sprite_height) * texel_height
-    };
+        if ((sprite->flags & SBATCH_FLIP_Y) != SBATCH_FLIP_NONE) {
+            const float temp = tex_coord_bottom_right.y;
+            tex_coord_bottom_right.y = tex_coord_top_left.y;
+            tex_coord_top_left.y = temp;
+        }
 
-    if ((sprite->flags & SB_FLIP_Y) != SB_FLIP_NONE)
-    {
-        const float temp         = tex_coord_bottom_right.y;
-        tex_coord_bottom_right.y = tex_coord_top_left.y;
-        tex_coord_top_left.y     = temp;
-    }
+        if ((sprite->flags & SBATCH_FLIP_X) != SBATCH_FLIP_NONE) {
+            const float temp = tex_coord_bottom_right.x;
+            tex_coord_bottom_right.x = tex_coord_top_left.x;
+            tex_coord_top_left.x = temp;
+        }
 
-    if ((sprite->flags & SB_FLIP_X) != SB_FLIP_NONE)
-    {
-        const float temp         = tex_coord_bottom_right.x;
-        tex_coord_bottom_right.x = tex_coord_top_left.x;
-        tex_coord_top_left.x     = temp;
+        uint32_t packed_color = _sbatch_pack_color(&sprite->color);
+        packed_color = packed_color == 0 ? 0xFFFFFFFF : packed_color;
+
+        ctx->images[sprite_index] = sprite->image;
+
+        const int base_vertex_index = sprite_index * 4;
+        _sbatch_vertex* vertices = ctx->vertices + base_vertex_index;
+
+        if (sprite->rotation == 0.0f) {
+            _sbatch_init_quad(vertices,
+                sprite->position.x - scaled_origin.x,
+                sprite->position.y - scaled_origin.y,
+                width,
+                height,
+                packed_color,
+                tex_coord_top_left,
+                tex_coord_bottom_right,
+                sprite->depth);
+        }
+        else {
+            _sbatch_init_quad_rotated(vertices,
+                sprite->position.x,
+                sprite->position.y,
+                -scaled_origin.x,
+                -scaled_origin.y,
+                width,
+                height,
+                sinf(sprite->rotation),
+                cosf(sprite->rotation),
+                packed_color,
+                tex_coord_top_left,
+                tex_coord_bottom_right,
+                sprite->depth);
+        }
+    }
+    else {
+        if (ctx->label.buf[0] != '\0') {
+            SOKOL_LOG("sokol_spritebatch.h: dropped sprites, increase max_sprites of sbatch_context:");
+            SOKOL_LOG(ctx->label.buf);
+        }
+        else {
+            SOKOL_LOG("sokol_spritebatch.h: dropped sprites, increase max_sprites");
+        }
     }
+}
 
-    const float scaled_origin_x = scale_x * sprite->origin.x;
-    const float scaled_origin_y = scale_y * sprite->origin.y;
+SOKOL_API_IMPL void sbatch_push_sprite_rect(const sbatch_sprite_rect* sprite) {
+    SOKOL_ASSERT(sprite);
+    SOKOL_ASSERT(sprite->image.id != SG_INVALID_ID);
 
-    _sb_quad* quad = &_sb.quads[_sb.quad_count++];
-    quad->sort_key = _sb_make_sort_key(sprite);
-    quad->image = sprite->image;
+    _sbatch_context* ctx = _sbatch_context_at(_sbatch.ctx_id.id);
+    SOKOL_ASSERT(ctx);
 
-    uint32_t packed_color = _sb_pack_color(&sprite->color);
-    packed_color = packed_color == 0 ? 0xFFFFFFFF : packed_color;
+    if (ctx->sprite_count < ctx->desc.max_sprites) {
 
-    if (sprite->rotation == 0.0f)
-    {
-        _sb_init_quad(quad,
-            sprite->flags,
-            sprite->position.x - scaled_origin_x,
-            sprite->position.y - scaled_origin_y,
-            width,
-            height,
-            packed_color,
-            tex_coord_top_left,
-            tex_coord_bottom_right,
-            sprite->depth);
+        const int sprite_index = ctx->sprite_count++;
+
+        _sbatch_sprite_data* cached_sprite_data = &_sbatch.sprite_pool.data[_sg_image_slot_index(sprite->image.id)];
+
+        if (cached_sprite_data->image.id != sprite->image.id) {
+            const sg_image_info info = sg_query_image_info(sprite->image);
+            cached_sprite_data->height = info.height;
+            cached_sprite_data->width = info.width;
+            cached_sprite_data->texel_height = 1.0f / (float)info.height;
+            cached_sprite_data->texel_width = 1.0f / (float)info.width;
+            cached_sprite_data->image = sprite->image;
+        }
+
+        sbatch_float2 scaled_origin;
+        sbatch_float2 tex_coord_top_left;
+        sbatch_float2 tex_coord_bottom_right;
+
+        if (_sbatch_rect_is_valid(&sprite->source)) {
+            tex_coord_top_left.x = sprite->source.x * cached_sprite_data->texel_width;
+            tex_coord_top_left.y = sprite->source.y * cached_sprite_data->texel_height;
+            tex_coord_bottom_right.x = (sprite->source.x + sprite->source.width) * cached_sprite_data->texel_width;
+            tex_coord_bottom_right.y = (sprite->source.y + sprite->source.height) * cached_sprite_data->texel_height;
+            scaled_origin.x = sprite->origin.x * sprite->destination.width / sprite->source.width;
+            scaled_origin.y = sprite->origin.y * sprite->destination.height / sprite->source.height;
+        } else {
+            tex_coord_top_left.x = 0.0f;
+            tex_coord_top_left.y = 0.0f;
+            tex_coord_bottom_right.x = 1.0f;
+            tex_coord_bottom_right.y = 1.0f;
+            scaled_origin.x = sprite->origin.x * sprite->destination.width * cached_sprite_data->texel_width;
+            scaled_origin.y = sprite->origin.y * sprite->destination.height * cached_sprite_data->texel_height;
+        }
+
+        if ((sprite->flags & SBATCH_FLIP_Y) != SBATCH_FLIP_NONE) {
+            const float temp = tex_coord_bottom_right.y;
+            tex_coord_bottom_right.y = tex_coord_top_left.y;
+            tex_coord_top_left.y = temp;
+        }
+
+        if ((sprite->flags & SBATCH_FLIP_X) != SBATCH_FLIP_NONE) {
+            const float temp = tex_coord_bottom_right.x;
+            tex_coord_bottom_right.x = tex_coord_top_left.x;
+            tex_coord_top_left.x = temp;
+        }
+
+        uint32_t packed_color = _sbatch_pack_color(&sprite->color);
+        packed_color = packed_color == 0 ? 0xFFFFFFFF : packed_color;
+
+        ctx->images[sprite_index] = sprite->image;
+
+        const int base_vertex_index = sprite_index * 4;
+        _sbatch_vertex* vertices = ctx->vertices + base_vertex_index;
+
+        if (sprite->rotation == 0.0f) {
+            _sbatch_init_quad(vertices,
+                sprite->destination.x - scaled_origin.x,
+                sprite->destination.y - scaled_origin.y,
+                sprite->destination.width,
+                sprite->destination.height,
+                packed_color,
+                tex_coord_top_left,
+                tex_coord_bottom_right,
+                sprite->depth);
+        } else {
+            _sbatch_init_quad_rotated(vertices,
+                sprite->destination.x,
+                sprite->destination.y,
+                -scaled_origin.x,
+                -scaled_origin.y,
+                sprite->destination.width,
+                sprite->destination.height,
+                sinf(sprite->rotation),
+                cosf(sprite->rotation),
+                packed_color,
+                tex_coord_top_left,
+                tex_coord_bottom_right,
+                sprite->depth);
+        }
     }
-    else
-    {
-        _sb_init_quad_rotated(quad,
-            sprite->position.x,
-            sprite->position.y,
-            -scaled_origin_x,
-            -scaled_origin_y,
-            width,
-            height,
-            sinf(sprite->rotation),
-            cosf(sprite->rotation),
-            packed_color,
-            tex_coord_top_left,
-            tex_coord_bottom_right,
-            sprite->depth);
+    else {
+        if (ctx->label.buf[0] != '\0') {
+            SOKOL_LOG("sokol_spritebatch.h: dropped sprites, increase max_sprites of sbatch_context:");
+            SOKOL_LOG(ctx->label.buf);
+        } else {
+            SOKOL_LOG("sokol_spritebatch.h: dropped sprites, increase max_sprites");
+        }
     }
 }
 
-SOKOL_API_IMPL void sb_end(void) {
+static void _sbatch_draw(int base_element, sg_image current_image, int num_elements) {
+    _sbatch.bindings.fs_images[0] = current_image;
+    sg_apply_bindings(&_sbatch.bindings);
+    sg_draw(base_element, num_elements, 1);
+}
 
-    SOKOL_ASSERT(_sb.begin_called);
-    _sb.begin_called = false;
+static _sbatch_mat4x4 _sbatch_orthographic_off_center(float left, float right, float bottom, float top, float z_near, float z_far) {
+    _sbatch_mat4x4 result;
 
-    if (_sb.quad_count == 0) {
-        return;
-    }
+    result.m[0][0] = 2.0f / (right - left);
+    result.m[0][1] = 0.0f;
+    result.m[0][2] = 0.0f;
+    result.m[0][3] = 0.0f;
 
-    if (_sb.render_state.sort_mode != SB_SORT_MODE_DEFERRED) {
-        qsort(_sb.quads, _sb.quad_count, sizeof(_sb_quad), _sb_quad_compare);
-    }
+    result.m[1][0] = 0.0f;
+    result.m[1][1] = 2.0f / (top - bottom);
+    result.m[1][2] = 0.0f;
+    result.m[1][3] = 0.0f;
 
-    /*
-        If this is moved to storing the quad data SOA, we would be able to use the array that stores all the
-        vertex data verbatim without copying vertex data into an auxiliary buffer. This would significantly
-        complicate the code though, especially sorting the quads before submission. Can't use a standard sorting
-        algorithm to sort SOA data. Would need to implement a bespoke solution and that'd be a bit of a pain to
-        profile and maintain. It is something to think about though.
-    */
-    for (size_t i = 0; i < _sb.quad_count; i++) {
-        memcpy(_sb.vertex_buffer_data + (i * 4), &_sb.quads[i], 4 * sizeof(_sb_vertex));
-    }
+    result.m[2][0] = 0.0f;
+    result.m[2][1] = 0.0f;
+    result.m[2][2] = 1.0f / (z_near - z_far);
+    result.m[2][3] = 0.0f;
 
-    _sb_init_batches();
+    result.m[3][0] = (left + right) / (left - right);
+    result.m[3][1] = (bottom + top) / (bottom - top);
+    result.m[3][2] = z_near / (z_near - z_far);
+    result.m[3][3] = 1.0f;
+
+    return result;
 }
 
-SOKOL_API_IMPL void sb_draw(void) {
+void sbatch_end(void) {
+    SOKOL_ASSERT(_sbatch.begin_called);
+    _sbatch.begin_called = false;
+
+    _sbatch_context* ctx = _sbatch_context_at(_sbatch.ctx_id.id);
+    SOKOL_ASSERT(ctx);
 
-    if (_sb.batch_data.batch_size == 0) {
+    if (ctx->sprite_count == 0) {
         return;
     }
 
-    const sg_range range = { _sb.vertex_buffer_data, _sb.quad_count * 4 * sizeof(_sb_vertex) };
-    sg_update_buffer(_sb.vertex_buffer, &range);
+    const int max_vertices = 4 * ctx->sprite_count;
+    const size_t vbuf_size = (size_t)max_vertices * sizeof(_sbatch_vertex);
+    const sg_range range = { ctx->vertices, vbuf_size };
+    sg_update_buffer(ctx->vertex_buffer, &range);
+
+    int batch_size = 0;
+    int base_element = 0;
+    sg_image current_image = ctx->images[0];
+
+    sg_apply_pipeline(ctx->pipeline);
+
+    _sbatch_mat4x4 matrix =
+        _sbatch_orthographic_off_center(0.0f, (float)ctx->desc.canvas_width, (float)ctx->desc.canvas_height, 0.0f, 0.0f, 1000.0f);
+
+    const sg_range matrix_range = { &matrix, sizeof matrix };
+    sg_apply_uniforms(SG_SHADERSTAGE_VS, 0, &matrix_range);
 
-    for (size_t i = 1; i < _sb.batch_data.batch_size; i++) {
-        _sb_batch* batch = _sb.batch_data.batches + i;
-        sg_apply_pipeline(_sb.default_pipeline);
-        sg_apply_uniforms(SG_SHADERSTAGE_VS, 0, &SG_RANGE(batch->matrix));
-        _sb.bindings.fs_images[0] = batch->image;
-        sg_apply_bindings(&_sb.bindings);
-        sg_draw(batch->base_element, batch->num_elements, 1);
+    if(ctx->fs_uniform_state.data) {
+        sg_apply_uniforms(SG_SHADERSTAGE_FS, ctx->fs_uniform_state.ub_index, ctx->fs_uniform_state.data);
     }
 
-    _sb.quad_count = 0;
-    _sb.batch_data.batch_size = 0;
+    for (int i = 0; i < ctx->sprite_count; ++i, ++batch_size) {
+        if (ctx->images[i].id != current_image.id) {
+            const int num_elements = batch_size * 6;
+            _sbatch_draw(base_element, current_image, num_elements);
+            batch_size = 0;
+            base_element += num_elements;
+            current_image = ctx->images[i];
+        }
+    }
+
+    const int num_elements = batch_size * 6;
+    _sbatch_draw(base_element, current_image, num_elements);
+
+    ctx->sprite_count = 0;
+}
+
+SOKOL_API_IMPL void sbatch_apply_fs_uniforms(int ub_index, const sg_range* data) {
+    SOKOL_ASSERT(data);
+    SOKOL_ASSERT(data->ptr);
+    _sbatch_context* ctx = _sbatch_context_at(_sbatch.ctx_id.id);
+    ctx->fs_uniform_state.ub_index = ub_index;
+    ctx->fs_uniform_state.data = data;
+}
+
+SOKOL_API_IMPL void sbatch_premultiply_alpha_rgba8(uint8_t* pixels, int pixel_count) {
+    SOKOL_ASSERT(pixels);
+    for (int i = 0; i < pixel_count; ++i) {
+        pixels[0] = pixels[0] * pixels[3] / 255;
+        pixels[1] = pixels[1] * pixels[3] / 255;
+        pixels[2] = pixels[2] * pixels[3] / 255;
+        pixels += 4;
+    }
 }
 
 #endif /* SOKOL_SPRITEBATCH_IMPL */