diff --git a/CMakeLists.txt b/CMakeLists.txt index 0da6405ed..f0c100437 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,6 +21,7 @@ if(NOT ANDROID) option(BUILD_SDL_FRONTEND "Build the SDL frontend" ON) option(BUILD_QT_FRONTEND "Build the Qt frontend" ON) option(BUILD_LIBRETRO_CORE "Build a libretro core" OFF) + option(BUILD_GO2_FRONTEND "Build the ODROID-GO Advance frontend" OFF) option(ENABLE_DISCORD_PRESENCE "Build with Discord Rich Presence support" ON) option(USE_SDL2 "Link with SDL2 for controller support" ON) endif() diff --git a/dep/CMakeLists.txt b/dep/CMakeLists.txt index 068ac3185..07a036ae8 100644 --- a/dep/CMakeLists.txt +++ b/dep/CMakeLists.txt @@ -35,3 +35,6 @@ if(${CPU_ARCH} STREQUAL "aarch64") add_subdirectory(vixl) endif() +if(BUILD_OGA_FRONTEND) + add_subdirectory(libgo2) +endif() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e91fb70a8..52c46e25b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -13,7 +13,7 @@ if(NOT BUILD_LIBRETRO_CORE) endif() endif() -if(ANDROID OR BUILD_SDL_FRONTEND OR BUILD_QT_FRONTEND OR BUILD_LIBRETRO_CORE) +if(ANDROID OR BUILD_SDL_FRONTEND OR BUILD_QT_FRONTEND OR BUILD_LIBRETRO_CORE OR BUILD_GO2_FRONTEND) add_subdirectory(frontend-common) endif() @@ -29,3 +29,6 @@ if(BUILD_LIBRETRO_CORE) add_subdirectory(duckstation-libretro) endif() +if(BUILD_GO2_FRONTEND) + add_subdirectory(duckstation-go2) +endif() diff --git a/src/duckstation-go2/CMakeLists.txt b/src/duckstation-go2/CMakeLists.txt new file mode 100644 index 000000000..054795bea --- /dev/null +++ b/src/duckstation-go2/CMakeLists.txt @@ -0,0 +1,16 @@ +find_package(EGL REQUIRED) + +add_executable(duckstation-go2 + go2_controller_interface.cpp + go2_controller_interface.h + go2_host_display.cpp + go2_host_display.h + go2_host_interface.cpp + go2_host_interface.h + go2_opengl_host_display.cpp + go2_opengl_host_display.h + main.cpp +) + +target_link_libraries(duckstation-go2 PRIVATE core common imgui glad frontend-common scmversion vulkan-loader go2 EGL::EGL) + diff --git a/src/duckstation-go2/go2_controller_interface.cpp b/src/duckstation-go2/go2_controller_interface.cpp new file mode 100644 index 000000000..21b87bccd --- /dev/null +++ b/src/duckstation-go2/go2_controller_interface.cpp @@ -0,0 +1,222 @@ +#include "go2_controller_interface.h" +#include "common/assert.h" +#include "common/file_system.h" +#include "common/log.h" +#include "core/controller.h" +#include "core/host_interface.h" +#include "core/system.h" +#include +Log_SetChannel(Go2ControllerInterface); + +Go2ControllerInterface::Go2ControllerInterface() = default; + +Go2ControllerInterface::~Go2ControllerInterface() +{ + if (m_input_state) + go2_input_state_destroy(m_input_state); + if (m_input) + go2_input_destroy(m_input); +} + +ControllerInterface::Backend Go2ControllerInterface::GetBackend() const +{ + return ControllerInterface::Backend::None; +} + +bool Go2ControllerInterface::Initialize(CommonHostInterface* host_interface) +{ + m_input = go2_input_create(); + m_input_state = go2_input_state_create(); + if (!m_input || !m_input_state) + return false; + + if (!ControllerInterface::Initialize(host_interface)) + return false; + + return true; +} + +void Go2ControllerInterface::Shutdown() +{ + ControllerInterface::Shutdown(); +} + +void Go2ControllerInterface::PollEvents() +{ + go2_input_state_read(m_input, m_input_state); + CheckForStateChanges(); +} + +void Go2ControllerInterface::CheckForStateChanges() +{ + for (u32 i = 0; i < NUM_BUTTONS; i++) + { + const bool new_state = go2_input_state_button_get(m_input_state, static_cast(i)) == ButtonState_Pressed; + if (m_last_button_state[i] == new_state) + continue; + + HandleButtonEvent(i, new_state); + m_last_button_state[i] = new_state; + } + + go2_thumb_t thumb = go2_input_state_thumbstick_get(m_input_state, Go2InputThumbstick_Left); + if (thumb.x != m_last_axis_state[0]) + { + HandleAxisEvent(Axis::X, thumb.x); + m_last_axis_state[0] = thumb.x; + } + if (thumb.y != m_last_axis_state[1]) + { + HandleAxisEvent(Axis::Y, thumb.y); + m_last_axis_state[1] = thumb.y; + } +} + +void Go2ControllerInterface::ClearBindings() +{ + for (AxisCallback& ac : m_axis_mapping) + ac = {}; + for (ButtonCallback& bc : m_button_mapping) + bc = {}; +} + +bool Go2ControllerInterface::BindControllerAxis(int controller_index, int axis_number, AxisCallback callback) +{ + if (controller_index != 0) + return false; + + if (axis_number < 0 || axis_number >= NUM_AXISES) + return false; + + m_axis_mapping[axis_number] = std::move(callback); + return true; +} + +bool Go2ControllerInterface::BindControllerButton(int controller_index, int button_number, ButtonCallback callback) +{ + if (controller_index != 0) + return false; + + if (button_number < 0 || button_number >= NUM_BUTTONS) + return false; + + m_button_mapping[button_number] = std::move(callback); + return true; +} + +bool Go2ControllerInterface::BindControllerAxisToButton(int controller_index, int axis_number, bool direction, + ButtonCallback callback) +{ + if (controller_index != 0) + return false; + + if (axis_number < 0 || axis_number >= NUM_AXISES) + return false; + + m_axis_button_mapping[axis_number][BoolToUInt8(direction)] = std::move(callback); + return true; +} + +bool Go2ControllerInterface::BindControllerButtonToAxis(int controller_index, int button_number, + AxisCallback callback) +{ + if (controller_index != 0) + return false; + + if (button_number < 0 || button_number >= NUM_BUTTONS) + return false; + + m_button_axis_mapping[button_number] = std::move(callback); + return true; +} + +bool Go2ControllerInterface::HandleAxisEvent(Axis axis, float value) +{ + Log_DevPrintf("axis %u %f", static_cast(axis), value); + if (DoEventHook(Hook::Type::Axis, 0, static_cast(axis), value)) + return true; + + const AxisCallback& cb = m_axis_mapping[static_cast(axis)]; + if (cb) + { + // Apply axis scaling only when controller axis is mapped to an axis + cb(std::clamp(m_axis_scale * value, -1.0f, 1.0f)); + return true; + } + + // set the other direction to false so large movements don't leave the opposite on + const bool outside_deadzone = (std::abs(value) >= m_deadzone); + const bool positive = (value >= 0.0f); + const ButtonCallback& other_button_cb = + m_axis_button_mapping[static_cast(axis)][BoolToUInt8(!positive)]; + const ButtonCallback& button_cb = + m_axis_button_mapping[static_cast(axis)][BoolToUInt8(positive)]; + if (button_cb) + { + button_cb(outside_deadzone); + if (other_button_cb) + other_button_cb(false); + return true; + } + else if (other_button_cb) + { + other_button_cb(false); + return true; + } + else + { + return false; + } +} + +bool Go2ControllerInterface::HandleButtonEvent(u32 button, bool pressed) +{ + Log_DevPrintf("button %u %s", button, pressed ? "pressed" : "released"); + if (DoEventHook(Hook::Type::Button, 0, button, pressed ? 1.0f : 0.0f)) + return true; + + const ButtonCallback& cb = m_button_mapping[button]; + if (cb) + { + cb(pressed); + return true; + } + + // Assume a half-axis, i.e. in 0..1 range + const AxisCallback& axis_cb = m_button_axis_mapping[button]; + if (axis_cb) + { + axis_cb(pressed ? 1.0f : 0.0f); + } + return true; +} + +u32 Go2ControllerInterface::GetControllerRumbleMotorCount(int controller_index) +{ + return 0; +} + +void Go2ControllerInterface::SetControllerRumbleStrength(int controller_index, const float* strengths, + u32 num_motors) +{ +} + +bool Go2ControllerInterface::SetControllerAxisScale(int controller_index, float scale /* = 1.00f */) +{ + if (controller_index != 0) + return false; + + m_axis_scale = std::clamp(std::abs(scale), 0.01f, 1.50f); + Log_InfoPrintf("Controller %d axis scale set to %f", controller_index, m_axis_scale); + return true; +} + +bool Go2ControllerInterface::SetControllerDeadzone(int controller_index, float size /* = 0.25f */) +{ + if (controller_index != 0) + return false; + + m_deadzone = std::clamp(std::abs(size), 0.01f, 0.99f); + Log_InfoPrintf("Controller %d deadzone size set to %f", controller_index, m_deadzone); + return true; +} diff --git a/src/duckstation-go2/go2_controller_interface.h b/src/duckstation-go2/go2_controller_interface.h new file mode 100644 index 000000000..2b3659eed --- /dev/null +++ b/src/duckstation-go2/go2_controller_interface.h @@ -0,0 +1,72 @@ +#pragma once +#include "frontend-common/controller_interface.h" +#include "core/types.h" +#include +#include +#include +#include +#include + +class Go2ControllerInterface final : public ControllerInterface +{ +public: + Go2ControllerInterface(); + ~Go2ControllerInterface() override; + + Backend GetBackend() const override; + bool Initialize(CommonHostInterface* host_interface) override; + void Shutdown() override; + + // Removes all bindings. Call before setting new bindings. + void ClearBindings() override; + + // Binding to events. If a binding for this axis/button already exists, returns false. + bool BindControllerAxis(int controller_index, int axis_number, AxisCallback callback) override; + bool BindControllerButton(int controller_index, int button_number, ButtonCallback callback) override; + bool BindControllerAxisToButton(int controller_index, int axis_number, bool direction, + ButtonCallback callback) override; + bool BindControllerButtonToAxis(int controller_index, int button_number, AxisCallback callback) override; + + // Changing rumble strength. + u32 GetControllerRumbleMotorCount(int controller_index) override; + void SetControllerRumbleStrength(int controller_index, const float* strengths, u32 num_motors) override; + + // Set scaling that will be applied on axis-to-axis mappings + bool SetControllerAxisScale(int controller_index, float scale = 1.00f) override; + + // Set deadzone that will be applied on axis-to-button mappings + bool SetControllerDeadzone(int controller_index, float size = 0.25f) override; + + void PollEvents() override; + +private: + enum : u32 + { + NUM_BUTTONS = Go2InputButton_TriggerRight + 1, + NUM_AXISES = 2 + }; + enum class Axis : u32 + { + X, + Y + }; + + void CheckForStateChanges(); + bool HandleAxisEvent(Axis axis, float value); + bool HandleButtonEvent(u32 button, bool pressed); + + go2_input_t* m_input = nullptr; + go2_input_state_t* m_input_state = nullptr; + + std::array m_last_button_state = {}; + std::array m_last_axis_state = {}; + + // Scaling value of 1.30f to 1.40f recommended when using recent controllers + float m_axis_scale = 1.00f; + float m_deadzone = 0.25f; + + std::array m_axis_mapping; + std::array m_button_mapping; + std::array, NUM_AXISES> m_axis_button_mapping; + std::array m_button_axis_mapping; +}; diff --git a/src/duckstation-go2/go2_host_display.cpp b/src/duckstation-go2/go2_host_display.cpp new file mode 100644 index 000000000..bc64e676f --- /dev/null +++ b/src/duckstation-go2/go2_host_display.cpp @@ -0,0 +1,221 @@ +#include "go2_host_display.h" +#include "common/assert.h" +#include "common/log.h" +#include "imgui.h" +#include +#include +#include +Log_SetChannel(Go2HostDisplay); + +Go2HostDisplay::Go2HostDisplay() = default; + +Go2HostDisplay::~Go2HostDisplay() +{ + Assert(!m_display && !m_surface && !m_presenter); +} + +HostDisplay::RenderAPI Go2HostDisplay::GetRenderAPI() const +{ + return RenderAPI::None; +} + +void* Go2HostDisplay::GetRenderDevice() const +{ + return nullptr; +} + +void* Go2HostDisplay::GetRenderContext() const +{ + return nullptr; +} + +bool Go2HostDisplay::HasRenderDevice() const +{ + return true; +} + +bool Go2HostDisplay::HasRenderSurface() const +{ + return true; +} + +bool Go2HostDisplay::MakeRenderContextCurrent() +{ + return true; +} + +bool Go2HostDisplay::DoneRenderContextCurrent() +{ + return true; +} + +void Go2HostDisplay::DestroyRenderSurface() +{ + // noop +} + +bool Go2HostDisplay::ChangeRenderWindow(const WindowInfo& wi) +{ + m_window_info = wi; + return true; +} + +bool Go2HostDisplay::CreateResources() +{ + return true; +} + +void Go2HostDisplay::DestroyResources() +{ + // noop +} + +bool Go2HostDisplay::SetPostProcessingChain(const std::string_view& config) +{ + return false; +} + +void Go2HostDisplay::ResizeRenderWindow(s32 new_window_width, s32 new_window_height) +{ + m_window_info.surface_width = static_cast(new_window_width); + m_window_info.surface_height = static_cast(new_window_height); +} + +std::unique_ptr Go2HostDisplay::CreateTexture(u32 width, u32 height, const void* data, + u32 data_stride, bool dynamic /*= false*/) +{ + return nullptr; +} + +void Go2HostDisplay::UpdateTexture(HostDisplayTexture* texture, u32 x, u32 y, u32 width, u32 height, const void* data, + u32 data_stride) +{ + // noop +} + +bool Go2HostDisplay::DownloadTexture(const void* texture_handle, u32 x, u32 y, u32 width, u32 height, void* out_data, + u32 out_data_stride) +{ + return false; +} + +void Go2HostDisplay::SetVSync(bool enabled) +{ + // noop +} + +bool Go2HostDisplay::CreateRenderDevice(const WindowInfo& wi, std::string_view adapter_name, bool debug_device) +{ + m_display = go2_display_create(); + if (!m_display) + return false; + + m_presenter = go2_presenter_create(m_display, DRM_FORMAT_RGB565, 0xff000000); + if (!m_presenter) + return false; + + m_window_info = wi; + m_window_info.surface_width = go2_display_width_get(m_display); + m_window_info.surface_height = go2_display_height_get(m_display); + + ImGui::GetIO().DisplaySize.x = static_cast(m_window_info.surface_width); + ImGui::GetIO().DisplaySize.y = static_cast(m_window_info.surface_height); + unsigned char* pixels; + int width, height; + ImGui::GetIO().Fonts->GetTexDataAsRGBA32(&pixels, &width, &height); + + return true; +} + +bool Go2HostDisplay::InitializeRenderDevice(std::string_view shader_cache_directory, bool debug_device) +{ + return true; +} + +void Go2HostDisplay::DestroyRenderDevice() +{ + if (m_surface) + { + go2_surface_destroy(m_surface); + m_surface = nullptr; + } + + if (m_presenter) + { + go2_presenter_destroy(m_presenter); + m_presenter = nullptr; + } + + if (m_display) + { + go2_display_destroy(m_display); + m_display = nullptr; + } +} + +static constexpr std::array(HostDisplayPixelFormat::Count)> s_display_pixel_format_mapping = { + {DRM_FORMAT_INVALID, DRM_FORMAT_RGBA8888, DRM_FORMAT_INVALID, DRM_FORMAT_RGB565, DRM_FORMAT_RGBA5551}}; + +bool Go2HostDisplay::CheckSurface(u32 width, u32 height, HostDisplayPixelFormat format) +{ + if (width <= m_surface_width && height <= m_surface_height && format == m_surface_format) + return true; + + if (m_surface) + go2_surface_destroy(m_surface); + + m_surface = go2_surface_create(m_display, width, height, s_display_pixel_format_mapping[static_cast(format)]); + if (!m_surface) + Panic("Failed to create surface"); + + m_surface_width = width; + m_surface_height = height; + m_surface_format = format; + return false; +} + +bool Go2HostDisplay::SupportsDisplayPixelFormat(HostDisplayPixelFormat format) const +{ + return (s_display_pixel_format_mapping[static_cast(format)] != DRM_FORMAT_INVALID); +} + +bool Go2HostDisplay::BeginSetDisplayPixels(HostDisplayPixelFormat format, u32 width, u32 height, void** out_buffer, + u32* out_pitch) +{ + if (!CheckSurface(width, height, format)) + return false; + + void* map = go2_surface_map(m_surface); + if (!map) + return false; + + *out_buffer = map; + *out_pitch = static_cast(go2_surface_stride_get(m_surface)); + SetDisplayTexture(m_surface, format, m_surface_width, m_surface_height, 0, 0, width, height); + return true; +} + +void Go2HostDisplay::EndSetDisplayPixels() +{ + go2_surface_unmap(m_surface); +} + +bool Go2HostDisplay::Render() +{ + ImGui::Render(); + + if (HasDisplayTexture()) + { + s32 left, top, width, height, left_padding, top_padding; + CalculateDrawRect(m_window_info.surface_height, m_window_info.surface_width, + static_cast(m_window_info.surface_height) / + static_cast(m_window_info.surface_width), + &left, &top, &width, &height, &left_padding, &top_padding, nullptr, nullptr, true); + + go2_presenter_post(m_presenter, static_cast(m_display_texture_handle), m_display_texture_view_x, + m_display_texture_view_y, m_display_texture_view_width, m_display_texture_view_height, + top + top_padding, left + left_padding, height, width, GO2_ROTATION_DEGREES_270); + } + + return true; +} diff --git a/src/duckstation-go2/go2_host_display.h b/src/duckstation-go2/go2_host_display.h new file mode 100644 index 000000000..fd649c3cf --- /dev/null +++ b/src/duckstation-go2/go2_host_display.h @@ -0,0 +1,60 @@ +#pragma once +#include "core/host_display.h" +#include +#include +#include + +class Go2HostDisplay final : public HostDisplay +{ +public: + Go2HostDisplay(); + ~Go2HostDisplay(); + + RenderAPI GetRenderAPI() const override; + void* GetRenderDevice() const override; + void* GetRenderContext() const override; + + bool HasRenderDevice() const override; + bool HasRenderSurface() const override; + + bool CreateRenderDevice(const WindowInfo& wi, std::string_view adapter_name, bool debug_device) override; + bool InitializeRenderDevice(std::string_view shader_cache_directory, bool debug_device) override; + void DestroyRenderDevice() override; + + bool MakeRenderContextCurrent() override; + bool DoneRenderContextCurrent() override; + void DestroyRenderSurface() override; + + bool ChangeRenderWindow(const WindowInfo& wi) override; + void ResizeRenderWindow(s32 new_window_width, s32 new_window_height) override; + bool CreateResources() override; + void DestroyResources() override; + bool SetPostProcessingChain(const std::string_view& config) override; + + std::unique_ptr CreateTexture(u32 width, u32 height, const void* data, u32 data_stride, + bool dynamic = false) override; + void UpdateTexture(HostDisplayTexture* texture, u32 x, u32 y, u32 width, u32 height, const void* data, + u32 data_stride) override; + bool DownloadTexture(const void* texture_handle, u32 x, u32 y, u32 width, u32 height, void* out_data, + u32 out_data_stride) override; + + bool SupportsDisplayPixelFormat(HostDisplayPixelFormat format) const override; + bool BeginSetDisplayPixels(HostDisplayPixelFormat format, u32 width, u32 height, void** out_buffer, + u32* out_pitch) override; + void EndSetDisplayPixels() override; + + void SetVSync(bool enabled) override; + + bool Render() override; + +private: + bool CheckSurface(u32 width, u32 height, HostDisplayPixelFormat format); + + go2_display_t* m_display = nullptr; + go2_surface_t* m_surface = nullptr; + go2_presenter_t* m_presenter = nullptr; + + u32 m_surface_width = 0; + u32 m_surface_height = 0; + HostDisplayPixelFormat m_surface_format = HostDisplayPixelFormat::Unknown; +}; diff --git a/src/duckstation-go2/go2_host_interface.cpp b/src/duckstation-go2/go2_host_interface.cpp new file mode 100644 index 000000000..fbe3f132a --- /dev/null +++ b/src/duckstation-go2/go2_host_interface.cpp @@ -0,0 +1,289 @@ +#include "go2_host_interface.h" +#include "common/assert.h" +#include "common/byte_stream.h" +#include "common/file_system.h" +#include "common/log.h" +#include "common/string_util.h" +#include "core/controller.h" +#include "core/gpu.h" +#include "core/host_display.h" +#include "core/system.h" +#include "frontend-common/icon.h" +#include "frontend-common/imgui_styles.h" +#include "frontend-common/ini_settings_interface.h" +#include "go2_controller_interface.h" +#include "go2_host_display.h" +#include "go2_opengl_host_display.h" +#include "scmversion/scmversion.h" +#include +#include +#include +#include +Log_SetChannel(Go2HostInterface); + +Go2HostInterface::Go2HostInterface() = default; + +Go2HostInterface::~Go2HostInterface() = default; + +const char* Go2HostInterface::GetFrontendName() const +{ + return "DuckStation ODROID-Go Advance Frontend"; +} + +ALWAYS_INLINE static TinyString GetWindowTitle() +{ + return TinyString::FromFormat("DuckStation %s (%s)", g_scm_tag_str, g_scm_branch_str); +} + +void Go2HostInterface::CreateImGuiContext() +{ + const float framebuffer_scale = 1.0f; + + ImGui::CreateContext(); + ImGui::GetIO().IniFilename = nullptr; + // ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; + ImGui::GetIO().DisplayFramebufferScale.x = framebuffer_scale; + ImGui::GetIO().DisplayFramebufferScale.y = framebuffer_scale; + ImGui::GetStyle().ScaleAllSizes(framebuffer_scale); + + ImGui::StyleColorsDarker(); + ImGui::AddRobotoRegularFont(15.0f * framebuffer_scale); +} + +bool Go2HostInterface::AcquireHostDisplay() +{ + WindowInfo wi; + + CreateImGuiContext(); + + if (g_settings.gpu_renderer == GPURenderer::HardwareOpenGL) + m_display = std::make_unique(); + else + m_display = std::make_unique(); + + if (!m_display->CreateRenderDevice(wi, g_settings.gpu_adapter, g_settings.gpu_use_debug_device) || + !m_display->InitializeRenderDevice(GetShaderCacheBasePath(), g_settings.gpu_use_debug_device)) + { + ReportError("Failed to create/initialize display render device"); + m_display.reset(); + ImGui::DestroyContext(); + return false; + } + + ImGui::NewFrame(); + return true; +} + +void Go2HostInterface::ReleaseHostDisplay() +{ + m_display->DestroyRenderDevice(); + m_display.reset(); + + ImGui::DestroyContext(); +} + +std::optional Go2HostInterface::GetHostKeyCode(const std::string_view key_code) const +{ + return std::nullopt; +} + +void Go2HostInterface::UpdateInputMap() +{ + CommonHostInterface::UpdateInputMap(*m_settings_interface.get()); +} + +void Go2HostInterface::OnRunningGameChanged() +{ + CommonHostInterface::OnRunningGameChanged(); + + Settings old_settings(std::move(g_settings)); + CommonHostInterface::LoadSettings(*m_settings_interface.get()); + CommonHostInterface::ApplyGameSettings(true); + CommonHostInterface::FixIncompatibleSettings(true); + CheckForSettingsChanges(old_settings); +} + +void Go2HostInterface::OnSystemPerformanceCountersUpdated() +{ + HostInterface::OnSystemPerformanceCountersUpdated(); + + Log_InfoPrintf("FPS: %.2f VPS: %.2f Average: %.2fms Worst: %.2fms", System::GetFPS(), System::GetVPS(), + System::GetAverageFrameTime(), System::GetWorstFrameTime()); +} + +void Go2HostInterface::RequestExit() +{ + Log_ErrorPrintf("TODO"); +} + +void Go2HostInterface::PollAndUpdate() +{ + CommonHostInterface::PollAndUpdate(); + + if (m_controller_interface) + m_controller_interface->PollEvents(); +} + +bool Go2HostInterface::IsFullscreen() const +{ + return true; +} + +bool Go2HostInterface::SetFullscreen(bool enabled) +{ + return false; +} + +std::unique_ptr Go2HostInterface::Create() +{ + return std::make_unique(); +} + +bool Go2HostInterface::Initialize() +{ + if (!CommonHostInterface::Initialize()) + return false; + + // Change to the user directory so that all default/relative paths in the config are after this. + if (!FileSystem::SetWorkingDirectory(m_user_directory.c_str())) + Log_ErrorPrintf("Failed to set working directory to '%s'", m_user_directory.c_str()); + + // process events to pick up controllers before updating input map + UpdateInputMap(); + return true; +} + +void Go2HostInterface::Shutdown() +{ + CommonHostInterface::Shutdown(); +} + +std::string Go2HostInterface::GetStringSettingValue(const char* section, const char* key, + const char* default_value /*= ""*/) +{ + return m_settings_interface->GetStringValue(section, key, default_value); +} + +bool Go2HostInterface::GetBoolSettingValue(const char* section, const char* key, bool default_value /* = false */) +{ + return m_settings_interface->GetBoolValue(section, key, default_value); +} + +int Go2HostInterface::GetIntSettingValue(const char* section, const char* key, int default_value /* = 0 */) +{ + return m_settings_interface->GetIntValue(section, key, default_value); +} + +float Go2HostInterface::GetFloatSettingValue(const char* section, const char* key, float default_value /* = 0.0f */) +{ + return m_settings_interface->GetFloatValue(section, key, default_value); +} + +void Go2HostInterface::LoadSettings() +{ + // Settings need to be loaded prior to creating the window for OpenGL bits. + m_settings_interface = std::make_unique(GetSettingsFileName()); + if (!CommonHostInterface::CheckSettings(*m_settings_interface.get())) + m_settings_interface->Save(); + + CommonHostInterface::LoadSettings(*m_settings_interface.get()); + CommonHostInterface::FixIncompatibleSettings(false); +} + +void Go2HostInterface::SetDefaultSettings(SettingsInterface& si) +{ + CommonHostInterface::SetDefaultSettings(si); + + si.SetBoolValue("Main", "ConfirmPowerOff", false); + si.SetStringValue("GPU", "Renderer", "Software"); + + si.SetStringValue("Controller1", "Type", "AnalogController"); + si.SetBoolValue("Controller1", "AutoEnableAnalog", true); + + si.SetStringValue("Controller1", "ButtonUp", "Controller0/Button0"); + si.SetStringValue("Controller1", "ButtonDown", "Controller0/Button1"); + si.SetStringValue("Controller1", "ButtonLeft", "Controller0/Button2"); + si.SetStringValue("Controller1", "ButtonRight", "Controller0/Button3"); + si.SetStringValue("Controller1", "ButtonSelect", "Controller0/Button10"); + si.SetStringValue("Controller1", "ButtonStart", "Controller0/Button11"); + si.SetStringValue("Controller1", "ButtonTriangle", "Controller0/Button6"); + si.SetStringValue("Controller1", "ButtonCross", "Controller0/Button5"); + si.SetStringValue("Controller1", "ButtonSquare", "Controller0/Button7"); + si.SetStringValue("Controller1", "ButtonCircle", "Controller0/Button4"); + si.SetStringValue("Controller1", "ButtonL1", "Controller0/Button16"); + si.SetStringValue("Controller1", "ButtonL2", "Controller0/Button14"); + si.SetStringValue("Controller1", "ButtonR1", "Controller0/Button17"); + si.SetStringValue("Controller1", "ButtonR2", "Controller0/Button15"); + si.SetStringValue("Controller1", "AxisLeftX", "Controller0/Axis0"); + si.SetStringValue("Controller1", "AxisLeftY", "Controller0/Axis1"); + + si.SetStringValue("Hotkeys", "PowerOff", "Controller0/Button8"); + + si.SetStringValue("Logging", "LogLevel", "Info"); + si.SetBoolValue("Logging", "LogToConsole", true); + + si.SetBoolValue("Display", "ShowOSDMessages", true); + si.SetBoolValue("Display", "ShowFPS", false); + si.SetBoolValue("Display", "ShowVPS", false); + si.SetBoolValue("Display", "ShowSpeed", false); + si.SetBoolValue("Display", "ShowResolution", false); + + si.SetBoolValue("BIOS", "PatchFastBoot", true); + si.SetStringValue("BIOS", "SearchDirectory", "/roms/bios"); +} + +void Go2HostInterface::UpdateControllerInterface() +{ + if (m_controller_interface) + return; + + m_controller_interface = std::make_unique(); + if (!m_controller_interface->Initialize(this)) + { + Log_WarningPrintf("Failed to initialize go2 controller interface"); + m_controller_interface.reset(); + } +} + +void Go2HostInterface::Run() +{ + while (!m_quit_request) + { + PollAndUpdate(); + + if (System::IsRunning()) + { + System::RunFrame(); + UpdateControllerRumble(); + if (m_frame_step_request) + { + m_frame_step_request = false; + PauseSystem(true); + } + } + + // rendering + { + DrawImGuiWindows(); + + m_display->Render(); + ImGui::NewFrame(); + + if (System::IsRunning()) + { + System::UpdatePerformanceCounters(); + + if (m_speed_limiter_enabled) + System::Throttle(); + } + } + } + + // Save state on exit so it can be resumed + if (!System::IsShutdown()) + { + if (g_settings.save_state_on_exit) + SaveResumeSaveState(); + DestroySystem(); + } +} diff --git a/src/duckstation-go2/go2_host_interface.h b/src/duckstation-go2/go2_host_interface.h new file mode 100644 index 000000000..2a3af142a --- /dev/null +++ b/src/duckstation-go2/go2_host_interface.h @@ -0,0 +1,64 @@ +#pragma once +#include "common/gl/program.h" +#include "common/gl/texture.h" +#include "core/host_display.h" +#include "core/host_interface.h" +#include "frontend-common/common_host_interface.h" +#include +#include +#include +#include +#include +#include + +class AudioStream; + +class INISettingsInterface; + +class Go2HostInterface final : public CommonHostInterface +{ +public: + Go2HostInterface(); + ~Go2HostInterface(); + + static std::unique_ptr Create(); + + const char* GetFrontendName() const override; + + bool Initialize() override; + void Shutdown() override; + + std::string GetStringSettingValue(const char* section, const char* key, const char* default_value = "") override; + bool GetBoolSettingValue(const char* section, const char* key, bool default_value = false) override; + int GetIntSettingValue(const char* section, const char* key, int default_value = 0) override; + float GetFloatSettingValue(const char* section, const char* key, float default_value = 0.0f) override; + + void Run(); + +protected: + void LoadSettings() override; + void SetDefaultSettings(SettingsInterface &si) override; + void UpdateControllerInterface() override; + + bool AcquireHostDisplay() override; + void ReleaseHostDisplay() override; + + void OnRunningGameChanged() override; + void OnSystemPerformanceCountersUpdated() override; + + void RequestExit() override; + void PollAndUpdate() override; + + std::optional GetHostKeyCode(const std::string_view key_code) const override; + void UpdateInputMap() override; + +private: + void CreateImGuiContext(); + + bool IsFullscreen() const override; + bool SetFullscreen(bool enabled) override; + + std::unique_ptr m_settings_interface; + + bool m_quit_request = false; +}; diff --git a/src/duckstation-go2/go2_opengl_host_display.cpp b/src/duckstation-go2/go2_opengl_host_display.cpp new file mode 100644 index 000000000..e8bb7252b --- /dev/null +++ b/src/duckstation-go2/go2_opengl_host_display.cpp @@ -0,0 +1,142 @@ +#include "common/assert.h" +#include "common/log.h" +#include "go2_opengl_host_display.h" +#include "imgui.h" +#include +#include +#include +#include + +// Must come after imgui.h +#include "frontend-common/imgui_impl_opengl3.h" + +Log_SetChannel(Go2OpenGLHostDisplay); + +Go2OpenGLHostDisplay::Go2OpenGLHostDisplay() = default; + +Go2OpenGLHostDisplay::~Go2OpenGLHostDisplay() +{ + Assert(!m_display && !m_context && !m_presenter); +} + +HostDisplay::RenderAPI Go2OpenGLHostDisplay::GetRenderAPI() const +{ + return HostDisplay::RenderAPI::OpenGLES; +} + +void Go2OpenGLHostDisplay::SetVSync(bool enabled) +{ + // Window framebuffer has to be bound to call SetSwapInterval. + GLint current_fbo = 0; + glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, ¤t_fbo); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + eglSwapInterval(go2_context_egldisplay_get(m_context), enabled ? 1 : 0); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, current_fbo); +} + +bool Go2OpenGLHostDisplay::CreateRenderDevice(const WindowInfo& wi, std::string_view adapter_name, bool debug_device) +{ + m_display = go2_display_create(); + if (!m_display) + return false; + + m_presenter = go2_presenter_create(m_display, DRM_FORMAT_RGB565, 0xff080808); + if (!m_presenter) + return false; + + m_window_info = wi; + m_window_info.surface_width = go2_display_height_get(m_display); + m_window_info.surface_height = go2_display_width_get(m_display); + + static constexpr std::array, 4> versions_to_try = {{{3, 2}, {3, 1}, {3, 0}, {2, 0}}}; + + go2_context_attributes_t attributes = {}; + attributes.red_bits = 5; + attributes.green_bits = 6; + attributes.blue_bits = 5; + attributes.alpha_bits = 0; + attributes.depth_bits = 0; + attributes.stencil_bits = 0; + + for (const auto [major, minor] : versions_to_try) + { + attributes.major = major; + attributes.minor = minor; + + Log_InfoPrintf("Trying a OpenGL ES %d.%d context", major, minor); + m_context = go2_context_create(m_display, m_window_info.surface_width, m_window_info.surface_height, &attributes); + if (m_context) + { + Log_InfoPrintf("Got a OpenGL ES %d.%d context", major, minor); + break; + } + } + + if (!m_context) + { + Log_ErrorPrintf("Failed to create any GL context"); + return false; + } + + // Load GLAD. + go2_context_make_current(m_context); + if (!gladLoadGLES2Loader(reinterpret_cast(eglGetProcAddress))) + { + Log_ErrorPrintf("Failed to load GL functions"); + return false; + } + + // start with vsync on + eglSwapInterval(go2_context_egldisplay_get(m_context), 1); + return true; +} + +void Go2OpenGLHostDisplay::DestroyRenderDevice() +{ + if (ImGui::GetCurrentContext()) + DestroyImGuiContext(); + + DestroyResources(); + + if (m_context) + { + go2_context_make_current(nullptr); + go2_context_destroy(m_context); + } + + if (m_presenter) + go2_presenter_destroy(m_presenter); + + if (m_display) + go2_display_destroy(m_display); +} + +bool Go2OpenGLHostDisplay::Render() +{ + glDisable(GL_SCISSOR_TEST); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + glClear(GL_COLOR_BUFFER_BIT); + + if (HasDisplayTexture()) + { + const auto [left, top, width, height] = + CalculateDrawRect(m_window_info.surface_width, m_window_info.surface_height, 0, false); + RenderDisplay(left, top, width, height, m_display_texture_handle, m_display_texture_width, m_display_texture_height, + m_display_texture_view_x, m_display_texture_view_y, m_display_texture_view_width, + m_display_texture_view_height, m_display_linear_filtering); + } + + ImGui::Render(); + ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); + GL::Program::ResetLastProgram(); + + go2_context_swap_buffers(m_context); + go2_surface_t* gles_surface = go2_context_surface_lock(m_context); + go2_presenter_post(m_presenter, gles_surface, 0, 0, m_window_info.surface_width, m_window_info.surface_height, 0, 0, + m_window_info.surface_height, m_window_info.surface_width, GO2_ROTATION_DEGREES_270); + go2_context_surface_unlock(m_context, gles_surface); + + ImGui_ImplOpenGL3_NewFrame(); + return true; +} diff --git a/src/duckstation-go2/go2_opengl_host_display.h b/src/duckstation-go2/go2_opengl_host_display.h new file mode 100644 index 000000000..25585e066 --- /dev/null +++ b/src/duckstation-go2/go2_opengl_host_display.h @@ -0,0 +1,28 @@ +#pragma once +#include "common/gl/program.h" +#include "common/gl/texture.h" +#include "frontend-common/opengl_host_display.h" +#include +#include +#include + +class Go2OpenGLHostDisplay final : public FrontendCommon::OpenGLHostDisplay +{ +public: + Go2OpenGLHostDisplay(); + ~Go2OpenGLHostDisplay(); + + RenderAPI GetRenderAPI() const override; + + bool CreateRenderDevice(const WindowInfo& wi, std::string_view adapter_name, bool debug_device) override; + void DestroyRenderDevice() override; + + void SetVSync(bool enabled) override; + + bool Render() override; + +private: + go2_display_t* m_display = nullptr; + go2_context_t* m_context = nullptr; + go2_presenter_t* m_presenter = nullptr; +}; diff --git a/src/duckstation-go2/main.cpp b/src/duckstation-go2/main.cpp new file mode 100644 index 000000000..0ada561db --- /dev/null +++ b/src/duckstation-go2/main.cpp @@ -0,0 +1,43 @@ +#include "common/assert.h" +#include "common/log.h" +#include "core/system.h" +#include "frontend-common/sdl_initializer.h" +#include "go2_host_interface.h" +#include +#include + +int main(int argc, char* argv[]) +{ + std::unique_ptr host_interface = Go2HostInterface::Create(); + std::unique_ptr boot_params; + if (!host_interface->ParseCommandLineParameters(argc, argv, &boot_params)) + return EXIT_FAILURE; + + if (!host_interface->Initialize()) + { + host_interface->Shutdown(); + return EXIT_FAILURE; + } + + if (boot_params) + { + if (!host_interface->BootSystem(*boot_params) && host_interface->InBatchMode()) + { + host_interface->Shutdown(); + host_interface.reset(); + return EXIT_FAILURE; + } + + boot_params.reset(); + host_interface->Run(); + } + else + { + std::fprintf(stderr, "No file specified.\n"); + } + + host_interface->Shutdown(); + host_interface.reset(); + + return EXIT_SUCCESS; +}