Browse Source

merge from master

Andre Weissflog 6 tháng trước cách đây
mục cha
commit
ba58d5447d

+ 65 - 0
.github/workflows/gen_bindings.yml

@@ -66,6 +66,10 @@ jobs:
         with:
           repository: colinbellino/sokol-jai
           path: bindgen/sokol-jai
+      - uses: actions/checkout@main
+        with:
+          repository: floooh/sokol-c3
+          path: bindgen/sokol-c3
       - name: generate
         run: |
           cd bindgen
@@ -108,6 +112,12 @@ jobs:
           name: ignore-me-jai
           retention-days: 1
           path: bindgen/sokol-jai/sokol
+      - name: upload-c3-artifact
+        uses: actions/upload-artifact@main
+        with:
+          name: ignore-me-c3
+          retention-days: 1
+          path: bindgen/sokol-c3/sokol.c3l
 
   test-zig:
     needs: gen-bindings
@@ -323,6 +333,40 @@ jobs:
       - name: build
         run: echo "run jai here"
 
+  test-c3:
+    needs: gen-bindings
+    strategy:
+      # Continue build even if some platforms fail.
+      fail-fast: false
+      matrix:
+        include:
+          - os: macos-latest
+            arch: arm64
+          - os: ubuntu-latest
+            arch: x64
+          - os: windows-latest
+            arch: x64
+    runs-on: ${{matrix.os}}
+    steps:
+      - uses: actions/checkout@main
+        with:
+          repository: floooh/sokol-c3
+      - uses: radekm/setup-c3@v2
+        with:
+          version: v0.6.6
+      - uses: actions/download-artifact@main
+        with:
+          name: ignore-me-c3
+          path: sokol.c3l
+      - name: prepare
+        if: runner.os == 'Linux'
+        run: |
+            sudo apt-get update
+            sudo apt-get install libgl1-mesa-dev libegl1-mesa-dev mesa-common-dev xorg-dev libasound-dev
+      - name: build
+        shell: bash
+        run: ./build-examples.sh
+
   # only deploy the bindings for commits on the main branch
   deploy-zig:
     needs: test-zig
@@ -456,3 +500,24 @@ jobs:
           git add -A
           git diff-index --quiet HEAD || git commit -m "updated (https://github.com/floooh/sokol/commit/${{ github.sha }})"
           git push
+
+  deploy-c3:
+    needs: test-c3
+    if: github.ref == 'refs/heads/master'
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@main
+        with:
+          repository: floooh/sokol-c3
+          ssh-key: ${{ secrets.GHACTIONS_C3_PUSH }}
+      - uses: actions/download-artifact@main
+        with:
+          name: ignore-me-c3
+          path: sokol.c3l
+      - name: "commit and push"
+        run: |
+          git config user.email "none"
+          git config user.name "GH Action"
+          git add -A
+          git diff-index --quiet HEAD || git commit -m "updated (https://github.com/floooh/sokol/commit/${{ github.sha }})"
+          git push

+ 24 - 0
CHANGELOG.md

@@ -1,5 +1,29 @@
 ## Updates
 
