mesa: Add python to parse the formats CSV file
authorJason Ekstrand <jason.ekstrand@intel.com>
Sat, 2 Aug 2014 01:09:46 +0000 (18:09 -0700)
committerJason Ekstrand <jason.ekstrand@intel.com>
Tue, 5 Aug 2014 17:56:15 +0000 (10:56 -0700)
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 <jason.ekstrand@intel.com>
Reviewed-by: Brian Paul <brianp@vmware.com>
src/mesa/main/format_parser.py [new file with mode: 0755]

diff --git a/src/mesa/main/format_parser.py b/src/mesa/main/format_parser.py
new file mode 100755 (executable)
index 0000000..5e45c74
--- /dev/null
@@ -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)