Compare commits

...

29 Commits

Author SHA1 Message Date
e354e20e54 Implemented/fixed Vulkan instance and device creation. 2025-09-24 00:51:40 +02:00
Patrick Wuttke
d56918e71d Added some utility coroutines for showing message boxes. 2025-09-23 11:14:47 +02:00
Patrick Wuttke
69fdae991a Added handleCloseRequested() callback to allow applications to intercept close requests. 2025-09-23 10:43:48 +02:00
da8d1589b3 Added multi-column sorting to data tables. 2025-09-22 22:19:29 +02:00
Patrick Wuttke
9d02d97af3 Added missing dependency to yaml-cpp and added count parameter to ConfigArray::removeAt(). 2025-09-22 21:45:47 +02:00
Patrick Wuttke
20020318e1 Added configuration helper types. 2025-09-22 17:12:35 +02:00
Patrick Wuttke
9bc56a2748 Added header for ImPlot utility and functions for drawing horizontal/vertical lines and boxes. 2025-09-22 10:39:50 +02:00
5ebd609507 Added first frame (data tables) to the test application. 2025-09-21 23:32:54 +02:00
b38fa1b68b Added utility functions for creating string/value data table columns. 2025-09-21 23:32:35 +02:00
41258cda5b Moved Application declarations to the appropriate header. 2025-09-21 17:44:43 +02:00
ee4ea4ae0a Properly seperated/cleaned up OpenGL and Vulkan implementations. 2025-09-21 17:42:07 +02:00
b35edfcf20 Added ApplicationConfig argument to QuickApp constructor. 2025-09-21 15:26:42 +02:00
180f2b70fa Disable X11 on Wayland unless explicitly activated. 2025-09-21 15:18:02 +02:00
917309e99c Prefer SDL X11 video driver on Linux for ImGui viewport support. 2025-09-20 15:28:08 +02:00
66668959d3 Fixed recipe repository name. 2025-09-20 15:27:29 +02:00
58329a2033 Converted test application from QuickApp to proper sub class. 2025-09-20 15:27:08 +02:00
Patrick Wuttke
0f2f5973fb Merge branch 'master' of https://git.mewin.de/mewin/raid 2025-09-20 14:28:17 +02:00
Patrick Wuttke
6657606e81 Made data table header a const char* to avoid unnecessary allocations. 2025-09-20 14:26:18 +02:00
bc8d241592 Removed Jinja tool due to S++ including Jinja as an addon now. 2025-09-20 12:20:00 +02:00
db09eb5e3a Replaced S++ submodule with loader script. 2025-09-20 11:04:49 +02:00
Patrick Wuttke
d3b56d3fd0 Added DataTable function. 2025-09-19 13:53:42 +02:00
Patrick Wuttke
7470878f9c Added mMainWindowStyles variable to Application. 2025-09-19 13:53:24 +02:00
d74474a042 Use JINJA_FILE_SEARCHPATH variable so the fonts will also be found when built as a library. 2025-03-28 14:53:36 +01:00
71a268b5ad Added Application::initMemoryFS() function to allow apps to init the memory FS before further initialization. 2025-03-28 11:53:29 +01:00
63fcb6181f Added memory FS and put the default font there so it is easier to load it. 2025-03-28 11:48:16 +01:00
6cf748b05d Added license. 2025-03-18 17:20:48 +01:00
Patrick Wuttke
d1addb438c Enable local config and data dirs on Windows, even in release builds. 2025-03-18 09:59:56 +01:00
aef6eb03d2 Fixed RAID_RELEASE macro not defined in release_debug and profile builds. 2025-03-13 23:43:31 +01:00
8d1a7bc957 Fixed compilation warnings due to unused result in release versions. 2025-03-13 10:52:13 +01:00
31 changed files with 2621 additions and 406 deletions

4
.gitignore vendored
View File

@ -28,6 +28,10 @@ compile_commands.json
/config.py
/config_*.py
# Generated files
*.gen.*
!*.gen.*.jinja
# Prerequisites
*.d

3
.gitmodules vendored
View File

@ -1,3 +0,0 @@
[submodule "scons-plus-plus"]
path = external/scons-plus-plus
url = https://git.mewin.de/mewin/scons-plus-plus.git

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Patrick Wuttke
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,10 +1,12 @@
config = {
'PROJECT_NAME': 'RAID Framework',
'CXX_NO_EXCEPTIONS': True
'PROJECT_NAME': 'RAID Framework'
}
env = SConscript('external/scons-plus-plus/SConscript', exports = ['config'])
env.Append(CPPPATH = [Dir('private'), Dir('public')])
# add the default recipe repository
env.RecipeRepo('mewin', 'https://git.mewin.de/mewin/spp_recipes.git', 'stable')
# library
env = env.Module('private/raid/SModule')

View File

@ -3,7 +3,8 @@ Import('env')
public_dir = env.Dir('public')
env.Append(CPPPATH = [public_dir])
if env['BUILD_TYPE'] == 'release':
env.Append(JINJA_FILE_SEARCHPATH = [env.Dir('.')])
if env['BUILD_TYPE'] in ('release', 'release_debug', 'profile'):
env.Append(CPPDEFINES = ['RAID_RELEASE'])
env = env.Module('private/raid/SModule')

View File

@ -13,5 +13,6 @@
"SDL": {
"min": [3,0,0]
},
"stb": {}
"stb": {},
"yaml-cpp": {}
}

@ -1 +0,0 @@
Subproject commit c994752c3244fdf9835b1d3a5238094d2a799855

84
external/scons-plus-plus/SConscript vendored Normal file
View File

@ -0,0 +1,84 @@
"""
SCons++ Bootstrapper
"""
import os
from pathlib import Path
import shutil
import subprocess
import sys
from SCons.Environment import Environment
Import('config')
_SPP_FOLDER_NAME = 'scons-plus-plus'
_SPP_DEFAULT_REPOSITORY = 'https://git.mewin.de/mewin/scons-plus-plus.git'
_SPP_DEFAULT_BRANCH = 'master'
spp_root: Path
spp_repository: str
spp_branch: str
def _main() -> Environment:
global spp_root, spp_repository, spp_branch
spp_root = config.get('SPP_ROOT')
if spp_root is None:
spp_root = _get_default_spp_root()
elif not isinstance(spp_root, Path):
spp_root = Path(str(spp_root))
spp_root = spp_root.absolute()
spp_repository = config.get('SPP_REPOSITORY', _SPP_DEFAULT_REPOSITORY)
spp_branch = config.get('SPP_BRANCH', _SPP_DEFAULT_BRANCH)
_printinfo(f'Using SCons++ root at: {spp_root}')
if not spp_root.exists():
_printinfo('SCons++ does not yet exist, downloading it.')
_install_spp()
spp_script = spp_root / 'SConscript'
if not spp_script.exists():
_printerr(f'SCons++ main script not found at {spp_script}!')
sys.exit(1)
return SConscript(spp_script, exports=['config'])
def _get_default_spp_root() -> Path:
if os.name == 'posix':
# follow XDG specification -> first try $XDG_DATA_HOME, then $HOME/.local/share
data_home = os.environ.get('XDG_DATA_HOME')
if data_home is not None:
return Path(data_home, _SPP_FOLDER_NAME)
home = os.environ.get('HOME')
if home is not None:
return Path(home, '.local', 'share', _SPP_FOLDER_NAME)
elif os.name == 'nt':
# just use LocalAppData, which should always be set on Windows
return Path(os.environ['LocalAppData'], _SPP_FOLDER_NAME)
_printinfo(f'Could not detect SCons++ root directory, falling back to ./{_SPP_FOLDER_NAME}.')
return Path(_SPP_FOLDER_NAME)
def _install_spp() -> None:
git_exe = shutil.which('git')
if git_exe is None:
_printerr('No git executable found, cannot install SCons++.')
sys.exit(1)
_exec_checked((git_exe, 'clone', '-b', spp_branch, '--progress', spp_repository, spp_root))
def _exec_checked(args: Sequence[str], **kwargs) -> None:
subprocess.run(args, stdout=sys.stdout, stderr=sys.stderr, check=True, **kwargs)
if not GetOption('silent'):
_printinfo = print
else:
def _printinfo(*args): ...
def _printerr(*args) -> None:
print(*args, file=sys.stderr)
env = _main()
Return('env')

View File

@ -3,11 +3,19 @@ import json
Import('env')
if not hasattr(env, 'Jinja'):
env.Error('RAID requires Jinja.')
src_files = Split("""
application.cpp
config.cpp
fonts.gen.cpp
stb_image.cpp
""")
env.Jinja("fonts.gen.cpp")
env.Jinja("fonts.gen.hpp")
with open('../../dependencies.json', 'r') as f:
dependencies = env.DepsFromJson(json.load(f))

View File

