From fbeba9eb5fda781b935575a1b1561c3f74f1e5a1 Mon Sep 17 00:00:00 2001 From: Jupeyy Date: Sun, 20 Mar 2022 18:04:00 +0100 Subject: [PATCH] Add Vulkan backend --- .github/workflows/build.yaml | 8 +- .github/workflows/clang-tidy.yml | 2 +- .github/workflows/codeql-analysis.yaml | 2 +- .github/workflows/style.yml | 2 +- CMakeLists.txt | 65 +- README.md | 12 +- cmake/BuildVulkanShaders.cmake | 175 + cmake/FindVulkan.cmake | 61 + data/editor/background.png | Bin 53387 -> 56183 bytes data/editor/checker.png | Bin 103 -> 114 bytes data/shader/vulkan/prim.frag | 20 + data/shader/vulkan/prim.vert | 20 + data/shader/vulkan/prim3d.frag | 24 + data/shader/vulkan/prim3d.vert | 24 + data/shader/vulkan/primex.frag | 24 + data/shader/vulkan/primex.vert | 33 + data/shader/vulkan/quad.frag | 62 + data/shader/vulkan/quad.vert | 83 + data/shader/vulkan/quadbo.vertfrag | 13 + data/shader/vulkan/spritemulti.frag | 23 + data/shader/vulkan/spritemulti.vert | 50 + data/shader/vulkan/text.frag | 43 + data/shader/vulkan/text.vert | 22 + data/shader/vulkan/tile.frag | 25 + data/shader/vulkan/tile.vert | 49 + scripts/android/cmake_android.sh | 3 +- src/engine/client/backend/backend_base.h | 2 +- .../client/backend/opengl/backend_opengl.cpp | 9 +- .../client/backend/opengl/backend_opengl.h | 3 +- .../client/backend/opengl/backend_opengl3.cpp | 1 + .../client/backend/vulkan/backend_vulkan.cpp | 7060 +++++++++++++++++ .../client/backend/vulkan/backend_vulkan.h | 12 + src/engine/client/backend_sdl.cpp | 97 +- src/engine/client/backend_sdl.h | 17 +- src/engine/client/graphics_threaded.cpp | 109 +- src/engine/client/graphics_threaded.h | 35 +- src/engine/client/graphics_threaded_null.h | 6 +- src/engine/graphics.h | 28 +- src/engine/shared/config_variables.h | 4 +- src/game/client/components/maplayers.cpp | 21 +- src/game/client/components/menus_settings.cpp | 200 +- src/game/client/components/particles.cpp | 2 +- src/game/editor/editor.cpp | 2 +- 43 files changed, 8294 insertions(+), 159 deletions(-) create mode 100644 cmake/BuildVulkanShaders.cmake create mode 100644 cmake/FindVulkan.cmake create mode 100644 data/shader/vulkan/prim.frag create mode 100644 data/shader/vulkan/prim.vert create mode 100644 data/shader/vulkan/prim3d.frag create mode 100644 data/shader/vulkan/prim3d.vert create mode 100644 data/shader/vulkan/primex.frag create mode 100644 data/shader/vulkan/primex.vert create mode 100644 data/shader/vulkan/quad.frag create mode 100644 data/shader/vulkan/quad.vert create mode 100644 data/shader/vulkan/quadbo.vertfrag create mode 100644 data/shader/vulkan/spritemulti.frag create mode 100644 data/shader/vulkan/spritemulti.vert create mode 100644 data/shader/vulkan/text.frag create mode 100644 data/shader/vulkan/text.vert create mode 100644 data/shader/vulkan/tile.frag create mode 100644 data/shader/vulkan/tile.vert create mode 100644 src/engine/client/backend/vulkan/backend_vulkan.cpp create mode 100644 src/engine/client/backend/vulkan/backend_vulkan.h diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 31648aafc..d067096c3 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -14,7 +14,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macOS-latest, windows-latest, ubuntu-18.04] + os: [ubuntu-latest, macOS-latest, windows-latest, ubuntu-20.04] include: - os: ubuntu-latest cmake-args: -G Ninja @@ -23,7 +23,7 @@ jobs: env: CFLAGS: -Wdeclaration-after-statement -Werror CXXFLAGS: -Werror - - os: ubuntu-18.04 + - os: ubuntu-20.04 cmake-path: /usr/bin/ cmake-args: -G Ninja package-file: "*-linux_x86_64.tar.xz" @@ -57,7 +57,7 @@ jobs: if: contains(matrix.os, 'ubuntu') run: | sudo apt-get update -y - sudo apt-get install pkg-config cmake ninja-build libfreetype6-dev libnotify-dev libsdl2-dev libsqlite3-dev libavcodec-dev libavformat-dev libavutil-dev libswresample-dev libswscale-dev libx264-dev -y + sudo apt-get install pkg-config cmake ninja-build libfreetype6-dev libnotify-dev libsdl2-dev libsqlite3-dev libvulkan-dev glslang-tools spirv-tools libavcodec-dev libavformat-dev libavutil-dev libswresample-dev libswscale-dev libx264-dev -y - name: Prepare Linux (fancy) if: contains(matrix.os, 'ubuntu') && matrix.fancy @@ -78,7 +78,7 @@ jobs: if: contains(matrix.os, 'macOS') run: | brew update || true - brew install pkg-config sdl2 ffmpeg python3 ninja + brew install pkg-config sdl2 ffmpeg python3 ninja molten-vk vulkan-headers glslang spirv-tools brew upgrade freetype pip3 install dmgbuild sudo rm -rf /Library/Developer/CommandLineTools diff --git a/.github/workflows/clang-tidy.yml b/.github/workflows/clang-tidy.yml index 5a202ca2a..d4f207924 100644 --- a/.github/workflows/clang-tidy.yml +++ b/.github/workflows/clang-tidy.yml @@ -20,7 +20,7 @@ jobs: - name: Install clang-tidy run: | sudo apt-get update -y - sudo apt-get install pkg-config cmake ninja-build libfreetype6-dev libnotify-dev libsdl2-dev libsqlite3-dev libavcodec-dev libavformat-dev libavutil-dev libswresample-dev libswscale-dev libx264-dev clang-tidy -y + sudo apt-get install pkg-config cmake ninja-build libfreetype6-dev libnotify-dev libsdl2-dev libsqlite3-dev libavcodec-dev libavformat-dev libavutil-dev libswresample-dev libswscale-dev libx264-dev clang-tidy libvulkan-dev glslang-tools spirv-tools -y - name: Build with clang-tidy run: | mkdir clang-tidy diff --git a/.github/workflows/codeql-analysis.yaml b/.github/workflows/codeql-analysis.yaml index 0984f0f48..e8aeda739 100644 --- a/.github/workflows/codeql-analysis.yaml +++ b/.github/workflows/codeql-analysis.yaml @@ -38,7 +38,7 @@ jobs: if: matrix.language == 'cpp' run: | sudo apt-get update -y - sudo apt-get install pkg-config cmake ninja-build libfreetype6-dev libnotify-dev libsdl2-dev libsqlite3-dev libavcodec-dev libavformat-dev libavutil-dev libswresample-dev libswscale-dev libx264-dev libmariadbclient-dev libwebsockets-dev -y + sudo apt-get install pkg-config cmake ninja-build libfreetype6-dev libnotify-dev libsdl2-dev libsqlite3-dev libavcodec-dev libavformat-dev libavutil-dev libswresample-dev libswscale-dev libx264-dev libmariadbclient-dev libwebsockets-dev libvulkan-dev glslang-tools spirv-tools -y # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/style.yml b/.github/workflows/style.yml index 33394b54a..2bdfb3166 100644 --- a/.github/workflows/style.yml +++ b/.github/workflows/style.yml @@ -19,7 +19,7 @@ jobs: - name: Prepare run: | sudo apt-get update -y - sudo apt-get install clang-format imagemagick ddnet-tools shellcheck pkg-config cmake ninja-build libfreetype6-dev libnotify-dev libsdl2-dev libsqlite3-dev libavcodec-dev libavformat-dev libavutil-dev libswresample-dev libswscale-dev libx264-dev pylint3 python3-clang -y + sudo apt-get install clang-format imagemagick ddnet-tools shellcheck pkg-config cmake ninja-build libfreetype6-dev libnotify-dev libsdl2-dev libsqlite3-dev libavcodec-dev libavformat-dev libavutil-dev libswresample-dev libswscale-dev libx264-dev pylint3 python3-clang libvulkan-dev glslang-tools spirv-tools -y mkdir release cd release cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -DDOWNLOAD_GTEST=OFF -DCMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE=. .. diff --git a/CMakeLists.txt b/CMakeLists.txt index cb3005332..cad0e5a6e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -93,9 +93,14 @@ else() endif() set(AUTO_DEPENDENCIES_DEFAULT OFF) +set(AUTO_VULKAN_BACKEND ON) if(TARGET_OS STREQUAL "windows") set(AUTO_DEPENDENCIES_DEFAULT ON) -elseif(TARGET_OS STREQUAL "linux") + if(TARGET_CPU_ARCHITECTURE STREQUAL "x86") + set(AUTO_VULKAN_BACKEND OFF) + endif() +elseif(TARGET_OS STREQUAL "mac") + set(AUTO_VULKAN_BACKEND OFF) endif() option(WEBSOCKETS "Enable websockets support" OFF) @@ -116,6 +121,7 @@ option(DISCORD "Enable Discord rich presence support" OFF) option(DISCORD_DYNAMIC "Enable discovering Discord rich presence libraries at runtime (Linux only)" OFF) option(PREFER_BUNDLED_LIBS "Prefer bundled libraries over system libraries" ${AUTO_DEPENDENCIES_DEFAULT}) option(DEV "Don't generate stuff necessary for packaging" OFF) +option(VULKAN "Enable the vulkan backend" ${AUTO_VULKAN_BACKEND}) option(EXCEPTION_HANDLING "Enable exception handling (only works with Windows as of now)" OFF) @@ -467,6 +473,15 @@ if(TARGET_OS AND TARGET_OS STREQUAL "mac") find_program(DMGBUILD dmgbuild) endif() +set(VULKAN_SHADER_FILE_LIST "" CACHE STRING "Vulkan shader file list") +if(VULKAN) + find_package(Vulkan) + include(cmake/BuildVulkanShaders.cmake) +else() + set(VULKAN_LIBRARIES) + set(VULKAN_INCLUDE_DIRS) +endif() + message(STATUS "******** ${CMAKE_PROJECT_NAME} ********") set(TARGET "Target OS: ${TARGET_OS} ${CMAKE_SYSTEM_PROCESSOR}") if(TARGET_OS STREQUAL "mac") @@ -528,6 +543,10 @@ if(WEBSOCKETS) show_dependency_status("Websockets" WEBSOCKETS) endif() +if(VULKAN) + show_dependency_status("Vulkan" VULKAN) +endif() + if(CLIENT AND NOT(CURL_FOUND)) message(SEND_ERROR "You must install Curl to compile DDNet") endif() @@ -591,6 +610,10 @@ if(NOT(GTEST_FOUND)) endif() endif() +if(VULKAN AND CLIENT AND NOT(VULKAN_FOUND)) + message(SEND_ERROR "You must install Vulkan libraries to compile the DDNet client") +endif() + if(TARGET_OS STREQUAL "windows") set(PLATFORM_CLIENT) set(PLATFORM_CLIENT_LIBS opengl32 winmm) @@ -1277,6 +1300,20 @@ set(EXPECTED_DATA shader/text.vert shader/tile.frag shader/tile.vert + shader/vulkan/prim.frag + shader/vulkan/prim.vert + shader/vulkan/prim3d.frag + shader/vulkan/prim3d.vert + shader/vulkan/primex.frag + shader/vulkan/primex.vert + shader/vulkan/quad.frag + shader/vulkan/quad.vert + shader/vulkan/spritemulti.frag + shader/vulkan/spritemulti.vert + shader/vulkan/text.frag + shader/vulkan/text.vert + shader/vulkan/tile.frag + shader/vulkan/tile.vert skins/Aoe4leg.png skins/PaladiN.png skins/antiantey.png @@ -1422,6 +1459,7 @@ set(COPY_FILES ${FFMPEG_COPY_FILES} ${WEBSOCKETS_COPY_FILES} ${DISCORDSDK_COPY_FILES} + ${VULKAN_COPY_FILES} ${EXCEPTION_HANDLING_COPY_FILES} ) file(COPY ${COPY_FILES} DESTINATION .) @@ -1775,6 +1813,8 @@ if(CLIENT) backend/opengles/gles_class_defines.h backend/opengles/opengles_sl.cpp backend/opengles/opengles_sl_program.cpp + backend/vulkan/backend_vulkan.cpp + backend/vulkan/backend_vulkan.h backend_sdl.cpp backend_sdl.h blocklist_driver.cpp @@ -1962,6 +2002,8 @@ if(CLIENT) ${OPUS_LIBRARIES} ${OGG_LIBRARIES} + ${VULKAN_LIBRARIES} + ${TARGET_STEAMAPI} ${PLATFORM_CLIENT_LIBS} @@ -2033,6 +2075,8 @@ if(CLIENT) ${FFMPEG_INCLUDE_DIRS} ${DISCORDSDK_INCLUDE_DIRS} + ${VULKAN_INCLUDE_DIRS} + ${PLATFORM_CLIENT_INCLUDE_DIRS} ) @@ -2065,6 +2109,10 @@ if(CLIENT) target_compile_definitions(${TARGET_CLIENT} PRIVATE CONF_GLEW_HAS_CONTEXT_INIT) endif() + if(VULKAN) + target_compile_definitions(${TARGET_CLIENT} PRIVATE CONF_BACKEND_VULKAN) + endif() + list(APPEND TARGETS_OWN ${TARGET_CLIENT}) list(APPEND TARGETS_LINK ${TARGET_CLIENT}) endif() @@ -2578,6 +2626,11 @@ set(CPACK_FILES storage.cfg ${COPY_FILES} ) + +set(CPACK_GEN_FILES + ${VULKAN_SHADER_FILE_LIST} +) + if(TARGET_OS STREQUAL "windows") list(APPEND CPACK_FILES other/config_directory.bat) endif() @@ -2600,6 +2653,9 @@ if(NOT DEV) install(FILES other/icons/DDNet_${SIZE}x${SIZE}x32.png DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/${SIZE}x${SIZE}/apps RENAME ddnet.png COMPONENT client) install(FILES other/icons/DDNet-Server_${SIZE}x${SIZE}x32.png DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/${SIZE}x${SIZE}/apps RENAME ddnet-server.png COMPONENT server) endforeach() + foreach(file ${VULKAN_SHADER_FILE_LIST}) + install(FILES ${PROJECT_BINARY_DIR}/${file} DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/ddnet/data/shader/vulkan COMPONENT client) + endforeach() endif() if(DEV) @@ -2610,7 +2666,8 @@ else() set(EXTRA_ARGS DESTINATION ${CPACK_PACKAGE_FILE_NAME} COMPONENT portable EXCLUDE_FROM_ALL) install(TARGETS ${CPACK_TARGETS} ${EXTRA_ARGS}) install(DIRECTORY ${CPACK_DIRS} ${EXTRA_ARGS}) - install(FILES ${CPACK_FILES} ${EXTRA_ARGS}) + set(CPACK_FILES_TMP ${CPACK_FILES} ${CPACK_GEN_FILES}) + install(FILES ${CPACK_FILES_TMP} ${EXTRA_ARGS}) endif() set(PACKAGE_TARGETS) @@ -2714,6 +2771,9 @@ foreach(ext zip tar.gz tar.xz) foreach(file ${CPACK_FILES}) list(APPEND COPY_FILE_COMMANDS COMMAND ${CMAKE_COMMAND} -E copy ${PROJECT_SOURCE_DIR}/${file} ${TMPDIR}/) endforeach() + foreach(file ${CPACK_GEN_FILES}) + list(APPEND COPY_FILE_COMMANDS COMMAND ${CMAKE_COMMAND} -E copy ${PROJECT_BINARY_DIR}/${file} ${TMPDIR}/${file}) + endforeach() foreach(dir ${CPACK_DIRS}) list(APPEND COPY_DIR_COMMANDS COMMAND ${CMAKE_COMMAND} -E copy_directory ${PROJECT_SOURCE_DIR}/${dir} ${TMPDIR}/${dir}) endforeach() @@ -2761,6 +2821,7 @@ unset(CPACK_SOURCE_FILES_INVERTED) unset(CPACK_TARGETS) unset(CPACK_DIRS) unset(CPACK_FILES) +unset(CPACK_GEN_FILES) include(CPack) diff --git a/README.md b/README.md index 9a03b4c8c..fecf58ab0 100644 --- a/README.md +++ b/README.md @@ -34,21 +34,21 @@ Dependencies on Linux / macOS You can install the required libraries on your system, `touch CMakeLists.txt` and CMake will use the system-wide libraries by default. You can install all required dependencies and CMake on Debian or Ubuntu like this: - sudo apt install build-essential cmake git google-mock libcurl4-openssl-dev libssl-dev libfreetype6-dev libglew-dev libnotify-dev libogg-dev libopus-dev libopusfile-dev libpnglite-dev libsdl2-dev libsqlite3-dev libwavpack-dev python libx264-dev libavfilter-dev libavdevice-dev libavformat-dev libavcodec-extra libavutil-dev + sudo apt install build-essential cmake git google-mock libcurl4-openssl-dev libssl-dev libfreetype6-dev libglew-dev libnotify-dev libogg-dev libopus-dev libopusfile-dev libpnglite-dev libsdl2-dev libsqlite3-dev libwavpack-dev python libx264-dev libavfilter-dev libavdevice-dev libavformat-dev libavcodec-extra libavutil-dev libvulkan-dev glslang-tools spirv-tools Or on CentOS, RedHat and AlmaLinux like this: - sudo yum install gcc gcc-c++ make cmake git python2 gtest-devel gmock-devel libcurl-devel openssl-devel freetype-devel glew-devel libnotify-devel libogg-devel opus-devel opusfile-devel pnglite-devel SDL2-devel sqlite-devel wavpack-devel libx264-devel ffmpeg-devel + sudo yum install gcc gcc-c++ make cmake git python2 gtest-devel gmock-devel libcurl-devel openssl-devel freetype-devel glew-devel libnotify-devel libogg-devel opus-devel opusfile-devel pnglite-devel SDL2-devel sqlite-devel wavpack-devel libx264-devel ffmpeg-devel vulkan-devel glslang spirv-tools Or on Arch Linux like this: - sudo pacman -S --needed base-devel cmake curl freetype2 git glew gmock libnotify opusfile python sdl2 sqlite wavpack x264 ffmpeg + sudo pacman -S --needed base-devel cmake curl freetype2 git glew gmock libnotify opusfile python sdl2 sqlite wavpack x264 ffmpeg vulkan-icd-loader vulkan-headers glslang spirv-tools There is an [AUR package for pnglite](https://aur.archlinux.org/packages/pnglite/). For instructions on installing it, see [AUR packages installation instructions on ArchWiki](https://wiki.archlinux.org/index.php/Arch_User_Repository#Installing_packages). On macOS you can use [homebrew](https://brew.sh/) to install build dependencies like this: - brew install cmake freetype glew googletest opusfile SDL2 wavpack x264 ffmpeg + brew install cmake freetype glew googletest opusfile SDL2 wavpack x264 ffmpeg molten-vk vulkan-headers glslang spirv-tools If you don't want to use the system libraries, you can pass the `-DPREFER_BUNDLED_LIBS=ON` parameter to cmake. @@ -109,6 +109,10 @@ Whether to optimize for development, speeding up the compilation process a littl Whether to enable UPnP support for the server. You need to install `libminiupnpc-dev` on Debian, `miniupnpc` on Arch Linux. +* **-DVULKAN=[ON|OFF]**
+Whether to enable the vulkan backend. +On Windows you need to install the Vulkan SDK and set the `VULKAN_SDK` environment flag accordingly. + * **-GNinja**
Use the Ninja build system instead of Make. This automatically parallizes the build and is generally faster. Compile with `ninja` instead of `make`. Install Ninja with `sudo apt install ninja-build` on Debian, `sudo pacman -S --needed ninja` on Arch Linux. diff --git a/cmake/BuildVulkanShaders.cmake b/cmake/BuildVulkanShaders.cmake new file mode 100644 index 000000000..f2bcdbe1c --- /dev/null +++ b/cmake/BuildVulkanShaders.cmake @@ -0,0 +1,175 @@ +find_program(GLSLANG_VALIDATOR_PROGRAM glslangValidator) +find_program(SPIRV_OPTIMIZER_PROGRAM spirv-opt) + +set(GLSLANG_VALIDATOR_PROGRAM_FOUND TRUE) +if(NOT GLSLANG_VALIDATOR_PROGRAM) + set(GLSLANG_VALIDATOR_PROGRAM_FOUND FALSE) + if(TARGET_OS STREQUAL "windows") + if(${TARGET_CPU_ARCHITECTURE} STREQUAL "x86_64") + set(GLSLANG_VALIDATOR_PROGRAM "$ENV{VULKAN_SDK}/Bin/glslangValidator.exe") + else() + set(GLSLANG_VALIDATOR_PROGRAM "$ENV{VULKAN_SDK}/Bin32/glslangValidator.exe") + endif() + endif() + + if(EXISTS ${GLSLANG_VALIDATOR_PROGRAM}) + set(GLSLANG_VALIDATOR_PROGRAM_FOUND TRUE) + elseif(${TARGET_CPU_ARCHITECTURE} STREQUAL "x86_64") + set(GLSLANG_VALIDATOR_PROGRAM "${PROJECT_SOURCE_DIR}/ddnet-libs/vulkan/windows/lib64/glslangValidator.exe") + if(EXISTS ${GLSLANG_VALIDATOR_PROGRAM}) + set(GLSLANG_VALIDATOR_PROGRAM_FOUND TRUE) + endif() + endif() + + if(${GLSLANG_VALIDATOR_PROGRAM_FOUND} EQUAL FALSE) + message(FATAL_ERROR "glslangValidator binary was not found. Did you install the Vulkan SDK / packages ?") + endif() +endif() + +set(SPIRV_OPTIMIZER_PROGRAM_FOUND TRUE) +if(NOT SPIRV_OPTIMIZER_PROGRAM) + set(SPIRV_OPTIMIZER_PROGRAM_FOUND FALSE) + if(TARGET_OS STREQUAL "windows") + if (${TARGET_CPU_ARCHITECTURE} STREQUAL "x86_64") + set(SPIRV_OPTIMIZER_PROGRAM "$ENV{VULKAN_SDK}/Bin/spirv-opt.exe") + else() + set(SPIRV_OPTIMIZER_PROGRAM "$ENV{VULKAN_SDK}/Bin32/spirv-opt.exe") + endif() + endif() + + if(EXISTS ${SPIRV_OPTIMIZER_PROGRAM}) + set(SPIRV_OPTIMIZER_PROGRAM_FOUND TRUE) + endif() +endif() + +file(GLOB_RECURSE GLSL_SHADER_FILES + "data/shaders/vulkan/*.frag" + "data/shaders/vulkan/*.vert" +) + +set(TMP_SHADER_SHA256_LIST "") +foreach(GLSL_SHADER_FILE ${GLSL_SHADER_FILES}) + file(SHA256 ${FILE_NAME} TMP_FILE_SHA) + set(TMP_SHADER_SHA256_LIST "${TMP_SHADER_SHA256_LIST}${TMP_FILE_SHA}") +endforeach(GLSL_SHADER_FILE) + +string(SHA256 GLSL_SHADER_SHA256 "${TMP_SHADER_SHA256_LIST}") +set(GLSL_SHADER_SHA256 "${GLSL_SHADER_SHA256}@v1") + +set(FOUND_MATCHING_SHA256_FILE FALSE) + +if(EXISTS "${PROJECT_BINARY_DIR}/vulkan_shaders_sha256.txt") + file(STRINGS "${PROJECT_BINARY_DIR}/vulkan_shaders_sha256.txt" VULKAN_SHADERS_SHA256_FILE_CONTENT) + if("${VULKAN_SHADERS_SHA256_FILE_CONTENT}" STREQUAL "${GLSL_SHADER_SHA256}") + set(FOUND_MATCHING_SHA256_FILE TRUE) + endif() +endif() + +set(TW_VULKAN_VERSION "vulkan100") + +set(GLSLANG_VALIDATOR_COMMAND_LIST) +set(GLSLANG_VALIDATOR_DELETE_LIST) +set(SPIRV_OPTIMIZER_COMMAND_LIST) +function(generate_shader_file FILE_ARGS1 FILE_ARGS2 FILE_NAME FILE_OUTPUT_NAME) + set(FILE_TMP_NAME_POSTFIX "") + if(SPIRV_OPTIMIZER_PROGRAM_FOUND) + set(FILE_TMP_NAME_POSTFIX ".tmp") + endif() + list(APPEND GLSLANG_VALIDATOR_COMMAND_LIST COMMAND ${GLSLANG_VALIDATOR_PROGRAM} --client ${TW_VULKAN_VERSION} ${FILE_ARGS1} ${FILE_ARGS2} ${FILE_NAME} -o "${PROJECT_BINARY_DIR}/${FILE_OUTPUT_NAME}${FILE_TMP_NAME_POSTFIX}") + if(SPIRV_OPTIMIZER_PROGRAM_FOUND) + list(APPEND SPIRV_OPTIMIZER_COMMAND_LIST COMMAND ${SPIRV_OPTIMIZER_PROGRAM} -O "${PROJECT_BINARY_DIR}/${FILE_OUTPUT_NAME}${FILE_TMP_NAME_POSTFIX}" -o "${PROJECT_BINARY_DIR}/${FILE_OUTPUT_NAME}") + list(APPEND GLSLANG_VALIDATOR_DELETE_LIST "${PROJECT_BINARY_DIR}/${FILE_OUTPUT_NAME}${FILE_TMP_NAME_POSTFIX}") + endif() + file(RELATIVE_PATH TMP_SHADER_FILE_REL "${PROJECT_SOURCE_DIR}" "${PROJECT_BINARY_DIR}/${FILE_OUTPUT_NAME}") + list(APPEND VULKAN_SHADER_FILE_LIST "${FILE_OUTPUT_NAME}") + set(VULKAN_SHADER_FILE_LIST ${VULKAN_SHADER_FILE_LIST} PARENT_SCOPE) + set(GLSLANG_VALIDATOR_DELETE_LIST ${GLSLANG_VALIDATOR_DELETE_LIST} PARENT_SCOPE) + set(SPIRV_OPTIMIZER_COMMAND_LIST ${SPIRV_OPTIMIZER_COMMAND_LIST} PARENT_SCOPE) + set(GLSLANG_VALIDATOR_COMMAND_LIST ${GLSLANG_VALIDATOR_COMMAND_LIST} PARENT_SCOPE) +endfunction() + +if(NOT FOUND_MATCHING_SHA256_FILE) + message(STATUS "Building vulkan shaders") + execute_process(COMMAND ${CMAKE_COMMAND} -E make_directory "${PROJECT_BINARY_DIR}/data/shader/vulkan/") + + unset(VULKAN_SHADER_FILE_LIST CACHE) + + # primitives + generate_shader_file("" "" "${PROJECT_SOURCE_DIR}/data/shader/vulkan/prim.frag" "data/shader/vulkan/prim.frag.spv") + generate_shader_file("-DTW_TEXTURED" "" "${PROJECT_SOURCE_DIR}/data/shader/vulkan/prim.frag" "data/shader/vulkan/prim_textured.frag.spv") + + generate_shader_file("" "" "${PROJECT_SOURCE_DIR}/data/shader/vulkan/prim.vert" "data/shader/vulkan/prim.vert.spv") + generate_shader_file("-DTW_TEXTURED" "" "${PROJECT_SOURCE_DIR}/data/shader/vulkan/prim.vert" "data/shader/vulkan/prim_textured.vert.spv") + + generate_shader_file("" "" "${PROJECT_SOURCE_DIR}/data/shader/vulkan/prim3d.frag" "data/shader/vulkan/prim3d.frag.spv") + generate_shader_file("-DTW_TEXTURED" "" "${PROJECT_SOURCE_DIR}/data/shader/vulkan/prim3d.frag" "data/shader/vulkan/prim3d_textured.frag.spv") + + generate_shader_file("" "" "${PROJECT_SOURCE_DIR}/data/shader/vulkan/prim3d.vert" "data/shader/vulkan/prim3d.vert.spv") + generate_shader_file("-DTW_TEXTURED" "" "${PROJECT_SOURCE_DIR}/data/shader/vulkan/prim3d.vert" "data/shader/vulkan/prim3d_textured.vert.spv") + + # text + generate_shader_file("" "" "${PROJECT_SOURCE_DIR}/data/shader/vulkan/text.frag" "data/shader/vulkan/text.frag.spv") + generate_shader_file("" "" "${PROJECT_SOURCE_DIR}/data/shader/vulkan/text.vert" "data/shader/vulkan/text.vert.spv") + + # quad container + generate_shader_file("" "" "${PROJECT_SOURCE_DIR}/data/shader/vulkan/primex.frag" "data/shader/vulkan/primex.frag.spv") + generate_shader_file("" "" "${PROJECT_SOURCE_DIR}/data/shader/vulkan/primex.vert" "data/shader/vulkan/primex.vert.spv") + + generate_shader_file("" "" "${PROJECT_SOURCE_DIR}/data/shader/vulkan/primex.frag" "data/shader/vulkan/primex_rotationless.frag.spv") + generate_shader_file("-DTW_ROTATIONLESS" "" "${PROJECT_SOURCE_DIR}/data/shader/vulkan/primex.vert" "data/shader/vulkan/primex_rotationless.vert.spv") + + generate_shader_file("-DTW_TEXTURED" "" "${PROJECT_SOURCE_DIR}/data/shader/vulkan/primex.frag" "data/shader/vulkan/primex_tex.frag.spv") + generate_shader_file("" "" "${PROJECT_SOURCE_DIR}/data/shader/vulkan/primex.vert" "data/shader/vulkan/primex_tex.vert.spv") + + generate_shader_file("-DTW_TEXTURED" "" "${PROJECT_SOURCE_DIR}/data/shader/vulkan/primex.frag" "data/shader/vulkan/primex_tex_rotationless.frag.spv") + generate_shader_file("-DTW_ROTATIONLESS" "" "${PROJECT_SOURCE_DIR}/data/shader/vulkan/primex.vert" "data/shader/vulkan/primex_tex_rotationless.vert.spv") + + generate_shader_file("" "" "${PROJECT_SOURCE_DIR}/data/shader/vulkan/spritemulti.frag" "data/shader/vulkan/spritemulti.frag.spv") + generate_shader_file("" "" "${PROJECT_SOURCE_DIR}/data/shader/vulkan/spritemulti.vert" "data/shader/vulkan/spritemulti.vert.spv") + + generate_shader_file("-DTW_PUSH_CONST" "" "${PROJECT_SOURCE_DIR}/data/shader/vulkan/spritemulti.frag" "data/shader/vulkan/spritemulti_push.frag.spv") + generate_shader_file("-DTW_PUSH_CONST" "" "${PROJECT_SOURCE_DIR}/data/shader/vulkan/spritemulti.vert" "data/shader/vulkan/spritemulti_push.vert.spv") + + # tile layer + generate_shader_file("" "" "${PROJECT_SOURCE_DIR}/data/shader/vulkan/tile.frag" "data/shader/vulkan/tile.frag.spv") + generate_shader_file("" "" "${PROJECT_SOURCE_DIR}/data/shader/vulkan/tile.vert" "data/shader/vulkan/tile.vert.spv") + + generate_shader_file("-DTW_TILE_TEXTURED" "" "${PROJECT_SOURCE_DIR}/data/shader/vulkan/tile.frag" "data/shader/vulkan/tile_textured.frag.spv") + generate_shader_file("-DTW_TILE_TEXTURED" "" "${PROJECT_SOURCE_DIR}/data/shader/vulkan/tile.vert" "data/shader/vulkan/tile_textured.vert.spv") + + generate_shader_file("-DTW_TILE_BORDER" "" "${PROJECT_SOURCE_DIR}/data/shader/vulkan/tile.frag" "data/shader/vulkan/tile_border.frag.spv") + generate_shader_file("-DTW_TILE_BORDER" "" "${PROJECT_SOURCE_DIR}/data/shader/vulkan/tile.vert" "data/shader/vulkan/tile_border.vert.spv") + + generate_shader_file("-DTW_TILE_BORDER" "-DTW_TILE_TEXTURED" "${PROJECT_SOURCE_DIR}/data/shader/vulkan/tile.frag" "data/shader/vulkan/tile_border_textured.frag.spv") + generate_shader_file("-DTW_TILE_BORDER" "-DTW_TILE_TEXTURED" "${PROJECT_SOURCE_DIR}/data/shader/vulkan/tile.vert" "data/shader/vulkan/tile_border_textured.vert.spv") + + generate_shader_file("-DTW_TILE_BORDER_LINE" "" "${PROJECT_SOURCE_DIR}/data/shader/vulkan/tile.frag" "data/shader/vulkan/tile_border_line.frag.spv") + generate_shader_file("-DTW_TILE_BORDER_LINE" "" "${PROJECT_SOURCE_DIR}/data/shader/vulkan/tile.vert" "data/shader/vulkan/tile_border_line.vert.spv") + + generate_shader_file("-DTW_TILE_BORDER_LINE" "-DTW_TILE_TEXTURED" "${PROJECT_SOURCE_DIR}/data/shader/vulkan/tile.frag" "data/shader/vulkan/tile_border_line_textured.frag.spv") + generate_shader_file("-DTW_TILE_BORDER_LINE" "-DTW_TILE_TEXTURED" "${PROJECT_SOURCE_DIR}/data/shader/vulkan/tile.vert" "data/shader/vulkan/tile_border_line_textured.vert.spv") + + # quad layer + generate_shader_file("" "" "${PROJECT_SOURCE_DIR}/data/shader/vulkan/quad.frag" "data/shader/vulkan/quad.frag.spv") + generate_shader_file("" "" "${PROJECT_SOURCE_DIR}/data/shader/vulkan/quad.vert" "data/shader/vulkan/quad.vert.spv") + + generate_shader_file("-DTW_PUSH_CONST" "" "${PROJECT_SOURCE_DIR}/data/shader/vulkan/quad.frag" "data/shader/vulkan/quad_push.frag.spv") + generate_shader_file("-DTW_PUSH_CONST" "" "${PROJECT_SOURCE_DIR}/data/shader/vulkan/quad.vert" "data/shader/vulkan/quad_push.vert.spv") + + generate_shader_file("-DTW_QUAD_TEXTURED" "" "${PROJECT_SOURCE_DIR}/data/shader/vulkan/quad.frag" "data/shader/vulkan/quad_textured.frag.spv") + generate_shader_file("-DTW_QUAD_TEXTURED" "" "${PROJECT_SOURCE_DIR}/data/shader/vulkan/quad.vert" "data/shader/vulkan/quad_textured.vert.spv") + + generate_shader_file("-DTW_QUAD_TEXTURED" "-DTW_PUSH_CONST" "${PROJECT_SOURCE_DIR}/data/shader/vulkan/quad.frag" "data/shader/vulkan/quad_push_textured.frag.spv") + generate_shader_file("-DTW_QUAD_TEXTURED" "-DTW_PUSH_CONST" "${PROJECT_SOURCE_DIR}/data/shader/vulkan/quad.vert" "data/shader/vulkan/quad_push_textured.vert.spv") + + execute_process(${GLSLANG_VALIDATOR_COMMAND_LIST}) + if(SPIRV_OPTIMIZER_PROGRAM_FOUND) + execute_process(${SPIRV_OPTIMIZER_COMMAND_LIST}) + file(REMOVE ${GLSLANG_VALIDATOR_DELETE_LIST}) + endif() + + set(VULKAN_SHADER_FILE_LIST ${VULKAN_SHADER_FILE_LIST} CACHE STRING "Vulkan shader file list" FORCE) + + message(STATUS "Finished building vulkan shaders") + file(WRITE "${PROJECT_BINARY_DIR}/vulkan_shaders_sha256.txt" "${GLSL_SHADER_SHA256}") +endif() diff --git a/cmake/FindVulkan.cmake b/cmake/FindVulkan.cmake new file mode 100644 index 000000000..4935fefb6 --- /dev/null +++ b/cmake/FindVulkan.cmake @@ -0,0 +1,61 @@ +if(NOT CMAKE_CROSSCOMPILING) + find_package(PkgConfig QUIET) + pkg_check_modules(PC_VULKAN vulkan) + if(PC_VULKAN_FOUND) + set(VULKAN_INCLUDE_DIRS "${PC_VULKAN_INCLUDE_DIRS}") + set(VULKAN_LIBRARIES "${PC_VULKAN_LIBRARIES}") + set(VULKAN_FOUND TRUE) + endif() +endif() + +if(NOT VULKAN_FOUND) + if(TARGET_OS STREQUAL "android") + find_library(VULKAN_LIBRARIES + NAMES vulkan + ) + + find_path( + VULKAN_INCLUDE_DIRS + NAMES vulkan/vulkan.h + ) + elseif(TARGET_OS STREQUAL "mac") + find_library(VULKAN_LIBRARIES + NAMES MoltenVK + ) + + find_path( + VULKAN_INCLUDE_DIRS + NAMES vulkan/vulkan.h + ) + else() + set_extra_dirs_lib(VULKAN vulkan) + find_library(VULKAN_LIBRARIES + NAMES vulkan vulkan-1 + HINTS ${HINTS_VULKAN_LIBDIR} ${PC_VULKAN_LIBDIR} ${PC_VULKAN_LIBRARY_DIRS} + PATHS ${PATHS_VULKAN_LIBDIR} + ${CROSSCOMPILING_NO_CMAKE_SYSTEM_PATH} + ) + + set_extra_dirs_include(VULKAN vulkan "${VULKAN_LIBRARIES}") + find_path( + VULKAN_INCLUDE_DIRS + NAMES vulkan/vulkan.h + HINTS ${HINTS_VULKAN_INCLUDEDIR} ${PC_VULKAN_INCLUDEDIR} ${PC_VULKAN_INCLUDE_DIRS} + PATHS ${PATHS_VULKAN_INCLUDEDIR} + ${CROSSCOMPILING_NO_CMAKE_SYSTEM_PATH} + ) + endif() + + if(VULKAN_INCLUDE_DIRS AND VULKAN_LIBRARIES) + set(VULKAN_FOUND TRUE) + else(VULKAN_INCLUDE_DIRS AND VULKAN_LIBRARIES) + set(VULKAN_FOUND FALSE) + endif(VULKAN_INCLUDE_DIRS AND VULKAN_LIBRARIES) +endif() + +if(TARGET_OS STREQUAL "windows") + is_bundled(VULKAN_BUNDLED "${VULKAN_LIBRARIES}") + if(VULKAN_BUNDLED) + set(VULKAN_COPY_FILES "${EXTRA_VULKAN_LIBDIR}/vulkan-1.dll") + endif() +endif() diff --git a/data/editor/background.png b/data/editor/background.png index a437bd26ad8573178e43aaafbaabd25957b60e73..7d83990e6ef21aeaee8f34acc1aeb73402e034f0 100644 GIT binary patch literal 56183 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

tZ}FFbDtc$yuekv?eA>}unyC8Q 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 mG6-E~bq9{Z)qJk5&Ho2bsK0tE literal 53387 zcmV((K;XZLP)G%8fxn8gL`@IN^6c`M^!{HFKPvqTh zx7loR;P3Z4JyImXk47V4@yFwV@Or(TPN(q8<%cm^H;jFo>w){QZ8IC5m@C9Xyn~(z_~3E6SmtaRADR2Q7yopDc&L45vr*I$(Ua5fHUtX*oM74hBGg zJ;_*{;eq!ks&mT9rEz{RiKr5OY?Xe$pWPMBfMR@CbZ4?zo%9b}2A9(nL+TW<)1;W7 zXU;Zch!VA`;qas%%PkJ0oK-M-wcNULW`XPnLIA=tG9xy3U8&^^v!ID*(X&i+N^G@S zl+fH4@O2-M7 zEBt&B4C?xYh!=*4%=COlFe=q^ngb*aSp_h@bdIc4Owbu}oD2q=PsaKGW5^4dq8H7K{V5$_g;kBWIRpO~fK1mkC;LL2275jOJt$9bkas5k&gvYcr3l2vB3 z)$8?OX#3;APEZtzfglX$tIZD|Q~-HClU}R4Itr9_0OHI3oYHU=22)kY zDbpFmi0n?Z40Td0^gP>^ay0{^%+gH>QHvdsNOp8Q_rfBUXeTW8lQgHaj6$2FuIB-e zH?)gRD*Eh{DBvZS?npZ`Bg2vjWZtK4S=^$xZJok7i7l_$jPd5UvM*5CfXyUTh)QG| zdbwtd346cD^eL%&xge01ZRyqf#a0&9bacuY!$;6uiS`~pkH&kSIvOPfu`9L?IzGS= z*x4RxWUF>HBOpYD+T^HAzGr`5ZP1}gw62VednV9Br$R+a+m=^6YsKCJgkjRvPkdLyu@J}FBI6jrsFB4<4((Rqo%eRx)U0TdT!Uv~Wd{)UVe zLe$+Y$sULk4w|dc{8!U-C8SS<(Q%3v5$*wW%NPa<3Y~itp)Z}IrCQSKp@UREpU<>M z6vec!(M6^*2Igb}!*raGGQrf|ui76VBVr&>}OW{N=5BMB|;3^pSW z@>@cu?3L*E`)#uXX-QD4VAMv6Mp@yLgyPVzA`d)V+Z@FPU~8#EUbNwKOigR0ZRJw_ zWD3(e9JU}}twI;K^d54|Fqx$dNCP5PDw-K|0z?k@N!+=SsEzEVWQD6H{hAQpD>MDYj<23rBM-@F72K`l$NM<{w@ zu*st)nTGE^lcpD_Ol8>u1Ea4wf+YZ55}C)gtqxu;7ob$G#wTdz0;b*U6hzzK9!O{d zu{^R3HpIUpZN^b1ScP}8$prvhU?t~8IT0=>x*&pUaaekUTVfC^@U@hUHtgbvm*v3S zbaXjF&&obgaIBK{tcqJ&>))!PrN1gC#!%-TEOQ~agUQ!afC*%WunIoOW?2jn6%-ug zu=_v{(78Jt3LRG%F)_O$X&GrmLxio=)i1=#T&M5CH&5r6b`B!OgE1;PMS#5CrO2La zAlA?fQavzOh0Jn2xFbT>X8lWwUDb%H7JDh6HPk|OaZa58vJKU1 ze0`4gTVkGTz3Hbk28^O=^z{k%<3O=(nKm^t_mkryl{GrCM6Q6_zq0rT>WbA4$xHgQh1%V5SmMWP<2N z#R4+u!##UC1=cx|!M&8YAwwx{w1e#A809<5cz5)cl(mgIe-cCm4T`%>>8#2l+qg56 zlm`;fBGc|FAA;ZScMQCwGq?@h$8zpn>@i316dGIfDpAF}Ho?eFX$Ruaw$t}0FSoOx z^X%)e;iu(%OOA1zi!9O<*;Td}0k@e?>4UPVMn=7a&Wz4PFSLf!)0tC~dnH*fy%l4o+N!5=bMycybXe1Z<8rD^*KdCTb zp7V~@JcBv|aFRHAn&YXjF*iwA|)@+0@%6R|<#4V*grp?4L_JL2V_7fiOIW_QLY^{Yv)fUcm=H zO$>xd(wT{Idg05y?9PbC_O=^#4;!EZX<7{Bpwkzf{J^6*rtdd}Qq}U_fC6a0Sni<#!3n*Z;3dCgdF7>TG&9ys{ zyl}Qo$l`C$+ah(37N=D3DbrpmtDDm4Y~VH)%LK=ptLPpdy{x(Td*6**#C; zdr;dab=W{ecGChI;41Hs!S1uHI#2DteNmma56#9M*zxsx?IvUZvfq06+cka_qR_l$ z9E}(HSE^6S_N%H!UIGi;h8lkT`Za6g1sQTHWy>I#z5*jS#>5^`1tQGO*%s+Vf0Ms+ zj&$Tini3Ev`xJ*jZ2O&BtoH!}q`Aon9uzIr?gPUX>vRp8wVSM`6jFnNM8b(ur&H8~ z#w@lL_aR3aQmFtB*5pPl#|8gxd(Hb)+7#H9Qo7T2+mf`qrDW3c`P>FfF01R4Ur4Smp6ZL+*mV9au-;#XQ0&20Nv zie>}WrrpdthXx~2=>@&XfkK5gkSeR89fOF10QsSjXVd^ynp*y1M<7)t)_g!7*h7Ux zsHGwrx;52G$I9@cj(NTqp2#H3!cMZve`RmDL^l@fkNZ_5bEbBMtOeQJC8=tbAPDM3 zpUqncI9t@wnV&y@-ugW~(ZSUcmnYUVclyK+^utLx^CEfoS?mt|JGo(fpZonjzTYtq zNn0%yfI|Ax&k(G9+)v5}S0&W^gmNWRl!H5gj3Z{uj>?>vmlbpS%8cCCal`C{QJb$V zBRrakI6oh?shID%f?Pz=1C`gNl5@fY!@qz3NMu-dBtFv4qLhk|nnfm#GZKFheTKZ#3QhK=vM}iqY!Db?m_H4 z`$)*>{eJHRc>qgth}e@=>gR??XrW?UQSj%(7{M@ZKPAF}1v)HLw3DXUI z8&<*`vYRvY^mc*HW*g?-SH6AwHr60ZQ1w2}!g}R0?oenkkMnI`H?}!|M=M0()}~6H zH3O+KDP8NF@t`HD$&-sO;A0A>WZ~piqInAqyq4PZYVeO;# zn8S}VV47y=^|*?;D2IIF3cn&Y=0OQj)O`f7$&S}080H|!Fvr&D*BRNJt&x0=GH};r zHjSQ!-V?VwuXv^Yu?CW^*Ga3Cx;b{ntBdG(sNgzvCyua3ch)e(l9r)-?>_GDe2D2A z`LSQwHj@mj#)FKEBltnAme5RfVZD+}7|_Nu62`7-nwc)(9Fm4Z@wDS#e&Fazb;qOp z*M!=BmKIE`whgdWjME?pfQy_6zhxkq5aO%!xYsyG9KzAc5+QRum;<&p{8tDtH>R@D(=2>yEiM zaQu3Hf;&y@S=`kNyh3Q3RjmpV>B6H4`etn+$L{JBrL}fCyECR+DX;m{E}&CLJ1Mpe zSImqQ_Z zl}7lmzKSJGq+d;HV3b(eQ)VG0LqSQ(bhBmPf+ll94I;%~V#Wp?n5`veow~+AN-1H3 zm|?WmmX*ax2>~ezNVa*+fz(9*#19H%tx#kkYfG>sD^2Sx=ubj%A9byqoQM~1w64kl zSh>iMdvz3{f*dsrcJtQMymCMc`_IIEpgIx*K^UG(zN2i;d#TMir#o%I2R|}k9L5@{ z)uFn($z)6xW?Z*if&y zsZvYz83sMzx1fk!{O;Ym!I)hzqbyTVG1%u)UP5Q7(QkYx0c90+?^$eY>ODg6agg|x zD6)dA(xD!tV6ynehFu@sh&$JGGZRo!`IypsZ4Y>8m_ay-iB3O=5tcan=tN!HOA*i< zcql+((^a zIdY#!4I#YBUeznORW40t1Eif)ZGobaqRHnk6<8IQDgwsONhczD5Nlr*Fsbi(Q!8b$ zoN*9{#IWSe4Q3^OGt}d>PZh{btnzW|64od&*AOAaGc!}4u#acjedf&+vFSr~HJ;`8F0Ayd+l< zIxbT&4-Yq{PY#_7j-z}OyU**1BT6tj;f!@v)V+BB{{4Qn7eY@3)4*bd6b=RjjY3c( zpo_l|qt4iUGOOA}j=9A5%usE1lo$wvceL?5y`^#aUMJH)EmvyK;;shPi7R_hD^nQY zRTpNdEO1B8^(L{7G}&&7RJpxhj45Io=~84{>QTuI3V zkX7sk?k_eT-3`|X$erRafglsVL#o9tW{k?k&ShnDO#2p0P(zERk(ZG#i8=G6LbMS~ zU`pkh_%+dHui$Uh3=5;>>=-_gAq7GzN04{`eOTh#w{O3G{rd9d%e4RP+qawUlP6DR zLbhR3-0PYHG>li%BeNN{!L7>zE4)aTIgi?E#&IPdA9x5;%0@`g2)d{EiiULYC3c(LwfVm`dwhKx8-(-M``dF=O8her`gLTTj4ak4^tgQVVp zDS3&X=%mW(>|at)&O^xIJj|_J)*W-?rtP(%$hZDLj=zSY1jq&OF+3x5aQ}-!$Vh}h z5rKHjS-&coCNUNj528fVtmJ%d#My_hI;3kaIwjaqSM$$EI`w;MZMh>oU>ij(7$86u zUBdj!cEA|uylu7Pj0!{{6K}D^!?eO3W!=nWt}89Xs?gqvBg~jxX_w2~6!HP%(j+-| zZ6yj>QA9qF)sda2I^u(rDu(QR#xhmH~rq6L1nnPvBUSUHM% zf*s9}Tx@)xC~B9x+cOeDcytP54v%C9pRxjh*a;98^eUQPhYd1lvfj zjTby?0K2a{T26A0<>B?N=h)P;CV7^|Jos-f!Yyu;x`d0wkn-c$A{E5SC1UTkL1kH_ zM>rMhEX0!4tSzU$yTT6c;}8oQ!>{3J_z_;9`ayojl-S-L<^uxJD;tLnWD67tNt+gN zf~1;}t2nda0KDrj8<}zwfu1?A$UtE}CZ&GgzM8klqT!q)!O!M|1QQcNiJ1!S0TCA5 zX@j3Ya?dxZvbET>v$M{0lr=K5T7GT>a`;JEnaYyTUGlTS%ZVt=%{a38VnysjmRuIg z#nxwVWdsfGL#^HCxDtQ^n8&?xQU(-`Xj;*%jJf^VLtA#gMSK)AfLjx@zjg|33=vDN zubuCBcdIywy;mw#KN*j}d6{`%Zh+RhasD+U6YOZP302G@k;bRc5K;5?Rc9tr6ob== z&IYt4Sw*#ccVR_w#nRCr5EW^hSL> z-R+q9j7{%|#_!f8V;YMK3oB<=gCZqUl2GMd4bEdyIVC=Lq^bkA0zrZM=-=f7bqP5& z@&p`c_$67Pj+YeRw4#m5x(3$!Qg`lJ@2ZGhw?NENjP}NmrR~V|?UfpQ7 zlenb2ZUi{R2ky+5hc%x&8s!G0F8}T?FshN!>5AX7Hxa+{627$%io!p|`m9GLbz@Ex zt^$`FqvEWYmprVNFvX?!)*_kf&xJb7p6ewslCk!AL5PB^@ZM`bR|g~cD!)17Hrn+m zhBSk!n3$5k!tU@J4@Ed?|I_=~LlsY;g{KE&L}RyhJLb&*!kJ8W6P|0Pu~O}VIkvK2 zIp^I8Cq}|NYUEwj^-b3)Ow<5>M6sKRba3;OfT1fY@q9Lz_az{U$)V;6&gjWb4Knb*G za<+e9)yl*hIe_7IldW_aUCfegjYunnMSeC~*?)EYG}Sqc;!oJj+&asW=7FYEBpPOs zv$w`EvZ8x&7*>?LQgp3%QOHo|&ns%1Fek-NA#sp4jWVFHP_|Zo)A&@e>;;)^euPz6 zIBWO%vf1Xr!q)d)*VgFj9-G9=moIrZ2L?dQc(b-a3IYZYw>_vJW2?gJWpA}Gy-kBX zyMXUBpH9|`#a(GW(!@kinORERf4xwzXhN0hfi$jyI>M|Z4n9i1%ql1>ti>}^smO;x ziT*E)y6;|+JWLg?QHU2DoQ}W@7sO0ByqEAf8lqq`dP5ct0N9kuCL!rG()cm$XhoL^ zXzs{jJ!}~(LbX_P16JIk_B>kXel`8^R~lz6+g)r>JxaEby99WcI>eZ!`O6PFW^U*R z_7{0x4Cfu(w>t`77lqi+_L$|NXLJ;1&F5XZ;{DCd&F$N_)pjbnyKym*%BI#8(JC3$ zNw6h?QEzJqB>Gw;m5IVs&hGF`^f-=tbt=eG1n;l`2Iv`H@RKuX>bSjB1?qIRi%HoC zagfw-;(k&Zs748*qX@IZDaW2!*TlQ}^0sq8*|2I&l7tXKsFJMUko>5XqI_GFaMvjg zFTz@BVG)&{VeHmtOp^6R&4>HrZdL2|g2}vn)p}*^5O%2{1~x|i=ApN>Pa<#OwX@o= zTDfdA>Hbs<|7pkG7iyMb#zE4cDL7EL)Hgs?&s8p4A%%u{NL4!qNvyu|FB;~_P4!;R@dM4@ynMW7}-WTNYI^BlBzSuxFvD` z);2hctcD*F#k>_3OrvUUu5)`^fJ%!9I5AZO%&U1_C(C|FxgZ(0V>87K2JAzworujR zcwJG<|4?uTkpjfPP9a;Jyj<3c%+vHFWCq7F(!(MFs?RY`dRxAXmhi2jR5;_*jlmjF zCoNLN%?3Zj&mvFBXk8lE3}G`0Q`B)X6bf2$puEn5*)k8pAtgDwl8!O)_-E>xp719F zs8r~gW8R)lxp$c@6YwV`i;{~@L*_Sk{ZP8I?M%gX?DLu221Kvip2{37#0;&g9R2d1 z|FG+svdvRJG6@b_%CFW7K3VEnpsgDf)T;`VA=pp`SJg{?kicA z<;K>vw~j4fy&)T_V>tz^Qy_kGCocYB0mUU+Kn<|Gu=^;yXAZ$U==7H^8WO@K1fHpZJdl(w+qOd7Oa=X+`;n)@YlXWN(&l^NA^Q@XWYEVY?+ z#-5-}*jO&yzGYS3YYey_+*nN=EydnrS?JeVEk)}{D4nkIeax)8a*RbjCBMV0>d?f?TWS{18zLGhO632QFIE&M>gSj-9_g?{;xdBWkm&P7Y$>5qP0H#y2hJ8@kM5IdFk+vG^<#4Np zqc$=QrJvlSkI#Rtrk)31;>O!-nA<3sWk2#m?DyXA(@#HfJY)~{(ysK9SZf1Of2$fo zmZHkb&z$nmD%}oLEeLK<1vOcvUieS5>qEos2`cQ^`~BOm=EFcX;i*n`t+phIl?zUAr^d9l+z6=O&*BjK;a7x@o5U5NsBH76%EJ3K0F;CYx#I~qt5(Sh@CW#uRv_j=y(gkxro7!} zB_;`0jH!y$E`DuaB7UYd;;NK~ankC&7vw4+=3>cd&=;8zcw@|;I8;q42)DRB>gt2$ zk;Ok%O)>a&Ki6y>S`!1pJw)Pk1J5bc2M!iXA|onQA)sJocd)B(|48`|E;+LD@HRdx z8LpQWNQHha)N_NtIv4F|Gu*P_LA8-Qxry>A7fU zHH53dDuWQmbE=`-vzx3wqrT^1DHfJ^h4z$z79(A3irD$CRzQjhrctFHsUn5<944cs zs0&~&=c=rwud{~0cx`_lqD7a9%6*Eo_bDN)4wL9xIt4MV%w^51se@G=@TstXvhK6=zu&Z{g(#&&qTJRg8FOy3 z$}ncr%EtCZj|Q43@5N!=lGT+zmT&Z)`Wp+kx7h|CX^i1{QbLX|(+}^5a=@WtU;3*> z{8<<1?rxz>R?k|5jRaY;NJ)GVUl`Q^3ox*D#eD4q(wrWdf{iO8%QI%Ta%* zs!i$1c%hYI-pR28=0t=c$Ls)oS@lqY>((hMw+P1eS9_3wZnH0q=QgsR|Es7yIfmunfRHLOmx^wGW^IYhekKk&o=WHEphL;2tSot@h5vPwh%s9#^YT zT!*BQ7M=Tg76qr&>Bv_LG_^^sMl`k(j-i852E8K}Yt_&cL|nP2SkquuGO@4N>gjt& zns--o*zV+0ZISjtiy0m*T~KpNO&8VpYba7bpCAV(r_C*nblZ+fL@fF%#Mv{J#z~_E zEA6|auBK9nmp_#gU_E$WbYCf*P%?HP_tm#Ucsoc|vZ75($8%ZxAUeLft~?B3` zfQh%8MKFc)%C1?CndV3rd>A67YO{v8j1fIod+xmf-GGZ?X`(z-K*-u=Msg5??VbLT zoq-0Fgf*(iZlII@f?2I*+o|1%kB6J3&*&T7)E4Y6nZKf$KTTYFj)vJxK^Gx#C5Ka?~B4`F3=vJzpK?xpwO8J0_OImVQ;pJXLQbno?bE zeixCRgk=fqQNd+xa(Pw0@}AkHq18Q1_e+80@>P8SV$J0o!~m3%01v`Tly@~IpTN{#BA%CJWf2nfA0OSvGWDJf-LxFc+1 zCEL@8x?1(T{onI*TWjH;?;f8~sIY$_eG~xYTAS?7eJ&6=u3Ru=%H0He1rFZ+?uBL1 zSL(~aMx{H$H=;%G#G>NmG}p|u1OgB+eA+D)!B=C6))M|8`m}mMtNc_>SIQ4kKIOfy z;C)S$#n4k)k%(Nmx;9yTrTSdZ$QsRYBeki9ibZp9n+j755QoQ+>kO2=&fC2|QztNfYDgGlFXZJmQ!u_Z6stℜyPd@U?|**%+rRzWPRJst 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

2@>a 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 omUSsVP#p+FuwP?_uo#{mdKI;Vst0JA+FDF6Tf delta 83 zcmXR)pCIYS#K6GtN8;^bAf@N&;uuoF`1Y(LBZGnfv%+ul9a`S$ZGS@+GGu#ca=znW kVQN%Zs>unY9&{xz|2zCC%R8vR2dI<5)78&qol`;+07d>9 0.0) + FragClr = vec4(finalFragColor / RealAlpha, RealAlpha); + else + FragClr = vec4(0.0, 0.0, 0.0, 0.0); +} diff --git a/data/shader/vulkan/text.vert b/data/shader/vulkan/text.vert new file mode 100644 index 000000000..1daf50903 --- /dev/null +++ b/data/shader/vulkan/text.vert @@ -0,0 +1,22 @@ +#version 450 +#extension GL_ARB_separate_shader_objects : enable + +layout (location = 0) in vec2 inVertex; +layout (location = 1) in vec2 inVertexTexCoord; +layout (location = 2) in vec4 inVertexColor; + +layout(push_constant) uniform SPosBO { + layout(offset = 0) mat4x2 gPos; + layout(offset = 32) float gTextureSize; +} gPosBO; + +layout (location = 0) noperspective out vec2 texCoord; +layout (location = 1) noperspective out vec4 outVertColor; + +void main() +{ + gl_Position = vec4(gPosBO.gPos * vec4(inVertex, 0.0, 1.0), 0.0, 1.0); + + texCoord = vec2(inVertexTexCoord.x / gPosBO.gTextureSize, inVertexTexCoord.y / gPosBO.gTextureSize); + outVertColor = inVertexColor; +} diff --git a/data/shader/vulkan/tile.frag b/data/shader/vulkan/tile.frag new file mode 100644 index 000000000..85205bcd7 --- /dev/null +++ b/data/shader/vulkan/tile.frag @@ -0,0 +1,25 @@ +#version 450 +#extension GL_ARB_separate_shader_objects : enable + +#ifdef TW_TILE_TEXTURED +layout(binding = 0) uniform sampler2DArray gTextureSampler; +#endif + +layout(push_constant) uniform SVertexColorBO { + layout(offset = 64) uniform vec4 gVertColor; +} gColorBO; + +#ifdef TW_TILE_TEXTURED +layout (location = 0) noperspective in vec3 TexCoord; +#endif + +layout (location = 0) out vec4 FragClr; +void main() +{ +#ifdef TW_TILE_TEXTURED + vec4 TexColor = texture(gTextureSampler, TexCoord.xyz); + FragClr = TexColor * gColorBO.gVertColor; +#else + FragClr = gColorBO.gVertColor; +#endif +} diff --git a/data/shader/vulkan/tile.vert b/data/shader/vulkan/tile.vert new file mode 100644 index 000000000..0dfebab96 --- /dev/null +++ b/data/shader/vulkan/tile.vert @@ -0,0 +1,49 @@ +#version 450 +#extension GL_ARB_separate_shader_objects : enable + +layout (location = 0) in vec2 inVertex; +#ifdef TW_TILE_TEXTURED +layout (location = 1) in vec3 inVertexTexCoord; +#endif + +layout(push_constant) uniform SPosBO { + layout(offset = 0) uniform mat4x2 gPos; + +#if defined(TW_TILE_BORDER) || defined(TW_TILE_BORDER_LINE) + layout(offset = 32) uniform vec2 gDir; + layout(offset = 40) uniform vec2 gOffset; +#endif + +#if defined(TW_TILE_BORDER) + layout(offset = 48) uniform int gJumpIndex; +#endif +} gPosBO; + +#ifdef TW_TILE_TEXTURED +layout (location = 0) noperspective out vec3 TexCoord; +#endif + +void main() +{ +#if defined(TW_TILE_BORDER) + vec4 VertPos = vec4(inVertex, 0.0, 1.0); + int XCount = gl_InstanceIndex - (int(gl_InstanceIndex/gPosBO.gJumpIndex) * gPosBO.gJumpIndex); + int YCount = (int(gl_InstanceIndex/gPosBO.gJumpIndex)); + VertPos.x += gPosBO.gOffset.x + gPosBO.gDir.x * float(XCount); + VertPos.y += gPosBO.gOffset.y + gPosBO.gDir.y * float(YCount); + + gl_Position = vec4(gPosBO.gPos * VertPos, 0.0, 1.0); +#elif defined(TW_TILE_BORDER_LINE) + vec4 VertPos = vec4(inVertex.x + gPosBO.gOffset.x, inVertex.y + gPosBO.gOffset.y, 0.0, 1.0); + VertPos.x += gPosBO.gDir.x * float(gl_InstanceIndex); + VertPos.y += gPosBO.gDir.y * float(gl_InstanceIndex); + + gl_Position = vec4(gPosBO.gPos * VertPos, 0.0, 1.0); +#else + gl_Position = vec4(gPosBO.gPos * vec4(inVertex, 0.0, 1.0), 0.0, 1.0); +#endif + +#ifdef TW_TILE_TEXTURED + TexCoord = inVertexTexCoord; +#endif +} diff --git a/scripts/android/cmake_android.sh b/scripts/android/cmake_android.sh index 86252360e..553bf6f6b 100755 --- a/scripts/android/cmake_android.sh +++ b/scripts/android/cmake_android.sh @@ -104,7 +104,8 @@ function build_for_type() { -DTOOLS=OFF \ -DDEV=TRUE \ -DCMAKE_CROSSCOMPILING=ON \ - -DPREFER_BUNDLED_LIBS=ON + -DPREFER_BUNDLED_LIBS=ON \ + -DVULKAN=ON ( cd "build_android/$_ANDROID_SUB_BUILD_DIR/$1" || exit 1 cmake --build . --target DDNet diff --git a/src/engine/client/backend/backend_base.h b/src/engine/client/backend/backend_base.h index 911e53ac4..dcd9ba2b3 100644 --- a/src/engine/client/backend/backend_base.h +++ b/src/engine/client/backend/backend_base.h @@ -29,7 +29,7 @@ public: virtual ~CCommandProcessorFragment_GLBase() = default; virtual bool RunCommand(const CCommandBuffer::SCommand *pBaseCommand) = 0; - virtual void StartCommands(size_t CommandCount) {} + virtual void StartCommands(size_t CommandCount, size_t EstimatedRenderCallCount) {} virtual void EndCommands() {} enum diff --git a/src/engine/client/backend/opengl/backend_opengl.cpp b/src/engine/client/backend/opengl/backend_opengl.cpp index 6275d1886..e4d2fb8bf 100644 --- a/src/engine/client/backend/opengl/backend_opengl.cpp +++ b/src/engine/client/backend/opengl/backend_opengl.cpp @@ -1005,8 +1005,10 @@ void CCommandProcessorFragment_OpenGL::Cmd_Render(const CCommandBuffer::SCommand #endif } -void CCommandProcessorFragment_OpenGL::Cmd_Screenshot(const CCommandBuffer::SCommand_Screenshot *pCommand) +void CCommandProcessorFragment_OpenGL::Cmd_Screenshot(const CCommandBuffer::SCommand_TrySwapAndScreenshot *pCommand) { + *pCommand->m_pSwapped = false; + // fetch image data GLint aViewport[4] = {0, 0, 0, 0}; glGetIntegerv(GL_VIEWPORT, aViewport); @@ -1088,8 +1090,8 @@ bool CCommandProcessorFragment_OpenGL::RunCommand(const CCommandBuffer::SCommand case CCommandBuffer::CMD_RENDER_TEX3D: Cmd_RenderTex3D(static_cast(pBaseCommand)); break; - case CCommandBuffer::CMD_SCREENSHOT: - Cmd_Screenshot(static_cast(pBaseCommand)); + case CCommandBuffer::CMD_TRY_SWAP_AND_SCREENSHOT: + Cmd_Screenshot(static_cast(pBaseCommand)); break; case CCommandBuffer::CMD_UPDATE_VIEWPORT: Cmd_Update_Viewport(static_cast(pBaseCommand)); @@ -1971,6 +1973,7 @@ void CCommandProcessorFragment_OpenGL2::Cmd_CreateBufferContainer(const CCommand { SBufferContainer Container; Container.m_ContainerInfo.m_Stride = 0; + Container.m_ContainerInfo.m_VertBufferBindingIndex = -1; m_BufferContainers.push_back(Container); } } diff --git a/src/engine/client/backend/opengl/backend_opengl.h b/src/engine/client/backend/opengl/backend_opengl.h index 8345a2a05..43959e76f 100644 --- a/src/engine/client/backend/opengl/backend_opengl.h +++ b/src/engine/client/backend/opengl/backend_opengl.h @@ -103,7 +103,7 @@ protected: virtual void Cmd_Clear(const CCommandBuffer::SCommand_Clear *pCommand); virtual void Cmd_Render(const CCommandBuffer::SCommand_Render *pCommand); virtual void Cmd_RenderTex3D(const CCommandBuffer::SCommand_RenderTex3D *pCommand) { dbg_assert(false, "Call of unsupported Cmd_RenderTex3D"); } - virtual void Cmd_Screenshot(const CCommandBuffer::SCommand_Screenshot *pCommand); + virtual void Cmd_Screenshot(const CCommandBuffer::SCommand_TrySwapAndScreenshot *pCommand); virtual void Cmd_Update_Viewport(const CCommandBuffer::SCommand_Update_Viewport *pCommand); virtual void Cmd_Finish(const CCommandBuffer::SCommand_Finish *pCommand); @@ -139,7 +139,6 @@ class CCommandProcessorFragment_OpenGL2 : public CCommandProcessorFragment_OpenG { struct SBufferContainer { - SBufferContainer() {} SBufferContainerInfo m_ContainerInfo; }; std::vector m_BufferContainers; diff --git a/src/engine/client/backend/opengl/backend_opengl3.cpp b/src/engine/client/backend/opengl/backend_opengl3.cpp index c0ef1553a..79fe01120 100644 --- a/src/engine/client/backend/opengl/backend_opengl3.cpp +++ b/src/engine/client/backend/opengl/backend_opengl3.cpp @@ -1042,6 +1042,7 @@ void CCommandProcessorFragment_OpenGL3_3::Cmd_CreateBufferContainer(const CComma { SBufferContainer Container; Container.m_ContainerInfo.m_Stride = 0; + Container.m_ContainerInfo.m_VertBufferBindingIndex = -1; m_BufferContainers.push_back(Container); } } diff --git a/src/engine/client/backend/vulkan/backend_vulkan.cpp b/src/engine/client/backend/vulkan/backend_vulkan.cpp new file mode 100644 index 000000000..85e19c231 --- /dev/null +++ b/src/engine/client/backend/vulkan/backend_vulkan.cpp @@ -0,0 +1,7060 @@ +#if defined(CONF_BACKEND_VULKAN) + +#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 +#include +#include + +#include +#include + +#ifndef VK_API_VERSION_MAJOR +#define VK_API_VERSION_MAJOR VK_VERSION_MAJOR +#define VK_API_VERSION_MINOR VK_VERSION_MINOR +#define VK_API_VERSION_PATCH VK_VERSION_PATCH +#endif + +class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase +{ + enum EMemoryBlockUsage + { + MEMORY_BLOCK_USAGE_TEXTURE = 0, + MEMORY_BLOCK_USAGE_BUFFER, + MEMORY_BLOCK_USAGE_STREAM, + MEMORY_BLOCK_USAGE_STAGING, + + // whenever dummy is used, make sure to deallocate all memory + MEMORY_BLOCK_USAGE_DUMMY, + }; + + void NotImplemented() + { + dbg_assert(false, "function not implemented!"); + } + + bool IsVerbose() + { + return g_Config.m_DbgGfx == DEBUG_GFX_MODE_VERBOSE || g_Config.m_DbgGfx == DEBUG_GFX_MODE_ALL; + } + + void VerboseAllocatedMemory(VkDeviceSize Size, size_t FrameImageIndex, EMemoryBlockUsage MemUsage) + { + const char *pUsage = "unknown"; + switch(MemUsage) + { + case MEMORY_BLOCK_USAGE_TEXTURE: + pUsage = "texture"; + break; + case MEMORY_BLOCK_USAGE_BUFFER: + pUsage = "buffer"; + break; + case MEMORY_BLOCK_USAGE_STREAM: + pUsage = "stream"; + break; + case MEMORY_BLOCK_USAGE_STAGING: + pUsage = "staging buffer"; + break; + default: break; + } + dbg_msg("vulkan", "allocated chunk of memory with size: %zu for frame %zu (%s)", (size_t)Size, (size_t)m_CurImageIndex, pUsage); + } + + void VerboseDeallocatedMemory(VkDeviceSize Size, size_t FrameImageIndex, EMemoryBlockUsage MemUsage) + { + const char *pUsage = "unknown"; + switch(MemUsage) + { + case MEMORY_BLOCK_USAGE_TEXTURE: + pUsage = "texture"; + break; + case MEMORY_BLOCK_USAGE_BUFFER: + pUsage = "buffer"; + break; + case MEMORY_BLOCK_USAGE_STREAM: + pUsage = "stream"; + break; + case MEMORY_BLOCK_USAGE_STAGING: + pUsage = "staging buffer"; + break; + default: break; + } + dbg_msg("vulkan", "deallocated chunk of memory with size: %zu for frame %zu (%s)", (size_t)Size, (size_t)m_CurImageIndex, pUsage); + } + + /************************ + * STRUCT DEFINITIONS + ************************/ + + static constexpr size_t s_StagingBufferCacheID = 0; + static constexpr size_t s_StagingBufferImageCacheID = 1; + static constexpr size_t s_VertexBufferCacheID = 2; + static constexpr size_t s_ImageBufferCacheID = 3; + + struct SDeviceMemoryBlock + { + VkDeviceMemory m_Mem = VK_NULL_HANDLE; + VkDeviceSize m_Size = 0; + EMemoryBlockUsage m_UsageType; + }; + + struct SDeviceDescriptorPools; + + struct SDeviceDescriptorSet + { + VkDescriptorSet m_Descriptor = VK_NULL_HANDLE; + SDeviceDescriptorPools *m_pPools = nullptr; + size_t m_PoolIndex = std::numeric_limits::max(); + }; + + struct SDeviceDescriptorPool + { + VkDescriptorPool m_Pool; + VkDeviceSize m_Size = 0; + VkDeviceSize m_CurSize = 0; + }; + + struct SDeviceDescriptorPools + { + std::vector m_Pools; + VkDeviceSize m_DefaultAllocSize = 0; + bool m_IsUniformPool = false; + }; + + // some mix of queue and binary tree + struct SMemoryHeap + { + struct SMemoryHeapElement; + struct SMemoryHeapQueueElement + { + size_t m_AllocationSize; + // only useful information for the heap + size_t m_OffsetInHeap; + // useful for the user of this element + size_t m_OffsetToAlign; + SMemoryHeapElement *m_pElementInHeap; + bool operator>(const SMemoryHeapQueueElement &Other) const { return m_AllocationSize > Other.m_AllocationSize; } + }; + + typedef std::multiset> TMemoryHeapQueue; + + struct SMemoryHeapElement + { + size_t m_AllocationSize; + size_t m_Offset; + SMemoryHeapElement *m_pParent; + std::unique_ptr m_pLeft; + std::unique_ptr m_pRight; + + bool m_InUse; + TMemoryHeapQueue::iterator m_InQueue; + }; + + SMemoryHeapElement m_Root; + TMemoryHeapQueue m_Elements; + + void Init(size_t Size, size_t Offset) + { + m_Root.m_AllocationSize = Size; + m_Root.m_Offset = Offset; + m_Root.m_pParent = nullptr; + m_Root.m_InUse = false; + + SMemoryHeapQueueElement QueueEl; + QueueEl.m_AllocationSize = Size; + QueueEl.m_OffsetInHeap = Offset; + QueueEl.m_OffsetToAlign = Offset; + QueueEl.m_pElementInHeap = &m_Root; + m_Root.m_InQueue = m_Elements.insert(QueueEl); + } + + bool Allocate(size_t AllocSize, size_t AllocAlignment, SMemoryHeapQueueElement &AllocatedMemory) + { + if(m_Elements.empty()) + { + return false; + } + else + { + // calculate the alignment + size_t ExtraSizeAlign = m_Elements.begin()->m_OffsetInHeap % AllocAlignment; + if(ExtraSizeAlign != 0) + ExtraSizeAlign = AllocAlignment - ExtraSizeAlign; + size_t RealAllocSize = AllocSize + ExtraSizeAlign; + + // check if there is enough space in this instance + if(m_Elements.begin()->m_AllocationSize < RealAllocSize) + { + return false; + } + else + { + auto TopEl = *m_Elements.begin(); + m_Elements.erase(TopEl.m_pElementInHeap->m_InQueue); + + TopEl.m_pElementInHeap->m_InUse = true; + + // the heap element gets children + TopEl.m_pElementInHeap->m_pLeft = std::make_unique(); + TopEl.m_pElementInHeap->m_pLeft->m_AllocationSize = RealAllocSize; + TopEl.m_pElementInHeap->m_pLeft->m_Offset = TopEl.m_OffsetInHeap; + TopEl.m_pElementInHeap->m_pLeft->m_pParent = TopEl.m_pElementInHeap; + TopEl.m_pElementInHeap->m_pLeft->m_InUse = true; + + if(RealAllocSize < TopEl.m_AllocationSize) + { + SMemoryHeapQueueElement RemainingEl; + RemainingEl.m_OffsetInHeap = TopEl.m_OffsetInHeap + RealAllocSize; + RemainingEl.m_AllocationSize = TopEl.m_AllocationSize - RealAllocSize; + + TopEl.m_pElementInHeap->m_pRight = std::make_unique(); + TopEl.m_pElementInHeap->m_pRight->m_AllocationSize = RemainingEl.m_AllocationSize; + TopEl.m_pElementInHeap->m_pRight->m_Offset = RemainingEl.m_OffsetInHeap; + TopEl.m_pElementInHeap->m_pRight->m_pParent = TopEl.m_pElementInHeap; + TopEl.m_pElementInHeap->m_pRight->m_InUse = false; + + RemainingEl.m_pElementInHeap = TopEl.m_pElementInHeap->m_pRight.get(); + RemainingEl.m_pElementInHeap->m_InQueue = m_Elements.insert(RemainingEl); + } + + AllocatedMemory.m_pElementInHeap = TopEl.m_pElementInHeap->m_pLeft.get(); + AllocatedMemory.m_AllocationSize = RealAllocSize; + AllocatedMemory.m_OffsetInHeap = TopEl.m_OffsetInHeap; + AllocatedMemory.m_OffsetToAlign = TopEl.m_OffsetInHeap + ExtraSizeAlign; + return true; + } + } + } + + void Free(SMemoryHeapQueueElement &AllocatedMemory) + { + bool ContinueFree = true; + SMemoryHeapQueueElement ThisEl = AllocatedMemory; + while(ContinueFree) + { + // first check if the other block is in use, if not merge them again + SMemoryHeapElement *pThisHeapObj = ThisEl.m_pElementInHeap; + SMemoryHeapElement *pThisParent = pThisHeapObj->m_pParent; + pThisHeapObj->m_InUse = false; + SMemoryHeapElement *pOtherHeapObj = nullptr; + if(pThisParent != nullptr && pThisHeapObj == pThisParent->m_pLeft.get()) + pOtherHeapObj = pThisHeapObj->m_pParent->m_pRight.get(); + else if(pThisParent != nullptr) + pOtherHeapObj = pThisHeapObj->m_pParent->m_pLeft.get(); + + if((pThisParent != nullptr && pOtherHeapObj == nullptr) || (pOtherHeapObj != nullptr && !pOtherHeapObj->m_InUse)) + { + // merge them + if(pOtherHeapObj != nullptr) + { + m_Elements.erase(pOtherHeapObj->m_InQueue); + pOtherHeapObj->m_InUse = false; + } + + SMemoryHeapQueueElement ParentEl; + ParentEl.m_OffsetInHeap = pThisParent->m_Offset; + ParentEl.m_AllocationSize = pThisParent->m_AllocationSize; + ParentEl.m_pElementInHeap = pThisParent; + + pThisParent->m_pLeft = nullptr; + pThisParent->m_pRight = nullptr; + + ThisEl = ParentEl; + } + else + { + // else just put this back into queue + ThisEl.m_pElementInHeap->m_InQueue = m_Elements.insert(ThisEl); + ContinueFree = false; + } + } + } + + bool IsUnused() + { + return !m_Root.m_InUse; + } + }; + + template + struct SMemoryBlock + { + SMemoryHeap::SMemoryHeapQueueElement m_HeapData; + + VkDeviceSize m_UsedSize; + + // optional + VkBuffer m_Buffer; + + SDeviceMemoryBlock m_BufferMem; + void *m_pMappedBuffer; + + bool m_IsCached; + SMemoryHeap *m_pHeap; + }; + + template + struct SMemoryImageBlock : public SMemoryBlock + { + uint32_t m_ImageMemoryBits; + }; + + template + struct SMemoryBlockCache + { + struct SMemoryCacheType + { + struct SMemoryCacheHeap + { + SMemoryHeap m_Heap; + VkBuffer m_Buffer; + + SDeviceMemoryBlock m_BufferMem; + void *m_pMappedBuffer; + }; + std::vector m_MemoryHeaps; + }; + SMemoryCacheType m_MemoryCaches; + std::vector>> m_FrameDelayedCachedBufferCleanup; + + bool m_CanShrink = false; + + void Init(size_t SwapChainImageCount) + { + m_FrameDelayedCachedBufferCleanup.resize(SwapChainImageCount); + } + + void DestroyFrameData(size_t ImageCount) + { + for(size_t i = 0; i < ImageCount; ++i) + Cleanup(i); + m_FrameDelayedCachedBufferCleanup.clear(); + } + + void Destroy(VkDevice &Device) + { + for(auto it = m_MemoryCaches.m_MemoryHeaps.begin(); it != m_MemoryCaches.m_MemoryHeaps.end();) + { + auto *pHeap = *it; + if(pHeap->m_pMappedBuffer != nullptr) + vkUnmapMemory(Device, pHeap->m_BufferMem.m_Mem); + if(pHeap->m_Buffer != VK_NULL_HANDLE) + vkDestroyBuffer(Device, pHeap->m_Buffer, nullptr); + vkFreeMemory(Device, pHeap->m_BufferMem.m_Mem, nullptr); + + delete pHeap; + it = m_MemoryCaches.m_MemoryHeaps.erase(it); + } + + m_MemoryCaches.m_MemoryHeaps.clear(); + m_FrameDelayedCachedBufferCleanup.clear(); + } + + void Cleanup(size_t ImgIndex) + { + for(auto &MemBlock : m_FrameDelayedCachedBufferCleanup[ImgIndex]) + { + MemBlock.m_UsedSize = 0; + MemBlock.m_pHeap->Free(MemBlock.m_HeapData); + + m_CanShrink = true; + } + m_FrameDelayedCachedBufferCleanup[ImgIndex].clear(); + } + + void FreeMemBlock(SMemoryBlock &Block, size_t ImgIndex) + { + m_FrameDelayedCachedBufferCleanup[ImgIndex].push_back(Block); + } + + // returns the total free'd memory + size_t Shrink(VkDevice &Device) + { + size_t FreeedMemory = 0; + if(m_CanShrink) + { + m_CanShrink = false; + if(m_MemoryCaches.m_MemoryHeaps.size() > 1) + { + for(auto it = m_MemoryCaches.m_MemoryHeaps.begin(); it != m_MemoryCaches.m_MemoryHeaps.end();) + { + auto *pHeap = *it; + if(pHeap->m_Heap.IsUnused()) + { + if(pHeap->m_pMappedBuffer != nullptr) + vkUnmapMemory(Device, pHeap->m_BufferMem.m_Mem); + if(pHeap->m_Buffer != VK_NULL_HANDLE) + vkDestroyBuffer(Device, pHeap->m_Buffer, nullptr); + vkFreeMemory(Device, pHeap->m_BufferMem.m_Mem, nullptr); + FreeedMemory += pHeap->m_BufferMem.m_Size; + + delete pHeap; + it = m_MemoryCaches.m_MemoryHeaps.erase(it); + if(m_MemoryCaches.m_MemoryHeaps.size() == 1) + break; + } + else + ++it; + } + } + } + + return FreeedMemory; + } + }; + + struct CTexture + { + VkImage m_Img = VK_NULL_HANDLE; + SMemoryImageBlock m_ImgMem; + VkImageView m_ImgView = VK_NULL_HANDLE; + VkSampler m_aSamplers[2] = {VK_NULL_HANDLE, VK_NULL_HANDLE}; + + VkImage m_Img3D = VK_NULL_HANDLE; + SMemoryImageBlock m_Img3DMem; + VkImageView m_Img3DView = VK_NULL_HANDLE; + VkSampler m_Sampler3D = VK_NULL_HANDLE; + + uint32_t m_Width = 0; + uint32_t m_Height = 0; + uint32_t m_RescaleCount = 0; + + uint32_t m_MipMapCount = 1; + + std::array m_aVKStandardTexturedDescrSets; + SDeviceDescriptorSet m_VKStandard3DTexturedDescrSet; + SDeviceDescriptorSet m_VKTextDescrSet; + }; + + struct SBufferObject + { + SMemoryBlock m_Mem; + }; + + struct SBufferObjectFrame + { + SBufferObject m_BufferObject; + + // since stream buffers can be used the cur buffer should always be used for rendering + bool m_IsStreamedBuffer = false; + VkBuffer m_CurBuffer = VK_NULL_HANDLE; + size_t m_CurBufferOffset = 0; + }; + + struct SBufferContainer + { + int m_BufferObjectIndex; + }; + + struct SFrameBuffers + { + VkBuffer m_Buffer; + SDeviceMemoryBlock m_BufferMem; + size_t m_OffsetInBuffer = 0; + size_t m_Size; + size_t m_UsedSize; + void *m_pMappedBufferData; + + SFrameBuffers(VkBuffer Buffer, SDeviceMemoryBlock BufferMem, size_t OffsetInBuffer, size_t Size, size_t UsedSize, void *pMappedBufferData) : + m_Buffer(Buffer), m_BufferMem(BufferMem), m_OffsetInBuffer(OffsetInBuffer), m_Size(Size), m_UsedSize(UsedSize), m_pMappedBufferData(pMappedBufferData) + { + } + }; + + struct SFrameUniformBuffers : public SFrameBuffers + { + std::array m_aUniformSets; + + SFrameUniformBuffers(VkBuffer Buffer, SDeviceMemoryBlock BufferMem, size_t OffsetInBuffer, size_t Size, size_t UsedSize, void *pMappedBufferData) : + SFrameBuffers(Buffer, BufferMem, OffsetInBuffer, Size, UsedSize, pMappedBufferData) {} + }; + + template + struct SStreamMemory + { + typedef std::vector> TBufferObjectsOfFrame; + typedef std::vector> TMemoryMapRangesOfFrame; + typedef std::vector TStreamUseCount; + TBufferObjectsOfFrame m_BufferObjectsOfFrame; + TMemoryMapRangesOfFrame m_BufferObjectsOfFrameRangeData; + TStreamUseCount m_CurrentUsedCount; + + std::vector &GetBuffers(size_t FrameImageIndex) + { + return m_BufferObjectsOfFrame[FrameImageIndex]; + } + + std::vector &GetRanges(size_t FrameImageIndex) + { + return m_BufferObjectsOfFrameRangeData[FrameImageIndex]; + } + + size_t GetUsedCount(size_t FrameImageIndex) + { + return m_CurrentUsedCount[FrameImageIndex]; + } + + void IncreaseUsedCount(size_t FrameImageIndex) + { + ++m_CurrentUsedCount[FrameImageIndex]; + } + + bool IsUsed(size_t FrameImageIndex) + { + return GetUsedCount(FrameImageIndex) > 0; + } + + void ResetFrame(size_t FrameImageIndex) + { + m_CurrentUsedCount[FrameImageIndex] = 0; + } + + void Init(size_t FrameImageCount) + { + m_BufferObjectsOfFrame.resize(FrameImageCount); + m_BufferObjectsOfFrameRangeData.resize(FrameImageCount); + m_CurrentUsedCount.resize(FrameImageCount); + } + + typedef std::function TDestroyBufferFunc; + + void Destroy(TDestroyBufferFunc &&DestroyBuffer) + { + size_t ImageIndex = 0; + for(auto &vBuffersOfFrame : m_BufferObjectsOfFrame) + { + for(auto &BufferOfFrame : vBuffersOfFrame) + { + VkDeviceMemory BufferMem = BufferOfFrame.m_BufferMem.m_Mem; + DestroyBuffer(ImageIndex, BufferOfFrame); + + // delete similar buffers + for(auto &BufferOfFrameDel : vBuffersOfFrame) + { + if(BufferOfFrameDel.m_BufferMem.m_Mem == BufferMem) + { + BufferOfFrameDel.m_Buffer = VK_NULL_HANDLE; + BufferOfFrameDel.m_BufferMem.m_Mem = VK_NULL_HANDLE; + } + } + } + ++ImageIndex; + } + m_BufferObjectsOfFrame.clear(); + m_BufferObjectsOfFrameRangeData.clear(); + m_CurrentUsedCount.clear(); + } + }; + + struct SShaderModule + { + VkShaderModule m_VertShaderModule; + VkShaderModule m_FragShaderModule; + + VkDevice m_VKDevice; + + ~SShaderModule() + { + vkDestroyShaderModule(m_VKDevice, m_VertShaderModule, nullptr); + vkDestroyShaderModule(m_VKDevice, m_FragShaderModule, nullptr); + } + }; + + enum EVulkanBackendAddressModes + { + VULKAN_BACKEND_ADDRESS_MODE_REPEAT = 0, + VULKAN_BACKEND_ADDRESS_MODE_CLAMP_EDGES, + + VULKAN_BACKEND_ADDRESS_MODE_COUNT, + }; + + enum EVulkanBackendBlendModes + { + VULKAN_BACKEND_BLEND_MODE_ALPHA = 0, + VULKAN_BACKEND_BLEND_MODE_NONE, + VULKAN_BACKEND_BLEND_MODE_ADDITATIVE, + + VULKAN_BACKEND_BLEND_MODE_COUNT, + }; + + enum EVulkanBackendClipModes + { + VULKAN_BACKEND_CLIP_MODE_NONE = 0, + VULKAN_BACKEND_CLIP_MODE_DYNAMIC_SCISSOR_AND_VIEWPORT, + + VULKAN_BACKEND_CLIP_MODE_COUNT, + }; + + enum EVulkanBackendTextureModes + { + VULKAN_BACKEND_TEXTURE_MODE_NOT_TEXTURED = 0, + VULKAN_BACKEND_TEXTURE_MODE_TEXTURED, + + VULKAN_BACKEND_TEXTURE_MODE_COUNT, + }; + + struct SPipelineContainer + { + // 3 blend modes - 2 viewport & scissor modes - 2 texture modes + std::array, VULKAN_BACKEND_CLIP_MODE_COUNT>, VULKAN_BACKEND_BLEND_MODE_COUNT> m_aaaPipelineLayouts; + std::array, VULKAN_BACKEND_CLIP_MODE_COUNT>, VULKAN_BACKEND_BLEND_MODE_COUNT> m_aaaPipelines; + + SPipelineContainer() + { + for(auto &aaPipeLayouts : m_aaaPipelineLayouts) + { + for(auto &aPipeLayouts : aaPipeLayouts) + { + for(auto &PipeLayout : aPipeLayouts) + { + PipeLayout = VK_NULL_HANDLE; + } + } + } + for(auto &aaPipe : m_aaaPipelines) + { + for(auto &aPipe : aaPipe) + { + for(auto &Pipe : aPipe) + { + Pipe = VK_NULL_HANDLE; + } + } + } + } + + void Destroy(VkDevice &Device) + { + for(auto &aaPipeLayouts : m_aaaPipelineLayouts) + { + for(auto &aPipeLayouts : aaPipeLayouts) + { + for(auto &PipeLayout : aPipeLayouts) + { + if(PipeLayout != VK_NULL_HANDLE) + vkDestroyPipelineLayout(Device, PipeLayout, nullptr); + PipeLayout = VK_NULL_HANDLE; + } + } + } + for(auto &aaPipe : m_aaaPipelines) + { + for(auto &aPipe : aaPipe) + { + for(auto &Pipe : aPipe) + { + if(Pipe != VK_NULL_HANDLE) + vkDestroyPipeline(Device, Pipe, nullptr); + Pipe = VK_NULL_HANDLE; + } + } + } + } + }; + + /******************************* + * UNIFORM PUSH CONSTANT LAYOUTS + ********************************/ + + struct SUniformGPos + { + float m_aPos[4 * 2]; + }; + + struct SUniformGTextPos + { + float m_aPos[4 * 2]; + float m_TextureSize; + }; + + struct SUniformTextGFragmentOffset + { + float m_Padding[3]; + }; + + struct SUniformTextGFragmentConstants + { + float m_aTextColor[4]; + float m_aTextOutlineColor[4]; + }; + + struct SUniformTextFragment + { + SUniformTextGFragmentConstants m_Constants; + }; + + struct SUniformTileGPos + { + float m_aPos[4 * 2]; + }; + + struct SUniformTileGPosBorderLine : public SUniformTileGPos + { + vec2 m_Dir; + vec2 m_Offset; + }; + + struct SUniformTileGPosBorder : public SUniformTileGPosBorderLine + { + int32_t m_JumpIndex; + }; + + struct SUniformTileGVertColor + { + float m_aColor[4]; + }; + + struct SUniformTileGVertColorAlign + { + float m_aPad[(64 - 52) / 4]; + }; + + struct SUniformPrimExGPosRotationless + { + float m_aPos[4 * 2]; + }; + + struct SUniformPrimExGPos : public SUniformPrimExGPosRotationless + { + vec2 m_Center; + float m_Rotation; + }; + + struct SUniformPrimExGVertColor + { + float m_aColor[4]; + }; + + struct SUniformPrimExGVertColorAlign + { + float m_aPad[(48 - 44) / 4]; + }; + + struct SUniformSpriteMultiGPos + { + float m_aPos[4 * 2]; + vec2 m_Center; + }; + + struct SUniformSpriteMultiGVertColor + { + float m_aColor[4]; + }; + + struct SUniformSpriteMultiGVertColorAlign + { + float m_aPad[(48 - 40) / 4]; + }; + + struct SUniformSpriteMultiPushGPosBase + { + float m_aPos[4 * 2]; + vec2 m_Center; + vec2 m_Padding; + }; + + struct SUniformSpriteMultiPushGPos : public SUniformSpriteMultiPushGPosBase + { + vec4 m_aPSR[1]; + }; + + struct SUniformSpriteMultiPushGVertColor + { + float m_aColor[4]; + }; + + struct SUniformQuadGPosBase + { + float m_aPos[4 * 2]; + int32_t m_QuadOffset; + }; + + struct SUniformQuadPushGBufferObject + { + vec4 m_VertColor; + vec2 m_Offset; + float m_Rotation; + float m_Padding; + }; + + struct SUniformQuadPushGPos + { + float m_aPos[4 * 2]; + SUniformQuadPushGBufferObject m_BOPush; + int32_t m_QuadOffset; + }; + + struct SUniformQuadGPos + { + float m_aPos[4 * 2]; + int32_t m_QuadOffset; + }; + + enum ESupportedSamplerTypes + { + SUPPORTED_SAMPLER_TYPE_REPEAT = 0, + SUPPORTED_SAMPLER_TYPE_CLAMP_TO_EDGE, + SUPPORTED_SAMPLER_TYPE_2D_TEXTURE_ARRAY, + + SUPPORTED_SAMPLER_TYPE_COUNT, + }; + + struct SShaderFileCache + { + std::vector m_Binary; + }; + + struct SSwapImgViewportExtent + { + VkExtent2D m_SwapImg; + VkExtent2D m_Viewport; + }; + + /************************ + * MEMBER VARIABLES + ************************/ + + std::unordered_map m_ShaderFiles; + + SMemoryBlockCache m_StagingBufferCache; + SMemoryBlockCache m_StagingBufferCacheImage; + SMemoryBlockCache m_VertexBufferCache; + std::map> m_ImageBufferCaches; + + std::vector m_NonFlushedStagingBufferRange; + + std::vector m_Textures; + + std::atomic *m_pTextureMemoryUsage; + std::atomic *m_pBufferMemoryUsage; + std::atomic *m_pStreamMemoryUsage; + std::atomic *m_pStagingMemoryUsage; + + TTWGraphicsGPUList *m_pGPUList; + + int m_GlobalTextureLodBIAS; + uint32_t m_MultiSamplingCount = 1; + + bool m_RecreateSwapChain = false; + bool m_SwapchainCreated = false; + bool m_RenderingPaused = false; + bool m_HasDynamicViewport = false; + VkOffset2D m_DynamicViewportOffset; + VkExtent2D m_DynamicViewportSize; + + bool m_AllowsLinearBlitting = false; + bool m_OptimalSwapChainImageBlitting = false; + bool m_OptimalRGBAImageBlitting = false; + bool m_LinearRGBAImageBlitting = false; + + VkBuffer m_IndexBuffer; + SDeviceMemoryBlock m_IndexBufferMemory; + + VkBuffer m_RenderIndexBuffer; + SDeviceMemoryBlock m_RenderIndexBufferMemory; + size_t m_CurRenderIndexPrimitiveCount; + + VkDeviceSize m_NonCoherentMemAlignment; + VkDeviceSize m_OptimalImageCopyMemAlignment; + uint32_t m_MaxTextureSize; + uint32_t m_MaxSamplerAnisotropy; + VkSampleCountFlags m_MaxMultiSample; + + uint32_t m_MinUniformAlign; + + std::vector m_ScreenshotHelper; + + SDeviceMemoryBlock m_GetPresentedImgDataHelperMem; + VkImage m_GetPresentedImgDataHelperImage = VK_NULL_HANDLE; + uint8_t *m_pGetPresentedImgDataHelperMappedMemory = nullptr; + VkDeviceSize m_GetPresentedImgDataHelperMappedLayoutOffset = 0; + uint32_t m_GetPresentedImgDataHelperWidth = 0; + uint32_t m_GetPresentedImgDataHelperHeight = 0; + VkFence m_GetPresentedImgDataHelperFence = VK_NULL_HANDLE; + + std::array m_aSamplers; + + class IStorage *m_pStorage; + + struct SDelayedBufferCleanupItem + { + VkBuffer m_Buffer; + SDeviceMemoryBlock m_Mem; + void *m_pMappedData = nullptr; + }; + + std::vector> m_FrameDelayedBufferCleanup; + std::vector> m_FrameDelayedTextureCleanup; + std::vector>> m_FrameDelayedTextTexturesCleanup; + + size_t m_ThreadCount = 7; + static constexpr size_t ms_MainThreadIndex = 0; + size_t m_CurCommandInPipe = 0; + size_t m_CurRenderCallCountInPipe = 0; + size_t m_CommandsInPipe = 0; + size_t m_RenderCallsInPipe = 0; + size_t m_LastCommandsInPipeThreadIndex = 0; + + struct SRenderThread + { + bool m_IsRendering = false; + std::thread m_Thread; + std::mutex m_Mutex; + std::condition_variable m_Cond; + bool m_Finished = false; + bool m_Started = false; + }; + std::vector> m_RenderThreads; + +private: + std::vector m_SwapChainImageViewList; + std::vector m_FramebufferList; + std::vector m_MainDrawCommandBuffers; + + std::vector> m_ThreadDrawCommandBuffers; + std::vector m_HelperThreadDrawCommandBuffers; + std::vector> m_UsedThreadDrawCommandBuffer; + + std::vector m_MemoryCommandBuffers; + std::vector m_UsedMemoryCommandBuffer; + + // swapped by use case + std::vector m_WaitSemaphores; + std::vector m_SigSemaphores; + + std::vector m_MemorySemaphores; + + std::vector m_FrameFences; + std::vector m_ImagesFences; + + uint64_t m_CurFrame = 0; + std::vector m_ImageLastFrameCheck; + + uint32_t m_LastPresentedSwapChainImageIndex; + + std::vector m_BufferObjects; + + std::vector m_BufferContainers; + + VkInstance m_VKInstance; + VkPhysicalDevice m_VKGPU; + uint32_t m_VKGraphicsQueueIndex = std::numeric_limits::max(); + VkDevice m_VKDevice; + VkQueue m_VKGraphicsQueue, m_VKPresentQueue; + VkSurfaceKHR m_VKPresentSurface; + SSwapImgViewportExtent m_VKSwapImgAndViewportExtent; + + VkDebugUtilsMessengerEXT m_DebugMessenger; + + VkDescriptorSetLayout m_StandardTexturedDescriptorSetLayout; + VkDescriptorSetLayout m_Standard3DTexturedDescriptorSetLayout; + + VkDescriptorSetLayout m_TextDescriptorSetLayout; + + VkDescriptorSetLayout m_SpriteMultiUniformDescriptorSetLayout; + VkDescriptorSetLayout m_QuadUniformDescriptorSetLayout; + + SPipelineContainer m_StandardPipeline; + SPipelineContainer m_StandardLinePipeline; + SPipelineContainer m_Standard3DPipeline; + SPipelineContainer m_TextPipeline; + SPipelineContainer m_TilePipeline; + SPipelineContainer m_TileBorderPipeline; + SPipelineContainer m_TileBorderLinePipeline; + SPipelineContainer m_PrimExPipeline; + SPipelineContainer m_PrimExRotationlessPipeline; + SPipelineContainer m_SpriteMultiPipeline; + SPipelineContainer m_SpriteMultiPushPipeline; + SPipelineContainer m_QuadPipeline; + SPipelineContainer m_QuadPushPipeline; + + std::vector m_vLastPipeline; + + std::vector m_vCommandPools; + + VkRenderPass m_VKRenderPass; + + VkSurfaceFormatKHR m_VKSurfFormat; + + SDeviceDescriptorPools m_StandardTextureDescrPool; + SDeviceDescriptorPools m_TextTextureDescrPool; + + std::vector m_UniformBufferDescrPools; + + VkSwapchainKHR m_VKSwapChain = VK_NULL_HANDLE; + std::vector m_SwapChainImages; + uint32_t m_SwapChainImageCount = 0; + + std::vector> m_vStreamedVertexBuffers; + std::vector> m_vStreamedUniformBuffers; + + uint32_t m_CurFrames = 0; + uint32_t m_CurImageIndex = 0; + + uint32_t m_CanvasWidth; + uint32_t m_CanvasHeight; + + SDL_Window *m_pWindow; + + std::array m_aClearColor = {0, 0, 0, 0}; + + struct SRenderCommandExecuteBuffer + { + CCommandBuffer::ECommandBufferCMD m_Command; + const CCommandBuffer::SCommand *m_pRawCommand; + uint32_t m_ThreadIndex; + + // must be calculated when the buffer gets filled + size_t m_EstimatedRenderCallCount = 0; + + // usefull data + VkBuffer m_Buffer; + size_t m_BufferOff; + std::array m_aDescriptors; + + VkBuffer m_IndexBuffer; + + bool m_ClearColorInRenderThread = false; + + bool m_HasDynamicState = false; + VkViewport m_Viewport; + VkRect2D m_Scissor; + }; + + typedef std::vector TCommandList; + typedef std::vector TThreadCommandList; + + TThreadCommandList m_ThreadCommandLists; + std::vector m_ThreadHelperHadCommands; + + typedef std::function TCommandBufferCommandCallback; + typedef std::function TCommandBufferFillExecuteBufferFunc; + + struct SCommandCallback + { + bool m_IsRenderCommand; + TCommandBufferFillExecuteBufferFunc m_FillExecuteBuffer; + TCommandBufferCommandCallback m_CommandCB; + }; + std::array m_aCommandCallbacks; + +protected: + /************************ + * ERROR MANAGMENT + ************************/ + + char m_aError[1024]; + + void SetError(const char *pErr, const char *pErrStrExtra = nullptr) + { + char aError[1024]; + if(pErrStrExtra == nullptr) + str_format(aError, std::size(aError), "%s", pErr); + else + str_format(aError, std::size(aError), "%s: %s", pErr, pErrStrExtra); + dbg_msg("vulkan", "vulkan error: %s", aError); + dbg_assert(false, aError); + } + + void SetWarning(const char *pErr) + { + dbg_msg("vulkan", "vulkan warning: %s", pErr); + } + + const char *CheckVulkanCriticalError(VkResult CallResult) + { + const char *pCriticalError = nullptr; + switch(CallResult) + { + case VK_ERROR_OUT_OF_HOST_MEMORY: + pCriticalError = "host ran out of memory"; + dbg_msg("vulkan", "%s", pCriticalError); + break; + case VK_ERROR_OUT_OF_DEVICE_MEMORY: + pCriticalError = "device ran out of memory"; + dbg_msg("vulkan", "%s", pCriticalError); + break; + case VK_ERROR_DEVICE_LOST: + pCriticalError = "device lost"; + dbg_msg("vulkan", "%s", pCriticalError); + break; + case VK_ERROR_OUT_OF_DATE_KHR: + { + if(IsVerbose()) + { + dbg_msg("vulkan", "queueing swap chain recreation because the current is out of date"); + } + m_RecreateSwapChain = true; + break; + } + case VK_ERROR_SURFACE_LOST_KHR: + dbg_msg("vulkan", "surface lost"); + break; + /*case VK_ERROR_FULL_SCREEN_EXCLUSIVE_MODE_LOST_EXT: + dbg_msg("vulkan", "fullscreen exlusive mode lost"); + break;*/ + case VK_ERROR_INCOMPATIBLE_DRIVER: + pCriticalError = "no compatible driver found. Vulkan 1.1 is required."; + dbg_msg("vulkan", "%s", pCriticalError); + break; + case VK_ERROR_INITIALIZATION_FAILED: + pCriticalError = "initialization failed for unknown reason."; + dbg_msg("vulkan", "%s", pCriticalError); + break; + case VK_ERROR_LAYER_NOT_PRESENT: + SetWarning("One Vulkan layer was not present. (try to disable them)"); + break; + case VK_ERROR_EXTENSION_NOT_PRESENT: + SetWarning("One Vulkan extension was not present. (try to disable them)"); + break; + case VK_ERROR_NATIVE_WINDOW_IN_USE_KHR: + dbg_msg("vulkan", "native window in use"); + break; + case VK_SUCCESS: + break; + case VK_SUBOPTIMAL_KHR: + if(IsVerbose()) + { + dbg_msg("vulkan", "queueing swap chain recreation because the current is sub optimal"); + } + m_RecreateSwapChain = true; + break; + default: + str_format(m_aError, std::size(m_aError), "unknown error %u", (uint32_t)CallResult); + pCriticalError = m_aError; + break; + } + + return pCriticalError; + } + + /************************ + * COMMAND CALLBACKS + ************************/ + + size_t CommandBufferCMDOff(CCommandBuffer::ECommandBufferCMD CommandBufferCMD) + { + return (size_t)CommandBufferCMD - CCommandBuffer::ECommandBufferCMD::CMD_FIRST; + } + + void RegisterCommands() + { + m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_TEXTURE_CREATE)] = {false, [](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) {}, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { Cmd_Texture_Create(static_cast(pBaseCommand)); return true; }}; + m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_TEXTURE_DESTROY)] = {false, [](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) {}, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { Cmd_Texture_Destroy(static_cast(pBaseCommand)); return true; }}; + m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_TEXTURE_UPDATE)] = {false, [](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) {}, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { Cmd_Texture_Update(static_cast(pBaseCommand)); return true; }}; + m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_TEXT_TEXTURES_CREATE)] = {false, [](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) {}, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { Cmd_TextTextures_Create(static_cast(pBaseCommand)); return true; }}; + m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_TEXT_TEXTURES_DESTROY)] = {false, [](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) {}, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { Cmd_TextTextures_Destroy(static_cast(pBaseCommand)); return true; }}; + m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_TEXT_TEXTURE_UPDATE)] = {false, [](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) {}, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { Cmd_TextTexture_Update(static_cast(pBaseCommand)); return true; }}; + + m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_CLEAR)] = {true, [this](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) { Cmd_Clear_FillExecuteBuffer(ExecBuffer, static_cast(pBaseCommand)); }, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { Cmd_Clear(ExecBuffer, static_cast(pBaseCommand)); return true; }}; + m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_RENDER)] = {true, [this](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) { Cmd_Render_FillExecuteBuffer(ExecBuffer, static_cast(pBaseCommand)); }, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { Cmd_Render(static_cast(pBaseCommand), ExecBuffer); return true; }}; + m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_RENDER_TEX3D)] = {true, [this](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) { Cmd_RenderTex3D_FillExecuteBuffer(ExecBuffer, static_cast(pBaseCommand)); }, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { Cmd_RenderTex3D(static_cast(pBaseCommand), ExecBuffer); return true; }}; + + m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_CREATE_BUFFER_OBJECT)] = {false, [](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) {}, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { Cmd_CreateBufferObject(static_cast(pBaseCommand)); return true; }}; + m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_RECREATE_BUFFER_OBJECT)] = {false, [](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) {}, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { Cmd_RecreateBufferObject(static_cast(pBaseCommand)); return true; }}; + m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_UPDATE_BUFFER_OBJECT)] = {false, [](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) {}, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { Cmd_UpdateBufferObject(static_cast(pBaseCommand)); return true; }}; + m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_COPY_BUFFER_OBJECT)] = {false, [](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) {}, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { Cmd_CopyBufferObject(static_cast(pBaseCommand)); return true; }}; + m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_DELETE_BUFFER_OBJECT)] = {false, [](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) {}, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { Cmd_DeleteBufferObject(static_cast(pBaseCommand)); return true; }}; + + m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_CREATE_BUFFER_CONTAINER)] = {false, [](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) {}, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { Cmd_CreateBufferContainer(static_cast(pBaseCommand)); return true; }}; + m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_DELETE_BUFFER_CONTAINER)] = {false, [](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) {}, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { Cmd_DeleteBufferContainer(static_cast(pBaseCommand)); return true; }}; + m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_UPDATE_BUFFER_CONTAINER)] = {false, [](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) {}, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { Cmd_UpdateBufferContainer(static_cast(pBaseCommand)); return true; }}; + + m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_INDICES_REQUIRED_NUM_NOTIFY)] = {false, [](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) {}, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { Cmd_IndicesRequiredNumNotify(static_cast(pBaseCommand)); return true; }}; + + m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_RENDER_TILE_LAYER)] = {true, [this](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) { Cmd_RenderTileLayer_FillExecuteBuffer(ExecBuffer, static_cast(pBaseCommand)); }, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { Cmd_RenderTileLayer(static_cast(pBaseCommand), ExecBuffer); return true; }}; + m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_RENDER_BORDER_TILE)] = {true, [this](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) { Cmd_RenderBorderTile_FillExecuteBuffer(ExecBuffer, static_cast(pBaseCommand)); }, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { Cmd_RenderBorderTile(static_cast(pBaseCommand), ExecBuffer); return true; }}; + m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_RENDER_BORDER_TILE_LINE)] = {true, [this](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) { Cmd_RenderBorderTileLine_FillExecuteBuffer(ExecBuffer, static_cast(pBaseCommand)); }, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { Cmd_RenderBorderTileLine(static_cast(pBaseCommand), ExecBuffer); return true; }}; + m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_RENDER_QUAD_LAYER)] = {true, [this](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) { Cmd_RenderQuadLayer_FillExecuteBuffer(ExecBuffer, static_cast(pBaseCommand)); }, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { Cmd_RenderQuadLayer(static_cast(pBaseCommand), ExecBuffer); return true; }}; + m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_RENDER_TEXT)] = {true, [this](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) { Cmd_RenderText_FillExecuteBuffer(ExecBuffer, static_cast(pBaseCommand)); }, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { Cmd_RenderText(static_cast(pBaseCommand), ExecBuffer); return true; }}; + m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_RENDER_QUAD_CONTAINER)] = {true, [this](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) { Cmd_RenderQuadContainer_FillExecuteBuffer(ExecBuffer, static_cast(pBaseCommand)); }, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { Cmd_RenderQuadContainer(static_cast(pBaseCommand), ExecBuffer); return true; }}; + m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_RENDER_QUAD_CONTAINER_EX)] = {true, [this](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) { Cmd_RenderQuadContainerEx_FillExecuteBuffer(ExecBuffer, static_cast(pBaseCommand)); }, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { Cmd_RenderQuadContainerEx(static_cast(pBaseCommand), ExecBuffer); return true; }}; + m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_RENDER_QUAD_CONTAINER_SPRITE_MULTIPLE)] = {true, [this](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) { Cmd_RenderQuadContainerAsSpriteMultiple_FillExecuteBuffer(ExecBuffer, static_cast(pBaseCommand)); }, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { Cmd_RenderQuadContainerAsSpriteMultiple(static_cast(pBaseCommand), ExecBuffer); return true; }}; + + m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_SWAP)] = {false, [](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) {}, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { Cmd_Swap(static_cast(pBaseCommand)); return true; }}; + m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_FINISH)] = {false, [](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) {}, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { Cmd_Finish(static_cast(pBaseCommand)); return true; }}; + + m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_VSYNC)] = {false, [](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) {}, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { Cmd_VSync(static_cast(pBaseCommand)); return true; }}; + m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_TRY_SWAP_AND_SCREENSHOT)] = {false, [](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) {}, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { Cmd_Screenshot(static_cast(pBaseCommand)); return true; }}; + + m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_UPDATE_VIEWPORT)] = {true, [this](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) { + const auto* pCommand = static_cast(pBaseCommand); + if(pCommand->m_ByResize) { + Cmd_Update_Viewport(pCommand, true); + } + else { + Cmd_Update_Viewport_FillExecuteBuffer(ExecBuffer, pCommand); + } }, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { Cmd_Update_Viewport(static_cast(pBaseCommand), false); return true; }}; + + m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_WINDOW_CREATE_NTF)] = {false, [](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) {}, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { Cmd_WindowCreateNtf(static_cast(pBaseCommand)); return false; }}; + m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_WINDOW_DESTROY_NTF)] = {false, [](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) {}, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { Cmd_WindowDestroyNtf(static_cast(pBaseCommand)); return false; }}; + + for(auto &Callback : m_aCommandCallbacks) + { + if(!(bool)Callback.m_CommandCB) + Callback = {false, [](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) {}, [](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { return true; }}; + } + } + + /***************************** + * VIDEO AND SCREENSHOT HELPER + ******************************/ + + uint8_t *PreparePresentedImageDataImage(uint32_t Width, uint32_t Height) + { + bool NeedsNewImg = Width != m_GetPresentedImgDataHelperWidth || Height != m_GetPresentedImgDataHelperHeight; + if(m_GetPresentedImgDataHelperImage == VK_NULL_HANDLE || NeedsNewImg) + { + if(m_GetPresentedImgDataHelperImage != VK_NULL_HANDLE) + { + DeletePresentedImageDataImage(); + } + m_GetPresentedImgDataHelperWidth = Width; + m_GetPresentedImgDataHelperHeight = Height; + + VkImageCreateInfo ImageInfo{}; + ImageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + ImageInfo.imageType = VK_IMAGE_TYPE_2D; + ImageInfo.extent.width = Width; + ImageInfo.extent.height = Height; + ImageInfo.extent.depth = 1; + ImageInfo.mipLevels = 1; + ImageInfo.arrayLayers = 1; + ImageInfo.format = VK_FORMAT_R8G8B8A8_UNORM; + ImageInfo.tiling = VK_IMAGE_TILING_LINEAR; + ImageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + ImageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT; + ImageInfo.samples = VK_SAMPLE_COUNT_1_BIT; + ImageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + + vkCreateImage(m_VKDevice, &ImageInfo, nullptr, &m_GetPresentedImgDataHelperImage); + // Create memory to back up the image + VkMemoryRequirements MemRequirements; + vkGetImageMemoryRequirements(m_VKDevice, m_GetPresentedImgDataHelperImage, &MemRequirements); + + VkMemoryAllocateInfo MemAllocInfo{}; + MemAllocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + MemAllocInfo.allocationSize = MemRequirements.size; + MemAllocInfo.memoryTypeIndex = FindMemoryType(m_VKGPU, MemRequirements.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_CACHED_BIT); + + vkAllocateMemory(m_VKDevice, &MemAllocInfo, nullptr, &m_GetPresentedImgDataHelperMem.m_Mem); + vkBindImageMemory(m_VKDevice, m_GetPresentedImgDataHelperImage, m_GetPresentedImgDataHelperMem.m_Mem, 0); + + ImageBarrier(m_GetPresentedImgDataHelperImage, 0, 1, 0, 1, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL); + + VkImageSubresource SubResource{VK_IMAGE_ASPECT_COLOR_BIT, 0, 0}; + VkSubresourceLayout SubResourceLayout; + vkGetImageSubresourceLayout(m_VKDevice, m_GetPresentedImgDataHelperImage, &SubResource, &SubResourceLayout); + + vkMapMemory(m_VKDevice, m_GetPresentedImgDataHelperMem.m_Mem, 0, VK_WHOLE_SIZE, 0, (void **)&m_pGetPresentedImgDataHelperMappedMemory); + m_GetPresentedImgDataHelperMappedLayoutOffset = SubResourceLayout.offset; + m_pGetPresentedImgDataHelperMappedMemory += m_GetPresentedImgDataHelperMappedLayoutOffset; + + VkFenceCreateInfo FenceInfo{}; + FenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; + FenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT; + vkCreateFence(m_VKDevice, &FenceInfo, nullptr, &m_GetPresentedImgDataHelperFence); + } + return m_pGetPresentedImgDataHelperMappedMemory; + } + + void DeletePresentedImageDataImage() + { + if(m_GetPresentedImgDataHelperImage != VK_NULL_HANDLE) + { + vkDestroyFence(m_VKDevice, m_GetPresentedImgDataHelperFence, nullptr); + + m_GetPresentedImgDataHelperFence = VK_NULL_HANDLE; + + vkDestroyImage(m_VKDevice, m_GetPresentedImgDataHelperImage, nullptr); + vkUnmapMemory(m_VKDevice, m_GetPresentedImgDataHelperMem.m_Mem); + vkFreeMemory(m_VKDevice, m_GetPresentedImgDataHelperMem.m_Mem, nullptr); + + m_GetPresentedImgDataHelperImage = VK_NULL_HANDLE; + m_GetPresentedImgDataHelperMem = {}; + m_pGetPresentedImgDataHelperMappedMemory = nullptr; + + m_GetPresentedImgDataHelperWidth = 0; + m_GetPresentedImgDataHelperHeight = 0; + } + } + + bool GetPresentedImageDataImpl(uint32_t &Width, uint32_t &Height, uint32_t &Format, std::vector &DstData, bool FlipImgData, bool ResetAlpha) + { + bool IsB8G8R8A8 = m_VKSurfFormat.format == VK_FORMAT_B8G8R8A8_UNORM; + bool UsesRGBALikeFormat = m_VKSurfFormat.format == VK_FORMAT_R8G8B8A8_UNORM || IsB8G8R8A8; + if(UsesRGBALikeFormat && m_LastPresentedSwapChainImageIndex != std::numeric_limits::max()) + { + Width = m_VKSwapImgAndViewportExtent.m_Viewport.width; + Height = m_VKSwapImgAndViewportExtent.m_Viewport.height; + Format = CImageInfo::FORMAT_RGBA; + + size_t ImageTotalSize = (size_t)Width * Height * 4; + if(DstData.size() < ImageTotalSize + (Width * 4)) + DstData.resize(ImageTotalSize + (Width * 4)); // extra space for flipping + + PreparePresentedImageDataImage(Width, Height); + + VkCommandBuffer CommandBuffer = GetMemoryCommandBuffer(); + + VkBufferImageCopy Region{}; + Region.bufferOffset = 0; + Region.bufferRowLength = 0; + Region.bufferImageHeight = 0; + Region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + Region.imageSubresource.mipLevel = 0; + Region.imageSubresource.baseArrayLayer = 0; + Region.imageSubresource.layerCount = 1; + Region.imageOffset = {0, 0, 0}; + Region.imageExtent = {m_VKSwapImgAndViewportExtent.m_Viewport.width, m_VKSwapImgAndViewportExtent.m_Viewport.height, 1}; + + auto &SwapImg = m_SwapChainImages[m_LastPresentedSwapChainImageIndex]; + + ImageBarrier(m_GetPresentedImgDataHelperImage, 0, 1, 0, 1, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + ImageBarrier(SwapImg, 0, 1, 0, 1, m_VKSurfFormat.format, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + + // If source and destination support blit we'll blit as this also does automatic format conversion (e.g. from BGR to RGB) + if(m_OptimalSwapChainImageBlitting && m_LinearRGBAImageBlitting) + { + VkOffset3D BlitSize; + BlitSize.x = Width; + BlitSize.y = Height; + BlitSize.z = 1; + VkImageBlit ImageBlitRegion{}; + ImageBlitRegion.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + ImageBlitRegion.srcSubresource.layerCount = 1; + ImageBlitRegion.srcOffsets[1] = BlitSize; + ImageBlitRegion.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + ImageBlitRegion.dstSubresource.layerCount = 1; + ImageBlitRegion.dstOffsets[1] = BlitSize; + + // Issue the blit command + vkCmdBlitImage(CommandBuffer, SwapImg, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + m_GetPresentedImgDataHelperImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + 1, &ImageBlitRegion, VK_FILTER_NEAREST); + + // transformed to RGBA + IsB8G8R8A8 = false; + } + else + { + // Otherwise use image copy (requires us to manually flip components) + VkImageCopy ImageCopyRegion{}; + ImageCopyRegion.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + ImageCopyRegion.srcSubresource.layerCount = 1; + ImageCopyRegion.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + ImageCopyRegion.dstSubresource.layerCount = 1; + ImageCopyRegion.extent.width = Width; + ImageCopyRegion.extent.height = Height; + ImageCopyRegion.extent.depth = 1; + + // Issue the copy command + vkCmdCopyImage(CommandBuffer, SwapImg, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + m_GetPresentedImgDataHelperImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + 1, &ImageCopyRegion); + } + + ImageBarrier(m_GetPresentedImgDataHelperImage, 0, 1, 0, 1, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL); + ImageBarrier(SwapImg, 0, 1, 0, 1, m_VKSurfFormat.format, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); + + vkEndCommandBuffer(CommandBuffer); + m_UsedMemoryCommandBuffer[m_CurImageIndex] = false; + + VkSubmitInfo SubmitInfo{}; + SubmitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + + SubmitInfo.commandBufferCount = 1; + SubmitInfo.pCommandBuffers = &CommandBuffer; + + vkResetFences(m_VKDevice, 1, &m_GetPresentedImgDataHelperFence); + vkQueueSubmit(m_VKGraphicsQueue, 1, &SubmitInfo, m_GetPresentedImgDataHelperFence); + vkWaitForFences(m_VKDevice, 1, &m_GetPresentedImgDataHelperFence, VK_TRUE, std::numeric_limits::max()); + + VkMappedMemoryRange MemRange{}; + MemRange.sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE; + MemRange.memory = m_GetPresentedImgDataHelperMem.m_Mem; + MemRange.offset = m_GetPresentedImgDataHelperMappedLayoutOffset; + MemRange.size = VK_WHOLE_SIZE; + vkInvalidateMappedMemoryRanges(m_VKDevice, 1, &MemRange); + + mem_copy(DstData.data(), m_pGetPresentedImgDataHelperMappedMemory, ImageTotalSize); + + if(IsB8G8R8A8 || ResetAlpha) + { + // swizzle + for(uint32_t Y = 0; Y < Height; ++Y) + { + for(uint32_t X = 0; X < Width; ++X) + { + size_t ImgOff = (Y * Width * 4) + (X * 4); + if(IsB8G8R8A8) + { + std::swap(DstData[ImgOff], DstData[ImgOff + 2]); + } + DstData[ImgOff + 3] = 255; + } + } + } + + if(FlipImgData) + { + uint8_t *pTempRow = DstData.data() + Width * Height * 4; + for(uint32_t Y = 0; Y < Height / 2; ++Y) + { + mem_copy(pTempRow, DstData.data() + Y * Width * 4, Width * 4); + mem_copy(DstData.data() + Y * Width * 4, DstData.data() + ((Height - Y) - 1) * Width * 4, Width * 4); + mem_copy(DstData.data() + ((Height - Y) - 1) * Width * 4, pTempRow, Width * 4); + } + } + + return true; + } + else + { + if(!UsesRGBALikeFormat) + { + dbg_msg("vulkan", "swap chain image was not in a RGBA like format."); + } + else + { + dbg_msg("vulkan", "swap chain image was not ready to be copied."); + } + return false; + } + } + + bool GetPresentedImageData(uint32_t &Width, uint32_t &Height, uint32_t &Format, std::vector &DstData) override + { + return GetPresentedImageDataImpl(Width, Height, Format, DstData, false, false); + } + + /************************ + * MEMORY MANAGMENT + ************************/ + + bool AllocateVulkanMemory(const VkMemoryAllocateInfo *pAllocateInfo, VkDeviceMemory *pMemory) + { + VkResult Res = vkAllocateMemory(m_VKDevice, pAllocateInfo, nullptr, pMemory); + if(Res != VK_SUCCESS) + { + dbg_msg("vulkan", "vulkan memory allocation failed, trying to recover."); + if(Res == VK_ERROR_OUT_OF_HOST_MEMORY || Res == VK_ERROR_OUT_OF_DEVICE_MEMORY) + { + // aggressivly try to get more memory + vkDeviceWaitIdle(m_VKDevice); + for(size_t i = 0; i < m_SwapChainImageCount + 1; ++i) + NextFrame(); + Res = vkAllocateMemory(m_VKDevice, pAllocateInfo, nullptr, pMemory); + } + if(Res != VK_SUCCESS) + { + dbg_msg("vulkan", "vulkan memory allocation failed."); + return false; + } + } + return true; + } + + void GetBufferImpl(VkDeviceSize RequiredSize, EMemoryBlockUsage MemUsage, VkBuffer &Buffer, SDeviceMemoryBlock &BufferMemory, VkBufferUsageFlags BufferUsage, VkMemoryPropertyFlags BufferProperties) + { + CreateBuffer(RequiredSize, MemUsage, BufferUsage, BufferProperties, Buffer, BufferMemory); + } + + template + SMemoryBlock GetBufferBlockImpl(SMemoryBlockCache &MemoryCache, VkBufferUsageFlags BufferUsage, VkMemoryPropertyFlags BufferProperties, const void *pBufferData, VkDeviceSize RequiredSize, VkDeviceSize TargetAlignment) + { + SMemoryBlock RetBlock; + + auto &&CreateCacheBlock = [&]() { + bool FoundAllocation = false; + SMemoryHeap::SMemoryHeapQueueElement AllocatedMem; + SDeviceMemoryBlock TmpBufferMemory; + typename SMemoryBlockCache::SMemoryCacheType::SMemoryCacheHeap *pCacheHeap = nullptr; + auto &Heaps = MemoryCache.m_MemoryCaches.m_MemoryHeaps; + for(size_t i = 0; i < Heaps.size(); ++i) + { + auto *pHeap = Heaps[i]; + if(pHeap->m_Heap.Allocate(RequiredSize, TargetAlignment, AllocatedMem)) + { + TmpBufferMemory = pHeap->m_BufferMem; + FoundAllocation = true; + pCacheHeap = pHeap; + break; + } + } + if(!FoundAllocation) + { + typename SMemoryBlockCache::SMemoryCacheType::SMemoryCacheHeap *pNewHeap = new typename SMemoryBlockCache::SMemoryCacheType::SMemoryCacheHeap(); + + VkBuffer TmpBuffer; + GetBufferImpl(MemoryBlockSize * BlockCount, RequiresMapping ? MEMORY_BLOCK_USAGE_STAGING : MEMORY_BLOCK_USAGE_BUFFER, TmpBuffer, TmpBufferMemory, BufferUsage, BufferProperties); + + void *pMapData = nullptr; + + if(RequiresMapping) + { + if(vkMapMemory(m_VKDevice, TmpBufferMemory.m_Mem, 0, VK_WHOLE_SIZE, 0, &pMapData) != VK_SUCCESS) + { + SetError("Failed to map buffer block memory."); + } + } + + pNewHeap->m_Buffer = TmpBuffer; + + pNewHeap->m_BufferMem = TmpBufferMemory; + pNewHeap->m_pMappedBuffer = pMapData; + + pCacheHeap = pNewHeap; + Heaps.emplace_back(pNewHeap); + Heaps.back()->m_Heap.Init(MemoryBlockSize * BlockCount, 0); + if(!Heaps.back()->m_Heap.Allocate(RequiredSize, TargetAlignment, AllocatedMem)) + { + dbg_assert(false, "Heap allocation failed directly after creating fresh heap"); + } + } + + RetBlock.m_Buffer = pCacheHeap->m_Buffer; + RetBlock.m_BufferMem = TmpBufferMemory; + if(RequiresMapping) + RetBlock.m_pMappedBuffer = ((uint8_t *)pCacheHeap->m_pMappedBuffer) + AllocatedMem.m_OffsetToAlign; + else + RetBlock.m_pMappedBuffer = nullptr; + RetBlock.m_IsCached = true; + RetBlock.m_pHeap = &pCacheHeap->m_Heap; + RetBlock.m_HeapData = AllocatedMem; + RetBlock.m_UsedSize = RequiredSize; + + if(RequiresMapping) + mem_copy(RetBlock.m_pMappedBuffer, pBufferData, RequiredSize); + }; + + if(RequiredSize < (VkDeviceSize)MemoryBlockSize) + { + CreateCacheBlock(); + } + else + { + VkBuffer TmpBuffer; + SDeviceMemoryBlock TmpBufferMemory; + GetBufferImpl(RequiredSize, RequiresMapping ? MEMORY_BLOCK_USAGE_STAGING : MEMORY_BLOCK_USAGE_BUFFER, TmpBuffer, TmpBufferMemory, BufferUsage, BufferProperties); + + void *pMapData = nullptr; + if(RequiresMapping) + { + vkMapMemory(m_VKDevice, TmpBufferMemory.m_Mem, 0, VK_WHOLE_SIZE, 0, &pMapData); + mem_copy(pMapData, pBufferData, static_cast(RequiredSize)); + } + + RetBlock.m_Buffer = TmpBuffer; + RetBlock.m_BufferMem = TmpBufferMemory; + RetBlock.m_pMappedBuffer = pMapData; + RetBlock.m_pHeap = nullptr; + RetBlock.m_IsCached = false; + RetBlock.m_HeapData.m_OffsetToAlign = 0; + RetBlock.m_HeapData.m_AllocationSize = RequiredSize; + RetBlock.m_UsedSize = RequiredSize; + } + + return RetBlock; + } + + SMemoryBlock GetStagingBuffer(const void *pBufferData, VkDeviceSize RequiredSize) + { + return GetBufferBlockImpl(m_StagingBufferCache, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_CACHED_BIT, pBufferData, RequiredSize, maximum(m_NonCoherentMemAlignment, 16)); + } + + SMemoryBlock GetStagingBufferImage(const void *pBufferData, VkDeviceSize RequiredSize) + { + return GetBufferBlockImpl(m_StagingBufferCacheImage, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_CACHED_BIT, pBufferData, RequiredSize, maximum(m_OptimalImageCopyMemAlignment, maximum(m_NonCoherentMemAlignment, 16))); + } + + template + void PrepareStagingMemRange(SMemoryBlock &Block) + { + VkMappedMemoryRange UploadRange{}; + UploadRange.sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE; + UploadRange.memory = Block.m_BufferMem.m_Mem; + UploadRange.offset = Block.m_HeapData.m_OffsetToAlign; + + auto AlignmentMod = ((VkDeviceSize)Block.m_HeapData.m_AllocationSize % m_NonCoherentMemAlignment); + auto AlignmentReq = (m_NonCoherentMemAlignment - AlignmentMod); + if(AlignmentMod == 0) + AlignmentReq = 0; + UploadRange.size = Block.m_HeapData.m_AllocationSize + AlignmentReq; + + if(UploadRange.offset + UploadRange.size > Block.m_BufferMem.m_Size) + UploadRange.size = VK_WHOLE_SIZE; + + m_NonFlushedStagingBufferRange.push_back(UploadRange); + } + + void UploadAndFreeStagingMemBlock(SMemoryBlock &Block) + { + PrepareStagingMemRange(Block); + if(!Block.m_IsCached) + { + m_FrameDelayedBufferCleanup[m_CurImageIndex].push_back({Block.m_Buffer, Block.m_BufferMem, Block.m_pMappedBuffer}); + } + else + { + m_StagingBufferCache.FreeMemBlock(Block, m_CurImageIndex); + } + } + + void UploadAndFreeStagingImageMemBlock(SMemoryBlock &Block) + { + PrepareStagingMemRange(Block); + if(!Block.m_IsCached) + { + m_FrameDelayedBufferCleanup[m_CurImageIndex].push_back({Block.m_Buffer, Block.m_BufferMem, Block.m_pMappedBuffer}); + } + else + { + m_StagingBufferCacheImage.FreeMemBlock(Block, m_CurImageIndex); + } + } + + SMemoryBlock GetVertexBuffer(VkDeviceSize RequiredSize) + { + return GetBufferBlockImpl(m_VertexBufferCache, VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, nullptr, RequiredSize, 16); + } + + void FreeVertexMemBlock(SMemoryBlock &Block) + { + if(!Block.m_IsCached) + { + m_FrameDelayedBufferCleanup[m_CurImageIndex].push_back({Block.m_Buffer, Block.m_BufferMem, nullptr}); + } + else + { + m_VertexBufferCache.FreeMemBlock(Block, m_CurImageIndex); + } + } + + static size_t ImageMipLevelCount(size_t Width, size_t Height, size_t Depth) + { + return floor(log2(maximum(Width, maximum(Height, Depth)))) + 1; + } + + static size_t ImageMipLevelCount(VkExtent3D &ImgExtent) + { + return ImageMipLevelCount(ImgExtent.width, ImgExtent.height, ImgExtent.depth); + } + + // good approximation of 1024x1024 image with mipmaps + static constexpr int64_t s_1024x1024ImgSize = (1024 * 1024 * 4) * 2; + + bool GetImageMemoryImpl(VkDeviceSize RequiredSize, uint32_t RequiredMemoryTypeBits, SDeviceMemoryBlock &BufferMemory, VkMemoryPropertyFlags BufferProperties) + { + VkMemoryAllocateInfo MemAllocInfo{}; + MemAllocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + MemAllocInfo.allocationSize = RequiredSize; + MemAllocInfo.memoryTypeIndex = FindMemoryType(m_VKGPU, RequiredMemoryTypeBits, BufferProperties); + + BufferMemory.m_Size = RequiredSize; + m_pTextureMemoryUsage->store(m_pTextureMemoryUsage->load(std::memory_order_relaxed) + RequiredSize, std::memory_order_relaxed); + + if(IsVerbose()) + { + VerboseAllocatedMemory(RequiredSize, m_CurImageIndex, MEMORY_BLOCK_USAGE_TEXTURE); + } + + if(!AllocateVulkanMemory(&MemAllocInfo, &BufferMemory.m_Mem)) + { + SetError("Allocation for image memory failed."); + return false; + } + + BufferMemory.m_UsageType = MEMORY_BLOCK_USAGE_TEXTURE; + + return true; + } + + template + SMemoryImageBlock GetImageMemoryBlockImpl(SMemoryBlockCache &MemoryCache, VkMemoryPropertyFlags BufferProperties, VkDeviceSize RequiredSize, VkDeviceSize RequiredAlignment, uint32_t RequiredMemoryTypeBits) + { + SMemoryImageBlock RetBlock; + + auto &&CreateCacheBlock = [&]() { + bool FoundAllocation = false; + SMemoryHeap::SMemoryHeapQueueElement AllocatedMem; + SDeviceMemoryBlock TmpBufferMemory; + typename SMemoryBlockCache::SMemoryCacheType::SMemoryCacheHeap *pCacheHeap = nullptr; + for(size_t i = 0; i < MemoryCache.m_MemoryCaches.m_MemoryHeaps.size(); ++i) + { + auto *pHeap = MemoryCache.m_MemoryCaches.m_MemoryHeaps[i]; + if(pHeap->m_Heap.Allocate(RequiredSize, RequiredAlignment, AllocatedMem)) + { + TmpBufferMemory = pHeap->m_BufferMem; + FoundAllocation = true; + pCacheHeap = pHeap; + break; + } + } + if(!FoundAllocation) + { + typename SMemoryBlockCache::SMemoryCacheType::SMemoryCacheHeap *pNewHeap = new typename SMemoryBlockCache::SMemoryCacheType::SMemoryCacheHeap(); + + GetImageMemoryImpl(MemoryBlockSize * BlockCount, RequiredMemoryTypeBits, TmpBufferMemory, BufferProperties); + + pNewHeap->m_Buffer = VK_NULL_HANDLE; + + pNewHeap->m_BufferMem = TmpBufferMemory; + pNewHeap->m_pMappedBuffer = nullptr; + + auto &Heaps = MemoryCache.m_MemoryCaches.m_MemoryHeaps; + pCacheHeap = pNewHeap; + Heaps.emplace_back(pNewHeap); + Heaps.back()->m_Heap.Init(MemoryBlockSize * BlockCount, 0); + if(!Heaps.back()->m_Heap.Allocate(RequiredSize, RequiredAlignment, AllocatedMem)) + { + dbg_assert(false, "Heap allocation failed directly after creating fresh heap for image"); + } + } + + RetBlock.m_Buffer = VK_NULL_HANDLE; + RetBlock.m_BufferMem = TmpBufferMemory; + RetBlock.m_pMappedBuffer = nullptr; + RetBlock.m_IsCached = true; + RetBlock.m_pHeap = &pCacheHeap->m_Heap; + RetBlock.m_HeapData = AllocatedMem; + RetBlock.m_UsedSize = RequiredSize; + }; + + if(RequiredSize < (VkDeviceSize)MemoryBlockSize) + { + CreateCacheBlock(); + } + else + { + SDeviceMemoryBlock TmpBufferMemory; + GetImageMemoryImpl(RequiredSize, RequiredMemoryTypeBits, TmpBufferMemory, BufferProperties); + + RetBlock.m_Buffer = VK_NULL_HANDLE; + RetBlock.m_BufferMem = TmpBufferMemory; + RetBlock.m_pMappedBuffer = nullptr; + RetBlock.m_IsCached = false; + RetBlock.m_pHeap = nullptr; + RetBlock.m_HeapData.m_OffsetToAlign = 0; + RetBlock.m_HeapData.m_AllocationSize = RequiredSize; + RetBlock.m_UsedSize = RequiredSize; + } + + RetBlock.m_ImageMemoryBits = RequiredMemoryTypeBits; + + return RetBlock; + } + + SMemoryImageBlock GetImageMemory(VkDeviceSize RequiredSize, VkDeviceSize RequiredAlignment, uint32_t RequiredMemoryTypeBits) + { + auto it = m_ImageBufferCaches.find(RequiredMemoryTypeBits); + if(it == m_ImageBufferCaches.end()) + { + it = m_ImageBufferCaches.insert({RequiredMemoryTypeBits, {}}).first; + + it->second.Init(m_SwapChainImageCount); + } + return GetImageMemoryBlockImpl(it->second, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, RequiredSize, RequiredAlignment, RequiredMemoryTypeBits); + } + + void FreeImageMemBlock(SMemoryImageBlock &Block) + { + if(!Block.m_IsCached) + { + m_FrameDelayedBufferCleanup[m_CurImageIndex].push_back({Block.m_Buffer, Block.m_BufferMem, nullptr}); + } + else + { + m_ImageBufferCaches[Block.m_ImageMemoryBits].FreeMemBlock(Block, m_CurImageIndex); + } + } + + template + void UploadStreamedBuffer(SStreamMemory &StreamedBuffer) + { + size_t RangeUpdateCount = 0; + if(StreamedBuffer.IsUsed(m_CurImageIndex)) + { + for(size_t i = 0; i < StreamedBuffer.GetUsedCount(m_CurImageIndex); ++i) + { + auto &BufferOfFrame = StreamedBuffer.GetBuffers(m_CurImageIndex)[i]; + auto &MemRange = StreamedBuffer.GetRanges(m_CurImageIndex)[RangeUpdateCount++]; + MemRange.sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE; + MemRange.memory = BufferOfFrame.m_BufferMem.m_Mem; + MemRange.offset = BufferOfFrame.m_OffsetInBuffer; + auto AlignmentMod = ((VkDeviceSize)BufferOfFrame.m_UsedSize % m_NonCoherentMemAlignment); + auto AlignmentReq = (m_NonCoherentMemAlignment - AlignmentMod); + if(AlignmentMod == 0) + AlignmentReq = 0; + MemRange.size = BufferOfFrame.m_UsedSize + AlignmentReq; + + if(MemRange.offset + MemRange.size > BufferOfFrame.m_BufferMem.m_Size) + MemRange.size = VK_WHOLE_SIZE; + + BufferOfFrame.m_UsedSize = 0; + } + if(RangeUpdateCount > 0 && FlushForRendering) + { + vkFlushMappedMemoryRanges(m_VKDevice, RangeUpdateCount, StreamedBuffer.GetRanges(m_CurImageIndex).data()); + } + } + StreamedBuffer.ResetFrame(m_CurImageIndex); + } + + void CleanBufferPair(size_t ImageIndex, VkBuffer &Buffer, SDeviceMemoryBlock &BufferMem) + { + bool IsBuffer = Buffer != VK_NULL_HANDLE; + if(IsBuffer) + { + vkDestroyBuffer(m_VKDevice, Buffer, nullptr); + + Buffer = VK_NULL_HANDLE; + } + if(BufferMem.m_Mem != VK_NULL_HANDLE) + { + vkFreeMemory(m_VKDevice, BufferMem.m_Mem, nullptr); + if(BufferMem.m_UsageType == MEMORY_BLOCK_USAGE_BUFFER) + m_pBufferMemoryUsage->store(m_pBufferMemoryUsage->load(std::memory_order_relaxed) - BufferMem.m_Size, std::memory_order_relaxed); + else if(BufferMem.m_UsageType == MEMORY_BLOCK_USAGE_TEXTURE) + m_pTextureMemoryUsage->store(m_pTextureMemoryUsage->load(std::memory_order_relaxed) - BufferMem.m_Size, std::memory_order_relaxed); + else if(BufferMem.m_UsageType == MEMORY_BLOCK_USAGE_STREAM) + m_pStreamMemoryUsage->store(m_pStreamMemoryUsage->load(std::memory_order_relaxed) - BufferMem.m_Size, std::memory_order_relaxed); + else if(BufferMem.m_UsageType == MEMORY_BLOCK_USAGE_STAGING) + m_pStagingMemoryUsage->store(m_pStagingMemoryUsage->load(std::memory_order_relaxed) - BufferMem.m_Size, std::memory_order_relaxed); + + if(IsVerbose()) + { + VerboseDeallocatedMemory(BufferMem.m_Size, (size_t)ImageIndex, BufferMem.m_UsageType); + } + + BufferMem.m_Mem = VK_NULL_HANDLE; + } + } + + void DestroyTexture(CTexture &Texture) + { + if(Texture.m_Img != VK_NULL_HANDLE) + { + FreeImageMemBlock(Texture.m_ImgMem); + vkDestroyImage(m_VKDevice, Texture.m_Img, nullptr); + + vkDestroyImageView(m_VKDevice, Texture.m_ImgView, nullptr); + } + + if(Texture.m_Img3D != VK_NULL_HANDLE) + { + FreeImageMemBlock(Texture.m_Img3DMem); + vkDestroyImage(m_VKDevice, Texture.m_Img3D, nullptr); + + vkDestroyImageView(m_VKDevice, Texture.m_Img3DView, nullptr); + } + + DestroyTexturedStandardDescriptorSets(Texture, 0); + DestroyTexturedStandardDescriptorSets(Texture, 1); + + DestroyTextured3DStandardDescriptorSets(Texture); + } + + void DestroyTextTexture(CTexture &Texture, CTexture &TextureOutline) + { + if(Texture.m_Img != VK_NULL_HANDLE) + { + FreeImageMemBlock(Texture.m_ImgMem); + vkDestroyImage(m_VKDevice, Texture.m_Img, nullptr); + + vkDestroyImageView(m_VKDevice, Texture.m_ImgView, nullptr); + } + + if(TextureOutline.m_Img != VK_NULL_HANDLE) + { + FreeImageMemBlock(TextureOutline.m_ImgMem); + vkDestroyImage(m_VKDevice, TextureOutline.m_Img, nullptr); + + vkDestroyImageView(m_VKDevice, Texture.m_ImgView, nullptr); + } + + DestroyTextDescriptorSets(Texture, TextureOutline); + } + + void ClearFrameData(size_t FrameImageIndex) + { + UploadStagingBuffers(); + + // clear pending buffers, that require deletion + for(auto &BufferPair : m_FrameDelayedBufferCleanup[FrameImageIndex]) + { + if(BufferPair.m_pMappedData != nullptr) + { + vkUnmapMemory(m_VKDevice, BufferPair.m_Mem.m_Mem); + } + CleanBufferPair(FrameImageIndex, BufferPair.m_Buffer, BufferPair.m_Mem); + } + m_FrameDelayedBufferCleanup[FrameImageIndex].clear(); + + // clear pending textures, that require deletion + for(auto &Texture : m_FrameDelayedTextureCleanup[FrameImageIndex]) + { + DestroyTexture(Texture); + } + m_FrameDelayedTextureCleanup[FrameImageIndex].clear(); + + for(auto &TexturePair : m_FrameDelayedTextTexturesCleanup[FrameImageIndex]) + { + DestroyTextTexture(TexturePair.first, TexturePair.second); + } + m_FrameDelayedTextTexturesCleanup[FrameImageIndex].clear(); + + m_StagingBufferCache.Cleanup(FrameImageIndex); + m_StagingBufferCacheImage.Cleanup(FrameImageIndex); + m_VertexBufferCache.Cleanup(FrameImageIndex); + for(auto &ImageBufferCache : m_ImageBufferCaches) + ImageBufferCache.second.Cleanup(FrameImageIndex); + } + + void ShrinkUnusedCaches() + { + size_t FreeedMemory = 0; + FreeedMemory += m_StagingBufferCache.Shrink(m_VKDevice); + FreeedMemory += m_StagingBufferCacheImage.Shrink(m_VKDevice); + if(FreeedMemory > 0) + { + m_pStagingMemoryUsage->store(m_pStagingMemoryUsage->load(std::memory_order_relaxed) - FreeedMemory, std::memory_order_relaxed); + if(IsVerbose()) + { + dbg_msg("vulkan", "deallocated chunks of memory with size: %zu from all frames (staging buffer)", (size_t)FreeedMemory); + } + } + FreeedMemory = 0; + FreeedMemory += m_VertexBufferCache.Shrink(m_VKDevice); + if(FreeedMemory > 0) + { + m_pBufferMemoryUsage->store(m_pBufferMemoryUsage->load(std::memory_order_relaxed) - FreeedMemory, std::memory_order_relaxed); + if(IsVerbose()) + { + dbg_msg("vulkan", "deallocated chunks of memory with size: %zu from all frames (buffer)", (size_t)FreeedMemory); + } + } + FreeedMemory = 0; + for(auto &ImageBufferCache : m_ImageBufferCaches) + FreeedMemory += ImageBufferCache.second.Shrink(m_VKDevice); + if(FreeedMemory > 0) + { + m_pTextureMemoryUsage->store(m_pTextureMemoryUsage->load(std::memory_order_relaxed) - FreeedMemory, std::memory_order_relaxed); + if(IsVerbose()) + { + dbg_msg("vulkan", "deallocated chunks of memory with size: %zu from all frames (texture)", (size_t)FreeedMemory); + } + } + } + + void MemoryBarrier(VkBuffer Buffer, VkDeviceSize Offset, VkDeviceSize Size, VkAccessFlags BufferAccessType, bool BeforeCommand) + { + VkCommandBuffer MemCommandBuffer = GetMemoryCommandBuffer(); + + VkBufferMemoryBarrier Barrier{}; + Barrier.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER; + Barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + Barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + Barrier.buffer = Buffer; + Barrier.offset = Offset; + Barrier.size = Size; + + VkPipelineStageFlags SourceStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + VkPipelineStageFlags DestinationStage = VK_PIPELINE_STAGE_TRANSFER_BIT; + + if(BeforeCommand) + { + Barrier.srcAccessMask = BufferAccessType; + Barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + + SourceStage = VK_PIPELINE_STAGE_VERTEX_INPUT_BIT; + DestinationStage = VK_PIPELINE_STAGE_TRANSFER_BIT; + } + else + { + Barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + Barrier.dstAccessMask = BufferAccessType; + + SourceStage = VK_PIPELINE_STAGE_TRANSFER_BIT; + DestinationStage = VK_PIPELINE_STAGE_VERTEX_INPUT_BIT; + } + + vkCmdPipelineBarrier( + MemCommandBuffer, + SourceStage, DestinationStage, + 0, + 0, nullptr, + 1, &Barrier, + 0, nullptr); + } + + /************************ + * SWAPPING MECHANISM + ************************/ + + void StartRenderThread(size_t ThreadIndex) + { + auto &List = m_ThreadCommandLists[ThreadIndex]; + if(!List.empty()) + { + m_ThreadHelperHadCommands[ThreadIndex] = true; + auto *pThread = m_RenderThreads[ThreadIndex].get(); + std::unique_lock Lock(pThread->m_Mutex); + pThread->m_IsRendering = true; + pThread->m_Cond.notify_one(); + } + } + + void FinishRenderThreads() + { + if(m_ThreadCount > 1) + { + // execute threads + + for(size_t ThreadIndex = 0; ThreadIndex < m_ThreadCount - 1; ++ThreadIndex) + { + if(!m_ThreadHelperHadCommands[ThreadIndex]) + { + StartRenderThread(ThreadIndex); + } + } + + for(size_t ThreadIndex = 0; ThreadIndex < m_ThreadCount - 1; ++ThreadIndex) + { + if(m_ThreadHelperHadCommands[ThreadIndex]) + { + auto &pRenderThread = m_RenderThreads[ThreadIndex]; + m_ThreadHelperHadCommands[ThreadIndex] = false; + std::unique_lock Lock(pRenderThread->m_Mutex); + pRenderThread->m_Cond.wait(Lock, [&pRenderThread] { return !pRenderThread->m_IsRendering; }); + m_vLastPipeline[ThreadIndex + 1] = VK_NULL_HANDLE; + } + } + } + } + + void ExecuteMemoryCommandBuffer() + { + if(m_UsedMemoryCommandBuffer[m_CurImageIndex]) + { + auto &MemoryCommandBuffer = m_MemoryCommandBuffers[m_CurImageIndex]; + vkEndCommandBuffer(MemoryCommandBuffer); + + VkSubmitInfo SubmitInfo{}; + SubmitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + + SubmitInfo.commandBufferCount = 1; + SubmitInfo.pCommandBuffers = &MemoryCommandBuffer; + vkQueueSubmit(m_VKGraphicsQueue, 1, &SubmitInfo, VK_NULL_HANDLE); + vkQueueWaitIdle(m_VKGraphicsQueue); + + m_UsedMemoryCommandBuffer[m_CurImageIndex] = false; + } + } + + void ClearFrameMemoryUsage() + { + ClearFrameData(m_CurImageIndex); + ShrinkUnusedCaches(); + } + + void WaitFrame() + { + FinishRenderThreads(); + m_LastCommandsInPipeThreadIndex = 0; + + UploadNonFlushedBuffers(); + + auto &CommandBuffer = GetMainGraphicCommandBuffer(); + + // render threads + if(m_ThreadCount > 1) + { + size_t ThreadedCommandsUsedCount = 0; + size_t RenderThreadCount = m_ThreadCount - 1; + for(size_t i = 0; i < RenderThreadCount; ++i) + { + if(m_UsedThreadDrawCommandBuffer[i + 1][m_CurImageIndex]) + { + auto &GraphicThreadCommandBuffer = m_ThreadDrawCommandBuffers[i + 1][m_CurImageIndex]; + m_HelperThreadDrawCommandBuffers[ThreadedCommandsUsedCount++] = GraphicThreadCommandBuffer; + + m_UsedThreadDrawCommandBuffer[i + 1][m_CurImageIndex] = false; + } + } + if(ThreadedCommandsUsedCount > 0) + { + vkCmdExecuteCommands(CommandBuffer, ThreadedCommandsUsedCount, m_HelperThreadDrawCommandBuffers.data()); + } + + // special case if swap chain was not completed in one runbuffer call + + if(m_UsedThreadDrawCommandBuffer[0][m_CurImageIndex]) + { + auto &GraphicThreadCommandBuffer = m_ThreadDrawCommandBuffers[0][m_CurImageIndex]; + vkEndCommandBuffer(GraphicThreadCommandBuffer); + + vkCmdExecuteCommands(CommandBuffer, 1, &GraphicThreadCommandBuffer); + + m_UsedThreadDrawCommandBuffer[0][m_CurImageIndex] = false; + } + } + + vkCmdEndRenderPass(CommandBuffer); + + if(vkEndCommandBuffer(CommandBuffer) != VK_SUCCESS) + { + SetError("Command buffer cannot be ended anymore."); + } + + VkSemaphore WaitSemaphore = m_WaitSemaphores[m_CurFrames]; + + VkSubmitInfo SubmitInfo{}; + SubmitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + + SubmitInfo.commandBufferCount = 1; + SubmitInfo.pCommandBuffers = &CommandBuffer; + + std::array aCommandBuffers = {}; + + if(m_UsedMemoryCommandBuffer[m_CurImageIndex]) + { + auto &MemoryCommandBuffer = m_MemoryCommandBuffers[m_CurImageIndex]; + vkEndCommandBuffer(MemoryCommandBuffer); + + aCommandBuffers[0] = MemoryCommandBuffer; + aCommandBuffers[1] = CommandBuffer; + SubmitInfo.commandBufferCount = 2; + SubmitInfo.pCommandBuffers = aCommandBuffers.data(); + + m_UsedMemoryCommandBuffer[m_CurImageIndex] = false; + } + + std::array aWaitSemaphores = {WaitSemaphore}; + std::array aWaitStages = {(VkPipelineStageFlags)VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT}; + SubmitInfo.waitSemaphoreCount = aWaitSemaphores.size(); + SubmitInfo.pWaitSemaphores = aWaitSemaphores.data(); + SubmitInfo.pWaitDstStageMask = aWaitStages.data(); + + std::array aSignalSemaphores = {m_SigSemaphores[m_CurFrames]}; + SubmitInfo.signalSemaphoreCount = aSignalSemaphores.size(); + SubmitInfo.pSignalSemaphores = aSignalSemaphores.data(); + + vkResetFences(m_VKDevice, 1, &m_FrameFences[m_CurFrames]); + + VkResult QueueSubmitRes = vkQueueSubmit(m_VKGraphicsQueue, 1, &SubmitInfo, m_FrameFences[m_CurFrames]); + if(QueueSubmitRes != VK_SUCCESS) + { + const char *pCritErrorMsg = CheckVulkanCriticalError(QueueSubmitRes); + if(pCritErrorMsg != nullptr) + SetError("Submitting to graphics queue failed.", pCritErrorMsg); + } + + std::swap(m_WaitSemaphores[m_CurFrames], m_SigSemaphores[m_CurFrames]); + + VkPresentInfoKHR PresentInfo{}; + PresentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; + + PresentInfo.waitSemaphoreCount = aSignalSemaphores.size(); + PresentInfo.pWaitSemaphores = aSignalSemaphores.data(); + + std::array aSwapChains = {m_VKSwapChain}; + PresentInfo.swapchainCount = aSwapChains.size(); + PresentInfo.pSwapchains = aSwapChains.data(); + + PresentInfo.pImageIndices = &m_CurImageIndex; + + m_LastPresentedSwapChainImageIndex = m_CurImageIndex; + + VkResult QueuePresentRes = vkQueuePresentKHR(m_VKPresentQueue, &PresentInfo); + if(QueuePresentRes != VK_SUCCESS && QueuePresentRes != VK_SUBOPTIMAL_KHR) + { + const char *pCritErrorMsg = CheckVulkanCriticalError(QueuePresentRes); + if(pCritErrorMsg != nullptr) + SetError("Presenting graphics queue failed.", pCritErrorMsg); + } + + m_CurFrames = (m_CurFrames + 1) % m_SwapChainImageCount; + } + + void PrepareFrame() + { + if(m_RecreateSwapChain) + { + m_RecreateSwapChain = false; + if(IsVerbose()) + { + dbg_msg("vulkan", "recreating swap chain requested by user (prepare frame)."); + } + RecreateSwapChain(); + } + + auto AcqResult = vkAcquireNextImageKHR(m_VKDevice, m_VKSwapChain, std::numeric_limits::max(), m_SigSemaphores[m_CurFrames], VK_NULL_HANDLE, &m_CurImageIndex); + if(AcqResult != VK_SUCCESS) + { + if(AcqResult == VK_ERROR_OUT_OF_DATE_KHR || m_RecreateSwapChain) + { + m_RecreateSwapChain = false; + if(IsVerbose()) + { + dbg_msg("vulkan", "recreating swap chain requested by acquire next image (prepare frame)."); + } + RecreateSwapChain(); + PrepareFrame(); + return; + } + else + { + if(AcqResult != VK_SUBOPTIMAL_KHR) + dbg_msg("vulkan", "acquire next image failed %d", (int)AcqResult); + + const char *pCritErrorMsg = CheckVulkanCriticalError(AcqResult); + if(pCritErrorMsg != nullptr) + SetError("Acquiring next image failed.", pCritErrorMsg); + else if(AcqResult == VK_ERROR_SURFACE_LOST_KHR) + { + m_RenderingPaused = true; + return; + } + } + } + std::swap(m_WaitSemaphores[m_CurFrames], m_SigSemaphores[m_CurFrames]); + + if(m_ImagesFences[m_CurImageIndex] != VK_NULL_HANDLE) + { + vkWaitForFences(m_VKDevice, 1, &m_ImagesFences[m_CurImageIndex], VK_TRUE, std::numeric_limits::max()); + } + m_ImagesFences[m_CurImageIndex] = m_FrameFences[m_CurFrames]; + + // next frame + m_CurFrame++; + m_ImageLastFrameCheck[m_CurImageIndex] = m_CurFrame; + + // check if older frames weren't used in a long time + for(size_t FrameImageIndex = 0; FrameImageIndex < m_ImageLastFrameCheck.size(); ++FrameImageIndex) + { + auto LastFrame = m_ImageLastFrameCheck[FrameImageIndex]; + if(m_CurFrame - LastFrame > (uint64_t)m_SwapChainImageCount) + { + if(m_ImagesFences[FrameImageIndex] != VK_NULL_HANDLE) + { + vkWaitForFences(m_VKDevice, 1, &m_ImagesFences[FrameImageIndex], VK_TRUE, std::numeric_limits::max()); + ClearFrameData(FrameImageIndex); + m_ImagesFences[FrameImageIndex] = VK_NULL_HANDLE; + } + m_ImageLastFrameCheck[FrameImageIndex] = m_CurFrame; + } + } + + // clear frame's memory data + ClearFrameMemoryUsage(); + + // clear frame + vkResetCommandBuffer(GetMainGraphicCommandBuffer(), VK_COMMAND_BUFFER_RESET_RELEASE_RESOURCES_BIT); + + auto &CommandBuffer = GetMainGraphicCommandBuffer(); + VkCommandBufferBeginInfo BeginInfo{}; + BeginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + BeginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + + if(vkBeginCommandBuffer(CommandBuffer, &BeginInfo) != VK_SUCCESS) + { + SetError("Command buffer cannot be filled anymore."); + } + + VkRenderPassBeginInfo RenderPassInfo{}; + RenderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + RenderPassInfo.renderPass = m_VKRenderPass; + RenderPassInfo.framebuffer = m_FramebufferList[m_CurImageIndex]; + RenderPassInfo.renderArea.offset = {0, 0}; + RenderPassInfo.renderArea.extent = m_VKSwapImgAndViewportExtent.m_Viewport; + + VkClearValue ClearColorVal = {{{m_aClearColor[0], m_aClearColor[1], m_aClearColor[2], m_aClearColor[3]}}}; + RenderPassInfo.clearValueCount = 1; + RenderPassInfo.pClearValues = &ClearColorVal; + + vkCmdBeginRenderPass(CommandBuffer, &RenderPassInfo, m_ThreadCount > 1 ? VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS : VK_SUBPASS_CONTENTS_INLINE); + + for(auto &LastPipe : m_vLastPipeline) + LastPipe = VK_NULL_HANDLE; + } + + void UploadStagingBuffers() + { + if(!m_NonFlushedStagingBufferRange.empty()) + { + vkFlushMappedMemoryRanges(m_VKDevice, m_NonFlushedStagingBufferRange.size(), m_NonFlushedStagingBufferRange.data()); + + m_NonFlushedStagingBufferRange.clear(); + } + } + + template + void UploadNonFlushedBuffers() + { + // streamed vertices + for(auto &StreamVertexBuffer : m_vStreamedVertexBuffers) + UploadStreamedBuffer(StreamVertexBuffer); + // now the buffer objects + for(auto &StreamUniformBuffer : m_vStreamedUniformBuffers) + UploadStreamedBuffer(StreamUniformBuffer); + + UploadStagingBuffers(); + } + + void PureMemoryFrame() + { + ExecuteMemoryCommandBuffer(); + + // reset streamed data + UploadNonFlushedBuffers(); + + ClearFrameMemoryUsage(); + } + + void NextFrame() + { + if(!m_RenderingPaused) + { + WaitFrame(); + PrepareFrame(); + } + // else only execute the memory command buffer + else + { + PureMemoryFrame(); + } + } + + /************************ + * TEXTURES + ************************/ + + size_t VulkanFormatToImageColorChannelCount(VkFormat Format) + { + if(Format == VK_FORMAT_R8G8B8_UNORM) + return 3; + else if(Format == VK_FORMAT_R8G8B8A8_UNORM) + return 4; + else if(Format == VK_FORMAT_R8_UNORM) + return 1; + return 4; + } + + void UpdateTexture(size_t TextureSlot, VkFormat Format, void *&pData, int64_t XOff, int64_t YOff, size_t Width, size_t Height, size_t ColorChannelCount) + { + size_t ImageSize = Width * Height * ColorChannelCount; + auto StagingBuffer = GetStagingBufferImage(pData, ImageSize); + + auto &Tex = m_Textures[TextureSlot]; + + if(Tex.m_RescaleCount > 0) + { + for(uint32_t i = 0; i < Tex.m_RescaleCount; ++i) + { + Width >>= 1; + Height >>= 1; + + XOff /= 2; + YOff /= 2; + } + + void *pTmpData = Resize((const uint8_t *)pData, Width, Height, Width, Height, VulkanFormatToImageColorChannelCount(Format)); + free(pData); + pData = pTmpData; + } + + ImageBarrier(Tex.m_Img, 0, Tex.m_MipMapCount, 0, 1, Format, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + CopyBufferToImage(StagingBuffer.m_Buffer, StagingBuffer.m_HeapData.m_OffsetToAlign, Tex.m_Img, XOff, YOff, Width, Height, 1); + + if(Tex.m_MipMapCount > 1) + BuildMipmaps(Tex.m_Img, Format, Width, Height, 1, Tex.m_MipMapCount); + else + ImageBarrier(Tex.m_Img, 0, 1, 0, 1, Format, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + + UploadAndFreeStagingImageMemBlock(StagingBuffer); + } + + void CreateTextureCMD( + int Slot, + int Width, + int Height, + int PixelSize, + VkFormat Format, + VkFormat StoreFormat, + int Flags, + void *&pData) + { + size_t ImageIndex = (size_t)Slot; + int ImageColorChannels = VulkanFormatToImageColorChannelCount(Format); + + while(ImageIndex >= m_Textures.size()) + { + m_Textures.resize((m_Textures.size() * 2) + 1); + } + + // resample if needed + uint32_t RescaleCount = 0; + if((size_t)Width > m_MaxTextureSize || (size_t)Height > m_MaxTextureSize) + { + do + { + Width >>= 1; + Height >>= 1; + ++RescaleCount; + } while((size_t)Width > m_MaxTextureSize || (size_t)Height > m_MaxTextureSize); + + void *pTmpData = Resize((const uint8_t *)(pData), Width, Height, Width, Height, ImageColorChannels); + free(pData); + pData = pTmpData; + } + + bool Requires2DTexture = (Flags & CCommandBuffer::TEXFLAG_NO_2D_TEXTURE) == 0; + bool Requires2DTextureArray = (Flags & (CCommandBuffer::TEXFLAG_TO_2D_ARRAY_TEXTURE | CCommandBuffer::TEXFLAG_TO_2D_ARRAY_TEXTURE_SINGLE_LAYER)) != 0; + bool Is2DTextureSingleLayer = (Flags & CCommandBuffer::TEXFLAG_TO_2D_ARRAY_TEXTURE_SINGLE_LAYER) != 0; + bool RequiresMipMaps = (Flags & CCommandBuffer::TEXFLAG_NOMIPMAPS) == 0; + size_t MipMapLevelCount = 1; + if(RequiresMipMaps) + { + VkExtent3D ImgSize{(uint32_t)Width, (uint32_t)Height, 1}; + MipMapLevelCount = ImageMipLevelCount(ImgSize); + if(!m_OptimalRGBAImageBlitting) + MipMapLevelCount = 1; + } + + CTexture &Texture = m_Textures[ImageIndex]; + + Texture.m_Width = Width; + Texture.m_Height = Height; + Texture.m_RescaleCount = RescaleCount; + Texture.m_MipMapCount = MipMapLevelCount; + + if(Requires2DTexture) + { + CreateTextureImage(ImageIndex, Texture.m_Img, Texture.m_ImgMem, pData, Format, Width, Height, 1, PixelSize, MipMapLevelCount); + VkFormat ImgFormat = Format; + VkImageView ImgView = CreateTextureImageView(Texture.m_Img, ImgFormat, VK_IMAGE_VIEW_TYPE_2D, 1, MipMapLevelCount); + Texture.m_ImgView = ImgView; + VkSampler ImgSampler = GetTextureSampler(SUPPORTED_SAMPLER_TYPE_REPEAT); + Texture.m_aSamplers[0] = ImgSampler; + ImgSampler = GetTextureSampler(SUPPORTED_SAMPLER_TYPE_CLAMP_TO_EDGE); + Texture.m_aSamplers[1] = ImgSampler; + + CreateNewTexturedStandardDescriptorSets(ImageIndex, 0); + CreateNewTexturedStandardDescriptorSets(ImageIndex, 1); + } + + if(Requires2DTextureArray) + { + int Image3DWidth = Width; + int Image3DHeight = Height; + + int ConvertWidth = Width; + int ConvertHeight = Height; + + if(!Is2DTextureSingleLayer) + { + if(ConvertWidth == 0 || (ConvertWidth % 16) != 0 || ConvertHeight == 0 || (ConvertHeight % 16) != 0) + { + dbg_msg("vulkan", "3D/2D array texture was resized"); + int NewWidth = maximum(HighestBit(ConvertWidth), 16); + int NewHeight = maximum(HighestBit(ConvertHeight), 16); + uint8_t *pNewTexData = (uint8_t *)Resize((const uint8_t *)pData, ConvertWidth, ConvertHeight, NewWidth, NewHeight, ImageColorChannels); + + ConvertWidth = NewWidth; + ConvertHeight = NewHeight; + + free(pData); + pData = pNewTexData; + } + } + + void *p3DTexData = pData; + bool Needs3DTexDel = false; + if(!Is2DTextureSingleLayer) + { + p3DTexData = malloc((size_t)ImageColorChannels * ConvertWidth * ConvertHeight); + if(!Texture2DTo3D(pData, ConvertWidth, ConvertHeight, ImageColorChannels, 16, 16, p3DTexData, Image3DWidth, Image3DHeight)) + { + free(p3DTexData); + p3DTexData = nullptr; + } + Needs3DTexDel = true; + } + else + { + Image3DWidth = ConvertWidth; + Image3DHeight = ConvertHeight; + } + + if(p3DTexData != nullptr) + { + const size_t ImageDepth2DArray = Is2DTextureSingleLayer ? 1 : ((size_t)16 * 16); + VkExtent3D ImgSize{(uint32_t)Image3DWidth, (uint32_t)Image3DHeight, 1}; + if(RequiresMipMaps) + { + MipMapLevelCount = ImageMipLevelCount(ImgSize); + if(!m_OptimalRGBAImageBlitting) + MipMapLevelCount = 1; + } + + CreateTextureImage(ImageIndex, Texture.m_Img3D, Texture.m_Img3DMem, p3DTexData, Format, Image3DWidth, Image3DHeight, ImageDepth2DArray, PixelSize, MipMapLevelCount); + VkFormat ImgFormat = Format; + VkImageView ImgView = CreateTextureImageView(Texture.m_Img3D, ImgFormat, VK_IMAGE_VIEW_TYPE_2D_ARRAY, ImageDepth2DArray, MipMapLevelCount); + Texture.m_Img3DView = ImgView; + VkSampler ImgSampler = GetTextureSampler(SUPPORTED_SAMPLER_TYPE_2D_TEXTURE_ARRAY); + Texture.m_Sampler3D = ImgSampler; + + CreateNew3DTexturedStandardDescriptorSets(ImageIndex); + + if(Needs3DTexDel) + free(p3DTexData); + } + } + } + + VkFormat TextureFormatToVulkanFormat(int TexFormat) + { + if(TexFormat == CCommandBuffer::TEXFORMAT_RGBA) + return VK_FORMAT_R8G8B8A8_UNORM; + return VK_FORMAT_R8G8B8A8_UNORM; + } + + void BuildMipmaps(VkImage Image, VkFormat ImageFormat, size_t Width, size_t Height, size_t Depth, size_t MipMapLevelCount) + { + VkCommandBuffer MemCommandBuffer = GetMemoryCommandBuffer(); + + VkImageMemoryBarrier Barrier{}; + Barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + Barrier.image = Image; + Barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + Barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + Barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + Barrier.subresourceRange.levelCount = 1; + Barrier.subresourceRange.baseArrayLayer = 0; + Barrier.subresourceRange.layerCount = Depth; + + int32_t TmpMipWidth = (int32_t)Width; + int32_t TmpMipHeight = (int32_t)Height; + + for(size_t i = 1; i < MipMapLevelCount; ++i) + { + Barrier.subresourceRange.baseMipLevel = i - 1; + Barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + Barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + Barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + Barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + + vkCmdPipelineBarrier(MemCommandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &Barrier); + + VkImageBlit Blit{}; + Blit.srcOffsets[0] = {0, 0, 0}; + Blit.srcOffsets[1] = {TmpMipWidth, TmpMipHeight, 1}; + Blit.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + Blit.srcSubresource.mipLevel = i - 1; + Blit.srcSubresource.baseArrayLayer = 0; + Blit.srcSubresource.layerCount = Depth; + Blit.dstOffsets[0] = {0, 0, 0}; + Blit.dstOffsets[1] = {TmpMipWidth > 1 ? TmpMipWidth / 2 : 1, TmpMipHeight > 1 ? TmpMipHeight / 2 : 1, 1}; + Blit.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + Blit.dstSubresource.mipLevel = i; + Blit.dstSubresource.baseArrayLayer = 0; + Blit.dstSubresource.layerCount = Depth; + + vkCmdBlitImage(MemCommandBuffer, + Image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + Image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + 1, &Blit, + m_AllowsLinearBlitting ? VK_FILTER_LINEAR : VK_FILTER_NEAREST); + + Barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + Barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + Barrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + Barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + + vkCmdPipelineBarrier(MemCommandBuffer, + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, + 0, nullptr, + 0, nullptr, + 1, &Barrier); + + if(TmpMipWidth > 1) + TmpMipWidth /= 2; + if(TmpMipHeight > 1) + TmpMipHeight /= 2; + } + + Barrier.subresourceRange.baseMipLevel = MipMapLevelCount - 1; + Barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + Barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + Barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + Barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + + vkCmdPipelineBarrier(MemCommandBuffer, + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, + 0, nullptr, + 0, nullptr, + 1, &Barrier); + } + + bool CreateTextureImage(size_t ImageIndex, VkImage &NewImage, SMemoryImageBlock &NewImgMem, const void *pData, VkFormat Format, size_t Width, size_t Height, size_t Depth, size_t PixelSize, size_t MipMapLevelCount) + { + int ImageSize = Width * Height * Depth * PixelSize; + + auto StagingBuffer = GetStagingBufferImage(pData, ImageSize); + + VkFormat ImgFormat = Format; + + CreateImage(Width, Height, Depth, MipMapLevelCount, ImgFormat, VK_IMAGE_TILING_OPTIMAL, NewImage, NewImgMem); + + ImageBarrier(NewImage, 0, MipMapLevelCount, 0, Depth, ImgFormat, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + CopyBufferToImage(StagingBuffer.m_Buffer, StagingBuffer.m_HeapData.m_OffsetToAlign, NewImage, 0, 0, static_cast(Width), static_cast(Height), Depth); + //ImageBarrier(NewImage, 0, 1, 0, Depth, ImgFormat, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + + UploadAndFreeStagingImageMemBlock(StagingBuffer); + + if(MipMapLevelCount > 1) + BuildMipmaps(NewImage, ImgFormat, Width, Height, Depth, MipMapLevelCount); + else + ImageBarrier(NewImage, 0, 1, 0, Depth, ImgFormat, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + + return true; + } + + VkImageView CreateTextureImageView(VkImage TexImage, VkFormat ImgFormat, VkImageViewType ViewType, size_t Depth, size_t MipMapLevelCount) + { + return CreateImageView(TexImage, ImgFormat, ViewType, Depth, MipMapLevelCount); + } + + bool CreateTextureSamplersImpl(VkSampler &CreatedSampler, VkSamplerAddressMode AddrModeU, VkSamplerAddressMode AddrModeV, VkSamplerAddressMode AddrModeW) + { + VkSamplerCreateInfo SamplerInfo{}; + SamplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; + SamplerInfo.magFilter = VK_FILTER_LINEAR; + SamplerInfo.minFilter = VK_FILTER_LINEAR; + SamplerInfo.addressModeU = AddrModeU; + SamplerInfo.addressModeV = AddrModeV; + SamplerInfo.addressModeW = AddrModeW; + SamplerInfo.anisotropyEnable = VK_FALSE; + SamplerInfo.maxAnisotropy = m_MaxSamplerAnisotropy; + SamplerInfo.borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK; + SamplerInfo.unnormalizedCoordinates = VK_FALSE; + SamplerInfo.compareEnable = VK_FALSE; + SamplerInfo.compareOp = VK_COMPARE_OP_ALWAYS; + SamplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; + SamplerInfo.mipLodBias = (m_GlobalTextureLodBIAS / 1000.0f); + SamplerInfo.minLod = -1000; + SamplerInfo.maxLod = 1000; + + if(vkCreateSampler(m_VKDevice, &SamplerInfo, nullptr, &CreatedSampler) != VK_SUCCESS) + { + dbg_msg("vulkan", "failed to create texture sampler!"); + return false; + } + return true; + } + + bool CreateTextureSamplers() + { + bool Ret = true; + Ret &= CreateTextureSamplersImpl(m_aSamplers[SUPPORTED_SAMPLER_TYPE_REPEAT], VkSamplerAddressMode::VK_SAMPLER_ADDRESS_MODE_REPEAT, VkSamplerAddressMode::VK_SAMPLER_ADDRESS_MODE_REPEAT, VkSamplerAddressMode::VK_SAMPLER_ADDRESS_MODE_REPEAT); + Ret &= CreateTextureSamplersImpl(m_aSamplers[SUPPORTED_SAMPLER_TYPE_CLAMP_TO_EDGE], VkSamplerAddressMode::VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, VkSamplerAddressMode::VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, VkSamplerAddressMode::VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE); + Ret &= CreateTextureSamplersImpl(m_aSamplers[SUPPORTED_SAMPLER_TYPE_2D_TEXTURE_ARRAY], VkSamplerAddressMode::VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, VkSamplerAddressMode::VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, VkSamplerAddressMode::VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT); + return Ret; + } + + void DestroyTextureSamplers() + { + vkDestroySampler(m_VKDevice, m_aSamplers[SUPPORTED_SAMPLER_TYPE_REPEAT], nullptr); + vkDestroySampler(m_VKDevice, m_aSamplers[SUPPORTED_SAMPLER_TYPE_CLAMP_TO_EDGE], nullptr); + vkDestroySampler(m_VKDevice, m_aSamplers[SUPPORTED_SAMPLER_TYPE_2D_TEXTURE_ARRAY], nullptr); + } + + VkSampler GetTextureSampler(ESupportedSamplerTypes SamplerType) + { + return m_aSamplers[SamplerType]; + } + + VkImageView CreateImageView(VkImage Image, VkFormat Format, VkImageViewType ViewType, size_t Depth, size_t MipMapLevelCount) + { + VkImageViewCreateInfo ViewCreateInfo{}; + ViewCreateInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + ViewCreateInfo.image = Image; + ViewCreateInfo.viewType = ViewType; + ViewCreateInfo.format = Format; + ViewCreateInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + ViewCreateInfo.subresourceRange.baseMipLevel = 0; + ViewCreateInfo.subresourceRange.levelCount = MipMapLevelCount; + ViewCreateInfo.subresourceRange.baseArrayLayer = 0; + ViewCreateInfo.subresourceRange.layerCount = Depth; + + VkImageView ImageView; + if(vkCreateImageView(m_VKDevice, &ViewCreateInfo, nullptr, &ImageView) != VK_SUCCESS) + { + return VK_NULL_HANDLE; + } + + return ImageView; + } + + void CreateImage(uint32_t Width, uint32_t Height, uint32_t Depth, size_t MipMapLevelCount, VkFormat Format, VkImageTiling Tiling, VkImage &Image, SMemoryImageBlock &ImageMemory) + { + VkImageCreateInfo ImageInfo{}; + ImageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + ImageInfo.imageType = VK_IMAGE_TYPE_2D; + ImageInfo.extent.width = Width; + ImageInfo.extent.height = Height; + ImageInfo.extent.depth = 1; + ImageInfo.mipLevels = MipMapLevelCount; + ImageInfo.arrayLayers = Depth; + ImageInfo.format = Format; + ImageInfo.tiling = Tiling; + ImageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + ImageInfo.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + ImageInfo.samples = VK_SAMPLE_COUNT_1_BIT; + ImageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + + if(vkCreateImage(m_VKDevice, &ImageInfo, nullptr, &Image) != VK_SUCCESS) + { + dbg_msg("vulkan", "failed to create image!"); + } + + VkMemoryRequirements MemRequirements; + vkGetImageMemoryRequirements(m_VKDevice, Image, &MemRequirements); + + auto ImageMem = GetImageMemory(MemRequirements.size, MemRequirements.alignment, MemRequirements.memoryTypeBits); + + ImageMemory = ImageMem; + vkBindImageMemory(m_VKDevice, Image, ImageMem.m_BufferMem.m_Mem, ImageMem.m_HeapData.m_OffsetToAlign); + } + + void ImageBarrier(VkImage &Image, size_t MipMapBase, size_t MipMapCount, size_t LayerBase, size_t LayerCount, VkFormat Format, VkImageLayout OldLayout, VkImageLayout NewLayout) + { + VkCommandBuffer MemCommandBuffer = GetMemoryCommandBuffer(); + + VkImageMemoryBarrier Barrier{}; + Barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + Barrier.oldLayout = OldLayout; + Barrier.newLayout = NewLayout; + Barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + Barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + Barrier.image = Image; + Barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + Barrier.subresourceRange.baseMipLevel = MipMapBase; + Barrier.subresourceRange.levelCount = MipMapCount; + Barrier.subresourceRange.baseArrayLayer = LayerBase; + Barrier.subresourceRange.layerCount = LayerCount; + + VkPipelineStageFlags SourceStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + VkPipelineStageFlags DestinationStage = VK_PIPELINE_STAGE_TRANSFER_BIT; + + if(OldLayout == VK_IMAGE_LAYOUT_UNDEFINED && NewLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) + { + Barrier.srcAccessMask = 0; + Barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + + SourceStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + DestinationStage = VK_PIPELINE_STAGE_TRANSFER_BIT; + } + else if(OldLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL && NewLayout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) + { + Barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + Barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + + SourceStage = VK_PIPELINE_STAGE_TRANSFER_BIT; + DestinationStage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; + } + else if(OldLayout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL && NewLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) + { + Barrier.srcAccessMask = VK_ACCESS_SHADER_READ_BIT; + Barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + + SourceStage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; + DestinationStage = VK_PIPELINE_STAGE_TRANSFER_BIT; + } + else if(OldLayout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL && NewLayout == VK_IMAGE_LAYOUT_PRESENT_SRC_KHR) + { + Barrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + Barrier.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT; + + SourceStage = VK_PIPELINE_STAGE_TRANSFER_BIT; + DestinationStage = VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT; + } + else if(OldLayout == VK_IMAGE_LAYOUT_PRESENT_SRC_KHR && NewLayout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL) + { + Barrier.srcAccessMask = VK_ACCESS_MEMORY_READ_BIT; + Barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + + SourceStage = VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT; + DestinationStage = VK_PIPELINE_STAGE_TRANSFER_BIT; + } + else if(OldLayout == VK_IMAGE_LAYOUT_UNDEFINED && NewLayout == VK_IMAGE_LAYOUT_GENERAL) + { + Barrier.srcAccessMask = 0; + Barrier.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT; + + SourceStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + DestinationStage = VK_PIPELINE_STAGE_TRANSFER_BIT; + } + else if(OldLayout == VK_IMAGE_LAYOUT_GENERAL && NewLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) + { + Barrier.srcAccessMask = VK_ACCESS_MEMORY_READ_BIT; + Barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + + SourceStage = VK_PIPELINE_STAGE_TRANSFER_BIT; + DestinationStage = VK_PIPELINE_STAGE_TRANSFER_BIT; + } + else if(OldLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL && NewLayout == VK_IMAGE_LAYOUT_GENERAL) + { + Barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + Barrier.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT; + + SourceStage = VK_PIPELINE_STAGE_TRANSFER_BIT; + DestinationStage = VK_PIPELINE_STAGE_TRANSFER_BIT; + } + else + { + dbg_msg("vulkan", "unsupported layout transition!"); + } + + vkCmdPipelineBarrier( + MemCommandBuffer, + SourceStage, DestinationStage, + 0, + 0, nullptr, + 0, nullptr, + 1, &Barrier); + } + + void CopyBufferToImage(VkBuffer Buffer, VkDeviceSize BufferOffset, VkImage Image, int32_t X, int32_t Y, uint32_t Width, uint32_t Height, size_t Depth) + { + VkCommandBuffer CommandBuffer = GetMemoryCommandBuffer(); + + VkBufferImageCopy Region{}; + Region.bufferOffset = BufferOffset; + Region.bufferRowLength = 0; + Region.bufferImageHeight = 0; + Region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + Region.imageSubresource.mipLevel = 0; + Region.imageSubresource.baseArrayLayer = 0; + Region.imageSubresource.layerCount = Depth; + Region.imageOffset = {X, Y, 0}; + Region.imageExtent = { + Width, + Height, + 1}; + + vkCmdCopyBufferToImage(CommandBuffer, Buffer, Image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &Region); + } + + /************************ + * BUFFERS + ************************/ + + void CreateBufferObject(size_t BufferIndex, const void *pUploadData, VkDeviceSize BufferDataSize, bool IsOneFrameBuffer) + { + void *pUploadDataTmp = nullptr; + if(pUploadData == nullptr) + { + pUploadDataTmp = malloc(BufferDataSize); + pUploadData = pUploadDataTmp; + } + + while(BufferIndex >= m_BufferObjects.size()) + { + m_BufferObjects.resize((m_BufferObjects.size() * 2) + 1); + } + auto &BufferObject = m_BufferObjects[BufferIndex]; + + VkBuffer VertexBuffer; + size_t BufferOffset = 0; + if(!IsOneFrameBuffer) + { + auto StagingBuffer = GetStagingBuffer(pUploadData, BufferDataSize); + + auto Mem = GetVertexBuffer(BufferDataSize); + + BufferObject.m_BufferObject.m_Mem = Mem; + VertexBuffer = Mem.m_Buffer; + BufferOffset = Mem.m_HeapData.m_OffsetToAlign; + + MemoryBarrier(VertexBuffer, Mem.m_HeapData.m_OffsetToAlign, BufferDataSize, VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT, true); + CopyBuffer(StagingBuffer.m_Buffer, VertexBuffer, StagingBuffer.m_HeapData.m_OffsetToAlign, Mem.m_HeapData.m_OffsetToAlign, BufferDataSize); + MemoryBarrier(VertexBuffer, Mem.m_HeapData.m_OffsetToAlign, BufferDataSize, VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT, false); + UploadAndFreeStagingMemBlock(StagingBuffer); + } + else + { + SDeviceMemoryBlock VertexBufferMemory; + CreateStreamVertexBuffer(ms_MainThreadIndex, VertexBuffer, VertexBufferMemory, BufferOffset, pUploadData, BufferDataSize); + } + BufferObject.m_IsStreamedBuffer = IsOneFrameBuffer; + BufferObject.m_CurBuffer = VertexBuffer; + BufferObject.m_CurBufferOffset = BufferOffset; + + if(pUploadDataTmp != nullptr) + free(pUploadDataTmp); + } + + void DeleteBufferObject(size_t BufferIndex) + { + auto &BufferObject = m_BufferObjects[BufferIndex]; + if(!BufferObject.m_IsStreamedBuffer) + { + FreeVertexMemBlock(BufferObject.m_BufferObject.m_Mem); + } + BufferObject = {}; + } + + void CopyBuffer(VkBuffer SrcBuffer, VkBuffer DstBuffer, VkDeviceSize SrcOffset, VkDeviceSize DstOffset, VkDeviceSize CopySize) + { + VkCommandBuffer CommandBuffer = GetMemoryCommandBuffer(); + VkBufferCopy CopyRegion{}; + CopyRegion.srcOffset = SrcOffset; + CopyRegion.dstOffset = DstOffset; + CopyRegion.size = CopySize; + vkCmdCopyBuffer(CommandBuffer, SrcBuffer, DstBuffer, 1, &CopyRegion); + } + + /************************ + * RENDER STATES + ************************/ + + void GetStateMatrix(const CCommandBuffer::SState &State, std::array &Matrix) + { + Matrix = { + // column 1 + 2.f / (State.m_ScreenBR.x - State.m_ScreenTL.x), + 0, + // column 2 + 0, + 2.f / (State.m_ScreenBR.y - State.m_ScreenTL.y), + // column 3 + 0, + 0, + // column 4 + -((State.m_ScreenTL.x + State.m_ScreenBR.x) / (State.m_ScreenBR.x - State.m_ScreenTL.x)), + -((State.m_ScreenTL.y + State.m_ScreenBR.y) / (State.m_ScreenBR.y - State.m_ScreenTL.y)), + }; + } + + bool GetIsTextured(const CCommandBuffer::SState &State) + { + return State.m_Texture != -1; + } + + size_t GetAddressModeIndex(const CCommandBuffer::SState &State) + { + return State.m_WrapMode == CCommandBuffer::WRAP_REPEAT ? VULKAN_BACKEND_ADDRESS_MODE_REPEAT : VULKAN_BACKEND_ADDRESS_MODE_CLAMP_EDGES; + } + + size_t GetBlendModeIndex(const CCommandBuffer::SState &State) + { + return State.m_BlendMode == CCommandBuffer::BLEND_ADDITIVE ? VULKAN_BACKEND_BLEND_MODE_ADDITATIVE : (State.m_BlendMode == CCommandBuffer::BLEND_NONE ? VULKAN_BACKEND_BLEND_MODE_NONE : VULKAN_BACKEND_BLEND_MODE_ALPHA); + } + + size_t GetDynamicModeIndex(const CCommandBuffer::SState &State) + { + return (State.m_ClipEnable || m_HasDynamicViewport) ? VULKAN_BACKEND_CLIP_MODE_DYNAMIC_SCISSOR_AND_VIEWPORT : VULKAN_BACKEND_CLIP_MODE_NONE; + } + + VkPipeline &GetPipeline(SPipelineContainer &Container, bool IsTextured, size_t BlendModeIndex, size_t DynamicIndex) + { + return Container.m_aaaPipelines[BlendModeIndex][DynamicIndex][(size_t)IsTextured]; + } + + VkPipelineLayout &GetPipeLayout(SPipelineContainer &Container, bool IsTextured, size_t BlendModeIndex, size_t DynamicIndex) + { + return Container.m_aaaPipelineLayouts[BlendModeIndex][DynamicIndex][(size_t)IsTextured]; + } + + VkPipelineLayout &GetStandardPipeLayout(bool IsLineGeometry, bool IsTextured, size_t BlendModeIndex, size_t DynamicIndex) + { + if(IsLineGeometry) + return GetPipeLayout(m_StandardLinePipeline, IsTextured, BlendModeIndex, DynamicIndex); + else + return GetPipeLayout(m_StandardPipeline, IsTextured, BlendModeIndex, DynamicIndex); + } + + VkPipeline &GetStandardPipe(bool IsLineGeometry, bool IsTextured, size_t BlendModeIndex, size_t DynamicIndex) + { + if(IsLineGeometry) + return GetPipeline(m_StandardLinePipeline, IsTextured, BlendModeIndex, DynamicIndex); + else + return GetPipeline(m_StandardPipeline, IsTextured, BlendModeIndex, DynamicIndex); + } + + VkPipelineLayout &GetTileLayerPipeLayout(int Type, bool IsTextured, size_t BlendModeIndex, size_t DynamicIndex) + { + if(Type == 0) + return GetPipeLayout(m_TilePipeline, IsTextured, BlendModeIndex, DynamicIndex); + else if(Type == 1) + return GetPipeLayout(m_TileBorderPipeline, IsTextured, BlendModeIndex, DynamicIndex); + else + return GetPipeLayout(m_TileBorderLinePipeline, IsTextured, BlendModeIndex, DynamicIndex); + } + + VkPipeline &GetTileLayerPipe(int Type, bool IsTextured, size_t BlendModeIndex, size_t DynamicIndex) + { + if(Type == 0) + return GetPipeline(m_TilePipeline, IsTextured, BlendModeIndex, DynamicIndex); + else if(Type == 1) + return GetPipeline(m_TileBorderPipeline, IsTextured, BlendModeIndex, DynamicIndex); + else + return GetPipeline(m_TileBorderLinePipeline, IsTextured, BlendModeIndex, DynamicIndex); + } + + void GetStateIndices(const CCommandBuffer::SState &State, bool &IsTextured, size_t &BlendModeIndex, size_t &DynamicIndex, size_t &AddressModeIndex) + { + IsTextured = GetIsTextured(State); + AddressModeIndex = GetAddressModeIndex(State); + BlendModeIndex = GetBlendModeIndex(State); + DynamicIndex = GetDynamicModeIndex(State); + } + + void ExecBufferFillDynamicStates(const CCommandBuffer::SState &State, SRenderCommandExecuteBuffer &ExecBuffer) + { + size_t DynamicStateIndex = GetDynamicModeIndex(State); + if(DynamicStateIndex == VULKAN_BACKEND_CLIP_MODE_DYNAMIC_SCISSOR_AND_VIEWPORT) + { + VkViewport Viewport; + if(m_HasDynamicViewport) + { + Viewport.x = (float)m_DynamicViewportOffset.x; + Viewport.y = (float)m_DynamicViewportOffset.y; + Viewport.width = (float)m_DynamicViewportSize.width; + Viewport.height = (float)m_DynamicViewportSize.height; + Viewport.minDepth = 0.0f; + Viewport.maxDepth = 1.0f; + } + else + { + Viewport.x = 0.0f; + Viewport.y = 0.0f; + Viewport.width = (float)m_VKSwapImgAndViewportExtent.m_Viewport.width; + Viewport.height = (float)m_VKSwapImgAndViewportExtent.m_Viewport.height; + Viewport.minDepth = 0.0f; + Viewport.maxDepth = 1.0f; + } + + VkRect2D Scissor; + // convert from OGL to vulkan clip + int32_t ScissorY = (int32_t)m_VKSwapImgAndViewportExtent.m_Viewport.height - ((int32_t)State.m_ClipY + (int32_t)State.m_ClipH); + uint32_t ScissorH = (int32_t)State.m_ClipH; + Scissor.offset = {(int32_t)State.m_ClipX, ScissorY}; + Scissor.extent = {(uint32_t)State.m_ClipW, ScissorH}; + + Viewport.x = clamp(Viewport.x, 0.0f, std::numeric_limits::max()); + Viewport.y = clamp(Viewport.y, 0.0f, std::numeric_limits::max()); + + Scissor.offset.x = clamp(Scissor.offset.x, 0, std::numeric_limits::max()); + Scissor.offset.y = clamp(Scissor.offset.y, 0, std::numeric_limits::max()); + + ExecBuffer.m_HasDynamicState = true; + ExecBuffer.m_Viewport = Viewport; + ExecBuffer.m_Scissor = Scissor; + } + else + { + ExecBuffer.m_HasDynamicState = false; + } + } + + void BindPipeline(size_t RenderThreadIndex, VkCommandBuffer &CommandBuffer, SRenderCommandExecuteBuffer &ExecBuffer, VkPipeline &BindingPipe, const CCommandBuffer::SState &State) + { + if(m_vLastPipeline[RenderThreadIndex] != BindingPipe) + { + vkCmdBindPipeline(CommandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, BindingPipe); + m_vLastPipeline[RenderThreadIndex] = BindingPipe; + } + + size_t DynamicStateIndex = GetDynamicModeIndex(State); + if(DynamicStateIndex == VULKAN_BACKEND_CLIP_MODE_DYNAMIC_SCISSOR_AND_VIEWPORT) + { + vkCmdSetViewport(CommandBuffer, 0, 1, &ExecBuffer.m_Viewport); + vkCmdSetScissor(CommandBuffer, 0, 1, &ExecBuffer.m_Scissor); + } + } + + /************************** + * RENDERING IMPLEMENTATION + ***************************/ + + void RenderTileLayer_FillExecuteBuffer(SRenderCommandExecuteBuffer &ExecBuffer, size_t DrawCalls, const CCommandBuffer::SState &State, size_t BufferContainerIndex) + { + size_t BufferObjectIndex = (size_t)m_BufferContainers[BufferContainerIndex].m_BufferObjectIndex; + auto &BufferObject = m_BufferObjects[BufferObjectIndex]; + + ExecBuffer.m_Buffer = BufferObject.m_CurBuffer; + ExecBuffer.m_BufferOff = BufferObject.m_CurBufferOffset; + + bool IsTextured = GetIsTextured(State); + if(IsTextured) + { + auto &DescrSet = m_Textures[State.m_Texture].m_VKStandard3DTexturedDescrSet; + ExecBuffer.m_aDescriptors[0] = DescrSet; + } + + ExecBuffer.m_IndexBuffer = m_RenderIndexBuffer; + + ExecBuffer.m_EstimatedRenderCallCount = DrawCalls; + + ExecBufferFillDynamicStates(State, ExecBuffer); + } + + void RenderTileLayer(SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SState &State, int Type, const GL_SColorf &Color, const vec2 &Dir, const vec2 &Off, int32_t JumpIndex, size_t IndicesDrawNum, char *const *pIndicesOffsets, const unsigned int *pDrawCount, size_t InstanceCount) + { + std::array m; + GetStateMatrix(State, m); + + bool IsTextured; + size_t BlendModeIndex; + size_t DynamicIndex; + size_t AddressModeIndex; + GetStateIndices(State, IsTextured, BlendModeIndex, DynamicIndex, AddressModeIndex); + auto &PipeLayout = GetTileLayerPipeLayout(Type, IsTextured, BlendModeIndex, DynamicIndex); + auto &PipeLine = GetTileLayerPipe(Type, IsTextured, BlendModeIndex, DynamicIndex); + + auto &CommandBuffer = GetGraphicCommandBuffer(ExecBuffer.m_ThreadIndex); + + BindPipeline(ExecBuffer.m_ThreadIndex, CommandBuffer, ExecBuffer, PipeLine, State); + + std::array aVertexBuffers = {ExecBuffer.m_Buffer}; + std::array aOffsets = {(VkDeviceSize)ExecBuffer.m_BufferOff}; + vkCmdBindVertexBuffers(CommandBuffer, 0, 1, aVertexBuffers.data(), aOffsets.data()); + + if(IsTextured) + { + vkCmdBindDescriptorSets(CommandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, PipeLayout, 0, 1, &ExecBuffer.m_aDescriptors[0].m_Descriptor, 0, nullptr); + } + + SUniformTileGPosBorder VertexPushConstants; + size_t VertexPushConstantSize = sizeof(SUniformTileGPos); + SUniformTileGVertColor FragPushConstants; + size_t FragPushConstantSize = sizeof(SUniformTileGVertColor); + + mem_copy(VertexPushConstants.m_aPos, m.data(), m.size() * sizeof(float)); + mem_copy(FragPushConstants.m_aColor, &Color, sizeof(FragPushConstants.m_aColor)); + + if(Type == 1) + { + mem_copy(&VertexPushConstants.m_Dir, &Dir, sizeof(Dir)); + mem_copy(&VertexPushConstants.m_Offset, &Off, sizeof(Off)); + VertexPushConstants.m_JumpIndex = JumpIndex; + VertexPushConstantSize = sizeof(SUniformTileGPosBorder); + } + else if(Type == 2) + { + mem_copy(&VertexPushConstants.m_Dir, &Dir, sizeof(Dir)); + mem_copy(&VertexPushConstants.m_Offset, &Off, sizeof(Off)); + VertexPushConstantSize = sizeof(SUniformTileGPosBorderLine); + } + + vkCmdPushConstants(CommandBuffer, PipeLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, VertexPushConstantSize, &VertexPushConstants); + vkCmdPushConstants(CommandBuffer, PipeLayout, VK_SHADER_STAGE_FRAGMENT_BIT, sizeof(SUniformTileGPosBorder) + sizeof(SUniformTileGVertColorAlign), FragPushConstantSize, &FragPushConstants); + + size_t DrawCount = (size_t)IndicesDrawNum; + vkCmdBindIndexBuffer(CommandBuffer, ExecBuffer.m_IndexBuffer, 0, VK_INDEX_TYPE_UINT32); + for(size_t i = 0; i < DrawCount; ++i) + { + VkDeviceSize IndexOffset = (VkDeviceSize)((ptrdiff_t)pIndicesOffsets[i] / sizeof(uint32_t)); + + vkCmdDrawIndexed(CommandBuffer, static_cast(pDrawCount[i]), InstanceCount, IndexOffset, 0, 0); + } + } + + template + void RenderStandard(SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SState &State, int PrimType, const TName *pVertices, int PrimitiveCount) + { + std::array m; + GetStateMatrix(State, m); + + bool IsLineGeometry = PrimType == CCommandBuffer::PRIMTYPE_LINES; + + bool IsTextured; + size_t BlendModeIndex; + size_t DynamicIndex; + size_t AddressModeIndex; + GetStateIndices(State, IsTextured, BlendModeIndex, DynamicIndex, AddressModeIndex); + auto &PipeLayout = Is3DTextured ? GetPipeLayout(m_Standard3DPipeline, IsTextured, BlendModeIndex, DynamicIndex) : GetStandardPipeLayout(IsLineGeometry, IsTextured, BlendModeIndex, DynamicIndex); + auto &PipeLine = Is3DTextured ? GetPipeline(m_Standard3DPipeline, IsTextured, BlendModeIndex, DynamicIndex) : GetStandardPipe(IsLineGeometry, IsTextured, BlendModeIndex, DynamicIndex); + + auto &CommandBuffer = GetGraphicCommandBuffer(ExecBuffer.m_ThreadIndex); + + BindPipeline(ExecBuffer.m_ThreadIndex, CommandBuffer, ExecBuffer, PipeLine, State); + + size_t VertPerPrim = 2; + bool IsIndexed = false; + if(PrimType == CCommandBuffer::PRIMTYPE_QUADS) + { + VertPerPrim = 4; + IsIndexed = true; + } + else if(PrimType == CCommandBuffer::PRIMTYPE_TRIANGLES) + { + VertPerPrim = 3; + } + + VkBuffer VKBuffer; + SDeviceMemoryBlock VKBufferMem; + size_t BufferOff = 0; + CreateStreamVertexBuffer(ExecBuffer.m_ThreadIndex, VKBuffer, VKBufferMem, BufferOff, pVertices, VertPerPrim * sizeof(TName) * PrimitiveCount); + + std::array aVertexBuffers = {VKBuffer}; + std::array aOffsets = {(VkDeviceSize)BufferOff}; + vkCmdBindVertexBuffers(CommandBuffer, 0, 1, aVertexBuffers.data(), aOffsets.data()); + + if(IsIndexed) + vkCmdBindIndexBuffer(CommandBuffer, ExecBuffer.m_IndexBuffer, 0, VK_INDEX_TYPE_UINT32); + + if(IsTextured) + { + vkCmdBindDescriptorSets(CommandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, PipeLayout, 0, 1, &ExecBuffer.m_aDescriptors[0].m_Descriptor, 0, nullptr); + } + + vkCmdPushConstants(CommandBuffer, PipeLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(SUniformGPos), m.data()); + + if(IsIndexed) + vkCmdDrawIndexed(CommandBuffer, static_cast(PrimitiveCount * 6), 1, 0, 0, 0); + else + vkCmdDraw(CommandBuffer, static_cast(PrimitiveCount * VertPerPrim), 1, 0, 0); + } + +public: + CCommandProcessorFragment_Vulkan() + { + m_Textures.reserve(CCommandBuffer::MAX_TEXTURES); + } + + /************************ + * VULKAN SETUP CODE + ************************/ + + bool GetVulkanExtensions(SDL_Window *pWindow, std::vector &VKExtensions) + { + unsigned int ExtCount = 0; + if(!SDL_Vulkan_GetInstanceExtensions(pWindow, &ExtCount, nullptr)) + { + SetError("Could not get instance extensions from SDL."); + return false; + } + + std::vector ExtensionList(ExtCount); + if(!SDL_Vulkan_GetInstanceExtensions(pWindow, &ExtCount, ExtensionList.data())) + { + SetError("Could not get instance extensions from SDL."); + return false; + } + + for(uint32_t i = 0; i < ExtCount; i++) + { + VKExtensions.emplace_back(ExtensionList[i]); + } + + return true; + } + + std::set OurVKLayers() + { + std::set OurLayers; + + if(g_Config.m_DbgGfx == DEBUG_GFX_MODE_MINIMUM || g_Config.m_DbgGfx == DEBUG_GFX_MODE_ALL) + { + OurLayers.emplace("VK_LAYER_KHRONOS_validation"); + // deprecated, but VK_LAYER_KHRONOS_validation was released after vulkan 1.1 + OurLayers.emplace("VK_LAYER_LUNARG_standard_validation"); + } + + return OurLayers; + } + + std::set OurDeviceExtensions() + { + std::set OurExt; + OurExt.emplace(VK_KHR_SWAPCHAIN_EXTENSION_NAME); + return OurExt; + } + + std::vector OurImageUsages() + { + std::vector ImgUsages; + + ImgUsages.emplace_back(VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT); + ImgUsages.emplace_back(VK_IMAGE_USAGE_TRANSFER_SRC_BIT); + + return ImgUsages; + } + + bool GetVulkanLayers(std::vector &VKLayers) + { + uint32_t LayerCount = 0; + VkResult Res = vkEnumerateInstanceLayerProperties(&LayerCount, NULL); + if(Res != VK_SUCCESS) + { + SetError("Could not get vulkan layers."); + return false; + } + + std::vector VKInstanceLayers(LayerCount); + Res = vkEnumerateInstanceLayerProperties(&LayerCount, VKInstanceLayers.data()); + if(Res != VK_SUCCESS) + { + SetError("Could not get vulkan layers."); + return false; + } + + std::set ReqLayerNames = OurVKLayers(); + VKLayers.clear(); + for(const auto &LayerName : VKInstanceLayers) + { + auto it = ReqLayerNames.find(std::string(LayerName.layerName)); + if(it != ReqLayerNames.end()) + { + VKLayers.emplace_back(LayerName.layerName); + } + } + + return true; + } + + bool CreateVulkanInstance(const std::vector &VKLayers, const std::vector &VKExtensions, bool TryDebugExtensions) + { + std::vector LayersCStr; + LayersCStr.reserve(VKLayers.size()); + for(const auto &Layer : VKLayers) + LayersCStr.emplace_back(Layer.c_str()); + + std::vector ExtCStr; + ExtCStr.reserve(VKExtensions.size() + 1); + for(const auto &Ext : VKExtensions) + ExtCStr.emplace_back(Ext.c_str()); + + if(TryDebugExtensions && (g_Config.m_DbgGfx == DEBUG_GFX_MODE_MINIMUM || g_Config.m_DbgGfx == DEBUG_GFX_MODE_ALL)) + { + // debug message support + ExtCStr.emplace_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME); + } + + VkApplicationInfo VKAppInfo = {}; + VKAppInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; + VKAppInfo.pNext = NULL; + VKAppInfo.pApplicationName = "DDNet"; + VKAppInfo.applicationVersion = 1; + VKAppInfo.pEngineName = "DDNet-Vulkan"; + VKAppInfo.engineVersion = 1; + VKAppInfo.apiVersion = VK_API_VERSION_1_0; + + void *pExt = nullptr; +#if defined(VK_EXT_validation_features) && VK_EXT_VALIDATION_FEATURES_SPEC_VERSION >= 5 + VkValidationFeaturesEXT Features = {}; + std::array aEnables = {VK_VALIDATION_FEATURE_ENABLE_SYNCHRONIZATION_VALIDATION_EXT, VK_VALIDATION_FEATURE_ENABLE_BEST_PRACTICES_EXT}; + if(TryDebugExtensions && (g_Config.m_DbgGfx == DEBUG_GFX_MODE_AFFECTS_PERFORMANCE || g_Config.m_DbgGfx == DEBUG_GFX_MODE_ALL)) + { + Features.sType = VK_STRUCTURE_TYPE_VALIDATION_FEATURES_EXT; + Features.enabledValidationFeatureCount = aEnables.size(); + Features.pEnabledValidationFeatures = aEnables.data(); + + pExt = &Features; + } +#endif + + VkInstanceCreateInfo VKInstanceInfo = {}; + VKInstanceInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; + VKInstanceInfo.pNext = pExt; + VKInstanceInfo.flags = 0; + VKInstanceInfo.pApplicationInfo = &VKAppInfo; + VKInstanceInfo.enabledExtensionCount = static_cast(ExtCStr.size()); + VKInstanceInfo.ppEnabledExtensionNames = ExtCStr.data(); + VKInstanceInfo.enabledLayerCount = static_cast(LayersCStr.size()); + VKInstanceInfo.ppEnabledLayerNames = LayersCStr.data(); + + bool TryAgain = false; + + VkResult Res = vkCreateInstance(&VKInstanceInfo, NULL, &m_VKInstance); + const char *pCritErrorMsg = CheckVulkanCriticalError(Res); + if(pCritErrorMsg != nullptr) + { + SetError("Creating instance failed.", pCritErrorMsg); + return false; + } + else if(Res == VK_ERROR_LAYER_NOT_PRESENT || Res == VK_ERROR_EXTENSION_NOT_PRESENT) + TryAgain = true; + + if(TryAgain && TryDebugExtensions) + return CreateVulkanInstance(VKLayers, VKExtensions, false); + + return true; + } + + bool SelectGPU(char *pRendererName, char *pVendorName, char *pVersionName) + { + uint32_t DevicesCount = 0; + vkEnumeratePhysicalDevices(m_VKInstance, &DevicesCount, nullptr); + if(DevicesCount == 0) + { + SetError("No vulkan compatible devices found."); + return false; + } + + std::vector DeviceList(DevicesCount); + vkEnumeratePhysicalDevices(m_VKInstance, &DevicesCount, DeviceList.data()); + + size_t Index = 0; + std::vector DevicePropList(DeviceList.size()); + m_pGPUList->m_GPUs.reserve(DeviceList.size()); + + size_t FoundDeviceIndex = 0; + size_t AutoGPUIndex = 0; + + bool IsAutoGPU = str_comp(g_Config.m_GfxGPUName, "auto") == 0; + + for(auto &CurDevice : DeviceList) + { + vkGetPhysicalDeviceProperties(CurDevice, &(DevicePropList[Index])); + + auto &DeviceProp = DevicePropList[Index]; + + STWGraphicGPU::STWGraphicGPUItem NewGPU; + str_copy(NewGPU.m_Name, DeviceProp.deviceName, minimum(sizeof(DeviceProp.deviceName), sizeof(NewGPU.m_Name))); + NewGPU.m_IsDiscreteGPU = DeviceProp.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU; + m_pGPUList->m_GPUs.push_back(NewGPU); + + Index++; + + int DevAPIMajor = (int)VK_API_VERSION_MAJOR(DeviceProp.apiVersion); + int DevAPIMinor = (int)VK_API_VERSION_MINOR(DeviceProp.apiVersion); + + if((AutoGPUIndex == 0 && DeviceProp.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) && (DevAPIMajor > gs_BackendVulkanMajor || (DevAPIMajor == gs_BackendVulkanMajor && DevAPIMinor >= gs_BackendVulkanMinor))) + { + str_copy(m_pGPUList->m_AutoGPU.m_Name, DeviceProp.deviceName, minimum(sizeof(DeviceProp.deviceName), sizeof(m_pGPUList->m_AutoGPU.m_Name))); + m_pGPUList->m_AutoGPU.m_IsDiscreteGPU = DeviceProp.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU; + + AutoGPUIndex = Index; + } + + if(((IsAutoGPU && FoundDeviceIndex == 0 && DeviceProp.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) || str_comp(DeviceProp.deviceName, g_Config.m_GfxGPUName) == 0) && (DevAPIMajor > gs_BackendVulkanMajor || (DevAPIMajor == gs_BackendVulkanMajor && DevAPIMinor >= gs_BackendVulkanMinor))) + { + FoundDeviceIndex = Index; + } + } + + if(FoundDeviceIndex == 0) + FoundDeviceIndex = 1; + + { + auto &DeviceProp = DevicePropList[FoundDeviceIndex - 1]; + + int DevAPIMajor = (int)VK_API_VERSION_MAJOR(DeviceProp.apiVersion); + int DevAPIMinor = (int)VK_API_VERSION_MINOR(DeviceProp.apiVersion); + int DevAPIPatch = (int)VK_API_VERSION_PATCH(DeviceProp.apiVersion); + + str_copy(pRendererName, DeviceProp.deviceName, gs_GPUInfoStringSize); + const char *pVendorNameStr = NULL; + switch(DeviceProp.vendorID) + { + case 0x1002: + pVendorNameStr = "AMD"; + break; + case 0x1010: + pVendorNameStr = "ImgTec"; + break; + case 0x106B: + pVendorNameStr = "Apple"; + break; + case 0x10DE: + pVendorNameStr = "NVIDIA"; + break; + case 0x13B5: + pVendorNameStr = "ARM"; + break; + case 0x5143: + pVendorNameStr = "Qualcomm"; + break; + case 0x8086: + pVendorNameStr = "INTEL"; + break; + case 0x10005: + pVendorNameStr = "Mesa"; + break; + default: + dbg_msg("vulkan", "unknown gpu vendor %u", DeviceProp.vendorID); + pVendorNameStr = "unknown"; + break; + } + str_copy(pVendorName, pVendorNameStr, gs_GPUInfoStringSize); + str_format(pVersionName, gs_GPUInfoStringSize, "Vulkan %d.%d.%d", DevAPIMajor, DevAPIMinor, DevAPIPatch); + + // get important device limits + m_NonCoherentMemAlignment = DeviceProp.limits.nonCoherentAtomSize; + m_OptimalImageCopyMemAlignment = DeviceProp.limits.optimalBufferCopyOffsetAlignment; + m_MaxTextureSize = DeviceProp.limits.maxImageDimension2D; + m_MaxSamplerAnisotropy = DeviceProp.limits.maxSamplerAnisotropy; + + m_MinUniformAlign = DeviceProp.limits.minUniformBufferOffsetAlignment; + m_MaxMultiSample = DeviceProp.limits.framebufferColorSampleCounts; + + if(IsVerbose()) + { + dbg_msg("vulkan", "device prop: non-coherent align: %zu, optimal image copy align: %zu, max texture size: %u, max sampler anisotropy: %u", (size_t)m_NonCoherentMemAlignment, (size_t)m_OptimalImageCopyMemAlignment, m_MaxTextureSize, m_MaxSamplerAnisotropy); + dbg_msg("vulkan", "device prop: min uniform align: %u, multi sample: %u", m_MinUniformAlign, (uint32_t)m_MaxMultiSample); + } + } + + VkPhysicalDevice CurDevice = DeviceList[FoundDeviceIndex - 1]; + + uint32_t FamQueueCount = 0; + vkGetPhysicalDeviceQueueFamilyProperties(CurDevice, &FamQueueCount, nullptr); + if(FamQueueCount == 0) + { + SetError("No vulkan queue family properties found."); + return false; + } + + std::vector QueuePropList(FamQueueCount); + vkGetPhysicalDeviceQueueFamilyProperties(CurDevice, &FamQueueCount, QueuePropList.data()); + + uint32_t QueueNodeIndex = std::numeric_limits::max(); + for(uint32_t i = 0; i < FamQueueCount; i++) + { + if(QueuePropList[i].queueCount > 0 && (QueuePropList[i].queueFlags & VK_QUEUE_GRAPHICS_BIT)) + { + QueueNodeIndex = i; + } + /*if(QueuePropList[i].queueCount > 0 && (QueuePropList[i].queueFlags & VK_QUEUE_COMPUTE_BIT)) + { + QueueNodeIndex = i; + }*/ + } + + if(QueueNodeIndex == std::numeric_limits::max()) + { + SetError("No vulkan queue found that matches the requirements: graphics queue"); + return false; + } + + m_VKGPU = CurDevice; + m_VKGraphicsQueueIndex = QueueNodeIndex; + return true; + } + + bool CreateLogicalDevice(const std::vector &VKLayers) + { + std::vector LayerCNames; + LayerCNames.reserve(VKLayers.size()); + for(const auto &Layer : VKLayers) + LayerCNames.emplace_back(Layer.c_str()); + + uint32_t DevPropCount = 0; + if(vkEnumerateDeviceExtensionProperties(m_VKGPU, NULL, &DevPropCount, NULL) != VK_SUCCESS) + { + SetError("Querying logical device extension propterties failed."); + return false; + } + + std::vector DevPropList(DevPropCount); + if(vkEnumerateDeviceExtensionProperties(m_VKGPU, NULL, &DevPropCount, DevPropList.data()) != VK_SUCCESS) + { + SetError("Querying logical device extension propterties failed."); + return false; + } + + std::vector DevPropCNames; + std::set OurDevExt = OurDeviceExtensions(); + + for(const auto &CurExtProp : DevPropList) + { + auto it = OurDevExt.find(std::string(CurExtProp.extensionName)); + if(it != OurDevExt.end()) + { + DevPropCNames.emplace_back(CurExtProp.extensionName); + } + } + + VkDeviceQueueCreateInfo VKQueueCreateInfo; + VKQueueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; + VKQueueCreateInfo.queueFamilyIndex = m_VKGraphicsQueueIndex; + VKQueueCreateInfo.queueCount = 1; + std::vector queue_prio = {1.0f}; + VKQueueCreateInfo.pQueuePriorities = queue_prio.data(); + VKQueueCreateInfo.pNext = NULL; + VKQueueCreateInfo.flags = 0; + + VkDeviceCreateInfo VKCreateInfo; + VKCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; + VKCreateInfo.queueCreateInfoCount = 1; + VKCreateInfo.pQueueCreateInfos = &VKQueueCreateInfo; + VKCreateInfo.ppEnabledLayerNames = LayerCNames.data(); + VKCreateInfo.enabledLayerCount = static_cast(LayerCNames.size()); + VKCreateInfo.ppEnabledExtensionNames = DevPropCNames.data(); + VKCreateInfo.enabledExtensionCount = static_cast(DevPropCNames.size()); + VKCreateInfo.pNext = NULL; + VKCreateInfo.pEnabledFeatures = NULL; + VKCreateInfo.flags = 0; + + VkResult res = vkCreateDevice(m_VKGPU, &VKCreateInfo, nullptr, &m_VKDevice); + if(res != VK_SUCCESS) + { + SetError("Logical device could not be created."); + return false; + } + + return true; + } + + bool CreateSurface(SDL_Window *pWindow) + { + if(!SDL_Vulkan_CreateSurface(pWindow, m_VKInstance, &m_VKPresentSurface)) + { + dbg_msg("vulkan", "error from sdl: %s", SDL_GetError()); + SetError("Creating a vulkan surface for the SDL window failed."); + return false; + } + + VkBool32 IsSupported = false; + vkGetPhysicalDeviceSurfaceSupportKHR(m_VKGPU, m_VKGraphicsQueueIndex, m_VKPresentSurface, &IsSupported); + if(!IsSupported) + { + SetError("The device surface does not support presenting the framebuffer to a screen. (maybe the wrong GPU was selected?)"); + return false; + } + + return true; + } + + void DestroySurface() + { + vkDestroySurfaceKHR(m_VKInstance, m_VKPresentSurface, nullptr); + } + + bool GetPresentationMode(VkPresentModeKHR &VKIOMode) + { + uint32_t PresentModeCount = 0; + if(vkGetPhysicalDeviceSurfacePresentModesKHR(m_VKGPU, m_VKPresentSurface, &PresentModeCount, NULL) != VK_SUCCESS) + { + SetError("The device surface presentation modes could not be fetched."); + return false; + } + + std::vector PresentModeList(PresentModeCount); + if(vkGetPhysicalDeviceSurfacePresentModesKHR(m_VKGPU, m_VKPresentSurface, &PresentModeCount, PresentModeList.data()) != VK_SUCCESS) + { + SetError("The device surface presentation modes could not be fetched."); + return false; + } + + VKIOMode = g_Config.m_GfxVsync ? VK_PRESENT_MODE_FIFO_KHR : VK_PRESENT_MODE_IMMEDIATE_KHR; + for(auto &Mode : PresentModeList) + { + if(Mode == VKIOMode) + return true; + } + + dbg_msg("vulkan", "warning: requested presentation mode was not available. falling back to mailbox / fifo relaxed."); + VKIOMode = g_Config.m_GfxVsync ? VK_PRESENT_MODE_FIFO_RELAXED_KHR : VK_PRESENT_MODE_MAILBOX_KHR; + for(auto &Mode : PresentModeList) + { + if(Mode == VKIOMode) + return true; + } + + dbg_msg("vulkan", "warning: requested presentation mode was not available. using first available."); + if(PresentModeCount > 0) + VKIOMode = PresentModeList[0]; + + return true; + } + + bool GetSurfaceProperties(VkSurfaceCapabilitiesKHR &VKSurfCapabilities) + { + if(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(m_VKGPU, m_VKPresentSurface, &VKSurfCapabilities) != VK_SUCCESS) + { + SetError("The device surface capabilities could not be fetched."); + return false; + } + return true; + } + + uint32_t GetNumberOfSwapImages(const VkSurfaceCapabilitiesKHR &VKCapabilities) + { + uint32_t ImgNumber = VKCapabilities.minImageCount + 1; + if(IsVerbose()) + { + dbg_msg("vulkan", "minimal swap image count %u", VKCapabilities.minImageCount); + } + return (VKCapabilities.maxImageCount > 0 && ImgNumber > VKCapabilities.maxImageCount) ? VKCapabilities.maxImageCount : ImgNumber; + } + + SSwapImgViewportExtent GetSwapImageSize(const VkSurfaceCapabilitiesKHR &VKCapabilities) + { + VkExtent2D RetSize = {m_CanvasWidth, m_CanvasHeight}; + + if(VKCapabilities.currentExtent.width == std::numeric_limits::max()) + { + RetSize.width = clamp(RetSize.width, VKCapabilities.minImageExtent.width, VKCapabilities.maxImageExtent.width); + RetSize.height = clamp(RetSize.height, VKCapabilities.minImageExtent.height, VKCapabilities.maxImageExtent.height); + } + else + { + RetSize = VKCapabilities.currentExtent; + } + + VkExtent2D AutoViewportExtent = RetSize; + // keep this in sync with graphics_threaded AdjustViewport's check + if(AutoViewportExtent.height > 4 * AutoViewportExtent.width / 5) + AutoViewportExtent.height = 4 * AutoViewportExtent.width / 5; + + return {RetSize, AutoViewportExtent}; + } + + bool GetImageUsage(const VkSurfaceCapabilitiesKHR &VKCapabilities, VkImageUsageFlags &VKOutUsage) + { + std::vector OurImgUsages = OurImageUsages(); + if(OurImgUsages.empty()) + { + SetError("Framebuffer image attachment types not supported."); + return false; + } + + VKOutUsage = OurImgUsages[0]; + + for(const auto &ImgUsage : OurImgUsages) + { + VkImageUsageFlags ImgUsageFlags = ImgUsage & VKCapabilities.supportedUsageFlags; + if(ImgUsageFlags != ImgUsage) + { + SetError("Framebuffer image attachment types not supported."); + return false; + } + + VKOutUsage = (VKOutUsage | ImgUsage); + } + + return true; + } + + VkSurfaceTransformFlagBitsKHR GetTransform(const VkSurfaceCapabilitiesKHR &VKCapabilities) + { + if(VKCapabilities.supportedTransforms & VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR) + return VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR; + return VKCapabilities.currentTransform; + } + + bool GetFormat() + { + uint32_t SurfFormats = 0; + VkResult Res = vkGetPhysicalDeviceSurfaceFormatsKHR(m_VKGPU, m_VKPresentSurface, &SurfFormats, nullptr); + if(Res != VK_SUCCESS && Res != VK_INCOMPLETE) + { + SetError("The device surface format fetching failed."); + return false; + } + + std::vector SurfFormatList(SurfFormats); + Res = vkGetPhysicalDeviceSurfaceFormatsKHR(m_VKGPU, m_VKPresentSurface, &SurfFormats, SurfFormatList.data()); + if(Res != VK_SUCCESS && Res != VK_INCOMPLETE) + { + SetError("The device surface format fetching failed."); + return false; + } + + if(Res == VK_INCOMPLETE) + { + dbg_msg("vulkan", "warning: not all surface formats are requestable with your current settings."); + } + + if(SurfFormatList.size() == 1 && SurfFormatList[0].format == VK_FORMAT_UNDEFINED) + { + m_VKSurfFormat.format = VK_FORMAT_B8G8R8A8_UNORM; + m_VKSurfFormat.colorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; + dbg_msg("vulkan", "warning: surface format was undefined. This can potentially cause bugs."); + return true; + } + + for(const auto &FindFormat : SurfFormatList) + { + if(FindFormat.format == VK_FORMAT_B8G8R8A8_UNORM && FindFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) + { + m_VKSurfFormat = FindFormat; + return true; + } + else if(FindFormat.format == VK_FORMAT_R8G8B8A8_UNORM && FindFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) + { + m_VKSurfFormat = FindFormat; + return true; + } + } + + dbg_msg("vulkan", "warning: surface format was not RGBA(or variants of it). This can potentially cause weird looking images(too bright etc.)."); + m_VKSurfFormat = SurfFormatList[0]; + return true; + } + + bool CreateSwapChain(VkSwapchainKHR &OldSwapChain) + { + VkSurfaceCapabilitiesKHR VKSurfCap; + if(!GetSurfaceProperties(VKSurfCap)) + return false; + + VkPresentModeKHR PresentMode = VK_PRESENT_MODE_IMMEDIATE_KHR; + if(!GetPresentationMode(PresentMode)) + return false; + + uint32_t SwapImgCount = GetNumberOfSwapImages(VKSurfCap); + + m_VKSwapImgAndViewportExtent = GetSwapImageSize(VKSurfCap); + + VkImageUsageFlags UsageFlags; + if(!GetImageUsage(VKSurfCap, UsageFlags)) + return false; + + VkSurfaceTransformFlagBitsKHR TransformFlagBits = GetTransform(VKSurfCap); + + if(!GetFormat()) + return false; + + OldSwapChain = m_VKSwapChain; + + VkSwapchainCreateInfoKHR SwapInfo; + SwapInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR; + SwapInfo.pNext = nullptr; + SwapInfo.flags = 0; + SwapInfo.surface = m_VKPresentSurface; + SwapInfo.minImageCount = SwapImgCount; + SwapInfo.imageFormat = m_VKSurfFormat.format; + SwapInfo.imageColorSpace = m_VKSurfFormat.colorSpace; + SwapInfo.imageExtent = m_VKSwapImgAndViewportExtent.m_SwapImg; + SwapInfo.imageArrayLayers = 1; + SwapInfo.imageUsage = UsageFlags; + SwapInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; + SwapInfo.queueFamilyIndexCount = 0; + SwapInfo.pQueueFamilyIndices = nullptr; + SwapInfo.preTransform = TransformFlagBits; + SwapInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; + SwapInfo.presentMode = PresentMode; + SwapInfo.clipped = true; + SwapInfo.oldSwapchain = OldSwapChain; + + m_VKSwapChain = VK_NULL_HANDLE; + VkResult SwapchainCreateRes = vkCreateSwapchainKHR(m_VKDevice, &SwapInfo, nullptr, &m_VKSwapChain); + const char *pCritErrorMsg = CheckVulkanCriticalError(SwapchainCreateRes); + if(pCritErrorMsg != nullptr) + { + SetError("Creating the swap chain failed.", pCritErrorMsg); + return false; + } + else if(SwapchainCreateRes == VK_ERROR_NATIVE_WINDOW_IN_USE_KHR) + return false; + + return true; + } + + void DestroySwapChain(bool ForceDestroy) + { + if(ForceDestroy) + { + vkDestroySwapchainKHR(m_VKDevice, m_VKSwapChain, nullptr); + m_VKSwapChain = VK_NULL_HANDLE; + } + } + + bool GetSwapChainImageHandles() + { + uint32_t ImgCount = 0; + VkResult res = vkGetSwapchainImagesKHR(m_VKDevice, m_VKSwapChain, &ImgCount, nullptr); + if(res != VK_SUCCESS) + { + SetError("Could not get swap chain images."); + return false; + } + + m_SwapChainImageCount = ImgCount; + + m_SwapChainImages.resize(ImgCount); + if(vkGetSwapchainImagesKHR(m_VKDevice, m_VKSwapChain, &ImgCount, m_SwapChainImages.data()) != VK_SUCCESS) + { + SetError("Could not get swap chain images."); + return false; + } + + return true; + } + + void ClearSwapChainImageHandles() + { + m_SwapChainImages.clear(); + } + + void GetDeviceQueue() + { + vkGetDeviceQueue(m_VKDevice, m_VKGraphicsQueueIndex, 0, &m_VKGraphicsQueue); + vkGetDeviceQueue(m_VKDevice, m_VKGraphicsQueueIndex, 0, &m_VKPresentQueue); + } + + static VKAPI_ATTR VkBool32 VKAPI_CALL VKDebugCallback(VkDebugUtilsMessageSeverityFlagBitsEXT MessageSeverity, VkDebugUtilsMessageTypeFlagsEXT MessageType, const VkDebugUtilsMessengerCallbackDataEXT *pCallbackData, void *pUserData) + { + if((MessageSeverity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) != 0) + { + dbg_msg("vulkan_debug", "validation error: %s", pCallbackData->pMessage); + } + else + { + dbg_msg("vulkan_debug", "%s", pCallbackData->pMessage); + } + + return VK_FALSE; + } + + VkResult CreateDebugUtilsMessengerEXT(const VkDebugUtilsMessengerCreateInfoEXT *pCreateInfo, const VkAllocationCallbacks *pAllocator, VkDebugUtilsMessengerEXT *pDebugMessenger) + { + auto func = (PFN_vkCreateDebugUtilsMessengerEXT)vkGetInstanceProcAddr(m_VKInstance, "vkCreateDebugUtilsMessengerEXT"); + if(func != nullptr) + { + return func(m_VKInstance, pCreateInfo, pAllocator, pDebugMessenger); + } + else + { + return VK_ERROR_EXTENSION_NOT_PRESENT; + } + } + + void DestroyDebugUtilsMessengerEXT(VkDebugUtilsMessengerEXT &DebugMessenger) + { + auto func = (PFN_vkDestroyDebugUtilsMessengerEXT)vkGetInstanceProcAddr(m_VKInstance, "vkDestroyDebugUtilsMessengerEXT"); + if(func != nullptr) + { + func(m_VKInstance, DebugMessenger, nullptr); + } + } + + void SetupDebugCallback() + { + VkDebugUtilsMessengerCreateInfoEXT CreateInfo = {}; + CreateInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT; + CreateInfo.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT; + CreateInfo.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT; // | VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT <- too annoying + CreateInfo.pfnUserCallback = VKDebugCallback; + + if(CreateDebugUtilsMessengerEXT(&CreateInfo, nullptr, &m_DebugMessenger) != VK_SUCCESS) + { + m_DebugMessenger = VK_NULL_HANDLE; + dbg_msg("vulkan", "didn't find vulkan debug layer."); + } + else + { + dbg_msg("vulkan", "enabled vulkan debug context."); + } + } + + void UnregisterDebugCallback() + { + if(m_DebugMessenger != VK_NULL_HANDLE) + DestroyDebugUtilsMessengerEXT(m_DebugMessenger); + } + + bool CreateImageViews() + { + m_SwapChainImageViewList.resize(m_SwapChainImageCount); + + for(size_t i = 0; i < m_SwapChainImageCount; i++) + { + VkImageViewCreateInfo CreateInfo{}; + CreateInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + CreateInfo.image = m_SwapChainImages[i]; + CreateInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + CreateInfo.format = m_VKSurfFormat.format; + CreateInfo.components.r = VK_COMPONENT_SWIZZLE_IDENTITY; + CreateInfo.components.g = VK_COMPONENT_SWIZZLE_IDENTITY; + CreateInfo.components.b = VK_COMPONENT_SWIZZLE_IDENTITY; + CreateInfo.components.a = VK_COMPONENT_SWIZZLE_IDENTITY; + CreateInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + CreateInfo.subresourceRange.baseMipLevel = 0; + CreateInfo.subresourceRange.levelCount = 1; + CreateInfo.subresourceRange.baseArrayLayer = 0; + CreateInfo.subresourceRange.layerCount = 1; + + if(vkCreateImageView(m_VKDevice, &CreateInfo, nullptr, &m_SwapChainImageViewList[i]) != VK_SUCCESS) + { + SetError("Could not create image views for the swap chain framebuffers."); + return false; + } + } + + return true; + } + + void DestroyImageViews() + { + for(auto &ImageView : m_SwapChainImageViewList) + { + vkDestroyImageView(m_VKDevice, ImageView, nullptr); + } + + m_SwapChainImageViewList.clear(); + } + + bool CreateRenderPass(bool ClearAttachs) + { + VkAttachmentDescription ColorAttachment{}; + ColorAttachment.format = m_VKSurfFormat.format; + ColorAttachment.samples = GetSampleCount(); + ColorAttachment.loadOp = ClearAttachs ? VK_ATTACHMENT_LOAD_OP_CLEAR : VK_ATTACHMENT_LOAD_OP_DONT_CARE; + ColorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + ColorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + ColorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + ColorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + ColorAttachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + + VkAttachmentReference ColorAttachmentRef{}; + ColorAttachmentRef.attachment = 0; + ColorAttachmentRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + + VkSubpassDescription Subpass{}; + Subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + Subpass.colorAttachmentCount = 1; + Subpass.pColorAttachments = &ColorAttachmentRef; + + VkSubpassDependency Dependency{}; + Dependency.srcSubpass = VK_SUBPASS_EXTERNAL; + Dependency.dstSubpass = 0; + Dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + Dependency.srcAccessMask = 0; + Dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + Dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + + VkRenderPassCreateInfo CreateRenderPassInfo{}; + CreateRenderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; + CreateRenderPassInfo.attachmentCount = 1; + CreateRenderPassInfo.pAttachments = &ColorAttachment; + CreateRenderPassInfo.subpassCount = 1; + CreateRenderPassInfo.pSubpasses = &Subpass; + CreateRenderPassInfo.dependencyCount = 1; + CreateRenderPassInfo.pDependencies = &Dependency; + + if(vkCreateRenderPass(m_VKDevice, &CreateRenderPassInfo, nullptr, &m_VKRenderPass) != VK_SUCCESS) + { + SetError("Creating the render pass failed."); + return false; + } + + return true; + } + + void DestroyRenderPass() + { + vkDestroyRenderPass(m_VKDevice, m_VKRenderPass, nullptr); + } + + bool CreateFramebuffers() + { + m_FramebufferList.resize(m_SwapChainImageCount); + + for(size_t i = 0; i < m_SwapChainImageCount; i++) + { + std::array aAttachments = {m_SwapChainImageViewList[i]}; + + VkFramebufferCreateInfo FramebufferInfo{}; + FramebufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; + FramebufferInfo.renderPass = m_VKRenderPass; + FramebufferInfo.attachmentCount = aAttachments.size(); + FramebufferInfo.pAttachments = aAttachments.data(); + FramebufferInfo.width = m_VKSwapImgAndViewportExtent.m_SwapImg.width; + FramebufferInfo.height = m_VKSwapImgAndViewportExtent.m_SwapImg.height; + FramebufferInfo.layers = 1; + + if(vkCreateFramebuffer(m_VKDevice, &FramebufferInfo, nullptr, &m_FramebufferList[i]) != VK_SUCCESS) + { + SetError("Creating the framebuffers failed."); + return false; + } + } + + return true; + } + + void DestroyFramebuffers() + { + for(auto &FrameBuffer : m_FramebufferList) + { + vkDestroyFramebuffer(m_VKDevice, FrameBuffer, nullptr); + } + + m_FramebufferList.clear(); + } + + bool CreateShaderModule(const std::vector &Code, VkShaderModule &ShaderModule) + { + VkShaderModuleCreateInfo CreateInfo{}; + CreateInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; + CreateInfo.codeSize = Code.size(); + CreateInfo.pCode = (const uint32_t *)(Code.data()); + + if(vkCreateShaderModule(m_VKDevice, &CreateInfo, nullptr, &ShaderModule) != VK_SUCCESS) + { + SetError("Shader module was not created."); + return false; + } + + return true; + } + + bool CreateDescriptorSetLayouts() + { + VkDescriptorSetLayoutBinding SamplerLayoutBinding{}; + SamplerLayoutBinding.binding = 0; + SamplerLayoutBinding.descriptorCount = 1; + SamplerLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + SamplerLayoutBinding.pImmutableSamplers = nullptr; + SamplerLayoutBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + + std::array aBindings = {SamplerLayoutBinding}; + VkDescriptorSetLayoutCreateInfo LayoutInfo{}; + LayoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; + LayoutInfo.bindingCount = aBindings.size(); + LayoutInfo.pBindings = aBindings.data(); + + if(vkCreateDescriptorSetLayout(m_VKDevice, &LayoutInfo, nullptr, &m_StandardTexturedDescriptorSetLayout) != VK_SUCCESS) + { + SetError("Creating descriptor layout failed."); + return false; + } + + if(vkCreateDescriptorSetLayout(m_VKDevice, &LayoutInfo, nullptr, &m_Standard3DTexturedDescriptorSetLayout) != VK_SUCCESS) + { + SetError("Creating descriptor layout failed."); + return false; + } + return true; + } + + void DestroyDescriptorSetLayouts() + { + vkDestroyDescriptorSetLayout(m_VKDevice, m_StandardTexturedDescriptorSetLayout, nullptr); + vkDestroyDescriptorSetLayout(m_VKDevice, m_Standard3DTexturedDescriptorSetLayout, nullptr); + } + + bool LoadShader(const char *pFileName, std::vector *&pShaderData) + { + auto it = m_ShaderFiles.find(pFileName); + if(it == m_ShaderFiles.end()) + { + auto *pShaderCodeFile = m_pStorage->OpenFile(pFileName, IOFLAG_READ, IStorage::TYPE_ALL); + + std::vector ShaderBuff; + if(pShaderCodeFile) + { + long FileSize = io_length(pShaderCodeFile); + ShaderBuff.resize(FileSize); + io_read(pShaderCodeFile, ShaderBuff.data(), FileSize); + io_close(pShaderCodeFile); + } + else + return false; + + it = m_ShaderFiles.insert({pFileName, {std::move(ShaderBuff)}}).first; + } + + pShaderData = &it->second.m_Binary; + + return true; + } + + bool CreateShaders(const char *pVertName, const char *pFragName, VkPipelineShaderStageCreateInfo (&aShaderStages)[2], SShaderModule &ShaderModule) + { + bool ShaderLoaded = true; + + std::vector *pVertBuff; + std::vector *pFragBuff; + ShaderLoaded &= LoadShader(pVertName, pVertBuff); + ShaderLoaded &= LoadShader(pFragName, pFragBuff); + + if(!ShaderLoaded) + { + SetError("A shader file could not load correctly"); + return false; + } + + if(!CreateShaderModule(*pVertBuff, ShaderModule.m_VertShaderModule)) + return false; + + if(!CreateShaderModule(*pFragBuff, ShaderModule.m_FragShaderModule)) + return false; + + VkPipelineShaderStageCreateInfo &VertShaderStageInfo = aShaderStages[0]; + VertShaderStageInfo = {}; + VertShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + VertShaderStageInfo.stage = VK_SHADER_STAGE_VERTEX_BIT; + VertShaderStageInfo.module = ShaderModule.m_VertShaderModule; + VertShaderStageInfo.pName = "main"; + + VkPipelineShaderStageCreateInfo &FragShaderStageInfo = aShaderStages[1]; + FragShaderStageInfo = {}; + FragShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + FragShaderStageInfo.stage = VK_SHADER_STAGE_FRAGMENT_BIT; + FragShaderStageInfo.module = ShaderModule.m_FragShaderModule; + FragShaderStageInfo.pName = "main"; + + ShaderModule.m_VKDevice = m_VKDevice; + return true; + } + + bool GetStandardPipelineInfo(VkPipelineInputAssemblyStateCreateInfo &InputAssembly, + VkViewport &Viewport, + VkRect2D &Scissor, + VkPipelineViewportStateCreateInfo &ViewportState, + VkPipelineRasterizationStateCreateInfo &Rasterizer, + VkPipelineMultisampleStateCreateInfo &Multisampling, + VkPipelineColorBlendAttachmentState &ColorBlendAttachment, + VkPipelineColorBlendStateCreateInfo &ColorBlending) + { + InputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; + InputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + InputAssembly.primitiveRestartEnable = VK_FALSE; + + Viewport.x = 0.0f; + Viewport.y = 0.0f; + Viewport.width = (float)m_VKSwapImgAndViewportExtent.m_Viewport.width; + Viewport.height = (float)m_VKSwapImgAndViewportExtent.m_Viewport.height; + Viewport.minDepth = 0.0f; + Viewport.maxDepth = 1.0f; + + Scissor.offset = {0, 0}; + Scissor.extent = m_VKSwapImgAndViewportExtent.m_Viewport; + + ViewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; + ViewportState.viewportCount = 1; + ViewportState.pViewports = &Viewport; + ViewportState.scissorCount = 1; + ViewportState.pScissors = &Scissor; + + Rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; + Rasterizer.depthClampEnable = VK_FALSE; + Rasterizer.rasterizerDiscardEnable = VK_FALSE; + Rasterizer.polygonMode = VK_POLYGON_MODE_FILL; + Rasterizer.lineWidth = 1.0f; + Rasterizer.cullMode = VK_CULL_MODE_NONE; + Rasterizer.frontFace = VK_FRONT_FACE_CLOCKWISE; + Rasterizer.depthBiasEnable = VK_FALSE; + + Multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; + Multisampling.sampleShadingEnable = VK_FALSE; + Multisampling.rasterizationSamples = GetSampleCount(); + + ColorBlendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; + ColorBlendAttachment.blendEnable = VK_TRUE; + + ColorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; + ColorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + ColorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD; + ColorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; + ColorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + ColorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD; + + ColorBlending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; + ColorBlending.logicOpEnable = VK_FALSE; + ColorBlending.logicOp = VK_LOGIC_OP_COPY; + ColorBlending.attachmentCount = 1; + ColorBlending.pAttachments = &ColorBlendAttachment; + ColorBlending.blendConstants[0] = 0.0f; + ColorBlending.blendConstants[1] = 0.0f; + ColorBlending.blendConstants[2] = 0.0f; + ColorBlending.blendConstants[3] = 0.0f; + + return true; + } + + template + bool CreateGraphicsPipeline(const char *pVertName, const char *pFragName, SPipelineContainer &PipeContainer, uint32_t Stride, std::array &aInputAttr, + std::array &aSetLayouts, std::array &aPushConstants, EVulkanBackendTextureModes TexMode, + EVulkanBackendBlendModes BlendMode, EVulkanBackendClipModes DynamicMode, bool IsLinePrim = false) + { + VkPipelineShaderStageCreateInfo aShaderStages[2]; + SShaderModule Module; + if(!CreateShaders(pVertName, pFragName, aShaderStages, Module)) + return false; + + bool HasSampler = TexMode == VULKAN_BACKEND_TEXTURE_MODE_TEXTURED; + + VkPipelineVertexInputStateCreateInfo VertexInputInfo{}; + VertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO; + VkVertexInputBindingDescription BindingDescription{}; + BindingDescription.binding = 0; + BindingDescription.stride = Stride; + BindingDescription.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + + VertexInputInfo.vertexBindingDescriptionCount = 1; + VertexInputInfo.vertexAttributeDescriptionCount = aInputAttr.size(); + VertexInputInfo.pVertexBindingDescriptions = &BindingDescription; + VertexInputInfo.pVertexAttributeDescriptions = aInputAttr.data(); + + VkPipelineInputAssemblyStateCreateInfo InputAssembly{}; + VkViewport Viewport{}; + VkRect2D Scissor{}; + VkPipelineViewportStateCreateInfo ViewportState{}; + VkPipelineRasterizationStateCreateInfo Rasterizer{}; + VkPipelineMultisampleStateCreateInfo Multisampling{}; + VkPipelineColorBlendAttachmentState ColorBlendAttachment{}; + VkPipelineColorBlendStateCreateInfo ColorBlending{}; + + GetStandardPipelineInfo(InputAssembly, Viewport, Scissor, ViewportState, Rasterizer, Multisampling, ColorBlendAttachment, ColorBlending); + InputAssembly.topology = IsLinePrim ? VK_PRIMITIVE_TOPOLOGY_LINE_LIST : VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + + VkPipelineLayoutCreateInfo PipelineLayoutInfo{}; + PipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; + PipelineLayoutInfo.setLayoutCount = (HasSampler || ForceRequireDescriptors) ? aSetLayouts.size() : 0; + PipelineLayoutInfo.pSetLayouts = (HasSampler || ForceRequireDescriptors) && !aSetLayouts.empty() ? aSetLayouts.data() : nullptr; + + PipelineLayoutInfo.pushConstantRangeCount = aPushConstants.size(); + PipelineLayoutInfo.pPushConstantRanges = !aPushConstants.empty() ? aPushConstants.data() : nullptr; + + VkPipelineLayout &PipeLayout = GetPipeLayout(PipeContainer, HasSampler, size_t(BlendMode), size_t(DynamicMode)); + VkPipeline &Pipeline = GetPipeline(PipeContainer, HasSampler, size_t(BlendMode), size_t(DynamicMode)); + + if(vkCreatePipelineLayout(m_VKDevice, &PipelineLayoutInfo, nullptr, &PipeLayout) != VK_SUCCESS) + { + SetError("Creating pipeline layout failed."); + return false; + } + + VkGraphicsPipelineCreateInfo PipelineInfo{}; + PipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; + PipelineInfo.stageCount = 2; + PipelineInfo.pStages = aShaderStages; + PipelineInfo.pVertexInputState = &VertexInputInfo; + PipelineInfo.pInputAssemblyState = &InputAssembly; + PipelineInfo.pViewportState = &ViewportState; + PipelineInfo.pRasterizationState = &Rasterizer; + PipelineInfo.pMultisampleState = &Multisampling; + PipelineInfo.pColorBlendState = &ColorBlending; + PipelineInfo.layout = PipeLayout; + PipelineInfo.renderPass = m_VKRenderPass; + PipelineInfo.subpass = 0; + PipelineInfo.basePipelineHandle = VK_NULL_HANDLE; + + std::array aDynamicStates = { + VK_DYNAMIC_STATE_VIEWPORT, + VK_DYNAMIC_STATE_SCISSOR, + }; + + VkPipelineDynamicStateCreateInfo DynamicStateCreate{}; + DynamicStateCreate.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; + DynamicStateCreate.dynamicStateCount = aDynamicStates.size(); + DynamicStateCreate.pDynamicStates = aDynamicStates.data(); + + if(DynamicMode == VULKAN_BACKEND_CLIP_MODE_DYNAMIC_SCISSOR_AND_VIEWPORT) + { + PipelineInfo.pDynamicState = &DynamicStateCreate; + } + + if(vkCreateGraphicsPipelines(m_VKDevice, VK_NULL_HANDLE, 1, &PipelineInfo, nullptr, &Pipeline) != VK_SUCCESS) + { + SetError("Creating the graphic pipeline failed."); + return false; + } + + return true; + } + + bool CreateStandardGraphicsPipelineImpl(const char *pVertName, const char *pFragName, SPipelineContainer &PipeContainer, EVulkanBackendTextureModes TexMode, EVulkanBackendBlendModes BlendMode, EVulkanBackendClipModes DynamicMode, bool IsLinePrim) + { + std::array aAttributeDescriptions = {}; + + aAttributeDescriptions[0] = {0, 0, VK_FORMAT_R32G32_SFLOAT, 0}; + aAttributeDescriptions[1] = {1, 0, VK_FORMAT_R32G32_SFLOAT, sizeof(float) * 2}; + aAttributeDescriptions[2] = {2, 0, VK_FORMAT_R8G8B8A8_UNORM, sizeof(float) * (2 + 2)}; + + std::array aSetLayouts = {m_StandardTexturedDescriptorSetLayout}; + + std::array aPushConstants{}; + aPushConstants[0] = {VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(SUniformGPos)}; + + return CreateGraphicsPipeline(pVertName, pFragName, PipeContainer, sizeof(float) * (2 + 2) + sizeof(uint8_t) * 4, aAttributeDescriptions, aSetLayouts, aPushConstants, TexMode, BlendMode, DynamicMode, IsLinePrim); + } + + bool CreateStandardGraphicsPipeline(const char *pVertName, const char *pFragName, bool HasSampler, bool IsLinePipe) + { + bool Ret = true; + + EVulkanBackendTextureModes TexMode = HasSampler ? VULKAN_BACKEND_TEXTURE_MODE_TEXTURED : VULKAN_BACKEND_TEXTURE_MODE_NOT_TEXTURED; + + for(size_t i = 0; i < VULKAN_BACKEND_BLEND_MODE_COUNT; ++i) + { + for(size_t j = 0; j < VULKAN_BACKEND_CLIP_MODE_COUNT; ++j) + { + Ret &= CreateStandardGraphicsPipelineImpl(pVertName, pFragName, IsLinePipe ? m_StandardLinePipeline : m_StandardPipeline, TexMode, EVulkanBackendBlendModes(i), EVulkanBackendClipModes(j), IsLinePipe); + } + } + + return Ret; + } + + bool CreateStandard3DGraphicsPipelineImpl(const char *pVertName, const char *pFragName, SPipelineContainer &PipeContainer, EVulkanBackendTextureModes TexMode, EVulkanBackendBlendModes BlendMode, EVulkanBackendClipModes DynamicMode) + { + std::array aAttributeDescriptions = {}; + + aAttributeDescriptions[0] = {0, 0, VK_FORMAT_R32G32_SFLOAT, 0}; + aAttributeDescriptions[1] = {1, 0, VK_FORMAT_R8G8B8A8_UNORM, sizeof(float) * 2}; + aAttributeDescriptions[2] = {2, 0, VK_FORMAT_R32G32B32_SFLOAT, sizeof(float) * 2 + sizeof(uint8_t) * 4}; + + std::array aSetLayouts = {m_Standard3DTexturedDescriptorSetLayout}; + + std::array aPushConstants{}; + aPushConstants[0] = {VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(SUniformGPos)}; + + return CreateGraphicsPipeline(pVertName, pFragName, PipeContainer, sizeof(float) * 2 + sizeof(uint8_t) * 4 + sizeof(float) * 3, aAttributeDescriptions, aSetLayouts, aPushConstants, TexMode, BlendMode, DynamicMode); + } + + bool CreateStandard3DGraphicsPipeline(const char *pVertName, const char *pFragName, bool HasSampler) + { + bool Ret = true; + + EVulkanBackendTextureModes TexMode = HasSampler ? VULKAN_BACKEND_TEXTURE_MODE_TEXTURED : VULKAN_BACKEND_TEXTURE_MODE_NOT_TEXTURED; + + for(size_t i = 0; i < VULKAN_BACKEND_BLEND_MODE_COUNT; ++i) + { + for(size_t j = 0; j < VULKAN_BACKEND_CLIP_MODE_COUNT; ++j) + { + Ret &= CreateStandard3DGraphicsPipelineImpl(pVertName, pFragName, m_Standard3DPipeline, TexMode, EVulkanBackendBlendModes(i), EVulkanBackendClipModes(j)); + } + } + + return Ret; + } + + bool CreateTextDescriptorSetLayout() + { + VkDescriptorSetLayoutBinding SamplerLayoutBinding{}; + SamplerLayoutBinding.binding = 0; + SamplerLayoutBinding.descriptorCount = 1; + SamplerLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + SamplerLayoutBinding.pImmutableSamplers = nullptr; + SamplerLayoutBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + + auto SamplerLayoutBinding2 = SamplerLayoutBinding; + SamplerLayoutBinding2.binding = 1; + + std::array aBindings = {SamplerLayoutBinding, SamplerLayoutBinding2}; + VkDescriptorSetLayoutCreateInfo LayoutInfo{}; + LayoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; + LayoutInfo.bindingCount = aBindings.size(); + LayoutInfo.pBindings = aBindings.data(); + + if(vkCreateDescriptorSetLayout(m_VKDevice, &LayoutInfo, nullptr, &m_TextDescriptorSetLayout) != VK_SUCCESS) + { + SetError("Creating descriptor layout failed."); + return false; + } + + return true; + } + + void DestroyTextDescriptorSetLayout() + { + vkDestroyDescriptorSetLayout(m_VKDevice, m_TextDescriptorSetLayout, nullptr); + } + + bool CreateTextGraphicsPipelineImpl(const char *pVertName, const char *pFragName, SPipelineContainer &PipeContainer, EVulkanBackendTextureModes TexMode, EVulkanBackendBlendModes BlendMode, EVulkanBackendClipModes DynamicMode) + { + std::array aAttributeDescriptions = {}; + aAttributeDescriptions[0] = {0, 0, VK_FORMAT_R32G32_SFLOAT, 0}; + aAttributeDescriptions[1] = {1, 0, VK_FORMAT_R32G32_SFLOAT, sizeof(float) * 2}; + aAttributeDescriptions[2] = {2, 0, VK_FORMAT_R8G8B8A8_UNORM, sizeof(float) * (2 + 2)}; + + std::array aSetLayouts = {m_TextDescriptorSetLayout}; + + std::array aPushConstants{}; + aPushConstants[0] = {VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(SUniformGTextPos)}; + aPushConstants[1] = {VK_SHADER_STAGE_FRAGMENT_BIT, sizeof(SUniformGTextPos) + sizeof(SUniformTextGFragmentOffset), sizeof(SUniformTextGFragmentConstants)}; + + return CreateGraphicsPipeline(pVertName, pFragName, PipeContainer, sizeof(float) * (2 + 2) + sizeof(uint8_t) * 4, aAttributeDescriptions, aSetLayouts, aPushConstants, TexMode, BlendMode, DynamicMode); + } + + bool CreateTextGraphicsPipeline(const char *pVertName, const char *pFragName) + { + bool Ret = true; + + EVulkanBackendTextureModes TexMode = VULKAN_BACKEND_TEXTURE_MODE_TEXTURED; + + for(size_t i = 0; i < VULKAN_BACKEND_BLEND_MODE_COUNT; ++i) + { + for(size_t j = 0; j < VULKAN_BACKEND_CLIP_MODE_COUNT; ++j) + { + Ret &= CreateTextGraphicsPipelineImpl(pVertName, pFragName, m_TextPipeline, TexMode, EVulkanBackendBlendModes(i), EVulkanBackendClipModes(j)); + } + } + + return Ret; + } + + template + bool CreateTileGraphicsPipelineImpl(const char *pVertName, const char *pFragName, int Type, SPipelineContainer &PipeContainer, EVulkanBackendTextureModes TexMode, EVulkanBackendBlendModes BlendMode, EVulkanBackendClipModes DynamicMode) + { + std::array aAttributeDescriptions = {}; + aAttributeDescriptions[0] = {0, 0, VK_FORMAT_R32G32_SFLOAT, 0}; + if(HasSampler) + aAttributeDescriptions[1] = {1, 0, VK_FORMAT_R32G32B32_SFLOAT, sizeof(float) * 2}; + + std::array aSetLayouts; + aSetLayouts[0] = m_Standard3DTexturedDescriptorSetLayout; + + uint32_t VertPushConstantSize = sizeof(SUniformTileGPos); + if(Type == 1) + VertPushConstantSize = sizeof(SUniformTileGPosBorder); + else if(Type == 2) + VertPushConstantSize = sizeof(SUniformTileGPosBorderLine); + + uint32_t FragPushConstantSize = sizeof(SUniformTileGVertColor); + + std::array aPushConstants{}; + aPushConstants[0] = {VK_SHADER_STAGE_VERTEX_BIT, 0, VertPushConstantSize}; + aPushConstants[1] = {VK_SHADER_STAGE_FRAGMENT_BIT, sizeof(SUniformTileGPosBorder) + sizeof(SUniformTileGVertColorAlign), FragPushConstantSize}; + + return CreateGraphicsPipeline(pVertName, pFragName, PipeContainer, HasSampler ? (sizeof(float) * (2 + 3)) : (sizeof(float) * 2), aAttributeDescriptions, aSetLayouts, aPushConstants, TexMode, BlendMode, DynamicMode); + } + + template + bool CreateTileGraphicsPipeline(const char *pVertName, const char *pFragName, int Type) + { + bool Ret = true; + + EVulkanBackendTextureModes TexMode = HasSampler ? VULKAN_BACKEND_TEXTURE_MODE_TEXTURED : VULKAN_BACKEND_TEXTURE_MODE_NOT_TEXTURED; + + for(size_t i = 0; i < VULKAN_BACKEND_BLEND_MODE_COUNT; ++i) + { + for(size_t j = 0; j < VULKAN_BACKEND_CLIP_MODE_COUNT; ++j) + { + Ret &= CreateTileGraphicsPipelineImpl(pVertName, pFragName, Type, Type == 0 ? m_TilePipeline : (Type == 1 ? m_TileBorderPipeline : m_TileBorderLinePipeline), TexMode, EVulkanBackendBlendModes(i), EVulkanBackendClipModes(j)); + } + } + + return Ret; + } + + bool CreatePrimExGraphicsPipelineImpl(const char *pVertName, const char *pFragName, bool Rotationless, SPipelineContainer &PipeContainer, EVulkanBackendTextureModes TexMode, EVulkanBackendBlendModes BlendMode, EVulkanBackendClipModes DynamicMode) + { + std::array aAttributeDescriptions = {}; + aAttributeDescriptions[0] = {0, 0, VK_FORMAT_R32G32_SFLOAT, 0}; + aAttributeDescriptions[1] = {1, 0, VK_FORMAT_R32G32_SFLOAT, sizeof(float) * 2}; + aAttributeDescriptions[2] = {2, 0, VK_FORMAT_R8G8B8A8_UNORM, sizeof(float) * (2 + 2)}; + + std::array aSetLayouts; + aSetLayouts[0] = m_StandardTexturedDescriptorSetLayout; + uint32_t VertPushConstantSize = sizeof(SUniformPrimExGPos); + if(Rotationless) + VertPushConstantSize = sizeof(SUniformPrimExGPosRotationless); + + uint32_t FragPushConstantSize = sizeof(SUniformPrimExGVertColor); + + std::array aPushConstants{}; + aPushConstants[0] = {VK_SHADER_STAGE_VERTEX_BIT, 0, VertPushConstantSize}; + aPushConstants[1] = {VK_SHADER_STAGE_FRAGMENT_BIT, sizeof(SUniformPrimExGPos) + sizeof(SUniformPrimExGVertColorAlign), FragPushConstantSize}; + + return CreateGraphicsPipeline(pVertName, pFragName, PipeContainer, sizeof(float) * (2 + 2) + sizeof(uint8_t) * 4, aAttributeDescriptions, aSetLayouts, aPushConstants, TexMode, BlendMode, DynamicMode); + } + + bool CreatePrimExGraphicsPipeline(const char *pVertName, const char *pFragName, bool HasSampler, bool Rotationless) + { + bool Ret = true; + + EVulkanBackendTextureModes TexMode = HasSampler ? VULKAN_BACKEND_TEXTURE_MODE_TEXTURED : VULKAN_BACKEND_TEXTURE_MODE_NOT_TEXTURED; + + for(size_t i = 0; i < VULKAN_BACKEND_BLEND_MODE_COUNT; ++i) + { + for(size_t j = 0; j < VULKAN_BACKEND_CLIP_MODE_COUNT; ++j) + { + Ret &= CreatePrimExGraphicsPipelineImpl(pVertName, pFragName, Rotationless, Rotationless ? m_PrimExRotationlessPipeline : m_PrimExPipeline, TexMode, EVulkanBackendBlendModes(i), EVulkanBackendClipModes(j)); + } + } + + return Ret; + } + + bool CreateUniformDescriptorSetLayout(VkDescriptorSetLayout &SetLayout, VkShaderStageFlags StageFlags) + { + VkDescriptorSetLayoutBinding SamplerLayoutBinding{}; + SamplerLayoutBinding.binding = 1; + SamplerLayoutBinding.descriptorCount = 1; + SamplerLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + SamplerLayoutBinding.pImmutableSamplers = nullptr; + SamplerLayoutBinding.stageFlags = StageFlags; + + std::array aBindings = {SamplerLayoutBinding}; + VkDescriptorSetLayoutCreateInfo LayoutInfo{}; + LayoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; + LayoutInfo.bindingCount = aBindings.size(); + LayoutInfo.pBindings = aBindings.data(); + + if(vkCreateDescriptorSetLayout(m_VKDevice, &LayoutInfo, nullptr, &SetLayout) != VK_SUCCESS) + { + SetError("Creating descriptor layout failed."); + return false; + } + return true; + } + + bool CreateSpriteMultiUniformDescriptorSetLayout() + { + return CreateUniformDescriptorSetLayout(m_SpriteMultiUniformDescriptorSetLayout, VK_SHADER_STAGE_VERTEX_BIT); + } + + bool CreateQuadUniformDescriptorSetLayout() + { + return CreateUniformDescriptorSetLayout(m_QuadUniformDescriptorSetLayout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT); + } + + void DestroyUniformDescriptorSetLayouts() + { + vkDestroyDescriptorSetLayout(m_VKDevice, m_QuadUniformDescriptorSetLayout, nullptr); + vkDestroyDescriptorSetLayout(m_VKDevice, m_SpriteMultiUniformDescriptorSetLayout, nullptr); + } + + bool CreateUniformDescriptorSets(size_t RenderThreadIndex, VkDescriptorSetLayout &SetLayout, SDeviceDescriptorSet *pSets, size_t SetCount, VkBuffer BindBuffer, size_t SingleBufferInstanceSize, VkDeviceSize MemoryOffset) + { + GetDescriptorPoolForAlloc(m_UniformBufferDescrPools[RenderThreadIndex], pSets, SetCount); + VkDescriptorSetAllocateInfo DesAllocInfo{}; + DesAllocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + DesAllocInfo.descriptorSetCount = 1; + DesAllocInfo.pSetLayouts = &SetLayout; + for(size_t i = 0; i < SetCount; ++i) + { + DesAllocInfo.descriptorPool = pSets[i].m_pPools->m_Pools[pSets[i].m_PoolIndex].m_Pool; + if(vkAllocateDescriptorSets(m_VKDevice, &DesAllocInfo, &pSets[i].m_Descriptor) != VK_SUCCESS) + { + return false; + } + + VkDescriptorBufferInfo BufferInfo{}; + BufferInfo.buffer = BindBuffer; + BufferInfo.offset = MemoryOffset + SingleBufferInstanceSize * i; + BufferInfo.range = SingleBufferInstanceSize; + + std::array aDescriptorWrites{}; + + aDescriptorWrites[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + aDescriptorWrites[0].dstSet = pSets[i].m_Descriptor; + aDescriptorWrites[0].dstBinding = 1; + aDescriptorWrites[0].dstArrayElement = 0; + aDescriptorWrites[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + aDescriptorWrites[0].descriptorCount = 1; + aDescriptorWrites[0].pBufferInfo = &BufferInfo; + + vkUpdateDescriptorSets(m_VKDevice, static_cast(aDescriptorWrites.size()), aDescriptorWrites.data(), 0, nullptr); + } + + return true; + } + + void DestroyUniformDescriptorSets(SDeviceDescriptorSet *pSets, size_t SetCount) + { + for(size_t i = 0; i < SetCount; ++i) + { + vkFreeDescriptorSets(m_VKDevice, pSets[i].m_pPools->m_Pools[pSets[i].m_PoolIndex].m_Pool, 1, &pSets[i].m_Descriptor); + pSets[i].m_Descriptor = VK_NULL_HANDLE; + } + } + + bool CreateSpriteMultiGraphicsPipelineImpl(const char *pVertName, const char *pFragName, SPipelineContainer &PipeContainer, EVulkanBackendTextureModes TexMode, EVulkanBackendBlendModes BlendMode, EVulkanBackendClipModes DynamicMode) + { + std::array aAttributeDescriptions = {}; + aAttributeDescriptions[0] = {0, 0, VK_FORMAT_R32G32_SFLOAT, 0}; + aAttributeDescriptions[1] = {1, 0, VK_FORMAT_R32G32_SFLOAT, sizeof(float) * 2}; + aAttributeDescriptions[2] = {2, 0, VK_FORMAT_R8G8B8A8_UNORM, sizeof(float) * (2 + 2)}; + + std::array aSetLayouts; + aSetLayouts[0] = m_StandardTexturedDescriptorSetLayout; + aSetLayouts[1] = m_SpriteMultiUniformDescriptorSetLayout; + + uint32_t VertPushConstantSize = sizeof(SUniformSpriteMultiGPos); + uint32_t FragPushConstantSize = sizeof(SUniformSpriteMultiGVertColor); + + std::array aPushConstants{}; + aPushConstants[0] = {VK_SHADER_STAGE_VERTEX_BIT, 0, VertPushConstantSize}; + aPushConstants[1] = {VK_SHADER_STAGE_FRAGMENT_BIT, sizeof(SUniformSpriteMultiGPos) + sizeof(SUniformSpriteMultiGVertColorAlign), FragPushConstantSize}; + + return CreateGraphicsPipeline(pVertName, pFragName, PipeContainer, sizeof(float) * (2 + 2) + sizeof(uint8_t) * 4, aAttributeDescriptions, aSetLayouts, aPushConstants, TexMode, BlendMode, DynamicMode); + } + + bool CreateSpriteMultiGraphicsPipeline(const char *pVertName, const char *pFragName) + { + bool Ret = true; + + EVulkanBackendTextureModes TexMode = VULKAN_BACKEND_TEXTURE_MODE_TEXTURED; + + for(size_t i = 0; i < VULKAN_BACKEND_BLEND_MODE_COUNT; ++i) + { + for(size_t j = 0; j < VULKAN_BACKEND_CLIP_MODE_COUNT; ++j) + { + Ret &= CreateSpriteMultiGraphicsPipelineImpl(pVertName, pFragName, m_SpriteMultiPipeline, TexMode, EVulkanBackendBlendModes(i), EVulkanBackendClipModes(j)); + } + } + + return Ret; + } + + bool CreateSpriteMultiPushGraphicsPipelineImpl(const char *pVertName, const char *pFragName, SPipelineContainer &PipeContainer, EVulkanBackendTextureModes TexMode, EVulkanBackendBlendModes BlendMode, EVulkanBackendClipModes DynamicMode) + { + std::array aAttributeDescriptions = {}; + aAttributeDescriptions[0] = {0, 0, VK_FORMAT_R32G32_SFLOAT, 0}; + aAttributeDescriptions[1] = {1, 0, VK_FORMAT_R32G32_SFLOAT, sizeof(float) * 2}; + aAttributeDescriptions[2] = {2, 0, VK_FORMAT_R8G8B8A8_UNORM, sizeof(float) * (2 + 2)}; + + std::array aSetLayouts; + aSetLayouts[0] = m_StandardTexturedDescriptorSetLayout; + + uint32_t VertPushConstantSize = sizeof(SUniformSpriteMultiPushGPos); + uint32_t FragPushConstantSize = sizeof(SUniformSpriteMultiPushGVertColor); + + std::array aPushConstants{}; + aPushConstants[0] = {VK_SHADER_STAGE_VERTEX_BIT, 0, VertPushConstantSize}; + aPushConstants[1] = {VK_SHADER_STAGE_FRAGMENT_BIT, sizeof(SUniformSpriteMultiPushGPos), FragPushConstantSize}; + + return CreateGraphicsPipeline(pVertName, pFragName, PipeContainer, sizeof(float) * (2 + 2) + sizeof(uint8_t) * 4, aAttributeDescriptions, aSetLayouts, aPushConstants, TexMode, BlendMode, DynamicMode); + } + + bool CreateSpriteMultiPushGraphicsPipeline(const char *pVertName, const char *pFragName) + { + bool Ret = true; + + EVulkanBackendTextureModes TexMode = VULKAN_BACKEND_TEXTURE_MODE_TEXTURED; + + for(size_t i = 0; i < VULKAN_BACKEND_BLEND_MODE_COUNT; ++i) + { + for(size_t j = 0; j < VULKAN_BACKEND_CLIP_MODE_COUNT; ++j) + { + Ret &= CreateSpriteMultiPushGraphicsPipelineImpl(pVertName, pFragName, m_SpriteMultiPushPipeline, TexMode, EVulkanBackendBlendModes(i), EVulkanBackendClipModes(j)); + } + } + + return Ret; + } + + template + bool CreateQuadGraphicsPipelineImpl(const char *pVertName, const char *pFragName, SPipelineContainer &PipeContainer, EVulkanBackendTextureModes TexMode, EVulkanBackendBlendModes BlendMode, EVulkanBackendClipModes DynamicMode) + { + std::array aAttributeDescriptions = {}; + aAttributeDescriptions[0] = {0, 0, VK_FORMAT_R32G32B32A32_SFLOAT, 0}; + aAttributeDescriptions[1] = {1, 0, VK_FORMAT_R8G8B8A8_UNORM, sizeof(float) * 4}; + if(IsTextured) + aAttributeDescriptions[2] = {2, 0, VK_FORMAT_R32G32_SFLOAT, sizeof(float) * 4 + sizeof(uint8_t) * 4}; + + std::array aSetLayouts; + if(IsTextured) + { + aSetLayouts[0] = m_StandardTexturedDescriptorSetLayout; + aSetLayouts[1] = m_QuadUniformDescriptorSetLayout; + } + else + { + aSetLayouts[0] = m_QuadUniformDescriptorSetLayout; + } + + uint32_t PushConstantSize = sizeof(SUniformQuadGPos); + + std::array aPushConstants{}; + aPushConstants[0] = {VK_SHADER_STAGE_VERTEX_BIT, 0, PushConstantSize}; + + return CreateGraphicsPipeline(pVertName, pFragName, PipeContainer, sizeof(float) * 4 + sizeof(uint8_t) * 4 + (IsTextured ? (sizeof(float) * 2) : 0), aAttributeDescriptions, aSetLayouts, aPushConstants, TexMode, BlendMode, DynamicMode); + } + + template + bool CreateQuadGraphicsPipeline(const char *pVertName, const char *pFragName) + { + bool Ret = true; + + EVulkanBackendTextureModes TexMode = HasSampler ? VULKAN_BACKEND_TEXTURE_MODE_TEXTURED : VULKAN_BACKEND_TEXTURE_MODE_NOT_TEXTURED; + + for(size_t i = 0; i < VULKAN_BACKEND_BLEND_MODE_COUNT; ++i) + { + for(size_t j = 0; j < VULKAN_BACKEND_CLIP_MODE_COUNT; ++j) + { + Ret &= CreateQuadGraphicsPipelineImpl(pVertName, pFragName, m_QuadPipeline, TexMode, EVulkanBackendBlendModes(i), EVulkanBackendClipModes(j)); + } + } + + return Ret; + } + + template + bool CreateQuadPushGraphicsPipelineImpl(const char *pVertName, const char *pFragName, SPipelineContainer &PipeContainer, EVulkanBackendTextureModes TexMode, EVulkanBackendBlendModes BlendMode, EVulkanBackendClipModes DynamicMode) + { + std::array aAttributeDescriptions = {}; + aAttributeDescriptions[0] = {0, 0, VK_FORMAT_R32G32B32A32_SFLOAT, 0}; + aAttributeDescriptions[1] = {1, 0, VK_FORMAT_R8G8B8A8_UNORM, sizeof(float) * 4}; + if(IsTextured) + aAttributeDescriptions[2] = {2, 0, VK_FORMAT_R32G32_SFLOAT, sizeof(float) * 4 + sizeof(uint8_t) * 4}; + + std::array aSetLayouts; + aSetLayouts[0] = m_StandardTexturedDescriptorSetLayout; + + uint32_t PushConstantSize = sizeof(SUniformQuadPushGPos); + + std::array aPushConstants{}; + aPushConstants[0] = {VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, PushConstantSize}; + + return CreateGraphicsPipeline(pVertName, pFragName, PipeContainer, sizeof(float) * 4 + sizeof(uint8_t) * 4 + (IsTextured ? (sizeof(float) * 2) : 0), aAttributeDescriptions, aSetLayouts, aPushConstants, TexMode, BlendMode, DynamicMode); + } + + template + bool CreateQuadPushGraphicsPipeline(const char *pVertName, const char *pFragName) + { + bool Ret = true; + + EVulkanBackendTextureModes TexMode = HasSampler ? VULKAN_BACKEND_TEXTURE_MODE_TEXTURED : VULKAN_BACKEND_TEXTURE_MODE_NOT_TEXTURED; + + for(size_t i = 0; i < VULKAN_BACKEND_BLEND_MODE_COUNT; ++i) + { + for(size_t j = 0; j < VULKAN_BACKEND_CLIP_MODE_COUNT; ++j) + { + Ret &= CreateQuadPushGraphicsPipelineImpl(pVertName, pFragName, m_QuadPushPipeline, TexMode, EVulkanBackendBlendModes(i), EVulkanBackendClipModes(j)); + } + } + + return Ret; + } + + bool CreateCommandPool() + { + VkCommandPoolCreateInfo CreatePoolInfo{}; + CreatePoolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; + CreatePoolInfo.queueFamilyIndex = m_VKGraphicsQueueIndex; + CreatePoolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; + + m_vCommandPools.resize(m_ThreadCount); + for(size_t i = 0; i < m_ThreadCount; ++i) + { + if(vkCreateCommandPool(m_VKDevice, &CreatePoolInfo, nullptr, &m_vCommandPools[i]) != VK_SUCCESS) + { + SetError("Creating the command pool failed."); + return false; + } + } + return true; + } + + void DestroyCommandPool() + { + for(size_t i = 0; i < m_ThreadCount; ++i) + { + vkDestroyCommandPool(m_VKDevice, m_vCommandPools[i], nullptr); + } + } + + bool CreateCommandBuffers() + { + m_MainDrawCommandBuffers.resize(m_SwapChainImageCount); + if(m_ThreadCount > 1) + { + m_ThreadDrawCommandBuffers.resize(m_ThreadCount); + m_UsedThreadDrawCommandBuffer.resize(m_ThreadCount); + m_HelperThreadDrawCommandBuffers.resize(m_ThreadCount); + for(auto &ThreadDrawCommandBuffers : m_ThreadDrawCommandBuffers) + { + ThreadDrawCommandBuffers.resize(m_SwapChainImageCount); + } + for(auto &UsedThreadDrawCommandBuffer : m_UsedThreadDrawCommandBuffer) + { + UsedThreadDrawCommandBuffer.resize(m_SwapChainImageCount, false); + } + } + m_MemoryCommandBuffers.resize(m_SwapChainImageCount); + m_UsedMemoryCommandBuffer.resize(m_SwapChainImageCount, false); + + VkCommandBufferAllocateInfo AllocInfo{}; + AllocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; + AllocInfo.commandPool = m_vCommandPools[0]; + AllocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + AllocInfo.commandBufferCount = (uint32_t)m_MainDrawCommandBuffers.size(); + + if(vkAllocateCommandBuffers(m_VKDevice, &AllocInfo, m_MainDrawCommandBuffers.data()) != VK_SUCCESS) + { + SetError("Allocating command buffers failed."); + return false; + } + + AllocInfo.commandBufferCount = (uint32_t)m_MemoryCommandBuffers.size(); + + if(vkAllocateCommandBuffers(m_VKDevice, &AllocInfo, m_MemoryCommandBuffers.data()) != VK_SUCCESS) + { + SetError("Allocating memory command buffers failed."); + return false; + } + + if(m_ThreadCount > 1) + { + size_t Count = 0; + for(auto &ThreadDrawCommandBuffers : m_ThreadDrawCommandBuffers) + { + AllocInfo.commandPool = m_vCommandPools[Count]; + ++Count; + AllocInfo.commandBufferCount = (uint32_t)ThreadDrawCommandBuffers.size(); + AllocInfo.level = VK_COMMAND_BUFFER_LEVEL_SECONDARY; + if(vkAllocateCommandBuffers(m_VKDevice, &AllocInfo, ThreadDrawCommandBuffers.data()) != VK_SUCCESS) + { + SetError("Allocating thread command buffers failed."); + return false; + } + } + } + + return true; + } + + void DestroyCommandBuffer() + { + if(m_ThreadCount > 1) + { + size_t Count = 0; + for(auto &ThreadDrawCommandBuffers : m_ThreadDrawCommandBuffers) + { + vkFreeCommandBuffers(m_VKDevice, m_vCommandPools[Count], static_cast(ThreadDrawCommandBuffers.size()), ThreadDrawCommandBuffers.data()); + ++Count; + } + } + + vkFreeCommandBuffers(m_VKDevice, m_vCommandPools[0], static_cast(m_MemoryCommandBuffers.size()), m_MemoryCommandBuffers.data()); + vkFreeCommandBuffers(m_VKDevice, m_vCommandPools[0], static_cast(m_MainDrawCommandBuffers.size()), m_MainDrawCommandBuffers.data()); + + m_ThreadDrawCommandBuffers.clear(); + m_UsedThreadDrawCommandBuffer.clear(); + m_HelperThreadDrawCommandBuffers.clear(); + + m_MainDrawCommandBuffers.clear(); + m_MemoryCommandBuffers.clear(); + m_UsedMemoryCommandBuffer.clear(); + } + + bool CreateSyncObjects() + { + m_WaitSemaphores.resize(m_SwapChainImageCount); + m_SigSemaphores.resize(m_SwapChainImageCount); + + m_MemorySemaphores.resize(m_SwapChainImageCount); + + m_FrameFences.resize(m_SwapChainImageCount); + m_ImagesFences.resize(m_SwapChainImageCount, VK_NULL_HANDLE); + + VkSemaphoreCreateInfo CreateSemaphoreInfo{}; + CreateSemaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; + + VkFenceCreateInfo FenceInfo{}; + FenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; + FenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT; + + for(size_t i = 0; i < m_SwapChainImageCount; i++) + { + if(vkCreateSemaphore(m_VKDevice, &CreateSemaphoreInfo, nullptr, &m_WaitSemaphores[i]) != VK_SUCCESS || + vkCreateSemaphore(m_VKDevice, &CreateSemaphoreInfo, nullptr, &m_SigSemaphores[i]) != VK_SUCCESS || + vkCreateSemaphore(m_VKDevice, &CreateSemaphoreInfo, nullptr, &m_MemorySemaphores[i]) != VK_SUCCESS || + vkCreateFence(m_VKDevice, &FenceInfo, nullptr, &m_FrameFences[i]) != VK_SUCCESS) + { + SetError("Creating swap chain sync objects(fences, semaphores) failed."); + return false; + } + } + + return true; + } + + void DestroySyncObjects() + { + for(size_t i = 0; i < m_SwapChainImageCount; i++) + { + vkDestroySemaphore(m_VKDevice, m_WaitSemaphores[i], nullptr); + vkDestroySemaphore(m_VKDevice, m_SigSemaphores[i], nullptr); + vkDestroySemaphore(m_VKDevice, m_MemorySemaphores[i], nullptr); + vkDestroyFence(m_VKDevice, m_FrameFences[i], nullptr); + } + + m_WaitSemaphores.clear(); + m_SigSemaphores.clear(); + + m_MemorySemaphores.clear(); + + m_FrameFences.clear(); + m_ImagesFences.clear(); + } + + void DestroyBufferOfFrame(size_t ImageIndex, SFrameBuffers &Buffer) + { + CleanBufferPair(ImageIndex, Buffer.m_Buffer, Buffer.m_BufferMem); + } + + void DestroyUniBufferOfFrame(size_t ImageIndex, SFrameUniformBuffers &Buffer) + { + CleanBufferPair(ImageIndex, Buffer.m_Buffer, Buffer.m_BufferMem); + for(auto &DescrSet : Buffer.m_aUniformSets) + { + if(DescrSet.m_Descriptor != VK_NULL_HANDLE) + { + DestroyUniformDescriptorSets(&DescrSet, 1); + } + } + } + + /************* + * SWAP CHAIN + **************/ + + void CleanupVulkanSwapChain(bool ForceSwapChainDestruct) + { + m_StandardPipeline.Destroy(m_VKDevice); + m_StandardLinePipeline.Destroy(m_VKDevice); + m_Standard3DPipeline.Destroy(m_VKDevice); + m_TextPipeline.Destroy(m_VKDevice); + m_TilePipeline.Destroy(m_VKDevice); + m_TileBorderPipeline.Destroy(m_VKDevice); + m_TileBorderLinePipeline.Destroy(m_VKDevice); + m_PrimExPipeline.Destroy(m_VKDevice); + m_PrimExRotationlessPipeline.Destroy(m_VKDevice); + m_SpriteMultiPipeline.Destroy(m_VKDevice); + m_SpriteMultiPushPipeline.Destroy(m_VKDevice); + m_QuadPipeline.Destroy(m_VKDevice); + m_QuadPushPipeline.Destroy(m_VKDevice); + + DestroyFramebuffers(); + + DestroyRenderPass(); + + DestroyImageViews(); + ClearSwapChainImageHandles(); + + DestroySwapChain(ForceSwapChainDestruct); + + m_SwapchainCreated = false; + } + + template + void CleanupVulkan() + { + if(IsLastCleanup) + { + // clean all images, buffers, buffer containers + for(auto &Texture : m_Textures) + { + if(Texture.m_VKTextDescrSet.m_Descriptor != VK_NULL_HANDLE && IsVerbose()) + { + dbg_msg("vulkan", "text textures not cleared over cmd."); + } + DestroyTexture(Texture); + } + + for(auto &BufferObject : m_BufferObjects) + { + if(!BufferObject.m_IsStreamedBuffer) + FreeVertexMemBlock(BufferObject.m_BufferObject.m_Mem); + } + + m_BufferContainers.clear(); + } + + m_ImageLastFrameCheck.clear(); + + m_vLastPipeline.clear(); + + for(size_t i = 0; i < m_ThreadCount; ++i) + { + m_vStreamedVertexBuffers[i].Destroy([&](size_t ImageIndex, SFrameBuffers &Buffer) { DestroyBufferOfFrame(ImageIndex, Buffer); }); + m_vStreamedUniformBuffers[i].Destroy([&](size_t ImageIndex, SFrameUniformBuffers &Buffer) { DestroyUniBufferOfFrame(ImageIndex, Buffer); }); + } + m_vStreamedVertexBuffers.clear(); + m_vStreamedUniformBuffers.clear(); + + for(size_t i = 0; i < m_SwapChainImageCount; ++i) + { + ClearFrameData(i); + } + + m_FrameDelayedBufferCleanup.clear(); + m_FrameDelayedTextureCleanup.clear(); + m_FrameDelayedTextTexturesCleanup.clear(); + + m_StagingBufferCache.DestroyFrameData(m_SwapChainImageCount); + m_StagingBufferCacheImage.DestroyFrameData(m_SwapChainImageCount); + m_VertexBufferCache.DestroyFrameData(m_SwapChainImageCount); + for(auto &ImageBufferCache : m_ImageBufferCaches) + ImageBufferCache.second.DestroyFrameData(m_SwapChainImageCount); + + if(IsLastCleanup) + { + m_StagingBufferCache.Destroy(m_VKDevice); + m_StagingBufferCacheImage.Destroy(m_VKDevice); + m_VertexBufferCache.Destroy(m_VKDevice); + for(auto &ImageBufferCache : m_ImageBufferCaches) + ImageBufferCache.second.Destroy(m_VKDevice); + + m_ImageBufferCaches.clear(); + + DestroyTextureSamplers(); + DestroyDescriptorPools(); + + DeletePresentedImageDataImage(); + } + + DestroySyncObjects(); + DestroyCommandBuffer(); + + if(IsLastCleanup) + { + DestroyCommandPool(); + } + + if(IsLastCleanup) + { + if(m_SwapchainCreated) + CleanupVulkanSwapChain(true); + + DestroyUniformDescriptorSetLayouts(); + DestroyTextDescriptorSetLayout(); + DestroyDescriptorSetLayouts(); + } + } + + void CleanupVulkanSDL() + { + if(m_VKInstance != VK_NULL_HANDLE) + { + DestroySurface(); + vkDestroyDevice(m_VKDevice, nullptr); + + if(g_Config.m_DbgGfx == DEBUG_GFX_MODE_MINIMUM || g_Config.m_DbgGfx == DEBUG_GFX_MODE_ALL) + { + UnregisterDebugCallback(); + } + vkDestroyInstance(m_VKInstance, nullptr); + } + } + + int RecreateSwapChain() + { + int Ret = 0; + vkDeviceWaitIdle(m_VKDevice); + + if(IsVerbose()) + { + dbg_msg("vulkan", "recreating swap chain."); + } + + VkSwapchainKHR OldSwapChain = VK_NULL_HANDLE; + uint32_t OldSwapChainImageCount = m_SwapChainImageCount; + + if(m_SwapchainCreated) + CleanupVulkanSwapChain(false); + if(!m_SwapchainCreated) + Ret = InitVulkanSwapChain(OldSwapChain); + + if(OldSwapChainImageCount != m_SwapChainImageCount) + { + CleanupVulkan(); + InitVulkan(); + } + + if(OldSwapChain != VK_NULL_HANDLE) + { + vkDestroySwapchainKHR(m_VKDevice, OldSwapChain, nullptr); + } + + if(Ret != 0 && IsVerbose()) + { + dbg_msg("vulkan", "recreating swap chain failed."); + } + + return Ret; + } + + int InitVulkanSDL(SDL_Window *pWindow, uint32_t CanvasWidth, uint32_t CanvasHeight, char *pRendererString, char *pVendorString, char *pVersionString) + { + std::vector VKExtensions; + std::vector VKLayers; + + m_CanvasWidth = CanvasWidth; + m_CanvasHeight = CanvasHeight; + + if(!GetVulkanExtensions(pWindow, VKExtensions)) + return -1; + + if(!GetVulkanLayers(VKLayers)) + return -1; + + if(!CreateVulkanInstance(VKLayers, VKExtensions, true)) + return -1; + + if(g_Config.m_DbgGfx == DEBUG_GFX_MODE_MINIMUM || g_Config.m_DbgGfx == DEBUG_GFX_MODE_ALL) + { + SetupDebugCallback(); + + for(auto &VKLayer : VKLayers) + { + dbg_msg("vulkan", "Validation layer: %s", VKLayer.c_str()); + } + } + + if(!SelectGPU(pRendererString, pVendorString, pVersionString)) + return -1; + + if(!CreateLogicalDevice(VKLayers)) + return -1; + + GetDeviceQueue(); + + if(!CreateSurface(pWindow)) + return -1; + + return 0; + } + + /************************ + * MEMORY MANAGMENT + ************************/ + + uint32_t FindMemoryType(VkPhysicalDevice PhyDevice, uint32_t TypeFilter, VkMemoryPropertyFlags Properties) + { + VkPhysicalDeviceMemoryProperties MemProperties; + vkGetPhysicalDeviceMemoryProperties(PhyDevice, &MemProperties); + + for(uint32_t i = 0; i < MemProperties.memoryTypeCount; i++) + { + if((TypeFilter & (1 << i)) && (MemProperties.memoryTypes[i].propertyFlags & Properties) == Properties) + { + return i; + } + } + + return 0; + } + + bool CreateBuffer(VkDeviceSize BufferSize, EMemoryBlockUsage MemUsage, VkBufferUsageFlags BufferUsage, VkMemoryPropertyFlags MemoryProperties, VkBuffer &VKBuffer, SDeviceMemoryBlock &VKBufferMemory) + { + VkBufferCreateInfo BufferInfo{}; + BufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + BufferInfo.size = BufferSize; + BufferInfo.usage = BufferUsage; + BufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + + if(vkCreateBuffer(m_VKDevice, &BufferInfo, nullptr, &VKBuffer) != VK_SUCCESS) + { + SetError("Buffer creation failed."); + return false; + } + + VkMemoryRequirements MemRequirements; + vkGetBufferMemoryRequirements(m_VKDevice, VKBuffer, &MemRequirements); + + VkMemoryAllocateInfo MemAllocInfo{}; + MemAllocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + MemAllocInfo.allocationSize = MemRequirements.size; + MemAllocInfo.memoryTypeIndex = FindMemoryType(m_VKGPU, MemRequirements.memoryTypeBits, MemoryProperties); + + VKBufferMemory.m_Size = MemRequirements.size; + + if(MemUsage == MEMORY_BLOCK_USAGE_BUFFER) + m_pBufferMemoryUsage->store(m_pBufferMemoryUsage->load(std::memory_order_relaxed) + MemRequirements.size, std::memory_order_relaxed); + else if(MemUsage == MEMORY_BLOCK_USAGE_STAGING) + m_pStagingMemoryUsage->store(m_pStagingMemoryUsage->load(std::memory_order_relaxed) + MemRequirements.size, std::memory_order_relaxed); + else if(MemUsage == MEMORY_BLOCK_USAGE_STREAM) + m_pStreamMemoryUsage->store(m_pStreamMemoryUsage->load(std::memory_order_relaxed) + MemRequirements.size, std::memory_order_relaxed); + + if(IsVerbose()) + { + VerboseAllocatedMemory(MemRequirements.size, m_CurImageIndex, MemUsage); + } + + if(!AllocateVulkanMemory(&MemAllocInfo, &VKBufferMemory.m_Mem)) + { + SetError("Allocation for buffer object failed."); + return false; + } + + VKBufferMemory.m_UsageType = MemUsage; + + if(vkBindBufferMemory(m_VKDevice, VKBuffer, VKBufferMemory.m_Mem, 0) != VK_SUCCESS) + { + SetError("Binding memory to buffer failed."); + return false; + } + + return true; + } + + bool AllocateDescriptorPool(SDeviceDescriptorPools &DescriptorPools, size_t AllocPoolSize) + { + SDeviceDescriptorPool NewPool; + NewPool.m_Size = AllocPoolSize; + + VkDescriptorPoolSize PoolSize{}; + if(DescriptorPools.m_IsUniformPool) + PoolSize.type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + else + PoolSize.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + PoolSize.descriptorCount = AllocPoolSize; + + VkDescriptorPoolCreateInfo PoolInfo{}; + PoolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + PoolInfo.poolSizeCount = 1; + PoolInfo.pPoolSizes = &PoolSize; + PoolInfo.maxSets = AllocPoolSize; + PoolInfo.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT; + + if(vkCreateDescriptorPool(m_VKDevice, &PoolInfo, nullptr, &NewPool.m_Pool) != VK_SUCCESS) + { + SetError("Creating the descriptor pool failed."); + return false; + } + + DescriptorPools.m_Pools.push_back(NewPool); + + return true; + } + + bool CreateDescriptorPools() + { + m_StandardTextureDescrPool.m_IsUniformPool = false; + m_StandardTextureDescrPool.m_DefaultAllocSize = 1024; + m_TextTextureDescrPool.m_IsUniformPool = false; + m_TextTextureDescrPool.m_DefaultAllocSize = 8; + + m_UniformBufferDescrPools.resize(m_ThreadCount); + for(auto &UniformBufferDescrPool : m_UniformBufferDescrPools) + { + UniformBufferDescrPool.m_IsUniformPool = true; + UniformBufferDescrPool.m_DefaultAllocSize = 512; + } + + bool Ret = AllocateDescriptorPool(m_StandardTextureDescrPool, CCommandBuffer::MAX_TEXTURES); + Ret |= AllocateDescriptorPool(m_TextTextureDescrPool, 8); + + for(auto &UniformBufferDescrPool : m_UniformBufferDescrPools) + { + Ret |= AllocateDescriptorPool(UniformBufferDescrPool, 64); + } + + return Ret; + } + + void DestroyDescriptorPools() + { + for(auto &DescrPool : m_StandardTextureDescrPool.m_Pools) + vkDestroyDescriptorPool(m_VKDevice, DescrPool.m_Pool, nullptr); + for(auto &DescrPool : m_TextTextureDescrPool.m_Pools) + vkDestroyDescriptorPool(m_VKDevice, DescrPool.m_Pool, nullptr); + + for(auto &UniformBufferDescrPool : m_UniformBufferDescrPools) + { + for(auto &DescrPool : UniformBufferDescrPool.m_Pools) + vkDestroyDescriptorPool(m_VKDevice, DescrPool.m_Pool, nullptr); + } + m_UniformBufferDescrPools.clear(); + } + + VkDescriptorPool GetDescriptorPoolForAlloc(SDeviceDescriptorPools &DescriptorPools, SDeviceDescriptorSet *pSets, size_t AllocNum) + { + size_t CurAllocNum = AllocNum; + size_t CurAllocOffset = 0; + VkDescriptorPool RetDescr = VK_NULL_HANDLE; + + while(CurAllocNum > 0) + { + size_t AllocatedInThisRun = 0; + + bool Found = false; + size_t DescriptorPoolIndex = std::numeric_limits::max(); + for(size_t i = 0; i < DescriptorPools.m_Pools.size(); ++i) + { + auto &Pool = DescriptorPools.m_Pools[i]; + if(Pool.m_CurSize + CurAllocNum < Pool.m_Size) + { + AllocatedInThisRun = CurAllocNum; + Pool.m_CurSize += CurAllocNum; + Found = true; + if(RetDescr == VK_NULL_HANDLE) + RetDescr = Pool.m_Pool; + DescriptorPoolIndex = i; + break; + } + else + { + size_t RemainingPoolCount = Pool.m_Size - Pool.m_CurSize; + if(RemainingPoolCount > 0) + { + AllocatedInThisRun = RemainingPoolCount; + Pool.m_CurSize += RemainingPoolCount; + Found = true; + if(RetDescr == VK_NULL_HANDLE) + RetDescr = Pool.m_Pool; + DescriptorPoolIndex = i; + break; + } + } + } + + if(!Found) + { + DescriptorPoolIndex = DescriptorPools.m_Pools.size(); + + AllocateDescriptorPool(DescriptorPools, DescriptorPools.m_DefaultAllocSize); + + AllocatedInThisRun = minimum((size_t)DescriptorPools.m_DefaultAllocSize, CurAllocNum); + + auto &Pool = DescriptorPools.m_Pools.back(); + Pool.m_CurSize += AllocatedInThisRun; + if(RetDescr == VK_NULL_HANDLE) + RetDescr = Pool.m_Pool; + } + + for(size_t i = CurAllocOffset; i < CurAllocOffset + AllocatedInThisRun; ++i) + { + pSets[i].m_pPools = &DescriptorPools; + pSets[i].m_PoolIndex = DescriptorPoolIndex; + } + CurAllocOffset += AllocatedInThisRun; + CurAllocNum -= AllocatedInThisRun; + } + + return RetDescr; + } + + bool CreateNewTexturedStandardDescriptorSets(size_t TextureSlot, size_t DescrIndex) + { + auto &Texture = m_Textures[TextureSlot]; + + auto &DescrSet = Texture.m_aVKStandardTexturedDescrSets[DescrIndex]; + + VkDescriptorSetAllocateInfo DesAllocInfo{}; + DesAllocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + DesAllocInfo.descriptorPool = GetDescriptorPoolForAlloc(m_StandardTextureDescrPool, &DescrSet, 1); + DesAllocInfo.descriptorSetCount = 1; + DesAllocInfo.pSetLayouts = &m_StandardTexturedDescriptorSetLayout; + + if(vkAllocateDescriptorSets(m_VKDevice, &DesAllocInfo, &DescrSet.m_Descriptor) != VK_SUCCESS) + { + return false; + } + + VkDescriptorImageInfo ImageInfo{}; + ImageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + ImageInfo.imageView = Texture.m_ImgView; + ImageInfo.sampler = Texture.m_aSamplers[DescrIndex]; + + std::array aDescriptorWrites{}; + + aDescriptorWrites[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + aDescriptorWrites[0].dstSet = DescrSet.m_Descriptor; + aDescriptorWrites[0].dstBinding = 0; + aDescriptorWrites[0].dstArrayElement = 0; + aDescriptorWrites[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + aDescriptorWrites[0].descriptorCount = 1; + aDescriptorWrites[0].pImageInfo = &ImageInfo; + + vkUpdateDescriptorSets(m_VKDevice, static_cast(aDescriptorWrites.size()), aDescriptorWrites.data(), 0, nullptr); + + return true; + } + + void DestroyTexturedStandardDescriptorSets(CTexture &Texture, size_t DescrIndex) + { + auto &DescrSet = Texture.m_aVKStandardTexturedDescrSets[DescrIndex]; + if(DescrSet.m_PoolIndex != std::numeric_limits::max()) + vkFreeDescriptorSets(m_VKDevice, DescrSet.m_pPools->m_Pools[DescrSet.m_PoolIndex].m_Pool, 1, &DescrSet.m_Descriptor); + DescrSet = {}; + } + + bool CreateNew3DTexturedStandardDescriptorSets(size_t TextureSlot) + { + auto &Texture = m_Textures[TextureSlot]; + + auto &DescrSet = Texture.m_VKStandard3DTexturedDescrSet; + + VkDescriptorSetAllocateInfo DesAllocInfo{}; + DesAllocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + DesAllocInfo.descriptorPool = GetDescriptorPoolForAlloc(m_StandardTextureDescrPool, &DescrSet, 1); + DesAllocInfo.descriptorSetCount = 1; + DesAllocInfo.pSetLayouts = &m_Standard3DTexturedDescriptorSetLayout; + + if(vkAllocateDescriptorSets(m_VKDevice, &DesAllocInfo, &DescrSet.m_Descriptor) != VK_SUCCESS) + { + return false; + } + + VkDescriptorImageInfo ImageInfo{}; + ImageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + ImageInfo.imageView = Texture.m_Img3DView; + ImageInfo.sampler = Texture.m_Sampler3D; + + std::array aDescriptorWrites{}; + + aDescriptorWrites[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + aDescriptorWrites[0].dstSet = DescrSet.m_Descriptor; + aDescriptorWrites[0].dstBinding = 0; + aDescriptorWrites[0].dstArrayElement = 0; + aDescriptorWrites[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + aDescriptorWrites[0].descriptorCount = 1; + aDescriptorWrites[0].pImageInfo = &ImageInfo; + + vkUpdateDescriptorSets(m_VKDevice, static_cast(aDescriptorWrites.size()), aDescriptorWrites.data(), 0, nullptr); + + return true; + } + + void DestroyTextured3DStandardDescriptorSets(CTexture &Texture) + { + auto &DescrSet = Texture.m_VKStandard3DTexturedDescrSet; + if(DescrSet.m_PoolIndex != std::numeric_limits::max()) + vkFreeDescriptorSets(m_VKDevice, DescrSet.m_pPools->m_Pools[DescrSet.m_PoolIndex].m_Pool, 1, &DescrSet.m_Descriptor); + } + + bool CreateNewTextDescriptorSets(size_t Texture, size_t TextureOutline) + { + auto &TextureText = m_Textures[Texture]; + auto &TextureTextOutline = m_Textures[TextureOutline]; + auto &DescrSetText = TextureText.m_VKTextDescrSet; + auto &DescrSetTextOutline = TextureText.m_VKTextDescrSet; + + VkDescriptorSetAllocateInfo DesAllocInfo{}; + DesAllocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + DesAllocInfo.descriptorPool = GetDescriptorPoolForAlloc(m_TextTextureDescrPool, &DescrSetText, 1); + DesAllocInfo.descriptorSetCount = 1; + DesAllocInfo.pSetLayouts = &m_TextDescriptorSetLayout; + + if(vkAllocateDescriptorSets(m_VKDevice, &DesAllocInfo, &DescrSetText.m_Descriptor) != VK_SUCCESS) + { + return false; + } + + std::array aImageInfo{}; + aImageInfo[0].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + aImageInfo[0].imageView = TextureText.m_ImgView; + aImageInfo[0].sampler = TextureText.m_aSamplers[0]; + aImageInfo[1].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + aImageInfo[1].imageView = TextureTextOutline.m_ImgView; + aImageInfo[1].sampler = TextureTextOutline.m_aSamplers[0]; + + std::array aDescriptorWrites{}; + + aDescriptorWrites[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + aDescriptorWrites[0].dstSet = DescrSetText.m_Descriptor; + aDescriptorWrites[0].dstBinding = 0; + aDescriptorWrites[0].dstArrayElement = 0; + aDescriptorWrites[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + aDescriptorWrites[0].descriptorCount = 1; + aDescriptorWrites[0].pImageInfo = aImageInfo.data(); + aDescriptorWrites[1] = aDescriptorWrites[0]; + aDescriptorWrites[1].dstBinding = 1; + aDescriptorWrites[1].pImageInfo = &aImageInfo[1]; + + vkUpdateDescriptorSets(m_VKDevice, static_cast(aDescriptorWrites.size()), aDescriptorWrites.data(), 0, nullptr); + + DescrSetTextOutline = DescrSetText; + + return true; + } + + void DestroyTextDescriptorSets(CTexture &Texture, CTexture &TextureOutline) + { + auto &DescrSet = Texture.m_VKTextDescrSet; + if(DescrSet.m_PoolIndex != std::numeric_limits::max()) + vkFreeDescriptorSets(m_VKDevice, DescrSet.m_pPools->m_Pools[DescrSet.m_PoolIndex].m_Pool, 1, &DescrSet.m_Descriptor); + } + + VkSampleCountFlagBits GetSampleCount() + { + if(m_MultiSamplingCount >= 64 && m_MaxMultiSample & VK_SAMPLE_COUNT_64_BIT) + return VK_SAMPLE_COUNT_64_BIT; + else if(m_MultiSamplingCount >= 32 && m_MaxMultiSample & VK_SAMPLE_COUNT_32_BIT) + return VK_SAMPLE_COUNT_32_BIT; + else if(m_MultiSamplingCount >= 16 && m_MaxMultiSample & VK_SAMPLE_COUNT_16_BIT) + return VK_SAMPLE_COUNT_16_BIT; + else if(m_MultiSamplingCount >= 8 && m_MaxMultiSample & VK_SAMPLE_COUNT_8_BIT) + return VK_SAMPLE_COUNT_8_BIT; + else if(m_MultiSamplingCount >= 4 && m_MaxMultiSample & VK_SAMPLE_COUNT_4_BIT) + return VK_SAMPLE_COUNT_4_BIT; + else if(m_MultiSamplingCount >= 2 && m_MaxMultiSample & VK_SAMPLE_COUNT_2_BIT) + return VK_SAMPLE_COUNT_2_BIT; + + return VK_SAMPLE_COUNT_1_BIT; + } + + int InitVulkanSwapChain(VkSwapchainKHR &OldSwapChain) + { + OldSwapChain = VK_NULL_HANDLE; + if(!CreateSwapChain(OldSwapChain)) + return -1; + + if(!GetSwapChainImageHandles()) + return -1; + + if(!CreateImageViews()) + return -1; + + m_LastPresentedSwapChainImageIndex = std::numeric_limits::max(); + + if(!CreateRenderPass(true)) + return -1; + + if(!CreateFramebuffers()) + return -1; + + if(!CreateStandardGraphicsPipeline("shader/vulkan/prim.vert.spv", "shader/vulkan/prim.frag.spv", false, false)) + return -1; + + if(!CreateStandardGraphicsPipeline("shader/vulkan/prim_textured.vert.spv", "shader/vulkan/prim_textured.frag.spv", true, false)) + return -1; + + if(!CreateStandardGraphicsPipeline("shader/vulkan/prim.vert.spv", "shader/vulkan/prim.frag.spv", false, true)) + return -1; + + if(!CreateStandard3DGraphicsPipeline("shader/vulkan/prim3d.vert.spv", "shader/vulkan/prim3d.frag.spv", false)) + return -1; + + if(!CreateStandard3DGraphicsPipeline("shader/vulkan/prim3d_textured.vert.spv", "shader/vulkan/prim3d_textured.frag.spv", true)) + return -1; + + if(!CreateTextGraphicsPipeline("shader/vulkan/text.vert.spv", "shader/vulkan/text.frag.spv")) + return -1; + + if(!CreateTileGraphicsPipeline("shader/vulkan/tile.vert.spv", "shader/vulkan/tile.frag.spv", 0)) + return -1; + + if(!CreateTileGraphicsPipeline("shader/vulkan/tile_textured.vert.spv", "shader/vulkan/tile_textured.frag.spv", 0)) + return -1; + + if(!CreateTileGraphicsPipeline("shader/vulkan/tile_border.vert.spv", "shader/vulkan/tile_border.frag.spv", 1)) + return -1; + + if(!CreateTileGraphicsPipeline("shader/vulkan/tile_border_textured.vert.spv", "shader/vulkan/tile_border_textured.frag.spv", 1)) + return -1; + + if(!CreateTileGraphicsPipeline("shader/vulkan/tile_border_line.vert.spv", "shader/vulkan/tile_border_line.frag.spv", 2)) + return -1; + + if(!CreateTileGraphicsPipeline("shader/vulkan/tile_border_line_textured.vert.spv", "shader/vulkan/tile_border_line_textured.frag.spv", 2)) + return -1; + + if(!CreatePrimExGraphicsPipeline("shader/vulkan/primex_rotationless.vert.spv", "shader/vulkan/primex_rotationless.frag.spv", false, true)) + return -1; + + if(!CreatePrimExGraphicsPipeline("shader/vulkan/primex_tex_rotationless.vert.spv", "shader/vulkan/primex_tex_rotationless.frag.spv", true, true)) + return -1; + + if(!CreatePrimExGraphicsPipeline("shader/vulkan/primex.vert.spv", "shader/vulkan/primex.frag.spv", false, false)) + return -1; + + if(!CreatePrimExGraphicsPipeline("shader/vulkan/primex_tex.vert.spv", "shader/vulkan/primex_tex.frag.spv", true, false)) + return -1; + + if(!CreateSpriteMultiGraphicsPipeline("shader/vulkan/spritemulti.vert.spv", "shader/vulkan/spritemulti.frag.spv")) + return -1; + + if(!CreateSpriteMultiPushGraphicsPipeline("shader/vulkan/spritemulti_push.vert.spv", "shader/vulkan/spritemulti_push.frag.spv")) + return -1; + + if(!CreateQuadGraphicsPipeline("shader/vulkan/quad.vert.spv", "shader/vulkan/quad.frag.spv")) + return -1; + + if(!CreateQuadGraphicsPipeline("shader/vulkan/quad_textured.vert.spv", "shader/vulkan/quad_textured.frag.spv")) + return -1; + + if(!CreateQuadPushGraphicsPipeline("shader/vulkan/quad_push.vert.spv", "shader/vulkan/quad_push.frag.spv")) + return -1; + + if(!CreateQuadPushGraphicsPipeline("shader/vulkan/quad_push_textured.vert.spv", "shader/vulkan/quad_push_textured.frag.spv")) + return -1; + + m_SwapchainCreated = true; + return 0; + } + + template + int InitVulkan() + { + if(!CreateDescriptorSetLayouts()) + return -1; + + if(!CreateTextDescriptorSetLayout()) + return -1; + + if(!CreateSpriteMultiUniformDescriptorSetLayout()) + return -1; + + if(!CreateQuadUniformDescriptorSetLayout()) + return -1; + + if(IsFirstInitialization) + { + VkSwapchainKHR OldSwapChain = VK_NULL_HANDLE; + if(InitVulkanSwapChain(OldSwapChain) != 0) + return -1; + } + + if(IsFirstInitialization) + { + if(!CreateCommandPool()) + return -1; + } + + if(!CreateCommandBuffers()) + return -1; + + if(!CreateSyncObjects()) + return -1; + + if(IsFirstInitialization) + { + if(!CreateDescriptorPools()) + return -1; + + if(!CreateTextureSamplers()) + return -1; + } + + m_vStreamedVertexBuffers.resize(m_ThreadCount); + m_vStreamedUniformBuffers.resize(m_ThreadCount); + for(size_t i = 0; i < m_ThreadCount; ++i) + { + m_vStreamedVertexBuffers[i].Init(m_SwapChainImageCount); + m_vStreamedUniformBuffers[i].Init(m_SwapChainImageCount); + } + + m_vLastPipeline.resize(m_ThreadCount, VK_NULL_HANDLE); + + m_FrameDelayedBufferCleanup.resize(m_SwapChainImageCount); + m_FrameDelayedTextureCleanup.resize(m_SwapChainImageCount); + m_FrameDelayedTextTexturesCleanup.resize(m_SwapChainImageCount); + m_StagingBufferCache.Init(m_SwapChainImageCount); + m_StagingBufferCacheImage.Init(m_SwapChainImageCount); + m_VertexBufferCache.Init(m_SwapChainImageCount); + for(auto &ImageBufferCache : m_ImageBufferCaches) + ImageBufferCache.second.Init(m_SwapChainImageCount); + + m_ImageLastFrameCheck.resize(m_SwapChainImageCount, 0); + + if(IsFirstInitialization) + { + // check if image format supports linear blitting + VkFormatProperties FormatProperties; + vkGetPhysicalDeviceFormatProperties(m_VKGPU, VK_FORMAT_R8G8B8A8_UNORM, &FormatProperties); + if((FormatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT) != 0) + { + m_AllowsLinearBlitting = true; + } + if((FormatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_SRC_BIT) != 0 && (FormatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_DST_BIT) != 0) + { + m_OptimalRGBAImageBlitting = true; + } + // check if image format supports blitting to linear tiled images + if((FormatProperties.linearTilingFeatures & VK_FORMAT_FEATURE_BLIT_DST_BIT) != 0) + { + m_LinearRGBAImageBlitting = true; + } + + vkGetPhysicalDeviceFormatProperties(m_VKGPU, m_VKSurfFormat.format, &FormatProperties); + if((FormatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_SRC_BIT) != 0) + { + m_OptimalSwapChainImageBlitting = true; + } + } + + return 0; + } + + VkCommandBuffer &GetMemoryCommandBuffer() + { + VkCommandBuffer &MemCommandBuffer = m_MemoryCommandBuffers[m_CurImageIndex]; + if(!m_UsedMemoryCommandBuffer[m_CurImageIndex]) + { + m_UsedMemoryCommandBuffer[m_CurImageIndex] = true; + + vkResetCommandBuffer(MemCommandBuffer, VK_COMMAND_BUFFER_RESET_RELEASE_RESOURCES_BIT); + + VkCommandBufferBeginInfo BeginInfo{}; + BeginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + BeginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + if(vkBeginCommandBuffer(MemCommandBuffer, &BeginInfo) != VK_SUCCESS) + { + SetError("Command buffer cannot be filled anymore."); + } + } + return MemCommandBuffer; + } + + VkCommandBuffer &GetGraphicCommandBuffer(size_t RenderThreadIndex) + { + if(m_ThreadCount < 2) + { + return m_MainDrawCommandBuffers[m_CurImageIndex]; + } + else + { + VkCommandBuffer &DrawCommandBuffer = m_ThreadDrawCommandBuffers[RenderThreadIndex][m_CurImageIndex]; + if(!m_UsedThreadDrawCommandBuffer[RenderThreadIndex][m_CurImageIndex]) + { + m_UsedThreadDrawCommandBuffer[RenderThreadIndex][m_CurImageIndex] = true; + + vkResetCommandBuffer(DrawCommandBuffer, VK_COMMAND_BUFFER_RESET_RELEASE_RESOURCES_BIT); + + VkCommandBufferBeginInfo BeginInfo{}; + BeginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + BeginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT | VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT; + + VkCommandBufferInheritanceInfo InheretInfo{}; + InheretInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_INHERITANCE_INFO; + InheretInfo.framebuffer = m_FramebufferList[m_CurImageIndex]; + InheretInfo.occlusionQueryEnable = VK_FALSE; + InheretInfo.renderPass = m_VKRenderPass; + InheretInfo.subpass = 0; + + BeginInfo.pInheritanceInfo = &InheretInfo; + + if(vkBeginCommandBuffer(DrawCommandBuffer, &BeginInfo) != VK_SUCCESS) + { + SetError("Thread draw command buffer cannot be filled anymore."); + } + } + return DrawCommandBuffer; + } + } + + VkCommandBuffer &GetMainGraphicCommandBuffer() + { + return m_MainDrawCommandBuffers[m_CurImageIndex]; + } + + /************************ + * STREAM BUFFERS SETUP + ************************/ + + typedef std::function TNewMemFunc; + + // returns true, if the stream memory was just allocated + template + void CreateStreamBuffer(TStreamMemName *&pBufferMem, TNewMemFunc &&NewMemFunc, SStreamMemory &StreamUniformBuffer, VkBufferUsageFlagBits Usage, VkBuffer &NewBuffer, SDeviceMemoryBlock &NewBufferMem, size_t &BufferOffset, const void *pData, size_t DataSize) + { + VkBuffer Buffer = VK_NULL_HANDLE; + SDeviceMemoryBlock BufferMem; + size_t Offset = 0; + + uint8_t *pMem = nullptr; + + size_t it = 0; + if(UsesCurrentCountOffset) + it = StreamUniformBuffer.GetUsedCount(m_CurImageIndex); + for(; it < StreamUniformBuffer.GetBuffers(m_CurImageIndex).size(); ++it) + { + auto &BufferOfFrame = StreamUniformBuffer.GetBuffers(m_CurImageIndex)[it]; + if(BufferOfFrame.m_Size >= DataSize + BufferOfFrame.m_UsedSize) + { + if(BufferOfFrame.m_UsedSize == 0) + StreamUniformBuffer.IncreaseUsedCount(m_CurImageIndex); + Buffer = BufferOfFrame.m_Buffer; + BufferMem = BufferOfFrame.m_BufferMem; + Offset = BufferOfFrame.m_UsedSize; + BufferOfFrame.m_UsedSize += DataSize; + pMem = (uint8_t *)BufferOfFrame.m_pMappedBufferData; + pBufferMem = &BufferOfFrame; + break; + } + } + + if(BufferMem.m_Mem == VK_NULL_HANDLE) + { + // create memory + VkBuffer StreamBuffer; + SDeviceMemoryBlock StreamBufferMemory; + const VkDeviceSize NewBufferSingleSize = sizeof(TInstanceTypeName) * InstanceTypeCount; + const VkDeviceSize NewBufferSize = NewBufferSingleSize * BufferCreateCount; + CreateBuffer(NewBufferSize, MEMORY_BLOCK_USAGE_STREAM, Usage, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_CACHED_BIT, StreamBuffer, StreamBufferMemory); + + void *pMappedData = nullptr; + vkMapMemory(m_VKDevice, StreamBufferMemory.m_Mem, 0, VK_WHOLE_SIZE, 0, &pMappedData); + + size_t NewBufferIndex = StreamUniformBuffer.GetBuffers(m_CurImageIndex).size(); + for(size_t i = 0; i < BufferCreateCount; ++i) + { + StreamUniformBuffer.GetBuffers(m_CurImageIndex).push_back(TStreamMemName(StreamBuffer, StreamBufferMemory, NewBufferSingleSize * i, NewBufferSingleSize, 0, ((uint8_t *)pMappedData) + (NewBufferSingleSize * i))); + StreamUniformBuffer.GetRanges(m_CurImageIndex).push_back({}); + NewMemFunc(StreamUniformBuffer.GetBuffers(m_CurImageIndex).back(), StreamBuffer, NewBufferSingleSize * i); + } + auto &NewStreamBuffer = StreamUniformBuffer.GetBuffers(m_CurImageIndex)[NewBufferIndex]; + + Buffer = StreamBuffer; + BufferMem = StreamBufferMemory; + + pBufferMem = &NewStreamBuffer; + pMem = (uint8_t *)NewStreamBuffer.m_pMappedBufferData; + Offset = NewStreamBuffer.m_OffsetInBuffer; + NewStreamBuffer.m_UsedSize += DataSize; + + StreamUniformBuffer.IncreaseUsedCount(m_CurImageIndex); + } + + { + mem_copy(pMem + Offset, pData, (size_t)DataSize); + } + + NewBuffer = Buffer; + NewBufferMem = BufferMem; + BufferOffset = Offset; + } + + void CreateStreamVertexBuffer(size_t RenderThreadIndex, VkBuffer &NewBuffer, SDeviceMemoryBlock &NewBufferMem, size_t &BufferOffset, const void *pData, size_t DataSize) + { + SFrameBuffers *pStreamBuffer; + CreateStreamBuffer( + pStreamBuffer, [](SFrameBuffers &, VkBuffer, VkDeviceSize) {}, m_vStreamedVertexBuffers[RenderThreadIndex], VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, NewBuffer, NewBufferMem, BufferOffset, pData, DataSize); + } + + template + void GetUniformBufferObjectImpl(size_t RenderThreadIndex, bool RequiresSharedStagesDescriptor, SStreamMemory &StreamUniformBuffer, SDeviceDescriptorSet &DescrSet, const void *pData, size_t DataSize) + { + VkBuffer NewBuffer; + SDeviceMemoryBlock NewBufferMem; + size_t BufferOffset; + SFrameUniformBuffers *pMem; + CreateStreamBuffer( + pMem, + [this, RenderThreadIndex](SFrameBuffers &Mem, VkBuffer Buffer, VkDeviceSize MemOffset) { + CreateUniformDescriptorSets(RenderThreadIndex, m_SpriteMultiUniformDescriptorSetLayout, ((SFrameUniformBuffers *)(&Mem))->m_aUniformSets.data(), 1, Buffer, InstanceMaxParticleCount * sizeof(TName), MemOffset); + CreateUniformDescriptorSets(RenderThreadIndex, m_QuadUniformDescriptorSetLayout, &((SFrameUniformBuffers *)(&Mem))->m_aUniformSets[1], 1, Buffer, InstanceMaxParticleCount * sizeof(TName), MemOffset); + }, + StreamUniformBuffer, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, NewBuffer, NewBufferMem, BufferOffset, pData, DataSize); + + DescrSet = pMem->m_aUniformSets[RequiresSharedStagesDescriptor ? 1 : 0]; + } + + void GetUniformBufferObject(size_t RenderThreadIndex, bool RequiresSharedStagesDescriptor, SDeviceDescriptorSet &DescrSet, size_t ParticleCount, const void *pData, size_t DataSize) + { + GetUniformBufferObjectImpl(RenderThreadIndex, RequiresSharedStagesDescriptor, m_vStreamedUniformBuffers[RenderThreadIndex], DescrSet, pData, DataSize); + } + + bool CreateIndexBuffer(void *pData, size_t DataSize, VkBuffer &Buffer, SDeviceMemoryBlock &Memory) + { + VkDeviceSize BufferDataSize = DataSize; + + auto StagingBuffer = GetStagingBuffer(pData, DataSize); + + SDeviceMemoryBlock VertexBufferMemory; + VkBuffer VertexBuffer; + CreateBuffer(BufferDataSize, MEMORY_BLOCK_USAGE_BUFFER, VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, VertexBuffer, VertexBufferMemory); + + MemoryBarrier(VertexBuffer, 0, BufferDataSize, VK_ACCESS_INDEX_READ_BIT, true); + CopyBuffer(StagingBuffer.m_Buffer, VertexBuffer, StagingBuffer.m_HeapData.m_OffsetToAlign, 0, BufferDataSize); + MemoryBarrier(VertexBuffer, 0, BufferDataSize, VK_ACCESS_INDEX_READ_BIT, false); + + UploadAndFreeStagingMemBlock(StagingBuffer); + + Buffer = VertexBuffer; + Memory = VertexBufferMemory; + return true; + } + + void DestroyIndexBuffer(VkBuffer &Buffer, SDeviceMemoryBlock &Memory) + { + CleanBufferPair(0, Buffer, Memory); + } + + /************************ + * COMMAND IMPLEMENTATION + ************************/ + template + static bool IsInCommandRange(TName CMD, TName Min, TName Max) + { + return CMD >= Min && CMD < Max; + } + + bool RunCommand(const CCommandBuffer::SCommand *pBaseCommand) override + { + if(IsInCommandRangem_Cmd)>(pBaseCommand->m_Cmd, CCommandBuffer::CMD_FIRST, CCommandBuffer::CMD_COUNT)) + { + auto &CallbackObj = m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::ECommandBufferCMD(pBaseCommand->m_Cmd))]; + SRenderCommandExecuteBuffer Buffer; + Buffer.m_Command = (CCommandBuffer::ECommandBufferCMD)pBaseCommand->m_Cmd; + Buffer.m_pRawCommand = pBaseCommand; + Buffer.m_ThreadIndex = 0; + + if(m_CurCommandInPipe + 1 == m_CommandsInPipe && Buffer.m_Command != CCommandBuffer::CMD_FINISH) + { + m_LastCommandsInPipeThreadIndex = std::numeric_limits::max(); + } + + bool CanStartThread = false; + if(CallbackObj.m_IsRenderCommand) + { + bool ForceSingleThread = m_LastCommandsInPipeThreadIndex == std::numeric_limits::max(); + + size_t PotentiallyNextThread = (((m_CurCommandInPipe * (m_ThreadCount - 1)) / m_CommandsInPipe) + 1); + if(PotentiallyNextThread - 1 > m_LastCommandsInPipeThreadIndex) + { + CanStartThread = true; + m_LastCommandsInPipeThreadIndex = PotentiallyNextThread - 1; + } + Buffer.m_ThreadIndex = m_ThreadCount > 1 && !ForceSingleThread ? (m_LastCommandsInPipeThreadIndex + 1) : 0; + CallbackObj.m_FillExecuteBuffer(Buffer, pBaseCommand); + m_CurRenderCallCountInPipe += Buffer.m_EstimatedRenderCallCount; + } + bool Ret = true; + if(!CallbackObj.m_IsRenderCommand || (Buffer.m_ThreadIndex == 0 && !m_RenderingPaused)) + { + Ret = CallbackObj.m_CommandCB(pBaseCommand, Buffer); + } + else if(!m_RenderingPaused) + { + if(CanStartThread) + { + StartRenderThread(m_LastCommandsInPipeThreadIndex - 1); + } + m_ThreadCommandLists[Buffer.m_ThreadIndex - 1].push_back(Buffer); + } + + ++m_CurCommandInPipe; + return Ret; + } + + if(m_CurCommandInPipe + 1 == m_CommandsInPipe) + { + m_LastCommandsInPipeThreadIndex = std::numeric_limits::max(); + } + ++m_CurCommandInPipe; + + switch(pBaseCommand->m_Cmd) + { + case CCommandProcessorFragment_GLBase::CMD_INIT: + Cmd_Init(static_cast(pBaseCommand)); + break; + case CCommandProcessorFragment_GLBase::CMD_SHUTDOWN: + Cmd_Shutdown(static_cast(pBaseCommand)); + break; + + case CCommandProcessorFragment_GLBase::CMD_PRE_INIT: + Cmd_PreInit(static_cast(pBaseCommand)); + break; + case CCommandProcessorFragment_GLBase::CMD_POST_SHUTDOWN: + Cmd_PostShutdown(static_cast(pBaseCommand)); + break; + default: + return false; + } + + return true; + } + + void Cmd_Init(const SCommand_Init *pCommand) + { + pCommand->m_pCapabilities->m_TileBuffering = true; + pCommand->m_pCapabilities->m_QuadBuffering = true; + pCommand->m_pCapabilities->m_TextBuffering = true; + pCommand->m_pCapabilities->m_QuadContainerBuffering = true; + pCommand->m_pCapabilities->m_ShaderSupport = true; + + pCommand->m_pCapabilities->m_MipMapping = true; + pCommand->m_pCapabilities->m_3DTextures = false; + pCommand->m_pCapabilities->m_2DArrayTextures = true; + pCommand->m_pCapabilities->m_NPOTTextures = true; + + pCommand->m_pCapabilities->m_ContextMajor = 1; + pCommand->m_pCapabilities->m_ContextMinor = 1; + pCommand->m_pCapabilities->m_ContextPatch = 0; + + pCommand->m_pCapabilities->m_TrianglesAsQuads = true; + + m_GlobalTextureLodBIAS = g_Config.m_GfxGLTextureLODBIAS; + m_pTextureMemoryUsage = pCommand->m_pTextureMemoryUsage; + m_pBufferMemoryUsage = pCommand->m_pBufferMemoryUsage; + m_pStreamMemoryUsage = pCommand->m_pStreamMemoryUsage; + m_pStagingMemoryUsage = pCommand->m_pStagingMemoryUsage; + + m_MultiSamplingCount = (g_Config.m_GfxFsaaSamples & 0xFFFFFFFE); // ignore the uneven bit, only even multi sampling works + + TGLBackendReadPresentedImageData &ReadPresentedImgDataFunc = *pCommand->m_pReadPresentedImageDataFunc; + ReadPresentedImgDataFunc = [this](uint32_t &Width, uint32_t &Height, uint32_t &Format, std::vector &DstData) { return GetPresentedImageData(Width, Height, Format, DstData); }; + + m_pWindow = pCommand->m_pWindow; + + *pCommand->m_pInitError = m_VKInstance != VK_NULL_HANDLE ? 0 : -1; + + if(m_VKInstance == VK_NULL_HANDLE) + { + return; + } + + m_pStorage = pCommand->m_pStorage; + InitVulkan(); + + std::array aIndices; + int Primq = 0; + for(int i = 0; i < CCommandBuffer::MAX_VERTICES / 4 * 6; i += 6) + { + aIndices[i] = Primq; + aIndices[i + 1] = Primq + 1; + aIndices[i + 2] = Primq + 2; + aIndices[i + 3] = Primq; + aIndices[i + 4] = Primq + 2; + aIndices[i + 5] = Primq + 3; + Primq += 4; + } + PrepareFrame(); + + CreateIndexBuffer(aIndices.data(), sizeof(uint32_t) * aIndices.size(), m_IndexBuffer, m_IndexBufferMemory); + CreateIndexBuffer(aIndices.data(), sizeof(uint32_t) * aIndices.size(), m_RenderIndexBuffer, m_RenderIndexBufferMemory); + m_CurRenderIndexPrimitiveCount = CCommandBuffer::MAX_VERTICES / 4; + } + + void Cmd_Shutdown(const SCommand_Shutdown *pCommand) + { + vkDeviceWaitIdle(m_VKDevice); + + DestroyIndexBuffer(m_IndexBuffer, m_IndexBufferMemory); + DestroyIndexBuffer(m_RenderIndexBuffer, m_RenderIndexBufferMemory); + + CleanupVulkan(); + } + + void Cmd_Texture_Update(const CCommandBuffer::SCommand_Texture_Update *pCommand) + { + size_t IndexTex = pCommand->m_Slot; + + void *pData = pCommand->m_pData; + + UpdateTexture(IndexTex, VK_FORMAT_B8G8R8A8_UNORM, pData, pCommand->m_X, pCommand->m_Y, pCommand->m_Width, pCommand->m_Height, TexFormatToImageColorChannelCount(pCommand->m_Format)); + + free(pData); + } + + void Cmd_Texture_Destroy(const CCommandBuffer::SCommand_Texture_Destroy *pCommand) + { + size_t ImageIndex = (size_t)pCommand->m_Slot; + auto &Texture = m_Textures[ImageIndex]; + + m_FrameDelayedTextureCleanup[m_CurImageIndex].push_back(Texture); + + Texture = CTexture{}; + } + + void Cmd_Texture_Create(const CCommandBuffer::SCommand_Texture_Create *pCommand) + { + int Slot = pCommand->m_Slot; + int Width = pCommand->m_Width; + int Height = pCommand->m_Height; + int PixelSize = pCommand->m_PixelSize; + int Format = pCommand->m_Format; + int StoreFormat = pCommand->m_StoreFormat; + int Flags = pCommand->m_Flags; + void *pData = pCommand->m_pData; + + CreateTextureCMD(Slot, Width, Height, PixelSize, TextureFormatToVulkanFormat(Format), TextureFormatToVulkanFormat(StoreFormat), Flags, pData); + + free(pData); + } + + void Cmd_TextTextures_Create(const CCommandBuffer::SCommand_TextTextures_Create *pCommand) + { + int Slot = pCommand->m_Slot; + int SlotOutline = pCommand->m_SlotOutline; + int Width = pCommand->m_Width; + int Height = pCommand->m_Height; + + void *pTmpData = pCommand->m_pTextData; + void *pTmpData2 = pCommand->m_pTextOutlineData; + + CreateTextureCMD(Slot, Width, Height, 1, VK_FORMAT_R8_UNORM, VK_FORMAT_R8_UNORM, CCommandBuffer::TEXFLAG_NOMIPMAPS, pTmpData); + CreateTextureCMD(SlotOutline, Width, Height, 1, VK_FORMAT_R8_UNORM, VK_FORMAT_R8_UNORM, CCommandBuffer::TEXFLAG_NOMIPMAPS, pTmpData2); + + CreateNewTextDescriptorSets(Slot, SlotOutline); + + free(pTmpData); + free(pTmpData2); + } + + void Cmd_TextTextures_Destroy(const CCommandBuffer::SCommand_TextTextures_Destroy *pCommand) + { + size_t ImageIndex = (size_t)pCommand->m_Slot; + size_t ImageIndexOutline = (size_t)pCommand->m_SlotOutline; + auto &Texture = m_Textures[ImageIndex]; + auto &TextureOutline = m_Textures[ImageIndexOutline]; + + m_FrameDelayedTextTexturesCleanup[m_CurImageIndex].push_back({Texture, TextureOutline}); + + Texture = {}; + TextureOutline = {}; + } + + void Cmd_TextTexture_Update(const CCommandBuffer::SCommand_TextTexture_Update *pCommand) + { + size_t IndexTex = pCommand->m_Slot; + + void *pData = pCommand->m_pData; + + UpdateTexture(IndexTex, VK_FORMAT_R8_UNORM, pData, pCommand->m_X, pCommand->m_Y, pCommand->m_Width, pCommand->m_Height, 1); + + free(pData); + } + + void Cmd_Clear_FillExecuteBuffer(SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand_Clear *pCommand) + { + if(!pCommand->m_ForceClear) + { + bool ColorChanged = m_aClearColor[0] != pCommand->m_Color.r || m_aClearColor[1] != pCommand->m_Color.g || + m_aClearColor[2] != pCommand->m_Color.b || m_aClearColor[3] != pCommand->m_Color.a; + m_aClearColor[0] = pCommand->m_Color.r; + m_aClearColor[1] = pCommand->m_Color.g; + m_aClearColor[2] = pCommand->m_Color.b; + m_aClearColor[3] = pCommand->m_Color.a; + if(ColorChanged) + ExecBuffer.m_ClearColorInRenderThread = true; + } + else + { + ExecBuffer.m_ClearColorInRenderThread = true; + } + ExecBuffer.m_EstimatedRenderCallCount = 0; + } + + void Cmd_Clear(SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand_Clear *pCommand) + { + if(ExecBuffer.m_ClearColorInRenderThread) + { + std::array aAttachments = {VkClearAttachment{VK_IMAGE_ASPECT_COLOR_BIT, 0, VkClearValue{VkClearColorValue{{pCommand->m_Color.r, pCommand->m_Color.g, pCommand->m_Color.b, pCommand->m_Color.a}}}}}; + std::array aClearRects = {VkClearRect{{{0, 0}, m_VKSwapImgAndViewportExtent.m_SwapImg}, 0, 1}}; + vkCmdClearAttachments(GetGraphicCommandBuffer(ExecBuffer.m_ThreadIndex), aAttachments.size(), aAttachments.data(), aClearRects.size(), aClearRects.data()); + } + } + + void Cmd_Render_FillExecuteBuffer(SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand_Render *pCommand) + { + bool IsTextured = GetIsTextured(pCommand->m_State); + if(IsTextured) + { + size_t AddressModeIndex = GetAddressModeIndex(pCommand->m_State); + auto &DescrSet = m_Textures[pCommand->m_State.m_Texture].m_aVKStandardTexturedDescrSets[AddressModeIndex]; + ExecBuffer.m_aDescriptors[0] = DescrSet; + } + + ExecBuffer.m_IndexBuffer = m_IndexBuffer; + + ExecBuffer.m_EstimatedRenderCallCount = 1; + + ExecBufferFillDynamicStates(pCommand->m_State, ExecBuffer); + } + + void Cmd_Render(const CCommandBuffer::SCommand_Render *pCommand, SRenderCommandExecuteBuffer &ExecBuffer) + { + RenderStandard(ExecBuffer, pCommand->m_State, pCommand->m_PrimType, pCommand->m_pVertices, pCommand->m_PrimCount); + } + + void Cmd_Screenshot(const CCommandBuffer::SCommand_TrySwapAndScreenshot *pCommand) + { + NextFrame(); + *pCommand->m_pSwapped = true; + + uint32_t Width; + uint32_t Height; + uint32_t Format; + if(GetPresentedImageDataImpl(Width, Height, Format, m_ScreenshotHelper, false, true)) + { + size_t ImgSize = (size_t)Width * (size_t)Height * (size_t)4; + pCommand->m_pImage->m_pData = malloc(ImgSize); + mem_copy(pCommand->m_pImage->m_pData, m_ScreenshotHelper.data(), ImgSize); + } + else + { + pCommand->m_pImage->m_pData = nullptr; + } + pCommand->m_pImage->m_Width = (int)Width; + pCommand->m_pImage->m_Height = (int)Height; + pCommand->m_pImage->m_Format = (int)Format; + } + + void Cmd_RenderTex3D_FillExecuteBuffer(SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand_RenderTex3D *pCommand) + { + bool IsTextured = GetIsTextured(pCommand->m_State); + if(IsTextured) + { + auto &DescrSet = m_Textures[pCommand->m_State.m_Texture].m_VKStandard3DTexturedDescrSet; + ExecBuffer.m_aDescriptors[0] = DescrSet; + } + + ExecBuffer.m_IndexBuffer = m_IndexBuffer; + + ExecBuffer.m_EstimatedRenderCallCount = 1; + + ExecBufferFillDynamicStates(pCommand->m_State, ExecBuffer); + } + + void Cmd_RenderTex3D(const CCommandBuffer::SCommand_RenderTex3D *pCommand, SRenderCommandExecuteBuffer &ExecBuffer) + { + RenderStandard(ExecBuffer, pCommand->m_State, pCommand->m_PrimType, pCommand->m_pVertices, pCommand->m_PrimCount); + } + + void Cmd_Update_Viewport_FillExecuteBuffer(SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand_Update_Viewport *pCommand) + { + ExecBuffer.m_EstimatedRenderCallCount = 0; + } + + void Cmd_Update_Viewport(const CCommandBuffer::SCommand_Update_Viewport *pCommand, bool CalledByMainRenderThread) + { + if(pCommand->m_ByResize && CalledByMainRenderThread) + { + if(IsVerbose()) + { + dbg_msg("vulkan", "queueing swap chain recreation because the viewport changed"); + } + m_CanvasWidth = (uint32_t)pCommand->m_Width; + m_CanvasHeight = (uint32_t)pCommand->m_Height; + m_RecreateSwapChain = true; + } + else if(!pCommand->m_ByResize && !CalledByMainRenderThread) + { + if(pCommand->m_X != 0 || pCommand->m_Y != 0 || (uint32_t)pCommand->m_Width != m_VKSwapImgAndViewportExtent.m_Viewport.width || (uint32_t)pCommand->m_Height != m_VKSwapImgAndViewportExtent.m_Viewport.height) + { + m_HasDynamicViewport = true; + + // convert viewport from OGL to vulkan + int32_t ViewportY = (int32_t)m_VKSwapImgAndViewportExtent.m_Viewport.height - ((int32_t)pCommand->m_Y + (int32_t)pCommand->m_Height); + uint32_t ViewportH = (int32_t)pCommand->m_Height; + m_DynamicViewportOffset = {(int32_t)pCommand->m_X, ViewportY}; + m_DynamicViewportSize = {(uint32_t)pCommand->m_Width, ViewportH}; + } + else + { + m_HasDynamicViewport = false; + } + } + } + + void Cmd_VSync(const CCommandBuffer::SCommand_VSync *pCommand) + { + if(IsVerbose()) + { + dbg_msg("vulkan", "queueing swap chain recreation because vsync was changed"); + } + m_RecreateSwapChain = true; + *pCommand->m_pRetOk = true; + } + + void Cmd_Finish(const CCommandBuffer::SCommand_Finish *pCommand) + { // just ignore it with vulkan + } + + void Cmd_Swap(const CCommandBuffer::SCommand_Swap *pCommand) + { + NextFrame(); + } + + void Cmd_CreateBufferObject(const CCommandBuffer::SCommand_CreateBufferObject *pCommand) + { + bool IsOneFrameBuffer = (pCommand->m_Flags & IGraphics::EBufferObjectCreateFlags::BUFFER_OBJECT_CREATE_FLAGS_ONE_TIME_USE_BIT) != 0; + CreateBufferObject((size_t)pCommand->m_BufferIndex, pCommand->m_pUploadData, (VkDeviceSize)pCommand->m_DataSize, IsOneFrameBuffer); + if(pCommand->m_DeletePointer) + free(pCommand->m_pUploadData); + } + + void Cmd_UpdateBufferObject(const CCommandBuffer::SCommand_UpdateBufferObject *pCommand) + { + size_t BufferIndex = (size_t)pCommand->m_BufferIndex; + bool DeletePointer = pCommand->m_DeletePointer; + VkDeviceSize Offset = (VkDeviceSize)((intptr_t)pCommand->m_pOffset); + void *pUploadData = pCommand->m_pUploadData; + VkDeviceSize DataSize = (VkDeviceSize)pCommand->m_DataSize; + + auto StagingBuffer = GetStagingBuffer(pUploadData, DataSize); + + auto &MemBlock = m_BufferObjects[BufferIndex].m_BufferObject.m_Mem; + VkBuffer VertexBuffer = MemBlock.m_Buffer; + MemoryBarrier(VertexBuffer, Offset + MemBlock.m_HeapData.m_OffsetToAlign, DataSize, VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT, true); + CopyBuffer(StagingBuffer.m_Buffer, VertexBuffer, StagingBuffer.m_HeapData.m_OffsetToAlign, Offset + MemBlock.m_HeapData.m_OffsetToAlign, DataSize); + MemoryBarrier(VertexBuffer, Offset + MemBlock.m_HeapData.m_OffsetToAlign, DataSize, VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT, false); + + UploadAndFreeStagingMemBlock(StagingBuffer); + + if(DeletePointer) + free(pUploadData); + } + + void Cmd_RecreateBufferObject(const CCommandBuffer::SCommand_RecreateBufferObject *pCommand) + { + DeleteBufferObject((size_t)pCommand->m_BufferIndex); + bool IsOneFrameBuffer = (pCommand->m_Flags & IGraphics::EBufferObjectCreateFlags::BUFFER_OBJECT_CREATE_FLAGS_ONE_TIME_USE_BIT) != 0; + CreateBufferObject((size_t)pCommand->m_BufferIndex, pCommand->m_pUploadData, (VkDeviceSize)pCommand->m_DataSize, IsOneFrameBuffer); + } + + void Cmd_CopyBufferObject(const CCommandBuffer::SCommand_CopyBufferObject *pCommand) + { + size_t ReadBufferIndex = (size_t)pCommand->m_ReadBufferIndex; + size_t WriteBufferIndex = (size_t)pCommand->m_WriteBufferIndex; + auto &ReadMemBlock = m_BufferObjects[ReadBufferIndex].m_BufferObject.m_Mem; + auto &WriteMemBlock = m_BufferObjects[WriteBufferIndex].m_BufferObject.m_Mem; + VkBuffer ReadBuffer = ReadMemBlock.m_Buffer; + VkBuffer WriteBuffer = WriteMemBlock.m_Buffer; + + VkDeviceSize DataSize = (VkDeviceSize)pCommand->m_CopySize; + VkDeviceSize ReadOffset = (VkDeviceSize)pCommand->m_pReadOffset + ReadMemBlock.m_HeapData.m_OffsetToAlign; + VkDeviceSize WriteOffset = (VkDeviceSize)pCommand->m_pWriteOffset + WriteMemBlock.m_HeapData.m_OffsetToAlign; + + MemoryBarrier(ReadBuffer, ReadOffset, DataSize, VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT, true); + MemoryBarrier(WriteBuffer, WriteOffset, DataSize, VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT, true); + CopyBuffer(ReadBuffer, WriteBuffer, ReadOffset, WriteOffset, DataSize); + MemoryBarrier(WriteBuffer, WriteOffset, DataSize, VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT, false); + MemoryBarrier(ReadBuffer, ReadOffset, DataSize, VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT, false); + } + + void Cmd_DeleteBufferObject(const CCommandBuffer::SCommand_DeleteBufferObject *pCommand) + { + size_t BufferIndex = (size_t)pCommand->m_BufferIndex; + DeleteBufferObject(BufferIndex); + } + + void Cmd_CreateBufferContainer(const CCommandBuffer::SCommand_CreateBufferContainer *pCommand) + { + size_t ContainerIndex = (size_t)pCommand->m_BufferContainerIndex; + while(ContainerIndex >= m_BufferContainers.size()) + m_BufferContainers.resize((m_BufferContainers.size() * 2) + 1); + + m_BufferContainers[ContainerIndex].m_BufferObjectIndex = pCommand->m_VertBufferBindingIndex; + } + + void Cmd_UpdateBufferContainer(const CCommandBuffer::SCommand_UpdateBufferContainer *pCommand) + { + size_t ContainerIndex = (size_t)pCommand->m_BufferContainerIndex; + m_BufferContainers[ContainerIndex].m_BufferObjectIndex = pCommand->m_VertBufferBindingIndex; + } + + void Cmd_DeleteBufferContainer(const CCommandBuffer::SCommand_DeleteBufferContainer *pCommand) + { + size_t ContainerIndex = (size_t)pCommand->m_BufferContainerIndex; + bool DeleteAllBO = pCommand->m_DestroyAllBO; + if(DeleteAllBO) + { + size_t BufferIndex = (size_t)m_BufferContainers[ContainerIndex].m_BufferObjectIndex; + DeleteBufferObject(BufferIndex); + } + } + + void Cmd_IndicesRequiredNumNotify(const CCommandBuffer::SCommand_IndicesRequiredNumNotify *pCommand) + { + size_t IndicesCount = pCommand->m_RequiredIndicesNum; + if(m_CurRenderIndexPrimitiveCount < IndicesCount / 6) + { + m_FrameDelayedBufferCleanup[m_CurImageIndex].push_back({m_RenderIndexBuffer, m_RenderIndexBufferMemory}); + std::vector Indices(IndicesCount); + uint32_t Primq = 0; + for(size_t i = 0; i < IndicesCount; i += 6) + { + Indices[i] = Primq; + Indices[i + 1] = Primq + 1; + Indices[i + 2] = Primq + 2; + Indices[i + 3] = Primq; + Indices[i + 4] = Primq + 2; + Indices[i + 5] = Primq + 3; + Primq += 4; + } + CreateIndexBuffer(Indices.data(), Indices.size() * sizeof(uint32_t), m_RenderIndexBuffer, m_RenderIndexBufferMemory); + m_CurRenderIndexPrimitiveCount = IndicesCount / 6; + } + } + + void Cmd_RenderTileLayer_FillExecuteBuffer(SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand_RenderTileLayer *pCommand) + { + RenderTileLayer_FillExecuteBuffer(ExecBuffer, pCommand->m_IndicesDrawNum, pCommand->m_State, pCommand->m_BufferContainerIndex); + } + + void Cmd_RenderTileLayer(const CCommandBuffer::SCommand_RenderTileLayer *pCommand, SRenderCommandExecuteBuffer &ExecBuffer) + { + int Type = 0; + vec2 Dir{}; + vec2 Off{}; + int32_t JumpIndex = 0; + RenderTileLayer(ExecBuffer, pCommand->m_State, Type, pCommand->m_Color, Dir, Off, JumpIndex, (size_t)pCommand->m_IndicesDrawNum, pCommand->m_pIndicesOffsets, pCommand->m_pDrawCount, 1); + } + + void Cmd_RenderBorderTile_FillExecuteBuffer(SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand_RenderBorderTile *pCommand) + { + RenderTileLayer_FillExecuteBuffer(ExecBuffer, 1, pCommand->m_State, pCommand->m_BufferContainerIndex); + } + + void Cmd_RenderBorderTile(const CCommandBuffer::SCommand_RenderBorderTile *pCommand, SRenderCommandExecuteBuffer &ExecBuffer) + { + int Type = 1; + vec2 Dir = {pCommand->m_Dir[0], pCommand->m_Dir[1]}; + vec2 Off = {pCommand->m_Offset[0], pCommand->m_Offset[1]}; + unsigned int DrawNum = 6; + RenderTileLayer(ExecBuffer, pCommand->m_State, Type, pCommand->m_Color, Dir, Off, pCommand->m_JumpIndex, (size_t)1, &pCommand->m_pIndicesOffset, &DrawNum, pCommand->m_DrawNum); + } + + void Cmd_RenderBorderTileLine_FillExecuteBuffer(SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand_RenderBorderTileLine *pCommand) + { + RenderTileLayer_FillExecuteBuffer(ExecBuffer, 1, pCommand->m_State, pCommand->m_BufferContainerIndex); + } + + void Cmd_RenderBorderTileLine(const CCommandBuffer::SCommand_RenderBorderTileLine *pCommand, SRenderCommandExecuteBuffer &ExecBuffer) + { + int Type = 2; + vec2 Dir = {pCommand->m_Dir[0], pCommand->m_Dir[1]}; + vec2 Off = {pCommand->m_Offset[0], pCommand->m_Offset[1]}; + RenderTileLayer(ExecBuffer, pCommand->m_State, Type, pCommand->m_Color, Dir, Off, 0, (size_t)1, &pCommand->m_pIndicesOffset, &pCommand->m_IndexDrawNum, pCommand->m_DrawNum); + } + + void Cmd_RenderQuadLayer_FillExecuteBuffer(SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand_RenderQuadLayer *pCommand) + { + size_t BufferContainerIndex = (size_t)pCommand->m_BufferContainerIndex; + size_t BufferObjectIndex = (size_t)m_BufferContainers[BufferContainerIndex].m_BufferObjectIndex; + auto &BufferObject = m_BufferObjects[BufferObjectIndex]; + + ExecBuffer.m_Buffer = BufferObject.m_CurBuffer; + ExecBuffer.m_BufferOff = BufferObject.m_CurBufferOffset; + + bool IsTextured = GetIsTextured(pCommand->m_State); + if(IsTextured) + { + size_t AddressModeIndex = GetAddressModeIndex(pCommand->m_State); + auto &DescrSet = m_Textures[pCommand->m_State.m_Texture].m_aVKStandardTexturedDescrSets[AddressModeIndex]; + ExecBuffer.m_aDescriptors[0] = DescrSet; + } + + ExecBuffer.m_IndexBuffer = m_RenderIndexBuffer; + + ExecBuffer.m_EstimatedRenderCallCount = ((pCommand->m_QuadNum - 1) / gs_GraphicsMaxQuadsRenderCount) + 1; + + ExecBufferFillDynamicStates(pCommand->m_State, ExecBuffer); + } + + void Cmd_RenderQuadLayer(const CCommandBuffer::SCommand_RenderQuadLayer *pCommand, SRenderCommandExecuteBuffer &ExecBuffer) + { + std::array m; + GetStateMatrix(pCommand->m_State, m); + + bool CanBePushed = pCommand->m_QuadNum == 1; + + bool IsTextured; + size_t BlendModeIndex; + size_t DynamicIndex; + size_t AddressModeIndex; + GetStateIndices(pCommand->m_State, IsTextured, BlendModeIndex, DynamicIndex, AddressModeIndex); + auto &PipeLayout = GetPipeLayout(CanBePushed ? m_QuadPushPipeline : m_QuadPipeline, IsTextured, BlendModeIndex, DynamicIndex); + auto &PipeLine = GetPipeline(CanBePushed ? m_QuadPushPipeline : m_QuadPipeline, IsTextured, BlendModeIndex, DynamicIndex); + + auto &CommandBuffer = GetGraphicCommandBuffer(ExecBuffer.m_ThreadIndex); + + BindPipeline(ExecBuffer.m_ThreadIndex, CommandBuffer, ExecBuffer, PipeLine, pCommand->m_State); + + std::array aVertexBuffers = {ExecBuffer.m_Buffer}; + std::array aOffsets = {(VkDeviceSize)ExecBuffer.m_BufferOff}; + vkCmdBindVertexBuffers(CommandBuffer, 0, 1, aVertexBuffers.data(), aOffsets.data()); + + vkCmdBindIndexBuffer(CommandBuffer, ExecBuffer.m_IndexBuffer, 0, VK_INDEX_TYPE_UINT32); + + if(IsTextured) + { + vkCmdBindDescriptorSets(CommandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, PipeLayout, 0, 1, &ExecBuffer.m_aDescriptors[0].m_Descriptor, 0, nullptr); + } + + if(CanBePushed) + { + SUniformQuadPushGPos PushConstantVertex; + + mem_copy(&PushConstantVertex.m_BOPush, &pCommand->m_pQuadInfo[0], sizeof(PushConstantVertex.m_BOPush)); + + mem_copy(PushConstantVertex.m_aPos, m.data(), sizeof(PushConstantVertex.m_aPos)); + PushConstantVertex.m_QuadOffset = pCommand->m_QuadOffset; + + vkCmdPushConstants(CommandBuffer, PipeLayout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(SUniformQuadPushGPos), &PushConstantVertex); + } + else + { + SUniformQuadGPos PushConstantVertex; + mem_copy(PushConstantVertex.m_aPos, m.data(), sizeof(PushConstantVertex.m_aPos)); + PushConstantVertex.m_QuadOffset = pCommand->m_QuadOffset; + + vkCmdPushConstants(CommandBuffer, PipeLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(PushConstantVertex), &PushConstantVertex); + } + + uint32_t DrawCount = (uint32_t)pCommand->m_QuadNum; + size_t RenderOffset = 0; + + while(DrawCount > 0) + { + uint32_t RealDrawCount = (DrawCount > gs_GraphicsMaxQuadsRenderCount ? gs_GraphicsMaxQuadsRenderCount : DrawCount); + + VkDeviceSize IndexOffset = (VkDeviceSize)((ptrdiff_t)(pCommand->m_QuadOffset + RenderOffset) * 6); + if(!CanBePushed) + { + // create uniform buffer + SDeviceDescriptorSet UniDescrSet; + GetUniformBufferObject(ExecBuffer.m_ThreadIndex, true, UniDescrSet, RealDrawCount, (const float *)(pCommand->m_pQuadInfo + RenderOffset), RealDrawCount * sizeof(SQuadRenderInfo)); + + vkCmdBindDescriptorSets(CommandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, PipeLayout, IsTextured ? 1 : 0, 1, &UniDescrSet.m_Descriptor, 0, nullptr); + if(RenderOffset > 0) + { + int32_t QuadOffset = pCommand->m_QuadOffset + RenderOffset; + vkCmdPushConstants(CommandBuffer, PipeLayout, VK_SHADER_STAGE_VERTEX_BIT, sizeof(SUniformQuadGPos) - sizeof(int32_t), sizeof(int32_t), &QuadOffset); + } + } + + vkCmdDrawIndexed(CommandBuffer, static_cast(RealDrawCount * 6), 1, IndexOffset, 0, 0); + + RenderOffset += RealDrawCount; + DrawCount -= RealDrawCount; + } + } + + void Cmd_RenderText_FillExecuteBuffer(SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand_RenderText *pCommand) + { + size_t BufferContainerIndex = (size_t)pCommand->m_BufferContainerIndex; + size_t BufferObjectIndex = (size_t)m_BufferContainers[BufferContainerIndex].m_BufferObjectIndex; + auto &BufferObject = m_BufferObjects[BufferObjectIndex]; + + ExecBuffer.m_Buffer = BufferObject.m_CurBuffer; + ExecBuffer.m_BufferOff = BufferObject.m_CurBufferOffset; + + auto &TextTextureDescr = m_Textures[pCommand->m_TextTextureIndex].m_VKTextDescrSet; + ExecBuffer.m_aDescriptors[0] = TextTextureDescr; + + ExecBuffer.m_IndexBuffer = m_RenderIndexBuffer; + + ExecBuffer.m_EstimatedRenderCallCount = 1; + + ExecBufferFillDynamicStates(pCommand->m_State, ExecBuffer); + } + + void Cmd_RenderText(const CCommandBuffer::SCommand_RenderText *pCommand, SRenderCommandExecuteBuffer &ExecBuffer) + { + std::array m; + GetStateMatrix(pCommand->m_State, m); + + bool IsTextured; + size_t BlendModeIndex; + size_t DynamicIndex; + size_t AddressModeIndex; + GetStateIndices(pCommand->m_State, IsTextured, BlendModeIndex, DynamicIndex, AddressModeIndex); + IsTextured = true; // text is always textured + auto &PipeLayout = GetPipeLayout(m_TextPipeline, IsTextured, BlendModeIndex, DynamicIndex); + auto &PipeLine = GetPipeline(m_TextPipeline, IsTextured, BlendModeIndex, DynamicIndex); + + auto &CommandBuffer = GetGraphicCommandBuffer(ExecBuffer.m_ThreadIndex); + + BindPipeline(ExecBuffer.m_ThreadIndex, CommandBuffer, ExecBuffer, PipeLine, pCommand->m_State); + + std::array aVertexBuffers = {ExecBuffer.m_Buffer}; + std::array aOffsets = {(VkDeviceSize)ExecBuffer.m_BufferOff}; + vkCmdBindVertexBuffers(CommandBuffer, 0, 1, aVertexBuffers.data(), aOffsets.data()); + + vkCmdBindIndexBuffer(CommandBuffer, ExecBuffer.m_IndexBuffer, 0, VK_INDEX_TYPE_UINT32); + + vkCmdBindDescriptorSets(CommandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, PipeLayout, 0, 1, &ExecBuffer.m_aDescriptors[0].m_Descriptor, 0, nullptr); + + SUniformGTextPos PosTexSizeConstant; + mem_copy(PosTexSizeConstant.m_aPos, m.data(), m.size() * sizeof(float)); + PosTexSizeConstant.m_TextureSize = pCommand->m_TextureSize; + + vkCmdPushConstants(CommandBuffer, PipeLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(SUniformGTextPos), &PosTexSizeConstant); + + SUniformTextFragment FragmentConstants; + + mem_copy(FragmentConstants.m_Constants.m_aTextColor, pCommand->m_aTextColor, sizeof(FragmentConstants.m_Constants.m_aTextColor)); + mem_copy(FragmentConstants.m_Constants.m_aTextOutlineColor, pCommand->m_aTextOutlineColor, sizeof(FragmentConstants.m_Constants.m_aTextOutlineColor)); + vkCmdPushConstants(CommandBuffer, PipeLayout, VK_SHADER_STAGE_FRAGMENT_BIT, sizeof(SUniformGTextPos) + sizeof(SUniformTextGFragmentOffset), sizeof(SUniformTextFragment), &FragmentConstants); + + vkCmdDrawIndexed(CommandBuffer, static_cast(pCommand->m_DrawNum), 1, 0, 0, 0); + } + + void BufferContainer_FillExecuteBuffer(SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SState &State, size_t BufferContainerIndex, size_t DrawCalls) + { + size_t BufferObjectIndex = (size_t)m_BufferContainers[BufferContainerIndex].m_BufferObjectIndex; + auto &BufferObject = m_BufferObjects[BufferObjectIndex]; + + ExecBuffer.m_Buffer = BufferObject.m_CurBuffer; + ExecBuffer.m_BufferOff = BufferObject.m_CurBufferOffset; + + bool IsTextured = GetIsTextured(State); + if(IsTextured) + { + size_t AddressModeIndex = GetAddressModeIndex(State); + auto &DescrSet = m_Textures[State.m_Texture].m_aVKStandardTexturedDescrSets[AddressModeIndex]; + ExecBuffer.m_aDescriptors[0] = DescrSet; + } + + ExecBuffer.m_IndexBuffer = m_RenderIndexBuffer; + + ExecBuffer.m_EstimatedRenderCallCount = DrawCalls; + + ExecBufferFillDynamicStates(State, ExecBuffer); + } + + void Cmd_RenderQuadContainer_FillExecuteBuffer(SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand_RenderQuadContainer *pCommand) + { + BufferContainer_FillExecuteBuffer(ExecBuffer, pCommand->m_State, (size_t)pCommand->m_BufferContainerIndex, 1); + } + + void Cmd_RenderQuadContainer(const CCommandBuffer::SCommand_RenderQuadContainer *pCommand, SRenderCommandExecuteBuffer &ExecBuffer) + { + std::array m; + GetStateMatrix(pCommand->m_State, m); + + bool IsTextured; + size_t BlendModeIndex; + size_t DynamicIndex; + size_t AddressModeIndex; + GetStateIndices(pCommand->m_State, IsTextured, BlendModeIndex, DynamicIndex, AddressModeIndex); + auto &PipeLayout = GetStandardPipeLayout(false, IsTextured, BlendModeIndex, DynamicIndex); + auto &PipeLine = GetStandardPipe(false, IsTextured, BlendModeIndex, DynamicIndex); + + auto &CommandBuffer = GetGraphicCommandBuffer(ExecBuffer.m_ThreadIndex); + + BindPipeline(ExecBuffer.m_ThreadIndex, CommandBuffer, ExecBuffer, PipeLine, pCommand->m_State); + + std::array aVertexBuffers = {ExecBuffer.m_Buffer}; + std::array aOffsets = {(VkDeviceSize)ExecBuffer.m_BufferOff}; + vkCmdBindVertexBuffers(CommandBuffer, 0, 1, aVertexBuffers.data(), aOffsets.data()); + + VkDeviceSize IndexOffset = (VkDeviceSize)((ptrdiff_t)pCommand->m_pOffset); + + vkCmdBindIndexBuffer(CommandBuffer, ExecBuffer.m_IndexBuffer, IndexOffset, VK_INDEX_TYPE_UINT32); + + if(IsTextured) + { + vkCmdBindDescriptorSets(CommandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, PipeLayout, 0, 1, &ExecBuffer.m_aDescriptors[0].m_Descriptor, 0, nullptr); + } + + vkCmdPushConstants(CommandBuffer, PipeLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(SUniformGPos), m.data()); + + vkCmdDrawIndexed(CommandBuffer, static_cast(pCommand->m_DrawNum), 1, 0, 0, 0); + } + + void Cmd_RenderQuadContainerEx_FillExecuteBuffer(SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand_RenderQuadContainerEx *pCommand) + { + BufferContainer_FillExecuteBuffer(ExecBuffer, pCommand->m_State, (size_t)pCommand->m_BufferContainerIndex, 1); + } + + void Cmd_RenderQuadContainerEx(const CCommandBuffer::SCommand_RenderQuadContainerEx *pCommand, SRenderCommandExecuteBuffer &ExecBuffer) + { + std::array m; + GetStateMatrix(pCommand->m_State, m); + + bool IsRotationless = !(pCommand->m_Rotation != 0); + bool IsTextured; + size_t BlendModeIndex; + size_t DynamicIndex; + size_t AddressModeIndex; + GetStateIndices(pCommand->m_State, IsTextured, BlendModeIndex, DynamicIndex, AddressModeIndex); + auto &PipeLayout = GetPipeLayout(IsRotationless ? m_PrimExRotationlessPipeline : m_PrimExPipeline, IsTextured, BlendModeIndex, DynamicIndex); + auto &PipeLine = GetPipeline(IsRotationless ? m_PrimExRotationlessPipeline : m_PrimExPipeline, IsTextured, BlendModeIndex, DynamicIndex); + + auto &CommandBuffer = GetGraphicCommandBuffer(ExecBuffer.m_ThreadIndex); + + BindPipeline(ExecBuffer.m_ThreadIndex, CommandBuffer, ExecBuffer, PipeLine, pCommand->m_State); + + std::array aVertexBuffers = {ExecBuffer.m_Buffer}; + std::array aOffsets = {(VkDeviceSize)ExecBuffer.m_BufferOff}; + vkCmdBindVertexBuffers(CommandBuffer, 0, 1, aVertexBuffers.data(), aOffsets.data()); + + VkDeviceSize IndexOffset = (VkDeviceSize)((ptrdiff_t)pCommand->m_pOffset); + + vkCmdBindIndexBuffer(CommandBuffer, ExecBuffer.m_IndexBuffer, IndexOffset, VK_INDEX_TYPE_UINT32); + + if(IsTextured) + { + vkCmdBindDescriptorSets(CommandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, PipeLayout, 0, 1, &ExecBuffer.m_aDescriptors[0].m_Descriptor, 0, nullptr); + } + + SUniformPrimExGVertColor PushConstantColor; + SUniformPrimExGPos PushConstantVertex; + size_t VertexPushConstantSize = sizeof(PushConstantVertex); + + mem_copy(PushConstantColor.m_aColor, &pCommand->m_VertexColor, sizeof(PushConstantColor.m_aColor)); + + mem_copy(PushConstantVertex.m_aPos, m.data(), sizeof(PushConstantVertex.m_aPos)); + + if(!IsRotationless) + { + PushConstantVertex.m_Rotation = pCommand->m_Rotation; + PushConstantVertex.m_Center = {pCommand->m_Center.x, pCommand->m_Center.y}; + } + else + { + VertexPushConstantSize = sizeof(SUniformPrimExGPosRotationless); + } + + vkCmdPushConstants(CommandBuffer, PipeLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, VertexPushConstantSize, &PushConstantVertex); + vkCmdPushConstants(CommandBuffer, PipeLayout, VK_SHADER_STAGE_FRAGMENT_BIT, sizeof(SUniformPrimExGPos) + sizeof(SUniformPrimExGVertColorAlign), sizeof(PushConstantColor), &PushConstantColor); + + vkCmdDrawIndexed(CommandBuffer, static_cast(pCommand->m_DrawNum), 1, 0, 0, 0); + } + + void Cmd_RenderQuadContainerAsSpriteMultiple_FillExecuteBuffer(SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand_RenderQuadContainerAsSpriteMultiple *pCommand) + { + BufferContainer_FillExecuteBuffer(ExecBuffer, pCommand->m_State, (size_t)pCommand->m_BufferContainerIndex, ((pCommand->m_DrawCount - 1) / gs_GraphicsMaxParticlesRenderCount) + 1); + } + + void Cmd_RenderQuadContainerAsSpriteMultiple(const CCommandBuffer::SCommand_RenderQuadContainerAsSpriteMultiple *pCommand, SRenderCommandExecuteBuffer &ExecBuffer) + { + std::array m; + GetStateMatrix(pCommand->m_State, m); + + bool CanBePushed = pCommand->m_DrawCount <= 1; + + bool IsTextured; + size_t BlendModeIndex; + size_t DynamicIndex; + size_t AddressModeIndex; + GetStateIndices(pCommand->m_State, IsTextured, BlendModeIndex, DynamicIndex, AddressModeIndex); + auto &PipeLayout = GetPipeLayout(CanBePushed ? m_SpriteMultiPushPipeline : m_SpriteMultiPipeline, IsTextured, BlendModeIndex, DynamicIndex); + auto &PipeLine = GetPipeline(CanBePushed ? m_SpriteMultiPushPipeline : m_SpriteMultiPipeline, IsTextured, BlendModeIndex, DynamicIndex); + + auto &CommandBuffer = GetGraphicCommandBuffer(ExecBuffer.m_ThreadIndex); + + BindPipeline(ExecBuffer.m_ThreadIndex, CommandBuffer, ExecBuffer, PipeLine, pCommand->m_State); + + std::array aVertexBuffers = {ExecBuffer.m_Buffer}; + std::array aOffsets = {(VkDeviceSize)ExecBuffer.m_BufferOff}; + vkCmdBindVertexBuffers(CommandBuffer, 0, 1, aVertexBuffers.data(), aOffsets.data()); + + VkDeviceSize IndexOffset = (VkDeviceSize)((ptrdiff_t)pCommand->m_pOffset); + vkCmdBindIndexBuffer(CommandBuffer, ExecBuffer.m_IndexBuffer, IndexOffset, VK_INDEX_TYPE_UINT32); + + vkCmdBindDescriptorSets(CommandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, PipeLayout, 0, 1, &ExecBuffer.m_aDescriptors[0].m_Descriptor, 0, nullptr); + + if(CanBePushed) + { + SUniformSpriteMultiPushGVertColor PushConstantColor; + SUniformSpriteMultiPushGPos PushConstantVertex; + + mem_copy(PushConstantColor.m_aColor, &pCommand->m_VertexColor, sizeof(PushConstantColor.m_aColor)); + + mem_copy(PushConstantVertex.m_aPos, m.data(), sizeof(PushConstantVertex.m_aPos)); + mem_copy(&PushConstantVertex.m_Center, &pCommand->m_Center, sizeof(PushConstantVertex.m_Center)); + + for(size_t i = 0; i < pCommand->m_DrawCount; ++i) + PushConstantVertex.m_aPSR[i] = vec4(pCommand->m_pRenderInfo[i].m_Pos[0], pCommand->m_pRenderInfo[i].m_Pos[1], pCommand->m_pRenderInfo[i].m_Scale, pCommand->m_pRenderInfo[i].m_Rotation); + + vkCmdPushConstants(CommandBuffer, PipeLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(SUniformSpriteMultiPushGPosBase) + sizeof(vec4) * pCommand->m_DrawCount, &PushConstantVertex); + vkCmdPushConstants(CommandBuffer, PipeLayout, VK_SHADER_STAGE_FRAGMENT_BIT, sizeof(SUniformSpriteMultiPushGPos), sizeof(PushConstantColor), &PushConstantColor); + } + else + { + SUniformSpriteMultiGVertColor PushConstantColor; + SUniformSpriteMultiGPos PushConstantVertex; + + mem_copy(PushConstantColor.m_aColor, &pCommand->m_VertexColor, sizeof(PushConstantColor.m_aColor)); + + mem_copy(PushConstantVertex.m_aPos, m.data(), sizeof(PushConstantVertex.m_aPos)); + mem_copy(&PushConstantVertex.m_Center, &pCommand->m_Center, sizeof(PushConstantVertex.m_Center)); + + vkCmdPushConstants(CommandBuffer, PipeLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(PushConstantVertex), &PushConstantVertex); + vkCmdPushConstants(CommandBuffer, PipeLayout, VK_SHADER_STAGE_FRAGMENT_BIT, sizeof(SUniformSpriteMultiGPos) + sizeof(SUniformSpriteMultiGVertColorAlign), sizeof(PushConstantColor), &PushConstantColor); + } + + const int RSPCount = 512; + int DrawCount = pCommand->m_DrawCount; + size_t RenderOffset = 0; + + while(DrawCount > 0) + { + int UniformCount = (DrawCount > RSPCount ? RSPCount : DrawCount); + + if(!CanBePushed) + { + // create uniform buffer + SDeviceDescriptorSet UniDescrSet; + GetUniformBufferObject(ExecBuffer.m_ThreadIndex, false, UniDescrSet, UniformCount, (const float *)(pCommand->m_pRenderInfo + RenderOffset), UniformCount * sizeof(IGraphics::SRenderSpriteInfo)); + + vkCmdBindDescriptorSets(CommandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, PipeLayout, 1, 1, &UniDescrSet.m_Descriptor, 0, nullptr); + } + + vkCmdDrawIndexed(CommandBuffer, static_cast(pCommand->m_DrawNum), UniformCount, 0, 0, 0); + + RenderOffset += RSPCount; + DrawCount -= RSPCount; + } + } + + void Cmd_WindowCreateNtf(const CCommandBuffer::SCommand_WindowCreateNtf *pCommand) + { + dbg_msg("vulkan", "creating new surface."); + m_pWindow = SDL_GetWindowFromID(pCommand->m_WindowID); + if(m_RenderingPaused) + { +#ifdef CONF_PLATFORM_ANDROID + CreateSurface(m_pWindow); + m_RecreateSwapChain = true; +#endif + m_RenderingPaused = false; + PureMemoryFrame(); + PrepareFrame(); + } + } + + void Cmd_WindowDestroyNtf(const CCommandBuffer::SCommand_WindowDestroyNtf *pCommand) + { + dbg_msg("vulkan", "surface got destroyed."); + if(!m_RenderingPaused) + { + WaitFrame(); + m_RenderingPaused = true; + vkDeviceWaitIdle(m_VKDevice); +#ifdef CONF_PLATFORM_ANDROID + CleanupVulkanSwapChain(true); +#endif + } + } + + void Cmd_PreInit(const CCommandProcessorFragment_GLBase::SCommand_PreInit *pCommand) + { + m_pGPUList = pCommand->m_pGPUList; + if(InitVulkanSDL(pCommand->m_pWindow, pCommand->m_Width, pCommand->m_Height, pCommand->m_pRendererString, pCommand->m_pVendorString, pCommand->m_pVersionString) != 0) + { + m_VKInstance = VK_NULL_HANDLE; + } + + RegisterCommands(); + + m_ThreadCount = g_Config.m_GfxRenderThreadCount; + if(m_ThreadCount <= 1) + m_ThreadCount = 1; + else + { + m_ThreadCount = clamp(m_ThreadCount, 3, std::thread::hardware_concurrency()); + } + + // start threads + dbg_assert(m_ThreadCount != 2, "Either use 1 main thread or at least 2 extra rendering threads."); + if(m_ThreadCount > 1) + { + m_ThreadCommandLists.resize(m_ThreadCount - 1); + m_ThreadHelperHadCommands.resize(m_ThreadCount - 1, false); + for(auto &ThreadCommandList : m_ThreadCommandLists) + { + ThreadCommandList.reserve(256); + } + + for(size_t i = 0; i < m_ThreadCount - 1; ++i) + { + auto *pRenderThread = new SRenderThread(); + std::unique_lock Lock(pRenderThread->m_Mutex); + m_RenderThreads.emplace_back(pRenderThread); + pRenderThread->m_Thread = std::thread([this, i]() { RunThread(i); }); + // wait until thread started + pRenderThread->m_Cond.wait(Lock, [pRenderThread]() -> bool { return pRenderThread->m_Started; }); + } + } + } + + void Cmd_PostShutdown(const CCommandProcessorFragment_GLBase::SCommand_PostShutdown *pCommand) + { + m_ThreadCommandLists.clear(); + m_ThreadHelperHadCommands.clear(); + + for(size_t i = 0; i < m_ThreadCount - 1; ++i) + { + auto *pThread = m_RenderThreads[i].get(); + { + std::unique_lock Lock(pThread->m_Mutex); + pThread->m_Finished = true; + pThread->m_Cond.notify_one(); + } + pThread->m_Thread.join(); + } + m_RenderThreads.clear(); + + CleanupVulkanSDL(); + } + + void StartCommands(size_t CommandCount, size_t EstimatedRenderCallCount) override + { + m_CommandsInPipe = CommandCount; + m_RenderCallsInPipe = EstimatedRenderCallCount; + m_CurCommandInPipe = 0; + m_CurRenderCallCountInPipe = 0; + } + + void EndCommands() override + { + FinishRenderThreads(); + m_CommandsInPipe = 0; + m_RenderCallsInPipe = 0; + } + + /**************** + * RENDER THREADS + *****************/ + + void RunThread(size_t ThreadIndex) + { + auto *pThread = m_RenderThreads[ThreadIndex].get(); + std::unique_lock Lock(pThread->m_Mutex); + pThread->m_Started = true; + pThread->m_Cond.notify_one(); + + while(!pThread->m_Finished) + { + pThread->m_Cond.wait(Lock, [pThread]() -> bool { return pThread->m_IsRendering || pThread->m_Finished; }); + pThread->m_Cond.notify_one(); + + // set this to true, if you want to benchmark the render thread times + static constexpr bool s_BenchmarkRenderThreads = false; + int64_t ThreadRenderTime = 0; + if(IsVerbose() && s_BenchmarkRenderThreads) + { + ThreadRenderTime = time_get_microseconds(); + } + + for(auto &NextCmd : m_ThreadCommandLists[ThreadIndex]) + { + m_aCommandCallbacks[CommandBufferCMDOff(NextCmd.m_Command)].m_CommandCB(NextCmd.m_pRawCommand, NextCmd); + } + m_ThreadCommandLists[ThreadIndex].clear(); + + if(!pThread->m_Finished) + { + if(m_UsedThreadDrawCommandBuffer[ThreadIndex + 1][m_CurImageIndex]) + { + auto &GraphicThreadCommandBuffer = m_ThreadDrawCommandBuffers[ThreadIndex + 1][m_CurImageIndex]; + vkEndCommandBuffer(GraphicThreadCommandBuffer); + } + } + + if(IsVerbose() && s_BenchmarkRenderThreads) + { + dbg_msg("vulkan", "render thread %zu took %d microseconds to finish", ThreadIndex, (int)(time_get_microseconds() - ThreadRenderTime)); + } + + pThread->m_IsRendering = false; + } + } +}; + +CCommandProcessorFragment_GLBase *CreateVulkanCommandProcessorFragment() +{ + return new CCommandProcessorFragment_Vulkan(); +} + +#endif diff --git a/src/engine/client/backend/vulkan/backend_vulkan.h b/src/engine/client/backend/vulkan/backend_vulkan.h new file mode 100644 index 000000000..6ad4a04bc --- /dev/null +++ b/src/engine/client/backend/vulkan/backend_vulkan.h @@ -0,0 +1,12 @@ +#ifndef ENGINE_CLIENT_BACKEND_VULKAN_BACKEND_VULKAN_H +#define ENGINE_CLIENT_BACKEND_VULKAN_BACKEND_VULKAN_H + +#include +#include + +static constexpr int gs_BackendVulkanMajor = 1; +static constexpr int gs_BackendVulkanMinor = 0; + +CCommandProcessorFragment_GLBase *CreateVulkanCommandProcessorFragment(); + +#endif diff --git a/src/engine/client/backend_sdl.cpp b/src/engine/client/backend_sdl.cpp index 16a0df5de..29e762ef7 100644 --- a/src/engine/client/backend_sdl.cpp +++ b/src/engine/client/backend_sdl.cpp @@ -68,10 +68,10 @@ void CGraphicsBackend_Threaded::ThreadFunc(void *pUser) std::unique_lock Lock(pSelf->m_BufferSwapMutex); // notify, that the thread started pSelf->m_Started = true; - pSelf->m_BufferDoneCond.notify_all(); + pSelf->m_BufferSwapCond.notify_all(); while(!pSelf->m_Shutdown) { - pSelf->m_BufferSwapCond.wait(Lock); + pSelf->m_BufferSwapCond.wait(Lock, [&pSelf] { return pSelf->m_pBuffer != nullptr || pSelf->m_Shutdown; }); if(pSelf->m_pBuffer) { #ifdef CONF_PLATFORM_MACOS @@ -81,7 +81,7 @@ void CGraphicsBackend_Threaded::ThreadFunc(void *pUser) pSelf->m_pBuffer = nullptr; pSelf->m_BufferInProcess.store(false, std::memory_order_relaxed); - pSelf->m_BufferDoneCond.notify_all(); + pSelf->m_BufferSwapCond.notify_all(); #if defined(CONF_VIDEORECORDER) if(IVideo::Current()) @@ -100,16 +100,18 @@ CGraphicsBackend_Threaded::CGraphicsBackend_Threaded() void CGraphicsBackend_Threaded::StartProcessor(ICommandProcessor *pProcessor) { + dbg_assert(m_Shutdown, "Processor was already not shut down."); m_Shutdown = false; m_pProcessor = pProcessor; std::unique_lock Lock(m_BufferSwapMutex); m_Thread = thread_init(ThreadFunc, this, "Graphics thread"); // wait for the thread to start - m_BufferDoneCond.wait(Lock, [this]() -> bool { return m_Started; }); + m_BufferSwapCond.wait(Lock, [this]() -> bool { return m_Started; }); } void CGraphicsBackend_Threaded::StopProcessor() { + dbg_assert(!m_Shutdown, "Processor was already shut down."); m_Shutdown = true; { std::unique_lock Lock(m_BufferSwapMutex); @@ -140,8 +142,7 @@ bool CGraphicsBackend_Threaded::IsIdle() const void CGraphicsBackend_Threaded::WaitForIdle() { std::unique_lock Lock(m_BufferSwapMutex); - while(m_pBuffer != nullptr) - m_BufferDoneCond.wait(Lock); + m_BufferSwapCond.wait(Lock, [this]() { return m_pBuffer == nullptr; }); } // ------------ CCommandProcessorFragment_General @@ -236,7 +237,7 @@ bool CCommandProcessorFragment_SDL::RunCommand(const CCommandBuffer::SCommand *p void CCommandProcessor_SDL_GL::RunBuffer(CCommandBuffer *pBuffer) { - m_pGLBackend->StartCommands(pBuffer->m_CommandCount); + m_pGLBackend->StartCommands(pBuffer->m_CommandCount, pBuffer->m_RenderCallCount); for(CCommandBuffer::SCommand *pCommand = pBuffer->Head(); pCommand; pCommand = pCommand->m_pNext) { @@ -560,10 +561,8 @@ static int IsVersionSupportedGlew(EBackendType BackendType, int VersionMajor, in EBackendType CGraphicsBackend_SDL_GL::DetectBackend() { - // TODO - return BACKEND_TYPE_VULKAN; EBackendType RetBackendType = BACKEND_TYPE_OPENGL; - const char *pEnvDriver = getenv("DDNET_DRIVER"); + const char *pEnvDriver = SDL_getenv("DDNET_DRIVER"); if(pEnvDriver && str_comp_nocase(pEnvDriver, "GLES") == 0) RetBackendType = BACKEND_TYPE_OPENGL_ES; else if(pEnvDriver && str_comp_nocase(pEnvDriver, "Vulkan") == 0) @@ -581,9 +580,15 @@ EBackendType CGraphicsBackend_SDL_GL::DetectBackend() else if(str_comp_nocase(pConfBackend, "OpenGL") == 0) RetBackendType = BACKEND_TYPE_OPENGL; } +#if !defined(CONF_BACKEND_VULKAN) + RetBackendType = BACKEND_TYPE_OPENGL; +#endif #if !defined(CONF_BACKEND_OPENGL_ES) && !defined(CONF_BACKEND_OPENGL_ES3) if(RetBackendType == BACKEND_TYPE_OPENGL_ES) RetBackendType = BACKEND_TYPE_OPENGL; +#elif defined(CONF_BACKEND_OPENGL_ES) + if(RetBackendType == BACKEND_TYPE_OPENGL) + RetBackendType = BACKEND_TYPE_OPENGL_ES; #endif return RetBackendType; } @@ -635,9 +640,11 @@ void CGraphicsBackend_SDL_GL::ClampDriverVersion(EBackendType BackendType) } else if(BackendType == BACKEND_TYPE_VULKAN) { - g_Config.m_GfxGLMajor = 1; - g_Config.m_GfxGLMinor = 1; +#if defined(CONF_BACKEND_VULKAN) + g_Config.m_GfxGLMajor = gs_BackendVulkanMajor; + g_Config.m_GfxGLMinor = gs_BackendVulkanMinor; g_Config.m_GfxGLPatch = 0; +#endif } } @@ -653,50 +660,81 @@ bool CGraphicsBackend_SDL_GL::IsModernAPI(EBackendType BackendType) return false; } -void CGraphicsBackend_SDL_GL::GetDriverVersion(EGraphicsDriverAgeType DriverAgeType, int &Major, int &Minor, int &Patch) +bool CGraphicsBackend_SDL_GL::GetDriverVersion(EGraphicsDriverAgeType DriverAgeType, int &Major, int &Minor, int &Patch, const char *&pName, EBackendType BackendType) { - if(m_BackendType == BACKEND_TYPE_OPENGL) + if(BackendType == BACKEND_TYPE_AUTO) + BackendType = m_BackendType; + if(BackendType == BACKEND_TYPE_OPENGL) { + pName = "OpenGL"; if(DriverAgeType == GRAPHICS_DRIVER_AGE_TYPE_LEGACY) { Major = 1; Minor = 4; Patch = 0; + return true; } else if(DriverAgeType == GRAPHICS_DRIVER_AGE_TYPE_DEFAULT) { Major = 3; Minor = 0; Patch = 0; + return true; } else if(DriverAgeType == GRAPHICS_DRIVER_AGE_TYPE_MODERN) { Major = 3; Minor = 3; Patch = 0; + return true; } } - else if(m_BackendType == BACKEND_TYPE_OPENGL_ES) + else if(BackendType == BACKEND_TYPE_OPENGL_ES) { + pName = "GLES"; +#ifdef CONF_BACKEND_OPENGL_ES if(DriverAgeType == GRAPHICS_DRIVER_AGE_TYPE_LEGACY) { Major = 1; Minor = 0; Patch = 0; + return true; } else if(DriverAgeType == GRAPHICS_DRIVER_AGE_TYPE_DEFAULT) { Major = 3; Minor = 0; Patch = 0; + // there isn't really a default one + return false; } - else if(DriverAgeType == GRAPHICS_DRIVER_AGE_TYPE_MODERN) +#endif +#ifdef CONF_BACKEND_OPENGL_ES3 + if(DriverAgeType == GRAPHICS_DRIVER_AGE_TYPE_MODERN) { Major = 3; Minor = 0; Patch = 0; + return true; } +#endif } + else if(BackendType == BACKEND_TYPE_VULKAN) + { + pName = "Vulkan"; +#ifdef CONF_BACKEND_VULKAN + if(DriverAgeType == GRAPHICS_DRIVER_AGE_TYPE_DEFAULT) + { + Major = gs_BackendVulkanMajor; + Minor = gs_BackendVulkanMinor; + Patch = 0; + return true; + } +#else + return false; +#endif + } + return false; } static void DisplayToVideoMode(CVideoMode *pVMode, SDL_DisplayMode *pMode, int HiDPIScale, int RefreshRate) @@ -830,12 +868,19 @@ int CGraphicsBackend_SDL_GL::Init(const char *pName, int *pScreen, int *pWidth, dbg_msg("gfx", "unable to init SDL video: %s", SDL_GetError()); return EGraphicsBackendErrorCodes::GRAPHICS_BACKEND_ERROR_CODE_SDL_INIT_FAILED; } -#if defined(CONF_FAMILY_WINDOWS) && defined(CONF_BACKEND_VULKAN) - SDL_Vulkan_LoadLibrary("libvulkan-1.dll"); -#endif } + EBackendType OldBackendType = m_BackendType; m_BackendType = DetectBackend(); + // little fallback for Vulkan + if(OldBackendType != BACKEND_TYPE_AUTO) + { + if(m_BackendType == BACKEND_TYPE_VULKAN) + { + SDL_setenv("DDNET_DRIVER", "OpenGL", 1); + m_BackendType = DetectBackend(); + } + } ClampDriverVersion(m_BackendType); @@ -918,7 +963,7 @@ int CGraphicsBackend_SDL_GL::Init(const char *pName, int *pScreen, int *pWidth, // set flags int SdlFlags = SDL_WINDOW_INPUT_GRABBED | SDL_WINDOW_INPUT_FOCUS | SDL_WINDOW_MOUSE_FOCUS; - SdlFlags |= (m_BackendType == BACKEND_TYPE_OPENGL || m_BackendType == BACKEND_TYPE_OPENGL_ES) ? SDL_WINDOW_OPENGL : SDL_WINDOW_VULKAN; + SdlFlags |= (IsOpenGLFamilyBackend) ? SDL_WINDOW_OPENGL : SDL_WINDOW_VULKAN; if(Flags & IGraphicsBackend::INITFLAG_HIGHDPI) SdlFlags |= SDL_WINDOW_ALLOW_HIGHDPI; if(Flags & IGraphicsBackend::INITFLAG_RESIZABLE) @@ -981,7 +1026,10 @@ int CGraphicsBackend_SDL_GL::Init(const char *pName, int *pScreen, int *pWidth, if(m_pWindow == NULL) { dbg_msg("gfx", "unable to create window: %s", SDL_GetError()); - return EGraphicsBackendErrorCodes::GRAPHICS_BACKEND_ERROR_CODE_SDL_WINDOW_CREATE_FAILED; + if(m_BackendType == BACKEND_TYPE_VULKAN) + return EGraphicsBackendErrorCodes::GRAPHICS_BACKEND_ERROR_CODE_GL_CONTEXT_FAILED; + else + return EGraphicsBackendErrorCodes::GRAPHICS_BACKEND_ERROR_CODE_SDL_WINDOW_CREATE_FAILED; } int GlewMajor = 0; @@ -1013,7 +1061,7 @@ int CGraphicsBackend_SDL_GL::Init(const char *pName, int *pScreen, int *pWidth, InitError = IsVersionSupportedGlew(m_BackendType, g_Config.m_GfxGLMajor, g_Config.m_GfxGLMinor, g_Config.m_GfxGLPatch, GlewMajor, GlewMinor, GlewPatch); // SDL_GL_GetDrawableSize reports HiDPI resolution even with SDL_WINDOW_ALLOW_HIGHDPI not set, which is wrong - if(SdlFlags & SDL_WINDOW_ALLOW_HIGHDPI) + if(SdlFlags & SDL_WINDOW_ALLOW_HIGHDPI && IsOpenGLFamilyBackend) SDL_GL_GetDrawableSize(m_pWindow, pCurrentWidth, pCurrentHeight); else SDL_GetWindowSize(m_pWindow, pCurrentWidth, pCurrentHeight); @@ -1039,6 +1087,7 @@ int CGraphicsBackend_SDL_GL::Init(const char *pName, int *pScreen, int *pWidth, } // start the command processor + dbg_assert(m_pProcessor == nullptr, "Processor was not cleaned up properly."); m_pProcessor = new CCommandProcessor_SDL_GL(m_BackendType, g_Config.m_GfxGLMajor, g_Config.m_GfxGLMinor, g_Config.m_GfxGLPatch); StartProcessor(m_pProcessor); @@ -1124,7 +1173,7 @@ int CGraphicsBackend_SDL_GL::Init(const char *pName, int *pScreen, int *pWidth, // stop and delete the processor StopProcessor(); delete m_pProcessor; - m_pProcessor = 0; + m_pProcessor = nullptr; if(m_GLContext) SDL_GL_DeleteContext(m_GLContext); @@ -1188,7 +1237,7 @@ int CGraphicsBackend_SDL_GL::Shutdown() // stop and delete the processor StopProcessor(); delete m_pProcessor; - m_pProcessor = 0; + m_pProcessor = nullptr; SDL_GL_DeleteContext(m_GLContext); SDL_DestroyWindow(m_pWindow); diff --git a/src/engine/client/backend_sdl.h b/src/engine/client/backend_sdl.h index 719ac8b61..c1750128a 100644 --- a/src/engine/client/backend_sdl.h +++ b/src/engine/client/backend_sdl.h @@ -5,6 +5,7 @@ #include +#include "engine/graphics.h" #include "graphics_defines.h" #include "blocklist_driver.h" @@ -70,9 +71,8 @@ private: ICommandProcessor *m_pProcessor; std::mutex m_BufferSwapMutex; std::condition_variable m_BufferSwapCond; - std::condition_variable m_BufferDoneCond; CCommandBuffer *m_pBuffer; - std::atomic_bool m_Shutdown; + std::atomic_bool m_Shutdown = true; bool m_Started = false; std::atomic_bool m_BufferInProcess; void *m_Thread; @@ -90,13 +90,6 @@ public: bool RunCommand(const CCommandBuffer::SCommand *pBaseCommand); }; -enum EBackendType -{ - BACKEND_TYPE_OPENGL = 0, - BACKEND_TYPE_OPENGL_ES, - BACKEND_TYPE_VULKAN, -}; - struct SBackendCapabilites { bool m_TileBuffering; @@ -183,7 +176,7 @@ class CGraphicsBackend_SDL_GL : public CGraphicsBackend_Threaded { SDL_Window *m_pWindow = NULL; SDL_GLContext m_GLContext; - ICommandProcessor *m_pProcessor; + ICommandProcessor *m_pProcessor = nullptr; std::atomic m_TextureMemoryUsage{0}; std::atomic m_BufferMemoryUsage{0}; std::atomic m_StreamMemoryUsage{0}; @@ -201,7 +194,7 @@ class CGraphicsBackend_SDL_GL : public CGraphicsBackend_Threaded char m_aVersionString[gs_GPUInfoStringSize] = {}; char m_aRendererString[gs_GPUInfoStringSize] = {}; - EBackendType m_BackendType; + EBackendType m_BackendType = BACKEND_TYPE_AUTO; char m_aErrorString[256]; @@ -241,7 +234,7 @@ public: virtual void WindowDestroyNtf(uint32_t WindowID); virtual void WindowCreateNtf(uint32_t WindowID); - virtual void GetDriverVersion(EGraphicsDriverAgeType DriverAgeType, int &Major, int &Minor, int &Patch); + virtual bool GetDriverVersion(EGraphicsDriverAgeType DriverAgeType, int &Major, int &Minor, int &Patch, const char *&pName, EBackendType BackendType); virtual bool IsConfigModernAPI() { return IsModernAPI(m_BackendType); } virtual bool UseTrianglesAsQuad() { return m_Capabilites.m_TrianglesAsQuads; } virtual bool HasTileBuffering() { return m_Capabilites.m_TileBuffering; } diff --git a/src/engine/client/graphics_threaded.cpp b/src/engine/client/graphics_threaded.cpp index 9c3090db7..568278375 100644 --- a/src/engine/client/graphics_threaded.cpp +++ b/src/engine/client/graphics_threaded.cpp @@ -496,7 +496,7 @@ IGraphics::CTextureHandle CGraphics_Threaded::LoadTextureRaw(int Width, int Heig void *pTmpData = malloc(MemSize); if(!ConvertToRGBA((uint8_t *)pTmpData, (const uint8_t *)pData, Width, Height, Format)) { - dbg_msg("graphics", "converted image %s to RGBA, consider making its file format using RGBA", pTexName); + dbg_msg("graphics", "converted image %s to RGBA, consider making its file format RGBA", pTexName); } Cmd.m_pData = pTmpData; @@ -624,7 +624,7 @@ bool CGraphics_Threaded::UpdateTextTexture(CTextureHandle TextureID, int x, int AddCmd( Cmd, [] { return true; }, "failed to update text texture."); - return 0; + return true; } int CGraphics_Threaded::LoadPNG(CImageInfo *pImg, const char *pFilename, int StorageType) @@ -739,7 +739,7 @@ bool CGraphics_Threaded::CheckImageDivisibility(const char *pFileName, CImageInf bool CGraphics_Threaded::IsImageFormatRGBA(const char *pFileName, CImageInfo &Img) { - if(Img.m_Format != CImageInfo::FORMAT_RGBA && Img.m_Format != CImageInfo::FORMAT_ALPHA) + if(Img.m_Format != CImageInfo::FORMAT_RGBA) { SWarning NewWarning; char aText[128]; @@ -787,14 +787,17 @@ void CGraphics_Threaded::KickCommandBuffer() m_pCommandBuffer->Reset(); } -void CGraphics_Threaded::ScreenshotDirect() +bool CGraphics_Threaded::ScreenshotDirect() { // add swap command CImageInfo Image; mem_zero(&Image, sizeof(Image)); - CCommandBuffer::SCommand_Screenshot Cmd; + bool DidSwap = false; + + CCommandBuffer::SCommand_TrySwapAndScreenshot Cmd; Cmd.m_pImage = &Image; + Cmd.m_pSwapped = &DidSwap; AddCmd( Cmd, [] { return true; }, "failed to take screenshot."); @@ -826,6 +829,8 @@ void CGraphics_Threaded::ScreenshotDirect() free(Image.m_pData); } + + return DidSwap; } void CGraphics_Threaded::TextureSet(CTextureHandle TextureID) @@ -834,13 +839,14 @@ void CGraphics_Threaded::TextureSet(CTextureHandle TextureID) m_State.m_Texture = TextureID.Id(); } -void CGraphics_Threaded::Clear(float r, float g, float b) +void CGraphics_Threaded::Clear(float r, float g, float b, bool ForceClearNow) { CCommandBuffer::SCommand_Clear Cmd; Cmd.m_Color.r = r; Cmd.m_Color.g = g; Cmd.m_Color.b = b; Cmd.m_Color.a = 0; + Cmd.m_ForceClear = ForceClearNow || m_IsForcedViewport; AddCmd( Cmd, [] { return true; }, "failed to clear graphics."); } @@ -1218,25 +1224,25 @@ void CGraphics_Threaded::QuadsText(float x, float y, float Size, const char *pTe } } -void CGraphics_Threaded::RenderTileLayer(int BufferContainerIndex, float *pColor, char **pOffsets, unsigned int *IndicedVertexDrawNum, size_t NumIndicesOffet) +void CGraphics_Threaded::RenderTileLayer(int BufferContainerIndex, float *pColor, char **pOffsets, unsigned int *IndicedVertexDrawNum, size_t NumIndicesOffset) { - if(NumIndicesOffet == 0) + if(NumIndicesOffset == 0) return; // add the VertexArrays and draw CCommandBuffer::SCommand_RenderTileLayer Cmd; Cmd.m_State = m_State; - Cmd.m_IndicesDrawNum = NumIndicesOffet; + Cmd.m_IndicesDrawNum = NumIndicesOffset; Cmd.m_BufferContainerIndex = BufferContainerIndex; mem_copy(&Cmd.m_Color, pColor, sizeof(Cmd.m_Color)); - void *Data = m_pCommandBuffer->AllocData((sizeof(char *) + sizeof(unsigned int)) * NumIndicesOffet); + void *Data = m_pCommandBuffer->AllocData((sizeof(char *) + sizeof(unsigned int)) * NumIndicesOffset); if(Data == 0x0) { // kick command buffer and try again KickCommandBuffer(); - Data = m_pCommandBuffer->AllocData((sizeof(char *) + sizeof(unsigned int)) * NumIndicesOffet); + Data = m_pCommandBuffer->AllocData((sizeof(char *) + sizeof(unsigned int)) * NumIndicesOffset); if(Data == 0x0) { dbg_msg("graphics", "failed to allocate data for vertices"); @@ -1244,18 +1250,18 @@ void CGraphics_Threaded::RenderTileLayer(int BufferContainerIndex, float *pColor } } Cmd.m_pIndicesOffsets = (char **)Data; - Cmd.m_pDrawCount = (unsigned int *)(((char *)Data) + (sizeof(char *) * NumIndicesOffet)); + Cmd.m_pDrawCount = (unsigned int *)(((char *)Data) + (sizeof(char *) * NumIndicesOffset)); if(!AddCmd( Cmd, [&] { - Data = m_pCommandBuffer->AllocData((sizeof(char *) + sizeof(unsigned int)) * NumIndicesOffet); + Data = m_pCommandBuffer->AllocData((sizeof(char *) + sizeof(unsigned int)) * NumIndicesOffset); if(Data == 0x0) { dbg_msg("graphics", "failed to allocate data for vertices"); return false; } Cmd.m_pIndicesOffsets = (char **)Data; - Cmd.m_pDrawCount = (unsigned int *)(((char *)Data) + (sizeof(char *) * NumIndicesOffet)); + Cmd.m_pDrawCount = (unsigned int *)(((char *)Data) + (sizeof(char *) * NumIndicesOffset)); return true; }, "failed to allocate memory for render command")) @@ -1263,9 +1269,10 @@ void CGraphics_Threaded::RenderTileLayer(int BufferContainerIndex, float *pColor return; } - mem_copy(Cmd.m_pIndicesOffsets, pOffsets, sizeof(char *) * NumIndicesOffet); - mem_copy(Cmd.m_pDrawCount, IndicedVertexDrawNum, sizeof(unsigned int) * NumIndicesOffet); + mem_copy(Cmd.m_pIndicesOffsets, pOffsets, sizeof(char *) * NumIndicesOffset); + mem_copy(Cmd.m_pDrawCount, IndicedVertexDrawNum, sizeof(unsigned int) * NumIndicesOffset); + m_pCommandBuffer->AddRenderCalls(NumIndicesOffset); // todo max indices group check!! } @@ -1294,6 +1301,8 @@ void CGraphics_Threaded::RenderBorderTiles(int BufferContainerIndex, float *pCol { return; } + + m_pCommandBuffer->AddRenderCalls(1); } void CGraphics_Threaded::RenderBorderTileLines(int BufferContainerIndex, float *pColor, char *pIndexBufferOffset, float *pOffset, float *pDir, unsigned int IndexDrawNum, unsigned int RedrawNum) @@ -1321,6 +1330,8 @@ void CGraphics_Threaded::RenderBorderTileLines(int BufferContainerIndex, float * { return; } + + m_pCommandBuffer->AddRenderCalls(1); } void CGraphics_Threaded::RenderQuadLayer(int BufferContainerIndex, SQuadRenderInfo *pQuadInfo, int QuadNum, int QuadOffset) @@ -1356,6 +1367,8 @@ void CGraphics_Threaded::RenderQuadLayer(int BufferContainerIndex, SQuadRenderIn } mem_copy(Cmd.m_pQuadInfo, pQuadInfo, sizeof(SQuadRenderInfo) * QuadNum); + + m_pCommandBuffer->AddRenderCalls(((QuadNum - 1) / gs_GraphicsMaxQuadsRenderCount) + 1); } void CGraphics_Threaded::RenderText(int BufferContainerIndex, int TextQuadNum, int TextureSize, int TextureTextIndex, int TextureTextOutlineIndex, float *pTextColor, float *pTextoutlineColor) @@ -1378,6 +1391,8 @@ void CGraphics_Threaded::RenderText(int BufferContainerIndex, int TextQuadNum, i { return; } + + m_pCommandBuffer->AddRenderCalls(1); } int CGraphics_Threaded::CreateQuadContainer(bool AutomaticUpload) @@ -1594,6 +1609,8 @@ void CGraphics_Threaded::RenderQuadContainer(int ContainerIndex, int QuadOffset, { return; } + + m_pCommandBuffer->AddRenderCalls(1); } else { @@ -1671,6 +1688,8 @@ void CGraphics_Threaded::RenderQuadContainerEx(int ContainerIndex, int QuadOffse { return; } + + m_pCommandBuffer->AddRenderCalls(1); } else { @@ -1815,6 +1834,9 @@ void CGraphics_Threaded::RenderQuadContainerAsSpriteMultiple(int ContainerIndex, } mem_copy(Cmd.m_pRenderInfo, pRenderInfo, sizeof(IGraphics::SRenderSpriteInfo) * DrawCount); + + m_pCommandBuffer->AddRenderCalls(((DrawCount - 1) / gs_GraphicsMaxParticlesRenderCount) + 1); + WrapNormal(); } else @@ -2235,6 +2257,37 @@ int CGraphics_Threaded::IssueInit() return r; } +void CGraphics_Threaded::AdjustViewport(bool SendViewportChangeToBackend) +{ + // adjust the viewport to only allow certain aspect ratios + // keep this in sync with backend_vulkan GetSwapImageSize's check + if(m_ScreenHeight > 4 * m_ScreenWidth / 5) + { + m_IsForcedViewport = true; + m_ScreenHeight = 4 * m_ScreenWidth / 5; + + if(SendViewportChangeToBackend) + { + CCommandBuffer::SCommand_Update_Viewport Cmd; + Cmd.m_X = 0; + Cmd.m_Y = 0; + Cmd.m_Width = m_ScreenWidth; + Cmd.m_Height = m_ScreenHeight; + Cmd.m_ByResize = true; + + if(!AddCmd( + Cmd, [] { return true; }, "failed to add resize command")) + { + return; + } + } + } + else + { + m_IsForcedViewport = false; + } +} + void CGraphics_Threaded::AddBackEndWarningIfExists() { const char *pErrStr = m_pBackend->GetErrorString(); @@ -2354,6 +2407,16 @@ int CGraphics_Threaded::InitWindow() return 0; } + // at the very end, just try to set to gl 1.4 + { + g_Config.m_GfxGLMajor = 1; + g_Config.m_GfxGLMinor = 4; + g_Config.m_GfxGLPatch = 0; + + if(IssueInit() == 0) + return 0; + } + dbg_msg("gfx", "out of ideas. failed to init graphics"); return -1; @@ -2413,6 +2476,8 @@ int CGraphics_Threaded::Init() str_format(aBuf, sizeof(aBuf), "GPU version: %s", GetVersionString()); m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "gfx", aBuf, GPUInfoPrintColor); + AdjustViewport(true); + return 0; } @@ -2460,6 +2525,7 @@ bool CGraphics_Threaded::SetWindowScreen(int Index) } m_pBackend->GetViewportSize(m_ScreenWidth, m_ScreenHeight); + AdjustViewport(true); m_ScreenHiDPIScale = m_ScreenWidth / (float)g_Config.m_GfxScreenWidth; return true; } @@ -2475,6 +2541,7 @@ void CGraphics_Threaded::Move(int x, int y) const int CurScreen = m_pBackend->GetWindowScreen(); m_pBackend->UpdateDisplayMode(CurScreen); m_pBackend->GetViewportSize(m_ScreenWidth, m_ScreenHeight); + AdjustViewport(true); m_ScreenHiDPIScale = m_ScreenWidth / (float)g_Config.m_GfxScreenWidth; } @@ -2511,9 +2578,7 @@ void CGraphics_Threaded::GotResized(int w, int h, int RefreshRate) // if the size change event is triggered, set all parameters and change the viewport m_pBackend->GetViewportSize(m_ScreenWidth, m_ScreenHeight); - // adjust the viewport to only allow certain aspect ratios - if(m_ScreenHeight > 4 * m_ScreenWidth / 5) - m_ScreenHeight = 4 * m_ScreenWidth / 5; + AdjustViewport(false); m_ScreenRefreshRate = RefreshRate; @@ -2635,14 +2700,16 @@ void CGraphics_Threaded::Swap() } } - // TODO: screenshot support + bool TookScreenshotAndSwapped = false; + if(m_DoScreenshot) { if(WindowActive()) - ScreenshotDirect(); + TookScreenshotAndSwapped = ScreenshotDirect(); m_DoScreenshot = false; } + if(!TookScreenshotAndSwapped) { // add swap command CCommandBuffer::SCommand_Swap Cmd; diff --git a/src/engine/client/graphics_threaded.h b/src/engine/client/graphics_threaded.h index da6d3fbe5..772275a5b 100644 --- a/src/engine/client/graphics_threaded.h +++ b/src/engine/client/graphics_threaded.h @@ -61,6 +61,7 @@ class CCommandBuffer public: CBuffer m_CmdBuffer; size_t m_CommandCount = 0; + size_t m_RenderCallCount = 0; CBuffer m_DataBuffer; @@ -128,7 +129,7 @@ public: // misc CMD_VSYNC, - CMD_SCREENSHOT, + CMD_TRY_SWAP_AND_SCREENSHOT, CMD_UPDATE_VIEWPORT, // in Android a window that minimizes gets destroyed @@ -213,6 +214,7 @@ public: SCommand_Clear() : SCommand(CMD_CLEAR) {} SColorf m_Color; + bool m_ForceClear; }; struct SCommand_Signal : public SCommand @@ -477,11 +479,12 @@ public: void *m_pOffset; }; - struct SCommand_Screenshot : public SCommand + struct SCommand_TrySwapAndScreenshot : public SCommand { - SCommand_Screenshot() : - SCommand(CMD_SCREENSHOT) {} + SCommand_TrySwapAndScreenshot() : + SCommand(CMD_TRY_SWAP_AND_SCREENSHOT) {} CImageInfo *m_pImage; // processor will fill this out, the one who adds this command must free the data as well + bool *m_pSwapped; }; struct SCommand_Swap : public SCommand @@ -663,6 +666,12 @@ public: m_DataBuffer.Reset(); m_CommandCount = 0; + m_RenderCallCount = 0; + } + + void AddRenderCalls(size_t RenderCallCountToAdd) + { + m_RenderCallCount += RenderCallCountToAdd; } }; @@ -733,7 +742,7 @@ public: virtual bool IsIdle() const = 0; virtual void WaitForIdle() = 0; - virtual void GetDriverVersion(EGraphicsDriverAgeType DriverAgeType, int &Major, int &Minor, int &Patch) {} + virtual bool GetDriverVersion(EGraphicsDriverAgeType DriverAgeType, int &Major, int &Minor, int &Patch, const char *&pName, EBackendType BackendType) = 0; // checks if the current values of the config are a graphics modern API virtual bool IsConfigModernAPI() { return false; } virtual bool UseTrianglesAsQuad() { return false; } @@ -806,6 +815,10 @@ class CGraphics_Threaded : public IEngineGraphics std::vector m_Warnings; + // is a non full windowed (in a sense that the viewport won't include the whole window), + // forced viewport, so that it justifies our UI ratio needs + bool m_IsForcedViewport = false; + struct SVertexArrayInfo { SVertexArrayInfo() : @@ -906,6 +919,8 @@ class CGraphics_Threaded : public IEngineGraphics void AddBackEndWarningIfExists(); + void AdjustViewport(bool SendViewportChangeToBackend); + int IssueInit(); int InitWindow(); @@ -962,11 +977,11 @@ public: void CopyTextureBufferSub(uint8_t *pDestBuffer, uint8_t *pSourceBuffer, int FullWidth, int FullHeight, int ColorChannelCount, int SubOffsetX, int SubOffsetY, int SubCopyWidth, int SubCopyHeight) override; void CopyTextureFromTextureBufferSub(uint8_t *pDestBuffer, int DestWidth, int DestHeight, uint8_t *pSourceBuffer, int SrcWidth, int SrcHeight, int ColorChannelCount, int SrcSubOffsetX, int SrcSubOffsetY, int SrcSubCopyWidth, int SrcSubCopyHeight) override; - void ScreenshotDirect(); + bool ScreenshotDirect(); void TextureSet(CTextureHandle TextureID) override; - void Clear(float r, float g, float b) override; + void Clear(float r, float g, float b, bool ForceClearNow = false) override; void QuadsBegin() override; void QuadsEnd() override; @@ -1197,12 +1212,14 @@ public: { return; } + + m_pCommandBuffer->AddRenderCalls(1); } void FlushVertices(bool KeepVertices = false) override; void FlushVerticesTex3D() override; - void RenderTileLayer(int BufferContainerIndex, float *pColor, char **pOffsets, unsigned int *IndicedVertexDrawNum, size_t NumIndicesOffet) override; + void RenderTileLayer(int BufferContainerIndex, float *pColor, char **pOffsets, unsigned int *IndicedVertexDrawNum, size_t NumIndicesOffset) override; void RenderBorderTiles(int BufferContainerIndex, float *pColor, char *pIndexBufferOffset, float *pOffset, float *pDir, int JumpIndex, unsigned int DrawNum) override; void RenderBorderTileLines(int BufferContainerIndex, float *pColor, char *pIndexBufferOffset, float *pOffset, float *pDir, unsigned int IndexDrawNum, unsigned int RedrawNum) override; void RenderQuadLayer(int BufferContainerIndex, SQuadRenderInfo *pQuadInfo, int QuadNum, int QuadOffset) override; @@ -1261,7 +1278,7 @@ public: SWarning *GetCurWarning() override; - void GetDriverVersion(EGraphicsDriverAgeType DriverAgeType, int &Major, int &Minor, int &Patch) override { m_pBackend->GetDriverVersion(DriverAgeType, Major, Minor, Patch); } + bool GetDriverVersion(EGraphicsDriverAgeType DriverAgeType, int &Major, int &Minor, int &Patch, const char *&pName, EBackendType BackendType) override { return m_pBackend->GetDriverVersion(DriverAgeType, Major, Minor, Patch, pName, BackendType); } bool IsConfigModernAPI() override { return m_pBackend->IsConfigModernAPI(); } bool IsTileBufferingEnabled() override { return m_GLTileBufferingEnabled; } bool IsQuadBufferingEnabled() override { return m_GLQuadBufferingEnabled; } diff --git a/src/engine/client/graphics_threaded_null.h b/src/engine/client/graphics_threaded_null.h index e96a76bbb..72a92dd5c 100644 --- a/src/engine/client/graphics_threaded_null.h +++ b/src/engine/client/graphics_threaded_null.h @@ -82,7 +82,7 @@ public: void TextureSet(CTextureHandle TextureID) override{}; - void Clear(float r, float g, float b) override{}; + void Clear(float r, float g, float b, bool ForceClearNow = false) override{}; void QuadsBegin() override{}; void QuadsEnd() override{}; @@ -138,7 +138,7 @@ public: void FlushVertices(bool KeepVertices = false) override{}; void FlushVerticesTex3D() override{}; - void RenderTileLayer(int BufferContainerIndex, float *pColor, char **pOffsets, unsigned int *IndicedVertexDrawNum, size_t NumIndicesOffet) override{}; + void RenderTileLayer(int BufferContainerIndex, float *pColor, char **pOffsets, unsigned int *IndicedVertexDrawNum, size_t NumIndicesOffset) override{}; void RenderBorderTiles(int BufferContainerIndex, float *pColor, char *pIndexBufferOffset, float *pOffset, float *pDir, int JumpIndex, unsigned int DrawNum) override{}; void RenderBorderTileLines(int BufferContainerIndex, float *pColor, char *pIndexBufferOffset, float *pOffset, float *pDir, unsigned int IndexDrawNum, unsigned int RedrawNum) override{}; void RenderQuadLayer(int BufferContainerIndex, SQuadRenderInfo *pQuadInfo, int QuadNum, int QuadOffset) override{}; @@ -194,7 +194,7 @@ public: SWarning *GetCurWarning() override { return NULL; } - void GetDriverVersion(EGraphicsDriverAgeType DriverAgeType, int &Major, int &Minor, int &Patch) override {} + bool GetDriverVersion(EGraphicsDriverAgeType DriverAgeType, int &Major, int &Minor, int &Patch, const char *&pName, EBackendType BackendType) override { return false; } bool IsConfigModernAPI() override { return false; } bool IsTileBufferingEnabled() override { return false; } bool IsQuadBufferingEnabled() override { return false; } diff --git a/src/engine/graphics.h b/src/engine/graphics.h index c2b98ef4b..086ee53ab 100644 --- a/src/engine/graphics.h +++ b/src/engine/graphics.h @@ -42,6 +42,8 @@ struct SQuadRenderInfo float m_aColor[4]; float m_aOffsets[2]; float m_Rotation; + // allows easier upload for uniform buffers because of the alignment requirements + float m_Padding; }; struct SGraphicTile @@ -151,11 +153,28 @@ struct GL_SVertexTex3DStream GL_STexCoord3D m_Tex; }; +static constexpr size_t gs_GraphicsMaxQuadsRenderCount = 256; +static constexpr size_t gs_GraphicsMaxParticlesRenderCount = 512; + enum EGraphicsDriverAgeType { GRAPHICS_DRIVER_AGE_TYPE_LEGACY = 0, GRAPHICS_DRIVER_AGE_TYPE_DEFAULT, GRAPHICS_DRIVER_AGE_TYPE_MODERN, + + GRAPHICS_DRIVER_AGE_TYPE_COUNT, +}; + +enum EBackendType +{ + BACKEND_TYPE_OPENGL = 0, + BACKEND_TYPE_OPENGL_ES, + BACKEND_TYPE_VULKAN, + + // special value to tell the backend to identify the current backend + BACKEND_TYPE_AUTO, + + BACKEND_TYPE_COUNT, }; struct STWGraphicGPU @@ -235,7 +254,8 @@ public: virtual void WindowDestroyNtf(uint32_t WindowID) = 0; virtual void WindowCreateNtf(uint32_t WindowID) = 0; - virtual void Clear(float r, float g, float b) = 0; + // ForceClearNow forces the backend to trigger a clear, even at performance cost, else it might be delayed by one frame + virtual void Clear(float r, float g, float b, bool ForceClearNow = false) = 0; virtual void ClipEnable(int x, int y, int w, int h) = 0; virtual void ClipDisable() = 0; @@ -291,7 +311,7 @@ public: virtual void FlushVerticesTex3D() = 0; // specific render functions - virtual void RenderTileLayer(int BufferContainerIndex, float *pColor, char **pOffsets, unsigned int *IndicedVertexDrawNum, size_t NumIndicesOffet) = 0; + virtual void RenderTileLayer(int BufferContainerIndex, float *pColor, char **pOffsets, unsigned int *IndicedVertexDrawNum, size_t NumIndicesOffset) = 0; virtual void RenderBorderTiles(int BufferContainerIndex, float *pColor, char *pIndexBufferOffset, float *pOffset, float *pDir, int JumpIndex, unsigned int DrawNum) = 0; virtual void RenderBorderTileLines(int BufferContainerIndex, float *pColor, char *pIndexBufferOffset, float *pOffset, float *pDir, unsigned int IndexDrawNum, unsigned int RedrawNum) = 0; virtual void RenderQuadLayer(int BufferContainerIndex, SQuadRenderInfo *pQuadInfo, int QuadNum, int QuadOffset) = 0; @@ -315,7 +335,8 @@ public: virtual void DeleteBufferContainer(int ContainerIndex, bool DestroyAllBO = true) = 0; virtual void IndicesNumRequiredNotify(unsigned int RequiredIndicesCount) = 0; - virtual void GetDriverVersion(EGraphicsDriverAgeType DriverAgeType, int &Major, int &Minor, int &Patch) = 0; + // returns true if the driver age type is supported, passing BACKEND_TYPE_AUTO for BackendType will query the values for the currently used backend + virtual bool GetDriverVersion(EGraphicsDriverAgeType DriverAgeType, int &Major, int &Minor, int &Patch, const char *&pName, EBackendType BackendType) = 0; virtual bool IsConfigModernAPI() = 0; virtual bool IsTileBufferingEnabled() = 0; virtual bool IsQuadBufferingEnabled() = 0; @@ -440,6 +461,7 @@ public: virtual void NotifyWindow() = 0; // be aware that this function should only be called from the graphics thread, and even then you should really know what you are doing + // this function always returns the pixels in RGB virtual TGLBackendReadPresentedImageData &GetReadPresentedImageDataFuncUnsafe() = 0; virtual SWarning *GetCurWarning() = 0; diff --git a/src/engine/shared/config_variables.h b/src/engine/shared/config_variables.h index e0c9aed79..4647c7348 100644 --- a/src/engine/shared/config_variables.h +++ b/src/engine/shared/config_variables.h @@ -178,7 +178,7 @@ MACRO_CONFIG_INT(DbgCurl, dbg_curl, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SERVER, "D MACRO_CONFIG_INT(DbgPref, dbg_pref, 0, 0, 1, CFGFLAG_SERVER, "Performance outputs") MACRO_CONFIG_INT(DbgGraphs, dbg_graphs, 0, 0, 1, CFGFLAG_CLIENT, "Performance graphs") MACRO_CONFIG_INT(DbgHitch, dbg_hitch, 0, 0, 0, CFGFLAG_SERVER, "Hitch warnings") -MACRO_CONFIG_INT(DbgGfx, dbg_gfx, 0, 0, 4, CFGFLAG_CLIENT, "Show graphic library warnings and errors, if the GPU supports it (0: none, 1: minimal, 2: affects performance, 3: verbose, 4: all") +MACRO_CONFIG_INT(DbgGfx, dbg_gfx, 0, 0, 4, CFGFLAG_CLIENT, "Show graphic library warnings and errors, if the GPU supports it (0: none, 1: minimal, 2: affects performance, 3: verbose, 4: all)") #ifdef CONF_DEBUG MACRO_CONFIG_INT(DbgStress, dbg_stress, 0, 0, 0, CFGFLAG_CLIENT | CFGFLAG_SERVER, "Stress systems (Debug build only)") MACRO_CONFIG_INT(DbgStressNetwork, dbg_stress_network, 0, 0, 0, CFGFLAG_CLIENT | CFGFLAG_SERVER, "Stress network (Debug build only)") @@ -418,7 +418,7 @@ MACRO_CONFIG_STR(Gfx3DTextureAnalysisRenderer, gfx_3d_texture_analysis_renderer, MACRO_CONFIG_STR(Gfx3DTextureAnalysisVersion, gfx_3d_texture_analysis_version, 128, "", CFGFLAG_SAVE | CFGFLAG_CLIENT, "The version on which the analysis was performed") MACRO_CONFIG_STR(GfxGPUName, gfx_gpu_name, 256, "auto", CFGFLAG_SAVE | CFGFLAG_CLIENT, "The GPU's name, which will be selected by the backend. (if supported by the backend)") -MACRO_CONFIG_STR(GfxBackend, gfx_backend, 256, "auto", CFGFLAG_SAVE | CFGFLAG_CLIENT, "The backend to use (e.g. opengl or vulkan)") +MACRO_CONFIG_STR(GfxBackend, gfx_backend, 256, "OpenGL", CFGFLAG_SAVE | CFGFLAG_CLIENT, "The backend to use (e.g. OpenGL or Vulkan)") MACRO_CONFIG_INT(GfxRenderThreadCount, gfx_render_thread_count, 3, 0, 0, CFGFLAG_SAVE | CFGFLAG_CLIENT, "Number of threads the backend can use for rendering. (note: the value can be ignored by the backend)") MACRO_CONFIG_INT(GfxDriverIsBlocked, gfx_driver_is_blocked, 0, 0, 1, CFGFLAG_SAVE | CFGFLAG_CLIENT, "If 1, the current driver is in a blocked error state.") diff --git a/src/game/client/components/maplayers.cpp b/src/game/client/components/maplayers.cpp index 813549a8f..7cf44f35c 100644 --- a/src/game/client/components/maplayers.cpp +++ b/src/game/client/components/maplayers.cpp @@ -844,6 +844,7 @@ void CMapLayers::OnMapLoad() // then create the buffer container SBufferContainerInfo ContainerInfo; ContainerInfo.m_Stride = (DoTextureCoords ? (sizeof(float) * 2 + sizeof(vec3)) : 0); + ContainerInfo.m_VertBufferBindingIndex = BufferObjectIndex; ContainerInfo.m_Attributes.emplace_back(); SBufferContainerInfo::SAttribute *pAttr = &ContainerInfo.m_Attributes.back(); pAttr->m_DataTypeCount = 2; @@ -946,6 +947,7 @@ void CMapLayers::OnMapLoad() // then create the buffer container SBufferContainerInfo ContainerInfo; ContainerInfo.m_Stride = (Textured ? (sizeof(STmpQuadTextured) / 4) : (sizeof(STmpQuad) / 4)); + ContainerInfo.m_VertBufferBindingIndex = BufferObjectIndex; ContainerInfo.m_Attributes.emplace_back(); SBufferContainerInfo::SAttribute *pAttr = &ContainerInfo.m_Attributes.back(); pAttr->m_DataTypeCount = 4; @@ -1401,6 +1403,17 @@ void CMapLayers::RenderQuadLayer(int LayerIndex, CMapItemLayerQuads *pQuadLayer, Rot = aChannels[2] / 180.0f * pi; } + bool NeedsFlush = QuadsRenderCount == gs_GraphicsMaxQuadsRenderCount || !(aColor[3] > 0); + + if(NeedsFlush) + { + // render quads of the current offset directly(cancel batching) + Graphics()->RenderQuadLayer(Visuals.m_BufferContainerIndex, &s_QuadRenderInfo[0], QuadsRenderCount, CurQuadOffset); + QuadsRenderCount = 0; + // since this quad is ignored, the offset is the next quad + CurQuadOffset = i + 1; + } + if(aColor[3] > 0) { SQuadRenderInfo &QInfo = s_QuadRenderInfo[QuadsRenderCount++]; @@ -1409,14 +1422,6 @@ void CMapLayers::RenderQuadLayer(int LayerIndex, CMapItemLayerQuads *pQuadLayer, QInfo.m_aOffsets[1] = OffsetY; QInfo.m_Rotation = Rot; } - else - { - // render quads of the current offset directly(cancel batching) - Graphics()->RenderQuadLayer(Visuals.m_BufferContainerIndex, &s_QuadRenderInfo[0], QuadsRenderCount, CurQuadOffset); - QuadsRenderCount = 0; - // since this quad is ignored, the offset is the next quad - CurQuadOffset = i + 1; - } } Graphics()->RenderQuadLayer(Visuals.m_BufferContainerIndex, &s_QuadRenderInfo[0], QuadsRenderCount, CurQuadOffset); } diff --git a/src/game/client/components/menus_settings.cpp b/src/game/client/components/menus_settings.cpp index 0f4420773..2f2be6c9c 100644 --- a/src/game/client/components/menus_settings.cpp +++ b/src/game/client/components/menus_settings.cpp @@ -35,6 +35,8 @@ #include #include +#include + CMenusKeyBinder CMenus::m_Binder; CMenusKeyBinder::CMenusKeyBinder() @@ -1100,9 +1102,7 @@ void CMenus::RenderSettingsGraphics(CUIRect MainView) static CVideoMode s_aModes[MAX_RESOLUTIONS]; static int s_NumNodes = Graphics()->GetVideoModes(s_aModes, MAX_RESOLUTIONS, g_Config.m_GfxScreen); static int s_GfxFsaaSamples = g_Config.m_GfxFsaaSamples; - static int s_GfxModernGLVersion = Graphics()->IsConfigModernAPI(); - static int s_GfxEnableTextureUnitOptimization = g_Config.m_GfxEnableTextureUnitOptimization; - static int s_GfxUsePreinitBuffer = g_Config.m_GfxUsePreinitBuffer; + static bool s_GfxBackendChanged = false; static int s_GfxHighdpi = g_Config.m_GfxHighdpi; static int s_InitDisplayAllVideoModes = g_Config.m_GfxDisplayAllVideoModes; @@ -1185,6 +1185,8 @@ void CMenus::RenderSettingsGraphics(CUIRect MainView) aWindowModeIDs[i] = &s_aWindowModeIDs[i]; static int s_WindowModeDropDownState = 0; + static int s_OldSelectedBackend = -1; + OldSelected = (g_Config.m_GfxFullscreen ? (g_Config.m_GfxFullscreen == 1 ? 4 : (g_Config.m_GfxFullscreen == 2 ? 3 : 2)) : (g_Config.m_GfxBorderless ? 1 : 0)); const int NewWindowMode = RenderDropDown(s_WindowModeDropDownState, &MainView, OldSelected, aWindowModeIDs, pWindowModes, s_NumWindowMode, &s_NumWindowMode, s_ScrollValueDrop); @@ -1244,46 +1246,6 @@ void CMenus::RenderSettingsGraphics(CUIRect MainView) if(DoButton_CheckBox(&g_Config.m_GfxHighDetail, Localize("High Detail"), g_Config.m_GfxHighDetail, &Button)) g_Config.m_GfxHighDetail ^= 1; - bool IsModernGL = Graphics()->IsConfigModernAPI(); - - // only promote modern GL in menu settings if the driver isn't on the blocklist already - if(g_Config.m_GfxDriverIsBlocked == 0) - { - MainView.HSplitTop(20.0f, &Button, &MainView); - - if(DoButton_CheckBox(&g_Config.m_GfxGLMajor, Localize("Use modern OpenGL"), IsModernGL, &Button)) - { - CheckSettings = true; - if(IsModernGL) - { - Graphics()->GetDriverVersion(GRAPHICS_DRIVER_AGE_TYPE_DEFAULT, g_Config.m_GfxGLMajor, g_Config.m_GfxGLMinor, g_Config.m_GfxGLPatch); - IsModernGL = false; - } - else - { - Graphics()->GetDriverVersion(GRAPHICS_DRIVER_AGE_TYPE_MODERN, g_Config.m_GfxGLMajor, g_Config.m_GfxGLMinor, g_Config.m_GfxGLPatch); - IsModernGL = true; - } - } - - if(IsModernGL) - { - MainView.HSplitTop(20.0f, &Button, &MainView); - if(DoButton_CheckBox(&g_Config.m_GfxUsePreinitBuffer, Localize("Preinit VBO (iGPUs only)"), g_Config.m_GfxUsePreinitBuffer, &Button)) - { - CheckSettings = true; - g_Config.m_GfxUsePreinitBuffer ^= 1; - } - - MainView.HSplitTop(20.0f, &Button, &MainView); - if(DoButton_CheckBox(&g_Config.m_GfxEnableTextureUnitOptimization, Localize("Multiple texture units (disable for macOS)"), g_Config.m_GfxEnableTextureUnitOptimization, &Button)) - { - CheckSettings = true; - g_Config.m_GfxEnableTextureUnitOptimization ^= 1; - } - } - } - MainView.HSplitTop(20.0f, &Button, &MainView); if(DoButton_CheckBox(&g_Config.m_GfxHighdpi, Localize("Use high DPI"), g_Config.m_GfxHighdpi, &Button)) { @@ -1291,16 +1253,6 @@ void CMenus::RenderSettingsGraphics(CUIRect MainView) g_Config.m_GfxHighdpi ^= 1; } - // check if the new settings require a restart - if(CheckSettings) - { - m_NeedRestartGraphics = !(s_GfxFsaaSamples == g_Config.m_GfxFsaaSamples && - s_GfxModernGLVersion == (int)IsModernGL && - s_GfxUsePreinitBuffer == g_Config.m_GfxUsePreinitBuffer && - s_GfxEnableTextureUnitOptimization == g_Config.m_GfxEnableTextureUnitOptimization && - s_GfxHighdpi == g_Config.m_GfxHighdpi); - } - MainView.HSplitTop(20.0f, &Label, &MainView); Label.VSplitLeft(160.0f, &Label, &Button); if(g_Config.m_GfxRefreshRate) @@ -1322,13 +1274,143 @@ void CMenus::RenderSettingsGraphics(CUIRect MainView) MainView.y = HSLBar.y; MainView.h = MainView.h - MainView.y; + // Backend list + struct SMenuBackendInfo + { + int m_Major = 0; + int m_Minor = 0; + int m_Patch = 0; + const char *m_pBackendName = ""; + bool m_Found = false; + }; + std::array, EBackendType::BACKEND_TYPE_COUNT> aaSupportedBackends{}; + uint32_t FoundBackendCount = 0; + for(uint32_t i = 0; i < BACKEND_TYPE_COUNT; ++i) + { + if(EBackendType(i) == BACKEND_TYPE_AUTO) + continue; + for(uint32_t n = 0; n < GRAPHICS_DRIVER_AGE_TYPE_COUNT; ++n) + { + auto &Info = aaSupportedBackends[i][n]; + if(Graphics()->GetDriverVersion(EGraphicsDriverAgeType(n), Info.m_Major, Info.m_Minor, Info.m_Patch, Info.m_pBackendName, EBackendType(i))) + { + // don't count blocked opengl drivers + if(EBackendType(i) != BACKEND_TYPE_OPENGL || EGraphicsDriverAgeType(n) == GRAPHICS_DRIVER_AGE_TYPE_LEGACY || g_Config.m_GfxDriverIsBlocked == 0) + { + Info.m_Found = true; + ++FoundBackendCount; + } + } + } + } + + if(FoundBackendCount > 1) + { + MainView.HSplitTop(10.0f, nullptr, &MainView); + MainView.HSplitTop(20.0f, &Text, &MainView); + UI()->DoLabelScaled(&Text, Localize("Renderer"), 16.0f, TEXTALIGN_CENTER); + + static float s_ScrollValueDropBackend = 0; + static int s_BackendDropDownState = 0; + + static std::vector> vBackendIDs; + static std::vector vBackendIDPtrs; + static std::vector vBackendIDNames; + static std::vector vBackendIDNamesCStr; + static std::vector vBackendInfos; + + size_t BackendCount = FoundBackendCount + 1; + vBackendIDs.resize(BackendCount); + vBackendIDPtrs.resize(BackendCount); + vBackendIDNames.resize(BackendCount); + vBackendIDNamesCStr.resize(BackendCount); + vBackendInfos.resize(BackendCount); + + char aTmpBackendName[256]; + + auto IsInfoDefault = [](const SMenuBackendInfo &CheckInfo) { + return str_comp_nocase(CheckInfo.m_pBackendName, "OpenGL") == 0 && CheckInfo.m_Major == 3 && CheckInfo.m_Minor == 0 && CheckInfo.m_Patch == 0; + }; + + int OldSelectedBackend = -1; + uint32_t CurCounter = 0; + for(uint32_t i = 0; i < BACKEND_TYPE_COUNT; ++i) + { + for(uint32_t n = 0; n < GRAPHICS_DRIVER_AGE_TYPE_COUNT; ++n) + { + auto &Info = aaSupportedBackends[i][n]; + if(Info.m_Found) + { + if(vBackendIDs[CurCounter].get() == nullptr) + vBackendIDs[CurCounter] = std::make_unique(); + vBackendIDPtrs[CurCounter] = vBackendIDs[CurCounter].get(); + + { + bool IsDefault = IsInfoDefault(Info); + str_format(aTmpBackendName, sizeof(aTmpBackendName), "%s (%d.%d.%d)%s%s", Info.m_pBackendName, Info.m_Major, Info.m_Minor, Info.m_Patch, IsDefault ? " - " : "", IsDefault ? Localize("default") : ""); + vBackendIDNames[CurCounter] = aTmpBackendName; + vBackendIDNamesCStr[CurCounter] = vBackendIDNames[CurCounter].c_str(); + if(str_comp_nocase(Info.m_pBackendName, g_Config.m_GfxBackend) == 0 && g_Config.m_GfxGLMajor == Info.m_Major && g_Config.m_GfxGLMinor == Info.m_Minor && g_Config.m_GfxGLPatch == Info.m_Patch) + { + OldSelectedBackend = CurCounter; + } + + vBackendInfos[CurCounter] = Info; + } + ++CurCounter; + } + } + } + + if(OldSelectedBackend != -1) + { + // no custom selected + BackendCount -= 1; + } + else + { + // custom selected one + if(vBackendIDs[CurCounter].get() == nullptr) + vBackendIDs[CurCounter] = std::make_unique(); + vBackendIDPtrs[CurCounter] = vBackendIDs[CurCounter].get(); + + str_format(aTmpBackendName, sizeof(aTmpBackendName), "%s (%s %d.%d.%d)", Localize("custom"), g_Config.m_GfxBackend, g_Config.m_GfxGLMajor, g_Config.m_GfxGLMinor, g_Config.m_GfxGLPatch); + vBackendIDNames[CurCounter] = aTmpBackendName; + vBackendIDNamesCStr[CurCounter] = vBackendIDNames[CurCounter].c_str(); + OldSelectedBackend = CurCounter; + + vBackendInfos[CurCounter].m_pBackendName = "custom"; + vBackendInfos[CurCounter].m_Major = g_Config.m_GfxGLMajor; + vBackendInfos[CurCounter].m_Minor = g_Config.m_GfxGLMinor; + vBackendInfos[CurCounter].m_Patch = g_Config.m_GfxGLPatch; + } + + if(s_OldSelectedBackend == -1) + s_OldSelectedBackend = OldSelectedBackend; + + static int s_BackendCount = 0; + s_BackendCount = BackendCount; + + const int NewBackend = RenderDropDown(s_BackendDropDownState, &MainView, OldSelectedBackend, vBackendIDPtrs.data(), vBackendIDNamesCStr.data(), s_BackendCount, &s_BackendCount, s_ScrollValueDropBackend); + if(OldSelectedBackend != NewBackend) + { + str_copy(g_Config.m_GfxBackend, vBackendInfos[NewBackend].m_pBackendName, sizeof(g_Config.m_GfxBackend)); + g_Config.m_GfxGLMajor = vBackendInfos[NewBackend].m_Major; + g_Config.m_GfxGLMinor = vBackendInfos[NewBackend].m_Minor; + g_Config.m_GfxGLPatch = vBackendInfos[NewBackend].m_Patch; + + CheckSettings = true; + s_GfxBackendChanged = s_OldSelectedBackend != NewBackend; + } + } + // GPU list const auto &GPUList = Graphics()->GetGPUs(); if(GPUList.m_GPUs.size() > 1) { MainView.HSplitTop(10.0f, nullptr, &MainView); MainView.HSplitTop(20.0f, &Text, &MainView); - UI()->DoLabelScaled(&Text, Localize("Graphic cards"), 16.0f, TEXTALIGN_CENTER); + UI()->DoLabelScaled(&Text, Localize("Graphics cards"), 16.0f, TEXTALIGN_CENTER); static float s_ScrollValueDropGPU = 0; static int s_GPUDropDownState = 0; @@ -1348,7 +1430,7 @@ void CMenus::RenderSettingsGraphics(CUIRect MainView) for(size_t i = 0; i < GPUCount; ++i) { if(vGPUIDs[i].get() == nullptr) - vGPUIDs[i] = std::unique_ptr(new int()); + vGPUIDs[i] = std::make_unique(); vGPUIDPtrs[i] = vGPUIDs[i].get(); if(i == 0) { @@ -1381,6 +1463,14 @@ void CMenus::RenderSettingsGraphics(CUIRect MainView) str_copy(g_Config.m_GfxGPUName, GPUList.m_GPUs[NewGPU - 1].m_Name, sizeof(g_Config.m_GfxGPUName)); } } + + // check if the new settings require a restart + if(CheckSettings) + { + m_NeedRestartGraphics = !(s_GfxFsaaSamples == g_Config.m_GfxFsaaSamples && + !s_GfxBackendChanged && + s_GfxHighdpi == g_Config.m_GfxHighdpi); + } } void CMenus::RenderSettingsSound(CUIRect MainView) diff --git a/src/game/client/components/particles.cpp b/src/game/client/components/particles.cpp index efff96c07..199169373 100644 --- a/src/game/client/components/particles.cpp +++ b/src/game/client/components/particles.cpp @@ -231,7 +231,7 @@ void CParticles::RenderGroup(int Group) // the current position, respecting the size, is inside the viewport, render it, else ignore if(ParticleIsVisibleOnScreen(p, Size)) { - if(LastColor[0] != m_aParticles[i].m_Color.r || LastColor[1] != m_aParticles[i].m_Color.g || LastColor[2] != m_aParticles[i].m_Color.b || LastColor[3] != m_aParticles[i].m_Color.a || LastQuadOffset != QuadOffset) + if((size_t)CurParticleRenderCount == gs_GraphicsMaxParticlesRenderCount || LastColor[0] != m_aParticles[i].m_Color.r || LastColor[1] != m_aParticles[i].m_Color.g || LastColor[2] != m_aParticles[i].m_Color.b || LastColor[3] != m_aParticles[i].m_Color.a || LastQuadOffset != QuadOffset) { Graphics()->TextureSet(GameClient()->m_ParticlesSkin.m_SpriteParticles[LastQuadOffset - SPRITE_PART_SLICE]); Graphics()->RenderQuadContainerAsSpriteMultiple(m_ParticleQuadContainerIndex, LastQuadOffset, CurParticleRenderCount, s_aParticleRenderInfo); diff --git a/src/game/editor/editor.cpp b/src/game/editor/editor.cpp index 01fcf7902..d31471268 100644 --- a/src/game/editor/editor.cpp +++ b/src/game/editor/editor.cpp @@ -5666,7 +5666,7 @@ void CEditor::RenderMenubar(CUIRect MenuBar) void CEditor::Render() { // basic start - Graphics()->Clear(1.0f, 0.0f, 1.0f); + Graphics()->Clear(0.0f, 0.0f, 0.0f); CUIRect View = *UI()->Screen(); UI()->MapScreen();