Skip to content

Native C++ AMF reader (Config::CreateFromAMF) — ACES 1.x + ACES 2, rebased from AJA, validated bit-exact#1

Open
giardiello wants to merge 10 commits into
mainfrom
feature/amf-cpp-reader
Open

Native C++ AMF reader (Config::CreateFromAMF) — ACES 1.x + ACES 2, rebased from AJA, validated bit-exact#1
giardiello wants to merge 10 commits into
mainfrom
feature/amf-cpp-reader

Conversation

@giardiello

Copy link
Copy Markdown
Owner

Scope note: This PR targets my own fork's main, not AcademySoftwareFoundation/OpenColorIO. It exists to document and consolidate the AMF C++ reader work in one place. It is not an upstream submission — no CLA/TSC/Core-Library-Changes process has been started, and it should not be pushed upstream until we decide to. See "Path to upstream" below.

Summary

This branch turns the ACES Metadata File (AMF) reader into a first-class, in-process OCIO primitive: Config::CreateFromAMF() (plus a free CreateFromAMF() and Python bindings) that reads an AMF and returns a live OCIO Config implementing the AMF viewing pipeline, along with an AMFInfo describing how the pipeline maps onto the config.

It starts from the AJA prototype on chandyn/OpenColorIO@amf_to_config (tracked by upstream issue AcademySoftwareFoundation#1927), rebased cleanly onto current main (OCIO 2.6-dev), then extended for ACES 2 / OCIO 2.5+, given Python bindings, tests, and bit-exact numerical validation against the reference pyocioamf tool.

Authorship: the baseline import commit is authored by giardiello with Co-authored-by: credit to Chandy Navaratan and Bill Bowen (AJA); design guidance from Doug Walker. All commits are DCO signed-off.

Two independent version axes (important, easy to conflate)

  • AMF schema version — the file format: XML namespace amf:v1.0 vs amf:v2.0, SOPNode vs ASC_SOP. Drives parsing.
  • ACES transform versiontransformId:v1.5: (ACES 1.x) vs :v2.0: (ACES 2.x). Drives which reference config we resolve against.

They are orthogonal: an amf:v2.0 file can carry ACES-1.5 transforms. Config selection is keyed on the ACES transform version, never the schema namespace (verified — an amf:v2.0 + :v1.5: file correctly routes to the ACES 1.3 config).

What changed (by commit)

  1. Add C++ AMF reader — AJA baseline (38 commits squashed), applies cleanly onto current main. Expat-based parser, public AMFInfo, CreateFromAMF(), unit tests + data.
  2. Resolve transform IDs via amf_transform_idsElementMatchesTransformId() matches against the OCIO 2.5+ getInterchangeAttribute("amf_transform_ids"), with description-substring fallback for older configs. Also fixes a latent size_t/-1 sentinel bug in m_numLooksBeforeWorkingLocation and clears warnings.
  3. Support ACES 2 configs via auto-selected reference config — the reference config is chosen after parsing: if the AMF references any :v2.0: transform IDs, use the ACES 2 studio builtin, else the ACES 1.3 studio builtin (explicit configFilePath still overrides). Standard color spaces resolved via roles (works with configs that name e.g. CIE-XYZ-D65 differently); generated config version matches the reference config; interop IDs on copied color spaces are cleared before validation (and AMFInfo names re-pointed). Adds an ACES 2 end-to-end unit test.
  4. Add Config::CreateFromAMF factory — mirrors CreateFromFile/CreateFromBuiltinConfig.
  5. Python bindingsAMFInfo type + Config.CreateFromAMF(amfFilePath, configFilePath="") returning a (Config, AMFInfo) tuple.
  6. Python tests + hardeningAMFTest.py (ACES 2, ACES 1, missing-file), throw on unopenable file, fetch ACES space by name for no-input AMFs.
  7. Coding style — tabs → spaces per OCIO guidelines.
  8. Document aces-amf swap points — file-level map + NOTE(aces-amf) anchors marking what a future official ACES AMF C++ library could replace (XML parse/model/validation) vs. what stays OCIO-specific (the AMF→Config mapping).
  9. Validate aces-amf-lib swap points + spec-compliant fixture — prototyped the boundary end-to-end against the official Python aces-amf-lib v0.1.0; made the test fixture v2.0-spec-compliant (required uuid/dateTime).

Validation (bit-exact vs pyocioamf)

Cross-checked our CreateFromAMF config output against the established pyocioamf reference (baked CTF) on identical inputs:

Path Reference config Max abs diff
ACES 2 (:v2.0: transforms) builtin ACES 2 studio (auto-selected) 0.0
ACES 1 (:v1.5: transforms) merged all-views aces-v1.3-v2.0 config via configFilePath 0.0

Both exercise the full pipeline (input CSC/IDT → RGC look → CDL working-space sandwich → ODT / combined RRTODT), proving transforms are correctly applied, not merely resolved. All C++ AMFParser tests (3) and Python AMFTest cases (3) pass.

Relationship to the official ACES AMF library

The Academy's aces-amf library (Python today at ampas/aces-amf; a C++ version is planned) parses/validates/authors AMF. It would replace only the front-end (XML parse → object model + validation); the AMF→OCIO mapping (~70% of this code) stays ours regardless. Validated against aces-amf-lib v0.1.0: its object model exposes everything our mapping needs. Tradeoff: the library is currently AMF-schema-v2.0-only and strictly validates required fields, whereas this expat reader is more permissive and also reads schema v1.0. Swap points are annotated in-code with NOTE(aces-amf).

Design notes / open decisions

  • Config selection: auto-switch between the two ACES studio builtins by transform-ID version. A single merged "all-views" config (as produced by the OCIO_ACES_MERGER_AND_PARSER project) resolves both generations and handles mixed AMFs — if such a config ever ships as an OCIO builtin, CreateFromAMF could default to it and drop auto-switching.
  • AMF v2-only option: we could restrict to AMF schema v2.0 (matching the official library). This is orthogonal to ACES version — ACES 1.x transforms remain supported. Would require migrating the v1.0-schema fixtures.
  • AMFInfo kept as the AJA struct (stable); an RcPtr-with-accessors rework is a public-API decision to defer to upstream discussion.

Test plan

  • test_cpu_exec --run_only AMFParser (ACES 1 + ACES 2 + working-location)
  • Python AMFTest (ACES 2, ACES 1, missing-file error)
  • Bit-exact vs pyocioamf (ACES 1 and ACES 2)
  • Builds against OCIO 2.6-dev (core lib + tests + Python module)

Path to upstream (when/if we choose)

Requires: EasyCLA signed; Core Library Changes process via issue AcademySoftwareFoundation#1927 (design proposal + TSC); 2 Committer approvals + green CI; final PR linked to AcademySoftwareFoundation#1927. Not started.

Made with Cursor

giardiello and others added 10 commits July 5, 2026 10:06
Import the AMFParser C++ implementation, public API, and unit tests from
the AJA amf_to_config branch:
  https://github.com/chandyn/OpenColorIO/tree/amf_to_config

This squashes 38 commits from AJA into a single baseline that applies
cleanly onto current main. It adds:
  - CreateFromAMF(): builds an in-memory OCIO Config from an AMF file
  - An expat-based AMFParser (fileformats/amf/AMFParser.{h,cpp})
  - The public AMFInfo struct (OpenColorTypes.h)
  - Unit tests and AMF test data

Design guidance on the original work came from Doug Walker and Bill Bowen.
Tracks issue AcademySoftwareFoundation#1927 (Add a C++ implementation for ACES Metadata File support).

Co-authored-by: Chandy Navaratan <chandy@raje.sh>
Co-authored-by: Bill Bowen <bilbo@aja.com>
Signed-off-by: giardiello <giardiello@me.com>
Phase 0 (baseline hardening) + Phase 1 (config-agnostic ID resolution):

- Fix unused-variable warning in HandleLookTransformStartElement.
- Fix latent workingLocation bug: m_numLooksBeforeWorkingLocation was a
  size_t with a -1 sentinel, so the "< 0" checks never fired. Changed to a
  signed type, which also clears the sign-compare warnings.
- Add ElementMatchesTransformId() which matches an ACES Transform ID against
  the OCIO 2.5+ "amf_transform_ids" interchange attribute, falling back to
  description-substring matching for older configs. searchColorSpaces,
  searchViewTransforms and searchLookTransforms now use it.
- m_useAmfIds is set from the reference config profile version (>= 2.5).

Signed-off-by: giardiello <giardiello@me.com>
Phase 2 (remove hardcoded config/version assumptions):

- The reference config is now chosen after parsing: if the AMF references any
  ACES v2.0 transform IDs, the ACES 2 studio builtin config is used; otherwise
  the ACES 1.3 studio config. An explicit config path still overrides this.
- initAMFConfig resolves the standard color spaces (scene_linear, color_timing,
  cie_xyz_d65_interchange, data) through roles so it works with configs that
  name them differently (e.g. the ACES 2 CIE-XYZ-D65 space).
- The generated config version now matches the reference config version instead
  of a hardcoded 2.3, so ACES 2 color spaces validate.
- Clear interop IDs on color spaces copied from the reference config (their
  interop targets are not present in the minimal generated config), re-pointing
  AMFInfo names afterwards.

Adds an ACES 2 end-to-end unit test (S-Log3 -> RGC look + CDL -> P3-D65 SDR).

Signed-off-by: giardiello <giardiello@me.com>
Phase 4 (idiomatic public API): add a static Config::CreateFromAMF() factory
mirroring CreateFromFile/CreateFromBuiltinConfig, so callers can build an AMF
config the same way as any other config. The existing free CreateFromAMF()
function is retained. The ACES 2 unit test now exercises the new factory.

Signed-off-by: giardiello <giardiello@me.com>
Phase 5 (Python bindings):

- Bind the AMFInfo struct as a read-only type exposing clipIdentifier,
  clipColorSpaceName, inputColorSpaceName, numLooksApplied, displayName and
  viewName.
- Add Config.CreateFromAMF(amfFilePath, configFilePath="") which returns a
  (Config, AMFInfo) tuple (Python has no out-parameters).

Verified from Python: an ACES 2 AMF auto-selects the ACES 2 studio config and
produces a working processor, while an ACES 1 AMF selects the ACES 1.3 config.

Signed-off-by: giardiello <giardiello@me.com>
Phase 6 (tests + robustness):

- Add AMFTest.py covering the ACES 2 path, the ACES 1 path and the missing
  file error, registered in the Python test suite.
- Throw when the AMF file cannot be opened, instead of silently producing a
  degenerate config (matches Config::CreateFromFile behaviour).
- Resolve the ACES2065-1 color space directly by name when the AMF has no
  input transform (it cannot be found via amf_transform_ids).

Signed-off-by: giardiello <giardiello@me.com>
Replace the remaining hard tabs (inherited from the original AJA branch) with
spaces in AMFParser.cpp and AMFParser_tests.cpp, per the OpenColorIO coding
style guide.

Signed-off-by: giardiello <giardiello@me.com>
The Academy is developing an official ACES AMF library (Python today, with a
C++ version planned). Add a file-level map plus inline NOTE(aces-amf) anchors
marking exactly which parts of the reader could later be delegated to that
library (XML parsing, the intermediate model, and schema/semantic validation)
and which parts are OCIO-specific and would stay (the AMF -> Config mapping and
transform-ID resolution against the OCIO config). No behavior change.

Signed-off-by: giardiello <giardiello@me.com>
…pliant

Prototyped the documented integration boundary end-to-end against the official
Python aces-amf-lib v0.1.0: parse -> object model -> this reader's mapping
logic -> OCIO processor. Confirmed the library exposes every field process()
needs as typed values, and recorded the accessor shape plus a key tradeoff
(the library is AMF v2.0-only and strictly validates required fields, whereas
this expat reader is more permissive and also reads v1.0).

The validation revealed the aces2 test fixture was missing v2.0-required fields
(amfInfo/pipelineInfo/clipId uuid and dateTime); add them so the fixture is
spec-compliant and parses under the official library's schema validation.

Signed-off-by: giardiello <giardiello@me.com>
The slogtopq_1.clf / slogtopq_wlook_1.clf fixtures inherited from the AJA
baseline are ~7.7 MB baked 3D LUTs (274k lines each) that dominated the diff.
Untrack them and add a .gitignore entry; they remain on disk for the local
slogtopq* tests. Small, self-contained fixtures should replace them before any
upstream submission.

Signed-off-by: giardiello <giardiello@me.com>
@davisadam10

Copy link
Copy Markdown

@giardiello - Notes

  1. In the aces-amf we decided to drop support for AMF v1.x all together as it was just a spec and no one ever implemented it. Would it not be cleaner to do the same here? if no one ever used it, its been superseeded then why add new functionality to support something thats already dead or never even lived

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants