Lime3DS/src/citra_libretro/emu_window/libretro_window.cpp
OpenSauce04 9b045bf837 libretro: Replace render_touchscreen setting with enable_touch_pointer_timeout
Touch pointer rendering is now always enabled, but unless a controller is being used to move the touchscreen cursor, it will be hidden due to the timeout which is also enabled by default.
2026-03-23 13:07:39 +00:00

349 lines
11 KiB
C++

// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#ifdef ENABLE_OPENGL
#include <glad/glad.h>
#endif
#include <libretro.h>
#include "audio_core/audio_types.h"
#include "citra_libretro/citra_libretro.h"
#include "citra_libretro/core_settings.h"
#include "citra_libretro/environment.h"
#include "citra_libretro/input/input_factory.h"
#include "common/settings.h"
#include "core/3ds.h"
#ifdef ENABLE_OPENGL
#include "video_core/renderer_opengl/gl_state.h"
#endif
#include "video_core/gpu.h"
#include "video_core/renderer_software/renderer_software.h"
#ifdef ENABLE_OPENGL
/// LibRetro expects a "default" GL state.
void ResetGLState() {
// Reset internal state.
OpenGL::OpenGLState state{};
state.Apply();
// Clean up global state.
if (!Settings::values.use_gles) {
glLogicOp(GL_COPY);
}
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LESS);
glDepthMask(GL_TRUE);
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
glDisable(GL_STENCIL_TEST);
glStencilFunc(GL_ALWAYS, 0, 0xFFFFFFFF);
glEnable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ZERO);
glBlendEquation(GL_FUNC_ADD);
glBlendFuncSeparate(GL_ONE, GL_ZERO, GL_ONE, GL_ZERO);
glBlendColor(0, 0, 0, 0);
glDisable(GL_COLOR_LOGIC_OP);
glDisable(GL_DITHER);
glDisable(GL_CULL_FACE);
glCullFace(GL_BACK);
glActiveTexture(GL_TEXTURE0);
}
#endif
EmuWindow_LibRetro::EmuWindow_LibRetro() {
strict_context_required = true;
window_info.type = Frontend::WindowSystemType::LibRetro;
}
EmuWindow_LibRetro::~EmuWindow_LibRetro() {}
void EmuWindow_LibRetro::SwapBuffers() {
if (suppressPresentation)
return;
submittedFrame = true;
switch (Settings::values.graphics_api.GetValue()) {
case Settings::GraphicsAPI::OpenGL: {
#ifdef ENABLE_OPENGL
auto current_state = OpenGL::OpenGLState::GetCurState();
ResetGLState();
if (enableEmulatedPointer && tracker) {
tracker->Render(width, height);
}
LibRetro::UploadVideoFrame(RETRO_HW_FRAME_BUFFER_VALID, static_cast<unsigned>(width),
static_cast<unsigned>(height), 0);
current_state.Apply();
#endif
break;
}
case Settings::GraphicsAPI::Vulkan: {
#ifdef ENABLE_VULKAN
// Cursor is drawn inside the Vulkan render pass (RendererVulkan::DrawCursor)
LibRetro::UploadVideoFrame(RETRO_HW_FRAME_BUFFER_VALID, static_cast<unsigned>(width),
static_cast<unsigned>(height), 0);
#endif
break;
}
case Settings::GraphicsAPI::Software: {
retro_framebuffer fb;
u8* data;
size_t pitch;
bool did_malloc = false;
if (LibRetro::GetSoftwareFramebuffer(&fb, width, height)) {
data = static_cast<u8*>(fb.data);
pitch = fb.pitch;
} else {
pitch = static_cast<size_t>(width) * 4;
data = static_cast<u8*>(calloc(1, pitch * height));
did_malloc = true;
}
std::memset(data, 0, pitch * height);
auto& system = Core::System::GetInstance();
const auto& renderer = static_cast<SwRenderer::RendererSoftware&>(system.GPU().Renderer());
const auto& layout = GetFramebufferLayout();
// Blit a single screen from ScreenInfo (column-major RGBA) to the
// output buffer (row-major XRGB8888), rotating and scaling as needed.
// The 3DS framebuffer is portrait-oriented; ScreenInfo stores pixels
// column-major so the transpose gives us the landscape orientation:
// display (dx, dy) -> ScreenInfo (x=dy, y=dx)
auto blit_screen = [&](VideoCore::ScreenId screen_id, const Common::Rectangle<u32>& rect) {
const auto& info = renderer.Screen(screen_id);
if (info.pixels.empty())
return;
const u32 rect_w = rect.GetWidth();
const u32 rect_h = rect.GetHeight();
if (rect_w == 0 || rect_h == 0)
return;
// Landscape display dimensions (transposed from portrait storage)
const u32 native_w = info.height;
const u32 native_h = info.width;
for (u32 oy = 0; oy < rect_h; oy++) {
for (u32 ox = 0; ox < rect_w; ox++) {
const u32 dx = ox * native_w / rect_w;
const u32 dy = oy * native_h / rect_h;
const u32 src_off = (dy * info.height + dx) * 4;
if (src_off + 3 >= info.pixels.size())
continue;
const u8* src = info.pixels.data() + src_off;
const size_t dst_off = static_cast<size_t>(rect.top + oy) * pitch +
static_cast<size_t>(rect.left + ox) * 4;
// RGBA -> XRGB8888 (little-endian: B, G, R, 0)
data[dst_off + 0] = src[2];
data[dst_off + 1] = src[1];
data[dst_off + 2] = src[0];
data[dst_off + 3] = 0;
}
}
};
if (layout.top_screen_enabled) {
blit_screen(VideoCore::ScreenId::TopLeft, layout.top_screen);
}
if (layout.bottom_screen_enabled) {
blit_screen(VideoCore::ScreenId::Bottom, layout.bottom_screen);
}
// Software cursor rendering with framebuffer access
if (enableEmulatedPointer && tracker) {
tracker->Render(width, height, data);
}
LibRetro::UploadVideoFrame(data, static_cast<unsigned>(width),
static_cast<unsigned>(height), pitch);
if (did_malloc)
free(data);
break;
}
}
}
void EmuWindow_LibRetro::SetupFramebuffer() {
if (Settings::values.graphics_api.GetValue() != Settings::GraphicsAPI::OpenGL)
return;
#ifdef ENABLE_OPENGL
// TODO: Expose interface in renderer_opengl to configure this in it's internal state
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast<GLuint>(LibRetro::GetFramebuffer()));
// glClear can be a slow path - skip clearing if we don't need to.
if (doCleanFrame) {
glClear(GL_COLOR_BUFFER_BIT);
doCleanFrame = false;
}
#endif
}
void EmuWindow_LibRetro::PollEvents() {
// The software renderer doesn't call render_window.SwapBuffers() — standalone
// frontends (Qt/SDL) use separate presentation threads that pull from screen_infos
// instead. In libretro there's no such thread, so we present here: PollEvents is
// called from EndFrame() during each VBlank, right after PrepareRenderTarget has
// filled the screen pixel buffers.
if (Settings::values.graphics_api.GetValue() == Settings::GraphicsAPI::Software) {
SwapBuffers();
}
LibRetro::PollInput();
// TODO: Poll for right click for motion emu
if (enableEmulatedPointer && tracker) {
tracker->Update(width, height, GetFramebufferLayout());
if (tracker->IsPressed()) {
auto mousePos = tracker->GetPressedPosition();
if (hasTouched) {
TouchMoved(mousePos.first, mousePos.second);
} else {
TouchPressed(mousePos.first, mousePos.second);
hasTouched = true;
}
} else if (hasTouched) {
hasTouched = false;
TouchReleased();
}
}
}
void EmuWindow_LibRetro::MakeCurrent() {
// They don't get any say in the matter - GL context is always current!
}
void EmuWindow_LibRetro::DoneCurrent() {
// They don't get any say in the matter - GL context is always current!
}
void EmuWindow_LibRetro::OnMinimalClientAreaChangeRequest(std::pair<u32, u32> _minimal_size) {}
LayoutGeometry ComputeLayoutGeometry() {
unsigned baseX;
unsigned baseY;
bool emulated_pointer = true;
float scaling = Settings::values.resolution_factor.GetValue();
bool swapped = Settings::values.swap_screen.GetValue();
switch (Settings::values.layout_option.GetValue()) {
case Settings::LayoutOption::SingleScreen:
if (swapped) { // Bottom screen visible
baseX = Core::kScreenBottomWidth;
baseY = Core::kScreenBottomHeight;
} else { // Top screen visible
baseX = Core::kScreenTopWidth;
baseY = Core::kScreenTopHeight;
emulated_pointer = false;
}
baseX *= scaling;
baseY *= scaling;
break;
case Settings::LayoutOption::LargeScreen:
if (swapped) { // Bottom screen biggest
baseX = Core::kScreenBottomWidth + Core::kScreenTopWidth / 4;
baseY = Core::kScreenBottomHeight;
} else { // Top screen biggest
baseX = Core::kScreenTopWidth + Core::kScreenBottomWidth / 4;
baseY = Core::kScreenTopHeight;
}
if (scaling < 4) {
// Unfortunately, to get this aspect ratio correct (and have non-blurry 1x scaling),
// we have to have a pretty large buffer for the minimum ratio.
baseX *= 4;
baseY *= 4;
} else {
baseX *= scaling;
baseY *= scaling;
}
break;
case Settings::LayoutOption::SideScreen:
baseX = Core::kScreenBottomWidth + Core::kScreenTopWidth;
baseY = Core::kScreenTopHeight;
baseX *= scaling;
baseY *= scaling;
break;
case Settings::LayoutOption::Default:
default:
baseX = Core::kScreenTopWidth;
baseY = Core::kScreenTopHeight + Core::kScreenBottomHeight;
baseX *= scaling;
baseY *= scaling;
break;
}
return {baseX, baseY, emulated_pointer};
}
void EmuWindow_LibRetro::UpdateLayout() {
auto geom = ComputeLayoutGeometry();
unsigned baseX = geom.width;
unsigned baseY = geom.height;
enableEmulatedPointer = geom.emulated_pointer;
// Update Libretro with our status
struct retro_system_av_info info{};
info.timing.fps = 60.0;
info.timing.sample_rate = AudioCore::native_sample_rate;
info.geometry.aspect_ratio = (float)baseX / (float)baseY;
info.geometry.base_width = baseX;
info.geometry.base_height = baseY;
info.geometry.max_width = baseX;
info.geometry.max_height = baseY;
if (!LibRetro::SetGeometry(&info)) {
LOG_CRITICAL(Frontend, "Failed to update 3DS layout in frontend!");
}
width = baseX;
height = baseY;
UpdateCurrentFramebufferLayout(baseX, baseY);
doCleanFrame = true;
}
bool EmuWindow_LibRetro::NeedsClearing() const {
// We manage this ourselves.
return false;
}
bool EmuWindow_LibRetro::HasSubmittedFrame() {
bool state = submittedFrame;
submittedFrame = false;
return state;
}
void EmuWindow_LibRetro::CreateContext() {
tracker = std::make_unique<LibRetro::Input::MouseTracker>();
doCleanFrame = true;
}
void EmuWindow_LibRetro::DestroyContext() {
tracker = nullptr;
}
Frontend::EmuWindow::CursorInfo EmuWindow_LibRetro::GetCursorInfo() const {
if (enableEmulatedPointer && tracker) {
return tracker->GetCursorInfo();
}
return {};
}