Compare commits

...

19 Commits

Author SHA1 Message Date
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
22 changed files with 1193 additions and 219 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')

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,18 @@ import json
Import('env')
if not hasattr(env, 'Jinja'):
env.Error('RAID requires Jinja.')
src_files = Split("""
application.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

@@ -5,6 +5,8 @@
#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)
@@ -94,8 +104,16 @@ int Application::run(int argc, char** argv)
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();
@@ -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)
@@ -274,6 +345,11 @@ void Application::handleSDLEvent(const SDL_Event& event)
}
}
void Application::handleSDLError(const char* message)
{
msgError("SDL: {}", message);
}
bool Application::init()
{
auto addConfigDir = [&](const fs::path& path)
@@ -294,6 +370,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 +384,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 +397,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 +434,19 @@ 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;
}
}
if (!initImGui())
{
@@ -383,14 +472,29 @@ 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.instance != nullptr)
{
vk.DestroyInstance(vk.instance, nullptr);
}
SDL_Vulkan_UnloadLibrary();
break;
}
if (mWindow != nullptr)
{
@@ -401,54 +505,159 @@ 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);
msgInfo("SDL video driver: {}", SDL_GetCurrentVideoDriver());
// 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);
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.CreateInstance = reinterpret_cast<vkCreateInstance_fn_t>(vk.GetInstanceProc(nullptr, "vkCreateInstance"));
const VkApplicationInfo applicationInfo = {
.apiVersion = vkMakeApiVersion(0, 1, 3, 0) // TODO: probably should let the user specify this?
};
const VkInstanceCreateInfo instanceCreateInfo = {
.pApplicationInfo = &applicationInfo
};
if (const VkResult result = vk.CreateInstance(&instanceCreateInfo, nullptr, &vk.instance); result != VK_SUCCESS)
{
msgError("Error creating Vulkan instance: {: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"));
// 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: {: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)
{
if (queueFamilyProperties[queueFamilyIndex].queueFamilyProperties.queueFlags & VK_QUEUE_GRAPHICS_BIT) {
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_surface"
};
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: {: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 +666,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 +689,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

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,8 @@ Import('env')
src_files = Split("""
main.cpp
application.cpp
""")
prog_app = env.UnityProgram(

View File

@@ -0,0 +1,57 @@
#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);
return true;
}
void Application::configureImgui()
{
raid::Application::configureImgui();
ImGuiIO& imguiIO = ImGui::GetIO();
imguiIO.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
}
void Application::render()
{
if (ImGui::BeginMenuBar())
{
if (ImGui::BeginMenu("File"))
{
if (ImGui::MenuItem("Quit"))
{
requestQuit();
}
ImGui::EndMenu();
}
ImGui::EndMenuBar();
}
ImGui::Text("hi");
ImGui::ShowMetricsWindow();
ImGui::Begin("Test");
ImGui::Text("Test Content");
ImGui::End();
}
std::string Application::getFolderName()
{
return "raid_test_app";
}
std::string Application::getWindowTitle()
{
return "RAID Test Application";
}
} // namespace raid_test

View File

@@ -0,0 +1,24 @@
#pragma once
#if !defined(RAID_TEST_APPLICATION_HPP_INCLUDED)
#define RAID_TEST_APPLICATION_HPP_INCLUDED 1
#include "raid/raid.hpp"
namespace raid_test
{
class Application : public raid::Application
{
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

@@ -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);
}

View File

@@ -4,7 +4,14 @@
#if !defined(RAID_PUBLIC_RAID_IMRAID_HPP_INCLUDED)
#define RAID_PUBLIC_RAID_IMRAID_HPP_INCLUDED 1
#include <algorithm>
#include <functional>
#include <span>
#include <string>
#include <vector>
#include <imgui.h>
#include <mijin/debug/assert.hpp>
namespace ImRaid
{
@@ -47,6 +54,106 @@ inline bool ToggleImageButton(const char* strId, ImTextureID textureId, const Im
}
return clicked;
}
struct DataTableState
{
std::vector<std::size_t> sortedIndices;
std::size_t sortColumn = 0;
bool sortDescending = false;
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;
};
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;
const ImGuiTableColumnSortSpecs& specs = *sortSpecs->Specs;
state.dirty |= (specs.ColumnIndex != state.sortColumn);
state.sortColumn = specs.ColumnIndex;
state.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) {
return options.columns[state.sortColumn].comparator(data[leftIdx], data[rightIdx]);
});
state.dirty = false;
}
for (std::size_t indexIdx = 0; indexIdx < state.sortedIndices.size(); ++indexIdx)
{
const std::size_t dataIdx = state.sortDescending ? state.sortedIndices[state.sortedIndices.size() - indexIdx - 1]
: state.sortedIndices[indexIdx];
const TObject& object = data[dataIdx];
ImGui::TableNextRow();
for (const DataTableColumn<TObject>& column : options.columns)
{
ImGui::TableNextColumn();
column.renderer({
.object = object
});
}
}
ImGui::EndTable();
}
} // namespace ImRaid
#endif // !defined(RAID_PUBLIC_RAID_IMRAID_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,176 @@
#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
// flags
using VkFlags = std::uint32_t;
using VkInstanceCreateFlags = VkFlags;
using VkQueueFlags = VkFlags;
using VkDeviceCreateFlags = VkFlags;
using VkDeviceQueueCreateFlags = VkFlags;
// enums
enum VkResult
{
VK_SUCCESS = 0
};
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
};
enum VkSystemAllocationScope {};
enum VkInternalAllocationType {};
enum VkQueueFlagBits : VkFlags
{
VK_QUEUE_GRAPHICS_BIT = 0x00000001
};
// handles
using VkDevice = struct VkDevice_*;
using VkQueue = struct VkQueue_*;
// structs
struct VkExtent3D
{
std::uint32_t width = 0;
std::uint32_t height = 0;
std::uint32_t depth = 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 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;
};
// 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);
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
using vkGetInstanceProcAddr_fn_t = PFN_vkVoidFunction (*)(VkInstance instance, const char* pName);
using vkCreateInstance_fn_t = VkResult (*)(const VkInstanceCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkInstance* pInstance);
using vkDestroyInstance_fn_t = void (*)(VkInstance instance, const VkAllocationCallbacks* pAllocator);
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);
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;
VkPhysicalDevice physicalDevice;
VkDevice device;
VkQueue queue;
VkSurfaceKHR surface;
vkGetInstanceProcAddr_fn_t GetInstanceProc;
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

@@ -6,12 +6,17 @@
#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
{
@@ -21,6 +26,7 @@ inline constexpr ImGuiWindowFlags DEFAULT_MAIN_WINDOW_FLAGS = 0
| ImGuiWindowFlags_NoDecoration
| ImGuiWindowFlags_NoBringToFrontOnFocus
| ImGuiWindowFlags_NoNav;
inline constexpr const char* DEFAULT_FONT_PATH = "/data/fonts/NotoSans-Regular.ttf";
enum class MessageSeverity : unsigned char
{
@@ -48,49 +54,65 @@ struct FontConfig
FontFlags flags;
};
class Application
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;
SDL_GLContext mGLContext = 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;
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;
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; }
@@ -98,6 +120,8 @@ public:
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]]
@@ -115,14 +139,18 @@ public:
[[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);
void msgInfo(const char* text)
{
@@ -179,7 +207,8 @@ protected:
virtual void cleanup();
private:
bool initSDL();
bool initGL();
bool initOpenGL();
bool initVulkan();
bool initImGui();
void handleSDLEvents();
void loadImGuiConfig();
@@ -205,6 +234,8 @@ private:
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;

View File

@@ -1,93 +1,93 @@
Copyright 2022 The Noto Project Authors (https://github.com/notofonts/latin-greek-cyrillic)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Copyright 2022 The Noto Project Authors (https://github.com/notofonts/latin-greek-cyrillic)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

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)