From 68fa5dc2dac94eb4f33a7f292e5c4a1489b47733 Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Sat, 24 Oct 2026 03:06:33 +0200 Subject: [PATCH] feat(vulkan): gfxrenderer picks the opengl 4.6 or the vulkan 1.4 renderer (B1-B3) --- CMakeLists.txt | 26 + application/application.cpp | 172 +++--- application/application.h | 9 +- application/driveruipanels.cpp | 4 + application/uilayer.cpp | 6 +- rendering/opengl46/opengl46backend.cpp | 107 ++++ rendering/opengl46/opengl46renderer.cpp | 2 +- rendering/opengl46/opengl46renderer.h | 2 + rendering/renderer.cpp | 20 +- rendering/renderer.h | 30 +- rendering/vulkan14/vulkan14capabilities.cpp | 288 +++++++++ rendering/vulkan14/vulkan14capabilities.h | 108 ++++ rendering/vulkan14/vulkan14common.cpp | 145 +++++ rendering/vulkan14/vulkan14common.h | 42 ++ rendering/vulkan14/vulkan14device.cpp | 328 ++++++++++ rendering/vulkan14/vulkan14device.h | 91 +++ rendering/vulkan14/vulkan14instance.cpp | 342 +++++++++++ rendering/vulkan14/vulkan14instance.h | 64 ++ rendering/vulkan14/vulkan14renderer.cpp | 628 ++++++++++++++++++++ rendering/vulkan14/vulkan14renderer.h | 106 ++++ rendering/vulkan14/vulkan14swapchain.cpp | 455 ++++++++++++++ rendering/vulkan14/vulkan14swapchain.h | 122 ++++ scripting/pythonscreenviewer.cpp | 6 + utilities/Globals.cpp | 30 +- utilities/Globals.h | 6 + 25 files changed, 3056 insertions(+), 83 deletions(-) create mode 100644 rendering/opengl46/opengl46backend.cpp create mode 100644 rendering/vulkan14/vulkan14capabilities.cpp create mode 100644 rendering/vulkan14/vulkan14capabilities.h create mode 100644 rendering/vulkan14/vulkan14common.cpp create mode 100644 rendering/vulkan14/vulkan14common.h create mode 100644 rendering/vulkan14/vulkan14device.cpp create mode 100644 rendering/vulkan14/vulkan14device.h create mode 100644 rendering/vulkan14/vulkan14instance.cpp create mode 100644 rendering/vulkan14/vulkan14instance.h create mode 100644 rendering/vulkan14/vulkan14renderer.cpp create mode 100644 rendering/vulkan14/vulkan14renderer.h create mode 100644 rendering/vulkan14/vulkan14swapchain.cpp create mode 100644 rendering/vulkan14/vulkan14swapchain.h diff --git a/CMakeLists.txt b/CMakeLists.txt index eaf086e0..6dd55612 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -50,6 +50,7 @@ file(GLOB HEADERS "*.h" "scene/*.h" "rendering/*.h" "rendering/opengl46/*.h" +"rendering/vulkan14/*.h" "audio/*.h" "environment/*.h" "application/*.h" @@ -69,6 +70,7 @@ if (APPLE) endif() option(WITH_OPENGL_MODERN "Compile with OpenGL modern renderer" ON) +option(WITH_VULKAN "Compile with the Vulkan 1.4 renderer (gfxrenderer vulkan); needs the submodules ref/volk and ref/Vulkan-Headers" OFF) option(WITH_PYTHON "Python 3.14 embedded scripting" ON) option(WITH_UART "Compile with libserialport" ON) option(WITH_LUA "Compile with lua scripting support" ON) @@ -400,10 +402,29 @@ if (WITH_OPENGL_MODERN) "rendering/opengl46/opengl46gpuparticles.cpp" "rendering/opengl46/opengl46terrain.cpp" "rendering/opengl46/opengl46grass.cpp" + "rendering/opengl46/opengl46backend.cpp" "rendering/opengl46/opengl46renderer.cpp") set(DEFINITIONS ${DEFINITIONS} "WITH_OPENGL_MODERN") endif() +if (WITH_VULKAN) + # the vulkan loader is loaded by volk when the program runs, so the program starts without it (and runs with opengl) + set(VULKAN_HEADERS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ref/Vulkan-Headers/include") + set(VOLK_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ref/volk") + if (NOT EXISTS "${VOLK_DIR}/volk.h" OR NOT EXISTS "${VULKAN_HEADERS_DIR}/vulkan/vulkan_core.h") + message(FATAL_ERROR "WITH_VULKAN=ON needs the submodules ref/volk and ref/Vulkan-Headers: git submodule update --init ref/volk ref/Vulkan-Headers") + endif() + set(SOURCES ${SOURCES} + "rendering/vulkan14/vulkan14common.cpp" + "rendering/vulkan14/vulkan14capabilities.cpp" + "rendering/vulkan14/vulkan14instance.cpp" + "rendering/vulkan14/vulkan14device.cpp" + "rendering/vulkan14/vulkan14swapchain.cpp" + "rendering/vulkan14/vulkan14renderer.cpp" + "ref/volk/volk.c") + set(DEFINITIONS ${DEFINITIONS} "WITH_VULKAN") +endif() + add_subdirectory("${DEPS_DIR}/glfw/" EXCLUDE_FROM_ALL) set(SOURCES ${SOURCES} "extras/piped_proc.cpp" "widgets/cameraview_extcam.cpp") @@ -529,6 +550,11 @@ set_target_properties(eu07_core PROPERTIES add_executable(${PROJECT_NAME} ${SOURCES} ${HEADERS}) target_link_libraries(${PROJECT_NAME} eu07_core) +if (WITH_VULKAN) + # the vulkan headers are seen by the files of rendering/vulkan14 only, they include them + target_include_directories(${PROJECT_NAME} PRIVATE "${VULKAN_HEADERS_DIR}" "${VOLK_DIR}") + target_link_libraries(${PROJECT_NAME} ${CMAKE_DL_LIBS}) +endif() target_compile_definitions(eu07_settings INTERFACE ${DEFINITIONS}) diff --git a/application/application.cpp b/application/application.cpp index c8cfd0c4..0ad93f69 100644 --- a/application/application.cpp +++ b/application/application.cpp @@ -381,11 +381,6 @@ int eu07_application::init(int Argc, char *Argv[]) ErrorLog("Failed to initialize glfw!"); return result; } - if ((result = init_ogl()) != 0) - { - ErrorLog("Failed to initialize ogl!"); - return result; - } if (crashreport_is_pending()) { // run crashgui as early as possible @@ -966,6 +961,13 @@ GLFWwindow *eu07_application::window(int const Windowindex, bool visible, int wi glfwWindowHint(GLFW_VISIBLE, visible); + if (share_ctx && (m_gfxbackend != nullptr) && (false == m_gfxbackend->gl_context)) + { + // the windows of the renderer have no opengl context to share + WriteLog("window with an opengl context shared with the main window isn't available with the " + m_gfxbackend->name + " renderer"); + return nullptr; + } + auto *childwindow = glfwCreateWindow(width, height, "eu07window", monitor, share_ctx ? m_windows.front() : nullptr); if (!childwindow) return nullptr; @@ -1221,33 +1223,6 @@ int eu07_application::init_locale() return 0; } -// the context of the required version couldn't be created. a context with default settings tells which version the -// driver does provide, so the user can be told what is missing -static std::string describe_gl_driver() -{ - glfwDefaultWindowHints(); - glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE); - auto *probe = glfwCreateWindow(64, 64, "eu07 opengl probe", nullptr, nullptr); - if (probe == nullptr) - return "no OpenGL context"; - - glfwMakeContextCurrent(probe); - std::string description; - auto const getstring = reinterpret_cast(glfwGetProcAddress("glGetString")); - if (getstring != nullptr) - { - auto const text = [&](GLenum const Name) { - auto const *value = reinterpret_cast(getstring(Name)); - return std::string(value != nullptr ? value : "?"); - }; - description = "OpenGL " + text(GL_VERSION) + ", " + text(GL_RENDERER) + " (" + text(GL_VENDOR) + ")"; - } - glfwMakeContextCurrent(nullptr); - glfwDestroyWindow(probe); - - return description.empty() ? "unknown OpenGL version" : description; -} - int eu07_application::init_glfw() { if (glfwInit() == GLFW_FALSE) @@ -1267,29 +1242,64 @@ int eu07_application::init_glfw() } } + select_gfx_backend(); + + return create_main_window(); +} + +void eu07_application::select_gfx_backend() +{ + auto *factory{gfx_renderer_factory::get_instance()}; + m_gfxbackend = nullptr; + + if (Global.GfxRenderer == "vulkan") + { + // a renderer which can't run here falls back to opengl, so a setting in eu07.ini doesn't leave the user with a black screen + std::string reason; + auto const *vulkan{factory->backend("vulkan")}; + if (vulkan == nullptr) + { + reason = "this build of the simulator has no vulkan renderer"; + } + else if ((vulkan->probe == nullptr) || vulkan->probe(reason)) + { + m_gfxbackend = vulkan; + } + if (m_gfxbackend == nullptr) + { + WriteLog("gfxrenderer vulkan: not available (" + reason + "), using opengl"); + crashreport_add_info("gfxrenderer_fallback", reason); + Global.GfxRenderer = "default"; + } + } + + if (m_gfxbackend == nullptr) + { + m_gfxbackend = factory->backend("modern"); + } + + crashreport_add_info("gfxrenderer", Global.GfxRenderer); + WriteLog("renderer: " + (m_gfxbackend != nullptr ? m_gfxbackend->name : std::string("none"))); +} + +int eu07_application::create_main_window() +{ + if (m_gfxbackend == nullptr) + { + ErrorLog("Bad init: no renderer found"); + return -1; + } + auto *monitor{find_monitor(Global.fullscreen_monitor)}; if (!monitor) monitor = glfwGetPrimaryMonitor(); + // the window may be made a second time, after another backend was tried + glfwDefaultWindowHints(); glfwWindowHint(GLFW_AUTO_ICONIFY, GLFW_FALSE); - if (Global.gfx_skippipeline && Global.iMultisampling > 0) - { - glfwWindowHint(GLFW_SAMPLES, 1 << Global.iMultisampling); - } - - crashreport_add_info("gfxrenderer", Global.GfxRenderer); - - // activate core profile for opengl 4.6 renderer // NOTE: the hints stay in effect for the additional windows and shared contexts created later through window() - glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); - glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE); - glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4); - glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6); - - if (Global.gfx_gldebug) - glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GLFW_TRUE); - - glfwWindowHint(GLFW_SRGB_CAPABLE, !Global.gfx_shadergamma); + if (m_gfxbackend->window_hints != nullptr) + m_gfxbackend->window_hints(); if (Global.fullscreen_windowed) { @@ -1306,21 +1316,16 @@ int eu07_application::init_glfw() char const *glfwerror = nullptr; glfwGetError(&glfwerror); std::string const reason = (glfwerror != nullptr ? glfwerror : "unknown error"); - auto const driver = describe_gl_driver(); - ErrorLog("Bad init: failed to create OpenGL 4.6 core profile context (" + reason + "); the graphics card/driver provides: " + driver); -#ifdef _WIN32 - auto const message = "MaSzyna requires OpenGL 4.6 (core profile), but the OpenGL 4.6 context could not be created.\n\n" - "Your graphics card/driver provides:\n" + driver + "\n\n" - "Details: " + reason + "\n\n" - "Update the graphics driver or use a graphics card with OpenGL 4.6 support."; - MessageBoxA(nullptr, message.c_str(), "MaSzyna", MB_ICONERROR | MB_OK); -#endif + if (m_gfxbackend->window_failed != nullptr) + m_gfxbackend->window_failed(reason); ErrorLog("Bad init: failed to create glfw window"); return -1; } - glfwMakeContextCurrent(mainwindow); - glfwSwapInterval(Global.VSync ? 1 : 0); // vsync + if ((m_gfxbackend->after_window != nullptr) && (false == m_gfxbackend->after_window(mainwindow))) + { + return -1; + } { int width, height; @@ -1343,6 +1348,23 @@ int eu07_application::init_glfw() return 0; } +int eu07_application::fallback_to_opengl(std::string const &Reason) +{ + WriteLog("gfxrenderer vulkan: " + Reason + ", using opengl"); + crashreport_add_info("gfxrenderer_fallback", Reason); + Global.GfxRenderer = "default"; + m_gfxbackend = gfx_renderer_factory::get_instance()->backend("modern"); + crashreport_add_info("gfxrenderer", Global.GfxRenderer); + WriteLog("renderer: " + (m_gfxbackend != nullptr ? m_gfxbackend->name : std::string("none"))); + + // nothing hangs on the window yet (the ui is set up later), so it can be made anew with the hints of opengl + for (auto *window : m_windows) + glfwDestroyWindow(window); + m_windows.clear(); + + return create_main_window(); +} + void eu07_application::init_callbacks() { @@ -1358,17 +1380,6 @@ void eu07_application::init_callbacks() glfwSetCursorEnterCallback(window, cursor_enter_callback); } -int eu07_application::init_ogl() -{ - if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) - { - ErrorLog("Bad init: failed to initialize glad"); - return -1; - } - - return 0; -} - int eu07_application::init_ui() { if (false == ui_layer::init(m_windows.front())) @@ -1383,7 +1394,7 @@ int eu07_application::init_gfx() { // NOTE: global_settings maps unsupported gfxrenderer values (e.g. the removed "legacy") to "default" - GfxRenderer = gfx_renderer_factory::get_instance()->create("modern"); + GfxRenderer = (m_gfxbackend != nullptr && m_gfxbackend->create != nullptr) ? m_gfxbackend->create() : nullptr; if (!GfxRenderer) { @@ -1393,7 +1404,22 @@ int eu07_application::init_gfx() if (false == GfxRenderer->Init(m_windows.front())) { - return -1; + if (Global.GfxRenderer != "vulkan") + { + return -1; + } + // the reason is in the log already + GfxRenderer->Shutdown(); + GfxRenderer.reset(); + if (fallback_to_opengl("the renderer failed to start") != 0) + { + return -1; + } + GfxRenderer = m_gfxbackend->create(); + if (!GfxRenderer || (false == GfxRenderer->Init(m_windows.front()))) + { + return -1; + } } for (const global_settings::extraviewport_config &conf : Global.extra_viewports) diff --git a/application/application.h b/application/application.h index 0657d090..1306c1c7 100644 --- a/application/application.h +++ b/application/application.h @@ -17,6 +17,8 @@ http://mozilla.org/MPL/2.0/. #include "utilities/uart.h" #endif +struct gfx_backend_desc; + class eu07_application { const int MAX_NETWORK_PER_FRAME = 1000; @@ -105,8 +107,12 @@ private: int init_settings( int Argc, char *Argv[] ); int init_locale(); int init_glfw(); + // the backend of the renderer picked by gfxrenderer, opengl if it can't run here + void select_gfx_backend(); + int create_main_window(); + // the backend picked can't run after all: the main window is made anew for opengl + int fallback_to_opengl( std::string const &Reason ); void init_callbacks(); - int init_ogl(); int init_ui(); int init_gfx(); int init_audio(); @@ -122,6 +128,7 @@ private: mode_stack m_modestack; // current behaviour mode python_taskqueue m_taskqueue; std::vector m_windows; + gfx_backend_desc const *m_gfxbackend { nullptr }; // owned by gfx_renderer_factory std::optional m_network; std::optional m_headtrack; diff --git a/application/driveruipanels.cpp b/application/driveruipanels.cpp index b9a5df72..a501e250 100644 --- a/application/driveruipanels.cpp +++ b/application/driveruipanels.cpp @@ -1460,6 +1460,10 @@ debug_panel::update_section_renderer( std::vector &Output ) { { textline += "Shaders"; } + else if (Global.GfxRenderer == "vulkan") + { + textline += "Vulkan"; + } textline += " "; diff --git a/application/uilayer.cpp b/application/uilayer.cpp index 998ea46c..a2b49b00 100644 --- a/application/uilayer.cpp +++ b/application/uilayer.cpp @@ -389,7 +389,11 @@ bool ui_layer::init(GLFWwindow *Window) imgui_style(); - ImGui_ImplGlfw_InitForOpenGL(m_window, false); + // a window without an opengl context belongs to the vulkan renderer + if (glfwGetWindowAttrib(m_window, GLFW_CLIENT_API) == GLFW_NO_API) + ImGui_ImplGlfw_InitForVulkan(m_window, false); + else + ImGui_ImplGlfw_InitForOpenGL(m_window, false); if (!GfxRenderer->GetImguiRenderer() || !GfxRenderer->GetImguiRenderer()->Init()) { diff --git a/rendering/opengl46/opengl46backend.cpp b/rendering/opengl46/opengl46backend.cpp new file mode 100644 index 00000000..e050daa3 --- /dev/null +++ b/rendering/opengl46/opengl46backend.cpp @@ -0,0 +1,107 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +// how the application makes the windows of the opengl 4.6 renderer: a core profile context of the main window, which the +// windows made later share, and the gl functions loaded through it + +#include "stdafx.h" +#include "rendering/opengl46/opengl46renderer.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" + +namespace +{ + +// the context of the required version couldn't be created. a context with default settings tells which version the +// driver does provide, so the user can be told what is missing +std::string describe_gl_driver() +{ + glfwDefaultWindowHints(); + glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE); + auto *probe = glfwCreateWindow(64, 64, "eu07 opengl probe", nullptr, nullptr); + if (probe == nullptr) + return "no OpenGL context"; + + glfwMakeContextCurrent(probe); + std::string description; + auto const getstring = reinterpret_cast(glfwGetProcAddress("glGetString")); + if (getstring != nullptr) + { + auto const text = [&](GLenum const Name) { + auto const *value = reinterpret_cast(getstring(Name)); + return std::string(value != nullptr ? value : "?"); + }; + description = "OpenGL " + text(GL_VERSION) + ", " + text(GL_RENDERER) + " (" + text(GL_VENDOR) + ")"; + } + glfwMakeContextCurrent(nullptr); + glfwDestroyWindow(probe); + + return description.empty() ? "unknown OpenGL version" : description; +} + +void window_hints() +{ + if (Global.gfx_skippipeline && Global.iMultisampling > 0) + { + glfwWindowHint(GLFW_SAMPLES, 1 << Global.iMultisampling); + } + + // activate core profile for opengl 4.6 renderer + glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_API); + glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); + glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE); + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6); + + if (Global.gfx_gldebug) + glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GLFW_TRUE); + + glfwWindowHint(GLFW_SRGB_CAPABLE, !Global.gfx_shadergamma); +} + +bool after_window(GLFWwindow *Window) +{ + glfwMakeContextCurrent(Window); + glfwSwapInterval(Global.VSync ? 1 : 0); // vsync + + if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) + { + ErrorLog("Bad init: failed to initialize glad"); + return false; + } + + return true; +} + +void window_failed(std::string const &Reason) +{ + auto const driver = describe_gl_driver(); + ErrorLog("Bad init: failed to create OpenGL 4.6 core profile context (" + Reason + "); the graphics card/driver provides: " + driver); +#ifdef _WIN32 + auto const message = "MaSzyna requires OpenGL 4.6 (core profile), but the OpenGL 4.6 context could not be created.\n\n" + "Your graphics card/driver provides:\n" + driver + "\n\n" + "Details: " + Reason + "\n\n" + "Update the graphics driver or use a graphics card with OpenGL 4.6 support."; + MessageBoxA(nullptr, message.c_str(), "MaSzyna", MB_ICONERROR | MB_OK); +#endif +} + +} // namespace + +gfx_backend_desc opengl46_renderer::backend_desc() +{ + gfx_backend_desc desc; + desc.name = "opengl 4.6"; + desc.create = &opengl46_renderer::create_func; + desc.window_hints = &window_hints; + desc.after_window = &after_window; + desc.window_failed = &window_failed; + desc.gl_context = true; + return desc; +} diff --git a/rendering/opengl46/opengl46renderer.cpp b/rendering/opengl46/opengl46renderer.cpp index 732d3537..4e5df29e 100644 --- a/rendering/opengl46/opengl46renderer.cpp +++ b/rendering/opengl46/opengl46renderer.cpp @@ -10215,7 +10215,7 @@ std::unique_ptr opengl46_renderer::create_func() return std::unique_ptr(new opengl46_renderer()); } -bool opengl46_renderer::renderer_register = gfx_renderer_factory::get_instance()->register_backend("modern", opengl46_renderer::create_func); +bool opengl46_renderer::renderer_register = gfx_renderer_factory::get_instance()->register_backend("modern", opengl46_renderer::backend_desc()); bool opengl46_renderer::opengl46_imgui_renderer::Init() { diff --git a/rendering/opengl46/opengl46renderer.h b/rendering/opengl46/opengl46renderer.h index 01bb1321..9141174c 100644 --- a/rendering/opengl46/opengl46renderer.h +++ b/rendering/opengl46/opengl46renderer.h @@ -185,6 +185,8 @@ class opengl46_renderer : public gfx_renderer { GLenum static const sunlight{0}; static std::unique_ptr create_func(); + // how the windows of the renderer are made (opengl46backend.cpp) + static gfx_backend_desc backend_desc(); private: // types diff --git a/rendering/renderer.cpp b/rendering/renderer.cpp index d3116c80..f9f444f8 100644 --- a/rendering/renderer.cpp +++ b/rendering/renderer.cpp @@ -15,15 +15,29 @@ std::unique_ptr GfxRenderer; bool gfx_renderer_factory::register_backend(const std::string &backend, gfx_renderer_factory::create_method func) { - backends[backend] = func; + gfx_backend_desc desc; + desc.name = backend; + desc.create = func; + return register_backend(backend, desc); +} + +bool gfx_renderer_factory::register_backend(const std::string &backend, gfx_backend_desc const &desc) +{ + backends[backend] = desc; return true; } +gfx_backend_desc const *gfx_renderer_factory::backend(const std::string &name) const +{ + auto it = backends.find(name); + return (it != backends.end() ? &it->second : nullptr); +} + std::unique_ptr gfx_renderer_factory::create(const std::string &backend) { auto it = backends.find(backend); - if (it != backends.end()) - return it->second(); + if ((it != backends.end()) && (it->second.create != nullptr)) + return it->second.create(); ErrorLog("renderer \"" + backend + "\" not found!"); return nullptr; diff --git a/rendering/renderer.h b/rendering/renderer.h index 5b3041d1..f33a545a 100644 --- a/rendering/renderer.h +++ b/rendering/renderer.h @@ -184,18 +184,44 @@ public: virtual auto Render_Preview( TAnimModel *Instance, int const Size, int const Margin, bool const Shadows, std::vector &Image ) -> bool { return false; } }; +// what the application has to know about a backend before its renderer exists: whether it can run here at all, and how its +// windows are made. a window made for opengl can't be drawn to with vulkan and the other way round, so the application asks +// the backend picked by gfxrenderer before the main window is made, and falls back to opengl when it can't run +struct gfx_backend_desc +{ + using create_method = std::unique_ptr(*)(); + + std::string name; // for the log + create_method create { nullptr }; + // whether the backend can run on this machine. false: Reason tells why. null: it always can + bool (*probe)( std::string &Reason ) { nullptr }; + // glfw hints of the windows of the backend, set over the defaults before the main window is made. they stay in effect for + // the windows made after it + void (*window_hints)() { nullptr }; + // after the main window was made, before the renderer is. false: the backend can't use the window + bool (*after_window)( GLFWwindow *Window ) { nullptr }; + // the main window couldn't be made; Reason is the error of glfw. tells the user what is missing + void (*window_failed)( std::string const &Reason ) { nullptr }; + // the windows have an opengl context, which the windows made later can share + bool gl_context { true }; +}; + class gfx_renderer_factory { public: - using create_method = std::unique_ptr(*)(); + using create_method = gfx_backend_desc::create_method; + // a backend with no window of its own to care about (e.g. "null") bool register_backend(const std::string &backend, create_method func); + bool register_backend(const std::string &backend, gfx_backend_desc const &desc); + // the description of the backend registered by the name, null if there's none (e.g. not built in) + gfx_backend_desc const *backend(const std::string &name) const; std::unique_ptr create(const std::string &name); static gfx_renderer_factory* get_instance(); private: - std::unordered_map backends; + std::unordered_map backends; static gfx_renderer_factory *instance; }; diff --git a/rendering/vulkan14/vulkan14capabilities.cpp b/rendering/vulkan14/vulkan14capabilities.cpp new file mode 100644 index 00000000..a954897b --- /dev/null +++ b/rendering/vulkan14/vulkan14capabilities.cpp @@ -0,0 +1,288 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "rendering/vulkan14/vulkan14capabilities.h" + +#include + +namespace vk14 +{ + +namespace +{ + +// one VkBool32 in the chain; without captures, so it turns into a plain function pointer and the table stays constant +#define VK14_FIELD(member, field) [](feature_chain &chain) -> VkBool32 & { return chain.member.field; } +#define VK14_CORE(field) VK14_FIELD(features2.features, field) + +// columns: structure, name, required, guaranteed by vulkan 1.4, purpose. +// +// beyond what vulkan 1.4 requires of every device the renderer needs multiDrawIndirect, shaderDrawParameters, +// textureCompressionBC and geometryShader, so a gpu without one of them is turned down by its name rather than failing later. +// "guaranteed" follows the minimum requirements of vulkan 1.2-1.4 (VP_LUNARG_minimum_requirements_1_4 of the vulkan sdk) +std::vector const g_feature_table = { + // VkPhysicalDeviceFeatures (1.0) + {"VkPhysicalDeviceFeatures", "geometryShader", true, false, "map icons and cube shadow maps of the lamps until multiview takes them over", VK14_CORE(geometryShader)}, + {"VkPhysicalDeviceFeatures", "multiDrawIndirect", true, false, "multi draw indirect of the gpu culling", VK14_CORE(multiDrawIndirect)}, + {"VkPhysicalDeviceFeatures", "textureCompressionBC", true, false, "bc1-bc7 compressed dds textures", VK14_CORE(textureCompressionBC)}, + {"VkPhysicalDeviceFeatures", "drawIndirectFirstInstance", true, true, "instanced indirect draws", VK14_CORE(drawIndirectFirstInstance)}, + {"VkPhysicalDeviceFeatures", "fullDrawIndexUint32", true, true, "32 bit index buffers", VK14_CORE(fullDrawIndexUint32)}, + {"VkPhysicalDeviceFeatures", "imageCubeArray", true, true, "environment map and cube shadow maps", VK14_CORE(imageCubeArray)}, + {"VkPhysicalDeviceFeatures", "independentBlend", true, true, "blending per attachment", VK14_CORE(independentBlend)}, + {"VkPhysicalDeviceFeatures", "sampleRateShading", true, true, "multisampling", VK14_CORE(sampleRateShading)}, + {"VkPhysicalDeviceFeatures", "depthClamp", true, true, "shadow maps", VK14_CORE(depthClamp)}, + {"VkPhysicalDeviceFeatures", "depthBiasClamp", true, true, "shadow maps", VK14_CORE(depthBiasClamp)}, + {"VkPhysicalDeviceFeatures", "samplerAnisotropy", true, true, "anisotropic texture filtering", VK14_CORE(samplerAnisotropy)}, + {"VkPhysicalDeviceFeatures", "largePoints", true, true, "points larger than a pixel", VK14_CORE(largePoints)}, + {"VkPhysicalDeviceFeatures", "fragmentStoresAndAtomics", true, true, "clustered lights and post-processing", VK14_CORE(fragmentStoresAndAtomics)}, + {"VkPhysicalDeviceFeatures", "shaderImageGatherExtended", true, true, "shadow filtering", VK14_CORE(shaderImageGatherExtended)}, + {"VkPhysicalDeviceFeatures", "shaderStorageImageExtendedFormats", true, true, "compute passes", VK14_CORE(shaderStorageImageExtendedFormats)}, + {"VkPhysicalDeviceFeatures", "fillModeNonSolid", false, false, "wireframe", VK14_CORE(fillModeNonSolid)}, + {"VkPhysicalDeviceFeatures", "pipelineStatisticsQuery", false, false, "draw statistics", VK14_CORE(pipelineStatisticsQuery)}, + + // VkPhysicalDeviceVulkan11Features + {"VkPhysicalDeviceVulkan11Features", "shaderDrawParameters", true, false, "gl_DrawID and gl_BaseInstance of multi draws", VK14_FIELD(v11, shaderDrawParameters)}, + {"VkPhysicalDeviceVulkan11Features", "multiview", true, true, "cube shadow maps of the lamps in one pass", VK14_FIELD(v11, multiview)}, + + // VkPhysicalDeviceVulkan12Features + {"VkPhysicalDeviceVulkan12Features", "timelineSemaphore", true, true, "synchronization of the frames", VK14_FIELD(v12, timelineSemaphore)}, + {"VkPhysicalDeviceVulkan12Features", "scalarBlockLayout", true, true, "buffer layouts shared with the c++ code", VK14_FIELD(v12, scalarBlockLayout)}, + {"VkPhysicalDeviceVulkan12Features", "uniformBufferStandardLayout", true, true, "std430 uniform buffers", VK14_FIELD(v12, uniformBufferStandardLayout)}, + {"VkPhysicalDeviceVulkan12Features", "hostQueryReset", true, true, "gpu times of the passes", VK14_FIELD(v12, hostQueryReset)}, + {"VkPhysicalDeviceVulkan12Features", "separateDepthStencilLayouts", true, true, "depth read while the stencil is written", VK14_FIELD(v12, separateDepthStencilLayouts)}, + {"VkPhysicalDeviceVulkan12Features", "drawIndirectCount", false, false, "draw counts made on the gpu", VK14_FIELD(v12, drawIndirectCount)}, + {"VkPhysicalDeviceVulkan12Features", "samplerFilterMinmax", false, false, "min/max reduction of the depth pyramid", VK14_FIELD(v12, samplerFilterMinmax)}, + {"VkPhysicalDeviceVulkan12Features", "shaderOutputLayer", false, false, "layered rendering without a geometry shader", VK14_FIELD(v12, shaderOutputLayer)}, + + // VkPhysicalDeviceVulkan13Features + {"VkPhysicalDeviceVulkan13Features", "dynamicRendering", true, true, "passes without VkRenderPass and VkFramebuffer", VK14_FIELD(v13, dynamicRendering)}, + {"VkPhysicalDeviceVulkan13Features", "synchronization2", true, true, "barriers and submits", VK14_FIELD(v13, synchronization2)}, + {"VkPhysicalDeviceVulkan13Features", "maintenance4", true, true, "pipeline layouts and buffer sizes", VK14_FIELD(v13, maintenance4)}, + {"VkPhysicalDeviceVulkan13Features", "shaderDemoteToHelperInvocation", true, true, "alpha tested geometry", VK14_FIELD(v13, shaderDemoteToHelperInvocation)}, + {"VkPhysicalDeviceVulkan13Features", "pipelineCreationCacheControl", true, true, "pipelines made on worker threads", VK14_FIELD(v13, pipelineCreationCacheControl)}, + + // VkPhysicalDeviceVulkan14Features + {"VkPhysicalDeviceVulkan14Features", "pushDescriptor", true, true, "descriptors of materials and compute passes without pools", VK14_FIELD(v14, pushDescriptor)}, + {"VkPhysicalDeviceVulkan14Features", "maintenance5", true, true, "pipelines without shader modules", VK14_FIELD(v14, maintenance5)}, + {"VkPhysicalDeviceVulkan14Features", "maintenance6", true, true, "descriptor and push constant calls of 1.4", VK14_FIELD(v14, maintenance6)}, + {"VkPhysicalDeviceVulkan14Features", "hostImageCopy", false, false, "texture uploads without command buffers", VK14_FIELD(v14, hostImageCopy)}, +}; + +#undef VK14_CORE +#undef VK14_FIELD + +} // namespace + +char const *device_type_string(VkPhysicalDeviceType Type) +{ + switch (Type) + { + case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU: + return "integrated gpu"; + case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU: + return "discrete gpu"; + case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU: + return "virtual gpu"; + case VK_PHYSICAL_DEVICE_TYPE_CPU: + return "cpu"; + default: + break; + } + return "other"; +} + +std::string vendor_string(std::uint32_t VendorId) +{ + switch (VendorId) + { + case 0x1002: + return "AMD"; + case 0x106B: + return "Apple"; + case 0x10DE: + return "NVIDIA"; + case 0x13B5: + return "ARM"; + case 0x1414: + return "Microsoft"; + case 0x5143: + return "Qualcomm"; + case 0x8086: + return "Intel"; + case 0x10005: + return "Mesa"; + default: + break; + } + char buffer[16] = {}; + std::snprintf(buffer, sizeof(buffer), "0x%04X", VendorId); + return std::string(buffer); +} + +feature_chain::feature_chain() +{ + reset(); +} + +void feature_chain::reset() +{ + features2 = {}; + features2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; + v11 = {}; + v11.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_FEATURES; + v12 = {}; + v12.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES; + v13 = {}; + v13.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES; + v14 = {}; + v14.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_4_FEATURES; + + features2.pNext = &v11; + v11.pNext = &v12; + v12.pNext = &v13; + v13.pNext = &v14; + v14.pNext = nullptr; +} + +std::vector const &feature_table() +{ + return g_feature_table; +} + +void query_capabilities(VkPhysicalDevice Physical, feature_chain &Queried, capabilities &Caps) +{ + Caps = capabilities(); + + // extensions + std::uint32_t extensioncount{0}; + std::vector extensions; + if (VK14_VERIFY(vkEnumerateDeviceExtensionProperties(Physical, nullptr, &extensioncount, nullptr)) && (extensioncount > 0)) + { + extensions.resize(extensioncount); + if (!VK14_VERIFY(vkEnumerateDeviceExtensionProperties(Physical, nullptr, &extensioncount, extensions.data()))) + { + extensions.clear(); + } + } + auto const hasextension = [&extensions](char const *Name) { + return std::any_of(extensions.begin(), extensions.end(), [Name](VkExtensionProperties const &Entry) { return std::strcmp(Entry.extensionName, Name) == 0; }); + }; + + Caps.ext_swapchain = hasextension(VK_KHR_SWAPCHAIN_EXTENSION_NAME); + Caps.ext_memory_budget = hasextension(VK_EXT_MEMORY_BUDGET_EXTENSION_NAME); + Caps.ext_swapchain_maintenance1 = hasextension(VK_KHR_SWAPCHAIN_MAINTENANCE_1_EXTENSION_NAME) || hasextension(VK_EXT_SWAPCHAIN_MAINTENANCE_1_EXTENSION_NAME); + Caps.ext_extended_dynamic_state3 = hasextension(VK_EXT_EXTENDED_DYNAMIC_STATE_3_EXTENSION_NAME); + + // properties + VkPhysicalDeviceVulkan12Properties properties12{}; + properties12.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_PROPERTIES; + VkPhysicalDeviceProperties2 properties{}; + properties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2; + properties.pNext = &properties12; + vkGetPhysicalDeviceProperties2(Physical, &properties); + + Caps.device_name = properties.properties.deviceName; + Caps.device_type = properties.properties.deviceType; + Caps.api_version = properties.properties.apiVersion; + Caps.driver_version = properties.properties.driverVersion; + Caps.vendor_id = properties.properties.vendorID; + Caps.device_id = properties.properties.deviceID; + Caps.driver_name = properties12.driverName; + Caps.driver_info = properties12.driverInfo; + Caps.timestamp_period = properties.properties.limits.timestampPeriod; + + VkPhysicalDeviceMemoryProperties memory{}; + vkGetPhysicalDeviceMemoryProperties(Physical, &memory); + for (std::uint32_t i = 0; i < memory.memoryHeapCount; ++i) + { + if ((memory.memoryHeaps[i].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) != 0) + { + Caps.device_local_memory += memory.memoryHeaps[i].size; + } + } + + // the version first: the structures of 1.4 mean nothing to an older driver + if (Caps.api_version < VK_API_VERSION_1_4) + { + Caps.missing.push_back("the driver gives vulkan " + version_string(Caps.api_version) + ", the renderer needs 1.4"); + return; + } + + // features + Queried.reset(); + vkGetPhysicalDeviceFeatures2(Physical, Queried.head()); + + Caps.draw_indirect_count = (Queried.v12.drawIndirectCount == VK_TRUE); + Caps.host_image_copy = (Queried.v14.hostImageCopy == VK_TRUE); + Caps.fill_mode_non_solid = (Queried.features2.features.fillModeNonSolid == VK_TRUE); + Caps.pipeline_statistics_query = (Queried.features2.features.pipelineStatisticsQuery == VK_TRUE); + Caps.sampler_filter_minmax = (Queried.v12.samplerFilterMinmax == VK_TRUE); + Caps.shader_output_layer = (Queried.v12.shaderOutputLayer == VK_TRUE); + + // can the renderer run on it + if (!Caps.ext_swapchain) + { + Caps.missing.push_back(std::string(VK_KHR_SWAPCHAIN_EXTENSION_NAME) + " isn't supported, the device can't show images in a window"); + } + if (properties.properties.limits.timestampComputeAndGraphics == VK_FALSE) + { + Caps.missing.push_back("timestampComputeAndGraphics isn't supported (needed for the gpu times of the passes)"); + } + for (auto const &entry : g_feature_table) + { + if ((false == entry.required) || (entry.field(Queried) == VK_TRUE)) + { + continue; + } + auto message{std::string(entry.structure) + "::" + entry.name + " isn't supported (needed for " + entry.purpose + ")"}; + if (entry.guaranteed) + { + message += "; vulkan 1.4 requires it, the driver doesn't conform"; + } + Caps.missing.push_back(message); + } +} + +void build_enabled_features(feature_chain &Queried, feature_chain &Enabled) +{ + Enabled.reset(); + for (auto const &entry : g_feature_table) + { + if (entry.field(Queried) == VK_TRUE) + { + entry.field(Enabled) = VK_TRUE; + } + } +} + +std::string capability_report(capabilities const &Caps) +{ + std::string report{"gpu capabilities\n"}; + auto const line = [&report](std::string const &Text) { report += " " + Text + "\n"; }; + auto const flag = [](bool const Value) { return Value ? "yes" : "no"; }; + + line("device " + Caps.device_name + " (" + vendor_string(Caps.vendor_id) + ", " + device_type_string(Caps.device_type) + ")"); + line("api " + version_string(Caps.api_version)); + line("driver " + Caps.driver_name + " " + Caps.driver_info); + line("device memory " + memory_size_string(Caps.device_local_memory)); + line(std::string("draw indirect count ") + flag(Caps.draw_indirect_count)); + line(std::string("extended dynamic state3 ") + flag(Caps.ext_extended_dynamic_state3)); + line(std::string("memory budget ") + flag(Caps.ext_memory_budget)); + line(std::string("swapchain maintenance1 ") + flag(Caps.ext_swapchain_maintenance1)); + line(std::string("host image copy ") + flag(Caps.host_image_copy)); + line(std::string("sampler filter minmax ") + flag(Caps.sampler_filter_minmax)); + line(std::string("shader output layer ") + flag(Caps.shader_output_layer)); + line(std::string("fill mode non solid ") + flag(Caps.fill_mode_non_solid)); + line(std::string("pipeline statistics ") + flag(Caps.pipeline_statistics_query)); + + return report; +} + +} // namespace vk14 diff --git a/rendering/vulkan14/vulkan14capabilities.h b/rendering/vulkan14/vulkan14capabilities.h new file mode 100644 index 00000000..9c1dfdef --- /dev/null +++ b/rendering/vulkan14/vulkan14capabilities.h @@ -0,0 +1,108 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +// what a gpu can do, and whether the renderer can run on it. +// +// one table of features drives three things: what is asked of the driver, what is switched on when the device is made, and +// what a gpu is turned down for (by the name of the missing feature). the code of the renderer goes by the flags of +// vk14::capabilities, each optional one with a way without it + +#pragma once + +#include "rendering/vulkan14/vulkan14common.h" + +namespace vk14 +{ + +// the structures of the features the renderer cares about, in one object so a single pNext chain can go over them. +// not copyable: the chain points into the object itself +struct feature_chain +{ + VkPhysicalDeviceFeatures2 features2{}; + VkPhysicalDeviceVulkan11Features v11{}; + VkPhysicalDeviceVulkan12Features v12{}; + VkPhysicalDeviceVulkan13Features v13{}; + VkPhysicalDeviceVulkan14Features v14{}; + + feature_chain(); + feature_chain(feature_chain const &) = delete; + feature_chain &operator=(feature_chain const &) = delete; + + // every structure with its sType and no features, linked into the chain + void reset(); + + // head of the chain, for vkGetPhysicalDeviceFeatures2 or VkDeviceCreateInfo::pNext + VkPhysicalDeviceFeatures2 *head() + { + return &features2; + } +}; + +struct feature_entry +{ + char const *structure; + char const *name; + // the renderer doesn't run on a gpu without it + bool required; + // vulkan 1.4 requires it of every device, so a 1.4 device without it is a driver which doesn't conform + bool guaranteed; + // what it is for, for the log + char const *purpose; + VkBool32 &(*field)(feature_chain &); +}; + +std::vector const &feature_table(); + +struct capabilities +{ + // identification + std::string device_name; + std::string driver_name; + std::string driver_info; + VkPhysicalDeviceType device_type{VK_PHYSICAL_DEVICE_TYPE_OTHER}; + std::uint32_t api_version{0}; + std::uint32_t driver_version{0}; + std::uint32_t vendor_id{0}; + std::uint32_t device_id{0}; + VkDeviceSize device_local_memory{0}; + float timestamp_period{0.f}; // nanoseconds per tick + + // device extensions + bool ext_swapchain{false}; + bool ext_memory_budget{false}; + bool ext_swapchain_maintenance1{false}; // the khr one, or the ext one it came from + bool ext_extended_dynamic_state3{false}; + + // optional features, each with a way without it + bool draw_indirect_count{false}; + bool host_image_copy{false}; + bool fill_mode_non_solid{false}; + bool pipeline_statistics_query{false}; + bool sampler_filter_minmax{false}; + bool shader_output_layer{false}; + + // empty when the renderer can run on the device, otherwise one line per missing thing + std::vector missing; +}; + +// the features, properties and extensions of a gpu. Queried gets what the device reports; the device is made with the +// features switched on from it +void query_capabilities(VkPhysicalDevice Physical, feature_chain &Queried, capabilities &Caps); + +// the required features and the optional ones the device has, switched on +void build_enabled_features(feature_chain &Queried, feature_chain &Enabled); + +// the report for the log, several lines +std::string capability_report(capabilities const &Caps); + +// the kind of the gpu and its maker, for the log +char const *device_type_string(VkPhysicalDeviceType Type); +std::string vendor_string(std::uint32_t VendorId); + +} // namespace vk14 diff --git a/rendering/vulkan14/vulkan14common.cpp b/rendering/vulkan14/vulkan14common.cpp new file mode 100644 index 00000000..a8aa9ed8 --- /dev/null +++ b/rendering/vulkan14/vulkan14common.cpp @@ -0,0 +1,145 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "rendering/vulkan14/vulkan14common.h" +#include "utilities/Logs.h" + +namespace vk14 +{ + +char const *result_string(VkResult Result) +{ + switch (Result) + { + case VK_SUCCESS: + return "VK_SUCCESS"; + case VK_NOT_READY: + return "VK_NOT_READY"; + case VK_TIMEOUT: + return "VK_TIMEOUT"; + case VK_EVENT_SET: + return "VK_EVENT_SET"; + case VK_EVENT_RESET: + return "VK_EVENT_RESET"; + case VK_INCOMPLETE: + return "VK_INCOMPLETE"; + case VK_ERROR_OUT_OF_HOST_MEMORY: + return "VK_ERROR_OUT_OF_HOST_MEMORY"; + case VK_ERROR_OUT_OF_DEVICE_MEMORY: + return "VK_ERROR_OUT_OF_DEVICE_MEMORY"; + case VK_ERROR_INITIALIZATION_FAILED: + return "VK_ERROR_INITIALIZATION_FAILED"; + case VK_ERROR_DEVICE_LOST: + return "VK_ERROR_DEVICE_LOST"; + case VK_ERROR_MEMORY_MAP_FAILED: + return "VK_ERROR_MEMORY_MAP_FAILED"; + case VK_ERROR_LAYER_NOT_PRESENT: + return "VK_ERROR_LAYER_NOT_PRESENT"; + case VK_ERROR_EXTENSION_NOT_PRESENT: + return "VK_ERROR_EXTENSION_NOT_PRESENT"; + case VK_ERROR_FEATURE_NOT_PRESENT: + return "VK_ERROR_FEATURE_NOT_PRESENT"; + case VK_ERROR_INCOMPATIBLE_DRIVER: + return "VK_ERROR_INCOMPATIBLE_DRIVER"; + case VK_ERROR_TOO_MANY_OBJECTS: + return "VK_ERROR_TOO_MANY_OBJECTS"; + case VK_ERROR_FORMAT_NOT_SUPPORTED: + return "VK_ERROR_FORMAT_NOT_SUPPORTED"; + case VK_ERROR_FRAGMENTED_POOL: + return "VK_ERROR_FRAGMENTED_POOL"; + case VK_ERROR_UNKNOWN: + return "VK_ERROR_UNKNOWN"; + case VK_ERROR_OUT_OF_POOL_MEMORY: + return "VK_ERROR_OUT_OF_POOL_MEMORY"; + case VK_ERROR_INVALID_EXTERNAL_HANDLE: + return "VK_ERROR_INVALID_EXTERNAL_HANDLE"; + case VK_ERROR_FRAGMENTATION: + return "VK_ERROR_FRAGMENTATION"; + case VK_ERROR_INVALID_OPAQUE_CAPTURE_ADDRESS: + return "VK_ERROR_INVALID_OPAQUE_CAPTURE_ADDRESS"; + case VK_ERROR_SURFACE_LOST_KHR: + return "VK_ERROR_SURFACE_LOST_KHR"; + case VK_ERROR_NATIVE_WINDOW_IN_USE_KHR: + return "VK_ERROR_NATIVE_WINDOW_IN_USE_KHR"; + case VK_SUBOPTIMAL_KHR: + return "VK_SUBOPTIMAL_KHR"; + case VK_ERROR_OUT_OF_DATE_KHR: + return "VK_ERROR_OUT_OF_DATE_KHR"; + case VK_ERROR_INCOMPATIBLE_DISPLAY_KHR: + return "VK_ERROR_INCOMPATIBLE_DISPLAY_KHR"; + case VK_ERROR_VALIDATION_FAILED_EXT: + return "VK_ERROR_VALIDATION_FAILED_EXT"; + case VK_ERROR_FULL_SCREEN_EXCLUSIVE_MODE_LOST_EXT: + return "VK_ERROR_FULL_SCREEN_EXCLUSIVE_MODE_LOST_EXT"; + default: + break; + } + return "VK_ERROR_"; +} + +std::string version_string(std::uint32_t Version) +{ + return std::to_string(VK_API_VERSION_MAJOR(Version)) + "." + std::to_string(VK_API_VERSION_MINOR(Version)) + "." + std::to_string(VK_API_VERSION_PATCH(Version)); +} + +std::string memory_size_string(VkDeviceSize Bytes) +{ + auto const mib{static_cast(Bytes) / (1024.0 * 1024.0)}; + + char buffer[64] = {}; + if (mib >= 1024.0) + { + std::snprintf(buffer, sizeof(buffer), "%.2f GiB", mib / 1024.0); + } + else + { + std::snprintf(buffer, sizeof(buffer), "%.0f MiB", mib); + } + return std::string(buffer); +} + +bool verify_result(VkResult Result, char const *Call, char const *File, int Line) +{ + if (Result == VK_SUCCESS) + { + return true; + } + + // only the name of the file, the path of the build is noise in the log + char const *name{File}; + for (char const *c = File; *c != '\0'; ++c) + { + if ((*c == '/') || (*c == '\\')) + { + name = c + 1; + } + } + + log_error(std::string(Call) + " failed with " + result_string(Result) + " (" + name + ":" + std::to_string(Line) + ")"); + + return false; +} + +void log_info(std::string const &Message) +{ + WriteLog("vulkan: " + Message); +} + +void log_warning(std::string const &Message) +{ + WriteLog("vulkan: warning: " + Message); +} + +void log_error(std::string const &Message) +{ + ErrorLog("vulkan: " + Message); +} + +} // namespace vk14 diff --git a/rendering/vulkan14/vulkan14common.h b/rendering/vulkan14/vulkan14common.h new file mode 100644 index 00000000..0301bc39 --- /dev/null +++ b/rendering/vulkan14/vulkan14common.h @@ -0,0 +1,42 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +// the vulkan api of the vulkan 1.4 renderer, for the files of rendering/vulkan14 only: the rest of the program doesn't see +// the vulkan headers. the functions come from volk, which loads the vulkan loader when the program runs, so the program starts +// on a machine without vulkan too (and runs with opengl there) + +#pragma once + +#include + +// glfw declares its vulkan functions only when the vulkan headers come before its own, and stdafx.h brings glfw in first +extern "C" GLFWAPI VkResult glfwCreateWindowSurface(VkInstance instance, GLFWwindow *window, VkAllocationCallbacks const *allocator, VkSurfaceKHR *surface); +extern "C" GLFWAPI int glfwGetPhysicalDevicePresentationSupport(VkInstance instance, VkPhysicalDevice device, std::uint32_t queuefamily); + +namespace vk14 +{ + +// human readable VkResult, for the log +char const *result_string(VkResult Result); +// "1.4.309" +std::string version_string(std::uint32_t Version); +// bytes as MiB or GiB +std::string memory_size_string(VkDeviceSize Bytes); +// logs a failed call and gives back false; true for VK_SUCCESS +bool verify_result(VkResult Result, char const *Call, char const *File, int Line); + +// the log of the renderer, with the "vulkan: " prefix +void log_info(std::string const &Message); +void log_warning(std::string const &Message); +void log_error(std::string const &Message); + +} // namespace vk14 + +// true when the call succeeded; a failure is logged with the call and the place +#define VK14_VERIFY(expr) (::vk14::verify_result((expr), #expr, __FILE__, __LINE__)) diff --git a/rendering/vulkan14/vulkan14device.cpp b/rendering/vulkan14/vulkan14device.cpp new file mode 100644 index 00000000..2f0822b2 --- /dev/null +++ b/rendering/vulkan14/vulkan14device.cpp @@ -0,0 +1,328 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "rendering/vulkan14/vulkan14device.h" +#include "rendering/vulkan14/vulkan14instance.h" + +#include + +namespace vk14 +{ + +namespace +{ + +// "1" -> 1; anything which isn't a plain number -> -1 +int index_of(std::string const &Text) +{ + if (Text.empty() || (Text.size() > 3) || (false == std::all_of(Text.begin(), Text.end(), [](char const Char) { return std::isdigit(static_cast(Char)) != 0; }))) + { + return -1; + } + return std::stoi(Text); +} + +std::string lowercase(std::string Text) +{ + std::transform(Text.begin(), Text.end(), Text.begin(), [](unsigned char const Char) { return static_cast(std::tolower(Char)); }); + return Text; +} + +} // namespace + +device::~device() +{ + destroy(); +} + +queue_families device::find_queue_families(VkInstance Instance, VkPhysicalDevice Physical, VkSurfaceKHR Surface, std::uint32_t &TimestampBits) +{ + queue_families families; + TimestampBits = 0; + + std::uint32_t count{0}; + vkGetPhysicalDeviceQueueFamilyProperties(Physical, &count, nullptr); + std::vector properties(count); + vkGetPhysicalDeviceQueueFamilyProperties(Physical, &count, properties.data()); + + for (std::uint32_t i = 0; i < count; ++i) + { + if ((properties[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) == 0) + { + continue; + } + VkBool32 present{VK_FALSE}; + if (Surface != VK_NULL_HANDLE) + { + if (!VK14_VERIFY(vkGetPhysicalDeviceSurfaceSupportKHR(Physical, i, Surface, &present))) + { + present = VK_FALSE; + } + } + else + { + present = (glfwGetPhysicalDevicePresentationSupport(Instance, Physical, i) == GLFW_TRUE ? VK_TRUE : VK_FALSE); + } + if (present == VK_TRUE) + { + families.graphics = i; + TimestampBits = properties[i].timestampValidBits; + break; + } + } + if (families.graphics == queue_families::invalid) + { + return families; + } + + families.compute = families.graphics; + families.transfer = families.graphics; + // a compute family without graphics can work alongside the frame + for (std::uint32_t i = 0; i < count; ++i) + { + auto const flags{properties[i].queueFlags}; + if (((flags & VK_QUEUE_COMPUTE_BIT) != 0) && ((flags & VK_QUEUE_GRAPHICS_BIT) == 0)) + { + families.compute = i; + families.dedicated_compute = true; + break; + } + } + // a family of transfers only is usually the copy engine + for (std::uint32_t i = 0; i < count; ++i) + { + auto const flags{properties[i].queueFlags}; + if (((flags & VK_QUEUE_TRANSFER_BIT) != 0) && ((flags & (VK_QUEUE_GRAPHICS_BIT | VK_QUEUE_COMPUTE_BIT)) == 0)) + { + families.transfer = i; + families.dedicated_transfer = true; + break; + } + } + return families; +} + +std::uint64_t device::score(capabilities const &Caps) +{ + std::uint64_t score{0}; + switch (Caps.device_type) + { + case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU: + score = 3; + break; + case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU: + score = 2; + break; + case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU: + score = 1; + break; + default: + break; + } + // then the memory, in mebibytes + return (score << 40) + (Caps.device_local_memory >> 20); +} + +bool device::select(VkInstance Instance, VkSurfaceKHR Surface, std::string const &Wanted, bool const Verbose, std::string &Reason) +{ + m_physical = VK_NULL_HANDLE; + + std::uint32_t count{0}; + if (!VK14_VERIFY(vkEnumeratePhysicalDevices(Instance, &count, nullptr)) || (count == 0)) + { + Reason = "no vulkan device"; + return false; + } + std::vector devices(count); + if (!VK14_VERIFY(vkEnumeratePhysicalDevices(Instance, &count, devices.data()))) + { + Reason = "the vulkan devices can't be listed"; + return false; + } + + auto const wantedindex{index_of(Wanted)}; + auto const wantedname{lowercase(Wanted)}; + if (Verbose) + { + log_info(std::to_string(count) + " device(s)"); + } + + VkPhysicalDevice best{VK_NULL_HANDLE}; + VkPhysicalDevice chosen{VK_NULL_HANDLE}; + std::uint64_t bestscore{0}; + bool wantedfound{false}; + std::string firstmissing; + + for (std::uint32_t i = 0; i < count; ++i) + { + feature_chain queried; + capabilities caps; + query_capabilities(devices[i], queried, caps); + std::uint32_t timestampbits{0}; + auto const families{find_queue_families(Instance, devices[i], Surface, timestampbits)}; + if (families.graphics == queue_families::invalid) + { + caps.missing.push_back("no queue family both draws and shows images in a window"); + } + else if (timestampbits == 0) + { + caps.missing.push_back("the graphics queue has no timestamps (needed for the gpu times of the passes)"); + } + + auto const label{"device " + std::to_string(i) + " \"" + caps.device_name + "\" (" + device_type_string(caps.device_type) + ", vulkan " + version_string(caps.api_version) + ")"}; + auto const iswanted{(false == Wanted.empty()) && ((wantedindex >= 0) ? (wantedindex == static_cast(i)) : (lowercase(caps.device_name).find(wantedname) != std::string::npos))}; + wantedfound |= iswanted; + + if (false == caps.missing.empty()) + { + if (Verbose || iswanted) + { + log_warning(label + " can't be used:"); + for (auto const &reason : caps.missing) + { + log_warning(" " + reason); + } + } + if (firstmissing.empty()) + { + firstmissing = caps.device_name + ": " + caps.missing.front(); + } + continue; + } + if (Verbose) + { + log_info(label + " can be used"); + } + if (iswanted && (chosen == VK_NULL_HANDLE)) + { + chosen = devices[i]; + } + auto const devicescore{score(caps)}; + if ((best == VK_NULL_HANDLE) || (devicescore > bestscore)) + { + best = devices[i]; + bestscore = devicescore; + } + } + + if (Verbose && (false == Wanted.empty()) && (chosen == VK_NULL_HANDLE)) + { + log_warning("gfx.vulkan.device " + Wanted + (wantedfound ? " can't be used" : " matches no device") + ", picking the device by itself"); + } + if (chosen == VK_NULL_HANDLE) + { + chosen = best; + } + if (chosen == VK_NULL_HANDLE) + { + Reason = "no gpu has what the vulkan renderer needs (" + firstmissing + ")"; + return false; + } + + m_physical = chosen; + query_capabilities(m_physical, m_queried, m_capabilities); + std::uint32_t timestampbits{0}; + m_families = find_queue_families(Instance, m_physical, Surface, timestampbits); + return true; +} + +bool device::create(instance const &Instance, std::string &Reason) +{ + if (m_device != VK_NULL_HANDLE) + { + return true; + } + if (m_physical == VK_NULL_HANDLE) + { + Reason = "no gpu was picked"; + return false; + } + + feature_chain enabled; + build_enabled_features(m_queried, enabled); + + std::vector families{m_families.graphics}; + for (auto const family : {m_families.compute, m_families.transfer}) + { + if (std::find(families.begin(), families.end(), family) == families.end()) + { + families.push_back(family); + } + } + float const priority{1.f}; + std::vector queues; + for (auto const family : families) + { + VkDeviceQueueCreateInfo info{}; + info.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; + info.queueFamilyIndex = family; + info.queueCount = 1; + info.pQueuePriorities = &priority; + queues.push_back(info); + } + + // everything else the renderer uses is in the core of vulkan 1.4 + std::vector extensions{VK_KHR_SWAPCHAIN_EXTENSION_NAME}; + { + // a device of the portability subset (vulkan on metal) has to be told the program knows it + std::uint32_t count{0}; + vkEnumerateDeviceExtensionProperties(m_physical, nullptr, &count, nullptr); + std::vector available(count); + vkEnumerateDeviceExtensionProperties(m_physical, nullptr, &count, available.data()); + m_portability_subset = std::any_of(available.begin(), available.end(), [](VkExtensionProperties const &Entry) { return std::strcmp(Entry.extensionName, "VK_KHR_portability_subset") == 0; }); + if (m_portability_subset) + { + extensions.push_back("VK_KHR_portability_subset"); + } + } + + VkDeviceCreateInfo create{}; + create.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; + create.pNext = enabled.head(); + create.queueCreateInfoCount = static_cast(queues.size()); + create.pQueueCreateInfos = queues.data(); + create.enabledExtensionCount = static_cast(extensions.size()); + create.ppEnabledExtensionNames = extensions.data(); + + auto const result{vkCreateDevice(m_physical, &create, nullptr, &m_device)}; + if (result != VK_SUCCESS) + { + m_device = VK_NULL_HANDLE; + Reason = std::string("vkCreateDevice failed with ") + result_string(result); + return false; + } + // the functions of the device straight from the driver, without the dispatch of the loader + volkLoadDevice(m_device); + + vkGetDeviceQueue(m_device, m_families.graphics, 0, &m_graphics_queue); + vkGetDeviceQueue(m_device, m_families.compute, 0, &m_compute_queue); + vkGetDeviceQueue(m_device, m_families.transfer, 0, &m_transfer_queue); + + Instance.set_object_name(m_device, VK_OBJECT_TYPE_DEVICE, reinterpret_cast(m_device), "eu07 device"); + Instance.set_object_name(m_device, VK_OBJECT_TYPE_QUEUE, reinterpret_cast(m_graphics_queue), "graphics queue"); + + log_info("using " + m_capabilities.device_name + ", queue families: graphics " + std::to_string(m_families.graphics) + ", compute " + std::to_string(m_families.compute) + (m_families.dedicated_compute ? " (own)" : " (shared)") + ", transfer " + + std::to_string(m_families.transfer) + (m_families.dedicated_transfer ? " (own)" : " (shared)")); + return true; +} + +void device::destroy() +{ + if (m_device != VK_NULL_HANDLE) + { + vkDestroyDevice(m_device, nullptr); + m_device = VK_NULL_HANDLE; + } + m_graphics_queue = VK_NULL_HANDLE; + m_compute_queue = VK_NULL_HANDLE; + m_transfer_queue = VK_NULL_HANDLE; +} + +} // namespace vk14 diff --git a/rendering/vulkan14/vulkan14device.h b/rendering/vulkan14/vulkan14device.h new file mode 100644 index 00000000..17a9e642 --- /dev/null +++ b/rendering/vulkan14/vulkan14device.h @@ -0,0 +1,91 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +// the gpu the renderer runs on, and the VkDevice with its queues. +// +// the gpu is the one named by gfx.vulkan.device (its index in the list of the log, or a part of its name), or else the best +// usable one: discrete before integrated, then the one with more memory. a gpu missing something the renderer needs is +// turned down with the name of what it lacks + +#pragma once + +#include "rendering/vulkan14/vulkan14common.h" +#include "rendering/vulkan14/vulkan14capabilities.h" + +namespace vk14 +{ + +class instance; + +struct queue_families +{ + static constexpr std::uint32_t invalid{VK_QUEUE_FAMILY_IGNORED}; + + // graphics and presentation, from one family (every desktop driver has one) + std::uint32_t graphics{invalid}; + // asynchronous compute and transfers; the graphics family where there's no family of their own + std::uint32_t compute{invalid}; + std::uint32_t transfer{invalid}; + bool dedicated_compute{false}; + bool dedicated_transfer{false}; +}; + +class device +{ + public: + device() = default; + device(device const &) = delete; + device &operator=(device const &) = delete; + ~device(); + + // picks the gpu. Surface: the window to show images in, null before there's one (the presentation is then asked of + // glfw). Verbose: every gpu and why it can't be used goes to the log. false: no usable gpu, Reason tells why + bool select(VkInstance Instance, VkSurfaceKHR Surface, std::string const &Wanted, bool const Verbose, std::string &Reason); + // makes the VkDevice of the gpu picked by select() + bool create(instance const &Instance, std::string &Reason); + void destroy(); + + VkPhysicalDevice physical() const + { + return m_physical; + } + VkDevice handle() const + { + return m_device; + } + capabilities const &caps() const + { + return m_capabilities; + } + queue_families const &families() const + { + return m_families; + } + VkQueue graphics_queue() const + { + return m_graphics_queue; + } + + private: + static queue_families find_queue_families(VkInstance Instance, VkPhysicalDevice Physical, VkSurfaceKHR Surface, std::uint32_t &TimestampBits); + static std::uint64_t score(capabilities const &Caps); + + VkPhysicalDevice m_physical{VK_NULL_HANDLE}; + VkDevice m_device{VK_NULL_HANDLE}; + capabilities m_capabilities; + queue_families m_families; + feature_chain m_queried; + bool m_portability_subset{false}; + + VkQueue m_graphics_queue{VK_NULL_HANDLE}; + VkQueue m_compute_queue{VK_NULL_HANDLE}; + VkQueue m_transfer_queue{VK_NULL_HANDLE}; +}; + +} // namespace vk14 diff --git a/rendering/vulkan14/vulkan14instance.cpp b/rendering/vulkan14/vulkan14instance.cpp new file mode 100644 index 00000000..b5ff6cd9 --- /dev/null +++ b/rendering/vulkan14/vulkan14instance.cpp @@ -0,0 +1,342 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "rendering/vulkan14/vulkan14instance.h" + +#include + +namespace vk14 +{ + +namespace +{ + +char const *const validation_layer_name = "VK_LAYER_KHRONOS_validation"; + +bool contains(std::vector const &List, char const *Name) +{ + return std::any_of(List.begin(), List.end(), [Name](char const *Entry) { return std::strcmp(Entry, Name) == 0; }); +} + +std::vector instance_extensions(char const *Layer) +{ + std::uint32_t count{0}; + std::vector extensions; + if (VK14_VERIFY(vkEnumerateInstanceExtensionProperties(Layer, &count, nullptr)) && (count > 0)) + { + extensions.resize(count); + if (!VK14_VERIFY(vkEnumerateInstanceExtensionProperties(Layer, &count, extensions.data()))) + { + extensions.clear(); + } + } + return extensions; +} + +bool has_extension(std::vector const &Extensions, char const *Name) +{ + return std::any_of(Extensions.begin(), Extensions.end(), [Name](VkExtensionProperties const &Entry) { return std::strcmp(Entry.extensionName, Name) == 0; }); +} + +VKAPI_ATTR VkBool32 VKAPI_CALL debug_callback(VkDebugUtilsMessageSeverityFlagBitsEXT Severity, VkDebugUtilsMessageTypeFlagsEXT Types, VkDebugUtilsMessengerCallbackDataEXT const *Data, void * /*UserData*/) +{ + std::string message{(Types & VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT) != 0 ? "validation" : (Types & VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT) != 0 ? "performance" : "general"}; + if ((Data != nullptr) && (Data->pMessageIdName != nullptr)) + { + message += " ["; + message += Data->pMessageIdName; + message += "]"; + } + message += ": "; + message += ((Data != nullptr) && (Data->pMessage != nullptr)) ? Data->pMessage : ""; + + if ((Severity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) != 0) + { + log_error(message); + } + else if ((Severity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) != 0) + { + log_warning(message); + } + else + { + log_info(message); + } + // anything else is for the development of layers + return VK_FALSE; +} + +void fill_messenger_info(VkDebugUtilsMessengerCreateInfoEXT &Info) +{ + Info = {}; + Info.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT; + Info.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT; + Info.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT; + Info.pfnUserCallback = &debug_callback; +} + +} // namespace + +bool load_vulkan(std::string &Reason) +{ + // the loader is loaded once for the run; volk keeps it + static VkResult const loaded{volkInitialize()}; + if (loaded != VK_SUCCESS) + { + Reason = "no vulkan loader (vulkan-1.dll / libvulkan.so.1) on the machine; the graphics driver has no vulkan"; + return false; + } + if (glfwVulkanSupported() == GLFW_FALSE) + { + Reason = "glfw finds no vulkan loader with the extensions to show images in a window"; + return false; + } + + auto const loaderversion{volkGetInstanceVersion()}; + // an instance of vulkan 1.4 needs a loader of 1.1 or newer; the device has to give 1.4 itself + if (loaderversion < VK_API_VERSION_1_1) + { + Reason = "the vulkan loader is of version " + version_string(loaderversion) + ", update the graphics driver"; + return false; + } + return true; +} + +instance::~instance() +{ + destroy(); +} + +bool instance::create(instance_config const &Config, std::string &Reason) +{ + if (m_instance != VK_NULL_HANDLE) + { + return true; + } + if (false == load_vulkan(Reason)) + { + return false; + } + + std::vector layers; + std::vector extensions; + if (false == collect_layers(layers, Config)) + { + Reason = "the validation layer can't be listed"; + return false; + } + if (false == collect_extensions(extensions, Config, Reason)) + { + return false; + } + + VkApplicationInfo application{}; + application.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; + application.pApplicationName = "MaSzyna"; + application.applicationVersion = VK_MAKE_API_VERSION(0, 1, 0, 0); + application.pEngineName = "MaSzyna vulkan14"; + application.engineVersion = VK_MAKE_API_VERSION(0, 1, 0, 0); + application.apiVersion = VK_API_VERSION_1_4; + + VkInstanceCreateInfo create{}; + create.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; + create.pApplicationInfo = &application; +#ifdef __APPLE__ + // vulkan on metal (moltenvk, kosmickrisp) is listed only to the programs which ask for the devices of the portability subset + if (contains(extensions, VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME)) + { + create.flags |= VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR; + } +#endif + create.enabledLayerCount = static_cast(layers.size()); + create.ppEnabledLayerNames = layers.empty() ? nullptr : layers.data(); + create.enabledExtensionCount = static_cast(extensions.size()); + create.ppEnabledExtensionNames = extensions.empty() ? nullptr : extensions.data(); + + void const **tail{&create.pNext}; + // in the chain the messenger sees the creation and the destruction of the instance too + VkDebugUtilsMessengerCreateInfoEXT messenger{}; + if (m_debug_utils_enabled) + { + fill_messenger_info(messenger); + *tail = &messenger; + tail = &messenger.pNext; + } + // synchronization validation checks the barriers; the gpu-assisted one the accesses of the shaders + std::vector validationfeatures; + VkValidationFeaturesEXT validation{}; + if (m_validation_enabled && contains(extensions, VK_EXT_VALIDATION_FEATURES_EXTENSION_NAME)) + { + validationfeatures.push_back(VK_VALIDATION_FEATURE_ENABLE_SYNCHRONIZATION_VALIDATION_EXT); + if (Config.validation >= 2) + { + validationfeatures.push_back(VK_VALIDATION_FEATURE_ENABLE_GPU_ASSISTED_EXT); + } + validation.sType = VK_STRUCTURE_TYPE_VALIDATION_FEATURES_EXT; + validation.enabledValidationFeatureCount = static_cast(validationfeatures.size()); + validation.pEnabledValidationFeatures = validationfeatures.data(); + *tail = &validation; + tail = &validation.pNext; + } + + auto const result{vkCreateInstance(&create, nullptr, &m_instance)}; + if (result != VK_SUCCESS) + { + m_instance = VK_NULL_HANDLE; + Reason = (result == VK_ERROR_INCOMPATIBLE_DRIVER ? std::string("no installed driver gives vulkan 1.4 (vkCreateInstance: VK_ERROR_INCOMPATIBLE_DRIVER)") : std::string("vkCreateInstance failed with ") + result_string(result)); + return false; + } + // the functions of the instance; those of the device come from it directly, once it's made + volkLoadInstanceOnly(m_instance); + + if (m_debug_utils_enabled) + { + VkDebugUtilsMessengerCreateInfoEXT info{}; + fill_messenger_info(info); + if ((vkCreateDebugUtilsMessengerEXT == nullptr) || !VK14_VERIFY(vkCreateDebugUtilsMessengerEXT(m_instance, &info, nullptr, &m_messenger))) + { + // the renderer works, only the messages of the validation layer don't reach the log + m_messenger = VK_NULL_HANDLE; + log_warning("the messages of the validation layer won't reach the log"); + } + } + + return true; +} + +bool instance::collect_layers(std::vector &Layers, instance_config const &Config) +{ + Layers.clear(); + m_validation_enabled = false; + if (Config.validation <= 0) + { + return true; + } + + std::uint32_t count{0}; + if (!VK14_VERIFY(vkEnumerateInstanceLayerProperties(&count, nullptr))) + { + return false; + } + std::vector available(count); + if ((count > 0) && !VK14_VERIFY(vkEnumerateInstanceLayerProperties(&count, available.data()))) + { + return false; + } + + if (std::any_of(available.begin(), available.end(), [](VkLayerProperties const &Layer) { return std::strcmp(Layer.layerName, validation_layer_name) == 0; })) + { + Layers.push_back(validation_layer_name); + m_validation_enabled = true; + log_info(std::string("validation layer on") + (Config.validation >= 2 ? " (with gpu-assisted validation)" : "")); + } + else + { + // gfx.vulkan.validation doesn't stop the renderer on a machine without the vulkan sdk + log_warning(std::string(validation_layer_name) + " isn't installed (vulkan sdk), running without validation"); + } + return true; +} + +bool instance::collect_extensions(std::vector &Extensions, instance_config const &Config, std::string &Reason) +{ + Extensions.clear(); + m_debug_utils_enabled = false; + + std::uint32_t glfwcount{0}; + auto const **glfwextensions{glfwGetRequiredInstanceExtensions(&glfwcount)}; + if ((glfwextensions == nullptr) || (glfwcount == 0)) + { + Reason = "no vulkan driver gives the extensions to show images in a window"; + return false; + } + for (std::uint32_t i = 0; i < glfwcount; ++i) + { + Extensions.push_back(glfwextensions[i]); + } + + auto const available{instance_extensions(nullptr)}; + for (auto const *name : Extensions) + { + if (false == has_extension(available, name)) + { + Reason = std::string("the instance extension ") + name + " to show images in a window isn't available"; + return false; + } + } + + if (Config.debug_utils || m_validation_enabled) + { + if (has_extension(available, VK_EXT_DEBUG_UTILS_EXTENSION_NAME)) + { + if (false == contains(Extensions, VK_EXT_DEBUG_UTILS_EXTENSION_NAME)) + { + Extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME); + } + m_debug_utils_enabled = true; + } + else + { + log_warning(std::string(VK_EXT_DEBUG_UTILS_EXTENSION_NAME) + " isn't available, no names of objects and no messages of the validation layer"); + } + } +#ifdef __APPLE__ + if (has_extension(available, VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME)) + { + Extensions.push_back(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME); + } +#endif + if (m_validation_enabled && has_extension(instance_extensions(validation_layer_name), VK_EXT_VALIDATION_FEATURES_EXTENSION_NAME)) + { + Extensions.push_back(VK_EXT_VALIDATION_FEATURES_EXTENSION_NAME); + } + + std::string list; + for (auto const *name : Extensions) + { + list += (list.empty() ? "" : ", "); + list += name; + } + log_info("instance extensions: " + list); + + return true; +} + +void instance::set_object_name(VkDevice Device, VkObjectType Type, std::uint64_t Handle, std::string const &Name) const +{ + if ((false == m_debug_utils_enabled) || (vkSetDebugUtilsObjectNameEXT == nullptr) || (Device == VK_NULL_HANDLE) || (Handle == 0)) + { + return; + } + VkDebugUtilsObjectNameInfoEXT info{}; + info.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT; + info.objectType = Type; + info.objectHandle = Handle; + info.pObjectName = Name.c_str(); + vkSetDebugUtilsObjectNameEXT(Device, &info); +} + +void instance::destroy() +{ + if ((m_messenger != VK_NULL_HANDLE) && (vkDestroyDebugUtilsMessengerEXT != nullptr)) + { + vkDestroyDebugUtilsMessengerEXT(m_instance, m_messenger, nullptr); + } + m_messenger = VK_NULL_HANDLE; + if (m_instance != VK_NULL_HANDLE) + { + vkDestroyInstance(m_instance, nullptr); + m_instance = VK_NULL_HANDLE; + } + m_validation_enabled = false; + m_debug_utils_enabled = false; +} + +} // namespace vk14 diff --git a/rendering/vulkan14/vulkan14instance.h b/rendering/vulkan14/vulkan14instance.h new file mode 100644 index 00000000..812961b5 --- /dev/null +++ b/rendering/vulkan14/vulkan14instance.h @@ -0,0 +1,64 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +// the vulkan loader, loaded when the program runs, and the VkInstance: vulkan 1.4, the extensions glfw needs to show images +// in a window and, with gfx.vulkan.validation, the khronos validation layer with its messages passed to the log + +#pragma once + +#include "rendering/vulkan14/vulkan14common.h" + +namespace vk14 +{ + +// loads the vulkan loader (once). false: there's no vulkan on the machine, Reason tells what is missing +bool load_vulkan(std::string &Reason); + +struct instance_config +{ + // 0: none, 1: VK_LAYER_KHRONOS_validation, 2: also its gpu-assisted validation + int validation{0}; + // VK_EXT_debug_utils: the messages of the validation layer in the log, names of objects + bool debug_utils{false}; +}; + +class instance +{ + public: + instance() = default; + instance(instance const &) = delete; + instance &operator=(instance const &) = delete; + ~instance(); + + // false: Reason tells why + bool create(instance_config const &Config, std::string &Reason); + void destroy(); + + VkInstance handle() const + { + return m_instance; + } + bool validation_enabled() const + { + return m_validation_enabled; + } + // a name of a vulkan object, for the messages of the validation layer and for captures. nothing without debug utils + void set_object_name(VkDevice Device, VkObjectType Type, std::uint64_t Handle, std::string const &Name) const; + + private: + bool collect_layers(std::vector &Layers, instance_config const &Config); + bool collect_extensions(std::vector &Extensions, instance_config const &Config, std::string &Reason); + + VkInstance m_instance{VK_NULL_HANDLE}; + VkDebugUtilsMessengerEXT m_messenger{VK_NULL_HANDLE}; + bool m_validation_enabled{false}; + bool m_debug_utils_enabled{false}; +}; + +} // namespace vk14 diff --git a/rendering/vulkan14/vulkan14renderer.cpp b/rendering/vulkan14/vulkan14renderer.cpp new file mode 100644 index 00000000..24e9b478 --- /dev/null +++ b/rendering/vulkan14/vulkan14renderer.cpp @@ -0,0 +1,628 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "rendering/vulkan14/vulkan14renderer.h" +#include "rendering/vulkan14/vulkan14common.h" +#include "rendering/vulkan14/vulkan14instance.h" +#include "rendering/vulkan14/vulkan14device.h" +#include "rendering/vulkan14/vulkan14swapchain.h" +#include "rendering/nullrenderer.h" +#include "application/application.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "utilities/Timer.h" +#include "utilities/utilities.h" +#include "imgui/imgui.h" + +namespace +{ + +// the colour the windows are cleared to, the initial background of the opengl renderer (srgb) +glm::vec3 const clear_colour{51.f / 255.f, 102.f / 255.f, 85.f / 255.f}; + +float srgb_to_linear(float const Value) +{ + return (Value <= 0.04045f ? Value / 12.92f : std::pow((Value + 0.055f) / 1.055f, 2.4f)); +} + +} // namespace + +// the objects of the vulkan api. destroyed in the reverse order: the swapchains and the frames, then the device, then the instance +struct vulkan14_renderer::context +{ + struct frame + { + VkCommandPool pool{VK_NULL_HANDLE}; + VkCommandBuffer commands{VK_NULL_HANDLE}; + // the value of the timeline semaphore the gpu reaches when it's done with the frame; 0: never submitted + std::uint64_t done{0}; + }; + + vk14::instance instance; + vk14::device device; + // [0]: the main window, then the windows of the extra viewports + std::vector> windows; + std::vector frames; + // one semaphore counts the frames done by the gpu; the binary ones are only between the swapchains and the queue + VkSemaphore timeline{VK_NULL_HANDLE}; + std::uint64_t timelinevalue{0}; + // frames recorded since the start + std::uint64_t framecount{0}; + // the windows with an image acquired in the frame being made + std::vector acquired; + bool devicelost{false}; + + ~context() + { + destroy(); + } + + bool create_frames(std::uint32_t const Count, std::string &Reason) + { + auto const handle{device.handle()}; + + VkSemaphoreTypeCreateInfo timelineinfo{}; + timelineinfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO; + timelineinfo.semaphoreType = VK_SEMAPHORE_TYPE_TIMELINE; + timelineinfo.initialValue = 0; + VkSemaphoreCreateInfo semaphoreinfo{}; + semaphoreinfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; + semaphoreinfo.pNext = &timelineinfo; + if (!VK14_VERIFY(vkCreateSemaphore(handle, &semaphoreinfo, nullptr, &timeline))) + { + Reason = "no timeline semaphore"; + return false; + } + instance.set_object_name(handle, VK_OBJECT_TYPE_SEMAPHORE, reinterpret_cast(timeline), "frame timeline"); + + frames.resize(Count); + for (std::uint32_t i = 0; i < Count; ++i) + { + VkCommandPoolCreateInfo poolinfo{}; + poolinfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; + poolinfo.flags = VK_COMMAND_POOL_CREATE_TRANSIENT_BIT; + poolinfo.queueFamilyIndex = device.families().graphics; + if (!VK14_VERIFY(vkCreateCommandPool(handle, &poolinfo, nullptr, &frames[i].pool))) + { + Reason = "no command pool"; + return false; + } + VkCommandBufferAllocateInfo bufferinfo{}; + bufferinfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; + bufferinfo.commandPool = frames[i].pool; + bufferinfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + bufferinfo.commandBufferCount = 1; + if (!VK14_VERIFY(vkAllocateCommandBuffers(handle, &bufferinfo, &frames[i].commands))) + { + Reason = "no command buffer"; + return false; + } + instance.set_object_name(handle, VK_OBJECT_TYPE_COMMAND_BUFFER, reinterpret_cast(frames[i].commands), "frame " + std::to_string(i)); + } + return true; + } + + void destroy() + { + auto const handle{device.handle()}; + if (handle != VK_NULL_HANDLE) + { + // the end of the renderer, not a frame: everything on the gpu has to be done + vkDeviceWaitIdle(handle); + } + acquired.clear(); + windows.clear(); + if (handle != VK_NULL_HANDLE) + { + for (auto &frame : frames) + { + if (frame.pool != VK_NULL_HANDLE) + { + vkDestroyCommandPool(handle, frame.pool, nullptr); + } + } + if (timeline != VK_NULL_HANDLE) + { + vkDestroySemaphore(handle, timeline, nullptr); + } + } + frames.clear(); + timeline = VK_NULL_HANDLE; + device.destroy(); + instance.destroy(); + } +}; + +namespace +{ + +// whether the renderer can run on the machine, before there is a window: the loader, an instance of vulkan 1.4 and a gpu +// with everything the renderer needs +bool probe(std::string &Reason) +{ + vk14::instance instance; + if (false == instance.create(vk14::instance_config{}, Reason)) + { + return false; + } + vk14::device device; + if (false == device.select(instance.handle(), VK_NULL_HANDLE, Global.gfx_vulkan_device, true, Reason)) + { + return false; + } + WriteLog(vk14::capability_report(device.caps())); + return true; +} + +void window_hints() +{ + // the swapchain shows the images; a window with an opengl context couldn't be given to vulkan + glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); +} + +void window_failed(std::string const &Reason) +{ + vk14::log_error("the window couldn't be made (" + Reason + ")"); +} + +} // namespace + +vulkan14_renderer::vulkan14_renderer() = default; + +vulkan14_renderer::~vulkan14_renderer() +{ + Shutdown(); +} + +std::unique_ptr vulkan14_renderer::create_func() +{ + return std::make_unique(); +} + +gfx_backend_desc vulkan14_renderer::backend_desc() +{ + gfx_backend_desc desc; + desc.name = "vulkan 1.4"; + desc.create = &vulkan14_renderer::create_func; + desc.probe = &probe; + desc.window_hints = &window_hints; + desc.window_failed = &window_failed; + desc.gl_context = false; + return desc; +} + +bool vulkan14_renderer::renderer_register = gfx_renderer_factory::get_instance()->register_backend("vulkan", vulkan14_renderer::backend_desc()); + +bool vulkan14_renderer::Init(GLFWwindow *Window) +{ + m_context = std::make_unique(); + auto &ctx{*m_context}; + + std::string reason; + vk14::instance_config config; + config.validation = Global.gfx_vulkan_validation; + config.debug_utils = (Global.gfx_vulkan_validation > 0); + + auto const framesinflight{static_cast(std::clamp(Global.gfx_vulkan_framesinflight, 2, 3))}; + auto ok{ctx.instance.create(config, reason) && ctx.device.select(ctx.instance.handle(), VK_NULL_HANDLE, Global.gfx_vulkan_device, false, reason) && ctx.device.create(ctx.instance, reason) && ctx.create_frames(framesinflight, reason)}; + if (ok) + { + auto mainwindow{std::make_unique()}; + ok = mainwindow->create(ctx.instance, ctx.device, Window, "main window", framesinflight, reason); + ctx.windows.emplace_back(std::move(mainwindow)); + } + if (false == ok) + { + vk14::log_error("the renderer can't start: " + reason); + m_context.reset(); + return false; + } + + vk14::log_info(std::to_string(framesinflight) + " frames in flight"); + // until the stages of the plan bring them, these parts of the simulator aren't there with vulkan + vk14::log_info("not available with vulkan yet: the scenery and the vehicles (D1-D7), the user interface (R5), picking, mouse position in the scene and screenshots (D6), " + "python screens in their own windows (N5), panels in their own windows (R5b), VR (X4)"); + WriteLog("Gfx Renderer: setup complete"); + update_info(); + return true; +} + +bool vulkan14_renderer::AddViewport(global_settings::extraviewport_config const &conf) +{ + if (!m_context) + { + return false; + } + if (conf.monitor == "MAIN") + { + // the projection of the main window is changed only, which the drawing of the scenery takes care of (D6) + vk14::log_info("extra viewport on the main window: drawn from the stage D6 of the plan"); + return true; + } + + // a window of its own, without an opengl context + auto *window{Application.window(-1, true, conf.width, conf.height, Application.find_monitor(conf.monitor), true, false)}; + if (window == nullptr) + { + vk14::log_error("the window of the extra viewport on " + conf.monitor + " couldn't be made"); + return false; + } + auto &ctx{*m_context}; + auto viewport{std::make_unique()}; + std::string reason; + if (false == viewport->create(ctx.instance, ctx.device, window, "viewport " + conf.monitor, static_cast(ctx.frames.size()), reason)) + { + vk14::log_error(reason); + return false; + } + ctx.windows.emplace_back(std::move(viewport)); + return true; +} + +void vulkan14_renderer::Shutdown() +{ + m_context.reset(); +} + +bool vulkan14_renderer::Render() +{ + if (!m_context || m_context->devicelost) + { + return false; + } + auto &ctx{*m_context}; + auto const device{ctx.device.handle()}; + auto const slot{static_cast(ctx.framecount % ctx.frames.size())}; + auto &frame{ctx.frames[slot]}; + + Timer::subsystem.gfx_total.start(); + + // the gpu has to be done with the frame which used the slot before + if (frame.done > 0) + { + VkSemaphoreWaitInfo wait{}; + wait.sType = VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO; + wait.semaphoreCount = 1; + wait.pSemaphores = &ctx.timeline; + wait.pValues = &frame.done; + if (!VK14_VERIFY(vkWaitSemaphores(device, &wait, UINT64_MAX))) + { + ctx.devicelost = true; + return false; + } + } + VK14_VERIFY(vkResetCommandPool(device, frame.pool, 0)); + + ctx.acquired.clear(); + for (auto &window : ctx.windows) + { + switch (window->acquire(slot)) + { + case vk14::swapchain::acquire_result::ready: + ctx.acquired.push_back(window.get()); + break; + case vk14::swapchain::acquire_result::skipped: + break; + default: + ctx.devicelost = true; + return false; + } + } + + // the frames of the user interface go on (its input works too), though nothing of it is drawn yet + Timer::subsystem.gfx_gui.start(); + Application.render_ui(); + Timer::subsystem.gfx_gui.stop(); + + VkCommandBufferBeginInfo begin{}; + begin.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + VK14_VERIFY(vkBeginCommandBuffer(frame.commands, &begin)); + for (auto *window : ctx.acquired) + { + auto const colour{window->srgb() ? glm::vec3{srgb_to_linear(clear_colour.r), srgb_to_linear(clear_colour.g), srgb_to_linear(clear_colour.b)} : clear_colour}; + window->record_clear(frame.commands, glm::vec4{colour, 1.f}); + } + VK14_VERIFY(vkEndCommandBuffer(frame.commands)); + + std::vector waits; + std::vector signals; + for (auto *window : ctx.acquired) + { + VkSemaphoreSubmitInfo wait{}; + wait.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO; + wait.semaphore = window->acquire_semaphore(slot); + wait.stageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT; + waits.push_back(wait); + + VkSemaphoreSubmitInfo signal{}; + signal.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO; + signal.semaphore = window->present_semaphore(); + signal.stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT; + signals.push_back(signal); + } + VkSemaphoreSubmitInfo timelinesignal{}; + timelinesignal.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO; + timelinesignal.semaphore = ctx.timeline; + timelinesignal.value = ctx.timelinevalue + 1; + timelinesignal.stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT; + signals.push_back(timelinesignal); + + VkCommandBufferSubmitInfo commands{}; + commands.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO; + commands.commandBuffer = frame.commands; + + VkSubmitInfo2 submit{}; + submit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2; + submit.waitSemaphoreInfoCount = static_cast(waits.size()); + submit.pWaitSemaphoreInfos = waits.data(); + submit.commandBufferInfoCount = 1; + submit.pCommandBufferInfos = &commands; + submit.signalSemaphoreInfoCount = static_cast(signals.size()); + submit.pSignalSemaphoreInfos = signals.data(); + if (!VK14_VERIFY(vkQueueSubmit2(ctx.device.graphics_queue(), 1, &submit, VK_NULL_HANDLE))) + { + ctx.devicelost = true; + return false; + } + frame.done = ++ctx.timelinevalue; + + Timer::subsystem.gfx_total.stop(); + return true; +} + +void vulkan14_renderer::SwapBuffers() +{ + if (!m_context) + { + return; + } + auto &ctx{*m_context}; + Timer::subsystem.gfx_swap.start(); + + if (false == ctx.acquired.empty()) + { + std::vector waits; + std::vector swapchains; + std::vector images; + std::vector results(ctx.acquired.size(), VK_SUCCESS); + for (auto *window : ctx.acquired) + { + waits.push_back(window->present_semaphore()); + swapchains.push_back(window->handle()); + images.push_back(window->image_index()); + } + VkPresentInfoKHR present{}; + present.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; + present.waitSemaphoreCount = static_cast(waits.size()); + present.pWaitSemaphores = waits.data(); + present.swapchainCount = static_cast(swapchains.size()); + present.pSwapchains = swapchains.data(); + present.pImageIndices = images.data(); + present.pResults = results.data(); + auto const result{vkQueuePresentKHR(ctx.device.graphics_queue(), &present)}; + for (std::size_t i = 0; i < ctx.acquired.size(); ++i) + { + ctx.acquired[i]->presented(results[i]); + } + if ((result == VK_ERROR_DEVICE_LOST) || (result == VK_ERROR_SURFACE_LOST_KHR)) + { + vk14::log_error(std::string("vkQueuePresentKHR failed with ") + vk14::result_string(result)); + ctx.devicelost = true; + } + ctx.acquired.clear(); + } + ++ctx.framecount; + + Timer::subsystem.gfx_swap.stop(); +} + +float vulkan14_renderer::Framerate() +{ + return m_framerate; +} + +// geometry: kept in memory for the simulation, nothing on the gpu until D1 + +gfx::geometrybank_handle vulkan14_renderer::Create_Bank() +{ + return m_geometry.register_bank(std::make_unique()); +} + +gfx::geometry_handle vulkan14_renderer::Insert(gfx::index_array &Indices, gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometrybank_handle const &Geometry, int const Type) +{ + // NOTE: we expect indexed geometry to come with calculated tangents + return m_geometry.create_chunk(Indices, Vertices, Userdata, Geometry, Type); +} + +gfx::geometry_handle vulkan14_renderer::Insert(gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometrybank_handle const &Geometry, int const Type) +{ + gfx::calculate_tangents(Vertices, gfx::index_array(), Type); + return m_geometry.create_chunk(Vertices, Userdata, Geometry, Type); +} + +bool vulkan14_renderer::Replace(gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry, int const Type, std::size_t const Offset) +{ + gfx::calculate_tangents(Vertices, gfx::index_array(), Type); + return m_geometry.replace(Vertices, Userdata, Geometry, Offset); +} + +bool vulkan14_renderer::Append(gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry, int const Type) +{ + gfx::calculate_tangents(Vertices, gfx::index_array(), Type); + return m_geometry.append(Vertices, Userdata, Geometry); +} + +gfx::index_array const &vulkan14_renderer::Indices(gfx::geometry_handle const &Geometry) const +{ + return m_geometry.indices(Geometry); +} + +gfx::vertex_array const &vulkan14_renderer::Vertices(gfx::geometry_handle const &Geometry) const +{ + return m_geometry.vertices(Geometry); +} + +gfx::userdata_array const &vulkan14_renderer::UserData(gfx::geometry_handle const &Geometry) const +{ + return m_geometry.userdata(Geometry); +} + +// materials, shaders, textures: none until D1 and D2 + +material_handle vulkan14_renderer::Fetch_Material(std::string const &Filename, bool const Loadnow) +{ + return null_handle; +} + +void vulkan14_renderer::Bind_Material(material_handle const Material, TSubModel const *sm, lighting_data const *lighting) {} + +IMaterial const *vulkan14_renderer::Material(material_handle const Material) const +{ + return IMaterial::null_material(); +} + +std::shared_ptr vulkan14_renderer::Fetch_Shader(std::string const &name) +{ + return nullptr; +} + +texture_handle vulkan14_renderer::Fetch_Texture(std::string const &Filename, bool const Loadnow, GLint format_hint) +{ + return null_handle; +} + +void vulkan14_renderer::Bind_Texture(texture_handle const Texture) {} + +void vulkan14_renderer::Bind_Texture(std::size_t const Unit, texture_handle const Texture) {} + +ITexture &vulkan14_renderer::Texture(texture_handle const Texture) +{ + return *ITexture::null_texture(); +} + +ITexture const &vulkan14_renderer::Texture(texture_handle const Texture) const +{ + return *ITexture::null_texture(); +} + +// picking: D6 + +void vulkan14_renderer::Pick_Control_Callback(std::function Callback) {} + +void vulkan14_renderer::Pick_Node_Callback(std::function Callback) {} + +TSubModel const *vulkan14_renderer::Pick_Control() const +{ + return nullptr; +} + +scene::basic_node const *vulkan14_renderer::Pick_Node() const +{ + return nullptr; +} + +glm::dvec3 vulkan14_renderer::Mouse_Position() const +{ + return glm::dvec3(0.0); +} + +// maintenance + +void vulkan14_renderer::Update(double const Deltatime) +{ + m_updateaccumulator += Deltatime; + if (m_updateaccumulator < 1.0) + { + return; + } + m_updateaccumulator = 0.0; + m_framerate = 1000.f / Timer::subsystem.mainloop_total.average(); + update_info(); +} + +void vulkan14_renderer::update_info() +{ + m_infotimes.clear(); + m_infostats.clear(); + if (!m_context) + { + return; + } + auto const &ctx{*m_context}; + auto const &caps{ctx.device.caps()}; + m_infotimes = "vulkan " + vk14::version_string(caps.api_version) + ": " + caps.device_name + " (" + caps.driver_name + ")\n" + "frames: " + std::to_string(ctx.framecount) + ", in flight " + std::to_string(ctx.frames.size()) + "\n" + + "mainloop total: " + to_string(Timer::subsystem.mainloop_total.average(), 2) + " ms, frame cpu: " + to_string(Timer::subsystem.gfx_total.average(), 2) + " ms, present: " + to_string(Timer::subsystem.gfx_swap.average(), 2) + " ms\n"; + for (auto const &window : ctx.windows) + { + auto const extent{window->extent()}; + m_infostats += "window: " + std::to_string(extent.width) + "x" + std::to_string(extent.height) + (window->srgb() ? ", srgb" : ", unorm") + (window->present_mode() == VK_PRESENT_MODE_FIFO_KHR ? ", vsync" : ", no vsync") + "\n"; + } +} + +void vulkan14_renderer::Update_Pick_Control() {} + +void vulkan14_renderer::Update_Pick_Node() {} + +glm::dvec3 vulkan14_renderer::Update_Mouse_Position() +{ + return glm::dvec3(0.0); +} + +bool vulkan14_renderer::Debug_Ui_State(std::optional State) +{ + if (State) + { + m_debug_ui_active = *State; + } + return m_debug_ui_active; +} + +std::string const &vulkan14_renderer::info_times() const +{ + return m_infotimes; +} + +std::string const &vulkan14_renderer::info_stats() const +{ + return m_infostats; +} + +imgui_renderer *vulkan14_renderer::GetImguiRenderer() +{ + return &m_imgui; +} + +void vulkan14_renderer::MakeScreenshot() +{ + vk14::log_warning("screenshots aren't made by the vulkan renderer yet (D6)"); +} + +// imgui: the font atlas is built so its frames go on; nothing is drawn until R5 + +bool vulkan14_renderer::imgui_stub::Init() +{ + auto &io{ImGui::GetIO()}; + io.BackendRendererName = "vulkan14 (no drawing yet)"; + unsigned char *pixels{nullptr}; + int width{0}, height{0}; + io.Fonts->GetTexDataAsRGBA32(&pixels, &width, &height); + io.Fonts->SetTexID(static_cast(0)); + return true; +} + +void vulkan14_renderer::imgui_stub::Shutdown() +{ + ImGui::GetIO().BackendRendererName = nullptr; +} + +void vulkan14_renderer::imgui_stub::BeginFrame() {} + +void vulkan14_renderer::imgui_stub::Render() {} diff --git a/rendering/vulkan14/vulkan14renderer.h b/rendering/vulkan14/vulkan14renderer.h new file mode 100644 index 00000000..0c118882 --- /dev/null +++ b/rendering/vulkan14/vulkan14renderer.h @@ -0,0 +1,106 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +// the vulkan 1.4 renderer, picked with gfxrenderer vulkan. so far the frame of the renderer only: the gpu, the windows with +// their swapchains, the frames in flight; each window is cleared to a colour. the scenery, the materials and the textures +// come in the later stages of the plan (claude/vulkan14-renderer-plan.md); until then those methods keep the data the +// simulation reads back and draw nothing. +// +// the vulkan headers stay in the files of rendering/vulkan14: the objects of the api live in vulkan14_renderer::context + +#pragma once + +#include "rendering/renderer.h" +#include "rendering/geometrybank.h" + +class vulkan14_renderer : public gfx_renderer +{ + public: + vulkan14_renderer(); + ~vulkan14_renderer() override; + + // lifecycle + bool Init(GLFWwindow *Window) override; + bool AddViewport(global_settings::extraviewport_config const &conf) override; + void Shutdown() override; + bool Render() override; + void SwapBuffers() override; + float Framerate() override; + // geometry + gfx::geometrybank_handle Create_Bank() override; + gfx::geometry_handle Insert(gfx::index_array &Indices, gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometrybank_handle const &Geometry, int const Type) override; + gfx::geometry_handle Insert(gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometrybank_handle const &Geometry, int const Type) override; + bool Replace(gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry, int const Type, std::size_t const Offset = 0) override; + bool Append(gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry, int const Type) override; + gfx::index_array const &Indices(gfx::geometry_handle const &Geometry) const override; + gfx::vertex_array const &Vertices(gfx::geometry_handle const &Geometry) const override; + gfx::userdata_array const &UserData(gfx::geometry_handle const &Geometry) const override; + // materials + material_handle Fetch_Material(std::string const &Filename, bool const Loadnow = true) override; + void Bind_Material(material_handle const Material, TSubModel const *sm = nullptr, lighting_data const *lighting = nullptr) override; + IMaterial const *Material(material_handle const Material) const override; + // shaders + std::shared_ptr Fetch_Shader(std::string const &name) override; + // textures + texture_handle Fetch_Texture(std::string const &Filename, bool const Loadnow = true, GLint format_hint = GL_SRGB_ALPHA) override; + void Bind_Texture(texture_handle const Texture) override; + void Bind_Texture(std::size_t const Unit, texture_handle const Texture) override; + ITexture &Texture(texture_handle const Texture) override; + ITexture const &Texture(texture_handle const Texture) const override; + // picking + void Pick_Control_Callback(std::function Callback) override; + void Pick_Node_Callback(std::function Callback) override; + TSubModel const *Pick_Control() const override; + scene::basic_node const *Pick_Node() const override; + glm::dvec3 Mouse_Position() const override; + // maintenance + void Update(double const Deltatime) override; + void Update_Pick_Control() override; + void Update_Pick_Node() override; + glm::dvec3 Update_Mouse_Position() override; + bool Debug_Ui_State(std::optional State) override; + // debug + std::string const &info_times() const override; + std::string const &info_stats() const override; + // ui + imgui_renderer *GetImguiRenderer() override; + void MakeScreenshot() override; + + static std::unique_ptr create_func(); + // how the application makes the windows of the renderer, and whether it can run on the machine + static gfx_backend_desc backend_desc(); + + private: + // the objects of the vulkan api (vulkan14renderer.cpp) + struct context; + + // imgui without drawing: the frames of the ui go on (its input works), nothing is drawn until the stage R5 of the plan + class imgui_stub : public imgui_renderer + { + public: + bool Init() override; + void Shutdown() override; + void BeginFrame() override; + void Render() override; + }; + + void update_info(); + + std::unique_ptr m_context; + // the data of the geometry, which the simulation reads back; nothing on the gpu yet + gfx::geometrybank_manager m_geometry; + imgui_stub m_imgui; + float m_framerate{0.f}; + double m_updateaccumulator{0.0}; + bool m_debug_ui_active{false}; + std::string m_infotimes; + std::string m_infostats; + + static bool renderer_register; +}; diff --git a/rendering/vulkan14/vulkan14swapchain.cpp b/rendering/vulkan14/vulkan14swapchain.cpp new file mode 100644 index 00000000..e44c88cb --- /dev/null +++ b/rendering/vulkan14/vulkan14swapchain.cpp @@ -0,0 +1,455 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "rendering/vulkan14/vulkan14swapchain.h" +#include "rendering/vulkan14/vulkan14instance.h" +#include "rendering/vulkan14/vulkan14device.h" +#include "utilities/Globals.h" + +namespace vk14 +{ + +namespace +{ + +char const *present_mode_string(VkPresentModeKHR const Mode) +{ + switch (Mode) + { + case VK_PRESENT_MODE_IMMEDIATE_KHR: + return "immediate"; + case VK_PRESENT_MODE_MAILBOX_KHR: + return "mailbox"; + case VK_PRESENT_MODE_FIFO_KHR: + return "fifo"; + case VK_PRESENT_MODE_FIFO_RELAXED_KHR: + return "fifo relaxed"; + default: + break; + } + return "other"; +} + +bool is_srgb(VkFormat const Format) +{ + switch (Format) + { + case VK_FORMAT_B8G8R8A8_SRGB: + case VK_FORMAT_R8G8B8A8_SRGB: + case VK_FORMAT_A8B8G8R8_SRGB_PACK32: + return true; + default: + break; + } + return false; +} + +// the settings the swapchain follows: vsync, and whether the shaders write gamma themselves (as GLFW_SRGB_CAPABLE in opengl) +bool wanted_vsync() +{ + return Global.VSync; +} + +bool wanted_srgb() +{ + return (false == Global.gfx_shadergamma); +} + +} // namespace + +swapchain::~swapchain() +{ + destroy(); +} + +bool swapchain::create(instance const &Instance, device const &Device, GLFWwindow *Window, std::string const &Name, std::uint32_t const Frames, std::string &Reason) +{ + m_instance = &Instance; + m_device = &Device; + m_window = Window; + m_name = Name; + + if (!VK14_VERIFY(glfwCreateWindowSurface(Instance.handle(), Window, nullptr, &m_surface))) + { + m_surface = VK_NULL_HANDLE; + Reason = "the window of " + Name + " gives no vulkan surface"; + return false; + } + Instance.set_object_name(Device.handle(), VK_OBJECT_TYPE_SURFACE_KHR, reinterpret_cast(m_surface), Name + " surface"); + + VkBool32 present{VK_FALSE}; + if (!VK14_VERIFY(vkGetPhysicalDeviceSurfaceSupportKHR(Device.physical(), Device.families().graphics, m_surface, &present)) || (present == VK_FALSE)) + { + Reason = "the gpu can't show images in the window of " + Name; + return false; + } + + VkSemaphoreCreateInfo semaphoreinfo{}; + semaphoreinfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; + m_acquiresemaphores.assign(Frames, VK_NULL_HANDLE); + for (std::uint32_t i = 0; i < Frames; ++i) + { + if (!VK14_VERIFY(vkCreateSemaphore(Device.handle(), &semaphoreinfo, nullptr, &m_acquiresemaphores[i]))) + { + Reason = "no semaphores for " + Name; + return false; + } + Instance.set_object_name(Device.handle(), VK_OBJECT_TYPE_SEMAPHORE, reinterpret_cast(m_acquiresemaphores[i]), Name + " acquire " + std::to_string(i)); + } + + return build(Reason); +} + +bool swapchain::outdated() const +{ + if (m_rebuild || (m_swapchain == VK_NULL_HANDLE) || (m_vsync != wanted_vsync()) || (m_srgb != wanted_srgb())) + { + return true; + } + int width{0}, height{0}; + glfwGetFramebufferSize(m_window, &width, &height); + return (width != m_framebuffer.x) || (height != m_framebuffer.y); +} + +bool swapchain::build(std::string &Reason) +{ + auto const physical{m_device->physical()}; + auto const device{m_device->handle()}; + + int width{0}, height{0}; + glfwGetFramebufferSize(m_window, &width, &height); + if ((width <= 0) || (height <= 0)) + { + // minimized: no swapchain of 0x0, the window is skipped until it comes back + m_rebuild = true; + return true; + } + + VkSurfaceCapabilitiesKHR capabilities{}; + auto const result{vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physical, m_surface, &capabilities)}; + if (result != VK_SUCCESS) + { + Reason = std::string("the surface of ") + m_name + " can't be queried (" + result_string(result) + ")"; + return false; + } + + VkExtent2D extent{static_cast(width), static_cast(height)}; + if (capabilities.currentExtent.width != 0xFFFFFFFFu) + { + extent = capabilities.currentExtent; + } + extent.width = std::clamp(extent.width, capabilities.minImageExtent.width, capabilities.maxImageExtent.width); + extent.height = std::clamp(extent.height, capabilities.minImageExtent.height, capabilities.maxImageExtent.height); + if ((extent.width == 0) || (extent.height == 0)) + { + m_rebuild = true; + return true; + } + + // the format: srgb when the shaders write linear colours (gfx.shadergamma no), as GLFW_SRGB_CAPABLE in opengl + auto const srgb{wanted_srgb()}; + std::uint32_t formatcount{0}; + vkGetPhysicalDeviceSurfaceFormatsKHR(physical, m_surface, &formatcount, nullptr); + std::vector formats(formatcount); + vkGetPhysicalDeviceSurfaceFormatsKHR(physical, m_surface, &formatcount, formats.data()); + if (formats.empty()) + { + Reason = "the surface of " + m_name + " has no formats"; + return false; + } + std::vector const wanted{srgb ? std::vector{VK_FORMAT_B8G8R8A8_SRGB, VK_FORMAT_R8G8B8A8_SRGB, VK_FORMAT_A8B8G8R8_SRGB_PACK32} : std::vector{VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_A8B8G8R8_UNORM_PACK32}}; + VkSurfaceFormatKHR chosen{formats.front()}; + if ((formats.size() == 1) && (formats.front().format == VK_FORMAT_UNDEFINED)) + { + // any format will do + chosen = {wanted.front(), VK_COLOR_SPACE_SRGB_NONLINEAR_KHR}; + } + else + { + auto found{false}; + for (auto const format : wanted) + { + auto const lookup{std::find_if(formats.begin(), formats.end(), [format](VkSurfaceFormatKHR const &Entry) { return (Entry.format == format) && (Entry.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR); })}; + if (lookup != formats.end()) + { + chosen = *lookup; + found = true; + break; + } + } + if (false == found) + { + log_warning(m_name + ": no " + (srgb ? "srgb" : "unorm") + " 8 bit format of the surface, using format " + std::to_string(chosen.format)); + } + } + + // the presentation: vsync waits for the screen (fifo, always there), without vsync mailbox, or else immediate + auto const vsync{wanted_vsync()}; + std::uint32_t modecount{0}; + vkGetPhysicalDeviceSurfacePresentModesKHR(physical, m_surface, &modecount, nullptr); + std::vector modes(modecount); + vkGetPhysicalDeviceSurfacePresentModesKHR(physical, m_surface, &modecount, modes.data()); + auto presentmode{VK_PRESENT_MODE_FIFO_KHR}; + if (false == vsync) + { + for (auto const mode : {VK_PRESENT_MODE_MAILBOX_KHR, VK_PRESENT_MODE_IMMEDIATE_KHR}) + { + if (std::find(modes.begin(), modes.end(), mode) != modes.end()) + { + presentmode = mode; + break; + } + } + } + + auto imagecount{capabilities.minImageCount + 1}; + if (capabilities.maxImageCount > 0) + { + imagecount = std::min(imagecount, capabilities.maxImageCount); + } + + auto compositealpha{VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR}; + if ((capabilities.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR) == 0) + { + for (auto const alpha : {VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR, VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR, VK_COMPOSITE_ALPHA_POST_MULTIPLIED_BIT_KHR}) + { + if ((capabilities.supportedCompositeAlpha & alpha) != 0) + { + compositealpha = alpha; + break; + } + } + } + + // the images still in use by the frames in flight and the presentation have to be done with before they go + auto const oldswapchain{m_swapchain}; + if (oldswapchain != VK_NULL_HANDLE) + { + vkQueueWaitIdle(m_device->graphics_queue()); + release_images(); + } + + VkSwapchainCreateInfoKHR create{}; + create.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR; + create.surface = m_surface; + create.minImageCount = imagecount; + create.imageFormat = chosen.format; + create.imageColorSpace = chosen.colorSpace; + create.imageExtent = extent; + create.imageArrayLayers = 1; + // drawn into as a colour attachment; the frame is copied into it at the end (flipped, see K8 of the plan) + create.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + create.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; + create.preTransform = capabilities.currentTransform; + create.compositeAlpha = compositealpha; + create.presentMode = presentmode; + create.clipped = VK_TRUE; + create.oldSwapchain = oldswapchain; + + VkSwapchainKHR swapchain{VK_NULL_HANDLE}; + auto const created{vkCreateSwapchainKHR(device, &create, nullptr, &swapchain)}; + if (oldswapchain != VK_NULL_HANDLE) + { + vkDestroySwapchainKHR(device, oldswapchain, nullptr); + } + m_swapchain = VK_NULL_HANDLE; + if (created != VK_SUCCESS) + { + Reason = "the swapchain of " + m_name + " can't be made (" + result_string(created) + ")"; + return false; + } + m_swapchain = swapchain; + m_format = chosen.format; + m_colorspace = chosen.colorSpace; + m_presentmode = presentmode; + m_extent = extent; + m_framebuffer = {width, height}; + m_vsync = vsync; + m_srgb = srgb; + m_rebuild = false; + m_instance->set_object_name(device, VK_OBJECT_TYPE_SWAPCHAIN_KHR, reinterpret_cast(m_swapchain), m_name + " swapchain"); + + std::uint32_t count{0}; + vkGetSwapchainImagesKHR(device, m_swapchain, &count, nullptr); + m_images.resize(count); + vkGetSwapchainImagesKHR(device, m_swapchain, &count, m_images.data()); + + VkSemaphoreCreateInfo semaphoreinfo{}; + semaphoreinfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; + m_views.assign(count, VK_NULL_HANDLE); + m_presentsemaphores.assign(count, VK_NULL_HANDLE); + for (std::uint32_t i = 0; i < count; ++i) + { + VkImageViewCreateInfo view{}; + view.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + view.image = m_images[i]; + view.viewType = VK_IMAGE_VIEW_TYPE_2D; + view.format = m_format; + view.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + if (!VK14_VERIFY(vkCreateImageView(device, &view, nullptr, &m_views[i])) || !VK14_VERIFY(vkCreateSemaphore(device, &semaphoreinfo, nullptr, &m_presentsemaphores[i]))) + { + Reason = "the images of the swapchain of " + m_name + " can't be used"; + return false; + } + m_instance->set_object_name(device, VK_OBJECT_TYPE_IMAGE, reinterpret_cast(m_images[i]), m_name + " image " + std::to_string(i)); + m_instance->set_object_name(device, VK_OBJECT_TYPE_SEMAPHORE, reinterpret_cast(m_presentsemaphores[i]), m_name + " present " + std::to_string(i)); + } + + log_info("swapchain of " + m_name + ": " + std::to_string(m_extent.width) + "x" + std::to_string(m_extent.height) + ", " + std::to_string(count) + " images, " + (is_srgb(m_format) ? "srgb" : "unorm") + " format " + std::to_string(m_format) + ", present mode " + + present_mode_string(m_presentmode) + (m_vsync ? " (vsync)" : "")); + return true; +} + +swapchain::acquire_result swapchain::acquire(std::uint32_t const Frame) +{ + if (outdated()) + { + std::string reason; + if (false == build(reason)) + { + log_error(reason); + return acquire_result::failed; + } + } + if ((m_swapchain == VK_NULL_HANDLE) || m_rebuild) + { + return acquire_result::skipped; + } + + auto const result{vkAcquireNextImageKHR(m_device->handle(), m_swapchain, UINT64_MAX, m_acquiresemaphores[Frame], VK_NULL_HANDLE, &m_image)}; + switch (result) + { + case VK_SUCCESS: + return acquire_result::ready; + case VK_SUBOPTIMAL_KHR: + // the image is acquired and its semaphore will be signalled, so it's drawn and shown; the swapchain is made anew after + m_rebuild = true; + return acquire_result::ready; + case VK_ERROR_OUT_OF_DATE_KHR: + m_rebuild = true; + return acquire_result::skipped; + default: + break; + } + log_error(std::string("vkAcquireNextImageKHR of ") + m_name + " failed with " + result_string(result)); + return acquire_result::failed; +} + +void swapchain::presented(VkResult const Result) +{ + if ((Result == VK_SUBOPTIMAL_KHR) || (Result == VK_ERROR_OUT_OF_DATE_KHR)) + { + m_rebuild = true; + } +} + +void swapchain::record_clear(VkCommandBuffer Commands, glm::vec4 const &Colour) const +{ + VkImageMemoryBarrier2 barrier{}; + barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2; + // the acquire semaphore is waited for at the colour output, so the change of layout waits for it too + barrier.srcStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT; + barrier.srcAccessMask = VK_ACCESS_2_NONE; + barrier.dstStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT; + barrier.dstAccessMask = VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT; + barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + barrier.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = m_images[m_image]; + barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + + VkDependencyInfo dependency{}; + dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; + dependency.imageMemoryBarrierCount = 1; + dependency.pImageMemoryBarriers = &barrier; + vkCmdPipelineBarrier2(Commands, &dependency); + + VkRenderingAttachmentInfo colour{}; + colour.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO; + colour.imageView = m_views[m_image]; + colour.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + colour.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; + colour.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + colour.clearValue.color = {{Colour.r, Colour.g, Colour.b, Colour.a}}; + + VkRenderingInfo rendering{}; + rendering.sType = VK_STRUCTURE_TYPE_RENDERING_INFO; + rendering.renderArea = {{0, 0}, m_extent}; + rendering.layerCount = 1; + rendering.colorAttachmentCount = 1; + rendering.pColorAttachments = &colour; + vkCmdBeginRendering(Commands, &rendering); + vkCmdEndRendering(Commands); + + barrier.srcStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT; + barrier.srcAccessMask = VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT; + barrier.dstStageMask = VK_PIPELINE_STAGE_2_NONE; + barrier.dstAccessMask = VK_ACCESS_2_NONE; + barrier.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + barrier.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + vkCmdPipelineBarrier2(Commands, &dependency); +} + +void swapchain::release_images() +{ + auto const device{m_device != nullptr ? m_device->handle() : VK_NULL_HANDLE}; + if (device == VK_NULL_HANDLE) + { + return; + } + for (auto view : m_views) + { + if (view != VK_NULL_HANDLE) + { + vkDestroyImageView(device, view, nullptr); + } + } + for (auto semaphore : m_presentsemaphores) + { + if (semaphore != VK_NULL_HANDLE) + { + vkDestroySemaphore(device, semaphore, nullptr); + } + } + m_views.clear(); + m_presentsemaphores.clear(); + m_images.clear(); +} + +void swapchain::destroy() +{ + auto const device{m_device != nullptr ? m_device->handle() : VK_NULL_HANDLE}; + if (device != VK_NULL_HANDLE) + { + release_images(); + if (m_swapchain != VK_NULL_HANDLE) + { + vkDestroySwapchainKHR(device, m_swapchain, nullptr); + } + for (auto semaphore : m_acquiresemaphores) + { + if (semaphore != VK_NULL_HANDLE) + { + vkDestroySemaphore(device, semaphore, nullptr); + } + } + } + m_swapchain = VK_NULL_HANDLE; + m_acquiresemaphores.clear(); + if ((m_surface != VK_NULL_HANDLE) && (m_instance != nullptr) && (m_instance->handle() != VK_NULL_HANDLE)) + { + vkDestroySurfaceKHR(m_instance->handle(), m_surface, nullptr); + } + m_surface = VK_NULL_HANDLE; + m_window = nullptr; +} + +} // namespace vk14 diff --git a/rendering/vulkan14/vulkan14swapchain.h b/rendering/vulkan14/vulkan14swapchain.h new file mode 100644 index 00000000..1513693a --- /dev/null +++ b/rendering/vulkan14/vulkan14swapchain.h @@ -0,0 +1,122 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +// a window the renderer shows images in: its surface and swapchain, made anew when the window changes size, when the driver +// says the swapchain is out of date, and when vsync or gfx.shadergamma change. a minimized window (0x0) is skipped until +// it comes back + +#pragma once + +#include "rendering/vulkan14/vulkan14common.h" + +namespace vk14 +{ + +class instance; +class device; + +class swapchain +{ + public: + enum class acquire_result + { + ready, // an image to draw into, its acquire semaphore will be signalled + skipped, // nothing to draw this frame (minimized, or made anew) + failed // the device is gone + }; + + swapchain() = default; + swapchain(swapchain const &) = delete; + swapchain &operator=(swapchain const &) = delete; + ~swapchain(); + + // the surface of the window and, when the window isn't minimized, the swapchain. Frames: frames in flight + bool create(instance const &Instance, device const &Device, GLFWwindow *Window, std::string const &Name, std::uint32_t const Frames, std::string &Reason); + void destroy(); + + // the image of the frame Frame (index of the frame in flight) + acquire_result acquire(std::uint32_t const Frame); + // the image acquired, from undefined to cleared with Colour (linear) and ready to be shown + void record_clear(VkCommandBuffer Commands, glm::vec4 const &Colour) const; + // the semaphore signalled when the image of the frame Frame is acquired + VkSemaphore acquire_semaphore(std::uint32_t const Frame) const + { + return m_acquiresemaphores[Frame]; + } + // the semaphore the presentation of the acquired image waits for + VkSemaphore present_semaphore() const + { + return m_presentsemaphores[m_image]; + } + VkSwapchainKHR handle() const + { + return m_swapchain; + } + std::uint32_t image_index() const + { + return m_image; + } + // the result of the presentation of the image: out of date or suboptimal makes the swapchain anew before the next image + void presented(VkResult const Result); + + GLFWwindow *window() const + { + return m_window; + } + VkExtent2D extent() const + { + return m_extent; + } + VkFormat format() const + { + return m_format; + } + VkPresentModeKHR present_mode() const + { + return m_presentmode; + } + bool srgb() const + { + return m_srgb; + } + + private: + // false only when the device is gone or the surface is lost; a minimized window gives true with no swapchain + bool build(std::string &Reason); + void release_images(); + // whether the swapchain has to be made anew before the next image + bool outdated() const; + + instance const *m_instance{nullptr}; + device const *m_device{nullptr}; + GLFWwindow *m_window{nullptr}; + std::string m_name; + VkSurfaceKHR m_surface{VK_NULL_HANDLE}; + VkSwapchainKHR m_swapchain{VK_NULL_HANDLE}; + VkFormat m_format{VK_FORMAT_UNDEFINED}; + VkColorSpaceKHR m_colorspace{VK_COLOR_SPACE_SRGB_NONLINEAR_KHR}; + VkPresentModeKHR m_presentmode{VK_PRESENT_MODE_FIFO_KHR}; + VkExtent2D m_extent{0, 0}; + // the size of the window the swapchain was made for; the swapchain itself may be clamped to the limits of the surface + glm::ivec2 m_framebuffer{0, 0}; + // the settings the swapchain was made for + bool m_vsync{false}; + bool m_srgb{false}; + bool m_rebuild{false}; + + std::vector m_images; + std::vector m_views; + // one per image: a semaphore given to the presentation can be reused only once the image comes back + std::vector m_presentsemaphores; + // one per frame in flight: free again once the frame which waited for it is done + std::vector m_acquiresemaphores; + std::uint32_t m_image{0}; +}; + +} // namespace vk14 diff --git a/scripting/pythonscreenviewer.cpp b/scripting/pythonscreenviewer.cpp index 21a17f65..f7a0633e 100644 --- a/scripting/pythonscreenviewer.cpp +++ b/scripting/pythonscreenviewer.cpp @@ -46,6 +46,12 @@ python_screen_viewer::python_screen_viewer(std::shared_ptr rt, std::s conf->window = Application.window(-1, true, conf->window_size.x, conf->window_size.y, monitor, false, Global.python_sharectx); + if (conf->window == nullptr) + { + // e.g. the renderer has no opengl context to share + ErrorLog("python screen \"" + surfacename + "\": window couldn't be made"); + continue; + } { int w; diff --git a/utilities/Globals.cpp b/utilities/Globals.cpp index db09f80a..0660e2ee 100644 --- a/utilities/Globals.cpp +++ b/utilities/Globals.cpp @@ -317,10 +317,13 @@ bool global_settings::ConfigParseGraphics(cParser& Parser, const std::string& to { ParseOne(Parser, GfxRenderer); - if (GfxRenderer == "full") + // "default": the opengl 4.6 renderer, "vulkan": the vulkan 1.4 one (opengl when it can't run on the machine) + if ((GfxRenderer == "full") || (GfxRenderer == "opengl") || (GfxRenderer == "gl46")) GfxRenderer = "default"; + else if (GfxRenderer == "vulkan14") + GfxRenderer = "vulkan"; - if (GfxRenderer != "default") + if ((GfxRenderer != "default") && (GfxRenderer != "vulkan")) { // the legacy OpenGL 2.1 renderer ("legacy", "simple" and other values) and the NVRHI renderer ("experimental") were removed WriteLog("eu07.ini: gfxrenderer \"" + GfxRenderer + "\" is no longer supported, using \"default\""); @@ -1741,6 +1744,26 @@ global_settings::ConfigParse_gfx( cParser &Parser, std::string_view const Token Parser >> gfx_shadow_sdsm_split; gfx_shadow_sdsm_split = std::clamp(gfx_shadow_sdsm_split, 0.f, 1.f); } + else if (Token == "gfx.vulkan.device") + { + // index of the gpu in the list of the log, or a part of its name + Parser.getTokens(1, false); + Parser >> gfx_vulkan_device; + } + else if (Token == "gfx.vulkan.validation") + { + // no/yes, 2: also gpu-assisted + std::string value; + Parser.getTokens(1); + Parser >> value; + gfx_vulkan_validation = (value == "2" ? 2 : (value == "yes" || value == "true" || value == "1") ? 1 : 0); + } + else if (Token == "gfx.vulkan.framesinflight") + { + Parser.getTokens(1); + Parser >> gfx_vulkan_framesinflight; + gfx_vulkan_framesinflight = std::clamp(gfx_vulkan_framesinflight, 2, 3); + } else if (ConfigParse_gfx_sky(Parser, Token)) { // parsed @@ -2158,6 +2181,9 @@ global_settings::export_as_text( std::ostream &Output ) const { export_as_text( Output, "gfx.skippipeline", gfx_skippipeline ); export_as_text( Output, "gfx.extraeffects", gfx_extraeffects ); export_as_text( Output, "gfx.shadergamma", gfx_shadergamma ); + export_as_text( Output, "gfx.vulkan.device", gfx_vulkan_device ); + export_as_text( Output, "gfx.vulkan.validation", gfx_vulkan_validation ); + export_as_text( Output, "gfx.vulkan.framesinflight", gfx_vulkan_framesinflight ); export_as_text( Output, "gfx.shadow.angle.min", gfx_shadow_angle_min ); export_as_text( Output, "gfx.shadow.rank.cutoff", gfx_shadow_rank_cutoff ); export_as_text( Output, "gfx.shadow.sdsm", gfx_shadow_sdsm ); diff --git a/utilities/Globals.h b/utilities/Globals.h index 54bfb8d3..a25408dd 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -367,6 +367,12 @@ struct global_settings { bool gfx_extraeffects = true; bool gfx_shadergamma = false; bool gfx_gldebug = false; + // (vulkan) the gpu to run on: its index in the list of the log or a part of its name; empty: the best one + std::string gfx_vulkan_device; + // (vulkan) 0: no validation layer, 1: validation layer, 2: also gpu-assisted validation + int gfx_vulkan_validation { 0 }; + // (vulkan) frames recorded while the gpu works on the earlier ones, 2 or 3 + int gfx_vulkan_framesinflight { 2 }; bool vr = false; std::string vr_backend;