Added texture managment and overridable init/cleanup functions.

This commit is contained in:
Patrick 2025-03-04 00:16:57 +01:00
parent 53efca4729
commit 08ea619a8e
5 changed files with 253 additions and 120 deletions

View File

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

View File

@ -5,6 +5,7 @@ Import('env')
src_files = Split(""" src_files = Split("""
application.cpp application.cpp
stb_image.cpp
""") """)
with open('../../dependencies.json', 'r') as f: with open('../../dependencies.json', 'r') as f:

View File

@ -13,14 +13,51 @@
#include <imgui.h> #include <imgui.h>
#include <backends/imgui_impl_opengl3.h> #include <backends/imgui_impl_opengl3.h>
#include <backends/imgui_impl_sdl3.h> #include <backends/imgui_impl_sdl3.h>
#include <stb_image.h>
namespace raid namespace raid
{ {
namespace namespace
{ {
constexpr int GL_COLOR_BUFFER_BIT = 0x00004000; constexpr std::uint32_t GL_COLOR_BUFFER_BIT = 0x00004000;
constexpr std::uint32_t GL_TEXTURE_2D = 0x0DE1;
constexpr std::uint32_t GL_TEXTURE_MIN_FILTER = 0x2801;
constexpr std::uint32_t GL_TEXTURE_MAG_FILTER = 0x2800;
constexpr std::uint32_t GL_LINEAR = 0x2601;
constexpr std::uint32_t GL_UNPACK_ROW_LENGTH = 0x0CF2;
constexpr std::uint32_t GL_RGBA = 0x1908;
constexpr std::uint32_t GL_UNSIGNED_BYTE = 0x1401;
const char* IMGUI_GLSL_VERSION = "#version 130"; const char* IMGUI_GLSL_VERSION = "#version 130";
QuickApp* gAppInstance = nullptr; QuickApp* gAppInstance = nullptr;
int stbiRead(void* user, char* data, int size)
{
mijin::Stream& stream = *static_cast<mijin::Stream*>(user);
std::size_t read = 0;
if (const mijin::StreamError error = stream.readRaw(data, size, {.partial = true}, &read); error != mijin::StreamError::SUCCESS)
{
MIJIN_ERROR("IO error.");
return -1;
}
return static_cast<int>(read);
}
void stbiSkip(void* user, int diff)
{
mijin::Stream& stream = *static_cast<mijin::Stream*>(user);
if (const mijin::StreamError error = stream.seek(diff, mijin::SeekMode::RELATIVE); error != mijin::StreamError::SUCCESS)
{
MIJIN_ERROR("IO error.");
// not much we can do :/
}
}
int stbiEof(void* user)
{
mijin::Stream& stream = *static_cast<mijin::Stream*>(user);
return stream.isAtEnd();
}
} }
int Application::run(int argc, char** argv) int Application::run(int argc, char** argv)
@ -32,90 +69,7 @@ int Application::run(int argc, char** argv)
cleanup(); cleanup();
}; };
auto addConfigDir = [&](const fs::path path) if (!init())
{
using namespace mijin::vfs_pipe;
mFS.addAdapter(os()
| relative_to(path)
| map_to("/config")
);
};
auto addDataDir = [&](const fs::path path)
{
using namespace mijin::vfs_pipe;
mFS.addAdapter(os()
| relative_to(path)
| map_to("/data")
);
};
addConfigDir(mijin::getKnownFolder(mijin::KnownFolder::USER_CONFIG_ROOT) / getFolderName());
addDataDir(mijin::getKnownFolder(mijin::KnownFolder::USER_DATA_ROOT) / getFolderName());
auto createUserDir = [&](const fs::path& virtualPath)
{
mijin::Optional<fs::path> pathOpt = mFS.getNativePath(virtualPath);
if (!pathOpt.empty())
{
const fs::path path = std::move(*pathOpt);
if (!fs::exists(path))
{
const bool result = fs::create_directories(path);
MIJIN_ASSERT(result, "Error creating user folder.");
}
}
else
{
MIJIN_ERROR("User folder path shouldn't be empty.");
}
};
createUserDir("/config");
createUserDir("/data");
// in development builds, also add the development folders
#if !defined(RAID_RELEASE)
addConfigDir(fs::current_path() / "data/config");
addDataDir(fs::current_path() / "data/data");
#endif
// now also add the system folders
for (const fs::path& path : mijin::getAllConfigFolders(mijin::IncludeUser::NO))
{
addConfigDir(path / getFolderName());
}
for (const fs::path& path : mijin::getAllDataFolders(mijin::IncludeUser::NO))
{
addDataDir(path / getFolderName());
}
#if !defined(RAID_RELEASE)
for (const mijin::PathReference& reference : mFS.getAllPaths("/config"))
{
reference.getNativePath().then([&](const fs::path& path)
{
msgInfo("Config folder: {}", path.generic_string());
});
}
for (const mijin::PathReference& reference : mFS.getAllPaths("/data"))
{
reference.getNativePath().then([&](const fs::path& path)
{
msgInfo("Data folder: {}", path.generic_string());
});
}
#endif
if (!initSDL())
{
return ERR_INIT_FAILED;
}
if (!initGL())
{
return ERR_INIT_FAILED;
}
if (!initImGui())
{ {
return ERR_INIT_FAILED; return ERR_INIT_FAILED;
} }
@ -198,6 +152,52 @@ bool Application::loadFont(const fs::path& path)
return true; return true;
} }
ImTextureID Application::getOrLoadTexture(fs::path path)
{
if (auto it = mTextures.find(path); it != mTextures.end())
{
return it->second;
}
const stbi_io_callbacks callbacks = {
.read = &stbiRead,
.skip = &stbiSkip,
.eof = &stbiEof
};
std::unique_ptr<mijin::Stream> stream;
if (const mijin::StreamError error = mFS.open(path, mijin::FileOpenMode::READ, stream); error != mijin::StreamError::SUCCESS)
{
msgError("Error opening file {} for reading: {}.", path.generic_string(), mijin::errorName(error));
return 0;
}
int width = 0;
int height = 0;
unsigned char* imageData = stbi_load_from_callbacks(&callbacks, stream.get(), &width, &height, nullptr, 4);
if (imageData == nullptr)
{
msgError("Error parsing image at {}.", path.generic_string());
return 0;
}
// create texture
GLuint texture = 0;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
// setup texture
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// upload image
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, imageData);
mTextures.emplace(std::move(path), texture);
return texture;
}
void Application::configureImgui() void Application::configureImgui()
{ {
ImGuiIO& imguiIO = ImGui::GetIO(); ImGuiIO& imguiIO = ImGui::GetIO();
@ -236,6 +236,131 @@ void Application::handleSDLEvent(const SDL_Event& event)
} }
} }
bool Application::init()
{
auto addConfigDir = [&](const fs::path& path)
{
using namespace mijin::vfs_pipe;
mFS.addAdapter(os()
| relative_to(path)
| map_to("/config")
);
};
auto addDataDir = [&](const fs::path& path)
{
using namespace mijin::vfs_pipe;
mFS.addAdapter(os()
| relative_to(path)
| map_to("/data")
);
};
addConfigDir(mijin::getKnownFolder(mijin::KnownFolder::USER_CONFIG_ROOT) / getFolderName());
addDataDir(mijin::getKnownFolder(mijin::KnownFolder::USER_DATA_ROOT) / getFolderName());
auto createUserDir = [&](const fs::path& virtualPath)
{
mijin::Optional<fs::path> pathOpt = mFS.getNativePath(virtualPath);
if (!pathOpt.empty())
{
const fs::path path = std::move(*pathOpt);
if (!fs::exists(path))
{
const bool result = fs::create_directories(path);
MIJIN_ASSERT(result, "Error creating user folder.");
}
}
else
{
MIJIN_ERROR("User folder path shouldn't be empty.");
}
};
createUserDir("/config");
createUserDir("/data");
// in development builds, also add the development folders
#if !defined(RAID_RELEASE)
addConfigDir(fs::current_path() / "data/config");
addDataDir(fs::current_path() / "data/data");
#endif
// now also add the system folders
for (const fs::path& path : mijin::getAllConfigFolders(mijin::IncludeUser::NO))
{
addConfigDir(path / getFolderName());
}
for (const fs::path& path : mijin::getAllDataFolders(mijin::IncludeUser::NO))
{
addDataDir(path / getFolderName());
}
#if !defined(RAID_RELEASE)
for (const mijin::PathReference& reference : mFS.getAllPaths("/config"))
{
reference.getNativePath().then([&](const fs::path& path)
{
msgInfo("Config folder: {}", path.generic_string());
});
}
for (const mijin::PathReference& reference : mFS.getAllPaths("/data"))
{
reference.getNativePath().then([&](const fs::path& path)
{
msgInfo("Data folder: {}", path.generic_string());
});
}
#endif
if (!initSDL())
{
return false;
}
if (!initGL())
{
return false;
}
if (!initImGui())
{
return false;
}
return true;
}
void Application::cleanup()
{
if (ImGui::GetCurrentContext() != nullptr)
{
saveImGuiConfig();
const ImGuiIO& imguiIO = ImGui::GetIO();
if (imguiIO.BackendRendererUserData != nullptr)
{
ImGui_ImplOpenGL3_Shutdown();
}
if (imguiIO.BackendPlatformUserData != nullptr)
{
ImGui_ImplSDL3_Shutdown();
}
ImGui::DestroyContext();
}
for (const auto& [path, texture] : mTextures)
{
const GLuint asUint = static_cast<GLuint>(texture);
glDeleteTextures(1, &asUint);
}
if (mGLContext != nullptr)
{
SDL_GL_DestroyContext(mGLContext);
}
if (mWindow != nullptr)
{
SDL_DestroyWindow(mWindow);
}
SDL_Quit();
}
bool Application::initSDL() bool Application::initSDL()
{ {
if (!SDL_Init(0)) if (!SDL_Init(0))
@ -280,6 +405,12 @@ bool Application::initGL()
glClear = reinterpret_cast<glClear_fn_t>(SDL_GL_GetProcAddress("glClear")); glClear = reinterpret_cast<glClear_fn_t>(SDL_GL_GetProcAddress("glClear"));
glClearColor = reinterpret_cast<glClearColor_fn_t>(SDL_GL_GetProcAddress("glClearColor")); glClearColor = reinterpret_cast<glClearColor_fn_t>(SDL_GL_GetProcAddress("glClearColor"));
glGenTextures = reinterpret_cast<glGenTextures_fn_t>(SDL_GL_GetProcAddress("glGenTextures"));
glBindTexture = reinterpret_cast<glBindTexture_fn_t>(SDL_GL_GetProcAddress("glBindTexture"));
glTexParameteri = reinterpret_cast<glTexParameteri_fn_t>(SDL_GL_GetProcAddress("glTexParameteri"));
glPixelStorei = reinterpret_cast<glPixelStorei_fn_t>(SDL_GL_GetProcAddress("glPixelStorei"));
glTexImage2D = reinterpret_cast<glTexImage2D_fn_t>(SDL_GL_GetProcAddress("glTexImage2D"));
glDeleteTextures = reinterpret_cast<glDeleteTextures_fn_t>(SDL_GL_GetProcAddress("glDeleteTextures"));
return true; return true;
} }
@ -330,34 +461,6 @@ bool Application::initImGui()
return true; return true;
} }
void Application::cleanup()
{
if (ImGui::GetCurrentContext() != nullptr)
{
saveImGuiConfig();
const ImGuiIO& imguiIO = ImGui::GetIO();
if (imguiIO.BackendRendererUserData != nullptr)
{
ImGui_ImplOpenGL3_Shutdown();
}
if (imguiIO.BackendPlatformUserData != nullptr)
{
ImGui_ImplSDL3_Shutdown();
}
ImGui::DestroyContext();
}
if (mGLContext != nullptr)
{
SDL_GL_DestroyContext(mGLContext);
}
if (mWindow != nullptr)
{
SDL_DestroyWindow(mWindow);
}
SDL_Quit();
}
void Application::handleSDLEvents() void Application::handleSDLEvents()
{ {
SDL_Event event; SDL_Event event;
@ -401,7 +504,7 @@ void Application::saveImGuiConfig()
} }
} }
void QuickApp::init(QuickAppOptions options) void QuickApp::preInit(QuickAppOptions options)
{ {
MIJIN_ASSERT_FATAL(options.callbacks.render, "Missing render callback."); MIJIN_ASSERT_FATAL(options.callbacks.render, "Missing render callback.");
mRenderCallback = std::move(options.callbacks.render); mRenderCallback = std::move(options.callbacks.render);
@ -435,7 +538,7 @@ int runQuick(int argc, char* argv[], QuickAppOptions options)
{ {
QuickApp app; QuickApp app;
gAppInstance = &app; gAppInstance = &app;
app.init(std::move(options)); app.preInit(std::move(options));
return app.run(argc, argv); return app.run(argc, argv);
} }
} }

