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 _memory = std::nullopt; + Config _config; + size_t _size = 0; + +public: + BaseBuffer(BaseBuffer const& other) = delete; + BaseBuffer(BaseBuffer&& other) noexcept = default; + BaseBuffer& operator=(BaseBuffer const& other) = delete; + BaseBuffer& operator=(BaseBuffer&& other) noexcept = default; + + static constexpr void debug_check(BaseBuffer const&); +}; +} + +namespace cvk { +struct BaseBuffer::State { + vk_not_null vkBuffer; + /** + * @brief in bytes + */ + size_t size; + /** + * @brief std::nullopt -> the buffer does not own its memory + */ + std::optional memory = std::nullopt; +}; +} + +namespace cvk { +constexpr Memory const& BaseBuffer::ownMemory() const { + CTH_CRITICAL(memory() == nullptr, "the buffer must own its memory") {} + return *_memory; +} + +constexpr void BaseBuffer::debug_check(BaseBuffer const& buffer) { + CTH_CRITICAL(!buffer.created(), "buffer must be created") {} +} +} diff --git a/cvk/incl/cvk/data/buffer/buffer.hpp b/cvk/incl/cvk/data/buffer/buffer.hpp new file mode 100644 index 00000000..2c409923 --- /dev/null +++ b/cvk/incl/cvk/data/buffer/buffer.hpp @@ -0,0 +1,160 @@ +#pragma once +#include "base_buffer.hpp" + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + + +namespace cvk { +/** + * @brief T is an index type of Vulkan 1.3 (VK_INDEX_TYPE_UINT8 needs an extension) + */ +template +concept index_element = std::same_as || std::same_as; + +/** + * @brief VkIndexType of T + */ +template +constexpr VkIndexType index_type = std::same_as ? VK_INDEX_TYPE_UINT16 : VK_INDEX_TYPE_UINT32; + +/** + * @brief @ref BaseBuffer with an element type, sizes and offsets in elements + * @tparam T element, the gpu copies it byte by byte + * @details adds no members: a @ref BaseBuffer reference to it sees the same buffer in bytes + */ +template +class Buffer : public BaseBuffer { +public: + /** + * @brief base constructor + * @pre @ref Core::created() + */ + Buffer(Core const& core, Config const& config) : BaseBuffer{core, config} {} + + /** + * @brief constructs and wraps + * @details calls: + * - @ref Buffer(Core const&, Config const&) + * - @ref wrap() + */ + Buffer(Core const& core, Config const& config, State const& state) : Buffer{core, config} { wrap(state); } + + /** + * @brief constructs and creates + * @details calls: + * - @ref Buffer(Core const&, Config const&) + * - @ref create() + */ + Buffer(Core const& core, Config const& config, size_t count) : Buffer{core, config} { create(count); } + + /** + * @brief wraps the state + * @pre @ref State::size is a multiple of sizeof(T) + * @details calls @ref BaseBuffer::wrap() + */ + void wrap(State const& state) { + CTH_CRITICAL(state.size % sizeof(T) != 0, "size must be a multiple of the element size") {} + BaseBuffer::wrap(state); + } + + /** + * @brief creates the buffer and its memory + * @param count of elements + * @details calls @ref BaseBuffer::create() + */ + void create(size_t count) { BaseBuffer::create(count * sizeof(T)); } + + /** + * @brief maps the range of the memory as elements + * @param range in elements + * @return the mapped elements, the destructor unmaps + * @details calls @ref BaseBuffer::map() + */ + [[nodiscard]] Mapping map(Range range = {}) const { return BaseBuffer::map(bytes(range)).template as(); } + + /** + * @brief makes host writes visible to the device + * @param range in elements + * @details calls @ref BaseBuffer::flush() + */ + void flush(Range range = {}) const { BaseBuffer::flush(bytes(range)); } + + /** + * @brief makes device writes visible to the host + * @param range in elements + * @details calls @ref BaseBuffer::invalidate() + */ + void invalidate(Range range = {}) const { BaseBuffer::invalidate(bytes(range)); } + + /** + * @brief copies the data to the buffer + * @param offset in elements + * @details calls @ref BaseBuffer::write() + */ + void write(std::span data, size_t offset = 0) const { + BaseBuffer::write(std::as_bytes(data), offset * sizeof(T)); + } + + /** + * @brief records a copy from the source buffer + * @param region in elements + * @details calls @ref BaseBuffer::copy() + */ + void copy(BaseCmdBuffer const& cmdBuffer, Buffer const& src, BufferCopy region = {}) const { + BaseBuffer::copy( + cmdBuffer, + src, + { + .srcOffset = region.srcOffset * sizeof(T), + .dstOffset = region.dstOffset * sizeof(T), + .size = bytes(region.size), + } + ); + } + + /** + * @brief records a copy from the mip level of the image + * @param dstOffset in elements + * @details calls @ref BaseBuffer::copy(BaseCmdBuffer const&, Image const&, size_t, uint32_t, VkImageLayout) const + */ + void copy( + BaseCmdBuffer const& cmdBuffer, + Image const& src, + size_t dstOffset = 0, + uint32_t mipLevel = 0, + VkImageLayout layout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL + ) const { + BaseBuffer::copy(cmdBuffer, src, dstOffset * sizeof(T), mipLevel, layout); + } + + [[nodiscard]] constexpr size_t elements() const { return size() / sizeof(T); } + +private: + /** + * @param count (VK_WHOLE_SIZE => VK_WHOLE_SIZE) + */ + [[nodiscard]] static constexpr size_t bytes(size_t count) { + return count == VK_WHOLE_SIZE ? VK_WHOLE_SIZE : count * sizeof(T); + } + + [[nodiscard]] static constexpr Range bytes(Range range) { + return {.offset = range.offset * sizeof(T), .size = bytes(range.size)}; + } + +public: + Buffer(Buffer const& other) = delete; + Buffer(Buffer&& other) noexcept = default; + Buffer& operator=(Buffer const& other) = delete; + Buffer& operator=(Buffer&& other) noexcept = default; +}; +} diff --git a/cvk/incl/cvk/data/buffer/buffer_config.hpp b/cvk/incl/cvk/data/buffer/buffer_config.hpp new file mode 100644 index 00000000..27b2413e --- /dev/null +++ b/cvk/incl/cvk/data/buffer/buffer_config.hpp @@ -0,0 +1,69 @@ +#pragma once +#include "cvk/base/queue/queue_family.hpp" +#include "cvk/data/memory/memory_config.hpp" + +#include + +#include + +#include +#include + + +namespace cvk { +/** + * @brief what the buffer is for, who uses it and where its memory lives + */ +struct BufferConfig { + /** + * @pre != 0 + */ + VkBufferUsageFlags usage = 0; + + /** + * @brief sparse binding, protected memory, ... + */ + VkBufferCreateFlags flags = 0; + + /** + * @brief queue families that use the buffer at the same time + * @details size < 2 -> VK_SHARING_MODE_EXCLUSIVE, else VK_SHARING_MODE_CONCURRENT + * @pre unique + */ + std::vector queueFamilies{}; + + /** + * @brief memory of the buffer, VMA_MEMORY_USAGE_AUTO picks the memory type from @ref usage + */ + MemoryConfig memory{}; + + /** + * @param size in bytes + * @attention pQueueFamilyIndices points into @ref queueFamilies + */ + [[nodiscard]] constexpr VkBufferCreateInfo createInfo(size_t size) const { + return VkBufferCreateInfo{ + .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, + .pNext = nullptr, + .flags = flags, + .size = size, + .usage = usage, + .sharingMode = queueFamilies.size() >= 2 ? VK_SHARING_MODE_CONCURRENT : VK_SHARING_MODE_EXCLUSIVE, + .queueFamilyIndexCount = static_cast(queueFamilies.size()), + .pQueueFamilyIndices = queueFamilies.data(), + }; + } + + static constexpr void debug_check(BufferConfig const&); +}; +} + +namespace cvk { +constexpr void BufferConfig::debug_check(BufferConfig const& config) { + CTH_CRITICAL(config.usage == 0, "usage must not be 0") {} + + auto families = config.queueFamilies; + std::ranges::sort(families); + CTH_CRITICAL(std::ranges::adjacent_find(families) != families.end(), "queue families must be unique") {} +} +} diff --git a/cvk/incl/cvk/data/descriptor/descriptor_pool.hpp b/cvk/incl/cvk/data/descriptor/descriptor_pool.hpp new file mode 100644 index 00000000..6eadb0b4 --- /dev/null +++ b/cvk/incl/cvk/data/descriptor/descriptor_pool.hpp @@ -0,0 +1,168 @@ +#pragma once +#include "descriptor_pool_config.hpp" + +#include "cvk/base/device_table.hpp" +#include "cvk/utility/constants.hpp" +#include "cvk/utility/handle/core_handle.hpp" +#include "cvk/utility/types.hpp" + +#include + +#include + +#include + + +namespace cvk { +class Core; + +/** + * @brief VkDescriptorPool wrapper, counts its descriptor sets and its resets + * @details the sets allocate themselves, see @ref DescriptorSet::create() + */ +class DescriptorPool { +public: + using vk_handle_t = VkDescriptorPool; + using Config = DescriptorPoolConfig; + struct State; + + /** + * @brief base constructor + * @pre @ref Core::created() + */ + DescriptorPool(Core const&, Config); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref DescriptorPool(Core const&, Config) + * - @ref wrap() + */ + DescriptorPool(Core const&, Config, State const&); + + /** + * @brief constructs and creates + * @details calls: + * - @ref DescriptorPool(Core const&, Config) + * - @ref create() + */ + explicit DescriptorPool(Core const&, Config, create_t); + + /** + * @note calls @ref optDestroy() + */ + ~DescriptorPool() { optDestroy(); } + + /** + * @brief wraps the state + * @pre the pool was created with the config + * @post @ref created() + * @note calls @ref optDestroy() + */ + void wrap(State const&); + + /** + * @brief creates the pool + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vkCreateDescriptorPool() + */ + void create(); + + /** + * @brief destroys and forgets + * @pre @ref created() + * @pre @ref unused() + */ + 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() + * @pre @ref unused() + */ + State release(); + + /** + * @brief frees every set of the pool + * @pre @ref created() + * @pre the gpu does not use a set of the pool + * @post @ref unused(), @ref DescriptorSet::created() of every set of the pool is false + * @throws cvk::vk_result_exception result of vkResetDescriptorPool() + */ + void reset() const; + + /** + * @brief counts a set of the pool + * @details @ref DescriptorSet::create() and @ref DescriptorSet::wrap() call it, + * code that keeps a released set of the pool may call it too + */ + void add() const; + + /** + * @brief stops to count a set of the pool + * @details @ref DescriptorSet::destroy() and @ref DescriptorSet::release() call it, + * code that frees a counted set itself calls it too + * @pre !@ref unused() + */ + void remove() const; + + static void destroy(DeviceTable table, VkDescriptorPool); + + [[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 no set of the pool exists + */ + [[nodiscard]] constexpr bool unused() const { return _sets == 0; } + + /** + * @return number of @ref reset() calls + */ + [[nodiscard]] constexpr uint64_t resets() const { return _resets; } + + /** + * @return VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT is set + */ + [[nodiscard]] constexpr bool freesSets() const { + return (_config.flags & VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT) != 0; + } + +private: + void forget(); + + core_handle _handle; + Config _config; + + mutable size_t _sets = 0; + mutable uint64_t _resets = 0; + +public: + DescriptorPool(DescriptorPool const& other) = delete; + DescriptorPool(DescriptorPool&& other) noexcept = default; + DescriptorPool& operator=(DescriptorPool const& other) = delete; + DescriptorPool& operator=(DescriptorPool&& other) noexcept = default; + + static constexpr void debug_check(DescriptorPool const&); +}; +} + +namespace cvk { +struct DescriptorPool::State { + vk_not_null vkPool; +}; +} + +namespace cvk { +constexpr void DescriptorPool::debug_check(DescriptorPool const& pool) { + CTH_CRITICAL(!pool.created(), "descriptor pool must be created") {} +} +} diff --git a/cvk/incl/cvk/data/descriptor/descriptor_pool_config.hpp b/cvk/incl/cvk/data/descriptor/descriptor_pool_config.hpp new file mode 100644 index 00000000..7d80a2b6 --- /dev/null +++ b/cvk/incl/cvk/data/descriptor/descriptor_pool_config.hpp @@ -0,0 +1,127 @@ +#pragma once +#include "cvk/render/pipeline/layout/descriptor_set_layout.hpp" + +#include + +#include + +#include +#include +#include +#include + + +namespace cvk { +/** + * @brief set count, descriptor counts and flags of a descriptor pool + */ +struct DescriptorPoolConfig { + /** + * @brief count sets of one layout + */ + struct Sets { + not_null_ptr layout; + uint32_t count = 1; + + /** + * @brief descriptors of the variable count binding per set (0 => its maximum count) + */ + uint32_t variableCount = 0; + }; + + /** + * @brief counts the sets and descriptors of the layouts + * @param flags update after bind layouts add VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT + * @details calls @ref add() per entry + */ + [[nodiscard]] static constexpr DescriptorPoolConfig ForLayouts( + std::initializer_list sets, + VkDescriptorPoolCreateFlags flags = 0 + ) { + DescriptorPoolConfig config{.flags = flags}; + for(auto const& [layout, count, variableCount] : sets) + config.add(*layout, count, variableCount); + return config; + } + + /** + * @pre > 0 + */ + uint32_t maxSets = 0; + + /** + * @brief descriptors per type, for all sets of the pool together + * @pre every count > 0 + */ + std::vector sizes{}; + + /** + * @brief VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT -> @ref DescriptorSet::destroy() frees the set, + * VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT -> sets of update after bind layouts + */ + VkDescriptorPoolCreateFlags flags = 0; + + /** + * @brief adds count sets of the layout to the set count and the descriptor counts + * @param count of sets + * @param variableCount descriptors of the variable count binding per set (0 => its maximum count) + * @details update after bind layout -> adds VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT + * @pre variableCount != 0 -> the layout has a variable count binding with a maximum >= variableCount + */ + constexpr DescriptorPoolConfig& add(DescriptorSetLayout const&, uint32_t count = 1, uint32_t variableCount = 0); + + /** + * @attention pPoolSizes points into @ref sizes + */ + [[nodiscard]] constexpr VkDescriptorPoolCreateInfo createInfo() const { + return VkDescriptorPoolCreateInfo{ + .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO, + .pNext = nullptr, + .flags = flags, + .maxSets = maxSets, + .poolSizeCount = static_cast(sizes.size()), + .pPoolSizes = sizes.data(), + }; + } + + static constexpr void debug_check(DescriptorPoolConfig const&); +}; +} + +namespace cvk { +constexpr DescriptorPoolConfig& DescriptorPoolConfig::add( + DescriptorSetLayout const& layout, + uint32_t count, + uint32_t variableCount +) { + auto const* variable = layout.variableBinding(); + CTH_CRITICAL(variableCount != 0 && variable == nullptr, "a variable count needs a variable count binding") {} + CTH_CRITICAL( + variable != nullptr && variableCount > variable->descriptorCount, + "the variable count must not exceed the maximum count of the binding" + ) {} + + maxSets += count; + if(layout.updateAfterBind()) flags |= VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT; + + for(auto const& binding : layout.bindings()) { + uint32_t const descriptors = &binding == variable && variableCount != 0 ? variableCount + : binding.descriptorCount; + if(descriptors == 0) continue; + + auto const it = std::ranges::find(sizes, binding.descriptorType, &VkDescriptorPoolSize::type); + if(it != sizes.end()) it->descriptorCount += descriptors * count; + else sizes.push_back({binding.descriptorType, descriptors * count}); + } + + return *this; +} + +constexpr void DescriptorPoolConfig::debug_check(DescriptorPoolConfig const& config) { + CTH_CRITICAL(config.maxSets == 0, "a descriptor pool needs at least one set") {} + CTH_CRITICAL( + std::ranges::any_of(config.sizes, [](VkDescriptorPoolSize const& size) { return size.descriptorCount == 0; }), + "descriptor counts must be > 0" + ) {} +} +} diff --git a/cvk/incl/cvk/data/descriptor/descriptor_set.hpp b/cvk/incl/cvk/data/descriptor/descriptor_set.hpp new file mode 100644 index 00000000..8102dd8c --- /dev/null +++ b/cvk/incl/cvk/data/descriptor/descriptor_set.hpp @@ -0,0 +1,206 @@ +#pragma once +#include "descriptor_pool.hpp" +#include "descriptor_write.hpp" + +#include "cvk/base/device_table.hpp" +#include "cvk/render/pipeline/layout/descriptor_set_layout.hpp" +#include "cvk/utility/constants.hpp" +#include "cvk/utility/handle/unique_handle.hpp" +#include "cvk/utility/types.hpp" + +#include + +#include + +#include +#include +#include + + +namespace cvk { +class Core; + +/** + * @brief VkDescriptorSet wrapper, allocated from its pool with its layout + * @details @ref DescriptorPool::reset() frees the set, @ref created() is false after it + */ +class DescriptorSet { +public: + using vk_handle_t = VkDescriptorSet; + struct State; + + /** + * @brief base constructor + * @param variableCount descriptors of the variable count binding (0 => its maximum count) + * @pre @ref DescriptorPool::created(), @ref DescriptorSetLayout::created() + * @pre variableCount != 0 -> the layout has a variable count binding with a maximum >= variableCount + */ + DescriptorSet(DescriptorPool const&, DescriptorSetLayout const&, uint32_t variableCount = 0); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref DescriptorSet(DescriptorPool const&, DescriptorSetLayout const&, uint32_t) + * - @ref wrap() + */ + DescriptorSet(DescriptorPool const&, DescriptorSetLayout const&, uint32_t variableCount, State const&); + + /** + * @brief constructs and creates + * @details calls: + * - @ref DescriptorSet(DescriptorPool const&, DescriptorSetLayout const&, uint32_t) + * - @ref create() + */ + explicit DescriptorSet(DescriptorPool const&, DescriptorSetLayout const&, uint32_t variableCount, create_t); + + /** + * @brief constructs and creates with the maximum variable count + * @details calls @ref DescriptorSet(DescriptorPool const&, DescriptorSetLayout const&, uint32_t, create_t) + */ + explicit DescriptorSet(DescriptorPool const& pool, DescriptorSetLayout const& layout, create_t) : DescriptorSet{ + pool, + layout, + 0, + cvk::create + } {} + + /** + * @note calls @ref optDestroy() + */ + ~DescriptorSet() { optDestroy(); } + + /** + * @brief wraps the state + * @pre the pool allocated the set with the layout and the variable count + * @post @ref created() + * @note calls @ref optDestroy() + */ + void wrap(State const&); + + /** + * @brief allocates the set from the pool + * @pre @ref DescriptorPool::created() + * @pre update after bind layout -> the pool has VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vkAllocateDescriptorSets() + */ + void create(); + + /** + * @brief destroys and forgets + * @details the pool frees sets -> vkFreeDescriptorSets(), else the memory returns with @ref DescriptorPool::reset() + * @pre @ref created() + * @pre the gpu does not use the set + */ + 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 writes the descriptors into the set with one vkUpdateDescriptorSets() + * @pre @ref created() + * @pre the bindings exist, the kind of each write matches the binding type + * @pre the elements lie in the bindings, the buffer ranges in the buffers + * @pre the gpu does not use the written descriptors, except with VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT + * @attention texel buffer, inline uniform block and acceleration structure descriptors are not supported + */ + void write(std::span) const; + + /** + * @brief writes the descriptors into the set, for a braced list: set.write({{0, ubo}, {1, view, sampler}}) + * @details calls @ref write(std::span) const + */ + void write(std::initializer_list writes) const { write(std::span{writes.begin(), writes.size()}); } + + /** + * @brief writes one descriptor into the set: set.write({0, ubo}) + * @details calls @ref write(std::span) const + */ + void write(DescriptorWrite const& descriptorWrite) const { write(std::span{&descriptorWrite, 1}); } + + static void destroy(DeviceTable table, VkDescriptorPool, VkDescriptorSet); + + /** + * @return the set is allocated and its pool was not reset since + */ + [[nodiscard]] constexpr bool created() const { + return static_cast(_handle) && _handle.deleter().resets() == pool().resets(); + } + [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); } + [[nodiscard]] constexpr DescriptorPool const& pool() const { return _handle.deleter().pool(); } + [[nodiscard]] constexpr Core const& core() const { return pool().core(); } + [[nodiscard]] constexpr DescriptorSetLayout const& layout() const { return *_layout; } + + /** + * @return descriptors of the variable count binding, 0 -> its maximum count or no such binding + */ + [[nodiscard]] constexpr uint32_t variableCount() const { return _variableCount; } + +private: + /** + * @brief frees through the pool, nothing if the pool was reset since the allocation + */ + class pool_deleter { + public: + constexpr explicit pool_deleter(DescriptorPool const& pool) : _pool{&pool}, _resets{pool.resets()} {} + + void operator()(VkDescriptorSet) const; + + [[nodiscard]] constexpr DescriptorPool const& pool() const { return *_pool; } + + /** + * @return resets of the pool at the allocation + */ + [[nodiscard]] constexpr uint64_t resets() const { return _resets; } + + private: + not_null_ptr _pool; + uint64_t _resets; + }; + + /** + * @return descriptor count of the binding, the variable count for the variable count binding + */ + [[nodiscard]] constexpr uint32_t capacity(VkDescriptorSetLayoutBinding const&) const; + + void forget(); + + unique_handle _handle; + not_null_ptr _layout; + uint32_t _variableCount; + +public: + DescriptorSet(DescriptorSet const& other) = delete; + DescriptorSet(DescriptorSet&& other) noexcept = default; + DescriptorSet& operator=(DescriptorSet const& other) = delete; + DescriptorSet& operator=(DescriptorSet&& other) noexcept = default; + + static constexpr void debug_check(DescriptorSet const&); +}; +} + +namespace cvk { +struct DescriptorSet::State { + vk_not_null vkSet; +}; +} + +namespace cvk { +constexpr uint32_t DescriptorSet::capacity(VkDescriptorSetLayoutBinding const& binding) const { + return &binding == _layout->variableBinding() && _variableCount != 0 ? _variableCount : binding.descriptorCount; +} + +constexpr void DescriptorSet::debug_check(DescriptorSet const& set) { + CTH_CRITICAL(!set.created(), "descriptor set must be created, a pool reset frees it") {} +} +} diff --git a/cvk/incl/cvk/data/descriptor/descriptor_write.hpp b/cvk/incl/cvk/data/descriptor/descriptor_write.hpp new file mode 100644 index 00000000..55f55c2d --- /dev/null +++ b/cvk/incl/cvk/data/descriptor/descriptor_write.hpp @@ -0,0 +1,86 @@ +#pragma once +#include "cvk/utility/range.hpp" +#include "cvk/utility/types.hpp" + +#include + +#include +#include + + +namespace cvk { +class BaseBuffer; +class DescriptorSet; +class ImageView; +class Sampler; + +/** + * @brief one descriptor of one binding, the constructor decides the kind + * @details the binding type in the layout of the set decides the descriptor type and the default image layout: + * storage image -> VK_IMAGE_LAYOUT_GENERAL, input attachment -> VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR, + * else VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL + */ +class DescriptorWrite { +public: + /** + * @brief buffer descriptor: uniform or storage buffer, dynamic or not + * @param range in bytes + * @param element array element of the binding + */ + constexpr DescriptorWrite(uint32_t binding, BaseBuffer const& buffer, Range range = {}, uint32_t element = 0) : + _binding{binding}, _element{element}, _resource{BufferResource{&buffer, range}} {} + + /** + * @brief image descriptor without sampler: sampled image, storage image, input attachment + * @param element array element of the binding + * @param layout of the view at the gpu access (VK_IMAGE_LAYOUT_MAX_ENUM => the default of the binding type) + */ + constexpr DescriptorWrite( + uint32_t binding, + ImageView const& view, + uint32_t element = 0, + VkImageLayout layout = VK_IMAGE_LAYOUT_MAX_ENUM + ) : _binding{binding}, _element{element}, _resource{ImageResource{&view, nullptr, layout}} {} + + /** + * @brief combined image sampler descriptor + * @param element array element of the binding + * @param layout of the view at the gpu access (VK_IMAGE_LAYOUT_MAX_ENUM => the default of the binding type) + */ + constexpr DescriptorWrite( + uint32_t binding, + ImageView const& view, + Sampler const& sampler, + uint32_t element = 0, + VkImageLayout layout = VK_IMAGE_LAYOUT_MAX_ENUM + ) : _binding{binding}, _element{element}, _resource{ImageResource{&view, &sampler, layout}} {} + + /** + * @brief sampler descriptor + * @param element array element of the binding + */ + constexpr DescriptorWrite(uint32_t binding, Sampler const& sampler, uint32_t element = 0) : + _binding{binding}, _element{element}, _resource{ImageResource{nullptr, &sampler, VK_IMAGE_LAYOUT_UNDEFINED}} {} + +private: + struct BufferResource { + not_null_ptr buffer; + Range range; + }; + + /** + * @brief view nullptr -> sampler descriptor, sampler nullptr -> no sampler + */ + struct ImageResource { + ImageView const* view; + Sampler const* sampler; + VkImageLayout layout; + }; + + uint32_t _binding; + uint32_t _element; + std::variant _resource; + + friend DescriptorSet; +}; +} diff --git a/cvk/incl/cvk/data/img/image.hpp b/cvk/incl/cvk/data/img/image.hpp new file mode 100644 index 00000000..b29456f2 --- /dev/null +++ b/cvk/incl/cvk/data/img/image.hpp @@ -0,0 +1,246 @@ +#pragma once +#include "image_config.hpp" +#include "image_format.hpp" + +#include "cvk/base/device_table.hpp" +#include "cvk/data/memory/memory.hpp" +#include "cvk/utility/handle/core_handle.hpp" +#include "cvk/utility/types.hpp" + +#include + +#include + +#include +#include +#include +#include +#include + + +namespace cvk { +class BaseBuffer; +class BaseCmdBuffer; +class Core; + +/** + * @brief VkImage wrapper, owns the image and its memory + * @details @ref create() creates both with vmaCreateImage(), + * swapchain images are wrapped without memory, + * the image holds no layout, the commands take it as a value + */ +class Image { +public: + using vk_handle_t = VkImage; + using Config = ImageConfig; + struct State; + + /** + * @brief base constructor + * @pre @ref Core::created() + */ + Image(Core const&, Config const&); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref Image(Core const&, Config const&) + * - @ref wrap() + */ + Image(Core const&, Config const&, State const&); + + /** + * @brief constructs and creates + * @details calls: + * - @ref Image(Core const&, Config const&) + * - @ref create() + */ + Image(Core const&, Config const&, VkExtent2D extent); + + /** + * @note calls @ref optDestroy() + */ + ~Image() { optDestroy(); } + + /** + * @brief wraps the state + * @details std::nullopt @ref State::memory -> no own memory, @ref destroy() only destroys the image + * @pre the image is bound to its memory + * @pre the image was created with the config + * @post @ref created() + * @note calls @ref optDestroy() + */ + void wrap(State const&); + + /** + * @brief creates the image and its memory + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vmaCreateImage() + */ + void create(VkExtent2D extent); + + /** + * @brief destroys the image, then its memory, and forgets + * @pre @ref created() + * @pre the gpu does not use the image + */ + void destroy(); + + /** + * @brief if @ref created() -> calls @ref destroy() + */ + void optDestroy() { if(created()) destroy(); } + + /** + * @brief releases ownership of the image and its memory, returns the state and forgets + * @pre @ref created() + */ + State release(); + + /** + * @brief records a copy from the buffer into every layer of the mip level + * @param srcOffset in bytes, the layers follow each other in the buffer + * @param layout of the mip level, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL or VK_IMAGE_LAYOUT_GENERAL + * @pre @ref created() and src created + * @pre @ref BaseCmdBuffer::recording() + * @pre mipLevel < @ref mipLevels() + * @pre @ref aspectMask() has one bit + * @pre src usage has VK_BUFFER_USAGE_TRANSFER_SRC_BIT, the usage has VK_IMAGE_USAGE_TRANSFER_DST_BIT + */ + void copy( + BaseCmdBuffer const&, + BaseBuffer const& src, + size_t srcOffset = 0, + uint32_t mipLevel = 0, + VkImageLayout layout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL + ) const; + + /** + * @brief records a copy from every layer of the mip level into the buffer + * @param dstOffset in bytes, the layers follow each other in the buffer + * @param layout of the mip level, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL or VK_IMAGE_LAYOUT_GENERAL + * @pre @ref created() and dst created + * @pre @ref BaseCmdBuffer::recording() + * @pre mipLevel < @ref mipLevels() + * @pre @ref aspectMask() has one bit + * @pre the usage has VK_IMAGE_USAGE_TRANSFER_SRC_BIT, dst usage has VK_BUFFER_USAGE_TRANSFER_DST_BIT + * @pre the mip level fits into dst after dstOffset + */ + void copyTo( + BaseCmdBuffer const&, + BaseBuffer const& dst, + size_t dstOffset = 0, + uint32_t mipLevel = 0, + VkImageLayout layout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL + ) const; + + /** + * @brief records blits that fill each mip level from the level before it, every layer + * @param firstMipLevel first level to fill, the blits read from firstMipLevel - 1 + * @param mipLevels (VK_REMAINING_MIP_LEVELS => to the last mip level) + * @param filter depth formats need VK_FILTER_NEAREST + * @pre @ref created(), @ref BaseCmdBuffer::recording() + * @pre 0 < firstMipLevel, the levels lie in the image + * @pre the usage has VK_IMAGE_USAGE_TRANSFER_SRC_BIT and VK_IMAGE_USAGE_TRANSFER_DST_BIT + * @pre the format supports blits and the filter + * @pre the levels from firstMipLevel - 1 to the last filled level are in VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL + * @post the same levels are in VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL + */ + void blitMipLevels( + BaseCmdBuffer const&, + uint32_t firstMipLevel = 1, + uint32_t mipLevels = VK_REMAINING_MIP_LEVELS, + VkFilter filter = VK_FILTER_LINEAR + ) const; + + static void destroy(DeviceTable table, VkImage); + + [[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; } + + [[nodiscard]] constexpr VkExtent2D extent() const { return _extent; } + [[nodiscard]] constexpr VkFormat format() const { return _config.format; } + /** + * @return @ref Config::aspectMask, 0 -> the aspects of the format + */ + [[nodiscard]] constexpr VkImageAspectFlags aspectMask() const { + return _config.aspectMask != 0 ? _config.aspectMask : format_aspects(_config.format); + } + [[nodiscard]] constexpr uint32_t mipLevels() const { return _config.mipLevels; } + [[nodiscard]] constexpr uint32_t arrayLayers() const { return _config.arrayLayers; } + + /** + * @return nullptr -> the image does not own its memory + */ + [[nodiscard]] constexpr Memory const* memory() const { + return _memory.has_value() && _memory->created() ? &*_memory : nullptr; + } + +private: + /** + * @brief checks the preconditions that both copy directions share + * @return region of the whole mip level at the buffer offset + */ + [[nodiscard]] VkBufferImageCopy2 bufferCopy( + BaseCmdBuffer const&, + BaseBuffer const& buffer, + size_t bufferOffset, + uint32_t mipLevel + ) const; + + void forget(); + + // declared before the memory: move assignment destroys the old image before its memory + core_handle _handle; + std::optional _memory = std::nullopt; + Config _config; + VkExtent2D _extent{}; + +public: + Image(Image const& other) = delete; + Image(Image&& other) noexcept = default; + Image& operator=(Image const& other) = delete; + Image& operator=(Image&& other) noexcept = default; + + static constexpr void debug_check(Image const&); +}; + +/** + * @return mip levels down to 1x1 + * @pre width > 0, height > 0 + */ +[[nodiscard]] constexpr uint32_t mip_level_count(VkExtent2D extent) { + return static_cast(std::bit_width(std::max(extent.width, extent.height))); +} + +/** + * @return extent of the mip level, at least 1x1, also beyond @ref mip_level_count() + */ +[[nodiscard]] constexpr VkExtent2D mip_extent(VkExtent2D extent, uint32_t mipLevel) { + // a shift by the bit count or more is undefined + auto const shrink = [mipLevel](uint32_t size) { + return mipLevel < std::numeric_limits::digits ? std::max(size >> mipLevel, 1u) : 1u; + }; + return VkExtent2D{shrink(extent.width), shrink(extent.height)}; +} +} + +namespace cvk { +struct Image::State { + vk_not_null vkImage; + VkExtent2D extent; + /** + * @brief std::nullopt -> the image does not own its memory, e.g. swapchain images + */ + std::optional memory = std::nullopt; +}; +} + +namespace cvk { +constexpr void Image::debug_check(Image const& image) { + CTH_CRITICAL(!image.created(), "image must be created") {} +} +} diff --git a/cvk/incl/cvk/data/img/image_config.hpp b/cvk/incl/cvk/data/img/image_config.hpp new file mode 100644 index 00000000..345e7c6f --- /dev/null +++ b/cvk/incl/cvk/data/img/image_config.hpp @@ -0,0 +1,152 @@ +#pragma once +#include "image_format.hpp" + +#include "cvk/base/queue/queue_family.hpp" +#include "cvk/data/memory/memory_config.hpp" + +#include + +#include + +#include +#include + + +namespace cvk { +/** + * @brief 2D image: format, usage, mip levels, layers, who uses it and where its memory lives + */ +struct ImageConfig { + /** + * @brief 0 -> the aspects of @ref format, see @ref format_aspects() + * @pre the aspects of @ref format contain it + */ + VkImageAspectFlags aspectMask = 0; + + /** + * @pre != VK_FORMAT_UNDEFINED + */ + VkFormat format = VK_FORMAT_UNDEFINED; + + /** + * @pre != 0 + */ + VkImageUsageFlags usage = 0; + + /** + * @brief cube compatible, mutable format, sparse, ... + */ + VkImageCreateFlags flags = 0; + + VkImageTiling tiling = VK_IMAGE_TILING_OPTIMAL; + + /** + * @pre >= 1 + */ + uint32_t mipLevels = 1; + + /** + * @pre >= 1 + * @pre VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT -> a multiple of 6, one layer per face + */ + uint32_t arrayLayers = 1; + + VkSampleCountFlagBits samples = VK_SAMPLE_COUNT_1_BIT; + + /** + * @pre VK_IMAGE_LAYOUT_UNDEFINED or VK_IMAGE_LAYOUT_PREINITIALIZED + */ + VkImageLayout initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + + /** + * @brief queue families that use the image at the same time + * @details size < 2 -> VK_SHARING_MODE_EXCLUSIVE, else VK_SHARING_MODE_CONCURRENT + * @pre unique + */ + std::vector queueFamilies{}; + + /** + * @brief memory of the image + */ + MemoryConfig memory{}; + + /** + * @brief depth attachment, depth aspect only + */ + [[nodiscard]] static constexpr ImageConfig DepthBuffer(VkFormat depthFormat, VkSampleCountFlagBits samples) { + return ImageConfig{ + .aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT, + .format = depthFormat, + .usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, + .samples = samples, + }; + } + + /** + * @brief sampled cube map, the six faces are the layers + * @param mipLevels > 1 -> transfer src usage for @ref Image::blitMipLevels() + * @details the faces: +x, -x, +y, -y, +z, -z + */ + [[nodiscard]] static constexpr ImageConfig CubeMap(VkFormat format, uint32_t mipLevels = 1) { + return ImageConfig{ + .format = format, + .usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT + | (mipLevels > 1 ? VK_IMAGE_USAGE_TRANSFER_SRC_BIT : VkImageUsageFlags{0}), + .flags = VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT, + .mipLevels = mipLevels, + .arrayLayers = 6, + }; + } + + /** + * @attention pQueueFamilyIndices points into @ref queueFamilies + */ + [[nodiscard]] constexpr VkImageCreateInfo createInfo(VkExtent2D extent) const { + return VkImageCreateInfo{ + .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, + .pNext = nullptr, + .flags = flags, + .imageType = VK_IMAGE_TYPE_2D, + .format = format, + .extent = {extent.width, extent.height, 1}, + .mipLevels = mipLevels, + .arrayLayers = arrayLayers, + .samples = samples, + .tiling = tiling, + .usage = usage, + .sharingMode = queueFamilies.size() >= 2 ? VK_SHARING_MODE_CONCURRENT : VK_SHARING_MODE_EXCLUSIVE, + .queueFamilyIndexCount = static_cast(queueFamilies.size()), + .pQueueFamilyIndices = queueFamilies.data(), + .initialLayout = initialLayout, + }; + } + + static constexpr void debug_check(ImageConfig const&); +}; +} + +namespace cvk { +constexpr void ImageConfig::debug_check(ImageConfig const& config) { + CTH_CRITICAL(config.format == VK_FORMAT_UNDEFINED, "format must not be undefined") {} + CTH_CRITICAL( + (config.aspectMask & ~format_aspects(config.format)) != 0, + "the aspects of the format must contain the aspect mask" + ) {} + CTH_CRITICAL(config.usage == 0, "usage must not be 0") {} + CTH_CRITICAL(config.mipLevels == 0, "at least one mip level is required") {} + CTH_CRITICAL(config.arrayLayers == 0, "at least one layer is required") {} + CTH_CRITICAL( + (config.flags & VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT) != 0 && config.arrayLayers % 6 != 0, + "a cube compatible image needs a multiple of 6 layers, got {}", + config.arrayLayers + ) {} + CTH_CRITICAL( + config.initialLayout != VK_IMAGE_LAYOUT_UNDEFINED && config.initialLayout != VK_IMAGE_LAYOUT_PREINITIALIZED, + "initial layout must be undefined or preinitialized" + ) {} + + auto families = config.queueFamilies; + std::ranges::sort(families); + CTH_CRITICAL(std::ranges::adjacent_find(families) != families.end(), "queue families must be unique") {} +} +} diff --git a/cvk/incl/cvk/data/img/image_format.hpp b/cvk/incl/cvk/data/img/image_format.hpp new file mode 100644 index 00000000..58e669ac --- /dev/null +++ b/cvk/incl/cvk/data/img/image_format.hpp @@ -0,0 +1,22 @@ +#pragma once +#include + + +namespace cvk { +/** + * @return the aspects of the format: depth, stencil, depth and stencil, else color + * @details multi planar formats -> color, cvk does not support their plane aspects + */ +[[nodiscard]] constexpr VkImageAspectFlags format_aspects(VkFormat format) { + switch(format) { + case VK_FORMAT_D16_UNORM: + case VK_FORMAT_X8_D24_UNORM_PACK32: + case VK_FORMAT_D32_SFLOAT: return VK_IMAGE_ASPECT_DEPTH_BIT; + case VK_FORMAT_S8_UINT: return VK_IMAGE_ASPECT_STENCIL_BIT; + case VK_FORMAT_D16_UNORM_S8_UINT: + case VK_FORMAT_D24_UNORM_S8_UINT: + case VK_FORMAT_D32_SFLOAT_S8_UINT: return VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT; + default: return VK_IMAGE_ASPECT_COLOR_BIT; + } +} +} diff --git a/cvk/incl/cvk/data/img/image_view.hpp b/cvk/incl/cvk/data/img/image_view.hpp new file mode 100644 index 00000000..56db7972 --- /dev/null +++ b/cvk/incl/cvk/data/img/image_view.hpp @@ -0,0 +1,151 @@ +#pragma once +#include "image_view_config.hpp" + +#include "cvk/base/device_table.hpp" +#include "cvk/data/img/image.hpp" +#include "cvk/utility/constants.hpp" +#include "cvk/utility/handle/core_handle.hpp" +#include "cvk/utility/types.hpp" + +#include + +#include + + +namespace cvk { +/** + * @brief VkImageView wrapper, view of a mip and layer range of its image + */ +class ImageView { +public: + using vk_handle_t = VkImageView; + using Config = ImageViewConfig; + struct State; + + /** + * @brief base constructor + * @details the core comes from the image + */ + ImageView(Image const&, Config const&); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref ImageView(Image const&, Config const&) + * - @ref wrap() + */ + ImageView(Image const&, Config const&, State const&); + + /** + * @brief constructs and creates + * @details calls: + * - @ref ImageView(Image const&, Config const&) + * - @ref create() + */ + explicit ImageView(Image const&, Config const&, create_t); + + /** + * @note calls @ref optDestroy() + */ + ~ImageView() { optDestroy(); } + + /** + * @brief wraps the state + * @pre the view was created for the image with the config + * @post @ref created() + * @note calls @ref optDestroy() + */ + void wrap(State const&); + + /** + * @brief creates the view + * @pre @ref Image::created() + * @pre the mip range and the layer range lie in the image + * @pre the layers fit the view type + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vkCreateImageView() + */ + void create(); + + /** + * @brief destroys and forgets + * @pre @ref created() + * @pre the gpu does not use the view + */ + 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(DeviceTable table, VkImageView); + + [[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 Image const& image() const { return *_image; } + [[nodiscard]] constexpr Config const& config() const { return _config; } + + /** + * @return the subresource range of the view in the image + */ + [[nodiscard]] constexpr VkImageSubresourceRange range() const { return _config.range(_image->aspectMask()); } + + /** + * @return the layers of the view + */ + [[nodiscard]] constexpr uint32_t arrayLayers() const { + return _config.arrayLayers == VK_REMAINING_ARRAY_LAYERS ? _image->arrayLayers() - _config.baseArrayLayer + : _config.arrayLayers; + } + + /** + * @return @ref Config::viewType, VK_IMAGE_VIEW_TYPE_MAX_ENUM -> inferred: + * one layer -> 2D, cube compatible image with 6 layers -> cube, a multiple of 6 -> cube array, else 2D array + */ + [[nodiscard]] constexpr VkImageViewType viewType() const { + if(_config.viewType != VK_IMAGE_VIEW_TYPE_MAX_ENUM) return _config.viewType; + + auto const layers = arrayLayers(); + if(layers == 1) return VK_IMAGE_VIEW_TYPE_2D; + if((_image->config().flags & VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT) == 0 || layers % 6 != 0) + return VK_IMAGE_VIEW_TYPE_2D_ARRAY; + return layers == 6 ? VK_IMAGE_VIEW_TYPE_CUBE : VK_IMAGE_VIEW_TYPE_CUBE_ARRAY; + } + +private: + void forget(); + + core_handle _handle; + not_null_ptr _image; + Config _config; + +public: + ImageView(ImageView const& other) = delete; + ImageView(ImageView&& other) noexcept = default; + ImageView& operator=(ImageView const& other) = delete; + ImageView& operator=(ImageView&& other) noexcept = default; + + static constexpr void debug_check(ImageView const&); +}; +} + +namespace cvk { +struct ImageView::State { + vk_not_null vkImageView; +}; +} + +namespace cvk { +constexpr void ImageView::debug_check(ImageView const& view) { + CTH_CRITICAL(!view.created(), "image view must be created") {} +} +} diff --git a/cvk/incl/cvk/data/img/image_view_config.hpp b/cvk/incl/cvk/data/img/image_view_config.hpp new file mode 100644 index 00000000..ec07d419 --- /dev/null +++ b/cvk/incl/cvk/data/img/image_view_config.hpp @@ -0,0 +1,47 @@ +#pragma once +#include + + +namespace cvk { +/** + * @brief type, mip range, layer range and aspect of an image view + */ +struct ImageViewConfig { + /** + * @brief VK_IMAGE_VIEW_TYPE_MAX_ENUM -> inferred, see @ref ImageView::viewType() + */ + VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_MAX_ENUM; + + uint32_t baseMipLevel = 0; + + /** + * @brief VK_REMAINING_MIP_LEVELS -> to the last mip level of the image + */ + uint32_t mipLevels = VK_REMAINING_MIP_LEVELS; + + /** + * @brief 0 -> the aspect mask of the image + */ + VkImageAspectFlags aspectMask = 0; + + uint32_t baseArrayLayer = 0; + + /** + * @brief VK_REMAINING_ARRAY_LAYERS -> to the last layer of the image + */ + uint32_t arrayLayers = VK_REMAINING_ARRAY_LAYERS; + + /** + * @param imageAspectMask aspect mask of the image + */ + [[nodiscard]] constexpr VkImageSubresourceRange range(VkImageAspectFlags imageAspectMask) const { + return VkImageSubresourceRange{ + .aspectMask = aspectMask != 0 ? aspectMask : imageAspectMask, + .baseMipLevel = baseMipLevel, + .levelCount = mipLevels, + .baseArrayLayer = baseArrayLayer, + .layerCount = arrayLayers, + }; + } +}; +} diff --git a/cvk/incl/cvk/data/img/sampler.hpp b/cvk/incl/cvk/data/img/sampler.hpp new file mode 100644 index 00000000..49be3996 --- /dev/null +++ b/cvk/incl/cvk/data/img/sampler.hpp @@ -0,0 +1,125 @@ +#pragma once +#include "sampler_config.hpp" + +#include "cvk/base/device_table.hpp" +#include "cvk/utility/constants.hpp" +#include "cvk/utility/handle/core_handle.hpp" +#include "cvk/utility/types.hpp" + +#include + +#include + + +namespace cvk { +class Core; + +/** + * @brief VkSampler wrapper + */ +class Sampler { +public: + using vk_handle_t = VkSampler; + using Config = SamplerConfig; + struct State; + + /** + * @brief base constructor + * @details clamps @ref Config::maxAnisotropy to maxSamplerAnisotropy of the device, warns if it clamps + * @pre @ref Core::created() + */ + Sampler(Core const&, Config const&); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref Sampler(Core const&, Config const&) + * - @ref wrap() + */ + Sampler(Core const&, Config const&, State const&); + + /** + * @brief constructs and creates + * @details calls: + * - @ref Sampler(Core const&, Config const&) + * - @ref create() + */ + explicit Sampler(Core const&, Config const&, create_t); + + /** + * @note calls @ref optDestroy() + */ + ~Sampler() { optDestroy(); } + + /** + * @brief wraps the state + * @pre the sampler was created with the config + * @post @ref created() + * @note calls @ref optDestroy() + */ + void wrap(State const&); + + /** + * @brief creates the sampler + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vkCreateSampler() + */ + void create(); + + /** + * @brief destroys and forgets + * @pre @ref created() + * @pre the gpu does not use the sampler + */ + 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(DeviceTable table, VkSampler); + + [[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(); } + + /** + * @return the config, with the clamped anisotropy + */ + [[nodiscard]] constexpr Config const& config() const { return _config; } + +private: + void forget(); + + core_handle _handle; + Config _config; + +public: + Sampler(Sampler const& other) = delete; + Sampler(Sampler&& other) noexcept = default; + Sampler& operator=(Sampler const& other) = delete; + Sampler& operator=(Sampler&& other) noexcept = default; + + static constexpr void debug_check(Sampler const&); +}; +} + +namespace cvk { +struct Sampler::State { + vk_not_null vkSampler; +}; +} + +namespace cvk { +constexpr void Sampler::debug_check(Sampler const& sampler) { + CTH_CRITICAL(!sampler.created(), "sampler must be created") {} +} +} diff --git a/cvk/incl/cvk/data/img/sampler_config.hpp b/cvk/incl/cvk/data/img/sampler_config.hpp new file mode 100644 index 00000000..1a6f7f26 --- /dev/null +++ b/cvk/incl/cvk/data/img/sampler_config.hpp @@ -0,0 +1,63 @@ +#pragma once +#include + +#include + + +namespace cvk { +/** + * @brief sampler state, defaults: linear filtering, repeat, 16x anisotropy + */ +struct SamplerConfig { + VkFilter magFilter = VK_FILTER_LINEAR; + VkFilter minFilter = VK_FILTER_LINEAR; + VkSamplerMipmapMode mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; + + VkSamplerAddressMode addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT; + VkSamplerAddressMode addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT; + VkSamplerAddressMode addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT; + + float mipLodBias = 0.f; + float minLod = 0.f; + /** + * @brief VK_LOD_CLAMP_NONE -> no clamp + */ + float maxLod = VK_LOD_CLAMP_NONE; + + /** + * @brief 0 -> no anisotropic filtering + */ + float maxAnisotropy = 16.f; + + /** + * @brief std::nullopt -> no comparison + */ + std::optional compareOp = std::nullopt; + + VkBorderColor borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK; + VkBool32 unnormalizedCoordinates = VK_FALSE; + + [[nodiscard]] constexpr VkSamplerCreateInfo createInfo() const { + return VkSamplerCreateInfo{ + .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .magFilter = magFilter, + .minFilter = minFilter, + .mipmapMode = mipmapMode, + .addressModeU = addressModeU, + .addressModeV = addressModeV, + .addressModeW = addressModeW, + .mipLodBias = mipLodBias, + .anisotropyEnable = maxAnisotropy != 0.f ? VK_TRUE : VK_FALSE, + .maxAnisotropy = maxAnisotropy, + .compareEnable = compareOp.has_value() ? VK_TRUE : VK_FALSE, + .compareOp = compareOp.value_or(VK_COMPARE_OP_NEVER), + .minLod = minLod, + .maxLod = maxLod, + .borderColor = borderColor, + .unnormalizedCoordinates = unnormalizedCoordinates, + }; + } +}; +} diff --git a/cvk/incl/cvk/data/memory/mapping.hpp b/cvk/incl/cvk/data/memory/mapping.hpp new file mode 100644 index 00000000..d50bf6f9 --- /dev/null +++ b/cvk/incl/cvk/data/memory/mapping.hpp @@ -0,0 +1,57 @@ +#pragma once +#include "cvk/utility/types.hpp" + +#include +#include +#include + + +namespace cvk { +class Memory; + +/** + * @brief mapped range of a memory, the destructor unmaps + * @tparam T element, std::byte for the bytes of a memory + * @details move constructible, not copyable, not assignable, a moved from mapping does not unmap + */ +template +class Mapping { +public: + /** + * @brief takes over one map of the memory, @ref Memory::map() constructs it + * @pre the range lies in the mapped memory + */ + constexpr Mapping(Memory const& memory, std::span range) : _memory{&memory}, _range{range} {} + + /** + * @brief unmaps the memory once + */ + ~Mapping(); + + /** + * @brief the range as elements of U, takes over the map + * @details starts the lifetime of the elements, keeps their values + * @pre the size is a multiple of sizeof(U), the data is aligned for U + */ + template requires std::same_as + [[nodiscard]] Mapping as() &&; + + [[nodiscard]] constexpr std::span span() const { return _range; } + [[nodiscard]] constexpr T* data() const { return _range.data(); } + [[nodiscard]] constexpr size_t size() const { return _range.size(); } + [[nodiscard]] constexpr bool empty() const { return _range.empty(); } + [[nodiscard]] constexpr auto begin() const { return _range.begin(); } + [[nodiscard]] constexpr auto end() const { return _range.end(); } + [[nodiscard]] constexpr T& operator[](size_t index) const { return _range[index]; } + +private: + move_ptr _memory; + std::span _range; + +public: + Mapping(Mapping const& other) = delete; + Mapping(Mapping&& other) noexcept = default; + Mapping& operator=(Mapping const& other) = delete; + Mapping& operator=(Mapping&& other) = delete; +}; +} diff --git a/cvk/incl/cvk/data/memory/memory.hpp b/cvk/incl/cvk/data/memory/memory.hpp new file mode 100644 index 00000000..90eea394 --- /dev/null +++ b/cvk/incl/cvk/data/memory/memory.hpp @@ -0,0 +1,229 @@ +#pragma once +#include "mapping.hpp" +#include "memory_config.hpp" + +#include "cvk/utility/handle/vma_handle.hpp" +#include "cvk/utility/range.hpp" +#include "cvk/utility/types.hpp" + +#include +#include + +#include + +#include +#include +#include +#include + + +namespace cvk { +class Core; + +/** + * @brief VmaAllocation wrapper, a range of device memory + * @details VMA places many allocations in one VkDeviceMemory, @ref info() returns the range + * @attention calls with one memory are not thread safe + */ +class Memory { +public: + using vk_handle_t = VmaAllocation; + using Config = MemoryConfig; + struct State; + + /** + * @brief base constructor + * @pre @ref Core::created() + */ + Memory(Core const&, Config const&); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref Memory(Core const&, Config const&) + * - @ref wrap() + */ + Memory(Core const&, Config const&, State const&); + + /** + * @brief constructs and creates + * @details calls: + * - @ref Memory(Core const&, Config const&) + * - @ref create() + */ + Memory(Core const&, Config const&, VkMemoryRequirements const&); + + /** + * @note calls @ref optDestroy() + */ + ~Memory() { optDestroy(); } + + /** + * @brief wraps the state + * @pre the allocator of the core created the allocation + * @post @ref created() + * @note calls @ref optDestroy() + */ + void wrap(State const&); + + /** + * @brief allocates memory for the requirements + * @pre !@ref Config::autoUsage(), VMA_MEMORY_USAGE_AUTO* needs a buffer or an image + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vmaAllocateMemory() + */ + void create(VkMemoryRequirements const&); + + /** + * @brief frees the memory and forgets + * @pre @ref created() + * @pre the gpu does not use the memory + */ + 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 maps the range of the memory + * @param range in bytes + * @return the mapped range, its destructor unmaps + * @pre @ref created() + * @pre @ref properties() has VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT + * @pre the range lies in the memory + * @note VMA counts the maps, a map while a mapping lives returns the same memory + * @throws cvk::vk_result_exception result of vmaMapMemory() + */ + [[nodiscard]] Mapping map(Range range = {}) const; + + /** + * @brief makes host writes visible to the device + * @param range in bytes + * @details host coherent memory -> does nothing + * @pre @ref created() + * @pre the memory is mapped: a @ref Mapping lives or VMA_ALLOCATION_CREATE_MAPPED_BIT + * @throws cvk::vk_result_exception result of vmaFlushAllocation() + */ + void flush(Range range = {}) const; + + /** + * @brief makes device writes visible to the host + * @param range in bytes + * @details host coherent memory -> does nothing + * @pre @ref created() + * @pre the memory is mapped: a @ref Mapping lives or VMA_ALLOCATION_CREATE_MAPPED_BIT + * @throws cvk::vk_result_exception result of vmaInvalidateAllocation() + */ + void invalidate(Range range = {}) const; + + /** + * @brief copies the data to the memory + * @details maps, copies, flushes and unmaps + * @param offset in bytes + * @pre @ref created() + * @pre @ref properties() has VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT + * @pre the data fits into the memory after the offset + * @throws cvk::vk_result_exception result of vmaMapMemory() + * @throws cvk::vk_result_exception result of vmaFlushAllocation() + */ + void write(std::span data, size_t offset = 0) const; + + static void destroy(VmaAllocator, VmaAllocation); + + [[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 the VkDeviceMemory, the offset and the size of the range + * @pre @ref created() + */ + [[nodiscard]] VmaAllocationInfo info() const; + + /** + * @return the property flags of the memory type that VMA picked + * @pre @ref created() + */ + [[nodiscard]] VkMemoryPropertyFlags properties() const; + + /** + * @pre @ref created() + */ + [[nodiscard]] size_t size() const { return info().size; } + + /** + * @return the whole memory if mapped, not mapped -> empty + * @details a living @ref Mapping or VMA_ALLOCATION_CREATE_MAPPED_BIT maps it + * @pre @ref created() + */ + [[nodiscard]] std::span mapped() const; + +private: + template + friend class Mapping; + + /** + * @brief unmaps the memory once, the destructor of a @ref Mapping calls it + */ + void unmap() const; + + void forget(); + + [[nodiscard]] VmaAllocator allocator() const; + + vma_handle _handle; + Config _config; + +public: + Memory(Memory const& other) = delete; + Memory(Memory&& other) noexcept = default; + Memory& operator=(Memory const& other) = delete; + Memory& operator=(Memory&& other) noexcept = default; + + static constexpr void debug_check(Memory const&); +}; +} + +namespace cvk { +struct Memory::State { + vk_not_null vmaAllocation; +}; +} + +namespace cvk { +template +Mapping::~Mapping() { + if(_memory != nullptr) _memory->unmap(); +} + +template +template requires std::same_as +Mapping Mapping::as() && { + CTH_CRITICAL(_range.size() % sizeof(U) != 0, "the size must be a multiple of the element size") {} + + size_t const count = _range.size() / sizeof(U); + auto const& memory = *_memory.release(); +#ifdef __cpp_lib_start_lifetime_as + return Mapping{memory, {std::start_lifetime_as_array(_range.data(), count), count}}; +#else + //TODO remove when the MSVC STL has start_lifetime_as on clang (microsoft/STL GH-6169) + return Mapping{memory, {reinterpret_cast(_range.data()), count}}; +#endif +} +} + +namespace cvk { +constexpr void Memory::debug_check(Memory const& memory) { + CTH_CRITICAL(!memory.created(), "memory must be created") {} +} +} diff --git a/cvk/incl/cvk/data/memory/memory_config.hpp b/cvk/incl/cvk/data/memory/memory_config.hpp new file mode 100644 index 00000000..0b9fe3e6 --- /dev/null +++ b/cvk/incl/cvk/data/memory/memory_config.hpp @@ -0,0 +1,56 @@ +#pragma once +#include +#include + + +namespace cvk { +/** + * @brief what the memory is for, VMA picks the memory type from it + */ +struct MemoryConfig { + /** + * @brief VMA_ALLOCATION_CREATE_* flags, e.g. host access, mapped, dedicated memory + */ + VmaAllocationCreateFlags flags = 0; + + /** + * @brief VMA_MEMORY_USAGE_AUTO* -> only for buffers and images that VMA creates with the memory + */ + VmaMemoryUsage usage = VMA_MEMORY_USAGE_AUTO; + + /** + * @brief the memory type must have these flags + */ + VkMemoryPropertyFlags requiredFlags = 0; + + /** + * @brief VMA prefers memory types with these flags + */ + VkMemoryPropertyFlags preferredFlags = 0; + + /** + * @return create info with the members of the config, no pool + */ + [[nodiscard]] constexpr VmaAllocationCreateInfo createInfo() const { + return VmaAllocationCreateInfo{ + .flags = flags, + .usage = usage, + .requiredFlags = requiredFlags, + .preferredFlags = preferredFlags, + .memoryTypeBits = 0, + .pool = VK_NULL_HANDLE, + .pUserData = nullptr, + .priority = 0.0f, + .minAlignment = 0, + }; + } + + /** + * @return @ref usage is VMA_MEMORY_USAGE_AUTO* + */ + [[nodiscard]] constexpr bool autoUsage() const { + return usage == VMA_MEMORY_USAGE_AUTO || usage == VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE || + usage == VMA_MEMORY_USAGE_AUTO_PREFER_HOST; + } +}; +} diff --git a/cvk/incl/cvk/debug/debug_messenger.hpp b/cvk/incl/cvk/debug/debug_messenger.hpp new file mode 100644 index 00000000..cbc2d921 --- /dev/null +++ b/cvk/incl/cvk/debug/debug_messenger.hpp @@ -0,0 +1,156 @@ +#pragma once +#include "cvk/utility/constants.hpp" +#include "cvk/utility/handle/instance_handle.hpp" +#include "cvk/utility/types.hpp" + +#include + +#include +#include + + +namespace cvk { +class Instance; + +/** + * @brief writes the message to cth::log + * @return VK_FALSE, the call that caused the message continues + */ +VKAPI_ATTR VkBool32 VKAPI_CALL default_debug_callback( + VkDebugUtilsMessageSeverityFlagBitsEXT, + VkDebugUtilsMessageTypeFlagsEXT, + VkDebugUtilsMessengerCallbackDataEXT const*, + void* userData +); + +/** + * @brief VkDebugUtilsMessengerEXT wrapper + */ +class DebugMessenger { +public: + using vk_handle_t = VkDebugUtilsMessengerEXT; + struct Config; + struct State; + using callback_t = PFN_vkDebugUtilsMessengerCallbackEXT; + + /** + * @brief base constructor + * @pre @ref Instance::created() + */ + DebugMessenger(Instance const&, Config); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref DebugMessenger(Instance const&, Config) + * - @ref wrap() + */ + DebugMessenger(Instance const&, Config, State const&); + + /** + * @brief constructs and creates + * @details calls: + * - @ref DebugMessenger(Instance const&, Config) + * - @ref create() + */ + explicit DebugMessenger(Instance const&, Config, create_t); + + /** + * @note calls @ref optDestroy() + */ + ~DebugMessenger() { optDestroy(); } + + /** + * @brief wraps the state + * @pre the instance created the messenger + * @post @ref created() + * @note calls @ref optDestroy() + */ + void wrap(State const&); + + /** + * @brief creates the messenger + * @pre the instance enables VK_EXT_debug_utils + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vkCreateDebugUtilsMessengerEXT() + */ + void create(); + + /** + * @brief destroys and forgets + * @pre @ref created() + */ + void destroy(); + + /** + * @brief if @ref created() -> calls @ref destroy() + */ + void optDestroy() { if(created()) destroy(); } + + static void destroy(vk_not_null, VkDebugUtilsMessengerEXT vkMessenger); + + /** + * @brief releases ownership, returns the state and forgets + * @pre @ref created() + */ + State release(); + + struct Config { + /** + * @pre != nullptr + */ + callback_t callback = &default_debug_callback; + VkDebugUtilsMessageSeverityFlagsEXT messageSeverities = constants::DEBUG_MESSAGE_SEVERITY; + VkDebugUtilsMessageTypeFlagsEXT messageTypes = constants::DEBUG_MESSAGE_TYPE; + + [[nodiscard]] constexpr VkDebugUtilsMessengerCreateInfoEXT createInfo() const { + return VkDebugUtilsMessengerCreateInfoEXT{ + .sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT, + .pNext = nullptr, + .flags = 0, + .messageSeverity = messageSeverities, + .messageType = messageTypes, + .pfnUserCallback = callback, + .pUserData = nullptr, + }; + } + + static constexpr void debug_check(Config const&); + }; + + [[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; } + +private: + void forget(); + + Config _config; + instance_handle _handle; + +public: + DebugMessenger(DebugMessenger const& other) = delete; + DebugMessenger(DebugMessenger&& other) noexcept = default; + DebugMessenger& operator=(DebugMessenger const& other) = delete; + DebugMessenger& operator=(DebugMessenger&& other) noexcept = default; + + static constexpr void debug_check(DebugMessenger const&); +}; +} + +namespace cvk { +struct DebugMessenger::State { + vk_not_null vkMessenger; +}; +} + +namespace cvk { +constexpr void DebugMessenger::debug_check(DebugMessenger const& debugMessenger) { + CTH_CRITICAL(!debugMessenger.created(), "debug messenger must be created") {} +} + +constexpr void DebugMessenger::Config::debug_check(Config const& config) { + CTH_CRITICAL(config.callback == nullptr, "callback must not be nullptr") {} +} +} diff --git a/cvk/incl/cvk/render/cmd/cmd_buffer.hpp b/cvk/incl/cvk/render/cmd/cmd_buffer.hpp new file mode 100644 index 00000000..2e3eac9b --- /dev/null +++ b/cvk/incl/cvk/render/cmd/cmd_buffer.hpp @@ -0,0 +1,541 @@ +#pragma once +#include "cmd_pool.hpp" +#include "rendering_info.hpp" + +#include "cvk/data/buffer/buffer.hpp" + +#include "cvk/base/device_table.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 +#include +#include +#include + + +namespace cvk { +class ComputePipeline; +class Core; +class DescriptorSet; +class GraphicsPipeline; +class PipelineLayout; + +/** + * @brief contiguous range of trivially copyable elements + */ +template +concept push_constant_range = std::ranges::contiguous_range + && cth::mta::range_over_cpt; + +/** + * @brief @ref push_constant_range or trivially copyable value that is no pointer + */ +template +concept push_constant_data = push_constant_range || (cth::mta::trivially_copyable && !std::is_pointer_v); + +/** + * @brief VkCommandBuffer wrapper, allocated from its pool, base of @ref PrimaryCmdBuffer and @ref SecondaryCmdBuffer + * @details the derived classes add the begin of the recording and their commands, + * the destructor is protected: delete through the derived class + */ +class BaseCmdBuffer { +public: + using vk_handle_t = VkCommandBuffer; + struct State; + + /** + * @brief wraps the state + * @pre the pool of the command buffer allocated it with the level + * @pre the command buffer is not recording + * @post @ref created() + * @note calls @ref optDestroy() + */ + void wrap(State const&); + + /** + * @brief allocates the command buffer from the pool + * @pre @ref CmdPool::created() + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vkAllocateCommandBuffers() + */ + void create(); + + /** + * @brief frees and forgets + * @pre @ref created() + * @pre the gpu does not use the command buffer + */ + 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 ends the recording + * @pre @ref recording() + * @throws cvk::vk_result_exception result of vkEndCommandBuffer() + */ + void end(); + + /** + * @brief resets the command buffer to the initial state + * @param flags VK_COMMAND_BUFFER_RESET_RELEASE_RESOURCES_BIT -> returns the memory to the pool + * @pre @ref created() + * @pre the pool has VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT + * @throws cvk::vk_result_exception result of vkResetCommandBuffer() + */ + void reset(VkCommandBufferResetFlags flags = 0); + + /** + * @brief begins a dynamic rendering + * @details std::nullopt @ref RenderingInfo::area -> the extent that fits every attachment + * @pre @ref recording(), outside of a rendering + * @pre the attachment views are created and have one mip level + * @pre the attachment layouts are the layouts of the views + * @pre the attachment layouts are attachment layouts (not undefined, transfer, shader read or present) + * @pre color attachments have color formats, the depth and stencil attachments depth and stencil formats + * @pre @ref AttachmentMapping::debug_check() passes for the color attachments + * @post @ref rendering() is the info with the area + * @note inline contents -> sets viewport 0 and scissor 0 to the area, depth 0 to 1 + * @note a mapping other than the default -> sets it after the begin + */ + void beginRendering(RenderingInfo const&); + + /** + * @brief ends the dynamic rendering + * @pre @ref rendering() + */ + void endRendering(); + + /** + * @brief maps the attachments of the rendering to the fragment shader outputs and input attachments + * @details the mapping holds until the next call or the end of the rendering + * @pre @ref rendering() + * @pre @ref AttachmentMapping::debug_check() passes for the color attachments of the rendering + * @post @ref rendering() has the mapping + */ + void setAttachmentMapping(AttachmentMapping const&); + + /** + * @brief sets viewport 0 + * @pre @ref recording() + */ + void setViewport(VkViewport const&) const; + + /** + * @brief sets scissor 0 + * @pre @ref recording() + */ + void setScissor(VkRect2D const&) const; + + /** + * @brief binds the pipeline to the graphics bind point + * @details @ref rendering() -> sets the attachment mapping of the pipeline if the current one differs + * @pre @ref recording(), @ref GraphicsPipeline::created() + * @pre a secondary inside the rendering of a primary -> the attachment mapping of the pipeline == the inherited one + * @note outside of a rendering: sets no attachment mapping, the draws need the default of @ref beginRendering() + */ + void bindPipeline(GraphicsPipeline const&); + + /** + * @brief binds the pipeline to the compute bind point + * @pre @ref recording(), @ref ComputePipeline::created() + */ + void bindPipeline(ComputePipeline const&) const; + + /** + * @brief binds the set to the set number of the pipeline layout + * @param setIndex set number in the pipeline layout + * @param dynamicOffsets one per dynamic buffer descriptor of the set + * @pre @ref recording(), @ref DescriptorSet::created() + * @pre the set layout of setIndex in the pipeline layout is compatible with the layout of the set + */ + void bindDescriptorSet( + DescriptorSet const&, + PipelineLayout const&, + uint32_t setIndex, + VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS, + std::span dynamicOffsets = {} + ) const; + + /** + * @brief records push constants + * @param data @ref push_constant_range -> the bytes of the elements, else the bytes of the value + * @param offset in bytes + * @pre @ref recording(), @ref PipelineLayout::created() + * @pre data has bytes, offset and the byte count are multiples of 4 + * @pre the push constant ranges of the layout cover the bytes for the stages + */ + template + void pushConstants( + PipelineLayout const& layout, + VkShaderStageFlags stages, + T const& data, + uint32_t offset = 0 + ) const { + if constexpr(push_constant_range) pushConstantBytes(layout, stages, std::as_bytes(std::span{data}), offset); + else pushConstantBytes(layout, stages, std::as_bytes(std::span{&data, 1}), offset); + } + + /** + * @brief binds the buffer to the vertex input binding + * @param offset in bytes + * @pre @ref recording(), buffer created with VK_BUFFER_USAGE_VERTEX_BUFFER_BIT + */ + void bindVertexBuffer(uint32_t binding, BaseBuffer const&, size_t offset = 0) const; + + /** + * @brief binds the index buffer + * @param offset in bytes + * @pre @ref recording(), buffer created with VK_BUFFER_USAGE_INDEX_BUFFER_BIT + * @pre offset is a multiple of the index size + */ + void bindIndexBuffer(BaseBuffer const&, VkIndexType, size_t offset = 0) const; + + /** + * @brief binds the index buffer, the index type follows from T + * @param offset in bytes + * @details calls @ref bindIndexBuffer(BaseBuffer const&, VkIndexType, size_t) + */ + template + void bindIndexBuffer(Buffer const& buffer, size_t offset = 0) const { + bindIndexBuffer(static_cast(buffer), index_type, offset); + } + + /** + * @pre @ref rendering() or a secondary inside the rendering of a primary + * @pre @ref rendering() -> without VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT + */ + void draw( + uint32_t vertexCount, + uint32_t instanceCount = 1, + uint32_t firstVertex = 0, + uint32_t firstInstance = 0 + ) const; + + /** + * @pre the preconditions of @ref draw(), an index buffer is bound + */ + void drawIndexed( + uint32_t indexCount, + uint32_t instanceCount = 1, + uint32_t firstIndex = 0, + int32_t vertexOffset = 0, + uint32_t firstInstance = 0 + ) const; + + /** + * @brief draws with the VkDrawIndirectCommands in the buffer + * @param offset in bytes + * @pre the preconditions of @ref draw(), buffer created with VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT + * @pre the commands lie in the buffer + */ + void drawIndirect( + BaseBuffer const&, + size_t offset = 0, + uint32_t drawCount = 1, + uint32_t stride = sizeof(VkDrawIndirectCommand) + ) const; + + /** + * @brief draws with the VkDrawIndexedIndirectCommands in the buffer + * @param offset in bytes + * @pre the preconditions of @ref draw(), buffer created with VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT + * @pre an index buffer is bound + * @pre the commands lie in the buffer + */ + void drawIndexedIndirect( + BaseBuffer const&, + size_t offset = 0, + uint32_t drawCount = 1, + uint32_t stride = sizeof(VkDrawIndexedIndirectCommand) + ) const; + + /** + * @brief records a dispatch of the bound compute pipeline + * @param groupCountX work groups in x, the same for y and z + * @pre @ref recording(), outside of a rendering + */ + void dispatch(uint32_t groupCountX, uint32_t groupCountY = 1, uint32_t groupCountZ = 1) const; + + /** + * @brief dispatches with the VkDispatchIndirectCommand in the buffer + * @param offset in bytes + * @pre the preconditions of @ref dispatch(), buffer created with VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT + * @pre the command lies in the buffer + */ + void dispatchIndirect(BaseBuffer const&, size_t offset = 0) const; + + static void destroy(DeviceTable table, VkCommandPool, VkCommandBuffer); + + /** + * @brief frees the command buffers at once + */ + static void destroy(DeviceTable table, VkCommandPool, std::span vkBuffers); + + [[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 CmdPool const& pool() const { return _handle.deleter().pool(); } + [[nodiscard]] constexpr VkCommandBufferLevel level() const { return _level; } + + /** + * @return recording, a reset of the pool ends it, a moved from command buffer does not record + */ + [[nodiscard]] constexpr bool recording() const { + return created() && _recording && _beginResets == pool().resets(); + } + + /** + * @return info of the rendering the command buffer began, with the area and the current attachment mapping + * @return nullptr -> no rendering of the command buffer + */ + [[nodiscard]] constexpr RenderingInfo const* rendering() const { + return recording() && _rendering.has_value() ? &*_rendering : nullptr; + } + +protected: + /** + * @brief base constructor + * @pre @ref CmdPool::created() + */ + BaseCmdBuffer(CmdPool const&, VkCommandBufferLevel); + + /** + * @note calls @ref optDestroy() + */ + ~BaseCmdBuffer() { optDestroy(); } + + /** + * @brief begins the recording + * @pre @ref created() + * @pre !@ref recording() + * @post @ref recording() + * @throws cvk::vk_result_exception result of vkBeginCommandBuffer() + */ + void begin(VkCommandBufferBeginInfo const&); + +private: + /** + * @brief frees the command buffer through its pool, the pool stops to count it + */ + class pool_deleter { + public: + constexpr explicit pool_deleter(CmdPool const& pool) : _pool{&pool} {} + + void operator()(VkCommandBuffer) const; + + [[nodiscard]] constexpr CmdPool const& pool() const { return *_pool; } + [[nodiscard]] constexpr Core const& core() const { return _pool->core(); } + + private: + not_null_ptr _pool; + }; + + /** + * @brief records the attachment mapping of @ref rendering() + */ + void recordAttachmentMapping() const; + + /** + * @brief records the bytes as push constants + */ + void pushConstantBytes( + PipelineLayout const&, + VkShaderStageFlags stages, + std::span data, + uint32_t offset + ) const; + + void forget(); + + unique_handle _handle; + VkCommandBufferLevel _level; + bool _recording = false; + uint64_t _beginResets = 0; + std::optional _rendering = std::nullopt; + +public: + BaseCmdBuffer(BaseCmdBuffer const& other) = delete; + BaseCmdBuffer(BaseCmdBuffer&& other) noexcept = default; + BaseCmdBuffer& operator=(BaseCmdBuffer const& other) = delete; + BaseCmdBuffer& operator=(BaseCmdBuffer&& other) noexcept = default; + + static constexpr void debug_check(BaseCmdBuffer const&); +}; +} + +namespace cvk { +struct BaseCmdBuffer::State { + vk_not_null vkBuffer; +}; +} + +namespace cvk { +constexpr void BaseCmdBuffer::debug_check(BaseCmdBuffer const& buffer) { + CTH_CRITICAL(!buffer.created(), "command buffer must be created") {} +} +} + + +namespace cvk { +class SecondaryCmdBuffer; + +/** + * @brief primary command buffer, queues submit it + */ +class PrimaryCmdBuffer : public BaseCmdBuffer { +public: + /** + * @brief base constructor + * @pre @ref CmdPool::created() + */ + explicit PrimaryCmdBuffer(CmdPool const&); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref PrimaryCmdBuffer(CmdPool const&) + * - @ref wrap() + */ + PrimaryCmdBuffer(CmdPool const&, State const&); + + /** + * @brief constructs and creates + * @details calls: + * - @ref PrimaryCmdBuffer(CmdPool const&) + * - @ref create() + */ + explicit PrimaryCmdBuffer(CmdPool const&, create_t); + + /** + * @brief begins the recording + * @param usage VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, ... + * @pre @ref created() + * @pre !@ref recording() + * @post @ref recording() + * @throws cvk::vk_result_exception result of vkBeginCommandBuffer() + */ + void begin(VkCommandBufferUsageFlags usage = 0); + + /** + * @brief records the secondary command buffers + * @pre @ref recording() + * @pre the secondaries are created and not recording + * @pre @ref rendering() -> VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT + * @pre @ref rendering() -> the secondaries continue it with the current attachment mapping + * @pre outside of a rendering -> the secondaries continue no rendering + */ + void execute(std::span secondaries) const; + + /** + * @brief records the secondary command buffers, the secondaries can lie anywhere + * @pre the preconditions of @ref execute(std::span) const + * @pre no pointer is nullptr + */ + void execute(std::span secondaries) const; + + /** + * @brief records the secondary command buffers of a braced list: execute({&a, &b}) + * @details calls @ref execute(std::span) const, + * without this overload {&a, &b} binds to std::span as iterator pair -> records [&a, &b) + */ + void execute(std::initializer_list secondaries) const; + + /** + * @brief records the secondary command buffer + * @details calls @ref execute(std::span) const + */ + void execute(SecondaryCmdBuffer const& secondary) const; + + /** + * @brief what a secondary inherits from the rendering + * @details the formats and samples of the attachments, the flags without + * VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT, the current attachment mapping + * @pre @ref rendering() + */ + [[nodiscard]] RenderingInheritance inheritance() const; + +private: + /** + * @brief records vkCmdExecuteCommands() with the command buffers + * @pre @ref recording() + * @pre @ref rendering() -> VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT + */ + void recordExecute(std::span) const; +}; +} + + +namespace cvk { +/** + * @brief secondary command buffer, primary command buffers execute it + */ +class SecondaryCmdBuffer : public BaseCmdBuffer { +public: + using Inheritance = RenderingInheritance; + + /** + * @brief base constructor + * @pre @ref CmdPool::created() + */ + explicit SecondaryCmdBuffer(CmdPool const&); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref SecondaryCmdBuffer(CmdPool const&) + * - @ref wrap() + */ + SecondaryCmdBuffer(CmdPool const&, State const&); + + /** + * @brief constructs and creates + * @details calls: + * - @ref SecondaryCmdBuffer(CmdPool const&) + * - @ref create() + */ + explicit SecondaryCmdBuffer(CmdPool const&, create_t); + + /** + * @brief begins the recording for use outside of a rendering + * @param usage VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, ... + * @pre @ref created() + * @pre !@ref recording() + * @post @ref recording() + * @throws cvk::vk_result_exception result of vkBeginCommandBuffer() + */ + void begin(VkCommandBufferUsageFlags usage = 0); + + /** + * @brief begins the recording for use inside the rendering of a primary + * @param usage VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, ... + * @pre @ref created() + * @pre !@ref recording() + * @pre @ref AttachmentMapping::debug_check() passes for the color formats + * @post @ref recording() + * @note adds VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT + * @throws cvk::vk_result_exception result of vkBeginCommandBuffer() + */ + void begin(Inheritance const&, VkCommandBufferUsageFlags usage = 0); +}; +} diff --git a/cvk/incl/cvk/render/cmd/cmd_pool.hpp b/cvk/incl/cvk/render/cmd/cmd_pool.hpp new file mode 100644 index 00000000..592722e2 --- /dev/null +++ b/cvk/incl/cvk/render/cmd/cmd_pool.hpp @@ -0,0 +1,161 @@ +#pragma once +#include "cmd_pool_config.hpp" + +#include "cvk/base/device_table.hpp" +#include "cvk/utility/constants.hpp" +#include "cvk/utility/handle/core_handle.hpp" +#include "cvk/utility/types.hpp" + +#include + +#include + + +namespace cvk { +class Core; + +/** + * @brief VkCommandPool wrapper, counts its command buffers and its resets + * @details the pool and its command buffers are used by one thread at a time + */ +class CmdPool { +public: + using vk_handle_t = VkCommandPool; + using Config = CmdPoolConfig; + struct State; + + /** + * @brief base constructor + * @pre @ref Core::created() + */ + CmdPool(Core const&, Config const&); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref CmdPool(Core const&, Config const&) + * - @ref wrap() + */ + CmdPool(Core const&, Config const&, State const&); + + /** + * @brief constructs and creates + * @details calls: + * - @ref CmdPool(Core const&, Config const&) + * - @ref create() + */ + explicit CmdPool(Core const&, Config const&, create_t); + + /** + * @note calls @ref optDestroy() + */ + ~CmdPool() { optDestroy(); } + + /** + * @brief wraps the state + * @pre the pool was created with the config + * @post @ref created() + * @note calls @ref optDestroy() + */ + void wrap(State const&); + + /** + * @brief creates the pool + * @pre the config has a queue family (!= VK_QUEUE_FAMILY_IGNORED) + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vkCreateCommandPool() + */ + void create(); + + /** + * @brief destroys and forgets + * @pre @ref created() + * @pre @ref unused() + */ + 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() + * @pre @ref unused() + */ + State release(); + + /** + * @brief resets every command buffer of the pool to the initial state + * @param flags VK_COMMAND_POOL_RESET_RELEASE_RESOURCES_BIT -> returns the memory of the pool to the system + * @pre @ref created() + * @pre no command buffer of the pool is pending + * @post @ref BaseCmdBuffer::recording() of every command buffer of the pool is false + * @throws cvk::vk_result_exception result of vkResetCommandPool() + */ + void reset(VkCommandPoolResetFlags flags = 0) const; + + /** + * @brief counts a command buffer of the pool + * @details @ref BaseCmdBuffer::create() and @ref BaseCmdBuffer::wrap() call it, + * code that keeps a released command buffer of the pool may call it too + */ + void add() const; + + /** + * @brief stops to count a command buffer of the pool + * @details @ref BaseCmdBuffer::destroy() and @ref BaseCmdBuffer::release() call it, + * code that frees a counted command buffer itself calls it too + * @pre !@ref unused() + */ + void remove() const; + + static void destroy(DeviceTable table, VkCommandPool); + + [[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 no command buffer of the pool exists + */ + [[nodiscard]] constexpr bool unused() const { return _buffers == 0; } + + /** + * @return number of @ref reset() calls + */ + [[nodiscard]] constexpr uint64_t resets() const { return _resets; } + +private: + void forget(); + + core_handle _handle; + Config _config; + + mutable size_t _buffers = 0; + mutable uint64_t _resets = 0; + +public: + CmdPool(CmdPool const& other) = delete; + CmdPool(CmdPool&& other) noexcept = default; + CmdPool& operator=(CmdPool const& other) = delete; + CmdPool& operator=(CmdPool&& other) noexcept = default; + + static constexpr void debug_check(CmdPool const&); +}; +} + +namespace cvk { +struct CmdPool::State { + vk_not_null vkPool; +}; +} + +namespace cvk { +constexpr void CmdPool::debug_check(CmdPool const& pool) { + CTH_CRITICAL(!pool.created(), "command pool must be created") {} +} +} diff --git a/cvk/incl/cvk/render/cmd/cmd_pool_config.hpp b/cvk/incl/cvk/render/cmd/cmd_pool_config.hpp new file mode 100644 index 00000000..15024c38 --- /dev/null +++ b/cvk/incl/cvk/render/cmd/cmd_pool_config.hpp @@ -0,0 +1,35 @@ +#pragma once +#include "cvk/base/queue/queue_family.hpp" + +#include + + +namespace cvk { +/** + * @brief queue family and flags of a command pool + */ +struct CmdPoolConfig { + static constexpr VkCommandPoolCreateFlags DEFAULT_FLAGS = VK_COMMAND_POOL_CREATE_TRANSIENT_BIT + | VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; + + /** + * @brief the family of the queues that the command buffers are submitted to + * @details VK_QUEUE_FAMILY_IGNORED is not allowed at create + */ + queue_family_index_t queueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + + /** + * @brief VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT -> @ref BaseCmdBuffer::reset() is allowed + */ + VkCommandPoolCreateFlags flags = DEFAULT_FLAGS; + + [[nodiscard]] constexpr VkCommandPoolCreateInfo createInfo() const { + return VkCommandPoolCreateInfo{ + .sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO, + .pNext = nullptr, + .flags = flags, + .queueFamilyIndex = queueFamilyIndex, + }; + } +}; +} diff --git a/cvk/incl/cvk/render/cmd/rendering_info.hpp b/cvk/incl/cvk/render/cmd/rendering_info.hpp new file mode 100644 index 00000000..f48336df --- /dev/null +++ b/cvk/incl/cvk/render/cmd/rendering_info.hpp @@ -0,0 +1,225 @@ +#pragma once +#include "cvk/utility/types.hpp" + +#include + +#include + +#include +#include +#include +#include +#include + + +namespace cvk { +class ImageView; + +/** + * @brief maps the attachments of a rendering to the fragment shader outputs and input attachments + * @details a draw requires the same mapping in the bound pipeline and in the command buffer + */ +struct AttachmentMapping { + /** + * @brief fragment shader output location per color attachment (VK_ATTACHMENT_UNUSED => no output) + * @details empty -> color attachment i writes location i + */ + std::vector locations{}; + + /** + * @brief input attachment index per color attachment (VK_ATTACHMENT_UNUSED => no input attachment) + * @details empty -> color attachment i is input attachment i + */ + std::vector colorInputIndices{}; + + /** + * @brief std::nullopt -> the depth attachment is the input attachment without index + */ + std::optional depthInputIndex = std::nullopt; + + /** + * @brief std::nullopt -> the stencil attachment is the input attachment without index + */ + std::optional stencilInputIndex = std::nullopt; + + /** + * @param colorAttachments of the rendering + * @pre @ref debug_check() passes for colorAttachments + * @attention pColorAttachmentLocations points into @ref locations + */ + [[nodiscard]] constexpr VkRenderingAttachmentLocationInfoKHR locationInfo(uint32_t colorAttachments) const { + return VkRenderingAttachmentLocationInfoKHR{ + .sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_LOCATION_INFO_KHR, + .pNext = nullptr, + .colorAttachmentCount = colorAttachments, + .pColorAttachmentLocations = locations.empty() ? nullptr : locations.data(), + }; + } + + /** + * @param colorAttachments of the rendering + * @pre @ref debug_check() passes for colorAttachments + * @attention the pointers point into this mapping + */ + [[nodiscard]] constexpr VkRenderingInputAttachmentIndexInfoKHR inputIndexInfo(uint32_t colorAttachments) const { + return VkRenderingInputAttachmentIndexInfoKHR{ + .sType = VK_STRUCTURE_TYPE_RENDERING_INPUT_ATTACHMENT_INDEX_INFO_KHR, + .pNext = nullptr, + .colorAttachmentCount = colorAttachments, + .pColorAttachmentInputIndices = colorInputIndices.empty() ? nullptr : colorInputIndices.data(), + .pDepthInputAttachmentIndex = depthInputIndex.has_value() ? &*depthInputIndex : nullptr, + .pStencilInputAttachmentIndex = stencilInputIndex.has_value() ? &*stencilInputIndex : nullptr, + }; + } + + /** + * @brief same mapping, empty == the default + */ + [[nodiscard]] constexpr bool operator==(AttachmentMapping const&) const; + + /** + * @param colorAttachments of the rendering + */ + static constexpr void debug_check(AttachmentMapping const&, size_t colorAttachments); +}; + +/** + * @brief formats, samples, view mask, flags and attachment mapping of a rendering, what a secondary inherits from it + */ +struct RenderingInheritance { + std::vector colorFormats{}; + VkFormat depthFormat = VK_FORMAT_UNDEFINED; + VkFormat stencilFormat = VK_FORMAT_UNDEFINED; + VkSampleCountFlagBits samples = VK_SAMPLE_COUNT_1_BIT; + uint32_t viewMask = 0; + VkRenderingFlags flags = 0; + + /** + * @brief mapping at the begin of the secondary + * @pre == the mapping of the primary at the execute + */ + AttachmentMapping attachmentMapping{}; + + /** + * @attention pColorAttachmentFormats points into @ref colorFormats + */ + [[nodiscard]] constexpr VkCommandBufferInheritanceRenderingInfo renderingInfo() const { + return VkCommandBufferInheritanceRenderingInfo{ + .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_INHERITANCE_RENDERING_INFO, + .pNext = nullptr, + .flags = flags, + .viewMask = viewMask, + .colorAttachmentCount = static_cast(colorFormats.size()), + .pColorAttachmentFormats = colorFormats.data(), + .depthAttachmentFormat = depthFormat, + .stencilAttachmentFormat = stencilFormat, + .rasterizationSamples = samples, + }; + } +}; + +/** + * @brief attachment of a rendering + */ +struct RenderingAttachment { + /** + * @pre one mip level + */ + not_null_ptr view; + + /** + * @brief layout of the view during the rendering, VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL fits every aspect + */ + VkImageLayout layout = VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL; + + VkAttachmentLoadOp loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + VkAttachmentStoreOp storeOp = VK_ATTACHMENT_STORE_OP_STORE; + + /** + * @brief VK_ATTACHMENT_LOAD_OP_CLEAR -> the clear value + */ + VkClearValue clearValue{}; + + /** + * @brief nullptr -> no resolve + */ + ImageView const* resolveView = nullptr; + + /** + * @brief layout of @ref resolveView during the rendering + */ + VkImageLayout resolveLayout = VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL; + + /** + * @pre != VK_RESOLVE_MODE_NONE if @ref resolveView + */ + VkResolveModeFlagBits resolveMode = VK_RESOLVE_MODE_NONE; +}; + +/** + * @brief attachments, area, flags and attachment mapping of a dynamic rendering + */ +struct RenderingInfo { + std::vector colorAttachments{}; + std::optional depthAttachment = std::nullopt; + std::optional stencilAttachment = std::nullopt; + + /** + * @brief std::nullopt -> the extent that fits every attachment + */ + std::optional area = std::nullopt; + + /** + * @brief layers that the rendering writes, viewMask != 0 -> ignored + * @pre the attachment views have the layers + */ + uint32_t layerCount = 1; + + /** + * @brief multiview, bit i -> view i renders into layer i + * @pre the attachment views have a layer per view + */ + uint32_t viewMask = 0; + + /** + * @brief VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT -> secondaries record the rendering + */ + VkRenderingFlags flags = 0; + + /** + * @brief mapping at the begin, @ref BaseCmdBuffer::setAttachmentMapping() changes it inside the rendering + */ + AttachmentMapping attachmentMapping{}; +}; +} + +namespace cvk { +constexpr bool AttachmentMapping::operator==(AttachmentMapping const& other) const { + auto const same = [](std::vector const& left, std::vector const& right) { + // the default maps attachment i to i + auto const identity = [](std::vector const& indices) { + return std::ranges::equal(indices, std::views::iota(uint32_t{0}, static_cast(indices.size()))); + }; + if(left.empty()) return identity(right); + if(right.empty()) return identity(left); + return left == right; + }; + return same(locations, other.locations) && same(colorInputIndices, other.colorInputIndices) + && depthInputIndex == other.depthInputIndex && stencilInputIndex == other.stencilInputIndex; +} + +constexpr void AttachmentMapping::debug_check(AttachmentMapping const& mapping, size_t colorAttachments) { + CTH_CRITICAL( + !mapping.locations.empty() && mapping.locations.size() != colorAttachments, + "one location per color attachment, got {} for {}", + mapping.locations.size(), + colorAttachments + ) {} + CTH_CRITICAL( + !mapping.colorInputIndices.empty() && mapping.colorInputIndices.size() != colorAttachments, + "one input attachment index per color attachment, got {} for {}", + mapping.colorInputIndices.size(), + colorAttachments + ) {} +} +} diff --git a/cvk/incl/cvk/render/pipeline/base_pipeline.hpp b/cvk/incl/cvk/render/pipeline/base_pipeline.hpp new file mode 100644 index 00000000..60508423 --- /dev/null +++ b/cvk/incl/cvk/render/pipeline/base_pipeline.hpp @@ -0,0 +1,117 @@ +#pragma once +#include "cvk/base/device_table.hpp" +#include "cvk/utility/handle/core_handle.hpp" +#include "cvk/utility/types.hpp" + +#include + +#include + + +namespace cvk { +class Core; +class PipelineLayout; + +/** + * @brief VkPipeline wrapper, base of @ref GraphicsPipeline and @ref ComputePipeline + * @details the derived classes build the create info, the base creates the pipeline, + * the layout is a parent of the pipeline: it outlives the pipeline, + * the destructor is protected: delete through the derived class + */ +class BasePipeline { +public: + using vk_handle_t = VkPipeline; + struct State; + + /** + * @brief wraps the state + * @pre the pipeline was created with the layout and the config of the derived class + * @post @ref created() + * @note calls @ref optDestroy() + */ + void wrap(State const&); + + /** + * @brief destroys and forgets + * @pre @ref created() + * @pre the gpu does not use the pipeline + */ + 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(DeviceTable table, VkPipeline); + + [[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 PipelineLayout const& layout() const { return *_layout; } + [[nodiscard]] constexpr VkPipelineBindPoint bindPoint() const { return _bindPoint; } + +protected: + /** + * @brief base constructor, keeps the layout, the core comes from the layout + * @pre @ref Core::created() of the layout + */ + BasePipeline(PipelineLayout const&, VkPipelineBindPoint); + + /** + * @note calls @ref optDestroy() + */ + ~BasePipeline() { optDestroy(); } + + /** + * @brief creates the pipeline with the info + * @pre the layout of the info is @ref layout() + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vkCreateGraphicsPipelines() + */ + void create(VkGraphicsPipelineCreateInfo const&); + + /** + * @brief creates the pipeline with the info + * @pre the layout of the info is @ref layout() + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vkCreateComputePipelines() + */ + void create(VkComputePipelineCreateInfo const&); + +private: + void forget(); + + core_handle _handle; + not_null_ptr _layout; + VkPipelineBindPoint _bindPoint; + +public: + BasePipeline(BasePipeline const& other) = delete; + BasePipeline(BasePipeline&& other) noexcept = default; + BasePipeline& operator=(BasePipeline const& other) = delete; + BasePipeline& operator=(BasePipeline&& other) noexcept = default; + + static constexpr void debug_check(BasePipeline const&); +}; +} + +namespace cvk { +struct BasePipeline::State { + vk_not_null vkPipeline; +}; +} + +namespace cvk { +constexpr void BasePipeline::debug_check(BasePipeline const& pipeline) { + CTH_CRITICAL(!pipeline.created(), "pipeline must be created") {} +} +} diff --git a/cvk/incl/cvk/render/pipeline/compute_pipeline.hpp b/cvk/incl/cvk/render/pipeline/compute_pipeline.hpp new file mode 100644 index 00000000..04d0d171 --- /dev/null +++ b/cvk/incl/cvk/render/pipeline/compute_pipeline.hpp @@ -0,0 +1,72 @@ +#pragma once +#include "base_pipeline.hpp" + +#include "cvk/utility/constants.hpp" + +#include + + +namespace cvk { +class PipelineLayout; + +/** + * @brief shader stage and flags of a compute pipeline + */ +struct ComputePipelineConfig { + /** + * @brief stage of a compute shader, see @ref Shader::stageInfo() + * @attention the specialization info and the entry point name outlive @ref ComputePipeline::create() + */ + VkPipelineShaderStageCreateInfo stage{}; + + /** + * @brief VK_PIPELINE_CREATE_DISPATCH_BASE_BIT, ... + */ + VkPipelineCreateFlags flags = 0; +}; + +/** + * @brief compute VkPipeline wrapper + */ +class ComputePipeline : public BasePipeline { +public: + using Config = ComputePipelineConfig; + + /** + * @brief base constructor, keeps the layout + * @pre @ref Core::created() of the layout + */ + ComputePipeline(PipelineLayout const&, Config const&); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref ComputePipeline(PipelineLayout const&, Config const&) + * - @ref wrap() + */ + ComputePipeline(PipelineLayout const&, Config const&, State const&); + + /** + * @brief constructs and creates + * @details calls: + * - @ref ComputePipeline(PipelineLayout const&, Config const&) + * - @ref create() + */ + explicit ComputePipeline(PipelineLayout const&, Config const&, create_t); + + /** + * @brief creates the pipeline + * @pre the stage of the config is a compute shader + * @pre the layout and the shader module of the config are not destroyed + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vkCreateComputePipelines() + */ + void create(); + + [[nodiscard]] constexpr Config const& config() const { return _config; } + +private: + Config _config; +}; +} diff --git a/cvk/incl/cvk/render/pipeline/graphics_pipeline.hpp b/cvk/incl/cvk/render/pipeline/graphics_pipeline.hpp new file mode 100644 index 00000000..e879695a --- /dev/null +++ b/cvk/incl/cvk/render/pipeline/graphics_pipeline.hpp @@ -0,0 +1,77 @@ +#pragma once +#include "base_pipeline.hpp" +#include "graphics_pipeline_config.hpp" + +#include "cvk/utility/constants.hpp" + +#include + + +namespace cvk { +class PipelineLayout; + +/** + * @brief graphics VkPipeline wrapper for dynamic rendering + */ +class GraphicsPipeline : public BasePipeline { +public: + using Config = GraphicsPipelineConfig; + + /** + * @brief base constructor, keeps the layout + * @pre @ref Core::created() of the layout + */ + GraphicsPipeline(PipelineLayout const&, Config); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref GraphicsPipeline(PipelineLayout const&, Config) + * - @ref wrap() + */ + GraphicsPipeline(PipelineLayout const&, Config, State const&); + + /** + * @brief constructs and creates + * @details calls: + * - @ref GraphicsPipeline(PipelineLayout const&, Config) + * - @ref create() + */ + explicit GraphicsPipeline(PipelineLayout const&, Config, create_t); + + /** + * @brief base constructor of a derivative pipeline, keeps the layout and copies the handle of the parent + * @pre @ref created() of the parent, with VK_PIPELINE_CREATE_ALLOW_DERIVATIVES_BIT in its flags + */ + GraphicsPipeline(GraphicsPipeline const& parent, Config); + + /** + * @brief constructs and creates a derivative pipeline + * @details calls: + * - @ref GraphicsPipeline(GraphicsPipeline const&, Config) + * - @ref create() + */ + explicit GraphicsPipeline(GraphicsPipeline const& parent, Config, create_t); + + /** + * @brief creates the pipeline + * @details derivative pipeline -> adds VK_PIPELINE_CREATE_DERIVATIVE_BIT + * empty @ref Config::colorBlendAttachments -> @ref Config::ALPHA_BLENDING per color format, the config is kept + * @pre the layout, the parent and the shader modules of the config are not destroyed + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vkCreateGraphicsPipelines() + */ + void create(); + + /** + * @return VK_NULL_HANDLE -> not a derivative pipeline + */ + [[nodiscard]] constexpr VkPipeline parent() const { return _vkParent; } + [[nodiscard]] constexpr Config const& config() const { return _config; } + +private: + VkPipeline _vkParent = VK_NULL_HANDLE; + Config _config; +}; +} diff --git a/cvk/incl/cvk/render/pipeline/graphics_pipeline_config.hpp b/cvk/incl/cvk/render/pipeline/graphics_pipeline_config.hpp new file mode 100644 index 00000000..cb218992 --- /dev/null +++ b/cvk/incl/cvk/render/pipeline/graphics_pipeline_config.hpp @@ -0,0 +1,165 @@ +#pragma once +#include "cvk/render/cmd/rendering_info.hpp" + +#include + +#include + +#include + + +namespace cvk { +class Shader; + +/** + * @brief graphics pipeline state, the defaults are the legacy defaults + * @details @ref GraphicsPipeline::create() sets the pointer and count members of the Vulkan structs from the vectors + */ +struct GraphicsPipelineConfig { + /** + * @brief alpha blending: src alpha, one minus src alpha + */ + static constexpr VkPipelineColorBlendAttachmentState ALPHA_BLENDING{ + .blendEnable = VK_TRUE, + .srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA, + .dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, + .colorBlendOp = VK_BLEND_OP_ADD, + .srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE, + .dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO, + .alphaBlendOp = VK_BLEND_OP_ADD, + .colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT + | VK_COLOR_COMPONENT_A_BIT, + }; + + /** + * @brief adds the stage of the shader + * @param specialization nullptr -> no specialization constants + * @param entryPoint name of the entry function + * @attention @ref specialization and @ref entryPoint must outlive @ref GraphicsPipeline::create() + * @pre @ref Shader::created() + * @pre no stage of the same type + */ + GraphicsPipelineConfig& addStage( + Shader const&, + VkSpecializationInfo const* specialization = nullptr, + char const* entryPoint = "main" + ); + + /** + * @pre a stage of the type exists + */ + constexpr GraphicsPipelineConfig& removeStage(VkShaderStageFlagBits stage); + + /** + * @brief VK_PIPELINE_CREATE_ALLOW_DERIVATIVES_BIT -> the pipeline can be a parent + */ + VkPipelineCreateFlags flags = 0; + + std::vector stages{}; + + std::vector vertexBindings{}; + std::vector vertexAttributes{}; + + VkPipelineInputAssemblyStateCreateInfo inputAssembly{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, + .primitiveRestartEnable = VK_FALSE, + }; + + VkPipelineRasterizationStateCreateInfo rasterization{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .depthClampEnable = VK_FALSE, + .rasterizerDiscardEnable = VK_FALSE, + .polygonMode = VK_POLYGON_MODE_FILL, + .cullMode = VK_CULL_MODE_BACK_BIT, + .frontFace = VK_FRONT_FACE_CLOCKWISE, + .depthBiasEnable = VK_FALSE, + .depthBiasConstantFactor = 0.f, + .depthBiasClamp = 0.f, + .depthBiasSlopeFactor = 0.f, + .lineWidth = 1.f, + }; + + VkPipelineMultisampleStateCreateInfo multisample{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .rasterizationSamples = VK_SAMPLE_COUNT_1_BIT, + .sampleShadingEnable = VK_FALSE, + .minSampleShading = 0.f, + .pSampleMask = nullptr, + .alphaToCoverageEnable = VK_FALSE, + .alphaToOneEnable = VK_FALSE, + }; + + VkPipelineDepthStencilStateCreateInfo depthStencil{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .depthTestEnable = VK_TRUE, + .depthWriteEnable = VK_TRUE, + .depthCompareOp = VK_COMPARE_OP_LESS, + .depthBoundsTestEnable = VK_FALSE, + .stencilTestEnable = VK_FALSE, + .front = {}, + .back = {}, + .minDepthBounds = 0.f, + .maxDepthBounds = 1.f, + }; + + /** + * @brief color blend state, attachmentCount and pAttachments come from @ref colorBlendAttachments + */ + VkPipelineColorBlendStateCreateInfo colorBlend{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .logicOpEnable = VK_FALSE, + .logicOp = VK_LOGIC_OP_COPY, + .attachmentCount = 0, + .pAttachments = nullptr, + .blendConstants = {0.f, 0.f, 0.f, 0.f}, + }; + + /** + * @brief blend state per color attachment, empty -> @ref ALPHA_BLENDING per color format + * @pre empty or size == @ref colorFormats size + */ + std::vector colorBlendAttachments{}; + + uint32_t viewportCount = 1; + uint32_t scissorCount = 1; + + std::vector dynamicStates{VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}; + + /** + * @brief formats of the color attachments of the rendering + */ + std::vector colorFormats{}; + VkFormat depthFormat = VK_FORMAT_UNDEFINED; + VkFormat stencilFormat = VK_FORMAT_UNDEFINED; + uint32_t viewMask = 0; + + /** + * @brief == the mapping of the command buffer at a draw, see @ref BaseCmdBuffer::setAttachmentMapping() + * @pre @ref AttachmentMapping::debug_check() passes for @ref colorFormats + */ + AttachmentMapping attachmentMapping{}; +}; +} + +namespace cvk { +constexpr GraphicsPipelineConfig& GraphicsPipelineConfig::removeStage(VkShaderStageFlagBits stage) { + auto const removed = std::erase_if( + stages, + [stage](VkPipelineShaderStageCreateInfo const& info) { return info.stage == stage; } + ); + CTH_CRITICAL(removed == 0, "stage not added") {} + + return *this; +} +} diff --git a/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp b/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp new file mode 100644 index 00000000..5e845e8f --- /dev/null +++ b/cvk/incl/cvk/render/pipeline/layout/descriptor_set_layout.hpp @@ -0,0 +1,280 @@ +#pragma once +#include "cvk/base/device_table.hpp" +#include "cvk/utility/constants.hpp" +#include "cvk/utility/handle/core_handle.hpp" +#include "cvk/utility/types.hpp" + +#include + +#include + +#include +#include +#include +#include +#include +#include + + +namespace cvk { +class Core; + +/** + * @brief VkDescriptorSetLayout wrapper + * @details a binding with VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT -> the layout has + * VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT + */ +class DescriptorSetLayout { +public: + using vk_handle_t = VkDescriptorSetLayout; + struct Config; + struct State; + + /** + * @brief base constructor + * @pre @ref Core::created() + */ + DescriptorSetLayout(Core const&, Config); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref DescriptorSetLayout(Core const&, Config) + * - @ref wrap() + */ + DescriptorSetLayout(Core const&, Config, State const&); + + /** + * @brief constructs and creates + * @details calls: + * - @ref DescriptorSetLayout(Core const&, Config) + * - @ref create() + */ + explicit DescriptorSetLayout(Core const&, Config, create_t); + + /** + * @note calls @ref optDestroy() + */ + ~DescriptorSetLayout() { optDestroy(); } + + /** + * @brief wraps the state + * @pre the layout was created with the config + * @post @ref created() + * @note calls @ref optDestroy() + */ + void wrap(State const&); + + /** + * @brief creates the layout + * @pre @ref Config::debug_check() of the config + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vkCreateDescriptorSetLayout() + */ + void create(); + + /** + * @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(); + + static void destroy(DeviceTable table, VkDescriptorSetLayout); + + [[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(); } + + /** + * @return bindings, sorted by binding number + */ + [[nodiscard]] constexpr std::span bindings() const { return _bindings; } + + /** + * @return nullptr -> no binding with that number + */ + [[nodiscard]] constexpr VkDescriptorSetLayoutBinding const* binding(uint32_t binding) const; + + /** + * @return flags of the binding, no binding with that number -> 0 + */ + [[nodiscard]] constexpr VkDescriptorBindingFlags bindingFlags(uint32_t binding) const; + + /** + * @return a binding has VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT + */ + [[nodiscard]] constexpr bool updateAfterBind() const; + + /** + * @return binding with VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT, nullptr -> none + */ + [[nodiscard]] constexpr VkDescriptorSetLayoutBinding const* variableBinding() const; + +private: + void forget(); + + core_handle _handle; + std::vector _bindings; + std::vector _bindingFlags; + +public: + DescriptorSetLayout(DescriptorSetLayout const& other) = delete; + DescriptorSetLayout(DescriptorSetLayout&& other) noexcept = default; + DescriptorSetLayout& operator=(DescriptorSetLayout const& other) = delete; + DescriptorSetLayout& operator=(DescriptorSetLayout&& other) noexcept = default; + + static constexpr void debug_check(DescriptorSetLayout const&); +}; +} + +namespace cvk { +struct DescriptorSetLayout::Config { + /** + * @brief adds a binding, the numbers may have gaps + * @param binding number of the binding + * @param count of descriptors, 0 -> the binding is reserved without descriptors, + * VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT -> the maximum count + * @param flags VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT, VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT, ... + * @pre no binding with the same number + */ + constexpr Config& add( + uint32_t binding, + VkDescriptorType type, + VkShaderStageFlags stages, + uint32_t count = 1, + VkDescriptorBindingFlags flags = 0 + ); + + /** + * @brief removes a binding + * @pre a binding with the number exists + */ + constexpr Config& remove(uint32_t binding); + + /** + * @brief bindings sorted by binding number + */ + std::vector bindings{}; + + /** + * @brief flags per binding in the order of @ref bindings, empty -> no flags + */ + std::vector bindingFlags{}; + + static constexpr void debug_check(Config const&); +}; + +struct DescriptorSetLayout::State { + vk_not_null vkLayout; +}; +} + +namespace cvk { +constexpr DescriptorSetLayout::Config& DescriptorSetLayout::Config::add( + uint32_t binding, + VkDescriptorType type, + VkShaderStageFlags stages, + uint32_t count, + VkDescriptorBindingFlags flags +) { + auto const it = std::ranges::lower_bound(bindings, binding, {}, &VkDescriptorSetLayoutBinding::binding); + CTH_CRITICAL(it != bindings.end() && it->binding == binding, "binding {} already exists", binding) {} + + // flags are stored for every binding once one binding has flags + if(flags != 0 || !bindingFlags.empty()) { + auto const index = it - bindings.begin(); + bindingFlags.resize(bindings.size(), 0); + bindingFlags.insert(bindingFlags.begin() + index, flags); + } + + bindings.insert( + it, + VkDescriptorSetLayoutBinding{ + .binding = binding, + .descriptorType = type, + .descriptorCount = count, + .stageFlags = stages, + .pImmutableSamplers = nullptr, + } + ); + return *this; +} + +constexpr DescriptorSetLayout::Config& DescriptorSetLayout::Config::remove(uint32_t binding) { + auto const it = std::ranges::lower_bound(bindings, binding, {}, &VkDescriptorSetLayoutBinding::binding); + CTH_CRITICAL(it == bindings.end() || it->binding != binding, "binding {} does not exist", binding) {} + + if(!bindingFlags.empty()) bindingFlags.erase(bindingFlags.begin() + (it - bindings.begin())); + bindings.erase(it); + return *this; +} +} + +namespace cvk { +constexpr void DescriptorSetLayout::Config::debug_check(Config const& config) { + CTH_CRITICAL( + !config.bindingFlags.empty() && config.bindingFlags.size() != config.bindings.size(), + "binding flags must be empty or one per binding" + ) {} + if(config.bindingFlags.empty()) return; + + for(auto const [index, flags] : config.bindingFlags | std::views::enumerate) { + auto const& binding = config.bindings[index]; + bool const dynamic = binding.descriptorType == VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC + || binding.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC; + + CTH_CRITICAL( + (flags & VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT) != 0 + && static_cast(index) + 1 != config.bindings.size(), + "only the binding with the highest number can have a variable descriptor count, got binding {}", + binding.binding + ) {} + CTH_CRITICAL( + dynamic && (flags & (VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT + | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT)) != 0, + "dynamic buffer bindings cannot update after bind or have a variable descriptor count, got binding {}", + binding.binding + ) {} + } +} + +constexpr VkDescriptorSetLayoutBinding const* DescriptorSetLayout::binding(uint32_t binding) const { + auto const it = std::ranges::lower_bound(_bindings, binding, {}, &VkDescriptorSetLayoutBinding::binding); + return it != _bindings.end() && it->binding == binding ? std::to_address(it) : nullptr; +} + +constexpr VkDescriptorBindingFlags DescriptorSetLayout::bindingFlags(uint32_t binding) const { + if(_bindingFlags.empty()) return 0; + + auto const it = std::ranges::lower_bound(_bindings, binding, {}, &VkDescriptorSetLayoutBinding::binding); + return it != _bindings.end() && it->binding == binding ? _bindingFlags[it - _bindings.begin()] : 0; +} + +constexpr bool DescriptorSetLayout::updateAfterBind() const { + return std::ranges::any_of(_bindingFlags, [](VkDescriptorBindingFlags flags) { + return (flags & VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT) != 0; + }); +} + +constexpr VkDescriptorSetLayoutBinding const* DescriptorSetLayout::variableBinding() const { + if(_bindingFlags.empty() || (_bindingFlags.back() & VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT) == 0) + return nullptr; + return &_bindings.back(); +} + +constexpr void DescriptorSetLayout::debug_check(DescriptorSetLayout const& layout) { + CTH_CRITICAL(!layout.created(), "descriptor set layout must be created") {} +} +} diff --git a/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp b/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp new file mode 100644 index 00000000..5e75a008 --- /dev/null +++ b/cvk/incl/cvk/render/pipeline/layout/pipeline_layout.hpp @@ -0,0 +1,138 @@ +#pragma once +#include "cvk/base/device_table.hpp" +#include "cvk/utility/constants.hpp" +#include "cvk/utility/handle/core_handle.hpp" +#include "cvk/utility/types.hpp" + +#include + +#include + +#include +#include + + +namespace cvk { +class Core; +class DescriptorSetLayout; + +/** + * @brief VkPipelineLayout wrapper + */ +class PipelineLayout { +public: + using vk_handle_t = VkPipelineLayout; + struct Config; + struct State; + + /** + * @brief base constructor, copies the handles of the set layouts + * @pre @ref Core::created() + * @pre the set layouts are created + */ + PipelineLayout(Core const&, Config const&); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref PipelineLayout(Core const&, Config const&) + * - @ref wrap() + */ + PipelineLayout(Core const&, Config const&, State const&); + + /** + * @brief constructs and creates + * @details calls: + * - @ref PipelineLayout(Core const&, Config const&) + * - @ref create() + */ + explicit PipelineLayout(Core const&, Config const&, create_t); + + /** + * @note calls @ref optDestroy() + */ + ~PipelineLayout() { optDestroy(); } + + /** + * @brief wraps the state + * @pre the layout was created with the config + * @post @ref created() + * @note calls @ref optDestroy() + */ + void wrap(State const&); + + /** + * @brief creates the layout + * @pre the set layouts of the config are not destroyed + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::cvk_exception reason: more set layouts than maxBoundDescriptorSets + * @throws cvk::vk_result_exception result of vkCreatePipelineLayout() + */ + void create(); + + /** + * @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(); + + static void destroy(DeviceTable table, VkPipelineLayout); + + [[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(); } + + /** + * @return handle of the set layout per set number + */ + [[nodiscard]] constexpr std::span setLayouts() const { return _vkSetLayouts; } + +private: + void forget(); + + core_handle _handle; + std::vector _vkSetLayouts; + std::vector _pushConstantRanges; + +public: + PipelineLayout(PipelineLayout const& other) = delete; + PipelineLayout(PipelineLayout&& other) noexcept = default; + PipelineLayout& operator=(PipelineLayout const& other) = delete; + PipelineLayout& operator=(PipelineLayout&& other) noexcept = default; + + static constexpr void debug_check(PipelineLayout const&); +}; +} + +namespace cvk { +struct PipelineLayout::Config { + /** + * @brief set layout per set number, index == set number + */ + std::vector> setLayouts{}; + + std::vector pushConstantRanges{}; +}; + +struct PipelineLayout::State { + vk_not_null vkLayout; +}; +} + +namespace cvk { +constexpr void PipelineLayout::debug_check(PipelineLayout const& layout) { + CTH_CRITICAL(!layout.created(), "pipeline layout must be created") {} +} +} diff --git a/cvk/incl/cvk/render/pipeline/shader/shader.hpp b/cvk/incl/cvk/render/pipeline/shader/shader.hpp new file mode 100644 index 00000000..69623bb0 --- /dev/null +++ b/cvk/incl/cvk/render/pipeline/shader/shader.hpp @@ -0,0 +1,191 @@ +#pragma once +#include "cvk/base/device_table.hpp" +#include "cvk/utility/handle/core_handle.hpp" +#include "cvk/utility/types.hpp" + +#include + +#include + +#include +#include +#include +#include +#include + + +namespace cvk { +class Core; + +/** + * @brief VkShaderModule wrapper + * @details a pipeline does not use the module after its creation + */ +class Shader { +public: + using vk_handle_t = VkShaderModule; + struct State; + using path_t = std::filesystem::path; + + static constexpr std::string_view DEFAULT_COMPILER = "glslc"; + + /** + * @brief base constructor + * @param stage of the shader + * @pre @ref Core::created() + */ + Shader(Core const&, VkShaderStageFlagBits stage); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref Shader(Core const&, VkShaderStageFlagBits) + * - @ref wrap() + */ + Shader(Core const&, VkShaderStageFlagBits stage, State const&); + + /** + * @brief constructs and creates + * @details calls: + * - @ref Shader(Core const&, VkShaderStageFlagBits) + * - @ref create(std::span) + */ + Shader(Core const&, VkShaderStageFlagBits stage, std::span spv); + + /** + * @brief constructs, loads and creates + * @details calls: + * - @ref Shader(Core const&, VkShaderStageFlagBits) + * - @ref create(path_t const&) + */ + Shader(Core const&, VkShaderStageFlagBits stage, path_t const& spvPath); + + /** + * @brief constructs, compiles the glsl file, loads and creates + * @details for development, release builds compile shaders at build time + * calls: + * - @ref compile() + * - @ref Shader(Core const&, VkShaderStageFlagBits, path_t const&) + * @note warns in release builds + */ + Shader( + Core const&, + VkShaderStageFlagBits stage, + path_t const& spvPath, + path_t const& glslPath, + path_t const& compilerPath = DEFAULT_COMPILER + ); + + /** + * @note calls @ref optDestroy() + */ + ~Shader() { optDestroy(); } + + /** + * @brief wraps the state + * @post @ref created() + * @note calls @ref optDestroy() + */ + void wrap(State const&); + + /** + * @brief creates the module + * @param spv SPIR-V code + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vkCreateShaderModule() + */ + void create(std::span spv); + + /** + * @brief loads the SPIR-V file and creates the module + * @details calls: + * - @ref load() + * - @ref create(std::span) + */ + void create(path_t const& spvPath); + + /** + * @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(); + + /** + * @return stage info for a pipeline + * @param specialization nullptr -> no specialization constants + * @param entryPoint name of the entry function + * @pre @ref created() + * @attention @ref specialization and @ref entryPoint must outlive the pipeline creation + */ + [[nodiscard]] VkPipelineShaderStageCreateInfo stageInfo( + VkSpecializationInfo const* specialization = nullptr, + char const* entryPoint = "main" + ) const; + + /** + * @brief reads a SPIR-V file + * @throws cvk::cvk_exception reason: file missing, size not a multiple of 4 or 0 + */ + [[nodiscard]] static std::vector load(path_t const& spvPath); + + /** + * @brief compiles a glsl file to a SPIR-V file + * @param compilerPath glslc compatible compiler + * @param flags for the compiler + * @details the compiler writes its output to the file spvPath + ".log", the call reads and removes the file, + * compiler output without error -> logged as warning + * @throws cvk::cvk_exception reason: glsl file missing, compiler failed, details: compiler output + */ + static void compile( + path_t const& glslPath, + path_t const& spvPath, + path_t const& compilerPath = DEFAULT_COMPILER, + std::string_view flags = "-O" + ); + + static void destroy(DeviceTable table, VkShaderModule); + + [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } + [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); } + [[nodiscard]] constexpr VkShaderStageFlagBits stage() const { return _stage; } + [[nodiscard]] constexpr Core const& core() const { return _handle.core(); } + +private: + void forget(); + + core_handle _handle; + VkShaderStageFlagBits _stage; + +public: + Shader(Shader const& other) = delete; + Shader(Shader&& other) noexcept = default; + Shader& operator=(Shader const& other) = delete; + Shader& operator=(Shader&& other) noexcept = default; + + static constexpr void debug_check(Shader const&); +}; +} + +namespace cvk { +struct Shader::State { + vk_not_null vkShader; +}; +} + +namespace cvk { +constexpr void Shader::debug_check(Shader const& shader) { + CTH_CRITICAL(!shader.created(), "shader must be created") {} +} +} diff --git a/cvk/incl/cvk/render/sync/barrier.hpp b/cvk/incl/cvk/render/sync/barrier.hpp new file mode 100644 index 00000000..53585d06 --- /dev/null +++ b/cvk/incl/cvk/render/sync/barrier.hpp @@ -0,0 +1,381 @@ +#pragma once +#include "cvk/base/queue/queue_family.hpp" +#include "cvk/render/cmd/cmd_buffer.hpp" +#include "cvk/utility/format.hpp" +#include "cvk/utility/types.hpp" + +#include + +#include + +#include +#include +#include + + +namespace cvk { +class BaseBuffer; +class Image; + +/** + * @brief one side of a barrier: stages, access and queue family + */ +struct BarrierScope { + VkPipelineStageFlags2 stages = VK_PIPELINE_STAGE_2_NONE; + VkAccessFlags2 access = VK_ACCESS_2_NONE; + + /** + * @brief VK_QUEUE_FAMILY_IGNORED -> no queue family ownership transfer + */ + queue_family_index_t queueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; +}; + +/** + * @brief image part of a barrier + */ +struct ImageBarrierInfo { + BarrierScope src{}; + BarrierScope dst{}; + + /** + * @brief == @ref oldLayout -> no layout transition + */ + VkImageLayout newLayout = VK_IMAGE_LAYOUT_UNDEFINED; + + /** + * @brief layout of the mip range before the barrier + * @attention VK_IMAGE_LAYOUT_UNDEFINED discards the contents, the validation layers do not report it + */ + VkImageLayout oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + + uint32_t firstMipLevel = 0; + + /** + * @brief VK_REMAINING_MIP_LEVELS -> to the last mip level + */ + uint32_t mipLevels = VK_REMAINING_MIP_LEVELS; + + uint32_t firstArrayLayer = 0; + + /** + * @brief VK_REMAINING_ARRAY_LAYERS -> to the last layer + */ + uint32_t arrayLayers = VK_REMAINING_ARRAY_LAYERS; + + /** + * @brief 0 -> the aspect mask of the image + */ + VkImageAspectFlags aspectMask = 0; + + [[nodiscard]] static constexpr ImageBarrierInfo LayoutTransition( + VkImageLayout newLayout, + VkImageLayout oldLayout, + BarrierScope const& dst, + BarrierScope const& src, + uint32_t firstMipLevel = 0, + uint32_t mipLevels = VK_REMAINING_MIP_LEVELS + ) { + return ImageBarrierInfo{ + .src = src, + .dst = dst, + .newLayout = newLayout, + .oldLayout = oldLayout, + .firstMipLevel = firstMipLevel, + .mipLevels = mipLevels, + }; + } + + /** + * @brief queue family ownership transfer without layout transition, dst and src name the families + * @param layout of the image + */ + [[nodiscard]] static constexpr ImageBarrierInfo QueueTransition( + VkImageLayout layout, + BarrierScope const& dst, + BarrierScope const& src + ) { + return ImageBarrierInfo{.src = src, .dst = dst, .newLayout = layout, .oldLayout = layout}; + } +}; + +/** + * @brief buffer part of a barrier + */ +struct BufferBarrierInfo { + BarrierScope src{}; + BarrierScope dst{}; + + /** + * @brief in bytes + */ + size_t offset = 0; + + /** + * @brief in bytes (VK_WHOLE_SIZE => to the end) + */ + size_t size = VK_WHOLE_SIZE; + + /** + * @brief queue family ownership transfer, dst and src name the families + */ + [[nodiscard]] static constexpr BufferBarrierInfo QueueTransition(BarrierScope const& dst, BarrierScope const& src) { + return BufferBarrierInfo{.src = src, .dst = dst}; + } +}; + +/** + * @brief global memory part of a barrier + */ +struct MemoryBarrierInfo { + BarrierScope src{}; + BarrierScope dst{}; +}; + +/** + * @brief pipeline barrier: memory, buffer and image barriers in one vkCmdPipelineBarrier2() + * @details aggregate, the add functions are shortcuts, + * @ref execute() checks the barriers + * @attention the images and buffers do not move until @ref execute() + */ +struct Barrier { + using MemoryInfo = MemoryBarrierInfo; + using ImageInfo = ImageBarrierInfo; + using BufferInfo = BufferBarrierInfo; + + /** + * @brief shader stages of @ref addTransition(): the layout decides, fragment shader for shader reads + */ + static constexpr VkPipelineStageFlags2 INFERRED_SHADER_STAGES = VK_PIPELINE_STAGE_2_NONE; + + struct BufferEntry { + not_null_ptr buffer; + BufferInfo info; + }; + + struct ImageEntry { + not_null_ptr image; + ImageInfo info; + }; + + /** + * @brief VK_DEPENDENCY_BY_REGION_BIT -> the dependency is per framebuffer region, e.g. per tile + */ + VkDependencyFlags flags = 0; + + std::vector memory{}; + std::vector buffers{}; + std::vector images{}; + + /** + * @brief adds a global memory barrier + */ + constexpr void add(BarrierScope const& dst, BarrierScope const& src) { + memory.push_back({.src = src, .dst = dst}); + } + + /** + * @brief adds an image barrier + */ + constexpr void add(Image const& image, ImageInfo const& info) { images.push_back({&image, info}); } + + /** + * @brief adds a layout transition of every layer with the scopes of @ref layout_transition() + * @param shaderStages stages of the shader reads in the layouts (@ref INFERRED_SHADER_STAGES => fragment shader) + * @param mipLevels (VK_REMAINING_MIP_LEVELS => to the last mip level) + * @pre @ref layout_scope() knows both layouts + */ + void addTransition( + Image const&, + VkImageLayout newLayout, + VkImageLayout oldLayout, + VkPipelineStageFlags2 shaderStages = INFERRED_SHADER_STAGES, + uint32_t firstMipLevel = 0, + uint32_t mipLevels = VK_REMAINING_MIP_LEVELS + ); + + /** + * @brief adds a buffer barrier + */ + constexpr void add(BaseBuffer const& buffer, BufferInfo const& info) { buffers.push_back({&buffer, info}); } + + /** + * @brief records the barriers + * @details inside a rendering: checks the rules of barriers inside a dynamic rendering + * @pre @ref BaseCmdBuffer::recording() + * @pre the images and buffers are created and the ranges lie in them + * @pre memory barriers have no queue family ownership transfer + * @pre the new layout of a layout transition or queue family ownership transfer + * is not VK_IMAGE_LAYOUT_UNDEFINED or VK_IMAGE_LAYOUT_PREINITIALIZED + * @pre inside a rendering: framebuffer space stages, VK_DEPENDENCY_BY_REGION_BIT, + * no layout transition, no queue family ownership transfer, attachments in local read or general layout + */ + void execute(BaseCmdBuffer const&) const; + + /** + * @brief removes every barrier, keeps the flags + */ + constexpr void clear() { + memory.clear(); + buffers.clear(); + images.clear(); + } + + [[nodiscard]] constexpr bool empty() const { return memory.empty() && buffers.empty() && images.empty(); } + +private: + /** + * @brief checks the rules of barriers inside a dynamic rendering + * @param rendering the command buffer began + */ + void debug_check_rendering( + RenderingInfo const& rendering, + std::span, + std::span, + std::span + ) const; +}; +} + +namespace cvk { +/** + * @brief scopes of a layout transition + */ +struct LayoutTransition { + BarrierScope src; + BarrierScope dst; +}; + +/** + * @brief what the gpu does with an image in the layout + * @details input attachments: fragment shader only, general: all commands + * @param aspects of the image, pick color or depth for the attachment, read only and local read layouts + * @param shaderStages stages of the shader reads (@ref Barrier::INFERRED_SHADER_STAGES => fragment shader) + * @return std::nullopt -> no scope for the layout: undefined, preinitialized or not known + * @pre shaderStages has only shader stages + */ +[[nodiscard]] constexpr std::optional layout_scope( + VkImageLayout, + VkImageAspectFlags aspects, + VkPipelineStageFlags2 shaderStages = Barrier::INFERRED_SHADER_STAGES +); + +/** + * @brief scopes of the transition: src from the old layout, dst from the new layout + * @details src keeps the writes of the old layout, + * old layout undefined, preinitialized or present -> src waits for the dst stages without access + * @return std::nullopt -> no scope for a layout + * @pre shaderStages has only shader stages + */ +[[nodiscard]] constexpr std::optional layout_transition( + VkImageLayout newLayout, + VkImageLayout oldLayout, + VkImageAspectFlags aspects, + VkPipelineStageFlags2 shaderStages = Barrier::INFERRED_SHADER_STAGES +); +} + +namespace cvk { +constexpr std::optional layout_scope( + VkImageLayout layout, + VkImageAspectFlags aspects, + VkPipelineStageFlags2 shaderStages +) { + constexpr VkPipelineStageFlags2 shaderStageMask = VK_PIPELINE_STAGE_2_VERTEX_SHADER_BIT + | VK_PIPELINE_STAGE_2_TESSELLATION_CONTROL_SHADER_BIT + | VK_PIPELINE_STAGE_2_TESSELLATION_EVALUATION_SHADER_BIT + | VK_PIPELINE_STAGE_2_GEOMETRY_SHADER_BIT + | VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT + | VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT + | VK_PIPELINE_STAGE_2_PRE_RASTERIZATION_SHADERS_BIT + | VK_PIPELINE_STAGE_2_TASK_SHADER_BIT_EXT + | VK_PIPELINE_STAGE_2_MESH_SHADER_BIT_EXT + | VK_PIPELINE_STAGE_2_RAY_TRACING_SHADER_BIT_KHR; + CTH_CRITICAL( + (shaderStages & ~shaderStageMask) != 0, + "shader stages must only have shader stage bits, got {:#x}", + shaderStages + ) {} + + auto const shaders = shaderStages != Barrier::INFERRED_SHADER_STAGES + ? shaderStages + : VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT; + bool const color = (aspects & VK_IMAGE_ASPECT_COLOR_BIT) != 0; + + constexpr VkPipelineStageFlags2 tests = VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT + | VK_PIPELINE_STAGE_2_LATE_FRAGMENT_TESTS_BIT; + constexpr BarrierScope colorAttachment{ + VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_ACCESS_2_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT + }; + constexpr BarrierScope depthAttachment{ + tests, + VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT + }; + BarrierScope const shaderRead{shaders, VK_ACCESS_2_SHADER_READ_BIT}; + BarrierScope const depthRead{ + tests | shaders, + VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_2_SHADER_READ_BIT + }; + // attachments that the rendering also reads as input attachments + constexpr auto localRead = [](BarrierScope const& attachment) { + return BarrierScope{ + attachment.stages | VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT, + attachment.access | VK_ACCESS_2_INPUT_ATTACHMENT_READ_BIT + }; + }; + + switch(layout) { + case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL: + return BarrierScope{VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_READ_BIT}; + case VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL: + return BarrierScope{VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT}; + case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL: return shaderRead; + case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL: return colorAttachment; + case VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL: + case VK_IMAGE_LAYOUT_STENCIL_ATTACHMENT_OPTIMAL: + case VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL: + case VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL: + case VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL: return depthAttachment; + case VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_OPTIMAL: + case VK_IMAGE_LAYOUT_STENCIL_READ_ONLY_OPTIMAL: + case VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL: return depthRead; + case VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL: return color ? colorAttachment : depthAttachment; + case VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL: return color ? shaderRead : depthRead; + case VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR: return localRead(color ? colorAttachment : depthAttachment); + case VK_IMAGE_LAYOUT_GENERAL: + return BarrierScope{ + VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, + VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT + }; + case VK_IMAGE_LAYOUT_PRESENT_SRC_KHR: return BarrierScope{}; + default: return std::nullopt; + } +} + +constexpr std::optional layout_transition( + VkImageLayout newLayout, + VkImageLayout oldLayout, + VkImageAspectFlags aspects, + VkPipelineStageFlags2 shaderStages +) { + auto const dst = layout_scope(newLayout, aspects, shaderStages); + if(!dst.has_value()) return std::nullopt; + + // no content to keep: src waits for the dst stages without access, + // orders the transition after their last use and after an acquire semaphore wait on these stages + if(oldLayout == VK_IMAGE_LAYOUT_UNDEFINED || oldLayout == VK_IMAGE_LAYOUT_PREINITIALIZED + || oldLayout == VK_IMAGE_LAYOUT_PRESENT_SRC_KHR) + return LayoutTransition{.src = {dst->stages}, .dst = *dst}; + + auto const src = layout_scope(oldLayout, aspects, shaderStages); + if(!src.has_value()) return std::nullopt; + + // reads leave nothing to make visible, only their stages finish first + constexpr VkAccessFlags2 writes = VK_ACCESS_2_TRANSFER_WRITE_BIT + | VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT + | VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT + | VK_ACCESS_2_SHADER_WRITE_BIT + | VK_ACCESS_2_MEMORY_WRITE_BIT; + return LayoutTransition{.src = {src->stages, src->access & writes}, .dst = *dst}; +} +} diff --git a/cvk/incl/cvk/render/sync/fence.hpp b/cvk/incl/cvk/render/sync/fence.hpp new file mode 100644 index 00000000..df8ca013 --- /dev/null +++ b/cvk/incl/cvk/render/sync/fence.hpp @@ -0,0 +1,193 @@ +#pragma once +#include "cvk/base/device_table.hpp" +#include "cvk/utility/constants.hpp" +#include "cvk/utility/handle/core_handle.hpp" +#include "cvk/utility/types.hpp" + +#include + +#include +#include + +#include + + +namespace cvk { +class Core; + +/** + * @brief VkFence wrapper + */ +class Fence { +public: + using vk_handle_t = VkFence; + struct State; + + /** + * @brief base constructor + * @pre @ref Core::created() + */ + explicit Fence(Core const&); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref Fence(Core const&) + * - @ref wrap() + */ + Fence(Core const&, State const&); + + /** + * @brief constructs and creates + * @details calls: + * - @ref Fence(Core const&) + * - @ref create() + */ + explicit Fence(Core const&, create_t, VkFenceCreateFlags flags = 0, VkExternalFenceHandleTypeFlags exportTypes = 0); + + /** + * @note calls @ref optDestroy() + */ + ~Fence() { optDestroy(); } + + /** + * @brief wraps the state + * @post @ref created() + * @note calls @ref optDestroy() + */ + void wrap(State const&); + + /** + * @brief creates the fence + * @param flags VK_FENCE_CREATE_SIGNALED_BIT -> created signaled + * @param exportTypes handle types the fence can export, 0 -> not exportable, + * @ref constants::OS_FENCE_HANDLE_TYPE for @ref extractOsHandle() + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vkCreateFence() + */ + void create(VkFenceCreateFlags flags = 0, VkExternalFenceHandleTypeFlags exportTypes = 0); + + /** + * @brief destroys and forgets + * @pre @ref created() + * @pre the gpu does not use the fence + */ + 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(); + + /** + * @return VK_SUCCESS -> signaled, VK_NOT_READY -> not signaled + * @pre @ref created() + * @throws cvk::vk_result_exception other result of vkGetFenceStatus() + */ + [[nodiscard]] VkResult status() const; + + /** + * @pre @ref created() + */ + [[nodiscard]] bool signaled() const { return status() == VK_SUCCESS; } + + /** + * @brief sets the fence to not signaled + * @pre @ref created() + * @throws cvk::vk_result_exception result of vkResetFences() + */ + void reset() const; + + /** + * @brief blocks until the fence is signaled or the timeout ends + * @param timeout in nanoseconds + * @return VK_SUCCESS or VK_TIMEOUT + * @pre @ref created() + * @throws cvk::vk_result_exception other result of vkWaitForFences() + */ + [[nodiscard]] VkResult wait(uint64_t timeout) const; + + /** + * @brief blocks until the fence is signaled + * @pre @ref created() + * @throws cvk::vk_result_exception result of vkWaitForFences() + */ + void wait() const; + + /** + * @brief waits, then resets if signaled + * @param timeout in nanoseconds + * @return VK_SUCCESS -> reset, VK_TIMEOUT -> not reset + * @details calls: + * - @ref wait(uint64_t) const + * - @ref reset() + */ + [[nodiscard]] VkResult waitReset(uint64_t timeout) const; + + /** + * @brief waits and resets + * @details calls: + * - @ref wait() const + * - @ref reset() + */ + void waitReset() const; + + /** + * @brief exports the fence as an OS handle, see @ref constants::OS_FENCE_HANDLE_TYPE + * @details windows: the fence keeps its state, + * posix: the export resets the fence (sync fd, copy transference), invalid handle -> already signaled + * @return handle, waitable with cth::os::wait() or a scheduler + * @pre @ref created() with @ref constants::OS_FENCE_HANDLE_TYPE in the export types + * @pre the device enables @ref constants::OS_FENCE_DEVICE_EXTENSIONS + * @pre posix: the fence is signaled or a signal operation is pending + * @attention posix: blocks until the pending work reaches the kernel, e.g. a wait before signal on a timeline + * semaphore that the host signals later -> deadlock + * @throws cvk::vk_result_exception result of vkGetFenceWin32HandleKHR() or vkGetFenceFdKHR() + */ + [[nodiscard]] cth::os::unique_native_handle extractOsHandle() const; + + static void destroy(DeviceTable table, VkFence); + + [[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 VkExternalFenceHandleTypeFlags exportTypes() const { return _exportTypes; } + +private: + void forget(); + + core_handle _handle; + VkExternalFenceHandleTypeFlags _exportTypes = 0; + +public: + Fence(Fence const& other) = delete; + Fence(Fence&& other) noexcept = default; + Fence& operator=(Fence const& other) = delete; + Fence& operator=(Fence&& other) noexcept = default; + + static constexpr void debug_check(Fence const&); +}; +} + +namespace cvk { +struct Fence::State { + vk_not_null vkFence; + /** + * @brief handle types the fence was created to export + */ + VkExternalFenceHandleTypeFlags exportTypes = 0; +}; +} + +namespace cvk { +constexpr void Fence::debug_check(Fence const& fence) { + CTH_CRITICAL(!fence.created(), "fence must be created") {} +} +} diff --git a/cvk/incl/cvk/render/sync/semaphore.hpp b/cvk/incl/cvk/render/sync/semaphore.hpp new file mode 100644 index 00000000..f4f08578 --- /dev/null +++ b/cvk/incl/cvk/render/sync/semaphore.hpp @@ -0,0 +1,123 @@ +#pragma once +#include "cvk/base/device_table.hpp" +#include "cvk/utility/constants.hpp" +#include "cvk/utility/handle/core_handle.hpp" +#include "cvk/utility/types.hpp" + +#include + +#include + + +namespace cvk { +class Core; + +/** + * @brief binary VkSemaphore wrapper + */ +class Semaphore { +public: + using vk_handle_t = VkSemaphore; + struct State; + + /** + * @brief base constructor + * @pre @ref Core::created() + */ + explicit Semaphore(Core const&); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref Semaphore(Core const&) + * - @ref wrap() + */ + Semaphore(Core const&, State const&); + + /** + * @brief constructs and creates + * @details calls: + * - @ref Semaphore(Core const&) + * - @ref create() + */ + explicit Semaphore(Core const&, create_t); + + /** + * @note calls @ref optDestroy() + */ + ~Semaphore() { optDestroy(); } + + /** + * @brief wraps the state + * @post @ref created() + * @note calls @ref optDestroy() + */ + void wrap(State const&); + + /** + * @brief creates the semaphore + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vkCreateSemaphore() + */ + void create(); + + /** + * @brief destroys and forgets + * @pre @ref created() + * @pre the gpu does not use the semaphore + */ + 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(DeviceTable table, VkSemaphore); + + [[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(); } + +protected: + /** + * @brief creates the semaphore with info + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vkCreateSemaphore() + */ + void create(VkSemaphoreCreateInfo const& info); + +private: + void forget(); + + core_handle _handle; + +public: + Semaphore(Semaphore const& other) = delete; + Semaphore(Semaphore&& other) noexcept = default; + Semaphore& operator=(Semaphore const& other) = delete; + Semaphore& operator=(Semaphore&& other) noexcept = default; + + static constexpr void debug_check(Semaphore const&); +}; +} + +namespace cvk { +struct Semaphore::State { + vk_not_null vkSemaphore; +}; +} + +namespace cvk { +constexpr void Semaphore::debug_check(Semaphore const& semaphore) { + CTH_CRITICAL(!semaphore.created(), "semaphore must be created") {} +} +} diff --git a/cvk/incl/cvk/render/sync/timeline_semaphore.hpp b/cvk/incl/cvk/render/sync/timeline_semaphore.hpp new file mode 100644 index 00000000..a873c182 --- /dev/null +++ b/cvk/incl/cvk/render/sync/timeline_semaphore.hpp @@ -0,0 +1,151 @@ +#pragma once +#include "semaphore.hpp" + +#include + + +namespace cvk { +/** + * @brief timeline VkSemaphore with a host counter + * @details the counter is the last value that @ref next() or @ref signal() gave out, + * private base: a timeline semaphore must not pass as a binary @ref Semaphore + * @attention not thread safe: @ref next() and @ref signal() change the counter + */ +class TimelineSemaphore : Semaphore { +public: + using Semaphore::vk_handle_t; + struct State; + + /** + * @brief base constructor + * @pre @ref Core::created() + */ + explicit TimelineSemaphore(Core const&); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref TimelineSemaphore(Core const&) + * - @ref wrap() + */ + TimelineSemaphore(Core const&, State const&); + + /** + * @brief constructs and creates + * @details calls: + * - @ref TimelineSemaphore(Core const&) + * - @ref create() with the initial value + */ + explicit TimelineSemaphore(Core const&, create_t, uint64_t initialValue = 0); + + /** + * @note calls @ref optDestroy() + */ + ~TimelineSemaphore() = default; + + /** + * @brief wraps the state + * @post @ref created() + * @note calls @ref optDestroy() + */ + void wrap(State const&); + + /** + * @brief creates the semaphore + * @param initialValue of the semaphore and the counter + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vkCreateSemaphore() + */ + void create(uint64_t initialValue = 0); + + using Semaphore::destroy; + using Semaphore::optDestroy; + + /** + * @brief releases ownership, returns the state and forgets + * @pre @ref created() + */ + State release(); + + /** + * @brief advances the counter and signals the semaphore from the host + * @return the signaled value + * @pre @ref created() + * @pre no signal of the semaphore is pending + * @throws cvk::vk_result_exception result of vkSignalSemaphore() + */ + uint64_t signal(); + + /** + * @brief blocks until the semaphore reaches the value or the timeout ends + * @param timeout in nanoseconds + * @return VK_SUCCESS or VK_TIMEOUT + * @pre @ref created() + * @throws cvk::vk_result_exception other result of vkWaitSemaphores() + */ + [[nodiscard]] VkResult wait(uint64_t value, uint64_t timeout) const; + + /** + * @brief blocks until the semaphore reaches the value + * @pre @ref created() + * @throws cvk::vk_result_exception result of vkWaitSemaphores() + */ + void wait(uint64_t value) const; + + /** + * @brief blocks until the semaphore reaches @ref value() + * @details calls @ref wait(uint64_t) const + */ + void wait() const { wait(_value); } + + /** + * @brief advances the counter + * @return value for the next signal operation + * @attention signal the values in counter order: one queue keeps the order, + * two queues need a wait between their submits + */ + [[nodiscard]] constexpr uint64_t next() const { return ++_value; } + + /** + * @return the counter, value to wait for + */ + [[nodiscard]] constexpr uint64_t value() const { return _value; } + + /** + * @return current value of the semaphore + * @pre @ref created() + * @throws cvk::vk_result_exception result of vkGetSemaphoreCounterValue() + */ + [[nodiscard]] uint64_t gpuValue() const; + + using Semaphore::core; + using Semaphore::created; + using Semaphore::get; + +private: + mutable uint64_t _value = 0; + +public: + TimelineSemaphore(TimelineSemaphore const& other) = delete; + TimelineSemaphore(TimelineSemaphore&& other) noexcept = default; + TimelineSemaphore& operator=(TimelineSemaphore const& other) = delete; + TimelineSemaphore& operator=(TimelineSemaphore&& other) noexcept = default; + + static constexpr void debug_check(TimelineSemaphore const& semaphore); +}; +} + +namespace cvk { +struct TimelineSemaphore::State { + vk_not_null vkSemaphore; + /** + * @brief counter, see @ref value() + */ + uint64_t value; +}; +} + +namespace cvk { +constexpr void TimelineSemaphore::debug_check(TimelineSemaphore const& semaphore) { Semaphore::debug_check(semaphore); } +} diff --git a/cvk/incl/cvk/surface/surface.hpp b/cvk/incl/cvk/surface/surface.hpp new file mode 100644 index 00000000..227b785f --- /dev/null +++ b/cvk/incl/cvk/surface/surface.hpp @@ -0,0 +1,125 @@ +#pragma once +#include "cvk/utility/handle/instance_handle.hpp" +#include "cvk/utility/types.hpp" + +#include + +#include + +#include + + +namespace cvk { +class Instance; +class PhysicalDevice; + +/** + * @brief VkSurfaceKHR wrapper + * @details the window system creates the surface, @ref wrap() takes it + */ +class Surface { +public: + using vk_handle_t = VkSurfaceKHR; + struct State; + + /** + * @brief base constructor + * @pre @ref Instance::created() + */ + explicit Surface(Instance const&); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref Surface(Instance const&) + * - @ref wrap() + */ + Surface(Instance const&, State const&); + + /** + * @note calls @ref optDestroy() + */ + ~Surface() { optDestroy(); } + + /** + * @brief wraps the state + * @post @ref created() + * @note calls @ref optDestroy() + */ + void wrap(State 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(); + + /** + * @return queue family @ref familyIndex can present to this surface + * @throws cvk::vk_result_exception result of vkGetPhysicalDeviceSurfaceSupportKHR() + */ + [[nodiscard]] bool supportsFamily(PhysicalDevice const&, uint32_t familyIndex) const; + + /** + * @return present modes the physical device offers for the surface + * @throws cvk::vk_result_exception result of vkGetPhysicalDeviceSurfacePresentModesKHR() + */ + [[nodiscard]] std::vector presentModes(PhysicalDevice const&) const; + + /** + * @throws cvk::vk_result_exception result of vkGetPhysicalDeviceSurfaceCapabilitiesKHR() + */ + [[nodiscard]] VkSurfaceCapabilitiesKHR capabilities(PhysicalDevice const&) const; + + /** + * @return formats the physical device offers for the surface + * @attention no format -> the surface is not usable with the physical device + * @throws cvk::vk_result_exception result of vkGetPhysicalDeviceSurfaceFormatsKHR() + */ + [[nodiscard]] std::vector formats(PhysicalDevice const&) const; + + static void destroy(vk_not_null, VkSurfaceKHR); + + [[nodiscard]] constexpr bool created() const { return static_cast(_handle); } + [[nodiscard]] constexpr vk_handle_t get() const { return _handle.get(); } + +private: + void forget(); + + instance_handle _handle; + +public: + Surface(Surface const& other) = delete; + Surface(Surface&& other) noexcept = default; + Surface& operator=(Surface const& other) = delete; + Surface& operator=(Surface&& other) noexcept = default; + + static constexpr void debug_check(Surface const&); + static constexpr void debug_check_handle(vk_not_null); +}; +} + +namespace cvk { +struct Surface::State { + vk_not_null vkSurface; +}; +} + +namespace cvk { +constexpr void Surface::debug_check(Surface const& surface) { + CTH_CRITICAL(!surface.created(), "surface must be created") {} + debug_check_handle(surface.get()); +} +constexpr void Surface::debug_check_handle([[maybe_unused]] vk_not_null vkSurface) {} +} diff --git a/cvk/incl/cvk/surface/swapchain.hpp b/cvk/incl/cvk/surface/swapchain.hpp new file mode 100644 index 00000000..36b3409f --- /dev/null +++ b/cvk/incl/cvk/surface/swapchain.hpp @@ -0,0 +1,363 @@ +#pragma once +#include "swapchain_config.hpp" + +#include "cvk/base/device_table.hpp" +#include "cvk/base/queue/queue.hpp" +#include "cvk/data/img/image.hpp" +#include "cvk/data/img/image_view.hpp" +#include "cvk/render/sync/fence.hpp" +#include "cvk/render/sync/semaphore.hpp" +#include "cvk/utility/handle/core_handle.hpp" +#include "cvk/utility/types.hpp" + +#include + +#include + +#include +#include +#include +#include +#include +#include + + +namespace cvk { +class Core; +class Surface; + +/** + * @brief VkSwapchainKHR wrapper, owns its images, their views, a present semaphore and a present fence per image + * @details the images are wrapped without memory and released before the swapchain is destroyed, + * @ref resize() and @ref recreate() retire the old swapchain, + * the swapchain destroys it once its present fences signal + * @attention not thread safe + */ +class Swapchain { +public: + using vk_handle_t = VkSwapchainKHR; + using Config = SwapchainConfig; + struct State; + + /** + * @brief result of @ref acquire() + */ + struct Acquired { + /** + * @brief VK_SUCCESS, VK_SUBOPTIMAL_KHR, VK_ERROR_OUT_OF_DATE_KHR, VK_TIMEOUT or VK_NOT_READY + */ + VkResult result; + + /** + * @brief std::nullopt -> no image acquired, the semaphore stays unsignaled + */ + std::optional imageIndex; + }; + + /** + * @brief base constructor + * @pre @ref Core::created(), @ref Surface::created() + * @pre the queue can present to the surface + */ + Swapchain(Core const&, Queue const& presentQueue, Surface const&, Config); + + /** + * @brief constructs and wraps + * @details calls: + * - @ref Swapchain(Core const&, Queue const&, Surface const&, Config) + * - @ref wrap() + */ + Swapchain(Core const&, Queue const& presentQueue, Surface const&, Config, State const&); + + /** + * @brief constructs and creates + * @details calls: + * - @ref Swapchain(Core const&, Queue const&, Surface const&, Config) + * - @ref create() + */ + Swapchain(Core const&, Queue const& presentQueue, Surface const&, Config, VkExtent2D extent); + + /** + * @note calls @ref optDestroy() + */ + ~Swapchain() { optDestroy(); } + + /** + * @brief wraps the state, gets the images, creates their views, present semaphores and present fences + * @details the format and the present mode come from the state, not from the config + * @pre the swapchain was created for the surface with the config and the state + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::vk_result_exception result of vkGetSwapchainImagesKHR() + */ + void wrap(State const&); + + /** + * @brief picks the format and the present mode of the config, creates the swapchain + * @param extent of the window, a fixed currentExtent of the surface wins + * @details the first entries of @ref Config::formats and @ref Config::presentModes that the surface offers win + * @pre extent width and height > 0 + * @post @ref created() + * @note calls @ref optDestroy() + * @throws cvk::cvk_exception reason: the surface offers no allowed format or present mode + * @throws cvk::vk_result_exception result of vkCreateSwapchainKHR() + */ + void create(VkExtent2D extent); + + /** + * @brief creates a new swapchain with the old one as oldSwapchain, the old one retires + * @param extent of the window, a fixed currentExtent of the surface wins + * @details keeps the format and the present mode, queries the surface capabilities again, + * unacquires the images that are still acquired, + * the swapchain destroys the retired one once its present fences signal, + * a format or present mode change needs @ref recreate() + * @pre @ref created() + * @pre extent width and height > 0 + * @pre the surface still supports the format and the present mode of the swapchain + * @pre no present semaphore is signaled without its present + * @throws cvk::vk_result_exception result of vkCreateSwapchainKHR(), destroys the old swapchains and forgets + */ + void resize(VkExtent2D extent); + + /** + * @brief applies the config, e.g. another present mode, retires the old swapchain like @ref resize() + * @param extent of the window, a fixed currentExtent of the surface wins + * @details picks the format and the present mode of the config again, see @ref create() + * @pre @ref created() + * @pre extent width and height > 0 + * @pre no present semaphore is signaled without its present + * @note a new format needs new pipelines for the swapchain images + * @throws cvk::cvk_exception reason: the surface offers no allowed format or present mode, nothing changes + * @throws cvk::vk_result_exception result of vkCreateSwapchainKHR(), destroys the old swapchains and forgets + */ + void recreate(VkExtent2D extent, Config); + + /** + * @brief waits for the present fences, destroys the swapchain and forgets + * @details releases the images, destroys the views, present semaphores, present fences and retired swapchains + * @pre @ref created() + * @pre no present semaphore is signaled without its present + */ + void destroy(); + + /** + * @brief if @ref created() -> calls @ref destroy() + */ + void optDestroy() { if(created()) destroy(); } + + /** + * @brief waits for the present fences, releases ownership, returns the state and forgets + * @details releases the images, destroys the views, present semaphores, present fences and retired swapchains + * @pre @ref created() + * @pre no present semaphore is signaled without its present + */ + State release(); + + /** + * @brief acquires the next image + * @param signal signaled when the image is ready + * @param timeout in nanoseconds + * @details destroys the retired swapchains with signaled present fences + * @pre @ref created() + * @pre signal is unsignaled and has no pending operation + * @throws cvk::vk_result_exception other result of vkAcquireNextImageKHR() + */ + [[nodiscard]] Acquired acquire(Semaphore const& signal, uint64_t timeout = std::numeric_limits::max()); + + /** + * @brief presents the image once its present semaphore signals + * @return VK_SUCCESS, VK_SUBOPTIMAL_KHR or VK_ERROR_OUT_OF_DATE_KHR + * @details VK_ERROR_OUT_OF_DATE_KHR -> the present still waits for the semaphore, + * destroys the retired swapchains with signaled present fences + * @pre the image is acquired + * @pre a submit signals @ref presentSemaphore() of the image + * @throws cvk::vk_result_exception other result of vkQueuePresentKHR() + */ + [[nodiscard]] VkResult present(uint32_t imageIndex); + + /** + * @brief gives acquired images back without a present + * @pre the images are acquired and all uses of them completed + * @attention the acquire semaphores of the images still signal, wait for them before reuse + * @throws cvk::vk_result_exception result of vkReleaseSwapchainImagesKHR() + */ + void unacquire(std::span imageIndices); + + static void destroy(DeviceTable table, VkSwapchainKHR); + + /** + * @return the extent clamped to the limits of the surface, a fixed currentExtent wins + */ + [[nodiscard]] static constexpr VkExtent2D evalExtent(VkExtent2D extent, VkSurfaceCapabilitiesKHR const&); + + /** + * @param minImageCount 0 -> minImageCount of the surface + 1 + * @return the count clamped to the limits of the surface (maxImageCount 0 => no limit) + */ + [[nodiscard]] static constexpr uint32_t evalImageCount(uint32_t minImageCount, VkSurfaceCapabilitiesKHR const&); + + [[nodiscard]] constexpr bool created() const { return !_generations.empty(); } + [[nodiscard]] constexpr vk_handle_t get() const { return created() ? current().handle.get() : VK_NULL_HANDLE; } + [[nodiscard]] constexpr Core const& core() const { return *_core; } + [[nodiscard]] constexpr Config const& config() const { return _config; } + + [[nodiscard]] constexpr VkExtent2D extent() const { return _extent; } + [[nodiscard]] constexpr VkFormat format() const { return _surfaceFormat.format; } + [[nodiscard]] constexpr VkColorSpaceKHR colorSpace() const { return _surfaceFormat.colorSpace; } + [[nodiscard]] constexpr VkPresentModeKHR presentMode() const { return _presentMode; } + [[nodiscard]] constexpr uint32_t imageCount() const { + return created() ? static_cast(current().images.size()) : 0; + } + + /** + * @return width / height + * @pre @ref created() + */ + [[nodiscard]] constexpr float aspectRatio() const { + return static_cast(_extent.width) / static_cast(_extent.height); + } + + [[nodiscard]] constexpr std::span images() const { + return created() ? std::span{current().images} : std::span{}; + } + [[nodiscard]] constexpr std::span views() const { + return created() ? std::span{current().views} : std::span{}; + } + + /** + * @return the semaphore that @ref present() of the image waits for, a submit signals it + * @pre imageIndex < @ref imageCount() + */ + [[nodiscard]] constexpr Semaphore const& presentSemaphore(uint32_t imageIndex) const { + return current().presentSemaphores[imageIndex]; + } + + /** + * @pre imageIndex < @ref imageCount() + */ + [[nodiscard]] constexpr bool acquired(uint32_t imageIndex) const { return current().acquired[imageIndex]; } + + /** + * @return count of the retired swapchains that wait for their present fences + */ + [[nodiscard]] constexpr size_t retired() const { return created() ? _generations.size() - 1 : 0; } + +private: + /** + * @brief one VkSwapchainKHR with its images, views, present semaphores, present fences and acquire states + * @details the destructor waits for the present fences and releases the images, + * then the members destroy in reverse order: the swapchain last, + * a moved from generation is empty, its destructor does nothing, + * no move assignment: a member wise one destroys the images without the wait and the release + */ + struct Generation { + explicit Generation(core_handle swapchain) : handle{std::move(swapchain)} {} + ~Generation(); + + core_handle handle; + std::vector images; + std::vector views; + std::vector presentSemaphores; + std::vector presentFences; + std::vector acquired; + + Generation(Generation const& other) = delete; + Generation(Generation&& other) noexcept = default; + Generation& operator=(Generation const& other) = delete; + Generation& operator=(Generation&& other) = delete; + }; + + /** + * @pre @ref created() + */ + [[nodiscard]] constexpr Generation& current() { return _generations.back(); } + + /** + * @pre @ref created() + */ + [[nodiscard]] constexpr Generation const& current() const { return _generations.back(); } + + /** + * @return the first allowed format and present mode of the config that the surface offers + * @throws cvk::cvk_exception reason: the surface offers no allowed format or present mode + */ + [[nodiscard]] std::pair pick(Config const&) const; + + /** + * @brief creates a swapchain with @ref _surfaceFormat and @ref _presentMode as the new current generation + * @details a failure destroys every generation and forgets + */ + void createSwapchain(VkExtent2D extent, VkSwapchainKHR oldSwapchain); + + /** + * @brief gets the images of the current generation, creates their views, present semaphores and present fences + */ + void createImages(); + + /** + * @brief unacquires the acquired images of the current generation + */ + void unacquireAll(); + + /** + * @brief destroys the retired generations with signaled present fences + */ + void collectRetired(); + + void forget(); + + Queue _presentQueue; + not_null_ptr _surface; + not_null_ptr _core; + Config _config; + + /** + * @brief back() is the current swapchain, the others are retired, empty -> not created + */ + std::vector _generations; + + VkExtent2D _extent{}; + VkSurfaceFormatKHR _surfaceFormat{}; + VkPresentModeKHR _presentMode = VK_PRESENT_MODE_FIFO_KHR; + +public: + Swapchain(Swapchain const& other) = delete; + Swapchain(Swapchain&& other) noexcept = default; + Swapchain& operator=(Swapchain const& other) = delete; + Swapchain& operator=(Swapchain&& other) noexcept = default; + + static constexpr void debug_check(Swapchain const&); +}; +} + +namespace cvk { +struct Swapchain::State { + vk_not_null vkSwapchain; + VkExtent2D extent; + VkSurfaceFormatKHR surfaceFormat; + VkPresentModeKHR presentMode; +}; +} + +namespace cvk { +constexpr VkExtent2D Swapchain::evalExtent(VkExtent2D extent, VkSurfaceCapabilitiesKHR const& capabilities) { + if(capabilities.currentExtent.width != std::numeric_limits::max()) return capabilities.currentExtent; + + return VkExtent2D{ + std::clamp(extent.width, capabilities.minImageExtent.width, capabilities.maxImageExtent.width), + std::clamp(extent.height, capabilities.minImageExtent.height, capabilities.maxImageExtent.height), + }; +} + +constexpr uint32_t Swapchain::evalImageCount(uint32_t minImageCount, VkSurfaceCapabilitiesKHR const& capabilities) { + uint32_t const count = std::max( + minImageCount != 0 ? minImageCount : capabilities.minImageCount + 1, + capabilities.minImageCount + ); + return capabilities.maxImageCount != 0 ? std::min(count, capabilities.maxImageCount) : count; +} + +constexpr void Swapchain::debug_check(Swapchain const& swapchain) { + CTH_CRITICAL(!swapchain.created(), "swapchain must be created") {} +} +} diff --git a/cvk/incl/cvk/surface/swapchain_config.hpp b/cvk/incl/cvk/surface/swapchain_config.hpp new file mode 100644 index 00000000..22db5e5b --- /dev/null +++ b/cvk/incl/cvk/surface/swapchain_config.hpp @@ -0,0 +1,98 @@ +#pragma once +#include "cvk/base/queue/queue_family.hpp" + +#include + +#include + +#include +#include + + +namespace cvk { +/** + * @brief allowed formats and present modes, image count, usage and sharing of a swapchain + * @details @ref formats and @ref presentModes are preferences, one entry -> exactly this entry or a throw + */ +struct SwapchainConfig { + /** + * @brief allowed formats, the first one the surface offers wins + * @details empty -> the first format of the surface + */ + std::vector formats{ + {VK_FORMAT_B8G8R8A8_SRGB, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR}, + {VK_FORMAT_R8G8B8A8_SRGB, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR}, + }; + + /** + * @brief allowed present modes, the first one the surface offers wins + * @details empty -> VK_PRESENT_MODE_FIFO_KHR (always available) + */ + std::vector presentModes{VK_PRESENT_MODE_FIFO_KHR}; + + /** + * @brief 0 -> minImageCount of the surface + 1, clamped to the limits of the surface + */ + uint32_t minImageCount = 0; + + /** + * @pre != 0 + */ + VkImageUsageFlags usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; + + /** + * @brief queue families that use the images at the same time + * @details size < 2 -> VK_SHARING_MODE_EXCLUSIVE, else VK_SHARING_MODE_CONCURRENT + * @pre unique + */ + std::vector queueFamilies{}; + + VkCompositeAlphaFlagBitsKHR compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; + + /** + * @attention pQueueFamilyIndices points into @ref queueFamilies + */ + [[nodiscard]] constexpr VkSwapchainCreateInfoKHR createInfo( + VkSurfaceKHR surface, + VkSurfaceFormatKHR format, + VkPresentModeKHR presentMode, + VkSurfaceTransformFlagBitsKHR transform, + VkExtent2D extent, + uint32_t imageCount, + VkSwapchainKHR oldSwapchain + ) const { + return VkSwapchainCreateInfoKHR{ + .sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR, + .pNext = nullptr, + .flags = 0, + .surface = surface, + .minImageCount = imageCount, + .imageFormat = format.format, + .imageColorSpace = format.colorSpace, + .imageExtent = extent, + .imageArrayLayers = 1, + .imageUsage = usage, + .imageSharingMode = queueFamilies.size() >= 2 ? VK_SHARING_MODE_CONCURRENT : VK_SHARING_MODE_EXCLUSIVE, + .queueFamilyIndexCount = static_cast(queueFamilies.size()), + .pQueueFamilyIndices = queueFamilies.data(), + .preTransform = transform, + .compositeAlpha = compositeAlpha, + .presentMode = presentMode, + .clipped = VK_TRUE, + .oldSwapchain = oldSwapchain, + }; + } + + static constexpr void debug_check(SwapchainConfig const&); +}; +} + +namespace cvk { +constexpr void SwapchainConfig::debug_check(SwapchainConfig const& config) { + CTH_CRITICAL(config.usage == 0, "usage must not be 0") {} + + auto families = config.queueFamilies; + std::ranges::sort(families); + CTH_CRITICAL(std::ranges::adjacent_find(families) != families.end(), "queue families must be unique") {} +} +} diff --git a/cvk/incl/cvk/utility/constant/debug_constants.hpp b/cvk/incl/cvk/utility/constant/debug_constants.hpp new file mode 100644 index 00000000..5e40f3f1 --- /dev/null +++ b/cvk/incl/cvk/utility/constant/debug_constants.hpp @@ -0,0 +1,20 @@ +#pragma once +#include + +#include + +namespace cvk::constants { +/** + * @brief true in debug builds, follows @ref cth::debug_mode() + */ +constexpr bool DEBUG_MODE = cth::debug_mode(); + +constexpr VkDebugUtilsMessageSeverityFlagsEXT DEBUG_MESSAGE_SEVERITY = + VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT + | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT; + +constexpr VkDebugUtilsMessageTypeFlagsEXT DEBUG_MESSAGE_TYPE = + VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT + | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT + | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT; +} diff --git a/cvk/incl/cvk/utility/constant/device_constants.hpp b/cvk/incl/cvk/utility/constant/device_constants.hpp new file mode 100644 index 00000000..63c16ca8 --- /dev/null +++ b/cvk/incl/cvk/utility/constant/device_constants.hpp @@ -0,0 +1,61 @@ +#pragma once +#include "cvk/utility/device/device_features.hpp" + +#include + +#include +#include + +namespace cvk::constants { +/** + * @brief features that every device enables + * @details bindless: descriptorIndexing guarantees the minimum set of descriptor indexing features, they are enabled, + * descriptorBindingVariableDescriptorCount is not part of the set, the one extra requirement, + * dynamicRenderingLocalRead: tile local reads in a dynamic rendering, like subpasses + */ +inline constexpr DeviceFeatures BASE_DEVICE_FEATURES{ + .samplerAnisotropy = true, + .shaderSampledImageArrayDynamicIndexing = true, + .shaderStorageBufferArrayDynamicIndexing = true, + .descriptorIndexing = true, + .shaderUniformTexelBufferArrayDynamicIndexing = true, + .shaderStorageTexelBufferArrayDynamicIndexing = true, + .shaderSampledImageArrayNonUniformIndexing = true, + .shaderStorageBufferArrayNonUniformIndexing = true, + .shaderUniformTexelBufferArrayNonUniformIndexing = true, + .descriptorBindingSampledImageUpdateAfterBind = true, + .descriptorBindingStorageImageUpdateAfterBind = true, + .descriptorBindingStorageBufferUpdateAfterBind = true, + .descriptorBindingUniformTexelBufferUpdateAfterBind = true, + .descriptorBindingStorageTexelBufferUpdateAfterBind = true, + .descriptorBindingUpdateUnusedWhilePending = true, + .descriptorBindingPartiallyBound = true, + .descriptorBindingVariableDescriptorCount = true, + .runtimeDescriptorArray = true, + .timelineSemaphore = true, + .synchronization2 = true, + .dynamicRendering = true, + .dynamicRenderingLocalRead = true, +}; + +/** + * @brief features that a device with a present queue enables + */ +inline constexpr DeviceFeatures PRESENT_DEVICE_FEATURES{.swapchainMaintenance1 = true}; + +/** + * @brief extensions without feature bits that a device with a present queue enables + */ +inline constexpr std::array PRESENT_DEVICE_EXTENSIONS{ + VK_KHR_SWAPCHAIN_EXTENSION_NAME, +}; + +/** + * @brief instance extensions that a device with a present queue requires, besides the platform surface extension + */ +inline constexpr std::array PRESENT_INSTANCE_EXTENSIONS{ + VK_KHR_SURFACE_EXTENSION_NAME, + VK_KHR_GET_SURFACE_CAPABILITIES_2_EXTENSION_NAME, + VK_KHR_SURFACE_MAINTENANCE_1_EXTENSION_NAME, +}; +} diff --git a/cvk/incl/cvk/utility/constant/os_constants.hpp b/cvk/incl/cvk/utility/constant/os_constants.hpp new file mode 100644 index 00000000..40eee19b --- /dev/null +++ b/cvk/incl/cvk/utility/constant/os_constants.hpp @@ -0,0 +1,31 @@ +#pragma once +#include + +#include + +#include +#include + +namespace cvk::constants { +/** + * @brief handle type of @ref Fence::extractOsHandle() + * @details windows: opaque win32 handle, posix: sync fd, pollable + */ +inline constexpr VkExternalFenceHandleTypeFlagBits OS_FENCE_HANDLE_TYPE = +#ifdef CTH_PLATFORM_WINDOWS + VK_EXTERNAL_FENCE_HANDLE_TYPE_OPAQUE_WIN32_BIT; +#else + VK_EXTERNAL_FENCE_HANDLE_TYPE_SYNC_FD_BIT; +#endif + +/** + * @brief device extensions that @ref Fence::extractOsHandle() requires + */ +inline constexpr std::array OS_FENCE_DEVICE_EXTENSIONS{ +#ifdef CTH_PLATFORM_WINDOWS + "VK_KHR_external_fence_win32", +#else + VK_KHR_EXTERNAL_FENCE_FD_EXTENSION_NAME, +#endif +}; +} diff --git a/cvk/incl/cvk/utility/constants.hpp b/cvk/incl/cvk/utility/constants.hpp new file mode 100644 index 00000000..e3680c1f --- /dev/null +++ b/cvk/incl/cvk/utility/constants.hpp @@ -0,0 +1,16 @@ +#pragma once +// ReSharper disable CppUnusedIncludeDirective +#include "constant/debug_constants.hpp" +// ReSharper restore CppUnusedIncludeDirective + +#include + +namespace cvk { +struct create_t {}; + +inline constexpr create_t create{}; +} + +namespace cvk::constants { +constexpr bool ENABLE_VALIDATION_LAYERS = DEBUG_MODE; +} diff --git a/cvk/incl/cvk/utility/device/device_features.hpp b/cvk/incl/cvk/utility/device/device_features.hpp new file mode 100644 index 00000000..b1695cdd --- /dev/null +++ b/cvk/incl/cvk/utility/device/device_features.hpp @@ -0,0 +1,206 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include + + +namespace cvk { +/** + * @brief feature bits of a device, one bool per Vulkan feature + * @details the fields follow the Vulkan feature structs in order, see @ref dev::feature_structs, + * a true bit of an extension struct enables its extension, see @ref dev::extensions_of() + */ +struct DeviceFeatures { + // Vulkan 1.0, VkPhysicalDeviceFeatures + bool robustBufferAccess = false; + bool fullDrawIndexUint32 = false; + bool imageCubeArray = false; + bool independentBlend = false; + bool geometryShader = false; + bool tessellationShader = false; + bool sampleRateShading = false; + bool dualSrcBlend = false; + bool logicOp = false; + bool multiDrawIndirect = false; + bool drawIndirectFirstInstance = false; + bool depthClamp = false; + bool depthBiasClamp = false; + bool fillModeNonSolid = false; + bool depthBounds = false; + bool wideLines = false; + bool largePoints = false; + bool alphaToOne = false; + bool multiViewport = false; + bool samplerAnisotropy = false; + bool textureCompressionETC2 = false; + bool textureCompressionASTC_LDR = false; + bool textureCompressionBC = false; + bool occlusionQueryPrecise = false; + bool pipelineStatisticsQuery = false; + bool vertexPipelineStoresAndAtomics = false; + bool fragmentStoresAndAtomics = false; + bool shaderTessellationAndGeometryPointSize = false; + bool shaderImageGatherExtended = false; + bool shaderStorageImageExtendedFormats = false; + bool shaderStorageImageMultisample = false; + bool shaderStorageImageReadWithoutFormat = false; + bool shaderStorageImageWriteWithoutFormat = false; + bool shaderUniformBufferArrayDynamicIndexing = false; + bool shaderSampledImageArrayDynamicIndexing = false; + bool shaderStorageBufferArrayDynamicIndexing = false; + bool shaderStorageImageArrayDynamicIndexing = false; + bool shaderClipDistance = false; + bool shaderCullDistance = false; + bool shaderFloat64 = false; + bool shaderInt64 = false; + bool shaderInt16 = false; + bool shaderResourceResidency = false; + bool shaderResourceMinLod = false; + bool sparseBinding = false; + bool sparseResidencyBuffer = false; + bool sparseResidencyImage2D = false; + bool sparseResidencyImage3D = false; + bool sparseResidency2Samples = false; + bool sparseResidency4Samples = false; + bool sparseResidency8Samples = false; + bool sparseResidency16Samples = false; + bool sparseResidencyAliased = false; + bool variableMultisampleRate = false; + bool inheritedQueries = false; + + // Vulkan 1.1, VkPhysicalDeviceVulkan11Features + bool storageBuffer16BitAccess = false; + bool uniformAndStorageBuffer16BitAccess = false; + bool storagePushConstant16 = false; + bool storageInputOutput16 = false; + bool multiview = false; + bool multiviewGeometryShader = false; + bool multiviewTessellationShader = false; + bool variablePointersStorageBuffer = false; + bool variablePointers = false; + bool protectedMemory = false; + bool samplerYcbcrConversion = false; + bool shaderDrawParameters = false; + + // Vulkan 1.2, VkPhysicalDeviceVulkan12Features + bool samplerMirrorClampToEdge = false; + bool drawIndirectCount = false; + bool storageBuffer8BitAccess = false; + bool uniformAndStorageBuffer8BitAccess = false; + bool storagePushConstant8 = false; + bool shaderBufferInt64Atomics = false; + bool shaderSharedInt64Atomics = false; + bool shaderFloat16 = false; + bool shaderInt8 = false; + bool descriptorIndexing = false; + bool shaderInputAttachmentArrayDynamicIndexing = false; + bool shaderUniformTexelBufferArrayDynamicIndexing = false; + bool shaderStorageTexelBufferArrayDynamicIndexing = false; + bool shaderUniformBufferArrayNonUniformIndexing = false; + bool shaderSampledImageArrayNonUniformIndexing = false; + bool shaderStorageBufferArrayNonUniformIndexing = false; + bool shaderStorageImageArrayNonUniformIndexing = false; + bool shaderInputAttachmentArrayNonUniformIndexing = false; + bool shaderUniformTexelBufferArrayNonUniformIndexing = false; + bool shaderStorageTexelBufferArrayNonUniformIndexing = false; + bool descriptorBindingUniformBufferUpdateAfterBind = false; + bool descriptorBindingSampledImageUpdateAfterBind = false; + bool descriptorBindingStorageImageUpdateAfterBind = false; + bool descriptorBindingStorageBufferUpdateAfterBind = false; + bool descriptorBindingUniformTexelBufferUpdateAfterBind = false; + bool descriptorBindingStorageTexelBufferUpdateAfterBind = false; + bool descriptorBindingUpdateUnusedWhilePending = false; + bool descriptorBindingPartiallyBound = false; + bool descriptorBindingVariableDescriptorCount = false; + bool runtimeDescriptorArray = false; + bool samplerFilterMinmax = false; + bool scalarBlockLayout = false; + bool imagelessFramebuffer = false; + bool uniformBufferStandardLayout = false; + bool shaderSubgroupExtendedTypes = false; + bool separateDepthStencilLayouts = false; + bool hostQueryReset = false; + bool timelineSemaphore = false; + bool bufferDeviceAddress = false; + bool bufferDeviceAddressCaptureReplay = false; + bool bufferDeviceAddressMultiDevice = false; + bool vulkanMemoryModel = false; + bool vulkanMemoryModelDeviceScope = false; + bool vulkanMemoryModelAvailabilityVisibilityChains = false; + bool shaderOutputViewportIndex = false; + bool shaderOutputLayer = false; + bool subgroupBroadcastDynamicId = false; + + // Vulkan 1.3, VkPhysicalDeviceVulkan13Features + bool robustImageAccess = false; + bool inlineUniformBlock = false; + bool descriptorBindingInlineUniformBlockUpdateAfterBind = false; + bool pipelineCreationCacheControl = false; + bool privateData = false; + bool shaderDemoteToHelperInvocation = false; + bool shaderTerminateInvocation = false; + bool subgroupSizeControl = false; + bool computeFullSubgroups = false; + bool synchronization2 = false; + bool textureCompressionASTC_HDR = false; + bool shaderZeroInitializeWorkgroupMemory = false; + bool dynamicRendering = false; + bool shaderIntegerDotProduct = false; + bool maintenance4 = false; + + // VK_KHR_swapchain_maintenance1, VkPhysicalDeviceSwapchainMaintenance1FeaturesKHR + bool swapchainMaintenance1 = false; + + // VK_KHR_dynamic_rendering_local_read, VkPhysicalDeviceDynamicRenderingLocalReadFeaturesKHR + bool dynamicRenderingLocalRead = false; + + /** + * @brief enables every feature of other + */ + constexpr DeviceFeatures& merge(DeviceFeatures const& other); + + /** + * @return names of the features that required enables and this does not + */ + [[nodiscard]] std::vector missing(DeviceFeatures const& required) const; + + [[nodiscard]] constexpr bool operator==(DeviceFeatures const&) const = default; +}; +} + +namespace cvk::dev { +/** + * @brief feature bits in @ref DeviceFeatures, every field is a bool without padding + */ +inline constexpr size_t FEATURE_COUNT = sizeof(DeviceFeatures); + +using feature_bits_t = std::array; + +/** + * @return field i of the features as element i + */ +[[nodiscard]] constexpr feature_bits_t to_bits(DeviceFeatures const& features) { + return std::bit_cast(features); +} + +/** + * @return element i of the bits as field i + */ +[[nodiscard]] constexpr DeviceFeatures to_features(feature_bits_t const& bits) { + return std::bit_cast(bits); +} +} + +namespace cvk { +constexpr DeviceFeatures& DeviceFeatures::merge(DeviceFeatures const& other) { + auto bits = dev::to_bits(*this); + std::ranges::transform(bits, dev::to_bits(other), bits.begin(), std::logical_or{}); + + *this = dev::to_features(bits); + return *this; +} +} diff --git a/cvk/incl/cvk/utility/device/feature_chain.hpp b/cvk/incl/cvk/utility/device/feature_chain.hpp new file mode 100644 index 00000000..3c94cd2f --- /dev/null +++ b/cvk/incl/cvk/utility/device/feature_chain.hpp @@ -0,0 +1,214 @@ +#pragma once +#include "device_features.hpp" + +#include "cvk/utility/types.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +namespace cvk::dev { +/** + * @brief the Vulkan feature structs of @ref DeviceFeatures, in the order of its fields + * @details a new struct: an entry here, a @ref feature_struct specialization, its fields in @ref DeviceFeatures + */ +using feature_structs_t = std::tuple< + VkPhysicalDeviceFeatures2, + VkPhysicalDeviceVulkan11Features, + VkPhysicalDeviceVulkan12Features, + VkPhysicalDeviceVulkan13Features, + VkPhysicalDeviceSwapchainMaintenance1FeaturesKHR, + VkPhysicalDeviceDynamicRenderingLocalReadFeaturesKHR +>; + +inline constexpr size_t FEATURE_STRUCTS = std::tuple_size_v; + +/** + * @brief sType, VkBool32 flags and extension of a feature struct + * @details OFFSET: byte offset of the first flag, COUNT: flags, EXTENSION: empty -> core struct + */ +template +struct feature_struct; + +template<> +struct feature_struct { + static constexpr auto TYPE = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; + static constexpr size_t OFFSET = offsetof(VkPhysicalDeviceFeatures2, features); + static constexpr size_t COUNT = sizeof(VkPhysicalDeviceFeatures) / sizeof(VkBool32); + static constexpr std::string_view EXTENSION{}; +}; + +template<> +struct feature_struct { + static constexpr auto TYPE = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_FEATURES; + static constexpr size_t OFFSET = offsetof(VkPhysicalDeviceVulkan11Features, storageBuffer16BitAccess); + static constexpr size_t COUNT = + (offsetof(VkPhysicalDeviceVulkan11Features, shaderDrawParameters) - OFFSET) / sizeof(VkBool32) + 1; + static constexpr std::string_view EXTENSION{}; +}; + +template<> +struct feature_struct { + static constexpr auto TYPE = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES; + static constexpr size_t OFFSET = offsetof(VkPhysicalDeviceVulkan12Features, samplerMirrorClampToEdge); + static constexpr size_t COUNT = + (offsetof(VkPhysicalDeviceVulkan12Features, subgroupBroadcastDynamicId) - OFFSET) / sizeof(VkBool32) + 1; + static constexpr std::string_view EXTENSION{}; +}; + +template<> +struct feature_struct { + static constexpr auto TYPE = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES; + static constexpr size_t OFFSET = offsetof(VkPhysicalDeviceVulkan13Features, robustImageAccess); + static constexpr size_t COUNT = + (offsetof(VkPhysicalDeviceVulkan13Features, maintenance4) - OFFSET) / sizeof(VkBool32) + 1; + static constexpr std::string_view EXTENSION{}; +}; + +template<> +struct feature_struct { + static constexpr auto TYPE = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SWAPCHAIN_MAINTENANCE_1_FEATURES_KHR; + static constexpr size_t OFFSET = offsetof(VkPhysicalDeviceSwapchainMaintenance1FeaturesKHR, swapchainMaintenance1); + static constexpr size_t COUNT = 1; + static constexpr std::string_view EXTENSION = VK_KHR_SWAPCHAIN_MAINTENANCE_1_EXTENSION_NAME; +}; + +template<> +struct feature_struct { + static constexpr auto TYPE = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_LOCAL_READ_FEATURES_KHR; + static constexpr size_t OFFSET = + offsetof(VkPhysicalDeviceDynamicRenderingLocalReadFeaturesKHR, dynamicRenderingLocalRead); + static constexpr size_t COUNT = 1; + static constexpr std::string_view EXTENSION = VK_KHR_DYNAMIC_RENDERING_LOCAL_READ_EXTENSION_NAME; +}; + +static_assert(feature_struct::COUNT == 55); +static_assert(feature_struct::COUNT == 12); +static_assert(feature_struct::COUNT == 47); +static_assert(feature_struct::COUNT == 15); + +/** + * @brief index of the first field of T in @ref DeviceFeatures + */ +template +inline constexpr size_t feature_offset = [](std::type_identity>) { + size_t offset = 0; + bool found = false; + ((found = found || std::same_as, offset += found ? 0 : feature_struct::COUNT), ...); + return offset; +}(std::type_identity{}); + +static_assert( + [](std::type_identity>) { + return (feature_struct::COUNT + ...); + }(std::type_identity{}) == FEATURE_COUNT, + "DeviceFeatures must have one field per flag of the feature structs" +); +static_assert(alignof(DeviceFeatures) == 1, "DeviceFeatures must have bool fields only"); + +/** + * @return the extensions of the structs with a true bit in the features + */ +[[nodiscard]] constexpr std::vector extensions_of(DeviceFeatures const& features) { + auto const bits = to_bits(features); + std::vector extensions{}; + + [&](std::type_identity>) { + ([&] { + if constexpr(!feature_struct::EXTENSION.empty()) { + auto const flags = std::span{bits}.subspan(feature_offset, feature_struct::COUNT); + if(std::ranges::contains(flags, true)) extensions.push_back(feature_struct::EXTENSION); + } + }(), ...); + }(std::type_identity{}); + + return extensions; +} + +/** + * @brief the feature structs as one pNext chain for Vulkan, not copyable, not movable: the chain points into the object + * @details an extension struct is in the chain only with its extension + */ +class FeatureChain { +public: + /** + * @brief every feature false + * @param extensions of the device, the chain includes their structs + */ + explicit FeatureChain(std::span extensions); + + /** + * @brief the bits of the features + * @param extensions of the device, the chain includes their structs + * @pre the extensions contain @ref extensions_of() the features + */ + FeatureChain(DeviceFeatures const&, std::span extensions); + + ~FeatureChain() = default; + + /** + * @return the bits of the structs, a struct out of the chain -> false + */ + [[nodiscard]] DeviceFeatures features() const; + + /** + * @return the struct is in the chain + * @tparam T one of @ref feature_structs_t + */ + template + [[nodiscard]] constexpr bool linked() const; + + /** + * @brief start of the pNext chain + */ + [[nodiscard]] constexpr auto* get(this auto& self) { return &std::get(self._structs); } + +private: + /** + * @brief links the pNext members of the structs in @ref _linked + */ + void link(); + + feature_structs_t _structs; + std::array _linked{}; + +public: + FeatureChain(FeatureChain const& other) = delete; + FeatureChain(FeatureChain&& other) = delete; + FeatureChain& operator=(FeatureChain const& other) = delete; + FeatureChain& operator=(FeatureChain&& other) = delete; +}; + +/** + * @brief queries the features of the device + * @param extensions of the device, the query includes their structs + */ +[[nodiscard]] DeviceFeatures query_features(vk_not_null, std::span extensions); +} + +namespace cvk::dev { +/** + * @brief position of T in @ref feature_structs_t + */ +template +inline constexpr size_t feature_index = [](std::type_identity>) { + constexpr std::array same{std::same_as...}; + return static_cast(std::ranges::find(same, true) - same.begin()); +}(std::type_identity{}); + +template +constexpr bool FeatureChain::linked() const { + static_assert(feature_index < FEATURE_STRUCTS, "T must be a feature struct"); + return _linked[feature_index]; +} +} diff --git a/cvk/incl/cvk/utility/enumerate.hpp b/cvk/incl/cvk/utility/enumerate.hpp new file mode 100644 index 00000000..5608afd5 --- /dev/null +++ b/cvk/incl/cvk/utility/enumerate.hpp @@ -0,0 +1,84 @@ +#pragma once +#include "cvk/utility/vk_exceptions.hpp" + +#include + +#include +#include +#include +#include +#include + + +namespace cvk::dev { +/** + * @brief element type of a two-call query: the pointee of its last parameter + */ +template +struct enumerate_element {}; + +template +struct enumerate_element { + using type = std::remove_pointer_t>>; +}; + +template +using enumerate_element_t = enumerate_element::type; + +/** + * @brief max count and write rounds of @ref enumerate() while the result is VK_INCOMPLETE + */ +constexpr uint32_t MAX_QUERY_ATTEMPTS = 4; + +/** + * @brief calls a two-call query as fn(args..., &count, data), first for the count, then for the elements + * @param what name of the elements for the error message + * @param fn function pointer: a volk global or a VolkDeviceTable member + * @param args arguments before the count + * @details VkResult fn -> repeats both calls while the result is VK_INCOMPLETE, + * at most @ref MAX_QUERY_ATTEMPTS rounds, + * void fn -> one count and one write call + * @return the elements, as many as the last call wrote + * @throws cvk::vk_result_exception result of fn other than VK_SUCCESS or VK_INCOMPLETE + * @throws cvk::vk_result_exception VK_INCOMPLETE in all @ref MAX_QUERY_ATTEMPTS rounds + */ +template +[[nodiscard]] std::vector> enumerate( + [[maybe_unused]] std::string_view what, + Fn fn, + Args const&... args +) { + using element_t = enumerate_element_t; + + std::vector elements{}; + uint32_t count = 0; + + if constexpr(std::is_void_v>) { + fn(args..., &count, nullptr); + elements.resize(count); + fn(args..., &count, elements.data()); + } else { + VkResult result = VK_INCOMPLETE; + for(uint32_t attempt = 0; attempt < MAX_QUERY_ATTEMPTS && result == VK_INCOMPLETE; ++attempt) { + auto const countResult = fn(args..., &count, nullptr); + CVK_RESULT_STABLE_THROW(countResult != VK_SUCCESS, countResult, "failed to query {}", what) {} + // data() of an empty vector can be nullptr, a second count call could return a new count + if(count == 0) return {}; + + elements.resize(count); + result = fn(args..., &count, elements.data()); + } + CVK_RESULT_STABLE_THROW( + result == VK_INCOMPLETE, + result, + "failed to query {}, the count changed on every attempt, got {} attempts", + what, + MAX_QUERY_ATTEMPTS + ) {} + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to query {}", what) {} + } + + elements.resize(count); + return elements; +} +} diff --git a/cvk/incl/cvk/utility/format.hpp b/cvk/incl/cvk/utility/format.hpp new file mode 100644 index 00000000..2c3e0c60 --- /dev/null +++ b/cvk/incl/cvk/utility/format.hpp @@ -0,0 +1,34 @@ +#pragma once +// ReSharper disable CppClangTidyCertDcl58Cpp +#include +#include + +#include + +#include +#include + + +CTH_FORMAT_ALIAS(VkResult, std::string_view, string_VkResult); + +CTH_FORMAT_ALIAS(VkFormat, std::string_view, string_VkFormat); + +CTH_FORMAT_ALIAS(VkStructureType, std::string_view, string_VkStructureType); + +CTH_FORMAT_ALIAS(VkDescriptorType, std::string_view, string_VkDescriptorType); + +CTH_FORMAT_ALIAS(VkColorSpaceKHR, std::string_view, string_VkColorSpaceKHR); + +CTH_FORMAT_ALIAS(VkPresentModeKHR, std::string_view, string_VkPresentModeKHR); + +CTH_FORMAT_ALIAS(VkObjectType, std::string_view, string_VkObjectType); + +CTH_FORMAT_ALIAS(VkImageLayout, std::string_view, string_VkImageLayout); + +CTH_FORMAT_CLASS( + VkSurfaceFormatKHR, + "{}, {}", + [](VkSurfaceFormatKHR const& f) { return std::tie(f.format, f.colorSpace); } +); + +CTH_FORMAT_CLASS(VkExtent2D, "{}, {}", [](VkExtent2D const& e) { return std::tie(e.width, e.height); }); diff --git a/cvk/incl/cvk/utility/handle/core_handle.hpp b/cvk/incl/cvk/utility/handle/core_handle.hpp new file mode 100644 index 00000000..2e6511f3 --- /dev/null +++ b/cvk/incl/cvk/utility/handle/core_handle.hpp @@ -0,0 +1,34 @@ +#pragma once +#include "unique_handle.hpp" + +#include "cvk/base/core.hpp" +#include "cvk/utility/types.hpp" + + +namespace cvk { +/** + * @brief deleter of device objects, context: the core + * @details calls T::destroy(core.deviceTable(), handle) + * @pre the core outlives the deleter + */ +template +class core_deleter { +public: + constexpr explicit core_deleter(Core const& core) : _core{&core} {} + + template + constexpr void operator()(Handle handle) const { T::destroy(_core->deviceTable(), handle); } + + [[nodiscard]] constexpr Core const& core() const { return *_core; } + +private: + not_null_ptr _core; +}; + +/** + * @brief @ref unique_handle of a device object, the deleter stores the core + * @tparam T owner class with vk_handle_t and a static destroy(DeviceTable, vk_handle_t) + */ +template +using core_handle = unique_handle>; +} diff --git a/cvk/incl/cvk/utility/handle/instance_handle.hpp b/cvk/incl/cvk/utility/handle/instance_handle.hpp new file mode 100644 index 00000000..34efd2b9 --- /dev/null +++ b/cvk/incl/cvk/utility/handle/instance_handle.hpp @@ -0,0 +1,35 @@ +#pragma once +#include "unique_handle.hpp" + +#include "cvk/utility/types.hpp" + +#include + + +namespace cvk { +/** + * @brief deleter of instance objects, context: the VkInstance + * @details calls T::destroy(vkInstance, handle), + * stores the handle, not the Instance: the instance owns its messenger and can move + */ +template +class instance_deleter { +public: + constexpr explicit instance_deleter(VkInstance vkInstance) : _vkInstance{vkInstance} {} + + template + constexpr void operator()(Handle handle) const { T::destroy(_vkInstance.get(), handle); } + + [[nodiscard]] constexpr VkInstance vkInstance() const { return _vkInstance.get(); } + +private: + vk_not_null _vkInstance; +}; + +/** + * @brief @ref unique_handle of an instance object, the deleter stores the VkInstance + * @tparam T owner class with vk_handle_t and a static destroy(VkInstance, vk_handle_t) + */ +template +using instance_handle = unique_handle>; +} diff --git a/cvk/incl/cvk/utility/handle/unique_handle.hpp b/cvk/incl/cvk/utility/handle/unique_handle.hpp new file mode 100644 index 00000000..dd521196 --- /dev/null +++ b/cvk/incl/cvk/utility/handle/unique_handle.hpp @@ -0,0 +1,113 @@ +#pragma once +#include "cvk/utility/types.hpp" + +#include + +#include +#include +#include +#include + + +namespace cvk { +/** + * @brief T names the Vulkan handle it holds: using vk_handle_t = VkX + */ +template +concept vk_handle_owner = requires { typename T::vk_handle_t; }; + +/** + * @brief deleter without context, calls T::destroy(handle) + */ +template +struct handle_deleter { + template + constexpr void operator()(Handle handle) const { T::destroy(handle); } +}; + +/** + * @brief owning Vulkan handle, std::unique_ptr with a deleter that holds the destroy context + * @tparam Handle Vulkan handle, or Deleter::pointer for handles that are not pointers + * @tparam Deleter @ref core_deleter, @ref vma_deleter, @ref instance_deleter, @ref handle_deleter, + * a deleter derived from them or one of its own + * @details a handle comes only together with its context: the constructors and reset() construct the deleter, + * a missing context argument does not compile, + * move assignment destroys the old handle with the old context, then takes handle and context, + * the size is the size of the handle plus the size of the context + */ +template +class unique_handle { + using ptr_t = std::unique_ptr, Deleter>; + static_assert(std::same_as, "Handle must be a pointer or Deleter::pointer"); + +public: + using handle_t = Handle; + using deleter_t = Deleter; + + /** + * @brief owns no handle + * @param context to construct the deleter + */ + template requires std::constructible_from + constexpr explicit unique_handle(Args&&... context) : _ptr{Handle{}, Deleter{std::forward(context)...}} {} + + /** + * @brief owns @ref handle + * @param context to construct the deleter + */ + template requires std::constructible_from + constexpr explicit unique_handle(Handle handle, Args&&... context) : + _ptr{handle, Deleter{std::forward(context)...}} {} + + constexpr ~unique_handle() = default; + + /** + * @brief destroys the owned handle, keeps the context + */ + constexpr void reset() noexcept { _ptr.reset(); } + + /** + * @brief destroys the owned handle with the old context, then owns @ref handle with the new context + * @param context to construct the deleter + */ + template requires std::constructible_from + constexpr void reset(Handle handle, Args&&... context) noexcept { + _ptr.reset(); + _ptr.get_deleter() = Deleter{std::forward(context)...}; + _ptr.reset(handle); + } + + /** + * @brief gives up the handle without destroying, keeps the context + */ + [[nodiscard]] constexpr Handle release() noexcept { return _ptr.release(); } + + [[nodiscard]] constexpr Handle get() const noexcept { return _ptr.get(); } + [[nodiscard]] constexpr explicit operator bool() const noexcept { return static_cast(_ptr); } + + [[nodiscard]] constexpr Deleter const& deleter() const noexcept { return _ptr.get_deleter(); } + + /** + * @pre the deleter has core() + */ + [[nodiscard]] constexpr decltype(auto) core() const requires requires(Deleter const& d) { d.core(); } { + return deleter().core(); + } + + /** + * @pre the deleter has vkInstance() + */ + [[nodiscard]] constexpr VkInstance vkInstance() const requires requires(Deleter const& d) { d.vkInstance(); } { + return deleter().vkInstance(); + } + +private: + ptr_t _ptr; + +public: + unique_handle(unique_handle const& other) = delete; + constexpr unique_handle(unique_handle&& other) noexcept = default; + unique_handle& operator=(unique_handle const& other) = delete; + constexpr unique_handle& operator=(unique_handle&& other) noexcept = default; +}; +} diff --git a/cvk/incl/cvk/utility/handle/vma_handle.hpp b/cvk/incl/cvk/utility/handle/vma_handle.hpp new file mode 100644 index 00000000..1789bdec --- /dev/null +++ b/cvk/incl/cvk/utility/handle/vma_handle.hpp @@ -0,0 +1,33 @@ +#pragma once +#include "core_handle.hpp" +#include "unique_handle.hpp" + +#include "cvk/base/allocator.hpp" +#include "cvk/base/core.hpp" + +#include +#include + + +namespace cvk { +/** + * @brief deleter of memory objects, context: the core + * @details calls T::destroy(core.allocator().get(), handle) + * @pre the core outlives the deleter + */ +template +class vma_deleter : public core_deleter { +public: + using core_deleter::core_deleter; + + template + constexpr void operator()(Handle handle) const { T::destroy(this->core().allocator().get(), handle); } +}; + +/** + * @brief @ref unique_handle of a memory object, the deleter stores the core and frees through its allocator + * @tparam T owner class with vk_handle_t and a static destroy(VmaAllocator, vk_handle_t) + */ +template +using vma_handle = unique_handle>; +} diff --git a/cvk/incl/cvk/utility/range.hpp b/cvk/incl/cvk/utility/range.hpp new file mode 100644 index 00000000..3b5c9fe2 --- /dev/null +++ b/cvk/incl/cvk/utility/range.hpp @@ -0,0 +1,20 @@ +#pragma once +#include + +#include + + +namespace cvk { +/** + * @brief offset and size of a part of a buffer or a memory + * @details in bytes, in elements for a typed buffer + */ +struct Range { + size_t offset = 0; + + /** + * @brief (VK_WHOLE_SIZE => to the end) + */ + size_t size = VK_WHOLE_SIZE; +}; +} diff --git a/cvk/incl/cvk/utility/types.hpp b/cvk/incl/cvk/utility/types.hpp new file mode 100644 index 00000000..95c6a600 --- /dev/null +++ b/cvk/incl/cvk/utility/types.hpp @@ -0,0 +1,18 @@ +#pragma once +#include + +#include +#include + +namespace cvk { +template +using vk_not_null = cth::not_null; +template +using not_null_ptr = cth::not_null_ptr; + +template +using move_ptr = cth::move_ptr; + +template +using unique_not_null = cth::unique_not_null; +} diff --git a/cvk/incl/cvk/utility/vk_exceptions.hpp b/cvk/incl/cvk/utility/vk_exceptions.hpp new file mode 100644 index 00000000..166a6121 --- /dev/null +++ b/cvk/incl/cvk/utility/vk_exceptions.hpp @@ -0,0 +1,97 @@ +#pragma once +#include "format.hpp" + +#include + +#include +#include + + +namespace cvk { +/** + * @brief exception of cvk, the message starts with "cvk: " + */ +class cvk_exception : public cth::except::default_exception { + static constexpr auto PREFIX = "cvk: "; + +public: + cvk_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 cvk_exception(cth::except::default_exception defaultException) + : default_exception{std::move(defaultException)} { prepend(PREFIX); } +}; + +/** + * @brief exception for a vulkan call that did not return VK_SUCCESS + */ +class vk_result_exception : public cvk_exception { + static constexpr auto SEVERITY = cth::except::ERR; + static constexpr std::string_view MESSAGE_TEMPLATE = "vk error: {}"; + +public: + vk_result_exception( + std::string_view msg, + cth::except::Severity severity = SEVERITY, + std::source_location sourceLocation = {}, + std::stacktrace stacktrace = {} + ) : cvk_exception{ + std::format(MESSAGE_TEMPLATE, msg), + severity, + std::move(sourceLocation), + std::move(stacktrace) + } {} + + vk_result_exception( + VkResult result, + std::string_view msg, + std::source_location sourceLocation = {}, + std::stacktrace stacktrace = {} + ) : vk_result_exception{msg, SEVERITY, std::move(sourceLocation), std::move(stacktrace)} { add(result); } + + vk_result_exception(VkResult result, cth::except::default_exception defaultException) + : cvk_exception{std::move(defaultException)} { add(result); } + + using cvk_exception::add; + + /** + * @brief stores the result and adds it to the message + */ + vk_result_exception& add(VkResult result) { + _vkResult = result; + return cvk_exception::add("VkResult: ({})", result); + } + + [[nodiscard]] VkResult vkResult() const noexcept { return _vkResult; } + +private: + VkResult _vkResult = VK_RESULT_MAX_ENUM; +}; +} + +#define CVK_STABLE_THROW(expression, msg, ...) \ + CTH_STABLE_THROW_T(cvk::cvk_exception, expression, msg, __VA_ARGS__) + +#define CVK_VK_STABLE_THROW(expression, msg, ...) \ + CTH_STABLE_THROW_T(cvk::vk_result_exception, expression, msg, __VA_ARGS__) + +/** + * @brief acts like @ref CVK_VK_STABLE_THROW and adds the result to the exception + * @param expression true -> throws + * @param result to add to the exception + * @param msg format string + * @details braces and else work like the other CTH_ log macros + * a range for loop over a one element init list evaluates the add before the body + */ +#define CVK_RESULT_STABLE_THROW(expression, result, msg, ...)\ + CVK_VK_STABLE_THROW(expression, msg, __VA_ARGS__) \ + for(auto&& _ : {(details->e().add(result), 0)}) diff --git a/cvk/incl/cvk/utility/vk_hash.hpp b/cvk/incl/cvk/utility/vk_hash.hpp new file mode 100644 index 00000000..50e840e0 --- /dev/null +++ b/cvk/incl/cvk/utility/vk_hash.hpp @@ -0,0 +1,7 @@ +#pragma once +#include + +#include + +CTH_HASH_AGGREGATE(VkExtent3D) +CTH_HASH_AGGREGATE(VkQueueFamilyProperties) diff --git a/cvk/incl/cvk/utility/vk_overloads.hpp b/cvk/incl/cvk/utility/vk_overloads.hpp new file mode 100644 index 00000000..f5486ca6 --- /dev/null +++ b/cvk/incl/cvk/utility/vk_overloads.hpp @@ -0,0 +1,21 @@ +#pragma once +#include + +constexpr bool operator==(VkSurfaceFormatKHR const& a, VkSurfaceFormatKHR const& b) { + return a.format == b.format && a.colorSpace == b.colorSpace; +} + +constexpr bool operator==(VkExtent2D const& a, VkExtent2D const& b) { + return a.width == b.width && a.height == b.height; +} + +constexpr bool operator==(VkExtent3D const& a, VkExtent3D const& b) { + return a.width == b.width && a.height == b.height && a.depth == b.depth; +} + +constexpr bool operator==(VkQueueFamilyProperties const& a, VkQueueFamilyProperties const& b) { + return a.queueFlags == b.queueFlags + && a.queueCount == b.queueCount + && a.timestampValidBits == b.timestampValidBits + && a.minImageTransferGranularity == b.minImageTransferGranularity; +} diff --git a/cvk/src/base/allocator.cpp b/cvk/src/base/allocator.cpp new file mode 100644 index 00000000..71d96497 --- /dev/null +++ b/cvk/src/base/allocator.cpp @@ -0,0 +1,96 @@ +#include "cvk/base/allocator.hpp" + +#include "cvk/base/device.hpp" +#include "cvk/base/instance.hpp" +#include "cvk/base/physical_device.hpp" +#include "cvk/utility/vk_exceptions.hpp" + +#include +#include + + +namespace cvk { +Allocator::Allocator(Device const& device) : _device{&device} { Device::debug_check(device); } + +Allocator::Allocator(Device const& device, State const& state) : Allocator{device} { wrap(state); } + +Allocator::Allocator(Device const& device, create_t) : Allocator{device} { create(); } + +void Allocator::wrap(State const& state) { + optDestroy(); + + _handle.reset(state.vmaAllocator.get()); +} + +void Allocator::create() { + optDestroy(); + + VmaAllocatorCreateInfo info{ + .flags = flags(*_device), + .physicalDevice = _device->physicalDevice().get(), + .device = _device->get(), + .preferredLargeHeapBlockSize = 0, + .pAllocationCallbacks = nullptr, + .pDeviceMemoryCallbacks = nullptr, + .pHeapSizeLimit = nullptr, + .pVulkanFunctions = nullptr, + .instance = _device->instance().get(), + .vulkanApiVersion = VK_API_VERSION_1_3, + .pTypeExternalMemoryHandleTypes = nullptr, + }; + + VmaVulkanFunctions functions{}; + auto const importResult = vmaImportVulkanFunctionsFromVolk(&info, &functions); + CTH_STABLE_ERR(importResult != VK_SUCCESS, "failed to import the vulkan functions for VMA") { + forget(); + throw cvk::vk_result_exception{importResult, details->exception()}; + } + info.pVulkanFunctions = &functions; + + VmaAllocator ptr = VK_NULL_HANDLE; + auto const result = vmaCreateAllocator(&info, &ptr); + + CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create allocator") { + forget(); + throw cvk::vk_result_exception{result, details->exception()}; + } + + _handle.reset(ptr); +} + +void Allocator::destroy() { + debug_check(*this); + + _handle.reset(); + + forget(); +} + +Allocator::State Allocator::release() { + debug_check(*this); + + State const state{_handle.release()}; + forget(); + return state; +} + +void Allocator::destroy(VmaAllocator vmaAllocator) { + CTH_WARN(vmaAllocator == VK_NULL_HANDLE, "vmaAllocator invalid (VK_NULL_HANDLE)") {} + + vmaDestroyAllocator(vmaAllocator); +} + +VmaAllocatorCreateFlags Allocator::flags(Device const& device) { + auto const& features = device.features(); + + VmaAllocatorCreateFlags flags = 0; + if(features.bufferDeviceAddress) flags |= VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT; + if(features.maintenance4) flags |= VMA_ALLOCATOR_CREATE_KHR_MAINTENANCE4_BIT; + if(std::ranges::contains(device.extensions(), std::string_view{VK_EXT_MEMORY_BUDGET_EXTENSION_NAME})) + flags |= VMA_ALLOCATOR_CREATE_EXT_MEMORY_BUDGET_BIT; + + return flags; +} + +void Allocator::forget() {} +} diff --git a/cvk/src/base/core.cpp b/cvk/src/base/core.cpp new file mode 100644 index 00000000..55f65d4c --- /dev/null +++ b/cvk/src/base/core.cpp @@ -0,0 +1,262 @@ +#include "cvk/base/core.hpp" + +#include "cvk/base/allocator.hpp" +#include "cvk/base/device.hpp" +#include "cvk/base/instance.hpp" +#include "cvk/base/physical_device.hpp" +#include "cvk/surface/surface.hpp" +#include "cvk/utility/constant/device_constants.hpp" +#include "cvk/utility/vk_exceptions.hpp" + +#include +#include +#include + + +namespace cvk { +using std::span; +using std::vector; + +namespace { + /** + * @return the uuid as hex in the 8-4-4-4-12 form + */ + [[nodiscard]] std::string uuid_string(device_uuid_t const& uuid) { + std::string result{}; + for(auto const [index, byte] : std::views::enumerate(uuid)) { + if(index == 4 || index == 6 || index == 8 || index == 10) result += '-'; + result += std::format("{:02x}", byte); + } + return result; + } +} + +Core::Core(Instance const& instance) : _instance{&instance} {} + +Core::Core(Instance const& instance, span surfaces, Config const& config) : Core{instance} { + create(surfaces, config); +} + +Core::Core(Instance const& instance, State state) : Core{instance} { wrap(std::move(state)); } + +Core::~Core() { optDestroy(); } + +void Core::wrap(State state) { + PhysicalDevice::debug_check(*state.physicalDevice); + Device::debug_check(*state.device); + CTH_CRITICAL( + &state.device->physicalDevice() != state.physicalDevice.get(), + "device must be created with the physical device of the state" + ) {} + if(state.allocator != nullptr) { + Allocator::debug_check(*state.allocator); + CTH_CRITICAL( + &state.allocator->device() != state.device.get(), + "allocator must be created with the device of the state" + ) {} + } + CTH_CRITICAL( + std::ranges::any_of(state.queueIndices, [&](size_t index) { return index >= state.device->queues().size(); }), + "queue indices must be < queue count of the device" + ) {} + + optDestroy(); + + _physicalDevice = state.physicalDevice.release_val(); + _device = state.device.release_val(); + _allocator = std::move(state.allocator); + if(_allocator == nullptr) _allocator = std::make_unique(*_device, cvk::create); + _queueSetIndex = state.queueSetIndex; + + if(state.queueIndices.empty()) _queueIndices.assign_range(std::views::iota(0uz, _device->queues().size())); + else _queueIndices = std::move(state.queueIndices); +} + +void Core::create(span surfaces, Config const& config) { + Config::debug_check(config); + + optDestroy(); + + // the members change after the last step that can throw, a throw leaves the core not created + auto required = requirements(config); + auto pick = pickPhysicalDevice(surfaces, config, required); + auto device = std::make_unique( + *_instance, + *pick.physicalDevice, + Device::Config{ + .queueFamilyIndices = std::move(pick.uniqueQueueFamilyIndices), + .features = required.features, + .extensions = std::move(required.extensions), + } + ); + auto allocator = std::make_unique(*device, cvk::create); + + _physicalDevice = std::move(pick.physicalDevice); + _device = std::move(device); + _allocator = std::move(allocator); + _queueSetIndex = pick.queueSetIndex; + _queueIndices = std::move(pick.queuesToUniqueQueues); +} + +void Core::destroy() { + debug_check(*this); + forget(); +} + +Core::State Core::release() { + debug_check(*this); + + State state{ + .physicalDevice = std::move(_physicalDevice), + .device = std::move(_device), + .allocator = std::move(_allocator), + .queueIndices = std::move(_queueIndices), + .queueSetIndex = _queueSetIndex, + }; + forget(); + return state; +} + +size_t Core::queueSetIndex() const { + debug_check(*this); + CTH_CRITICAL(!_queueSetIndex.has_value(), "queue set index has no value, the config had no queue sets") {} + return *_queueSetIndex; +} + +VkInstance Core::vkInstance() const { return _instance->get(); } + +PhysicalDevice const& Core::physicalDevice() const { + debug_check(*this); + return *_physicalDevice; +} + +VkPhysicalDevice Core::vkPhysicalDevice() const { return physicalDevice().get(); } + +Device const& Core::device() const { + debug_check(*this); + return *_device; +} + +DeviceTable Core::deviceTable() const { return device().table(); } +VolkDeviceTable const* Core::functions() const { return device().functions(); } +VkDevice Core::vkDevice() const { return device().get(); } + +Allocator const& Core::allocator() const { + debug_check(*this); + return *_allocator; +} + +Queue const& Core::queue(size_t idx) const { return device().queues()[queueIndex(idx)]; } + +void Core::forget() { + _queueIndices.clear(); + _allocator = nullptr; + _device = nullptr; + _physicalDevice = nullptr; + _queueSetIndex = std::nullopt; +} + +auto Core::requirements(Config const& config) -> device_requirements { + device_requirements required{.features = constants::BASE_DEVICE_FEATURES, .extensions = config.extensions}; + required.features.merge(config.features); + + bool const present = std::ranges::any_of( + config.queueProperties, + [](QueueFamilyProperties properties) { return contains(properties, QueueFamilyProperties::PRESENT); } + ); + if(!present) return required; + + required.features.merge(constants::PRESENT_DEVICE_FEATURES); + for(auto const extension : constants::PRESENT_DEVICE_EXTENSIONS) + if(!std::ranges::contains(required.extensions, extension)) required.extensions.emplace_back(extension); + + return required; +} + +auto Core::pickPhysicalDevice( + span surfaces, + Config const& config, + device_requirements const& required +) const -> device_pick { + if(config.queueSets.empty()) { + auto pick = tryPickPhysicalDevice(surfaces, config, required, queue_set_t{config.queueProperties.size()}); + CVK_STABLE_THROW( + !pick && config.device.has_value(), + "physical device {} is missing or does not support the queues", + uuid_string(*config.device) + ) {} + CVK_STABLE_THROW(!pick, "no physical device supports the queues") {} + return *std::move(pick); + } + + for(auto const& [setIndex, queueSet] : std::views::enumerate(config.queueSets)) { + auto pick = tryPickPhysicalDevice(surfaces, config, required, queueSet); + if(!pick) continue; + + pick->queueSetIndex = static_cast(setIndex); + return *std::move(pick); + } + + CVK_STABLE_THROW( + config.device.has_value(), + "physical device {} is missing or does not support any queue set", + uuid_string(*config.device) + ) {} + CVK_STABLE_THROW(true, "no physical device supports any queue set") {} + std::unreachable(); +} + +auto Core::createQueueSetRelations( + queue_set_t const& queueSet, + span queueProperties +) -> queue_set_relations { + using index_type = queue_set_t::index_type; + + auto const roots = queueSet.roots(); + + vector rootToQueue(queueSet.size()); + for(auto const [root, queue] : std::views::zip(roots, std::views::iota(0uz))) + rootToQueue[root] = queue; + + vector uniqueQueues(roots.size()); + vector queueToUniqueQueue(queueProperties.size()); + + for(index_type i = 0; i < queueSet.size(); i++) { + auto const queueIdx = rootToQueue[queueSet.find(i)]; + queueToUniqueQueue[i] = queueIdx; + uniqueQueues[queueIdx] |= queueProperties[i]; + } + + return {std::move(uniqueQueues), std::move(queueToUniqueQueue)}; +} + +auto Core::tryPickPhysicalDevice( + span surfaces, + Config const& config, + device_requirements const& required, + queue_set_t const& queueSet +) const -> std::optional { + CTH_CRITICAL(queueSet.size() != config.queueProperties.size(), "queue set must span all queues") {} + + auto [uniqueQueues, queueToUniqueQueue] = createQueueSetRelations(queueSet, config.queueProperties); + + auto result = pick_physical_device( + *_instance, + surfaces, + uniqueQueues, + required.features, + required.extensions, + config.device + ); + if(!result) return std::nullopt; + + return device_pick{ + .physicalDevice = std::make_unique(std::move(result->device)), + .uniqueQueueFamilyIndices = std::move(result->queueFamilyIndices), + .queuesToUniqueQueues = std::move(queueToUniqueQueue), + }; +} + +Core::Core(Core&& other) noexcept = default; +Core& Core::operator=(Core&& other) noexcept = default; +} diff --git a/cvk/src/base/cvk.cpp b/cvk/src/base/cvk.cpp new file mode 100644 index 00000000..429f27c9 --- /dev/null +++ b/cvk/src/base/cvk.cpp @@ -0,0 +1,34 @@ +#include "cvk/base/cvk.hpp" + +#include "cvk/utility/vk_exceptions.hpp" + +#include +#include + + +namespace cvk { +Cvk::Cvk() { loader().acquire(); } + +Cvk::~Cvk() { loader().release(); } + +bool Cvk::initialized() { return loader().acquired(); } + +cth::co::global_switch& Cvk::loader() { + // constructed at the first call, also when the first call comes from the static initialization of another unit + static cth::co::global_switch loaderSwitch{&load, &unload}; + return loaderSwitch; +} + +void Cvk::load() { + auto const result = volkInitialize(); + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to load vulkan") {} + + log::msg("cvk initialized, loaded vulkan (volk)"); +} + +void Cvk::unload() { + volkFinalize(); + + log::msg("cvk terminated, unloaded vulkan (volk)"); +} +} diff --git a/cvk/src/base/device.cpp b/cvk/src/base/device.cpp new file mode 100644 index 00000000..f659d027 --- /dev/null +++ b/cvk/src/base/device.cpp @@ -0,0 +1,214 @@ +#include "cvk/base/device.hpp" + +#include "cvk/base/instance.hpp" +#include "cvk/base/physical_device.hpp" +#include "cvk/utility/device/feature_chain.hpp" +#include "cvk/utility/vk_exceptions.hpp" + +#include + +#include +#include +#include +#include + + +namespace cvk { +using std::span; +using std::vector; + +Device::Device(Instance const& instance, PhysicalDevice const& physicalDevice) : _instance{&instance}, + _physicalDevice{&physicalDevice} {} + +Device::Device(Instance const& instance, PhysicalDevice const& physicalDevice, State state) : Device{ + instance, + physicalDevice +} { wrap(std::move(state)); } + +Device::Device( + Instance const& instance, + PhysicalDevice const& physicalDevice, + Config const& config +) : Device{ + instance, + physicalDevice +} { create(config); } + +void Device::wrap(State state) { + optDestroy(); + + _functionTable = std::move(state.functionTable); + if(_functionTable == nullptr) { + _functionTable = std::make_unique(); + loadFunctionTable(state.vkDevice.get()); + } + + _handle.reset(state.vkDevice.get()); + _features = state.features; + _extensions = std::move(state.extensions); + createQueues(state.queueFamilyIndices); +} + +void Device::create(Config const& config) { + optDestroy(); + + _features = config.features; + _extensions = config.extensions; + for(auto const extension : dev::extensions_of(config.features)) + if(!std::ranges::contains(_extensions, extension)) _extensions.emplace_back(extension); + + auto const& familyIndices = config.queueFamilyIndices; + auto const queueCounts = calcQueueFamiliesQueueCounts(familyIndices); + + createLogicalDevice(queueCounts); + + createQueues(familyIndices); +} + +void Device::destroy() { + debug_check(*this); + + _handle.reset(); + forget(); +} + +Device::State Device::release() { + debug_check(*this); + + State state{ + .vkDevice = _handle.release(), + .functionTable = std::move(_functionTable), + .queueFamilyIndices = {std::from_range, _queues | std::views::transform(&Queue::familyIndex)}, + .features = _features, + .extensions = std::move(_extensions), + }; + forget(); + return state; +} + +void Device::waitIdle() const { + debug_check(*this); + + auto const result = functions()->vkDeviceWaitIdle(_handle.get()); + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to wait for device") {} +} + +void Device::destroy(VkDevice vkDevice) { + CTH_WARN(vkDevice == VK_NULL_HANDLE, "vkDevice invalid (VK_NULL_HANDLE)") {} + + auto const destroyDevice = reinterpret_cast(vkGetDeviceProcAddr(vkDevice, "vkDestroyDevice")); + destroyDevice(vkDevice, nullptr); + + log::msg("destroyed device"); +} + +void Device::forget() { + _queues.clear(); + _features = {}; + _extensions.clear(); + + if(_functionTable == nullptr) _functionTable = std::make_unique(); + std::memset(_functionTable.get(), 0, sizeof(VolkDeviceTable)); +} + +vector Device::calcQueueFamiliesQueueCounts(span familyIndices) const { + vector queueCounts(_physicalDevice->queueFamilies().size()); + + for(auto const familyIndex : familyIndices) { + CTH_CRITICAL( + familyIndex >= queueCounts.size(), + "queue family index must be less than the family count {}, got {}", + queueCounts.size(), + familyIndex + ) {} + ++queueCounts[familyIndex]; + } + + return queueCounts; +} + +void Device::createLogicalDevice(span queueFamiliesQueueCounts) { + CTH_CRITICAL( + std::ranges::all_of(queueFamiliesQueueCounts, [](uint32_t count) { return count == 0; }), + "at least one queue is required" + ) {} + CTH_CRITICAL(created(), "device must not be created") {} + + vector queueCreateInfos{}; + queueCreateInfos.reserve(queueFamiliesQueueCounts.size()); + + vector> queuePriorities{}; + queuePriorities.reserve(queueFamiliesQueueCounts.size()); + + for(auto const [queueFamily, queueCount] : std::views::zip(std::views::iota(0u), queueFamiliesQueueCounts)) { + if(queueCount == 0) continue; + + queuePriorities.emplace_back(queueCount, 1.0f); + + queueCreateInfos.push_back( + { + .sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .queueFamilyIndex = queueFamily, + .queueCount = queueCount, + .pQueuePriorities = queuePriorities.back().data() + } + ); + } + auto const extensions = cth::str::to_c_str_vector(_extensions); + dev::FeatureChain const features{_features, _extensions}; + + VkDeviceCreateInfo const createInfo{ + .sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, + .pNext = features.get(), + .flags = 0, + .queueCreateInfoCount = static_cast(queueCreateInfos.size()), + .pQueueCreateInfos = queueCreateInfos.data(), + .enabledLayerCount = 0, + .ppEnabledLayerNames = nullptr, + .enabledExtensionCount = static_cast(extensions.size()), + .ppEnabledExtensionNames = extensions.data(), + .pEnabledFeatures = nullptr, + }; + VkDevice ptr = VK_NULL_HANDLE; + + auto const createResult = vkCreateDevice(_physicalDevice->get(), &createInfo, nullptr, &ptr); + CTH_STABLE_ERR(createResult != VK_SUCCESS, "failed to create logical device") { + forget(); + throw cvk::vk_result_exception{createResult, details->exception()}; + } + + loadFunctionTable(ptr); + _handle.reset(ptr); +} + +void Device::loadFunctionTable(VkDevice vkDevice) const { volkLoadDeviceTable(_functionTable.get(), vkDevice); } + +void Device::createQueues(span familyIndices) { + vector queueCounts(_physicalDevice->queueFamilies().size()); + + _queues.reserve(familyIndices.size()); + + for(auto const familyIndex : familyIndices) { + CTH_CRITICAL( + familyIndex >= queueCounts.size(), + "queue family index must be less than the family count {}, got {}", + queueCounts.size(), + familyIndex + ) {} + auto& queueIndex = queueCounts[familyIndex]; + + VkQueue ptr = VK_NULL_HANDLE; + table()->vkGetDeviceQueue(_handle.get(), familyIndex, queueIndex, &ptr); + + // vkGetDeviceQueue() has no failure, a null queue means the device has no such queue + CTH_CRITICAL(ptr == VK_NULL_HANDLE, "the device has no queue {} in family {}", queueIndex, familyIndex) {} + + _queues.emplace_back( + _physicalDevice->queueFamily(familyIndex)->properties, + Queue::State{_functionTable.get(), ptr, familyIndex, queueIndex++} + ); + } +} +} diff --git a/cvk/src/base/instance.cpp b/cvk/src/base/instance.cpp new file mode 100644 index 00000000..d62ad8ee --- /dev/null +++ b/cvk/src/base/instance.cpp @@ -0,0 +1,190 @@ +#include "cvk/base/instance.hpp" + +#include "cvk/debug/debug_messenger.hpp" +#include "cvk/utility/enumerate.hpp" +#include "cvk/utility/vk_exceptions.hpp" + +#include + +#include +#include +#include + + +namespace cvk { +using std::string; +using std::string_view; +using std::vector; + +namespace { + /** + * @return the names that are not available + */ + [[nodiscard]] vector missing(std::ranges::range auto const& names, vector const& available) { + vector missing{}; + for(string_view const name : names) + if(!std::ranges::contains(available, name)) missing.emplace_back(name); + return missing; + } +} + +Instance::Instance(Config config) : _config{std::move(config)} {} + +Instance::Instance(Config config, State state) : Instance{std::move(config)} { wrap(std::move(state)); } + +Instance::Instance(Config config, create_t, std::optional messengerConfig) : + Instance{std::move(config)} { + create(std::move(messengerConfig)); +} + +void Instance::wrap(State state) { + optDestroy(); + + _handle.reset(state.vkInstance.get()); + _debugMessenger = std::move(state.debugMessenger); + _extensions = std::move(state.extensions); + + loadInstanceFunctions(state.vkInstance); +} + +void Instance::create(std::optional messengerConfig) { + optDestroy(); + + // the members change after the checks, a throw from them finds the members forgotten + auto requiredExtensions = _config.extensions; + if constexpr(constants::ENABLE_VALIDATION_LAYERS) { + for(string_view const extension : VALIDATION_LAYER_EXTENSIONS) + if(!std::ranges::contains(requiredExtensions, extension)) requiredExtensions.emplace_back(extension); + + if(!messengerConfig.has_value()) messengerConfig = DebugMessenger::Config{}; + } + + auto const missingExtensions = missing(requiredExtensions, available_instance_extensions()); + CTH_STABLE_ERR(!missingExtensions.empty(), "instance extensions missing") { + for(auto const& extension : missingExtensions) + details->add(extension); + throw cvk::cvk_exception{details->exception()}; + } + + if constexpr(constants::ENABLE_VALIDATION_LAYERS) { + auto const missingLayers = missing(VALIDATION_LAYERS, available_layers()); + CTH_STABLE_ERR(!missingLayers.empty(), "validation layers missing") { + for(auto const& layer : missingLayers) + details->add(layer); + throw cvk::cvk_exception{details->exception()}; + } + } + + _extensions = std::move(requiredExtensions); + auto const extensions = cth::str::to_c_str_vector(_extensions); + auto const appInfo = _config.appInfo(); + + VkInstanceCreateInfo createInfo{ + .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .pApplicationInfo = &appInfo, + .enabledLayerCount = 0, + .ppEnabledLayerNames = nullptr, + .enabledExtensionCount = static_cast(extensions.size()), + .ppEnabledExtensionNames = extensions.data(), + }; + + VkLayerSettingsCreateInfoEXT const layerSettings{ + .sType = VK_STRUCTURE_TYPE_LAYER_SETTINGS_CREATE_INFO_EXT, + .pNext = nullptr, + .settingCount = static_cast(VALIDATION_LAYER_SETTINGS.size()), + .pSettings = VALIDATION_LAYER_SETTINGS.data(), + }; + VkDebugUtilsMessengerCreateInfoEXT debugCreateInfo{}; + + if constexpr(constants::ENABLE_VALIDATION_LAYERS) { + createInfo.enabledLayerCount = static_cast(VALIDATION_LAYERS.size()); + createInfo.ppEnabledLayerNames = VALIDATION_LAYERS.data(); + + // the messenger of the create info reports the messages of vkCreateInstance() and vkDestroyInstance() + debugCreateInfo = messengerConfig->createInfo(); + debugCreateInfo.pNext = &layerSettings; + createInfo.pNext = &debugCreateInfo; + } + + VkInstance ptr = VK_NULL_HANDLE; + auto const result = vkCreateInstance(&createInfo, nullptr, &ptr); + + CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create instance") { + forget(); + throw cvk::vk_result_exception{result, details->exception()}; + } + + _handle.reset(ptr); + loadInstanceFunctions(ptr); + + if constexpr(constants::ENABLE_VALIDATION_LAYERS) { + // the messenger needs the created instance, only a catch can destroy the instance when the messenger fails + try { + _debugMessenger.emplace(*this, *messengerConfig, cvk::create); + } catch(...) { + destroy(); + throw; + } + } +} + +void Instance::destroy() { + debug_check(*this); + + _debugMessenger.reset(); + _handle.reset(); + + forget(); +} + +Instance::State Instance::release() { + debug_check(*this); + + State state{ + .vkInstance = _handle.release(), + .debugMessenger = std::move(_debugMessenger), + .extensions = std::move(_extensions), + }; + forget(); + return state; +} + +void Instance::destroy(VkInstance vkInstance) { + CTH_WARN(vkInstance == nullptr, "vkInstance invalid") {} + + vkDestroyInstance(vkInstance, nullptr); + + log::msg("destroyed instance"); +} + +void Instance::forget() { + _debugMessenger.reset(); + _extensions.clear(); +} + +void Instance::loadInstanceFunctions(vk_not_null vkInstance) { volkLoadInstanceOnly(vkInstance.get()); } +} + +namespace cvk { +vector available_instance_extensions() { + auto const properties = dev::enumerate("instance extensions", vkEnumerateInstanceExtensionProperties, nullptr); + + return { + std::from_range, + properties | std::views::transform([](VkExtensionProperties const& extension) { + return string{extension.extensionName}; + }) + }; +} + +vector available_layers() { + auto const properties = dev::enumerate("instance layers", vkEnumerateInstanceLayerProperties); + + return { + std::from_range, + properties | std::views::transform([](VkLayerProperties const& layer) { return string{layer.layerName}; }) + }; +} +} diff --git a/cvk/src/base/physical_device.cpp b/cvk/src/base/physical_device.cpp new file mode 100644 index 00000000..7091f15b --- /dev/null +++ b/cvk/src/base/physical_device.cpp @@ -0,0 +1,250 @@ +#include "cvk/base/physical_device.hpp" + +#include "cvk/base/instance.hpp" +#include "cvk/surface/surface.hpp" +#include "cvk/utility/device/feature_chain.hpp" +#include "cvk/utility/enumerate.hpp" +#include "cvk/utility/vk_exceptions.hpp" + +#include +#include + +#include +#include + + +namespace cvk { +using std::span; +using std::string; +using std::vector; + +namespace { + [[nodiscard]] vector query_extensions(vk_not_null vkDevice) { + auto const properties = dev::enumerate( + "device extensions", + vkEnumerateDeviceExtensionProperties, + vkDevice.get(), + nullptr + ); + + return { + std::from_range, + properties | std::views::transform([](VkExtensionProperties const& extension) { + return string{extension.extensionName}; + }) + }; + } + + struct queried_properties { + VkPhysicalDeviceProperties properties; + device_uuid_t uuid; + }; + + [[nodiscard]] queried_properties query_properties(vk_not_null vkDevice) { + VkPhysicalDeviceIDProperties idProperties{ + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ID_PROPERTIES, + .pNext = nullptr, + }; + VkPhysicalDeviceProperties2 properties{ + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, + .pNext = &idProperties, + }; + vkGetPhysicalDeviceProperties2(vkDevice.get(), &properties); + + queried_properties result{.properties = properties.properties, .uuid = {}}; + std::ranges::copy(idProperties.deviceUUID, result.uuid.begin()); + return result; + } + + [[nodiscard]] VkPhysicalDeviceMemoryProperties query_memory_properties(vk_not_null vkDevice) { + VkPhysicalDeviceMemoryProperties properties{}; + vkGetPhysicalDeviceMemoryProperties(vkDevice.get(), &properties); + return properties; + } + + /** + * @param surfaces a family presents only if it presents to every surface, empty -> no present support + */ + [[nodiscard]] vector query_queue_families( + span surfaces, + vk_not_null vkDevice + ) { + auto const familyProperties = dev::enumerate( + "queue families", + vkGetPhysicalDeviceQueueFamilyProperties, + vkDevice.get() + ); + + vector queueFamilies{}; + queueFamilies.reserve(familyProperties.size()); + + for(uint32_t i = 0; i < familyProperties.size(); i++) { + bool presentSupport = !surfaces.empty(); + + for(auto const& surface : surfaces) { + VkBool32 support = VK_FALSE; + auto const result = vkGetPhysicalDeviceSurfaceSupportKHR(vkDevice.get(), i, surface.get(), &support); + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "device-surface support query failed") {} + + presentSupport = presentSupport && support == VK_TRUE; + } + + queueFamilies.push_back(QueueFamily::Vk(i, familyProperties[i], presentSupport)); + } + + return queueFamilies; + } + + [[nodiscard]] VkSampleCountFlagBits max_sample_count(VkPhysicalDeviceProperties const& properties) { + VkSampleCountFlags const supportedSamples = properties.limits.framebufferColorSampleCounts + & properties.limits.framebufferDepthSampleCounts; + + constexpr std::array sampleFlagBits = { + VK_SAMPLE_COUNT_64_BIT, + VK_SAMPLE_COUNT_32_BIT, + VK_SAMPLE_COUNT_16_BIT, + VK_SAMPLE_COUNT_8_BIT, + VK_SAMPLE_COUNT_4_BIT, + VK_SAMPLE_COUNT_2_BIT, + VK_SAMPLE_COUNT_1_BIT + }; + + for(auto const sampleFlagBit : sampleFlagBits) + if(supportedSamples & sampleFlagBit) + return sampleFlagBit; + + CTH_CRITICAL(true, "invalid state, sample count not supported") {} + return VK_SAMPLE_COUNT_1_BIT; + } +} + +PhysicalDevice::PhysicalDevice(vk_not_null vkDevice, span surfaces) : + _handle{vkDevice.get()}, + _extensions{query_extensions(vkDevice)}, + _features{dev::query_features(vkDevice, _extensions)}, + _memoryProperties{query_memory_properties(vkDevice)}, + _queueFamilies{query_queue_families(surfaces, vkDevice)} { + auto const [properties, uuid] = query_properties(vkDevice); + _properties = properties; + _uuid = uuid; + _maxSampleCount = max_sample_count(_properties); +} + +vector PhysicalDevice::missing(DeviceFeatures const& features, span extensions) const { + debug_check(*this); + + vector missing{}; + for(auto const name : _features.missing(features)) + missing.emplace_back(name); + + auto const add = [&](std::string_view extension) { + if(!std::ranges::contains(_extensions, extension) && !std::ranges::contains(missing, extension)) + missing.emplace_back(extension); + }; + for(auto const& extension : extensions) + add(extension); + for(auto const extension : dev::extensions_of(features)) + add(extension); + + return missing; +} + +auto PhysicalDevice::queueFamilyIndices( + span queueProperties +) const -> vector { + debug_check(*this); + + if(queueProperties.empty()) + return {}; + + vector> queueIndices{queueProperties.size()}; + + for(auto [requiredProperties, indices] : std::views::zip(queueProperties, queueIndices)) + for(auto const& family : _queueFamilies) + if((family.properties & requiredProperties) == requiredProperties) + indices.push_back(family.index); + + vector familiesMaxQueues(_queueFamilies.size()); + + for(auto [src, dst] : std::views::zip(_queueFamilies, familiesMaxQueues)) + dst = src.vkProperties.queueCount; + + return cth::alg::assign(queueIndices, familiesMaxQueues); +} + +auto PhysicalDevice::findSupportedFormat( + span candidates, + VkImageTiling tiling, + VkFormatFeatureFlags features +) const -> VkFormat { + debug_check(*this); + + for(VkFormat const format : candidates) { + VkFormatProperties props{}; + vkGetPhysicalDeviceFormatProperties(_handle.get(), format, &props); + + if(tiling == VK_IMAGE_TILING_OPTIMAL && (props.optimalTilingFeatures & features) == features) + return format; + if(tiling == VK_IMAGE_TILING_LINEAR && (props.linearTilingFeatures & features) == features) + return format; + } + CVK_STABLE_THROW(true, "format unsupported") {} + return VK_FORMAT_MAX_ENUM; +} + +QueueFamily const* PhysicalDevice::queueFamily(queue_family_index_t idx) const { + auto const it = std::ranges::find(_queueFamilies, idx, &QueueFamily::index); + + if(it == _queueFamilies.end()) + return nullptr; + + return &*it; +} + +vector enumerate_physical_devices(Instance const& instance, span surfaces) { + Instance::debug_check(instance); + + auto const vkDevices = dev::enumerate("physical devices", vkEnumeratePhysicalDevices, instance.get()); + + return { + std::from_range, + vkDevices | std::views::transform([surfaces](VkPhysicalDevice vkDevice) { + return PhysicalDevice{vkDevice, surfaces}; + }) + }; +} + +std::optional pick_physical_device( + Instance const& instance, + span surfaces, + span queueProperties, + DeviceFeatures const& features, + span extensions, + std::optional const& uuid +) { + std::optional best{}; + + for(auto& device : enumerate_physical_devices(instance, surfaces)) { + if(uuid.has_value() && device.uuid() != *uuid) continue; + + auto const missing = device.missing(features, extensions); + auto indices = device.queueFamilyIndices(queueProperties); + + if(missing.empty() && !indices.empty()) { + auto const rank = dev::device_type_rank(device.properties().deviceType); + if(!best.has_value() || rank < dev::device_type_rank(best->device.properties().deviceType)) + best = PhysicalDevicePick{std::move(device), std::move(indices)}; + continue; + } + + cth::dt::string_joiner reasons{", "}; + if(indices.empty()) reasons += "queue families"; + for(auto const& name : missing) + reasons += name; + log::msg("physical device {} misses: {}", device.properties().deviceName, std::string{reasons}); + } + + if(best.has_value()) log::msg("chosen physical device: {}", best->device.properties().deviceName); + return best; +} +} diff --git a/cvk/src/base/queue/queue.cpp b/cvk/src/base/queue/queue.cpp new file mode 100644 index 00000000..43243952 --- /dev/null +++ b/cvk/src/base/queue/queue.cpp @@ -0,0 +1,157 @@ +#include "cvk/base/queue/queue.hpp" + +#include "cvk/render/cmd/cmd_buffer.hpp" +#include "cvk/render/sync/fence.hpp" +#include "cvk/render/sync/semaphore.hpp" +#include "cvk/render/sync/timeline_semaphore.hpp" +#include "cvk/utility/vk_exceptions.hpp" + +#include +#include +#include + + +namespace cvk { +using std::span; + +namespace { + VkSemaphoreSubmitInfo vk_semaphore_info(VkSemaphore vkSemaphore, uint64_t value, VkPipelineStageFlags2 stages) { + return VkSemaphoreSubmitInfo{ + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO, + .pNext = nullptr, + .semaphore = vkSemaphore, + .value = value, + .stageMask = stages, + .deviceIndex = 0, + }; + } + + /** + * @param resolve the value of a timeline semaphore without value + */ + template + VkSemaphoreSubmitInfo semaphore_info(SemaphoreSubmitInfo const& info, Resolve resolve) { + return std::visit( + [&](not_null_ptr semaphore) { + T::debug_check(*semaphore); + + if constexpr(std::same_as) { + auto const value = info.value.has_value() ? *info.value : resolve(*semaphore); + return vk_semaphore_info(semaphore->get(), value, info.stages); + } else { + CTH_CRITICAL(info.value.has_value(), "binary semaphores have no value") {} + return vk_semaphore_info(semaphore->get(), 0, info.stages); + } + }, + info.semaphore + ); + } +} + +Queue::Queue(QueueFamilyProperties familyProperties, State const& state) : _familyProperties{familyProperties}, + _functions{state.functions.get()}, + _handle{state.vkQueue.get()}, + _familyIndex{state.familyIndex}, + _queueIndex{state.queueIndex} {} + +void Queue::submit(span infos, Fence const* fence) const { + debug_check(*this); + if(fence != nullptr) Fence::debug_check(*fence); + + size_t cmdBufferCount = 0; + size_t semaphoreCount = 0; + for(auto const& info : infos) { + cmdBufferCount += info.cmdBuffers.size(); + semaphoreCount += info.waits.size() + info.signals.size(); + } + + // reserved: the submit infos point into the vectors + std::vector cmdBuffers{}; + cmdBuffers.reserve(cmdBufferCount); + std::vector semaphores{}; + semaphores.reserve(semaphoreCount); + std::vector submits{}; + submits.reserve(infos.size()); + + auto const waitValue = [](TimelineSemaphore const& timeline) { return timeline.value(); }; + auto const signalValue = [](TimelineSemaphore const& timeline) { return timeline.next(); }; + + for(auto const& info : infos) { + auto const* const waits = semaphores.data() + semaphores.size(); + for(auto const& wait : info.waits) + semaphores.push_back(semaphore_info(wait, waitValue)); + + auto const* const signals = semaphores.data() + semaphores.size(); + for(auto const& signal : info.signals) + semaphores.push_back(semaphore_info(signal, signalValue)); + + auto const* const buffers = cmdBuffers.data() + cmdBuffers.size(); + for(auto const& cmdBuffer : info.cmdBuffers) { + BaseCmdBuffer::debug_check(*cmdBuffer); + CTH_CRITICAL(cmdBuffer->recording(), "command buffers must end their recording first") {} + + cmdBuffers.push_back( + VkCommandBufferSubmitInfo{ + .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO, + .pNext = nullptr, + .commandBuffer = cmdBuffer->get(), + .deviceMask = 0, + } + ); + } + + submits.push_back( + VkSubmitInfo2{ + .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2, + .pNext = nullptr, + .flags = 0, + .waitSemaphoreInfoCount = static_cast(info.waits.size()), + .pWaitSemaphoreInfos = waits, + .commandBufferInfoCount = static_cast(info.cmdBuffers.size()), + .pCommandBufferInfos = buffers, + .signalSemaphoreInfoCount = static_cast(info.signals.size()), + .pSignalSemaphoreInfos = signals, + } + ); + } + + auto const result = _functions->vkQueueSubmit2( + _handle.get(), + static_cast(submits.size()), + submits.data(), + fence != nullptr ? fence->get() : VK_NULL_HANDLE + ); + + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to submit to queue") {} +} + +void Queue::rawSubmit(VkSubmitInfo2 const& submitInfo, VkFence fence) const { + debug_check(*this); + + auto const result = _functions->vkQueueSubmit2(_handle.get(), 1, &submitInfo, fence); + + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to submit to queue") {} +} + +VkResult Queue::rawPresent(VkPresentInfoKHR const& presentInfo) const { + debug_check_present(*this); + + auto const result = _functions->vkQueuePresentKHR(_handle.get(), &presentInfo); + + CVK_RESULT_STABLE_THROW( + result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR && result != VK_ERROR_OUT_OF_DATE_KHR, + result, + "failed to present" + ) {} + + return result; +} + +void Queue::waitIdle() const { + debug_check(*this); + + auto const result = _functions->vkQueueWaitIdle(_handle.get()); + + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to wait for queue") {} +} +} diff --git a/cvk/src/data/buffer/base_buffer.cpp b/cvk/src/data/buffer/base_buffer.cpp new file mode 100644 index 00000000..2af0b0a6 --- /dev/null +++ b/cvk/src/data/buffer/base_buffer.cpp @@ -0,0 +1,166 @@ +#include "cvk/data/buffer/base_buffer.hpp" + +#include "cvk/base/allocator.hpp" +#include "cvk/base/core.hpp" +#include "cvk/data/img/image.hpp" +#include "cvk/render/cmd/cmd_buffer.hpp" +#include "cvk/utility/vk_exceptions.hpp" + +#include + + +namespace cvk { +BaseBuffer::BaseBuffer(Core const& core, Config const& config) : _handle{core}, _config{config} { + Core::debug_check(core); + Config::debug_check(config); +} + +BaseBuffer::BaseBuffer(Core const& core, Config const& config, State const& state) : BaseBuffer{core, config} { + wrap(state); +} + +BaseBuffer::BaseBuffer(Core const& core, Config const& config, size_t size) : BaseBuffer{core, config} { create(size); } + +void BaseBuffer::wrap(State const& state) { + optDestroy(); + + _handle.reset(state.vkBuffer.get(), core()); + if(state.memory.has_value()) _memory.emplace(core(), _config.memory, *state.memory); + _size = state.size; +} + +void BaseBuffer::create(size_t size) { + optDestroy(); + + auto const bufferInfo = _config.createInfo(size); + auto const allocationInfo = _config.memory.createInfo(); + + VkBuffer vkBuffer = VK_NULL_HANDLE; + VmaAllocation vmaAllocation = VK_NULL_HANDLE; + auto const result = vmaCreateBuffer( + core().allocator().get(), + &bufferInfo, + &allocationInfo, + &vkBuffer, + &vmaAllocation, + nullptr + ); + + CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create buffer") { + forget(); + throw cvk::vk_result_exception{result, details->exception()}; + } + + _handle.reset(vkBuffer, core()); + _memory.emplace(core(), _config.memory, Memory::State{vmaAllocation}); + _size = size; +} + +void BaseBuffer::destroy() { + debug_check(*this); + + _handle.reset(); + _memory.reset(); + + forget(); +} + +BaseBuffer::State BaseBuffer::release() { + debug_check(*this); + + State state{.vkBuffer = _handle.release(), .size = _size, .memory = std::nullopt}; + if(_memory.has_value()) state.memory = _memory->release(); + + forget(); + return state; +} + +void BaseBuffer::copy(BaseCmdBuffer const& cmdBuffer, BaseBuffer const& src, BufferCopy region) const { + auto const [srcOffset, dstOffset, size] = region; + + debug_check(*this); + debug_check(src); + CTH_CRITICAL(!cmdBuffer.recording(), "command buffer must be recording") {} + CTH_CRITICAL( + (src._config.usage & VK_BUFFER_USAGE_TRANSFER_SRC_BIT) == 0, + "src usage must have VK_BUFFER_USAGE_TRANSFER_SRC_BIT" + ) {} + CTH_CRITICAL( + (_config.usage & VK_BUFFER_USAGE_TRANSFER_DST_BIT) == 0, + "dst usage must have VK_BUFFER_USAGE_TRANSFER_DST_BIT" + ) {} + CTH_CRITICAL(srcOffset > src._size || dstOffset > _size, "offsets must lie in the buffers") {} + + size_t const copySize = size == VK_WHOLE_SIZE ? std::min(src._size - srcOffset, _size - dstOffset) : size; + + CTH_CRITICAL(copySize == 0, "copy size must not be 0") {} + CTH_CRITICAL( + copySize > src._size - srcOffset || copySize > _size - dstOffset, + "copy range must lie in the buffers, got src {} + {} of {}, dst {} + {} of {}", + srcOffset, + copySize, + src._size, + dstOffset, + copySize, + _size + ) {} + + VkBufferCopy2 const vkRegion{ + .sType = VK_STRUCTURE_TYPE_BUFFER_COPY_2, + .pNext = nullptr, + .srcOffset = srcOffset, + .dstOffset = dstOffset, + .size = copySize, + }; + VkCopyBufferInfo2 const info{ + .sType = VK_STRUCTURE_TYPE_COPY_BUFFER_INFO_2, + .pNext = nullptr, + .srcBuffer = src.get(), + .dstBuffer = get(), + .regionCount = 1, + .pRegions = &vkRegion, + }; + core().functions()->vkCmdCopyBuffer2(cmdBuffer.get(), &info); +} + +void BaseBuffer::copy( + BaseCmdBuffer const& cmdBuffer, + Image const& src, + size_t dstOffset, + uint32_t mipLevel, + VkImageLayout layout +) const { + src.copyTo(cmdBuffer, *this, dstOffset, mipLevel, layout); +} + +Mapping BaseBuffer::map(Range range) const { + debug_check(*this); + return ownMemory().map(range); +} + +void BaseBuffer::flush(Range range) const { + debug_check(*this); + ownMemory().flush(range); +} + +void BaseBuffer::invalidate(Range range) const { + debug_check(*this); + ownMemory().invalidate(range); +} + +void BaseBuffer::write(std::span data, size_t offset) const { + debug_check(*this); + ownMemory().write(data, offset); +} + +void BaseBuffer::destroy(DeviceTable table, VkBuffer vkBuffer) { + CTH_WARN(vkBuffer == VK_NULL_HANDLE, "vkBuffer invalid (VK_NULL_HANDLE)") {} + + table->vkDestroyBuffer(table.device(), vkBuffer, nullptr); +} + +void BaseBuffer::forget() { + _memory.reset(); + _size = 0; +} +} diff --git a/cvk/src/data/descriptor/descriptor_pool.cpp b/cvk/src/data/descriptor/descriptor_pool.cpp new file mode 100644 index 00000000..da5fa673 --- /dev/null +++ b/cvk/src/data/descriptor/descriptor_pool.cpp @@ -0,0 +1,86 @@ +#include "cvk/data/descriptor/descriptor_pool.hpp" + +#include "cvk/base/core.hpp" +#include "cvk/utility/vk_exceptions.hpp" + + +namespace cvk { +DescriptorPool::DescriptorPool(Core const& core, Config config) : _handle{core}, _config{std::move(config)} { + Core::debug_check(core); + Config::debug_check(_config); +} + +DescriptorPool::DescriptorPool(Core const& core, Config config, State const& state) : DescriptorPool{ + core, + std::move(config) +} { wrap(state); } + +DescriptorPool::DescriptorPool(Core const& core, Config config, create_t) : DescriptorPool{core, std::move(config)} { + create(); +} + +void DescriptorPool::wrap(State const& state) { + optDestroy(); + + _handle.reset(state.vkPool.get(), core()); +} + +void DescriptorPool::create() { + optDestroy(); + + auto const info = _config.createInfo(); + + VkDescriptorPool ptr = VK_NULL_HANDLE; + auto const result = core().functions()->vkCreateDescriptorPool(core().vkDevice(), &info, nullptr, &ptr); + + CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create descriptor pool") { + forget(); + throw cvk::vk_result_exception{result, details->exception()}; + } + + _handle.reset(ptr, core()); +} + +void DescriptorPool::destroy() { + debug_check(*this); + CTH_CRITICAL(!unused(), "descriptor sets of the pool must be destroyed first, got {}", _sets) {} + + _handle.reset(); + + forget(); +} + +DescriptorPool::State DescriptorPool::release() { + debug_check(*this); + CTH_CRITICAL(!unused(), "descriptor sets of the pool must be destroyed first, got {}", _sets) {} + + State const state{_handle.release()}; + forget(); + return state; +} + +void DescriptorPool::reset() const { + debug_check(*this); + + auto const result = core().functions()->vkResetDescriptorPool(core().vkDevice(), get(), 0); + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to reset descriptor pool") {} + + _sets = 0; + ++_resets; +} + +void DescriptorPool::add() const { ++_sets; } + +void DescriptorPool::remove() const { + CTH_CRITICAL(unused(), "the pool counts no descriptor set") {} + --_sets; +} + +void DescriptorPool::destroy(DeviceTable table, VkDescriptorPool vkPool) { + CTH_WARN(vkPool == VK_NULL_HANDLE, "vkPool invalid (VK_NULL_HANDLE)") {} + + table->vkDestroyDescriptorPool(table.device(), vkPool, nullptr); +} + +void DescriptorPool::forget() {} +} diff --git a/cvk/src/data/descriptor/descriptor_set.cpp b/cvk/src/data/descriptor/descriptor_set.cpp new file mode 100644 index 00000000..02a31de4 --- /dev/null +++ b/cvk/src/data/descriptor/descriptor_set.cpp @@ -0,0 +1,267 @@ +#include "cvk/data/descriptor/descriptor_set.hpp" + +#include "cvk/base/core.hpp" +#include "cvk/data/buffer/base_buffer.hpp" +#include "cvk/data/img/image_view.hpp" +#include "cvk/data/img/sampler.hpp" +#include "cvk/utility/vk_exceptions.hpp" + +#include +#include +#include + + +namespace cvk { +using std::span; + +namespace { + enum class DescriptorKind { + IMAGE, + BUFFER, + UNSUPPORTED, + }; + + constexpr DescriptorKind descriptor_kind(VkDescriptorType type) { + switch(type) { + case VK_DESCRIPTOR_TYPE_SAMPLER: + case VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER: + case VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE: + case VK_DESCRIPTOR_TYPE_STORAGE_IMAGE: + case VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT: return DescriptorKind::IMAGE; + case VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER: + case VK_DESCRIPTOR_TYPE_STORAGE_BUFFER: + case VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC: + case VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC: return DescriptorKind::BUFFER; + default: return DescriptorKind::UNSUPPORTED; + } + } + + /** + * @return the layout of a view in a descriptor of the type + */ + constexpr VkImageLayout descriptor_layout(VkDescriptorType type) { + switch(type) { + case VK_DESCRIPTOR_TYPE_STORAGE_IMAGE: return VK_IMAGE_LAYOUT_GENERAL; + case VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT: return VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR; + default: return VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + } + } + + void debug_check_image(ImageView const* view, Sampler const* sampler, VkDescriptorType type) { + bool const samples = type == VK_DESCRIPTOR_TYPE_SAMPLER || type == VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + CTH_CRITICAL(samples && sampler == nullptr, "sampler descriptors need a sampler") {} + CTH_CRITICAL(!samples && sampler != nullptr, "the descriptor type takes no sampler") {} + CTH_CRITICAL(type != VK_DESCRIPTOR_TYPE_SAMPLER && view == nullptr, "image descriptors need a view") {} + if(view != nullptr) ImageView::debug_check(*view); + if(sampler != nullptr) Sampler::debug_check(*sampler); + } + + void debug_check_buffer(BaseBuffer const& buffer, Range range) { + BaseBuffer::debug_check(buffer); + CTH_CRITICAL(range.offset > buffer.size(), "offset must lie in the buffer") {} + CTH_CRITICAL( + range.size != VK_WHOLE_SIZE && range.size > buffer.size() - range.offset, + "range must lie in the buffer" + ) {} + } +} + +DescriptorSet::DescriptorSet(DescriptorPool const& pool, DescriptorSetLayout const& layout, uint32_t variableCount) : + _handle{pool}, _layout{&layout}, _variableCount{variableCount} { + DescriptorPool::debug_check(pool); + DescriptorSetLayout::debug_check(layout); + + auto const* variable = layout.variableBinding(); + CTH_CRITICAL(variableCount != 0 && variable == nullptr, "a variable count needs a variable count binding") {} + CTH_CRITICAL( + variable != nullptr && variableCount > variable->descriptorCount, + "the variable count must not exceed the maximum count of the binding" + ) {} +} + +DescriptorSet::DescriptorSet( + DescriptorPool const& pool, + DescriptorSetLayout const& layout, + uint32_t variableCount, + State const& state +) : DescriptorSet{pool, layout, variableCount} { wrap(state); } + +DescriptorSet::DescriptorSet( + DescriptorPool const& pool, + DescriptorSetLayout const& layout, + uint32_t variableCount, + create_t +) : DescriptorSet{pool, layout, variableCount} { create(); } + +void DescriptorSet::wrap(State const& state) { + optDestroy(); + + _handle.reset(state.vkSet.get(), pool()); + pool().add(); +} + +void DescriptorSet::create() { + DescriptorPool::debug_check(pool()); + CTH_CRITICAL( + _layout->updateAfterBind() && (pool().config().flags & VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT) == 0, + "update after bind layouts need a pool with VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT" + ) {} + + optDestroy(); + + auto const* variable = _layout->variableBinding(); + uint32_t const variableCount = variable != nullptr ? capacity(*variable) : 0; + VkDescriptorSetVariableDescriptorCountAllocateInfo const variableInfo{ + .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_VARIABLE_DESCRIPTOR_COUNT_ALLOCATE_INFO, + .pNext = nullptr, + .descriptorSetCount = 1, + .pDescriptorCounts = &variableCount, + }; + + VkDescriptorSetLayout const vkLayout = _layout->get(); + VkDescriptorSetAllocateInfo const info{ + .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, + .pNext = variable != nullptr ? &variableInfo : nullptr, + .descriptorPool = pool().get(), + .descriptorSetCount = 1, + .pSetLayouts = &vkLayout, + }; + + VkDescriptorSet ptr = VK_NULL_HANDLE; + auto const result = core().functions()->vkAllocateDescriptorSets(core().vkDevice(), &info, &ptr); + + CTH_STABLE_ERR(result != VK_SUCCESS, "failed to allocate descriptor set") { + forget(); + throw cvk::vk_result_exception{result, details->exception()}; + } + + _handle.reset(ptr, pool()); + pool().add(); +} + +void DescriptorSet::destroy() { + debug_check(*this); + + _handle.reset(); + + forget(); +} + +DescriptorSet::State DescriptorSet::release() { + debug_check(*this); + + State const state{_handle.release()}; + pool().remove(); + + forget(); + return state; +} + +void DescriptorSet::write(span writes) const { + debug_check(*this); + + // reserved: the writes point into the infos + std::vector imageInfos{}; + imageInfos.reserve(writes.size()); + std::vector bufferInfos{}; + bufferInfos.reserve(writes.size()); + std::vector vkWrites{}; + vkWrites.reserve(writes.size()); + + for(auto const& write : writes) { + auto const* binding = _layout->binding(write._binding); + CTH_CRITICAL(binding == nullptr, "the layout has no binding {}", write._binding) {} + + auto const type = binding->descriptorType; + auto const kind = descriptor_kind(type); + auto const* buffer = std::get_if(&write._resource); + + CTH_CRITICAL( + kind == DescriptorKind::UNSUPPORTED, + "descriptor type {} of binding {} not supported", + type, + write._binding + ) {} + CTH_CRITICAL( + (kind == DescriptorKind::BUFFER) != (buffer != nullptr), + "the kind of the write must match the type of the binding, got binding {} of type {}", + write._binding, + type + ) {} + CTH_CRITICAL( + write._element >= capacity(*binding), + "the element must lie in the binding, got element {} of binding {}", + write._element, + write._binding + ) {} + + VkWriteDescriptorSet vkWrite{ + .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, + .pNext = nullptr, + .dstSet = get(), + .dstBinding = write._binding, + .dstArrayElement = write._element, + .descriptorCount = 1, + .descriptorType = type, + .pImageInfo = nullptr, + .pBufferInfo = nullptr, + .pTexelBufferView = nullptr, + }; + + if(buffer != nullptr) { + debug_check_buffer(*buffer->buffer, buffer->range); + + auto const [offset, size] = buffer->range; + bufferInfos.push_back( + VkDescriptorBufferInfo{ + .buffer = buffer->buffer->get(), + .offset = offset, + .range = size == VK_WHOLE_SIZE ? buffer->buffer->size() - offset : size, + } + ); + vkWrite.pBufferInfo = &bufferInfos.back(); + } else { + auto const& [view, sampler, layout] = std::get(write._resource); + debug_check_image(view, sampler, type); + + imageInfos.push_back( + VkDescriptorImageInfo{ + .sampler = sampler != nullptr ? sampler->get() : VK_NULL_HANDLE, + .imageView = view != nullptr ? view->get() : VK_NULL_HANDLE, + .imageLayout = view == nullptr ? VK_IMAGE_LAYOUT_UNDEFINED + : layout != VK_IMAGE_LAYOUT_MAX_ENUM ? layout + : descriptor_layout(type), + } + ); + vkWrite.pImageInfo = &imageInfos.back(); + } + + vkWrites.push_back(vkWrite); + } + + core().functions()->vkUpdateDescriptorSets( + core().vkDevice(), + static_cast(vkWrites.size()), + vkWrites.data(), + 0, + nullptr + ); +} + +void DescriptorSet::destroy(DeviceTable table, VkDescriptorPool vkPool, VkDescriptorSet vkSet) { + CTH_WARN(vkSet == VK_NULL_HANDLE, "vkSet invalid (VK_NULL_HANDLE)") {} + + auto const result = table->vkFreeDescriptorSets(table.device(), vkPool, 1, &vkSet); + CTH_WARN(result != VK_SUCCESS, "failed to free descriptor set") {} +} + +void DescriptorSet::forget() {} + +void DescriptorSet::pool_deleter::operator()(VkDescriptorSet vkSet) const { + // a reset of the pool freed the set already + if(_resets != _pool->resets()) return; + + _pool->remove(); + if(_pool->freesSets()) destroy(_pool->core().deviceTable(), _pool->get(), vkSet); +} +} diff --git a/cvk/src/data/img/image.cpp b/cvk/src/data/img/image.cpp new file mode 100644 index 00000000..26866c6f --- /dev/null +++ b/cvk/src/data/img/image.cpp @@ -0,0 +1,274 @@ +#include "cvk/data/img/image.hpp" + +#include "cvk/base/allocator.hpp" +#include "cvk/base/core.hpp" +#include "cvk/data/buffer/base_buffer.hpp" +#include "cvk/render/cmd/cmd_buffer.hpp" +#include "cvk/render/sync/barrier.hpp" +#include "cvk/utility/vk_exceptions.hpp" + +#include +#include + + +namespace cvk { +Image::Image(Core const& core, Config const& config) : _handle{core}, _config{config} { + Core::debug_check(core); + Config::debug_check(config); +} + +Image::Image(Core const& core, Config const& config, State const& state) : Image{core, config} { wrap(state); } + +Image::Image(Core const& core, Config const& config, VkExtent2D extent) : Image{core, config} { create(extent); } + +void Image::wrap(State const& state) { + optDestroy(); + + _handle.reset(state.vkImage.get(), core()); + if(state.memory.has_value()) _memory.emplace(core(), _config.memory, *state.memory); + _extent = state.extent; +} + +void Image::create(VkExtent2D extent) { + CTH_CRITICAL( + (_config.flags & VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT) != 0 && extent.width != extent.height, + "a cube compatible image needs a square extent" + ) {} + + optDestroy(); + + auto const imageInfo = _config.createInfo(extent); + auto const allocationInfo = _config.memory.createInfo(); + + VkImage vkImage = VK_NULL_HANDLE; + VmaAllocation vmaAllocation = VK_NULL_HANDLE; + auto const result = vmaCreateImage( + core().allocator().get(), + &imageInfo, + &allocationInfo, + &vkImage, + &vmaAllocation, + nullptr + ); + + CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create image") { + forget(); + throw cvk::vk_result_exception{result, details->exception()}; + } + + _handle.reset(vkImage, core()); + _memory.emplace(core(), _config.memory, Memory::State{vmaAllocation}); + _extent = extent; +} + +void Image::destroy() { + debug_check(*this); + + _handle.reset(); + _memory.reset(); + + forget(); +} + +Image::State Image::release() { + debug_check(*this); + + State state{ + .vkImage = _handle.release(), + .extent = _extent, + .memory = std::nullopt, + }; + if(_memory.has_value()) state.memory = _memory->release(); + + forget(); + return state; +} + +void Image::copy( + BaseCmdBuffer const& cmdBuffer, + BaseBuffer const& src, + size_t srcOffset, + uint32_t mipLevel, + VkImageLayout layout +) const { + auto const region = bufferCopy(cmdBuffer, src, srcOffset, mipLevel); + CTH_CRITICAL( + (src.config().usage & VK_BUFFER_USAGE_TRANSFER_SRC_BIT) == 0, + "src usage must have VK_BUFFER_USAGE_TRANSFER_SRC_BIT" + ) {} + CTH_CRITICAL( + (_config.usage & VK_IMAGE_USAGE_TRANSFER_DST_BIT) == 0, + "image usage must have VK_IMAGE_USAGE_TRANSFER_DST_BIT" + ) {} + + CTH_CRITICAL( + layout != VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL && layout != VK_IMAGE_LAYOUT_GENERAL, + "mip level must be in VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL or VK_IMAGE_LAYOUT_GENERAL, got {}", + layout + ) {} + + VkCopyBufferToImageInfo2 const info{ + .sType = VK_STRUCTURE_TYPE_COPY_BUFFER_TO_IMAGE_INFO_2, + .pNext = nullptr, + .srcBuffer = src.get(), + .dstImage = get(), + .dstImageLayout = layout, + .regionCount = 1, + .pRegions = ®ion, + }; + core().functions()->vkCmdCopyBufferToImage2(cmdBuffer.get(), &info); +} + +void Image::copyTo( + BaseCmdBuffer const& cmdBuffer, + BaseBuffer const& dst, + size_t dstOffset, + uint32_t mipLevel, + VkImageLayout layout +) const { + auto const region = bufferCopy(cmdBuffer, dst, dstOffset, mipLevel); + CTH_CRITICAL( + (dst.config().usage & VK_BUFFER_USAGE_TRANSFER_DST_BIT) == 0, + "dst usage must have VK_BUFFER_USAGE_TRANSFER_DST_BIT" + ) {} + CTH_CRITICAL( + (_config.usage & VK_IMAGE_USAGE_TRANSFER_SRC_BIT) == 0, + "image usage must have VK_IMAGE_USAGE_TRANSFER_SRC_BIT" + ) {} + + CTH_CRITICAL( + layout != VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL && layout != VK_IMAGE_LAYOUT_GENERAL, + "mip level must be in VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL or VK_IMAGE_LAYOUT_GENERAL, got {}", + layout + ) {} + + VkCopyImageToBufferInfo2 const info{ + .sType = VK_STRUCTURE_TYPE_COPY_IMAGE_TO_BUFFER_INFO_2, + .pNext = nullptr, + .srcImage = get(), + .srcImageLayout = layout, + .dstBuffer = dst.get(), + .regionCount = 1, + .pRegions = ®ion, + }; + core().functions()->vkCmdCopyImageToBuffer2(cmdBuffer.get(), &info); +} + +void Image::blitMipLevels( + BaseCmdBuffer const& cmdBuffer, + uint32_t firstMipLevel, + uint32_t mipLevels, + VkFilter filter +) const { + debug_check(*this); + CTH_CRITICAL(!cmdBuffer.recording(), "command buffer must be recording") {} + CTH_CRITICAL( + firstMipLevel == 0 || firstMipLevel >= _config.mipLevels, + "first mip level must lie in [1, mip levels)" + ) {} + + auto const levels = mipLevels == VK_REMAINING_MIP_LEVELS ? _config.mipLevels - firstMipLevel : mipLevels; + CTH_CRITICAL(levels == 0 || levels > _config.mipLevels - firstMipLevel, "mip levels must lie in the image") {} + + constexpr VkImageUsageFlags transfer = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + CTH_CRITICAL( + (_config.usage & transfer) != transfer, + "image usage must have VK_IMAGE_USAGE_TRANSFER_SRC_BIT and VK_IMAGE_USAGE_TRANSFER_DST_BIT" + ) {} + + auto const corner = [this](uint32_t level) { + auto const extent = mip_extent(_extent, level); + return VkOffset3D{static_cast(extent.width), static_cast(extent.height), 1}; + }; + // the level is written, the next blit reads it + auto const toSrc = [&](uint32_t level) { + Barrier barrier{}; + barrier.addTransition( + *this, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + Barrier::INFERRED_SHADER_STAGES, + level, + 1 + ); + barrier.execute(cmdBuffer); + }; + + for(uint32_t level = firstMipLevel; level < firstMipLevel + levels; ++level) { + toSrc(level - 1); + + VkImageBlit2 const region{ + .sType = VK_STRUCTURE_TYPE_IMAGE_BLIT_2, + .pNext = nullptr, + .srcSubresource = { + .aspectMask = aspectMask(), + .mipLevel = level - 1, + .baseArrayLayer = 0, + .layerCount = arrayLayers(), + }, + .srcOffsets = {{0, 0, 0}, corner(level - 1)}, + .dstSubresource = { + .aspectMask = aspectMask(), + .mipLevel = level, + .baseArrayLayer = 0, + .layerCount = arrayLayers(), + }, + .dstOffsets = {{0, 0, 0}, corner(level)}, + }; + VkBlitImageInfo2 const info{ + .sType = VK_STRUCTURE_TYPE_BLIT_IMAGE_INFO_2, + .pNext = nullptr, + .srcImage = get(), + .srcImageLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + .dstImage = get(), + .dstImageLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + .regionCount = 1, + .pRegions = ®ion, + .filter = filter, + }; + core().functions()->vkCmdBlitImage2(cmdBuffer.get(), &info); + } + toSrc(firstMipLevel + levels - 1); +} + +VkBufferImageCopy2 Image::bufferCopy( + BaseCmdBuffer const& cmdBuffer, + BaseBuffer const& buffer, + size_t bufferOffset, + uint32_t mipLevel +) const { + debug_check(*this); + BaseBuffer::debug_check(buffer); + CTH_CRITICAL(!cmdBuffer.recording(), "command buffer must be recording") {} + CTH_CRITICAL(mipLevel >= mipLevels(), "mip level must lie in the image") {} + CTH_CRITICAL(!std::has_single_bit(aspectMask()), "a copy needs exactly one aspect") {} + + auto const extent = mip_extent(_extent, mipLevel); + return VkBufferImageCopy2{ + .sType = VK_STRUCTURE_TYPE_BUFFER_IMAGE_COPY_2, + .pNext = nullptr, + .bufferOffset = bufferOffset, + .bufferRowLength = 0, + .bufferImageHeight = 0, + .imageSubresource = { + .aspectMask = aspectMask(), + .mipLevel = mipLevel, + .baseArrayLayer = 0, + .layerCount = arrayLayers(), + }, + .imageOffset = {0, 0, 0}, + .imageExtent = {extent.width, extent.height, 1}, + }; +} + +void Image::destroy(DeviceTable table, VkImage vkImage) { + CTH_WARN(vkImage == VK_NULL_HANDLE, "vkImage invalid (VK_NULL_HANDLE)") {} + + table->vkDestroyImage(table.device(), vkImage, nullptr); +} + +void Image::forget() { + _memory.reset(); + _extent = {}; +} +} diff --git a/cvk/src/data/img/image_view.cpp b/cvk/src/data/img/image_view.cpp new file mode 100644 index 00000000..734fbc92 --- /dev/null +++ b/cvk/src/data/img/image_view.cpp @@ -0,0 +1,89 @@ +#include "cvk/data/img/image_view.hpp" + +#include "cvk/base/core.hpp" +#include "cvk/utility/vk_exceptions.hpp" + + +namespace cvk { +ImageView::ImageView(Image const& image, Config const& config) : _handle{image.core()}, _image{&image}, + _config{config} {} + +ImageView::ImageView(Image const& image, Config const& config, State const& state) : ImageView{image, config} { + wrap(state); +} + +ImageView::ImageView(Image const& image, Config const& config, create_t) : ImageView{image, config} { create(); } + +void ImageView::wrap(State const& state) { + optDestroy(); + + _handle.reset(state.vkImageView.get(), core()); +} + +void ImageView::create() { + Image::debug_check(*_image); + CTH_CRITICAL(_config.baseMipLevel >= _image->mipLevels(), "base mip level must lie in the image") {} + CTH_CRITICAL( + _config.mipLevels != VK_REMAINING_MIP_LEVELS && _config.mipLevels > _image->mipLevels() - _config.baseMipLevel, + "mip levels must lie in the image" + ) {} + CTH_CRITICAL(_config.baseArrayLayer >= _image->arrayLayers(), "base array layer must lie in the image") {} + CTH_CRITICAL( + _config.arrayLayers != VK_REMAINING_ARRAY_LAYERS + && _config.arrayLayers > _image->arrayLayers() - _config.baseArrayLayer, + "array layers must lie in the image" + ) {} + + optDestroy(); + + VkImageViewCreateInfo const info{ + .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .image = _image->get(), + .viewType = viewType(), + .format = _image->format(), + .components = { + .r = VK_COMPONENT_SWIZZLE_IDENTITY, + .g = VK_COMPONENT_SWIZZLE_IDENTITY, + .b = VK_COMPONENT_SWIZZLE_IDENTITY, + .a = VK_COMPONENT_SWIZZLE_IDENTITY, + }, + .subresourceRange = range(), + }; + + VkImageView ptr = VK_NULL_HANDLE; + auto const result = core().functions()->vkCreateImageView(core().vkDevice(), &info, nullptr, &ptr); + + CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create image view") { + forget(); + throw cvk::vk_result_exception{result, details->exception()}; + } + + _handle.reset(ptr, core()); +} + +void ImageView::destroy() { + debug_check(*this); + + _handle.reset(); + + forget(); +} + +ImageView::State ImageView::release() { + debug_check(*this); + + State const state{_handle.release()}; + forget(); + return state; +} + +void ImageView::destroy(DeviceTable table, VkImageView vkImageView) { + CTH_WARN(vkImageView == VK_NULL_HANDLE, "vkImageView invalid (VK_NULL_HANDLE)") {} + + table->vkDestroyImageView(table.device(), vkImageView, nullptr); +} + +void ImageView::forget() {} +} diff --git a/cvk/src/data/img/sampler.cpp b/cvk/src/data/img/sampler.cpp new file mode 100644 index 00000000..2271c8e8 --- /dev/null +++ b/cvk/src/data/img/sampler.cpp @@ -0,0 +1,73 @@ +#include "cvk/data/img/sampler.hpp" + +#include "cvk/base/core.hpp" +#include "cvk/base/physical_device.hpp" +#include "cvk/utility/vk_exceptions.hpp" + +#include + + +namespace cvk { +Sampler::Sampler(Core const& core, Config const& config) : _handle{core}, _config{config} { + Core::debug_check(core); + + auto const limit = core.physicalDevice().limits().maxSamplerAnisotropy; + CTH_WARN( + _config.maxAnisotropy > limit, + "maxAnisotropy clamped to the device limit {}, got {}", + limit, + _config.maxAnisotropy + ) {} + _config.maxAnisotropy = std::min(_config.maxAnisotropy, limit); +} + +Sampler::Sampler(Core const& core, Config const& config, State const& state) : Sampler{core, config} { wrap(state); } + +Sampler::Sampler(Core const& core, Config const& config, create_t) : Sampler{core, config} { create(); } + +void Sampler::wrap(State const& state) { + optDestroy(); + + _handle.reset(state.vkSampler.get(), core()); +} + +void Sampler::create() { + optDestroy(); + + auto const info = _config.createInfo(); + + VkSampler ptr = VK_NULL_HANDLE; + auto const result = core().functions()->vkCreateSampler(core().vkDevice(), &info, nullptr, &ptr); + + CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create sampler") { + forget(); + throw cvk::vk_result_exception{result, details->exception()}; + } + + _handle.reset(ptr, core()); +} + +void Sampler::destroy() { + debug_check(*this); + + _handle.reset(); + + forget(); +} + +Sampler::State Sampler::release() { + debug_check(*this); + + State const state{_handle.release()}; + forget(); + return state; +} + +void Sampler::destroy(DeviceTable table, VkSampler vkSampler) { + CTH_WARN(vkSampler == VK_NULL_HANDLE, "vkSampler invalid (VK_NULL_HANDLE)") {} + + table->vkDestroySampler(table.device(), vkSampler, nullptr); +} + +void Sampler::forget() {} +} diff --git a/cvk/src/data/memory/memory.cpp b/cvk/src/data/memory/memory.cpp new file mode 100644 index 00000000..82540f10 --- /dev/null +++ b/cvk/src/data/memory/memory.cpp @@ -0,0 +1,147 @@ +#include "cvk/data/memory/memory.hpp" + +#include "cvk/base/allocator.hpp" +#include "cvk/base/core.hpp" +#include "cvk/utility/vk_exceptions.hpp" + +#include + + +namespace cvk { +Memory::Memory(Core const& core, Config const& config) : _handle{core}, _config{config} { Core::debug_check(core); } + +Memory::Memory(Core const& core, Config const& config, State const& state) : Memory{core, config} { wrap(state); } + +Memory::Memory(Core const& core, Config const& config, VkMemoryRequirements const& requirements) : Memory{ + core, + config +} { create(requirements); } + +void Memory::wrap(State const& state) { + optDestroy(); + + _handle.reset(state.vmaAllocation.get(), core()); +} + +void Memory::create(VkMemoryRequirements const& requirements) { + CTH_CRITICAL(_config.autoUsage(), "VMA_MEMORY_USAGE_AUTO* needs a buffer or an image, use the flags") {} + + optDestroy(); + + auto const info = _config.createInfo(); + + VmaAllocation ptr = VK_NULL_HANDLE; + auto const result = vmaAllocateMemory(allocator(), &requirements, &info, &ptr, nullptr); + + CTH_STABLE_ERR(result != VK_SUCCESS, "failed to allocate memory") { + forget(); + throw cvk::vk_result_exception{result, details->exception()}; + } + + _handle.reset(ptr, core()); +} + +void Memory::destroy() { + debug_check(*this); + + _handle.reset(); + + forget(); +} + +Memory::State Memory::release() { + debug_check(*this); + + State const state{_handle.release()}; + forget(); + return state; +} + +Mapping Memory::map(Range range) const { + debug_check(*this); + CTH_CRITICAL((properties() & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) == 0, "memory must be host visible") {} + + size_t const memorySize = size(); + CTH_CRITICAL(range.offset > memorySize, "offset must lie in the memory") {} + + size_t const mapSize = range.size == VK_WHOLE_SIZE ? memorySize - range.offset : range.size; + CTH_CRITICAL(mapSize > memorySize - range.offset, "range must lie in the memory") {} + + void* data = nullptr; + auto const result = vmaMapMemory(allocator(), get(), &data); + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to map memory") {} + + return Mapping{*this, {static_cast(data) + range.offset, mapSize}}; +} + +void Memory::unmap() const { + debug_check(*this); + + vmaUnmapMemory(allocator(), get()); +} + +void Memory::flush(Range range) const { + debug_check(*this); + + auto const result = vmaFlushAllocation(allocator(), get(), range.offset, range.size); + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to flush memory") {} +} + +void Memory::invalidate(Range range) const { + debug_check(*this); + + auto const result = vmaInvalidateAllocation(allocator(), get(), range.offset, range.size); + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to invalidate memory") {} +} + +void Memory::write(std::span data, size_t offset) const { + debug_check(*this); + CTH_CRITICAL((properties() & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) == 0, "memory must be host visible") {} + CTH_CRITICAL(offset > size() || data.size() > size() - offset, "range must lie in the memory") {} + + if(data.empty()) return; + + // not vmaCopyMemoryToAllocation(): it flushes after the unmap, non coherent memory must be mapped to flush + VkResult result = VK_SUCCESS; + { + auto const mapping = map({.offset = offset, .size = data.size()}); + std::ranges::copy(data, mapping.begin()); + result = vmaFlushAllocation(allocator(), get(), offset, data.size()); + } + + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to flush memory") {} +} + +void Memory::destroy(VmaAllocator vmaAllocator, VmaAllocation vmaAllocation) { + CTH_WARN(vmaAllocation == VK_NULL_HANDLE, "vmaAllocation invalid (VK_NULL_HANDLE)") {} + + vmaFreeMemory(vmaAllocator, vmaAllocation); +} + +VmaAllocationInfo Memory::info() const { + debug_check(*this); + + VmaAllocationInfo info{}; + vmaGetAllocationInfo(allocator(), get(), &info); + return info; +} + +std::span Memory::mapped() const { + auto const allocationInfo = info(); + if(allocationInfo.pMappedData == nullptr) return {}; + + return {static_cast(allocationInfo.pMappedData), allocationInfo.size}; +} + +VkMemoryPropertyFlags Memory::properties() const { + debug_check(*this); + + VkMemoryPropertyFlags flags = 0; + vmaGetAllocationMemoryProperties(allocator(), get(), &flags); + return flags; +} + +void Memory::forget() {} + +VmaAllocator Memory::allocator() const { return core().allocator().get(); } +} diff --git a/cvk/src/debug/debug_messenger.cpp b/cvk/src/debug/debug_messenger.cpp new file mode 100644 index 00000000..796bf2e5 --- /dev/null +++ b/cvk/src/debug/debug_messenger.cpp @@ -0,0 +1,168 @@ +#include "cvk/debug/debug_messenger.hpp" + +#include "cvk/base/instance.hpp" +#include "cvk/utility/vk_exceptions.hpp" + +#include + +#include + + +namespace cvk { +DebugMessenger::DebugMessenger(Instance const& instance, Config config) : _config{std::move(config)}, + _handle{instance.get()} { + Instance::debug_check(instance); + Config::debug_check(_config); +} + +DebugMessenger::DebugMessenger(Instance const& instance, Config config, State const& state) : DebugMessenger{ + instance, + std::move(config) +} { wrap(state); } + +DebugMessenger::DebugMessenger(Instance const& instance, Config config, create_t) : DebugMessenger{ + instance, + std::move(config) +} { create(); } + +void DebugMessenger::wrap(State const& state) { + optDestroy(); + + _handle.reset(state.vkMessenger.get(), _handle.vkInstance()); +} + +void DebugMessenger::create() { + optDestroy(); + + auto const info = _config.createInfo(); + + VkDebugUtilsMessengerEXT ptr = VK_NULL_HANDLE; + auto const createResult = vkCreateDebugUtilsMessengerEXT(_handle.vkInstance(), &info, nullptr, &ptr); + + CTH_STABLE_ERR(createResult != VK_SUCCESS, "failed to set up debug messenger") { + forget(); + throw cvk::vk_result_exception{createResult, details->exception()}; + } + + _handle.reset(ptr, _handle.vkInstance()); +} + +void DebugMessenger::destroy() { + debug_check(*this); + + _handle.reset(); + + forget(); +} + + +void DebugMessenger::destroy(vk_not_null vkInstance, VkDebugUtilsMessengerEXT vkMessenger) { + Instance::debug_check_handle(vkInstance.get()); + CTH_WARN(vkMessenger == VK_NULL_HANDLE, "messenger invalid") {} + + vkDestroyDebugUtilsMessengerEXT(vkInstance.get(), vkMessenger, nullptr); +} + +DebugMessenger::State DebugMessenger::release() { + debug_check(*this); + + State const state{_handle.release()}; + forget(); + return state; +} + +void DebugMessenger::forget() {} +} + + +namespace cvk::dev { +struct component_info { + std::string name; + VkObjectType objectType; + uint64_t handle; +}; +} + +CTH_FORMAT_CLASS_CUSTOM( + cvk::dev::component_info, + "name: {0}, type: {1}, handle: {2:#x}", + [](cvk::dev::component_info const& c) { return std::tie(c.name, c.objectType, c.handle); } +); + + +namespace cvk::dev { +namespace { + auto to_severity(VkDebugUtilsMessageSeverityFlagsEXT vkSeverity) { + constexpr std::array severities{ + std::pair{VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT, cth::except::LOG}, + std::pair{VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT, cth::except::INFO}, + std::pair{VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT, cth::except::WARNING}, + std::pair{VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT, cth::except::ERR} + }; + + + for(auto [vkSeverityFlagBits, severity] : severities) + if(vkSeverity <= vkSeverityFlagBits) + return severity; + + return cth::except::CRITICAL; + } + + std::string to_msg(VkDebugUtilsMessageTypeFlagsEXT vkMsgType) { + constexpr std::array msgTypes{ + std::pair{VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT, "GENERAL"}, + std::pair{VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT, "VALIDATION"}, + std::pair{VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT, "PERFORMANCE"} + }; + + cth::dt::string_joiner joiner{" | "}; + + for(auto [vkMsgTypeFlagBit, name] : msgTypes) + if(vkMsgType & vkMsgTypeFlagBit) + joiner += name; + + return joiner; + } +} +} + +namespace cvk { +VKAPI_ATTR VkBool32 VKAPI_CALL default_debug_callback( + VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, + VkDebugUtilsMessageTypeFlagsEXT messageType, + VkDebugUtilsMessengerCallbackDataEXT const* callbackData, + [[maybe_unused]] void* userData +) { + auto const severity = dev::to_severity(messageSeverity); + + auto type = dev::to_msg(messageType); + if(type.empty()) type = "UNKNOWN"; + + std::vector objects{}; + objects.reserve(callbackData->objectCount); + + for(auto const& object : std::span{callbackData->pObjects, callbackData->objectCount}) { + auto const namePtr = object.pObjectName; + objects.emplace_back( + namePtr == nullptr ? "UNKNOWN" : namePtr, + object.objectType, + object.objectHandle + ); + } + + auto const idNamePtr = callbackData->pMessageIdName; + + cth::log::msg( + severity, + "VALIDATION LAYER: {0} {1}:\n NAME: {2}\n\t (CODE: {3})\n{4}\n OBJECTS: {5}\n", + type, + to_string(severity), + idNamePtr == nullptr ? "UNKNOWN" : idNamePtr, + callbackData->messageIdNumber, + callbackData->pMessage, + objects + ); + + return VK_FALSE; +} +} diff --git a/cvk/src/pch/cvk/pch_def.hpp b/cvk/src/pch/cvk/pch_def.hpp new file mode 100644 index 00000000..7f1e76dc --- /dev/null +++ b/cvk/src/pch/cvk/pch_def.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include +#include + +namespace cvk { +namespace except = cth::except; +namespace log = cth::log; +} diff --git a/cvk/src/pch/glob/cth.hpp b/cvk/src/pch/glob/cth.hpp new file mode 100644 index 00000000..710fa5cf --- /dev/null +++ b/cvk/src/pch/glob/cth.hpp @@ -0,0 +1,9 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include diff --git a/jolly/src/pch/glob/std.hpp b/cvk/src/pch/glob/std.hpp similarity index 100% rename from jolly/src/pch/glob/std.hpp rename to cvk/src/pch/glob/std.hpp diff --git a/cvk/src/pch/glob/volk.hpp b/cvk/src/pch/glob/volk.hpp new file mode 100644 index 00000000..e78ae406 --- /dev/null +++ b/cvk/src/pch/glob/volk.hpp @@ -0,0 +1,2 @@ +#pragma once +#include diff --git a/cvk/src/pch/pch.hpp b/cvk/src/pch/pch.hpp new file mode 100644 index 00000000..309c960d --- /dev/null +++ b/cvk/src/pch/pch.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "glob/cth.hpp" +#include "glob/std.hpp" + +#include "glob/volk.hpp" + +#include "cvk/pch_def.hpp" + +#undef far +#undef near diff --git a/cvk/src/render/cmd/cmd_buffer.cpp b/cvk/src/render/cmd/cmd_buffer.cpp new file mode 100644 index 00000000..a7452b0b --- /dev/null +++ b/cvk/src/render/cmd/cmd_buffer.cpp @@ -0,0 +1,685 @@ +#include "cvk/render/cmd/cmd_buffer.hpp" + +#include "cvk/base/core.hpp" +#include "cvk/data/descriptor/descriptor_set.hpp" +#include "cvk/data/img/image.hpp" +#include "cvk/data/img/image_format.hpp" +#include "cvk/data/img/image_view.hpp" +#include "cvk/render/pipeline/compute_pipeline.hpp" +#include "cvk/render/pipeline/graphics_pipeline.hpp" +#include "cvk/render/pipeline/layout/pipeline_layout.hpp" +#include "cvk/utility/vk_exceptions.hpp" + +#include +#include +#include + + +namespace cvk { +using std::span; + +namespace { + void debug_check_attachment_layout(VkImageLayout layout) { + CTH_CRITICAL( + layout == VK_IMAGE_LAYOUT_UNDEFINED || layout == VK_IMAGE_LAYOUT_PREINITIALIZED + || layout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL || layout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL + || layout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL || layout == VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, + "attachment must be in an attachment layout, got {}", + layout + ) {} + } + + constexpr bool dynamic_buffer(VkDescriptorType type) { + return type == VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC || type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC; + } + + uint32_t view_mip_levels(ImageView const& view) { + auto const range = view.range(); + return range.levelCount == VK_REMAINING_MIP_LEVELS ? view.image().mipLevels() - range.baseMipLevel + : range.levelCount; + } + + void debug_check_draw(BaseCmdBuffer const& cmdBuffer) { + BaseCmdBuffer::debug_check(cmdBuffer); + + // a secondary can be inside the rendering of its primary + auto const* rendering = cmdBuffer.rendering(); + CTH_CRITICAL( + rendering == nullptr && cmdBuffer.level() == VK_COMMAND_BUFFER_LEVEL_PRIMARY, + "draws need a rendering" + ) {} + CTH_CRITICAL( + rendering != nullptr && (rendering->flags & VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT) != 0 + && (rendering->flags & VK_RENDERING_CONTENTS_INLINE_BIT_KHR) == 0, + "the secondaries record the draws of a rendering with VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT" + ) {} + } + + void debug_check_indirect( + BaseBuffer const& buffer, + size_t offset, + uint32_t drawCount, + uint32_t stride, + size_t size + ) { + BaseBuffer::debug_check(buffer); + CTH_CRITICAL( + (buffer.config().usage & VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT) == 0, + "buffer usage must have VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT" + ) {} + CTH_CRITICAL(offset % 4 != 0, "offset must be a multiple of 4") {} + CTH_CRITICAL( + drawCount > 0 && offset + size_t{drawCount - 1} * stride + size > buffer.size(), + "the commands must lie in the buffer" + ) {} + } + + VkExtent2D view_extent(ImageView const& view) { + return mip_extent(view.image().extent(), view.range().baseMipLevel); + } + + void debug_check_secondary(SecondaryCmdBuffer const& secondary) { + CTH_CRITICAL(!secondary.created() || secondary.recording(), "secondary must be created and not recording") {} + } +} + +BaseCmdBuffer::BaseCmdBuffer(CmdPool const& pool, VkCommandBufferLevel level) : _handle{pool}, _level{level} { + CmdPool::debug_check(pool); +} + +void BaseCmdBuffer::wrap(State const& state) { + optDestroy(); + + _handle.reset(state.vkBuffer.get(), pool()); + pool().add(); + _recording = false; + _rendering.reset(); +} + +void BaseCmdBuffer::create() { + CmdPool::debug_check(pool()); + + optDestroy(); + + VkCommandBufferAllocateInfo const info{ + .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, + .pNext = nullptr, + .commandPool = pool().get(), + .level = _level, + .commandBufferCount = 1, + }; + + VkCommandBuffer ptr = VK_NULL_HANDLE; + auto const result = core().functions()->vkAllocateCommandBuffers(core().vkDevice(), &info, &ptr); + + CTH_STABLE_ERR(result != VK_SUCCESS, "failed to allocate command buffer") { + forget(); + throw cvk::vk_result_exception{result, details->exception()}; + } + + _handle.reset(ptr, pool()); + pool().add(); + _recording = false; + _rendering.reset(); +} + +void BaseCmdBuffer::destroy() { + debug_check(*this); + + _handle.reset(); + + forget(); +} + +BaseCmdBuffer::State BaseCmdBuffer::release() { + debug_check(*this); + + State const state{_handle.release()}; + pool().remove(); + + forget(); + return state; +} + +void BaseCmdBuffer::end() { + debug_check(*this); + CTH_CRITICAL(!recording(), "command buffer must be recording") {} + CTH_CRITICAL(rendering() != nullptr, "rendering must be ended first") {} + + auto const result = core().functions()->vkEndCommandBuffer(get()); + _recording = false; + _rendering.reset(); + + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to end command buffer") {} +} + +void BaseCmdBuffer::reset(VkCommandBufferResetFlags flags) { + debug_check(*this); + CTH_CRITICAL( + (pool().config().flags & VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT) == 0, + "pool must allow command buffer resets" + ) {} + + auto const result = core().functions()->vkResetCommandBuffer(get(), flags); + _recording = false; + _rendering.reset(); + + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to reset command buffer") {} +} + +void BaseCmdBuffer::beginRendering(RenderingInfo const& info) { + debug_check(*this); + CTH_CRITICAL(!recording(), "command buffer must be recording") {} + CTH_CRITICAL(rendering() != nullptr, "command buffer must be outside of a rendering") {} + AttachmentMapping::debug_check(info.attachmentMapping, info.colorAttachments.size()); + + std::optional extent{}; + + auto const attachmentInfo = [&](RenderingAttachment const& attachment) { + ImageView const& view = *attachment.view; + ImageView::debug_check(view); + CTH_CRITICAL(view_mip_levels(view) != 1, "attachment views must have one mip level") {} + // multiview: view i renders into layer i + auto const layers = info.viewMask != 0 ? static_cast(std::bit_width(info.viewMask)) : info.layerCount; + CTH_CRITICAL( + view.arrayLayers() < layers, + "attachment views need a layer per rendered layer or view, got {} for {}", + view.arrayLayers(), + layers + ) {} + + auto const viewExtent = view_extent(view); + extent = extent.has_value() + ? VkExtent2D{std::min(extent->width, viewExtent.width), std::min(extent->height, viewExtent.height)} + : viewExtent; + debug_check_attachment_layout(attachment.layout); + + VkImageView resolveView = VK_NULL_HANDLE; + VkImageLayout resolveLayout = VK_IMAGE_LAYOUT_UNDEFINED; + if(attachment.resolveView != nullptr) { + ImageView::debug_check(*attachment.resolveView); + CTH_CRITICAL(attachment.resolveMode == VK_RESOLVE_MODE_NONE, "a resolve view needs a resolve mode") {} + + debug_check_attachment_layout(attachment.resolveLayout); + + resolveView = attachment.resolveView->get(); + resolveLayout = attachment.resolveLayout; + } + + return VkRenderingAttachmentInfo{ + .sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO, + .pNext = nullptr, + .imageView = view.get(), + .imageLayout = attachment.layout, + .resolveMode = attachment.resolveMode, + .resolveImageView = resolveView, + .resolveImageLayout = resolveLayout, + .loadOp = attachment.loadOp, + .storeOp = attachment.storeOp, + .clearValue = attachment.clearValue, + }; + }; + + std::vector colors{}; + colors.reserve(info.colorAttachments.size()); + for(auto const& attachment : info.colorAttachments) { + colors.push_back(attachmentInfo(attachment)); + + auto const format = attachment.view->image().format(); + CTH_CRITICAL( + format_aspects(format) != VK_IMAGE_ASPECT_COLOR_BIT, + "color attachments need a color format, got {}", + format + ) {} + } + + std::optional depth{}; + if(info.depthAttachment.has_value()) { + depth = attachmentInfo(*info.depthAttachment); + + auto const format = info.depthAttachment->view->image().format(); + CTH_CRITICAL( + (format_aspects(format) & VK_IMAGE_ASPECT_DEPTH_BIT) == 0, + "the depth attachment needs a depth format, got {}", + format + ) {} + } + + std::optional stencil{}; + if(info.stencilAttachment.has_value()) { + stencil = attachmentInfo(*info.stencilAttachment); + + auto const format = info.stencilAttachment->view->image().format(); + CTH_CRITICAL( + (format_aspects(format) & VK_IMAGE_ASPECT_STENCIL_BIT) == 0, + "the stencil attachment needs a stencil format, got {}", + format + ) {} + } + + CTH_CRITICAL(!extent.has_value() && !info.area.has_value(), "a rendering without attachments needs an area") {} + + VkRenderingInfo const renderingInfo{ + .sType = VK_STRUCTURE_TYPE_RENDERING_INFO, + .pNext = nullptr, + .flags = info.flags, + .renderArea = info.area.value_or(VkRect2D{.offset = {0, 0}, .extent = extent.value_or(VkExtent2D{})}), + .layerCount = info.layerCount, + .viewMask = info.viewMask, + .colorAttachmentCount = static_cast(colors.size()), + .pColorAttachments = colors.data(), + .pDepthAttachment = depth.has_value() ? &*depth : nullptr, + .pStencilAttachment = stencil.has_value() ? &*stencil : nullptr, + }; + core().functions()->vkCmdBeginRendering(get(), &renderingInfo); + + _rendering = info; + _rendering->area = renderingInfo.renderArea; + if(_rendering->attachmentMapping != AttachmentMapping{}) recordAttachmentMapping(); + + // secondaries do not inherit dynamic state + bool const inlineContents = (info.flags & VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT) == 0 + || (info.flags & VK_RENDERING_CONTENTS_INLINE_BIT_KHR) != 0; + if(!inlineContents) return; + + auto const& area = renderingInfo.renderArea; + setViewport( + VkViewport{ + .x = static_cast(area.offset.x), + .y = static_cast(area.offset.y), + .width = static_cast(area.extent.width), + .height = static_cast(area.extent.height), + .minDepth = 0.f, + .maxDepth = 1.f, + } + ); + setScissor(area); +} + +void BaseCmdBuffer::endRendering() { + debug_check(*this); + CTH_CRITICAL(rendering() == nullptr, "command buffer must be inside its own rendering") {} + + core().functions()->vkCmdEndRendering(get()); + + _rendering.reset(); +} + +void BaseCmdBuffer::setAttachmentMapping(AttachmentMapping const& mapping) { + debug_check(*this); + CTH_CRITICAL(rendering() == nullptr, "command buffer must be inside its own rendering") {} + AttachmentMapping::debug_check(mapping, _rendering->colorAttachments.size()); + + _rendering->attachmentMapping = mapping; + recordAttachmentMapping(); +} + +void BaseCmdBuffer::setViewport(VkViewport const& viewport) const { + debug_check(*this); + CTH_CRITICAL(!recording(), "command buffer must be recording") {} + + core().functions()->vkCmdSetViewport(get(), 0, 1, &viewport); +} + +void BaseCmdBuffer::setScissor(VkRect2D const& scissor) const { + debug_check(*this); + CTH_CRITICAL(!recording(), "command buffer must be recording") {} + + core().functions()->vkCmdSetScissor(get(), 0, 1, &scissor); +} + +void BaseCmdBuffer::bindPipeline(GraphicsPipeline const& pipeline) { + debug_check(*this); + CTH_CRITICAL(!recording(), "command buffer must be recording") {} + GraphicsPipeline::debug_check(pipeline); + + auto const& mapping = pipeline.config().attachmentMapping; + if(rendering() != nullptr && mapping != rendering()->attachmentMapping) setAttachmentMapping(mapping); + + core().functions()->vkCmdBindPipeline(get(), VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline.get()); +} + +void BaseCmdBuffer::bindPipeline(ComputePipeline const& pipeline) const { + debug_check(*this); + CTH_CRITICAL(!recording(), "command buffer must be recording") {} + ComputePipeline::debug_check(pipeline); + + core().functions()->vkCmdBindPipeline(get(), VK_PIPELINE_BIND_POINT_COMPUTE, pipeline.get()); +} + +void BaseCmdBuffer::bindDescriptorSet( + DescriptorSet const& set, + PipelineLayout const& layout, + uint32_t setIndex, + VkPipelineBindPoint bindPoint, + span dynamicOffsets +) const { + debug_check(*this); + CTH_CRITICAL(!recording(), "command buffer must be recording") {} + DescriptorSet::debug_check(set); + PipelineLayout::debug_check(layout); + CTH_CRITICAL(setIndex >= layout.setLayouts().size(), "the pipeline layout has no set {}", setIndex) {} + + auto const dynamicCount = std::ranges::fold_left( + set.layout().bindings(), + uint32_t{0}, + [](uint32_t sum, VkDescriptorSetLayoutBinding const& binding) { + return dynamic_buffer(binding.descriptorType) ? sum + binding.descriptorCount : sum; + } + ); + CTH_CRITICAL( + dynamicOffsets.size() != dynamicCount, + "one dynamic offset per dynamic buffer descriptor, got {} for {}", + dynamicOffsets.size(), + dynamicCount + ) {} + + VkDescriptorSet const vkSet = set.get(); + core().functions()->vkCmdBindDescriptorSets( + get(), + bindPoint, + layout.get(), + setIndex, + 1, + &vkSet, + static_cast(dynamicOffsets.size()), + dynamicOffsets.data() + ); +} + +void BaseCmdBuffer::pushConstantBytes( + PipelineLayout const& layout, + VkShaderStageFlags stages, + span data, + uint32_t offset +) const { + debug_check(*this); + CTH_CRITICAL(!recording(), "command buffer must be recording") {} + PipelineLayout::debug_check(layout); + CTH_CRITICAL(data.empty(), "push constants must not be empty") {} + CTH_CRITICAL(offset % 4 != 0 || data.size() % 4 != 0, "offset and size must be multiples of 4") {} + + core().functions()->vkCmdPushConstants( + get(), + layout.get(), + stages, + offset, + static_cast(data.size()), + data.data() + ); +} + +void BaseCmdBuffer::bindVertexBuffer(uint32_t binding, BaseBuffer const& buffer, size_t offset) const { + debug_check(*this); + BaseBuffer::debug_check(buffer); + CTH_CRITICAL(!recording(), "command buffer must be recording") {} + CTH_CRITICAL( + (buffer.config().usage & VK_BUFFER_USAGE_VERTEX_BUFFER_BIT) == 0, + "buffer usage must have VK_BUFFER_USAGE_VERTEX_BUFFER_BIT" + ) {} + CTH_CRITICAL(offset >= buffer.size(), "offset must lie in the buffer") {} + + VkBuffer const vkBuffer = buffer.get(); + VkDeviceSize const vkOffset = offset; + core().functions()->vkCmdBindVertexBuffers(get(), binding, 1, &vkBuffer, &vkOffset); +} + +void BaseCmdBuffer::bindIndexBuffer(BaseBuffer const& buffer, VkIndexType type, size_t offset) const { + debug_check(*this); + BaseBuffer::debug_check(buffer); + CTH_CRITICAL(!recording(), "command buffer must be recording") {} + CTH_CRITICAL( + (buffer.config().usage & VK_BUFFER_USAGE_INDEX_BUFFER_BIT) == 0, + "buffer usage must have VK_BUFFER_USAGE_INDEX_BUFFER_BIT" + ) {} + CTH_CRITICAL(offset >= buffer.size(), "offset must lie in the buffer") {} + CTH_CRITICAL( + offset % (type == VK_INDEX_TYPE_UINT16 ? sizeof(uint16_t) : sizeof(uint32_t)) != 0, + "offset must be a multiple of the index size" + ) {} + + core().functions()->vkCmdBindIndexBuffer(get(), buffer.get(), offset, type); +} + +void BaseCmdBuffer::draw( + uint32_t vertexCount, + uint32_t instanceCount, + uint32_t firstVertex, + uint32_t firstInstance +) const { + debug_check_draw(*this); + + core().functions()->vkCmdDraw(get(), vertexCount, instanceCount, firstVertex, firstInstance); +} + +void BaseCmdBuffer::drawIndexed( + uint32_t indexCount, + uint32_t instanceCount, + uint32_t firstIndex, + int32_t vertexOffset, + uint32_t firstInstance +) const { + debug_check_draw(*this); + + core().functions()->vkCmdDrawIndexed(get(), indexCount, instanceCount, firstIndex, vertexOffset, firstInstance); +} + +void BaseCmdBuffer::drawIndirect(BaseBuffer const& buffer, size_t offset, uint32_t drawCount, uint32_t stride) const { + debug_check_draw(*this); + debug_check_indirect(buffer, offset, drawCount, stride, sizeof(VkDrawIndirectCommand)); + + core().functions()->vkCmdDrawIndirect(get(), buffer.get(), offset, drawCount, stride); +} + +void BaseCmdBuffer::drawIndexedIndirect( + BaseBuffer const& buffer, + size_t offset, + uint32_t drawCount, + uint32_t stride +) const { + debug_check_draw(*this); + debug_check_indirect(buffer, offset, drawCount, stride, sizeof(VkDrawIndexedIndirectCommand)); + + core().functions()->vkCmdDrawIndexedIndirect(get(), buffer.get(), offset, drawCount, stride); +} + +void BaseCmdBuffer::dispatch(uint32_t groupCountX, uint32_t groupCountY, uint32_t groupCountZ) const { + debug_check(*this); + CTH_CRITICAL(!recording(), "command buffer must be recording") {} + CTH_CRITICAL(rendering() != nullptr, "dispatches must be outside of a rendering") {} + + core().functions()->vkCmdDispatch(get(), groupCountX, groupCountY, groupCountZ); +} + +void BaseCmdBuffer::dispatchIndirect(BaseBuffer const& buffer, size_t offset) const { + debug_check(*this); + CTH_CRITICAL(!recording(), "command buffer must be recording") {} + CTH_CRITICAL(rendering() != nullptr, "dispatches must be outside of a rendering") {} + debug_check_indirect(buffer, offset, 1, 0, sizeof(VkDispatchIndirectCommand)); + + core().functions()->vkCmdDispatchIndirect(get(), buffer.get(), offset); +} + +void BaseCmdBuffer::destroy(DeviceTable table, VkCommandPool vkPool, VkCommandBuffer vkBuffer) { + CTH_WARN(vkBuffer == VK_NULL_HANDLE, "vkBuffer invalid (VK_NULL_HANDLE)") {} + + table->vkFreeCommandBuffers(table.device(), vkPool, 1, &vkBuffer); +} + +void BaseCmdBuffer::destroy(DeviceTable table, VkCommandPool vkPool, span vkBuffers) { + CTH_WARN(std::ranges::contains(vkBuffers, VK_NULL_HANDLE), "> 0 vkBuffers invalid (VK_NULL_HANDLE)") {} + + table->vkFreeCommandBuffers(table.device(), vkPool, static_cast(vkBuffers.size()), vkBuffers.data()); +} + +void BaseCmdBuffer::begin(VkCommandBufferBeginInfo const& info) { + debug_check(*this); + CTH_CRITICAL(recording(), "command buffer must not be recording") {} + + auto const result = core().functions()->vkBeginCommandBuffer(get(), &info); + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to begin command buffer") {} + + _recording = true; + _beginResets = pool().resets(); + _rendering.reset(); +} + +void BaseCmdBuffer::recordAttachmentMapping() const { + auto const& mapping = _rendering->attachmentMapping; + auto const colorAttachments = static_cast(_rendering->colorAttachments.size()); + + auto const locations = mapping.locationInfo(colorAttachments); + auto const inputIndices = mapping.inputIndexInfo(colorAttachments); + core().functions()->vkCmdSetRenderingAttachmentLocationsKHR(get(), &locations); + core().functions()->vkCmdSetRenderingInputAttachmentIndicesKHR(get(), &inputIndices); +} + +void BaseCmdBuffer::forget() { + _recording = false; + _rendering.reset(); +} + +void BaseCmdBuffer::pool_deleter::operator()(VkCommandBuffer vkBuffer) const { + _pool->remove(); + destroy(_pool->core().deviceTable(), _pool->get(), vkBuffer); +} +} + +namespace cvk { +PrimaryCmdBuffer::PrimaryCmdBuffer(CmdPool const& pool) : BaseCmdBuffer{pool, VK_COMMAND_BUFFER_LEVEL_PRIMARY} {} + +PrimaryCmdBuffer::PrimaryCmdBuffer(CmdPool const& pool, State const& state) : PrimaryCmdBuffer{pool} { wrap(state); } + +PrimaryCmdBuffer::PrimaryCmdBuffer(CmdPool const& pool, create_t) : PrimaryCmdBuffer{pool} { create(); } + +void PrimaryCmdBuffer::begin(VkCommandBufferUsageFlags usage) { + VkCommandBufferBeginInfo const info{ + .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, + .pNext = nullptr, + .flags = usage, + .pInheritanceInfo = nullptr, + }; + BaseCmdBuffer::begin(info); +} + +void PrimaryCmdBuffer::execute(span secondaries) const { + for(auto const& secondary : secondaries) + debug_check_secondary(secondary); + + std::vector const vkBuffers{std::from_range, secondaries | std::views::transform(&SecondaryCmdBuffer::get)}; + recordExecute(vkBuffers); +} + +void PrimaryCmdBuffer::execute(span secondaries) const { + for(auto const* secondary : secondaries) { + CTH_CRITICAL(secondary == nullptr, "secondary must not be nullptr") {} + debug_check_secondary(*secondary); + } + + std::vector const vkBuffers{std::from_range, secondaries | std::views::transform(&SecondaryCmdBuffer::get)}; + recordExecute(vkBuffers); +} + +void PrimaryCmdBuffer::execute(std::initializer_list secondaries) const { + execute(span{secondaries.begin(), secondaries.size()}); +} + +void PrimaryCmdBuffer::execute(SecondaryCmdBuffer const& secondary) const { execute({&secondary, 1}); } + +RenderingInheritance PrimaryCmdBuffer::inheritance() const { + debug_check(*this); + CTH_CRITICAL(rendering() == nullptr, "command buffer must be inside its own rendering") {} + + auto const& info = *rendering(); + auto const format = [](RenderingAttachment const& attachment) { return attachment.view->image().format(); }; + + // every attachment has the same sample count + auto const samples = [&info] { + auto const of = [](RenderingAttachment const& attachment) { return attachment.view->image().config().samples; }; + if(!info.colorAttachments.empty()) return of(info.colorAttachments.front()); + if(info.depthAttachment.has_value()) return of(*info.depthAttachment); + if(info.stencilAttachment.has_value()) return of(*info.stencilAttachment); + return VK_SAMPLE_COUNT_1_BIT; + }; + + return RenderingInheritance{ + .colorFormats = {std::from_range, info.colorAttachments | std::views::transform(format)}, + .depthFormat = info.depthAttachment.has_value() ? format(*info.depthAttachment) : VK_FORMAT_UNDEFINED, + .stencilFormat = info.stencilAttachment.has_value() ? format(*info.stencilAttachment) : VK_FORMAT_UNDEFINED, + .samples = samples(), + .viewMask = info.viewMask, + .flags = info.flags & ~VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT, + .attachmentMapping = info.attachmentMapping, + }; +} + +void PrimaryCmdBuffer::recordExecute(span vkBuffers) const { + debug_check(*this); + CTH_CRITICAL(!recording(), "command buffer must be recording") {} + CTH_CRITICAL( + rendering() != nullptr && (rendering()->flags & VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT) == 0, + "the rendering must have VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT" + ) {} + + core().functions()->vkCmdExecuteCommands(get(), static_cast(vkBuffers.size()), vkBuffers.data()); +} +} + +namespace cvk { +SecondaryCmdBuffer::SecondaryCmdBuffer(CmdPool const& pool) : BaseCmdBuffer{pool, VK_COMMAND_BUFFER_LEVEL_SECONDARY} {} + +SecondaryCmdBuffer::SecondaryCmdBuffer(CmdPool const& pool, State const& state) : SecondaryCmdBuffer{pool} { + wrap(state); +} + +SecondaryCmdBuffer::SecondaryCmdBuffer(CmdPool const& pool, create_t) : SecondaryCmdBuffer{pool} { create(); } + +void SecondaryCmdBuffer::begin(VkCommandBufferUsageFlags usage) { + VkCommandBufferInheritanceInfo const inheritanceInfo{ + .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_INHERITANCE_INFO, + .pNext = nullptr, + .renderPass = VK_NULL_HANDLE, + .subpass = 0, + .framebuffer = VK_NULL_HANDLE, + .occlusionQueryEnable = VK_FALSE, + .queryFlags = 0, + .pipelineStatistics = 0, + }; + VkCommandBufferBeginInfo const info{ + .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, + .pNext = nullptr, + .flags = usage, + .pInheritanceInfo = &inheritanceInfo, + }; + BaseCmdBuffer::begin(info); +} + +void SecondaryCmdBuffer::begin(Inheritance const& inheritance, VkCommandBufferUsageFlags usage) { + auto const colorAttachments = static_cast(inheritance.colorFormats.size()); + AttachmentMapping::debug_check(inheritance.attachmentMapping, colorAttachments); + + auto const inputIndices = inheritance.attachmentMapping.inputIndexInfo(colorAttachments); + auto locations = inheritance.attachmentMapping.locationInfo(colorAttachments); + locations.pNext = &inputIndices; + auto renderingInfo = inheritance.renderingInfo(); + renderingInfo.pNext = &locations; + + VkCommandBufferInheritanceInfo const inheritanceInfo{ + .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_INHERITANCE_INFO, + .pNext = &renderingInfo, + .renderPass = VK_NULL_HANDLE, + .subpass = 0, + .framebuffer = VK_NULL_HANDLE, + .occlusionQueryEnable = VK_FALSE, + .queryFlags = 0, + .pipelineStatistics = 0, + }; + VkCommandBufferBeginInfo const info{ + .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, + .pNext = nullptr, + .flags = usage | VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT, + .pInheritanceInfo = &inheritanceInfo, + }; + BaseCmdBuffer::begin(info); +} +} diff --git a/cvk/src/render/cmd/cmd_pool.cpp b/cvk/src/render/cmd/cmd_pool.cpp new file mode 100644 index 00000000..74ff89cf --- /dev/null +++ b/cvk/src/render/cmd/cmd_pool.cpp @@ -0,0 +1,81 @@ +#include "cvk/render/cmd/cmd_pool.hpp" + +#include "cvk/base/core.hpp" +#include "cvk/utility/vk_exceptions.hpp" + + +namespace cvk { +CmdPool::CmdPool(Core const& core, Config const& config) : _handle{core}, _config{config} { + Core::debug_check(core); +} + +CmdPool::CmdPool(Core const& core, Config const& config, State const& state) : CmdPool{core, config} { wrap(state); } + +CmdPool::CmdPool(Core const& core, Config const& config, create_t) : CmdPool{core, config} { create(); } + +void CmdPool::wrap(State const& state) { + optDestroy(); + + _handle.reset(state.vkPool.get(), core()); +} + +void CmdPool::create() { + CTH_CRITICAL(_config.queueFamilyIndex == VK_QUEUE_FAMILY_IGNORED, "the config needs a queue family") {} + + optDestroy(); + + auto const info = _config.createInfo(); + + VkCommandPool ptr = VK_NULL_HANDLE; + auto const result = core().functions()->vkCreateCommandPool(core().vkDevice(), &info, nullptr, &ptr); + + CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create command pool") { + forget(); + throw cvk::vk_result_exception{result, details->exception()}; + } + + _handle.reset(ptr, core()); +} + +void CmdPool::destroy() { + debug_check(*this); + CTH_CRITICAL(!unused(), "command buffers of the pool must be destroyed first, got {}", _buffers) {} + + _handle.reset(); + + forget(); +} + +CmdPool::State CmdPool::release() { + debug_check(*this); + CTH_CRITICAL(!unused(), "command buffers of the pool must be destroyed first, got {}", _buffers) {} + + State const state{_handle.release()}; + forget(); + return state; +} + +void CmdPool::reset(VkCommandPoolResetFlags flags) const { + debug_check(*this); + + auto const result = core().functions()->vkResetCommandPool(core().vkDevice(), get(), flags); + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to reset command pool") {} + + ++_resets; +} + +void CmdPool::add() const { ++_buffers; } + +void CmdPool::remove() const { + CTH_CRITICAL(unused(), "the pool counts no command buffer") {} + --_buffers; +} + +void CmdPool::destroy(DeviceTable table, VkCommandPool vkPool) { + CTH_WARN(vkPool == VK_NULL_HANDLE, "vkPool invalid (VK_NULL_HANDLE)") {} + + table->vkDestroyCommandPool(table.device(), vkPool, nullptr); +} + +void CmdPool::forget() {} +} diff --git a/cvk/src/render/pipeline/base_pipeline.cpp b/cvk/src/render/pipeline/base_pipeline.cpp new file mode 100644 index 00000000..d0a36fc3 --- /dev/null +++ b/cvk/src/render/pipeline/base_pipeline.cpp @@ -0,0 +1,91 @@ +#include "cvk/render/pipeline/base_pipeline.hpp" + +#include "cvk/base/core.hpp" +#include "cvk/render/pipeline/layout/pipeline_layout.hpp" +#include "cvk/utility/vk_exceptions.hpp" + + +namespace cvk { +BasePipeline::BasePipeline(PipelineLayout const& layout, VkPipelineBindPoint bindPoint) : _handle{layout.core()}, + _layout{&layout}, _bindPoint{bindPoint} { + Core::debug_check(layout.core()); +} + +void BasePipeline::wrap(State const& state) { + optDestroy(); + + _handle.reset(state.vkPipeline.get(), core()); +} + +void BasePipeline::destroy() { + debug_check(*this); + + _handle.reset(); + + forget(); +} + +BasePipeline::State BasePipeline::release() { + debug_check(*this); + + State const state{_handle.release()}; + forget(); + return state; +} + +void BasePipeline::destroy(DeviceTable table, VkPipeline vkPipeline) { + CTH_WARN(vkPipeline == VK_NULL_HANDLE, "vkPipeline invalid (VK_NULL_HANDLE)") {} + + table->vkDestroyPipeline(table.device(), vkPipeline, nullptr); +} + +void BasePipeline::create(VkGraphicsPipelineCreateInfo const& info) { + PipelineLayout::debug_check(*_layout); + CTH_CRITICAL(info.layout != _layout->get(), "the create info must use the layout of the pipeline") {} + + optDestroy(); + + VkPipeline ptr = VK_NULL_HANDLE; + auto const result = core().functions()->vkCreateGraphicsPipelines( + core().vkDevice(), + VK_NULL_HANDLE, + 1, + &info, + nullptr, + &ptr + ); + + CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create graphics pipeline") { + forget(); + throw cvk::vk_result_exception{result, details->exception()}; + } + + _handle.reset(ptr, core()); +} + +void BasePipeline::create(VkComputePipelineCreateInfo const& info) { + PipelineLayout::debug_check(*_layout); + CTH_CRITICAL(info.layout != _layout->get(), "the create info must use the layout of the pipeline") {} + + optDestroy(); + + VkPipeline ptr = VK_NULL_HANDLE; + auto const result = core().functions()->vkCreateComputePipelines( + core().vkDevice(), + VK_NULL_HANDLE, + 1, + &info, + nullptr, + &ptr + ); + + CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create compute pipeline") { + forget(); + throw cvk::vk_result_exception{result, details->exception()}; + } + + _handle.reset(ptr, core()); +} + +void BasePipeline::forget() {} +} diff --git a/cvk/src/render/pipeline/compute_pipeline.cpp b/cvk/src/render/pipeline/compute_pipeline.cpp new file mode 100644 index 00000000..26bfb684 --- /dev/null +++ b/cvk/src/render/pipeline/compute_pipeline.cpp @@ -0,0 +1,36 @@ +#include "cvk/render/pipeline/compute_pipeline.hpp" + +#include "cvk/render/pipeline/layout/pipeline_layout.hpp" + + +namespace cvk { +ComputePipeline::ComputePipeline(PipelineLayout const& layout, Config const& config) : + BasePipeline{layout, VK_PIPELINE_BIND_POINT_COMPUTE}, _config{config} {} + +ComputePipeline::ComputePipeline(PipelineLayout const& layout, Config const& config, State const& state) : + ComputePipeline{layout, config} { + wrap(state); +} + +ComputePipeline::ComputePipeline(PipelineLayout const& layout, Config const& config, create_t) : + ComputePipeline{layout, config} { + create(); +} + +void ComputePipeline::create() { + CTH_CRITICAL(_config.stage.stage != VK_SHADER_STAGE_COMPUTE_BIT, "the stage must be a compute shader") {} + CTH_CRITICAL(_config.stage.module == VK_NULL_HANDLE, "the stage needs a shader module") {} + + VkComputePipelineCreateInfo const info{ + .sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO, + .pNext = nullptr, + .flags = _config.flags, + .stage = _config.stage, + .layout = layout().get(), + .basePipelineHandle = VK_NULL_HANDLE, + .basePipelineIndex = -1, + }; + + BasePipeline::create(info); +} +} diff --git a/cvk/src/render/pipeline/graphics_pipeline.cpp b/cvk/src/render/pipeline/graphics_pipeline.cpp new file mode 100644 index 00000000..08bc928f --- /dev/null +++ b/cvk/src/render/pipeline/graphics_pipeline.cpp @@ -0,0 +1,128 @@ +#include "cvk/render/pipeline/graphics_pipeline.hpp" + +#include "cvk/base/core.hpp" +#include "cvk/render/pipeline/layout/pipeline_layout.hpp" +#include "cvk/utility/vk_exceptions.hpp" + + +namespace cvk { +GraphicsPipeline::GraphicsPipeline(PipelineLayout const& layout, Config config) : + BasePipeline{layout, VK_PIPELINE_BIND_POINT_GRAPHICS}, _config{std::move(config)} {} + +GraphicsPipeline::GraphicsPipeline(PipelineLayout const& layout, Config config, State const& state) : + GraphicsPipeline{layout, std::move(config)} { + wrap(state); +} + +GraphicsPipeline::GraphicsPipeline(PipelineLayout const& layout, Config config, create_t) : + GraphicsPipeline{layout, std::move(config)} { + create(); +} + +GraphicsPipeline::GraphicsPipeline(GraphicsPipeline const& parent, Config config) : + BasePipeline{parent.layout(), VK_PIPELINE_BIND_POINT_GRAPHICS}, + _vkParent{parent.get()}, + _config{std::move(config)} { + debug_check(parent); + CTH_CRITICAL( + (parent.config().flags & VK_PIPELINE_CREATE_ALLOW_DERIVATIVES_BIT) == 0, + "parent must allow derivatives (VK_PIPELINE_CREATE_ALLOW_DERIVATIVES_BIT)" + ) {} +} + +GraphicsPipeline::GraphicsPipeline(GraphicsPipeline const& parent, Config config, create_t) : + GraphicsPipeline{parent, std::move(config)} { + create(); +} + +void GraphicsPipeline::create() { + CTH_CRITICAL( + !_config.colorBlendAttachments.empty() && _config.colorBlendAttachments.size() != _config.colorFormats.size(), + "one color blend attachment per color format required, got {} for {}", + _config.colorBlendAttachments.size(), + _config.colorFormats.size() + ) {} + AttachmentMapping::debug_check(_config.attachmentMapping, _config.colorFormats.size()); + + auto const& config = _config; + + VkPipelineVertexInputStateCreateInfo const vertexInput{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .vertexBindingDescriptionCount = static_cast(config.vertexBindings.size()), + .pVertexBindingDescriptions = config.vertexBindings.data(), + .vertexAttributeDescriptionCount = static_cast(config.vertexAttributes.size()), + .pVertexAttributeDescriptions = config.vertexAttributes.data(), + }; + + VkPipelineViewportStateCreateInfo const viewport{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .viewportCount = config.viewportCount, + .pViewports = nullptr, + .scissorCount = config.scissorCount, + .pScissors = nullptr, + }; + + std::vector const defaultBlendAttachments( + config.colorBlendAttachments.empty() ? config.colorFormats.size() : 0, + Config::ALPHA_BLENDING + ); + auto const& blendAttachments = config.colorBlendAttachments.empty() + ? defaultBlendAttachments + : config.colorBlendAttachments; + + auto colorBlend = config.colorBlend; + colorBlend.attachmentCount = static_cast(blendAttachments.size()); + colorBlend.pAttachments = blendAttachments.data(); + + VkPipelineDynamicStateCreateInfo const dynamicState{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .dynamicStateCount = static_cast(config.dynamicStates.size()), + .pDynamicStates = config.dynamicStates.data(), + }; + + auto const colorAttachments = static_cast(config.colorFormats.size()); + auto const inputIndices = config.attachmentMapping.inputIndexInfo(colorAttachments); + auto locations = config.attachmentMapping.locationInfo(colorAttachments); + locations.pNext = &inputIndices; + + VkPipelineRenderingCreateInfo const rendering{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO, + .pNext = &locations, + .viewMask = config.viewMask, + .colorAttachmentCount = colorAttachments, + .pColorAttachmentFormats = config.colorFormats.data(), + .depthAttachmentFormat = config.depthFormat, + .stencilAttachmentFormat = config.stencilFormat, + }; + + VkGraphicsPipelineCreateInfo const info{ + .sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, + .pNext = &rendering, + .flags = config.flags | (_vkParent != VK_NULL_HANDLE ? VK_PIPELINE_CREATE_DERIVATIVE_BIT : 0u), + .stageCount = static_cast(config.stages.size()), + .pStages = config.stages.data(), + .pVertexInputState = &vertexInput, + .pInputAssemblyState = &config.inputAssembly, + .pTessellationState = nullptr, + .pViewportState = &viewport, + .pRasterizationState = &config.rasterization, + .pMultisampleState = &config.multisample, + .pDepthStencilState = &config.depthStencil, + .pColorBlendState = &colorBlend, + .pDynamicState = &dynamicState, + .layout = layout().get(), + .renderPass = VK_NULL_HANDLE, + .subpass = 0, + .basePipelineHandle = _vkParent, + .basePipelineIndex = -1, + }; + + BasePipeline::create(info); +} +} diff --git a/cvk/src/render/pipeline/graphics_pipeline_config.cpp b/cvk/src/render/pipeline/graphics_pipeline_config.cpp new file mode 100644 index 00000000..5880c255 --- /dev/null +++ b/cvk/src/render/pipeline/graphics_pipeline_config.cpp @@ -0,0 +1,22 @@ +#include "cvk/render/pipeline/graphics_pipeline_config.hpp" + +#include "cvk/render/pipeline/shader/shader.hpp" + +#include + + +namespace cvk { +GraphicsPipelineConfig& GraphicsPipelineConfig::addStage( + Shader const& shader, + VkSpecializationInfo const* specialization, + char const* entryPoint +) { + CTH_CRITICAL( + std::ranges::contains(stages, shader.stage(), &VkPipelineShaderStageCreateInfo::stage), + "stage already added" + ) {} + + stages.push_back(shader.stageInfo(specialization, entryPoint)); + return *this; +} +} diff --git a/cvk/src/render/pipeline/layout/descriptor_set_layout.cpp b/cvk/src/render/pipeline/layout/descriptor_set_layout.cpp new file mode 100644 index 00000000..4f8a7f6c --- /dev/null +++ b/cvk/src/render/pipeline/layout/descriptor_set_layout.cpp @@ -0,0 +1,85 @@ +#include "cvk/render/pipeline/layout/descriptor_set_layout.hpp" + +#include "cvk/base/core.hpp" +#include "cvk/utility/vk_exceptions.hpp" + +#include + + +namespace cvk { +DescriptorSetLayout::DescriptorSetLayout(Core const& core, Config config) : _handle{core} { + Core::debug_check(core); + Config::debug_check(config); + + _bindings = std::move(config.bindings); + _bindingFlags = std::move(config.bindingFlags); +} + +DescriptorSetLayout::DescriptorSetLayout(Core const& core, Config config, State const& state) : DescriptorSetLayout{ + core, + std::move(config) +} { wrap(state); } + +DescriptorSetLayout::DescriptorSetLayout(Core const& core, Config config, create_t) : DescriptorSetLayout{ + core, + std::move(config) +} { create(); } + +void DescriptorSetLayout::wrap(State const& state) { + optDestroy(); + + _handle.reset(state.vkLayout.get(), core()); +} + +void DescriptorSetLayout::create() { + optDestroy(); + + VkDescriptorSetLayoutBindingFlagsCreateInfo const flagsInfo{ + .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_BINDING_FLAGS_CREATE_INFO, + .pNext = nullptr, + .bindingCount = static_cast(_bindingFlags.size()), + .pBindingFlags = _bindingFlags.data(), + }; + VkDescriptorSetLayoutCreateInfo const info{ + .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, + .pNext = _bindingFlags.empty() ? nullptr : &flagsInfo, + .flags = updateAfterBind() ? VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT : 0u, + .bindingCount = static_cast(_bindings.size()), + .pBindings = _bindings.data(), + }; + + VkDescriptorSetLayout ptr = VK_NULL_HANDLE; + auto const result = core().functions()->vkCreateDescriptorSetLayout(core().vkDevice(), &info, nullptr, &ptr); + + CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create descriptor set layout") { + forget(); + throw cvk::vk_result_exception{result, details->exception()}; + } + + _handle.reset(ptr, core()); +} + +void DescriptorSetLayout::destroy() { + debug_check(*this); + + _handle.reset(); + + forget(); +} + +DescriptorSetLayout::State DescriptorSetLayout::release() { + debug_check(*this); + + State const state{_handle.release()}; + forget(); + return state; +} + +void DescriptorSetLayout::destroy(DeviceTable table, VkDescriptorSetLayout vkLayout) { + CTH_WARN(vkLayout == VK_NULL_HANDLE, "vkLayout invalid (VK_NULL_HANDLE)") {} + + table->vkDestroyDescriptorSetLayout(table.device(), vkLayout, nullptr); +} + +void DescriptorSetLayout::forget() {} +} diff --git a/cvk/src/render/pipeline/layout/pipeline_layout.cpp b/cvk/src/render/pipeline/layout/pipeline_layout.cpp new file mode 100644 index 00000000..5a18037a --- /dev/null +++ b/cvk/src/render/pipeline/layout/pipeline_layout.cpp @@ -0,0 +1,96 @@ +#include "cvk/render/pipeline/layout/pipeline_layout.hpp" + +#include "cvk/base/core.hpp" +#include "cvk/base/physical_device.hpp" +#include "cvk/render/pipeline/layout/descriptor_set_layout.hpp" +#include "cvk/utility/vk_exceptions.hpp" + +#include + + +namespace cvk { +PipelineLayout::PipelineLayout(Core const& core, Config const& config) : _handle{core}, + _pushConstantRanges{config.pushConstantRanges} { + Core::debug_check(core); + + _vkSetLayouts.reserve(config.setLayouts.size()); + for(auto const& layout : config.setLayouts) { + DescriptorSetLayout::debug_check(*layout); + _vkSetLayouts.push_back(layout->get()); + } +} + +PipelineLayout::PipelineLayout(Core const& core, Config const& config, State const& state) : PipelineLayout{ + core, + config +} { wrap(state); } + +PipelineLayout::PipelineLayout(Core const& core, Config const& config, create_t) : PipelineLayout{core, config} { + create(); +} + +void PipelineLayout::wrap(State const& state) { + optDestroy(); + + _handle.reset(state.vkLayout.get(), core()); +} + +void PipelineLayout::create() { + optDestroy(); + + auto const maxSets = core().physicalDevice().limits().maxBoundDescriptorSets; + CTH_STABLE_ERR( + _vkSetLayouts.size() > maxSets, + "more set layouts than maxBoundDescriptorSets, got {} for {}", + _vkSetLayouts.size(), + maxSets + ) { + forget(); + throw cvk::cvk_exception{details->exception()}; + } + + VkPipelineLayoutCreateInfo const info{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .setLayoutCount = static_cast(_vkSetLayouts.size()), + .pSetLayouts = _vkSetLayouts.data(), + .pushConstantRangeCount = static_cast(_pushConstantRanges.size()), + .pPushConstantRanges = _pushConstantRanges.data(), + }; + + VkPipelineLayout ptr = VK_NULL_HANDLE; + auto const result = core().functions()->vkCreatePipelineLayout(core().vkDevice(), &info, nullptr, &ptr); + + CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create pipeline layout") { + forget(); + throw cvk::vk_result_exception{result, details->exception()}; + } + + _handle.reset(ptr, core()); +} + +void PipelineLayout::destroy() { + debug_check(*this); + + _handle.reset(); + + forget(); +} + +PipelineLayout::State PipelineLayout::release() { + debug_check(*this); + + State const state{_handle.release()}; + forget(); + return state; +} + +void PipelineLayout::destroy(DeviceTable table, VkPipelineLayout vkLayout) { + CTH_WARN(vkLayout == VK_NULL_HANDLE, "vkLayout invalid (VK_NULL_HANDLE)") {} + + table->vkDestroyPipelineLayout(table.device(), vkLayout, nullptr); +} + +void PipelineLayout::forget() {} +} diff --git a/cvk/src/render/pipeline/shader/shader.cpp b/cvk/src/render/pipeline/shader/shader.cpp new file mode 100644 index 00000000..86f37539 --- /dev/null +++ b/cvk/src/render/pipeline/shader/shader.cpp @@ -0,0 +1,190 @@ +#include "cvk/render/pipeline/shader/shader.hpp" + +#include "cvk/base/core.hpp" +#include "cvk/utility/constants.hpp" +#include "cvk/utility/vk_exceptions.hpp" + +#include +#include + +#include +#include +#include + + +namespace cvk { +Shader::Shader(Core const& core, VkShaderStageFlagBits stage) : _handle{core}, _stage{stage} { + Core::debug_check(core); +} + +Shader::Shader(Core const& core, VkShaderStageFlagBits stage, State const& state) : Shader{core, stage} { + wrap(state); +} + +Shader::Shader(Core const& core, VkShaderStageFlagBits stage, std::span spv) : Shader{core, stage} { + create(spv); +} + +Shader::Shader(Core const& core, VkShaderStageFlagBits stage, path_t const& spvPath) : Shader{core, stage} { + create(spvPath); +} + +Shader::Shader( + Core const& core, + VkShaderStageFlagBits stage, + path_t const& spvPath, + path_t const& glslPath, + path_t const& compilerPath +) : Shader{core, stage} { + CTH_STABLE_WARN(!constants::DEBUG_MODE, "compiles shaders at run time, use this in debug builds only") {} + + compile(glslPath, spvPath, compilerPath); + create(spvPath); +} + +void Shader::wrap(State const& state) { + optDestroy(); + + _handle.reset(state.vkShader.get(), core()); +} + +void Shader::create(std::span spv) { + CTH_CRITICAL(spv.empty(), "spv must not be empty") {} + + optDestroy(); + + VkShaderModuleCreateInfo const info{ + .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .codeSize = spv.size_bytes(), + .pCode = spv.data(), + }; + + VkShaderModule ptr = VK_NULL_HANDLE; + auto const result = core().functions()->vkCreateShaderModule(core().vkDevice(), &info, nullptr, &ptr); + + CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create shader module") { + forget(); + throw cvk::vk_result_exception{result, details->exception()}; + } + + _handle.reset(ptr, core()); +} + +void Shader::create(path_t const& spvPath) { + auto const spv = load(spvPath); + create(spv); +} + +void Shader::destroy() { + debug_check(*this); + + _handle.reset(); + + forget(); +} + +Shader::State Shader::release() { + debug_check(*this); + + State const state{_handle.release()}; + forget(); + return state; +} + +VkPipelineShaderStageCreateInfo Shader::stageInfo( + VkSpecializationInfo const* specialization, + char const* entryPoint +) const { + debug_check(*this); + + return VkPipelineShaderStageCreateInfo{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .stage = _stage, + .module = _handle.get(), + .pName = entryPoint, + .pSpecializationInfo = specialization, + }; +} + +std::vector Shader::load(path_t const& spvPath) { + CVK_STABLE_THROW(!std::filesystem::exists(spvPath), "SPIR-V file {} does not exist", spvPath.string()) {} + + auto const size = std::filesystem::file_size(spvPath); + CVK_STABLE_THROW( + size == 0 || size % sizeof(uint32_t) != 0, + "SPIR-V file size must be a multiple of 4, got {} for {}", + size, + spvPath.string() + ) {} + + std::vector spv(size / sizeof(uint32_t)); + + std::ifstream file{spvPath, std::ios::binary}; + file.read(reinterpret_cast(spv.data()), static_cast(size)); + CVK_STABLE_THROW(!file, "failed to read SPIR-V file {}", spvPath.string()) {} + + return spv; +} + +void Shader::compile( + path_t const& glslPath, + path_t const& spvPath, + path_t const& compilerPath, + std::string_view flags +) { + CVK_STABLE_THROW(!std::filesystem::exists(glslPath), "glsl file {} does not exist", glslPath.string()) {} + +#ifdef CTH_PLATFORM_WINDOWS + constexpr std::string_view nullDevice = "NUL"; +#else + constexpr std::string_view nullDevice = "/dev/null"; +#endif + // one log per SPIR-V path: two processes that compile into other directories do not share a log + auto logPath = spvPath; + logPath += ".log"; + + auto command = std::format( + R"("{}" {} -c "{}" -o "{}" >{} 2>"{}")", + compilerPath.string(), + flags, + glslPath.string(), + spvPath.string(), + nullDevice, + logPath.string() + ); +#ifdef CTH_PLATFORM_WINDOWS + // cmd.exe /c removes the outer quotes of a command that starts with a quote + command = std::format("\"{}\"", command); +#endif + + int const result = std::system(command.c_str()); + + std::vector output{}; + if(std::filesystem::exists(logPath)) { + output = cth::io::file::chop(logPath); + std::erase_if(output, [](std::string const& line) { return line.empty(); }); + std::filesystem::remove(logPath); + } + + CVK_STABLE_THROW(result != 0, "shader compilation of {} failed", glslPath.string()) { + details->add("command: {}", command); + for(auto const& line : output) details->add("{}", line); + } + + CTH_STABLE_WARN(!output.empty(), "shader compiler output for {}", glslPath.string()) { + for(auto const& line : output) details->add("{}", line); + } +} + +void Shader::destroy(DeviceTable table, VkShaderModule vkShader) { + CTH_WARN(vkShader == VK_NULL_HANDLE, "vkShader invalid (VK_NULL_HANDLE)") {} + + table->vkDestroyShaderModule(table.device(), vkShader, nullptr); +} + +void Shader::forget() {} +} diff --git a/cvk/src/render/sync/barrier.cpp b/cvk/src/render/sync/barrier.cpp new file mode 100644 index 00000000..d6773eab --- /dev/null +++ b/cvk/src/render/sync/barrier.cpp @@ -0,0 +1,232 @@ +#include "cvk/render/sync/barrier.hpp" + +#include "cvk/base/core.hpp" +#include "cvk/data/buffer/base_buffer.hpp" +#include "cvk/data/img/image.hpp" +#include "cvk/data/img/image_view.hpp" +#include "cvk/render/cmd/cmd_buffer.hpp" + +#include + +#include +#include +#include + + +namespace cvk { +namespace { + constexpr VkPipelineStageFlags2 FRAMEBUFFER_STAGES = VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT + | VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT + | VK_PIPELINE_STAGE_2_LATE_FRAGMENT_TESTS_BIT + | VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT; + + [[nodiscard]] constexpr bool framebuffer_space(VkPipelineStageFlags2 srcStages, VkPipelineStageFlags2 dstStages) { + return ((srcStages | dstStages) & ~FRAMEBUFFER_STAGES) == 0; + } + + VkMemoryBarrier2 memory_barrier(MemoryBarrierInfo const& info) { + bool const transfer = info.src.queueFamilyIndex != VK_QUEUE_FAMILY_IGNORED + || info.dst.queueFamilyIndex != VK_QUEUE_FAMILY_IGNORED; + CTH_CRITICAL(transfer, "memory barriers have no queue family ownership transfer") {} + + return VkMemoryBarrier2{ + .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2, + .pNext = nullptr, + .srcStageMask = info.src.stages, + .srcAccessMask = info.src.access, + .dstStageMask = info.dst.stages, + .dstAccessMask = info.dst.access, + }; + } + + VkBufferMemoryBarrier2 buffer_barrier(BaseBuffer const& buffer, BufferBarrierInfo const& info) { + BaseBuffer::debug_check(buffer); + CTH_CRITICAL( + info.offset > buffer.size() || (info.size != VK_WHOLE_SIZE && info.size > buffer.size() - info.offset), + "range must lie in the buffer" + ) {} + + return VkBufferMemoryBarrier2{ + .sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2, + .pNext = nullptr, + .srcStageMask = info.src.stages, + .srcAccessMask = info.src.access, + .dstStageMask = info.dst.stages, + .dstAccessMask = info.dst.access, + .srcQueueFamilyIndex = info.src.queueFamilyIndex, + .dstQueueFamilyIndex = info.dst.queueFamilyIndex, + .buffer = buffer.get(), + .offset = info.offset, + .size = info.size, + }; + } + + VkImageMemoryBarrier2 image_barrier(Image const& image, ImageBarrierInfo const& info) { + Image::debug_check(image); + CTH_CRITICAL(info.mipLevels == 0, "mip levels must not be 0") {} + CTH_CRITICAL(info.firstMipLevel >= image.mipLevels(), "first mip level must lie in the image") {} + CTH_CRITICAL( + info.mipLevels != VK_REMAINING_MIP_LEVELS && info.mipLevels > image.mipLevels() - info.firstMipLevel, + "mip levels must lie in the image" + ) {} + CTH_CRITICAL(info.arrayLayers == 0, "array layers must not be 0") {} + CTH_CRITICAL(info.firstArrayLayer >= image.arrayLayers(), "first array layer must lie in the image") {} + CTH_CRITICAL( + info.arrayLayers != VK_REMAINING_ARRAY_LAYERS + && info.arrayLayers > image.arrayLayers() - info.firstArrayLayer, + "array layers must lie in the image" + ) {} + + bool const transfer = info.src.queueFamilyIndex != info.dst.queueFamilyIndex; + CTH_CRITICAL( + (info.oldLayout != info.newLayout || transfer) + && (info.newLayout == VK_IMAGE_LAYOUT_UNDEFINED || info.newLayout == VK_IMAGE_LAYOUT_PREINITIALIZED), + "a transition or queue family transfer must not have an undefined or preinitialized new layout, " + "got {} -> {}", + info.oldLayout, + info.newLayout + ) {} + + return VkImageMemoryBarrier2{ + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, + .pNext = nullptr, + .srcStageMask = info.src.stages, + .srcAccessMask = info.src.access, + .dstStageMask = info.dst.stages, + .dstAccessMask = info.dst.access, + .oldLayout = info.oldLayout, + .newLayout = info.newLayout, + .srcQueueFamilyIndex = info.src.queueFamilyIndex, + .dstQueueFamilyIndex = info.dst.queueFamilyIndex, + .image = image.get(), + .subresourceRange = { + .aspectMask = info.aspectMask != 0 ? info.aspectMask : image.aspectMask(), + .baseMipLevel = info.firstMipLevel, + .levelCount = info.mipLevels, + .baseArrayLayer = info.firstArrayLayer, + .layerCount = info.arrayLayers, + }, + }; + } +} + +void Barrier::debug_check_rendering( + RenderingInfo const& rendering, + std::span memoryBarriers, + std::span bufferBarriers, + std::span imageBarriers +) const { + bool srcStages = false; + bool framebufferSpace = true; + auto const check = [&](VkPipelineStageFlags2 src, VkPipelineStageFlags2 dst) { + srcStages |= src != VK_PIPELINE_STAGE_2_NONE; + framebufferSpace &= framebuffer_space(src, dst); + }; + + for(auto const& barrier : memoryBarriers) + check(barrier.srcStageMask, barrier.dstStageMask); + for(auto const& barrier : bufferBarriers) + check(barrier.srcStageMask, barrier.dstStageMask); + for(auto const& barrier : imageBarriers) + check(barrier.srcStageMask, barrier.dstStageMask); + + CTH_CRITICAL(!framebufferSpace, "barriers inside a rendering must only use framebuffer space stages") {} + CTH_CRITICAL( + srcStages && (flags & VK_DEPENDENCY_BY_REGION_BIT) == 0, + "barriers inside a rendering need VK_DEPENDENCY_BY_REGION_BIT" + ) {} + CTH_CRITICAL( + std::popcount(rendering.viewMask) > 1 && (flags & VK_DEPENDENCY_VIEW_LOCAL_BIT) == 0, + "barriers inside a rendering with more than one view need VK_DEPENDENCY_VIEW_LOCAL_BIT" + ) {} + auto const transfer = [](VkBufferMemoryBarrier2 const& barrier) { + return barrier.srcQueueFamilyIndex != barrier.dstQueueFamilyIndex; + }; + CTH_CRITICAL( + std::ranges::any_of(bufferBarriers, transfer), + "barriers inside a rendering have no queue family ownership transfer" + ) {} + + auto const attached = [&rendering](Image const& image) { + auto const uses = [&image](RenderingAttachment const& attachment) { + return &attachment.view->image() == &image + || (attachment.resolveView != nullptr && &attachment.resolveView->image() == &image); + }; + return std::ranges::any_of(rendering.colorAttachments, uses) + || (rendering.depthAttachment.has_value() && uses(*rendering.depthAttachment)) + || (rendering.stencilAttachment.has_value() && uses(*rendering.stencilAttachment)); + }; + + for(auto const [barrier, entry] : std::views::zip(imageBarriers, images)) { + CTH_CRITICAL(barrier.oldLayout != barrier.newLayout, "barriers inside a rendering have no layout transition") {} + CTH_CRITICAL( + barrier.srcQueueFamilyIndex != barrier.dstQueueFamilyIndex, + "barriers inside a rendering have no queue family ownership transfer" + ) {} + CTH_CRITICAL( + attached(*entry.image) && barrier.newLayout != VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR + && barrier.newLayout != VK_IMAGE_LAYOUT_GENERAL, + "attachments inside a rendering must be in the rendering local read or general layout, got {}", + barrier.newLayout + ) {} + } +} + +void Barrier::addTransition( + Image const& image, + VkImageLayout newLayout, + VkImageLayout oldLayout, + VkPipelineStageFlags2 shaderStages, + uint32_t firstMipLevel, + uint32_t mipLevels +) { + auto const transition = layout_transition(newLayout, oldLayout, image.aspectMask(), shaderStages); + CTH_CRITICAL( + !transition.has_value(), + "layout transition {} -> {} has no scope for a layout, use add() with scopes", + oldLayout, + newLayout + ) {} + + add( + image, + ImageInfo::LayoutTransition(newLayout, oldLayout, transition->dst, transition->src, firstMipLevel, mipLevels) + ); +} + +void Barrier::execute(BaseCmdBuffer const& cmdBuffer) const { + BaseCmdBuffer::debug_check(cmdBuffer); + CTH_CRITICAL(!cmdBuffer.recording(), "command buffer must be recording") {} + + std::vector memoryBarriers{}; + memoryBarriers.reserve(memory.size()); + for(auto const& info : memory) + memoryBarriers.push_back(memory_barrier(info)); + + std::vector bufferBarriers{}; + bufferBarriers.reserve(buffers.size()); + for(auto const& [buffer, info] : buffers) + bufferBarriers.push_back(buffer_barrier(*buffer, info)); + + std::vector imageBarriers{}; + imageBarriers.reserve(images.size()); + for(auto const& [image, info] : images) + imageBarriers.push_back(image_barrier(*image, info)); + + if(auto const* rendering = cmdBuffer.rendering()) + debug_check_rendering(*rendering, memoryBarriers, bufferBarriers, imageBarriers); + + VkDependencyInfo const dependency{ + .sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, + .pNext = nullptr, + .dependencyFlags = flags, + .memoryBarrierCount = static_cast(memoryBarriers.size()), + .pMemoryBarriers = memoryBarriers.data(), + .bufferMemoryBarrierCount = static_cast(bufferBarriers.size()), + .pBufferMemoryBarriers = bufferBarriers.data(), + .imageMemoryBarrierCount = static_cast(imageBarriers.size()), + .pImageMemoryBarriers = imageBarriers.data(), + }; + cmdBuffer.core().functions()->vkCmdPipelineBarrier2(cmdBuffer.get(), &dependency); +} +} diff --git a/cvk/src/render/sync/fence.cpp b/cvk/src/render/sync/fence.cpp new file mode 100644 index 00000000..c5838943 --- /dev/null +++ b/cvk/src/render/sync/fence.cpp @@ -0,0 +1,171 @@ +#include "cvk/render/sync/fence.hpp" + +#include "cvk/base/core.hpp" +#include "cvk/utility/constant/os_constants.hpp" +#include "cvk/utility/vk_exceptions.hpp" + +#include + +#ifdef CTH_PLATFORM_WINDOWS +#define NOMINMAX +#define WIN32_LEAN_AND_MEAN +#include + +#include +#endif + + +namespace cvk { +Fence::Fence(Core const& core) : _handle{core} { Core::debug_check(core); } + +Fence::Fence(Core const& core, State const& state) : Fence{core} { wrap(state); } + +Fence::Fence(Core const& core, create_t, VkFenceCreateFlags flags, VkExternalFenceHandleTypeFlags exportTypes) : + Fence{core} { + create(flags, exportTypes); +} + +void Fence::wrap(State const& state) { + optDestroy(); + + _handle.reset(state.vkFence.get(), core()); + _exportTypes = state.exportTypes; +} + +void Fence::create(VkFenceCreateFlags flags, VkExternalFenceHandleTypeFlags exportTypes) { + optDestroy(); + + VkExportFenceCreateInfo const exportInfo{ + .sType = VK_STRUCTURE_TYPE_EXPORT_FENCE_CREATE_INFO, + .pNext = nullptr, + .handleTypes = exportTypes, + }; + VkFenceCreateInfo const info{ + .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, + .pNext = exportTypes != 0 ? &exportInfo : nullptr, + .flags = flags, + }; + + VkFence ptr = VK_NULL_HANDLE; + auto const result = core().functions()->vkCreateFence(core().vkDevice(), &info, nullptr, &ptr); + + CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create fence") { + forget(); + throw cvk::vk_result_exception{result, details->exception()}; + } + + _handle.reset(ptr, core()); + _exportTypes = exportTypes; +} + +void Fence::destroy() { + debug_check(*this); + + _handle.reset(); + + forget(); +} + +Fence::State Fence::release() { + debug_check(*this); + + State const state{_handle.release(), _exportTypes}; + forget(); + return state; +} + +VkResult Fence::status() const { + debug_check(*this); + + auto const result = core().functions()->vkGetFenceStatus(core().vkDevice(), _handle.get()); + + CVK_RESULT_STABLE_THROW( + result != VK_SUCCESS && result != VK_NOT_READY, + result, + "failed to get fence status" + ) {} + + return result; +} + +void Fence::reset() const { + debug_check(*this); + + auto const vkFence = _handle.get(); + auto const result = core().functions()->vkResetFences(core().vkDevice(), 1, &vkFence); + + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to reset fence") {} +} + +VkResult Fence::wait(uint64_t timeout) const { + debug_check(*this); + + auto const vkFence = _handle.get(); + auto const result = core().functions()->vkWaitForFences(core().vkDevice(), 1, &vkFence, VK_TRUE, timeout); + + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS && result != VK_TIMEOUT, result, "failed to wait for fence") {} + + return result; +} + +void Fence::wait() const { [[maybe_unused]] auto const result = wait(std::numeric_limits::max()); } + +VkResult Fence::waitReset(uint64_t timeout) const { + auto const result = wait(timeout); + if(result == VK_TIMEOUT) return result; + + reset(); + return result; +} + +void Fence::waitReset() const { + wait(); + reset(); +} + +cth::os::unique_native_handle Fence::extractOsHandle() const { + debug_check(*this); + CTH_CRITICAL( + (_exportTypes & constants::OS_FENCE_HANDLE_TYPE) == 0, + "fence must be created with constants::OS_FENCE_HANDLE_TYPE in the export types" + ) {} + +#ifdef CTH_PLATFORM_WINDOWS + // volk loads platform functions only if it was built with the platform define + auto const getHandle = reinterpret_cast( + vkGetDeviceProcAddr(core().vkDevice(), "vkGetFenceWin32HandleKHR") + ); + CVK_STABLE_THROW(getHandle == nullptr, "vkGetFenceWin32HandleKHR not available") {} + + VkFenceGetWin32HandleInfoKHR const info{ + .sType = VK_STRUCTURE_TYPE_FENCE_GET_WIN32_HANDLE_INFO_KHR, + .pNext = nullptr, + .fence = _handle.get(), + .handleType = constants::OS_FENCE_HANDLE_TYPE, + }; + HANDLE handle = nullptr; + auto const result = getHandle(core().vkDevice(), &info, &handle); +#else + VkFenceGetFdInfoKHR const info{ + .sType = VK_STRUCTURE_TYPE_FENCE_GET_FD_INFO_KHR, + .pNext = nullptr, + .fence = _handle.get(), + .handleType = constants::OS_FENCE_HANDLE_TYPE, + }; + int handle = cth::os::invalid_handle(); + auto const result = core().functions()->vkGetFenceFdKHR(core().vkDevice(), &info, &handle); +#endif + + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to export the fence") {} + + return cth::os::unique_native_handle{handle}; +} + +void Fence::destroy(DeviceTable table, VkFence vkFence) { + CTH_WARN(vkFence == VK_NULL_HANDLE, "vkFence invalid (VK_NULL_HANDLE)") {} + + table->vkDestroyFence(table.device(), vkFence, nullptr); +} + +void Fence::forget() { _exportTypes = 0; } +} diff --git a/cvk/src/render/sync/semaphore.cpp b/cvk/src/render/sync/semaphore.cpp new file mode 100644 index 00000000..79fa5fab --- /dev/null +++ b/cvk/src/render/sync/semaphore.cpp @@ -0,0 +1,66 @@ +#include "cvk/render/sync/semaphore.hpp" + +#include "cvk/base/core.hpp" +#include "cvk/utility/vk_exceptions.hpp" + + +namespace cvk { +Semaphore::Semaphore(Core const& core) : _handle{core} { Core::debug_check(core); } + +Semaphore::Semaphore(Core const& core, State const& state) : Semaphore{core} { wrap(state); } + +Semaphore::Semaphore(Core const& core, create_t) : Semaphore{core} { create(); } + +void Semaphore::wrap(State const& state) { + optDestroy(); + + _handle.reset(state.vkSemaphore.get(), core()); +} + +void Semaphore::create() { + constexpr VkSemaphoreCreateInfo info{ + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + }; + create(info); +} + +void Semaphore::destroy() { + debug_check(*this); + + _handle.reset(); + + forget(); +} + +Semaphore::State Semaphore::release() { + debug_check(*this); + + State const state{_handle.release()}; + forget(); + return state; +} + +void Semaphore::destroy(DeviceTable table, VkSemaphore vkSemaphore) { + CTH_WARN(vkSemaphore == VK_NULL_HANDLE, "vkSemaphore invalid (VK_NULL_HANDLE)") {} + + table->vkDestroySemaphore(table.device(), vkSemaphore, nullptr); +} + +void Semaphore::create(VkSemaphoreCreateInfo const& info) { + optDestroy(); + + VkSemaphore ptr = VK_NULL_HANDLE; + auto const result = core().functions()->vkCreateSemaphore(core().vkDevice(), &info, nullptr, &ptr); + + CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create semaphore") { + forget(); + throw cvk::vk_result_exception{result, details->exception()}; + } + + _handle.reset(ptr, core()); +} + +void Semaphore::forget() {} +} diff --git a/cvk/src/render/sync/timeline_semaphore.cpp b/cvk/src/render/sync/timeline_semaphore.cpp new file mode 100644 index 00000000..7d768710 --- /dev/null +++ b/cvk/src/render/sync/timeline_semaphore.cpp @@ -0,0 +1,105 @@ +#include "cvk/render/sync/timeline_semaphore.hpp" + +#include "cvk/base/core.hpp" +#include "cvk/utility/vk_exceptions.hpp" + +#include +#include + + +namespace cvk { +TimelineSemaphore::TimelineSemaphore(Core const& core) : Semaphore{core} {} + +TimelineSemaphore::TimelineSemaphore(Core const& core, State const& state) : TimelineSemaphore{core} { + wrap(state); +} + +TimelineSemaphore::TimelineSemaphore(Core const& core, create_t, uint64_t initialValue) : TimelineSemaphore{core} { + create(initialValue); +} + +void TimelineSemaphore::wrap(State const& state) { + Semaphore::wrap(Semaphore::State{state.vkSemaphore}); + _value = state.value; +} + +void TimelineSemaphore::create(uint64_t initialValue) { + VkSemaphoreTypeCreateInfo const typeInfo{ + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO, + .pNext = nullptr, + .semaphoreType = VK_SEMAPHORE_TYPE_TIMELINE, + .initialValue = initialValue, + }; + VkSemaphoreCreateInfo const info{ + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, + .pNext = &typeInfo, + .flags = 0, + }; + Semaphore::create(info); + + _value = initialValue; +} + +TimelineSemaphore::State TimelineSemaphore::release() { + auto const [vkSemaphore] = Semaphore::release(); + return State{vkSemaphore, std::exchange(_value, 0)}; +} + +uint64_t TimelineSemaphore::signal() { + debug_check(*this); + + auto const table = core().deviceTable(); + VkSemaphoreSignalInfo const info{ + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_SIGNAL_INFO, + .pNext = nullptr, + .semaphore = get(), + .value = next(), + }; + auto const result = table->vkSignalSemaphore(table.device(), &info); + + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to signal semaphore") {} + + return info.value; +} + +VkResult TimelineSemaphore::wait(uint64_t value, uint64_t timeout) const { + debug_check(*this); + + auto const table = core().deviceTable(); + auto const vkSemaphore = get(); + + VkSemaphoreWaitInfo const info{ + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO, + .pNext = nullptr, + .flags = 0, + .semaphoreCount = 1, + .pSemaphores = &vkSemaphore, + .pValues = &value, + }; + auto const result = table->vkWaitSemaphores(table.device(), &info, timeout); + + CVK_RESULT_STABLE_THROW( + result != VK_SUCCESS && result != VK_TIMEOUT, + result, + "failed to wait for semaphore" + ) {} + + return result; +} + +void TimelineSemaphore::wait(uint64_t value) const { + [[maybe_unused]] auto const result = wait(value, std::numeric_limits::max()); +} + +uint64_t TimelineSemaphore::gpuValue() const { + debug_check(*this); + + auto const table = core().deviceTable(); + uint64_t value = 0; + auto const result = table->vkGetSemaphoreCounterValue(table.device(), get(), &value); + + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to get semaphore counter value") {} + + return value; +} +} diff --git a/cvk/src/surface/surface.cpp b/cvk/src/surface/surface.cpp new file mode 100644 index 00000000..c43664ba --- /dev/null +++ b/cvk/src/surface/surface.cpp @@ -0,0 +1,94 @@ +#include "cvk/surface/surface.hpp" + +#include "cvk/base/instance.hpp" +#include "cvk/base/physical_device.hpp" +#include "cvk/utility/enumerate.hpp" +#include "cvk/utility/vk_exceptions.hpp" + + +namespace cvk { +using std::vector; + +Surface::Surface(Instance const& instance) : _handle{instance.get()} {} + +Surface::Surface(Instance const& instance, State const& state) : Surface{instance} { wrap(state); } + +void Surface::wrap(State const& state) { + debug_check_handle(state.vkSurface); + optDestroy(); + + _handle.reset(state.vkSurface.get(), _handle.vkInstance()); +} + +void Surface::destroy() { + debug_check(*this); + + _handle.reset(); + + forget(); +} + +Surface::State Surface::release() { + debug_check(*this); + State const state{_handle.release()}; + + forget(); + return state; +} + +bool Surface::supportsFamily(PhysicalDevice const& physicalDevice, uint32_t familyIndex) const { + debug_check(*this); + + VkBool32 support = false; + auto const result = vkGetPhysicalDeviceSurfaceSupportKHR( + physicalDevice.get(), + familyIndex, + _handle.get(), + &support + ); + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "device-surface support query failed") {} + + return support; +} + +vector Surface::presentModes(PhysicalDevice const& physicalDevice) const { + debug_check(*this); + + return dev::enumerate( + "surface present modes", + vkGetPhysicalDeviceSurfacePresentModesKHR, + physicalDevice.get(), + _handle.get() + ); +} + +vector Surface::formats(PhysicalDevice const& physicalDevice) const { + debug_check(*this); + + return dev::enumerate("surface formats", vkGetPhysicalDeviceSurfaceFormatsKHR, physicalDevice.get(), _handle.get()); +} + +VkSurfaceCapabilitiesKHR Surface::capabilities(PhysicalDevice const& physicalDevice) const { + debug_check(*this); + + VkSurfaceCapabilitiesKHR capabilities{}; + auto const result = vkGetPhysicalDeviceSurfaceCapabilitiesKHR( + physicalDevice.get(), + _handle.get(), + &capabilities + ); + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "device-surface capabilities query failed") {} + + return capabilities; +} + +void Surface::destroy(vk_not_null vkInstance, VkSurfaceKHR vkSurface) { + CTH_WARN(vkSurface == VK_NULL_HANDLE, "vkSurface invalid (VK_NULL_HANDLE)") {} + + vkDestroySurfaceKHR(vkInstance.get(), vkSurface, nullptr); + + log::msg("destroyed surface"); +} + +void Surface::forget() {} +} diff --git a/cvk/src/surface/swapchain.cpp b/cvk/src/surface/swapchain.cpp new file mode 100644 index 00000000..560fed3e --- /dev/null +++ b/cvk/src/surface/swapchain.cpp @@ -0,0 +1,334 @@ +#include "cvk/surface/swapchain.hpp" + +#include "cvk/base/core.hpp" +#include "cvk/base/physical_device.hpp" +#include "cvk/surface/surface.hpp" +#include "cvk/utility/enumerate.hpp" +#include "cvk/utility/vk_exceptions.hpp" +#include "cvk/utility/vk_overloads.hpp" + +#include +#include + + +namespace cvk { +using std::span; +using std::vector; + +Swapchain::Swapchain(Core const& core, Queue const& presentQueue, Surface const& surface, Config config) : + _presentQueue{presentQueue}, _surface{&surface}, _core{&core}, _config{std::move(config)} { + Core::debug_check(core); + Surface::debug_check(surface); + Queue::debug_check_present(presentQueue); + Config::debug_check(_config); +} + +Swapchain::Swapchain( + Core const& core, + Queue const& presentQueue, + Surface const& surface, + Config config, + State const& state +) : Swapchain{core, presentQueue, surface, std::move(config)} { wrap(state); } + +Swapchain::Swapchain( + Core const& core, + Queue const& presentQueue, + Surface const& surface, + Config config, + VkExtent2D extent +) : Swapchain{core, presentQueue, surface, std::move(config)} { create(extent); } + +void Swapchain::wrap(State const& state) { + optDestroy(); + + _generations.emplace_back(core_handle{state.vkSwapchain.get(), core()}); + _extent = state.extent; + _surfaceFormat = state.surfaceFormat; + _presentMode = state.presentMode; + + createImages(); +} + +void Swapchain::create(VkExtent2D extent) { + CTH_CRITICAL(extent.width == 0 || extent.height == 0, "extent width and height must be > 0, got {}", extent) {} + optDestroy(); + + auto const [surfaceFormat, presentMode] = pick(_config); + _surfaceFormat = surfaceFormat; + _presentMode = presentMode; + createSwapchain(extent, VK_NULL_HANDLE); +} + +void Swapchain::resize(VkExtent2D extent) { + debug_check(*this); + CTH_CRITICAL(extent.width == 0 || extent.height == 0, "extent width and height must be > 0, got {}", extent) {} + + unacquireAll(); + createSwapchain(extent, get()); +} + +void Swapchain::recreate(VkExtent2D extent, Config config) { + debug_check(*this); + CTH_CRITICAL(extent.width == 0 || extent.height == 0, "extent width and height must be > 0, got {}", extent) {} + Config::debug_check(config); + + auto const [surfaceFormat, presentMode] = pick(config); + + unacquireAll(); + _config = std::move(config); + _surfaceFormat = surfaceFormat; + _presentMode = presentMode; + createSwapchain(extent, get()); +} + +void Swapchain::destroy() { + debug_check(*this); + + _generations.clear(); + + forget(); +} + +Swapchain::State Swapchain::release() { + debug_check(*this); + + State const state{ + .vkSwapchain = current().handle.release(), + .extent = _extent, + .surfaceFormat = _surfaceFormat, + .presentMode = _presentMode, + }; + // waits for the present fences, releases the images, the current generation has no handle to destroy + _generations.clear(); + + forget(); + return state; +} + +Swapchain::Acquired Swapchain::acquire(Semaphore const& signal, uint64_t timeout) { + debug_check(*this); + Semaphore::debug_check(signal); + + collectRetired(); + + uint32_t imageIndex = 0; + auto const result = core().functions()->vkAcquireNextImageKHR( + core().vkDevice(), + get(), + timeout, + signal.get(), + VK_NULL_HANDLE, + &imageIndex + ); + + if(result == VK_SUCCESS || result == VK_SUBOPTIMAL_KHR) { + current().acquired[imageIndex] = true; + return {result, imageIndex}; + } + + CVK_RESULT_STABLE_THROW( + result != VK_ERROR_OUT_OF_DATE_KHR && result != VK_TIMEOUT && result != VK_NOT_READY, + result, + "failed to acquire a swapchain image" + ) {} + + return {result, std::nullopt}; +} + +VkResult Swapchain::present(uint32_t imageIndex) { + debug_check(*this); + CTH_CRITICAL(imageIndex >= imageCount() || !acquired(imageIndex), "image {} must be acquired", imageIndex) {} + + collectRetired(); + + auto& generation = current(); + + // signaled by the last present of the image or created signaled + auto const& fence = generation.presentFences[imageIndex]; + fence.waitReset(); + + VkFence const vkFence = fence.get(); + VkSwapchainPresentFenceInfoKHR const fenceInfo{ + .sType = VK_STRUCTURE_TYPE_SWAPCHAIN_PRESENT_FENCE_INFO_KHR, + .pNext = nullptr, + .swapchainCount = 1, + .pFences = &vkFence, + }; + + VkSemaphore const vkSemaphore = generation.presentSemaphores[imageIndex].get(); + VkSwapchainKHR const vkSwapchain = generation.handle.get(); + VkPresentInfoKHR const info{ + .sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR, + .pNext = &fenceInfo, + .waitSemaphoreCount = 1, + .pWaitSemaphores = &vkSemaphore, + .swapchainCount = 1, + .pSwapchains = &vkSwapchain, + .pImageIndices = &imageIndex, + .pResults = nullptr, + }; + + auto const result = _presentQueue.rawPresent(info); + generation.acquired[imageIndex] = false; + return result; +} + +void Swapchain::unacquire(span imageIndices) { + debug_check(*this); + for(auto const index : imageIndices) + CTH_CRITICAL(index >= imageCount() || !acquired(index), "image {} must be acquired", index) {} + + if(imageIndices.empty()) return; + + VkReleaseSwapchainImagesInfoKHR const info{ + .sType = VK_STRUCTURE_TYPE_RELEASE_SWAPCHAIN_IMAGES_INFO_KHR, + .pNext = nullptr, + .swapchain = get(), + .imageIndexCount = static_cast(imageIndices.size()), + .pImageIndices = imageIndices.data(), + }; + auto const result = core().functions()->vkReleaseSwapchainImagesKHR(core().vkDevice(), &info); + + CVK_RESULT_STABLE_THROW(result != VK_SUCCESS, result, "failed to unacquire swapchain images") {} + + for(auto const index : imageIndices) + current().acquired[index] = false; +} + +void Swapchain::destroy(DeviceTable table, VkSwapchainKHR vkSwapchain) { + CTH_WARN(vkSwapchain == VK_NULL_HANDLE, "vkSwapchain invalid (VK_NULL_HANDLE)") {} + + table->vkDestroySwapchainKHR(table.device(), vkSwapchain, nullptr); +} + +Swapchain::Generation::~Generation() { + for(auto const& fence : presentFences) + fence.wait(); + + // the swapchain destroys its images + for(auto& image : images) + image.release(); +} + +std::pair Swapchain::pick(Config const& config) const { + auto const& physicalDevice = core().physicalDevice(); + + auto const formats = _surface->formats(physicalDevice); + auto const& allowedFormats = config.formats.empty() ? formats : config.formats; + auto const formatIt = std::ranges::find_first_of(allowedFormats, formats); + CVK_STABLE_THROW( + formatIt == allowedFormats.end(), + "the surface offers no allowed format, allowed: {}, offered: {}", + config.formats, + formats + ) {} + + if(config.presentModes.empty()) return {*formatIt, VK_PRESENT_MODE_FIFO_KHR}; + + auto const presentModes = _surface->presentModes(physicalDevice); + auto const presentModeIt = std::ranges::find_first_of(config.presentModes, presentModes); + CVK_STABLE_THROW( + presentModeIt == config.presentModes.end(), + "the surface offers no allowed present mode, allowed: {}, offered: {}", + config.presentModes, + presentModes + ) {} + + return {*formatIt, *presentModeIt}; +} + +void Swapchain::createSwapchain(VkExtent2D extent, VkSwapchainKHR oldSwapchain) { + Surface::debug_check(*_surface); + + auto const capabilities = _surface->capabilities(core().physicalDevice()); + auto const imageExtent = evalExtent(extent, capabilities); + + auto const info = _config.createInfo( + _surface->get(), + _surfaceFormat, + _presentMode, + //TODO override of the pre transform, for pre-rotation on rotated mobile displays + capabilities.currentTransform, + imageExtent, + evalImageCount(_config.minImageCount, capabilities), + oldSwapchain + ); + + VkSwapchainKHR ptr = VK_NULL_HANDLE; + auto const result = core().functions()->vkCreateSwapchainKHR(core().vkDevice(), &info, nullptr, &ptr); + + CTH_STABLE_ERR(result != VK_SUCCESS, "failed to create swapchain") { + // the old swapchain retires also when the creation fails, no generation stays current + _generations.clear(); + forget(); + throw cvk::vk_result_exception{result, details->exception()}; + } + + _generations.emplace_back(core_handle{ptr, core()}); + _extent = imageExtent; + + createImages(); +} + +void Swapchain::createImages() { + auto& generation = current(); + + auto const vkImages = dev::enumerate( + "swapchain images", + core().functions()->vkGetSwapchainImagesKHR, + core().vkDevice(), + generation.handle.get() + ); + auto const count = static_cast(vkImages.size()); + + Image::Config const imageConfig{ + .format = _surfaceFormat.format, + .usage = _config.usage, + .queueFamilies = _config.queueFamilies, + }; + + generation.images.reserve(count); + for(auto const vkImage : vkImages) + generation.images.emplace_back(core(), imageConfig, Image::State{.vkImage = vkImage, .extent = _extent}); + + generation.views.reserve(count); + for(auto const& image : generation.images) + generation.views.emplace_back(image, ImageView::Config{}, cvk::create); + + generation.presentSemaphores.reserve(count); + generation.presentFences.reserve(count); + for(uint32_t index = 0; index < count; ++index) { + generation.presentSemaphores.emplace_back(core(), cvk::create); + generation.presentFences.emplace_back(core(), cvk::create, VK_FENCE_CREATE_SIGNALED_BIT); + } + + generation.acquired.assign(count, false); +} + +void Swapchain::unacquireAll() { + vector acquiredImages{}; + for(uint32_t index = 0; index < imageCount(); ++index) + if(acquired(index)) acquiredImages.push_back(index); + unacquire(acquiredImages); +} + +void Swapchain::collectRetired() { + if(_generations.size() < 2) return; + + // the generations have no move assignment, the kept ones move into a new vector + vector kept{}; + kept.reserve(_generations.size()); + for(auto& generation : _generations | std::views::take(_generations.size() - 1)) + if(!std::ranges::all_of(generation.presentFences, &Fence::signaled)) kept.push_back(std::move(generation)); + kept.push_back(std::move(current())); + + _generations = std::move(kept); +} + +void Swapchain::forget() { + _extent = {}; + _surfaceFormat = {}; + _presentMode = VK_PRESENT_MODE_FIFO_KHR; +} +} diff --git a/cvk/src/utility/device/device_features.cpp b/cvk/src/utility/device/device_features.cpp new file mode 100644 index 00000000..579f10eb --- /dev/null +++ b/cvk/src/utility/device/device_features.cpp @@ -0,0 +1,20 @@ +#include "cvk/utility/device/device_features.hpp" + +#include + + +namespace cvk { +std::vector DeviceFeatures::missing(DeviceFeatures const& required) const { + //IMPLEMENT C++26 reflection gives the names, boost-pfr goes + static constexpr auto NAMES = boost::pfr::names_as_array(); + + auto const available = dev::to_bits(*this); + auto const needed = dev::to_bits(required); + + std::vector names{}; + for(size_t i = 0; i < dev::FEATURE_COUNT; ++i) + if(needed[i] && !available[i]) names.push_back(NAMES[i]); + + return names; +} +} diff --git a/cvk/src/utility/device/feature_chain.cpp b/cvk/src/utility/device/feature_chain.cpp new file mode 100644 index 00000000..88416747 --- /dev/null +++ b/cvk/src/utility/device/feature_chain.cpp @@ -0,0 +1,96 @@ +#include "cvk/utility/device/feature_chain.hpp" + +#include +#include +#include + + +namespace cvk::dev { +using std::span; + +namespace { + /** + * @return the VkBool32 flags of the feature struct + */ + template + [[nodiscard]] auto flags(T& features) { + using feature_t = std::remove_const_t; + using bool_t = std::conditional_t, VkBool32 const, VkBool32>; + using byte_t = std::conditional_t, std::byte const, std::byte>; + + constexpr size_t count = feature_struct::COUNT; + + auto* const begin = reinterpret_cast(&features) + feature_struct::OFFSET; + return std::span{reinterpret_cast(begin), count}; + } + + template + [[nodiscard]] feature_structs_t empty_structs(std::type_identity>) { + return feature_structs_t{Ts{.sType = feature_struct::TYPE, .pNext = nullptr}...}; + } +} + +FeatureChain::FeatureChain(span extensions) : + _structs{empty_structs(std::type_identity{})} { + [&](std::index_sequence) { + ((_linked[Is] = [&] { + constexpr auto name = feature_struct>::EXTENSION; + return name.empty() || std::ranges::contains(extensions, name); + }()), ...); + }(std::make_index_sequence{}); + + link(); +} + +FeatureChain::FeatureChain(DeviceFeatures const& features, span extensions) : + FeatureChain{extensions} { + auto const bits = to_bits(features); + + std::apply( + [&](Ts&... structs) { + ([&] { + for(auto const [index, flag] : std::views::enumerate(flags(structs))) + flag = bits[feature_offset + static_cast(index)] ? VK_TRUE : VK_FALSE; + }(), ...); + }, + _structs + ); +} + +DeviceFeatures FeatureChain::features() const { + feature_bits_t bits{}; + + std::apply( + [&](Ts const&... structs) { + ([&] { + for(auto const [index, flag] : std::views::enumerate(flags(structs))) + bits[feature_offset + static_cast(index)] = flag == VK_TRUE; + }(), ...); + }, + _structs + ); + + return to_features(bits); +} + +void FeatureChain::link() { + void* next = nullptr; + + // back to front: each linked struct points to the next linked one + [&](std::index_sequence) { + ([&] { + constexpr size_t index = FEATURE_STRUCTS - 1 - Is; + auto& features = std::get(_structs); + + features.pNext = _linked[index] ? next : nullptr; + if(_linked[index]) next = &features; + }(), ...); + }(std::make_index_sequence{}); +} + +DeviceFeatures query_features(vk_not_null vkDevice, span extensions) { + FeatureChain chain{extensions}; + vkGetPhysicalDeviceFeatures2(vkDevice.get(), chain.get()); + return chain.features(); +} +} diff --git a/cvk/src/utility/vma.cpp b/cvk/src/utility/vma.cpp new file mode 100644 index 00000000..1c54d24b --- /dev/null +++ b/cvk/src/utility/vma.cpp @@ -0,0 +1,5 @@ +// the implementation of VMA, the headers only declare it +#define VMA_IMPLEMENTATION + +#include +#include diff --git a/cvk/tests/CMakeLists.txt b/cvk/tests/CMakeLists.txt new file mode 100644 index 00000000..e8e66ebf --- /dev/null +++ b/cvk/tests/CMakeLists.txt @@ -0,0 +1,25 @@ +cth_glob_cpp(CVK_TESTS_SRC "src") + +add_executable(cvk_tests ${CVK_TESTS_SRC}) + +cth_target_enable_build_cache(cvk_tests OPTIONAL) + +target_include_directories( + cvk_tests + PRIVATE src +) + +target_precompile_headers( + cvk_tests + PRIVATE + src/pch/pch.hpp +) + +target_compile_definitions(cvk_tests PRIVATE CVK_TEST_SHADER_DIR="${CMAKE_CURRENT_SOURCE_DIR}/shaders") + +find_package(GTest CONFIG REQUIRED) + +target_link_libraries(cvk_tests PRIVATE GTest::gtest_main cvk) + +# one process: the tests share their instance and device +add_test(NAME cvk_tests COMMAND cvk_tests) diff --git a/cvk/tests/shaders/bindless.frag b/cvk/tests/shaders/bindless.frag new file mode 100644 index 00000000..d94f5f06 --- /dev/null +++ b/cvk/tests/shaders/bindless.frag @@ -0,0 +1,12 @@ +#version 450 +#extension GL_EXT_nonuniform_qualifier : require + +layout(set = 0, binding = 0) uniform sampler2D textures[]; + +layout(push_constant) uniform Push { + uint index; +} push; + +layout(location = 0) out vec4 outColor; + +void main() { outColor = texture(textures[nonuniformEXT(push.index)], vec2(0.5)); } diff --git a/cvk/tests/shaders/fill.vert b/cvk/tests/shaders/fill.vert new file mode 100644 index 00000000..3df30248 --- /dev/null +++ b/cvk/tests/shaders/fill.vert @@ -0,0 +1,5 @@ +#version 450 + +layout(location = 0) in vec3 position; + +void main() { gl_Position = vec4(position, 1.0); } diff --git a/cvk/tests/shaders/local_read.frag b/cvk/tests/shaders/local_read.frag new file mode 100644 index 00000000..a64508b1 --- /dev/null +++ b/cvk/tests/shaders/local_read.frag @@ -0,0 +1,7 @@ +#version 450 + +layout(input_attachment_index = 0, set = 0, binding = 0) uniform subpassInput color; + +layout(location = 0) out vec4 outColor; + +void main() { outColor = subpassLoad(color); } diff --git a/cvk/tests/shaders/texture.frag b/cvk/tests/shaders/texture.frag new file mode 100644 index 00000000..b7888abc --- /dev/null +++ b/cvk/tests/shaders/texture.frag @@ -0,0 +1,7 @@ +#version 450 + +layout(set = 0, binding = 0) uniform sampler2D tex; + +layout(location = 0) out vec4 outColor; + +void main() { outColor = texture(tex, vec2(0.5)); } diff --git a/cvk/tests/shaders/triangle.frag b/cvk/tests/shaders/triangle.frag new file mode 100644 index 00000000..459e1b3e --- /dev/null +++ b/cvk/tests/shaders/triangle.frag @@ -0,0 +1,9 @@ +#version 450 + +layout(push_constant) uniform Push { + vec4 color; +} push; + +layout(location = 0) out vec4 outColor; + +void main() { outColor = push.color; } diff --git a/cvk/tests/shaders/triangle.vert b/cvk/tests/shaders/triangle.vert new file mode 100644 index 00000000..c9ae4568 --- /dev/null +++ b/cvk/tests/shaders/triangle.vert @@ -0,0 +1,9 @@ +#version 450 + +layout(location = 0) in vec3 position; + +layout(set = 0, binding = 0) uniform Camera { + mat4 viewProjection; +} camera; + +void main() { gl_Position = camera.viewProjection * vec4(position, 1.0); } diff --git a/cvk/tests/shaders/uniform_color.frag b/cvk/tests/shaders/uniform_color.frag new file mode 100644 index 00000000..05f4820a --- /dev/null +++ b/cvk/tests/shaders/uniform_color.frag @@ -0,0 +1,9 @@ +#version 450 + +layout(set = 0, binding = 0) uniform Color { + vec4 color; +} ubo; + +layout(location = 0) out vec4 outColor; + +void main() { outColor = ubo.color; } diff --git a/cvk/tests/shaders/values.comp b/cvk/tests/shaders/values.comp new file mode 100644 index 00000000..9772e6df --- /dev/null +++ b/cvk/tests/shaders/values.comp @@ -0,0 +1,9 @@ +#version 450 + +layout(local_size_x = 64) in; + +layout(set = 0, binding = 0) buffer Values { + uint values[]; +}; + +void main() { values[gl_GlobalInvocationID.x] = gl_GlobalInvocationID.x * 3u; } diff --git a/cvk/tests/src/base/allocator_test.cpp b/cvk/tests/src/base/allocator_test.cpp new file mode 100644 index 00000000..49f18000 --- /dev/null +++ b/cvk/tests/src/base/allocator_test.cpp @@ -0,0 +1,78 @@ +#include "context.hpp" +#include "test.hpp" + +#include "cvk/base/allocator.hpp" +#include "cvk/base/device.hpp" +#include "cvk/base/physical_device.hpp" + + +namespace cvk { +CVK_TEST(Allocator, core_creates_the_allocator) { + test::Context const context{}; + + auto const& allocator = context.core.allocator(); + + EXPECT_TRUE(allocator.created()); + EXPECT_EQ(&allocator.device(), &context.core.device()); +} + +CVK_TEST(Allocator, base_constructor_does_not_create) { + test::Context const context{}; + + Allocator const allocator{context.core.device()}; + + EXPECT_FALSE(allocator.created()); +} + +CVK_TEST(Allocator, create_and_destroy) { + test::Context const context{}; + Allocator allocator{context.core.device(), create}; + + ASSERT_TRUE(allocator.created()); + + allocator.destroy(); + EXPECT_FALSE(allocator.created()); +} + +CVK_TEST(Allocator, release_and_wrap) { + test::Context const context{}; + Allocator allocator{context.core.device(), create}; + auto const vmaAllocator = allocator.get(); + + auto const state = allocator.release(); + EXPECT_FALSE(allocator.created()); + + Allocator const wrapped{context.core.device(), state}; + EXPECT_EQ(wrapped.get(), vmaAllocator); +} + +CVK_TEST(Allocator, move_assignment_destroys_the_old_allocator) { + test::Context const context{}; + Allocator allocator{context.core.device(), create}; + Allocator other{context.core.device(), create}; + auto const vmaAllocator = other.get(); + + allocator = std::move(other); + + EXPECT_EQ(allocator.get(), vmaAllocator); + EXPECT_FALSE(other.created()); +} + +CVK_TEST(Allocator, flags_follow_the_enabled_features) { + auto config = test::Context::CONFIG; + config.features.bufferDeviceAddress = true; + test::Context const context{config}; + + auto const flags = Allocator::flags(context.core.device()); + + EXPECT_NE(flags & VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT, 0u); +} + +CVK_TEST(Allocator, no_flags_without_the_features) { + test::Context const context{}; + + auto const flags = Allocator::flags(context.core.device()); + + EXPECT_EQ(flags & VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT, 0u); +} +} diff --git a/cvk/tests/src/base/core_test.cpp b/cvk/tests/src/base/core_test.cpp new file mode 100644 index 00000000..a7df5c26 --- /dev/null +++ b/cvk/tests/src/base/core_test.cpp @@ -0,0 +1,249 @@ +#include "context.hpp" +#include "test.hpp" + +#include "cvk/base/allocator.hpp" +#include "cvk/base/core.hpp" +#include "cvk/base/device.hpp" +#include "cvk/base/instance.hpp" +#include "cvk/base/physical_device.hpp" +#include "cvk/surface/surface.hpp" +#include "cvk/utility/constant/device_constants.hpp" +#include "cvk/utility/constants.hpp" +#include "cvk/utility/vk_exceptions.hpp" + +#include +#include +#include + + +namespace cvk { +namespace { + using test::Context; + + Core::Config const TWO_QUEUES{.queueProperties = {QueueFamilyProperties::GRAPHICS, QueueFamilyProperties::COMPUTE}}; + + /** + * @brief every queue in one group + */ + Core::queue_set_t one_group(size_t queues) { + Core::queue_set_t::group_t group(queues); + std::iota(group.begin(), group.end(), 0uz); + return Core::queue_set_t{queues, {group}}; + } +} + +CVK_TEST(Core, base_constructor_does_not_create) { + Context const context{}; + + Core const core{context.instance}; + + EXPECT_FALSE(core.created()); + EXPECT_EQ(&core.instance(), &context.instance); +} + +CVK_TEST(Core, create_without_queue_sets) { + Context const context{}; + + Core const core{context.instance, {}, Context::CONFIG}; + + ASSERT_TRUE(core.created()); + EXPECT_EQ(core.vkInstance(), context.instance.get()); + EXPECT_EQ(core.vkPhysicalDevice(), core.physicalDevice().get()); + EXPECT_EQ(core.vkDevice(), core.device().get()); + EXPECT_EQ(core.functions(), core.device().functions()); + EXPECT_EQ(core.deviceTable().device(), core.vkDevice()); + EXPECT_EQ(&core.device().physicalDevice(), &core.physicalDevice()); + + ASSERT_EQ(core.queueIndices().size(), Context::CONFIG.queueProperties.size()); + EXPECT_EQ(core.device().queues().size(), Context::CONFIG.queueProperties.size()); + + for(size_t i = 0; i < Context::CONFIG.queueProperties.size(); i++) { + auto const& queue = core.queue(i); + ASSERT_NE(queue.get(), VK_NULL_HANDLE); + EXPECT_TRUE(contains(queue.familyProperties(), Context::CONFIG.queueProperties[i])); + queue.waitIdle(); + } +} + +CVK_TEST(Core, create_one_queue_per_entry) { + Context const context{}; + + if(!pick_physical_device(context.instance, {}, TWO_QUEUES.queueProperties, constants::BASE_DEVICE_FEATURES)) + GTEST_SKIP() << "needs a device with two queues"; + + Core const core{context.instance, {}, TWO_QUEUES}; + + ASSERT_EQ(core.queueIndices().size(), 2); + EXPECT_EQ(core.device().queues().size(), 2); + EXPECT_NE(core.queueIndex(0), core.queueIndex(1)); + EXPECT_NE(core.queue(0).get(), core.queue(1).get()); +} + +CVK_TEST(Core, queue_set_group_shares_a_queue) { + Context const context{}; + + auto config = TWO_QUEUES; + config.queueSets.push_back(one_group(config.queueProperties.size())); + + Core const core{context.instance, {}, config}; + + ASSERT_TRUE(core.created()); + EXPECT_EQ(core.queueSetIndex(), 0); + EXPECT_EQ(core.device().queues().size(), 1); + EXPECT_EQ(core.queueIndex(0), core.queueIndex(1)); + EXPECT_EQ(&core.queue(0), &core.queue(1)); + + EXPECT_TRUE(contains(core.queue(0).familyProperties(), QueueFamilyProperties::GRAPHICS)); + EXPECT_TRUE(contains(core.queue(0).familyProperties(), QueueFamilyProperties::COMPUTE)); +} + +CVK_TEST(Core, first_possible_queue_set_wins) { + Context const context{}; + + // no device has this many compute queues + constexpr size_t queues = 256; + + Core::Config const config{ + .queueProperties = std::vector(queues, QueueFamilyProperties::COMPUTE), + .queueSets = {Core::queue_set_t{queues}, one_group(queues)}, + }; + + Core const core{context.instance, {}, config}; + + ASSERT_TRUE(core.created()); + EXPECT_EQ(core.queueSetIndex(), 1); + EXPECT_EQ(core.device().queues().size(), 1); + EXPECT_EQ(core.queueIndices().size(), queues); +} + +CVK_TEST(Core, no_suitable_device_throws) { + Context const context{}; + Core::Config const config{.queueProperties = {QueueFamilyProperties::PRESENT}}; + + auto const construct = [&] { Core const core{context.instance, {}, config}; }; + + EXPECT_THROW(construct(), cvk_exception); +} + +CVK_TEST(Core, device_uuid_picks_exactly_this_device) { + Context const context{}; + auto config = Context::CONFIG; + + // every suitable device, not only the one that the automatic pick takes + for(auto const& device : enumerate_physical_devices(context.instance)) { + bool const suitable = device.missing(constants::BASE_DEVICE_FEATURES).empty() + && !device.queueFamilyIndices(config.queueProperties).empty(); + if(!suitable) continue; + + config.device = device.uuid(); + Core const core{context.instance, {}, config}; + + EXPECT_EQ(core.physicalDevice().uuid(), device.uuid()); + EXPECT_EQ(core.vkPhysicalDevice(), device.get()); + } +} + +CVK_TEST(Core, missing_device_uuid_throws) { + Context const context{}; + device_uuid_t unknown{}; + unknown.fill(0xFF); + + Core::Config const withoutSets{.queueProperties = Context::CONFIG.queueProperties, .device = unknown}; + Core::Config const withSets{ + .queueProperties = Context::CONFIG.queueProperties, + .queueSets = {Core::queue_set_t{1}}, + .device = unknown, + }; + + auto const construct = [&](Core::Config const& config) { Core const core{context.instance, {}, config}; }; + + EXPECT_THROW(construct(withoutSets), cvk_exception); + EXPECT_THROW(construct(withSets), cvk_exception); +} + +CVK_TEST(Core, release_and_wrap) { + Context const context{}; + + Core core{context.instance, {}, Context::CONFIG}; + auto const vkDevice = core.vkDevice(); + auto const vmaAllocator = core.allocator().get(); + + auto state = core.release(); + EXPECT_FALSE(core.created()); + EXPECT_TRUE(core.queueIndices().empty()); + + Core const wrapped{context.instance, std::move(state)}; + + ASSERT_TRUE(wrapped.created()); + EXPECT_EQ(wrapped.vkDevice(), vkDevice); + EXPECT_EQ(wrapped.allocator().get(), vmaAllocator); + ASSERT_EQ(wrapped.queueIndices().size(), Context::CONFIG.queueProperties.size()); + wrapped.queue(0).waitIdle(); +} + +CVK_TEST(Core, wrap_without_queue_indices_takes_the_device_queues) { + Context const context{}; + + Core core{context.instance, {}, Context::CONFIG}; + auto state = core.release(); + state.queueIndices.clear(); + + Core const wrapped{context.instance, std::move(state)}; + + ASSERT_EQ(wrapped.queueIndices().size(), wrapped.device().queues().size()); + for(size_t i = 0; i < wrapped.queueIndices().size(); i++) + EXPECT_EQ(&wrapped.queue(i), &wrapped.device().queues()[i]); +} + +CVK_TEST(Core, wrap_without_allocator_creates_one) { + Context const context{}; + + Core core{context.instance, {}, Context::CONFIG}; + auto state = core.release(); + state.allocator = nullptr; + + Core const wrapped{context.instance, std::move(state)}; + + EXPECT_TRUE(wrapped.allocator().created()); + EXPECT_EQ(&wrapped.allocator().device(), &wrapped.device()); +} + +CVK_TEST(Core, queues_work_after_move) { + Context const context{}; + + Core core{context.instance, {}, Context::CONFIG}; + Core const moved{std::move(core)}; + + EXPECT_FALSE(core.created()); + ASSERT_TRUE(moved.created()); + for(size_t i = 0; i < moved.queueIndices().size(); i++) + moved.queue(i).waitIdle(); +} + +CVK_TEST(Core, present_queue_with_headless_surface) { + test::PresentContext const context{}; + std::span const surfaces{&context.surface(), 1}; + + Core const core{context.instance, surfaces, test::PresentContext::CONFIG}; + + ASSERT_TRUE(core.created()); + EXPECT_TRUE(core.queue(0).canPresent()); + EXPECT_TRUE(context.surface().supportsFamily(core.physicalDevice(), core.queue(0).familyIndex())); +} + +CVK_TEST(Core, move_assignment_destroys_the_old_core) { + if constexpr(!constants::ENABLE_VALIDATION_LAYERS) GTEST_SKIP() << "needs the validation layers"; + + test::InstanceContext const context{}; + + Core core{context.instance, {}, Context::CONFIG}; + Core other{context.instance, {}, Context::CONFIG}; + auto const vkDevice = other.vkDevice(); + + core = std::move(other); + + ASSERT_EQ(core.vkDevice(), vkDevice); + EXPECT_FALSE(other.created()); + core.queue(0).waitIdle(); +} +} diff --git a/cvk/tests/src/base/cvk_test.cpp b/cvk/tests/src/base/cvk_test.cpp new file mode 100644 index 00000000..39c64cbc --- /dev/null +++ b/cvk/tests/src/base/cvk_test.cpp @@ -0,0 +1,33 @@ +#include "test.hpp" + +#include "cvk/base/cvk.hpp" + +#include + +#include + + +namespace cvk { +CVK_TEST(Cvk, is_not_copyable_or_movable) { + static_assert(!std::is_copy_constructible_v && !std::is_copy_assignable_v); + static_assert(!std::is_move_constructible_v && !std::is_move_assignable_v); +} + +CVK_TEST(Cvk, nested_objects_keep_the_loader) { + // the test environment holds a Cvk, the count never reaches 0 here + ASSERT_TRUE(Cvk::initialized()); + { + Cvk const outer{}; + { + Cvk const inner{}; + EXPECT_TRUE(Cvk::initialized()); + } + EXPECT_TRUE(Cvk::initialized()); + } + EXPECT_TRUE(Cvk::initialized()); + + uint32_t version = 0; + ASSERT_NE(vkEnumerateInstanceVersion, nullptr); + EXPECT_EQ(vkEnumerateInstanceVersion(&version), VK_SUCCESS); +} +} diff --git a/cvk/tests/src/base/device_test.cpp b/cvk/tests/src/base/device_test.cpp new file mode 100644 index 00000000..483202f2 --- /dev/null +++ b/cvk/tests/src/base/device_test.cpp @@ -0,0 +1,219 @@ +#include "context.hpp" +#include "headless.hpp" +#include "test.hpp" + +#include "cvk/base/device.hpp" +#include "cvk/base/instance.hpp" +#include "cvk/base/physical_device.hpp" +#include "cvk/utility/constant/device_constants.hpp" +#include "cvk/utility/constants.hpp" + +#include +#include +#include +#include +#include + + +namespace cvk { +namespace { + using test::Context; + using test::QUEUES; + + constexpr std::array THREE_QUEUES{ + QueueFamilyProperties::GRAPHICS, + QueueFamilyProperties::COMPUTE, + QueueFamilyProperties::TRANSFER + }; +} + +CVK_TEST(Device, base_constructor_does_not_create) { + Context const context{}; + auto const [physicalDevice, _] = test::pick(context.instance, QUEUES); + + Device const device{context.instance, physicalDevice}; + + EXPECT_FALSE(device.created()); + EXPECT_TRUE(device.queues().empty()); +} + +CVK_TEST(Device, create_retrieves_the_queue) { + Context const context{}; + auto const [physicalDevice, familyIndices] = test::pick(context.instance, QUEUES); + + Device const device{context.instance, physicalDevice, Device::Config{familyIndices}}; + + ASSERT_TRUE(device.created()); + EXPECT_EQ(&device.physicalDevice(), &physicalDevice); + EXPECT_EQ(&device.instance(), &context.instance); + ASSERT_EQ(device.queues().size(), 1); + + auto const& queue = device.queues()[0]; + ASSERT_NE(queue.get(), VK_NULL_HANDLE); + EXPECT_EQ(queue.familyIndex(), familyIndices[0]); + EXPECT_EQ(queue.index(), 0); + EXPECT_TRUE(contains(queue.familyProperties(), QUEUES[0])); + queue.waitIdle(); + + device.waitIdle(); +} + +CVK_TEST(Device, create_retrieves_the_queues) { + Context const context{}; + + auto const result = pick_physical_device(context.instance, {}, THREE_QUEUES, {}); + if(!result) GTEST_SKIP() << "needs a device with three queues"; + auto const& [physicalDevice, familyIndices] = *result; + + Device const device{context.instance, physicalDevice, Device::Config{familyIndices}}; + + ASSERT_TRUE(device.created()); + ASSERT_EQ(device.queues().size(), THREE_QUEUES.size()); + + std::map queueIndices{}; + for(auto const& [queue, familyIndex, properties] : std::views::zip(device.queues(), familyIndices, THREE_QUEUES)) { + ASSERT_NE(queue.get(), VK_NULL_HANDLE); + EXPECT_EQ(queue.familyIndex(), familyIndex); + EXPECT_EQ(queue.index(), queueIndices[familyIndex]++); + EXPECT_TRUE(contains(queue.familyProperties(), properties)); + queue.waitIdle(); + } + + device.waitIdle(); +} + +CVK_TEST(Device, queues_of_one_family_get_increasing_indices) { + Context const context{}; + auto const [physicalDevice, _] = test::pick(context.instance, QUEUES); + + auto const families = physicalDevice.queueFamilies(); + auto const family = std::ranges::find_if(families, [](auto const& f) { return f.vkProperties.queueCount >= 2; }); + if(family == families.end()) GTEST_SKIP() << "needs a queue family with two queues"; + + Device const device{context.instance, physicalDevice, Device::Config{{family->index, family->index}}}; + + ASSERT_EQ(device.queues().size(), 2); + EXPECT_EQ(device.queues()[0].index(), 0); + EXPECT_EQ(device.queues()[1].index(), 1); + EXPECT_NE(device.queues()[0].get(), device.queues()[1].get()); +} + +CVK_TEST(Device, queues_work_after_move) { + Context const context{}; + auto const [physicalDevice, familyIndices] = test::pick(context.instance, QUEUES); + + Device device{context.instance, physicalDevice, Device::Config{familyIndices}}; + auto const vkDevice = device.get(); + + Device const moved{std::move(device)}; + + EXPECT_FALSE(device.created()); + ASSERT_TRUE(moved.created()); + EXPECT_EQ(moved.get(), vkDevice); + + for(auto const& queue : moved.queues()) + queue.waitIdle(); +} + +CVK_TEST(Device, release_and_wrap) { + Context const context{}; + auto const [physicalDevice, familyIndices] = test::pick(context.instance, QUEUES); + + Device device{context.instance, physicalDevice, Device::Config{familyIndices}}; + auto const vkDevice = device.get(); + + auto state = device.release(); + + EXPECT_FALSE(device.created()); + EXPECT_TRUE(device.queues().empty()); + EXPECT_EQ(state.vkDevice.get(), vkDevice); + ASSERT_NE(state.functionTable, nullptr); + EXPECT_EQ(state.queueFamilyIndices, familyIndices); + + Device const wrapped{context.instance, physicalDevice, std::move(state)}; + + ASSERT_TRUE(wrapped.created()); + EXPECT_EQ(wrapped.get(), vkDevice); + ASSERT_EQ(wrapped.queues().size(), familyIndices.size()); + for(auto const& queue : wrapped.queues()) + queue.waitIdle(); +} + +CVK_TEST(Device, wrap_without_table_loads_it) { + Context const context{}; + auto const [physicalDevice, familyIndices] = test::pick(context.instance, QUEUES); + + Device device{context.instance, physicalDevice, Device::Config{familyIndices}}; + auto state = device.release(); + state.functionTable = nullptr; + + Device const wrapped{context.instance, physicalDevice, std::move(state)}; + + ASSERT_TRUE(wrapped.created()); + EXPECT_NE(wrapped.functions()->vkDestroyDevice, nullptr); + wrapped.waitIdle(); +} + +CVK_TEST(Device, release_twice_is_safe) { + Context const context{}; + auto const [physicalDevice, familyIndices] = test::pick(context.instance, QUEUES); + + Device device{context.instance, physicalDevice, Device::Config{familyIndices}}; + Device first{context.instance, physicalDevice, device.release()}; + + device.create(Device::Config{familyIndices}); + ASSERT_TRUE(device.created()); + EXPECT_NE(device.functions()->vkDestroyDevice, nullptr); + + Device const second{context.instance, physicalDevice, device.release()}; + EXPECT_TRUE(second.created()); +} + +CVK_TEST(Device, present_enables_swapchain_maintenance1) { + test::PresentContext const context{}; + std::span const surfaces{&context.surface(), 1}; + std::array const queues{QueueFamilyProperties::GRAPHICS | QueueFamilyProperties::PRESENT}; + + auto const [physicalDevice, familyIndices] = test::pick(context.instance, queues, surfaces); + + Device const device{ + context.instance, + physicalDevice, + Device::Config{ + .queueFamilyIndices = familyIndices, + .features = constants::PRESENT_DEVICE_FEATURES, + .extensions = {VK_KHR_SWAPCHAIN_EXTENSION_NAME}, + } + }; + + ASSERT_TRUE(device.created()); + ASSERT_EQ(device.queues().size(), 1); + EXPECT_TRUE(device.queues()[0].canPresent()); + EXPECT_TRUE(device.features().swapchainMaintenance1); + + // the feature bit adds its extension + std::string const maintenance1{VK_KHR_SWAPCHAIN_MAINTENANCE_1_EXTENSION_NAME}; + EXPECT_TRUE(std::ranges::contains(device.extensions(), maintenance1)); + EXPECT_NE(device.functions()->vkCreateSwapchainKHR, nullptr); + EXPECT_NE(device.functions()->vkReleaseSwapchainImagesKHR, nullptr); +} + +CVK_TEST(Device, move_assignment_destroys_the_old_device) { + if constexpr(!constants::ENABLE_VALIDATION_LAYERS) GTEST_SKIP() << "needs the validation layers"; + + test::InstanceContext const context{}; + auto const [physicalDevice, familyIndices] = test::pick(context.instance, QUEUES); + + Device device{context.instance, physicalDevice, Device::Config{familyIndices}}; + Device other{context.instance, physicalDevice, Device::Config{familyIndices}}; + auto const vkDevice = other.get(); + + device = std::move(other); + + ASSERT_EQ(device.get(), vkDevice); + EXPECT_FALSE(other.created()); + ASSERT_EQ(device.queues().size(), 1); + device.queues()[0].waitIdle(); + device.waitIdle(); +} +} diff --git a/cvk/tests/src/base/instance_test.cpp b/cvk/tests/src/base/instance_test.cpp new file mode 100644 index 00000000..e783eac0 --- /dev/null +++ b/cvk/tests/src/base/instance_test.cpp @@ -0,0 +1,146 @@ +#include "context.hpp" +#include "test.hpp" +#include "validation.hpp" + +#include "cvk/base/instance.hpp" +#include "cvk/utility/constants.hpp" +#include "cvk/utility/vk_exceptions.hpp" + +#include +#include +#include + + +namespace cvk { +using test::APP_NAME; + +CVK_TEST(Instance, base_constructor_does_not_create) { + Instance const instance{test::instance_config()}; + + EXPECT_FALSE(instance.created()); +} + +CVK_TEST(Instance, create_and_destroy) { + test::Context::ValidationCheck const validationCheck{}; + Instance instance{test::instance_config()}; + + instance.create(test::counting_messenger()); + ASSERT_TRUE(instance.created()); + EXPECT_EQ(volkGetLoadedInstance(), instance.get()); + + instance.destroy(); + EXPECT_FALSE(instance.created()); +} + +CVK_TEST(Instance, constructor_creates) { + test::Context::ValidationCheck const validationCheck{}; + + Instance const instance{test::instance_config(), create, test::counting_messenger()}; + + EXPECT_TRUE(instance.created()); +} + +CVK_TEST(Instance, create_twice_loads_the_new_instance) { + test::InstanceContext context{}; + + context.instance.create(test::counting_messenger()); + + ASSERT_TRUE(context.instance.created()); + EXPECT_EQ(volkGetLoadedInstance(), context.instance.get()); +} + +CVK_TEST(Instance, create_enables_the_extensions_of_the_config) { + std::string const surface{VK_KHR_SURFACE_EXTENSION_NAME}; + + test::InstanceContext const context{{surface}}; + + auto const& instance = context.instance; + EXPECT_TRUE(std::ranges::contains(instance.extensions(), surface)); + if constexpr(constants::ENABLE_VALIDATION_LAYERS) { + std::string const debugUtils{VK_EXT_DEBUG_UTILS_EXTENSION_NAME}; + EXPECT_TRUE(std::ranges::contains(instance.extensions(), debugUtils)); + EXPECT_NE(instance.debugMessenger(), nullptr); + } +} + +CVK_TEST(Instance, create_with_a_missing_extension_throws_and_forgets) { + // a created instance with the config of the missing extension: create() destroys it first + test::InstanceContext context{}; + Instance instance{test::instance_config({"VK_CVK_extension_that_does_not_exist"}), context.instance.release()}; + ASSERT_TRUE(instance.created()); + + EXPECT_THROW(instance.create(), cvk_exception); + EXPECT_FALSE(instance.created()); + EXPECT_TRUE(instance.extensions().empty()); + EXPECT_EQ(instance.debugMessenger(), nullptr); +} + +CVK_TEST(Instance, available_extensions_contain_the_surface) { + std::string const surface{VK_KHR_SURFACE_EXTENSION_NAME}; + EXPECT_TRUE(std::ranges::contains(available_instance_extensions(), surface)); +} + +CVK_TEST(Instance, wrap_takes_ownership) { + VkApplicationInfo const appInfo{ + .sType = VK_STRUCTURE_TYPE_APPLICATION_INFO, + .pNext = nullptr, + .pApplicationName = APP_NAME.data(), + .applicationVersion = 0, + .pEngineName = APP_NAME.data(), + .engineVersion = 0, + .apiVersion = VK_API_VERSION_1_3, + }; + VkInstanceCreateInfo const createInfo{ + .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .pApplicationInfo = &appInfo, + .enabledLayerCount = 0, + .ppEnabledLayerNames = nullptr, + .enabledExtensionCount = 0, + .ppEnabledExtensionNames = nullptr, + }; + VkInstance vkInstance = VK_NULL_HANDLE; + ASSERT_EQ(vkCreateInstance(&createInfo, nullptr, &vkInstance), VK_SUCCESS); + + Instance instance{test::instance_config(), Instance::State{vkInstance}}; + + ASSERT_TRUE(instance.created()); + EXPECT_EQ(instance.get(), vkInstance); + EXPECT_EQ(volkGetLoadedInstance(), vkInstance); + + instance.destroy(); + EXPECT_FALSE(instance.created()); +} + +CVK_TEST(Instance, move_assignment_takes_the_other_instance) { + test::InstanceContext context{}; + Instance other{test::instance_config(), create, test::counting_messenger()}; + auto const vkInstance = other.get(); + + context.instance = std::move(other); + + EXPECT_EQ(context.instance.get(), vkInstance); + EXPECT_FALSE(other.created()); +} + +CVK_TEST(Instance, release_and_wrap) { + test::InstanceContext context{}; + auto& instance = context.instance; + auto const vkInstance = instance.get(); + + auto const extensions = std::vector{std::from_range, instance.extensions()}; + + auto state = instance.release(); + EXPECT_FALSE(instance.created()); + EXPECT_EQ(instance.debugMessenger(), nullptr); + EXPECT_EQ(state.vkInstance.get(), vkInstance); + EXPECT_EQ(state.extensions, extensions); + + Instance const wrapped{test::instance_config(), std::move(state)}; + ASSERT_TRUE(wrapped.created()); + EXPECT_EQ(wrapped.get(), vkInstance); + EXPECT_TRUE(std::ranges::equal(wrapped.extensions(), extensions)); + if constexpr(constants::ENABLE_VALIDATION_LAYERS) EXPECT_NE(wrapped.debugMessenger(), nullptr); +} +} diff --git a/cvk/tests/src/base/physical_device_test.cpp b/cvk/tests/src/base/physical_device_test.cpp new file mode 100644 index 00000000..c9480863 --- /dev/null +++ b/cvk/tests/src/base/physical_device_test.cpp @@ -0,0 +1,157 @@ +#include "context.hpp" +#include "headless.hpp" +#include "test.hpp" + +#include "cvk/base/instance.hpp" +#include "cvk/base/physical_device.hpp" +#include "cvk/utility/constant/device_constants.hpp" +#include "cvk/utility/vk_exceptions.hpp" + +#include +#include +#include +#include +#include + + +namespace cvk { +namespace { + using test::Context; + using test::QUEUES; + + constexpr std::array PRESENT_QUEUES{QueueFamilyProperties::GRAPHICS | QueueFamilyProperties::PRESENT}; + + std::vector const UNKNOWN_EXTENSION{"VK_CVK_extension_that_does_not_exist"}; +} + +CVK_TEST(PhysicalDevice, pick_finds_the_queues) { + Context const context{}; + + auto const pick = pick_physical_device(context.instance, {}, QUEUES, constants::BASE_DEVICE_FEATURES); + + ASSERT_TRUE(pick.has_value()); + auto const& [device, familyIndices] = *pick; + ASSERT_EQ(familyIndices.size(), QUEUES.size()); + + for(auto const [familyIndex, properties] : std::views::zip(familyIndices, QUEUES)) { + auto const* family = device.queueFamily(familyIndex); + ASSERT_NE(family, nullptr); + EXPECT_TRUE(contains(family->properties, properties)); + } +} + +CVK_TEST(PhysicalDevice, picked_device_has_the_features) { + Context const context{}; + + auto const [device, _] = test::pick(context.instance, QUEUES); + + EXPECT_TRUE(device.missing(constants::BASE_DEVICE_FEATURES).empty()); + EXPECT_TRUE(device.features().dynamicRenderingLocalRead); + std::string const localRead{VK_KHR_DYNAMIC_RENDERING_LOCAL_READ_EXTENSION_NAME}; + EXPECT_TRUE(std::ranges::contains(device.extensions(), localRead)); +} + +CVK_TEST(PhysicalDevice, missing_names_the_extensions) { + Context const context{}; + auto const [device, _] = test::pick(context.instance, QUEUES); + + EXPECT_EQ(device.missing({}, UNKNOWN_EXTENSION), UNKNOWN_EXTENSION); +} + +CVK_TEST(PhysicalDevice, pick_with_a_missing_extension_finds_nothing) { + Context const context{}; + + EXPECT_FALSE(pick_physical_device(context.instance, {}, QUEUES, {}, UNKNOWN_EXTENSION).has_value()); +} + +CVK_TEST(PhysicalDevice, pick_present_without_surfaces_finds_nothing) { + Context const context{}; + + EXPECT_FALSE(pick_physical_device(context.instance, {}, PRESENT_QUEUES, {}).has_value()); +} + +CVK_TEST(PhysicalDevice, pick_present_finds_a_family_of_the_surface) { + test::PresentContext const context{}; + std::span const surfaces{&context.surface(), 1}; + + auto const [device, familyIndices] = test::pick(context.instance, PRESENT_QUEUES, surfaces); + + EXPECT_TRUE(device.features().swapchainMaintenance1); + ASSERT_EQ(familyIndices.size(), 1); + EXPECT_TRUE(context.surface().supportsFamily(device, familyIndices[0])); +} + +CVK_TEST(PhysicalDevice, enumerate_without_surfaces_has_no_present) { + Context const context{}; + + for(auto const& device : enumerate_physical_devices(context.instance)) + for(auto const& family : device.queueFamilies()) + EXPECT_FALSE(contains(family.properties, QueueFamilyProperties::PRESENT)); +} + +CVK_TEST(PhysicalDevice, copy_keeps_the_queried_members) { + Context const context{}; + auto const [device, _] = test::pick(context.instance, QUEUES); + + PhysicalDevice const copy{device}; + + EXPECT_EQ(copy.get(), device.get()); + EXPECT_EQ(copy.features(), device.features()); + EXPECT_TRUE(std::ranges::equal(copy.extensions(), device.extensions())); + EXPECT_TRUE(std::ranges::equal(copy.queueFamilies(), device.queueFamilies())); + EXPECT_EQ(copy.maxSampleCount(), device.maxSampleCount()); + EXPECT_STREQ(copy.properties().deviceName, device.properties().deviceName); + EXPECT_EQ(copy.uuid(), device.uuid()); +} + +CVK_TEST(PhysicalDevice, uuid_is_the_device_uuid) { + Context const context{}; + auto const& device = context.core.physicalDevice(); + + VkPhysicalDeviceIDProperties idProperties{ + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ID_PROPERTIES, + .pNext = nullptr, + }; + VkPhysicalDeviceProperties2 properties{ + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, + .pNext = &idProperties, + }; + vkGetPhysicalDeviceProperties2(device.get(), &properties); + + EXPECT_TRUE(std::ranges::equal(device.uuid(), idProperties.deviceUUID)); + EXPECT_STREQ(device.properties().deviceName, properties.properties.deviceName); +} + +CVK_TEST(PhysicalDevice, type_rank_prefers_discrete_integrated_virtual_cpu_other) { + constexpr auto rank = dev::device_type_rank; + static_assert(rank(VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) == 0); + static_assert(rank(VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) < rank(VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU)); + static_assert(rank(VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU) < rank(VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU)); + static_assert(rank(VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU) < rank(VK_PHYSICAL_DEVICE_TYPE_CPU)); + static_assert(rank(VK_PHYSICAL_DEVICE_TYPE_CPU) < rank(VK_PHYSICAL_DEVICE_TYPE_OTHER)); +} + +CVK_TEST(PhysicalDevice, find_supported_format) { + Context const context{}; + auto const& device = context.core.physicalDevice(); + + // R8G8B8A8_UNORM sampled images with optimal tiling are mandatory + std::array const candidates{VK_FORMAT_R8G8B8A8_UNORM}; + + auto const format = device.findSupportedFormat( + candidates, + VK_IMAGE_TILING_OPTIMAL, + VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT + ); + EXPECT_EQ(format, VK_FORMAT_R8G8B8A8_UNORM); + + auto const findNone = [&] { + [[maybe_unused]] auto const none = device.findSupportedFormat( + {}, + VK_IMAGE_TILING_OPTIMAL, + VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT + ); + }; + EXPECT_THROW(findNone(), cvk_exception); +} +} diff --git a/cvk/tests/src/base/queue/queue_test.cpp b/cvk/tests/src/base/queue/queue_test.cpp new file mode 100644 index 00000000..b1ade939 --- /dev/null +++ b/cvk/tests/src/base/queue/queue_test.cpp @@ -0,0 +1,88 @@ +#include "context.hpp" +#include "headless.hpp" +#include "test.hpp" + +#include "cvk/base/device.hpp" +#include "cvk/base/instance.hpp" +#include "cvk/base/physical_device.hpp" + +#include + + +namespace cvk { +namespace { + VkSubmitInfo2 const EMPTY_SUBMIT{ + .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2, + .pNext = nullptr, + .flags = 0, + .waitSemaphoreInfoCount = 0, + .pWaitSemaphoreInfos = nullptr, + .commandBufferInfoCount = 0, + .pCommandBufferInfos = nullptr, + .signalSemaphoreInfoCount = 0, + .pSignalSemaphoreInfos = nullptr, + }; +} + +CVK_TEST(QueueFamily, graphics_or_compute_supports_transfer) { + using enum QueueFamilyProperties; + static_assert(to_queue_properties(VK_QUEUE_GRAPHICS_BIT, false) == (GRAPHICS | TRANSFER)); + static_assert(to_queue_properties(VK_QUEUE_COMPUTE_BIT, false) == (COMPUTE | TRANSFER)); + static_assert(to_queue_properties(VK_QUEUE_TRANSFER_BIT, true) == (TRANSFER | PRESENT)); + static_assert(to_queue_properties(VK_QUEUE_SPARSE_BINDING_BIT, false) == NONE); +} + +CVK_TEST(Queue, copies_share_the_queue) { + test::Context const context{}; + auto const& queue = context.core.queue(0); + + Queue const copy{queue}; + + EXPECT_EQ(copy.get(), queue.get()); + EXPECT_EQ(copy.familyIndex(), queue.familyIndex()); + EXPECT_EQ(copy.index(), queue.index()); + copy.waitIdle(); +} + +CVK_TEST(Queue, raw_submit_signals_the_fence) { + test::Context const context{}; + auto const table = context.core.deviceTable(); + + VkFenceCreateInfo const fenceInfo{.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, .pNext = nullptr, .flags = 0}; + VkFence fence = VK_NULL_HANDLE; + ASSERT_EQ(table->vkCreateFence(table.device(), &fenceInfo, nullptr, &fence), VK_SUCCESS); + + context.core.queue(0).rawSubmit(EMPTY_SUBMIT, fence); + + EXPECT_EQ(table->vkWaitForFences(table.device(), 1, &fence, VK_TRUE, UINT64_MAX), VK_SUCCESS); + + table->vkDestroyFence(table.device(), fence, nullptr); +} + +CVK_TEST(Queue, raw_submit_without_fence) { + test::Context const context{}; + auto const& queue = context.core.queue(0); + + queue.rawSubmit(EMPTY_SUBMIT); + queue.waitIdle(); +} + +CVK_TEST(Queue, copies_keep_the_queue_through_a_device_release_and_wrap) { + test::Context const context{}; + auto const [physicalDevice, familyIndices] = test::pick(context.instance, test::QUEUES); + Device device{context.instance, physicalDevice, Device::Config{familyIndices}}; + + Queue const copy{device.queues()[0]}; + Device const wrapped{context.instance, physicalDevice, device.release()}; + + ASSERT_EQ(wrapped.queues().size(), 1); + EXPECT_EQ(wrapped.queues()[0].get(), copy.get()); + copy.waitIdle(); +} + +CVK_TEST(Queue, present_family_can_present) { + test::PresentContext const context{}; + + EXPECT_TRUE(context.queue().canPresent()); +} +} diff --git a/cvk/tests/src/base/queue/submit_test.cpp b/cvk/tests/src/base/queue/submit_test.cpp new file mode 100644 index 00000000..0ab16db7 --- /dev/null +++ b/cvk/tests/src/base/queue/submit_test.cpp @@ -0,0 +1,97 @@ +#include "context.hpp" +#include "test.hpp" + +#include "cvk/base/queue/queue.hpp" +#include "cvk/render/cmd/cmd_buffer.hpp" +#include "cvk/render/cmd/cmd_pool.hpp" +#include "cvk/render/sync/fence.hpp" +#include "cvk/render/sync/semaphore.hpp" +#include "cvk/render/sync/timeline_semaphore.hpp" + +#include +#include + + +namespace cvk { +CVK_TEST(Submit, semaphore_info_takes_the_kind_from_the_semaphore) { + test::Context const context{}; + Semaphore const binary{context.core, create}; + TimelineSemaphore const timeline{context.core, create}; + + SemaphoreSubmitInfo const binaryInfo{binary}; + SemaphoreSubmitInfo const timelineInfo{timeline, VK_PIPELINE_STAGE_2_TRANSFER_BIT, 3}; + + EXPECT_TRUE(std::holds_alternative>(binaryInfo.semaphore)); + EXPECT_EQ(binaryInfo.stages, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT); + EXPECT_FALSE(binaryInfo.value.has_value()); + + EXPECT_TRUE(std::holds_alternative>(timelineInfo.semaphore)); + EXPECT_EQ(timelineInfo.stages, VK_PIPELINE_STAGE_2_TRANSFER_BIT); + EXPECT_EQ(timelineInfo.value, 3); +} + +CVK_TEST(Submit, command_buffer_without_fence) { + test::Context const context{}; + CmdPool const pool{context.core, test::pool_config(context.core), create}; + PrimaryCmdBuffer primary{pool, create}; + primary.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); + primary.end(); + + context.core.queue(0).submit({.cmdBuffers = {&primary}}); + + context.core.queue(0).waitIdle(); +} + +CVK_TEST(Submit, binary_semaphore_orders_two_infos) { + test::Context const context{}; + Semaphore const semaphore{context.core, create}; + Fence const fence{context.core, create}; + + std::array const infos{ + SubmitInfo{.signals = {semaphore}}, + SubmitInfo{.waits = {semaphore}}, + }; + context.core.queue(0).submit(infos, &fence); + + fence.wait(); +} + +CVK_TEST(Submit, timeline_signal_takes_the_next_value) { + test::Context const context{}; + TimelineSemaphore const timeline{context.core, create}; + + context.core.queue(0).submit({.signals = {timeline}}); + + EXPECT_EQ(timeline.value(), 1); + timeline.wait(); + EXPECT_EQ(timeline.gpuValue(), 1); +} + +CVK_TEST(Submit, timeline_values_resolve_in_order) { + test::Context const context{}; + TimelineSemaphore const first{context.core, create}; + TimelineSemaphore const second{context.core, create}; + + // the wait of the second info resolves after the signal of the first: it waits for 1 + std::array const infos{ + SubmitInfo{.signals = {first}}, + SubmitInfo{.waits = {first}, .signals = {second}}, + }; + context.core.queue(0).submit(infos); + + second.wait(); + EXPECT_EQ(first.gpuValue(), 1); + EXPECT_EQ(second.gpuValue(), 1); +} + +CVK_TEST(Submit, explicit_timeline_value_keeps_the_counter) { + test::Context const context{}; + TimelineSemaphore const timeline{context.core, create}; + + context.core.queue(0).submit({.signals = {{timeline, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, 5}}}); + + context.core.queue(0).waitIdle(); + EXPECT_EQ(timeline.gpuValue(), 5); + EXPECT_EQ(timeline.value(), 0); +} +} diff --git a/cvk/tests/src/context.cpp b/cvk/tests/src/context.cpp new file mode 100644 index 00000000..ff15f40e --- /dev/null +++ b/cvk/tests/src/context.cpp @@ -0,0 +1,184 @@ +#include "context.hpp" + +#include "validation.hpp" + +#include "cvk/render/cmd/cmd_buffer.hpp" +#include "cvk/render/pipeline/shader/shader.hpp" +#include "cvk/render/sync/fence.hpp" + +#include +#include +#include +#include +#include + + +namespace cvk::test { +Core::Config const Context::CONFIG{.queueProperties = {std::from_range, QUEUES}}; + +Core::Config const PresentContext::CONFIG{ + .queueProperties = {QueueFamilyProperties::GRAPHICS | QueueFamilyProperties::PRESENT}, +}; + +namespace { + /** + * @brief one instance for both shared contexts, the cores and the surface come at their first use + * @details volk holds the functions of one instance, cvk assumes one instance per process + */ + struct SharedContexts { + Instance instance{instance_config(headless_extensions()), create, counting_messenger()}; + std::optional core{}; + std::optional surface{}; + std::optional presentCore{}; + }; + + std::optional shared{}; + + SharedContexts& shared_contexts() { + if(!shared.has_value()) shared.emplace(); + // a test with an own instance loaded its functions into volk + volkLoadInstanceOnly(shared->instance.get()); + return *shared; + } + + Core const& shared_core() { + auto& contexts = shared_contexts(); + if(!contexts.core.has_value()) + contexts.core.emplace(contexts.instance, std::span{}, Context::CONFIG); + return *contexts.core; + } + + Surface const& shared_surface() { + auto& contexts = shared_contexts(); + if(!contexts.surface.has_value()) contexts.surface.emplace(headless_surface(contexts.instance)); + return *contexts.surface; + } + + Core const& shared_present_core() { + auto& contexts = shared_contexts(); + if(!contexts.presentCore.has_value()) { + contexts.presentCore.emplace( + contexts.instance, + std::span{&shared_surface(), 1}, + PresentContext::CONFIG + ); + } + return *contexts.presentCore; + } +} + +Context::Context() : instance{shared_contexts().instance}, core{shared_core()} {} + +Context::Context(Core::Config const& config) : + _core{std::in_place, shared_contexts().instance, std::span{}, config}, + instance{shared_contexts().instance}, core{*_core} {} + +PresentContext::PresentContext() : instance{shared_contexts().instance}, core{shared_present_core()}, + _surface{shared_surface()} {} + +InstanceContext::InstanceContext(std::vector extensions) : + instance{instance_config(std::move(extensions)), create, counting_messenger()} {} + +CmdPool::Config pool_config(Core const& core) { return {.queueFamilyIndex = core.queue(0).familyIndex()}; } + +GraphicsPipeline::Config fullscreen_config( + Shader const& vertex, + Shader const& fragment, + std::span colorFormats +) { + GraphicsPipeline::Config config{.colorFormats = {std::from_range, colorFormats}}; + config.addStage(vertex).addStage(fragment); + config.vertexBindings = {{.binding = 0, .stride = sizeof(Vec3), .inputRate = VK_VERTEX_INPUT_RATE_VERTEX}}; + config.vertexAttributes = {{.location = 0, .binding = 0, .format = VK_FORMAT_R32G32B32_SFLOAT, .offset = 0}}; + config.rasterization.cullMode = VK_CULL_MODE_NONE; + return config; +} + +bool every_pixel_is(BaseBuffer const& readback, std::array pixel) { + auto const pixels = readback.map(); + readback.invalidate(); + auto const asByte = [](std::byte b) { return std::to_integer(b); }; + return std::ranges::all_of( + pixels | std::views::chunk(4), + [&](auto const& bytes) { return std::ranges::equal(bytes, pixel, {}, asByte); } + ); +} + +void destroy_shared_contexts() { + if(!shared.has_value()) return; + + volkLoadInstanceOnly(shared->instance.get()); + reset_validation_errors(); + shared.reset(); + EXPECT_EQ(validation_errors(), 0) << "errors at the destruction of the shared contexts, e.g. leaks"; +} + +Context::ValidationCheck::ValidationCheck() { reset_validation_errors(); } + +Context::ValidationCheck::~ValidationCheck() { EXPECT_EQ(validation_errors(), 0); } + +void submit_and_wait(Context const& context, PrimaryCmdBuffer const& primary) { + Fence const fence{context.core, create}; + + context.core.queue(0).submit({.cmdBuffers = {&primary}}, &fence); + fence.wait(); +} + +void host_read_barrier(Context const& context, BaseCmdBuffer const& cmdBuffer) { + VkMemoryBarrier2 const barrier{ + .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2, + .pNext = nullptr, + .srcStageMask = VK_PIPELINE_STAGE_2_TRANSFER_BIT, + .srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT, + .dstStageMask = VK_PIPELINE_STAGE_2_HOST_BIT, + .dstAccessMask = VK_ACCESS_2_HOST_READ_BIT, + }; + VkDependencyInfo const dependency{ + .sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, + .pNext = nullptr, + .dependencyFlags = 0, + .memoryBarrierCount = 1, + .pMemoryBarriers = &barrier, + .bufferMemoryBarrierCount = 0, + .pBufferMemoryBarriers = nullptr, + .imageMemoryBarrierCount = 0, + .pImageMemoryBarriers = nullptr, + }; + context.core.functions()->vkCmdPipelineBarrier2(cmdBuffer.get(), &dependency); +} + +void copy_to_buffer( + Context const& context, + BaseCmdBuffer const& cmdBuffer, + Image const& image, + BaseBuffer const& dst, + size_t dstOffset, + uint32_t mipLevel, + VkImageLayout layout +) { + VkBufferImageCopy2 const region{ + .sType = VK_STRUCTURE_TYPE_BUFFER_IMAGE_COPY_2, + .pNext = nullptr, + .bufferOffset = dstOffset, + .bufferRowLength = 0, + .bufferImageHeight = 0, + .imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, mipLevel, 0, 1}, + .imageOffset = {0, 0, 0}, + .imageExtent = { + std::max(image.extent().width >> mipLevel, 1u), + std::max(image.extent().height >> mipLevel, 1u), + 1, + }, + }; + VkCopyImageToBufferInfo2 const info{ + .sType = VK_STRUCTURE_TYPE_COPY_IMAGE_TO_BUFFER_INFO_2, + .pNext = nullptr, + .srcImage = image.get(), + .srcImageLayout = layout, + .dstBuffer = dst.get(), + .regionCount = 1, + .pRegions = ®ion, + }; + context.core.functions()->vkCmdCopyImageToBuffer2(cmdBuffer.get(), &info); +} +} diff --git a/cvk/tests/src/context.hpp b/cvk/tests/src/context.hpp new file mode 100644 index 00000000..5cf2ce3e --- /dev/null +++ b/cvk/tests/src/context.hpp @@ -0,0 +1,239 @@ +#pragma once +#include "headless.hpp" +#include "test.hpp" + +#include "cvk/base/core.hpp" +#include "cvk/base/instance.hpp" +#include "cvk/data/buffer/base_buffer.hpp" +#include "cvk/data/img/image.hpp" +#include "cvk/render/cmd/cmd_buffer.hpp" +#include "cvk/render/cmd/cmd_pool.hpp" +#include "cvk/render/pipeline/graphics_pipeline.hpp" + +#include +#include +#include +#include +#include +#include + + +namespace cvk { +class Shader; +} + +namespace cvk::test { +/** + * @brief one queue, some devices have one queue only + */ +inline constexpr std::array QUEUES{QueueFamilyProperties::GRAPHICS | QueueFamilyProperties::COMPUTE}; + +/** + * @brief buffer the cpu writes once + */ +[[nodiscard]] constexpr BaseBuffer::Config host_written(VkBufferUsageFlags usage) { + return {.usage = usage, .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT}}; +} + +/** + * @brief buffer the cpu writes once and the gpu copies from + */ +inline constexpr BaseBuffer::Config STAGING_CONFIG = host_written(VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + +/** + * @brief buffer the gpu copies into and the cpu reads back + */ +inline constexpr BaseBuffer::Config READBACK_CONFIG{ + .usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT, + .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT}, +}; + +/** + * @brief binding flags of a bindless descriptor array + */ +inline constexpr VkDescriptorBindingFlags BINDLESS = VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT + | VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT + | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT; + +/** + * @brief vertex of @ref FULLSCREEN, the vec3 position at location 0 of the test vertex shaders + */ +struct Vec3 { + float x, y, z; +}; + +/** + * @brief one triangle that covers the whole viewport + */ +inline constexpr std::array FULLSCREEN{Vec3{-1.f, -1.f, 0.f}, Vec3{3.f, -1.f, 0.f}, Vec3{-1.f, 3.f, 0.f}}; + +/** + * @brief color of the fill draws + */ +inline constexpr std::array GREEN{0.f, 1.f, 0.f, 1.f}; + +/** + * @brief @ref GREEN as a VK_FORMAT_R8G8B8A8_UNORM pixel + */ +inline constexpr std::array GREEN_PIXEL{0, 255, 0, 255}; + +/** + * @brief pipeline config that draws @ref FULLSCREEN from vertex binding 0 without culling + * @pre the shaders are created + */ +[[nodiscard]] GraphicsPipeline::Config fullscreen_config( + Shader const& vertex, + Shader const& fragment, + std::span colorFormats +); + +/** + * @return every VK_FORMAT_R8G8B8A8_UNORM pixel of the buffer is the pixel + * @pre the buffer is host visible, the gpu writes are visible to the host + */ +[[nodiscard]] bool every_pixel_is(BaseBuffer const& readback, std::array pixel); + +/** + * @brief instance and core for tests, fails the test on validation errors + * @details the default is the shared instance and core of the process, made at the first use, + * @ref destroy_shared_contexts() destroys them, the leaks of all tests show up there + */ +struct Context { + /** + * @brief the queues of @ref QUEUES + */ + static Core::Config const CONFIG; + + struct ValidationCheck { + ValidationCheck(); + ~ValidationCheck(); + }; + + /** + * @brief the shared instance and core + */ + Context(); + + /** + * @brief the shared instance and an own core with the config + * @details destroys the core at the end -> the check sees the errors of vkDestroyDevice() too, e.g. leaks + * @throws cvk::cvk_exception reason: no physical device meets the requirements + */ + explicit Context(Core::Config const&); + + ValidationCheck validationCheck; + +private: + std::optional _core{}; + +public: + Instance const& instance; + Core const& core; + + Context(Context const& other) = delete; + Context(Context&& other) = delete; + Context& operator=(Context const& other) = delete; + Context& operator=(Context&& other) = delete; +}; + +/** + * @brief instance, headless surface and core with a present queue, fails the test on validation errors + * @details the shared ones of the process, made at the first use, see @ref Context + */ +struct PresentContext { + /** + * @brief one graphics queue that presents + */ + static Core::Config const CONFIG; + + PresentContext(); + + Context::ValidationCheck validationCheck; + + Instance const& instance; + Core const& core; + + [[nodiscard]] Surface const& surface() const { return _surface; } + [[nodiscard]] Queue const& queue() const { return core.queue(0); } + +private: + Surface const& _surface; + +public: + PresentContext(PresentContext const& other) = delete; + PresentContext(PresentContext&& other) = delete; + PresentContext& operator=(PresentContext const& other) = delete; + PresentContext& operator=(PresentContext&& other) = delete; +}; + +/** + * @brief own instance with @ref counting_messenger(), fails the test on validation errors + * @details for the tests that need an own instance, e.g. the leak reports of vkDestroyInstance(): + * the check runs after the instance is destroyed + */ +struct InstanceContext { + explicit InstanceContext(std::vector extensions = {}); + + Context::ValidationCheck validationCheck; + Instance instance; + + InstanceContext(InstanceContext const& other) = delete; + InstanceContext(InstanceContext&& other) = delete; + InstanceContext& operator=(InstanceContext const& other) = delete; + InstanceContext& operator=(InstanceContext&& other) = delete; +}; + +/** + * @brief pool config for the first queue of the core + */ +[[nodiscard]] CmdPool::Config pool_config(Core const&); + +/** + * @brief destroys the shared contexts, fails on their validation errors + * @details the test environment calls it before the loader unloads + */ +void destroy_shared_contexts(); + +/** + * @brief submits the command buffer to the first queue and waits for the gpu + * @pre the command buffer is executable + */ +void submit_and_wait(Context const&, PrimaryCmdBuffer const&); + +/** + * @brief records a memory barrier: transfer writes -> host reads + * @pre the command buffer is recording + */ +void host_read_barrier(Context const&, BaseCmdBuffer const&); + +/** + * @brief records a copy of the mip level into the buffer, without cvk: tests need a second path + * @param layout of the mip level, a transfer src layout + * @pre the image has a color aspect + */ +void copy_to_buffer( + Context const&, + BaseCmdBuffer const&, + Image const&, + BaseBuffer const& dst, + size_t dstOffset, + uint32_t mipLevel, + VkImageLayout layout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL +); + +/** + * @brief records into a primary command buffer of a new pool, submits it and waits for the gpu + * @param record called with the recording PrimaryCmdBuffer& + */ +template +void record_and_submit(Context const& context, Record&& record) { + CmdPool const pool{context.core, pool_config(context.core), create}; + PrimaryCmdBuffer primary{pool, create}; + + primary.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); + std::forward(record)(primary); + primary.end(); + + submit_and_wait(context, primary); +} +} diff --git a/cvk/tests/src/data/buffer/base_buffer_test.cpp b/cvk/tests/src/data/buffer/base_buffer_test.cpp new file mode 100644 index 00000000..e575b777 --- /dev/null +++ b/cvk/tests/src/data/buffer/base_buffer_test.cpp @@ -0,0 +1,227 @@ +#include "context.hpp" +#include "test.hpp" + +#include "cvk/base/physical_device.hpp" +#include "cvk/data/buffer/base_buffer.hpp" +#include "cvk/render/cmd/cmd_buffer.hpp" + +#include +#include +#include + + +namespace cvk { +namespace { + using test::READBACK_CONFIG; + using test::STAGING_CONFIG; + + constexpr size_t SIZE = 256; + + /** + * @brief gpu only buffer + */ + constexpr BaseBuffer::Config DEVICE_CONFIG{.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT}; +} + +CVK_TEST(BaseBuffer, config_create_info) { + constexpr auto info = STAGING_CONFIG.createInfo(SIZE); + + static_assert(info.size == SIZE); + static_assert(info.usage == VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + static_assert(info.flags == 0); + static_assert(info.sharingMode == VK_SHARING_MODE_EXCLUSIVE); + static_assert(info.queueFamilyIndexCount == 0 && info.pQueueFamilyIndices == STAGING_CONFIG.queueFamilies.data()); + static_assert(STAGING_CONFIG.memory.autoUsage()); +} + +CVK_TEST(BaseBuffer, config_one_queue_family_is_exclusive) { + static_assert([] { + BaseBuffer::Config config{.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT, .queueFamilies = {2}}; + auto const info = config.createInfo(SIZE); + return info.sharingMode == VK_SHARING_MODE_EXCLUSIVE && info.queueFamilyIndexCount == 1; + }()); +} + +CVK_TEST(BaseBuffer, config_queue_families_are_concurrent) { + static_assert([] { + BaseBuffer::Config config{ + .usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT, + .flags = VK_BUFFER_CREATE_PROTECTED_BIT, + .queueFamilies = {0, 2}, + }; + auto const info = config.createInfo(SIZE); + return info.sharingMode == VK_SHARING_MODE_CONCURRENT && info.queueFamilyIndexCount == 2 + && info.pQueueFamilyIndices == config.queueFamilies.data() && info.flags == VK_BUFFER_CREATE_PROTECTED_BIT; + }()); +} + +CVK_TEST(BaseBuffer, config_debug_check_is_constexpr) { + static_assert([] { + BaseBuffer::Config::debug_check({.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT, .queueFamilies = {1, 0}}); + return true; + }()); +} + +CVK_TEST(BaseBuffer, base_constructor_does_not_create) { + test::Context const context{}; + + BaseBuffer const buffer{context.core, STAGING_CONFIG}; + + EXPECT_FALSE(buffer.created()); + EXPECT_EQ(buffer.memory(), nullptr); +} + +CVK_TEST(BaseBuffer, create_and_destroy) { + test::Context const context{}; + BaseBuffer buffer{context.core, DEVICE_CONFIG, SIZE}; + + ASSERT_TRUE(buffer.created()); + EXPECT_EQ(&buffer.core(), &context.core); + EXPECT_EQ(buffer.size(), SIZE); + ASSERT_NE(buffer.memory(), nullptr); + EXPECT_GE(buffer.memory()->size(), SIZE); + + buffer.destroy(); + EXPECT_FALSE(buffer.created()); + EXPECT_EQ(buffer.memory(), nullptr); + EXPECT_EQ(buffer.size(), 0); +} + +CVK_TEST(BaseBuffer, create_concurrent) { + test::Context const context{}; + auto const familyCount = context.core.physicalDevice().queueFamilies().size(); + if(familyCount < 2) GTEST_SKIP() << "needs 2 queue families"; + + auto config = DEVICE_CONFIG; + config.queueFamilies = {0, 1}; + + BaseBuffer const buffer{context.core, config, SIZE}; + + EXPECT_TRUE(buffer.created()); +} + +CVK_TEST(BaseBuffer, auto_usage_picks_host_visible_memory_for_host_access) { + test::Context const context{}; + + BaseBuffer const buffer{context.core, STAGING_CONFIG, SIZE}; + + EXPECT_NE(buffer.memory()->properties() & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, 0u); +} + +CVK_TEST(BaseBuffer, write_and_read) { + test::Context const context{}; + BaseBuffer const buffer{context.core, READBACK_CONFIG, SIZE}; + std::array data{}; + std::ranges::fill(data, std::byte{'x'}); + + buffer.write(data); + + auto const mapped = buffer.map(); + buffer.invalidate(); + EXPECT_TRUE(std::ranges::equal(mapped, data)); +} + +CVK_TEST(BaseBuffer, copy_a_range) { + test::Context const context{}; + BaseBuffer const staging{context.core, STAGING_CONFIG, SIZE}; + BaseBuffer const readback{context.core, READBACK_CONFIG, SIZE}; + std::array data{}; + std::ranges::fill(data, std::byte{'c'}); + staging.write(data, 16); + + test::record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { + readback.copy(primary, staging, {.srcOffset = 16, .dstOffset = 32, .size = data.size()}); + test::host_read_barrier(context, primary); + }); + + auto const mapped = readback.map({.offset = 32, .size = data.size()}); + readback.invalidate(); + EXPECT_TRUE(std::ranges::equal(mapped, data)); +} + +CVK_TEST(BaseBuffer, copy_whole_size_fits_both_buffers) { + test::Context const context{}; + BaseBuffer const staging{context.core, STAGING_CONFIG, SIZE}; + BaseBuffer const readback{context.core, READBACK_CONFIG, SIZE / 2}; + std::array data{}; + std::ranges::fill(data, std::byte{'w'}); + staging.write(data); + + test::record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { + readback.copy(primary, staging); + test::host_read_barrier(context, primary); + }); + + auto const mapped = readback.map(); + readback.invalidate(); + EXPECT_TRUE(std::ranges::all_of(mapped, [](std::byte b) { return b == std::byte{'w'}; })); +} + +CVK_TEST(BaseBuffer, copy_the_whole_source_to_an_offset) { + test::Context const context{}; + BaseBuffer const staging{context.core, STAGING_CONFIG, 64}; + BaseBuffer const readback{context.core, READBACK_CONFIG, SIZE}; + std::array data{}; + std::ranges::fill(data, std::byte{'s'}); + staging.write(data); + + test::record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { + readback.copy(primary, staging, {.dstOffset = 8}); + test::host_read_barrier(context, primary); + }); + + auto const mapped = readback.map({.offset = 8, .size = data.size()}); + readback.invalidate(); + EXPECT_TRUE(std::ranges::equal(mapped, data)); +} + +CVK_TEST(BaseBuffer, release_and_wrap) { + test::Context const context{}; + BaseBuffer buffer{context.core, DEVICE_CONFIG, SIZE}; + auto const vkBuffer = buffer.get(); + auto const vmaAllocation = buffer.memory()->get(); + + auto const state = buffer.release(); + EXPECT_FALSE(buffer.created()); + EXPECT_EQ(buffer.memory(), nullptr); + ASSERT_TRUE(state.memory.has_value()); + + BaseBuffer const wrapped{context.core, DEVICE_CONFIG, state}; + EXPECT_EQ(wrapped.get(), vkBuffer); + ASSERT_NE(wrapped.memory(), nullptr); + EXPECT_EQ(wrapped.memory()->get(), vmaAllocation); + EXPECT_EQ(wrapped.size(), SIZE); +} + +CVK_TEST(BaseBuffer, wrap_without_memory_destroys_only_the_buffer) { + test::Context const context{}; + BaseBuffer buffer{context.core, DEVICE_CONFIG, SIZE}; + auto state = buffer.release(); + + // another owner frees the memory, after the buffer + Memory const memory{context.core, DEVICE_CONFIG.memory, *state.memory}; + state.memory = std::nullopt; + + { + BaseBuffer const wrapped{context.core, DEVICE_CONFIG, state}; + EXPECT_EQ(wrapped.memory(), nullptr); + EXPECT_EQ(wrapped.size(), SIZE); + } + + EXPECT_TRUE(memory.created()); +} + +CVK_TEST(BaseBuffer, move_assignment_destroys_the_old_buffer) { + test::Context const context{}; + BaseBuffer buffer{context.core, DEVICE_CONFIG, SIZE}; + BaseBuffer other{context.core, DEVICE_CONFIG, SIZE}; + auto const vkBuffer = other.get(); + + buffer = std::move(other); + + EXPECT_EQ(buffer.get(), vkBuffer); + EXPECT_NE(buffer.memory(), nullptr); + EXPECT_FALSE(other.created()); + EXPECT_EQ(other.memory(), nullptr); +} +} diff --git a/cvk/tests/src/data/buffer/buffer_test.cpp b/cvk/tests/src/data/buffer/buffer_test.cpp new file mode 100644 index 00000000..efcf41ad --- /dev/null +++ b/cvk/tests/src/data/buffer/buffer_test.cpp @@ -0,0 +1,153 @@ +#include "context.hpp" +#include "test.hpp" + +#include "cvk/data/buffer/buffer.hpp" +#include "cvk/render/cmd/cmd_buffer.hpp" + +#include +#include +#include + + +namespace cvk { +namespace { + using test::READBACK_CONFIG; + using test::STAGING_CONFIG; + + struct Vertex { + float x, y, z; + uint32_t color; + + friend constexpr bool operator==(Vertex const&, Vertex const&) = default; + }; + + constexpr size_t COUNT = 8; + + constexpr std::array TRIANGLE{ + Vertex{0.f, 1.f, 0.f, 0xff0000ff}, + Vertex{1.f, 0.f, 0.f, 0x00ff00ff}, + Vertex{0.f, 0.f, 1.f, 0x0000ffff}, + }; + + /** + * @brief the cpu writes and reads + */ + constexpr Buffer::Config HOST_CONFIG{ + .usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, + .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT}, + }; + + /** + * @brief Buffer is a valid type + */ + template + concept valid_buffer = requires { sizeof(Buffer); }; + + void take_base(BaseBuffer const& buffer, size_t size) { EXPECT_EQ(buffer.size(), size); } +} + +CVK_TEST(Buffer, elements_must_be_trivially_copyable) { + static_assert(valid_buffer); + static_assert(valid_buffer); + static_assert(!valid_buffer); +} + +CVK_TEST(Buffer, adds_no_members) { static_assert(sizeof(Buffer) == sizeof(BaseBuffer)); } + +CVK_TEST(Buffer, create_counts_elements) { + test::Context const context{}; + + Buffer const buffer{context.core, HOST_CONFIG, COUNT}; + + ASSERT_TRUE(buffer.created()); + EXPECT_EQ(buffer.elements(), COUNT); + EXPECT_EQ(buffer.size(), COUNT * sizeof(Vertex)); + take_base(buffer, COUNT * sizeof(Vertex)); +} + +CVK_TEST(Buffer, write_and_map) { + test::Context const context{}; + Buffer const buffer{context.core, HOST_CONFIG, COUNT}; + + buffer.write(TRIANGLE, 2); + + auto const mapped = buffer.map({.offset = 2, .size = TRIANGLE.size()}); + buffer.invalidate(); + EXPECT_TRUE(std::ranges::equal(mapped, TRIANGLE)); +} + +CVK_TEST(Buffer, map_whole_buffer) { + test::Context const context{}; + Buffer const buffer{context.core, HOST_CONFIG, COUNT}; + + auto const whole = buffer.map(); + EXPECT_EQ(whole.size(), COUNT); + + std::ranges::copy(TRIANGLE, whole.begin()); + buffer.flush(); + buffer.flush({.offset = 2, .size = 1}); + + auto const end = buffer.map({.offset = 5}); + EXPECT_EQ(end.size(), COUNT - 5); + EXPECT_EQ(end.data(), whole.data() + 5); +} + +CVK_TEST(Buffer, copy_elements) { + test::Context const context{}; + Buffer const staging{context.core, STAGING_CONFIG, COUNT}; + Buffer const readback{context.core, READBACK_CONFIG, COUNT}; + staging.write(TRIANGLE, 1); + + test::record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { + readback.copy(primary, staging, {.srcOffset = 1, .dstOffset = 4, .size = TRIANGLE.size()}); + test::host_read_barrier(context, primary); + }); + + auto const mapped = readback.map({.offset = 4, .size = TRIANGLE.size()}); + readback.invalidate(); + EXPECT_TRUE(std::ranges::equal(mapped, TRIANGLE)); +} + +CVK_TEST(Buffer, copy_the_whole_source_to_an_element) { + test::Context const context{}; + Buffer const staging{context.core, STAGING_CONFIG, TRIANGLE.size()}; + Buffer const readback{context.core, READBACK_CONFIG, COUNT}; + staging.write(TRIANGLE); + + test::record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { + readback.copy(primary, staging, {.dstOffset = 5}); + test::host_read_barrier(context, primary); + }); + + auto const mapped = readback.map({.offset = 5, .size = TRIANGLE.size()}); + readback.invalidate(); + EXPECT_TRUE(std::ranges::equal(mapped, TRIANGLE)); +} + +CVK_TEST(Buffer, release_and_wrap_as_bytes) { + test::Context const context{}; + Buffer buffer{context.core, HOST_CONFIG, COUNT}; + buffer.write(TRIANGLE); + auto const vkBuffer = buffer.get(); + + auto const state = buffer.release(); + EXPECT_FALSE(buffer.created()); + + Buffer const bytes{context.core, HOST_CONFIG, state}; + EXPECT_EQ(bytes.get(), vkBuffer); + EXPECT_EQ(bytes.elements(), COUNT * sizeof(Vertex)); +} + +CVK_TEST(Buffer, move_assignment_destroys_the_old_buffer) { + test::Context const context{}; + Buffer buffer{context.core, HOST_CONFIG, COUNT}; + Buffer other{context.core, HOST_CONFIG, 2 * COUNT}; + auto const vkBuffer = other.get(); + + buffer = std::move(other); + + EXPECT_EQ(buffer.get(), vkBuffer); + EXPECT_EQ(buffer.elements(), 2 * COUNT); + EXPECT_FALSE(other.created()); +} +} diff --git a/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp b/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp new file mode 100644 index 00000000..7235eb1f --- /dev/null +++ b/cvk/tests/src/data/descriptor/descriptor_draw_test.cpp @@ -0,0 +1,242 @@ +#include "context.hpp" +#include "shaders.hpp" +#include "test.hpp" + +#include "cvk/data/buffer/buffer.hpp" +#include "cvk/data/descriptor/descriptor_pool.hpp" +#include "cvk/data/descriptor/descriptor_set.hpp" +#include "cvk/data/img/image.hpp" +#include "cvk/data/img/image_view.hpp" +#include "cvk/data/img/sampler.hpp" +#include "cvk/render/cmd/cmd_buffer.hpp" +#include "cvk/render/cmd/cmd_pool.hpp" +#include "cvk/render/pipeline/graphics_pipeline.hpp" +#include "cvk/render/pipeline/layout/descriptor_set_layout.hpp" +#include "cvk/render/pipeline/layout/pipeline_layout.hpp" +#include "cvk/render/pipeline/shader/shader.hpp" +#include "cvk/render/sync/barrier.hpp" + +#include +#include +#include +#include +#include +#include + + +namespace cvk { +namespace { + using test::host_written; + using test::Vec3; + + constexpr VkExtent2D EXTENT{16, 16}; + constexpr std::array COLOR_FORMATS{VK_FORMAT_R8G8B8A8_UNORM}; + + /** + * @brief fullscreen pipeline with one descriptor set and the fragment shader + */ + struct Fill { + Fill(Core const& core, DescriptorSetLayout const& setLayout, std::span fragmentCode, + std::vector pushConstants = {}) : layout{ + core, + PipelineLayout::Config{.setLayouts = {&setLayout}, .pushConstantRanges = std::move(pushConstants)}, + create + }, + vertex{core, VK_SHADER_STAGE_VERTEX_BIT, test::shaders::FILL_VERT}, + fragment{core, VK_SHADER_STAGE_FRAGMENT_BIT, fragmentCode}, + pipeline{layout, test::fullscreen_config(vertex, fragment, COLOR_FORMATS), create}, + vertices{core, host_written(VK_BUFFER_USAGE_VERTEX_BUFFER_BIT), test::FULLSCREEN.size()} { + vertices.write(test::FULLSCREEN); + } + + PipelineLayout layout; + Shader vertex; + Shader fragment; + GraphicsPipeline pipeline; + Buffer vertices; + }; + + /** + * @brief target image, its view and a readback buffer + */ + struct Target { + explicit Target(Core const& core) : image{ + core, + { + .format = COLOR_FORMATS[0], + .usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT, + }, + EXTENT + }, + view{image, {}, create}, + readback{core, test::READBACK_CONFIG, size_t{EXTENT.width} * EXTENT.height * 4} {} + + Image image; + ImageView view; + BaseBuffer readback; + }; + + /** + * @brief records: clear the target, draw with draw(), copy the target into the readback buffer + */ + void record_fill( + test::Context const& context, + BaseCmdBuffer& cmd, + Fill const& fill, + Target const& target, + std::function const& draw + ) { + Barrier toAttachment{}; + toAttachment.addTransition(target.image, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED); + toAttachment.execute(cmd); + + cmd.beginRendering( + { + .colorAttachments = { + RenderingAttachment{ + .view = &target.view, + .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR, + .clearValue = {.color = {.float32 = {0.f, 0.f, 0.f, 1.f}}}, + }, + }, + } + ); + cmd.bindPipeline(fill.pipeline); + cmd.bindVertexBuffer(0, fill.vertices); + draw(cmd); + cmd.draw(3); + cmd.endRendering(); + + Barrier toTransfer{}; + toTransfer.add( + target.image, + ImageBarrierInfo::LayoutTransition( + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + {VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_READ_BIT}, + {VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT} + ) + ); + toTransfer.execute(cmd); + + test::copy_to_buffer(context, cmd, target.image, target.readback, 0, 0); + test::host_read_barrier(context, cmd); + } +} + +CVK_TEST(DescriptorDraw, uniform_buffer_color) { + test::Context const context{}; + DescriptorSetLayout::Config setConfig{}; + setConfig.add(0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_FRAGMENT_BIT); + DescriptorSetLayout const setLayout{context.core, setConfig, create}; + Fill const fill{context.core, setLayout, test::shaders::UNIFORM_COLOR_FRAG}; + Target const target{context.core}; + + Buffer> color{context.core, host_written(VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT), 1}; + color.write(std::array, 1>{test::GREEN}); + + DescriptorPool const pool{context.core, DescriptorPool::Config::ForLayouts({{&setLayout}}), create}; + DescriptorSet const set{pool, setLayout, create}; + set.write({0, color}); + + test::record_and_submit(context, [&](PrimaryCmdBuffer& cmd) { + record_fill(context, cmd, fill, target, [&](BaseCmdBuffer& c) { c.bindDescriptorSet(set, fill.layout, 0); }); + }); + + EXPECT_TRUE(test::every_pixel_is(target.readback, test::GREEN_PIXEL)); +} + +namespace { + /** + * @brief draws a green texture + * @param layout of the texture at the draw (VK_IMAGE_LAYOUT_MAX_ENUM => the default, shader read only) + * @param afterBind true -> bindless binding of 64, the descriptor at element 17 comes after the recording, + * false -> one sampler2D, the descriptor comes before the recording, the layers check its layout at the draw + */ + void draw_texture(test::Context const& context, VkImageLayout layout, bool afterBind) { + uint32_t const index = afterBind ? 17 : 0; + auto const textureLayout = layout != VK_IMAGE_LAYOUT_MAX_ENUM ? layout + : VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + + DescriptorSetLayout::Config setConfig{}; + setConfig.add( + 0, + VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, + VK_SHADER_STAGE_FRAGMENT_BIT, + afterBind ? 64 : 1, + afterBind ? test::BINDLESS : 0 + ); + DescriptorSetLayout const setLayout{context.core, setConfig, create}; + Fill const fill{ + context.core, + setLayout, + afterBind ? std::span{test::shaders::BINDLESS_FRAG} + : std::span{test::shaders::TEXTURE_FRAG}, + {{VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(uint32_t)}} + }; + Target const target{context.core}; + + // 1x1 green texture + Image const texture{ + context.core, + {.format = VK_FORMAT_R8G8B8A8_UNORM, .usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT}, + VkExtent2D{1, 1} + }; + ImageView const view{texture, {}, create}; + Sampler const sampler{context.core, {}, create}; + Buffer staging{context.core, test::STAGING_CONFIG, 4}; + staging.write(test::GREEN_PIXEL); + + uint32_t const variableCount = afterBind ? 32 : 0; + DescriptorPool const pool{ + context.core, + DescriptorPool::Config::ForLayouts({{&setLayout, 1, variableCount}}), + create + }; + DescriptorSet const set{pool, setLayout, variableCount, create}; + if(!afterBind) set.write({0, view, sampler, index, layout}); + + CmdPool const cmdPool{context.core, test::pool_config(context.core), create}; + PrimaryCmdBuffer cmd{cmdPool, create}; + cmd.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); + + Barrier toTransfer{}; + toTransfer.addTransition(texture, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED); + toTransfer.execute(cmd); + texture.copy(cmd, staging); + Barrier toShader{}; + toShader.add( + texture, + ImageBarrierInfo::LayoutTransition( + textureLayout, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + {VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT, VK_ACCESS_2_SHADER_READ_BIT}, + {VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT} + ) + ); + toShader.execute(cmd); + + record_fill(context, cmd, fill, target, [&](BaseCmdBuffer& c) { + c.bindDescriptorSet(set, fill.layout, 0); + c.pushConstants(fill.layout, VK_SHADER_STAGE_FRAGMENT_BIT, index); + }); + cmd.end(); + + // update after bind: the bind is recorded, the descriptor comes now + if(afterBind) set.write({0, view, sampler, index, layout}); + test::submit_and_wait(context, cmd); + + EXPECT_TRUE(test::every_pixel_is(target.readback, test::GREEN_PIXEL)); + } +} + +CVK_TEST(DescriptorDraw, bindless_texture_written_after_bind) { + test::Context const context{}; + draw_texture(context, VK_IMAGE_LAYOUT_MAX_ENUM, true); +} + +CVK_TEST(DescriptorDraw, texture_in_the_layout_of_the_write) { + test::Context const context{}; + draw_texture(context, VK_IMAGE_LAYOUT_GENERAL, false); +} +} diff --git a/cvk/tests/src/data/descriptor/descriptor_pool_test.cpp b/cvk/tests/src/data/descriptor/descriptor_pool_test.cpp new file mode 100644 index 00000000..44c7ad7e --- /dev/null +++ b/cvk/tests/src/data/descriptor/descriptor_pool_test.cpp @@ -0,0 +1,128 @@ +#include "context.hpp" +#include "test.hpp" + +#include "cvk/data/descriptor/descriptor_pool.hpp" +#include "cvk/render/pipeline/layout/descriptor_set_layout.hpp" + +#include + + +namespace cvk { +namespace { + /** + * @brief binding 0: 2 uniform buffers, binding 1: 1 combined image sampler + */ + DescriptorSetLayout::Config buffers_and_texture() { + DescriptorSetLayout::Config config{}; + config.add(0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_VERTEX_BIT, 2) + .add(1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT); + return config; + } + + /** + * @brief binding 0: up to 1024 combined image samplers, bindless flags + */ + DescriptorSetLayout::Config bindless() { + DescriptorSetLayout::Config config{}; + config.add(0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT, 1024, test::BINDLESS); + return config; + } + + uint32_t count_of(DescriptorPool::Config const& config, VkDescriptorType type) { + auto const it = std::ranges::find(config.sizes, type, &VkDescriptorPoolSize::type); + return it != config.sizes.end() ? it->descriptorCount : 0; + } +} + +CVK_TEST(DescriptorPool, config_add_counts_the_descriptors_of_the_layout) { + test::Context const context{}; + DescriptorSetLayout const layout{context.core, buffers_and_texture(), create}; + + DescriptorPool::Config config{}; + config.add(layout, 3); + + EXPECT_EQ(config.maxSets, 3); + EXPECT_EQ(config.sizes.size(), 2); + EXPECT_EQ(count_of(config, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER), 6); + EXPECT_EQ(count_of(config, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER), 3); + EXPECT_EQ(config.flags, 0); +} + +CVK_TEST(DescriptorPool, config_add_takes_the_variable_count_and_update_after_bind) { + test::Context const context{}; + DescriptorSetLayout const layout{context.core, bindless(), create}; + + DescriptorPool::Config config{}; + config.add(layout, 2, 100); + + EXPECT_EQ(config.maxSets, 2); + EXPECT_EQ(count_of(config, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER), 200); + EXPECT_NE(config.flags & VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT, 0); +} + +CVK_TEST(DescriptorPool, config_is_an_aggregate) { + static_assert(std::is_aggregate_v); + + DescriptorPool::Config const config{.maxSets = 2, .flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT}; + + EXPECT_EQ(config.maxSets, 2); + EXPECT_TRUE(config.sizes.empty()); + EXPECT_EQ(config.flags, VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT); +} + +CVK_TEST(DescriptorPool, for_layouts_counts_every_layout) { + test::Context const context{}; + DescriptorSetLayout const layout{context.core, buffers_and_texture(), create}; + DescriptorSetLayout const textures{context.core, bindless(), create}; + + auto const config = DescriptorPool::Config::ForLayouts( + {{&layout, 2}, {&textures, 1, 100}}, + VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT + ); + + EXPECT_EQ(config.maxSets, 3); + EXPECT_EQ(count_of(config, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER), 4); + EXPECT_EQ(count_of(config, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER), 102); + EXPECT_NE(config.flags & VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT, 0); + EXPECT_NE(config.flags & VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT, 0); +} + +CVK_TEST(DescriptorPool, create_and_destroy) { + test::Context const context{}; + DescriptorSetLayout const layout{context.core, buffers_and_texture(), create}; + + DescriptorPool pool{context.core, DescriptorPool::Config::ForLayouts({{&layout, 4}}), create}; + + ASSERT_TRUE(pool.created()); + EXPECT_TRUE(pool.unused()); + EXPECT_FALSE(pool.freesSets()); + + pool.destroy(); + EXPECT_FALSE(pool.created()); +} + +CVK_TEST(DescriptorPool, reset_counts) { + test::Context const context{}; + DescriptorSetLayout const layout{context.core, buffers_and_texture(), create}; + DescriptorPool const pool{context.core, DescriptorPool::Config::ForLayouts({{&layout}}), create}; + + pool.reset(); + pool.reset(); + + EXPECT_EQ(pool.resets(), 2); +} + +CVK_TEST(DescriptorPool, release_and_wrap) { + test::Context const context{}; + DescriptorSetLayout const layout{context.core, buffers_and_texture(), create}; + auto const config = DescriptorPool::Config::ForLayouts({{&layout}}); + DescriptorPool pool{context.core, config, create}; + auto const vkPool = pool.get(); + + auto const state = pool.release(); + EXPECT_FALSE(pool.created()); + + DescriptorPool const wrapped{context.core, config, state}; + EXPECT_EQ(wrapped.get(), vkPool); +} +} diff --git a/cvk/tests/src/data/descriptor/descriptor_set_test.cpp b/cvk/tests/src/data/descriptor/descriptor_set_test.cpp new file mode 100644 index 00000000..3121bd9a --- /dev/null +++ b/cvk/tests/src/data/descriptor/descriptor_set_test.cpp @@ -0,0 +1,160 @@ +#include "context.hpp" +#include "test.hpp" + +#include "cvk/data/buffer/base_buffer.hpp" +#include "cvk/data/descriptor/descriptor_pool.hpp" +#include "cvk/data/descriptor/descriptor_set.hpp" +#include "cvk/data/img/image.hpp" +#include "cvk/data/img/image_view.hpp" +#include "cvk/data/img/sampler.hpp" +#include "cvk/render/pipeline/layout/descriptor_set_layout.hpp" + +#include + + +namespace cvk { +namespace { + /** + * @brief binding 0: uniform buffer, binding 1: combined image sampler + */ + DescriptorSetLayout::Config buffer_and_texture() { + DescriptorSetLayout::Config config{}; + config.add(0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_VERTEX_BIT) + .add(1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT); + return config; + } + + /** + * @brief binding 0: up to 64 combined image samplers, bindless flags + */ + DescriptorSetLayout::Config bindless() { + DescriptorSetLayout::Config config{}; + config.add(0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT, 64, test::BINDLESS); + return config; + } + + constexpr Image::Config TEXTURE_CONFIG{.format = VK_FORMAT_R8G8B8A8_UNORM, .usage = VK_IMAGE_USAGE_SAMPLED_BIT}; +} + +CVK_TEST(DescriptorSet, base_constructor_does_not_create) { + test::Context const context{}; + DescriptorSetLayout const layout{context.core, buffer_and_texture(), create}; + DescriptorPool const pool{context.core, DescriptorPool::Config::ForLayouts({{&layout}}), create}; + + DescriptorSet const set{pool, layout}; + + EXPECT_FALSE(set.created()); + EXPECT_EQ(&set.pool(), &pool); + EXPECT_EQ(&set.layout(), &layout); + EXPECT_TRUE(pool.unused()); +} + +CVK_TEST(DescriptorSet, destroy_frees_with_a_freeing_pool) { + test::Context const context{}; + DescriptorSetLayout const layout{context.core, buffer_and_texture(), create}; + DescriptorPool const pool{ + context.core, + DescriptorPool::Config::ForLayouts({{&layout}}, VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT), + create + }; + + DescriptorSet set{pool, layout, create}; + ASSERT_TRUE(set.created()); + EXPECT_FALSE(pool.unused()); + + set.destroy(); + EXPECT_FALSE(set.created()); + EXPECT_TRUE(pool.unused()); + + // the freed set is available again + set.create(); + EXPECT_TRUE(set.created()); +} + +CVK_TEST(DescriptorSet, destroy_without_free_only_forgets) { + test::Context const context{}; + DescriptorSetLayout const layout{context.core, buffer_and_texture(), create}; + DescriptorPool const pool{context.core, DescriptorPool::Config::ForLayouts({{&layout}}), create}; + + DescriptorSet set{pool, layout, create}; + set.destroy(); + + EXPECT_FALSE(set.created()); + EXPECT_TRUE(pool.unused()); + pool.reset(); +} + +CVK_TEST(DescriptorSet, pool_reset_frees_the_set) { + test::Context const context{}; + DescriptorSetLayout const layout{context.core, buffer_and_texture(), create}; + DescriptorPool const pool{context.core, DescriptorPool::Config::ForLayouts({{&layout}}), create}; + DescriptorSet set{pool, layout, create}; + + pool.reset(); + + EXPECT_FALSE(set.created()); + EXPECT_TRUE(pool.unused()); + + set.create(); + EXPECT_TRUE(set.created()); +} + +CVK_TEST(DescriptorSet, write_buffer_and_image) { + test::Context const context{}; + DescriptorSetLayout const layout{context.core, buffer_and_texture(), create}; + DescriptorPool const pool{context.core, DescriptorPool::Config::ForLayouts({{&layout}}), create}; + DescriptorSet const set{pool, layout, create}; + + BaseBuffer const buffer{context.core, {.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT}, 64}; + Image const image{context.core, TEXTURE_CONFIG, VkExtent2D{4, 4}}; + ImageView const view{image, {}, create}; + Sampler const sampler{context.core, {}, create}; + + set.write({{0, buffer, {.offset = 0, .size = 16}}, {1, view, sampler}}); +} + +CVK_TEST(DescriptorSet, write_one_descriptor_and_the_whole_buffer) { + test::Context const context{}; + DescriptorSetLayout const layout{context.core, buffer_and_texture(), create}; + DescriptorPool const pool{context.core, DescriptorPool::Config::ForLayouts({{&layout}}), create}; + DescriptorSet const set{pool, layout, create}; + + BaseBuffer const buffer{context.core, {.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT}, 64}; + + set.write({0, buffer}); +} + +CVK_TEST(DescriptorSet, variable_count) { + test::Context const context{}; + DescriptorSetLayout const layout{context.core, bindless(), create}; + DescriptorPool const pool{context.core, DescriptorPool::Config::ForLayouts({{&layout, 1, 8}}), create}; + DescriptorSet const set{pool, layout, 8, create}; + + Image const image{context.core, TEXTURE_CONFIG, VkExtent2D{4, 4}}; + ImageView const view{image, {}, create}; + Sampler const sampler{context.core, {}, create}; + + EXPECT_EQ(set.variableCount(), 8); + set.write({{0, view, sampler, 6}, {0, view, sampler, 7}}); +} + +CVK_TEST(DescriptorSet, release_and_wrap) { + test::Context const context{}; + DescriptorSetLayout const layout{context.core, buffer_and_texture(), create}; + DescriptorPool const pool{ + context.core, + DescriptorPool::Config::ForLayouts({{&layout}}, VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT), + create + }; + DescriptorSet set{pool, layout, create}; + auto const vkSet = set.get(); + + auto const state = set.release(); + EXPECT_FALSE(set.created()); + EXPECT_TRUE(pool.unused()); + + DescriptorSet const wrapped{pool, layout, 0, state}; + EXPECT_EQ(wrapped.get(), vkSet); + EXPECT_FALSE(pool.unused()); +} +} diff --git a/cvk/tests/src/data/img/image_test.cpp b/cvk/tests/src/data/img/image_test.cpp new file mode 100644 index 00000000..aadc2bb8 --- /dev/null +++ b/cvk/tests/src/data/img/image_test.cpp @@ -0,0 +1,404 @@ +#include "context.hpp" +#include "test.hpp" + +#include "cvk/data/buffer/base_buffer.hpp" +#include "cvk/data/buffer/buffer.hpp" +#include "cvk/data/img/image.hpp" +#include "cvk/render/sync/barrier.hpp" + +#include +#include +#include +#include +#include + + +namespace cvk { +namespace { + using test::READBACK_CONFIG; + using test::STAGING_CONFIG; + + constexpr VkExtent2D EXTENT{64, 32}; + + /** + * @brief sampled texture with a full mip chain + */ + constexpr Image::Config TEXTURE_CONFIG{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .format = VK_FORMAT_R8G8B8A8_UNORM, + .usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT, + .mipLevels = mip_level_count(EXTENT), + }; + + /** + * @brief image that the gpu copies into and out of, two mip levels + */ + constexpr Image::Config COPY_CONFIG{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .format = VK_FORMAT_R8G8B8A8_UNORM, + .usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT, + .mipLevels = 2, + }; + + /** + * @brief image that fills its mip chain with blits + */ + constexpr Image::Config BLIT_CONFIG{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .format = VK_FORMAT_R8G8B8A8_UNORM, + .usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT, + .mipLevels = mip_level_count(EXTENT), + }; +} + +CVK_TEST(Image, mip_level_count) { + static_assert(mip_level_count({1, 1}) == 1); + static_assert(mip_level_count({2, 1}) == 2); + static_assert(mip_level_count({256, 256}) == 9); + static_assert(mip_level_count({300, 200}) == 9); + static_assert(mip_level_count({1, 1024}) == 11); +} + +CVK_TEST(Image, mip_extent) { + static_assert(mip_extent({1, 1}, 0) == VkExtent2D{1, 1}); + static_assert(mip_extent({1, 1}, 3) == VkExtent2D{1, 1}); + static_assert(mip_extent({256, 256}, 0) == VkExtent2D{256, 256}); + static_assert(mip_extent({256, 256}, 8) == VkExtent2D{1, 1}); + static_assert(mip_extent({300, 200}, 1) == VkExtent2D{150, 100}); + static_assert(mip_extent({300, 200}, 3) == VkExtent2D{37, 25}); + static_assert(mip_extent({1, 1024}, 4) == VkExtent2D{1, 64}); + static_assert(mip_extent({1024, 4}, 4) == VkExtent2D{64, 1}); + + // levels beyond the count, also beyond the bit count of uint32_t + static_assert(mip_extent({300, 200}, mip_level_count({300, 200})) == VkExtent2D{1, 1}); + static_assert(mip_extent({4096, 4096}, 32) == VkExtent2D{1, 1}); + static_assert(mip_extent({4096, 4096}, 100) == VkExtent2D{1, 1}); +} + +CVK_TEST(Image, format_aspects) { + static_assert(format_aspects(VK_FORMAT_R8G8B8A8_UNORM) == VK_IMAGE_ASPECT_COLOR_BIT); + static_assert(format_aspects(VK_FORMAT_D32_SFLOAT) == VK_IMAGE_ASPECT_DEPTH_BIT); + static_assert(format_aspects(VK_FORMAT_S8_UINT) == VK_IMAGE_ASPECT_STENCIL_BIT); + static_assert( + format_aspects(VK_FORMAT_D24_UNORM_S8_UINT) == (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT) + ); +} + +CVK_TEST(Image, config_aspect_mask_fits_the_format) { + static_assert([] { + Image::Config const config{.format = VK_FORMAT_D24_UNORM_S8_UINT, .usage = VK_IMAGE_USAGE_SAMPLED_BIT}; + Image::Config::debug_check(config); + return true; + }()); + constexpr auto depthBuffer = Image::Config::DepthBuffer(VK_FORMAT_D24_UNORM_S8_UINT, VK_SAMPLE_COUNT_1_BIT); + static_assert((Image::Config::debug_check(depthBuffer), true)); +} + +CVK_TEST(Image, aspect_mask_comes_from_the_format) { + test::Context const context{}; + + Image const color{context.core, {.format = VK_FORMAT_R8G8B8A8_UNORM, .usage = VK_IMAGE_USAGE_SAMPLED_BIT}}; + Image const depth{ + context.core, + {.format = VK_FORMAT_D16_UNORM, .usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT} + }; + + EXPECT_EQ(color.aspectMask(), VK_IMAGE_ASPECT_COLOR_BIT); + EXPECT_EQ(depth.aspectMask(), VK_IMAGE_ASPECT_DEPTH_BIT); +} + +CVK_TEST(Image, config_create_info) { + constexpr auto info = TEXTURE_CONFIG.createInfo(EXTENT); + + static_assert(info.imageType == VK_IMAGE_TYPE_2D); + static_assert(info.extent.width == EXTENT.width && info.extent.height == EXTENT.height && info.extent.depth == 1); + static_assert(info.mipLevels == 7); + static_assert(info.arrayLayers == 1); + static_assert(info.sharingMode == VK_SHARING_MODE_EXCLUSIVE); + static_assert(info.initialLayout == VK_IMAGE_LAYOUT_UNDEFINED); +} + +CVK_TEST(Image, config_queue_families_are_concurrent) { + static_assert([] { + auto config = TEXTURE_CONFIG; + config.queueFamilies = {0, 1}; + auto const info = config.createInfo(EXTENT); + return info.sharingMode == VK_SHARING_MODE_CONCURRENT && info.queueFamilyIndexCount == 2; + }()); +} + +CVK_TEST(Image, config_depth_buffer) { + constexpr auto config = Image::Config::DepthBuffer(VK_FORMAT_D16_UNORM, VK_SAMPLE_COUNT_1_BIT); + + static_assert(config.aspectMask == VK_IMAGE_ASPECT_DEPTH_BIT); + static_assert(config.usage == VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT); + static_assert(config.memory.autoUsage()); + static_assert((Image::Config::debug_check(config), true)); +} + +CVK_TEST(Image, config_cube_map) { + constexpr auto config = Image::Config::CubeMap(VK_FORMAT_R8G8B8A8_UNORM, 3); + static_assert(config.arrayLayers == 6); + static_assert((config.flags & VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT) != 0); + static_assert((config.usage & VK_IMAGE_USAGE_TRANSFER_SRC_BIT) != 0); + static_assert((Image::Config::CubeMap(VK_FORMAT_R8G8B8A8_UNORM).usage & VK_IMAGE_USAGE_TRANSFER_SRC_BIT) == 0); + static_assert(config.createInfo({8, 8}).arrayLayers == 6); + static_assert((Image::Config::debug_check(config), true)); +} + +CVK_TEST(Image, base_constructor_does_not_create) { + test::Context const context{}; + + Image const image{context.core, TEXTURE_CONFIG}; + + EXPECT_FALSE(image.created()); + EXPECT_EQ(image.memory(), nullptr); +} + +CVK_TEST(Image, create_and_destroy) { + test::Context const context{}; + Image image{context.core, TEXTURE_CONFIG, EXTENT}; + + ASSERT_TRUE(image.created()); + EXPECT_EQ(&image.core(), &context.core); + EXPECT_EQ(image.extent().width, EXTENT.width); + EXPECT_EQ(image.extent().height, EXTENT.height); + EXPECT_EQ(image.mipLevels(), 7); + EXPECT_NE(image.memory(), nullptr); + + image.destroy(); + EXPECT_FALSE(image.created()); + EXPECT_EQ(image.memory(), nullptr); + EXPECT_EQ(image.extent().width, 0); +} + +CVK_TEST(Image, create_depth_buffer) { + test::Context const context{}; + + Image const image{context.core, Image::Config::DepthBuffer(VK_FORMAT_D16_UNORM, VK_SAMPLE_COUNT_1_BIT), EXTENT}; + + EXPECT_TRUE(image.created()); + EXPECT_EQ(image.aspectMask(), VK_IMAGE_ASPECT_DEPTH_BIT); +} + +CVK_TEST(Image, copy_from_a_buffer_into_two_mip_levels) { + test::Context const context{}; + Image const image{context.core, COPY_CONFIG, EXTENT}; + + size_t const mip0 = size_t{EXTENT.width} * EXTENT.height * 4; + size_t const mip1 = mip0 / 4; + BaseBuffer const staging{context.core, STAGING_CONFIG, mip0 + mip1}; + BaseBuffer const readback{context.core, READBACK_CONFIG, mip0 + mip1}; + + std::vector pixels(mip0 + mip1); + for(size_t i = 0; i < pixels.size(); ++i) + pixels[i] = static_cast(i % 251); + staging.write(pixels); + + test::record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { + Barrier toDst{}; + toDst.addTransition(image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED); + toDst.execute(primary); + + image.copy(primary, staging, 0, 0); + image.copy(primary, staging, mip0, 1); + + Barrier toSrc{}; + toSrc.addTransition(image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + toSrc.execute(primary); + test::copy_to_buffer(context, primary, image, readback, 0, 0); + test::copy_to_buffer(context, primary, image, readback, mip0, 1); + test::host_read_barrier(context, primary); + }); + + auto const mapped = readback.map({.size = pixels.size()}); + readback.invalidate(); + EXPECT_TRUE(std::ranges::equal(mapped, pixels)); +} + +CVK_TEST(Image, copy_two_mip_levels_to_a_buffer) { + test::Context const context{}; + Image const image{context.core, COPY_CONFIG, EXTENT}; + + size_t const mip0 = size_t{EXTENT.width} * EXTENT.height * 4; + size_t const mip1 = mip0 / 4; + BaseBuffer const staging{context.core, STAGING_CONFIG, mip0 + mip1}; + BaseBuffer const readback{context.core, READBACK_CONFIG, mip0 + mip1}; + + std::vector pixels(mip0 + mip1); + for(size_t i = 0; i < pixels.size(); ++i) + pixels[i] = static_cast(i % 251); + staging.write(pixels); + + test::record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { + Barrier toDst{}; + toDst.addTransition(image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED); + toDst.execute(primary); + + image.copy(primary, staging, 0, 0); + image.copy(primary, staging, mip0, 1); + + Barrier toSrc{}; + toSrc.addTransition(image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + toSrc.execute(primary); + image.copyTo(primary, readback, 0, 0); + readback.copy(primary, image, mip0, 1); + test::host_read_barrier(context, primary); + }); + + auto const mapped = readback.map({.size = pixels.size()}); + readback.invalidate(); + EXPECT_TRUE(std::ranges::equal(mapped, pixels)); +} + +CVK_TEST(Image, typed_buffer_copies_a_mip_level_in_elements) { + test::Context const context{}; + Image const image{context.core, COPY_CONFIG, EXTENT}; + + // one R8G8B8A8 texel per element + size_t const mip1 = size_t{EXTENT.width / 2} * (EXTENT.height / 2); + BaseBuffer const staging{context.core, STAGING_CONFIG, mip1 * sizeof(uint32_t)}; + Buffer const readback{context.core, READBACK_CONFIG, mip1 + 1}; + + std::vector texels(mip1); + for(size_t i = 0; i < texels.size(); ++i) + texels[i] = static_cast(i * 2654435761u); + staging.write(std::as_bytes(std::span{texels})); + + test::record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { + Barrier toDst{}; + toDst.addTransition(image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED); + toDst.execute(primary); + image.copy(primary, staging, 0, 1); + + Barrier toSrc{}; + toSrc.addTransition(image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + toSrc.execute(primary); + readback.copy(primary, image, 1, 1); + test::host_read_barrier(context, primary); + }); + + auto const mapped = readback.map({.offset = 1}); + readback.invalidate(); + EXPECT_TRUE(std::ranges::equal(mapped, texels)); +} + +CVK_TEST(Image, blit_mip_levels_fills_the_mip_chain) { + test::Context const context{}; + Image const image{context.core, BLIT_CONFIG, EXTENT}; + + // one color: linear blits keep it in every level + constexpr uint32_t color = 0x80'40'20'10; + std::vector const texels(size_t{EXTENT.width} * EXTENT.height, color); + BaseBuffer const staging{context.core, STAGING_CONFIG, texels.size() * sizeof(uint32_t)}; + staging.write(std::as_bytes(std::span{texels})); + + uint32_t const last = BLIT_CONFIG.mipLevels - 1; + size_t const level2 = size_t{EXTENT.width >> 2} * (EXTENT.height >> 2); + Buffer const readback{context.core, READBACK_CONFIG, level2 + 1}; + + test::record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { + Barrier toDst{}; + toDst.addTransition(image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED); + toDst.execute(primary); + image.copy(primary, staging); + + image.blitMipLevels(primary); + + // every level is in VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL + readback.copy(primary, image, 0, 2); + readback.copy(primary, image, level2, last); + test::host_read_barrier(context, primary); + }); + + auto const mapped = readback.map(); + readback.invalidate(); + EXPECT_TRUE(std::ranges::all_of(mapped, [](uint32_t texel) { return texel == color; })); +} + +CVK_TEST(Image, cube_map_copies_and_blits_every_face) { + test::Context const context{}; + constexpr VkExtent2D extent{8, 8}; + Image const image{ + context.core, + Image::Config::CubeMap(VK_FORMAT_R8G8B8A8_UNORM, mip_level_count(extent)), + extent + }; + + // one color per face, linear blits keep it, the faces follow each other in the buffer + auto const color = [](uint32_t face) { return 0x01'01'01'01u * (face + 1) * 30; }; + size_t const faceTexels = size_t{extent.width} * extent.height; + std::vector texels(6 * faceTexels); + for(uint32_t face = 0; face < 6; ++face) + std::ranges::fill(std::span{texels}.subspan(face * faceTexels, faceTexels), color(face)); + + BaseBuffer const staging{context.core, STAGING_CONFIG, texels.size() * sizeof(uint32_t)}; + staging.write(std::as_bytes(std::span{texels})); + Buffer const readback{context.core, READBACK_CONFIG, 6}; + + test::record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { + Barrier toDst{}; + toDst.addTransition(image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED); + toDst.execute(primary); + + image.copy(primary, staging); + image.blitMipLevels(primary); + + // the last level has one texel per face + image.copyTo(primary, readback, 0, image.mipLevels() - 1); + test::host_read_barrier(context, primary); + }); + + auto const mapped = readback.map(); + readback.invalidate(); + for(uint32_t face = 0; face < 6; ++face) + EXPECT_EQ(mapped[face], color(face)); +} + +CVK_TEST(Image, release_and_wrap) { + test::Context const context{}; + Image image{context.core, TEXTURE_CONFIG, EXTENT}; + auto const vkImage = image.get(); + auto const vmaAllocation = image.memory()->get(); + + auto const state = image.release(); + EXPECT_FALSE(image.created()); + EXPECT_EQ(image.memory(), nullptr); + ASSERT_TRUE(state.memory.has_value()); + + Image const wrapped{context.core, TEXTURE_CONFIG, state}; + EXPECT_EQ(wrapped.get(), vkImage); + EXPECT_EQ(wrapped.memory()->get(), vmaAllocation); + EXPECT_EQ(wrapped.extent().width, EXTENT.width); +} + +CVK_TEST(Image, wrap_without_memory_destroys_only_the_image) { + test::Context const context{}; + Image image{context.core, TEXTURE_CONFIG, EXTENT}; + auto state = image.release(); + + // another owner frees the memory, after the image, like the swapchain + Memory const memory{context.core, TEXTURE_CONFIG.memory, *state.memory}; + state.memory = std::nullopt; + + { + Image const wrapped{context.core, TEXTURE_CONFIG, state}; + EXPECT_EQ(wrapped.memory(), nullptr); + } + + EXPECT_TRUE(memory.created()); +} + +CVK_TEST(Image, move_assignment_destroys_the_old_image) { + test::Context const context{}; + Image image{context.core, TEXTURE_CONFIG, EXTENT}; + Image other{context.core, TEXTURE_CONFIG, EXTENT}; + auto const vkImage = other.get(); + + image = std::move(other); + + EXPECT_EQ(image.get(), vkImage); + EXPECT_NE(image.memory(), nullptr); + EXPECT_FALSE(other.created()); +} +} diff --git a/cvk/tests/src/data/img/image_view_test.cpp b/cvk/tests/src/data/img/image_view_test.cpp new file mode 100644 index 00000000..d6990fb6 --- /dev/null +++ b/cvk/tests/src/data/img/image_view_test.cpp @@ -0,0 +1,125 @@ +#include "context.hpp" +#include "test.hpp" + +#include "cvk/data/img/image.hpp" +#include "cvk/data/img/image_view.hpp" + + +namespace cvk { +namespace { + constexpr VkExtent2D EXTENT{32, 32}; + + constexpr Image::Config TEXTURE_CONFIG{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .format = VK_FORMAT_R8G8B8A8_UNORM, + .usage = VK_IMAGE_USAGE_SAMPLED_BIT, + .mipLevels = mip_level_count(EXTENT), + }; +} + +CVK_TEST(ImageView, config_range) { + constexpr ImageView::Config config{.baseMipLevel = 2}; + constexpr auto range = config.range(VK_IMAGE_ASPECT_COLOR_BIT); + + static_assert(range.aspectMask == VK_IMAGE_ASPECT_COLOR_BIT); + static_assert(range.baseMipLevel == 2); + static_assert(range.levelCount == VK_REMAINING_MIP_LEVELS); + static_assert(range.baseArrayLayer == 0); + static_assert(range.layerCount == VK_REMAINING_ARRAY_LAYERS); + + constexpr ImageView::Config depth{.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT}; + static_assert(depth.range(VK_IMAGE_ASPECT_COLOR_BIT).aspectMask == VK_IMAGE_ASPECT_DEPTH_BIT); +} + +CVK_TEST(ImageView, view_type_follows_the_layers) { + test::Context const context{}; + Image const flat{context.core, TEXTURE_CONFIG, EXTENT}; + auto layeredConfig = TEXTURE_CONFIG; + layeredConfig.arrayLayers = 4; + Image const layered{context.core, layeredConfig, EXTENT}; + Image const cube{context.core, Image::Config::CubeMap(VK_FORMAT_R8G8B8A8_UNORM), EXTENT}; + + ImageView const flatView{flat, {}, create}; + ImageView const arrayView{layered, {}, create}; + ImageView const cubeView{cube, {}, create}; + ImageView const faceView{cube, {.baseArrayLayer = 2, .arrayLayers = 1}, create}; + ImageView const facesView{cube, {.viewType = VK_IMAGE_VIEW_TYPE_2D_ARRAY}, create}; + + EXPECT_EQ(flatView.viewType(), VK_IMAGE_VIEW_TYPE_2D); + EXPECT_EQ(arrayView.viewType(), VK_IMAGE_VIEW_TYPE_2D_ARRAY); + EXPECT_EQ(arrayView.arrayLayers(), 4); + EXPECT_EQ(cubeView.viewType(), VK_IMAGE_VIEW_TYPE_CUBE); + EXPECT_EQ(faceView.viewType(), VK_IMAGE_VIEW_TYPE_2D); + EXPECT_EQ(facesView.viewType(), VK_IMAGE_VIEW_TYPE_2D_ARRAY); +} + +CVK_TEST(ImageView, base_constructor_does_not_create) { + test::Context const context{}; + Image const image{context.core, TEXTURE_CONFIG, EXTENT}; + + ImageView const view{image, {}}; + + EXPECT_FALSE(view.created()); + EXPECT_EQ(&view.image(), &image); + EXPECT_EQ(&view.core(), &context.core); +} + +CVK_TEST(ImageView, create_and_destroy) { + test::Context const context{}; + Image const image{context.core, TEXTURE_CONFIG, EXTENT}; + ImageView view{image, {}, create}; + + ASSERT_TRUE(view.created()); + EXPECT_EQ(view.range().levelCount, VK_REMAINING_MIP_LEVELS); + + view.destroy(); + EXPECT_FALSE(view.created()); +} + +CVK_TEST(ImageView, create_on_a_mip_range) { + test::Context const context{}; + Image const image{context.core, TEXTURE_CONFIG, EXTENT}; + + ImageView const view{image, {.baseMipLevel = 1, .mipLevels = 2}, create}; + + EXPECT_TRUE(view.created()); + EXPECT_EQ(view.range().baseMipLevel, 1); + EXPECT_EQ(view.range().levelCount, 2); +} + +CVK_TEST(ImageView, depth_view_takes_the_aspect_of_the_image) { + test::Context const context{}; + Image const image{context.core, Image::Config::DepthBuffer(VK_FORMAT_D16_UNORM, VK_SAMPLE_COUNT_1_BIT), EXTENT}; + + ImageView const view{image, {}, create}; + + EXPECT_TRUE(view.created()); + EXPECT_EQ(view.range().aspectMask, VK_IMAGE_ASPECT_DEPTH_BIT); +} + +CVK_TEST(ImageView, release_and_wrap) { + test::Context const context{}; + Image const image{context.core, TEXTURE_CONFIG, EXTENT}; + ImageView view{image, {}, create}; + auto const vkView = view.get(); + + auto const state = view.release(); + EXPECT_FALSE(view.created()); + + ImageView const wrapped{image, {}, state}; + EXPECT_EQ(wrapped.get(), vkView); +} + +CVK_TEST(ImageView, move_assignment_destroys_the_old_view) { + test::Context const context{}; + Image const image{context.core, TEXTURE_CONFIG, EXTENT}; + ImageView view{image, {}, create}; + ImageView other{image, {.baseMipLevel = 1}, create}; + auto const vkView = other.get(); + + view = std::move(other); + + EXPECT_EQ(view.get(), vkView); + EXPECT_FALSE(other.created()); +} +} diff --git a/cvk/tests/src/data/img/sampler_test.cpp b/cvk/tests/src/data/img/sampler_test.cpp new file mode 100644 index 00000000..654d78bd --- /dev/null +++ b/cvk/tests/src/data/img/sampler_test.cpp @@ -0,0 +1,82 @@ +#include "context.hpp" +#include "test.hpp" + +#include "cvk/base/physical_device.hpp" +#include "cvk/data/img/sampler.hpp" + + +namespace cvk { +CVK_TEST(Sampler, config_create_info) { + Sampler::Config config{}; + + auto info = config.createInfo(); + EXPECT_EQ(info.anisotropyEnable, VK_TRUE); + EXPECT_EQ(info.compareEnable, VK_FALSE); + + config.maxAnisotropy = 0.f; + config.compareOp = VK_COMPARE_OP_NEVER; + info = config.createInfo(); + EXPECT_EQ(info.anisotropyEnable, VK_FALSE); + EXPECT_EQ(info.compareEnable, VK_TRUE); + EXPECT_EQ(info.compareOp, VK_COMPARE_OP_NEVER); +} + +CVK_TEST(Sampler, config_create_info_is_constexpr) { + static_assert(Sampler::Config{}.createInfo().anisotropyEnable == VK_TRUE); + static_assert(Sampler::Config{.compareOp = VK_COMPARE_OP_LESS}.createInfo().compareOp == VK_COMPARE_OP_LESS); +} + +CVK_TEST(Sampler, base_constructor_clamps_anisotropy) { + test::Context const context{}; + auto const limit = context.core.physicalDevice().limits().maxSamplerAnisotropy; + + Sampler const sampler{context.core, Sampler::Config{.maxAnisotropy = limit * 2}}; + + EXPECT_FALSE(sampler.created()); + EXPECT_EQ(sampler.config().maxAnisotropy, limit); +} + +CVK_TEST(Sampler, create_default) { + test::Context const context{}; + + Sampler const sampler{context.core, {}, create}; + + EXPECT_TRUE(sampler.created()); +} + +CVK_TEST(Sampler, create_compare_without_anisotropy) { + test::Context const context{}; + + Sampler const sampler{ + context.core, + Sampler::Config{.maxAnisotropy = 0.f, .compareOp = VK_COMPARE_OP_LESS_OR_EQUAL}, + create + }; + + EXPECT_TRUE(sampler.created()); +} + +CVK_TEST(Sampler, release_and_wrap) { + test::Context const context{}; + Sampler sampler{context.core, {}, create}; + auto const vkSampler = sampler.get(); + + auto const state = sampler.release(); + EXPECT_FALSE(sampler.created()); + + Sampler const wrapped{context.core, {}, state}; + EXPECT_EQ(wrapped.get(), vkSampler); +} + +CVK_TEST(Sampler, move_assignment_destroys_the_old_sampler) { + test::Context const context{}; + Sampler sampler{context.core, {}, create}; + Sampler other{context.core, {}, create}; + auto const vkSampler = other.get(); + + sampler = std::move(other); + + EXPECT_EQ(sampler.get(), vkSampler); + EXPECT_FALSE(other.created()); +} +} diff --git a/cvk/tests/src/data/memory/memory_test.cpp b/cvk/tests/src/data/memory/memory_test.cpp new file mode 100644 index 00000000..a4b4969d --- /dev/null +++ b/cvk/tests/src/data/memory/memory_test.cpp @@ -0,0 +1,195 @@ +#include "context.hpp" +#include "test.hpp" + +#include "cvk/data/memory/memory.hpp" + +#include +#include +#include +#include +#include + + +namespace cvk { +namespace { + constexpr size_t SIZE = 1024; + + constexpr VkMemoryRequirements REQUIREMENTS{.size = SIZE, .alignment = 16, .memoryTypeBits = ~0u}; + + /** + * @brief host visible memory, memory without a buffer or an image cannot use VMA_MEMORY_USAGE_AUTO + */ + constexpr Memory::Config HOST_CONFIG{ + .usage = VMA_MEMORY_USAGE_UNKNOWN, + .requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, + }; +} + +CVK_TEST(Memory, config_create_info) { + constexpr Memory::Config config{ + .flags = VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT, + .usage = VMA_MEMORY_USAGE_UNKNOWN, + .requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, + .preferredFlags = VK_MEMORY_PROPERTY_HOST_CACHED_BIT, + }; + + constexpr auto info = config.createInfo(); + + EXPECT_EQ(info.flags, VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT); + EXPECT_EQ(info.usage, VMA_MEMORY_USAGE_UNKNOWN); + EXPECT_EQ(info.requiredFlags, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT); + EXPECT_EQ(info.preferredFlags, VK_MEMORY_PROPERTY_HOST_CACHED_BIT); + EXPECT_EQ(info.pool, VK_NULL_HANDLE); +} + +CVK_TEST(Memory, config_auto_usage) { + static_assert(Memory::Config{}.autoUsage()); + static_assert(Memory::Config{.usage = VMA_MEMORY_USAGE_AUTO_PREFER_HOST}.autoUsage()); + static_assert(!HOST_CONFIG.autoUsage()); +} + +CVK_TEST(Memory, base_constructor_does_not_create) { + test::Context const context{}; + + Memory const memory{context.core, HOST_CONFIG}; + + EXPECT_FALSE(memory.created()); +} + +CVK_TEST(Memory, create_and_destroy) { + test::Context const context{}; + Memory memory{context.core, HOST_CONFIG, REQUIREMENTS}; + + ASSERT_TRUE(memory.created()); + EXPECT_EQ(&memory.core(), &context.core); + EXPECT_NE(memory.info().deviceMemory, VK_NULL_HANDLE); + EXPECT_GE(memory.size(), SIZE); + + memory.destroy(); + EXPECT_FALSE(memory.created()); +} + +CVK_TEST(Memory, properties_have_the_required_flags) { + test::Context const context{}; + Memory const memory{context.core, HOST_CONFIG, REQUIREMENTS}; + + auto const properties = memory.properties(); + + EXPECT_EQ(properties & HOST_CONFIG.requiredFlags, HOST_CONFIG.requiredFlags); +} + +CVK_TEST(Memory, map_write_and_read) { + test::Context const context{}; + Memory const memory{context.core, HOST_CONFIG, REQUIREMENTS}; + + { + auto const written = memory.map(); + ASSERT_EQ(written.size(), memory.size()); + std::ranges::fill(written, std::byte{'x'}); + memory.flush(); + } + + auto const read = memory.map(); + memory.invalidate(); + EXPECT_TRUE(std::ranges::all_of(read, [](std::byte b) { return b == std::byte{'x'}; })); +} + +CVK_TEST(Memory, map_a_range) { + test::Context const context{}; + Memory const memory{context.core, HOST_CONFIG, REQUIREMENTS}; + + auto const whole = memory.map(); + auto const part = memory.map({.offset = 32, .size = 16}); + auto const end = memory.map({.offset = 32}); + + EXPECT_EQ(part.data(), whole.data() + 32); + EXPECT_EQ(part.size(), 16); + EXPECT_EQ(end.size(), memory.size() - 32); +} + +CVK_TEST(Memory, write_copies_the_data) { + test::Context const context{}; + Memory const memory{context.core, HOST_CONFIG, REQUIREMENTS}; + std::array data{}; + std::ranges::fill(data, std::byte{'y'}); + + memory.write(data, 32); + + auto const mapped = memory.map({.offset = 32, .size = data.size()}); + EXPECT_TRUE(std::ranges::equal(mapped, data)); +} + +CVK_TEST(Memory, the_mapping_unmaps) { + test::Context const context{}; + Memory const memory{context.core, HOST_CONFIG, REQUIREMENTS}; + + EXPECT_TRUE(memory.mapped().empty()); + { + auto const mapping = memory.map(); + EXPECT_EQ(memory.mapped().data(), mapping.data()); + EXPECT_EQ(memory.mapped().size(), memory.size()); + } + EXPECT_TRUE(memory.mapped().empty()); +} + +CVK_TEST(Memory, a_moved_mapping_unmaps_once) { + test::Context const context{}; + Memory const memory{context.core, HOST_CONFIG, REQUIREMENTS}; + + { + auto mapping = memory.map(); + Mapping const moved{std::move(mapping)}; + EXPECT_FALSE(memory.mapped().empty()); + } + EXPECT_TRUE(memory.mapped().empty()); +} + +CVK_TEST(Memory, mapping_as_elements) { + test::Context const context{}; + Memory const memory{context.core, HOST_CONFIG, REQUIREMENTS}; + + { + auto bytes = memory.map({.offset = 0, .size = 64}); + auto const* data = bytes.data(); + auto const elements = std::move(bytes).as(); + + EXPECT_EQ(elements.size(), 16); + EXPECT_EQ(static_cast(elements.data()), static_cast(data)); + } + EXPECT_TRUE(memory.mapped().empty()); +} + +CVK_TEST(Memory, mapped_bit_maps_persistently) { + test::Context const context{}; + auto config = HOST_CONFIG; + config.flags |= VMA_ALLOCATION_CREATE_MAPPED_BIT; + + Memory const memory{context.core, config, REQUIREMENTS}; + + EXPECT_FALSE(memory.mapped().empty()); +} + +CVK_TEST(Memory, release_and_wrap) { + test::Context const context{}; + Memory memory{context.core, HOST_CONFIG, REQUIREMENTS}; + auto const vmaAllocation = memory.get(); + + auto const state = memory.release(); + EXPECT_FALSE(memory.created()); + + Memory const wrapped{context.core, HOST_CONFIG, state}; + EXPECT_EQ(wrapped.get(), vmaAllocation); +} + +CVK_TEST(Memory, move_assignment_destroys_the_old_memory) { + test::Context const context{}; + Memory memory{context.core, HOST_CONFIG, REQUIREMENTS}; + Memory other{context.core, HOST_CONFIG, REQUIREMENTS}; + auto const vmaAllocation = other.get(); + + memory = std::move(other); + + EXPECT_EQ(memory.get(), vmaAllocation); + EXPECT_FALSE(other.created()); +} +} diff --git a/cvk/tests/src/debug/debug_messenger_test.cpp b/cvk/tests/src/debug/debug_messenger_test.cpp new file mode 100644 index 00000000..d7da01d1 --- /dev/null +++ b/cvk/tests/src/debug/debug_messenger_test.cpp @@ -0,0 +1,161 @@ +#include "context.hpp" +#include "test.hpp" + +#include "cvk/base/instance.hpp" +#include "cvk/debug/debug_messenger.hpp" + +#include + + +namespace cvk { +namespace { + constexpr std::string_view TEST_MESSAGE = "cvk test message"; + + size_t received = 0; + + VKAPI_ATTR VkBool32 VKAPI_CALL count_test_messages( + VkDebugUtilsMessageSeverityFlagBitsEXT, + VkDebugUtilsMessageTypeFlagsEXT, + VkDebugUtilsMessengerCallbackDataEXT const* callbackData, + void* + ) { + if(callbackData->pMessage == TEST_MESSAGE) ++received; + return VK_FALSE; + } + + void submit_test_message(Instance const& instance) { + VkDebugUtilsMessengerCallbackDataEXT const data{ + .sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CALLBACK_DATA_EXT, + .pNext = nullptr, + .flags = 0, + .pMessageIdName = nullptr, + .messageIdNumber = 0, + .pMessage = TEST_MESSAGE.data(), + .queueLabelCount = 0, + .pQueueLabels = nullptr, + .cmdBufLabelCount = 0, + .pCmdBufLabels = nullptr, + .objectCount = 0, + .pObjects = nullptr, + }; + + vkSubmitDebugUtilsMessageEXT( + instance.get(), + VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT, + VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT, + &data + ); + } +} + +CVK_TEST(DebugMessenger, default_config_create_info) { + constexpr auto info = DebugMessenger::Config{}.createInfo(); + + static_assert(info.sType == VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT); + static_assert(info.pfnUserCallback == &default_debug_callback); + static_assert(info.messageSeverity == constants::DEBUG_MESSAGE_SEVERITY); + static_assert(info.messageType == constants::DEBUG_MESSAGE_TYPE); +} + +CVK_TEST(DebugMessenger, forwards_messages_to_the_callback) { + if constexpr(!constants::ENABLE_VALIDATION_LAYERS) GTEST_SKIP() << "needs the validation layers"; + + test::Context const context{}; + DebugMessenger const messenger{context.instance, DebugMessenger::Config{.callback = count_test_messages}, create}; + ASSERT_TRUE(messenger.created()); + + received = 0; + submit_test_message(context.instance); + + EXPECT_EQ(received, 1); +} + +CVK_TEST(DebugMessenger, accepts_a_lambda) { + if constexpr(!constants::ENABLE_VALIDATION_LAYERS) GTEST_SKIP() << "needs the validation layers"; + + test::Context const context{}; + + DebugMessenger::callback_t const callback = []( + VkDebugUtilsMessageSeverityFlagBitsEXT, + VkDebugUtilsMessageTypeFlagsEXT, + VkDebugUtilsMessengerCallbackDataEXT const* callbackData, + void* + ) -> VkBool32 { + if(callbackData->pMessage == TEST_MESSAGE) ++received; + return VK_FALSE; + }; + DebugMessenger const messenger{context.instance, DebugMessenger::Config{.callback = callback}, create}; + + received = 0; + submit_test_message(context.instance); + + EXPECT_EQ(received, 1); +} + +CVK_TEST(DebugMessenger, destroy_stops_the_messages) { + if constexpr(!constants::ENABLE_VALIDATION_LAYERS) GTEST_SKIP() << "needs the validation layers"; + + test::Context const context{}; + DebugMessenger messenger{context.instance, DebugMessenger::Config{.callback = count_test_messages}, create}; + + messenger.destroy(); + EXPECT_FALSE(messenger.created()); + + received = 0; + submit_test_message(context.instance); + + EXPECT_EQ(received, 0); +} + +CVK_TEST(DebugMessenger, release_keeps_the_messenger) { + if constexpr(!constants::ENABLE_VALIDATION_LAYERS) GTEST_SKIP() << "needs the validation layers"; + + test::Context const context{}; + DebugMessenger messenger{context.instance, DebugMessenger::Config{.callback = count_test_messages}, create}; + + auto const state = messenger.release(); + EXPECT_FALSE(messenger.created()); + + received = 0; + submit_test_message(context.instance); + EXPECT_EQ(received, 1); + + DebugMessenger::destroy(context.instance.get(), state.vkMessenger.get()); +} + +CVK_TEST(DebugMessenger, release_and_wrap) { + if constexpr(!constants::ENABLE_VALIDATION_LAYERS) GTEST_SKIP() << "needs the validation layers"; + + test::Context const context{}; + DebugMessenger messenger{context.instance, DebugMessenger::Config{.callback = count_test_messages}, create}; + auto const vkMessenger = messenger.get(); + + auto const state = messenger.release(); + DebugMessenger const wrapped{context.instance, messenger.config(), state}; + + ASSERT_TRUE(wrapped.created()); + EXPECT_EQ(wrapped.get(), vkMessenger); + + received = 0; + submit_test_message(context.instance); + EXPECT_EQ(received, 1); +} + +CVK_TEST(DebugMessenger, move_assignment_destroys_the_old_messenger) { + if constexpr(!constants::ENABLE_VALIDATION_LAYERS) GTEST_SKIP() << "needs the validation layers"; + + test::InstanceContext const context{}; + DebugMessenger messenger{context.instance, DebugMessenger::Config{.callback = count_test_messages}, create}; + DebugMessenger other{context.instance, DebugMessenger::Config{.callback = count_test_messages}, create}; + auto const vkMessenger = other.get(); + + messenger = std::move(other); + + EXPECT_EQ(messenger.get(), vkMessenger); + EXPECT_FALSE(other.created()); + + received = 0; + submit_test_message(context.instance); + EXPECT_EQ(received, 1); +} +} diff --git a/cvk/tests/src/environment.cpp b/cvk/tests/src/environment.cpp new file mode 100644 index 00000000..29d66f29 --- /dev/null +++ b/cvk/tests/src/environment.cpp @@ -0,0 +1,30 @@ +#include "context.hpp" + +#include "cvk/base/cvk.hpp" + +#include + +#include + + +namespace cvk::test { +namespace { + /** + * @brief holds the vulkan loader for all tests + * @details the shared contexts are destroyed before the loader + */ + class Environment : public testing::Environment { + public: + void SetUp() override { _cvk.emplace(); } + void TearDown() override { + destroy_shared_contexts(); + _cvk.reset(); + } + + private: + std::optional _cvk{}; + }; + + [[maybe_unused]] testing::Environment* const ENVIRONMENT = testing::AddGlobalTestEnvironment(new Environment{}); +} +} diff --git a/cvk/tests/src/headless.cpp b/cvk/tests/src/headless.cpp new file mode 100644 index 00000000..15f5ea0f --- /dev/null +++ b/cvk/tests/src/headless.cpp @@ -0,0 +1,55 @@ +#include "headless.hpp" + +#include "cvk/utility/constant/device_constants.hpp" + +#include +#include + + +namespace cvk::test { +std::vector headless_extensions() { + std::vector extensions{ + std::from_range, + constants::PRESENT_INSTANCE_EXTENSIONS | std::views::transform( + [](std::string_view extension) { return std::string{extension}; } + ) + }; + extensions.emplace_back(VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME); + return extensions; +} + +Surface headless_surface(Instance const& instance) { + VkHeadlessSurfaceCreateInfoEXT const createInfo{ + .sType = VK_STRUCTURE_TYPE_HEADLESS_SURFACE_CREATE_INFO_EXT, + .pNext = nullptr, + .flags = 0, + }; + + VkSurfaceKHR vkSurface = VK_NULL_HANDLE; + auto const result = vkCreateHeadlessSurfaceEXT(instance.get(), &createInfo, nullptr, &vkSurface); + CTH_STABLE_THROW(result != VK_SUCCESS, "failed to create headless surface") {} + + return Surface{instance, Surface::State{vkSurface}}; +} + +PhysicalDevicePick pick( + Instance const& instance, + std::span queueProperties, + std::span surfaces +) { + auto features = constants::BASE_DEVICE_FEATURES; + std::vector extensions{}; + + bool const present = std::ranges::any_of( + queueProperties, + [](QueueFamilyProperties properties) { return contains(properties, QueueFamilyProperties::PRESENT); } + ); + if(present) { + features.merge(constants::PRESENT_DEVICE_FEATURES); + for(auto const extension : constants::PRESENT_DEVICE_EXTENSIONS) + extensions.emplace_back(extension); + } + + return pick_physical_device(instance, surfaces, queueProperties, features, extensions).value(); +} +} diff --git a/cvk/tests/src/headless.hpp b/cvk/tests/src/headless.hpp new file mode 100644 index 00000000..e92f14e1 --- /dev/null +++ b/cvk/tests/src/headless.hpp @@ -0,0 +1,33 @@ +#pragma once +#include "cvk/base/instance.hpp" +#include "cvk/base/physical_device.hpp" +#include "cvk/surface/surface.hpp" + +#include +#include +#include + + +namespace cvk::test { +/** + * @brief instance extensions for headless surfaces with present support + */ +[[nodiscard]] std::vector headless_extensions(); + +/** + * @brief creates a headless surface + * @pre the instance enables @ref headless_extensions() + */ +[[nodiscard]] Surface headless_surface(Instance const&); + +/** + * @brief @ref pick_physical_device() with @ref constants::BASE_DEVICE_FEATURES + * @details a present queue adds @ref constants::PRESENT_DEVICE_FEATURES and @ref constants::PRESENT_DEVICE_EXTENSIONS + * @throws std::bad_optional_access no device meets the requirements + */ +[[nodiscard]] PhysicalDevicePick pick( + Instance const&, + std::span queueProperties, + std::span surfaces = {} +); +} diff --git a/cvk/tests/src/pch/glob/gtest.hpp b/cvk/tests/src/pch/glob/gtest.hpp new file mode 100644 index 00000000..e469bb31 --- /dev/null +++ b/cvk/tests/src/pch/glob/gtest.hpp @@ -0,0 +1,2 @@ +#pragma once +#include diff --git a/cvk/tests/src/pch/pch.hpp b/cvk/tests/src/pch/pch.hpp new file mode 100644 index 00000000..d97ada7c --- /dev/null +++ b/cvk/tests/src/pch/pch.hpp @@ -0,0 +1,5 @@ +#pragma once +#include "glob/gtest.hpp" + +#include "context.hpp" +#include "test.hpp" diff --git a/cvk/tests/src/render/cmd/cmd_buffer_test.cpp b/cvk/tests/src/render/cmd/cmd_buffer_test.cpp new file mode 100644 index 00000000..53af016f --- /dev/null +++ b/cvk/tests/src/render/cmd/cmd_buffer_test.cpp @@ -0,0 +1,214 @@ +#include "context.hpp" +#include "test.hpp" + +#include "cvk/render/cmd/cmd_buffer.hpp" + +#include +#include +#include +#include +#include + + +namespace cvk { +using test::pool_config; + +CVK_TEST(CmdBuffer, inheritance_rendering_info) { + static_assert([] { + SecondaryCmdBuffer::Inheritance inheritance{ + .colorFormats = {VK_FORMAT_B8G8R8A8_UNORM}, + .depthFormat = VK_FORMAT_D16_UNORM, + .samples = VK_SAMPLE_COUNT_4_BIT, + }; + auto const info = inheritance.renderingInfo(); + return info.colorAttachmentCount == 1 && info.pColorAttachmentFormats == inheritance.colorFormats.data() + && info.depthAttachmentFormat == VK_FORMAT_D16_UNORM && info.rasterizationSamples == VK_SAMPLE_COUNT_4_BIT; + }()); +} + +CVK_TEST(CmdBuffer, push_constant_data) { + static_assert(push_constant_range>); + static_assert(push_constant_range>); + static_assert(push_constant_range>); + static_assert(!push_constant_range>); + + static_assert(push_constant_data); + static_assert(push_constant_data>); + static_assert(!push_constant_data); + static_assert(!push_constant_data>); +} + +CVK_TEST(CmdBuffer, delete_only_through_the_derived_class) { + static_assert(!std::is_destructible_v); + static_assert(std::is_destructible_v); + static_assert(std::is_destructible_v); +} + +CVK_TEST(CmdBuffer, base_constructor_does_not_create) { + test::Context const context{}; + CmdPool const pool{context.core, pool_config(context.core), create}; + + PrimaryCmdBuffer const primary{pool}; + SecondaryCmdBuffer const secondary{pool}; + + EXPECT_FALSE(primary.created()); + EXPECT_FALSE(secondary.created()); + EXPECT_EQ(primary.level(), VK_COMMAND_BUFFER_LEVEL_PRIMARY); + EXPECT_EQ(secondary.level(), VK_COMMAND_BUFFER_LEVEL_SECONDARY); + EXPECT_EQ(&primary.pool(), &pool); +} + +CVK_TEST(CmdBuffer, create_and_destroy) { + test::Context const context{}; + CmdPool const pool{context.core, pool_config(context.core), create}; + PrimaryCmdBuffer primary{pool, create}; + + ASSERT_TRUE(primary.created()); + EXPECT_EQ(&primary.core(), &context.core); + EXPECT_FALSE(primary.recording()); + + primary.destroy(); + EXPECT_FALSE(primary.created()); +} + +CVK_TEST(CmdBuffer, begin_and_end) { + test::Context const context{}; + CmdPool const pool{context.core, pool_config(context.core), create}; + PrimaryCmdBuffer primary{pool, create}; + + primary.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); + EXPECT_TRUE(primary.recording()); + + primary.end(); + EXPECT_FALSE(primary.recording()); +} + +CVK_TEST(CmdBuffer, submit) { + test::Context const context{}; + CmdPool const pool{context.core, pool_config(context.core), create}; + PrimaryCmdBuffer primary{pool, create}; + primary.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); + primary.end(); + + test::submit_and_wait(context, primary); +} + +CVK_TEST(CmdBuffer, reset_and_record_again) { + test::Context const context{}; + CmdPool const pool{context.core, pool_config(context.core), create}; + PrimaryCmdBuffer primary{pool, create}; + primary.begin(); + + primary.reset(); + EXPECT_FALSE(primary.recording()); + + primary.begin(); + primary.end(); + test::submit_and_wait(context, primary); +} + +CVK_TEST(CmdBuffer, execute_secondaries) { + test::Context const context{}; + CmdPool const pool{context.core, pool_config(context.core), create}; + std::array secondaries{ + SecondaryCmdBuffer{pool, create}, + SecondaryCmdBuffer{pool, create}, + }; + for(auto& secondary : secondaries) { + secondary.begin(); + secondary.end(); + } + + PrimaryCmdBuffer primary{pool, create}; + primary.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); + primary.execute(secondaries); + primary.end(); + + test::submit_and_wait(context, primary); +} + +CVK_TEST(CmdBuffer, execute_secondaries_through_pointers) { + test::Context const context{}; + CmdPool const pool{context.core, pool_config(context.core), create}; + // secondaries[1] is not recorded, the iterator pair [&secondaries[0], &secondaries[2]) would execute it + std::array secondaries{ + SecondaryCmdBuffer{pool, create}, + SecondaryCmdBuffer{pool, create}, + SecondaryCmdBuffer{pool, create}, + }; + for(auto* secondary : {&secondaries[0], &secondaries[2]}) { + secondary->begin(); + secondary->end(); + } + + PrimaryCmdBuffer primary{pool, create}; + primary.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); + primary.execute({&secondaries[0], &secondaries[2]}); + primary.end(); + + test::submit_and_wait(context, primary); +} + +CVK_TEST(CmdBuffer, secondary_begin_with_rendering_inheritance) { + test::Context const context{}; + CmdPool const pool{context.core, pool_config(context.core), create}; + SecondaryCmdBuffer secondary{pool, create}; + + secondary.begin( + SecondaryCmdBuffer::Inheritance{ + .colorFormats = {VK_FORMAT_R8G8B8A8_UNORM}, + .depthFormat = VK_FORMAT_D16_UNORM, + } + ); + EXPECT_TRUE(secondary.recording()); + + secondary.end(); +} + +CVK_TEST(CmdBuffer, release_and_wrap) { + test::Context const context{}; + CmdPool const pool{context.core, pool_config(context.core), create}; + PrimaryCmdBuffer primary{pool, create}; + auto const vkBuffer = primary.get(); + + auto const state = primary.release(); + EXPECT_FALSE(primary.created()); + + PrimaryCmdBuffer const wrapped{pool, state}; + EXPECT_EQ(wrapped.get(), vkBuffer); + EXPECT_FALSE(pool.unused()); +} + +CVK_TEST(CmdBuffer, moved_from_buffer_does_not_record) { + test::Context const context{}; + CmdPool const pool{context.core, pool_config(context.core), create}; + PrimaryCmdBuffer primary{pool, create}; + primary.begin(); + + PrimaryCmdBuffer moved{std::move(primary)}; + + EXPECT_FALSE(primary.recording()); + EXPECT_TRUE(moved.recording()); + + primary.create(); + EXPECT_FALSE(primary.recording()); + + moved.end(); +} + +CVK_TEST(CmdBuffer, move_assignment_frees_the_old_buffer) { + test::Context const context{}; + CmdPool const pool{context.core, pool_config(context.core), create}; + PrimaryCmdBuffer primary{pool, create}; + PrimaryCmdBuffer other{pool, create}; + auto const vkBuffer = other.get(); + + primary = std::move(other); + + EXPECT_EQ(primary.get(), vkBuffer); + EXPECT_FALSE(other.created()); + + primary.destroy(); + EXPECT_TRUE(pool.unused()); +} +} diff --git a/cvk/tests/src/render/cmd/cmd_pool_test.cpp b/cvk/tests/src/render/cmd/cmd_pool_test.cpp new file mode 100644 index 00000000..e144d75b --- /dev/null +++ b/cvk/tests/src/render/cmd/cmd_pool_test.cpp @@ -0,0 +1,149 @@ +#include "context.hpp" +#include "test.hpp" + +#include "cvk/render/cmd/cmd_buffer.hpp" +#include "cvk/render/cmd/cmd_pool.hpp" + +#include + + +namespace cvk { +using test::pool_config; + +CVK_TEST(CmdPool, config_create_info) { + constexpr CmdPool::Config config{.queueFamilyIndex = 2, .flags = VK_COMMAND_POOL_CREATE_TRANSIENT_BIT}; + constexpr auto info = config.createInfo(); + + static_assert(info.queueFamilyIndex == 2); + static_assert(info.flags == VK_COMMAND_POOL_CREATE_TRANSIENT_BIT); +} + +CVK_TEST(CmdPool, config_is_an_aggregate_with_defaults) { + static_assert(std::is_aggregate_v); + + constexpr CmdPool::Config empty{}; + static_assert(empty.queueFamilyIndex == VK_QUEUE_FAMILY_IGNORED); + static_assert(empty.flags == CmdPool::Config::DEFAULT_FLAGS); + + constexpr CmdPool::Config withFamily{.queueFamilyIndex = 3}; + static_assert(withFamily.queueFamilyIndex == 3); + static_assert((withFamily.flags & VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT) != 0); +} + +CVK_TEST(CmdPool, base_constructor_does_not_create) { + test::Context const context{}; + + CmdPool const pool{context.core, pool_config(context.core)}; + + EXPECT_FALSE(pool.created()); + EXPECT_TRUE(pool.unused()); +} + +CVK_TEST(CmdPool, create_and_destroy) { + test::Context const context{}; + CmdPool pool{context.core, pool_config(context.core), create}; + + ASSERT_TRUE(pool.created()); + EXPECT_EQ(&pool.core(), &context.core); + + pool.destroy(); + EXPECT_FALSE(pool.created()); +} + +CVK_TEST(CmdPool, counts_its_command_buffers) { + test::Context const context{}; + CmdPool const pool{context.core, pool_config(context.core), create}; + + { + PrimaryCmdBuffer primary{pool, create}; + SecondaryCmdBuffer const secondary{pool, create}; + EXPECT_FALSE(pool.unused()); + + primary.destroy(); + EXPECT_FALSE(pool.unused()); + } + + EXPECT_TRUE(pool.unused()); +} + +CVK_TEST(CmdPool, release_of_a_buffer_stops_the_count) { + test::Context const context{}; + CmdPool const pool{context.core, pool_config(context.core), create}; + PrimaryCmdBuffer primary{pool, create}; + + auto const state = primary.release(); + EXPECT_TRUE(pool.unused()); + + BaseCmdBuffer::destroy(context.core.deviceTable(), pool.get(), state.vkBuffer.get()); +} + +CVK_TEST(CmdPool, owner_of_a_released_buffer_counts_it) { + test::Context const context{}; + CmdPool const pool{context.core, pool_config(context.core), create}; + PrimaryCmdBuffer primary{pool, create}; + + auto const state = primary.release(); + pool.add(); + EXPECT_FALSE(pool.unused()); + + BaseCmdBuffer::destroy(context.core.deviceTable(), pool.get(), state.vkBuffer.get()); + pool.remove(); + EXPECT_TRUE(pool.unused()); +} + +CVK_TEST(CmdPool, reset_returns_the_buffers_to_the_initial_state) { + test::Context const context{}; + CmdPool const pool{context.core, pool_config(context.core), create}; + PrimaryCmdBuffer primary{pool, create}; + primary.begin(); + primary.end(); + + pool.reset(); + EXPECT_EQ(pool.resets(), 1); + + primary.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); + primary.end(); +} + +CVK_TEST(CmdPool, reset_ends_the_recording) { + test::Context const context{}; + CmdPool const pool{context.core, pool_config(context.core), create}; + PrimaryCmdBuffer primary{pool, create}; + SecondaryCmdBuffer secondary{pool, create}; + primary.begin(); + secondary.begin(); + + pool.reset(); + + EXPECT_FALSE(primary.recording()); + EXPECT_FALSE(secondary.recording()); + + primary.begin(); + EXPECT_TRUE(primary.recording()); + primary.end(); +} + +CVK_TEST(CmdPool, release_and_wrap) { + test::Context const context{}; + CmdPool pool{context.core, pool_config(context.core), create}; + auto const vkPool = pool.get(); + + auto const state = pool.release(); + EXPECT_FALSE(pool.created()); + + CmdPool const wrapped{context.core, pool_config(context.core), state}; + EXPECT_EQ(wrapped.get(), vkPool); +} + +CVK_TEST(CmdPool, move_assignment_destroys_the_old_pool) { + test::Context const context{}; + CmdPool pool{context.core, pool_config(context.core), create}; + CmdPool other{context.core, pool_config(context.core), create}; + auto const vkPool = other.get(); + + pool = std::move(other); + + EXPECT_EQ(pool.get(), vkPool); + EXPECT_FALSE(other.created()); +} +} diff --git a/cvk/tests/src/render/cmd/draw_test.cpp b/cvk/tests/src/render/cmd/draw_test.cpp new file mode 100644 index 00000000..c8e43eba --- /dev/null +++ b/cvk/tests/src/render/cmd/draw_test.cpp @@ -0,0 +1,178 @@ +#include "context.hpp" +#include "shaders.hpp" +#include "test.hpp" + +#include "cvk/data/buffer/buffer.hpp" +#include "cvk/data/img/image.hpp" +#include "cvk/data/img/image_view.hpp" +#include "cvk/render/cmd/cmd_buffer.hpp" +#include "cvk/render/pipeline/graphics_pipeline.hpp" +#include "cvk/render/pipeline/layout/pipeline_layout.hpp" +#include "cvk/render/pipeline/shader/shader.hpp" +#include "cvk/render/sync/barrier.hpp" + +#include +#include +#include +#include + + +namespace cvk { +namespace { + using test::GREEN; + using test::host_written; + using test::Vec3; + + constexpr VkExtent2D EXTENT{16, 16}; + constexpr std::array COLOR_FORMATS{VK_FORMAT_R8G8B8A8_UNORM}; + + /** + * @brief pipeline that fills the triangle with the push constant color + */ + struct Fill { + explicit Fill(Core const& core) : layout{ + core, + PipelineLayout::Config{.pushConstantRanges = {{VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(GREEN)}}}, + create + }, + vertex{core, VK_SHADER_STAGE_VERTEX_BIT, test::shaders::FILL_VERT}, + fragment{core, VK_SHADER_STAGE_FRAGMENT_BIT, test::shaders::TRIANGLE_FRAG}, + pipeline{layout, test::fullscreen_config(vertex, fragment, COLOR_FORMATS), create}, + vertices{core, host_written(VK_BUFFER_USAGE_VERTEX_BUFFER_BIT), test::FULLSCREEN.size()} { + vertices.write(test::FULLSCREEN); + } + + PipelineLayout layout; + Shader vertex; + Shader fragment; + GraphicsPipeline pipeline; + Buffer vertices; + }; + + /** + * @brief clears an attachment to black, draws with draw() and expects every pixel green + */ + template + void expect_filled(test::Context const& context, Fill const& fill, Draw draw) { + Image const image{ + context.core, + { + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .format = COLOR_FORMATS[0], + .usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT, + }, + EXTENT + }; + ImageView const view{image, {}, create}; + BaseBuffer const readback{context.core, test::READBACK_CONFIG, size_t{EXTENT.width} * EXTENT.height * 4}; + + test::record_and_submit(context, [&](PrimaryCmdBuffer& cmd) { + Barrier transition{}; + transition.addTransition(image, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED); + transition.execute(cmd); + cmd.beginRendering( + { + .colorAttachments = { + RenderingAttachment{ + .view = &view, + .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR, + .storeOp = VK_ATTACHMENT_STORE_OP_STORE, + .clearValue = {.color = {.float32 = {0.f, 0.f, 0.f, 1.f}}}, + }, + }, + } + ); + + cmd.bindPipeline(fill.pipeline); + cmd.bindVertexBuffer(0, fill.vertices); + // a span pushes its elements, not itself + cmd.pushConstants(fill.layout, VK_SHADER_STAGE_FRAGMENT_BIT, std::span{GREEN}); + draw(cmd); + + cmd.endRendering(); + + Barrier barrier{}; + barrier.addTransition( + image, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL + ); + barrier.execute(cmd); + + test::copy_to_buffer(context, cmd, image, readback, 0, 0); + test::host_read_barrier(context, cmd); + }); + + EXPECT_TRUE(test::every_pixel_is(readback, test::GREEN_PIXEL)); + } +} + +CVK_TEST(Draw, index_type_follows_the_element) { + static_assert(index_type == VK_INDEX_TYPE_UINT16); + static_assert(index_type == VK_INDEX_TYPE_UINT32); + static_assert(!index_element); + static_assert(!index_element); +} + +CVK_TEST(Draw, draw) { + test::Context const context{}; + Fill const fill{context.core}; + + expect_filled(context, fill, [](PrimaryCmdBuffer const& cmd) { cmd.draw(3); }); +} + +CVK_TEST(Draw, draw_indexed) { + test::Context const context{}; + Fill const fill{context.core}; + Buffer indices{context.core, host_written(VK_BUFFER_USAGE_INDEX_BUFFER_BIT), 3}; + indices.write(std::array{0, 1, 2}); + + expect_filled(context, fill, [&](PrimaryCmdBuffer const& cmd) { + cmd.bindIndexBuffer(indices); + cmd.drawIndexed(3); + }); +} + +CVK_TEST(Draw, draw_indirect) { + test::Context const context{}; + Fill const fill{context.core}; + Buffer commands{ + context.core, + host_written(VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT), + 1 + }; + commands.write( + std::array{VkDrawIndirectCommand{.vertexCount = 3, .instanceCount = 1, .firstVertex = 0, .firstInstance = 0}} + ); + + expect_filled(context, fill, [&](PrimaryCmdBuffer const& cmd) { cmd.drawIndirect(commands); }); +} + +CVK_TEST(Draw, draw_indexed_indirect) { + test::Context const context{}; + Fill const fill{context.core}; + Buffer indices{context.core, host_written(VK_BUFFER_USAGE_INDEX_BUFFER_BIT), 3}; + indices.write(std::array{0, 1, 2}); + Buffer commands{ + context.core, + host_written(VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT), + 1 + }; + commands.write( + std::array{ + VkDrawIndexedIndirectCommand{ + .indexCount = 3, + .instanceCount = 1, + .firstIndex = 0, + .vertexOffset = 0, + .firstInstance = 0, + }, + } + ); + + expect_filled(context, fill, [&](PrimaryCmdBuffer const& cmd) { + cmd.bindIndexBuffer(indices); + cmd.drawIndexedIndirect(commands); + }); +} +} diff --git a/cvk/tests/src/render/cmd/local_read_test.cpp b/cvk/tests/src/render/cmd/local_read_test.cpp new file mode 100644 index 00000000..aa51d7bd --- /dev/null +++ b/cvk/tests/src/render/cmd/local_read_test.cpp @@ -0,0 +1,297 @@ +#include "context.hpp" +#include "shaders.hpp" +#include "test.hpp" + +#include "cvk/data/buffer/buffer.hpp" +#include "cvk/data/descriptor/descriptor_pool.hpp" +#include "cvk/data/descriptor/descriptor_set.hpp" +#include "cvk/data/img/image.hpp" +#include "cvk/data/img/image_view.hpp" +#include "cvk/render/cmd/cmd_buffer.hpp" +#include "cvk/render/cmd/cmd_pool.hpp" +#include "cvk/render/pipeline/graphics_pipeline.hpp" +#include "cvk/render/pipeline/layout/descriptor_set_layout.hpp" +#include "cvk/render/pipeline/layout/pipeline_layout.hpp" +#include "cvk/render/pipeline/shader/shader.hpp" +#include "cvk/render/sync/barrier.hpp" + +#include +#include +#include + + +namespace cvk { +namespace { + using test::GREEN; + using test::Vec3; + + constexpr VkExtent2D EXTENT{16, 16}; + constexpr std::array COLOR_FORMATS{VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_R8G8B8A8_UNORM}; + + constexpr BarrierScope COLOR_WRITE{ + VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT + }; + constexpr BarrierScope INPUT_READ{VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT, VK_ACCESS_2_INPUT_ATTACHMENT_READ_BIT}; + + /** + * @brief location 0 -> attachment 0, attachment 1 without output + */ + AttachmentMapping write_first() { return {.locations = {0, VK_ATTACHMENT_UNUSED}}; } + + /** + * @brief attachment 0 -> input attachment 0, location 0 -> attachment 1 + */ + AttachmentMapping copy_first_to_second() { + return {.locations = {VK_ATTACHMENT_UNUSED, 0}, .colorInputIndices = {0, VK_ATTACHMENT_UNUSED}}; + } + + /** + * @brief attachment 0: input attachment, attachment 1: transfer src, both with a view + */ + struct Attachments { + explicit Attachments(Core const& core) : first{ + core, + { + .format = COLOR_FORMATS[0], + .usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT, + }, + EXTENT + }, + second{ + core, + { + .format = COLOR_FORMATS[1], + .usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT, + }, + EXTENT + }, + firstView{first, {}, create}, + secondView{second, {}, create} {} + + /** + * @brief records the transitions: first -> rendering local read, second -> color attachment + */ + void toRendering(BaseCmdBuffer const& cmd) const { + Barrier barrier{}; + barrier.addTransition(first, VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR, VK_IMAGE_LAYOUT_UNDEFINED); + barrier.addTransition(second, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED); + barrier.execute(cmd); + } + + /** + * @brief both attachments cleared to black + */ + [[nodiscard]] RenderingInfo renderingInfo(VkRenderingFlags flags = 0, AttachmentMapping mapping = {}) const { + auto const clear = [](ImageView const& view, VkImageLayout layout) { + return RenderingAttachment{ + .view = &view, + .layout = layout, + .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR, + .storeOp = VK_ATTACHMENT_STORE_OP_STORE, + .clearValue = {.color = {.float32 = {0.f, 0.f, 0.f, 1.f}}}, + }; + }; + return RenderingInfo{ + .colorAttachments = { + clear(firstView, VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR), + clear(secondView, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL), + }, + .flags = flags, + .attachmentMapping = std::move(mapping), + }; + } + + Image first; + Image second; + ImageView firstView; + ImageView secondView; + }; + + /** + * @brief fullscreen pipeline for the attachments + */ + GraphicsPipeline::Config fullscreen(Shader const& vertex, Shader const& fragment, AttachmentMapping mapping) { + auto config = test::fullscreen_config(vertex, fragment, COLOR_FORMATS); + config.attachmentMapping = std::move(mapping); + return config; + } + + /** + * @brief binding 0: input attachment + */ + DescriptorSetLayout::Config input_attachment() { + DescriptorSetLayout::Config config{}; + config.add(0, VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT, VK_SHADER_STAGE_FRAGMENT_BIT); + return config; + } + + /** + * @brief write: push constant color -> attachment 0, read: attachment 0 -> attachment 1 + */ + struct Draws { + Draws(Core const& core, ImageView const& first) : + vertex{core, VK_SHADER_STAGE_VERTEX_BIT, test::shaders::FILL_VERT}, + color{core, VK_SHADER_STAGE_FRAGMENT_BIT, test::shaders::TRIANGLE_FRAG}, + localRead{core, VK_SHADER_STAGE_FRAGMENT_BIT, test::shaders::LOCAL_READ_FRAG}, + writeLayout{core, {.pushConstantRanges = {{VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(GREEN)}}}, create}, + write{writeLayout, fullscreen(vertex, color, write_first()), create}, + inputLayout{core, input_attachment(), create}, + readLayout{core, {.setLayouts = {&inputLayout}}, create}, + read{readLayout, fullscreen(vertex, localRead, copy_first_to_second()), create}, + pool{core, DescriptorPool::Config::ForLayouts({{&inputLayout}}), create}, + input{pool, inputLayout, create}, + vertices{core, test::host_written(VK_BUFFER_USAGE_VERTEX_BUFFER_BIT), test::FULLSCREEN.size()} { + // input attachment: VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR from the binding type + input.write({0, first}); + vertices.write(test::FULLSCREEN); + } + + /** + * @brief records the draw of @ref write + * @pre the command buffer has a viewport and a scissor + */ + void drawWrite(BaseCmdBuffer& cmd) const { + cmd.bindVertexBuffer(0, vertices); + cmd.bindPipeline(write); + cmd.pushConstants(writeLayout, VK_SHADER_STAGE_FRAGMENT_BIT, GREEN); + cmd.draw(3); + } + + Shader vertex; + Shader color; + Shader localRead; + PipelineLayout writeLayout; + GraphicsPipeline write; + DescriptorSetLayout inputLayout; + PipelineLayout readLayout; + GraphicsPipeline read; + DescriptorPool pool; + DescriptorSet input; + Buffer vertices; + }; +} + +CVK_TEST(AttachmentMapping, empty_maps_to_the_default) { + static_assert([] { + AttachmentMapping const mapping{}; + auto const locations = mapping.locationInfo(2); + auto const indices = mapping.inputIndexInfo(2); + + return locations.colorAttachmentCount == 2 && locations.pColorAttachmentLocations == nullptr + && indices.colorAttachmentCount == 2 && indices.pColorAttachmentInputIndices == nullptr + && indices.pDepthInputAttachmentIndex == nullptr && indices.pStencilInputAttachmentIndex == nullptr; + }()); +} + +CVK_TEST(AttachmentMapping, infos_point_into_the_mapping) { + static_assert([] { + //TEMP not const: gcc 16 rejects a const aggregate with a vector and another member in a constant expression + AttachmentMapping mapping{ + .locations = {1, 0}, + .colorInputIndices = {VK_ATTACHMENT_UNUSED, 0}, + .depthInputIndex = 1, + }; + auto const locations = mapping.locationInfo(2); + auto const indices = mapping.inputIndexInfo(2); + + return locations.pColorAttachmentLocations[0] == 1 && locations.pColorAttachmentLocations[1] == 0 + && indices.pColorAttachmentInputIndices[1] == 0 && *indices.pDepthInputAttachmentIndex == 1 + && indices.pStencilInputAttachmentIndex == nullptr; + }()); +} + +CVK_TEST(AttachmentMapping, empty_equals_the_default) { + static_assert([] { + //TEMP not const: gcc 16 rejects a const aggregate with a vector and another member in a constant expression + AttachmentMapping identity{.locations = {0, 1}, .colorInputIndices = {0, 1}}; + AttachmentMapping swapped{.locations = {1, 0}}; + AttachmentMapping depth{.depthInputIndex = 0}; + + return AttachmentMapping{} == identity && AttachmentMapping{} != swapped && AttachmentMapping{} != depth; + }()); +} + +CVK_TEST(LocalRead, draw_reads_an_attachment_of_the_same_rendering) { + test::Context const context{}; + Attachments const attachments{context.core}; + Draws const draws{context.core, attachments.firstView}; + BaseBuffer const readback{context.core, test::READBACK_CONFIG, size_t{EXTENT.width} * EXTENT.height * 4}; + + test::record_and_submit(context, [&](PrimaryCmdBuffer& cmd) { + attachments.toRendering(cmd); + + // the begin sets the mapping of the first draw, the bind of the second pipeline its own + cmd.beginRendering(attachments.renderingInfo(0, write_first())); + EXPECT_EQ(cmd.rendering()->attachmentMapping, write_first()); + + draws.drawWrite(cmd); + + Barrier toInput{VK_DEPENDENCY_BY_REGION_BIT}; + toInput.add( + attachments.first, + { + .src = COLOR_WRITE, + .dst = INPUT_READ, + .newLayout = VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR, + .oldLayout = VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR, + } + ); + toInput.execute(cmd); + + cmd.bindPipeline(draws.read); + EXPECT_EQ(cmd.rendering()->attachmentMapping, copy_first_to_second()); + cmd.bindDescriptorSet(draws.input, draws.readLayout, 0); + cmd.draw(3); + cmd.endRendering(); + + Barrier toTransfer{}; + toTransfer.add( + attachments.second, + ImageBarrierInfo::LayoutTransition( + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + {VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_READ_BIT}, + COLOR_WRITE + ) + ); + toTransfer.execute(cmd); + + test::copy_to_buffer(context, cmd, attachments.second, readback, 0, 0); + test::host_read_barrier(context, cmd); + }); + + EXPECT_TRUE(test::every_pixel_is(readback, test::GREEN_PIXEL)); +} + +CVK_TEST(LocalRead, secondary_inherits_the_mapping) { + test::Context const context{}; + Attachments const attachments{context.core}; + Draws const draws{context.core, attachments.firstView}; + CmdPool const pool{context.core, test::pool_config(context.core), create}; + SecondaryCmdBuffer secondary{pool, create}; + + test::record_and_submit(context, [&](PrimaryCmdBuffer& cmd) { + attachments.toRendering(cmd); + cmd.beginRendering(attachments.renderingInfo(VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT)); + EXPECT_EQ(cmd.rendering()->attachmentMapping, AttachmentMapping{}); + + cmd.setAttachmentMapping(write_first()); + EXPECT_EQ(cmd.rendering()->attachmentMapping, write_first()); + + // the draw needs the mapping of the write pipeline in the secondary + auto const inheritance = cmd.inheritance(); + EXPECT_EQ(inheritance.attachmentMapping, write_first()); + secondary.begin(inheritance, VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); + secondary.setViewport( + {0.f, 0.f, static_cast(EXTENT.width), static_cast(EXTENT.height), 0.f, 1.f} + ); + secondary.setScissor({{0, 0}, EXTENT}); + draws.drawWrite(secondary); + secondary.end(); + + cmd.execute(secondary); + cmd.endRendering(); + }); +} +} diff --git a/cvk/tests/src/render/cmd/rendering_test.cpp b/cvk/tests/src/render/cmd/rendering_test.cpp new file mode 100644 index 00000000..1377e96b --- /dev/null +++ b/cvk/tests/src/render/cmd/rendering_test.cpp @@ -0,0 +1,243 @@ +#include "context.hpp" +#include "test.hpp" + +#include "cvk/data/buffer/base_buffer.hpp" +#include "cvk/data/img/image.hpp" +#include "cvk/data/img/image_view.hpp" +#include "cvk/render/cmd/cmd_buffer.hpp" +#include "cvk/render/sync/barrier.hpp" + +#include +#include + + +namespace cvk { +namespace { + constexpr VkExtent2D EXTENT{16, 16}; + + constexpr Image::Config COLOR_CONFIG{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .format = VK_FORMAT_R8G8B8A8_UNORM, + .usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT, + }; + + constexpr Image::Config LOCAL_READ_CONFIG{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .format = VK_FORMAT_R8G8B8A8_UNORM, + .usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT, + }; + + constexpr BarrierScope COLOR_WRITE{ + VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT + }; + + /** + * @brief the clear color of @ref clear_red() as a VK_FORMAT_R8G8B8A8_UNORM pixel + */ + constexpr std::array RED_PIXEL{255, 0, 0, 255}; + + RenderingAttachment clear_red(ImageView const& view, VkImageLayout layout = VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL) { + return RenderingAttachment{ + .view = &view, + .layout = layout, + .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR, + .storeOp = VK_ATTACHMENT_STORE_OP_STORE, + .clearValue = {.color = {.float32 = {1.f, 0.f, 0.f, 1.f}}}, + }; + } +} + +CVK_TEST(Rendering, clear_a_color_attachment) { + test::Context const context{}; + Image const image{context.core, COLOR_CONFIG, EXTENT}; + ImageView const view{image, {}, create}; + BaseBuffer const readback{context.core, test::READBACK_CONFIG, size_t{EXTENT.width} * EXTENT.height * 4}; + + test::record_and_submit(context, [&](PrimaryCmdBuffer& cmd) { + Barrier transition{}; + transition.addTransition(image, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED); + transition.execute(cmd); + + cmd.beginRendering({.colorAttachments = {clear_red(view)}}); + cmd.endRendering(); + + Barrier barrier{}; + barrier.add( + image, + ImageBarrierInfo::LayoutTransition( + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + {VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_READ_BIT}, + COLOR_WRITE + ) + ); + barrier.execute(cmd); + + test::copy_to_buffer(context, cmd, image, readback, 0, 0); + test::host_read_barrier(context, cmd); + }); + + EXPECT_TRUE(test::every_pixel_is(readback, RED_PIXEL)); +} + +CVK_TEST(Rendering, clear_every_layer) { + test::Context const context{}; + auto config = COLOR_CONFIG; + config.arrayLayers = 2; + Image const image{context.core, config, EXTENT}; + ImageView const view{image, {}, create}; + size_t const layerSize = size_t{EXTENT.width} * EXTENT.height * 4; + BaseBuffer const readback{context.core, test::READBACK_CONFIG, 2 * layerSize}; + + test::record_and_submit(context, [&](PrimaryCmdBuffer& cmd) { + Barrier transition{}; + transition.addTransition(image, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED); + transition.execute(cmd); + + cmd.beginRendering({.colorAttachments = {clear_red(view)}, .layerCount = 2}); + cmd.endRendering(); + + Barrier toTransfer{}; + toTransfer.addTransition(image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + toTransfer.execute(cmd); + image.copyTo(cmd, readback); + test::host_read_barrier(context, cmd); + }); + + EXPECT_TRUE(test::every_pixel_is(readback, RED_PIXEL)); +} + +CVK_TEST(Rendering, tracks_the_current_rendering) { + test::Context const context{}; + Image const image{context.core, COLOR_CONFIG, EXTENT}; + ImageView const view{image, {}, create}; + + test::record_and_submit(context, [&](PrimaryCmdBuffer& cmd) { + Barrier transition{}; + transition.addTransition(image, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED); + transition.execute(cmd); + EXPECT_EQ(cmd.rendering(), nullptr); + + cmd.beginRendering({.colorAttachments = {clear_red(view)}}); + auto const* rendering = cmd.rendering(); + ASSERT_NE(rendering, nullptr); + ASSERT_EQ(rendering->colorAttachments.size(), 1); + EXPECT_EQ(rendering->colorAttachments[0].view.get(), &view); + ASSERT_TRUE(rendering->area.has_value()); + EXPECT_EQ(rendering->area->extent.width, EXTENT.width); + EXPECT_EQ(rendering->area->extent.height, EXTENT.height); + + auto const inheritance = cmd.inheritance(); + ASSERT_EQ(inheritance.colorFormats.size(), 1); + EXPECT_EQ(inheritance.colorFormats[0], COLOR_CONFIG.format); + + cmd.endRendering(); + EXPECT_EQ(cmd.rendering(), nullptr); + }); +} + +CVK_TEST(Rendering, clear_a_depth_attachment) { + test::Context const context{}; + Image const image{context.core, Image::Config::DepthBuffer(VK_FORMAT_D16_UNORM, VK_SAMPLE_COUNT_1_BIT), EXTENT}; + ImageView const view{image, {}, create}; + + test::record_and_submit(context, [&](PrimaryCmdBuffer& cmd) { + Barrier transition{}; + transition.addTransition(image, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED); + transition.execute(cmd); + + cmd.beginRendering( + { + .depthAttachment = RenderingAttachment{ + .view = &view, + .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR, + .storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE, + .clearValue = {.depthStencil = {1.f, 0}}, + }, + } + ); + EXPECT_EQ(cmd.inheritance().depthFormat, VK_FORMAT_D16_UNORM); + cmd.endRendering(); + }); +} + +CVK_TEST(Rendering, depth_attachment_in_the_default_layout) { + test::Context const context{}; + Image const image{context.core, Image::Config::DepthBuffer(VK_FORMAT_D16_UNORM, VK_SAMPLE_COUNT_1_BIT), EXTENT}; + ImageView const view{image, {}, create}; + + test::record_and_submit(context, [&](PrimaryCmdBuffer& cmd) { + // the aspect of the image decides the scopes of VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL + Barrier transition{}; + transition.addTransition(image, VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED); + transition.execute(cmd); + + cmd.beginRendering( + { + .depthAttachment = RenderingAttachment{ + .view = &view, + .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR, + .storeOp = VK_ATTACHMENT_STORE_OP_STORE, + .clearValue = {.depthStencil = {1.f, 0}}, + }, + } + ); + cmd.endRendering(); + }); +} + +CVK_TEST(Rendering, secondary_inherits_the_rendering) { + test::Context const context{}; + Image const image{context.core, COLOR_CONFIG, EXTENT}; + ImageView const view{image, {}, create}; + CmdPool const pool{context.core, test::pool_config(context.core), create}; + SecondaryCmdBuffer secondary{pool, create}; + + test::record_and_submit(context, [&](PrimaryCmdBuffer& cmd) { + Barrier transition{}; + transition.addTransition(image, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED); + transition.execute(cmd); + cmd.beginRendering( + {.colorAttachments = {clear_red(view)}, .flags = VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT} + ); + + secondary.begin(cmd.inheritance(), VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); + EXPECT_TRUE(secondary.recording()); + EXPECT_EQ(secondary.rendering(), nullptr); + secondary.end(); + + cmd.execute(secondary); + cmd.endRendering(); + }); +} + +CVK_TEST(Rendering, local_read_inside_the_rendering) { + test::Context const context{}; + Image const image{context.core, LOCAL_READ_CONFIG, EXTENT}; + ImageView const view{image, {}, create}; + + test::record_and_submit(context, [&](PrimaryCmdBuffer& cmd) { + Barrier transition{}; + transition.addTransition(image, VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR, VK_IMAGE_LAYOUT_UNDEFINED); + transition.execute(cmd); + + cmd.beginRendering({.colorAttachments = {clear_red(view, VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR)}}); + cmd.setAttachmentMapping({.locations = {0}, .colorInputIndices = {0}}); + + Barrier barrier{VK_DEPENDENCY_BY_REGION_BIT}; + barrier.add( + image, + { + .src = COLOR_WRITE, + .dst = {VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT, VK_ACCESS_2_INPUT_ATTACHMENT_READ_BIT}, + .newLayout = VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR, + .oldLayout = VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR, + } + ); + barrier.execute(cmd); + + cmd.endRendering(); + }); +} +} diff --git a/cvk/tests/src/render/pipeline/compute_pipeline_test.cpp b/cvk/tests/src/render/pipeline/compute_pipeline_test.cpp new file mode 100644 index 00000000..543584e1 --- /dev/null +++ b/cvk/tests/src/render/pipeline/compute_pipeline_test.cpp @@ -0,0 +1,151 @@ +#include "context.hpp" +#include "shaders.hpp" +#include "test.hpp" + +#include "cvk/data/buffer/buffer.hpp" +#include "cvk/data/descriptor/descriptor_pool.hpp" +#include "cvk/data/descriptor/descriptor_set.hpp" +#include "cvk/render/cmd/cmd_buffer.hpp" +#include "cvk/render/pipeline/compute_pipeline.hpp" +#include "cvk/render/pipeline/layout/descriptor_set_layout.hpp" +#include "cvk/render/pipeline/layout/pipeline_layout.hpp" +#include "cvk/render/pipeline/shader/shader.hpp" +#include "cvk/render/sync/barrier.hpp" + +#include +#include + + +namespace cvk { +namespace { + /** + * @brief work groups of the values shader, 64 invocations each + */ + constexpr uint32_t GROUPS = 4; + constexpr uint32_t VALUES = GROUPS * 64; + + constexpr Buffer::Config OUTPUT_CONFIG{ + .usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, + .memory = {.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT}, + }; + + /** + * @brief set layout, pipeline layout and shader of the values shader + */ + struct Values { + explicit Values(Core const& core) : setLayout{core, set_config(), create}, + layout{core, PipelineLayout::Config{.setLayouts = {&setLayout}}, create}, + shader{core, VK_SHADER_STAGE_COMPUTE_BIT, test::shaders::VALUES_COMP} {} + + [[nodiscard]] ComputePipeline::Config config() const { return {.stage = shader.stageInfo()}; } + + static DescriptorSetLayout::Config set_config() { + DescriptorSetLayout::Config config{}; + config.add(0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, VK_SHADER_STAGE_COMPUTE_BIT); + return config; + } + + DescriptorSetLayout setLayout; + PipelineLayout layout; + Shader shader; + }; + + /** + * @brief binds the values pipeline and its storage buffer, records the dispatch and checks the values + * @param dispatch records the dispatch with the command buffer + */ + void expect_values(test::Context const& context, auto&& dispatch) { + Values const values{context.core}; + ComputePipeline const pipeline{values.layout, values.config(), create}; + Buffer const output{context.core, OUTPUT_CONFIG, VALUES}; + DescriptorPool const pool{context.core, DescriptorPool::Config::ForLayouts({{&values.setLayout}}), create}; + DescriptorSet const set{pool, values.setLayout, create}; + set.write({0, output}); + + test::record_and_submit(context, [&](PrimaryCmdBuffer const& cmd) { + cmd.bindPipeline(pipeline); + cmd.bindDescriptorSet(set, values.layout, 0, VK_PIPELINE_BIND_POINT_COMPUTE); + dispatch(cmd); + + Barrier toHost{}; + toHost.add( + {VK_PIPELINE_STAGE_2_HOST_BIT, VK_ACCESS_2_HOST_READ_BIT}, + {VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_WRITE_BIT} + ); + toHost.execute(cmd); + }); + + auto const mapped = output.map(); + output.invalidate(); + for(uint32_t i = 0; i < VALUES; ++i) + EXPECT_EQ(mapped[i], 3 * i); + } +} + +CVK_TEST(ComputePipeline, base_constructor_does_not_create) { + test::Context const context{}; + Values const values{context.core}; + + ComputePipeline const pipeline{values.layout, values.config()}; + + EXPECT_FALSE(pipeline.created()); + EXPECT_EQ(&pipeline.layout(), &values.layout); + EXPECT_EQ(pipeline.bindPoint(), VK_PIPELINE_BIND_POINT_COMPUTE); +} + +CVK_TEST(ComputePipeline, create_and_destroy) { + test::Context const context{}; + Values const values{context.core}; + + ComputePipeline pipeline{values.layout, values.config(), create}; + ASSERT_TRUE(pipeline.created()); + EXPECT_EQ(&pipeline.core(), &context.core); + + pipeline.destroy(); + EXPECT_FALSE(pipeline.created()); +} + +CVK_TEST(ComputePipeline, release_and_wrap) { + test::Context const context{}; + Values const values{context.core}; + ComputePipeline pipeline{values.layout, values.config(), create}; + auto const vkPipeline = pipeline.get(); + + auto const state = pipeline.release(); + EXPECT_FALSE(pipeline.created()); + + ComputePipeline const wrapped{values.layout, values.config(), state}; + EXPECT_EQ(wrapped.get(), vkPipeline); +} + +CVK_TEST(ComputePipeline, move_assignment_destroys_the_old_pipeline) { + test::Context const context{}; + Values const values{context.core}; + ComputePipeline pipeline{values.layout, values.config(), create}; + ComputePipeline other{values.layout, values.config(), create}; + auto const vkPipeline = other.get(); + + pipeline = std::move(other); + + EXPECT_EQ(pipeline.get(), vkPipeline); + EXPECT_FALSE(other.created()); +} + +CVK_TEST(ComputePipeline, dispatch_fills_a_storage_buffer) { + test::Context const context{}; + + expect_values(context, [](BaseCmdBuffer const& cmd) { cmd.dispatch(GROUPS); }); +} + +CVK_TEST(ComputePipeline, dispatch_indirect_fills_a_storage_buffer) { + test::Context const context{}; + Buffer const indirect{ + context.core, + test::host_written(VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT), + 1 + }; + indirect.write(std::array{VkDispatchIndirectCommand{GROUPS, 1, 1}}); + + expect_values(context, [&](BaseCmdBuffer const& cmd) { cmd.dispatchIndirect(indirect); }); +} +} diff --git a/cvk/tests/src/render/pipeline/graphics_pipeline_test.cpp b/cvk/tests/src/render/pipeline/graphics_pipeline_test.cpp new file mode 100644 index 00000000..0e4ec7dd --- /dev/null +++ b/cvk/tests/src/render/pipeline/graphics_pipeline_test.cpp @@ -0,0 +1,254 @@ +#include "context.hpp" +#include "shaders.hpp" +#include "test.hpp" + +#include "cvk/render/cmd/cmd_buffer.hpp" +#include "cvk/render/pipeline/graphics_pipeline.hpp" +#include "cvk/render/pipeline/layout/descriptor_set_layout.hpp" +#include "cvk/render/pipeline/layout/pipeline_layout.hpp" +#include "cvk/render/pipeline/shader/shader.hpp" + +#include +#include +#include +#include + + +namespace cvk { +namespace { + constexpr std::array COLOR_FORMATS{VK_FORMAT_B8G8R8A8_UNORM}; + constexpr VkFormat DEPTH_FORMAT = VK_FORMAT_D32_SFLOAT; + + /** + * @brief layout and shaders of the triangle shaders + */ + struct Triangle { + explicit Triangle(Core const& core) : setLayout{core, set_config(), create}, + layout{ + core, + PipelineLayout::Config{ + .setLayouts = {&setLayout}, + .pushConstantRanges = {{.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT, .offset = 0, .size = 16}}, + }, + create + }, + vertex{core, VK_SHADER_STAGE_VERTEX_BIT, test::shaders::TRIANGLE_VERT}, + fragment{core, VK_SHADER_STAGE_FRAGMENT_BIT, test::shaders::TRIANGLE_FRAG} {} + + [[nodiscard]] GraphicsPipeline::Config config() const { + GraphicsPipeline::Config config{ + .colorFormats = {std::from_range, COLOR_FORMATS}, + .depthFormat = DEPTH_FORMAT, + }; + config.addStage(vertex).addStage(fragment); + config.vertexBindings = { + {.binding = 0, .stride = 3 * sizeof(float), .inputRate = VK_VERTEX_INPUT_RATE_VERTEX}, + }; + config.vertexAttributes = { + {.location = 0, .binding = 0, .format = VK_FORMAT_R32G32B32_SFLOAT, .offset = 0}, + }; + return config; + } + + static DescriptorSetLayout::Config set_config() { + DescriptorSetLayout::Config config{}; + config.add(0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_VERTEX_BIT); + return config; + } + + DescriptorSetLayout setLayout; + PipelineLayout layout; + Shader vertex; + Shader fragment; + }; +} + +CVK_TEST(GraphicsPipeline, config_is_an_aggregate_without_blend_attachments) { + static_assert(std::is_aggregate_v); + + GraphicsPipeline::Config const config{ + .colorFormats = {std::from_range, COLOR_FORMATS}, + .depthFormat = DEPTH_FORMAT, + }; + + EXPECT_TRUE(std::ranges::equal(config.colorFormats, COLOR_FORMATS)); + EXPECT_TRUE(config.colorBlendAttachments.empty()); + EXPECT_EQ(config.depthFormat, DEPTH_FORMAT); +} + +CVK_TEST(GraphicsPipeline, config_is_constexpr) { + static_assert([] { + GraphicsPipeline::Config config{ + .colorFormats = {std::from_range, COLOR_FORMATS}, + .depthFormat = DEPTH_FORMAT, + }; + config.stages.push_back( + VkPipelineShaderStageCreateInfo{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .stage = VK_SHADER_STAGE_VERTEX_BIT, + .module = VK_NULL_HANDLE, + .pName = "main", + .pSpecializationInfo = nullptr, + } + ); + config.removeStage(VK_SHADER_STAGE_VERTEX_BIT); + return config.colorFormats.size() == 1 && config.colorBlendAttachments.empty() && config.stages.empty(); + }()); +} + +CVK_TEST(GraphicsPipeline, config_stages) { + test::Context const context{}; + Triangle const triangle{context.core}; + + auto config = triangle.config(); + ASSERT_EQ(config.stages.size(), 2); + EXPECT_EQ(config.stages[0].module, triangle.vertex.get()); + + config.removeStage(VK_SHADER_STAGE_VERTEX_BIT); + ASSERT_EQ(config.stages.size(), 1); + EXPECT_EQ(config.stages[0].stage, VK_SHADER_STAGE_FRAGMENT_BIT); +} + +CVK_TEST(GraphicsPipeline, config_add_stage_takes_the_entry_point) { + test::Context const context{}; + Triangle const triangle{context.core}; + char const* const entryPoint = "other"; + + GraphicsPipeline::Config config{}; + config.addStage(triangle.vertex).addStage(triangle.fragment, nullptr, entryPoint); + + ASSERT_EQ(config.stages.size(), 2); + EXPECT_STREQ(config.stages[0].pName, "main"); + EXPECT_EQ(config.stages[1].pName, entryPoint); +} + +CVK_TEST(GraphicsPipeline, bind) { + test::Context const context{}; + Triangle const triangle{context.core}; + GraphicsPipeline const pipeline{triangle.layout, triangle.config(), create}; + CmdPool const pool{context.core, test::pool_config(context.core), create}; + PrimaryCmdBuffer primary{pool, create}; + + primary.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); + primary.bindPipeline(pipeline); + primary.end(); + + test::submit_and_wait(context, primary); +} + +CVK_TEST(GraphicsPipeline, base_constructor_does_not_create) { + test::Context const context{}; + Triangle const triangle{context.core}; + + GraphicsPipeline const pipeline{triangle.layout, triangle.config()}; + + EXPECT_FALSE(pipeline.created()); + EXPECT_EQ(&pipeline.layout(), &triangle.layout); + EXPECT_EQ(pipeline.bindPoint(), VK_PIPELINE_BIND_POINT_GRAPHICS); +} + +CVK_TEST(GraphicsPipeline, create) { + test::Context const context{}; + Triangle const triangle{context.core}; + + GraphicsPipeline const pipeline{triangle.layout, triangle.config(), create}; + + EXPECT_TRUE(pipeline.created()); + EXPECT_EQ(pipeline.config().stages.size(), 2); +} + +CVK_TEST(GraphicsPipeline, create_keeps_the_empty_blend_attachments) { + test::Context const context{}; + Triangle const triangle{context.core}; + + GraphicsPipeline const pipeline{triangle.layout, triangle.config(), create}; + + EXPECT_TRUE(pipeline.created()); + EXPECT_TRUE(pipeline.config().colorBlendAttachments.empty()); +} + +CVK_TEST(GraphicsPipeline, create_with_a_blend_attachment_per_color_format) { + test::Context const context{}; + Triangle const triangle{context.core}; + + auto config = triangle.config(); + config.colorBlendAttachments = { + {.blendEnable = VK_FALSE, .colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT}, + }; + + GraphicsPipeline const pipeline{triangle.layout, std::move(config), create}; + + EXPECT_TRUE(pipeline.created()); + ASSERT_EQ(pipeline.config().colorBlendAttachments.size(), 1); + EXPECT_EQ(pipeline.config().colorBlendAttachments[0].blendEnable, VK_FALSE); +} + +CVK_TEST(GraphicsPipeline, create_without_depth) { + test::Context const context{}; + Triangle const triangle{context.core}; + + auto config = triangle.config(); + config.depthFormat = VK_FORMAT_UNDEFINED; + + GraphicsPipeline const pipeline{triangle.layout, std::move(config), create}; + + EXPECT_TRUE(pipeline.created()); +} + +CVK_TEST(GraphicsPipeline, shaders_can_be_destroyed_after_create) { + test::Context const context{}; + auto triangle = std::make_unique(context.core); + + GraphicsPipeline pipeline{triangle->layout, triangle->config(), create}; + triangle->vertex.destroy(); + triangle->fragment.destroy(); + + EXPECT_TRUE(pipeline.created()); +} + +CVK_TEST(GraphicsPipeline, release_and_wrap) { + test::Context const context{}; + Triangle const triangle{context.core}; + GraphicsPipeline pipeline{triangle.layout, triangle.config(), create}; + auto const vkPipeline = pipeline.get(); + + auto const state = pipeline.release(); + EXPECT_FALSE(pipeline.created()); + + GraphicsPipeline const wrapped{triangle.layout, triangle.config(), state}; + EXPECT_EQ(wrapped.get(), vkPipeline); +} + +CVK_TEST(GraphicsPipeline, move_assignment_destroys_the_old_pipeline) { + test::Context const context{}; + Triangle const triangle{context.core}; + GraphicsPipeline pipeline{triangle.layout, triangle.config(), create}; + GraphicsPipeline other{triangle.layout, triangle.config(), create}; + auto const vkPipeline = other.get(); + + pipeline = std::move(other); + + EXPECT_EQ(pipeline.get(), vkPipeline); + EXPECT_FALSE(other.created()); +} + +CVK_TEST(GraphicsPipeline, derivative) { + test::Context const context{}; + Triangle const triangle{context.core}; + + auto parentConfig = triangle.config(); + parentConfig.flags = VK_PIPELINE_CREATE_ALLOW_DERIVATIVES_BIT; + GraphicsPipeline const parent{triangle.layout, std::move(parentConfig), create}; + + auto childConfig = triangle.config(); + childConfig.rasterization.cullMode = VK_CULL_MODE_NONE; + GraphicsPipeline const child{parent, std::move(childConfig), create}; + + ASSERT_TRUE(child.created()); + EXPECT_EQ(child.parent(), parent.get()); + EXPECT_EQ(&child.layout(), &triangle.layout); + EXPECT_EQ(parent.parent(), VK_NULL_HANDLE); +} +} diff --git a/cvk/tests/src/render/pipeline/layout/descriptor_set_layout_test.cpp b/cvk/tests/src/render/pipeline/layout/descriptor_set_layout_test.cpp new file mode 100644 index 00000000..426f2849 --- /dev/null +++ b/cvk/tests/src/render/pipeline/layout/descriptor_set_layout_test.cpp @@ -0,0 +1,142 @@ +#include "context.hpp" +#include "test.hpp" + +#include "cvk/render/pipeline/layout/descriptor_set_layout.hpp" + +#include + + +namespace cvk { +namespace { + /** + * @brief bindings 0 and 2, gap at 1 + */ + constexpr DescriptorSetLayout::Config gap_config() { + DescriptorSetLayout::Config config{}; + config.add(2, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT) + .add(0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_VERTEX_BIT, 2); + return config; + } +} + +CVK_TEST(DescriptorSetLayout, config_sorts_bindings) { + auto const config = gap_config(); + + ASSERT_EQ(config.bindings.size(), 2); + EXPECT_EQ(config.bindings[0].binding, 0); + EXPECT_EQ(config.bindings[0].descriptorCount, 2); + EXPECT_EQ(config.bindings[1].binding, 2); +} + +CVK_TEST(DescriptorSetLayout, config_remove) { + auto config = gap_config(); + + config.remove(0); + + ASSERT_EQ(config.bindings.size(), 1); + EXPECT_EQ(config.bindings[0].binding, 2); +} + +CVK_TEST(DescriptorSetLayout, config_is_constexpr) { + static_assert([] { + auto config = gap_config(); + config.remove(0); + return config.bindings.size() == 1 && config.bindings[0].binding == 2; + }()); +} + +CVK_TEST(DescriptorSetLayout, create_with_gaps) { + test::Context const context{}; + + DescriptorSetLayout const layout{context.core, gap_config(), create}; + + ASSERT_TRUE(layout.created()); + EXPECT_EQ(layout.bindings().size(), 2); + + auto const* binding = layout.binding(2); + ASSERT_NE(binding, nullptr); + EXPECT_EQ(binding->descriptorType, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER); + EXPECT_EQ(layout.binding(1), nullptr); + EXPECT_EQ(layout.binding(3), nullptr); +} + +CVK_TEST(DescriptorSetLayout, create_empty) { + test::Context const context{}; + + DescriptorSetLayout const layout{context.core, {}, create}; + + EXPECT_TRUE(layout.created()); + EXPECT_TRUE(layout.bindings().empty()); +} + +CVK_TEST(DescriptorSetLayout, release_and_wrap) { + test::Context const context{}; + DescriptorSetLayout layout{context.core, gap_config(), create}; + auto const vkLayout = layout.get(); + + auto const state = layout.release(); + EXPECT_FALSE(layout.created()); + + DescriptorSetLayout const wrapped{context.core, gap_config(), state}; + EXPECT_EQ(wrapped.get(), vkLayout); + EXPECT_EQ(wrapped.bindings().size(), 2); +} + +CVK_TEST(DescriptorSetLayout, move_assignment_destroys_the_old_layout) { + test::Context const context{}; + DescriptorSetLayout layout{context.core, {}, create}; + DescriptorSetLayout other{context.core, gap_config(), create}; + auto const vkLayout = other.get(); + + layout = std::move(other); + + EXPECT_EQ(layout.get(), vkLayout); + EXPECT_EQ(layout.bindings().size(), 2); + EXPECT_FALSE(other.created()); +} + +CVK_TEST(DescriptorSetLayout, config_keeps_the_flags_in_binding_order) { + static_assert([] { + DescriptorSetLayout::Config config{}; + config.add(0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_VERTEX_BIT) + .add(2, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, VK_SHADER_STAGE_FRAGMENT_BIT, 8, + VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT) + .add(1, VK_DESCRIPTOR_TYPE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT); + bool const added = config.bindingFlags.size() == 3 && config.bindingFlags[0] == 0 + && config.bindingFlags[1] == 0 && config.bindingFlags[2] == VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT; + + config.remove(1); + return added && config.bindingFlags.size() == 2 + && config.bindingFlags[1] == VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT; + }()); +} + +CVK_TEST(DescriptorSetLayout, config_keeps_the_flags_of_the_first_binding) { + static_assert([] { + DescriptorSetLayout::Config config{}; + config.add(0, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, VK_SHADER_STAGE_FRAGMENT_BIT, 8, + VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT); + return config.bindingFlags.size() == 1 && config.bindingFlags[0] == VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT; + }()); +} + +CVK_TEST(DescriptorSetLayout, config_without_flags_keeps_no_flags) { + static_assert(gap_config().bindingFlags.empty()); +} + +CVK_TEST(DescriptorSetLayout, create_bindless) { + test::Context const context{}; + using test::BINDLESS; + + DescriptorSetLayout::Config config{}; + config.add(0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_VERTEX_BIT) + .add(1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT, 1024, BINDLESS); + DescriptorSetLayout const layout{context.core, config, create}; + + ASSERT_TRUE(layout.created()); + EXPECT_TRUE(layout.updateAfterBind()); + EXPECT_EQ(layout.bindingFlags(1), BINDLESS); + EXPECT_EQ(layout.bindingFlags(0), 0); + EXPECT_EQ(layout.variableBinding(), layout.binding(1)); +} +} diff --git a/cvk/tests/src/render/pipeline/layout/pipeline_layout_test.cpp b/cvk/tests/src/render/pipeline/layout/pipeline_layout_test.cpp new file mode 100644 index 00000000..098394ad --- /dev/null +++ b/cvk/tests/src/render/pipeline/layout/pipeline_layout_test.cpp @@ -0,0 +1,89 @@ +#include "context.hpp" +#include "test.hpp" + +#include "cvk/base/physical_device.hpp" +#include "cvk/render/pipeline/layout/descriptor_set_layout.hpp" +#include "cvk/render/pipeline/layout/pipeline_layout.hpp" +#include "cvk/utility/vk_exceptions.hpp" + + +namespace cvk { +namespace { + DescriptorSetLayout::Config uniform_config() { + DescriptorSetLayout::Config config{}; + config.add(0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_VERTEX_BIT); + return config; + } +} + +CVK_TEST(PipelineLayout, create_empty) { + test::Context const context{}; + + PipelineLayout const layout{context.core, {}, create}; + + EXPECT_TRUE(layout.created()); +} + +CVK_TEST(PipelineLayout, create_with_sets_and_push_constants) { + test::Context const context{}; + DescriptorSetLayout const set0{context.core, uniform_config(), create}; + DescriptorSetLayout const set1{context.core, {}, create}; + + PipelineLayout::Config const config{ + .setLayouts = {&set0, &set1}, + .pushConstantRanges = {{.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT, .offset = 0, .size = 16}}, + }; + + PipelineLayout const layout{context.core, config, create}; + + EXPECT_TRUE(layout.created()); +} + +CVK_TEST(PipelineLayout, too_many_sets_throws) { + test::Context const context{}; + DescriptorSetLayout const set{context.core, {}, create}; + + auto const maxSets = context.core.physicalDevice().limits().maxBoundDescriptorSets; + PipelineLayout::Config const config{ + .setLayouts = std::vector>(maxSets + 1, &set), + }; + + auto const construct = [&] { PipelineLayout const layout{context.core, config, create}; }; + + EXPECT_THROW(construct(), cvk_exception); +} + +CVK_TEST(PipelineLayout, set_layouts_can_be_destroyed_after_create) { + test::Context const context{}; + auto set = std::make_unique(context.core, uniform_config(), create); + + PipelineLayout const layout{context.core, PipelineLayout::Config{.setLayouts = {set.get()}}, create}; + set = nullptr; + + EXPECT_TRUE(layout.created()); +} + +CVK_TEST(PipelineLayout, release_and_wrap) { + test::Context const context{}; + PipelineLayout layout{context.core, {}, create}; + auto const vkLayout = layout.get(); + + auto const state = layout.release(); + EXPECT_FALSE(layout.created()); + + PipelineLayout const wrapped{context.core, {}, state}; + EXPECT_EQ(wrapped.get(), vkLayout); +} + +CVK_TEST(PipelineLayout, move_assignment_destroys_the_old_layout) { + test::Context const context{}; + PipelineLayout layout{context.core, {}, create}; + PipelineLayout other{context.core, {}, create}; + auto const vkLayout = other.get(); + + layout = std::move(other); + + EXPECT_EQ(layout.get(), vkLayout); + EXPECT_FALSE(other.created()); +} +} diff --git a/cvk/tests/src/render/pipeline/shader/shader_test.cpp b/cvk/tests/src/render/pipeline/shader/shader_test.cpp new file mode 100644 index 00000000..7b0ebcea --- /dev/null +++ b/cvk/tests/src/render/pipeline/shader/shader_test.cpp @@ -0,0 +1,182 @@ +#include "context.hpp" +#include "shaders.hpp" +#include "test.hpp" + +#include "cvk/render/pipeline/shader/shader.hpp" +#include "cvk/utility/vk_exceptions.hpp" + +#include + +#ifdef CTH_PLATFORM_WINDOWS +#include +#define CVK_NULL_DEVICE "NUL" +#else +#include +#define CVK_NULL_DEVICE "/dev/null" +#endif + +#include +#include +#include +#include +#include + + +namespace cvk { +namespace { + using test::shaders::TRIANGLE_VERT; + + /** + * @return glslc runs, the check runs once per process + */ + bool glslc_available() { + static bool const available = std::system("glslc --version >" CVK_NULL_DEVICE " 2>&1") == 0; + return available; + } + + auto process_id() { +#ifdef CTH_PLATFORM_WINDOWS + return _getpid(); +#else + return getpid(); +#endif + } + + /** + * @return directory of this process for the temp files, the process exit removes it + * @details one directory per process: test processes can run at the same time + */ + std::filesystem::path const& temp_dir() { + struct Directory { + Directory() { std::filesystem::create_directories(path); } + ~Directory() { + std::error_code error{}; + std::filesystem::remove_all(path, error); + } + + std::filesystem::path path = std::filesystem::temp_directory_path() + / std::format("cvk_tests_{}", process_id()); + }; + static Directory const directory{}; + return directory.path; + } + + /** + * @brief writes bytes to a file in @ref temp_dir() + */ + std::filesystem::path write_temp(std::string_view name, std::span bytes) { + auto path = temp_dir() / name; + std::ofstream file{path, std::ios::binary}; + file.write(bytes.data(), static_cast(bytes.size())); + return path; + } +} + +CVK_TEST(Shader, base_constructor_does_not_create) { + test::Context const context{}; + + Shader const shader{context.core, VK_SHADER_STAGE_VERTEX_BIT}; + + EXPECT_FALSE(shader.created()); + EXPECT_EQ(shader.stage(), VK_SHADER_STAGE_VERTEX_BIT); +} + +CVK_TEST(Shader, create_from_spv) { + test::Context const context{}; + + Shader const shader{context.core, VK_SHADER_STAGE_VERTEX_BIT, TRIANGLE_VERT}; + + ASSERT_TRUE(shader.created()); + + auto const info = shader.stageInfo(); + EXPECT_EQ(info.sType, VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO); + EXPECT_EQ(info.stage, VK_SHADER_STAGE_VERTEX_BIT); + EXPECT_EQ(info.module, shader.get()); + EXPECT_STREQ(info.pName, "main"); + EXPECT_EQ(info.pSpecializationInfo, nullptr); +} + +CVK_TEST(Shader, create_from_file) { + test::Context const context{}; + std::span const bytes{reinterpret_cast(TRIANGLE_VERT.data()), sizeof(TRIANGLE_VERT)}; + auto const path = write_temp("cvk_test_triangle.vert.spv", bytes); + + Shader const shader{context.core, VK_SHADER_STAGE_VERTEX_BIT, path}; + + EXPECT_TRUE(shader.created()); + EXPECT_TRUE(std::ranges::equal(Shader::load(path), TRIANGLE_VERT)); + + std::filesystem::remove(path); +} + +CVK_TEST(Shader, load_rejects_bad_files) { + auto const missing = temp_dir() / "cvk_test_missing.spv"; + EXPECT_THROW([[maybe_unused]] auto const spv = Shader::load(missing), cvk_exception); + + constexpr std::array notWords{1, 2, 3, 4, 5}; + auto const odd = write_temp("cvk_test_odd.spv", notWords); + EXPECT_THROW([[maybe_unused]] auto const spv = Shader::load(odd), cvk_exception); + std::filesystem::remove(odd); + + auto const empty = write_temp("cvk_test_empty.spv", {}); + EXPECT_THROW([[maybe_unused]] auto const spv = Shader::load(empty), cvk_exception); + std::filesystem::remove(empty); +} + +CVK_TEST(Shader, release_and_wrap) { + test::Context const context{}; + Shader shader{context.core, VK_SHADER_STAGE_VERTEX_BIT, TRIANGLE_VERT}; + auto const vkShader = shader.get(); + + auto const state = shader.release(); + EXPECT_FALSE(shader.created()); + + Shader const wrapped{context.core, VK_SHADER_STAGE_VERTEX_BIT, state}; + EXPECT_EQ(wrapped.get(), vkShader); +} + +CVK_TEST(Shader, move_assignment_destroys_the_old_shader) { + test::Context const context{}; + Shader shader{context.core, VK_SHADER_STAGE_VERTEX_BIT, TRIANGLE_VERT}; + Shader other{context.core, VK_SHADER_STAGE_FRAGMENT_BIT, test::shaders::TRIANGLE_FRAG}; + auto const vkShader = other.get(); + + shader = std::move(other); + + EXPECT_EQ(shader.get(), vkShader); + EXPECT_EQ(shader.stage(), VK_SHADER_STAGE_FRAGMENT_BIT); + EXPECT_FALSE(other.created()); +} + +CVK_TEST(Shader, compile_and_create) { + if(!glslc_available()) GTEST_SKIP() << "glslc not found"; + test::Context const context{}; + auto const spvPath = temp_dir() / "cvk_test_compiled.vert.spv"; + + Shader const shader{context.core, VK_SHADER_STAGE_VERTEX_BIT, spvPath, test::SHADER_DIR / "triangle.vert"}; + + EXPECT_TRUE(shader.created()); + EXPECT_TRUE(std::filesystem::exists(spvPath)); + EXPECT_FALSE(std::filesystem::exists(spvPath.string() + ".log")); + + std::filesystem::remove(spvPath); +} + +CVK_TEST(Shader, compile_error_throws) { + if(!glslc_available()) GTEST_SKIP() << "glslc not found"; + constexpr std::string_view broken = "#version 450\nvoid main() { undeclared = 1; }\n"; + auto const glslPath = write_temp("cvk_test_broken.vert", broken); + auto const spvPath = temp_dir() / "cvk_test_broken.vert.spv"; + + EXPECT_THROW(Shader::compile(glslPath, spvPath), cvk_exception); + EXPECT_FALSE(std::filesystem::exists(spvPath.string() + ".log")); + + std::filesystem::remove(glslPath); +} + +CVK_TEST(Shader, compile_missing_glsl_throws) { + auto const missing = temp_dir() / "cvk_test_missing.vert"; + + EXPECT_THROW(Shader::compile(missing, missing.string() + ".spv"), cvk_exception); +} +} diff --git a/cvk/tests/src/render/sync/barrier_test.cpp b/cvk/tests/src/render/sync/barrier_test.cpp new file mode 100644 index 00000000..10b666fd --- /dev/null +++ b/cvk/tests/src/render/sync/barrier_test.cpp @@ -0,0 +1,292 @@ +#include "context.hpp" +#include "test.hpp" + +#include "cvk/data/buffer/base_buffer.hpp" +#include "cvk/data/img/image.hpp" +#include "cvk/render/cmd/cmd_buffer.hpp" +#include "cvk/render/sync/barrier.hpp" + +#include + + +namespace cvk { +namespace { + constexpr VkExtent2D EXTENT{16, 16}; + + constexpr Image::Config IMAGE_CONFIG{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .format = VK_FORMAT_R8G8B8A8_UNORM, + .usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT, + .mipLevels = mip_level_count(EXTENT), + }; + + constexpr BarrierScope TRANSFER_WRITE{VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT}; + constexpr BarrierScope FRAGMENT_READ{VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT, VK_ACCESS_2_SHADER_READ_BIT}; + + constexpr VkImageAspectFlags COLOR = VK_IMAGE_ASPECT_COLOR_BIT; + constexpr VkImageAspectFlags DEPTH = VK_IMAGE_ASPECT_DEPTH_BIT; + constexpr VkPipelineStageFlags2 TESTS = VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT + | VK_PIPELINE_STAGE_2_LATE_FRAGMENT_TESTS_BIT; +} + +CVK_TEST(Barrier, layout_transition_from_undefined_waits_for_the_dst_stages) { + constexpr auto toTransfer = layout_transition( + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_UNDEFINED, + COLOR + ); + static_assert(toTransfer.has_value()); + static_assert(toTransfer->src.stages == VK_PIPELINE_STAGE_2_TRANSFER_BIT); + static_assert(toTransfer->src.access == VK_ACCESS_2_NONE); + static_assert(toTransfer->dst.access == VK_ACCESS_2_TRANSFER_WRITE_BIT); + + constexpr auto toLocalRead = layout_transition( + VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR, + VK_IMAGE_LAYOUT_UNDEFINED, + COLOR + ); + static_assert(toLocalRead.has_value()); + static_assert((toLocalRead->dst.access & VK_ACCESS_2_INPUT_ATTACHMENT_READ_BIT) != 0); + + constexpr auto toUndefined = layout_transition( + VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + COLOR + ); + static_assert(!toUndefined.has_value()); +} + +CVK_TEST(Barrier, layout_transition_src_from_the_old_dst_from_the_new_layout) { + constexpr auto readback = layout_transition( + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + COLOR + ); + static_assert(readback.has_value()); + static_assert(readback->src.stages == VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT); + static_assert(readback->src.access == VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT); + static_assert(readback->dst.stages == VK_PIPELINE_STAGE_2_TRANSFER_BIT); + static_assert(readback->dst.access == VK_ACCESS_2_TRANSFER_READ_BIT); +} + +CVK_TEST(Barrier, layout_transition_src_keeps_only_the_writes) { + constexpr auto renderAgain = layout_transition( + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + COLOR + ); + static_assert(renderAgain.has_value()); + static_assert(renderAgain->src.stages == VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT); + static_assert(renderAgain->src.access == VK_ACCESS_2_NONE); +} + +CVK_TEST(Barrier, layout_transition_aspects_pick_color_or_depth) { + constexpr auto color = layout_transition(VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED, COLOR); + constexpr auto depth = layout_transition(VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED, DEPTH); + static_assert(color->dst.stages == VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT); + static_assert(depth->dst.stages == TESTS); + + constexpr auto depthLocalRead = layout_scope(VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR, DEPTH); + static_assert(depthLocalRead->stages == (TESTS | VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT)); +} + +CVK_TEST(Barrier, layout_transition_shader_stages_replace_the_fragment_shader) { + constexpr auto toCompute = layout_transition( + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + COLOR, + VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT + ); + static_assert(toCompute->dst.stages == VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT); + + constexpr auto fromCompute = layout_transition( + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + COLOR, + VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT + ); + static_assert(fromCompute->src.stages == VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT); + + // input attachments are fragment shader only + constexpr auto localRead = layout_scope( + VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR, + COLOR, + VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT + ); + static_assert((localRead->stages & VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT) == 0); +} + +CVK_TEST(Barrier, every_pair_of_known_layouts_has_a_transition) { + static_assert([] { + constexpr std::array layouts{ + VK_IMAGE_LAYOUT_GENERAL, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, + VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL, + VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL, + VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, + VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_OPTIMAL, + VK_IMAGE_LAYOUT_STENCIL_ATTACHMENT_OPTIMAL, + VK_IMAGE_LAYOUT_STENCIL_READ_ONLY_OPTIMAL, + VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL, + VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL, + VK_IMAGE_LAYOUT_RENDERING_LOCAL_READ_KHR, + VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, + }; + for(auto const aspects : {COLOR, DEPTH}) + for(auto const newLayout : layouts) + for(auto const oldLayout : layouts) + if(!layout_transition(newLayout, oldLayout, aspects).has_value()) return false; + return true; + }()); +} + +CVK_TEST(Barrier, layout_transition_info_covers_the_remaining_mip_levels) { + constexpr auto info = ImageBarrierInfo::LayoutTransition( + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_UNDEFINED, + TRANSFER_WRITE, + BarrierScope{} + ); + + static_assert(info.firstMipLevel == 0); + static_assert(info.mipLevels == VK_REMAINING_MIP_LEVELS); + static_assert(info.newLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + static_assert(info.oldLayout == VK_IMAGE_LAYOUT_UNDEFINED); + static_assert(info.dst.access == VK_ACCESS_2_TRANSFER_WRITE_BIT); + static_assert(info.src.access == VK_ACCESS_2_NONE); +} + +CVK_TEST(Barrier, queue_transition_info_keeps_the_layout) { + constexpr auto info = ImageBarrierInfo::QueueTransition( + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + {.queueFamilyIndex = 1}, + {.queueFamilyIndex = 0} + ); + + static_assert(info.newLayout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + static_assert(info.oldLayout == info.newLayout); + static_assert(info.dst.queueFamilyIndex == 1); + static_assert(info.src.queueFamilyIndex == 0); +} + +CVK_TEST(Barrier, memory_barriers_are_constexpr) { + static_assert([] { + Barrier barrier{VK_DEPENDENCY_BY_REGION_BIT}; + barrier.add(FRAGMENT_READ, TRANSFER_WRITE); + bool const added = !barrier.empty(); + bool const ordered = barrier.memory.front().dst.access == FRAGMENT_READ.access; + barrier.clear(); + return added && ordered && barrier.empty(); + }()); +} + +CVK_TEST(Barrier, aggregate_is_constexpr) { + static_assert([] { + //TEMP not const: gcc 16 rejects a const aggregate with a vector and another member in a constant expression + Barrier barrier{ + .flags = VK_DEPENDENCY_BY_REGION_BIT, + .memory = {{TRANSFER_WRITE, FRAGMENT_READ}}, + }; + return !barrier.empty() && barrier.memory.size() == 1 && barrier.buffers.empty(); + }()); +} + +CVK_TEST(Barrier, add_and_clear) { + test::Context const context{}; + BaseBuffer const buffer{context.core, {.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT}, 64}; + Barrier barrier{VK_DEPENDENCY_BY_REGION_BIT}; + EXPECT_TRUE(barrier.empty()); + + barrier.add(FRAGMENT_READ, TRANSFER_WRITE); + barrier.add(buffer, {.src = TRANSFER_WRITE, .dst = FRAGMENT_READ}); + EXPECT_FALSE(barrier.empty()); + + barrier.clear(); + EXPECT_TRUE(barrier.empty()); + EXPECT_EQ(barrier.flags, VK_DEPENDENCY_BY_REGION_BIT); +} + +CVK_TEST(Barrier, execute_memory_and_buffer_barriers) { + test::Context const context{}; + BaseBuffer const buffer{context.core, {.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT}, 64}; + Barrier barrier{VK_DEPENDENCY_BY_REGION_BIT}; + barrier.add(FRAGMENT_READ, TRANSFER_WRITE); + barrier.add(buffer, {.src = TRANSFER_WRITE, .dst = FRAGMENT_READ, .offset = 16, .size = 32}); + + test::record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { barrier.execute(primary); }); +} + +CVK_TEST(Barrier, image_layout_transition) { + test::Context const context{}; + Image const image{context.core, IMAGE_CONFIG, EXTENT}; + + Barrier const barrier{ + .images = { + { + &image, + ImageBarrierInfo::LayoutTransition( + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_UNDEFINED, + TRANSFER_WRITE, + {} + ), + }, + }, + }; + test::record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { barrier.execute(primary); }); +} + +CVK_TEST(Barrier, add_transition_on_a_mip_range) { + test::Context const context{}; + Image const image{context.core, IMAGE_CONFIG, EXTENT}; + + // the validation layers check every old layout + test::record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { + Barrier toTransfer{}; + toTransfer.addTransition(image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED); + toTransfer.execute(primary); + + Barrier toRead{}; + toRead.addTransition( + image, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + Barrier::INFERRED_SHADER_STAGES, + 1, + 2 + ); + toRead.addTransition( + image, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + Barrier::INFERRED_SHADER_STAGES, + 3 + ); + toRead.execute(primary); + + Barrier back{}; + back.addTransition( + image, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + Barrier::INFERRED_SHADER_STAGES, + 3 + ); + back.execute(primary); + }); +} + +CVK_TEST(Barrier, memory_only_image_barrier) { + test::Context const context{}; + Image const image{context.core, IMAGE_CONFIG, EXTENT}; + + Barrier barrier{}; + barrier.add(image, {.src = TRANSFER_WRITE, .dst = TRANSFER_WRITE}); + test::record_and_submit(context, [&](PrimaryCmdBuffer const& primary) { barrier.execute(primary); }); +} +} diff --git a/cvk/tests/src/render/sync/fence_test.cpp b/cvk/tests/src/render/sync/fence_test.cpp new file mode 100644 index 00000000..d40e76de --- /dev/null +++ b/cvk/tests/src/render/sync/fence_test.cpp @@ -0,0 +1,187 @@ +#include "context.hpp" +#include "test.hpp" + +#include "cvk/base/physical_device.hpp" +#include "cvk/render/sync/fence.hpp" +#include "cvk/utility/constant/os_constants.hpp" +#include "cvk/utility/vk_exceptions.hpp" + +#include + +#include +#include +#include + + +namespace cvk { +namespace { + VkSubmitInfo2 const EMPTY_SUBMIT{ + .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2, + .pNext = nullptr, + .flags = 0, + .waitSemaphoreInfoCount = 0, + .pWaitSemaphoreInfos = nullptr, + .commandBufferInfoCount = 0, + .pCommandBufferInfos = nullptr, + .signalSemaphoreInfoCount = 0, + .pSignalSemaphoreInfos = nullptr, + }; + + Core::Config os_fence_config() { + auto config = test::Context::CONFIG; + config.extensions.append_range( + constants::OS_FENCE_DEVICE_EXTENSIONS | std::views::transform( + [](std::string_view extension) { return std::string{extension}; } + ) + ); + return config; + } + + /** + * @return the device can export @ref constants::OS_FENCE_HANDLE_TYPE + */ + bool os_fence_exportable(Core const& core) { + VkPhysicalDeviceExternalFenceInfo const info{ + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_FENCE_INFO, + .pNext = nullptr, + .handleType = constants::OS_FENCE_HANDLE_TYPE, + }; + VkExternalFenceProperties properties{.sType = VK_STRUCTURE_TYPE_EXTERNAL_FENCE_PROPERTIES, .pNext = nullptr}; + vkGetPhysicalDeviceExternalFenceProperties(core.vkPhysicalDevice(), &info, &properties); + return (properties.externalFenceFeatures & VK_EXTERNAL_FENCE_FEATURE_EXPORTABLE_BIT) != 0; + } +} + +CVK_TEST(Fence, base_constructor_does_not_create) { + test::Context const context{}; + + Fence const fence{context.core}; + + EXPECT_FALSE(fence.created()); +} + +CVK_TEST(Fence, create_not_signaled) { + test::Context const context{}; + + Fence const fence{context.core, create}; + + ASSERT_TRUE(fence.created()); + EXPECT_EQ(fence.status(), VK_NOT_READY); + EXPECT_FALSE(fence.signaled()); + EXPECT_EQ(fence.wait(0), VK_TIMEOUT); +} + +CVK_TEST(Fence, create_signaled_and_reset) { + test::Context const context{}; + + Fence const fence{context.core, create, VK_FENCE_CREATE_SIGNALED_BIT}; + + ASSERT_TRUE(fence.signaled()); + fence.wait(); + + fence.reset(); + EXPECT_FALSE(fence.signaled()); +} + +CVK_TEST(Fence, submit_signals) { + test::Context const context{}; + Fence const fence{context.core, create}; + + context.core.queue(0).rawSubmit(EMPTY_SUBMIT, fence.get()); + + fence.wait(); + EXPECT_TRUE(fence.signaled()); + + fence.waitReset(); + EXPECT_FALSE(fence.signaled()); +} + +CVK_TEST(Fence, wait_reset_timeout_keeps_the_state) { + test::Context const context{}; + Fence const fence{context.core, create}; + + EXPECT_EQ(fence.waitReset(0), VK_TIMEOUT); + EXPECT_FALSE(fence.signaled()); + + context.core.queue(0).rawSubmit(EMPTY_SUBMIT, fence.get()); + + EXPECT_EQ(fence.waitReset(UINT64_MAX), VK_SUCCESS); + EXPECT_FALSE(fence.signaled()); +} + +CVK_TEST(Fence, release_and_wrap) { + test::Context const context{}; + Fence fence{context.core, create, VK_FENCE_CREATE_SIGNALED_BIT}; + auto const vkFence = fence.get(); + + auto const state = fence.release(); + EXPECT_FALSE(fence.created()); + EXPECT_EQ(state.vkFence.get(), vkFence); + + Fence const wrapped{context.core, state}; + ASSERT_TRUE(wrapped.created()); + EXPECT_EQ(wrapped.get(), vkFence); + EXPECT_TRUE(wrapped.signaled()); +} + +CVK_TEST(Fence, move_assignment_destroys_the_old_fence) { + test::Context const context{}; + Fence fence{context.core, create}; + Fence other{context.core, create, VK_FENCE_CREATE_SIGNALED_BIT}; + auto const vkFence = other.get(); + + fence = std::move(other); + + EXPECT_EQ(fence.get(), vkFence); + EXPECT_FALSE(other.created()); + EXPECT_TRUE(fence.signaled()); +} + +CVK_TEST(Fence, extract_os_handle_waits_for_the_submit) { + std::optional context{}; + try { context.emplace(os_fence_config()); } catch(cvk_exception const&) { GTEST_SKIP() << "no os fence extension"; } + if(!os_fence_exportable(context->core)) GTEST_SKIP() << "os fence handle type not exportable"; + + Fence const fence{context->core, create, 0, constants::OS_FENCE_HANDLE_TYPE}; + EXPECT_EQ(fence.exportTypes(), constants::OS_FENCE_HANDLE_TYPE); + + auto const& queue = context->core.queue(0); + queue.rawSubmit(EMPTY_SUBMIT, fence.get()); + + auto const handle = fence.extractOsHandle(); + if(handle) EXPECT_EQ(cth::os::wait(handle.get(), std::chrono::seconds{5}), cth::os::wait_result::WAITED); + +#ifndef CTH_PLATFORM_WINDOWS + // sync fd export resets the fence + EXPECT_FALSE(fence.signaled()); +#endif + + queue.waitIdle(); +} + +CVK_TEST(Fence, extract_os_handle_of_a_signaled_fence) { + std::optional context{}; + try { context.emplace(os_fence_config()); } catch(cvk_exception const&) { GTEST_SKIP() << "no os fence extension"; } + if(!os_fence_exportable(context->core)) GTEST_SKIP() << "os fence handle type not exportable"; + + Fence const fence{context->core, create, VK_FENCE_CREATE_SIGNALED_BIT, constants::OS_FENCE_HANDLE_TYPE}; + + auto const handle = fence.extractOsHandle(); + + // invalid handle -> already signaled + EXPECT_TRUE(!handle || cth::os::check_ready(handle.get())); +} + +CVK_TEST(Fence, release_and_wrap_keep_the_export_types) { + test::Context const context{}; + if(!os_fence_exportable(context.core)) GTEST_SKIP() << "os fence handle type not exportable"; + Fence fence{context.core, create, 0, constants::OS_FENCE_HANDLE_TYPE}; + + auto const state = fence.release(); + EXPECT_EQ(state.exportTypes, constants::OS_FENCE_HANDLE_TYPE); + EXPECT_EQ(fence.exportTypes(), 0); + + Fence const wrapped{context.core, state}; + EXPECT_EQ(wrapped.exportTypes(), constants::OS_FENCE_HANDLE_TYPE); +} +} diff --git a/cvk/tests/src/render/sync/semaphore_test.cpp b/cvk/tests/src/render/sync/semaphore_test.cpp new file mode 100644 index 00000000..e56d1e2c --- /dev/null +++ b/cvk/tests/src/render/sync/semaphore_test.cpp @@ -0,0 +1,101 @@ +#include "context.hpp" +#include "test.hpp" + +#include "cvk/render/sync/fence.hpp" +#include "cvk/render/sync/semaphore.hpp" + + +namespace cvk { +namespace { + VkSemaphoreSubmitInfo semaphore_info(Semaphore const& semaphore) { + return VkSemaphoreSubmitInfo{ + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO, + .pNext = nullptr, + .semaphore = semaphore.get(), + .value = 0, + .stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, + .deviceIndex = 0, + }; + } +} + +CVK_TEST(Semaphore, base_constructor_does_not_create) { + test::Context const context{}; + + Semaphore const semaphore{context.core}; + + EXPECT_FALSE(semaphore.created()); +} + +CVK_TEST(Semaphore, create_and_destroy) { + test::Context const context{}; + Semaphore semaphore{context.core, create}; + + ASSERT_TRUE(semaphore.created()); + EXPECT_EQ(&semaphore.core(), &context.core); + + semaphore.destroy(); + EXPECT_FALSE(semaphore.created()); +} + +CVK_TEST(Semaphore, submit_signals_and_waits) { + test::Context const context{}; + Semaphore const semaphore{context.core, create}; + Fence const fence{context.core, create}; + auto const& queue = context.core.queue(0); + + auto const info = semaphore_info(semaphore); + + VkSubmitInfo2 const signalSubmit{ + .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2, + .pNext = nullptr, + .flags = 0, + .waitSemaphoreInfoCount = 0, + .pWaitSemaphoreInfos = nullptr, + .commandBufferInfoCount = 0, + .pCommandBufferInfos = nullptr, + .signalSemaphoreInfoCount = 1, + .pSignalSemaphoreInfos = &info, + }; + VkSubmitInfo2 const waitSubmit{ + .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2, + .pNext = nullptr, + .flags = 0, + .waitSemaphoreInfoCount = 1, + .pWaitSemaphoreInfos = &info, + .commandBufferInfoCount = 0, + .pCommandBufferInfos = nullptr, + .signalSemaphoreInfoCount = 0, + .pSignalSemaphoreInfos = nullptr, + }; + + queue.rawSubmit(signalSubmit); + queue.rawSubmit(waitSubmit, fence.get()); + + fence.wait(); +} + +CVK_TEST(Semaphore, release_and_wrap) { + test::Context const context{}; + Semaphore semaphore{context.core, create}; + auto const vkSemaphore = semaphore.get(); + + auto const state = semaphore.release(); + EXPECT_FALSE(semaphore.created()); + + Semaphore const wrapped{context.core, state}; + EXPECT_EQ(wrapped.get(), vkSemaphore); +} + +CVK_TEST(Semaphore, move_assignment_destroys_the_old_semaphore) { + test::Context const context{}; + Semaphore semaphore{context.core, create}; + Semaphore other{context.core, create}; + auto const vkSemaphore = other.get(); + + semaphore = std::move(other); + + EXPECT_EQ(semaphore.get(), vkSemaphore); + EXPECT_FALSE(other.created()); +} +} diff --git a/cvk/tests/src/render/sync/timeline_semaphore_test.cpp b/cvk/tests/src/render/sync/timeline_semaphore_test.cpp new file mode 100644 index 00000000..e7a97dde --- /dev/null +++ b/cvk/tests/src/render/sync/timeline_semaphore_test.cpp @@ -0,0 +1,149 @@ +#include "context.hpp" +#include "test.hpp" + +#include "cvk/render/sync/timeline_semaphore.hpp" + +#include + + +namespace cvk { +CVK_TEST(TimelineSemaphore, base_constructor_does_not_create) { + test::Context const context{}; + + TimelineSemaphore const semaphore{context.core}; + + EXPECT_FALSE(semaphore.created()); +} + +CVK_TEST(TimelineSemaphore, create_sets_the_initial_value) { + test::Context const context{}; + TimelineSemaphore semaphore{context.core}; + + semaphore.create(5); + + ASSERT_TRUE(semaphore.created()); + EXPECT_EQ(semaphore.value(), 5); + EXPECT_EQ(semaphore.gpuValue(), 5); + EXPECT_EQ(semaphore.wait(semaphore.value(), 0), VK_SUCCESS); +} + +CVK_TEST(TimelineSemaphore, construct_with_an_initial_value) { + test::Context const context{}; + + TimelineSemaphore const semaphore{context.core, create, 5}; + + ASSERT_TRUE(semaphore.created()); + EXPECT_EQ(semaphore.value(), 5); + EXPECT_EQ(semaphore.gpuValue(), 5); +} + +CVK_TEST(TimelineSemaphore, signal_advances_the_counter) { + test::Context const context{}; + TimelineSemaphore semaphore{context.core, create}; + + EXPECT_EQ(semaphore.signal(), 1); + EXPECT_EQ(semaphore.value(), 1); + EXPECT_EQ(semaphore.gpuValue(), 1); + semaphore.wait(); +} + +CVK_TEST(TimelineSemaphore, wait_for_a_value_not_signaled_times_out) { + test::Context const context{}; + TimelineSemaphore const semaphore{context.core, create}; + + EXPECT_EQ(semaphore.next(), 1); + + EXPECT_EQ(semaphore.wait(semaphore.value(), 0), VK_TIMEOUT); + EXPECT_EQ(semaphore.gpuValue(), 0); +} + +CVK_TEST(TimelineSemaphore, wait_for_an_older_value) { + test::Context const context{}; + TimelineSemaphore semaphore{context.core, create}; + + EXPECT_EQ(semaphore.signal(), 1); + EXPECT_EQ(semaphore.next(), 2); + + // the counter is ahead of the semaphore, an older value is reached + EXPECT_EQ(semaphore.wait(1, 0), VK_SUCCESS); + EXPECT_EQ(semaphore.wait(2, 0), VK_TIMEOUT); + semaphore.wait(1); +} + +CVK_TEST(TimelineSemaphore, submit_signals_the_next_value) { + test::Context const context{}; + TimelineSemaphore const semaphore{context.core, create}; + + VkSemaphoreSubmitInfo const signalInfo{ + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO, + .pNext = nullptr, + .semaphore = semaphore.get(), + .value = semaphore.next(), + .stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, + .deviceIndex = 0, + }; + VkSubmitInfo2 const submit{ + .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2, + .pNext = nullptr, + .flags = 0, + .waitSemaphoreInfoCount = 0, + .pWaitSemaphoreInfos = nullptr, + .commandBufferInfoCount = 0, + .pCommandBufferInfos = nullptr, + .signalSemaphoreInfoCount = 1, + .pSignalSemaphoreInfos = &signalInfo, + }; + + context.core.queue(0).rawSubmit(submit); + + semaphore.wait(); + EXPECT_EQ(semaphore.gpuValue(), signalInfo.value); +} + +CVK_TEST(TimelineSemaphore, release_and_wrap_keep_the_counter) { + test::Context const context{}; + TimelineSemaphore semaphore{context.core, create}; + [[maybe_unused]] auto const signaled = semaphore.signal(); + + auto const state = semaphore.release(); + EXPECT_FALSE(semaphore.created()); + EXPECT_EQ(semaphore.value(), 0); + EXPECT_EQ(state.value, 1); + + TimelineSemaphore const wrapped{context.core, state}; + EXPECT_EQ(wrapped.get(), state.vkSemaphore.get()); + EXPECT_EQ(wrapped.value(), 1); + EXPECT_EQ(wrapped.gpuValue(), 1); +} + +CVK_TEST(TimelineSemaphore, move_keeps_the_counter) { + test::Context const context{}; + TimelineSemaphore semaphore{context.core, create}; + semaphore.create(3); + + TimelineSemaphore const moved{std::move(semaphore)}; + + EXPECT_FALSE(semaphore.created()); + EXPECT_EQ(moved.value(), 3); + EXPECT_EQ(moved.gpuValue(), 3); +} + +CVK_TEST(TimelineSemaphore, move_assignment_destroys_the_old_semaphore) { + test::Context const context{}; + TimelineSemaphore semaphore{context.core, create}; + TimelineSemaphore other{context.core, create}; + other.create(7); + auto const vkSemaphore = other.get(); + + semaphore = std::move(other); + + EXPECT_EQ(semaphore.get(), vkSemaphore); + EXPECT_EQ(semaphore.value(), 7); + EXPECT_FALSE(other.created()); +} + +CVK_TEST(TimelineSemaphore, is_not_a_binary_semaphore) { + static_assert(!std::convertible_to); + static_assert(!std::convertible_to); +} +} diff --git a/cvk/tests/src/shaders.hpp b/cvk/tests/src/shaders.hpp new file mode 100644 index 00000000..0bda3bfb --- /dev/null +++ b/cvk/tests/src/shaders.hpp @@ -0,0 +1,203 @@ +#pragma once +#include +#include + +/** + * generated from cvk/tests/shaders, regenerate with: + * glslc --target-env=vulkan1.3 -O -mfmt=num -o .num + */ +namespace cvk::test::shaders { +/** + * @brief vertex shader: location 0 vec3 position, set 0 binding 0 uniform buffer + */ +inline constexpr std::array TRIANGLE_VERT{ + 0x07230203, 0x00010600, 0x000d000b, 0x00000023, 0x00000000, 0x00020011, 0x00000001, 0x0006000b, + 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001, + 0x0008000f, 0x00000000, 0x00000004, 0x6e69616d, 0x00000000, 0x0000000d, 0x00000013, 0x00000019, + 0x00030047, 0x0000000b, 0x00000002, 0x00050048, 0x0000000b, 0x00000000, 0x0000000b, 0x00000000, + 0x00050048, 0x0000000b, 0x00000001, 0x0000000b, 0x00000001, 0x00050048, 0x0000000b, 0x00000002, + 0x0000000b, 0x00000003, 0x00050048, 0x0000000b, 0x00000003, 0x0000000b, 0x00000004, 0x00030047, + 0x00000011, 0x00000002, 0x00040048, 0x00000011, 0x00000000, 0x00000005, 0x00050048, 0x00000011, + 0x00000000, 0x00000007, 0x00000010, 0x00050048, 0x00000011, 0x00000000, 0x00000023, 0x00000000, + 0x00040047, 0x00000013, 0x00000021, 0x00000000, 0x00040047, 0x00000013, 0x00000022, 0x00000000, + 0x00040047, 0x00000019, 0x0000001e, 0x00000000, 0x00020013, 0x00000002, 0x00030021, 0x00000003, + 0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040017, 0x00000007, 0x00000006, 0x00000004, + 0x00040015, 0x00000008, 0x00000020, 0x00000000, 0x0004002b, 0x00000008, 0x00000009, 0x00000001, + 0x0004001c, 0x0000000a, 0x00000006, 0x00000009, 0x0006001e, 0x0000000b, 0x00000007, 0x00000006, + 0x0000000a, 0x0000000a, 0x00040020, 0x0000000c, 0x00000003, 0x0000000b, 0x0004003b, 0x0000000c, + 0x0000000d, 0x00000003, 0x00040015, 0x0000000e, 0x00000020, 0x00000001, 0x0004002b, 0x0000000e, + 0x0000000f, 0x00000000, 0x00040018, 0x00000010, 0x00000007, 0x00000004, 0x0003001e, 0x00000011, + 0x00000010, 0x00040020, 0x00000012, 0x00000002, 0x00000011, 0x0004003b, 0x00000012, 0x00000013, + 0x00000002, 0x00040020, 0x00000014, 0x00000002, 0x00000010, 0x00040017, 0x00000017, 0x00000006, + 0x00000003, 0x00040020, 0x00000018, 0x00000001, 0x00000017, 0x0004003b, 0x00000018, 0x00000019, + 0x00000001, 0x0004002b, 0x00000006, 0x0000001b, 0x3f800000, 0x00040020, 0x00000021, 0x00000003, + 0x00000007, 0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003, 0x000200f8, 0x00000005, + 0x00050041, 0x00000014, 0x00000015, 0x00000013, 0x0000000f, 0x0004003d, 0x00000010, 0x00000016, + 0x00000015, 0x0004003d, 0x00000017, 0x0000001a, 0x00000019, 0x00050051, 0x00000006, 0x0000001c, + 0x0000001a, 0x00000000, 0x00050051, 0x00000006, 0x0000001d, 0x0000001a, 0x00000001, 0x00050051, + 0x00000006, 0x0000001e, 0x0000001a, 0x00000002, 0x00070050, 0x00000007, 0x0000001f, 0x0000001c, + 0x0000001d, 0x0000001e, 0x0000001b, 0x00050091, 0x00000007, 0x00000020, 0x00000016, 0x0000001f, + 0x00050041, 0x00000021, 0x00000022, 0x0000000d, 0x0000000f, 0x0003003e, 0x00000022, 0x00000020, + 0x000100fd, 0x00010038, +}; + +/** + * @brief fragment shader: push constant vec4 color, location 0 color output + */ +inline constexpr std::array TRIANGLE_FRAG{ + 0x07230203, 0x00010600, 0x000d000b, 0x00000012, 0x00000000, 0x00020011, 0x00000001, 0x0006000b, + 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001, + 0x0007000f, 0x00000004, 0x00000004, 0x6e69616d, 0x00000000, 0x00000009, 0x0000000c, 0x00030010, + 0x00000004, 0x00000007, 0x00040047, 0x00000009, 0x0000001e, 0x00000000, 0x00030047, 0x0000000a, + 0x00000002, 0x00050048, 0x0000000a, 0x00000000, 0x00000023, 0x00000000, 0x00020013, 0x00000002, + 0x00030021, 0x00000003, 0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040017, 0x00000007, + 0x00000006, 0x00000004, 0x00040020, 0x00000008, 0x00000003, 0x00000007, 0x0004003b, 0x00000008, + 0x00000009, 0x00000003, 0x0003001e, 0x0000000a, 0x00000007, 0x00040020, 0x0000000b, 0x00000009, + 0x0000000a, 0x0004003b, 0x0000000b, 0x0000000c, 0x00000009, 0x00040015, 0x0000000d, 0x00000020, + 0x00000001, 0x0004002b, 0x0000000d, 0x0000000e, 0x00000000, 0x00040020, 0x0000000f, 0x00000009, + 0x00000007, 0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003, 0x000200f8, 0x00000005, + 0x00050041, 0x0000000f, 0x00000010, 0x0000000c, 0x0000000e, 0x0004003d, 0x00000007, 0x00000011, + 0x00000010, 0x0003003e, 0x00000009, 0x00000011, 0x000100fd, 0x00010038, +}; + +/** + * @brief vertex shader: location 0 vec3 position, no descriptors + */ +inline constexpr std::array FILL_VERT{ + 0x07230203, 0x00010600, 0x000d000b, 0x0000001b, 0x00000000, 0x00020011, 0x00000001, 0x0006000b, + 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001, + 0x0007000f, 0x00000000, 0x00000004, 0x6e69616d, 0x00000000, 0x0000000d, 0x00000012, 0x00030047, + 0x0000000b, 0x00000002, 0x00050048, 0x0000000b, 0x00000000, 0x0000000b, 0x00000000, 0x00050048, + 0x0000000b, 0x00000001, 0x0000000b, 0x00000001, 0x00050048, 0x0000000b, 0x00000002, 0x0000000b, + 0x00000003, 0x00050048, 0x0000000b, 0x00000003, 0x0000000b, 0x00000004, 0x00040047, 0x00000012, + 0x0000001e, 0x00000000, 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002, 0x00030016, + 0x00000006, 0x00000020, 0x00040017, 0x00000007, 0x00000006, 0x00000004, 0x00040015, 0x00000008, + 0x00000020, 0x00000000, 0x0004002b, 0x00000008, 0x00000009, 0x00000001, 0x0004001c, 0x0000000a, + 0x00000006, 0x00000009, 0x0006001e, 0x0000000b, 0x00000007, 0x00000006, 0x0000000a, 0x0000000a, + 0x00040020, 0x0000000c, 0x00000003, 0x0000000b, 0x0004003b, 0x0000000c, 0x0000000d, 0x00000003, + 0x00040015, 0x0000000e, 0x00000020, 0x00000001, 0x0004002b, 0x0000000e, 0x0000000f, 0x00000000, + 0x00040017, 0x00000010, 0x00000006, 0x00000003, 0x00040020, 0x00000011, 0x00000001, 0x00000010, + 0x0004003b, 0x00000011, 0x00000012, 0x00000001, 0x0004002b, 0x00000006, 0x00000014, 0x3f800000, + 0x00040020, 0x00000019, 0x00000003, 0x00000007, 0x00050036, 0x00000002, 0x00000004, 0x00000000, + 0x00000003, 0x000200f8, 0x00000005, 0x0004003d, 0x00000010, 0x00000013, 0x00000012, 0x00050051, + 0x00000006, 0x00000015, 0x00000013, 0x00000000, 0x00050051, 0x00000006, 0x00000016, 0x00000013, + 0x00000001, 0x00050051, 0x00000006, 0x00000017, 0x00000013, 0x00000002, 0x00070050, 0x00000007, + 0x00000018, 0x00000015, 0x00000016, 0x00000017, 0x00000014, 0x00050041, 0x00000019, 0x0000001a, + 0x0000000d, 0x0000000f, 0x0003003e, 0x0000001a, 0x00000018, 0x000100fd, 0x00010038, +}; + +/** + * @brief fragment shader: set 0 binding 0 uniform buffer vec4 color, location 0 color output + */ +inline constexpr std::array UNIFORM_COLOR_FRAG{ + 0x07230203, 0x00010600, 0x000d000b, 0x00000012, 0x00000000, 0x00020011, 0x00000001, 0x0006000b, + 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001, + 0x0007000f, 0x00000004, 0x00000004, 0x6e69616d, 0x00000000, 0x00000009, 0x0000000c, 0x00030010, + 0x00000004, 0x00000007, 0x00040047, 0x00000009, 0x0000001e, 0x00000000, 0x00030047, 0x0000000a, + 0x00000002, 0x00050048, 0x0000000a, 0x00000000, 0x00000023, 0x00000000, 0x00040047, 0x0000000c, + 0x00000021, 0x00000000, 0x00040047, 0x0000000c, 0x00000022, 0x00000000, 0x00020013, 0x00000002, + 0x00030021, 0x00000003, 0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040017, 0x00000007, + 0x00000006, 0x00000004, 0x00040020, 0x00000008, 0x00000003, 0x00000007, 0x0004003b, 0x00000008, + 0x00000009, 0x00000003, 0x0003001e, 0x0000000a, 0x00000007, 0x00040020, 0x0000000b, 0x00000002, + 0x0000000a, 0x0004003b, 0x0000000b, 0x0000000c, 0x00000002, 0x00040015, 0x0000000d, 0x00000020, + 0x00000001, 0x0004002b, 0x0000000d, 0x0000000e, 0x00000000, 0x00040020, 0x0000000f, 0x00000002, + 0x00000007, 0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003, 0x000200f8, 0x00000005, + 0x00050041, 0x0000000f, 0x00000010, 0x0000000c, 0x0000000e, 0x0004003d, 0x00000007, 0x00000011, + 0x00000010, 0x0003003e, 0x00000009, 0x00000011, 0x000100fd, 0x00010038, +}; + +/** + * @brief fragment shader: set 0 binding 0 runtime array of combined image samplers, push constant uint index + */ +inline constexpr std::array BINDLESS_FRAG{ + 0x07230203, 0x00010600, 0x000d000b, 0x00000020, 0x00000000, 0x00020011, 0x00000001, 0x00020011, + 0x000014b5, 0x00020011, 0x000014b6, 0x00020011, 0x000014bb, 0x0006000b, 0x00000001, 0x4c534c47, + 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001, 0x0008000f, 0x00000004, + 0x00000004, 0x6e69616d, 0x00000000, 0x00000009, 0x0000000e, 0x00000012, 0x00030010, 0x00000004, + 0x00000007, 0x00040047, 0x00000009, 0x0000001e, 0x00000000, 0x00040047, 0x0000000e, 0x00000021, + 0x00000000, 0x00040047, 0x0000000e, 0x00000022, 0x00000000, 0x00030047, 0x00000010, 0x00000002, + 0x00050048, 0x00000010, 0x00000000, 0x00000023, 0x00000000, 0x00030047, 0x00000018, 0x000014b4, + 0x00030047, 0x0000001a, 0x000014b4, 0x00030047, 0x0000001b, 0x000014b4, 0x00020013, 0x00000002, + 0x00030021, 0x00000003, 0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040017, 0x00000007, + 0x00000006, 0x00000004, 0x00040020, 0x00000008, 0x00000003, 0x00000007, 0x0004003b, 0x00000008, + 0x00000009, 0x00000003, 0x00090019, 0x0000000a, 0x00000006, 0x00000001, 0x00000000, 0x00000000, + 0x00000000, 0x00000001, 0x00000000, 0x0003001b, 0x0000000b, 0x0000000a, 0x0003001d, 0x0000000c, + 0x0000000b, 0x00040020, 0x0000000d, 0x00000000, 0x0000000c, 0x0004003b, 0x0000000d, 0x0000000e, + 0x00000000, 0x00040015, 0x0000000f, 0x00000020, 0x00000000, 0x0003001e, 0x00000010, 0x0000000f, + 0x00040020, 0x00000011, 0x00000009, 0x00000010, 0x0004003b, 0x00000011, 0x00000012, 0x00000009, + 0x00040015, 0x00000013, 0x00000020, 0x00000001, 0x0004002b, 0x00000013, 0x00000014, 0x00000000, + 0x00040020, 0x00000015, 0x00000009, 0x0000000f, 0x00040020, 0x00000019, 0x00000000, 0x0000000b, + 0x00040017, 0x0000001c, 0x00000006, 0x00000002, 0x0004002b, 0x00000006, 0x0000001d, 0x3f000000, + 0x0005002c, 0x0000001c, 0x0000001e, 0x0000001d, 0x0000001d, 0x00050036, 0x00000002, 0x00000004, + 0x00000000, 0x00000003, 0x000200f8, 0x00000005, 0x00050041, 0x00000015, 0x00000016, 0x00000012, + 0x00000014, 0x0004003d, 0x0000000f, 0x00000017, 0x00000016, 0x00040053, 0x0000000f, 0x00000018, + 0x00000017, 0x00050041, 0x00000019, 0x0000001a, 0x0000000e, 0x00000018, 0x0004003d, 0x0000000b, + 0x0000001b, 0x0000001a, 0x00050057, 0x00000007, 0x0000001f, 0x0000001b, 0x0000001e, 0x0003003e, + 0x00000009, 0x0000001f, 0x000100fd, 0x00010038, +}; + +/** + * @brief fragment shader: set 0 binding 0 input attachment index 0, location 0 color output of its value + */ +inline constexpr std::array LOCAL_READ_FRAG{ + 0x07230203, 0x00010600, 0x000d000b, 0x00000013, 0x00000000, 0x00020011, 0x00000001, 0x00020011, + 0x00000028, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, + 0x00000000, 0x00000001, 0x0007000f, 0x00000004, 0x00000004, 0x6e69616d, 0x00000000, 0x00000009, + 0x0000000c, 0x00030010, 0x00000004, 0x00000007, 0x00040047, 0x00000009, 0x0000001e, 0x00000000, + 0x00040047, 0x0000000c, 0x00000021, 0x00000000, 0x00040047, 0x0000000c, 0x00000022, 0x00000000, + 0x00040047, 0x0000000c, 0x0000002b, 0x00000000, 0x00020013, 0x00000002, 0x00030021, 0x00000003, + 0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040017, 0x00000007, 0x00000006, 0x00000004, + 0x00040020, 0x00000008, 0x00000003, 0x00000007, 0x0004003b, 0x00000008, 0x00000009, 0x00000003, + 0x00090019, 0x0000000a, 0x00000006, 0x00000006, 0x00000000, 0x00000000, 0x00000000, 0x00000002, + 0x00000000, 0x00040020, 0x0000000b, 0x00000000, 0x0000000a, 0x0004003b, 0x0000000b, 0x0000000c, + 0x00000000, 0x00040015, 0x0000000e, 0x00000020, 0x00000001, 0x0004002b, 0x0000000e, 0x0000000f, + 0x00000000, 0x00040017, 0x00000010, 0x0000000e, 0x00000002, 0x0005002c, 0x00000010, 0x00000011, + 0x0000000f, 0x0000000f, 0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003, 0x000200f8, + 0x00000005, 0x0004003d, 0x0000000a, 0x0000000d, 0x0000000c, 0x00050062, 0x00000007, 0x00000012, + 0x0000000d, 0x00000011, 0x0003003e, 0x00000009, 0x00000012, 0x000100fd, 0x00010038, +}; + +/** + * @brief fragment shader: set 0 binding 0 combined image sampler, location 0 color output of its center + */ +inline constexpr std::array TEXTURE_FRAG{ + 0x07230203, 0x00010600, 0x000d000b, 0x00000013, 0x00000000, 0x00020011, 0x00000001, 0x0006000b, + 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001, + 0x0007000f, 0x00000004, 0x00000004, 0x6e69616d, 0x00000000, 0x00000009, 0x0000000d, 0x00030010, + 0x00000004, 0x00000007, 0x00040047, 0x00000009, 0x0000001e, 0x00000000, 0x00040047, 0x0000000d, + 0x00000021, 0x00000000, 0x00040047, 0x0000000d, 0x00000022, 0x00000000, 0x00020013, 0x00000002, + 0x00030021, 0x00000003, 0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040017, 0x00000007, + 0x00000006, 0x00000004, 0x00040020, 0x00000008, 0x00000003, 0x00000007, 0x0004003b, 0x00000008, + 0x00000009, 0x00000003, 0x00090019, 0x0000000a, 0x00000006, 0x00000001, 0x00000000, 0x00000000, + 0x00000000, 0x00000001, 0x00000000, 0x0003001b, 0x0000000b, 0x0000000a, 0x00040020, 0x0000000c, + 0x00000000, 0x0000000b, 0x0004003b, 0x0000000c, 0x0000000d, 0x00000000, 0x00040017, 0x0000000f, + 0x00000006, 0x00000002, 0x0004002b, 0x00000006, 0x00000010, 0x3f000000, 0x0005002c, 0x0000000f, + 0x00000011, 0x00000010, 0x00000010, 0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003, + 0x000200f8, 0x00000005, 0x0004003d, 0x0000000b, 0x0000000e, 0x0000000d, 0x00050057, 0x00000007, + 0x00000012, 0x0000000e, 0x00000011, 0x0003003e, 0x00000009, 0x00000012, 0x000100fd, 0x00010038, +}; + +/** + * @brief compute shader: local size 64, set 0 binding 0 storage buffer uint values[], value i = 3 * i + */ +inline constexpr std::array VALUES_COMP{ + 0x07230203, 0x00010600, 0x000d000b, 0x0000001a, 0x00000000, 0x00020011, 0x00000001, 0x0006000b, + 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001, + 0x0007000f, 0x00000005, 0x00000004, 0x6e69616d, 0x00000000, 0x0000000a, 0x0000000f, 0x00060010, + 0x00000004, 0x00000011, 0x00000040, 0x00000001, 0x00000001, 0x00040047, 0x00000007, 0x00000006, + 0x00000004, 0x00030047, 0x00000008, 0x00000002, 0x00050048, 0x00000008, 0x00000000, 0x00000023, + 0x00000000, 0x00040047, 0x0000000a, 0x00000021, 0x00000000, 0x00040047, 0x0000000a, 0x00000022, + 0x00000000, 0x00040047, 0x0000000f, 0x0000000b, 0x0000001c, 0x00020013, 0x00000002, 0x00030021, + 0x00000003, 0x00000002, 0x00040015, 0x00000006, 0x00000020, 0x00000000, 0x0003001d, 0x00000007, + 0x00000006, 0x0003001e, 0x00000008, 0x00000007, 0x00040020, 0x00000009, 0x0000000c, 0x00000008, + 0x0004003b, 0x00000009, 0x0000000a, 0x0000000c, 0x00040015, 0x0000000b, 0x00000020, 0x00000001, + 0x0004002b, 0x0000000b, 0x0000000c, 0x00000000, 0x00040017, 0x0000000d, 0x00000006, 0x00000003, + 0x00040020, 0x0000000e, 0x00000001, 0x0000000d, 0x0004003b, 0x0000000e, 0x0000000f, 0x00000001, + 0x0004002b, 0x00000006, 0x00000010, 0x00000000, 0x00040020, 0x00000011, 0x00000001, 0x00000006, + 0x0004002b, 0x00000006, 0x00000016, 0x00000003, 0x00040020, 0x00000018, 0x0000000c, 0x00000006, + 0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003, 0x000200f8, 0x00000005, 0x00050041, + 0x00000011, 0x00000012, 0x0000000f, 0x00000010, 0x0004003d, 0x00000006, 0x00000013, 0x00000012, + 0x00050084, 0x00000006, 0x00000017, 0x00000013, 0x00000016, 0x00060041, 0x00000018, 0x00000019, + 0x0000000a, 0x0000000c, 0x00000013, 0x0003003e, 0x00000019, 0x00000017, 0x000100fd, 0x00010038, +}; +} diff --git a/cvk/tests/src/surface/surface_test.cpp b/cvk/tests/src/surface/surface_test.cpp new file mode 100644 index 00000000..f0d8f76b --- /dev/null +++ b/cvk/tests/src/surface/surface_test.cpp @@ -0,0 +1,94 @@ +#include "context.hpp" +#include "headless.hpp" +#include "test.hpp" + +#include "cvk/base/instance.hpp" +#include "cvk/base/physical_device.hpp" +#include "cvk/surface/surface.hpp" +#include "cvk/utility/constants.hpp" +#include "cvk/utility/vk_overloads.hpp" + +#include +#include + + +namespace cvk { +CVK_TEST(Surface, wrap_takes_ownership) { + test::Context const context{}; + + auto surface = test::headless_surface(context.instance); + + ASSERT_TRUE(surface.created()); + EXPECT_NE(surface.get(), VK_NULL_HANDLE); + + surface.destroy(); + EXPECT_FALSE(surface.created()); +} + +CVK_TEST(Surface, release_keeps_the_surface) { + test::Context const context{}; + auto surface = test::headless_surface(context.instance); + + auto const state = surface.release(); + EXPECT_FALSE(surface.created()); + + Surface::destroy(context.instance.get(), state.vkSurface.get()); +} + +CVK_TEST(Surface, formats_are_every_offered_format) { + test::PresentContext const context{}; + auto const vkPhysicalDevice = context.core.physicalDevice().get(); + auto const vkSurface = context.surface().get(); + + uint32_t count = 0; + ASSERT_EQ(vkGetPhysicalDeviceSurfaceFormatsKHR(vkPhysicalDevice, vkSurface, &count, nullptr), VK_SUCCESS); + std::vector offered(count); + ASSERT_EQ(vkGetPhysicalDeviceSurfaceFormatsKHR(vkPhysicalDevice, vkSurface, &count, offered.data()), VK_SUCCESS); + + auto const formats = context.surface().formats(context.core.physicalDevice()); + + EXPECT_FALSE(formats.empty()); + EXPECT_EQ(formats, offered); +} + +CVK_TEST(Surface, present_modes_are_every_offered_present_mode) { + test::PresentContext const context{}; + auto const vkPhysicalDevice = context.core.physicalDevice().get(); + auto const vkSurface = context.surface().get(); + + uint32_t count = 0; + ASSERT_EQ(vkGetPhysicalDeviceSurfacePresentModesKHR(vkPhysicalDevice, vkSurface, &count, nullptr), VK_SUCCESS); + std::vector offered(count); + ASSERT_EQ( + vkGetPhysicalDeviceSurfacePresentModesKHR(vkPhysicalDevice, vkSurface, &count, offered.data()), + VK_SUCCESS + ); + + auto const presentModes = context.surface().presentModes(context.core.physicalDevice()); + + EXPECT_TRUE(std::ranges::contains(presentModes, VK_PRESENT_MODE_FIFO_KHR)); + EXPECT_EQ(presentModes, offered); +} + +CVK_TEST(Surface, capabilities) { + test::PresentContext const context{}; + + auto const capabilities = context.surface().capabilities(context.core.physicalDevice()); + + EXPECT_GE(capabilities.minImageCount, 1); +} + +CVK_TEST(Surface, move_assignment_destroys_the_old_surface) { + if constexpr(!constants::ENABLE_VALIDATION_LAYERS) GTEST_SKIP() << "needs the validation layers"; + + test::InstanceContext const context{test::headless_extensions()}; + auto surface = test::headless_surface(context.instance); + auto other = test::headless_surface(context.instance); + auto const vkSurface = other.get(); + + surface = std::move(other); + + EXPECT_EQ(surface.get(), vkSurface); + EXPECT_FALSE(other.created()); +} +} diff --git a/cvk/tests/src/surface/swapchain_test.cpp b/cvk/tests/src/surface/swapchain_test.cpp new file mode 100644 index 00000000..70a017dd --- /dev/null +++ b/cvk/tests/src/surface/swapchain_test.cpp @@ -0,0 +1,378 @@ +#include "context.hpp" +#include "headless.hpp" +#include "test.hpp" + +#include "cvk/base/queue/queue.hpp" +#include "cvk/render/cmd/cmd_buffer.hpp" +#include "cvk/render/cmd/cmd_pool.hpp" +#include "cvk/render/sync/barrier.hpp" +#include "cvk/render/sync/fence.hpp" +#include "cvk/render/sync/semaphore.hpp" +#include "cvk/surface/surface.hpp" +#include "cvk/surface/swapchain.hpp" +#include "cvk/utility/vk_exceptions.hpp" + +#include +#include +#include +#include +#include + + +namespace cvk { +namespace { + constexpr VkExtent2D EXTENT{64, 64}; + constexpr uint32_t ANY = std::numeric_limits::max(); + + constexpr VkSurfaceCapabilitiesKHR capabilities( + VkExtent2D currentExtent, + uint32_t minImageCount = 2, + uint32_t maxImageCount = 0 + ) { + return VkSurfaceCapabilitiesKHR{ + .minImageCount = minImageCount, + .maxImageCount = maxImageCount, + .currentExtent = currentExtent, + .minImageExtent = {16, 16}, + .maxImageExtent = {1024, 1024}, + .maxImageArrayLayers = 1, + .supportedTransforms = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR, + .currentTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR, + .supportedCompositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR, + .supportedUsageFlags = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, + }; + } + + /** + * @brief acquires an image, clears it and presents it + * @return result of @ref Swapchain::present() + */ + VkResult present_cleared_image(test::PresentContext const& context, Swapchain& swapchain) { + Semaphore const acquired{context.core, create}; + auto const [result, index] = swapchain.acquire(acquired); + EXPECT_TRUE(index.has_value()) << "acquire result: " << result; + if(!index.has_value()) return result; + + auto const& image = swapchain.images()[*index]; + CmdPool const pool{context.core, test::pool_config(context.core), create}; + PrimaryCmdBuffer cmd{pool, create}; + + cmd.begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); + + Barrier toAttachment{}; + toAttachment.addTransition(image, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED); + toAttachment.execute(cmd); + + cmd.beginRendering( + { + .colorAttachments = { + RenderingAttachment{ + .view = &swapchain.views()[*index], + .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR, + .clearValue = {.color = {.float32 = {0.f, 0.f, 1.f, 1.f}}}, + }, + }, + } + ); + cmd.endRendering(); + + Barrier toPresent{}; + toPresent.addTransition(image, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + toPresent.execute(cmd); + + cmd.end(); + + Fence const fence{context.core, create}; + context.queue().submit( + { + .cmdBuffers = {&cmd}, + .waits = {{acquired, VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT}}, + .signals = {swapchain.presentSemaphore(*index)}, + }, + &fence + ); + auto const presented = swapchain.present(*index); + fence.wait(); + + return presented; + } + + /** + * @brief waits for the acquire semaphore of an image that was not presented + */ + void consume(test::PresentContext const& context, Semaphore const& acquired) { + Fence const fence{context.core, create}; + context.queue().submit({.waits = {acquired}}, &fence); + fence.wait(); + } +} + +CVK_TEST(Swapchain, eval_extent) { + static_assert(Swapchain::evalExtent(EXTENT, capabilities({800, 600})).width == 800); + static_assert(Swapchain::evalExtent(EXTENT, capabilities({ANY, ANY})).width == EXTENT.width); + static_assert(Swapchain::evalExtent({4096, 8}, capabilities({ANY, ANY})).width == 1024); + static_assert(Swapchain::evalExtent({4096, 8}, capabilities({ANY, ANY})).height == 16); +} + +CVK_TEST(Swapchain, eval_image_count) { + static_assert(Swapchain::evalImageCount(0, capabilities({ANY, ANY}, 2, 0)) == 3); + static_assert(Swapchain::evalImageCount(0, capabilities({ANY, ANY}, 2, 2)) == 2); + static_assert(Swapchain::evalImageCount(1, capabilities({ANY, ANY}, 2, 8)) == 2); + static_assert(Swapchain::evalImageCount(5, capabilities({ANY, ANY}, 2, 0)) == 5); +} + +CVK_TEST(Swapchain, base_constructor_does_not_create) { + test::PresentContext const context{}; + + Swapchain const swapchain{context.core, context.queue(), context.surface(), {}}; + + EXPECT_FALSE(swapchain.created()); + EXPECT_EQ(swapchain.imageCount(), 0); +} + +CVK_TEST(Swapchain, create_and_destroy) { + test::PresentContext const context{}; + auto const capabilities = context.surface().capabilities(context.core.physicalDevice()); + + Swapchain swapchain{context.core, context.queue(), context.surface(), {}, EXTENT}; + + ASSERT_TRUE(swapchain.created()); + EXPECT_GE(swapchain.imageCount(), capabilities.minImageCount); + EXPECT_EQ(swapchain.views().size(), swapchain.imageCount()); + EXPECT_EQ(swapchain.extent().width, Swapchain::evalExtent(EXTENT, capabilities).width); + auto const offered = context.surface().formats(context.core.physicalDevice()); + EXPECT_TRUE(std::ranges::contains(offered, VkSurfaceFormatKHR{swapchain.format(), swapchain.colorSpace()})); + EXPECT_EQ(swapchain.images()[0].aspectMask(), VK_IMAGE_ASPECT_COLOR_BIT); + + swapchain.destroy(); + EXPECT_FALSE(swapchain.created()); + EXPECT_EQ(swapchain.imageCount(), 0); +} + +CVK_TEST(Swapchain, create_picks_the_first_allowed_format_the_surface_offers) { + test::PresentContext const context{}; + auto const offered = context.surface().formats(context.core.physicalDevice()); + if(offered.size() < 2) GTEST_SKIP() << "needs two surface formats"; + + Swapchain::Config const config{ + .formats = {{VK_FORMAT_R64G64B64A64_SFLOAT, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR}, offered[1], offered[0]}, + }; + Swapchain const swapchain{context.core, context.queue(), context.surface(), config, EXTENT}; + + EXPECT_EQ(swapchain.format(), offered[1].format); + EXPECT_EQ(swapchain.colorSpace(), offered[1].colorSpace); +} + +CVK_TEST(Swapchain, create_without_an_offered_format_throws) { + test::PresentContext const context{}; + Swapchain::Config const config{.formats = {{VK_FORMAT_R64G64B64A64_SFLOAT, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR}}}; + Swapchain swapchain{context.core, context.queue(), context.surface(), config}; + + EXPECT_THROW(swapchain.create(EXTENT), cvk_exception); + EXPECT_FALSE(swapchain.created()); +} + +CVK_TEST(Swapchain, create_without_an_offered_present_mode_throws) { + test::PresentContext const context{}; + Swapchain::Config const config{.presentModes = {VK_PRESENT_MODE_SHARED_DEMAND_REFRESH_KHR}}; + Swapchain swapchain{context.core, context.queue(), context.surface(), config}; + + EXPECT_THROW(swapchain.create(EXTENT), cvk_exception); + EXPECT_FALSE(swapchain.created()); +} + +CVK_TEST(Swapchain, empty_lists_take_the_first_format_and_fifo) { + test::PresentContext const context{}; + auto const offered = context.surface().formats(context.core.physicalDevice()); + + Swapchain::Config const config{.formats = {}, .presentModes = {}}; + Swapchain const swapchain{context.core, context.queue(), context.surface(), config, EXTENT}; + + EXPECT_EQ(swapchain.format(), offered[0].format); + EXPECT_EQ(swapchain.colorSpace(), offered[0].colorSpace); + EXPECT_EQ(swapchain.presentMode(), VK_PRESENT_MODE_FIFO_KHR); +} + +CVK_TEST(Swapchain, acquire_clear_present) { + test::PresentContext const context{}; + Swapchain swapchain{context.core, context.queue(), context.surface(), {}, EXTENT}; + + for(uint32_t frame = 0; frame < swapchain.imageCount() + 1; ++frame) { + auto const result = present_cleared_image(context, swapchain); + EXPECT_TRUE(result == VK_SUCCESS || result == VK_SUBOPTIMAL_KHR) << "present result: " << result; + } +} + +CVK_TEST(Swapchain, resize_retires_the_old_swapchain) { + test::PresentContext const context{}; + Swapchain swapchain{context.core, context.queue(), context.surface(), {}, EXTENT}; + EXPECT_EQ(present_cleared_image(context, swapchain), VK_SUCCESS); + + swapchain.resize({32, 32}); + + EXPECT_EQ(swapchain.retired(), 1); + EXPECT_EQ(swapchain.extent().width, 32); + EXPECT_EQ(present_cleared_image(context, swapchain), VK_SUCCESS); +} + +CVK_TEST(Swapchain, resize_keeps_the_format_and_the_present_mode) { + test::PresentContext const context{}; + auto const offered = context.surface().formats(context.core.physicalDevice()); + if(offered.size() < 2) GTEST_SKIP() << "needs two surface formats"; + + Swapchain created{context.core, context.queue(), context.surface(), {.formats = {offered[1]}}, EXTENT}; + auto const state = created.release(); + // the config prefers another format -> a resize that picks again changes the format + Swapchain swapchain{context.core, context.queue(), context.surface(), {.formats = {offered[0]}}, state}; + + swapchain.resize({32, 32}); + + EXPECT_EQ(swapchain.format(), offered[1].format); + EXPECT_EQ(swapchain.colorSpace(), offered[1].colorSpace); + EXPECT_EQ(swapchain.presentMode(), state.presentMode); + EXPECT_EQ(present_cleared_image(context, swapchain), VK_SUCCESS); +} + +CVK_TEST(Swapchain, recreate_applies_the_config) { + test::PresentContext const context{}; + auto const& physicalDevice = context.core.physicalDevice(); + auto const formats = context.surface().formats(physicalDevice); + auto const presentModes = context.surface().presentModes(physicalDevice); + if(formats.size() < 2 || presentModes.size() < 2) GTEST_SKIP() << "needs two surface formats and present modes"; + + Swapchain::Config const config{.formats = {formats[0]}, .presentModes = {presentModes[0]}}; + Swapchain swapchain{context.core, context.queue(), context.surface(), config, EXTENT}; + EXPECT_EQ(present_cleared_image(context, swapchain), VK_SUCCESS); + + swapchain.recreate({32, 32}, {.formats = {formats[1]}, .presentModes = {presentModes[1]}}); + + EXPECT_EQ(swapchain.format(), formats[1].format); + EXPECT_EQ(swapchain.colorSpace(), formats[1].colorSpace); + EXPECT_EQ(swapchain.presentMode(), presentModes[1]); + EXPECT_EQ(swapchain.config().presentModes, std::vector{presentModes[1]}); + EXPECT_EQ(swapchain.retired(), 1); + EXPECT_EQ(swapchain.extent().width, 32); + EXPECT_EQ(present_cleared_image(context, swapchain), VK_SUCCESS); +} + +CVK_TEST(Swapchain, recreate_without_an_offered_format_throws_and_keeps_the_swapchain) { + test::PresentContext const context{}; + Swapchain::Config const config{}; + Swapchain swapchain{context.core, context.queue(), context.surface(), config, EXTENT}; + auto const vkSwapchain = swapchain.get(); + auto const extent = swapchain.extent(); + auto const format = swapchain.format(); + auto const colorSpace = swapchain.colorSpace(); + auto const presentMode = swapchain.presentMode(); + + Swapchain::Config const unoffered{.formats = {{VK_FORMAT_R64G64B64A64_SFLOAT, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR}}}; + EXPECT_THROW(swapchain.recreate({32, 32}, unoffered), cvk_exception); + + ASSERT_TRUE(swapchain.created()); + EXPECT_EQ(swapchain.get(), vkSwapchain); + EXPECT_EQ(swapchain.extent(), extent); + EXPECT_EQ(swapchain.format(), format); + EXPECT_EQ(swapchain.colorSpace(), colorSpace); + EXPECT_EQ(swapchain.presentMode(), presentMode); + EXPECT_EQ(swapchain.config().formats, config.formats); + EXPECT_EQ(swapchain.config().presentModes, config.presentModes); + EXPECT_EQ(swapchain.retired(), 0); + EXPECT_EQ(present_cleared_image(context, swapchain), VK_SUCCESS); +} + +CVK_TEST(Swapchain, presents_destroy_the_retired_swapchain_after_its_present_fences_signal) { + test::PresentContext const context{}; + Swapchain swapchain{context.core, context.queue(), context.surface(), {}, EXTENT}; + EXPECT_EQ(present_cleared_image(context, swapchain), VK_SUCCESS); + swapchain.resize({32, 32}); + auto const vkSwapchain = swapchain.get(); + ASSERT_EQ(swapchain.retired(), 1); + + // the headless surface completes presents at once, the bound only ends a broken loop + for(uint32_t frame = 0; frame < 16 && swapchain.retired() != 0; ++frame) + EXPECT_EQ(present_cleared_image(context, swapchain), VK_SUCCESS); + + EXPECT_EQ(swapchain.retired(), 0); + EXPECT_EQ(swapchain.get(), vkSwapchain); +} + +CVK_TEST(Swapchain, unacquire_gives_the_image_back) { + test::PresentContext const context{}; + Swapchain swapchain{context.core, context.queue(), context.surface(), {}, EXTENT}; + Semaphore const acquired{context.core, create}; + + auto const [result, index] = swapchain.acquire(acquired); + ASSERT_TRUE(index.has_value()); + EXPECT_TRUE(swapchain.acquired(*index)); + + swapchain.unacquire(std::array{*index}); + + EXPECT_FALSE(swapchain.acquired(*index)); + consume(context, acquired); +} + +CVK_TEST(Swapchain, resize_unacquires_the_acquired_images) { + test::PresentContext const context{}; + Swapchain swapchain{context.core, context.queue(), context.surface(), {}, EXTENT}; + Semaphore const acquired{context.core, create}; + + auto const [result, index] = swapchain.acquire(acquired); + ASSERT_TRUE(index.has_value()); + + swapchain.resize({32, 32}); + + consume(context, acquired); + EXPECT_EQ(present_cleared_image(context, swapchain), VK_SUCCESS); +} + +CVK_TEST(Swapchain, release_and_wrap) { + test::PresentContext const context{}; + Swapchain swapchain{context.core, context.queue(), context.surface(), {}, EXTENT}; + auto const vkSwapchain = swapchain.get(); + auto const imageCount = swapchain.imageCount(); + + auto const state = swapchain.release(); + EXPECT_FALSE(swapchain.created()); + + Swapchain wrapped{context.core, context.queue(), context.surface(), {}, state}; + EXPECT_EQ(wrapped.get(), vkSwapchain); + EXPECT_EQ(wrapped.imageCount(), imageCount); + EXPECT_EQ(present_cleared_image(context, wrapped), VK_SUCCESS); +} + +CVK_TEST(Swapchain, move_assignment_destroys_the_old_swapchain) { + test::PresentContext const context{}; + auto const other = test::headless_surface(context.instance); + Swapchain swapchain{context.core, context.queue(), context.surface(), {}, EXTENT}; + Swapchain moved{context.core, context.queue(), other, {}, EXTENT}; + auto const vkSwapchain = moved.get(); + + swapchain = std::move(moved); + + EXPECT_EQ(swapchain.get(), vkSwapchain); + EXPECT_FALSE(moved.created()); + EXPECT_EQ(present_cleared_image(context, swapchain), VK_SUCCESS); +} + +CVK_TEST(Swapchain, move_assignment_over_a_retired_swapchain_with_a_present_in_flight) { + test::PresentContext const context{}; + auto const other = test::headless_surface(context.instance); + Swapchain swapchain{context.core, context.queue(), context.surface(), {}, EXTENT}; + // no acquire or present after the resize: the retired swapchain stays with its present fence + EXPECT_EQ(present_cleared_image(context, swapchain), VK_SUCCESS); + swapchain.resize({32, 32}); + ASSERT_EQ(swapchain.retired(), 1); + + Swapchain moved{context.core, context.queue(), other, {}, EXTENT}; + auto const vkSwapchain = moved.get(); + + swapchain = std::move(moved); + + ASSERT_TRUE(swapchain.created()); + EXPECT_EQ(swapchain.get(), vkSwapchain); + EXPECT_EQ(swapchain.retired(), 0); + EXPECT_EQ(swapchain.extent().width, EXTENT.width); + EXPECT_FALSE(moved.created()); + EXPECT_EQ(present_cleared_image(context, swapchain), VK_SUCCESS); +} +} diff --git a/cvk/tests/src/test.hpp b/cvk/tests/src/test.hpp new file mode 100644 index 00000000..7d0c9020 --- /dev/null +++ b/cvk/tests/src/test.hpp @@ -0,0 +1,27 @@ +#pragma once +#include "cvk/base/instance_config.hpp" + +#include + +#include +#include +#include +#include + +#define CVK_TEST(suite, test_name) S_TEST(cvk, suite, test_name) + +namespace cvk::test { +constexpr std::string_view APP_NAME = "cvk_tests"; + +/** + * @brief instance config with @ref APP_NAME + */ +[[nodiscard]] inline InstanceConfig instance_config(std::vector extensions = {}) { + return InstanceConfig{.appName = std::string{APP_NAME}, .extensions = std::move(extensions)}; +} + +/** + * @brief glsl sources of the test shaders + */ +inline std::filesystem::path const SHADER_DIR{CVK_TEST_SHADER_DIR}; +} diff --git a/cvk/tests/src/utility/device/device_features_test.cpp b/cvk/tests/src/utility/device/device_features_test.cpp new file mode 100644 index 00000000..91830df5 --- /dev/null +++ b/cvk/tests/src/utility/device/device_features_test.cpp @@ -0,0 +1,42 @@ +#include "test.hpp" + +#include "cvk/utility/constant/device_constants.hpp" +#include "cvk/utility/device/device_features.hpp" + +#include +#include +#include + + +namespace cvk { +CVK_TEST(DeviceFeatures, default_is_false) { + static_assert(std::ranges::none_of(dev::to_bits(DeviceFeatures{}), std::identity{})); +} + +CVK_TEST(DeviceFeatures, bits_follow_the_field_order) { + static_assert(dev::to_bits(DeviceFeatures{.robustBufferAccess = true})[0]); + static_assert(dev::to_bits(DeviceFeatures{.dynamicRenderingLocalRead = true})[dev::FEATURE_COUNT - 1]); + static_assert(dev::to_features(dev::to_bits(constants::BASE_DEVICE_FEATURES)) == constants::BASE_DEVICE_FEATURES); +} + +CVK_TEST(DeviceFeatures, merge_enables_the_features_of_both) { + static_assert([] { + DeviceFeatures features{.samplerAnisotropy = true}; + features.merge(DeviceFeatures{.synchronization2 = true}); + + return features.samplerAnisotropy && features.synchronization2 && !features.dynamicRendering; + }()); +} + +CVK_TEST(DeviceFeatures, missing_names_the_features) { + DeviceFeatures const available{.timelineSemaphore = true}; + DeviceFeatures const required{.samplerAnisotropy = true, .timelineSemaphore = true, .synchronization2 = true}; + + std::vector const expected{"samplerAnisotropy", "synchronization2"}; + EXPECT_EQ(available.missing(required), expected); +} + +CVK_TEST(DeviceFeatures, nothing_misses_in_itself) { + EXPECT_TRUE(constants::BASE_DEVICE_FEATURES.missing(constants::BASE_DEVICE_FEATURES).empty()); +} +} diff --git a/cvk/tests/src/utility/device/feature_chain_test.cpp b/cvk/tests/src/utility/device/feature_chain_test.cpp new file mode 100644 index 00000000..6278deab --- /dev/null +++ b/cvk/tests/src/utility/device/feature_chain_test.cpp @@ -0,0 +1,119 @@ +#include "test.hpp" + +#include "cvk/utility/constant/device_constants.hpp" +#include "cvk/utility/device/feature_chain.hpp" + +#include +#include +#include + + +namespace cvk::dev { +namespace { + using local_read_t = VkPhysicalDeviceDynamicRenderingLocalReadFeaturesKHR; + using maintenance1_t = VkPhysicalDeviceSwapchainMaintenance1FeaturesKHR; + + std::vector const LOCAL_READ{VK_KHR_DYNAMIC_RENDERING_LOCAL_READ_EXTENSION_NAME}; + std::vector const BOTH{ + VK_KHR_SWAPCHAIN_MAINTENANCE_1_EXTENSION_NAME, + VK_KHR_DYNAMIC_RENDERING_LOCAL_READ_EXTENSION_NAME, + }; + + std::vector const CORE{ + VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, + VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_FEATURES, + VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES, + VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES, + }; + + VkBaseInStructure const* first(FeatureChain const& chain) { + return reinterpret_cast(chain.get()); + } + + /** + * @return the structs of the chain in order, every struct lies in the chain object + */ + std::vector chain_types(FeatureChain const& chain) { + std::vector types{}; + auto const* const begin = reinterpret_cast(&chain); + auto const* const end = begin + sizeof(FeatureChain); + + for(auto const* node = first(chain); node != nullptr; node = node->pNext) { + auto const* const address = reinterpret_cast(node); + EXPECT_TRUE(address >= begin && address < end) << "the chain points out of its object"; + types.push_back(node->sType); + } + return types; + } + + /** + * @return the struct of the type in the chain, nullptr -> not in the chain + */ + template + T const* find(FeatureChain const& chain) { + for(auto const* node = first(chain); node != nullptr; node = node->pNext) + if(node->sType == feature_struct::TYPE) return reinterpret_cast(node); + return nullptr; + } +} + +CVK_TEST(FeatureChain, without_extensions_links_the_core_structs) { + FeatureChain const chain{std::vector{}}; + + EXPECT_EQ(chain_types(chain), CORE); + EXPECT_TRUE(chain.linked()); + EXPECT_FALSE(chain.linked()); + EXPECT_FALSE(chain.linked()); +} + +CVK_TEST(FeatureChain, links_the_structs_of_the_extensions) { + FeatureChain const localRead{LOCAL_READ}; + auto expected = CORE; + expected.push_back(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_LOCAL_READ_FEATURES_KHR); + EXPECT_EQ(chain_types(localRead), expected); + + FeatureChain const both{BOTH}; + expected = CORE; + expected.push_back(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SWAPCHAIN_MAINTENANCE_1_FEATURES_KHR); + expected.push_back(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_LOCAL_READ_FEATURES_KHR); + EXPECT_EQ(chain_types(both), expected); +} + +CVK_TEST(FeatureChain, is_not_copyable_or_movable) { + static_assert(!std::is_copy_constructible_v && !std::is_move_constructible_v); + static_assert(!std::is_copy_assignable_v && !std::is_move_assignable_v); +} + +CVK_TEST(FeatureChain, bits_go_into_their_vulkan_member) { + FeatureChain const chain{ + DeviceFeatures{.samplerAnisotropy = true, .synchronization2 = true, .dynamicRenderingLocalRead = true}, + LOCAL_READ + }; + + EXPECT_EQ(chain.get()->features.samplerAnisotropy, VK_TRUE); + EXPECT_EQ(chain.get()->features.robustBufferAccess, VK_FALSE); + + auto const* const features13 = find(chain); + ASSERT_NE(features13, nullptr); + EXPECT_EQ(features13->synchronization2, VK_TRUE); + EXPECT_EQ(features13->dynamicRendering, VK_FALSE); + + auto const* const localRead = find(chain); + ASSERT_NE(localRead, nullptr); + EXPECT_EQ(localRead->dynamicRenderingLocalRead, VK_TRUE); +} + +CVK_TEST(FeatureChain, features_round_trip) { + FeatureChain const chain{constants::BASE_DEVICE_FEATURES, LOCAL_READ}; + + EXPECT_EQ(chain.features(), constants::BASE_DEVICE_FEATURES); +} + +CVK_TEST(FeatureChain, extensions_of_the_features) { + static_assert(extensions_of(DeviceFeatures{.synchronization2 = true}).empty()); + static_assert(extensions_of(constants::PRESENT_DEVICE_FEATURES).size() == 1); + + std::vector const expected{VK_KHR_DYNAMIC_RENDERING_LOCAL_READ_EXTENSION_NAME}; + EXPECT_EQ(extensions_of(constants::BASE_DEVICE_FEATURES), expected); +} +} diff --git a/cvk/tests/src/utility/enumerate_test.cpp b/cvk/tests/src/utility/enumerate_test.cpp new file mode 100644 index 00000000..02ed0e52 --- /dev/null +++ b/cvk/tests/src/utility/enumerate_test.cpp @@ -0,0 +1,133 @@ +#include "test.hpp" + +#include "cvk/utility/enumerate.hpp" +#include "cvk/utility/vk_exceptions.hpp" + +#include + +#include +#include +#include +#include +#include + + +namespace cvk { +namespace { + constexpr std::array ELEMENTS{1, 2, 3, 4}; + + /** + * @brief acts like a Vulkan query with the available elements + * @return VK_INCOMPLETE -> data has less space than available + */ + VkResult query(std::span available, uint32_t* count, int* data) { + if(data == nullptr) { + *count = static_cast(available.size()); + return VK_SUCCESS; + } + + auto const written = std::min(*count, available.size()); + std::ranges::copy(available.first(written), data); + *count = static_cast(written); + return written < available.size() ? VK_INCOMPLETE : VK_SUCCESS; + } + + /** + * @brief 2 elements at the first call, 4 at the later calls -> the first write is incomplete + */ + VkResult growing(uint32_t* calls, uint32_t* count, int* data) { + auto const call = (*calls)++; + return query(std::span{ELEMENTS}.first(call == 0 ? 2 : 4), count, data); + } + + /** + * @brief 4 elements at the count call, 2 at the write call + */ + VkResult shrinking(uint32_t* calls, uint32_t* count, int* data) { + auto const call = (*calls)++; + return query(std::span{ELEMENTS}.first(call == 0 ? 4 : 2), count, data); + } + + /** + * @brief one element at every count call, VK_INCOMPLETE at every write call + */ + VkResult always_incomplete(uint32_t* calls, uint32_t* count, int* data) { + ++*calls; + if(data != nullptr) return VK_INCOMPLETE; + + *count = 1; + return VK_SUCCESS; + } + + /** + * @return VK_ERROR_OUT_OF_HOST_MEMORY at the call with the index failingCall + */ + VkResult failing(uint32_t failingCall, uint32_t* calls, uint32_t* count, int* data) { + if((*calls)++ == failingCall) return VK_ERROR_OUT_OF_HOST_MEMORY; + return query(ELEMENTS, count, data); + } + + /** + * @brief writes the first elementCount elements, like vkGetPhysicalDeviceQueueFamilyProperties() + */ + void void_query(uint32_t elementCount, uint32_t* count, int* data) { + [[maybe_unused]] auto const result = query(std::span{ELEMENTS}.first(elementCount), count, data); + } +} + +CVK_TEST(enumerate, element_type_is_the_pointee_of_the_last_parameter) { + static_assert(std::same_as, int>); + static_assert(std::same_as, VkPhysicalDevice>); + static_assert(std::same_as, VkImage>); +} + +CVK_TEST(enumerate, incomplete_repeats_until_complete) { + uint32_t calls = 0; + + auto const elements = dev::enumerate("fake elements", growing, &calls); + + EXPECT_EQ(calls, 4); + EXPECT_TRUE(std::ranges::equal(elements, ELEMENTS)); +} + +CVK_TEST(enumerate, incomplete_in_every_attempt_throws) { + uint32_t calls = 0; + + try { + [[maybe_unused]] auto const elements = dev::enumerate("fake elements", always_incomplete, &calls); + ADD_FAILURE() << "no throw"; + } catch(vk_result_exception const& exception) { + EXPECT_EQ(exception.vkResult(), VK_INCOMPLETE); + } + EXPECT_EQ(calls, 2 * dev::MAX_QUERY_ATTEMPTS); +} + +CVK_TEST(enumerate, fewer_written_elements_shrink_the_result) { + uint32_t calls = 0; + + auto const elements = dev::enumerate("fake elements", shrinking, &calls); + + EXPECT_EQ(calls, 2); + EXPECT_TRUE(std::ranges::equal(elements, std::span{ELEMENTS}.first(2))); +} + +CVK_TEST(enumerate, error_throws_the_result) { + // count call, write call + for(uint32_t const failingCall : {0u, 1u}) { + uint32_t calls = 0; + try { + [[maybe_unused]] auto const elements = dev::enumerate("fake elements", failing, failingCall, &calls); + ADD_FAILURE() << "no throw at call " << failingCall; + } catch(vk_result_exception const& exception) { + EXPECT_EQ(exception.vkResult(), VK_ERROR_OUT_OF_HOST_MEMORY); + } + EXPECT_EQ(calls, failingCall + 1); + } +} + +CVK_TEST(enumerate, void_function_returns_its_elements) { + auto const elements = dev::enumerate("fake elements", void_query, 3u); + + EXPECT_TRUE(std::ranges::equal(elements, std::span{ELEMENTS}.first(3))); +} +} diff --git a/cvk/tests/src/utility/handle/unique_handle_test.cpp b/cvk/tests/src/utility/handle/unique_handle_test.cpp new file mode 100644 index 00000000..5a8cc8dd --- /dev/null +++ b/cvk/tests/src/utility/handle/unique_handle_test.cpp @@ -0,0 +1,329 @@ +#include "context.hpp" +#include "headless.hpp" +#include "test.hpp" + +#include "cvk/base/instance.hpp" +#include "cvk/data/memory/memory.hpp" +#include "cvk/render/sync/fence.hpp" +#include "cvk/render/sync/timeline_semaphore.hpp" +#include "cvk/surface/surface.hpp" +#include "cvk/utility/constants.hpp" +#include "cvk/utility/handle/core_handle.hpp" +#include "cvk/utility/handle/instance_handle.hpp" +#include "cvk/utility/handle/unique_handle.hpp" +#include "cvk/utility/handle/vma_handle.hpp" + +#include +#include + + +namespace cvk { +namespace { + VkFence create_vk_fence(Core const& core) { + VkFenceCreateInfo const info{.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, .pNext = nullptr, .flags = 0}; + VkFence vkFence = VK_NULL_HANDLE; + [[maybe_unused]] auto const result = core.functions()->vkCreateFence(core.vkDevice(), &info, nullptr, &vkFence); + return vkFence; + } + + /** + * @brief owner with a destroy that needs the pool, like a command buffer + */ + struct PoolOwned { + using vk_handle_t = VkCommandBuffer; + + static void destroy(DeviceTable table, VkCommandPool vkPool, VkCommandBuffer vkBuffer) { + table->vkFreeCommandBuffers(table.device(), vkPool, 1, &vkBuffer); + } + }; + + /** + * @brief custom deleter: derives from core_deleter, adds the pool + */ + struct pool_deleter : core_deleter { + pool_deleter(Core const& core, VkCommandPool vkPool) : core_deleter{core}, pool{vkPool} {} + + void operator()(VkCommandBuffer vkBuffer) const { + PoolOwned::destroy(core().deviceTable(), pool, vkBuffer); + } + + VkCommandPool pool; + }; + + /** + * @brief two handles destroyed together, like a VMA image with its allocation + */ + struct pair_handle { + int* first = nullptr; + int* second = nullptr; + + pair_handle() = default; + pair_handle(std::nullptr_t) {} + pair_handle(int* first, int* second) : first{first}, second{second} {} + + // libstdc++ unique_ptr tests the pointer with if(ptr) + explicit operator bool() const { return first != nullptr; } + + friend bool operator==(pair_handle const&, pair_handle const&) = default; + friend bool operator==(pair_handle const& handle, std::nullptr_t) { return handle.first == nullptr; } + }; + + struct PairOwned { + using vk_handle_t = pair_handle; + + static inline int destroyed = 0; + + static void destroy(pair_handle handle) { + delete handle.first; + delete handle.second; + ++destroyed; + } + }; + + struct pair_deleter : handle_deleter { + using pointer = pair_handle; + }; + + /** + * @brief owner of heap memory, constant evaluation rejects leaks and double frees + */ + struct HeapOwned { + using vk_handle_t = int*; + + static constexpr void destroy(int* ptr) { delete ptr; } + }; + + using heap_handle = unique_handle>; + + consteval int move_assigned_value() { + heap_handle handle{}; + handle.reset(new int{1}); + heap_handle other{new int{2}}; + + handle = std::move(other); + return *handle.get(); + } + + consteval int released_value() { + heap_handle handle{new int{3}}; + int* const ptr = handle.release(); + + int const value = *ptr; + delete ptr; + return value; + } + + /** + * @brief reset(handle, context...) compiles + */ + template + concept resettable = requires(H& handle, typename H::handle_t vkHandle, Context&&... context) { + handle.reset(vkHandle, std::forward(context)...); + }; + + template + concept deleter_assignable = requires(H& handle, typename H::deleter_t deleter) { handle.deleter() = deleter; }; +} + +namespace { + /** + * @brief a class without vk_handle_t + */ + struct Unnamed { + static void destroy(DeviceTable, VkFence) {} + }; + + template + concept core_deleter_of = requires { typename core_deleter; }; +} + +CVK_TEST(unique_handle, owner_names_its_handle) { + static_assert(vk_handle_owner); + static_assert(vk_handle_owner); + static_assert(std::same_as); + static_assert(std::same_as::handle_t, VkFence>); + + static_assert(!vk_handle_owner); + static_assert(!core_deleter_of); +} + +CVK_TEST(unique_handle, a_handle_needs_its_context) { + using fence_handle = core_handle; + static_assert(!std::default_initializable); + static_assert(!std::constructible_from); + static_assert(!resettable); + static_assert(resettable); + + using surface_handle = instance_handle; + static_assert(!resettable); + static_assert(resettable); + + using instance_handle = unique_handle>; + static_assert(std::default_initializable); + static_assert(resettable); + + using memory_handle = vma_handle; + static_assert(!resettable); + static_assert(resettable); + + using pool_handle = unique_handle; + static_assert(!resettable); + static_assert(resettable); + + // the context of a handle cannot change on its own + static_assert(!deleter_assignable); +} + +CVK_TEST(unique_handle, constexpr) { + static_assert(move_assigned_value() == 2); + static_assert(released_value() == 3); +} + +CVK_TEST(unique_handle, size) { + static_assert(sizeof(unique_handle>) == sizeof(VkInstance)); + static_assert(sizeof(core_handle) == sizeof(Core const*) + sizeof(VkFence)); + static_assert(sizeof(vma_handle) == sizeof(Core const*) + sizeof(VmaAllocation)); + static_assert( + sizeof(instance_handle) == sizeof(VkInstance) + sizeof(VkSurfaceKHR) + ); +} + +CVK_TEST(unique_handle, core_deleter) { + test::Context const context{}; + + core_handle handle{context.core}; + EXPECT_FALSE(handle); + EXPECT_EQ(&handle.core(), &context.core); + + auto const vkFence = create_vk_fence(context.core); + handle.reset(vkFence, context.core); + ASSERT_TRUE(handle); + EXPECT_EQ(handle.get(), vkFence); + + handle.reset(); + EXPECT_FALSE(handle); +} + +CVK_TEST(unique_handle, move_assignment_destroys_the_old_handle) { + test::Context const context{}; + core_handle handle{context.core}; + core_handle other{context.core}; + handle.reset(create_vk_fence(context.core), context.core); + auto const vkFence = create_vk_fence(context.core); + other.reset(vkFence, context.core); + + handle = std::move(other); + + EXPECT_EQ(handle.get(), vkFence); + EXPECT_FALSE(other); + EXPECT_EQ(&other.core(), &context.core); +} + +CVK_TEST(unique_handle, release_keeps_the_handle) { + test::Context const context{}; + auto const vkFence = create_vk_fence(context.core); + core_handle handle{vkFence, context.core}; + + EXPECT_EQ(handle.release(), vkFence); + EXPECT_FALSE(handle); + + Fence::destroy(context.core.deviceTable(), vkFence); +} + +CVK_TEST(unique_handle, instance_deleter) { + if constexpr(!constants::ENABLE_VALIDATION_LAYERS) GTEST_SKIP() << "needs the validation layers"; + + test::InstanceContext const context{test::headless_extensions()}; + auto surface = test::headless_surface(context.instance); + + instance_handle handle{surface.release().vkSurface.get(), context.instance.get()}; + + EXPECT_EQ(handle.vkInstance(), context.instance.get()); +} + +CVK_TEST(unique_handle, handle_deleter) { + VkApplicationInfo const appInfo{ + .sType = VK_STRUCTURE_TYPE_APPLICATION_INFO, + .pNext = nullptr, + .pApplicationName = test::APP_NAME.data(), + .applicationVersion = 0, + .pEngineName = test::APP_NAME.data(), + .engineVersion = 0, + .apiVersion = VK_API_VERSION_1_3, + }; + VkInstanceCreateInfo const info{ + .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .pApplicationInfo = &appInfo, + .enabledLayerCount = 0, + .ppEnabledLayerNames = nullptr, + .enabledExtensionCount = 0, + .ppEnabledExtensionNames = nullptr, + }; + VkInstance vkInstance = VK_NULL_HANDLE; + ASSERT_EQ(vkCreateInstance(&info, nullptr, &vkInstance), VK_SUCCESS); + // Instance::destroy() calls vkDestroyInstance through volk + volkLoadInstanceOnly(vkInstance); + + unique_handle> handle{}; + handle.reset(vkInstance); + + EXPECT_EQ(handle.get(), vkInstance); +} + +CVK_TEST(unique_handle, custom_deleter) { + test::Context const context{}; + auto const table = context.core.deviceTable(); + + VkCommandPoolCreateInfo const poolInfo{ + .sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .queueFamilyIndex = context.core.queue(0).familyIndex(), + }; + VkCommandPool vkPool = VK_NULL_HANDLE; + ASSERT_EQ(table->vkCreateCommandPool(table.device(), &poolInfo, nullptr, &vkPool), VK_SUCCESS); + + { + unique_handle handle{context.core, vkPool}; + EXPECT_EQ(&handle.core(), &context.core); + EXPECT_EQ(handle.deleter().pool, vkPool); + + VkCommandBufferAllocateInfo const allocInfo{ + .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, + .pNext = nullptr, + .commandPool = vkPool, + .level = VK_COMMAND_BUFFER_LEVEL_PRIMARY, + .commandBufferCount = 1, + }; + VkCommandBuffer vkBuffer = VK_NULL_HANDLE; + ASSERT_EQ(table->vkAllocateCommandBuffers(table.device(), &allocInfo, &vkBuffer), VK_SUCCESS); + + handle.reset(vkBuffer, context.core, vkPool); + EXPECT_EQ(handle.get(), vkBuffer); + } + + table->vkDestroyCommandPool(table.device(), vkPool, nullptr); +} + +CVK_TEST(unique_handle, non_pointer_handle) { + PairOwned::destroyed = 0; + { + unique_handle handle{}; + EXPECT_FALSE(handle); + + handle.reset(pair_handle{new int{1}, new int{2}}); + ASSERT_TRUE(handle); + EXPECT_EQ(*handle.get().second, 2); + + unique_handle other{}; + other.reset(pair_handle{new int{3}, new int{4}}); + + handle = std::move(other); + EXPECT_EQ(PairOwned::destroyed, 1); + EXPECT_EQ(*handle.get().first, 3); + } + EXPECT_EQ(PairOwned::destroyed, 2); +} +} diff --git a/cvk/tests/src/validation.cpp b/cvk/tests/src/validation.cpp new file mode 100644 index 00000000..e2a053a8 --- /dev/null +++ b/cvk/tests/src/validation.cpp @@ -0,0 +1,24 @@ +#include "validation.hpp" + + +namespace cvk::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 default_debug_callback(severity, types, callbackData, userData); + } +} + +DebugMessenger::Config counting_messenger() { return DebugMessenger::Config{.callback = count_errors}; } + +size_t validation_errors() { return errors; } + +void reset_validation_errors() { errors = 0; } +} diff --git a/cvk/tests/src/validation.hpp b/cvk/tests/src/validation.hpp new file mode 100644 index 00000000..bd633d03 --- /dev/null +++ b/cvk/tests/src/validation.hpp @@ -0,0 +1,20 @@ +#pragma once +#include "cvk/debug/debug_messenger.hpp" + +#include + + +namespace cvk::test { +/** + * @brief messenger config that logs every message and counts the errors + * @details for the instance create info too -> counts the errors of vkDestroyInstance(), e.g. leaked objects + */ +[[nodiscard]] DebugMessenger::Config counting_messenger(); + +/** + * @return errors since the last @ref reset_validation_errors() + */ +[[nodiscard]] size_t validation_errors(); + +void reset_validation_errors(); +} diff --git a/jvk/prework/flat_hash_map.hpp b/jvk/prework/flat_hash_map.hpp new file mode 100644 index 00000000..28df9519 --- /dev/null +++ b/jvk/prework/flat_hash_map.hpp @@ -0,0 +1,11 @@ +#pragma once +#include + + +namespace cvk { +/** + * @brief hash map without pointer stability, a rehash moves the elements + */ +template +using flat_hash_map = cth::dt::flat_hash_map; +} diff --git a/jvk/prework/image_layouts.cpp b/jvk/prework/image_layouts.cpp new file mode 100644 index 00000000..b30a0bac --- /dev/null +++ b/jvk/prework/image_layouts.cpp @@ -0,0 +1,48 @@ +#include "image_layouts.hpp" + +#include "cvk/data/img/image.hpp" + +#include + +#include + + +namespace cvk { +VkImageLayout ImageLayouts::layout(Image const& image, uint32_t mipLevel) const { + CTH_CRITICAL(mipLevel >= image.mipLevels(), "mip level must lie in the image") {} + + auto const levels = layouts(image); + return levels.empty() ? VK_IMAGE_LAYOUT_UNDEFINED : levels[mipLevel]; +} + +std::span ImageLayouts::layouts(Image const& image) const { + auto const it = _layouts.find(image.get()); + if(it == _layouts.end()) return {}; + + CTH_CRITICAL( + it->second.size() != image.mipLevels(), + "the entry has other mip levels than the image, a destroyed image left it" + ) {} + return it->second; +} + +void ImageLayouts::set(Image const& image, VkImageLayout layout, uint32_t firstMipLevel, uint32_t mipLevels) { + Image::debug_check(image); + CTH_CRITICAL(firstMipLevel >= image.mipLevels(), "first mip level must lie in the image") {} + + uint32_t const count = mipLevels == VK_REMAINING_MIP_LEVELS ? image.mipLevels() - firstMipLevel : mipLevels; + CTH_CRITICAL(count > image.mipLevels() - firstMipLevel, "mip levels must lie in the image") {} + + auto& levels = _layouts.try_emplace(image.get(), image.mipLevels(), VK_IMAGE_LAYOUT_UNDEFINED).first->second; + CTH_CRITICAL( + levels.size() != image.mipLevels(), + "the entry has other mip levels than the image, a destroyed image left it" + ) {} + + std::ranges::fill_n(levels.begin() + firstMipLevel, static_cast(count), layout); +} + +void ImageLayouts::erase(Image const& image) { _layouts.erase(image.get()); } + +bool ImageLayouts::contains(Image const& image) const { return _layouts.contains(image.get()); } +} diff --git a/jvk/prework/image_layouts.hpp b/jvk/prework/image_layouts.hpp new file mode 100644 index 00000000..c449b48e --- /dev/null +++ b/jvk/prework/image_layouts.hpp @@ -0,0 +1,62 @@ +#pragma once +#include "flat_hash_map.hpp" + +#include + +#include +#include +#include + + +namespace cvk { +class Image; + +/** + * @brief layout of each mip level of the images, for the functions that transition them + * @details the caller owns it and decides the scope: one command buffer, one thread or one serial frame, + * unknown image -> every mip level is VK_IMAGE_LAYOUT_UNDEFINED + * @attention follows the order of the commands, use it on one thread + * @attention keeps the entry of a destroyed image and a new image can get the same handle, + * call @ref erase() before the image is destroyed + */ +class ImageLayouts { +public: + /** + * @return the layout of the mip level, unknown image -> VK_IMAGE_LAYOUT_UNDEFINED + * @pre mipLevel < @ref Image::mipLevels() + */ + [[nodiscard]] VkImageLayout layout(Image const&, uint32_t mipLevel = 0) const; + + /** + * @return the layout of each mip level, unknown image -> empty + */ + [[nodiscard]] std::span layouts(Image const&) const; + + /** + * @brief sets the layout of the mip levels + * @param mipLevels (VK_REMAINING_MIP_LEVELS => to the last mip level) + * @details unknown image -> the other mip levels are VK_IMAGE_LAYOUT_UNDEFINED + * @pre @ref Image::created() + * @pre the mip range lies in the image + */ + void set(Image const&, VkImageLayout, uint32_t firstMipLevel = 0, uint32_t mipLevels = VK_REMAINING_MIP_LEVELS); + + /** + * @brief removes the image, its layouts are unknown after + */ + void erase(Image const&); + + /** + * @brief removes every image + */ + void clear() { _layouts.clear(); } + + [[nodiscard]] bool contains(Image const&) const; + [[nodiscard]] size_t size() const { return _layouts.size(); } + [[nodiscard]] bool empty() const { return _layouts.empty(); } + +private: + //IMPLEMENT key (image, mip level) -> layout: no vector per image, no mip count check + flat_hash_map> _layouts{}; +}; +} diff --git a/jvk/prework/image_layouts_test.cpp b/jvk/prework/image_layouts_test.cpp new file mode 100644 index 00000000..080ce3f4 --- /dev/null +++ b/jvk/prework/image_layouts_test.cpp @@ -0,0 +1,89 @@ +#include "context.hpp" +#include "test.hpp" + +#include "cvk/data/img/image.hpp" +#include "image_layouts.hpp" + +#include + + +namespace cvk { +namespace { + constexpr VkExtent2D EXTENT{16, 16}; + + constexpr Image::Config IMAGE_CONFIG{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .format = VK_FORMAT_R8G8B8A8_UNORM, + .usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT, + .mipLevels = mip_level_count(EXTENT), + }; +} + +CVK_TEST(ImageLayouts, unknown_image_is_undefined) { + test::Context const context{}; + Image const image{context.core, IMAGE_CONFIG, EXTENT}; + ImageLayouts const layouts{}; + + EXPECT_TRUE(layouts.empty()); + EXPECT_FALSE(layouts.contains(image)); + EXPECT_TRUE(layouts.layouts(image).empty()); + EXPECT_EQ(layouts.layout(image, 2), VK_IMAGE_LAYOUT_UNDEFINED); +} + +CVK_TEST(ImageLayouts, set_a_mip_range) { + test::Context const context{}; + Image const image{context.core, IMAGE_CONFIG, EXTENT}; + ImageLayouts layouts{}; + + layouts.set(image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, 2); + + EXPECT_TRUE(layouts.contains(image)); + EXPECT_EQ(layouts.size(), 1); + ASSERT_EQ(layouts.layouts(image).size(), image.mipLevels()); + EXPECT_EQ(layouts.layout(image, 0), VK_IMAGE_LAYOUT_UNDEFINED); + EXPECT_EQ(layouts.layout(image, 1), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + EXPECT_EQ(layouts.layout(image, 2), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + EXPECT_EQ(layouts.layout(image, 3), VK_IMAGE_LAYOUT_UNDEFINED); +} + +CVK_TEST(ImageLayouts, set_the_remaining_mip_levels) { + test::Context const context{}; + Image const image{context.core, IMAGE_CONFIG, EXTENT}; + ImageLayouts layouts{}; + + layouts.set(image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + layouts.set(image, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, 2); + + EXPECT_EQ(layouts.layout(image, 1), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + EXPECT_EQ(layouts.layout(image, 2), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + EXPECT_EQ(layouts.layout(image, image.mipLevels() - 1), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); +} + +CVK_TEST(ImageLayouts, follows_the_handle_when_the_image_moves) { + test::Context const context{}; + Image image{context.core, IMAGE_CONFIG, EXTENT}; + ImageLayouts layouts{}; + layouts.set(image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + + Image const moved{std::move(image)}; + + EXPECT_EQ(layouts.layout(moved), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); +} + +CVK_TEST(ImageLayouts, erase_and_clear) { + test::Context const context{}; + Image const first{context.core, IMAGE_CONFIG, EXTENT}; + Image const second{context.core, IMAGE_CONFIG, EXTENT}; + ImageLayouts layouts{}; + layouts.set(first, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + layouts.set(second, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + + layouts.erase(first); + EXPECT_FALSE(layouts.contains(first)); + EXPECT_EQ(layouts.layout(first), VK_IMAGE_LAYOUT_UNDEFINED); + EXPECT_TRUE(layouts.contains(second)); + + layouts.clear(); + EXPECT_TRUE(layouts.empty()); +} +} diff --git a/demo/CMakeLists.txt b/legacy/demo/CMakeLists.txt similarity index 100% rename from demo/CMakeLists.txt rename to legacy/demo/CMakeLists.txt diff --git a/demo/assets/bin/shader/shader.frag.spv b/legacy/demo/assets/bin/shader/shader.frag.spv similarity index 100% rename from demo/assets/bin/shader/shader.frag.spv rename to legacy/demo/assets/bin/shader/shader.frag.spv diff --git a/demo/assets/bin/shader/shader.vert.spv b/legacy/demo/assets/bin/shader/shader.vert.spv similarity index 100% rename from demo/assets/bin/shader/shader.vert.spv rename to legacy/demo/assets/bin/shader/shader.vert.spv diff --git a/demo/assets/img/texture/first_texture.png b/legacy/demo/assets/img/texture/first_texture.png similarity index 100% rename from demo/assets/img/texture/first_texture.png rename to legacy/demo/assets/img/texture/first_texture.png diff --git a/demo/demo_shaders/CMakeLists.txt b/legacy/demo/demo_shaders/CMakeLists.txt similarity index 100% rename from demo/demo_shaders/CMakeLists.txt rename to legacy/demo/demo_shaders/CMakeLists.txt diff --git a/demo/demo_shaders/src/shader.frag b/legacy/demo/demo_shaders/src/shader.frag similarity index 100% rename from demo/demo_shaders/src/shader.frag rename to legacy/demo/demo_shaders/src/shader.frag diff --git a/demo/demo_shaders/src/shader.vert b/legacy/demo/demo_shaders/src/shader.vert similarity index 100% rename from demo/demo_shaders/src/shader.vert rename to legacy/demo/demo_shaders/src/shader.vert diff --git a/demo/demo_shaders/src/test.bat b/legacy/demo/demo_shaders/src/test.bat similarity index 100% rename from demo/demo_shaders/src/test.bat rename to legacy/demo/demo_shaders/src/test.bat diff --git a/demo/src/HlcApp.cpp b/legacy/demo/src/HlcApp.cpp similarity index 100% rename from demo/src/HlcApp.cpp rename to legacy/demo/src/HlcApp.cpp diff --git a/demo/src/HlcApp.hpp b/legacy/demo/src/HlcApp.hpp similarity index 100% rename from demo/src/HlcApp.hpp rename to legacy/demo/src/HlcApp.hpp diff --git a/demo/src/demo_main.cpp b/legacy/demo/src/demo_main.cpp similarity index 100% rename from demo/src/demo_main.cpp rename to legacy/demo/src/demo_main.cpp diff --git a/demo/src/pch/glob/boost.hpp b/legacy/demo/src/pch/glob/boost.hpp similarity index 100% rename from demo/src/pch/glob/boost.hpp rename to legacy/demo/src/pch/glob/boost.hpp diff --git a/demo/src/pch/glob/cth.hpp b/legacy/demo/src/pch/glob/cth.hpp similarity index 100% rename from demo/src/pch/glob/cth.hpp rename to legacy/demo/src/pch/glob/cth.hpp diff --git a/demo/src/pch/glob/glm.hpp b/legacy/demo/src/pch/glob/glm.hpp similarity index 100% rename from demo/src/pch/glob/glm.hpp rename to legacy/demo/src/pch/glob/glm.hpp diff --git a/demo/src/pch/glob/std.hpp b/legacy/demo/src/pch/glob/std.hpp similarity index 100% rename from demo/src/pch/glob/std.hpp rename to legacy/demo/src/pch/glob/std.hpp diff --git a/demo/src/pch/glob/volk.hpp b/legacy/demo/src/pch/glob/volk.hpp similarity index 100% rename from demo/src/pch/glob/volk.hpp rename to legacy/demo/src/pch/glob/volk.hpp diff --git a/demo/src/pch/pch.cpp b/legacy/demo/src/pch/pch.cpp similarity index 100% rename from demo/src/pch/pch.cpp rename to legacy/demo/src/pch/pch.cpp diff --git a/demo/src/pch/pch.hpp b/legacy/demo/src/pch/pch.hpp similarity index 100% rename from demo/src/pch/pch.hpp rename to legacy/demo/src/pch/pch.hpp diff --git a/demo/src/render/FrameResources.cpp b/legacy/demo/src/render/FrameResources.cpp similarity index 100% rename from demo/src/render/FrameResources.cpp rename to legacy/demo/src/render/FrameResources.cpp diff --git a/demo/src/render/FrameResources.hpp b/legacy/demo/src/render/FrameResources.hpp similarity index 100% rename from demo/src/render/FrameResources.hpp rename to legacy/demo/src/render/FrameResources.hpp diff --git a/demo/src/render/HlcFrameInfo.hpp b/legacy/demo/src/render/HlcFrameInfo.hpp similarity index 100% rename from demo/src/render/HlcFrameInfo.hpp rename to legacy/demo/src/render/HlcFrameInfo.hpp diff --git a/demo/src/render/HlcRenderSystem.cpp b/legacy/demo/src/render/HlcRenderSystem.cpp similarity index 100% rename from demo/src/render/HlcRenderSystem.cpp rename to legacy/demo/src/render/HlcRenderSystem.cpp diff --git a/demo/src/render/HlcRenderSystem.hpp b/legacy/demo/src/render/HlcRenderSystem.hpp similarity index 100% rename from demo/src/render/HlcRenderSystem.hpp rename to legacy/demo/src/render/HlcRenderSystem.hpp diff --git a/demo/src/render/frame_resources_config.hpp b/legacy/demo/src/render/frame_resources_config.hpp similarity index 100% rename from demo/src/render/frame_resources_config.hpp rename to legacy/demo/src/render/frame_resources_config.hpp diff --git a/demo/src/render/push_constant.hpp b/legacy/demo/src/render/push_constant.hpp similarity index 100% rename from demo/src/render/push_constant.hpp rename to legacy/demo/src/render/push_constant.hpp diff --git a/jolly/CMakeLists.txt b/legacy/jolly/CMakeLists.txt similarity index 100% rename from jolly/CMakeLists.txt rename to legacy/jolly/CMakeLists.txt diff --git a/jolly/doc/roadmap.md b/legacy/jolly/doc/roadmap.md similarity index 100% rename from jolly/doc/roadmap.md rename to legacy/jolly/doc/roadmap.md diff --git a/jolly/doc/scene.md b/legacy/jolly/doc/scene.md similarity index 100% rename from jolly/doc/scene.md rename to legacy/jolly/doc/scene.md diff --git a/jolly/doc/tinygltf.md b/legacy/jolly/doc/tinygltf.md similarity index 100% rename from jolly/doc/tinygltf.md rename to legacy/jolly/doc/tinygltf.md diff --git a/jolly/incl/jolly/core/core.hpp b/legacy/jolly/incl/jolly/core/core.hpp similarity index 100% rename from jolly/incl/jolly/core/core.hpp rename to legacy/jolly/incl/jolly/core/core.hpp diff --git a/jolly/incl/jolly/core/core_config.hpp b/legacy/jolly/incl/jolly/core/core_config.hpp similarity index 100% rename from jolly/incl/jolly/core/core_config.hpp rename to legacy/jolly/incl/jolly/core/core_config.hpp diff --git a/jolly/incl/jolly/data/model/primitive/loader/gltf_utility.hpp b/legacy/jolly/incl/jolly/data/model/primitive/loader/gltf_utility.hpp similarity index 100% rename from jolly/incl/jolly/data/model/primitive/loader/gltf_utility.hpp rename to legacy/jolly/incl/jolly/data/model/primitive/loader/gltf_utility.hpp diff --git a/jolly/incl/jolly/data/model/primitive/vertex_buffer.hpp b/legacy/jolly/incl/jolly/data/model/primitive/vertex_buffer.hpp similarity index 100% rename from jolly/incl/jolly/data/model/primitive/vertex_buffer.hpp rename to legacy/jolly/incl/jolly/data/model/primitive/vertex_buffer.hpp diff --git a/jolly/incl/jolly/data/model/primitive/vertex_key.hpp b/legacy/jolly/incl/jolly/data/model/primitive/vertex_key.hpp similarity index 100% rename from jolly/incl/jolly/data/model/primitive/vertex_key.hpp rename to legacy/jolly/incl/jolly/data/model/primitive/vertex_key.hpp diff --git a/jolly/incl/jolly/data/staging_allocator.hpp b/legacy/jolly/incl/jolly/data/staging_allocator.hpp similarity index 100% rename from jolly/incl/jolly/data/staging_allocator.hpp rename to legacy/jolly/incl/jolly/data/staging_allocator.hpp diff --git a/jolly/incl/jolly/object/HlcRenderObject.hpp b/legacy/jolly/incl/jolly/object/HlcRenderObject.hpp similarity index 100% rename from jolly/incl/jolly/object/HlcRenderObject.hpp rename to legacy/jolly/incl/jolly/object/HlcRenderObject.hpp diff --git a/jolly/incl/jolly/object/HlcStandardObject.hpp b/legacy/jolly/incl/jolly/object/HlcStandardObject.hpp similarity index 100% rename from jolly/incl/jolly/object/HlcStandardObject.hpp rename to legacy/jolly/incl/jolly/object/HlcStandardObject.hpp diff --git a/jolly/incl/jolly/render/graph/RenderStage.hpp b/legacy/jolly/incl/jolly/render/graph/RenderStage.hpp similarity index 100% rename from jolly/incl/jolly/render/graph/RenderStage.hpp rename to legacy/jolly/incl/jolly/render/graph/RenderStage.hpp diff --git a/jolly/incl/jolly/render/graph/RenderStageConfig.hpp b/legacy/jolly/incl/jolly/render/graph/RenderStageConfig.hpp similarity index 100% rename from jolly/incl/jolly/render/graph/RenderStageConfig.hpp rename to legacy/jolly/incl/jolly/render/graph/RenderStageConfig.hpp diff --git a/jolly/incl/jolly/render/graph/dag.hpp b/legacy/jolly/incl/jolly/render/graph/dag.hpp similarity index 100% rename from jolly/incl/jolly/render/graph/dag.hpp rename to legacy/jolly/incl/jolly/render/graph/dag.hpp diff --git a/jolly/incl/jolly/render/graph/renderer.hpp b/legacy/jolly/incl/jolly/render/graph/renderer.hpp similarity index 100% rename from jolly/incl/jolly/render/graph/renderer.hpp rename to legacy/jolly/incl/jolly/render/graph/renderer.hpp diff --git a/jolly/incl/jolly/render/model/CthVertex.hpp b/legacy/jolly/incl/jolly/render/model/CthVertex.hpp similarity index 100% rename from jolly/incl/jolly/render/model/CthVertex.hpp rename to legacy/jolly/incl/jolly/render/model/CthVertex.hpp diff --git a/jolly/incl/jolly/render/model/HlcModel.hpp b/legacy/jolly/incl/jolly/render/model/HlcModel.hpp similarity index 100% rename from jolly/incl/jolly/render/model/HlcModel.hpp rename to legacy/jolly/incl/jolly/render/model/HlcModel.hpp diff --git a/jolly/incl/jolly/render/model/HlcModelManager.hpp b/legacy/jolly/incl/jolly/render/model/HlcModelManager.hpp similarity index 100% rename from jolly/incl/jolly/render/model/HlcModelManager.hpp rename to legacy/jolly/incl/jolly/render/model/HlcModelManager.hpp diff --git a/jolly/incl/jolly/render/render_pipeline.hpp b/legacy/jolly/incl/jolly/render/render_pipeline.hpp similarity index 100% rename from jolly/incl/jolly/render/render_pipeline.hpp rename to legacy/jolly/incl/jolly/render/render_pipeline.hpp diff --git a/jolly/incl/jolly/render/submit/cmd/primary_cmd_buffer.hpp b/legacy/jolly/incl/jolly/render/submit/cmd/primary_cmd_buffer.hpp similarity index 100% rename from jolly/incl/jolly/render/submit/cmd/primary_cmd_buffer.hpp rename to legacy/jolly/incl/jolly/render/submit/cmd/primary_cmd_buffer.hpp diff --git a/jolly/incl/jolly/render/submit/cmd/secondary_cmd_buffer.hpp b/legacy/jolly/incl/jolly/render/submit/cmd/secondary_cmd_buffer.hpp similarity index 100% rename from jolly/incl/jolly/render/submit/cmd/secondary_cmd_buffer.hpp rename to legacy/jolly/incl/jolly/render/submit/cmd/secondary_cmd_buffer.hpp diff --git a/jolly/incl/jolly/render/submit/cmd/secondary_cmd_buffer_config.hpp b/legacy/jolly/incl/jolly/render/submit/cmd/secondary_cmd_buffer_config.hpp similarity index 100% rename from jolly/incl/jolly/render/submit/cmd/secondary_cmd_buffer_config.hpp rename to legacy/jolly/incl/jolly/render/submit/cmd/secondary_cmd_buffer_config.hpp diff --git a/jolly/incl/jolly/render/submit/cmd/utility/task_container.hpp b/legacy/jolly/incl/jolly/render/submit/cmd/utility/task_container.hpp similarity index 100% rename from jolly/incl/jolly/render/submit/cmd/utility/task_container.hpp rename to legacy/jolly/incl/jolly/render/submit/cmd/utility/task_container.hpp diff --git a/jolly/incl/jolly/render/submit/cmd/utility/task_container_mixin.hpp b/legacy/jolly/incl/jolly/render/submit/cmd/utility/task_container_mixin.hpp similarity index 100% rename from jolly/incl/jolly/render/submit/cmd/utility/task_container_mixin.hpp rename to legacy/jolly/incl/jolly/render/submit/cmd/utility/task_container_mixin.hpp diff --git a/jolly/incl/jolly/render/submit/cmd/utility/thread_safe_task_container.hpp b/legacy/jolly/incl/jolly/render/submit/cmd/utility/thread_safe_task_container.hpp similarity index 100% rename from jolly/incl/jolly/render/submit/cmd/utility/thread_safe_task_container.hpp rename to legacy/jolly/incl/jolly/render/submit/cmd/utility/thread_safe_task_container.hpp diff --git a/jolly/incl/jolly/render/submit/queue.hpp b/legacy/jolly/incl/jolly/render/submit/queue.hpp similarity index 100% rename from jolly/incl/jolly/render/submit/queue.hpp rename to legacy/jolly/incl/jolly/render/submit/queue.hpp diff --git a/jolly/incl/jolly/render/submit/queue_properties.hpp b/legacy/jolly/incl/jolly/render/submit/queue_properties.hpp similarity index 100% rename from jolly/incl/jolly/render/submit/queue_properties.hpp rename to legacy/jolly/incl/jolly/render/submit/queue_properties.hpp diff --git a/jolly/incl/jolly/render/submit/submit_info.hpp b/legacy/jolly/incl/jolly/render/submit/submit_info.hpp similarity index 100% rename from jolly/incl/jolly/render/submit/submit_info.hpp rename to legacy/jolly/incl/jolly/render/submit/submit_info.hpp diff --git a/jolly/incl/jolly/render/sync/RenderPulse.hpp b/legacy/jolly/incl/jolly/render/sync/RenderPulse.hpp similarity index 100% rename from jolly/incl/jolly/render/sync/RenderPulse.hpp rename to legacy/jolly/incl/jolly/render/sync/RenderPulse.hpp diff --git a/jolly/incl/jolly/render/sync/fence.hpp b/legacy/jolly/incl/jolly/render/sync/fence.hpp similarity index 100% rename from jolly/incl/jolly/render/sync/fence.hpp rename to legacy/jolly/incl/jolly/render/sync/fence.hpp diff --git a/jolly/incl/jolly/render/sync/pipeline_stage_flags.hpp b/legacy/jolly/incl/jolly/render/sync/pipeline_stage_flags.hpp similarity index 100% rename from jolly/incl/jolly/render/sync/pipeline_stage_flags.hpp rename to legacy/jolly/incl/jolly/render/sync/pipeline_stage_flags.hpp diff --git a/jolly/incl/jolly/render/sync/pipeline_wait_stage.hpp b/legacy/jolly/incl/jolly/render/sync/pipeline_wait_stage.hpp similarity index 100% rename from jolly/incl/jolly/render/sync/pipeline_wait_stage.hpp rename to legacy/jolly/incl/jolly/render/sync/pipeline_wait_stage.hpp diff --git a/jolly/incl/jolly/user/HlcCamera.hpp b/legacy/jolly/incl/jolly/user/HlcCamera.hpp similarity index 100% rename from jolly/incl/jolly/user/HlcCamera.hpp rename to legacy/jolly/incl/jolly/user/HlcCamera.hpp diff --git a/jolly/incl/jolly/user/HlcInputController.hpp b/legacy/jolly/incl/jolly/user/HlcInputController.hpp similarity index 100% rename from jolly/incl/jolly/user/HlcInputController.hpp rename to legacy/jolly/incl/jolly/user/HlcInputController.hpp diff --git a/jolly/incl/jolly/user/HlcUser.hpp b/legacy/jolly/incl/jolly/user/HlcUser.hpp similarity index 100% rename from jolly/incl/jolly/user/HlcUser.hpp rename to legacy/jolly/incl/jolly/user/HlcUser.hpp diff --git a/jolly/incl/jolly/utility/CthOSWindow.hpp b/legacy/jolly/incl/jolly/utility/CthOSWindow.hpp similarity index 100% rename from jolly/incl/jolly/utility/CthOSWindow.hpp rename to legacy/jolly/incl/jolly/utility/CthOSWindow.hpp diff --git a/jolly/incl/jolly/utility/GraphicsCore.hpp b/legacy/jolly/incl/jolly/utility/GraphicsCore.hpp similarity index 100% rename from jolly/incl/jolly/utility/GraphicsCore.hpp rename to legacy/jolly/incl/jolly/utility/GraphicsCore.hpp diff --git a/jolly/incl/jolly/utility/GraphicsSyncConfig.hpp b/legacy/jolly/incl/jolly/utility/GraphicsSyncConfig.hpp similarity index 100% rename from jolly/incl/jolly/utility/GraphicsSyncConfig.hpp rename to legacy/jolly/incl/jolly/utility/GraphicsSyncConfig.hpp diff --git a/jolly/incl/jolly/utility/graphics_core_config.hpp b/legacy/jolly/incl/jolly/utility/graphics_core_config.hpp similarity index 100% rename from jolly/incl/jolly/utility/graphics_core_config.hpp rename to legacy/jolly/incl/jolly/utility/graphics_core_config.hpp diff --git a/jolly/incl/jolly/utility/jvk_conversion.hpp b/legacy/jolly/incl/jolly/utility/jvk_conversion.hpp similarity index 100% rename from jolly/incl/jolly/utility/jvk_conversion.hpp rename to legacy/jolly/incl/jolly/utility/jvk_conversion.hpp diff --git a/jolly/incl/jolly/utility/os/os_def.hpp b/legacy/jolly/incl/jolly/utility/os/os_def.hpp similarity index 100% rename from jolly/incl/jolly/utility/os/os_def.hpp rename to legacy/jolly/incl/jolly/utility/os/os_def.hpp diff --git a/jolly/incl/jolly/utility/types.hpp b/legacy/jolly/incl/jolly/utility/types.hpp similarity index 100% rename from jolly/incl/jolly/utility/types.hpp rename to legacy/jolly/incl/jolly/utility/types.hpp diff --git a/jolly/src/core/core.cpp b/legacy/jolly/src/core/core.cpp similarity index 100% rename from jolly/src/core/core.cpp rename to legacy/jolly/src/core/core.cpp diff --git a/jolly/src/core/queue.cpp b/legacy/jolly/src/core/queue.cpp similarity index 100% rename from jolly/src/core/queue.cpp rename to legacy/jolly/src/core/queue.cpp diff --git a/jolly/src/core/submit_info.cpp b/legacy/jolly/src/core/submit_info.cpp similarity index 100% rename from jolly/src/core/submit_info.cpp rename to legacy/jolly/src/core/submit_info.cpp diff --git a/jolly/src/data/model/primitive/vertex_buffer.cpp b/legacy/jolly/src/data/model/primitive/vertex_buffer.cpp similarity index 100% rename from jolly/src/data/model/primitive/vertex_buffer.cpp rename to legacy/jolly/src/data/model/primitive/vertex_buffer.cpp diff --git a/jolly/src/data/staging_allocator.cpp b/legacy/jolly/src/data/staging_allocator.cpp similarity index 100% rename from jolly/src/data/staging_allocator.cpp rename to legacy/jolly/src/data/staging_allocator.cpp diff --git a/jolly/src/object/HlcStandardObject.cpp b/legacy/jolly/src/object/HlcStandardObject.cpp similarity index 100% rename from jolly/src/object/HlcStandardObject.cpp rename to legacy/jolly/src/object/HlcStandardObject.cpp diff --git a/jolly/src/pch/glob/boost.hpp b/legacy/jolly/src/pch/glob/boost.hpp similarity index 100% rename from jolly/src/pch/glob/boost.hpp rename to legacy/jolly/src/pch/glob/boost.hpp diff --git a/jolly/src/pch/glob/cth.hpp b/legacy/jolly/src/pch/glob/cth.hpp similarity index 100% rename from jolly/src/pch/glob/cth.hpp rename to legacy/jolly/src/pch/glob/cth.hpp diff --git a/jolly/src/pch/glob/glm.hpp b/legacy/jolly/src/pch/glob/glm.hpp similarity index 100% rename from jolly/src/pch/glob/glm.hpp rename to legacy/jolly/src/pch/glob/glm.hpp diff --git a/legacy/jolly/src/pch/glob/std.hpp b/legacy/jolly/src/pch/glob/std.hpp new file mode 100644 index 00000000..ad7924f4 --- /dev/null +++ b/legacy/jolly/src/pch/glob/std.hpp @@ -0,0 +1,27 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include diff --git a/jolly/src/pch/pch.cpp b/legacy/jolly/src/pch/pch.cpp similarity index 100% rename from jolly/src/pch/pch.cpp rename to legacy/jolly/src/pch/pch.cpp diff --git a/jolly/src/pch/pch.hpp b/legacy/jolly/src/pch/pch.hpp similarity index 100% rename from jolly/src/pch/pch.hpp rename to legacy/jolly/src/pch/pch.hpp diff --git a/jolly/src/render/graph/RenderStage.cpp b/legacy/jolly/src/render/graph/RenderStage.cpp similarity index 100% rename from jolly/src/render/graph/RenderStage.cpp rename to legacy/jolly/src/render/graph/RenderStage.cpp diff --git a/jolly/src/render/graph/renderer.cpp b/legacy/jolly/src/render/graph/renderer.cpp similarity index 100% rename from jolly/src/render/graph/renderer.cpp rename to legacy/jolly/src/render/graph/renderer.cpp diff --git a/jolly/src/render/model/HlcModel.cpp b/legacy/jolly/src/render/model/HlcModel.cpp similarity index 100% rename from jolly/src/render/model/HlcModel.cpp rename to legacy/jolly/src/render/model/HlcModel.cpp diff --git a/jolly/src/render/model/HlcModelManager.cpp b/legacy/jolly/src/render/model/HlcModelManager.cpp similarity index 100% rename from jolly/src/render/model/HlcModelManager.cpp rename to legacy/jolly/src/render/model/HlcModelManager.cpp diff --git a/jolly/src/render/submit/cmd/primary_cmd_buffer.cpp b/legacy/jolly/src/render/submit/cmd/primary_cmd_buffer.cpp similarity index 100% rename from jolly/src/render/submit/cmd/primary_cmd_buffer.cpp rename to legacy/jolly/src/render/submit/cmd/primary_cmd_buffer.cpp diff --git a/jolly/src/render/submit/cmd/secondary_cmd_buffer.cpp b/legacy/jolly/src/render/submit/cmd/secondary_cmd_buffer.cpp similarity index 100% rename from jolly/src/render/submit/cmd/secondary_cmd_buffer.cpp rename to legacy/jolly/src/render/submit/cmd/secondary_cmd_buffer.cpp diff --git a/jolly/src/render/sync/fence.cpp b/legacy/jolly/src/render/sync/fence.cpp similarity index 100% rename from jolly/src/render/sync/fence.cpp rename to legacy/jolly/src/render/sync/fence.cpp diff --git a/jolly/src/render/sync/pipeline_stage_flags_helpers.hpp b/legacy/jolly/src/render/sync/pipeline_stage_flags_helpers.hpp similarity index 100% rename from jolly/src/render/sync/pipeline_stage_flags_helpers.hpp rename to legacy/jolly/src/render/sync/pipeline_stage_flags_helpers.hpp diff --git a/jolly/src/render/sync/pipeline_wait_stage_helpers.hpp b/legacy/jolly/src/render/sync/pipeline_wait_stage_helpers.hpp similarity index 100% rename from jolly/src/render/sync/pipeline_wait_stage_helpers.hpp rename to legacy/jolly/src/render/sync/pipeline_wait_stage_helpers.hpp diff --git a/jolly/src/user/HlcCamera.cpp b/legacy/jolly/src/user/HlcCamera.cpp similarity index 100% rename from jolly/src/user/HlcCamera.cpp rename to legacy/jolly/src/user/HlcCamera.cpp diff --git a/jolly/src/user/HlcInputController.cpp b/legacy/jolly/src/user/HlcInputController.cpp similarity index 100% rename from jolly/src/user/HlcInputController.cpp rename to legacy/jolly/src/user/HlcInputController.cpp diff --git a/jolly/src/utility/CthGraphicsCore.cpp b/legacy/jolly/src/utility/CthGraphicsCore.cpp similarity index 100% rename from jolly/src/utility/CthGraphicsCore.cpp rename to legacy/jolly/src/utility/CthGraphicsCore.cpp diff --git a/jolly/src/utility/CthGraphicsSyncConfig.cpp b/legacy/jolly/src/utility/CthGraphicsSyncConfig.cpp similarity index 100% rename from jolly/src/utility/CthGraphicsSyncConfig.cpp rename to legacy/jolly/src/utility/CthGraphicsSyncConfig.cpp diff --git a/jolly/src/utility/CthOSWindow.cpp b/legacy/jolly/src/utility/CthOSWindow.cpp similarity index 100% rename from jolly/src/utility/CthOSWindow.cpp rename to legacy/jolly/src/utility/CthOSWindow.cpp diff --git a/jolly/src/utility/vk_convert.hpp b/legacy/jolly/src/utility/vk_convert.hpp similarity index 100% rename from jolly/src/utility/vk_convert.hpp rename to legacy/jolly/src/utility/vk_convert.hpp diff --git a/jvk/CMakeLists.txt b/legacy/jvk/CMakeLists.txt similarity index 100% rename from jvk/CMakeLists.txt rename to legacy/jvk/CMakeLists.txt diff --git a/jvk/incl/jvk/base/core.hpp b/legacy/jvk/incl/jvk/base/core.hpp similarity index 100% rename from jvk/incl/jvk/base/core.hpp rename to legacy/jvk/incl/jvk/base/core.hpp diff --git a/jvk/incl/jvk/base/core_config.hpp b/legacy/jvk/incl/jvk/base/core_config.hpp similarity index 100% rename from jvk/incl/jvk/base/core_config.hpp rename to legacy/jvk/incl/jvk/base/core_config.hpp diff --git a/jvk/incl/jvk/base/destruction_queue.hpp b/legacy/jvk/incl/jvk/base/destruction_queue.hpp similarity index 100% rename from jvk/incl/jvk/base/destruction_queue.hpp rename to legacy/jvk/incl/jvk/base/destruction_queue.hpp diff --git a/jvk/incl/jvk/base/destruction_queue_config.hpp b/legacy/jvk/incl/jvk/base/destruction_queue_config.hpp similarity index 100% rename from jvk/incl/jvk/base/destruction_queue_config.hpp rename to legacy/jvk/incl/jvk/base/destruction_queue_config.hpp diff --git a/jvk/incl/jvk/base/device.hpp b/legacy/jvk/incl/jvk/base/device.hpp similarity index 100% rename from jvk/incl/jvk/base/device.hpp rename to legacy/jvk/incl/jvk/base/device.hpp diff --git a/jvk/incl/jvk/base/device_config.hpp b/legacy/jvk/incl/jvk/base/device_config.hpp similarity index 100% rename from jvk/incl/jvk/base/device_config.hpp rename to legacy/jvk/incl/jvk/base/device_config.hpp diff --git a/jvk/incl/jvk/base/device_table.hpp b/legacy/jvk/incl/jvk/base/device_table.hpp similarity index 100% rename from jvk/incl/jvk/base/device_table.hpp rename to legacy/jvk/incl/jvk/base/device_table.hpp diff --git a/jvk/incl/jvk/base/instance.hpp b/legacy/jvk/incl/jvk/base/instance.hpp similarity index 100% rename from jvk/incl/jvk/base/instance.hpp rename to legacy/jvk/incl/jvk/base/instance.hpp diff --git a/jvk/incl/jvk/base/jvk.hpp b/legacy/jvk/incl/jvk/base/jvk.hpp similarity index 100% rename from jvk/incl/jvk/base/jvk.hpp rename to legacy/jvk/incl/jvk/base/jvk.hpp diff --git a/jvk/incl/jvk/base/physical_device.hpp b/legacy/jvk/incl/jvk/base/physical_device.hpp similarity index 100% rename from jvk/incl/jvk/base/physical_device.hpp rename to legacy/jvk/incl/jvk/base/physical_device.hpp diff --git a/jvk/incl/jvk/base/queue/present_info.hpp b/legacy/jvk/incl/jvk/base/queue/present_info.hpp similarity index 100% rename from jvk/incl/jvk/base/queue/present_info.hpp rename to legacy/jvk/incl/jvk/base/queue/present_info.hpp diff --git a/jvk/incl/jvk/base/queue/queue.hpp b/legacy/jvk/incl/jvk/base/queue/queue.hpp similarity index 100% rename from jvk/incl/jvk/base/queue/queue.hpp rename to legacy/jvk/incl/jvk/base/queue/queue.hpp diff --git a/jvk/incl/jvk/base/queue/queue_family.hpp b/legacy/jvk/incl/jvk/base/queue/queue_family.hpp similarity index 100% rename from jvk/incl/jvk/base/queue/queue_family.hpp rename to legacy/jvk/incl/jvk/base/queue/queue_family.hpp diff --git a/jvk/incl/jvk/base/queue/queue_state.hpp b/legacy/jvk/incl/jvk/base/queue/queue_state.hpp similarity index 100% rename from jvk/incl/jvk/base/queue/queue_state.hpp rename to legacy/jvk/incl/jvk/base/queue/queue_state.hpp diff --git a/jvk/incl/jvk/base/queue/submit_info.hpp b/legacy/jvk/incl/jvk/base/queue/submit_info.hpp similarity index 100% rename from jvk/incl/jvk/base/queue/submit_info.hpp rename to legacy/jvk/incl/jvk/base/queue/submit_info.hpp diff --git a/jvk/incl/jvk/data/buffer/base_buffer.hpp b/legacy/jvk/incl/jvk/data/buffer/base_buffer.hpp similarity index 100% rename from jvk/incl/jvk/data/buffer/base_buffer.hpp rename to legacy/jvk/incl/jvk/data/buffer/base_buffer.hpp diff --git a/jvk/incl/jvk/data/buffer/buffer.hpp b/legacy/jvk/incl/jvk/data/buffer/buffer.hpp similarity index 100% rename from jvk/incl/jvk/data/buffer/buffer.hpp rename to legacy/jvk/incl/jvk/data/buffer/buffer.hpp diff --git a/jvk/incl/jvk/data/buffer/inl/buffer.inl b/legacy/jvk/incl/jvk/data/buffer/inl/buffer.inl similarity index 100% rename from jvk/incl/jvk/data/buffer/inl/buffer.inl rename to legacy/jvk/incl/jvk/data/buffer/inl/buffer.inl diff --git a/jvk/incl/jvk/data/descriptor/descriptor.hpp b/legacy/jvk/incl/jvk/data/descriptor/descriptor.hpp similarity index 100% rename from jvk/incl/jvk/data/descriptor/descriptor.hpp rename to legacy/jvk/incl/jvk/data/descriptor/descriptor.hpp diff --git a/jvk/incl/jvk/data/descriptor/descriptor_pool.hpp b/legacy/jvk/incl/jvk/data/descriptor/descriptor_pool.hpp similarity index 100% rename from jvk/incl/jvk/data/descriptor/descriptor_pool.hpp rename to legacy/jvk/incl/jvk/data/descriptor/descriptor_pool.hpp diff --git a/jvk/incl/jvk/data/descriptor/descriptor_set.hpp b/legacy/jvk/incl/jvk/data/descriptor/descriptor_set.hpp similarity index 100% rename from jvk/incl/jvk/data/descriptor/descriptor_set.hpp rename to legacy/jvk/incl/jvk/data/descriptor/descriptor_set.hpp diff --git a/jvk/incl/jvk/data/descriptor/types/buffer_descriptors.hpp b/legacy/jvk/incl/jvk/data/descriptor/types/buffer_descriptors.hpp similarity index 100% rename from jvk/incl/jvk/data/descriptor/types/buffer_descriptors.hpp rename to legacy/jvk/incl/jvk/data/descriptor/types/buffer_descriptors.hpp diff --git a/jvk/incl/jvk/data/descriptor/types/image_descriptors.hpp b/legacy/jvk/incl/jvk/data/descriptor/types/image_descriptors.hpp similarity index 100% rename from jvk/incl/jvk/data/descriptor/types/image_descriptors.hpp rename to legacy/jvk/incl/jvk/data/descriptor/types/image_descriptors.hpp diff --git a/jvk/incl/jvk/data/img/image.hpp b/legacy/jvk/incl/jvk/data/img/image.hpp similarity index 100% rename from jvk/incl/jvk/data/img/image.hpp rename to legacy/jvk/incl/jvk/data/img/image.hpp diff --git a/jvk/incl/jvk/data/img/image_config.hpp b/legacy/jvk/incl/jvk/data/img/image_config.hpp similarity index 100% rename from jvk/incl/jvk/data/img/image_config.hpp rename to legacy/jvk/incl/jvk/data/img/image_config.hpp diff --git a/jvk/incl/jvk/data/img/image_view.hpp b/legacy/jvk/incl/jvk/data/img/image_view.hpp similarity index 100% rename from jvk/incl/jvk/data/img/image_view.hpp rename to legacy/jvk/incl/jvk/data/img/image_view.hpp diff --git a/jvk/incl/jvk/data/img/texture/sampler.hpp b/legacy/jvk/incl/jvk/data/img/texture/sampler.hpp similarity index 100% rename from jvk/incl/jvk/data/img/texture/sampler.hpp rename to legacy/jvk/incl/jvk/data/img/texture/sampler.hpp diff --git a/jvk/incl/jvk/data/img/texture/texture.hpp b/legacy/jvk/incl/jvk/data/img/texture/texture.hpp similarity index 100% rename from jvk/incl/jvk/data/img/texture/texture.hpp rename to legacy/jvk/incl/jvk/data/img/texture/texture.hpp diff --git a/jvk/incl/jvk/data/memory/memory.hpp b/legacy/jvk/incl/jvk/data/memory/memory.hpp similarity index 100% rename from jvk/incl/jvk/data/memory/memory.hpp rename to legacy/jvk/incl/jvk/data/memory/memory.hpp diff --git a/jvk/incl/jvk/debug/debug_messenger.hpp b/legacy/jvk/incl/jvk/debug/debug_messenger.hpp similarity index 100% rename from jvk/incl/jvk/debug/debug_messenger.hpp rename to legacy/jvk/incl/jvk/debug/debug_messenger.hpp diff --git a/jvk/incl/jvk/render/cmd/cmd_buffer.hpp b/legacy/jvk/incl/jvk/render/cmd/cmd_buffer.hpp similarity index 100% rename from jvk/incl/jvk/render/cmd/cmd_buffer.hpp rename to legacy/jvk/incl/jvk/render/cmd/cmd_buffer.hpp diff --git a/jvk/incl/jvk/render/cmd/cmd_buffer_config.hpp b/legacy/jvk/incl/jvk/render/cmd/cmd_buffer_config.hpp similarity index 100% rename from jvk/incl/jvk/render/cmd/cmd_buffer_config.hpp rename to legacy/jvk/incl/jvk/render/cmd/cmd_buffer_config.hpp diff --git a/jvk/incl/jvk/render/cmd/cmd_pool.hpp b/legacy/jvk/incl/jvk/render/cmd/cmd_pool.hpp similarity index 100% rename from jvk/incl/jvk/render/cmd/cmd_pool.hpp rename to legacy/jvk/incl/jvk/render/cmd/cmd_pool.hpp diff --git a/jvk/incl/jvk/render/pass/attachment/attachment_collection.hpp b/legacy/jvk/incl/jvk/render/pass/attachment/attachment_collection.hpp similarity index 100% rename from jvk/incl/jvk/render/pass/attachment/attachment_collection.hpp rename to legacy/jvk/incl/jvk/render/pass/attachment/attachment_collection.hpp diff --git a/jvk/incl/jvk/render/pass/attachment/attachment_collection_config.hpp b/legacy/jvk/incl/jvk/render/pass/attachment/attachment_collection_config.hpp similarity index 100% rename from jvk/incl/jvk/render/pass/attachment/attachment_collection_config.hpp rename to legacy/jvk/incl/jvk/render/pass/attachment/attachment_collection_config.hpp diff --git a/jvk/incl/jvk/render/pass/attachment/attachment_description.hpp b/legacy/jvk/incl/jvk/render/pass/attachment/attachment_description.hpp similarity index 100% rename from jvk/incl/jvk/render/pass/attachment/attachment_description.hpp rename to legacy/jvk/incl/jvk/render/pass/attachment/attachment_description.hpp diff --git a/jvk/incl/jvk/render/pass/framebuffer/fb_collection.hpp b/legacy/jvk/incl/jvk/render/pass/framebuffer/fb_collection.hpp similarity index 100% rename from jvk/incl/jvk/render/pass/framebuffer/fb_collection.hpp rename to legacy/jvk/incl/jvk/render/pass/framebuffer/fb_collection.hpp diff --git a/jvk/incl/jvk/render/pass/framebuffer/fb_collection_config.hpp b/legacy/jvk/incl/jvk/render/pass/framebuffer/fb_collection_config.hpp similarity index 100% rename from jvk/incl/jvk/render/pass/framebuffer/fb_collection_config.hpp rename to legacy/jvk/incl/jvk/render/pass/framebuffer/fb_collection_config.hpp diff --git a/jvk/incl/jvk/render/pass/framebuffer/framebuffer.hpp b/legacy/jvk/incl/jvk/render/pass/framebuffer/framebuffer.hpp similarity index 100% rename from jvk/incl/jvk/render/pass/framebuffer/framebuffer.hpp rename to legacy/jvk/incl/jvk/render/pass/framebuffer/framebuffer.hpp diff --git a/jvk/incl/jvk/render/pass/framebuffer/swapchain_fb_collection.hpp b/legacy/jvk/incl/jvk/render/pass/framebuffer/swapchain_fb_collection.hpp similarity index 100% rename from jvk/incl/jvk/render/pass/framebuffer/swapchain_fb_collection.hpp rename to legacy/jvk/incl/jvk/render/pass/framebuffer/swapchain_fb_collection.hpp diff --git a/jvk/incl/jvk/render/pass/render_pass.hpp b/legacy/jvk/incl/jvk/render/pass/render_pass.hpp similarity index 100% rename from jvk/incl/jvk/render/pass/render_pass.hpp rename to legacy/jvk/incl/jvk/render/pass/render_pass.hpp diff --git a/jvk/incl/jvk/render/pass/render_pass_begin_config.hpp b/legacy/jvk/incl/jvk/render/pass/render_pass_begin_config.hpp similarity index 100% rename from jvk/incl/jvk/render/pass/render_pass_begin_config.hpp rename to legacy/jvk/incl/jvk/render/pass/render_pass_begin_config.hpp diff --git a/jvk/incl/jvk/render/pass/render_pass_config.hpp b/legacy/jvk/incl/jvk/render/pass/render_pass_config.hpp similarity index 100% rename from jvk/incl/jvk/render/pass/render_pass_config.hpp rename to legacy/jvk/incl/jvk/render/pass/render_pass_config.hpp diff --git a/jvk/incl/jvk/render/pass/subpass.hpp b/legacy/jvk/incl/jvk/render/pass/subpass.hpp similarity index 100% rename from jvk/incl/jvk/render/pass/subpass.hpp rename to legacy/jvk/incl/jvk/render/pass/subpass.hpp diff --git a/jvk/incl/jvk/render/pipeline/layout/descriptor_set_layout.hpp b/legacy/jvk/incl/jvk/render/pipeline/layout/descriptor_set_layout.hpp similarity index 100% rename from jvk/incl/jvk/render/pipeline/layout/descriptor_set_layout.hpp rename to legacy/jvk/incl/jvk/render/pipeline/layout/descriptor_set_layout.hpp diff --git a/jvk/incl/jvk/render/pipeline/layout/pipeline_layout.hpp b/legacy/jvk/incl/jvk/render/pipeline/layout/pipeline_layout.hpp similarity index 100% rename from jvk/incl/jvk/render/pipeline/layout/pipeline_layout.hpp rename to legacy/jvk/incl/jvk/render/pipeline/layout/pipeline_layout.hpp diff --git a/jvk/incl/jvk/render/pipeline/pipeline.hpp b/legacy/jvk/incl/jvk/render/pipeline/pipeline.hpp similarity index 100% rename from jvk/incl/jvk/render/pipeline/pipeline.hpp rename to legacy/jvk/incl/jvk/render/pipeline/pipeline.hpp diff --git a/jvk/incl/jvk/render/pipeline/pipeline_vertex_description.hpp b/legacy/jvk/incl/jvk/render/pipeline/pipeline_vertex_description.hpp similarity index 100% rename from jvk/incl/jvk/render/pipeline/pipeline_vertex_description.hpp rename to legacy/jvk/incl/jvk/render/pipeline/pipeline_vertex_description.hpp diff --git a/jvk/incl/jvk/render/pipeline/shader/shader.hpp b/legacy/jvk/incl/jvk/render/pipeline/shader/shader.hpp similarity index 100% rename from jvk/incl/jvk/render/pipeline/shader/shader.hpp rename to legacy/jvk/incl/jvk/render/pipeline/shader/shader.hpp diff --git a/jvk/incl/jvk/render/sync/fence.hpp b/legacy/jvk/incl/jvk/render/sync/fence.hpp similarity index 100% rename from jvk/incl/jvk/render/sync/fence.hpp rename to legacy/jvk/incl/jvk/render/sync/fence.hpp diff --git a/jvk/incl/jvk/render/sync/os_fence_handle.hpp b/legacy/jvk/incl/jvk/render/sync/os_fence_handle.hpp similarity index 100% rename from jvk/incl/jvk/render/sync/os_fence_handle.hpp rename to legacy/jvk/incl/jvk/render/sync/os_fence_handle.hpp diff --git a/jvk/incl/jvk/render/sync/pipeline_barrier.hpp b/legacy/jvk/incl/jvk/render/sync/pipeline_barrier.hpp similarity index 100% rename from jvk/incl/jvk/render/sync/pipeline_barrier.hpp rename to legacy/jvk/incl/jvk/render/sync/pipeline_barrier.hpp diff --git a/jvk/incl/jvk/render/sync/pipeline_wait_stage.hpp b/legacy/jvk/incl/jvk/render/sync/pipeline_wait_stage.hpp similarity index 100% rename from jvk/incl/jvk/render/sync/pipeline_wait_stage.hpp rename to legacy/jvk/incl/jvk/render/sync/pipeline_wait_stage.hpp diff --git a/jvk/incl/jvk/render/sync/semaphore.hpp b/legacy/jvk/incl/jvk/render/sync/semaphore.hpp similarity index 100% rename from jvk/incl/jvk/render/sync/semaphore.hpp rename to legacy/jvk/incl/jvk/render/sync/semaphore.hpp diff --git a/jvk/incl/jvk/render/sync/timline_semaphore.hpp b/legacy/jvk/incl/jvk/render/sync/timline_semaphore.hpp similarity index 100% rename from jvk/incl/jvk/render/sync/timline_semaphore.hpp rename to legacy/jvk/incl/jvk/render/sync/timline_semaphore.hpp diff --git a/jvk/incl/jvk/surface/surface.hpp b/legacy/jvk/incl/jvk/surface/surface.hpp similarity index 100% rename from jvk/incl/jvk/surface/surface.hpp rename to legacy/jvk/incl/jvk/surface/surface.hpp diff --git a/jvk/incl/jvk/surface/surface_config.hpp b/legacy/jvk/incl/jvk/surface/surface_config.hpp similarity index 100% rename from jvk/incl/jvk/surface/surface_config.hpp rename to legacy/jvk/incl/jvk/surface/surface_config.hpp diff --git a/jvk/incl/jvk/surface/swapchain/swapchain.hpp b/legacy/jvk/incl/jvk/surface/swapchain/swapchain.hpp similarity index 100% rename from jvk/incl/jvk/surface/swapchain/swapchain.hpp rename to legacy/jvk/incl/jvk/surface/swapchain/swapchain.hpp diff --git a/jvk/incl/jvk/surface/swapchain/swapchain_config.hpp b/legacy/jvk/incl/jvk/surface/swapchain/swapchain_config.hpp similarity index 100% rename from jvk/incl/jvk/surface/swapchain/swapchain_config.hpp rename to legacy/jvk/incl/jvk/surface/swapchain/swapchain_config.hpp diff --git a/jvk/incl/jvk/surface/swapchain/swapchain_subpass_config.hpp b/legacy/jvk/incl/jvk/surface/swapchain/swapchain_subpass_config.hpp similarity index 100% rename from jvk/incl/jvk/surface/swapchain/swapchain_subpass_config.hpp rename to legacy/jvk/incl/jvk/surface/swapchain/swapchain_subpass_config.hpp diff --git a/jvk/incl/jvk/utility/constant/debug_constants.hpp b/legacy/jvk/incl/jvk/utility/constant/debug_constants.hpp similarity index 100% rename from jvk/incl/jvk/utility/constant/debug_constants.hpp rename to legacy/jvk/incl/jvk/utility/constant/debug_constants.hpp diff --git a/jvk/incl/jvk/utility/constant/device_constants.hpp b/legacy/jvk/incl/jvk/utility/constant/device_constants.hpp similarity index 100% rename from jvk/incl/jvk/utility/constant/device_constants.hpp rename to legacy/jvk/incl/jvk/utility/constant/device_constants.hpp diff --git a/jvk/incl/jvk/utility/constants.hpp b/legacy/jvk/incl/jvk/utility/constants.hpp similarity index 100% rename from jvk/incl/jvk/utility/constants.hpp rename to legacy/jvk/incl/jvk/utility/constants.hpp diff --git a/jvk/incl/jvk/utility/device/physical_device_features.hpp b/legacy/jvk/incl/jvk/utility/device/physical_device_features.hpp similarity index 100% rename from jvk/incl/jvk/utility/device/physical_device_features.hpp rename to legacy/jvk/incl/jvk/utility/device/physical_device_features.hpp diff --git a/jvk/incl/jvk/utility/format.hpp b/legacy/jvk/incl/jvk/utility/format.hpp similarity index 100% rename from jvk/incl/jvk/utility/format.hpp rename to legacy/jvk/incl/jvk/utility/format.hpp diff --git a/jvk/incl/jvk/utility/format/format_string.hpp b/legacy/jvk/incl/jvk/utility/format/format_string.hpp similarity index 100% rename from jvk/incl/jvk/utility/format/format_string.hpp rename to legacy/jvk/incl/jvk/utility/format/format_string.hpp diff --git a/jvk/incl/jvk/utility/format/vk_to_string.hpp b/legacy/jvk/incl/jvk/utility/format/vk_to_string.hpp similarity index 100% rename from jvk/incl/jvk/utility/format/vk_to_string.hpp rename to legacy/jvk/incl/jvk/utility/format/vk_to_string.hpp diff --git a/jvk/incl/jvk/utility/format/vk_to_string.inl b/legacy/jvk/incl/jvk/utility/format/vk_to_string.inl similarity index 100% rename from jvk/incl/jvk/utility/format/vk_to_string.inl rename to legacy/jvk/incl/jvk/utility/format/vk_to_string.inl diff --git a/jvk/incl/jvk/utility/os/os_constants.hpp b/legacy/jvk/incl/jvk/utility/os/os_constants.hpp similarity index 100% rename from jvk/incl/jvk/utility/os/os_constants.hpp rename to legacy/jvk/incl/jvk/utility/os/os_constants.hpp diff --git a/jvk/incl/jvk/utility/os/os_def.hpp b/legacy/jvk/incl/jvk/utility/os/os_def.hpp similarity index 100% rename from jvk/incl/jvk/utility/os/os_def.hpp rename to legacy/jvk/incl/jvk/utility/os/os_def.hpp diff --git a/jvk/incl/jvk/utility/os/os_exception.hpp b/legacy/jvk/incl/jvk/utility/os/os_exception.hpp similarity index 100% rename from jvk/incl/jvk/utility/os/os_exception.hpp rename to legacy/jvk/incl/jvk/utility/os/os_exception.hpp diff --git a/jvk/incl/jvk/utility/os/os_helper_include.hpp b/legacy/jvk/incl/jvk/utility/os/os_helper_include.hpp similarity index 100% rename from jvk/incl/jvk/utility/os/os_helper_include.hpp rename to legacy/jvk/incl/jvk/utility/os/os_helper_include.hpp diff --git a/jvk/incl/jvk/utility/os/os_window.hpp b/legacy/jvk/incl/jvk/utility/os/os_window.hpp similarity index 100% rename from jvk/incl/jvk/utility/os/os_window.hpp rename to legacy/jvk/incl/jvk/utility/os/os_window.hpp diff --git a/jvk/incl/jvk/utility/types.hpp b/legacy/jvk/incl/jvk/utility/types.hpp similarity index 100% rename from jvk/incl/jvk/utility/types.hpp rename to legacy/jvk/incl/jvk/utility/types.hpp diff --git a/jvk/incl/jvk/utility/vk_exceptions.hpp b/legacy/jvk/incl/jvk/utility/vk_exceptions.hpp similarity index 100% rename from jvk/incl/jvk/utility/vk_exceptions.hpp rename to legacy/jvk/incl/jvk/utility/vk_exceptions.hpp diff --git a/jvk/incl/jvk/utility/vk_hash.hpp b/legacy/jvk/incl/jvk/utility/vk_hash.hpp similarity index 100% rename from jvk/incl/jvk/utility/vk_hash.hpp rename to legacy/jvk/incl/jvk/utility/vk_hash.hpp diff --git a/jvk/incl/jvk/utility/vk_overloads.hpp b/legacy/jvk/incl/jvk/utility/vk_overloads.hpp similarity index 100% rename from jvk/incl/jvk/utility/vk_overloads.hpp rename to legacy/jvk/incl/jvk/utility/vk_overloads.hpp diff --git a/jvk/src/base/core.cpp b/legacy/jvk/src/base/core.cpp similarity index 100% rename from jvk/src/base/core.cpp rename to legacy/jvk/src/base/core.cpp diff --git a/jvk/src/base/device.cpp b/legacy/jvk/src/base/device.cpp similarity index 100% rename from jvk/src/base/device.cpp rename to legacy/jvk/src/base/device.cpp diff --git a/jvk/src/base/instance.cpp b/legacy/jvk/src/base/instance.cpp similarity index 100% rename from jvk/src/base/instance.cpp rename to legacy/jvk/src/base/instance.cpp diff --git a/jvk/src/base/jvk.cpp b/legacy/jvk/src/base/jvk.cpp similarity index 100% rename from jvk/src/base/jvk.cpp rename to legacy/jvk/src/base/jvk.cpp diff --git a/jvk/src/base/physical_device.cpp b/legacy/jvk/src/base/physical_device.cpp similarity index 100% rename from jvk/src/base/physical_device.cpp rename to legacy/jvk/src/base/physical_device.cpp diff --git a/jvk/src/base/queue/present_info.cpp b/legacy/jvk/src/base/queue/present_info.cpp similarity index 100% rename from jvk/src/base/queue/present_info.cpp rename to legacy/jvk/src/base/queue/present_info.cpp diff --git a/jvk/src/base/queue/queue.cpp b/legacy/jvk/src/base/queue/queue.cpp similarity index 100% rename from jvk/src/base/queue/queue.cpp rename to legacy/jvk/src/base/queue/queue.cpp diff --git a/jvk/src/base/queue/submit_info.cpp b/legacy/jvk/src/base/queue/submit_info.cpp similarity index 100% rename from jvk/src/base/queue/submit_info.cpp rename to legacy/jvk/src/base/queue/submit_info.cpp diff --git a/jvk/src/data/buffer/base_buffer.cpp b/legacy/jvk/src/data/buffer/base_buffer.cpp similarity index 100% rename from jvk/src/data/buffer/base_buffer.cpp rename to legacy/jvk/src/data/buffer/base_buffer.cpp diff --git a/jvk/src/data/descriptor/descriptor_pool.cpp b/legacy/jvk/src/data/descriptor/descriptor_pool.cpp similarity index 100% rename from jvk/src/data/descriptor/descriptor_pool.cpp rename to legacy/jvk/src/data/descriptor/descriptor_pool.cpp diff --git a/jvk/src/data/descriptor/descriptor_set.cpp b/legacy/jvk/src/data/descriptor/descriptor_set.cpp similarity index 100% rename from jvk/src/data/descriptor/descriptor_set.cpp rename to legacy/jvk/src/data/descriptor/descriptor_set.cpp diff --git a/jvk/src/data/destruction_queue.cpp b/legacy/jvk/src/data/destruction_queue.cpp similarity index 100% rename from jvk/src/data/destruction_queue.cpp rename to legacy/jvk/src/data/destruction_queue.cpp diff --git a/jvk/src/data/img/image.cpp b/legacy/jvk/src/data/img/image.cpp similarity index 100% rename from jvk/src/data/img/image.cpp rename to legacy/jvk/src/data/img/image.cpp diff --git a/jvk/src/data/img/image_config.cpp b/legacy/jvk/src/data/img/image_config.cpp similarity index 100% rename from jvk/src/data/img/image_config.cpp rename to legacy/jvk/src/data/img/image_config.cpp diff --git a/jvk/src/data/img/image_view.cpp b/legacy/jvk/src/data/img/image_view.cpp similarity index 100% rename from jvk/src/data/img/image_view.cpp rename to legacy/jvk/src/data/img/image_view.cpp diff --git a/jvk/src/data/img/texture/sampler.cpp b/legacy/jvk/src/data/img/texture/sampler.cpp similarity index 100% rename from jvk/src/data/img/texture/sampler.cpp rename to legacy/jvk/src/data/img/texture/sampler.cpp diff --git a/jvk/src/data/img/texture/texture.cpp b/legacy/jvk/src/data/img/texture/texture.cpp similarity index 100% rename from jvk/src/data/img/texture/texture.cpp rename to legacy/jvk/src/data/img/texture/texture.cpp diff --git a/jvk/src/data/memory/memory.cpp b/legacy/jvk/src/data/memory/memory.cpp similarity index 100% rename from jvk/src/data/memory/memory.cpp rename to legacy/jvk/src/data/memory/memory.cpp diff --git a/jvk/src/debug/debug_messenger.cpp b/legacy/jvk/src/debug/debug_messenger.cpp similarity index 100% rename from jvk/src/debug/debug_messenger.cpp rename to legacy/jvk/src/debug/debug_messenger.cpp diff --git a/jvk/src/pch/glob/boost.hpp b/legacy/jvk/src/pch/glob/boost.hpp similarity index 100% rename from jvk/src/pch/glob/boost.hpp rename to legacy/jvk/src/pch/glob/boost.hpp diff --git a/jvk/src/pch/glob/cth.hpp b/legacy/jvk/src/pch/glob/cth.hpp similarity index 100% rename from jvk/src/pch/glob/cth.hpp rename to legacy/jvk/src/pch/glob/cth.hpp diff --git a/jvk/src/pch/glob/glm.hpp b/legacy/jvk/src/pch/glob/glm.hpp similarity index 100% rename from jvk/src/pch/glob/glm.hpp rename to legacy/jvk/src/pch/glob/glm.hpp diff --git a/jvk/src/pch/glob/gsl.hpp b/legacy/jvk/src/pch/glob/gsl.hpp similarity index 100% rename from jvk/src/pch/glob/gsl.hpp rename to legacy/jvk/src/pch/glob/gsl.hpp diff --git a/jvk/src/pch/glob/std.hpp b/legacy/jvk/src/pch/glob/std.hpp similarity index 100% rename from jvk/src/pch/glob/std.hpp rename to legacy/jvk/src/pch/glob/std.hpp diff --git a/jvk/src/pch/glob/volk.hpp b/legacy/jvk/src/pch/glob/volk.hpp similarity index 100% rename from jvk/src/pch/glob/volk.hpp rename to legacy/jvk/src/pch/glob/volk.hpp diff --git a/jvk/src/pch/jvk/pch_def.hpp b/legacy/jvk/src/pch/jvk/pch_def.hpp similarity index 100% rename from jvk/src/pch/jvk/pch_def.hpp rename to legacy/jvk/src/pch/jvk/pch_def.hpp diff --git a/jvk/src/pch/lib/volk/volk.cpp b/legacy/jvk/src/pch/lib/volk/volk.cpp similarity index 100% rename from jvk/src/pch/lib/volk/volk.cpp rename to legacy/jvk/src/pch/lib/volk/volk.cpp diff --git a/jvk/src/pch/pch.cpp b/legacy/jvk/src/pch/pch.cpp similarity index 100% rename from jvk/src/pch/pch.cpp rename to legacy/jvk/src/pch/pch.cpp diff --git a/jvk/src/pch/pch.hpp b/legacy/jvk/src/pch/pch.hpp similarity index 100% rename from jvk/src/pch/pch.hpp rename to legacy/jvk/src/pch/pch.hpp diff --git a/jvk/src/render/cmd/cmd_buffer.cpp b/legacy/jvk/src/render/cmd/cmd_buffer.cpp similarity index 100% rename from jvk/src/render/cmd/cmd_buffer.cpp rename to legacy/jvk/src/render/cmd/cmd_buffer.cpp diff --git a/jvk/src/render/cmd/cmd_pool.cpp b/legacy/jvk/src/render/cmd/cmd_pool.cpp similarity index 100% rename from jvk/src/render/cmd/cmd_pool.cpp rename to legacy/jvk/src/render/cmd/cmd_pool.cpp diff --git a/jvk/src/render/pass/attachment/attachment_collection.cpp b/legacy/jvk/src/render/pass/attachment/attachment_collection.cpp similarity index 100% rename from jvk/src/render/pass/attachment/attachment_collection.cpp rename to legacy/jvk/src/render/pass/attachment/attachment_collection.cpp diff --git a/jvk/src/render/pass/attachment/attachment_description.cpp b/legacy/jvk/src/render/pass/attachment/attachment_description.cpp similarity index 100% rename from jvk/src/render/pass/attachment/attachment_description.cpp rename to legacy/jvk/src/render/pass/attachment/attachment_description.cpp diff --git a/jvk/src/render/pass/framebuffer/fb_collection.cpp b/legacy/jvk/src/render/pass/framebuffer/fb_collection.cpp similarity index 100% rename from jvk/src/render/pass/framebuffer/fb_collection.cpp rename to legacy/jvk/src/render/pass/framebuffer/fb_collection.cpp diff --git a/jvk/src/render/pass/framebuffer/fb_collection_config.cpp b/legacy/jvk/src/render/pass/framebuffer/fb_collection_config.cpp similarity index 100% rename from jvk/src/render/pass/framebuffer/fb_collection_config.cpp rename to legacy/jvk/src/render/pass/framebuffer/fb_collection_config.cpp diff --git a/jvk/src/render/pass/framebuffer/framebuffer.cpp b/legacy/jvk/src/render/pass/framebuffer/framebuffer.cpp similarity index 100% rename from jvk/src/render/pass/framebuffer/framebuffer.cpp rename to legacy/jvk/src/render/pass/framebuffer/framebuffer.cpp diff --git a/jvk/src/render/pass/framebuffer/swapchain_fb_collection.cpp b/legacy/jvk/src/render/pass/framebuffer/swapchain_fb_collection.cpp similarity index 100% rename from jvk/src/render/pass/framebuffer/swapchain_fb_collection.cpp rename to legacy/jvk/src/render/pass/framebuffer/swapchain_fb_collection.cpp diff --git a/jvk/src/render/pass/render_pass.cpp b/legacy/jvk/src/render/pass/render_pass.cpp similarity index 100% rename from jvk/src/render/pass/render_pass.cpp rename to legacy/jvk/src/render/pass/render_pass.cpp diff --git a/jvk/src/render/pass/subpass.cpp b/legacy/jvk/src/render/pass/subpass.cpp similarity index 100% rename from jvk/src/render/pass/subpass.cpp rename to legacy/jvk/src/render/pass/subpass.cpp diff --git a/jvk/src/render/pipeline/layout/descriptor_set_layout.cpp b/legacy/jvk/src/render/pipeline/layout/descriptor_set_layout.cpp similarity index 100% rename from jvk/src/render/pipeline/layout/descriptor_set_layout.cpp rename to legacy/jvk/src/render/pipeline/layout/descriptor_set_layout.cpp diff --git a/jvk/src/render/pipeline/layout/pipeline_layout.cpp b/legacy/jvk/src/render/pipeline/layout/pipeline_layout.cpp similarity index 100% rename from jvk/src/render/pipeline/layout/pipeline_layout.cpp rename to legacy/jvk/src/render/pipeline/layout/pipeline_layout.cpp diff --git a/jvk/src/render/pipeline/pipeline.cpp b/legacy/jvk/src/render/pipeline/pipeline.cpp similarity index 100% rename from jvk/src/render/pipeline/pipeline.cpp rename to legacy/jvk/src/render/pipeline/pipeline.cpp diff --git a/jvk/src/render/pipeline/shader/shader.cpp b/legacy/jvk/src/render/pipeline/shader/shader.cpp similarity index 100% rename from jvk/src/render/pipeline/shader/shader.cpp rename to legacy/jvk/src/render/pipeline/shader/shader.cpp diff --git a/jvk/src/render/sync/fence.cpp b/legacy/jvk/src/render/sync/fence.cpp similarity index 100% rename from jvk/src/render/sync/fence.cpp rename to legacy/jvk/src/render/sync/fence.cpp diff --git a/jvk/src/render/sync/os_fence_handle.cpp b/legacy/jvk/src/render/sync/os_fence_handle.cpp similarity index 100% rename from jvk/src/render/sync/os_fence_handle.cpp rename to legacy/jvk/src/render/sync/os_fence_handle.cpp diff --git a/jvk/src/render/sync/pipeline_barrier.cpp b/legacy/jvk/src/render/sync/pipeline_barrier.cpp similarity index 100% rename from jvk/src/render/sync/pipeline_barrier.cpp rename to legacy/jvk/src/render/sync/pipeline_barrier.cpp diff --git a/jvk/src/render/sync/semaphore.cpp b/legacy/jvk/src/render/sync/semaphore.cpp similarity index 100% rename from jvk/src/render/sync/semaphore.cpp rename to legacy/jvk/src/render/sync/semaphore.cpp diff --git a/jvk/src/render/sync/timline_semaphore.cpp b/legacy/jvk/src/render/sync/timline_semaphore.cpp similarity index 100% rename from jvk/src/render/sync/timline_semaphore.cpp rename to legacy/jvk/src/render/sync/timline_semaphore.cpp diff --git a/jvk/src/surface/surface.cpp b/legacy/jvk/src/surface/surface.cpp similarity index 100% rename from jvk/src/surface/surface.cpp rename to legacy/jvk/src/surface/surface.cpp diff --git a/jvk/src/surface/swapchain/swapchain.cpp b/legacy/jvk/src/surface/swapchain/swapchain.cpp similarity index 100% rename from jvk/src/surface/swapchain/swapchain.cpp rename to legacy/jvk/src/surface/swapchain/swapchain.cpp diff --git a/jvk/src/surface/swapchain/swapchain_subpass_config.cpp b/legacy/jvk/src/surface/swapchain/swapchain_subpass_config.cpp similarity index 100% rename from jvk/src/surface/swapchain/swapchain_subpass_config.cpp rename to legacy/jvk/src/surface/swapchain/swapchain_subpass_config.cpp diff --git a/jvk/src/utility/device/physical_device_features.cpp b/legacy/jvk/src/utility/device/physical_device_features.cpp similarity index 100% rename from jvk/src/utility/device/physical_device_features.cpp rename to legacy/jvk/src/utility/device/physical_device_features.cpp diff --git a/jvk/src/utility/os/incl_os.hpp b/legacy/jvk/src/utility/os/incl_os.hpp similarity index 100% rename from jvk/src/utility/os/incl_os.hpp rename to legacy/jvk/src/utility/os/incl_os.hpp diff --git a/jvk/src/utility/os/incl_posix.hpp b/legacy/jvk/src/utility/os/incl_posix.hpp similarity index 100% rename from jvk/src/utility/os/incl_posix.hpp rename to legacy/jvk/src/utility/os/incl_posix.hpp diff --git a/jvk/src/utility/os/os_window.cpp b/legacy/jvk/src/utility/os/os_window.cpp similarity index 100% rename from jvk/src/utility/os/os_window.cpp rename to legacy/jvk/src/utility/os/os_window.cpp diff --git a/jvk/src/utility/os/win/incl_windows.hpp b/legacy/jvk/src/utility/os/win/incl_windows.hpp similarity index 100% rename from jvk/src/utility/os/win/incl_windows.hpp rename to legacy/jvk/src/utility/os/win/incl_windows.hpp diff --git a/jvk/src/utility/os/win/win_window.hpp b/legacy/jvk/src/utility/os/win/win_window.hpp similarity index 100% rename from jvk/src/utility/os/win/win_window.hpp rename to legacy/jvk/src/utility/os/win/win_window.hpp diff --git a/legacy/tests/CMakeLists.txt b/legacy/tests/CMakeLists.txt new file mode 100644 index 00000000..4a827f09 --- /dev/null +++ b/legacy/tests/CMakeLists.txt @@ -0,0 +1,16 @@ +cth_glob_cpp(TEST_SRC "src") + +add_executable(tests ${TEST_SRC}) + +target_include_directories( + tests PRIVATE + incl + ../../cth/ +) + +find_package(GTest CONFIG REQUIRED) + +target_link_libraries(tests PRIVATE GTest::gtest_main jvk jolly) + +include(GoogleTest) +gtest_discover_tests(tests) \ No newline at end of file diff --git a/tests/src/test_utils/pch.cpp b/legacy/tests/src/test_utils/pch.cpp similarity index 100% rename from tests/src/test_utils/pch.cpp rename to legacy/tests/src/test_utils/pch.cpp diff --git a/tests/src/test_utils/pch.h b/legacy/tests/src/test_utils/pch.h similarity index 100% rename from tests/src/test_utils/pch.h rename to legacy/tests/src/test_utils/pch.h diff --git a/tests/src/vulkan/utility/format/format_string_test.cpp b/legacy/tests/src/vulkan/utility/format/format_string_test.cpp similarity index 100% rename from tests/src/vulkan/utility/format/format_string_test.cpp rename to legacy/tests/src/vulkan/utility/format/format_string_test.cpp diff --git a/lib/cmake/cth.cmake b/lib/cmake/cth.cmake new file mode 100644 index 00000000..05376501 --- /dev/null +++ b/lib/cmake/cth.cmake @@ -0,0 +1,13 @@ +# cth from the lib/cth submodule +if(NOT TARGET cth::cth) + if(NOT EXISTS "${CMAKE_CURRENT_LIST_DIR}/../cth/CMakeLists.txt") + message(FATAL_ERROR "lib/cth is empty, run: git submodule update --init lib/cth") + endif() + + # the engine needs cth and cth_os + set(CTH_BUILD_CORO OFF) + set(CTH_BUILD_NET OFF) + set(CTH_BUILD_WIN OFF) + + add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/../cth" "${CMAKE_BINARY_DIR}/lib/cth") +endif() diff --git a/lib/cmake/rgfw.cmake b/lib/cmake/rgfw.cmake new file mode 100644 index 00000000..cefa0a64 --- /dev/null +++ b/lib/cmake/rgfw.cmake @@ -0,0 +1,80 @@ +# RGFW from the lib/rgfw submodule: the header with its platform libraries +# cgfw compiles the implementation (RGFW_IMPLEMENTATION) in one C source file +# platforms: Win32 and Wayland, no X11, no OpenGL and no EGL +if(NOT TARGET rgfw::rgfw) + set(RGFW_DIR "${CMAKE_CURRENT_LIST_DIR}/../rgfw") + if(NOT EXISTS "${RGFW_DIR}/RGFW.h") + message(FATAL_ERROR "lib/rgfw is empty, run: git submodule update --init lib/rgfw") + endif() + + add_library(rgfw INTERFACE) + add_library(rgfw::rgfw ALIAS rgfw) + + # the implementation and the generated protocol code are C + enable_language(C) + + target_include_directories(rgfw INTERFACE "${RGFW_DIR}") + # RGFWDEF empty: the functions have external linkage, the default inline needs the definitions in every user + target_compile_definitions(rgfw INTERFACE RGFW_VULKAN "RGFWDEF=") + + if(WIN32) + # RGFW includes windows.h: no min and max macros + target_compile_definitions(rgfw INTERFACE NOMINMAX) + # RGFW loads winmm, dwmapi, shcore and opengl32 at run time + target_link_libraries(rgfw INTERFACE gdi32 user32 shell32) + elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_compile_definitions(rgfw INTERFACE RGFW_WAYLAND RGFW_NO_X11) + + # wayland and xkbcommon come from the system, the vcpkg ports of them are empty on linux + find_package(PkgConfig REQUIRED) + pkg_check_modules(RGFW_WAYLAND REQUIRED IMPORTED_TARGET wayland-client wayland-cursor xkbcommon) + find_program(WAYLAND_SCANNER wayland-scanner REQUIRED) + + # the protocol XML files of the vcpkg port wayland-protocols + pkg_get_variable(WAYLAND_PROTOCOLS_DIR wayland-protocols pkgdatadir) + if(NOT WAYLAND_PROTOCOLS_DIR) + message(FATAL_ERROR "wayland-protocols not found, vcpkg installs it on linux") + endif() + + # the protocols that RGFW includes, see the xdg-shell.c rule in lib/rgfw/Makefile + set(RGFW_WAYLAND_PROTOCOLS + "stable/xdg-shell/xdg-shell.xml" + "unstable/xdg-decoration/xdg-decoration-unstable-v1.xml" + "staging/xdg-toplevel-icon/xdg-toplevel-icon-v1.xml" + "unstable/xdg-output/xdg-output-unstable-v1.xml" + "unstable/relative-pointer/relative-pointer-unstable-v1.xml" + "unstable/pointer-constraints/pointer-constraints-unstable-v1.xml" + ) + + set(RGFW_PROTOCOL_DIR "${CMAKE_BINARY_DIR}/lib/rgfw/wayland") + file(MAKE_DIRECTORY "${RGFW_PROTOCOL_DIR}") + + set(RGFW_PROTOCOL_FILES "") + foreach(protocol IN LISTS RGFW_WAYLAND_PROTOCOLS) + get_filename_component(name "${protocol}" NAME_WE) + set(xml "${WAYLAND_PROTOCOLS_DIR}/${protocol}") + set(header "${RGFW_PROTOCOL_DIR}/${name}.h") + set(code "${RGFW_PROTOCOL_DIR}/${name}.c") + + add_custom_command( + OUTPUT "${header}" "${code}" + COMMAND "${WAYLAND_SCANNER}" client-header "${xml}" "${header}" + COMMAND "${WAYLAND_SCANNER}" public-code "${xml}" "${code}" + DEPENDS "${xml}" + COMMENT "wayland-scanner ${name}" + VERBATIM + ) + list(APPEND RGFW_PROTOCOL_FILES "${header}" "${code}") + endforeach() + + add_library(rgfw_wayland_protocols STATIC ${RGFW_PROTOCOL_FILES}) + target_include_directories(rgfw_wayland_protocols PUBLIC "${RGFW_PROTOCOL_DIR}") + target_link_libraries(rgfw_wayland_protocols PUBLIC PkgConfig::RGFW_WAYLAND) + + target_link_libraries(rgfw INTERFACE rgfw_wayland_protocols) + + #TODO libdecor for frames on GNOME, GNOME has no xdg-decoration (KDE and sway have it), vcpkg has no port + else() + message(FATAL_ERROR "RGFW: only Windows and Linux (Wayland) are supported, got ${CMAKE_SYSTEM_NAME}") + endif() +endif() diff --git a/lib/cth b/lib/cth new file mode 160000 index 00000000..9e94b4a5 --- /dev/null +++ b/lib/cth @@ -0,0 +1 @@ +Subproject commit 9e94b4a5b245883b2087a60d8614adc0290c8d11 diff --git a/lib/cth_cmake b/lib/cth_cmake index 34b81860..f81d24a7 160000 --- a/lib/cth_cmake +++ b/lib/cth_cmake @@ -1 +1 @@ -Subproject commit 34b818607e8ae63da2159a53a5d3f13208ea0210 +Subproject commit f81d24a7b99bf0fad8a1c887ef85dced779441b6 diff --git a/lib/rgfw b/lib/rgfw new file mode 160000 index 00000000..951c486a --- /dev/null +++ b/lib/rgfw @@ -0,0 +1 @@ +Subproject commit 951c486a8145f63b93aff62f3072c456d2fca3ab diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 4a827f09..593a6c9f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -2,15 +2,10 @@ cth_glob_cpp(TEST_SRC "src") add_executable(tests ${TEST_SRC}) -target_include_directories( - tests PRIVATE - incl - ../../cth/ -) - find_package(GTest CONFIG REQUIRED) +find_package(volk CONFIG REQUIRED) -target_link_libraries(tests PRIVATE GTest::gtest_main jvk jolly) +target_link_libraries(tests PRIVATE GTest::gtest_main volk::volk cth::cth) include(GoogleTest) -gtest_discover_tests(tests) \ No newline at end of file +gtest_discover_tests(tests) diff --git a/tests/src/loader_test.cpp b/tests/src/loader_test.cpp new file mode 100644 index 00000000..8cc3634b --- /dev/null +++ b/tests/src/loader_test.cpp @@ -0,0 +1,52 @@ +#include +#include + +#include + + +namespace { + +TEST(loader, supports_vulkan_1_3) { + ASSERT_EQ(volkInitialize(), VK_SUCCESS); + + uint32_t version = 0; + ASSERT_EQ(vkEnumerateInstanceVersion(&version), VK_SUCCESS); + EXPECT_GE(version, VK_API_VERSION_1_3); + + volkFinalize(); +} + +TEST(loader, finds_vulkan_1_3_device) { + ASSERT_EQ(volkInitialize(), VK_SUCCESS); + + VkApplicationInfo const appInfo{ + .sType = VK_STRUCTURE_TYPE_APPLICATION_INFO, + .apiVersion = VK_API_VERSION_1_3, + }; + VkInstanceCreateInfo const createInfo{ + .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, + .pApplicationInfo = &appInfo, + }; + + VkInstance instance = VK_NULL_HANDLE; + ASSERT_EQ(vkCreateInstance(&createInfo, nullptr, &instance), VK_SUCCESS); + volkLoadInstanceOnly(instance); + + uint32_t deviceCount = 0; + ASSERT_EQ(vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr), VK_SUCCESS); + std::vector devices(deviceCount); + ASSERT_EQ(vkEnumeratePhysicalDevices(instance, &deviceCount, devices.data()), VK_SUCCESS); + + bool found = false; + for(auto const device : devices) { + VkPhysicalDeviceProperties properties{}; + vkGetPhysicalDeviceProperties(device, &properties); + found |= properties.apiVersion >= VK_API_VERSION_1_3; + } + EXPECT_TRUE(found); + + vkDestroyInstance(instance, nullptr); + volkFinalize(); +} + +} diff --git a/vcpkg-configuration.json b/vcpkg-configuration.json index 5375da75..bbd85de6 100644 --- a/vcpkg-configuration.json +++ b/vcpkg-configuration.json @@ -1,13 +1,14 @@ { "default-registry": { - "kind": "builtin", - "baseline": "6ecbbbdf31cba47aafa7cf6189b1e73e10ac61f8" + "kind": "git", + "baseline": "3cbc1db4d867ec83c89fba4c461321c11f78b5e3", + "repository": "https://github.com/microsoft/vcpkg" }, "registries": [ { - "kind": "builtin", - "baseline": "6ecbbbdf31cba47aafa7cf6189b1e73e10ac61f8", - "packages": [] + "kind": "artifact", + "location": "https://github.com/microsoft/vcpkg-ce-catalog/archive/refs/heads/main.zip", + "name": "microsoft" } ] } diff --git a/vcpkg.json b/vcpkg.json index 01323ebe..33d4ab7c 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,13 +1,18 @@ { "dependencies": [ - "boost-asio", "boost-pfr", - "glfw3", - "glm", + "gtl", "gtest", - "ms-gsl", - "range-v3", - "stb", - "volk" + "volk", + "vulkan-memory-allocator", + "vulkan-utility-libraries", + { + "name": "wayland", + "platform": "linux" + }, + { + "name": "wayland-protocols", + "platform": "linux" + } ] }