/*
 * colconv.c
 * Copyright (C) 2002 P.Everett <peter@everett9981.freeserve.co.uk>
 *
 * This file is part of KinoAMP, a free RISCOS MPEG program stream decoder.
 *
 * KinoAMP is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * KinoAMP is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * This software is based on Kino v0.3 by eQ R&D <http://www.eqrd.net>
 */

#include "ka_colconv.h"

#include "kernel.h"
#include "swis.h"
#include "inttypes.h"
#include "config.h"
#include "ka_drawers.h"
#include "ka_error.h"
#include "ka_mem.h"

typedef ka_drawer_f (*ColourConverter_fsetup)(ka_screen_t* screen, ka_colourspace_t* options, ka_drawer_f f);
typedef ka_drawer_f ka_drawer_list[ka_chroma_count][7];
typedef ka_drawer_f ka_drawer_z1list[2*ka_chroma_count];

// Build option, comment or uncomment at will
#define YUV_LOOKUP

/*
The 256-colours display uses a y5u4v4->colour number lookup table
and this build switch controls how this table is built.

1) If YUV_LOOKUP is #defined

In a first step that only needs to be done at startup or if a palette
change occurs, the 256-colours palette is translated to YUV and a normalized
y5u4v4->colour table is built from it by finding the best matching YUV
translated palette colour for each yuv triplet. This process is somewhat slow
but it doesn't really matter since this is a one-shot.

The second step is perform at startup and after any brightness/contrast/colour
level modification. It builds the real lookup table by applying the level
corrections to each yuv triplet and looking in the original table with
the level corrected triplet to find the corresponding colour number.

2) If YUV_LOOKUP is not #defined

In a first step that only needs to be done at startup or if a palette
change occurs, a r4g4b4->colour table is built from it by finding the best
matching RGB palette colour for each rgb triplet.

The second step is perform at startup and after any brightness/contrast/colour
level modification. It builds the real lookup table by applying the level
corrections to each yuv triplet, converting it to an r8g8b8 triplet and
using the r4g4b4->colour table to find the corresponding colour number.
*/

#define _8TO4(in,out)  (out) = ((in)+7)>>4;       \
                       if ((out)>15)  (out) = 15; \
                       if ((out)<0)   (out) = 0;
#define _8TO5(in,out)  (out) = ((in)+15)>>3;       \
                       if ((out)>31)  (out) = 31; \
                       if ((out)<0)   (out) = 0;

#define APPLY_BRIGHTNESS(v,brightness) (v)+(brightness-100)
#define APPLY_CONTRAST(v,contrast)     (((v)-128)*(contrast))/100+128
#define APPLY_COLOUR(v,colour)         ((v)*(colour))/100

/*
 * Colour conversion for 16 and 32 bit colour modes
 * -----------------------------------------------------------------------------
 */

// defined in s.yuvtables
extern uint8_t ka_Y_Map[256];
extern uint8_t ka_UV_Map[256];

extern int ka_U_Table[256];
extern int ka_V_Table[256];
extern uint8_t ka_Yc_Table_8bit[2*(224*2+256+224*2)];
extern uint8_t ka_Yc_Table_6bit[2*(224*2+256+224*2)];
extern uint8_t ka_Yc_Table_5bit[2*(224*2+256+224*2)];
extern uint8_t ka_Yc_Table_4bit[2*(224*2+256+224*2)];
extern uint32_t ka_Yc_Table_6bit_hi_q[224*2+256+224*2];
extern uint32_t ka_Yc_Table_5bit_hi_q[224*2+256+224*2];
extern uint32_t ka_Yc_Table_4bit_hi_q[224*2+256+224*2];

#define YC 1.164
#define UR 1.596
#define UG 0.813
#define VG 0.391
#define VB 2.018

/**
 * Builds chrominance to luminance conversion tables.
 *
 * @param  colour  Colour saturation level to apply.
 *                 Normal level is 100%, no colour is 0%.
 */
static void build_uv_table(int colour)
{
  int i, a, b;

  // Ur, Ug [0, 255]
  for (i=0; i<256; i++)
  {
    int uv;
    uv = i-128;
    uv = APPLY_COLOUR(uv, colour);
    a = (int)(UR/YC*(float)uv);
    b = (int)(UG/YC*(float)uv);
    ka_U_Table[i] = ((a & 0xffff)<<16) + (b & 0xffff);
  }

  // Vb, Vg [0, 255]
  for (i=0; i<256; i++)
  {
    int uv;
    uv = i-128;
    uv = APPLY_COLOUR(uv, colour);
    a = (int)(VG/YC*(float)uv);
    b = (int)(VB/YC*(float)uv);
    ka_V_Table[i] = ((b & 0xffff)<<16) + (a & 0xffff);
  }
}

/**
 * Builds luminance to 4-bit colour intensity table.
 *
 * @param  brightness  Brightness correction to apply.
 * @param  contrast    Contrast correction to apply.
 * @param  dither      1 to use dithering, 0 otherwise.
 */

static void build_y12_table(int brightness, int contrast, int dither)
{
  int i, a;

  // Yc [-224*2, 255+ 224*2] due to (U, V) -> Y translation
  for (i=-224*2; i<(256+224*2); i++)
  {
    int err, y0, y1;
    a = (int)(YC*(float)((int)(i-16)));
    a = APPLY_BRIGHTNESS(a, brightness);
    a = APPLY_CONTRAST(a, contrast);
    if ((a < 16) || !dither)
      y0 = y1 = (a+8)/16;
    else {
      err = a & 15;
      a = a/16;
      if (err < 4)
        y0 = y1 = a;
      else if (err >= 12)
        y0 = y1 = a+1;
      else {
        y0 = a;
        y1 = a+1;
      }
    }
    if (y0 < 0)   y0 = 0;
    if (y0 > 15)  y0 = 15;
    if (y1 < 0)   y1 = 0;
    if (y1 > 15)  y1 = 15;
    ka_Yc_Table_4bit[i+224*2] = y0;
    ka_Yc_Table_4bit[i+224*2+(2*224+256+2*224)] = y1;
    ka_Yc_Table_4bit_hi_q[i+224*2] = y0 | (y1<<16);
  }
}

/**
 * Builds luminance to 5-bit colour intensity table.
 *
 * @param  brightness  Brightness correction to apply.
 * @param  contrast    Contrast correction to apply.
 * @param  dither      1 to use dithering, 0 otherwise.
 */

static void build_y15_table(int brightness, int contrast, int dither)
{
  int i, a;

  // Yc [-224*2, 255+ 224*2] due to (U, V) -> Y translation
  for (i=-224*2; i<(256+224*2); i++)
  {
    int err, y0, y1;
    a = (int)(YC*(float)((int)(i-16)));
    a = APPLY_BRIGHTNESS(a, brightness);
    a = APPLY_CONTRAST(a, contrast);
    if ((a < 16) || !dither)
      y0 = y1 = (a+4)/8;
    else {
      err = a & 7;
      a = a/8;
      if (err < 2)
        y0 = y1 = a;
      else if (err >= 6)
        y0 = y1 = a+1;
      else {
        y0 = a;
        y1 = a+1;
      }
    }
    if (y0 < 0)   y0 = 0;
    if (y0 > 31)  y0 = 31;
    if (y1 < 0)   y1 = 0;
    if (y1 > 31)  y1 = 31;
    ka_Yc_Table_5bit[i+224*2] = y0;
    ka_Yc_Table_5bit[i+224*2+(2*224+256+2*224)] = y1;
    ka_Yc_Table_5bit_hi_q[i+224*2] = y0 | (y1<<16);
  }
}

/**
 * Builds luminance to 6-bit colour intensity table.
 *
 * @param  brightness  Brightness correction to apply.
 * @param  contrast    Contrast correction to apply.
 * @param  dither      1 to use dithering, 0 otherwise.
 */

static void build_y16_table(int brightness, int contrast, int dither)
{
  int i, a;

  // Yc [-224*2, 255+ 224*2] due to (U, V) -> Y translation
  for (i=-224*2; i<(256+224*2); i++)
  {
    int err, y0, y1;
    a = (int)(YC*(float)((int)(i-16)));
    a = APPLY_BRIGHTNESS(a, brightness);
    a = APPLY_CONTRAST(a, contrast);
    if ((a < 16) || !dither)
      y0 = y1 = (a+2)/4;
    else {
      err = a & 3;
      a = a/4;
      if (err < 1)
        y0 = y1 = a;
      else if (err >= 3)
        y0 = y1 = a+1;
      else {
        y0 = a;
        y1 = a+1;
      }
    }
    if (y0 < 0)   y0 = 0;
    if (y0 > 63)  y0 = 63;
    if (y1 < 0)   y1 = 0;
    if (y1 > 63)  y1 = 63;
    ka_Yc_Table_6bit[i+224*2] = y0;
    ka_Yc_Table_6bit[i+224*2+(2*224+256+2*224)] = y1;
    ka_Yc_Table_6bit_hi_q[i+224*2] = y0 | (y1<<16);
  }
}

