''' /************************************************************************** * * 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