diff --git a/src/Animation.cpp b/src/Animation.cpp index 636b025..a739d67 100644 --- a/src/Animation.cpp +++ b/src/Animation.cpp @@ -1,3 +1,13 @@ + /* ✨ +------------------------------------------------------+ + ____/ \____ ✨/| Open source game framework licensed to freely use, | +✨\ / / | copy, and modify. Created for 🌠dank.game🌠 | ++--\ . . /--+ | | +| ~/ οΈΆ \πŸ‘| | 🌐 https://open.shampoo.ooo/shampoo/spacebox | +| ~~~🌊~~~~🌊~ | +------------------------------------------------------+ +| SPACE πŸͺπŸ…± OX | / +| 🌊 ~ ~~~~ ~~ |/ ++-------------*/ + #include "Animation.hpp" Animation::Animation() diff --git a/src/Animation.hpp b/src/Animation.hpp index 1c5211a..1a02c98 100644 --- a/src/Animation.hpp +++ b/src/Animation.hpp @@ -1,11 +1,11 @@ - /* +------------------------------------------------------+ - ____/ \____ /| - Open source game framework licensed to freely use, | - \ / / | copy, modify and sell without restriction | -+--\ ^__^ /--+ | | -| ~/ \~ | | - created for | -| ~~~~~~~~~~~~ | +------------------------------------------------------+ -| SPACE ~~~~~ | / -| ~~~~~~~ BOX |/ + /* ✨ +------------------------------------------------------+ + ____/ \____ ✨/| Open source game framework licensed to freely use, | +✨\ / / | copy, and modify. Created for 🌠dank.game🌠 | ++--\ . . /--+ | | +| ~/ οΈΆ \πŸ‘| | 🌐 https://open.shampoo.ooo/shampoo/spacebox | +| ~~~🌊~~~~🌊~ | +------------------------------------------------------+ +| SPACE πŸͺπŸ…± OX | / +| 🌊 ~ ~~~~ ~~ |/ +-------------*/ #pragma once @@ -57,8 +57,8 @@ public: void frame_length(float length); /*! - * Turn the play state to on, causing the animation's callback to run once every frame length. If a delay is given, wait before running. If - * the play_once flag is set to true, only play the callback once after the delay. + * Turn the play state to on, causing the animation's callback to run once every frame length. If a delay is given, wait before + * running. If the play_once flag is set to true, only play the callback once after the delay. * * @param delay Amount of seconds to delay before running * @param play_once If true, only run the callback once instead of once every frame length @@ -91,8 +91,8 @@ public: /*! * Update the timer and check the function's return value to determine whether a new frame of the animation should be produced. * - * This will run the callback automatically if it is stored in this object, but the ability to store the callback in this object is deprecated and - * will be removed soon. + * This will run the callback automatically if it is stored in this object, but the ability to store the callback in this object is + * deprecated and will be removed soon. * * @param timestamp Seconds since the program has started, which can be obtained from Game::update(float) * @return True if the next frame of animation should be triggered, false otherwise diff --git a/src/Configuration.cpp b/src/Configuration.cpp index e09a279..45870da 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -19,7 +19,7 @@ void Configuration::set_defaults() { config["keys"] = { {"record", {"CTRL", "SHIFT", "i"}}, - {"save-current-stash", {"CTRL", "SHIFT", "v"}}, + {"save current stash", {"CTRL", "SHIFT", "v"}}, {"screenshot", {"CTRL", "i"}}, {"action", "space"}, {"up", "up"}, @@ -43,7 +43,7 @@ void Configuration::set_defaults() {"sdl delay", 6}, {"title", "[SPACEBOX]"}, {"debug", false}, - {"show-cursor", false}, + {"show cursor", false}, {"render-test-spacing", 2}, {"render driver", "opengl"}, {"fluid resize", false}, @@ -55,33 +55,35 @@ void Configuration::set_defaults() }; config["audio"] = { {"default-sfx-root", "resource/sfx"}, - {"default-bgm-root", "resource/bgm"} + {"default-bgm-root", "resource/bgm"}, + {"frequency", 48000}, + {"chunk size", 2048} }; config["gl"] = { - {"depth-size", 16}, - {"red-size", 8}, - {"green-size", 8}, - {"blue-size", 8}, - {"share-with-current-context", true}, - {"double-buffer", true}, - {"major-version", 3}, - {"minor-version", 2} + {"depth size", 16}, + {"red size", 8}, + {"green size", 8}, + {"blue size", 8}, + {"share with current context", true}, + {"double buffer", true}, + {"major version", 3}, + {"minor version", 2} }, config["recording"] = { {"enabled", false}, - {"video frame length", 60.0f}, - {"screenshot-prefix", "screenshot-"}, - {"screenshot-extension", ".png"}, - {"screenshot-zfill", 5}, - {"screenshot-directory", "."}, - {"gif-frame-length", 0.1f}, - {"video-directory", "."}, - {"write-mp4", false}, - {"max-stash-length", 5.0f}, - {"max-in-game-stashes", 3}, - {"max-video-stashes", 40}, - {"max-video-memory", 1000}, - {"mp4-pixel-format", "yuv444p"} + {"video frame length", 1.0f / 60.0f}, + {"screenshot prefix", "screenshot-"}, + {"screenshot extension", ".png"}, + {"screenshot zfill", 5}, + {"screenshot directory", "."}, + {"gif frame length", 0.1f}, + {"video directory", "."}, + {"write mp4", false}, + {"max stash length", 5.0f}, + {"max in game stashes", 3}, + {"max video stashes", 40}, + {"max video memory", 1000}, + {"mp4 pixel format", "yuv444p"} }; config["animation"] = { {"all frames frameset name", "all"} diff --git a/src/Delegate.cpp b/src/Delegate.cpp index 244f7d3..9fa30c4 100644 --- a/src/Delegate.cpp +++ b/src/Delegate.cpp @@ -1,11 +1,11 @@ - /* +------------------------------------------------------+ - ____/ \____ /| - Open source game framework licensed to freely use, | - \ / / | copy, modify and sell without restriction | -+--\ ^__^ /--+ | | -| ~/ \~ | | - created for | -| ~~~~~~~~~~~~ | +------------------------------------------------------+ -| SPACE ~~~~~ | / -| ~~~~~~~ BOX |/ + /* ✨ +------------------------------------------------------+ + ____/ \____ ✨/| Open source game framework licensed to freely use, | +✨\ / / | copy, and modify. Created for 🌠dank.game🌠 | ++--\ . . /--+ | | +| ~/ οΈΆ \πŸ‘| | 🌐 https://open.shampoo.ooo/shampoo/spacebox | +| ~~~🌊~~~~🌊~ | +------------------------------------------------------+ +| SPACE πŸͺπŸ…± OX | / +| 🌊 ~ ~~~~ ~~ |/ +-------------*/ #include "Delegate.hpp" diff --git a/src/Delegate.hpp b/src/Delegate.hpp index 39ac269..a943a10 100644 --- a/src/Delegate.hpp +++ b/src/Delegate.hpp @@ -1,11 +1,11 @@ - /* +------------------------------------------------------+ - ____/ \____ /| - Open source game framework licensed to freely use, | - \ / / | copy, modify and sell without restriction | -+--\ ^__^ /--+ | | -| ~/ \~ | | - created for | -| ~~~~~~~~~~~~ | +------------------------------------------------------+ -| SPACE ~~~~~ | / -| ~~~~~~~ BOX |/ + /* ✨ +------------------------------------------------------+ + ____/ \____ ✨/| Open source game framework licensed to freely use, | +✨\ / / | copy, and modify. Created for 🌠dank.game🌠 | ++--\ . . /--+ | | +| ~/ οΈΆ \πŸ‘| | 🌐 https://open.shampoo.ooo/shampoo/spacebox | +| ~~~🌊~~~~🌊~ | +------------------------------------------------------+ +| SPACE πŸͺπŸ…± OX | / +| 🌊 ~ ~~~~ ~~ |/ +-------------*/ #pragma once diff --git a/src/Display.cpp b/src/Display.cpp index 2cca5ee..e3d4c7d 100644 --- a/src/Display.cpp +++ b/src/Display.cpp @@ -73,45 +73,33 @@ Uint32 sb::Display::pixel_format(int display_index) const } } -/* Fill the supplied, pre-allocated buffer with 32-bit pixels (8 bits per component) from the GL - * read buffer if in GL context or from the SDL renderer if in SDL context */ void sb::Display::screen_pixels(unsigned char* pixels, int w, int h, int x, int y) const { - if (get_root()->is_gl_context) + GLenum format; + + /* GL_BGRA is not defined in Open GL ES (some info available at + * https://community.khronos.org/t/why-opengles-2-spec-doesnt-support-bgra-texture-format/72853) */ +#if !defined(__EMSCRIPTEN__) && !defined(__ANDROID__) && !defined(ANDROID) + if constexpr (SDL_BYTEORDER == SDL_BIG_ENDIAN) { - GLenum format; - - /* GL_BGRA is not defined in Open GL ES (some info available at - * https://community.khronos.org/t/why-opengles-2-spec-doesnt-support-bgra-texture-format/72853) */ -#if !defined(__EMSCRIPTEN__) && !defined(__ANDROID__) && !defined(ANDROID) - if constexpr (SDL_BYTEORDER == SDL_BIG_ENDIAN) - { - format = GL_BGRA; - } - else - { -#endif - format = GL_RGBA; -#if !defined(__EMSCRIPTEN__) && !defined(__ANDROID__) && !defined(ANDROID) - } -#endif - - glReadBuffer(GL_FRONT); - glReadPixels(x, y, w, h, format, GL_UNSIGNED_BYTE, pixels); - /* Debug statement showing the framebuffer status and first pixel read */ - std::ostringstream message; - message << "read framebuffer status: " << glCheckFramebufferStatus(GL_READ_FRAMEBUFFER) << - ", draw framebuffer status: " << glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) << - ", upper left screen pixel value read: " << pixels[0] << " " << pixels[1] << " " << pixels[2] << " " << pixels[3]; - sb::Log::log(message, sb::Log::DEBUG); + format = GL_BGRA; } else { - SDL_Renderer* renderer = const_cast(get_renderer()); - SDL_SetRenderTarget(renderer, nullptr); - SDL_RenderPresent(renderer); - SDL_RenderReadPixels(renderer, nullptr, SDL_PIXELFORMAT_RGBA32, pixels, bpp / 8 * w); +#endif + format = GL_RGBA; +#if !defined(__EMSCRIPTEN__) && !defined(__ANDROID__) && !defined(ANDROID) } +#endif + + glReadBuffer(GL_FRONT); + glReadPixels(x, y, w, h, format, GL_UNSIGNED_BYTE, pixels); + /* Debug statement showing the framebuffer status and first pixel read */ + std::ostringstream message; + message << "read framebuffer status: " << glCheckFramebufferStatus(GL_READ_FRAMEBUFFER) << + ", draw framebuffer status: " << glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) << + ", upper left screen pixel value read: " << pixels[0] << " " << pixels[1] << " " << pixels[2] << " " << pixels[3]; + sb::Log::log(message, sb::Log::VERBOSE); } SDL_Surface* sb::Display::screen_surface() const @@ -119,7 +107,7 @@ SDL_Surface* sb::Display::screen_surface() const glm::ivec2 size = window_size(); unsigned char* pixels = new unsigned char[bpp / 8 * size.x * size.y]; screen_pixels(pixels, size.x, size.y); - SDL_Surface* surface = screen_surface_from_pixels(pixels, get_root()->is_gl_context); + SDL_Surface* surface = screen_surface_from_pixels(pixels); delete[] pixels; return surface; } diff --git a/src/Display.hpp b/src/Display.hpp index bab73df..179ed11 100644 --- a/src/Display.hpp +++ b/src/Display.hpp @@ -44,9 +44,21 @@ namespace sb Box window_box(bool = false) const; Box ndc_subsection(const Box&) const; Box ndc_to_pixel(const Box&) const; - void screen_pixels(unsigned char*, int, int, int = 0, int = 0) const; + + /*! + * Fill the supplied, pre-allocated buffer with 32-bit pixels (8 bits per component) from the GL read buffer. This buffer of + * unsigned 8-bit values must be large enough to hold w x h x 32-bits of data + * + * @param pixels Pre-allocated, unsigned 8-bit buffer that will be filled with pixel color data + * @param w Size in width of the region to read from the GL read buffer + * @param h Size in height of the region to read from the GL read buffer + * @param x X position of the corner to start reading from in the GL read buffer + * @param y Y position of the corner to start reading from in the GL read buffer + */ + void screen_pixels(unsigned char* pixels, int w, int h, int x = 0, int y = 0) const; + SDL_Surface* screen_surface() const; - SDL_Surface* screen_surface_from_pixels(unsigned char*, bool) const; + SDL_Surface* screen_surface_from_pixels(unsigned char* pixels, bool flip = true) const; /*! * Respond to full screen requests and window resize events. If fluid resize is enabled in the configuration, respond to diff --git a/src/Game.cpp b/src/Game.cpp index 5a8d73a..61fd18d 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -1,11 +1,11 @@ - /* +------------------------------------------------------+ - ____/ \____ /| - Open source game framework licensed to freely use, | - \ / / | copy, modify and sell without restriction | -+--\ ^__^ /--+ | | -| ~/ \~ | | - created for | -| ~~~~~~~~~~~~ | +------------------------------------------------------+ -| SPACE ~~~~~ | / -| ~~~~~~~ BOX |/ + /* ✨ +------------------------------------------------------+ + ____/ \____ ✨/| Open source game framework licensed to freely use, | +✨\ / / | copy, and modify. Created for 🌠dank.game🌠 | ++--\ . . /--+ | | +| ~/ οΈΆ \πŸ‘| | 🌐 https://open.shampoo.ooo/shampoo/spacebox | +| ~~~🌊~~~~🌊~ | +------------------------------------------------------+ +| SPACE πŸͺπŸ…± OX | / +| 🌊 ~ ~~~~ ~~ |/ +-------------*/ #include "Game.hpp" @@ -46,13 +46,9 @@ Game::Game(std::initializer_list configuration_merge) } /* If recording is enabled by configuration, activate it. */ - if (configuration()["recording"]["enabled"]) + if (configuration()("recording", "enabled")) { - /* Recorder object disabled because of a memory leak - * - * recorder.animation.play() - */ - activate(); + recorder.animation.play(); } /* Log the current working directory as seen by std::filesystem */ @@ -68,7 +64,8 @@ Game::Game(std::initializer_list configuration_merge) log_message = std::ostringstream(); log_message << "Using Android SDK version " << SDL_GetAndroidSDKVersion() << std::endl; log_message << "SDL_AndroidGetInternalStoragePath() is " << SDL_AndroidGetInternalStoragePath() << std::endl; - log_message << "SDL_AndroidGetExternalStorageState() is " << SDL_AndroidGetExternalStorageState() << " (1=read, 2=write, 3=r/w)" << std::endl; + log_message << "SDL_AndroidGetExternalStorageState() is " << SDL_AndroidGetExternalStorageState() << + " (1=read, 2=write, 3=r/w)" << std::endl; log_message << "SDL_AndroidGetExternalStoragePath() is " << SDL_AndroidGetExternalStoragePath(); sb::Log::log(log_message); #endif @@ -78,14 +75,15 @@ Game::Game(std::initializer_list configuration_merge) log_message << std::setw(4) << configuration()() << std::endl; sb::Log::log(log_message, sb::Log::DEBUG); - /* Tell SDL which render driver you will be requesting when calling SDL_CreateRenderer */ + /* Tell SDL which render driver you will be requesting (not sure whether this only affects SDL_ GL */ SDL_SetHint(SDL_HINT_RENDER_DRIVER, configuration()["display"]["render driver"].get().c_str()); /* Initialize the buffer of frame lengths which will be used to calculate and display FPS */ frame_length_history.reserve(5000); - /* Subscribe to SDL's quit event */ + /* Subscribe to events */ _delegate.subscribe(&Game::handle_quit_event, this, SDL_QUIT); + _delegate.subscribe(&Game::respond, this); /* Needed for displaying fullscreen correctly on Linux (?) Also might need SDL_VIDEO_CENTERED (?) */ std::string fullscreen_env_assigment = "SDL_VIDEO_X11_LEGACY_FULLSCREEN=0"; @@ -116,67 +114,85 @@ Game::Game(std::initializer_list configuration_merge) #endif sb::Log::log(log_message.str()); - glm::ivec2 window_size = configuration()["display"]["dimensions"].get(); + glm::ivec2 window_size = _configuration("display", "dimensions").get(); - /* Set GL context attributes before creating a window (see SDL_GLattr.html). Don't ask Emscripten of Android for a specific GL context version. */ + /* Set GL context attributes before creating a window (see SDL_GLattr.html). Don't ask Emscripten or Android for a specific GL + * context version. */ #if !defined(__EMSCRIPTEN__) && !defined(__ANDROID__) && !defined(ANDROID) - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, configuration()["gl"]["major-version"]); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, configuration()["gl"]["minor-version"]); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, _configuration("gl", "major version")); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, _configuration("gl", "minor version")); #endif - SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, configuration()["gl"]["depth-size"]); - SDL_GL_SetAttribute(SDL_GL_RED_SIZE, configuration()["gl"]["red-size"]); - SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, configuration()["gl"]["green-size"]); - SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, configuration()["gl"]["blue-size"]); - SDL_GL_SetAttribute(SDL_GL_SHARE_WITH_CURRENT_CONTEXT, configuration()["gl"]["share-with-current-context"]); - SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, configuration()["gl"]["double-buffer"]); + SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, _configuration("gl", "depth size")); + SDL_GL_SetAttribute(SDL_GL_RED_SIZE, _configuration("gl", "red size")); + SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, _configuration("gl", "green size")); + SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, _configuration("gl", "blue size")); + SDL_GL_SetAttribute(SDL_GL_SHARE_WITH_CURRENT_CONTEXT, _configuration("gl", "share with current context")); + SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, _configuration("gl", "double buffer")); -#if !defined(__MINGW32__) - /* Set the profile to ES so that desktop and web builds are both using the same profile. This should be handled by a configuration - * option in the fututre. */ - SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES); -#else - /* Unless on Windows, which may need to be run in compatibility mode */ + /* Use compatibility mode on Windows because it seemed to be required on one version of Windows tested */ +#if defined(__MINGW32__) SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_COMPATIBILITY); #endif /* Create a window with dimensions set in the config, centered, and flagged to be usable in OpenGL context */ Uint32 flags = SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE; - if (configuration()("display", "fullscreen")) flags |= SDL_WINDOW_FULLSCREEN; - _window = SDL_CreateWindow( - configuration()["display"]["title"].get_ref().c_str(), SDL_WINDOWPOS_CENTERED, - SDL_WINDOWPOS_CENTERED, window_size.x, window_size.y, flags); + if (_configuration("display", "fullscreen")) + { + flags |= SDL_WINDOW_FULLSCREEN; + } + _window = SDL_CreateWindow(_configuration("display", "title").get_ref().c_str(), SDL_WINDOWPOS_CENTERED, + SDL_WINDOWPOS_CENTERED, window_size.x, window_size.y, flags); if (_window == nullptr) { sb::Log::sdl_error("Could not create window"); flag_to_end(); } - - /* Create an SDL renderer for clearing the screen to black and for logging renderer properties. Destroy renderer when finished. - * Skip this in emscripten because it causes a mouse event bug. */ -#ifndef __EMSCRIPTEN__ - if ((renderer = SDL_CreateRenderer(_window, -1, SDL_RENDERER_TARGETTEXTURE | SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC)) == nullptr) + + /* Create a single GL context for the game. This is restrictive since it forces the game to use only one context, so it should + * be improved in the future. */ + if ((glcontext = SDL_GL_CreateContext(window())) == nullptr) { - sb::Log::sdl_error("Could not create renderer"); + sb::Log::sdl_error("Could not get GL context"); flag_to_end(); } + + /* Clear window to black */ + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + SDL_GL_SwapWindow(_window); + + /* Try setting vsync */ + bool vsync_enabled = configuration()("display", "vsync").get(); + if (SDL_GL_SetSwapInterval(static_cast(vsync_enabled)) == 0) + { + std::ostringstream message; + message << "Set vsync to " << vsync_enabled; + sb::Log::log(message); + } else { - int w, h; - /* log the renderer resolution */ - SDL_GetRendererOutputSize(renderer, &w, &h); - log_message = std::ostringstream(); - log_message << "renderer output size is " << w << "x" << h; - sb::Log::log(log_message); - /* clear screen to black */ - SDL_SetRenderTarget(renderer, nullptr); - SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); - SDL_RenderClear(renderer); - SDL_RenderPresent(renderer); - SDL_RenderFlush(renderer); - SDL_DestroyRenderer(renderer); + sb::Log::log("Setting vysnc is not supported"); + } + + /* Initialize GLEW for GL function discovery on all platforms except Android */ +#if !defined(__ANDROID__) && !defined(ANDROID) + GLenum error = glewInit(); + if (error != GLEW_OK) + { + std::ostringstream message; + message << "GLEW could not initialize " << glewGetErrorString(error); + sb::Log::log(message, sb::Log::ERR); } #endif - SDL_ShowCursor(configuration()["display"]["show-cursor"]); + + /* Log the GL profile that was chosen */ + log_gl_properties(); + log_display_mode(); + + /* Show or hide cursor depending on the configuration */ + SDL_ShowCursor(_configuration("display", "show cursor")); + + /* Initialize SDL TTF font library */ if (TTF_Init() < 0) { sb::Log::sdl_error("Could not initialize SDL ttf"); @@ -209,7 +225,8 @@ Game::Game(std::initializer_list configuration_merge) } /* Open the audio device chosen automatically by SDL mixer */ - if (Mix_OpenAudio(48000, MIX_DEFAULT_FORMAT, MIX_DEFAULT_CHANNELS, 4096) < 0) + if (Mix_OpenAudio(_configuration("audio", "frequency"), MIX_DEFAULT_FORMAT, MIX_DEFAULT_CHANNELS, + _configuration("audio", "chunk size")) < 0) { sb::Log::sdl_error("Could not set up audio"); } @@ -227,79 +244,6 @@ Game::Game(std::initializer_list configuration_merge) last_frame_timestamp = SDL_GetTicks(); } -void Game::load_sdl_context() -{ - if (glcontext != nullptr) - { - SDL_GL_DeleteContext(glcontext); - glcontext = nullptr; - } - SDL_RendererInfo renderer_info; - int render_driver_count = SDL_GetNumRenderDrivers(); - SDL_Log("Render drivers:"); - for (int ii = 0; ii < render_driver_count; ii++) - { - SDL_GetRenderDriverInfo(ii, &renderer_info); - log_renderer_info(renderer_info); - } - if ((renderer = SDL_CreateRenderer(_window, -1, SDL_RENDERER_TARGETTEXTURE | SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC)) == nullptr) - { - sb::Log::sdl_error("Could not create renderer"); - flag_to_end(); - } - else - { - SDL_Log("Current renderer:"); - SDL_GetRendererInfo(renderer, &renderer_info); - log_renderer_info(renderer_info); - SDL_Log("Renderer supports the use of render targets? %d", SDL_RenderTargetSupported(renderer)); - } - is_gl_context = false; - log_display_mode(); -} - -void Game::load_gl_context() -{ - if (renderer != nullptr) - { - SDL_DestroyRenderer(renderer); - renderer = nullptr; - } - if ((glcontext = SDL_GL_CreateContext(window())) == nullptr) - { - sb::Log::sdl_error("Could not get GL context"); - flag_to_end(); - } - - /* Try setting vsync */ - bool vsync_enabled = configuration()("display", "vsync").get(); - if (SDL_GL_SetSwapInterval(static_cast(vsync_enabled)) == 0) - { - std::ostringstream message; - message << "Set vsync to " << vsync_enabled; - sb::Log::log(message); - } - else - { - sb::Log::log("Setting vysnc is not supported"); - } - - /* Android does not use GLEW*/ -#if !defined(__ANDROID__) && !defined(ANDROID) - GLenum error = glewInit(); - if (error != GLEW_OK) - { - std::ostringstream message; - message << "GLEW could not initialize " << glewGetErrorString(error); - sb::Log::log(message, sb::Log::ERR); - } -#endif - - log_gl_properties(); - is_gl_context = true; - log_display_mode(); -} - void Game::sdl_log_override(void* userdata, int category, SDL_LogPriority priority, const char* message) { Game* game = static_cast(userdata); @@ -312,7 +256,8 @@ void Game::sdl_log_override(void* userdata, int category, SDL_LogPriority priori /* If printing to stdout, print to Android log as well */ #if defined(__ANDROID__) || defined(ANDROID) - __android_log_print(ANDROID_LOG_VERBOSE, game->configuration()["log"]["short-name"].get_ref().c_str(), "%s", message); + __android_log_print(ANDROID_LOG_VERBOSE, game->configuration()["log"]["short-name"].get_ref().c_str(), + "%s", message); #endif } @@ -419,15 +364,6 @@ bool Game::link_shader(GLuint program) const } } -void Game::log_renderer_info(SDL_RendererInfo& info) -{ - std::ostringstream message; - message << "renderer name: " << info.name << ", flags: " << info.flags << ", texture formats: " << - info.num_texture_formats << ", max texture w: " << info.max_texture_width << ", max texture h: " << - info.max_texture_height; - sb::Log::log(message); -} - void Game::log_display_mode() const { SDL_DisplayMode current; @@ -463,28 +399,28 @@ void Game::log_gl_properties() const int attribute; int status = SDL_GL_GetAttribute(SDL_GL_RED_SIZE, &attribute); if (!status) { - message << ", SDL_GL_RED_SIZE: requested " << configuration()["gl"]["red-size"] << ", got " << attribute; + message << ", SDL_GL_RED_SIZE: requested " << _configuration("gl", "red size") << ", got " << attribute; } else { sb::Log::sdl_error("Failed to get SDL_GL_RED_SIZE"); } status = SDL_GL_GetAttribute(SDL_GL_GREEN_SIZE, &attribute); if (!status) { - message << ", SDL_GL_GREEN_SIZE: requested " << configuration()["gl"]["green-size"] << ", got " << attribute; + message << ", SDL_GL_GREEN_SIZE: requested " << _configuration("gl", "green size") << ", got " << attribute; } else { sb::Log::sdl_error("Failed to get SDL_GL_GREEN_SIZE"); } status = SDL_GL_GetAttribute(SDL_GL_BLUE_SIZE, &attribute); if (!status) { - message << ", SDL_GL_BLUE_SIZE: requested " << configuration()["gl"]["blue-size"] << ", got " << attribute; + message << ", SDL_GL_BLUE_SIZE: requested " << _configuration("gl", "blue size") << ", got " << attribute; } else { sb::Log::sdl_error("Failed to get SDL_GL_BLUE_SIZE"); } status = SDL_GL_GetAttribute(SDL_GL_DEPTH_SIZE, &attribute); if (!status) { - message << ", SDL_GL_DEPTH_SIZE: requested " << configuration()["gl"]["depth-size"] << ", got " << attribute; + message << ", SDL_GL_DEPTH_SIZE: requested " << _configuration("gl", "depth size") << ", got " << attribute; } else { sb::Log::sdl_error("Failed to get SDL_GL_DEPTH_SIZE"); } @@ -535,16 +471,6 @@ SDL_Window* Game::window() return _window; } -const SDL_Renderer* Game::get_renderer() const -{ - return renderer; -} - -SDL_Renderer* Game::get_renderer() -{ - return renderer; -} - const Input& Game::get_input() const { return input; @@ -594,7 +520,8 @@ void Game::run() SDL_Log("using emscripten main loop"); emscripten_set_main_loop_arg(&loop, this, -1, true); - /* Other builds request a frame repeatedly until the game quits, delaying for the configured amount of milliseconds between each request. */ + /* Other builds request a frame repeatedly until the game quits, delaying for the configured amount of milliseconds between each + * request. */ #else SDL_Log("using standard main loop"); while (!done) @@ -605,6 +532,26 @@ void Game::run() #endif } +void Game::respond(SDL_Event& event) +{ + if (_delegate.compare(event, "screenshot")) + { + recorder.capture_screen(); + } + else if (_delegate.compare(event, "record")) + { + recorder.toggle(); + } + else if (_delegate.compare(event, "save current stash")) + { + recorder.grab_stash(); + } + else if (_delegate.compare(event, "log video memory size")) + { + recorder.log_video_memory_size(); + } +} + void Game::frame(float timestamp) { /* Max framerate is the maximum number of frames to display per second. It is unlimited if the value is -1. */ @@ -626,21 +573,14 @@ void Game::frame(float timestamp) /* Save the timestamp */ last_frame_timestamp = timestamp; - // /* Only build a frame if the last frame was under a second. */ - // if (last_frame_length < 1000) - // { - float timestamp_seconds = timestamp / 1000.0f; - // recorder.update(timestamp_seconds); - _delegate.dispatch(); - input.unsuppress_animation.update(timestamp_seconds); - update(timestamp_seconds); - _configuration.update(timestamp_seconds); - if (!is_gl_context) - { - SDL_SetRenderTarget(renderer, nullptr); - SDL_RenderPresent(renderer); - } - // } + /* Update game state and render frame (update and draw should be separated into different functions in a future revision to + * allow for different timing intervals for each) */ + float timestamp_seconds = timestamp / 1000.0f; + recorder.update(timestamp_seconds); + _delegate.dispatch(); + input.unsuppress_animation.update(timestamp_seconds); + update(timestamp_seconds); + _configuration.update(timestamp_seconds); /* Update frame count per second for verbose log */ frame_count_this_second++; @@ -694,10 +634,6 @@ void Game::quit() { SDL_GL_DeleteContext(glcontext); } - if (renderer != nullptr) - { - SDL_DestroyRenderer(renderer); - } if (_window != nullptr) { SDL_DestroyWindow(_window); diff --git a/src/Game.hpp b/src/Game.hpp index 0092bd3..07afe16 100644 --- a/src/Game.hpp +++ b/src/Game.hpp @@ -1,11 +1,11 @@ - /* +------------------------------------------------------+ - ____/ \____ /| - Open source game framework licensed to freely use, | - \ / / | copy, modify and sell without restriction | -+--\ ^__^ /--+ | | -| ~/ \~ | | - created for | -| ~~~~~~~~~~~~ | +------------------------------------------------------+ -| SPACE ~~~~~ | / -| ~~~~~~~ BOX |/ + /* ✨ +------------------------------------------------------+ + ____/ \____ ✨/| Open source game framework licensed to freely use, | +✨\ / / | copy, and modify. Created for 🌠dank.game🌠 | ++--\ . . /--+ | | +| ~/ οΈΆ \πŸ‘| | 🌐 https://open.shampoo.ooo/shampoo/spacebox | +| ~~~🌊~~~~🌊~ | +------------------------------------------------------+ +| SPACE πŸͺπŸ…± OX | / +| 🌊 ~ ~~~~ ~~ |/ +-------------*/ #pragma once @@ -99,24 +99,19 @@ public: Game(Game&&) = delete; Game& operator=(Game&&) = delete; - SDL_Renderer* renderer = nullptr; SDL_GLContext glcontext = nullptr; int frame_count_this_second = 0, last_frame_length, current_frames_per_second = 0; float frame_time_overflow = 0, last_frame_timestamp, last_frame_count_timestamp; - bool done = false, show_framerate = true, is_gl_context = true; + bool done = false, show_framerate = true; sb::Display display {this}; - // Recorder recorder {this}; Input input {this}; std::vector frame_length_history; + sb::Recorder recorder {_configuration, display}; Game(std::initializer_list configuration_merge); - virtual void reset() { activate(); } void print_frame_length_history(); - void load_sdl_context(); - void load_gl_context(); GLuint load_shader(const fs::path&, GLenum) const; bool link_shader(GLuint program) const; - void log_renderer_info(SDL_RendererInfo&); /*! * Write resolution, monitor refresh rate, and pixel format to the log. Taken from SDL_GetCurrentDisplayMode.html @@ -130,15 +125,53 @@ public: void log_gl_properties() const; void log_surface_format(SDL_Surface*, std::string = "surface"); + + /*! + * @deprecated Global configuration will be removed in favor of a mix of default configuration and user configuration(s) + * which can be swapped in and out arbitrarily. For now, it's better to pass the global configuration directly to objects + * which need it instead of relying on this public accessor. + */ const Configuration& configuration() const; + + /*! + * @deprecated Global configuration will be removed in favor of a mix of default configuration and user configuration(s) + * which can be swapped in and out arbitrarily. For now, it's better to pass the global configuration directly to objects + * which need it instead of relying on this public accessor. + */ Configuration& configuration(); + + /*! + * @deprecated The delegate class will be kept private to the Game object. Instead of subscribing individual objects to specific + * input, subscribe and respond to all events by extending Game::response and subscribing new events if necessary. + */ const sb::Delegate& delegate() const; + + /*! + * @deprecated The delegate class will be kept private to the Game object. Instead of subscribing individual objects to specific + * input, subscribe and respond to all events by extending Game::response and subscribing new events if necessary. + */ sb::Delegate& delegate(); + + /*! + * @deprecated The window will be kept private and access will have to come directly from the class. + */ const SDL_Window* window() const; + + /*! + * @deprecated The window will be kept private and access will have to come directly from the class. + */ SDL_Window* window(); - const SDL_Renderer* get_renderer() const; - SDL_Renderer* get_renderer(); + + /*! + * @deprecated The input class's functionality will be kept private to Game objects, so access will need to go through the Game + * class. + */ const Input& get_input() const; + + /*! + * @deprecated The input class's functionality will be kept private to Game objects, so access will need to go through the Game + * class. + */ Input& get_input(); /*! @@ -147,14 +180,31 @@ public: std::shared_ptr font() const; /*! - * Get a new font object with the given font size. If the font cannot be loaded, the default font will be returned. If there was an error, - * the shared pointer will point to `nullptr`. + * Get a new font object with the given font size. If the font cannot be loaded, the default font will be returned. If there was an + * error, the shared pointer will point to `nullptr`. * * @return shared pointer to the font object created from the TTF font file at the given path */ std::shared_ptr font(const fs::path& path, int size) const; void run(); + + /*! + * Dispatch framework events to relevant manager classes like Input, Display, and Recorder. + * + * Extend this function to respond to custom framework events. Use Delegate::compare to check which event has been fired. + * + * Framework events like "reset", "quit", and "up", have default input bindings that can be reconfigured or extended to respond + * to more bindings. + * + * @see Input::add_to_key_map + * @see Input::load_key_map + * + * To respond directly to lower-level SDL events like keyboard, mouse, and gamepad input directly, subscribe this function + * to a specific type of SDL event using Delegate::add_subscriber. + */ + virtual void respond(SDL_Event& event); + void frame(float); void flag_to_end(); virtual void update(float timestamp) = 0; diff --git a/src/Node.cpp b/src/Node.cpp index 9198510..a773ab1 100644 --- a/src/Node.cpp +++ b/src/Node.cpp @@ -23,16 +23,6 @@ void Node::set_parent(Node* other) parent = other; } -void Node::set_canvas(SDL_Texture* texture) -{ - canvas = std::shared_ptr(texture, SDL_DestroyTexture); -} - -SDL_Texture* Node::get_canvas() -{ - return canvas.get(); -} - bool Node::is_active() const { return active; diff --git a/src/Node.hpp b/src/Node.hpp index bd05f63..40896b5 100644 --- a/src/Node.hpp +++ b/src/Node.hpp @@ -35,6 +35,10 @@ namespace sb class Display; } +/*! + * @deprecated Use an alternative to this class if possible because it will be removed soon. Global access to functionality in + * this class should instead go through the Game class directly. + */ class Node { @@ -46,8 +50,6 @@ public: virtual void reset() { deactivate(); } virtual void activate() { active = true; } virtual void deactivate() { active = false; } - void set_canvas(SDL_Texture*); - SDL_Texture* get_canvas(); bool is_active() const; const Configuration& configuration() const; Configuration& configuration(); @@ -100,7 +102,6 @@ public: private: Node* parent; - std::shared_ptr canvas; bool active = true; }; diff --git a/src/Recorder.cpp b/src/Recorder.cpp index 36f5bcf..0db41ec 100644 --- a/src/Recorder.cpp +++ b/src/Recorder.cpp @@ -1,68 +1,56 @@ + /* ✨ +------------------------------------------------------+ + ____/ \____ ✨/| Open source game framework licensed to freely use, | +✨\ / / | copy, and modify. Created for 🌠dank.game🌠 | ++--\ . . /--+ | | +| ~/ οΈΆ \πŸ‘| | 🌐 https://open.shampoo.ooo/shampoo/spacebox | +| ~~~🌊~~~~🌊~ | +------------------------------------------------------+ +| SPACE πŸͺπŸ…± OX | / +| 🌊 ~ ~~~~ ~~ |/ ++-------------*/ + #include "gif-h/gif.h" #include "Game.hpp" #include "extension.hpp" #include "Recorder.hpp" +using namespace sb; + /* Create a Recorder instance. Subscribe to command input and set audio callback. */ -Recorder::Recorder(Node* parent) : Node(parent) +Recorder::Recorder(sb::Configuration& configuration, sb::Display& display) : configuration(configuration), display(display) { - get_delegate().subscribe(&Recorder::respond, this); - // Mix_SetPostMix(Recorder::process_audio, this); + Mix_SetPostMix(Recorder::process_audio, this); +} + +void Recorder::toggle() +{ + if (is_recording) + { + end_recording(); + } + else if (!writing_recording) + { + start_recording(); + } + else + { + sb::Log::log("Writing in progress, cannot start recording", sb::Log::WARN); + } } /* Returns length of a recorded video frame in seconds */ float Recorder::frame_length() { - return configuration()["recording"]["video frame length"]; + return configuration("recording", "video frame length"); } -/* Handle commands for screenshot, record video and save video */ -void Recorder::respond(SDL_Event& event) -{ - if (get_delegate().compare(event, "screenshot")) - { - capture_screen(); - } - else if (get_delegate().compare(event, "record")) - { - if (is_recording) - { - end_recording(); - } - else if (!writing_recording) - { - start_recording(); - } - else - { - sb::Log::log("Writing in progress, cannot start recording"); - } - } - else if (get_delegate().compare(event, "save-current-stash")) - { - grab_stash(); - } - else if (get_delegate().compare(event, "print-video-memory-size")) - { - std::ostringstream message; - message << "Video memory size is " << get_memory_size() << "MB"; - sb::Log::log(message); - } -} - -/* Save the current screen pixels as a PNG in the path specified by the configuration. Parent - * directories in the path will be created if necessary. The file name will be auto generated - * based on the configuration. An index will be included in the file name based on previous - * screenshots found in the output directory. */ void Recorder::capture_screen() { - const nlohmann::json& config = configuration()(); - SDL_Surface* surface = get_display().screen_surface(); - fs::path directory = config["recording"]["screenshot-directory"].get(); + SDL_Surface* surface = display.screen_surface(); + fs::path directory = configuration("recording", "screenshot directory").get(); fs::create_directories(directory); - std::string prefix = config["recording"]["screenshot-prefix"].get(); - std::string extension = config["recording"]["screenshot-extension"].get(); - int zfill = config["recording"]["screenshot-zfill"]; + std::string prefix = configuration("recording", "screenshot prefix").get(); + std::string extension = configuration("recording", "screenshot extension").get(); + int zfill = configuration("recording", "screenshot zfill"); fs::path path = sb::get_next_file_name(directory, zfill, prefix, extension); IMG_SavePNG(surface, path.string().c_str()); SDL_FreeSurface(surface); @@ -78,9 +66,9 @@ void Recorder::grab_stash() { if (!is_recording and !writing_recording) { - int length = configuration()["recording"]["max-stash-length"]; + int length = configuration("recording", "max stash length"); std::ostringstream message; - message << "stashing most recent " << length / 1000.0f << " seconds of video"; + message << "stashing most recent " << length << " seconds of video"; sb::Log::log(message); most_recent_stash = current_stash; current_stash = Stash(); @@ -107,7 +95,7 @@ void Recorder::write_most_recent_frames() most_recent_stash.audio_buffer_lengths.erase(most_recent_stash.audio_buffer_lengths.begin()); } audio_file.close(); - if (configuration()["recording"]["write-mp4"]) + if (configuration("recording", "write mp4")) { write_mp4(); } @@ -143,11 +131,11 @@ void Recorder::open_audio_file() void Recorder::add_frame() { - glm::ivec2 size = get_display().window_size(); + glm::ivec2 size = display.window_size(); int bytes = sb::Display::bpp / 8 * size.x * size.y; unsigned char* pixels = new unsigned char[bytes]; - get_display().screen_pixels(pixels, size.x, size.y); - int max_length = configuration()["recording"]["max-stash-length"]; + display.screen_pixels(pixels, size.x, size.y); + int max_length = configuration("recording", "max stash length"); float length = frame_length() * current_stash.pixel_buffers.size(); if (length > max_length) { @@ -156,76 +144,63 @@ void Recorder::add_frame() current_stash.flipped.erase(current_stash.flipped.begin()); } current_stash.pixel_buffers.push_back(pixels); - current_stash.flipped.push_back(get_root()->is_gl_context); + current_stash.flipped.push_back(true); if (is_recording) { unsigned char* vid_pixels = new unsigned char[bytes]; memcpy(vid_pixels, pixels, bytes); video_stashes.back().pixel_buffers.push_back(vid_pixels); - video_stashes.back().flipped.push_back(get_root()->is_gl_context); + video_stashes.back().flipped.push_back(true); if (video_stashes.back().pixel_buffers.size() * frame_length() > max_length) { std::function f = std::bind(&Recorder::write_stash_frames, this, std::placeholders::_1); std::thread writing(f, &video_stashes.back()); writing.detach(); - // int frame_offset = video_stashes.back().frame_offset; - // std::vector pixel_buffers = video_stashes.back().pixel_buffers; - // std::vector flipped = video_stashes.back().flipped; - // for (int ii = frame_offset; ii < pixel_buffers.size() + frame_offset; ii++) - // { - // SDL_Surface* frame = get_display().screen_surface_from_pixels( - // pixel_buffers[ii - frame_offset], flipped[ii - frame_offset]); - // std::stringstream name; - // name << sb::pad(ii, 5) << ".png"; - // fs::path path = current_video_directory / name.str(); - // SDL_Log("%s (%i, %i) (%i, %i, %i, %i)", path.c_str(), frame->w, frame->h, - // ((unsigned char*) frame->pixels)[0], ((unsigned char*) frame->pixels)[1], - // ((unsigned char*) frame->pixels)[2], ((unsigned char*) frame->pixels)[3]); - // IMG_SavePNG(frame, path.string().c_str()); - // } - // end_recording(); - // write_stash_frames(video_stashes.back().pixel_buffers, - // video_stashes.back().flipped, - // video_stashes.back().frame_offset); - video_stashes.push_back(Stash(video_stashes.back().frame_offset + - video_stashes.back().pixel_buffers.size())); + video_stashes.push_back(Stash(video_stashes.back().frame_offset + video_stashes.back().pixel_buffers.size())); } } } -int Recorder::get_memory_size() +float Recorder::get_memory_size() const { - glm::ivec2 window = get_display().window_size(); - int bytes_per_frame = sb::Display::bpp / 8 * window.x * window.y, - size_in_bytes = 0; - for (Stash& stash : in_game_stashes) + glm::ivec2 window = display.window_size(); + int bytes_per_frame = sb::Display::bpp / 8 * window.x * window.y; + int size_in_bytes = 0; + for (const Stash& stash : in_game_stashes) { size_in_bytes += stash.pixel_buffers.size() * bytes_per_frame; - for (int& length : stash.audio_buffer_lengths) + for (const int& length : stash.audio_buffer_lengths) { size_in_bytes += length; } } - for (Stash& stash : video_stashes) + for (const Stash& stash : video_stashes) { size_in_bytes += stash.pixel_buffers.size() * bytes_per_frame; } size_in_bytes += current_stash.pixel_buffers.size() * bytes_per_frame; - for (int& length : current_stash.audio_buffer_lengths) + for (const int& length : current_stash.audio_buffer_lengths) { size_in_bytes += length; } size_in_bytes += most_recent_stash.pixel_buffers.size() * bytes_per_frame; - for (int& length : most_recent_stash.audio_buffer_lengths) + for (const int& length : most_recent_stash.audio_buffer_lengths) { size_in_bytes += length; } - return size_in_bytes / 1000000; + return size_in_bytes / 1'000'000.0; +} + +void Recorder::log_video_memory_size() const +{ + std::ostringstream message; + message << "Video memory size is " << std::fixed << std::setprecision(2) << get_memory_size() << "MB"; + sb::Log::log(message); } void Recorder::make_directory() { - fs::path root = configuration()["recording"]["video-directory"].get(); + fs::path root = configuration("recording", "video directory").get(); fs::create_directories(root); fs::path directory = sb::get_next_file_name(root, 5, "video-"); fs::create_directories(directory); @@ -237,35 +212,29 @@ void Recorder::write_stash_frames(Stash* stash) SDL_Log("Writing stash offset %i to %s...", stash->frame_offset, current_video_directory.string().c_str()); SDL_Surface* frame; GifWriter gif_writer; - int gif_frame_length = configuration()["recording"]["gif-frame-length"]; - fs::path gif_path = sb::get_next_file_name( - current_video_directory, 3, "gif-", ".gif"); + float gif_frame_length = configuration("recording", "gif frame length"); + fs::path gif_path = sb::get_next_file_name(current_video_directory, 3, "gif-", ".gif"); float elapsed = 0, last_gif_write = 0, gif_write_overflow = 0; - for (int ii = stash->frame_offset; not stash->pixel_buffers.empty(); ii++) + for (int ii = stash->frame_offset; !stash->pixel_buffers.empty(); ii++) { - frame = get_display().screen_surface_from_pixels( - stash->pixel_buffers.front(), stash->flipped.front()); + frame = display.screen_surface_from_pixels(stash->pixel_buffers.front(), stash->flipped.front()); std::stringstream name; name << sb::pad(ii, 5) << ".png"; fs::path path = current_video_directory / name.str(); IMG_SavePNG(frame, path.string().c_str()); - if (ii == stash->frame_offset or - elapsed - last_gif_write + gif_write_overflow >= gif_frame_length) + if (ii == stash->frame_offset || elapsed - last_gif_write + gif_write_overflow >= gif_frame_length) { if (ii == stash->frame_offset) { - GifBegin(&gif_writer, gif_path.string().c_str(), frame->w, - frame->h, gif_frame_length * 100); + GifBegin(&gif_writer, gif_path.string().c_str(), frame->w, frame->h, gif_frame_length * 100); } else { gif_write_overflow += elapsed - (last_gif_write + gif_frame_length); last_gif_write = elapsed; } - SDL_Surface* converted = SDL_ConvertSurfaceFormat( - frame, SDL_PIXELFORMAT_ABGR8888, 0); - GifWriteFrame(&gif_writer, (const uint8_t*) converted->pixels, - frame->w, frame->h, gif_frame_length * 100); + SDL_Surface* converted = SDL_ConvertSurfaceFormat(frame, SDL_PIXELFORMAT_ABGR8888, 0); + GifWriteFrame(&gif_writer, (const uint8_t*) converted->pixels, frame->w, frame->h, gif_frame_length * 100); } elapsed += frame_length(); delete[] stash->pixel_buffers.front(); @@ -280,7 +249,7 @@ void Recorder::keep_stash() { in_game_stashes.push_back(current_stash); current_stash = Stash(); - auto max_stashes = configuration()["recording"]["max-in-game-stashes"]; + auto max_stashes = configuration("recording", "max in game stashes"); if (in_game_stashes.size() > max_stashes) { Stash& stash = in_game_stashes.front(); @@ -322,7 +291,7 @@ void Recorder::finish_writing_video() } } video_stashes.clear(); - if (configuration()["recording"]["write-mp4"]) + if (configuration("recording", "write mp4")) { write_mp4(); } @@ -334,13 +303,21 @@ void Recorder::finish_writing_video() * This requires ffmpeg to be installed on the user's system. Might only work on Linux (?) */ void Recorder::write_mp4() { - glm::ivec2 size = get_display().window_size(); + glm::ivec2 size = display.window_size(); std::ostringstream mp4_command; - std::string pixel_format = configuration()["recording"]["mp4-pixel-format"].get(); + std::string pixel_format = configuration("recording", "mp4 pixel format").get(); + int audio_frequency; + Mix_QuerySpec(&audio_frequency, nullptr, nullptr); fs::path images_match = current_video_directory / "%05d.png"; - mp4_command << "ffmpeg -f s16le -ac 2 -ar 22050 -i " << current_audio_path.string() << + int frame_count = 0; + for (auto& p: fs::directory_iterator(current_video_directory)) + { + frame_count++; + } + float video_length = frame_count * frame_length(); + mp4_command << "ffmpeg -f s16le -ac 2 -ar " << audio_frequency << " -i " << current_audio_path.string() << " -f image2 -framerate " << (1.0f / frame_length()) << - " -i " << images_match.string() << " -s " << size.x << "x" << size.y << + " -i " << images_match.string() << " -s " << size.x << "x" << size.y << " -t " << video_length << " -c:v libx264 -crf 17 -pix_fmt " << pixel_format << " " << current_video_directory.string() << ".mp4"; std::string mp4_command_str = mp4_command.str(); @@ -355,7 +332,7 @@ void Recorder::write_audio(Uint8* stream, int len) void Recorder::update(float timestamp) { - if (is_recording and get_memory_size() > configuration()["recording"]["max-video-memory"]) + if (is_recording && get_memory_size() > configuration("recording", "max video memory")) { end_recording(); } @@ -366,16 +343,15 @@ void Recorder::update(float timestamp) void Recorder::process_audio(void* context, Uint8* stream, int len) { Recorder* recorder = static_cast(context); - if (recorder->is_active()) + if (recorder->configuration("recording", "enabled")) { - int max_length = recorder->configuration()["recording"]["max-stash-length"]; + int max_length = recorder->configuration("recording", "max stash length"); float length = recorder->frame_length() * recorder->current_stash.pixel_buffers.size(); if (length > max_length) { delete[] recorder->current_stash.audio_buffers.front(); recorder->current_stash.audio_buffers.erase(recorder->current_stash.audio_buffers.begin()); - recorder->current_stash.audio_buffer_lengths.erase( - recorder->current_stash.audio_buffer_lengths.begin()); + recorder->current_stash.audio_buffer_lengths.erase(recorder->current_stash.audio_buffer_lengths.begin()); } Uint8* stream_copy = new Uint8[len]; std::memcpy(stream_copy, stream, len); diff --git a/src/Recorder.hpp b/src/Recorder.hpp index 2e5b08e..a2cb84d 100644 --- a/src/Recorder.hpp +++ b/src/Recorder.hpp @@ -1,11 +1,11 @@ - /* +------------------------------------------------------+ - ____/ \____ /| - Open source game framework licensed to freely use, | - \ / / | copy, modify and sell without restriction | -+--\ ^__^ /--+ | | -| ~/ \~ | | - created for | -| ~~~~~~~~~~~~ | +------------------------------------------------------+ -| SPACE ~~~~~ | / -| ~~~~~~~ BOX |/ + /* ✨ +------------------------------------------------------+ + ____/ \____ ✨/| Open source game framework licensed to freely use, | +✨\ / / | copy, and modify. Created for 🌠dank.game🌠 | ++--\ . . /--+ | | +| ~/ οΈΆ \πŸ‘| | 🌐 https://open.shampoo.ooo/shampoo/spacebox | +| ~~~🌊~~~~🌊~ | +------------------------------------------------------+ +| SPACE πŸͺπŸ…± OX | / +| 🌊 ~ ~~~~ ~~ |/ +-------------*/ #pragma once @@ -25,59 +25,85 @@ #include "glm/ext.hpp" #include "json/json.hpp" #include "filesystem.hpp" -#include "Node.hpp" #include "Configuration.hpp" #include "Animation.hpp" #include "Log.hpp" -struct Stash +namespace sb { - std::vector pixel_buffers; - std::vector flipped; - std::vector audio_buffers; - std::vector audio_buffer_lengths; - int frame_offset; + struct Stash + { + std::vector pixel_buffers; + std::vector flipped; + std::vector audio_buffers; + std::vector audio_buffer_lengths; + int frame_offset; - Stash(int frame_offset = 0) : frame_offset(frame_offset) {} -}; + Stash(int frame_offset = 0) : frame_offset(frame_offset) {} + }; -class Recorder : public Node -{ + class Recorder + { -private: + private: - Stash current_stash = Stash(); - Stash most_recent_stash; - std::list in_game_stashes; - std::list video_stashes; - fs::path current_video_directory, current_audio_path; - bool is_recording = false, writing_recording = false, writing_most_recent = false; - std::ofstream audio_file; + Stash current_stash = Stash(); + Stash most_recent_stash; + std::list in_game_stashes; + std::list video_stashes; + fs::path current_video_directory, current_audio_path; + bool is_recording = false, writing_recording = false, writing_most_recent = false; + std::ofstream audio_file; + sb::Configuration& configuration; + sb::Display& display; - float frame_length(); - static void process_audio(void*, Uint8*, int); + float frame_length(); + static void process_audio(void*, Uint8*, int); -public: + public: - Animation animation = Animation(std::bind(&Recorder::add_frame, this)); + Animation animation = Animation(std::bind(&Recorder::add_frame, this)); - Recorder(Node*); - void respond(SDL_Event&); - void capture_screen(); - void grab_stash(); - void write_most_recent_frames(); - void start_recording(); - void open_audio_file(); - void add_frame(); - int get_memory_size(); - void make_directory(); - void write_stash_frames(Stash*); - void keep_stash(); - void end_recording(); - void finish_writing_video(); - void write_mp4(); - void write_audio(Uint8*, int); - void update(float timestamp); - virtual std::string class_name() const { return "Recorder"; } + Recorder(sb::Configuration& configuration, sb::Display& display); + /*! + * If not recording, start recording. If recording, stop recording. + * + * If a recording is being written, recording will not begin, and a warning will be logged. + */ + void toggle(); + + /*! + * Save the current screen pixels as a PNG in the path specified by the configuration. + * + * Parent directories in the path will be created if necessary. The file name will be auto generated based on the + * configuration. + * + * An index will be included in the file name, one higher than the highest of previous screenshots found in the output + * directory. + */ + void capture_screen(); + + void grab_stash(); + void write_most_recent_frames(); + void start_recording(); + void open_audio_file(); + void add_frame(); + float get_memory_size() const; + + /*! + * Write the size of the video memory in MB to the log. + */ + void log_video_memory_size() const; + + void make_directory(); + void write_stash_frames(Stash*); + void keep_stash(); + void end_recording(); + void finish_writing_video(); + void write_mp4(); + void write_audio(Uint8*, int); + void update(float timestamp); + + }; }; diff --git a/src/Texture.cpp b/src/Texture.cpp index 85db7c4..d6a37f1 100644 --- a/src/Texture.cpp +++ b/src/Texture.cpp @@ -1,14 +1,15 @@ - /* +------------------------------------------------------+ - ____/ \____ /| - Open source game framework licensed to freely use, | - \ / / | copy, modify and sell without restriction | -+--\ ^__^ /--+ | | -| ~/ \~ | | - created for | -| ~~~~~~~~~~~~ | +------------------------------------------------------+ -| SPACE ~~~~~ | / -| ~~~~~~~ BOX |/ + /* ✨ +------------------------------------------------------+ + ____/ \____ ✨/| Open source game framework licensed to freely use, | +✨\ / / | copy, and modify. Created for 🌠dank.game🌠 | ++--\ . . /--+ | | +| ~/ οΈΆ \πŸ‘| | 🌐 https://open.shampoo.ooo/shampoo/spacebox | +| ~~~🌊~~~~🌊~ | +------------------------------------------------------+ +| SPACE πŸͺπŸ…± OX | / +| 🌊 ~ ~~~~ ~~ |/ +-------------*/ #include "Texture.hpp" + using namespace sb; Texture::Texture() : GLObject(texture_deleter) {} @@ -35,8 +36,8 @@ void Texture::generate() void Texture::generate(glm::vec2 size, GLenum format, std::optional filter_value) { - /* Only generate a new texture ID and reallocate memory if the current texture ID hasn't been registered by this object as having identically - * sized memory with the same format. */ + /* Only generate a new texture ID and reallocate memory if the current texture ID hasn't been registered by this object as having + * identically sized memory with the same format. */ if (!generated() || !_size.has_value() || !_format.has_value() || _size != size || _format != format) { generate(); @@ -105,7 +106,8 @@ void Texture::load(fs::path path) std::unique_ptr surface(IMG_Load(path.string().c_str()), SDL_FreeSurface); if (surface.get() != nullptr) { - std::unique_ptr flipped_surface(rotozoomSurfaceXY(surface.get(), 0, 1, -1, 0), SDL_FreeSurface); + std::unique_ptr flipped_surface( + rotozoomSurfaceXY(surface.get(), 0, 1, -1, 0), SDL_FreeSurface); if (flipped_surface.get() != nullptr) { load(flipped_surface.get()); @@ -134,10 +136,11 @@ void Texture::load(fs::path path) void Texture::load(SDL_RWops* rw) { - /* Load RW object as path as a surface object to access pixel data and flip into OpenGL orientation. Attach a destructor so it will free - * itself when it goes out of scope at the end of this function. */ + /* Load RW object as path as a surface object to access pixel data and flip into OpenGL orientation. Attach a destructor so it will + * free itself when it goes out of scope at the end of this function. */ std::unique_ptr surface(IMG_Load_RW(rw, 0), SDL_FreeSurface); - std::unique_ptr flipped_surface(rotozoomSurfaceXY(surface.get(), 0, 1, -1, 0), SDL_FreeSurface); + std::unique_ptr flipped_surface( + rotozoomSurfaceXY(surface.get(), 0, 1, -1, 0), SDL_FreeSurface); load(flipped_surface.get()); } @@ -146,7 +149,8 @@ void Texture::load(SDL_Surface* surface) std::ostringstream message; if (surface->w > 0 && surface->h > 0) { - message << "Loading image from SDL surface (" << surface->w << "Γ—" << surface->h << ", " << SDL_GetPixelFormatName(surface->format->format) << ")"; + message << "Loading image from SDL surface (" << surface->w << "Γ—" << surface->h << ", " << + SDL_GetPixelFormatName(surface->format->format) << ")"; sb::Log::log(message, sb::Log::VERBOSE); load(surface->pixels, {surface->w, surface->h}, GL_RGBA, GL_UNSIGNED_BYTE); }