/**
 * Builds luminance to 8-bit colour intensity table.
 *
 * @param  brightness  Brightness correction to apply.
 * @param  contrast    Contrast correction to apply.
 */
static void build_y32_table(int brightness, int contrast)
{
  int i, a;

  // Yc [-224*2, 255+ 224*2] due to (U, V) -> Y translation
  for (i=-224*2; i<(256+224*2); i++)
  {
    a = (int)(0.5 + YC*(float)((int)(i-16)));
    a = APPLY_BRIGHTNESS(a, brightness);
    a = APPLY_CONTRAST(a, contrast);

    if(a < 0) a = 0;
    if(a > 255) a = 255;
    ka_Yc_Table_8bit[i+224*2] = a;
  }
}

static const ka_drawer_list drawers_tbgr12 =
{
  // YUV 400 = mono
  { ka_drawy_z05_tbgr12
  , ka_drawy_z1_tbgr12
  , ka_drawy_z2_tbgr12
  , ka_drawy_z3_tbgr12
  , ka_drawy_z4_tbgr12
  , ka_drawy_z2_tbgr12
  , ka_drawy_z1_tbgr12
  }
  // YUV 420
, { ka_drawyuv420_z05_tbgr12
  , ka_drawyuv420_z1_tbgr12
  , ka_drawyuv420_z2_tbgr12
  , ka_drawyuv420_z3_tbgr12
  , ka_drawyuv420_z4_tbgr12
  , ka_drawyuv420_z2_tbgr12_hq
  , ka_drawyuv420_z1_tbgr12_neon
  }
  // YUV 422
, { ka_drawyuv422_z05_tbgr12
  , ka_drawyuv422_z1_tbgr12
  , ka_drawyuv422_z2_tbgr12
  , ka_drawyuv422_z3_tbgr12
  , ka_drawyuv422_z4_tbgr12
  , ka_drawyuv422_z2_tbgr12_hq
  , ka_drawyuv422_z1_tbgr12_neon
  }
  // YUV 444
, { ka_drawyuv444_z05_tbgr12
  , ka_drawyuv444_z1_tbgr12
  , ka_drawyuv444_z2_tbgr12
  , ka_drawyuv444_z3_tbgr12
  , ka_drawyuv444_z4_tbgr12
  , ka_drawyuv444_z2_tbgr12_hq
  , ka_drawyuv444_z1_tbgr12_neon
  }
  // BGR
, { ka_drawbgr_z05_tbgr12
  , ka_drawbgr_z1_tbgr12
  , ka_drawbgr_z2_tbgr12
  , ka_drawbgr_z3_tbgr12
  , ka_drawbgr_z4_tbgr12
  , ka_drawbgr_z2_tbgr12
  , ka_drawbgr_z1_tbgr12
  }
};

static const ka_drawer_list drawers_abgr12 =
{
  // YUV 400 = mono
  { ka_drawy_z05_abgr12
  , ka_drawy_z1_abgr12
  , ka_drawy_z2_abgr12
  , ka_drawy_z3_abgr12
  , ka_drawy_z4_abgr12
  , ka_drawy_z2_abgr12
  , ka_drawy_z1_abgr12
  }
  // YUV 420
, { ka_drawyuv420_z05_abgr12
  , ka_drawyuv420_z1_abgr12
  , ka_drawyuv420_z2_abgr12
  , ka_drawyuv420_z3_abgr12
  , ka_drawyuv420_z4_abgr12
  , ka_drawyuv420_z2_abgr12_hq
  , ka_drawyuv420_z1_abgr12_neon
  }
  // YUV 422
, { ka_drawyuv422_z05_abgr12
  , ka_drawyuv422_z1_abgr12
  , ka_drawyuv422_z2_abgr12
  , ka_drawyuv422_z3_abgr12
  , ka_drawyuv422_z4_abgr12
  , ka_drawyuv422_z2_abgr12_hq
  , ka_drawyuv422_z1_abgr12_neon
  }
  // YUV 444
, { ka_drawyuv444_z05_abgr12
  , ka_drawyuv444_z1_abgr12
  , ka_drawyuv444_z2_abgr12
  , ka_drawyuv444_z3_abgr12
  , ka_drawyuv444_z4_abgr12
  , ka_drawyuv444_z2_abgr12_hq
  , ka_drawyuv444_z1_abgr12_neon
  }
  // BGR
, { ka_drawbgr_z05_abgr12
  , ka_drawbgr_z1_abgr12
  , ka_drawbgr_z2_abgr12
  , ka_drawbgr_z3_abgr12
  , ka_drawbgr_z4_abgr12
  , ka_drawbgr_z2_abgr12
  , ka_drawbgr_z1_abgr12
  }
};

static const ka_drawer_list drawers_trgb12 =
{
  // YUV 400 = mono
  { ka_drawy_z05_tbgr12
  , ka_drawy_z1_tbgr12
  , ka_drawy_z2_tbgr12
  , ka_drawy_z3_tbgr12
  , ka_drawy_z4_tbgr12
  , ka_drawy_z2_tbgr12
  , ka_drawy_z1_tbgr12
  }
  // YUV 420
, { ka_drawyuv420_z05_trgb12
  , ka_drawyuv420_z1_trgb12
  , ka_drawyuv420_z2_trgb12
  , ka_drawyuv420_z3_trgb12
  , ka_drawyuv420_z4_trgb12
  , ka_drawyuv420_z2_trgb12_hq
  , ka_drawyuv420_z1_trgb12_neon
  }
  // YUV 422
, { ka_drawyuv422_z05_trgb12
  , ka_drawyuv422_z1_trgb12
  , ka_drawyuv422_z2_trgb12
  , ka_drawyuv422_z3_trgb12
  , ka_drawyuv422_z4_trgb12
  , ka_drawyuv422_z2_trgb12_hq
  , ka_drawyuv422_z1_trgb12_neon
  }
  // YUV 444
, { ka_drawyuv444_z05_trgb12
  , ka_drawyuv444_z1_trgb12
  , ka_drawyuv444_z2_trgb12
  , ka_drawyuv444_z3_trgb12
  , ka_drawyuv444_z4_trgb12
  , ka_drawyuv444_z2_trgb12_hq
  , ka_drawyuv444_z1_trgb12_neon
  }
  // BGR
, { ka_drawbgr_z05_trgb12
  , ka_drawbgr_z1_trgb12
  , ka_drawbgr_z2_trgb12
  , ka_drawbgr_z3_trgb12
  , ka_drawbgr_z4_trgb12
  , ka_drawbgr_z2_trgb12
  , ka_drawbgr_z1_trgb12
  }
};

static const ka_drawer_list drawers_argb12 =
{
  // YUV 400 = mono
  { ka_drawy_z05_abgr12
  , ka_drawy_z1_abgr12
  , ka_drawy_z2_abgr12
  , ka_drawy_z3_abgr12
  , ka_drawy_z4_abgr12
  , ka_drawy_z2_abgr12
  , ka_drawy_z1_abgr12
  }
  // YUV 420
, { ka_drawyuv420_z05_argb12
  , ka_drawyuv420_z1_argb12
  , ka_drawyuv420_z2_argb12
  , ka_drawyuv420_z3_argb12
  , ka_drawyuv420_z4_argb12
  , ka_drawyuv420_z2_argb12_hq
  , ka_drawyuv420_z1_argb12_neon
  }
  // YUV 422
, { ka_drawyuv422_z05_argb12
  , ka_drawyuv422_z1_argb12
  , ka_drawyuv422_z2_argb12
  , ka_drawyuv422_z3_argb12
  , ka_drawyuv422_z4_argb12
  , ka_drawyuv422_z2_argb12_hq
  , ka_drawyuv422_z1_argb12_neon
  }
  // YUV 444
, { ka_drawyuv444_z05_argb12
  , ka_drawyuv444_z1_argb12
  , ka_drawyuv444_z2_argb12
  , ka_drawyuv444_z3_argb12
  , ka_drawyuv444_z4_argb12
  , ka_drawyuv444_z2_argb12_hq
  , ka_drawyuv444_z1_argb12_neon
  }
  // BGR
, { ka_drawbgr_z05_argb12
  , ka_drawbgr_z1_argb12
  , ka_drawbgr_z2_argb12
  , ka_drawbgr_z3_argb12
  , ka_drawbgr_z4_argb12
  , ka_drawbgr_z2_argb12
  , ka_drawbgr_z1_argb12
  }
};

static const ka_drawer_list drawers_tbgr15 =
{
  // YUV 400 = mono
  { ka_drawy_z05_tbgr15
  , ka_drawy_z1_tbgr15
  , ka_drawy_z2_tbgr15
  , ka_drawy_z3_tbgr15
  , ka_drawy_z4_tbgr15
  , ka_drawy_z2_tbgr15
  , ka_drawy_z1_tbgr15
  }
  // YUV 420
, { ka_drawyuv420_z05_tbgr15
  , ka_drawyuv420_z1_tbgr15
  , ka_drawyuv420_z2_tbgr15
  , ka_drawyuv420_z3_tbgr15
  , ka_drawyuv420_z4_tbgr15
  , ka_drawyuv420_z2_tbgr15_hq
  , ka_drawyuv420_z1_tbgr15_neon
  }
  // YUV 422
, { ka_drawyuv422_z05_tbgr15
  , ka_drawyuv422_z1_tbgr15
  , ka_drawyuv422_z2_tbgr15
  , ka_drawyuv422_z3_tbgr15
  , ka_drawyuv422_z4_tbgr15
  , ka_drawyuv422_z2_tbgr15_hq
  , ka_drawyuv422_z1_tbgr15_neon
  }
  // YUV 444
, { ka_drawyuv444_z05_tbgr15
  , ka_drawyuv444_z1_tbgr15
  , ka_drawyuv444_z2_tbgr15
  , ka_drawyuv444_z3_tbgr15
  , ka_drawyuv444_z4_tbgr15
  , ka_drawyuv444_z2_tbgr15_hq
  , ka_drawyuv444_z1_tbgr15_neon
  }
  // BGR
, { ka_drawbgr_z05_tbgr15
  , ka_drawbgr_z1_tbgr15
  , ka_drawbgr_z2_tbgr15
  , ka_drawbgr_z3_tbgr15
  , ka_drawbgr_z4_tbgr15
  , ka_drawbgr_z2_tbgr15
  , ka_drawbgr_z1_tbgr15
  }
};

static const ka_drawer_list drawers_abgr15 =
{
  // YUV 400 = mono
  { ka_drawy_z05_abgr15
  , ka_drawy_z1_abgr15
  , ka_drawy_z2_abgr15
  , ka_drawy_z3_abgr15
  , ka_drawy_z4_abgr15
  , ka_drawy_z2_abgr15
  , ka_drawy_z1_abgr15
  }
  // YUV 420
, { ka_drawyuv420_z05_abgr15
  , ka_drawyuv420_z1_abgr15
  , ka_drawyuv420_z2_abgr15
  , ka_drawyuv420_z3_abgr15
  , ka_drawyuv420_z4_abgr15
  , ka_drawyuv420_z2_abgr15_hq
  , ka_drawyuv420_z1_abgr15_neon
  }
  // YUV 422
, { ka_drawyuv422_z05_abgr15
  , ka_drawyuv422_z1_abgr15
  , ka_drawyuv422_z2_abgr15
  , ka_drawyuv422_z3_abgr15
  , ka_drawyuv422_z4_abgr15
  , ka_drawyuv422_z2_abgr15_hq
  , ka_drawyuv422_z1_abgr15_neon
  }
  // YUV 444
, { ka_drawyuv444_z05_abgr15
  , ka_drawyuv444_z1_abgr15
  , ka_drawyuv444_z2_abgr15
  , ka_drawyuv444_z3_abgr15
  , ka_drawyuv444_z4_abgr15
  , ka_drawyuv444_z2_abgr15_hq
  , ka_drawyuv444_z1_abgr15_neon
  }
  // BGR
, { ka_drawbgr_z05_abgr15
  , ka_drawbgr_z1_abgr15
  , ka_drawbgr_z2_abgr15
  , ka_drawbgr_z3_abgr15
  , ka_drawbgr_z4_abgr15
  , ka_drawbgr_z2_abgr15
  , ka_drawbgr_z1_abgr15
  }
};

static const ka_drawer_list drawers_trgb15 =
{
  // YUV 400 = mono
  { ka_drawy_z05_tbgr15
  , ka_drawy_z1_tbgr15
  , ka_drawy_z2_tbgr15
  , ka_drawy_z3_tbgr15
  , ka_drawy_z4_tbgr15
  , ka_drawy_z2_tbgr15
  , ka_drawy_z1_tbgr15
  }
  // YUV 420
, { ka_drawyuv420_z05_trgb15
  , ka_drawyuv420_z1_trgb15
  , ka_drawyuv420_z2_trgb15
  , ka_drawyuv420_z3_trgb15
  , ka_drawyuv420_z4_trgb15
  , ka_drawyuv420_z2_trgb15_hq
  , ka_drawyuv420_z1_trgb15_neon
  }
  // YUV 422
, { ka_drawyuv422_z05_trgb15
  , ka_drawyuv422_z1_trgb15
  , ka_drawyuv422_z2_trgb15
  , ka_drawyuv422_z3_trgb15
  , ka_drawyuv422_z4_trgb15
  , ka_drawyuv422_z2_trgb15_hq
  , ka_drawyuv422_z1_trgb15_neon
  }
  // YUV 444
, { ka_drawyuv444_z05_trgb15
  , ka_drawyuv444_z1_trgb15
  , ka_drawyuv444_z2_trgb15
  , ka_drawyuv444_z3_trgb15
  , ka_drawyuv444_z4_trgb15
  , ka_drawyuv444_z2_trgb15_hq
  , ka_drawyuv444_z1_trgb15_neon
  }
  // BGR
, { ka_drawbgr_z05_trgb15
  , ka_drawbgr_z1_trgb15
  , ka_drawbgr_z2_trgb15
  , ka_drawbgr_z3_trgb15
  , ka_drawbgr_z4_trgb15
  , ka_drawbgr_z2_trgb15
  , ka_drawbgr_z1_trgb15
  }
};

static const ka_drawer_list drawers_argb15 =
{
  // YUV 400 = mono
  { ka_drawy_z05_abgr15
  , ka_drawy_z1_abgr15
  , ka_drawy_z2_abgr15
  , ka_drawy_z3_abgr15
  , ka_drawy_z4_abgr15
  , ka_drawy_z2_abgr15
  , ka_drawy_z1_abgr15
  }
  // YUV 420
, { ka_drawyuv420_z05_argb15
  , ka_drawyuv420_z1_argb15
  , ka_drawyuv420_z2_argb15
  , ka_drawyuv420_z3_argb15
  , ka_drawyuv420_z4_argb15
  , ka_drawyuv420_z2_argb15_hq
  , ka_drawyuv420_z1_argb15_neon
  }
  // YUV 422
, { ka_drawyuv422_z05_argb15
  , ka_drawyuv422_z1_argb15
  , ka_drawyuv422_z2_argb15
  , ka_drawyuv422_z3_argb15
  , ka_drawyuv422_z4_argb15
  , ka_drawyuv422_z2_argb15_hq
  , ka_drawyuv422_z1_argb15_neon
  }
  // YUV 444
, { ka_drawyuv444_z05_argb15
  , ka_drawyuv444_z1_argb15
  , ka_drawyuv444_z2_argb15
  , ka_drawyuv444_z3_argb15
  , ka_drawyuv444_z4_argb15
  , ka_drawyuv444_z2_argb15_hq
  , ka_drawyuv444_z1_argb15_neon
  }
  // BGR
, { ka_drawbgr_z05_argb15
  , ka_drawbgr_z1_argb15
  , ka_drawbgr_z2_argb15
  , ka_drawbgr_z3_argb15
  , ka_drawbgr_z4_argb15
  , ka_drawbgr_z2_argb15
  , ka_drawbgr_z1_argb15
  }
};

