Part of Ashlar — a display connection stack in Rust, HDMI 2.1 first: from EDID parsing through mode negotiation to FRL link training.
display-types · piaf · concordance · cartouche · culvert · plumbob · hdmi-hal · hdmi-hal-i2c-dev
HDMI 2.1 mode negotiation — policy layer of the display connection stack.
Given sink, source, and cable capabilities, concordance answers: "what modes can I drive on this display, in what priority order, using what color format and bit depth?"
The library takes three caller-supplied structs — SinkCapabilities, SourceCapabilities,
and CableCapabilities — and runs them through a three-stage pipeline: enumerate all
candidate configurations, check each against HDMI constraints, rank the
accepted candidates according to a policy. The output is a ranked Vec<NegotiatedConfig>,
each entry carrying the resolved mode, color format, FRL tier, and any non-fatal warnings.
A ReasoningTrace on each entry records why a configuration was accepted or rejected, so
every output is fully explainable.
For embedded and firmware targets, the constraint engine is also exposed as a standalone
no_std-compatible probe:
use concordance::{is_config_viable, SinkCapabilities, SourceCapabilities, CableCapabilities, CandidateConfig};
use display_types::{ColorBitDepth, ColorFormat};
use display_types::cea861::{HdmiForumFrl, vic_to_mode};
// For standard CTA modes, use vic_to_mode — it carries the exact pixel clock from
// the CEA-861 timing table, so bandwidth ceiling checks are precise.
// VIC 97 = 3840×2160 @ 60 Hz (594 MHz). Other common VICs: 16 = 1080p@60, 97 = 4K@60.
let mode = vic_to_mode(97).unwrap();
let config = CandidateConfig::new(
&mode,
ColorFormat::Rgb444,
ColorBitDepth::Depth8,
HdmiForumFrl::NotSupported,
false,
);
match is_config_viable(&sink, &source, &cable, &config) {
Ok(_warnings) => println!("viable"),
Err(violations) => {
// Each violation is a TaggedViolation<Violation>: Display shows "[rule] message".
for v in violations.iter() { eprintln!("rejected: {v}"); }
}
}For the full ranked pipeline (alloc/std):
use concordance::NegotiatorBuilder;
let configs = NegotiatorBuilder::default()
.negotiate(&sink, &source, &cable);
for cfg in &configs {
println!("{}×{}@{} {:?} {:?}",
cfg.resolved.mode.width, cfg.resolved.mode.height, cfg.resolved.mode.refresh_rate,
cfg.resolved.color_encoding, cfg.resolved.bit_depth);
}Custom constraint rules can be injected without replacing the default engine:
use concordance::NegotiatorBuilder;
let configs = NegotiatorBuilder::default()
.with_extra_rule(PlatformBandwidthRule::new(limits))
.negotiate(&sink, &source, &cable);Ranked iterator, not a verdict. There is no single right answer. The library enumerates all valid configurations in a defined, documented priority order and lets the caller pick. No mode is silently discarded — rejections appear in the reasoning trace.
Every output is explainable. Each NegotiatedConfig carries a ReasoningTrace
recording the decisions made during negotiation. A driver or diagnostic tool can explain
exactly why a given mode was selected or excluded.
Extensible without forking. The three pipeline components — ConstraintEngine,
CandidateEnumerator, ConfigRanker — are traits with default implementations. Any
component can be replaced or wrapped via NegotiatorBuilder without touching crate source.
For the common case of adding a rule on top of the default checks, with_extra_rule injects
a single ConstraintRule without reimplementing everything else.
Custom violation and warning types. ConstraintEngine and ConfigRanker declare
associated Warning and Violation types bounded by Diagnostic, and a ConstraintRule
can report warnings of its engine's type. A custom component emits its own variants with
full type fidelity — no wrapping, no loss of structured information.
FRL rates that can carry the mode. An FRL rate within every ceiling is rejected when it
cannot carry the timing: audio in the blanking, borrowing, and the line's characters with
the link overhead. A candidate whose capacity cannot be checked (no blanking, DSC, unknown
audio) is accepted with a warning saying so. The computation follows the public
implementations (amdgpu, i915) and evaluates every reading where they disagree, so a rate
that fits under some readings only is accepted with a warning naming what decides, and
NegotiationPolicy ranks it after the sure ones. See doc/architecture.md.
Cable is a first-class constraint. Link capability is determined by source, sink, and
cable. A cable that cannot carry FRL or whose TMDS clock ceiling is too low produces a
Violation like any other constraint failure. CableCapabilities::unconstrained() is
provided for callers without cable information.
no_std at all resource levels. Three build tiers are explicitly supported:
- Bare
no_std(no allocator):is_config_viableborrows all inputs, no heap use. no_std + alloc: the ranked pipeline andReasoningTraceare available.std: full feature set, additive on top ofalloc.
Serde on all public types. Every public type derives Serialize/Deserialize behind
the serde feature flag, covering inputs, outputs, and policy types.
| Flag | Default | Description |
|---|---|---|
std |
yes | Enables the ranked pipeline and ReasoningTrace; implies alloc |
alloc |
no | Enables the ranked pipeline without std |
serde |
no | Derives Serialize/Deserialize on all public types |
Without alloc or std, only is_config_viable is available. The three input structs and
CandidateConfig are available at all tiers.
concordance is the policy layer. It consumes the typed model; it does not produce it.
flowchart LR
piaf["piaf\n(EDID parser)"]
dc["DisplayCapabilities"]
sink["SinkCapabilities"]
nb["NegotiatorBuilder"]
out["Vec<NegotiatedConfig>"]
integ["integration layer\n(GPU / cable discovery)"]
piaf --> dc
dc -->|"sink_capabilities_from_display"| sink
sink --> nb
integ -->|"SourceCapabilities\nCableCapabilities"| nb
nb --> out
All types in SinkCapabilities — VideoMode, HdmiForumSinkCap, HdmiVsdb,
ColorCapabilities, etc. — are from
display-types, the shared vocabulary crate.
sink_capabilities_from_display in concordance bridges a parsed DisplayCapabilities
directly into SinkCapabilities; the integration layer only needs to supply source and
cable capabilities.
- EDID parsing — that's piaf.
- Source capability discovery — querying DRM/KMS or VBIOS for GPU limits.
- Cable capability discovery — reading the HDMI cable type marker or accepting a user override.
- Link training — determining whether a negotiated FRL tier is achievable on real hardware.
- InfoFrame encoding — signaling the negotiated configuration to the sink.
- HDCP — out of scope for the entire stack.
Extended documentation lives under doc/.
Understanding the library
doc/architecture.md— pipeline structure, constraint rules, ranking algorithm, and design principlesdoc/enumerator.md— candidate enumeration, Cartesian product dimensions, and pre-filtering
Contributing
doc/setup.md— build, test, and coverage commandsdoc/testing.md— testing strategy, fixture corpus, and CI expectationsdoc/roadmap.md— planned features and future work
Releases are built on GitHub Actions and attested with
SLSA Build Level 2 provenance (from 0.2.0). To verify a release
.crate against its signed provenance, install the
GitHub CLI and run:
gh attestation verify concordance-X.Y.Z.crate --repo ashlar-display/concordanceThe release from before the repository moved to the ashlar-display organization (0.2.0)
was attested under its previous owner, and verifies against it:
gh attestation verify concordance-0.2.0.crate --owner DracoWhitefire \
--signer-repo DracoWhitefire/concordanceThe attested .crate is attached to each
GitHub release.
Those releases are also uploaded through crates.io trusted publishing: the publish workflow obtains a short-lived token via OpenID Connect instead of using a stored API token. crates.io can also reject uploads made with an API token for this crate; the badge above shows whether that setting is on. Each version's publishing details, including the workflow run and commit, are available from the crates.io API:
curl -A "your-tool (contact)" https://crates.io/api/v1/crates/concordance/X.Y.Z
# see .version.trustpub_data0.1.0 was uploaded with an API token; it carries no provenance and has no GitHub release.
Licensed under the Mozilla Public License 2.0.