Texturing overhaul: GPU compression, sRGB sampling, swizzles, etc. (#240)

* WIP compressed textures, swizzles, sRGB sampling, ...

* refactor texture type info & fix random bugs

* fix preprocessing of sRGB textures

* handle y-flipped basis textures

* glcommon: better WebGL compat for compressed format detection

* missed WEBGL_compressed_texture_pvrtc

* implement compressed texture xcoding and uploading

* Add basis_universal submodule

* Reorganize texture loader code

Clean up some code
Isolate Basis Universal loader into a separate module

* Add wrapper script for encoding .basis textures

* basisu: honor custom metadata written by the mkbasis.py script

* mkbasis.py: add --incredibly-slow and --dry-run

* Move pixmap code from util/ to pixmap/

* Add an on-disk transcode cache for basis textures to speed up loads

* Compress texture cache with zlib

* Use readable format names for basisu cache filenames

* basisu: mip bias test code

* basisu: small caching cleanup

* add TAISEI_BASISU_MIP_BIAS env variable

* Improve OpenGL format matching heuristics

* Document considerations for compressed format priority

* Remove dead code

* Enable two forgotten formats, BC3_RGBA and ATC_RGBA

Also prefer BC7 over BC1/BC3

* Recognize GL_ANGLE_compressed_texture_etc for ETC2 textures

* Default depth buffers to 24-bit; remove ANGLE hack

* Fix glcommon_check_extension for GLES2/legacy gl

* Add renderer feature bit for texture swizzle masks

* glcommon: Fixup internal formats for GLES2

Sized internal formats are not allowed in GLES2

* Fix emscripten compile errors

* Update basis_universal

* remove more dead code

* revert irrelevant stage4 change

* shut up UBSan

* basisu: shut up some debug spam

* Add normalmap sampling helper to util.glslh

* basisu: add a gray-alpha mode

* mkbasis.py: Abort if image dimansions aren't multiples of 4

* Add basic Basis Universal encoding documentation (WIP)

* doc/basisu: Add paragraph about modes; minor tweaks

* basisu: workarounds for GL texture size requirements

* gles20: fix uncompressed sRGB formats

* Partial workaround for missing swizzles in gles2 and webgl

* remove invalid assertion

* New renderer API to expose glDrawBuffers-like functionality

* stagedraw: disable all color outputs for copy_depth pass

required for WebGL compatibility

* support GL_ANGLE_request_extension

* emscripten: include *.basis in gfx package

Also fix a potential problem when more than one .pkgdir is used to
construct emscripten packages

* Don't rely on emscripten runtime to enable webgl extensions
This commit is contained in:
Andrei Alexeyev 2020-08-15 14:51:12 +03:00 committed by GitHub
parent 324dccb79a
commit a5fd6fe5d9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
83 changed files with 5330 additions and 1561 deletions

6
.gitmodules vendored
View file

@ -8,4 +8,8 @@
branch = master
[submodule "external/koishi"]
path = external/koishi
url = https://github.com/taisei-project/koishi
url = https://github.com/taisei-project/koishi.git
[submodule "external/basis_universal"]
path = external/basis_universal
url = https://github.com/taisei-project/basis_universal.git
branch = taisei

142
doc/BASISU.rst Normal file
View file

@ -0,0 +1,142 @@
Basis Universal
===============
.. contents::
Intro
-----
`Basis Universal <https://github.com/taisei-project/basis_universal>`__ is a GPU texture compression system with ability
to transcode images into a wide variety of formats at runtime from a single source. This document explains how to author
Basis Universal (``.basis``) textures for Taisei, and highlights limitations and caveats of our Basis support.
First-time setup
----------------
Step 1: The encoder
~~~~~~~~~~~~~~~~~~~
Compile the encoder and put it somewhere in your ``PATH``. Assuming you have a working and up to date clone of the
Taisei repository, including submodules, this can be done like so on Linux:
.. code:: sh
cd subprojects/basis_universal
meson setup --buildtype=release -Db_lto=true -Dcpp_args=-march=native build
meson compile -C build basisu
ln -s $PWD/build/basisu ~/.local/bin
Verify that the encoder is working by running ``basisu``. It should print a long list of options. If the command is not
found, make sure ``~/.local/bin`` is in your ``PATH``, or choose another directory that is.
The optimization options in ``meson setup`` are optional but highly recommended, as the encoding process can be quite
slow.
It's also possible to use `the upstream encoder <https://github.com/BinomialLLC/basis_universal>`__, which may be
packaged by your distribution. However, this is not recommended. As of 2020-08-06, the upstream encoder is missing some
important performance optimizations; see
`BinomialLLC/basis_universal#105 <https://github.com/BinomialLLC/basis_universal/pull/105>`__
`BinomialLLC/basis_universal#112 <https://github.com/BinomialLLC/basis_universal/pull/112>`__
`BinomialLLC/basis_universal#113 <https://github.com/BinomialLLC/basis_universal/pull/113>`__.
Step 2: The wrapper
~~~~~~~~~~~~~~~~~~~
The ``mkbasis`` wrapper script is what you'll actually use to create ``.basis`` files. Simply symlink it into your
``PATH``:
.. code:: sh
ln -s $PWD/scripts/mkbasis.py ~/.local/bin/mkbasis
Verify that it works by running ``mkbasis``.
Encoding TL;DR
--------------
Encode a **diffuse or ambient map** (sRGB data, decoded to linear when sampled in a shader):
.. code:: sh
# Outputs to foo.basis
mkbasis foo.png
# Outputs to /path/to/bar.basis
mkbasis foo.png -o /path/to/bar.basis
Encode a **tangent-space normal map** (special case):
.. code:: sh
mkbasis foo.png --normal
Encode a **roughness map** (single-channel linear data):
.. code:: sh
mkbasis foo.png --channels=r --linear
# Equivalent to:
mkbasis foo.png --r --linear
Encode **RGBA** color data and **pre-multiply alpha**:
.. code:: sh
mkbasis foo.png --channels=rgba
# Equivalent to:
mkbasis foo.png --rgba
Encode **Gray+Alpha** data and **pre-multiply alpha**:
.. code:: sh
mkbasis foo.png --channels=gray-alpha
# Equivalent to:
mkbasis foo.png --gray-alpha
Do **not** pre-multiply alpha:
.. code:: sh
mkbasis foo.png --no-multiply-alpha
Sacrifice quality to speed up the encoding process:
.. code:: sh
mkbasis foo.png --fast
For a complete list of options and their default values, see
.. code:: sh
mkbasis --help
Encoding details
----------------
Encoding modes
~~~~~~~~~~~~~~
Basis Universal supports two very different encoding modes: ETC1S and UASTC. The primary difference between the two is
the size/quality trade-off.
ETC1S is the default mode. It offers medium/low quality and excellent compression.
UASTC has significantly higher quality, but much larger file sizes. UASTC-encoded Basis files must also be additionally
compressed with an LZ-based scheme, such as deflate (zlib). Zopfli-compressed UASTC files are roughly 4 times as large
as their ETC1S equivalents (including mipmaps), comparable to the source file stored with lossless PNG or WebP
compression.
Although UASTC should theoretically work, it has not been well tested with Taisei yet. The ``mkbasis`` wrapper also does
not apply LZ compression to UASTC files automatically yet, and Taisei wouldn't pick them up either (unless they are
stored compressed inside of a ``.zip`` package). If you want to use UASTC nonetheless, pass ``--uastc`` to ``mkbasis``.
*TODO*
Caveats and limitations
-----------------------
*TODO*

View file

@ -28,11 +28,20 @@ var glContext = canvasElement.getContext('webgl2', {
'stencil' : false,
});
// This actually enables the extension. Galaxy-brain API right here.
glContext.getExtension('WEBGL_debug_renderer_info');
if(!glContext) {
throw "Could not create a WebGL 2 context";
}
// glContext = WebGLDebugUtils.makeDebugContext(glContext);
// WebGL extensions have to be explicitly enabled to make the functionality available.
// Note that Emscripten will always report all *supported* extensions in GL_EXTENSIONS,
// regardless of whether they are enabled or not. This is non-conformant from the GLES
// perspective. The easiest way to fix that is to enable all of them here.
glContext.getSupportedExtensions().forEach(function(ext) {
glContext.getExtension(ext);
});
canvasElement.addEventListener("webglcontextlost", function(e) {
alert('WebGL context lost. You will need to reload the page.');
e.preventDefault();

1
external/basis_universal vendored Submodule

@ -0,0 +1 @@
Subproject commit 8abe8b36d5c2fae27a876a2238e0fa07b6c3ec38

View file

@ -148,11 +148,13 @@ dep_crypto = dependency('libcrypto', required : f
dep_m = cc.find_library('m', required : false)
dep_basisu_transcoder = subproject('basis_universal').get_variable('basisu_transcoder_dep')
dep_cglm = subproject('cglm').get_variable('cglm_dep')
dep_glad = subproject('glad').get_variable('glad_dep')
dep_koishi = subproject('koishi').get_variable('koishi_dep')
taisei_deps = [
dep_basisu_transcoder,
dep_cglm,
dep_freetype,
dep_koishi,
@ -252,6 +254,8 @@ else
endif
config.set('TAISEI_BUILDCONF_USE_GNU_EXTENSIONS', get_option('use_gnu_ext'))
config.set('TAISEI_BUILDCONF_HAVE_BUILTIN_POPCOUNTLL', cc.has_function('__builtin_popcountll'))
config.set('TAISEI_BUILDCONF_HAVE_BUILTIN_POPCOUNT', cc.has_function('__builtin_popcount'))
prefer_relpath_systems = [
'windows',

View file

@ -84,6 +84,12 @@ vec3 hueShift(vec3 c, float s) {
return hsv2rgb(hsv);
}
vec3 sample_normalmap(sampler2D src, vec2 uv) {
vec2 xy = texture(src, uv).rg * 2 - 1;
float z = sqrt(1 - xy.x * xy.x - xy.y * xy.y);
return vec3(xy, z);
}
float flip_topleft_to_native(float x) {
#ifdef NATIVE_ORIGIN_BOTTOMLEFT
return 1 - x;

View file

@ -7,6 +7,7 @@ UNIFORM(1) sampler2D alphamap;
UNIFORM(2) int linearize;
UNIFORM(3) int multiply_alpha;
UNIFORM(4) int apply_alphamap;
UNIFORM(5) int write_srgb;
/*
* Adapted from https://github.com/tobspr/GLSL-Color-Spaces/blob/master/ColorSpaces.inc.glsl
@ -22,6 +23,14 @@ float srgb_to_linear(float channel) {
}
}
float linear_to_srgb(float channel) {
if(channel <= 0.0031308) {
return 12.92 * channel;
} else {
return (1.0 + SRGB_ALPHA) * pow(channel, 1.0/2.4) - SRGB_ALPHA;
}
}
void main(void) {
fragColor = textureLod(tex, texCoordRaw, 0);
@ -40,4 +49,12 @@ void main(void) {
if(apply_alphamap != 0) {
fragColor.a *= textureLod(alphamap, texCoordRaw, 0).r;
}
if(write_srgb != 0) {
fragColor.rgb = vec3(
linear_to_srgb(fragColor.r),
linear_to_srgb(fragColor.g),
linear_to_srgb(fragColor.b)
);
}
}

View file

@ -16,6 +16,7 @@ if host_machine.system() == 'emscripten'
em_bundle_gfx_patterns = [
'gfx/*.png',
'gfx/*.webp',
'gfx/*.basis',
'fonts/*.ttf',
'fonts/*.otf',
]
@ -66,13 +67,15 @@ foreach pkg : packages
assert(glob_result.returncode() == 0, 'Glob script failed')
foreach file : glob_result.stdout().strip().split('\n')
fpath = join_paths(meson.current_source_dir(), pkg_pkgdir, file)
if file != ''
fpath = join_paths(meson.current_source_dir(), pkg_pkgdir, file)
set_variable(var_files, get_variable(var_files) + [fpath])
set_variable(var_files, get_variable(var_files) + [fpath])
set_variable(var_packer_args, get_variable(var_packer_args) + [
'--preload', '@0@@@1@/@2@'.format(fpath, em_data_prefix, file)
])
set_variable(var_packer_args, get_variable(var_packer_args) + [
'--preload', '@0@@@1@/@2@'.format(fpath, em_data_prefix, file)
])
endif
endforeach
endforeach
elif package_data

429
scripts/mkbasis.py Executable file
View file

@ -0,0 +1,429 @@
#!/usr/bin/env python3
from taiseilib.common import (
add_common_args,
run_main,
)
from pathlib import Path
from tempfile import TemporaryDirectory
import argparse
import subprocess
import sys
BASISU_TAISEI_ID = 0x52656900
BASISU_TAISEI_CHANNELS = ('r', 'rg', 'rgb', 'rgba', 'gray-alpha')
BASISU_TAISEI_CHANNELS_MASK = 0x3
BASISU_TAISEI_SRGB = (BASISU_TAISEI_CHANNELS_MASK + 1) << 0
BASISU_TAISEI_NORMALMAP = (BASISU_TAISEI_CHANNELS_MASK + 1) << 1
BASISU_TAISEI_GRAYALPHA = (BASISU_TAISEI_CHANNELS_MASK + 1) << 2
def channels_have_alpha(chans):
return chans in ('rgba', 'gray-alpha')
def format_cmd(cmd):
s = []
for a in [str(x) for x in cmd]:
if repr(a) != f"'{a}'" or ' ' in a:
a = repr(a)
s.append(a)
return ' '.join(s)
def run(args, cmd):
print('RUN: ' + format_cmd(cmd))
if not args.dry_run:
subprocess.check_call(cmd)
def image_size(img_path):
o = subprocess.check_output(['convert', img_path, '-print', '%wx%h', 'null:'])
return tuple(int(d) for d in o.strip().decode('utf8').split('x'))
def preprocess(args, tempdir):
cmd = [
'convert',
'-verbose',
args.input
]
if args.channels == 'gray-alpha':
cmd += [
'-colorspace', 'gray'
]
if channels_have_alpha(args.channels):
if args.multiply_alpha:
cmd += [
'(',
'+clone',
'-background', 'black',
'-alpha', 'remove',
')',
'+swap',
'-compose', 'copy_opacity',
'-composite',
]
if args.alphamap:
cmd += [
'(',
'+clone',
'-channel', 'a',
'-separate',
args.alphamap_path,
'-colorspace', 'gray',
'-compose', 'multiply',
'-composite',
')',
'-compose', 'copy_opacity',
'-composite',
]
elif args.channels == 'r':
cmd += [
'-alpha', 'off',
'-channel', 'r',
'-separate',
'-colorspace', 'gray'
]
if cmd[-1] != args.input or args.input.suffix not in ('.png', '.jpg'):
dst = tempdir / 'preprocessed.png'
cmd += [dst]
run(args, cmd)
return dst
return args.input
def process(args):
width, height = image_size(args.input)
if width % 4 != 0 or height % 4 != 0:
print(f'{args.input}: image dimensions are not multiples of 4 ({width}x{height})', file=sys.stderr)
exit(1)
with TemporaryDirectory() as tempdir:
tempdir = Path(tempdir)
img = preprocess(args, tempdir)
cmd = [
args.basisu,
'-file', img,
'-output_file', args.output,
]
if args.channels == 'gray-alpha':
taisei_flags = BASISU_TAISEI_CHANNELS.index('rg') | BASISU_TAISEI_GRAYALPHA
else:
taisei_flags = BASISU_TAISEI_CHANNELS.index(args.channels)
assert taisei_flags == taisei_flags & BASISU_TAISEI_CHANNELS_MASK
if args.y_flip:
cmd += ['-y_flip']
if args.normal:
taisei_flags |= BASISU_TAISEI_NORMALMAP
cmd += ['-normal_map']
if args.srgb_sampling:
taisei_flags |= BASISU_TAISEI_SRGB
if not args.srgb:
cmd += ['-linear']
if args.mipmaps:
cmd += ['-mipmap']
if args.srgb:
cmd += ['-mip_srgb']
else:
cmd += ['-mip_linear']
if args.normal:
cmd += ['-mip_scale', '0.5']
if args.channels == 'rg':
cmd += ['-separate_rg_to_color_alpha']
if not channels_have_alpha(args.channels):
cmd += ['-no_alpha']
cmd += [
'-userdata0', str(taisei_flags),
'-userdata1', str(BASISU_TAISEI_ID)
]
if args.uastc:
cmd += ['-uastc']
profiles = {
'slow': [
'-uastc_level', '3',
'-uastc_rdo_q', '4',
],
'fast': [
'-uastc_level', '1',
],
'incredibly_slow': [
'-uastc_level', '4',
'-uastc_rdo_q', '4',
],
}
else:
profiles = {
'slow': [
'-comp_level', '4',
'-q', '255'
],
'fast': [],
'incredibly_slow': [
'-comp_level', '5',
'-max_endpoints', '16128',
'-max_selectors', '16128',
'-no_selector_rdo',
'-no_endpoint_rdo',
],
}
cmd += profiles[args.profile]
"""
if args.basisu_args is not None:
cmd += args.basisu_args
"""
run(args, cmd)
if args.uastc and not args.dry_run:
print('\nNOTE: UASTC textures must be additionally compressed with a general-purpose lossless algorithm!')
def main(args):
parser = argparse.ArgumentParser(
description='Generate a Basis Universal compressed texture from image.',
prog=args[0]
)
def make_action(func):
class ActionWrapper(argparse.Action):
def __call__(self, parser, namespace, values, option_string):
return func(parser, namespace, values, option_string)
return ActionWrapper
def normal(parser, namespace, *a):
namespace.channels = 'rg'
namespace.normal = True
namespace.srgb = False
image_suffixes = ('.webp', '.png')
parser.add_argument('input',
help='the input image file',
type=Path,
)
parser.add_argument('-o', '--output',
help='the output .basis file',
type=Path,
)
parser.add_argument('--basisu',
help='Basis Universal encoder command (default: basisu)',
metavar='COMMAND',
default='basisu',
)
parser.add_argument('--mipmaps',
dest='mipmaps',
help='generate mipmaps (default)',
action='store_true',
default=True,
)
parser.add_argument('--no-mipmaps',
dest='mipmaps',
help="don't generate mipmaps",
action='store_false',
default=True,
)
parser.add_argument('--srgb',
dest='srgb',
help='treat input as sRGB encoded (default)',
action='store_true',
default=True,
)
parser.add_argument('--linear',
dest='srgb',
help='treat input as linear data, disable sRGB sampling',
action='store_false',
)
parser.add_argument('--srgb-sampling',
dest='srgb_sampling',
help='enable sRGB decoding when sampling texture; ignored unless --srgb is active (default)',
action='store_true',
default=True,
)
parser.add_argument('--no-srgb-sampling',
dest='srgb_sampling',
help='disable sRGB decoding when sampling texture; ignored unless --srgb is active',
action='store_false',
)
parser.add_argument('--force-srgb-sampling',
help="force sRGB decoding of linear data when sampling; ignored unless --linear is active (you probably don't want this!)",
action='store_true',
)
channels_default = 'rgb'
parser.add_argument('-c', '--channels',
help=f'which input channels must be preserved (default: {channels_default})',
default=channels_default,
choices=BASISU_TAISEI_CHANNELS,
)
for ch in BASISU_TAISEI_CHANNELS:
parser.add_argument(f'--{ch}',
dest='channels',
help=f'alias for --channels={ch}{" (default)" if ch == channels_default else ""}',
action='store_const',
const=ch,
)
parser.add_argument('--normal',
help='treat texture as a tangent space normalmap; implies --linear --channels=rg',
action=make_action(normal),
nargs=0,
)
parser.add_argument('--multiply-alpha',
dest='multiply_alpha',
help='premultiply color channels with alpha; ignored unless --channels=rgba (default)',
action='store_true',
default=True,
)
parser.add_argument('--no-multiply-alpha',
dest='multiply_alpha',
help="don't premultiply color channels with alpha; ignored unless --channels=rgba",
action='store_false',
)
parser.add_argument('--alphamap',
dest='alphamap',
help='multiply alpha channel with a custom grayscale image; done after alpha-premultiplication; ignored unless --channels=rgba (default)',
action='store_true',
default=True,
)
parser.add_argument('--no-alphamap',
dest='alphamap',
help="don't multiply alpha channel with a custom grayscale image; ignored unless --channels=rgba",
action='store_false',
)
parser.add_argument('--alphamap-path',
help=f'path to alphamap image; defaults to {{input_basename}}.alphamap.{{{",".join(x[1:] for x in image_suffixes)}}} if exists',
default=None,
type=Path,
)
parser.add_argument('--y-flip',
dest='y_flip',
help='flip the texture vertically (default)',
action='store_true',
default=True,
)
parser.add_argument('--no-y-flip',
dest='y_flip',
help="don't flip the texture vertically",
action='store_false',
default=True,
)
parser.add_argument('--slow',
dest='profile',
help='compress slowly, emphasize quality (default)',
action='store_const',
const='slow',
default='slow',
)
parser.add_argument('--fast',
dest='profile',
help='compress much faster, significantly lower quality',
action='store_const',
const='fast',
)
parser.add_argument('--incredibly-slow',
dest='profile',
help='takes forever, maximum quality',
action='store_const',
const='incredibly_slow',
)
parser.add_argument('--etc1s',
dest='uastc',
help='encode to ETC1S: small size, low/mediocre quality (default)',
action='store_false',
default=False,
)
parser.add_argument('--uastc',
dest='uastc',
help='encode to UASTC: large size, high quality',
action='store_true',
)
parser.add_argument('--dry-run',
help='do nothing, print commands that would have been run',
action='store_true',
)
# FIXME this doesn't work properly (goddamn argparse)
"""
parser.add_argument('--basisu-args',
help='pass remaining args through to the encoder',
nargs='*',
)
"""
args = parser.parse_args(args[1:])
if channels_have_alpha(args.channels) and args.alphamap and args.alphamap_path is None:
try:
for s in image_suffixes:
p = args.input.with_suffix(f'.alphamap{s}')
if p.is_file():
args.alphamap_path = p
break
except ValueError:
pass
args.alphamap = args.alphamap_path is not None
if args.output is None:
args.output = args.input.with_suffix('.basis')
if not args.srgb:
args.srgb_sampling = args.force_srgb_sampling
# print(args)
process(args)
if __name__ == '__main__':
run_main(main)

View file

@ -121,7 +121,7 @@ void log_set_gui_error_appendix(const char *message);
#undef UNREACHABLE
#define UNREACHABLE log_fatal("This code should never be reached (%s:%i)", __FILE__, __LINE__)
#else
#define log_debug(...)
#define log_debug(...) ((void)0)
#define LOG_NO_FILENAMES
#endif

View file

@ -123,6 +123,7 @@ subdir('audio')
subdir('dialog')
subdir('eventloop')
subdir('menu')
subdir('pixmap')
subdir('plrmodes')
subdir('renderer')
subdir('resource')
@ -138,6 +139,7 @@ taisei_src += [
dialog_src,
eventloop_src,
menu_src,
pixmap_src,
plrmodes_src,
renderer_src,
resource_src,
@ -167,21 +169,22 @@ if host_machine.system() == 'emscripten'
em_link_args = [
'-O@0@'.format(get_option('optimization')),
'-s', 'ALLOW_MEMORY_GROWTH=1',
'-s', 'DEFAULT_LIBRARY_FUNCS_TO_INCLUDE=["$autoResumeAudioContext"]',
'-s', 'ENVIRONMENT=web',
'-s', 'EXIT_RUNTIME=0',
'-s', 'EXPORTED_FUNCTIONS=["_main", "_vfs_sync_callback"]',
'-s', 'EXTRA_EXPORTED_RUNTIME_METHODS=["ccall"]',
'-s', 'DEFAULT_LIBRARY_FUNCS_TO_INCLUDE=["$autoResumeAudioContext"]',
'-s', 'FILESYSTEM=1',
'-s', 'FORCE_FILESYSTEM=1',
'-s', 'GL_POOL_TEMP_BUFFERS=0',
'-s', 'GL_PREINITIALIZED_CONTEXT=1',
'-s', 'GL_SUPPORT_AUTOMATIC_ENABLE_EXTENSIONS=0',
'-s', 'INITIAL_MEMORY=268435456',
'-s', 'LZ4=1',
'-s', 'MAX_WEBGL_VERSION=2',
'-s', 'MIN_WEBGL_VERSION=2',
'-s', 'MODULARIZE=0',
'-s', 'STRICT=1',
'-s', 'INITIAL_MEMORY=268435456',
'-s', 'WASM=1',
'-lGL',
'-legl.js',

View file

@ -8,9 +8,9 @@
#include "taisei.h"
#include "util.h"
#include "loaders.h"
#include "../pngcruft.h"
#include "util.h"
#include "util/pngcruft.h"
static uchar png_magic[] = { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A };
@ -112,12 +112,12 @@ static bool px_png_load(SDL_RWops *stream, Pixmap *pixmap, PixmapFormat preferre
size_t pixel_size = PIXMAP_FORMAT_PIXEL_SIZE(pixmap->format);
if(pixmap->height > PNG_SIZE_MAX/(pixmap->width * pixel_size)) {
if(pixmap->height > PIXMAP_BUFFER_MAX_SIZE / (pixmap->width * pixel_size)) {
error = "The image is too large";
goto done;
}
png_bytep buffer = pixmap->data.untyped = pixmap_alloc_buffer_for_copy(pixmap);
png_bytep buffer = pixmap->data.untyped = pixmap_alloc_buffer_for_copy(pixmap, &pixmap->data_size);
for(int pass = 0; pass < num_passes; ++pass) {
for(int row = pixmap->height - 1; row >= 0; --row) {

View file

@ -98,18 +98,18 @@ static bool px_webp_load(SDL_RWops *stream, Pixmap *pixmap, PixmapFormat preferr
size_t pixel_size = PIXMAP_FORMAT_PIXEL_SIZE(pixmap->format);
if(pixmap->height > ((size_t)-1)/(pixmap->width * pixel_size)) {
if(pixmap->height > PIXMAP_BUFFER_MAX_SIZE / (pixmap->width * pixel_size)) {
WebPIDelete(idec);
log_error("The image is too large");
return false;
}
pixmap->data.untyped = pixmap_alloc_buffer_for_copy(pixmap);
pixmap->data.untyped = pixmap_alloc_buffer_for_copy(pixmap, &pixmap->data_size);
config.output.is_external_memory = true;
config.output.colorspace = features.has_alpha ? MODE_RGBA : MODE_RGB;
config.output.u.RGBA.rgba = pixmap->data.untyped;
config.output.u.RGBA.size = pixmap_data_size(pixmap);
config.output.u.RGBA.size = pixmap->data_size;
config.output.u.RGBA.stride = pixel_size * pixmap->width;
do {

View file

@ -6,8 +6,8 @@
* Copyright (c) 2012-2019, Andrei Alexeyev <akari@taisei-project.org>.
*/
#ifndef IGUARD_util_pixmap_loaders_loaders_h
#define IGUARD_util_pixmap_loaders_loaders_h
#ifndef IGUARD_pixmap_loaders_loaders_h
#define IGUARD_pixmap_loaders_loaders_h
#include "taisei.h"
@ -24,4 +24,4 @@ typedef struct PixmapLoader {
extern PixmapLoader pixmap_loader_png;
extern PixmapLoader pixmap_loader_webp;
#endif // IGUARD_util_pixmap_loaders_loaders_h
#endif // IGUARD_pixmap_loaders_loaders_h

8
src/pixmap/meson.build Normal file
View file

@ -0,0 +1,8 @@
pixmap_src = files(
'pixmap.c',
'serialize.c',
)
subdir('loaders')
pixmap_src += pixmap_loaders_src

View file

@ -9,8 +9,8 @@
#include "taisei.h"
#include "pixmap.h"
#include "loaders/loaders.h"
#include "util.h"
#include "pixmap_loaders/loaders.h"
// NOTE: this is pretty stupid and not at all optimized, patches welcome
@ -134,7 +134,7 @@ typedef void (*convfunc_t)(
size_t num_pixels,
void *vbuf_in,
void *vbuf_out,
void *default_pixel
int swizzle[4]
);
#define CONV(in, in_depth, out, out_depth) { convert_##in##_to_##out, in_depth, out_depth }
@ -179,35 +179,32 @@ static struct conversion_def* find_conversion(uint depth_in, uint depth_out) {
log_fatal("Pixmap conversion for %upbc -> %upbc undefined, please add", depth_in, depth_out);
}
static void *default_pixel(uint depth) {
static uint8_t default_u8[] = { 0, 0, 0, UINT8_MAX };
static uint16_t default_u16[] = { 0, 0, 0, UINT16_MAX };
static uint32_t default_u32[] = { 0, 0, 0, UINT32_MAX };
static float default_f32[] = { 0, 0, 0, 1.0f };
switch(depth) {
case 8: return default_u8;
case 16: return default_u16;
case 32: return default_u32;
case 32 | DEPTH_FLOAT_BIT: return default_f32;
default: UNREACHABLE;
}
}
void *pixmap_alloc_buffer(PixmapFormat format, size_t width, size_t height) {
static uint32_t pixmap_data_size(PixmapFormat format, uint32_t width, uint32_t height) {
assert(width >= 1);
assert(height >= 1);
size_t pixel_size = PIXMAP_FORMAT_PIXEL_SIZE(format);
uint64_t pixel_size = PIXMAP_FORMAT_PIXEL_SIZE(format);
assert(pixel_size >= 1);
return calloc(width * height, pixel_size);
uint64_t s = (uint64_t)width * (uint64_t)height * pixel_size;
assert(s <= PIXMAP_BUFFER_MAX_SIZE);
return s;
}
void *pixmap_alloc_buffer_for_copy(const Pixmap *src) {
return pixmap_alloc_buffer(src->format, src->width, src->height);
void *pixmap_alloc_buffer(PixmapFormat format, uint32_t width, uint32_t height, uint32_t *out_bufsize) {
uint32_t s = pixmap_data_size(format, width, height);
if(out_bufsize) {
*out_bufsize = s;
}
return calloc(1, s);
}
void *pixmap_alloc_buffer_for_conversion(const Pixmap *src, PixmapFormat format) {
return pixmap_alloc_buffer(format, src->width, src->height);
void *pixmap_alloc_buffer_for_copy(const Pixmap *src, uint32_t *out_bufsize) {
return pixmap_alloc_buffer(src->format, src->width, src->height, out_bufsize);
}
void *pixmap_alloc_buffer_for_conversion(const Pixmap *src, PixmapFormat format, uint32_t *out_bufsize) {
return pixmap_alloc_buffer(format, src->width, src->height, out_bufsize);
}
static void pixmap_copy_meta(const Pixmap *src, Pixmap *dst) {
@ -242,12 +239,50 @@ void pixmap_convert(const Pixmap *src, Pixmap *dst, PixmapFormat format) {
num_pixels,
src->data.untyped,
dst->data.untyped,
default_pixel(PIXMAP_FORMAT_DEPTH(dst->format) | (PIXMAP_FORMAT_IS_FLOAT(dst->format) * DEPTH_FLOAT_BIT))
NULL
);
}
static int swizzle_idx(char s) {
switch(s) {
case 'r': return 0;
case 'g': return 1;
case 'b': return 2;
case 'a': return 3;
case '0': return 4;
case '1': return 5;
default: UNREACHABLE;
}
}
void pixmap_swizzle_inplace(Pixmap *px, SwizzleMask swizzle) {
uint channels = pixmap_format_layout(px->format);
swizzle = swizzle_canonize(swizzle);
if(!swizzle_is_significant(swizzle, channels)) {
return;
}
uint cvt_id = pixmap_format_depth(px->format) | (pixmap_format_is_float(px->format) * DEPTH_FLOAT_BIT);
struct conversion_def *cv = find_conversion(cvt_id, cvt_id);
cv->func(
channels,
channels,
px->width * px->height,
px->data.untyped,
px->data.untyped,
(int[]) {
swizzle_idx(swizzle.r),
swizzle_idx(swizzle.g),
swizzle_idx(swizzle.b),
swizzle_idx(swizzle.a),
}
);
}
void pixmap_convert_alloc(const Pixmap *src, Pixmap *dst, PixmapFormat format) {
dst->data.untyped = pixmap_alloc_buffer_for_conversion(src, format);
dst->data.untyped = pixmap_alloc_buffer_for_conversion(src, format, &dst->data_size);
pixmap_convert(src, dst, format);
}
@ -268,19 +303,22 @@ void pixmap_convert_inplace_realloc(Pixmap *src, PixmapFormat format) {
void pixmap_copy(const Pixmap *src, Pixmap *dst) {
assert(dst->data.untyped != NULL);
assert(src->data_size > 0);
assert(dst->data_size >= src->data_size);
if(!pixmap_format_is_compressed(src->format)) {
assert(src->data_size == pixmap_data_size(src->format, src->width, src->height));
}
pixmap_copy_meta(src, dst);
memcpy(dst->data.untyped, src->data.untyped, pixmap_data_size(src));
memcpy(dst->data.untyped, src->data.untyped, src->data_size);
}
void pixmap_copy_alloc(const Pixmap *src, Pixmap *dst) {
dst->data.untyped = pixmap_alloc_buffer_for_copy(src);
dst->data.untyped = pixmap_alloc_buffer_for_copy(src, &dst->data_size);
pixmap_copy(src, dst);
}
size_t pixmap_data_size(const Pixmap *px) {
return px->width * px->height * PIXMAP_FORMAT_PIXEL_SIZE(px->format);
}
void pixmap_flip_y(const Pixmap *src, Pixmap *dst) {
assert(dst->data.untyped != NULL);
pixmap_copy_meta(src, dst);
@ -297,7 +335,7 @@ void pixmap_flip_y(const Pixmap *src, Pixmap *dst) {
}
void pixmap_flip_y_alloc(const Pixmap *src, Pixmap *dst) {
dst->data.untyped = pixmap_alloc_buffer_for_copy(src);
dst->data.untyped = pixmap_alloc_buffer_for_copy(src, &dst->data_size);
pixmap_flip_y(src, dst);
}
@ -326,7 +364,7 @@ void pixmap_flip_to_origin(const Pixmap *src, Pixmap *dst, PixmapOrigin origin)
}
void pixmap_flip_to_origin_alloc(const Pixmap *src, Pixmap *dst, PixmapOrigin origin) {
dst->data.untyped = pixmap_alloc_buffer_for_copy(src);
dst->data.untyped = pixmap_alloc_buffer_for_copy(src, &dst->data_size);
pixmap_flip_to_origin(src, dst, origin);
}
@ -428,3 +466,75 @@ char *pixmap_source_path(const char *prefix, const char *path) {
return NULL;
}
const char *pixmap_format_name(PixmapFormat fmt) {
switch(fmt) {
#define HANDLE(f) case f: return #f
HANDLE(PIXMAP_FORMAT_R8);
HANDLE(PIXMAP_FORMAT_R16);
HANDLE(PIXMAP_FORMAT_R16F);
HANDLE(PIXMAP_FORMAT_R32F);
HANDLE(PIXMAP_FORMAT_RG8);
HANDLE(PIXMAP_FORMAT_RG16);
HANDLE(PIXMAP_FORMAT_RG16F);
HANDLE(PIXMAP_FORMAT_RG32F);
HANDLE(PIXMAP_FORMAT_RGB8);
HANDLE(PIXMAP_FORMAT_RGB16);
HANDLE(PIXMAP_FORMAT_RGB16F);
HANDLE(PIXMAP_FORMAT_RGB32F);
HANDLE(PIXMAP_FORMAT_RGBA8);
HANDLE(PIXMAP_FORMAT_RGBA16);
HANDLE(PIXMAP_FORMAT_RGBA16F);
HANDLE(PIXMAP_FORMAT_RGBA32F);
#define HANDLE_COMPRESSED(comp, layout, ...) HANDLE(PIXMAP_FORMAT_##comp);
PIXMAP_COMPRESSION_FORMATS(HANDLE_COMPRESSED,)
#undef HANDLE_COMPRESSED
#undef HANDLE
default: return NULL;
}
}
SwizzleMask swizzle_canonize(SwizzleMask sw_in) {
SwizzleMask sw_out;
sw_out.r = sw_in.r ? sw_in.r : 'r';
sw_out.g = sw_in.g ? sw_in.g : 'g';
sw_out.b = sw_in.b ? sw_in.b : 'b';
sw_out.a = sw_in.a ? sw_in.a : 'a';
assert(swizzle_is_valid(sw_out));
return sw_out;
}
static bool swizzle_component_is_valid(char c) {
switch(c) {
case 'r': case 'g': case 'b': case 'a': case '0': case '1':
return true;
default:
return false;
}
}
bool swizzle_is_valid(SwizzleMask sw) {
for(int i = 0; i < 4; ++i) {
if(!swizzle_component_is_valid(sw.rgba[i])) {
return false;
}
}
return true;
}
bool swizzle_is_significant(SwizzleMask sw, uint num_significant_channels) {
assert(num_significant_channels <= 4);
for(uint i = 0; i < num_significant_channels; ++i) {
assert(swizzle_component_is_valid(sw.rgba[i]));
if(sw.rgba[i] != "rgba"[i]) {
return true;
}
}
return false;
}

288
src/pixmap/pixmap.h Normal file
View file

@ -0,0 +1,288 @@
/*
* This software is licensed under the terms of the MIT License.
* See COPYING for further information.
* ---
* Copyright (c) 2011-2019, Lukas Weber <laochailan@web.de>.
* Copyright (c) 2012-2019, Andrei Alexeyev <akari@taisei-project.org>.
*/
#ifndef IGUARD_pixmap_pixmap_h
#define IGUARD_pixmap_pixmap_h
#include "taisei.h"
#include <SDL.h>
#define PIXMAP_BUFFER_MAX_SIZE INT32_MAX
typedef enum PixmapLayout {
PIXMAP_LAYOUT_R = 1,
PIXMAP_LAYOUT_RG,
PIXMAP_LAYOUT_RGB,
PIXMAP_LAYOUT_RGBA,
} PixmapLayout;
typedef enum PixmapOrigin {
PIXMAP_ORIGIN_TOPLEFT,
PIXMAP_ORIGIN_BOTTOMLEFT,
} PixmapOrigin;
#define PIXMAP_COMPRESSION_FORMATS(X, ...) \
X(ETC1_RGB, RGB, __VA_ARGS__) \
X(ETC2_RGBA, RGBA, __VA_ARGS__) \
X(BC1_RGB, RGB, __VA_ARGS__) \
X(BC3_RGBA, RGBA, __VA_ARGS__) \
X(BC4_R, R, __VA_ARGS__) \
X(BC5_RG, RG, __VA_ARGS__) \
X(BC7_RGBA, RGBA, __VA_ARGS__) \
X(PVRTC1_4_RGB, RGB, __VA_ARGS__) \
X(PVRTC1_4_RGBA, RGBA, __VA_ARGS__) \
X(ASTC_4x4_RGBA, RGBA, __VA_ARGS__) \
X(ATC_RGB, RGB, __VA_ARGS__) \
X(ATC_RGBA, RGBA, __VA_ARGS__) \
X(PVRTC2_4_RGB, RGB, __VA_ARGS__) \
X(PVRTC2_4_RGBA, RGBA, __VA_ARGS__) \
X(ETC2_EAC_R11, R, __VA_ARGS__) \
X(ETC2_EAC_RG11, RG, __VA_ARGS__) \
X(FXT1_RGB, RGB, __VA_ARGS__) \
typedef enum PixmapCompression {
#define DEFINE_PIXMAP_COMPRESSION(cformat, ...) \
PIXMAP_COMPRESSION_##cformat,
PIXMAP_COMPRESSION_FORMATS(DEFINE_PIXMAP_COMPRESSION,)
#undef DEFINE_PIXMAP_COMPRESSION
} PixmapCompression;
enum {
PIXMAP_FLOAT_BIT = 0x80,
PIXMAP_COMPRESSED_BIT = 0x8000,
PIXMAP_FLAG_BITS = PIXMAP_FLOAT_BIT | PIXMAP_COMPRESSED_BIT,
};
#define PIXMAP_MAKE_FORMAT(layout, depth) \
(((layout) << 8) | (depth))
#define PIXMAP_MAKE_FLOAT_FORMAT(layout, depth) \
(PIXMAP_MAKE_FORMAT(layout, depth) | PIXMAP_FLOAT_BIT)
#define PIXMAP_MAKE_COMPRESSED_FORMAT(layout, compression) \
(PIXMAP_MAKE_FORMAT(layout, compression) | PIXMAP_COMPRESSED_BIT)
#define _impl_pixmap_format_is_compressed(fmt) \
((fmt) & PIXMAP_COMPRESSED_BIT)
#define _impl_pixmap_format_is_float(fmt) \
((fmt) & PIXMAP_FLOAT_BIT)
#define _impl_pixmap_format_layout(fmt) \
((fmt) & ~PIXMAP_FLAG_BITS) >> 8
#define _impl_pixmap_format_depth(fmt) \
((fmt) & ~PIXMAP_FLAG_BITS & 0xff)
#define _impl_pixmap_format_compression(fmt) \
((fmt) & ~PIXMAP_FLAG_BITS & 0xff)
#define _impl_pixmap_format_pixel_size(fmt) \
((_impl_pixmap_format_depth(fmt) >> 3) * _impl_pixmap_format_layout(fmt))
typedef enum PixmapFormat {
PIXMAP_FORMAT_R8 = PIXMAP_MAKE_FORMAT(PIXMAP_LAYOUT_R, 8),
PIXMAP_FORMAT_R16 = PIXMAP_MAKE_FORMAT(PIXMAP_LAYOUT_R, 16),
PIXMAP_FORMAT_R32 = PIXMAP_MAKE_FORMAT(PIXMAP_LAYOUT_R, 32),
PIXMAP_FORMAT_R16F = PIXMAP_MAKE_FLOAT_FORMAT(PIXMAP_LAYOUT_R, 16),
PIXMAP_FORMAT_R32F = PIXMAP_MAKE_FLOAT_FORMAT(PIXMAP_LAYOUT_R, 32),
PIXMAP_FORMAT_RG8 = PIXMAP_MAKE_FORMAT(PIXMAP_LAYOUT_RG, 8),
PIXMAP_FORMAT_RG16 = PIXMAP_MAKE_FORMAT(PIXMAP_LAYOUT_RG, 16),
PIXMAP_FORMAT_RG32 = PIXMAP_MAKE_FORMAT(PIXMAP_LAYOUT_RG, 32),
PIXMAP_FORMAT_RG16F = PIXMAP_MAKE_FLOAT_FORMAT(PIXMAP_LAYOUT_RG, 16),
PIXMAP_FORMAT_RG32F = PIXMAP_MAKE_FLOAT_FORMAT(PIXMAP_LAYOUT_RG, 32),
PIXMAP_FORMAT_RGB8 = PIXMAP_MAKE_FORMAT(PIXMAP_LAYOUT_RGB, 8),
PIXMAP_FORMAT_RGB16 = PIXMAP_MAKE_FORMAT(PIXMAP_LAYOUT_RGB, 16),
PIXMAP_FORMAT_RGB32 = PIXMAP_MAKE_FORMAT(PIXMAP_LAYOUT_RGB, 32),
PIXMAP_FORMAT_RGB16F = PIXMAP_MAKE_FLOAT_FORMAT(PIXMAP_LAYOUT_RGB, 16),
PIXMAP_FORMAT_RGB32F = PIXMAP_MAKE_FLOAT_FORMAT(PIXMAP_LAYOUT_RGB, 32),
PIXMAP_FORMAT_RGBA8 = PIXMAP_MAKE_FORMAT(PIXMAP_LAYOUT_RGBA, 8),
PIXMAP_FORMAT_RGBA16 = PIXMAP_MAKE_FORMAT(PIXMAP_LAYOUT_RGBA, 16),
PIXMAP_FORMAT_RGBA32 = PIXMAP_MAKE_FORMAT(PIXMAP_LAYOUT_RGBA, 32),
PIXMAP_FORMAT_RGBA16F = PIXMAP_MAKE_FLOAT_FORMAT(PIXMAP_LAYOUT_RGBA, 16),
PIXMAP_FORMAT_RGBA32F = PIXMAP_MAKE_FLOAT_FORMAT(PIXMAP_LAYOUT_RGBA, 32),
#define DEFINE_PIXMAP_COMPRESSED_FORMAT(cformat, layout, ...) \
PIXMAP_FORMAT_##cformat = PIXMAP_MAKE_COMPRESSED_FORMAT(PIXMAP_LAYOUT_##layout, PIXMAP_COMPRESSION_##cformat),
PIXMAP_COMPRESSION_FORMATS(DEFINE_PIXMAP_COMPRESSED_FORMAT,)
#undef DEFINE_PIXMAP_COMPRESSION
} PixmapFormat;
// sanity check
static_assert_nomsg(_impl_pixmap_format_layout(PIXMAP_FORMAT_ETC2_EAC_RG11) == PIXMAP_LAYOUT_RG);
static_assert_nomsg(_impl_pixmap_format_compression(PIXMAP_FORMAT_ASTC_4x4_RGBA) == PIXMAP_COMPRESSION_ASTC_4x4_RGBA);
static_assert_nomsg(_impl_pixmap_format_depth(PIXMAP_FORMAT_RGB16F) == 16);
static_assert_nomsg(_impl_pixmap_format_depth(PIXMAP_FORMAT_RGB8) == 8);
static_assert_nomsg(_impl_pixmap_format_layout(PIXMAP_FORMAT_RGB8) == PIXMAP_LAYOUT_RGB);
INLINE bool pixmap_format_is_compressed(PixmapFormat fmt) {
return _impl_pixmap_format_is_compressed(fmt);
}
INLINE bool pixmap_format_is_float(PixmapFormat fmt) {
assert(!pixmap_format_is_compressed(fmt));
return _impl_pixmap_format_is_float(fmt);
}
INLINE PixmapLayout pixmap_format_layout(PixmapFormat fmt) {
return _impl_pixmap_format_layout(fmt);
}
INLINE uint pixmap_format_depth(PixmapFormat fmt) {
assert(!pixmap_format_is_compressed(fmt));
return _impl_pixmap_format_depth(fmt);
}
INLINE PixmapCompression pixmap_format_compression(PixmapFormat fmt) {
assert(pixmap_format_is_compressed(fmt));
return _impl_pixmap_format_compression(fmt);
}
INLINE uint pixmap_format_pixel_size(PixmapFormat fmt) {
assert(!pixmap_format_is_compressed(fmt));
return _impl_pixmap_format_pixel_size(fmt);
}
// TODO deprecate?
#define PIXMAP_FORMAT_LAYOUT(format) (pixmap_format_layout(format))
#define PIXMAP_FORMAT_DEPTH(format) (pixmap_format_depth(format))
#define PIXMAP_FORMAT_PIXEL_SIZE(format) (pixmap_format_pixel_size(format))
#define PIXMAP_FORMAT_IS_FLOAT(format) (pixmap_format_is_float(format))
#define PIXMAP_FORMAT_IS_COMPRESSED(format) (pixmap_format_is_compressed(format))
#define PIXMAP_PIXEL_STRUCT_R(type) union { \
type values[1]; \
struct { \
type r; \
}; \
}
#define PIXMAP_PIXEL_STRUCT_RG(type) union { \
type values[2]; \
struct { \
type r; \
type g; \
}; \
}
#define PIXMAP_PIXEL_STRUCT_RGB(type) union { \
type values[3]; \
struct { \
type r; \
type g; \
type b; \
}; \
}
#define PIXMAP_PIXEL_STRUCT_RGBA(type) union { \
type values[4]; \
struct { \
type r; \
type g; \
type b; \
type a; \
}; \
}
typedef PIXMAP_PIXEL_STRUCT_R(uint8_t) PixelR8;
typedef PIXMAP_PIXEL_STRUCT_R(uint16_t) PixelR16;
typedef PIXMAP_PIXEL_STRUCT_R(uint32_t) PixelR32;
typedef PIXMAP_PIXEL_STRUCT_R(float) PixelR32F;
typedef PIXMAP_PIXEL_STRUCT_RG(uint8_t) PixelRG8;
typedef PIXMAP_PIXEL_STRUCT_RG(uint16_t) PixelRG16;
typedef PIXMAP_PIXEL_STRUCT_RG(uint32_t) PixelRG32;
typedef PIXMAP_PIXEL_STRUCT_RG(float) PixelRG32F;
typedef PIXMAP_PIXEL_STRUCT_RGB(uint8_t) PixelRGB8;
typedef PIXMAP_PIXEL_STRUCT_RGB(uint16_t) PixelRGB16;
typedef PIXMAP_PIXEL_STRUCT_RGB(uint32_t) PixelRGB32;
typedef PIXMAP_PIXEL_STRUCT_RGB(float) PixelRGB32F;
typedef PIXMAP_PIXEL_STRUCT_RGBA(uint8_t) PixelRGBA8;
typedef PIXMAP_PIXEL_STRUCT_RGBA(uint16_t) PixelRGBA16;
typedef PIXMAP_PIXEL_STRUCT_RGBA(uint32_t) PixelRGBA32;
typedef PIXMAP_PIXEL_STRUCT_RGBA(float) PixelRGBA32F;
typedef struct Pixmap {
union {
void *untyped;
PixelR8 *r8;
PixelR16 *r16;
PixelR32 *r32;
PixelR32F *r32f;
PixelRG8 *rg8;
PixelRG16 *rg16;
PixelRG32 *rg32;
PixelRG32F *rg32f;
PixelRGB8 *rgb8;
PixelRGB16 *rgb16;
PixelRGB32 *rgb32;
PixelRGB32F *rgb32f;
PixelRGBA8 *rgba8;
PixelRGBA16 *rgba16;
PixelRGBA32 *rgba32;
PixelRGBA32F *rgba32f;
} data;
uint32_t width;
uint32_t height;
uint32_t data_size;
PixmapFormat format;
PixmapOrigin origin;
} Pixmap;
typedef union SwizzleMask {
char rgba[4];
struct {
char r, g, b, a;
};
} SwizzleMask;
void *pixmap_alloc_buffer(PixmapFormat format, uint32_t width, uint32_t height, uint32_t *out_bufsize) attr_returns_allocated;
void *pixmap_alloc_buffer_for_copy(const Pixmap *src, uint32_t *out_bufsize) attr_nonnull(1) attr_returns_allocated;
void *pixmap_alloc_buffer_for_conversion(const Pixmap *src, PixmapFormat format, uint32_t *out_bufsize) attr_nonnull(1) attr_returns_allocated;
void pixmap_copy(const Pixmap *src, Pixmap *dst) attr_nonnull(1, 2);
void pixmap_copy_alloc(const Pixmap *src, Pixmap *dst) attr_nonnull(1, 2);
void pixmap_convert(const Pixmap *src, Pixmap *dst, PixmapFormat format) attr_nonnull(1, 2);
void pixmap_convert_alloc(const Pixmap *src, Pixmap *dst, PixmapFormat format) attr_nonnull(1, 2);
void pixmap_convert_inplace_realloc(Pixmap *src, PixmapFormat format);
void pixmap_swizzle_inplace(Pixmap *px, SwizzleMask swizzle);
void pixmap_flip_y(const Pixmap *src, Pixmap *dst) attr_nonnull(1, 2);
void pixmap_flip_y_alloc(const Pixmap *src, Pixmap *dst) attr_nonnull(1, 2);
void pixmap_flip_y_inplace(Pixmap *src) attr_nonnull(1);
void pixmap_flip_to_origin(const Pixmap *src, Pixmap *dst, PixmapOrigin origin) attr_nonnull(1, 2);
void pixmap_flip_to_origin_alloc(const Pixmap *src, Pixmap *dst, PixmapOrigin origin) attr_nonnull(1, 2);
void pixmap_flip_to_origin_inplace(Pixmap *src, PixmapOrigin origin) attr_nonnull(1);
bool pixmap_load_file(const char *path, Pixmap *dst, PixmapFormat preferred_format) attr_nonnull(1, 2) attr_nodiscard;
bool pixmap_load_stream(SDL_RWops *stream, Pixmap *dst, PixmapFormat preferred_format) attr_nonnull(1, 2) attr_nodiscard;
bool pixmap_check_filename(const char *path);
char *pixmap_source_path(const char *prefix, const char *path) attr_nodiscard;
const char *pixmap_format_name(PixmapFormat fmt);
SwizzleMask swizzle_canonize(SwizzleMask sw_in);
bool swizzle_is_valid(SwizzleMask sw);
bool swizzle_is_significant(SwizzleMask sw, uint num_significant_channels);