static const ka_drawer_list drawers_bgr16 =
{
  // YUV 400 = mono
  { ka_drawy_z05_bgr16
  , ka_drawy_z1_bgr16
  , ka_drawy_z2_bgr16
  , ka_drawy_z3_bgr16
  , ka_drawy_z4_bgr16
  , ka_drawy_z2_bgr16
  , ka_drawy_z1_bgr16
  }
  // YUV 420
, { ka_drawyuv420_z05_bgr16
  , ka_drawyuv420_z1_bgr16
  , ka_drawyuv420_z2_bgr16
  , ka_drawyuv420_z3_bgr16
  , ka_drawyuv420_z4_bgr16
  , ka_drawyuv420_z2_bgr16_hq
  , ka_drawyuv420_z1_bgr16_neon
  }
  // YUV 422
, { ka_drawyuv422_z05_bgr16
  , ka_drawyuv422_z1_bgr16
  , ka_drawyuv422_z2_bgr16
  , ka_drawyuv422_z3_bgr16
  , ka_drawyuv422_z4_bgr16
  , ka_drawyuv422_z2_bgr16_hq
  , ka_drawyuv422_z1_bgr16_neon
  }
  // YUV 444
, { ka_drawyuv444_z05_bgr16
  , ka_drawyuv444_z1_bgr16
  , ka_drawyuv444_z2_bgr16
  , ka_drawyuv444_z3_bgr16
  , ka_drawyuv444_z4_bgr16
  , ka_drawyuv444_z2_bgr16_hq
  , ka_drawyuv444_z1_bgr16_neon
  }
  // BGR
, { ka_drawbgr_z05_bgr16
  , ka_drawbgr_z1_bgr16
  , ka_drawbgr_z2_bgr16
  , ka_drawbgr_z3_bgr16
  , ka_drawbgr_z4_bgr16
  , ka_drawbgr_z2_bgr16
  , ka_drawbgr_z1_bgr16
  }
};

static const ka_drawer_list drawers_rgb16 =
{
  // YUV 400 = mono
  { ka_drawy_z05_bgr16
  , ka_drawy_z1_bgr16
  , ka_drawy_z2_bgr16
  , ka_drawy_z3_bgr16
  , ka_drawy_z4_bgr16
  , ka_drawy_z2_bgr16
  , ka_drawy_z1_bgr16
  }
  // YUV 420
, { ka_drawyuv420_z05_rgb16
  , ka_drawyuv420_z1_rgb16
  , ka_drawyuv420_z2_rgb16
  , ka_drawyuv420_z3_rgb16
  , ka_drawyuv420_z4_rgb16
  , ka_drawyuv420_z2_rgb16_hq
  , ka_drawyuv420_z1_rgb16_neon
  }
  // YUV 422
, { ka_drawyuv422_z05_rgb16
  , ka_drawyuv422_z1_rgb16
  , ka_drawyuv422_z2_rgb16
  , ka_drawyuv422_z3_rgb16
  , ka_drawyuv422_z4_rgb16
  , ka_drawyuv422_z2_rgb16_hq
  , ka_drawyuv422_z1_rgb16_neon
  }
  // YUV 444
, { ka_drawyuv444_z05_rgb16
  , ka_drawyuv444_z1_rgb16
  , ka_drawyuv444_z2_rgb16
  , ka_drawyuv444_z3_rgb16
  , ka_drawyuv444_z4_rgb16
  , ka_drawyuv444_z2_rgb16_hq
  , ka_drawyuv444_z1_rgb16_neon
  }
  // BGR
, { ka_drawbgr_z05_rgb16
  , ka_drawbgr_z1_rgb16
  , ka_drawbgr_z2_rgb16
  , ka_drawbgr_z3_rgb16
  , ka_drawbgr_z4_rgb16
  , ka_drawbgr_z2_rgb16
  , ka_drawbgr_z1_rgb16
  }
};

static const ka_drawer_list drawers_tbgr32 =
{ // YUV 400 == mono
  { ka_drawy_z05_tbgr32
  , ka_drawy_z1_tbgr32
  , ka_drawy_z2_tbgr32
  , ka_drawy_z3_tbgr32
  , ka_drawy_z4_tbgr32
  , ka_drawy_z2_tbgr32
  , ka_drawy_z1_tbgr32
  }
  // YUV 420
, { ka_drawyuv420_z05_tbgr32
  , ka_drawyuv420_z1_tbgr32
  , ka_drawyuv420_z2_tbgr32
  , ka_drawyuv420_z3_tbgr32
  , ka_drawyuv420_z4_tbgr32
  , ka_drawyuv420_z2_tbgr32
  , ka_drawyuv420_z1_tbgr32_neon
  }
  // YUV 422
, { ka_drawyuv422_z05_tbgr32
  , ka_drawyuv422_z1_tbgr32
  , ka_drawyuv422_z2_tbgr32
  , ka_drawyuv422_z3_tbgr32
  , ka_drawyuv422_z4_tbgr32
  , ka_drawyuv422_z2_tbgr32
  , ka_drawyuv422_z1_tbgr32_neon
  }
  // YUV 444
, { ka_drawyuv444_z05_tbgr32
  , ka_drawyuv444_z1_tbgr32
  , ka_drawyuv444_z2_tbgr32
  , ka_drawyuv444_z3_tbgr32
  , ka_drawyuv444_z4_tbgr32
  , ka_drawyuv444_z2_tbgr32
  , ka_drawyuv444_z1_tbgr32_neon
  }
  // BGR
, { ka_drawbgr_z05_tbgr32
  , ka_drawbgr_z1_tbgr32
  , ka_drawbgr_z2_tbgr32
  , ka_drawbgr_z3_tbgr32
  , ka_drawbgr_z4_tbgr32
  , ka_drawbgr_z2_tbgr32
  , ka_drawbgr_z1_tbgr32
  }
};

static const ka_drawer_list drawers_abgr32 =
{ // YUV 400 == mono
  { ka_drawy_z05_abgr32
  , ka_drawy_z1_abgr32
  , ka_drawy_z2_abgr32
  , ka_drawy_z3_abgr32
  , ka_drawy_z4_abgr32
  , ka_drawy_z2_abgr32
  , ka_drawy_z1_abgr32
  }
  // YUV 420
, { ka_drawyuv420_z05_abgr32
  , ka_drawyuv420_z1_abgr32
  , ka_drawyuv420_z2_abgr32
  , ka_drawyuv420_z3_abgr32
  , ka_drawyuv420_z4_abgr32
  , ka_drawyuv420_z2_abgr32
  , ka_drawyuv420_z1_abgr32_neon
  }
  // YUV 422
, { ka_drawyuv422_z05_abgr32
  , ka_drawyuv422_z1_abgr32
  , ka_drawyuv422_z2_abgr32
  , ka_drawyuv422_z3_abgr32
  , ka_drawyuv422_z4_abgr32
  , ka_drawyuv422_z2_abgr32
  , ka_drawyuv422_z1_abgr32_neon
  }
  // YUV 444
, { ka_drawyuv444_z05_abgr32
  , ka_drawyuv444_z1_abgr32
  , ka_drawyuv444_z2_abgr32
  , ka_drawyuv444_z3_abgr32
  , ka_drawyuv444_z4_abgr32
  , ka_drawyuv444_z2_abgr32
  , ka_drawyuv444_z1_abgr32_neon
  }
  // BGR
, { ka_drawbgr_z05_abgr32
  , ka_drawbgr_z1_abgr32
  , ka_drawbgr_z2_abgr32
  , ka_drawbgr_z3_abgr32
  , ka_drawbgr_z4_abgr32
  , ka_drawbgr_z2_abgr32
  , ka_drawbgr_z1_abgr32
  }
};

static const ka_drawer_list drawers_trgb32 =
{ // YUV 400 == mono
  { ka_drawy_z05_tbgr32
  , ka_drawy_z1_tbgr32
  , ka_drawy_z2_tbgr32
  , ka_drawy_z3_tbgr32
  , ka_drawy_z4_tbgr32
  , ka_drawy_z2_tbgr32
  , ka_drawy_z1_tbgr32
  }
  // YUV 420
, { ka_drawyuv420_z05_trgb32
  , ka_drawyuv420_z1_trgb32
  , ka_drawyuv420_z2_trgb32
  , ka_drawyuv420_z3_trgb32
  , ka_drawyuv420_z4_trgb32
  , ka_drawyuv420_z2_trgb32
  , ka_drawyuv420_z1_trgb32_neon
  }
  // YUV 422
, { ka_drawyuv422_z05_trgb32
  , ka_drawyuv422_z1_trgb32
  , ka_drawyuv422_z2_trgb32
  , ka_drawyuv422_z3_trgb32
  , ka_drawyuv422_z4_trgb32
  , ka_drawyuv422_z2_trgb32
  , ka_drawyuv422_z1_trgb32_neon
  }
  // YUV 444
, { ka_drawyuv444_z05_trgb32
  , ka_drawyuv444_z1_trgb32
  , ka_drawyuv444_z2_trgb32
  , ka_drawyuv444_z3_trgb32
  , ka_drawyuv444_z4_trgb32
  , ka_drawyuv444_z2_trgb32
  , ka_drawyuv444_z1_trgb32_neon
  }
  // BGR
, { ka_drawbgr_z05_trgb32
  , ka_drawbgr_z1_trgb32
  , ka_drawbgr_z2_trgb32
  , ka_drawbgr_z3_trgb32
  , ka_drawbgr_z4_trgb32
  , ka_drawbgr_z2_trgb32
  , ka_drawbgr_z1_trgb32
  }
};

