mirror of
https://github.com/Relintai/pandemonium_engine.git
synced 2024-12-27 14:17:37 +01:00
506 lines
14 KiB
C++
506 lines
14 KiB
C++
// sdl2_adapter.h
|
|
/*
|
|
FRT - A Godot platform targeting single board computers
|
|
Copyright (c) 2017-2022 Emanuele Fornara
|
|
SPDX-License-Identifier: MIT
|
|
*/
|
|
|
|
/*
|
|
|
|
KNOWN ISSUES:
|
|
|
|
Unicode
|
|
|
|
Keyboard/Text SDL2 events don't map very well to Godot input events.
|
|
If a SDL2 keypress event might need to be "translated", that event is saved
|
|
and passed to Godot only when a translation is received.
|
|
This is incomplete and likely to be a source of bugs. Ctrl+... combinations
|
|
for example don't seem to generate a translation. Multiple keypresses
|
|
are also going to have a wrong unicode translation.
|
|
Still, this implementation seems a good compromise: simple enough,
|
|
yet functional enough.
|
|
The 2D platformer feels responsive (even when adding WASD input mappings
|
|
and pressing multiple keys), and the editor is usable for basic code editing
|
|
(tested with US and IT keymaps).
|
|
|
|
*/
|
|
|
|
#include <SDL.h>
|
|
|
|
namespace frt {
|
|
|
|
void *(*get_proc_address)(const char *) = SDL_GL_GetProcAddress;
|
|
|
|
struct SampleProducer {
|
|
virtual void produce_samples(int n_of_frames, int32_t *frames) = 0;
|
|
virtual ~SampleProducer();
|
|
};
|
|
|
|
SampleProducer::~SampleProducer() {
|
|
}
|
|
|
|
void audio_callback(void *userdata, Uint8 *stream, int len);
|
|
|
|
class Audio {
|
|
private:
|
|
SampleProducer *producer_;
|
|
SDL_mutex *mutex_;
|
|
int32_t *samples_;
|
|
int n_of_samples_;
|
|
public:
|
|
Audio(SampleProducer *producer) : producer_(producer) {
|
|
mutex_ = 0;
|
|
}
|
|
bool init(int mix_rate, int samples) {
|
|
SDL_AudioSpec desired, obtained;
|
|
memset(&desired, 0, sizeof(desired));
|
|
desired.freq = mix_rate;
|
|
desired.format = AUDIO_S16;
|
|
desired.channels = 2;
|
|
desired.samples = samples;
|
|
desired.callback = audio_callback;
|
|
desired.userdata = this;
|
|
if (SDL_OpenAudio(&desired, &obtained))
|
|
return false;
|
|
mutex_ = SDL_CreateMutex();
|
|
n_of_samples_ = obtained.channels * obtained.samples;
|
|
samples_ = new int32_t[n_of_samples_];
|
|
return true;
|
|
}
|
|
void start() {
|
|
SDL_PauseAudio(SDL_FALSE);
|
|
}
|
|
void lock() {
|
|
SDL_LockMutex(mutex_);
|
|
}
|
|
void unlock() {
|
|
SDL_UnlockMutex(mutex_);
|
|
}
|
|
void finish() {
|
|
SDL_PauseAudio(SDL_TRUE);
|
|
SDL_LockAudio();
|
|
// calling of sdl2 callback not expected after pause+lock
|
|
SDL_DestroyMutex(mutex_);
|
|
mutex_ = 0;
|
|
delete[] samples_;
|
|
samples_ = 0;
|
|
SDL_UnlockAudio();
|
|
SDL_CloseAudio();
|
|
}
|
|
void fill_buffer(unsigned char *data, int length) {
|
|
int n_of_samples = length / sizeof(int16_t);
|
|
if (n_of_samples > n_of_samples_) // just in case, it shouldn't happen
|
|
n_of_samples = n_of_samples_;
|
|
const int channels = 2;
|
|
SDL_LockMutex(mutex_);
|
|
producer_->produce_samples(n_of_samples / channels, samples_);
|
|
SDL_UnlockMutex(mutex_);
|
|
int16_t *data16 = (int16_t *)data; // assume alignment is fine
|
|
for (int i = 0; i < n_of_samples; i++)
|
|
data16[i] = samples_[i] >> 16;
|
|
}
|
|
};
|
|
|
|
void audio_callback(void *userdata, Uint8 *stream, int len) {
|
|
Audio *audio = (Audio *)userdata;
|
|
audio->fill_buffer(stream, len);
|
|
}
|
|
|
|
struct ivec2 {
|
|
int x;
|
|
int y;
|
|
};
|
|
|
|
enum HatMask {
|
|
HatUp = 1,
|
|
HatRight = 2,
|
|
HatDown = 4,
|
|
HatLeft = 8
|
|
};
|
|
|
|
enum MouseButton {
|
|
ButtonLeft = 1,
|
|
ButtonRight = 2,
|
|
ButtonMiddle = 3,
|
|
WheelUp = 4,
|
|
WheelDown = 5
|
|
};
|
|
|
|
enum MouseMode {
|
|
MouseVisible,
|
|
MouseHidden,
|
|
MouseCaptured
|
|
};
|
|
|
|
struct EventHandler {
|
|
virtual ~EventHandler();
|
|
virtual void handle_resize_event(ivec2 size) = 0;
|
|
virtual void handle_key_event(int sdl2_code, int unicode, bool pressed) = 0;
|
|
virtual void handle_mouse_motion_event(ivec2 pos, ivec2 dpos) = 0;
|
|
virtual void handle_mouse_button_event(int button, bool pressed, bool doubleclick) = 0;
|
|
virtual void handle_js_status_event(int id, bool connected, const char *name, const char *guid) = 0;
|
|
virtual void handle_js_button_event(int id, int button, bool pressed) = 0;
|
|
virtual void handle_js_axis_event(int id, int axis, float value) = 0;
|
|
virtual void handle_js_hat_event(int id, int mask) = 0;
|
|
virtual void handle_quit_event() = 0;
|
|
virtual void handle_flush_events() = 0;
|
|
};
|
|
|
|
EventHandler::~EventHandler() {
|
|
}
|
|
|
|
struct InputModifierState {
|
|
bool shift;
|
|
bool alt;
|
|
bool control;
|
|
bool meta;
|
|
InputModifierState() : shift(false), alt(false), control(false), meta(false) {}
|
|
};
|
|
|
|
enum GraphicsAPI {
|
|
API_OpenGL_ES2,
|
|
API_OpenGL_ES3
|
|
};
|
|
|
|
class OS_FRT {
|
|
private:
|
|
static const int MAX_JOYSTICKS = 16;
|
|
static const int REQUEST_UNICODE = -1;
|
|
SDL_Window *window_;
|
|
SDL_GLContext context_;
|
|
EventHandler *handler_;
|
|
InputModifierState st_;
|
|
MouseMode mouse_mode_;
|
|
SDL_KeyboardEvent key_ev_;
|
|
int key_unicode_;
|
|
SDL_Joystick *js_[MAX_JOYSTICKS];
|
|
bool exit_shortcuts_;
|
|
void resize_event(const SDL_Event &ev) {
|
|
ivec2 size;
|
|
SDL_GL_GetDrawableSize(window_, &size.x, &size.y);
|
|
handler_->handle_resize_event(size);
|
|
}
|
|
int utf8_to_unicode(const char *s) {
|
|
if ((s[0] & 0x80) == 0)
|
|
return s[0];
|
|
else if ((s[0] & 0xe0) == 0xc0)
|
|
return ((s[0] & 0x1f) << 6) | (s[1] & 0x3f);
|
|
else if ((s[0] & 0xf0) == 0xe0)
|
|
return ((s[0] & 0x0f) << 12) | ((s[1] & 0x3f) << 6) | (s[2] & 0x3f);
|
|
else
|
|
return 0;
|
|
}
|
|
void text_event(const SDL_TextInputEvent &text) {
|
|
if (key_unicode_ != REQUEST_UNICODE)
|
|
return;
|
|
key_unicode_ = utf8_to_unicode(text.text);
|
|
int sdl2_code = key_ev_.keysym.sym;
|
|
handler_->handle_key_event(sdl2_code, key_unicode_, true);
|
|
}
|
|
bool require_unicode(int c) {
|
|
return (c >= 0x20 && c < 0x80) || (c >= 0xa0 && c < 0xff);
|
|
}
|
|
void key_event(const SDL_KeyboardEvent &key) {
|
|
st_.shift = key.keysym.mod & KMOD_SHIFT;
|
|
st_.alt = key.keysym.mod & KMOD_ALT;
|
|
st_.control = key.keysym.mod & KMOD_CTRL;
|
|
st_.meta = key.keysym.mod & KMOD_GUI;
|
|
bool pressed = key.state == SDL_PRESSED;
|
|
int sdl2_code = key.keysym.sym;
|
|
if (exit_shortcuts_ && st_.alt && pressed && sdl2_code == SDLK_KP_ENTER)
|
|
fatal("exit_shortcut (alt+enter), disable by setting FRT_NO_EXIT_SHORTCUTS");
|
|
if (pressed && !key.repeat && require_unicode(sdl2_code)) {
|
|
key_ev_ = key;
|
|
key_unicode_ = REQUEST_UNICODE;
|
|
return;
|
|
}
|
|
handler_->handle_key_event(sdl2_code, key_unicode_, pressed);
|
|
if (!pressed)
|
|
key_unicode_ = 0;
|
|
}
|
|
void mouse_event(const SDL_Event &ev) {
|
|
int os_button;
|
|
if (ev.type == SDL_MOUSEMOTION) {
|
|
ivec2 pos = { ev.motion.x, ev.motion.y };
|
|
ivec2 dpos = { ev.motion.xrel, ev.motion.yrel };
|
|
handler_->handle_mouse_motion_event(pos, dpos);
|
|
} else if (ev.type == SDL_MOUSEWHEEL) {
|
|
if (ev.wheel.y > 0)
|
|
os_button = WheelUp;
|
|
else if (ev.wheel.y < 0)
|
|
os_button = WheelDown;
|
|
else
|
|
return;
|
|
handler_->handle_mouse_button_event(os_button, true, false);
|
|
handler_->handle_mouse_button_event(os_button, false, false);
|
|
} else { // SDL_MOUSEBUTTONUP, SDL_MOUSEBUTTONDOWN
|
|
switch (ev.button.button) {
|
|
case SDL_BUTTON_LEFT:
|
|
os_button = ButtonLeft;
|
|
break;
|
|
case SDL_BUTTON_MIDDLE:
|
|
os_button = ButtonMiddle;
|
|
break;
|
|
case SDL_BUTTON_RIGHT:
|
|
os_button = ButtonRight;
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
handler_->handle_mouse_button_event(os_button, ev.button.state == SDL_PRESSED, ev.button.clicks > 1);
|
|
}
|
|
}
|
|
int get_js_id(int inst_id) {
|
|
SDL_Joystick *js = SDL_JoystickFromInstanceID(inst_id);
|
|
for (int id = 0; id < MAX_JOYSTICKS; id++)
|
|
if (js_[id] == js)
|
|
return id;
|
|
return -1;
|
|
}
|
|
void js_event(const SDL_Event &ev) {
|
|
int id;
|
|
switch (ev.type) {
|
|
case SDL_JOYAXISMOTION: {
|
|
if ((id = get_js_id(ev.jaxis.which)) < 0)
|
|
return;
|
|
int axis = ev.jaxis.axis;
|
|
float value = (float)ev.jaxis.value / 32768.0f;
|
|
handler_->handle_js_axis_event(id, axis, value);
|
|
} break;
|
|
case SDL_JOYHATMOTION: {
|
|
if ((id = get_js_id(ev.jhat.which)) < 0)
|
|
return;
|
|
if (ev.jhat.hat != 0)
|
|
return;
|
|
int mask = 0;
|
|
switch (ev.jhat.value) {
|
|
case SDL_HAT_LEFT:
|
|
mask = HatLeft;
|
|
break;
|
|
case SDL_HAT_LEFTUP:
|
|
mask = HatLeft | HatUp;
|
|
break;
|
|
case SDL_HAT_UP:
|
|
mask = HatUp;
|
|
break;
|
|
case SDL_HAT_RIGHTUP:
|
|
mask = HatRight | HatUp;
|
|
break;
|
|
case SDL_HAT_RIGHT:
|
|
mask = HatRight;
|
|
break;
|
|
case SDL_HAT_RIGHTDOWN:
|
|
mask = HatRight | HatDown;
|
|
break;
|
|
case SDL_HAT_DOWN:
|
|
mask = HatDown;
|
|
break;
|
|
case SDL_HAT_LEFTDOWN:
|
|
mask = HatLeft | HatDown;
|
|
break;
|
|
}
|
|
handler_->handle_js_hat_event(id, mask);
|
|
} break;
|
|
case SDL_JOYBUTTONDOWN:
|
|
case SDL_JOYBUTTONUP: {
|
|
if ((id = get_js_id(ev.jbutton.which)) < 0)
|
|
return;
|
|
int button = ev.jbutton.button;
|
|
bool pressed = ev.jbutton.state == SDL_PRESSED;
|
|
if (exit_shortcuts_ && button == 6 && pressed)
|
|
fatal("exit_shortcut (joystick button #6), disable by setting FRT_NO_EXIT_SHORTCUTS");
|
|
handler_->handle_js_button_event(id, button, pressed);
|
|
} break;
|
|
case SDL_JOYDEVICEADDED: {
|
|
id = ev.jdevice.which;
|
|
if (id >= MAX_JOYSTICKS)
|
|
return;
|
|
const char *name = SDL_JoystickNameForIndex(id);
|
|
char guid[64];
|
|
SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(id), guid, sizeof(guid));
|
|
handler_->handle_js_status_event(id, true, name, guid);
|
|
js_[id] = SDL_JoystickOpen(id);
|
|
} break;
|
|
case SDL_JOYDEVICEREMOVED: {
|
|
if ((id = get_js_id(ev.jdevice.which)) < 0)
|
|
return;
|
|
SDL_JoystickClose(js_[id]);
|
|
js_[id] = 0;
|
|
handler_->handle_js_status_event(id, false, "", "");
|
|
} break;
|
|
}
|
|
}
|
|
public:
|
|
OS_FRT(EventHandler *handler) : handler_(handler) {
|
|
mouse_mode_ = MouseVisible;
|
|
key_unicode_ = 0;
|
|
memset(js_, 0, sizeof(js_));
|
|
exit_shortcuts_ = !getenv("FRT_NO_EXIT_SHORTCUTS");
|
|
}
|
|
void init(GraphicsAPI api, int width, int height, bool resizable, bool borderless, bool always_on_top) {
|
|
setenv("SDL_VIDEO_RPI_OPTIONS", "gravity=center,scale=letterbox,background=1", 0);
|
|
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_JOYSTICK) < 0)
|
|
fatal("SDL_Init failed: %s.", SDL_GetError());
|
|
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
|
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, api == API_OpenGL_ES2 ? 2 : 3);
|
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
|
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
|
|
int flags = SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN | SDL_WINDOW_ALLOW_HIGHDPI;
|
|
if (resizable)
|
|
flags |= SDL_WINDOW_RESIZABLE;
|
|
if (borderless)
|
|
flags |= SDL_WINDOW_BORDERLESS;
|
|
if (always_on_top)
|
|
flags |= SDL_WINDOW_ALWAYS_ON_TOP;
|
|
if (!(window_ = SDL_CreateWindow("frt2", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, width, height, flags)))
|
|
fatal("SDL_CreateWindow failed: %s.", SDL_GetError());
|
|
context_ = SDL_GL_CreateContext(window_);
|
|
SDL_GL_MakeCurrent(window_, context_);
|
|
}
|
|
void cleanup() {
|
|
SDL_DestroyWindow(window_);
|
|
SDL_Quit();
|
|
}
|
|
void make_current() {
|
|
SDL_GL_MakeCurrent(window_, context_);
|
|
}
|
|
void release_current() {
|
|
// TODO: add release
|
|
}
|
|
void swap_buffers() {
|
|
SDL_GL_SwapWindow(window_);
|
|
}
|
|
void set_use_vsync(bool enable) {
|
|
SDL_GL_SetSwapInterval(enable ? 1 : 0);
|
|
}
|
|
bool is_vsync_enabled() const {
|
|
return SDL_GL_GetSwapInterval() != 0;
|
|
}
|
|
void dispatch_events() {
|
|
SDL_Event ev;
|
|
while (SDL_PollEvent(&ev)) {
|
|
switch (ev.type) {
|
|
case SDL_WINDOWEVENT:
|
|
if (ev.window.event == SDL_WINDOWEVENT_SIZE_CHANGED)
|
|
resize_event(ev);
|
|
break;
|
|
case SDL_TEXTINPUT:
|
|
text_event(ev.text);
|
|
break;
|
|
case SDL_KEYUP:
|
|
case SDL_KEYDOWN:
|
|
key_event(ev.key);
|
|
break;
|
|
case SDL_MOUSEMOTION:
|
|
case SDL_MOUSEWHEEL:
|
|
case SDL_MOUSEBUTTONUP:
|
|
case SDL_MOUSEBUTTONDOWN:
|
|
mouse_event(ev);
|
|
break;
|
|
case SDL_JOYAXISMOTION:
|
|
case SDL_JOYHATMOTION:
|
|
case SDL_JOYBUTTONDOWN:
|
|
case SDL_JOYBUTTONUP:
|
|
case SDL_JOYDEVICEADDED:
|
|
case SDL_JOYDEVICEREMOVED:
|
|
js_event(ev);
|
|
break;
|
|
case SDL_QUIT:
|
|
handler_->handle_quit_event();
|
|
break;
|
|
}
|
|
}
|
|
handler_->handle_flush_events();
|
|
}
|
|
const InputModifierState *get_modifier_state() const {
|
|
return &st_;
|
|
}
|
|
void set_title(const char *title) {
|
|
SDL_SetWindowTitle(window_, title);
|
|
}
|
|
void set_icon(int width, int height, const unsigned char *data) {
|
|
SDL_Surface *icon = SDL_CreateRGBSurfaceWithFormat(0, width, height, 32, SDL_PIXELFORMAT_ABGR8888);
|
|
if (!icon)
|
|
return;
|
|
SDL_LockSurface(icon);
|
|
memcpy(icon->pixels, data, width * height * 4);
|
|
SDL_UnlockSurface(icon);
|
|
SDL_SetWindowIcon(window_, icon);
|
|
SDL_FreeSurface(icon);
|
|
}
|
|
void set_pos(ivec2 pos) {
|
|
SDL_SetWindowPosition(window_, pos.x, pos.y);
|
|
}
|
|
ivec2 get_pos() const {
|
|
ivec2 pos;
|
|
SDL_GetWindowPosition(window_, &pos.x, &pos.y);
|
|
return pos;
|
|
}
|
|
void set_size(ivec2 size) {
|
|
SDL_SetWindowSize(window_, size.x, size.y);
|
|
}
|
|
ivec2 get_size() const {
|
|
ivec2 size;
|
|
SDL_GetWindowSize(window_, &size.x, &size.y);
|
|
return size;
|
|
}
|
|
void set_fullscreen(bool enable) {
|
|
SDL_SetWindowFullscreen(window_, enable ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0); // TODO: hint
|
|
}
|
|
bool is_fullscreen() const {
|
|
return SDL_GetWindowFlags(window_) & SDL_WINDOW_FULLSCREEN_DESKTOP;
|
|
}
|
|
void set_always_on_top(bool enable) {
|
|
// NOT IMPLEMENTED
|
|
}
|
|
bool is_always_on_top() const {
|
|
return SDL_GetWindowFlags(window_) & SDL_WINDOW_ALWAYS_ON_TOP;
|
|
}
|
|
void set_resizable(bool enable) {
|
|
SDL_SetWindowResizable(window_, enable ? SDL_TRUE : SDL_FALSE);
|
|
}
|
|
bool is_resizable() const {
|
|
return SDL_GetWindowFlags(window_) & SDL_WINDOW_RESIZABLE;
|
|
}
|
|
void set_maximized(bool enable) {
|
|
if (enable)
|
|
SDL_MaximizeWindow(window_);
|
|
else
|
|
SDL_RestoreWindow(window_);
|
|
}
|
|
bool is_maximized() const {
|
|
return SDL_GetWindowFlags(window_) & SDL_WINDOW_MAXIMIZED;
|
|
}
|
|
void set_minimized(bool enable) {
|
|
if (enable)
|
|
SDL_MinimizeWindow(window_);
|
|
else
|
|
SDL_RestoreWindow(window_);
|
|
}
|
|
bool is_minimized() const {
|
|
return SDL_GetWindowFlags(window_) & SDL_WINDOW_MINIMIZED;
|
|
}
|
|
void set_mouse_mode(MouseMode mouse_mode) {
|
|
switch (mouse_mode) {
|
|
case MouseVisible:
|
|
SDL_CaptureMouse(SDL_FALSE);
|
|
SDL_ShowCursor(1);
|
|
break;
|
|
case MouseHidden:
|
|
SDL_CaptureMouse(SDL_FALSE);
|
|
SDL_ShowCursor(0);
|
|
break;
|
|
case MouseCaptured:
|
|
SDL_ShowCursor(0);
|
|
SDL_CaptureMouse(SDL_TRUE);
|
|
break;
|
|
}
|
|
mouse_mode_ = mouse_mode;
|
|
}
|
|
MouseMode get_mouse_mode() const {
|
|
return mouse_mode_;
|
|
}
|
|
};
|
|
|
|
} // namespace frt
|