From 08ea619a8e1a3a7d8a21d8e8208305f7ece8b08c Mon Sep 17 00:00:00 2001 From: Patrick Wuttke Date: Tue, 4 Mar 2025 00:16:57 +0100 Subject: [PATCH] Added texture managment and overridable init/cleanup functions. --- dependencies.json | 3 +- private/raid/SModule | 1 + private/raid/application.cpp | 333 +++++++++++++++++++++++------------ private/raid/stb_image.cpp | 4 + public/raid/raid.hpp | 32 +++- 5 files changed, 253 insertions(+), 120 deletions(-) create mode 100644 private/raid/stb_image.cpp diff --git a/dependencies.json b/dependencies.json index 4401aa3..5d08343 100644 --- a/dependencies.json +++ b/dependencies.json @@ -12,5 +12,6 @@ }, "SDL": { "min": [3,0,0] - } + }, + "stb": {} } diff --git a/private/raid/SModule b/private/raid/SModule index aa122c9..254ed18 100644 --- a/private/raid/SModule +++ b/private/raid/SModule @@ -5,6 +5,7 @@ Import('env') src_files = Split(""" application.cpp + stb_image.cpp """) with open('../../dependencies.json', 'r') as f: diff --git a/private/raid/application.cpp b/private/raid/application.cpp index 5b3e6cb..aaa78fc 100644 --- a/private/raid/application.cpp +++ b/private/raid/application.cpp @@ -13,14 +13,51 @@ #include #include #include +#include namespace raid { 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"; QuickApp* gAppInstance = nullptr; + +int stbiRead(void* user, char* data, int size) +{ + mijin::Stream& stream = *static_cast(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(read); +} + +void stbiSkip(void* user, int diff) +{ + mijin::Stream& stream = *static_cast(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(user); + return stream.isAtEnd(); +} } int Application::run(int argc, char** argv) @@ -32,90 +69,7 @@ int Application::run(int argc, char** argv) cleanup(); }; - 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 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()) + if (!init()) { return ERR_INIT_FAILED; } @@ -198,6 +152,52 @@ bool Application::loadFont(const fs::path& path) 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 stream; + if (const mijin::StreamError error = mFS.open(path, mijin::FileOpenMode::READ, stream); error != mijin::StreamError::SUCCESS) + { + msgError("Error opening file {} for reading: {}.", path.generic_string(), mijin::errorName(error)); + return 0; + } + int width = 0; + int height = 0; + unsigned char* imageData = stbi_load_from_callbacks(&callbacks, stream.get(), &width, &height, nullptr, 4); + if (imageData == nullptr) + { + msgError("Error parsing image at {}.", path.generic_string()); + return 0; + } + + // create texture + GLuint texture = 0; + glGenTextures(1, &texture); + glBindTexture(GL_TEXTURE_2D, texture); + + // setup texture + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + // upload image + glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, imageData); + + mTextures.emplace(std::move(path), texture); + + return texture; +} + void Application::configureImgui() { ImGuiIO& imguiIO = ImGui::GetIO(); @@ -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 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(texture); + glDeleteTextures(1, &asUint); + } + if (mGLContext != nullptr) + { + SDL_GL_DestroyContext(mGLContext); + } + if (mWindow != nullptr) + { + SDL_DestroyWindow(mWindow); + } + SDL_Quit(); +} + bool Application::initSDL() { if (!SDL_Init(0)) @@ -280,6 +405,12 @@ bool Application::initGL() glClear = reinterpret_cast(SDL_GL_GetProcAddress("glClear")); glClearColor = reinterpret_cast(SDL_GL_GetProcAddress("glClearColor")); + glGenTextures = reinterpret_cast(SDL_GL_GetProcAddress("glGenTextures")); + glBindTexture = reinterpret_cast(SDL_GL_GetProcAddress("glBindTexture")); + glTexParameteri = reinterpret_cast(SDL_GL_GetProcAddress("glTexParameteri")); + glPixelStorei = reinterpret_cast(SDL_GL_GetProcAddress("glPixelStorei")); + glTexImage2D = reinterpret_cast(SDL_GL_GetProcAddress("glTexImage2D")); + glDeleteTextures = reinterpret_cast(SDL_GL_GetProcAddress("glDeleteTextures")); return true; } @@ -330,34 +461,6 @@ bool Application::initImGui() 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() { 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."); mRenderCallback = std::move(options.callbacks.render); @@ -435,7 +538,7 @@ int runQuick(int argc, char* argv[], QuickAppOptions options) { QuickApp app; gAppInstance = &app; - app.init(std::move(options)); + app.preInit(std::move(options)); return app.run(argc, argv); } } diff --git a/private/raid/stb_image.cpp b/private/raid/stb_image.cpp new file mode 100644 index 0000000..c5a52c5 --- /dev/null +++ b/private/raid/stb_image.cpp @@ -0,0 +1,4 @@ + +#define STB_IMAGE_IMPLEMENTATION +#include +#undef STB_IMAGE_IMPLEMENTATION diff --git a/public/raid/raid.hpp b/public/raid/raid.hpp index 3a10f4d..d3a5b99 100644 --- a/public/raid/raid.hpp +++ b/public/raid/raid.hpp @@ -40,15 +40,35 @@ private: SDL_GLContext mGLContext = nullptr; mijin::StackedFileSystemAdapter mFS; + std::unordered_map mTextures; bool mRunning = true; ImGuiWindowFlags mMainWindowFlags = DEFAULT_MAIN_WINDOW_FLAGS; - using glClear_fn_t = void (*)(std::uint32_t); - using glClearColor_fn_t = void (*)(float, float, float, float); + 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; @@ -64,6 +84,9 @@ public: [[nodiscard]] bool loadFont(const fs::path& path); + [[nodiscard]] + ImTextureID getOrLoadTexture(fs::path path); + protected: virtual void render() = 0; virtual std::string getFolderName() = 0; @@ -123,11 +146,12 @@ protected: std::string text = fmt::format(format, std::forward(arg), std::forward(args)...); msgError(text); } + virtual bool init(); + virtual void cleanup(); private: bool initSDL(); bool initGL(); bool initImGui(); - void cleanup(); void handleSDLEvents(); void loadImGuiConfig(); void saveImGuiConfig(); @@ -152,7 +176,7 @@ private: std::string mFolderName; std::string mWindowTitle; public: - void init(QuickAppOptions options); + void preInit(QuickAppOptions options); void render() override; std::string getFolderName() override; std::string getWindowTitle() override;