raid/private/raid/application.cpp

629 lines
18 KiB
C++

#include "raid/raid.hpp"
#include <chrono>
#include <thread>
#include <fmt/base.h>
#include <SDL3/SDL.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();
}
}
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();
glClearColor(0.3f, 0.3f, 0.3f, 1.f);
glClear(GL_COLOR_BUFFER_BIT);
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
if (imguiIO.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
{
ImGui::UpdatePlatformWindows();
ImGui::RenderPlatformWindowsDefault();
SDL_GL_MakeCurrent(mWindow, mGLContext);
}
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
GLuint texture = 0;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
// setup texture
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// upload image
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, imageData);
mTextures.emplace(std::move(path), texture);
return texture;
}
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;
default:
ImGui_ImplSDL3_ProcessEvent(&event);
break;
}
}
void Application::handleSDLError(const char* message)
{
msgError("SDL: {}", message);
}
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;
}
if (!initGL())
{
return false;
}
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& [path, texture] : mTextures)
{
const GLuint asUint = static_cast<GLuint>(texture);
glDeleteTextures(1, &asUint);
}
if (mGLContext != nullptr)
{
SDL_GL_DestroyContext(mGLContext);
}
if (mWindow != nullptr)
{
SDL_DestroyWindow(mWindow);
}
SDL_Quit();
}
bool Application::initSDL()
{
#if MIJIN_TARGET_OS == MIJIN_OS_LINUX
// prefer x11 over wayland, as ImGui viewports don't work with wayland
// TODO: this still doesn't work all the time, maybe there will be an update to ImGui in the future?
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "x11,wayland");
#endif
if (!SDL_Init(SDL_INIT_VIDEO))
{
msgError("Error initializing SDL: {}.", SDL_GetError());
return false;
}
msgInfo("SDL video driver: {}", SDL_GetCurrentVideoDriver());
// 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);
const SDL_WindowFlags WINDOW_FLAGS = 0
| SDL_WINDOW_OPENGL
| SDL_WINDOW_RESIZABLE
| SDL_WINDOW_HIGH_PIXEL_DENSITY;
mWindow = SDL_CreateWindow(
/* title = */ getWindowTitle().c_str(),
/* w = */ 1280,
/* h = */ 720,
/* flags = */ WINDOW_FLAGS
);
return true;
}
bool Application::initGL()
{
mGLContext = SDL_GL_CreateContext(mWindow);
if (mWindow == nullptr)
{
msgError("Error creating SDL window: {}.", SDL_GetError());
return false;
}
SDL_GL_MakeCurrent(mWindow, mGLContext);
SDL_GL_SetSwapInterval(1); // enable vsync, at least for now
glClear = reinterpret_cast<glClear_fn_t>(SDL_GL_GetProcAddress("glClear"));
glClearColor = reinterpret_cast<glClearColor_fn_t>(SDL_GL_GetProcAddress("glClearColor"));
glGenTextures = reinterpret_cast<glGenTextures_fn_t>(SDL_GL_GetProcAddress("glGenTextures"));
glBindTexture = reinterpret_cast<glBindTexture_fn_t>(SDL_GL_GetProcAddress("glBindTexture"));
glTexParameteri = reinterpret_cast<glTexParameteri_fn_t>(SDL_GL_GetProcAddress("glTexParameteri"));
glPixelStorei = reinterpret_cast<glPixelStorei_fn_t>(SDL_GL_GetProcAddress("glPixelStorei"));
glTexImage2D = reinterpret_cast<glTexImage2D_fn_t>(SDL_GL_GetProcAddress("glTexImage2D"));
glDeleteTextures = reinterpret_cast<glDeleteTextures_fn_t>(SDL_GL_GetProcAddress("glDeleteTextures"));
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
if (!ImGui_ImplSDL3_InitForOpenGL(mWindow, mGLContext))
{
msgError("Error initializing ImGui SDL3 backend.");
return false;
}
if (!ImGui_ImplOpenGL3_Init(IMGUI_GLSL_VERSION))
{
msgError("Error initializing ImGui OpenGL3 backend.");
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));
}
}
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);
}
}