diff --git a/.appveyor.yml b/.appveyor.yml index bc82bea47..3238cd264 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: Apache-2.0 -version: 1.1.41.{build} +version: 1.1.42.{build} image: Visual Studio 2017 diff --git a/.azure-pipelines/openxr-sdk-source.yml b/.azure-pipelines/openxr-sdk-source.yml index 13285d044..76edef4e6 100644 --- a/.azure-pipelines/openxr-sdk-source.yml +++ b/.azure-pipelines/openxr-sdk-source.yml @@ -21,7 +21,7 @@ stages: - job: loader_docs pool: vmImage: "ubuntu-latest" - container: khronosgroup/docker-images:openxr.20240805@sha256:05e900737234daf09d29a1c525a017ea8cc54a0d1f808569488e9ae6018053f2 + container: khronosgroup/docker-images:openxr.20240924@sha256:3d595e68d21b2bba12cb7bbfa8cb135f66a2d9b97efeda6de3c6c8412163c4b7 steps: - script: make loader workingDirectory: specification @@ -34,7 +34,7 @@ stages: - job: archive pool: vmImage: "ubuntu-latest" - container: khronosgroup/docker-images:openxr.20240805@sha256:05e900737234daf09d29a1c525a017ea8cc54a0d1f808569488e9ae6018053f2 + container: khronosgroup/docker-images:openxr.20240924@sha256:3d595e68d21b2bba12cb7bbfa8cb135f66a2d9b97efeda6de3c6c8412163c4b7 steps: - script: make loader workingDirectory: specification @@ -62,7 +62,7 @@ stages: presentationBackend: wayland pool: vmImage: "ubuntu-latest" - container: khronosgroup/docker-images:openxr-pregenerated-sdk.20240412@sha256:0006d0994219477d460953f0eda1fa6053f770c68da23b190be7c0d067f078b5 + container: khronosgroup/docker-images:openxr-pregenerated-sdk.20240924@sha256:5ddce09400cc7e17a81e65a0e7e2c26376f21e0844a6cf932270c2955213eef6 # container: sha256:845e19e59f22c29fedc33dcaa4f3c5f8aa44e367283ef955c7549239cf9a46d2 steps: - task: DownloadPipelineArtifact@2 diff --git a/.azure-pipelines/shared/build_jobs.yml b/.azure-pipelines/shared/build_jobs.yml index c9a7d65a2..2152ba7e8 100644 --- a/.azure-pipelines/shared/build_jobs.yml +++ b/.azure-pipelines/shared/build_jobs.yml @@ -26,7 +26,7 @@ jobs: presentationBackend: wayland pool: vmImage: "ubuntu-latest" - container: khronosgroup/docker-images:openxr-sdk.20240805@sha256:bbc790ccdc56f291d72877c6fdb825edbb9c3222efb63dc4d3ac34e3bf13dd7a + container: khronosgroup/docker-images:openxr-sdk.20240924@sha256:5e6a6f5d72dc4a20d5c33f12550fdd9b6a1206e94d6cf1382e0697a5726c424c steps: # First build as debug diff --git a/.azure-pipelines/shared/check_clang_format.yml b/.azure-pipelines/shared/check_clang_format.yml index c2e3b2071..ba44d3482 100644 --- a/.azure-pipelines/shared/check_clang_format.yml +++ b/.azure-pipelines/shared/check_clang_format.yml @@ -6,7 +6,7 @@ jobs: displayName: "clang-format" pool: vmImage: "ubuntu-latest" - container: khronosgroup/docker-images:openxr.20240805@sha256:05e900737234daf09d29a1c525a017ea8cc54a0d1f808569488e9ae6018053f2 + container: khronosgroup/docker-images:openxr.20240924@sha256:3d595e68d21b2bba12cb7bbfa8cb135f66a2d9b97efeda6de3c6c8412163c4b7 steps: - checkout: self diff --git a/.azure-pipelines/shared/check_file_format.yml b/.azure-pipelines/shared/check_file_format.yml index b135fca73..a55c7249e 100644 --- a/.azure-pipelines/shared/check_file_format.yml +++ b/.azure-pipelines/shared/check_file_format.yml @@ -5,7 +5,7 @@ jobs: displayName: "Check file formatting" pool: vmImage: "ubuntu-latest" - container: khronosgroup/docker-images:openxr-sdk.20240805@sha256:bbc790ccdc56f291d72877c6fdb825edbb9c3222efb63dc4d3ac34e3bf13dd7a + container: khronosgroup/docker-images:openxr-sdk.20240924@sha256:5e6a6f5d72dc4a20d5c33f12550fdd9b6a1206e94d6cf1382e0697a5726c424c steps: - script: ./file_format.sh diff --git a/.azure-pipelines/shared/codespell.yml b/.azure-pipelines/shared/codespell.yml index 974bdd228..e9e2c2de5 100644 --- a/.azure-pipelines/shared/codespell.yml +++ b/.azure-pipelines/shared/codespell.yml @@ -6,7 +6,7 @@ jobs: displayName: "codespell" pool: vmImage: "ubuntu-latest" - container: khronosgroup/docker-images:openxr-sdk.20240805@sha256:bbc790ccdc56f291d72877c6fdb825edbb9c3222efb63dc4d3ac34e3bf13dd7a + container: khronosgroup/docker-images:openxr-sdk.20240924@sha256:5e6a6f5d72dc4a20d5c33f12550fdd9b6a1206e94d6cf1382e0697a5726c424c steps: - checkout: self diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 9518599b8..7b1963392 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -25,7 +25,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Get modern CMake and Ninja - uses: "lukka/get-cmake@v3.30.3" + uses: "lukka/get-cmake@v3.30.5" # Do this before building aar since it affects the version - name: Touch SNAPSHOT marker file @@ -47,7 +47,7 @@ jobs: steps: - uses: "actions/checkout@v4" - name: "Get modern CMake and Ninja" - uses: "lukka/get-cmake@v3.30.3" + uses: "lukka/get-cmake@v3.30.5" - name: "set up JDK 11" uses: "actions/setup-java@v4" with: diff --git a/.github/workflows/check_clang_format_and_codespell.yml b/.github/workflows/check_clang_format_and_codespell.yml index b8dedc16f..36ba40b7d 100644 --- a/.github/workflows/check_clang_format_and_codespell.yml +++ b/.github/workflows/check_clang_format_and_codespell.yml @@ -10,7 +10,7 @@ jobs: clang-format: runs-on: ubuntu-latest container: - image: khronosgroup/docker-images:openxr.20240805@sha256:05e900737234daf09d29a1c525a017ea8cc54a0d1f808569488e9ae6018053f2 + image: khronosgroup/docker-images:openxr.20240924@sha256:3d595e68d21b2bba12cb7bbfa8cb135f66a2d9b97efeda6de3c6c8412163c4b7 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/msvc-build-preset.yml b/.github/workflows/msvc-build-preset.yml index a0c18b999..5b94b9500 100644 --- a/.github/workflows/msvc-build-preset.yml +++ b/.github/workflows/msvc-build-preset.yml @@ -32,9 +32,11 @@ jobs: - uses: actions/checkout@v4 with: lfs: true + fetch-tags: "${{ !github.event.release }}" + fetch-depth: "${{ github.event.release && '0' || '1'}}" - name: Get modern CMake and Ninja - uses: lukka/get-cmake@v3.30.3 + uses: lukka/get-cmake@v3.30.5 - name: Add msbuild to PATH uses: microsoft/setup-msbuild@v2 diff --git a/.reuse/dep5 b/.reuse/dep5 index ac74aa843..ac059d994 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -50,7 +50,7 @@ Files: src/external/tinygltf/README.md src/external/tinygltf/tiny_gltf.h Copyright: 2015-Present, Syoyo Fujita, Aurélien Chatelain and many contributors License: MIT -Comment: Unmodified, vendored copy of a subset of the tiny-gltf repo v2.8.9 +Comment: Unmodified, vendored copy of a subset of the tiny-gltf repo v2.9.3 Files: src/external/d3dx12/* Copyright: Copyright (c) Microsoft Corporation. diff --git a/CHANGELOG.SDK.md b/CHANGELOG.SDK.md index 28405f45a..53f329890 100644 --- a/CHANGELOG.SDK.md +++ b/CHANGELOG.SDK.md @@ -21,6 +21,56 @@ along with any public pull requests that have been accepted. In this repository in particular, since it is primarily software, pull requests may be integrated as they are accepted even between periodic updates. +## OpenXR SDK 1.1.42 (2024-10-25) + +This release updates a vendor extension with renamed enumerants, adds +architecture support for `loong64`, and delivers substantial improvements and +fixes to the XML registry, particularly the description of interaction profile + +- Registry + - Fix: Update schema to reflect that `XrPathString_t` should allow dash in + interaction profile paths. + ([internal MR 3493](https://gitlab.khronos.org/openxr/openxr/merge_requests/3493)) + - Fix: `XR_VARJO_xr4_controller_interaction` did not properly define its + interaction profile in XML. + ([internal MR 3493](https://gitlab.khronos.org/openxr/openxr/merge_requests/3493), + [internal MR 3548](https://gitlab.khronos.org/openxr/openxr/merge_requests/3548)) + - Fix: Correct XML description of OpenXR 1.1 related additions to the promoted + Meta Touch Plus, Touch Pro, and Touch (Rift CV1) controller interaction + profiles. + ([internal MR 3513](https://gitlab.khronos.org/openxr/openxr/merge_requests/3513), + [internal issue 2350](https://gitlab.khronos.org/openxr/openxr/issues/2350), + [internal issue 2375](https://gitlab.khronos.org/openxr/openxr/issues/2375)) + - Fix: Add missing XML description of `EXT_hand_interaction` additions to several + interaction profiles, and add comments to clarify where profile additions + should be located. + ([internal MR 3517](https://gitlab.khronos.org/openxr/openxr/merge_requests/3517), + [internal MR 3541](https://gitlab.khronos.org/openxr/openxr/merge_requests/3541), + [internal MR 3552](https://gitlab.khronos.org/openxr/openxr/merge_requests/3552)) + - Fix: Corrections to the Schema chapter of the style guide. + ([internal MR 3521](https://gitlab.khronos.org/openxr/openxr/merge_requests/3521)) + - Improvement: Small consistency clean-up. + ([internal MR 3512](https://gitlab.khronos.org/openxr/openxr/merge_requests/3512)) + - Improvement: Clean up `.rnc` schema to improve readability. + ([internal MR 3521](https://gitlab.khronos.org/openxr/openxr/merge_requests/3521)) + - Scripts: Improve `update_version.py` used in release process. + ([internal MR 3543](https://gitlab.khronos.org/openxr/openxr/merge_requests/3543)) + - Update: Change naming convention in `XR_HTC_facial_expression`: rename + `XR_LIP_EXPRESSION_MOUTH_SMILE_RIGHT_HTC` to + `XR_LIP_EXPRESSION_MOUTH_RAISER_RIGHT_HTC`, + `XR_LIP_EXPRESSION_MOUTH_SMILE_LEFT_HTC` to + `XR_LIP_EXPRESSION_MOUTH_RAISER_LEFT_HTC`, + `XR_LIP_EXPRESSION_MOUTH_SAD_RIGHT_HTC` to + `XR_LIP_EXPRESSION_MOUTH_STRETCHER_RIGHT_HTC` and + `XR_LIP_EXPRESSION_MOUTH_SAD_LEFT_HTC` to + `XR_LIP_EXPRESSION_MOUTH_STRETCHER_LEFT_HTC`, providing the old names as + compatibility aliases. + ([internal MR 3408](https://gitlab.khronos.org/openxr/openxr/merge_requests/3408)) +- SDK + - Loader: Fix build error on `loong64`, and add `loong64` in the architecture + table in the loader documentation. + ([OpenXR-SDK-Source PR 479](https://github.com/KhronosGroup/OpenXR-SDK-Source/pull/479)) + ## OpenXR SDK 1.1.41 (2024-09-25) This release features several new vendor extensions, as well as some small diff --git a/changes/sdk/pr.479.gh.OpenXR-SDK-Source.md b/changes/sdk/pr.479.gh.OpenXR-SDK-Source.md deleted file mode 100644 index 123559d52..000000000 --- a/changes/sdk/pr.479.gh.OpenXR-SDK-Source.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -- pr.479.gh.OpenXR-SDK-Source ---- -Loader: Fix build error on loong64, and add loong64 in the architecture table in the loader documentation. diff --git a/maintainer-scripts/common.sh b/maintainer-scripts/common.sh index c2d8373e0..9335d93ab 100644 --- a/maintainer-scripts/common.sh +++ b/maintainer-scripts/common.sh @@ -300,6 +300,7 @@ getConformanceFilenames() { maintainer-scripts/check-changelog-fragments.sh \ specification/.gitignore \ specification/registry/*.xml \ + specification/config \ specification/scripts \ specification/Makefile \ specification/README.md \ diff --git a/specification/Makefile b/specification/Makefile index f9e92a09a..76ae74a74 100644 --- a/specification/Makefile +++ b/specification/Makefile @@ -39,7 +39,7 @@ endif VERSIONS := XR_VERSION_1_0 XR_VERSION_1_1 XR_LOADER_VERSION_1_0 VERSIONOPTIONS := $(foreach version,$(VERSIONS),-feature $(version)) -SPECREVISION = 1.1.41 +SPECREVISION = 1.1.42 REVISION_COMPONENTS = $(subst ., ,$(SPECREVISION)) MAJORMINORVER = $(word 1,$(REVISION_COMPONENTS)).$(word 2,$(REVISION_COMPONENTS)) @@ -334,7 +334,8 @@ html: $(HTMLSPEC) ASCIIDOCTOR_TARGETS += $(HTMLSPEC) # Target-specific variables and deps customizing the AsciiDoctor rule -$(HTMLSPEC): ATTRIBOPTS += -a sectanchors +# EXTRAATRIBS is for build-time customization +$(HTMLSPEC): ATTRIBOPTS += -a sectanchors $(EXTRAATTRIBS) $(HTMLSPEC): ADOCOPTS += $(ADOCHTMLOPTS) $(HTMLSPEC): $(COMMONDOCS) @@ -349,7 +350,8 @@ pdfA4: $(PDFA4SPEC) ASCIIDOCTOR_TARGETS += $(PDFSPEC) $(PDFA4SPEC) # Target-specific variables and deps customizing the AsciiDoctor rule -$(PDFSPEC) $(PDFA4SPEC): BACKEND_ARGS=--backend pdf --require asciidoctor-pdf -a compress --require ./scripts/pdf-index-customizer.rb +# EXTRAATRIBS is for build-time customization +$(PDFSPEC) $(PDFA4SPEC): BACKEND_ARGS=--backend pdf --require asciidoctor-pdf -a compress --require ./scripts/pdf-index-customizer.rb $(EXTRAATTRIBS) $(PDFSPEC): PAGESIZE=LETTER $(PDFA4SPEC): PAGESIZE=A4 $(PDFSPEC) $(PDFA4SPEC): $(COMMONDOCS) diff --git a/specification/registry/xr.xml b/specification/registry/xr.xml index be125c34d..17ea68110 100644 --- a/specification/registry/xr.xml +++ b/specification/registry/xr.xml @@ -135,7 +135,7 @@ maintained in the default branch of the Khronos OpenXR GitHub project. updates them automatically by processing a line at a time. --> // OpenXR current version number. -#define XR_CURRENT_API_VERSION XR_MAKE_VERSION(1, 1, 41) +#define XR_CURRENT_API_VERSION XR_MAKE_VERSION(1, 1, 42) + + + + + + + + + + + + + + + + + + + + + + + - + @@ -8474,8 +8499,6 @@ typedef XrResult (XRAPI_PTR *PFN_xrCreateApiLayerInstance)( - - @@ -8491,15 +8514,13 @@ typedef XrResult (XRAPI_PTR *PFN_xrCreateApiLayerInstance)( - - - + @@ -8588,16 +8609,22 @@ typedef XrResult (XRAPI_PTR *PFN_xrCreateApiLayerInstance)( - + + + + + + - + + @@ -8622,13 +8649,13 @@ typedef XrResult (XRAPI_PTR *PFN_xrCreateApiLayerInstance)( - - + + @@ -8687,11 +8714,11 @@ typedef XrResult (XRAPI_PTR *PFN_xrCreateApiLayerInstance)( - + @@ -8715,11 +8742,11 @@ typedef XrResult (XRAPI_PTR *PFN_xrCreateApiLayerInstance)( - + @@ -8743,11 +8770,11 @@ typedef XrResult (XRAPI_PTR *PFN_xrCreateApiLayerInstance)( - - + + @@ -8776,11 +8803,11 @@ typedef XrResult (XRAPI_PTR *PFN_xrCreateApiLayerInstance)( - + @@ -8804,14 +8831,13 @@ typedef XrResult (XRAPI_PTR *PFN_xrCreateApiLayerInstance)( - - + - + @@ -8829,10 +8855,9 @@ typedef XrResult (XRAPI_PTR *PFN_xrCreateApiLayerInstance)( - - + @@ -8849,12 +8874,11 @@ typedef XrResult (XRAPI_PTR *PFN_xrCreateApiLayerInstance)( - + - @@ -8884,7 +8908,7 @@ typedef XrResult (XRAPI_PTR *PFN_xrCreateApiLayerInstance)( - + @@ -8895,7 +8919,7 @@ typedef XrResult (XRAPI_PTR *PFN_xrCreateApiLayerInstance)( - + @@ -8909,7 +8933,6 @@ typedef XrResult (XRAPI_PTR *PFN_xrCreateApiLayerInstance)( - @@ -8918,7 +8941,6 @@ typedef XrResult (XRAPI_PTR *PFN_xrCreateApiLayerInstance)( - @@ -9757,6 +9779,11 @@ typedef XrResult (XRAPI_PTR *PFN_xrCreateApiLayerInstance)( + @@ -10202,7 +10229,7 @@ typedef XrResult (XRAPI_PTR *PFN_xrCreateApiLayerInstance)( - + @@ -10223,6 +10250,12 @@ typedef XrResult (XRAPI_PTR *PFN_xrCreateApiLayerInstance)( + + + + + + @@ -10653,8 +10686,20 @@ typedef XrResult (XRAPI_PTR *PFN_xrCreateApiLayerInstance)( - + + + + + + + + + + + + + @@ -11236,6 +11281,17 @@ typedef XrResult (XRAPI_PTR *PFN_xrCreateApiLayerInstance)( + + + + + + + + + + + @@ -11343,6 +11399,11 @@ typedef XrResult (XRAPI_PTR *PFN_xrCreateApiLayerInstance)( + @@ -11361,12 +11422,6 @@ typedef XrResult (XRAPI_PTR *PFN_xrCreateApiLayerInstance)( - - - - - - @@ -12361,6 +12416,17 @@ typedef XrResult (XRAPI_PTR *PFN_xrCreateApiLayerInstance)( + + + + + + + + + + + @@ -12589,6 +12655,40 @@ typedef XrResult (XRAPI_PTR *PFN_xrCreateApiLayerInstance)( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -15867,6 +15967,11 @@ typedef XrResult (XRAPI_PTR *PFN_xrCreateApiLayerInstance)( + + + + + diff --git a/specification/scripts/docgenerator.py b/specification/scripts/docgenerator.py index 72960e490..e31f5a6e5 100644 --- a/specification/scripts/docgenerator.py +++ b/specification/scripts/docgenerator.py @@ -34,6 +34,7 @@ class _Enumerant: comment: str extname: Optional[str] = None deprecated: Optional[str] = None + alias: Optional[str] = None def orgLevelKey(name): @@ -447,7 +448,6 @@ def genStruct(self, typeinfo, typeName, alias): self.writeInclude('structs', typeName, body) - def _maybe_return_enumerant_object_for_table( self, elems, elem, missing_comments: List[str] ) -> Optional[_Enumerant]: @@ -466,15 +466,21 @@ def _maybe_return_enumerant_object_for_table( return comment = elem.get("comment") + alias = elem.get("alias") if comment is None: if name.endswith("_UNKNOWN") and num_val == 0: # This is a placeholder for 0-initialization to be clearly invalid. # Just skip this silently return - # Skip but record this in case it is an odd-one-out missing - # a comment. - missing_comments.append(name) - return + if alias is not None: + # oh it's an alias. That's fine. We can generate a comment. + comment = f"Alias for ename:{alias}" + + else: + # Skip but record this in case it is an odd-one-out missing + # a comment. + missing_comments.append(name) + return assert num_val is not None @@ -531,13 +537,18 @@ def genEnumTable(self, groupinfo, groupName): if maybe_data: values.append(maybe_data) - if values: - # If any had a comment, output it. + if values and any(v.alias is None for v in values): + # If any had a (non-alias) comment, output it. if missing_comments: - self.logMsg('warn', 'The following values for', groupName, - 'were omitted from the table due to missing comment attributes:', - ', '.join(missing_comments)) + # Warn if it looks like we have comments, but some were missed. + if len(missing_comments) < len(values): + self.logMsg('warn', 'The following value(s) for', groupName, + 'were omitted from the table due to missing comment attributes:', + ', '.join(missing_comments)) + else: + self.logMsg('warn', 'The enumeration ', groupName, + 'appears to be missing comments for most of its elements') group_type = groupinfo.elem.get('type') if groupName == self.result_type: diff --git a/specification/scripts/update_version.py b/specification/scripts/update_version.py index 423737452..a6d4bbc9e 100755 --- a/specification/scripts/update_version.py +++ b/specification/scripts/update_version.py @@ -14,6 +14,15 @@ import fileinput import configparser + +def update_makefile(fn, spec_version): + for line in fileinput.input(fn, inplace=True): + if 'SPECREVISION = ' in line: + print('SPECREVISION = %s.%s.%s' % spec_version) + else: + print(line, end='') + + if __name__ == "__main__": # Get the current version from the 'current_version.ini' file. @@ -21,8 +30,6 @@ config = configparser.ConfigParser() config.read_file(fp) versions = config['Version'] - major_version = versions['MAJOR'] - minor_version = versions['MINOR'] spec_version = (versions['MAJOR'], versions['MINOR'], versions['PATCH']) # Now update the version in the appropriate places in the @@ -30,22 +37,16 @@ # print('Replacing version lines in the registry') for line in fileinput.input('registry/xr.xml', inplace=True): - printed = False - if 'XR_CURRENT_API_VERSION' in line: - if 'XR_MAKE_VERSION' in line: - printed = True - print('#define XR_CURRENT_API_VERSION XR_MAKE_VERSION(%s, %s, %s)' % spec_version) - if not printed: - print(f"{line}", end='') + if 'XR_CURRENT_API_VERSION' in line and 'XR_MAKE_VERSION' in line: + print('#define XR_CURRENT_API_VERSION XR_MAKE_VERSION(%s, %s, %s)' % spec_version) + else: + print(line, end='') # Now update the version in the appropriate places in the # specification make file (Makefile). # print('Replacing version lines in the specification Makefile') - for line in fileinput.input('Makefile', inplace=True): - printed = False - if 'SPECREVISION = ' in line: - printed = True - print('SPECREVISION = %s.%s.%s' % spec_version) - if not printed: - print(f"{line}", end='') + update_makefile('Makefile', spec_version) + + print('Replacing version lines in the CTS Usage Makefile') + update_makefile('../src/conformance/usage/Makefile', spec_version) diff --git a/src/scripts/interaction_profile_processor.py b/src/scripts/interaction_profile_processor.py index 6313248c2..7dcaad492 100644 --- a/src/scripts/interaction_profile_processor.py +++ b/src/scripts/interaction_profile_processor.py @@ -19,23 +19,33 @@ def _format_conjunction(and_terms): if len(and_terms) > 1: - return f"({'+'.join(and_terms)})" + return f"({'+'.join(sorted(and_terms))})" assert and_terms return tuple(and_terms)[0] def _repr_conjunction(and_terms): - terms = ", ".join(f"'{t}'" for t in and_terms) + terms = ", ".join(f"'{t}'" for t in sorted(and_terms)) return "".join(("{", terms, "}")) FrozenAvailability = Tuple[Tuple[str, ...], ...] + class Availability: """ Information on when something is available. In 'disjunctive normal form' - an OR of ANDs. + + >>> Availability([{'a'}, {'b'}]) + [{'a'}, {'b'}] + + >>> Availability([{'a'}, {'a', 'b'}]) + [{'a'}] + + >>> Availability([{'a', 'b'}, {'a'}]) + [{'a'}] """ def __init__(self, conjunctions: Iterable[Set[str]]): @@ -54,16 +64,107 @@ def add(self, features: Set[str]) -> bool: Add a new set of features that would make this available. Return true if this feature set is redundant given what already exists. + + >>> Availability([{'a'}]).add({'a', 'b'}) + True + + >>> Availability([{'a'}]).add({'b'}) + False + + >>> a = Availability([{'a'}]) + >>> a.add({'b'}) + False + >>> a + [{'a'}, {'b'}] + + >>> a = Availability([{'a'}]) + >>> a.add({'a', 'b'}) + True + >>> a + [{'a'}] + + >>> a = Availability([{'a', 'b'}]) + >>> a.add({'a'}) + False + >>> a + [{'a'}] """ return self._add_impl(frozenset(features)) def merge(self, other: 'Availability') -> bool: - """Merge two availabilities.""" + """Merge two availabilities (by OR).""" redundant = [self._add_impl(condition) for condition in other.conjunctions] return all(redundant) + def merged(self, other: 'Availability') -> 'Availability': + """ + Return the results of merging without changing this object. + + + >>> Availability([{'a'}]).merged(Availability([{'b', 'c'}])) + [{'a'}, {'b', 'c'}] + + >>> Availability([{'a'}]).merged(Availability([{'a', 'b'}, {'b', 'c'}])) + [{'a'}, {'b', 'c'}] + + >>> Availability([{'a'}, {'b', 'c'}]).merged(Availability([{'a', 'b'}])) + [{'a'}, {'b', 'c'}] + """ + clone = deepcopy(self) + clone.merge(other) + return clone + + def anded(self, other: 'Availability') -> 'Availability': + """ + Return the boolean AND of this and another availability. + + >>> Availability([{'a'}]).anded(Availability([{'b', 'c'}])) + [{'a', 'b', 'c'}] + + + >>> Availability([{'a'}]).anded(Availability([{'a', 'b'}])) + [{'a', 'b'}] + + >>> # a+b+c drops out because a+b is sufficient + >>> Availability([{'a'}]).anded(Availability([{'a', 'b'}, {'b', 'c'}])) + [{'a', 'b'}] + + >>> Availability([{'a'}]).anded(Availability([{'b'}, {'c'}])) + [{'a', 'b'}, {'a', 'c'}] + + >>> Availability([{'a'}]).anded(Availability([{'b', 'c'}, {'d', 'e'}])) + [{'a', 'b', 'c'}, {'a', 'd', 'e'}] + + >>> Availability([{'a', 'b'}]).anded(Availability([{'c'}, {'d'}])) + [{'a', 'b', 'c'}, {'a', 'b', 'd'}] + + >>> Availability([{'a'}]).anded(Availability([])) + [] + + >>> Availability([]).anded(Availability([{'a'}])) + [] + """ + ret = Availability([]) + + for self_term in self.conjunctions: + for other_term in other.conjunctions: + ret._add_impl(self_term.union(other_term)) + + return ret + def test(self, present_features: Set[str]) -> bool: - """See if the provided features satisfy any of the conjunctions.""" + """ + See if the provided features satisfy any of the conjunctions. + + >>> Availability([{'a'}]).test({'b'}) + False + + >>> Availability([{'a'}]).test({'a'}) + True + + >>> Availability([]).test({'a'}) + False + """ for condition in self.conjunctions: if condition.issubset(present_features): return True @@ -76,27 +177,14 @@ def is_consistent(self) -> bool: if len(set(self.conjunctions)) < len(self.conjunctions): # got a dupe return False - for a, b in itertools.permutations(self.conjunctions): + for a, b in itertools.permutations(self.conjunctions, 2): if a.issubset(b): # One condition is a subset of another return False return True - def issubset(self, potential_superset: "Availability") -> bool: - for condition in self.conjunctions: - for other_condition in potential_superset.conjunctions: - if condition.issubset(other_condition): - return True - return False - - def issuperset(self, other: "Availability") -> bool: - for condition in self.conjunctions: - for other_condition in other.conjunctions: - if condition.issuperset(other_condition): - return True - return False - def make_frozen(self) -> FrozenAvailability: + """Convert to a tuple of term tuples.""" terms = [] for condition in self.conjunctions: terms.append(tuple(sorted(condition))) @@ -107,21 +195,34 @@ def as_normalized_symbol(self) -> str: for c in sorted(self.conjunctions)] return "_or_".join(conjs) + def cleaned(self, cleaner) -> 'Availability': + """Return an availability where each term has been filtered by your function.""" + ret = Availability([]) + for term in self.conjunctions: + ret.add(set(cleaner(term))) + return ret + def _add_impl(self, features: FrozenSet[str]) -> bool: if features in self.conjunctions: return True - # these we can't be sure which is right - redundant = any(features.issubset(c) or features.issuperset(c) for c in self.conjunctions) - self.conjunctions.append(features) - return redundant + # Do not add this term if we are a superset of any existing term + # e.g. A, A+B -> A + if any(features.issuperset(c) for c in self.conjunctions): + return True + + # Drop any terms that are a superset of the new term + remaining = [term for term in self.conjunctions if not term.issuperset(features)] + remaining.append(features) + self.conjunctions = remaining + return False def __str__(self): - conjs = [_format_conjunction(c) for c in self.conjunctions] + conjs = [_format_conjunction(c) for c in sorted(self.conjunctions)] return f'({" OR ".join(conjs)})' def __repr__(self) -> str: - conjs = [_repr_conjunction(c) for c in self.conjunctions] + conjs = [_repr_conjunction(c) for c in sorted(self.conjunctions)] return f"[{ ', '.join(conjs)}]" @classmethod @@ -144,6 +245,38 @@ def make_frozen(self) -> List[Tuple[str, FrozenAvailability]]: for key in sorted(self.syms.keys())] +def _version_cleaner(term: FrozenSet[str]) -> FrozenSet[str]: + versions = list(sorted((s for s in term if s.startswith("XR_VERSION_")))) + if not versions: + return term + + mutable_term = set(term) + # Saying the default version is redundant if we have more interesting dependencies. + if len(versions) == 1 and versions[0] == _DEFAULT_VER and len(term) > 1: + mutable_term.discard(_DEFAULT_VER) + + # keep the last one + redundant_versions = versions[:-1] + if redundant_versions: + mutable_term.difference_update(redundant_versions) + + return frozenset(mutable_term) + + +_DEFAULT_VER = 'XR_VERSION_1_0' + + +def _process_depends_string(s: Optional[str]) -> Availability: + if not s: + return Availability.create({_DEFAULT_VER}) + + terms = [{feat.strip() for feat in t.split("+")} for t in s.split(',')] + for t in terms: + if not any(feat.startswith('XR_VERSION_') for feat in t): + t.add(_DEFAULT_VER) + return Availability(terms) + + @dataclass class InteractionProfileComponent: """A component of an interaction profile""" @@ -200,16 +333,13 @@ def add_component(self, limit_to_user_path: Optional[str] = None, verbose: bool = _VERBOSE_INTERACTION_PROFILE_PROCESSING ) -> InteractionProfileComponent: - # if not avail.issuperset(self.availability): - # # the component availability does not include all the requirements of the profile availability? - # print(f"{self.name}: Add {subpath}: component availability {avail} is not a superset of profile availability {self.availability}") if limit_to_user_path: user_paths = {limit_to_user_path} else: - user_paths = deepcopy(self.valid_user_paths) - if subpath in self.components: - component = self.components[subpath] + user_paths = self.valid_user_paths + component = self.components.get(subpath) + if component: if action_type != component.action_type: raise RuntimeError(f"{self.name}: Add {subpath} again: Action type mismatch") if system != component.system: @@ -217,11 +347,9 @@ def add_component(self, if user_paths != component.valid_user_paths: raise RuntimeError(f"{self.name}: Add {subpath} again: Valid user paths mismatch") - assert len(avail.conjunctions) == 1 - # Just extend the availability - redundant = component.availability.merge(avail) - if redundant and verbose: - print(f"{self.name}{subpath}: Redundant availability {avail}") + # Just extend the availability. Not caring about return value (whether the avail is redundant) + component.availability.merge(avail) + return component ret = InteractionProfileComponent( @@ -235,14 +363,39 @@ def add_component(self, self.components[subpath] = ret return ret + def yield_user_path_and_component_pairs(self): + """ + Yield a user path and a component object. + + Outer iteration is over user paths. + """ + for user_path in sorted(self.valid_user_paths): + for subpath in sorted(self.components.keys()): + component = self.components[subpath] + if user_path in component.valid_user_paths: + yield user_path, component + + def generate_binding_paths(self): + """ + Yield a full binding path, availability, and the component object. + + Outer iteration is over user paths. + """ + for user_path, component in self.yield_user_path_and_component_pairs(): + yield (f"{user_path}{component.subpath}", + self.compute_component_availability(component), + component) + + def compute_component_availability(self, component: InteractionProfileComponent) -> Availability: + return self.availability.anded(component.availability).cleaned(_version_cleaner) + class InteractionProfileProcessor: """Encapsulates the logic to process interaction profiles in the XML.""" def __init__(self): self.interaction_profiles: Dict[str, InteractionProfile] = dict() - self.deferred_added_components = defaultdict(list) - self.availabilities: Set[FrozenAvailability] = set() + self.processed_features: List[et.Element] = [] self.finished = False self.verbose = _VERBOSE_INTERACTION_PROFILE_PROCESSING @@ -255,12 +408,12 @@ def process_feature(self, root: et.Element, interface: et.Element, emit: bool): root is the root of the registry XML tree. interface is either a `` tag or `` tag. - This may queue added components, if a feature requiring their profile - is not yet processed. Call process_deferred() to resolve this once all features - are processed. + This logs the features processed, in order, to perform additional passes + when finishing processing. Call `finish_processing()` once all features + are processed to perform these subsequent processing passes. """ if self.finished: - print("Hey, we think we are already finished!") + raise ValueError("Cannot process more features once we are finished!") interface_name = interface.get('name') assert interface_name @@ -268,11 +421,12 @@ def process_feature(self, root: et.Element, interface: et.Element, emit: bool): if self.verbose: print(f"Handling {interface.tag}: {interface_name} {emit}") + # Only grab interaction profiles in this first pass for require in interface.findall('./require[interaction_profile]'): - required_features = self._compute_deps_for_interaction_profile(interface, require) + avail = self._compute_deps_for_interaction_profile(interface, require) if self.verbose: - print(interface.tag, interface_name, required_features) + print(interface.tag, interface_name, avail) for include_ipp in require.findall('./interaction_profile'): # Path @@ -282,61 +436,58 @@ def process_feature(self, root: et.Element, interface: et.Element, emit: bool): if self.verbose: print(interface_name, name) - self._include_interaction_profile(root, name, required_features) + self._include_interaction_profile(root, name, avail) + self.processed_features.append(interface) + def _process_extending_bindings(self, interface: et.Element): + + interface_name = interface.get('name') + assert interface_name + + if self.verbose: + print(f"Handling {interface.tag}: {interface_name}: Pass 2, additional binding paths") for require in interface.findall('./require[extend]'): - required_features = self._compute_deps_for_interaction_profile(interface, require) + avail = self._compute_deps_for_interaction_profile(interface, require) for extend in require.findall('./extend[@interaction_profile_path]'): profile_name = extend.get("interaction_profile_path") assert profile_name - profile = self.interaction_profiles.get(profile_name) - if profile: - self._add_interaction_profile_components(profile, extend, required_features) - else: - self.deferred_added_components[profile_name].append((extend, required_features)) + profile = self.interaction_profiles[profile_name] + self._add_interaction_profile_components(profile, extend, avail) - def process_deferred(self): + def finish_processing(self): """ - Process components originating in features processed before their profile. + Perform the second pass over features and other finish-up work. - This is idempotent: you can call it multiple times safely. - However, you must have processed the profiles referred to by deferred components - first! So, just call it once from endFile() if you are using the common - Generator classes. + To guarantee correct processing, only call this once, after all + calls to `process_feature()` are complete. + Call it once from `endFile()` if you are using the common + `Generator` classes. """ - for profile_name, components in self.deferred_added_components.items(): - for extend, deps in components: - profile = self.interaction_profiles.get(profile_name) - assert profile - self._add_interaction_profile_components(profile, extend, deps) - self.deferred_added_components.clear() - - for profile in self.interaction_profiles.values(): - self.availabilities.add(profile.availability.make_frozen()) - for component in profile.components.values(): - self.availabilities.add(component.availability.make_frozen()) + # Second pass: extend binding paths (components) + for feature in self.processed_features: + self._process_extending_bindings(feature) + + self.processed_features.clear() self.finished = True - # for ip in self.interaction_profiles.values(): - # for component in ip.components: - def _include_interaction_profile(self, root: et.Element, name: str, required_features: Set[str]): + def _include_interaction_profile(self, root: et.Element, name: str, avail: Availability): if name in self.interaction_profiles: profile = self.interaction_profiles[name] - redundant = profile.availability.add(required_features) + redundant = profile.availability.merge(avail) if redundant and self.verbose: - print(f"{name}: Adding redundant set of required features! {required_features}") + print(f"{name}: Adding redundant availability! {avail}") # Add our new route of availability to all components defined inline, without # going through the XML again. for component in profile.components.values(): if component.integral: - redundant = component.availability.add(required_features) + redundant = component.availability.merge(avail) if redundant and self.verbose: - print(f"{name}{component.subpath}: Adding redundant set of required features!") - print(f" new: {required_features} existing availability: {component.availability}") + print(f"{name}{component.subpath}: Adding redundant availability!") + print(f" new: {avail} existing availability: {component.availability}") return @@ -348,37 +499,27 @@ def _include_interaction_profile(self, root: et.Element, name: str, required_fea title = profile_elt.get("title") assert title - avail = Availability.create(required_features) profile = InteractionProfile(valid_user_paths=user_paths, name=name, title=title, availability=avail) self.interaction_profiles[name] = profile - self._add_interaction_profile_components(profile, profile_elt, required_features, integral=True) + self._add_interaction_profile_components(profile, profile_elt, avail, integral=True) - def _compute_deps_for_interaction_profile(self, feature_elt: et.Element, require_elt: et.Element) -> Set[str]: + def _compute_deps_for_interaction_profile(self, feature_elt: et.Element, require_elt: et.Element) -> Availability: feature_name = feature_elt.get("name") assert feature_name - deps = {feature_name} - - # TODO finish fixing for schema update - if feature_elt.tag == "extension": - requiresCore = feature_elt.get("requiresCore", "1.0") - deps.add(f"XR_VERSION_{requiresCore.replace('.', '_')}") - - additional_extension = require_elt.get('extension') - if additional_extension is None: - additional_extension = require_elt.get('depends') - if additional_extension is not None: - deps.add(additional_extension) + deps = Availability.create({feature_name}) - return deps + require_depends = require_elt.get('depends') + if require_depends: + deps = deps.anded(_process_depends_string(require_depends)) - def _add_interaction_profile_components(self, profile: InteractionProfile, component_parent, required_features: Set[str], integral: bool = False): + return deps.cleaned(_version_cleaner) - avail = Availability.create(required_features) + def _add_interaction_profile_components(self, profile: InteractionProfile, component_parent, avail: Availability, integral: bool = False): for component in component_parent.findall("./component"): system = False if component.get("system") is not None: diff --git a/src/scripts/template_interaction_info_generated.cpp b/src/scripts/template_interaction_info_generated.cpp index d9385aa2c..e6edc388a 100644 --- a/src/scripts/template_interaction_info_generated.cpp +++ b/src/scripts/template_interaction_info_generated.cpp @@ -9,12 +9,12 @@ namespace Conformance { -//# macro make_qualified_path_entry(user_path, component) +//# macro make_path_entry(binding_path, avail, component) InputSourcePathAvailData{ - /*{ (user_path + component.subpath) | quote_string }*/, + /*{ binding_path | quote_string }*/, /*{ component.action_type }*/, - InteractionProfileAvailability::Avail_/*{- component.availability.as_normalized_symbol() }*/ - //# if component.system + InteractionProfileAvailability::Avail_/*{- avail.as_normalized_symbol() }*/ + //#- if component.system , true //# endif } @@ -24,13 +24,14 @@ const std::vector& GetAllInteractionProfiles() // // Generated lists of component paths for interaction profiles, with metadata and availability expressions. // + //# for path, profile in interaction_profiles.items() - static const InputSourcePathAvailCollection /*{'c' + (path | replace("/", "_")) }*/{ -//# for component in profile.components.values() -//# for user_path in component.valid_user_paths - /*{ make_qualified_path_entry(user_path, component) | collapse_whitespace }*/, -//# endfor + // Interaction profile path: /*{ path }*/ + // Availability: /*{ profile.availability }*/ + static const InputSourcePathAvailCollection /*{'c' + (path | replace("/", "_") | replace("-", "_")) }*/{ +//# for binding_path, avail, component in profile.generate_binding_paths() + /*{ make_path_entry(binding_path, avail, component) | collapse_whitespace }*/, //# endfor }; @@ -46,12 +47,12 @@ const std::vector& GetAllInteractionProfiles() /*{ profile.name | quote_string }*/, /*{ profile.name | replace("/interaction_profiles/", "") | quote_string }*/, { - //# for user_path in profile.valid_user_paths + //# for user_path in profile.valid_user_paths | sort /*{ user_path | quote_string }*/, //# endfor }, InteractionProfileAvailability::Avail_/*{- profile.availability.as_normalized_symbol() -}*/, - /*{'c' + (path |replace("/", "_")) }*/ + /*{'c' + (path | replace("/", "_") | replace("-", "_")) }*/ }, //# endfor }; diff --git a/src/scripts/template_interaction_info_generated.h b/src/scripts/template_interaction_info_generated.h index 65ba08568..8f8fc467f 100644 --- a/src/scripts/template_interaction_info_generated.h +++ b/src/scripts/template_interaction_info_generated.h @@ -61,7 +61,7 @@ static const std::array kInteract /// This is a generated list of all interaction profiles in the order returned by GetAllInteractionProfiles. enum class InteractionProfileIndex { //# for path, profile in interaction_profiles.items() - Profile_/*{ (path | replace("/interaction_profiles/", "") | replace("/", "_")) }*/, + Profile_/*{ (path | replace("/interaction_profiles/", "") | replace("/", "_") | replace("-", "_")) }*/, //# endfor };