From 2cba3f65744b39a86771e9d98a66026866a760fe Mon Sep 17 00:00:00 2001 From: Peter Harris Date: Thu, 22 Oct 2026 14:59:55 +0100 Subject: [PATCH 1/3] Allow layer to be outside of the project tree --- .github/workflows/native_test.yaml | 23 ++++- docs/creating.md | 29 +++++-- generator/generate_vulkan_layer.py | 129 +++++++++++++++++++++++------ 3 files changed, 146 insertions(+), 35 deletions(-) diff --git a/.github/workflows/native_test.yaml b/.github/workflows/native_test.yaml index 17d2fd0..f68d13d 100644 --- a/.github/workflows/native_test.yaml +++ b/.github/workflows/native_test.yaml @@ -174,8 +174,8 @@ jobs: run: | git diff --exit-code - build-ubuntu-x64-clang-new-project: - name: Ubuntu x64 generate new layer + build-ubuntu-x64-clang-new-layer: + name: Ubuntu x64 generate new in-tree layer runs-on: ubuntu-22.04 steps: - name: Git checkout @@ -186,8 +186,25 @@ jobs: - name: Generate layer_test run: | python3 ./generator/generate_vulkan_common.py - python3 ./generator/generate_vulkan_layer.py --project-name Test --layer-name VkLayerTest --output layer_test + python3 ./generator/generate_vulkan_layer.py --project-name Test --layer-name VkLayerTest --layer-dir layer_test mkdir layer_test/build_rel cd layer_test/build_rel cmake -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Release .. cmake --build . + + build-ubuntu-x64-new-standalone-layer: + name: Ubuntu x64 generate new standalone layer + runs-on: ubuntu-22.04 + steps: + - name: Git checkout + uses: actions/checkout@v6 + with: + submodules: 'true' + + - name: Generate and build standalone example layer + run: | + root_dir="${RUNNER_TEMP}/standalone_project" + layer_dir="${root_dir}/layer_example" + python3 ./generator/generate_vulkan_layer.py --project-name Example --layer-name VkLayerExample --root-dir "${root_dir}" --layer-dir layer_example + cmake -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Release -S "${layer_dir}" -B "${layer_dir}/build_rel" + cmake --build "${layer_dir}/build_rel" diff --git a/docs/creating.md b/docs/creating.md index d13d812..99ee069 100644 --- a/docs/creating.md +++ b/docs/creating.md @@ -15,14 +15,14 @@ git clone https://github.com/ARM-software/libGPUlayers ./ git submodule update --init ``` -## Generate the new layer project +## Generate a new layer in this project -Create your new layer, using a Python script to generate the project -directory containing the layer code. Replace the placeholder "Demo" with your -layer name. +You can create a new layer as a component of this project, sharing common code with the other layers in this repository. Create your layer using the generator Python script, which will populate the layer directory with a bare-bones layer and build system that you can start extending to add your desired functionality. + +Replace the placeholder "Demo" in the instructions below with your desired layer name. ```sh -python3 ./generator/generate_vulkan_layer.py --project-name VkLayerDemo --output layer_demo +python3 ./generator/generate_vulkan_layer.py --project-name VkLayerDemo --layer-dir layer_demo ``` * The Vulkan layer name must start with `VkLayer` and have a title-case name, @@ -34,7 +34,22 @@ python3 ./generator/generate_vulkan_layer.py --project-name VkLayerDemo --output You now have a complete layer that is ready to use! This layer does nothing useful yet, but it is functional and is deployable. You must now edit the layer -code to make it do something useful. +code to implement your desired behavior. + +### Generate a standalone project + +Alternatively, you can create a standalone layer that does not depend on the original project checkout. Create your new project, including a new layer, using the generator Python script. Specify both the project root directory and a layer directory: + +```sh +python3 ./generator/generate_vulkan_layer.py --project-name VkLayerDemo --root-dir ../standalone_demo --layer-dir layer_demo +``` + +The new project root must not be a descendant of the original project. The new layer directory must be a direct child of the new project root. The generator +copies the common source code and supporting tools into the new project root, +and generates the new layer in the named layer directory. + +> [!IMPORTANT] +> The new project will be populated with a copy of the code from the `source_third_party` directory. It is important that the submodules in this directory are initialized before you perform the standalone layer generation. ### Generate the common code (optional) @@ -146,4 +161,4 @@ implementations are found in `source_common/framework/manual_functions.cpp`. - - - -_Copyright © 2024-2025, Arm Limited and contributors._ +_Copyright © 2024-2026, Arm Limited and contributors._ diff --git a/generator/generate_vulkan_layer.py b/generator/generate_vulkan_layer.py index cbe306f..61c6cf7 100755 --- a/generator/generate_vulkan_layer.py +++ b/generator/generate_vulkan_layer.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # SPDX-License-Identifier: MIT # ----------------------------------------------------------------------------- -# Copyright (c) 2024-2025 Arm Limited +# Copyright (c) 2024-2026 Arm Limited # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to @@ -26,13 +26,35 @@ ''' import argparse -import os import re import shutil import sys +from pathlib import Path from typing import TextIO +COMMON_DIRECTORIES = ( + 'cmake', + 'generator', + 'lglpy', + 'source_common', + 'source_third_party', +) + +COMMON_FILES = ( + 'CMakeLists.txt', + 'lgl_android_install.py', + 'lgl_host_server.py', + 'LICENSE.txt', +) + +COPY_IGNORE_PATTERNS = ( + '.git', + '__pycache__', + '*.pyc', +) + + def load_template(path: str) -> str: ''' Load a template from the generator template directory. @@ -43,11 +65,11 @@ def load_template(path: str) -> str: Returns: The loaded text template. ''' - base_dir = os.path.dirname(__file__) - template_file = os.path.join(base_dir, 'vk_codegen', path) + base_dir = Path(__file__).parent + template_file = base_dir / 'vk_codegen' / path # Load the data from the file - with open(template_file, 'r', encoding='utf-8') as handle: + with template_file.open('r', encoding='utf-8') as handle: data = handle.read() return data @@ -165,7 +187,7 @@ def generate_install_manifest(file: TextIO, vendor: str, layer: str) -> None: file.write(data) -def copy_resource(src_dir: str, dst_dir: str) -> None: +def copy_resource(src_dir: Path, dst_dir: Path) -> None: ''' Copy a directory of resources to another location. @@ -173,9 +195,32 @@ def copy_resource(src_dir: str, dst_dir: str) -> None: src_dir: The source directory location. out_dir: The destination directory location. ''' - dst_dir = os.path.abspath(dst_dir) - os.makedirs(dst_dir, exist_ok=True) - shutil.copytree(src_dir, dst_dir, dirs_exist_ok=True) + dst_dir = dst_dir.resolve() + dst_dir.mkdir(parents=True, exist_ok=True) + shutil.copytree( + src_dir, + dst_dir, + dirs_exist_ok=True, + ignore=shutil.ignore_patterns(*COPY_IGNORE_PATTERNS)) + + +def copy_common_resources(src_dir: Path, dst_dir: Path) -> None: + ''' + Copy common project resources for a standalone layer project. + + Args: + src_dir: The source project root directory. + dst_dir: The destination project root directory. + ''' + for directory in COMMON_DIRECTORIES: + source = src_dir / directory + destination = dst_dir / directory + copy_resource(source, destination) + + for filename in COMMON_FILES: + source = src_dir / filename + destination = dst_dir / filename + shutil.copy2(source, destination) def parse_command_line() -> argparse.Namespace: @@ -196,8 +241,11 @@ def parse_command_line() -> argparse.Namespace: parser.add_argument('--layer-name', default=None, help='layer name (optional)') - parser.add_argument('--output', metavar='DIR', required=True, - help='output directory for layer project') + parser.add_argument('--root-dir', metavar='DIR', + help='project root directory (optional)') + + parser.add_argument('--layer-dir', metavar='DIR', required=True, + help='layer directory within the project root') parser.add_argument('--overwrite', action='store_true', default=False, help='overwrite an existing output directory') @@ -225,33 +273,64 @@ def main() -> int: print(f'ERROR: Layer name "{args.layer_name}" must start with VkLayer') return 1 + base_dir = Path(__file__).parent + project_dir = base_dir.parent.resolve() + + # A root outside this checkout creates a standalone project. If no root is + # specified, or this checkout is specified, generate a local layer. + if args.root_dir: + root_dir = Path(args.root_dir).resolve() + else: + root_dir = project_dir + + standalone = root_dir != project_dir + if standalone and root_dir.is_relative_to(project_dir): + print('ERROR: Standalone root dir must not be inside current project') + return 1 + + layer_dir = Path(args.layer_dir) + if layer_dir.is_absolute(): + outdir = layer_dir.resolve() + else: + outdir = (root_dir / layer_dir).resolve() + + if outdir.parent != root_dir: + print('ERROR: Layer dir must be a direct child of the root') + return 1 + + chk_dir = root_dir if standalone else outdir + chk_dir_t = 'Root' if standalone else 'Layer' + # Check that output directory is either empty or over-writable - outdir = args.output - if os.path.exists(outdir): - if not os.path.isdir(outdir): - print(f'ERROR: Output location "{outdir}" is not a directory') + if chk_dir.exists(): + if not chk_dir.is_dir(): + print(f'ERROR: {chk_dir_t} location "{chk_dir}" is not a dir') return 1 - if len(os.listdir(outdir)) != 0 and not args.overwrite: - print(f'ERROR: Output directory "{outdir}" is not empty') + if any(chk_dir.iterdir()) and not args.overwrite: + print(f'ERROR: {chk_dir_t} dir "{chk_dir}" is not empty') return 1 + # Copy project-wide resources for standalone output. + if standalone: + root_dir.mkdir(parents=True, exist_ok=True) + copy_common_resources(project_dir, root_dir) + # Generate static resources - base_dir = os.path.dirname(__file__) - source_dir = os.path.join(base_dir, 'vk_layer') + source_dir = base_dir / 'vk_layer' copy_resource(source_dir, outdir) # Generate templated resources - outfile = os.path.join(outdir, 'CMakeLists.txt') - with open(outfile, 'w', encoding='utf-8', newline='\n') as handle: + outfile = outdir / 'CMakeLists.txt' + with outfile.open('w', encoding='utf-8', newline='\n') as handle: generate_root_cmake(handle, args.project_name, args.layer_name) - outfile = os.path.join(outdir, 'source/CMakeLists.txt') - with open(outfile, 'w', encoding='utf-8', newline='\n') as handle: + outfile = outdir / 'source' / 'CMakeLists.txt' + with outfile.open('w', encoding='utf-8', newline='\n') as handle: generate_source_cmake(handle, args.vendor_name, args.layer_name) - outfile = os.path.join(outdir, 'manifest.json') - with open(outfile, 'w', encoding='utf-8', newline='\n') as handle: + outfile = outdir / 'manifest.json' + with outfile.open('w', encoding='utf-8', newline='\n') as handle: generate_install_manifest(handle, args.vendor_name, args.layer_name) return 0 From 1bb0aadbd1a624e10bdd5c6f01c2afefd7dae175 Mon Sep 17 00:00:00 2001 From: Peter Harris Date: Thu, 22 Oct 2026 16:22:03 +0100 Subject: [PATCH 2/3] Reduce scope of copied third-party dependencies --- docs/creating.md | 4 +++- generator/generate_vulkan_layer.py | 11 ++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/docs/creating.md b/docs/creating.md index 99ee069..cc9aa5c 100644 --- a/docs/creating.md +++ b/docs/creating.md @@ -49,7 +49,9 @@ copies the common source code and supporting tools into the new project root, and generates the new layer in the named layer directory. > [!IMPORTANT] -> The new project will be populated with a copy of the code from the `source_third_party` directory. It is important that the submodules in this directory are initialized before you perform the standalone layer generation. +> The new project will include copies of the `gtest` and `khronos` directories +> from `source_third_party`. Ensure the submodules in these directories are +> initialized before generating the standalone layer. ### Generate the common code (optional) diff --git a/generator/generate_vulkan_layer.py b/generator/generate_vulkan_layer.py index 61c6cf7..d5e5ff9 100755 --- a/generator/generate_vulkan_layer.py +++ b/generator/generate_vulkan_layer.py @@ -38,7 +38,11 @@ 'generator', 'lglpy', 'source_common', - 'source_third_party', +) + +THIRD_PARTY_DIRECTORIES = ( + 'gtest', + 'khronos', ) COMMON_FILES = ( @@ -217,6 +221,11 @@ def copy_common_resources(src_dir: Path, dst_dir: Path) -> None: destination = dst_dir / directory copy_resource(source, destination) + for directory in THIRD_PARTY_DIRECTORIES: + source = src_dir / 'source_third_party' / directory + destination = dst_dir / 'source_third_party' / directory + copy_resource(source, destination) + for filename in COMMON_FILES: source = src_dir / filename destination = dst_dir / filename From 8d8204ebe24e88c255174f4ea3f8317b05d8f6f5 Mon Sep 17 00:00:00 2001 From: Peter Harris Date: Thu, 22 Oct 2026 16:34:59 +0100 Subject: [PATCH 3/3] Clone documentation, excluding upstream-specifics --- README.md | 8 ++++++ generator/generate_vulkan_layer.py | 42 +++++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 75c4154..dc6c36a 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,13 @@ libGPULayers provides tooling to create new Vulkan layer drivers, allowing you to quickly generate new layers suitable for creation of new developer tools or for ad hoc experiments during development. + In addition, we provide a number of pre-built layers that have been built using this framework. These layers might be used as standalone tools in their own right, or might be used alongside other Arm tools such as [Arm Performance Studio][2]. + ## What are layer drivers? @@ -23,6 +25,7 @@ Layer drivers are designed in to the Vulkan API, and they are the mechanism for common workflows such as error checking using the Vulkan Validation Layer (VVL), but they are also a general purpose mechanism suitable for all kinds of developer tooling. + ## What is the purpose of this project? @@ -51,6 +54,7 @@ which you use to quickly create a new layer that is ready to deploy. This library is currently tested on devices running Android or Linux, and using Arm® Immortalis™ and Arm Mali™ GPUs. Contributions adding support for other platforms are welcome. + # License @@ -69,6 +73,7 @@ Common documentation * [Running using a layer on Linux](./docs/running_linux.md) * [About layers design notes](./docs/about_layers.md) * [Frequently asked questions](./docs/faq.md) + Layer documentation @@ -84,10 +89,13 @@ raise them in the project's GitHub issue tracker. If you have any questions about Arm GPUs, application development for Arm GPUs, or general mobile graphics development or technology, please submit them on the [Arm Community graphics forums][1]. + - - - _Copyright © 2024-2026, Arm Limited and contributors._ + [1]: https://community.arm.com/support-forums/f/graphics-gaming-and-vr-forum/ [2]: https://developer.arm.com/Tools%20and%20Software/Arm%20Performance%20Studio + diff --git a/generator/generate_vulkan_layer.py b/generator/generate_vulkan_layer.py index d5e5ff9..0f8338b 100755 --- a/generator/generate_vulkan_layer.py +++ b/generator/generate_vulkan_layer.py @@ -35,6 +35,7 @@ COMMON_DIRECTORIES = ( 'cmake', + 'docs', 'generator', 'lglpy', 'source_common', @@ -50,6 +51,7 @@ 'lgl_android_install.py', 'lgl_host_server.py', 'LICENSE.txt', + 'README.md', ) COPY_IGNORE_PATTERNS = ( @@ -58,6 +60,9 @@ '*.pyc', ) +STANDALONE_EXCLUDE_START = '' +STANDALONE_EXCLUDE_END = '' + def load_template(path: str) -> str: ''' @@ -208,6 +213,38 @@ def copy_resource(src_dir: Path, dst_dir: Path) -> None: ignore=shutil.ignore_patterns(*COPY_IGNORE_PATTERNS)) +def copy_standalone_readme(src_file: Path, dst_file: Path) -> None: + ''' + Copy the project README, excluding non-standalone content blocks. + + Args: + src_file: The source README file. + dst_file: The destination README file. + ''' + output = [] + excluding = False + + with src_file.open('r', encoding='utf-8') as handle: + for line in handle: + marker = line.strip() + if marker == STANDALONE_EXCLUDE_START: + if excluding: + raise ValueError('Nested standalone exclusion block') + excluding = True + elif marker == STANDALONE_EXCLUDE_END: + if not excluding: + raise ValueError('Unmatched standalone exclusion end') + excluding = False + elif not excluding: + output.append(line) + + if excluding: + raise ValueError('Unterminated standalone exclusion block') + + with dst_file.open('w', encoding='utf-8', newline='\n') as handle: + handle.writelines(output) + + def copy_common_resources(src_dir: Path, dst_dir: Path) -> None: ''' Copy common project resources for a standalone layer project. @@ -229,7 +266,10 @@ def copy_common_resources(src_dir: Path, dst_dir: Path) -> None: for filename in COMMON_FILES: source = src_dir / filename destination = dst_dir / filename - shutil.copy2(source, destination) + if filename == 'README.md': + copy_standalone_readme(source, destination) + else: + shutil.copy2(source, destination) def parse_command_line() -> argparse.Namespace: