Instead of having a hardcoded table to convert between sRGB formats and their linear-gamma equivalents (and vice-versa), generate this from the information in the format table. This requires adding a 'sublayout' attribute to differentiate between, e.g. DXT1 and DXT3, which otherwise appear to be equivalent but for their name prefix. As an anonymous union is being used, we also need named initialisers for the util_format_description entries. Signed-off-by: Daniel Stone <daniels@collabora.com> Part-of: <https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/29649>
596 lines
20 KiB
Python
596 lines
20 KiB
Python
|
|
'''
|
|
/**************************************************************************
|
|
*
|
|
* Copyright 2009 VMware, Inc.
|
|
* All Rights Reserved.
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a
|
|
* copy of this software and associated documentation files (the
|
|
* "Software"), to deal in the Software without restriction, including
|
|
* without limitation the rights to use, copy, modify, merge, publish,
|
|
* distribute, sub license, and/or sell copies of the Software, and to
|
|
* permit persons to whom the Software is furnished to do so, subject to
|
|
* the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice (including the
|
|
* next paragraph) shall be included in all copies or substantial portions
|
|
* of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT.
|
|
* IN NO EVENT SHALL VMWARE AND/OR ITS SUPPLIERS BE LIABLE FOR
|
|
* ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
*
|
|
**************************************************************************/
|
|
'''
|
|
|
|
|
|
import copy
|
|
import yaml
|
|
import sys
|
|
|
|
try:
|
|
from yaml import CSafeLoader as YAMLSafeLoader
|
|
except:
|
|
from yaml import SafeLoader as YAMLSafeLoader
|
|
|
|
VOID, UNSIGNED, SIGNED, FIXED, FLOAT = range(5)
|
|
|
|
SWIZZLE_X, SWIZZLE_Y, SWIZZLE_Z, SWIZZLE_W, SWIZZLE_0, SWIZZLE_1, SWIZZLE_NONE, = range(7)
|
|
|
|
PLAIN = 'plain'
|
|
|
|
RGB = 'RGB'
|
|
SRGB = 'SRGB'
|
|
YUV = 'YUV'
|
|
ZS = 'ZS'
|
|
|
|
|
|
_type_parse_map = {
|
|
'': VOID,
|
|
'X': VOID,
|
|
'U': UNSIGNED,
|
|
'S': SIGNED,
|
|
'H': FIXED,
|
|
'F': FLOAT,
|
|
}
|
|
|
|
_swizzle_parse_map = {
|
|
'X': SWIZZLE_X,
|
|
'Y': SWIZZLE_Y,
|
|
'Z': SWIZZLE_Z,
|
|
'W': SWIZZLE_W,
|
|
'0': SWIZZLE_0,
|
|
'1': SWIZZLE_1,
|
|
'_': SWIZZLE_NONE,
|
|
}
|
|
|
|
|
|
def is_pot(x):
|
|
return (x & (x - 1)) == 0
|
|
|
|
|
|
VERY_LARGE = 99999999999999999999999
|
|
|
|
def validate_str(x):
|
|
if not isinstance(x, str):
|
|
raise ValueError(type(x))
|
|
|
|
def validate_int(x):
|
|
if not isinstance(x, int):
|
|
raise ValueError(f"invalid type {type(x)}")
|
|
|
|
def validate_list_str_4(x):
|
|
if not isinstance(x, list):
|
|
raise ValueError(f"invalid type {type(x)}")
|
|
if len(x) != 4:
|
|
raise ValueError(f"invalid length {len(x)}")
|
|
for i in range(len(x)):
|
|
if isinstance(x[i], int):
|
|
x[i] = str(x[i])
|
|
if not isinstance(x[i], str):
|
|
raise ValueError(f"invalid member type {type(x[i])}")
|
|
|
|
def validate_list_str_le4(x):
|
|
if not isinstance(x, list):
|
|
raise ValueError(f"invalid type {type(x)}")
|
|
if len(x) > 4:
|
|
raise ValueError(f"invalid length {len(x)}")
|
|
for i in range(len(x)):
|
|
if isinstance(x[i], int):
|
|
x[i] = str(x[i])
|
|
if not isinstance(x[i], str):
|
|
raise ValueError(f"invalid member type {type(x[i])}")
|
|
|
|
|
|
def get_and_delete(d, k):
|
|
ret = d[k]
|
|
del(d[k])
|
|
return ret
|
|
|
|
def do_consume(d, *args):
|
|
if len(args) == 1:
|
|
return get_and_delete(d, args[0])
|
|
else:
|
|
return do_consume(d[args[0]], *args[1:])
|
|
|
|
def consume(f, validate, d, *args):
|
|
if len(args) > 1:
|
|
sub = " under " + ".".join([f"'{a}'" for a in args[:-1]])
|
|
else:
|
|
sub = ""
|
|
|
|
try:
|
|
ret = do_consume(d, *args)
|
|
validate(ret)
|
|
return ret
|
|
except KeyError:
|
|
raise RuntimeError(f"Key '{args[-1]}' not present{sub} in format {f.name}")
|
|
except ValueError as e:
|
|
raise RuntimeError(f"Key '{args[-1]}' invalid{sub} in format {f.name}: {e.args[0]}")
|
|
|
|
def consume_str(f, d, *args):
|
|
return consume(f, validate_str, d, *args)
|
|
|
|
def consume_int(f, d, *args):
|
|
return consume(f, validate_int, d, *args)
|
|
|
|
def consume_list_str_4(f, d, *args):
|
|
return consume(f, validate_list_str_4, d, *args)
|
|
|
|
def consume_list_str_le4(f, d, *args):
|
|
return consume(f, validate_list_str_le4, d, *args)
|
|
|
|
def consumed(f, d, *args):
|
|
if args:
|
|
d = do_consume(d, *args)
|
|
if len(d) > 0:
|
|
keys = ", ".join([f"'{k}'" for k in d.keys()])
|
|
if args:
|
|
sub = " under " + ".".join([f"'{a}'" for a in args])
|
|
else:
|
|
sub = ""
|
|
raise RuntimeError(f"Unknown keys ({keys}) present in format {f.name}{sub}")
|
|
|
|
|
|
class Channel:
|
|
'''Describe the channel of a color channel.'''
|
|
|
|
def __init__(self, type, norm, pure, size, name=''):
|
|
self.type = type
|
|
self.norm = norm
|
|
self.pure = pure
|
|
self.size = size
|
|
self.sign = type in (SIGNED, FIXED, FLOAT)
|
|
self.name = name
|
|
|
|
def __str__(self):
|
|
s = str(self.type)
|
|
if self.norm:
|
|
s += 'n'
|
|
if self.pure:
|
|
s += 'p'
|
|
s += str(self.size)
|
|
return s
|
|
|
|
def __repr__(self):
|
|
return "Channel({})".format(self.__str__())
|
|
|
|
def __eq__(self, other):
|
|
if other is None:
|
|
return False
|
|
|
|
return self.type == other.type and self.norm == other.norm and self.pure == other.pure and self.size == other.size
|
|
|
|
def __ne__(self, other):
|
|
return not self == other
|
|
|
|
def max(self):
|
|
'''Maximum representable number.'''
|
|
if self.type == FLOAT:
|
|
return VERY_LARGE
|
|
if self.type == FIXED:
|
|
return (1 << (self.size // 2)) - 1
|
|
if self.norm:
|
|
return 1
|
|
if self.type == UNSIGNED:
|
|
return (1 << self.size) - 1
|
|
if self.type == SIGNED:
|
|
return (1 << (self.size - 1)) - 1
|
|
assert False
|
|
|
|
def min(self):
|
|
'''Minimum representable number.'''
|
|
if self.type == FLOAT:
|
|
return -VERY_LARGE
|
|
if self.type == FIXED:
|
|
return -(1 << (self.size // 2))
|
|
if self.type == UNSIGNED:
|
|
return 0
|
|
if self.norm:
|
|
return -1
|
|
if self.type == SIGNED:
|
|
return -(1 << (self.size - 1))
|
|
assert False
|
|
|
|
|
|
class Format:
|
|
'''Describe a pixel format.'''
|
|
|
|
def __init__(self, source):
|
|
self.name = "unknown"
|
|
self.name = f"PIPE_FORMAT_{consume_str(self, source, 'name')}"
|
|
self.layout = consume_str(self, source, 'layout')
|
|
if 'sublayout' in source:
|
|
self.sublayout = consume_str(self, source, 'sublayout')
|
|
else:
|
|
self.sublayout = None
|
|
self.block_width = consume_int(self, source, 'block', 'width')
|
|
self.block_height = consume_int(self, source, 'block', 'height')
|
|
self.block_depth = consume_int(self, source, 'block', 'depth')
|
|
consumed(self, source, 'block')
|
|
self.colorspace = consume_str(self, source, 'colorspace')
|
|
self.srgb_equivalent = None
|
|
self.linear_equivalent = None
|
|
|
|
# Formats with no endian-dependent swizzling declare their channel and
|
|
# swizzle layout at the top level. Else they can declare an
|
|
# endian-dependent swizzle. This only applies to packed formats,
|
|
# however we can't use is_array() or is_bitmask() to test because they
|
|
# depend on the channels having already been parsed.
|
|
if 'swizzles' in source:
|
|
self.le_swizzles = list(map(lambda x: _swizzle_parse_map[x],
|
|
consume_list_str_4(self, source, 'swizzles')))
|
|
self.le_channels = _parse_channels(consume_list_str_le4(self, source, 'channels'),
|
|
self.layout, self.colorspace, self.le_swizzles)
|
|
self.be_swizzles = None
|
|
self.be_channels = None
|
|
if source.get('little_endian', {}).get('swizzles') or \
|
|
source.get('big_endian', {}).get('swizzles'):
|
|
raise RuntimeError(f"Format {self.name} must not declare endian-dependent and endian-independent swizzles")
|
|
else:
|
|
self.le_swizzles = list(map(lambda x: _swizzle_parse_map[x],
|
|
consume_list_str_4(self, source, 'little_endian', 'swizzles')))
|
|
self.le_channels = _parse_channels(consume_list_str_le4(self, source, 'little_endian', 'channels'),
|
|
self.layout, self.colorspace, self.le_swizzles)
|
|
self.be_swizzles = list(map(lambda x: _swizzle_parse_map[x],
|
|
consume_list_str_4(self, source, 'big_endian', 'swizzles')))
|
|
self.be_channels = _parse_channels(consume_list_str_le4(self, source, 'big_endian', 'channels'),
|
|
self.layout, self.colorspace, self.be_swizzles)
|
|
if self.is_array():
|
|
raise RuntimeError("Array format {self.name} must not define endian-specific swizzles")
|
|
if self.is_bitmask():
|
|
raise RuntimeError("Bitmask format {self.name} must not define endian-specific swizzles")
|
|
|
|
self.le_alias = None
|
|
self.be_alias = None
|
|
if 'little_endian' in source:
|
|
if 'alias' in source['little_endian']:
|
|
self.le_alias = f"PIPE_FORMAT_{consume_str(self, source, 'little_endian', 'alias')}"
|
|
consumed(self, source, 'little_endian')
|
|
if 'big_endian' in source:
|
|
if 'alias' in source['big_endian']:
|
|
self.be_alias = f"PIPE_FORMAT_{consume_str(self, source, 'big_endian', 'alias')}"
|
|
consumed(self, source, 'big_endian')
|
|
|
|
consumed(self, source)
|
|
del(source)
|
|
|
|
if self.is_bitmask() and not self.is_array():
|
|
# Bitmask formats are "load a word the size of the block and
|
|
# bitshift channels out of it." However, the channel shifts
|
|
# defined in u_format_table.c are numbered right-to-left on BE
|
|
# for some historical reason (see below), which is hard to
|
|
# change due to llvmpipe, so we also have to flip the channel
|
|
# order and the channel-to-rgba swizzle values to read
|
|
# right-to-left from the defined (non-VOID) channels so that the
|
|
# correct shifts happen.
|
|
#
|
|
# This is nonsense, but it's the nonsense that makes
|
|
# u_format_test pass and you get the right colors in softpipe at
|
|
# least.
|
|
chans = self.nr_channels()
|
|
self.be_channels = self.le_channels[chans -
|
|
1::-1] + self.le_channels[chans:4]
|
|
|
|
xyzw = [SWIZZLE_X, SWIZZLE_Y, SWIZZLE_Z, SWIZZLE_W]
|
|
chan_map = {SWIZZLE_X: xyzw[chans - 1] if chans >= 1 else SWIZZLE_X,
|
|
SWIZZLE_Y: xyzw[chans - 2] if chans >= 2 else SWIZZLE_X,
|
|
SWIZZLE_Z: xyzw[chans - 3] if chans >= 3 else SWIZZLE_X,
|
|
SWIZZLE_W: xyzw[chans - 4] if chans >= 4 else SWIZZLE_X,
|
|
SWIZZLE_1: SWIZZLE_1,
|
|
SWIZZLE_0: SWIZZLE_0,
|
|
SWIZZLE_NONE: SWIZZLE_NONE}
|
|
self.be_swizzles = [chan_map[s] for s in self.le_swizzles]
|
|
elif not self.be_channels:
|
|
self.be_channels = copy.deepcopy(self.le_channels)
|
|
self.be_swizzles = self.le_swizzles
|
|
|
|
le_shift = 0
|
|
for channel in self.le_channels:
|
|
channel.shift = le_shift
|
|
le_shift += channel.size
|
|
|
|
be_shift = 0
|
|
for channel in reversed(self.be_channels):
|
|
channel.shift = be_shift
|
|
be_shift += channel.size
|
|
|
|
assert le_shift == be_shift
|
|
for i in range(4):
|
|
assert (self.le_swizzles[i] != SWIZZLE_NONE) == (
|
|
self.be_swizzles[i] != SWIZZLE_NONE)
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
def __eq__(self, other):
|
|
if not other:
|
|
return False
|
|
return self.name == other.name
|
|
|
|
def __hash__(self):
|
|
return hash(self.name)
|
|
|
|
def short_name(self):
|
|
'''Make up a short norm for a format, suitable to be used as suffix in
|
|
function names.'''
|
|
|
|
name = self.name
|
|
if name.startswith('PIPE_FORMAT_'):
|
|
name = name[len('PIPE_FORMAT_'):]
|
|
name = name.lower()
|
|
return name
|
|
|
|
def block_size(self):
|
|
size = 0
|
|
for channel in self.le_channels:
|
|
size += channel.size
|
|
return size
|
|
|
|
def nr_channels(self):
|
|
nr_channels = 0
|
|
for channel in self.le_channels:
|
|
if channel.size:
|
|
nr_channels += 1
|
|
return nr_channels
|
|
|
|
def array_element(self):
|
|
if self.layout != PLAIN:
|
|
return None
|
|
ref_channel = self.le_channels[0]
|
|
if ref_channel.type == VOID:
|
|
ref_channel = self.le_channels[1]
|
|
for channel in self.le_channels:
|
|
if channel.size and (channel.size != ref_channel.size or channel.size % 8):
|
|
return None
|
|
if channel.type != VOID:
|
|
if channel.type != ref_channel.type:
|
|
return None
|
|
if channel.norm != ref_channel.norm:
|
|
return None
|
|
if channel.pure != ref_channel.pure:
|
|
return None
|
|
return ref_channel
|
|
|
|
def is_array(self):
|
|
return self.array_element() != None
|
|
|
|
def is_mixed(self):
|
|
if self.layout != PLAIN:
|
|
return False
|
|
ref_channel = self.le_channels[0]
|
|
if ref_channel.type == VOID:
|
|
ref_channel = self.le_channels[1]
|
|
for channel in self.le_channels[1:]:
|
|
if channel.type != VOID:
|
|
if channel.type != ref_channel.type:
|
|
return True
|
|
if channel.norm != ref_channel.norm:
|
|
return True
|
|
if channel.pure != ref_channel.pure:
|
|
return True
|
|
return False
|
|
|
|
def is_compressed(self):
|
|
for channel in self.le_channels:
|
|
if channel.type != VOID:
|
|
return False
|
|
return True
|
|
|
|
def is_unorm(self):
|
|
# Non-compressed formats all have unorm or srgb in their name.
|
|
for keyword in ['_UNORM', '_SRGB']:
|
|
if keyword in self.name:
|
|
return True
|
|
|
|
# All the compressed formats in GLES3.2 and GL4.6 ("Table 8.14: Generic
|
|
# and specific compressed internal formats.") that aren't snorm for
|
|
# border colors are unorm, other than BPTC_*_FLOAT.
|
|
return self.is_compressed() and not ('FLOAT' in self.name or self.is_snorm())
|
|
|
|
def is_snorm(self):
|
|
return '_SNORM' in self.name
|
|
|
|
def is_pot(self):
|
|
return is_pot(self.block_size())
|
|
|
|
def is_int(self):
|
|
if self.layout != PLAIN:
|
|
return False
|
|
for channel in self.le_channels:
|
|
if channel.type not in (VOID, UNSIGNED, SIGNED):
|
|
return False
|
|
return True
|
|
|
|
def is_float(self):
|
|
if self.layout != PLAIN:
|
|
return False
|
|
for channel in self.le_channels:
|
|
if channel.type not in (VOID, FLOAT):
|
|
return False
|
|
return True
|
|
|
|
def is_bitmask(self):
|
|
if self.layout != PLAIN:
|
|
return False
|
|
if self.block_size() not in (8, 16, 32):
|
|
return False
|
|
for channel in self.le_channels:
|
|
if channel.type not in (VOID, UNSIGNED, SIGNED):
|
|
return False
|
|
return True
|
|
|
|
def is_pure_color(self):
|
|
if self.layout != PLAIN or self.colorspace == ZS:
|
|
return False
|
|
pures = [channel.pure
|
|
for channel in self.le_channels
|
|
if channel.type != VOID]
|
|
for x in pures:
|
|
assert x == pures[0]
|
|
return pures[0]
|
|
|
|
def channel_type(self):
|
|
types = [channel.type
|
|
for channel in self.le_channels
|
|
if channel.type != VOID]
|
|
for x in types:
|
|
assert x == types[0]
|
|
return types[0]
|
|
|
|
def is_pure_signed(self):
|
|
return self.is_pure_color() and self.channel_type() == SIGNED
|
|
|
|
def is_pure_unsigned(self):
|
|
return self.is_pure_color() and self.channel_type() == UNSIGNED
|
|
|
|
def has_channel(self, id):
|
|
return self.le_swizzles[id] != SWIZZLE_NONE
|
|
|
|
def has_depth(self):
|
|
return self.colorspace == ZS and self.has_channel(0)
|
|
|
|
def has_stencil(self):
|
|
return self.colorspace == ZS and self.has_channel(1)
|
|
|
|
def stride(self):
|
|
return self.block_size()/8
|
|
|
|
|
|
|
|
def _parse_channels(fields, layout, colorspace, swizzles):
|
|
if layout == PLAIN:
|
|
names = ['']*4
|
|
if colorspace in (RGB, SRGB):
|
|
for i in range(4):
|
|
swizzle = swizzles[i]
|
|
if swizzle < 4:
|
|
names[swizzle] += 'rgba'[i]
|
|
elif colorspace == ZS:
|
|
for i in range(4):
|
|
swizzle = swizzles[i]
|
|
if swizzle < 4:
|
|
names[swizzle] += 'zs'[i]
|
|
else:
|
|
assert False
|
|
for i in range(4):
|
|
if names[i] == '':
|
|
names[i] = 'x'
|
|
else:
|
|
names = ['x', 'y', 'z', 'w']
|
|
|
|
channels = []
|
|
for i in range(0, 4):
|
|
if i < len(fields):
|
|
field = fields[i]
|
|
type = _type_parse_map[field[0]]
|
|
if field[1] == 'N':
|
|
norm = True
|
|
pure = False
|
|
size = int(field[2:])
|
|
elif field[1] == 'P':
|
|
pure = True
|
|
norm = False
|
|
size = int(field[2:])
|
|
else:
|
|
norm = False
|
|
pure = False
|
|
size = int(field[1:])
|
|
else:
|
|
type = VOID
|
|
norm = False
|
|
pure = False
|
|
size = 0
|
|
channel = Channel(type, norm, pure, size, names[i])
|
|
channels.append(channel)
|
|
|
|
return channels
|
|
|
|
def mostly_equivalent(one, two):
|
|
if one.layout != two.layout or \
|
|
one.sublayout != two.sublayout or \
|
|
one.block_width != two.block_width or \
|
|
one.block_height != two.block_height or \
|
|
one.block_depth != two.block_depth or \
|
|
one.le_swizzles != two.le_swizzles or \
|
|
one.le_channels != two.le_channels or \
|
|
one.be_swizzles != two.be_swizzles or \
|
|
one.be_channels != two.be_channels:
|
|
return False
|
|
return True
|
|
|
|
def should_ignore_for_mapping(fmt):
|
|
# This format is a really special reinterpretation of depth/stencil as
|
|
# RGB. Until we figure out something better, just special-case it so
|
|
# we won't consider it as equivalent to anything.
|
|
if fmt.name == "PIPE_FORMAT_Z24_UNORM_S8_UINT_AS_R8G8B8A8":
|
|
return True
|
|
return False
|
|
|
|
|
|
def parse(filename):
|
|
'''Parse the format description in YAML format in terms of the
|
|
Channel and Format classes above.'''
|
|
|
|
stream = open(filename)
|
|
doc = yaml.load(stream, Loader=YAMLSafeLoader)
|
|
assert(isinstance(doc, list))
|
|
|
|
ret = []
|
|
for entry in doc:
|
|
assert(isinstance(entry, dict))
|
|
try:
|
|
f = Format(copy.deepcopy(entry))
|
|
except Exception as e:
|
|
raise RuntimeError(f"Failed to parse entry {entry}: {e}")
|
|
if f in ret:
|
|
raise RuntimeError(f"Duplicate format entry {f.name}")
|
|
ret.append(f)
|
|
|
|
for fmt in ret:
|
|
if should_ignore_for_mapping(fmt):
|
|
continue
|
|
if fmt.colorspace != RGB and fmt.colorspace != SRGB:
|
|
continue
|
|
if fmt.colorspace == RGB:
|
|
for equiv in ret:
|
|
if equiv.colorspace != SRGB or not mostly_equivalent(fmt, equiv) or \
|
|
should_ignore_for_mapping(equiv):
|
|
continue
|
|
assert(fmt.srgb_equivalent == None)
|
|
fmt.srgb_equivalent = equiv
|
|
elif fmt.colorspace == SRGB:
|
|
for equiv in ret:
|
|
if equiv.colorspace != RGB or not mostly_equivalent(fmt, equiv) or \
|
|
should_ignore_for_mapping(equiv):
|
|
continue
|
|
assert(fmt.linear_equivalent == None)
|
|
fmt.linear_equivalent = equiv
|
|
|
|
return ret
|