@ -1,10 +1,12 @@
#include "raid/raid.hpp"
#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>
@ -12,9 +14,11 @@
#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
{
@ -59,6 +63,12 @@ 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)
@ -95,7 +105,15 @@ int Application::run(int argc, char** argv)
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();
@ -103,15 +121,26 @@ int Application::run(int argc, char** argv)
ImGui::Render();
glClearColor(0.3f, 0.3f, 0.3f, 1.f);
glClear(GL_COLOR_BUFFER_BIT);
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
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();
SDL_GL_MakeCurrent(mWindow, mGLContext);
if (mConfig.graphicsApi == GraphicsAPI::OPENGL) {
SDL_GL_MakeCurrent(mWindow, gl.context);
}
}
SDL_GL_SwapWindow(mWindow);
@ -212,23 +241,60 @@ ImTextureID Application::getOrLoadTexture(fs::path path)
}
// create texture
GLuint texture = 0;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, 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
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// 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
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, imageData);
// 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);
mTextures.emplace(std::move(path), texture);
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();
@ -241,10 +307,15 @@ void Application::configureImgui()
std::vector<FontConfig> Application::getDefaultFonts()
{
return {{
.path = "/data/fonts/NotoSans-Regular.ttf"
.path = DEFAULT_FONT_PATH
}};
}
void Application::initMemoryFS()
{
mMemoryFS->addFile(DEFAULT_FONT_PATH, NOTO_SANS_DATA);
}
void Application::handleMessage(const Message& message)
{
switch (message.severity)
@ -268,12 +339,29 @@ void Application::handleSDLEvent(const SDL_Event& event)
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)
@ -294,6 +382,9 @@ bool Application::init()
);
};
mMemoryFS = mFS.emplaceAdapter<mijin::MemoryFileSystemAdapter>();
initMemoryFS();
addConfigDir(mijin::getKnownFolder(mijin::KnownFolder::USER_CONFIG_ROOT) / getFolderName());
addDataDir(mijin::getKnownFolder(mijin::KnownFolder::USER_DATA_ROOT) / getFolderName());
@ -305,7 +396,7 @@ bool Application::init()
const fs::path path = std::move(*pathOpt);
if (!fs::exists(path))
{
const bool result = fs::create_directories(path);
[[maybe_unused]] const bool result = fs::create_directories(path);
MIJIN_ASSERT(result, "Error creating user folder.");
}
}
@ -318,7 +409,7 @@ bool Application::init()
createUserDir("/data");
// in development builds, also add the development folders
#if !defined(RAID_RELEASE)
#if !defined(RAID_RELEASE) || (MIJIN_TARGET_OS == MIJIN_OS_WINDOWS)
addConfigDir(fs::current_path() / "data/config");
addDataDir(fs::current_path() / "data/data");
#endif
@ -355,9 +446,20 @@ bool Application::init()
{
return false;
}
if (!initGL())
switch (mConfig.graphicsApi)
{
return false;
case GraphicsAPI::OPENGL:
if (!initOpenGL())
{
return false;
}
break;
case GraphicsAPI::VULKAN:
if (!initVulkan())
{
return false;
}
break;
}
if (!initImGui())
{
@ -383,14 +485,33 @@ void Application::cleanup()
}
ImGui::DestroyContext();
}
for (const auto& [path, texture] : mTextures)
{
const GLuint asUint = static_cast<GLuint>(texture);
glDeleteTextures(1, &asUint);
for (const auto& [_, texture] : mTextures) {
destroyTexture(texture);
}
if (mGLContext != nullptr)
switch (mConfig.graphicsApi)
{
SDL_GL_DestroyContext(mGLContext);
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)
{
@ -401,54 +522,260 @@ void Application::cleanup()
bool Application::initSDL()
{
if (!SDL_Init(0))
#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;
}
// 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);
SDL_SetHint(SDL_HINT_QUIT_ON_LAST_WINDOW_CLOSE, "0"); // let us handle ourselves
// 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);
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;
}
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
/* flags = */ windowFlags
);
return true;
}
bool Application::initGL()
bool Application::initOpenGL()
{
mGLContext = SDL_GL_CreateContext(mWindow);
gl.context = SDL_GL_CreateContext(mWindow);
if (mWindow == nullptr)
{
msgError("Error creating SDL window: {}.", SDL_GetError());
msgError("Error creating OpenGL context: {}.", SDL_GetError());
return false;
}
SDL_GL_MakeCurrent(mWindow, mGLContext);
SDL_GL_MakeCurrent(mWindow, gl.context);
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"));
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;
}
@ -457,12 +784,18 @@ bool Application::initImGui()
{
IMGUI_CHECKVERSION(); // not exactly useful when using static libs, but won't hurt
if (ImGui::CreateContext() == nullptr)
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();
@ -474,15 +807,33 @@ bool Application::initImGui()
ImGui::StyleColorsDark();
// init the backends
if (!ImGui_ImplSDL3_InitForOpenGL(mWindow, mGLContext))
switch (mConfig.graphicsApi)
{
msgError("Error initializing ImGui SDL3 backend.");
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 (!ImGui_ImplOpenGL3_Init(IMGUI_GLSL_VERSION))
if (imguiIO.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
{
msgError("Error initializing ImGui OpenGL3 backend.");
return false;
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
@ -541,6 +892,25 @@ void Application::saveImGuiConfig()
}
}
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.");

367
private/raid/config.cpp Normal file
View File

@ -0,0 +1,367 @@
#include "raid/config.hpp"
#include <mijin/async/coroutine_sleep.hpp>
#include <mijin/io/stlstream.hpp>
#include <mijin/util/string.hpp>
#include <mijin/util/variant.hpp>
#include <yaml-cpp/yaml.h>
template<>
struct YAML::convert<raid::ConfigSection>
{
static Node encode(const raid::ConfigSection& section)
{
Node node;
for (const auto& [key, value] : section.getValues()) {
node[key] = value;
}
return node;
}
static bool decode(const Node& node, raid::ConfigSection& section)
{
if (!node.IsMap()) {
return false;
}
mijin::VectorMap<std::string, raid::ConfigValue> values;
for (YAML::const_iterator it = node.begin(); it != node.end(); ++it) {
values.emplace(it->first.as<std::string>(), it->second.as<raid::ConfigValue>());
}
section = raid::ConfigSection(std::move(values));
return true;
}
};
template<>
struct YAML::convert<raid::ConfigArray>
{
static Node encode(const raid::ConfigArray& array)
{
Node node;
for (const raid::ConfigValue& value : array.getValues()) {
node.push_back(value);
}
return node;
}
static bool decode(const Node& node, raid::ConfigArray& array)
{
if (!node.IsSequence()) {
return false;
}
std::vector<raid::ConfigValue> values;
for (const YAML::Node& value : node) {
values.push_back(value.as<raid::ConfigValue>());
}
array = raid::ConfigArray(std::move(values));
return true;
}
};
template<>
struct YAML::convert<raid::ConfigValue>
{
static Node encode(const raid::ConfigValue& value)
{
Node node;
value.visit([&]<typename T>(const T& content) {
if constexpr (!std::is_same_v<T, std::nullptr_t>) {
node = content;
}
else {
node = {};
}
});
return node;
}
static bool decode(const Node& node, raid::ConfigValue& value)
{
switch (node.Type())
{
case YAML::NodeType::Null:
case YAML::NodeType::Undefined:
value = {};
break;
case YAML::NodeType::Sequence:
value = node.as<raid::ConfigArray>();
break;
case YAML::NodeType::Map:
value = node.as<raid::ConfigSection>();
break;
case YAML::NodeType::Scalar:
try
{
value = node.as<raid::config_int_t>();
break;
}
catch(const YAML::Exception&) {} // NOLINT(bugprone-empty-catch)
try
{
value = node.as<double>();
}
catch(const YAML::Exception&) {} // NOLINT(bugprone-empty-catch)
value = node.as<std::string>();
break;
default:
return false;
}
return true;
}
};
namespace raid
{
namespace
{
const ConfigValue EMPTY_VALUE;
}
const ConfigValue& ConfigSection::operator[](std::string_view key) const noexcept
{
auto it = mValues.find(key);
if (it == mValues.end()) {
return EMPTY_VALUE;
}
return it->second;
}
void ConfigArray::append(ConfigValue value)
{
mValues.push_back(std::move(value));
}
void ConfigArray::setAt(std::size_t idx, ConfigValue value)
{
mValues[idx] = std::move(value);
}
void ConfigArray::removeAt(std::size_t idx, std::size_t count)
{
auto itStart = mValues.begin() + static_cast<std::ptrdiff_t>(idx);
auto itEnd = itStart + static_cast<std::ptrdiff_t>(count);
mValues.erase(itStart, itEnd);
}
ConfigValue& ConfigSection::getOrAdd(std::string_view key)
{
return mValues[key];
}
void ConfigSection::set(std::string_view key, ConfigValue value)
{
mValues[key] = std::move(value);
}
bool ConfigValue::asBool() const noexcept
{
return visit(mijin::Visitor{
[](std::nullptr_t) { return false; },
[](bool boolValue) { return boolValue; },
[](config_int_t intValue) { return intValue != 0; },
[](double doubleValue) { return doubleValue != 0.0; },
[](const std::string& stringValue) { return stringValue == "true"; },
[](const ConfigArray&) { return false; },
[](const ConfigSection&) { return false; }
});
}
config_int_t ConfigValue::asInt() const noexcept
{
return visit(mijin::Visitor{
[](std::nullptr_t) { return config_int_t(0); },
[](bool boolValue) { return config_int_t(boolValue); },
[](config_int_t intValue) { return intValue; },
[](double doubleValue) { return static_cast<config_int_t>(doubleValue); },
[](const std::string& stringValue) {
config_int_t intValue = 0;
if (mijin::toNumber(stringValue, intValue)) {
return intValue;
}
return config_int_t(0);
},
[](const ConfigArray&) { return config_int_t(0); },
[](const ConfigSection&) { return config_int_t(0); }
});
}
double ConfigValue::asDouble() const noexcept
{
return visit(mijin::Visitor{
[](std::nullptr_t) { return 0.0; },
[](bool boolValue) { return double(boolValue); },
[](config_int_t intValue) { return static_cast<double>(intValue); },
[](double doubleValue) { return doubleValue; },
[](const std::string& stringValue) {
double doubleValue = 0;
if (mijin::toNumber(stringValue, doubleValue)) {
return doubleValue;
}
return 0.0;
},
[](const ConfigArray&) { return 0.0; },
[](const ConfigSection&) { return 0.0; }
});
}
const std::string& ConfigValue::asString() const noexcept
{
static const std::string TRUE = "true";
static const std::string FALSE = "false";
static const std::string EMPTY{};
static thread_local std::string convertBuffer;
return visit(mijin::Visitor{
[](std::nullptr_t) -> const std::string& { return EMPTY; },
[](bool boolValue) -> const std::string& { return boolValue ? TRUE : FALSE; },
[](config_int_t intValue) -> const std::string& {
convertBuffer = std::to_string(intValue);
return convertBuffer;
},
[](double doubleValue) -> const std::string& {
convertBuffer = std::to_string(doubleValue);
return convertBuffer;
},
[](const std::string& stringValue) -> const std::string& {
return stringValue; // NOLINT(bugprone-return-const-ref-from-parameter)
},
[](const ConfigArray&) -> const std::string& { return EMPTY; },
[](const ConfigSection&) -> const std::string& { return EMPTY; }
});
}
const ConfigArray& ConfigValue::asArray() const noexcept
{
static const ConfigArray EMPTY;
if (isArray()) {
return std::get<ConfigArray>(mContent);
}
return EMPTY;
}
ConfigSection& ConfigValue::asMutableSection() noexcept
{
MIJIN_ASSERT(isSection(), "Cannot call this on non-section values!");
return std::get<ConfigSection>(mContent);
}
const ConfigSection& ConfigValue::asSection() const noexcept
{
static const ConfigSection EMPTY;
if (isSection()) {
return std::get<ConfigSection>(mContent);
}
return EMPTY;
}
const ConfigValue& FileConfig::getValue(std::string_view path) const noexcept
{
const ConfigSection* section = &mRoot;
while(true)
{
MIJIN_ASSERT(!path.empty(), "Invalid config value path.");
const std::string_view::size_type pos = path.find('/');
if (pos == std::string_view::npos) {
break;
}
section = &(*section)[path.substr(0, pos)].asSection();
path = path.substr(pos + 1);
}
return (*section)[path];
}
void FileConfig::setValue(std::string_view path, ConfigValue value) noexcept
{
ConfigSection* section = &mRoot;
while (true)
{
MIJIN_ASSERT(!path.empty(), "Invalid config value path.");
const std::string_view::size_type pos = path.find('/');
if (pos == std::string_view::npos) {
break;
}
ConfigValue& existing = section->getOrAdd(path.substr(0, pos));
if (existing.isUndefined()) {
existing = ConfigSection();
}
else if (!existing.isSection())
{
MIJIN_ERROR("Value already exists, but is not a section.");
return;
}
section = &existing.asMutableSection();
path = path.substr(pos + 1);
}
section->set(path, std::move(value));
mDirty = true;
}
mijin::Result<> FileConfig::init(mijin::PathReference path)
{
mPath = std::move(path);
if (mPath.getInfo().exists) {
return load();
}
return {};
}
mijin::Result<> FileConfig::load()
{
std::unique_ptr<mijin::Stream> stream;
if (const mijin::StreamError result = mPath.open(mijin::FileOpenMode::READ, stream); result != mijin::StreamError::SUCCESS) {
return mijin::ResultError(mijin::errorName(result));
}
mijin::IOStreamAdapter streamAdapter(*stream);
YAML::Node root;
try {
root = YAML::Load(streamAdapter);
}
catch(const YAML::Exception& exc) {
return mijin::ResultError(exc.what());
}
if (!root.IsMap()) {
return mijin::ResultError("invalid config file, expected a map");
}
try {
mRoot = root.as<ConfigSection>();
}
catch(const YAML::Exception& exc) {
return mijin::ResultError(exc.what());
}
return {};
}
mijin::Result<> FileConfig::save(bool force)
{
if (!force && !mDirty) {
return {};
}
mDirty = false;
std::unique_ptr<mijin::Stream> stream;
if (const mijin::StreamError result = mPath.open(mijin::FileOpenMode::WRITE, stream); result != mijin::StreamError::SUCCESS) {
return mijin::ResultError(mijin::errorName(result));
}
YAML::Emitter emitter;
emitter << YAML::Node(mRoot);
if (const mijin::StreamError result = stream->writeText(emitter.c_str()); result != mijin::StreamError::SUCCESS) {
return mijin::ResultError(mijin::errorName(result));
}
return {};
}
}

View File

@ -0,0 +1,7 @@
#include "./fonts.gen.hpp"
namespace raid
{
extern const std::array<std::uint8_t, NOTO_SANS_SIZE> NOTO_SANS_DATA = { {{ file_content_hex('res/fonts/NotoSans-Regular.ttf') }} };
}

View File

@ -0,0 +1,16 @@
#pragma once
#if !defined(RAID_PRIVATE_RAID_FONTS_GEN_HPP_INCLUDED)
#define RAID_PRIVATE_RAID_FONTS_GEN_HPP_INCLUDED 1
#include <array>
#include <cstdint>
namespace raid
{
inline constexpr std::size_t NOTO_SANS_SIZE = {{ file_size('res/fonts/NotoSans-Regular.ttf' )}};
extern const std::array<std::uint8_t, NOTO_SANS_SIZE> NOTO_SANS_DATA;
} // namespace raid
#endif // !defined(RAID_PRIVATE_RAID_FONTS_GEN_HPP_INCLUDED)

View File

@ -3,6 +3,10 @@ Import('env')
src_files = Split("""
main.cpp
application.cpp
frames/config.cpp
frames/data_table.cpp
""")
prog_app = env.UnityProgram(

View File

@ -0,0 +1,87 @@
#include "raid_test/application.hpp"
namespace raid_test
{
Application gApplication;
bool Application::init()
{
if (!raid::Application::init()) {
return false;
}
setMainWindowFlags(raid::DEFAULT_MAIN_WINDOW_FLAGS | ImGuiWindowFlags_MenuBar | ImGuiWindowFlags_NoDocking);
setMainWindowStyle(ImGuiStyleVar_WindowPadding, ImVec2());
setMainWindowStyle(ImGuiStyleVar_WindowBorderSize, 0.f);
std::ranges::fill(mFrameOpen, true);
if (const mijin::Result<> result = mConfig.init(getFS().getPath("/config/persistent.yml")); !result.isSuccess()) {
msgError("Error initializing config: {}.", result.getError().message);
}
return true;
}
void Application::configureImgui()
{
raid::Application::configureImgui();
ImGuiIO& imguiIO = ImGui::GetIO();
imguiIO.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
}
void Application::render()
{
if (const mijin::Result<> result = mConfig.save(); !result.isSuccess()) {
msgError("Error while saving config: {}.", result.getError().message);
}
if (ImGui::BeginMenuBar())
{
if (ImGui::BeginMenu("File"))
{
if (ImGui::MenuItem("Quit"))
{
requestQuit();
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Frames"))
{
for (std::size_t idx = 0; idx < NUM_FRAMES; ++idx) {
ImGui::MenuItem(FRAMES[idx].title, nullptr, &mFrameOpen[idx]);
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Debug"))
{
ImGui::MenuItem("ImGui Metrics", nullptr, &mShowMetrics);
ImGui::EndMenu();
}
ImGui::EndMenuBar();
}
const ImGuiID dockID = ImGui::GetID(this);
ImGui::DockSpace(dockID);
if (mShowMetrics) {
ImGui::ShowMetricsWindow(&mShowMetrics);
}
for (std::size_t idx = 0; idx < NUM_FRAMES; ++idx)
{
ImGui::SetNextWindowDockID(dockID, ImGuiCond_FirstUseEver);
if (mFrameOpen[idx]) {
FRAMES[idx].render(mFrameOpen[idx]);
}
}
}
std::string Application::getFolderName()
{
return "raid_test_app";
}
std::string Application::getWindowTitle()
{
return "RAID Test Application";
}
} // namespace raid_test

View File

@ -0,0 +1,46 @@
#pragma once
#if !defined(RAID_TEST_APPLICATION_HPP_INCLUDED)
#define RAID_TEST_APPLICATION_HPP_INCLUDED 1
#include <array>
#include "raid/raid.hpp"
#include "./frames/config.hpp"
#include "./frames/data_table.hpp"
namespace raid_test
{
class Application : public raid::Application
{
private:
struct Frame
{
using render_fn_t = void (*)(bool& open);
const char* title;
render_fn_t render;
};
static constexpr Frame FRAMES[] = {
{.title = CONFIG_TITLE, .render = &renderConfig},
{.title = DATA_TABLE_TITLE, .render = &renderDataTable}
};
static constexpr std::size_t NUM_FRAMES = sizeof(FRAMES) / sizeof(FRAMES[0]);
raid::FileConfig mConfig;
bool mShowMetrics = false;
std::array<bool, NUM_FRAMES> mFrameOpen{};
public:
[[nodiscard]]
raid::FileConfig& getConfig() noexcept { return mConfig; }
protected:
bool init() override;
void configureImgui() override;
void render() override;
std::string getFolderName() override;
std::string getWindowTitle() override;
};
extern Application gApplication;
} // namespace raid_test
#endif // !defined(RAID_TEST_APPLICATION_HPP_INCLUDED)

View File

@ -0,0 +1,26 @@
#include "raid_test/frames/config.hpp"
#include <imgui.h>
#include "raid/config.hpp"
#include "raid_test/application.hpp"
namespace raid_test
{
void renderConfig(bool& open)
{
if (!ImGui::Begin(CONFIG_TITLE, &open))
{
ImGui::End();
return;
}
static constexpr const char* TEST_BOOL_PATH = "test/section/bool";
bool testBool = gApplication.getConfig().getValue(TEST_BOOL_PATH).asBool();
if (ImGui::Checkbox("Test Bool", &testBool)) {
gApplication.getConfig().setValue(TEST_BOOL_PATH, testBool);
}
ImGui::End();
}
}

View File

@ -0,0 +1,14 @@
#pragma once
#if !defined(RAID_TEST_FRAMES_CONFIG_HPP_INCLUDED)
#define RAID_TEST_FRAMES_CONFIG_HPP_INCLUDED 1
namespace raid_test
{
inline constexpr const char* CONFIG_TITLE = "Config";
void renderConfig(bool& open);
} // namespace raid_test
#endif // !defined(RAID_TEST_FRAMES_CONFIG_HPP_INCLUDED)

View File

@ -0,0 +1,90 @@
#include "raid_test/frames/data_table.hpp"
#include <array>
#include <chrono>
#include <random>
#include <imgui.h>
#include "raid/imraid.hpp"
namespace raid_test
{
namespace
{
const std::array FIRST_NAMES = {
"Emma", "Hannah", "Mia", "Leonie", "Lina", "Marie", "Sophia", "Charlotte", "Paula", "Greta", "Frieda", "Ella", "Freia",
"Leon", "Paul", "Maximilian", "Ben", "Lukas", "Finn", "Fiete", "Felix", "Moritz", "Jakob", "Tim", "Emil", "Theo",
"James", "Mary", "Michael", "Patricia", "John", "Jennifer", "Robert", "Linda", "David", "Elizabeth", "William",
"Barbara", "Richard", "Susan", "Joseph", "Jessica", "Thomas", "Karen", "Christopher", "Sarah"
};
const std::array LAST_NAMES = {
"Müller", "Schmidt", "Schneider", "Fischer", "Meyer", "Weber", "Hofmann", "Wagner", "Becker", "Schulz", "Schäfer",
"Koch", "Bauer", "Richter", "Klein", "Schröder", "Wolf", "Neumann", "Schwarz", "Schmitz", "Smith", "Johnson",
"Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodríguez", "Martínez", "Hernández", "López", "Gonzalez",
"Wilson", "Anderson", "Thomas", "Taylor", "Moore", "Jackson", "Martin"
};
constexpr int MIN_AGE = 18;
constexpr int MAX_AGE = 120;
constexpr double HEIGHT_MEAN = 1.75;
constexpr double HEIGHT_STDDEV = 0.1;
constexpr std::size_t NUM_PEOPLE = 5000;
struct Person
{
const char* firstName;
const char* lastName;
int age;
double height;
};
using rand_int_t = std::mt19937::result_type;
std::mt19937 gRandom(std::random_device{}());
std::uniform_int_distribution<rand_int_t> gFirstNameDistribution(0, static_cast<rand_int_t>(FIRST_NAMES.size()-1));
std::uniform_int_distribution<rand_int_t> gLastNameDistribution(0, static_cast<rand_int_t>(LAST_NAMES.size()-1));
std::uniform_int_distribution<rand_int_t> gAgeDistribution(static_cast<rand_int_t>(MIN_AGE), static_cast<rand_int_t>(MAX_AGE));
std::normal_distribution gHeightDistribution(HEIGHT_MEAN, HEIGHT_STDDEV);
[[nodiscard]]
Person randomPerson()
{
return {
.firstName = FIRST_NAMES[gFirstNameDistribution(gRandom)],
.lastName = LAST_NAMES[gLastNameDistribution(gRandom)],
.age = static_cast<int>(gAgeDistribution(gRandom)),
.height = gHeightDistribution(gRandom)
};
}
const std::array<Person, NUM_PEOPLE> PEOPLE = []()
{
std::array<Person, NUM_PEOPLE> result;
std::ranges::generate(result, randomPerson);
return result;
}();
const std::array DATA_TABLE_COLUMNS = {
ImRaid::MakeStringColumn("First Name", &Person::firstName),
ImRaid::MakeStringColumn("Last Name", &Person::lastName),
ImRaid::MakeColumn("Age", "%d years", &Person::age),
ImRaid::MakeColumn("Height", "%.2f m", &Person::height)
};
const ImRaid::DataTableOptions<Person> DATA_TABLE_OPTIONS = {
.columns = DATA_TABLE_COLUMNS
};
ImRaid::DataTableState gDataTableState;
}
void renderDataTable(bool& open)
{
if (!ImGui::Begin(DATA_TABLE_TITLE, &open))
{
ImGui::End();
return;
}
ImRaid::DataTable("people", DATA_TABLE_OPTIONS, PEOPLE, gDataTableState);
ImGui::End();
}
} // namespace raid_test

View File

@ -0,0 +1,14 @@
#pragma once
#if !defined(RAID_TEST_FRAMES_DATA_TABLE_HPP_INCLUDED)
#define RAID_TEST_FRAMES_DATA_TABLE_HPP_INCLUDED 1
namespace raid_test
{
inline constexpr const char* DATA_TABLE_TITLE = "Data Table";
void renderDataTable(bool& open);
} // namespace raid_test
#endif // !defined(RAID_TEST_FRAMES_DATA_TABLE_HPP_INCLUDED)

View File

@ -1,40 +1,7 @@
#include "raid/raid.hpp"
#include <imgui.h>
namespace
{
void render()
{
if (ImGui::BeginMenuBar())
{
if (ImGui::BeginMenu("File"))
{
if (ImGui::MenuItem("Quit"))
{
raid::QuickApp::get().requestQuit();
}
ImGui::EndMenu();
}
ImGui::EndMenuBar();
}
ImGui::Text("hi");
ImGui::Begin("Test");
ImGui::Text("Test Content");
ImGui::End();
}
}
#include "./application.hpp"
int main(int argc, char* argv[])
{
return raid::runQuick(argc, argv, {
.callbacks = {
.render = &render
},
.folderName = "raid_test_app",
.windowTitle = "RAID Test App",
.mainWindowFlags = raid::DEFAULT_MAIN_WINDOW_FLAGS | ImGuiWindowFlags_MenuBar
});
return raid_test::gApplication.run(argc, argv);
}

257
public/raid/application.hpp Normal file
View File

@ -0,0 +1,257 @@
#pragma once
#if !defined(RAID_APPLICATION_HPP_INCLUDED)
#define RAID_APPLICATION_HPP_INCLUDED 1
#include <cstdint>
#include <functional>
#include <variant>
#include <fmt/format.h>
#include <imgui.h>
#include <mijin/async/coroutine.hpp>
#include <mijin/util/bitflags.hpp>
#include <mijin/virtual_filesystem/memory.hpp>
#include <mijin/virtual_filesystem/stacked.hpp>
#include <SDL3/SDL.h>
#include <SDL3/SDL_vulkan.h>
#include "./internal/opengl.hpp"
#include "./internal/vulkan.hpp"
namespace raid
{
inline constexpr int ERR_INIT_FAILED = 100;
inline constexpr ImGuiWindowFlags DEFAULT_MAIN_WINDOW_FLAGS = 0
| ImGuiWindowFlags_NoBackground
| ImGuiWindowFlags_NoDecoration
| ImGuiWindowFlags_NoBringToFrontOnFocus
| ImGuiWindowFlags_NoNav;
inline constexpr const char* DEFAULT_FONT_PATH = "/data/fonts/NotoSans-Regular.ttf";
enum class MessageSeverity : unsigned char
{
INFO,
WARNING,
ERROR
};
struct Message
{
MessageSeverity severity;
const char* text;
};
struct FontFlags : mijin::BitFlags<FontFlags>
{
bool pixelSnapH : 1 = false;
};
struct FontConfig
{
fs::path path;
std::vector<std::pair<ImWchar, ImWchar>> glyphRanges;
float size = 20.f;
FontFlags flags;
};
struct ApplicationFlags : mijin::BitFlags<ApplicationFlags>
{
/**
* (Linux only) prefer X11 even when on Wayland. Required for multi-viewport support, but currently really buggy.
* \see https://github.com/ocornut/imgui/issues/8609
* \see https://github.com/ocornut/imgui/issues/8587
*/
bool x11OnWayland : 1 = false;
};
enum class GraphicsAPI : std::uint8_t
{
OPENGL,
VULKAN
};
struct ApplicationConfig
{
ApplicationFlags flags = {};
GraphicsAPI graphicsApi = GraphicsAPI::OPENGL;
};
class Application : private MixinOpenGLApplication, MixinVulkanApplication
{
private:
SDL_Window* mWindow = nullptr;
mijin::StackedFileSystemAdapter mFS;
mijin::MemoryFileSystemAdapter* mMemoryFS = nullptr;
mijin::SimpleTaskLoop mTaskLoop;
std::unordered_map<fs::path, ImTextureID> mTextures;
bool mRunning = true;
ImGuiWindowFlags mMainWindowFlags = DEFAULT_MAIN_WINDOW_FLAGS;
std::unordered_map<ImGuiStyleVar, std::variant<float, ImVec2>> mMainWindowStyles;
const ApplicationConfig mConfig;
union
{
OpenGLData gl;
VulkanData vk;
};
public:
explicit Application(ApplicationConfig config = {}) noexcept : mConfig(config) {}
virtual ~Application() = default;
[[nodiscard]]
const ApplicationConfig& getConfig() const noexcept { return mConfig; }
[[nodiscard]]
mijin::StackedFileSystemAdapter& getFS() { return mFS; }
[[nodiscard]]
mijin::MemoryFileSystemAdapter& getMemoryFS()
{
MIJIN_ASSERT_FATAL(mMemoryFS != nullptr, "Memory FS has not been initialized yet.");
return *mMemoryFS;
}
[[nodiscard]]
mijin::SimpleTaskLoop& getLoop() { return mTaskLoop; }
[[nodiscard]]
ImGuiWindowFlags getMainWindowFlags() const { return mMainWindowFlags; }
void setMainWindowFlags(ImGuiWindowFlags flags) { mMainWindowFlags = flags; }
void setMainWindowStyle(ImGuiStyleVar variable, std::variant<float, ImVec2> value) { mMainWindowStyles.emplace(variable, value); }
void unsetMainWindowStyle(ImGuiStyleVar variable) { mMainWindowStyles.erase(variable); }
void requestQuit() { mRunning = false; }
[[nodiscard]]
int run(int argc, char* argv[]);
[[nodiscard]]
bool loadFonts(std::span<const FontConfig> fonts);
[[nodiscard]]
bool loadFont(const FontConfig& font)
{
return loadFonts({&font, 1});
}
[[nodiscard]]
ImTextureID getOrLoadTexture(fs::path path);
void destroyTexture(ImTextureID texture);
protected:
virtual void render() = 0;
virtual std::string getFolderName() = 0;
virtual std::string getWindowTitle() = 0;
virtual void configureImgui();
virtual std::vector<FontConfig> getDefaultFonts();
virtual void initMemoryFS();
virtual void handleMessage(const Message& message);
virtual void handleSDLEvent(const SDL_Event& event);
virtual void handleSDLError(const char* message);
virtual void handleCloseRequested();
void msgInfo(const char* text)
{
handleMessage({
.severity = MessageSeverity::INFO,
.text = text
});
}
void msgWarning(const char* text)
{
handleMessage({
.severity = MessageSeverity::WARNING,
.text = text
});
}
void msgError(const char* text)
{
handleMessage({
.severity = MessageSeverity::ERROR,
.text = text
});
}
void msgInfo(const std::string& text)
{
msgInfo(text.c_str());
}
void msgWarning(const std::string& text)
{
msgWarning(text.c_str());
}
void msgError(const std::string& text)
{
msgError(text.c_str());
}
template<typename TArg, typename... TArgs>
void msgInfo(fmt::format_string<TArg, TArgs...> format, TArg&& arg, TArgs&&... args)
{
std::string text = fmt::format(format, std::forward<TArg>(arg), std::forward<TArgs>(args)...);
msgInfo(text);
}
template<typename TArg, typename... TArgs>
void msgWarning(fmt::format_string<TArg, TArgs...> format, TArg&& arg, TArgs&&... args)
{
std::string text = fmt::format(format, std::forward<TArg>(arg), std::forward<TArgs>(args)...);
msgWarning(text);
}
template<typename TArg, typename... TArgs>
void msgError(fmt::format_string<TArg, TArgs...> format, TArg&& arg, TArgs&&... args)
{
std::string text = fmt::format(format, std::forward<TArg>(arg), std::forward<TArgs>(args)...);
msgError(text);
}
virtual bool init();
virtual void cleanup();
private:
bool initSDL();
bool initOpenGL();
bool initVulkan();
bool initImGui();
void handleSDLEvents();
void loadImGuiConfig();
void saveImGuiConfig();
VkBool32 handleDebugUtilsMessage(VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity,
VkDebugUtilsMessageTypeFlagsEXT messageTypes,
const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData);
};
using render_cb_t = std::function<void()>;
struct QuickAppOptions
{
struct
{
render_cb_t render;
} callbacks;
std::string folderName = "raid";
std::string windowTitle = "RAID";
ImGuiWindowFlags mainWindowFlags = DEFAULT_MAIN_WINDOW_FLAGS;
};
class QuickApp : public Application
{
private:
render_cb_t mRenderCallback;
std::string mFolderName;
std::string mWindowTitle;
public:
explicit QuickApp(ApplicationConfig config = {}) noexcept : Application(config) {}
void preInit(QuickAppOptions options);
void render() override;
std::string getFolderName() override;
std::string getWindowTitle() override;
static QuickApp& get();
};
[[nodiscard]]
int runQuick(int argc, char* argv[], QuickAppOptions options);
} // namespace raid
#endif // !defined(RAID_APPLICATION_HPP_INCLUDED)

189
public/raid/config.hpp Normal file
View File

@ -0,0 +1,189 @@
#pragma once
#if !defined(RAID_PUBLIC_RAID_CONFIG_HPP_INCLUDED)
#define RAID_PUBLIC_RAID_CONFIG_HPP_INCLUDED 1
#include <cstdint>
#include <string>
#include <string_view>
#include <variant>
#include <vector>
#include <mijin/async/coroutine.hpp>
#include <mijin/container/vector_map.hpp>
#include <mijin/memory/dynamic_pointer.hpp>
#include <mijin/types/result.hpp>
#include <mijin/virtual_filesystem/filesystem.hpp>
namespace raid
{
class ConfigValue;
class ConfigArray
{
public:
using iterator = std::vector<ConfigValue>::iterator;
using const_iterator = std::vector<ConfigValue>::const_iterator;
private:
std::vector<ConfigValue> mValues;
public:
ConfigArray() noexcept = default;
ConfigArray(const ConfigArray&) = default;
ConfigArray(ConfigArray&&) noexcept = default;
explicit ConfigArray(std::vector<ConfigValue> values) noexcept : mValues(std::move(values)) {}
ConfigArray& operator=(const ConfigArray&) = default;
ConfigArray& operator=(ConfigArray&&) noexcept = default;
const ConfigValue& operator[](std::size_t idx) const noexcept { return mValues[idx]; }
[[nodiscard]]
const std::vector<ConfigValue>& getValues() const noexcept { return mValues; }
[[nodiscard]]
std::size_t getSize() const noexcept { return mValues.size(); }
void append(ConfigValue value);
void setAt(std::size_t idx, ConfigValue value);
void removeAt(std::size_t idx, std::size_t count = 1);
[[nodiscard]]
bool isEmpty() const noexcept { return mValues.empty(); }
[[nodiscard]]
iterator begin() noexcept { return mValues.begin(); }
[[nodiscard]]
iterator end() noexcept { return mValues.end(); }
[[nodiscard]]
const_iterator begin() const noexcept { return mValues.begin(); }
[[nodiscard]]
const_iterator end() const noexcept { return mValues.end(); }
};
class ConfigSection
{
private:
mijin::VectorMap<std::string, ConfigValue> mValues;
public:
ConfigSection() noexcept = default;
ConfigSection(const ConfigSection&) = default;
ConfigSection(ConfigSection&&) noexcept = default;
explicit ConfigSection(mijin::VectorMap<std::string, ConfigValue> values) noexcept : mValues(std::move(values)) {}
ConfigSection& operator=(const ConfigSection&) = default;
ConfigSection& operator=(ConfigSection&&) noexcept = default;
const ConfigValue& operator[](std::string_view key) const noexcept;
[[nodiscard]]
ConfigValue& getOrAdd(std::string_view key);
void set(std::string_view key, ConfigValue value);
[[nodiscard]]
mijin::VectorMap<std::string, ConfigValue>& getValues() noexcept { return mValues; }
[[nodiscard]]
const mijin::VectorMap<std::string, ConfigValue>& getValues() const noexcept { return mValues; }
};
using config_int_t = std::int64_t;
class ConfigValue
{
private:
std::variant<std::nullptr_t, bool, config_int_t, double, std::string, ConfigArray, ConfigSection> mContent;
public:
ConfigValue() noexcept : mContent(nullptr) {}
ConfigValue(const ConfigValue&) = default;
ConfigValue(ConfigValue&&) noexcept = default;
ConfigValue(bool content) noexcept : mContent(content) {}
ConfigValue(config_int_t content) noexcept : mContent(content) {}
ConfigValue(double content) noexcept : mContent(content) {}
ConfigValue(std::string content) noexcept : mContent(std::move(content)) {}
ConfigValue(const char* content) noexcept : mContent(std::string(content)) {}
ConfigValue(ConfigArray content) noexcept : mContent(std::move(content)) {}
ConfigValue(ConfigSection content) noexcept : mContent(std::move(content)) {}
ConfigValue& operator=(const ConfigValue&) = default;
ConfigValue& operator=(ConfigValue&&) noexcept = default;
[[nodiscard]]
bool isUndefined() const noexcept { return std::holds_alternative<std::nullptr_t>(mContent); }
[[nodiscard]]
bool isBool() const noexcept { return std::holds_alternative<bool>(mContent); }
[[nodiscard]]
bool isInt() const noexcept { return std::holds_alternative<config_int_t>(mContent); }
[[nodiscard]]
bool isDouble() const noexcept { return std::holds_alternative<double>(mContent); }
[[nodiscard]]
bool isString() const noexcept { return std::holds_alternative<std::string>(mContent); }
[[nodiscard]]
bool isArray() const noexcept { return std::holds_alternative<ConfigArray>(mContent); }
[[nodiscard]]
bool isSection() const noexcept { return std::holds_alternative<ConfigSection>(mContent); }
[[nodiscard]]
bool asBool() const noexcept;
[[nodiscard]]
config_int_t asInt() const noexcept;
[[nodiscard]]
double asDouble() const noexcept;
[[nodiscard]]
const std::string& asString() const noexcept;
[[nodiscard]]
const ConfigArray& asArray() const noexcept;
[[nodiscard]]
ConfigSection& asMutableSection() noexcept;
[[nodiscard]]
const ConfigSection& asSection() const noexcept;
template<typename TFunc>
decltype(auto) visit(TFunc&& func) const noexcept
{
return std::visit(std::forward<TFunc>(func), mContent);
}
};
class FileConfig
{
private:
ConfigSection mRoot;
mijin::PathReference mPath;
bool mDirty = false;
public:
[[nodiscard]]
const ConfigSection& getRoot() const noexcept { return mRoot; }
[[nodiscard]]
const ConfigValue& getValue(std::string_view path) const noexcept;
void setValue(std::string_view path, ConfigValue value) noexcept;
[[nodiscard]]
mijin::Result<> init(mijin::PathReference path);
[[nodiscard]]
mijin::Result<> load();
[[nodiscard]]
mijin::Result<> save(bool force = false);
};
}
#endif // !defined(RAID_PUBLIC_RAID_CONFIG_HPP_INCLUDED)

View File

@ -4,7 +4,17 @@
#if !defined(RAID_PUBLIC_RAID_IMRAID_HPP_INCLUDED)
#define RAID_PUBLIC_RAID_IMRAID_HPP_INCLUDED 1
#include <algorithm>
#include <cstring>
#include <format>
#include <functional>
#include <span>
#include <string>
#include <vector>
#include <imgui.h>
#include <mijin/async/coroutine.hpp>
#include <mijin/debug/assert.hpp>
namespace ImRaid
{
@ -47,6 +57,268 @@ inline bool ToggleImageButton(const char* strId, ImTextureID textureId, const Im
}
return clicked;
}
inline bool BeginPopupButton(const char* label)
{
char popupId[128] = {"popup##"};
std::strcat(popupId, label);
const float popupX = ImGui::GetCursorScreenPos().x;
if (ImGui::Button(label)) {
ImGui::OpenPopup(popupId);
}
const float popupY = ImGui::GetCursorScreenPos().y;
ImGui::SetNextWindowPos({popupX, popupY});
return ImGui::BeginPopup(popupId, ImGuiWindowFlags_NoNav);
}
struct DataTableState
{
std::vector<std::size_t> sortedIndices;
struct SortColumn
{
std::size_t index;
bool sortDescending = false;
};
std::vector<SortColumn> sortColumns;
bool dirty = true;
};
template<typename TObject>
struct DataTableColumn
{
struct CellRendererArgs
{
const TObject& object;
};
using renderer_t = std::function<void(const CellRendererArgs&)>;
using comparator_t = std::function<bool(const TObject&, const TObject&)>;
const char* header;
renderer_t renderer;
comparator_t comparator;
};
template<typename TObject>
struct DataTableOptions
{
std::span<const DataTableColumn<TObject>> columns;
ImGuiTableFlags tableFlags = ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Sortable
| ImGuiTableFlags_ScrollY | ImGuiTableFlags_SortMulti;
};
template<typename TObject,typename TData>
inline void DataTable(const char* strId, const DataTableOptions<TObject>& options, const TData& data, DataTableState& state, ImVec2 outerSize = {})
{
if (outerSize.y <= 0.f) {
outerSize.y = ImGui::GetContentRegionAvail().y;
}
if (!ImGui::BeginTable(strId, static_cast<int>(options.columns.size()), options.tableFlags, outerSize)) {
return;
}
for (const DataTableColumn<TObject>& column : options.columns)
{
ImGuiTableColumnFlags flags = 0;
MIJIN_ASSERT(column.renderer, "Missing column renderer.");
if (!column.comparator) {
flags |= ImGuiTableColumnFlags_NoSort;
}
ImGui::TableSetupColumn(column.header, flags);
}
ImGui::TableSetupScrollFreeze(0, 1);
ImGui::TableHeadersRow();
ImGuiTableSortSpecs* sortSpecs = ImGui::TableGetSortSpecs();
if (sortSpecs != nullptr && sortSpecs->SpecsDirty)
{
sortSpecs->SpecsDirty = false;
if (state.sortColumns.size() != static_cast<std::size_t>(sortSpecs->SpecsCount))
{
state.dirty = true;
state.sortColumns.resize(sortSpecs->SpecsCount);
}
for (int idx = 0; idx < sortSpecs->SpecsCount; ++idx)
{
const ImGuiTableColumnSortSpecs& specs = sortSpecs->Specs[idx];
DataTableState::SortColumn& column = state.sortColumns[idx];
state.dirty |= (static_cast<std::size_t>(specs.ColumnIndex) != column.index);
state.dirty |= column.sortDescending != (specs.SortDirection == ImGuiSortDirection_Descending);
column = {
.index = static_cast<std::size_t>(specs.ColumnIndex),
.sortDescending = (specs.SortDirection == ImGuiSortDirection_Descending)
};
}
}
if (state.sortedIndices.size() != data.size())
{
state.dirty = true;
state.sortedIndices.resize(data.size());
}
if (state.dirty)
{
for (std::size_t idx = 0; idx < data.size(); ++idx) {
state.sortedIndices[idx] = idx;
}
std::ranges::sort(state.sortedIndices, [&](std::size_t leftIdx, std::size_t rightIdx)
{
for (const DataTableState::SortColumn& column : state.sortColumns)
{
const bool less = options.columns[column.index].comparator(data[leftIdx], data[rightIdx]);
if (less)
{ // left < right
return !column.sortDescending;
}
if (options.columns[column.index].comparator(data[rightIdx], data[leftIdx]))
{ // left > right
return column.sortDescending;
}
}
// left == right
return false;
});
state.dirty = false;
}
for (const std::size_t dataIdx : state.sortedIndices)
{
const TObject& object = data[dataIdx];
ImGui::TableNextRow();
for (const DataTableColumn<TObject>& column : options.columns)
{
ImGui::TableNextColumn();
column.renderer({
.object = object
});
}
}
ImGui::EndTable();
}
template<typename TObject>
inline DataTableColumn<TObject> MakeStringColumn(const char* header, const char* TObject::* member)
{
return {
.header = header,
.renderer = [member](const auto& args) { ImGui::TextUnformatted(args.object.*member); },
.comparator = [member](const TObject& left, const TObject& right) { return std::string_view(left.*member) < std::string_view(right.*member); }
};
}
template<typename TObject>
inline DataTableColumn<TObject> MakeStringColumn(const char* header, std::string TObject::* member)
{
return {
.header = header,
.renderer = [member](const auto& args) { ImGui::TextUnformatted((args.object.*member).c_str()); },
.comparator = [member](const TObject& left, const TObject& right) { return left.*member < right.*member; }
};
}
template<typename TObject, typename TMember>
inline DataTableColumn<TObject> MakeColumn(const char* header, const char* fmt, TMember TObject::* member)
{
return {
.header = header,
.renderer = [fmt, member](const auto& args) { ImGui::Text(fmt, args.object.*member); },
.comparator = [member](const TObject& left, const TObject& right) { return left.*member < right.*member; }
};
}
template<typename TFunc>
mijin::Task<> c_MessageBox(const char* titleId, TFunc&& renderFunc)
{
ImGui::OpenPopup(titleId);
bool open = true;
while (ImGui::BeginPopupModal(titleId, &open, ImGuiWindowFlags_NoResize))
{
std::invoke(std::forward<TFunc>(renderFunc));
ImGui::EndPopup();
co_await mijin::c_suspend();
}
}
template<typename... TFormatArgs>
mijin::Task<int> c_MessageBox(const char* titleId, std::span<const char*> buttons, std::format_string<TFormatArgs...> format, TFormatArgs&&... formatArgs)
{
const std::string message = std::format(format, std::forward<TFormatArgs>(formatArgs)...);
int buttonIdx = -1;
const ImGuiStyle& style = ImGui::GetStyle();
float buttonsWidth = 0.f;
for (const char* button : buttons) {
buttonsWidth += ImGui::CalcTextSize(button).x;
}
buttonsWidth += static_cast<float>(buttons.size()) * 2.f * style.FramePadding.x;;
buttonsWidth += static_cast<float>(buttons.size() - 1) * style.ItemSpacing.x;
co_await c_MessageBox(titleId, [&]()
{
ImGui::TextUnformatted(message.c_str());
const float offset = 0.5f * (ImGui::GetWindowWidth() - buttonsWidth);
ImGui::SetCursorPosX(offset);
for (int idx = 0; idx < static_cast<int>(buttons.size()); ++idx)
{
if (idx != 0) {
ImGui::SameLine();
}
if (ImGui::Button(buttons[idx]))
{
buttonIdx = idx;
ImGui::CloseCurrentPopup();
}
}
});
co_return buttonIdx;
}
struct YesNo
{
enum Value
{
YES,
NO
};
Value value = Value::NO;
constexpr YesNo() noexcept = default;
constexpr YesNo(const YesNo&) noexcept = default;
constexpr YesNo(Value value_) noexcept : value(value_) {}
constexpr YesNo& operator=(const YesNo&) noexcept = default;
constexpr auto operator<=>(const YesNo&) const noexcept = default;
constexpr operator bool() const noexcept { return value == YES; }
constexpr bool operator!() const noexcept { return value == NO; }
};
template<YesNo::Value DefaultResult = YesNo::NO, typename... TFormatArgs>
mijin::Task<YesNo> c_MessageBoxYesNo(const char* titleId, std::format_string<TFormatArgs...> format, TFormatArgs&&... formatArgs)
{
static std::array<const char*, 2> BUTTONS = {"Yes", "No"};
const int idx = co_await c_MessageBox(titleId, BUTTONS, format, std::forward<TFormatArgs>(formatArgs)...);
switch (idx)
{
case 0:
co_return YesNo::YES;
case 1:
co_return YesNo::NO;
default:
co_return DefaultResult;
}
}
} // namespace ImRaid
#endif // !defined(RAID_PUBLIC_RAID_IMRAID_HPP_INCLUDED)

View File

@ -0,0 +1,60 @@
#pragma once
#if !defined(RAID_PUBLIC_RAID_IMRAID_PLOT_HPP_INCLUDED)
#define RAID_PUBLIC_RAID_IMRAID_PLOT_HPP_INCLUDED 1
#include <imgui.h>
#include <implot.h>
#include <implot_internal.h>
namespace ImRaid::Plot
{
inline void AddVerticalLine(double xValue, ImU32 color, float thickness = 1.f, ImAxis dataAxis = ImAxis_X1)
{
const ImVec2 plotPos = ImPlot::GetPlotPos();
const ImVec2 plotSize = ImPlot::GetPlotSize();
const float xScaled = ImPlot::GetCurrentPlot()->Axes[dataAxis].PlotToPixels(xValue);
const ImVec2 v0 = ImVec2(xScaled, plotPos.y);
const ImVec2 v1 = ImVec2(xScaled, plotPos.y + plotSize.y);
ImPlot::GetPlotDrawList()->AddLine(v0, v1, color, thickness);
}
inline void AddHorizontalLine(double yValue, ImU32 color, float thickness = 1.f, ImAxis dataAxis = ImAxis_Y1)
{
const ImVec2 plotPos = ImPlot::GetPlotPos();
const ImVec2 plotSize = ImPlot::GetPlotSize();
const float yScaled = ImPlot::GetCurrentPlot()->Axes[dataAxis].PlotToPixels(yValue);
const ImVec2 v0 = ImVec2(plotPos.y, yScaled);
const ImVec2 v1 = ImVec2(plotPos.y + plotSize.y, yScaled);
ImPlot::GetPlotDrawList()->AddLine(v0, v1, color, thickness);
}
inline void AddVerticalRect(double xStart, double xEnd, ImU32 color, ImAxis dataAxis = ImAxis_X1)
{
const ImVec2 plotPos = ImPlot::GetPlotPos();
const ImVec2 plotSize = ImPlot::GetPlotSize();
const float xStartScaled = ImPlot::GetCurrentPlot()->Axes[dataAxis].PlotToPixels(xStart);
const float xEndScaled = ImPlot::GetCurrentPlot()->Axes[dataAxis].PlotToPixels(xEnd);
const ImVec2 v0 = ImVec2(xStartScaled, plotPos.y);
const ImVec2 v1 = ImVec2(xEndScaled, plotPos.y + plotSize.y);
ImPlot::GetPlotDrawList()->AddRectFilled(v0, v1, color);
}
inline void AddHorizontalRect(double yStart, double yEnd, ImU32 color, ImAxis dataAxis = ImAxis_Y1)
{
const ImVec2 plotPos = ImPlot::GetPlotPos();
const ImVec2 plotSize = ImPlot::GetPlotSize();
const float yStartScaled = ImPlot::GetCurrentPlot()->Axes[dataAxis].PlotToPixels(yStart);
const float yEndScaled = ImPlot::GetCurrentPlot()->Axes[dataAxis].PlotToPixels(yEnd);
const ImVec2 v0 = ImVec2(plotPos.x, yStartScaled);
const ImVec2 v1 = ImVec2(plotPos.x + plotSize.x, yEndScaled);
ImPlot::GetPlotDrawList()->AddRectFilled(v0, v1, color);
}
}
#endif // !defined(RAID_PUBLIC_RAID_IMRAID_PLOT_HPP_INCLUDED)

View File

@ -0,0 +1,45 @@
#pragma once
#if !defined(RAID_INTERNAL_OPENGL_HPP_INCLUDED)
#define RAID_INTERNAL_OPENGL_HPP_INCLUDED 1
#include <cstdint>
namespace raid
{
struct MixinOpenGLApplication
{
using GLbitfield = std::uint32_t;
using GLint = std::int32_t;
using GLuint = std::uint32_t;
using GLsizei = std::int32_t;
using GLenum = std::uint32_t;
using GLfloat = float;
using glClear_fn_t = void (*)(GLbitfield);
using glClearColor_fn_t = void (*)(GLfloat, GLfloat, GLfloat, GLfloat);
using glGenTextures_fn_t = void (*)(GLsizei, GLuint*);
using glBindTexture_fn_t = void (*)(GLenum, GLuint);
using glTexParameteri_fn_t = void (*)(GLenum, GLenum, GLint);
using glPixelStorei_fn_t = void (*)(GLenum, GLint);
using glTexImage2D_fn_t = void (*)(GLenum, GLint, GLint, GLsizei, GLsizei, GLint, GLenum, GLenum, const void*);
using glDeleteTextures_fn_t = void (*)(GLsizei, const GLuint*);
struct OpenGLData
{
SDL_GLContext context;
glClear_fn_t Clear;
glClearColor_fn_t ClearColor;
glGenTextures_fn_t GenTextures;
glBindTexture_fn_t BindTexture;
glTexParameteri_fn_t TexParameteri;
glPixelStorei_fn_t PixelStorei;
glTexImage2D_fn_t TexImage2D;
glDeleteTextures_fn_t DeleteTextures;
};
};
} // namespace raid
#endif // !defined(RAID_INTERNAL_OPENGL_HPP_INCLUDED)

View File

@ -0,0 +1,283 @@
#pragma once
#if !defined(RAID_INTERNAL_VULKAN_HPP_INCLUDED)
#define RAID_INTERNAL_VULKAN_HPP_INCLUDED 1
#include <cstdint>
#if !defined(RAID_USE_VULKAN_H)
#define RAID_USE_VULKAN_H 0
#endif
#if RAID_USE_VULKAN_H
#include <vulkan/vulkan.h>
#endif
namespace raid
{
struct MixinVulkanApplication
{
#if !RAID_USE_VULKAN_H
// basic types
using VkBool32 = std::uint32_t;
// flags
using VkFlags = std::uint32_t;
using VkInstanceCreateFlags = VkFlags;
using VkDebugUtilsMessengerCreateFlagsEXT = VkFlags;
using VkDebugUtilsMessageSeverityFlagsEXT = VkFlags;
using VkDebugUtilsMessageTypeFlagsEXT = VkFlags;
using VkDebugUtilsMessengerCallbackDataFlagsEXT = VkFlags;
using VkQueueFlags = VkFlags;
using VkDeviceCreateFlags = VkFlags;
using VkDeviceQueueCreateFlags = VkFlags;
// constants
static constexpr std::uint32_t VK_MAX_EXTENSION_NAME_SIZE = 256;
static constexpr std::uint32_t VK_MAX_DESCRIPTION_SIZE = 256;
// enums
enum VkResult
{
VK_SUCCESS = 0,
VK_INCOMPLETE = 5
};
enum VkStructureType
{
VK_STRUCTURE_TYPE_APPLICATION_INFO = 0,
VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO = 1,
VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO = 2,
VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO = 3,
VK_STRUCTURE_TYPE_QUEUE_FAMILY_PROPERTIES_2 = 1000059005,
VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT = 1000128000,
VK_STRUCTURE_TYPE_DEBUG_UTILS_LABEL_EXT = 1000128002,
VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CALLBACK_DATA_EXT = 1000128003,
VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT = 1000128004
};
enum VkSystemAllocationScope {};
enum VkInternalAllocationType {};
enum VkObjectType {};
enum VkDebugUtilsMessageSeverityFlagBitsEXT : VkFlags
{
VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT = 0x00000001,
VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT = 0x00000010,
VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT = 0x00000100,
VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT = 0x00001000
};
enum VkDebugUtilsMessageTypeFlagBitsEXT : VkFlags
{
VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT = 0x00000001,
VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT = 0x00000002,
VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT = 0x00000004
};
enum VkQueueFlagBits : VkFlags
{
VK_QUEUE_GRAPHICS_BIT = 0x00000001
};
// handles
using VkDevice = struct VkDevice_*;
using VkQueue = struct VkQueue_*;
using VkDebugUtilsMessengerEXT = struct VkDebugUtilsMessengerEXT_*;
struct VkDebugUtilsMessengerCallbackDataEXT;
// Vulkan function pointer types
using PFN_vkVoidFunction = void (*)();
// TODO: VKAPI_PTR?
using PFN_vkAllocationFunction = void* (*)(void* pUserData, std::size_t size, std::size_t alignment, VkSystemAllocationScope allocationScope);
using PFN_vkReallocationFunction = void* (*)(void* pUserData, void* pOriginal, std::size_t size, std::size_t alignment, VkSystemAllocationScope allocationScope);
using PFN_vkFreeFunction = void (*)(void* pUserData, void* pMemory);
using PFN_vkInternalAllocationNotification = void (*)(void* pUserData, std::size_t size, VkInternalAllocationType allocationType, VkSystemAllocationScope allocationScope);
using PFN_vkInternalFreeNotification = void (*)(void* pUserData, std::size_t size, VkInternalAllocationType allocationType, VkSystemAllocationScope allocationScope);
using PFN_vkDebugUtilsMessengerCallbackEXT = VkBool32 (*)(VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, VkDebugUtilsMessageTypeFlagsEXT messageTypes, const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData, void* pUserData);
// structs
struct VkExtent3D
{
std::uint32_t width = 0;
std::uint32_t height = 0;
std::uint32_t depth = 0;
};
struct VkLayerProperties
{
char layerName[VK_MAX_EXTENSION_NAME_SIZE] = {0};
std::uint32_t specVersion = 0;
std::uint32_t implementationVersion = 0;
char description[VK_MAX_DESCRIPTION_SIZE] = {0};
};
struct VkExtensionProperties
{
char extensionName[VK_MAX_EXTENSION_NAME_SIZE] = {0};
std::uint32_t specVersion = 0;
};
struct VkApplicationInfo
{
VkStructureType sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
const void* pNext = nullptr;
const char* pApplicationNane = nullptr;
std::uint32_t applicationVersion = 0;
const char* pEngineName = nullptr;
std::uint32_t engineVersion = 0;
std::uint32_t apiVersion = 0;
};
struct VkInstanceCreateInfo
{
VkStructureType sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
const void* pNext = nullptr;
VkInstanceCreateFlags flags = 0;
const VkApplicationInfo* pApplicationInfo = nullptr;
std::uint32_t enabledLayerCount = 0;
const char* const* ppEnabledLayerNames = nullptr;
std::uint32_t enabledExtensionCount = 0;
const char* const* ppEnabledExtensionNames = nullptr;
};
struct VkDebugUtilsMessengerCreateInfoEXT
{
VkStructureType sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;
const void* pNext = nullptr;
VkDebugUtilsMessengerCreateFlagsEXT flags = 0;
VkDebugUtilsMessageSeverityFlagsEXT messageSeverity = 0;
VkDebugUtilsMessageTypeFlagsEXT messageType = 0;
PFN_vkDebugUtilsMessengerCallbackEXT pfnUserCallback;
void* pUserData = nullptr;
};
struct VkDebugUtilsLabelEXT
{
VkStructureType sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_LABEL_EXT;
const void* pNext = nullptr;
const char* pLabelName = nullptr;
float color[4] = {0.f, 0.f, 0.f, 0.f};
};
struct VkDebugUtilsObjectNameInfoEXT
{
VkStructureType sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT;
const void* pNext = nullptr;
VkObjectType objectType = static_cast<VkObjectType>(0);
std::uint64_t objectHandle = 0;
const char* pObjectName = nullptr;
};
struct VkDebugUtilsMessengerCallbackDataEXT
{
VkStructureType sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CALLBACK_DATA_EXT;
const void* pNext = nullptr;
VkDebugUtilsMessengerCallbackDataFlagsEXT flags = 0;
const char* pMessageIdName;
std::int32_t messageIdNumber;
const char* pMessage;
std::uint32_t queueLabelCount;
const VkDebugUtilsLabelEXT* pQueueLabels;
std::uint32_t cmdBufLabelCount;
const VkDebugUtilsLabelEXT* pCmdBufLabels;
std::uint32_t objectCount;
const VkDebugUtilsObjectNameInfoEXT* pObjects;
};
struct VkQueueFamilyProperties
{
VkQueueFlags queueFlags = 0;
std::uint32_t queueCount = 0;
std::uint32_t timestampValidBits = 0;
VkExtent3D minImageTransferGranularity = {};
};
struct VkQueueFamilyProperties2
{
VkStructureType sType = VK_STRUCTURE_TYPE_QUEUE_FAMILY_PROPERTIES_2;
void* pNext = nullptr;
VkQueueFamilyProperties queueFamilyProperties = {};
};
struct VkDeviceQueueCreateInfo
{
VkStructureType sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
const void* pNext = nullptr;
VkDeviceQueueCreateFlags flags = 0;
std::uint32_t queueFamilyIndex = 0;
std::uint32_t queueCount = 0;
const float* pQueuePriorities = nullptr;
};
struct VkPhysicalDeviceFeatures;
struct VkDeviceCreateInfo
{
VkStructureType sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
const void* pNext = nullptr;
VkDeviceCreateFlags flags;
std::uint32_t queueCreateInfoCount = 0;
const VkDeviceQueueCreateInfo* pQueueCreateInfos = nullptr;
std::uint32_t enabledLayerCount = 0;
const char* const* ppEnabledLayerNames = nullptr;
uint32_t enabledExtensionCount = 0;
const char* const* ppEnabledExtensionNames = nullptr;
const VkPhysicalDeviceFeatures* pEnabledFeatures = nullptr;
};
struct VkAllocationCallbacks
{
void* pUserData = nullptr;
PFN_vkAllocationFunction pfnAllocation = nullptr;
PFN_vkReallocationFunction pfnReallocation = nullptr;
PFN_vkFreeFunction pfnFree = nullptr;
PFN_vkInternalAllocationNotification pfnInternalAllocation = nullptr;
PFN_vkInternalFreeNotification pfnInternalFree = nullptr;
};
// -- function pointers
// instance creation
using vkGetInstanceProcAddr_fn_t = PFN_vkVoidFunction (*)(VkInstance instance, const char* pName);
using vkEnumerateInstanceLayerProperties_fn_t = VkResult (*)(std::uint32_t* pPropertyCount, VkLayerProperties* pProperties);
using vkEnumerateInstanceExtensionProperties_fn_t = VkResult (*)(const char* pLayerName, std::uint32_t* pPropertyCount, VkExtensionProperties* pProperties);
using vkCreateDebugUtilsMessengerEXT_fn_t = VkResult (*)(VkInstance instance, const VkDebugUtilsMessengerCreateInfoEXT* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkDebugUtilsMessengerEXT* pMessenger);
using vkDestroyDebugUtilsMessengerEXT_fn_t = void (*)(VkInstance instance, VkDebugUtilsMessengerEXT messenger, const VkAllocationCallbacks* pAllocator);
using vkCreateInstance_fn_t = VkResult (*)(const VkInstanceCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkInstance* pInstance);
using vkDestroyInstance_fn_t = void (*)(VkInstance instance, const VkAllocationCallbacks* pAllocator);
// device creation
using vkEnumeratePhysicalDevices_fn_t = VkResult (*)(VkInstance instance, std::uint32_t* pPhysicalDeviceCount, VkPhysicalDevice* pPhysicalDevices);
using vkGetPhysicalDeviceQueueFamilyProperties2_fn_t = void (*)(VkPhysicalDevice physicalDevice, std::uint32_t* pQueueFamilyPropertyCount, VkQueueFamilyProperties2* pQueueFamilyProperties);
using vkCreateDevice_fn_t = VkResult (*)(VkPhysicalDevice physicalDevice, const VkDeviceCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkDevice* pDevice);
using vkDestroyDevice_fn_t = void (*)(VkDevice device, const VkAllocationCallbacks* pAllocator);
using vkDeviceWaitIdle_fn_t = VkResult (*)(VkDevice device);
// other creation
using vkGetDeviceQueue_fn_t = void (*)(VkDevice device, std::uint32_t queueFamilyIndex, std::uint32_t queueIndex, VkQueue* pQueue);
#endif // !RAID_USE_VULKAN_H
struct VulkanData
{
VkInstance instance;
VkDebugUtilsMessengerEXT debugUtilsMessenger;
VkPhysicalDevice physicalDevice;
VkDevice device;
VkQueue queue;
VkSurfaceKHR surface;
vkGetInstanceProcAddr_fn_t GetInstanceProc;
vkEnumerateInstanceLayerProperties_fn_t EnumerateInstanceLayerProperties;
vkEnumerateInstanceExtensionProperties_fn_t EnumerateInstanceExtensionProperties;
vkCreateDebugUtilsMessengerEXT_fn_t CreateDebugUtilsMessengerEXT;
vkDestroyDebugUtilsMessengerEXT_fn_t DestroyDebugUtilsMessengerEXT;
vkCreateInstance_fn_t CreateInstance;
vkDestroyInstance_fn_t DestroyInstance;
vkEnumeratePhysicalDevices_fn_t EnumeratePhysicalDevices;
vkGetPhysicalDeviceQueueFamilyProperties2_fn_t GetPhysicalDeviceQueueFamilyProperties2;
vkCreateDevice_fn_t CreateDevice;
vkDestroyDevice_fn_t DestroyDevice;
vkDeviceWaitIdle_fn_t DeviceWaitIdle;
vkGetDeviceQueue_fn_t GetDeviceQueue;
std::uint32_t queueFamilyIndex;
};
};
} // namespace raid
#endif // !defined(RAID_INTERNAL_VULKAN_HPP_INCLUDED)

View File

@ -4,217 +4,7 @@
#if !defined(RAID_PUBLIC_RAID_RAID_HPP_INCLUDED)
#define RAID_PUBLIC_RAID_RAID_HPP_INCLUDED 1
#include <cstdint>
#include <functional>
#include <fmt/format.h>
#include <imgui.h>
#include <mijin/async/coroutine.hpp>
#include <mijin/util/bitflags.hpp>
#include <mijin/virtual_filesystem/stacked.hpp>
#include <SDL3/SDL.h>
namespace raid
{
inline constexpr int ERR_INIT_FAILED = 100;
inline constexpr ImGuiWindowFlags DEFAULT_MAIN_WINDOW_FLAGS = 0
| ImGuiWindowFlags_NoBackground
| ImGuiWindowFlags_NoDecoration
| ImGuiWindowFlags_NoBringToFrontOnFocus
| ImGuiWindowFlags_NoNav;
enum class MessageSeverity : unsigned char
{
INFO,
WARNING,
ERROR
};
struct Message
{
MessageSeverity severity;
const char* text;
};
struct FontFlags : mijin::BitFlags<FontFlags>
{
bool pixelSnapH : 1 = false;
};
struct FontConfig
{
fs::path path;
std::vector<std::pair<ImWchar, ImWchar>> glyphRanges;
float size = 20.f;
FontFlags flags;
};
class Application
{
private:
SDL_Window* mWindow = nullptr;
SDL_GLContext mGLContext = nullptr;
mijin::StackedFileSystemAdapter mFS;
mijin::SimpleTaskLoop mTaskLoop;
std::unordered_map<fs::path, ImTextureID> mTextures;
bool mRunning = true;
ImGuiWindowFlags mMainWindowFlags = DEFAULT_MAIN_WINDOW_FLAGS;
using GLbitfield = std::uint32_t;
using GLint = std::int32_t;
using GLuint = std::uint32_t;
using GLsizei = std::int32_t;
using GLenum = std::uint32_t;
using GLfloat = float;
using glClear_fn_t = void (*)(GLbitfield);
using glClearColor_fn_t = void (*)(GLfloat, GLfloat, GLfloat, GLfloat);
using glGenTextures_fn_t = void (*)(GLsizei, GLuint*);
using glBindTexture_fn_t = void (*)(GLenum, GLuint);
using glTexParameteri_fn_t = void (*)(GLenum, GLenum, GLint);
using glPixelStorei_fn_t = void (*)(GLenum, GLint);
using glTexImage2D_fn_t = void (*)(GLenum, GLint, GLint, GLsizei, GLsizei, GLint, GLenum, GLenum, const void*);
using glDeleteTextures_fn_t = void (*)(GLsizei, const GLuint*);
glClear_fn_t glClear;
glClearColor_fn_t glClearColor;
glGenTextures_fn_t glGenTextures;
glBindTexture_fn_t glBindTexture;
glTexParameteri_fn_t glTexParameteri;
glPixelStorei_fn_t glPixelStorei;
glTexImage2D_fn_t glTexImage2D;
glDeleteTextures_fn_t glDeleteTextures;
public:
virtual ~Application() = default;
[[nodiscard]]
mijin::StackedFileSystemAdapter& getFS() { return mFS; }
[[nodiscard]]
mijin::SimpleTaskLoop& getLoop() { return mTaskLoop; }
[[nodiscard]]
ImGuiWindowFlags getMainWindowFlags() const { return mMainWindowFlags; }
void setMainWindowFlags(ImGuiWindowFlags flags) { mMainWindowFlags = flags; }
void requestQuit() { mRunning = false; }
[[nodiscard]]
int run(int argc, char* argv[]);
[[nodiscard]]
bool loadFonts(std::span<const FontConfig> fonts);
[[nodiscard]]
bool loadFont(const FontConfig& font)
{
return loadFonts({&font, 1});
}
[[nodiscard]]
ImTextureID getOrLoadTexture(fs::path path);
protected:
virtual void render() = 0;
virtual std::string getFolderName() = 0;
virtual std::string getWindowTitle() = 0;
virtual void configureImgui();
virtual std::vector<FontConfig> getDefaultFonts();
virtual void handleMessage(const Message& message);
virtual void handleSDLEvent(const SDL_Event& event);
void msgInfo(const char* text)
{
handleMessage({
.severity = MessageSeverity::INFO,
.text = text
});
}
void msgWarning(const char* text)
{
handleMessage({
.severity = MessageSeverity::WARNING,
.text = text
});
}
void msgError(const char* text)
{
handleMessage({
.severity = MessageSeverity::ERROR,
.text = text
});
}
void msgInfo(const std::string& text)
{
msgInfo(text.c_str());
}
void msgWarning(const std::string& text)
{
msgWarning(text.c_str());
}
void msgError(const std::string& text)
{
msgError(text.c_str());
}
template<typename TArg, typename... TArgs>
void msgInfo(fmt::format_string<TArg, TArgs...> format, TArg&& arg, TArgs&&... args)
{
std::string text = fmt::format(format, std::forward<TArg>(arg), std::forward<TArgs>(args)...);
msgInfo(text);
}
template<typename TArg, typename... TArgs>
void msgWarning(fmt::format_string<TArg, TArgs...> format, TArg&& arg, TArgs&&... args)
{
std::string text = fmt::format(format, std::forward<TArg>(arg), std::forward<TArgs>(args)...);
msgWarning(text);
}
template<typename TArg, typename... TArgs>
void msgError(fmt::format_string<TArg, TArgs...> format, TArg&& arg, TArgs&&... args)
{
std::string text = fmt::format(format, std::forward<TArg>(arg), std::forward<TArgs>(args)...);
msgError(text);
}
virtual bool init();
virtual void cleanup();
private:
bool initSDL();
bool initGL();
bool initImGui();
void handleSDLEvents();
void loadImGuiConfig();
void saveImGuiConfig();
};
using render_cb_t = std::function<void()>;
struct QuickAppOptions
{
struct
{
render_cb_t render;
} callbacks;
std::string folderName = "raid";
std::string windowTitle = "RAID";
ImGuiWindowFlags mainWindowFlags = DEFAULT_MAIN_WINDOW_FLAGS;
};
class QuickApp : public Application
{
private:
render_cb_t mRenderCallback;
std::string mFolderName;
std::string mWindowTitle;
public:
void preInit(QuickAppOptions options);
void render() override;
std::string getFolderName() override;
std::string getWindowTitle() override;
static QuickApp& get();
};
[[nodiscard]]
int runQuick(int argc, char* argv[], QuickAppOptions options);
} // namespace raid
#include "./application.hpp"
#include "./config.hpp"
#endif // !defined(RAID_PUBLIC_RAID_RAID_HPP_INCLUDED)

View File

@ -0,0 +1,194 @@
# source: https://github.com/hgomersall/scons-jinja
# 2025-03-28: Added JINJA_GLOBALS options.
# 2023-12-02: Added JINJA_FILTERS option.
#
# Copyright (c) 2013 Henry Gomersall <heng@kedevelopments.co.uk>
# Copyright (c) 2025 Patrick Wuttke <mewin@mewin.de>
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
import SCons.Builder
import SCons.Tool
from SCons.Errors import StopError
import jinja2
from jinja2 import FileSystemLoader
from jinja2.utils import open_if_exists
from jinja2.exceptions import TemplateNotFound
import os
class FileSystemLoaderRecorder(FileSystemLoader):
''' A wrapper around FileSystemLoader that records files as they are
loaded. These are contained within loaded_filenames set attribute.
'''
def __init__(self, searchpath, encoding='utf-8'):
self.loaded_filenames = set()
super(FileSystemLoaderRecorder, self).__init__(searchpath, encoding)
def get_source(self, environment, template):
'''Overwritten FileSystemLoader.get_source method that extracts the
filename that is used to load each filename and adds it to
self.loaded_filenames.
'''
for searchpath in self.searchpath:
filename = os.path.join(searchpath, template)
f = open_if_exists(filename)
if f is None:
continue
try:
contents = f.read().decode(self.encoding)
finally:
f.close()
self.loaded_filenames.add(filename)
return super(FileSystemLoaderRecorder, self).get_source(
environment, template)
# If the template isn't found, then we have to drop out.
raise TemplateNotFound(template)
def jinja_scanner(node, env, path):
# Instantiate the file as necessary
node.get_text_contents()
node_dir = os.path.dirname(str(node))
template_dir, filename = os.path.split(str(node))
template_search_path = ([template_dir] +
env.subst(env['JINJA_TEMPLATE_SEARCHPATH']))
template_loader = FileSystemLoaderRecorder(template_search_path)
jinja_env = jinja2.Environment(loader=template_loader,
extensions=['jinja2.ext.do'], **env['JINJA_ENVIRONMENT_VARS'])
jinja_env.filters.update(env['JINJA_FILTERS'])
jinja_env.globals.update(env['JINJA_GLOBALS'])
try:
template = jinja_env.get_template(filename)
except TemplateNotFound as e:
raise StopError('Missing template: ' +
os.path.join(template_dir, str(e)))
# We need to render the template to do all the necessary loading.
#
# It's necessary to respond to missing templates by grabbing
# the content as the exception is raised. This makes sure of the
# existence of the file upon which the current scanned node depends.
#
# I suspect that this is pretty inefficient, but it does
# work reliably.
context = env['JINJA_CONTEXT']
last_missing_file = ''
while True:
try:
template.render(**context)
except TemplateNotFound as e:
if last_missing_file == str(e):
# We've already been round once for this file,
# so need to raise
raise StopError('Missing template: ' +
os.path.join(template_dir, str(e)))
last_missing_file = str(e)
# Find where the template came from (using the same ordering
# as Jinja uses).
for searchpath in template_search_path:
filename = os.path.join(searchpath, last_missing_file)
if os.path.exists(filename):
continue
else:
env.File(filename).get_text_contents()
continue
break
# Get all the files that were loaded. The set includes the current node,
# so we remove that.
found_nodes_names = list(template_loader.loaded_filenames)
try:
found_nodes_names.remove(str(node))
except ValueError as e:
raise StopError('Missing template node: ' + str(node))
return [env.File(f) for f in found_nodes_names]
def render_jinja_template(target, source, env):
output_str = ''
if not source:
source = [f'{target}.jinja']
for template_file in source:
template_dir, filename = os.path.split(str(template_file))
template_search_path = ([template_dir] +
env.subst(env['JINJA_TEMPLATE_SEARCHPATH']))
template_loader = FileSystemLoaderRecorder(template_search_path)
jinja_env = jinja2.Environment(loader=template_loader,
extensions=['jinja2.ext.do'], **env['JINJA_ENVIRONMENT_VARS'])
jinja_env.filters.update(env['JINJA_FILTERS'])
jinja_env.globals.update(env['JINJA_GLOBALS'])
template = jinja_env.get_template(filename)
context = env['JINJA_CONTEXT']
template.render(**context)
output_str += template.render(**context)
with open(str(target[0]), 'w') as target_file:
target_file.write(output_str)
return None
def generate(env):
env.SetDefault(JINJA_CONTEXT={})
env.SetDefault(JINJA_ENVIRONMENT_VARS={})
env.SetDefault(JINJA_FILTERS={})
env.SetDefault(JINJA_GLOBALS={})
env.SetDefault(JINJA_TEMPLATE_SEARCHPATH=[])
env['BUILDERS']['Jinja'] = SCons.Builder.Builder(
action=render_jinja_template)
scanner = env.Scanner(function=jinja_scanner,
skeys=['.jinja'])
env.Append(SCANNERS=scanner)
def exists(env):
try:
import jinja2
except ImportError as e:
raise StopError(ImportError, e.message)