Trade Study: Metric Key Set — how it is carried - rdkcentral/rdk-halif-aidl GitHub Wiki
- Type: Trade Study
- Audience: Architecture and engineering. Product and programme readers welcome
- Purpose: To set out the ways a metric key set can be carried by an interface that will be frozen, what each one costs once it is, what Android's HALs do at the same boundary, and the conditions under which each option wins
A metrics interface carries a key set — the list of figures a component measures — rather than a capability. A capability set is settled once the hardware is understood. A key set keeps moving for as long as anything downstream asks new questions of the data, and that difference is what decides where the key set should live.
Decision required. Whether the metric key set is carried inside each interface's AIDL source set or generated beside it on its own cadence. The surface is unmerged and no released version carries it, so the decision is open until the branch lands.
The metrics module is types only — no service, no manager, no key set. Every figure crosses as a derived identifier with a status beside it, inside a fixed envelope.
flowchart LR
SNAP["<b>MetricSnapshot</b><br/>timestampNs — one latching instant"]
GRP["<b>MetricGroup</b><br/>id — which element"]
ATT["attributes<br/>which thing was measured"]
VAL["values<br/>what was measured"]
MV["<b>MetricValue</b><br/>id · value · status"]
EV["<b>MetricEvent</b><br/>timestampNs · eventId"]
SNAP --> GRP
GRP --> ATT
GRP --> VAL
ATT --> MV
VAL --> MV
EV --> MV
style SNAP fill:#FCF3CF,color:#1a1a1a,stroke:#8a6d1f
style GRP fill:#FCF3CF,color:#1a1a1a,stroke:#8a6d1f
style EV fill:#FCF3CF,color:#1a1a1a,stroke:#8a6d1f
style ATT fill:#EAECEE,color:#1a1a1a,stroke:#7b7d7d
style VAL fill:#EAECEE,color:#1a1a1a,stroke:#7b7d7d
style MV fill:#ABEBC6,color:#1a1a1a,stroke:#1e7a4d
Four consequences follow.
- Adding a figure adds no member to any parcelable. The envelope is fixed so the contents can be free, and a consumer walks a group without code written for that element.
- Identity is derived, not allocated. The identifier is a truncated hash over the composed path with the unit and, for a value, the kind. Nothing consults a registry, so six components share one identifier space without allocating from it, and a product serving microseconds where the declaration says milliseconds yields a different identifier rather than a figure quietly wrong by a factor of a thousand.
-
Capability is answered per value.
NOT_SUPPORTEDis a property of the implementation,NOT_AVAILABLEa property of the moment, and both ride the value rather than needing a separate query. - Ownership follows production. Each component declares the keys for the values it measures and the metrics module declares none, so a new decoder figure is a change to the decoder's own key set.
The selector names identifiers. Six interfaces declare getMetrics(in @nullable long[] metricIds), the same long the snapshot returns in MetricValue.id, and an identifier the product does not know comes back NOT_SUPPORTED. What still ties the key set to the interface is a file: the generator writes each component's Metric enum into that component's own AIDL source directory, so a key-set revision changes the interface's source set, and nothing measures that only because the working version carries no frozen hash yet.
The recommended call names nothing. Passing null asks for everything the resource serves, and the contract names that as the call a consumer polling at a cadence should use: one round trip, one sampling instant, figures the product cannot measure simply absent. A caller names a subset by generated constant, and the reason to name rather than filter client-side is that a named key the product cannot measure comes back NOT_SUPPORTED.
The coupling is therefore a file in the source set, named by no signature — a generated enum the interface compiles and nothing in the data path consumes. At the freeze it is in the hash.
A′ — a hand-maintained enum inside the interface. The .kvc declarations and the generator are deleted, and each component's four enum files inside its AIDL sources, plus the constants for consumers not built from AIDL, are edited by hand and kept in step. A new key is an enumerator in a file the interface compiles, so the key set and the interface move together.
A — a generated enum inside the interface. What the branch carries. scripts/kvcc.py reads each component's .kvc and writes four .aidl enums into that component's own source directory. A new key regenerates a file the interface compiles, so the key set and the interface still move together. This is the default if nothing is decided.
B — a generated enum in a sibling AIDL package. Metric moves into a types-only package beside the component, com.rdk.hal.videosink.keys, with its own version and no methods. The generator writes it there. A new key is a version of the keys package, which a vendor adopts with nothing to implement. This is the shape Android's vehicle HAL moved to at Android 14 and gralloc chose at Android 11.
C — a generated key set beside the interface, outside AIDL. The generator writes the key constants to the module's own header and emits nothing into any interface's source set. Callers name keys by generated constant. A key-set revision releases on its own cadence and moves no interface hash.
What decides the cost is how many parties must act before the figure exists on a product that needs it, and whether any of them is electing an interface migration to deliver it.
Under A′ and A, a figure lands in a new interface version and a vendor picks it up by migrating. Migration is all or nothing: taking one counter means taking every other change in that version, on that vendor's schedule and against that vendor's risk appetite. The platform can require the figure. It cannot require the migration. Availability across the estate is the union of several independent decisions with no date attached, and the middleware carries a mixed estate for as long as that takes.
flowchart LR
REQ["<b>figure required</b>"]
NEWV["<b>interface 0.2.0.0</b><br/>carries the figure<br/>and every other change"]
VA["<b>Vendor A</b> on 0.2.0.0<br/>figure served"]
VB["<b>Vendor B</b> on 0.1.0.0<br/>figure absent until it moves"]
VC["<b>Vendor C</b> on 0.1.0.0<br/>figure absent"]
MW["<b>Middleware</b><br/>carries both interface versions<br/>while the estate is mixed"]
REQ --> NEWV
NEWV -->|"migrates"| VA
NEWV -.->|"migrates later"| VB
NEWV -.->|"does not migrate"| VC
VA --> MW
VB --> MW
VC --> MW
style REQ fill:#FCF3CF,color:#1a1a1a,stroke:#8a6d1f
style NEWV fill:#F5B7B1,color:#1a1a1a,stroke:#922b21
style VA fill:#ABEBC6,color:#1a1a1a,stroke:#1e7a4d
style VB fill:#FDEBD0,color:#1a1a1a,stroke:#b9770e
style VC fill:#FADBD8,color:#1a1a1a,stroke:#922b21
style MW fill:#D6EAF8,color:#1a1a1a,stroke:#1f618d
Under B and C, a key-set revision is published and a vendor adopts it. Under C the revision is a directory carrying the declaration and its generated header, committed; a vendor selects it and compiles against the header as published. Under B it is a frozen version of the keys package, which a vendor imports; the package has no methods, so adopting it changes nothing the vendor implements. Either way, implementing the counter is an internal change inside a release the vendor was shipping anyway.
flowchart LR
REQ["<b>figure required</b>"]
REV["<b>key set 1.1.0</b> published<br/>declaration and header, committed<br/>no interface file touched"]
VA["<b>Vendor A</b> on 0.1.0.0<br/>figure served"]
VB["<b>Vendor B</b> on 0.1.0.0<br/>answers NOT_SUPPORTED"]
VC["<b>Vendor C</b> on 0.1.0.0<br/>answers NOT_SUPPORTED"]
MW["<b>Middleware</b><br/>carries one interface version<br/>reads the answer per value"]
REQ --> REV
REV -->|"selects 1.1.0, adds the counter"| VA
REV -.->|"stays on 1.0.0"| VB
REV -.->|"cannot measure it"| VC
VA --> MW
VB --> MW
VC --> MW
style REQ fill:#FCF3CF,color:#1a1a1a,stroke:#8a6d1f
style REV fill:#ABEBC6,color:#1a1a1a,stroke:#1e7a4d
style VA fill:#ABEBC6,color:#1a1a1a,stroke:#1e7a4d
style VB fill:#FDEBD0,color:#1a1a1a,stroke:#b9770e
style VC fill:#EAECEE,color:#1a1a1a,stroke:#7b7d7d
style MW fill:#D6EAF8,color:#1a1a1a,stroke:#1f618d
Producer and consumer select independently, which is what the floor is for.
flowchart TB
ARCH["<b>Architecture</b><br/>publishes key-set revisions<br/>declaration + generated header, committed"]
R11["revision 1.1.0"]
R13["revision 1.3.0"]
ARCH --> R11
ARCH --> R13
VEN["<b>Vendor</b> builds against 1.1.0<br/>no generator run<br/>interface version unchanged"]
CON["<b>Consumer</b> builds against 1.3.0<br/>names identifiers from 1.3.0"]
R11 --> VEN
R13 --> CON
CON -->|"names a 1.3.0 key"| ANS
VEN -->|"serves 1.1.0"| ANS
ANS["<b>answered per value</b><br/>SUPPORTED where served<br/>NOT_SUPPORTED where not"]
style ARCH fill:#FCF3CF,color:#1a1a1a,stroke:#8a6d1f
style R11 fill:#EAECEE,color:#1a1a1a,stroke:#7b7d7d
style R13 fill:#EAECEE,color:#1a1a1a,stroke:#7b7d7d
style VEN fill:#FDEBD0,color:#1a1a1a,stroke:#b9770e
style CON fill:#D6EAF8,color:#1a1a1a,stroke:#1f618d
style ANS fill:#ABEBC6,color:#1a1a1a,stroke:#1e7a4d
Neither side waits for the other, because the interface names a key-set floor rather than a pin. Availability is bounded by whether the vendor implemented the counter.
Backwards compatibility answers disruption, not availability. A version bump breaks nobody, which is true and is a different problem. A certification figure is needed by a date someone else set. The property that makes adoption safe — a vendor moves when it chooses — is the property that makes availability unpredictable.
The figures are not ours to specify. Each traces to a consumer publishing on its own schedule, and the field dictionary names them key by key.
-
CMCDv2.
underflowedcarriesbsa,underflow_duration_mscarriesbsda,buffer_depth_mscarriesbl,first_frame_presentedmapsstop,state_changedcarriessta. CMCDv2 is in development at CTA-WAVE rather than ratified, so of the five it is the set most likely to move, and a key renamed or added there lands in our declaration. - CTA-2066, revising independently of CMCD.
- Partner certification programmes, each re-issued on its own schedule, none synchronised with the others.
- QoE and CSAT requirements, which are ours, and which move when the product does rather than when a standard does.
Five clocks terminate in one field set. The component declarations carry 77 items today — 51 polled keys, 10 event kinds and 16 event fields — before CMCDv2 ratifies or any certification re-issue lands.
Every Android HAL is a static interface: methods, parcelables and a hash frozen per version. They differ in whether the list of what can be asked for is part of what freezes. At android-17.0.0_r1 both shapes are in use, each HAL's owning team chose its own, and no owner is shared between the two groups. The HAL-by-HAL detail, the owners and the method behind every figure are in Trade Study: Metric Key Set — Android HAL evidence.
| List typed into the interface — A′, A | List beside the interface — B, C | |
|---|---|---|
| HALs and owners | TV tuner and TV media quality — one team; thermal and power — one team; health; sensors | power stats; Codec2; camera; vehicle; gralloc |
| The read | tuner: getStatus(in FrontendStatusType[] statusTypes)
|
power stats: getStateResidency(in int[] powerEntityIds); Codec2: query(in int[] indices, …)
|
| Where the list is | an enum, or one parcelable member per figure, in the interface's own package | a separate AIDL package (vehicle, gralloc — shape B); a header in another repository (Codec2), a schema file with its own generator (camera), or data the product returns (power stats) — shape C |
| Size | 21 – 112 | 146, 374, unbounded |
| Who adds to it | one owner | the owner and vendors |
| When it moves | once, with the yearly platform release | off the interface's cadence |
| One new item costs | a frozen interface version | nothing to the interface |
The typed shape's cost is measured in the same tree. Power version 6 → 7 is one enumerator, AUDIO_PERFORMANCE. Sensors 2 → 3 is MOISTURE_INTRUSION. Power stats 1 → 2 is CAMERA, added to the one closed enum in a HAL whose measurable lists are otherwise runtime data. Tuner 2 → 3 is one status type. Health cut a version at every release from Android 13 to 17. And once a list is frozen and the language cannot extend it, the cost is a second enum and a second method: [email protected] added getStatusExt1_1(vec<FrontendStatusTypeExt1_1>) beside getStatus to carry 15 more status types.
Two teams moved a list out, and both gave cadence as the reason. Power statistics were methods on the Power HAL until Android 10 replaced them with product-returned lists read by id. The vehicle team shipped its property list inside the interface at Android 13 and moved it to a separate package at Android 14; the commit, written by that HAL's principal author, says: "so that we can update the property separatly. This allows us to backport new property definition to older releases more easily." Gralloc recorded the same reasoning when it chose the shape: a HIDL enum "would have required a new IMapper version just to add future standard buffer metadata types". No team has moved the other way, and both moves went into another AIDL package rather than a second definition language.
Android's guidance states the rule both groups follow, not a direction: "Enums should be closed sets. Note: only the interface owner can add enum elements", and "for system-to-vendor APIs, this should happen with the yearly platform revision".
Which group the getMetrics() key set belongs in is decided by ownership and cadence, not size:
| Property | Typed-in lists | Lists beside | The getMetrics() key set |
|---|---|---|---|
| Size | 21 – 112 | 146, 374, unbounded | 77 |
| Who adds to it | one owner | the owner and vendors | five sources |
| When it moves | once, with the yearly release | off the interface's cadence | five unsynchronised clocks |
| Variance per product | none declared | vendor ranges, runtime lists | per-value NOT_SUPPORTED across several SoCs |
Both runs were made against the branch as it stands, adding the same counter to videosink.kvc and running the generator. The hash is the AIDL surface hash the release script computes — sha256 over every *.aidl file in the component — and it is the figure the release gate reads to decide whether a version bump is warranted.
| A — as the branch stands | C — key set beside the interface | |
|---|---|---|
| Tracked files changed |
videosink.kvc, and Metric.aidl inside the interface's source set |
videosink.kvc and the module's key header |
| AIDL surface hash before | 6e013919197d3593… |
7b4666cd11066de6… |
| AIDL surface hash after | 649f83caa192b633… |
7b4666cd11066de6… |
| Verdict the release gate reaches | CHANGED — a version is cut | unchanged — no bump is warranted |
| Identifier the key derives | 0x7739b47f68e6770c |
0x7739b47f68e6770c |
The identifier is the same under both, because it derives from the key's path, unit and kind rather than from where it is declared. What differs is whether declaring it moves a file the interface compiles. Under B the component's own hash is likewise unmoved; whether the release script then forces the component to bump because its import moved is open item 8, and it decides whether B delivers the property in this repository.
Moving to C from the branch head is twenty generated enum files leaving the interface tree — about 1,500 lines — and one edit to the generator's output path. No signature changes.
Bumps batch. Several additions in one release window become one version, not several, so the cost is not a count of releases: the repository froze components on 2026-05-26, on 2026-07-07, and took two patch releases on 2026-07-10.
The cost is what each release then asks of everyone else. A new enum value is a Minor Change, and post-baseline every additive change "goes through a standard 14+5 review cycle. The AIDL version integer is incremented." Test suites are bound to the interface by MAJOR.MINOR and "must be cloned or branched per MAJOR.MINOR version". A vendor adopts a version whole or not at all, so taking one counter means taking everything else in it, on that vendor's release train rather than on the certification date.
- Getting a figure onto the products that need it. How many parties must act, and whether any of them is choosing an interface migration to deliver it.
- Cost to change now. One-off, and the window closes at the freeze.
- Identity and capability variance. Does a figure keep one identity across six components, and can a product say it does not serve one?
- Hot-path cost. The first ground on which enums were chosen.
- Compile-time safety and testability. The second.
- What has to be owned. The toolchain, the governance and the debugging story each option obliges someone to carry.
Criteria 1 and 2 are the trade. Criteria 4 and 5 are the stated objection, and neither separates the options. Criterion 6 is where B and C cost most.
| Option | Effort | What the estimate includes and excludes |
|---|---|---|
| A′ | Deleting 437 lines, then paying for them | Removes the generator. Each later addition is then edited by hand into four .aidl files in the component, and into the constants used by consumers not built from AIDL, with nothing checking the two agree. Identity becomes allocated rather than derived, so a number registry and a collision rule are needed, and unit and kind stop being part of identity. |
| A | None now | Excludes every later addition. Once frozen, each one regenerates a file the interface compiles, so the key set cannot be revised without an interface version. |
| B | Small before the freeze; a freeze per revision after | Six new types-only modules, one per component, each with metadata.yaml, current/ and its own release. The generator's output path changes; the signature does not. Every key-set revision then goes through the AIDL freeze — snapshot, hash, review — with no methods to implement or test. Open item 9 must close first. |
| C — before the freeze | An edit to the unmerged branch | Measured: twenty generated enum files removed, one generator edit, no signature touched. No frozen version carries getMetrics or any generated enum — the whole surface sits in current/, which holds no hash. Callers already name keys by generated constant. |
| C — the remaining work | Medium, and it is real | Publish a kvc/x.y.z/ revision with its header committed; record the key-set floor in frozen interface.yaml; validate feature profiles against it; scan published key sets for identifier collisions; settle an alias and deprecation rule for renames. None of it blocks the signature, and none of it is optional if the key set becomes the contract. |
A key-set release is a conscious act, as an interface freeze is. Under B and C the architecture team cuts a key-set revision on its cadence exactly as it cuts an interface version: a deliberate freeze, reviewed, with its generated output committed. The generator is what that release runs, as the release script is what an interface freeze runs, and it is the same 437-line generator option A already uses. B's release is the AIDL freeze the repository already runs; C's is a second release kind for the same team.
| A′ — hand-maintained enum, in the interface | A — generated enum, in the interface | B — generated enum, sibling AIDL package | C — generated key set, outside AIDL | |
|---|---|---|---|---|
| Getting a figure onto the products that need it | A file the interface compiles is edited, so a frozen version cannot absorb it. Each vendor elects whether to migrate, and migration carries every other change in that version. | The same, with the file written by a generator rather than by hand. | A version of the keys package, with nothing to implement. A vendor imports it and writes the counter inside a release it was already shipping — if open item 8 closes in B's favour. | A key-set revision publishes with its header committed. A vendor selects it and writes the counter inside a release it was already shipping, with no interface migration and no generator to run. |
| Cost to change now | Deleting the generator and the declarations, then hand-maintaining what they produced. | None. | Six new modules and a generator path. | Twenty generated files removed and one generator edit, on a branch that has not merged. After the freeze the same change is one new version, once. |
| Identity and capability variance | Allocated per component, so a number is unique only within its own group and the shared envelope carries no source. Unit and kind are not part of identity. Needs a registry. | Derived from path, unit and kind; per-value tri-state. | Identical to A. | Identical to A. |
| Hot-path cost | Identical. The polling call passes null and names nothing. | Identical. | Identical. | Identical. |
| Compile-time safety and testability | A hand-written enumerator per key, and nothing checks it against a declaration. | A generated enumerator per key, committed inside the frozen interface version. A key-set change and an ABI change diff as one thing. | A generated enumerator per key, committed inside a frozen keys-package version. A key-set change diffs as its own version. | A generated constant per key, committed inside a frozen key-set revision. A key-set comparison is scoped to the key set. |
| Android precedent | TV tuner getStatus(in FrontendStatusType[]), health, thermal, sensors, power. |
The same HALs; none generates its enum. | Vehicle from Android 14, with the reason in the commit; gralloc; graphics.common. |
Codec2, camera, power stats. Camera is the one with its own schema language and generator. |
| What has to be owned | No toolchain. A number registry, a collision rule, and four files kept in step by hand. | The generator, already written and already in use. | The generator, plus six modules on the existing release process. | The generator, plus a key-set release the architecture team cuts on its cadence as it cuts an interface release. Raw identifiers in logs need the decode table the generator already emits. |
The hot path does not separate any of them, and neither does compile-time safety in the sense the objection means it: a generated constant and a generated enumerator are both compile-time artefacts, and both fail the build on a name the pinned revision does not carry. What separates them is where a new key lands, and what that obliges.
C, taken before the freeze. Adding a key is not a change to what the interface can do, and the measurement above is the whole argument: the same key, the same derived identifier, and an interface hash that moves in one case and not the other. Where the key set is written decides whether every vendor is asked to adopt an interface version in order to field a counter. The Android teams whose list has one owner and one yearly revision type it in; the teams whose list moves on another cadence keep it out, and the two that changed their minds changed in that direction. This key set has five sources and no shared date.
B is the same property with the tooling the interface already uses. If the objection that holds is a second definition language rather than the coupling, B keeps AIDL, checkapi and generated toString, at the cost of six more modules, the freeze ceremony per revision, and open item 8. It is the shape both Android moves went to. After the freeze, B and C each cost one new version to reach, once, so neither closes at the freeze.
The strongest argument against C, as its proponents put it. Android does exactly this on an AV HAL: the TV tuner selects status by an enum array and has grown its list through three frozen versions, and the same team used the shape again for media quality at Android 16 with 112 parameter names. Where Android did move a list out, it moved it into another AIDL package, never into a second definition language, and Android's own guidance says to prefer typed parcelables over serialised data. The AIDL HAL maintains backwards compatibility and runs on its own cadence, so a version bump breaks nobody and a vendor adopts only when a product requires it. An enumerator is the natural way to express a closed set in an interface language, it reads better at a call site than a hashed constant, and it lets a reviewer see the key set in the interface rather than having to find the file beside it. The existing entries are stable, are declared across nine HALs and anchor imminent test-suite work. A declared key set is itself a normative artefact whose additions are reviewed, so the alternative is not free. And single responsibility argues that a module's contract should state what that module serves, in its own interface, rather than deferring it to a file alongside.
-
Engineering says a
Minor Changebump costs a vendor, middleware and the test suite nothing in practice. Then criterion 1 does not separate the options and A wins on effort. This is the row an impact analysis settles, and the study cannot settle it alone. - The key set can be held to one owner and one revision a year. If certification re-issues and CMCDv2 additions can wait for the next interface release, the key set is in the tuner's position and A is right.
- The selector has a job the identifier does not do. A generated switch a vendor must exhaust would be one. If it has one, A is right.
- Open item 9 closes in B's favour, and the objection to C is the second definition language. Then B delivers the property with the existing release process, and C's remaining advantage is a lighter revision than an AIDL freeze.
- Open item 9 closes against B. Then a sibling package bumps the component anyway, and C is the only option that keeps the interface still.
| # | Item |
|---|---|
| 1 | The capture control available today is selection at read time and a declared cadence. Filtering before serialisation is designed, not built. |
| 2 | The key set floor each component declares is configuration no tool reads, and no key set has been revised. |
| 3 | A feature profile names the key set revision it was written against but is not validated against it. |
| 4 | A frozen interface version carries interface.yaml but no kvc block, so it does not record which key set revision it requires. The floor is declared in current/ only. |
| 5 | HAL.METRICS.19 specifies setMetric, which no interface carries. |
| 6 | The declarations and the floor carry two positions today rather than three, so the patch position has nowhere to record a documentation-only revision. |
| 7 | A rename derives a new identifier, so names are part of identity. An alias and deprecation rule is needed before a key is renamed rather than after. |
| 8 | Whether a component must bump when an imported types-only package bumps. release.sh:1786 says importers bump with the imported module; the hash gate at :3213 suppresses a bump when the importer's own .aidl is byte-identical; and videosink/0.2.0.0 pins a videodecoder version released three days after it. A release.sh dry-run on a scratch worktree settles it. |
A certification clause requires a new figure at the sink, counting frames dropped because their presentation deadline had passed.
Common to both options. The figure is specified, typed, given its unit and kind, and traced to its clause. A key is added to the component's vocabulary and the identifier is derived from the composed path with the unit and kind. No parcelable gains a member. The vendor that measures it implements the counter. A product that does not measure it answers NOT_SUPPORTED on that value if a caller names it, and omits it if the caller passed null.
Under A, once the version is frozen.
- The selector enum regenerates into the path the interface declares as its sources, so the interface has changed.
- A frozen version cannot absorb it, so a new interface version directory is cut and the interface copied forward.
- The change goes through interface governance.
- Each vendor decides whether to migrate. The decision sits on that vendor's release train rather than on the certification date, and migration is all or nothing, so taking this figure means taking every other change in the version.
- The middleware carries code for both versions while products on the old one remain in the field.
- The test suite gains a version-scoped increment.
flowchart TB
DECL["figure declared<br/>key added to the vocabulary"]
GEN["selector enum regenerates<br/>into the interface source set"]
NEWV["<b>new interface version</b><br/>frozen one cannot absorb it<br/>interface copied forward"]
GOV["interface governance"]
VA["Vendor A — migrate?<br/>takes the whole version"]
VB["Vendor B — migrate?<br/>takes the whole version"]
VC["Vendor C — migrate?<br/>takes the whole version"]
MW["middleware carries<br/>both versions"]
TS["test suite gains a<br/>version increment"]
PROD["figure on a product"]
DECL --> GEN --> NEWV --> GOV
GOV --> VA --> PROD
GOV --> VB --> PROD
GOV --> VC --> PROD
GOV --> MW
GOV --> TS
style DECL fill:#FCF3CF,color:#1a1a1a,stroke:#8a6d1f
style GEN fill:#FADBD8,color:#1a1a1a,stroke:#922b21
style NEWV fill:#F5B7B1,color:#1a1a1a,stroke:#922b21
style GOV fill:#F5B7B1,color:#1a1a1a,stroke:#922b21
style VA fill:#FDEBD0,color:#1a1a1a,stroke:#b9770e
style VB fill:#FDEBD0,color:#1a1a1a,stroke:#b9770e
style VC fill:#FDEBD0,color:#1a1a1a,stroke:#b9770e
style MW fill:#D6EAF8,color:#1a1a1a,stroke:#1f618d
style TS fill:#D6EAF8,color:#1a1a1a,stroke:#1f618d
style PROD fill:#ABEBC6,color:#1a1a1a,stroke:#1e7a4d
Under C.
- Architecture publishes the vocabulary revision, on its cadence, as a directory beside the interface versions carrying the declaration and its generated header. No interface file changes, no frozen hash moves, no interface version is cut.
- The change goes through vocabulary review.
- The vendor that measures the figure selects that revision, compiles against the published header, implements the counter and ships a driver build. It runs no generator, its interface version does not change, and no other vendor does anything.
- The consumer selects the revision it wants, independently of what any vendor has adopted, and names the identifier from the same published header.
- The test suite binds to the revision the product declares.
flowchart TB
DECL["figure declared<br/>key added to the vocabulary"]
REV["<b>vocabulary revision published</b><br/>declaration + generated header, committed<br/>no interface file touched"]
GOV["vocabulary review"]
VEN["the vendor that measures it<br/>selects the revision, implements,<br/>ships a build it was already shipping"]
CON["the consumer<br/>selects its own revision"]
READ["names the identifier"]
ANS["answered per value<br/>NOT_SUPPORTED where not served"]
PROD["figure on a product"]
NOOP["other vendors: nothing<br/>interface version: unchanged<br/>test suite: binds to the declared revision"]
DECL --> REV --> GOV
GOV --> VEN --> PROD
GOV --> CON --> READ --> ANS
REV -.-> NOOP
style DECL fill:#FCF3CF,color:#1a1a1a,stroke:#8a6d1f
style REV fill:#ABEBC6,color:#1a1a1a,stroke:#1e7a4d
style GOV fill:#D5F5E3,color:#1a1a1a,stroke:#1e7a4d
style VEN fill:#FDEBD0,color:#1a1a1a,stroke:#b9770e
style CON fill:#D6EAF8,color:#1a1a1a,stroke:#1f618d
style READ fill:#D6EAF8,color:#1a1a1a,stroke:#1f618d
style ANS fill:#ABEBC6,color:#1a1a1a,stroke:#1e7a4d
style PROD fill:#ABEBC6,color:#1a1a1a,stroke:#1e7a4d
style NOOP fill:#EAECEE,color:#1a1a1a,stroke:#7b7d7d
The declaration step is shared. What follows it differs: one path reaches a product through a new interface version and a migration decision per vendor, the other through a build the vendor was already shipping.
The objection. One number answers everything today. A product states its interface version and that settles what it implements. Separate the vocabulary and there are two numbers, the compatibility matrix gains a dimension, and every consumer reasons about both.
The second number already exists. Each vocabulary carries a version and each component declares a minimum it requires. What is being decided is whether the interface signature continues to duplicate what that number states.
The placement is the mechanism. A vocabulary sits beside the frozen interface versions, never inside one.
videosink/
├── metadata.yaml component name, current version
│
├── 0.1.0.0/ an interface release — ABI fixed here
│ ├── .hash the frozen hash. this file is what "frozen" means
│ ├── interface.yaml imports pinned exactly
│ ├── com/rdk/hal/videosink/*.aidl the interface
│ ├── include/ src/ generated bindings, committed
│ ├── hfp-videosink.yaml what a product declares against it
│ └── docs/
├── 0.2.0.0/ a second interface release. 0.1.0.0 keeps serving
├── current/ unfrozen. no .hash, generated output not committed
│ ├── interface.yaml imports pinned, and the vocabulary floor named here
│ └── com/rdk/hal/videosink/*.aidl
│
└── kvc/ the vocabulary. beside the versions, never inside one
├── 1.0.0/ a vocabulary release
│ ├── videosink.kvc the declaration
│ └── include/VideoSinkKeys.h generated header, committed
├── 1.1.0/ a second vocabulary release. 1.0.0 keeps serving
│ ├── videosink.kvc
│ └── include/VideoSinkKeys.h
└── current/ unfrozen. header regenerated on build, not committed
└── videosink.kvc
An interface release cuts a new a.b.c.d/ directory, copies the interface forward and writes a
.hash. Everything inside is fixed from that moment, and a consumer names it exactly:
imports: [email protected].
A vocabulary release creates a new kvc/x.y.z/ directory holding the declaration and its
generated header. No a.b.c.d/ directory is touched and no .hash moves. A consumer names it as a
floor: minimum: 1.1.0.
The two therefore release independently, by construction rather than by process. Today only
kvc/current/ exists — no vocabulary revision has been published yet, so the mechanism is
described and not exercised.
The version has three positions, each defined by the compatibility rule it corresponds to.
| Position | Moves when | Effect on a consumer built against an earlier version in the same major |
|---|---|---|
Major — 2.0.0
|
a key is removed, a key's meaning or type changes, or a closed-vocabulary member's meaning or number changes | an identifier it computed may no longer be served, or may now mean something else |
Minor — 1.2.0
|
a key, an event kind or a closed-vocabulary member is added | none. Every existing identifier is unchanged, and a key it does not know about it does not name |
Patch — 1.1.3
|
documentation only, such as a description or a population rule reworded | none. No identifier moves |
Because an identifier is derived from the composed path together with the unit and the kind, changing any of those three yields a different identifier rather than the same identifier meaning something new. That is why a unit change sits in the major position: the old identifier does not change meaning, it stops being served.
A floor holds within a major. minimum: 1.1.0 is satisfied by 1.1.0, by 1.2.0 and by 1.3.4,
and not by 2.0.0. The major position is therefore where a vocabulary is permitted to break, and an
interface frozen against a 1.x.y floor needs a new floor to follow the vocabulary past it.
flowchart LR
subgraph MOD["videosink/"]
direction TB
subgraph KVC["kvc/ — the vocabulary"]
direction TB
K10["1.0.0<br/>videosink.kvc<br/>include/VideoSinkKeys.h"]
K11["1.1.0<br/>videosink.kvc<br/>include/VideoSinkKeys.h"]
KC["current<br/>regenerated on build<br/>not committed"]
end
subgraph IFC["interface versions"]
direction TB
I1["0.1.0.0<br/>kvc: videosink >= 1.0.0"]
I2["0.2.0.0<br/>kvc: videosink >= 1.1.0"]
IC["current"]
end
end
I1 -.->|"floor"| K10
I2 -.->|"floor"| K11
style K10 fill:#ABEBC6,color:#1a1a1a,stroke:#1e7a4d
style K11 fill:#ABEBC6,color:#1a1a1a,stroke:#1e7a4d
style KC fill:#EAECEE,color:#1a1a1a,stroke:#7b7d7d
style I1 fill:#FCF3CF,color:#1a1a1a,stroke:#8a6d1f
style I2 fill:#FCF3CF,color:#1a1a1a,stroke:#8a6d1f
style IC fill:#EAECEE,color:#1a1a1a,stroke:#7b7d7d
style KVC fill:#F4FBF6,color:#1a1a1a,stroke:#1e7a4d
style IFC fill:#FFFDF3,color:#1a1a1a,stroke:#8a6d1f
style MOD fill:#FFFFFF,color:#1a1a1a,stroke:#7b7d7d
Publishing revision 1.2.0 creates kvc/1.2.0/ and touches no interface directory, so a key-set revision cannot release an interface even by accident. A frozen revision commits its generated header, so a consumer pinned to it reads the bytes the generator produced rather than a claim about them.
The two are not the same kind of statement, and the declarations say so. Imports pin exactly, because ABI compatibility is exact:
imports:
- [email protected]
- [email protected]A vocabulary is named as a floor, because adding a key is compatible by construction:
kvc:
vocabulary: videosink
minimum: 1.1.0A floor does not multiply the matrix. An interface frozen against 1.1.0 keeps serving when the vocabulary reaches 1.3.4, and a caller built from 1.3.0 names a key the older platform lacks and is told so on that value.
The capability question sits on the value, not the version. A consumer names a key, or passes null, and reads SUPPORTED, NOT_SUPPORTED or NOT_AVAILABLE per figure. instance is an attribute rather than part of an element's identifier for the same reason: how many decoders are live is a property of the moment, not of the contract. For an estate holding one contract across several SoCs with real capability variance, the per-value answer is what a version number was standing in for.
A feature profile binds a product to a revision. It is an instance of a vocabulary and names the revision it was written against, so it is the artefact a test suite binds to.
| States | Varies by | Owner | |
|---|---|---|---|
Vocabulary (.kvc) |
that supportsAVSync is a key, its type, and what it means |
vocabulary revision | the module |
Feature profile (hfp-*.yaml) |
that this product's video sink has supportsAVSync: true at 2 ms |
product | the integration |
Holding both in one file would couple a per-product value to a cross-product definition and force one release cadence on two owners who change for different reasons.
The cost. Two artefacts are reviewed and versioned rather than one. The remaining gaps are in Open items.
| Date | Author | Comments |
|---|---|---|
| 17th September 2026 | G. Weatherup | Initial revision |
| 17th September 2026 | G. Weatherup | Option C effort restated as two steps. |
| 21st September 2026 | G. Weatherup | The Android atom pattern studied as a precedent and not a candidate. |
| 22nd September 2026 | G. Weatherup | Rewritten against the Android 17 HAL tree. Both shapes Android uses are stated, with what each version bump carried, which teams changed shape and why, and Android's published guidance. Options are now A′, A, B (a sibling AIDL package, on the vehicle and gralloc precedent) and C. The release-count table and the Android atom section are withdrawn; the cost is per-release adoption, measured on the branch. A "What would change the answer" section replaces the decisions list. |
| 22nd September 2026 | G. Weatherup | The selector is long[] on the branch. The parameter type is no longer part of the decision; what remains is where the generated enum file lives. |
| 22nd September 2026 | G. Weatherup | A key-set release is a conscious freeze by the architecture team on its cadence, as an interface release is; continuous integration is not a requirement of it. |