diff --git a/.github/actions/build-container/action.yml b/.github/actions/build-container/action.yml
new file mode 100644
index 00000000..a2921688
--- /dev/null
+++ b/.github/actions/build-container/action.yml
@@ -0,0 +1,48 @@
+name: 'Build Container'
+description: 'Builds and pushes the Linux Docker image'
+inputs:
+ dockerfile:
+ description: 'Path to the Dockerfile'
+ required: true
+ tag:
+ description: 'Tag suffix for the image'
+ required: true
+ image-subdir:
+ description: 'Subdirectory to push image to'
+ default: 'cache'
+
+outputs:
+ container-image-adress:
+ description: Address of container image
+ value: ${{ steps.set-image.outputs.image }}
+
+runs:
+ using: "composite"
+ steps:
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v4
+
+ - name: Set image names
+ id: set-image
+ shell: bash
+ run: |
+ image="ghcr.io/${{ github.repository }}/${{ inputs.image-subdir }}:${{ inputs.tag }}"
+ echo "image=$image" >> $GITHUB_OUTPUT
+ echo "cache_image=${image}-cache" >> $GITHUB_OUTPUT
+
+ - name: Log in to the Container registry
+ uses: docker/login-action@v4
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ github.token }}
+
+ - name: Build Docker image
+ uses: docker/build-push-action@v7
+ with:
+ context: .
+ file: ${{ inputs.dockerfile }}
+ push: true
+ tags: ${{ steps.set-image.outputs.image }}
+ cache-from: type=registry,ref=${{ steps.set-image.outputs.cache_image }}
+ cache-to: type=registry,ref=${{ steps.set-image.outputs.cache_image }},mode=max
\ No newline at end of file
diff --git a/.github/actions/build-engine/action.yml b/.github/actions/build-engine/action.yml
new file mode 100644
index 00000000..a9fb274b
--- /dev/null
+++ b/.github/actions/build-engine/action.yml
@@ -0,0 +1,49 @@
+name: 'Build engine'
+description: 'Handles Caching, Configuration, and Compilation'
+inputs:
+ cache_prefix:
+ description: 'Cache Prefix e.g. OS'
+ required: true
+ preset:
+ description: 'CI preset from ci/CMakeUserPresets.json'
+ required: true
+
+runs:
+ using: "composite"
+ steps:
+ # --- 1. setup vcpkg cache ---
+ - name: setup vcpkg
+ uses: ./.github/actions/setup-vcpkg-cache
+
+ # --- 2. configure ---
+ - name: Configure CMake
+ shell: bash
+ run: |
+ [ -f ci/CMakeUserPresets.json ] && cp ci/CMakeUserPresets.json CMakeUserPresets.json
+ cmake --preset ${{ inputs.preset }}
+
+ # --- 3. restore BuildCache ---
+ - name: Restore BuildCache
+ id: buildcache
+ uses: ./.github/actions/restore-cache
+ with:
+ path: .buildcache
+ key: buildcache-${{ inputs.cache_prefix }}-${{ inputs.preset }}-${{ github.run_id }}
+ restore-keys: buildcache-${{ inputs.cache_prefix }}-${{ inputs.preset }}-
+ global-tag: buildcache-${{ inputs.cache_prefix }}-${{ inputs.preset }}
+ gh-token: ${{ github.token }}
+
+ # --- 4. build ---
+ - name: Build
+ shell: bash
+ run: cmake --build --preset ${{ inputs.preset }}
+
+ # --- 5. save BuildCache ---
+ - name: Save BuildCache
+ if: always()
+ uses: ./.github/actions/save-cache
+ with:
+ path: .buildcache
+ key: ${{ steps.buildcache.outputs.primary-key }}
+ global-tag: buildcache-${{ inputs.cache_prefix }}-${{ inputs.preset }}
+ gh-token: ${{ github.token }}
diff --git a/.github/actions/restore-cache/action.yml b/.github/actions/restore-cache/action.yml
new file mode 100644
index 00000000..c165d23f
--- /dev/null
+++ b/.github/actions/restore-cache/action.yml
@@ -0,0 +1,73 @@
+name: 'Restore Cache'
+description: 'Restores from GitHub Cache or GHCR'
+inputs:
+ path:
+ description: 'File path to restore'
+ required: true
+ key:
+ description: 'Primary key for GitHub Cache'
+ required: true
+ restore-keys:
+ description: 'Fallback keys for GitHub Cache'
+ required: false
+ global-tag:
+ description: 'OCI Tag for GHCR'
+ required: true
+ gh-token:
+ description: 'GitHub Token for GHCR auth'
+ required: true
+ default: ${{ github.token }}
+
+outputs:
+ cache-hit:
+ description: 'true if cache was found in either github or GHCR'
+ value: ${{ steps.local-cache.outputs.cache-hit == 'true' || steps.global-cache.outputs.hit == 'true' }}
+ primary-key:
+ description: 'The key used for saving later'
+ value: ${{ steps.local-cache.outputs.cache-primary-key }}
+
+runs:
+ using: "composite"
+ steps:
+ # --- Local GitHub cache ---
+ - name: Restore local cache
+ id: local-cache
+ uses: actions/cache/restore@v6
+ with:
+ path: ${{ inputs.path }}
+ key: ${{ inputs.key }}
+ restore-keys: ${{ inputs.restore-keys }}
+
+ # --- Setup ORAS (Only if local missed) ---
+ - name: Set up ORAS
+ if: steps.local-cache.outputs.cache-matched-key == ''
+ uses: oras-project/setup-oras@v2
+
+ # --- Login ---
+ - name: Login to GHCR (Native ORAS)
+ if: steps.local-cache.outputs.cache-matched-key == ''
+ shell: bash
+ run: |
+ echo "${{ inputs.gh-token }}" | oras login ghcr.io \
+ --username "${{ github.actor }}" \
+ --password-stdin
+
+ # --- Global GHCR cache ---
+ - name: Restore global cache
+ id: global-cache
+ if: steps.local-cache.outputs.cache-matched-key == ''
+ shell: bash
+ env:
+ REPO: ghcr.io/${{ github.repository }}
+ TAG: master-${{ inputs.global-tag }}
+ TARGET_PATH: ${{ inputs.path }}
+ run: |
+ echo "Local miss. Pulling from GHCR: $TAG"
+ if oras pull "$REPO/cache:$TAG"; then
+ echo "hit=true" >> $GITHUB_OUTPUT
+ tar -xzf cache-archive.tar.gz
+ rm cache-archive.tar.gz
+ else
+ echo "hit=false" >> $GITHUB_OUTPUT
+ echo "Global cache miss."
+ fi
\ No newline at end of file
diff --git a/.github/actions/save-cache/action.yml b/.github/actions/save-cache/action.yml
new file mode 100644
index 00000000..990f2276
--- /dev/null
+++ b/.github/actions/save-cache/action.yml
@@ -0,0 +1,58 @@
+name: 'Save Cache'
+description: 'Saves to GitHub Cache (Always) and GHCR (Only on master)'
+inputs:
+ path:
+ description: 'File path to save'
+ required: true
+ key:
+ description: 'Key used in the restore step'
+ required: true
+ global-tag:
+ description: 'OCI Tag for GHCR'
+ required: true
+ gh-token:
+ description: 'GitHub Token for GHCR auth'
+ required: true
+ default: ${{ github.token }}
+
+runs:
+ using: "composite"
+ steps:
+ # --- Local github cache ---
+ - name: Save Local Cache
+ uses: actions/cache/save@v6
+ with:
+ path: ${{ inputs.path }}
+ key: ${{ inputs.key }}
+
+ # --- Setup for Global (Only on master) ---
+ - name: Set up ORAS
+ if: github.ref == 'refs/heads/master'
+ uses: oras-project/setup-oras@v2
+
+ # --- Login ---
+ - name: Login to GHCR (Native ORAS)
+ if: github.ref == 'refs/heads/master'
+ shell: bash
+ run: |
+ echo "${{ inputs.gh-token }}" | oras login ghcr.io \
+ --username "${{ github.actor }}" \
+ --password-stdin
+
+ # --- Global GHCR cache (master only) ---
+ - name: Save Global Cache
+ if: github.ref == 'refs/heads/master'
+ shell: bash
+ env:
+ REPO: ghcr.io/${{ github.repository }}
+ TAG: master-${{ inputs.global-tag }}
+ TARGET_PATH: ${{ inputs.path }}
+ run: |
+ if [ -d "$TARGET_PATH" ]; then
+ echo "Compressing for Global Cache..."
+ tar -czf cache-archive.tar.gz "$TARGET_PATH"
+
+ echo "Pushing to GHCR..."
+ oras push "$REPO/cache:$TAG" cache-archive.tar.gz
+ rm cache-archive.tar.gz
+ fi
\ No newline at end of file
diff --git a/.github/actions/setup-vcpkg-cache/action.yml b/.github/actions/setup-vcpkg-cache/action.yml
new file mode 100644
index 00000000..b51bd6c4
--- /dev/null
+++ b/.github/actions/setup-vcpkg-cache/action.yml
@@ -0,0 +1,42 @@
+name: 'Vcpkg Cache'
+description: 'sets up vcpkg to use NuGet for caching'
+inputs:
+ gh-token:
+ description: 'GitHub Token for GHCR auth'
+ required: true
+ default: ${{ github.token }}
+
+runs:
+ using: "composite"
+ steps:
+ # --- WINDOWS: (dotnet nuget) ---
+ - name: setup vcpkg nuget source
+ if: runner.os == 'Windows'
+ shell: bash
+ run: |
+ dotnet nuget add source "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json" \
+ --name "GitHub" \
+ --username "${{ github.repository_owner }}" \
+ --password "${{ inputs.gh-token }}" \
+ --store-password-in-clear-text
+
+ # --- other: (mono + nuget.exe) ---
+ - name: setup vcpkg nuget source
+ if: runner.os != 'Windows'
+ shell: bash
+ run: |
+ # download nuget.exe
+ wget -qO nuget.exe https://dist.nuget.org/win-x86-commandline/latest/nuget.exe
+ NUGET_PATH="$(pwd)/nuget.exe"
+
+ # add source
+ mono "$NUGET_PATH" sources add \
+ -Name "GitHub" \
+ -Source "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json" \
+ -UserName "${{ github.repository_owner }}" \
+ -Password "${{ inputs.gh-token }}" \
+ -StorePasswordInClearText
+
+ # set API key for push
+ mono "$NUGET_PATH" setapikey "${{ inputs.gh-token }}" \
+ -Source "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json"
\ No newline at end of file
diff --git a/.github/actions/setup-windows/action.yml b/.github/actions/setup-windows/action.yml
new file mode 100644
index 00000000..647a7ff0
--- /dev/null
+++ b/.github/actions/setup-windows/action.yml
@@ -0,0 +1,47 @@
+name: 'Setup Windows Environment'
+description: 'Sets up MSVC, Vcpkg, BuildCache, CMake and Ninja'
+inputs:
+ compiler:
+ description: 'compiler of the CI preset (msvc or clang)'
+ required: true
+
+runs:
+ using: "composite"
+ steps:
+ - uses: ilammy/msvc-dev-cmd@v1
+
+ - name: Setup Vcpkg Environment
+ shell: pwsh
+ run: |
+ $vcpkgPath = $env:VCPKG_INSTALLATION_ROOT
+ if (-not (Test-Path "$vcpkgPath")) {
+ Write-Error "Vcpkg not found at VCPKG_INSTALLATION_ROOT ($vcpkgPath)"
+ exit 1
+ }
+ "VCPKG_ROOT=$vcpkgPath" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
+
+ - name: Install BuildCache
+ shell: pwsh
+ run: |
+ Invoke-WebRequest -Uri "https://gitlab.com/bits-n-bites/buildcache/-/releases/v0.31.7/downloads/buildcache-windows.zip" -OutFile "buildcache.zip"
+ Expand-Archive buildcache.zip -DestinationPath c:\buildcache
+ echo "c:\buildcache\buildcache\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
+
+ - name: Install LLVM
+ if: inputs.compiler == 'clang'
+ shell: pwsh
+ run: |
+ $version = "23.1.2"
+ $name = "clang+llvm-$version-x86_64-pc-windows-msvc"
+ curl.exe -fsSL -o "$env:RUNNER_TEMP\llvm.tar.xz" "https://github.com/llvm/llvm-project/releases/download/llvmorg-$version/$name.tar.xz"
+ cmd /c "7z x %RUNNER_TEMP%\llvm.tar.xz -so | 7z x -si -ttar -oc:\llvm > nul"
+ & "c:\llvm\$name\bin\clang++.exe" --version
+ echo "c:\llvm\$name\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
+
+ - uses: actions/setup-python@v7
+ with:
+ python-version: "3.x"
+
+ - name: Install CMake & Ninja
+ run: pip install --upgrade cmake ninja
+ shell: powershell
diff --git a/.github/actions/test-engine/action.yml b/.github/actions/test-engine/action.yml
new file mode 100644
index 00000000..2aaf9dba
--- /dev/null
+++ b/.github/actions/test-engine/action.yml
@@ -0,0 +1,13 @@
+name: 'Test engine'
+description: 'Runs CTest with the given CI test preset'
+inputs:
+ preset:
+ description: 'CI preset from ci/CMakeUserPresets.json'
+ required: true
+
+runs:
+ using: "composite"
+ steps:
+ - name: Test
+ shell: bash
+ run: ctest --preset ${{ inputs.preset }} --output-on-failure
diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml
new file mode 100644
index 00000000..ab36f70b
--- /dev/null
+++ b/.github/workflows/build-test.yml
@@ -0,0 +1,36 @@
+name: Build & Test
+
+on:
+ push: { branches: ["master", "v2"] }
+ pull_request: { branches: ["master", "v2"] }
+ workflow_dispatch:
+
+permissions: { contents: read, packages: write }
+
+jobs:
+ linux:
+ name: Linux (${{ matrix.compiler }} ${{ matrix.config }})
+ strategy:
+ fail-fast: false
+ matrix:
+ compiler: [gcc, clang]
+ # debug runs the validation layers with synchronization validation
+ config: [debug, release]
+
+ uses: ./.github/workflows/linux-pipeline.yml
+ with:
+ compiler: ${{ matrix.compiler }}
+ config: ${{ matrix.config }}
+ secrets: inherit
+
+ windows:
+ name: Windows (${{ matrix.compiler }})
+ strategy:
+ fail-fast: false
+ matrix:
+ compiler: [msvc, clang]
+
+ uses: ./.github/workflows/windows-pipeline.yml
+ with:
+ compiler: ${{ matrix.compiler }}
+ secrets: inherit
diff --git a/.github/workflows/linux-pipeline.yml b/.github/workflows/linux-pipeline.yml
new file mode 100644
index 00000000..e77e9ffd
--- /dev/null
+++ b/.github/workflows/linux-pipeline.yml
@@ -0,0 +1,62 @@
+name: Linux Pipeline
+
+on:
+ workflow_call:
+ inputs:
+ compiler:
+ required: true
+ type: string
+ config:
+ required: true
+ type: string
+
+permissions:
+ contents: read
+ packages: write
+
+jobs:
+ prepare:
+ name: Build Container
+ runs-on: ubuntu-latest
+ outputs:
+ container-image-adress: ${{ steps.build.outputs.container-image-adress }}
+ steps:
+ - uses: actions/checkout@v7
+ - id: build
+ name: Build & Push
+ uses: ./.github/actions/build-container
+ with:
+ dockerfile: ci/docker/linux/Dockerfile
+ tag: linux-container
+
+ build:
+ name: Linux (${{ inputs.compiler }} ${{ inputs.config }})
+ needs: prepare
+ runs-on: ubuntu-latest
+ # the container has no gpu: the tests run on lavapipe (VK_DRIVER_FILES in the Dockerfile),
+ # tests that need more queues than lavapipe has skip
+ container:
+ image: ${{ needs.prepare.outputs.container-image-adress }}
+ credentials:
+ username: ${{ github.actor }}
+ password: ${{ github.token }}
+ options: >-
+ -v /usr/local/share/vcpkg:/vcpkg
+ -e VCPKG_ROOT=/vcpkg
+ env:
+ VCPKG_ROOT: /vcpkg
+ steps:
+ - uses: actions/checkout@v7
+ with:
+ submodules: recursive
+
+ - name: Build
+ uses: ./.github/actions/build-engine
+ with:
+ cache_prefix: engine-linux
+ preset: ci_linux_${{ inputs.compiler }}_${{ inputs.config }}
+
+ - name: Test
+ uses: ./.github/actions/test-engine
+ with:
+ preset: ci_linux_${{ inputs.compiler }}_${{ inputs.config }}
diff --git a/.github/workflows/windows-pipeline.yml b/.github/workflows/windows-pipeline.yml
new file mode 100644
index 00000000..da8c03e7
--- /dev/null
+++ b/.github/workflows/windows-pipeline.yml
@@ -0,0 +1,36 @@
+name: Windows Pipeline
+
+on:
+ workflow_call:
+ inputs:
+ compiler:
+ required: true
+ type: string
+
+permissions:
+ contents: read
+ packages: write
+
+jobs:
+ # build only: the runners have no vulkan driver, the tests need a device
+ build:
+ name: Windows (${{ inputs.compiler }})
+ runs-on: windows-latest
+ defaults:
+ run:
+ shell: bash # Forces Git Bash for consistency with Linux scripts
+
+ steps:
+ - uses: actions/checkout@v7
+ with:
+ submodules: recursive
+
+ - uses: ./.github/actions/setup-windows
+ with:
+ compiler: ${{ inputs.compiler }}
+
+ - name: Build
+ uses: ./.github/actions/build-engine
+ with:
+ cache_prefix: engine-windows
+ preset: ci_windows_${{ inputs.compiler }}
diff --git a/.gitignore b/.gitignore
index 62bb99dd..e7bb914a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
vcpkg_installed/
out/
-.vs/
\ No newline at end of file
+.vs/
+/CMakeUserPresets.json
diff --git a/.gitmodules b/.gitmodules
index 166339fa..1008123d 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,11 @@
[submodule "cth_cmake"]
path = lib/cth_cmake
url = https://github.com/codethinki/cth_cmake
+[submodule "cth"]
+ path = lib/cth
+ url = https://github.com/codethinki/cth
+ branch = master
+[submodule "lib/rgfw"]
+ path = lib/rgfw
+ url = https://github.com/codethinki/RGFW
+ branch = cth_engine
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
new file mode 100644
index 00000000..20849850
--- /dev/null
+++ b/.idea/codeStyles/Project.xml
@@ -0,0 +1,569 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 00000000..114eed13
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,328 @@
+# Agent guide
+
+How to write code and documentation in this repository.
+Read it before you change a file. Match the existing code.
+
+## Repository
+
+| Path | Content |
+|---|---|
+| `cvk/` | C++ wrapper of the Vulkan C API: one class per Vulkan object, objects own their handle |
+| `cgfw/` | C++ wrapper of RGFW: windows of the platform (Win32, Wayland) and their Vulkan surface |
+| `jvk/` | coroutine layer on top of `cvk` (not started) |
+| `jvk/prework/` | parts for `jvk` that left `cvk`, not built |
+| `legacy/` | the old `jvk`, `jolly` and `demo` code, reference only, not built |
+| `tests/` | loader smoke tests |
+| `lib/cth` | cth (submodule), `lib/cmake/cth.cmake` adds it with only the components the engine needs |
+| `lib/cth_cmake` | cmake utilities (submodule) |
+| `lib/rgfw` | RGFW windowing library, our fork (submodule) on branch `cth_engine`, fixes are commits there |
+| `lib/cmake` | one cmake file per library that adds it: `.cmake` |
+
+- Vulkan 1.3 is the baseline. Do not port code that 1.3 makes unnecessary (render passes, framebuffers, the old sync API).
+- Features beyond 1.3 come from required extensions, not from a higher version.
+- Device features are the `bool` fields of `DeviceFeatures`. A true bit of an extension struct enables its extension.
+ New extension struct: entry in `dev::feature_structs_t`, `dev::feature_struct` specialization, fields in `DeviceFeatures` in the same order.
+- Use the Vulkan C API through volk. Do not use Vulkan-Hpp.
+- Memory comes from VMA.
+- `cvk` keeps only the state it needs to work: no destruction queue, no layout tracking, no thread sync.
+ - other state is a parameter, with a default where the use fixes it: the image layout
+ - `jvk` tracks it, the validation layers do the checks that need it
+- Hash maps: `cth::dt::flat_hash_map`, `cth::dt::flat_hash_set`. No `std::unordered_map`.
+- Commits have no `Co-Authored-By` or "Generated with" lines.
+
+## Branches
+
+- `master` is the main branch. The rebuild goes into it when it is finished.
+- `v2` is the main branch of the rebuild until then.
+- `_v2/jvk_rebuild` is the current work branch. It goes into `v2`.
+- Topic branches off the work branch are named `_v2/_jvk_rebuild/`.
+- `dev` and the `_dev/...` branches are old. Do not use them.
+- Work locally. Do not open pull requests for single steps.
+- CI runs on pushes and pull requests to `master` and `v2`. The work branch has a draft pull request into `v2`, it runs the CI on every push.
+
+## Build and test
+
+```bash
+cmake --preset gcc_release
+cmake --build out/build/gcc_release
+ctest --test-dir out/build/gcc_release --output-on-failure
+```
+
+- Presets: `gcc_debug`, `gcc_release`, `clang_debug`, `clang_release`, `msvc_debug`, `msvc_release`.
+- cth comes from the submodule `lib/cth`: clone with `--recursive` or run `git submodule update --init`.
+- Debug builds enable the validation layers with synchronization validation (`Instance::VALIDATION_LAYER_SETTINGS`). Run the tests in a debug build before you report a change as done.
+- Tests that need more queues than the device has skip. Mesa Intel devices get more queues with `ANV_QUEUE_OVERRIDE=gc=2,c=1`.
+- Tests that need a device use `test::Context` (`cvk/tests/src/context.hpp`). It fails the test on validation errors, leaked objects included.
+- `VCPKG_ROOT` must point at vcpkg.
+
+## Code style
+
+### Formatting
+
+`.clang-format` is the reference. Short form:
+
+- 4 spaces, no tabs, 120 columns.
+- Attached braces: `class X {`, `void f() {`, `if(x) {`.
+- No space between a keyword and `(`: `if(`, `for(`, `while(`, `switch(`, `if constexpr(`.
+- Pointer and reference bind left: `T* ptr`, `T const& ref`.
+- `const` comes after the type: `T const&`, `auto const`.
+- Nested namespaces inside a file are indented one level (`namespace dev { ... }`).
+- No empty line right after `namespace x {` and no empty line right before its closing `}`.
+
+### Naming
+
+| Kind | Style | Example |
+|---|---|---|
+| class, struct of `cvk` / `jvk` | `PascalCase` | `Semaphore`, `DeviceTable`, `DestructionQueue` |
+| nested config and state | `Config`, `State` | `Instance::State` |
+| generic library type, trait, concept | `snake_case` | `basic_deletable`, `handle_t`, `destroyable` |
+| type alias that renames a type | `snake_case_t` | `callback_t`, `queue_set_t` |
+| member function | `camelCase` | `optDestroy()`, `createInfo()` |
+| named constructor (static factory) | `PascalCase` | `CubeMap()`, `DepthBuffer()`, `LayoutTransition()` |
+| free function | `snake_case` | `to_queue_properties()`, `default_debug_callback()` |
+| static check function | `debug_check`, `debug_check_handle` | |
+| function parameter | `camelCase` | `appName`, `requiredExtensions` |
+| local variable | `camelCase` | `createResult`, `requiredExtVec` |
+| member variable | `_camelCase` | `_handle`, `_debugMessenger` |
+| constant | `UPPER_SNAKE_CASE` | `VALIDATION_LAYERS`, `MAX_SECONDARY_CMD_BUFFERS` |
+| template parameter | `PascalCase` | `T`, `Rng`, `Context` |
+| macro | `UPPER_SNAKE_CASE` with library prefix | `CVK_RESULT_STABLE_THROW` |
+| namespace | short lower case | `cvk`, `cvk::constants`, `cvk::dev` |
+
+- A raw handle in a variable or parameter has the prefix of its API: `vkInstance`, `vmaAllocation`.
+- `_t` only for aliases that rename a type: `callback_t`, `queue_set_t`. An alias that is a type of its own gets a std style name, like `std::string`.
+- Declarations leave out parameter names that repeat the type: `wrap(State const&)`, not `VkSemaphore vkSemaphore`. Definitions keep them.
+- `::dev` holds implementation details that are not part of the API.
+
+### Language
+
+- Mark every function `constexpr` that can run at compile time, also when nothing uses it there yet.
+ A handle owner cannot exist at compile time: its lifecycle is not `constexpr`.
+- Put `[[nodiscard]]` on every function that returns a value the caller must use.
+- Put `explicit` on constructors with one parameter and on tag constructors.
+- Use `auto const` for locals that do not change.
+- Initialize aggregates with designated initializers. Vulkan structs always get `.sType` and `.pNext`.
+- Data structs (`SubmitInfo`, `Barrier`, info structs) are aggregates. Member functions like `add()` are optional shortcuts, checks run at the use.
+- A command is a member of the object it acts on: the command buffer for its own state (`cmd.bindPipeline(pipeline)`), else the object (`barrier.execute(cmd)`).
+- Call patterns follow the modern std library conventions.
+- Use deducing `this` (`this auto& self`) where a function must work on the derived type.
+- Qualify `std::` names. A `.cpp` file can declare `using std::span;` and similar at the top of its namespace.
+- No raw `new` and `delete`. Use `std::unique_ptr`, `cth::move_ptr` and `cth::not_null`.
+- Prefer compile time checks (concepts, `static_assert`) to run time checks.
+- An automatically chosen value has an override if:
+ - the choice is not guaranteed to be optimal
+ - there is a legitimate interest: debugging, user preference
+- Never erase types into integers or `void*`. Exception: opaque OS handles are `void*`, like in `cth::os`.
+
+### Includes
+
+Order in a file:
+
+1. the own header (in a `.cpp`)
+2. project headers with quotes: `"cvk/base/instance.hpp"`
+3. library headers: ``, ``, ``
+4. standard headers: ``, ``
+
+Separate the groups with one empty line. Forward declare a class when a header only needs its name.
+
+### Files
+
+- Headers go to `incl///.hpp`, sources to `src//.cpp`. The folders match.
+- Config structs that are large get their own `_config.hpp`.
+- Every library and its tests have a precompiled header in `src/pch/`: `pch.hpp` includes one `glob/.hpp` per external library.
+- Every header starts with `#pragma once`.
+
+### Class layout
+
+Classes that own a Vulkan handle follow this order:
+
+```cpp
+class Semaphore {
+public:
+ using vk_handle_t = VkSemaphore;
+ struct State;
+
+ // constants
+
+ explicit Semaphore(Core const&); // base constructor
+ Semaphore(Core const&, State const&); // constructs and wraps
+ explicit Semaphore(Core const&, create_t); // constructs and creates
+
+ ~Semaphore() { optDestroy(); }
+
+ void wrap(State const& state);
+ void create();
+ void destroy();
+ void optDestroy() { if(created()) destroy(); }
+ State release();
+
+ // operations
+
+ static void destroy(DeviceTable table, VkSemaphore);
+
+ // accessors: created(), get() returns vk_handle_t, ...
+
+protected:
+ // helpers that derived classes use
+
+private:
+ // helpers
+ // members
+
+public:
+ // copy and move members
+ // static debug_check functions
+};
+```
+
+- The first public section holds the API: types, constants, lifecycle, operations, accessors.
+- The last public section holds what every class has and readers rarely need: copy and move members, `debug_check` functions.
+- Classes that own a handle are move only. A class that differs says so in its `@brief`.
+- Every class of a Vulkan or VMA object declares `using vk_handle_t = VkX;` as its first type, also classes that do not own their handle. `get()` returns `vk_handle_t`.
+- The handle lives in `cvk::unique_handle`: a `std::unique_ptr` whose deleter holds the destroy context and calls the static `destroy`.
+ `cvk/utility/handle/` has the deleters and the short forms (`core_handle`).
+- Handle and context go in together: `_handle.reset(ptr, core())`. A missing context argument does not compile.
+- Move constructor and move assignment are `= default`. The move assignment destroys the old handle with the old context.
+- A class that owns several objects declares the objects that depend on its handle first, `= default` assigns in that order: `_debugMessenger` before `_handle`.
+- Classes that do not own their handle (`Queue`, `PhysicalDevice`) keep `cth::move_ptr`.
+
+After the class, in the same header:
+
+```cpp
+namespace cvk {
+struct Semaphore::State {
+ cvk::vk_not_null vkSemaphore;
+};
+}
+
+namespace cvk {
+inline void Semaphore::debug_check(Semaphore const& semaphore) {
+ CTH_CRITICAL(!semaphore.created(), "semaphore must be created") {}
+}
+}
+```
+
+### Object lifecycle
+
+| Function | Meaning |
+|---|---|
+| base constructor | stores the configuration, does not create a Vulkan object |
+| `wrap(State)` | takes ownership of an existing Vulkan object, calls `optDestroy()` first |
+| `create(...)` | creates a new Vulkan object, calls `optDestroy()` first |
+| `destroy()` | destroys the Vulkan object now and forgets, requires `created()` |
+| `optDestroy()` | calls `destroy()` if `created()` |
+| `release()` | gives the `State` to the caller and forgets, does not destroy |
+| `static destroy(...)` | destroys the raw handles, used by the deleter |
+| `forget()` | sets the members to the not created state, does not destroy |
+
+- The destructor calls `optDestroy()`.
+- std and Vulkan names keep their meaning: the lifecycle uses `forget()`, not `reset()`.
+- Device objects take `Core const&` in the base constructor. The `core_deleter` of the handle stores it, `core()` returns it.
+- A parent (instance, core, device, pool, ...) does not move and is not destroyed while objects depend on it. Code does not have to support it.
+- A failed `create()` calls `forget()` before it throws.
+- `release()` exists so that an owner can use an object it must not destroy (for example swapchain images).
+- Create constructor: the base constructor parameters, then the `create()` parameters. `create()` without required parameters -> `create_t` first: `Fence(core, create_t, flags = 0)`.
+
+### Errors
+
+- `CTH_CRITICAL(condition, "message") {}`: precondition check, active in debug builds only.
+- `CTH_STABLE_ERR(condition, "message") { throw ::_exception{details->exception()}; }`: error that can happen at run time.
+- `CTH_WARN(condition, "message") {}`: suspicious state that is not an error.
+- `CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "message") {}`: throws `cvk::vk_result_exception` with the `VkResult`.
+- A module of cth_engine throws only its exception type `::_exception` or a derived type: `cvk::cvk_exception`.
+- The condition is the error case: `CTH_CRITICAL(!created(), ...)`, not the expected case.
+
+### Comment markers
+
+- `//TODO` work that is still open
+- `//TEMP` code that must change before it is final
+- `//IMPLEMENT` a planned change of the design
+
+No space after `//` in markers. Do not put section comments above namespaces.
+
+## Documentation style
+
+Documentation uses doxygen comments and follows ASD-STE100 (Simplified Technical English), adapted to the short style of this code base.
+
+### Form
+
+- Use `/** ... */` blocks with one tag per line.
+- Start after the tag with a lower case letter.
+- Do not put a period at the end of a line.
+- Write short fragments, not full sentences. Start them with a verb in the present tense: `creates the instance`, `destroys and forgets`.
+- Fragments win over STE100: where STE100 asks for a full sentence or an article, write the shorter fragment.
+- Use `@ref` for every name of code: `@ref create()`, `@ref Core::created()`.
+- Do not document what the signature already tells. A getter without conditions needs no comment.
+
+### Tags
+
+| Tag | Use |
+|---|---|
+| `@brief` | what the function does, one line |
+| `@details` | more information. `calls:` followed by a list of the functions it calls |
+| `@param` | name, then what it is for: `@param config to use` |
+| `@tparam` | template parameter, same form as `@param` |
+| `@return` | what the result is |
+| `@pre` | condition that must be true before the call: `@pre @ref created()` |
+| `@post` | condition that is true after the call: `@post @ref created()` |
+| `@throws` | exception type and the cause: `@throws cvk::vk_result_exception result of vkCreateInstance()` |
+| `@note` | side effect the caller must know: `@note calls @ref optDestroy()` |
+| `@attention` | danger that is not a precondition: undefined behavior, not thread safe |
+
+- Write preconditions as `@pre`, not as `@attention requires ...`.
+- For an exception of the module itself, write the cause after `reason:`: `@throws cvk::cvk_exception reason: required extension missing`.
+
+### Math like notation
+
+Use symbols when they are shorter and clearer than words:
+
+- implication: `nullptr -> no messenger`, `@ref memory == nullptr -> memory unknown`
+- conditions and ranges: `@pre 0 < workers < MAX_INT`
+- special values: `(constants::WHOLE_SIZE => whole buffer)`
+
+### STE100 rules that apply
+
+1. Use one word for one meaning. Do not use synonyms. The terms in the table below are fixed.
+2. Write descriptions with 25 words or fewer and instructions with 20 words or fewer.
+3. Write one topic in one fragment. Split two actions into two lines.
+4. Use the active voice and the present tense.
+5. Write instructions to the caller in the imperative: `call @ref create() first`.
+6. Do not use phrasal verbs (`set up`, `carry out`). Use one verb (`create`, `do`).
+7. Do not use `-ing` forms as nouns or adjectives.
+8. Use `it`, `this` and `they` only when the reference is clear.
+9. Write the most important condition first, then the consequence.
+
+| Term | Meaning |
+|---|---|
+| construct | the C++ object comes into existence |
+| create | the Vulkan object comes into existence |
+| destroy | the Vulkan object stops to exist |
+| wrap | take ownership of an existing Vulkan object |
+| release | give the ownership to the caller without destroying |
+| forget | set the members to the not created state |
+| handle | the raw Vulkan handle, for example `VkInstance` |
+| state | the `State` struct: everything that `wrap` needs |
+
+### Example
+
+```cpp
+/**
+ * @brief creates the instance
+ * @param messengerConfig std::nullopt -> no messenger, debug builds use the default config
+ * @pre volk is initialized
+ * @post @ref created()
+ * @note calls @ref optDestroy()
+ * @throws cvk::vk_result_exception result of vkCreateInstance()
+ */
+void create(std::optional messengerConfig = std::nullopt);
+```
+
+### When you port legacy code
+
+- Apply this guide to ported code. The legacy macros `cxpr`, `cval` and `declauto` become keywords.
+- Check every comment against the code. The legacy comments are not always up to date.
+- Report bugs that you find. Do not fix a behavior change silently.
+
+### Repository text
+
+- Do not use em dashes or en dashes in comments, docs, commit messages or markdown.
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 00000000..43c994c2
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1 @@
+@AGENTS.md
diff --git a/CMakeLists.txt b/CMakeLists.txt
index deede4de..d1eb2b72 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -9,14 +9,25 @@ cth_set_compiler_specifics()
cth_set_latest_c_cpp_standard()
set(CMAKE_CXX_SCAN_FOR_MODULES OFF)
-
-# Add subprojects
-add_subdirectory(jvk)
-add_subdirectory(jolly)
-add_subdirectory(demo)
+# lld links much faster than the default linker
+find_program(LLD_LINKER ld.lld)
+if(LLD_LINKER AND NOT MSVC)
+ set(CMAKE_LINKER_TYPE LLD)
+endif()
-# Tests
-enable_testing()
+include(lib/cmake/cth.cmake)
+include(lib/cmake/rgfw.cmake)
+
+if(JOLLY_ENABLE_TESTS)
+ enable_testing()
+endif()
-add_subdirectory(tests)
+# Add subprojects
+add_subdirectory(cvk)
+add_subdirectory(cgfw)
+
+# Tests
+if(JOLLY_ENABLE_TESTS)
+ add_subdirectory(tests)
+endif()
diff --git a/CMakePresets.json b/CMakePresets.json
index faac3861..cdd29c56 100644
--- a/CMakePresets.json
+++ b/CMakePresets.json
@@ -2,40 +2,116 @@
"version": 3,
"configurePresets": [
{
- "name": "cth_cmake",
+ "name": "base",
"hidden": true,
- "toolchainFile": "${sourceDir}/lib/cth_cmake/toolchain.cmake"
+ "cacheVariables": {
+ "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/lib/cth_cmake/toolchain.cmake"
+ }
},
{
- "name": "windows-msvc-base",
+ "name": "add_tests",
"hidden": true,
- "inherits": "cth_cmake",
- "generator": "Ninja",
- "binaryDir": "${sourceDir}/out/build/${presetName}",
"cacheVariables": {
- "CMAKE_CXX_COMPILER": "cl.exe"
- },
- "condition": {
- "type": "equals",
- "lhs": "${hostSystemName}",
- "rhs": "Windows"
+ "JOLLY_ENABLE_TESTS": true
}
},
{
- "name": "win-msvc-debug",
- "inherits": "windows-msvc-base",
+ "name": "debug-base",
"hidden": true,
+ "inherits": [
+ "base",
+ "add_tests"
+ ],
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
}
},
{
- "name": "win-msvc-release",
- "inherits": "windows-msvc-base",
+ "name": "release-base",
"hidden": true,
+ "inherits": [
+ "base",
+ "add_tests"
+ ],
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
}
+ },
+ {
+ "name": "ninja-base",
+ "hidden": true,
+ "generator": "Ninja",
+ "binaryDir": "${sourceDir}/out/build/${presetName}",
+ "installDir": "${sourceDir}/out/install/${presetName}"
+ },
+ {
+ "name": "msvc-base",
+ "hidden": true,
+ "inherits": "ninja-base",
+ "cacheVariables": {
+ "CMAKE_C_COMPILER": "cl",
+ "CMAKE_CXX_COMPILER": "cl"
+ }
+ },
+ {
+ "name": "clang-base",
+ "hidden": true,
+ "inherits": "ninja-base",
+ "cacheVariables": {
+ "CMAKE_C_COMPILER": "clang",
+ "CMAKE_CXX_COMPILER": "clang++"
+ }
+ },
+ {
+ "name": "gcc-base",
+ "hidden": true,
+ "inherits": "ninja-base",
+ "cacheVariables": {
+ "CMAKE_C_COMPILER": "gcc",
+ "CMAKE_CXX_COMPILER": "g++"
+ }
+ },
+ {
+ "name": "msvc_debug",
+ "inherits": [
+ "msvc-base",
+ "debug-base"
+ ]
+ },
+ {
+ "name": "msvc_release",
+ "inherits": [
+ "msvc-base",
+ "release-base"
+ ]
+ },
+ {
+ "name": "clang_debug",
+ "inherits": [
+ "clang-base",
+ "debug-base"
+ ]
+ },
+ {
+ "name": "clang_release",
+ "inherits": [
+ "clang-base",
+ "release-base"
+ ]
+ },
+ {
+ "name": "gcc_debug",
+ "inherits": [
+ "gcc-base",
+ "debug-base"
+ ]
+ },
+ {
+ "name": "gcc_release",
+ "inherits": [
+ "gcc-base",
+ "release-base"
+ ]
}
]
}
diff --git a/CMakeUserPresets.json b/CMakeUserPresets.json
deleted file mode 100644
index b36c5a94..00000000
--- a/CMakeUserPresets.json
+++ /dev/null
@@ -1,33 +0,0 @@
-{
- "version": 3,
- "configurePresets": [
- {
- "name": "win-debug-settings",
- "hidden": true,
- "cacheVariables": {
- "cth_ROOT": "${sourceDir}/../cth/out/install/msvc_debug"
- }
- },
- {
- "name": "win-release-settings",
- "hidden": true,
- "cacheVariables": {
- "cth_ROOT": "${sourceDir}/../cth/out/install/msvc_release"
- }
- },
- {
- "name": "win_msvc_debug",
- "inherits": [
- "win-msvc-debug",
- "win-debug-settings"
- ]
- },
- {
- "name": "win_msvc_release",
- "inherits": [
- "win-msvc-release",
- "win-release-settings"
- ]
- }
- ]
-}
\ No newline at end of file
diff --git a/cgfw/CMakeLists.txt b/cgfw/CMakeLists.txt
new file mode 100644
index 00000000..99bac6fc
--- /dev/null
+++ b/cgfw/CMakeLists.txt
@@ -0,0 +1,34 @@
+cth_glob_cpp(CGFW_SRC "src")
+cth_glob_cpp(CGFW_INCL "incl")
+
+add_library(cgfw STATIC ${CGFW_SRC} ${CGFW_INCL})
+
+cth_target_enable_build_cache(cgfw OPTIONAL)
+
+target_include_directories(
+ cgfw
+ PUBLIC incl
+ PRIVATE src
+)
+
+target_precompile_headers(
+ cgfw
+ PRIVATE
+ src/pch/pch.hpp
+)
+
+# the RGFW implementation is C, cth_glob_cpp() skips .c files, the pch is C++
+target_sources(cgfw PRIVATE src/window/rgfw.c)
+set_source_files_properties(src/window/rgfw.c PROPERTIES SKIP_PRECOMPILE_HEADERS ON)
+
+# the public headers do not include RGFW.h
+target_link_libraries(
+ cgfw
+ PUBLIC cvk
+ PRIVATE rgfw::rgfw
+)
+
+# tests
+if(JOLLY_ENABLE_TESTS)
+ add_subdirectory(tests)
+endif()
diff --git a/cgfw/incl/cgfw/utility/exception.hpp b/cgfw/incl/cgfw/utility/exception.hpp
new file mode 100644
index 00000000..4a103cec
--- /dev/null
+++ b/cgfw/incl/cgfw/utility/exception.hpp
@@ -0,0 +1,38 @@
+#pragma once
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+
+namespace cgfw {
+/**
+ * @brief exception of cgfw, the message starts with "cgfw: "
+ */
+class cgfw_exception : public cth::except::default_exception {
+ static constexpr auto PREFIX = "cgfw: ";
+
+public:
+ cgfw_exception(
+ std::string_view msg,
+ cth::except::Severity const severity,
+ std::source_location loc,
+ std::stacktrace trace
+ ) : default_exception{
+ std::format("{}{}", PREFIX, msg),
+ severity,
+ std::move(loc),
+ std::move(trace)
+ } {}
+
+ explicit cgfw_exception(cth::except::default_exception defaultException)
+ : default_exception{std::move(defaultException)} { prepend(PREFIX); }
+};
+}
+
+#define CGFW_STABLE_THROW(expression, msg, ...) \
+ CTH_STABLE_THROW_T(cgfw::cgfw_exception, expression, msg, __VA_ARGS__)
diff --git a/cgfw/incl/cgfw/window/window.hpp b/cgfw/incl/cgfw/window/window.hpp
new file mode 100644
index 00000000..9b4bade1
--- /dev/null
+++ b/cgfw/incl/cgfw/window/window.hpp
@@ -0,0 +1,241 @@
+#pragma once
+#include "cvk/surface/surface.hpp"
+#include "cvk/utility/constants.hpp"
+#include "cvk/utility/handle/unique_handle.hpp"
+#include "cvk/utility/types.hpp"
+
+#include
+
+#include
+#include
+
+#include
+#include
+#include
+
+
+struct RGFW_window;
+
+namespace cvk {
+class Instance;
+}
+
+namespace cgfw {
+using cvk::create;
+using cvk::create_t;
+
+/**
+ * @brief creation flags of a @ref Window, the values of the RGFW_windowFlags
+ */
+enum class WindowFlags : uint32_t {
+ NONE = 0,
+ NO_BORDER = cth::en::flag_val(0),
+ NO_RESIZE = cth::en::flag_val(1),
+ FULLSCREEN = cth::en::flag_val(4),
+ CENTER = cth::en::flag_val(6),
+ SCALE_TO_MONITOR = cth::en::flag_val(8),
+ /**
+ * @brief the window is not visible
+ */
+ HIDE = cth::en::flag_val(9),
+ MAXIMIZE = cth::en::flag_val(10),
+ FLOATING = cth::en::flag_val(12),
+ FOCUS_ON_SHOW = cth::en::flag_val(13),
+ MINIMIZE = cth::en::flag_val(14),
+};
+}
+
+CTH_GEN_ENUM_FLAG_OVERLOADS(cgfw::WindowFlags)
+
+namespace cgfw {
+/**
+ * @brief opaque native handles of a window
+ * @details windows: display = HINSTANCE, window = HWND
+ * wayland: display = wl_display*, window = wl_surface*
+ * windows that exist at the same time have the same display
+ */
+struct native_window {
+ void* display;
+ void* window;
+};
+
+namespace dev {
+ /**
+ * @brief deleter of a window, without context: RGFW keeps one global state
+ * @details calls @ref Window::destroy(RGFW_window*)
+ */
+ struct window_deleter {
+ void operator()(RGFW_window*) const;
+ };
+}
+
+/**
+ * @brief RGFW_window wrapper, a window of the platform: Win32 or Wayland
+ * @details RGFW keeps one global state: the first window initializes it, the destruction of the last one
+ * deinitializes it, @ref pollEvents() handles the events of every window
+ * @attention not thread safe: RGFW keeps one global state
+ */
+class Window {
+public:
+ using handle_t = RGFW_window*;
+ struct State;
+
+ /**
+ * @brief title, size and flags of the window
+ */
+ struct Config {
+ std::string title{};
+
+ /**
+ * @brief requested size of the window
+ * @pre width > 0, height > 0
+ */
+ VkExtent2D extent{};
+
+ WindowFlags flags = WindowFlags::NONE;
+
+ static constexpr void debug_check(Config const&);
+ };
+
+ /**
+ * @brief base constructor
+ */
+ explicit Window(Config);
+
+ /**
+ * @brief constructs and wraps
+ * @details calls:
+ * - @ref Window(Config)
+ * - @ref wrap()
+ */
+ Window(Config, State const&);
+
+ /**
+ * @brief constructs and creates
+ * @details calls:
+ * - @ref Window(Config)
+ * - @ref create()
+ */
+ explicit Window(Config, create_t);
+
+ /**
+ * @note calls @ref optDestroy()
+ */
+ ~Window() { optDestroy(); }
+
+ /**
+ * @brief wraps the state
+ * @pre RGFW_createWindow() created the window with the config
+ * @post @ref created()
+ * @note calls @ref optDestroy()
+ */
+ void wrap(State const&);
+
+ /**
+ * @brief creates the window with the config
+ * @details no window exists -> initializes RGFW first
+ * @pre @ref Config::debug_check() passes for the config
+ * @post @ref created()
+ * @note calls @ref optDestroy()
+ * @throws cgfw::cgfw_exception reason: RGFW_createWindow() failed, e.g. no display
+ */
+ void create();
+
+ /**
+ * @brief destroys and forgets
+ * @pre @ref created()
+ * @pre every surface of the window is destroyed
+ * @note the last window deinitializes RGFW
+ */
+ void destroy();
+
+ /**
+ * @brief if @ref created() -> calls @ref destroy()
+ */
+ void optDestroy() { if(created()) destroy(); }
+
+ /**
+ * @brief releases ownership, returns the state and forgets
+ * @pre @ref created()
+ */
+ State release();
+
+ /**
+ * @brief creates a surface of the window
+ * @pre @ref created()
+ * @pre the instance enables @ref instance_extensions()
+ * @throws cgfw::cgfw_exception reason: RGFW_window_createSurface_Vulkan() failed, the message has the VkResult
+ */
+ [[nodiscard]] cvk::Surface surface(cvk::Instance const&) const;
+
+ /**
+ * @brief handles the pending events of every window
+ * @details RGFW handles the events of all windows together
+ * @pre @ref created()
+ */
+ void pollEvents() const;
+
+ /**
+ * @return the user or the platform requested to close the window
+ * @details @ref pollEvents() receives the requests
+ * @pre @ref created()
+ */
+ [[nodiscard]] bool shouldClose() const;
+
+ /**
+ * @return the native handles of the window
+ * @pre @ref created()
+ */
+ [[nodiscard]] native_window nativeWindow() const;
+
+ /**
+ * @brief closes the window and frees it
+ * @pre the window is not nullptr
+ */
+ static void destroy(RGFW_window*);
+
+ [[nodiscard]] constexpr bool created() const { return static_cast(_handle); }
+ [[nodiscard]] constexpr handle_t get() const { return _handle.get(); }
+ [[nodiscard]] constexpr Config const& config() const { return _config; }
+
+private:
+ void forget();
+
+ Config _config;
+ cvk::unique_handle _handle{};
+
+public:
+ Window(Window const& other) = delete;
+ Window(Window&& other) noexcept = default;
+ Window& operator=(Window const& other) = delete;
+ Window& operator=(Window&& other) noexcept = default;
+
+ static constexpr void debug_check(Window const&);
+};
+
+/**
+ * @return the instance extensions of @ref Window::surface(): VK_KHR_surface and the surface extension of the platform
+ */
+[[nodiscard]] std::vector instance_extensions();
+}
+
+namespace cgfw {
+struct Window::State {
+ cvk::not_null_ptr rgfwWindow;
+};
+}
+
+namespace cgfw {
+constexpr void Window::Config::debug_check(Config const& config) {
+ CTH_CRITICAL(
+ config.extent.width == 0 || config.extent.height == 0,
+ "the extent must not be 0, got {}x{}",
+ config.extent.width,
+ config.extent.height
+ ) {}
+}
+
+constexpr void Window::debug_check(Window const& window) {
+ CTH_CRITICAL(!window.created(), "window must be created") {}
+}
+}
diff --git a/cgfw/src/pch/glob/cth.hpp b/cgfw/src/pch/glob/cth.hpp
new file mode 100644
index 00000000..7d66d320
--- /dev/null
+++ b/cgfw/src/pch/glob/cth.hpp
@@ -0,0 +1,6 @@
+#pragma once
+#include
+#include
+#include
+#include
+#include
diff --git a/cgfw/src/pch/glob/std.hpp b/cgfw/src/pch/glob/std.hpp
new file mode 100644
index 00000000..52f7dc07
--- /dev/null
+++ b/cgfw/src/pch/glob/std.hpp
@@ -0,0 +1,9 @@
+#pragma once
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
diff --git a/cgfw/src/pch/glob/volk.hpp b/cgfw/src/pch/glob/volk.hpp
new file mode 100644
index 00000000..e78ae406
--- /dev/null
+++ b/cgfw/src/pch/glob/volk.hpp
@@ -0,0 +1,2 @@
+#pragma once
+#include
diff --git a/cgfw/src/pch/pch.hpp b/cgfw/src/pch/pch.hpp
new file mode 100644
index 00000000..5aeb856c
--- /dev/null
+++ b/cgfw/src/pch/pch.hpp
@@ -0,0 +1,6 @@
+#pragma once
+
+#include "glob/cth.hpp"
+#include "glob/std.hpp"
+
+#include "glob/volk.hpp"
diff --git a/cgfw/src/window/rgfw.c b/cgfw/src/window/rgfw.c
new file mode 100644
index 00000000..40827fc9
--- /dev/null
+++ b/cgfw/src/window/rgfw.c
@@ -0,0 +1,8 @@
+// the RGFW implementation, no other source file defines RGFW_IMPLEMENTATION
+// C: the Wayland part of RGFW is no valid C++ (void* conversions, designator order)
+
+// volk first: RGFW includes vulkan.h, volk needs VK_NO_PROTOTYPES before it
+#include
+
+#define RGFW_IMPLEMENTATION
+#include
diff --git a/cgfw/src/window/window.cpp b/cgfw/src/window/window.cpp
new file mode 100644
index 00000000..e288e7ca
--- /dev/null
+++ b/cgfw/src/window/window.cpp
@@ -0,0 +1,135 @@
+#include "cgfw/window/window.hpp"
+
+#include "cgfw/utility/exception.hpp"
+
+#include "cvk/base/instance.hpp"
+#include "cvk/utility/format.hpp"
+
+#include
+
+#include
+
+#include
+
+
+namespace cgfw {
+namespace {
+ constexpr bool same_flag(WindowFlags flag, RGFW_windowFlags rgfwFlag) {
+ return static_cast(flag) == rgfwFlag;
+ }
+
+ static_assert(same_flag(WindowFlags::NO_BORDER, RGFW_windowNoBorder));
+ static_assert(same_flag(WindowFlags::NO_RESIZE, RGFW_windowNoResize));
+ static_assert(same_flag(WindowFlags::FULLSCREEN, RGFW_windowFullscreen));
+ static_assert(same_flag(WindowFlags::CENTER, RGFW_windowCenter));
+ static_assert(same_flag(WindowFlags::SCALE_TO_MONITOR, RGFW_windowScaleToMonitor));
+ static_assert(same_flag(WindowFlags::HIDE, RGFW_windowHide));
+ static_assert(same_flag(WindowFlags::MAXIMIZE, RGFW_windowMaximize));
+ static_assert(same_flag(WindowFlags::FLOATING, RGFW_windowFloating));
+ static_assert(same_flag(WindowFlags::FOCUS_ON_SHOW, RGFW_windowFocusOnShow));
+ static_assert(same_flag(WindowFlags::MINIMIZE, RGFW_windowMinimize));
+}
+
+void dev::window_deleter::operator()(RGFW_window* rgfwWindow) const { Window::destroy(rgfwWindow); }
+
+Window::Window(Config config) : _config{std::move(config)} {}
+
+Window::Window(Config config, State const& state) : Window{std::move(config)} { wrap(state); }
+
+Window::Window(Config config, create_t) : Window{std::move(config)} { create(); }
+
+void Window::wrap(State const& state) {
+ optDestroy();
+
+ _handle.reset(state.rgfwWindow.get());
+}
+
+void Window::create() {
+ Config::debug_check(_config);
+ optDestroy();
+
+ auto* const rgfwWindow = RGFW_createWindow(
+ _config.title.c_str(),
+ 0,
+ 0,
+ static_cast(_config.extent.width),
+ static_cast(_config.extent.height),
+ static_cast(_config.flags)
+ );
+ CGFW_STABLE_THROW(rgfwWindow == nullptr, "failed to create the window {}, e.g. no display", _config.title) {
+ forget();
+ }
+
+ _handle.reset(rgfwWindow);
+}
+
+void Window::destroy() {
+ debug_check(*this);
+
+ _handle.reset();
+
+ forget();
+}
+
+Window::State Window::release() {
+ debug_check(*this);
+
+ State const state{_handle.release()};
+ forget();
+ return state;
+}
+
+cvk::Surface Window::surface(cvk::Instance const& instance) const {
+ debug_check(*this);
+ cvk::Instance::debug_check(instance);
+
+ VkSurfaceKHR vkSurface = VK_NULL_HANDLE;
+ auto const result = RGFW_window_createSurface_Vulkan(_handle.get(), instance.get(), &vkSurface);
+ CGFW_STABLE_THROW(
+ result != VK_SUCCESS,
+ "failed to create the surface of the window {}, got {}",
+ _config.title,
+ result
+ ) {}
+
+ return cvk::Surface{instance, cvk::Surface::State{vkSurface}};
+}
+
+void Window::pollEvents() const {
+ debug_check(*this);
+
+ RGFW_pollEvents();
+}
+
+bool Window::shouldClose() const {
+ debug_check(*this);
+
+ return RGFW_window_shouldClose(_handle.get()) != RGFW_FALSE;
+}
+
+native_window Window::nativeWindow() const {
+ debug_check(*this);
+
+#ifdef CTH_PLATFORM_WINDOWS
+ // RGFW has no getter, it gives the Win32 surface this HINSTANCE too
+ return native_window{.display = GetModuleHandleW(nullptr), .window = RGFW_window_getHWND(_handle.get())};
+#else
+ return native_window{.display = RGFW_getDisplay_Wayland(), .window = RGFW_window_getWindow_Wayland(_handle.get())};
+#endif
+}
+
+void Window::destroy(RGFW_window* rgfwWindow) {
+ CTH_CRITICAL(rgfwWindow == nullptr, "rgfwWindow must not be nullptr") {}
+
+ RGFW_window_close(rgfwWindow);
+}
+
+void Window::forget() {}
+
+std::vector instance_extensions() {
+ size_t count = 0;
+ char const** const extensions = RGFW_getRequiredInstanceExtensions_Vulkan(&count);
+
+ return std::vector{std::from_range, std::span{extensions, count}};
+}
+}
diff --git a/cgfw/tests/CMakeLists.txt b/cgfw/tests/CMakeLists.txt
new file mode 100644
index 00000000..773945cd
--- /dev/null
+++ b/cgfw/tests/CMakeLists.txt
@@ -0,0 +1,23 @@
+cth_glob_cpp(CGFW_TESTS_SRC "src")
+
+add_executable(cgfw_tests ${CGFW_TESTS_SRC})
+
+cth_target_enable_build_cache(cgfw_tests OPTIONAL)
+
+target_include_directories(
+ cgfw_tests
+ PRIVATE src
+)
+
+target_precompile_headers(
+ cgfw_tests
+ PRIVATE
+ src/pch/pch.hpp
+)
+
+find_package(GTest CONFIG REQUIRED)
+
+target_link_libraries(cgfw_tests PRIVATE GTest::gtest_main cgfw)
+
+# one process: RGFW keeps one global state
+add_test(NAME cgfw_tests COMMAND cgfw_tests)
diff --git a/cgfw/tests/src/context.cpp b/cgfw/tests/src/context.cpp
new file mode 100644
index 00000000..76a85049
--- /dev/null
+++ b/cgfw/tests/src/context.cpp
@@ -0,0 +1,40 @@
+#include "context.hpp"
+
+#include
+
+#include
+#include
+
+
+namespace cgfw::test {
+namespace {
+ size_t errors = 0;
+
+ VKAPI_ATTR VkBool32 VKAPI_CALL count_errors(
+ VkDebugUtilsMessageSeverityFlagBitsEXT severity,
+ VkDebugUtilsMessageTypeFlagsEXT types,
+ VkDebugUtilsMessengerCallbackDataEXT const* callbackData,
+ void* userData
+ ) {
+ if(severity >= VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) ++errors;
+ return cvk::default_debug_callback(severity, types, callbackData, userData);
+ }
+}
+
+bool display_available() {
+#ifdef CTH_PLATFORM_WINDOWS
+ return true;
+#else
+ //TODO a headless weston in the CI container, the tests skip there
+ return std::getenv("WAYLAND_DISPLAY") != nullptr;
+#endif
+}
+
+cvk::DebugMessenger::Config counting_messenger() { return cvk::DebugMessenger::Config{.callback = count_errors}; }
+
+ValidationCheck::ValidationCheck() { errors = 0; }
+
+ValidationCheck::~ValidationCheck() { EXPECT_EQ(errors, 0); }
+
+InstanceContext::InstanceContext() : instance{instance_config(instance_extensions()), cvk::create, counting_messenger()} {}
+}
diff --git a/cgfw/tests/src/context.hpp b/cgfw/tests/src/context.hpp
new file mode 100644
index 00000000..baa4324d
--- /dev/null
+++ b/cgfw/tests/src/context.hpp
@@ -0,0 +1,40 @@
+#pragma once
+#include "test.hpp"
+
+#include "cvk/base/instance.hpp"
+#include "cvk/debug/debug_messenger.hpp"
+
+
+
+namespace cgfw::test {
+/**
+ * @brief messenger config that logs every message and counts the errors
+ * @details a small copy of the cvk tests, the instance create info gets it too:
+ * counts the errors of vkDestroyInstance(), e.g. leaked surfaces
+ */
+[[nodiscard]] cvk::DebugMessenger::Config counting_messenger();
+
+/**
+ * @brief resets the error count, the destructor fails the test on errors
+ */
+struct ValidationCheck {
+ ValidationCheck();
+ ~ValidationCheck();
+};
+
+/**
+ * @brief own instance with @ref instance_extensions() and @ref counting_messenger(), fails the test on validation errors
+ * @details the check runs after the instance is destroyed
+ */
+struct InstanceContext {
+ InstanceContext();
+
+ ValidationCheck validationCheck;
+ cvk::Instance instance;
+
+ InstanceContext(InstanceContext const& other) = delete;
+ InstanceContext(InstanceContext&& other) = delete;
+ InstanceContext& operator=(InstanceContext const& other) = delete;
+ InstanceContext& operator=(InstanceContext&& other) = delete;
+};
+}
diff --git a/cgfw/tests/src/environment.cpp b/cgfw/tests/src/environment.cpp
new file mode 100644
index 00000000..4f372d57
--- /dev/null
+++ b/cgfw/tests/src/environment.cpp
@@ -0,0 +1,24 @@
+#include "cvk/base/cvk.hpp"
+
+#include
+
+#include
+
+
+namespace cgfw::test {
+namespace {
+ /**
+ * @brief holds the vulkan loader for all tests
+ */
+ class Environment : public testing::Environment {
+ public:
+ void SetUp() override { _cvk.emplace(); }
+ void TearDown() override { _cvk.reset(); }
+
+ private:
+ std::optional _cvk{};
+ };
+
+ [[maybe_unused]] testing::Environment* const ENVIRONMENT = testing::AddGlobalTestEnvironment(new Environment{});
+}
+}
diff --git a/cgfw/tests/src/pch/glob/gtest.hpp b/cgfw/tests/src/pch/glob/gtest.hpp
new file mode 100644
index 00000000..e469bb31
--- /dev/null
+++ b/cgfw/tests/src/pch/glob/gtest.hpp
@@ -0,0 +1,2 @@
+#pragma once
+#include
diff --git a/cgfw/tests/src/pch/pch.hpp b/cgfw/tests/src/pch/pch.hpp
new file mode 100644
index 00000000..d97ada7c
--- /dev/null
+++ b/cgfw/tests/src/pch/pch.hpp
@@ -0,0 +1,5 @@
+#pragma once
+#include "glob/gtest.hpp"
+
+#include "context.hpp"
+#include "test.hpp"
diff --git a/cgfw/tests/src/test.hpp b/cgfw/tests/src/test.hpp
new file mode 100644
index 00000000..596e447c
--- /dev/null
+++ b/cgfw/tests/src/test.hpp
@@ -0,0 +1,41 @@
+#pragma once
+#include "cgfw/window/window.hpp"
+
+#include "cvk/base/instance_config.hpp"
+
+#include
+
+#include
+#include
+#include
+
+#define CGFW_TEST(suite, test_name) S_TEST(cgfw, suite, test_name)
+
+/**
+ * @brief skips the test without a display
+ */
+#define CGFW_SKIP_WITHOUT_DISPLAY() \
+ if(!cgfw::test::display_available()) GTEST_SKIP() << "no display"
+
+namespace cgfw::test {
+constexpr std::string_view APP_NAME = "cgfw_tests";
+
+/**
+ * @return a display exists, linux: WAYLAND_DISPLAY is set
+ */
+[[nodiscard]] bool display_available();
+
+/**
+ * @brief hidden window: a test run shows no window
+ */
+[[nodiscard]] inline Window::Config window_config(std::string title = "cgfw test") {
+ return Window::Config{.title = std::move(title), .extent = {64, 64}, .flags = WindowFlags::HIDE};
+}
+
+/**
+ * @brief instance config with @ref APP_NAME
+ */
+[[nodiscard]] inline cvk::InstanceConfig instance_config(std::vector extensions = {}) {
+ return cvk::InstanceConfig{.appName = std::string{APP_NAME}, .extensions = std::move(extensions)};
+}
+}
diff --git a/cgfw/tests/src/window/window_test.cpp b/cgfw/tests/src/window/window_test.cpp
new file mode 100644
index 00000000..ba618f39
--- /dev/null
+++ b/cgfw/tests/src/window/window_test.cpp
@@ -0,0 +1,147 @@
+#include "context.hpp"
+#include "test.hpp"
+
+#include "cgfw/window/window.hpp"
+
+#include "cvk/surface/surface.hpp"
+
+#include
+
+#include
+#include
+#include
+
+
+namespace cgfw {
+CGFW_TEST(Window, base_constructor_does_not_create) {
+ Window const window{test::window_config()};
+
+ EXPECT_FALSE(window.created());
+ EXPECT_EQ(window.get(), nullptr);
+ EXPECT_EQ(window.config().title, test::window_config().title);
+}
+
+CGFW_TEST(Window, create_and_destroy) {
+ CGFW_SKIP_WITHOUT_DISPLAY();
+ Window window{test::window_config()};
+
+ window.create();
+ ASSERT_TRUE(window.created());
+ EXPECT_NE(window.get(), nullptr);
+
+ window.destroy();
+ EXPECT_FALSE(window.created());
+}
+
+CGFW_TEST(Window, constructor_creates) {
+ CGFW_SKIP_WITHOUT_DISPLAY();
+
+ Window const window{test::window_config(), create};
+
+ EXPECT_TRUE(window.created());
+ EXPECT_FALSE(window.shouldClose());
+}
+
+CGFW_TEST(Window, release_and_wrap) {
+ CGFW_SKIP_WITHOUT_DISPLAY();
+ Window window{test::window_config(), create};
+ auto const rgfwWindow = window.get();
+
+ auto const state = window.release();
+ EXPECT_FALSE(window.created());
+ EXPECT_EQ(state.rgfwWindow.get(), rgfwWindow);
+
+ Window const wrapped{test::window_config(), state};
+ EXPECT_TRUE(wrapped.created());
+ EXPECT_EQ(wrapped.get(), rgfwWindow);
+}
+
+CGFW_TEST(Window, move_keeps_the_window) {
+ CGFW_SKIP_WITHOUT_DISPLAY();
+ Window window{test::window_config(), create};
+ auto const rgfwWindow = window.get();
+
+ Window const moved{std::move(window)};
+
+ EXPECT_FALSE(window.created());
+ EXPECT_EQ(moved.get(), rgfwWindow);
+ moved.pollEvents();
+ EXPECT_FALSE(moved.shouldClose());
+}
+
+CGFW_TEST(Window, move_assignment_destroys_the_old_window) {
+ CGFW_SKIP_WITHOUT_DISPLAY();
+ Window window{test::window_config("old"), create};
+ Window other{test::window_config("new"), create};
+ auto const rgfwWindow = other.get();
+
+ window = std::move(other);
+
+ EXPECT_EQ(window.get(), rgfwWindow);
+ EXPECT_EQ(window.config().title, "new");
+ EXPECT_FALSE(other.created());
+}
+
+CGFW_TEST(Window, windows_exist_at_the_same_time) {
+ CGFW_SKIP_WITHOUT_DISPLAY();
+
+ std::array const windows{
+ Window{test::window_config("first"), create},
+ Window{test::window_config("second"), create},
+ Window{test::window_config("third"), create},
+ };
+
+ windows[0].pollEvents();
+ for(auto const& window : windows) {
+ EXPECT_TRUE(window.created());
+ EXPECT_FALSE(window.shouldClose());
+ }
+ EXPECT_NE(windows[0].get(), windows[1].get());
+ EXPECT_NE(windows[1].get(), windows[2].get());
+ EXPECT_NE(windows[0].get(), windows[2].get());
+}
+
+CGFW_TEST(Window, native_handles_are_not_nullptr) {
+ CGFW_SKIP_WITHOUT_DISPLAY();
+ Window const window{test::window_config(), create};
+
+ auto const [display, nativeWindow] = window.nativeWindow();
+
+ EXPECT_NE(display, nullptr);
+ EXPECT_NE(nativeWindow, nullptr);
+}
+
+CGFW_TEST(Window, windows_at_the_same_time_have_the_same_display) {
+ CGFW_SKIP_WITHOUT_DISPLAY();
+ Window const first{test::window_config("first"), create};
+ Window const second{test::window_config("second"), create};
+
+ auto const firstNative = first.nativeWindow();
+ auto const secondNative = second.nativeWindow();
+
+ EXPECT_EQ(firstNative.display, secondNative.display);
+ EXPECT_NE(firstNative.window, secondNative.window);
+}
+
+CGFW_TEST(Window, surface_of_an_instance_with_the_extensions) {
+ CGFW_SKIP_WITHOUT_DISPLAY();
+ test::InstanceContext const context{};
+ Window const window{test::window_config(), create};
+
+ auto const surface = window.surface(context.instance);
+
+ EXPECT_TRUE(surface.created());
+}
+
+CGFW_TEST(instance_extensions, contain_the_surface_extensions) {
+ auto const extensions = instance_extensions();
+
+ ASSERT_FALSE(extensions.empty());
+ EXPECT_TRUE(std::ranges::contains(extensions, std::string{VK_KHR_SURFACE_EXTENSION_NAME}));
+#ifdef CTH_PLATFORM_WINDOWS
+ EXPECT_TRUE(std::ranges::contains(extensions, std::string{"VK_KHR_win32_surface"}));
+#else
+ EXPECT_TRUE(std::ranges::contains(extensions, std::string{"VK_KHR_wayland_surface"}));
+#endif
+}
+}
diff --git a/ci/CMakeUserPresets.json b/ci/CMakeUserPresets.json
new file mode 100644
index 00000000..05bc52e3
--- /dev/null
+++ b/ci/CMakeUserPresets.json
@@ -0,0 +1,160 @@
+{
+ "version": 3,
+ "configurePresets": [
+ {
+ "name": "ci-vcpkg",
+ "hidden": true,
+ "cacheVariables": {
+ "VCPKG_INSTALLED_DIR": "${sourceDir}/vcpkg_installed"
+ },
+ "environment": {
+ "VCPKG_BINARY_SOURCES": "clear;nuget,GitHub,readwrite"
+ }
+ },
+ {
+ "name": "ci-buildcache",
+ "hidden": true,
+ "environment": {
+ "BUILDCACHE_DIR": "${sourceDir}/.buildcache",
+ "BUILDCACHE_ACCURACY": "SLOPPY",
+ "BUILDCACHE_MAX_CACHE_SIZE": "524288000"
+ }
+ },
+ {
+ "name": "ci-config-base",
+ "hidden": true,
+ "inherits": [
+ "ci-vcpkg",
+ "ci-buildcache"
+ ]
+ },
+ {
+ "name": "ci_linux_gcc_debug",
+ "inherits": [
+ "ci-config-base",
+ "gcc_debug"
+ ]
+ },
+ {
+ "name": "ci_linux_gcc_release",
+ "inherits": [
+ "ci-config-base",
+ "gcc_release"
+ ]
+ },
+ {
+ "name": "ci_linux_clang_debug",
+ "inherits": [
+ "ci-config-base",
+ "clang_debug"
+ ]
+ },
+ {
+ "name": "ci_linux_clang_release",
+ "inherits": [
+ "ci-config-base",
+ "clang_release"
+ ]
+ },
+ {
+ "name": "ci_windows_msvc",
+ "inherits": [
+ "ci-config-base",
+ "msvc_release"
+ ]
+ },
+ {
+ "name": "ci_windows_clang",
+ "inherits": [
+ "ci-config-base",
+ "clang_release"
+ ]
+ }
+ ],
+ "buildPresets": [
+ {
+ "name": "ci-build-debug",
+ "hidden": true,
+ "configuration": "Debug"
+ },
+ {
+ "name": "ci-build-release",
+ "hidden": true,
+ "configuration": "Release"
+ },
+ {
+ "name": "ci_linux_gcc_debug",
+ "configurePreset": "ci_linux_gcc_debug",
+ "inherits": "ci-build-debug"
+ },
+ {
+ "name": "ci_linux_gcc_release",
+ "configurePreset": "ci_linux_gcc_release",
+ "inherits": "ci-build-release"
+ },
+ {
+ "name": "ci_linux_clang_debug",
+ "configurePreset": "ci_linux_clang_debug",
+ "inherits": "ci-build-debug"
+ },
+ {
+ "name": "ci_linux_clang_release",
+ "configurePreset": "ci_linux_clang_release",
+ "inherits": "ci-build-release"
+ },
+ {
+ "name": "ci_windows_msvc",
+ "configurePreset": "ci_windows_msvc",
+ "inherits": "ci-build-release"
+ },
+ {
+ "name": "ci_windows_clang",
+ "configurePreset": "ci_windows_clang",
+ "inherits": "ci-build-release"
+ }
+ ],
+ "testPresets": [
+ {
+ "name": "ci-test-base",
+ "hidden": true,
+ "output": {
+ "outputOnFailure": true
+ },
+ "execution": {
+ "noTestsAction": "error"
+ }
+ },
+ {
+ "name": "ci-test-debug",
+ "hidden": true,
+ "inherits": "ci-test-base",
+ "configuration": "Debug"
+ },
+ {
+ "name": "ci-test-release",
+ "hidden": true,
+ "inherits": "ci-test-base",
+ "configuration": "Release"
+ },
+ {
+ "name": "ci_linux_gcc_debug",
+ "configurePreset": "ci_linux_gcc_debug",
+ "inherits": "ci-test-debug"
+ },
+ {
+ "name": "ci_linux_gcc_release",
+ "configurePreset": "ci_linux_gcc_release",
+ "inherits": "ci-test-release"
+ },
+ {
+ "name": "ci_linux_clang_debug",
+ "configurePreset": "ci_linux_clang_debug",
+ "inherits": "ci-test-debug"
+ },
+ {
+ "name": "ci_linux_clang_release",
+ "configurePreset": "ci_linux_clang_release",
+ "inherits": "ci-test-release"
+ }
+ ]
+}
diff --git a/ci/docker/linux/Dockerfile b/ci/docker/linux/Dockerfile
new file mode 100644
index 00000000..24ab254d
--- /dev/null
+++ b/ci/docker/linux/Dockerfile
@@ -0,0 +1,32 @@
+FROM cachyos/cachyos:latest
+
+# vulkan-swrast: lavapipe, the cpu vulkan driver of mesa
+# vulkan-validation-layers: debug builds enable them with synchronization validation
+# shaderc: glslc for the shader compile tests
+# wayland, libxkbcommon: the RGFW Wayland backend of cgfw, the vcpkg ports of them are empty on linux
+RUN pacman -Syu --noconfirm && \
+ pacman -S --noconfirm \
+ base-devel \
+ cmake \
+ ninja \
+ git \
+ gcc \
+ clang \
+ lld \
+ python \
+ curl \
+ wget \
+ zip \
+ unzip \
+ tar \
+ mono \
+ buildcache-git \
+ vulkan-icd-loader \
+ vulkan-swrast \
+ vulkan-validation-layers \
+ shaderc \
+ wayland \
+ libxkbcommon
+
+# the tests run on lavapipe, also if another driver is installed
+ENV VK_DRIVER_FILES=/usr/share/vulkan/icd.d/lvp_icd.json
diff --git a/cvk/CMakeLists.txt b/cvk/CMakeLists.txt
new file mode 100644
index 00000000..8470f9c8
--- /dev/null
+++ b/cvk/CMakeLists.txt
@@ -0,0 +1,32 @@
+cth_glob_cpp(CVK_SRC "src")
+cth_glob_cpp(CVK_INCL "incl")
+
+add_library(cvk STATIC ${CVK_SRC} ${CVK_INCL})
+
+cth_target_enable_build_cache(cvk OPTIONAL)
+
+target_include_directories(
+ cvk
+ PUBLIC incl
+ PRIVATE src
+)
+
+target_precompile_headers(
+ cvk
+ PRIVATE
+ src/pch/pch.hpp
+)
+
+find_package(volk CONFIG REQUIRED)
+find_package(VulkanMemoryAllocator CONFIG REQUIRED)
+find_package(VulkanUtilityLibraries CONFIG REQUIRED)
+
+target_link_libraries(
+ cvk
+ PUBLIC volk::volk Vulkan::UtilityHeaders GPUOpen::VulkanMemoryAllocator cth::cth cth::os
+)
+
+# tests
+if(JOLLY_ENABLE_TESTS)
+ add_subdirectory(tests)
+endif()
diff --git a/cvk/incl/cvk/base/allocator.hpp b/cvk/incl/cvk/base/allocator.hpp
new file mode 100644
index 00000000..90d447aa
--- /dev/null
+++ b/cvk/incl/cvk/base/allocator.hpp
@@ -0,0 +1,127 @@
+#pragma once
+#include "cvk/utility/constants.hpp"
+#include "cvk/utility/handle/unique_handle.hpp"
+#include "cvk/utility/types.hpp"
+
+#include
+#include
+
+#include
+
+
+namespace cvk {
+class Device;
+
+/**
+ * @brief VmaAllocator wrapper, allocates the memory of one device
+ * @details VMA places many allocations in one VkDeviceMemory,
+ * one allocator per device is enough,
+ * calls with the allocator are thread safe, VMA synchronizes them
+ */
+class Allocator {
+public:
+ using vk_handle_t = VmaAllocator;
+ struct State;
+
+ /**
+ * @brief base constructor
+ * @pre @ref Device::created()
+ */
+ explicit Allocator(Device const&);
+
+ /**
+ * @brief constructs and wraps
+ * @details calls:
+ * - @ref Allocator(Device const&)
+ * - @ref wrap()
+ */
+ Allocator(Device const&, State const&);
+
+ /**
+ * @brief constructs and creates
+ * @details calls:
+ * - @ref Allocator(Device const&)
+ * - @ref create()
+ */
+ explicit Allocator(Device const&, create_t);
+
+ /**
+ * @note calls @ref optDestroy()
+ */
+ ~Allocator() { optDestroy(); }
+
+ /**
+ * @brief wraps the state
+ * @pre the allocator was created with the device
+ * @post @ref created()
+ * @note calls @ref optDestroy()
+ */
+ void wrap(State const&);
+
+ /**
+ * @brief creates the allocator
+ * @details the flags follow the enabled features and extensions of the device: @ref flags()
+ * @post @ref created()
+ * @note calls @ref optDestroy()
+ * @throws cvk::vk_result_exception result of vmaImportVulkanFunctionsFromVolk()
+ * @throws cvk::vk_result_exception result of vmaCreateAllocator()
+ */
+ void create();
+
+ /**
+ * @brief destroys and forgets
+ * @pre @ref created()
+ * @pre every memory of the allocator is destroyed
+ */
+ void destroy();
+
+ /**
+ * @brief if @ref created() -> calls @ref destroy()
+ */
+ void optDestroy() { if(created()) destroy(); }
+
+ /**
+ * @brief releases ownership, returns the state and forgets
+ * @pre @ref created()
+ */
+ State release();
+
+ static void destroy(VmaAllocator);
+
+ /**
+ * @return VMA flags for the enabled features and extensions of the device
+ * @details bufferDeviceAddress, maintenance4, VK_EXT_memory_budget
+ */
+ [[nodiscard]] static VmaAllocatorCreateFlags flags(Device const&);
+
+ [[nodiscard]] constexpr bool created() const { return static_cast(_handle); }
+ [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); }
+ [[nodiscard]] constexpr Device const& device() const { return *_device; }
+
+private:
+ void forget();
+
+ not_null_ptr _device;
+ unique_handle> _handle;
+
+public:
+ Allocator(Allocator const& other) = delete;
+ Allocator(Allocator&& other) noexcept = default;
+ Allocator& operator=(Allocator const& other) = delete;
+ Allocator& operator=(Allocator&& other) noexcept = default;
+
+ static constexpr void debug_check(Allocator const&);
+};
+}
+
+namespace cvk {
+struct Allocator::State {
+ vk_not_null vmaAllocator;
+};
+}
+
+namespace cvk {
+constexpr void Allocator::debug_check(Allocator const& allocator) {
+ CTH_CRITICAL(!allocator.created(), "allocator must be created") {}
+}
+}
diff --git a/cvk/incl/cvk/base/core.hpp b/cvk/incl/cvk/base/core.hpp
new file mode 100644
index 00000000..565a7da1
--- /dev/null
+++ b/cvk/incl/cvk/base/core.hpp
@@ -0,0 +1,267 @@
+#pragma once
+#include "core_config.hpp"
+#include "device_table.hpp"
+
+#include "cvk/base/queue/queue.hpp"
+#include "cvk/utility/types.hpp"
+
+#include
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+
+namespace cvk {
+class Allocator;
+class Device;
+class Instance;
+class PhysicalDevice;
+class Surface;
+
+/**
+ * @brief picks the physical device and owns it with the device, the allocator and the queues
+ * @details the instance outlives the core,
+ * the queues stay valid when the core moves
+ */
+class Core {
+public:
+ using Config = CoreConfig;
+ using queue_set_t = Config::queue_set_t;
+ struct State;
+
+ /**
+ * @brief base constructor
+ * @pre @ref Instance::created()
+ */
+ explicit Core(Instance const&);
+
+ /**
+ * @brief constructs and creates
+ * @details calls:
+ * - @ref Core(Instance const&)
+ * - @ref create()
+ */
+ Core(Instance const&, std::span surfaces, Config const&);
+
+ /**
+ * @brief constructs and wraps
+ * @details calls:
+ * - @ref Core(Instance const&)
+ * - @ref wrap()
+ */
+ Core(Instance const&, State);
+
+ /**
+ * @note calls @ref optDestroy()
+ */
+ ~Core();
+
+ /**
+ * @brief wraps the state
+ * @details empty @ref State::queueIndices -> entry i uses queue i of the device,
+ * nullptr @ref State::allocator -> creates the allocator
+ * @post @ref created()
+ * @note calls @ref optDestroy()
+ */
+ void wrap(State);
+
+ /**
+ * @brief picks a physical device for the first possible queue set, creates the device, the allocator and the queues
+ * @param surfaces the present queues present to every surface
+ * @details the first queue set that a physical device supports wins,
+ * then the best physical device for this set, see @ref pick_physical_device() and @ref Config::device
+ * @post @ref created()
+ * @note calls @ref optDestroy()
+ * @throws cvk::cvk_exception reason: no physical device meets the requirements
+ * @throws cvk::cvk_exception reason: @ref Config::device is missing or does not meet the requirements
+ * @throws cvk::vk_result_exception result of vkCreateDevice()
+ * @throws cvk::vk_result_exception result of vmaCreateAllocator()
+ */
+ void create(std::span surfaces, Config const&);
+
+ /**
+ * @brief destroys and forgets
+ * @pre @ref created()
+ */
+ void destroy();
+
+ /**
+ * @brief if @ref created() -> calls @ref destroy()
+ */
+ void optDestroy() { if(created()) destroy(); }
+
+ /**
+ * @brief releases ownership, returns the state and forgets
+ * @pre @ref created()
+ */
+ State release();
+
+ [[nodiscard]] constexpr bool created() const { return _device != nullptr; }
+
+ /**
+ * @return index of the used queue set in @ref Config::queueSets
+ * @pre @ref created()
+ * @pre the config had queue sets
+ */
+ [[nodiscard]] size_t queueSetIndex() const;
+
+ [[nodiscard]] constexpr Instance const& instance() const { return *_instance; }
+ [[nodiscard]] VkInstance vkInstance() const;
+
+ /**
+ * @pre @ref created()
+ */
+ [[nodiscard]] PhysicalDevice const& physicalDevice() const;
+ [[nodiscard]] VkPhysicalDevice vkPhysicalDevice() const;
+
+ /**
+ * @pre @ref created()
+ */
+ [[nodiscard]] Device const& device() const;
+ [[nodiscard]] DeviceTable deviceTable() const;
+ [[nodiscard]] VolkDeviceTable const* functions() const;
+ [[nodiscard]] VkDevice vkDevice() const;
+
+ /**
+ * @pre @ref created()
+ */
+ [[nodiscard]] Allocator const& allocator() const;
+
+ /**
+ * @param idx index in @ref Config::queueProperties
+ * @return the queue of the device, entries with the same @ref queueIndex() return the same queue
+ * @pre @ref created()
+ */
+ [[nodiscard]] Queue const& queue(size_t idx) const;
+
+ /**
+ * @param idx index in @ref Config::queueProperties
+ * @return index of the queue in @ref Device::queues(), entries with the same index share the VkQueue
+ * @pre @ref created()
+ */
+ [[nodiscard]] constexpr size_t queueIndex(size_t idx) const {
+ debug_check(*this);
+ return _queueIndices[idx];
+ }
+
+ /**
+ * @return @ref queueIndex() per entry of @ref Config::queueProperties
+ */
+ [[nodiscard]] constexpr std::span queueIndices() const { return _queueIndices; }
+
+private:
+ /**
+ * @brief features and extensions of the device
+ */
+ struct device_requirements {
+ DeviceFeatures features;
+ std::vector extensions;
+ };
+
+ /**
+ * @brief physical device for a queue set and the mapping of the queues
+ */
+ struct device_pick {
+ std::unique_ptr physicalDevice;
+ std::vector uniqueQueueFamilyIndices;
+ std::vector queuesToUniqueQueues;
+
+ /**
+ * @brief std::nullopt -> the config had no queue sets
+ */
+ std::optional queueSetIndex = std::nullopt;
+ };
+ struct queue_set_relations {
+ std::vector uniqueQueues;
+ std::vector queueToUniqueQueue;
+ };
+
+ void forget();
+
+ /**
+ * @return the base set, the config and the present set with a present queue
+ */
+ [[nodiscard]] static device_requirements requirements(Config const&);
+
+ /**
+ * @brief picks a physical device for the first possible queue set
+ * @throws cvk::cvk_exception reason: no physical device meets the requirements
+ * @throws cvk::cvk_exception reason: @ref Config::device is missing or does not meet the requirements
+ */
+ [[nodiscard]] device_pick pickPhysicalDevice(
+ std::span surfaces,
+ Config const&,
+ device_requirements const&
+ ) const;
+
+ [[nodiscard]] static queue_set_relations createQueueSetRelations(
+ queue_set_t const& queueSet,
+ std::span queueProperties
+ );
+
+ /**
+ * @return std::nullopt -> no physical device supports the queue set
+ */
+ [[nodiscard]] std::optional tryPickPhysicalDevice(
+ std::span surfaces,
+ Config const&,
+ device_requirements const&,
+ queue_set_t const& queueSet
+ ) const;
+
+ not_null_ptr _instance;
+
+ std::unique_ptr _physicalDevice;
+ // declared before the device: move assignment destroys the old allocator before the old device
+ std::unique_ptr _allocator;
+ std::unique_ptr _device;
+ std::optional _queueSetIndex;
+ std::vector _queueIndices;
+
+public:
+ Core(Core const& other) = delete;
+ Core(Core&& other) noexcept;
+ Core& operator=(Core const& other) = delete;
+ Core& operator=(Core&& other) noexcept;
+
+ static constexpr void debug_check(Core const&);
+};
+}
+
+namespace cvk {
+struct Core::State {
+ /**
+ * @pre @ref PhysicalDevice::created()
+ */
+ unique_not_null physicalDevice;
+ /**
+ * @pre @ref Device::created()
+ * @pre created with @ref physicalDevice
+ */
+ unique_not_null device;
+ /**
+ * @details nullptr -> @ref wrap() creates it
+ * @pre @ref Allocator::created()
+ * @pre created with @ref device
+ */
+ std::unique_ptr allocator;
+ /**
+ * @brief index in @ref Device::queues() per entry of @ref Config::queueProperties
+ * @details empty -> entry i uses queue i of @ref device
+ * @pre every index < queue count of @ref device
+ */
+ std::vector queueIndices{};
+ std::optional queueSetIndex = std::nullopt;
+};
+}
+
+namespace cvk {
+constexpr void Core::debug_check(Core const& core) {
+ CTH_CRITICAL(!core.created(), "core must be created") {}
+}
+}
diff --git a/cvk/incl/cvk/base/core_config.hpp b/cvk/incl/cvk/base/core_config.hpp
new file mode 100644
index 00000000..e2c4ced4
--- /dev/null
+++ b/cvk/incl/cvk/base/core_config.hpp
@@ -0,0 +1,66 @@
+#pragma once
+#include "physical_device.hpp"
+#include "queue/queue_family.hpp"
+
+#include "cvk/utility/device/device_features.hpp"
+
+#include
+#include
+
+#include
+#include
+#include
+#include
+
+
+namespace cvk {
+struct CoreConfig {
+ using queue_set_t = cth::dt::union_find;
+
+ /**
+ * @brief properties of each queue to create
+ */
+ std::vector queueProperties;
+
+ /**
+ * @brief groups of queues, the first possible set is used, a group shares one vulkan queue
+ * @details each root is a group, the properties of a group are combined,
+ * groups go to different queue families where possible
+ * @note empty -> every queue is a group, same as {0, ..., N - 1}
+ */
+ std::vector queueSets{};
+
+ /**
+ * @brief device features besides @ref constants::BASE_DEVICE_FEATURES
+ * @details a present queue adds @ref constants::PRESENT_DEVICE_FEATURES
+ */
+ DeviceFeatures features{};
+
+ /**
+ * @brief device extensions besides the extensions of the features
+ * @details a present queue adds @ref constants::PRESENT_DEVICE_EXTENSIONS
+ */
+ std::vector extensions{};
+
+ /**
+ * @brief std::nullopt -> the best suitable device
+ * @details set -> exactly this device, see @ref PhysicalDevice::uuid()
+ */
+ std::optional device = std::nullopt;
+
+ static constexpr void debug_check(CoreConfig const& config);
+};
+}
+
+namespace cvk {
+constexpr void CoreConfig::debug_check(CoreConfig const& config) {
+ CTH_CRITICAL(config.queueProperties.empty(), "at least one queue is required") {}
+ CTH_CRITICAL(
+ std::ranges::any_of(
+ config.queueSets,
+ [queues = config.queueProperties.size()](queue_set_t const& set) { return set.size() != queues; }
+ ),
+ "sets must span all queues"
+ ) {}
+}
+}
diff --git a/cvk/incl/cvk/base/cvk.hpp b/cvk/incl/cvk/base/cvk.hpp
new file mode 100644
index 00000000..14cb9762
--- /dev/null
+++ b/cvk/incl/cvk/base/cvk.hpp
@@ -0,0 +1,53 @@
+#pragma once
+
+namespace cth::co {
+class global_switch;
+}
+
+namespace cvk {
+/**
+ * @brief loads the vulkan loader while a Cvk exists, neither copyable nor movable
+ * @details reference counted like std::ios_base::Init: every Cvk shares the one loader of the process,
+ * the first Cvk loads it, the last one unloads it, thread safe,
+ * the Cvk outlives every instance
+ */
+class Cvk {
+public:
+ /**
+ * @brief the first Cvk loads the vulkan loader through volk
+ * @post @ref initialized()
+ * @throws cvk::vk_result_exception result of volkInitialize()
+ */
+ Cvk();
+
+ /**
+ * @brief the last Cvk unloads the vulkan loader
+ * @pre the last Cvk -> no instance exists
+ */
+ ~Cvk();
+
+ /**
+ * @return a Cvk exists
+ */
+ [[nodiscard]] static bool initialized();
+
+private:
+ /**
+ * @return the switch that loads and unloads the loader
+ * @details a function local static: a Cvk at namespace scope in another translation unit finds it constructed
+ */
+ [[nodiscard]] static cth::co::global_switch& loader();
+
+ /**
+ * @throws cvk::vk_result_exception result of volkInitialize()
+ */
+ static void load();
+ static void unload();
+
+public:
+ Cvk(Cvk const& other) = delete;
+ Cvk(Cvk&& other) = delete;
+ Cvk& operator=(Cvk const& other) = delete;
+ Cvk& operator=(Cvk&& other) = delete;
+};
+}
diff --git a/cvk/incl/cvk/base/device.hpp b/cvk/incl/cvk/base/device.hpp
new file mode 100644
index 00000000..aef4a2e7
--- /dev/null
+++ b/cvk/incl/cvk/base/device.hpp
@@ -0,0 +1,214 @@
+#pragma once
+#include "device_config.hpp"
+#include "device_table.hpp"
+
+#include "cvk/base/queue/queue.hpp"
+#include "cvk/utility/handle/unique_handle.hpp"
+#include "cvk/utility/types.hpp"
+
+#include
+
+#include
+
+#include
+#include
+#include
+#include
+
+
+namespace cvk {
+class Instance;
+class PhysicalDevice;
+
+/**
+ * @brief VkDevice wrapper, owns the function table and the queues
+ * @details the queues stay valid when the device moves
+ */
+class Device {
+public:
+ using vk_handle_t = VkDevice;
+ using Config = DeviceConfig;
+ struct State;
+
+ /**
+ * @brief base constructor
+ * @pre @ref Instance::created()
+ * @pre @ref PhysicalDevice::created()
+ */
+ Device(Instance const&, PhysicalDevice const&);
+
+ /**
+ * @brief constructs and wraps
+ * @details calls:
+ * - @ref Device(Instance const&, PhysicalDevice const&)
+ * - @ref wrap()
+ */
+ Device(Instance const&, PhysicalDevice const&, State);
+
+ /**
+ * @brief constructs and creates
+ * @details calls:
+ * - @ref Device(Instance const&, PhysicalDevice const&)
+ * - @ref create()
+ */
+ Device(Instance const&, PhysicalDevice const&, Config const&);
+
+ /**
+ * @note calls @ref optDestroy()
+ */
+ ~Device() { optDestroy(); }
+
+ /**
+ * @brief wraps the state and retrieves the queues of @ref State::queueFamilyIndices
+ * @details nullptr @ref State::functionTable -> loads the functions
+ * @pre the device has the queues of @ref State::queueFamilyIndices
+ * @post @ref created()
+ * @note calls @ref optDestroy()
+ */
+ void wrap(State);
+
+ /**
+ * @brief creates the device with the features and extensions of the config and retrieves the queues
+ * @details queues of one family get the indices 0, 1, ... in the order of @ref Config::queueFamilyIndices
+ * @pre the physical device has the features and the extensions of the config
+ * @post @ref created()
+ * @note calls @ref optDestroy()
+ * @throws cvk::vk_result_exception result of vkCreateDevice()
+ */
+ void create(Config const&);
+
+ /**
+ * @brief destroys and forgets
+ * @pre @ref created()
+ */
+ void destroy();
+
+ /**
+ * @brief if @ref created() -> calls @ref destroy()
+ */
+ void optDestroy() { if(created()) destroy(); }
+
+ /**
+ * @brief releases ownership, returns the state and forgets
+ * @pre @ref created()
+ */
+ State release();
+
+ /**
+ * @brief blocks until every queue of the device finished its work
+ * @pre @ref created()
+ * @pre no other thread uses a queue of the device, Vulkan requires it
+ * @throws cvk::vk_result_exception result of vkDeviceWaitIdle()
+ */
+ void waitIdle() const;
+
+ /**
+ * @brief destroys the device, gets vkDestroyDevice through vkGetDeviceProcAddr()
+ */
+ static void destroy(VkDevice);
+
+ [[nodiscard]] constexpr std::span queues() const { return _queues; }
+ [[nodiscard]] constexpr std::span queues() { return _queues; }
+
+ [[nodiscard]] constexpr DeviceTable table() const { return DeviceTable{_handle.get(), _functionTable.get()}; }
+ [[nodiscard]] constexpr VolkDeviceTable const* functions() const { return _functionTable.get(); }
+ [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); }
+
+ [[nodiscard]] constexpr Instance const& instance() const { return *_instance; }
+ [[nodiscard]] constexpr PhysicalDevice const& physicalDevice() const { return *_physicalDevice; }
+
+ /**
+ * @return the enabled features
+ */
+ [[nodiscard]] constexpr DeviceFeatures const& features() const { return _features; }
+
+ /**
+ * @return the enabled extensions, the extensions of the features included
+ */
+ [[nodiscard]] constexpr std::span extensions() const { return _extensions; }
+
+ [[nodiscard]] constexpr bool created() const { return static_cast(_handle); }
+
+private:
+ void forget();
+
+ /**
+ * @param familyIndices family index of each queue
+ * @return count of queues per family, one entry per family of the physical device
+ * @pre the physical device has a family for each index
+ */
+ [[nodiscard]] std::vector calcQueueFamiliesQueueCounts(std::span familyIndices) const;
+
+ /**
+ * @brief creates the device with @ref _features and @ref _extensions
+ * @param queueFamiliesQueueCounts count of queues per family, the index is the family index
+ * @pre some count > 0
+ * @throws cvk::vk_result_exception result of vkCreateDevice()
+ */
+ void createLogicalDevice(std::span queueFamiliesQueueCounts);
+
+ void loadFunctionTable(VkDevice vkDevice) const;
+
+ /**
+ * @brief retrieves the queues
+ * @param familyIndices family index of each queue
+ * @pre the device has the queues
+ */
+ void createQueues(std::span familyIndices);
+
+
+ not_null_ptr _instance;
+ not_null_ptr _physicalDevice;
+
+ unique_handle> _handle;
+ std::unique_ptr _functionTable = std::make_unique();
+
+ DeviceFeatures _features{};
+ std::vector _extensions{};
+ std::vector _queues{};
+
+public:
+ Device(Device const& other) = delete;
+ Device(Device&& other) noexcept = default;
+ Device& operator=(Device const& other) = delete;
+ Device& operator=(Device&& other) noexcept = default;
+
+ static constexpr void debug_check(Device const&);
+ static constexpr void debug_check_handle(vk_not_null);
+};
+}
+
+namespace cvk {
+struct Device::State {
+ vk_not_null vkDevice;
+ /**
+ * @brief volk function table of @ref vkDevice
+ * @details nullptr -> @ref wrap() loads it
+ */
+ std::unique_ptr functionTable;
+
+ /**
+ * @brief family index of each queue, see @ref Config::queueFamilyIndices
+ */
+ std::vector queueFamilyIndices{};
+
+ /**
+ * @brief the features that the device enables
+ */
+ DeviceFeatures features{};
+
+ /**
+ * @brief the extensions that the device enables, the extensions of the features included
+ */
+ std::vector extensions{};
+};
+}
+
+namespace cvk {
+constexpr void Device::debug_check(Device const& device) {
+ CTH_CRITICAL(!device.created(), "device must be created") {}
+ debug_check_handle(device.get());
+}
+
+constexpr void Device::debug_check_handle([[maybe_unused]] vk_not_null vkDevice) {}
+}
diff --git a/cvk/incl/cvk/base/device_config.hpp b/cvk/incl/cvk/base/device_config.hpp
new file mode 100644
index 00000000..61686c58
--- /dev/null
+++ b/cvk/incl/cvk/base/device_config.hpp
@@ -0,0 +1,30 @@
+#pragma once
+#include "queue/queue_family.hpp"
+
+#include "cvk/utility/device/device_features.hpp"
+
+#include
+#include
+
+namespace cvk {
+/**
+ * @brief queues, features and extensions of a device
+ * @details cvk needs @ref constants::BASE_DEVICE_FEATURES, @ref Core adds them
+ */
+struct DeviceConfig {
+ /**
+ * @brief family index of every queue to create
+ */
+ std::vector queueFamilyIndices;
+
+ /**
+ * @brief features to enable, a true bit of an extension struct enables its extension
+ */
+ DeviceFeatures features{};
+
+ /**
+ * @brief extensions to enable, besides the extensions of the features
+ */
+ std::vector extensions{};
+};
+}
diff --git a/cvk/incl/cvk/base/device_table.hpp b/cvk/incl/cvk/base/device_table.hpp
new file mode 100644
index 00000000..1a54f665
--- /dev/null
+++ b/cvk/incl/cvk/base/device_table.hpp
@@ -0,0 +1,14 @@
+#pragma once
+#include "cvk/utility/types.hpp"
+
+#include
+
+namespace cvk {
+struct DeviceTable {
+ vk_not_null vkDevice;
+ not_null_ptr table;
+
+ VolkDeviceTable const* operator ->() const { return table.get(); }
+ [[nodiscard]] constexpr VkDevice device() const { return vkDevice.get(); }
+};
+}
diff --git a/cvk/incl/cvk/base/instance.hpp b/cvk/incl/cvk/base/instance.hpp
new file mode 100644
index 00000000..f6142a93
--- /dev/null
+++ b/cvk/incl/cvk/base/instance.hpp
@@ -0,0 +1,201 @@
+#pragma once
+#include "instance_config.hpp"
+
+#include "cvk/debug/debug_messenger.hpp"
+#include "cvk/utility/constants.hpp"
+#include "cvk/utility/handle/unique_handle.hpp"
+#include "cvk/utility/types.hpp"
+
+#include
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+
+namespace cvk {
+/**
+ * @brief VkInstance wrapper, owns the debug messenger
+ */
+class Instance {
+public:
+ using vk_handle_t = VkInstance;
+ using Config = InstanceConfig;
+ struct State;
+
+ static constexpr std::array VALIDATION_LAYERS = {
+ "VK_LAYER_KHRONOS_validation"
+ };
+ static constexpr std::array VALIDATION_LAYER_EXTENSIONS{
+ VK_EXT_DEBUG_UTILS_EXTENSION_NAME,
+ };
+
+ static constexpr VkBool32 VALIDATE_SYNC = VK_TRUE;
+
+ /**
+ * @brief settings of the validation layers
+ * @details validate_sync: reports missing and too narrow barriers, semaphores and other dependencies
+ */
+ static constexpr std::array VALIDATION_LAYER_SETTINGS{
+ VkLayerSettingEXT{
+ .pLayerName = VALIDATION_LAYERS[0],
+ .pSettingName = "validate_sync",
+ .type = VK_LAYER_SETTING_TYPE_BOOL32_EXT,
+ .valueCount = 1,
+ .pValues = &VALIDATE_SYNC,
+ },
+ };
+
+ /**
+ * @brief base constructor
+ */
+ explicit Instance(Config);
+
+ /**
+ * @brief constructs and wraps
+ * @details calls:
+ * - @ref Instance(Config)
+ * - @ref wrap()
+ */
+ Instance(Config, State);
+
+ /**
+ * @brief constructs and creates
+ * @details calls:
+ * - @ref Instance(Config)
+ * - @ref create()
+ */
+ explicit Instance(Config, create_t, std::optional messengerConfig = std::nullopt);
+
+ /**
+ * @note calls @ref optDestroy()
+ */
+ ~Instance() { optDestroy(); }
+
+ /**
+ * @brief wraps the state
+ * @post @ref created()
+ * @post volk uses the functions of this instance
+ * @note calls @ref optDestroy()
+ */
+ void wrap(State);
+
+ /**
+ * @brief creates the instance with the extensions of the config, debug builds with the validation layers
+ * @param messengerConfig std::nullopt -> no messenger, debug builds use the default @ref DebugMessenger::Config
+ * @pre @ref Cvk::initialized()
+ * @post @ref created()
+ * @post volk uses the functions of this instance
+ * @note calls @ref optDestroy()
+ * @note no validation layers -> no messenger
+ * @throws cvk::cvk_exception reason: instance extension missing
+ * @throws cvk::cvk_exception reason: validation layer missing
+ * @throws cvk::vk_result_exception result of vkEnumerateInstanceExtensionProperties()
+ * @throws cvk::vk_result_exception result of vkEnumerateInstanceLayerProperties()
+ * @throws cvk::vk_result_exception result of vkCreateInstance()
+ * @throws cvk::vk_result_exception result of vkCreateDebugUtilsMessengerEXT(), destroys the instance
+ */
+ void create(std::optional messengerConfig = std::nullopt);
+
+ /**
+ * @brief destroys the messenger and the instance and forgets
+ * @pre @ref created()
+ */
+ void destroy();
+
+ /**
+ * @brief if @ref created() -> calls @ref destroy()
+ */
+ void optDestroy() { if(created()) destroy(); }
+
+ /**
+ * @brief releases ownership of the instance and the messenger, returns the state and forgets
+ * @pre @ref created()
+ */
+ State release();
+
+ /**
+ * @pre volk loaded the instance functions, @ref create() and @ref wrap() load them
+ */
+ static void destroy(VkInstance);
+
+ [[nodiscard]] constexpr bool created() const { return static_cast(_handle); }
+ [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); }
+ [[nodiscard]] constexpr Config const& config() const { return _config; }
+
+ /**
+ * @return the enabled extensions, debug builds include @ref VALIDATION_LAYER_EXTENSIONS
+ */
+ [[nodiscard]] constexpr std::span extensions() const { return _extensions; }
+
+ /**
+ * @return nullptr -> no messenger
+ */
+ [[nodiscard]] constexpr DebugMessenger const* debugMessenger() const {
+ return _debugMessenger.has_value() ? &*_debugMessenger : nullptr;
+ }
+
+private:
+ void forget();
+
+ static void loadInstanceFunctions(vk_not_null);
+
+ Config _config;
+ std::vector _extensions{};
+
+ // declared before the handle: move assignment destroys the old messenger before the old instance
+ std::optional _debugMessenger = std::nullopt;
+ unique_handle> _handle;
+
+public:
+ Instance(Instance const& other) = delete;
+ Instance& operator=(Instance const& other) = delete;
+ Instance(Instance&& other) noexcept = default;
+ Instance& operator=(Instance&& other) noexcept = default;
+
+ static constexpr void debug_check(Instance const&);
+ static constexpr void debug_check_handle(vk_not_null);
+};
+
+/**
+ * @brief queries the instance extensions of the loader and the implicit layers
+ * @pre @ref Cvk::initialized()
+ * @throws cvk::vk_result_exception result of vkEnumerateInstanceExtensionProperties()
+ */
+[[nodiscard]] std::vector available_instance_extensions();
+
+/**
+ * @brief queries the instance layers
+ * @pre @ref Cvk::initialized()
+ * @throws cvk::vk_result_exception result of vkEnumerateInstanceLayerProperties()
+ */
+[[nodiscard]] std::vector available_layers();
+}
+
+
+namespace cvk {
+struct Instance::State {
+ vk_not_null vkInstance;
+
+ /**
+ * @brief messenger of @ref vkInstance, std::nullopt -> no messenger
+ */
+ std::optional debugMessenger = std::nullopt;
+
+ /**
+ * @brief the extensions that the instance enables
+ */
+ std::vector extensions{};
+};
+}
+
+namespace cvk {
+constexpr void Instance::debug_check(Instance const& instance) {
+ CTH_CRITICAL(!instance.created(), "instance must be created") {}
+}
+constexpr void Instance::debug_check_handle([[maybe_unused]] vk_not_null vkInstance) {}
+}
diff --git a/cvk/incl/cvk/base/instance_config.hpp b/cvk/incl/cvk/base/instance_config.hpp
new file mode 100644
index 00000000..4abbdd5a
--- /dev/null
+++ b/cvk/incl/cvk/base/instance_config.hpp
@@ -0,0 +1,39 @@
+#pragma once
+#include
+
+#include
+#include
+#include
+
+
+namespace cvk {
+/**
+ * @brief application, engine and extensions of an instance
+ */
+struct InstanceConfig {
+ std::string appName;
+ uint32_t appVersion = 0;
+ std::string engineName = "cvk";
+ uint32_t engineVersion = 0;
+
+ /**
+ * @brief instance extensions, debug builds add @ref Instance::VALIDATION_LAYER_EXTENSIONS
+ */
+ std::vector extensions{};
+
+ /**
+ * @attention the names point into the config
+ */
+ [[nodiscard]] constexpr VkApplicationInfo appInfo() const {
+ return VkApplicationInfo{
+ .sType = VK_STRUCTURE_TYPE_APPLICATION_INFO,
+ .pNext = nullptr,
+ .pApplicationName = appName.c_str(),
+ .applicationVersion = appVersion,
+ .pEngineName = engineName.c_str(),
+ .engineVersion = engineVersion,
+ .apiVersion = VK_API_VERSION_1_3,
+ };
+ }
+};
+}
diff --git a/cvk/incl/cvk/base/physical_device.hpp b/cvk/incl/cvk/base/physical_device.hpp
new file mode 100644
index 00000000..7916d2bf
--- /dev/null
+++ b/cvk/incl/cvk/base/physical_device.hpp
@@ -0,0 +1,171 @@
+#pragma once
+#include "cvk/base/queue/queue_family.hpp"
+#include "cvk/utility/device/device_features.hpp"
+#include "cvk/utility/types.hpp"
+
+#include
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+
+namespace cvk {
+class Instance;
+class Surface;
+
+using device_uuid_t = std::array;
+
+/**
+ * @brief VkPhysicalDevice with its queried extensions, features, properties and queue families
+ * @details copyable, the instance owns the physical device
+ */
+class PhysicalDevice {
+public:
+ using vk_handle_t = VkPhysicalDevice;
+
+ /**
+ * @brief queries the device
+ * @param surfaces a family presents only if it presents to every surface, empty -> no present support
+ * @pre the instance of the device is created
+ * @throws cvk::vk_result_exception result of vkEnumerateDeviceExtensionProperties()
+ * @throws cvk::vk_result_exception result of vkGetPhysicalDeviceSurfaceSupportKHR()
+ */
+ explicit PhysicalDevice(vk_not_null, std::span surfaces = {});
+
+ ~PhysicalDevice() = default;
+
+ /**
+ * @param features to require
+ * @param extensions to require, besides the extensions of the features
+ * @return names of the features and the extensions that the device does not have
+ */
+ [[nodiscard]] std::vector missing(
+ DeviceFeatures const& features,
+ std::span extensions = {}
+ ) const;
+
+ /**
+ * @brief finds queue families for the requested queues
+ * @param queueProperties requested queues
+ * @return family index per requested queue, empty -> no combination
+ */
+ [[nodiscard]] std::vector queueFamilyIndices(
+ std::span queueProperties
+ ) const;
+
+ /**
+ * @return first of the candidates that supports the features with the tiling
+ * @throws cvk::cvk_exception reason: no candidate supports the features
+ */
+ [[nodiscard]] VkFormat findSupportedFormat(
+ std::span candidates,
+ VkImageTiling tiling,
+ VkFormatFeatureFlags features
+ ) const;
+
+ /**
+ * @param idx queue family index
+ * @return nullptr -> no family with the index
+ */
+ [[nodiscard]] QueueFamily const* queueFamily(queue_family_index_t idx) const;
+
+ [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); }
+ [[nodiscard]] constexpr std::span extensions() const { return _extensions; }
+ [[nodiscard]] constexpr DeviceFeatures const& features() const { return _features; }
+ [[nodiscard]] constexpr VkPhysicalDeviceProperties const& properties() const { return _properties; }
+ [[nodiscard]] constexpr VkPhysicalDeviceLimits const& limits() const { return _properties.limits; }
+
+ /**
+ * @return deviceUUID of VkPhysicalDeviceIDProperties, the same across instances and processes
+ */
+ [[nodiscard]] constexpr device_uuid_t const& uuid() const { return _uuid; }
+ [[nodiscard]] constexpr VkPhysicalDeviceMemoryProperties const& memoryProperties() const {
+ return _memoryProperties;
+ }
+ [[nodiscard]] constexpr VkSampleCountFlagBits maxSampleCount() const { return _maxSampleCount; }
+ [[nodiscard]] constexpr std::span queueFamilies() const { return _queueFamilies; }
+
+private:
+ move_ptr _handle;
+ std::vector _extensions;
+ DeviceFeatures _features;
+ VkPhysicalDeviceProperties _properties;
+ device_uuid_t _uuid;
+ VkPhysicalDeviceMemoryProperties _memoryProperties;
+ VkSampleCountFlagBits _maxSampleCount;
+ std::vector _queueFamilies;
+
+public:
+ PhysicalDevice(PhysicalDevice const& other) = default;
+ PhysicalDevice(PhysicalDevice&& other) noexcept = default;
+ PhysicalDevice& operator=(PhysicalDevice const& other) = default;
+ PhysicalDevice& operator=(PhysicalDevice&& other) noexcept = default;
+
+ static constexpr void debug_check(PhysicalDevice const&);
+};
+
+/**
+ * @brief physical device for the requested queues, with the family index of each queue
+ */
+struct PhysicalDevicePick {
+ PhysicalDevice device;
+ std::vector queueFamilyIndices;
+};
+
+/**
+ * @brief queries every physical device of the instance
+ * @param surfaces a family presents only if it presents to every surface, empty -> no present support
+ * @pre @ref Instance::created()
+ * @throws cvk::vk_result_exception result of vkEnumeratePhysicalDevices()
+ */
+[[nodiscard]] std::vector enumerate_physical_devices(
+ Instance const&,
+ std::span surfaces = {}
+);
+
+/**
+ * @brief picks the best physical device with the features, the extensions and the queues
+ * @param surfaces a family presents only if it presents to every surface
+ * @param extensions to require, besides the extensions of the features
+ * @param uuid std::nullopt -> every device counts, set -> only the device with this uuid counts
+ * @details best: the lowest @ref dev::device_type_rank(), ties go to the first in enumeration order
+ * @return std::nullopt -> no device meets the requirements
+ * @pre @ref Instance::created()
+ * @throws cvk::vk_result_exception result of vkEnumeratePhysicalDevices()
+ */
+[[nodiscard]] std::optional pick_physical_device(
+ Instance const&,
+ std::span surfaces,
+ std::span queueProperties,
+ DeviceFeatures const& features,
+ std::span extensions = {},
+ std::optional const& uuid = std::nullopt
+);
+}
+
+namespace cvk::dev {
+/**
+ * @return rank of the device type for the pick, 0 is the best: discrete, integrated, virtual, cpu, other
+ */
+[[nodiscard]] constexpr uint32_t device_type_rank(VkPhysicalDeviceType type) {
+ switch(type) {
+ case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU: return 0;
+ case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU: return 1;
+ case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU: return 2;
+ case VK_PHYSICAL_DEVICE_TYPE_CPU: return 3;
+ default: return 4;
+ }
+}
+}
+
+namespace cvk {
+constexpr void PhysicalDevice::debug_check(PhysicalDevice const& device) {
+ CTH_CRITICAL(device.get() == VK_NULL_HANDLE, "physical device must not be moved from") {}
+}
+}
diff --git a/cvk/incl/cvk/base/queue/queue.hpp b/cvk/incl/cvk/base/queue/queue.hpp
new file mode 100644
index 00000000..ae71b4bc
--- /dev/null
+++ b/cvk/incl/cvk/base/queue/queue.hpp
@@ -0,0 +1,109 @@
+#pragma once
+#include "queue_family.hpp"
+#include "queue_state.hpp"
+#include "submit_info.hpp"
+
+#include
+
+#include
+
+#include
+
+
+namespace cvk {
+class Fence;
+
+/**
+ * @brief VkQueue of a device
+ * @details copyable, copies share the vulkan queue,
+ * the device owns the vulkan queue, a queue has no lifecycle,
+ * a copy stays valid while the VkDevice and its function table exist,
+ * also through a release and wrap of the device with the function table of the state
+ * @attention not thread safe: one thread at a time per vulkan queue, Vulkan requires it
+ */
+class Queue {
+public:
+ using vk_handle_t = VkQueue;
+ using State = QueueState;
+
+ /**
+ * @brief the device constructs its queues
+ */
+ Queue(QueueFamilyProperties familyProperties, State const&);
+
+ /**
+ * @brief submits the infos in one vkQueueSubmit2()
+ * @param fence nullptr -> no fence
+ * @details resolves the std::nullopt timeline values in order: the waits, then the signals of each info
+ * @pre @ref created()
+ * @pre the command buffers ended their recording
+ * @pre the semaphores and the fence are created, the fence is unsignaled
+ * @throws cvk::vk_result_exception result of vkQueueSubmit2()
+ */
+ void submit(std::span infos, Fence const* fence = nullptr) const;
+
+ /**
+ * @brief submits the info
+ * @details calls @ref submit(std::span, Fence const*) const
+ */
+ void submit(SubmitInfo const& info, Fence const* fence = nullptr) const { submit({&info, 1}, fence); }
+
+ /**
+ * @brief submits to the vulkan queue, for what @ref SubmitInfo does not cover
+ * @param fence VK_NULL_HANDLE -> no fence
+ * @throws cvk::vk_result_exception result of vkQueueSubmit2()
+ */
+ void rawSubmit(VkSubmitInfo2 const& submitInfo, VkFence fence = VK_NULL_HANDLE) const;
+
+ /**
+ * @brief presents through vkQueuePresentKHR()
+ * @return VK_SUCCESS, VK_SUBOPTIMAL_KHR or VK_ERROR_OUT_OF_DATE_KHR
+ * @pre @ref canPresent()
+ * @throws cvk::vk_result_exception other result of vkQueuePresentKHR()
+ */
+ [[nodiscard]] VkResult rawPresent(VkPresentInfoKHR const& presentInfo) const;
+
+ /**
+ * @brief blocks until the queue finished all its work
+ * @throws cvk::vk_result_exception result of vkQueueWaitIdle()
+ */
+ void waitIdle() const;
+
+ [[nodiscard]] constexpr bool canPresent() const {
+ return contains(_familyProperties, QueueFamilyProperties::PRESENT);
+ }
+ [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); }
+ [[nodiscard]] constexpr uint32_t index() const { return _queueIndex; }
+ [[nodiscard]] constexpr uint32_t familyIndex() const { return _familyIndex; }
+ [[nodiscard]] constexpr QueueFamilyProperties familyProperties() const { return _familyProperties; }
+
+private:
+ QueueFamilyProperties _familyProperties;
+ VolkDeviceTable const* _functions = nullptr;
+
+ move_ptr _handle = VK_NULL_HANDLE;
+
+ uint32_t _familyIndex = 0;
+ uint32_t _queueIndex = 0;
+
+public:
+ Queue(Queue const& other) = default;
+ Queue(Queue&& other) = default;
+ Queue& operator=(Queue const& other) = default;
+ Queue& operator=(Queue&& other) = default;
+
+ static constexpr void debug_check(Queue const&);
+ static constexpr void debug_check_present(Queue const&);
+};
+}
+
+namespace cvk {
+constexpr void Queue::debug_check(Queue const& queue) {
+ CTH_CRITICAL(queue.get() == VK_NULL_HANDLE, "queue must not be moved from") {}
+}
+
+constexpr void Queue::debug_check_present(Queue const& queue) {
+ debug_check(queue);
+ CTH_CRITICAL(!queue.canPresent(), "queue is not a present queue") {}
+}
+}
diff --git a/cvk/incl/cvk/base/queue/queue_family.hpp b/cvk/incl/cvk/base/queue/queue_family.hpp
new file mode 100644
index 00000000..0d9402ca
--- /dev/null
+++ b/cvk/incl/cvk/base/queue/queue_family.hpp
@@ -0,0 +1,63 @@
+#pragma once
+#include "cvk/utility/vk_hash.hpp"
+#include "cvk/utility/vk_overloads.hpp"
+
+#include
+
+#include
+
+
+namespace cvk {
+enum class QueueFamilyProperties {
+ NONE = 0,
+ GRAPHICS = cth::en::flag_val(0),
+ COMPUTE = cth::en::flag_val(1),
+ TRANSFER = cth::en::flag_val(2),
+ PRESENT = cth::en::flag_val(3)
+};
+}
+
+CTH_GEN_ENUM_FLAG_OVERLOADS(cvk::QueueFamilyProperties)
+
+namespace cvk {
+/**
+ * @param presentSupport the family presents to the surfaces
+ * @return the properties of a queue family with the flags
+ * @details graphics or compute -> transfer, also without VK_QUEUE_TRANSFER_BIT:
+ * Vulkan lets graphics and compute families leave the bit out
+ */
+constexpr QueueFamilyProperties to_queue_properties(VkQueueFlags flags, bool presentSupport) {
+ constexpr VkQueueFlags transfer = VK_QUEUE_TRANSFER_BIT | VK_QUEUE_GRAPHICS_BIT | VK_QUEUE_COMPUTE_BIT;
+
+ auto result = QueueFamilyProperties::NONE;
+ if(flags & VK_QUEUE_GRAPHICS_BIT) result |= QueueFamilyProperties::GRAPHICS;
+ if(flags & VK_QUEUE_COMPUTE_BIT) result |= QueueFamilyProperties::COMPUTE;
+ if(flags & transfer) result |= QueueFamilyProperties::TRANSFER;
+ if(presentSupport) result |= QueueFamilyProperties::PRESENT;
+ return result;
+}
+
+using queue_family_index_t = uint32_t;
+
+struct QueueFamily {
+ queue_family_index_t index;
+ QueueFamilyProperties properties;
+ VkQueueFamilyProperties vkProperties;
+
+ static constexpr QueueFamily Vk(
+ queue_family_index_t index,
+ VkQueueFamilyProperties const& vkProperties,
+ bool presentSupport
+ ) {
+ return {
+ .index = index,
+ .properties = to_queue_properties(vkProperties.queueFlags, presentSupport),
+ .vkProperties = vkProperties
+ };
+ }
+
+ [[nodiscard]] constexpr bool operator==(QueueFamily const&) const = default;
+};
+}
+
+CTH_HASH_AGGREGATE(cvk::QueueFamily)
diff --git a/cvk/incl/cvk/base/queue/queue_state.hpp b/cvk/incl/cvk/base/queue/queue_state.hpp
new file mode 100644
index 00000000..a7d87585
--- /dev/null
+++ b/cvk/incl/cvk/base/queue/queue_state.hpp
@@ -0,0 +1,23 @@
+#pragma once
+#include "cvk/utility/types.hpp"
+
+#include
+
+namespace cvk {
+struct QueueState {
+ /**
+ * @brief function table of the device
+ * @details stable when the device moves
+ */
+ not_null_ptr functions;
+ vk_not_null vkQueue;
+ /**
+ * @brief vulkan family index
+ */
+ uint32_t familyIndex;
+ /**
+ * @brief index in the family
+ */
+ uint32_t queueIndex;
+};
+}
diff --git a/cvk/incl/cvk/base/queue/submit_info.hpp b/cvk/incl/cvk/base/queue/submit_info.hpp
new file mode 100644
index 00000000..03e3752a
--- /dev/null
+++ b/cvk/incl/cvk/base/queue/submit_info.hpp
@@ -0,0 +1,63 @@
+#pragma once
+#include "cvk/utility/types.hpp"
+
+#include
+
+#include
+#include
+#include
+#include
+
+
+namespace cvk {
+class PrimaryCmdBuffer;
+class Semaphore;
+class TimelineSemaphore;
+
+/**
+ * @brief semaphore of a submit, binary or timeline
+ * @details the constructor decides the kind
+ */
+struct SemaphoreSubmitInfo {
+ /**
+ * @param stages wait -> the stages that wait, signal -> the stages that finish before the signal
+ */
+ constexpr SemaphoreSubmitInfo(
+ Semaphore const& semaphore,
+ VkPipelineStageFlags2 stages = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT
+ ) : semaphore{&semaphore}, stages{stages} {}
+
+ /**
+ * @param stages wait -> the stages that wait, signal -> the stages that finish before the signal
+ * @param value std::nullopt -> resolved at the submit,
+ * wait: @ref TimelineSemaphore::value(), signal: @ref TimelineSemaphore::next()
+ */
+ constexpr SemaphoreSubmitInfo(
+ TimelineSemaphore const& semaphore,
+ VkPipelineStageFlags2 stages = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
+ std::optional value = std::nullopt
+ ) : semaphore{&semaphore}, stages{stages}, value{value} {}
+
+ std::variant, not_null_ptr> semaphore;
+ VkPipelineStageFlags2 stages;
+
+ /**
+ * @brief timeline semaphores only
+ * @attention an explicit value does not change the counter of the semaphore
+ */
+ std::optional value = std::nullopt;
+};
+
+/**
+ * @brief one batch of a queue submit
+ */
+struct SubmitInfo {
+ /**
+ * @brief the gpu executes them in order
+ */
+ std::vector> cmdBuffers{};
+
+ std::vector waits{};
+ std::vector signals{};
+};
+}
diff --git a/cvk/incl/cvk/data/buffer/base_buffer.hpp b/cvk/incl/cvk/data/buffer/base_buffer.hpp
new file mode 100644
index 00000000..ede10c1c
--- /dev/null
+++ b/cvk/incl/cvk/data/buffer/base_buffer.hpp
@@ -0,0 +1,236 @@
+#pragma once
+#include "buffer_config.hpp"
+
+#include "cvk/base/device_table.hpp"
+#include "cvk/data/memory/memory.hpp"
+#include "cvk/utility/handle/core_handle.hpp"
+#include "cvk/utility/range.hpp"
+#include "cvk/utility/types.hpp"
+
+#include
+
+#include
+
+#include
+#include
+#include
+
+
+namespace cvk {
+class BaseCmdBuffer;
+class Core;
+class Image;
+
+/**
+ * @brief offsets and size of a buffer copy
+ * @details in bytes, in elements for a typed buffer
+ */
+struct BufferCopy {
+ size_t srcOffset = 0;
+ size_t dstOffset = 0;
+
+ /**
+ * @brief (VK_WHOLE_SIZE => as much as fits into both buffers)
+ */
+ size_t size = VK_WHOLE_SIZE;
+};
+
+/**
+ * @brief VkBuffer wrapper, owns the buffer and its memory, sizes in bytes
+ * @details @ref create() creates both with vmaCreateBuffer(),
+ * map, flush, invalidate and write forward to the memory,
+ * @ref Buffer adds an element type
+ */
+class BaseBuffer {
+public:
+ using vk_handle_t = VkBuffer;
+ using Config = BufferConfig;
+ struct State;
+
+ /**
+ * @brief base constructor
+ * @pre @ref Core::created()
+ */
+ BaseBuffer(Core const&, Config const&);
+
+ /**
+ * @brief constructs and wraps
+ * @details calls:
+ * - @ref BaseBuffer(Core const&, Config const&)
+ * - @ref wrap()
+ */
+ BaseBuffer(Core const&, Config const&, State const&);
+
+ /**
+ * @brief constructs and creates
+ * @details calls:
+ * - @ref BaseBuffer(Core const&, Config const&)
+ * - @ref create()
+ */
+ BaseBuffer(Core const&, Config const&, size_t size);
+
+ /**
+ * @note calls @ref optDestroy()
+ */
+ ~BaseBuffer() { optDestroy(); }
+
+ /**
+ * @brief wraps the state
+ * @details std::nullopt @ref State::memory -> no own memory, @ref destroy() only destroys the buffer
+ * @pre the buffer is bound to its memory
+ * @pre the buffer was created with the usage of the config
+ * @post @ref created()
+ * @note calls @ref optDestroy()
+ */
+ void wrap(State const&);
+
+ /**
+ * @brief creates the buffer and its memory
+ * @param size in bytes
+ * @post @ref created()
+ * @note calls @ref optDestroy()
+ * @throws cvk::vk_result_exception result of vmaCreateBuffer()
+ */
+ void create(size_t size);
+
+ /**
+ * @brief destroys the buffer, then its memory, and forgets
+ * @pre @ref created()
+ * @pre the gpu does not use the buffer
+ */
+ void destroy();
+
+ /**
+ * @brief if @ref created() -> calls @ref destroy()
+ */
+ void optDestroy() { if(created()) destroy(); }
+
+ /**
+ * @brief releases ownership of the buffer and its memory, returns the state and forgets
+ * @pre @ref created()
+ */
+ State release();
+
+ /**
+ * @brief records a copy from the source buffer
+ * @param region in bytes
+ * @pre @ref created() and src created
+ * @pre @ref BaseCmdBuffer::recording()
+ * @pre src usage has VK_BUFFER_USAGE_TRANSFER_SRC_BIT, the usage has VK_BUFFER_USAGE_TRANSFER_DST_BIT
+ * @pre 0 < copied size, the ranges lie in the buffers
+ */
+ void copy(BaseCmdBuffer const&, BaseBuffer const& src, BufferCopy region = {}) const;
+
+ /**
+ * @brief records a copy from the mip level of the image
+ * @param dstOffset in bytes
+ * @details calls @ref Image::copyTo()
+ */
+ void copy(
+ BaseCmdBuffer const&,
+ Image const& src,
+ size_t dstOffset = 0,
+ uint32_t mipLevel = 0,
+ VkImageLayout layout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL
+ ) const;
+
+ /**
+ * @brief maps the range of the memory
+ * @param range in bytes
+ * @return the mapped range, its destructor unmaps
+ * @pre @ref memory() != nullptr
+ * @details calls @ref Memory::map()
+ */
+ [[nodiscard]] Mapping map(Range range = {}) const;
+
+ /**
+ * @brief makes host writes visible to the device
+ * @param range in bytes
+ * @pre @ref memory() != nullptr, the memory is mapped
+ * @details calls @ref Memory::flush()
+ */
+ void flush(Range range = {}) const;
+
+ /**
+ * @brief makes device writes visible to the host
+ * @param range in bytes
+ * @pre @ref memory() != nullptr, the memory is mapped
+ * @details calls @ref Memory::invalidate()
+ */
+ void invalidate(Range range = {}) const;
+
+ /**
+ * @brief copies the data to the buffer
+ * @param offset in bytes
+ * @pre @ref memory() != nullptr
+ * @details calls @ref Memory::write()
+ */
+ void write(std::span data, size_t offset = 0) const;
+
+ static void destroy(DeviceTable table, VkBuffer);
+
+ [[nodiscard]] constexpr bool created() const { return static_cast(_handle); }
+ [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); }
+ [[nodiscard]] constexpr Core const& core() const { return _handle.core(); }
+ [[nodiscard]] constexpr Config const& config() const { return _config; }
+
+ /**
+ * @return size in bytes
+ */
+ [[nodiscard]] constexpr size_t size() const { return _size; }
+
+ /**
+ * @return nullptr -> the buffer does not own its memory
+ */
+ [[nodiscard]] constexpr Memory const* memory() const {
+ return _memory.has_value() && _memory->created() ? &*_memory : nullptr;
+ }
+
+private:
+ void forget();
+
+ /**
+ * @pre @ref memory() != nullptr
+ */
+ [[nodiscard]] constexpr Memory const& ownMemory() const;
+
+ // declared before the memory: move assignment destroys the old buffer before its memory
+ core_handle _handle;
+ std::optional