+### 10-Feb-2025
+
+The [sokol-c3](https://github.com/floooh/sokol-c3) bindings are now 'official'
+and properly integrated (e.g. they are updated automatically on commits to the
+sokol repo, and [sokol-shdc](https://github.com/floooh/sokol-tools) gained a
+C3 output format.
+
+Many thanks to @radekm for kicking this off and doing all the work :)
+
+Related pull request: https://github.com/floooh/sokol/pull/1148
+
+### 09-Feb-2025
+
+- sokol_gfx.h: added the missing blend factors `SG_BLENDFACTOR_MIN` and `SG_BLENDFACTOR_MAX`
+  See issue https://github.com/floooh/sokol/issues/1208 and PR https://github.com/floooh/sokol/pull/1209
+  for details, and the new sample [blend-op-sapp.c](https://floooh.github.io/sokol-html5/blend-op-sapp-ui.html).
+
+  Many thanks to @jdah for bringing up the issue and providing the PR and sample code!
+
+- sokol_gfx.h: removed support for PVRTC compressed pixel formats (the latest iOS SDK started
+  to issue deprecation warnings, and this also removed quite a lot of hacky special-case code from
+  sokol_gfx.h). In the unlikely case that you were still using PVRTC textures, please switch
+  to the ETC2 or ASTC formats instead (associated ticket: https://github.com/floooh/sokol/issues/1206)
+
 ### 25-Jan-2025
 
 Some internal sokol_gfx.h cleanup in `sg_make_shader()`, no behaviour changes

+ 3 - 2
README.md

@@ -6,9 +6,9 @@
 
 # Sokol
 
-[**See what's new**](https://github.com/floooh/sokol/blob/master/CHANGELOG.md) (**23-Jan-2025** sokol_gfx.h gl: fix GL storage buffer bindslot range (may be breaking))
+[**See what's new**](https://github.com/floooh/sokol/blob/master/CHANGELOG.md) (**09-Feb-2025** sokol_gfx.h: remove PVRTC support, add min/max blend-op.
 
-[![Build](/../../actions/workflows/main.yml/badge.svg)](/../../actions/workflows/main.yml) [![Bindings](/../../actions/workflows/gen_bindings.yml/badge.svg)](/../../actions/workflows/gen_bindings.yml) [![build](https://github.com/floooh/sokol-zig/actions/workflows/main.yml/badge.svg)](https://github.com/floooh/sokol-zig/actions/workflows/main.yml) [![build](https://github.com/floooh/sokol-nim/actions/workflows/main.yml/badge.svg)](https://github.com/floooh/sokol-nim/actions/workflows/main.yml) [![Odin](https://github.com/floooh/sokol-odin/actions/workflows/main.yml/badge.svg)](https://github.com/floooh/sokol-odin/actions/workflows/main.yml)[![Rust](https://github.com/floooh/sokol-rust/actions/workflows/main.yml/badge.svg)](https://github.com/floooh/sokol-rust/actions/workflows/main.yml)[![Dlang](https://github.com/kassane/sokol-d/actions/workflows/build.yml/badge.svg)](https://github.com/kassane/sokol-d/actions/workflows/build.yml)
+[![Build](/../../actions/workflows/main.yml/badge.svg)](/../../actions/workflows/main.yml) [![Bindings](/../../actions/workflows/gen_bindings.yml/badge.svg)](/../../actions/workflows/gen_bindings.yml) [![build](https://github.com/floooh/sokol-zig/actions/workflows/main.yml/badge.svg)](https://github.com/floooh/sokol-zig/actions/workflows/main.yml) [![build](https://github.com/floooh/sokol-nim/actions/workflows/main.yml/badge.svg)](https://github.com/floooh/sokol-nim/actions/workflows/main.yml) [![Odin](https://github.com/floooh/sokol-odin/actions/workflows/main.yml/badge.svg)](https://github.com/floooh/sokol-odin/actions/workflows/main.yml)[![Rust](https://github.com/floooh/sokol-rust/actions/workflows/main.yml/badge.svg)](https://github.com/floooh/sokol-rust/actions/workflows/main.yml)[![Dlang](https://github.com/kassane/sokol-d/actions/workflows/build.yml/badge.svg)](https://github.com/kassane/sokol-d/actions/workflows/build.yml)[![C3](https://github.com/floooh/sokol-c3/actions/workflows/build.yml/badge.svg)](https://github.com/floooh/sokol-c3/actions/workflows/build.yml)
 
 ## Examples and Related Projects
 
@@ -84,6 +84,7 @@ These are automatically updated on changes to the C headers:
 - [sokol-rust](https://github.com/floooh/sokol-rust)
 - [sokol-d](https://github.com/kassane/sokol-d)
 - [sokol-jai](https://github.com/colinbellino/sokol-jai)
+- [sokol-c3](https://github.com/floooh/sokol-c3)
 
 ## Notes
 

+ 1 - 0
bindgen/.gitignore

@@ -8,3 +8,4 @@ sokol-odin/
 sokol-rust/
 sokol-d/
 sokol-jai/
+sokol-c3/

+ 1 - 2
bindgen/README.md

@@ -1,7 +1,5 @@
 ## Language Binding Generation Scripts
 
-> REMINDER: we can pass `-fparse-all-comments` to the clang ast-dump command line which adds the following node types to the ast-dump.json: FullComment, ParagraphComment, TextComment. This might allow us to preserve comments in the language bindings (might be useful as part of a bigger change to make sokol header comments autodoc and Intellisense-friendly)
-
 ### Updating the bindings
 
 First make sure that clang and python3 are in the path:
@@ -28,6 +26,7 @@ To update the Zig bindings:
 > git clone https://github.com/floooh/sokol-rust
 > git clone https://github.com/floooh/sokol-d
 > git clone https://github.com/colinbellino/sokol-jai
+> git clone https://github.com/floooh/sokol-c3
 > python3 gen_all.py
 ```
 

+ 7 - 1
bindgen/gen_all.py

@@ -1,4 +1,4 @@
-import os, gen_nim, gen_zig, gen_odin, gen_rust, gen_d, gen_jai
+import os, gen_nim, gen_zig, gen_odin, gen_rust, gen_d, gen_jai, gen_c3
 
 tasks = [
     [ '../sokol_log.h',            'slog_',     [] ],
@@ -58,3 +58,9 @@ gen_rust.prepare()
 for task in tasks:
     [c_header_path, main_prefix, dep_prefixes] = task
     gen_rust.gen(c_header_path, main_prefix, dep_prefixes)
+
+# C3
+gen_c3.prepare()
+for task in tasks:
+    [c_header_path, main_prefix, dep_prefixes] = task
+    gen_c3.gen(c_header_path, main_prefix, dep_prefixes)

+ 445 - 0
bindgen/gen_c3.py

@@ -0,0 +1,445 @@
+#-------------------------------------------------------------------------------
+#   gen_c3.py
+#
+#   Generate C3 bindings.
+#-------------------------------------------------------------------------------
+import gen_ir
+import gen_util as util
+import os, shutil, sys
+
+bindings_root = 'sokol-c3'
+c_root = f'{bindings_root}/sokol.c3l/c'
+module_root = f'{bindings_root}/sokol.c3l'
+
+# TODO: Consider chaning module names to something shorter.
+#       For example we could C prefixes, for example `sg` instead of current `gfx`.
+module_names = {
+    'slog_':    'slog',
+    'sg_':      'sg',
+    'sapp_':    'sapp',
+    'stm_':     'stm',
+    'saudio_':  'saudio',
+    'sgl_':     'sgl',
+    'sdtx_':    'sdtx',
+    'sshape_':  'sshape',
+    'sglue_':   'sglue',
+}
+
+c_source_names = {
+    'slog_':    'sokol_log.c',
+    'sg_':      'sokol_gfx.c',
+    'sapp_':    'sokol_app.c',
+    'sapp_sg':  'sokol_glue.c',
+    'stm_':     'sokol_time.c',
+    'saudio_':  'sokol_audio.c',
+    'sgl_':     'sokol_gl.c',
+    'sdtx_':    'sokol_debugtext.c',
+    'sshape_':  'sokol_shape.c',
+    'sglue_':   'sokol_glue.c',
+}
+
+ignores = [
+    'sdtx_printf',
+    'sdtx_vprintf',
+    'sg_install_trace_hooks',
+    'sg_trace_hooks',
+]
+
+overrides = {
+    # `any` is treated specially in C3.
+    'any': '_any',
+    # Constants must be uppercase - lowercase `x` is not allowed.
+    'SG_PIXELFORMAT_ASTC_4x4_RGBA': 'SG_PIXELFORMAT_ASTC_4X4_RGBA',
+    'SG_PIXELFORMAT_ASTC_4x4_SRGBA': 'SG_PIXELFORMAT_ASTC_4X4_SRGBA',
+}
+
+prim_types = {
+    'int':          'CInt',
+    # TODO: Check whether we should translate to `CBool` instead?
+    'bool':         'bool',
+    'char':         'char',
+    'int8_t':       'ichar',
+    'uint8_t':      'char',
+    'int16_t':      'short',
+    'uint16_t':     'ushort',
+    'int32_t':      'int',
+    'uint32_t':     'uint',
+    'int64_t':      'long',
+    'uint64_t':     'ulong',
+    'float':        'float',
+    'double':       'double',
+    'uintptr_t':    'uptr',
+    'intptr_t':     'iptr',
+    'size_t':       'usz'
+}
+
+prim_defaults = {
+    'int':          '0',
+    'bool':         'false',
+    'int8_t':       '0',
+    'uint8_t':      '0',
+    'int16_t':      '0',
+    'uint16_t':     '0',
+    'int32_t':      '0',
+    'uint32_t':     '0',
+    'int64_t':      '0',
+    'uint64_t':     '0',
+    'float':        '0.0',
+    'double':       '0.0',
+    'uintptr_t':    '0',
+    'intptr_t':     '0',
+    'size_t':       '0'
+}
+
+special_constant_types = {
+    "SG_INVALID_ID": "uint",
+    "SAPP_MODIFIER_SHIFT": "uint",
+    "SAPP_MODIFIER_CTRL": "uint",
+    "SAPP_MODIFIER_ALT": "uint",
+    "SAPP_MODIFIER_SUPER": "uint",
+    "SAPP_MODIFIER_LMB": "uint",
+    "SAPP_MODIFIER_RMB": "uint",
+    "SAPP_MODIFIER_MMB": "uint",
+}
+
+# Aliases for function pointers.
+# Function pointers must be aliased - we can't use them directly inside structs,
+# instead we have to create an alias and use the alias in the struct.
+aliases = {
+    # C type -> (Alias name, Right hand side of alias).
+    "void (*)(void *)":
+        ("DataCb", "fn void(void*)"),
+    "void *(*)(size_t, void *)":
+        ("AllocCb", "fn void*(usz, void*)"),
+    "void (*)(void *, void *)": 
+        ("FreeCb", "fn void*(usz, void*)"),
+    "void (*)(const char *, uint32_t, uint32_t, const char *, uint32_t, const char *, void *)":
+        ("LogCb", "fn void(ZString, uint, uint, ZString, uint, ZString, void*)"),
+    "void (*)(void)":
+        ("Cb", "fn void()"),
+    "void (*)(const sapp_event *)":
+        ("EventCb", "fn void(Event*)"),
+    "void (*)(const sapp_event *, void *)":
+        ("EventDataCb", "fn void(Event*, void*)"),
+    "void (*)(const sapp_html5_fetch_response *)":
+        ("ResponseCb", "fn void(Html5FetchResponse*)"),
+    "void (*)(float *, int, int)":
+        ("StreamCb", "fn void(float*, CInt, CInt)"),
+    "void (*)(float *, int, int, void *)":
+        ("StreamDataCb", "fn void(float*, CInt, CInt, void*)"),
+}
+
+struct_types = []
+enum_types = []
+enum_items = {}
+# Which alias were used in current module.
+# At the end of module we emit only used aliases.
+used_aliases = []  # We shouldn't use `set()` because the order differs among runs.
+out_lines = ''
+
+def reset_globals():
+    global struct_types
+    global enum_types
+    global enum_items
+    global used_aliases
+    global out_lines
+    struct_types = []
+    enum_types = []
+    enum_items = {}
+    used_aliases = []
+    out_lines = ''
+
+def l(s):
+    global out_lines
+    out_lines += s + '\n'
+
+def check_override(name, default=None):
+    if name in overrides:
+        return overrides[name]
+    elif default is None:
+        return name
+    else:
+        return default
+
+def check_ignore(name):
+    return name in ignores
+
+# PREFIX_BLA_BLUB to BLA_BLUB, prefix_bla_blub to bla_blub
+def as_snake_case(s, prefix):
+    outp = s
+    if outp.lower().startswith(prefix):
+        outp = outp[len(prefix):]
+    return outp
+
+def get_c3_module_path(c_prefix):
+    return f'{module_root}'
+
+def get_csource_path(c_prefix):
+    return f'{c_root}/{c_source_names[c_prefix]}'
+
+def make_c3_module_directory(c_prefix):
+    path = get_c3_module_path(c_prefix)
+    if not os.path.isdir(path):
+        os.makedirs(path)
+
+def as_prim_type(s):
+    return prim_types[s]
+
+def as_upper_snake_case(s, prefix):
+    outp = s.lower()
+    if outp.startswith(prefix):
+        outp = outp[len(prefix):]
+    return outp.upper()
+
+def as_module_name_for_enum_type(enum_name, prefix):
+    parts = enum_name.lower().split('_')
+    parent_module = module_names[prefix]
+    if parts[-1] == 't':
+        # Ignore '_t' suffix.
+        module = "_".join(parts[1:-1])
+    else:
+        module = "_".join(parts[1:])
+    return f"sokol::{parent_module}::{module}"
+
+def as_parent_module_name_for_enum_type(enum_name, prefix):
+    parent_module = module_names[prefix]
+    return f"sokol::{parent_module}"
+
+# prefix_bla_blub(_t) => (dep::)BlaBlub
+def as_struct_or_enum_type(s, prefix):
+    parts = s.lower().split('_')
+    outp = '' if s.startswith(prefix) else f'sokol::{module_names[parts[0]+"_"]}::'
+    for part in parts[1:]:
+        # ignore '_t' type postfix
+        if part != 't':
+            outp += part.capitalize()
+    return outp
+
+# PREFIX_ENUM_BLA_BLUB => BLA_BLUB, _PREFIX_ENUM_BLA_BLUB => BLA_BLUB
+def as_enum_item_name(s):
+    outp = s.lstrip('_')
+    parts = outp.split('_')[2:]
+    outp = '_'.join(parts)
+    if outp[0].isdigit():
+        if outp in ["2D", "3D"]:
+            outp = "TYPE_" + outp
+        else:
+            outp = 'NUM_' + outp
+    return outp
+
+def is_prim_type(s):
+    return s in prim_types
+
+def is_int_type(s):
+    return s == "int"
+
+def is_struct_type(s):
+    return s in struct_types
+
+def is_enum_type(s):
+    return s in enum_types
+
+def is_const_prim_ptr(s):
+    for prim_type in prim_types:
+        if s == f"const {prim_type} *":
+            return True
+    return False
+
+def is_prim_ptr(s):
+    for prim_type in prim_types:
+        if s == f"{prim_type} *":
+            return True
+    return False
+
+def is_const_struct_ptr(s):
+    for struct_type in struct_types:
+        if s == f"const {struct_type} *":
+            return True
+    return False
+
+def type_default_value(s):
+    return prim_defaults[s]
+
+def map_type(type, prefix, sub_type):
+    if sub_type not in ['c_arg', 'struct_field']:
+        sys.exit(f"Error: map_type(): unknown sub_type '{sub_type}")
+    if type == "void":
+        return ""
+    elif is_prim_type(type):
+        return as_prim_type(type)
+    elif is_struct_type(type):
+        return as_struct_or_enum_type(type, prefix)
+    elif is_enum_type(type):
+        return as_struct_or_enum_type(type, prefix)
+    elif util.is_void_ptr(type):
+        return "void*"
+    elif util.is_const_void_ptr(type):
+        return "void*"
+    elif util.is_string_ptr(type):
+        return "ZString"
+    elif is_const_struct_ptr(type):
+        return f"{as_struct_or_enum_type(util.extract_ptr_type(type), prefix)}*"
+    elif is_prim_ptr(type):
+        return f"{as_prim_type(util.extract_ptr_type(type))}*"
+    elif is_const_prim_ptr(type):
+        return f"{as_prim_type(util.extract_ptr_type(type))}*"
+    elif util.is_1d_array_type(type):
+        array_type = util.extract_array_type(type)
+        array_sizes = util.extract_array_sizes(type)
+        return f"{map_type(array_type, prefix, sub_type)}[{array_sizes[0]}]"
+    elif util.is_2d_array_type(type):
+        array_type = util.extract_array_type(type)
+        array_sizes = util.extract_array_sizes(type)
+        # TODO: Check if the dimensions are in correct order.
+        return f"{map_type(array_type, prefix, sub_type)}[{array_sizes[0]}][{array_sizes[1]}]"
+    elif util.is_func_ptr(type):
+        if type in aliases:
+            alias_name, _ = aliases[type]
+            if type not in used_aliases:
+                used_aliases.append(type)
+            return alias_name
+        else:
+            sys.exit(f"Error map_type(): missing alias for function pointer '{type}'")
+    else:
+        sys.exit(f"Error map_type(): unknown type '{type}'")
+
+def funcdecl_args_c(decl, prefix):
+    s = ''
+    func_name = decl['name']
+    for param_decl in decl['params']:
+        if s != '':
+            s += ', '
+        param_name = param_decl['name']
+        param_type = check_override(f'{func_name}.{param_name}', default=param_decl['type'])
+        s += f"{map_type(param_type, prefix, 'c_arg')} {param_name}"
+    return s
+
+def funcdecl_result_c(decl, prefix):
+    func_name = decl['name']
+    decl_type = decl['type']
+    res_c_type = decl_type[:decl_type.index('(')].strip()
+    return map_type(check_override(f'{func_name}.RESULT', default=res_c_type), prefix, 'c_arg')
+
+def gen_c_imports(inp, c_prefix, prefix):
+    prefix = inp['prefix']
+    for decl in inp['decls']:
+        if decl['kind'] == 'func' and not decl['is_dep'] and not check_ignore(decl['name']):
+            args = funcdecl_args_c(decl, prefix)
+            res_type = funcdecl_result_c(decl, prefix)
+            res_str = 'void' if res_type == '' else res_type
+            l(f'extern fn {res_str} {check_override(as_snake_case(decl["name"], c_prefix))}({args}) @extern("{decl["name"]}");')
+    l('')
+
+def gen_consts(decl, prefix):
+    for item in decl["items"]:
+        #
+        # TODO: What type should these constants have? Currently giving all `usz`
+        #       unless specifically overridden by `special_constant_types`
+        #
+
+        item_name = check_override(item["name"])
+        tpe = "usz"
+        if item_name in special_constant_types:
+            tpe = special_constant_types[item_name]
+        l(f"const {tpe} {as_upper_snake_case(item_name, prefix)} = {item['value']};")
+    l('')
+
+def gen_struct(decl, prefix):
+    c_struct_name = check_override(decl['name'])
+    struct_name = as_struct_or_enum_type(c_struct_name, prefix)
+    l(f'struct {struct_name}')
+    l('{')
+    for field in decl['fields']:
+        field_name = check_override(field['name'])
+        field_type = map_type(check_override(f'{c_struct_name}.{field_name}', default=field['type']), prefix, 'struct_field')
+        l(f'    {field_type} {field_name};')
+    l('}')
+    l('')
+
+def gen_enum(decl, prefix):
+    enum_name = check_override(decl['name'])
+    tpe = "int"
+    if any(as_enum_item_name(check_override(item['name'])) == 'FORCE_U32' for item in decl['items']):
+        tpe = "uint"
+    l(f'distinct {as_struct_or_enum_type(enum_name, prefix)} = {tpe};')
+    # Constants are in submodule.
+    l(f'module {as_module_name_for_enum_type(enum_name, prefix)};')
+    value = "-1"
+    for item in decl['items']:
+        item_name = as_enum_item_name(check_override(item['name']))
+        if item_name != 'FORCE_U32':
+            if 'value' in item:
+                value = item['value']
+            else:
+                value = str(int(value) + 1)
+            l(f"const {as_struct_or_enum_type(enum_name, prefix)} {item_name} = {value};")
+    l(f'module {as_parent_module_name_for_enum_type(enum_name, prefix)};')
+    # After reopening the original module all dependencies must be reimported.
+    l(f'import sokol;')
+    l('')
+
+def gen_imports(dep_prefixes):
+    l(f'import sokol;')
+    l('')
+
+def gen_function_pointer_aliases():
+    for type in used_aliases:
+        alias_name, right_hand_side = aliases[type]
+        l(f'def {alias_name} = {right_hand_side};')
+    l('')
+
+def gen_module(inp, c_prefix, dep_prefixes):
+    pre_parse(inp)
+    l('// machine generated, do not edit')
+    l('')
+    l(f"module sokol::{module_names[c_prefix]};")
+    gen_imports(dep_prefixes)
+    prefix = inp['prefix']
+    gen_c_imports(inp, c_prefix, prefix)
+    for decl in inp['decls']:
+        if not decl['is_dep']:
+            kind = decl['kind']
+            if kind == 'consts':
+                gen_consts(decl, prefix)
+            elif not check_ignore(decl['name']):
+                if kind == 'struct':
+                    gen_struct(decl, prefix)
+                elif kind == 'enum':
+                    gen_enum(decl, prefix)
+    gen_function_pointer_aliases()
+
+def pre_parse(inp):
+    global struct_types
+    global enum_types
+    for decl in inp['decls']:
+        kind = decl['kind']
+        if kind == 'struct':
+            struct_types.append(decl['name'])
+        elif kind == 'enum':
+            enum_name = decl['name']
+            enum_types.append(enum_name)
+            enum_items[enum_name] = []
+            for item in decl['items']:
+                enum_items[enum_name].append(as_enum_item_name(item['name']))
+
+def prepare():
+    print('=== Generating C3 bindings:')
+    if not os.path.isdir(module_root):
+        os.makedirs(module_root)
+    if not os.path.isdir(c_root):
+        os.makedirs(c_root)
+
+def gen(c_header_path, c_prefix, dep_c_prefixes):
+    if not c_prefix in module_names:
+        print(f'  >> warning: skipping generation for {c_prefix} prefix...')
+        return
+    reset_globals()
+    make_c3_module_directory(c_prefix)
+    print(f'  {c_header_path} => {module_names[c_prefix]}')
+    shutil.copyfile(c_header_path, f'{c_root}/{os.path.basename(c_header_path)}')
+    csource_path = get_csource_path(c_prefix)
+    module_name = module_names[c_prefix]
+    ir = gen_ir.gen(c_header_path, csource_path, module_name, c_prefix, dep_c_prefixes)
+    gen_module(ir, c_prefix, dep_c_prefixes)
+    with open(f"{module_root}/{ir['module']}.c3", 'w', newline='\n') as f_outp:
+        f_outp.write(out_lines)

+ 5 - 0
bindgen/gen_odin.py

@@ -360,6 +360,8 @@ def gen_c_imports(inp, c_prefix, prefix):
     linux_gl_libs = get_system_libs(prefix, 'linux', 'gl')
     l( 'import "core:c"')
     l( '')
+    l( '_ :: c')
+    l( '')
     l( 'SOKOL_DEBUG :: #config(SOKOL_DEBUG, ODIN_DEBUG)')
     l( '')
     l(f'DEBUG :: #config(SOKOL_{module_name.upper()}_DEBUG, SOKOL_DEBUG)')
@@ -421,6 +423,9 @@ def gen_c_imports(inp, c_prefix, prefix):
     l(f'        when DEBUG {{ foreign import {clib_import} {{ "{clib_prefix}_linux_x64_gl_debug.a"{linux_gl_libs} }} }}')
     l(f'        else       {{ foreign import {clib_import} {{ "{clib_prefix}_linux_x64_gl_release.a"{linux_gl_libs} }} }}')
     l( '    }')
+    l( '} else when ODIN_ARCH == .wasm32 || ODIN_ARCH == .wasm64p32 {')
+    l(f'    // Feed {clib_prefix}_wasm_gl_debug.a or {clib_prefix}_wasm_gl_release.a into emscripten compiler.')
+    l(f'    foreign import {clib_import} {{ "env.o" }}')
     l( '} else {')
     l( '    #panic("This OS is currently not supported")')
     l( '}')

+ 16 - 20
sokol_app.h

@@ -4863,7 +4863,7 @@ _SOKOL_PRIVATE void _sapp_ios_show_keyboard(bool shown) {
 #if defined(_SAPP_EMSCRIPTEN)
 
 #if defined(EM_JS_DEPS)
-EM_JS_DEPS(sokol_app, "$withStackSave,$stringToUTF8OnStack,$findCanvasEventTarget");
+EM_JS_DEPS(sokol_app, "$withStackSave,$stringToUTF8OnStack,$findCanvasEventTarget")
 #endif
 
 #ifdef __cplusplus
@@ -4970,11 +4970,11 @@ EM_JS(void, sapp_js_add_beforeunload_listener, (void), {
         }
     };
     window.addEventListener('beforeunload', Module.sokol_beforeunload);
-});
+})
 
 EM_JS(void, sapp_js_remove_beforeunload_listener, (void), {
     window.removeEventListener('beforeunload', Module.sokol_beforeunload);
-});
+})
 
 EM_JS(void, sapp_js_add_clipboard_listener, (void), {
     Module.sokol_paste = (event) => {
@@ -4985,11 +4985,11 @@ EM_JS(void, sapp_js_add_clipboard_listener, (void), {
         });
     };
     window.addEventListener('paste', Module.sokol_paste);
-});
+})
 
 EM_JS(void, sapp_js_remove_clipboard_listener, (void), {
     window.removeEventListener('paste', Module.sokol_paste);
-});
+})
 
 EM_JS(void, sapp_js_write_clipboard, (const char* c_str), {
     const str = UTF8ToString(c_str);
@@ -5007,7 +5007,7 @@ EM_JS(void, sapp_js_write_clipboard, (const char* c_str), {
     ta.select();
     document.execCommand('copy');
     document.body.removeChild(ta);
-});
+})
 
 _SOKOL_PRIVATE void _sapp_emsc_set_clipboard_string(const char* str) {
     sapp_js_write_clipboard(str);
@@ -5053,7 +5053,7 @@ EM_JS(void, sapp_js_add_dragndrop_listeners, (void), {
     canvas.addEventListener('dragleave', Module.sokol_dragleave, false);
     canvas.addEventListener('dragover',  Module.sokol_dragover, false);
     canvas.addEventListener('drop',      Module.sokol_drop, false);
-});
+})
 
 EM_JS(uint32_t, sapp_js_dropped_file_size, (int index), {
     \x2F\x2A\x2A @suppress {missingProperties} \x2A\x2F
@@ -5064,7 +5064,7 @@ EM_JS(uint32_t, sapp_js_dropped_file_size, (int index), {
     else {
         return files[index].size;
     }
-});
+})
 
 EM_JS(void, sapp_js_fetch_dropped_file, (int index, _sapp_html5_fetch_callback callback, void* buf_ptr, uint32_t buf_size, void* user_data), {
     const reader = new FileReader();
@@ -5086,7 +5086,7 @@ EM_JS(void, sapp_js_fetch_dropped_file, (int index, _sapp_html5_fetch_callback c
     \x2F\x2A\x2A @suppress {missingProperties} \x2A\x2F
     const files = Module.sokol_dropped_files;
     reader.readAsArrayBuffer(files[index]);
-});
+})
 
 EM_JS(void, sapp_js_remove_dragndrop_listeners, (void), {
     \x2F\x2A\x2A @suppress {missingProperties} \x2A\x2F
@@ -5095,7 +5095,7 @@ EM_JS(void, sapp_js_remove_dragndrop_listeners, (void), {
     canvas.removeEventListener('dragleave', Module.sokol_dragleave);
     canvas.removeEventListener('dragover',  Module.sokol_dragover);
     canvas.removeEventListener('drop',      Module.sokol_drop);
-});
+})
 
 EM_JS(void, sapp_js_init, (const char* c_str_target_selector, const char* c_str_document_title), {
     if (c_str_document_title !== 0) {
@@ -5116,7 +5116,7 @@ EM_JS(void, sapp_js_init, (const char* c_str_target_selector, const char* c_str_
     if (!Module.sapp_emsc_target.requestPointerLock) {
         console.warn("sokol_app.h: target doesn't support requestPointerLock: ", target_selector_str);
     }
-});
+})
 
 _SOKOL_PRIVATE EM_BOOL _sapp_emsc_pointerlockchange_cb(int emsc_type, const EmscriptenPointerlockChangeEvent* emsc_event, void* user_data) {
     _SOKOL_UNUSED(emsc_type);
@@ -5140,13 +5140,13 @@ EM_JS(void, sapp_js_request_pointerlock, (void), {
             Module.sapp_emsc_target.requestPointerLock();
         }
     }
-});
+})
 
 EM_JS(void, sapp_js_exit_pointerlock, (void), {
     if (document.exitPointerLock) {
         document.exitPointerLock();
     }
-});
+})
 
 _SOKOL_PRIVATE void _sapp_emsc_lock_mouse(bool lock) {
     if (lock) {
@@ -5193,7 +5193,7 @@ EM_JS(void, sapp_js_set_cursor, (int cursor_type, int shown), {
         }
         Module.sapp_emsc_target.style.cursor = cursor;
     }
-});
+})
 
 _SOKOL_PRIVATE void _sapp_emsc_update_cursor(sapp_mouse_cursor cursor, bool shown) {
     SOKOL_ASSERT((cursor >= 0) && (cursor < _SAPP_MOUSECURSOR_NUM));
@@ -5206,7 +5206,7 @@ EM_JS(void, sapp_js_clear_favicon, (void), {
     if (link) {
         document.head.removeChild(link);
     }
-});
+})
 
 EM_JS(void, sapp_js_set_favicon, (int w, int h, const uint8_t* pixels), {
     const canvas = document.createElement('canvas');
@@ -5221,7 +5221,7 @@ EM_JS(void, sapp_js_set_favicon, (int w, int h, const uint8_t* pixels), {
     new_link.rel = 'shortcut icon';
     new_link.href = canvas.toDataURL();
     document.head.appendChild(new_link);
-});
+})
 
 _SOKOL_PRIVATE void _sapp_emsc_set_icon(const sapp_icon_desc* icon_desc, int num_images) {
     SOKOL_ASSERT((num_images > 0) && (num_images <= SAPP_MAX_ICONIMAGES));
@@ -5720,10 +5720,6 @@ _SOKOL_PRIVATE void _sapp_emsc_webgl_init(void) {
     // FIXME: error message?
     emscripten_webgl_make_context_current(ctx);
     glGetIntegerv(GL_FRAMEBUFFER_BINDING, (GLint*)&_sapp.gl.framebuffer);
-
-    // FIXME: remove PVRTC support here and in sokol-gfx at some point
-    // some WebGL extension are not enabled automatically by emscripten
-    emscripten_webgl_enable_extension(ctx, "WEBKIT_WEBGL_compressed_texture_pvrtc");
 }
 #endif
 

+ 2 - 2
sokol_args.h

@@ -685,7 +685,7 @@ extern "C" {
 #endif
 
 #if defined(EM_JS_DEPS)
-EM_JS_DEPS(sokol_audio, "$withStackSave,$stringToUTF8OnStack");
+EM_JS_DEPS(sokol_audio, "$withStackSave,$stringToUTF8OnStack")
 #endif
 
 EMSCRIPTEN_KEEPALIVE void _sargs_add_kvp(const char* key, const char* val) {
@@ -729,7 +729,7 @@ EM_JS(void, sargs_js_parse_url, (void), {
             __sargs_add_kvp(key_cstr, val_cstr)
         });
     }
-});
+})
 
 #endif // EMSCRIPTEN
 

+ 7 - 7
sokol_audio.h

@@ -1431,8 +1431,8 @@ _SOKOL_PRIVATE int _saudio_fifo_read(_saudio_fifo_t* fifo, uint8_t* ptr, int num
 _SOKOL_PRIVATE bool _saudio_dummy_backend_init(void) {
     _saudio.bytes_per_frame = _saudio.num_channels * (int)sizeof(float);
     return true;
-};
-_SOKOL_PRIVATE void _saudio_dummy_backend_shutdown(void) { };
+}
+_SOKOL_PRIVATE void _saudio_dummy_backend_shutdown(void) { }
 
 //  █████  ██      ███████  █████
 // ██   ██ ██      ██      ██   ██
@@ -1851,7 +1851,7 @@ EM_JS(int, saudio_js_init, (int sample_rate, int num_channels, int buffer_size),
     else {
         return 0;
     }
-});
+})
 
 /* shutdown the WebAudioContext and ScriptProcessorNode */
 EM_JS(void, saudio_js_shutdown, (void), {
@@ -1865,7 +1865,7 @@ EM_JS(void, saudio_js_shutdown, (void), {
         Module._saudio_context = null;
         Module._saudio_node = null;
     }
-});
+})
 
 /* get the actual sample rate back from the WebAudio context */
 EM_JS(int, saudio_js_sample_rate, (void), {
@@ -1875,7 +1875,7 @@ EM_JS(int, saudio_js_sample_rate, (void), {
     else {
         return 0;
     }
-});
+})
 
 /* get the actual buffer size in number of frames */
 EM_JS(int, saudio_js_buffer_frames, (void), {
@@ -1885,7 +1885,7 @@ EM_JS(int, saudio_js_buffer_frames, (void), {
     else {
         return 0;
     }
-});
+})
 
 /* return 1 if the WebAudio context is currently suspended, else 0 */
 EM_JS(int, saudio_js_suspended, (void), {
@@ -1897,7 +1897,7 @@ EM_JS(int, saudio_js_suspended, (void), {
             return 0;
         }
     }
-});
+})
 
 _SOKOL_PRIVATE bool _saudio_webaudio_backend_init(void) {
     if (saudio_js_init(_saudio.sample_rate, _saudio.num_channels, _saudio.buffer_frames)) {

+ 2 - 2
sokol_fetch.h

@@ -2213,7 +2213,7 @@ EM_JS(void, sfetch_js_send_head_request, (uint32_t slot_id, const char* path_cst
         }
     };
     req.send();
-});
+})
 
 /* if bytes_to_read != 0, a range-request will be sent, otherwise a normal request */
 EM_JS(void, sfetch_js_send_get_request, (uint32_t slot_id, const char* path_cstr, uint32_t offset, uint32_t bytes_to_read, void* buf_ptr, uint32_t buf_size), {
@@ -2244,7 +2244,7 @@ EM_JS(void, sfetch_js_send_get_request, (uint32_t slot_id, const char* path_cstr
         }
     };
     req.send();
-});
+})
 
 /*=== emscripten specific C helper functions =================================*/
 #ifdef __cplusplus

+ 40 - 123
sokol_gfx.h

@@ -1561,7 +1561,7 @@
       as soon as the vertex format shows up in webgpu.h, sokol_gfx.h will add support.
 
     - Likewise, the following sokol-gfx vertex formats are not supported in WebGPU:
-      R16, R16SN, RG16, RG16SN, RGBA16, RGBA16SN and all PVRTC compressed format.
+      R16, R16SN, RG16, RG16SN, RGBA16, RGBA16SN.
       Unlike unsupported vertex formats, unsupported pixel formats can be queried
       in cross-backend code via sg_query_pixel_format() though.
 
@@ -1827,10 +1827,6 @@ typedef enum sg_pixel_format {
     SG_PIXELFORMAT_BC6H_RGBUF,
     SG_PIXELFORMAT_BC7_RGBA,
     SG_PIXELFORMAT_BC7_SRGBA,
-    SG_PIXELFORMAT_PVRTC_RGB_2BPP,      // FIXME: deprecated
-    SG_PIXELFORMAT_PVRTC_RGB_4BPP,      // FIXME: deprecated
-    SG_PIXELFORMAT_PVRTC_RGBA_2BPP,     // FIXME: deprecated
-    SG_PIXELFORMAT_PVRTC_RGBA_4BPP,     // FIXME: deprecated
     SG_PIXELFORMAT_ETC2_RGB8,
     SG_PIXELFORMAT_ETC2_SRGB8,
     SG_PIXELFORMAT_ETC2_RGB8A1,
@@ -2409,7 +2405,9 @@ typedef enum sg_stencil_op {
                 .dst_factor_alpha
 
     The default value is SG_BLENDFACTOR_ONE for source
-    factors, and SG_BLENDFACTOR_ZERO for destination factors.
+    factors, and for the destination SG_BLENDFACTOR_ZERO if the associated
+    blend-op is ADD, SUBTRACT or REVERSE_SUBTRACT or SG_BLENDFACTOR_ONE
+    if the associated blend-op is MIN or MAX.
 */
 typedef enum sg_blend_factor {
     _SG_BLENDFACTOR_DEFAULT,    // value 0 reserved for default-init
@@ -2452,6 +2450,8 @@ typedef enum sg_blend_op {
     SG_BLENDOP_ADD,
     SG_BLENDOP_SUBTRACT,
     SG_BLENDOP_REVERSE_SUBTRACT,
+    SG_BLENDOP_MIN,
+    SG_BLENDOP_MAX,
     _SG_BLENDOP_NUM,
     _SG_BLENDOP_FORCE_U32 = 0x7FFFFFFF
 } sg_blend_op;
@@ -3909,6 +3909,7 @@ typedef struct sg_frame_stats {
     _SG_LOGITEM_XMACRO(VALIDATE_PIPELINEDESC_LAYOUT_STRIDE4, "sg_pipeline_desc.layout.buffers[].stride must be multiple of 4") \
     _SG_LOGITEM_XMACRO(VALIDATE_PIPELINEDESC_ATTR_SEMANTICS, "D3D11 missing vertex attribute semantics in shader") \
     _SG_LOGITEM_XMACRO(VALIDATE_PIPELINEDESC_SHADER_READONLY_STORAGEBUFFERS, "sg_pipeline_desc.shader: only readonly storage buffer bindings allowed in render pipelines") \
+    _SG_LOGITEM_XMACRO(VALIDATE_PIPELINEDESC_BLENDOP_MINMAX_REQUIRES_BLENDFACTOR_ONE, "SG_BLENDOP_MIN/MAX requires all blend factors to be SG_BLENDFACTOR_ONE") \
     _SG_LOGITEM_XMACRO(VALIDATE_ATTACHMENTSDESC_CANARY, "sg_attachments_desc not initialized") \
     _SG_LOGITEM_XMACRO(VALIDATE_ATTACHMENTSDESC_NO_ATTACHMENTS, "sg_attachments_desc no color or depth-stencil attachments") \
     _SG_LOGITEM_XMACRO(VALIDATE_ATTACHMENTSDESC_NO_CONT_COLOR_ATTS, "color attachments must occupy continuous slots") \
@@ -5024,6 +5025,8 @@ inline int sg_append_buffer(sg_buffer buf_id, const sg_range& data) { return sg_
         #define GL_TEXTURE_2D_MULTISAMPLE 0x9100
         #define GL_TEXTURE_2D_MULTISAMPLE_ARRAY 0x9102
         #define GL_SHADER_STORAGE_BARRIER_BIT 0x2000
+        #define GL_MIN 0x8007
+        #define GL_MAX 0x8008
     #endif
 
     #ifndef GL_UNSIGNED_INT_2_10_10_10_REV
@@ -5074,18 +5077,6 @@ inline int sg_append_buffer(sg_buffer buf_id, const sg_range& data) { return sg_
     #ifndef GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT_ARB
     #define GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT_ARB 0x8E8F
     #endif
-    #ifndef GL_COMPRESSED_RGB_PVRTC_2BPPV1_IMG
-    #define GL_COMPRESSED_RGB_PVRTC_2BPPV1_IMG 0x8C01
-    #endif
-    #ifndef GL_COMPRESSED_RGB_PVRTC_4BPPV1_IMG
-    #define GL_COMPRESSED_RGB_PVRTC_4BPPV1_IMG 0x8C00
-    #endif
-    #ifndef GL_COMPRESSED_RGBA_PVRTC_2BPPV1_IMG
-    #define GL_COMPRESSED_RGBA_PVRTC_2BPPV1_IMG 0x8C03
-    #endif
-    #ifndef GL_COMPRESSED_RGBA_PVRTC_4BPPV1_IMG
-    #define GL_COMPRESSED_RGBA_PVRTC_4BPPV1_IMG 0x8C02
-    #endif
     #ifndef GL_COMPRESSED_RGB8_ETC2
     #define GL_COMPRESSED_RGB8_ETC2 0x9274
     #endif
@@ -6548,10 +6539,6 @@ _SOKOL_PRIVATE bool _sg_is_compressed_pixel_format(sg_pixel_format fmt) {
         case SG_PIXELFORMAT_BC6H_RGBUF:
         case SG_PIXELFORMAT_BC7_RGBA:
         case SG_PIXELFORMAT_BC7_SRGBA:
-        case SG_PIXELFORMAT_PVRTC_RGB_2BPP:
-        case SG_PIXELFORMAT_PVRTC_RGB_4BPP:
-        case SG_PIXELFORMAT_PVRTC_RGBA_2BPP:
-        case SG_PIXELFORMAT_PVRTC_RGBA_4BPP:
         case SG_PIXELFORMAT_ETC2_RGB8:
         case SG_PIXELFORMAT_ETC2_SRGB8:
         case SG_PIXELFORMAT_ETC2_RGB8A1:
@@ -6665,19 +6652,6 @@ _SOKOL_PRIVATE bool _sg_multiple_u64(uint64_t val, uint64_t of) {
 /* return row pitch for an image
 
     see ComputePitch in https://github.com/microsoft/DirectXTex/blob/master/DirectXTex/DirectXTexUtil.cpp
-
-    For the special PVRTC pitch computation, see:
-    GL extension requirement (https://www.khronos.org/registry/OpenGL/extensions/IMG/IMG_texture_compression_pvrtc.txt)
-
-    Quote:
-
-    6) How is the imageSize argument calculated for the CompressedTexImage2D
-       and CompressedTexSubImage2D functions.
-
-       Resolution: For PVRTC 4BPP formats the imageSize is calculated as:
-          ( max(width, 8) * max(height, 8) * 4 + 7) / 8
-       For PVRTC 2BPP formats the imageSize is calculated as:
-          ( max(width, 16) * max(height, 8) * 2 + 7) / 8
 */
 _SOKOL_PRIVATE int _sg_row_pitch(sg_pixel_format fmt, int width, int row_align) {
     int pitch;
@@ -6711,14 +6685,6 @@ _SOKOL_PRIVATE int _sg_row_pitch(sg_pixel_format fmt, int width, int row_align)
             pitch = ((width + 3) / 4) * 16;
             pitch = pitch < 16 ? 16 : pitch;
             break;
-        case SG_PIXELFORMAT_PVRTC_RGB_4BPP:
-        case SG_PIXELFORMAT_PVRTC_RGBA_4BPP:
-            pitch = (_sg_max(width, 8) * 4 + 7) / 8;
-            break;
-        case SG_PIXELFORMAT_PVRTC_RGB_2BPP:
-        case SG_PIXELFORMAT_PVRTC_RGBA_2BPP:
-            pitch = (_sg_max(width, 16) * 2 + 7) / 8;
-            break;
         default:
             pitch = width * _sg_pixelformat_bytesize(fmt);
             break;
@@ -6756,18 +6722,6 @@ _SOKOL_PRIVATE int _sg_num_rows(sg_pixel_format fmt, int height) {
         case SG_PIXELFORMAT_ASTC_4x4_SRGBA:
             num_rows = ((height + 3) / 4);
             break;
-        case SG_PIXELFORMAT_PVRTC_RGB_4BPP:
-        case SG_PIXELFORMAT_PVRTC_RGBA_4BPP:
-        case SG_PIXELFORMAT_PVRTC_RGB_2BPP:
-        case SG_PIXELFORMAT_PVRTC_RGBA_2BPP:
-            /* NOTE: this is most likely not correct because it ignores any
-                PVCRTC block size, but multiplied with _sg_row_pitch()
-                it gives the correct surface pitch.
-
-                See: https://www.khronos.org/registry/OpenGL/extensions/IMG/IMG_texture_compression_pvrtc.txt
-            */
-            num_rows = ((_sg_max(height, 8) + 7) / 8) * 8;
-            break;
         default:
             num_rows = height;
             break;
@@ -7483,6 +7437,8 @@ _SOKOL_PRIVATE GLenum _sg_gl_blend_op(sg_blend_op op) {
         case SG_BLENDOP_ADD:                return GL_FUNC_ADD;
         case SG_BLENDOP_SUBTRACT:           return GL_FUNC_SUBTRACT;
         case SG_BLENDOP_REVERSE_SUBTRACT:   return GL_FUNC_REVERSE_SUBTRACT;
+        case SG_BLENDOP_MIN:                return GL_MIN;
+        case SG_BLENDOP_MAX:                return GL_MAX;
         default: SOKOL_UNREACHABLE; return 0;
     }
 }
@@ -7667,14 +7623,6 @@ _SOKOL_PRIVATE GLenum _sg_gl_teximage_format(sg_pixel_format fmt) {
             return GL_COMPRESSED_RGBA_BPTC_UNORM_ARB;
         case SG_PIXELFORMAT_BC7_SRGBA:
             return GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM_ARB;
-        case SG_PIXELFORMAT_PVRTC_RGB_2BPP:
-            return GL_COMPRESSED_RGB_PVRTC_2BPPV1_IMG;
-        case SG_PIXELFORMAT_PVRTC_RGB_4BPP:
-            return GL_COMPRESSED_RGB_PVRTC_4BPPV1_IMG;
-        case SG_PIXELFORMAT_PVRTC_RGBA_2BPP:
-            return GL_COMPRESSED_RGBA_PVRTC_2BPPV1_IMG;
-        case SG_PIXELFORMAT_PVRTC_RGBA_4BPP:
-            return GL_COMPRESSED_RGBA_PVRTC_4BPPV1_IMG;
         case SG_PIXELFORMAT_ETC2_RGB8:
             return GL_COMPRESSED_RGB8_ETC2;
         case SG_PIXELFORMAT_ETC2_SRGB8:
@@ -7764,10 +7712,6 @@ _SOKOL_PRIVATE GLenum _sg_gl_teximage_internal_format(sg_pixel_format fmt) {
         case SG_PIXELFORMAT_BC6H_RGBUF:         return GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT_ARB;
         case SG_PIXELFORMAT_BC7_RGBA:           return GL_COMPRESSED_RGBA_BPTC_UNORM_ARB;
         case SG_PIXELFORMAT_BC7_SRGBA:          return GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM_ARB;
-        case SG_PIXELFORMAT_PVRTC_RGB_2BPP:     return GL_COMPRESSED_RGB_PVRTC_2BPPV1_IMG;
-        case SG_PIXELFORMAT_PVRTC_RGB_4BPP:     return GL_COMPRESSED_RGB_PVRTC_4BPPV1_IMG;
-        case SG_PIXELFORMAT_PVRTC_RGBA_2BPP:    return GL_COMPRESSED_RGBA_PVRTC_2BPPV1_IMG;
-        case SG_PIXELFORMAT_PVRTC_RGBA_4BPP:    return GL_COMPRESSED_RGBA_PVRTC_4BPPV1_IMG;
         case SG_PIXELFORMAT_ETC2_RGB8:          return GL_COMPRESSED_RGB8_ETC2;
         case SG_PIXELFORMAT_ETC2_SRGB8:         return GL_COMPRESSED_SRGB8_ETC2;
         case SG_PIXELFORMAT_ETC2_RGB8A1:        return GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2;
@@ -7911,13 +7855,6 @@ _SOKOL_PRIVATE void _sg_gl_init_pixelformats_bptc(void) {
     _sg_pixelformat_sf(&_sg.formats[SG_PIXELFORMAT_BC7_SRGBA]);
 }
 
-_SOKOL_PRIVATE void _sg_gl_init_pixelformats_pvrtc(void) {
-    _sg_pixelformat_sf(&_sg.formats[SG_PIXELFORMAT_PVRTC_RGB_2BPP]);
-    _sg_pixelformat_sf(&_sg.formats[SG_PIXELFORMAT_PVRTC_RGB_4BPP]);
-    _sg_pixelformat_sf(&_sg.formats[SG_PIXELFORMAT_PVRTC_RGBA_2BPP]);
-    _sg_pixelformat_sf(&_sg.formats[SG_PIXELFORMAT_PVRTC_RGBA_4BPP]);
-}
-
 _SOKOL_PRIVATE void _sg_gl_init_pixelformats_etc2(void) {
     _sg_pixelformat_sf(&_sg.formats[SG_PIXELFORMAT_ETC2_RGB8]);
     _sg_pixelformat_sf(&_sg.formats[SG_PIXELFORMAT_ETC2_SRGB8]);
@@ -7996,7 +7933,6 @@ _SOKOL_PRIVATE void _sg_gl_init_caps_glcore(void) {
     bool has_s3tc = false;  // BC1..BC3
     bool has_rgtc = false;  // BC4 and BC5
     bool has_bptc = false;  // BC6H and BC7
-    bool has_pvrtc = false;
     bool has_etc2 = false;
     bool has_astc = false;
     GLint num_ext = 0;
@@ -8010,8 +7946,6 @@ _SOKOL_PRIVATE void _sg_gl_init_caps_glcore(void) {
                 has_rgtc = true;
             } else if (strstr(ext, "_texture_compression_bptc")) {
                 has_bptc = true;
-            } else if (strstr(ext, "_texture_compression_pvrtc")) {
-                has_pvrtc = true;
             } else if (strstr(ext, "_ES3_compatibility")) {
                 has_etc2 = true;
             } else if (strstr(ext, "_texture_filter_anisotropic")) {
@@ -8043,9 +7977,6 @@ _SOKOL_PRIVATE void _sg_gl_init_caps_glcore(void) {
     if (has_bptc) {
         _sg_gl_init_pixelformats_bptc();
     }
-    if (has_pvrtc) {
-        _sg_gl_init_pixelformats_pvrtc();
-    }
     if (has_etc2) {
         _sg_gl_init_pixelformats_etc2();
     }
@@ -8069,7 +8000,6 @@ _SOKOL_PRIVATE void _sg_gl_init_caps_gles3(void) {
     bool has_s3tc = false;  // BC1..BC3
     bool has_rgtc = false;  // BC4 and BC5
     bool has_bptc = false;  // BC6H and BC7
-    bool has_pvrtc = false;
     #if defined(__EMSCRIPTEN__)
         bool has_etc2 = false;
     #else
@@ -8093,10 +8023,6 @@ _SOKOL_PRIVATE void _sg_gl_init_caps_gles3(void) {
                 has_rgtc = true;
             } else if (strstr(ext, "_texture_compression_bptc")) {
                 has_bptc = true;
-            } else if (strstr(ext, "_texture_compression_pvrtc")) {
-                has_pvrtc = true;
-            } else if (strstr(ext, "_compressed_texture_pvrtc")) {
-                has_pvrtc = true;
             } else if (strstr(ext, "_compressed_texture_etc")) {
                 has_etc2 = true;
             } else if (strstr(ext, "_compressed_texture_astc")) {
@@ -8141,9 +8067,6 @@ _SOKOL_PRIVATE void _sg_gl_init_caps_gles3(void) {
     if (has_bptc) {
         _sg_gl_init_pixelformats_bptc();
     }
-    if (has_pvrtc) {
-        _sg_gl_init_pixelformats_pvrtc();
-    }
     if (has_etc2) {
         _sg_gl_init_pixelformats_etc2();
     }
@@ -10831,6 +10754,8 @@ _SOKOL_PRIVATE D3D11_BLEND_OP _sg_d3d11_blend_op(sg_blend_op op) {
         case SG_BLENDOP_ADD:                return D3D11_BLEND_OP_ADD;
         case SG_BLENDOP_SUBTRACT:           return D3D11_BLEND_OP_SUBTRACT;
         case SG_BLENDOP_REVERSE_SUBTRACT:   return D3D11_BLEND_OP_REV_SUBTRACT;
+        case SG_BLENDOP_MIN:                return D3D11_BLEND_OP_MIN;
+        case SG_BLENDOP_MAX:                return D3D11_BLEND_OP_MAX;
         default: SOKOL_UNREACHABLE; return (D3D11_BLEND_OP) 0;
     }
 }
@@ -12484,10 +12409,6 @@ _SOKOL_PRIVATE MTLPixelFormat _sg_mtl_pixel_format(sg_pixel_format fmt) {
         case SG_PIXELFORMAT_BC7_RGBA:               return MTLPixelFormatBC7_RGBAUnorm;
         case SG_PIXELFORMAT_BC7_SRGBA:              return MTLPixelFormatBC7_RGBAUnorm_sRGB;
         #else
-        case SG_PIXELFORMAT_PVRTC_RGB_2BPP:         return MTLPixelFormatPVRTC_RGB_2BPP;
-        case SG_PIXELFORMAT_PVRTC_RGB_4BPP:         return MTLPixelFormatPVRTC_RGB_4BPP;
-        case SG_PIXELFORMAT_PVRTC_RGBA_2BPP:        return MTLPixelFormatPVRTC_RGBA_2BPP;
-        case SG_PIXELFORMAT_PVRTC_RGBA_4BPP:        return MTLPixelFormatPVRTC_RGBA_4BPP;
         case SG_PIXELFORMAT_ETC2_RGB8:              return MTLPixelFormatETC2_RGB8;
         case SG_PIXELFORMAT_ETC2_SRGB8:             return MTLPixelFormatETC2_RGB8_sRGB;
         case SG_PIXELFORMAT_ETC2_RGB8A1:            return MTLPixelFormatETC2_RGB8A1;
@@ -12526,6 +12447,8 @@ _SOKOL_PRIVATE MTLBlendOperation _sg_mtl_blend_op(sg_blend_op op) {
         case SG_BLENDOP_ADD:                return MTLBlendOperationAdd;
         case SG_BLENDOP_SUBTRACT:           return MTLBlendOperationSubtract;
         case SG_BLENDOP_REVERSE_SUBTRACT:   return MTLBlendOperationReverseSubtract;
+        case SG_BLENDOP_MIN:                return MTLBlendOperationMin;
+        case SG_BLENDOP_MAX:                return MTLBlendOperationMax;
         default: SOKOL_UNREACHABLE; return (MTLBlendOperation)0;
     }
 }
@@ -12623,18 +12546,6 @@ _SOKOL_PRIVATE MTLTextureType _sg_mtl_texture_type(sg_image_type t) {
     }
 }
 
-_SOKOL_PRIVATE bool _sg_mtl_is_pvrtc(sg_pixel_format fmt) {
-    switch (fmt) {
-        case SG_PIXELFORMAT_PVRTC_RGB_2BPP:
-        case SG_PIXELFORMAT_PVRTC_RGB_4BPP:
-        case SG_PIXELFORMAT_PVRTC_RGBA_2BPP:
-        case SG_PIXELFORMAT_PVRTC_RGBA_4BPP:
-            return true;
-        default:
-            return false;
-    }
-}
-
 _SOKOL_PRIVATE MTLSamplerAddressMode _sg_mtl_address_mode(sg_wrap w) {
     if (_sg.features.image_clamp_to_border) {
         if (@available(macOS 12.0, iOS 14.0, *)) {
@@ -12964,10 +12875,6 @@ _SOKOL_PRIVATE void _sg_mtl_init_caps(void) {
         _sg_pixelformat_sf(&_sg.formats[SG_PIXELFORMAT_BC7_RGBA]);
         _sg_pixelformat_sf(&_sg.formats[SG_PIXELFORMAT_BC7_SRGBA]);
     #else
-        _sg_pixelformat_sf(&_sg.formats[SG_PIXELFORMAT_PVRTC_RGB_2BPP]);
-        _sg_pixelformat_sf(&_sg.formats[SG_PIXELFORMAT_PVRTC_RGB_4BPP]);
-        _sg_pixelformat_sf(&_sg.formats[SG_PIXELFORMAT_PVRTC_RGBA_2BPP]);
-        _sg_pixelformat_sf(&_sg.formats[SG_PIXELFORMAT_PVRTC_RGBA_4BPP]);
         _sg_pixelformat_sf(&_sg.formats[SG_PIXELFORMAT_ETC2_RGB8]);
         _sg_pixelformat_sf(&_sg.formats[SG_PIXELFORMAT_ETC2_SRGB8]);
         _sg_pixelformat_sf(&_sg.formats[SG_PIXELFORMAT_ETC2_RGB8A1]);
@@ -13110,13 +13017,8 @@ _SOKOL_PRIVATE void _sg_mtl_copy_image_data(const _sg_image_t* img, __unsafe_unr
             const uint8_t* data_ptr = (const uint8_t*)data->subimage[face_index][mip_index].ptr;
             const int mip_width = _sg_miplevel_dim(img->cmn.width, mip_index);
             const int mip_height = _sg_miplevel_dim(img->cmn.height, mip_index);
-            // special case PVRTC formats: bytePerRow and bytesPerImage must be 0
-            int bytes_per_row = 0;
-            int bytes_per_slice = 0;
-            if (!_sg_mtl_is_pvrtc(img->cmn.pixel_format)) {
-                bytes_per_row = _sg_row_pitch(img->cmn.pixel_format, mip_width, 1);
-                bytes_per_slice = _sg_surface_pitch(img->cmn.pixel_format, mip_width, mip_height, 1);
-            }
+            int bytes_per_row = _sg_row_pitch(img->cmn.pixel_format, mip_width, 1);
+            int bytes_per_slice = _sg_surface_pitch(img->cmn.pixel_format, mip_width, mip_height, 1);
             /* bytesPerImage special case: https://developer.apple.com/documentation/metal/mtltexture/1515679-replaceregion
 
                 "Supply a nonzero value only when you copy data to a MTLTextureType3D type texture"
@@ -14655,10 +14557,6 @@ _SOKOL_PRIVATE WGPUTextureFormat _sg_wgpu_textureformat(sg_pixel_format p) {
         case SG_PIXELFORMAT_RG16SN:
         case SG_PIXELFORMAT_RGBA16:
         case SG_PIXELFORMAT_RGBA16SN:
-        case SG_PIXELFORMAT_PVRTC_RGB_2BPP:
-        case SG_PIXELFORMAT_PVRTC_RGB_4BPP:
-        case SG_PIXELFORMAT_PVRTC_RGBA_2BPP:
-        case SG_PIXELFORMAT_PVRTC_RGBA_4BPP:
             return WGPUTextureFormat_Undefined;
 
         default:
@@ -14704,6 +14602,8 @@ _SOKOL_PRIVATE WGPUBlendOperation _sg_wgpu_blendop(sg_blend_op op) {
         case SG_BLENDOP_ADD:                return WGPUBlendOperation_Add;
         case SG_BLENDOP_SUBTRACT:           return WGPUBlendOperation_Subtract;
         case SG_BLENDOP_REVERSE_SUBTRACT:   return WGPUBlendOperation_ReverseSubtract;
+        case SG_BLENDOP_MIN:                return WGPUBlendOperation_Min;
+        case SG_BLENDOP_MAX:                return WGPUBlendOperation_Max;
         default:
             SOKOL_UNREACHABLE;
             return WGPUBlendOperation_Force32;
@@ -17965,6 +17865,15 @@ _SOKOL_PRIVATE bool _sg_validate_pipeline_desc(const sg_pipeline_desc* desc) {
                 }
             }
         }
+        for (size_t color_index = 0; color_index < (size_t)desc->color_count; color_index++) {
+            const sg_blend_state* bs = &desc->colors[color_index].blend;
+            if ((bs->op_rgb == SG_BLENDOP_MIN) || (bs->op_rgb == SG_BLENDOP_MAX)) {
+                _SG_VALIDATE((bs->src_factor_rgb == SG_BLENDFACTOR_ONE) && (bs->dst_factor_rgb == SG_BLENDFACTOR_ONE), VALIDATE_PIPELINEDESC_BLENDOP_MINMAX_REQUIRES_BLENDFACTOR_ONE);
+            }
+            if ((bs->op_alpha == SG_BLENDOP_MIN) || (bs->op_alpha == SG_BLENDOP_MAX)) {
+                _SG_VALIDATE((bs->src_factor_alpha == SG_BLENDFACTOR_ONE) && (bs->dst_factor_alpha == SG_BLENDFACTOR_ONE), VALIDATE_PIPELINEDESC_BLENDOP_MINMAX_REQUIRES_BLENDFACTOR_ONE);
+            }
+        }
         return _sg_validate_end();
     #endif
 }
@@ -18685,12 +18594,20 @@ _SOKOL_PRIVATE sg_pipeline_desc _sg_pipeline_desc_defaults(const sg_pipeline_des
         cs->pixel_format = _sg_def(cs->pixel_format, _sg.desc.environment.defaults.color_format);
         cs->write_mask = _sg_def(cs->write_mask, SG_COLORMASK_RGBA);
         sg_blend_state* bs = &def.colors[i].blend;
-        bs->src_factor_rgb = _sg_def(bs->src_factor_rgb, SG_BLENDFACTOR_ONE);
-        bs->dst_factor_rgb = _sg_def(bs->dst_factor_rgb, SG_BLENDFACTOR_ZERO);
         bs->op_rgb = _sg_def(bs->op_rgb, SG_BLENDOP_ADD);
-        bs->src_factor_alpha = _sg_def(bs->src_factor_alpha, SG_BLENDFACTOR_ONE);
-        bs->dst_factor_alpha = _sg_def(bs->dst_factor_alpha, SG_BLENDFACTOR_ZERO);
+        bs->src_factor_rgb = _sg_def(bs->src_factor_rgb, SG_BLENDFACTOR_ONE);
+        if ((bs->op_rgb == SG_BLENDOP_MIN) || (bs->op_rgb == SG_BLENDOP_MAX)) {
+            bs->dst_factor_rgb = _sg_def(bs->dst_factor_rgb, SG_BLENDFACTOR_ONE);
+        } else {
+            bs->dst_factor_rgb = _sg_def(bs->dst_factor_rgb, SG_BLENDFACTOR_ZERO);
+        }
         bs->op_alpha = _sg_def(bs->op_alpha, SG_BLENDOP_ADD);
+        bs->src_factor_alpha = _sg_def(bs->src_factor_alpha, SG_BLENDFACTOR_ONE);
+        if ((bs->op_alpha == SG_BLENDOP_MIN) || (bs->op_alpha == SG_BLENDOP_MAX)) {
+            bs->dst_factor_alpha = _sg_def(bs->dst_factor_alpha, SG_BLENDFACTOR_ONE);
+        } else {
+            bs->dst_factor_alpha = _sg_def(bs->dst_factor_alpha, SG_BLENDFACTOR_ZERO);
+        }
     }
 
     for (int attr_index = 0; attr_index < SG_MAX_VERTEX_ATTRIBUTES; attr_index++) {

+ 2 - 2
sokol_log.h

@@ -123,7 +123,7 @@ extern "C" {
 
 /*
     Plug this function into the 'logger.func' struct item when initializing any of the sokol
-    headers. For instance for sokol_audio.h it would loom like this:
+    headers. For instance for sokol_audio.h it would look like this:
 
     saudio_setup(&(saudio_desc){
         .logger = {
@@ -245,7 +245,7 @@ EM_JS(void, slog_js_log, (uint32_t level, const char* c_str), {
         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) {

+ 5 - 1
tests/CMakeLists.txt

@@ -52,7 +52,11 @@ if (CMAKE_CXX_COMPILER_ID MATCHES "MSVC")
     set(cxx_flags ${cxx_flags} /W4 /WX /EHsc /D_CRT_SECURE_NO_WARNINGS)
 else()
     set(c_flags ${c_flags} -Wall -Wextra -Werror -Wsign-conversion -Wstrict-prototypes)
-    set(cxx_flags ${cxx_flags} -Wall -Wextra -Werror -Wsign-conversion -fno-rtti -fno-exceptions)
+    # GCC complains about -Wextra-semi in the C compiler, only accepts it for C++
+    if (CMAKE_CXX_COMPILER_ID MATCHES "Clang")
+        set(c_flags ${c_flags} -Wextra-semi)
+    endif()
+    set(cxx_flags ${cxx_flags} -Wall -Wextra -Werror -Wsign-conversion -fno-rtti -fno-exceptions -Wextra-semi)
     if (CMAKE_CXX_COMPILER_ID MATCHES "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
         set(c_flags ${c_flags} -Wno-missing-field-initializers)
         set(cxx_flags ${cxx_flags} -Wno-missing-field-initializers)

+ 0 - 4
tests/functional/sokol_gfx_test.c

@@ -2917,10 +2917,6 @@ UTEST(sokol_gfx, query_pixelformat_bytesperpixel) {
     T(sg_query_pixelformat(SG_PIXELFORMAT_BC6H_RGBF).bytes_per_pixel == 0);
     T(sg_query_pixelformat(SG_PIXELFORMAT_BC6H_RGBUF).bytes_per_pixel == 0);
     T(sg_query_pixelformat(SG_PIXELFORMAT_BC7_RGBA).bytes_per_pixel == 0);
-    T(sg_query_pixelformat(SG_PIXELFORMAT_PVRTC_RGB_2BPP).bytes_per_pixel == 0);
-    T(sg_query_pixelformat(SG_PIXELFORMAT_PVRTC_RGB_4BPP).bytes_per_pixel == 0);
-    T(sg_query_pixelformat(SG_PIXELFORMAT_PVRTC_RGBA_2BPP).bytes_per_pixel == 0);
-    T(sg_query_pixelformat(SG_PIXELFORMAT_PVRTC_RGBA_4BPP).bytes_per_pixel == 0);
     T(sg_query_pixelformat(SG_PIXELFORMAT_ETC2_RGB8).bytes_per_pixel == 0);
     T(sg_query_pixelformat(SG_PIXELFORMAT_ETC2_RGB8A1).bytes_per_pixel == 0);
     T(sg_query_pixelformat(SG_PIXELFORMAT_ETC2_RGBA8).bytes_per_pixel == 0);

+ 1 - 1
tests/functional/sokol_test.c

@@ -4,4 +4,4 @@
 //------------------------------------------------------------------------------
 #include "utest.h"
 
-UTEST_MAIN();
+UTEST_MAIN()

+ 2 - 4
util/sokol_gfx_imgui.h

@@ -1270,10 +1270,6 @@ _SOKOL_PRIVATE const char* _sgimgui_pixelformat_string(sg_pixel_format fmt) {
         case SG_PIXELFORMAT_BC6H_RGBF: return "SG_PIXELFORMAT_BC6H_RGBF";
         case SG_PIXELFORMAT_BC6H_RGBUF: return "SG_PIXELFORMAT_BC6H_RGBUF";
         case SG_PIXELFORMAT_BC7_RGBA: return "SG_PIXELFORMAT_BC7_RGBA";
-        case SG_PIXELFORMAT_PVRTC_RGB_2BPP: return "SG_PIXELFORMAT_PVRTC_RGB_2BPP";
-        case SG_PIXELFORMAT_PVRTC_RGB_4BPP: return "SG_PIXELFORMAT_PVRTC_RGB_4BPP";
-        case SG_PIXELFORMAT_PVRTC_RGBA_2BPP: return "SG_PIXELFORMAT_PVRTC_RGBA_2BPP";
-        case SG_PIXELFORMAT_PVRTC_RGBA_4BPP: return "SG_PIXELFORMAT_PVRTC_RGBA_4BPP";
         case SG_PIXELFORMAT_ETC2_RGB8: return "SG_PIXELFORMAT_ETC2_RGB8";
         case SG_PIXELFORMAT_ETC2_RGB8A1: return "SG_PIXELFORMAT_ETC2_RGB8A1";
         case SG_PIXELFORMAT_ETC2_RGBA8: return "SG_PIXELFORMAT_ETC2_RGBA8";
@@ -1437,6 +1433,8 @@ _SOKOL_PRIVATE const char* _sgimgui_blendop_string(sg_blend_op op) {
         case SG_BLENDOP_ADD:                return "SG_BLENDOP_ADD";
         case SG_BLENDOP_SUBTRACT:           return "SG_BLENDOP_SUBTRACT";
         case SG_BLENDOP_REVERSE_SUBTRACT:   return "SG_BLENDOP_REVERSE_SUBTRACT";
+        case SG_BLENDOP_MIN:                return "SG_BLENDOP_MIN";
+        case SG_BLENDOP_MAX:                return "SG_BLENDOP_MAX";
         default:                            return "???";
     }
 }

+ 1 - 1
util/sokol_imgui.h

@@ -2233,7 +2233,7 @@ EM_JS(int, simgui_js_is_osx, (void), {
     } else {
         return 0;
     }
-});
+})
 #endif
 
 // ██       ██████   ██████   ██████  ██ ███    ██  ██████

+ 1 - 1
util/sokol_nuklear.h

@@ -2215,7 +2215,7 @@ EM_JS(int, snk_js_is_osx, (void), {
     } else {
         return 0;
     }
-});
+})
 #endif
 
 static bool _snk_is_osx(void) {