# DisplayID Extension Handler
DisplayID (tag `0x70`) is a VESA standard for display identification that carries richer
timing and capability data than the base EDID block. It is most common on DisplayPort
Alt Mode devices, docks, and professional monitors.
## Multi-block sections
Unlike CEA-861, where each 128-byte extension block is self-contained, a single logical
DisplayID section may span several consecutive 128-byte blocks all tagged `0x70`. The
dispatch layer collects all `0x70` blocks in stream order and passes them to
`DisplayIdHandler` as a slice — the handler owns reassembly. No other part of the pipeline
needs to know about DisplayID's multi-block structure.
In `alloc`/`std` builds, full multi-block reassembly is supported. In bare `no_std` builds
(no allocator), extension blocks cannot be stored after parsing, so the static pipeline
receives only base-block data; DisplayID content is unavailable at that tier.
## Dynamic pipeline
`DisplayIdHandler` is registered automatically by `ExtensionLibrary::with_standard_handlers()`.
After calling `capabilities_from_edid`, retrieve the parsed DisplayID section via
`get_extension_data`:
```rust
use piaf::{DisplayIdCapabilities, capabilities_from_edid, parse_edid};
let library = ExtensionLibrary::with_standard_handlers();
let parsed = parse_edid(&bytes, &library)?;
let caps = capabilities_from_edid(&parsed, &library);
if let Some(did) = caps.get_extension_data::<DisplayIdCapabilities>(0x70) {
println!("DisplayID version: 0x{:02X}", did.version);
println!("Product type: {}", did.product_type);
}
```
Video modes decoded from DisplayID timing blocks are added to `caps.supported_modes`
alongside modes from the base block and CEA-861.
## Static pipeline
`DisplayIdHandler` is included in `STANDARD_HANDLERS`. It decodes video modes from all
timing blocks (Types I–VI, VESA bitmap, CTA bitmap) and pushes them into the static output:
```rust
use piaf::{STANDARD_HANDLERS, StaticDisplayCapabilities, capabilities_from_edid_static, parse_edid};
let parsed = parse_edid(&bytes, STANDARD_HANDLERS)?;
let caps: StaticDisplayCapabilities<64> = capabilities_from_edid_static(&parsed, STANDARD_HANDLERS);
for mode in caps.iter_modes() {
println!("{}×{}@{}Hz", mode.width, mode.height, mode.refresh_rate);
}
```
`DisplayIdCapabilities` is not available from the static pipeline — rich metadata requires
the dynamic pipeline.
## `DisplayIdCapabilities`
Stored under tag `0x70` in `DisplayCapabilities::extension_data`:
| `version` | `u8` | Version byte from the section header (0x10–0x1F = v1.x, 0x20 = v2.x) |
| `product_type` | `u8` | Display product primary use case, bits 2:0 of header byte 3 |
## Extracted identification data
**Product Identification Block** (tag `0x00`) is decoded into the following
`DisplayCapabilities` fields (dynamic pipeline only):
| `manufacturer` | Manufacturer PNP ID, 2-byte packed encoding (same as EDID base block) |
| `product_code` | LE uint16 at payload bytes 2–3 |
| `serial_number` | LE uint32 at payload bytes 4–7; `0` is treated as unspecified |
| `manufacture_date` | Week/year bytes 8–9 (`year = byte + 1990`; week `0xFF` = model year) |
| `display_name` | ASCII product name starting at byte 10 (up to 13 bytes stored) |
**Display Parameters Block** (tag `0x01`) is decoded into the following
`DisplayCapabilities` fields (dynamic pipeline only):
| `preferred_image_size_mm` | LE uint16 pairs at payload bytes 0–3; `0` on either axis = not defined |
| `color_bit_depth` | Payload byte 5, bits 4:0; same `001=6bpc … 110=16bpc` encoding as EDID `0x14` |
**Color Characteristics Block** (tag `0x02`) is decoded into the following
`DisplayCapabilities` field (dynamic pipeline only):
| `chromaticity` | 8 × LE uint16 at payload bytes 0–15: Red x/y, Green x/y, Blue x/y, White x/y; 1/1024 scale, lower 10 bits significant |
Payloads shorter than 16 bytes are silently ignored.
**Video Timing Range Limits Block** (tag `0x09`) is decoded into the following
`DisplayCapabilities` fields (dynamic pipeline only):
| `max_pixel_clock_mhz` | Payload bytes 3–5 (LE 24-bit, 10 kHz units ÷ 100) |
| `min_h_rate_khz` | Payload byte 6 |
| `max_h_rate_khz` | Payload byte 7 |
| `min_v_rate` | Payload byte 10 |
| `max_v_rate` | Payload byte 11 |
Fields are written only when the payload is long enough to contain them.
**Product Serial Number Block** (tag `0x0A`) is decoded into the following
`DisplayCapabilities` field (dynamic pipeline only):
| `serial_number_string` | Payload bytes (ASCII, up to 13 bytes, `0x0A`-terminated) |
**General Purpose ASCII String Blocks** (tag `0x0B`) are decoded into successive
`unspecified_text` slots (dynamic pipeline only). Up to four blocks are stored; extras
are silently dropped.
| `unspecified_text[0..4]` | Each `0x0B` block payload (up to 13 bytes, `0x0A`-terminated) |
**Display Device Data Block** (tag `0x0C`) is decoded into the following
`DisplayCapabilities` fields (dynamic pipeline only):
| `display_technology` | Byte 0 bits 7:4 — `DisplayTechnology` enum |
| `display_subtype` | Byte 0 bits 3:0 — raw technology-specific sub-type (0–15) |
| `operating_mode` | Byte 1 bits 3:0 — `OperatingMode` enum |
| `backlight_type` | Byte 1 bits 5:4 — `BacklightType` enum |
| `data_enable_used` | Byte 1 bit 6 — `true` if DE signal is used |
| `data_enable_positive` | Byte 1 bit 7 — DE polarity (`true` = positive) |
| `native_pixels` | Bytes 2–5, two LE uint16 — `(width_px, height_px)`; `None` if either is 0 |
| `panel_aspect_ratio_100` | Byte 6 — raw byte; AR = value / 100 + 1 |
| `physical_orientation` | Byte 7 bits 1:0 — `PhysicalOrientation` enum |
| `rotation_capability` | Byte 7 bits 3:2 — `RotationCapability` enum |
| `zero_pixel_location` | Byte 7 bits 5:4 — `ZeroPixelLocation` enum |
| `scan_direction` | Byte 7 bits 7:6 — `ScanDirection` enum |
| `subpixel_layout` | Byte 8 — `SubpixelLayout` enum |
| `pixel_pitch_hundredths_mm` | Bytes 9–10 — `(h, v)` in 0.01 mm; `None` if either is 0 |
| `color_bit_depth` | Byte 11 bits 3:0 — bpc − 1 converted to `ColorBitDepth` |
| `pixel_response_time_ms` | Byte 12 — milliseconds; `None` if 0 |
**Interface Power Sequencing Block** (tag `0x0D`) is decoded into the following
`DisplayCapabilities` field (dynamic pipeline only):
| `power_sequencing` | All 6 timing fields (T1–T6) decoded into a `PowerSequencing` struct; raw counts in 2 ms units; `None` if payload < 6 bytes |
**Transfer Characteristics Block** (tag `0x0E`) is decoded into the following
`DisplayCapabilities` field (dynamic pipeline only):
| `transfer_characteristic` | Sample encoding (8/10/12-bit), channel mode (luminance or RGB), and normalized `[0.0, 1.0]` sample points decoded into a `DisplayIdTransferCharacteristic`; `None` if payload < 2 bytes or encoding byte is reserved |
In single-channel mode the curve is `TransferCurve::Luminance(Vec<f32>)`. In multi-channel
mode (byte 0 bit 5 set) the curve is `TransferCurve::Rgb { red, green, blue }` with the
sample data split into three equal sequential regions. Blocks where the sample bytes cannot
be split evenly are silently skipped.
**Display Interface Data Block** (tag `0x0F`) is decoded into the following
`DisplayCapabilities` field:
| `display_id_interface` | Interface type, spread-spectrum flag, lane count, min/max pixel clock (in 10 kHz units), and content protection type decoded into a `DisplayIdInterface` struct; `None` if payload < 7 bytes |
**Stereo Display Interface Data Block** (tag `0x10`) is decoded into the following
`DisplayCapabilities` field:
| `stereo_interface` | Stereo viewing mode, 3D sync signal polarity, and glasses sync interface decoded into a `DisplayIdStereoInterface` struct; `None` if payload < 2 bytes |
**Tiled Display Topology Data Block** (tag `0x12`) is decoded into the following
`DisplayCapabilities` field:
| `tiled_topology` | Grid dimensions, tile position, tile pixel size, single-enclosure flag, missing-tile behavior, and optional per-edge bezel sizes decoded into a `DisplayIdTiledTopology` struct; `None` if payload < 7 bytes |
The `h_tile_count` and `v_tile_count` fields store actual counts (raw value + 1). The
`bezel` field is `None` when the `has_bezel_info` flag is clear or the payload has fewer
than 11 bytes.
If the DisplayID block appears alongside an EDID base block, DisplayID values overwrite
any base-block values for the same fields.
## Extracted timing data
**Type I Video Timing blocks** (tag `0x03`) are decoded in both the dynamic and static
pipelines. Each 20-byte descriptor maps to a `VideoMode` with full timing detail:
| `width`, `height` | Horizontal/Vertical Active (exact pixel/line counts) |
| `refresh_rate` | Derived: `pixel_clock_hz / (h_total × v_total)` |
| `pixel_clock_khz` | Bytes 1–2 × 10 (10 kHz units converted to kHz) |
| `interlaced` | Byte 19 bit 0 |
| `h_front_porch`, `h_sync_width` | Bytes 7–10 |
| `v_front_porch`, `v_sync_width` | Bytes 15–18 |
| `sync` | `DigitalSeparate`; polarities from byte 19 bits 3–4 |
Null descriptors (pixel clock = 0) are silently skipped.
**Type II Video Timing blocks** (tag `0x04`) are decoded in both the dynamic and static
pipelines. Each 11-byte descriptor maps to a `VideoMode`:
| `width`, `height` | Horizontal/Vertical Active (8-pixel and 1-line granules respectively) |
| `refresh_rate` | Derived: `(raw_clock + 1) × 10 000 / (h_total × v_total)` |
| `pixel_clock_khz` | Bytes 0–2 `(raw + 1) × 10` (10 kHz units converted to kHz) |
| `interlaced` | Byte 3 bit 4 |
| `h_front_porch`, `h_sync_width` | Byte 6 nibbles (8-pixel granule) |
| `v_front_porch`, `v_sync_width` | Byte 9 nibbles (1-line granule) |
| `sync` | `DigitalSeparate`; polarities from byte 3 bits 3–2 |
Byte 9 is dual-role: the full byte encodes the total `v_blank`; the upper/lower nibbles
encode `v_front_porch − 1` and `v_sync_width − 1`. The implied back porch is
`v_blank − v_front_porch − v_sync_width`.
**Type III Short Video Timing blocks** (tag `0x05`) are decoded in both the dynamic and static
pipelines. Each 3-byte descriptor encodes only the horizontal active, aspect ratio, and refresh
rate; vertical active is derived from the aspect ratio:
| `width` | `(byte 1 + 1) × 8` pixels (8-pixel granule, max 2048) |
| `height` | `width × height_factor / width_factor` from aspect ratio |
| `refresh_rate` | Byte 2 bits 6:0, plus 1 Hz |
| `interlaced` | Byte 2 bit 7 |
| `h_front_porch`, `h_sync_width`, `v_front_porch`, `v_sync_width` | 0 (not encoded) |
| `sync` | `None` (not encoded) |
Descriptors with undefined or reserved aspect ratio codes are silently skipped, as are those
where the derived vertical active is not a whole number of lines.
**Type IV Timing Code blocks** (tag `0x06`) are decoded in both the dynamic and static
pipelines. Each payload byte is a timing identifier resolved via a lookup table. The code
space is selected by bits 7:6 of the data block's revision byte:
| `0` | VESA DMT ID | DMT v1.13 table (IDs 0x01–0x58) |
| `1` | CTA-861 VIC | VIC table (codes 1–219) |
| `2` | HDMI VIC | 4 codes: 1=3840×2160@30, 2=@25, 3=@24, 4=4096×2160@24 |
| `3` | Reserved | silently skipped |
Timing detail fields (`h_front_porch`, `h_sync_width`, `v_front_porch`, `v_sync_width`,
`sync`) are fully populated for both DMT-resolved and VIC-resolved modes. HDMI VIC modes
carry only `width`, `height`, and `refresh_rate` (timing detail is not defined for them).
Unrecognised codes are silently skipped.
**VESA Video Timing blocks** (tag `0x07`) are decoded in both the dynamic and static
pipelines. The payload is a presence bitmap: up to 10 bytes encoding DMT IDs 0x01–0x50.
Bit `i` (0-indexed, LSB-first within each byte) corresponds to DMT ID `i + 1`. Set bits
are resolved via the DMT v1.13 table with full timing detail; payload bytes beyond 10 are
ignored.
| `width`, `height`, `refresh_rate`, `interlaced` | DMT v1.13 table entry |
| `h_front_porch`, `h_sync_width`, `v_front_porch`, `v_sync_width` | DMT v1.13 table entry |
| `sync` | `DigitalSeparate`; polarities from DMT v1.13 table entry |
**CTA-861 Video Timing blocks** (tag `0x08`) are decoded in both the dynamic and static
pipelines. The payload is a presence bitmap: up to 8 bytes encoding CTA-861 VICs 1–64.
Bit `i` (0-indexed, LSB-first within each byte) corresponds to VIC `i + 1`. Set bits are
resolved via the CTA-861 VIC table with full timing detail; payload bytes beyond 8 are ignored.
| `width`, `height`, `refresh_rate`, `interlaced` | CTA-861 VIC table entry |
| `h_front_porch`, `h_sync_width`, `v_front_porch`, `v_sync_width` | CTA-861 VIC table entry |
| `sync` | `DigitalSeparate`; polarities from CTA-861 VIC table entry |
**Type V Short Video Timing blocks** (tag `0x11`) are decoded in both the dynamic and static
pipelines. Each 7-byte descriptor encodes width, height, and refresh rate directly; no blanking
detail or pixel clock is stored. Type V is always progressive.
| `width` | Bytes 1–2 exact pixel count (LE uint16) |
| `height` | Bytes 3–4 exact line count (LE uint16) |
| `refresh_rate` | Byte 5 + 1 Hz (range 1–256, clamped to 255) |
| `interlaced` | Always `false` — Type V defines progressive-only timings |
| `pixel_clock_khz` | Not populated — not encoded in the descriptor |
| `h_front_porch`, `h_sync_width`, `v_front_porch`, `v_sync_width` | 0 (not encoded) |
| `sync` | `None` (not encoded) |
Descriptors with zero width or height are silently skipped.
**Type VI Detailed Video Timing blocks** (tag `0x13`) are decoded in both the dynamic and static
pipelines. Each descriptor is 14 or 17 bytes; the 17-byte form includes optional aspect/size
bytes that are not currently decoded. Pixel clock is stored in 1 kHz steps (not 10 kHz like
Types I and II), allowing higher precision up to ~4194 MHz.
| `width` | Bytes 3–4 bits 14:0 (15-bit, exact pixel count) |
| `height` | Bytes 5–6 bits 14:0 (15-bit, exact line count) |
| `refresh_rate` | Derived: `pixel_clock_hz / (h_total × v_total)` |
| `pixel_clock_khz` | Bytes 0–2 bits 21:0 (1 kHz steps, stored directly) |
| `interlaced` | Byte 13 bit 7 |
| `h_front_porch` | Bytes 7–9 (H-fp, 12-bit packed across byte 8 and byte 9 bits 7:4) |
| `h_sync_width` | Byte 10 (8-bit) |
| `v_front_porch` | Byte 12 (8-bit) |
| `v_sync_width` | Byte 13 bits 3:0 |
| `sync` | `DigitalSeparate`; h_sync_positive = byte 3–4 bit 15, v_sync_positive = byte 5–6 bit 15 |
Null descriptors (pixel clock = 0) advance the cursor without emitting a mode. The descriptor
size (14 or 17) is determined by byte 2 bit 22.
## Warnings
| `DisplayIdVersionUnknown(u8)` | Version byte is outside the known ranges (0x10–0x1F, 0x20). The block is skipped. |
| `DisplayIdExtensionCountMismatch { declared, found }` | The extension count in the first fragment's header does not match the number of continuation blocks actually present. Processing continues with whatever fragments are available. |
| `DisplayIdChecksumMismatch` | The DisplayID section checksum byte does not make `block[1..=4+N]` sum to zero mod 256. Processing continues with whatever data is present in the fragment. |
| `DisplayIdSectionBytesOutOfRange(u8)` | The `section_byte_count` field (byte 2) is larger than 122, placing the checksum byte outside the 128-byte extension block. The section is still parsed using the clamped available bytes. |
## Fragment layout reference
Each 128-byte EDID extension block carrying DisplayID has the following structure:
```
Byte 0: 0x70 (EDID extension tag)
Byte 1: DisplayID version/revision
Byte 2: Section byte count N (data block payload bytes in this fragment; max 122)
Byte 3: Bits [7:3] = continuation block count
Bits [2:0] = display product primary use case
Bytes 4..4+N-1: DisplayID data blocks
Byte 4+N: DisplayID section checksum (sum of bytes 1..=4+N must be 0 mod 256)
Bytes 4+N+1..126: Padding zeros
Byte 127: EDID extension block checksum (sum of all 128 bytes must be 0 mod 256)
```
Data blocks within the payload each begin with a 3-byte header:
```
Byte 0: Block tag
Byte 1: Revision
Byte 2: Payload length (bytes following this header)
```
Iteration stops at an end-of-section sentinel (tag `0x00`, length `0`) or when a block's
declared length would extend past the available payload.