- restore video recording

- remove sdl context
- configurable audio device settings
- deprecate node
- move audio recording input handling to game object
This commit is contained in:
ohsqueezy 2024-03-04 19:44:06 -05:00
parent 71ff65d588
commit 0e4f06d779
14 changed files with 466 additions and 471 deletions

View File

@ -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()

View File

@ -1,11 +1,11 @@
/* +------------------------------------------------------+
____/ \____ /| - Open source game framework licensed to freely use, |
\ / / | copy, modify and sell without restriction |
+--\ ^__^ /--+ | |
| ~/ \~ | | - created for <https://foam.shampoo.ooo> |
| ~~~~~~~~~~~~ | +------------------------------------------------------+
| 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

View File

@ -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"}

View File

@ -1,11 +1,11 @@
/* +------------------------------------------------------+
____/ \____ /| - Open source game framework licensed to freely use, |
\ / / | copy, modify and sell without restriction |
+--\ ^__^ /--+ | |
| ~/ \~ | | - created for <https://foam.shampoo.ooo> |
| ~~~~~~~~~~~~ | +------------------------------------------------------+
| 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"

View File

@ -1,11 +1,11 @@
/* +------------------------------------------------------+
____/ \____ /| - Open source game framework licensed to freely use, |
\ / / | copy, modify and sell without restriction |
+--\ ^__^ /--+ | |
| ~/ \~ | | - created for <https://foam.shampoo.ooo> |
| ~~~~~~~~~~~~ | +------------------------------------------------------+
| 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

View File

@ -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<SDL_Renderer*>(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;
}

View File

@ -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

View File

@ -1,11 +1,11 @@
/* +------------------------------------------------------+
____/ \____ /| - Open source game framework licensed to freely use, |
\ / / | copy, modify and sell without restriction |
+--\ ^__^ /--+ | |
| ~/ \~ | | - created for <https://foam.shampoo.ooo> |
| ~~~~~~~~~~~~ | +------------------------------------------------------+
| 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<std::string> 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<std::string> 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<std::string> 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<std::string>().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<std::string> configuration_merge)
#endif
sb::Log::log(log_message.str());
glm::ivec2 window_size = configuration()["display"]["dimensions"].get<glm::ivec2>();
glm::ivec2 window_size = _configuration("display", "dimensions").get<glm::ivec2>();
/* 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<const std::string&>().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<const std::string&>().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<bool>();
if (SDL_GL_SetSwapInterval(static_cast<int>(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<std::string> 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<std::string> 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<bool>();
if (SDL_GL_SetSwapInterval(static_cast<int>(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<Game*>(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<const std::string&>().c_str(), "%s", message);
__android_log_print(ANDROID_LOG_VERBOSE, game->configuration()["log"]["short-name"].get_ref<const std::string&>().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);

View File

@ -1,11 +1,11 @@
/* +------------------------------------------------------+
____/ \____ /| - Open source game framework licensed to freely use, |
\ / / | copy, modify and sell without restriction |
+--\ ^__^ /--+ | |
| ~/ \~ | | - created for <https://foam.shampoo.ooo> |
| ~~~~~~~~~~~~ | +------------------------------------------------------+
| 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<float> frame_length_history;
sb::Recorder recorder {_configuration, display};
Game(std::initializer_list<std::string> 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<TTF_Font> 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<TTF_Font> 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;

View File

@ -23,16 +23,6 @@ void Node::set_parent(Node* other)
parent = other;
}
void Node::set_canvas(SDL_Texture* texture)
{
canvas = std::shared_ptr<SDL_Texture>(texture, SDL_DestroyTexture);
}
SDL_Texture* Node::get_canvas()
{
return canvas.get();
}
bool Node::is_active() const
{
return active;

View File

@ -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<SDL_Texture> canvas;
bool active = true;
};

View File

@ -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<std::string>();
SDL_Surface* surface = display.screen_surface();
fs::path directory = configuration("recording", "screenshot directory").get<std::string>();
fs::create_directories(directory);
std::string prefix = config["recording"]["screenshot-prefix"].get<std::string>();
std::string extension = config["recording"]["screenshot-extension"].get<std::string>();
int zfill = config["recording"]["screenshot-zfill"];
std::string prefix = configuration("recording", "screenshot prefix").get<std::string>();
std::string extension = configuration("recording", "screenshot extension").get<std::string>();
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<void(Stash*)> 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<unsigned char*> pixel_buffers = video_stashes.back().pixel_buffers;
// std::vector<bool> 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<std::string>();
fs::path root = configuration("recording", "video directory").get<std::string>();
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>();
std::string pixel_format = configuration("recording", "mp4 pixel format").get<std::string>();
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<Recorder*>(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);

View File

@ -1,11 +1,11 @@
/* +------------------------------------------------------+
____/ \____ /| - Open source game framework licensed to freely use, |
\ / / | copy, modify and sell without restriction |
+--\ ^__^ /--+ | |
| ~/ \~ | | - created for <https://foam.shampoo.ooo> |
| ~~~~~~~~~~~~ | +------------------------------------------------------+
| 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<unsigned char*> pixel_buffers;
std::vector<bool> flipped;
std::vector<Uint8*> audio_buffers;
std::vector<int> audio_buffer_lengths;
int frame_offset;
struct Stash
{
std::vector<unsigned char*> pixel_buffers;
std::vector<bool> flipped;
std::vector<Uint8*> audio_buffers;
std::vector<int> 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<Stash> in_game_stashes;
std::list<Stash> 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<Stash> in_game_stashes;
std::list<Stash> 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);
};
};

View File

@ -1,14 +1,15 @@
/* +------------------------------------------------------+
____/ \____ /| - Open source game framework licensed to freely use, |
\ / / | copy, modify and sell without restriction |
+--\ ^__^ /--+ | |
| ~/ \~ | | - created for <https://foam.shampoo.ooo> |
| ~~~~~~~~~~~~ | +------------------------------------------------------+
| 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<GLint> 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<SDL_Surface, decltype(&SDL_FreeSurface)> surface(IMG_Load(path.string().c_str()), SDL_FreeSurface);
if (surface.get() != nullptr)
{
std::unique_ptr<SDL_Surface, decltype(&SDL_FreeSurface)> flipped_surface(rotozoomSurfaceXY(surface.get(), 0, 1, -1, 0), SDL_FreeSurface);
std::unique_ptr<SDL_Surface, decltype(&SDL_FreeSurface)> 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<SDL_Surface, decltype(&SDL_FreeSurface)> surface(IMG_Load_RW(rw, 0), SDL_FreeSurface);
std::unique_ptr<SDL_Surface, decltype(&SDL_FreeSurface)> flipped_surface(rotozoomSurfaceXY(surface.get(), 0, 1, -1, 0), SDL_FreeSurface);
std::unique_ptr<SDL_Surface, decltype(&SDL_FreeSurface)> 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);
}