Feature: HDMI color controls via tranceiver CSC (#715)

* Addition of HDMI color controls (brightness, contrast, saturation, hue, gain, offset) that can be tweaked via MiSTer.ini (examples in-file). These controls are used to construct a matrix for the HDMI chip's color space converter. Due to the nature of this change, HDMI limited and YCbCr options have been reworked and combined into the color matrix routine (hdmi_config_set_csc)

* Change cc_ prefix to video_, as per suggestion and inline with existing video options.
This commit is contained in:
Sam Hardeman
2022-12-21 14:38:47 +09:00
committed by GitHub
parent caac5f52b2
commit 3214046c50
5 changed files with 369 additions and 27 deletions

View File

@@ -117,6 +117,24 @@ refresh_max=0
;video_mode_ntsc=0
;video_mode_pal=7
; Provided below are parameters for HDMI color controls.
; The defaults that are set below will result in an unaltered image.
; Brightness, contrast and saturation all can be set to 0 - 100.
; Hue can be set to 0 - 360, observing the HSL color representation.
; Each of 6 (mandatory) values in gain/offset can be set to -2 - 2.
; These 6 values represent gain and offset in order: Rg,Ro,Gg,Go,Bg,Bo
; Example 1: Inverted colors, hue shifted 180 degrees:
; video_hue= 180
; video_gain_offset= -1, 1, -1, 1, -1, 1
; Example 2: Slightly desaturated, warm display
; video_saturation= 80
; video_gain_offset= 1.5, -0.1, 1.3, -0.15, 0.9, 0.05
video_brightness=50
video_contrast=50
video_saturation=100
video_hue=0
video_gain_offset=1,0,1,0,1,0
; 1-10 (seconds) to display controller's button map upon first time key press
; 0 - disable
controller_info=6

10
cfg.cpp
View File

@@ -108,6 +108,11 @@ static const ini_var_t ini_vars[] =
{ "PLAYER_3_CONTROLLER", (void*)(&(cfg.player_controller[2])), STRING, 0, sizeof(cfg.player_controller[2]) - 1 },
{ "PLAYER_4_CONTROLLER", (void*)(&(cfg.player_controller[3])), STRING, 0, sizeof(cfg.player_controller[3]) - 1 },
{ "DISABLE_AUTOFIRE", (void *)(&(cfg.disable_autofire)), UINT8, 0, 1},
{ "VIDEO_BRIGHTNESS", (void *)(&(cfg.video_brightness)), UINT16, 0, 100},
{ "VIDEO_CONTRAST", (void *)(&(cfg.video_contrast)), UINT16, 0, 100},
{ "VIDEO_SATURATION", (void *)(&(cfg.video_saturation)), UINT16, 0, 100},
{ "VIDEO_HUE", (void *)(&(cfg.video_hue)), UINT16, 0, 360},
{ "VIDEO_GAIN_OFFSET", (void *)(&(cfg.video_gain_offset)), STRING, 0, sizeof(cfg.video_gain_offset)},
};
static const int nvars = (int)(sizeof(ini_vars) / sizeof(ini_var_t));
@@ -440,6 +445,11 @@ void cfg_parse()
cfg.rumble = 1;
cfg.wheel_force = 50;
cfg.dvi_mode = 2;
cfg.video_brightness = 50;
cfg.video_contrast = 50;
cfg.video_saturation = 100;
cfg.video_hue = 0;
strcpy(cfg.video_gain_offset, "1, 0, 1, 0, 1, 0");
has_video_sections = false;
using_video_section = false;
cfg_error_count = 0;

5
cfg.h
View File

@@ -80,6 +80,11 @@ typedef struct {
uint8_t vrr_vesa_framerate;
uint16_t video_off;
uint8_t disable_autofire;
uint16_t video_brightness;
uint16_t video_contrast;
uint16_t video_saturation;
uint16_t video_hue;
char video_gain_offset[256];
} cfg_t;
extern cfg_t cfg;

93
mat4x4.h Executable file
View File

