Browse Source

Merge pull request #783 from floooh/saudio-new-logging

Implement new-style logging for all headers (which had any logging).
Andre Weissflog 2 years ago
parent
commit
fc651cf76c

+ 0 - 9
.github/workflows/main.yml

@@ -63,12 +63,3 @@ jobs:
           run: |
             cd tests
             ./test_android.sh
-    uwp:
-        runs-on: windows-latest
-        steps:
-        - uses: actions/checkout@v3
-        - name: test_uwp
-          run: |
-            cd tests
-            test_uwp.cmd
-          shell: cmd

+ 83 - 0
CHANGELOG.md

@@ -1,5 +1,88 @@
 ## Updates
 
+- **13-Feb-2023**: The way logging works has been completely revamped in
+  the sokol headers. UWP support has been removed from sokol_audio.h
+  and sokol_app.h (this also means that the sokol headers no longer contain
+  any C++ code).
+
+  **REQUIRED ACTION**: Since the sokol headers are now completely silent
+  without a logging callback (explanation below), it is highly recommened
+  to use the standard logging callback provided by the new header ```sokol_log.h```.
+  For instance for sokol_gfx.h it looks like this:
+
+    ```c
+    #include "sokol_log.h"
+    //...
+        sg_setup(&(sg_desc){
+            //...
+            .logger.func = slog_func,
+        });
+    ```
+
+  All sokol samples have been updated to use sokol_log.h for logging.
+
+  The former logging callback is now a combined
+  logging- and error-reporting callback, and more information is available
+  to the logging function:
+    - a 'tag string' which identifies the sokol headers, this string
+      is identical with the API prefix (e.g. "sg" for sokol_gfx.h,
+      "sapp" for sokol_app.h etc...)
+    - a numeric log level: 0=panic, 1=error, 2=warning, 3=info
+    - a numeric 'log item id' (think of it as error code, but since
+      not only errors are reported I called it a log item id)
+    - a human readable error message
+    - a source file line number where the log item was reported
+    - the file path of the sokol header
+
+  Log level ```panic``` is special in that it terminates execution inside
+  the log function. When a sokol header issues a panic log message, it means
+  that the problem is so big that execution can not continue. By default,
+  the sokol headers and the standard log function in sokol_log.h call
+  ```abort()``` when a panic log message is issued.
+
+  In debug mode (NDEBUG not defined, or SOKOL_DEBUG defined), a log message
+  (form sokol_spine.h) will look like this:
+
+  ```
+  [sspine][error][id:12] /Users/floh/projects/sokol/util/sokol_spine.h:3472:0:
+      SKELETON_DESC_NO_ATLAS: no atlas object provided in sspine_skeleton_desc.atlas
+  ```
+  The information can be 'parsed' like this:
+    - ```[sspine]```: it's a message from sokol_spine.h
+    - ```[error]```: it's an error
+    - ```[id:12]```: the numeric log item id (associated with ```SKELETON_DESC_NO_ATLAS``` below)
+    - source file path and line number in a compiler-specific format - in some IDEs and terminals
+      this is a clickable link
+    - the line below is the human readable log item id and message
+
+  In release mode (NDEBUG is defined and SOKOL_DEBUG is not defined), log messages
+  are drastically reduced (the reason is to not bloat the executable with all the extra string data):
+
+  ```
+  [sspine][error][id:12][line:3472]
+  ```
+  ...this reduced information still gives all the necessary information to identify the location and type of error.
+
+  A custom logging function must adhere to a few rules:
+
+    - must be re-entrant because it might be called from different threads
+    - must treat **all** provided string pointers as optional (can be null)
+    - don't store the string pointers, copy the string data instead
+    - must not return for log level panic
+
+  A new header ```sokol_log.h``` has been added to provide a standard logging callback implementation
+  which provides logging output on all platforms to stderr and/or platform specific logging
+  facilities. ```sokol_log.h``` only uses fputs() and platform specific logging function instead
+  of fprintf() to preverse some executable size.
+
+  **QUESTION**: Why are the sokol headers now silent, unless a logging callback is installed?
+  This is mainly because a standard logging function which does something meaningful on all
+  platforms (including Windows and Android) isn't trivial. E.g. printing to stderr is not
+  enough. It's better to move that stuff into a centralized place in a separate header,
+  but since the core sokol headers must not (statically) depend on other sokol headers
+  the only solution that made sense was to provide a standard logging function which must
+  be 'registered' as a callback.
+
 - **26-Jan-2023**: Work on SRGB support in sokol_gfx.h has started, but
   this requires more effort to be really usable. For now, only a new
   pixel format has been added: SG_PIXELFORMAT_SRGB8A8 (see https://github.com/floooh/sokol/pull/758,

+ 17 - 6
README.md

@@ -4,7 +4,8 @@ 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-Jan-2023** sokol_audio.h: new AAudio backend for Android, activation fix for WebAudio on Chrome for Android.
+[**See what's new**](https://github.com/floooh/sokol/blob/master/CHANGELOG.md) (**13-Feb-2023** logging has been replaced with a
+combined logging- and error-reporting callback, **ACTION REQUIRED** (see changelog for details))
 
 [![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)
 
@@ -95,6 +96,7 @@ A triangle in C99 with GLFW:
 #define SOKOL_IMPL
 #define SOKOL_GLCORE33
 #include "sokol_gfx.h"
+#include "sokol_log.h"
 #define GLFW_INCLUDE_NONE
 #include "GLFW/glfw3.h"
 
@@ -111,7 +113,9 @@ int main() {
     glfwSwapInterval(1);
 
     /* setup sokol_gfx */
-    sg_setup(&(sg_desc){0});
+    sg_setup(&(sg_desc){
+        .logger.func = slog_func,
+    });
 
     /* a vertex buffer */
     const float vertices[] = {
@@ -202,13 +206,15 @@ to split the Objective-C code from the C code of the sample):
 ```c
 #include "sokol_gfx.h"
 #include "sokol_app.h"
+#include "sokol_log.h"
 #include "sokol_glue.h"
 
 sg_pass_action pass_action;
 
 void init(void) {
     sg_setup(&(sg_desc){
-        .context = sapp_sgcontext()
+        .context = sapp_sgcontext(),
+        .logger.func = slog_func,
     });
     pass_action = (sg_pass_action) {
         .colors[0] = { .action=SG_ACTION_CLEAR, .value={1.0f, 0.0f, 0.0f, 1.0f} }
@@ -235,6 +241,7 @@ sapp_desc sokol_main(int argc, char* argv[]) {
         .width = 400,
         .height = 300,
         .window_title = "Clear Sample",
+        .logger.func = slog_func,
     };
 }
 ```
@@ -269,7 +276,8 @@ static void stream_cb(float* buffer, int num_frames, int num_channels) {
 int main() {
     // init sokol-audio with default params
     saudio_setup(&(saudio_desc){
-        .stream_cb = stream_cb
+        .stream_cb = stream_cb,
+        .logger.func = slog_func,
     });
 
     // run main loop
@@ -286,7 +294,9 @@ The same code using the push-model
 #define BUF_SIZE (32)
 int main() {
     // init sokol-audio with default params, no callback
-    saudio_setup(&(saudio_desc){0});
+    saudio_setup(&(saudio_desc){
+        .logger.func = slog_func,
+    });
     assert(saudio_channels() == 1);
 
     // a small intermediate buffer so we don't need to push
@@ -327,6 +337,7 @@ Simple C99 example loading a file into a static buffer:
 
 ```c
 #include "sokol_fetch.h"
+#include "sokol_log.h"
 
 static void response_callback(const sfetch_response*);
 
@@ -337,7 +348,7 @@ static uint8_t buffer[MAX_FILE_SIZE];
 static void init(void) {
     ...
     // setup sokol-fetch with default config:
-    sfetch_setup(&(sfetch_desc_t){0});
+    sfetch_setup(&(sfetch_desc_t){ .logger.func = slog_func });
 
     // start loading a file into a statically allocated buffer:
     sfetch_send(&(sfetch_request_t){

+ 1 - 0
bindgen/gen_all.py

@@ -1,6 +1,7 @@
 import os, gen_nim, gen_zig, gen_odin
 
 tasks = [
+    [ '../sokol_log.h',            'slog_',     [] ],
     [ '../sokol_gfx.h',            'sg_',       [] ],
     [ '../sokol_app.h',            'sapp_',     [] ],
     [ '../sokol_glue.h',           'sapp_sg',   ['sg_'] ],

+ 27 - 15
bindgen/gen_nim.py

@@ -10,6 +10,7 @@ import gen_util as util
 import os, shutil, sys
 
 module_names = {
+    'slog_':    'log',
     'sg_':      'gfx',
     'sapp_':    'app',
     'sapp_sg':  'glue',
@@ -21,6 +22,7 @@ module_names = {
 }
 
 c_source_paths = {
+    'slog_':    'sokol-nim/src/sokol/c/sokol_log.c',
     'sg_':      'sokol-nim/src/sokol/c/sokol_gfx.c',
     'sapp_':    'sokol-nim/src/sokol/c/sokol_app.c',
     'sapp_sg':  'sokol-nim/src/sokol/c/sokol_glue.c',
@@ -31,6 +33,10 @@ c_source_paths = {
     'sshape_':  'sokol-nim/src/sokol/c/sokol_shape.c',
 }
 
+c_callbacks = [
+    'slog_func',
+]
+
 ignores = [
     'sdtx_printf',
     'sdtx_vprintf',
@@ -47,6 +53,8 @@ overrides = {
     'SG_BUFFERTYPE_INDEXBUFFER':    'SG_BUFFERTYPE_INDEX_BUFFER',
     'SG_ACTION_DONTCARE':           'SG_ACTION_DONT_CARE',
     'ptr':                          'addr', # range ptr
+    'func':                         'fn',
+    'slog_func':                    'fn',
 }
 
 enumPrefixOverrides = {
@@ -412,22 +420,26 @@ def funcdecl_result(decl, prefix):
     return nim_res_type
 
 def gen_func_nim(decl, prefix):
-    nim_func_name = as_camel_case(check_override(decl['name']), prefix, wrap=False)
+    c_func_name = decl['name']
+    nim_func_name = as_camel_case(check_override(c_func_name), prefix, wrap=False)
     nim_res_type = funcdecl_result(decl, prefix)
-    l(f"proc c_{nim_func_name}({funcdecl_args_c(decl, prefix)}):{nim_res_type} {{.cdecl, importc:\"{decl['name']}\".}}")
-    l(f"proc {wrap_keywords(nim_func_name)}*({funcdecl_args_nim(decl, prefix)}):{nim_res_type} =")
-    s = f"    c_{nim_func_name}("
-    for i, param_decl in enumerate(decl['params']):
-        if i > 0:
-            s += ", "
-        arg_name = param_decl['name']
-        arg_type = param_decl['type']
-        if is_const_struct_ptr(arg_type):
-            s += f"unsafeAddr({arg_name})"
-        else:
-            s += arg_name
-    s += ")"
-    l(s)
+    if c_func_name in c_callbacks:
+        l(f"proc {nim_func_name}*({funcdecl_args_c(decl, prefix)}):{nim_res_type} {{.cdecl, importc:\"{c_func_name}\".}}")
+    else:
+        l(f"proc c_{nim_func_name}({funcdecl_args_c(decl, prefix)}):{nim_res_type} {{.cdecl, importc:\"{c_func_name}\".}}")
+        l(f"proc {wrap_keywords(nim_func_name)}*({funcdecl_args_nim(decl, prefix)}):{nim_res_type} =")
+        s = f"    c_{nim_func_name}("
+        for i, param_decl in enumerate(decl['params']):
+            if i > 0:
+                s += ", "
+            arg_name = param_decl['name']
+            arg_type = param_decl['type']
+            if is_const_struct_ptr(arg_type):
+                s += f"unsafeAddr({arg_name})"
+            else:
+                s += arg_name
+        s += ")"
+        l(s)
     l("")
 
 def gen_array_converters(decl, prefix):

+ 2 - 0
bindgen/gen_odin.py

@@ -12,6 +12,7 @@ c_root = f'{bindings_root}/c'
 module_root = f'{bindings_root}/sokol'
 
 module_names = {
+    'slog_':    'log',
     'sg_':      'gfx',
     'sapp_':    'app',
     'sapp_sg':  'glue',
@@ -65,6 +66,7 @@ system_libs = {
 }
 
 c_source_names = {
+    'slog_':    'sokol_log.c',
     'sg_':      'sokol_gfx.c',
     'sapp_':    'sokol_app.c',
     'sapp_sg':  'sokol_glue.c',

+ 34 - 23
bindgen/gen_zig.py

@@ -12,6 +12,7 @@ import os, shutil, sys
 import gen_util as util
 
 module_names = {
+    'slog_':    'log',
     'sg_':      'gfx',
     'sapp_':    'app',
     'stm_':     'time',
@@ -22,6 +23,7 @@ module_names = {
 }
 
 c_source_paths = {
+    'slog_':    'sokol-zig/src/sokol/c/sokol_log.c',
     'sg_':      'sokol-zig/src/sokol/c/sokol_gfx.c',
     'sapp_':    'sokol-zig/src/sokol/c/sokol_app.c',
     'stm_':     'sokol-zig/src/sokol/c/sokol_time.c',
@@ -38,6 +40,11 @@ ignores = [
     'sg_trace_hooks',
 ]
 
+# functions that need to be exposed as 'raw' C callbacks without a Zig wrapper function
+c_callbacks = [
+    'slog_func'
+]
+
 # NOTE: syntax for function results: "func_name.RESULT"
 overrides = {
     'sgl_error':                            'sgl_get_error',   # 'error' is reserved in Zig
@@ -391,31 +398,35 @@ def gen_func_c(decl, prefix):
 def gen_func_zig(decl, prefix):
     c_func_name = decl['name']
     zig_func_name = util.as_lower_camel_case(check_override(decl['name']), prefix)
-    zig_res_type = funcdecl_result_zig(decl, prefix)
-    l(f"pub fn {zig_func_name}({funcdecl_args_zig(decl, prefix)}) {zig_res_type} {{")
-    if is_zig_string(zig_res_type):
-        # special case: convert C string to Zig string slice
-        s = f"    return cStrToZig({c_func_name}("
-    elif zig_res_type != 'void':
-        s = f"    return {c_func_name}("
+    if c_func_name in c_callbacks:
+        # a simple forwarded C callback function
+        l(f"pub const {zig_func_name} = {c_func_name};")
     else:
-        s = f"    {c_func_name}("
-    for i, param_decl in enumerate(decl['params']):
-        if i > 0:
-            s += ", "
-        arg_name = param_decl['name']
-        arg_type = param_decl['type']
-        if is_const_struct_ptr(arg_type):
-            s += f"&{arg_name}"
-        elif util.is_string_ptr(arg_type):
-            s += f"@ptrCast([*c]const u8,{arg_name})"
+        zig_res_type = funcdecl_result_zig(decl, prefix)
+        l(f"pub fn {zig_func_name}({funcdecl_args_zig(decl, prefix)}) {zig_res_type} {{")
+        if is_zig_string(zig_res_type):
+            # special case: convert C string to Zig string slice
+            s = f"    return cStrToZig({c_func_name}("
+        elif zig_res_type != 'void':
+            s = f"    return {c_func_name}("
         else:
-            s += arg_name
-    if is_zig_string(zig_res_type):
-        s += ")"
-    s += ");"
-    l(s)
-    l("}")
+            s = f"    {c_func_name}("
+        for i, param_decl in enumerate(decl['params']):
+            if i > 0:
+                s += ", "
+            arg_name = param_decl['name']
+            arg_type = param_decl['type']
+            if is_const_struct_ptr(arg_type):
+                s += f"&{arg_name}"
+            elif util.is_string_ptr(arg_type):
+                s += f"@ptrCast([*c]const u8,{arg_name})"
+            else:
+                s += arg_name
+        if is_zig_string(zig_res_type):
+            s += ")"
+        s += ");"
+        l(s)
+        l("}")
 
 def pre_parse(inp):
     global struct_types

File diff suppressed because it is too large
+ 358 - 235
sokol_app.h


File diff suppressed because it is too large
+ 305 - 326
sokol_audio.h


+ 245 - 123
sokol_fetch.h

@@ -75,9 +75,11 @@
 
     (1) initialize sokol-fetch with default parameters (but NOTE that the
         default setup parameters provide a safe-but-slow "serialized"
-        operation):
+        operation). In order to see any logging output in case or errors
+        you should always provide a logging function
+        (such as 'slog_func' from sokol_log.h):
 
-        sfetch_setup(&(sfetch_desc_t){ 0 });
+        sfetch_setup(&(sfetch_desc_t){ .logger.func = slog_func });
 
     (2) send a fetch-request to load a file from the current directory
         into a buffer big enough to hold the entire file content:
@@ -834,26 +836,46 @@
     was called, so you don't need to worry about thread-safety.
 
 
-    LOG FUNCTION OVERRIDE
-    =====================
-    You can override the log function at initialization time like this:
+    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:
 
-        void my_log(const char* message, void* user_data) {
-            printf("sfetch says: \s\n", message);
+        #include "sokol_log.h"
+
+        sfetch_setup(&(sfetch_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. 'sfetch'
+                    uint32_t log_level,             // 0=panic, 1=error, 2=warn, 3=info
+                    uint32_t log_item_id,           // SFETCH_LOGITEM_*
+                    const char* message_or_null,    // a message string, may be nullptr in release mode
+                    uint32_t line_nr,               // line number in sokol_fetch.h
+                    const char* filename_or_null,   // source filename, may be nullptr in release mode
+                    void* user_data)
+        {
+            ...
         }
 
-        ...
-            sfetch_setup(&(sfetch_desc_t){
-                // ...
-                .logger = {
-                    .log_cb = my_log,
-                    .user_data = ...,
-                }
-            });
-        ...
+    ...and then setup sokol-fetch like this:
+
+        sfetch_setup(&(sfetch_desc_t){
+            .logger = {
+                .func = my_log,
+                .user_data = my_user_data,
+            }
+        });
 
-    If no overrides are provided, puts will be used on most platforms.
-    On Android, __android_log_write will be used instead.
+    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.
 
 
     FUTURE PLANS / V2.0 IDEA DUMP
@@ -921,6 +943,55 @@
 extern "C" {
 #endif
 
+/*
+    sfetch_log_item_t
+
+    Log items are defined via X-Macros, and expanded to an
+    enum 'sfetch_log_item', and in debug mode only,
+    corresponding strings.
+
+    Used as parameter in the logging callback.
+*/
+#define _SFETCH_LOG_ITEMS \
+    _SFETCH_LOGITEM_XMACRO(OK, "Ok") \
+    _SFETCH_LOGITEM_XMACRO(MALLOC_FAILED, "memory allocation failed") \
+    _SFETCH_LOGITEM_XMACRO(FILE_PATH_UTF8_DECODING_FAILED, "failed converting file path from UTF8 to wide") \
+    _SFETCH_LOGITEM_XMACRO(SEND_QUEUE_FULL, "send queue full (adjust via sfetch_desc_t.max_requests)")  \
+    _SFETCH_LOGITEM_XMACRO(REQUEST_CHANNEL_INDEX_TOO_BIG, "channel index too big (adjust via sfetch_desc_t.num_channels)") \
+    _SFETCH_LOGITEM_XMACRO(REQUEST_PATH_IS_NULL, "file path is nullptr (sfetch_request_t.path)") \
+    _SFETCH_LOGITEM_XMACRO(REQUEST_PATH_TOO_LONG, "file path is too long (SFETCH_MAX_PATH)") \
+    _SFETCH_LOGITEM_XMACRO(REQUEST_CALLBACK_MISSING, "no callback provided (sfetch_request_t.callback)") \
+    _SFETCH_LOGITEM_XMACRO(REQUEST_CHUNK_SIZE_GREATER_BUFFER_SIZE, "chunk size is greater buffer size (sfetch_request_t.chunk_size vs .buffer.size)") \
+    _SFETCH_LOGITEM_XMACRO(REQUEST_USERDATA_PTR_IS_SET_BUT_USERDATA_SIZE_IS_NULL, "user data ptr is set but user data size is null (sfetch_request_t.user_data.ptr vs .size)") \
+    _SFETCH_LOGITEM_XMACRO(REQUEST_USERDATA_PTR_IS_NULL_BUT_USERDATA_SIZE_IS_NOT, "user data ptr is null but size is not (sfetch_request_t.user_data.ptr vs .size)") \
+    _SFETCH_LOGITEM_XMACRO(REQUEST_USERDATA_SIZE_TOO_BIG, "user data size too big (see SFETCH_MAX_USERDATA_UINT64)") \
+    _SFETCH_LOGITEM_XMACRO(CLAMPING_NUM_CHANNELS_TO_MAX_CHANNELS, "clamping num channels to SFETCH_MAX_CHANNELS") \
+    _SFETCH_LOGITEM_XMACRO(REQUEST_POOL_EXHAUSTED, "request pool exhausted (tweak via sfetch_desc_t.max_requests)") \
+
+#define _SFETCH_LOGITEM_XMACRO(item,msg) SFETCH_LOGITEM_##item,
+typedef enum sfetch_log_item_t {
+    _SFETCH_LOG_ITEMS
+} sfetch_log_item_t;
+#undef _SFETCH_LOGITEM_XMACRO
+
+/*
+    sfetch_logger_t
+
+    Used in sfetch_desc_t to provide a custom logging and error reporting
+    callback to sokol-fetch.
+*/
+typedef struct sfetch_logger_t {
+    void (*func)(
+        const char* tag,                // always "sfetch"
+        uint32_t log_level,             // 0=panic, 1=error, 2=warning, 3=info
+        uint32_t log_item_id,           // SFETCH_LOGITEM_*
+        const char* message_or_null,    // a message string, may be nullptr in release mode
+        uint32_t line_nr,               // line number in sokol_fetch.h
+        const char* filename_or_null,   // source filename, may be nullptr in release mode
+        void* user_data);
+    void* user_data;
+} sfetch_logger_t;
+
 /*
     sfetch_range_t
 
@@ -958,24 +1029,13 @@ typedef struct sfetch_allocator_t {
     void* user_data;
 } sfetch_allocator_t;
 
-/*
-    sfetch_logger_t
-
-    Used in sfetch_desc_t to provide custom log callbacks to sokol_fetch.h.
-    Default behavior is SOKOL_LOG(message).
-*/
-typedef struct sfetch_logger_t {
-    void (*log_cb)(const char* message, void* user_data);
-    void* user_data;
-} sfetch_logger_t;
-
 /* configuration values for sfetch_setup() */
 typedef struct sfetch_desc_t {
     uint32_t max_requests;          // max number of active requests across all channels (default: 128)
     uint32_t num_channels;          // number of channels to fetch requests in parallel (default: 1)
     uint32_t num_lanes;             // max number of requests active on the same channel (default: 1)
     sfetch_allocator_t allocator;   // optional memory allocation overrides (default: malloc/free)
-    sfetch_logger_t logger;         // optional log function overrides (default: SOKOL_LOG(message))
+    sfetch_logger_t logger;         // optional log function overrides (default: NO LOGGING!)
 } sfetch_desc_t;
 
 /* a request handle to identify an active fetch request, returned by sfetch_send() */
@@ -1065,7 +1125,13 @@ inline sfetch_handle_t sfetch_send(const sfetch_request_t& request) { return sfe
 #endif
 #endif // SOKOL_FETCH_INCLUDED
 
-/*--- IMPLEMENTATION ---------------------------------------------------------*/
+// ██ ███    ███ ██████  ██      ███████ ███    ███ ███████ ███    ██ ████████  █████  ████████ ██  ██████  ███    ██
+// ██ ████  ████ ██   ██ ██      ██      ████  ████ ██      ████   ██    ██    ██   ██    ██    ██ ██    ██ ████   ██
+// ██ ██ ████ ██ ██████  ██      █████   ██ ████ ██ █████   ██ ██  ██    ██    ███████    ██    ██ ██    ██ ██ ██  ██
+// ██ ██  ██  ██ ██      ██      ██      ██  ██  ██ ██      ██  ██ ██    ██    ██   ██    ██    ██ ██    ██ ██  ██ ██
+// ██ ██      ██ ██      ███████ ███████ ██      ██ ███████ ██   ████    ██    ██   ██    ██    ██  ██████  ██   ████
+//
+// >>implementation
 #ifdef SOKOL_FETCH_IMPL
 #define SOKOL_FETCH_IMPL_INCLUDED (1)
 
@@ -1099,21 +1165,6 @@ inline sfetch_handle_t sfetch_send(const sfetch_request_t& request) { return sfe
     #define SOKOL_ASSERT(c) assert(c)
 #endif
 
-#if !defined(SOKOL_DEBUG)
-    #define SFETCH_LOG(s)
-#else
-    #define SFETCH_LOG(s) _sfetch_log(s)
-    #ifndef SOKOL_LOG
-        #if defined(__ANDROID__)
-            #include <android/log.h>
-            #define SOKOL_LOG(s) __android_log_write(ANDROID_LOG_INFO, "SOKOL_FETCH", s)
-        #else
-            #include <stdio.h>
-            #define SOKOL_LOG(s) puts(s)
-        #endif
-    #endif
-#endif
-
 #ifndef _SOKOL_PRIVATE
     #if defined(__GNUC__) || defined(__clang__)
         #define _SOKOL_PRIVATE __attribute__((unused)) static
@@ -1153,7 +1204,13 @@ inline sfetch_handle_t sfetch_send(const sfetch_request_t& request) { return sfe
     #define _SFETCH_HAS_THREADS (1)
 #endif
 
-/*=== private type definitions ===============================================*/
+// ███████ ████████ ██████  ██    ██  ██████ ████████ ███████
+// ██         ██    ██   ██ ██    ██ ██         ██    ██
+// ███████    ██    ██████  ██    ██ ██         ██    ███████
+//      ██    ██    ██   ██ ██    ██ ██         ██         ██
+// ███████    ██    ██   ██  ██████   ██████    ██    ███████
+//
+// >>structs
 typedef struct _sfetch_path_t {
     char buf[SFETCH_MAX_PATH];
 } _sfetch_path_t;
@@ -1311,10 +1368,54 @@ static __thread _sfetch_t* _sfetch;
 #else
 static _sfetch_t* _sfetch;
 #endif
-
-/*=== general helper functions and macros =====================================*/
 #define _sfetch_def(val, def) (((val) == 0) ? (def) : (val))
 
+// ██       ██████   ██████   ██████  ██ ███    ██  ██████
+// ██      ██    ██ ██       ██       ██ ████   ██ ██
+// ██      ██    ██ ██   ███ ██   ███ ██ ██ ██  ██ ██   ███
+// ██      ██    ██ ██    ██ ██    ██ ██ ██  ██ ██ ██    ██
+// ███████  ██████   ██████   ██████  ██ ██   ████  ██████
+//
+// >>logging
+#if defined(SOKOL_DEBUG)
+#define _SFETCH_LOGITEM_XMACRO(item,msg) #item ": " msg,
+static const char* _sfetch_log_messages[] = {
+    _SFETCH_LOG_ITEMS
+};
+#undef _SFETCH_LOGITEM_XMACRO
+#endif // SOKOL_DEBUG
+
+#define _SFETCH_PANIC(code) _sfetch_log(SFETCH_LOGITEM_ ##code, 0, __LINE__)
+#define _SFETCH_ERROR(code) _sfetch_log(SFETCH_LOGITEM_ ##code, 1, __LINE__)
+#define _SFETCH_WARN(code) _sfetch_log(SFETCH_LOGITEM_ ##code, 2, __LINE__)
+#define _SFETCH_INFO(code) _sfetch_log(SFETCH_LOGITEM_ ##code, 3, __LINE__)
+
+static void _sfetch_log(sfetch_log_item_t log_item, uint32_t log_level, uint32_t line_nr) {
+    if (_sfetch->desc.logger.func) {
+        #if defined(SOKOL_DEBUG)
+            const char* filename = __FILE__;
+            const char* message = _sfetch_log_messages[log_item];
+        #else
+            const char* filename = 0;
+            const char* message = 0;
+        #endif
+        _sfetch->desc.logger.func("sfetch", log_level, log_item, message, line_nr, filename, _sfetch->desc.logger.user_data);
+    }
+    else {
+        // for log level PANIC it would be 'undefined behaviour' to continue
+        if (log_level == 0) {
+            abort();
+        }
+    }
+}
+
+// ███    ███ ███████ ███    ███  ██████  ██████  ██    ██
+// ████  ████ ██      ████  ████ ██    ██ ██   ██  ██  ██
+// ██ ████ ██ █████   ██ ████ ██ ██    ██ ██████    ████
+// ██  ██  ██ ██      ██  ██  ██ ██    ██ ██   ██    ██
+// ██      ██ ███████ ██      ██  ██████  ██   ██    ██
+//
+// >>memory
 _SOKOL_PRIVATE void _sfetch_clear(void* ptr, size_t size) {
     SOKOL_ASSERT(ptr && (size > 0));
     memset(ptr, 0, size);
@@ -1329,7 +1430,9 @@ _SOKOL_PRIVATE void* _sfetch_malloc_with_allocator(const sfetch_allocator_t* all
     else {
         ptr = malloc(size);
     }
-    SOKOL_ASSERT(ptr);
+    if (0 == ptr) {
+        _SFETCH_PANIC(MALLOC_FAILED);
+    }
     return ptr;
 }
 
@@ -1352,17 +1455,6 @@ _SOKOL_PRIVATE void _sfetch_free(void* ptr) {
     }
 }
 
-#if defined(SOKOL_DEBUG)
-_SOKOL_PRIVATE void _sfetch_log(const char* msg) {
-    if (_sfetch->desc.logger.log_cb) {
-        _sfetch->desc.logger.log_cb(msg, _sfetch->desc.logger.user_data);
-    }
-    else {
-        SOKOL_LOG(msg);
-    }
-}
-#endif
-
 _SOKOL_PRIVATE _sfetch_t* _sfetch_ctx(void) {
     return _sfetch;
 }
@@ -1388,21 +1480,13 @@ _SOKOL_PRIVATE _sfetch_path_t _sfetch_path_make(const char* str) {
     return res;
 }
 
-_SOKOL_PRIVATE uint32_t _sfetch_make_id(uint32_t index, uint32_t gen_ctr) {
-    return (gen_ctr<<16) | (index & 0xFFFF);
-}
-
-_SOKOL_PRIVATE sfetch_handle_t _sfetch_make_handle(uint32_t slot_id) {
-    sfetch_handle_t h;
-    h.id = slot_id;
-    return h;
-}
-
-_SOKOL_PRIVATE uint32_t _sfetch_slot_index(uint32_t slot_id) {
-    return slot_id & 0xFFFF;
-}
-
-/*=== a circular message queue ===============================================*/
+// ███    ███ ███████ ███████ ███████  █████   ██████  ███████      ██████  ██    ██ ███████ ██    ██ ███████
+// ████  ████ ██      ██      ██      ██   ██ ██       ██          ██    ██ ██    ██ ██      ██    ██ ██
+// ██ ████ ██ █████   ███████ ███████ ███████ ██   ███ █████       ██    ██ ██    ██ █████   ██    ██ █████
+// ██  ██  ██ ██           ██      ██ ██   ██ ██    ██ ██          ██ ▄▄ ██ ██    ██ ██      ██    ██ ██
+// ██      ██ ███████ ███████ ███████ ██   ██  ██████  ███████      ██████   ██████  ███████  ██████  ███████
+//                                                                     ▀▀
+// >>message queue
 _SOKOL_PRIVATE uint32_t _sfetch_ring_wrap(const _sfetch_ring_t* rb, uint32_t i) {
     return i % rb->num;
 }
@@ -1484,7 +1568,27 @@ _SOKOL_PRIVATE uint32_t _sfetch_ring_peek(const _sfetch_ring_t* rb, uint32_t ind
     return rb->buf[rb_index];
 }
 
-/*=== request pool implementation ============================================*/
+// ██████  ███████  ██████  ██    ██ ███████ ███████ ████████     ██████   ██████   ██████  ██
+// ██   ██ ██      ██    ██ ██    ██ ██      ██         ██        ██   ██ ██    ██ ██    ██ ██
+// ██████  █████   ██    ██ ██    ██ █████   ███████    ██        ██████  ██    ██ ██    ██ ██
+// ██   ██ ██      ██ ▄▄ ██ ██    ██ ██           ██    ██        ██      ██    ██ ██    ██ ██
+// ██   ██ ███████  ██████   ██████  ███████ ███████    ██        ██       ██████   ██████  ███████
+//                     ▀▀
+// >>request pool
+_SOKOL_PRIVATE uint32_t _sfetch_make_id(uint32_t index, uint32_t gen_ctr) {
+    return (gen_ctr<<16) | (index & 0xFFFF);
+}
+
+_SOKOL_PRIVATE sfetch_handle_t _sfetch_make_handle(uint32_t slot_id) {
+    sfetch_handle_t h;
+    h.id = slot_id;
+    return h;
+}
+
+_SOKOL_PRIVATE uint32_t _sfetch_slot_index(uint32_t slot_id) {
+    return slot_id & 0xFFFF;
+}
+
 _SOKOL_PRIVATE void _sfetch_item_init(_sfetch_item_t* item, uint32_t slot_id, const sfetch_request_t* request) {
     SOKOL_ASSERT(item && (0 == item->handle.id));
     SOKOL_ASSERT(request && request->path);
@@ -1614,7 +1718,13 @@ _SOKOL_PRIVATE _sfetch_item_t* _sfetch_pool_item_lookup(_sfetch_pool_t* pool, ui
     return 0;
 }
 
-/*=== PLATFORM WRAPPER FUNCTIONS =============================================*/
+// ██████   ██████  ███████ ██ ██   ██
+// ██   ██ ██    ██ ██      ██  ██ ██
+// ██████  ██    ██ ███████ ██   ███
+// ██      ██    ██      ██ ██  ██ ██
+// ██       ██████  ███████ ██ ██   ██
+//
+// >>posix
 #if _SFETCH_PLATFORM_POSIX
 _SOKOL_PRIVATE _sfetch_file_handle_t _sfetch_file_open(const _sfetch_path_t* path) {
     return fopen(path->buf, "rb");
@@ -1771,6 +1881,13 @@ _SOKOL_PRIVATE void _sfetch_thread_dequeue_outgoing(_sfetch_thread_t* thread, _s
 }
 #endif /* _SFETCH_PLATFORM_POSIX */
 
+// ██     ██ ██ ███    ██ ██████   ██████  ██     ██ ███████
+// ██     ██ ██ ████   ██ ██   ██ ██    ██ ██     ██ ██
+// ██  █  ██ ██ ██ ██  ██ ██   ██ ██    ██ ██  █  ██ ███████
+// ██ ███ ██ ██ ██  ██ ██ ██   ██ ██    ██ ██ ███ ██      ██
+//  ███ ███  ██ ██   ████ ██████   ██████   ███ ███  ███████
+//
+// >>windows
 #if _SFETCH_PLATFORM_WINDOWS
 _SOKOL_PRIVATE bool _sfetch_win32_utf8_to_wide(const char* src, wchar_t* dst, int dst_num_bytes) {
     SOKOL_ASSERT(src && dst && (dst_num_bytes > 1));
@@ -1790,7 +1907,7 @@ _SOKOL_PRIVATE bool _sfetch_win32_utf8_to_wide(const char* src, wchar_t* dst, in
 _SOKOL_PRIVATE _sfetch_file_handle_t _sfetch_file_open(const _sfetch_path_t* path) {
     wchar_t w_path[SFETCH_MAX_PATH];
     if (!_sfetch_win32_utf8_to_wide(path->buf, w_path, sizeof(w_path))) {
-        SFETCH_LOG("_sfetch_file_open: error converting UTF-8 path to wide string");
+        _SFETCH_ERROR(FILE_PATH_UTF8_DECODING_FAILED);
         return 0;
     }
     _sfetch_file_handle_t h = CreateFileW(
@@ -1950,7 +2067,13 @@ _SOKOL_PRIVATE void _sfetch_thread_dequeue_outgoing(_sfetch_thread_t* thread, _s
 }
 #endif /* _SFETCH_PLATFORM_WINDOWS */
 
-/*=== IO CHANNEL implementation ==============================================*/
+//  ██████ ██   ██  █████  ███    ██ ███    ██ ███████ ██      ███████
+// ██      ██   ██ ██   ██ ████   ██ ████   ██ ██      ██      ██
+// ██      ███████ ███████ ██ ██  ██ ██ ██  ██ █████   ██      ███████
+// ██      ██   ██ ██   ██ ██  ██ ██ ██  ██ ██ ██      ██           ██
+//  ██████ ██   ██ ██   ██ ██   ████ ██   ████ ███████ ███████ ███████
+//
+// >>channels
 
 /* per-channel request handler for native platforms accessing the local filesystem */
 #if _SFETCH_HAS_THREADS
@@ -2074,7 +2197,6 @@ _SOKOL_PRIVATE void* _sfetch_channel_thread_func(void* arg) {
 #endif /* _SFETCH_HAS_THREADS */
 
 #if _SFETCH_PLATFORM_EMSCRIPTEN
-/*=== embedded Javascript helper functions ===================================*/
 EM_JS(void, sfetch_js_send_head_request, (uint32_t slot_id, const char* path_cstr), {
     const path_str = UTF8ToString(path_cstr);
     const req = new XMLHttpRequest();
@@ -2308,7 +2430,7 @@ _SOKOL_PRIVATE bool _sfetch_channel_send(_sfetch_channel_t* chn, uint32_t slot_i
         return true;
     }
     else {
-        SFETCH_LOG("sfetch_send: user_sent queue is full)");
+        _SFETCH_ERROR(SEND_QUEUE_FULL);
         return false;
     }
 }
@@ -2449,45 +2571,39 @@ _SOKOL_PRIVATE void _sfetch_channel_dowork(_sfetch_channel_t* chn, _sfetch_pool_
     }
 }
 
-/*=== private high-level functions ===========================================*/
 _SOKOL_PRIVATE bool _sfetch_validate_request(_sfetch_t* ctx, const sfetch_request_t* req) {
-    #if defined(SOKOL_DEBUG)
-        if (req->channel >= ctx->desc.num_channels) {
-            SFETCH_LOG("_sfetch_validate_request: request.channel too big!");
-            return false;
-        }
-        if (!req->path) {
-            SFETCH_LOG("_sfetch_validate_request: request.path is null!");
-            return false;
-        }
-        if (strlen(req->path) >= (SFETCH_MAX_PATH-1)) {
-            SFETCH_LOG("_sfetch_validate_request: request.path is too long (must be < SFETCH_MAX_PATH-1)");
-            return false;
-        }
-        if (!req->callback) {
-            SFETCH_LOG("_sfetch_validate_request: request.callback missing");
-            return false;
-        }
-        if (req->chunk_size > req->buffer.size) {
-            SFETCH_LOG("_sfetch_validate_request: request.chunk_size is greater request.buffer.size)");
-            return false;
-        }
-        if (req->user_data.ptr && (req->user_data.size == 0)) {
-            SFETCH_LOG("_sfetch_validate_request: request.user_data.ptr is set, but request.user_data.size is null");
-            return false;
-        }
-        if (!req->user_data.ptr && (req->user_data.size > 0)) {
-            SFETCH_LOG("_sfetch_validate_request: request.user_data.ptr is null, but request.user_data.size is not");
-            return false;
-        }
-        if (req->user_data.size > SFETCH_MAX_USERDATA_UINT64 * sizeof(uint64_t)) {
-            SFETCH_LOG("_sfetch_validate_request: request.user_data.size is too big (see SFETCH_MAX_USERDATA_UINT64");
-            return false;
-        }
-    #else
-        /* silence unused warnings in release*/
-        (void)(ctx && req);
-    #endif
+    if (req->channel >= ctx->desc.num_channels) {
+        _SFETCH_ERROR(REQUEST_CHANNEL_INDEX_TOO_BIG);
+        return false;
+    }
+    if (!req->path) {
+        _SFETCH_ERROR(REQUEST_PATH_IS_NULL);
+        return false;
+    }
+    if (strlen(req->path) >= (SFETCH_MAX_PATH-1)) {
+        _SFETCH_ERROR(REQUEST_PATH_TOO_LONG);
+        return false;
+    }
+    if (!req->callback) {
+        _SFETCH_ERROR(REQUEST_CALLBACK_MISSING);
+        return false;
+    }
+    if (req->chunk_size > req->buffer.size) {
+        _SFETCH_ERROR(REQUEST_CHUNK_SIZE_GREATER_BUFFER_SIZE);
+        return false;
+    }
+    if (req->user_data.ptr && (req->user_data.size == 0)) {
+        _SFETCH_ERROR(REQUEST_USERDATA_PTR_IS_SET_BUT_USERDATA_SIZE_IS_NULL);
+        return false;
+    }
+    if (!req->user_data.ptr && (req->user_data.size > 0)) {
+        _SFETCH_ERROR(REQUEST_USERDATA_PTR_IS_NULL_BUT_USERDATA_SIZE_IS_NOT);
+        return false;
+    }
+    if (req->user_data.size > SFETCH_MAX_USERDATA_UINT64 * sizeof(uint64_t)) {
+        _SFETCH_ERROR(REQUEST_USERDATA_SIZE_TOO_BIG);
+        return false;
+    }
     return true;
 }
 
@@ -2500,7 +2616,13 @@ _SOKOL_PRIVATE sfetch_desc_t _sfetch_desc_defaults(const sfetch_desc_t* desc) {
     return res;
 }
 
-/*=== PUBLIC API FUNCTIONS ===================================================*/
+// ██████  ██    ██ ██████  ██      ██  ██████
+// ██   ██ ██    ██ ██   ██ ██      ██ ██
+// ██████  ██    ██ ██████  ██      ██ ██
+// ██      ██    ██ ██   ██ ██      ██ ██
+// ██       ██████  ██████  ███████ ██  ██████
+//
+// >>public
 SOKOL_API_IMPL void sfetch_setup(const sfetch_desc_t* desc_) {
     SOKOL_ASSERT(desc_);
     SOKOL_ASSERT(0 == _sfetch);
@@ -2517,7 +2639,7 @@ SOKOL_API_IMPL void sfetch_setup(const sfetch_desc_t* desc_) {
     /* replace zero-init items with default values */
     if (ctx->desc.num_channels > SFETCH_MAX_CHANNELS) {
         ctx->desc.num_channels = SFETCH_MAX_CHANNELS;
-        SFETCH_LOG("sfetch_setup: clamping num_channels to SFETCH_MAX_CHANNELS");
+        _SFETCH_WARN(CLAMPING_NUM_CHANNELS_TO_MAX_CHANNELS);
     }
 
     /* setup the global request item pool */
@@ -2589,7 +2711,7 @@ SOKOL_API_IMPL sfetch_handle_t sfetch_send(const sfetch_request_t* request) {
 
     uint32_t slot_id = _sfetch_pool_item_alloc(&ctx->pool, request);
     if (0 == slot_id) {
-        SFETCH_LOG("sfetch_send: request pool exhausted (too many active requests)");
+        _SFETCH_WARN(REQUEST_POOL_EXHAUSTED);
         return invalid_handle;
     }
     if (!_sfetch_channel_send(&ctx->chn[request->channel], slot_id)) {

File diff suppressed because it is too large
+ 473 - 308
sokol_gfx.h


+ 343 - 0
sokol_log.h

@@ -0,0 +1,343 @@
+#if defined(SOKOL_IMPL) && !defined(SOKOL_LOG_IMPL)
+#define SOKOL_LOG_IMPL
+#endif
+#ifndef SOKOL_LOG_INCLUDED
+/*
+    sokol_log.h -- common logging callback for sokol headers
+
+    Project URL: https://github.com/floooh/sokol
+
+    Example code: https://github.com/floooh/sokol-samples
+
+    Do this:
+        #define SOKOL_IMPL or
+        #define SOKOL_LOG_IMPL
+    before you include this file in *one* C or C++ file to create the
+    implementation.
+
+    Optionally provide the following defines when building the implementation:
+
+    SOKOL_ASSERT(c)             - your own assert macro (default: assert(c))
+    SOKOL_UNREACHABLE()         - a guard macro for unreachable code (default: assert(false))
+    SOKOL_LOG_API_DECL          - public function declaration prefix (default: extern)
+    SOKOL_API_DECL              - same as SOKOL_GFX_API_DECL
+    SOKOL_API_IMPL              - public function implementation prefix (default: -)
+
+    Optionally define the following for verbose output:
+
+    SOKOL_DEBUG         - by default this is defined if _DEBUG is defined
+
+
+    OVERVIEW
+    ========
+    sokol_log.h provides a default logging callback for other sokol headers.
+
+    To use the default log callback, just include sokol_log.h and provide
+    a function pointer to the 'slog_func' function when setting up the
+    sokol library:
+
+    For instance with sokol_audio.h:
+
+        #include "sokol_log.h"
+        ...
+        saudio_setup(&(saudio_desc){ .logger.func = slog_func });
+
+    Logging output goes to stderr and/or a platform specific logging subsystem
+    (which means that in some scenarios you might see logging messages duplicated):
+
+        - Windows: stderr + OutputDebugStringA()
+        - macOS/iOS/Linux: stderr + syslog()
+        - Emscripten: console.info()/warn()/error()
+        - Android: __android_log_write()
+
+    On Windows with sokol_app.h also note the runtime config items to make
+    stdout/stderr output visible on the console for WinMain() applications
+    via sapp_desc.win32_console_attach or sapp_desc.win32_console_create,
+    however when running in a debugger on Windows, the logging output should
+    show up on the debug output UI panel.
+
+    In debug mode, a log message might look like this:
+
+        [sspine][error][id:12] /Users/floh/projects/sokol/util/sokol_spine.h:3472:0:
+            SKELETON_DESC_NO_ATLAS: no atlas object provided in sspine_skeleton_desc.atlas
+
+    The source path and line number is formatted like compiler errors, in some IDEs (like VSCode)
+    such error messages are clickable.
+
+    In release mode, logging is less verbose as to not bloat the executable with string data, but you still get
+    enough information to identify the type and location of an error:
+
+        [sspine][error][id:12][line:3472]
+
+    RULES FOR WRITING YOUR OWN LOGGING FUNCTION
+    ===========================================
+    - must be re-entrant because it might be called from different threads
+    - must treat **all** provided string pointers as optional (can be null)
+    - don't store the string pointers, copy the string data instead
+    - must not return for log level panic
+
+    LICENSE
+    =======
+    zlib/libpng license
+
+    Copyright (c) 2023 Andre Weissflog
+
+    This software is provided 'as-is', without any express or implied warranty.
+    In no event will the authors be held liable for any damages arising from the
+    use of this software.
+
+    Permission is granted to anyone to use this software for any purpose,
+    including commercial applications, and to alter it and redistribute it
+    freely, subject to the following restrictions:
+
+        1. The origin of this software must not be misrepresented; you must not
+        claim that you wrote the original software. If you use this software in a
+        product, an acknowledgment in the product documentation would be
+        appreciated but is not required.
+
+        2. Altered source versions must be plainly marked as such, and must not
+        be misrepresented as being the original software.
+
+        3. This notice may not be removed or altered from any source
+        distribution.
+*/
+#define SOKOL_LOG_INCLUDED (1)
+#include <stdint.h>
+
+#if defined(SOKOL_API_DECL) && !defined(SOKOL_LOG_API_DECL)
+#define SOKOL_LOG_API_DECL SOKOL_API_DECL
+#endif
+#ifndef SOKOL_LOG_API_DECL
+#if defined(_WIN32) && defined(SOKOL_DLL) && defined(SOKOL_LOG_IMPL)
+#define SOKOL_LOG_API_DECL __declspec(dllexport)
+#elif defined(_WIN32) && defined(SOKOL_DLL)
+#define SOKOL_LOG_API_DECL __declspec(dllimport)
+#else
+#define SOKOL_LOG_API_DECL extern
+#endif
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*
+    Plug this function into the 'logger.func' struct item when initializating any of the sokol
+    headers. For instance for sokol_audio.h it would loom like this:
+
+    saudio_setup(&(saudio_desc){
+        .logger = {
+            .func = slog_func
+        }
+    });
+*/
+SOKOL_LOG_API_DECL void slog_func(const char* tag, uint32_t log_level, uint32_t log_item, const char* message, uint32_t line_nr, const char* filename, void* user_data);
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
+#endif // SOKOL_LOG_INCLUDED
+
+// ██ ███    ███ ██████  ██      ███████ ███    ███ ███████ ███    ██ ████████  █████  ████████ ██  ██████  ███    ██
+// ██ ████  ████ ██   ██ ██      ██      ████  ████ ██      ████   ██    ██    ██   ██    ██    ██ ██    ██ ████   ██
+// ██ ██ ████ ██ ██████  ██      █████   ██ ████ ██ █████   ██ ██  ██    ██    ███████    ██    ██ ██    ██ ██ ██  ██
+// ██ ██  ██  ██ ██      ██      ██      ██  ██  ██ ██      ██  ██ ██    ██    ██   ██    ██    ██ ██    ██ ██  ██ ██
+// ██ ██      ██ ██      ███████ ███████ ██      ██ ███████ ██   ████    ██    ██   ██    ██    ██  ██████  ██   ████
+//
+// >>implementation
+#ifdef SOKOL_LOG_IMPL
+#define SOKOL_LOG_IMPL_INCLUDED (1)
+
+#ifndef SOKOL_API_IMPL
+    #define SOKOL_API_IMPL
+#endif
+#ifndef SOKOL_DEBUG
+    #ifndef NDEBUG
+        #define SOKOL_DEBUG
+    #endif
+#endif
+#ifndef SOKOL_ASSERT
+    #include <assert.h>
+    #define SOKOL_ASSERT(c) assert(c)
+#endif
+
+#ifndef _SOKOL_PRIVATE
+    #if defined(__GNUC__) || defined(__clang__)
+        #define _SOKOL_PRIVATE __attribute__((unused)) static
+    #else
+        #define _SOKOL_PRIVATE static
+    #endif
+#endif
+
+#ifndef _SOKOL_UNUSED
+    #define _SOKOL_UNUSED(x) (void)(x)
+#endif
+
+// platform detection
+#if defined(__APPLE__)
+    #define _SLOG_APPLE (1)
+#elif defined(__EMSCRIPTEN__)
+    #define _SLOG_EMSCRIPTEN (1)
+#elif defined(_WIN32)
+    #define _SLOG_WINDOWS (1)
+#elif defined(__ANDROID__)
+    #define _SLOG_ANDROID (1)
+#elif defined(__linux__) || defined(__unix__)
+    #define _SLOG_LINUX (1)
+#else
+#error "sokol_log.h: unknown platform"
+#endif
+
+#include <stdlib.h> // abort
+#include <stdio.h>  // fputs
+#include <stddef.h> // size_t
+
+#if defined(_SLOG_EMSCRIPTEN)
+#include <emscripten/em_js.h>
+#elif defined(_SLOG_WINDOWS)
+#ifndef WIN32_LEAN_AND_MEAN
+    #define WIN32_LEAN_AND_MEAN
+#endif
+#ifndef NOMINMAX
+    #define NOMINMAX
+#endif
+#include <windows.h>
+#elif defined(_SLOG_ANDROID)
+#include <android/log.h>
+#elif defined(_SLOG_LINUX) || defined(_SLOG_APPLE)
+#include <syslog.h>
+#endif
+
+// size of line buffer (on stack!) in bytes including terminating zero
+#define _SLOG_LINE_LENGTH (512)
+
+_SOKOL_PRIVATE char* _slog_append(const char* str, char* dst, char* end) {
+    if (str) {
+        char c;
+        while (((c = *str++) != 0) && (dst < (end - 1))) {
+            *dst++ = c;
+        }
+    }
+    *dst = 0;
+    return dst;
+}
+
+_SOKOL_PRIVATE char* _slog_itoa(uint32_t x, char* buf, size_t buf_size) {
+    const size_t max_digits_and_null = 11;
+    if (buf_size < max_digits_and_null) {
+        return 0;
+    }
+    char* p = buf + max_digits_and_null;
+    *--p = 0;
+    do {
+        *--p = '0' + (x % 10);
+        x /= 10;
+    } while (x != 0);
+    return p;
+}
+
+#if defined(_SLOG_EMSCRIPTEN)
+EM_JS(void, slog_js_log, (uint32_t level, const char* c_str), {
+    const str = UTF8ToString(c_str);
+    switch (level) {
+        case 0: console.error(str); break;
+        case 1: console.error(str); break;
+        case 2: console.warn(str); break;
+        default: console.info(str); break;
+    }
+});
+#endif
+
+SOKOL_API_IMPL void slog_func(const char* tag, uint32_t log_level, uint32_t log_item, const char* message, uint32_t line_nr, const char* filename, void* user_data) {
+    _SOKOL_UNUSED(user_data);
+
+    const char* log_level_str;
+    switch (log_level) {
+        case 0: log_level_str = "panic"; break;
+        case 1: log_level_str = "error"; break;
+        case 2: log_level_str = "warning"; break;
+        default: log_level_str = "info"; break;
+    }
+
+    // build log output line
+    char line_buf[_SLOG_LINE_LENGTH];
+    char* str = line_buf;
+    char* end = line_buf + sizeof(line_buf);
+    char num_buf[32];
+    if (tag) {
+        str = _slog_append("[", str, end);
+        str = _slog_append(tag, str, end);
+        str = _slog_append("]", str, end);
+    }
+    str = _slog_append("[", str, end);
+    str = _slog_append(log_level_str, str, end);
+    str = _slog_append("]", str, end);
+    str = _slog_append("[id:", str, end);
+    str = _slog_append(_slog_itoa(log_item, num_buf, sizeof(num_buf)), str, end);
+    str = _slog_append("]", str, end);
+    // if a filename is provided, build a clickable log message that's compatible with compiler error messages
+    if (filename) {
+        str = _slog_append(" ", str, end);
+        #if defined(_MSC_VER)
+            // MSVC compiler error format
+            str = _slog_append(filename, str, end);
+            str = _slog_append("(", str, end);
+            str = _slog_append(_slog_itoa(line_nr, num_buf, sizeof(num_buf)), str, end);
+            str = _slog_append("): ", str, end);
+        #else
+            // gcc/clang compiler error format
+            str = _slog_append(filename, str, end);
+            str = _slog_append(":", str, end);
+            str = _slog_append(_slog_itoa(line_nr, num_buf, sizeof(num_buf)), str, end);
+            str = _slog_append(":0: ", str, end);
+        #endif
+    }
+    else {
+        str = _slog_append("[line:", str, end);
+        str = _slog_append(_slog_itoa(line_nr, num_buf, sizeof(num_buf)), str, end);
+        str = _slog_append("] ", str, end);
+    }
+    if (message) {
+        str = _slog_append("\n\t", str, end);
+        str = _slog_append(message, str, end);
+    }
+    str = _slog_append("\n\n", str, end);
+    if (0 == log_level) {
+        str = _slog_append("ABORTING because of [panic]\n", str, end);
+        (void)str;
+    }
+
+    // print to stderr?
+    #if defined(_SLOG_LINUX) || defined(_SLOG_WINDOWS) || defined(_SLOG_APPLE)
+        fputs(line_buf, stderr);
+    #endif
+
+    // platform specific logging calls
+    #if defined(_SLOG_WINDOWS)
+        OutputDebugStringA(line_buf);
+    #elif defined(_SLOG_ANDROID)
+        int prio;
+        switch (log_level) {
+            case 0: prio = ANDROID_LOG_FATAL; break;
+            case 1: prio = ANDROID_LOG_ERROR; break;
+            case 2: prio = ANDROID_LOG_WARN; break;
+            default: prio = ANDROID_LOG_INFO; break;
+        }
+        __android_log_write(prio, "SOKOL", line_buf);
+    #elif defined(_SLOG_EMSCRIPTEN)
+        slog_js_log(log_level, line_buf);
+    #elif defined(_SLOG_LINUX) || defined(_SLOG_APPLE)
+        int prio;
+        switch (log_level) {
+            case 0: prio = LOG_CRIT; break;
+            case 1: prio = LOG_ERR; break;
+            case 2: prio = LOG_WARNING; break;
+            default: prio = LOG_INFO; break;
+        }
+        syslog(prio, "%s", line_buf);
+    #endif
+    if (0 == log_level) {
+        abort();
+    }
+}
+#endif // SOKOL_LOG_IMPL

+ 1 - 5
tests/CMakeLists.txt

@@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.20)
 project(sokol-test)
 set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
 set(CMAKE_C_STANDARD 11)
-set(CMAKE_CXX_STANDARD 17)  # needed for UWP
+set(CMAKE_CXX_STANDARD 11)
 
 # SOKOL_GLCORE33, SOKOL_GLES2, SOKOL_GLES3, SOKOL_D3D11, SOKOL_METAL, SOKOL_WGPU, SOKOL_DUMMY
 set(SOKOL_BACKEND "SOKOL_DUMMY_BACKEND" CACHE STRING "Select 3D backend API")
@@ -22,8 +22,6 @@ elseif (CMAKE_SYSTEM_NAME STREQUAL Linux)
     set(LINUX 1)
 elseif (CMAKE_SYSTEM_NAME STREQUAL Darwin)
     set(OSX_MACOS 1)
-elseif (CMAKE_SYSTEM_NAME STREQUAL WindowsStore)
-    set(UWP 1)
 elseif (CMAKE_SYSTEM_NAME STREQUAL Windows)
     set(WINDOWS 1)
 else()
@@ -110,8 +108,6 @@ elseif (OSX_MACOS)
     else()
         set(system_libs ${system_libs} "-framework OpenGL")
     endif()
-elseif (UWP)
-    set(exe_type WIN32)
 elseif (WINDOWS)
     set(exe_type WIN32)
 endif()

+ 0 - 19
tests/CMakePresets.json

@@ -509,15 +509,6 @@
                 "CMAKE_C_COMPILER": "clang",
                 "CMAKE_CXX_COMPILER": "clang++"
             }
-        },
-        {
-            "name": "win_uwp",
-            "binaryDir": "build/win_uwp",
-            "cacheVariables": {
-                "SOKOL_BACKEND": "SOKOL_D3D11",
-                "CMAKE_SYSTEM_NAME": "WindowsStore",
-                "CMAKE_SYSTEM_VERSION": "10.0.19041.0 "
-            }
         }
     ],
     "buildPresets": [
@@ -724,16 +715,6 @@
         {
             "name": "win_d3d11_analyze",
             "configurePreset": "win_d3d11_analyze"
-        },
-        {
-            "name": "win_uwp_debug",
-            "configurePreset": "win_uwp",
-            "configuration": "Debug"
-        },
-        {
-            "name": "win_uwp_release",
-            "configurePreset": "win_uwp",
-            "configuration": "Release"
         }
     ]
 }

+ 10 - 10
tests/compile/CMakeLists.txt

@@ -14,8 +14,9 @@ set(c_sources
     sokol_nuklear.c
     sokol_color.c
     sokol_spine.c
+    sokol_log.c
     sokol_main.c)
-if (NOT ANDROID AND NOT UWP)
+if (NOT ANDROID)
     set(c_sources ${c_sources} sokol_fetch.c)
 endif()
 
@@ -34,20 +35,19 @@ set(cxx_sources
     sokol_shape.cc
     sokol_color.cc
     sokol_spine.cc
+    sokol_log.cc
     sokol_main.cc)
-if (NOT ANDROID AND NOT UWP)
+if (NOT ANDROID)
     set(cxx_sources ${cxx_sources} sokol_fetch.cc)
 endif()
 
-if (NOT UWP)
-    if (ANDROID)
-        add_library(sokol-compiletest-c SHARED ${c_sources})
-    else()
-        add_executable(sokol-compiletest-c ${exe_type} sokol_app.c sokol_glue.c ${c_sources})
-    endif()
-    target_link_libraries(sokol-compiletest-c PUBLIC cimgui nuklear spine)
-    configure_c(sokol-compiletest-c)
+if (ANDROID)
+    add_library(sokol-compiletest-c SHARED ${c_sources})
+else()
+    add_executable(sokol-compiletest-c ${exe_type} sokol_app.c sokol_glue.c ${c_sources})
 endif()
+target_link_libraries(sokol-compiletest-c PUBLIC cimgui nuklear spine)
+configure_c(sokol-compiletest-c)
 
 if (ANDROID)
     add_library(sokol-compiletest-cxx SHARED ${cxx_sources})

+ 6 - 0
tests/compile/sokol_log.c

@@ -0,0 +1,6 @@
+#define SOKOL_IMPL
+#include "sokol_log.h"
+
+void use_sokol_log(void) {
+    slog_func("bla", 1, 123, "123", 42, "bla.c", 0);
+}

+ 6 - 0
tests/compile/sokol_log.cc

@@ -0,0 +1,6 @@
+#define SOKOL_IMPL
+#include "sokol_log.h"
+
+void use_sokol_log(void) {
+    slog_func("bla", 1, 123, "123", 42, "bla.c", 0);
+}

+ 1 - 1
tests/functional/CMakeLists.txt

@@ -1,4 +1,4 @@
-if (NOT ANDROID AND NOT UWP)
+if (NOT ANDROID)
 
 file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/assets/comsi.s3m DESTINATION ${CMAKE_BINARY_DIR})
 file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/assets/comsi.s3m DESTINATION ${CMAKE_BINARY_DIR}/Debug)

+ 21 - 4
tests/functional/sokol_gfx_test.c

@@ -12,16 +12,23 @@
 #define T(b) EXPECT_TRUE(b)
 
 static int num_log_called = 0;
-static void test_logger(const char* msg, void* userdata) {
-    (void)userdata;
+static sg_log_item log_item;
+
+static void test_logger(const char* tag, uint32_t log_level, uint32_t log_item_id, const char* message_or_null, uint32_t line_nr, const char* filename_or_null, void* user_data) {
+    (void)tag;
+    (void)log_level;
+    (void)message_or_null;
+    (void)line_nr;
+    (void)filename_or_null;
+    (void)user_data;
     num_log_called++;
-    puts(msg);
+    log_item = log_item_id;
 }
 
 static void setup_with_logger(void) {
     num_log_called = 0;
     sg_setup(&(sg_desc){
-        .logger = { .log_cb = test_logger }
+        .logger = { .func = test_logger }
     });
 }
 
@@ -1139,6 +1146,7 @@ UTEST(sokol_gfx, make_dealloc_buffer_warns) {
     sg_buffer buf = create_buffer();
     T(sg_query_buffer_state(buf) == SG_RESOURCESTATE_VALID);
     sg_dealloc_buffer(buf);
+    T(log_item == SG_LOGITEM_DEALLOC_BUFFER_INVALID_STATE);
     T(num_log_called == 1);
     T(sg_query_buffer_state(buf) == SG_RESOURCESTATE_VALID);
     sg_destroy_buffer(buf);
@@ -1151,6 +1159,7 @@ UTEST(sokol_gfx, make_dealloc_image_warns) {
     sg_image img = create_image();
     T(sg_query_image_state(img) == SG_RESOURCESTATE_VALID);
     sg_dealloc_image(img);
+    T(log_item == SG_LOGITEM_DEALLOC_IMAGE_INVALID_STATE);
     T(num_log_called == 1);
     T(sg_query_image_state(img) == SG_RESOURCESTATE_VALID);
     sg_destroy_image(img);
@@ -1163,6 +1172,7 @@ UTEST(sokol_gfx, make_dealloc_shader_warns) {
     sg_shader shd = create_shader();
     T(sg_query_shader_state(shd) == SG_RESOURCESTATE_VALID);
     sg_dealloc_shader(shd);
+    T(log_item == SG_LOGITEM_DEALLOC_SHADER_INVALID_STATE);
     T(num_log_called == 1);
     T(sg_query_shader_state(shd) == SG_RESOURCESTATE_VALID);
     sg_destroy_shader(shd);
@@ -1175,6 +1185,7 @@ UTEST(sokol_gfx, make_dealloc_pipeline_warns) {
     sg_pipeline pip = create_pipeline();
     T(sg_query_pipeline_state(pip) == SG_RESOURCESTATE_VALID);
     sg_dealloc_pipeline(pip);
+    T(log_item == SG_LOGITEM_DEALLOC_PIPELINE_INVALID_STATE);
     T(num_log_called == 1);
     T(sg_query_pipeline_state(pip) == SG_RESOURCESTATE_VALID);
     sg_destroy_pipeline(pip);
@@ -1187,6 +1198,7 @@ UTEST(sokol_gfx, make_dealloc_pass_warns) {
     sg_pass pass = create_pass();
     T(sg_query_pass_state(pass) == SG_RESOURCESTATE_VALID);
     sg_dealloc_pass(pass);
+    T(log_item == SG_LOGITEM_DEALLOC_PASS_INVALID_STATE);
     T(num_log_called == 1);
     T(sg_query_pass_state(pass) == SG_RESOURCESTATE_VALID);
     sg_destroy_pass(pass);
@@ -1199,6 +1211,7 @@ UTEST(sokol_gfx, alloc_uninit_buffer_warns) {
     sg_buffer buf = sg_alloc_buffer();
     T(sg_query_buffer_state(buf) == SG_RESOURCESTATE_ALLOC);
     sg_uninit_buffer(buf);
+    T(log_item == SG_LOGITEM_UNINIT_BUFFER_INVALID_STATE);
     T(num_log_called == 1);
     T(sg_query_buffer_state(buf) == SG_RESOURCESTATE_ALLOC);
     sg_shutdown();
@@ -1209,6 +1222,7 @@ UTEST(sokol_gfx, alloc_uninit_image_warns) {
     sg_image img = sg_alloc_image();
     T(sg_query_image_state(img) == SG_RESOURCESTATE_ALLOC);
     sg_uninit_image(img);
+    T(log_item == SG_LOGITEM_UNINIT_IMAGE_INVALID_STATE);
     T(num_log_called == 1);
     T(sg_query_image_state(img) == SG_RESOURCESTATE_ALLOC);
     sg_shutdown();
@@ -1219,6 +1233,7 @@ UTEST(sokol_gfx, alloc_uninit_shader_warns) {
     sg_shader shd = sg_alloc_shader();
     T(sg_query_shader_state(shd) == SG_RESOURCESTATE_ALLOC);
     sg_uninit_shader(shd);
+    T(log_item == SG_LOGITEM_UNINIT_SHADER_INVALID_STATE);
     T(num_log_called == 1);
     T(sg_query_shader_state(shd) == SG_RESOURCESTATE_ALLOC);
     sg_shutdown();
@@ -1229,6 +1244,7 @@ UTEST(sokol_gfx, alloc_uninit_pipeline_warns) {
     sg_pipeline pip = sg_alloc_pipeline();
     T(sg_query_pipeline_state(pip) == SG_RESOURCESTATE_ALLOC);
     sg_uninit_pipeline(pip);
+    T(log_item == SG_LOGITEM_UNINIT_PIPELINE_INVALID_STATE);
     T(num_log_called == 1);
     T(sg_query_pipeline_state(pip) == SG_RESOURCESTATE_ALLOC);
     sg_shutdown();
@@ -1239,6 +1255,7 @@ UTEST(sokol_gfx, alloc_uninit_pass_warns) {
     sg_pass pass = sg_alloc_pass();
     T(sg_query_pass_state(pass) == SG_RESOURCESTATE_ALLOC);
     sg_uninit_pass(pass);
+    T(log_item == SG_LOGITEM_UNINIT_PASS_INVALID_STATE);
     T(num_log_called == 1);
     T(sg_query_pass_state(pass) == SG_RESOURCESTATE_ALLOC);
     sg_shutdown();

+ 23 - 23
tests/functional/sokol_spine_test.c

@@ -9,20 +9,20 @@
 
 #define T(b) EXPECT_TRUE(b)
 
-static sspine_error last_error = SSPINE_ERROR_OK;
-static void log_func(const char* tag, uint32_t log_level, uint32_t error_code, const char* error_id, int line_nr, const char* filename, void* user_data) {
-    (void)tag; (void)log_level; (void)error_id; (void)line_nr; (void)filename; (void)user_data;
-    last_error = error_code;
+static sspine_log_item last_logitem = SSPINE_LOGITEM_OK;
+static void log_func(const char* tag, uint32_t log_level, uint32_t log_item, const char* message, uint32_t line_nr, const char* filename, void* user_data) {
+    (void)tag; (void)log_level; (void)message; (void)line_nr; (void)filename; (void)user_data;
+    last_logitem = log_item;
 }
 
 static void init() {
-    last_error = SSPINE_ERROR_OK;
+    last_logitem = SSPINE_LOGITEM_OK;
     sg_setup(&(sg_desc){0});
     sspine_setup(&(sspine_desc){ .logger = { .func = log_func } });
 }
 
 static void init_with_desc(const sspine_desc* desc) {
-    last_error = SSPINE_ERROR_OK;
+    last_logitem = SSPINE_LOGITEM_OK;
     sspine_desc desc1 = *desc;
     desc1.logger.func = log_func;
     sg_setup(&(sg_desc){0});
@@ -109,12 +109,12 @@ UTEST(sokol_spine, atlas_pool_exhausted) {
     for (int i = 0; i < 4; i++) {
         sspine_atlas atlas = sspine_make_atlas(&(sspine_atlas_desc){0});
         T(sspine_get_atlas_resource_state(atlas) == SSPINE_RESOURCESTATE_FAILED);
-        T(last_error == SSPINE_ERROR_ATLAS_DESC_NO_DATA);
+        T(last_logitem == SSPINE_LOGITEM_ATLAS_DESC_NO_DATA);
     }
     sspine_atlas atlas = sspine_make_atlas(&(sspine_atlas_desc){0});
     T(SSPINE_INVALID_ID == atlas.id);
     T(sspine_get_atlas_resource_state(atlas) == SSPINE_RESOURCESTATE_INVALID);
-    T(last_error == SSPINE_ERROR_ATLAS_POOL_EXHAUSTED);
+    T(last_logitem == SSPINE_LOGITEM_ATLAS_POOL_EXHAUSTED);
     shutdown();
 }
 
@@ -133,7 +133,7 @@ UTEST(sokol_spine, make_atlas_fail_no_data) {
     init();
     sspine_atlas atlas = sspine_make_atlas(&(sspine_atlas_desc){0});
     T(atlas.id != SSPINE_INVALID_ID);
-    T(last_error == SSPINE_ERROR_ATLAS_DESC_NO_DATA);
+    T(last_logitem == SSPINE_LOGITEM_ATLAS_DESC_NO_DATA);
     T(sspine_get_atlas_resource_state(atlas) == SSPINE_RESOURCESTATE_FAILED);
     T(!sspine_atlas_valid(atlas));
     shutdown();
@@ -143,7 +143,7 @@ UTEST(sokol_spine, make_atlas_fail_no_data) {
 UTEST(sokol_spine, failed_atlas_no_images) {
     init();
     sspine_atlas atlas = sspine_make_atlas(&(sspine_atlas_desc){0});
-    T(last_error == SSPINE_ERROR_ATLAS_DESC_NO_DATA);
+    T(last_logitem == SSPINE_LOGITEM_ATLAS_DESC_NO_DATA);
     T(atlas.id != SSPINE_INVALID_ID);
     T(!sspine_atlas_valid(atlas));
     T(sspine_num_images(atlas) == 0);
@@ -221,12 +221,12 @@ UTEST(sokol_spine, skeleton_pool_exhausted) {
     for (int i = 0; i < 4; i++) {
         sspine_skeleton skeleton = sspine_make_skeleton(&(sspine_skeleton_desc){0});
         T(sspine_get_skeleton_resource_state(skeleton) == SSPINE_RESOURCESTATE_FAILED);
-        T(last_error == SSPINE_ERROR_SKELETON_DESC_NO_DATA);
+        T(last_logitem == SSPINE_LOGITEM_SKELETON_DESC_NO_DATA);
     }
     sspine_skeleton skeleton = sspine_make_skeleton(&(sspine_skeleton_desc){0});
     T(SSPINE_INVALID_ID == skeleton.id);
     T(sspine_get_skeleton_resource_state(skeleton) == SSPINE_RESOURCESTATE_INVALID);
-    T(last_error == SSPINE_ERROR_SKELETON_POOL_EXHAUSTED);
+    T(last_logitem == SSPINE_LOGITEM_SKELETON_POOL_EXHAUSTED);
     shutdown();
 }
 
@@ -260,7 +260,7 @@ UTEST(sokol_spine, make_skeleton_fail_no_data) {
     });
     T(sspine_get_skeleton_resource_state(skeleton) == SSPINE_RESOURCESTATE_FAILED);
     T(!sspine_skeleton_valid(skeleton));
-    T(last_error == SSPINE_ERROR_SKELETON_DESC_NO_DATA);
+    T(last_logitem == SSPINE_LOGITEM_SKELETON_DESC_NO_DATA);
     shutdown();
 }
 
@@ -273,19 +273,19 @@ UTEST(sokol_spine, make_skeleton_fail_no_atlas) {
     free_data(skeleton_json_data);
     T(sspine_get_skeleton_resource_state(skeleton) == SSPINE_RESOURCESTATE_FAILED);
     T(!sspine_skeleton_valid(skeleton));
-    T(last_error == SSPINE_ERROR_SKELETON_DESC_NO_ATLAS);
+    T(last_logitem == SSPINE_LOGITEM_SKELETON_DESC_NO_ATLAS);
     shutdown();
 }
 
 UTEST(sokol_spine, make_skeleton_fail_with_failed_atlas) {
     init();
     sspine_atlas atlas = sspine_make_atlas(&(sspine_atlas_desc){0});
-    T(last_error == SSPINE_ERROR_ATLAS_DESC_NO_DATA);
+    T(last_logitem == SSPINE_LOGITEM_ATLAS_DESC_NO_DATA);
     T(sspine_get_atlas_resource_state(atlas) == SSPINE_RESOURCESTATE_FAILED);
     sspine_skeleton skeleton = create_skeleton_json(atlas);
     T(sspine_get_skeleton_resource_state(skeleton) == SSPINE_RESOURCESTATE_FAILED);
     T(!sspine_skeleton_valid(skeleton));
-    T(last_error == SSPINE_ERROR_SKELETON_ATLAS_NOT_VALID);
+    T(last_logitem == SSPINE_LOGITEM_SKELETON_ATLAS_NOT_VALID);
     shutdown();
 }
 
@@ -298,7 +298,7 @@ UTEST(sokol_spine, make_skeleton_json_fail_corrupt_data) {
         .json_data = (const char*)invalid_json_data,
     });
     T(sspine_get_skeleton_resource_state(skeleton) == SSPINE_RESOURCESTATE_FAILED);
-    T(last_error == SSPINE_ERROR_SPINE_SKELETON_DATA_CREATION_FAILED);
+    T(last_logitem == SSPINE_LOGITEM_CREATE_SKELETON_DATA_FROM_JSON_FAILED);
     sspine_destroy_skeleton(skeleton);
     T(sspine_get_skeleton_resource_state(skeleton) == SSPINE_RESOURCESTATE_INVALID);
     shutdown();
@@ -328,12 +328,12 @@ UTEST(sokol_spine, instance_pool_exhausted) {
     for (int i = 0; i < 4; i++) {
         sspine_instance instance = sspine_make_instance(&(sspine_instance_desc){0});
         T(sspine_get_instance_resource_state(instance) == SSPINE_RESOURCESTATE_FAILED);
-        T(last_error == SSPINE_ERROR_INSTANCE_DESC_NO_SKELETON);
+        T(last_logitem == SSPINE_LOGITEM_INSTANCE_DESC_NO_SKELETON);
     }
     sspine_instance instance = sspine_make_instance(&(sspine_instance_desc){0});
     T(SSPINE_INVALID_ID == instance.id);
     T(sspine_get_instance_resource_state(instance) == SSPINE_RESOURCESTATE_INVALID);
-    T(last_error == SSPINE_ERROR_INSTANCE_POOL_EXHAUSTED);
+    T(last_logitem == SSPINE_LOGITEM_INSTANCE_POOL_EXHAUSTED);
     shutdown();
 }
 
@@ -354,7 +354,7 @@ UTEST(sokol_spine, make_instance_fail_no_skeleton) {
     init();
     sspine_instance instance = sspine_make_instance(&(sspine_instance_desc){0});
     T(sspine_get_instance_resource_state(instance) == SSPINE_RESOURCESTATE_FAILED);
-    T(last_error == SSPINE_ERROR_INSTANCE_DESC_NO_SKELETON);
+    T(last_logitem == SSPINE_LOGITEM_INSTANCE_DESC_NO_SKELETON);
     sspine_destroy_instance(instance);
     T(sspine_get_instance_resource_state(instance) == SSPINE_RESOURCESTATE_INVALID);
     shutdown();
@@ -363,13 +363,13 @@ UTEST(sokol_spine, make_instance_fail_no_skeleton) {
 UTEST(sokol_spine, make_instance_fail_with_failed_skeleton) {
     init();
     sspine_skeleton failed_skeleton = sspine_make_skeleton(&(sspine_skeleton_desc){0});
-    T(last_error == SSPINE_ERROR_SKELETON_DESC_NO_DATA);
+    T(last_logitem == SSPINE_LOGITEM_SKELETON_DESC_NO_DATA);
     T(sspine_get_skeleton_resource_state(failed_skeleton) == SSPINE_RESOURCESTATE_FAILED);
     sspine_instance instance = sspine_make_instance(&(sspine_instance_desc){
         .skeleton = failed_skeleton
     });
     T(sspine_get_instance_resource_state(instance) == SSPINE_RESOURCESTATE_FAILED);
-    T(last_error == SSPINE_ERROR_INSTANCE_SKELETON_NOT_VALID);
+    T(last_logitem == SSPINE_LOGITEM_INSTANCE_SKELETON_NOT_VALID);
     shutdown();
 }
 
@@ -385,7 +385,7 @@ UTEST(sokol_spine, make_instance_fail_with_destroyed_atlas) {
         .skeleton = skeleton
     });
     T(sspine_get_instance_resource_state(instance) == SSPINE_RESOURCESTATE_FAILED);
-    T(last_error == SSPINE_ERROR_INSTANCE_ATLAS_NOT_VALID);
+    T(last_logitem == SSPINE_LOGITEM_INSTANCE_ATLAS_NOT_VALID);
     shutdown();
 }
 

+ 0 - 3
tests/test_uwp.cmd

@@ -1,3 +0,0 @@
-cmake --preset win_uwp || exit /b 10
-cmake --build --preset win_uwp_debug || exit /b 10
-cmake --build --preset win_uwp_release || exit /b 10

+ 175 - 66
util/sokol_debugtext.h

@@ -60,6 +60,15 @@
 
             sdtx_setup(&(sdtx_desc_t){ ... });
 
+        To see any warnings and errors, you should always install a logging callback.
+        The easiest way is via sokol_log.h:
+
+            #include "sokol_log.h"
+
+            sdtx_setup(&(sdtx_desc_t){
+                .logger.func = slog_func,
+            });
+
     --- configure sokol-debugtext by populating the sdtx_desc_t struct:
 
         .context_pool_size (default: 8)
@@ -429,26 +438,47 @@
     If no overrides are provided, malloc and free will be used.
 
 
-    LOG FUNCTION OVERRIDE
-    =====================
-    You can override the log function at initialization time like this:
+    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"
+
+        sdtx_setup(&(sdtx_desc_t){
+            // ...
+            .logger.func = slog_func
+        });
 
-        void my_log(const char* message, void* user_data) {
-            printf("sdtx says: \s\n", message);
+    To override logging with your own callback, first write a logging function like this:
+
+        void my_log(const char* tag,                // e.g. 'sdtx'
+                    uint32_t log_level,             // 0=panic, 1=error, 2=warn, 3=info
+                    uint32_t log_item_id,           // SDTX_LOGITEM_*
+                    const char* message_or_null,    // a message string, may be nullptr in release mode
+                    uint32_t line_nr,               // line number in sokol_debugtext.h
+                    const char* filename_or_null,   // source filename, may be nullptr in release mode
+                    void* user_data)
+        {
+            ...
         }
 
-        ...
-            sdtx_setup(&(sdtx_desc_t){
-                // ...
-                .logger = {
-                    .log_cb = my_log,
-                    .user_data = ...,
-                }
-            });
-        ...
+    ...and then setup sokol-debugtext like this:
+
+        sdtx_setup(&(sdtx_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.
 
-    If no overrides are provided, puts will be used on most platforms.
-    On Android, __android_log_write will be used instead.
 
     LICENSE
     =======
@@ -508,6 +538,46 @@
 extern "C" {
 #endif
 
+/*
+    sdtx_log_item_t
+
+    Log items are defined via X-Macros, and expanded to an
+    enum 'sdtx_log_item' - and in debug mode only - corresponding strings.
+
+    Used as parameter in the logging callback.
+*/
+#define _SDTX_LOG_ITEMS \
+    _SDTX_LOGITEM_XMACRO(OK, "Ok") \
+    _SDTX_LOGITEM_XMACRO(MALLOC_FAILED, "memory allocation failed") \
+    _SDTX_LOGITEM_XMACRO(ADD_COMMIT_LISTENER_FAILED, "sg_add_commit_listener() failed") \
+    _SDTX_LOGITEM_XMACRO(COMMAND_BUFFER_FULL, "command buffer full (adjust via sdtx_context_desc_t.max_commands)") \
+    _SDTX_LOGITEM_XMACRO(CONTEXT_POOL_EXHAUSTED, "context pool exhausted (adjust via sdtx_desc_t.context_pool_size)") \
+    _SDTX_LOGITEM_XMACRO(CANNOT_DESTROY_DEFAULT_CONTEXT, "cannot destroy default context") \
+
+#define _SDTX_LOGITEM_XMACRO(item,msg) SDTX_LOGITEM_##item,
+typedef enum sdtx_log_item_t {
+    _SDTX_LOG_ITEMS
+} sdtx_log_item_t;
+#undef _SDTX_LOGITEM_XMACRO
+
+/*
+    sdtx_logger_t
+
+    Used in sdtx_desc_t to provide a custom logging and error reporting
+    callback to sokol-debugtext.
+*/
+typedef struct sdtx_logger_t {
+    void (*func)(
+        const char* tag,                // always "sdtx"
+        uint32_t log_level,             // 0=panic, 1=error, 2=warning, 3=info
+        uint32_t log_item_id,           // SDTX_LOGITEM_*
+        const char* message_or_null,    // a message string, may be nullptr in release mode
+        uint32_t line_nr,               // line number in sokol_debugtext.h
+        const char* filename_or_null,   // source filename, may be nullptr in release mode
+        void* user_data);
+    void* user_data;
+} sdtx_logger_t;
+
 /* a rendering context handle */
 typedef struct sdtx_context { uint32_t id; } sdtx_context;
 
@@ -598,17 +668,6 @@ typedef struct sdtx_allocator_t {
     void* user_data;
 } sdtx_allocator_t;
 
-/*
-    sdtx_logger_t
-
-    Used in sdtx_desc_t to provide custom log callbacks to sokol_debugtext.h.
-    Default behavior is SOKOL_LOG(message).
-*/
-typedef struct sdtx_logger_t {
-    void (*log_cb)(const char* message, void* user_data);
-    void* user_data;
-} sdtx_logger_t;
-
 /*
     sdtx_desc_t
 
@@ -631,7 +690,7 @@ typedef struct sdtx_desc_t {
     sdtx_font_desc_t fonts[SDTX_MAX_FONTS]; // up to 8 fonts descriptions
     sdtx_context_desc_t context;            // the default context creation parameters
     sdtx_allocator_t allocator;             // optional memory allocation overrides (default: malloc/free)
-    sdtx_logger_t logger;                   // optional log override functions (default: SOKOL_LOG(message))
+    sdtx_logger_t logger;                   // optional log override function (default: NO LOGGING)
 } sdtx_desc_t;
 
 /* initialization/shutdown */
@@ -703,7 +762,13 @@ inline sdtx_context sdtx_make_context(const sdtx_context_desc_t& desc) { return
 #endif
 #endif /* SOKOL_DEBUGTEXT_INCLUDED */
 
-/*-- IMPLEMENTATION ----------------------------------------------------------*/
+// ██ ███    ███ ██████  ██      ███████ ███    ███ ███████ ███    ██ ████████  █████  ████████ ██  ██████  ███    ██
+// ██ ████  ████ ██   ██ ██      ██      ████  ████ ██      ████   ██    ██    ██   ██    ██    ██ ██    ██ ████   ██
+// ██ ██ ████ ██ ██████  ██      █████   ██ ████ ██ █████   ██ ██  ██    ██    ███████    ██    ██ ██    ██ ██ ██  ██
+// ██ ██  ██  ██ ██      ██      ██      ██  ██  ██ ██      ██  ██ ██    ██    ██   ██    ██    ██ ██    ██ ██  ██ ██
+// ██ ██      ██ ██      ███████ ███████ ██      ██ ███████ ██   ████    ██    ██   ██    ██    ██  ██████  ██   ████
+//
+// >>implementation
 #ifdef SOKOL_DEBUGTEXT_IMPL
 #define SOKOL_DEBUGTEXT_IMPL_INCLUDED (1)
 
@@ -729,21 +794,6 @@ inline sdtx_context sdtx_make_context(const sdtx_context_desc_t& desc) { return
     #define SOKOL_ASSERT(c) assert(c)
 #endif
 
-#if !defined(SOKOL_DEBUG)
-    #define SDTX_LOG(s)
-#else
-    #define SDTX_LOG(s) _sdtx_log(s)
-    #ifndef SOKOL_LOG
-        #if defined(__ANDROID__)
-            #include <android/log.h>
-            #define SOKOL_LOG(s) __android_log_write(ANDROID_LOG_INFO, "SOKOL_DEBUGTEXT", s)
-        #else
-            #include <stdio.h>
-            #define SOKOL_LOG(s) puts(s)
-        #endif
-    #endif
-#endif
-
 #ifndef SOKOL_UNREACHABLE
     #define SOKOL_UNREACHABLE SOKOL_ASSERT(false)
 #endif
@@ -3498,6 +3548,13 @@ static const char* _sdtx_fs_src_dummy = "";
 #error "Please define one of SOKOL_GLCORE33, SOKOL_GLES2, SOKOL_GLES3, SOKOL_D3D11, SOKOL_METAL, SOKOL_WGPU or SOKOL_DUMMY_BACKEND!"
 #endif
 
+// ███████ ████████ ██████  ██    ██  ██████ ████████ ███████
+// ██         ██    ██   ██ ██    ██ ██         ██    ██
+// ███████    ██    ██████  ██    ██ ██         ██    ███████
+//      ██    ██    ██   ██ ██    ██ ██         ██         ██
+// ███████    ██    ██   ██  ██████   ██████    ██    ███████
+//
+// >>structs
 typedef struct {
     uint32_t id;
     sg_resource_state state;
@@ -3573,8 +3630,52 @@ typedef struct {
 } _sdtx_t;
 static _sdtx_t _sdtx;
 
-/*=== MEMORY HELPERS =========================================================*/
+// ██       ██████   ██████   ██████  ██ ███    ██  ██████
+// ██      ██    ██ ██       ██       ██ ████   ██ ██
+// ██      ██    ██ ██   ███ ██   ███ ██ ██ ██  ██ ██   ███
+// ██      ██    ██ ██    ██ ██    ██ ██ ██  ██ ██ ██    ██
+// ███████  ██████   ██████   ██████  ██ ██   ████  ██████
+//
+// >>logging
+#if defined(SOKOL_DEBUG)
+#define _SDTX_LOGITEM_XMACRO(item,msg) #item ": " msg,
+static const char* _sdtx_log_messages[] = {
+    _SDTX_LOG_ITEMS
+};
+#undef _SDTX_LOGITEM_XMACRO
+#endif // SOKOL_DEBUG
+
+#define _SDTX_PANIC(code) _sdtx_log(SDTX_LOGITEM_ ##code, 0, __LINE__)
+#define _SDTX_ERROR(code) _sdtx_log(SDTX_LOGITEM_ ##code, 1, __LINE__)
+#define _SDTX_WARN(code) _sdtx_log(SDTX_LOGITEM_ ##code, 2, __LINE__)
+#define _SDTX_INFO(code) _sdtx_log(SDTX_LOGITEM_ ##code, 3, __LINE__)
+
+static void _sdtx_log(sdtx_log_item_t log_item, uint32_t log_level, uint32_t line_nr) {
+    if (_sdtx.desc.logger.func) {
+        #if defined(SOKOL_DEBUG)
+            const char* filename = __FILE__;
+            const char* message = _sdtx_log_messages[log_item];
+        #else
+            const char* filename = 0;
+            const char* message = 0;
+        #endif
+        _sdtx.desc.logger.func("sdtx", log_level, log_item, message, line_nr, filename, _sdtx.desc.logger.user_data);
+    }
+    else {
+        // for log level PANIC it would be 'undefined behaviour' to continue
+        if (log_level == 0) {
+            abort();
+        }
+    }
+}
 
+// ███    ███ ███████ ███    ███  ██████  ██████  ██    ██
+// ████  ████ ██      ████  ████ ██    ██ ██   ██  ██  ██
+// ██ ████ ██ █████   ██ ████ ██ ██    ██ ██████    ████
+// ██  ██  ██ ██      ██  ██  ██ ██    ██ ██   ██    ██
+// ██      ██ ███████ ██      ██  ██████  ██   ██    ██
+//
+// >>memory
 static void _sdtx_clear(void* ptr, size_t size) {
     SOKOL_ASSERT(ptr && (size > 0));
     memset(ptr, 0, size);
@@ -3589,7 +3690,9 @@ static void* _sdtx_malloc(size_t size) {
     else {
         ptr = malloc(size);
     }
-    SOKOL_ASSERT(ptr);
+    if (0 == ptr) {
+        _SDTX_PANIC(MALLOC_FAILED);
+    }
     return ptr;
 }
 
@@ -3608,18 +3711,13 @@ static void _sdtx_free(void* ptr) {
     }
 }
 
-#if defined(SOKOL_DEBUG)
-static void _sdtx_log(const char* msg) {
-    SOKOL_ASSERT(msg);
-    if (_sdtx.desc.logger.log_cb) {
-        _sdtx.desc.logger.log_cb(msg, _sdtx.desc.logger.user_data);
-    } else {
-        SOKOL_LOG(msg);
-    }
-}
-#endif
-
-/*=== CONTEXT POOL ===========================================================*/
+//  ██████  ██████  ███    ██ ████████ ███████ ██   ██ ████████     ██████   ██████   ██████  ██
+// ██      ██    ██ ████   ██    ██    ██       ██ ██     ██        ██   ██ ██    ██ ██    ██ ██
+// ██      ██    ██ ██ ██  ██    ██    █████     ███      ██        ██████  ██    ██ ██    ██ ██
+// ██      ██    ██ ██  ██ ██    ██    ██       ██ ██     ██        ██      ██    ██ ██    ██ ██
+//  ██████  ██████  ██   ████    ██    ███████ ██   ██    ██        ██       ██████   ██████  ███████
+//
+// >>context pool
 static void _sdtx_init_pool(_sdtx_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 */
@@ -3853,9 +3951,7 @@ static void _sdtx_init_context(sdtx_context ctx_id, const sdtx_context_desc_t* i
     ctx->color = _SDTX_DEFAULT_COLOR;
 
     if (!sg_add_commit_listener(_sdtx_make_commit_listener(ctx))) {
-        // FIXME: this should actually result in an invalid context,
-        // fix this when proper error logging/reporting is added
-        SDTX_LOG("sokol_debugtext.h: failed to add sokol-gfx commit listener");
+        _SDTX_ERROR(ADD_COMMIT_LISTENER_FAILED);
     }
     sg_pop_debug_group();
 }
@@ -3889,6 +3985,14 @@ static bool _sdtx_is_default_context(sdtx_context ctx_id) {
     return ctx_id.id == SDTX_DEFAULT_CONTEXT.id;
 }
 
+// ███    ███ ██ ███████  ██████
+// ████  ████ ██ ██      ██
+// ██ ████ ██ ██ ███████ ██
+// ██  ██  ██ ██      ██ ██
+// ██      ██ ██ ███████  ██████
+//
+// >>misc
+
 /* unpack linear 8x8 bits-per-pixel font data into 2D byte-per-pixel texture data */
 static void _sdtx_unpack_font(const sdtx_font_desc_t* font_desc, uint8_t* out_pixels) {
     SOKOL_ASSERT(font_desc->data.ptr);
@@ -4065,7 +4169,7 @@ static _sdtx_command_t* _sdtx_next_command(_sdtx_context_t* ctx) {
         return &ctx->commands.ptr[ctx->commands.next++];
     }
     else {
-        SDTX_LOG("sokol_debugtext.h: command buffer full");
+        _SDTX_ERROR(COMMAND_BUFFER_FULL);
         return 0;
     }
 }
@@ -4191,8 +4295,13 @@ static sdtx_desc_t _sdtx_desc_defaults(const sdtx_desc_t* desc) {
     return res;
 }
 
-/*=== PUBLIC API FUNCTIONS ===================================================*/
-
+// ██████  ██    ██ ██████  ██      ██  ██████
+// ██   ██ ██    ██ ██   ██ ██      ██ ██
+// ██████  ██    ██ ██████  ██      ██ ██
+// ██      ██    ██ ██   ██ ██      ██ ██
+// ██       ██████  ██████  ███████ ██  ██████
+//
+// >>public
 SOKOL_API_IMPL void sdtx_setup(const sdtx_desc_t* desc) {
     SOKOL_ASSERT(desc);
     _sdtx_clear(&_sdtx, sizeof(_sdtx));
@@ -4254,7 +4363,7 @@ SOKOL_API_IMPL sdtx_context sdtx_make_context(const sdtx_context_desc_t* desc) {
         _sdtx_init_context(ctx_id, desc);
     }
     else {
-        SDTX_LOG("sokol_debugtext.h: context pool exhausted!");
+        _SDTX_ERROR(CONTEXT_POOL_EXHAUSTED);
     }
     return ctx_id;
 }
@@ -4262,7 +4371,7 @@ SOKOL_API_IMPL sdtx_context sdtx_make_context(const sdtx_context_desc_t* desc) {
 SOKOL_API_IMPL void sdtx_destroy_context(sdtx_context ctx_id) {
     SOKOL_ASSERT(_SDTX_INIT_COOKIE == _sdtx.init_cookie);
     if (_sdtx_is_default_context(ctx_id)) {
-        SDTX_LOG("sokol_debugtext.h: cannot destroy default context");
+        _SDTX_ERROR(CANNOT_DESTROY_DEFAULT_CONTEXT);
         return;
     }
     _sdtx_destroy_context(ctx_id);

+ 238 - 110
util/sokol_gl.h

@@ -101,14 +101,26 @@
         sokol-gfx resource objects.
 
         If you're intending to render to the default pass, and also don't
-        want to tweak memory usage, you can just keep sgl_desc_t zero-initialized:
+        want to tweak memory usage, and don't want any logging output you can
+        just keep sgl_desc_t zero-initialized:
 
             sgl_setup(&(sgl_desc_t*){ 0 });
 
         In this case, sokol-gl will create internal sg_pipeline objects that
-        are compatible with the sokol-app default framebuffer. If you want
-        to render into a framebuffer with different pixel-format and MSAA
-        attributes you need to provide the matching attributes in the
+        are compatible with the sokol-app default framebuffer.
+
+        I would recommend to at least install a logging callback so that
+        you'll see any warnings and errors. The easiest way is through
+        sokol_log.h:
+
+            #include "sokol_log.h"
+
+            sgl_setup(&(sgl_desc_t){
+                .logger.func = slog_func.
+            });
+
+        If you want to render into a framebuffer with different pixel-format
+        and MSAA attributes you need to provide the matching attributes in the
         sgl_setup() call:
 
             sgl_setup(&(sgl_desc_t*){
@@ -601,26 +613,46 @@
     If no overrides are provided, malloc and free will be used.
 
 
-    LOG FUNCTION OVERRIDE
-    =====================
-    You can override the log function at initialization time like this:
+    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"
+
+        sgl_setup(&(sgl_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* message, void* user_data) {
-            printf("sgl says: \s\n", message);
+        void my_log(const char* tag,                // e.g. 'sgl'
+                    uint32_t log_level,             // 0=panic, 1=error, 2=warn, 3=info
+                    uint32_t log_item_id,           // SGL_LOGITEM_*
+                    const char* message_or_null,    // a message string, may be nullptr in release mode
+                    uint32_t line_nr,               // line number in sokol_gl.h
+                    const char* filename_or_null,   // source filename, may be nullptr in release mode
+                    void* user_data)
+        {
+            ...
         }
 
-        ...
-            sgl_setup(&(sgl_desc_t){
-                // ...
-                .logger = {
-                    .log_cb = my_log,
-                    .user_data = ...,
-                }
-            });
-        ...
+    ...and then setup sokol-gl like this:
 
-    If no overrides are provided, puts will be used on most platforms.
-    On Android, __android_log_write will be used instead.
+        sgl_setup(&(sgl_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.
 
 
     LICENSE
@@ -674,6 +706,47 @@
 extern "C" {
 #endif
 
+/*
+    sgl_log_item_t
+
+    Log items are defined via X-Macros, and expanded to an
+    enum 'sgl_log_item' - and in debug mode only - corresponding strings.
+
+    Used as parameter in the logging callback.
+*/
+#define _SGL_LOG_ITEMS \
+    _SGL_LOGITEM_XMACRO(OK, "Ok") \
+    _SGL_LOGITEM_XMACRO(MALLOC_FAILED, "memory allocation failed") \
+    _SGL_LOGITEM_XMACRO(MAKE_PIPELINE_FAILED, "sg_make_pipeline() failed") \
+    _SGL_LOGITEM_XMACRO(PIPELINE_POOL_EXHAUSTED, "pipeline pool exhausted (use sgl_desc_t.pipeline_pool_size to adjust)") \
+    _SGL_LOGITEM_XMACRO(ADD_COMMIT_LISTENER_FAILED, "sg_add_commit_listener() failed") \
+    _SGL_LOGITEM_XMACRO(CONTEXT_POOL_EXHAUSTED, "context pool exhausted (use sgl_desc_t.context_pool_size to adjust)") \
+    _SGL_LOGITEM_XMACRO(CANNOT_DESTROY_DEFAULT_CONTEXT, "cannot destroy default context") \
+
+#define _SGL_LOGITEM_XMACRO(item,msg) SGL_LOGITEM_##item,
+typedef enum sgl_log_item_t {
+    _SGL_LOG_ITEMS
+} sgl_log_item_t;
+#undef _SGL_LOGITEM_XMACRO
+
+/*
+    sgl_logger_t
+
+    Used in sgl_desc_t to provide a custom logging and error reporting
+    callback to sokol-gl.
+*/
+typedef struct sgl_logger_t {
+    void (*func)(
+        const char* tag,                // always "sgl"
+        uint32_t log_level,             // 0=panic, 1=error, 2=warning, 3=info
+        uint32_t log_item_id,           // SGL_LOGITEM_*
+        const char* message_or_null,    // a message string, may be nullptr in release mode
+        uint32_t line_nr,               // line number in sokol_gl.h
+        const char* filename_or_null,   // source filename, may be nullptr in release mode
+        void* user_data);
+    void* user_data;
+} sgl_logger_t;
+
 /* sokol_gl pipeline handle (created with sgl_make_pipeline()) */
 typedef struct sgl_pipeline { uint32_t id; } sgl_pipeline;
 
@@ -725,17 +798,6 @@ typedef struct sgl_allocator_t {
     void* user_data;
 } sgl_allocator_t;
 
-/*
-    sgl_logger_t
-
-    Used in sgl_desc_t to provide custom log callbacks to sokol_gl.h.
-    Default behavior is SOKOL_LOG(message).
-*/
-typedef struct sgl_logger_t {
-    void (*log_cb)(const char* message, void* user_data);
-    void* user_data;
-} sgl_logger_t;
-
 typedef struct sgl_desc_t {
     int max_vertices;               // default: 64k
     int max_commands;               // default: 16k
@@ -746,7 +808,7 @@ typedef struct sgl_desc_t {
     int sample_count;
     sg_face_winding face_winding;   // default: SG_FACEWINDING_CCW
     sgl_allocator_t allocator;      // optional memory allocation overrides (default: malloc/free)
-    sgl_logger_t logger;            // optional memory allocation overrides (default: SOKOL_LOG(message))
+    sgl_logger_t logger;            // optional log function override (default: NO LOGGING)
 } sgl_desc_t;
 
 /* the default context handle */
@@ -867,7 +929,13 @@ inline sgl_pipeline sgl_context_make_pipeline(sgl_context ctx, const sg_pipeline
 #endif
 #endif /* SOKOL_GL_INCLUDED */
 
-/*-- IMPLEMENTATION ----------------------------------------------------------*/
+// ██ ███    ███ ██████  ██      ███████ ███    ███ ███████ ███    ██ ████████  █████  ████████ ██  ██████  ███    ██
+// ██ ████  ████ ██   ██ ██      ██      ████  ████ ██      ████   ██    ██    ██   ██    ██    ██ ██    ██ ████   ██
+// ██ ██ ████ ██ ██████  ██      █████   ██ ████ ██ █████   ██ ██  ██    ██    ███████    ██    ██ ██    ██ ██ ██  ██
+// ██ ██  ██  ██ ██      ██      ██      ██  ██  ██ ██      ██  ██ ██    ██    ██   ██    ██    ██ ██    ██ ██  ██ ██
+// ██ ██      ██ ██      ███████ ███████ ██      ██ ███████ ██   ████    ██    ██   ██    ██    ██  ██████  ██   ████
+//
+// >>implementation
 #ifdef SOKOL_GL_IMPL
 #define SOKOL_GL_IMPL_INCLUDED (1)
 
@@ -896,21 +964,6 @@ inline sgl_pipeline sgl_context_make_pipeline(sgl_context ctx, const sg_pipeline
     #define SOKOL_ASSERT(c) assert(c)
 #endif
 
-#if !defined(SOKOL_DEBUG)
-    #define SGL_LOG(s)
-#else
-    #define SGL_LOG(s) _sgl_log(s)
-    #ifndef SOKOL_LOG
-        #if defined(__ANDROID__)
-            #include <android/log.h>
-            #define SOKOL_LOG(s) __android_log_write(ANDROID_LOG_INFO, "SOKOL_GL", s)
-        #else
-            #include <stdio.h>
-            #define SOKOL_LOG(s) puts(s)
-        #endif
-    #endif
-#endif
-
 #define _sgl_def(val, def) (((val) == 0) ? (def) : (val))
 #define _SGL_INIT_COOKIE (0xABCDABCD)
 
@@ -2215,6 +2268,13 @@ static const char* _sgl_fs_source_dummy = "";
 #error "Please define one of SOKOL_GLCORE33, SOKOL_GLES2, SOKOL_GLES3, SOKOL_D3D11, SOKOL_METAL, SOKOL_WGPU or SOKOL_DUMMY_BACKEND!"
 #endif
 
+// ████████ ██    ██ ██████  ███████ ███████
+//    ██     ██  ██  ██   ██ ██      ██
+//    ██      ████   ██████  █████   ███████
+//    ██       ██    ██      ██           ██
+//    ██       ██    ██      ███████ ███████
+//
+// >>types
 typedef enum {
     SGL_PRIMITIVETYPE_POINTS = 0,
     SGL_PRIMITIVETYPE_LINES,
@@ -2384,7 +2444,52 @@ typedef struct {
 } _sgl_t;
 static _sgl_t _sgl;
 
-/*== PRIVATE FUNCTIONS =======================================================*/
+// ██       ██████   ██████   ██████  ██ ███    ██  ██████
+// ██      ██    ██ ██       ██       ██ ████   ██ ██
+// ██      ██    ██ ██   ███ ██   ███ ██ ██ ██  ██ ██   ███
+// ██      ██    ██ ██    ██ ██    ██ ██ ██  ██ ██ ██    ██
+// ███████  ██████   ██████   ██████  ██ ██   ████  ██████
+//
+// >>logging
+#if defined(SOKOL_DEBUG)
+#define _SGL_LOGITEM_XMACRO(item,msg) #item ": " msg,
+static const char* _sgl_log_messages[] = {
+    _SGL_LOG_ITEMS
+};
+#undef _SGL_LOGITEM_XMACRO
+#endif // SOKOL_DEBUG
+
+#define _SGL_PANIC(code) _sgl_log(SGL_LOGITEM_ ##code, 0, __LINE__)
+#define _SGL_ERROR(code) _sgl_log(SGL_LOGITEM_ ##code, 1, __LINE__)
+#define _SGL_WARN(code) _sgl_log(SGL_LOGITEM_ ##code, 2, __LINE__)
+#define _SGL_INFO(code) _sgl_log(SGL_LOGITEM_ ##code, 3, __LINE__)
+
+static void _sgl_log(sgl_log_item_t log_item, uint32_t log_level, uint32_t line_nr) {
+    if (_sgl.desc.logger.func) {
+        #if defined(SOKOL_DEBUG)
+            const char* filename = __FILE__;
+            const char* message = _sgl_log_messages[log_item];
+        #else
+            const char* filename = 0;
+            const char* message = 0;
+        #endif
+        _sgl.desc.logger.func("sgl", log_level, log_item, message, line_nr, filename, _sgl.desc.logger.user_data);
+    }
+    else {
+        // for log level PANIC it would be 'undefined behaviour' to continue
+        if (log_level == 0) {
+            abort();
+        }
+    }
+}
+
+// ███    ███ ███████ ███    ███  ██████  ██████  ██    ██
+// ████  ████ ██      ████  ████ ██    ██ ██   ██  ██  ██
+// ██ ████ ██ █████   ██ ████ ██ ██    ██ ██████    ████
+// ██  ██  ██ ██      ██  ██  ██ ██    ██ ██   ██    ██
+// ██      ██ ███████ ██      ██  ██████  ██   ██    ██
+//
+// >>memory
 static void _sgl_clear(void* ptr, size_t size) {
     SOKOL_ASSERT(ptr && (size > 0));
     memset(ptr, 0, size);
@@ -2399,7 +2504,9 @@ static void* _sgl_malloc(size_t size) {
     else {
         ptr = malloc(size);
     }
-    SOKOL_ASSERT(ptr);
+    if (0 == ptr) {
+        _SGL_PANIC(MALLOC_FAILED);
+    }
     return ptr;
 }
 
@@ -2418,17 +2525,13 @@ static void _sgl_free(void* ptr) {
     }
 }
 
-#if defined(SOKOL_DEBUG)
-static void _sgl_log(const char* msg) {
-    SOKOL_ASSERT(msg);
-    if (_sgl.desc.logger.log_cb) {
-        _sgl.desc.logger.log_cb(msg, _sgl.desc.logger.user_data);
-    } else {
-        SOKOL_LOG(msg);
-    }
-}
-#endif
-
+// ██████   ██████   ██████  ██
+// ██   ██ ██    ██ ██    ██ ██
+// ██████  ██    ██ ██    ██ ██
+// ██      ██    ██ ██    ██ ██
+// ██       ██████   ██████  ███████
+//
+// >>pool
 static void _sgl_init_pool(_sgl_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 */
@@ -2486,47 +2589,6 @@ static void _sgl_pool_free_index(_sgl_pool_t* pool, int slot_index) {
     SOKOL_ASSERT(pool->queue_top <= (pool->size-1));
 }
 
-static void _sgl_reset_context(_sgl_context_t* ctx) {
-    SOKOL_ASSERT(ctx);
-    SOKOL_ASSERT(0 == ctx->vertices.ptr);
-    SOKOL_ASSERT(0 == ctx->uniforms.ptr);
-    SOKOL_ASSERT(0 == ctx->commands.ptr);
-    _sgl_clear(ctx, sizeof(_sgl_context_t));
-}
-
-static void _sgl_setup_context_pool(int pool_size) {
-    /* note: the pools here will have an additional item, since slot 0 is reserved */
-    SOKOL_ASSERT((pool_size > 0) && (pool_size < _SGL_MAX_POOL_SIZE));
-    _sgl_init_pool(&_sgl.context_pool.pool, pool_size);
-    size_t pool_byte_size = sizeof(_sgl_context_t) * (size_t)_sgl.context_pool.pool.size;
-    _sgl.context_pool.contexts = (_sgl_context_t*) _sgl_malloc_clear(pool_byte_size);
-}
-
-static void _sgl_discard_context_pool(void) {
-    SOKOL_ASSERT(0 != _sgl.context_pool.contexts);
-    _sgl_free(_sgl.context_pool.contexts); _sgl.context_pool.contexts = 0;
-    _sgl_discard_pool(&_sgl.context_pool.pool);
-}
-
-static void _sgl_reset_pipeline(_sgl_pipeline_t* pip) {
-    SOKOL_ASSERT(pip);
-    _sgl_clear(pip, sizeof(_sgl_pipeline_t));
-}
-
-static void _sgl_setup_pipeline_pool(int pool_size) {
-    /* note: the pools here will have an additional item, since slot 0 is reserved */
-    SOKOL_ASSERT((pool_size > 0) && (pool_size < _SGL_MAX_POOL_SIZE));
-    _sgl_init_pool(&_sgl.pip_pool.pool, pool_size);
-    size_t pool_byte_size = sizeof(_sgl_pipeline_t) * (size_t)_sgl.pip_pool.pool.size;
-    _sgl.pip_pool.pips = (_sgl_pipeline_t*) _sgl_malloc_clear(pool_byte_size);
-}
-
-static void _sgl_discard_pipeline_pool(void) {
-    SOKOL_ASSERT(0 != _sgl.pip_pool.pips);
-    _sgl_free(_sgl.pip_pool.pips); _sgl.pip_pool.pips = 0;
-    _sgl_discard_pool(&_sgl.pip_pool.pool);
-}
-
 /* allocate the slot at slot_index:
     - bump the slot's generation counter
     - create a resource id from the generation counter and slot index
@@ -2555,6 +2617,32 @@ static int _sgl_slot_index(uint32_t id) {
     return slot_index;
 }
 
+// ██████  ██ ██████  ███████ ██      ██ ███    ██ ███████ ███████
+// ██   ██ ██ ██   ██ ██      ██      ██ ████   ██ ██      ██
+// ██████  ██ ██████  █████   ██      ██ ██ ██  ██ █████   ███████
+// ██      ██ ██      ██      ██      ██ ██  ██ ██ ██           ██
+// ██      ██ ██      ███████ ███████ ██ ██   ████ ███████ ███████
+//
+// >>pipelines
+static void _sgl_reset_pipeline(_sgl_pipeline_t* pip) {
+    SOKOL_ASSERT(pip);
+    _sgl_clear(pip, sizeof(_sgl_pipeline_t));
+}
+
+static void _sgl_setup_pipeline_pool(int pool_size) {
+    /* note: the pools here will have an additional item, since slot 0 is reserved */
+    SOKOL_ASSERT((pool_size > 0) && (pool_size < _SGL_MAX_POOL_SIZE));
+    _sgl_init_pool(&_sgl.pip_pool.pool, pool_size);
+    size_t pool_byte_size = sizeof(_sgl_pipeline_t) * (size_t)_sgl.pip_pool.pool.size;
+    _sgl.pip_pool.pips = (_sgl_pipeline_t*) _sgl_malloc_clear(pool_byte_size);
+}
+
+static void _sgl_discard_pipeline_pool(void) {
+    SOKOL_ASSERT(0 != _sgl.pip_pool.pips);
+    _sgl_free(_sgl.pip_pool.pips); _sgl.pip_pool.pips = 0;
+    _sgl_discard_pool(&_sgl.pip_pool.pool);
+}
+
 /* get pipeline pointer without id-check */
 static _sgl_pipeline_t* _sgl_pipeline_at(uint32_t pip_id) {
     SOKOL_ASSERT(SG_INVALID_ID != pip_id);
@@ -2665,7 +2753,7 @@ static void _sgl_init_pipeline(sgl_pipeline pip_id, const sg_pipeline_desc* in_d
         else {
             pip->pip[i] = sg_make_pipeline(&desc);
             if (pip->pip[i].id == SG_INVALID_ID) {
-                SGL_LOG("sokol_gl.h: failed to create pipeline object");
+                _SGL_ERROR(MAKE_PIPELINE_FAILED);
                 pip->slot.state = SG_RESOURCESTATE_FAILED;
             }
         }
@@ -2679,7 +2767,7 @@ static sgl_pipeline _sgl_make_pipeline(const sg_pipeline_desc* desc, const sgl_c
         _sgl_init_pipeline(pip_id, desc, ctx_desc);
     }
     else {
-        SGL_LOG("sokol_gl.h: pipeline pool exhausted!");
+        _SGL_ERROR(PIPELINE_POOL_EXHAUSTED);
     }
     return pip_id;
 }
@@ -2710,6 +2798,35 @@ static sg_pipeline _sgl_get_pipeline(sgl_pipeline pip_id, _sgl_primitive_type_t
     }
 }
 
+//  ██████  ██████  ███    ██ ████████ ███████ ██   ██ ████████ ███████
+// ██      ██    ██ ████   ██    ██    ██       ██ ██     ██    ██
+// ██      ██    ██ ██ ██  ██    ██    █████     ███      ██    ███████
+// ██      ██    ██ ██  ██ ██    ██    ██       ██ ██     ██         ██
+//  ██████  ██████  ██   ████    ██    ███████ ██   ██    ██    ███████
+//
+// >>contexts
+static void _sgl_reset_context(_sgl_context_t* ctx) {
+    SOKOL_ASSERT(ctx);
+    SOKOL_ASSERT(0 == ctx->vertices.ptr);
+    SOKOL_ASSERT(0 == ctx->uniforms.ptr);
+    SOKOL_ASSERT(0 == ctx->commands.ptr);
+    _sgl_clear(ctx, sizeof(_sgl_context_t));
+}
+
+static void _sgl_setup_context_pool(int pool_size) {
+    /* note: the pools here will have an additional item, since slot 0 is reserved */
+    SOKOL_ASSERT((pool_size > 0) && (pool_size < _SGL_MAX_POOL_SIZE));
+    _sgl_init_pool(&_sgl.context_pool.pool, pool_size);
+    size_t pool_byte_size = sizeof(_sgl_context_t) * (size_t)_sgl.context_pool.pool.size;
+    _sgl.context_pool.contexts = (_sgl_context_t*) _sgl_malloc_clear(pool_byte_size);
+}
+
+static void _sgl_discard_context_pool(void) {
+    SOKOL_ASSERT(0 != _sgl.context_pool.contexts);
+    _sgl_free(_sgl.context_pool.contexts); _sgl.context_pool.contexts = 0;
+    _sgl_discard_pool(&_sgl.context_pool.pool);
+}
+
 // get context pointer without id-check
 static _sgl_context_t* _sgl_context_at(uint32_t ctx_id) {
     SOKOL_ASSERT(SG_INVALID_ID != ctx_id);
@@ -2792,9 +2909,7 @@ static void _sgl_init_context(sgl_context ctx_id, const sgl_context_desc_t* in_d
     def_pip_desc.depth.write_enabled = true;
     ctx->def_pip = _sgl_make_pipeline(&def_pip_desc, &ctx->desc);
     if (!sg_add_commit_listener(_sgl_make_commit_listener(ctx))) {
-        // FIXME: this should actually result in an invalid context,
-        // fix this when proper error logging/reporting is added
-        SGL_LOG("sokol_gl.h: failed to add sokol-gfx commit listener!");
+        _SGL_ERROR(ADD_COMMIT_LISTENER_FAILED);
     }
     sg_pop_debug_group();
 
@@ -2815,7 +2930,7 @@ static sgl_context _sgl_make_context(const sgl_context_desc_t* desc) {
         _sgl_init_context(ctx_id, desc);
     }
     else {
-        SGL_LOG("sokol_gl.h: context pool exhausted!");
+        _SGL_ERROR(CONTEXT_POOL_EXHAUSTED);
     }
     return ctx_id;
 }
@@ -2845,6 +2960,13 @@ static void _sgl_destroy_context(sgl_context ctx_id) {
     }
 }
 
+// ███    ███ ██ ███████  ██████
+// ████  ████ ██ ██      ██
+// ██ ████ ██ ██ ███████ ██
+// ██  ██  ██ ██      ██ ██
+// ██      ██ ██ ███████  ██████
+//
+// >>misc
 static void _sgl_begin(_sgl_context_t* ctx, _sgl_primitive_type_t mode) {
     ctx->in_begin = true;
     ctx->base_vertex = ctx->vertices.next;
@@ -3350,7 +3472,13 @@ static sgl_context_desc_t _sgl_as_context_desc(const sgl_desc_t* desc) {
     return ctx_desc;
 }
 
-/*== PUBLIC FUNCTIONS ========================================================*/
+// ██████  ██    ██ ██████  ██      ██  ██████
+// ██   ██ ██    ██ ██   ██ ██      ██ ██
+// ██████  ██    ██ ██████  ██      ██ ██
+// ██      ██    ██ ██   ██ ██      ██ ██
+// ██       ██████  ██████  ███████ ██  ██████
+//
+// >>public
 SOKOL_API_IMPL void sgl_setup(const sgl_desc_t* desc) {
     SOKOL_ASSERT(desc);
     _sgl_clear(&_sgl, sizeof(_sgl));
@@ -3418,7 +3546,7 @@ SOKOL_API_IMPL sgl_context sgl_make_context(const sgl_context_desc_t* desc) {
 SOKOL_API_IMPL void sgl_destroy_context(sgl_context ctx_id) {
     SOKOL_ASSERT(_SGL_INIT_COOKIE == _sgl.init_cookie);
     if (_sgl_is_default_context(ctx_id)) {
-        SGL_LOG("sokol_gl.h: cannot destroy default context");
+        _SGL_WARN(CANNOT_DESTROY_DEFAULT_CONTEXT);
         return;
     }
     _sgl_destroy_context(ctx_id);

+ 321 - 236
util/sokol_spine.h

@@ -166,6 +166,18 @@
         sg_setup(&(sg_desc){ ... });
         sspine_setup(&(sspine_desc){ ... });
 
+    - You should always provide a logging callback to sokol-spine, otherwise
+      no warning or errors will be logged. The easiest way is to use sokol_log.h
+      for this:
+
+        #include "sokol_log.h"
+
+        sspine_setup(&(sspine_desc){
+            .logger = {
+                .func = slog_func
+            }
+        });
+
     - You can tweak the memory usage of sokol-spine by limiting or expanding the
       maximum number of vertices, draw commands and pool sizes:
 
@@ -177,18 +189,28 @@
             .skeleton_pool_size = 1,    // default: 64
             .skinset_pool_size = 1,     // default: 64
             .instance_pool_size = 16,   // default: 1024
+            .logger = {
+                .func = slog_func,
+            }
         });
 
       Sokol-spine uses 32-bit vertex-indices for rendering
       (SG_INDEXTYPE_UINT32), so that the maximum number of Spine vertices
       in a frame isn't limited to (1<<16).
 
-    - You can override the default memory allocation and
-      error logging functions, this is explained in detail further down:
+    - You can override memory allocation and logging with your own
+      functions, this is explained in detail further down:
 
         sspine_setup(&(sspine_desc){
-            .allocator = { ... },
-            .logger = { ... }
+            .allocator = {
+                .alloc = my_alloc,
+                .free = my_free,
+                .user_data = ...,
+            },
+            .logger = {
+                .log_func = my_log_func,
+                .user_data = ...,
+            }
         });
 
     - After initialization, the first thing you need is an sspine_atlas object.
@@ -824,34 +846,24 @@
     Calling sspine_set_skinset() deactivates the effect of sspine_set_skin() and
     vice versa.
 
+
     ERROR REPORTING AND LOGGING
     ===========================
-    sokol_spine.h introduces a new combined logging- and error-reporting
-    mechanism which replaces the old SOKOL_LOG macro, and the more recent
-    logging callback.
-
-    The new reporting uses a more elaborate logger callback which provides:
-
-        - a short tag string identifying the header (for instance 'sspine')
-        - a numeric log level (panic, error, warning, info)
-        - a numeric error code (SSPINE_ERROR_*)
-        - in debug mode: the error code as human readable string
-        - a line number, where in the header the problem occured
-        - in debug mode: the filename of the header
-        - and a user data parameter
-
-    The logging callback will be standardized across all sokol headers,
-    so that it will be possible to use the same logging function with
-    all headers.
-
-    To override logging, first write a logging function like this:
-
-        void my_log(const char* tag,        // e.g. 'sspine'
-                    uint32_t log_level,     // 0=panic, 1=error, 2=warn, 3=info
-                    uint32_t error_code,    // SSPINE_ERROR_*
-                    const char* error_id,   // error as string, only in debug mode, otherwise empty string
-                    int line_nr,            // line number in sokol_spine.h
-                    const char* filename,   // debug mode only, otherwise empty string
+    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"
+
+        sspine_setup(&(sspine_desc){ .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. 'sspine'
+                    uint32_t log_level,             // 0=panic, 1=error, 2=warn, 3=info
+                    uint32_t log_item_id,           // SSPINE_LOGITEM_*
+                    const char* message_or_null,    // a message string, may be nullptr in release mode
+                    uint32_t line_nr,               // line number in sokol_spine.h
+                    const char* filename_or_null,   // source filename, may be nullptr in release mode
                     void* user_data)
         {
             ...
@@ -862,16 +874,17 @@
         sspine_setup(&(sspine_desc){
             .logger = {
                 .func = my_log,
-                .user_data = ...,
+                .user_data = my_user_data,
             }
         });
 
-    If no custom logger is provided, verbose default logging goes to stderr
-    (this means you won't see any logging messages on Android, or on Windows
-    unless the process is attached to a terminal!).
+    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.
 
-    Eventually there will be a more luxurious sokol_log.h header, which will
-    provide more control over logging, also on Windows or Android.
 
     MEMORY ALLOCATION OVERRIDE
     ==========================
@@ -902,6 +915,7 @@
     This only affects memory allocation calls done by sokol_gfx.h
     itself though, not any allocations in OS libraries.
 
+
     LICENSE
     =======
     zlib/libpng license
@@ -995,49 +1009,44 @@ typedef enum sspine_resource_state {
     _SSPINE_RESOURCESTATE_FORCE_U32 = 0x7FFFFFFF
 } sspine_resource_state;
 
-// error codes via x-macro magic
-#define _SSPINE_ERRORS \
-    _SSPINE_XMACRO(OK)\
-    _SSPINE_XMACRO(CONTEXT_POOL_EXHAUSTED)\
-    _SSPINE_XMACRO(ATLAS_POOL_EXHAUSTED)\
-    _SSPINE_XMACRO(SKELETON_POOL_EXHAUSTED)\
-    _SSPINE_XMACRO(SKINSET_POOL_EXHAUSTED)\
-    _SSPINE_XMACRO(INSTANCE_POOL_EXHAUSTED)\
-    _SSPINE_XMACRO(CANNOT_DESTROY_DEFAULT_CONTEXT)\
-    _SSPINE_XMACRO(ATLAS_DESC_NO_DATA)\
-    _SSPINE_XMACRO(SPINE_ATLAS_CREATION_FAILED)\
-    _SSPINE_XMACRO(SG_ALLOC_IMAGE_FAILED)\
-    _SSPINE_XMACRO(SKELETON_DESC_NO_DATA)\
-    _SSPINE_XMACRO(SKELETON_DESC_NO_ATLAS)\
-    _SSPINE_XMACRO(SKELETON_ATLAS_NOT_VALID)\
-    _SSPINE_XMACRO(SPINE_SKELETON_DATA_CREATION_FAILED)\
-    _SSPINE_XMACRO(SKINSET_DESC_NO_SKELETON)\
-    _SSPINE_XMACRO(SKINSET_SKELETON_NOT_VALID)\
-    _SSPINE_XMACRO(SKINSET_INVALID_SKIN_HANDLE)\
-    _SSPINE_XMACRO(INSTANCE_DESC_NO_SKELETON)\
-    _SSPINE_XMACRO(INSTANCE_SKELETON_NOT_VALID)\
-    _SSPINE_XMACRO(INSTANCE_ATLAS_NOT_VALID)\
-    _SSPINE_XMACRO(SPINE_SKELETON_CREATION_FAILED)\
-    _SSPINE_XMACRO(SPINE_ANIMATIONSTATE_CREATION_FAILED)\
-    _SSPINE_XMACRO(SPINE_SKELETONCLIPPING_CREATION_FAILED)\
-    _SSPINE_XMACRO(COMMAND_BUFFER_OVERFLOW)\
-    _SSPINE_XMACRO(VERTEX_BUFFER_OVERFLOW)\
-    _SSPINE_XMACRO(INDEX_BUFFER_OVERFLOW)\
-    _SSPINE_XMACRO(STRING_TRUNCATED)\
-    _SSPINE_XMACRO(SG_ADD_COMMIT_LISTENER_FAILED)\
-
-#define _SSPINE_XMACRO(code) SSPINE_ERROR_##code,
-typedef enum sspine_error {
-    _SSPINE_ERRORS
-} sspine_error;
-#undef _SSPINE_XMACRO
-
-typedef enum sspine_loglevel {
-    SSPINE_LOGLEVEL_PANIC = 0,
-    SSPINE_LOGLEVEL_ERROR = 1,
-    SSPINE_LOGLEVEL_WARN = 2,
-    SSPINE_LOGLEVEL_INFO = 3,
-} sspine_loglevel;
+// log item codes via x-macro magic
+#define _SSPINE_LOG_ITEMS \
+    _SSPINE_LOGITEM_XMACRO(OK, "Ok")\
+    _SSPINE_LOGITEM_XMACRO(MALLOC_FAILED, "memory allocation failed")\
+    _SSPINE_LOGITEM_XMACRO(CONTEXT_POOL_EXHAUSTED, "context pool exhausted (adjust via sspine_desc.context_pool_size)")\
+    _SSPINE_LOGITEM_XMACRO(ATLAS_POOL_EXHAUSTED, "atlas pool exhausted (adjust via sspine_desc.atlas_pool_size)")\
+    _SSPINE_LOGITEM_XMACRO(SKELETON_POOL_EXHAUSTED, "skeleton pool exhausted (adjust via sspine_desc.skeleton_pool_size)")\
+    _SSPINE_LOGITEM_XMACRO(SKINSET_POOL_EXHAUSTED, "skinset pool exhausted (adjust via sspine_desc.skinset_pool_size)")\
+    _SSPINE_LOGITEM_XMACRO(INSTANCE_POOL_EXHAUSTED, "instance pool exhausted (adjust via sspine_desc.instance_pool_size)")\
+    _SSPINE_LOGITEM_XMACRO(CANNOT_DESTROY_DEFAULT_CONTEXT, "cannot destroy default context")\
+    _SSPINE_LOGITEM_XMACRO(ATLAS_DESC_NO_DATA, "no data provided in sspine_atlas_desc.data")\
+    _SSPINE_LOGITEM_XMACRO(SPINE_ATLAS_CREATION_FAILED, "spAtlas_create() failed")\
+    _SSPINE_LOGITEM_XMACRO(SG_ALLOC_IMAGE_FAILED, "sg_alloc_image() failed")\
+    _SSPINE_LOGITEM_XMACRO(SKELETON_DESC_NO_DATA, "no data provided in sspine_skeleton_desc.json_data or .binary_data")\
+    _SSPINE_LOGITEM_XMACRO(SKELETON_DESC_NO_ATLAS, "no atlas object provided in sspine_skeleton_desc.atlas")\
+    _SSPINE_LOGITEM_XMACRO(SKELETON_ATLAS_NOT_VALID, "sspine_skeleton_desc.atlas is not in valid state")\
+    _SSPINE_LOGITEM_XMACRO(CREATE_SKELETON_DATA_FROM_JSON_FAILED, "spSkeletonJson_readSkeletonData() failed")\
+    _SSPINE_LOGITEM_XMACRO(CREATE_SKELETON_DATA_FROM_BINARY_FAILED, "spSkeletonBinary_readSkeletonData() failed")\
+    _SSPINE_LOGITEM_XMACRO(SKINSET_DESC_NO_SKELETON, "no skeleton object provided in sspine_skinset_desc.skeleton")\
+    _SSPINE_LOGITEM_XMACRO(SKINSET_SKELETON_NOT_VALID, "sspine_skinset_desc.skeleton is not in valid state")\
+    _SSPINE_LOGITEM_XMACRO(SKINSET_INVALID_SKIN_HANDLE, "invalid skin handle in sspine_skinset_desc.skins[]")\
+    _SSPINE_LOGITEM_XMACRO(INSTANCE_DESC_NO_SKELETON, "no skeleton object provided in sspine_instance_desc.skeleton")\
+    _SSPINE_LOGITEM_XMACRO(INSTANCE_SKELETON_NOT_VALID, "sspine_instance_desc.skeleton is not in valid state")\
+    _SSPINE_LOGITEM_XMACRO(INSTANCE_ATLAS_NOT_VALID, "skeleton's atlas object no longer valid via sspine_instance_desc.skeleton")\
+    _SSPINE_LOGITEM_XMACRO(SPINE_SKELETON_CREATION_FAILED, "spSkeleton_create() failed")\
+    _SSPINE_LOGITEM_XMACRO(SPINE_ANIMATIONSTATE_CREATION_FAILED, "spAnimationState_create() failed")\
+    _SSPINE_LOGITEM_XMACRO(SPINE_SKELETONCLIPPING_CREATION_FAILED, "spSkeletonClipping_create() failed")\
+    _SSPINE_LOGITEM_XMACRO(COMMAND_BUFFER_FULL, "command buffer full (adjust via sspine_desc.max_commands)")\
+    _SSPINE_LOGITEM_XMACRO(VERTEX_BUFFER_FULL, "vertex buffer (adjust via sspine_desc.max_vertices)")\
+    _SSPINE_LOGITEM_XMACRO(INDEX_BUFFER_FULL, "index buffer full (adjust via sspine_desc.max_vertices)")\
+    _SSPINE_LOGITEM_XMACRO(STRING_TRUNCATED, "a string has been truncated")\
+    _SSPINE_LOGITEM_XMACRO(ADD_COMMIT_LISTENER_FAILED, "sg_add_commit_listener() failed")\
+
+#define _SSPINE_LOGITEM_XMACRO(item,msg) SSPINE_LOGITEM_##item,
+typedef enum sspine_log_item {
+    _SSPINE_LOG_ITEMS
+} sspine_log_item;
+#undef _SSPINE_LOGITEM_XMACRO
 
 typedef struct sspine_layer_transform {
     sspine_vec2 size;
@@ -1187,12 +1196,12 @@ typedef struct sspine_allocator {
 
 typedef struct sspine_logger {
     void (*func)(
-        const char* tag,      // always "sspine"
-        uint32_t log_level,   // 0=panic, 1=error, 2=warning, 3=info
-        uint32_t error_code,  // SSPINE_ERROR_*
-        const char* error_id, // error as string, debug only, otherwise empty string
-        int line_nr,          // line number in sokol_spine.h
-        const char* filename, // debug mode only, otherwise empty string
+        const char* tag,                // always "sspine"
+        uint32_t log_level,             // 0=panic, 1=error, 2=warning, 3=info
+        uint32_t log_item_id,           // SSPINE_LOGITEM_*
+        const char* message_or_null,    // a message string, may be nullptr in release mode
+        uint32_t line_nr,               // line number in sokol_spine.h
+        const char* filename_or_null,   // the source filename, may be nullptr in release mode
         void* user_data);
     void* user_data;
 } sspine_logger;
@@ -1209,8 +1218,8 @@ typedef struct sspine_desc {
     sg_pixel_format depth_format;
     int sample_count;
     sg_color_mask color_write_mask;
-    sspine_allocator allocator;
-    sspine_logger logger;
+    sspine_allocator allocator;     // optional allocation override functions (default: malloc/free)
+    sspine_logger logger;           // optional logging function (default: NO LOGGING!)
 } sspine_desc;
 
 // setup/shutdown
@@ -1371,7 +1380,13 @@ SOKOL_SPINE_API_DECL void sspine_set_skin(sspine_instance instance, sspine_skin
 } // extern "C"
 #endif
 
-//-- IMPLEMENTATION ------------------------------------------------------------
+// ██ ███    ███ ██████  ██      ███████ ███    ███ ███████ ███    ██ ████████  █████  ████████ ██  ██████  ███    ██
+// ██ ████  ████ ██   ██ ██      ██      ████  ████ ██      ████   ██    ██    ██   ██    ██    ██ ██    ██ ████   ██
+// ██ ██ ████ ██ ██████  ██      █████   ██ ████ ██ █████   ██ ██  ██    ██    ███████    ██    ██ ██    ██ ██ ██  ██
+// ██ ██  ██  ██ ██      ██      ██      ██  ██  ██ ██      ██  ██ ██    ██    ██   ██    ██    ██ ██    ██ ██  ██ ██
+// ██ ██      ██ ██      ███████ ███████ ██      ██ ███████ ██   ████    ██    ██   ██    ██    ██  ██████  ██   ████
+//
+// >>implementation
 #ifdef SOKOL_SPINE_IMPL
 #define SOKOL_SPINE_IMPL_INCLUDED (1)
 
@@ -1400,8 +1415,15 @@ SOKOL_SPINE_API_DECL void sspine_set_skin(sspine_instance instance, sspine_skin
 
 #include <stdlib.h> // malloc/free
 #include <string.h> // memset, strcmp
-#include <stdio.h>  // stderr, fprintf (debug mode), fputs (release mode)
 
+// ███████╗██╗  ██╗ █████╗ ██████╗ ███████╗██████╗ ███████╗
+// ██╔════╝██║  ██║██╔══██╗██╔══██╗██╔════╝██╔══██╗██╔════╝
+// ███████╗███████║███████║██║  ██║█████╗  ██████╔╝███████╗
+// ╚════██║██╔══██║██╔══██║██║  ██║██╔══╝  ██╔══██╗╚════██║
+// ███████║██║  ██║██║  ██║██████╔╝███████╗██║  ██║███████║
+// ╚══════╝╚═╝  ╚═╝╚═╝  ╚═╝╚═════╝ ╚══════╝╚═╝  ╚═╝╚══════╝
+//
+// >>shaders
 /*
     Embedded source compiled with:
 
@@ -2544,7 +2566,7 @@ static const char _sspine_fs_source_metal_sim[721] = {
     0x00,
 };
 #elif defined(SOKOL_WGPU)
-FIXME
+#error "FIXME: wgpu shaders"
 #elif defined(SOKOL_DUMMY_BACKEND)
 static const char* _sspine_vs_source_dummy = "";
 static const char* _sspine_fs_source_dummy = "";
@@ -2552,6 +2574,14 @@ static const char* _sspine_fs_source_dummy = "";
 #error "Please define one of SOKOL_GLCORE33, SOKOL_GLES2, SOKOL_GLES3, SOKOL_D3D11, SOKOL_METAL, SOKOL_WGPU or SOKOL_DUMMY_BACKEND!"
 #endif
 
+// ███████ ████████ ██████  ██    ██  ██████ ████████ ███████
+// ██         ██    ██   ██ ██    ██ ██         ██    ██
+// ███████    ██    ██████  ██    ██ ██         ██    ███████
+//      ██    ██    ██   ██ ██    ██ ██         ██         ██
+// ███████    ██    ██   ██  ██████   ██████    ██    ███████
+//
+// >>structs
+
 #define _sspine_def(val, def) (((val) == 0) ? (def) : (val))
 #define _SSPINE_INIT_COOKIE (0xABBAABBA)
 #define _SSPINE_INVALID_SLOT_INDEX (0)
@@ -2567,14 +2597,6 @@ static const char* _sspine_fs_source_dummy = "";
 #define _SSPINE_MAX_POOL_SIZE (1<<_SSPINE_SLOT_SHIFT)
 #define _SSPINE_SLOT_MASK (_SSPINE_MAX_POOL_SIZE-1)
 
-#if defined(SOKOL_DEBUG)
-#define _SSPINE_XMACRO(code) #code,
-static const char* _sspine_error_ids[] = {
-    _SSPINE_ERRORS
-};
-#undef _SSPINE_XMACRO
-#endif // SOKOL_DEBUG
-
 typedef struct {
     float mvp[16];
 } _sspine_vsparams_t;
@@ -2769,54 +2791,52 @@ char* _spUtil_readFile(const char* path, int* length) {
 } // extern "C"
 #endif
 
-//=== HELPER FUNCTION ==========================================================
-#define _SSPINE_PANIC(code) _sspine_log(SSPINE_ERROR_ ##code, SSPINE_LOGLEVEL_PANIC, __LINE__)
-#define _SSPINE_ERROR(code) _sspine_log(SSPINE_ERROR_ ##code, SSPINE_LOGLEVEL_ERROR, __LINE__)
-#define _SSPINE_WARN(code) _sspine_log(SSPINE_ERROR_ ##code, SSPINE_LOGLEVEL_WARN, __LINE__)
-#define _SSPINE_INFO(code) _sspine_log(SSPINE_ERROR_ ##code, SSPINE_LOGLEVEL_INFO, __LINE__)
+// ██       ██████   ██████   ██████  ██ ███    ██  ██████
+// ██      ██    ██ ██       ██       ██ ████   ██ ██
+// ██      ██    ██ ██   ███ ██   ███ ██ ██ ██  ██ ██   ███
+// ██      ██    ██ ██    ██ ██    ██ ██ ██  ██ ██ ██    ██
+// ███████  ██████   ██████   ██████  ██ ██   ████  ██████
+//
+// >>logging
+#if defined(SOKOL_DEBUG)
+#define _SSPINE_LOGITEM_XMACRO(item,msg) #item ": " msg,
+static const char* _sspine_log_messages[] = {
+    _SSPINE_LOG_ITEMS
+};
+#undef _SSPINE_LOGITEM_XMACRO
+#endif // SOKOL_DEBUG
+
+#define _SSPINE_PANIC(code) _sspine_log(SSPINE_LOGITEM_ ##code, 0, __LINE__)
+#define _SSPINE_ERROR(code) _sspine_log(SSPINE_LOGITEM_ ##code, 1, __LINE__)
+#define _SSPINE_WARN(code) _sspine_log(SSPINE_LOGITEM_ ##code, 2, __LINE__)
+#define _SSPINE_INFO(code) _sspine_log(SSPINE_LOGITEM_ ##code, 3, __LINE__)
 
-static void _sspine_log(sspine_error error_code, sspine_loglevel log_level, int line_nr) {
+static void _sspine_log(sspine_log_item log_item, uint32_t log_level, uint32_t line_nr) {
     if (_sspine.desc.logger.func) {
         #if defined(SOKOL_DEBUG)
             const char* filename = __FILE__;
-            const char* error_id = _sspine_error_ids[error_code];
+            const char* message = _sspine_log_messages[log_item];
         #else
-            const char* filename = "";
-            const char* error_id = "";
+            const char* filename = 0;
+            const char* message = 0;
         #endif
-        _sspine.desc.logger.func("sspine", log_level, error_code, error_id, line_nr, filename, _sspine.desc.logger.user_data);
+        _sspine.desc.logger.func("sspine", log_level, log_item, message, line_nr, filename, _sspine.desc.logger.user_data);
     }
     else {
-        // default logging function, uses printf only if debugging is enabled to save executable size
-        const char* loglevel_str;
-        switch (log_level) {
-            case SSPINE_LOGLEVEL_PANIC: loglevel_str = "panic"; break;
-            case SSPINE_LOGLEVEL_ERROR: loglevel_str = "error"; break;
-            case SSPINE_LOGLEVEL_WARN:  loglevel_str = "warning"; break;
-            default: loglevel_str = "info"; break;
-        }
-        #if defined(SOKOL_DEBUG)
-        const char* error_id = _sspine_error_ids[error_code];
-        #if defined(_MSC_VER)
-            // Visual Studio compiler error format
-            fprintf(stderr, "[sspine] %s(%d): %s: %s\n", __FILE__, line_nr, loglevel_str, error_id);
-        #else
-            // GCC error format
-            fprintf(stderr, "[sspine] %s:%d:0: %s: %s\n", __FILE__, line_nr, loglevel_str, error_id);
-        #endif
-        #else
-            fputs("[sspine] ", stderr);
-            fputs(loglevel_str, stderr);
-            fputs(" (build in debug mode for more info)\n", stderr);
-        #endif // SOKOL_DEBUG
-
         // for log level PANIC it would be 'undefined behaviour' to continue
-        if (log_level == SSPINE_LOGLEVEL_PANIC) {
+        if (log_level == 0) {
             abort();
         }
     }
 }
 
+// ███    ███ ███████ ███    ███  ██████  ██████  ██    ██
+// ████  ████ ██      ████  ████ ██    ██ ██   ██  ██  ██
+// ██ ████ ██ █████   ██ ████ ██ ██    ██ ██████    ████
+// ██  ██  ██ ██      ██  ██  ██ ██    ██ ██   ██    ██
+// ██      ██ ███████ ██      ██  ██████  ██   ██    ██
+//
+// >>memory
 static void _sspine_clear(void* ptr, size_t size) {
     SOKOL_ASSERT(ptr && (size > 0));
     memset(ptr, 0, size);
@@ -2872,7 +2892,9 @@ static void* _sspine_malloc(size_t size) {
     else {
         ptr = malloc(size);
     }
-    SOKOL_ASSERT(ptr);
+    if (0 == ptr) {
+        _SSPINE_PANIC(MALLOC_FAILED);
+    }
     return ptr;
 }
 
@@ -2891,6 +2913,13 @@ static void _sspine_free(void* ptr) {
     }
 }
 
+// ██████  ███████ ███████ ███████
+// ██   ██ ██      ██      ██
+// ██████  █████   █████   ███████
+// ██   ██ ██      ██           ██
+// ██   ██ ███████ ██      ███████
+//
+// >>refs
 static bool _sspine_atlas_ref_valid(const _sspine_atlas_ref_t* ref) {
     return ref->ptr && (ref->ptr->slot.id == ref->id);
 }
@@ -2958,7 +2987,13 @@ static sspine_skin _sspine_skin(uint32_t skeleton_id, int index) {
     return skin;
 }
 
-//=== HANDLE POOL FUNCTIONS ====================================================
+// ██████   ██████   ██████  ██
+// ██   ██ ██    ██ ██    ██ ██
+// ██████  ██    ██ ██    ██ ██
+// ██      ██    ██ ██    ██ ██
+// ██       ██████   ██████  ███████
+//
+// >>pool
 static void _sspine_init_pool(_sspine_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
@@ -3062,7 +3097,13 @@ static void _sspine_discard_item_pool(_sspine_pool_t* pool, void** items_ptr) {
     _sspine_discard_pool(pool);
 }
 
-//== CONTEXT POOL FUNCTIONS ====================================================
+//  ██████  ██████  ███    ██ ████████ ███████ ██   ██ ████████
+// ██      ██    ██ ████   ██    ██    ██       ██ ██     ██
+// ██      ██    ██ ██ ██  ██    ██    █████     ███      ██
+// ██      ██    ██ ██  ██ ██    ██    ██       ██ ██     ██
+//  ██████  ██████  ██   ████    ██    ███████ ██   ██    ██
+//
+// >>context
 static void _sspine_setup_context_pool(int pool_size) {
     _sspine_context_pool_t* p = &_sspine.context_pool;
     _sspine_init_item_pool(&p->pool, pool_size, (void**)&p->items, sizeof(_sspine_context_t));
@@ -3228,7 +3269,13 @@ static bool _sspine_is_default_context(sspine_context ctx_id) {
     return ctx_id.id == 0x00010001;
 }
 
-//=== ATLAS POOL FUNCTIONS =====================================================
+//  █████  ████████ ██       █████  ███████
+// ██   ██    ██    ██      ██   ██ ██
+// ███████    ██    ██      ███████ ███████
+// ██   ██    ██    ██      ██   ██      ██
+// ██   ██    ██    ███████ ██   ██ ███████
+//
+// >>atlas
 static void _sspine_setup_atlas_pool(int pool_size) {
     _sspine_atlas_pool_t* p = &_sspine.atlas_pool;
     _sspine_init_item_pool(&p->pool, pool_size, (void**)&p->items, sizeof(_sspine_atlas_t));
@@ -3360,7 +3407,13 @@ static spAtlasPage* _sspine_lookup_atlas_page(uint32_t atlas_id, int page_index)
     return 0;
 }
 
-//=== SKELETON POOL FUNCTIONS ==================================================
+// ███████ ██   ██ ███████ ██      ███████ ████████  ██████  ███    ██
+// ██      ██  ██  ██      ██      ██         ██    ██    ██ ████   ██
+// ███████ █████   █████   ██      █████      ██    ██    ██ ██ ██  ██
+//      ██ ██  ██  ██      ██      ██         ██    ██    ██ ██  ██ ██
+// ███████ ██   ██ ███████ ███████ ███████    ██     ██████  ██   ████
+//
+// >>skeleton
 static void _sspine_setup_skeleton_pool(int pool_size) {
     _sspine_skeleton_pool_t* p = &_sspine.skeleton_pool;
     _sspine_init_item_pool(&p->pool, pool_size, (void**)&p->items, sizeof(_sspine_skeleton_t));
@@ -3440,7 +3493,7 @@ static sspine_resource_state _sspine_init_skeleton(_sspine_skeleton_t* skeleton,
         skeleton->sp_skel_data = spSkeletonJson_readSkeletonData(skel_json, desc->json_data);
         spSkeletonJson_dispose(skel_json); skel_json = 0;
         if (0 == skeleton->sp_skel_data) {
-            _SSPINE_ERROR(SPINE_SKELETON_DATA_CREATION_FAILED);
+            _SSPINE_ERROR(CREATE_SKELETON_DATA_FROM_JSON_FAILED);
             return SSPINE_RESOURCESTATE_FAILED;
         }
     }
@@ -3451,7 +3504,7 @@ static sspine_resource_state _sspine_init_skeleton(_sspine_skeleton_t* skeleton,
         skeleton->sp_skel_data = spSkeletonBinary_readSkeletonData(skel_bin, (const unsigned char*)desc->binary_data.ptr, (int)desc->binary_data.size);
         spSkeletonBinary_dispose(skel_bin); skel_bin = 0;
         if (0 == skeleton->sp_skel_data) {
-            _SSPINE_ERROR(SPINE_SKELETON_DATA_CREATION_FAILED);
+            _SSPINE_ERROR(CREATE_SKELETON_DATA_FROM_BINARY_FAILED);
             return SSPINE_RESOURCESTATE_FAILED;
         }
     }
@@ -3584,7 +3637,13 @@ static spSkin* _sspine_lookup_skin(uint32_t skeleton_id, int skin_index) {
     return 0;
 }
 
-//=== SKINSET POOL FUNCTIONS ===================================================
+// ███████ ██   ██ ██ ███    ██ ███████ ███████ ████████
+// ██      ██  ██  ██ ████   ██ ██      ██         ██
+// ███████ █████   ██ ██ ██  ██ ███████ █████      ██
+//      ██ ██  ██  ██ ██  ██ ██      ██ ██         ██
+// ███████ ██   ██ ██ ██   ████ ███████ ███████    ██
+//
+// >>skinset
 static void _sspine_setup_skinset_pool(int pool_size) {
     _sspine_skinset_pool_t* p = &_sspine.skinset_pool;
     _sspine_init_item_pool(&p->pool, pool_size, (void**)&p->items, sizeof(_sspine_skinset_t));
@@ -3696,7 +3755,13 @@ static sspine_skinset_desc _sspine_skinset_desc_defaults(const sspine_skinset_de
     return res;
 }
 
-//=== INSTANCE POOL FUNCTIONS ==================================================
+// ██ ███    ██ ███████ ████████  █████  ███    ██  ██████ ███████
+// ██ ████   ██ ██         ██    ██   ██ ████   ██ ██      ██
+// ██ ██ ██  ██ ███████    ██    ███████ ██ ██  ██ ██      █████
+// ██ ██  ██ ██      ██    ██    ██   ██ ██  ██ ██ ██      ██
+// ██ ██   ████ ███████    ██    ██   ██ ██   ████  ██████ ███████
+//
+// >>instance
 static void _sspine_setup_instance_pool(int pool_size) {
     _sspine_instance_pool_t* p = &_sspine.instance_pool;
     _sspine_init_item_pool(&p->pool, pool_size, (void**)&p->items, sizeof(_sspine_instance_t));
@@ -3944,95 +4009,13 @@ static sspine_instance_desc _sspine_instance_desc_defaults(const sspine_instance
     return res;
 }
 
-// return sspine_desc with patched defaults
-static sspine_desc _sspine_desc_defaults(const sspine_desc* desc) {
-    SOKOL_ASSERT((desc->allocator.alloc && desc->allocator.free) || (!desc->allocator.alloc && !desc->allocator.free));
-    sspine_desc res = *desc;
-    res.max_vertices = _sspine_def(desc->max_vertices, _SSPINE_DEFAULT_MAX_VERTICES);
-    res.max_commands = _sspine_def(desc->max_commands, _SSPINE_DEFAULT_MAX_COMMANDS);
-    res.context_pool_size = _sspine_def(desc->context_pool_size, _SSPINE_DEFAULT_CONTEXT_POOL_SIZE);
-    res.atlas_pool_size = _sspine_def(desc->atlas_pool_size, _SSPINE_DEFAULT_ATLAS_POOL_SIZE);
-    res.skeleton_pool_size = _sspine_def(desc->skeleton_pool_size, _SSPINE_DEFAULT_SKELETON_POOL_SIZE);
-    res.skinset_pool_size = _sspine_def(desc->skinset_pool_size, _SSPINE_DEFAULT_SKINSET_POOL_SIZE);
-    res.instance_pool_size = _sspine_def(desc->instance_pool_size, _SSPINE_DEFAULT_INSTANCE_POOL_SIZE);
-    return res;
-}
-
-static sspine_context_desc _sspine_as_context_desc(const sspine_desc* desc) {
-    sspine_context_desc ctx_desc;
-    _sspine_clear(&ctx_desc, sizeof(ctx_desc));
-    ctx_desc.max_vertices = desc->max_vertices;
-    ctx_desc.max_commands = desc->max_commands;
-    ctx_desc.color_format = desc->color_format;
-    ctx_desc.depth_format = desc->depth_format;
-    ctx_desc.sample_count = desc->sample_count;
-    ctx_desc.color_write_mask = desc->color_write_mask;
-    return ctx_desc;
-}
-
-static sg_filter _sspine_as_image_filter(spAtlasFilter filter) {
-    switch (filter) {
-        case SP_ATLAS_UNKNOWN_FILTER: return _SG_FILTER_DEFAULT;
-        case SP_ATLAS_NEAREST: return SG_FILTER_NEAREST;
-        case SP_ATLAS_LINEAR: return SG_FILTER_LINEAR;
-        case SP_ATLAS_MIPMAP: return SG_FILTER_LINEAR_MIPMAP_NEAREST;
-        case SP_ATLAS_MIPMAP_NEAREST_NEAREST: return SG_FILTER_NEAREST_MIPMAP_NEAREST;
-        case SP_ATLAS_MIPMAP_LINEAR_NEAREST: return SG_FILTER_LINEAR_MIPMAP_NEAREST;
-        case SP_ATLAS_MIPMAP_NEAREST_LINEAR: return SG_FILTER_NEAREST_MIPMAP_LINEAR;
-        case SP_ATLAS_MIPMAP_LINEAR_LINEAR: return SG_FILTER_LINEAR_MIPMAP_LINEAR;
-        default: return _SG_FILTER_DEFAULT;
-    }
-}
-
-static sg_wrap _sspine_as_image_wrap(spAtlasWrap wrap) {
-    switch (wrap) {
-        case SP_ATLAS_MIRROREDREPEAT: return SG_WRAP_MIRRORED_REPEAT;
-        case SP_ATLAS_CLAMPTOEDGE: return SG_WRAP_CLAMP_TO_EDGE;
-        case SP_ATLAS_REPEAT: return SG_WRAP_REPEAT;
-        default: return _SG_WRAP_DEFAULT;
-    }
-}
-
-static void _sspine_init_image_info(const _sspine_atlas_t* atlas, int index, sspine_image_info* info, bool with_overrides) {
-    spAtlasPage* page = _sspine_lookup_atlas_page(atlas->slot.id, index);
-    SOKOL_ASSERT(page);
-    SOKOL_ASSERT(page->name);
-    info->valid = true;
-    info->sgimage.id = (uint32_t)(uintptr_t)page->rendererObject;
-    if (with_overrides && (atlas->overrides.min_filter != _SG_FILTER_DEFAULT)) {
-        info->min_filter = atlas->overrides.min_filter;
-    }
-    else {
-        info->min_filter = _sspine_as_image_filter(page->minFilter);
-    }
-    if (with_overrides && (atlas->overrides.mag_filter != _SG_FILTER_DEFAULT)) {
-        info->mag_filter = atlas->overrides.mag_filter;
-    }
-    else {
-        info->mag_filter = _sspine_as_image_filter(page->magFilter);
-    }
-    if (with_overrides && (atlas->overrides.wrap_u != _SG_WRAP_DEFAULT)) {
-        info->wrap_u = atlas->overrides.wrap_u;
-    }
-    else {
-        info->wrap_u = _sspine_as_image_wrap(page->uWrap);
-    }
-    if (with_overrides && (atlas->overrides.wrap_v != _SG_WRAP_DEFAULT)) {
-        info->wrap_v = atlas->overrides.wrap_v;
-    }
-    else {
-        info->wrap_v = _sspine_as_image_wrap(page->vWrap);
-    }
-    info->width = page->width;
-    info->height = page->height;
-    // NOTE: override already happened in atlas init
-    info->premul_alpha = page->pma != 0;
-    info->filename = _sspine_string(page->name);
-    if (info->filename.truncated) {
-        _SSPINE_WARN(STRING_TRUNCATED);
-    }
-}
-
+// ██████  ██████   █████  ██     ██
+// ██   ██ ██   ██ ██   ██ ██     ██
+// ██   ██ ██████  ███████ ██  █  ██
+// ██   ██ ██   ██ ██   ██ ██ ███ ██
+// ██████  ██   ██ ██   ██  ███ ███
+//
+// >>draw
 static void _sspine_check_rewind_commands(_sspine_context_t* ctx) {
     if (_sspine.frame_id != ctx->commands.rewind_frame_id) {
         ctx->commands.next = 0;
@@ -4046,7 +4029,7 @@ static _sspine_command_t* _sspine_next_command(_sspine_context_t* ctx) {
         return &(ctx->commands.ptr[ctx->commands.next++]);
     }
     else {
-        _SSPINE_ERROR(COMMAND_BUFFER_OVERFLOW);
+        _SSPINE_ERROR(COMMAND_BUFFER_FULL);
         return 0;
     }
 }
@@ -4078,7 +4061,7 @@ static _sspine_alloc_vertices_result_t _sspine_alloc_vertices(_sspine_context_t*
         ctx->vertices.next += num;
     }
     else {
-        _SSPINE_ERROR(VERTEX_BUFFER_OVERFLOW);
+        _SSPINE_ERROR(VERTEX_BUFFER_FULL);
     }
     return res;
 }
@@ -4100,7 +4083,7 @@ static _sspine_alloc_indices_result_t _sspine_alloc_indices(_sspine_context_t* c
         ctx->indices.next += num;
     }
     else {
-        _SSPINE_ERROR(INDEX_BUFFER_OVERFLOW);
+        _SSPINE_ERROR(INDEX_BUFFER_FULL);
     }
     return res;
 }
@@ -4359,6 +4342,103 @@ static void _sspine_draw_layer(_sspine_context_t* ctx, int layer, const sspine_l
     }
 }
 
+// ███    ███ ██ ███████  ██████
+// ████  ████ ██ ██      ██
+// ██ ████ ██ ██ ███████ ██
+// ██  ██  ██ ██      ██ ██
+// ██      ██ ██ ███████  ██████
+//
+// >>misc
+
+// return sspine_desc with patched defaults
+static sspine_desc _sspine_desc_defaults(const sspine_desc* desc) {
+    SOKOL_ASSERT((desc->allocator.alloc && desc->allocator.free) || (!desc->allocator.alloc && !desc->allocator.free));
+    sspine_desc res = *desc;
+    res.max_vertices = _sspine_def(desc->max_vertices, _SSPINE_DEFAULT_MAX_VERTICES);
+    res.max_commands = _sspine_def(desc->max_commands, _SSPINE_DEFAULT_MAX_COMMANDS);
+    res.context_pool_size = _sspine_def(desc->context_pool_size, _SSPINE_DEFAULT_CONTEXT_POOL_SIZE);
+    res.atlas_pool_size = _sspine_def(desc->atlas_pool_size, _SSPINE_DEFAULT_ATLAS_POOL_SIZE);
+    res.skeleton_pool_size = _sspine_def(desc->skeleton_pool_size, _SSPINE_DEFAULT_SKELETON_POOL_SIZE);
+    res.skinset_pool_size = _sspine_def(desc->skinset_pool_size, _SSPINE_DEFAULT_SKINSET_POOL_SIZE);
+    res.instance_pool_size = _sspine_def(desc->instance_pool_size, _SSPINE_DEFAULT_INSTANCE_POOL_SIZE);
+    return res;
+}
+
+static sspine_context_desc _sspine_as_context_desc(const sspine_desc* desc) {
+    sspine_context_desc ctx_desc;
+    _sspine_clear(&ctx_desc, sizeof(ctx_desc));
+    ctx_desc.max_vertices = desc->max_vertices;
+    ctx_desc.max_commands = desc->max_commands;
+    ctx_desc.color_format = desc->color_format;
+    ctx_desc.depth_format = desc->depth_format;
+    ctx_desc.sample_count = desc->sample_count;
+    ctx_desc.color_write_mask = desc->color_write_mask;
+    return ctx_desc;
+}
+
+static sg_filter _sspine_as_image_filter(spAtlasFilter filter) {
+    switch (filter) {
+        case SP_ATLAS_UNKNOWN_FILTER: return _SG_FILTER_DEFAULT;
+        case SP_ATLAS_NEAREST: return SG_FILTER_NEAREST;
+        case SP_ATLAS_LINEAR: return SG_FILTER_LINEAR;
+        case SP_ATLAS_MIPMAP: return SG_FILTER_LINEAR_MIPMAP_NEAREST;
+        case SP_ATLAS_MIPMAP_NEAREST_NEAREST: return SG_FILTER_NEAREST_MIPMAP_NEAREST;
+        case SP_ATLAS_MIPMAP_LINEAR_NEAREST: return SG_FILTER_LINEAR_MIPMAP_NEAREST;
+        case SP_ATLAS_MIPMAP_NEAREST_LINEAR: return SG_FILTER_NEAREST_MIPMAP_LINEAR;
+        case SP_ATLAS_MIPMAP_LINEAR_LINEAR: return SG_FILTER_LINEAR_MIPMAP_LINEAR;
+        default: return _SG_FILTER_DEFAULT;
+    }
+}
+
+static sg_wrap _sspine_as_image_wrap(spAtlasWrap wrap) {
+    switch (wrap) {
+        case SP_ATLAS_MIRROREDREPEAT: return SG_WRAP_MIRRORED_REPEAT;
+        case SP_ATLAS_CLAMPTOEDGE: return SG_WRAP_CLAMP_TO_EDGE;
+        case SP_ATLAS_REPEAT: return SG_WRAP_REPEAT;
+        default: return _SG_WRAP_DEFAULT;
+    }
+}
+
+static void _sspine_init_image_info(const _sspine_atlas_t* atlas, int index, sspine_image_info* info, bool with_overrides) {
+    spAtlasPage* page = _sspine_lookup_atlas_page(atlas->slot.id, index);
+    SOKOL_ASSERT(page);
+    SOKOL_ASSERT(page->name);
+    info->valid = true;
+    info->sgimage.id = (uint32_t)(uintptr_t)page->rendererObject;
+    if (with_overrides && (atlas->overrides.min_filter != _SG_FILTER_DEFAULT)) {
+        info->min_filter = atlas->overrides.min_filter;
+    }
+    else {
+        info->min_filter = _sspine_as_image_filter(page->minFilter);
+    }
+    if (with_overrides && (atlas->overrides.mag_filter != _SG_FILTER_DEFAULT)) {
+        info->mag_filter = atlas->overrides.mag_filter;
+    }
+    else {
+        info->mag_filter = _sspine_as_image_filter(page->magFilter);
+    }
+    if (with_overrides && (atlas->overrides.wrap_u != _SG_WRAP_DEFAULT)) {
+        info->wrap_u = atlas->overrides.wrap_u;
+    }
+    else {
+        info->wrap_u = _sspine_as_image_wrap(page->uWrap);
+    }
+    if (with_overrides && (atlas->overrides.wrap_v != _SG_WRAP_DEFAULT)) {
+        info->wrap_v = atlas->overrides.wrap_v;
+    }
+    else {
+        info->wrap_v = _sspine_as_image_wrap(page->vWrap);
+    }
+    info->width = page->width;
+    info->height = page->height;
+    // NOTE: override already happened in atlas init
+    info->premul_alpha = page->pma != 0;
+    info->filename = _sspine_string(page->name);
+    if (info->filename.truncated) {
+        _SSPINE_WARN(STRING_TRUNCATED);
+    }
+}
+
 static void _sspine_init_shared(void) {
     sg_shader_desc shd_desc;
     _sspine_clear(&shd_desc, sizeof(shd_desc));
@@ -4437,8 +4517,13 @@ static sg_commit_listener _sspine_make_commit_listener(void) {
     return commit_listener;
 }
 
-//== PUBLIC FUNCTIONS ==========================================================
-
+// ██████  ██    ██ ██████  ██      ██  ██████
+// ██   ██ ██    ██ ██   ██ ██      ██ ██
+// ██████  ██    ██ ██████  ██      ██ ██
+// ██      ██    ██ ██   ██ ██      ██ ██
+// ██       ██████  ██████  ███████ ██  ██████
+//
+// >>public
 SOKOL_API_IMPL void sspine_setup(const sspine_desc* desc) {
     SOKOL_ASSERT(desc);
     spBone_setYDown(1);
@@ -4459,7 +4544,7 @@ SOKOL_API_IMPL void sspine_setup(const sspine_desc* desc) {
     SOKOL_ASSERT(_sspine_is_default_context(_sspine.def_ctx_id));
     sspine_set_context(_sspine.def_ctx_id);
     if (!sg_add_commit_listener(_sspine_make_commit_listener())) {
-        _SSPINE_ERROR(SG_ADD_COMMIT_LISTENER_FAILED);
+        _SSPINE_ERROR(ADD_COMMIT_LISTENER_FAILED);
     }
 }
 

Some files were not shown because too many files changed in this diff