Merge 3929d448156e75c74c145e9984d8d4a0642bbe9d into 26ce7e4f2844a445bf77b4b14977d62e6434df08

This commit is contained in:
David Griswold 2025-03-11 20:51:49 +00:00 committed by GitHub
commit ac5b74685c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 533 additions and 162 deletions

View File

@ -125,6 +125,10 @@ object NativeLibrary {
external fun surfaceDestroyed()
external fun doFrame()
//Second window
external fun secondarySurfaceChanged(secondary_surface: Surface)
external fun secondarySurfaceDestroyed()
external fun disableSecondaryScreen()
/**
* Unpauses emulation from a paused state.
*/

View File

@ -4,14 +4,18 @@
package org.citra.citra_emu.activities
import SecondScreenPresentation
import android.Manifest.permission
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Presentation
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.hardware.display.DisplayManager
import android.net.Uri
import android.os.Bundle
import android.view.Display
import android.view.InputDevice
import android.view.KeyEvent
import android.view.MotionEvent
@ -32,6 +36,7 @@ import org.citra.citra_emu.camera.StillImageCameraHelper.OnFilePickerResult
import org.citra.citra_emu.contracts.OpenFileResultContract
import org.citra.citra_emu.databinding.ActivityEmulationBinding
import org.citra.citra_emu.display.ScreenAdjustmentUtil
import org.citra.citra_emu.display.SecondaryScreenLayout
import org.citra.citra_emu.features.hotkeys.HotkeyUtility
import org.citra.citra_emu.features.settings.model.BooleanSetting
import org.citra.citra_emu.features.settings.model.IntSetting
@ -56,6 +61,34 @@ class EmulationActivity : AppCompatActivity() {
private lateinit var binding: ActivityEmulationBinding
private lateinit var screenAdjustmentUtil: ScreenAdjustmentUtil
private lateinit var hotkeyUtility: HotkeyUtility
private var secondScreenPresentation: Presentation? = null
private lateinit var displayManager: DisplayManager
private fun updatePresentation() {
displayManager = getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
val display = getCustomerDisplay();
if (secondScreenPresentation != null && (IntSetting.SECONDARY_SCREEN_LAYOUT.int == SecondaryScreenLayout.NONE.int || display == null)) {
releasePresentation();
}
if (secondScreenPresentation == null || secondScreenPresentation?.display != display) {
secondScreenPresentation?.dismiss()
if (display != null && IntSetting.SECONDARY_SCREEN_LAYOUT.int != SecondaryScreenLayout.NONE.int) {
secondScreenPresentation = SecondScreenPresentation(this, display)
secondScreenPresentation?.show();
}
}
}
private fun releasePresentation() {
if (secondScreenPresentation != null) {
NativeLibrary.disableSecondaryScreen()
secondScreenPresentation?.dismiss();
secondScreenPresentation = null;
}
}
private fun getCustomerDisplay(): Display? {
return displayManager?.displays?.firstOrNull { it.isValid && it.displayId != Display.DEFAULT_DISPLAY }
}
private val emulationFragment: EmulationFragment
get() {
@ -68,10 +101,9 @@ class EmulationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
ThemeUtil.setTheme(this)
settingsViewModel.settings.loadSettings()
super.onCreate(savedInstanceState)
updatePresentation();
binding = ActivityEmulationBinding.inflate(layoutInflater)
screenAdjustmentUtil = ScreenAdjustmentUtil(this, windowManager, settingsViewModel.settings)
@ -117,6 +149,11 @@ class EmulationActivity : AppCompatActivity() {
applyOrientationSettings() // Check for orientation settings changes on runtime
}
override fun onStop() {
releasePresentation()
super.onStop()
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
enableFullscreenImmersive()
@ -124,6 +161,7 @@ class EmulationActivity : AppCompatActivity() {
public override fun onRestart() {
super.onRestart()
updatePresentation()
NativeLibrary.reloadCameraDevices()
}
@ -141,6 +179,7 @@ class EmulationActivity : AppCompatActivity() {
EmulationLifecycleUtil.clear()
isEmulationRunning = false
instance = null
releasePresentation()
super.onDestroy()
}

View File

@ -49,3 +49,17 @@ enum class PortraitScreenLayout(val int: Int) {
}
}
}
enum class SecondaryScreenLayout(val int: Int) {
// These must match what is defined in src/common/settings.h
NONE(0),
TOP_SCREEN(1),
BOTTOM_SCREEN(2),
SIDE_BY_SIDE(3);
companion object {
fun from(int: Int): SecondaryScreenLayout {
return entries.firstOrNull { it.int == int } ?: NONE
}
}
}

View File

@ -0,0 +1,48 @@
import android.app.Presentation
import android.content.Context
import android.os.Bundle
import android.view.Display
import android.view.SurfaceHolder
import android.view.SurfaceView
import org.citra.citra_emu.NativeLibrary
class SecondScreenPresentation(
context: Context,
display: Display,
) : Presentation(context, display) {
private lateinit var surfaceView: SurfaceView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Initialize SurfaceView
surfaceView = SurfaceView(context)
surfaceView.holder.addCallback(object : SurfaceHolder.Callback {
override fun surfaceCreated(holder: SurfaceHolder) {
}
override fun surfaceChanged(
holder: SurfaceHolder,
format: Int,
width: Int,
height: Int
) {
NativeLibrary.secondarySurfaceChanged(holder.surface)
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
NativeLibrary.secondarySurfaceDestroyed()
}
})
setContentView(surfaceView) // Set SurfaceView as content
}
// Publicly accessible method to get the SurfaceHolder
fun getSurfaceHolder(): SurfaceHolder {
return surfaceView.holder
}
}

View File

@ -34,6 +34,7 @@ enum class IntSetting(
LANDSCAPE_BOTTOM_WIDTH("custom_bottom_width",Settings.SECTION_LAYOUT,640),
LANDSCAPE_BOTTOM_HEIGHT("custom_bottom_height",Settings.SECTION_LAYOUT,480),
PORTRAIT_SCREEN_LAYOUT("portrait_layout_option",Settings.SECTION_LAYOUT,0),
SECONDARY_SCREEN_LAYOUT("secondary_screen_layout",Settings.SECTION_LAYOUT,0),
PORTRAIT_TOP_X("custom_portrait_top_x",Settings.SECTION_LAYOUT,0),
PORTRAIT_TOP_Y("custom_portrait_top_y",Settings.SECTION_LAYOUT,0),
PORTRAIT_TOP_WIDTH("custom_portrait_top_width",Settings.SECTION_LAYOUT,800),

View File

@ -962,6 +962,17 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
IntSetting.PORTRAIT_SCREEN_LAYOUT.defaultValue
)
)
add(
SingleChoiceSetting(
IntSetting.SECONDARY_SCREEN_LAYOUT,
R.string.emulation_switch_secondary_layout,
0,
R.array.secondaryLayouts,
R.array.secondaryLayoutValues,
IntSetting.SECONDARY_SCREEN_LAYOUT.key,
IntSetting.SECONDARY_SCREEN_LAYOUT.defaultValue
)
)
add(
SingleChoiceSetting(
IntSetting.SMALL_SCREEN_POSITION,

View File

@ -272,6 +272,9 @@ object SettingsFile {
val settings = section.settings
val keySet: Set<String> = settings.keys
for (key in keySet) {
if (key.equals("secondary_screen_layout")) {
println("hi");
}
val setting = settings[key]
parser.put(header, setting!!.key, setting.valueAsString)
}

View File

@ -5,9 +5,12 @@
package org.citra.citra_emu.fragments
import android.annotation.SuppressLint
import android.app.Presentation
import android.content.Context
import android.content.DialogInterface
import android.content.SharedPreferences
import android.hardware.display.DisplayManager
import android.media.MediaRouter
import android.net.Uri
import android.os.Bundle
import android.os.Handler
@ -16,6 +19,7 @@ import android.os.SystemClock
import android.text.Editable
import android.text.TextWatcher
import android.view.Choreographer
import android.view.Display
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.Surface
@ -26,6 +30,7 @@ import android.widget.PopupMenu
import android.widget.TextView
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.core.content.ContextCompat.getSystemService
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.Insets
import androidx.core.view.ViewCompat
@ -79,6 +84,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
private lateinit var emulationState: EmulationState
private var perfStatsUpdater: Runnable? = null
private lateinit var emulationActivity: EmulationActivity
private var _binding: FragmentEmulationBinding? = null
@ -146,6 +152,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
retainInstance = true
emulationState = EmulationState(game.path)
emulationActivity = requireActivity() as EmulationActivity
screenAdjustmentUtil = ScreenAdjustmentUtil(requireContext(), requireActivity().windowManager, settingsViewModel.settings)
EmulationLifecycleUtil.addShutdownHook(hook = { emulationState.stop() })
EmulationLifecycleUtil.addPauseResumeHook(hook = { togglePause() })
@ -1214,6 +1221,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
private class EmulationState(private val gamePath: String) {
private var state: State
private var surface: Surface? = null
private var surface2: Surface? = null
init {
// Starting state is stopped.

View File

@ -204,6 +204,10 @@ void Config::ReadValues() {
static_cast<Settings::PortraitLayoutOption>(sdl2_config->GetInteger(
"Layout", "portrait_layout_option",
static_cast<int>(Settings::PortraitLayoutOption::PortraitTopFullWidth)));
Settings::values.secondary_screen_layout =
static_cast<Settings::SecondaryScreenLayout>(sdl2_config->GetInteger(
"Layout", "secondary_screen_layout",
static_cast<int>(Settings::SecondaryScreenLayout::None)));
ReadSetting("Layout", Settings::values.custom_portrait_top_x);
ReadSetting("Layout", Settings::values.custom_portrait_top_y);
ReadSetting("Layout", Settings::values.custom_portrait_top_width);

View File

@ -251,6 +251,15 @@ custom_portrait_bottom_height =
# 0 (default): Top Screen is prominent, 1: Bottom Screen is prominent
swap_screen =
# Secondary Screen Layout
# What the game should do if a secondary screen is connected physically or using
# Miracast / Chromecast screen mirroring
# 0 (default) - Use System Default (mirror)
# 1 - Show Top Screen Only
# 2 - Show Bottom Screen Only
# 3 - Show both screens side by side
secondary_screen_layout =
# Screen placement settings when using Cardboard VR (render3d = 4)
# 30 - 100: Screen size as a percentage of the viewport. 85 (default)
cardboard_screen_size =

View File

@ -49,16 +49,17 @@ void EmuWindow_Android::OnFramebufferSizeChanged() {
const int bigger{window_width > window_height ? window_width : window_height};
const int smaller{window_width < window_height ? window_width : window_height};
if (is_portrait_mode) {
if (is_portrait_mode && !is_secondary) {
UpdateCurrentFramebufferLayout(smaller, bigger, is_portrait_mode);
} else {
UpdateCurrentFramebufferLayout(bigger, smaller, is_portrait_mode);
}
}
EmuWindow_Android::EmuWindow_Android(ANativeWindow* surface) : host_window{surface} {
EmuWindow_Android::EmuWindow_Android(ANativeWindow *surface, bool is_secondary) : EmuWindow{
is_secondary}, host_window(surface) {
LOG_DEBUG(Frontend, "Initializing EmuWindow_Android");
if (is_secondary) LOG_DEBUG(Frontend, "Initializing secondary window Android");
if (!surface) {
LOG_CRITICAL(Frontend, "surface is nullptr");
return;

View File

@ -5,6 +5,7 @@
#pragma once
#include <vector>
#include <EGL/egl.h>
#include "core/frontend/emu_window.h"
namespace Core {
@ -13,7 +14,7 @@ class System;
class EmuWindow_Android : public Frontend::EmuWindow {
public:
EmuWindow_Android(ANativeWindow* surface);
EmuWindow_Android(ANativeWindow* surface, bool is_secondary = false);
~EmuWindow_Android();
/// Called by the onSurfaceChanges() method to change the surface
@ -30,7 +31,10 @@ public:
void DoneCurrent() override;
virtual void TryPresenting() {}
// EGL Context must be shared
// could probably use the existing
// SharedContext for this instead, this is maybe temporary
virtual EGLContext* GetEGLContext() {return NULL;}
virtual void StopPresenting() {}
protected:

View File

@ -72,8 +72,8 @@ private:
EGLContext egl_context{};
};
EmuWindow_Android_OpenGL::EmuWindow_Android_OpenGL(Core::System& system_, ANativeWindow* surface)
: EmuWindow_Android{surface}, system{system_} {
EmuWindow_Android_OpenGL::EmuWindow_Android_OpenGL(Core::System& system_, ANativeWindow* surface, bool is_secondary, EGLContext* sharedContext)
: EmuWindow_Android{surface,is_secondary}, system{system_} {
if (egl_display = eglGetDisplay(EGL_DEFAULT_DISPLAY); egl_display == EGL_NO_DISPLAY) {
LOG_CRITICAL(Frontend, "eglGetDisplay() failed");
return;
@ -96,8 +96,9 @@ EmuWindow_Android_OpenGL::EmuWindow_Android_OpenGL(Core::System& system_, ANativ
if (eglQuerySurface(egl_display, egl_surface, EGL_HEIGHT, &window_height) != EGL_TRUE) {
return;
}
if (egl_context = eglCreateContext(egl_display, egl_config, 0, egl_context_attribs.data());
if (sharedContext) {
egl_context = *sharedContext;
}else if (egl_context = eglCreateContext(egl_display, egl_config, 0, egl_context_attribs.data());
egl_context == EGL_NO_CONTEXT) {
LOG_CRITICAL(Frontend, "eglCreateContext() failed");
return;
@ -127,6 +128,10 @@ EmuWindow_Android_OpenGL::EmuWindow_Android_OpenGL(Core::System& system_, ANativ
OnFramebufferSizeChanged();
}
EGLContext* EmuWindow_Android_OpenGL::GetEGLContext() {
return &egl_context;
}
bool EmuWindow_Android_OpenGL::CreateWindowSurface() {
if (!host_window) {
return true;
@ -204,14 +209,15 @@ void EmuWindow_Android_OpenGL::TryPresenting() {
return;
}
if (presenting_state == PresentingState::Initial) [[unlikely]] {
eglMakeCurrent(egl_display, egl_surface, egl_surface, egl_context);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
presenting_state = PresentingState::Running;
}
if (presenting_state != PresentingState::Running) [[unlikely]] {
return;
}
// if (presenting_state != PresentingState::Running) [[unlikely]] {
// return;
// }
eglMakeCurrent(egl_display, egl_surface, egl_surface, egl_context);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
eglSwapInterval(egl_display, Settings::values.use_vsync_new ? 1 : 0);
system.GPU().Renderer().TryPresent(0);
system.GPU().Renderer().TryPresent(100,is_secondary);
eglSwapBuffers(egl_display, egl_surface);
}

View File

@ -19,13 +19,13 @@ struct ANativeWindow;
class EmuWindow_Android_OpenGL : public EmuWindow_Android {
public:
EmuWindow_Android_OpenGL(Core::System& system, ANativeWindow* surface);
EmuWindow_Android_OpenGL(Core::System& system, ANativeWindow* surface, bool is_secondary, EGLContext* sharedContext = NULL);
~EmuWindow_Android_OpenGL() override = default;
void TryPresenting() override;
void StopPresenting() override;
void PollEvents() override;
EGLContext* GetEGLContext() override;
std::unique_ptr<GraphicsContext> CreateSharedContext() const override;
private:

View File

@ -24,8 +24,8 @@ private:
};
EmuWindow_Android_Vulkan::EmuWindow_Android_Vulkan(
ANativeWindow* surface, std::shared_ptr<Common::DynamicLibrary> driver_library_)
: EmuWindow_Android{surface}, driver_library{driver_library_} {
ANativeWindow* surface, std::shared_ptr<Common::DynamicLibrary> driver_library_, bool is_secondary)
: EmuWindow_Android{surface,is_secondary}, driver_library{driver_library_} {
CreateWindowSurface();
if (core_context = CreateSharedContext(); !core_context) {

View File

@ -11,7 +11,7 @@ struct ANativeWindow;
class EmuWindow_Android_Vulkan : public EmuWindow_Android {
public:
EmuWindow_Android_Vulkan(ANativeWindow* surface,
std::shared_ptr<Common::DynamicLibrary> driver_library);
std::shared_ptr<Common::DynamicLibrary> driver_library, bool is_secondary);
~EmuWindow_Android_Vulkan() override = default;
void PollEvents() override {}

View File

@ -16,11 +16,15 @@
#include <core/hle/service/cfg/cfg.h>
#include "audio_core/dsp_interface.h"
#include "common/arch.h"
#if CITRA_ARCH(arm64)
#include "common/aarch64/cpu_detect.h"
#elif CITRA_ARCH(x86_64)
#include "common/x64/cpu_detect.h"
#endif
#include "common/common_paths.h"
#include "common/dynamic_library/dynamic_library.h"
#include "common/file_util.h"
@ -44,12 +48,18 @@
#include "jni/camera/ndk_camera.h"
#include "jni/camera/still_image_camera.h"
#include "jni/config.h"
#ifdef ENABLE_OPENGL
#include "jni/emu_window/emu_window_gl.h"
#endif
#ifdef ENABLE_VULKAN
#include "jni/emu_window/emu_window_vk.h"
#endif
#include "jni/id_cache.h"
#include "jni/input_manager.h"
#include "jni/ndk_motion.h"
@ -59,58 +69,63 @@
#include "video_core/renderer_base.h"
#if defined(ENABLE_VULKAN) && CITRA_ARCH(arm64)
#include <adrenotools/driver.h>
#endif
namespace {
ANativeWindow* s_surf;
ANativeWindow *s_surf;
ANativeWindow *s_secondary_surface;
bool secondary_enabled = false;
std::shared_ptr<Common::DynamicLibrary> vulkan_library{};
std::unique_ptr<EmuWindow_Android> window;
std::shared_ptr<Common::DynamicLibrary> vulkan_library{};
std::unique_ptr<EmuWindow_Android> window;
std::unique_ptr<EmuWindow_Android> second_window;
std::atomic<bool> stop_run{true};
std::atomic<bool> pause_emulation{false};
std::atomic<bool> stop_run{true};
std::atomic<bool> pause_emulation{false};
std::mutex paused_mutex;
std::mutex running_mutex;
std::condition_variable running_cv;
std::mutex paused_mutex;
std::mutex running_mutex;
std::condition_variable running_cv;
} // Anonymous namespace
static jobject ToJavaCoreError(Core::System::ResultStatus result) {
static const std::map<Core::System::ResultStatus, const char*> CoreErrorNameMap{
{Core::System::ResultStatus::ErrorSystemFiles, "ErrorSystemFiles"},
{Core::System::ResultStatus::ErrorSavestate, "ErrorSavestate"},
{Core::System::ResultStatus::ErrorArticDisconnected, "ErrorArticDisconnected"},
{Core::System::ResultStatus::ErrorUnknown, "ErrorUnknown"},
static const std::map<Core::System::ResultStatus, const char *> CoreErrorNameMap{
{Core::System::ResultStatus::ErrorSystemFiles, "ErrorSystemFiles"},
{Core::System::ResultStatus::ErrorSavestate, "ErrorSavestate"},
{Core::System::ResultStatus::ErrorArticDisconnected, "ErrorArticDisconnected"},
{Core::System::ResultStatus::ErrorUnknown, "ErrorUnknown"},
};
const auto name = CoreErrorNameMap.count(result) ? CoreErrorNameMap.at(result) : "ErrorUnknown";
JNIEnv* env = IDCache::GetEnvForThread();
JNIEnv *env = IDCache::GetEnvForThread();
const jclass core_error_class = IDCache::GetCoreErrorClass();
return env->GetStaticObjectField(
core_error_class, env->GetStaticFieldID(core_error_class, name,
"Lorg/citra/citra_emu/NativeLibrary$CoreError;"));
core_error_class, env->GetStaticFieldID(core_error_class, name,
"Lorg/citra/citra_emu/NativeLibrary$CoreError;"));
}
static bool HandleCoreError(Core::System::ResultStatus result, const std::string& details) {
JNIEnv* env = IDCache::GetEnvForThread();
static bool HandleCoreError(Core::System::ResultStatus result, const std::string &details) {
JNIEnv *env = IDCache::GetEnvForThread();
return env->CallStaticBooleanMethod(IDCache::GetNativeLibraryClass(), IDCache::GetOnCoreError(),
ToJavaCoreError(result),
env->NewStringUTF(details.c_str())) != JNI_FALSE;
}
static void LoadDiskCacheProgress(VideoCore::LoadCallbackStage stage, int progress, int max) {
JNIEnv* env = IDCache::GetEnvForThread();
JNIEnv *env = IDCache::GetEnvForThread();
env->CallStaticVoidMethod(IDCache::GetDiskCacheProgressClass(),
IDCache::GetDiskCacheLoadProgress(),
IDCache::GetJavaLoadCallbackStage(stage), static_cast<jint>(progress),
static_cast<jint>(max));
}
static Camera::NDK::Factory* g_ndk_factory{};
static Camera::NDK::Factory *g_ndk_factory{};
static void TryShutdown() {
if (!window) {
@ -118,8 +133,10 @@ static void TryShutdown() {
}
window->DoneCurrent();
if (second_window) second_window->DoneCurrent();
Core::System::GetInstance().Shutdown();
window.reset();
if (second_window) second_window.reset();
InputManager::Shutdown();
MicroProfileShutdown();
}
@ -129,7 +146,7 @@ static bool CheckMicPermission() {
IDCache::GetRequestMicPermission());
}
static Core::System::ResultStatus RunCitra(const std::string& filepath) {
static Core::System::ResultStatus RunCitra(const std::string &filepath) {
// Citra core only supports a single running instance
std::scoped_lock lock(running_mutex);
@ -142,33 +159,49 @@ static Core::System::ResultStatus RunCitra(const std::string& filepath) {
return Core::System::ResultStatus::ErrorLoader;
}
Core::System& system{Core::System::GetInstance()};
Core::System &system{Core::System::GetInstance()};
const auto graphics_api = Settings::values.graphics_api.GetValue();
switch (graphics_api) {
#ifdef ENABLE_OPENGL
case Settings::GraphicsAPI::OpenGL:
window = std::make_unique<EmuWindow_Android_OpenGL>(system, s_surf);
break;
case Settings::GraphicsAPI::OpenGL:
window = std::make_unique<EmuWindow_Android_OpenGL>(system, s_surf, false);
if (secondary_enabled) {
EGLContext *c = window->GetEGLContext();
second_window = std::make_unique<EmuWindow_Android_OpenGL>(system,
s_secondary_surface,
true, c);
}
break;
#endif
#ifdef ENABLE_VULKAN
case Settings::GraphicsAPI::Vulkan:
window = std::make_unique<EmuWindow_Android_Vulkan>(s_surf, vulkan_library);
break;
case Settings::GraphicsAPI::Vulkan:
window = std::make_unique<EmuWindow_Android_Vulkan>(s_surf, vulkan_library, false);
if (secondary_enabled)
second_window = std::make_unique<EmuWindow_Android_Vulkan>(s_secondary_surface,
vulkan_library, true);
break;
#endif
default:
LOG_CRITICAL(Frontend,
"Unknown or unsupported graphics API {}, falling back to available default",
graphics_api);
default:
LOG_CRITICAL(Frontend,
"Unknown or unsupported graphics API {}, falling back to available default",
graphics_api);
#ifdef ENABLE_OPENGL
window = std::make_unique<EmuWindow_Android_OpenGL>(system, s_surf);
window = std::make_unique<EmuWindow_Android_OpenGL>(system, s_surf, false);
if (secondary_enabled) {
EGLContext *c = window->GetEGLContext();
second_window = std::make_unique<EmuWindow_Android_OpenGL>(system,
s_secondary_surface,
true, c);
}
#elif ENABLE_VULKAN
window = std::make_unique<EmuWindow_Android_Vulkan>(s_surf, vulkan_library);
window = std::make_unique<EmuWindow_Android_Vulkan>(s_surf, vulkan_library);
if (secondary_enabled) second_window = std::make_unique<EmuWindow_Android_Vulkan>(s_secondary_surface, vulkan_library, true);
#else
// TODO: Add a null renderer backend for this, perhaps.
// TODO: Add a null renderer backend for this, perhaps.
#error "At least one renderer must be enabled."
#endif
break;
break;
}
// Forces a config reload on game boot, if the user changed settings in the UI
@ -202,7 +235,8 @@ static Core::System::ResultStatus RunCitra(const std::string& filepath) {
InputManager::Init();
window->MakeCurrent();
const Core::System::ResultStatus load_result{system.Load(*window, filepath)};
const Core::System::ResultStatus load_result{
system.Load(*window, filepath, second_window.get())};
if (load_result != Core::System::ResultStatus::Success) {
return load_result;
}
@ -249,18 +283,19 @@ static Core::System::ResultStatus RunCitra(const std::string& filepath) {
std::unique_lock pause_lock{paused_mutex};
running_cv.wait(pause_lock, [] { return !pause_emulation || stop_run; });
window->PollEvents();
//if (second_window) second_window->PollEvents();
}
}
return Core::System::ResultStatus::Success;
}
void InitializeGpuDriver(const std::string& hook_lib_dir, const std::string& custom_driver_dir,
const std::string& custom_driver_name,
const std::string& file_redirect_dir) {
void InitializeGpuDriver(const std::string &hook_lib_dir, const std::string &custom_driver_dir,
const std::string &custom_driver_name,
const std::string &file_redirect_dir) {
#if defined(ENABLE_VULKAN) && CITRA_ARCH(arm64)
void* handle{};
const char* file_redirect_dir_{};
void *handle{};
const char *file_redirect_dir_{};
int featureFlags{};
// Enable driver file redirection when renderer debugging is enabled.
@ -272,8 +307,8 @@ void InitializeGpuDriver(const std::string& hook_lib_dir, const std::string& cus
// Try to load a custom driver.
if (custom_driver_name.size()) {
handle = adrenotools_open_libvulkan(
RTLD_NOW, featureFlags | ADRENOTOOLS_DRIVER_CUSTOM, nullptr, hook_lib_dir.c_str(),
custom_driver_dir.c_str(), custom_driver_name.c_str(), file_redirect_dir_, nullptr);
RTLD_NOW, featureFlags | ADRENOTOOLS_DRIVER_CUSTOM, nullptr, hook_lib_dir.c_str(),
custom_driver_dir.c_str(), custom_driver_name.c_str(), file_redirect_dir_, nullptr);
}
// Try to load the system driver.
@ -288,7 +323,7 @@ void InitializeGpuDriver(const std::string& hook_lib_dir, const std::string& cus
extern "C" {
void Java_org_citra_citra_1emu_NativeLibrary_surfaceChanged(JNIEnv* env,
void Java_org_citra_citra_1emu_NativeLibrary_surfaceChanged(JNIEnv *env,
[[maybe_unused]] jobject obj,
jobject surf) {
s_surf = ANativeWindow_fromSurface(env, surf);
@ -298,15 +333,86 @@ void Java_org_citra_citra_1emu_NativeLibrary_surfaceChanged(JNIEnv* env,
notify = window->OnSurfaceChanged(s_surf);
}
auto& system = Core::System::GetInstance();
auto &system = Core::System::GetInstance();
if (notify && system.IsPoweredOn()) {
system.GPU().Renderer().NotifySurfaceChanged();
system.GPU().Renderer().NotifySurfaceChanged(false);
}
LOG_INFO(Frontend, "Surface changed");
}
void Java_org_citra_citra_1emu_NativeLibrary_surfaceDestroyed([[maybe_unused]] JNIEnv* env,
void Java_org_citra_citra_1emu_NativeLibrary_secondarySurfaceChanged(JNIEnv *env,
[[maybe_unused]] jobject obj,
jobject surf) {
auto &system = Core::System::GetInstance();
s_secondary_surface = ANativeWindow_fromSurface(env, surf);
secondary_enabled = true;
bool notify = false;
if (!s_secondary_surface) {
// did not create the surface, so disable second screen
secondary_enabled = false;
if (system.IsPoweredOn()) {
system.GPU().Renderer().setSecondaryWindow(nullptr);
}
return;
}
if (second_window) {
//second window already created, so update it
notify = second_window->OnSurfaceChanged(s_secondary_surface);
} else if (system.IsPoweredOn() && window) {
// emulation running, window is new
// create a new window and set it
const auto graphics_api = Settings::values.graphics_api.GetValue();
if (graphics_api == Settings::GraphicsAPI::OpenGL) {
EGLContext *c = window->GetEGLContext();
second_window = std::make_unique<EmuWindow_Android_OpenGL>(system,
s_secondary_surface,true, c);
}else{
second_window = std::make_unique<EmuWindow_Android_Vulkan>(s_secondary_surface,
vulkan_library, true);
}
system.GPU().Renderer().setSecondaryWindow(second_window.get());
}
if (notify && system.IsPoweredOn()) {
system.GPU().Renderer().NotifySurfaceChanged(true);
}
LOG_INFO(Frontend, "Secondary Surface changed");
}
void Java_org_citra_citra_1emu_NativeLibrary_secondarySurfaceDestroyed(JNIEnv *env,
[[maybe_unused]] jobject obj) {
//auto &system = Core::System::GetInstance();
secondary_enabled = false;
if (s_secondary_surface != nullptr) {
ANativeWindow_release(s_secondary_surface);
s_secondary_surface = nullptr;
}
LOG_INFO(Frontend, "Secondary Surface Destroyed");
}
void Java_org_citra_citra_1emu_NativeLibrary_disableSecondaryScreen(JNIEnv *env,
[[maybe_unused]] jobject obj) {
auto &system = Core::System::GetInstance();
secondary_enabled = false;
if (s_secondary_surface != nullptr) {
ANativeWindow_release(s_secondary_surface);
s_secondary_surface = nullptr;
}
if (system.IsPoweredOn()) {
system.GPU().Renderer().setSecondaryWindow(nullptr);
}
if (second_window) {
second_window.release();
second_window = nullptr;
}
LOG_INFO(Frontend, "Secondary Window Disabled");
}
void Java_org_citra_citra_1emu_NativeLibrary_surfaceDestroyed([[maybe_unused]] JNIEnv *env,
[[maybe_unused]] jobject obj) {
if (s_surf != nullptr) {
ANativeWindow_release(s_surf);
@ -317,7 +423,7 @@ void Java_org_citra_citra_1emu_NativeLibrary_surfaceDestroyed([[maybe_unused]] J
}
}
void Java_org_citra_citra_1emu_NativeLibrary_doFrame([[maybe_unused]] JNIEnv* env,
void Java_org_citra_citra_1emu_NativeLibrary_doFrame([[maybe_unused]] JNIEnv *env,
[[maybe_unused]] jobject obj) {
if (stop_run || pause_emulation) {
return;
@ -325,36 +431,39 @@ void Java_org_citra_citra_1emu_NativeLibrary_doFrame([[maybe_unused]] JNIEnv* en
if (window) {
window->TryPresenting();
}
if (second_window) {
second_window->TryPresenting();
}
}
void JNICALL Java_org_citra_citra_1emu_NativeLibrary_initializeGpuDriver(
JNIEnv* env, jobject obj, jstring hook_lib_dir, jstring custom_driver_dir,
jstring custom_driver_name, jstring file_redirect_dir) {
JNIEnv *env, jobject obj, jstring hook_lib_dir, jstring custom_driver_dir,
jstring custom_driver_name, jstring file_redirect_dir) {
InitializeGpuDriver(GetJString(env, hook_lib_dir), GetJString(env, custom_driver_dir),
GetJString(env, custom_driver_name), GetJString(env, file_redirect_dir));
}
void Java_org_citra_citra_1emu_NativeLibrary_notifyOrientationChange([[maybe_unused]] JNIEnv* env,
void Java_org_citra_citra_1emu_NativeLibrary_notifyOrientationChange([[maybe_unused]] JNIEnv *env,
[[maybe_unused]] jobject obj,
jint layout_option,
jint rotation,
jboolean portrait) {
Settings::values.layout_option = static_cast<Settings::LayoutOption>(layout_option);
}
void Java_org_citra_citra_1emu_NativeLibrary_updateFramebuffer([[maybe_unused]] JNIEnv* env,
void Java_org_citra_citra_1emu_NativeLibrary_updateFramebuffer([[maybe_unused]] JNIEnv *env,
[[maybe_unused]] jobject obj,
jboolean is_portrait_mode) {
auto& system = Core::System::GetInstance();
auto &system = Core::System::GetInstance();
if (system.IsPoweredOn()) {
system.GPU().Renderer().UpdateCurrentFramebufferLayout(is_portrait_mode);
}
}
void Java_org_citra_citra_1emu_NativeLibrary_swapScreens([[maybe_unused]] JNIEnv* env,
void Java_org_citra_citra_1emu_NativeLibrary_swapScreens([[maybe_unused]] JNIEnv *env,
[[maybe_unused]] jobject obj,
jboolean swap_screens, jint rotation) {
Settings::values.swap_screen = swap_screens;
auto& system = Core::System::GetInstance();
auto &system = Core::System::GetInstance();
if (system.IsPoweredOn()) {
system.GPU().Renderer().UpdateCurrentFramebufferLayout(IsPortraitMode());
}
@ -362,14 +471,14 @@ void Java_org_citra_citra_1emu_NativeLibrary_swapScreens([[maybe_unused]] JNIEnv
Camera::NDK::g_rotation = rotation;
}
jboolean Java_org_citra_citra_1emu_NativeLibrary_areKeysAvailable([[maybe_unused]] JNIEnv* env,
jboolean Java_org_citra_citra_1emu_NativeLibrary_areKeysAvailable([[maybe_unused]] JNIEnv *env,
[[maybe_unused]] jobject obj) {
HW::AES::InitKeys();
return HW::AES::IsKeyXAvailable(HW::AES::KeySlotID::NCCHSecure1) &&
HW::AES::IsKeyXAvailable(HW::AES::KeySlotID::NCCHSecure2);
}
jstring Java_org_citra_citra_1emu_NativeLibrary_getHomeMenuPath(JNIEnv* env,
jstring Java_org_citra_citra_1emu_NativeLibrary_getHomeMenuPath(JNIEnv *env,
[[maybe_unused]] jobject obj,
jint region) {
const std::string path = Core::GetHomeMenuNcchPath(region);
@ -379,43 +488,44 @@ jstring Java_org_citra_citra_1emu_NativeLibrary_getHomeMenuPath(JNIEnv* env,
return ToJString(env, "");
}
void Java_org_citra_citra_1emu_NativeLibrary_setUserDirectory(JNIEnv* env,
void Java_org_citra_citra_1emu_NativeLibrary_setUserDirectory(JNIEnv *env,
[[maybe_unused]] jobject obj,
jstring j_directory) {
FileUtil::SetCurrentDir(GetJString(env, j_directory));
}
jobjectArray Java_org_citra_citra_1emu_NativeLibrary_getInstalledGamePaths(
JNIEnv* env, [[maybe_unused]] jclass clazz) {
JNIEnv *env, [[maybe_unused]] jclass clazz) {
std::vector<std::string> games;
const FileUtil::DirectoryEntryCallable ScanDir =
[&games, &ScanDir](u64*, const std::string& directory, const std::string& virtual_name) {
std::string path = directory + virtual_name;
if (FileUtil::IsDirectory(path)) {
path += '/';
FileUtil::ForeachDirectoryEntry(nullptr, path, ScanDir);
} else {
if (!FileUtil::Exists(path))
return false;
auto loader = Loader::GetLoader(path);
if (loader) {
bool executable{};
const Loader::ResultStatus result = loader->IsExecutable(executable);
if (Loader::ResultStatus::Success == result && executable) {
games.emplace_back(path);
[&games, &ScanDir](u64 *, const std::string &directory,
const std::string &virtual_name) {
std::string path = directory + virtual_name;
if (FileUtil::IsDirectory(path)) {
path += '/';
FileUtil::ForeachDirectoryEntry(nullptr, path, ScanDir);
} else {
if (!FileUtil::Exists(path))
return false;
auto loader = Loader::GetLoader(path);
if (loader) {
bool executable{};
const Loader::ResultStatus result = loader->IsExecutable(executable);
if (Loader::ResultStatus::Success == result && executable) {
games.emplace_back(path);
}
}
}
}
return true;
};
return true;
};
ScanDir(nullptr, "",
FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir) +
"Nintendo "
"3DS/00000000000000000000000000000000/"
"00000000000000000000000000000000/title/00040000");
"Nintendo "
"3DS/00000000000000000000000000000000/"
"00000000000000000000000000000000/title/00040000");
ScanDir(nullptr, "",
FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) +
"00000000000000000000000000000000/title/00040010");
"00000000000000000000000000000000/title/00040010");
jobjectArray jgames = env->NewObjectArray(static_cast<jsize>(games.size()),
env->FindClass("java/lang/String"), nullptr);
for (jsize i = 0; i < games.size(); ++i)
@ -423,7 +533,7 @@ jobjectArray Java_org_citra_citra_1emu_NativeLibrary_getInstalledGamePaths(
return jgames;
}
jlongArray Java_org_citra_citra_1emu_NativeLibrary_getSystemTitleIds(JNIEnv* env,
jlongArray Java_org_citra_citra_1emu_NativeLibrary_getSystemTitleIds(JNIEnv *env,
[[maybe_unused]] jobject obj,
jint system_type,
jint region) {
@ -431,11 +541,11 @@ jlongArray Java_org_citra_citra_1emu_NativeLibrary_getSystemTitleIds(JNIEnv* env
const std::vector<u64> titles = Core::GetSystemTitleIds(mode, region);
jlongArray jTitles = env->NewLongArray(titles.size());
env->SetLongArrayRegion(jTitles, 0, titles.size(),
reinterpret_cast<const jlong*>(titles.data()));
reinterpret_cast<const jlong *>(titles.data()));
return jTitles;
}
jobject Java_org_citra_citra_1emu_NativeLibrary_downloadTitleFromNus([[maybe_unused]] JNIEnv* env,
jobject Java_org_citra_citra_1emu_NativeLibrary_downloadTitleFromNus([[maybe_unused]] JNIEnv *env,
[[maybe_unused]] jobject obj,
jlong title) {
[[maybe_unused]] const auto title_id = static_cast<u64>(title);
@ -453,7 +563,7 @@ jobject Java_org_citra_citra_1emu_NativeLibrary_downloadTitleFromNus([[maybe_unu
}
jboolean JNICALL Java_org_citra_citra_1emu_utils_GpuDriverHelper_supportsCustomDriverLoading(
JNIEnv* env, jobject instance) {
JNIEnv *env, jobject instance) {
#ifdef CITRA_ARCH_arm64
// If the KGSL device exists custom drivers can be loaded using adrenotools
return SupportsCustomDriver();
@ -463,40 +573,41 @@ jboolean JNICALL Java_org_citra_citra_1emu_utils_GpuDriverHelper_supportsCustomD
}
// TODO(xperia64): ensure these cannot be called in an invalid state (e.g. after StopEmulation)
void Java_org_citra_citra_1emu_NativeLibrary_unPauseEmulation([[maybe_unused]] JNIEnv* env,
void Java_org_citra_citra_1emu_NativeLibrary_unPauseEmulation([[maybe_unused]] JNIEnv *env,
[[maybe_unused]] jobject obj) {
pause_emulation = false;
running_cv.notify_all();
InputManager::NDKMotionHandler()->EnableSensors();
}
void Java_org_citra_citra_1emu_NativeLibrary_pauseEmulation([[maybe_unused]] JNIEnv* env,
void Java_org_citra_citra_1emu_NativeLibrary_pauseEmulation([[maybe_unused]] JNIEnv *env,
[[maybe_unused]] jobject obj) {
pause_emulation = true;
InputManager::NDKMotionHandler()->DisableSensors();
}
void Java_org_citra_citra_1emu_NativeLibrary_stopEmulation([[maybe_unused]] JNIEnv* env,
void Java_org_citra_citra_1emu_NativeLibrary_stopEmulation([[maybe_unused]] JNIEnv *env,
[[maybe_unused]] jobject obj) {
stop_run = true;
pause_emulation = false;
window->StopPresenting();
if (second_window) second_window->StopPresenting();
running_cv.notify_all();
}
jboolean Java_org_citra_citra_1emu_NativeLibrary_isRunning([[maybe_unused]] JNIEnv* env,
jboolean Java_org_citra_citra_1emu_NativeLibrary_isRunning([[maybe_unused]] JNIEnv *env,
[[maybe_unused]] jobject obj) {
return static_cast<jboolean>(!stop_run);
}
jlong Java_org_citra_citra_1emu_NativeLibrary_getRunningTitleId([[maybe_unused]] JNIEnv* env,
jlong Java_org_citra_citra_1emu_NativeLibrary_getRunningTitleId([[maybe_unused]] JNIEnv *env,
[[maybe_unused]] jobject obj) {
u64 title_id{};
Core::System::GetInstance().GetAppLoader().ReadProgramId(title_id);
return static_cast<jlong>(title_id);
}
jboolean Java_org_citra_citra_1emu_NativeLibrary_onGamePadEvent([[maybe_unused]] JNIEnv* env,
jboolean Java_org_citra_citra_1emu_NativeLibrary_onGamePadEvent([[maybe_unused]] JNIEnv *env,
[[maybe_unused]] jobject obj,
[[maybe_unused]] jstring j_device,
jint j_button, jint action) {
@ -511,8 +622,9 @@ jboolean Java_org_citra_citra_1emu_NativeLibrary_onGamePadEvent([[maybe_unused]]
}
jboolean Java_org_citra_citra_1emu_NativeLibrary_onGamePadMoveEvent(
[[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj, [[maybe_unused]] jstring j_device,
jint axis, jfloat x, jfloat y) {
[[maybe_unused]] JNIEnv *env, [[maybe_unused]] jobject obj,
[[maybe_unused]] jstring j_device,
jint axis, jfloat x, jfloat y) {
// Clamp joystick movement to supported minimum and maximum
// Citra uses an inverted y axis sent by the frontend
x = std::clamp(x, -1.f, 1.f);
@ -530,27 +642,28 @@ jboolean Java_org_citra_citra_1emu_NativeLibrary_onGamePadMoveEvent(
}
jboolean Java_org_citra_citra_1emu_NativeLibrary_onGamePadAxisEvent(
[[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj, [[maybe_unused]] jstring j_device,
jint axis_id, jfloat axis_val) {
[[maybe_unused]] JNIEnv *env, [[maybe_unused]] jobject obj,
[[maybe_unused]] jstring j_device,
jint axis_id, jfloat axis_val) {
return static_cast<jboolean>(
InputManager::ButtonHandler()->AnalogButtonEvent(axis_id, axis_val));
InputManager::ButtonHandler()->AnalogButtonEvent(axis_id, axis_val));
}
jboolean Java_org_citra_citra_1emu_NativeLibrary_onTouchEvent([[maybe_unused]] JNIEnv* env,
jboolean Java_org_citra_citra_1emu_NativeLibrary_onTouchEvent([[maybe_unused]] JNIEnv *env,
[[maybe_unused]] jobject obj,
jfloat x, jfloat y,
jboolean pressed) {
return static_cast<jboolean>(
window->OnTouchEvent(static_cast<int>(x + 0.5), static_cast<int>(y + 0.5), pressed));
window->OnTouchEvent(static_cast<int>(x + 0.5), static_cast<int>(y + 0.5), pressed));
}
void Java_org_citra_citra_1emu_NativeLibrary_onTouchMoved([[maybe_unused]] JNIEnv* env,
void Java_org_citra_citra_1emu_NativeLibrary_onTouchMoved([[maybe_unused]] JNIEnv *env,
[[maybe_unused]] jobject obj, jfloat x,
jfloat y) {
window->OnTouchMoved((int)x, (int)y);
window->OnTouchMoved((int) x, (int) y);
}
jlong Java_org_citra_citra_1emu_NativeLibrary_getTitleId(JNIEnv* env, [[maybe_unused]] jobject obj,
jlong Java_org_citra_citra_1emu_NativeLibrary_getTitleId(JNIEnv *env, [[maybe_unused]] jobject obj,
jstring j_filename) {
std::string filepath = GetJString(env, j_filename);
const auto loader = Loader::GetLoader(filepath);
@ -562,7 +675,7 @@ jlong Java_org_citra_citra_1emu_NativeLibrary_getTitleId(JNIEnv* env, [[maybe_un
return static_cast<jlong>(title_id);
}
jboolean Java_org_citra_citra_1emu_NativeLibrary_getIsSystemTitle(JNIEnv* env,
jboolean Java_org_citra_citra_1emu_NativeLibrary_getIsSystemTitle(JNIEnv *env,
[[maybe_unused]] jobject obj,
jstring path) {
const std::string filepath = GetJString(env, path);
@ -578,19 +691,19 @@ jboolean Java_org_citra_citra_1emu_NativeLibrary_getIsSystemTitle(JNIEnv* env,
return ((program_id >> 32) & 0xFFFFFFFF) == 0x00040010;
}
void Java_org_citra_citra_1emu_NativeLibrary_createConfigFile([[maybe_unused]] JNIEnv* env,
void Java_org_citra_citra_1emu_NativeLibrary_createConfigFile([[maybe_unused]] JNIEnv *env,
[[maybe_unused]] jobject obj) {
Config{};
}
void Java_org_citra_citra_1emu_NativeLibrary_createLogFile([[maybe_unused]] JNIEnv* env,
void Java_org_citra_citra_1emu_NativeLibrary_createLogFile([[maybe_unused]] JNIEnv *env,
[[maybe_unused]] jobject obj) {
Common::Log::Initialize();
Common::Log::Start();
LOG_INFO(Frontend, "Logging backend initialised");
}
void Java_org_citra_citra_1emu_NativeLibrary_logUserDirectory(JNIEnv* env,
void Java_org_citra_citra_1emu_NativeLibrary_logUserDirectory(JNIEnv *env,
[[maybe_unused]] jobject obj,
jstring j_path) {
std::string_view path = env->GetStringUTFChars(j_path, 0);
@ -598,10 +711,10 @@ void Java_org_citra_citra_1emu_NativeLibrary_logUserDirectory(JNIEnv* env,
env->ReleaseStringUTFChars(j_path, path.data());
}
void Java_org_citra_citra_1emu_NativeLibrary_reloadSettings([[maybe_unused]] JNIEnv* env,
void Java_org_citra_citra_1emu_NativeLibrary_reloadSettings([[maybe_unused]] JNIEnv *env,
[[maybe_unused]] jobject obj) {
Config{};
Core::System& system{Core::System::GetInstance()};
Core::System &system{Core::System::GetInstance()};
// Replace with game-specific settings
if (system.IsPoweredOn()) {
@ -612,9 +725,9 @@ void Java_org_citra_citra_1emu_NativeLibrary_reloadSettings([[maybe_unused]] JNI
system.ApplySettings();
}
jdoubleArray Java_org_citra_citra_1emu_NativeLibrary_getPerfStats(JNIEnv* env,
jdoubleArray Java_org_citra_citra_1emu_NativeLibrary_getPerfStats(JNIEnv *env,
[[maybe_unused]] jobject obj) {
auto& core = Core::System::GetInstance();
auto &core = Core::System::GetInstance();
jdoubleArray j_stats = env->NewDoubleArray(4);
if (core.IsPoweredOn()) {
@ -630,7 +743,7 @@ jdoubleArray Java_org_citra_citra_1emu_NativeLibrary_getPerfStats(JNIEnv* env,
return j_stats;
}
void Java_org_citra_citra_1emu_NativeLibrary_run__Ljava_lang_String_2(JNIEnv* env,
void Java_org_citra_citra_1emu_NativeLibrary_run__Ljava_lang_String_2(JNIEnv *env,
[[maybe_unused]] jobject obj,
jstring j_path) {
const std::string path = GetJString(env, j_path);
@ -647,19 +760,19 @@ void Java_org_citra_citra_1emu_NativeLibrary_run__Ljava_lang_String_2(JNIEnv* en
}
}
void Java_org_citra_citra_1emu_NativeLibrary_reloadCameraDevices([[maybe_unused]] JNIEnv* env,
void Java_org_citra_citra_1emu_NativeLibrary_reloadCameraDevices([[maybe_unused]] JNIEnv *env,
[[maybe_unused]] jobject obj) {
if (g_ndk_factory) {
g_ndk_factory->ReloadCameraDevices();
}
}
jboolean Java_org_citra_citra_1emu_NativeLibrary_loadAmiibo(JNIEnv* env,
jboolean Java_org_citra_citra_1emu_NativeLibrary_loadAmiibo(JNIEnv *env,
[[maybe_unused]] jobject obj,
jstring j_file) {
std::string filepath = GetJString(env, j_file);
Core::System& system{Core::System::GetInstance()};
Service::SM::ServiceManager& sm = system.ServiceManager();
Core::System &system{Core::System::GetInstance()};
Service::SM::ServiceManager &sm = system.ServiceManager();
auto nfc = sm.GetService<Service::NFC::Module::Interface>("nfc:u");
if (nfc == nullptr) {
return static_cast<jboolean>(false);
@ -668,10 +781,10 @@ jboolean Java_org_citra_citra_1emu_NativeLibrary_loadAmiibo(JNIEnv* env,
return static_cast<jboolean>(nfc->LoadAmiibo(filepath));
}
void Java_org_citra_citra_1emu_NativeLibrary_removeAmiibo([[maybe_unused]] JNIEnv* env,
void Java_org_citra_citra_1emu_NativeLibrary_removeAmiibo([[maybe_unused]] JNIEnv *env,
[[maybe_unused]] jobject obj) {
Core::System& system{Core::System::GetInstance()};
Service::SM::ServiceManager& sm = system.ServiceManager();
Core::System &system{Core::System::GetInstance()};
Service::SM::ServiceManager &sm = system.ServiceManager();
auto nfc = sm.GetService<Service::NFC::Module::Interface>("nfc:u");
if (nfc == nullptr) {
return;
@ -681,19 +794,20 @@ void Java_org_citra_citra_1emu_NativeLibrary_removeAmiibo([[maybe_unused]] JNIEn
}
JNIEXPORT jobject JNICALL Java_org_citra_citra_1emu_utils_CiaInstallWorker_installCIA(
JNIEnv* env, jobject jobj, jstring jpath) {
JNIEnv *env, jobject jobj, jstring jpath) {
std::string path = GetJString(env, jpath);
Service::AM::InstallStatus res = Service::AM::InstallCIA(
path, [env, jobj](std::size_t total_bytes_read, std::size_t file_size) {
env->CallVoidMethod(jobj, IDCache::GetCiaInstallHelperSetProgress(),
static_cast<jint>(file_size), static_cast<jint>(total_bytes_read));
});
path, [env, jobj](std::size_t total_bytes_read, std::size_t file_size) {
env->CallVoidMethod(jobj, IDCache::GetCiaInstallHelperSetProgress(),
static_cast<jint>(file_size),
static_cast<jint>(total_bytes_read));
});
return IDCache::GetJavaCiaInstallStatus(res);
}
jobjectArray Java_org_citra_citra_1emu_NativeLibrary_getSavestateInfo(
JNIEnv* env, [[maybe_unused]] jobject obj) {
JNIEnv *env, [[maybe_unused]] jobject obj) {
const jclass date_class = env->FindClass("java/util/Date");
const auto date_constructor = env->GetMethodID(date_class, "<init>", "(J)V");
@ -701,7 +815,7 @@ jobjectArray Java_org_citra_citra_1emu_NativeLibrary_getSavestateInfo(
const auto slot_field = env->GetFieldID(savestate_info_class, "slot", "I");
const auto date_field = env->GetFieldID(savestate_info_class, "time", "Ljava/util/Date;");
const Core::System& system{Core::System::GetInstance()};
const Core::System &system{Core::System::GetInstance()};
if (!system.IsPoweredOn()) {
return nullptr;
}
@ -713,7 +827,8 @@ jobjectArray Java_org_citra_citra_1emu_NativeLibrary_getSavestateInfo(
const auto savestates = Core::ListSaveStates(title_id, system.Movie().GetCurrentMovieID());
const jobjectArray array =
env->NewObjectArray(static_cast<jsize>(savestates.size()), savestate_info_class, nullptr);
env->NewObjectArray(static_cast<jsize>(savestates.size()), savestate_info_class,
nullptr);
for (std::size_t i = 0; i < savestates.size(); ++i) {
const jobject object = env->AllocObject(savestate_info_class);
env->SetIntField(object, slot_field, static_cast<jint>(savestates[i].slot));
@ -726,17 +841,17 @@ jobjectArray Java_org_citra_citra_1emu_NativeLibrary_getSavestateInfo(
return array;
}
void Java_org_citra_citra_1emu_NativeLibrary_saveState([[maybe_unused]] JNIEnv* env,
void Java_org_citra_citra_1emu_NativeLibrary_saveState([[maybe_unused]] JNIEnv *env,
[[maybe_unused]] jobject obj, jint slot) {
Core::System::GetInstance().SendSignal(Core::System::Signal::Save, slot);
}
void Java_org_citra_citra_1emu_NativeLibrary_loadState([[maybe_unused]] JNIEnv* env,
void Java_org_citra_citra_1emu_NativeLibrary_loadState([[maybe_unused]] JNIEnv *env,
[[maybe_unused]] jobject obj, jint slot) {
Core::System::GetInstance().SendSignal(Core::System::Signal::Load, slot);
}
void Java_org_citra_citra_1emu_NativeLibrary_logDeviceInfo([[maybe_unused]] JNIEnv* env,
void Java_org_citra_citra_1emu_NativeLibrary_logDeviceInfo([[maybe_unused]] JNIEnv *env,
[[maybe_unused]] jobject obj) {
LOG_INFO(Frontend, "Azahar Version: {} | {}-{}", Common::g_build_fullname, Common::g_scm_branch,
Common::g_scm_desc);
@ -745,4 +860,4 @@ void Java_org_citra_citra_1emu_NativeLibrary_logDeviceInfo([[maybe_unused]] JNIE
LOG_INFO(Frontend, "Host OS: Android API level {}", android_get_device_api_level());
}
} // extern "C"
}

View File

@ -34,11 +34,25 @@
<item>@string/emulation_screen_layout_custom</item>
</string-array>
<string-array name="secondaryLayouts">
<item>@string/emulation_secondary_screen_default</item>
<item>@string/emulation_top_screen</item>
<item>@string/emulation_bottom_screen</item>
<item>@string/emulation_screen_layout_sidebyside</item>
</string-array>
<integer-array name="portraitLayoutValues">
<item>0</item>
<item>1</item>
</integer-array>
<integer-array name="secondaryLayoutValues">
<item>0</item>
<item>1</item>
<item>2</item>
<item>3</item>
</integer-array>
<string-array name="smallScreenPositions">
<item>@string/small_screen_position_top_right</item>
<item>@string/small_screen_position_middle_right</item>

View File

@ -373,6 +373,7 @@
<string name="emulation_open_cheats">Open Cheats</string>
<string name="emulation_switch_screen_layout">Landscape Screen Layout</string>
<string name="emulation_switch_portrait_layout">Portrait Screen Layout</string>
<string name="emulation_switch_secondary_layout">Secondary Screen Layout</string>
<string name="emulation_screen_layout_largescreen">Large Screen</string>
<string name="emulation_screen_layout_portrait">Portrait</string>
<string name="emulation_screen_layout_single">Single Screen</string>
@ -380,6 +381,7 @@
<string name="emulation_screen_layout_hybrid">Hybrid Screens</string>
<string name="emulation_screen_layout_original">Original</string>
<string name="emulation_portrait_layout_top_full">Default</string>
<string name="emulation_secondary_screen_default">System Default (mirror)</string>
<string name="emulation_screen_layout_custom">Custom Layout</string>
<string name="emulation_small_screen_position">Small Screen Position</string>
<string name="small_screen_position_description">Where should the small screen appear relative to the large one in Large Screen Layout?</string>

View File

@ -113,6 +113,7 @@ void LogSettings() {
}
log_setting("Layout_LayoutOption", values.layout_option.GetValue());
log_setting("Layout_PortraitLayoutOption", values.portrait_layout_option.GetValue());
log_setting("Layout_SecondScreenLayout",values.secondary_screen_layout.GetValue());
log_setting("Layout_SwapScreen", values.swap_screen.GetValue());
log_setting("Layout_UprightScreen", values.upright_screen.GetValue());
log_setting("Layout_LargeScreenProportion", values.large_screen_proportion.GetValue());
@ -205,6 +206,7 @@ void RestoreGlobalState(bool is_powered_on) {
values.delay_game_render_thread_us.SetGlobal(true);
values.layout_option.SetGlobal(true);
values.portrait_layout_option.SetGlobal(true);
values.secondary_screen_layout.SetGlobal(true);
values.swap_screen.SetGlobal(true);
values.upright_screen.SetGlobal(true);
values.large_screen_proportion.SetGlobal(true);

View File

@ -53,6 +53,12 @@ enum class PortraitLayoutOption : u32 {
PortraitCustomLayout,
};
enum class SecondaryScreenLayout : u32 {
None,
TopScreenOnly,
BottomScreenOnly,
SideBySide
};
/** Defines where the small screen will appear relative to the large screen
* when in Large Screen mode
*/
@ -503,6 +509,7 @@ struct Values {
SwitchableSetting<LayoutOption> layout_option{LayoutOption::Default, "layout_option"};
SwitchableSetting<bool> swap_screen{false, "swap_screen"};
SwitchableSetting<bool> upright_screen{false, "upright_screen"};
SwitchableSetting<SecondaryScreenLayout> secondary_screen_layout{SecondaryScreenLayout::None, "secondary_screen_layout"};
SwitchableSetting<float, true> large_screen_proportion{4.f, 1.f, 16.f,
"large_screen_proportion"};
SwitchableSetting<SmallScreenPosition> small_screen_position{SmallScreenPosition::BottomRight,

View File

@ -251,6 +251,9 @@ void EmuWindow::UpdateCurrentFramebufferLayout(u32 width, u32 height, bool is_po
break;
}
}
#ifdef ANDROID
if (is_secondary) layout = Layout::AndroidSecondaryLayout(width,height);
#endif
UpdateMinimumWindowSize(min_size);
if (Settings::values.render_3d.GetValue() == Settings::StereoRenderOption::CardboardVR) {

View File

@ -71,7 +71,7 @@ FramebufferLayout SingleFrameLayout(u32 width, u32 height, bool swapped, bool up
const bool stretched = (Settings::values.screen_top_stretch.GetValue() && !swapped) ||
(Settings::values.screen_bottom_stretch.GetValue() && swapped);
const float window_aspect_ratio = static_cast<float>(height) / width;
const float window_aspect_ratio = static_cast<float>(height) / static_cast<float>(width);
if (stretched) {
top_screen = {Settings::values.screen_top_leftright_padding.GetValue(),
@ -113,7 +113,7 @@ FramebufferLayout LargeFrameLayout(u32 width, u32 height, bool swapped, bool upr
FramebufferLayout res{width, height, true, true, {}, {}, !upright};
// Split the window into two parts. Give proportional width to the smaller screen
// To do that, find the total emulation box and maximize that based on window size
const float window_aspect_ratio = static_cast<float>(height) / width;
const float window_aspect_ratio = static_cast<float>(height) / static_cast<float>(width);
float emulation_aspect_ratio;
float large_height =
@ -297,6 +297,22 @@ FramebufferLayout SeparateWindowsLayout(u32 width, u32 height, bool is_secondary
return SingleFrameLayout(width, height, is_secondary, upright);
}
FramebufferLayout AndroidSecondaryLayout(u32 width, u32 height) {
const Settings::SecondaryScreenLayout layout = Settings::values.secondary_screen_layout.GetValue();
switch (layout) {
case Settings::SecondaryScreenLayout::BottomScreenOnly:
return SingleFrameLayout(width, height, true, Settings::values.upright_screen.GetValue());
case Settings::SecondaryScreenLayout::SideBySide:
return LargeFrameLayout(width,height,false,Settings::values.upright_screen.GetValue(),1.0f,Settings::SmallScreenPosition::MiddleRight);
case Settings::SecondaryScreenLayout::None:
// this should never happen, but if it does, somehow, send the top screen
case Settings::SecondaryScreenLayout::TopScreenOnly:
default:
return SingleFrameLayout(width, height, false, Settings::values.upright_screen.GetValue());
}
}
FramebufferLayout CustomFrameLayout(u32 width, u32 height, bool is_swapped, bool is_portrait_mode) {
ASSERT(width > 0);
ASSERT(height > 0);

View File

@ -119,6 +119,16 @@ FramebufferLayout HybridScreenLayout(u32 width, u32 height, bool swapped, bool u
*/
FramebufferLayout SeparateWindowsLayout(u32 width, u32 height, bool is_secondary, bool upright);
/**
* Method for constructing the secondary layout for Android, based on
* the appropriate setting.
* @param width Window framebuffer width in pixels
* @param height Window framebuffer height in pixels
*/
FramebufferLayout AndroidSecondaryLayout(u32 width, u32 height);
/**
* Factory method for constructing a custom FramebufferLayout
* @param width Window framebuffer width in pixels

View File

@ -35,7 +35,7 @@ void RendererBase::UpdateCurrentFramebufferLayout(bool is_portrait_mode) {
window.UpdateCurrentFramebufferLayout(layout.width, layout.height, is_portrait_mode);
};
update_layout(render_window);
if (secondary_window) {
if (secondary_window != nullptr) {
update_layout(*secondary_window);
}
}
@ -67,4 +67,13 @@ void RendererBase::RequestScreenshot(void* data, std::function<void(bool)> callb
settings.screenshot_requested = true;
}
Frontend::EmuWindow *RendererBase::getSecondaryWindow() const {
return secondary_window;
}
void RendererBase::setSecondaryWindow(Frontend::EmuWindow *secondaryWindow) {
secondary_window = secondaryWindow;
if (secondary_window) secondary_window->PollEvents();
}
} // namespace VideoCore

View File

@ -64,7 +64,8 @@ public:
virtual void Sync() {}
/// This is called to notify the rendering backend of a surface change
virtual void NotifySurfaceChanged() {}
// if second == true then it is the second screen
virtual void NotifySurfaceChanged(bool second) {}
/// Returns the resolution scale factor relative to the native 3DS screen resolution
u32 GetResolutionScaleFactor();
@ -110,7 +111,14 @@ protected:
Core::System& system;
RendererSettings settings;
Frontend::EmuWindow& render_window; ///< Reference to the render window handle.
Frontend::EmuWindow* secondary_window; ///< Reference to the secondary render window handle.
Frontend::EmuWindow* secondary_window;
public:
Frontend::EmuWindow *getSecondaryWindow() const;
virtual void setSecondaryWindow(Frontend::EmuWindow *secondaryWindow);
protected:
///< Reference to the secondary render window handle.
f32 current_fps = 0.0f; ///< Current framerate, should be set by the renderer
s32 current_frame = 0; ///< Current frame, should be set by the renderer
};

View File

@ -87,6 +87,15 @@ RendererOpenGL::RendererOpenGL(Core::System& system, Pica::PicaCore& pica_,
}
RendererOpenGL::~RendererOpenGL() = default;
void RendererOpenGL::setSecondaryWindow(Frontend::EmuWindow *secondaryWindow) {
if (secondaryWindow) {
secondary_window = secondaryWindow;
secondary_window->mailbox = std::make_unique<OGLTextureMailbox>(driver.HasDebugTool());
}else {
secondary_window = nullptr;
// should I release something here? The mailbox??
}
}
void RendererOpenGL::SwapBuffers() {
// Maintain the rasterizer's state as a priority
@ -96,7 +105,7 @@ void RendererOpenGL::SwapBuffers() {
PrepareRendertarget();
RenderScreenshot();
const auto& main_layout = render_window.GetFramebufferLayout();
const auto &main_layout = render_window.GetFramebufferLayout();
RenderToMailbox(main_layout, render_window.mailbox, false);
#ifndef ANDROID
@ -106,6 +115,15 @@ void RendererOpenGL::SwapBuffers() {
RenderToMailbox(secondary_layout, secondary_window->mailbox, false);
secondary_window->PollEvents();
}
#endif
#ifdef ANDROID
// on android, if secondary_window is defined at all
// it means we have a second display
if (secondary_window) {
const auto &secondary_layout = secondary_window->GetFramebufferLayout();
RenderToMailbox(secondary_layout, secondary_window->mailbox, false);
secondary_window->PollEvents();
}
#endif
if (frame_dumper.IsDumping()) {
try {
@ -553,7 +571,7 @@ void RendererOpenGL::DrawSingleScreen(const ScreenInfo& screen_info, float x, fl
state.texture_units[0].texture_2d = 0;
state.texture_units[0].sampler = 0;
state.Apply();
}
}
/**
* Draws a single texture to the emulator window, rotating the texture to correct for the 3DS's LCD

View File

@ -54,6 +54,7 @@ public:
void PrepareVideoDumping() override;
void CleanupVideoDumping() override;
void Sync() override;
void setSecondaryWindow(Frontend::EmuWindow *secondaryWindow) override;
private:
void InitOpenGLObjects();

View File

@ -829,6 +829,16 @@ void RendererVulkan::SwapBuffers() {
RenderToWindow(*second_window, secondary_layout, false);
secondary_window->PollEvents();
}
#endif
#ifdef ANDROID
if (secondary_window) {
const auto &secondary_layout = secondary_window->GetFramebufferLayout();
if (!second_window) {
second_window = std::make_unique<PresentWindow>(*secondary_window, instance, scheduler);
}
RenderToWindow(*second_window, secondary_layout, false);
secondary_window->PollEvents();
}
#endif
rasterizer.TickFrame();
EndFrame();
@ -1119,4 +1129,10 @@ bool RendererVulkan::TryRenderScreenshotWithHostMemory() {
return true;
}
void RendererVulkan::NotifySurfaceChanged(bool second) {
if (second && second_window) second_window->NotifySurfaceChanged();
if (!second) main_window.NotifySurfaceChanged();
}
} // namespace Vulkan

View File

@ -74,9 +74,7 @@ public:
return &rasterizer;
}
void NotifySurfaceChanged() override {
main_window.NotifySurfaceChanged();
}
void NotifySurfaceChanged(bool second) override;
void SwapBuffers() override;
void TryPresent(int timeout_ms, bool is_secondary) override {}