winamp/src/duckstation-nogui/wayland_nogui_platform.cpp
2023-10-02 23:59:01 +10:00

484 lines
16 KiB
C++

// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
#include "wayland_nogui_platform.h"
#include "common/assert.h"
#include "common/log.h"
#include "common/string_util.h"
#include "common/threading.h"
#include "core/host.h"
#include "nogui_host.h"
#include "nogui_platform.h"
#include <linux/input-event-codes.h>
#include <sys/mman.h>
#include <thread>
#include <unistd.h>
Log_SetChannel(WaylandNoGUIPlatform);
WaylandNoGUIPlatform::WaylandNoGUIPlatform()
{
m_message_loop_running.store(true, std::memory_order_release);
}
WaylandNoGUIPlatform::~WaylandNoGUIPlatform()
{
if (m_xkb_state)
xkb_state_unref(m_xkb_state);
if (m_xkb_keymap)
xkb_keymap_unref(m_xkb_keymap);
if (m_wl_keyboard)
wl_keyboard_destroy(m_wl_keyboard);
if (m_wl_pointer)
wl_pointer_destroy(m_wl_pointer);
if (m_wl_seat)
wl_seat_destroy(m_wl_seat);
if (m_xkb_context)
xkb_context_unref(m_xkb_context);
if (m_registry)
wl_registry_destroy(m_registry);
}
bool WaylandNoGUIPlatform::Initialize()
{
m_xkb_context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
if (!m_xkb_context)
{
Panic("Failed to create XKB context");
return false;
}
m_display = wl_display_connect(nullptr);
if (!m_display)
{
Panic("Failed to connect to Wayland display.");
return false;
}
static const wl_registry_listener registry_listener = {GlobalRegistryHandler, GlobalRegistryRemover};
m_registry = wl_display_get_registry(m_display);
wl_registry_add_listener(m_registry, &registry_listener, this);
// Call back to registry listener to get compositor/shell.
wl_display_dispatch_pending(m_display);
wl_display_roundtrip(m_display);
// We need a shell/compositor, or at least one we understand.
if (!m_compositor || !m_xdg_wm_base)
{
Panic("Missing Wayland shell/compositor\n");
return false;
}
static const xdg_wm_base_listener xdg_wm_base_listener = {XDGWMBasePing};
xdg_wm_base_add_listener(m_xdg_wm_base, &xdg_wm_base_listener, this);
wl_display_dispatch_pending(m_display);
wl_display_roundtrip(m_display);
return true;
}
void WaylandNoGUIPlatform::ReportError(const std::string_view& title, const std::string_view& message)
{
// not implemented
}
bool WaylandNoGUIPlatform::ConfirmMessage(const std::string_view& title, const std::string_view& message)
{
// not implemented
return true;
}
void WaylandNoGUIPlatform::SetDefaultConfig(SettingsInterface& si) {}
bool WaylandNoGUIPlatform::CreatePlatformWindow(std::string title)
{
s32 window_x, window_y, window_width, window_height;
bool has_window_pos = NoGUIHost::GetSavedPlatformWindowGeometry(&window_x, &window_y, &window_width, &window_height);
if (!has_window_pos)
{
window_x = 0;
window_y = 0;
window_width = DEFAULT_WINDOW_WIDTH;
window_height = DEFAULT_WINDOW_HEIGHT;
}
// Create the compositor and shell surface.
if (!(m_surface = wl_compositor_create_surface(m_compositor)) ||
!(m_xdg_surface = xdg_wm_base_get_xdg_surface(m_xdg_wm_base, m_surface)) ||
!(m_xdg_toplevel = xdg_surface_get_toplevel(m_xdg_surface)))
{
Log_ErrorPrintf("Failed to create compositor/shell surfaces");
return false;
}
static const xdg_surface_listener shell_surface_listener = {XDGSurfaceConfigure};
xdg_surface_add_listener(m_xdg_surface, &shell_surface_listener, this);
static const xdg_toplevel_listener toplevel_listener = {TopLevelConfigure, TopLevelClose};
xdg_toplevel_add_listener(m_xdg_toplevel, &toplevel_listener, this);
// Create region in the surface to draw into.
m_region = wl_compositor_create_region(m_compositor);
wl_region_add(m_region, 0, 0, window_width, window_height);
wl_surface_set_opaque_region(m_surface, m_region);
wl_surface_commit(m_surface);
// This doesn't seem to have any effect on kwin...
if (has_window_pos)
{
xdg_surface_set_window_geometry(m_xdg_surface, window_x, window_y, window_width, window_height);
}
if (m_decoration_manager)
{
m_toplevel_decoration = zxdg_decoration_manager_v1_get_toplevel_decoration(m_decoration_manager, m_xdg_toplevel);
if (m_toplevel_decoration)
zxdg_toplevel_decoration_v1_set_mode(m_toplevel_decoration, ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE);
}
m_window_info.surface_width = static_cast<u32>(window_width);
m_window_info.surface_height = static_cast<u32>(window_height);
m_window_info.surface_scale = 1.0f;
m_window_info.type = WindowInfo::Type::Wayland;
m_window_info.window_handle = m_surface;
m_window_info.display_connection = m_display;
wl_display_dispatch_pending(m_display);
wl_display_roundtrip(m_display);
return true;
}
bool WaylandNoGUIPlatform::HasPlatformWindow() const
{
return (m_surface != nullptr);
}
void WaylandNoGUIPlatform::DestroyPlatformWindow()
{
m_window_info = {};
if (m_toplevel_decoration)
{
zxdg_toplevel_decoration_v1_destroy(m_toplevel_decoration);
m_toplevel_decoration = {};
}
if (m_xdg_toplevel)
{
xdg_toplevel_destroy(m_xdg_toplevel);
m_xdg_toplevel = {};
}
if (m_xdg_surface)
{
xdg_surface_destroy(m_xdg_surface);
m_xdg_surface = {};
}
if (m_surface)
{
wl_surface_destroy(m_surface);
m_surface = {};
}
wl_display_dispatch_pending(m_display);
wl_display_roundtrip(m_display);
}
std::optional<WindowInfo> WaylandNoGUIPlatform::GetPlatformWindowInfo()
{
if (m_window_info.type == WindowInfo::Type::Wayland)
return m_window_info;
else
return std::nullopt;
}
void WaylandNoGUIPlatform::SetPlatformWindowTitle(std::string title)
{
if (m_xdg_toplevel)
xdg_toplevel_set_title(m_xdg_toplevel, title.c_str());
}
std::optional<u32> WaylandNoGUIPlatform::ConvertHostKeyboardStringToCode(const std::string_view& str)
{
std::unique_lock lock(m_key_map_mutex);
for (const auto& it : m_key_map)
{
if (StringUtil::Strncasecmp(it.second.c_str(), str.data(), str.length()) == 0)
return it.first;
}
return std::nullopt;
}
std::optional<std::string> WaylandNoGUIPlatform::ConvertHostKeyboardCodeToString(u32 code)
{
std::unique_lock lock(m_key_map_mutex);
const auto it = m_key_map.find(static_cast<s32>(code));
return (it != m_key_map.end()) ? std::optional<std::string>(it->second) : std::nullopt;
}
void WaylandNoGUIPlatform::GlobalRegistryHandler(void* data, wl_registry* registry, uint32_t id, const char* interface,
uint32_t version)
{
WaylandNoGUIPlatform* platform = static_cast<WaylandNoGUIPlatform*>(data);
if (std::strcmp(interface, wl_compositor_interface.name) == 0)
{
platform->m_compositor =
static_cast<wl_compositor*>(wl_registry_bind(platform->m_registry, id, &wl_compositor_interface, 1));
}
else if (std::strcmp(interface, xdg_wm_base_interface.name) == 0)
{
platform->m_xdg_wm_base =
static_cast<xdg_wm_base*>(wl_registry_bind(platform->m_registry, id, &xdg_wm_base_interface, 1));
}
else if (std::strcmp(interface, zxdg_decoration_manager_v1_interface.name) == 0)
{
platform->m_decoration_manager = static_cast<zxdg_decoration_manager_v1*>(
wl_registry_bind(platform->m_registry, id, &zxdg_decoration_manager_v1_interface, 1));
}
else if (std::strcmp(interface, wl_seat_interface.name) == 0)
{
static const wl_seat_listener seat_listener = {&WaylandNoGUIPlatform::SeatCapabilities};
platform->m_wl_seat = static_cast<wl_seat*>(wl_registry_bind(registry, id, &wl_seat_interface, 1));
wl_seat_add_listener(platform->m_wl_seat, &seat_listener, platform);
}
}
void WaylandNoGUIPlatform::GlobalRegistryRemover(void* data, wl_registry* registry, uint32_t id) {}
void WaylandNoGUIPlatform::XDGWMBasePing(void* data, struct xdg_wm_base* xdg_wm_base, uint32_t serial)
{
xdg_wm_base_pong(xdg_wm_base, serial);
}
void WaylandNoGUIPlatform::XDGSurfaceConfigure(void* data, struct xdg_surface* xdg_surface, uint32_t serial)
{
xdg_surface_ack_configure(xdg_surface, serial);
}
void WaylandNoGUIPlatform::TopLevelConfigure(void* data, struct xdg_toplevel* xdg_toplevel, int32_t width,
int32_t height, struct wl_array* states)
{
// If this is zero, it's asking us to set the size.
if (width == 0 || height == 0)
return;
WaylandNoGUIPlatform* platform = static_cast<WaylandNoGUIPlatform*>(data);
platform->m_window_info.surface_width = width;
platform->m_window_info.surface_height = height;
NoGUIHost::ProcessPlatformWindowResize(width, height, platform->m_window_info.surface_scale);
}
void WaylandNoGUIPlatform::TopLevelClose(void* data, struct xdg_toplevel* xdg_toplevel)
{
Host::RunOnCPUThread([]() { Host::RequestExit(false); });
}
void WaylandNoGUIPlatform::SeatCapabilities(void* data, wl_seat* seat, uint32_t capabilities)
{
WaylandNoGUIPlatform* platform = static_cast<WaylandNoGUIPlatform*>(data);
if (capabilities & WL_SEAT_CAPABILITY_KEYBOARD)
{
static const wl_keyboard_listener keyboard_listener = {
&WaylandNoGUIPlatform::KeyboardKeymap, &WaylandNoGUIPlatform::KeyboardEnter, &WaylandNoGUIPlatform::KeyboardLeave,
&WaylandNoGUIPlatform::KeyboardKey, &WaylandNoGUIPlatform::KeyboardModifiers};
platform->m_wl_keyboard = wl_seat_get_keyboard(seat);
wl_keyboard_add_listener(platform->m_wl_keyboard, &keyboard_listener, platform);
}
if (capabilities & WL_SEAT_CAPABILITY_POINTER)
{
static const wl_pointer_listener pointer_listener = {
&WaylandNoGUIPlatform::PointerEnter, &WaylandNoGUIPlatform::PointerLeave, &WaylandNoGUIPlatform::PointerMotion,
&WaylandNoGUIPlatform::PointerButton, &WaylandNoGUIPlatform::PointerAxis};
platform->m_wl_pointer = wl_seat_get_pointer(seat);
wl_pointer_add_listener(platform->m_wl_pointer, &pointer_listener, platform);
}
}
void WaylandNoGUIPlatform::KeyboardKeymap(void* data, wl_keyboard* keyboard, uint32_t format, int32_t fd, uint32_t size)
{
WaylandNoGUIPlatform* platform = static_cast<WaylandNoGUIPlatform*>(data);
char* keymap_string = static_cast<char*>(mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0));
if (platform->m_xkb_keymap)
xkb_keymap_unref(platform->m_xkb_keymap);
platform->m_xkb_keymap = xkb_keymap_new_from_string(platform->m_xkb_context, keymap_string, XKB_KEYMAP_FORMAT_TEXT_V1,
XKB_KEYMAP_COMPILE_NO_FLAGS);
munmap(keymap_string, size);
close(fd);
if (platform->m_xkb_state)
xkb_state_unref(platform->m_xkb_state);
platform->m_xkb_state = xkb_state_new(platform->m_xkb_keymap);
platform->InitializeKeyMap();
}
void WaylandNoGUIPlatform::InitializeKeyMap()
{
m_key_map.clear();
Log_VerbosePrintf("Init keymap");
const xkb_keycode_t min_keycode = xkb_keymap_min_keycode(m_xkb_keymap);
const xkb_keycode_t max_keycode = xkb_keymap_max_keycode(m_xkb_keymap);
DebugAssert(max_keycode >= min_keycode);
for (xkb_keycode_t keycode = min_keycode; keycode <= max_keycode; keycode++)
{
const xkb_layout_index_t num_layouts = xkb_keymap_num_layouts_for_key(m_xkb_keymap, keycode);
if (num_layouts == 0)
continue;
// Take the first layout which we find a valid keysym for.
bool found_keysym = false;
for (xkb_layout_index_t layout = 0; layout < num_layouts && !found_keysym; layout++)
{
const xkb_level_index_t num_levels = xkb_keymap_num_levels_for_key(m_xkb_keymap, keycode, layout);
if (num_levels == 0)
continue;
// Take the first level which we find a valid keysym for.
for (xkb_level_index_t level = 0; level < num_levels; level++)
{
const xkb_keysym_t* keysyms;
const int num_syms = xkb_keymap_key_get_syms_by_level(m_xkb_keymap, keycode, layout, level, &keysyms);
if (num_syms == 0)
continue;
// Just take the first. Should only be one in most cases anyway.
const xkb_keysym_t keysym = xkb_keysym_to_upper(keysyms[0]);
char keysym_name_buf[64];
if (xkb_keysym_get_name(keysym, keysym_name_buf, sizeof(keysym_name_buf)) <= 0)
continue;
m_key_map.emplace(static_cast<s32>(keycode), keysym_name_buf);
found_keysym = false;
break;
}
}
}
}
void WaylandNoGUIPlatform::KeyboardEnter(void* data, wl_keyboard* keyboard, uint32_t serial, wl_surface* surface,
wl_array* keys)
{
}
void WaylandNoGUIPlatform::KeyboardLeave(void* data, wl_keyboard* keyboard, uint32_t serial, wl_surface* surface) {}
void WaylandNoGUIPlatform::KeyboardKey(void* data, wl_keyboard* keyboard, uint32_t serial, uint32_t time, uint32_t key,
uint32_t state)
{
const xkb_keycode_t keycode = static_cast<xkb_keycode_t>(key + 8);
const bool pressed = (state == WL_KEYBOARD_KEY_STATE_PRESSED);
NoGUIHost::ProcessPlatformKeyEvent(static_cast<s32>(keycode), pressed);
}
void WaylandNoGUIPlatform::KeyboardModifiers(void* data, wl_keyboard* keyboard, uint32_t serial,
uint32_t mods_depressed, uint32_t mods_latched, uint32_t mods_locked,
uint32_t group)
{
WaylandNoGUIPlatform* platform = static_cast<WaylandNoGUIPlatform*>(data);
xkb_state_update_mask(platform->m_xkb_state, mods_depressed, mods_latched, mods_locked, 0, 0, group);
}
void WaylandNoGUIPlatform::PointerEnter(void* data, wl_pointer* pointer, uint32_t serial, wl_surface* surface,
wl_fixed_t surface_x, wl_fixed_t surface_y)
{
}
void WaylandNoGUIPlatform::PointerLeave(void* data, wl_pointer* pointer, uint32_t serial, wl_surface* surface) {}
void WaylandNoGUIPlatform::PointerMotion(void* data, wl_pointer* pointer, uint32_t time, wl_fixed_t x, wl_fixed_t y)
{
const float pos_x = static_cast<float>(wl_fixed_to_double(x));
const float pos_y = static_cast<float>(wl_fixed_to_double(y));
NoGUIHost::ProcessPlatformMouseMoveEvent(static_cast<int>(pos_x), static_cast<int>(pos_y));
}
void WaylandNoGUIPlatform::PointerButton(void* data, wl_pointer* pointer, uint32_t serial, uint32_t time,
uint32_t button, uint32_t state)
{
if (button < BTN_MOUSE || (button - BTN_MOUSE) >= 32)
return;
const s32 button_index = (button - BTN_MOUSE);
const bool button_pressed = (state == WL_POINTER_BUTTON_STATE_PRESSED);
NoGUIHost::ProcessPlatformMouseButtonEvent(button_index, button_pressed);
}
void WaylandNoGUIPlatform::PointerAxis(void* data, wl_pointer* pointer, uint32_t time, uint32_t axis, wl_fixed_t value)
{
const float x = (axis == 1) ? std::clamp(static_cast<float>(wl_fixed_to_double(value)), -1.0f, 1.0f) : 0.0f;
const float y = (axis == 0) ? std::clamp(static_cast<float>(-wl_fixed_to_double(value)), -1.0f, 1.0f) : 0.0f;
NoGUIHost::ProcessPlatformMouseWheelEvent(x, y);
}
void WaylandNoGUIPlatform::RunMessageLoop()
{
while (m_message_loop_running.load(std::memory_order_acquire))
{
wl_display_dispatch_pending(m_display);
{
std::unique_lock lock(m_callback_queue_mutex);
while (!m_callback_queue.empty())
{
std::function<void()> func = std::move(m_callback_queue.front());
m_callback_queue.pop_front();
lock.unlock();
func();
lock.lock();
}
}
// TODO: Make this suck less.
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}
void WaylandNoGUIPlatform::ExecuteInMessageLoop(std::function<void()> func)
{
std::unique_lock lock(m_callback_queue_mutex);
m_callback_queue.push_back(std::move(func));
}
void WaylandNoGUIPlatform::QuitMessageLoop()
{
m_message_loop_running.store(false, std::memory_order_release);
}
void WaylandNoGUIPlatform::SetFullscreen(bool enabled)
{
// how the heck can we do this?
}
bool WaylandNoGUIPlatform::RequestRenderWindowSize(s32 new_window_width, s32 new_window_height)
{
return false;
}
bool WaylandNoGUIPlatform::OpenURL(const std::string_view& url)
{
Log_ErrorPrintf("WaylandNoGUIPlatform::OpenURL() not implemented: %.*s", static_cast<int>(url.size()), url.data());
return false;
}
bool WaylandNoGUIPlatform::CopyTextToClipboard(const std::string_view& text)
{
Log_ErrorPrintf("WaylandNoGUIPlatform::CopyTextToClipboard() not implemented: %.*s", static_cast<int>(text.size()), text.data());
return false;
}
std::unique_ptr<NoGUIPlatform> NoGUIPlatform::CreateWaylandPlatform()
{
std::unique_ptr<WaylandNoGUIPlatform> ret = std::unique_ptr<WaylandNoGUIPlatform>(new WaylandNoGUIPlatform());
if (!ret->Initialize())
return {};
return ret;
}