@@ -0,0 +1,93 @@
#ifndef MAT4x4_H
#define MAT4x4_H
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct mat4x4
{
union
{
struct
{
float
m11, m12, m13, m14,
m21, m22, m23, m24,
m31, m32, m33, m34,
m41, m42, m43, m44;
};
float comp[16];
float comp_2d[4][4];
};
mat4x4(void)
{
memset(comp, 0, 16*sizeof(float));
}
mat4x4(const float mat[16])
: mat4x4()
{
for ( size_t i = 0; i < 16; i++ )
{
comp[i] = mat[i];
}
}
void setIdentity()
{
m11 = m22 = m33 = m44 = 1.0f;
}
mat4x4 operator* (const mat4x4 b)
{
mat4x4 a = *this;
for( size_t r = 0; r < 4; r++ )
{
for( size_t c = 0; c < 4; c++ )
{
comp_2d[r][c] =
b.comp_2d[r][0] * a.comp_2d[0][c] +
b.comp_2d[r][1] * a.comp_2d[1][c] +
b.comp_2d[r][2] * a.comp_2d[2][c] +
b.comp_2d[r][3] * a.comp_2d[3][c];
}
}
return *this;
}
// if the matrix has values over x, compress the rest down to make sure it fits
void compress(float x)
{
float maximum = 0.0;
bool max_found = false;
// find maximum
for ( size_t i = 0; i < 16; i++)
{
float& y = comp[i];
if (y > x)
{
maximum = abs(y) > maximum ? abs(y) : maximum;
max_found = true;
}
}
// apply maximum, range will be [-x .. x]
if (max_found)
{
for ( size_t i = 0; i < 16; i++)
{
comp[i] /= maximum;
comp[i] *= x;
}
}
}
};
#endif

270
video.cpp
View File

