Grand app refactor: move main to sdl2 lib, make the rest of the engine independent of SDL2

This commit is contained in:
Nikita Lisitsa 2023-07-14 20:55:18 +03:00
parent 42f986ce4a
commit b910d16261
29 changed files with 2435 additions and 684 deletions

View file

@ -8,4 +8,4 @@ endif()
psemek_add_library(psemek-app ${PSEMEK_APP_HEADERS} ${PSEMEK_APP_SOURCES})
target_include_directories(psemek-app PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include")
target_link_libraries(psemek-app PUBLIC psemek-log psemek-util psemek-gfx psemek-ui psemek-sdl2)
target_link_libraries(psemek-app PUBLIC psemek-log psemek-util psemek-gfx psemek-ui)

View file

@ -1,67 +0,0 @@
#pragma once
#include <psemek/app/scene.hpp>
#include <psemek/app/scene_manager.hpp>
#include <psemek/geom/vector.hpp>
#include <psemek/util/pimpl.hpp>
#include <SDL2/SDL_keycode.h>
#include <memory>
namespace psemek::app
{
static const geom::vector<int, 2> common_resolutions[] =
{
{1024, 768},
{1280, 720},
{1280, 1024},
{1366, 768},
{1440, 900},
{1536, 864},
{1600, 900},
{1920, 1080},
};
struct app
: scene_base, scene_manager
{
struct options
{
int multisampling = 0;
std::optional<geom::vector<int, 2>> fixed_resolution = std::nullopt;
bool highdpi = false;
};
app(std::string const & name);
app(std::string const & name, int multisampling);
app(std::string const & name, options const & opts);
~app() override;
virtual bool running() const;
virtual void stop();
void on_resize(int width, int height) override;
void present() override;
void poll_events();
void run();
void push_scene(std::shared_ptr<scene> s) override;
std::shared_ptr<scene> pop_scene() override;
void show_cursor(bool show);
bool vsync() const;
void vsync(bool on);
float time() const;
private:
psemek_declare_pimpl
};
}

View file

@ -0,0 +1,47 @@
#pragma once
#include <psemek/app/event_handler.hpp>
#include <memory>
#include <functional>
namespace psemek::app
{
struct application
: event_handler
{
// Data sent to platform backend for initialization
struct options
{
std::string name;
int multisampling = 4;
bool highdpi = false;
};
// Data received from platform backend after initialization
struct context
{
std::function<void(bool)> show_cursor;
std::function<void(bool)> vsync;
};
struct factory
{
virtual application::options const & options() = 0;
virtual std::unique_ptr<application> create(struct application::options const & options, context const & context) = 0;
virtual ~factory() {}
};
virtual bool running() const = 0;
virtual void stop() = 0;
virtual void update() = 0;
virtual void present() = 0;
};
// Implemented by the user, called by platform backends
std::unique_ptr<application::factory> make_application_factory();
}

View file

@ -0,0 +1,31 @@
#pragma once
#include <psemek/app/application.hpp>
#include <psemek/app/event_state.hpp>
#include <vector>
namespace psemek::app
{
struct application_base
: application
{
void on_event(resize_event const &) override;
void on_event(focus_event const &) override;
void on_event(mouse_move_event const &) override;
void on_event(mouse_wheel_event const &) override;
void on_event(mouse_button_event const &) override;
void on_event(key_event const &) override;
void stop() override;
bool running() const override;
event_state const & state() const { return state_; }
private:
bool running_ = true;
event_state state_;
};
}

View file

@ -0,0 +1,36 @@
#pragma once
#include <psemek/app/application.hpp>
#include <memory>
namespace psemek::app
{
template <typename Application>
std::unique_ptr<application::factory> default_application_factory(application::options const & options)
{
struct factory_impl
: application::factory
{
application::options opts;
factory_impl(application::options const & options)
: opts(options)
{}
application::options const & options() override
{
return opts;
}
std::unique_ptr<application> create(application::options const & options, application::context const & context) override
{
return std::make_unique<Application>(options, context);
}
};
return std::make_unique<factory_impl>(options);
}
}

View file

@ -0,0 +1,20 @@
#pragma once
#include <psemek/app/events.hpp>
namespace psemek::app
{
struct event_handler
{
virtual void on_event(resize_event const &) {}
virtual void on_event(focus_event const &) {}
virtual void on_event(mouse_move_event const &) {}
virtual void on_event(mouse_wheel_event const &) {}
virtual void on_event(mouse_button_event const &) {}
virtual void on_event(key_event const &) {}
virtual ~event_handler() {}
};
}

View file

@ -0,0 +1,56 @@
#pragma once
#include <psemek/app/events.hpp>
#include <unordered_set>
namespace psemek::app
{
struct event_state
{
geom::vector<int, 2> size = {0, 0};
bool focus = true;
geom::point<int, 2> mouse = {0, 0};
int wheel = 0;
std::unordered_set<mouse_button> mouse_button_down;
std::unordered_set<keycode> key_down;
};
inline void apply(event_state & state, resize_event const & event)
{
state.size = event.size;
}
inline void apply(event_state & state, focus_event const & event)
{
state.focus = event.gained;
}
inline void apply(event_state & state, mouse_move_event const & event)
{
state.mouse = event.position;
}
inline void apply(event_state & state, mouse_wheel_event const & event)
{
state.wheel += event.delta;
}
inline void apply(event_state & state, mouse_button_event const & event)
{
if (event.down)
state.mouse_button_down.insert(event.button);
else
state.mouse_button_down.erase(event.button);
}
inline void apply(event_state & state, key_event const & event)
{
if (event.down)
state.key_down.insert(event.key);
else
state.key_down.erase(event.key);
}
}

View file

@ -0,0 +1,52 @@
#pragma once
#include <psemek/geom/point.hpp>
namespace psemek::app
{
struct resize_event
{
geom::vector<int, 2> size;
};
struct focus_event
{
bool gained;
};
struct mouse_move_event
{
geom::point<int, 2> position;
};
struct mouse_wheel_event
{
int delta;
};
enum class mouse_button
{
left,
middle,
right,
};
struct mouse_button_event
{
mouse_button button;
bool down;
};
enum class keycode
{
// TODO
};
struct key_event
{
keycode key;
bool down;
};
}

View file

@ -1,47 +0,0 @@
#pragma once
#include <psemek/log/log.hpp>
#include <psemek/util/pretty_print.hpp>
#include <psemek/util/clock.hpp>
#include <utility>
namespace psemek::app
{
template <typename App, typename ... Args>
int main(Args && ... args) try
{
util::clock<std::chrono::milliseconds, std::chrono::high_resolution_clock> clock;
#ifdef PSEMEK_PACKAGE_MODE
log::level stdio_log_level = log::level::info;
#else
log::level stdio_log_level = log::level::debug;
#endif
log::add_sink(log::default_sink(io::std_out(), stdio_log_level));
log::register_thread("main");
App app(std::forward<Args>(args)...);
log::info() << "Started in " << util::pretty(clock.duration(), std::chrono::milliseconds{1});
log::info() << "Running";
app.run();
log::info() << "Quitting";
return EXIT_SUCCESS;
}
catch (std::exception const & e)
{
log::error() << e.what();
return EXIT_FAILURE;
}
catch (...)
{
log::error() << "Unknown exception";
return EXIT_FAILURE;
}
}

View file

@ -1,93 +1,19 @@
#pragma once
#include <psemek/geom/point.hpp>
#include <SDL2/SDL_keycode.h>
#include <optional>
#include <set>
#include <psemek/app/event_handler.hpp>
#include <psemek/app/event_state.hpp>
namespace psemek::app
{
struct app;
struct scene
: event_handler
{
virtual ~scene() = 0;
virtual void on_scene_enter(app * /* parent */) {}
virtual void on_scene_exit() {}
virtual void on_resize(int /* width */, int /* height */) {}
virtual void on_focus_gained() {}
virtual void on_focus_lost() {}
virtual void on_mouse_move(int /* x */, int /* y */, int /* dx */, int /* dy */) {}
virtual void on_mouse_wheel(int /* delta */) {}
virtual void on_left_button_down() {}
virtual void on_left_button_up() {}
virtual void on_middle_button_down() {}
virtual void on_middle_button_up() {}
virtual void on_right_button_down() {}
virtual void on_right_button_up() {}
virtual void on_key_down(SDL_Keycode /* key */) {}
virtual void on_key_up(SDL_Keycode /* key */) {}
virtual void on_text_input(std::string_view /* text */) {}
virtual void on_enter(event_state const &) {}
virtual void on_exit() {}
virtual void update() {}
virtual void present() {}
};
inline scene::~scene() = default;
struct scene_base
: scene
{
void on_scene_enter(app * parent) override { parent_ = parent; }
void on_scene_exit() override { parent_ = nullptr; }
void on_resize(int width, int height) override { width_ = width; height_ = height; }
void on_mouse_move(int x, int y, int, int) override { mouse_ = geom::point{x, y}; }
void on_left_button_down() override { left_button_down_ = true; }
void on_left_button_up() override { left_button_down_ = false; }
void on_middle_button_down() override { middle_button_down_ = true; }
void on_middle_button_up() override { middle_button_down_ = false; }
void on_right_button_down() override { right_button_down_ = true; }
void on_right_button_up() override { right_button_down_ = false; }
void on_key_down(SDL_Keycode key) override { keys_.insert(key); }
void on_key_up(SDL_Keycode key) override { keys_.erase(key); }
bool active() const { return parent_ != nullptr; }
app * parent() const { return parent_; }
bool is_left_button_down() const { return left_button_down_; }
bool is_middle_button_down() const { return middle_button_down_; }
bool is_right_button_down() const { return right_button_down_; }
std::optional<geom::point<int, 2>> mouse() const { return mouse_; }
bool is_key_down(SDL_Keycode key) const { return keys_.count(key) > 0; }
int width() const { return width_; }
int height() const { return height_; }
private:
app * parent_ = nullptr;
int width_ = 0;
int height_ = 0;
bool left_button_down_ = false;
bool middle_button_down_ = false;
bool right_button_down_ = false;
std::optional<geom::point<int, 2>> mouse_;
std::set<SDL_Keycode> keys_;
};
}

View file

@ -0,0 +1,29 @@
#pragma once
#include <psemek/app/application_base.hpp>
#include <psemek/app/scene.hpp>
namespace psemek::app
{
struct scene_application
: application_base
{
void on_event(resize_event const &) override;
void on_event(focus_event const &) override;
void on_event(mouse_move_event const &) override;
void on_event(mouse_wheel_event const &) override;
void on_event(mouse_button_event const &) override;
void on_event(key_event const &) override;
void update() override;
void present() override;
virtual std::shared_ptr<scene> current_scene() = 0;
private:
template <typename Event>
void on_event_impl(Event const & event);
};
}

View file

@ -1,19 +0,0 @@
#pragma once
#include <psemek/app/scene.hpp>
#include <memory>
namespace psemek::app
{
struct scene_manager
{
virtual void push_scene(std::shared_ptr<scene>) = 0;
virtual std::shared_ptr<scene> pop_scene() = 0;
virtual ~scene_manager() {}
};
}

View file

@ -1,326 +0,0 @@
#include <psemek/app/app.hpp>
#include <psemek/gfx/gl.hpp>
#include <psemek/log/log.hpp>
#include <psemek/sdl2/init.hpp>
#include <SDL2/SDL.h>
#include <vector>
namespace psemek::app
{
using clock = std::chrono::high_resolution_clock;
struct app::impl
{
app * parent;
std::shared_ptr<void> sdl_init;
SDL_Window * window = nullptr;
SDL_GLContext gl_context = nullptr;
std::vector<std::shared_ptr<scene>> scene_stack;
bool running = false;
bool had_initial_resize = false;
bool had_scene_exit = false;
clock::time_point start_time;
impl(app * parent)
: parent(parent)
, sdl_init(sdl2::init(SDL_INIT_VIDEO))
{}
~impl()
{
if (gl_context) SDL_GL_DeleteContext(gl_context);
if (window) SDL_DestroyWindow(window);
}
std::shared_ptr<scene> get_scene()
{
if (!scene_stack.empty())
return scene_stack.back();
return std::shared_ptr<scene>(parent, [](scene *){});
}
};
app::app(std::string const & name)
: app(name, 0)
{}
app::app(std::string const & name, int multisampling)
: app(name, options{multisampling})
{}
app::app(std::string const & name, options const & opts)
: pimpl_{make_impl(this)}
{
impl().start_time = clock::now();
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, gl::sys::major_version());
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, gl::sys::minor_version());
SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
if (opts.multisampling == 0)
{
SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 0);
}
else
{
SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 1);
SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, opts.multisampling);
}
std::uint32_t flags = SDL_WINDOW_OPENGL | SDL_WINDOW_HIDDEN;
if (!opts.fixed_resolution)
flags |= SDL_WINDOW_RESIZABLE | SDL_WINDOW_MAXIMIZED;
if (opts.highdpi) flags |= SDL_WINDOW_ALLOW_HIGHDPI;
int width = opts.fixed_resolution ? (*opts.fixed_resolution)[0] : 1024;
int height = opts.fixed_resolution ? (*opts.fixed_resolution)[1] : 768;
impl().window = SDL_CreateWindow(name.data(), SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, width, height, flags);
if (!impl().window)
sdl2::fail("Failed to create window: ");
impl().gl_context = SDL_GL_CreateContext(impl().window);
if (!impl().gl_context)
sdl2::fail("Failed to create OpenGL context: ");
SDL_GL_MakeCurrent(impl().window, impl().gl_context);
if (!gl::sys::initialize())
throw std::runtime_error("Failed to load OpenGL functions");
auto vendor = gl::GetString(gl::VENDOR);
auto renderer = gl::GetString(gl::RENDERER);
int major, minor;
gl::GetIntegerv(gl::MAJOR_VERSION, &major);
gl::GetIntegerv(gl::MINOR_VERSION, &minor);
log::info() << "Initialized OpenGL " << major << '.' << minor << ", " << vendor << ", " << renderer;
SDL_GL_GetDrawableSize(impl().window, &width, &height);
scene_base::on_resize(width, height);
log::info() << "Initial window size: " << width << 'x' << height;
SDL_StopTextInput();
}
app::~app()
{}
bool app::running() const
{
return impl().running;
}
void app::stop()
{
impl().running = false;
}
void app::on_resize(int width, int height)
{
scene_base::on_resize(width, height);
gl::Viewport(0, 0, width, height);
}
void app::poll_events()
{
auto handler = [this]{ return impl().get_scene(); };
for (SDL_Event e; SDL_PollEvent(&e);) switch (e.type)
{
case SDL_QUIT:
stop();
break;
case SDL_WINDOWEVENT: switch (e.window.event)
{
case SDL_WINDOWEVENT_RESIZED:
impl().had_initial_resize = true;
{
int width, height;
SDL_GL_GetDrawableSize(impl().window, &width, &height);
handler()->on_resize(width, height);
log::info() << "Window resized to " << width << 'x' << height;
}
break;
case SDL_WINDOWEVENT_FOCUS_GAINED:
handler()->on_focus_gained();
break;
case SDL_WINDOWEVENT_FOCUS_LOST:
handler()->on_focus_lost();
break;
}
break;
case SDL_MOUSEMOTION:
handler()->on_mouse_move(e.motion.x, e.motion.y, e.motion.xrel, e.motion.yrel);
break;
case SDL_MOUSEWHEEL:
handler()->on_mouse_wheel(e.wheel.y);
break;
case SDL_MOUSEBUTTONDOWN:
switch (e.button.button)
{
case SDL_BUTTON_LEFT:
handler()->on_left_button_down();
break;
case SDL_BUTTON_MIDDLE:
handler()->on_middle_button_down();
break;
case SDL_BUTTON_RIGHT:
handler()->on_right_button_down();
break;
}
break;
case SDL_MOUSEBUTTONUP:
switch (e.button.button)
{
case SDL_BUTTON_LEFT:
handler()->on_left_button_up();
break;
case SDL_BUTTON_MIDDLE:
handler()->on_middle_button_up();
break;
case SDL_BUTTON_RIGHT:
handler()->on_right_button_up();
break;
}
break;
case SDL_KEYDOWN:
handler()->on_key_down(e.key.keysym.sym);
break;
case SDL_KEYUP:
handler()->on_key_up(e.key.keysym.sym);
break;
case SDL_TEXTINPUT:
handler()->on_text_input(e.text.text);
break;
}
}
void app::present()
{
gl::ClearColor(0.7f, 0.7f, 1.f, 1.f);
gl::Clear(gl::COLOR_BUFFER_BIT);
}
void app::run()
{
SDL_ShowWindow(impl().window);
impl().running = true;
if (impl().get_scene().get() == this)
on_scene_enter(this);
while (running())
{
poll_events();
auto handler = [this]{ return impl().get_scene(); };
auto send_initial_resize = [this, &handler]{
if (!impl().had_initial_resize)
{
int w, h;
SDL_GetWindowSize(impl().window, &w, &h);
impl().had_initial_resize = true;
handler()->on_resize(w, h);
}
};
if (!running()) break;
send_initial_resize();
handler()->update();
// The handler might have changed during the update
send_initial_resize();
handler()->present();
SDL_GL_SwapWindow(impl().window);
}
if (!impl().had_scene_exit)
{
impl().had_scene_exit = true;
impl().get_scene()->on_scene_exit();
}
}
void app::push_scene(std::shared_ptr<scene> s)
{
if (!impl().had_scene_exit)
{
impl().had_scene_exit = true;
impl().get_scene()->on_scene_exit();
}
impl().had_initial_resize = false;
impl().had_scene_exit = false;
impl().scene_stack.push_back(std::move(s));
impl().get_scene()->on_scene_enter(this);
}
std::shared_ptr<scene> app::pop_scene()
{
if (impl().scene_stack.empty())
return nullptr;
if (!impl().had_scene_exit)
{
impl().had_scene_exit = true;
impl().get_scene()->on_scene_exit();
}
impl().had_initial_resize = false;
auto s = std::move(impl().scene_stack.back());
impl().scene_stack.pop_back();
impl().get_scene()->on_scene_enter(this);
return s;
}
void app::show_cursor(bool show)
{
SDL_ShowCursor(show ? SDL_TRUE : SDL_FALSE);
SDL_SetRelativeMouseMode(show ? SDL_FALSE : SDL_TRUE);
}
float app::time() const
{
return std::chrono::duration_cast<std::chrono::duration<float>>(clock::now() - impl().start_time).count();
}
bool app::vsync() const
{
return SDL_GL_GetSwapInterval() != 0;
}
void app::vsync(bool on)
{
if (on)
{
// try adaptive vsync
if (SDL_GL_SetSwapInterval(-1) != 0)
{
// failed, try usual vsync then
SDL_GL_SetSwapInterval(1);
}
}
else
{
SDL_GL_SetSwapInterval(0);
}
}
}

