// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #include #include #include #include #include "citra_libretro/core_settings.h" #include "citra_libretro/environment.h" #include "citra_libretro/input/mouse_tracker.h" #include "common/settings.h" #include "core/frontend/framebuffer_layout.h" #ifdef ENABLE_OPENGL #include #include "video_core/shader/generator/glsl_shader_gen.h" #endif #ifdef ENABLE_VULKAN #include "core/core.h" #include "video_core/gpu.h" #include "video_core/renderer_vulkan/renderer_vulkan.h" #endif namespace LibRetro { namespace Input { /// Shared cursor coordinate calculation struct CursorCoordinates { float centerX, centerY; float renderWidth, renderHeight; float boundingLeft, boundingTop, boundingRight, boundingBottom; float verticalLeft, verticalRight, verticalTop, verticalBottom; float horizontalLeft, horizontalRight, horizontalTop, horizontalBottom; CursorCoordinates(int bufferWidth, int bufferHeight, float projectedX, float projectedY, float renderRatio, const Layout::FramebufferLayout& layout) { // Convert to normalized device coordinates centerX = (projectedX / bufferWidth) * 2 - 1; centerY = (projectedY / bufferHeight) * 2 - 1; renderWidth = renderRatio / bufferWidth; renderHeight = renderRatio / bufferHeight; boundingLeft = (layout.bottom_screen.left / (float)bufferWidth) * 2 - 1; boundingTop = (layout.bottom_screen.top / (float)bufferHeight) * 2 - 1; boundingRight = (layout.bottom_screen.right / (float)bufferWidth) * 2 - 1; boundingBottom = (layout.bottom_screen.bottom / (float)bufferHeight) * 2 - 1; // Calculate cursor dimensions verticalLeft = std::fmax(centerX - renderWidth / 5, boundingLeft); verticalRight = std::fmin(centerX + renderWidth / 5, boundingRight); verticalTop = -std::fmax(centerY - renderHeight, boundingTop); verticalBottom = -std::fmin(centerY + renderHeight, boundingBottom); horizontalLeft = std::fmax(centerX - renderWidth, boundingLeft); horizontalRight = std::fmin(centerX + renderWidth, boundingRight); horizontalTop = -std::fmax(centerY - renderHeight / 5, boundingTop); horizontalBottom = -std::fmin(centerY + renderHeight / 5, boundingBottom); } }; /// Helper function to check if coordinates are within the touchscreen area /// (uses the same logic as EmuWindow::IsWithinTouchscreen) static bool IsWithinTouchscreen(const Layout::FramebufferLayout& layout, unsigned framebuffer_x, unsigned framebuffer_y) { // Note: LibRetro doesn't support SeparateWindows, so we can skip that check Settings::StereoRenderOption render_3d_mode = Settings::values.render_3d.GetValue(); if (render_3d_mode == Settings::StereoRenderOption::SideBySide || render_3d_mode == Settings::StereoRenderOption::SideBySideFull) { return (framebuffer_y >= layout.bottom_screen.top && framebuffer_y < layout.bottom_screen.bottom && ((framebuffer_x >= layout.bottom_screen.left / 2 && framebuffer_x < layout.bottom_screen.right / 2) || (framebuffer_x >= (layout.bottom_screen.left / 2) + (layout.width / 2) && framebuffer_x < (layout.bottom_screen.right / 2) + (layout.width / 2)))); } else if (render_3d_mode == Settings::StereoRenderOption::CardboardVR) { return (framebuffer_y >= layout.bottom_screen.top && framebuffer_y < layout.bottom_screen.bottom && ((framebuffer_x >= layout.bottom_screen.left && framebuffer_x < layout.bottom_screen.right) || (framebuffer_x >= layout.cardboard.bottom_screen_right_eye + (layout.width / 2) && framebuffer_x < layout.cardboard.bottom_screen_right_eye + layout.bottom_screen.GetWidth() + (layout.width / 2)))); } else { return (framebuffer_y >= layout.bottom_screen.top && framebuffer_y < layout.bottom_screen.bottom && framebuffer_x >= layout.bottom_screen.left && framebuffer_x < layout.bottom_screen.right); } } MouseTracker::MouseTracker() { // Create renderer-specific cursor renderer based on current graphics API cursor_renderer = nullptr; switch (Settings::values.graphics_api.GetValue()) { case Settings::GraphicsAPI::OpenGL: #ifdef ENABLE_OPENGL cursor_renderer = std::make_unique(); #endif break; case Settings::GraphicsAPI::Vulkan: #ifdef ENABLE_VULKAN cursor_renderer = std::make_unique(); #endif break; case Settings::GraphicsAPI::Software: cursor_renderer = std::make_unique(); break; } last_moved = std::chrono::steady_clock::time_point::min(); } MouseTracker::~MouseTracker() = default; void MouseTracker::OnMouseMove(int deltaX, int deltaY) { x += deltaX; y += deltaY; } void MouseTracker::Restrict(int minX, int minY, int maxX, int maxY) { x = std::clamp(x, minX, maxX); y = std::clamp(y, minY, maxY); } void MouseTracker::Update(int bufferWidth, int bufferHeight, const Layout::FramebufferLayout& layout) { bool state = false; bool wasMoved = false; if (LibRetro::settings.enable_mouse_touchscreen) { // Check mouse input state |= LibRetro::CheckInput(0, RETRO_DEVICE_MOUSE, 0, RETRO_DEVICE_ID_MOUSE_LEFT); // Read in and convert pointer values to absolute values on the canvas auto pointerX = LibRetro::CheckInput(0, RETRO_DEVICE_POINTER, 0, RETRO_DEVICE_ID_POINTER_X); auto pointerY = LibRetro::CheckInput(0, RETRO_DEVICE_POINTER, 0, RETRO_DEVICE_ID_POINTER_Y); auto newX = static_cast((pointerX + 0x7fff) / (float)(0x7fff * 2) * bufferWidth); auto newY = static_cast((pointerY + 0x7fff) / (float)(0x7fff * 2) * bufferHeight); // Use mouse pointer movement if ((pointerX != 0 || pointerY != 0) && (newX != lastMouseX || newY != lastMouseY)) { lastMouseX = newX; lastMouseY = newY; // Use layout system to validate and map coordinates if (IsWithinTouchscreen(layout, newX, newY)) { x = std::clamp(newX, static_cast(layout.bottom_screen.left), static_cast(layout.bottom_screen.right)) - layout.bottom_screen.left; y = std::clamp(newY, static_cast(layout.bottom_screen.top), static_cast(layout.bottom_screen.bottom)) - layout.bottom_screen.top; } } } if (LibRetro::settings.enable_touch_touchscreen) { // Check touchscreen input state |= LibRetro::CheckInput(0, RETRO_DEVICE_POINTER, 0, RETRO_DEVICE_ID_POINTER_PRESSED); // Read in and convert pointer values to absolute values on the canvas auto pointerX = LibRetro::CheckInput(0, RETRO_DEVICE_POINTER, 0, RETRO_DEVICE_ID_POINTER_X); auto pointerY = LibRetro::CheckInput(0, RETRO_DEVICE_POINTER, 0, RETRO_DEVICE_ID_POINTER_Y); auto newX = static_cast((pointerX + 0x7fff) / (float)(0x7fff * 2) * bufferWidth); auto newY = static_cast((pointerY + 0x7fff) / (float)(0x7fff * 2) * bufferHeight); // Use mouse pointer movement if ((pointerX != 0 || pointerY != 0) && (newX != lastMouseX || newY != lastMouseY)) { lastMouseX = newX; lastMouseY = newY; // Use layout system to validate and map coordinates if (IsWithinTouchscreen(layout, newX, newY)) { x = std::clamp(newX, static_cast(layout.bottom_screen.left), static_cast(layout.bottom_screen.right)) - layout.bottom_screen.left; y = std::clamp(newY, static_cast(layout.bottom_screen.top), static_cast(layout.bottom_screen.bottom)) - layout.bottom_screen.top; } } } if (LibRetro::settings.analog_function != LibRetro::CStickFunction::CStick) { // Check right analog input state |= LibRetro::CheckInput(0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_R3); // TODO: Provide config option for ratios here auto widthSpeed = (layout.bottom_screen.GetWidth() / 20.0); auto heightSpeed = (layout.bottom_screen.GetHeight() / 20.0); // Use controller movement float controllerX = ((float)LibRetro::CheckInput(0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_RIGHT, RETRO_DEVICE_ID_ANALOG_X) / INT16_MAX); float controllerY = ((float)LibRetro::CheckInput(0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_RIGHT, RETRO_DEVICE_ID_ANALOG_Y) / INT16_MAX); // Deadzone the controller inputs float magnitudeX = std::abs(controllerX); float magnitudeY = std::abs(controllerY); if (magnitudeX < LibRetro::settings.analog_deadzone) { controllerX = 0; } if (magnitudeY < LibRetro::settings.analog_deadzone) { controllerY = 0; } if (controllerX != 0 || controllerY != 0) { wasMoved = true; } OnMouseMove(static_cast(controllerX * widthSpeed), static_cast(controllerY * heightSpeed)); } Restrict(0, 0, layout.bottom_screen.GetWidth(), layout.bottom_screen.GetHeight()); // Store as bottom-screen-local pixel coordinates projectedX = static_cast(x); projectedY = static_cast(y); isPressed = state; if (wasMoved) { last_moved = std::chrono::steady_clock::now(); } this->framebuffer_layout = layout; } void MouseTracker::Render(int bufferWidth, int bufferHeight, void* framebuffer_data) { if (GetCursorInfo().visible == false) { return; } // Delegate to renderer-specific implementation. // Convert from bottom-screen-local to layout-absolute for the legacy renderers. if (cursor_renderer) { const float abs_x = framebuffer_layout.bottom_screen.left + projectedX; const float abs_y = framebuffer_layout.bottom_screen.top + projectedY; const float ratio = static_cast(framebuffer_layout.bottom_screen.GetHeight()) / 30.0f; cursor_renderer->Render(bufferWidth, bufferHeight, abs_x, abs_y, ratio, framebuffer_layout, framebuffer_data); } } Frontend::EmuWindow::CursorInfo MouseTracker::GetCursorInfo() { bool visible = true; auto current = std::chrono::steady_clock::now(); uint64_t since_last_moved = std::chrono::duration_cast(current - last_moved).count(); constexpr auto timeout_secs = 4; // TODO: Make this configurable maybe? -OS if (LibRetro::settings.enable_touch_pointer_timeout && since_last_moved >= timeout_secs) { visible = false; } return {visible, projectedX, projectedY}; } #ifdef ENABLE_OPENGL // OpenGL-specific cursor renderer implementation OpenGLCursorRenderer::OpenGLCursorRenderer() { // Could potentially also use Citra's built-in shaders, if they can be // wrangled to cooperate. std::string vertex; if (Settings::values.use_gles) { vertex += fragment_shader_precision_OES; } vertex += R"( in vec2 position; void main() { gl_Position = vec4(position, 0.0, 1.0); } )"; std::string fragment; if (Settings::values.use_gles) { fragment += fragment_shader_precision_OES; } fragment += R"( out vec4 color; void main() { color = vec4(1.0, 1.0, 1.0, 1.0); } )"; vao.Create(); vbo.Create(); glBindVertexArray(vao.handle); glBindBuffer(GL_ARRAY_BUFFER, vbo.handle); shader.Create(vertex.c_str(), fragment.c_str()); auto positionVariable = (GLuint)glGetAttribLocation(shader.handle, "position"); glEnableVertexAttribArray(positionVariable); glVertexAttribPointer(positionVariable, 2, GL_FLOAT, GL_FALSE, 0, 0); } OpenGLCursorRenderer::~OpenGLCursorRenderer() { shader.Release(); vao.Release(); vbo.Release(); } void OpenGLCursorRenderer::Render(int bufferWidth, int bufferHeight, float projectedX, float projectedY, float renderRatio, const Layout::FramebufferLayout& layout, void* framebuffer_data) { // Use shared coordinate calculation CursorCoordinates coords(bufferWidth, bufferHeight, projectedX, projectedY, renderRatio, layout); glUseProgram(shader.handle); glBindVertexArray(vao.handle); // clang-format off GLfloat cursor[] = { // | in the cursor coords.verticalLeft, coords.verticalTop, coords.verticalRight, coords.verticalTop, coords.verticalRight, coords.verticalBottom, coords.verticalLeft, coords.verticalTop, coords.verticalRight, coords.verticalBottom, coords.verticalLeft, coords.verticalBottom, // - in the cursor coords.horizontalLeft, coords.horizontalTop, coords.horizontalRight, coords.horizontalTop, coords.horizontalRight, coords.horizontalBottom, coords.horizontalLeft, coords.horizontalTop, coords.horizontalRight, coords.horizontalBottom, coords.horizontalLeft, coords.horizontalBottom }; // clang-format on glEnable(GL_BLEND); glBlendFunc(GL_ONE_MINUS_DST_COLOR, GL_ONE_MINUS_SRC_COLOR); glBindBuffer(GL_ARRAY_BUFFER, vbo.handle); glBufferData(GL_ARRAY_BUFFER, sizeof(cursor), cursor, GL_STATIC_DRAW); glDrawArrays(GL_TRIANGLES, 0, 12); glBindVertexArray(0); glUseProgram(0); glDisable(GL_BLEND); } #endif #ifdef ENABLE_VULKAN // Vulkan cursor is drawn by RendererVulkan::DrawCursor() inside the render pass. // This class exists only to satisfy the CursorRenderer interface. VulkanCursorRenderer::VulkanCursorRenderer() = default; VulkanCursorRenderer::~VulkanCursorRenderer() = default; void VulkanCursorRenderer::Render(int, int, float, float, float, const Layout::FramebufferLayout&, void*) {} #endif // Software-specific cursor renderer implementation SoftwareCursorRenderer::SoftwareCursorRenderer() { // Software renderer initialization } SoftwareCursorRenderer::~SoftwareCursorRenderer() = default; void SoftwareCursorRenderer::Render(int bufferWidth, int bufferHeight, float projectedX, float projectedY, float renderRatio, const Layout::FramebufferLayout& layout, void* framebuffer_data) { if (!framebuffer_data) { return; // No framebuffer data available } // Convert coordinates to screen space int centerX = static_cast(projectedX); int centerY = static_cast(projectedY); int radius = static_cast(renderRatio); // Calculate cursor dimensions within bounds int verticalLeft = std::max(centerX - radius / 5, static_cast(layout.bottom_screen.left)); int verticalRight = std::min(centerX + radius / 5, static_cast(layout.bottom_screen.right)); int verticalTop = std::max(centerY - radius, static_cast(layout.bottom_screen.top)); int verticalBottom = std::min(centerY + radius, static_cast(layout.bottom_screen.bottom)); int horizontalLeft = std::max(centerX - radius, static_cast(layout.bottom_screen.left)); int horizontalRight = std::min(centerX + radius, static_cast(layout.bottom_screen.right)); int horizontalTop = std::max(centerY - radius / 5, static_cast(layout.bottom_screen.top)); int horizontalBottom = std::min(centerY + radius / 5, static_cast(layout.bottom_screen.bottom)); // Draw cursor directly to framebuffer (assuming RGBA8888 format) uint32_t* pixels = static_cast(framebuffer_data); const uint32_t cursorColor = 0xFFFFFFFF; // White cursor // Draw vertical line of cursor for (int y = verticalTop; y < verticalBottom; ++y) { for (int x = verticalLeft; x < verticalRight; ++x) { if (x >= 0 && x < bufferWidth && y >= 0 && y < bufferHeight) { int pixelIndex = y * bufferWidth + x; // XOR blend for visibility on any background pixels[pixelIndex] ^= cursorColor; } } } // Draw horizontal line of cursor for (int y = horizontalTop; y < horizontalBottom; ++y) { for (int x = horizontalLeft; x < horizontalRight; ++x) { if (x >= 0 && x < bufferWidth && y >= 0 && y < bufferHeight) { int pixelIndex = y * bufferWidth + x; // XOR blend for visibility on any background pixels[pixelIndex] ^= cursorColor; } } } } } // namespace Input } // namespace LibRetro