static const ka_drawer_list drawers_argb32 =
{ // YUV 400 == mono
  { ka_drawy_z05_abgr32
  , ka_drawy_z1_abgr32
  , ka_drawy_z2_abgr32
  , ka_drawy_z3_abgr32
  , ka_drawy_z4_abgr32
  , ka_drawy_z2_abgr32
  , ka_drawy_z1_abgr32
  }
  // YUV 420
, { ka_drawyuv420_z05_argb32
  , ka_drawyuv420_z1_argb32
  , ka_drawyuv420_z2_argb32
  , ka_drawyuv420_z3_argb32
  , ka_drawyuv420_z4_argb32
  , ka_drawyuv420_z2_argb32
  , ka_drawyuv420_z1_argb32_neon
  }
  // YUV 422
, { ka_drawyuv422_z05_argb32
  , ka_drawyuv422_z1_argb32
  , ka_drawyuv422_z2_argb32
  , ka_drawyuv422_z3_argb32
  , ka_drawyuv422_z4_argb32
  , ka_drawyuv422_z2_argb32
  , ka_drawyuv422_z1_argb32_neon
  }
  // YUV 444
, { ka_drawyuv444_z05_argb32
  , ka_drawyuv444_z1_argb32
  , ka_drawyuv444_z2_argb32
  , ka_drawyuv444_z3_argb32
  , ka_drawyuv444_z4_argb32
  , ka_drawyuv444_z2_argb32
  , ka_drawyuv444_z1_argb32_neon
  }
  // BGR
, { ka_drawbgr_z05_argb32
  , ka_drawbgr_z1_argb32
  , ka_drawbgr_z2_argb32
  , ka_drawbgr_z3_argb32
  , ka_drawbgr_z4_argb32
  , ka_drawbgr_z2_argb32
  , ka_drawbgr_z1_argb32
  }
};

/**
 * Builds YUV to [T|A][BGR|RGB]12 colour conversion tables and selects the most appropriate
 * drawer for the current display options.
 *
 * @param  screen    Output screen parameters.
 * @param  options   Redraw options.
 * @param  f         Preset drawer function.
 *
 * @returns Drawer function to use for plotting output.
 */
static ka_drawer_f ColourConverter_setupYUVto12bpp(ka_screen_t* screen, ka_colourspace_t* options, ka_drawer_f f)
{
  screen = screen; // Suppress warning
  build_uv_table(options->colour);
  build_y12_table(options->brightness, options->contrast
                , (options->config & cfg_video_dither) ? 1 : 0);

  return f;
}

/**
 * Builds YUV to [T|A][BGR|RGB]15 colour conversion tables and selects the most appropriate
 * drawer for the current display options.
 *
 * @param  screen    Output screen parameters.
 * @param  options   Redraw options.
 * @param  f         Preset drawer function.
 *
 * @returns Drawer function to use for plotting output.
 */
static ka_drawer_f ColourConverter_setupYUVto15bpp(ka_screen_t* screen, ka_colourspace_t* options, ka_drawer_f f)
{
  screen = screen; // Suppress warning
  build_uv_table(options->colour);
  build_y15_table(options->brightness, options->contrast
                , (options->config & cfg_video_dither) ? 1 : 0);

  return f;
}

/**
 * Builds YUV to [T|A][BGR|RGB]16 colour conversion tables and selects the most appropriate
 * drawer for the current display options.
 *
 * @param  screen    Output screen parameters.
 * @param  options   Redraw options.
 * @param  f         Preset drawer function.
 *
 * @returns Drawer function to use for plotting output.
 */
static ka_drawer_f ColourConverter_setupYUVto16bpp(ka_screen_t* screen, ka_colourspace_t* options, ka_drawer_f f)
{
  screen = screen; // Suppress warning
  build_uv_table(options->colour);
  build_y15_table(options->brightness, options->contrast
                , (options->config & cfg_video_dither) ? 1 : 0);
  build_y16_table(options->brightness, options->contrast
                , (options->config & cfg_video_dither) ? 1 : 0);

  return f;
}

/**
 * Builds YUV to [T|A][BGR|RGB]32 colour conversion tables and selects the most appropriate
 * drawer for the current display options.
 *
 * @param  screen    Output screen parameters.
 * @param  options   Redraw options.
 * @param  f         Preset drawer function.
 *
 * @returns Drawer function to use for plotting output.
 */
static ka_drawer_f ColourConverter_setupYUVto32bpp(ka_screen_t* screen, ka_colourspace_t* options, ka_drawer_f f)
{
  screen = screen; // Suppress warning
  build_uv_table(options->colour);
  build_y32_table(options->brightness, options->contrast);

  return f;
}

/*
 * Colour conversion for 8 bit colour modes
 * -----------------------------------------------------------------------------
 */

uint8_t* ka_rgb_colour_table = NULL;
uint8_t* ka_yuv_colour_table = NULL;
uint8_t* ka_grey_table = NULL;

#ifdef YUV_LOOKUP
static int32_t iY[256];
static int32_t iU[256];
static int32_t iV[256];

/**
 * Finds best matching 8bpp palette colour for an YUV colour.
 *
 * @param  y
 * @param  u
 * @param  v
 *
 * @returns best matching colour number.
 */
static uint8_t best_fity(int y, int u, int v)
{
  int min = 1 << 24, cmin = 0, c, cy, cu, cv;
  for (c = 0; c < 256; c++)
  {
    cy = iY[c] - y;
    cy *= cy;
    cu = iU[c] - u;
    cu *= cu;
    cv = iV[c] - v;
    cv *= cv;
    cy += cu + cv;
    if (cy < min)
    {
      min = cy;
      cmin = c;
    }
  }

  return cmin;
}

/**
 * Builds an YUV to colour number lookup table giving the best matches
 * for a given 8bpp palette. Also builds an YUV representation of 8bpp palette.
 *
 * @param  palette  Reference 256-colours palette.
 */
static void yuv_8bpp_tables(uint32_t* palette)
{
  // Build YUV for palette
  {
    int c, cr, cg, cb;

    int _Yr = (int)(float)(  0.257*4096);
    int _Yg = (int)(float)(  0.504*4096);
    int _Yb = (int)(float)(  0.098*4096);
    int _Ur = (int)(float)(  0.439*4096);
    int _Ug = (int)(float)( -0.368*4096);
    int _Ub = (int)(float)( -0.071*4096);
    int _Vr = (int)(float)( -0.148*4096);
    int _Vg = (int)(float)( -0.291*4096);
    int _Vb = (int)(float)(  0.439*4096);

    for (c = 0; c < 256; c++)
    {
      cr = palette[c];
      cb = (cr>>24) & 0xff;
      cg = (cr>>16) & 0xff;
      cr = (cr>>8) & 0xff;

      iY[c] = ((_Yr * cr + _Yg * cg + _Yb * cb + (1 << 11) - 1) >> 12) + 28;// + 16;
      iU[c] = ((_Ur * cr + _Ug * cg + _Ub * cb + (1 << 11) - 1) >> 12) + 128;
      iV[c] = ((_Vr * cr + _Vg * cg + _Vb * cb + (1 << 11) - 1) >> 12) + 128;
    }
  }

  // Build YUV -> color table
  {
    int y0, u0, v0, y, u, v;
    uint8_t* out = ka_rgb_colour_table;

    for (u0 = 0; u0 < 16; u0++)
    {
      u = u0*17;

      for (v0 = 0; v0 < 16; v0++)
      {
        v = v0*17;

        for (y0 = 0; y0 < 32; y0++)
        {
          y = (y0<<3) + (y0>>2);

          *out++ = best_fity(y, u, v);
        }
      }
    }
  }

  // Build Y -> grey table
  {
    uint8_t* out = ka_grey_table;
    int y;

    for (y = 0; y < 256; y++)
      *out++ = best_fity(y, 128, 128);
  }
}
#else
static uint32_t Vg8[16];
#endif

static uint32_t Vb8[16];
static uint32_t Yc8[32];

static const ka_drawer_f drawers_grey[7] =
{ ka_drawy_z05_grey
, ka_drawy_z1_grey
, ka_drawy_z2_grey
, ka_drawy_z3_grey
, ka_drawy_z4_grey
, ka_drawy_z2_grey
, ka_drawy_z1_grey
};

