Files
libretro-atari800/atari800/src/colours_pal.c
T
2015-12-14 14:00:35 +01:00

507 lines
19 KiB
C

/*
* colours_pal.c - Atari PAL colour palette generation and adjustment
*
* Copyright (C) 2009-2014 Atari800 development team (see DOC/CREDITS)
*
* This file is part of the Atari800 emulator project which emulates
* the Atari 400, 800, 800XL, 130XE, and 5200 8-bit computers.
*
* Atari800 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.
*
* Atari800 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 Atari800; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include <math.h>
#include <stdlib.h>
#include <string.h>
#include "colours_pal.h"
#include "atari.h" /* for TRUE/FALSE */
#include "colours.h"
#include "log.h"
#include "util.h"
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
Colours_setup_t COLOURS_PAL_setup;
/* PAL-specific default setup. */
static struct {
double color_delay;
} const default_setup = {
23.2, /* chosen by eye to give a smooth rainbow */
};
COLOURS_EXTERNAL_t COLOURS_PAL_external = { "", FALSE, FALSE };
/* Fills YUV_TABLE from external palette. External palette is not adjusted if
COLOURS_PAL_external.adjust is false. */
static void GetYUVFromExternal(double yuv_table[256*5])
{
unsigned char *ext_ptr = COLOURS_PAL_external.palette;
int n;
double const hue = COLOURS_PAL_setup.hue * M_PI;
double const s = sin(hue);
double const c = cos(hue);
for (n = 0; n < 256; n ++) {
/* Convert RGB values from external palette to YUV. */
double r = (double)*ext_ptr++ / 255.0;
double g = (double)*ext_ptr++ / 255.0;
double b = (double)*ext_ptr++ / 255.0;
double y, u, v, tmp_u;
Colours_RGB2YUV(r, g, b, &y, &u, &v);
tmp_u = u;
u = tmp_u * c - v * s;
v = tmp_u * s + v * c;
/* Optionally adjust external palette. */
if (COLOURS_PAL_external.adjust) {
y *= COLOURS_PAL_setup.contrast * 0.5 + 1;
y += COLOURS_PAL_setup.brightness * 0.5;
if (y > 1.0)
y = 1.0;
else if (y < 0.0)
y = 0.0;
u *= COLOURS_PAL_setup.saturation + 1.0;
v *= COLOURS_PAL_setup.saturation + 1.0;
}
*yuv_table++ = y;
/* Cannot retrieve different U/V values for even and odd lines from an
external palette - instead write each value twice. */
*yuv_table++ = u;
*yuv_table++ = u;
*yuv_table++ = v;
*yuv_table++ = v;
}
}
/* Below are conclusions from analysis of oscillograms of PAL GTIA output.
The oscillograms are available at:
http://www.atari.org.pl/forum/viewtopic.php?pid=164037#p164037
Like the NTSC GTIA, the PAL GTIA produces color signal by delaying
(phase-shifting) a base chrominance subcarrier by a specified time -
different for each displayed hue. The subcarrier's frequency is
4433618.75 Hz (4.43 MHz in short). The subcarrier is provided at the chip's
PAL pin; output signal is produced at the COL pin.
As in NTSC, the delay introduced by the PAL GTIA can be adjusted to a
certain extent by changing voltage applied at the DEL pin.
This is where the similarities between the PAL GTIA and its NTSC
counterpart end.
In NTSC, each screen line contains, in its back porch, a burst of about 10
cycles of color subcarrier (known as the coloburst). The colorburst is used
by the TV as a reference for decoding colors in each line. Signal with
phase shift equal to the colorburst's (ie. equal to the subcarrier's) is
decoded as 180 deg angle in the YUV colorspace (-U). Delaying the signal is
interpreted as rotating the angle clockwise in YUV.
In PAL, there's also colorburst, but it is not equal to the color
subcarrier. In even lines it lags the subcarrier by 45 deg, and in odd
lines it leads it subcarrier by 45 deg. The resulting 90 deg difference is
used by the TV to distinguish even and odd lines. The original subcarrier
is recreated by the receiver by summing colorbursts from two consecutive
lines (and adjusting its amplitude, since sum of two sine waves shifted by
90 deg has higher amplitude than the original waves).
Then, as in NTSC, signals equal in phase to the (recreated) subcarrier are
decoded as 180 deg angle in YUV. But other signals are decoded differently
in even and in odd lines. In even lines (or "NTSC lines"), delaying the
signal is interpreted as rotating the angle clockwise in YUV, just like in
NTSC. But in odd lines (or "PAL lines"), delaying the signal is akin to
rotating the angle counter-clockwise. So, to achieve the same color in even
and odd lines, the transmitter must send signal delayed wrt. the subcarrier
in even lines but "rushed" (anti-delayed?) in odd lines.
PAL GTIA can only delay input signal; it can't "rush" it, it would involve
creating output before input has arrived. So the requirements of the PAL
system are achieved in the chip by delaying the signal differently in even
and odd lines.
Measurements indicate that the PAL GTIA delays the input subcarrier by
95.2ns; then it might delay it by further 100.7ns if needed; and finally
can delay it further by X, 2*X, 3*X, ..., 7*X, where X is a delay adjusted
by voltage at the DEL pin. The general formula is:
DELAY(hue) = 95.2ns + ADD(hue)*100.7ns + MULT(hue)*X
where
ADD(hue) depends on a given hue and can be 0 or 1,
MULT(hue) depends on a given hue and can be 0, 1, 2, ..., 7,
X depends on voltage at the DEL pin.
So, the chip can shift the input signal by 8*2=16 different delays.
Mapping of ADD and MULT values to specific hues is random - it is
implemented in the source code as the del_coeffs structure.
The relation between DEL pin voltage and the resulting delay is not linear
- it was measured to be:
17.37ns at 5V,
10.60ns at 7V,
7.44ns at 9V.
The PAL GTIA apparently contains a series of 7 delay elements similarly to
the NTSC version (which contains 15 of them), and an additional 100.7ns
delay element. (When looking at the oscillograms, for each hue that passes
through this 100.7ns delay, the resulting waveform is different than for
hues that don't involve the delay - it looks like it was inverted
vertically. This gives a hint about how the delay is implemented
internally.)
The end result is, PAL GTIA actually produces more than 256 colors. It
produces different hues in odd and even lines, but it is not normally
visible since PAL TVs average hues on each two consecutive lines - but
it can be made visible with specially-crafted screens. Currently this
feature of the chip is not implemented - only 256 colors are produced,
each one being an average of "even" and "odd" hues.
In PAL GTIA, just as in NTSC, the colorburst signals have the same phase as
Hue 1. But by changing voltage at the DEL pin, we also change the relative
phase shift of the colorburst between even and odd lines - it is not always
90 degrees. It has the following consequences - all of them observed on a
real machine, and all currently emulated:
1. Changing phase shift between odd and even colorbursts also changes
amplitude of the resulting subcarrier, which is recreated by summing the
two colorbursts. Since amplitude of the color subcarrier is used as a
reference for decoding color saturation, changing voltage at DEL not only
changes hues of displayed colors; it also changes their saturation.
2. With appropriately chosen DEL voltage, phase shift between even and odd
colorbursts can reach 180 deg. At this point the PAL TV can no longer
distinguish even and odd lines, and stops interpreting color signal,
displaying screen as black-and-white with visible chroma dots.
3. Changing DEL voltage further causes phase shift between even and odd
colorbursts to to be larger than 180 deg, at which point the TV starts
interpreting color signal "backwards", as if the color subcarrier suddenly
shifted by 180 deg.
*/
/* Generates PAL palette into YUV_TABLE. */
static void GetYUVFromGenerated(double yuv_table[256*5])
{
struct del_coeff {
int add;
int mult;
};
/* Delay coefficients for each hue. */
static struct {
struct del_coeff even[15];
struct del_coeff odd[15];
} const del_coeffs = {
{ { 1, 5 }, /* Hue $1 in even lines */
{ 1, 6 }, /* Hue $2 in even lines */
{ 1, 7 },
{ 0, 0 },
{ 0, 1 },
{ 0, 2 },
{ 0, 4 },
{ 0, 5 },
{ 0, 6 },
{ 0, 7 },
{ 1, 1 },
{ 1, 2 },
{ 1, 3 },
{ 1, 4 },
{ 1, 5 } /* Hue $F in even lines */
},
{ { 1, 1 }, /* Hue $1 in odd lines */
{ 1, 0 }, /* Hue $2 in odd lines */
{ 0, 7 },
{ 0, 6 },
{ 0, 5 },
{ 0, 4 },
{ 0, 2 },
{ 0, 1 },
{ 0, 0 },
{ 1, 7 },
{ 1, 5 },
{ 1, 4 },
{ 1, 3 },
{ 1, 2 },
{ 1, 1 } /* Hue $F in odd lines */
}
};
int cr, lm;
double const scaled_black_level = (double)COLOURS_PAL_setup.black_level / 255.0f;
double const scaled_white_level = (double)COLOURS_PAL_setup.white_level / 255.0f;
/* NTSC luma multipliers from CGIA.PDF */
static double const luma_mult[16] = {
0.6941, 0.7091, 0.7241, 0.7401,
0.7560, 0.7741, 0.7931, 0.8121,
0.8260, 0.8470, 0.8700, 0.8930,
0.9160, 0.9420, 0.9690, 1.0000};
/* When phase shift between even and odd colorbursts is close to 180 deg, the
TV stops interpreting color signal. This value determines how close to 180
deg that phase shift must be. It is specific to a TV set. */
static double const color_disable_threshold = 0.05;
/* Base delay - 1/4.43MHz * base_del = ca. 95.2ns */
static double const base_del = 0.421894970414201;
/* Additional delay - 1/4.43MHz * add_del = ca. 100.7ns */
static double const add_del = 0.446563064859117;
/* Delay introduced by the DEL pin voltage. */
double const del_adj = COLOURS_PAL_setup.color_delay / 360.0;
/* Phase delays of colorbursts in even and odd lines. They are equal to
Hue 1. */
double const even_burst_del = base_del + add_del * del_coeffs.even[0].add + del_adj * del_coeffs.even[0].mult;
double const odd_burst_del = base_del + add_del * del_coeffs.odd[0].add + del_adj * del_coeffs.odd[0].mult;
/* Reciprocal of the recreated subcarrier's amplitude. */
double saturation_mult;
/* Phase delay of the recreated amplitude. */
double subcarrier_del = (even_burst_del + odd_burst_del + COLOURS_PAL_setup.hue) / 2.0;
/* Phase difference between colorbursts in even and odd lines. */
double burst_diff = even_burst_del - odd_burst_del;
burst_diff -= floor(burst_diff); /* Normalize to 0..1. */
if (burst_diff > 0.5 - color_disable_threshold && burst_diff < 0.5 + color_disable_threshold)
/* Shift between colorbursts close to 180 deg. Don't produce color. */
saturation_mult = 0.0;
else {
/* Subcarrier is a sum of two waves with equal frequency and amplitude,
but phase-shifted by 2pi*burst_diff. The formula is derived from
http://2000clicks.com/mathhelp/GeometryTrigEquivPhaseShift.aspx */
double subcarrier_amplitude = sqrt(2.0 * cos(burst_diff*2.0*M_PI) + 2.0);
/* Normalise saturation_mult by multiplying by sqrt(2), so that it
equals 1.0 when odd & even colorbursts are shifted by 90 deg (ie.
burst_diff == 0.25). */
saturation_mult = sqrt(2.0) / subcarrier_amplitude;
}
for (cr = 0; cr < 16; cr ++) {
double even_u = 0.0;
double odd_u = 0.0;
double even_v = 0.0;
double odd_v = 0.0;
if (cr) {
struct del_coeff const *even_delay = &(del_coeffs.even[cr - 1]);
struct del_coeff const *odd_delay = &(del_coeffs.odd[cr - 1]);
double even_del = base_del + add_del * even_delay->add + del_adj * even_delay->mult;
double odd_del = base_del + add_del * odd_delay->add + del_adj * odd_delay->mult;
double even_angle = (0.5 - (even_del - subcarrier_del)) * 2.0 * M_PI;
double odd_angle = (0.5 + (odd_del - subcarrier_del)) * 2.0 * M_PI;
double saturation = (COLOURS_PAL_setup.saturation + 1) * 0.175 * saturation_mult;
even_u = cos(even_angle) * saturation;
even_v = sin(even_angle) * saturation;
odd_u = cos(odd_angle) * saturation;
odd_v = sin(odd_angle) * saturation;
}
for (lm = 0; lm < 16; lm ++) {
/* calculate yuv for color entry */
double y = (luma_mult[lm] - luma_mult[0]) / (luma_mult[15] - luma_mult[0]);
y *= COLOURS_PAL_setup.contrast * 0.5 + 1;
y += COLOURS_PAL_setup.brightness * 0.5;
/* Scale the Y signal's range from 0..1 to
scaled_black_level..scaled_white_level */
y = y * (scaled_white_level - scaled_black_level) + scaled_black_level;
/*
if (y < scaled_black_level)
y = scaled_black_level;
else if (y > scaled_white_level)
y = scaled_white_level;
*/
*yuv_table++ = y;
*yuv_table++ = even_u;
*yuv_table++ = odd_u;
*yuv_table++ = even_v;
*yuv_table++ = odd_v;
}
}
}
void COLOURS_PAL_GetYUV(double yuv_table[256*5])
{
if (COLOURS_PAL_external.loaded)
GetYUVFromExternal(yuv_table);
else
GetYUVFromGenerated(yuv_table);
}
/* Averages YUV values from YUV_TABLE and converts them to RGB values. Stores
them in COLOURTABLE. */
static void YUV2RGB(int colourtable[256], double const yuv_table[256*5])
{
double const *yuv_ptr = yuv_table;
int n;
for (n = 0; n < 256; ++n) {
double y = *yuv_ptr++;
double even_u = *yuv_ptr++;
double odd_u = *yuv_ptr++;
double even_v = *yuv_ptr++;
double odd_v = *yuv_ptr++;
double r, g, b;
/* The different colors in odd and even lines are not
emulated - instead the palette contains averaged values. */
double u = (even_u + odd_u) / 2.0;
double v = (even_v + odd_v) / 2.0;
Colours_YUV2RGB(y, u, v, &r, &g, &b);
if (!COLOURS_PAL_external.loaded || COLOURS_PAL_external.adjust) {
/* The r, g, b values derived from the YUV signal are non-linear
(ie. gamma-corrected). We convert them to linear values,
assuming the CRT TV's gamma = COLOURS_PAL_setup.gamma. */
r = Colours_Gamma2Linear(r, COLOURS_PAL_setup.gamma);
g = Colours_Gamma2Linear(g, COLOURS_PAL_setup.gamma);
b = Colours_Gamma2Linear(b, COLOURS_PAL_setup.gamma);
/* Now we convert the linear values to the sRGB colourspace. */
r = Colours_Linear2sRGB(r);
g = Colours_Linear2sRGB(g);
b = Colours_Linear2sRGB(b);
}
Colours_SetRGB(n, (int) (r * 255), (int) (g * 255), (int) (b * 255), colourtable);
}
}
void COLOURS_PAL_Update(int colourtable[256])
{
double yuv_table[256*5];
COLOURS_PAL_GetYUV(yuv_table);
YUV2RGB(colourtable, yuv_table);
}
void COLOURS_PAL_RestoreDefaults(void)
{
COLOURS_PAL_setup.color_delay = default_setup.color_delay;
}
Colours_preset_t COLOURS_PAL_GetPreset()
{
if (Util_almostequal(COLOURS_PAL_setup.color_delay, default_setup.color_delay, 0.001))
return COLOURS_PRESET_STANDARD;
return COLOURS_PRESET_CUSTOM;
}
int COLOURS_PAL_ReadConfig(char *option, char *ptr)
{
if (strcmp(option, "COLOURS_PAL_SATURATION") == 0)
return Util_sscandouble(ptr, &COLOURS_PAL_setup.saturation);
else if (strcmp(option, "COLOURS_PAL_CONTRAST") == 0)
return Util_sscandouble(ptr, &COLOURS_PAL_setup.contrast);
else if (strcmp(option, "COLOURS_PAL_BRIGHTNESS") == 0)
return Util_sscandouble(ptr, &COLOURS_PAL_setup.brightness);
else if (strcmp(option, "COLOURS_PAL_GAMMA") == 0)
return Util_sscandouble(ptr, &COLOURS_PAL_setup.gamma);
else if (strcmp(option, "COLOURS_PAL_HUE") == 0)
return Util_sscandouble(ptr, &COLOURS_PAL_setup.hue);
else if (strcmp(option, "COLOURS_PAL_GTIA_DELAY") == 0)
return Util_sscandouble(ptr, &COLOURS_PAL_setup.color_delay);
else if (strcmp(option, "COLOURS_PAL_EXTERNAL_PALETTE") == 0)
Util_strlcpy(COLOURS_PAL_external.filename, ptr, sizeof(COLOURS_PAL_external.filename));
else if (strcmp(option, "COLOURS_PAL_EXTERNAL_PALETTE_LOADED") == 0)
/* Use the "loaded" flag to indicate that the palette must be loaded later. */
return (COLOURS_PAL_external.loaded = Util_sscanbool(ptr)) != -1;
else if (strcmp(option, "COLOURS_PAL_ADJUST_EXTERNAL_PALETTE") == 0)
return (COLOURS_PAL_external.adjust = Util_sscanbool(ptr)) != -1;
else
return FALSE;
return TRUE;
}
void COLOURS_PAL_WriteConfig(FILE *fp)
{
fprintf(fp, "COLOURS_PAL_SATURATION=%g\n", COLOURS_PAL_setup.saturation);
fprintf(fp, "COLOURS_PAL_CONTRAST=%g\n", COLOURS_PAL_setup.contrast);
fprintf(fp, "COLOURS_PAL_BRIGHTNESS=%g\n", COLOURS_PAL_setup.brightness);
fprintf(fp, "COLOURS_PAL_GAMMA=%g\n", COLOURS_PAL_setup.gamma);
fprintf(fp, "COLOURS_PAL_HUE=%g\n", COLOURS_PAL_setup.hue);
fprintf(fp, "COLOURS_PAL_GTIA_DELAY=%g\n", COLOURS_PAL_setup.color_delay);
fprintf(fp, "COLOURS_PAL_EXTERNAL_PALETTE=%s\n", COLOURS_PAL_external.filename);
fprintf(fp, "COLOURS_PAL_EXTERNAL_PALETTE_LOADED=%d\n", COLOURS_PAL_external.loaded);
fprintf(fp, "COLOURS_PAL_ADJUST_EXTERNAL_PALETTE=%d\n", COLOURS_PAL_external.adjust);
}
int COLOURS_PAL_Initialise(int *argc, char *argv[])
{
int i;
int j;
for (i = j = 1; i < *argc; i++) {
int i_a = (i + 1 < *argc); /* is argument available? */
int a_m = FALSE; /* error, argument missing! */
if (strcmp(argv[i], "-pal-saturation") == 0) {
if (i_a)
COLOURS_PAL_setup.saturation = atof(argv[++i]);
else a_m = TRUE;
}
else if (strcmp(argv[i], "-pal-contrast") == 0) {
if (i_a)
COLOURS_PAL_setup.contrast = atof(argv[++i]);
else a_m = TRUE;
}
else if (strcmp(argv[i], "-pal-brightness") == 0) {
if (i_a)
COLOURS_PAL_setup.brightness = atof(argv[++i]);
else a_m = TRUE;
}
else if (strcmp(argv[i], "-pal-gamma") == 0) {
if (i_a)
COLOURS_PAL_setup.gamma = atof(argv[++i]);
else a_m = TRUE;
}
else if (strcmp(argv[i], "-pal-tint") == 0) {
if (i_a)
COLOURS_PAL_setup.hue = atof(argv[++i]);
else a_m = TRUE;
}
else if (strcmp(argv[i], "-pal-colordelay") == 0) {
if (i_a)
COLOURS_PAL_setup.color_delay = atof(argv[++i]);
else a_m = TRUE;
}
else if (strcmp(argv[i], "-palettep") == 0) {
if (i_a) {
Util_strlcpy(COLOURS_PAL_external.filename, argv[++i], sizeof(COLOURS_PAL_external.filename));
/* Use the "loaded" flag to indicate that the palette must be loaded later. */
COLOURS_PAL_external.loaded = TRUE;
} else a_m = TRUE;
}
else if (strcmp(argv[i], "-palettep-adjust") == 0)
COLOURS_PAL_external.adjust = TRUE;
else {
if (strcmp(argv[i], "-help") == 0) {
Log_print("\t-pal-saturation <num> Set PAL color saturation");
Log_print("\t-pal-contrast <num> Set PAL contrast");
Log_print("\t-pal-brightness <num> Set PAL brightness");
Log_print("\t-pal-gamma <num> Set PAL color gamma factor");
Log_print("\t-pal-tint <num> Set PAL tint");
Log_print("\t-pal-colordelay <num> Set PAL GTIA color delay");
Log_print("\t-palettep <filename> Load PAL external palette");
Log_print("\t-palettep-adjust Apply adjustments to PAL external palette");
}
argv[j++] = argv[i];
}
if (a_m) {
Log_print("Missing argument for '%s'", argv[i]);
return FALSE;
}
}
*argc = j;
/* Try loading an external palette if needed. */
if (COLOURS_PAL_external.loaded && !COLOURS_EXTERNAL_Read(&COLOURS_PAL_external))
Log_print("Cannot read PAL palette from %s", COLOURS_PAL_external.filename);
return TRUE;
}