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;