952 lines
31 KiB
C++
952 lines
31 KiB
C++
|
|
#include "raid/application.hpp"
|
|
|
|
#include <chrono>
|
|
#include <thread>
|
|
#include <fmt/base.h>
|
|
#include <SDL3/SDL.h>
|
|
#include <SDL3/SDL_vulkan.h>
|
|
#include <mijin/detect.hpp>
|
|
#include <mijin/debug/assert.hpp>
|
|
#include <mijin/platform/folders.hpp>
|
|
#include <mijin/util/iterators.hpp>
|
|
#include <mijin/util/scope_guard.hpp>
|
|
#include <mijin/virtual_filesystem/mapping.hpp>
|
|
#include <mijin/virtual_filesystem/relative.hpp>
|
|
#include <imgui.h>
|
|
#include <imgui_internal.h>
|
|
#include <backends/imgui_impl_opengl3.h>
|
|
#include <backends/imgui_impl_sdl3.h>
|
|
#include <stb_image.h>
|
|
#include "./fonts.gen.hpp"
|
|
|
|
namespace raid
|
|
{
|
|
namespace
|
|
{
|
|
constexpr std::uint32_t GL_COLOR_BUFFER_BIT = 0x00004000;
|
|
constexpr std::uint32_t GL_TEXTURE_2D = 0x0DE1;
|
|
constexpr std::uint32_t GL_TEXTURE_MIN_FILTER = 0x2801;
|
|
constexpr std::uint32_t GL_TEXTURE_MAG_FILTER = 0x2800;
|
|
constexpr std::uint32_t GL_LINEAR = 0x2601;
|
|
constexpr std::uint32_t GL_UNPACK_ROW_LENGTH = 0x0CF2;
|
|
constexpr std::uint32_t GL_RGBA = 0x1908;
|
|
constexpr std::uint32_t GL_UNSIGNED_BYTE = 0x1401;
|
|
|
|
const char* IMGUI_GLSL_VERSION = "#version 130";
|
|
QuickApp* gAppInstance = nullptr;
|
|
|
|
int stbiRead(void* user, char* data, int size)
|
|
{
|
|
mijin::Stream& stream = *static_cast<mijin::Stream*>(user);
|
|
std::size_t read = 0;
|
|
if (const mijin::StreamError error = stream.readRaw(data, size, {.partial = true}, &read); error != mijin::StreamError::SUCCESS)
|
|
{
|
|
MIJIN_ERROR("IO error.");
|
|
return -1;
|
|
}
|
|
return static_cast<int>(read);
|
|
}
|
|
|
|
void stbiSkip(void* user, int diff)
|
|
{
|
|
mijin::Stream& stream = *static_cast<mijin::Stream*>(user);
|
|
if (const mijin::StreamError error = stream.seek(diff, mijin::SeekMode::RELATIVE); error != mijin::StreamError::SUCCESS)
|
|
{
|
|
MIJIN_ERROR("IO error.");
|
|
// not much we can do :/
|
|
}
|
|
}
|
|
|
|
int stbiEof(void* user)
|
|
{
|
|
mijin::Stream& stream = *static_cast<mijin::Stream*>(user);
|
|
return stream.isAtEnd();
|
|
}
|
|
|
|
[[nodiscard]]
|
|
constexpr std::uint32_t vkMakeApiVersion(std::uint32_t variant, std::uint32_t major, std::uint32_t minor, std::uint32_t patch) noexcept
|
|
{
|
|
return ((variant << 29U) | (major << 22U) | (minor << 12U) | patch);
|
|
}
|
|
}
|
|
|
|
int Application::run(int argc, char** argv)
|
|
{
|
|
(void) argc;
|
|
(void) argv;
|
|
|
|
MIJIN_SCOPE_EXIT {
|
|
cleanup();
|
|
};
|
|
|
|
if (!init())
|
|
{
|
|
return ERR_INIT_FAILED;
|
|
}
|
|
|
|
while (mRunning)
|
|
{
|
|
handleSDLEvents();
|
|
|
|
if (SDL_GetWindowFlags(mWindow) & SDL_WINDOW_MINIMIZED)
|
|
{
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
|
continue;
|
|
}
|
|
|
|
ImGuiIO& imguiIO = ImGui::GetIO();
|
|
|
|
ImGui_ImplOpenGL3_NewFrame();
|
|
ImGui_ImplSDL3_NewFrame();
|
|
ImGui::NewFrame();
|
|
|
|
ImGui::SetNextWindowPos(ImGui::GetMainViewport()->Pos);
|
|
ImGui::SetNextWindowSize(ImGui::GetMainViewport()->Size);
|
|
ImGui::SetNextWindowViewport(ImGui::GetMainViewport()->ID);
|
|
|
|
for (const auto& [variable, value] : mMainWindowStyles) {
|
|
std::visit([&](auto val) {
|
|
ImGui::PushStyleVar(variable, val);
|
|
}, value);
|
|
}
|
|
|
|
ImGui::Begin("##main", nullptr, mMainWindowFlags);
|
|
ImGui::PopStyleVar(static_cast<int>(mMainWindowStyles.size()));
|
|
|
|
render();
|
|
|
|
mTaskLoop.tick();
|
|
ImGui::End();
|
|
|
|
ImGui::Render();
|
|
|
|
switch (mConfig.graphicsApi)
|
|
{
|
|
case GraphicsAPI::OPENGL:
|
|
gl.ClearColor(0.3f, 0.3f, 0.3f, 1.f);
|
|
gl.Clear(GL_COLOR_BUFFER_BIT);
|
|
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
|
|
break;
|
|
case GraphicsAPI::VULKAN:
|
|
MIJIN_TRAP(); // TODO!
|
|
break;
|
|
}
|
|
|
|
if (imguiIO.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
|
|
{
|
|
ImGui::UpdatePlatformWindows();
|
|
ImGui::RenderPlatformWindowsDefault();
|
|
|
|
if (mConfig.graphicsApi == GraphicsAPI::OPENGL) {
|
|
SDL_GL_MakeCurrent(mWindow, gl.context);
|
|
}
|
|
}
|
|
|
|
SDL_GL_SwapWindow(mWindow);
|
|
|
|
if (imguiIO.WantSaveIniSettings)
|
|
{
|
|
saveImGuiConfig();
|
|
imguiIO.WantSaveIniSettings = false;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
bool Application::loadFonts(std::span<const FontConfig> fonts)
|
|
{
|
|
ImGuiIO& imguiIO = ImGui::GetIO();
|
|
|
|
std::vector<mijin::TypelessBuffer> buffers;
|
|
buffers.reserve(fonts.size());
|
|
|
|
for (const FontConfig& font : fonts)
|
|
{
|
|
std::unique_ptr<mijin::Stream> fontFile;
|
|
if (const mijin::StreamError error = mFS.open(font.path, mijin::FileOpenMode::READ, fontFile);
|
|
error != mijin::StreamError::SUCCESS)
|
|
{
|
|
msgError("Error opening font file {}: {}.", font.path.generic_string(), mijin::errorName(error));
|
|
return false;
|
|
}
|
|
|
|
mijin::TypelessBuffer& data = buffers.emplace_back();
|
|
if (const mijin::StreamError readError = fontFile->readRest(data); readError != mijin::StreamError::SUCCESS)
|
|
{
|
|
msgError("Error reading font data from {}: {}.", font.path.generic_string(), mijin::errorName(readError));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// by default ImGui takes ownership of the data, that's now what we want
|
|
ImFontConfig config;
|
|
config.FontDataOwnedByAtlas = false;
|
|
std::vector<std::vector<ImWchar>> glyphRangesConverted;
|
|
glyphRangesConverted.reserve(fonts.size());
|
|
for (const auto& [font, data] : mijin::zip(fonts, buffers))
|
|
{
|
|
ImWchar* glyphRanges = nullptr;
|
|
if (!font.glyphRanges.empty())
|
|
{
|
|
std::vector<ImWchar>& glyphData = glyphRangesConverted.emplace_back();
|
|
glyphData.reserve(2 * font.glyphRanges.size() + 1);
|
|
glyphData.clear();
|
|
for (const std::pair<ImWchar, ImWchar>& range : font.glyphRanges)
|
|
{
|
|
glyphData.push_back(range.first);
|
|
glyphData.push_back(range.second);
|
|
}
|
|
glyphData.push_back(0);
|
|
glyphRanges = glyphData.data();
|
|
}
|
|
config.PixelSnapH = font.flags.pixelSnapH;
|
|
imguiIO.Fonts->AddFontFromMemoryTTF(data.data(), static_cast<int>(data.byteSize()), font.size, &config, glyphRanges);
|
|
config.MergeMode = true; // for any more fonts
|
|
}
|
|
|
|
// but in that case Build() has to be run before the data gets deleted
|
|
imguiIO.Fonts->Build();
|
|
return true;
|
|
}
|
|
|
|
ImTextureID Application::getOrLoadTexture(fs::path path)
|
|
{
|
|
if (auto it = mTextures.find(path); it != mTextures.end())
|
|
{
|
|
return it->second;
|
|
}
|
|
|
|
const stbi_io_callbacks callbacks = {
|
|
.read = &stbiRead,
|
|
.skip = &stbiSkip,
|
|
.eof = &stbiEof
|
|
};
|
|
|
|
std::unique_ptr<mijin::Stream> stream;
|
|
if (const mijin::StreamError error = mFS.open(path, mijin::FileOpenMode::READ, stream); error != mijin::StreamError::SUCCESS)
|
|
{
|
|
msgError("Error opening file {} for reading: {}.", path.generic_string(), mijin::errorName(error));
|
|
return 0;
|
|
}
|
|
int width = 0;
|
|
int height = 0;
|
|
unsigned char* imageData = stbi_load_from_callbacks(&callbacks, stream.get(), &width, &height, nullptr, 4);
|
|
if (imageData == nullptr)
|
|
{
|
|
msgError("Error parsing image at {}.", path.generic_string());
|
|
return 0;
|
|
}
|
|
|
|
// create texture
|
|
ImTextureID texture = 0;
|
|
switch (mConfig.graphicsApi)
|
|
{
|
|
case GraphicsAPI::OPENGL:
|
|
{
|
|
GLuint glTexture = 0;
|
|
gl.GenTextures(1, &glTexture);
|
|
gl.BindTexture(GL_TEXTURE_2D, glTexture);
|
|
|
|
// setup texture
|
|
gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
|
|
// upload image
|
|
gl.PixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
|
gl.TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, imageData);
|
|
|
|
texture = glTexture;
|
|
break;
|
|
}
|
|
case GraphicsAPI::VULKAN:
|
|
MIJIN_TRAP(); // TODO!
|
|
break;
|
|
}
|
|
|
|
if (texture != 0) {
|
|
mTextures.emplace(std::move(path), texture);
|
|
}
|
|
|
|
return texture;
|
|
}
|
|
|
|
void Application::destroyTexture(ImTextureID texture)
|
|
{
|
|
auto it = std::ranges::find_if(mTextures, [texture](const auto& entry) {
|
|
return entry.second == texture;
|
|
});
|
|
MIJIN_ASSERT(it != mTextures.end(), "Invalid texture id.");
|
|
mTextures.erase(it);
|
|
|
|
switch (mConfig.graphicsApi)
|
|
{
|
|
case GraphicsAPI::OPENGL:
|
|
{
|
|
const GLuint asUint = static_cast<GLuint>(texture);
|
|
gl.DeleteTextures(1, &asUint);
|
|
break;
|
|
}
|
|
case GraphicsAPI::VULKAN:
|
|
MIJIN_TRAP(); // TODO
|
|
break;
|
|
}
|
|
}
|
|
|
|
void Application::configureImgui()
|
|
{
|
|
ImGuiIO& imguiIO = ImGui::GetIO();
|
|
imguiIO.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
|
|
imguiIO.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;
|
|
imguiIO.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable;
|
|
imguiIO.ConfigViewportsNoAutoMerge = true;
|
|
}
|
|
|
|
std::vector<FontConfig> Application::getDefaultFonts()
|
|
{
|
|
return {{
|
|
.path = DEFAULT_FONT_PATH
|
|
}};
|
|
}
|
|
|
|
void Application::initMemoryFS()
|
|
{
|
|
mMemoryFS->addFile(DEFAULT_FONT_PATH, NOTO_SANS_DATA);
|
|
}
|
|
|
|
void Application::handleMessage(const Message& message)
|
|
{
|
|
switch (message.severity)
|
|
{
|
|
case MessageSeverity::INFO:
|
|
fmt::println("INFO: {}", message.text);
|
|
break;
|
|
case MessageSeverity::WARNING:
|
|
fmt::println("WARNING: {}", message.text);
|
|
break;
|
|
case MessageSeverity::ERROR:
|
|
fmt::println(stderr, "ERROR: {}", message.text);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void Application::handleSDLEvent(const SDL_Event& event)
|
|
{
|
|
switch (event.type)
|
|
{
|
|
case SDL_EVENT_QUIT:
|
|
mRunning = false;
|
|
return;
|
|
case SDL_EVENT_WINDOW_CLOSE_REQUESTED:
|
|
if (SDL_GetWindowFromID(event.window.windowID) == mWindow) {
|
|
handleCloseRequested();
|
|
}
|
|
break;
|
|
default:
|
|
ImGui_ImplSDL3_ProcessEvent(&event);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void Application::handleSDLError(const char* message)
|
|
{
|
|
msgError("SDL: {}", message);
|
|
}
|
|
|
|
void Application::handleCloseRequested()
|
|
{
|
|
SDL_Event quitEvent;
|
|
quitEvent.type = SDL_EVENT_QUIT;
|
|
SDL_PushEvent(&quitEvent);
|
|
}
|
|
|
|
bool Application::init()
|
|
{
|
|
auto addConfigDir = [&](const fs::path& path)
|
|
{
|
|
using namespace mijin::vfs_pipe;
|
|
mFS.addAdapter(os()
|
|
| relative_to(path)
|
|
| map_to("/config")
|
|
);
|
|
};
|
|
|
|
auto addDataDir = [&](const fs::path& path)
|
|
{
|
|
using namespace mijin::vfs_pipe;
|
|
mFS.addAdapter(os()
|
|
| relative_to(path)
|
|
| map_to("/data")
|
|
);
|
|
};
|
|
|
|
mMemoryFS = mFS.emplaceAdapter<mijin::MemoryFileSystemAdapter>();
|
|
initMemoryFS();
|
|
|
|
addConfigDir(mijin::getKnownFolder(mijin::KnownFolder::USER_CONFIG_ROOT) / getFolderName());
|
|
addDataDir(mijin::getKnownFolder(mijin::KnownFolder::USER_DATA_ROOT) / getFolderName());
|
|
|
|
auto createUserDir = [&](const fs::path& virtualPath)
|
|
{
|
|
mijin::Optional<fs::path> pathOpt = mFS.getNativePath(virtualPath);
|
|
if (!pathOpt.empty())
|
|
{
|
|
const fs::path path = std::move(*pathOpt);
|
|
if (!fs::exists(path))
|
|
{
|
|
[[maybe_unused]] const bool result = fs::create_directories(path);
|
|
MIJIN_ASSERT(result, "Error creating user folder.");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
MIJIN_ERROR("User folder path shouldn't be empty.");
|
|
}
|
|
};
|
|
createUserDir("/config");
|
|
createUserDir("/data");
|
|
|
|
// in development builds, also add the development folders
|
|
#if !defined(RAID_RELEASE) || (MIJIN_TARGET_OS == MIJIN_OS_WINDOWS)
|
|
addConfigDir(fs::current_path() / "data/config");
|
|
addDataDir(fs::current_path() / "data/data");
|
|
#endif
|
|
|
|
// now also add the system folders
|
|
for (const fs::path& path : mijin::getAllConfigFolders(mijin::IncludeUser::NO))
|
|
{
|
|
addConfigDir(path / getFolderName());
|
|
}
|
|
|
|
for (const fs::path& path : mijin::getAllDataFolders(mijin::IncludeUser::NO))
|
|
{
|
|
addDataDir(path / getFolderName());
|
|
}
|
|
|
|
#if !defined(RAID_RELEASE)
|
|
for (const mijin::PathReference& reference : mFS.getAllPaths("/config"))
|
|
{
|
|
reference.getNativePath().then([&](const fs::path& path)
|
|
{
|
|
msgInfo("Config folder: {}", path.generic_string());
|
|
});
|
|
}
|
|
for (const mijin::PathReference& reference : mFS.getAllPaths("/data"))
|
|
{
|
|
reference.getNativePath().then([&](const fs::path& path)
|
|
{
|
|
msgInfo("Data folder: {}", path.generic_string());
|
|
});
|
|
}
|
|
#endif
|
|
|
|
if (!initSDL())
|
|
{
|
|
return false;
|
|
}
|
|
switch (mConfig.graphicsApi)
|
|
{
|
|
case GraphicsAPI::OPENGL:
|
|
if (!initOpenGL())
|
|
{
|
|
return false;
|
|
}
|
|
break;
|
|
case GraphicsAPI::VULKAN:
|
|
if (!initVulkan())
|
|
{
|
|
return false;
|
|
}
|
|
break;
|
|
}
|
|
if (!initImGui())
|
|
{
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void Application::cleanup()
|
|
{
|
|
if (ImGui::GetCurrentContext() != nullptr)
|
|
{
|
|
saveImGuiConfig();
|
|
|
|
const ImGuiIO& imguiIO = ImGui::GetIO();
|
|
if (imguiIO.BackendRendererUserData != nullptr)
|
|
{
|
|
ImGui_ImplOpenGL3_Shutdown();
|
|
}
|
|
if (imguiIO.BackendPlatformUserData != nullptr)
|
|
{
|
|
ImGui_ImplSDL3_Shutdown();
|
|
}
|
|
ImGui::DestroyContext();
|
|
}
|
|
for (const auto& [_, texture] : mTextures) {
|
|
destroyTexture(texture);
|
|
}
|
|
switch (mConfig.graphicsApi)
|
|
{
|
|
case GraphicsAPI::OPENGL:
|
|
if (gl.context != nullptr)
|
|
{
|
|
SDL_GL_DestroyContext(gl.context);
|
|
}
|
|
break;
|
|
case GraphicsAPI::VULKAN:
|
|
if (vk.device != nullptr)
|
|
{
|
|
vk.DeviceWaitIdle(vk.device);
|
|
vk.DestroyDevice(vk.device, nullptr);
|
|
}
|
|
if (vk.debugUtilsMessenger != nullptr)
|
|
{
|
|
vk.DestroyDebugUtilsMessengerEXT(vk.instance, vk.debugUtilsMessenger, /* pAllocator = */ nullptr);
|
|
}
|
|
if (vk.instance != nullptr)
|
|
{
|
|
vk.DestroyInstance(vk.instance, nullptr);
|
|
}
|
|
SDL_Vulkan_UnloadLibrary();
|
|
break;
|
|
}
|
|
if (mWindow != nullptr)
|
|
{
|
|
SDL_DestroyWindow(mWindow);
|
|
}
|
|
SDL_Quit();
|
|
}
|
|
|
|
bool Application::initSDL()
|
|
{
|
|
#if MIJIN_TARGET_OS == MIJIN_OS_LINUX
|
|
if (mConfig.flags.x11OnWayland) {
|
|
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "x11,wayland");
|
|
}
|
|
#endif
|
|
if (!SDL_Init(SDL_INIT_VIDEO))
|
|
{
|
|
msgError("Error initializing SDL: {}.", SDL_GetError());
|
|
return false;
|
|
}
|
|
|
|
SDL_SetHint(SDL_HINT_QUIT_ON_LAST_WINDOW_CLOSE, "0"); // let us handle ourselves
|
|
|
|
msgInfo("SDL video driver: {}", SDL_GetCurrentVideoDriver());
|
|
|
|
SDL_WindowFlags windowFlags = SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY;
|
|
switch (mConfig.graphicsApi)
|
|
{
|
|
case GraphicsAPI::OPENGL:
|
|
// GL attributes must be set before window creation
|
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
|
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 1);
|
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
|
|
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
|
|
|
|
// TODO: not sure if these really make sense, but they are in the example
|
|
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
|
|
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
|
|
windowFlags |= SDL_WINDOW_OPENGL;
|
|
break;
|
|
case GraphicsAPI::VULKAN:
|
|
if (!SDL_Vulkan_LoadLibrary(nullptr))
|
|
{
|
|
msgError("Error loading Vulkan library: {}.", SDL_GetError());
|
|
return false;
|
|
}
|
|
windowFlags |= SDL_WINDOW_VULKAN;
|
|
break;
|
|
}
|
|
|
|
mWindow = SDL_CreateWindow(
|
|
/* title = */ getWindowTitle().c_str(),
|
|
/* w = */ 1280,
|
|
/* h = */ 720,
|
|
/* flags = */ windowFlags
|
|
);
|
|
return true;
|
|
}
|
|
|
|
bool Application::initOpenGL()
|
|
{
|
|
gl.context = SDL_GL_CreateContext(mWindow);
|
|
if (mWindow == nullptr)
|
|
{
|
|
msgError("Error creating OpenGL context: {}.", SDL_GetError());
|
|
return false;
|
|
}
|
|
SDL_GL_MakeCurrent(mWindow, gl.context);
|
|
SDL_GL_SetSwapInterval(1); // enable vsync, at least for now
|
|
|
|
gl.Clear = reinterpret_cast<glClear_fn_t>(SDL_GL_GetProcAddress("glClear"));
|
|
gl.ClearColor = reinterpret_cast<glClearColor_fn_t>(SDL_GL_GetProcAddress("glClearColor"));
|
|
gl.GenTextures = reinterpret_cast<glGenTextures_fn_t>(SDL_GL_GetProcAddress("glGenTextures"));
|
|
gl.BindTexture = reinterpret_cast<glBindTexture_fn_t>(SDL_GL_GetProcAddress("glBindTexture"));
|
|
gl.TexParameteri = reinterpret_cast<glTexParameteri_fn_t>(SDL_GL_GetProcAddress("glTexParameteri"));
|
|
gl.PixelStorei = reinterpret_cast<glPixelStorei_fn_t>(SDL_GL_GetProcAddress("glPixelStorei"));
|
|
gl.TexImage2D = reinterpret_cast<glTexImage2D_fn_t>(SDL_GL_GetProcAddress("glTexImage2D"));
|
|
gl.DeleteTextures = reinterpret_cast<glDeleteTextures_fn_t>(SDL_GL_GetProcAddress("glDeleteTextures"));
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Application::initVulkan()
|
|
{
|
|
vk.GetInstanceProc = reinterpret_cast<vkGetInstanceProcAddr_fn_t>(SDL_Vulkan_GetVkGetInstanceProcAddr());
|
|
vk.EnumerateInstanceLayerProperties = reinterpret_cast<vkEnumerateInstanceLayerProperties_fn_t>(vk.GetInstanceProc(nullptr, "vkEnumerateInstanceLayerProperties"));
|
|
vk.EnumerateInstanceExtensionProperties = reinterpret_cast<vkEnumerateInstanceExtensionProperties_fn_t>(vk.GetInstanceProc(nullptr, "vkEnumerateInstanceExtensionProperties"));
|
|
vk.CreateInstance = reinterpret_cast<vkCreateInstance_fn_t>(vk.GetInstanceProc(nullptr, "vkCreateInstance"));
|
|
|
|
std::uint32_t numInstanceLayers = 0;
|
|
if (const VkResult result = vk.EnumerateInstanceLayerProperties(&numInstanceLayers, nullptr); result != VK_SUCCESS && result != VK_INCOMPLETE)
|
|
{
|
|
msgError("Error enumerating instance layers: 0x{:x}.", static_cast<unsigned>(result));
|
|
return false;
|
|
}
|
|
std::vector<VkLayerProperties> instanceLayers;
|
|
instanceLayers.resize(numInstanceLayers);
|
|
if (const VkResult result = vk.EnumerateInstanceLayerProperties(&numInstanceLayers, instanceLayers.data()); result != VK_SUCCESS && result != VK_INCOMPLETE)
|
|
{
|
|
msgError("Error enumerating instance layers: 0x{:x}.", static_cast<unsigned>(result));
|
|
return false;
|
|
}
|
|
|
|
std::vector<const char*> enabledInstanceLayers;
|
|
for (const VkLayerProperties& props : instanceLayers) {
|
|
if (std::strcmp(props.layerName, "VK_LAYER_KHRONOS_validation") == 0) {
|
|
enabledInstanceLayers.push_back("VK_LAYER_KHRONOS_validation");
|
|
}
|
|
}
|
|
|
|
std::uint32_t numInstanceExtensions = 0;
|
|
if (const VkResult result = vk.EnumerateInstanceExtensionProperties(nullptr, &numInstanceExtensions, nullptr); result != VK_SUCCESS && result != VK_INCOMPLETE)
|
|
{
|
|
msgError("Error enumerating instance extensions: 0x{:x}.", static_cast<unsigned>(result));
|
|
return false;
|
|
}
|
|
std::vector<VkExtensionProperties> instanceExtensions;
|
|
instanceExtensions.resize(numInstanceExtensions);
|
|
if (const VkResult result = vk.EnumerateInstanceExtensionProperties(nullptr, &numInstanceExtensions, instanceExtensions.data()); result != VK_SUCCESS && result != VK_INCOMPLETE)
|
|
{
|
|
msgError("Error enumerating instance extensions: 0x{:x}.", static_cast<unsigned>(result));
|
|
return false;
|
|
}
|
|
|
|
std::uint32_t numSdlExtensions = 0;
|
|
const char* const* sdlExtensions = SDL_Vulkan_GetInstanceExtensions(&numSdlExtensions);
|
|
std::uint32_t numSupportedSdlExtensions = 0;
|
|
|
|
bool hasDebugUtils = false;
|
|
std::vector<const char*> enabledInstanceExtensions;
|
|
for (const VkExtensionProperties& props : instanceExtensions) {
|
|
if (std::strcmp(props.extensionName, "VK_EXT_debug_utils") == 0)
|
|
{
|
|
enabledInstanceExtensions.push_back("VK_EXT_debug_utils");
|
|
hasDebugUtils = true;
|
|
}
|
|
else
|
|
{
|
|
for (std::uint32_t idx = 0; idx < numSdlExtensions; ++idx)
|
|
{
|
|
if (std::strncmp(props.extensionName, sdlExtensions[idx], VK_MAX_EXTENSION_NAME_SIZE) == 0)
|
|
{
|
|
enabledInstanceExtensions.push_back(sdlExtensions[idx]);
|
|
++numSupportedSdlExtensions;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (numSupportedSdlExtensions != numSdlExtensions)
|
|
{
|
|
msgError("Cannot create Vulkan device, not all required instance extensions are supported.");
|
|
return false;
|
|
}
|
|
|
|
const VkApplicationInfo applicationInfo = {
|
|
.apiVersion = vkMakeApiVersion(0, 1, 3, 0) // TODO: probably should let the user specify this?
|
|
};
|
|
const VkInstanceCreateInfo instanceCreateInfo = {
|
|
.pApplicationInfo = &applicationInfo,
|
|
.enabledLayerCount = static_cast<std::uint32_t>(enabledInstanceLayers.size()),
|
|
.ppEnabledLayerNames = enabledInstanceLayers.data(),
|
|
.enabledExtensionCount = static_cast<std::uint32_t>(enabledInstanceExtensions.size()),
|
|
.ppEnabledExtensionNames = enabledInstanceExtensions.data()
|
|
};
|
|
|
|
if (const VkResult result = vk.CreateInstance(&instanceCreateInfo, nullptr, &vk.instance); result != VK_SUCCESS)
|
|
{
|
|
msgError("Error creating Vulkan instance: 0x{:x}.", static_cast<unsigned>(result));
|
|
return false;
|
|
}
|
|
|
|
vk.DestroyInstance = reinterpret_cast<vkDestroyInstance_fn_t>(vk.GetInstanceProc(vk.instance, "vkDestroyInstance"));
|
|
vk.EnumeratePhysicalDevices = reinterpret_cast<vkEnumeratePhysicalDevices_fn_t>(vk.GetInstanceProc(vk.instance, "vkEnumeratePhysicalDevices"));
|
|
vk.GetPhysicalDeviceQueueFamilyProperties2 = reinterpret_cast<vkGetPhysicalDeviceQueueFamilyProperties2_fn_t>(vk.GetInstanceProc(vk.instance, "vkGetPhysicalDeviceQueueFamilyProperties2"));
|
|
vk.CreateDevice = reinterpret_cast<vkCreateDevice_fn_t>(vk.GetInstanceProc(vk.instance, "vkCreateDevice"));
|
|
vk.DestroyDevice = reinterpret_cast<vkDestroyDevice_fn_t>(vk.GetInstanceProc(vk.instance, "vkDestroyDevice"));
|
|
vk.DeviceWaitIdle = reinterpret_cast<vkDeviceWaitIdle_fn_t>(vk.GetInstanceProc(vk.instance, "vkDeviceWaitIdle"));
|
|
vk.GetDeviceQueue = reinterpret_cast<vkGetDeviceQueue_fn_t>(vk.GetInstanceProc(vk.instance, "vkGetDeviceQueue"));
|
|
|
|
if (hasDebugUtils)
|
|
{
|
|
vk.CreateDebugUtilsMessengerEXT = reinterpret_cast<vkCreateDebugUtilsMessengerEXT_fn_t>(vk.GetInstanceProc(vk.instance, "vkCreateDebugUtilsMessengerEXT"));
|
|
vk.DestroyDebugUtilsMessengerEXT = reinterpret_cast<vkDestroyDebugUtilsMessengerEXT_fn_t>(vk.GetInstanceProc(vk.instance, "vkDestroyDebugUtilsMessengerEXT"));
|
|
|
|
const VkDebugUtilsMessengerCreateInfoEXT messengerCreateInfo = {
|
|
.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT,
|
|
.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,
|
|
.pfnUserCallback = [](VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, VkDebugUtilsMessageTypeFlagsEXT messageTypes, const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData, void* pUserData) {
|
|
return static_cast<Application*>(pUserData)->handleDebugUtilsMessage(messageSeverity, messageTypes, pCallbackData);
|
|
},
|
|
.pUserData = this
|
|
};
|
|
|
|
if (const VkResult result = vk.CreateDebugUtilsMessengerEXT(vk.instance, &messengerCreateInfo, /* pAllocator = */ nullptr, &vk.debugUtilsMessenger); result != VK_SUCCESS)
|
|
{
|
|
msgWarning("Error creating Vulkan debug utils messenger: 0x{:x}.", static_cast<unsigned>(result));
|
|
vk.debugUtilsMessenger = nullptr;
|
|
}
|
|
}
|
|
else {
|
|
vk.debugUtilsMessenger = nullptr;
|
|
}
|
|
|
|
// TODO: this is really cheap... (but should be sufficient in most cases)
|
|
std::uint32_t physicalDeviceCount = 1;
|
|
if (const VkResult result = vk.EnumeratePhysicalDevices(vk.instance, &physicalDeviceCount, &vk.physicalDevice); result != VK_SUCCESS)
|
|
{
|
|
msgError("Error enumerating Vulkan physical devices: 0x{:x}.", static_cast<unsigned>(result));
|
|
return false;
|
|
}
|
|
|
|
std::uint32_t queueFamilyPropertyCount = 0;
|
|
vk.GetPhysicalDeviceQueueFamilyProperties2(vk.physicalDevice, &queueFamilyPropertyCount, nullptr);
|
|
|
|
std::vector<VkQueueFamilyProperties2> queueFamilyProperties;
|
|
queueFamilyProperties.resize(queueFamilyPropertyCount);
|
|
vk.GetPhysicalDeviceQueueFamilyProperties2(vk.physicalDevice, &queueFamilyPropertyCount, queueFamilyProperties.data());
|
|
|
|
// TODO: this should also check for surface support (but who cares?)
|
|
std::uint32_t queueFamilyIndex = 0;
|
|
for (; queueFamilyIndex < queueFamilyPropertyCount; ++queueFamilyIndex)
|
|
{
|
|
const bool supportsGraphics = queueFamilyProperties[queueFamilyIndex].queueFamilyProperties.queueFlags & VK_QUEUE_GRAPHICS_BIT;
|
|
const bool supportsPresent = SDL_Vulkan_GetPresentationSupport(vk.instance, vk.physicalDevice, queueFamilyIndex);
|
|
|
|
if (supportsGraphics && supportsPresent) {
|
|
break;
|
|
}
|
|
}
|
|
if (queueFamilyIndex == queueFamilyPropertyCount)
|
|
{
|
|
msgError("No suitable Vulkan queue family found.");
|
|
return false;
|
|
}
|
|
vk.queueFamilyIndex = queueFamilyIndex;
|
|
|
|
static const float queuePriority = 1.0f;
|
|
const VkDeviceQueueCreateInfo queueCreateInfo = {
|
|
.queueFamilyIndex = vk.queueFamilyIndex,
|
|
.queueCount = 1,
|
|
.pQueuePriorities = &queuePriority
|
|
};
|
|
static const std::array DEVICE_EXTENSIONS = {
|
|
"VK_KHR_swapchain"
|
|
};
|
|
const VkDeviceCreateInfo deviceCreateInfo = {
|
|
.queueCreateInfoCount = 1,
|
|
.pQueueCreateInfos = &queueCreateInfo,
|
|
.enabledExtensionCount = static_cast<std::uint32_t>(DEVICE_EXTENSIONS.size()),
|
|
.ppEnabledExtensionNames = DEVICE_EXTENSIONS.data()
|
|
};
|
|
if (const VkResult result = vk.CreateDevice(vk.physicalDevice, &deviceCreateInfo, nullptr, &vk.device); result != VK_SUCCESS)
|
|
{
|
|
msgError("Error creating Vulkan device: 0x{:x}.", static_cast<unsigned>(result));
|
|
return false;
|
|
}
|
|
vk.GetDeviceQueue(vk.device, /* queueFamilyIndex = */ 0, /* queueIndex = */ 0, &vk.queue);
|
|
|
|
if (!SDL_Vulkan_CreateSurface(mWindow, vk.instance, nullptr, &vk.surface))
|
|
{
|
|
msgError("Error creating SDL Vulkan surface: {}.", SDL_GetError());
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Application::initImGui()
|
|
{
|
|
IMGUI_CHECKVERSION(); // not exactly useful when using static libs, but won't hurt
|
|
|
|
ImGuiContext* imguiContext = ImGui::CreateContext();
|
|
if (imguiContext == nullptr)
|
|
{
|
|
msgError("Error initializing ImGui context.");
|
|
return false;
|
|
}
|
|
|
|
imguiContext->ErrorCallbackUserData = this;
|
|
imguiContext->ErrorCallback = [](ImGuiContext* /* ctx */, void* userData, const char* msg) {
|
|
static_cast<Application*>(userData)->handleSDLError(msg);
|
|
};
|
|
|
|
loadImGuiConfig();
|
|
|
|
configureImgui();
|
|
|
|
ImGuiIO& imguiIO = ImGui::GetIO();
|
|
imguiIO.IniFilename = nullptr; // disable automatic saving of settings
|
|
|
|
// default style
|
|
ImGui::StyleColorsDark();
|
|
|
|
// init the backends
|
|
switch (mConfig.graphicsApi)
|
|
{
|
|
case GraphicsAPI::OPENGL:
|
|
if (!ImGui_ImplSDL3_InitForOpenGL(mWindow, gl.context))
|
|
{
|
|
msgError("Error initializing ImGui SDL3 backend.");
|
|
return false;
|
|
}
|
|
if (!ImGui_ImplOpenGL3_Init(IMGUI_GLSL_VERSION))
|
|
{
|
|
msgError("Error initializing ImGui OpenGL3 backend.");
|
|
return false;
|
|
}
|
|
break;
|
|
case GraphicsAPI::VULKAN:
|
|
MIJIN_TRAP(); // TODO!
|
|
return false;
|
|
}
|
|
|
|
if (imguiIO.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
|
|
{
|
|
if (!(imguiIO.BackendFlags & ImGuiBackendFlags_PlatformHasViewports)) {
|
|
msgWarning("ImGUI viewports enabled, but platform doesn't support them.");
|
|
}
|
|
if (!(imguiIO.BackendFlags & ImGuiBackendFlags_RendererHasViewports)) {
|
|
msgWarning("ImGUI viewports enabled, but renderer doesn't support them.");
|
|
}
|
|
}
|
|
|
|
// init font
|
|
if (imguiIO.Fonts->Fonts.empty())
|
|
{
|
|
if (!loadFonts(getDefaultFonts()))
|
|
{
|
|
imguiIO.Fonts->AddFontDefault();
|
|
}
|
|
}
|
|
|
|
|
|
return true;
|
|
}
|
|
|
|
void Application::handleSDLEvents()
|
|
{
|
|
SDL_Event event;
|
|
while (SDL_PollEvent(&event))
|
|
{
|
|
handleSDLEvent(event);
|
|
}
|
|
}
|
|
|
|
void Application::loadImGuiConfig()
|
|
{
|
|
std::unique_ptr<mijin::Stream> iniFile;
|
|
if (mFS.open("/config/imgui.ini", mijin::FileOpenMode::READ, iniFile) != mijin::StreamError::SUCCESS)
|
|
{
|
|
return;
|
|
}
|
|
std::string config;
|
|
if (iniFile->readAsString(config) != mijin::StreamError::SUCCESS)
|
|
{
|
|
msgWarning("IO error reading ImGui config.");
|
|
return;
|
|
}
|
|
ImGui::LoadIniSettingsFromMemory(config.c_str());
|
|
}
|
|
|
|
void Application::saveImGuiConfig()
|
|
{
|
|
std::unique_ptr<mijin::Stream> iniFile;
|
|
if (const mijin::StreamError error = mFS.open("/config/imgui.ini", mijin::FileOpenMode::WRITE, iniFile);
|
|
error != mijin::StreamError::SUCCESS)
|
|
{
|
|
msgError("Error opening ImGui config file for writing: {}.", mijin::errorName(error));
|
|
return;
|
|
}
|
|
|
|
std::size_t length = 0;
|
|
const char* config = ImGui::SaveIniSettingsToMemory(&length);
|
|
if (const mijin::StreamError error = iniFile->writeText(config); error != mijin::StreamError::SUCCESS)
|
|
{
|
|
msgError("Error writing ImGui config: {}.", mijin::errorName(error));
|
|
}
|
|
}
|
|
|
|
auto Application::handleDebugUtilsMessage(VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity,
|
|
VkDebugUtilsMessageTypeFlagsEXT /* messageTypes */,
|
|
const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData) -> VkBool32
|
|
{
|
|
switch (messageSeverity)
|
|
{
|
|
case VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT:
|
|
msgError("Vulkan debug error message: {}.", pCallbackData->pMessage);
|
|
break;
|
|
case VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT:
|
|
msgWarning("Vulkan debug warning message: {}.", pCallbackData->pMessage);
|
|
break;
|
|
default:
|
|
msgInfo("Vulkan debug message: {}.", pCallbackData->pMessage);
|
|
break;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void QuickApp::preInit(QuickAppOptions options)
|
|
{
|
|
MIJIN_ASSERT_FATAL(options.callbacks.render, "Missing render callback.");
|
|
mRenderCallback = std::move(options.callbacks.render);
|
|
mFolderName = std::move(options.folderName);
|
|
mWindowTitle = std::move(options.windowTitle);
|
|
setMainWindowFlags(options.mainWindowFlags);
|
|
}
|
|
|
|
QuickApp& QuickApp::get()
|
|
{
|
|
MIJIN_ASSERT_FATAL(gAppInstance != nullptr, "No QuickApp is running.");
|
|
return *gAppInstance;
|
|
}
|
|
|
|
void QuickApp::render()
|
|
{
|
|
mRenderCallback();
|
|
}
|
|
|
|
std::string QuickApp::getFolderName()
|
|
{
|
|
return mFolderName;
|
|
}
|
|
|
|
std::string QuickApp::getWindowTitle()
|
|
{
|
|
return mWindowTitle;
|
|
}
|
|
|
|
int runQuick(int argc, char* argv[], QuickAppOptions options)
|
|
{
|
|
QuickApp app;
|
|
gAppInstance = &app;
|
|
app.preInit(std::move(options));
|
|
return app.run(argc, argv);
|
|
}
|
|
}
|