From d4c780e052c9cc361bed5958b72b42d8151800c2 Mon Sep 17 00:00:00 2001 From: Jason Ekstrand Date: Fri, 1 Aug 2014 18:09:46 -0700 Subject: [PATCH] mesa: Add python to parse the formats CSV file The basic concept for the format parser was taken from the format CSV parser in gallium/auxilliary/util. However, this one has been altered in a number of ways: * Removed big endian vs. little endian stuff (mesa doesn't need it) * Better documentation: Almost every method has a full docstring * An actual Swizzle class with methods for composition and inverses * Over-all cleaner (in my opinion) implementation and class interactions * A few bug fixes Signed-off-by: Jason Ekstrand Reviewed-by: Brian Paul --- src/mesa/main/format_parser.py | 521 +++++++++++++++++++++++++++++++++ 1 file changed, 521 insertions(+) create mode 100755 src/mesa/main/format_parser.py diff --git a/src/mesa/main/format_parser.py b/src/mesa/main/format_parser.py new file mode 100755 index 00000000000..5e45c74de3e --- /dev/null +++ b/src/mesa/main/format_parser.py @@ -0,0 +1,521 @@ +#!/usr/bin/env python +# +# Copyright 2009 VMware, Inc. +# Copyright 2014 Intel Corporation +# 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. + +VOID = 'x' +UNSIGNED = 'u' +SIGNED = 's' +FLOAT = 'f' + +ARRAY = 'array' +PACKED = 'packed' +OTHER = 'other' + +RGB = 'rgb' +SRGB = 'srgb' +YUV = 'yuv' +ZS = 'zs' + +def is_power_of_two(x): + return not bool(x & (x - 1)) + +VERY_LARGE = 99999999999999999999999 + +class Channel: + """Describes a color channel.""" + + def __init__(self, type, norm, size): + self.type = type + self.norm = norm + self.size = size + self.sign = type in (SIGNED, FLOAT) + self.name = None # Set when the channels are added to the format + self.shift = -1 # Set when the channels are added to the format + self.index = -1 # Set when the channels are added to the format + + def __str__(self): + s = str(self.type) + if self.norm: + s += 'n' + s += str(self.size) + return s + + def __eq__(self, other): + return self.type == other.type and self.norm == other.norm and self.size == other.size + + def max(self): + """Returns the maximum representable number.""" + if self.type == FLOAT: + return VERY_LARGE + 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): + """Returns the minimum representable number.""" + if self.type == FLOAT: + return -VERY_LARGE + if self.type == UNSIGNED: + return 0 + if self.norm: + return -1 + if self.type == SIGNED: + return -(1 << (self.size - 1)) + assert False + + def one(self): + """Returns the value that represents 1.0f.""" + if self.type == UNSIGNED: + return (1 << self.size) - 1 + if self.type == SIGNED: + return (1 << (self.size - 1)) - 1 + else: + return 1 + + def is_power_of_two(self): + """Returns true if the size of this channel is a power of two.""" + return is_power_of_two(self.size) + +class Swizzle: + """Describes a swizzle operation. + + A Swizzle is a mapping from one set of channels in one format to the + channels in another. Each channel in the destination format is + associated with one of the following constants: + + * SWIZZLE_X: The first channel in the source format + * SWIZZLE_Y: The second channel in the source format + * SWIZZLE_Z: The third channel in the source format + * SWIZZLE_W: The fourth channel in the source format + * SWIZZLE_ZERO: The numeric constant 0 + * SWIZZLE_ONE: THe numeric constant 1 + * SWIZZLE_NONE: No data available for this channel + + Sometimes a Swizzle is represented by a 4-character string. In this + case, the source channels are represented by the characters "x", "y", + "z", and "w"; the numeric constants are represented as "0" and "1"; and + no mapping is represented by "_". For instance, the map from + luminance-alpha to rgba is given by "xxxy" because each of the three rgb + channels maps to the first luminance-alpha channel and the alpha channel + maps to second luminance-alpha channel. The mapping from bgr to rgba is + given by "zyx1" because the first three colors are reversed and alpha is + always 1. + """ + + __identity_str = 'xyzw01_' + + SWIZZLE_X = 0 + SWIZZLE_Y = 1 + SWIZZLE_Z = 2 + SWIZZLE_W = 3 + SWIZZLE_ZERO = 4 + SWIZZLE_ONE = 5 + SWIZZLE_NONE = 6 + + def __init__(self, swizzle): + """Creates a Swizzle object from a string or array.""" + if isinstance(swizzle, str): + swizzle = [Swizzle.__identity_str.index(c) for c in swizzle] + else: + swizzle = list(swizzle) + for s in swizzle: + assert isinstance(s, int) and 0 <= s and s <= Swizzle.SWIZZLE_NONE + + assert len(swizzle) <= 4 + + self.__list = swizzle + [Swizzle.SWIZZLE_NONE] * (4 - len(swizzle)) + assert len(self.__list) == 4 + + def __iter__(self): + """Returns an iterator that iterates over this Swizzle. + + The values that the iterator produces are described by the SWIZZLE_* + constants. + """ + return self.__list.__iter__() + + def __str__(self): + """Returns a string representation of this Swizzle.""" + return ''.join(Swizzle.__identity_str[i] for i in self.__list) + + def __getitem__(self, idx): + """Returns the SWIZZLE_* constant for the given destination channel. + + Valid values for the destination channel include any of the SWIZZLE_* + constants or any of the following single-character strings: "x", "y", + "z", "w", "r", "g", "b", "a", "z" "s". + """ + + if isinstance(idx, int): + assert idx >= Swizzle.SWIZZLE_X and idx <= Swizzle.SWIZZLE_NONE + if idx <= Swizzle.SWIZZLE_W: + return self.__list.__getitem__(idx) + else: + return idx + elif isinstance(idx, str): + if idx in 'xyzw': + idx = 'xyzw'.find(idx) + elif idx in 'rgba': + idx = 'rgba'.find(idx) + elif idx in 'zs': + idx = 'zs'.find(idx) + else: + assert False + return self.__list.__getitem__(idx) + else: + assert False + + def __mul__(self, other): + """Returns the composition of this Swizzle with another Swizzle. + + The resulting swizzle is such that, for any valid input to + __getitem__, (a * b)[i] = a[b[i]]. + """ + assert isinstance(other, Swizzle) + return Swizzle(self[x] for x in other) + + def inverse(self): + """Returns a pseudo-inverse of this swizzle. + + Since swizzling isn't necisaraly a bijection, a Swizzle can never + be truely inverted. However, the swizzle returned is *almost* the + inverse of this swizzle in the sense that, for each i in range(3), + a[a.inverse()[i]] is either i or SWIZZLE_NONE. If swizzle is just + a permutation with no channels added or removed, then this + function returns the actual inverse. + + This "pseudo-inverse" idea can be demonstrated by mapping from + luminance-alpha to rgba that is given by "xxxy". To get from rgba + to lumanence-alpha, we use Swizzle("xxxy").inverse() or "xw__". + This maps the first component in the lumanence-alpha texture is + the red component of the rgba image and the second to the alpha + component, exactly as you would expect. + """ + rev = [Swizzle.SWIZZLE_NONE] * 4 + for i in xrange(4): + for j in xrange(4): + if self.__list[j] == i and rev[i] == Swizzle.SWIZZLE_NONE: + rev[i] = j + return Swizzle(rev) + + +class Format: + """Describes a pixel format.""" + + def __init__(self, name, layout, block_width, block_height, channels, swizzle, colorspace): + """Constructs a Format from some metadata and a list of channels. + + The channel objects must be unique to this Format and should not be + re-used to construct another Format. This is because certain channel + information such as shift, offset, and the channel name are set when + the Format is created and are calculated based on the entire list of + channels. + + Arguments: + name -- Name of the format such as 'MESA_FORMAT_A8R8G8B8' + layout -- One of 'array', 'packed' 'other', or a compressed layout + block_width -- The block width if the format is compressed, 1 otherwise + block_height -- The block height if the format is compressed, 1 otherwise + channels -- A list of Channel objects + swizzle -- A Swizzle from this format to rgba + colorspace -- one of 'rgb', 'srgb', 'yuv', or 'zs' + """ + self.name = name + self.layout = layout + self.block_width = block_width + self.block_height = block_height + self.channels = channels + assert isinstance(swizzle, Swizzle) + self.swizzle = swizzle + self.name = name + assert colorspace in (RGB, SRGB, YUV, ZS) + self.colorspace = colorspace + + # Name the channels + chan_names = ['']*4 + if self.colorspace in (RGB, SRGB): + for (i, s) in enumerate(swizzle): + if s < 4: + chan_names[s] += 'rgba'[i] + elif colorspace == ZS: + for (i, s) in enumerate(swizzle): + if s < 4: + chan_names[s] += 'zs'[i] + else: + chan_names = ['x', 'y', 'z', 'w'] + + for c, name in zip(self.channels, chan_names): + assert c.name is None + if name == 'rgb': + c.name = 'l' + elif name == 'rgba': + c.name = 'i' + elif name == '': + c.name = 'x' + else: + c.name = name + + # Set indices and offsets + if self.layout == PACKED: + shift = 0 + for channel in self.channels: + assert channel.shift == -1 + channel.shift = shift + shift += channel.size + for idx, channel in enumerate(self.channels): + assert channel.index == -1 + channel.index = idx + else: + pass # Shift means nothing here + + def __str__(self): + return self.name + + def short_name(self): + """Returns a short name for a format. + + The short name should be suitable to be used as suffix in function + names. + """ + + name = self.name + if name.startswith('MESA_FORMAT_'): + name = name[len('MESA_FORMAT_'):] + name = name.lower() + return name + + def block_size(self): + """Returns the block size (in bits) of the format.""" + size = 0 + for channel in self.channels: + size += channel.size + return size + + def num_channels(self): + """Returns the number of channels in the format.""" + nr_channels = 0 + for channel in self.channels: + if channel.size: + nr_channels += 1 + return nr_channels + + def array_element(self): + """Returns a non-void channel if this format is an array, otherwise None. + + If the returned channel is not None, then this format can be + considered to be an array of num_channels() channels identical to the + returned channel. + """ + if self.layout == ARRAY: + return self.channels[0] + elif self.layout == PACKED: + ref_channel = self.channels[0] + if ref_channel.type == VOID: + ref_channel = self.channels[1] + for channel in self.channels: + if channel.size == 0 or channel.type == VOID: + continue + if channel.size != ref_channel.size or channel.size % 8 != 0: + return None + if channel.type != ref_channel.type: + return None + if channel.norm != ref_channel.norm: + return None + return ref_channel + else: + return None + + def is_array(self): + """Returns true if this format can be considered an array format. + + This function will return true if self.layout == 'array'. However, + some formats, such as MESA_FORMAT_A8G8B8R8, can be considered as + array formats even though they are technically packed. + """ + return self.array_element() != None + + def is_compressed(self): + """Returns true if this is a compressed format.""" + return self.block_width != 1 or self.block_height != 1 + + def is_int(self): + """Returns true if this format is an integer format. + + See also: is_norm() + """ + if self.layout not in (ARRAY, PACKED): + return False + for channel in self.channels: + if channel.type not in (VOID, UNSIGNED, SIGNED): + return False + return True + + def is_float(self): + """Returns true if this format is an floating-point format.""" + if self.layout not in (ARRAY, PACKED): + return False + for channel in self.channels: + if channel.type not in (VOID, FLOAT): + return False + return True + + def channel_type(self): + """Returns the type of the channels in this format.""" + _type = VOID + for c in self.channels: + if c.type == VOID: + continue + if _type == VOID: + _type = c.type + assert c.type == _type + return _type + + def channel_size(self): + """Returns the size (in bits) of the channels in this format. + + This function should only be called if all of the channels have the + same size. This is always the case if is_array() returns true. + """ + size = None + for c in self.channels: + if c.type == VOID: + continue + if size is None: + size = c.size + assert c.size == size + return size + + def max_channel_size(self): + """Returns the size of the largest channel.""" + size = 0 + for c in self.channels: + if c.type == VOID: + continue + size = max(size, c.size) + return size + + def is_normalized(self): + """Returns true if this format is normalized. + + While only integer formats can be normalized, not all integer formats + are normalized. Normalized integer formats are those where the + integer value is re-interpreted as a fixed point value in the range + [0, 1]. + """ + norm = None + for c in self.channels: + if c.type == VOID: + continue + if norm is None: + norm = c.norm + assert c.norm == norm + return norm + + def has_channel(self, name): + """Returns true if this format has the given channel.""" + if self.is_compressed(): + # Compressed formats are a bit tricky because the list of channels + # contains a single channel of type void. Since we don't have any + # channel information there, we pull it from the swizzle. + if str(self.swizzle) == 'xxxx': + return name == 'i' + elif str(self.swizzle)[0:3] in ('xxx', 'yyy'): + if name == 'l': + return True + elif name == 'a': + return self.swizzle['a'] <= Swizzle.SWIZZLE_W + else: + return False + elif name in 'rgba': + return self.swizzle[name] <= Swizzle.SWIZZLE_W + else: + return False + else: + for channel in self.channels: + if channel.name == name: + return True + return False + + def get_channel(self, name): + """Returns the channel with the given name if it exists.""" + for channel in self.channels: + if channel.name == name: + return channel + return None + +def _parse_channels(fields, layout, colorspace, swizzle): + channels = [] + for field in fields: + if not field: + continue + + type = field[0] if field[0] else 'x' + + if field[1] == 'n': + norm = True + size = int(field[2:]) + else: + norm = False + size = int(field[1:]) + + channel = Channel(type, norm, size) + channels.append(channel) + + return channels + +def parse(filename): + """Parse a format descrition in CSV format. + + This function parses the given CSV file and returns an iterable of + channels.""" + + with open(filename) as stream: + for line in stream: + try: + comment = line.index('#') + except ValueError: + pass + else: + line = line[:comment] + line = line.strip() + if not line: + continue + + fields = [field.strip() for field in line.split(',')] + + name = fields[0] + layout = fields[1] + block_width = int(fields[2]) + block_height = int(fields[3]) + colorspace = fields[9] + + swizzle = Swizzle(fields[8]) + channels = _parse_channels(fields[4:8], layout, colorspace, swizzle) + + yield Format(name, layout, block_width, block_height, channels, swizzle, colorspace) -- 2.30.2