static const ka_drawer_list drawers_8bit =
{ // YUV 400 = mono
  { ka_drawy_z05_8bpp
  , ka_drawy_z1_8bpp
  , ka_drawy_z2_8bpp
  , ka_drawy_z3_8bpp
  , ka_drawy_z4_8bpp
  , ka_drawy_z2_8bpp
  , ka_drawy_z1_8bpp
  }
  // YUV420
, { ka_drawyuv420_z05_8bpp
  , ka_drawyuv420_z1_8bpp
  , ka_drawyuv420_z2_8bpp
  , ka_drawyuv420_z3_8bpp
  , ka_drawyuv420_z4_8bpp
  , ka_drawyuv420_z2_8bpp
  , ka_drawyuv420_z1_8bpp
  }
  // YUV422
, { ka_drawyuv422_z05_8bpp
  , ka_drawyuv422_z1_8bpp
  , ka_drawyuv422_z2_8bpp
  , ka_drawyuv422_z3_8bpp
  , ka_drawyuv422_z4_8bpp
  , ka_drawyuv422_z2_8bpp
  , ka_drawyuv422_z1_8bpp
  }
  // YUV 444
, { ka_drawyuv444_z05_8bpp
  , ka_drawyuv444_z1_8bpp
  , ka_drawyuv444_z2_8bpp
  , ka_drawyuv444_z3_8bpp
  , ka_drawyuv444_z4_8bpp
  , ka_drawyuv444_z2_8bpp
  , ka_drawyuv444_z1_8bpp
  }
  // BGR
, { ka_drawbgr_z05_8bpp
  , ka_drawbgr_z1_8bpp
  , ka_drawbgr_z2_8bpp
  , ka_drawbgr_z3_8bpp
  , ka_drawbgr_z4_8bpp
  , ka_drawbgr_z2_8bpp
  , ka_drawbgr_z1_8bpp
  }
};

/**
 * Builds YUV to 8bit colour conversion tables and selects the most appropriate
 * drawer for the current display options.
 *
 * @param  screen    Output screen parameters.
 * @param  options   Redraw options.
 * @param  f         Preset drawer function.
 *
 * @returns Drawer function to use for plotting output.
 */
static ka_drawer_f ColourConverter_setupYUVto8bit(ka_screen_t* screen, ka_colourspace_t* options, ka_drawer_f f)
{
  uint8_t *out;
  int y0, u0, v0;

  f = f; // Suppress warning
  build_y32_table(options->brightness, options->contrast);

  out = ka_yuv_colour_table;

  // We can simply copy Y to the screen if we're in a greyscale mode
  // not using zoom or contrast/brightness adjustments.
  if ((screen->greyscale)
  &&  (  (options->colour < 8)
      || (options->config & cfg_video_mono)))
    // use monodrawer
    return drawers_grey[options->zoom];

#ifdef YUV_LOOKUP

  // If colour has been turned (almost) all the way down,
  // ignore UV when displaying
  if ((options->colour < 8)
  ||  (options->config & cfg_video_mono))
  {
    int y;
    uint8_t c;

    for (y = 0; y < 256; y++)
    {
      int dsty, cury, y4;

      dsty = APPLY_BRIGHTNESS(y, options->brightness);
      dsty = APPLY_CONTRAST(dsty, options->contrast);

      if (dsty < 0) dsty = 0;
      if (dsty > 255) dsty = 255;
      *out++ = c = ka_grey_table[dsty];
      if (options->config & cfg_video_dither)
      {
        cury = iY[c];
        y4 = 2*dsty - cury;
        if (y4 < 0) y4 = 0;
        if (y4 > 255) y4 = 255;
        *out++ = c = ka_grey_table[y4];

        cury += iY[c];
        y4 = 2*dsty - cury/2;
        if (y4 < 0) y4 = 0;
        if (y4 > 255) y4 = 255;
        *out++ = c = ka_grey_table[y4];

        cury += iY[c];
        y4 = 2*dsty - cury/3;
        if (y4 < 0) y4 = 0;
        if (y4 > 255) y4 = 255;
        *out++ = ka_grey_table[y4];
      }
      else
      {
        *out++ = c;
        *out++ = c;
        *out++ = c;
      }
    }
    return drawers_8bit[0][options->zoom];
  }

  for (v0 = 0; v0 < 16; v0++)
  {
    int v;
    v = v0*17 - 128;    // scale to -128..127
    Vb8[v0] = 128 + APPLY_COLOUR(v, options->colour);
  }

  for (y0 = 0; y0 < 32; y0++)
  {
    int y;
    y = (y0<<3) + (y0>>2);      // scale to 0..255
    y = APPLY_BRIGHTNESS(y, options->brightness);
    Yc8[y0] = APPLY_CONTRAST(y, options->contrast);
  }

  for (u0 = 0; u0 < 16; u0++)
  {
    int dsty, dstu, dstv, c, y, u, v;

    dstu = u0*17 - 128;
    dstu = 128 + APPLY_COLOUR(dstu, options->colour);

    for (v0 = 0; v0 < 16; v0++)
    {
      dstv = Vb8[v0];

      for (y0 = 0; y0 < 32; y0++)
      {
         dsty = Yc8[y0];

         _8TO5(dsty,y);
         _8TO4(dstu,u);
         _8TO4(dstv,v);
         *out++ = c = ka_rgb_colour_table[y | (v<<5) | (u<<9)];
         if (options->config & cfg_video_dither)
         {
           int cury, curu, curv;

           cury = iY[c];
           curu = iU[c];
           curv = iV[c];
           y = 2*dsty - cury; // what_we_want - what_we've_got_so_far
           u = 2*dstu - curu;
           v = 2*dstv - curv;
           _8TO5(y,y);
           _8TO4(u,u);
           _8TO4(v,v);
           *out++ = c = ka_rgb_colour_table[y | (v<<5) | (u<<9)];

           cury += iY[c];
           curu += iU[c];
           curv += iV[c];
           y = 2*dsty - cury/2; // what_we_want - what_we've_got_so_far
           u = 2*dstu - curu/2;
           v = 2*dstv - curv/2;
           _8TO5(y,y);
           _8TO4(u,u);
           _8TO4(v,v);
           *out++ = c = ka_rgb_colour_table[y | (v<<5) | (u<<9)];

           cury += iY[c];
           curu += iU[c];
           curv += iV[c];
           y = 2*dsty - cury/3; // what_we_want - what_we've_got_so_far
           u = 2*dstu - curu/3;
           v = 2*dstv - curv/3;
           _8TO5(y,y);
           _8TO4(u,u);
           _8TO4(v,v);
           *out++ = ka_rgb_colour_table[y | (v<<5) | (u<<9)];
         }
         else
         {
           *out++ = c;
           *out++ = c;
           *out++ = c;
         }
       }
    }
  }

#else // YUV_LOOKUP is not defined

  // If colour has been turned (almost) all the way down,
  // ignore UV when displaying
  if ((options->colour < 8)
  ||  (options->config & cfg_video_mono))
  {
    uint32_t* palette = screen->palette;
    int y;
    uint8_t pix;

    for (y = 0; y < 256; y++)
    {
      int dsty, cury, y4;

      dsty = APPLY_BRIGHTNESS(y, options->brightness);
      dsty = APPLY_CONTRAST(dsty, options->contrast);

      if (dsty < 0) dsty = 0;
      if (dsty > 255) dsty = 255;
      pix = ka_grey_table[dsty];
      *out++ = pix;
      if (options->config & cfg_video_dither)
      {
        uint32_t rgb;

        rgb = palette[pix];
        cury = (5*((rgb>>8) & 255) + 8*((rgb>>16) & 255) + 3*((rgb>>24) & 255))>>4;
        y4 = 2*dsty - cury;
        if (y4 < 0) y4 = 0;
        if (y4 > 255) y4 = 255;
        *out++ = pix = ka_grey_table[y4];

        rgb = palette[pix];
        cury += (5*((rgb>>8) & 255) + 8*((rgb>>16) & 255) + 3*((rgb>>24) & 255))>>4;
        y4 = 2*dsty - cury/2;
        if (y4 < 0) y4 = 0;
        if (y4 > 255) y4 = 255;
        *out++ = pix = ka_grey_table[y4];

        rgb = palette[pix];
        cury += (5*((rgb>>8) & 255) + 8*((rgb>>16) & 255) + 3*((rgb>>24) & 255))>>4;
        y4 = 2*dsty - cury/3;
        if (y4 < 0) y4 = 0;
        if (y4 > 255) y4 = 255;
        *out++ = ka_grey_table[y4];
      }
      else
      {
        *out++ = pix;
        *out++ = pix;
        *out++ = pix;
      }
    }
    return drawers_8bit[0][options->zoom];
  }

#define KYc 1192  // = 1.164 * 1024
#define KUr 1634  // = 1.596 * 1024
#define KUg 833   // = 0.813 * 1024
#define KVb 2066  // = 2.018 * 1024
#define KVg 400   // = 0.391 * 1024

  for (v0 = 0; v0 < 16; v0++)
  {
    int v;
    v = v0*17 - 128;    // scale to -128..127
    v = APPLY_COLOUR(v, options->colour);
    Vb8[v0] = v * KVb;
    Vg8[v0] = v * KVg;
  }

  for (y0 = 0; y0 < 32; y0++)
  {
    int y;
    y = (y0<<3) + (y0>>2);      // scale to 0..255
    y = APPLY_BRIGHTNESS(y, options->brightness);
    y = APPLY_CONTRAST(y, options->contrast);

    Yc8[y0] = KYc * (y - 16) + 511;
  }

  for (u0 = 0; u0 < 16; u0++)
  {
    int Yc, Ur, Ug, UVg, Vb;
    uint32_t* palette = screen->palette;

    Ug = u0*17 - 128;    // scale to -128..127
    Ug = APPLY_COLOUR(Ug, options->colour);
    Ur = Ug * KUr;
    Ug = Ug * KUg;

    for (v0 = 0; v0 < 16; v0++)
    {
      Vb = Vb8[v0];
      UVg = Ug + Vg8[v0];

      for (y0 = 0; y0 < 32; y0++)
      {
         int r, g, b, dstr, dstg, dstb;
         uint32_t pix;

         Yc = Yc8[y0];

         // red
         dstr = (Yc + Ur) >> 10;
         if (dstr > 255)  dstr = 255;
         if (dstr < 0)    dstr = 0;
         // green
         dstg = (Yc - UVg) >> 10;
         if (dstg > 255)  dstg = 255;
         if (dstg < 0)    dstg = 0;
         // blue
         dstb = (Yc + Vb) >> 10;
         if (dstb > 255)  dstb = 255;
         if (dstb < 0)    dstb = 0;

         _8TO4(dstr,r);
         _8TO4(dstg,g);
         _8TO4(dstb,b);

         pix = ka_rgb_colour_table[r | (g<<4) | (b<<8)];
         *out++ = pix;
         if (options->config & cfg_video_dither)
         {
           uint32_t rgb;
           int curr, curg, curb;

           rgb = palette[pix];
           curr = (rgb>>8 ) &255;
           curg = (rgb>>16) &255;
           curb = (rgb>>24) &255;
           r = 2*dstr - curr; // what_we_want - what_we've_got_so_far
           g = 2*dstg - curg;
           b = 2*dstb - curb;
           _8TO4(r,r);
           _8TO4(g,g);
           _8TO4(b,b);
           pix = ka_rgb_colour_table[r | (g<<4) | (b<<8)];
           *out++ = pix;

           rgb = palette[pix];
           curr += (rgb>>8 ) &255;
           curg += (rgb>>16) &255;
           curb += (rgb>>24) &255;
           r = 2*dstr - curr/2;
           g = 2*dstg - curg/2;
           b = 2*dstb - curb/2;
           _8TO4(r,r);
           _8TO4(g,g);
           _8TO4(b,b);
           pix = ka_rgb_colour_table[r | (g<<4) | (b<<8)];
           *out++ = pix;

           rgb = palette[pix];
           curr += (rgb>>8 ) &255;
           curg += (rgb>>16) &255;
           curb += (rgb>>24) &255;
           r = 2*dstr - curr/3;
           g = 2*dstg - curg/3;
           b = 2*dstb - curb/3;
           _8TO4(r,r);
           _8TO4(g,g);
           _8TO4(b,b);
           *out++ = ka_rgb_colour_table[r | (g<<4) | (b<<8)];
         }
         else
         {
           *out++ = pix;
           *out++ = pix;
           *out++ = pix;
         }
       }
    }
  }
#endif

  return drawers_8bit[options->chroma_type][options->zoom];
}

