Saturday, 30 May 2015

Gregorian Date Calculation Golf 1

It's been a while since my last post to this blog, so I thought I'd return to a guilty pleasure of mine: Needless optimizations of date calculations. See Computus 3.

Date calculation optimizations are often overlooked because of the scope for subtle errors. If you are writing production code that is going to be used in anger, I strongly suggest you check and re-check everything you see hereafter. At the very least, run it through a thorough test suite.

So, what are the rules of "Gregorian Date Calculation Golf"? Well...

  1. Pseudo-code must be presented in C/C++/C# flavour.
  2. The integer type "int" is expected to be at least 32 bits long.
  3. The integer type "long" is expected to be at least 64 bits long.
  4. A (proleptic) Gregorian year is assumed to be between 1 (meaning 1 CE) and 65535 inclusive.
  5. A Gregorian month is assumed to be between 1 (January) and 12 (December) inclusive.
  6. Leap days must be handled correctly.
  7. Time and timezones are ignored.
  8. Integer, arithmetic divisions (and their cousins) are not permitted.
Rule 8 is where the fun starts!

To warm up, let's assume we want to decide whether a Gregorian year is a leap year.

According to Wikipedia, the logic is:
if (year is not exactly divisible by 4) then (it is a common year)
else
if (year is not exactly divisible by 100) then (it is a leap year)
else
if (year is not exactly divisible by 400) then (it is a common year)
else (it is a leap year)
 So here's an obvious implementation:

  bool IsLeapYear(int year) {
    if ((year % 4) != 0) {
      return false;
    }
    if ((year % 100) != 0) {
      return true;
    }
    if ((year % 400) != 0) {
      return false;
    }
    return true;
  }

However, the "%" modulo operator involves an implicit division, violating our Rule 8. We can remove two of the divisions by using the bitwise-and operator "&":

  bool IsLeapYear(int year) {
    if ((year & 3) != 0) {
      return false;
    }
    int century = year / 100;
    if ((century * 100) != year) {
      return true;
    }
    if ((century & 3) != 0) {
      return false;
    }
    return true;
  }

However, there's still a division by a constant (100). Fortunately, we can use the fact that:
x / c = x * (1 / c)
This technique is known as "Integer Division by Constants". Indeed, any optimizing compiler worth its salt will convert the division in the code above to a multiplication. But, in the spirit of Gregorian Date Calculation Golf, we'll do it explicitly.

  bool IsLeapYear(int year) {
    if ((year & 3) != 0) {
      return false;
    }
    int century = (((year * 0x47AF) >> 16) + year) >> 7;
    if ((century * 100) != year) {
      return true;
    }
    if ((century & 3) != 0) {
      return false;
    }
    return true;
  }

This can be needlessly re-factored to:

  bool IsLeapYear(int year) {
    if ((year & 3) == 0)
    {
      int century = (((year * 0x47AF) >> 16) + year) >> 7;
      return ((century * 100) != year) || ((century & 3) == 0);
    }
    return false;
  }

So, can we convert Gregorian year/month/day triplets to and from serials (i.e. the number of days since an epochal date)? Of course we can! Next time.

Saturday, 6 September 2014

RGB/HSV in HLSL 6

It is often common to need to take a colour image and change some aspect of it: its hue, saturation, brightness or contrast.

You could simply convert the image to an appropriate colour space (e.g. HSV) perform the manipulations and then convert back to RGB. However, of the three components of the HSV colour space, it is least likely that you'll want to change the hue, which is trickiest component to convert. So can you change the saturation and value without performing the full round-trip conversions?

Remember that for RGB to HSV:

  float U = min(R, min(G, B));
  float V = max(R, max(G, B));
  float C = V - U;

  float S = C / (V + Epsilon);

The hue 'H' is given by the difference between two RGB channel values divided by 'V'.

So if we maintain the absolute differences between RGB channel values, we maintain the chroma 'C' (because the 'V' and 'U' values change by the same amount) but not necessarily the HSV saturation 'S' (because the denominator 'V' changes).

This implies we can easily change the chroma (or HSV saturation) by manipulating 'U' without any impact on the value 'V'. However, changing the value 'V' (or lightness) is not so easy without also changing the chroma (or HSV saturation).

Consider another formulation of HSV saturation (without the epsilon term):

  float S = C / V;
          = (V - U) / V;
          = 1 - (U / V);

It is dependent on the ratio of the minimum and maximum RGB channel values. So if we maintain this ratio, we do not affect the HSV saturation.

Anyway, let's start with what appears to be the easier modification first. Suppose we have an RGB triplet and we want to change its HSV saturation to 'S_wanted':

  float U = min(R, min(G, B));
  float V = max(R, max(G, B));

  float Q = S_wanted * V / (V - U + Epsilon);
  RGB = (V - RGB) * Q;

Above, 'Q' is equivalent to 'S_wanted / S' and 'Epsilon' is an appropriately tiny number, e.g. 1e-10.

