In the previous installment we covered initializing NVAPI and obtaining GPU handles along with display Output IDs. This entry is the most technically dense in the series. We’ll dissect the EDID byte array retrieved from an NVIDIA GPU — byte by byte — examining exactly what structure it encodes and how our C# parser interprets it.
EDID (Extended Display Identification Data) is binary data that a monitor sends to the GPU to identify itself and declare what it supports. First standardized by VESA in 1994, the spec has evolved through EDID 1.4 and consists of a 128-byte base block plus optional 128-byte extension blocks.
When you plug a monitor into a computer, the GPU reads the EDID over the I2C bus (the DDC channel). The OS and driver use this data to determine supported resolutions, refresh rates, color spaces, HDR capabilities, and more. NVAPI exposes this at the C# level through the NvAPI_GPU_GetEDID function.
NvEdidV3 maps directly to NVAPI’s NV_EDID_V3 struct. The key constraint is that at most 256 bytes can be retrieved per call. Monitors with multiple CTA-861 extension blocks — 4K monitors being the typical case — can have an EDID exceeding 256 bytes (128×2), so we increment the Offset field and call in a loop.
NvEdidV3’s version field is initialized in a peculiar way:
The version number (3) goes in the upper 16 bits, and the struct size goes in the lower 16 bits via OR. This is a common NVAPI pattern that allows the driver to perform compatibility checks.
The last byte of the 128-byte block is a checksum. The EDID spec requires that the sum of all 128 bytes in the base block be divisible by 256.
1
2
3
4
5
6
7
publicstaticbool ValidateChecksum(byte[] data,int offset =0,int length =128){byte sum =0;for(int i = offset; i < offset + length; i++) sum += data[i];return sum ==0;// byte overflow 덕분에 자동으로 mod 256}
Since byte wraps at 256, the overflow acts as mod 256 automatically. When computing a new checksum byte, take the two’s complement of the sum of the other 127 bytes:
1
2
3
4
5
6
7
publicstaticbyte CalculateChecksum(byte[] data,int offset =0,int length =128){byte sum =0;for(int i = offset; i < offset + length -1; i++)// 마지막 바이트 제외 sum += data[i];return(byte)(256- sum);// 2의 보수}
This is the most interesting bit manipulation in EDID. It compresses a 3-character ASCII alphabetic string into a 2-byte (16-bit) integer.
Subtracting 'A' (65) from each character and adding 1 maps A=1, B=2, …, Z=26, fitting each into 5 bits. The three characters are packed into 15 bits with the most-significant bit always zero.
1
2
3
4
5
6
7
8
publicstaticstring DecodeManufacturerId(byte b1,byte b2){int id =(b1 <<8)| b2;char c1 =(char)(((id >>10)&0x1F)+'A'-1);// 비트 14:10char c2 =(char)(((id >>5)&0x1F)+'A'-1);// 비트 9:5char c3 =(char)((id &0x1F)+'A'-1);// 비트 4:0return$"{c1}{c2}{c3}";}
// Product code (bytes 10-11, little-endian)block.ProductCode =(ushort)(data[10]|(data[11]<<8));// Serial number (bytes 12-15, little-endian)block.SerialNumber =(uint)(data[12]|(data[13]<<8)|(data[14]<<16)|(data[15]<<24));
Following x86 convention, these are stored little-endian with the LSB at the lower address. A serial number of 0x00000000 or 0x01010101 signals that no serial is encoded here — the monitor serial string lives in the descriptor area (0xFC–0x7D) instead.
block.ManufactureWeek = data[16];// 1~53주block.ManufactureYear = data[17]+1990;// 오프셋 1990년
The year uses an offset encoding where 0 represents 1990. So data[17] = 34 means 2024. Some monitors set the week byte to 0xFF to indicate that only the year is specified.
Ten bytes pack the CIE 1931 chromaticity coordinates (Rx, Ry, Gx, Gy, Bx, By, Wx, Wy). Each coordinate has 10-bit precision. The layout is non-trivial: the two LSBs of each coordinate are packed together in the first two bytes, while the upper 8 bits of each follow in subsequent bytes.
Among the four 18-byte slots, the first typically holds a Detailed Timing Descriptor (DTD) representing the monitor’s native resolution. The remaining slots may be additional DTDs or special display descriptors.
Total horizontal pixels (HActive + HBlanking) multiplied by total vertical lines (VActive + VBlanking) gives the total pixels per frame. Dividing the pixel clock by this product yields frames per second — the refresh rate.
Slots with a pixel clock of zero are identified by the tag byte at data[offset + 3]:
1
2
3
4
5
6
7
8
9
10
11
12
publicenum DescriptorTag :byte{ MonitorSerialNumber =0xFF,// 모니터 시리얼 문자열 (최대 13자) DataString =0xFE,// 임의 ASCII 문자열 MonitorRangeLimits =0xFD,// GTF/CVT 주사율 범위 제한 MonitorName =0xFC,// 모니터 모델명 (최대 13자) ColorPoint =0xFB,// 추가 색도 좌표 StandardTimingId =0xFA,// 추가 표준 타이밍 8개 CVTTimingCodes =0xF8,// CVT 3바이트 타이밍 코드 EstablishedTimingsIII =0xF7,// 확립된 타이밍 3 (추가) Dummy =0x10,// 패딩}
The MonitorRangeLimits (0xFD) descriptor records the monitor’s supported vertical scan rate range (MinVRate–MaxVRate Hz), horizontal scan rate range (MinHRate–MaxHRate kHz), and maximum pixel clock. For variable refresh rate (VRR) monitors this range is especially significant.
HDMI and DisplayPort monitors almost universally have byte 0x7E (the extension count) set to 1 or more, signaling that a CTA-861 extension block follows. This second 128-byte block begins at byte[128].
var block =new CtaExtensionBlock
{ Tag = data[blockOffset],// 0x02 = CTA-861 Revision = data[blockOffset +1],// 보통 0x03 DTDOffset = data[blockOffset +2],// 데이터 블록 끝 오프셋 Flags = data[blockOffset +3]// 기능 플래그};
Luminance values are decoded with the formula 100 × 2^(MaxLuminance / 32) cd/m². Typical HDR monitors land at 600–1000 nits peak and 0.01–0.05 nits minimum.
publicstaticbyte[] LoadFromFile(string path){string ext = Path.GetExtension(path).ToLower();// 바이너리 파일: 원시 바이트if(ext ==".bin")return File.ReadAllBytes(path);string text = File.ReadAllText(path, Encoding.ASCII);// 형식 1: <D00FFFFFF...> — 꺾쇠 괄호 HEXif(text.Contains("<D"))return ParseAngleBracketFormat(text);// 형식 2: EDID BYTES: 테이블if(text.Contains("EDID BYTES:"))return ParseTableFormat(text);// 형식 3: 순수 HEX 문자열string cleaned = Regex.Replace(text,@"[^0-9A-Fa-f]","");if(cleaned.Length >=256)return HexStringToBytes(cleaned);// 폴백: 바이너리 헤더 확인var raw = File.ReadAllBytes(path);if(raw.Length >=128&& raw[0]==0x00&& raw[1]==0xFF)return raw;returnnull;}
Three text formats are supported because different EDID tools in the wild each use their own convention. The <D...> format comes from certain monitor configuration utilities; the table format is a human-readable hex dump.
Reading the first row: 4C 2D is manufacturer ID SAM (Samsung); E8 08 in little-endian is product code 0x08E8 = 2280; 0x3C 0x22 (60, 34) is the 60×34 cm screen size; and 0x78 (120) is gamma 2.20.
Checksum: sum of all 128 bytes mod 256 must equal zero
Manufacturer ID: three ASCII letters packed as 5 bits each into 2 bytes
DTD vs. descriptor: bytes[0,1] non-zero means timing data; zero means tag-based display descriptor
CTA-861: each data block opens with a 3-bit tag + 5-bit length header
Extended tags: HDR, colorimetry, and HDMI Forum VSDB are subtypes within Extended (tag 7) blocks
NVAPI reading: iterate in 256-byte chunks by incrementing the Offset field to collect the full EDID
In the next installment we’ll look at displaying and editing the parsed EDID data in a WinForms UI, and then injecting the modified EDID back into the GPU.
NVIDIA GPU Controller Dev Log 2026 -
This article is part of a series.