View File

@ -0,0 +1,4 @@
#define STB_IMAGE_IMPLEMENTATION
#include <stb_image.h>
#undef STB_IMAGE_IMPLEMENTATION

View File

@ -40,15 +40,35 @@ private:
SDL_GLContext mGLContext = nullptr; SDL_GLContext mGLContext = nullptr;
mijin::StackedFileSystemAdapter mFS; mijin::StackedFileSystemAdapter mFS;
std::unordered_map<fs::path, ImTextureID> mTextures;
bool mRunning = true; bool mRunning = true;
ImGuiWindowFlags mMainWindowFlags = DEFAULT_MAIN_WINDOW_FLAGS; ImGuiWindowFlags mMainWindowFlags = DEFAULT_MAIN_WINDOW_FLAGS;
using glClear_fn_t = void (*)(std::uint32_t); using GLbitfield = std::uint32_t;
using glClearColor_fn_t = void (*)(float, float, float, float); 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; glClear_fn_t glClear;
glClearColor_fn_t glClearColor; 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: public:
virtual ~Application() = default; virtual ~Application() = default;
@ -64,6 +84,9 @@ public:
[[nodiscard]] [[nodiscard]]
bool loadFont(const fs::path& path); bool loadFont(const fs::path& path);
[[nodiscard]]
ImTextureID getOrLoadTexture(fs::path path);
protected: protected:
virtual void render() = 0; virtual void render() = 0;
virtual std::string getFolderName() = 0; virtual std::string getFolderName() = 0;
@ -123,11 +146,12 @@ protected:
std::string text = fmt::format(format, std::forward<TArg>(arg), std::forward<TArgs>(args)...); std::string text = fmt::format(format, std::forward<TArg>(arg), std::forward<TArgs>(args)...);
msgError(text); msgError(text);
} }
virtual bool init();
virtual void cleanup();
private: private:
bool initSDL(); bool initSDL();
bool initGL(); bool initGL();
bool initImGui(); bool initImGui();
void cleanup();
void handleSDLEvents(); void handleSDLEvents();
void loadImGuiConfig(); void loadImGuiConfig();
void saveImGuiConfig(); void saveImGuiConfig();
@ -152,7 +176,7 @@ private:
std::string mFolderName; std::string mFolderName;
std::string mWindowTitle; std::string mWindowTitle;
public: public:
void init(QuickAppOptions options); void preInit(QuickAppOptions options);
void render() override; void render() override;
std::string getFolderName() override; std::string getFolderName() override;
std::string getWindowTitle() override; std::string getWindowTitle() override;