Now the trickier modification. Suppose we have an RGB triplet and we want to change its HSV value to 'V_wanted' without changing its HSV saturation (or hue). We can multiply all channel values by
'V_wanted / V' and then apply the saturation modification algorithm above to restore the original saturation. But wait! The channel multiplication does not change the ratio of the minimum and maximum RGB channel values, so it therefore does not change the saturation after all. Only a simple multiplicative scaling is required:

  float V = max(R, max(G, B));
  RGB *= V_wanted / (V + Epsilon);

Please remember that we're talking about simplifying modifications in the HSV colour space here. HSL is another matter entirely. 

Monday, 25 August 2014

Saturday, 21 June 2014

Not-So-Mini

Have you noticed how big retro small cars are becoming?

Take the Fiat 500. The original, from 1957, was a masterpiece of miniaturisation. The 50th anniversary reprise was heavier, longer, wider, taller, but almost certainly safer.


Volkswagen's 1997 re-imagining of the original Beetle was nowhere near as bloated. But then, the original Beetle wasn't a particularly small car.


 (Note that all the images here are to the same scale)

Another big (cough) culprit is BMW with their 21st century Mini ... sorry MINI ... series. The abandoned Mini Rocketman model would have reversed the trend, but as it is, they just keep getting bigger.

 

Sunday, 15 June 2014

Candy Crush Saga

Candy Crush Saga is undoubtedly the crack cocaine of casual games. Alas, I too have fallen victim to its dubious charms. So much so that I've decided to go cold turkey after reaching Level 100 ("proof" below).


Fortunately, I managed to get to this milestone without making any micro-payments or mugging pensioners on their way back from the Post Office for the price of an extra life.

So no more furtive "match three" highs for me: I'm on the wagon. If anyone sees me with a coconut wheel beneath my mouse, poised for a chained combo, you have my permission to slap me across the face and give me a shot of insulin.

Monday, 26 May 2014

I Couldn't Care Fewer T-Shirt

 
I brake all you're rule's.
And couldn't care fewer!

Wednesday, 23 April 2014

RGB/HSV in HLSL 5

It's been over three years since I started this thread on performing RGB/HSV conversions in HLSL. In those days, I was working with the XBox360 GPU, which has a component-wise 'max4' instruction that I took full advantage of. AMD's GCN architecture has 'min3'/'med3'/'max3' in hardware. However, the focus of GPU hardware designers seems to be shifting away from specialist SIMD pipelines. All this means that something like

  float V = max(R, max(G, B));

doesn't get optimised as well as you might hope.

But, given time, the Internet (or more specifically, Sam Hocevar and Emil Persson) comes up with the solution. And very elegant it is too.

HSV (and HSL) can be computed easily from HCV, where H is the hue [0..1], C is the chroma [0..1] and V is the value [0..1]. The core of a RGB-to-HCV conversion requires the RGB components to be sorted and the smallest and largest extracted:

  float U = min(R, min(G, B));
  float V = max(R, max(G, B));
  float C = V - U;

Any fool knows that three numbers can be sorted using, at most, three comparisons. So why are we doing (effectively) four comparisons? To make things worse, we then go on to compare the largest with the three components again to determine the hue offset.

So, here is the crux of Sam and Emil's optimization:

  float Epsilon = 1e-10;
  float4 P = (G < B) ? float4(B, G, -1.0, 2.0/3.0) : float4(G, B, 0.0, -1.0/3.0);
  float4 Q = (R < P.x) ? float4(P.xyw, R) : float4(R, P.yzx);
  float V = Q.x;
  float C = V - min(Q.w, Q.y);
  float H = abs((Q.w - Q.y) / (6 * C + Epsilon) + Q.z);

'Q.x' contains the largest RGB component, V, and either 'Q.w' or 'Q.y' contains the smallest, U. By shuffling components of P and Q, we get the hue offset and divisor "for free." Also note the cunning use of 'Epsilon' to remove the need to check for division-by-zero. More details here.

Once we have hue/chroma/value, we can compute the HSV saturation [0..1]:

  float S_hsv = C / (V + Epsilon);

Similarly, we can compute the HSL lightness [0..1] and saturation:

  float L = V - C * 0.5;
  float S_hsl = C / (1 - abs(L * 2 - 1) + Epsilon);

In terms of speed ... a notoriously difficult thing to estimate for GPU shaders ... this new RGB-to-HCV function is no slower than the old one and faster for many configurations.

However (and here I feel a tad smug), my original implementation of HSV-to-RGB is still faster than Sam's alternative. Though I'm sure that with a particularly aggressive optimizer on a more scalar configuration, there'd be hardly anything in it.

  float R = abs(H * 6 - 3) - 1;
  float G = 2 - abs(H * 6 - 2);
  float B = 2 - abs(H * 6 - 4);
  float3 RGB = ((saturate(float3(R,G,B)) - 1) * S + 1) * V;

I've placed the HLSL snippets on my main website; I hope no typos have crept in like last time.

For completeness, I've left in the HCY and HCL conversions.