@@ -17,6 +17,7 @@
#include "spi.h"
#include "cfg.h"
#include "file_io.h"
#include "mat4x4.h"
#include "menu.h"
#include "video.h"
#include "input.h"
@@ -1059,6 +1060,246 @@ static void hdmi_config_set_spare(bool val)
}
}
static void hdmi_config_set_csc()
{
// default color conversion matrices
// for the original hexadecimal versions please refer
// to the ADV7513 programming guide section 4.3.7
float ypbpr_coeffs[] = {
0.42944335937f, 1.64038085938f, 1.93017578125f, 0.49389648437f,
0.25683593750f, 0.50415039062f, 0.09790039062f, 0.06250f,
1.85498046875f, 1.71557617188f, 0.42944335937f, 0.49389648437f,
0.0f, 0.0f, 0.0f, 1.0f
};
// no transformation, so use identity matrix
float hdmi_full_coeffs[] = {
1.0f, 0.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f
};
float hdmi_limited_1_coeffs[] = {
0.8583984375f, 0.0f, 0.0f, 0.06250f,
0.0f, 0.8583984375f, 0.0f, 0.06250f,
0.0f, 0.0f, 0.8583984375f, 0.06250f,
0.0f, 0.0f, 0.0f, 1.0f
};
float hdmi_limited_2_coeffs[] = {
0.93701171875f, 0.0f, 0.0f, 0.06250f,
0.0f, 0.93701171875f, 0.0f, 0.06250f,
0.0f, 0.0f, 0.93701171875f, 0.06250f,
0.0f, 0.0f, 0.0f, 1.0f
};
const float pi = float(M_PI);
// select the base CSC
int ypbpr = cfg.ypbpr && cfg.direct_video;
int hdmi_limited_1 = cfg.hdmi_limited & 1;
int hdmi_limited_2 = cfg.hdmi_limited & 2;
mat4x4 coeffs = hdmi_full_coeffs;
if (ypbpr)
coeffs = ypbpr_coeffs;
else if (hdmi_limited_1)
coeffs = hdmi_limited_1_coeffs;
else if (hdmi_limited_2)
coeffs = hdmi_limited_2_coeffs;
else
coeffs = hdmi_full_coeffs;
mat4x4 csc(coeffs);
// apply color controls
float brightness = (((cfg.video_brightness/100.0f) - 0.5f)); // [-0.5 .. 0.5]
float contrast = ((cfg.video_contrast/100.0f) - 0.5f) * 2.0f + 1.0f; // [0 .. 2]
float saturation = ((cfg.video_saturation/100.0f)); // [0 .. 1]
float hue = (cfg.video_hue * pi / 180.0f);
char* gain_offset = cfg.video_gain_offset;
// we have to parse these
float gain_red = 1;
float gain_green = 1;
float gain_blue = 1;
float off_red = 0;
float off_green = 0;
float off_blue = 0;
size_t target = 0;
float* targets[6] = { &gain_red, &off_red, &gain_green, &off_green, &gain_blue, &off_blue };
for (size_t i = 0; i < strlen(gain_offset) && target < 6; i++)
{
// skip whitespace
if (gain_offset[i] == ' ' || gain_offset[i] == ',')
continue;
int numRead = 0;
int match = sscanf(gain_offset + i, "%f%n", targets[target], &numRead);
i += numRead > 0 ? numRead - 1 : 0;
if (match == 1)
target++;
}
// first apply hue matrix, because it does not touch luminance
float cos_hue = cos(hue);
float sin_hue = sin(hue);
float lr = 0.213f;
float lg = 0.715f;
float lb = 0.072f;
float ca = 0.143f;
float cb = 0.140f;
float cc = 0.283f;
mat4x4 mat_hue;
mat_hue.setIdentity();
mat_hue.m11 = lr+cos_hue*(1-lr)+sin_hue*(-lr);
mat_hue.m12 = lg+cos_hue*(-lg) +sin_hue*(-lg);
mat_hue.m13 = lb+cos_hue*(-lb) +sin_hue*(1-lb);
mat_hue.m21 = lr+cos_hue*(-lr) +sin_hue*(ca);
mat_hue.m22 = lg+cos_hue*(1-lg)+sin_hue*(cb);
mat_hue.m23 = lb+cos_hue*(-lb) +sin_hue*(cc);
mat_hue.m31 = lr+cos_hue*(-lr) +sin_hue*(-(1-lr));
mat_hue.m32 = lg+cos_hue*(-lg) +sin_hue*(lg);
mat_hue.m33 = lb+cos_hue*(1-lb)+sin_hue*(lb);
csc = csc * mat_hue;
// now saturation
float s = saturation;
float sr = ( 1.0f - s ) * .3086f;
float sg = ( 1.0f - s ) * .6094f;
float sb = ( 1.0f - s ) * .0920f;
float mat_saturation[] = {
sr + s, sg, sb, 0,
sr, sg + s, sb, 0,
sr, sg, sb + s, 0,
0, 0, 0, 1.0f
};
csc = csc * mat4x4(mat_saturation);
// now brightness and contrast
float b = brightness;
float c = contrast;
float t = (1.0f - c) / 2.0f;
float mat_brightness_contrast[] = {
c, 0, 0, (t+b),
0, c, 0, (t+b),
0, 0, c, (t+b),
0, 0, 0, 1.0f
};
csc = csc * mat4x4(mat_brightness_contrast);
// gain and offset
float rg = gain_red;
float ro = off_red;
float gg = gain_green;
float go = off_green;
float bg = gain_blue;
float bo = off_blue;
float mat_gain_off[] = {
rg, 0, 0, ro,
0, gg, 0, go,
0, 0, bg, bo,
0, 0, 0, 1.0f
};
csc = csc * mat4x4(mat_gain_off);
// final compression
csc.compress(2.0f);
// finally, apply a fixed multiplier to get it in
// correct range for ADV7513 chip
const int16_t csc_int16[12] = {
int16_t(csc.comp[0] * 2048.0f),
int16_t(csc.comp[1] * 2048.0f),
int16_t(csc.comp[2] * 2048.0f),
int16_t(csc.comp[3] * 2048.0f),
int16_t(csc.comp[4] * 2048.0f),
int16_t(csc.comp[5] * 2048.0f),
int16_t(csc.comp[6] * 2048.0f),
int16_t(csc.comp[7] * 2048.0f),
int16_t(csc.comp[8] * 2048.0f),
int16_t(csc.comp[9] * 2048.0f),
int16_t(csc.comp[10] * 2048.0f),
int16_t(csc.comp[11] * 2048.0f),
};
// Clamps to reinforce limited if necessary
// 0x100 = 16/256 * 4096 (12-bit mul)
// 0xEB0 = 235/256 * 4096
// 0xFFF = 4095 (12-bit max)
uint16_t clipMin = (hdmi_limited_1 || hdmi_limited_2) ? 0x100 : 0x000;
uint16_t clipMax = hdmi_limited_1 ? 0xEB0 : 0xFFF;
// pass to HDMI, use 0xA0 to set a mode of [-2 .. 2] per ADV7513 programming guide
uint8_t csc_data[] = {
0x18, (uint8_t)(0b10100000 | (( csc_int16[0] >> 8) & 0b00011111)), // csc Coefficients, Channel A
0x19, (uint8_t)(csc_int16[0] & 0xff),
0x1A, (uint8_t)(csc_int16[1] >> 8),
0x1B, (uint8_t)(csc_int16[1] & 0xff),
0x1C, (uint8_t)(csc_int16[2] >> 8),
0x1D, (uint8_t)(csc_int16[2] & 0xff),
0x1E, (uint8_t)(csc_int16[3] >> 8),
0x1F, (uint8_t)(csc_int16[3] & 0xff),
0x20, (uint8_t)(csc_int16[4] >> 8), // csc Coefficients, Channel B
0x21, (uint8_t)(csc_int16[4] & 0xff),
0x22, (uint8_t)(csc_int16[5] >> 8),
0x23, (uint8_t)(csc_int16[5] & 0xff),
0x24, (uint8_t)(csc_int16[6] >> 8),
0x25, (uint8_t)(csc_int16[6] & 0xff),
0x26, (uint8_t)(csc_int16[7] >> 8),
0x27, (uint8_t)(csc_int16[7] & 0xff),
0x28, (uint8_t)(csc_int16[8] >> 8), // csc Coefficients, Channel C
0x29, (uint8_t)(csc_int16[8] & 0xff),
0x2A, (uint8_t)(csc_int16[9] >> 8),
0x2B, (uint8_t)(csc_int16[9] & 0xff),
0x2C, (uint8_t)(csc_int16[10] >> 8),
0x2D, (uint8_t)(csc_int16[10] & 0xff),
0x2E, (uint8_t)(csc_int16[11] >> 8),
0x2F, (uint8_t)(csc_int16[11] & 0xff),
0xC0, (uint8_t)(clipMin >> 8), // HDMI limited clamps
0xC1, (uint8_t)(clipMin & 0xff),
0xC2, (uint8_t)(clipMax >> 8),
0xC3, (uint8_t)(clipMax & 0xff)
};
int fd = i2c_open(0x39, 0);
if (fd >= 0)
{
for (uint i = 0; i < sizeof(csc_data); i += 2)
{
int res = i2c_smbus_write_byte_data(fd, csc_data[i], csc_data[i + 1]);
if (res < 0) printf("i2c: write error (%02X %02X): %d\n", csc_data[i], csc_data[i + 1], res);
}
i2c_close(fd);
}
else
{
printf("*** ADV7513 not found on i2c bus! HDMI won't be available!\n");
}
}
static void hdmi_config_init()
{
int ypbpr = cfg.ypbpr && cfg.direct_video;
@@ -1100,33 +1341,6 @@ static void hdmi_config_init()
0x17, 0b01100010, // Aspect ratio 16:9 [1]=1, 4:3 [1]=0, invert sync polarity
0x18, (uint8_t)(ypbpr ? 0x86 : (cfg.hdmi_limited & 1) ? 0x8D : (cfg.hdmi_limited & 2) ? 0x8E : 0x00), // CSC Scaling Factors and Coefficients for RGB Full->Limited.
0x19, (uint8_t)(ypbpr ? 0xDF : (cfg.hdmi_limited & 1) ? 0xBC : 0xFE), // Taken from table in ADV7513 Programming Guide.
0x1A, (uint8_t)(ypbpr ? 0x1A : 0x00), // CSC Channel A.
0x1B, (uint8_t)(ypbpr ? 0x3F : 0x00),
0x1C, (uint8_t)(ypbpr ? 0x1E : 0x00),
0x1D, (uint8_t)(ypbpr ? 0xE2 : 0x00),
0x1E, (uint8_t)(ypbpr ? 0x07 : 0x01),
0x1F, (uint8_t)(ypbpr ? 0xE7 : 0x00),
0x20, (uint8_t)(ypbpr ? 0x04 : 0x00), // CSC Channel B.
0x21, (uint8_t)(ypbpr ? 0x1C : 0x00),
0x22, (uint8_t)(ypbpr ? 0x08 : (cfg.hdmi_limited & 1) ? 0x0D : 0x0E),
0x23, (uint8_t)(ypbpr ? 0x11 : (cfg.hdmi_limited & 1) ? 0xBC : 0xFE),
0x24, (uint8_t)(ypbpr ? 0x01 : 0x00),
0x25, (uint8_t)(ypbpr ? 0x91 : 0x00),
0x26, (uint8_t)(ypbpr ? 0x01 : 0x01),
0x27, 0x00,
0x28, (uint8_t)(ypbpr ? 0x1D : 0x00), // CSC Channel C.
0x29, (uint8_t)(ypbpr ? 0xAE : 0x00),
0x2A, (uint8_t)(ypbpr ? 0x1B : 0x00),
0x2B, (uint8_t)(ypbpr ? 0x73 : 0x00),
0x2C, (uint8_t)(ypbpr ? 0x06 : (cfg.hdmi_limited & 1) ? 0x0D : 0x0E),
0x2D, (uint8_t)(ypbpr ? 0xDF : (cfg.hdmi_limited & 1) ? 0xBC : 0xFE),
0x2E, (uint8_t)(ypbpr ? 0x07 : 0x01),
0x2F, (uint8_t)(ypbpr ? 0xE7 : 0x00),
0x3B, 0x0, // Automatic pixel repetition and VIC detection
@@ -1241,6 +1455,8 @@ static void hdmi_config_init()
{
printf("*** ADV7513 not found on i2c bus! HDMI won't be available!\n");
}
hdmi_config_set_csc();
}
static uint8_t last_sync_invert = 0xff;