early-access version 3998
This commit is contained in:
parent
f15b33ff50
commit
1382f73d4e
105 changed files with 3851 additions and 280 deletions
|
@ -260,6 +260,11 @@ if (UNIX)
|
||||||
add_definitions(-DYUZU_UNIX=1)
|
add_definitions(-DYUZU_UNIX=1)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if (ARCHITECTURE_arm64 AND (ANDROID OR ${CMAKE_SYSTEM_NAME} STREQUAL "Linux"))
|
||||||
|
set(HAS_NCE 1)
|
||||||
|
add_definitions(-DHAS_NCE=1)
|
||||||
|
endif()
|
||||||
|
|
||||||
# Configure C++ standard
|
# Configure C++ standard
|
||||||
# ===========================
|
# ===========================
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
yuzu emulator early access
|
yuzu emulator early access
|
||||||
=============
|
=============
|
||||||
|
|
||||||
This is the source code for early-access 3997.
|
This is the source code for early-access 3998.
|
||||||
|
|
||||||
## Legal Notice
|
## Legal Notice
|
||||||
|
|
||||||
|
|
8
externals/CMakeLists.txt
vendored
8
externals/CMakeLists.txt
vendored
|
@ -20,6 +20,10 @@ if ((ARCHITECTURE_x86 OR ARCHITECTURE_x86_64) AND NOT TARGET xbyak::xbyak)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Dynarmic
|
# Dynarmic
|
||||||
|
if (ARCHITECTURE_arm64 AND NOT TARGET merry::oaknut)
|
||||||
|
add_subdirectory(oaknut)
|
||||||
|
endif()
|
||||||
|
|
||||||
if ((ARCHITECTURE_x86_64 OR ARCHITECTURE_arm64) AND NOT TARGET dynarmic::dynarmic)
|
if ((ARCHITECTURE_x86_64 OR ARCHITECTURE_arm64) AND NOT TARGET dynarmic::dynarmic)
|
||||||
set(DYNARMIC_IGNORE_ASSERTS ON)
|
set(DYNARMIC_IGNORE_ASSERTS ON)
|
||||||
add_subdirectory(dynarmic)
|
add_subdirectory(dynarmic)
|
||||||
|
@ -189,6 +193,10 @@ if (ANDROID)
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if (UNIX)
|
||||||
|
add_subdirectory(gamemode)
|
||||||
|
endif()
|
||||||
|
|
||||||
# Breakpad
|
# Breakpad
|
||||||
# https://github.com/microsoft/vcpkg/blob/master/ports/breakpad/CMakeLists.txt
|
# https://github.com/microsoft/vcpkg/blob/master/ports/breakpad/CMakeLists.txt
|
||||||
if (YUZU_CRASH_DUMPS AND NOT TARGET libbreakpad_client)
|
if (YUZU_CRASH_DUMPS AND NOT TARGET libbreakpad_client)
|
||||||
|
|
11
externals/gamemode/CMakeLists.txt
vendored
Executable file
11
externals/gamemode/CMakeLists.txt
vendored
Executable file
|
@ -0,0 +1,11 @@
|
||||||
|
# SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
project(gamemode LANGUAGES CXX C)
|
||||||
|
|
||||||
|
add_library(gamemode include/gamemode_client.h)
|
||||||
|
|
||||||
|
target_link_libraries(gamemode PRIVATE common)
|
||||||
|
|
||||||
|
target_include_directories(gamemode PUBLIC include)
|
||||||
|
set_target_properties(gamemode PROPERTIES LINKER_LANGUAGE C)
|
379
externals/gamemode/include/gamemode_client.h
vendored
Executable file
379
externals/gamemode/include/gamemode_client.h
vendored
Executable file
|
@ -0,0 +1,379 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2017-2019 Feral Interactive
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Copyright (c) 2017-2019, Feral Interactive
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
* Neither the name of Feral Interactive nor the names of its contributors
|
||||||
|
may be used to endorse or promote products derived from this software
|
||||||
|
without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||||
|
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
*/
|
||||||
|
#ifndef CLIENT_GAMEMODE_H
|
||||||
|
#define CLIENT_GAMEMODE_H
|
||||||
|
/*
|
||||||
|
* GameMode supports the following client functions
|
||||||
|
* Requests are refcounted in the daemon
|
||||||
|
*
|
||||||
|
* int gamemode_request_start() - Request gamemode starts
|
||||||
|
* 0 if the request was sent successfully
|
||||||
|
* -1 if the request failed
|
||||||
|
*
|
||||||
|
* int gamemode_request_end() - Request gamemode ends
|
||||||
|
* 0 if the request was sent successfully
|
||||||
|
* -1 if the request failed
|
||||||
|
*
|
||||||
|
* GAMEMODE_AUTO can be defined to make the above two functions apply during static init and
|
||||||
|
* destruction, as appropriate. In this configuration, errors will be printed to stderr
|
||||||
|
*
|
||||||
|
* int gamemode_query_status() - Query the current status of gamemode
|
||||||
|
* 0 if gamemode is inactive
|
||||||
|
* 1 if gamemode is active
|
||||||
|
* 2 if gamemode is active and this client is registered
|
||||||
|
* -1 if the query failed
|
||||||
|
*
|
||||||
|
* int gamemode_request_start_for(pid_t pid) - Request gamemode starts for another process
|
||||||
|
* 0 if the request was sent successfully
|
||||||
|
* -1 if the request failed
|
||||||
|
* -2 if the request was rejected
|
||||||
|
*
|
||||||
|
* int gamemode_request_end_for(pid_t pid) - Request gamemode ends for another process
|
||||||
|
* 0 if the request was sent successfully
|
||||||
|
* -1 if the request failed
|
||||||
|
* -2 if the request was rejected
|
||||||
|
*
|
||||||
|
* int gamemode_query_status_for(pid_t pid) - Query status of gamemode for another process
|
||||||
|
* 0 if gamemode is inactive
|
||||||
|
* 1 if gamemode is active
|
||||||
|
* 2 if gamemode is active and this client is registered
|
||||||
|
* -1 if the query failed
|
||||||
|
*
|
||||||
|
* const char* gamemode_error_string() - Get an error string
|
||||||
|
* returns a string describing any of the above errors
|
||||||
|
*
|
||||||
|
* Note: All the above requests can be blocking - dbus requests can and will block while the daemon
|
||||||
|
* handles the request. It is not recommended to make these calls in performance critical code
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
#include <dlfcn.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
|
|
||||||
|
#include <sys/types.h>
|
||||||
|
|
||||||
|
static char internal_gamemode_client_error_string[512] = { 0 };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load libgamemode dynamically to dislodge us from most dependencies.
|
||||||
|
* This allows clients to link and/or use this regardless of runtime.
|
||||||
|
* See SDL2 for an example of the reasoning behind this in terms of
|
||||||
|
* dynamic versioning as well.
|
||||||
|
*/
|
||||||
|
static volatile int internal_libgamemode_loaded = 1;
|
||||||
|
|
||||||
|
/* Typedefs for the functions to load */
|
||||||
|
typedef int (*api_call_return_int)(void);
|
||||||
|
typedef const char *(*api_call_return_cstring)(void);
|
||||||
|
typedef int (*api_call_pid_return_int)(pid_t);
|
||||||
|
|
||||||
|
/* Storage for functors */
|
||||||
|
static api_call_return_int REAL_internal_gamemode_request_start = NULL;
|
||||||
|
static api_call_return_int REAL_internal_gamemode_request_end = NULL;
|
||||||
|
static api_call_return_int REAL_internal_gamemode_query_status = NULL;
|
||||||
|
static api_call_return_cstring REAL_internal_gamemode_error_string = NULL;
|
||||||
|
static api_call_pid_return_int REAL_internal_gamemode_request_start_for = NULL;
|
||||||
|
static api_call_pid_return_int REAL_internal_gamemode_request_end_for = NULL;
|
||||||
|
static api_call_pid_return_int REAL_internal_gamemode_query_status_for = NULL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal helper to perform the symbol binding safely.
|
||||||
|
*
|
||||||
|
* Returns 0 on success and -1 on failure
|
||||||
|
*/
|
||||||
|
__attribute__((always_inline)) static inline int internal_bind_libgamemode_symbol(
|
||||||
|
void *handle, const char *name, void **out_func, size_t func_size, bool required)
|
||||||
|
{
|
||||||
|
void *symbol_lookup = NULL;
|
||||||
|
char *dl_error = NULL;
|
||||||
|
|
||||||
|
/* Safely look up the symbol */
|
||||||
|
symbol_lookup = dlsym(handle, name);
|
||||||
|
dl_error = dlerror();
|
||||||
|
if (required && (dl_error || !symbol_lookup)) {
|
||||||
|
snprintf(internal_gamemode_client_error_string,
|
||||||
|
sizeof(internal_gamemode_client_error_string),
|
||||||
|
"dlsym failed - %s",
|
||||||
|
dl_error);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Have the symbol correctly, copy it to make it usable */
|
||||||
|
memcpy(out_func, &symbol_lookup, func_size);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads libgamemode and needed functions
|
||||||
|
*
|
||||||
|
* Returns 0 on success and -1 on failure
|
||||||
|
*/
|
||||||
|
__attribute__((always_inline)) static inline int internal_load_libgamemode(void)
|
||||||
|
{
|
||||||
|
/* We start at 1, 0 is a success and -1 is a fail */
|
||||||
|
if (internal_libgamemode_loaded != 1) {
|
||||||
|
return internal_libgamemode_loaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Anonymous struct type to define our bindings */
|
||||||
|
struct binding {
|
||||||
|
const char *name;
|
||||||
|
void **functor;
|
||||||
|
size_t func_size;
|
||||||
|
bool required;
|
||||||
|
} bindings[] = {
|
||||||
|
{ "real_gamemode_request_start",
|
||||||
|
(void **)&REAL_internal_gamemode_request_start,
|
||||||
|
sizeof(REAL_internal_gamemode_request_start),
|
||||||
|
true },
|
||||||
|
{ "real_gamemode_request_end",
|
||||||
|
(void **)&REAL_internal_gamemode_request_end,
|
||||||
|
sizeof(REAL_internal_gamemode_request_end),
|
||||||
|
true },
|
||||||
|
{ "real_gamemode_query_status",
|
||||||
|
(void **)&REAL_internal_gamemode_query_status,
|
||||||
|
sizeof(REAL_internal_gamemode_query_status),
|
||||||
|
false },
|
||||||
|
{ "real_gamemode_error_string",
|
||||||
|
(void **)&REAL_internal_gamemode_error_string,
|
||||||
|
sizeof(REAL_internal_gamemode_error_string),
|
||||||
|
true },
|
||||||
|
{ "real_gamemode_request_start_for",
|
||||||
|
(void **)&REAL_internal_gamemode_request_start_for,
|
||||||
|
sizeof(REAL_internal_gamemode_request_start_for),
|
||||||
|
false },
|
||||||
|
{ "real_gamemode_request_end_for",
|
||||||
|
(void **)&REAL_internal_gamemode_request_end_for,
|
||||||
|
sizeof(REAL_internal_gamemode_request_end_for),
|
||||||
|
false },
|
||||||
|
{ "real_gamemode_query_status_for",
|
||||||
|
(void **)&REAL_internal_gamemode_query_status_for,
|
||||||
|
sizeof(REAL_internal_gamemode_query_status_for),
|
||||||
|
false },
|
||||||
|
};
|
||||||
|
|
||||||
|
void *libgamemode = NULL;
|
||||||
|
|
||||||
|
/* Try and load libgamemode */
|
||||||
|
libgamemode = dlopen("libgamemode.so.0", RTLD_NOW);
|
||||||
|
if (!libgamemode) {
|
||||||
|
/* Attempt to load unversioned library for compatibility with older
|
||||||
|
* versions (as of writing, there are no ABI changes between the two -
|
||||||
|
* this may need to change if ever ABI-breaking changes are made) */
|
||||||
|
libgamemode = dlopen("libgamemode.so", RTLD_NOW);
|
||||||
|
if (!libgamemode) {
|
||||||
|
snprintf(internal_gamemode_client_error_string,
|
||||||
|
sizeof(internal_gamemode_client_error_string),
|
||||||
|
"dlopen failed - %s",
|
||||||
|
dlerror());
|
||||||
|
internal_libgamemode_loaded = -1;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Attempt to bind all symbols */
|
||||||
|
for (size_t i = 0; i < sizeof(bindings) / sizeof(bindings[0]); i++) {
|
||||||
|
struct binding *binder = &bindings[i];
|
||||||
|
|
||||||
|
if (internal_bind_libgamemode_symbol(libgamemode,
|
||||||
|
binder->name,
|
||||||
|
binder->functor,
|
||||||
|
binder->func_size,
|
||||||
|
binder->required)) {
|
||||||
|
internal_libgamemode_loaded = -1;
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Success */
|
||||||
|
internal_libgamemode_loaded = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirect to the real libgamemode
|
||||||
|
*/
|
||||||
|
__attribute__((always_inline)) static inline const char *gamemode_error_string(void)
|
||||||
|
{
|
||||||
|
/* If we fail to load the system gamemode, or we have an error string already, return our error
|
||||||
|
* string instead of diverting to the system version */
|
||||||
|
if (internal_load_libgamemode() < 0 || internal_gamemode_client_error_string[0] != '\0') {
|
||||||
|
return internal_gamemode_client_error_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Assert for static analyser that the function is not NULL */
|
||||||
|
assert(REAL_internal_gamemode_error_string != NULL);
|
||||||
|
|
||||||
|
return REAL_internal_gamemode_error_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirect to the real libgamemode
|
||||||
|
* Allow automatically requesting game mode
|
||||||
|
* Also prints errors as they happen.
|
||||||
|
*/
|
||||||
|
#ifdef GAMEMODE_AUTO
|
||||||
|
__attribute__((constructor))
|
||||||
|
#else
|
||||||
|
__attribute__((always_inline)) static inline
|
||||||
|
#endif
|
||||||
|
int gamemode_request_start(void)
|
||||||
|
{
|
||||||
|
/* Need to load gamemode */
|
||||||
|
if (internal_load_libgamemode() < 0) {
|
||||||
|
#ifdef GAMEMODE_AUTO
|
||||||
|
fprintf(stderr, "gamemodeauto: %s\n", gamemode_error_string());
|
||||||
|
#endif
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Assert for static analyser that the function is not NULL */
|
||||||
|
assert(REAL_internal_gamemode_request_start != NULL);
|
||||||
|
|
||||||
|
if (REAL_internal_gamemode_request_start() < 0) {
|
||||||
|
#ifdef GAMEMODE_AUTO
|
||||||
|
fprintf(stderr, "gamemodeauto: %s\n", gamemode_error_string());
|
||||||
|
#endif
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Redirect to the real libgamemode */
|
||||||
|
#ifdef GAMEMODE_AUTO
|
||||||
|
__attribute__((destructor))
|
||||||
|
#else
|
||||||
|
__attribute__((always_inline)) static inline
|
||||||
|
#endif
|
||||||
|
int gamemode_request_end(void)
|
||||||
|
{
|
||||||
|
/* Need to load gamemode */
|
||||||
|
if (internal_load_libgamemode() < 0) {
|
||||||
|
#ifdef GAMEMODE_AUTO
|
||||||
|
fprintf(stderr, "gamemodeauto: %s\n", gamemode_error_string());
|
||||||
|
#endif
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Assert for static analyser that the function is not NULL */
|
||||||
|
assert(REAL_internal_gamemode_request_end != NULL);
|
||||||
|
|
||||||
|
if (REAL_internal_gamemode_request_end() < 0) {
|
||||||
|
#ifdef GAMEMODE_AUTO
|
||||||
|
fprintf(stderr, "gamemodeauto: %s\n", gamemode_error_string());
|
||||||
|
#endif
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Redirect to the real libgamemode */
|
||||||
|
__attribute__((always_inline)) static inline int gamemode_query_status(void)
|
||||||
|
{
|
||||||
|
/* Need to load gamemode */
|
||||||
|
if (internal_load_libgamemode() < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (REAL_internal_gamemode_query_status == NULL) {
|
||||||
|
snprintf(internal_gamemode_client_error_string,
|
||||||
|
sizeof(internal_gamemode_client_error_string),
|
||||||
|
"gamemode_query_status missing (older host?)");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return REAL_internal_gamemode_query_status();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Redirect to the real libgamemode */
|
||||||
|
__attribute__((always_inline)) static inline int gamemode_request_start_for(pid_t pid)
|
||||||
|
{
|
||||||
|
/* Need to load gamemode */
|
||||||
|
if (internal_load_libgamemode() < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (REAL_internal_gamemode_request_start_for == NULL) {
|
||||||
|
snprintf(internal_gamemode_client_error_string,
|
||||||
|
sizeof(internal_gamemode_client_error_string),
|
||||||
|
"gamemode_request_start_for missing (older host?)");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return REAL_internal_gamemode_request_start_for(pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Redirect to the real libgamemode */
|
||||||
|
__attribute__((always_inline)) static inline int gamemode_request_end_for(pid_t pid)
|
||||||
|
{
|
||||||
|
/* Need to load gamemode */
|
||||||
|
if (internal_load_libgamemode() < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (REAL_internal_gamemode_request_end_for == NULL) {
|
||||||
|
snprintf(internal_gamemode_client_error_string,
|
||||||
|
sizeof(internal_gamemode_client_error_string),
|
||||||
|
"gamemode_request_end_for missing (older host?)");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return REAL_internal_gamemode_request_end_for(pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Redirect to the real libgamemode */
|
||||||
|
__attribute__((always_inline)) static inline int gamemode_query_status_for(pid_t pid)
|
||||||
|
{
|
||||||
|
/* Need to load gamemode */
|
||||||
|
if (internal_load_libgamemode() < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (REAL_internal_gamemode_query_status_for == NULL) {
|
||||||
|
snprintf(internal_gamemode_client_error_string,
|
||||||
|
sizeof(internal_gamemode_client_error_string),
|
||||||
|
"gamemode_query_status_for missing (older host?)");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return REAL_internal_gamemode_query_status_for(pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // CLIENT_GAMEMODE_H
|
|
@ -299,6 +299,11 @@ object NativeLibrary {
|
||||||
*/
|
*/
|
||||||
external fun getPerfStats(): DoubleArray
|
external fun getPerfStats(): DoubleArray
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current CPU backend.
|
||||||
|
*/
|
||||||
|
external fun getCpuBackend(): String
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notifies the core emulation that the orientation has changed.
|
* Notifies the core emulation that the orientation has changed.
|
||||||
*/
|
*/
|
||||||
|
|
76
src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt
Executable file
76
src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt
Executable file
|
@ -0,0 +1,76 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.adapters
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import android.text.TextUtils
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import org.yuzu.yuzu_emu.databinding.CardFolderBinding
|
||||||
|
import org.yuzu.yuzu_emu.fragments.GameFolderPropertiesDialogFragment
|
||||||
|
import org.yuzu.yuzu_emu.model.GameDir
|
||||||
|
import org.yuzu.yuzu_emu.model.GamesViewModel
|
||||||
|
|
||||||
|
class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesViewModel) :
|
||||||
|
ListAdapter<GameDir, FolderAdapter.FolderViewHolder>(
|
||||||
|
AsyncDifferConfig.Builder(DiffCallback()).build()
|
||||||
|
) {
|
||||||
|
override fun onCreateViewHolder(
|
||||||
|
parent: ViewGroup,
|
||||||
|
viewType: Int
|
||||||
|
): FolderAdapter.FolderViewHolder {
|
||||||
|
CardFolderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
|
.also { return FolderViewHolder(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: FolderAdapter.FolderViewHolder, position: Int) =
|
||||||
|
holder.bind(currentList[position])
|
||||||
|
|
||||||
|
inner class FolderViewHolder(val binding: CardFolderBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
|
private lateinit var gameDir: GameDir
|
||||||
|
|
||||||
|
fun bind(gameDir: GameDir) {
|
||||||
|
this.gameDir = gameDir
|
||||||
|
|
||||||
|
binding.apply {
|
||||||
|
path.text = Uri.parse(gameDir.uriString).path
|
||||||
|
path.postDelayed(
|
||||||
|
{
|
||||||
|
path.isSelected = true
|
||||||
|
path.ellipsize = TextUtils.TruncateAt.MARQUEE
|
||||||
|
},
|
||||||
|
3000
|
||||||
|
)
|
||||||
|
|
||||||
|
buttonEdit.setOnClickListener {
|
||||||
|
GameFolderPropertiesDialogFragment.newInstance(this@FolderViewHolder.gameDir)
|
||||||
|
.show(
|
||||||
|
activity.supportFragmentManager,
|
||||||
|
GameFolderPropertiesDialogFragment.TAG
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
buttonDelete.setOnClickListener {
|
||||||
|
gamesViewModel.removeFolder(this@FolderViewHolder.gameDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DiffCallback : DiffUtil.ItemCallback<GameDir>() {
|
||||||
|
override fun areItemsTheSame(oldItem: GameDir, newItem: GameDir): Boolean {
|
||||||
|
return oldItem == newItem
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: GameDir, newItem: GameDir): Boolean {
|
||||||
|
return oldItem == newItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ enum class IntSetting(
|
||||||
override val category: Settings.Category,
|
override val category: Settings.Category,
|
||||||
override val androidDefault: Int? = null
|
override val androidDefault: Int? = null
|
||||||
) : AbstractIntSetting {
|
) : AbstractIntSetting {
|
||||||
|
CPU_BACKEND("cpu_backend", Settings.Category.Cpu),
|
||||||
CPU_ACCURACY("cpu_accuracy", Settings.Category.Cpu),
|
CPU_ACCURACY("cpu_accuracy", Settings.Category.Cpu),
|
||||||
REGION_INDEX("region_index", Settings.Category.System),
|
REGION_INDEX("region_index", Settings.Category.System),
|
||||||
LANGUAGE_INDEX("language_index", Settings.Category.System),
|
LANGUAGE_INDEX("language_index", Settings.Category.System),
|
||||||
|
|
|
@ -3,33 +3,9 @@
|
||||||
|
|
||||||
package org.yuzu.yuzu_emu.features.settings.model
|
package org.yuzu.yuzu_emu.features.settings.model
|
||||||
|
|
||||||
import android.text.TextUtils
|
|
||||||
import android.widget.Toast
|
|
||||||
import org.yuzu.yuzu_emu.R
|
import org.yuzu.yuzu_emu.R
|
||||||
import org.yuzu.yuzu_emu.YuzuApplication
|
|
||||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
|
||||||
|
|
||||||
object Settings {
|
object Settings {
|
||||||
private val context get() = YuzuApplication.appContext
|
|
||||||
|
|
||||||
fun saveSettings(gameId: String = "") {
|
|
||||||
if (TextUtils.isEmpty(gameId)) {
|
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
context.getString(R.string.ini_saved),
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
NativeConfig.saveSettings()
|
|
||||||
} else {
|
|
||||||
// TODO: Save custom game settings
|
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
context.getString(R.string.gameid_saved, gameId),
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class Category {
|
enum class Category {
|
||||||
Android,
|
Android,
|
||||||
Audio,
|
Audio,
|
||||||
|
|
|
@ -77,6 +77,15 @@ abstract class SettingsItem(
|
||||||
"%"
|
"%"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
put(
|
||||||
|
SingleChoiceSetting(
|
||||||
|
IntSetting.CPU_BACKEND,
|
||||||
|
R.string.cpu_backend,
|
||||||
|
0,
|
||||||
|
R.array.cpuBackendArm64Names,
|
||||||
|
R.array.cpuBackendArm64Values
|
||||||
|
)
|
||||||
|
)
|
||||||
put(
|
put(
|
||||||
SingleChoiceSetting(
|
SingleChoiceSetting(
|
||||||
IntSetting.CPU_ACCURACY,
|
IntSetting.CPU_ACCURACY,
|
||||||
|
|
|
@ -19,12 +19,13 @@ import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.navigation.fragment.NavHostFragment
|
import androidx.navigation.fragment.NavHostFragment
|
||||||
import androidx.navigation.navArgs
|
import androidx.navigation.navArgs
|
||||||
import com.google.android.material.color.MaterialColors
|
import com.google.android.material.color.MaterialColors
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import org.yuzu.yuzu_emu.R
|
import org.yuzu.yuzu_emu.R
|
||||||
import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding
|
import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding
|
||||||
import org.yuzu.yuzu_emu.features.settings.model.Settings
|
|
||||||
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
|
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
|
||||||
import org.yuzu.yuzu_emu.fragments.ResetSettingsDialogFragment
|
import org.yuzu.yuzu_emu.fragments.ResetSettingsDialogFragment
|
||||||
import org.yuzu.yuzu_emu.model.SettingsViewModel
|
import org.yuzu.yuzu_emu.model.SettingsViewModel
|
||||||
|
@ -53,10 +54,6 @@ class SettingsActivity : AppCompatActivity() {
|
||||||
|
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
|
||||||
if (savedInstanceState != null) {
|
|
||||||
settingsViewModel.shouldSave = savedInstanceState.getBoolean(KEY_SHOULD_SAVE)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (InsetsHelper.getSystemGestureType(applicationContext) !=
|
if (InsetsHelper.getSystemGestureType(applicationContext) !=
|
||||||
InsetsHelper.GESTURE_NAVIGATION
|
InsetsHelper.GESTURE_NAVIGATION
|
||||||
) {
|
) {
|
||||||
|
@ -127,12 +124,6 @@ class SettingsActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
|
||||||
// Critical: If super method is not called, rotations will be busted.
|
|
||||||
super.onSaveInstanceState(outState)
|
|
||||||
outState.putBoolean(KEY_SHOULD_SAVE, settingsViewModel.shouldSave)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
// TODO: Load custom settings contextually
|
// TODO: Load custom settings contextually
|
||||||
|
@ -141,16 +132,10 @@ class SettingsActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* If this is called, the user has left the settings screen (potentially through the
|
|
||||||
* home button) and will expect their changes to be persisted. So we kick off an
|
|
||||||
* IntentService which will do so on a background thread.
|
|
||||||
*/
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
super.onStop()
|
super.onStop()
|
||||||
if (isFinishing && settingsViewModel.shouldSave) {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
Log.debug("[SettingsActivity] Settings activity stopping. Saving settings to INI...")
|
NativeConfig.saveSettings()
|
||||||
Settings.saveSettings()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,9 +145,6 @@ class SettingsActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onSettingsReset() {
|
fun onSettingsReset() {
|
||||||
// Prevents saving to a non-existent settings file
|
|
||||||
settingsViewModel.shouldSave = false
|
|
||||||
|
|
||||||
// Delete settings file because the user may have changed values that do not exist in the UI
|
// Delete settings file because the user may have changed values that do not exist in the UI
|
||||||
NativeConfig.unloadConfig()
|
NativeConfig.unloadConfig()
|
||||||
val settingsFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG)
|
val settingsFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG)
|
||||||
|
@ -194,8 +176,4 @@ class SettingsActivity : AppCompatActivity() {
|
||||||
windowInsets
|
windowInsets
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val KEY_SHOULD_SAVE = "should_save"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -105,7 +105,6 @@ class SettingsAdapter(
|
||||||
fun onBooleanClick(item: SwitchSetting, checked: Boolean) {
|
fun onBooleanClick(item: SwitchSetting, checked: Boolean) {
|
||||||
item.checked = checked
|
item.checked = checked
|
||||||
settingsViewModel.setShouldReloadSettingsList(true)
|
settingsViewModel.setShouldReloadSettingsList(true)
|
||||||
settingsViewModel.shouldSave = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onSingleChoiceClick(item: SingleChoiceSetting, position: Int) {
|
fun onSingleChoiceClick(item: SingleChoiceSetting, position: Int) {
|
||||||
|
@ -161,7 +160,6 @@ class SettingsAdapter(
|
||||||
epochTime += timePicker.hour.toLong() * 60 * 60
|
epochTime += timePicker.hour.toLong() * 60 * 60
|
||||||
epochTime += timePicker.minute.toLong() * 60
|
epochTime += timePicker.minute.toLong() * 60
|
||||||
if (item.value != epochTime) {
|
if (item.value != epochTime) {
|
||||||
settingsViewModel.shouldSave = true
|
|
||||||
notifyItemChanged(position)
|
notifyItemChanged(position)
|
||||||
item.value = epochTime
|
item.value = epochTime
|
||||||
}
|
}
|
||||||
|
|
|
@ -269,6 +269,7 @@ class SettingsFragmentPresenter(
|
||||||
add(BooleanSetting.RENDERER_DEBUG.key)
|
add(BooleanSetting.RENDERER_DEBUG.key)
|
||||||
|
|
||||||
add(HeaderSetting(R.string.cpu))
|
add(HeaderSetting(R.string.cpu))
|
||||||
|
add(IntSetting.CPU_BACKEND.key)
|
||||||
add(IntSetting.CPU_ACCURACY.key)
|
add(IntSetting.CPU_ACCURACY.key)
|
||||||
add(BooleanSetting.CPU_DEBUG_MODE.key)
|
add(BooleanSetting.CPU_DEBUG_MODE.key)
|
||||||
add(SettingsItem.FASTMEM_COMBINED)
|
add(SettingsItem.FASTMEM_COMBINED)
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.fragments
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import org.yuzu.yuzu_emu.R
|
||||||
|
import org.yuzu.yuzu_emu.databinding.DialogAddFolderBinding
|
||||||
|
import org.yuzu.yuzu_emu.model.GameDir
|
||||||
|
import org.yuzu.yuzu_emu.model.GamesViewModel
|
||||||
|
|
||||||
|
class AddGameFolderDialogFragment : DialogFragment() {
|
||||||
|
private val gamesViewModel: GamesViewModel by activityViewModels()
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
val binding = DialogAddFolderBinding.inflate(layoutInflater)
|
||||||
|
val folderUriString = requireArguments().getString(FOLDER_URI_STRING)
|
||||||
|
if (folderUriString == null) {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
binding.path.text = Uri.parse(folderUriString).path
|
||||||
|
|
||||||
|
return MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle(R.string.add_game_folder)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
|
||||||
|
val newGameDir = GameDir(folderUriString!!, binding.deepScanSwitch.isChecked)
|
||||||
|
gamesViewModel.addFolder(newGameDir)
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.setView(binding.root)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "AddGameFolderDialogFragment"
|
||||||
|
|
||||||
|
private const val FOLDER_URI_STRING = "FolderUriString"
|
||||||
|
|
||||||
|
fun newInstance(folderUriString: String): AddGameFolderDialogFragment {
|
||||||
|
val args = Bundle()
|
||||||
|
args.putString(FOLDER_URI_STRING, folderUriString)
|
||||||
|
val fragment = AddGameFolderDialogFragment()
|
||||||
|
fragment.arguments = args
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -414,8 +414,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||||
perfStatsUpdater = {
|
perfStatsUpdater = {
|
||||||
if (emulationViewModel.emulationStarted.value) {
|
if (emulationViewModel.emulationStarted.value) {
|
||||||
val perfStats = NativeLibrary.getPerfStats()
|
val perfStats = NativeLibrary.getPerfStats()
|
||||||
|
val cpuBackend = NativeLibrary.getCpuBackend()
|
||||||
if (_binding != null) {
|
if (_binding != null) {
|
||||||
binding.showFpsText.text = String.format("FPS: %.1f", perfStats[FPS])
|
binding.showFpsText.text =
|
||||||
|
String.format("FPS: %.1f\n%s", perfStats[FPS], cpuBackend)
|
||||||
}
|
}
|
||||||
perfStatsUpdateHandler.postDelayed(perfStatsUpdater!!, 800)
|
perfStatsUpdateHandler.postDelayed(perfStatsUpdater!!, 800)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.fragments
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import org.yuzu.yuzu_emu.R
|
||||||
|
import org.yuzu.yuzu_emu.databinding.DialogFolderPropertiesBinding
|
||||||
|
import org.yuzu.yuzu_emu.model.GameDir
|
||||||
|
import org.yuzu.yuzu_emu.model.GamesViewModel
|
||||||
|
import org.yuzu.yuzu_emu.utils.SerializableHelper.parcelable
|
||||||
|
|
||||||
|
class GameFolderPropertiesDialogFragment : DialogFragment() {
|
||||||
|
private val gamesViewModel: GamesViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private var deepScan = false
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
val binding = DialogFolderPropertiesBinding.inflate(layoutInflater)
|
||||||
|
val gameDir = requireArguments().parcelable<GameDir>(GAME_DIR)!!
|
||||||
|
|
||||||
|
// Restore checkbox state
|
||||||
|
binding.deepScanSwitch.isChecked =
|
||||||
|
savedInstanceState?.getBoolean(DEEP_SCAN) ?: gameDir.deepScan
|
||||||
|
|
||||||
|
// Ensure that we can get the checkbox state even if the view is destroyed
|
||||||
|
deepScan = binding.deepScanSwitch.isChecked
|
||||||
|
binding.deepScanSwitch.setOnClickListener {
|
||||||
|
deepScan = binding.deepScanSwitch.isChecked
|
||||||
|
}
|
||||||
|
|
||||||
|
return MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setView(binding.root)
|
||||||
|
.setTitle(R.string.game_folder_properties)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
|
||||||
|
val folderIndex = gamesViewModel.folders.value.indexOf(gameDir)
|
||||||
|
if (folderIndex != -1) {
|
||||||
|
gamesViewModel.folders.value[folderIndex].deepScan =
|
||||||
|
binding.deepScanSwitch.isChecked
|
||||||
|
gamesViewModel.updateGameDirs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
outState.putBoolean(DEEP_SCAN, deepScan)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "GameFolderPropertiesDialogFragment"
|
||||||
|
|
||||||
|
private const val GAME_DIR = "GameDir"
|
||||||
|
|
||||||
|
private const val DEEP_SCAN = "DeepScan"
|
||||||
|
|
||||||
|
fun newInstance(gameDir: GameDir): GameFolderPropertiesDialogFragment {
|
||||||
|
val args = Bundle()
|
||||||
|
args.putParcelable(GAME_DIR, gameDir)
|
||||||
|
val fragment = GameFolderPropertiesDialogFragment()
|
||||||
|
fragment.arguments = args
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
128
src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFoldersFragment.kt
Executable file
128
src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFoldersFragment.kt
Executable file
|
@ -0,0 +1,128 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.fragments
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import androidx.navigation.findNavController
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import com.google.android.material.transition.MaterialSharedAxis
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.yuzu.yuzu_emu.R
|
||||||
|
import org.yuzu.yuzu_emu.adapters.FolderAdapter
|
||||||
|
import org.yuzu.yuzu_emu.databinding.FragmentFoldersBinding
|
||||||
|
import org.yuzu.yuzu_emu.model.GamesViewModel
|
||||||
|
import org.yuzu.yuzu_emu.model.HomeViewModel
|
||||||
|
import org.yuzu.yuzu_emu.ui.main.MainActivity
|
||||||
|
|
||||||
|
class GameFoldersFragment : Fragment() {
|
||||||
|
private var _binding: FragmentFoldersBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||||
|
private val gamesViewModel: GamesViewModel by activityViewModels()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
||||||
|
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
||||||
|
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
||||||
|
|
||||||
|
gamesViewModel.onOpenGameFoldersFragment()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
_binding = FragmentFoldersBinding.inflate(inflater)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
homeViewModel.setNavigationVisibility(visible = false, animated = true)
|
||||||
|
homeViewModel.setStatusBarShadeVisibility(visible = false)
|
||||||
|
|
||||||
|
binding.toolbarFolders.setNavigationOnClickListener {
|
||||||
|
binding.root.findNavController().popBackStack()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.listFolders.apply {
|
||||||
|
layoutManager = GridLayoutManager(
|
||||||
|
requireContext(),
|
||||||
|
resources.getInteger(R.integer.grid_columns)
|
||||||
|
)
|
||||||
|
adapter = FolderAdapter(requireActivity(), gamesViewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
|
gamesViewModel.folders.collect {
|
||||||
|
(binding.listFolders.adapter as FolderAdapter).submitList(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val mainActivity = requireActivity() as MainActivity
|
||||||
|
binding.buttonAdd.setOnClickListener {
|
||||||
|
mainActivity.getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data)
|
||||||
|
}
|
||||||
|
|
||||||
|
setInsets()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
gamesViewModel.onCloseGameFoldersFragment()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setInsets() =
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(
|
||||||
|
binding.root
|
||||||
|
) { _: View, windowInsets: WindowInsetsCompat ->
|
||||||
|
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
|
||||||
|
|
||||||
|
val leftInsets = barInsets.left + cutoutInsets.left
|
||||||
|
val rightInsets = barInsets.right + cutoutInsets.right
|
||||||
|
|
||||||
|
val mlpToolbar = binding.toolbarFolders.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
mlpToolbar.leftMargin = leftInsets
|
||||||
|
mlpToolbar.rightMargin = rightInsets
|
||||||
|
binding.toolbarFolders.layoutParams = mlpToolbar
|
||||||
|
|
||||||
|
val fabSpacing = resources.getDimensionPixelSize(R.dimen.spacing_fab)
|
||||||
|
val mlpFab =
|
||||||
|
binding.buttonAdd.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
mlpFab.leftMargin = leftInsets + fabSpacing
|
||||||
|
mlpFab.rightMargin = rightInsets + fabSpacing
|
||||||
|
mlpFab.bottomMargin = barInsets.bottom + fabSpacing
|
||||||
|
binding.buttonAdd.layoutParams = mlpFab
|
||||||
|
|
||||||
|
val mlpListFolders = binding.listFolders.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
mlpListFolders.leftMargin = leftInsets
|
||||||
|
mlpListFolders.rightMargin = rightInsets
|
||||||
|
binding.listFolders.layoutParams = mlpListFolders
|
||||||
|
|
||||||
|
binding.listFolders.updatePadding(
|
||||||
|
bottom = barInsets.bottom +
|
||||||
|
resources.getDimensionPixelSize(R.dimen.spacing_bottom_list_fab)
|
||||||
|
)
|
||||||
|
|
||||||
|
windowInsets
|
||||||
|
}
|
||||||
|
}
|
|
@ -127,18 +127,13 @@ class HomeSettingsFragment : Fragment() {
|
||||||
)
|
)
|
||||||
add(
|
add(
|
||||||
HomeSetting(
|
HomeSetting(
|
||||||
R.string.select_games_folder,
|
R.string.manage_game_folders,
|
||||||
R.string.select_games_folder_description,
|
R.string.select_games_folder_description,
|
||||||
R.drawable.ic_add,
|
R.drawable.ic_add,
|
||||||
{
|
{
|
||||||
mainActivity.getGamesDirectory.launch(
|
binding.root.findNavController()
|
||||||
Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data
|
.navigate(R.id.action_homeSettingsFragment_to_gameFoldersFragment)
|
||||||
)
|
}
|
||||||
},
|
|
||||||
{ true },
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
homeViewModel.gamesDir
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
add(
|
add(
|
||||||
|
|
|
@ -52,7 +52,6 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
|
||||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
|
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
|
||||||
settingsViewModel.clickedItem!!.setting.reset()
|
settingsViewModel.clickedItem!!.setting.reset()
|
||||||
settingsViewModel.setAdapterItemChanged(position)
|
settingsViewModel.setAdapterItemChanged(position)
|
||||||
settingsViewModel.shouldSave = true
|
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
.create()
|
.create()
|
||||||
|
@ -137,24 +136,17 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
|
||||||
is SingleChoiceSetting -> {
|
is SingleChoiceSetting -> {
|
||||||
val scSetting = settingsViewModel.clickedItem as SingleChoiceSetting
|
val scSetting = settingsViewModel.clickedItem as SingleChoiceSetting
|
||||||
val value = getValueForSingleChoiceSelection(scSetting, which)
|
val value = getValueForSingleChoiceSelection(scSetting, which)
|
||||||
if (scSetting.selectedValue != value) {
|
|
||||||
settingsViewModel.shouldSave = true
|
|
||||||
}
|
|
||||||
scSetting.selectedValue = value
|
scSetting.selectedValue = value
|
||||||
}
|
}
|
||||||
|
|
||||||
is StringSingleChoiceSetting -> {
|
is StringSingleChoiceSetting -> {
|
||||||
val scSetting = settingsViewModel.clickedItem as StringSingleChoiceSetting
|
val scSetting = settingsViewModel.clickedItem as StringSingleChoiceSetting
|
||||||
val value = scSetting.getValueAt(which)
|
val value = scSetting.getValueAt(which)
|
||||||
if (scSetting.selectedValue != value) settingsViewModel.shouldSave = true
|
|
||||||
scSetting.selectedValue = value
|
scSetting.selectedValue = value
|
||||||
}
|
}
|
||||||
|
|
||||||
is SliderSetting -> {
|
is SliderSetting -> {
|
||||||
val sliderSetting = settingsViewModel.clickedItem as SliderSetting
|
val sliderSetting = settingsViewModel.clickedItem as SliderSetting
|
||||||
if (sliderSetting.selectedValue != settingsViewModel.sliderProgress.value) {
|
|
||||||
settingsViewModel.shouldSave = true
|
|
||||||
}
|
|
||||||
sliderSetting.selectedValue = settingsViewModel.sliderProgress.value
|
sliderSetting.selectedValue = settingsViewModel.sliderProgress.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,7 @@ import org.yuzu.yuzu_emu.model.SetupPage
|
||||||
import org.yuzu.yuzu_emu.model.StepState
|
import org.yuzu.yuzu_emu.model.StepState
|
||||||
import org.yuzu.yuzu_emu.ui.main.MainActivity
|
import org.yuzu.yuzu_emu.ui.main.MainActivity
|
||||||
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
|
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
|
||||||
import org.yuzu.yuzu_emu.utils.GameHelper
|
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||||
import org.yuzu.yuzu_emu.utils.ViewUtils
|
import org.yuzu.yuzu_emu.utils.ViewUtils
|
||||||
|
|
||||||
class SetupFragment : Fragment() {
|
class SetupFragment : Fragment() {
|
||||||
|
@ -184,11 +184,7 @@ class SetupFragment : Fragment() {
|
||||||
R.string.add_games_warning_description,
|
R.string.add_games_warning_description,
|
||||||
R.string.add_games_warning_help,
|
R.string.add_games_warning_help,
|
||||||
{
|
{
|
||||||
val preferences =
|
if (NativeConfig.getGameDirs().isNotEmpty()) {
|
||||||
PreferenceManager.getDefaultSharedPreferences(
|
|
||||||
YuzuApplication.appContext
|
|
||||||
)
|
|
||||||
if (preferences.getString(GameHelper.KEY_GAME_PATH, "")!!.isNotEmpty()) {
|
|
||||||
StepState.COMPLETE
|
StepState.COMPLETE
|
||||||
} else {
|
} else {
|
||||||
StepState.INCOMPLETE
|
StepState.INCOMPLETE
|
||||||
|
|
13
src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDir.kt
Executable file
13
src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDir.kt
Executable file
|
@ -0,0 +1,13 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.model
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class GameDir(
|
||||||
|
val uriString: String,
|
||||||
|
var deepScan: Boolean
|
||||||
|
) : Parcelable
|
|
@ -12,6 +12,7 @@ import java.util.Locale
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
|
@ -20,6 +21,7 @@ import org.yuzu.yuzu_emu.NativeLibrary
|
||||||
import org.yuzu.yuzu_emu.YuzuApplication
|
import org.yuzu.yuzu_emu.YuzuApplication
|
||||||
import org.yuzu.yuzu_emu.utils.GameHelper
|
import org.yuzu.yuzu_emu.utils.GameHelper
|
||||||
import org.yuzu.yuzu_emu.utils.GameMetadata
|
import org.yuzu.yuzu_emu.utils.GameMetadata
|
||||||
|
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||||
|
|
||||||
class GamesViewModel : ViewModel() {
|
class GamesViewModel : ViewModel() {
|
||||||
val games: StateFlow<List<Game>> get() = _games
|
val games: StateFlow<List<Game>> get() = _games
|
||||||
|
@ -40,6 +42,9 @@ class GamesViewModel : ViewModel() {
|
||||||
val searchFocused: StateFlow<Boolean> get() = _searchFocused
|
val searchFocused: StateFlow<Boolean> get() = _searchFocused
|
||||||
private val _searchFocused = MutableStateFlow(false)
|
private val _searchFocused = MutableStateFlow(false)
|
||||||
|
|
||||||
|
private val _folders = MutableStateFlow(mutableListOf<GameDir>())
|
||||||
|
val folders = _folders.asStateFlow()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// Ensure keys are loaded so that ROM metadata can be decrypted.
|
// Ensure keys are loaded so that ROM metadata can be decrypted.
|
||||||
NativeLibrary.reloadKeys()
|
NativeLibrary.reloadKeys()
|
||||||
|
@ -50,6 +55,7 @@ class GamesViewModel : ViewModel() {
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
|
getGameDirs()
|
||||||
if (storedGames!!.isNotEmpty()) {
|
if (storedGames!!.isNotEmpty()) {
|
||||||
val deserializedGames = mutableSetOf<Game>()
|
val deserializedGames = mutableSetOf<Game>()
|
||||||
storedGames.forEach {
|
storedGames.forEach {
|
||||||
|
@ -104,7 +110,7 @@ class GamesViewModel : ViewModel() {
|
||||||
_searchFocused.value = searchFocused
|
_searchFocused.value = searchFocused
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reloadGames(directoryChanged: Boolean) {
|
fun reloadGames(directoriesChanged: Boolean) {
|
||||||
if (isReloading.value) {
|
if (isReloading.value) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -116,10 +122,61 @@ class GamesViewModel : ViewModel() {
|
||||||
setGames(GameHelper.getGames())
|
setGames(GameHelper.getGames())
|
||||||
_isReloading.value = false
|
_isReloading.value = false
|
||||||
|
|
||||||
if (directoryChanged) {
|
if (directoriesChanged) {
|
||||||
setShouldSwapData(true)
|
setShouldSwapData(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addFolder(gameDir: GameDir) =
|
||||||
|
viewModelScope.launch {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
NativeConfig.addGameDir(gameDir)
|
||||||
|
getGameDirs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeFolder(gameDir: GameDir) =
|
||||||
|
viewModelScope.launch {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val gameDirs = _folders.value.toMutableList()
|
||||||
|
val removedDirIndex = gameDirs.indexOf(gameDir)
|
||||||
|
if (removedDirIndex != -1) {
|
||||||
|
gameDirs.removeAt(removedDirIndex)
|
||||||
|
NativeConfig.setGameDirs(gameDirs.toTypedArray())
|
||||||
|
getGameDirs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateGameDirs() =
|
||||||
|
viewModelScope.launch {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
NativeConfig.setGameDirs(_folders.value.toTypedArray())
|
||||||
|
getGameDirs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onOpenGameFoldersFragment() =
|
||||||
|
viewModelScope.launch {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
getGameDirs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onCloseGameFoldersFragment() =
|
||||||
|
viewModelScope.launch {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
getGameDirs(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getGameDirs(reloadList: Boolean = false) {
|
||||||
|
val gameDirs = NativeConfig.getGameDirs()
|
||||||
|
_folders.value = gameDirs.toMutableList()
|
||||||
|
if (reloadList) {
|
||||||
|
reloadGames(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,15 +3,9 @@
|
||||||
|
|
||||||
package org.yuzu.yuzu_emu.model
|
package org.yuzu.yuzu_emu.model
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.fragment.app.FragmentActivity
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import org.yuzu.yuzu_emu.YuzuApplication
|
|
||||||
import org.yuzu.yuzu_emu.utils.GameHelper
|
|
||||||
|
|
||||||
class HomeViewModel : ViewModel() {
|
class HomeViewModel : ViewModel() {
|
||||||
val navigationVisible: StateFlow<Pair<Boolean, Boolean>> get() = _navigationVisible
|
val navigationVisible: StateFlow<Pair<Boolean, Boolean>> get() = _navigationVisible
|
||||||
|
@ -23,14 +17,6 @@ class HomeViewModel : ViewModel() {
|
||||||
val shouldPageForward: StateFlow<Boolean> get() = _shouldPageForward
|
val shouldPageForward: StateFlow<Boolean> get() = _shouldPageForward
|
||||||
private val _shouldPageForward = MutableStateFlow(false)
|
private val _shouldPageForward = MutableStateFlow(false)
|
||||||
|
|
||||||
val gamesDir: StateFlow<String> get() = _gamesDir
|
|
||||||
private val _gamesDir = MutableStateFlow(
|
|
||||||
Uri.parse(
|
|
||||||
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
|
|
||||||
.getString(GameHelper.KEY_GAME_PATH, "")
|
|
||||||
).path ?: ""
|
|
||||||
)
|
|
||||||
|
|
||||||
var navigatedToSetup = false
|
var navigatedToSetup = false
|
||||||
|
|
||||||
fun setNavigationVisibility(visible: Boolean, animated: Boolean) {
|
fun setNavigationVisibility(visible: Boolean, animated: Boolean) {
|
||||||
|
@ -50,9 +36,4 @@ class HomeViewModel : ViewModel() {
|
||||||
fun setShouldPageForward(pageForward: Boolean) {
|
fun setShouldPageForward(pageForward: Boolean) {
|
||||||
_shouldPageForward.value = pageForward
|
_shouldPageForward.value = pageForward
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setGamesDir(activity: FragmentActivity, dir: String) {
|
|
||||||
ViewModelProvider(activity)[GamesViewModel::class.java].reloadGames(true)
|
|
||||||
_gamesDir.value = dir
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,8 +13,6 @@ import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
|
||||||
class SettingsViewModel : ViewModel() {
|
class SettingsViewModel : ViewModel() {
|
||||||
var game: Game? = null
|
var game: Game? = null
|
||||||
|
|
||||||
var shouldSave = false
|
|
||||||
|
|
||||||
var clickedItem: SettingsItem? = null
|
var clickedItem: SettingsItem? = null
|
||||||
|
|
||||||
val shouldRecreate: StateFlow<Boolean> get() = _shouldRecreate
|
val shouldRecreate: StateFlow<Boolean> get() = _shouldRecreate
|
||||||
|
@ -73,6 +71,5 @@ class SettingsViewModel : ViewModel() {
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
game = null
|
game = null
|
||||||
shouldSave = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,7 @@ import org.yuzu.yuzu_emu.R
|
||||||
import org.yuzu.yuzu_emu.activities.EmulationActivity
|
import org.yuzu.yuzu_emu.activities.EmulationActivity
|
||||||
import org.yuzu.yuzu_emu.databinding.ActivityMainBinding
|
import org.yuzu.yuzu_emu.databinding.ActivityMainBinding
|
||||||
import org.yuzu.yuzu_emu.features.settings.model.Settings
|
import org.yuzu.yuzu_emu.features.settings.model.Settings
|
||||||
|
import org.yuzu.yuzu_emu.fragments.AddGameFolderDialogFragment
|
||||||
import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment
|
import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment
|
||||||
import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
|
import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
|
||||||
import org.yuzu.yuzu_emu.getPublicFilesDir
|
import org.yuzu.yuzu_emu.getPublicFilesDir
|
||||||
|
@ -252,6 +253,13 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
NativeConfig.saveSettings()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
EmulationActivity.stopForegroundService(this)
|
EmulationActivity.stopForegroundService(this)
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
@ -293,20 +301,19 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
)
|
)
|
||||||
|
|
||||||
// When a new directory is picked, we currently will reset the existing games
|
val uriString = result.toString()
|
||||||
// database. This effectively means that only one game directory is supported.
|
val folder = gamesViewModel.folders.value.firstOrNull { it.uriString == uriString }
|
||||||
PreferenceManager.getDefaultSharedPreferences(applicationContext).edit()
|
if (folder != null) {
|
||||||
.putString(GameHelper.KEY_GAME_PATH, result.toString())
|
Toast.makeText(
|
||||||
.apply()
|
applicationContext,
|
||||||
|
R.string.folder_already_added,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
Toast.makeText(
|
AddGameFolderDialogFragment.newInstance(uriString)
|
||||||
applicationContext,
|
.show(supportFragmentManager, AddGameFolderDialogFragment.TAG)
|
||||||
R.string.games_dir_selected,
|
|
||||||
Toast.LENGTH_LONG
|
|
||||||
).show()
|
|
||||||
|
|
||||||
gamesViewModel.reloadGames(true)
|
|
||||||
homeViewModel.setGamesDir(this, result.path!!)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val getProdKey =
|
val getProdKey =
|
||||||
|
|
|
@ -364,6 +364,27 @@ object FileUtil {
|
||||||
.lowercase()
|
.lowercase()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isTreeUriValid(uri: Uri): Boolean {
|
||||||
|
val resolver = context.contentResolver
|
||||||
|
val columns = arrayOf(
|
||||||
|
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
|
||||||
|
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
|
||||||
|
DocumentsContract.Document.COLUMN_MIME_TYPE
|
||||||
|
)
|
||||||
|
return try {
|
||||||
|
val docId: String = if (isRootTreeUri(uri)) {
|
||||||
|
DocumentsContract.getTreeDocumentId(uri)
|
||||||
|
} else {
|
||||||
|
DocumentsContract.getDocumentId(uri)
|
||||||
|
}
|
||||||
|
val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, docId)
|
||||||
|
resolver.query(childrenUri, columns, null, null, null)
|
||||||
|
true
|
||||||
|
} catch (_: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun getStringFromFile(file: File): String =
|
fun getStringFromFile(file: File): String =
|
||||||
String(file.readBytes(), StandardCharsets.UTF_8)
|
String(file.readBytes(), StandardCharsets.UTF_8)
|
||||||
|
|
|
@ -11,10 +11,11 @@ import kotlinx.serialization.json.Json
|
||||||
import org.yuzu.yuzu_emu.NativeLibrary
|
import org.yuzu.yuzu_emu.NativeLibrary
|
||||||
import org.yuzu.yuzu_emu.YuzuApplication
|
import org.yuzu.yuzu_emu.YuzuApplication
|
||||||
import org.yuzu.yuzu_emu.model.Game
|
import org.yuzu.yuzu_emu.model.Game
|
||||||
|
import org.yuzu.yuzu_emu.model.GameDir
|
||||||
import org.yuzu.yuzu_emu.model.MinimalDocumentFile
|
import org.yuzu.yuzu_emu.model.MinimalDocumentFile
|
||||||
|
|
||||||
object GameHelper {
|
object GameHelper {
|
||||||
const val KEY_GAME_PATH = "game_path"
|
private const val KEY_OLD_GAME_PATH = "game_path"
|
||||||
const val KEY_GAMES = "Games"
|
const val KEY_GAMES = "Games"
|
||||||
|
|
||||||
private lateinit var preferences: SharedPreferences
|
private lateinit var preferences: SharedPreferences
|
||||||
|
@ -22,15 +23,43 @@ object GameHelper {
|
||||||
fun getGames(): List<Game> {
|
fun getGames(): List<Game> {
|
||||||
val games = mutableListOf<Game>()
|
val games = mutableListOf<Game>()
|
||||||
val context = YuzuApplication.appContext
|
val context = YuzuApplication.appContext
|
||||||
val gamesDir =
|
|
||||||
PreferenceManager.getDefaultSharedPreferences(context).getString(KEY_GAME_PATH, "")
|
|
||||||
val gamesUri = Uri.parse(gamesDir)
|
|
||||||
preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
|
||||||
|
val gameDirs = mutableListOf<GameDir>()
|
||||||
|
val oldGamesDir = preferences.getString(KEY_OLD_GAME_PATH, "") ?: ""
|
||||||
|
if (oldGamesDir.isNotEmpty()) {
|
||||||
|
gameDirs.add(GameDir(oldGamesDir, true))
|
||||||
|
preferences.edit().remove(KEY_OLD_GAME_PATH).apply()
|
||||||
|
}
|
||||||
|
gameDirs.addAll(NativeConfig.getGameDirs())
|
||||||
|
|
||||||
// Ensure keys are loaded so that ROM metadata can be decrypted.
|
// Ensure keys are loaded so that ROM metadata can be decrypted.
|
||||||
NativeLibrary.reloadKeys()
|
NativeLibrary.reloadKeys()
|
||||||
|
|
||||||
addGamesRecursive(games, FileUtil.listFiles(gamesUri), 3)
|
val badDirs = mutableListOf<Int>()
|
||||||
|
gameDirs.forEachIndexed { index: Int, gameDir: GameDir ->
|
||||||
|
val gameDirUri = Uri.parse(gameDir.uriString)
|
||||||
|
val isValid = FileUtil.isTreeUriValid(gameDirUri)
|
||||||
|
if (isValid) {
|
||||||
|
addGamesRecursive(
|
||||||
|
games,
|
||||||
|
FileUtil.listFiles(gameDirUri),
|
||||||
|
if (gameDir.deepScan) 3 else 1
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
badDirs.add(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all game dirs with insufficient permissions from config
|
||||||
|
if (badDirs.isNotEmpty()) {
|
||||||
|
var offset = 0
|
||||||
|
badDirs.forEach {
|
||||||
|
gameDirs.removeAt(it - offset)
|
||||||
|
offset++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NativeConfig.setGameDirs(gameDirs.toTypedArray())
|
||||||
|
|
||||||
// Cache list of games found on disk
|
// Cache list of games found on disk
|
||||||
val serializedGames = mutableSetOf<String>()
|
val serializedGames = mutableSetOf<String>()
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
|
|
||||||
package org.yuzu.yuzu_emu.utils
|
package org.yuzu.yuzu_emu.utils
|
||||||
|
|
||||||
|
import org.yuzu.yuzu_emu.model.GameDir
|
||||||
|
|
||||||
object NativeConfig {
|
object NativeConfig {
|
||||||
/**
|
/**
|
||||||
* Creates a Config object and opens the emulation config.
|
* Creates a Config object and opens the emulation config.
|
||||||
|
@ -54,4 +56,22 @@ object NativeConfig {
|
||||||
external fun getConfigHeader(category: Int): String
|
external fun getConfigHeader(category: Int): String
|
||||||
|
|
||||||
external fun getPairedSettingKey(key: String): String
|
external fun getPairedSettingKey(key: String): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets every [GameDir] in AndroidSettings::values.game_dirs
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
external fun getGameDirs(): Array<GameDir>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the AndroidSettings::values.game_dirs array and replaces them with the provided array
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
external fun setGameDirs(dirs: Array<GameDir>)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a single [GameDir] to the AndroidSettings::values.game_dirs array
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
external fun addGameDir(dir: GameDir)
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@ void AndroidConfig::SaveAllValues() {
|
||||||
void AndroidConfig::ReadAndroidValues() {
|
void AndroidConfig::ReadAndroidValues() {
|
||||||
if (global) {
|
if (global) {
|
||||||
ReadAndroidUIValues();
|
ReadAndroidUIValues();
|
||||||
|
ReadUIValues();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,9 +46,35 @@ void AndroidConfig::ReadAndroidUIValues() {
|
||||||
EndGroup();
|
EndGroup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AndroidConfig::ReadUIValues() {
|
||||||
|
BeginGroup(Settings::TranslateCategory(Settings::Category::Ui));
|
||||||
|
|
||||||
|
ReadPathValues();
|
||||||
|
|
||||||
|
EndGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AndroidConfig::ReadPathValues() {
|
||||||
|
BeginGroup(Settings::TranslateCategory(Settings::Category::Paths));
|
||||||
|
|
||||||
|
const int gamedirs_size = BeginArray(std::string("gamedirs"));
|
||||||
|
for (int i = 0; i < gamedirs_size; ++i) {
|
||||||
|
SetArrayIndex(i);
|
||||||
|
AndroidSettings::GameDir game_dir;
|
||||||
|
game_dir.path = ReadStringSetting(std::string("path"));
|
||||||
|
game_dir.deep_scan =
|
||||||
|
ReadBooleanSetting(std::string("deep_scan"), std::make_optional(false));
|
||||||
|
AndroidSettings::values.game_dirs.push_back(game_dir);
|
||||||
|
}
|
||||||
|
EndArray();
|
||||||
|
|
||||||
|
EndGroup();
|
||||||
|
}
|
||||||
|
|
||||||
void AndroidConfig::SaveAndroidValues() {
|
void AndroidConfig::SaveAndroidValues() {
|
||||||
if (global) {
|
if (global) {
|
||||||
SaveAndroidUIValues();
|
SaveAndroidUIValues();
|
||||||
|
SaveUIValues();
|
||||||
}
|
}
|
||||||
|
|
||||||
WriteToIni();
|
WriteToIni();
|
||||||
|
@ -61,6 +88,29 @@ void AndroidConfig::SaveAndroidUIValues() {
|
||||||
EndGroup();
|
EndGroup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AndroidConfig::SaveUIValues() {
|
||||||
|
BeginGroup(Settings::TranslateCategory(Settings::Category::Ui));
|
||||||
|
|
||||||
|
SavePathValues();
|
||||||
|
|
||||||
|
EndGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AndroidConfig::SavePathValues() {
|
||||||
|
BeginGroup(Settings::TranslateCategory(Settings::Category::Paths));
|
||||||
|
|
||||||
|
BeginArray(std::string("gamedirs"));
|
||||||
|
for (size_t i = 0; i < AndroidSettings::values.game_dirs.size(); ++i) {
|
||||||
|
SetArrayIndex(i);
|
||||||
|
const auto& game_dir = AndroidSettings::values.game_dirs[i];
|
||||||
|
WriteSetting(std::string("path"), game_dir.path);
|
||||||
|
WriteSetting(std::string("deep_scan"), game_dir.deep_scan, std::make_optional(false));
|
||||||
|
}
|
||||||
|
EndArray();
|
||||||
|
|
||||||
|
EndGroup();
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<Settings::BasicSetting*>& AndroidConfig::FindRelevantList(Settings::Category category) {
|
std::vector<Settings::BasicSetting*>& AndroidConfig::FindRelevantList(Settings::Category category) {
|
||||||
auto& map = Settings::values.linkage.by_category;
|
auto& map = Settings::values.linkage.by_category;
|
||||||
if (map.contains(category)) {
|
if (map.contains(category)) {
|
||||||
|
|
|
@ -19,9 +19,9 @@ protected:
|
||||||
void ReadAndroidUIValues();
|
void ReadAndroidUIValues();
|
||||||
void ReadHidbusValues() override {}
|
void ReadHidbusValues() override {}
|
||||||
void ReadDebugControlValues() override {}
|
void ReadDebugControlValues() override {}
|
||||||
void ReadPathValues() override {}
|
void ReadPathValues() override;
|
||||||
void ReadShortcutValues() override {}
|
void ReadShortcutValues() override {}
|
||||||
void ReadUIValues() override {}
|
void ReadUIValues() override;
|
||||||
void ReadUIGamelistValues() override {}
|
void ReadUIGamelistValues() override {}
|
||||||
void ReadUILayoutValues() override {}
|
void ReadUILayoutValues() override {}
|
||||||
void ReadMultiplayerValues() override {}
|
void ReadMultiplayerValues() override {}
|
||||||
|
@ -30,9 +30,9 @@ protected:
|
||||||
void SaveAndroidUIValues();
|
void SaveAndroidUIValues();
|
||||||
void SaveHidbusValues() override {}
|
void SaveHidbusValues() override {}
|
||||||
void SaveDebugControlValues() override {}
|
void SaveDebugControlValues() override {}
|
||||||
void SavePathValues() override {}
|
void SavePathValues() override;
|
||||||
void SaveShortcutValues() override {}
|
void SaveShortcutValues() override {}
|
||||||
void SaveUIValues() override {}
|
void SaveUIValues() override;
|
||||||
void SaveUIGamelistValues() override {}
|
void SaveUIGamelistValues() override {}
|
||||||
void SaveUILayoutValues() override {}
|
void SaveUILayoutValues() override {}
|
||||||
void SaveMultiplayerValues() override {}
|
void SaveMultiplayerValues() override {}
|
||||||
|
|
|
@ -9,9 +9,17 @@
|
||||||
|
|
||||||
namespace AndroidSettings {
|
namespace AndroidSettings {
|
||||||
|
|
||||||
|
struct GameDir {
|
||||||
|
std::string path;
|
||||||
|
bool deep_scan = false;
|
||||||
|
};
|
||||||
|
|
||||||
struct Values {
|
struct Values {
|
||||||
Settings::Linkage linkage;
|
Settings::Linkage linkage;
|
||||||
|
|
||||||
|
// Path settings
|
||||||
|
std::vector<GameDir> game_dirs;
|
||||||
|
|
||||||
// Android
|
// Android
|
||||||
Settings::Setting<bool> picture_in_picture{linkage, false, "picture_in_picture",
|
Settings::Setting<bool> picture_in_picture{linkage, false, "picture_in_picture",
|
||||||
Settings::Category::Android};
|
Settings::Category::Android};
|
||||||
|
|
|
@ -13,6 +13,8 @@ static JavaVM* s_java_vm;
|
||||||
static jclass s_native_library_class;
|
static jclass s_native_library_class;
|
||||||
static jclass s_disk_cache_progress_class;
|
static jclass s_disk_cache_progress_class;
|
||||||
static jclass s_load_callback_stage_class;
|
static jclass s_load_callback_stage_class;
|
||||||
|
static jclass s_game_dir_class;
|
||||||
|
static jmethodID s_game_dir_constructor;
|
||||||
static jmethodID s_exit_emulation_activity;
|
static jmethodID s_exit_emulation_activity;
|
||||||
static jmethodID s_disk_cache_load_progress;
|
static jmethodID s_disk_cache_load_progress;
|
||||||
static jmethodID s_on_emulation_started;
|
static jmethodID s_on_emulation_started;
|
||||||
|
@ -53,6 +55,14 @@ jclass GetDiskCacheLoadCallbackStageClass() {
|
||||||
return s_load_callback_stage_class;
|
return s_load_callback_stage_class;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jclass GetGameDirClass() {
|
||||||
|
return s_game_dir_class;
|
||||||
|
}
|
||||||
|
|
||||||
|
jmethodID GetGameDirConstructor() {
|
||||||
|
return s_game_dir_constructor;
|
||||||
|
}
|
||||||
|
|
||||||
jmethodID GetExitEmulationActivity() {
|
jmethodID GetExitEmulationActivity() {
|
||||||
return s_exit_emulation_activity;
|
return s_exit_emulation_activity;
|
||||||
}
|
}
|
||||||
|
@ -90,6 +100,11 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
|
||||||
s_load_callback_stage_class = reinterpret_cast<jclass>(env->NewGlobalRef(env->FindClass(
|
s_load_callback_stage_class = reinterpret_cast<jclass>(env->NewGlobalRef(env->FindClass(
|
||||||
"org/yuzu/yuzu_emu/disk_shader_cache/DiskShaderCacheProgress$LoadCallbackStage")));
|
"org/yuzu/yuzu_emu/disk_shader_cache/DiskShaderCacheProgress$LoadCallbackStage")));
|
||||||
|
|
||||||
|
const jclass game_dir_class = env->FindClass("org/yuzu/yuzu_emu/model/GameDir");
|
||||||
|
s_game_dir_class = reinterpret_cast<jclass>(env->NewGlobalRef(game_dir_class));
|
||||||
|
s_game_dir_constructor = env->GetMethodID(game_dir_class, "<init>", "(Ljava/lang/String;Z)V");
|
||||||
|
env->DeleteLocalRef(game_dir_class);
|
||||||
|
|
||||||
// Initialize methods
|
// Initialize methods
|
||||||
s_exit_emulation_activity =
|
s_exit_emulation_activity =
|
||||||
env->GetStaticMethodID(s_native_library_class, "exitEmulationActivity", "(I)V");
|
env->GetStaticMethodID(s_native_library_class, "exitEmulationActivity", "(I)V");
|
||||||
|
@ -120,6 +135,7 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) {
|
||||||
env->DeleteGlobalRef(s_native_library_class);
|
env->DeleteGlobalRef(s_native_library_class);
|
||||||
env->DeleteGlobalRef(s_disk_cache_progress_class);
|
env->DeleteGlobalRef(s_disk_cache_progress_class);
|
||||||
env->DeleteGlobalRef(s_load_callback_stage_class);
|
env->DeleteGlobalRef(s_load_callback_stage_class);
|
||||||
|
env->DeleteGlobalRef(s_game_dir_class);
|
||||||
|
|
||||||
// UnInitialize applets
|
// UnInitialize applets
|
||||||
SoftwareKeyboard::CleanupJNI(env);
|
SoftwareKeyboard::CleanupJNI(env);
|
||||||
|
|
|
@ -13,6 +13,8 @@ JNIEnv* GetEnvForThread();
|
||||||
jclass GetNativeLibraryClass();
|
jclass GetNativeLibraryClass();
|
||||||
jclass GetDiskCacheProgressClass();
|
jclass GetDiskCacheProgressClass();
|
||||||
jclass GetDiskCacheLoadCallbackStageClass();
|
jclass GetDiskCacheLoadCallbackStageClass();
|
||||||
|
jclass GetGameDirClass();
|
||||||
|
jmethodID GetGameDirConstructor();
|
||||||
jmethodID GetExitEmulationActivity();
|
jmethodID GetExitEmulationActivity();
|
||||||
jmethodID GetDiskCacheLoadProgress();
|
jmethodID GetDiskCacheLoadProgress();
|
||||||
jmethodID GetOnEmulationStarted();
|
jmethodID GetOnEmulationStarted();
|
||||||
|
|
|
@ -694,6 +694,14 @@ jdoubleArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPerfStats(JNIEnv* env, jcl
|
||||||
return j_stats;
|
return j_stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getCpuBackend(JNIEnv* env, jclass clazz) {
|
||||||
|
if (Settings::IsNceEnabled()) {
|
||||||
|
return ToJString(env, "NCE");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ToJString(env, "JIT");
|
||||||
|
}
|
||||||
|
|
||||||
void Java_org_yuzu_yuzu_1emu_utils_DirectoryInitialization_setSysDirectory(JNIEnv* env,
|
void Java_org_yuzu_yuzu_1emu_utils_DirectoryInitialization_setSysDirectory(JNIEnv* env,
|
||||||
jclass clazz,
|
jclass clazz,
|
||||||
jstring j_path) {}
|
jstring j_path) {}
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
#include "common/settings.h"
|
#include "common/settings.h"
|
||||||
#include "frontend_common/config.h"
|
#include "frontend_common/config.h"
|
||||||
#include "jni/android_common/android_common.h"
|
#include "jni/android_common/android_common.h"
|
||||||
|
#include "jni/id_cache.h"
|
||||||
|
|
||||||
std::unique_ptr<AndroidConfig> config;
|
std::unique_ptr<AndroidConfig> config;
|
||||||
|
|
||||||
|
@ -253,4 +254,55 @@ jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getPairedSettingKey(JNIEnv* e
|
||||||
return ToJString(env, setting->PairedSetting()->GetLabel());
|
return ToJString(env, setting->PairedSetting()->GetLabel());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jobjectArray Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getGameDirs(JNIEnv* env, jobject obj) {
|
||||||
|
jclass gameDirClass = IDCache::GetGameDirClass();
|
||||||
|
jmethodID gameDirConstructor = IDCache::GetGameDirConstructor();
|
||||||
|
jobjectArray jgameDirArray =
|
||||||
|
env->NewObjectArray(AndroidSettings::values.game_dirs.size(), gameDirClass, nullptr);
|
||||||
|
for (size_t i = 0; i < AndroidSettings::values.game_dirs.size(); ++i) {
|
||||||
|
jobject jgameDir =
|
||||||
|
env->NewObject(gameDirClass, gameDirConstructor,
|
||||||
|
ToJString(env, AndroidSettings::values.game_dirs[i].path),
|
||||||
|
static_cast<jboolean>(AndroidSettings::values.game_dirs[i].deep_scan));
|
||||||
|
env->SetObjectArrayElement(jgameDirArray, i, jgameDir);
|
||||||
|
}
|
||||||
|
return jgameDirArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setGameDirs(JNIEnv* env, jobject obj,
|
||||||
|
jobjectArray gameDirs) {
|
||||||
|
AndroidSettings::values.game_dirs.clear();
|
||||||
|
int size = env->GetArrayLength(gameDirs);
|
||||||
|
|
||||||
|
if (size == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
jobject dir = env->GetObjectArrayElement(gameDirs, 0);
|
||||||
|
jclass gameDirClass = IDCache::GetGameDirClass();
|
||||||
|
jfieldID uriStringField = env->GetFieldID(gameDirClass, "uriString", "Ljava/lang/String;");
|
||||||
|
jfieldID deepScanBooleanField = env->GetFieldID(gameDirClass, "deepScan", "Z");
|
||||||
|
for (int i = 0; i < size; ++i) {
|
||||||
|
dir = env->GetObjectArrayElement(gameDirs, i);
|
||||||
|
jstring juriString = static_cast<jstring>(env->GetObjectField(dir, uriStringField));
|
||||||
|
jboolean jdeepScanBoolean = env->GetBooleanField(dir, deepScanBooleanField);
|
||||||
|
std::string uriString = GetJString(env, juriString);
|
||||||
|
AndroidSettings::values.game_dirs.push_back(
|
||||||
|
AndroidSettings::GameDir{uriString, static_cast<bool>(jdeepScanBoolean)});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_addGameDir(JNIEnv* env, jobject obj,
|
||||||
|
jobject gameDir) {
|
||||||
|
jclass gameDirClass = IDCache::GetGameDirClass();
|
||||||
|
jfieldID uriStringField = env->GetFieldID(gameDirClass, "uriString", "Ljava/lang/String;");
|
||||||
|
jfieldID deepScanBooleanField = env->GetFieldID(gameDirClass, "deepScan", "Z");
|
||||||
|
|
||||||
|
jstring juriString = static_cast<jstring>(env->GetObjectField(gameDir, uriStringField));
|
||||||
|
jboolean jdeepScanBoolean = env->GetBooleanField(gameDir, deepScanBooleanField);
|
||||||
|
std::string uriString = GetJString(env, juriString);
|
||||||
|
AndroidSettings::values.game_dirs.push_back(
|
||||||
|
AndroidSettings::GameDir{uriString, static_cast<bool>(jdeepScanBoolean)});
|
||||||
|
}
|
||||||
|
|
||||||
} // extern "C"
|
} // extern "C"
|
||||||
|
|
70
src/android/app/src/main/res/layout/card_folder.xml
Executable file
70
src/android/app/src/main/res/layout/card_folder.xml
Executable file
|
@ -0,0 +1,70 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
style="?attr/materialCardViewOutlinedStyle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="16dp"
|
||||||
|
android:layout_marginVertical="12dp"
|
||||||
|
android:focusable="true">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:animateLayoutChanges="true">
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/path"
|
||||||
|
style="@style/TextAppearance.Material3.BodyLarge"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical|start"
|
||||||
|
android:ellipsize="none"
|
||||||
|
android:marqueeRepeatLimit="marquee_forever"
|
||||||
|
android:requiresFadingEdge="horizontal"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/button_layout"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="@string/select_gpu_driver_default" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/button_layout"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_edit"
|
||||||
|
style="@style/Widget.Material3.Button.IconButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription="@string/delete"
|
||||||
|
android:tooltipText="@string/edit"
|
||||||
|
app:icon="@drawable/ic_edit"
|
||||||
|
app:iconTint="?attr/colorControlNormal" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_delete"
|
||||||
|
style="@style/Widget.Material3.Button.IconButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription="@string/delete"
|
||||||
|
android:tooltipText="@string/delete"
|
||||||
|
app:icon="@drawable/ic_delete"
|
||||||
|
app:iconTint="?attr/colorControlNormal" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
45
src/android/app/src/main/res/layout/dialog_add_folder.xml
Executable file
45
src/android/app/src/main/res/layout/dialog_add_folder.xml
Executable file
|
@ -0,0 +1,45 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="24dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/path"
|
||||||
|
style="@style/TextAppearance.Material3.BodyLarge"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_gravity="center_vertical|start"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:ellipsize="marquee"
|
||||||
|
android:marqueeRepeatLimit="marquee_forever"
|
||||||
|
android:requiresFadingEdge="horizontal"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
|
tools:text="folder/folder/folder/folder" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingTop="8dp">
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
style="@style/TextAppearance.Material3.BodyMedium"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical|start"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/deep_scan"
|
||||||
|
android:textAlignment="viewStart" />
|
||||||
|
|
||||||
|
<com.google.android.material.checkbox.MaterialCheckBox
|
||||||
|
android:id="@+id/deep_scan_switch"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
30
src/android/app/src/main/res/layout/dialog_folder_properties.xml
Executable file
30
src/android/app/src/main/res/layout/dialog_folder_properties.xml
Executable file
|
@ -0,0 +1,30 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="24dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/deep_scan_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
style="@style/TextAppearance.Material3.BodyMedium"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical|start"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/deep_scan"
|
||||||
|
android:textAlignment="viewStart" />
|
||||||
|
|
||||||
|
<com.google.android.material.checkbox.MaterialCheckBox
|
||||||
|
android:id="@+id/deep_scan_switch"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
48
src/android/app/src/main/res/layout/fragment_folders.xml
Executable file
48
src/android/app/src/main/res/layout/fragment_folders.xml
Executable file
|
@ -0,0 +1,48 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:id="@+id/coordinator_folders"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="?attr/colorSurface">
|
||||||
|
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:id="@+id/appbar_folders"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fitsSystemWindows="true"
|
||||||
|
app:liftOnScrollTargetViewId="@id/list_folders">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@+id/toolbar_folders"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
app:navigationIcon="@drawable/ic_back"
|
||||||
|
app:title="@string/game_folders" />
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/list_folders"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
|
android:id="@+id/button_add"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom|end"
|
||||||
|
android:contentDescription="@string/add_games"
|
||||||
|
app:srcCompat="@drawable/ic_add"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -28,6 +28,9 @@
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_homeSettingsFragment_to_appletLauncherFragment"
|
android:id="@+id/action_homeSettingsFragment_to_appletLauncherFragment"
|
||||||
app:destination="@id/appletLauncherFragment" />
|
app:destination="@id/appletLauncherFragment" />
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_homeSettingsFragment_to_gameFoldersFragment"
|
||||||
|
app:destination="@id/gameFoldersFragment" />
|
||||||
</fragment>
|
</fragment>
|
||||||
|
|
||||||
<fragment
|
<fragment
|
||||||
|
@ -117,5 +120,9 @@
|
||||||
android:id="@+id/cabinetLauncherDialogFragment"
|
android:id="@+id/cabinetLauncherDialogFragment"
|
||||||
android:name="org.yuzu.yuzu_emu.fragments.CabinetLauncherDialogFragment"
|
android:name="org.yuzu.yuzu_emu.fragments.CabinetLauncherDialogFragment"
|
||||||
android:label="CabinetLauncherDialogFragment" />
|
android:label="CabinetLauncherDialogFragment" />
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/gameFoldersFragment"
|
||||||
|
android:name="org.yuzu.yuzu_emu.fragments.GameFoldersFragment"
|
||||||
|
android:label="GameFoldersFragment" />
|
||||||
|
|
||||||
</navigation>
|
</navigation>
|
||||||
|
|
|
@ -175,6 +175,24 @@
|
||||||
<item>2</item>
|
<item>2</item>
|
||||||
</integer-array>
|
</integer-array>
|
||||||
|
|
||||||
|
<string-array name="cpuBackendArm64Names">
|
||||||
|
<item>@string/cpu_backend_dynarmic</item>
|
||||||
|
<item>@string/cpu_backend_nce</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
|
<integer-array name="cpuBackendArm64Values">
|
||||||
|
<item>0</item>
|
||||||
|
<item>1</item>
|
||||||
|
</integer-array>
|
||||||
|
|
||||||
|
<string-array name="cpuBackendX86Names">
|
||||||
|
<item>@string/cpu_backend_dynarmic</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
|
<integer-array name="cpuBackendX86Values">
|
||||||
|
<item>0</item>
|
||||||
|
</integer-array>
|
||||||
|
|
||||||
<string-array name="cpuAccuracyNames">
|
<string-array name="cpuAccuracyNames">
|
||||||
<item>@string/auto</item>
|
<item>@string/auto</item>
|
||||||
<item>@string/cpu_accuracy_accurate</item>
|
<item>@string/cpu_accuracy_accurate</item>
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
<dimen name="menu_width">256dp</dimen>
|
<dimen name="menu_width">256dp</dimen>
|
||||||
<dimen name="card_width">165dp</dimen>
|
<dimen name="card_width">165dp</dimen>
|
||||||
<dimen name="icon_inset">24dp</dimen>
|
<dimen name="icon_inset">24dp</dimen>
|
||||||
<dimen name="spacing_bottom_list_fab">72dp</dimen>
|
<dimen name="spacing_bottom_list_fab">76dp</dimen>
|
||||||
<dimen name="spacing_fab">24dp</dimen>
|
<dimen name="spacing_fab">24dp</dimen>
|
||||||
|
|
||||||
<dimen name="dialog_margin">20dp</dimen>
|
<dimen name="dialog_margin">20dp</dimen>
|
||||||
|
|
|
@ -38,6 +38,7 @@
|
||||||
<string name="empty_gamelist">No files were found or no game directory has been selected yet.</string>
|
<string name="empty_gamelist">No files were found or no game directory has been selected yet.</string>
|
||||||
<string name="search_and_filter_games">Search and filter games</string>
|
<string name="search_and_filter_games">Search and filter games</string>
|
||||||
<string name="select_games_folder">Select games folder</string>
|
<string name="select_games_folder">Select games folder</string>
|
||||||
|
<string name="manage_game_folders">Manage game folders</string>
|
||||||
<string name="select_games_folder_description">Allows yuzu to populate the games list</string>
|
<string name="select_games_folder_description">Allows yuzu to populate the games list</string>
|
||||||
<string name="add_games_warning">Skip selecting games folder?</string>
|
<string name="add_games_warning">Skip selecting games folder?</string>
|
||||||
<string name="add_games_warning_description">Games won\'t be displayed in the Games list if a folder isn\'t selected.</string>
|
<string name="add_games_warning_description">Games won\'t be displayed in the Games list if a folder isn\'t selected.</string>
|
||||||
|
@ -124,6 +125,11 @@
|
||||||
<string name="manage_yuzu_data_description">Import/export firmware, keys, user data, and more!</string>
|
<string name="manage_yuzu_data_description">Import/export firmware, keys, user data, and more!</string>
|
||||||
<string name="share_save_file">Share save file</string>
|
<string name="share_save_file">Share save file</string>
|
||||||
<string name="export_save_failed">Failed to export save</string>
|
<string name="export_save_failed">Failed to export save</string>
|
||||||
|
<string name="game_folders">Game folders</string>
|
||||||
|
<string name="deep_scan">Deep scan</string>
|
||||||
|
<string name="add_game_folder">Add game folder</string>
|
||||||
|
<string name="folder_already_added">This folder was already added!</string>
|
||||||
|
<string name="game_folder_properties">Game folder properties</string>
|
||||||
|
|
||||||
<!-- Applet launcher strings -->
|
<!-- Applet launcher strings -->
|
||||||
<string name="applets">Applet launcher</string>
|
<string name="applets">Applet launcher</string>
|
||||||
|
@ -185,6 +191,7 @@
|
||||||
<string name="frame_limit_enable_description">Limits emulation speed to a specified percentage of normal speed.</string>
|
<string name="frame_limit_enable_description">Limits emulation speed to a specified percentage of normal speed.</string>
|
||||||
<string name="frame_limit_slider">Limit speed percent</string>
|
<string name="frame_limit_slider">Limit speed percent</string>
|
||||||
<string name="frame_limit_slider_description">Specifies the percentage to limit emulation speed. 100% is the normal speed. Values higher or lower will increase or decrease the speed limit.</string>
|
<string name="frame_limit_slider_description">Specifies the percentage to limit emulation speed. 100% is the normal speed. Values higher or lower will increase or decrease the speed limit.</string>
|
||||||
|
<string name="cpu_backend">CPU backend</string>
|
||||||
<string name="cpu_accuracy">CPU accuracy</string>
|
<string name="cpu_accuracy">CPU accuracy</string>
|
||||||
<string name="value_with_units">%1$s%2$s</string>
|
<string name="value_with_units">%1$s%2$s</string>
|
||||||
|
|
||||||
|
@ -257,6 +264,7 @@
|
||||||
<string name="cancelling">Cancelling</string>
|
<string name="cancelling">Cancelling</string>
|
||||||
<string name="install">Install</string>
|
<string name="install">Install</string>
|
||||||
<string name="delete">Delete</string>
|
<string name="delete">Delete</string>
|
||||||
|
<string name="edit">Edit</string>
|
||||||
<string name="export_success">Exported successfully</string>
|
<string name="export_success">Exported successfully</string>
|
||||||
|
|
||||||
<!-- GPU driver installation -->
|
<!-- GPU driver installation -->
|
||||||
|
@ -416,6 +424,10 @@
|
||||||
<string name="ratio_force_sixteen_ten">Force 16:10</string>
|
<string name="ratio_force_sixteen_ten">Force 16:10</string>
|
||||||
<string name="ratio_stretch">Stretch to window</string>
|
<string name="ratio_stretch">Stretch to window</string>
|
||||||
|
|
||||||
|
<!-- CPU Backend -->
|
||||||
|
<string name="cpu_backend_dynarmic">Dynarmic (Slow)</string>
|
||||||
|
<string name="cpu_backend_nce">Native code execution (NCE)</string>
|
||||||
|
|
||||||
<!-- CPU Accuracy -->
|
<!-- CPU Accuracy -->
|
||||||
<string name="cpu_accuracy_accurate">Accurate</string>
|
<string name="cpu_accuracy_accurate">Accurate</string>
|
||||||
<string name="cpu_accuracy_unsafe">Unsafe</string>
|
<string name="cpu_accuracy_unsafe">Unsafe</string>
|
||||||
|
|
|
@ -52,6 +52,7 @@ add_library(common STATIC
|
||||||
fiber.cpp
|
fiber.cpp
|
||||||
fiber.h
|
fiber.h
|
||||||
fixed_point.h
|
fixed_point.h
|
||||||
|
free_region_manager.h
|
||||||
fs/file.cpp
|
fs/file.cpp
|
||||||
fs/file.h
|
fs/file.h
|
||||||
fs/fs.cpp
|
fs/fs.cpp
|
||||||
|
@ -166,6 +167,13 @@ if (WIN32)
|
||||||
target_link_libraries(common PRIVATE ntdll)
|
target_link_libraries(common PRIVATE ntdll)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if (NOT WIN32)
|
||||||
|
target_sources(common PRIVATE
|
||||||
|
signal_chain.cpp
|
||||||
|
signal_chain.h
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
if(ANDROID)
|
if(ANDROID)
|
||||||
target_sources(common
|
target_sources(common
|
||||||
PRIVATE
|
PRIVATE
|
||||||
|
@ -174,6 +182,15 @@ if(ANDROID)
|
||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if (UNIX)
|
||||||
|
target_sources(common PRIVATE
|
||||||
|
linux/gamemode.cpp
|
||||||
|
linux/gamemode.h
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(common PRIVATE gamemode)
|
||||||
|
endif()
|
||||||
|
|
||||||
if(ARCHITECTURE_x86_64)
|
if(ARCHITECTURE_x86_64)
|
||||||
target_sources(common
|
target_sources(common
|
||||||
PRIVATE
|
PRIVATE
|
||||||
|
@ -191,7 +208,7 @@ if(ARCHITECTURE_x86_64)
|
||||||
target_link_libraries(common PRIVATE xbyak::xbyak)
|
target_link_libraries(common PRIVATE xbyak::xbyak)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if (ARCHITECTURE_arm64 AND (ANDROID OR LINUX))
|
if (HAS_NCE)
|
||||||
target_sources(common
|
target_sources(common
|
||||||
PRIVATE
|
PRIVATE
|
||||||
arm64/native_clock.cpp
|
arm64/native_clock.cpp
|
||||||
|
|
55
src/common/free_region_manager.h
Executable file
55
src/common/free_region_manager.h
Executable file
|
@ -0,0 +1,55 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <mutex>
|
||||||
|
#include <boost/icl/interval_set.hpp>
|
||||||
|
|
||||||
|
namespace Common {
|
||||||
|
|
||||||
|
class FreeRegionManager {
|
||||||
|
public:
|
||||||
|
explicit FreeRegionManager() = default;
|
||||||
|
~FreeRegionManager() = default;
|
||||||
|
|
||||||
|
void SetAddressSpace(void* start, size_t size) {
|
||||||
|
this->FreeBlock(start, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pair<void*, size_t> FreeBlock(void* block_ptr, size_t size) {
|
||||||
|
std::scoped_lock lk(m_mutex);
|
||||||
|
|
||||||
|
// Check to see if we are adjacent to any regions.
|
||||||
|
auto start_address = reinterpret_cast<uintptr_t>(block_ptr);
|
||||||
|
auto end_address = start_address + size;
|
||||||
|
auto it = m_free_regions.find({start_address - 1, end_address + 1});
|
||||||
|
|
||||||
|
// If we are, join with them, ensuring we stay in bounds.
|
||||||
|
if (it != m_free_regions.end()) {
|
||||||
|
start_address = std::min(start_address, it->lower());
|
||||||
|
end_address = std::max(end_address, it->upper());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Free the relevant region.
|
||||||
|
m_free_regions.insert({start_address, end_address});
|
||||||
|
|
||||||
|
// Return the adjusted pointers.
|
||||||
|
block_ptr = reinterpret_cast<void*>(start_address);
|
||||||
|
size = end_address - start_address;
|
||||||
|
return {block_ptr, size};
|
||||||
|
}
|
||||||
|
|
||||||
|
void AllocateBlock(void* block_ptr, size_t size) {
|
||||||
|
std::scoped_lock lk(m_mutex);
|
||||||
|
|
||||||
|
auto address = reinterpret_cast<uintptr_t>(block_ptr);
|
||||||
|
m_free_regions.subtract({address, address + size});
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::mutex m_mutex;
|
||||||
|
boost::icl::interval_set<uintptr_t> m_free_regions;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Common
|
|
@ -21,15 +21,18 @@
|
||||||
#include <boost/icl/interval_set.hpp>
|
#include <boost/icl/interval_set.hpp>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <sys/mman.h>
|
#include <sys/mman.h>
|
||||||
|
#include <sys/random.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include "common/scope_exit.h"
|
#include "common/scope_exit.h"
|
||||||
|
|
||||||
#endif // ^^^ Linux ^^^
|
#endif // ^^^ Linux ^^^
|
||||||
|
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
#include <random>
|
||||||
|
|
||||||
#include "common/alignment.h"
|
#include "common/alignment.h"
|
||||||
#include "common/assert.h"
|
#include "common/assert.h"
|
||||||
|
#include "common/free_region_manager.h"
|
||||||
#include "common/host_memory.h"
|
#include "common/host_memory.h"
|
||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
|
|
||||||
|
@ -141,7 +144,7 @@ public:
|
||||||
Release();
|
Release();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Map(size_t virtual_offset, size_t host_offset, size_t length) {
|
void Map(size_t virtual_offset, size_t host_offset, size_t length, MemoryPermission perms) {
|
||||||
std::unique_lock lock{placeholder_mutex};
|
std::unique_lock lock{placeholder_mutex};
|
||||||
if (!IsNiechePlaceholder(virtual_offset, length)) {
|
if (!IsNiechePlaceholder(virtual_offset, length)) {
|
||||||
Split(virtual_offset, length);
|
Split(virtual_offset, length);
|
||||||
|
@ -160,7 +163,7 @@ public:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Protect(size_t virtual_offset, size_t length, bool read, bool write) {
|
void Protect(size_t virtual_offset, size_t length, bool read, bool write, bool execute) {
|
||||||
DWORD new_flags{};
|
DWORD new_flags{};
|
||||||
if (read && write) {
|
if (read && write) {
|
||||||
new_flags = PAGE_READWRITE;
|
new_flags = PAGE_READWRITE;
|
||||||
|
@ -186,6 +189,11 @@ public:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void EnableDirectMappedAddress() {
|
||||||
|
// TODO
|
||||||
|
UNREACHABLE();
|
||||||
|
}
|
||||||
|
|
||||||
const size_t backing_size; ///< Size of the backing memory in bytes
|
const size_t backing_size; ///< Size of the backing memory in bytes
|
||||||
const size_t virtual_size; ///< Size of the virtual address placeholder in bytes
|
const size_t virtual_size; ///< Size of the virtual address placeholder in bytes
|
||||||
|
|
||||||
|
@ -353,6 +361,55 @@ private:
|
||||||
|
|
||||||
#elif defined(__linux__) || defined(__FreeBSD__) // ^^^ Windows ^^^ vvv Linux vvv
|
#elif defined(__linux__) || defined(__FreeBSD__) // ^^^ Windows ^^^ vvv Linux vvv
|
||||||
|
|
||||||
|
#ifdef ARCHITECTURE_arm64
|
||||||
|
|
||||||
|
static void* ChooseVirtualBase(size_t virtual_size) {
|
||||||
|
constexpr uintptr_t Map39BitSize = (1ULL << 39);
|
||||||
|
constexpr uintptr_t Map36BitSize = (1ULL << 36);
|
||||||
|
|
||||||
|
// This is not a cryptographic application, we just want something random.
|
||||||
|
std::mt19937_64 rng;
|
||||||
|
|
||||||
|
// We want to ensure we are allocating at an address aligned to the L2 block size.
|
||||||
|
// For Qualcomm devices, we must also allocate memory above 36 bits.
|
||||||
|
const size_t lower = Map36BitSize / HugePageSize;
|
||||||
|
const size_t upper = (Map39BitSize - virtual_size) / HugePageSize;
|
||||||
|
const size_t range = upper - lower;
|
||||||
|
|
||||||
|
// Try up to 64 times to allocate memory at random addresses in the range.
|
||||||
|
for (int i = 0; i < 64; i++) {
|
||||||
|
// Calculate a possible location.
|
||||||
|
uintptr_t hint_address = ((rng() % range) + lower) * HugePageSize;
|
||||||
|
|
||||||
|
// Try to map.
|
||||||
|
// Note: we may be able to take advantage of MAP_FIXED_NOREPLACE here.
|
||||||
|
void* map_pointer =
|
||||||
|
mmap(reinterpret_cast<void*>(hint_address), virtual_size, PROT_READ | PROT_WRITE,
|
||||||
|
MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0);
|
||||||
|
|
||||||
|
// If we successfully mapped, we're done.
|
||||||
|
if (reinterpret_cast<uintptr_t>(map_pointer) == hint_address) {
|
||||||
|
return map_pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmap if necessary, and try again.
|
||||||
|
if (map_pointer != MAP_FAILED) {
|
||||||
|
munmap(map_pointer, virtual_size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return MAP_FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
static void* ChooseVirtualBase(size_t virtual_size) {
|
||||||
|
return mmap(nullptr, virtual_size, PROT_READ | PROT_WRITE,
|
||||||
|
MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
class HostMemory::Impl {
|
class HostMemory::Impl {
|
||||||
public:
|
public:
|
||||||
explicit Impl(size_t backing_size_, size_t virtual_size_)
|
explicit Impl(size_t backing_size_, size_t virtual_size_)
|
||||||
|
@ -415,8 +472,7 @@ public:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
virtual_base = static_cast<u8*>(mmap(nullptr, virtual_size, PROT_NONE,
|
virtual_base = virtual_map_base = static_cast<u8*>(ChooseVirtualBase(virtual_size));
|
||||||
MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0));
|
|
||||||
if (virtual_base == MAP_FAILED) {
|
if (virtual_base == MAP_FAILED) {
|
||||||
LOG_CRITICAL(HW_Memory, "mmap failed: {}", strerror(errno));
|
LOG_CRITICAL(HW_Memory, "mmap failed: {}", strerror(errno));
|
||||||
throw std::bad_alloc{};
|
throw std::bad_alloc{};
|
||||||
|
@ -424,7 +480,7 @@ public:
|
||||||
madvise(virtual_base, virtual_size, MADV_HUGEPAGE);
|
madvise(virtual_base, virtual_size, MADV_HUGEPAGE);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
placeholders.add({0, virtual_size});
|
free_manager.SetAddressSpace(virtual_base, virtual_size);
|
||||||
good = true;
|
good = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -432,14 +488,29 @@ public:
|
||||||
Release();
|
Release();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Map(size_t virtual_offset, size_t host_offset, size_t length) {
|
void Map(size_t virtual_offset, size_t host_offset, size_t length, MemoryPermission perms) {
|
||||||
{
|
// Intersect the range with our address space.
|
||||||
std::scoped_lock lock{placeholder_mutex};
|
AdjustMap(&virtual_offset, &length);
|
||||||
placeholders.subtract({virtual_offset, virtual_offset + length});
|
|
||||||
}
|
|
||||||
|
|
||||||
void* ret = mmap(virtual_base + virtual_offset, length, PROT_READ | PROT_WRITE,
|
// We are removing a placeholder.
|
||||||
MAP_SHARED | MAP_FIXED, fd, host_offset);
|
free_manager.AllocateBlock(virtual_base + virtual_offset, length);
|
||||||
|
|
||||||
|
// Deduce mapping protection flags.
|
||||||
|
int flags = PROT_NONE;
|
||||||
|
if (True(perms & MemoryPermission::Read)) {
|
||||||
|
flags |= PROT_READ;
|
||||||
|
}
|
||||||
|
if (True(perms & MemoryPermission::Write)) {
|
||||||
|
flags |= PROT_WRITE;
|
||||||
|
}
|
||||||
|
#ifdef ARCHITECTURE_arm64
|
||||||
|
if (True(perms & MemoryPermission::Execute)) {
|
||||||
|
flags |= PROT_EXEC;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void* ret = mmap(virtual_base + virtual_offset, length, flags, MAP_SHARED | MAP_FIXED, fd,
|
||||||
|
host_offset);
|
||||||
ASSERT_MSG(ret != MAP_FAILED, "mmap failed: {}", strerror(errno));
|
ASSERT_MSG(ret != MAP_FAILED, "mmap failed: {}", strerror(errno));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -447,47 +518,54 @@ public:
|
||||||
// The method name is wrong. We're still talking about the virtual range.
|
// The method name is wrong. We're still talking about the virtual range.
|
||||||
// We don't want to unmap, we want to reserve this memory.
|
// We don't want to unmap, we want to reserve this memory.
|
||||||
|
|
||||||
{
|
// Intersect the range with our address space.
|
||||||
std::scoped_lock lock{placeholder_mutex};
|
AdjustMap(&virtual_offset, &length);
|
||||||
auto it = placeholders.find({virtual_offset - 1, virtual_offset + length + 1});
|
|
||||||
|
|
||||||
if (it != placeholders.end()) {
|
// Merge with any adjacent placeholder mappings.
|
||||||
size_t prev_upper = virtual_offset + length;
|
auto [merged_pointer, merged_size] =
|
||||||
virtual_offset = std::min(virtual_offset, it->lower());
|
free_manager.FreeBlock(virtual_base + virtual_offset, length);
|
||||||
length = std::max(it->upper(), prev_upper) - virtual_offset;
|
|
||||||
}
|
|
||||||
|
|
||||||
placeholders.add({virtual_offset, virtual_offset + length});
|
void* ret = mmap(merged_pointer, merged_size, PROT_NONE,
|
||||||
}
|
|
||||||
|
|
||||||
void* ret = mmap(virtual_base + virtual_offset, length, PROT_NONE,
|
|
||||||
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
|
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
|
||||||
ASSERT_MSG(ret != MAP_FAILED, "mmap failed: {}", strerror(errno));
|
ASSERT_MSG(ret != MAP_FAILED, "mmap failed: {}", strerror(errno));
|
||||||
}
|
}
|
||||||
|
|
||||||
void Protect(size_t virtual_offset, size_t length, bool read, bool write) {
|
void Protect(size_t virtual_offset, size_t length, bool read, bool write, bool execute) {
|
||||||
int flags = 0;
|
// Intersect the range with our address space.
|
||||||
|
AdjustMap(&virtual_offset, &length);
|
||||||
|
|
||||||
|
int flags = PROT_NONE;
|
||||||
if (read) {
|
if (read) {
|
||||||
flags |= PROT_READ;
|
flags |= PROT_READ;
|
||||||
}
|
}
|
||||||
if (write) {
|
if (write) {
|
||||||
flags |= PROT_WRITE;
|
flags |= PROT_WRITE;
|
||||||
}
|
}
|
||||||
|
#ifdef HAS_NCE
|
||||||
|
if (execute) {
|
||||||
|
flags |= PROT_EXEC;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
int ret = mprotect(virtual_base + virtual_offset, length, flags);
|
int ret = mprotect(virtual_base + virtual_offset, length, flags);
|
||||||
ASSERT_MSG(ret == 0, "mprotect failed: {}", strerror(errno));
|
ASSERT_MSG(ret == 0, "mprotect failed: {}", strerror(errno));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void EnableDirectMappedAddress() {
|
||||||
|
virtual_base = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
const size_t backing_size; ///< Size of the backing memory in bytes
|
const size_t backing_size; ///< Size of the backing memory in bytes
|
||||||
const size_t virtual_size; ///< Size of the virtual address placeholder in bytes
|
const size_t virtual_size; ///< Size of the virtual address placeholder in bytes
|
||||||
|
|
||||||
u8* backing_base{reinterpret_cast<u8*>(MAP_FAILED)};
|
u8* backing_base{reinterpret_cast<u8*>(MAP_FAILED)};
|
||||||
u8* virtual_base{reinterpret_cast<u8*>(MAP_FAILED)};
|
u8* virtual_base{reinterpret_cast<u8*>(MAP_FAILED)};
|
||||||
|
u8* virtual_map_base{reinterpret_cast<u8*>(MAP_FAILED)};
|
||||||
|
|
||||||
private:
|
private:
|
||||||
/// Release all resources in the object
|
/// Release all resources in the object
|
||||||
void Release() {
|
void Release() {
|
||||||
if (virtual_base != MAP_FAILED) {
|
if (virtual_map_base != MAP_FAILED) {
|
||||||
int ret = munmap(virtual_base, virtual_size);
|
int ret = munmap(virtual_map_base, virtual_size);
|
||||||
ASSERT_MSG(ret == 0, "munmap failed: {}", strerror(errno));
|
ASSERT_MSG(ret == 0, "munmap failed: {}", strerror(errno));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -502,10 +580,29 @@ private:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int fd{-1}; // memfd file descriptor, -1 is the error value of memfd_create
|
void AdjustMap(size_t* virtual_offset, size_t* length) {
|
||||||
|
if (virtual_base != nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
boost::icl::interval_set<size_t> placeholders; ///< Mapped placeholders
|
// If we are direct mapped, we want to make sure we are operating on a region
|
||||||
std::mutex placeholder_mutex; ///< Mutex for placeholders
|
// that is in range of our virtual mapping.
|
||||||
|
size_t intended_start = *virtual_offset;
|
||||||
|
size_t intended_end = intended_start + *length;
|
||||||
|
size_t address_space_start = reinterpret_cast<size_t>(virtual_map_base);
|
||||||
|
size_t address_space_end = address_space_start + virtual_size;
|
||||||
|
|
||||||
|
if (address_space_start > intended_end || intended_start > address_space_end) {
|
||||||
|
*virtual_offset = 0;
|
||||||
|
*length = 0;
|
||||||
|
} else {
|
||||||
|
*virtual_offset = std::max(intended_start, address_space_start);
|
||||||
|
*length = std::min(intended_end, address_space_end) - *virtual_offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int fd{-1}; // memfd file descriptor, -1 is the error value of memfd_create
|
||||||
|
FreeRegionManager free_manager{};
|
||||||
};
|
};
|
||||||
|
|
||||||
#else // ^^^ Linux ^^^ vvv Generic vvv
|
#else // ^^^ Linux ^^^ vvv Generic vvv
|
||||||
|
@ -518,11 +615,13 @@ public:
|
||||||
throw std::bad_alloc{};
|
throw std::bad_alloc{};
|
||||||
}
|
}
|
||||||
|
|
||||||
void Map(size_t virtual_offset, size_t host_offset, size_t length) {}
|
void Map(size_t virtual_offset, size_t host_offset, size_t length, MemoryPermission perm) {}
|
||||||
|
|
||||||
void Unmap(size_t virtual_offset, size_t length) {}
|
void Unmap(size_t virtual_offset, size_t length) {}
|
||||||
|
|
||||||
void Protect(size_t virtual_offset, size_t length, bool read, bool write) {}
|
void Protect(size_t virtual_offset, size_t length, bool read, bool write, bool execute) {}
|
||||||
|
|
||||||
|
void EnableDirectMappedAddress() {}
|
||||||
|
|
||||||
u8* backing_base{nullptr};
|
u8* backing_base{nullptr};
|
||||||
u8* virtual_base{nullptr};
|
u8* virtual_base{nullptr};
|
||||||
|
@ -535,15 +634,16 @@ HostMemory::HostMemory(size_t backing_size_, size_t virtual_size_)
|
||||||
try {
|
try {
|
||||||
// Try to allocate a fastmem arena.
|
// Try to allocate a fastmem arena.
|
||||||
// The implementation will fail with std::bad_alloc on errors.
|
// The implementation will fail with std::bad_alloc on errors.
|
||||||
impl = std::make_unique<HostMemory::Impl>(AlignUp(backing_size, PageAlignment),
|
impl =
|
||||||
AlignUp(virtual_size, PageAlignment) +
|
std::make_unique<HostMemory::Impl>(AlignUp(backing_size, PageAlignment),
|
||||||
3 * HugePageSize);
|
AlignUp(virtual_size, PageAlignment) + HugePageSize);
|
||||||
backing_base = impl->backing_base;
|
backing_base = impl->backing_base;
|
||||||
virtual_base = impl->virtual_base;
|
virtual_base = impl->virtual_base;
|
||||||
|
|
||||||
if (virtual_base) {
|
if (virtual_base) {
|
||||||
virtual_base += 2 * HugePageSize - 1;
|
// Ensure the virtual base is aligned to the L2 block size.
|
||||||
virtual_base -= reinterpret_cast<size_t>(virtual_base) & (HugePageSize - 1);
|
virtual_base = reinterpret_cast<u8*>(
|
||||||
|
Common::AlignUp(reinterpret_cast<uintptr_t>(virtual_base), HugePageSize));
|
||||||
virtual_base_offset = virtual_base - impl->virtual_base;
|
virtual_base_offset = virtual_base - impl->virtual_base;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -562,7 +662,8 @@ HostMemory::HostMemory(HostMemory&&) noexcept = default;
|
||||||
|
|
||||||
HostMemory& HostMemory::operator=(HostMemory&&) noexcept = default;
|
HostMemory& HostMemory::operator=(HostMemory&&) noexcept = default;
|
||||||
|
|
||||||
void HostMemory::Map(size_t virtual_offset, size_t host_offset, size_t length) {
|
void HostMemory::Map(size_t virtual_offset, size_t host_offset, size_t length,
|
||||||
|
MemoryPermission perms) {
|
||||||
ASSERT(virtual_offset % PageAlignment == 0);
|
ASSERT(virtual_offset % PageAlignment == 0);
|
||||||
ASSERT(host_offset % PageAlignment == 0);
|
ASSERT(host_offset % PageAlignment == 0);
|
||||||
ASSERT(length % PageAlignment == 0);
|
ASSERT(length % PageAlignment == 0);
|
||||||
|
@ -571,7 +672,7 @@ void HostMemory::Map(size_t virtual_offset, size_t host_offset, size_t length) {
|
||||||
if (length == 0 || !virtual_base || !impl) {
|
if (length == 0 || !virtual_base || !impl) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
impl->Map(virtual_offset + virtual_base_offset, host_offset, length);
|
impl->Map(virtual_offset + virtual_base_offset, host_offset, length, perms);
|
||||||
}
|
}
|
||||||
|
|
||||||
void HostMemory::Unmap(size_t virtual_offset, size_t length) {
|
void HostMemory::Unmap(size_t virtual_offset, size_t length) {
|
||||||
|
@ -584,14 +685,22 @@ void HostMemory::Unmap(size_t virtual_offset, size_t length) {
|
||||||
impl->Unmap(virtual_offset + virtual_base_offset, length);
|
impl->Unmap(virtual_offset + virtual_base_offset, length);
|
||||||
}
|
}
|
||||||
|
|
||||||
void HostMemory::Protect(size_t virtual_offset, size_t length, bool read, bool write) {
|
void HostMemory::Protect(size_t virtual_offset, size_t length, bool read, bool write,
|
||||||
|
bool execute) {
|
||||||
ASSERT(virtual_offset % PageAlignment == 0);
|
ASSERT(virtual_offset % PageAlignment == 0);
|
||||||
ASSERT(length % PageAlignment == 0);
|
ASSERT(length % PageAlignment == 0);
|
||||||
ASSERT(virtual_offset + length <= virtual_size);
|
ASSERT(virtual_offset + length <= virtual_size);
|
||||||
if (length == 0 || !virtual_base || !impl) {
|
if (length == 0 || !virtual_base || !impl) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
impl->Protect(virtual_offset + virtual_base_offset, length, read, write);
|
impl->Protect(virtual_offset + virtual_base_offset, length, read, write, execute);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HostMemory::EnableDirectMappedAddress() {
|
||||||
|
if (impl) {
|
||||||
|
impl->EnableDirectMappedAddress();
|
||||||
|
virtual_size += reinterpret_cast<uintptr_t>(virtual_base);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Common
|
} // namespace Common
|
||||||
|
|
|
@ -4,11 +4,20 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include "common/common_funcs.h"
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
#include "common/virtual_buffer.h"
|
#include "common/virtual_buffer.h"
|
||||||
|
|
||||||
namespace Common {
|
namespace Common {
|
||||||
|
|
||||||
|
enum class MemoryPermission : u32 {
|
||||||
|
Read = 1 << 0,
|
||||||
|
Write = 1 << 1,
|
||||||
|
ReadWrite = Read | Write,
|
||||||
|
Execute = 1 << 2,
|
||||||
|
};
|
||||||
|
DECLARE_ENUM_FLAG_OPERATORS(MemoryPermission)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A low level linear memory buffer, which supports multiple mappings
|
* A low level linear memory buffer, which supports multiple mappings
|
||||||
* Its purpose is to rebuild a given sparse memory layout, including mirrors.
|
* Its purpose is to rebuild a given sparse memory layout, including mirrors.
|
||||||
|
@ -31,11 +40,13 @@ public:
|
||||||
HostMemory(HostMemory&& other) noexcept;
|
HostMemory(HostMemory&& other) noexcept;
|
||||||
HostMemory& operator=(HostMemory&& other) noexcept;
|
HostMemory& operator=(HostMemory&& other) noexcept;
|
||||||
|
|
||||||
void Map(size_t virtual_offset, size_t host_offset, size_t length);
|
void Map(size_t virtual_offset, size_t host_offset, size_t length, MemoryPermission perms);
|
||||||
|
|
||||||
void Unmap(size_t virtual_offset, size_t length);
|
void Unmap(size_t virtual_offset, size_t length);
|
||||||
|
|
||||||
void Protect(size_t virtual_offset, size_t length, bool read, bool write);
|
void Protect(size_t virtual_offset, size_t length, bool read, bool write, bool execute = false);
|
||||||
|
|
||||||
|
void EnableDirectMappedAddress();
|
||||||
|
|
||||||
[[nodiscard]] u8* BackingBasePointer() noexcept {
|
[[nodiscard]] u8* BackingBasePointer() noexcept {
|
||||||
return backing_base;
|
return backing_base;
|
||||||
|
|
39
src/common/linux/gamemode.cpp
Executable file
39
src/common/linux/gamemode.cpp
Executable file
|
@ -0,0 +1,39 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include <gamemode_client.h>
|
||||||
|
|
||||||
|
#include "common/linux/gamemode.h"
|
||||||
|
#include "common/settings.h"
|
||||||
|
|
||||||
|
namespace Common::Linux {
|
||||||
|
|
||||||
|
void StartGamemode() {
|
||||||
|
if (Settings::values.enable_gamemode) {
|
||||||
|
if (gamemode_request_start() < 0) {
|
||||||
|
LOG_WARNING(Frontend, "Failed to start gamemode: {}", gamemode_error_string());
|
||||||
|
} else {
|
||||||
|
LOG_INFO(Frontend, "Started gamemode");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void StopGamemode() {
|
||||||
|
if (Settings::values.enable_gamemode) {
|
||||||
|
if (gamemode_request_end() < 0) {
|
||||||
|
LOG_WARNING(Frontend, "Failed to stop gamemode: {}", gamemode_error_string());
|
||||||
|
} else {
|
||||||
|
LOG_INFO(Frontend, "Stopped gamemode");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetGamemodeState(bool state) {
|
||||||
|
if (state) {
|
||||||
|
StartGamemode();
|
||||||
|
} else {
|
||||||
|
StopGamemode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Common::Linux
|
24
src/common/linux/gamemode.h
Executable file
24
src/common/linux/gamemode.h
Executable file
|
@ -0,0 +1,24 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace Common::Linux {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the (Feral Interactive) Linux gamemode if it is installed and it is activated
|
||||||
|
*/
|
||||||
|
void StartGamemode();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the (Feral Interactive) Linux gamemode if it is installed and it is activated
|
||||||
|
*/
|
||||||
|
void StopGamemode();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start or stop the (Feral Interactive) Linux gamemode if it is installed and it is activated
|
||||||
|
* @param state The new state the gamemode should have
|
||||||
|
*/
|
||||||
|
void SetGamemodeState(bool state);
|
||||||
|
|
||||||
|
} // namespace Common::Linux
|
|
@ -41,6 +41,7 @@ SWITCHABLE(AspectRatio, true);
|
||||||
SWITCHABLE(AstcDecodeMode, true);
|
SWITCHABLE(AstcDecodeMode, true);
|
||||||
SWITCHABLE(AstcRecompression, true);
|
SWITCHABLE(AstcRecompression, true);
|
||||||
SWITCHABLE(AudioMode, true);
|
SWITCHABLE(AudioMode, true);
|
||||||
|
SWITCHABLE(CpuBackend, true);
|
||||||
SWITCHABLE(CpuAccuracy, true);
|
SWITCHABLE(CpuAccuracy, true);
|
||||||
SWITCHABLE(FullscreenMode, true);
|
SWITCHABLE(FullscreenMode, true);
|
||||||
SWITCHABLE(GpuAccuracy, true);
|
SWITCHABLE(GpuAccuracy, true);
|
||||||
|
@ -155,6 +156,22 @@ bool IsFastmemEnabled() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool is_nce_enabled = false;
|
||||||
|
|
||||||
|
void SetNceEnabled(bool is_39bit) {
|
||||||
|
const bool is_nce_selected = values.cpu_backend.GetValue() == CpuBackend::Nce;
|
||||||
|
is_nce_enabled = IsFastmemEnabled() && is_nce_selected && is_39bit;
|
||||||
|
if (is_nce_selected && !is_nce_enabled) {
|
||||||
|
LOG_WARNING(
|
||||||
|
Common,
|
||||||
|
"Program does not utilize 39-bit address space, unable to natively execute code");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsNceEnabled() {
|
||||||
|
return is_nce_enabled;
|
||||||
|
}
|
||||||
|
|
||||||
bool IsDockedMode() {
|
bool IsDockedMode() {
|
||||||
return values.use_docked_mode.GetValue() == Settings::ConsoleMode::Docked;
|
return values.use_docked_mode.GetValue() == Settings::ConsoleMode::Docked;
|
||||||
}
|
}
|
||||||
|
@ -219,6 +236,8 @@ const char* TranslateCategory(Category category) {
|
||||||
return "Services";
|
return "Services";
|
||||||
case Category::Paths:
|
case Category::Paths:
|
||||||
return "Paths";
|
return "Paths";
|
||||||
|
case Category::Linux:
|
||||||
|
return "Linux";
|
||||||
case Category::MaxEnum:
|
case Category::MaxEnum:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,6 +63,7 @@ SWITCHABLE(AspectRatio, true);
|
||||||
SWITCHABLE(AstcDecodeMode, true);
|
SWITCHABLE(AstcDecodeMode, true);
|
||||||
SWITCHABLE(AstcRecompression, true);
|
SWITCHABLE(AstcRecompression, true);
|
||||||
SWITCHABLE(AudioMode, true);
|
SWITCHABLE(AudioMode, true);
|
||||||
|
SWITCHABLE(CpuBackend, true);
|
||||||
SWITCHABLE(CpuAccuracy, true);
|
SWITCHABLE(CpuAccuracy, true);
|
||||||
SWITCHABLE(FullscreenMode, true);
|
SWITCHABLE(FullscreenMode, true);
|
||||||
SWITCHABLE(GpuAccuracy, true);
|
SWITCHABLE(GpuAccuracy, true);
|
||||||
|
@ -179,6 +180,14 @@ struct Values {
|
||||||
&use_speed_limit};
|
&use_speed_limit};
|
||||||
|
|
||||||
// Cpu
|
// Cpu
|
||||||
|
SwitchableSetting<CpuBackend, true> cpu_backend{
|
||||||
|
linkage, CpuBackend::Dynarmic, CpuBackend::Dynarmic,
|
||||||
|
#ifdef HAS_NCE
|
||||||
|
CpuBackend::Nce,
|
||||||
|
#else
|
||||||
|
CpuBackend::Dynarmic,
|
||||||
|
#endif
|
||||||
|
"cpu_backend", Category::Cpu};
|
||||||
SwitchableSetting<CpuAccuracy, true> cpu_accuracy{linkage, CpuAccuracy::Auto,
|
SwitchableSetting<CpuAccuracy, true> cpu_accuracy{linkage, CpuAccuracy::Auto,
|
||||||
CpuAccuracy::Auto, CpuAccuracy::Paranoid,
|
CpuAccuracy::Auto, CpuAccuracy::Paranoid,
|
||||||
"cpu_accuracy", Category::Cpu};
|
"cpu_accuracy", Category::Cpu};
|
||||||
|
@ -429,6 +438,9 @@ struct Values {
|
||||||
true,
|
true,
|
||||||
true};
|
true};
|
||||||
|
|
||||||
|
// Linux
|
||||||
|
SwitchableSetting<bool> enable_gamemode{linkage, true, "enable_gamemode", Category::Linux};
|
||||||
|
|
||||||
// Controls
|
// Controls
|
||||||
InputSetting<std::array<PlayerInput, 10>> players;
|
InputSetting<std::array<PlayerInput, 10>> players;
|
||||||
|
|
||||||
|
@ -566,6 +578,8 @@ bool IsGPULevelExtreme();
|
||||||
bool IsGPULevelHigh();
|
bool IsGPULevelHigh();
|
||||||
|
|
||||||
bool IsFastmemEnabled();
|
bool IsFastmemEnabled();
|
||||||
|
void SetNceEnabled(bool is_64bit);
|
||||||
|
bool IsNceEnabled();
|
||||||
|
|
||||||
bool IsDockedMode();
|
bool IsDockedMode();
|
||||||
|
|
||||||
|
|
|
@ -41,6 +41,7 @@ enum class Category : u32 {
|
||||||
Multiplayer,
|
Multiplayer,
|
||||||
Services,
|
Services,
|
||||||
Paths,
|
Paths,
|
||||||
|
Linux,
|
||||||
MaxEnum,
|
MaxEnum,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -129,6 +129,8 @@ ENUM(ShaderBackend, Glsl, Glasm, SpirV);
|
||||||
|
|
||||||
ENUM(GpuAccuracy, Normal, High, Extreme);
|
ENUM(GpuAccuracy, Normal, High, Extreme);
|
||||||
|
|
||||||
|
ENUM(CpuBackend, Dynarmic, Nce);
|
||||||
|
|
||||||
ENUM(CpuAccuracy, Auto, Accurate, Unsafe, Paranoid);
|
ENUM(CpuAccuracy, Auto, Accurate, Unsafe, Paranoid);
|
||||||
|
|
||||||
ENUM(MemoryLayout, Memory_4Gb, Memory_6Gb, Memory_8Gb);
|
ENUM(MemoryLayout, Memory_4Gb, Memory_6Gb, Memory_8Gb);
|
||||||
|
|
42
src/common/signal_chain.cpp
Executable file
42
src/common/signal_chain.cpp
Executable file
|
@ -0,0 +1,42 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include <dlfcn.h>
|
||||||
|
|
||||||
|
#include "common/assert.h"
|
||||||
|
#include "common/dynamic_library.h"
|
||||||
|
#include "common/scope_exit.h"
|
||||||
|
#include "common/signal_chain.h"
|
||||||
|
|
||||||
|
namespace Common {
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
T* LookupLibcSymbol(const char* name) {
|
||||||
|
#if defined(__BIONIC__)
|
||||||
|
Common::DynamicLibrary provider("libc.so");
|
||||||
|
if (!provider.IsOpen()) {
|
||||||
|
UNREACHABLE_MSG("Failed to open libc!");
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
// For other operating environments, we assume the symbol is not overridden.
|
||||||
|
const char* base = nullptr;
|
||||||
|
Common::DynamicLibrary provider(base);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void* sym = provider.GetSymbolAddress(name);
|
||||||
|
if (sym == nullptr) {
|
||||||
|
sym = dlsym(RTLD_DEFAULT, name);
|
||||||
|
}
|
||||||
|
if (sym == nullptr) {
|
||||||
|
UNREACHABLE_MSG("Unable to find symbol {}!", name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return reinterpret_cast<T*>(sym);
|
||||||
|
}
|
||||||
|
|
||||||
|
int SigAction(int signum, const struct sigaction* act, struct sigaction* oldact) {
|
||||||
|
static auto libc_sigaction = LookupLibcSymbol<decltype(sigaction)>("sigaction");
|
||||||
|
return libc_sigaction(signum, act, oldact);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Common
|
19
src/common/signal_chain.h
Executable file
19
src/common/signal_chain.h
Executable file
|
@ -0,0 +1,19 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifndef _WIN32
|
||||||
|
|
||||||
|
#include <signal.h>
|
||||||
|
|
||||||
|
namespace Common {
|
||||||
|
|
||||||
|
// Android's ART overrides sigaction with its own wrapper. This is problematic for SIGSEGV
|
||||||
|
// in particular, because ART's handler accesses tpidr_el0, which conflicts with NCE.
|
||||||
|
// This extracts the libc symbol and calls it directly.
|
||||||
|
int SigAction(int signum, const struct sigaction* act, struct sigaction* oldact);
|
||||||
|
|
||||||
|
} // namespace Common
|
||||||
|
|
||||||
|
#endif
|
|
@ -10,7 +10,7 @@
|
||||||
#include "common/x64/rdtsc.h"
|
#include "common/x64/rdtsc.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if defined(ARCHITECTURE_arm64) && defined(__linux__)
|
#ifdef HAS_NCE
|
||||||
#include "common/arm64/native_clock.h"
|
#include "common/arm64/native_clock.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@ std::unique_ptr<WallClock> CreateOptimalClock() {
|
||||||
// - Is not more precise than 1 GHz (1ns resolution)
|
// - Is not more precise than 1 GHz (1ns resolution)
|
||||||
return std::make_unique<StandardWallClock>();
|
return std::make_unique<StandardWallClock>();
|
||||||
}
|
}
|
||||||
#elif defined(ARCHITECTURE_arm64) && defined(__linux__)
|
#elif defined(HAS_NCE)
|
||||||
return std::make_unique<Arm64::NativeClock>();
|
return std::make_unique<Arm64::NativeClock>();
|
||||||
#else
|
#else
|
||||||
return std::make_unique<StandardWallClock>();
|
return std::make_unique<StandardWallClock>();
|
||||||
|
|
|
@ -926,6 +926,22 @@ if (ENABLE_WEB_SERVICE)
|
||||||
target_link_libraries(core PRIVATE web_service)
|
target_link_libraries(core PRIVATE web_service)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if (HAS_NCE)
|
||||||
|
enable_language(C ASM)
|
||||||
|
set(CMAKE_ASM_FLAGS "${CFLAGS} -x assembler-with-cpp")
|
||||||
|
|
||||||
|
target_sources(core PRIVATE
|
||||||
|
arm/nce/arm_nce.cpp
|
||||||
|
arm/nce/arm_nce.h
|
||||||
|
arm/nce/arm_nce.s
|
||||||
|
arm/nce/guest_context.h
|
||||||
|
arm/nce/patch.cpp
|
||||||
|
arm/nce/patch.h
|
||||||
|
arm/nce/instructions.h
|
||||||
|
)
|
||||||
|
target_link_libraries(core PRIVATE merry::oaknut)
|
||||||
|
endif()
|
||||||
|
|
||||||
if (ARCHITECTURE_x86_64 OR ARCHITECTURE_arm64)
|
if (ARCHITECTURE_x86_64 OR ARCHITECTURE_arm64)
|
||||||
target_sources(core PRIVATE
|
target_sources(core PRIVATE
|
||||||
arm/dynarmic/arm_dynarmic.h
|
arm/dynarmic/arm_dynarmic.h
|
||||||
|
|
|
@ -201,6 +201,8 @@ void ARM_Interface::Run() {
|
||||||
if (True(hr & HaltReason::DataAbort)) {
|
if (True(hr & HaltReason::DataAbort)) {
|
||||||
if (system.DebuggerEnabled()) {
|
if (system.DebuggerEnabled()) {
|
||||||
system.GetDebugger().NotifyThreadWatchpoint(current_thread, *HaltedWatchpoint());
|
system.GetDebugger().NotifyThreadWatchpoint(current_thread, *HaltedWatchpoint());
|
||||||
|
} else {
|
||||||
|
LogBacktrace();
|
||||||
}
|
}
|
||||||
current_thread->RequestSuspend(SuspendType::Debug);
|
current_thread->RequestSuspend(SuspendType::Debug);
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -81,6 +81,9 @@ public:
|
||||||
// thread context to be 800 bytes in size.
|
// thread context to be 800 bytes in size.
|
||||||
static_assert(sizeof(ThreadContext64) == 0x320);
|
static_assert(sizeof(ThreadContext64) == 0x320);
|
||||||
|
|
||||||
|
/// Perform any backend-specific initialization.
|
||||||
|
virtual void Initialize() {}
|
||||||
|
|
||||||
/// Runs the CPU until an event happens
|
/// Runs the CPU until an event happens
|
||||||
void Run();
|
void Run();
|
||||||
|
|
||||||
|
|
400
src/core/arm/nce/arm_nce.cpp
Executable file
400
src/core/arm/nce/arm_nce.cpp
Executable file
|
@ -0,0 +1,400 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include <cinttypes>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include "common/signal_chain.h"
|
||||||
|
#include "core/arm/nce/arm_nce.h"
|
||||||
|
#include "core/arm/nce/patch.h"
|
||||||
|
#include "core/core.h"
|
||||||
|
#include "core/memory.h"
|
||||||
|
|
||||||
|
#include "core/hle/kernel/k_process.h"
|
||||||
|
|
||||||
|
#include <signal.h>
|
||||||
|
#include <sys/syscall.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
namespace Core {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct sigaction g_orig_action;
|
||||||
|
|
||||||
|
// Verify assembly offsets.
|
||||||
|
using NativeExecutionParameters = Kernel::KThread::NativeExecutionParameters;
|
||||||
|
static_assert(offsetof(NativeExecutionParameters, native_context) == TpidrEl0NativeContext);
|
||||||
|
static_assert(offsetof(NativeExecutionParameters, lock) == TpidrEl0Lock);
|
||||||
|
static_assert(offsetof(NativeExecutionParameters, magic) == TpidrEl0TlsMagic);
|
||||||
|
|
||||||
|
fpsimd_context* GetFloatingPointState(mcontext_t& host_ctx) {
|
||||||
|
_aarch64_ctx* header = reinterpret_cast<_aarch64_ctx*>(&host_ctx.__reserved);
|
||||||
|
while (header->magic != FPSIMD_MAGIC) {
|
||||||
|
header = reinterpret_cast<_aarch64_ctx*>(reinterpret_cast<char*>(header) + header->size);
|
||||||
|
}
|
||||||
|
return reinterpret_cast<fpsimd_context*>(header);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void* ARM_NCE::RestoreGuestContext(void* raw_context) {
|
||||||
|
// Retrieve the host context.
|
||||||
|
auto& host_ctx = static_cast<ucontext_t*>(raw_context)->uc_mcontext;
|
||||||
|
|
||||||
|
// Thread-local parameters will be located in x9.
|
||||||
|
auto* tpidr = reinterpret_cast<NativeExecutionParameters*>(host_ctx.regs[9]);
|
||||||
|
auto* guest_ctx = static_cast<GuestContext*>(tpidr->native_context);
|
||||||
|
|
||||||
|
// Retrieve the host floating point state.
|
||||||
|
auto* fpctx = GetFloatingPointState(host_ctx);
|
||||||
|
|
||||||
|
// Save host callee-saved registers.
|
||||||
|
std::memcpy(guest_ctx->host_ctx.host_saved_vregs.data(), &fpctx->vregs[8],
|
||||||
|
sizeof(guest_ctx->host_ctx.host_saved_vregs));
|
||||||
|
std::memcpy(guest_ctx->host_ctx.host_saved_regs.data(), &host_ctx.regs[19],
|
||||||
|
sizeof(guest_ctx->host_ctx.host_saved_regs));
|
||||||
|
|
||||||
|
// Save stack pointer.
|
||||||
|
guest_ctx->host_ctx.host_sp = host_ctx.sp;
|
||||||
|
|
||||||
|
// Restore all guest state except tpidr_el0.
|
||||||
|
host_ctx.sp = guest_ctx->sp;
|
||||||
|
host_ctx.pc = guest_ctx->pc;
|
||||||
|
host_ctx.pstate = guest_ctx->pstate;
|
||||||
|
fpctx->fpcr = guest_ctx->fpcr;
|
||||||
|
fpctx->fpsr = guest_ctx->fpsr;
|
||||||
|
std::memcpy(host_ctx.regs, guest_ctx->cpu_registers.data(), sizeof(host_ctx.regs));
|
||||||
|
std::memcpy(fpctx->vregs, guest_ctx->vector_registers.data(), sizeof(fpctx->vregs));
|
||||||
|
|
||||||
|
// Return the new thread-local storage pointer.
|
||||||
|
return tpidr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ARM_NCE::SaveGuestContext(GuestContext* guest_ctx, void* raw_context) {
|
||||||
|
// Retrieve the host context.
|
||||||
|
auto& host_ctx = static_cast<ucontext_t*>(raw_context)->uc_mcontext;
|
||||||
|
|
||||||
|
// Retrieve the host floating point state.
|
||||||
|
auto* fpctx = GetFloatingPointState(host_ctx);
|
||||||
|
|
||||||
|
// Save all guest registers except tpidr_el0.
|
||||||
|
std::memcpy(guest_ctx->cpu_registers.data(), host_ctx.regs, sizeof(host_ctx.regs));
|
||||||
|
std::memcpy(guest_ctx->vector_registers.data(), fpctx->vregs, sizeof(fpctx->vregs));
|
||||||
|
guest_ctx->fpsr = fpctx->fpsr;
|
||||||
|
guest_ctx->fpcr = fpctx->fpcr;
|
||||||
|
guest_ctx->pstate = static_cast<u32>(host_ctx.pstate);
|
||||||
|
guest_ctx->pc = host_ctx.pc;
|
||||||
|
guest_ctx->sp = host_ctx.sp;
|
||||||
|
|
||||||
|
// Restore stack pointer.
|
||||||
|
host_ctx.sp = guest_ctx->host_ctx.host_sp;
|
||||||
|
|
||||||
|
// Restore host callee-saved registers.
|
||||||
|
std::memcpy(&host_ctx.regs[19], guest_ctx->host_ctx.host_saved_regs.data(),
|
||||||
|
sizeof(guest_ctx->host_ctx.host_saved_regs));
|
||||||
|
std::memcpy(&fpctx->vregs[8], guest_ctx->host_ctx.host_saved_vregs.data(),
|
||||||
|
sizeof(guest_ctx->host_ctx.host_saved_vregs));
|
||||||
|
|
||||||
|
// Return from the call on exit by setting pc to x30.
|
||||||
|
host_ctx.pc = guest_ctx->host_ctx.host_saved_regs[11];
|
||||||
|
|
||||||
|
// Clear esr_el1 and return it.
|
||||||
|
host_ctx.regs[0] = guest_ctx->esr_el1.exchange(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ARM_NCE::HandleGuestFault(GuestContext* guest_ctx, void* raw_info, void* raw_context) {
|
||||||
|
auto& host_ctx = static_cast<ucontext_t*>(raw_context)->uc_mcontext;
|
||||||
|
auto* info = static_cast<siginfo_t*>(raw_info);
|
||||||
|
|
||||||
|
// Try to handle an invalid access.
|
||||||
|
// TODO: handle accesses which split a page?
|
||||||
|
const Common::ProcessAddress addr =
|
||||||
|
(reinterpret_cast<u64>(info->si_addr) & ~Memory::YUZU_PAGEMASK);
|
||||||
|
if (guest_ctx->system->ApplicationMemory().InvalidateNCE(addr, Memory::YUZU_PAGESIZE)) {
|
||||||
|
// We handled the access successfully and are returning to guest code.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can't handle the access, so determine why we crashed.
|
||||||
|
const bool is_prefetch_abort = host_ctx.pc == reinterpret_cast<u64>(info->si_addr);
|
||||||
|
|
||||||
|
// For data aborts, skip the instruction and return to guest code.
|
||||||
|
// This will allow games to continue in many scenarios where they would otherwise crash.
|
||||||
|
if (!is_prefetch_abort) {
|
||||||
|
host_ctx.pc += 4;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a prefetch abort.
|
||||||
|
guest_ctx->esr_el1.fetch_or(static_cast<u64>(HaltReason::PrefetchAbort));
|
||||||
|
|
||||||
|
// Forcibly mark the context as locked. We are still running.
|
||||||
|
// We may race with SignalInterrupt here:
|
||||||
|
// - If we lose the race, then SignalInterrupt will send us a signal we are masking,
|
||||||
|
// and it will do nothing when it is unmasked, as we have already left guest code.
|
||||||
|
// - If we win the race, then SignalInterrupt will wait for us to unlock first.
|
||||||
|
auto& thread_params = guest_ctx->parent->running_thread->GetNativeExecutionParameters();
|
||||||
|
thread_params.lock.store(SpinLockLocked);
|
||||||
|
|
||||||
|
// Return to host.
|
||||||
|
SaveGuestContext(guest_ctx, raw_context);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ARM_NCE::HandleHostFault(int sig, void* raw_info, void* raw_context) {
|
||||||
|
return g_orig_action.sa_sigaction(sig, static_cast<siginfo_t*>(raw_info), raw_context);
|
||||||
|
}
|
||||||
|
|
||||||
|
HaltReason ARM_NCE::RunJit() {
|
||||||
|
// Get the thread parameters.
|
||||||
|
// TODO: pass the current thread down from ::Run
|
||||||
|
auto* thread = Kernel::GetCurrentThreadPointer(system.Kernel());
|
||||||
|
auto* thread_params = &thread->GetNativeExecutionParameters();
|
||||||
|
|
||||||
|
{
|
||||||
|
// Lock our core context.
|
||||||
|
std::scoped_lock lk{lock};
|
||||||
|
|
||||||
|
// We should not be running.
|
||||||
|
ASSERT(running_thread == nullptr);
|
||||||
|
|
||||||
|
// Check if we need to run. If we have already been halted, we are done.
|
||||||
|
u64 halt = guest_ctx.esr_el1.exchange(0);
|
||||||
|
if (halt != 0) {
|
||||||
|
return static_cast<HaltReason>(halt);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark that we are running.
|
||||||
|
running_thread = thread;
|
||||||
|
|
||||||
|
// Acquire the lock on the thread parameters.
|
||||||
|
// This allows us to force synchronization with SignalInterrupt.
|
||||||
|
LockThreadParameters(thread_params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign current members.
|
||||||
|
guest_ctx.parent = this;
|
||||||
|
thread_params->native_context = &guest_ctx;
|
||||||
|
thread_params->tpidr_el0 = guest_ctx.tpidr_el0;
|
||||||
|
thread_params->tpidrro_el0 = guest_ctx.tpidrro_el0;
|
||||||
|
thread_params->is_running = true;
|
||||||
|
|
||||||
|
HaltReason halt{};
|
||||||
|
|
||||||
|
// TODO: finding and creating the post handler needs to be locked
|
||||||
|
// to deal with dynamic loading of NROs.
|
||||||
|
const auto& post_handlers = system.ApplicationProcess()->GetPostHandlers();
|
||||||
|
if (auto it = post_handlers.find(guest_ctx.pc); it != post_handlers.end()) {
|
||||||
|
halt = ReturnToRunCodeByTrampoline(thread_params, &guest_ctx, it->second);
|
||||||
|
} else {
|
||||||
|
halt = ReturnToRunCodeByExceptionLevelChange(thread_id, thread_params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unload members.
|
||||||
|
// The thread does not change, so we can persist the old reference.
|
||||||
|
guest_ctx.tpidr_el0 = thread_params->tpidr_el0;
|
||||||
|
thread_params->native_context = nullptr;
|
||||||
|
thread_params->is_running = false;
|
||||||
|
|
||||||
|
// Unlock the thread parameters.
|
||||||
|
UnlockThreadParameters(thread_params);
|
||||||
|
|
||||||
|
{
|
||||||
|
// Lock the core context.
|
||||||
|
std::scoped_lock lk{lock};
|
||||||
|
|
||||||
|
// On exit, we no longer have an active thread.
|
||||||
|
running_thread = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the halt reason.
|
||||||
|
return halt;
|
||||||
|
}
|
||||||
|
|
||||||
|
HaltReason ARM_NCE::StepJit() {
|
||||||
|
return HaltReason::StepThread;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 ARM_NCE::GetSvcNumber() const {
|
||||||
|
return guest_ctx.svc_swi;
|
||||||
|
}
|
||||||
|
|
||||||
|
ARM_NCE::ARM_NCE(System& system_, bool uses_wall_clock_, std::size_t core_index_)
|
||||||
|
: ARM_Interface{system_, uses_wall_clock_}, core_index{core_index_} {
|
||||||
|
guest_ctx.system = &system_;
|
||||||
|
}
|
||||||
|
|
||||||
|
ARM_NCE::~ARM_NCE() = default;
|
||||||
|
|
||||||
|
void ARM_NCE::Initialize() {
|
||||||
|
thread_id = gettid();
|
||||||
|
|
||||||
|
// Setup our signals
|
||||||
|
static std::once_flag flag;
|
||||||
|
std::call_once(flag, [] {
|
||||||
|
using HandlerType = decltype(sigaction::sa_sigaction);
|
||||||
|
|
||||||
|
sigset_t signal_mask;
|
||||||
|
sigemptyset(&signal_mask);
|
||||||
|
sigaddset(&signal_mask, ReturnToRunCodeByExceptionLevelChangeSignal);
|
||||||
|
sigaddset(&signal_mask, BreakFromRunCodeSignal);
|
||||||
|
sigaddset(&signal_mask, GuestFaultSignal);
|
||||||
|
|
||||||
|
struct sigaction return_to_run_code_action {};
|
||||||
|
return_to_run_code_action.sa_flags = SA_SIGINFO | SA_ONSTACK;
|
||||||
|
return_to_run_code_action.sa_sigaction = reinterpret_cast<HandlerType>(
|
||||||
|
&ARM_NCE::ReturnToRunCodeByExceptionLevelChangeSignalHandler);
|
||||||
|
return_to_run_code_action.sa_mask = signal_mask;
|
||||||
|
Common::SigAction(ReturnToRunCodeByExceptionLevelChangeSignal, &return_to_run_code_action,
|
||||||
|
nullptr);
|
||||||
|
|
||||||
|
struct sigaction break_from_run_code_action {};
|
||||||
|
break_from_run_code_action.sa_flags = SA_SIGINFO | SA_ONSTACK;
|
||||||
|
break_from_run_code_action.sa_sigaction =
|
||||||
|
reinterpret_cast<HandlerType>(&ARM_NCE::BreakFromRunCodeSignalHandler);
|
||||||
|
break_from_run_code_action.sa_mask = signal_mask;
|
||||||
|
Common::SigAction(BreakFromRunCodeSignal, &break_from_run_code_action, nullptr);
|
||||||
|
|
||||||
|
struct sigaction fault_action {};
|
||||||
|
fault_action.sa_flags = SA_SIGINFO | SA_ONSTACK | SA_RESTART;
|
||||||
|
fault_action.sa_sigaction =
|
||||||
|
reinterpret_cast<HandlerType>(&ARM_NCE::GuestFaultSignalHandler);
|
||||||
|
fault_action.sa_mask = signal_mask;
|
||||||
|
Common::SigAction(GuestFaultSignal, &fault_action, &g_orig_action);
|
||||||
|
|
||||||
|
// Simplify call for g_orig_action.
|
||||||
|
// These fields occupy the same space in memory, so this should be a no-op in practice.
|
||||||
|
if (!(g_orig_action.sa_flags & SA_SIGINFO)) {
|
||||||
|
g_orig_action.sa_sigaction =
|
||||||
|
reinterpret_cast<decltype(g_orig_action.sa_sigaction)>(g_orig_action.sa_handler);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void ARM_NCE::SetPC(u64 pc) {
|
||||||
|
guest_ctx.pc = pc;
|
||||||
|
}
|
||||||
|
|
||||||
|
u64 ARM_NCE::GetPC() const {
|
||||||
|
return guest_ctx.pc;
|
||||||
|
}
|
||||||
|
|
||||||
|
u64 ARM_NCE::GetSP() const {
|
||||||
|
return guest_ctx.sp;
|
||||||
|
}
|
||||||
|
|
||||||
|
u64 ARM_NCE::GetReg(int index) const {
|
||||||
|
return guest_ctx.cpu_registers[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
void ARM_NCE::SetReg(int index, u64 value) {
|
||||||
|
guest_ctx.cpu_registers[index] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
u128 ARM_NCE::GetVectorReg(int index) const {
|
||||||
|
return guest_ctx.vector_registers[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
void ARM_NCE::SetVectorReg(int index, u128 value) {
|
||||||
|
guest_ctx.vector_registers[index] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 ARM_NCE::GetPSTATE() const {
|
||||||
|
return guest_ctx.pstate;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ARM_NCE::SetPSTATE(u32 pstate) {
|
||||||
|
guest_ctx.pstate = pstate;
|
||||||
|
}
|
||||||
|
|
||||||
|
u64 ARM_NCE::GetTlsAddress() const {
|
||||||
|
return guest_ctx.tpidrro_el0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ARM_NCE::SetTlsAddress(u64 address) {
|
||||||
|
guest_ctx.tpidrro_el0 = address;
|
||||||
|
}
|
||||||
|
|
||||||
|
u64 ARM_NCE::GetTPIDR_EL0() const {
|
||||||
|
return guest_ctx.tpidr_el0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ARM_NCE::SetTPIDR_EL0(u64 value) {
|
||||||
|
guest_ctx.tpidr_el0 = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ARM_NCE::SaveContext(ThreadContext64& ctx) const {
|
||||||
|
ctx.cpu_registers = guest_ctx.cpu_registers;
|
||||||
|
ctx.sp = guest_ctx.sp;
|
||||||
|
ctx.pc = guest_ctx.pc;
|
||||||
|
ctx.pstate = guest_ctx.pstate;
|
||||||
|
ctx.vector_registers = guest_ctx.vector_registers;
|
||||||
|
ctx.fpcr = guest_ctx.fpcr;
|
||||||
|
ctx.fpsr = guest_ctx.fpsr;
|
||||||
|
ctx.tpidr = guest_ctx.tpidr_el0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ARM_NCE::LoadContext(const ThreadContext64& ctx) {
|
||||||
|
guest_ctx.cpu_registers = ctx.cpu_registers;
|
||||||
|
guest_ctx.sp = ctx.sp;
|
||||||
|
guest_ctx.pc = ctx.pc;
|
||||||
|
guest_ctx.pstate = ctx.pstate;
|
||||||
|
guest_ctx.vector_registers = ctx.vector_registers;
|
||||||
|
guest_ctx.fpcr = ctx.fpcr;
|
||||||
|
guest_ctx.fpsr = ctx.fpsr;
|
||||||
|
guest_ctx.tpidr_el0 = ctx.tpidr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ARM_NCE::SignalInterrupt() {
|
||||||
|
// Lock core context.
|
||||||
|
std::scoped_lock lk{lock};
|
||||||
|
|
||||||
|
// Add break loop condition.
|
||||||
|
guest_ctx.esr_el1.fetch_or(static_cast<u64>(HaltReason::BreakLoop));
|
||||||
|
|
||||||
|
// If there is no thread running, we are done.
|
||||||
|
if (running_thread == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lock the thread context.
|
||||||
|
auto* params = &running_thread->GetNativeExecutionParameters();
|
||||||
|
LockThreadParameters(params);
|
||||||
|
|
||||||
|
if (params->is_running) {
|
||||||
|
// We should signal to the running thread.
|
||||||
|
// The running thread will unlock the thread context.
|
||||||
|
syscall(SYS_tkill, thread_id, BreakFromRunCodeSignal);
|
||||||
|
} else {
|
||||||
|
// If the thread is no longer running, we have nothing to do.
|
||||||
|
UnlockThreadParameters(params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ARM_NCE::ClearInterrupt() {
|
||||||
|
guest_ctx.esr_el1 = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void ARM_NCE::ClearInstructionCache() {
|
||||||
|
// TODO: This is not possible to implement correctly on Linux because
|
||||||
|
// we do not have any access to ic iallu.
|
||||||
|
|
||||||
|
// Require accesses to complete.
|
||||||
|
std::atomic_thread_fence(std::memory_order_seq_cst);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ARM_NCE::InvalidateCacheRange(u64 addr, std::size_t size) {
|
||||||
|
this->ClearInstructionCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ARM_NCE::ClearExclusiveState() {
|
||||||
|
// No-op.
|
||||||
|
}
|
||||||
|
|
||||||
|
void ARM_NCE::PageTableChanged(Common::PageTable& page_table,
|
||||||
|
std::size_t new_address_space_size_in_bits) {
|
||||||
|
// No-op. Page table is never used.
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Core
|
108
src/core/arm/nce/arm_nce.h
Executable file
108
src/core/arm/nce/arm_nce.h
Executable file
|
@ -0,0 +1,108 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <memory>
|
||||||
|
#include <span>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/arm/arm_interface.h"
|
||||||
|
#include "core/arm/nce/guest_context.h"
|
||||||
|
|
||||||
|
namespace Core::Memory {
|
||||||
|
class Memory;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Core {
|
||||||
|
|
||||||
|
class System;
|
||||||
|
|
||||||
|
class ARM_NCE final : public ARM_Interface {
|
||||||
|
public:
|
||||||
|
ARM_NCE(System& system_, bool uses_wall_clock_, std::size_t core_index_);
|
||||||
|
|
||||||
|
~ARM_NCE() override;
|
||||||
|
|
||||||
|
void Initialize() override;
|
||||||
|
void SetPC(u64 pc) override;
|
||||||
|
u64 GetPC() const override;
|
||||||
|
u64 GetSP() const override;
|
||||||
|
u64 GetReg(int index) const override;
|
||||||
|
void SetReg(int index, u64 value) override;
|
||||||
|
u128 GetVectorReg(int index) const override;
|
||||||
|
void SetVectorReg(int index, u128 value) override;
|
||||||
|
|
||||||
|
u32 GetPSTATE() const override;
|
||||||
|
void SetPSTATE(u32 pstate) override;
|
||||||
|
u64 GetTlsAddress() const override;
|
||||||
|
void SetTlsAddress(u64 address) override;
|
||||||
|
void SetTPIDR_EL0(u64 value) override;
|
||||||
|
u64 GetTPIDR_EL0() const override;
|
||||||
|
|
||||||
|
Architecture GetArchitecture() const override {
|
||||||
|
return Architecture::Aarch64;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SaveContext(ThreadContext32& ctx) const override {}
|
||||||
|
void SaveContext(ThreadContext64& ctx) const override;
|
||||||
|
void LoadContext(const ThreadContext32& ctx) override {}
|
||||||
|
void LoadContext(const ThreadContext64& ctx) override;
|
||||||
|
|
||||||
|
void SignalInterrupt() override;
|
||||||
|
void ClearInterrupt() override;
|
||||||
|
void ClearExclusiveState() override;
|
||||||
|
void ClearInstructionCache() override;
|
||||||
|
void InvalidateCacheRange(u64 addr, std::size_t size) override;
|
||||||
|
void PageTableChanged(Common::PageTable& new_page_table,
|
||||||
|
std::size_t new_address_space_size_in_bits) override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
HaltReason RunJit() override;
|
||||||
|
HaltReason StepJit() override;
|
||||||
|
|
||||||
|
u32 GetSvcNumber() const override;
|
||||||
|
|
||||||
|
const Kernel::DebugWatchpoint* HaltedWatchpoint() const override {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RewindBreakpointInstruction() override {}
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Assembly definitions.
|
||||||
|
static HaltReason ReturnToRunCodeByTrampoline(void* tpidr, GuestContext* ctx,
|
||||||
|
u64 trampoline_addr);
|
||||||
|
static HaltReason ReturnToRunCodeByExceptionLevelChange(int tid, void* tpidr);
|
||||||
|
|
||||||
|
static void ReturnToRunCodeByExceptionLevelChangeSignalHandler(int sig, void* info,
|
||||||
|
void* raw_context);
|
||||||
|
static void BreakFromRunCodeSignalHandler(int sig, void* info, void* raw_context);
|
||||||
|
static void GuestFaultSignalHandler(int sig, void* info, void* raw_context);
|
||||||
|
|
||||||
|
static void LockThreadParameters(void* tpidr);
|
||||||
|
static void UnlockThreadParameters(void* tpidr);
|
||||||
|
|
||||||
|
private:
|
||||||
|
// C++ implementation functions for assembly definitions.
|
||||||
|
static void* RestoreGuestContext(void* raw_context);
|
||||||
|
static void SaveGuestContext(GuestContext* ctx, void* raw_context);
|
||||||
|
static bool HandleGuestFault(GuestContext* ctx, void* info, void* raw_context);
|
||||||
|
static void HandleHostFault(int sig, void* info, void* raw_context);
|
||||||
|
|
||||||
|
public:
|
||||||
|
// Members set on initialization.
|
||||||
|
std::size_t core_index{};
|
||||||
|
pid_t thread_id{-1};
|
||||||
|
|
||||||
|
// Core context.
|
||||||
|
GuestContext guest_ctx;
|
||||||
|
|
||||||
|
// Thread and invalidation info.
|
||||||
|
std::mutex lock;
|
||||||
|
Kernel::KThread* running_thread{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Core
|
222
src/core/arm/nce/arm_nce.s
Executable file
222
src/core/arm/nce/arm_nce.s
Executable file
|
@ -0,0 +1,222 @@
|
||||||
|
/* SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project */
|
||||||
|
/* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||||
|
|
||||||
|
#include "core/arm/nce/arm_nce_asm_definitions.h"
|
||||||
|
|
||||||
|
#define LOAD_IMMEDIATE_32(reg, val) \
|
||||||
|
mov reg, #(((val) >> 0x00) & 0xFFFF); \
|
||||||
|
movk reg, #(((val) >> 0x10) & 0xFFFF), lsl #16
|
||||||
|
|
||||||
|
|
||||||
|
/* static HaltReason Core::ARM_NCE::ReturnToRunCodeByTrampoline(void* tpidr, Core::GuestContext* ctx, u64 trampoline_addr) */
|
||||||
|
.section .text._ZN4Core7ARM_NCE27ReturnToRunCodeByTrampolineEPvPNS_12GuestContextEm, "ax", %progbits
|
||||||
|
.global _ZN4Core7ARM_NCE27ReturnToRunCodeByTrampolineEPvPNS_12GuestContextEm
|
||||||
|
.type _ZN4Core7ARM_NCE27ReturnToRunCodeByTrampolineEPvPNS_12GuestContextEm, %function
|
||||||
|
_ZN4Core7ARM_NCE27ReturnToRunCodeByTrampolineEPvPNS_12GuestContextEm:
|
||||||
|
/* Back up host sp to x3. */
|
||||||
|
/* Back up host tpidr_el0 to x4. */
|
||||||
|
mov x3, sp
|
||||||
|
mrs x4, tpidr_el0
|
||||||
|
|
||||||
|
/* Load guest sp. x5 is used as a scratch register. */
|
||||||
|
ldr x5, [x1, #(GuestContextSp)]
|
||||||
|
mov sp, x5
|
||||||
|
|
||||||
|
/* Offset GuestContext pointer to the host member. */
|
||||||
|
add x5, x1, #(GuestContextHostContext)
|
||||||
|
|
||||||
|
/* Save original host sp and tpidr_el0 (x3, x4) to host context. */
|
||||||
|
stp x3, x4, [x5, #(HostContextSpTpidrEl0)]
|
||||||
|
|
||||||
|
/* Save all callee-saved host GPRs. */
|
||||||
|
stp x19, x20, [x5, #(HostContextRegs+0x0)]
|
||||||
|
stp x21, x22, [x5, #(HostContextRegs+0x10)]
|
||||||
|
stp x23, x24, [x5, #(HostContextRegs+0x20)]
|
||||||
|
stp x25, x26, [x5, #(HostContextRegs+0x30)]
|
||||||
|
stp x27, x28, [x5, #(HostContextRegs+0x40)]
|
||||||
|
stp x29, x30, [x5, #(HostContextRegs+0x50)]
|
||||||
|
|
||||||
|
/* Save all callee-saved host FPRs. */
|
||||||
|
stp q8, q9, [x5, #(HostContextVregs+0x0)]
|
||||||
|
stp q10, q11, [x5, #(HostContextVregs+0x20)]
|
||||||
|
stp q12, q13, [x5, #(HostContextVregs+0x40)]
|
||||||
|
stp q14, q15, [x5, #(HostContextVregs+0x60)]
|
||||||
|
|
||||||
|
/* Load guest tpidr_el0 from argument. */
|
||||||
|
msr tpidr_el0, x0
|
||||||
|
|
||||||
|
/* Tail call the trampoline to restore guest state. */
|
||||||
|
br x2
|
||||||
|
|
||||||
|
|
||||||
|
/* static HaltReason Core::ARM_NCE::ReturnToRunCodeByExceptionLevelChange(int tid, void* tpidr) */
|
||||||
|
.section .text._ZN4Core7ARM_NCE37ReturnToRunCodeByExceptionLevelChangeEiPv, "ax", %progbits
|
||||||
|
.global _ZN4Core7ARM_NCE37ReturnToRunCodeByExceptionLevelChangeEiPv
|
||||||
|
.type _ZN4Core7ARM_NCE37ReturnToRunCodeByExceptionLevelChangeEiPv, %function
|
||||||
|
_ZN4Core7ARM_NCE37ReturnToRunCodeByExceptionLevelChangeEiPv:
|
||||||
|
/* This jumps to the signal handler, which will restore the entire context. */
|
||||||
|
/* On entry, x0 = thread id, which is already in the right place. */
|
||||||
|
|
||||||
|
/* Move tpidr to x9 so it is not trampled. */
|
||||||
|
mov x9, x1
|
||||||
|
|
||||||
|
/* Set up arguments. */
|
||||||
|
mov x8, #(__NR_tkill)
|
||||||
|
mov x1, #(ReturnToRunCodeByExceptionLevelChangeSignal)
|
||||||
|
|
||||||
|
/* Tail call the signal handler. */
|
||||||
|
svc #0
|
||||||
|
|
||||||
|
/* Block execution from flowing here. */
|
||||||
|
brk #1000
|
||||||
|
|
||||||
|
|
||||||
|
/* static void Core::ARM_NCE::ReturnToRunCodeByExceptionLevelChangeSignalHandler(int sig, void* info, void* raw_context) */
|
||||||
|
.section .text._ZN4Core7ARM_NCE50ReturnToRunCodeByExceptionLevelChangeSignalHandlerEiPvS1_, "ax", %progbits
|
||||||
|
.global _ZN4Core7ARM_NCE50ReturnToRunCodeByExceptionLevelChangeSignalHandlerEiPvS1_
|
||||||
|
.type _ZN4Core7ARM_NCE50ReturnToRunCodeByExceptionLevelChangeSignalHandlerEiPvS1_, %function
|
||||||
|
_ZN4Core7ARM_NCE50ReturnToRunCodeByExceptionLevelChangeSignalHandlerEiPvS1_:
|
||||||
|
stp x29, x30, [sp, #-0x10]!
|
||||||
|
mov x29, sp
|
||||||
|
|
||||||
|
/* Call the context restorer with the raw context. */
|
||||||
|
mov x0, x2
|
||||||
|
bl _ZN4Core7ARM_NCE19RestoreGuestContextEPv
|
||||||
|
|
||||||
|
/* Save the old value of tpidr_el0. */
|
||||||
|
mrs x8, tpidr_el0
|
||||||
|
ldr x9, [x0, #(TpidrEl0NativeContext)]
|
||||||
|
str x8, [x9, #(GuestContextHostContext + HostContextTpidrEl0)]
|
||||||
|
|
||||||
|
/* Set our new tpidr_el0. */
|
||||||
|
msr tpidr_el0, x0
|
||||||
|
|
||||||
|
/* Unlock the context. */
|
||||||
|
bl _ZN4Core7ARM_NCE22UnlockThreadParametersEPv
|
||||||
|
|
||||||
|
/* Returning from here will enter the guest. */
|
||||||
|
ldp x29, x30, [sp], #0x10
|
||||||
|
ret
|
||||||
|
|
||||||
|
|
||||||
|
/* static void Core::ARM_NCE::BreakFromRunCodeSignalHandler(int sig, void* info, void* raw_context) */
|
||||||
|
.section .text._ZN4Core7ARM_NCE29BreakFromRunCodeSignalHandlerEiPvS1_, "ax", %progbits
|
||||||
|
.global _ZN4Core7ARM_NCE29BreakFromRunCodeSignalHandlerEiPvS1_
|
||||||
|
.type _ZN4Core7ARM_NCE29BreakFromRunCodeSignalHandlerEiPvS1_, %function
|
||||||
|
_ZN4Core7ARM_NCE29BreakFromRunCodeSignalHandlerEiPvS1_:
|
||||||
|
/* Check to see if we have the correct TLS magic. */
|
||||||
|
mrs x8, tpidr_el0
|
||||||
|
ldr w9, [x8, #(TpidrEl0TlsMagic)]
|
||||||
|
|
||||||
|
LOAD_IMMEDIATE_32(w10, TlsMagic)
|
||||||
|
|
||||||
|
cmp w9, w10
|
||||||
|
b.ne 1f
|
||||||
|
|
||||||
|
/* Correct TLS magic, so this is a guest interrupt. */
|
||||||
|
/* Restore host tpidr_el0. */
|
||||||
|
ldr x0, [x8, #(TpidrEl0NativeContext)]
|
||||||
|
ldr x3, [x0, #(GuestContextHostContext + HostContextTpidrEl0)]
|
||||||
|
msr tpidr_el0, x3
|
||||||
|
|
||||||
|
/* Tail call the restorer. */
|
||||||
|
mov x1, x2
|
||||||
|
b _ZN4Core7ARM_NCE16SaveGuestContextEPNS_12GuestContextEPv
|
||||||
|
|
||||||
|
/* Returning from here will enter host code. */
|
||||||
|
|
||||||
|
1:
|
||||||
|
/* Incorrect TLS magic, so this is a spurious signal. */
|
||||||
|
ret
|
||||||
|
|
||||||
|
|
||||||
|
/* static void Core::ARM_NCE::GuestFaultSignalHandler(int sig, void* info, void* raw_context) */
|
||||||
|
.section .text._ZN4Core7ARM_NCE23GuestFaultSignalHandlerEiPvS1_, "ax", %progbits
|
||||||
|
.global _ZN4Core7ARM_NCE23GuestFaultSignalHandlerEiPvS1_
|
||||||
|
.type _ZN4Core7ARM_NCE23GuestFaultSignalHandlerEiPvS1_, %function
|
||||||
|
_ZN4Core7ARM_NCE23GuestFaultSignalHandlerEiPvS1_:
|
||||||
|
/* Check to see if we have the correct TLS magic. */
|
||||||
|
mrs x8, tpidr_el0
|
||||||
|
ldr w9, [x8, #(TpidrEl0TlsMagic)]
|
||||||
|
|
||||||
|
LOAD_IMMEDIATE_32(w10, TlsMagic)
|
||||||
|
|
||||||
|
cmp w9, w10
|
||||||
|
b.eq 1f
|
||||||
|
|
||||||
|
/* Incorrect TLS magic, so this is a host fault. */
|
||||||
|
/* Tail call the handler. */
|
||||||
|
b _ZN4Core7ARM_NCE15HandleHostFaultEiPvS1_
|
||||||
|
|
||||||
|
1:
|
||||||
|
/* Correct TLS magic, so this is a guest fault. */
|
||||||
|
stp x29, x30, [sp, #-0x20]!
|
||||||
|
str x19, [sp, #0x10]
|
||||||
|
mov x29, sp
|
||||||
|
|
||||||
|
/* Save the old tpidr_el0. */
|
||||||
|
mov x19, x8
|
||||||
|
|
||||||
|
/* Restore host tpidr_el0. */
|
||||||
|
ldr x0, [x8, #(TpidrEl0NativeContext)]
|
||||||
|
ldr x3, [x0, #(GuestContextHostContext + HostContextTpidrEl0)]
|
||||||
|
msr tpidr_el0, x3
|
||||||
|
|
||||||
|
/* Call the handler. */
|
||||||
|
bl _ZN4Core7ARM_NCE16HandleGuestFaultEPNS_12GuestContextEPvS3_
|
||||||
|
|
||||||
|
/* If the handler returned false, we want to preserve the host tpidr_el0. */
|
||||||
|
cbz x0, 2f
|
||||||
|
|
||||||
|
/* Otherwise, restore guest tpidr_el0. */
|
||||||
|
msr tpidr_el0, x19
|
||||||
|
|
||||||
|
2:
|
||||||
|
ldr x19, [sp, #0x10]
|
||||||
|
ldp x29, x30, [sp], #0x20
|
||||||
|
ret
|
||||||
|
|
||||||
|
|
||||||
|
/* static void Core::ARM_NCE::LockThreadParameters(void* tpidr) */
|
||||||
|
.section .text._ZN4Core7ARM_NCE20LockThreadParametersEPv, "ax", %progbits
|
||||||
|
.global _ZN4Core7ARM_NCE20LockThreadParametersEPv
|
||||||
|
.type _ZN4Core7ARM_NCE20LockThreadParametersEPv, %function
|
||||||
|
_ZN4Core7ARM_NCE20LockThreadParametersEPv:
|
||||||
|
/* Offset to lock member. */
|
||||||
|
add x0, x0, #(TpidrEl0Lock)
|
||||||
|
|
||||||
|
1:
|
||||||
|
/* Clear the monitor. */
|
||||||
|
clrex
|
||||||
|
|
||||||
|
2:
|
||||||
|
/* Load-linked with acquire ordering. */
|
||||||
|
ldaxr w1, [x0]
|
||||||
|
|
||||||
|
/* If the value was SpinLockLocked, clear monitor and retry. */
|
||||||
|
cbz w1, 1b
|
||||||
|
|
||||||
|
/* Store-conditional SpinLockLocked with relaxed ordering. */
|
||||||
|
stxr w1, wzr, [x0]
|
||||||
|
|
||||||
|
/* If we failed to store, retry. */
|
||||||
|
cbnz w1, 2b
|
||||||
|
|
||||||
|
ret
|
||||||
|
|
||||||
|
|
||||||
|
/* static void Core::ARM_NCE::UnlockThreadParameters(void* tpidr) */
|
||||||
|
.section .text._ZN4Core7ARM_NCE22UnlockThreadParametersEPv, "ax", %progbits
|
||||||
|
.global _ZN4Core7ARM_NCE22UnlockThreadParametersEPv
|
||||||
|
.type _ZN4Core7ARM_NCE22UnlockThreadParametersEPv, %function
|
||||||
|
_ZN4Core7ARM_NCE22UnlockThreadParametersEPv:
|
||||||
|
/* Offset to lock member. */
|
||||||
|
add x0, x0, #(TpidrEl0Lock)
|
||||||
|
|
||||||
|
/* Load SpinLockUnlocked. */
|
||||||
|
mov w1, #(SpinLockUnlocked)
|
||||||
|
|
||||||
|
/* Store value with release ordering. */
|
||||||
|
stlr w1, [x0]
|
||||||
|
|
||||||
|
ret
|
29
src/core/arm/nce/arm_nce_asm_definitions.h
Executable file
29
src/core/arm/nce/arm_nce_asm_definitions.h
Executable file
|
@ -0,0 +1,29 @@
|
||||||
|
/* SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project */
|
||||||
|
/* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#define __ASSEMBLY__
|
||||||
|
|
||||||
|
#include <asm-generic/signal.h>
|
||||||
|
#include <asm-generic/unistd.h>
|
||||||
|
|
||||||
|
#define ReturnToRunCodeByExceptionLevelChangeSignal SIGUSR2
|
||||||
|
#define BreakFromRunCodeSignal SIGURG
|
||||||
|
#define GuestFaultSignal SIGSEGV
|
||||||
|
|
||||||
|
#define GuestContextSp 0xF8
|
||||||
|
#define GuestContextHostContext 0x320
|
||||||
|
|
||||||
|
#define HostContextSpTpidrEl0 0xE0
|
||||||
|
#define HostContextTpidrEl0 0xE8
|
||||||
|
#define HostContextRegs 0x0
|
||||||
|
#define HostContextVregs 0x60
|
||||||
|
|
||||||
|
#define TpidrEl0NativeContext 0x10
|
||||||
|
#define TpidrEl0Lock 0x18
|
||||||
|
#define TpidrEl0TlsMagic 0x20
|
||||||
|
#define TlsMagic 0x555a5559
|
||||||
|
|
||||||
|
#define SpinLockLocked 0
|
||||||
|
#define SpinLockUnlocked 1
|
50
src/core/arm/nce/guest_context.h
Executable file
50
src/core/arm/nce/guest_context.h
Executable file
|
@ -0,0 +1,50 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "common/common_funcs.h"
|
||||||
|
#include "common/common_types.h"
|
||||||
|
#include "core/arm/arm_interface.h"
|
||||||
|
#include "core/arm/nce/arm_nce_asm_definitions.h"
|
||||||
|
|
||||||
|
namespace Core {
|
||||||
|
|
||||||
|
class ARM_NCE;
|
||||||
|
class System;
|
||||||
|
|
||||||
|
struct HostContext {
|
||||||
|
alignas(16) std::array<u64, 12> host_saved_regs{};
|
||||||
|
alignas(16) std::array<u128, 8> host_saved_vregs{};
|
||||||
|
u64 host_sp{};
|
||||||
|
void* host_tpidr_el0{};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct GuestContext {
|
||||||
|
std::array<u64, 31> cpu_registers{};
|
||||||
|
u64 sp{};
|
||||||
|
u64 pc{};
|
||||||
|
u32 fpcr{};
|
||||||
|
u32 fpsr{};
|
||||||
|
std::array<u128, 32> vector_registers{};
|
||||||
|
u32 pstate{};
|
||||||
|
alignas(16) HostContext host_ctx{};
|
||||||
|
u64 tpidrro_el0{};
|
||||||
|
u64 tpidr_el0{};
|
||||||
|
std::atomic<u64> esr_el1{};
|
||||||
|
u32 nzcv{};
|
||||||
|
u32 svc_swi{};
|
||||||
|
System* system{};
|
||||||
|
ARM_NCE* parent{};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Verify assembly offsets.
|
||||||
|
static_assert(offsetof(GuestContext, sp) == GuestContextSp);
|
||||||
|
static_assert(offsetof(GuestContext, host_ctx) == GuestContextHostContext);
|
||||||
|
static_assert(offsetof(HostContext, host_sp) == HostContextSpTpidrEl0);
|
||||||
|
static_assert(offsetof(HostContext, host_tpidr_el0) - 8 == HostContextSpTpidrEl0);
|
||||||
|
static_assert(offsetof(HostContext, host_tpidr_el0) == HostContextTpidrEl0);
|
||||||
|
static_assert(offsetof(HostContext, host_saved_regs) == HostContextRegs);
|
||||||
|
static_assert(offsetof(HostContext, host_saved_vregs) == HostContextVregs);
|
||||||
|
|
||||||
|
} // namespace Core
|
147
src/core/arm/nce/instructions.h
Executable file
147
src/core/arm/nce/instructions.h
Executable file
|
@ -0,0 +1,147 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright © 2020 Skyline Team and Contributors
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
#include "common/bit_field.h"
|
||||||
|
#include "common/common_types.h"
|
||||||
|
|
||||||
|
namespace Core::NCE {
|
||||||
|
|
||||||
|
enum SystemRegister : u32 {
|
||||||
|
TpidrEl0 = 0x5E82,
|
||||||
|
TpidrroEl0 = 0x5E83,
|
||||||
|
CntfrqEl0 = 0x5F00,
|
||||||
|
CntpctEl0 = 0x5F01,
|
||||||
|
};
|
||||||
|
|
||||||
|
// https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/SVC--Supervisor-Call-
|
||||||
|
union SVC {
|
||||||
|
constexpr explicit SVC(u32 raw_) : raw{raw_} {}
|
||||||
|
|
||||||
|
constexpr bool Verify() {
|
||||||
|
return (this->GetSig0() == 0x1 && this->GetSig1() == 0x6A0);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr u32 GetSig0() {
|
||||||
|
return decltype(sig0)::ExtractValue(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr u32 GetValue() {
|
||||||
|
return decltype(value)::ExtractValue(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr u32 GetSig1() {
|
||||||
|
return decltype(sig1)::ExtractValue(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 raw;
|
||||||
|
|
||||||
|
private:
|
||||||
|
BitField<0, 5, u32> sig0; // 0x1
|
||||||
|
BitField<5, 16, u32> value; // 16-bit immediate
|
||||||
|
BitField<21, 11, u32> sig1; // 0x6A0
|
||||||
|
};
|
||||||
|
static_assert(sizeof(SVC) == sizeof(u32));
|
||||||
|
static_assert(SVC(0xD40000C1).Verify());
|
||||||
|
static_assert(SVC(0xD40000C1).GetValue() == 0x6);
|
||||||
|
|
||||||
|
// https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/MRS--Move-System-Register-
|
||||||
|
union MRS {
|
||||||
|
constexpr explicit MRS(u32 raw_) : raw{raw_} {}
|
||||||
|
|
||||||
|
constexpr bool Verify() {
|
||||||
|
return (this->GetSig() == 0xD53);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr u32 GetRt() {
|
||||||
|
return decltype(rt)::ExtractValue(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr u32 GetSystemReg() {
|
||||||
|
return decltype(system_reg)::ExtractValue(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr u32 GetSig() {
|
||||||
|
return decltype(sig)::ExtractValue(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 raw;
|
||||||
|
|
||||||
|
private:
|
||||||
|
BitField<0, 5, u32> rt; // destination register
|
||||||
|
BitField<5, 15, u32> system_reg; // source system register
|
||||||
|
BitField<20, 12, u32> sig; // 0xD53
|
||||||
|
};
|
||||||
|
static_assert(sizeof(MRS) == sizeof(u32));
|
||||||
|
static_assert(MRS(0xD53BE020).Verify());
|
||||||
|
static_assert(MRS(0xD53BE020).GetSystemReg() == CntpctEl0);
|
||||||
|
static_assert(MRS(0xD53BE020).GetRt() == 0x0);
|
||||||
|
|
||||||
|
// https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/MSR--register---Move-general-purpose-register-to-System-Register-
|
||||||
|
union MSR {
|
||||||
|
constexpr explicit MSR(u32 raw_) : raw{raw_} {}
|
||||||
|
|
||||||
|
constexpr bool Verify() {
|
||||||
|
return this->GetSig() == 0xD51;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr u32 GetRt() {
|
||||||
|
return decltype(rt)::ExtractValue(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr u32 GetSystemReg() {
|
||||||
|
return decltype(system_reg)::ExtractValue(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr u32 GetSig() {
|
||||||
|
return decltype(sig)::ExtractValue(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 raw;
|
||||||
|
|
||||||
|
private:
|
||||||
|
BitField<0, 5, u32> rt; // source register
|
||||||
|
BitField<5, 15, u32> system_reg; // destination system register
|
||||||
|
BitField<20, 12, u32> sig; // 0xD51
|
||||||
|
};
|
||||||
|
static_assert(sizeof(MSR) == sizeof(u32));
|
||||||
|
static_assert(MSR(0xD51BD040).Verify());
|
||||||
|
static_assert(MSR(0xD51BD040).GetSystemReg() == TpidrEl0);
|
||||||
|
static_assert(MSR(0xD51BD040).GetRt() == 0x0);
|
||||||
|
|
||||||
|
// https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/LDXR--Load-Exclusive-Register-
|
||||||
|
// https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/LDXP--Load-Exclusive-Pair-of-Registers-
|
||||||
|
// https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/STXR--Store-Exclusive-Register-
|
||||||
|
// https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/STXP--Store-Exclusive-Pair-of-registers-
|
||||||
|
union Exclusive {
|
||||||
|
constexpr explicit Exclusive(u32 raw_) : raw{raw_} {}
|
||||||
|
|
||||||
|
constexpr bool Verify() {
|
||||||
|
return this->GetSig() == 0x10;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr u32 GetSig() {
|
||||||
|
return decltype(sig)::ExtractValue(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr u32 AsOrdered() {
|
||||||
|
return raw | decltype(o0)::FormatValue(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 raw;
|
||||||
|
|
||||||
|
private:
|
||||||
|
BitField<0, 5, u32> rt; // memory operand
|
||||||
|
BitField<5, 5, u32> rn; // register operand 1
|
||||||
|
BitField<10, 5, u32> rt2; // register operand 2
|
||||||
|
BitField<15, 1, u32> o0; // ordered
|
||||||
|
BitField<16, 5, u32> rs; // status register
|
||||||
|
BitField<21, 2, u32> l; // operation type
|
||||||
|
BitField<23, 7, u32> sig; // 0x10
|
||||||
|
BitField<30, 2, u32> size; // size
|
||||||
|
};
|
||||||
|
static_assert(Exclusive(0xC85FFC00).Verify());
|
||||||
|
static_assert(Exclusive(0xC85FFC00).AsOrdered() == 0xC85FFC00);
|
||||||
|
static_assert(Exclusive(0xC85F7C00).AsOrdered() == 0xC85FFC00);
|
||||||
|
static_assert(Exclusive(0xC8200440).AsOrdered() == 0xC8208440);
|
||||||
|
|
||||||
|
} // namespace Core::NCE
|
474
src/core/arm/nce/patch.cpp
Executable file
474
src/core/arm/nce/patch.cpp
Executable file
|
@ -0,0 +1,474 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "common/arm64/native_clock.h"
|
||||||
|
#include "common/bit_cast.h"
|
||||||
|
#include "common/literals.h"
|
||||||
|
#include "core/arm/nce/arm_nce.h"
|
||||||
|
#include "core/arm/nce/guest_context.h"
|
||||||
|
#include "core/arm/nce/instructions.h"
|
||||||
|
#include "core/arm/nce/patch.h"
|
||||||
|
#include "core/core.h"
|
||||||
|
#include "core/core_timing.h"
|
||||||
|
#include "core/hle/kernel/svc.h"
|
||||||
|
|
||||||
|
namespace Core::NCE {
|
||||||
|
|
||||||
|
using namespace Common::Literals;
|
||||||
|
using namespace oaknut::util;
|
||||||
|
|
||||||
|
using NativeExecutionParameters = Kernel::KThread::NativeExecutionParameters;
|
||||||
|
|
||||||
|
constexpr size_t MaxRelativeBranch = 128_MiB;
|
||||||
|
constexpr u32 ModuleCodeIndex = 0x24 / sizeof(u32);
|
||||||
|
|
||||||
|
Patcher::Patcher() : c(m_patch_instructions) {}
|
||||||
|
|
||||||
|
Patcher::~Patcher() = default;
|
||||||
|
|
||||||
|
void Patcher::PatchText(const Kernel::PhysicalMemory& program_image,
|
||||||
|
const Kernel::CodeSet::Segment& code) {
|
||||||
|
|
||||||
|
// Write save context helper function.
|
||||||
|
c.l(m_save_context);
|
||||||
|
WriteSaveContext();
|
||||||
|
|
||||||
|
// Write load context helper function.
|
||||||
|
c.l(m_load_context);
|
||||||
|
WriteLoadContext();
|
||||||
|
|
||||||
|
// Retrieve text segment data.
|
||||||
|
const auto text = std::span{program_image}.subspan(code.offset, code.size);
|
||||||
|
const auto text_words =
|
||||||
|
std::span<const u32>{reinterpret_cast<const u32*>(text.data()), text.size() / sizeof(u32)};
|
||||||
|
|
||||||
|
// Loop through instructions, patching as needed.
|
||||||
|
for (u32 i = ModuleCodeIndex; i < static_cast<u32>(text_words.size()); i++) {
|
||||||
|
const u32 inst = text_words[i];
|
||||||
|
|
||||||
|
const auto AddRelocations = [&] {
|
||||||
|
const uintptr_t this_offset = i * sizeof(u32);
|
||||||
|
const uintptr_t next_offset = this_offset + sizeof(u32);
|
||||||
|
|
||||||
|
// Relocate from here to patch.
|
||||||
|
this->BranchToPatch(this_offset);
|
||||||
|
|
||||||
|
// Relocate from patch to next instruction.
|
||||||
|
return next_offset;
|
||||||
|
};
|
||||||
|
|
||||||
|
// SVC
|
||||||
|
if (auto svc = SVC{inst}; svc.Verify()) {
|
||||||
|
WriteSvcTrampoline(AddRelocations(), svc.GetValue());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// MRS Xn, TPIDR_EL0
|
||||||
|
// MRS Xn, TPIDRRO_EL0
|
||||||
|
if (auto mrs = MRS{inst};
|
||||||
|
mrs.Verify() && (mrs.GetSystemReg() == TpidrroEl0 || mrs.GetSystemReg() == TpidrEl0)) {
|
||||||
|
const auto src_reg = mrs.GetSystemReg() == TpidrroEl0 ? oaknut::SystemReg::TPIDRRO_EL0
|
||||||
|
: oaknut::SystemReg::TPIDR_EL0;
|
||||||
|
const auto dest_reg = oaknut::XReg{static_cast<int>(mrs.GetRt())};
|
||||||
|
WriteMrsHandler(AddRelocations(), dest_reg, src_reg);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// MRS Xn, CNTPCT_EL0
|
||||||
|
if (auto mrs = MRS{inst}; mrs.Verify() && mrs.GetSystemReg() == CntpctEl0) {
|
||||||
|
WriteCntpctHandler(AddRelocations(), oaknut::XReg{static_cast<int>(mrs.GetRt())});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// MRS Xn, CNTFRQ_EL0
|
||||||
|
if (auto mrs = MRS{inst}; mrs.Verify() && mrs.GetSystemReg() == CntfrqEl0) {
|
||||||
|
UNREACHABLE();
|
||||||
|
}
|
||||||
|
|
||||||
|
// MSR TPIDR_EL0, Xn
|
||||||
|
if (auto msr = MSR{inst}; msr.Verify() && msr.GetSystemReg() == TpidrEl0) {
|
||||||
|
WriteMsrHandler(AddRelocations(), oaknut::XReg{static_cast<int>(msr.GetRt())});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto exclusive = Exclusive{inst}; exclusive.Verify()) {
|
||||||
|
m_exclusives.push_back(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine patching mode for the final relocation step
|
||||||
|
const size_t image_size = program_image.size();
|
||||||
|
this->mode = image_size > MaxRelativeBranch ? PatchMode::PreText : PatchMode::PostData;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Patcher::RelocateAndCopy(Common::ProcessAddress load_base,
|
||||||
|
const Kernel::CodeSet::Segment& code,
|
||||||
|
Kernel::PhysicalMemory& program_image,
|
||||||
|
EntryTrampolines* out_trampolines) {
|
||||||
|
const size_t patch_size = GetSectionSize();
|
||||||
|
const size_t image_size = program_image.size();
|
||||||
|
|
||||||
|
// Retrieve text segment data.
|
||||||
|
const auto text = std::span{program_image}.subspan(code.offset, code.size);
|
||||||
|
const auto text_words =
|
||||||
|
std::span<u32>{reinterpret_cast<u32*>(text.data()), text.size() / sizeof(u32)};
|
||||||
|
|
||||||
|
const auto ApplyBranchToPatchRelocation = [&](u32* target, const Relocation& rel) {
|
||||||
|
oaknut::CodeGenerator rc{target};
|
||||||
|
if (mode == PatchMode::PreText) {
|
||||||
|
rc.B(rel.patch_offset - patch_size - rel.module_offset);
|
||||||
|
} else {
|
||||||
|
rc.B(image_size - rel.module_offset + rel.patch_offset);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto ApplyBranchToModuleRelocation = [&](u32* target, const Relocation& rel) {
|
||||||
|
oaknut::CodeGenerator rc{target};
|
||||||
|
if (mode == PatchMode::PreText) {
|
||||||
|
rc.B(patch_size - rel.patch_offset + rel.module_offset);
|
||||||
|
} else {
|
||||||
|
rc.B(rel.module_offset - image_size - rel.patch_offset);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto RebasePatch = [&](ptrdiff_t patch_offset) {
|
||||||
|
if (mode == PatchMode::PreText) {
|
||||||
|
return GetInteger(load_base) + patch_offset;
|
||||||
|
} else {
|
||||||
|
return GetInteger(load_base) + image_size + patch_offset;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto RebasePc = [&](uintptr_t module_offset) {
|
||||||
|
if (mode == PatchMode::PreText) {
|
||||||
|
return GetInteger(load_base) + patch_size + module_offset;
|
||||||
|
} else {
|
||||||
|
return GetInteger(load_base) + module_offset;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// We are now ready to relocate!
|
||||||
|
for (const Relocation& rel : m_branch_to_patch_relocations) {
|
||||||
|
ApplyBranchToPatchRelocation(text_words.data() + rel.module_offset / sizeof(u32), rel);
|
||||||
|
}
|
||||||
|
for (const Relocation& rel : m_branch_to_module_relocations) {
|
||||||
|
ApplyBranchToModuleRelocation(m_patch_instructions.data() + rel.patch_offset / sizeof(u32),
|
||||||
|
rel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rewrite PC constants and record post trampolines
|
||||||
|
for (const Relocation& rel : m_write_module_pc_relocations) {
|
||||||
|
oaknut::CodeGenerator rc{m_patch_instructions.data() + rel.patch_offset / sizeof(u32)};
|
||||||
|
rc.dx(RebasePc(rel.module_offset));
|
||||||
|
}
|
||||||
|
for (const Trampoline& rel : m_trampolines) {
|
||||||
|
out_trampolines->insert({RebasePc(rel.module_offset), RebasePatch(rel.patch_offset)});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cortex-A57 seems to treat all exclusives as ordered, but newer processors do not.
|
||||||
|
// Convert to ordered to preserve this assumption.
|
||||||
|
for (const ModuleTextAddress i : m_exclusives) {
|
||||||
|
auto exclusive = Exclusive{text_words[i]};
|
||||||
|
text_words[i] = exclusive.AsOrdered();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy to program image
|
||||||
|
if (this->mode == PatchMode::PreText) {
|
||||||
|
std::memcpy(program_image.data(), m_patch_instructions.data(),
|
||||||
|
m_patch_instructions.size() * sizeof(u32));
|
||||||
|
} else {
|
||||||
|
program_image.resize(image_size + patch_size);
|
||||||
|
std::memcpy(program_image.data() + image_size, m_patch_instructions.data(),
|
||||||
|
m_patch_instructions.size() * sizeof(u32));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t Patcher::GetSectionSize() const noexcept {
|
||||||
|
return Common::AlignUp(m_patch_instructions.size() * sizeof(u32), Core::Memory::YUZU_PAGESIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Patcher::WriteLoadContext() {
|
||||||
|
// This function was called, which modifies X30, so use that as a scratch register.
|
||||||
|
// SP contains the guest X30, so save our return X30 to SP + 8, since we have allocated 16 bytes
|
||||||
|
// of stack.
|
||||||
|
c.STR(X30, SP, 8);
|
||||||
|
c.MRS(X30, oaknut::SystemReg::TPIDR_EL0);
|
||||||
|
c.LDR(X30, X30, offsetof(NativeExecutionParameters, native_context));
|
||||||
|
|
||||||
|
// Load system registers.
|
||||||
|
c.LDR(W0, X30, offsetof(GuestContext, fpsr));
|
||||||
|
c.MSR(oaknut::SystemReg::FPSR, X0);
|
||||||
|
c.LDR(W0, X30, offsetof(GuestContext, fpcr));
|
||||||
|
c.MSR(oaknut::SystemReg::FPCR, X0);
|
||||||
|
c.LDR(W0, X30, offsetof(GuestContext, nzcv));
|
||||||
|
c.MSR(oaknut::SystemReg::NZCV, X0);
|
||||||
|
|
||||||
|
// Load all vector registers.
|
||||||
|
static constexpr size_t VEC_OFF = offsetof(GuestContext, vector_registers);
|
||||||
|
for (int i = 0; i <= 30; i += 2) {
|
||||||
|
c.LDP(oaknut::QReg{i}, oaknut::QReg{i + 1}, X30, VEC_OFF + 16 * i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load all general-purpose registers except X30.
|
||||||
|
for (int i = 0; i <= 28; i += 2) {
|
||||||
|
c.LDP(oaknut::XReg{i}, oaknut::XReg{i + 1}, X30, 8 * i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload our return X30 from the stack and return.
|
||||||
|
// The patch code will reload the guest X30 for us.
|
||||||
|
c.LDR(X30, SP, 8);
|
||||||
|
c.RET();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Patcher::WriteSaveContext() {
|
||||||
|
// This function was called, which modifies X30, so use that as a scratch register.
|
||||||
|
// SP contains the guest X30, so save our X30 to SP + 8, since we have allocated 16 bytes of
|
||||||
|
// stack.
|
||||||
|
c.STR(X30, SP, 8);
|
||||||
|
c.MRS(X30, oaknut::SystemReg::TPIDR_EL0);
|
||||||
|
c.LDR(X30, X30, offsetof(NativeExecutionParameters, native_context));
|
||||||
|
|
||||||
|
// Store all general-purpose registers except X30.
|
||||||
|
for (int i = 0; i <= 28; i += 2) {
|
||||||
|
c.STP(oaknut::XReg{i}, oaknut::XReg{i + 1}, X30, 8 * i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store all vector registers.
|
||||||
|
static constexpr size_t VEC_OFF = offsetof(GuestContext, vector_registers);
|
||||||
|
for (int i = 0; i <= 30; i += 2) {
|
||||||
|
c.STP(oaknut::QReg{i}, oaknut::QReg{i + 1}, X30, VEC_OFF + 16 * i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store guest system registers, X30 and SP, using X0 as a scratch register.
|
||||||
|
c.STR(X0, SP, PRE_INDEXED, -16);
|
||||||
|
c.LDR(X0, SP, 16);
|
||||||
|
c.STR(X0, X30, 8 * 30);
|
||||||
|
c.ADD(X0, SP, 32);
|
||||||
|
c.STR(X0, X30, offsetof(GuestContext, sp));
|
||||||
|
c.MRS(X0, oaknut::SystemReg::FPSR);
|
||||||
|
c.STR(W0, X30, offsetof(GuestContext, fpsr));
|
||||||
|
c.MRS(X0, oaknut::SystemReg::FPCR);
|
||||||
|
c.STR(W0, X30, offsetof(GuestContext, fpcr));
|
||||||
|
c.MRS(X0, oaknut::SystemReg::NZCV);
|
||||||
|
c.STR(W0, X30, offsetof(GuestContext, nzcv));
|
||||||
|
c.LDR(X0, SP, POST_INDEXED, 16);
|
||||||
|
|
||||||
|
// Reload our return X30 from the stack, and return.
|
||||||
|
c.LDR(X30, SP, 8);
|
||||||
|
c.RET();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Patcher::WriteSvcTrampoline(ModuleDestLabel module_dest, u32 svc_id) {
|
||||||
|
// We are about to start saving state, so we need to lock the context.
|
||||||
|
this->LockContext();
|
||||||
|
|
||||||
|
// Store guest X30 to the stack. Then, save the context and restore the stack.
|
||||||
|
// This will save all registers except PC, but we know PC at patch time.
|
||||||
|
c.STR(X30, SP, PRE_INDEXED, -16);
|
||||||
|
c.BL(m_save_context);
|
||||||
|
c.LDR(X30, SP, POST_INDEXED, 16);
|
||||||
|
|
||||||
|
// Now that we've saved all registers, we can use any registers as scratch.
|
||||||
|
// Store PC + 4 to arm interface, since we know the instruction offset from the entry point.
|
||||||
|
oaknut::Label pc_after_svc;
|
||||||
|
c.MRS(X1, oaknut::SystemReg::TPIDR_EL0);
|
||||||
|
c.LDR(X1, X1, offsetof(NativeExecutionParameters, native_context));
|
||||||
|
c.LDR(X2, pc_after_svc);
|
||||||
|
c.STR(X2, X1, offsetof(GuestContext, pc));
|
||||||
|
|
||||||
|
// Store SVC number to execute when we return
|
||||||
|
c.MOV(X2, svc_id);
|
||||||
|
c.STR(W2, X1, offsetof(GuestContext, svc_swi));
|
||||||
|
|
||||||
|
// We are calling a SVC. Clear esr_el1 and return it.
|
||||||
|
static_assert(std::is_same_v<std::underlying_type_t<HaltReason>, u64>);
|
||||||
|
oaknut::Label retry;
|
||||||
|
c.ADD(X2, X1, offsetof(GuestContext, esr_el1));
|
||||||
|
c.l(retry);
|
||||||
|
c.LDAXR(X0, X2);
|
||||||
|
c.STLXR(W3, XZR, X2);
|
||||||
|
c.CBNZ(W3, retry);
|
||||||
|
|
||||||
|
// Add "calling SVC" flag. Since this is X0, this is now our return value.
|
||||||
|
c.ORR(X0, X0, static_cast<u64>(HaltReason::SupervisorCall));
|
||||||
|
|
||||||
|
// Offset the GuestContext pointer to the HostContext member.
|
||||||
|
// STP has limited range of [-512, 504] which we can't reach otherwise
|
||||||
|
// NB: Due to this all offsets below are from the start of HostContext.
|
||||||
|
c.ADD(X1, X1, offsetof(GuestContext, host_ctx));
|
||||||
|
|
||||||
|
// Reload host TPIDR_EL0 and SP.
|
||||||
|
static_assert(offsetof(HostContext, host_sp) + 8 == offsetof(HostContext, host_tpidr_el0));
|
||||||
|
c.LDP(X2, X3, X1, offsetof(HostContext, host_sp));
|
||||||
|
c.MOV(SP, X2);
|
||||||
|
c.MSR(oaknut::SystemReg::TPIDR_EL0, X3);
|
||||||
|
|
||||||
|
// Load callee-saved host registers and return to host.
|
||||||
|
static constexpr size_t HOST_REGS_OFF = offsetof(HostContext, host_saved_regs);
|
||||||
|
static constexpr size_t HOST_VREGS_OFF = offsetof(HostContext, host_saved_vregs);
|
||||||
|
c.LDP(X19, X20, X1, HOST_REGS_OFF);
|
||||||
|
c.LDP(X21, X22, X1, HOST_REGS_OFF + 2 * sizeof(u64));
|
||||||
|
c.LDP(X23, X24, X1, HOST_REGS_OFF + 4 * sizeof(u64));
|
||||||
|
c.LDP(X25, X26, X1, HOST_REGS_OFF + 6 * sizeof(u64));
|
||||||
|
c.LDP(X27, X28, X1, HOST_REGS_OFF + 8 * sizeof(u64));
|
||||||
|
c.LDP(X29, X30, X1, HOST_REGS_OFF + 10 * sizeof(u64));
|
||||||
|
c.LDP(Q8, Q9, X1, HOST_VREGS_OFF);
|
||||||
|
c.LDP(Q10, Q11, X1, HOST_VREGS_OFF + 2 * sizeof(u128));
|
||||||
|
c.LDP(Q12, Q13, X1, HOST_VREGS_OFF + 4 * sizeof(u128));
|
||||||
|
c.LDP(Q14, Q15, X1, HOST_VREGS_OFF + 6 * sizeof(u128));
|
||||||
|
c.RET();
|
||||||
|
|
||||||
|
// Write the post-SVC trampoline address, which will jump back to the guest after restoring its
|
||||||
|
// state.
|
||||||
|
m_trampolines.push_back({c.offset(), module_dest});
|
||||||
|
|
||||||
|
// Host called this location. Save the return address so we can
|
||||||
|
// unwind the stack properly when jumping back.
|
||||||
|
c.MRS(X2, oaknut::SystemReg::TPIDR_EL0);
|
||||||
|
c.LDR(X2, X2, offsetof(NativeExecutionParameters, native_context));
|
||||||
|
c.ADD(X0, X2, offsetof(GuestContext, host_ctx));
|
||||||
|
c.STR(X30, X0, offsetof(HostContext, host_saved_regs) + 11 * sizeof(u64));
|
||||||
|
|
||||||
|
// Reload all guest registers except X30 and PC.
|
||||||
|
// The function also expects 16 bytes of stack already allocated.
|
||||||
|
c.STR(X30, SP, PRE_INDEXED, -16);
|
||||||
|
c.BL(m_load_context);
|
||||||
|
c.LDR(X30, SP, POST_INDEXED, 16);
|
||||||
|
|
||||||
|
// Use X1 as a scratch register to restore X30.
|
||||||
|
c.STR(X1, SP, PRE_INDEXED, -16);
|
||||||
|
c.MRS(X1, oaknut::SystemReg::TPIDR_EL0);
|
||||||
|
c.LDR(X1, X1, offsetof(NativeExecutionParameters, native_context));
|
||||||
|
c.LDR(X30, X1, offsetof(GuestContext, cpu_registers) + sizeof(u64) * 30);
|
||||||
|
c.LDR(X1, SP, POST_INDEXED, 16);
|
||||||
|
|
||||||
|
// Unlock the context.
|
||||||
|
this->UnlockContext();
|
||||||
|
|
||||||
|
// Jump back to the instruction after the emulated SVC.
|
||||||
|
this->BranchToModule(module_dest);
|
||||||
|
|
||||||
|
// Store PC after call.
|
||||||
|
c.l(pc_after_svc);
|
||||||
|
this->WriteModulePc(module_dest);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Patcher::WriteMrsHandler(ModuleDestLabel module_dest, oaknut::XReg dest_reg,
|
||||||
|
oaknut::SystemReg src_reg) {
|
||||||
|
// Retrieve emulated TLS register from GuestContext.
|
||||||
|
c.MRS(dest_reg, oaknut::SystemReg::TPIDR_EL0);
|
||||||
|
if (src_reg == oaknut::SystemReg::TPIDRRO_EL0) {
|
||||||
|
c.LDR(dest_reg, dest_reg, offsetof(NativeExecutionParameters, tpidrro_el0));
|
||||||
|
} else {
|
||||||
|
c.LDR(dest_reg, dest_reg, offsetof(NativeExecutionParameters, tpidr_el0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jump back to the instruction after the emulated MRS.
|
||||||
|
this->BranchToModule(module_dest);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Patcher::WriteMsrHandler(ModuleDestLabel module_dest, oaknut::XReg src_reg) {
|
||||||
|
const auto scratch_reg = src_reg.index() == 0 ? X1 : X0;
|
||||||
|
c.STR(scratch_reg, SP, PRE_INDEXED, -16);
|
||||||
|
|
||||||
|
// Save guest value to NativeExecutionParameters::tpidr_el0.
|
||||||
|
c.MRS(scratch_reg, oaknut::SystemReg::TPIDR_EL0);
|
||||||
|
c.STR(src_reg, scratch_reg, offsetof(NativeExecutionParameters, tpidr_el0));
|
||||||
|
|
||||||
|
// Restore scratch register.
|
||||||
|
c.LDR(scratch_reg, SP, POST_INDEXED, 16);
|
||||||
|
|
||||||
|
// Jump back to the instruction after the emulated MSR.
|
||||||
|
this->BranchToModule(module_dest);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Patcher::WriteCntpctHandler(ModuleDestLabel module_dest, oaknut::XReg dest_reg) {
|
||||||
|
static Common::Arm64::NativeClock clock{};
|
||||||
|
const auto factor = clock.GetGuestCNTFRQFactor();
|
||||||
|
const auto raw_factor = Common::BitCast<std::array<u64, 2>>(factor);
|
||||||
|
|
||||||
|
const auto use_x2_x3 = dest_reg.index() == 0 || dest_reg.index() == 1;
|
||||||
|
oaknut::XReg scratch0 = use_x2_x3 ? X2 : X0;
|
||||||
|
oaknut::XReg scratch1 = use_x2_x3 ? X3 : X1;
|
||||||
|
|
||||||
|
oaknut::Label factorlo;
|
||||||
|
oaknut::Label factorhi;
|
||||||
|
|
||||||
|
// Save scratches.
|
||||||
|
c.STP(scratch0, scratch1, SP, PRE_INDEXED, -16);
|
||||||
|
|
||||||
|
// Load counter value.
|
||||||
|
c.MRS(dest_reg, oaknut::SystemReg::CNTVCT_EL0);
|
||||||
|
|
||||||
|
// Load scaling factor.
|
||||||
|
c.LDR(scratch0, factorlo);
|
||||||
|
c.LDR(scratch1, factorhi);
|
||||||
|
|
||||||
|
// Multiply low bits and get result.
|
||||||
|
c.UMULH(scratch0, dest_reg, scratch0);
|
||||||
|
|
||||||
|
// Multiply high bits and add low bit result.
|
||||||
|
c.MADD(dest_reg, dest_reg, scratch1, scratch0);
|
||||||
|
|
||||||
|
// Reload scratches.
|
||||||
|
c.LDP(scratch0, scratch1, SP, POST_INDEXED, 16);
|
||||||
|
|
||||||
|
// Jump back to the instruction after the emulated MRS.
|
||||||
|
this->BranchToModule(module_dest);
|
||||||
|
|
||||||
|
// Scaling factor constant values.
|
||||||
|
c.l(factorlo);
|
||||||
|
c.dx(raw_factor[0]);
|
||||||
|
c.l(factorhi);
|
||||||
|
c.dx(raw_factor[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Patcher::LockContext() {
|
||||||
|
oaknut::Label retry;
|
||||||
|
|
||||||
|
// Save scratches.
|
||||||
|
c.STP(X0, X1, SP, PRE_INDEXED, -16);
|
||||||
|
|
||||||
|
// Reload lock pointer.
|
||||||
|
c.l(retry);
|
||||||
|
c.CLREX();
|
||||||
|
c.MRS(X0, oaknut::SystemReg::TPIDR_EL0);
|
||||||
|
c.ADD(X0, X0, offsetof(NativeExecutionParameters, lock));
|
||||||
|
|
||||||
|
static_assert(SpinLockLocked == 0);
|
||||||
|
|
||||||
|
// Load-linked with acquire ordering.
|
||||||
|
c.LDAXR(W1, X0);
|
||||||
|
|
||||||
|
// If the value was SpinLockLocked, clear monitor and retry.
|
||||||
|
c.CBZ(W1, retry);
|
||||||
|
|
||||||
|
// Store-conditional SpinLockLocked with relaxed ordering.
|
||||||
|
c.STXR(W1, WZR, X0);
|
||||||
|
|
||||||
|
// If we failed to store, retry.
|
||||||
|
c.CBNZ(W1, retry);
|
||||||
|
|
||||||
|
// We succeeded! Reload scratches.
|
||||||
|
c.LDP(X0, X1, SP, POST_INDEXED, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Patcher::UnlockContext() {
|
||||||
|
// Save scratches.
|
||||||
|
c.STP(X0, X1, SP, PRE_INDEXED, -16);
|
||||||
|
|
||||||
|
// Load lock pointer.
|
||||||
|
c.MRS(X0, oaknut::SystemReg::TPIDR_EL0);
|
||||||
|
c.ADD(X0, X0, offsetof(NativeExecutionParameters, lock));
|
||||||
|
|
||||||
|
// Load SpinLockUnlocked.
|
||||||
|
c.MOV(W1, SpinLockUnlocked);
|
||||||
|
|
||||||
|
// Store value with release ordering.
|
||||||
|
c.STLR(W1, X0);
|
||||||
|
|
||||||
|
// Load scratches.
|
||||||
|
c.LDP(X0, X1, SP, POST_INDEXED, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Core::NCE
|
98
src/core/arm/nce/patch.h
Executable file
98
src/core/arm/nce/patch.h
Executable file
|
@ -0,0 +1,98 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <span>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
#include <oaknut/code_block.hpp>
|
||||||
|
#include <oaknut/oaknut.hpp>
|
||||||
|
|
||||||
|
#include "common/common_types.h"
|
||||||
|
#include "core/hle/kernel/code_set.h"
|
||||||
|
#include "core/hle/kernel/k_typed_address.h"
|
||||||
|
#include "core/hle/kernel/physical_memory.h"
|
||||||
|
|
||||||
|
namespace Core::NCE {
|
||||||
|
|
||||||
|
enum class PatchMode : u32 {
|
||||||
|
None,
|
||||||
|
PreText, ///< Patch section is inserted before .text
|
||||||
|
PostData, ///< Patch section is inserted after .data
|
||||||
|
};
|
||||||
|
|
||||||
|
using ModuleTextAddress = u64;
|
||||||
|
using PatchTextAddress = u64;
|
||||||
|
using EntryTrampolines = std::unordered_map<ModuleTextAddress, PatchTextAddress>;
|
||||||
|
|
||||||
|
class Patcher {
|
||||||
|
public:
|
||||||
|
explicit Patcher();
|
||||||
|
~Patcher();
|
||||||
|
|
||||||
|
void PatchText(const Kernel::PhysicalMemory& program_image,
|
||||||
|
const Kernel::CodeSet::Segment& code);
|
||||||
|
void RelocateAndCopy(Common::ProcessAddress load_base, const Kernel::CodeSet::Segment& code,
|
||||||
|
Kernel::PhysicalMemory& program_image, EntryTrampolines* out_trampolines);
|
||||||
|
size_t GetSectionSize() const noexcept;
|
||||||
|
|
||||||
|
[[nodiscard]] PatchMode GetPatchMode() const noexcept {
|
||||||
|
return mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
using ModuleDestLabel = uintptr_t;
|
||||||
|
|
||||||
|
struct Trampoline {
|
||||||
|
ptrdiff_t patch_offset;
|
||||||
|
uintptr_t module_offset;
|
||||||
|
};
|
||||||
|
|
||||||
|
void WriteLoadContext();
|
||||||
|
void WriteSaveContext();
|
||||||
|
void LockContext();
|
||||||
|
void UnlockContext();
|
||||||
|
void WriteSvcTrampoline(ModuleDestLabel module_dest, u32 svc_id);
|
||||||
|
void WriteMrsHandler(ModuleDestLabel module_dest, oaknut::XReg dest_reg,
|
||||||
|
oaknut::SystemReg src_reg);
|
||||||
|
void WriteMsrHandler(ModuleDestLabel module_dest, oaknut::XReg src_reg);
|
||||||
|
void WriteCntpctHandler(ModuleDestLabel module_dest, oaknut::XReg dest_reg);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void BranchToPatch(uintptr_t module_dest) {
|
||||||
|
m_branch_to_patch_relocations.push_back({c.offset(), module_dest});
|
||||||
|
}
|
||||||
|
|
||||||
|
void BranchToModule(uintptr_t module_dest) {
|
||||||
|
m_branch_to_module_relocations.push_back({c.offset(), module_dest});
|
||||||
|
c.dw(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WriteModulePc(uintptr_t module_dest) {
|
||||||
|
m_write_module_pc_relocations.push_back({c.offset(), module_dest});
|
||||||
|
c.dx(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
// List of patch instructions we have generated.
|
||||||
|
std::vector<u32> m_patch_instructions{};
|
||||||
|
|
||||||
|
// Relocation type for relative branch from module to patch.
|
||||||
|
struct Relocation {
|
||||||
|
ptrdiff_t patch_offset; ///< Offset in bytes from the start of the patch section.
|
||||||
|
uintptr_t module_offset; ///< Offset in bytes from the start of the text section.
|
||||||
|
};
|
||||||
|
|
||||||
|
oaknut::VectorCodeGenerator c;
|
||||||
|
std::vector<Trampoline> m_trampolines;
|
||||||
|
std::vector<Relocation> m_branch_to_patch_relocations{};
|
||||||
|
std::vector<Relocation> m_branch_to_module_relocations{};
|
||||||
|
std::vector<Relocation> m_write_module_pc_relocations{};
|
||||||
|
std::vector<ModuleTextAddress> m_exclusives{};
|
||||||
|
oaknut::Label m_save_context{};
|
||||||
|
oaknut::Label m_load_context{};
|
||||||
|
PatchMode mode{PatchMode::None};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Core::NCE
|
|
@ -211,6 +211,8 @@ void CpuManager::RunThread(std::stop_token token, std::size_t core) {
|
||||||
system.GPU().ObtainContext();
|
system.GPU().ObtainContext();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
system.ArmInterface(core).Initialize();
|
||||||
|
|
||||||
auto& kernel = system.Kernel();
|
auto& kernel = system.Kernel();
|
||||||
auto& scheduler = *kernel.CurrentScheduler();
|
auto& scheduler = *kernel.CurrentScheduler();
|
||||||
auto* thread = scheduler.GetSchedulerCurrentThread();
|
auto* thread = scheduler.GetSchedulerCurrentThread();
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
namespace Core {
|
namespace Core {
|
||||||
|
|
||||||
#ifdef ANDROID
|
#ifdef HAS_NCE
|
||||||
constexpr size_t VirtualReserveSize = 1ULL << 38;
|
constexpr size_t VirtualReserveSize = 1ULL << 38;
|
||||||
#else
|
#else
|
||||||
constexpr size_t VirtualReserveSize = 1ULL << 39;
|
constexpr size_t VirtualReserveSize = 1ULL << 39;
|
||||||
|
@ -15,6 +15,7 @@ constexpr size_t VirtualReserveSize = 1ULL << 39;
|
||||||
DeviceMemory::DeviceMemory()
|
DeviceMemory::DeviceMemory()
|
||||||
: buffer{Kernel::Board::Nintendo::Nx::KSystemControl::Init::GetIntendedMemorySize(),
|
: buffer{Kernel::Board::Nintendo::Nx::KSystemControl::Init::GetIntendedMemorySize(),
|
||||||
VirtualReserveSize} {}
|
VirtualReserveSize} {}
|
||||||
|
|
||||||
DeviceMemory::~DeviceMemory() = default;
|
DeviceMemory::~DeviceMemory() = default;
|
||||||
|
|
||||||
} // namespace Core
|
} // namespace Core
|
||||||
|
|
|
@ -75,12 +75,26 @@ struct CodeSet final {
|
||||||
return segments[2];
|
return segments[2];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef HAS_NCE
|
||||||
|
Segment& PatchSegment() {
|
||||||
|
return patch_segment;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Segment& PatchSegment() const {
|
||||||
|
return patch_segment;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
/// The overall data that backs this code set.
|
/// The overall data that backs this code set.
|
||||||
Kernel::PhysicalMemory memory;
|
Kernel::PhysicalMemory memory;
|
||||||
|
|
||||||
/// The segments that comprise this code set.
|
/// The segments that comprise this code set.
|
||||||
std::array<Segment, 3> segments;
|
std::array<Segment, 3> segments;
|
||||||
|
|
||||||
|
#ifdef HAS_NCE
|
||||||
|
Segment patch_segment;
|
||||||
|
#endif
|
||||||
|
|
||||||
/// The entry point address for this code set.
|
/// The entry point address for this code set.
|
||||||
KProcessAddress entrypoint = 0;
|
KProcessAddress entrypoint = 0;
|
||||||
};
|
};
|
||||||
|
|
|
@ -25,8 +25,8 @@ constexpr std::array<KAddressSpaceInfo, 13> AddressSpaceInfos{{
|
||||||
{ .bit_width = 36, .address = 2_GiB , .size = 64_GiB - 2_GiB , .type = KAddressSpaceInfo::Type::MapLarge, },
|
{ .bit_width = 36, .address = 2_GiB , .size = 64_GiB - 2_GiB , .type = KAddressSpaceInfo::Type::MapLarge, },
|
||||||
{ .bit_width = 36, .address = Size_Invalid, .size = 8_GiB , .type = KAddressSpaceInfo::Type::Heap, },
|
{ .bit_width = 36, .address = Size_Invalid, .size = 8_GiB , .type = KAddressSpaceInfo::Type::Heap, },
|
||||||
{ .bit_width = 36, .address = Size_Invalid, .size = 6_GiB , .type = KAddressSpaceInfo::Type::Alias, },
|
{ .bit_width = 36, .address = Size_Invalid, .size = 6_GiB , .type = KAddressSpaceInfo::Type::Alias, },
|
||||||
#ifdef ANDROID
|
#ifdef HAS_NCE
|
||||||
// With Android, we use a 38-bit address space due to memory limitations. This should (safely) truncate ASLR region.
|
// With NCE, we use a 38-bit address space due to memory limitations. This should (safely) truncate ASLR region.
|
||||||
{ .bit_width = 39, .address = 128_MiB , .size = 256_GiB - 128_MiB, .type = KAddressSpaceInfo::Type::Map39Bit, },
|
{ .bit_width = 39, .address = 128_MiB , .size = 256_GiB - 128_MiB, .type = KAddressSpaceInfo::Type::Map39Bit, },
|
||||||
#else
|
#else
|
||||||
{ .bit_width = 39, .address = 128_MiB , .size = 512_GiB - 128_MiB, .type = KAddressSpaceInfo::Type::Map39Bit, },
|
{ .bit_width = 39, .address = 128_MiB , .size = 512_GiB - 128_MiB, .type = KAddressSpaceInfo::Type::Map39Bit, },
|
||||||
|
|
|
@ -88,6 +88,22 @@ Result FlushDataCache(AddressType addr, u64 size) {
|
||||||
R_SUCCEED();
|
R_SUCCEED();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constexpr Common::MemoryPermission ConvertToMemoryPermission(KMemoryPermission perm) {
|
||||||
|
Common::MemoryPermission perms{};
|
||||||
|
if (True(perm & KMemoryPermission::UserRead)) {
|
||||||
|
perms |= Common::MemoryPermission::Read;
|
||||||
|
}
|
||||||
|
if (True(perm & KMemoryPermission::UserWrite)) {
|
||||||
|
perms |= Common::MemoryPermission::Write;
|
||||||
|
}
|
||||||
|
#ifdef HAS_NCE
|
||||||
|
if (True(perm & KMemoryPermission::UserExecute)) {
|
||||||
|
perms |= Common::MemoryPermission::Execute;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return perms;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void KPageTableBase::MemoryRange::Open() {
|
void KPageTableBase::MemoryRange::Open() {
|
||||||
|
@ -170,7 +186,8 @@ Result KPageTableBase::InitializeForProcess(Svc::CreateProcessFlag as_type, bool
|
||||||
KMemoryManager::Pool pool, KProcessAddress code_address,
|
KMemoryManager::Pool pool, KProcessAddress code_address,
|
||||||
size_t code_size, KSystemResource* system_resource,
|
size_t code_size, KSystemResource* system_resource,
|
||||||
KResourceLimit* resource_limit,
|
KResourceLimit* resource_limit,
|
||||||
Core::Memory::Memory& memory) {
|
Core::Memory::Memory& memory,
|
||||||
|
KProcessAddress aslr_space_start) {
|
||||||
// Calculate region extents.
|
// Calculate region extents.
|
||||||
const size_t as_width = GetAddressSpaceWidth(as_type);
|
const size_t as_width = GetAddressSpaceWidth(as_type);
|
||||||
const KProcessAddress start = 0;
|
const KProcessAddress start = 0;
|
||||||
|
@ -211,7 +228,8 @@ Result KPageTableBase::InitializeForProcess(Svc::CreateProcessFlag as_type, bool
|
||||||
heap_region_size = GetSpaceSize(KAddressSpaceInfo::Type::Heap);
|
heap_region_size = GetSpaceSize(KAddressSpaceInfo::Type::Heap);
|
||||||
stack_region_size = GetSpaceSize(KAddressSpaceInfo::Type::Stack);
|
stack_region_size = GetSpaceSize(KAddressSpaceInfo::Type::Stack);
|
||||||
kernel_map_region_size = GetSpaceSize(KAddressSpaceInfo::Type::MapSmall);
|
kernel_map_region_size = GetSpaceSize(KAddressSpaceInfo::Type::MapSmall);
|
||||||
m_code_region_start = GetSpaceStart(KAddressSpaceInfo::Type::Map39Bit);
|
m_code_region_start = m_address_space_start + aslr_space_start +
|
||||||
|
GetSpaceStart(KAddressSpaceInfo::Type::Map39Bit);
|
||||||
m_code_region_end = m_code_region_start + GetSpaceSize(KAddressSpaceInfo::Type::Map39Bit);
|
m_code_region_end = m_code_region_start + GetSpaceSize(KAddressSpaceInfo::Type::Map39Bit);
|
||||||
m_alias_code_region_start = m_code_region_start;
|
m_alias_code_region_start = m_code_region_start;
|
||||||
m_alias_code_region_end = m_code_region_end;
|
m_alias_code_region_end = m_code_region_end;
|
||||||
|
@ -5643,7 +5661,8 @@ Result KPageTableBase::Operate(PageLinkedList* page_list, KProcessAddress virt_a
|
||||||
case OperationType::Map: {
|
case OperationType::Map: {
|
||||||
ASSERT(virt_addr != 0);
|
ASSERT(virt_addr != 0);
|
||||||
ASSERT(Common::IsAligned(GetInteger(virt_addr), PageSize));
|
ASSERT(Common::IsAligned(GetInteger(virt_addr), PageSize));
|
||||||
m_memory->MapMemoryRegion(*m_impl, virt_addr, num_pages * PageSize, phys_addr);
|
m_memory->MapMemoryRegion(*m_impl, virt_addr, num_pages * PageSize, phys_addr,
|
||||||
|
ConvertToMemoryPermission(properties.perm));
|
||||||
|
|
||||||
// Open references to pages, if we should.
|
// Open references to pages, if we should.
|
||||||
if (this->IsHeapPhysicalAddress(phys_addr)) {
|
if (this->IsHeapPhysicalAddress(phys_addr)) {
|
||||||
|
@ -5658,8 +5677,11 @@ Result KPageTableBase::Operate(PageLinkedList* page_list, KProcessAddress virt_a
|
||||||
}
|
}
|
||||||
case OperationType::ChangePermissions:
|
case OperationType::ChangePermissions:
|
||||||
case OperationType::ChangePermissionsAndRefresh:
|
case OperationType::ChangePermissionsAndRefresh:
|
||||||
case OperationType::ChangePermissionsAndRefreshAndFlush:
|
case OperationType::ChangePermissionsAndRefreshAndFlush: {
|
||||||
|
m_memory->ProtectRegion(*m_impl, virt_addr, num_pages * PageSize,
|
||||||
|
ConvertToMemoryPermission(properties.perm));
|
||||||
R_SUCCEED();
|
R_SUCCEED();
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
UNREACHABLE();
|
UNREACHABLE();
|
||||||
}
|
}
|
||||||
|
@ -5687,7 +5709,8 @@ Result KPageTableBase::Operate(PageLinkedList* page_list, KProcessAddress virt_a
|
||||||
const size_t size{node.GetNumPages() * PageSize};
|
const size_t size{node.GetNumPages() * PageSize};
|
||||||
|
|
||||||
// Map the pages.
|
// Map the pages.
|
||||||
m_memory->MapMemoryRegion(*m_impl, virt_addr, size, node.GetAddress());
|
m_memory->MapMemoryRegion(*m_impl, virt_addr, size, node.GetAddress(),
|
||||||
|
ConvertToMemoryPermission(properties.perm));
|
||||||
|
|
||||||
virt_addr += size;
|
virt_addr += size;
|
||||||
}
|
}
|
||||||
|
|
|
@ -235,7 +235,8 @@ public:
|
||||||
bool enable_device_address_space_merge, bool from_back,
|
bool enable_device_address_space_merge, bool from_back,
|
||||||
KMemoryManager::Pool pool, KProcessAddress code_address,
|
KMemoryManager::Pool pool, KProcessAddress code_address,
|
||||||
size_t code_size, KSystemResource* system_resource,
|
size_t code_size, KSystemResource* system_resource,
|
||||||
KResourceLimit* resource_limit, Core::Memory::Memory& memory);
|
KResourceLimit* resource_limit, Core::Memory::Memory& memory,
|
||||||
|
KProcessAddress aslr_space_start);
|
||||||
|
|
||||||
void Finalize();
|
void Finalize();
|
||||||
|
|
||||||
|
|
|
@ -300,7 +300,7 @@ Result KProcess::Initialize(const Svc::CreateProcessParameter& params, const KPa
|
||||||
False(params.flags & Svc::CreateProcessFlag::DisableDeviceAddressSpaceMerge);
|
False(params.flags & Svc::CreateProcessFlag::DisableDeviceAddressSpaceMerge);
|
||||||
R_TRY(m_page_table.Initialize(as_type, enable_aslr, enable_das_merge, !enable_aslr, pool,
|
R_TRY(m_page_table.Initialize(as_type, enable_aslr, enable_das_merge, !enable_aslr, pool,
|
||||||
params.code_address, params.code_num_pages * PageSize,
|
params.code_address, params.code_num_pages * PageSize,
|
||||||
m_system_resource, res_limit, this->GetMemory()));
|
m_system_resource, res_limit, this->GetMemory(), 0));
|
||||||
}
|
}
|
||||||
ON_RESULT_FAILURE_2 {
|
ON_RESULT_FAILURE_2 {
|
||||||
m_page_table.Finalize();
|
m_page_table.Finalize();
|
||||||
|
@ -332,7 +332,7 @@ Result KProcess::Initialize(const Svc::CreateProcessParameter& params, const KPa
|
||||||
|
|
||||||
Result KProcess::Initialize(const Svc::CreateProcessParameter& params,
|
Result KProcess::Initialize(const Svc::CreateProcessParameter& params,
|
||||||
std::span<const u32> user_caps, KResourceLimit* res_limit,
|
std::span<const u32> user_caps, KResourceLimit* res_limit,
|
||||||
KMemoryManager::Pool pool) {
|
KMemoryManager::Pool pool, KProcessAddress aslr_space_start) {
|
||||||
ASSERT(res_limit != nullptr);
|
ASSERT(res_limit != nullptr);
|
||||||
|
|
||||||
// Set members.
|
// Set members.
|
||||||
|
@ -393,7 +393,7 @@ Result KProcess::Initialize(const Svc::CreateProcessParameter& params,
|
||||||
False(params.flags & Svc::CreateProcessFlag::DisableDeviceAddressSpaceMerge);
|
False(params.flags & Svc::CreateProcessFlag::DisableDeviceAddressSpaceMerge);
|
||||||
R_TRY(m_page_table.Initialize(as_type, enable_aslr, enable_das_merge, !enable_aslr, pool,
|
R_TRY(m_page_table.Initialize(as_type, enable_aslr, enable_das_merge, !enable_aslr, pool,
|
||||||
params.code_address, code_size, m_system_resource, res_limit,
|
params.code_address, code_size, m_system_resource, res_limit,
|
||||||
this->GetMemory()));
|
this->GetMemory(), aslr_space_start));
|
||||||
}
|
}
|
||||||
ON_RESULT_FAILURE_2 {
|
ON_RESULT_FAILURE_2 {
|
||||||
m_page_table.Finalize();
|
m_page_table.Finalize();
|
||||||
|
@ -1128,7 +1128,7 @@ KProcess::KProcess(KernelCore& kernel)
|
||||||
KProcess::~KProcess() = default;
|
KProcess::~KProcess() = default;
|
||||||
|
|
||||||
Result KProcess::LoadFromMetadata(const FileSys::ProgramMetadata& metadata, std::size_t code_size,
|
Result KProcess::LoadFromMetadata(const FileSys::ProgramMetadata& metadata, std::size_t code_size,
|
||||||
bool is_hbl) {
|
KProcessAddress aslr_space_start, bool is_hbl) {
|
||||||
// Create a resource limit for the process.
|
// Create a resource limit for the process.
|
||||||
const auto physical_memory_size =
|
const auto physical_memory_size =
|
||||||
m_kernel.MemoryManager().GetSize(Kernel::KMemoryManager::Pool::Application);
|
m_kernel.MemoryManager().GetSize(Kernel::KMemoryManager::Pool::Application);
|
||||||
|
@ -1179,7 +1179,7 @@ Result KProcess::LoadFromMetadata(const FileSys::ProgramMetadata& metadata, std:
|
||||||
.name = {},
|
.name = {},
|
||||||
.version = {},
|
.version = {},
|
||||||
.program_id = metadata.GetTitleID(),
|
.program_id = metadata.GetTitleID(),
|
||||||
.code_address = code_address,
|
.code_address = code_address + GetInteger(aslr_space_start),
|
||||||
.code_num_pages = static_cast<s32>(code_size / PageSize),
|
.code_num_pages = static_cast<s32>(code_size / PageSize),
|
||||||
.flags = flag,
|
.flags = flag,
|
||||||
.reslimit = Svc::InvalidHandle,
|
.reslimit = Svc::InvalidHandle,
|
||||||
|
@ -1193,7 +1193,7 @@ Result KProcess::LoadFromMetadata(const FileSys::ProgramMetadata& metadata, std:
|
||||||
|
|
||||||
// Initialize for application process.
|
// Initialize for application process.
|
||||||
R_TRY(this->Initialize(params, metadata.GetKernelCapabilities(), res_limit,
|
R_TRY(this->Initialize(params, metadata.GetKernelCapabilities(), res_limit,
|
||||||
KMemoryManager::Pool::Application));
|
KMemoryManager::Pool::Application, aslr_space_start));
|
||||||
|
|
||||||
// Assign remaining properties.
|
// Assign remaining properties.
|
||||||
m_is_hbl = is_hbl;
|
m_is_hbl = is_hbl;
|
||||||
|
@ -1214,6 +1214,17 @@ void KProcess::LoadModule(CodeSet code_set, KProcessAddress base_addr) {
|
||||||
ReprotectSegment(code_set.CodeSegment(), Svc::MemoryPermission::ReadExecute);
|
ReprotectSegment(code_set.CodeSegment(), Svc::MemoryPermission::ReadExecute);
|
||||||
ReprotectSegment(code_set.RODataSegment(), Svc::MemoryPermission::Read);
|
ReprotectSegment(code_set.RODataSegment(), Svc::MemoryPermission::Read);
|
||||||
ReprotectSegment(code_set.DataSegment(), Svc::MemoryPermission::ReadWrite);
|
ReprotectSegment(code_set.DataSegment(), Svc::MemoryPermission::ReadWrite);
|
||||||
|
|
||||||
|
#ifdef HAS_NCE
|
||||||
|
if (Settings::IsNceEnabled()) {
|
||||||
|
auto& buffer = m_kernel.System().DeviceMemory().buffer;
|
||||||
|
const auto& code = code_set.CodeSegment();
|
||||||
|
const auto& patch = code_set.PatchSegment();
|
||||||
|
buffer.Protect(GetInteger(base_addr + code.addr), code.size, true, true, true);
|
||||||
|
buffer.Protect(GetInteger(base_addr + patch.addr), patch.size, true, true, true);
|
||||||
|
ReprotectSegment(code_set.PatchSegment(), Svc::MemoryPermission::None);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
bool KProcess::InsertWatchpoint(KProcessAddress addr, u64 size, DebugWatchpointType type) {
|
bool KProcess::InsertWatchpoint(KProcessAddress addr, u64 size, DebugWatchpointType type) {
|
||||||
|
|
|
@ -120,6 +120,9 @@ private:
|
||||||
std::atomic<s64> m_num_ipc_messages{};
|
std::atomic<s64> m_num_ipc_messages{};
|
||||||
std::atomic<s64> m_num_ipc_replies{};
|
std::atomic<s64> m_num_ipc_replies{};
|
||||||
std::atomic<s64> m_num_ipc_receives{};
|
std::atomic<s64> m_num_ipc_receives{};
|
||||||
|
#ifdef HAS_NCE
|
||||||
|
std::unordered_map<u64, u64> m_post_handlers{};
|
||||||
|
#endif
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Result StartTermination();
|
Result StartTermination();
|
||||||
|
@ -150,7 +153,8 @@ public:
|
||||||
std::span<const u32> caps, KResourceLimit* res_limit,
|
std::span<const u32> caps, KResourceLimit* res_limit,
|
||||||
KMemoryManager::Pool pool, bool immortal);
|
KMemoryManager::Pool pool, bool immortal);
|
||||||
Result Initialize(const Svc::CreateProcessParameter& params, std::span<const u32> user_caps,
|
Result Initialize(const Svc::CreateProcessParameter& params, std::span<const u32> user_caps,
|
||||||
KResourceLimit* res_limit, KMemoryManager::Pool pool);
|
KResourceLimit* res_limit, KMemoryManager::Pool pool,
|
||||||
|
KProcessAddress aslr_space_start);
|
||||||
void Exit();
|
void Exit();
|
||||||
|
|
||||||
const char* GetName() const {
|
const char* GetName() const {
|
||||||
|
@ -466,6 +470,12 @@ public:
|
||||||
|
|
||||||
static void Switch(KProcess* cur_process, KProcess* next_process);
|
static void Switch(KProcess* cur_process, KProcess* next_process);
|
||||||
|
|
||||||
|
#ifdef HAS_NCE
|
||||||
|
std::unordered_map<u64, u64>& GetPostHandlers() noexcept {
|
||||||
|
return m_post_handlers;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
public:
|
public:
|
||||||
// Attempts to insert a watchpoint into a free slot. Returns false if none are available.
|
// Attempts to insert a watchpoint into a free slot. Returns false if none are available.
|
||||||
bool InsertWatchpoint(KProcessAddress addr, u64 size, DebugWatchpointType type);
|
bool InsertWatchpoint(KProcessAddress addr, u64 size, DebugWatchpointType type);
|
||||||
|
@ -479,7 +489,7 @@ public:
|
||||||
|
|
||||||
public:
|
public:
|
||||||
Result LoadFromMetadata(const FileSys::ProgramMetadata& metadata, std::size_t code_size,
|
Result LoadFromMetadata(const FileSys::ProgramMetadata& metadata, std::size_t code_size,
|
||||||
bool is_hbl);
|
KProcessAddress aslr_space_start, bool is_hbl);
|
||||||
|
|
||||||
void LoadModule(CodeSet code_set, KProcessAddress base_addr);
|
void LoadModule(CodeSet code_set, KProcessAddress base_addr);
|
||||||
|
|
||||||
|
|
|
@ -23,10 +23,11 @@ public:
|
||||||
Result Initialize(Svc::CreateProcessFlag as_type, bool enable_aslr, bool enable_das_merge,
|
Result Initialize(Svc::CreateProcessFlag as_type, bool enable_aslr, bool enable_das_merge,
|
||||||
bool from_back, KMemoryManager::Pool pool, KProcessAddress code_address,
|
bool from_back, KMemoryManager::Pool pool, KProcessAddress code_address,
|
||||||
size_t code_size, KSystemResource* system_resource,
|
size_t code_size, KSystemResource* system_resource,
|
||||||
KResourceLimit* resource_limit, Core::Memory::Memory& memory) {
|
KResourceLimit* resource_limit, Core::Memory::Memory& memory,
|
||||||
R_RETURN(m_page_table.InitializeForProcess(as_type, enable_aslr, enable_das_merge,
|
KProcessAddress aslr_space_start) {
|
||||||
from_back, pool, code_address, code_size,
|
R_RETURN(m_page_table.InitializeForProcess(
|
||||||
system_resource, resource_limit, memory));
|
as_type, enable_aslr, enable_das_merge, from_back, pool, code_address, code_size,
|
||||||
|
system_resource, resource_limit, memory, aslr_space_start));
|
||||||
}
|
}
|
||||||
|
|
||||||
void Finalize() {
|
void Finalize() {
|
||||||
|
|
|
@ -655,6 +655,21 @@ public:
|
||||||
return m_stack_top;
|
return m_stack_top;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
// TODO: This shouldn't be defined in kernel namespace
|
||||||
|
struct NativeExecutionParameters {
|
||||||
|
u64 tpidr_el0{};
|
||||||
|
u64 tpidrro_el0{};
|
||||||
|
void* native_context{};
|
||||||
|
std::atomic<u32> lock{1};
|
||||||
|
bool is_running{};
|
||||||
|
u32 magic{Common::MakeMagic('Y', 'U', 'Z', 'U')};
|
||||||
|
};
|
||||||
|
|
||||||
|
NativeExecutionParameters& GetNativeExecutionParameters() {
|
||||||
|
return m_native_execution_parameters;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
KThread* RemoveWaiterByKey(bool* out_has_waiters, KProcessAddress key,
|
KThread* RemoveWaiterByKey(bool* out_has_waiters, KProcessAddress key,
|
||||||
bool is_kernel_address_key);
|
bool is_kernel_address_key);
|
||||||
|
@ -914,6 +929,7 @@ private:
|
||||||
ThreadWaitReasonForDebugging m_wait_reason_for_debugging{};
|
ThreadWaitReasonForDebugging m_wait_reason_for_debugging{};
|
||||||
uintptr_t m_argument{};
|
uintptr_t m_argument{};
|
||||||
KProcessAddress m_stack_top{};
|
KProcessAddress m_stack_top{};
|
||||||
|
NativeExecutionParameters m_native_execution_parameters{};
|
||||||
|
|
||||||
public:
|
public:
|
||||||
using ConditionVariableThreadTreeType = ConditionVariableThreadTree;
|
using ConditionVariableThreadTreeType = ConditionVariableThreadTree;
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "common/settings.h"
|
||||||
#include "core/arm/dynarmic/arm_dynarmic_32.h"
|
#include "core/arm/dynarmic/arm_dynarmic_32.h"
|
||||||
#include "core/arm/dynarmic/arm_dynarmic_64.h"
|
#include "core/arm/dynarmic/arm_dynarmic_64.h"
|
||||||
|
#ifdef HAS_NCE
|
||||||
|
#include "core/arm/nce/arm_nce.h"
|
||||||
|
#endif
|
||||||
#include "core/core.h"
|
#include "core/core.h"
|
||||||
#include "core/hle/kernel/k_scheduler.h"
|
#include "core/hle/kernel/k_scheduler.h"
|
||||||
#include "core/hle/kernel/kernel.h"
|
#include "core/hle/kernel/kernel.h"
|
||||||
|
@ -14,7 +18,8 @@ PhysicalCore::PhysicalCore(std::size_t core_index, Core::System& system, KSchedu
|
||||||
: m_core_index{core_index}, m_system{system}, m_scheduler{scheduler} {
|
: m_core_index{core_index}, m_system{system}, m_scheduler{scheduler} {
|
||||||
#if defined(ARCHITECTURE_x86_64) || defined(ARCHITECTURE_arm64)
|
#if defined(ARCHITECTURE_x86_64) || defined(ARCHITECTURE_arm64)
|
||||||
// TODO(bunnei): Initialization relies on a core being available. We may later replace this with
|
// TODO(bunnei): Initialization relies on a core being available. We may later replace this with
|
||||||
// a 32-bit instance of Dynarmic. This should be abstracted out to a CPU manager.
|
// an NCE interface or a 32-bit instance of Dynarmic. This should be abstracted out to a CPU
|
||||||
|
// manager.
|
||||||
auto& kernel = system.Kernel();
|
auto& kernel = system.Kernel();
|
||||||
m_arm_interface = std::make_unique<Core::ARM_Dynarmic_64>(
|
m_arm_interface = std::make_unique<Core::ARM_Dynarmic_64>(
|
||||||
system, kernel.IsMulticore(),
|
system, kernel.IsMulticore(),
|
||||||
|
@ -28,6 +33,13 @@ PhysicalCore::PhysicalCore(std::size_t core_index, Core::System& system, KSchedu
|
||||||
PhysicalCore::~PhysicalCore() = default;
|
PhysicalCore::~PhysicalCore() = default;
|
||||||
|
|
||||||
void PhysicalCore::Initialize(bool is_64_bit) {
|
void PhysicalCore::Initialize(bool is_64_bit) {
|
||||||
|
#if defined(HAS_NCE)
|
||||||
|
if (Settings::IsNceEnabled()) {
|
||||||
|
m_arm_interface = std::make_unique<Core::ARM_NCE>(m_system, m_system.Kernel().IsMulticore(),
|
||||||
|
m_core_index);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
#if defined(ARCHITECTURE_x86_64) || defined(ARCHITECTURE_arm64)
|
#if defined(ARCHITECTURE_x86_64) || defined(ARCHITECTURE_arm64)
|
||||||
auto& kernel = m_system.Kernel();
|
auto& kernel = m_system.Kernel();
|
||||||
if (!is_64_bit) {
|
if (!is_64_bit) {
|
||||||
|
|
|
@ -16,7 +16,8 @@ namespace Service::HID {
|
||||||
constexpr std::size_t SHARED_MEMORY_OFFSET = 0x400;
|
constexpr std::size_t SHARED_MEMORY_OFFSET = 0x400;
|
||||||
|
|
||||||
TouchScreen::TouchScreen(Core::HID::HIDCore& hid_core_, u8* raw_shared_memory_)
|
TouchScreen::TouchScreen(Core::HID::HIDCore& hid_core_, u8* raw_shared_memory_)
|
||||||
: ControllerBase{hid_core_} {
|
: ControllerBase{hid_core_}, touchscreen_width(Layout::ScreenUndocked::Width),
|
||||||
|
touchscreen_height(Layout::ScreenUndocked::Height) {
|
||||||
static_assert(SHARED_MEMORY_OFFSET + sizeof(TouchSharedMemory) < shared_memory_size,
|
static_assert(SHARED_MEMORY_OFFSET + sizeof(TouchSharedMemory) < shared_memory_size,
|
||||||
"TouchSharedMemory is bigger than the shared memory");
|
"TouchSharedMemory is bigger than the shared memory");
|
||||||
shared_memory = std::construct_at(
|
shared_memory = std::construct_at(
|
||||||
|
@ -95,8 +96,8 @@ void TouchScreen::OnUpdate(const Core::Timing::CoreTiming& core_timing) {
|
||||||
if (id < active_fingers_count) {
|
if (id < active_fingers_count) {
|
||||||
const auto& [active_x, active_y] = active_fingers[id].position;
|
const auto& [active_x, active_y] = active_fingers[id].position;
|
||||||
touch_entry.position = {
|
touch_entry.position = {
|
||||||
.x = static_cast<u16>(active_x * Layout::ScreenUndocked::Width),
|
.x = static_cast<u16>(active_x * static_cast<float>(touchscreen_width)),
|
||||||
.y = static_cast<u16>(active_y * Layout::ScreenUndocked::Height),
|
.y = static_cast<u16>(active_y * static_cast<float>(touchscreen_height)),
|
||||||
};
|
};
|
||||||
touch_entry.diameter_x = Settings::values.touchscreen.diameter_x;
|
touch_entry.diameter_x = Settings::values.touchscreen.diameter_x;
|
||||||
touch_entry.diameter_y = Settings::values.touchscreen.diameter_y;
|
touch_entry.diameter_y = Settings::values.touchscreen.diameter_y;
|
||||||
|
@ -120,4 +121,9 @@ void TouchScreen::OnUpdate(const Core::Timing::CoreTiming& core_timing) {
|
||||||
shared_memory->touch_screen_lifo.WriteNextEntry(next_state);
|
shared_memory->touch_screen_lifo.WriteNextEntry(next_state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TouchScreen::SetTouchscreenDimensions(u32 width, u32 height) {
|
||||||
|
touchscreen_width = width;
|
||||||
|
touchscreen_height = height;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace Service::HID
|
} // namespace Service::HID
|
||||||
|
|
|
@ -28,6 +28,8 @@ public:
|
||||||
// When the controller is requesting an update for the shared memory
|
// When the controller is requesting an update for the shared memory
|
||||||
void OnUpdate(const Core::Timing::CoreTiming& core_timing) override;
|
void OnUpdate(const Core::Timing::CoreTiming& core_timing) override;
|
||||||
|
|
||||||
|
void SetTouchscreenDimensions(u32 width, u32 height);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static constexpr std::size_t MAX_FINGERS = 16;
|
static constexpr std::size_t MAX_FINGERS = 16;
|
||||||
|
|
||||||
|
@ -53,5 +55,7 @@ private:
|
||||||
Core::HID::EmulatedConsole* console = nullptr;
|
Core::HID::EmulatedConsole* console = nullptr;
|
||||||
|
|
||||||
std::array<Core::HID::TouchFinger, MAX_FINGERS> fingers{};
|
std::array<Core::HID::TouchFinger, MAX_FINGERS> fingers{};
|
||||||
|
u32 touchscreen_width;
|
||||||
|
u32 touchscreen_height;
|
||||||
};
|
};
|
||||||
} // namespace Service::HID
|
} // namespace Service::HID
|
||||||
|
|
|
@ -208,6 +208,7 @@ IHidServer::IHidServer(Core::System& system_, std::shared_ptr<ResourceManager> r
|
||||||
{1001, &IHidServer::GetNpadCommunicationMode, "GetNpadCommunicationMode"},
|
{1001, &IHidServer::GetNpadCommunicationMode, "GetNpadCommunicationMode"},
|
||||||
{1002, &IHidServer::SetTouchScreenConfiguration, "SetTouchScreenConfiguration"},
|
{1002, &IHidServer::SetTouchScreenConfiguration, "SetTouchScreenConfiguration"},
|
||||||
{1003, &IHidServer::IsFirmwareUpdateNeededForNotification, "IsFirmwareUpdateNeededForNotification"},
|
{1003, &IHidServer::IsFirmwareUpdateNeededForNotification, "IsFirmwareUpdateNeededForNotification"},
|
||||||
|
{1004, &IHidServer::SetTouchScreenResolution, "SetTouchScreenResolution"},
|
||||||
{2000, nullptr, "ActivateDigitizer"},
|
{2000, nullptr, "ActivateDigitizer"},
|
||||||
};
|
};
|
||||||
// clang-format on
|
// clang-format on
|
||||||
|
@ -2363,6 +2364,21 @@ void IHidServer::IsFirmwareUpdateNeededForNotification(HLERequestContext& ctx) {
|
||||||
rb.Push(false);
|
rb.Push(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void IHidServer::SetTouchScreenResolution(HLERequestContext& ctx) {
|
||||||
|
IPC::RequestParser rp{ctx};
|
||||||
|
const auto width{rp.Pop<u32>()};
|
||||||
|
const auto height{rp.Pop<u32>()};
|
||||||
|
const auto applet_resource_user_id{rp.Pop<u64>()};
|
||||||
|
|
||||||
|
GetResourceManager()->GetTouchScreen()->SetTouchscreenDimensions(width, height);
|
||||||
|
|
||||||
|
LOG_INFO(Service_HID, "called, width={}, height={}, applet_resource_user_id={}", width, height,
|
||||||
|
applet_resource_user_id);
|
||||||
|
|
||||||
|
IPC::ResponseBuilder rb{ctx, 2};
|
||||||
|
rb.Push(ResultSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
std::shared_ptr<ResourceManager> IHidServer::GetResourceManager() {
|
std::shared_ptr<ResourceManager> IHidServer::GetResourceManager() {
|
||||||
resource_manager->Initialize();
|
resource_manager->Initialize();
|
||||||
return resource_manager;
|
return resource_manager;
|
||||||
|
|
|
@ -141,6 +141,7 @@ private:
|
||||||
void GetNpadCommunicationMode(HLERequestContext& ctx);
|
void GetNpadCommunicationMode(HLERequestContext& ctx);
|
||||||
void SetTouchScreenConfiguration(HLERequestContext& ctx);
|
void SetTouchScreenConfiguration(HLERequestContext& ctx);
|
||||||
void IsFirmwareUpdateNeededForNotification(HLERequestContext& ctx);
|
void IsFirmwareUpdateNeededForNotification(HLERequestContext& ctx);
|
||||||
|
void SetTouchScreenResolution(HLERequestContext& ctx);
|
||||||
|
|
||||||
std::shared_ptr<ResourceManager> resource_manager;
|
std::shared_ptr<ResourceManager> resource_manager;
|
||||||
std::shared_ptr<HidFirmwareSettings> firmware_settings;
|
std::shared_ptr<HidFirmwareSettings> firmware_settings;
|
||||||
|
|
|
@ -34,7 +34,9 @@ Result GetFirmwareVersionImpl(FirmwareVersionFormat& out_firmware, Core::System&
|
||||||
nca = bis_system->GetEntry(FirmwareVersionSystemDataId, FileSys::ContentRecordType::Data);
|
nca = bis_system->GetEntry(FirmwareVersionSystemDataId, FileSys::ContentRecordType::Data);
|
||||||
}
|
}
|
||||||
if (nca) {
|
if (nca) {
|
||||||
romfs = FileSys::ExtractRomFS(nca->GetRomFS());
|
if (auto nca_romfs = nca->GetRomFS(); nca_romfs) {
|
||||||
|
romfs = FileSys::ExtractRomFS(nca_romfs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!romfs) {
|
if (!romfs) {
|
||||||
romfs = FileSys::ExtractRomFS(
|
romfs = FileSys::ExtractRomFS(
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
|
#include "common/settings.h"
|
||||||
#include "core/core.h"
|
#include "core/core.h"
|
||||||
#include "core/file_sys/content_archive.h"
|
#include "core/file_sys/content_archive.h"
|
||||||
#include "core/file_sys/control_metadata.h"
|
#include "core/file_sys/control_metadata.h"
|
||||||
|
@ -14,6 +15,10 @@
|
||||||
#include "core/loader/deconstructed_rom_directory.h"
|
#include "core/loader/deconstructed_rom_directory.h"
|
||||||
#include "core/loader/nso.h"
|
#include "core/loader/nso.h"
|
||||||
|
|
||||||
|
#ifdef HAS_NCE
|
||||||
|
#include "core/arm/nce/patch.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace Loader {
|
namespace Loader {
|
||||||
|
|
||||||
AppLoader_DeconstructedRomDirectory::AppLoader_DeconstructedRomDirectory(FileSys::VirtualFile file_,
|
AppLoader_DeconstructedRomDirectory::AppLoader_DeconstructedRomDirectory(FileSys::VirtualFile file_,
|
||||||
|
@ -124,21 +129,43 @@ AppLoader_DeconstructedRomDirectory::LoadResult AppLoader_DeconstructedRomDirect
|
||||||
}
|
}
|
||||||
metadata.Print();
|
metadata.Print();
|
||||||
|
|
||||||
const auto static_modules = {"rtld", "main", "subsdk0", "subsdk1", "subsdk2",
|
// Enable NCE only for programs with 39-bit address space.
|
||||||
"subsdk3", "subsdk4", "subsdk5", "subsdk6", "subsdk7",
|
const bool is_39bit =
|
||||||
"subsdk8", "subsdk9", "sdk"};
|
metadata.GetAddressSpaceType() == FileSys::ProgramAddressSpaceType::Is39Bit;
|
||||||
|
Settings::SetNceEnabled(is_39bit);
|
||||||
|
|
||||||
|
const std::array static_modules = {"rtld", "main", "subsdk0", "subsdk1", "subsdk2",
|
||||||
|
"subsdk3", "subsdk4", "subsdk5", "subsdk6", "subsdk7",
|
||||||
|
"subsdk8", "subsdk9", "sdk"};
|
||||||
|
|
||||||
|
std::size_t code_size{};
|
||||||
|
|
||||||
|
// Define an nce patch context for each potential module.
|
||||||
|
#ifdef HAS_NCE
|
||||||
|
std::array<Core::NCE::Patcher, 13> module_patchers;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
const auto GetPatcher = [&](size_t i) -> Core::NCE::Patcher* {
|
||||||
|
#ifdef HAS_NCE
|
||||||
|
if (Settings::IsNceEnabled()) {
|
||||||
|
return &module_patchers[i];
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
// Use the NSO module loader to figure out the code layout
|
// Use the NSO module loader to figure out the code layout
|
||||||
std::size_t code_size{};
|
for (size_t i = 0; i < static_modules.size(); i++) {
|
||||||
for (const auto& module : static_modules) {
|
const auto& module = static_modules[i];
|
||||||
const FileSys::VirtualFile module_file{dir->GetFile(module)};
|
const FileSys::VirtualFile module_file{dir->GetFile(module)};
|
||||||
if (!module_file) {
|
if (!module_file) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool should_pass_arguments = std::strcmp(module, "rtld") == 0;
|
const bool should_pass_arguments = std::strcmp(module, "rtld") == 0;
|
||||||
const auto tentative_next_load_addr = AppLoader_NSO::LoadModule(
|
const auto tentative_next_load_addr =
|
||||||
process, system, *module_file, code_size, should_pass_arguments, false);
|
AppLoader_NSO::LoadModule(process, system, *module_file, code_size,
|
||||||
|
should_pass_arguments, false, {}, GetPatcher(i));
|
||||||
if (!tentative_next_load_addr) {
|
if (!tentative_next_load_addr) {
|
||||||
return {ResultStatus::ErrorLoadingNSO, {}};
|
return {ResultStatus::ErrorLoadingNSO, {}};
|
||||||
}
|
}
|
||||||
|
@ -146,8 +173,18 @@ AppLoader_DeconstructedRomDirectory::LoadResult AppLoader_DeconstructedRomDirect
|
||||||
code_size = *tentative_next_load_addr;
|
code_size = *tentative_next_load_addr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enable direct memory mapping in case of NCE.
|
||||||
|
const u64 fastmem_base = [&]() -> size_t {
|
||||||
|
if (Settings::IsNceEnabled()) {
|
||||||
|
auto& buffer = system.DeviceMemory().buffer;
|
||||||
|
buffer.EnableDirectMappedAddress();
|
||||||
|
return reinterpret_cast<u64>(buffer.VirtualBasePointer());
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}();
|
||||||
|
|
||||||
// Setup the process code layout
|
// Setup the process code layout
|
||||||
if (process.LoadFromMetadata(metadata, code_size, is_hbl).IsError()) {
|
if (process.LoadFromMetadata(metadata, code_size, fastmem_base, is_hbl).IsError()) {
|
||||||
return {ResultStatus::ErrorUnableToParseKernelMetadata, {}};
|
return {ResultStatus::ErrorUnableToParseKernelMetadata, {}};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,7 +194,8 @@ AppLoader_DeconstructedRomDirectory::LoadResult AppLoader_DeconstructedRomDirect
|
||||||
VAddr next_load_addr{base_address};
|
VAddr next_load_addr{base_address};
|
||||||
const FileSys::PatchManager pm{metadata.GetTitleID(), system.GetFileSystemController(),
|
const FileSys::PatchManager pm{metadata.GetTitleID(), system.GetFileSystemController(),
|
||||||
system.GetContentProvider()};
|
system.GetContentProvider()};
|
||||||
for (const auto& module : static_modules) {
|
for (size_t i = 0; i < static_modules.size(); i++) {
|
||||||
|
const auto& module = static_modules[i];
|
||||||
const FileSys::VirtualFile module_file{dir->GetFile(module)};
|
const FileSys::VirtualFile module_file{dir->GetFile(module)};
|
||||||
if (!module_file) {
|
if (!module_file) {
|
||||||
continue;
|
continue;
|
||||||
|
@ -165,15 +203,16 @@ AppLoader_DeconstructedRomDirectory::LoadResult AppLoader_DeconstructedRomDirect
|
||||||
|
|
||||||
const VAddr load_addr{next_load_addr};
|
const VAddr load_addr{next_load_addr};
|
||||||
const bool should_pass_arguments = std::strcmp(module, "rtld") == 0;
|
const bool should_pass_arguments = std::strcmp(module, "rtld") == 0;
|
||||||
const auto tentative_next_load_addr = AppLoader_NSO::LoadModule(
|
const auto tentative_next_load_addr =
|
||||||
process, system, *module_file, load_addr, should_pass_arguments, true, pm);
|
AppLoader_NSO::LoadModule(process, system, *module_file, load_addr,
|
||||||
|
should_pass_arguments, true, pm, GetPatcher(i));
|
||||||
if (!tentative_next_load_addr) {
|
if (!tentative_next_load_addr) {
|
||||||
return {ResultStatus::ErrorLoadingNSO, {}};
|
return {ResultStatus::ErrorLoadingNSO, {}};
|
||||||
}
|
}
|
||||||
|
|
||||||
next_load_addr = *tentative_next_load_addr;
|
next_load_addr = *tentative_next_load_addr;
|
||||||
modules.insert_or_assign(load_addr, module);
|
modules.insert_or_assign(load_addr, module);
|
||||||
LOG_DEBUG(Loader, "loaded module {} @ 0x{:X}", module, load_addr);
|
LOG_DEBUG(Loader, "loaded module {} @ {:#X}", module, load_addr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the RomFS by searching for a ".romfs" file in this directory
|
// Find the RomFS by searching for a ".romfs" file in this directory
|
||||||
|
|
|
@ -91,7 +91,8 @@ AppLoader::LoadResult AppLoader_KIP::Load(Kernel::KProcess& process,
|
||||||
|
|
||||||
// Setup the process code layout
|
// Setup the process code layout
|
||||||
if (process
|
if (process
|
||||||
.LoadFromMetadata(FileSys::ProgramMetadata::GetDefault(), program_image.size(), false)
|
.LoadFromMetadata(FileSys::ProgramMetadata::GetDefault(), program_image.size(), 0,
|
||||||
|
false)
|
||||||
.IsError()) {
|
.IsError()) {
|
||||||
return {ResultStatus::ErrorNotInitialized, {}};
|
return {ResultStatus::ErrorNotInitialized, {}};
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,10 @@
|
||||||
#include "core/loader/nso.h"
|
#include "core/loader/nso.h"
|
||||||
#include "core/memory.h"
|
#include "core/memory.h"
|
||||||
|
|
||||||
|
#ifdef HAS_NCE
|
||||||
|
#include "core/arm/nce/patch.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace Loader {
|
namespace Loader {
|
||||||
|
|
||||||
struct NroSegmentHeader {
|
struct NroSegmentHeader {
|
||||||
|
@ -139,7 +143,8 @@ static constexpr u32 PageAlignSize(u32 size) {
|
||||||
return static_cast<u32>((size + Core::Memory::YUZU_PAGEMASK) & ~Core::Memory::YUZU_PAGEMASK);
|
return static_cast<u32>((size + Core::Memory::YUZU_PAGEMASK) & ~Core::Memory::YUZU_PAGEMASK);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool LoadNroImpl(Kernel::KProcess& process, const std::vector<u8>& data) {
|
static bool LoadNroImpl(Core::System& system, Kernel::KProcess& process,
|
||||||
|
const std::vector<u8>& data) {
|
||||||
if (data.size() < sizeof(NroHeader)) {
|
if (data.size() < sizeof(NroHeader)) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
@ -194,14 +199,61 @@ static bool LoadNroImpl(Kernel::KProcess& process, const std::vector<u8>& data)
|
||||||
|
|
||||||
codeset.DataSegment().size += bss_size;
|
codeset.DataSegment().size += bss_size;
|
||||||
program_image.resize(static_cast<u32>(program_image.size()) + bss_size);
|
program_image.resize(static_cast<u32>(program_image.size()) + bss_size);
|
||||||
|
size_t image_size = program_image.size();
|
||||||
|
|
||||||
|
#ifdef HAS_NCE
|
||||||
|
const auto& code = codeset.CodeSegment();
|
||||||
|
|
||||||
|
// NROs always have a 39-bit address space.
|
||||||
|
Settings::SetNceEnabled(true);
|
||||||
|
|
||||||
|
// Create NCE patcher
|
||||||
|
Core::NCE::Patcher patch{};
|
||||||
|
|
||||||
|
if (Settings::IsNceEnabled()) {
|
||||||
|
// Patch SVCs and MRS calls in the guest code
|
||||||
|
patch.PatchText(program_image, code);
|
||||||
|
|
||||||
|
// We only support PostData patching for NROs.
|
||||||
|
ASSERT(patch.GetPatchMode() == Core::NCE::PatchMode::PostData);
|
||||||
|
|
||||||
|
// Update patch section.
|
||||||
|
auto& patch_segment = codeset.PatchSegment();
|
||||||
|
patch_segment.addr = image_size;
|
||||||
|
patch_segment.size = static_cast<u32>(patch.GetSectionSize());
|
||||||
|
|
||||||
|
// Add patch section size to the module size.
|
||||||
|
image_size += patch_segment.size;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Enable direct memory mapping in case of NCE.
|
||||||
|
const u64 fastmem_base = [&]() -> size_t {
|
||||||
|
if (Settings::IsNceEnabled()) {
|
||||||
|
auto& buffer = system.DeviceMemory().buffer;
|
||||||
|
buffer.EnableDirectMappedAddress();
|
||||||
|
return reinterpret_cast<u64>(buffer.VirtualBasePointer());
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}();
|
||||||
|
|
||||||
// Setup the process code layout
|
// Setup the process code layout
|
||||||
if (process
|
if (process
|
||||||
.LoadFromMetadata(FileSys::ProgramMetadata::GetDefault(), program_image.size(), false)
|
.LoadFromMetadata(FileSys::ProgramMetadata::GetDefault(), image_size, fastmem_base,
|
||||||
|
false)
|
||||||
.IsError()) {
|
.IsError()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Relocate code patch and copy to the program_image if running under NCE.
|
||||||
|
// This needs to be after LoadFromMetadata so we can use the process entry point.
|
||||||
|
#ifdef HAS_NCE
|
||||||
|
if (Settings::IsNceEnabled()) {
|
||||||
|
patch.RelocateAndCopy(process.GetEntryPoint(), code, program_image,
|
||||||
|
&process.GetPostHandlers());
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// Load codeset for current process
|
// Load codeset for current process
|
||||||
codeset.memory = std::move(program_image);
|
codeset.memory = std::move(program_image);
|
||||||
process.LoadModule(std::move(codeset), process.GetEntryPoint());
|
process.LoadModule(std::move(codeset), process.GetEntryPoint());
|
||||||
|
@ -209,8 +261,9 @@ static bool LoadNroImpl(Kernel::KProcess& process, const std::vector<u8>& data)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool AppLoader_NRO::LoadNro(Kernel::KProcess& process, const FileSys::VfsFile& nro_file) {
|
bool AppLoader_NRO::LoadNro(Core::System& system, Kernel::KProcess& process,
|
||||||
return LoadNroImpl(process, nro_file.ReadAllBytes());
|
const FileSys::VfsFile& nro_file) {
|
||||||
|
return LoadNroImpl(system, process, nro_file.ReadAllBytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
AppLoader_NRO::LoadResult AppLoader_NRO::Load(Kernel::KProcess& process, Core::System& system) {
|
AppLoader_NRO::LoadResult AppLoader_NRO::Load(Kernel::KProcess& process, Core::System& system) {
|
||||||
|
@ -218,7 +271,7 @@ AppLoader_NRO::LoadResult AppLoader_NRO::Load(Kernel::KProcess& process, Core::S
|
||||||
return {ResultStatus::ErrorAlreadyLoaded, {}};
|
return {ResultStatus::ErrorAlreadyLoaded, {}};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!LoadNro(process, *file)) {
|
if (!LoadNro(system, process, *file)) {
|
||||||
return {ResultStatus::ErrorLoadingNRO, {}};
|
return {ResultStatus::ErrorLoadingNRO, {}};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -54,7 +54,7 @@ public:
|
||||||
bool IsRomFSUpdatable() const override;
|
bool IsRomFSUpdatable() const override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool LoadNro(Kernel::KProcess& process, const FileSys::VfsFile& nro_file);
|
bool LoadNro(Core::System& system, Kernel::KProcess& process, const FileSys::VfsFile& nro_file);
|
||||||
|
|
||||||
std::vector<u8> icon_data;
|
std::vector<u8> icon_data;
|
||||||
std::unique_ptr<FileSys::NACP> nacp;
|
std::unique_ptr<FileSys::NACP> nacp;
|
||||||
|
|
|
@ -20,6 +20,10 @@
|
||||||
#include "core/loader/nso.h"
|
#include "core/loader/nso.h"
|
||||||
#include "core/memory.h"
|
#include "core/memory.h"
|
||||||
|
|
||||||
|
#ifdef HAS_NCE
|
||||||
|
#include "core/arm/nce/patch.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace Loader {
|
namespace Loader {
|
||||||
namespace {
|
namespace {
|
||||||
struct MODHeader {
|
struct MODHeader {
|
||||||
|
@ -72,7 +76,8 @@ FileType AppLoader_NSO::IdentifyType(const FileSys::VirtualFile& in_file) {
|
||||||
std::optional<VAddr> AppLoader_NSO::LoadModule(Kernel::KProcess& process, Core::System& system,
|
std::optional<VAddr> AppLoader_NSO::LoadModule(Kernel::KProcess& process, Core::System& system,
|
||||||
const FileSys::VfsFile& nso_file, VAddr load_base,
|
const FileSys::VfsFile& nso_file, VAddr load_base,
|
||||||
bool should_pass_arguments, bool load_into_process,
|
bool should_pass_arguments, bool load_into_process,
|
||||||
std::optional<FileSys::PatchManager> pm) {
|
std::optional<FileSys::PatchManager> pm,
|
||||||
|
Core::NCE::Patcher* patch) {
|
||||||
if (nso_file.GetSize() < sizeof(NSOHeader)) {
|
if (nso_file.GetSize() < sizeof(NSOHeader)) {
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
@ -86,6 +91,16 @@ std::optional<VAddr> AppLoader_NSO::LoadModule(Kernel::KProcess& process, Core::
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Allocate some space at the beginning if we are patching in PreText mode.
|
||||||
|
const size_t module_start = [&]() -> size_t {
|
||||||
|
#ifdef HAS_NCE
|
||||||
|
if (patch && patch->GetPatchMode() == Core::NCE::PatchMode::PreText) {
|
||||||
|
return patch->GetSectionSize();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return 0;
|
||||||
|
}();
|
||||||
|
|
||||||
// Build program image
|
// Build program image
|
||||||
Kernel::CodeSet codeset;
|
Kernel::CodeSet codeset;
|
||||||
Kernel::PhysicalMemory program_image;
|
Kernel::PhysicalMemory program_image;
|
||||||
|
@ -95,11 +110,12 @@ std::optional<VAddr> AppLoader_NSO::LoadModule(Kernel::KProcess& process, Core::
|
||||||
if (nso_header.IsSegmentCompressed(i)) {
|
if (nso_header.IsSegmentCompressed(i)) {
|
||||||
data = DecompressSegment(data, nso_header.segments[i]);
|
data = DecompressSegment(data, nso_header.segments[i]);
|
||||||
}
|
}
|
||||||
program_image.resize(nso_header.segments[i].location + static_cast<u32>(data.size()));
|
program_image.resize(module_start + nso_header.segments[i].location +
|
||||||
std::memcpy(program_image.data() + nso_header.segments[i].location, data.data(),
|
static_cast<u32>(data.size()));
|
||||||
data.size());
|
std::memcpy(program_image.data() + module_start + nso_header.segments[i].location,
|
||||||
codeset.segments[i].addr = nso_header.segments[i].location;
|
data.data(), data.size());
|
||||||
codeset.segments[i].offset = nso_header.segments[i].location;
|
codeset.segments[i].addr = module_start + nso_header.segments[i].location;
|
||||||
|
codeset.segments[i].offset = module_start + nso_header.segments[i].location;
|
||||||
codeset.segments[i].size = nso_header.segments[i].size;
|
codeset.segments[i].size = nso_header.segments[i].size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,7 +134,7 @@ std::optional<VAddr> AppLoader_NSO::LoadModule(Kernel::KProcess& process, Core::
|
||||||
}
|
}
|
||||||
|
|
||||||
codeset.DataSegment().size += nso_header.segments[2].bss_size;
|
codeset.DataSegment().size += nso_header.segments[2].bss_size;
|
||||||
const u32 image_size{
|
u32 image_size{
|
||||||
PageAlignSize(static_cast<u32>(program_image.size()) + nso_header.segments[2].bss_size)};
|
PageAlignSize(static_cast<u32>(program_image.size()) + nso_header.segments[2].bss_size)};
|
||||||
program_image.resize(image_size);
|
program_image.resize(image_size);
|
||||||
|
|
||||||
|
@ -129,16 +145,45 @@ std::optional<VAddr> AppLoader_NSO::LoadModule(Kernel::KProcess& process, Core::
|
||||||
// Apply patches if necessary
|
// Apply patches if necessary
|
||||||
const auto name = nso_file.GetName();
|
const auto name = nso_file.GetName();
|
||||||
if (pm && (pm->HasNSOPatch(nso_header.build_id, name) || Settings::values.dump_nso)) {
|
if (pm && (pm->HasNSOPatch(nso_header.build_id, name) || Settings::values.dump_nso)) {
|
||||||
std::vector<u8> pi_header(sizeof(NSOHeader) + program_image.size());
|
std::span<u8> patchable_section(program_image.data() + module_start,
|
||||||
|
program_image.size() - module_start);
|
||||||
|
std::vector<u8> pi_header(sizeof(NSOHeader) + patchable_section.size());
|
||||||
std::memcpy(pi_header.data(), &nso_header, sizeof(NSOHeader));
|
std::memcpy(pi_header.data(), &nso_header, sizeof(NSOHeader));
|
||||||
std::memcpy(pi_header.data() + sizeof(NSOHeader), program_image.data(),
|
std::memcpy(pi_header.data() + sizeof(NSOHeader), patchable_section.data(),
|
||||||
program_image.size());
|
patchable_section.size());
|
||||||
|
|
||||||
pi_header = pm->PatchNSO(pi_header, name);
|
pi_header = pm->PatchNSO(pi_header, name);
|
||||||
|
|
||||||
std::copy(pi_header.begin() + sizeof(NSOHeader), pi_header.end(), program_image.data());
|
std::copy(pi_header.begin() + sizeof(NSOHeader), pi_header.end(), patchable_section.data());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef HAS_NCE
|
||||||
|
// If we are computing the process code layout and using nce backend, patch.
|
||||||
|
const auto& code = codeset.CodeSegment();
|
||||||
|
if (patch && patch->GetPatchMode() == Core::NCE::PatchMode::None) {
|
||||||
|
// Patch SVCs and MRS calls in the guest code
|
||||||
|
patch->PatchText(program_image, code);
|
||||||
|
|
||||||
|
// Add patch section size to the module size.
|
||||||
|
image_size += static_cast<u32>(patch->GetSectionSize());
|
||||||
|
} else if (patch) {
|
||||||
|
// Relocate code patch and copy to the program_image.
|
||||||
|
patch->RelocateAndCopy(load_base, code, program_image, &process.GetPostHandlers());
|
||||||
|
|
||||||
|
// Update patch section.
|
||||||
|
auto& patch_segment = codeset.PatchSegment();
|
||||||
|
patch_segment.addr =
|
||||||
|
patch->GetPatchMode() == Core::NCE::PatchMode::PreText ? 0 : image_size;
|
||||||
|
patch_segment.size = static_cast<u32>(patch->GetSectionSize());
|
||||||
|
|
||||||
|
// Add patch section size to the module size. In PreText mode image_size
|
||||||
|
// already contains the patch segment as part of module_start.
|
||||||
|
if (patch->GetPatchMode() == Core::NCE::PatchMode::PostData) {
|
||||||
|
image_size += patch_segment.size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// If we aren't actually loading (i.e. just computing the process code layout), we are done
|
// If we aren't actually loading (i.e. just computing the process code layout), we are done
|
||||||
if (!load_into_process) {
|
if (!load_into_process) {
|
||||||
return load_base + image_size;
|
return load_base + image_size;
|
||||||
|
|
|
@ -15,6 +15,10 @@ namespace Core {
|
||||||
class System;
|
class System;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace Core::NCE {
|
||||||
|
class Patcher;
|
||||||
|
}
|
||||||
|
|
||||||
namespace Kernel {
|
namespace Kernel {
|
||||||
class KProcess;
|
class KProcess;
|
||||||
}
|
}
|
||||||
|
@ -88,7 +92,8 @@ public:
|
||||||
static std::optional<VAddr> LoadModule(Kernel::KProcess& process, Core::System& system,
|
static std::optional<VAddr> LoadModule(Kernel::KProcess& process, Core::System& system,
|
||||||
const FileSys::VfsFile& nso_file, VAddr load_base,
|
const FileSys::VfsFile& nso_file, VAddr load_base,
|
||||||
bool should_pass_arguments, bool load_into_process,
|
bool should_pass_arguments, bool load_into_process,
|
||||||
std::optional<FileSys::PatchManager> pm = {});
|
std::optional<FileSys::PatchManager> pm = {},
|
||||||
|
Core::NCE::Patcher* patch = nullptr);
|
||||||
|
|
||||||
LoadResult Load(Kernel::KProcess& process, Core::System& system) override;
|
LoadResult Load(Kernel::KProcess& process, Core::System& system) override;
|
||||||
|
|
||||||
|
|
|
@ -53,7 +53,7 @@ struct Memory::Impl {
|
||||||
}
|
}
|
||||||
|
|
||||||
void MapMemoryRegion(Common::PageTable& page_table, Common::ProcessAddress base, u64 size,
|
void MapMemoryRegion(Common::PageTable& page_table, Common::ProcessAddress base, u64 size,
|
||||||
Common::PhysicalAddress target) {
|
Common::PhysicalAddress target, Common::MemoryPermission perms) {
|
||||||
ASSERT_MSG((size & YUZU_PAGEMASK) == 0, "non-page aligned size: {:016X}", size);
|
ASSERT_MSG((size & YUZU_PAGEMASK) == 0, "non-page aligned size: {:016X}", size);
|
||||||
ASSERT_MSG((base & YUZU_PAGEMASK) == 0, "non-page aligned base: {:016X}", GetInteger(base));
|
ASSERT_MSG((base & YUZU_PAGEMASK) == 0, "non-page aligned base: {:016X}", GetInteger(base));
|
||||||
ASSERT_MSG(target >= DramMemoryMap::Base, "Out of bounds target: {:016X}",
|
ASSERT_MSG(target >= DramMemoryMap::Base, "Out of bounds target: {:016X}",
|
||||||
|
@ -63,7 +63,7 @@ struct Memory::Impl {
|
||||||
|
|
||||||
if (Settings::IsFastmemEnabled()) {
|
if (Settings::IsFastmemEnabled()) {
|
||||||
system.DeviceMemory().buffer.Map(GetInteger(base),
|
system.DeviceMemory().buffer.Map(GetInteger(base),
|
||||||
GetInteger(target) - DramMemoryMap::Base, size);
|
GetInteger(target) - DramMemoryMap::Base, size, perms);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,6 +78,51 @@ struct Memory::Impl {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ProtectRegion(Common::PageTable& page_table, VAddr vaddr, u64 size,
|
||||||
|
Common::MemoryPermission perms) {
|
||||||
|
ASSERT_MSG((size & YUZU_PAGEMASK) == 0, "non-page aligned size: {:016X}", size);
|
||||||
|
ASSERT_MSG((vaddr & YUZU_PAGEMASK) == 0, "non-page aligned base: {:016X}", vaddr);
|
||||||
|
|
||||||
|
if (!Settings::IsFastmemEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool is_r = True(perms & Common::MemoryPermission::Read);
|
||||||
|
const bool is_w = True(perms & Common::MemoryPermission::Write);
|
||||||
|
const bool is_x =
|
||||||
|
True(perms & Common::MemoryPermission::Execute) && Settings::IsNceEnabled();
|
||||||
|
|
||||||
|
if (!current_page_table) {
|
||||||
|
system.DeviceMemory().buffer.Protect(vaddr, size, is_r, is_w, is_x);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
u64 protect_bytes{};
|
||||||
|
u64 protect_begin{};
|
||||||
|
for (u64 addr = vaddr; addr < vaddr + size; addr += YUZU_PAGESIZE) {
|
||||||
|
const Common::PageType page_type{
|
||||||
|
current_page_table->pointers[addr >> YUZU_PAGEBITS].Type()};
|
||||||
|
switch (page_type) {
|
||||||
|
case Common::PageType::RasterizerCachedMemory:
|
||||||
|
if (protect_bytes > 0) {
|
||||||
|
system.DeviceMemory().buffer.Protect(protect_begin, protect_bytes, is_r, is_w,
|
||||||
|
is_x);
|
||||||
|
protect_bytes = 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (protect_bytes == 0) {
|
||||||
|
protect_begin = addr;
|
||||||
|
}
|
||||||
|
protect_bytes += YUZU_PAGESIZE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (protect_bytes > 0) {
|
||||||
|
system.DeviceMemory().buffer.Protect(protect_begin, protect_bytes, is_r, is_w, is_x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[[nodiscard]] u8* GetPointerFromRasterizerCachedMemory(u64 vaddr) const {
|
[[nodiscard]] u8* GetPointerFromRasterizerCachedMemory(u64 vaddr) const {
|
||||||
const Common::PhysicalAddress paddr{
|
const Common::PhysicalAddress paddr{
|
||||||
current_page_table->backing_addr[vaddr >> YUZU_PAGEBITS]};
|
current_page_table->backing_addr[vaddr >> YUZU_PAGEBITS]};
|
||||||
|
@ -831,14 +876,19 @@ void Memory::SetCurrentPageTable(Kernel::KProcess& process, u32 core_id) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void Memory::MapMemoryRegion(Common::PageTable& page_table, Common::ProcessAddress base, u64 size,
|
void Memory::MapMemoryRegion(Common::PageTable& page_table, Common::ProcessAddress base, u64 size,
|
||||||
Common::PhysicalAddress target) {
|
Common::PhysicalAddress target, Common::MemoryPermission perms) {
|
||||||
impl->MapMemoryRegion(page_table, base, size, target);
|
impl->MapMemoryRegion(page_table, base, size, target, perms);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Memory::UnmapRegion(Common::PageTable& page_table, Common::ProcessAddress base, u64 size) {
|
void Memory::UnmapRegion(Common::PageTable& page_table, Common::ProcessAddress base, u64 size) {
|
||||||
impl->UnmapRegion(page_table, base, size);
|
impl->UnmapRegion(page_table, base, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Memory::ProtectRegion(Common::PageTable& page_table, Common::ProcessAddress vaddr, u64 size,
|
||||||
|
Common::MemoryPermission perms) {
|
||||||
|
impl->ProtectRegion(page_table, GetInteger(vaddr), size, perms);
|
||||||
|
}
|
||||||
|
|
||||||
bool Memory::IsValidVirtualAddress(const Common::ProcessAddress vaddr) const {
|
bool Memory::IsValidVirtualAddress(const Common::ProcessAddress vaddr) const {
|
||||||
const Kernel::KProcess& process = *system.ApplicationProcess();
|
const Kernel::KProcess& process = *system.ApplicationProcess();
|
||||||
const auto& page_table = process.GetPageTable().GetImpl();
|
const auto& page_table = process.GetPageTable().GetImpl();
|
||||||
|
@ -1001,4 +1051,17 @@ void Memory::FlushRegion(Common::ProcessAddress dest_addr, size_t size) {
|
||||||
impl->FlushRegion(dest_addr, size);
|
impl->FlushRegion(dest_addr, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Memory::InvalidateNCE(Common::ProcessAddress vaddr, size_t size) {
|
||||||
|
bool mapped = true;
|
||||||
|
u8* const ptr = impl->GetPointerImpl(
|
||||||
|
GetInteger(vaddr),
|
||||||
|
[&] {
|
||||||
|
LOG_ERROR(HW_Memory, "Unmapped InvalidateNCE for {} bytes @ {:#x}", size,
|
||||||
|
GetInteger(vaddr));
|
||||||
|
mapped = false;
|
||||||
|
},
|
||||||
|
[&] { impl->system.GPU().InvalidateRegion(GetInteger(vaddr), size); });
|
||||||
|
return mapped && ptr != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace Core::Memory
|
} // namespace Core::Memory
|
||||||
|
|
|
@ -15,8 +15,9 @@
|
||||||
#include "core/hle/result.h"
|
#include "core/hle/result.h"
|
||||||
|
|
||||||
namespace Common {
|
namespace Common {
|
||||||
|
enum class MemoryPermission : u32;
|
||||||
struct PageTable;
|
struct PageTable;
|
||||||
}
|
} // namespace Common
|
||||||
|
|
||||||
namespace Core {
|
namespace Core {
|
||||||
class System;
|
class System;
|
||||||
|
@ -82,9 +83,10 @@ public:
|
||||||
* @param size The amount of bytes to map. Must be page-aligned.
|
* @param size The amount of bytes to map. Must be page-aligned.
|
||||||
* @param target Buffer with the memory backing the mapping. Must be of length at least
|
* @param target Buffer with the memory backing the mapping. Must be of length at least
|
||||||
* `size`.
|
* `size`.
|
||||||
|
* @param perms The permissions to map the memory with.
|
||||||
*/
|
*/
|
||||||
void MapMemoryRegion(Common::PageTable& page_table, Common::ProcessAddress base, u64 size,
|
void MapMemoryRegion(Common::PageTable& page_table, Common::ProcessAddress base, u64 size,
|
||||||
Common::PhysicalAddress target);
|
Common::PhysicalAddress target, Common::MemoryPermission perms);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unmaps a region of the emulated process address space.
|
* Unmaps a region of the emulated process address space.
|
||||||
|
@ -95,6 +97,17 @@ public:
|
||||||
*/
|
*/
|
||||||
void UnmapRegion(Common::PageTable& page_table, Common::ProcessAddress base, u64 size);
|
void UnmapRegion(Common::PageTable& page_table, Common::ProcessAddress base, u64 size);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protects a region of the emulated process address space with the new permissions.
|
||||||
|
*
|
||||||
|
* @param page_table The page table of the emulated process.
|
||||||
|
* @param base The start address to re-protect. Must be page-aligned.
|
||||||
|
* @param size The amount of bytes to protect. Must be page-aligned.
|
||||||
|
* @param perms The permissions the address range is mapped.
|
||||||
|
*/
|
||||||
|
void ProtectRegion(Common::PageTable& page_table, Common::ProcessAddress base, u64 size,
|
||||||
|
Common::MemoryPermission perms);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether or not the supplied address is a valid virtual
|
* Checks whether or not the supplied address is a valid virtual
|
||||||
* address for the current process.
|
* address for the current process.
|
||||||
|
@ -472,6 +485,7 @@ public:
|
||||||
|
|
||||||
void SetGPUDirtyManagers(std::span<Core::GPUDirtyMemoryManager> managers);
|
void SetGPUDirtyManagers(std::span<Core::GPUDirtyMemoryManager> managers);
|
||||||
void InvalidateRegion(Common::ProcessAddress dest_addr, size_t size);
|
void InvalidateRegion(Common::ProcessAddress dest_addr, size_t size);
|
||||||
|
bool InvalidateNCE(Common::ProcessAddress vaddr, size_t size);
|
||||||
void FlushRegion(Common::ProcessAddress dest_addr, size_t size);
|
void FlushRegion(Common::ProcessAddress dest_addr, size_t size);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
|
@ -924,12 +924,14 @@ std::string Config::AdjustOutputString(const std::string& string) {
|
||||||
|
|
||||||
// Windows requires that two forward slashes are used at the start of a path for unmapped
|
// Windows requires that two forward slashes are used at the start of a path for unmapped
|
||||||
// network drives so we have to watch for that here
|
// network drives so we have to watch for that here
|
||||||
|
#ifndef ANDROID
|
||||||
if (string.substr(0, 2) == "//") {
|
if (string.substr(0, 2) == "//") {
|
||||||
boost::replace_all(adjusted_string, "//", "/");
|
boost::replace_all(adjusted_string, "//", "/");
|
||||||
adjusted_string.insert(0, "/");
|
adjusted_string.insert(0, "/");
|
||||||
} else {
|
} else {
|
||||||
boost::replace_all(adjusted_string, "//", "/");
|
boost::replace_all(adjusted_string, "//", "/");
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// Needed for backwards compatibility with QSettings deserialization
|
// Needed for backwards compatibility with QSettings deserialization
|
||||||
for (const auto& special_character : special_characters) {
|
for (const auto& special_character : special_characters) {
|
||||||
|
|
|
@ -11,6 +11,7 @@ using namespace Common::Literals;
|
||||||
|
|
||||||
static constexpr size_t VIRTUAL_SIZE = 1ULL << 39;
|
static constexpr size_t VIRTUAL_SIZE = 1ULL << 39;
|
||||||
static constexpr size_t BACKING_SIZE = 4_GiB;
|
static constexpr size_t BACKING_SIZE = 4_GiB;
|
||||||
|
static constexpr auto PERMS = Common::MemoryPermission::ReadWrite;
|
||||||
|
|
||||||
TEST_CASE("HostMemory: Initialize and deinitialize", "[common]") {
|
TEST_CASE("HostMemory: Initialize and deinitialize", "[common]") {
|
||||||
{ HostMemory mem(BACKING_SIZE, VIRTUAL_SIZE); }
|
{ HostMemory mem(BACKING_SIZE, VIRTUAL_SIZE); }
|
||||||
|
@ -19,7 +20,7 @@ TEST_CASE("HostMemory: Initialize and deinitialize", "[common]") {
|
||||||
|
|
||||||
TEST_CASE("HostMemory: Simple map", "[common]") {
|
TEST_CASE("HostMemory: Simple map", "[common]") {
|
||||||
HostMemory mem(BACKING_SIZE, VIRTUAL_SIZE);
|
HostMemory mem(BACKING_SIZE, VIRTUAL_SIZE);
|
||||||
mem.Map(0x5000, 0x8000, 0x1000);
|
mem.Map(0x5000, 0x8000, 0x1000, PERMS);
|
||||||
|
|
||||||
volatile u8* const data = mem.VirtualBasePointer() + 0x5000;
|
volatile u8* const data = mem.VirtualBasePointer() + 0x5000;
|
||||||
data[0] = 50;
|
data[0] = 50;
|
||||||
|
@ -28,8 +29,8 @@ TEST_CASE("HostMemory: Simple map", "[common]") {
|
||||||
|
|
||||||
TEST_CASE("HostMemory: Simple mirror map", "[common]") {
|
TEST_CASE("HostMemory: Simple mirror map", "[common]") {
|
||||||
HostMemory mem(BACKING_SIZE, VIRTUAL_SIZE);
|
HostMemory mem(BACKING_SIZE, VIRTUAL_SIZE);
|
||||||
mem.Map(0x5000, 0x3000, 0x2000);
|
mem.Map(0x5000, 0x3000, 0x2000, PERMS);
|
||||||
mem.Map(0x8000, 0x4000, 0x1000);
|
mem.Map(0x8000, 0x4000, 0x1000, PERMS);
|
||||||
|
|
||||||
volatile u8* const mirror_a = mem.VirtualBasePointer() + 0x5000;
|
volatile u8* const mirror_a = mem.VirtualBasePointer() + 0x5000;
|
||||||
volatile u8* const mirror_b = mem.VirtualBasePointer() + 0x8000;
|
volatile u8* const mirror_b = mem.VirtualBasePointer() + 0x8000;
|
||||||
|
@ -39,7 +40,7 @@ TEST_CASE("HostMemory: Simple mirror map", "[common]") {
|
||||||
|
|
||||||
TEST_CASE("HostMemory: Simple unmap", "[common]") {
|
TEST_CASE("HostMemory: Simple unmap", "[common]") {
|
||||||
HostMemory mem(BACKING_SIZE, VIRTUAL_SIZE);
|
HostMemory mem(BACKING_SIZE, VIRTUAL_SIZE);
|
||||||
mem.Map(0x5000, 0x3000, 0x2000);
|
mem.Map(0x5000, 0x3000, 0x2000, PERMS);
|
||||||
|
|
||||||
volatile u8* const data = mem.VirtualBasePointer() + 0x5000;
|
volatile u8* const data = mem.VirtualBasePointer() + 0x5000;
|
||||||
data[75] = 50;
|
data[75] = 50;
|
||||||
|
@ -50,7 +51,7 @@ TEST_CASE("HostMemory: Simple unmap", "[common]") {
|
||||||
|
|
||||||
TEST_CASE("HostMemory: Simple unmap and remap", "[common]") {
|
TEST_CASE("HostMemory: Simple unmap and remap", "[common]") {
|
||||||
HostMemory mem(BACKING_SIZE, VIRTUAL_SIZE);
|
HostMemory mem(BACKING_SIZE, VIRTUAL_SIZE);
|
||||||
mem.Map(0x5000, 0x3000, 0x2000);
|
mem.Map(0x5000, 0x3000, 0x2000, PERMS);
|
||||||
|
|
||||||
volatile u8* const data = mem.VirtualBasePointer() + 0x5000;
|
volatile u8* const data = mem.VirtualBasePointer() + 0x5000;
|
||||||
data[0] = 50;
|
data[0] = 50;
|
||||||
|
@ -58,79 +59,79 @@ TEST_CASE("HostMemory: Simple unmap and remap", "[common]") {
|
||||||
|
|
||||||
mem.Unmap(0x5000, 0x2000);
|
mem.Unmap(0x5000, 0x2000);
|
||||||
|
|
||||||
mem.Map(0x5000, 0x3000, 0x2000);
|
mem.Map(0x5000, 0x3000, 0x2000, PERMS);
|
||||||
REQUIRE(data[0] == 50);
|
REQUIRE(data[0] == 50);
|
||||||
|
|
||||||
mem.Map(0x7000, 0x2000, 0x5000);
|
mem.Map(0x7000, 0x2000, 0x5000, PERMS);
|
||||||
REQUIRE(data[0x3000] == 50);
|
REQUIRE(data[0x3000] == 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("HostMemory: Nieche allocation", "[common]") {
|
TEST_CASE("HostMemory: Nieche allocation", "[common]") {
|
||||||
HostMemory mem(BACKING_SIZE, VIRTUAL_SIZE);
|
HostMemory mem(BACKING_SIZE, VIRTUAL_SIZE);
|
||||||
mem.Map(0x0000, 0, 0x20000);
|
mem.Map(0x0000, 0, 0x20000, PERMS);
|
||||||
mem.Unmap(0x0000, 0x4000);
|
mem.Unmap(0x0000, 0x4000);
|
||||||
mem.Map(0x1000, 0, 0x2000);
|
mem.Map(0x1000, 0, 0x2000, PERMS);
|
||||||
mem.Map(0x3000, 0, 0x1000);
|
mem.Map(0x3000, 0, 0x1000, PERMS);
|
||||||
mem.Map(0, 0, 0x1000);
|
mem.Map(0, 0, 0x1000, PERMS);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("HostMemory: Full unmap", "[common]") {
|
TEST_CASE("HostMemory: Full unmap", "[common]") {
|
||||||
HostMemory mem(BACKING_SIZE, VIRTUAL_SIZE);
|
HostMemory mem(BACKING_SIZE, VIRTUAL_SIZE);
|
||||||
mem.Map(0x8000, 0, 0x4000);
|
mem.Map(0x8000, 0, 0x4000, PERMS);
|
||||||
mem.Unmap(0x8000, 0x4000);
|
mem.Unmap(0x8000, 0x4000);
|
||||||
mem.Map(0x6000, 0, 0x16000);
|
mem.Map(0x6000, 0, 0x16000, PERMS);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("HostMemory: Right out of bounds unmap", "[common]") {
|
TEST_CASE("HostMemory: Right out of bounds unmap", "[common]") {
|
||||||
HostMemory mem(BACKING_SIZE, VIRTUAL_SIZE);
|
HostMemory mem(BACKING_SIZE, VIRTUAL_SIZE);
|
||||||
mem.Map(0x0000, 0, 0x4000);
|
mem.Map(0x0000, 0, 0x4000, PERMS);
|
||||||
mem.Unmap(0x2000, 0x4000);
|
mem.Unmap(0x2000, 0x4000);
|
||||||
mem.Map(0x2000, 0x80000, 0x4000);
|
mem.Map(0x2000, 0x80000, 0x4000, PERMS);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("HostMemory: Left out of bounds unmap", "[common]") {
|
TEST_CASE("HostMemory: Left out of bounds unmap", "[common]") {
|
||||||
HostMemory mem(BACKING_SIZE, VIRTUAL_SIZE);
|
HostMemory mem(BACKING_SIZE, VIRTUAL_SIZE);
|
||||||
mem.Map(0x8000, 0, 0x4000);
|
mem.Map(0x8000, 0, 0x4000, PERMS);
|
||||||
mem.Unmap(0x6000, 0x4000);
|
mem.Unmap(0x6000, 0x4000);
|
||||||
mem.Map(0x8000, 0, 0x2000);
|
mem.Map(0x8000, 0, 0x2000, PERMS);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("HostMemory: Multiple placeholder unmap", "[common]") {
|
TEST_CASE("HostMemory: Multiple placeholder unmap", "[common]") {
|
||||||
HostMemory mem(BACKING_SIZE, VIRTUAL_SIZE);
|
HostMemory mem(BACKING_SIZE, VIRTUAL_SIZE);
|
||||||
mem.Map(0x0000, 0, 0x4000);
|
mem.Map(0x0000, 0, 0x4000, PERMS);
|
||||||
mem.Map(0x4000, 0, 0x1b000);
|
mem.Map(0x4000, 0, 0x1b000, PERMS);
|
||||||
mem.Unmap(0x3000, 0x1c000);
|
mem.Unmap(0x3000, 0x1c000);
|
||||||
mem.Map(0x3000, 0, 0x20000);
|
mem.Map(0x3000, 0, 0x20000, PERMS);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("HostMemory: Unmap between placeholders", "[common]") {
|
TEST_CASE("HostMemory: Unmap between placeholders", "[common]") {
|
||||||
HostMemory mem(BACKING_SIZE, VIRTUAL_SIZE);
|
HostMemory mem(BACKING_SIZE, VIRTUAL_SIZE);
|
||||||
mem.Map(0x0000, 0, 0x4000);
|
mem.Map(0x0000, 0, 0x4000, PERMS);
|
||||||
mem.Map(0x4000, 0, 0x4000);
|
mem.Map(0x4000, 0, 0x4000, PERMS);
|
||||||
mem.Unmap(0x2000, 0x4000);
|
mem.Unmap(0x2000, 0x4000);
|
||||||
mem.Map(0x2000, 0, 0x4000);
|
mem.Map(0x2000, 0, 0x4000, PERMS);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("HostMemory: Unmap to origin", "[common]") {
|
TEST_CASE("HostMemory: Unmap to origin", "[common]") {
|
||||||
HostMemory mem(BACKING_SIZE, VIRTUAL_SIZE);
|
HostMemory mem(BACKING_SIZE, VIRTUAL_SIZE);
|
||||||
mem.Map(0x4000, 0, 0x4000);
|
mem.Map(0x4000, 0, 0x4000, PERMS);
|
||||||
mem.Map(0x8000, 0, 0x4000);
|
mem.Map(0x8000, 0, 0x4000, PERMS);
|
||||||
mem.Unmap(0x4000, 0x4000);
|
mem.Unmap(0x4000, 0x4000);
|
||||||
mem.Map(0, 0, 0x4000);
|
mem.Map(0, 0, 0x4000, PERMS);
|
||||||
mem.Map(0x4000, 0, 0x4000);
|
mem.Map(0x4000, 0, 0x4000, PERMS);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("HostMemory: Unmap to right", "[common]") {
|
TEST_CASE("HostMemory: Unmap to right", "[common]") {
|
||||||
HostMemory mem(BACKING_SIZE, VIRTUAL_SIZE);
|
HostMemory mem(BACKING_SIZE, VIRTUAL_SIZE);
|
||||||
mem.Map(0x4000, 0, 0x4000);
|
mem.Map(0x4000, 0, 0x4000, PERMS);
|
||||||
mem.Map(0x8000, 0, 0x4000);
|
mem.Map(0x8000, 0, 0x4000, PERMS);
|
||||||
mem.Unmap(0x8000, 0x4000);
|
mem.Unmap(0x8000, 0x4000);
|
||||||
mem.Map(0x8000, 0, 0x4000);
|
mem.Map(0x8000, 0, 0x4000, PERMS);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("HostMemory: Partial right unmap check bindings", "[common]") {
|
TEST_CASE("HostMemory: Partial right unmap check bindings", "[common]") {
|
||||||
HostMemory mem(BACKING_SIZE, VIRTUAL_SIZE);
|
HostMemory mem(BACKING_SIZE, VIRTUAL_SIZE);
|
||||||
mem.Map(0x4000, 0x10000, 0x4000);
|
mem.Map(0x4000, 0x10000, 0x4000, PERMS);
|
||||||
|
|
||||||
volatile u8* const ptr = mem.VirtualBasePointer() + 0x4000;
|
volatile u8* const ptr = mem.VirtualBasePointer() + 0x4000;
|
||||||
ptr[0x1000] = 17;
|
ptr[0x1000] = 17;
|
||||||
|
@ -142,7 +143,7 @@ TEST_CASE("HostMemory: Partial right unmap check bindings", "[common]") {
|
||||||
|
|
||||||
TEST_CASE("HostMemory: Partial left unmap check bindings", "[common]") {
|
TEST_CASE("HostMemory: Partial left unmap check bindings", "[common]") {
|
||||||
HostMemory mem(BACKING_SIZE, VIRTUAL_SIZE);
|
HostMemory mem(BACKING_SIZE, VIRTUAL_SIZE);
|
||||||
mem.Map(0x4000, 0x10000, 0x4000);
|
mem.Map(0x4000, 0x10000, 0x4000, PERMS);
|
||||||
|
|
||||||
volatile u8* const ptr = mem.VirtualBasePointer() + 0x4000;
|
volatile u8* const ptr = mem.VirtualBasePointer() + 0x4000;
|
||||||
ptr[0x3000] = 19;
|
ptr[0x3000] = 19;
|
||||||
|
@ -156,7 +157,7 @@ TEST_CASE("HostMemory: Partial left unmap check bindings", "[common]") {
|
||||||
|
|
||||||
TEST_CASE("HostMemory: Partial middle unmap check bindings", "[common]") {
|
TEST_CASE("HostMemory: Partial middle unmap check bindings", "[common]") {
|
||||||
HostMemory mem(BACKING_SIZE, VIRTUAL_SIZE);
|
HostMemory mem(BACKING_SIZE, VIRTUAL_SIZE);
|
||||||
mem.Map(0x4000, 0x10000, 0x4000);
|
mem.Map(0x4000, 0x10000, 0x4000, PERMS);
|
||||||
|
|
||||||
volatile u8* const ptr = mem.VirtualBasePointer() + 0x4000;
|
volatile u8* const ptr = mem.VirtualBasePointer() + 0x4000;
|
||||||
ptr[0x0000] = 19;
|
ptr[0x0000] = 19;
|
||||||
|
@ -170,8 +171,8 @@ TEST_CASE("HostMemory: Partial middle unmap check bindings", "[common]") {
|
||||||
|
|
||||||
TEST_CASE("HostMemory: Partial sparse middle unmap and check bindings", "[common]") {
|
TEST_CASE("HostMemory: Partial sparse middle unmap and check bindings", "[common]") {
|
||||||
HostMemory mem(BACKING_SIZE, VIRTUAL_SIZE);
|
HostMemory mem(BACKING_SIZE, VIRTUAL_SIZE);
|
||||||
mem.Map(0x4000, 0x10000, 0x2000);
|
mem.Map(0x4000, 0x10000, 0x2000, PERMS);
|
||||||
mem.Map(0x6000, 0x20000, 0x2000);
|
mem.Map(0x6000, 0x20000, 0x2000, PERMS);
|
||||||
|
|
||||||
volatile u8* const ptr = mem.VirtualBasePointer() + 0x4000;
|
volatile u8* const ptr = mem.VirtualBasePointer() + 0x4000;
|
||||||
ptr[0x0000] = 19;
|
ptr[0x0000] = 19;
|
||||||
|
|
|
@ -58,7 +58,7 @@ private:
|
||||||
void TrackPage(u64 page, u64 offset, u64 size) noexcept {
|
void TrackPage(u64 page, u64 offset, u64 size) noexcept {
|
||||||
const size_t offset_in_page = offset % PAGE_BYTES;
|
const size_t offset_in_page = offset % PAGE_BYTES;
|
||||||
const size_t first_bit = offset_in_page >> BYTES_PER_BIT_SHIFT;
|
const size_t first_bit = offset_in_page >> BYTES_PER_BIT_SHIFT;
|
||||||
const size_t num_bits = std::min(size, PAGE_BYTES) >> BYTES_PER_BIT_SHIFT;
|
const size_t num_bits = std::min<size_t>(size, PAGE_BYTES) >> BYTES_PER_BIT_SHIFT;
|
||||||
const size_t mask = ~u64{0} >> (64 - num_bits);
|
const size_t mask = ~u64{0} >> (64 - num_bits);
|
||||||
pages[page] |= (~u64{0} & mask) << first_bit;
|
pages[page] |= (~u64{0} & mask) << first_bit;
|
||||||
}
|
}
|
||||||
|
@ -66,7 +66,7 @@ private:
|
||||||
bool IsPageUsed(u64 page, u64 offset, u64 size) const noexcept {
|
bool IsPageUsed(u64 page, u64 offset, u64 size) const noexcept {
|
||||||
const size_t offset_in_page = offset % PAGE_BYTES;
|
const size_t offset_in_page = offset % PAGE_BYTES;
|
||||||
const size_t first_bit = offset_in_page >> BYTES_PER_BIT_SHIFT;
|
const size_t first_bit = offset_in_page >> BYTES_PER_BIT_SHIFT;
|
||||||
const size_t num_bits = std::min(size, PAGE_BYTES) >> BYTES_PER_BIT_SHIFT;
|
const size_t num_bits = std::min<size_t>(size, PAGE_BYTES) >> BYTES_PER_BIT_SHIFT;
|
||||||
const size_t mask = ~u64{0} >> (64 - num_bits);
|
const size_t mask = ~u64{0} >> (64 - num_bits);
|
||||||
const size_t mask2 = (~u64{0} & mask) << first_bit;
|
const size_t mask2 = (~u64{0} & mask) << first_bit;
|
||||||
return (pages[page] & mask2) != 0;
|
return (pages[page] & mask2) != 0;
|
||||||
|
|
|
@ -386,7 +386,7 @@ if (NOT WIN32)
|
||||||
target_include_directories(yuzu PRIVATE ${Qt${QT_MAJOR_VERSION}Gui_PRIVATE_INCLUDE_DIRS})
|
target_include_directories(yuzu PRIVATE ${Qt${QT_MAJOR_VERSION}Gui_PRIVATE_INCLUDE_DIRS})
|
||||||
endif()
|
endif()
|
||||||
if (UNIX AND NOT APPLE)
|
if (UNIX AND NOT APPLE)
|
||||||
target_link_libraries(yuzu PRIVATE Qt${QT_MAJOR_VERSION}::DBus)
|
target_link_libraries(yuzu PRIVATE Qt${QT_MAJOR_VERSION}::DBus gamemode)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
target_compile_definitions(yuzu PRIVATE
|
target_compile_definitions(yuzu PRIVATE
|
||||||
|
|
|
@ -27,6 +27,13 @@ ConfigureCpu::ConfigureCpu(const Core::System& system_,
|
||||||
|
|
||||||
connect(accuracy_combobox, qOverload<int>(&QComboBox::currentIndexChanged), this,
|
connect(accuracy_combobox, qOverload<int>(&QComboBox::currentIndexChanged), this,
|
||||||
&ConfigureCpu::UpdateGroup);
|
&ConfigureCpu::UpdateGroup);
|
||||||
|
|
||||||
|
connect(backend_combobox, qOverload<int>(&QComboBox::currentIndexChanged), this,
|
||||||
|
&ConfigureCpu::UpdateGroup);
|
||||||
|
|
||||||
|
#ifdef HAS_NCE
|
||||||
|
ui->backend_group->setVisible(true);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
ConfigureCpu::~ConfigureCpu() = default;
|
ConfigureCpu::~ConfigureCpu() = default;
|
||||||
|
@ -34,6 +41,7 @@ ConfigureCpu::~ConfigureCpu() = default;
|
||||||
void ConfigureCpu::SetConfiguration() {}
|
void ConfigureCpu::SetConfiguration() {}
|
||||||
void ConfigureCpu::Setup(const ConfigurationShared::Builder& builder) {
|
void ConfigureCpu::Setup(const ConfigurationShared::Builder& builder) {
|
||||||
auto* accuracy_layout = ui->widget_accuracy->layout();
|
auto* accuracy_layout = ui->widget_accuracy->layout();
|
||||||
|
auto* backend_layout = ui->widget_backend->layout();
|
||||||
auto* unsafe_layout = ui->unsafe_widget->layout();
|
auto* unsafe_layout = ui->unsafe_widget->layout();
|
||||||
std::map<u32, QWidget*> unsafe_hold{};
|
std::map<u32, QWidget*> unsafe_hold{};
|
||||||
|
|
||||||
|
@ -62,6 +70,9 @@ void ConfigureCpu::Setup(const ConfigurationShared::Builder& builder) {
|
||||||
// Keep track of cpu_accuracy combobox to display/hide the unsafe settings
|
// Keep track of cpu_accuracy combobox to display/hide the unsafe settings
|
||||||
accuracy_layout->addWidget(widget);
|
accuracy_layout->addWidget(widget);
|
||||||
accuracy_combobox = widget->combobox;
|
accuracy_combobox = widget->combobox;
|
||||||
|
} else if (setting->Id() == Settings::values.cpu_backend.Id()) {
|
||||||
|
backend_layout->addWidget(widget);
|
||||||
|
backend_combobox = widget->combobox;
|
||||||
} else {
|
} else {
|
||||||
// Presently, all other settings here are unsafe checkboxes
|
// Presently, all other settings here are unsafe checkboxes
|
||||||
unsafe_hold.insert({setting->Id(), widget});
|
unsafe_hold.insert({setting->Id(), widget});
|
||||||
|
@ -73,6 +84,7 @@ void ConfigureCpu::Setup(const ConfigurationShared::Builder& builder) {
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateGroup(accuracy_combobox->currentIndex());
|
UpdateGroup(accuracy_combobox->currentIndex());
|
||||||
|
UpdateGroup(backend_combobox->currentIndex());
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConfigureCpu::UpdateGroup(int index) {
|
void ConfigureCpu::UpdateGroup(int index) {
|
||||||
|
|
|
@ -49,4 +49,5 @@ private:
|
||||||
std::vector<std::function<void(bool)>> apply_funcs{};
|
std::vector<std::function<void(bool)>> apply_funcs{};
|
||||||
|
|
||||||
QComboBox* accuracy_combobox;
|
QComboBox* accuracy_combobox;
|
||||||
|
QComboBox* backend_combobox;
|
||||||
};
|
};
|
||||||
|
|
|
@ -59,6 +59,36 @@
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QGroupBox" name="backend_group">
|
||||||
|
<property name="title">
|
||||||
|
<string>CPU Backend</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QWidget" name="widget_backend" native="true">
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout1">
|
||||||
|
<property name="leftMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="topMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="rightMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="bottomMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
<property name="visible">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QGroupBox" name="unsafe_group">
|
<widget class="QGroupBox" name="unsafe_group">
|
||||||
<property name="title">
|
<property name="title">
|
||||||
|
|
|
@ -36,12 +36,29 @@ ConfigureGeneral::~ConfigureGeneral() = default;
|
||||||
void ConfigureGeneral::SetConfiguration() {}
|
void ConfigureGeneral::SetConfiguration() {}
|
||||||
|
|
||||||
void ConfigureGeneral::Setup(const ConfigurationShared::Builder& builder) {
|
void ConfigureGeneral::Setup(const ConfigurationShared::Builder& builder) {
|
||||||
QLayout& layout = *ui->general_widget->layout();
|
QLayout& general_layout = *ui->general_widget->layout();
|
||||||
|
QLayout& linux_layout = *ui->linux_widget->layout();
|
||||||
|
|
||||||
std::map<u32, QWidget*> hold{};
|
std::map<u32, QWidget*> general_hold{};
|
||||||
|
std::map<u32, QWidget*> linux_hold{};
|
||||||
|
|
||||||
for (const auto setting :
|
std::vector<Settings::BasicSetting*> settings;
|
||||||
UISettings::values.linkage.by_category[Settings::Category::UiGeneral]) {
|
|
||||||
|
auto push = [&settings](auto& list) {
|
||||||
|
for (auto setting : list) {
|
||||||
|
settings.push_back(setting);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
push(UISettings::values.linkage.by_category[Settings::Category::UiGeneral]);
|
||||||
|
push(Settings::values.linkage.by_category[Settings::Category::Linux]);
|
||||||
|
|
||||||
|
// Only show Linux group on Unix
|
||||||
|
#ifndef __unix__
|
||||||
|
ui->LinuxGroupBox->setVisible(false);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
for (const auto setting : settings) {
|
||||||
auto* widget = builder.BuildWidget(setting, apply_funcs);
|
auto* widget = builder.BuildWidget(setting, apply_funcs);
|
||||||
|
|
||||||
if (widget == nullptr) {
|
if (widget == nullptr) {
|
||||||
|
@ -52,11 +69,23 @@ void ConfigureGeneral::Setup(const ConfigurationShared::Builder& builder) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
hold.emplace(setting->Id(), widget);
|
switch (setting->GetCategory()) {
|
||||||
|
case Settings::Category::UiGeneral:
|
||||||
|
general_hold.emplace(setting->Id(), widget);
|
||||||
|
break;
|
||||||
|
case Settings::Category::Linux:
|
||||||
|
linux_hold.emplace(setting->Id(), widget);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
widget->deleteLater();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const auto& [id, widget] : hold) {
|
for (const auto& [id, widget] : general_hold) {
|
||||||
layout.addWidget(widget);
|
general_layout.addWidget(widget);
|
||||||
|
}
|
||||||
|
for (const auto& [id, widget] : linux_hold) {
|
||||||
|
linux_layout.addWidget(widget);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -46,6 +46,33 @@
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QGroupBox" name="LinuxGroupBox">
|
||||||
|
<property name="title">
|
||||||
|
<string>Linux</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="LinuxVerticalLayout_1">
|
||||||
|
<item>
|
||||||
|
<widget class="QWidget" name="linux_widget" native="true">
|
||||||
|
<layout class="QVBoxLayout" name="LinuxVerticalLayout_2">
|
||||||
|
<property name="leftMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="topMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="rightMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="bottomMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<spacer name="verticalSpacer">
|
<spacer name="verticalSpacer">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue