mirror of
https://github.com/Lime3DS/Lime3DS.git
synced 2026-03-27 22:06:39 -06:00
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.
349 lines
11 KiB
C++
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 {};
|
|
}
|