- 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:
parent
71ff65d588
commit
0e4f06d779
|
@ -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"
|
#include "Animation.hpp"
|
||||||
|
|
||||||
Animation::Animation()
|
Animation::Animation()
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
/* +------------------------------------------------------+
|
/* ✨ +------------------------------------------------------+
|
||||||
____/ \____ /| - Open source game framework licensed to freely use, |
|
____/ \____ ✨/| Open source game framework licensed to freely use, |
|
||||||
\ / / | copy, modify and sell without restriction |
|
✨\ / / | copy, and modify. Created for 🌠dank.game🌠 |
|
||||||
+--\ ^__^ /--+ | |
|
+--\ . . /--+ | |
|
||||||
| ~/ \~ | | - created for <https://foam.shampoo.ooo> |
|
| ~/ ︶ \👍| | 🌐 https://open.shampoo.ooo/shampoo/spacebox |
|
||||||
| ~~~~~~~~~~~~ | +------------------------------------------------------+
|
| ~~~🌊~~~~🌊~ | +------------------------------------------------------+
|
||||||
| SPACE ~~~~~ | /
|
| SPACE 🪐🅱 OX | /
|
||||||
| ~~~~~~~ BOX |/
|
| 🌊 ~ ~~~~ ~~ |/
|
||||||
+-------------*/
|
+-------------*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
@ -57,8 +57,8 @@ public:
|
||||||
void frame_length(float length);
|
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
|
* Turn the play state to on, causing the animation's callback to run once every frame length. If a delay is given, wait before
|
||||||
* the play_once flag is set to true, only play the callback once after the delay.
|
* 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 delay Amount of seconds to delay before running
|
||||||
* @param play_once If true, only run the callback once instead of once every frame length
|
* @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.
|
* 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
|
* This will run the callback automatically if it is stored in this object, but the ability to store the callback in this object is
|
||||||
* will be removed soon.
|
* deprecated and will be removed soon.
|
||||||
*
|
*
|
||||||
* @param timestamp Seconds since the program has started, which can be obtained from Game::update(float)
|
* @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
|
* @return True if the next frame of animation should be triggered, false otherwise
|
||||||
|
|
|
@ -19,7 +19,7 @@ void Configuration::set_defaults()
|
||||||
{
|
{
|
||||||
config["keys"] = {
|
config["keys"] = {
|
||||||
{"record", {"CTRL", "SHIFT", "i"}},
|
{"record", {"CTRL", "SHIFT", "i"}},
|
||||||
{"save-current-stash", {"CTRL", "SHIFT", "v"}},
|
{"save current stash", {"CTRL", "SHIFT", "v"}},
|
||||||
{"screenshot", {"CTRL", "i"}},
|
{"screenshot", {"CTRL", "i"}},
|
||||||
{"action", "space"},
|
{"action", "space"},
|
||||||
{"up", "up"},
|
{"up", "up"},
|
||||||
|
@ -43,7 +43,7 @@ void Configuration::set_defaults()
|
||||||
{"sdl delay", 6},
|
{"sdl delay", 6},
|
||||||
{"title", "[SPACEBOX]"},
|
{"title", "[SPACEBOX]"},
|
||||||
{"debug", false},
|
{"debug", false},
|
||||||
{"show-cursor", false},
|
{"show cursor", false},
|
||||||
{"render-test-spacing", 2},
|
{"render-test-spacing", 2},
|
||||||
{"render driver", "opengl"},
|
{"render driver", "opengl"},
|
||||||
{"fluid resize", false},
|
{"fluid resize", false},
|
||||||
|
@ -55,33 +55,35 @@ void Configuration::set_defaults()
|
||||||
};
|
};
|
||||||
config["audio"] = {
|
config["audio"] = {
|
||||||
{"default-sfx-root", "resource/sfx"},
|
{"default-sfx-root", "resource/sfx"},
|
||||||
{"default-bgm-root", "resource/bgm"}
|
{"default-bgm-root", "resource/bgm"},
|
||||||
|
{"frequency", 48000},
|
||||||
|
{"chunk size", 2048}
|
||||||
};
|
};
|
||||||
config["gl"] = {
|
config["gl"] = {
|
||||||
{"depth-size", 16},
|
{"depth size", 16},
|
||||||
{"red-size", 8},
|
{"red size", 8},
|
||||||
{"green-size", 8},
|
{"green size", 8},
|
||||||
{"blue-size", 8},
|
{"blue size", 8},
|
||||||
{"share-with-current-context", true},
|
{"share with current context", true},
|
||||||
{"double-buffer", true},
|
{"double buffer", true},
|
||||||
{"major-version", 3},
|
{"major version", 3},
|
||||||
{"minor-version", 2}
|
{"minor version", 2}
|
||||||
},
|
},
|
||||||
config["recording"] = {
|
config["recording"] = {
|
||||||
{"enabled", false},
|
{"enabled", false},
|
||||||
{"video frame length", 60.0f},
|
{"video frame length", 1.0f / 60.0f},
|
||||||
{"screenshot-prefix", "screenshot-"},
|
{"screenshot prefix", "screenshot-"},
|
||||||
{"screenshot-extension", ".png"},
|
{"screenshot extension", ".png"},
|
||||||
{"screenshot-zfill", 5},
|
{"screenshot zfill", 5},
|
||||||
{"screenshot-directory", "."},
|
{"screenshot directory", "."},
|
||||||
{"gif-frame-length", 0.1f},
|
{"gif frame length", 0.1f},
|
||||||
{"video-directory", "."},
|
{"video directory", "."},
|
||||||
{"write-mp4", false},
|
{"write mp4", false},
|
||||||
{"max-stash-length", 5.0f},
|
{"max stash length", 5.0f},
|
||||||
{"max-in-game-stashes", 3},
|
{"max in game stashes", 3},
|
||||||
{"max-video-stashes", 40},
|
{"max video stashes", 40},
|
||||||
{"max-video-memory", 1000},
|
{"max video memory", 1000},
|
||||||
{"mp4-pixel-format", "yuv444p"}
|
{"mp4 pixel format", "yuv444p"}
|
||||||
};
|
};
|
||||||
config["animation"] = {
|
config["animation"] = {
|
||||||
{"all frames frameset name", "all"}
|
{"all frames frameset name", "all"}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
/* +------------------------------------------------------+
|
/* ✨ +------------------------------------------------------+
|
||||||
____/ \____ /| - Open source game framework licensed to freely use, |
|
____/ \____ ✨/| Open source game framework licensed to freely use, |
|
||||||
\ / / | copy, modify and sell without restriction |
|
✨\ / / | copy, and modify. Created for 🌠dank.game🌠 |
|
||||||
+--\ ^__^ /--+ | |
|
+--\ . . /--+ | |
|
||||||
| ~/ \~ | | - created for <https://foam.shampoo.ooo> |
|
| ~/ ︶ \👍| | 🌐 https://open.shampoo.ooo/shampoo/spacebox |
|
||||||
| ~~~~~~~~~~~~ | +------------------------------------------------------+
|
| ~~~🌊~~~~🌊~ | +------------------------------------------------------+
|
||||||
| SPACE ~~~~~ | /
|
| SPACE 🪐🅱 OX | /
|
||||||
| ~~~~~~~ BOX |/
|
| 🌊 ~ ~~~~ ~~ |/
|
||||||
+-------------*/
|
+-------------*/
|
||||||
|
|
||||||
#include "Delegate.hpp"
|
#include "Delegate.hpp"
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
/* +------------------------------------------------------+
|
/* ✨ +------------------------------------------------------+
|
||||||
____/ \____ /| - Open source game framework licensed to freely use, |
|
____/ \____ ✨/| Open source game framework licensed to freely use, |
|
||||||
\ / / | copy, modify and sell without restriction |
|
✨\ / / | copy, and modify. Created for 🌠dank.game🌠 |
|
||||||
+--\ ^__^ /--+ | |
|
+--\ . . /--+ | |
|
||||||
| ~/ \~ | | - created for <https://foam.shampoo.ooo> |
|
| ~/ ︶ \👍| | 🌐 https://open.shampoo.ooo/shampoo/spacebox |
|
||||||
| ~~~~~~~~~~~~ | +------------------------------------------------------+
|
| ~~~🌊~~~~🌊~ | +------------------------------------------------------+
|
||||||
| SPACE ~~~~~ | /
|
| SPACE 🪐🅱 OX | /
|
||||||
| ~~~~~~~ BOX |/
|
| 🌊 ~ ~~~~ ~~ |/
|
||||||
+-------------*/
|
+-------------*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
|
@ -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
|
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;
|
format = GL_BGRA;
|
||||||
|
|
||||||
/* 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);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
SDL_Renderer* renderer = const_cast<SDL_Renderer*>(get_renderer());
|
#endif
|
||||||
SDL_SetRenderTarget(renderer, nullptr);
|
format = GL_RGBA;
|
||||||
SDL_RenderPresent(renderer);
|
#if !defined(__EMSCRIPTEN__) && !defined(__ANDROID__) && !defined(ANDROID)
|
||||||
SDL_RenderReadPixels(renderer, nullptr, SDL_PIXELFORMAT_RGBA32, pixels, bpp / 8 * w);
|
|
||||||
}
|
}
|
||||||
|
#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
|
SDL_Surface* sb::Display::screen_surface() const
|
||||||
|
@ -119,7 +107,7 @@ SDL_Surface* sb::Display::screen_surface() const
|
||||||
glm::ivec2 size = window_size();
|
glm::ivec2 size = window_size();
|
||||||
unsigned char* pixels = new unsigned char[bpp / 8 * size.x * size.y];
|
unsigned char* pixels = new unsigned char[bpp / 8 * size.x * size.y];
|
||||||
screen_pixels(pixels, 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;
|
delete[] pixels;
|
||||||
return surface;
|
return surface;
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,9 +44,21 @@ namespace sb
|
||||||
Box window_box(bool = false) const;
|
Box window_box(bool = false) const;
|
||||||
Box ndc_subsection(const Box&) const;
|
Box ndc_subsection(const Box&) const;
|
||||||
Box ndc_to_pixel(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() 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
|
* Respond to full screen requests and window resize events. If fluid resize is enabled in the configuration, respond to
|
||||||
|
|
286
src/Game.cpp
286
src/Game.cpp
|
@ -1,11 +1,11 @@
|
||||||
/* +------------------------------------------------------+
|
/* ✨ +------------------------------------------------------+
|
||||||
____/ \____ /| - Open source game framework licensed to freely use, |
|
____/ \____ ✨/| Open source game framework licensed to freely use, |
|
||||||
\ / / | copy, modify and sell without restriction |
|
✨\ / / | copy, and modify. Created for 🌠dank.game🌠 |
|
||||||
+--\ ^__^ /--+ | |
|
+--\ . . /--+ | |
|
||||||
| ~/ \~ | | - created for <https://foam.shampoo.ooo> |
|
| ~/ ︶ \👍| | 🌐 https://open.shampoo.ooo/shampoo/spacebox |
|
||||||
| ~~~~~~~~~~~~ | +------------------------------------------------------+
|
| ~~~🌊~~~~🌊~ | +------------------------------------------------------+
|
||||||
| SPACE ~~~~~ | /
|
| SPACE 🪐🅱 OX | /
|
||||||
| ~~~~~~~ BOX |/
|
| 🌊 ~ ~~~~ ~~ |/
|
||||||
+-------------*/
|
+-------------*/
|
||||||
|
|
||||||
#include "Game.hpp"
|
#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 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();
|
||||||
*
|
|
||||||
* recorder.animation.play()
|
|
||||||
*/
|
|
||||||
activate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Log the current working directory as seen by std::filesystem */
|
/* 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 = std::ostringstream();
|
||||||
log_message << "Using Android SDK version " << SDL_GetAndroidSDKVersion() << std::endl;
|
log_message << "Using Android SDK version " << SDL_GetAndroidSDKVersion() << std::endl;
|
||||||
log_message << "SDL_AndroidGetInternalStoragePath() is " << SDL_AndroidGetInternalStoragePath() << 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();
|
log_message << "SDL_AndroidGetExternalStoragePath() is " << SDL_AndroidGetExternalStoragePath();
|
||||||
sb::Log::log(log_message);
|
sb::Log::log(log_message);
|
||||||
#endif
|
#endif
|
||||||
|
@ -78,14 +75,15 @@ Game::Game(std::initializer_list<std::string> configuration_merge)
|
||||||
log_message << std::setw(4) << configuration()() << std::endl;
|
log_message << std::setw(4) << configuration()() << std::endl;
|
||||||
sb::Log::log(log_message, sb::Log::DEBUG);
|
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());
|
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 */
|
/* Initialize the buffer of frame lengths which will be used to calculate and display FPS */
|
||||||
frame_length_history.reserve(5000);
|
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::handle_quit_event, this, SDL_QUIT);
|
||||||
|
_delegate.subscribe(&Game::respond, this);
|
||||||
|
|
||||||
/* Needed for displaying fullscreen correctly on Linux (?) Also might need SDL_VIDEO_CENTERED (?) */
|
/* Needed for displaying fullscreen correctly on Linux (?) Also might need SDL_VIDEO_CENTERED (?) */
|
||||||
std::string fullscreen_env_assigment = "SDL_VIDEO_X11_LEGACY_FULLSCREEN=0";
|
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
|
#endif
|
||||||
|
|
||||||
sb::Log::log(log_message.str());
|
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)
|
#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_MAJOR_VERSION, _configuration("gl", "major version"));
|
||||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, configuration()["gl"]["minor-version"]);
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, _configuration("gl", "minor version"));
|
||||||
#endif
|
#endif
|
||||||
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, configuration()["gl"]["depth-size"]);
|
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_RED_SIZE, _configuration("gl", "red size"));
|
||||||
SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, configuration()["gl"]["green-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_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_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_DOUBLEBUFFER, _configuration("gl", "double buffer"));
|
||||||
|
|
||||||
#if !defined(__MINGW32__)
|
/* Use compatibility mode on Windows because it seemed to be required on one version of Windows tested */
|
||||||
/* Set the profile to ES so that desktop and web builds are both using the same profile. This should be handled by a configuration
|
#if defined(__MINGW32__)
|
||||||
* 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 */
|
|
||||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_COMPATIBILITY);
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_COMPATIBILITY);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/* Create a window with dimensions set in the config, centered, and flagged to be usable in OpenGL context */
|
/* 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;
|
Uint32 flags = SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE;
|
||||||
if (configuration()("display", "fullscreen")) flags |= SDL_WINDOW_FULLSCREEN;
|
if (_configuration("display", "fullscreen"))
|
||||||
_window = SDL_CreateWindow(
|
{
|
||||||
configuration()["display"]["title"].get_ref<const std::string&>().c_str(), SDL_WINDOWPOS_CENTERED,
|
flags |= SDL_WINDOW_FULLSCREEN;
|
||||||
SDL_WINDOWPOS_CENTERED, window_size.x, window_size.y, flags);
|
}
|
||||||
|
_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)
|
if (_window == nullptr)
|
||||||
{
|
{
|
||||||
sb::Log::sdl_error("Could not create window");
|
sb::Log::sdl_error("Could not create window");
|
||||||
flag_to_end();
|
flag_to_end();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Create an SDL renderer for clearing the screen to black and for logging renderer properties. Destroy renderer when finished.
|
/* Create a single GL context for the game. This is restrictive since it forces the game to use only one context, so it should
|
||||||
* Skip this in emscripten because it causes a mouse event bug. */
|
* be improved in the future. */
|
||||||
#ifndef __EMSCRIPTEN__
|
if ((glcontext = SDL_GL_CreateContext(window())) == nullptr)
|
||||||
if ((renderer = SDL_CreateRenderer(_window, -1, SDL_RENDERER_TARGETTEXTURE | SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC)) == nullptr)
|
|
||||||
{
|
{
|
||||||
sb::Log::sdl_error("Could not create renderer");
|
sb::Log::sdl_error("Could not get GL context");
|
||||||
flag_to_end();
|
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
|
else
|
||||||
{
|
{
|
||||||
int w, h;
|
sb::Log::log("Setting vysnc is not supported");
|
||||||
/* log the renderer resolution */
|
}
|
||||||
SDL_GetRendererOutputSize(renderer, &w, &h);
|
|
||||||
log_message = std::ostringstream();
|
/* Initialize GLEW for GL function discovery on all platforms except Android */
|
||||||
log_message << "renderer output size is " << w << "x" << h;
|
#if !defined(__ANDROID__) && !defined(ANDROID)
|
||||||
sb::Log::log(log_message);
|
GLenum error = glewInit();
|
||||||
/* clear screen to black */
|
if (error != GLEW_OK)
|
||||||
SDL_SetRenderTarget(renderer, nullptr);
|
{
|
||||||
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
|
std::ostringstream message;
|
||||||
SDL_RenderClear(renderer);
|
message << "GLEW could not initialize " << glewGetErrorString(error);
|
||||||
SDL_RenderPresent(renderer);
|
sb::Log::log(message, sb::Log::ERR);
|
||||||
SDL_RenderFlush(renderer);
|
|
||||||
SDL_DestroyRenderer(renderer);
|
|
||||||
}
|
}
|
||||||
#endif
|
#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)
|
if (TTF_Init() < 0)
|
||||||
{
|
{
|
||||||
sb::Log::sdl_error("Could not initialize SDL ttf");
|
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 */
|
/* 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");
|
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();
|
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)
|
void Game::sdl_log_override(void* userdata, int category, SDL_LogPriority priority, const char* message)
|
||||||
{
|
{
|
||||||
Game* game = static_cast<Game*>(userdata);
|
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 printing to stdout, print to Android log as well */
|
||||||
#if defined(__ANDROID__) || defined(ANDROID)
|
#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
|
#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
|
void Game::log_display_mode() const
|
||||||
{
|
{
|
||||||
SDL_DisplayMode current;
|
SDL_DisplayMode current;
|
||||||
|
@ -463,28 +399,28 @@ void Game::log_gl_properties() const
|
||||||
int attribute;
|
int attribute;
|
||||||
int status = SDL_GL_GetAttribute(SDL_GL_RED_SIZE, &attribute);
|
int status = SDL_GL_GetAttribute(SDL_GL_RED_SIZE, &attribute);
|
||||||
if (!status) {
|
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 {
|
} else {
|
||||||
sb::Log::sdl_error("Failed to get SDL_GL_RED_SIZE");
|
sb::Log::sdl_error("Failed to get SDL_GL_RED_SIZE");
|
||||||
}
|
}
|
||||||
|
|
||||||
status = SDL_GL_GetAttribute(SDL_GL_GREEN_SIZE, &attribute);
|
status = SDL_GL_GetAttribute(SDL_GL_GREEN_SIZE, &attribute);
|
||||||
if (!status) {
|
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 {
|
} else {
|
||||||
sb::Log::sdl_error("Failed to get SDL_GL_GREEN_SIZE");
|
sb::Log::sdl_error("Failed to get SDL_GL_GREEN_SIZE");
|
||||||
}
|
}
|
||||||
|
|
||||||
status = SDL_GL_GetAttribute(SDL_GL_BLUE_SIZE, &attribute);
|
status = SDL_GL_GetAttribute(SDL_GL_BLUE_SIZE, &attribute);
|
||||||
if (!status) {
|
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 {
|
} else {
|
||||||
sb::Log::sdl_error("Failed to get SDL_GL_BLUE_SIZE");
|
sb::Log::sdl_error("Failed to get SDL_GL_BLUE_SIZE");
|
||||||
}
|
}
|
||||||
|
|
||||||
status = SDL_GL_GetAttribute(SDL_GL_DEPTH_SIZE, &attribute);
|
status = SDL_GL_GetAttribute(SDL_GL_DEPTH_SIZE, &attribute);
|
||||||
if (!status) {
|
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 {
|
} else {
|
||||||
sb::Log::sdl_error("Failed to get SDL_GL_DEPTH_SIZE");
|
sb::Log::sdl_error("Failed to get SDL_GL_DEPTH_SIZE");
|
||||||
}
|
}
|
||||||
|
@ -535,16 +471,6 @@ SDL_Window* Game::window()
|
||||||
return _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
|
const Input& Game::get_input() const
|
||||||
{
|
{
|
||||||
return input;
|
return input;
|
||||||
|
@ -594,7 +520,8 @@ void Game::run()
|
||||||
SDL_Log("using emscripten main loop");
|
SDL_Log("using emscripten main loop");
|
||||||
emscripten_set_main_loop_arg(&loop, this, -1, true);
|
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
|
#else
|
||||||
SDL_Log("using standard main loop");
|
SDL_Log("using standard main loop");
|
||||||
while (!done)
|
while (!done)
|
||||||
|
@ -605,6 +532,26 @@ void Game::run()
|
||||||
#endif
|
#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)
|
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. */
|
/* 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 */
|
/* Save the timestamp */
|
||||||
last_frame_timestamp = timestamp;
|
last_frame_timestamp = timestamp;
|
||||||
|
|
||||||
// /* Only build a frame if the last frame was under a second. */
|
/* Update game state and render frame (update and draw should be separated into different functions in a future revision to
|
||||||
// if (last_frame_length < 1000)
|
* allow for different timing intervals for each) */
|
||||||
// {
|
float timestamp_seconds = timestamp / 1000.0f;
|
||||||
float timestamp_seconds = timestamp / 1000.0f;
|
recorder.update(timestamp_seconds);
|
||||||
// recorder.update(timestamp_seconds);
|
_delegate.dispatch();
|
||||||
_delegate.dispatch();
|
input.unsuppress_animation.update(timestamp_seconds);
|
||||||
input.unsuppress_animation.update(timestamp_seconds);
|
update(timestamp_seconds);
|
||||||
update(timestamp_seconds);
|
_configuration.update(timestamp_seconds);
|
||||||
_configuration.update(timestamp_seconds);
|
|
||||||
if (!is_gl_context)
|
|
||||||
{
|
|
||||||
SDL_SetRenderTarget(renderer, nullptr);
|
|
||||||
SDL_RenderPresent(renderer);
|
|
||||||
}
|
|
||||||
// }
|
|
||||||
|
|
||||||
/* Update frame count per second for verbose log */
|
/* Update frame count per second for verbose log */
|
||||||
frame_count_this_second++;
|
frame_count_this_second++;
|
||||||
|
@ -694,10 +634,6 @@ void Game::quit()
|
||||||
{
|
{
|
||||||
SDL_GL_DeleteContext(glcontext);
|
SDL_GL_DeleteContext(glcontext);
|
||||||
}
|
}
|
||||||
if (renderer != nullptr)
|
|
||||||
{
|
|
||||||
SDL_DestroyRenderer(renderer);
|
|
||||||
}
|
|
||||||
if (_window != nullptr)
|
if (_window != nullptr)
|
||||||
{
|
{
|
||||||
SDL_DestroyWindow(_window);
|
SDL_DestroyWindow(_window);
|
||||||
|
|
88
src/Game.hpp
88
src/Game.hpp
|
@ -1,11 +1,11 @@
|
||||||
/* +------------------------------------------------------+
|
/* ✨ +------------------------------------------------------+
|
||||||
____/ \____ /| - Open source game framework licensed to freely use, |
|
____/ \____ ✨/| Open source game framework licensed to freely use, |
|
||||||
\ / / | copy, modify and sell without restriction |
|
✨\ / / | copy, and modify. Created for 🌠dank.game🌠 |
|
||||||
+--\ ^__^ /--+ | |
|
+--\ . . /--+ | |
|
||||||
| ~/ \~ | | - created for <https://foam.shampoo.ooo> |
|
| ~/ ︶ \👍| | 🌐 https://open.shampoo.ooo/shampoo/spacebox |
|
||||||
| ~~~~~~~~~~~~ | +------------------------------------------------------+
|
| ~~~🌊~~~~🌊~ | +------------------------------------------------------+
|
||||||
| SPACE ~~~~~ | /
|
| SPACE 🪐🅱 OX | /
|
||||||
| ~~~~~~~ BOX |/
|
| 🌊 ~ ~~~~ ~~ |/
|
||||||
+-------------*/
|
+-------------*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
@ -99,24 +99,19 @@ public:
|
||||||
Game(Game&&) = delete;
|
Game(Game&&) = delete;
|
||||||
Game& operator=(Game&&) = delete;
|
Game& operator=(Game&&) = delete;
|
||||||
|
|
||||||
SDL_Renderer* renderer = nullptr;
|
|
||||||
SDL_GLContext glcontext = nullptr;
|
SDL_GLContext glcontext = nullptr;
|
||||||
int frame_count_this_second = 0, last_frame_length, current_frames_per_second = 0;
|
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;
|
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};
|
sb::Display display {this};
|
||||||
// Recorder recorder {this};
|
|
||||||
Input input {this};
|
Input input {this};
|
||||||
std::vector<float> frame_length_history;
|
std::vector<float> frame_length_history;
|
||||||
|
sb::Recorder recorder {_configuration, display};
|
||||||
|
|
||||||
Game(std::initializer_list<std::string> configuration_merge);
|
Game(std::initializer_list<std::string> configuration_merge);
|
||||||
virtual void reset() { activate(); }
|
|
||||||
void print_frame_length_history();
|
void print_frame_length_history();
|
||||||
void load_sdl_context();
|
|
||||||
void load_gl_context();
|
|
||||||
GLuint load_shader(const fs::path&, GLenum) const;
|
GLuint load_shader(const fs::path&, GLenum) const;
|
||||||
bool link_shader(GLuint program) 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
|
* 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_gl_properties() const;
|
||||||
|
|
||||||
void log_surface_format(SDL_Surface*, std::string = "surface");
|
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;
|
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();
|
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;
|
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();
|
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;
|
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();
|
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;
|
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();
|
Input& get_input();
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
|
@ -147,14 +180,31 @@ public:
|
||||||
std::shared_ptr<TTF_Font> font() const;
|
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,
|
* 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
|
||||||
* the shared pointer will point to `nullptr`.
|
* 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
|
* @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;
|
std::shared_ptr<TTF_Font> font(const fs::path& path, int size) const;
|
||||||
|
|
||||||
void run();
|
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 frame(float);
|
||||||
void flag_to_end();
|
void flag_to_end();
|
||||||
virtual void update(float timestamp) = 0;
|
virtual void update(float timestamp) = 0;
|
||||||
|
|
10
src/Node.cpp
10
src/Node.cpp
|
@ -23,16 +23,6 @@ void Node::set_parent(Node* other)
|
||||||
parent = 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
|
bool Node::is_active() const
|
||||||
{
|
{
|
||||||
return active;
|
return active;
|
||||||
|
|
|
@ -35,6 +35,10 @@ namespace sb
|
||||||
class Display;
|
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
|
class Node
|
||||||
{
|
{
|
||||||
|
|
||||||
|
@ -46,8 +50,6 @@ public:
|
||||||
virtual void reset() { deactivate(); }
|
virtual void reset() { deactivate(); }
|
||||||
virtual void activate() { active = true; }
|
virtual void activate() { active = true; }
|
||||||
virtual void deactivate() { active = false; }
|
virtual void deactivate() { active = false; }
|
||||||
void set_canvas(SDL_Texture*);
|
|
||||||
SDL_Texture* get_canvas();
|
|
||||||
bool is_active() const;
|
bool is_active() const;
|
||||||
const Configuration& configuration() const;
|
const Configuration& configuration() const;
|
||||||
Configuration& configuration();
|
Configuration& configuration();
|
||||||
|
@ -100,7 +102,6 @@ public:
|
||||||
private:
|
private:
|
||||||
|
|
||||||
Node* parent;
|
Node* parent;
|
||||||
std::shared_ptr<SDL_Texture> canvas;
|
|
||||||
bool active = true;
|
bool active = true;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
202
src/Recorder.cpp
202
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 "gif-h/gif.h"
|
||||||
#include "Game.hpp"
|
#include "Game.hpp"
|
||||||
#include "extension.hpp"
|
#include "extension.hpp"
|
||||||
#include "Recorder.hpp"
|
#include "Recorder.hpp"
|
||||||
|
|
||||||
|
using namespace sb;
|
||||||
|
|
||||||
/* Create a Recorder instance. Subscribe to command input and set audio callback. */
|
/* 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 */
|
/* Returns length of a recorded video frame in seconds */
|
||||||
float Recorder::frame_length()
|
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()
|
void Recorder::capture_screen()
|
||||||
{
|
{
|
||||||
const nlohmann::json& config = configuration()();
|
SDL_Surface* surface = display.screen_surface();
|
||||||
SDL_Surface* surface = get_display().screen_surface();
|
fs::path directory = configuration("recording", "screenshot directory").get<std::string>();
|
||||||
fs::path directory = config["recording"]["screenshot-directory"].get<std::string>();
|
|
||||||
fs::create_directories(directory);
|
fs::create_directories(directory);
|
||||||
std::string prefix = config["recording"]["screenshot-prefix"].get<std::string>();
|
std::string prefix = configuration("recording", "screenshot prefix").get<std::string>();
|
||||||
std::string extension = config["recording"]["screenshot-extension"].get<std::string>();
|
std::string extension = configuration("recording", "screenshot extension").get<std::string>();
|
||||||
int zfill = config["recording"]["screenshot-zfill"];
|
int zfill = configuration("recording", "screenshot zfill");
|
||||||
fs::path path = sb::get_next_file_name(directory, zfill, prefix, extension);
|
fs::path path = sb::get_next_file_name(directory, zfill, prefix, extension);
|
||||||
IMG_SavePNG(surface, path.string().c_str());
|
IMG_SavePNG(surface, path.string().c_str());
|
||||||
SDL_FreeSurface(surface);
|
SDL_FreeSurface(surface);
|
||||||
|
@ -78,9 +66,9 @@ void Recorder::grab_stash()
|
||||||
{
|
{
|
||||||
if (!is_recording and !writing_recording)
|
if (!is_recording and !writing_recording)
|
||||||
{
|
{
|
||||||
int length = configuration()["recording"]["max-stash-length"];
|
int length = configuration("recording", "max stash length");
|
||||||
std::ostringstream message;
|
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);
|
sb::Log::log(message);
|
||||||
most_recent_stash = current_stash;
|
most_recent_stash = current_stash;
|
||||||
current_stash = 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());
|
most_recent_stash.audio_buffer_lengths.erase(most_recent_stash.audio_buffer_lengths.begin());
|
||||||
}
|
}
|
||||||
audio_file.close();
|
audio_file.close();
|
||||||
if (configuration()["recording"]["write-mp4"])
|
if (configuration("recording", "write mp4"))
|
||||||
{
|
{
|
||||||
write_mp4();
|
write_mp4();
|
||||||
}
|
}
|
||||||
|
@ -143,11 +131,11 @@ void Recorder::open_audio_file()
|
||||||
|
|
||||||
void Recorder::add_frame()
|
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;
|
int bytes = sb::Display::bpp / 8 * size.x * size.y;
|
||||||
unsigned char* pixels = new unsigned char[bytes];
|
unsigned char* pixels = new unsigned char[bytes];
|
||||||
get_display().screen_pixels(pixels, size.x, size.y);
|
display.screen_pixels(pixels, size.x, size.y);
|
||||||
int max_length = configuration()["recording"]["max-stash-length"];
|
int max_length = configuration("recording", "max stash length");
|
||||||
float length = frame_length() * current_stash.pixel_buffers.size();
|
float length = frame_length() * current_stash.pixel_buffers.size();
|
||||||
if (length > max_length)
|
if (length > max_length)
|
||||||
{
|
{
|
||||||
|
@ -156,76 +144,63 @@ void Recorder::add_frame()
|
||||||
current_stash.flipped.erase(current_stash.flipped.begin());
|
current_stash.flipped.erase(current_stash.flipped.begin());
|
||||||
}
|
}
|
||||||
current_stash.pixel_buffers.push_back(pixels);
|
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)
|
if (is_recording)
|
||||||
{
|
{
|
||||||
unsigned char* vid_pixels = new unsigned char[bytes];
|
unsigned char* vid_pixels = new unsigned char[bytes];
|
||||||
memcpy(vid_pixels, pixels, bytes);
|
memcpy(vid_pixels, pixels, bytes);
|
||||||
video_stashes.back().pixel_buffers.push_back(vid_pixels);
|
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)
|
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::function<void(Stash*)> f = std::bind(&Recorder::write_stash_frames, this, std::placeholders::_1);
|
||||||
std::thread writing(f, &video_stashes.back());
|
std::thread writing(f, &video_stashes.back());
|
||||||
writing.detach();
|
writing.detach();
|
||||||
// int frame_offset = video_stashes.back().frame_offset;
|
video_stashes.push_back(Stash(video_stashes.back().frame_offset + video_stashes.back().pixel_buffers.size()));
|
||||||
// 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()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int Recorder::get_memory_size()
|
float Recorder::get_memory_size() const
|
||||||
{
|
{
|
||||||
glm::ivec2 window = get_display().window_size();
|
glm::ivec2 window = display.window_size();
|
||||||
int bytes_per_frame = sb::Display::bpp / 8 * window.x * window.y,
|
int bytes_per_frame = sb::Display::bpp / 8 * window.x * window.y;
|
||||||
size_in_bytes = 0;
|
int size_in_bytes = 0;
|
||||||
for (Stash& stash : in_game_stashes)
|
for (const Stash& stash : in_game_stashes)
|
||||||
{
|
{
|
||||||
size_in_bytes += stash.pixel_buffers.size() * bytes_per_frame;
|
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;
|
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 += stash.pixel_buffers.size() * bytes_per_frame;
|
||||||
}
|
}
|
||||||
size_in_bytes += current_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 += length;
|
||||||
}
|
}
|
||||||
size_in_bytes += most_recent_stash.pixel_buffers.size() * bytes_per_frame;
|
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;
|
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()
|
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::create_directories(root);
|
||||||
fs::path directory = sb::get_next_file_name(root, 5, "video-");
|
fs::path directory = sb::get_next_file_name(root, 5, "video-");
|
||||||
fs::create_directories(directory);
|
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_Log("Writing stash offset %i to %s...", stash->frame_offset, current_video_directory.string().c_str());
|
||||||
SDL_Surface* frame;
|
SDL_Surface* frame;
|
||||||
GifWriter gif_writer;
|
GifWriter gif_writer;
|
||||||
int gif_frame_length = configuration()["recording"]["gif-frame-length"];
|
float gif_frame_length = configuration("recording", "gif frame length");
|
||||||
fs::path gif_path = sb::get_next_file_name(
|
fs::path gif_path = sb::get_next_file_name(current_video_directory, 3, "gif-", ".gif");
|
||||||
current_video_directory, 3, "gif-", ".gif");
|
|
||||||
float elapsed = 0, last_gif_write = 0, gif_write_overflow = 0;
|
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(
|
frame = display.screen_surface_from_pixels(stash->pixel_buffers.front(), stash->flipped.front());
|
||||||
stash->pixel_buffers.front(), stash->flipped.front());
|
|
||||||
std::stringstream name;
|
std::stringstream name;
|
||||||
name << sb::pad(ii, 5) << ".png";
|
name << sb::pad(ii, 5) << ".png";
|
||||||
fs::path path = current_video_directory / name.str();
|
fs::path path = current_video_directory / name.str();
|
||||||
IMG_SavePNG(frame, path.string().c_str());
|
IMG_SavePNG(frame, path.string().c_str());
|
||||||
if (ii == stash->frame_offset or
|
if (ii == stash->frame_offset || elapsed - last_gif_write + gif_write_overflow >= gif_frame_length)
|
||||||
elapsed - last_gif_write + gif_write_overflow >= gif_frame_length)
|
|
||||||
{
|
{
|
||||||
if (ii == stash->frame_offset)
|
if (ii == stash->frame_offset)
|
||||||
{
|
{
|
||||||
GifBegin(&gif_writer, gif_path.string().c_str(), frame->w,
|
GifBegin(&gif_writer, gif_path.string().c_str(), frame->w, frame->h, gif_frame_length * 100);
|
||||||
frame->h, gif_frame_length * 100);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
gif_write_overflow += elapsed - (last_gif_write + gif_frame_length);
|
gif_write_overflow += elapsed - (last_gif_write + gif_frame_length);
|
||||||
last_gif_write = elapsed;
|
last_gif_write = elapsed;
|
||||||
}
|
}
|
||||||
SDL_Surface* converted = SDL_ConvertSurfaceFormat(
|
SDL_Surface* converted = SDL_ConvertSurfaceFormat(frame, SDL_PIXELFORMAT_ABGR8888, 0);
|
||||||
frame, SDL_PIXELFORMAT_ABGR8888, 0);
|
GifWriteFrame(&gif_writer, (const uint8_t*) converted->pixels, frame->w, frame->h, gif_frame_length * 100);
|
||||||
GifWriteFrame(&gif_writer, (const uint8_t*) converted->pixels,
|
|
||||||
frame->w, frame->h, gif_frame_length * 100);
|
|
||||||
}
|
}
|
||||||
elapsed += frame_length();
|
elapsed += frame_length();
|
||||||
delete[] stash->pixel_buffers.front();
|
delete[] stash->pixel_buffers.front();
|
||||||
|
@ -280,7 +249,7 @@ void Recorder::keep_stash()
|
||||||
{
|
{
|
||||||
in_game_stashes.push_back(current_stash);
|
in_game_stashes.push_back(current_stash);
|
||||||
current_stash = 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)
|
if (in_game_stashes.size() > max_stashes)
|
||||||
{
|
{
|
||||||
Stash& stash = in_game_stashes.front();
|
Stash& stash = in_game_stashes.front();
|
||||||
|
@ -322,7 +291,7 @@ void Recorder::finish_writing_video()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
video_stashes.clear();
|
video_stashes.clear();
|
||||||
if (configuration()["recording"]["write-mp4"])
|
if (configuration("recording", "write mp4"))
|
||||||
{
|
{
|
||||||
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 (?) */
|
* This requires ffmpeg to be installed on the user's system. Might only work on Linux (?) */
|
||||||
void Recorder::write_mp4()
|
void Recorder::write_mp4()
|
||||||
{
|
{
|
||||||
glm::ivec2 size = get_display().window_size();
|
glm::ivec2 size = display.window_size();
|
||||||
std::ostringstream mp4_command;
|
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";
|
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()) <<
|
" -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 << " " <<
|
" -c:v libx264 -crf 17 -pix_fmt " << pixel_format << " " <<
|
||||||
current_video_directory.string() << ".mp4";
|
current_video_directory.string() << ".mp4";
|
||||||
std::string mp4_command_str = mp4_command.str();
|
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)
|
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();
|
end_recording();
|
||||||
}
|
}
|
||||||
|
@ -366,16 +343,15 @@ void Recorder::update(float timestamp)
|
||||||
void Recorder::process_audio(void* context, Uint8* stream, int len)
|
void Recorder::process_audio(void* context, Uint8* stream, int len)
|
||||||
{
|
{
|
||||||
Recorder* recorder = static_cast<Recorder*>(context);
|
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();
|
float length = recorder->frame_length() * recorder->current_stash.pixel_buffers.size();
|
||||||
if (length > max_length)
|
if (length > max_length)
|
||||||
{
|
{
|
||||||
delete[] recorder->current_stash.audio_buffers.front();
|
delete[] recorder->current_stash.audio_buffers.front();
|
||||||
recorder->current_stash.audio_buffers.erase(recorder->current_stash.audio_buffers.begin());
|
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.erase(recorder->current_stash.audio_buffer_lengths.begin());
|
||||||
recorder->current_stash.audio_buffer_lengths.begin());
|
|
||||||
}
|
}
|
||||||
Uint8* stream_copy = new Uint8[len];
|
Uint8* stream_copy = new Uint8[len];
|
||||||
std::memcpy(stream_copy, stream, len);
|
std::memcpy(stream_copy, stream, len);
|
||||||
|
|
124
src/Recorder.hpp
124
src/Recorder.hpp
|
@ -1,11 +1,11 @@
|
||||||
/* +------------------------------------------------------+
|
/* ✨ +------------------------------------------------------+
|
||||||
____/ \____ /| - Open source game framework licensed to freely use, |
|
____/ \____ ✨/| Open source game framework licensed to freely use, |
|
||||||
\ / / | copy, modify and sell without restriction |
|
✨\ / / | copy, and modify. Created for 🌠dank.game🌠 |
|
||||||
+--\ ^__^ /--+ | |
|
+--\ . . /--+ | |
|
||||||
| ~/ \~ | | - created for <https://foam.shampoo.ooo> |
|
| ~/ ︶ \👍| | 🌐 https://open.shampoo.ooo/shampoo/spacebox |
|
||||||
| ~~~~~~~~~~~~ | +------------------------------------------------------+
|
| ~~~🌊~~~~🌊~ | +------------------------------------------------------+
|
||||||
| SPACE ~~~~~ | /
|
| SPACE 🪐🅱 OX | /
|
||||||
| ~~~~~~~ BOX |/
|
| 🌊 ~ ~~~~ ~~ |/
|
||||||
+-------------*/
|
+-------------*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
@ -25,59 +25,85 @@
|
||||||
#include "glm/ext.hpp"
|
#include "glm/ext.hpp"
|
||||||
#include "json/json.hpp"
|
#include "json/json.hpp"
|
||||||
#include "filesystem.hpp"
|
#include "filesystem.hpp"
|
||||||
#include "Node.hpp"
|
|
||||||
#include "Configuration.hpp"
|
#include "Configuration.hpp"
|
||||||
#include "Animation.hpp"
|
#include "Animation.hpp"
|
||||||
#include "Log.hpp"
|
#include "Log.hpp"
|
||||||
|
|
||||||
struct Stash
|
namespace sb
|
||||||
{
|
{
|
||||||
std::vector<unsigned char*> pixel_buffers;
|
struct Stash
|
||||||
std::vector<bool> flipped;
|
{
|
||||||
std::vector<Uint8*> audio_buffers;
|
std::vector<unsigned char*> pixel_buffers;
|
||||||
std::vector<int> audio_buffer_lengths;
|
std::vector<bool> flipped;
|
||||||
int frame_offset;
|
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 current_stash = Stash();
|
||||||
Stash most_recent_stash;
|
Stash most_recent_stash;
|
||||||
std::list<Stash> in_game_stashes;
|
std::list<Stash> in_game_stashes;
|
||||||
std::list<Stash> video_stashes;
|
std::list<Stash> video_stashes;
|
||||||
fs::path current_video_directory, current_audio_path;
|
fs::path current_video_directory, current_audio_path;
|
||||||
bool is_recording = false, writing_recording = false, writing_most_recent = false;
|
bool is_recording = false, writing_recording = false, writing_most_recent = false;
|
||||||
std::ofstream audio_file;
|
std::ofstream audio_file;
|
||||||
|
sb::Configuration& configuration;
|
||||||
|
sb::Display& display;
|
||||||
|
|
||||||
float frame_length();
|
float frame_length();
|
||||||
static void process_audio(void*, Uint8*, int);
|
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*);
|
Recorder(sb::Configuration& configuration, sb::Display& display);
|
||||||
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"; }
|
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* 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);
|
||||||
|
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
/* +------------------------------------------------------+
|
/* ✨ +------------------------------------------------------+
|
||||||
____/ \____ /| - Open source game framework licensed to freely use, |
|
____/ \____ ✨/| Open source game framework licensed to freely use, |
|
||||||
\ / / | copy, modify and sell without restriction |
|
✨\ / / | copy, and modify. Created for 🌠dank.game🌠 |
|
||||||
+--\ ^__^ /--+ | |
|
+--\ . . /--+ | |
|
||||||
| ~/ \~ | | - created for <https://foam.shampoo.ooo> |
|
| ~/ ︶ \👍| | 🌐 https://open.shampoo.ooo/shampoo/spacebox |
|
||||||
| ~~~~~~~~~~~~ | +------------------------------------------------------+
|
| ~~~🌊~~~~🌊~ | +------------------------------------------------------+
|
||||||
| SPACE ~~~~~ | /
|
| SPACE 🪐🅱 OX | /
|
||||||
| ~~~~~~~ BOX |/
|
| 🌊 ~ ~~~~ ~~ |/
|
||||||
+-------------*/
|
+-------------*/
|
||||||
|
|
||||||
#include "Texture.hpp"
|
#include "Texture.hpp"
|
||||||
|
|
||||||
using namespace sb;
|
using namespace sb;
|
||||||
|
|
||||||
Texture::Texture() : GLObject(texture_deleter) {}
|
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)
|
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
|
/* Only generate a new texture ID and reallocate memory if the current texture ID hasn't been registered by this object as having
|
||||||
* sized memory with the same format. */
|
* identically sized memory with the same format. */
|
||||||
if (!generated() || !_size.has_value() || !_format.has_value() || _size != size || _format != format)
|
if (!generated() || !_size.has_value() || !_format.has_value() || _size != size || _format != format)
|
||||||
{
|
{
|
||||||
generate();
|
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);
|
std::unique_ptr<SDL_Surface, decltype(&SDL_FreeSurface)> surface(IMG_Load(path.string().c_str()), SDL_FreeSurface);
|
||||||
if (surface.get() != nullptr)
|
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)
|
if (flipped_surface.get() != nullptr)
|
||||||
{
|
{
|
||||||
load(flipped_surface.get());
|
load(flipped_surface.get());
|
||||||
|
@ -134,10 +136,11 @@ void Texture::load(fs::path path)
|
||||||
|
|
||||||
void Texture::load(SDL_RWops* rw)
|
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
|
/* Load RW object as path as a surface object to access pixel data and flip into OpenGL orientation. Attach a destructor so it will
|
||||||
* itself when it goes out of scope at the end of this function. */
|
* 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)> 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());
|
load(flipped_surface.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -146,7 +149,8 @@ void Texture::load(SDL_Surface* surface)
|
||||||
std::ostringstream message;
|
std::ostringstream message;
|
||||||
if (surface->w > 0 && surface->h > 0)
|
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);
|
sb::Log::log(message, sb::Log::VERBOSE);
|
||||||
load(surface->pixels, {surface->w, surface->h}, GL_RGBA, GL_UNSIGNED_BYTE);
|
load(surface->pixels, {surface->w, surface->h}, GL_RGBA, GL_UNSIGNED_BYTE);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue