// 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 #endif #include #include "audio_core/audio_types.h" #include "citra_libretro/citra_libretro.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(width), static_cast(height), 0); current_state.Apply(); #endif break; } case Settings::GraphicsAPI::Vulkan: { #ifdef ENABLE_VULKAN if (enableEmulatedPointer && tracker) { tracker->Render(width, height); } LibRetro::UploadVideoFrame(RETRO_HW_FRAME_BUFFER_VALID, static_cast(width), static_cast(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(fb.data); pitch = fb.pitch; } else { pitch = static_cast(width) * 4; data = static_cast(calloc(1, pitch * height)); did_malloc = true; } std::memset(data, 0, pitch * height); auto& system = Core::System::GetInstance(); const auto& renderer = static_cast(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& 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(rect.top + oy) * pitch + static_cast(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(width), static_cast(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(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 _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(); doCleanFrame = true; } void EmuWindow_LibRetro::DestroyContext() { tracker = nullptr; }