/**
 * Builds Y and U/V maps for YUV color correction.
 *
 * @param  colour      Colour saturation level to apply.
 *                     Normal level is 100%, no colour is 0%.
 * @param  brightness  Brightness correction to apply.
 * @param  contrast    Contrast correction to apply.
 */
static void build_yuv_map(int brightness, int contrast, int colour)
{
  for (int i=0; i<256; i++)
  {
    int uv = i-128;
    int y = i;

    y = APPLY_BRIGHTNESS(y, brightness);
    y = APPLY_CONTRAST(y, contrast);
    if (y < 0) y = 0;
    else if (y > 255) y = 255;
    ka_Y_Map[i] = y;

    uv = APPLY_COLOUR(uv, colour);
    uv += 128;
    if (uv < 0) uv = 0;
    else if (uv > 255) uv = 255;
    ka_UV_Map[i] = uv;
  }
}

static const ka_drawer_z1list drawers_nv12 =
{ ka_drawy_z1_NV12
, ka_drawyuv420_z1_NV12
, ka_drawyuv422_z1_NV12
, ka_drawyuv444_z1_NV12
, NULL
, ka_drawy_z1_NV12
, ka_drawyuv420_z1_NV12_neon
, ka_drawyuv422_z1_NV12_neon
, ka_drawyuv444_z1_NV12_neon
, NULL
};

static const ka_drawer_z1list drawers_nv21 =
{ ka_drawy_z1_NV12 // = NV21
, ka_drawyuv420_z1_NV21
, ka_drawyuv422_z1_NV21
, ka_drawyuv444_z1_NV21
, NULL
, ka_drawy_z1_NV12 // = NV21
, ka_drawyuv420_z1_NV21_neon
, ka_drawyuv422_z1_NV21_neon
, ka_drawyuv444_z1_NV21_neon
, NULL
};

static const ka_drawer_z1list drawers_uyvy =
{ ka_drawy_z1_UYVY
, ka_drawyuv420_z1_UYVY
, ka_drawyuv422_z1_UYVY
, ka_drawyuv444_z1_UYVY
, NULL
, ka_drawy_z1_UYVY
, ka_drawyuv420_z1_UYVY
, ka_drawyuv422_z1_UYVY
, ka_drawyuv444_z1_UYVY
, NULL
};

static const ka_drawer_z1list drawers_yuy2 =
{ ka_drawy_z1_YUY2
, ka_drawyuv420_z1_YUY2
, ka_drawyuv422_z1_YUY2
, ka_drawyuv444_z1_YUY2
, NULL
, ka_drawy_z1_YUY2
, ka_drawyuv420_z1_YUY2
, ka_drawyuv422_z1_YUY2
, ka_drawyuv444_z1_YUY2
, NULL
};

static const ka_drawer_z1list drawers_yv12 =
{ ka_drawy_z1_YV12
, ka_drawyuv420_z1_YV12
, ka_drawyuv422_z1_YV12
, ka_drawyuv444_z1_YV12
, NULL
, ka_drawy_z1_YV12
, ka_drawyuv420_z1_YV12_neon
, ka_drawyuv422_z1_YV12_neon
, ka_drawyuv444_z1_YV12_neon
, NULL
};

static const ka_drawer_z1list drawers_yv16 =
{ ka_drawy_z1_YV16
, ka_drawyuv420_z1_YV16
, ka_drawyuv422_z1_YV16
, ka_drawyuv444_z1_YV16
, NULL
, ka_drawy_z1_YV16
, ka_drawyuv420_z1_YV16_neon
, ka_drawyuv422_z1_YV16_neon
, ka_drawyuv444_z1_YV16_neon
, NULL
};

/**
 * Builds YUV to YUV colour conversion tables and selects the most appropriate
 * drawer for the current display options.
 *
 * @param  screen    Output screen parameters.
 * @param  options   Redraw options.
 * @param  f         Preset drawer function.
 *
 * @returns Drawer function to use for plotting output.
 */
static ka_drawer_f ColourConverter_setupYUVtoYUV(ka_screen_t* screen, ka_colourspace_t* options, ka_drawer_f f)
{
  screen = screen; // Suppress warning
  build_yuv_map(options->brightness, options->contrast, options->colour);

  return f;
}

static const char * col_tab[ka_col_depth_count] =
{ "256 colors"
, "NV12"
, "NV21"
, "UYVY"
, "YUY2"
, "YV12"
, "YV16"
, "4k (BGR)"
, "32k (BGR)"
, "64k (BGR)"
, "16m (BGR)"
, "4k (RGB)"
, "32k (RGB)"
, "64k (RGB)"
, "16m (RGB)"
, "4k (ABGR)"
, "32k (ABGR)"
, "64k (ABGR)"
, "16m (ABGR)"
, "4k (ARGB)"
, "32k (ARGB)"
, "64k (ARGB)"
, "16m (ARGB)"
};

static const char * chroma_tab[ka_chroma_count] =
{ "Y"
, "YUV420"
, "YUV422"
, "YUV444"
, "RGB"
};

const char* ColourConverter_ColourDepthName(ka_col_depth depth)
{
  return col_tab[depth];
}

