| #!/usr/bin/env python3 |
| """ |
| Script to find Vulkan API calls that are NOT wrapped with wrapper macros (see WRAPPER_MACROS). |
| Uses parenthesis-based parsing instead of regex for detecting wrapped calls. |
| Searches for Vulkan functions defined in volk.h as PFN_vk* types, including naming variations |
| (e.g., vkGetDeviceQueue, GetDeviceQueue, getDeviceQueue, pfn_vkGetDeviceQueue, etc.). |
| """ |
| |
| import argparse |
| import os |
| import re |
| import sys |
| from concurrent.futures import ProcessPoolExecutor |
| from dataclasses import dataclass |
| from typing import List, Set |
| |
| # ============================================================================= |
| # Configuration |
| # ============================================================================= |
| |
| ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) |
| |
| # Path to volk.h file containing PFN_vk* function definitions |
| VOLK_H_PATH = os.path.join(ROOT_DIR, "src", "third_party", "volk", "volk.h") |
| |
| # Default directory to search for source files |
| DEFAULT_SEARCH_DIR = os.path.join(ROOT_DIR, "src", "libANGLE", "renderer", "vulkan") |
| |
| # Test directory to exclude when searching in default directory |
| TEST_DIR = os.path.join(DEFAULT_SEARCH_DIR, "find_unwrapped_vk_calls_test") |
| |
| # File extensions to search for source files |
| SOURCE_FILE_EXTENSIONS = ('.h', '.cpp', '.mm') |
| |
| # Wrapper macros that indicate a Vulkan call is properly wrapped |
| # Note: VK_CALL is not included because it does not look like a Vulkan call. Example: |
| # VK_CALL(vkGetDeviceQueue2, device, &queueInfo2, queue); |
| WRAPPER_MACROS = {'VK_CALL_WITH_GROUP', 'VK_CALL_WITH_API', 'VK_SECONDARY_CMD_CALL'} |
| |
| # Timer class that indicates Vulkan calls are being timed (also considered wrapped) |
| TIMER_CLASS_NAME = 'ScopedVulkanApiPerfTimer' |
| |
| # Number of characters to show before and after the function name in context output |
| CONTEXT_CHAR_LIMIT = 20 |
| |
| # ============================================================================= |
| # Denylist of function names that may look like Vulkan functions but are not |
| # Vulkan API calls. This includes member functions and other non-Vulkan functions. |
| # Add more names here as needed. |
| # ============================================================================= |
| NON_VULKAN_FUNCTIONS = {'createPipelineLayout', 'endCommandBuffer'} |
| |
| # ============================================================================= |
| # Keywords that indicate a function declaration rather than a function call. |
| # Used by is_function_declaration() to skip function declarations/definitions. |
| # Includes return types (e.g., 'void', 'VkResult') and modifiers (e.g., 'inline', 'static'). |
| # |
| # Supported formats: |
| # - Simple types: 'void', 'VkResult', 'size_t' |
| # - Namespace-qualified types: 'angle::Result' |
| # - Modifiers: 'inline', 'static', 'constexpr', 'const' |
| # |
| # Unsupported formats (will not be matched): |
| # - Template types: 'SomeType<T>', 'std::vector<int>' |
| # - Types with modifiers: 'const char*' (only base type 'char' would match) |
| # - Pointer/reference notation: 'Type*', 'Type&' (modifiers are stripped before matching) |
| # |
| # Add more keywords here as needed. |
| # ============================================================================= |
| FUNCTION_DECLARATION_KEYWORDS = { |
| # Standard types |
| 'int32_t', |
| 'int64_t', |
| 'size_t', |
| 'uint32_t', |
| 'uint64_t', |
| 'void', |
| # Modifiers |
| 'const', |
| 'constexpr', |
| 'inline', |
| 'static', |
| 'ANGLE_INLINE', |
| # Vulkan types |
| 'PFN_vkVoidFunction', |
| 'VkBool32', |
| 'VkDeviceSize', |
| 'VkExtensionProperties', |
| 'VkExtent2D', |
| 'VkExtent3D', |
| 'VkFlags', |
| 'VkFormatProperties', |
| 'VkImageFormatProperties', |
| 'VkLayerProperties', |
| 'VkMemoryHeap', |
| 'VkMemoryType', |
| 'VkPhysicalDeviceFeatures', |
| 'VkPhysicalDeviceFeatures2', |
| 'VkPhysicalDeviceMemoryProperties', |
| 'VkPhysicalDeviceProperties', |
| 'VkPhysicalDeviceProperties2', |
| 'VkQueueFamilyProperties', |
| 'VkRect2D', |
| 'VkResult', |
| 'VkSampleCountFlags', |
| 'VkSparseImageFormatProperties', |
| # ANGLE types |
| 'angle::Result', |
| 'Result', |
| # ANGLE Impl types |
| 'BufferImpl', |
| 'ContextImpl', |
| 'DeviceImpl', |
| 'DisplayImpl', |
| 'FenceImpl', |
| 'FramebufferImpl', |
| 'ImageImpl', |
| 'ProgramImpl', |
| 'QueryImpl', |
| 'RenderbufferImpl', |
| 'SamplerImpl', |
| 'SemaphoreImpl', |
| 'ShaderImpl', |
| 'StreamImpl', |
| 'SurfaceImpl', |
| 'TextureImpl', |
| 'TransformFeedbackImpl', |
| } |
| |
| |
| @dataclass |
| class VkCallLocation: |
| """Represents a Vulkan API call location in source code.""" |
| file_path: str |
| line_number: int |
| call_text: str |
| context: str |
| |
| |
| def camel_to_snake(name: str) -> str: |
| """Convert CamelCase to snake_case. |
| |
| Handles consecutive uppercase letters as a group (acronyms): |
| QueuePresentKHR -> queue_present_khr |
| CmdSetViewportWScalingNV -> cmd_set_viewport_w_scaling_nv |
| CreateDirectFBSurfaceEXT -> create_direct_fb_surface_ext |
| CreateIOSSurfaceMVK -> create_ios_surface_mvk |
| """ |
| if not name: |
| return '' |
| |
| result = [] |
| n = len(name) |
| for i, char in enumerate(name): |
| if char.isupper() and i > 0: |
| prev_lower = name[i - 1].islower() |
| next_lower = (i + 1 < n) and name[i + 1].islower() |
| # Add underscore before this uppercase char if: |
| # 1. Previous char is lowercase (e.g., "getD" -> "get_d") |
| # 2. Or this is the last uppercase in a run followed by lowercase |
| # (e.g., "VKStructure" -> "vk_structure", not "v_ks_tructure") |
| if prev_lower or next_lower: |
| result.append('_') |
| result.append(char.lower()) |
| return ''.join(result) |
| |
| |
| def generate_function_variations(func_name: str) -> Set[str]: |
| """ |
| Generate naming variations for a vk* function name. |
| |
| For example, vkGetDeviceQueue2 produces: |
| |
| 1. Lower camelCase with ("", "p_", "pfn_") prefixes: |
| - getDeviceQueue2, p_getDeviceQueue2, pfn_getDeviceQueue2 |
| - vkGetDeviceQueue2, p_vkGetDeviceQueue2, pfn_vkGetDeviceQueue2 |
| |
| 2. Capital CamelCase with ("p", "pfn", "p_", "pfn_") prefixes (NOT starting with capital letter): |
| - pGetDeviceQueue2, pfnGetDeviceQueue2, p_GetDeviceQueue2, pfn_GetDeviceQueue2 |
| - pVkGetDeviceQueue2, pfnVkGetDeviceQueue2, p_VkGetDeviceQueue2, pfn_VkGetDeviceQueue2 |
| |
| 3. Lower snake_case with ("", "p_", "pfn_") prefixes: |
| - get_device_queue2, p_get_device_queue2, pfn_get_device_queue2 |
| """ |
| variations = set() |
| |
| # Strip the 'vk' prefix to get the capital camel case name |
| if not func_name.startswith('vk'): |
| return variations |
| |
| capital_camel = func_name[2:] # Remove 'vk' prefix |
| |
| # Check that capital_camel starts with a capital letter |
| if not capital_camel or not capital_camel[0].isupper(): |
| return variations |
| |
| # 1. Lower camelCase variations (with "", "p_", "pfn_" prefixes) |
| lower_camel = capital_camel[0].lower() + capital_camel[1:] |
| for prefix in ('', 'p_', 'pfn_'): |
| variations.add(prefix + lower_camel) |
| # Also add with 'vk' prefix (lowercase) for lower camelCase |
| variations.add(prefix + 'vk' + capital_camel) |
| |
| # 2. Capital CamelCase variations with prefixes that prevent starting with capital letter |
| # (with "p", "pfn", "p_", "pfn_" prefixes - NOT empty prefix to avoid capital-first names) |
| for prefix in ('p', 'pfn', 'p_', 'pfn_'): |
| variations.add(prefix + capital_camel) |
| # Also add with 'Vk' prefix (capital V) for capital CamelCase |
| variations.add(prefix + 'Vk' + capital_camel) |
| |
| # 3. Lower snake_case variations (with "", "p_", "pfn_" prefixes) |
| snake_case = camel_to_snake(capital_camel) |
| for prefix in ('', 'p_', 'pfn_'): |
| variations.add(prefix + snake_case) |
| |
| return variations |
| |
| |
| def extract_vk_function_names_from_volk(volk_h_path: str) -> Set[str]: |
| """ |
| Extract all vk* function names from volk.h that are defined as PFN_vk* types. |
| Returns a set of base function names (e.g., 'vkCreateDevice', 'vkQueueSubmit'). |
| Does NOT include naming variations - use generate_all_function_variations() for that. |
| """ |
| vk_functions = set() |
| |
| try: |
| with open(volk_h_path, 'r', encoding='utf-8', errors='replace') as f: |
| content = f.read() |
| except Exception as e: |
| print(f"Error reading {volk_h_path}: {e}", file=sys.stderr) |
| return vk_functions |
| |
| # Pattern to match PFN_vkFunctionName declarations |
| # Examples: |
| # PFN_vkAllocateCommandBuffers vkAllocateCommandBuffers; |
| # extern PFN_vkCreateDevice vkCreateDevice; |
| pattern = r'PFN_(vk\w+)\s+(vk\w+)\s*[;=]' |
| |
| for match in re.finditer(pattern, content): |
| func_name = match.group(2) |
| vk_functions.add(func_name) |
| |
| return vk_functions |
| |
| |
| def generate_all_function_variations(func_names: Set[str]) -> Set[str]: |
| """ |
| Generate all naming variations for a set of Vulkan function names. |
| Returns a set containing the original names plus all variations. |
| """ |
| all_variations = set() |
| for func_name in func_names: |
| all_variations.add(func_name) |
| all_variations.update(generate_function_variations(func_name)) |
| return all_variations |
| |
| |
| def remove_comments_and_string_contents(content: str) -> str: |
| """ |
| Remove C++ comments (single-line // and multi-line /* */) and neutralize string literals. |
| Macro definitions (preprocessor directives) are kept as-is. |
| Returns the cleaned content with newlines preserved for line counting. |
| String literals are preserved but their content is replaced with spaces to avoid |
| false positives from strings containing macro names. |
| Handles C++11 raw string literals: R"delimiter(content)delimiter" |
| """ |
| result = [] |
| i = 0 |
| n = len(content) |
| |
| while i < n: |
| # Check for C++11 raw string literal: R"delimiter(content)delimiter" |
| # The format is: R"delimiter(content)delimiter" where delimiter can be empty or any sequence |
| if content[i] == 'R' and i + 1 < n and content[i + 1] == '"': |
| result.append('R') |
| result.append('"') |
| i += 2 |
| |
| # Extract the delimiter (characters between R" and opening paren) |
| delimiter_start = i |
| while i < n and content[i] != '(': |
| result.append(content[i]) |
| i += 1 |
| |
| if i < n and content[i] == '(': |
| delimiter = content[delimiter_start:i] |
| result.append('(') |
| i += 1 |
| |
| # Find the closing )delimiter" |
| # We need to find ) followed by delimiter followed by " |
| while i < n: |
| if content[i] == ')': |
| # Check if this is followed by the delimiter and closing quote |
| closing_pos = i + 1 |
| delim_pos = 0 |
| while delim_pos < len(delimiter) and closing_pos < n and content[ |
| closing_pos] == delimiter[delim_pos]: |
| closing_pos += 1 |
| delim_pos += 1 |
| |
| if delim_pos == len( |
| delimiter) and closing_pos < n and content[closing_pos] == '"': |
| # Found the closing )delimiter" |
| result.append(')') |
| for ch in delimiter: |
| result.append(ch) |
| result.append('"') |
| i = closing_pos + 1 |
| break |
| else: |
| # Just a ) in the content, replace with space |
| result.append(' ') |
| i += 1 |
| elif content[i] == '\n': |
| result.append('\n') |
| i += 1 |
| else: |
| result.append(' ') |
| i += 1 |
| continue |
| |
| # Check for string literal |
| if content[i] == '"': |
| # Handle string literal |
| result.append('"') |
| i += 1 |
| while i < n: |
| if content[i] == '\\' and i + 1 < n: |
| # Escape sequence - replace with spaces |
| result.append(' ') |
| result.append(' ') |
| i += 2 |
| elif content[i] == '"': |
| result.append('"') |
| i += 1 |
| break |
| elif content[i] == '\n': |
| result.append('\n') |
| i += 1 |
| else: |
| result.append(' ') |
| i += 1 |
| # Check for character literal |
| elif content[i] == "'": |
| result.append("'") |
| i += 1 |
| while i < n: |
| if content[i] == '\\' and i + 1 < n: |
| result.append(' ') |
| result.append(' ') |
| i += 2 |
| elif content[i] == "'": |
| result.append("'") |
| i += 1 |
| break |
| elif content[i] == '\n': |
| result.append('\n') |
| i += 1 |
| else: |
| result.append(' ') |
| i += 1 |
| # Check for single-line comment |
| elif i + 1 < n and content[i:i + 2] == '//': |
| # Skip until end of line |
| while i < n and content[i] != '\n': |
| i += 1 |
| # Keep the newline for line counting |
| if i < n: |
| result.append('\n') |
| i += 1 |
| # Check for multi-line comment |
| elif i + 1 < n and content[i:i + 2] == '/*': |
| i += 2 |
| # Skip until */ |
| while i + 1 < n and content[i:i + 2] != '*/': |
| # Preserve newlines for line counting |
| if content[i] == '\n': |
| result.append('\n') |
| else: |
| result.append(' ') |
| i += 1 |
| if i + 1 < n: |
| i += 2 # Skip */ |
| else: |
| # Keep all other characters including preprocessor directives |
| result.append(content[i]) |
| i += 1 |
| |
| return ''.join(result) |
| |
| |
| def get_line_number(content: str, pos: int) -> int: |
| """Get the 1-based line number for a position in the content.""" |
| return content[:pos].count('\n') + 1 |
| |
| |
| def is_member_or_namespace_call(content: str, func_name_start: int) -> bool: |
| """ |
| Check if this function call is a member call (preceded by . or ->) |
| or namespace qualified call (preceded by ::). |
| |
| Returns True if it's a member/namespace call, False if it's a global call. |
| """ |
| # Look backwards from the function name |
| i = func_name_start - 1 |
| |
| # Skip whitespace backwards |
| while i >= 0 and content[i] in ' \t\n\r': |
| i -= 1 |
| |
| if i < 0: |
| return False |
| |
| # Check for member access operators . or -> or namespace qualifier :: |
| if content[i] == '.': |
| return True |
| if content[i] == '>' and i > 0 and content[i - 1] == '-': |
| return True |
| if content[i] == ':' and i > 0 and content[i - 1] == ':': |
| return True |
| |
| return False |
| |
| |
| def is_function_declaration(content: str, func_name_start: int) -> bool: |
| """ |
| Check if this function is a declaration/definition rather than a call. |
| |
| A function declaration/definition has the pattern: |
| [return_type] functionName( |
| |
| Where return_type is typically void, VkResult, etc. |
| A function call would be: |
| functionName( |
| where the preceding text is not a return type. |
| """ |
| # Look backwards from the function name to find what precedes it |
| i = func_name_start - 1 |
| |
| # Skip whitespace, * and & backwards (treat * and & same as whitespace for return types) |
| while i >= 0 and content[i] in ' \t\n\r*&': |
| i -= 1 |
| |
| if i < 0: |
| return False |
| |
| # If preceded by certain characters, it's likely a call |
| # e.g., (vkFunc(), =vkFunc(), ;vkFunc(), {vkFunc() |
| if content[i] in '(;={},<>|!+-/%': |
| return False |
| |
| # Extract the type/identifier before the function name |
| # This handles namespace-qualified types like "angle::Result" |
| id_end = i + 1 |
| while i >= 0 and (content[i].isalnum() or content[i] in '_:'): |
| i -= 1 |
| id_start = i + 1 |
| |
| if id_start == id_end: |
| return False |
| |
| preceding_id = content[id_start:id_end] |
| |
| # Normalize the identifier by removing any leading/trailing colons |
| # (handles cases like "::namespace::Type" or partial matches) |
| preceding_id = preceding_id.strip(':') |
| |
| return preceding_id in FUNCTION_DECLARATION_KEYWORDS |
| |
| |
| def is_wrapped_in_macro(content: str, call_start: int) -> bool: |
| """ |
| Check if the Vulkan call at call_start is wrapped in one of the WRAPPER_MACROS. |
| Uses parenthesis matching to determine this. |
| |
| Returns True if wrapped, False otherwise. |
| Note: Assumes call_start points to a valid function call. |
| """ |
| # Look backwards from the call to see if we're inside a wrapping macro call |
| # The structure would be: MACRO_NAME( ... functionName( ... ) ... ) |
| |
| paren_depth = 0 # Track depth of nested parens we're inside |
| brace_depth = 0 # Track depth of nested braces (for temporary objects like Type{}) |
| i = call_start - 1 |
| |
| while i >= 0: |
| ch = content[i] |
| |
| if ch == ')': |
| paren_depth += 1 |
| elif ch == '(': |
| if paren_depth > 0: |
| paren_depth -= 1 |
| else: |
| # Found an enclosing opening paren at depth 0 |
| # Check if this belongs to a wrapping macro |
| paren_pos = i |
| |
| # Extract macro name before this paren (skip whitespace, then extract backwards) |
| macro_name_end = paren_pos |
| while macro_name_end > 0 and content[macro_name_end - 1] in ' \t\n\r': |
| macro_name_end -= 1 |
| macro_name_start = macro_name_end |
| while macro_name_start > 0 and (content[macro_name_start - 1].isalnum() or |
| content[macro_name_start - 1] == '_'): |
| macro_name_start -= 1 |
| |
| if macro_name_start != macro_name_end: |
| macro_name = content[macro_name_start:macro_name_end] |
| if macro_name in WRAPPER_MACROS: |
| return True |
| |
| # Not a wrapping macro, continue looking for outer parens |
| elif ch == '}': |
| brace_depth += 1 |
| elif ch == '{': |
| if brace_depth > 0: |
| brace_depth -= 1 |
| elif paren_depth == 0: |
| # Statement boundary (not inside parens) - stop looking |
| break |
| elif ch == ';' and paren_depth == 0 and brace_depth == 0: |
| # Statement boundary - stop looking |
| break |
| |
| i -= 1 |
| |
| return False |
| |
| |
| def find_unwrapped_vk_calls_in_file(file_path: str, |
| vk_functions: Set[str]) -> List[VkCallLocation]: |
| """Find all Vulkan API calls in a file that are not wrapped with the required macros.""" |
| |
| try: |
| with open(file_path, 'r', encoding='utf-8', errors='replace') as f: |
| original_content = f.read() |
| except Exception as e: |
| print(f"Error reading {file_path}: {e}", file=sys.stderr) |
| return [] |
| |
| # Remove comments and neutralize string literals (keep macro definitions) |
| cleaned_content = remove_comments_and_string_contents(original_content) |
| |
| vk_calls = [] |
| n = len(cleaned_content) |
| i = 0 |
| |
| # Track if we're inside a timer class scope (see TIMER_CLASS_NAME) |
| timer_scope_depth = 0 # 0 means not inside timer scope |
| brace_depth = 0 |
| |
| while i < n: |
| # Track brace depth for scope tracking |
| if cleaned_content[i] == '{': |
| brace_depth += 1 |
| i += 1 |
| continue |
| elif cleaned_content[i] == '}': |
| # Exit timer scope when leaving the brace level where timer was declared |
| if timer_scope_depth > 0 and brace_depth == timer_scope_depth: |
| timer_scope_depth = 0 |
| brace_depth -= 1 |
| i += 1 |
| continue |
| |
| # Check for timer class declaration (only if not already in a timer scope) |
| if timer_scope_depth == 0: |
| if cleaned_content[i:i + len(TIMER_CLASS_NAME)] == TIMER_CLASS_NAME: |
| # Found a timer declaration - track this scope |
| # The timer is at the current brace depth |
| timer_scope_depth = brace_depth |
| i += len(TIMER_CLASS_NAME) |
| continue |
| |
| # Look for identifier start (letter or underscore) |
| if cleaned_content[i].isalpha() or cleaned_content[i] == '_': |
| # Extract the full identifier name |
| func_name_start = i |
| func_name_end = i |
| while func_name_end < n and (cleaned_content[func_name_end].isalnum() or |
| cleaned_content[func_name_end] == '_'): |
| func_name_end += 1 |
| |
| func_name = cleaned_content[func_name_start:func_name_end] |
| |
| # Check if this is a known vk function name or variation |
| if func_name in vk_functions: |
| # Skip whitespace to find opening paren |
| open_paren_pos = func_name_end |
| while open_paren_pos < n and cleaned_content[open_paren_pos] in ' \t\n\r': |
| open_paren_pos += 1 |
| |
| if open_paren_pos < n and cleaned_content[open_paren_pos] == '(': |
| # Check if this function is in the denylist of non-Vulkan functions |
| if func_name in NON_VULKAN_FUNCTIONS: |
| # Skip - this is a known non-Vulkan function |
| i = func_name_end |
| continue |
| |
| # Check if this is a call without arguments - Vulkan API calls always have arguments |
| # Skip whitespace after opening paren |
| close_paren_pos = open_paren_pos + 1 |
| while close_paren_pos < n and cleaned_content[close_paren_pos] in ' \t\n\r': |
| close_paren_pos += 1 |
| |
| if close_paren_pos < n and cleaned_content[close_paren_pos] == ')': |
| # This is a call without arguments - not a Vulkan API call |
| i = func_name_end |
| continue |
| |
| # This is a vk*() call or declaration |
| # First check if this is a function declaration/definition |
| if is_function_declaration(cleaned_content, func_name_start): |
| # Skip function declarations/definitions |
| i = func_name_end |
| continue |
| |
| # Check if this is a member or namespace call (not a global Vulkan call) |
| if is_member_or_namespace_call(cleaned_content, func_name_start): |
| # Skip member/namespace calls |
| i = func_name_end |
| continue |
| |
| # Check if it's wrapped (by macro or inside timer scope) |
| inside_timer_scope = (timer_scope_depth > 0) |
| if not inside_timer_scope and not is_wrapped_in_macro( |
| cleaned_content, func_name_start): |
| # Unwrapped call found |
| line_num = get_line_number(cleaned_content, func_name_start) |
| |
| # Get the single line containing the function name |
| line_start = cleaned_content.rfind('\n', 0, func_name_start) + 1 |
| line_end = cleaned_content.find('\n', func_name_start) |
| if line_end == -1: |
| line_end = n |
| |
| # Get context chars before and after the function name, limited to the line |
| context_start = max(line_start, func_name_start - CONTEXT_CHAR_LIMIT) |
| context_end = min(line_end, func_name_end + CONTEXT_CHAR_LIMIT) |
| context = cleaned_content[context_start:context_end].strip() |
| |
| vk_calls.append( |
| VkCallLocation( |
| file_path=file_path, |
| line_number=line_num, |
| call_text=func_name, |
| context=context)) |
| |
| i = func_name_end |
| else: |
| i += 1 |
| |
| return vk_calls |
| |
| |
| def find_source_files(directory: str, exclude_dir: str = None) -> List[str]: |
| """Find all source files in the directory with configured extensions (see SOURCE_FILE_EXTENSIONS). |
| If exclude_dir is specified, skip that directory during traversal. |
| Comparison is done by full path (not just directory name) to avoid accidentally |
| excluding directories that happen to share the same name at different locations.""" |
| source_files = [] |
| |
| exclude_dir_abs = os.path.abspath(exclude_dir) if exclude_dir else None |
| |
| for root, dirs, files in os.walk(directory): |
| # Modify dirs in-place to control which subdirectories os.walk descends into. |
| if exclude_dir_abs: |
| dirs[:] = [ |
| d for d in dirs if os.path.abspath(os.path.join(root, d)) != exclude_dir_abs |
| ] |
| |
| for file in files: |
| if file.endswith(SOURCE_FILE_EXTENSIONS): |
| full_path = os.path.join(root, file) |
| source_files.append(full_path) |
| |
| return source_files |
| |
| |
| def _process_file_wrapper(args): |
| """Wrapper function for parallel processing of files.""" |
| file_path, vk_functions = args |
| return find_unwrapped_vk_calls_in_file(file_path, vk_functions) |
| |
| |
| def main(): |
| volk_h_path = os.path.relpath(VOLK_H_PATH) |
| default_search_dir = os.path.relpath(DEFAULT_SEARCH_DIR) |
| test_dir = os.path.relpath(TEST_DIR) |
| |
| parser = argparse.ArgumentParser( |
| description='Find Vulkan API calls that are NOT wrapped with wrapper macros.') |
| parser.add_argument( |
| '--search-dir', |
| default=default_search_dir, |
| help=f'Directory to search for source files (default: "{default_search_dir}")') |
| parser.add_argument( |
| '--exclude-dir', |
| default=None, |
| help=f'Directory to exclude from search (default: "{test_dir}" ' |
| f'when --search-dir is also not specified)') |
| args = parser.parse_args() |
| |
| # Default exclude directory when using default search directory |
| exclude_dir = args.exclude_dir |
| if exclude_dir is None and args.search_dir == default_search_dir: |
| exclude_dir = test_dir |
| |
| # Check if volk.h exists |
| if not os.path.isfile(volk_h_path): |
| print(f"Error: volk.h file not found: {volk_h_path}", file=sys.stderr) |
| sys.exit(1) |
| |
| # Check if search directory exists |
| if not os.path.isdir(args.search_dir): |
| print(f"Error: Search directory not found: {args.search_dir}", file=sys.stderr) |
| sys.exit(1) |
| |
| # Extract Vulkan function names from volk.h |
| print(f"Extracting Vulkan function names from: {volk_h_path}") |
| vk_base_functions = extract_vk_function_names_from_volk(volk_h_path) |
| print(f"Found {len(vk_base_functions)} Vulkan function names in volk.h") |
| |
| if not vk_base_functions: |
| print("Error: No Vulkan functions found in volk.h", file=sys.stderr) |
| sys.exit(1) |
| |
| # Generate all naming variations |
| print("Generating naming variations...") |
| vk_functions = generate_all_function_variations(vk_base_functions) |
| print(f"Total function names and variations: {len(vk_functions)}") |
| print() |
| |
| # Find all source files |
| print(f"Searching for source files in: {args.search_dir}") |
| source_files = find_source_files(args.search_dir, exclude_dir) |
| print(f"Found {len(source_files)} source files to analyze") |
| print() |
| |
| # Find all unwrapped Vulkan calls using parallel processing |
| all_unwrapped_calls = [] |
| |
| # Prepare arguments for parallel processing |
| process_args = [(fp, vk_functions) for fp in sorted(source_files)] |
| |
| # Use ProcessPoolExecutor for parallel file processing |
| with ProcessPoolExecutor() as executor: |
| results = executor.map(_process_file_wrapper, process_args) |
| for unwrapped in results: |
| all_unwrapped_calls.extend(unwrapped) |
| |
| # Print results |
| print("=" * 80) |
| if all_unwrapped_calls: |
| print(f"Found {len(all_unwrapped_calls)} unwrapped Vulkan API calls") |
| else: |
| print("All Vulkan API calls are properly wrapped.") |
| print("=" * 80) |
| print() |
| |
| if not all_unwrapped_calls: |
| return |
| |
| # Group by file |
| calls_by_file = {} |
| for call in all_unwrapped_calls: |
| if call.file_path not in calls_by_file: |
| calls_by_file[call.file_path] = [] |
| calls_by_file[call.file_path].append(call) |
| |
| # Print summary by file |
| for file_path in sorted(calls_by_file.keys()): |
| calls = calls_by_file[file_path] |
| print(f"\n{file_path}: {len(calls)} unwrapped call(s)") |
| print("-" * 60) |
| for call in calls: |
| print(f" Line {call.line_number}: {call.context}") |
| |
| # Print summary |
| print("\n" + "=" * 80) |
| print("SUMMARY") |
| print("=" * 80) |
| print(f"Total files with unwrapped calls: {len(calls_by_file)}") |
| print(f"Total unwrapped Vulkan API calls: {len(all_unwrapped_calls)}") |
| |
| # Print unique function names |
| unique_calls = sorted(set(call.call_text for call in all_unwrapped_calls)) |
| print(f"\nUnique Vulkan functions called without wrapping: {len(unique_calls)}") |
| for name in unique_calls: |
| count = sum(1 for call in all_unwrapped_calls if call.call_text == name) |
| print(f" {name}: {count} occurrence(s)") |
| |
| # Exit with code 2 to indicate unwrapped calls were found |
| # (code 0 = all wrapped, code 1 = error, code 2 = unwrapped calls found) |
| sys.exit(2) |
| |
| |
| if __name__ == "__main__": |
| main() |