aboutsummaryrefslogtreecommitdiff
path: root/mesalib/src/mesa/main/format_parser.py
blob: 5e45c74de3e09aea1ef6df7073ac960edfd81d2f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
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)