| // SPDX-License-Identifier: MIT |
| /* |
| * Copyright 2006-2012 Red Hat, Inc. |
| * Copyright 2018-2020 Cisco Systems, Inc. and/or its affiliates. All rights reserved. |
| * |
| * Author: Adam Jackson <[email protected]> |
| * Maintainer: Hans Verkuil <[email protected]> |
| */ |
| |
| #include <ctype.h> |
| #include <fcntl.h> |
| #include <getopt.h> |
| #include <math.h> |
| #include <stdarg.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <unistd.h> |
| |
| #include "edid-decode.h" |
| |
| #define STR(x) #x |
| #define STRING(x) STR(x) |
| |
| static edid_state state; |
| |
| static unsigned char edid[EDID_PAGE_SIZE * EDID_MAX_BLOCKS]; |
| static bool odd_hex_digits; |
| |
| enum output_format { |
| OUT_FMT_DEFAULT, |
| OUT_FMT_HEX, |
| OUT_FMT_RAW, |
| OUT_FMT_CARRAY, |
| OUT_FMT_XML, |
| }; |
| |
| /* |
| * Options |
| * Please keep in alphabetical order of the short option. |
| * That makes it easier to see which options are still free. |
| */ |
| enum Option { |
| OptCheck = 'c', |
| OptCheckInline = 'C', |
| OptFBModeTimings = 'F', |
| OptHelp = 'h', |
| OptOnlyHexDump = 'H', |
| OptLongTimings = 'L', |
| OptNativeTimings = 'n', |
| OptOutputFormat = 'o', |
| OptPreferredTimings = 'p', |
| OptPhysicalAddress = 'P', |
| OptSkipHexDump = 's', |
| OptShortTimings = 'S', |
| OptV4L2Timings = 'V', |
| OptXModeLineTimings = 'X', |
| OptSkipSHA = 128, |
| OptHideSerialNumbers, |
| OptVersion, |
| OptSTD, |
| OptDMT, |
| OptVIC, |
| OptHDMIVIC, |
| OptCVT, |
| OptGTF, |
| OptListEstTimings, |
| OptListDMTs, |
| OptListVICs, |
| OptListHDMIVICs, |
| OptLast = 256 |
| }; |
| |
| static char options[OptLast]; |
| |
| static struct option long_options[] = { |
| { "help", no_argument, 0, OptHelp }, |
| { "output-format", required_argument, 0, OptOutputFormat }, |
| { "native-timings", no_argument, 0, OptNativeTimings }, |
| { "preferred-timings", no_argument, 0, OptPreferredTimings }, |
| { "physical-address", no_argument, 0, OptPhysicalAddress }, |
| { "skip-hex-dump", no_argument, 0, OptSkipHexDump }, |
| { "only-hex-dump", no_argument, 0, OptOnlyHexDump }, |
| { "skip-sha", no_argument, 0, OptSkipSHA }, |
| { "hide-serial-numbers", no_argument, 0, OptHideSerialNumbers }, |
| { "version", no_argument, 0, OptVersion }, |
| { "check-inline", no_argument, 0, OptCheckInline }, |
| { "check", no_argument, 0, OptCheck }, |
| { "short-timings", no_argument, 0, OptShortTimings }, |
| { "long-timings", no_argument, 0, OptLongTimings }, |
| { "xmodeline", no_argument, 0, OptXModeLineTimings }, |
| { "fbmode", no_argument, 0, OptFBModeTimings }, |
| { "v4l2-timings", no_argument, 0, OptV4L2Timings }, |
| { "std", required_argument, 0, OptSTD }, |
| { "dmt", required_argument, 0, OptDMT }, |
| { "vic", required_argument, 0, OptVIC }, |
| { "hdmi-vic", required_argument, 0, OptHDMIVIC }, |
| { "cvt", required_argument, 0, OptCVT }, |
| { "gtf", required_argument, 0, OptGTF }, |
| { "list-established-timings", no_argument, 0, OptListEstTimings }, |
| { "list-dmts", no_argument, 0, OptListDMTs }, |
| { "list-vics", no_argument, 0, OptListVICs }, |
| { "list-hdmi-vics", no_argument, 0, OptListHDMIVICs }, |
| { 0, 0, 0, 0 } |
| }; |
| |
| static void usage(void) |
| { |
| printf("Usage: edid-decode <options> [in [out]]\n" |
| " [in] EDID file to parse. Read from standard input if none given\n" |
| " or if the input filename is '-'.\n" |
| " [out] Output the read EDID to this file. Write to standard output\n" |
| " if the output filename is '-'.\n" |
| "\nOptions:\n" |
| " -o, --output-format <fmt>\n" |
| " If [out] is specified, then write the EDID in this format\n" |
| " <fmt> is one of:\n" |
| " hex: hex numbers in ascii text (default for stdout)\n" |
| " raw: binary data (default unless writing to stdout)\n" |
| " carray: c-program struct\n" |
| " xml: XML data\n" |
| " -c, --check Check if the EDID conforms to the standards, failures and\n" |
| " warnings are reported at the end.\n" |
| " -C, --check-inline Check if the EDID conforms to the standards, failures and\n" |
| " warnings are reported inline.\n" |
| " -n, --native-timings Report the native timings.\n" |
| " -p, --preferred-timings Report the preferred timings.\n" |
| " -P, --physical-address Only report the CEC physical address.\n" |
| " -S, --short-timings Report all video timings in a short format.\n" |
| " -L, --long-timings Report all video timings in a long format.\n" |
| " -X, --xmodeline Report all long video timings in Xorg.conf format.\n" |
| " -F, --fbmode Report all long video timings in fb.modes format.\n" |
| " -V, --v4l2-timings Report all long video timings in v4l2-dv-timings.h format.\n" |
| " -s, --skip-hex-dump Skip the initial hex dump of the EDID.\n" |
| " -H, --only-hex-dump Only output the hex dump of the EDID.\n" |
| " --skip-sha Skip the SHA report.\n" |
| " --hide-serial-numbers Replace serial numbers with '...'\n" |
| " --version show the edid-decode version (SHA)\n" |
| " --std <byte1>,<byte2> Show the standard timing represented by these two bytes.\n" |
| " --dmt <dmt> Show the timings for the DMT with the given DMT ID.\n" |
| " --vic <vic> Show the timings for this VIC.\n" |
| " --hdmi-vic <hdmivic> Show the timings for this HDMI VIC.\n" |
| " --cvt w=<width>,h=<height>,fps=<fps>[,rb=<rb>][,interlaced][,overscan][,alt][,hblank=<hblank]\n" |
| " Calculate the CVT timings for the given format.\n" |
| " <fps> is frames per second for progressive timings,\n" |
| " or fields per second for interlaced timings.\n" |
| " <rb> can be 0 (no reduced blanking, default), or\n" |
| " 1-3 for the reduced blanking version.\n" |
| " If 'interlaced' is given, then this is an interlaced format.\n" |
| " If 'overscan' is given, then this is an overscanned format.\n" |
| " If 'alt' is given and <rb>=2, then report the timings\n" |
| " optimized for video: 1000 / 1001 * <fps>.\n" |
| " If 'alt' is given and <rb>=3, then the horizontal blanking\n" |
| " is 160 instead of 80 pixels.\n" |
| " If 'hblank' is given and <rb>=3, then the horizontal blanking\n" |
| " is <hblank> pixels (range of 80-200), overriding 'alt'.\n" |
| " --gtf w=<width>,h=<height>[,fps=<fps>][,horfreq=<horfreq>][,pixclk=<pixclk>][,interlaced]\n" |
| " [,overscan][,secondary][,C=<c>][,M=<m>][,K=<k>][,J=<j>]\n" |
| " Calculate the GTF timings for the given format.\n" |
| " <fps> is frames per second for progressive timings,\n" |
| " or fields per second for interlaced timings.\n" |
| " <horfreq> is the horizontal frequency in kHz.\n" |
| " <pixclk> is the pixel clock frequency in MHz.\n" |
| " Only one of fps, horfreq or pixclk must be given.\n" |
| " If 'interlaced' is given, then this is an interlaced format.\n" |
| " If 'overscan' is given, then this is an overscanned format.\n" |
| " If 'secondary' is given, then the secondary GTF is used for\n" |
| " reduced blanking, where <c>, <m>, <k> and <j> are parameters\n" |
| " for the secondary curve.\n" |
| " --list-established-timings List all known Established Timings.\n" |
| " --list-dmts List all known DMTs.\n" |
| " --list-vics List all known VICs.\n" |
| " --list-hdmi-vics List all known HDMI VICs.\n" |
| " -h, --help Display this help message.\n"); |
| } |
| |
| static std::string s_msgs[EDID_MAX_BLOCKS + 1][2]; |
| |
| void msg(bool is_warn, const char *fmt, ...) |
| { |
| char buf[1024] = ""; |
| va_list ap; |
| |
| va_start(ap, fmt); |
| vsprintf(buf, fmt, ap); |
| va_end(ap); |
| |
| if (is_warn) |
| state.warnings++; |
| else |
| state.failures++; |
| if (state.data_block.empty()) |
| s_msgs[state.block_nr][is_warn] += std::string(" ") + buf; |
| else |
| s_msgs[state.block_nr][is_warn] += " " + state.data_block + ": " + buf; |
| |
| if (options[OptCheckInline]) |
| printf("%s: %s", is_warn ? "WARN" : "FAIL", buf); |
| } |
| |
| static void show_msgs(bool is_warn) |
| { |
| printf("\n%s:\n\n", is_warn ? "Warnings" : "Failures"); |
| for (unsigned i = 0; i < state.num_blocks; i++) { |
| if (s_msgs[i][is_warn].empty()) |
| continue; |
| printf("Block %u, %s:\n%s", |
| i, block_name(edid[i * EDID_PAGE_SIZE]).c_str(), |
| s_msgs[i][is_warn].c_str()); |
| } |
| if (s_msgs[EDID_MAX_BLOCKS][is_warn].empty()) |
| return; |
| printf("EDID:\n%s", |
| s_msgs[EDID_MAX_BLOCKS][is_warn].c_str()); |
| } |
| |
| |
| void do_checksum(const char *prefix, const unsigned char *x, size_t len) |
| { |
| unsigned char check = x[len - 1]; |
| unsigned char sum = 0; |
| unsigned i; |
| |
| printf("%sChecksum: 0x%02hhx", prefix, check); |
| |
| for (i = 0; i < len-1; i++) |
| sum += x[i]; |
| |
| if ((unsigned char)(check + sum) != 0) { |
| printf(" (should be 0x%02x)\n", -sum & 0xff); |
| fail("Invalid checksum 0x%02x (should be 0x%02x).\n", |
| check, -sum & 0xff); |
| return; |
| } |
| printf("\n"); |
| } |
| |
| static unsigned gcd(unsigned a, unsigned b) |
| { |
| while (b) { |
| unsigned t = b; |
| |
| b = a % b; |
| a = t; |
| } |
| return a; |
| } |
| |
| void calc_ratio(struct timings *t) |
| { |
| unsigned d = gcd(t->hact, t->vact); |
| |
| if (d == 0) { |
| t->hratio = t->vratio = 0; |
| return; |
| } |
| t->hratio = t->hact / d; |
| t->vratio = t->vact / d; |
| } |
| |
| std::string edid_state::dtd_type(unsigned cnt) |
| { |
| unsigned len = std::to_string(cta.preparsed_total_dtds).length(); |
| char buf[16]; |
| sprintf(buf, "DTD %*u", len, cnt); |
| return buf; |
| } |
| |
| bool edid_state::match_timings(const timings &t1, const timings &t2) |
| { |
| if (t1.hact != t2.hact || |
| t1.vact != t2.vact || |
| t1.rb != t2.rb || |
| t1.interlaced != t2.interlaced || |
| t1.hfp != t2.hfp || |
| t1.hbp != t2.hbp || |
| t1.hsync != t2.hsync || |
| t1.pos_pol_hsync != t2.pos_pol_hsync || |
| t1.hratio != t2.hratio || |
| t1.vfp != t2.vfp || |
| t1.vbp != t2.vbp || |
| t1.vsync != t2.vsync || |
| t1.pos_pol_vsync != t2.pos_pol_vsync || |
| t1.vratio != t2.vratio || |
| t1.pixclk_khz != t2.pixclk_khz) |
| return false; |
| return true; |
| } |
| |
| static void or_str(std::string &s, const std::string &flag, unsigned &num_flags) |
| { |
| if (!num_flags) |
| s = flag; |
| else if (num_flags % 2 == 0) |
| s = s + " | \\\n\t\t" + flag; |
| else |
| s = s + " | " + flag; |
| num_flags++; |
| } |
| |
| /* |
| * Return true if the timings are a close, but not identical, |
| * match. The only differences allowed are polarities and |
| * porches and syncs, provided the total blanking remains the |
| * same. |
| */ |
| bool timings_close_match(const timings &t1, const timings &t2) |
| { |
| // We don't want to deal with borders, you're on your own |
| // if you are using those. |
| if (t1.hborder || t1.vborder || |
| t2.hborder || t2.vborder) |
| return false; |
| if (t1.hact != t2.hact || t1.vact != t2.vact || |
| t1.interlaced != t2.interlaced || |
| t1.pixclk_khz != t2.pixclk_khz || |
| t1.hfp + t1.hsync + t1.hbp != t2.hfp + t2.hsync + t2.hbp || |
| t1.vfp + t1.vsync + t1.vbp != t2.vfp + t2.vsync + t2.vbp) |
| return false; |
| if (t1.hfp == t2.hfp && |
| t1.hsync == t2.hsync && |
| t1.hbp == t2.hbp && |
| t1.pos_pol_hsync == t2.pos_pol_hsync && |
| t1.vfp == t2.vfp && |
| t1.vsync == t2.vsync && |
| t1.vbp == t2.vbp && |
| t1.pos_pol_vsync == t2.pos_pol_vsync) |
| return false; |
| return true; |
| } |
| |
| static void print_modeline(unsigned indent, const struct timings *t, double refresh) |
| { |
| unsigned offset = (!t->even_vtotal && t->interlaced) ? 1 : 0; |
| unsigned hfp = t->hborder + t->hfp; |
| unsigned hbp = t->hborder + t->hbp; |
| unsigned vfp = t->vborder + t->vfp; |
| unsigned vbp = t->vborder + t->vbp; |
| |
| printf("%*sModeline \"%ux%u_%.2f%s\" %.3f %u %u %u %u %u %u %u %u %cHSync", |
| indent, "", |
| t->hact, t->vact, refresh, |
| t->interlaced ? "i" : "", t->pixclk_khz / 1000.0, |
| t->hact, t->hact + hfp, t->hact + hfp + t->hsync, |
| t->hact + hfp + t->hsync + hbp, |
| t->vact, t->vact + vfp, t->vact + vfp + t->vsync, |
| t->vact + vfp + t->vsync + vbp + offset, |
| t->pos_pol_hsync ? '+' : '-'); |
| if (!t->no_pol_vsync) |
| printf(" %cVSync", t->pos_pol_vsync ? '+' : '-'); |
| if (t->interlaced) |
| printf(" Interlace"); |
| printf("\n"); |
| } |
| |
| static void print_fbmode(unsigned indent, const struct timings *t, |
| double refresh, double hor_freq_khz) |
| { |
| printf("%*smode \"%ux%u-%u%s\"\n", |
| indent, "", |
| t->hact, t->vact, |
| (unsigned)(0.5 + (t->interlaced ? refresh / 2.0 : refresh)), |
| t->interlaced ? "-lace" : ""); |
| printf("%*s# D: %.2f MHz, H: %.3f kHz, V: %.2f Hz\n", |
| indent + 8, "", |
| t->pixclk_khz / 1000.0, hor_freq_khz, refresh); |
| printf("%*sgeometry %u %u %u %u 32\n", |
| indent + 8, "", |
| t->hact, t->vact, t->hact, t->vact); |
| unsigned mult = t->interlaced ? 2 : 1; |
| unsigned offset = !t->even_vtotal && t->interlaced; |
| unsigned hfp = t->hborder + t->hfp; |
| unsigned hbp = t->hborder + t->hbp; |
| unsigned vfp = t->vborder + t->vfp; |
| unsigned vbp = t->vborder + t->vbp; |
| printf("%*stimings %llu %d %d %d %u %u %u\n", |
| indent + 8, "", |
| (unsigned long long)(1000000000.0 / (double)(t->pixclk_khz) + 0.5), |
| hbp, hfp, mult * vbp, mult * vfp + offset, t->hsync, mult * t->vsync); |
| if (t->interlaced) |
| printf("%*slaced true\n", indent + 8, ""); |
| if (t->pos_pol_hsync) |
| printf("%*shsync high\n", indent + 8, ""); |
| if (t->pos_pol_vsync) |
| printf("%*svsync high\n", indent + 8, ""); |
| printf("%*sendmode\n", indent, ""); |
| } |
| |
| static void print_v4l2_timing(const struct timings *t, |
| double refresh, const char *type) |
| { |
| printf("\t#define V4L2_DV_BT_%uX%u%c%u_%02u { \\\n", |
| t->hact, t->vact, t->interlaced ? 'I' : 'P', |
| (unsigned)refresh, (unsigned)(0.5 + 100.0 * (refresh - (unsigned)refresh))); |
| printf("\t\t.type = V4L2_DV_BT_656_1120, \\\n"); |
| printf("\t\tV4L2_INIT_BT_TIMINGS(%u, %u, %u, ", |
| t->hact, t->vact, t->interlaced); |
| if (!t->pos_pol_hsync && !t->pos_pol_vsync) |
| printf("0, \\\n"); |
| else if (t->pos_pol_hsync && t->pos_pol_vsync) |
| printf("\\\n\t\t\tV4L2_DV_HSYNC_POS_POL | V4L2_DV_VSYNC_POS_POL, \\\n"); |
| else if (t->pos_pol_hsync) |
| printf("V4L2_DV_HSYNC_POS_POL, \\\n"); |
| else |
| printf("V4L2_DV_VSYNC_POS_POL, \\\n"); |
| unsigned hfp = t->hborder + t->hfp; |
| unsigned hbp = t->hborder + t->hbp; |
| unsigned vfp = t->vborder + t->vfp; |
| unsigned vbp = t->vborder + t->vbp; |
| printf("\t\t\t%lluULL, %d, %u, %d, %u, %u, %d, %u, %u, %d, \\\n", |
| t->pixclk_khz * 1000ULL, hfp, t->hsync, hbp, |
| vfp, t->vsync, vbp, |
| t->interlaced ? vfp : 0, |
| t->interlaced ? t->vsync : 0, |
| t->interlaced ? vbp + !t->even_vtotal : 0); |
| |
| std::string flags; |
| unsigned num_flags = 0; |
| unsigned vic = 0; |
| unsigned hdmi_vic = 0; |
| const char *std = "0"; |
| |
| if (t->interlaced && !t->even_vtotal) |
| or_str(flags, "V4L2_DV_FL_HALF_LINE", num_flags); |
| if (!memcmp(type, "VIC", 3)) { |
| or_str(flags, "V4L2_DV_FL_HAS_CEA861_VIC", num_flags); |
| or_str(flags, "V4L2_DV_FL_IS_CE_VIDEO", num_flags); |
| vic = strtoul(type + 4, 0, 0); |
| } |
| if (!memcmp(type, "HDMI VIC", 8)) { |
| or_str(flags, "V4L2_DV_FL_HAS_HDMI_VIC", num_flags); |
| or_str(flags, "V4L2_DV_FL_IS_CE_VIDEO", num_flags); |
| hdmi_vic = strtoul(type + 9, 0, 0); |
| vic = hdmi_vic_to_vic(hdmi_vic); |
| if (vic) |
| or_str(flags, "V4L2_DV_FL_HAS_CEA861_VIC", num_flags); |
| } |
| if (vic && (fmod(refresh, 6)) == 0.0) |
| or_str(flags, "V4L2_DV_FL_CAN_REDUCE_FPS", num_flags); |
| if (t->rb) |
| or_str(flags, "V4L2_DV_FL_REDUCED_BLANKING", num_flags); |
| if (t->hratio && t->vratio) |
| or_str(flags, "V4L2_DV_FL_HAS_PICTURE_ASPECT", num_flags); |
| |
| if (!memcmp(type, "VIC", 3) || !memcmp(type, "HDMI VIC", 8)) |
| std = "V4L2_DV_BT_STD_CEA861"; |
| else if (!memcmp(type, "DMT", 3)) |
| std = "V4L2_DV_BT_STD_DMT"; |
| else if (!memcmp(type, "CVT", 3)) |
| std = "V4L2_DV_BT_STD_CVT"; |
| else if (!memcmp(type, "GTF", 3)) |
| std = "V4L2_DV_BT_STD_GTF"; |
| printf("\t\t\t%s, \\\n", std); |
| printf("\t\t\t%s, \\\n", flags.empty() ? "0" : flags.c_str()); |
| printf("\t\t\t{ %u, %u }, %u, %u) \\\n", |
| t->hratio, t->vratio, vic, hdmi_vic); |
| printf("\t}\n"); |
| } |
| |
| static void print_detailed_timing(unsigned indent, const struct timings *t) |
| { |
| printf("%*sHfront %4d Hsync %3u Hback %3d Hpol %s", |
| indent, "", |
| t->hfp, t->hsync, t->hbp, t->pos_pol_hsync ? "P" : "N"); |
| if (t->hborder) |
| printf(" Hborder %u", t->hborder); |
| printf("\n"); |
| |
| printf("%*sVfront %4u Vsync %3u Vback %3d", |
| indent, "", t->vfp, t->vsync, t->vbp); |
| if (!t->no_pol_vsync) |
| printf(" Vpol %s", t->pos_pol_vsync ? "P" : "N"); |
| if (t->vborder) |
| printf(" Vborder %u", t->vborder); |
| if (t->even_vtotal) { |
| printf(" Both Fields"); |
| } else if (t->interlaced) { |
| printf(" Vfront +0.5 Odd Field\n"); |
| printf("%*sVfront %4d Vsync %3u Vback %3d", |
| indent, "", t->vfp, t->vsync, t->vbp); |
| if (!t->no_pol_vsync) |
| printf(" Vpol %s", t->pos_pol_vsync ? "P" : "N"); |
| if (t->vborder) |
| printf(" Vborder %u", t->vborder); |
| printf(" Vback +0.5 Even Field"); |
| } |
| printf("\n"); |
| } |
| |
| bool edid_state::print_timings(const char *prefix, const struct timings *t, |
| const char *type, const char *flags, |
| bool detailed, bool do_checks) |
| { |
| if (!t) { |
| // Should not happen |
| if (do_checks) |
| fail("Unknown video timings.\n"); |
| return false; |
| } |
| |
| if (detailed && options[OptShortTimings]) |
| detailed = false; |
| if (options[OptLongTimings]) |
| detailed = true; |
| |
| unsigned vact = t->vact; |
| unsigned hbl = t->hfp + t->hsync + t->hbp + 2 * t->hborder; |
| unsigned vbl = t->vfp + t->vsync + t->vbp + 2 * t->vborder; |
| unsigned htotal = t->hact + hbl; |
| double hor_freq_khz = htotal ? (double)t->pixclk_khz / htotal : 0; |
| |
| if (t->interlaced) |
| vact /= 2; |
| |
| if (t->ycbcr420) |
| hor_freq_khz /= 2; |
| |
| double vtotal = vact + vbl; |
| |
| bool ok = true; |
| |
| if (!t->hact || !hbl || !t->hfp || !t->hsync || |
| !vact || !vbl || (!t->vfp && !t->interlaced && !t->even_vtotal) || !t->vsync) { |
| if (do_checks) |
| fail("0 values in the video timing:\n" |
| " Horizontal Active/Blanking %u/%u\n" |
| " Horizontal Frontporch/Sync Width %u/%u\n" |
| " Vertical Active/Blanking %u/%u\n" |
| " Vertical Frontporch/Sync Width %u/%u\n", |
| t->hact, hbl, t->hfp, t->hsync, vact, vbl, t->vfp, t->vsync); |
| ok = false; |
| } |
| |
| if (t->even_vtotal) |
| vtotal = vact + t->vfp + t->vsync + t->vbp; |
| else if (t->interlaced) |
| vtotal = vact + t->vfp + t->vsync + t->vbp + 0.5; |
| |
| double refresh = (double)t->pixclk_khz * 1000.0 / (htotal * vtotal); |
| |
| std::string s; |
| unsigned rb = t->rb & ~RB_ALT; |
| if (rb) { |
| bool alt = t->rb & RB_ALT; |
| s = "RB"; |
| if (rb == RB_CVT_V2) |
| s += std::string("v2") + (alt ? ",video-optimized" : ""); |
| else if (rb == RB_CVT_V3) |
| s += std::string("v3") + (alt ? ",h-blank-160" : ""); |
| } |
| add_str(s, flags); |
| if (t->hsize_mm || t->vsize_mm) |
| add_str(s, std::to_string(t->hsize_mm) + " mm x " + std::to_string(t->vsize_mm) + " mm"); |
| if (t->hsize_mm > dtd_max_hsize_mm) |
| dtd_max_hsize_mm = t->hsize_mm; |
| if (t->vsize_mm > dtd_max_vsize_mm) |
| dtd_max_vsize_mm = t->vsize_mm; |
| if (!s.empty()) |
| s = " (" + s + ")"; |
| unsigned pixclk_khz = t->pixclk_khz / (t->ycbcr420 ? 2 : 1); |
| |
| char buf[10]; |
| |
| sprintf(buf, "%u%s", t->vact, t->interlaced ? "i" : ""); |
| printf("%s%s: %5ux%-5s %7.3f Hz %3u:%-3u %7.3f kHz %8.3f MHz%s\n", |
| prefix, type, |
| t->hact, buf, |
| refresh, |
| t->hratio, t->vratio, |
| hor_freq_khz, |
| pixclk_khz / 1000.0, |
| s.c_str()); |
| |
| unsigned len = strlen(prefix) + 2; |
| |
| if (!t->ycbcr420 && detailed && options[OptXModeLineTimings]) |
| print_modeline(len, t, refresh); |
| else if (!t->ycbcr420 && detailed && options[OptFBModeTimings]) |
| print_fbmode(len, t, refresh, hor_freq_khz); |
| else if (!t->ycbcr420 && detailed && options[OptV4L2Timings]) |
| print_v4l2_timing(t, refresh, type); |
| else if (detailed) |
| print_detailed_timing(len + strlen(type) + 6, t); |
| |
| if (!do_checks) |
| return ok; |
| |
| if (!memcmp(type, "DTD", 3)) { |
| unsigned vic, dmt; |
| const timings *vic_t = cta_close_match_to_vic(*t, vic); |
| |
| if (vic_t) |
| warn("DTD is similar but not identical to VIC %u.\n", vic); |
| |
| const timings *dmt_t = close_match_to_dmt(*t, dmt); |
| if (!vic_t && dmt_t) |
| warn("DTD is similar but not identical to DMT 0x%02x.\n", dmt); |
| } |
| |
| if (refresh) { |
| min_vert_freq_hz = min(min_vert_freq_hz, refresh); |
| max_vert_freq_hz = max(max_vert_freq_hz, refresh); |
| } |
| if (hor_freq_khz) { |
| min_hor_freq_hz = min(min_hor_freq_hz, hor_freq_khz * 1000.0); |
| max_hor_freq_hz = max(max_hor_freq_hz, hor_freq_khz * 1000.0); |
| max_pixclk_khz = max(max_pixclk_khz, pixclk_khz); |
| if (t->pos_pol_hsync && !t->pos_pol_vsync && t->vsync == 3) |
| base.max_pos_neg_hor_freq_khz = hor_freq_khz; |
| } |
| |
| if (t->ycbcr420 && t->pixclk_khz < 590000) |
| warn_once("Some YCbCr 4:2:0 timings are invalid for HDMI (which requires an RGB timings pixel rate >= 590 MHz).\n"); |
| if (t->hfp <= 0) |
| fail("0 or negative horizontal front porch.\n"); |
| if (t->hbp <= 0) |
| fail("0 or negative horizontal back porch.\n"); |
| if (t->vbp <= 0) |
| fail("0 or negative vertical back porch.\n"); |
| if (!base.max_display_width_mm && !base.max_display_height_mm) { |
| /* this is valid */ |
| } else if (!t->hsize_mm && !t->vsize_mm) { |
| /* this is valid */ |
| } else if (t->hsize_mm > base.max_display_width_mm + 9 || |
| t->vsize_mm > base.max_display_height_mm + 9) { |
| fail("Mismatch of image size %ux%u mm vs display size %ux%u mm.\n", |
| t->hsize_mm, t->vsize_mm, base.max_display_width_mm, base.max_display_height_mm); |
| } else if (t->hsize_mm < base.max_display_width_mm - 9 && |
| t->vsize_mm < base.max_display_height_mm - 9) { |
| fail("Mismatch of image size %ux%u mm vs display size %ux%u mm.\n", |
| t->hsize_mm, t->vsize_mm, base.max_display_width_mm, base.max_display_height_mm); |
| } |
| return ok; |
| } |
| |
| std::string containerid2s(const unsigned char *x) |
| { |
| char buf[40]; |
| |
| sprintf(buf, "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", |
| x[0], x[1], x[2], x[3], |
| x[4], x[5], |
| x[6], x[7], |
| x[8], x[9], |
| x[10], x[11], x[12], x[13], x[14], x[15]); |
| return buf; |
| } |
| |
| std::string utohex(unsigned char x) |
| { |
| char buf[10]; |
| |
| sprintf(buf, "0x%02hhx", x); |
| return buf; |
| } |
| |
| const char *oui_name(unsigned oui, bool reverse) |
| { |
| if (reverse) |
| oui = (oui >> 16) | (oui & 0xff00) | ((oui & 0xff) << 16); |
| |
| switch (oui) { |
| case 0x00001a: return "AMD"; |
| case 0x000c03: return "HDMI"; |
| case 0x00044b: return "NVIDIA"; |
| case 0x000c6e: return "ASUS"; |
| case 0x0010fa: return "Apple"; |
| case 0x0014b9: return "MSTAR"; |
| case 0x00d046: return "Dolby"; |
| case 0x00e047: return "InFocus"; |
| case 0x3a0292: return "VESA"; |
| case 0x90848b: return "HDR10+"; |
| case 0xc45dd8: return "HDMI Forum"; |
| case 0xca125c: return "Microsoft"; |
| default: return NULL; |
| } |
| } |
| |
| std::string ouitohex(unsigned oui) |
| { |
| char buf[32]; |
| |
| sprintf(buf, "%02X-%02X-%02X", (oui >> 16) & 0xff, (oui >> 8) & 0xff, oui & 0xff); |
| return buf; |
| } |
| |
| bool memchk(const unsigned char *x, unsigned len, unsigned char v) |
| { |
| for (unsigned i = 0; i < len; i++) |
| if (x[i] != v) |
| return false; |
| return true; |
| } |
| |
| void hex_block(const char *prefix, const unsigned char *x, |
| unsigned length, bool show_ascii, unsigned step) |
| { |
| unsigned i, j; |
| |
| if (!length) |
| return; |
| |
| for (i = 0; i < length; i += step) { |
| unsigned len = min(step, length - i); |
| |
| printf("%s", prefix); |
| for (j = 0; j < len; j++) |
| printf("%s%02x", j ? " " : "", x[i + j]); |
| |
| if (show_ascii) { |
| for (j = len; j < step; j++) |
| printf(" "); |
| printf(" '"); |
| for (j = 0; j < len; j++) |
| printf("%c", x[i + j] >= ' ' && x[i + j] <= '~' ? x[i + j] : '.'); |
| printf("'"); |
| } |
| printf("\n"); |
| } |
| } |
| |
| static bool edid_add_byte(const char *s, bool two_digits = true) |
| { |
| char buf[3]; |
| |
| if (state.edid_size == sizeof(edid)) |
| return false; |
| buf[0] = s[0]; |
| buf[1] = two_digits ? s[1] : 0; |
| buf[2] = 0; |
| edid[state.edid_size++] = strtoul(buf, NULL, 16); |
| return true; |
| } |
| |
| static bool extract_edid_quantumdata(const char *start) |
| { |
| /* Parse QuantumData 980 EDID files */ |
| do { |
| start = strstr(start, ">"); |
| if (!start) |
| return false; |
| start++; |
| for (unsigned i = 0; start[i] && start[i + 1] && i < 256; i += 2) |
| if (!edid_add_byte(start + i)) |
| return false; |
| start = strstr(start, "<BLOCK"); |
| } while (start); |
| return state.edid_size; |
| } |
| |
| static const char *ignore_chars = ",:;"; |
| |
| static bool extract_edid_hex(const char *s, bool require_two_digits = true) |
| { |
| for (; *s; s++) { |
| if (isspace(*s) || strchr(ignore_chars, *s)) |
| continue; |
| |
| if (*s == '0' && tolower(s[1]) == 'x') { |
| s++; |
| continue; |
| } |
| |
| /* Read one or two hex digits from the log */ |
| if (!isxdigit(s[0])) { |
| if (state.edid_size && state.edid_size % 128 == 0) |
| break; |
| return false; |
| } |
| if (require_two_digits && !isxdigit(s[1])) { |
| odd_hex_digits = true; |
| return false; |
| } |
| if (!edid_add_byte(s, isxdigit(s[1]))) |
| return false; |
| if (isxdigit(s[1])) |
| s++; |
| } |
| return state.edid_size; |
| } |
| |
| static bool extract_edid_xrandr(const char *start) |
| { |
| static const char indentation1[] = " "; |
| static const char indentation2[] = "\t\t"; |
| /* Used to detect that we've gone past the EDID property */ |
| static const char half_indentation1[] = " "; |
| static const char half_indentation2[] = "\t"; |
| const char *indentation; |
| const char *s; |
| |
| for (;;) { |
| unsigned j; |
| |
| /* Get the next start of the line of EDID hex, assuming spaces for indentation */ |
| s = strstr(start, indentation = indentation1); |
| /* Did we skip the start of another property? */ |
| if (s && s > strstr(start, half_indentation1)) |
| break; |
| |
| /* If we failed, retry assuming tabs for indentation */ |
| if (!s) { |
| s = strstr(start, indentation = indentation2); |
| /* Did we skip the start of another property? */ |
| if (s && s > strstr(start, half_indentation2)) |
| break; |
| } |
| |
| if (!s) |
| break; |
| |
| start = s + strlen(indentation); |
| |
| for (j = 0; j < 16; j++, start += 2) { |
| /* Read a %02x from the log */ |
| if (!isxdigit(start[0]) || !isxdigit(start[1])) { |
| if (j) |
| break; |
| return false; |
| } |
| if (!edid_add_byte(start)) |
| return false; |
| } |
| } |
| return state.edid_size; |
| } |
| |
| static bool extract_edid_xorg(const char *start) |
| { |
| bool find_first_num = true; |
| |
| for (; *start; start++) { |
| if (find_first_num) { |
| const char *s; |
| |
| /* skip ahead to the : */ |
| s = strstr(start, ": \t"); |
| if (!s) |
| s = strstr(start, ": "); |
| if (!s) |
| break; |
| start = s; |
| /* and find the first number */ |
| while (!isxdigit(start[1])) |
| start++; |
| find_first_num = false; |
| continue; |
| } else { |
| /* Read a %02x from the log */ |
| if (!isxdigit(*start)) { |
| find_first_num = true; |
| continue; |
| } |
| if (!edid_add_byte(start)) |
| return false; |
| start++; |
| } |
| } |
| return state.edid_size; |
| } |
| |
| static bool extract_edid(int fd, FILE *error) |
| { |
| std::vector<char> edid_data; |
| char buf[EDID_PAGE_SIZE]; |
| |
| for (;;) { |
| ssize_t i = read(fd, buf, sizeof(buf)); |
| |
| if (i < 0) |
| return false; |
| if (i == 0) |
| break; |
| edid_data.insert(edid_data.end(), buf, buf + i); |
| } |
| |
| if (edid_data.empty()) { |
| state.edid_size = 0; |
| return false; |
| } |
| |
| const char *data = &edid_data[0]; |
| const char *start; |
| |
| /* Look for edid-decode output */ |
| start = strstr(data, "EDID (hex):"); |
| if (!start) |
| start = strstr(data, "edid-decode (hex):"); |
| if (start) |
| return extract_edid_hex(strchr(start, ':')); |
| |
| /* Look for C-array */ |
| start = strstr(data, "unsigned char edid[] = {"); |
| if (start) |
| return extract_edid_hex(strchr(start, '{') + 1, false); |
| |
| /* Look for QuantumData EDID output */ |
| start = strstr(data, "<BLOCK"); |
| if (start) |
| return extract_edid_quantumdata(start); |
| |
| /* Look for xrandr --verbose output (lines of 16 hex bytes) */ |
| start = strstr(data, "EDID_DATA:"); |
| if (!start) |
| start = strstr(data, "EDID:"); |
| if (start) |
| return extract_edid_xrandr(start); |
| |
| /* Look for an EDID in an Xorg.0.log file */ |
| start = strstr(data, "EDID (in hex):"); |
| if (start) |
| start = strstr(start, "(II)"); |
| if (start) |
| return extract_edid_xorg(start); |
| |
| unsigned i; |
| |
| /* Is the EDID provided in hex? */ |
| for (i = 0; i < 32 && (isspace(data[i]) || strchr(ignore_chars, data[i]) || |
| tolower(data[i]) == 'x' || isxdigit(data[i])); i++); |
| |
| if (i == 32) |
| return extract_edid_hex(data); |
| |
| /* Assume binary */ |
| if (edid_data.size() > sizeof(edid)) { |
| fprintf(error, "Binary EDID length %zu is greater than %zu.\n", |
| edid_data.size(), sizeof(edid)); |
| return false; |
| } |
| memcpy(edid, data, edid_data.size()); |
| state.edid_size = edid_data.size(); |
| return true; |
| } |
| |
| static unsigned char crc_calc(const unsigned char *b) |
| { |
| unsigned char sum = 0; |
| unsigned i; |
| |
| for (i = 0; i < 127; i++) |
| sum += b[i]; |
| return 256 - sum; |
| } |
| |
| static int crc_ok(const unsigned char *b) |
| { |
| return crc_calc(b) == b[127]; |
| } |
| |
| static void hexdumpedid(FILE *f, const unsigned char *edid, unsigned size) |
| { |
| unsigned b, i, j; |
| |
| for (b = 0; b < size / 128; b++) { |
| const unsigned char *buf = edid + 128 * b; |
| |
| if (b) |
| fprintf(f, "\n"); |
| for (i = 0; i < 128; i += 0x10) { |
| fprintf(f, "%02x", buf[i]); |
| for (j = 1; j < 0x10; j++) { |
| fprintf(f, " %02x", buf[i + j]); |
| } |
| fprintf(f, "\n"); |
| } |
| if (!crc_ok(buf)) |
| fprintf(f, "Block %u has a checksum error (should be 0x%02x).\n", |
| b, crc_calc(buf)); |
| } |
| } |
| |
| static void carraydumpedid(FILE *f, const unsigned char *edid, unsigned size) |
| { |
| unsigned b, i, j; |
| |
| fprintf(f, "const unsigned char edid[] = {\n"); |
| for (b = 0; b < size / 128; b++) { |
| const unsigned char *buf = edid + 128 * b; |
| |
| if (b) |
| fprintf(f, "\n"); |
| for (i = 0; i < 128; i += 8) { |
| fprintf(f, "\t0x%02x,", buf[i]); |
| for (j = 1; j < 8; j++) { |
| fprintf(f, " 0x%02x,", buf[i + j]); |
| } |
| fprintf(f, "\n"); |
| } |
| if (!crc_ok(buf)) |
| fprintf(f, "\t/* Block %u has a checksum error (should be 0x%02x). */\n", |
| b, crc_calc(buf)); |
| } |
| fprintf(f, "};\n"); |
| } |
| |
| // This format can be read by the QuantumData EDID editor |
| static void xmldumpedid(FILE *f, const unsigned char *edid, unsigned size) |
| { |
| fprintf(f, "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n"); |
| fprintf(f, "<DATAOBJ>\n"); |
| fprintf(f, " <HEADER TYPE=\"DID\" VERSION=\"1.0\"/>\n"); |
| fprintf(f, " <DATA>\n"); |
| for (unsigned b = 0; b < size / 128; b++) { |
| const unsigned char *buf = edid + 128 * b; |
| |
| fprintf(f, " <BLOCK%u>", b); |
| for (unsigned i = 0; i < 128; i++) |
| fprintf(f, "%02X", buf[i]); |
| fprintf(f, "</BLOCK%u>\n", b); |
| } |
| fprintf(f, " </DATA>\n"); |
| fprintf(f, "</DATAOBJ>\n"); |
| } |
| |
| |
| static int edid_to_file(const char *to_file, enum output_format out_fmt) |
| { |
| FILE *out; |
| |
| if (!strcmp(to_file, "-")) { |
| to_file = "stdout"; |
| out = stdout; |
| } else if ((out = fopen(to_file, "w")) == NULL) { |
| perror(to_file); |
| return -1; |
| } |
| if (out_fmt == OUT_FMT_DEFAULT) |
| out_fmt = out == stdout ? OUT_FMT_HEX : OUT_FMT_RAW; |
| |
| switch (out_fmt) { |
| default: |
| case OUT_FMT_HEX: |
| hexdumpedid(out, edid, state.edid_size); |
| break; |
| case OUT_FMT_RAW: |
| fwrite(edid, state.edid_size, 1, out); |
| break; |
| case OUT_FMT_CARRAY: |
| carraydumpedid(out, edid, state.edid_size); |
| break; |
| case OUT_FMT_XML: |
| xmldumpedid(out, edid, state.edid_size); |
| break; |
| } |
| |
| if (out != stdout) |
| fclose(out); |
| return 0; |
| } |
| |
| static int edid_from_file(const char *from_file, FILE *error) |
| { |
| #ifdef O_BINARY |
| // Windows compatibility |
| int flags = O_RDONLY | O_BINARY; |
| #else |
| int flags = O_RDONLY; |
| #endif |
| int fd; |
| |
| if (!strcmp(from_file, "-")) { |
| from_file = "stdin"; |
| fd = 0; |
| } else if ((fd = open(from_file, flags)) == -1) { |
| perror(from_file); |
| return -1; |
| } |
| |
| odd_hex_digits = false; |
| if (!extract_edid(fd, error)) { |
| if (!state.edid_size) { |
| fprintf(error, "EDID of '%s' was empty.\n", from_file); |
| return -1; |
| } |
| fprintf(error, "EDID extract of '%s' failed: ", from_file); |
| if (odd_hex_digits) |
| fprintf(error, "odd number of hexadecimal digits.\n"); |
| else |
| fprintf(error, "unknown format.\n"); |
| return -1; |
| } |
| if (state.edid_size % EDID_PAGE_SIZE) { |
| fprintf(error, "EDID length %u is not a multiple of %u.\n", |
| state.edid_size, EDID_PAGE_SIZE); |
| return -1; |
| } |
| state.num_blocks = state.edid_size / EDID_PAGE_SIZE; |
| if (fd != 0) |
| close(fd); |
| |
| if (memcmp(edid, "\x00\xFF\xFF\xFF\xFF\xFF\xFF\x00", 8)) { |
| fprintf(error, "No EDID header found in '%s'.\n", from_file); |
| return -1; |
| } |
| return 0; |
| } |
| |
| /* generic extension code */ |
| |
| std::string block_name(unsigned char block) |
| { |
| char buf[10]; |
| |
| switch (block) { |
| case 0x00: return "Base EDID"; |
| case 0x02: return "CTA-861 Extension Block"; |
| case 0x10: return "Video Timing Extension Block"; |
| case 0x20: return "EDID 2.0 Extension Block"; |
| case 0x40: return "Display Information Extension Block"; |
| case 0x50: return "Localized String Extension Block"; |
| case 0x60: return "Microdisplay Interface Extension Block"; |
| case 0x70: return "DisplayID Extension Block"; |
| case 0xf0: return "Block Map Extension Block"; |
| case 0xff: return "Manufacturer-Specific Extension Block"; |
| default: |
| sprintf(buf, " 0x%02x", block); |
| return std::string("Unknown EDID Extension Block") + buf; |
| } |
| } |
| |
| void edid_state::parse_block_map(const unsigned char *x) |
| { |
| unsigned last_valid_block_tag = 0; |
| bool fail_once = false; |
| unsigned offset = 1; |
| unsigned i; |
| |
| if (block_nr == 1) |
| block_map.saw_block_1 = true; |
| else if (!block_map.saw_block_1) |
| fail("No EDID Block Map Extension found in block 1.\n"); |
| else if (block_nr == 128) |
| block_map.saw_block_128 = true; |
| |
| if (block_nr > 1) |
| offset = 128; |
| |
| for (i = 1; i < 127; i++) { |
| unsigned block = offset + i; |
| |
| if (x[i]) { |
| last_valid_block_tag++; |
| if (i != last_valid_block_tag && !fail_once) { |
| fail("Valid block tags are not consecutive.\n"); |
| fail_once = true; |
| } |
| printf(" Block %3u: %s\n", block, block_name(x[i]).c_str()); |
| if (block >= num_blocks) { |
| if (!fail_once) |
| fail("Invalid block number %u.\n", block); |
| fail_once = true; |
| } else if (x[i] != edid[block * EDID_PAGE_SIZE]) { |
| fail("Block %u tag mismatch: expected 0x%02x, but got 0x%02x.\n", |
| block, edid[block * EDID_PAGE_SIZE], x[i]); |
| } |
| } else if (block < num_blocks) { |
| fail("Block %u tag mismatch: expected 0x%02x, but got 0x00.\n", |
| block, edid[block * EDID_PAGE_SIZE]); |
| } |
| } |
| } |
| |
| void edid_state::preparse_extension(const unsigned char *x) |
| { |
| switch (x[0]) { |
| case 0x02: |
| has_cta = true; |
| preparse_cta_block(x); |
| break; |
| case 0x70: |
| has_dispid = true; |
| preparse_displayid_block(x); |
| break; |
| } |
| } |
| |
| void edid_state::parse_extension(const unsigned char *x) |
| { |
| block = block_name(x[0]); |
| data_block.clear(); |
| |
| printf("\n"); |
| if (block_nr && x[0] == 0) |
| block = "Unknown EDID Extension Block 0x00"; |
| printf("Block %u, %s:\n", block_nr, block.c_str()); |
| |
| switch (x[0]) { |
| case 0x02: |
| parse_cta_block(x); |
| break; |
| case 0x10: |
| parse_vtb_ext_block(x); |
| break; |
| case 0x20: |
| fail("Deprecated extension block for EDID 2.0, do not use.\n"); |
| break; |
| case 0x40: |
| parse_di_ext_block(x); |
| break; |
| case 0x50: |
| parse_ls_ext_block(x); |
| break; |
| case 0x70: |
| parse_displayid_block(x); |
| break; |
| case 0xf0: |
| parse_block_map(x); |
| if (block_nr != 1 && block_nr != 128) |
| fail("Must be used in block 1 and 128.\n"); |
| break; |
| default: |
| hex_block(" ", x, EDID_PAGE_SIZE); |
| fail("Unknown Extension Block.\n"); |
| break; |
| } |
| |
| data_block.clear(); |
| do_checksum("", x, EDID_PAGE_SIZE); |
| } |
| |
| int edid_state::parse_edid() |
| { |
| hide_serial_numbers = options[OptHideSerialNumbers]; |
| |
| for (unsigned i = 1; i < num_blocks; i++) |
| preparse_extension(edid + i * EDID_PAGE_SIZE); |
| |
| if (options[OptPhysicalAddress]) { |
| printf("%x.%x.%x.%x\n", |
| (cta.preparsed_phys_addr >> 12) & 0xf, |
| (cta.preparsed_phys_addr >> 8) & 0xf, |
| (cta.preparsed_phys_addr >> 4) & 0xf, |
| cta.preparsed_phys_addr & 0xf); |
| return 0; |
| } |
| |
| if (!options[OptSkipHexDump]) { |
| printf("edid-decode (hex):\n\n"); |
| for (unsigned i = 0; i < num_blocks; i++) { |
| hex_block("", edid + i * EDID_PAGE_SIZE, EDID_PAGE_SIZE, false); |
| if (i == num_blocks - 1 && options[OptOnlyHexDump]) |
| return 0; |
| printf("\n"); |
| } |
| printf("----------------\n\n"); |
| } |
| |
| block = block_name(0x00); |
| printf("Block %u, %s:\n", block_nr, block.c_str()); |
| parse_base_block(edid); |
| |
| for (unsigned i = 1; i < num_blocks; i++) { |
| block_nr++; |
| printf("\n----------------\n"); |
| parse_extension(edid + i * EDID_PAGE_SIZE); |
| } |
| |
| block = ""; |
| block_nr = EDID_MAX_BLOCKS; |
| |
| if (has_cta) |
| cta_resolve_svrs(); |
| |
| if (options[OptPreferredTimings] && base.preferred_timing.is_valid()) { |
| printf("\n----------------\n"); |
| printf("\nPreferred Video Timing if only Block 0 is parsed:\n"); |
| print_timings(" ", base.preferred_timing, true, false); |
| } |
| |
| if (options[OptNativeTimings] && |
| base.preferred_timing.is_valid() && base.preferred_is_also_native) { |
| printf("\n----------------\n"); |
| printf("\nNative Video Timing if only Block 0 is parsed:\n"); |
| print_timings(" ", base.preferred_timing, true, false); |
| } |
| |
| if (options[OptPreferredTimings] && !cta.preferred_timings.empty()) { |
| printf("\n----------------\n"); |
| printf("\nPreferred Video Timing%s if Block 0 and CTA-861 Blocks are parsed:\n", |
| cta.preferred_timings.size() > 1 ? "s" : ""); |
| for (vec_timings_ext::iterator iter = cta.preferred_timings.begin(); |
| iter != cta.preferred_timings.end(); ++iter) |
| print_timings(" ", *iter, true, false); |
| } |
| |
| if (options[OptNativeTimings] && !cta.native_timings.empty()) { |
| printf("\n----------------\n"); |
| printf("\nNative Video Timing%s if Block 0 and CTA-861 Blocks are parsed:\n", |
| cta.native_timings.size() > 1 ? "s" : ""); |
| for (vec_timings_ext::iterator iter = cta.native_timings.begin(); |
| iter != cta.native_timings.end(); ++iter) |
| print_timings(" ", *iter, true, false); |
| } |
| |
| if (options[OptPreferredTimings] && !dispid.preferred_timings.empty()) { |
| printf("\n----------------\n"); |
| printf("\nPreferred Video Timing%s if Block 0 and DisplayID Blocks are parsed:\n", |
| dispid.preferred_timings.size() > 1 ? "s" : ""); |
| for (vec_timings_ext::iterator iter = dispid.preferred_timings.begin(); |
| iter != dispid.preferred_timings.end(); ++iter) |
| print_timings(" ", *iter, true, false); |
| } |
| |
| if (!options[OptCheck] && !options[OptCheckInline]) |
| return 0; |
| |
| check_base_block(); |
| if (has_cta) |
| check_cta_blocks(); |
| if (has_dispid) |
| check_displayid_blocks(); |
| |
| printf("\n----------------\n"); |
| |
| if (!options[OptSkipSHA] && strlen(STRING(SHA))) { |
| printf("\nedid-decode SHA: %s %s\n", STRING(SHA), STRING(DATE)); |
| } |
| |
| if (options[OptCheck]) { |
| if (warnings) |
| show_msgs(true); |
| if (failures) |
| show_msgs(false); |
| } |
| printf("\nEDID conformity: %s\n", failures ? "FAIL" : "PASS"); |
| return failures ? -2 : 0; |
| } |
| |
| enum cvt_opts { |
| CVT_WIDTH = 0, |
| CVT_HEIGHT, |
| CVT_FPS, |
| CVT_INTERLACED, |
| CVT_OVERSCAN, |
| CVT_RB, |
| CVT_ALT, |
| CVT_RB_H_BLANK, |
| }; |
| |
| static int parse_cvt_subopt(char **subopt_str, double *value) |
| { |
| int opt; |
| char *opt_str; |
| |
| static const char * const subopt_list[] = { |
| "w", |
| "h", |
| "fps", |
| "interlaced", |
| "overscan", |
| "rb", |
| "alt", |
| "hblank", |
| nullptr |
| }; |
| |
| opt = getsubopt(subopt_str, (char* const*) subopt_list, &opt_str); |
| |
| if (opt == -1) { |
| fprintf(stderr, "Invalid suboptions specified.\n"); |
| usage(); |
| std::exit(EXIT_FAILURE); |
| } |
| if (opt_str == nullptr && opt != CVT_INTERLACED && opt != CVT_ALT && |
| opt != CVT_OVERSCAN) { |
| fprintf(stderr, "No value given to suboption <%s>.\n", |
| subopt_list[opt]); |
| usage(); |
| std::exit(EXIT_FAILURE); |
| } |
| |
| if (opt_str) |
| *value = strtod(opt_str, nullptr); |
| return opt; |
| } |
| |
| static void parse_cvt(char *optarg) |
| { |
| unsigned w = 0, h = 0; |
| double fps = 0; |
| unsigned rb = RB_NONE; |
| unsigned rb_h_blank = 0; |
| bool interlaced = false; |
| bool alt = false; |
| bool overscan = false; |
| |
| while (*optarg != '\0') { |
| int opt; |
| double opt_val; |
| |
| opt = parse_cvt_subopt(&optarg, &opt_val); |
| |
| switch (opt) { |
| case CVT_WIDTH: |
| w = round(opt_val); |
| break; |
| case CVT_HEIGHT: |
| h = round(opt_val); |
| break; |
| case CVT_FPS: |
| fps = opt_val; |
| break; |
| case CVT_RB: |
| rb = opt_val; |
| break; |
| case CVT_OVERSCAN: |
| overscan = true; |
| break; |
| case CVT_INTERLACED: |
| interlaced = opt_val; |
| break; |
| case CVT_ALT: |
| alt = opt_val; |
| break; |
| case CVT_RB_H_BLANK: |
| rb_h_blank = opt_val; |
| break; |
| default: |
| break; |
| } |
| } |
| |
| if (!w || !h || !fps) { |
| fprintf(stderr, "Missing width, height and/or fps.\n"); |
| usage(); |
| std::exit(EXIT_FAILURE); |
| } |
| if (interlaced) |
| fps /= 2; |
| timings t = state.calc_cvt_mode(w, h, fps, rb, interlaced, overscan, alt, rb_h_blank); |
| state.print_timings("", &t, "CVT", "", true, false); |
| } |
| |
| struct gtf_parsed_data { |
| unsigned w, h; |
| double freq; |
| double C, M, K, J; |
| bool overscan; |
| bool interlaced; |
| bool secondary; |
| bool params_from_edid; |
| enum gtf_ip_parm ip_parm; |
| }; |
| |
| enum gtf_opts { |
| GTF_WIDTH = 0, |
| GTF_HEIGHT, |
| GTF_FPS, |
| GTF_HORFREQ, |
| GTF_PIXCLK, |
| GTF_INTERLACED, |
| GTF_OVERSCAN, |
| GTF_SECONDARY, |
| GTF_C2, |
| GTF_M, |
| GTF_K, |
| GTF_J2, |
| }; |
| |
| static int parse_gtf_subopt(char **subopt_str, double *value) |
| { |
| int opt; |
| char *opt_str; |
| |
| static const char * const subopt_list[] = { |
| "w", |
| "h", |
| "fps", |
| "horfreq", |
| "pixclk", |
| "interlaced", |
| "overscan", |
| "secondary", |
| "C", |
| "M", |
| "K", |
| "J", |
| nullptr |
| }; |
| |
| opt = getsubopt(subopt_str, (char * const *)subopt_list, &opt_str); |
| |
| if (opt == -1) { |
| fprintf(stderr, "Invalid suboptions specified.\n"); |
| usage(); |
| std::exit(EXIT_FAILURE); |
| } |
| if (opt_str == nullptr && opt != GTF_INTERLACED && opt != GTF_OVERSCAN && |
| opt != GTF_SECONDARY) { |
| fprintf(stderr, "No value given to suboption <%s>.\n", |
| subopt_list[opt]); |
| usage(); |
| std::exit(EXIT_FAILURE); |
| } |
| |
| if (opt == GTF_C2 || opt == GTF_J2) |
| *value = round(2.0 * strtod(opt_str, nullptr)); |
| else if (opt_str) |
| *value = strtod(opt_str, nullptr); |
| return opt; |
| } |
| |
| static void parse_gtf(char *optarg, gtf_parsed_data &data) |
| { |
| memset(&data, 0, sizeof(data)); |
| data.params_from_edid = true; |
| data.C = 40; |
| data.M = 600; |
| data.K = 128; |
| data.J = 20; |
| |
| while (*optarg != '\0') { |
| int opt; |
| double opt_val; |
| |
| opt = parse_gtf_subopt(&optarg, &opt_val); |
| |
| switch (opt) { |
| case GTF_WIDTH: |
| data.w = round(opt_val); |
| break; |
| case GTF_HEIGHT: |
| data.h = round(opt_val); |
| break; |
| case GTF_FPS: |
| data.freq = opt_val; |
| data.ip_parm = gtf_ip_vert_freq; |
| break; |
| case GTF_HORFREQ: |
| data.freq = opt_val; |
| data.ip_parm = gtf_ip_hor_freq; |
| break; |
| case GTF_PIXCLK: |
| data.freq = opt_val; |
| data.ip_parm = gtf_ip_clk_freq; |
| break; |
| case GTF_INTERLACED: |
| data.interlaced = true; |
| break; |
| case GTF_OVERSCAN: |
| data.overscan = true; |
| break; |
| case GTF_SECONDARY: |
| data.secondary = true; |
| break; |
| case GTF_C2: |
| data.C = opt_val / 2.0; |
| data.params_from_edid = false; |
| break; |
| case GTF_M: |
| data.M = round(opt_val); |
| data.params_from_edid = false; |
| break; |
| case GTF_K: |
| data.K = round(opt_val); |
| data.params_from_edid = false; |
| break; |
| case GTF_J2: |
| data.J = opt_val / 2.0; |
| data.params_from_edid = false; |
| break; |
| default: |
| break; |
| } |
| } |
| |
| if (!data.w || !data.h) { |
| fprintf(stderr, "Missing width and/or height.\n"); |
| usage(); |
| std::exit(EXIT_FAILURE); |
| } |
| if (!data.freq) { |
| fprintf(stderr, "One of fps, horfreq or pixclk must be given.\n"); |
| usage(); |
| std::exit(EXIT_FAILURE); |
| } |
| if (!data.secondary) |
| data.params_from_edid = false; |
| if (data.interlaced && data.ip_parm == gtf_ip_vert_freq) |
| data.freq /= 2; |
| } |
| |
| static void show_gtf(gtf_parsed_data &data) |
| { |
| timings t; |
| |
| t = state.calc_gtf_mode(data.w, data.h, data.freq, data.interlaced, |
| data.ip_parm, data.overscan, data.secondary, |
| data.C, data.M, data.K, data.J); |
| calc_ratio(&t); |
| state.print_timings("", &t, "GTF", "", true, false); |
| } |
| |
| int main(int argc, char **argv) |
| { |
| char short_options[26 * 2 * 2 + 1]; |
| enum output_format out_fmt = OUT_FMT_DEFAULT; |
| gtf_parsed_data gtf_data; |
| int ret; |
| |
| while (1) { |
| int option_index = 0; |
| unsigned idx = 0; |
| unsigned i, val; |
| const timings *t; |
| char buf[16]; |
| |
| for (i = 0; long_options[i].name; i++) { |
| if (!isalpha(long_options[i].val)) |
| continue; |
| short_options[idx++] = long_options[i].val; |
| if (long_options[i].has_arg == required_argument) |
| short_options[idx++] = ':'; |
| } |
| short_options[idx] = 0; |
| int ch = getopt_long(argc, argv, short_options, |
| long_options, &option_index); |
| if (ch == -1) |
| break; |
| |
| options[ch] = 1; |
| switch (ch) { |
| case OptHelp: |
| usage(); |
| return -1; |
| case OptOutputFormat: |
| if (!strcmp(optarg, "hex")) { |
| out_fmt = OUT_FMT_HEX; |
| } else if (!strcmp(optarg, "raw")) { |
| out_fmt = OUT_FMT_RAW; |
| } else if (!strcmp(optarg, "carray")) { |
| out_fmt = OUT_FMT_CARRAY; |
| } else if (!strcmp(optarg, "xml")) { |
| out_fmt = OUT_FMT_XML; |
| } else { |
| usage(); |
| exit(1); |
| } |
| break; |
| case OptSTD: { |
| unsigned char byte1, byte2 = 0; |
| char *endptr; |
| |
| byte1 = strtoul(optarg, &endptr, 0); |
| if (*endptr == ',') |
| byte2 = strtoul(endptr + 1, NULL, 0); |
| state.print_standard_timing("", byte1, byte2, false, true); |
| break; |
| } |
| case OptDMT: |
| val = strtoul(optarg, NULL, 0); |
| t = find_dmt_id(val); |
| if (t) { |
| sprintf(buf, "DMT 0x%02x", val); |
| state.print_timings("", t, buf, "", true, false); |
| } else { |
| fprintf(stderr, "Unknown DMT code 0x%02x.\n", val); |
| } |
| break; |
| case OptVIC: |
| val = strtoul(optarg, NULL, 0); |
| t = find_vic_id(val); |
| if (t) { |
| sprintf(buf, "VIC %3u", val); |
| state.print_timings("", t, buf, "", true, false); |
| } else { |
| fprintf(stderr, "Unknown VIC code %u.\n", val); |
| } |
| break; |
| case OptHDMIVIC: |
| val = strtoul(optarg, NULL, 0); |
| t = find_hdmi_vic_id(val); |
| if (t) { |
| sprintf(buf, "HDMI VIC %u", val); |
| state.print_timings("", t, buf, "", true, false); |
| } else { |
| fprintf(stderr, "Unknown HDMI VIC code %u.\n", val); |
| } |
| break; |
| case OptCVT: |
| parse_cvt(optarg); |
| break; |
| case OptGTF: |
| parse_gtf(optarg, gtf_data); |
| break; |
| case ':': |
| fprintf(stderr, "Option '%s' requires a value.\n", |
| argv[optind]); |
| usage(); |
| return -1; |
| case '?': |
| fprintf(stderr, "Unknown argument '%s'.\n", |
| argv[optind]); |
| usage(); |
| return -1; |
| } |
| } |
| if (optind == argc && options[OptVersion]) { |
| if (strlen(STRING(SHA))) |
| printf("edid-decode SHA: %s %s\n", STRING(SHA), STRING(DATE)); |
| else |
| printf("edid-decode SHA: not available\n"); |
| return 0; |
| } |
| |
| if (options[OptListEstTimings]) |
| state.list_established_timings(); |
| if (options[OptListDMTs]) |
| state.list_dmts(); |
| if (options[OptListVICs]) |
| state.cta_list_vics(); |
| if (options[OptListHDMIVICs]) |
| state.cta_list_hdmi_vics(); |
| |
| if (options[OptListEstTimings] || options[OptListDMTs] || |
| options[OptListVICs] || options[OptListHDMIVICs]) |
| return 0; |
| |
| if (options[OptCVT] || options[OptDMT] || options[OptVIC] || |
| options[OptHDMIVIC] || options[OptSTD]) |
| return 0; |
| |
| if (options[OptGTF] && (!gtf_data.params_from_edid || optind == argc)) { |
| show_gtf(gtf_data); |
| return 0; |
| } |
| |
| if (optind == argc) |
| ret = edid_from_file("-", stdout); |
| else |
| ret = edid_from_file(argv[optind], argv[optind + 1] ? stderr : stdout); |
| |
| if (ret && options[OptPhysicalAddress]) { |
| printf("f.f.f.f\n"); |
| return 0; |
| } |
| if (optind < argc - 1) |
| return ret ? ret : edid_to_file(argv[optind + 1], out_fmt); |
| |
| if (options[OptGTF]) { |
| timings t; |
| |
| // Find the Secondary Curve |
| state.preparse_detailed_block(edid + 0x36); |
| state.preparse_detailed_block(edid + 0x48); |
| state.preparse_detailed_block(edid + 0x5a); |
| state.preparse_detailed_block(edid + 0x6c); |
| |
| t = state.calc_gtf_mode(gtf_data.w, gtf_data.h, gtf_data.freq, |
| gtf_data.interlaced, gtf_data.ip_parm, |
| gtf_data.overscan); |
| unsigned hbl = t.hfp + t.hsync + t.hbp; |
| unsigned htotal = t.hact + hbl; |
| double hor_freq_khz = htotal ? (double)t.pixclk_khz / htotal : 0; |
| |
| if (state.base.supports_sec_gtf && |
| hor_freq_khz >= state.base.sec_gtf_start_freq) { |
| t = state.calc_gtf_mode(gtf_data.w, gtf_data.h, gtf_data.freq, |
| gtf_data.interlaced, gtf_data.ip_parm, |
| gtf_data.overscan, true, |
| state.base.C, state.base.M, |
| state.base.K, state.base.J); |
| } |
| calc_ratio(&t); |
| if (t.hfp <= 0) |
| state.print_timings("", &t, "GTF", "INVALID: Hfront <= 0", true, false); |
| else |
| state.print_timings("", &t, "GTF", "", true, false); |
| return 0; |
| } |
| |
| return ret ? ret : state.parse_edid(); |
| } |
| |
| #ifdef __EMSCRIPTEN__ |
| /* |
| * The surrounding JavaScript implementation will call this function |
| * each time it wants to decode an EDID. So this should reset all the |
| * state and start over. |
| */ |
| extern "C" int parse_edid(const char *input) |
| { |
| for (unsigned i = 0; i < EDID_MAX_BLOCKS + 1; i++) { |
| s_msgs[i][0].clear(); |
| s_msgs[i][1].clear(); |
| } |
| options[OptCheck] = 1; |
| options[OptPreferredTimings] = 1; |
| options[OptNativeTimings] = 1; |
| state = edid_state(); |
| int ret = edid_from_file(input, stderr); |
| return ret ? ret : state.parse_edid(); |
| } |
| #endif |