From 41c1548ccf0ee8110890c6bffe05af20d03095d2 Mon Sep 17 00:00:00 2001 From: Christophe Date: Thu, 8 Oct 2026 14:00:30 +0200 Subject: [PATCH] vkprofiles: Add transform --strip unratified --- CHANGELOG.md | 1 + scripts/source/main_transform.py | 4 + scripts/source/transform_strip_unratified.py | 124 +++++++++++ scripts/source/transform_utils.py | 1 + scripts/tests/CMakeLists.txt | 18 +- .../tests/test_transform_strip_unratified.py | 205 ++++++++++++++++++ 6 files changed, 349 insertions(+), 4 deletions(-) create mode 100644 scripts/source/transform_strip_unratified.py create mode 100644 scripts/tests/test_transform_strip_unratified.py diff --git a/CHANGELOG.md b/CHANGELOG.md index c147f19b..1e1a4629 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ ### Improvements: - Add `validate` analysis check to report unratified extensions in profiles files +- Add `transform --strip unratified` to remove all unratified extensions in profiles files ## [Vulkan Profiles Tools 1.4.363](https://github.com/KhronosGroup/Vulkan-Profiles/tree/sdk-1.4.363.0) - September 2026 diff --git a/scripts/source/main_transform.py b/scripts/source/main_transform.py index ae01e29c..1832149d 100644 --- a/scripts/source/main_transform.py +++ b/scripts/source/main_transform.py @@ -39,6 +39,7 @@ from source.transform_strip_helper_values import strip_helper_values_profiles_files from source.transform_strip_duplication import strip_duplication_profiles_files from source.transform_strip_promoted_extensions import strip_promoted_extensions_profiles_files +from source.transform_strip_unratified import strip_unratified_extensions_profiles_files from source.transform_sort import sort_profiles_files from source.main_validate import main_validate @@ -77,6 +78,9 @@ def transform_profiles_files( if StripBits.PROMOTED_EXTENSIONS in strip_modes: strip_promoted_extensions_profiles_files(vk, json_files_dict) + if StripBits.UNRATIFIED in strip_modes: + strip_unratified_extensions_profiles_files(vk, json_files_dict) + if sort: sort_profiles_files(vk, json_files_dict) diff --git a/scripts/source/transform_strip_unratified.py b/scripts/source/transform_strip_unratified.py new file mode 100644 index 00000000..c98c9a89 --- /dev/null +++ b/scripts/source/transform_strip_unratified.py @@ -0,0 +1,124 @@ +#!/usr/bin/python3 +# +# Copyright (c) 2026-2026 Google, Inc. +# Copyright (C) 2026-2026 Valve Corporation +# Copyright (c) 2026-2026 LunarG, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License") +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Authors: +# - Christophe Riccio + +from source.vulkan_object_utils import ( + VulkanObject, + VK_VERSION, + is_extension_struct_name, + getStructDefiningExtensions, + getExtensionPromotedTo, +) +from source.profiles_json_utils import ( + collect_block_names, + get_topologically_sorted_file_keys +) + + +def strip_unratified_extensions_capabilities_block( + vk: VulkanObject, version: VK_VERSION, json_profiles_capabilities_block: dict +): + if not isinstance(json_profiles_capabilities_block, dict): + return + + # 1. Remove unratified extensions from "extensions" + block_exts = json_profiles_capabilities_block.get("extensions") + if isinstance(block_exts, dict): + exts_to_remove = [ + ext_name for ext_name in block_exts.keys() + if ext_name in vk.extensions and not vk.extensions[ext_name].ratified + ] + + for ext_name in exts_to_remove: + del block_exts[ext_name] + + if not block_exts: + del json_profiles_capabilities_block["extensions"] + + # 2. Remove structures defined by unratified extensions + remaining_exts = set(json_profiles_capabilities_block.get("extensions", {}).keys()) + + categories_to_check = [ + cat for cat, val in list(json_profiles_capabilities_block.items()) + if cat != "extensions" and isinstance(val, dict) + ] + + for category in categories_to_check: + category_dict = json_profiles_capabilities_block[category] + structs_to_remove = [] + + for struct_name in list(category_dict.keys()): + if is_extension_struct_name(vk, struct_name): + def_exts = getStructDefiningExtensions(vk, struct_name) + if def_exts: + retained = False + for ext_name in def_exts: + # Retain if the defining extension was promoted to core at or below target version + promoted = getExtensionPromotedTo(vk, ext_name) + for p_target in promoted: + p_ver = VK_VERSION.from_string(p_target) + if p_ver != VK_VERSION.NONE and version >= p_ver: + retained = True + break + if retained: + break + + # Retain if the defining extension is ratified and declared in the block + if ext_name in vk.extensions and vk.extensions[ext_name].ratified: + if ext_name in remaining_exts: + retained = True + break + + if not retained: + structs_to_remove.append(struct_name) + + for struct_name in structs_to_remove: + del category_dict[struct_name] + + if not category_dict: + del json_profiles_capabilities_block[category] + + +def strip_unratified_extensions_profiles_file(vk: VulkanObject, json_file_data: dict): + profiles_data = json_file_data.get("profiles", {}) + json_profiles_capabilities = json_file_data.get("capabilities", {}) + + for profile_key, profile_obj in profiles_data.items(): + api_version = VK_VERSION.from_string(profile_obj.get("api-version", "1.0.0")) + block_names = collect_block_names(profile_obj.get("capabilities", [])) + + for block_name in block_names: + if block_name in json_profiles_capabilities: + strip_unratified_extensions_capabilities_block( + vk, api_version, json_profiles_capabilities[block_name] + ) + + +def strip_unratified_extensions_profiles_files(vk: VulkanObject, json_files_dict: dict): + if not isinstance(json_files_dict, dict): + return + + if "profiles" in json_files_dict or "capabilities" in json_files_dict: + strip_unratified_extensions_profiles_file(vk, json_files_dict) + return + + sorted_file_keys = get_topologically_sorted_file_keys(json_files_dict) + for file_key in sorted_file_keys: + strip_unratified_extensions_profiles_file(vk, json_files_dict[file_key]) \ No newline at end of file diff --git a/scripts/source/transform_utils.py b/scripts/source/transform_utils.py index 8183d684..caadacf0 100644 --- a/scripts/source/transform_utils.py +++ b/scripts/source/transform_utils.py @@ -61,6 +61,7 @@ class StripBits(str, Enum): HELPER_VALUES = 'helper-values' # Removes non-bitpos bitmask helper values (composites, all-flags, and zero/none constants) from capability blocks. DUPLICATION = 'duplication' # Removes redundant duplicate features, properties, and extension requirements across inheritance trees and within blocks. PROMOTED_EXTENSIONS = 'promoted-extensions' # Removes extensions that are already promoted to the profile's target core Vulkan version. + UNRATIFIED = 'unratified' # Removes extensions and extension structures that are not ratified for Vulkan. class CategoryPriority(IntEnum): diff --git a/scripts/tests/CMakeLists.txt b/scripts/tests/CMakeLists.txt index b5d5ec26..428e09aa 100644 --- a/scripts/tests/CMakeLists.txt +++ b/scripts/tests/CMakeLists.txt @@ -16,13 +16,22 @@ find_package(Python3 REQUIRED COMPONENTS Interpreter) +# Use PYTHON_EXECUTABLE set by root CMakeLists.txt, or fallback to Python3_EXECUTABLE +if(NOT PYTHON_EXECUTABLE) + if(VENV_PYTHON_EXECUTABLE AND NOT SKIP_PYTHON_AUTO_INSTALL) + set(PYTHON_EXECUTABLE "${VENV_PYTHON_EXECUTABLE}") + else() + set(PYTHON_EXECUTABLE "${Python3_EXECUTABLE}") + endif() +endif() + function(add_vulkan_python_test TARGET_NAME SCRIPT_FILENAME) set(REGISTRY_PATH "${VULKAN_HEADERS_INSTALL_DIR}/${CMAKE_INSTALL_DATADIR}/vulkan/registry") set(FULL_SCRIPT_PATH "${CMAKE_CURRENT_SOURCE_DIR}/${SCRIPT_FILENAME}") # Register the script as a CTest test case add_test(NAME ${TARGET_NAME} - COMMAND ${VENV_PYTHON_EXECUTABLE} "${FULL_SCRIPT_PATH}" --registry "${REGISTRY_PATH}/vk.xml" + COMMAND ${PYTHON_EXECUTABLE} "${FULL_SCRIPT_PATH}" --registry "${REGISTRY_PATH}/vk.xml" WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" ) @@ -32,7 +41,7 @@ function(add_vulkan_python_test TARGET_NAME SCRIPT_FILENAME) ) endfunction() -if(NOT APPLE) # macOS doesn't handle dynamically loading Python jsonschema' +if(NOT APPLE) # macOS does not handle dynamically loading Python jsonschema add_vulkan_python_test(VkProfiles_TestVulkanProfilesUtil test_util.py) add_vulkan_python_test(VkProfiles_TestVulkanObjectEmpty test_init.py) add_vulkan_python_test(VkProfiles_TestVulkanVersion test_vulkan_version.py) @@ -60,6 +69,7 @@ if(NOT APPLE) # macOS doesn't handle dynamically loading Python jsonschema' add_vulkan_python_test(VkProfiles_TestTransformStripHelper test_transform_strip_helper.py) add_vulkan_python_test(VkProfiles_TestTransformStripDups test_transform_strip_duplication.py) add_vulkan_python_test(VkProfiles_TestTransformStripPromotedExts test_transform_strip_promoted_exts.py) + add_vulkan_python_test(VkProfiles_TestTransformStripUnratified test_transform_strip_unratified.py) add_vulkan_python_test(VkProfiles_TestTransformSort test_transform_sort.py) add_vulkan_python_test(VkProfiles_TestExtract test_extract.py) add_vulkan_python_test(VkProfiles_TestMinApiVersion test_min_api_version.py) @@ -74,7 +84,7 @@ set(PROFILES_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/profiles") add_custom_target(VpGenerate-Profiles-Android ALL COMMAND ${CMAKE_COMMAND} -E make_directory "${PROFILES_OUTPUT_DIR}" COMMAND ${CMAKE_COMMAND} -E env "PYTHONPATH=${REGISTRY_PATH}" - ${VENV_PYTHON_EXECUTABLE} "${CMAKE_CURRENT_SOURCE_DIR}/../vkprofiles.py" transform + ${PYTHON_EXECUTABLE} "${CMAKE_CURRENT_SOURCE_DIR}/../vkprofiles.py" transform --registry "${REGISTRY_PATH}/vk.xml" --input "${PROFILES_INPUT_DIR}" --output "${PROFILES_OUTPUT_DIR}" @@ -88,4 +98,4 @@ add_custom_target(VpGenerate-Profiles-Android ALL VERBATIM ) set_target_properties(VpGenerate-Profiles-Android PROPERTIES FOLDER "Profiles generator") -add_dependencies(VpGenerate-Profiles-Android VpProfilesProcessor) +add_dependencies(VpGenerate-Profiles-Android VpProfilesProcessor) \ No newline at end of file diff --git a/scripts/tests/test_transform_strip_unratified.py b/scripts/tests/test_transform_strip_unratified.py new file mode 100644 index 00000000..4a7c8ce0 --- /dev/null +++ b/scripts/tests/test_transform_strip_unratified.py @@ -0,0 +1,205 @@ +#!/usr/bin/python3 +# +# Copyright (c) 2026-2026 Google, Inc. +# Copyright (C) 2026-2026 Valve Corporation +# Copyright (c) 2026-2026 LunarG, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License") +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Authors: +# - Christophe Riccio + +import argparse +import json +from pathlib import Path +import sys +import unittest + +scripts_dir = Path(__file__).resolve().parent.parent +if str(scripts_dir) not in sys.path: + sys.path.insert(0, str(scripts_dir)) + +from vulkan_object import VulkanObject +from source.vulkan_object_utils import initVulkanObject +from source.transform_strip_unratified import strip_unratified_extensions_profiles_files + + +class TestConvertStripUnratifiedExtensions(unittest.TestCase): + registry_path = None + + def setUp(self): + self.vk: VulkanObject = initVulkanObject('vulkan', self.registry_path) + + def test_strip_unratified_extensions_ratified_retained(self): + """ + Verifies that ratified extensions (e.g., VK_KHR_variable_pointers) and their + structures (e.g., VkPhysicalDeviceVariablePointersFeatures) are retained. + """ + original_json_text = """{ + "$schema": "https://schema.khronos.org/vulkan/profiles-0.8.0-106.json#", + "profiles": { + "VP_TEST_profile": { + "version": 1, + "api-version": "1.0.68", + "capabilities": ["baseline"] + } + }, + "capabilities": { + "baseline": { + "extensions": { + "VK_KHR_variable_pointers": 1 + }, + "features": { + "VkPhysicalDeviceVariablePointersFeatures": { + "variablePointers": true + } + } + } + } + }""" + + expected_json_text = original_json_text + + original_data = json.loads(original_json_text) + expected_data = json.loads(expected_json_text) + + json_files_dict = {"test_profile.json": original_data} + strip_unratified_extensions_profiles_files(self.vk, json_files_dict) + + self.assertEqual(json_files_dict["test_profile.json"], expected_data) + + def test_strip_unratified_extensions_unratified_removed(self): + """ + Verifies that unratified extensions (e.g., VK_AMD_device_coherent_memory) and their + defining structures (e.g., VkPhysicalDeviceCoherentMemoryFeaturesAMD) are removed. + """ + original_json_text = """{ + "$schema": "https://schema.khronos.org/vulkan/profiles-0.8.0-106.json#", + "profiles": { + "VP_TEST_profile": { + "version": 1, + "api-version": "1.0.68", + "capabilities": ["baseline"] + } + }, + "capabilities": { + "baseline": { + "extensions": { + "VK_AMD_device_coherent_memory": 1 + }, + "features": { + "VkPhysicalDeviceCoherentMemoryFeaturesAMD": { + "deviceCoherentMemory": true + } + } + } + } + }""" + + expected_json_text = """{ + "$schema": "https://schema.khronos.org/vulkan/profiles-0.8.0-106.json#", + "profiles": { + "VP_TEST_profile": { + "version": 1, + "api-version": "1.0.68", + "capabilities": ["baseline"] + } + }, + "capabilities": { + "baseline": {} + } + }""" + + original_data = json.loads(original_json_text) + expected_data = json.loads(expected_json_text) + + json_files_dict = {"test_profile.json": original_data} + strip_unratified_extensions_profiles_files(self.vk, json_files_dict) + + self.assertEqual(json_files_dict["test_profile.json"], expected_data) + + def test_strip_unratified_extensions_mixed(self): + """ + Verifies that in a block with mixed extensions, unratified ones and their structures + are stripped while ratified extensions and their structures remain intact. + """ + original_json_text = """{ + "$schema": "https://schema.khronos.org/vulkan/profiles-0.8.0-106.json#", + "profiles": { + "VP_TEST_profile": { + "version": 1, + "api-version": "1.0.68", + "capabilities": ["baseline"] + } + }, + "capabilities": { + "baseline": { + "extensions": { + "VK_KHR_variable_pointers": 1, + "VK_AMD_device_coherent_memory": 1 + }, + "features": { + "VkPhysicalDeviceVariablePointersFeatures": { + "variablePointers": true + }, + "VkPhysicalDeviceCoherentMemoryFeaturesAMD": { + "deviceCoherentMemory": true + } + } + } + } + }""" + + expected_json_text = """{ + "$schema": "https://schema.khronos.org/vulkan/profiles-0.8.0-106.json#", + "profiles": { + "VP_TEST_profile": { + "version": 1, + "api-version": "1.0.68", + "capabilities": ["baseline"] + } + }, + "capabilities": { + "baseline": { + "extensions": { + "VK_KHR_variable_pointers": 1 + }, + "features": { + "VkPhysicalDeviceVariablePointersFeatures": { + "variablePointers": true + } + } + } + } + }""" + + original_data = json.loads(original_json_text) + expected_data = json.loads(expected_json_text) + + json_files_dict = {"test_profile.json": original_data} + strip_unratified_extensions_profiles_files(self.vk, json_files_dict) + + self.assertEqual(json_files_dict["test_profile.json"], expected_data) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument( + '--registry', '-r', action='store', required=False, + help='Use specified registry file instead of vk.xml.' + ) + + args, unparsed = parser.parse_known_args() + TestConvertStripUnratifiedExtensions.registry_path = args.registry + + unittest.main(argv=[sys.argv[0]] + unparsed) \ No newline at end of file