Bläddra i källkod

Merge pull request #862 from floooh/snuklear-image-sampler

sokol_nuklear.h: add support for custom images and samplers.
Andre Weissflog 2 år sedan
förälder
incheckning
520951dbbb
4 ändrade filer med 699 tillägg och 63 borttagningar
  1. 27 0
      CHANGELOG.md
  2. 1 1
      README.md
  3. 8 9
      util/sokol_imgui.h
  4. 663 53
      util/sokol_nuklear.h

+ 27 - 0
CHANGELOG.md

@@ -1,5 +1,32 @@
 ## Updates
 
+#### 26-Jul-2023
+
+**sokol_nuklear.h**: The same image+sampler support has been added as in sokol_imgui.h
+three days ago:
+
+- a new object type `snk_image_t` which wraps a sokol-gfx image and sampler
+  under a common handle
+- new functions:
+  - snk_make_image()
+  - snk_destroy_image()
+  - snk_query_image_desc()
+  - snk_image_from_nkhandle()
+- the function snk_nkhandle() now takes an snk_image_t handle instead of an sg_image handle
+- the nuklear.h header needs to be included before the declaration (not just the implementation),
+  this was already required before, but now you get a proper error message if the include is missing
+- the 'standard' logging- and error-reporting callback has been added as in the other sokol headers
+  (don't forget to add a logging callback in snk_setup(), otherwise sokol-nuklear will be silent)
+- since sokol-nuklear now needs to allocate memory, an allocator can now be provided to the
+  snk_setup() call (otherwise malloc/free will be used)
+
+Please also read the new documentation section `ON USER-PROVIDED IMAGES AND SAMPLERS`
+in sokol_nuklear.h, and also check out the (rewritten) sample:
+
+https://floooh.github.io/sokol-html5/nuklear-images-sapp.html
+
+Associated PR: https://github.com/floooh/sokol/pull/862
+
 #### 23-Jul-2023
 
 **sokol_imgui.h**: Add proper support for injecting user-provided sokol-gfx

+ 1 - 1
README.md

@@ -4,7 +4,7 @@ Simple
 [STB-style](https://github.com/nothings/stb/blob/master/docs/stb_howto.txt)
 cross-platform libraries for C and C++, written in C.
 
-[**See what's new**](https://github.com/floooh/sokol/blob/master/CHANGELOG.md) (**23-Jul-2023** proper image/sampler pair support in sokol_imgui.h)
+[**See what's new**](https://github.com/floooh/sokol/blob/master/CHANGELOG.md) (**26-Jul-2023** proper image/sampler pair support in sokol_nuklear.h)
 
 [![Build](/../../actions/workflows/main.yml/badge.svg)](/../../actions/workflows/main.yml) [![Bindings](/../../actions/workflows/gen_bindings.yml/badge.svg)](/../../actions/workflows/gen_bindings.yml) [![build](https://github.com/floooh/sokol-zig/actions/workflows/main.yml/badge.svg)](https://github.com/floooh/sokol-zig/actions/workflows/main.yml) [![build](https://github.com/floooh/sokol-nim/actions/workflows/main.yml/badge.svg)](https://github.com/floooh/sokol-nim/actions/workflows/main.yml) [![Odin](https://github.com/floooh/sokol-odin/actions/workflows/main.yml/badge.svg)](https://github.com/floooh/sokol-odin/actions/workflows/main.yml)[![Rust](https://github.com/floooh/sokol-rust/actions/workflows/main.yml/badge.svg)](https://github.com/floooh/sokol-rust/actions/workflows/main.yml)
 

+ 8 - 9
util/sokol_imgui.h

@@ -223,14 +223,6 @@
 
         simgui_shutdown()
 
-    --- use the following helper function to create an ImTextureID handle from
-        a sokol-gfx image (note that you cannot provide your own sampler currently):
-
-        ImTextureID tex_id = simgui_imtextureid(img);
-
-        ...an invalid handle {SG_INVALID_ID} will be replaced with the
-        default font texture or default sampler object
-
 
     ON USER-PROVIDED IMAGES AND SAMPLERS
     ====================================
@@ -487,7 +479,7 @@ typedef struct simgui_allocator_t {
 /*
     simgui_logger
 
-    Used in simgui_desc to provide a logging function. Please be aware
+    Used in simgui_desc_t to provide a logging function. Please be aware
     that without logging function, sokol-imgui will be completely
     silent, e.g. it will not report errors, warnings and
     validation layer messages. For maximum error verbosity,
@@ -2212,6 +2204,13 @@ static simgui_desc_t _simgui_desc_defaults(const simgui_desc_t* desc) {
     return res;
 }
 
+// ██████  ██    ██ ██████  ██      ██  ██████
+// ██   ██ ██    ██ ██   ██ ██      ██ ██
+// ██████  ██    ██ ██████  ██      ██ ██
+// ██      ██    ██ ██   ██ ██      ██ ██
+// ██       ██████  ██████  ███████ ██  ██████
+//
+// >>public
 SOKOL_API_IMPL void simgui_setup(const simgui_desc_t* desc) {
     SOKOL_ASSERT(desc);
     _simgui_clear(&_simgui, sizeof(_simgui));

+ 663 - 53
util/sokol_nuklear.h

@@ -50,10 +50,6 @@
 
         sokol_gfx.h
         sokol_app.h     (except SOKOL_NUKLEAR_NO_SOKOL_APP)
-
-    Additionally, include the following headers before including the
-    implementation:
-
         nuklear.h
 
     NOTE: Unlike most other sokol-headers, the implementation must be compiled
@@ -94,6 +90,10 @@
                 sokol-nuklear will use this to compute the size of the vertex-
                 and index-buffers allocated via sokol_gfx.h
 
+            int image_pool_size
+                Number of snk_image_t objects which can be alive at the same time.
+                The default is 256.
+
             sg_pixel_format color_format
                 The color pixel format of the render pass where the UI
                 will be rendered. The default is SG_PIXELFORMAT_RGBA8
@@ -155,13 +155,127 @@
         nk_edit_string(...), called snk_edit_string(...) which will show
         and hide the onscreen keyboard as required.
 
-    --- NOTE: to create a Nuklear image handle, use the helper function
-        `nk_handle snk_nkhandle(sg_image img)` together with
-        the Nuklear function nk_image_handle like this:
 
-        nk_image nki = nk_image_handle(snk_nkhandle(img));
+    ON USER-PROVIDED IMAGES AND SAMPLERS
+    ====================================
+    To render your own images via nk_image(), first create an snk_image_t
+    object from a sokol-gfx image and sampler object.
+
+        // create a sokol-nuklear image object which associates an sg_image with an sg_sampler
+        snk_image_t snk_img = snk_make_image(&(snk_image_desc_t){
+            .image = sg_make_image(...),
+            .sampler = sg_make_sampler(...),
+        });
+
+        // convert the returned image handle into an nk_handle object
+        struct nk_handle nk_hnd = snk_nkhandle(snk_img);
+
+        // create a nuklear image from the generic handle (note that there a different helper functions for this)
+        struct nk_image nk_img = nk_image_handle(nk_hnd);
+
+        // finally specify a Nuklear image UI object
+        nk_image(ctx, nk_img);
+
+    snk_image_t objects are small and cheap (literally just the image and sampler
+    handle).
+
+    You can omit the sampler handle in the snk_make_image() call, in this case a
+    default sampler will be used with nearest-filtering and clamp-to-edge.
+
+    Trying to render with an invalid snk_image_t handle will render a small 8x8
+    white default texture instead.
+
+    To destroy a sokol-nuklear image object, call
+
+        snk_destroy_image(snk_img);
+
+    But please be aware that the image object needs to be around until snk_render() is called
+    in a frame (if this turns out to be too much of a hassle we could introduce some sort
+    of garbage collection where destroyed snk_image_t objects are kept around until
+    the snk_render() call).
+
+    You can call:
+
+        snk_image_desc_t desc = snk_query_image_desc(img)
+
+    ...to get the original desc struct, useful if you need to get the sokol-gfx image
+    and sampler handle of the snk_image_t object.
+
+    You can convert an nk_handle back into an snk_image_t handle:
+
+        snk_image_t img = snk_image_from_nkhandle(nk_hnd);
+
+
+    MEMORY ALLOCATION OVERRIDE
+    ==========================
+    You can override the memory allocation functions at initialization time
+    like this:
+
+        void* my_alloc(size_t size, void* user_data) {
+            return malloc(size);
+        }
+
+        void my_free(void* ptr, void* user_data) {
+            free(ptr);
+        }
+
+        ...
+            snk_setup(&(snk_desc_t){
+                // ...
+                .allocator = {
+                    .alloc = my_alloc,
+                    .free = my_free,
+                    .user_data = ...;
+                }
+            });
+        ...
+
+    If no overrides are provided, malloc and free will be used.
+
+    This only affects memory allocation calls done by sokol_nuklear.h
+    itself though, not any allocations in Nuklear.
+
+
+    ERROR REPORTING AND LOGGING
+    ===========================
+    To get any logging information at all you need to provide a logging callback in the setup call
+    the easiest way is to use sokol_log.h:
+
+        #include "sokol_log.h"
+
+        snk_setup(&(snk_desc_t){
+            .logger.func = slog_func
+        });
+
+    To override logging with your own callback, first write a logging function like this:
+
+        void my_log(const char* tag,                // e.g. 'snk'
+                    uint32_t log_level,             // 0=panic, 1=error, 2=warn, 3=info
+                    uint32_t log_item_id,           // SNK_LOGITEM_*
+                    const char* message_or_null,    // a message string, may be nullptr in release mode
+                    uint32_t line_nr,               // line number in sokol_nuklear.h
+                    const char* filename_or_null,   // source filename, may be nullptr in release mode
+                    void* user_data)
+        {
+            ...
+        }
+
+    ...and then setup sokol-nuklear like this:
+
+        snk_setup(&(snk_desc_t){
+            .logger = {
+                .func = my_log,
+                .user_data = my_user_data,
+            }
+        });
+
+    The provided logging function must be reentrant (e.g. be callable from
+    different threads).
+
+    If you don't want to provide your own custom logger it is highly recommended to use
+    the standard logger in sokol_log.h instead, otherwise you won't see any warnings or
+    errors.
 
-        Note that it's currently not possible to provide a custom sg_sampler object.
 
     LICENSE
     =======
@@ -192,6 +306,7 @@
 #define SOKOL_NUKLEAR_INCLUDED (1)
 #include <stdint.h>
 #include <stdbool.h>
+#include <stddef.h>
 
 #if !defined(SOKOL_GFX_INCLUDED)
 #error "Please include sokol_gfx.h before sokol_nuklear.h"
@@ -199,6 +314,9 @@
 #if !defined(SOKOL_NUKLEAR_NO_SOKOL_APP) && !defined(SOKOL_APP_INCLUDED)
 #error "Please include sokol_app.h before sokol_nuklear.h"
 #endif
+#if !defined(NK_UNDEFINED)
+#error "Please include nuklear.h before sokol_nuklear.h"
+#endif
 
 #if defined(SOKOL_API_DECL) && !defined(SOKOL_NUKLEAR_API_DECL)
 #define SOKOL_NUKLEAR_API_DECL SOKOL_API_DECL
@@ -217,19 +335,106 @@
 extern "C" {
 #endif
 
+enum {
+    SNK_INVALID_ID = 0,
+};
+
+/*
+    snk_image_t
+
+    A combined image-sampler pair used to inject custom images and samplers into Nuklear
+
+    Create with snk_make_image(), and convert to an nk_handle via snk_nkhandle().
+*/
+typedef struct snk_image_t { uint32_t id; } snk_image_t;
+
+/*
+    snk_image_desc_t
+
+    Descriptor struct for snk_make_image(). You must provide
+    at least an sg_image handle. Keeping the sg_sampler handle
+    zero-initialized will select the builtin default sampler
+    which uses linear filtering.
+*/
+typedef struct snk_image_desc_t {
+    sg_image image;
+    sg_sampler sampler;
+} snk_image_desc_t;
+
+/*
+    snk_log_item
+
+    An enum with a unique item for each log message, warning, error
+    and validation layer message.
+*/
+#define _SNK_LOG_ITEMS \
+    _SNK_LOGITEM_XMACRO(OK, "Ok") \
+    _SNK_LOGITEM_XMACRO(MALLOC_FAILED, "memory allocation failed") \
+    _SNK_LOGITEM_XMACRO(IMAGE_POOL_EXHAUSTED, "image pool exhausted") \
+
+#define _SNK_LOGITEM_XMACRO(item,msg) SNK_LOGITEM_##item,
+typedef enum snk_log_item_t {
+    _SNK_LOG_ITEMS
+} snk_log_item_t;
+#undef _SNK_LOGITEM_XMACRO
+
+/*
+    snk_allocator_t
+
+    Used in snk_desc_t to provide custom memory-alloc and -free functions
+    to sokol_nuklear.h. If memory management should be overridden, both the
+    alloc and free function must be provided (e.g. it's not valid to
+    override one function but not the other).
+*/
+typedef struct snk_allocator_t {
+    void* (*alloc)(size_t size, void* user_data);
+    void (*free)(void* ptr, void* user_data);
+    void* user_data;
+} snk_allocator_t;
+
+/*
+    snk_logger
+
+    Used in snk_desc_t to provide a logging function. Please be aware
+    that without logging function, sokol-nuklear will be completely
+    silent, e.g. it will not report errors, warnings and
+    validation layer messages. For maximum error verbosity,
+    compile in debug mode (e.g. NDEBUG *not* defined) and install
+    a logger (for instance the standard logging function from sokol_log.h).
+*/
+typedef struct snk_logger_t {
+    void (*func)(
+        const char* tag,                // always "snk"
+        uint32_t log_level,             // 0=panic, 1=error, 2=warning, 3=info
+        uint32_t log_item_id,           // SNK_LOGITEM_*
+        const char* message_or_null,    // a message string, may be nullptr in release mode
+        uint32_t line_nr,               // line number in sokol_imgui.h
+        const char* filename_or_null,   // source filename, may be nullptr in release mode
+        void* user_data);
+    void* user_data;
+} snk_logger_t;
+
+
 typedef struct snk_desc_t {
-    int max_vertices;
+    int max_vertices;                   // default: 65536
+    int image_pool_size;                // default: 256
     sg_pixel_format color_format;
     sg_pixel_format depth_format;
     int sample_count;
     float dpi_scale;
     bool no_default_font;
+    snk_allocator_t allocator;          // optional memory allocation overrides (default: malloc/free)
+    snk_logger_t logger;                // optional log function override
 } snk_desc_t;
 
 SOKOL_NUKLEAR_API_DECL void snk_setup(const snk_desc_t* desc);
 SOKOL_NUKLEAR_API_DECL struct nk_context* snk_new_frame(void);
 SOKOL_NUKLEAR_API_DECL void snk_render(int width, int height);
-SOKOL_NUKLEAR_API_DECL nk_handle snk_nkhandle(sg_image img);
+SOKOL_NUKLEAR_API_DECL snk_image_t snk_make_image(const snk_image_desc_t* desc);
+SOKOL_NUKLEAR_API_DECL void snk_destroy_image(snk_image_t img);
+SOKOL_NUKLEAR_API_DECL snk_image_desc_t snk_query_image_desc(snk_image_t img);
+SOKOL_NUKLEAR_API_DECL nk_handle snk_nkhandle(snk_image_t img);
+SOKOL_NUKLEAR_API_DECL snk_image_t snk_image_from_nkhandle(nk_handle handle);
 #if !defined(SOKOL_NUKLEAR_NO_SOKOL_APP)
 SOKOL_NUKLEAR_API_DECL void snk_handle_event(const sapp_event* ev);
 SOKOL_NUKLEAR_API_DECL nk_flags snk_edit_string(struct nk_context *ctx, nk_flags flags, char *memory, int *len, int max, nk_plugin_filter filter);
@@ -241,6 +446,7 @@ SOKOL_NUKLEAR_API_DECL void snk_shutdown(void);
 
 /* reference-based equivalents for C++ */
 inline void snk_setup(const snk_desc_t& desc) { return snk_setup(&desc); }
+inline snk_image_t snk_make_image(const snk_image_desc_t& desc) { return snk_make_image(&desc); }
 
 #endif
 #endif /* SOKOL_NUKLEAR_INCLUDED */
@@ -249,10 +455,6 @@ inline void snk_setup(const snk_desc_t& desc) { return snk_setup(&desc); }
 #ifdef SOKOL_NUKLEAR_IMPL
 #define SOKOL_NUKLEAR_IMPL_INCLUDED (1)
 
-#if !defined(NK_UNDEFINED)
-#error "Please include nuklear.h before the sokol_nuklear.h implementation"
-#endif
-
 #if !defined(NK_INCLUDE_VERTEX_BUFFER_OUTPUT)
 #error "Please ensure that NK_INCLUDE_VERTEX_BUFFER_OUTPUT is #defined before including nuklear.h"
 #endif
@@ -261,9 +463,8 @@ inline void snk_setup(const snk_desc_t& desc) { return snk_setup(&desc); }
 #error "The sokol_nuklear.h implementation must be compiled as C."
 #endif
 
-
-#include <stddef.h> /* offsetof */
-#include <string.h> /* memset */
+#include <stdlib.h>
+#include <string.h> // memset
 
 #if defined(__EMSCRIPTEN__) && !defined(SOKOL_NUKLEAR_NO_SOKOL_APP) && !defined(SOKOL_DUMMY_BACKEND)
 #include <emscripten.h>
@@ -289,7 +490,13 @@ inline void snk_setup(const snk_desc_t& desc) { return snk_setup(&desc); }
     #endif
 #endif
 
-/* helper macros */
+#define _SNK_INIT_COOKIE (0xBABEBABE)
+#define _SNK_INVALID_SLOT_INDEX (0)
+#define _SNK_SLOT_SHIFT (16)
+#define _SNK_MAX_POOL_SIZE (1<<_SNK_SLOT_SHIFT)
+#define _SNK_SLOT_MASK (_SNK_MAX_POOL_SIZE-1)
+
+// helper macros
 #define _snk_def(val, def) (((val) == 0) ? (def) : (val))
 
 typedef struct _snk_vertex_t {
@@ -303,7 +510,40 @@ typedef struct _snk_vs_params_t {
     uint8_t _pad_8[8];
 } _snk_vs_params_t;
 
+typedef enum {
+    _SNK_RESOURCESTATE_INITIAL,
+    _SNK_RESOURCESTATE_ALLOC,
+    _SNK_RESOURCESTATE_VALID,
+    _SNK_RESOURCESTATE_FAILED,
+    _SNK_RESOURCESTATE_INVALID,
+    _SNK_RESOURCESTATE_FORCE_U32 = 0x7FFFFFFF
+} _snk_resource_state;
+
+typedef struct {
+    uint32_t id;
+    _snk_resource_state state;
+} _snk_slot_t;
+
+typedef struct {
+    int size;
+    int queue_top;
+    uint32_t* gen_ctrs;
+    int* free_queue;
+} _snk_pool_t;
+
+typedef struct {
+    _snk_slot_t slot;
+    sg_image image;
+    sg_sampler sampler;
+} _snk_image_t;
+
 typedef struct {
+    _snk_pool_t pool;
+    _snk_image_t* items;
+} _snk_image_pool_t;
+
+typedef struct {
+    uint32_t init_cookie;
     snk_desc_t desc;
     struct nk_context ctx;
     struct nk_font_atlas atlas;
@@ -312,11 +552,15 @@ typedef struct {
     size_t index_buffer_size;
     sg_buffer vbuf;
     sg_buffer ibuf;
-    sg_image img;
-    sg_sampler smp;
+    sg_image font_img;
+    sg_sampler font_smp;
+    snk_image_t default_font;
+    sg_image def_img;
+    sg_sampler def_smp;
     sg_shader shd;
     sg_pipeline pip;
-    bool is_osx;    /* return true if running on OSX (or HTML5 OSX), needed for copy/paste */
+    bool is_osx;    // true if running on OSX (or HTML5 OSX), needed for copy/paste
+    _snk_image_pool_t image_pool;
     #if !defined(SOKOL_NUKLEAR_NO_SOKOL_APP)
     int mouse_pos[2];
     float mouse_scroll[2];
@@ -1593,18 +1837,308 @@ static bool _snk_is_osx(void) {
 }
 #endif // !SOKOL_NUKLEAR_NO_SOKOL_APP
 
+// ██       ██████   ██████   ██████  ██ ███    ██  ██████
+// ██      ██    ██ ██       ██       ██ ████   ██ ██
+// ██      ██    ██ ██   ███ ██   ███ ██ ██ ██  ██ ██   ███
+// ██      ██    ██ ██    ██ ██    ██ ██ ██  ██ ██ ██    ██
+// ███████  ██████   ██████   ██████  ██ ██   ████  ██████
+//
+// >>logging
+#if defined(SOKOL_DEBUG)
+#define _SNK_LOGITEM_XMACRO(item,msg) #item ": " msg,
+static const char* _snk_log_messages[] = {
+    _SNK_LOG_ITEMS
+};
+#undef _SNK_LOGITEM_XMACRO
+#endif // SOKOL_DEBUG
+
+#define _SNK_PANIC(code) _snk_log(SNK_LOGITEM_ ##code, 0, 0, __LINE__)
+#define _SNK_ERROR(code) _snk_log(SNK_LOGITEM_ ##code, 1, 0, __LINE__)
+#define _SNK_WARN(code) _snk_log(SNK_LOGITEM_ ##code, 2, 0, __LINE__)
+#define _SNK_INFO(code) _snk_log(SNK_LOGITEM_ ##code, 3, 0, __LINE__)
+#define _SNK_LOGMSG(code,msg) _snk_log(SNK_LOGITEM_ ##code, 3, msg, __LINE__)
+
+static void _snk_log(snk_log_item_t log_item, uint32_t log_level, const char* msg, uint32_t line_nr) {
+    if (_snuklear.desc.logger.func) {
+        const char* filename = 0;
+        #if defined(SOKOL_DEBUG)
+            filename = __FILE__;
+            if (0 == msg) {
+                msg = _snk_log_messages[log_item];
+            }
+        #endif
+        _snuklear.desc.logger.func("snk", log_level, log_item, msg, line_nr, filename, _snuklear.desc.logger.user_data);
+    } else {
+        // for log level PANIC it would be 'undefined behaviour' to continue
+        if (log_level == 0) {
+            abort();
+        }
+    }
+}
+
+// ███    ███ ███████ ███    ███  ██████  ██████  ██    ██
+// ████  ████ ██      ████  ████ ██    ██ ██   ██  ██  ██
+// ██ ████ ██ █████   ██ ████ ██ ██    ██ ██████    ████
+// ██  ██  ██ ██      ██  ██  ██ ██    ██ ██   ██    ██
+// ██      ██ ███████ ██      ██  ██████  ██   ██    ██
+//
+// >>memory
+static void _snk_clear(void* ptr, size_t size) {
+    SOKOL_ASSERT(ptr && (size > 0));
+    memset(ptr, 0, size);
+}
+
+static void* _snk_malloc(size_t size) {
+    SOKOL_ASSERT(size > 0);
+    void* ptr;
+    if (_snuklear.desc.allocator.alloc) {
+        ptr = _snuklear.desc.allocator.alloc(size, _snuklear.desc.allocator.user_data);
+    } else {
+        ptr = malloc(size);
+    }
+    if (0 == ptr) {
+        _SNK_PANIC(MALLOC_FAILED);
+    }
+    return ptr;
+}
+
+static void* _snk_malloc_clear(size_t size) {
+    void* ptr = _snk_malloc(size);
+    _snk_clear(ptr, size);
+    return ptr;
+}
+
+static void _snk_free(void* ptr) {
+    if (_snuklear.desc.allocator.free) {
+        _snuklear.desc.allocator.free(ptr, _snuklear.desc.allocator.user_data);
+    } else {
+        free(ptr);
+    }
+}
+
+// ██████   ██████   ██████  ██
+// ██   ██ ██    ██ ██    ██ ██
+// ██████  ██    ██ ██    ██ ██
+// ██      ██    ██ ██    ██ ██
+// ██       ██████   ██████  ███████
+//
+// >>pool
+static void _snk_init_pool(_snk_pool_t* 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
+    size_t gen_ctrs_size = sizeof(uint32_t) * (size_t)pool->size;
+    pool->gen_ctrs = (uint32_t*) _snk_malloc_clear(gen_ctrs_size);
+    // it's not a bug to only reserve 'num' here
+    pool->free_queue = (int*) _snk_malloc_clear(sizeof(int) * (size_t)num);
+    // 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;
+    }
+}
+
+static void _snk_discard_pool(_snk_pool_t* pool) {
+    SOKOL_ASSERT(pool);
+    SOKOL_ASSERT(pool->free_queue);
+    _snk_free(pool->free_queue);
+    pool->free_queue = 0;
+    SOKOL_ASSERT(pool->gen_ctrs);
+    _snk_free(pool->gen_ctrs);
+    pool->gen_ctrs = 0;
+    pool->size = 0;
+    pool->queue_top = 0;
+}
+
+static int _snk_pool_alloc_index(_snk_pool_t* pool) {
+    SOKOL_ASSERT(pool);
+    SOKOL_ASSERT(pool->free_queue);
+    if (pool->queue_top > 0) {
+        int slot_index = pool->free_queue[--pool->queue_top];
+        SOKOL_ASSERT((slot_index > 0) && (slot_index < pool->size));
+        return slot_index;
+    } else {
+        // pool exhausted
+        return _SNK_INVALID_SLOT_INDEX;
+    }
+}
+
+static void _snk_pool_free_index(_snk_pool_t* pool, int slot_index) {
+    SOKOL_ASSERT((slot_index > _SNK_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));
+}
+
+/* initiailize a pool slot:
+    - 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 handle id
+*/
+static uint32_t _snk_slot_init(_snk_pool_t* pool, _snk_slot_t* 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 > _SNK_INVALID_SLOT_INDEX) && (slot_index < pool->size));
+    SOKOL_ASSERT((slot->state == _SNK_RESOURCESTATE_INITIAL) && (slot->id == SNK_INVALID_ID));
+    uint32_t ctr = ++pool->gen_ctrs[slot_index];
+    slot->id = (ctr<<_SNK_SLOT_SHIFT)|(slot_index & _SNK_SLOT_MASK);
+    slot->state = _SNK_RESOURCESTATE_ALLOC;
+    return slot->id;
+}
+
+// extract slot index from id
+static int _snk_slot_index(uint32_t id) {
+    int slot_index = (int) (id & _SNK_SLOT_MASK);
+    SOKOL_ASSERT(_SNK_INVALID_SLOT_INDEX != slot_index);
+    return slot_index;
+}
+
+static void _snk_init_item_pool(_snk_pool_t* pool, int pool_size, void** items_ptr, size_t item_size_bytes) {
+    // NOTE: the pools will have an additional item, since slot 0 is reserved
+    SOKOL_ASSERT(pool && (pool->size == 0));
+    SOKOL_ASSERT((pool_size > 0) && (pool_size < _SNK_MAX_POOL_SIZE));
+    SOKOL_ASSERT(items_ptr && (*items_ptr == 0));
+    SOKOL_ASSERT(item_size_bytes > 0);
+    _snk_init_pool(pool, pool_size);
+    const size_t pool_size_bytes = item_size_bytes * (size_t)pool->size;
+    *items_ptr = _snk_malloc_clear(pool_size_bytes);
+}
+
+static void _snk_discard_item_pool(_snk_pool_t* pool, void** items_ptr) {
+    SOKOL_ASSERT(pool && (pool->size != 0));
+    SOKOL_ASSERT(items_ptr && (*items_ptr != 0));
+    _snk_free(*items_ptr); *items_ptr = 0;
+    _snk_discard_pool(pool);
+}
+
+static void _snk_setup_image_pool(int pool_size) {
+    _snk_image_pool_t* p = &_snuklear.image_pool;
+    _snk_init_item_pool(&p->pool, pool_size, (void**)&p->items, sizeof(_snk_image_t));
+}
+
+static void _snk_discard_image_pool(void) {
+    _snk_image_pool_t* p = &_snuklear.image_pool;
+    _snk_discard_item_pool(&p->pool, (void**)&p->items);
+}
+
+static snk_image_t _snk_make_image_handle(uint32_t id) {
+    snk_image_t handle = { id };
+    return handle;
+}
+
+static _snk_image_t* _snk_image_at(uint32_t id) {
+    SOKOL_ASSERT(SNK_INVALID_ID != id);
+    const _snk_image_pool_t* p = &_snuklear.image_pool;
+    int slot_index = _snk_slot_index(id);
+    SOKOL_ASSERT((slot_index > _SNK_INVALID_SLOT_INDEX) && (slot_index < p->pool.size));
+    return &p->items[slot_index];
+}
+
+static _snk_image_t* _snk_lookup_image(uint32_t id) {
+    if (SNK_INVALID_ID != id) {
+        _snk_image_t* img = _snk_image_at(id);
+        if (img->slot.id == id) {
+            return img;
+        }
+    }
+    return 0;
+}
+
+static snk_image_t _snk_alloc_image(void) {
+    _snk_image_pool_t* p = &_snuklear.image_pool;
+    int slot_index = _snk_pool_alloc_index(&p->pool);
+    if (_SNK_INVALID_SLOT_INDEX != slot_index) {
+        uint32_t id = _snk_slot_init(&p->pool, &p->items[slot_index].slot, slot_index);
+        return _snk_make_image_handle(id);
+    } else {
+        // pool exhausted
+        return _snk_make_image_handle(SNK_INVALID_ID);
+    }
+}
+
+static _snk_resource_state _snk_init_image(_snk_image_t* img, const snk_image_desc_t* desc) {
+    SOKOL_ASSERT(img && (img->slot.state == _SNK_RESOURCESTATE_ALLOC));
+    SOKOL_ASSERT(desc);
+    img->image = desc->image;
+    img->sampler = desc->sampler;
+    return _SNK_RESOURCESTATE_VALID;
+}
+
+static void _snk_deinit_image(_snk_image_t* img) {
+    SOKOL_ASSERT(img);
+    img->image.id = SNK_INVALID_ID;
+    img->sampler.id = SNK_INVALID_ID;
+}
+
+static void _snk_destroy_image(snk_image_t img_id) {
+    _snk_image_t* img = _snk_lookup_image(img_id.id);
+    if (img) {
+        _snk_deinit_image(img);
+        _snk_image_pool_t* p = &_snuklear.image_pool;
+        _snk_clear(img, sizeof(_snk_image_t));
+        _snk_pool_free_index(&p->pool, _snk_slot_index(img_id.id));
+    }
+}
+
+static void _snk_destroy_all_images(void) {
+    _snk_image_pool_t* p = &_snuklear.image_pool;
+    for (int i = 0; i < p->pool.size; i++) {
+        _snk_image_t* img = &p->items[i];
+        _snk_destroy_image(_snk_make_image_handle(img->slot.id));
+    }
+}
+
+static snk_image_desc_t _snk_image_desc_defaults(const snk_image_desc_t* desc) {
+    SOKOL_ASSERT(desc);
+    snk_image_desc_t res = *desc;
+    res.image.id = _snk_def(res.image.id, _snuklear.def_img.id);
+    res.sampler.id = _snk_def(res.sampler.id, _snuklear.def_smp.id);
+    return res;
+}
+
+static snk_desc_t _snk_desc_defaults(const snk_desc_t* desc) {
+    SOKOL_ASSERT(desc);
+    snk_desc_t res = *desc;
+    res.max_vertices = _snk_def(res.max_vertices, 65536);
+    res.dpi_scale = _snk_def(res.dpi_scale, 1.0f);
+    res.image_pool_size = _snk_def(res.image_pool_size, 256);
+    return res;
+}
+
+// ██████  ██    ██ ██████  ██      ██  ██████
+// ██   ██ ██    ██ ██   ██ ██      ██ ██
+// ██████  ██    ██ ██████  ██      ██ ██
+// ██      ██    ██ ██   ██ ██      ██ ██
+// ██       ██████  ██████  ███████ ██  ██████
+//
+// >>public
 SOKOL_API_IMPL void snk_setup(const snk_desc_t* desc) {
     SOKOL_ASSERT(desc);
-    memset(&_snuklear, 0, sizeof(_snuklear));
-    _snuklear.desc = *desc;
-    _snuklear.desc.max_vertices = _snk_def(_snuklear.desc.max_vertices, 65536);
-    _snuklear.desc.dpi_scale = _snk_def(_snuklear.desc.dpi_scale, 1.0f);
+    _snk_clear(&_snuklear, sizeof(_snuklear));
+    _snuklear.init_cookie = _SNK_INIT_COOKIE;
+    _snuklear.desc = _snk_desc_defaults(desc);
     #if !defined(SOKOL_NUKLEAR_NO_SOKOL_APP)
     _snuklear.is_osx = _snk_is_osx();
     #endif
     // can keep color_format, depth_format and sample_count as is,
     // since sokol_gfx.h will do its own default-value handling
 
+    _snk_setup_image_pool(_snuklear.desc.image_pool_size);
+
     // initialize Nuklear
     nk_bool init_res = nk_init_default(&_snuklear.ctx, 0);
     SOKOL_ASSERT(1 == init_res); (void)init_res;    // silence unused warning in release mode
@@ -1633,6 +2167,24 @@ SOKOL_API_IMPL void snk_setup(const snk_desc_t* desc) {
         .label = "sokol-nuklear-indices"
     });
 
+    // default font sampler
+    _snuklear.font_smp = sg_make_sampler(&(sg_sampler_desc){
+        .min_filter = SG_FILTER_LINEAR,
+        .mag_filter = SG_FILTER_LINEAR,
+        .wrap_u = SG_WRAP_CLAMP_TO_EDGE,
+        .wrap_v = SG_WRAP_CLAMP_TO_EDGE,
+        .label = "sokol-nuklear-font-sampler",
+    });
+
+    // default user-image sampler
+    _snuklear.def_smp = sg_make_sampler(&(sg_sampler_desc){
+        .min_filter = SG_FILTER_NEAREST,
+        .mag_filter = SG_FILTER_NEAREST,
+        .wrap_u = SG_WRAP_CLAMP_TO_EDGE,
+        .wrap_v = SG_WRAP_CLAMP_TO_EDGE,
+        .label = "sokol-nuklear-default-sampler",
+    });
+
     // default font texture
     if (!_snuklear.desc.no_default_font) {
         nk_font_atlas_init_default(&_snuklear.atlas);
@@ -1640,7 +2192,7 @@ SOKOL_API_IMPL void snk_setup(const snk_desc_t* desc) {
         int font_width = 0, font_height = 0;
         const void* pixels = nk_font_atlas_bake(&_snuklear.atlas, &font_width, &font_height, NK_FONT_ATLAS_RGBA32);
         SOKOL_ASSERT((font_width > 0) && (font_height > 0));
-        _snuklear.img = sg_make_image(&(sg_image_desc){
+        _snuklear.font_img = sg_make_image(&(sg_image_desc){
             .width = font_width,
             .height = font_height,
             .pixel_format = SG_PIXELFORMAT_RGBA8,
@@ -1650,19 +2202,26 @@ SOKOL_API_IMPL void snk_setup(const snk_desc_t* desc) {
             },
             .label = "sokol-nuklear-font"
         });
-        nk_font_atlas_end(&_snuklear.atlas, nk_handle_id((int)_snuklear.img.id), 0);
+        _snuklear.default_font = snk_make_image(&(snk_image_desc_t){
+            .image = _snuklear.font_img,
+            .sampler = _snuklear.font_smp,
+        });
+        nk_font_atlas_end(&_snuklear.atlas, snk_nkhandle(_snuklear.default_font), 0);
         nk_font_atlas_cleanup(&_snuklear.atlas);
         if (_snuklear.atlas.default_font) {
             nk_style_set_font(&_snuklear.ctx, &_snuklear.atlas.default_font->handle);
         }
     }
 
-    // default font sampler
-    _snuklear.smp = sg_make_sampler(&(sg_sampler_desc){
-        .wrap_u = SG_WRAP_CLAMP_TO_EDGE,
-        .wrap_v = SG_WRAP_CLAMP_TO_EDGE,
-        .min_filter = SG_FILTER_LINEAR,
-        .mag_filter = SG_FILTER_LINEAR,
+    // default user image
+    static uint32_t def_pixels[64];
+    memset(def_pixels, 0xFF, sizeof(def_pixels));
+    _snuklear.def_img = sg_make_image(&(sg_image_desc){
+        .width = 8,
+        .height = 8,
+        .pixel_format = SG_PIXELFORMAT_RGBA8,
+        .data.subimage[0][0] = SG_RANGE(def_pixels),
+        .label = "sokol-nuklear-default-image",
     });
 
     // shader
@@ -1769,6 +2328,7 @@ SOKOL_API_IMPL void snk_setup(const snk_desc_t* desc) {
 }
 
 SOKOL_API_IMPL void snk_shutdown(void) {
+    SOKOL_ASSERT(_SNK_INIT_COOKIE == _snuklear.init_cookie);
     nk_free(&_snuklear.ctx);
     nk_font_atlas_clear(&_snuklear.atlas);
 
@@ -1776,14 +2336,20 @@ SOKOL_API_IMPL void snk_shutdown(void) {
     sg_push_debug_group("sokol-nuklear");
     sg_destroy_pipeline(_snuklear.pip);
     sg_destroy_shader(_snuklear.shd);
-    sg_destroy_sampler(_snuklear.smp);
-    sg_destroy_image(_snuklear.img);
+    sg_destroy_sampler(_snuklear.font_smp);
+    sg_destroy_image(_snuklear.font_img);
+    sg_destroy_sampler(_snuklear.def_smp);
+    sg_destroy_image(_snuklear.def_img);
     sg_destroy_buffer(_snuklear.ibuf);
     sg_destroy_buffer(_snuklear.vbuf);
     sg_pop_debug_group();
+    _snk_destroy_all_images();
+    _snk_discard_image_pool();
+    _snuklear.init_cookie = 0;
 }
 
 SOKOL_API_IMPL struct nk_context* snk_new_frame(void) {
+    SOKOL_ASSERT(_SNK_INIT_COOKIE == _snuklear.init_cookie);
     #if !defined(SOKOL_NUKLEAR_NO_SOKOL_APP)
     nk_input_begin(&_snuklear.ctx);
     if (_snuklear.mouse_did_move) {
@@ -1808,7 +2374,7 @@ SOKOL_API_IMPL struct nk_context* snk_new_frame(void) {
         for (size_t i = 0; i < char_buffer_len; i++) {
             nk_input_char(&_snuklear.ctx, _snuklear.char_buffer[i]);
         }
-        memset(_snuklear.char_buffer, 0, NK_INPUT_MAX);
+        _snk_clear(_snuklear.char_buffer, NK_INPUT_MAX);
     }
     for (int i = 0; i < NK_KEY_MAX; i++) {
         if (_snuklear.keys_down[i]) {
@@ -1827,19 +2393,62 @@ SOKOL_API_IMPL struct nk_context* snk_new_frame(void) {
     return &_snuklear.ctx;
 }
 
-SOKOL_API_IMPL nk_handle snk_nkhandle(sg_image img) {
-    return (nk_handle) { .ptr = (void*)(uintptr_t)img.id };
+SOKOL_API_IMPL snk_image_t snk_make_image(const snk_image_desc_t* desc) {
+    SOKOL_ASSERT(_SNK_INIT_COOKIE == _snuklear.init_cookie);
+    SOKOL_ASSERT(desc);
+    const snk_image_desc_t desc_def = _snk_image_desc_defaults(desc);
+    snk_image_t img_id = _snk_alloc_image();
+    _snk_image_t* img = _snk_lookup_image(img_id.id);
+    if (img) {
+        img->slot.state = _snk_init_image(img, &desc_def);
+        SOKOL_ASSERT((img->slot.state == _SNK_RESOURCESTATE_VALID) || (img->slot.state == _SNK_RESOURCESTATE_FAILED));
+    } else {
+        _SNK_ERROR(IMAGE_POOL_EXHAUSTED);
+    }
+    return img_id;
+}
+
+SOKOL_API_IMPL void snk_destroy_image(snk_image_t img_id) {
+    SOKOL_ASSERT(_SNK_INIT_COOKIE == _snuklear.init_cookie);
+    _snk_destroy_image(img_id);
 }
 
-_SOKOL_PRIVATE uint32_t _snk_imageid_from_nkhandle(nk_handle h) {
-    uint32_t img_id = (uint32_t)(uintptr_t)h.ptr;
-    if (0 == img_id) {
-        img_id = _snuklear.img.id;
+SOKOL_API_IMPL snk_image_desc_t snk_query_image_desc(snk_image_t img_id) {
+    SOKOL_ASSERT(_SNK_INIT_COOKIE == _snuklear.init_cookie);
+    _snk_image_t* img = _snk_lookup_image(img_id.id);
+    if (img) {
+        return (snk_image_desc_t){
+            .image = img->image,
+            .sampler = img->sampler,
+        };
+    } else {
+        return (snk_image_desc_t){0};
+    }
+}
+
+SOKOL_API_IMPL nk_handle snk_nkhandle(snk_image_t img) {
+    SOKOL_ASSERT(_SNK_INIT_COOKIE == _snuklear.init_cookie);
+    return (nk_handle) { .id = (int)img.id };
+}
+
+SOKOL_API_IMPL snk_image_t snk_image_from_nkhandle(nk_handle h) {
+    SOKOL_ASSERT(_SNK_INIT_COOKIE == _snuklear.init_cookie);
+    return (snk_image_t){ .id = (uint32_t) h.id };
+}
+
+static void _snk_bind_image_sampler(sg_bindings* bindings, nk_handle h) {
+    _snk_image_t* img = _snk_lookup_image((uint32_t)h.id);
+    if (img) {
+        bindings->fs.images[0] = img->image;
+        bindings->fs.samplers[0] = img->sampler;
+    } else {
+        bindings->fs.images[0] = _snuklear.def_img;
+        bindings->fs.samplers[0] = _snuklear.def_smp;
     }
-    return img_id;
 }
 
 SOKOL_API_IMPL void snk_render(int width, int height) {
+    SOKOL_ASSERT(_SNK_INIT_COOKIE == _snuklear.init_cookie);
     static const struct nk_draw_vertex_layout_element vertex_layout[] = {
         {NK_VERTEX_POSITION, NK_FORMAT_FLOAT, NK_OFFSETOF(struct _snk_vertex_t, pos)},
         {NK_VERTEX_TEXCOORD, NK_FORMAT_FLOAT, NK_OFFSETOF(struct _snk_vertex_t, uv)},
@@ -1889,23 +2498,22 @@ SOKOL_API_IMPL void snk_render(int width, int height) {
         // Iterate through the command list, rendering each one
         const struct nk_draw_command* cmd = NULL;
         int idx_offset = 0;
+        sg_bindings bindings = {
+            .vertex_buffers[0] = _snuklear.vbuf,
+            .index_buffer = _snuklear.ibuf,
+            .index_buffer_offset = idx_offset
+        };
         nk_draw_foreach(cmd, &_snuklear.ctx, &cmds) {
             if (cmd->elem_count > 0) {
-                sg_apply_bindings(&(sg_bindings){
-                    .fs.images[0].id = _snk_imageid_from_nkhandle(cmd->texture),
-                    .fs.samplers[0] = _snuklear.smp,
-                    .vertex_buffers[0] = _snuklear.vbuf,
-                    .index_buffer = _snuklear.ibuf,
-                    .vertex_buffer_offsets[0] = 0,
-                    .index_buffer_offset = idx_offset
-                });
+                _snk_bind_image_sampler(&bindings, cmd->texture);
+                sg_apply_bindings(&bindings);
                 sg_apply_scissor_rectf(cmd->clip_rect.x * dpi_scale,
                                        cmd->clip_rect.y * dpi_scale,
                                        cmd->clip_rect.w * dpi_scale,
                                        cmd->clip_rect.h * dpi_scale,
                                        true);
                 sg_draw(0, (int)cmd->elem_count, 1);
-                idx_offset += (int)cmd->elem_count * (int)sizeof(uint16_t);
+                bindings.index_buffer_offset += (int)cmd->elem_count * (int)sizeof(uint16_t);
             }
         }
         sg_apply_scissor_rect(0, 0, fb_width, fb_height, true);
@@ -1985,6 +2593,7 @@ _SOKOL_PRIVATE enum nk_keys _snk_event_to_nuklearkey(const sapp_event* ev) {
 }
 
 SOKOL_API_IMPL void snk_handle_event(const sapp_event* ev) {
+    SOKOL_ASSERT(_SNK_INIT_COOKIE == _snuklear.init_cookie);
     const float dpi_scale = _snuklear.desc.dpi_scale;
     switch (ev->type) {
         case SAPP_EVENTTYPE_MOUSE_DOWN:
@@ -2104,6 +2713,7 @@ SOKOL_API_IMPL void snk_handle_event(const sapp_event* ev) {
 }
 
 SOKOL_API_IMPL nk_flags snk_edit_string(struct nk_context *ctx, nk_flags flags, char *memory, int *len, int max, nk_plugin_filter filter) {
+    SOKOL_ASSERT(_SNK_INIT_COOKIE == _snuklear.init_cookie);
     nk_flags event = nk_edit_string(ctx, flags, memory, len, max, filter);
     if ((event & NK_EDIT_ACTIVATED) && !sapp_keyboard_shown()) {
         sapp_show_keyboard(true);