From 3214046c50823ce7bcc12caee61a5401b6bbe4c9 Mon Sep 17 00:00:00 2001 From: Sam Hardeman Date: Wed, 21 Dec 2022 14:38:47 +0900 Subject: [PATCH] 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. --- MiSTer.ini | 18 ++++ cfg.cpp | 10 ++ cfg.h | 5 + mat4x4.h | 93 ++++++++++++++++++ video.cpp | 270 +++++++++++++++++++++++++++++++++++++++++++++++------ 5 files changed, 369 insertions(+), 27 deletions(-) create mode 100755 mat4x4.h diff --git a/MiSTer.ini b/MiSTer.ini index 187cf32..035dbdc 100644 --- a/MiSTer.ini +++ b/MiSTer.ini @@ -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 diff --git a/cfg.cpp b/cfg.cpp index 6bb28e6..42eb650 100644 --- a/cfg.cpp +++ b/cfg.cpp @@ -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; diff --git a/cfg.h b/cfg.h index c663a97..e6cd7e5 100644 --- a/cfg.h +++ b/cfg.h @@ -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; diff --git a/mat4x4.h b/mat4x4.h new file mode 100755 index 0000000..0b4768f --- /dev/null +++ b/mat4x4.h @@ -0,0 +1,93 @@ +#ifndef MAT4x4_H +#define MAT4x4_H + +#include +#include +#include +#include + +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 diff --git a/video.cpp b/video.cpp index a43f5fa..8623a27 100644 --- a/video.cpp +++ b/video.cpp @@ -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;