View file

@ -0,0 +1,46 @@
#include <psemek/app/application_base.hpp>
namespace psemek::app
{
void application_base::on_event(resize_event const & event)
{
apply(state_, event);
}
void application_base::on_event(focus_event const & event)
{
apply(state_, event);
}
void application_base::on_event(mouse_move_event const & event)
{
apply(state_, event);
}
void application_base::on_event(mouse_wheel_event const & event)
{
apply(state_, event);
}
void application_base::on_event(mouse_button_event const & event)
{
apply(state_, event);
}
void application_base::on_event(key_event const & event)
{
apply(state_, event);
}
void application_base::stop()
{
running_ = false;
}
bool application_base::running() const
{
return running_;
}
}

View file

@ -0,0 +1,56 @@
#include <psemek/app/scene_application.hpp>
namespace psemek::app
{
void scene_application::on_event(resize_event const & event)
{
on_event_impl(event);
}
void scene_application::on_event(focus_event const & event)
{
on_event_impl(event);
}
void scene_application::on_event(mouse_move_event const & event)
{
on_event_impl(event);
}
void scene_application::on_event(mouse_wheel_event const & event)
{
on_event_impl(event);
}
void scene_application::on_event(mouse_button_event const & event)
{
on_event_impl(event);
}
void scene_application::on_event(key_event const & event)
{
on_event_impl(event);
}
void scene_application::update()
{
if (auto scene = current_scene())
scene->update();
}
void scene_application::present()
{
if (auto scene = current_scene())
scene->present();
}
template <typename Event>
void scene_application::on_event_impl(Event const & event)
{
application_base::on_event(event);
if (auto scene = current_scene())
scene->on_event(event);
}
}