const char* ColourConverter_ChromaTypeName(ka_chroma_type type)
{
  return chroma_tab[type];
}

typedef struct
{
  ColourConverter_fsetup setup;
  const ka_drawer_list* drawers;
  const ka_drawer_z1list* z1drawers;
} ConvertConfig;

static const ConvertConfig map[2 /* BGR/YUV */][ka_col_depth_count] =
{ { {NULL, &drawers_8bit, NULL}
  , {NULL, NULL, NULL}
  , {NULL, NULL, NULL}
  , {NULL, NULL, NULL}
  , {NULL, NULL, NULL}
  , {NULL, NULL, NULL}
  , {NULL, NULL, NULL}
  , {NULL, &drawers_tbgr12}
  , {NULL, &drawers_tbgr15}
  , {NULL, &drawers_bgr16}
  , {NULL, &drawers_tbgr32}
  , {NULL, &drawers_trgb12}
  , {NULL, &drawers_trgb15}
  , {NULL, &drawers_rgb16}
  , {NULL, &drawers_trgb32}
  , {NULL, &drawers_abgr12}
  , {NULL, &drawers_abgr15}
  , {NULL, &drawers_bgr16}
  , {NULL, &drawers_abgr32}
  , {NULL, &drawers_argb12}
  , {NULL, &drawers_argb15}
  , {NULL, &drawers_rgb16}
  , {NULL, &drawers_argb32}
  }
, { {ColourConverter_setupYUVto8bit, &drawers_8bit}
  , {ColourConverter_setupYUVtoYUV, NULL, &drawers_nv12}
  , {ColourConverter_setupYUVtoYUV, NULL, &drawers_nv21}
  , {ColourConverter_setupYUVtoYUV, NULL, &drawers_uyvy}
  , {ColourConverter_setupYUVtoYUV, NULL, &drawers_yuy2}
  , {ColourConverter_setupYUVtoYUV, NULL, &drawers_yv12}
  , {ColourConverter_setupYUVtoYUV, NULL, &drawers_yv16}
  , {ColourConverter_setupYUVto12bpp, &drawers_tbgr12}
  , {ColourConverter_setupYUVto15bpp, &drawers_tbgr15}
  , {ColourConverter_setupYUVto16bpp, &drawers_bgr16}
  , {ColourConverter_setupYUVto32bpp, &drawers_tbgr32}
  , {ColourConverter_setupYUVto12bpp, &drawers_trgb12}
  , {ColourConverter_setupYUVto15bpp, &drawers_trgb15}
  , {ColourConverter_setupYUVto16bpp, &drawers_rgb16}
  , {ColourConverter_setupYUVto32bpp, &drawers_trgb32}
  , {ColourConverter_setupYUVto12bpp, &drawers_abgr12}
  , {ColourConverter_setupYUVto15bpp, &drawers_abgr15}
  , {ColourConverter_setupYUVto16bpp, &drawers_bgr16}
  , {ColourConverter_setupYUVto32bpp, &drawers_abgr32}
  , {ColourConverter_setupYUVto12bpp, &drawers_argb12}
  , {ColourConverter_setupYUVto15bpp, &drawers_argb15}
  , {ColourConverter_setupYUVto16bpp, &drawers_rgb16}
  , {ColourConverter_setupYUVto32bpp, &drawers_argb32}
  }
};

/**
 * Builds colour conversion tables and selects the most appropriate drawer
 * for the current display options.
 *
 * This is a fast version of ColourConverter_setup which reuses some tables
 * from the last call to ColourConverter_setup and must not be used when
 * they become invalid, i.e. after screen mode change or a color space change.
 *
 * @param  screen   Output screen parameters.
 * @param  options  Redraw options.
 *
 * @returns Drawer function to use for plotting output.
 */
ka_drawer_f ColourConverter_fastsetup(ka_error_t* pErrorBlock, ka_screen_t* screen, ka_colourspace_t* options)
{
  int chroma = options->chroma_type;
  int zoom = options->zoom;

  if ((options->colour < 8)
  ||  (options->config & cfg_video_mono))
  {
    if (chroma != ka_chroma_BGR)
      chroma = ka_chroma_luminance;
  }

  if ((options->config & cfg_video_dither)
  &&  (zoom == ka_zoom_2))
    zoom = 5;

  if ((options->hardware & ka_hardware_neon)
  &&  (zoom == ka_zoom_1))
    zoom = 6;

  options->params.cur = (int) ((UR * (float)(int) (options->colour << 10)) / 100.);
  options->params.cvb = (int) ((VB * (float)(int) (options->colour << 10)) / 100.);
  options->params.cug = (int) ((UG * (float)(int) (options->colour << 10)) / 100.);
  options->params.cvg = (int) ((VG * (float)(int) (options->colour << 10)) / 100.);
  options->params.cy0 = (128 << 10) + (int) ((((float) options->contrast)
                                      * (YC*-16*1024. + (float)(int) ((options->brightness - 100 - 128) << 10))
                                      ) / 100.);
  options->params.cyc = (int) ((YC * (float)(int) (options->contrast << 10)) / 100.);

  options->params.yfac   = (64 * options->contrast) / 100;
  options->params.yadd   = options->params.yfac * (options->brightness - 100 - 128) + 128*64;
  options->params.colfac = (64 * options->colour) / 100;
  options->params.coladd = 128 * (64 - options->params.colfac);

  ConvertConfig config = map[options->chroma_type != ka_chroma_BGR][screen->col_depth];
  ka_drawer_f drawer = config.drawers
                     ? (*config.drawers)[chroma][zoom]
                     : (config.z1drawers && (options->zoom == ka_zoom_1))
                       ? (*config.z1drawers)[chroma + ((options->hardware & ka_hardware_neon) ? ka_chroma_count : 0)]
                       : NULL;
  if (drawer)
  {
    if (config.setup)
      return config.setup(screen, options, drawer);
    else
      return drawer;
  }

  ka_error_fill( pErrorBlock
               , "Chroma type %s cannot be converted to %s"
               , ColourConverter_ChromaTypeName(options->chroma_type)
               , ColourConverter_ColourDepthName(screen->col_depth)
               );

  return NULL;
}

/**
 * Builds colour conversion tables and selects the most appropriate drawer
 * for the current display options.
 *
 * @param  screen   Output screen parameters.
 * @param  options  Redraw options.
 *
 * @returns Drawer function to use for plotting output or NULL and error block filled.
 */
ka_drawer_f ColourConverter_setup(ka_error_t* pErrorBlock, ka_screen_t* screen, ka_colourspace_t* options)
{
  if (screen->col_depth == ka_col_depth_256)
  {
    // create colour translation table for the current mode & palette
#ifdef YUV_LOOKUP
    // use rgb table as y5u4v4 temporary table
    if (!ka_rgb_colour_table)  ka_rgb_colour_table = ka_mem_alloc(32*16*16);
#else
    if (!ka_rgb_colour_table)  ka_rgb_colour_table = ka_mem_alloc(16*16*16);
#endif
    if (!ka_grey_table)  ka_grey_table = ka_mem_alloc(256);
    if (!ka_rgb_colour_table || !ka_grey_table)
    {
      ka_error_fill(pErrorBlock, ka_error_nomem);
      return NULL;
    }

#ifdef YUV_LOOKUP
    if (options->chroma_type != ka_chroma_BGR)
      yuv_8bpp_tables(screen->palette);
    else
#endif
    {
      // make RGB444 to 8bpp colour table
      _kernel_swi_regs regs;
      int r, g, b;
      uint8_t* out = ka_rgb_colour_table;

      for (b = 0; b < 16; b++)
        for (g = 0; g < 16; g++)
          for (r = 0; r < 16; r++)
          {
            regs.r[0] = ((b*17)<<24) | ((g*17)<<16) | ((r*17)<<8);
            _kernel_swi(ColourTrans_ReturnColourNumber, &regs, &regs);
            *out++ = regs.r[0];
          }
      out = ka_grey_table;
      for (g = 0; g < 256; g++)
      {
        regs.r[0] = (g<<24) | (g<<16) | (g<<8);
        _kernel_swi(ColourTrans_ReturnColourNumber, &regs, &regs);
        *out++ = regs.r[0];
      }
    }

    // make yuv to 8bpp colour table
    if (options->chroma_type != ka_chroma_BGR)
    {
      if (!ka_yuv_colour_table)  ka_yuv_colour_table = ka_mem_alloc(4*32*16*16);
      if (!ka_yuv_colour_table)
      {
        ka_error_fill(pErrorBlock, ka_error_nomem);
        return NULL;
      }
    }
  }

  return ColourConverter_fastsetup(pErrorBlock, screen, options);
}
