Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions .github/workflows/native_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
<!-- #if !defined(standalone) -->

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].
<!-- #endif -->

## What are layer drivers?

Expand All @@ -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.
<!-- #if !defined(standalone) -->

## What is the purpose of this project?

Expand Down Expand Up @@ -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.
<!-- #endif -->

# License

Expand All @@ -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)
<!-- #if !defined(standalone) -->

Layer documentation

Expand All @@ -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].
<!-- #endif -->

- - -

_Copyright © 2024-2026, Arm Limited and contributors._
<!-- #if !defined(standalone) -->

[1]: https://community.arm.com/support-forums/f/graphics-gaming-and-vr-forum/
[2]: https://developer.arm.com/Tools%20and%20Software/Arm%20Performance%20Studio
<!-- #endif -->
31 changes: 24 additions & 7 deletions docs/creating.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -34,7 +34,24 @@ 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 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)

Expand Down Expand Up @@ -146,4 +163,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._
178 changes: 153 additions & 25 deletions generator/generate_vulkan_layer.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -26,13 +26,44 @@
'''

import argparse
import os
import re
import shutil
import sys
from pathlib import Path
from typing import TextIO


COMMON_DIRECTORIES = (
'cmake',
'docs',
'generator',
'lglpy',
'source_common',
)

THIRD_PARTY_DIRECTORIES = (
'gtest',
'khronos',
)

COMMON_FILES = (
'CMakeLists.txt',
'lgl_android_install.py',
'lgl_host_server.py',
'LICENSE.txt',
'README.md',
)

COPY_IGNORE_PATTERNS = (
'.git',
'__pycache__',
'*.pyc',
)

STANDALONE_EXCLUDE_START = '<!-- #if !defined(standalone) -->'
STANDALONE_EXCLUDE_END = '<!-- #endif -->'


def load_template(path: str) -> str:
'''
Load a template from the generator template directory.
Expand All @@ -43,11 +74,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
Expand Down Expand Up @@ -165,17 +196,80 @@ 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.

Args:
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_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.

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 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
if filename == 'README.md':
copy_standalone_readme(source, destination)
else:
shutil.copy2(source, destination)


def parse_command_line() -> argparse.Namespace:
Expand All @@ -196,8 +290,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')
Expand Down Expand Up @@ -225,33 +322,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
Expand Down