View file

@ -3,4 +3,4 @@ file(GLOB_RECURSE PSEMEK_AUDIO_SOURCES RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" "s
psemek_add_library(psemek-audio ${PSEMEK_AUDIO_HEADERS} ${PSEMEK_AUDIO_SOURCES})
target_include_directories(psemek-audio PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include")
target_link_libraries(psemek-audio PUBLIC psemek-sdl2 psemek-geom psemek-util psemek-log psemek-prof)
target_link_libraries(psemek-audio PUBLIC psemek-random psemek-geom psemek-util psemek-log psemek-prof)

File diff suppressed because it is too large Load diff

View file

@ -1,30 +1,20 @@
#pragma once
#include <psemek/audio/stream.hpp>
#include <psemek/audio/track.hpp>
#include <psemek/audio/channel.hpp>
#include <psemek/util/pimpl.hpp>
#include <psemek/util/span.hpp>
#include <psemek/io/stream.hpp>
#include <memory>
#include <string_view>
#include <vector>
namespace psemek::audio
{
struct engine
{
engine();
~engine();
virtual channel_ptr output() = 0;
channel_ptr output();
private:
psemek_declare_pimpl
virtual ~engine() {}
};
// Implemented by platform backend
std::unique_ptr<engine> make_engine();
}

View file

@ -1,97 +0,0 @@
#include <psemek/audio/engine.hpp>
#include <psemek/audio/constants.hpp>
#include <psemek/sdl2/init.hpp>
#include <psemek/log/log.hpp>
#include <psemek/prof/profiler.hpp>
#include <psemek/util/at_scope_exit.hpp>
#include <psemek/util/to_string.hpp>
#include <SDL2/SDL.h>
#include <mutex>
#include <atomic>
#include <vector>
#include <optional>
#include <cmath>
namespace psemek::audio
{
struct engine::impl
{
std::shared_ptr<void> sdl_init;
SDL_AudioDeviceID device;
std::vector<float> buffer;
bool thread_registered = false;
channel_ptr output;
impl();
~impl()
{
SDL_CloseAudioDevice(device);
}
static void callback(void * userdata, std::uint8_t * stream, int len);
};
engine::impl::impl()
: sdl_init(sdl2::init(SDL_INIT_AUDIO))
{
SDL_AudioSpec desired, obtained;
desired.freq = frequency;
desired.channels = 2;
desired.format = AUDIO_S16SYS;
desired.samples = 256;
desired.callback = &callback;
desired.userdata = this;
if (device = SDL_OpenAudioDevice(nullptr, 0, &desired, &obtained, 0); device == 0)
sdl2::fail("SDL_OpenAudioDevice failed:");
log::info() << "Initialized audio: " << static_cast<int>(obtained.channels) << " channels, " << obtained.freq << " Hz, " << obtained.samples << " samples";
buffer.resize(obtained.samples * obtained.channels);
output = std::make_shared<channel>();
SDL_PauseAudioDevice(device, 0);
}
void engine::impl::callback(void * userdata, std::uint8_t * dst_u8, int len)
{
static std::string const profiler_str = "audio";
prof::profiler prof(profiler_str);
auto self = static_cast<impl *>(userdata);
stream_ptr output = self->output->stream();
std::int16_t * dst = reinterpret_cast<std::int16_t *>(dst_u8);
if (!self->thread_registered)
{
log::register_thread("audio");
self->thread_registered = true;
}
std::size_t const size = len / 2;
std::size_t read = 0;
if (output)
read = output->read({self->buffer.data(), size});
std::fill(self->buffer.data() + read, self->buffer.data() + size, 0.f);
for (auto s : self->buffer)
*dst++ = static_cast<std::int16_t>(std::max(std::min((65535.f * s - 1.f) / 2.f, 32767.f), -32768.f));
}
engine::engine()
: pimpl_(make_impl())
{}
engine::~engine() = default;
channel_ptr engine::output()
{
return impl().output;
}
}

View file

@ -1,11 +1,9 @@
#include <psemek/audio/track.hpp>
#include <psemek/audio/constants.hpp>
#include <psemek/audio/detail/resampler.hpp>
#include <psemek/audio/audio_file/AudioFile.h>
#include <psemek/util/to_string.hpp>
#include <psemek/util/at_scope_exit.hpp>
#include <psemek/sdl2/init.hpp>
#include <SDL2/SDL_audio.h>
namespace psemek::audio
{
@ -13,38 +11,34 @@ namespace psemek::audio
namespace
{
std::vector<float> convert_audio(SDL_AudioSpec const & spec, std::uint8_t * samples, std::size_t length)
std::vector<float> convert_audio(std::vector<std::vector<float>> const & channels, int frequency)
{
if (spec.channels > 2)
throw std::runtime_error(util::to_string("Can't convert audio with ", static_cast<int>(spec.channels), " channels"));
if (channels.empty() || channels.size() > 2)
throw std::runtime_error(util::to_string("Can't convert audio with ", static_cast<int>(channels.size()), " channels"));
if (spec.format != AUDIO_S16SYS)
throw std::runtime_error(util::to_string("Can't convert audio with format ", spec.format));
std::vector<float> result(channels[0].size() * 2);
auto out = result.begin();
auto p = reinterpret_cast<std::int16_t *>(samples);
std::vector<float> result;
if (spec.channels == 1)
if (channels.size() == 1)
{
result.resize(length);
for (std::size_t i = 0; i < length / 2; ++i)
for (auto p = channels[0].begin(); p != channels[0].end(); ++p)
{
float v = (p[i] * 2.f + 1.f) / 65536.f;
result[2 * i + 0] = v;
result[2 * i + 1] = v;
*out++ = *p;
*out++ = *p;
}
}
else
else if (channels.size() == 2)
{
result.resize(length / 2);
for (std::size_t i = 0; i < length / 2; ++i)
result[i] = (p[i] * 2.f + 1.f) / 65536.f;
for (auto p0 = channels[0].begin(), p1 = channels[1].begin(); p0 != channels[0].end(); ++p0, ++p1)
{
*out++ = *p0;
*out++ = *p1;
}
}
if (spec.freq != audio::frequency)
if (frequency != audio::frequency)
{
audio::resampler resampler(audio::frequency * 1.f / spec.freq);
audio::resampler resampler(audio::frequency * 1.f / frequency);
resampler.feed(result);
result = resampler.grab_result();
}
@ -56,14 +50,17 @@ namespace psemek::audio
track_ptr load_wav(util::span<char const> data)
{
SDL_AudioSpec spec;
std::uint8_t * samples;
std::uint32_t length;
if (!SDL_LoadWAV_RW(SDL_RWFromConstMem(data.data(), data.size()), 1, &spec, &samples, &length))
sdl2::fail("SDL_LoadWAV_RW failed:");
std::vector<std::uint8_t> data_u8(data.size());
std::copy(data.begin(), data.end(), reinterpret_cast<char *>(data_u8.data()));
util::at_scope_exit release_samples([samples]{ SDL_FreeWAV(samples); });
return load_raw(convert_audio(spec, samples, length));
AudioFile<float> audio_file;
audio_file.shouldLogErrorsToConsole(false);
audio_file.onError = [](std::string const & error) {
throw std::runtime_error("failed to load WAV file: " + error);
};
audio_file.loadFromMemory(data_u8);
return load_raw(convert_audio(audio_file.samples, audio_file.getSampleRate()));
}
track_ptr load_wav(std::vector<char> const & data)

View file

@ -0,0 +1,8 @@
#pragma once
namespace psemek::gfx
{
void init();
}

22
libs/gfx/source/init.cpp Normal file
View file

@ -0,0 +1,22 @@
#include <psemek/gfx/init.hpp>
#include <psemek/gfx/gl.hpp>
#include <psemek/log/log.hpp>
namespace psemek::gfx
{
void init()
{
if (!gl::sys::initialize())
throw std::runtime_error("Failed to load OpenGL functions");
auto vendor = gl::GetString(gl::VENDOR);
auto renderer = gl::GetString(gl::RENDERER);
int major, minor;
gl::GetIntegerv(gl::MAJOR_VERSION, &major);
gl::GetIntegerv(gl::MINOR_VERSION, &minor);
log::info() << "Initialized OpenGL " << major << '.' << minor << ", " << vendor << ", " << renderer;
}
}

View file

@ -5,4 +5,4 @@ file(GLOB_RECURSE PSEMEK_SDL2_SOURCES RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" "so
psemek_add_library(psemek-sdl2 ${PSEMEK_SDL2_HEADERS} ${PSEMEK_SDL2_SOURCES})
target_include_directories(psemek-sdl2 PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include")
target_link_libraries(psemek-sdl2 PUBLIC psemek-log psemek-util psemek-gfx SDL2)
target_link_libraries(psemek-sdl2 PUBLIC psemek-log psemek-util psemek-gfx psemek-audio psemek-app SDL2)

View file

@ -0,0 +1,11 @@
#pragma once
#include <psemek/app/event_handler.hpp>
namespace psemek::sdl2
{
// Returns true is quit is requested
bool poll_events(app::event_handler & handler);
}

View file

@ -0,0 +1,28 @@
#pragma once
#include <psemek/app/application.hpp>
#include <SDL2/SDL.h>
namespace psemek::sdl2
{
struct window
{
window(psemek::app::application::options const & options);
~window();
geom::vector<int, 2> size() const;
void show();
void swap();
void show_cursor(bool show);
void vsync(bool on);
private:
std::shared_ptr<void> sdl_init_;
SDL_Window * window_ = nullptr;
SDL_GLContext gl_context_ = nullptr;
};
}

View file

@ -0,0 +1,111 @@
#include <psemek/audio/engine.hpp>
#include <psemek/audio/constants.hpp>
#include <psemek/sdl2/init.hpp>
#include <psemek/log/log.hpp>
#include <psemek/prof/profiler.hpp>
#include <psemek/util/at_scope_exit.hpp>
#include <psemek/util/to_string.hpp>
#include <SDL2/SDL.h>
#include <mutex>
#include <atomic>
#include <vector>
#include <optional>
#include <cmath>
namespace psemek::sdl2
{
namespace
{
struct audio_engine_impl
: audio::engine
{
audio_engine_impl();
~audio_engine_impl();
audio::channel_ptr output() override;
private:
std::shared_ptr<void> sdl_init_;
SDL_AudioDeviceID device_;
std::vector<float> buffer_;
bool thread_registered_ = false;
audio::channel_ptr output_;
static void callback(void * userdata, std::uint8_t * stream, int len);
};
audio_engine_impl::audio_engine_impl()
: sdl_init_(sdl2::init(SDL_INIT_AUDIO))
{
SDL_AudioSpec desired, obtained;
desired.freq = audio::frequency;
desired.channels = 2;
desired.format = AUDIO_S16SYS;
desired.samples = 256;
desired.callback = &callback;
desired.userdata = this;
if (device_ = SDL_OpenAudioDevice(nullptr, 0, &desired, &obtained, 0); device_ == 0)
fail("SDL_OpenAudioDevice failed:");
log::info() << "Initialized audio: " << static_cast<int>(obtained.channels) << " channels, " << obtained.freq << " Hz, " << obtained.samples << " samples";
buffer_.resize(obtained.samples * obtained.channels);
output_ = std::make_shared<audio::channel>();
SDL_PauseAudioDevice(device_, 0);
}
audio_engine_impl::~audio_engine_impl()
{
SDL_CloseAudioDevice(device_);
}
void audio_engine_impl::callback(void * userdata, std::uint8_t * dst_u8, int len)
{
static std::string const profiler_str = "audio";
prof::profiler prof(profiler_str);
auto self = static_cast<audio_engine_impl *>(userdata);
auto stream = self->output()->stream();
std::int16_t * dst = reinterpret_cast<std::int16_t *>(dst_u8);
if (!self->thread_registered_)
{
log::register_thread("audio");
self->thread_registered_ = true;
}
std::size_t const size = len / 2;
std::size_t read = 0;
if (stream)
read = stream->read({self->buffer_.data(), size});
std::fill(self->buffer_.data() + read, self->buffer_.data() + size, 0.f);
for (auto s : self->buffer_)
*dst++ = static_cast<std::int16_t>(std::max(std::min((65535.f * s - 1.f) / 2.f, 32767.f), -32768.f));
}
audio::channel_ptr audio_engine_impl::output()
{
return output_;
}
}
}
namespace psemek::audio
{
std::unique_ptr<engine> make_engine()
{
return std::make_unique<sdl2::audio_engine_impl>();
}
}

View file

@ -0,0 +1,85 @@
#include <psemek/sdl2/events.hpp>
#include <optional>
#include <SDL2/SDL.h>
namespace psemek::sdl2
{
namespace
{
std::optional<app::mouse_button> mouse_button(Uint8 button)
{
switch (button)
{
case SDL_BUTTON_LEFT:
return app::mouse_button::left;
case SDL_BUTTON_MIDDLE:
return app::mouse_button::middle;
case SDL_BUTTON_RIGHT:
return app::mouse_button::right;
default:
return std::nullopt;
}
}
std::optional<app::keycode> keycode(SDL_Keysym)
{
return std::nullopt;
}
}
bool poll_events(app::event_handler & handler)
{
for (SDL_Event e; SDL_PollEvent(&e);) switch (e.type)
{
case SDL_QUIT:
return true;
case SDL_WINDOWEVENT: switch (e.window.event)
{
case SDL_WINDOWEVENT_CLOSE:
return true;
case SDL_WINDOWEVENT_RESIZED:
handler.on_event(app::resize_event{{e.window.data1, e.window.data2}});
break;
case SDL_WINDOWEVENT_SIZE_CHANGED:
handler.on_event(app::resize_event{{e.window.data1, e.window.data2}});
break;
case SDL_WINDOWEVENT_FOCUS_GAINED:
handler.on_event(app::focus_event{true});
break;
case SDL_WINDOWEVENT_FOCUS_LOST:
handler.on_event(app::focus_event{false});
break;
}
break;
case SDL_MOUSEMOTION:
handler.on_event(app::mouse_move_event{{e.motion.x, e.motion.y}});
break;
case SDL_MOUSEWHEEL:
handler.on_event(app::mouse_wheel_event{e.wheel.y});
break;
case SDL_MOUSEBUTTONDOWN:
if (auto button = mouse_button(e.button.button))
handler.on_event(app::mouse_button_event{*button, true});
break;
case SDL_MOUSEBUTTONUP:
if (auto button = mouse_button(e.button.button))
handler.on_event(app::mouse_button_event{*button, false});
break;
case SDL_KEYDOWN:
if (auto key = keycode(e.key.keysym))
handler.on_event(app::key_event{*key, true});
break;
case SDL_KEYUP:
if (auto key = keycode(e.key.keysym))
handler.on_event(app::key_event{*key, false});
break;
}
return false;
}
}

68
libs/sdl2/source/main.cpp Normal file
View file

@ -0,0 +1,68 @@
#include <psemek/app/application.hpp>
#include <psemek/gfx/init.hpp>
#include <psemek/sdl2/init.hpp>
#include <psemek/sdl2/window.hpp>
#include <psemek/sdl2/events.hpp>
#include <psemek/util/clock.hpp>
#include <psemek/util/pretty_print.hpp>
#include <psemek/log/log.hpp>
#undef main
int main() try
{
using namespace psemek;
util::clock<std::chrono::milliseconds, std::chrono::high_resolution_clock> clock;
#ifdef PSEMEK_PACKAGE_MODE
log::level const stdio_log_level = log::level::info;
#else
log::level const stdio_log_level = log::level::debug;
#endif
log::add_sink(log::default_sink(io::std_out(), stdio_log_level));
log::register_thread("main");
auto const factory = app::make_application_factory();
auto const options = factory->options();
sdl2::window window(options);
gfx::init();
app::application::context context;
context.show_cursor = [&](bool show){ window.show_cursor(show); };
context.vsync = [&](bool on){ window.vsync(on); };
auto application = factory->create(options, context);
application->on_event(app::resize_event{window.size()});
window.show();
log::info() << "Started in " << util::pretty(clock.duration(), std::chrono::milliseconds{1});
log::info() << "Running";
while (application->running())
{
if (sdl2::poll_events(*application))
application->stop();
if (!application->running()) break;
application->update();
application->present();
window.swap();
}
log::info() << "Quitting";
return EXIT_SUCCESS;
}
catch (std::exception const & e)
{
psemek::log::error() << e.what();
return EXIT_FAILURE;
}
catch (...)
{
psemek::log::error() << "Unknown exception";
return EXIT_FAILURE;
}

View file

@ -0,0 +1,96 @@
#include <psemek/sdl2/window.hpp>
#include <psemek/sdl2/init.hpp>
#include <psemek/gfx/gl.hpp>
namespace psemek::sdl2
{
window::window(psemek::app::application::options const & options)
: sdl_init_(init(SDL_INIT_EVENTS | SDL_INIT_VIDEO))
{
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, gl::sys::major_version());
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, gl::sys::minor_version());
SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
if (options.multisampling == 0)
{
SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 0);
}
else
{
SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 1);
SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, options.multisampling);
}
std::uint32_t flags = SDL_WINDOW_OPENGL | SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_MAXIMIZED | SDL_WINDOW_BORDERLESS | SDL_WINDOW_FULLSCREEN_DESKTOP;
if (options.highdpi) flags |= SDL_WINDOW_ALLOW_HIGHDPI;
SDL_DisplayMode display_mode;
SDL_GetCurrentDisplayMode(0, &display_mode);
window_ = SDL_CreateWindow(options.name.data(), SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, display_mode.w, display_mode.h, flags);
if (!window_)
sdl2::fail("Failed to create window: ");
gl_context_ = SDL_GL_CreateContext(window_);
if (!gl_context_)
sdl2::fail("Failed to create OpenGL context: ");
SDL_GL_MakeCurrent(window_, gl_context_);
}
geom::vector<int, 2> window::size() const
{
geom::vector<int, 2> result;
SDL_GL_GetDrawableSize(window_, &result[0], &result[1]);
return result;
}
void window::show()
{
SDL_ShowWindow(window_);
}
void window::swap()
{
SDL_GL_SwapWindow(window_);
}
void window::show_cursor(bool show)
{
SDL_ShowCursor(show ? SDL_TRUE : SDL_FALSE);
SDL_SetRelativeMouseMode(show ? SDL_FALSE : SDL_TRUE);
}
void window::vsync(bool on)
{
if (on)
{
// try adaptive vsync
if (SDL_GL_SetSwapInterval(-1) != 0)
{
// failed, try usual vsync then
SDL_GL_SetSwapInterval(1);
}
}
else
{
SDL_GL_SetSwapInterval(0);
}
}
window::~window()
{
if (gl_context_)
SDL_GL_DeleteContext(gl_context_);
if (window_)
SDL_DestroyWindow(window_);
}
}