mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-08-31 17:55:23 +02:00
A breakpoint hit reached a WebSocket client as two fields on cpu.stepping: a reason string and one address. Everything else the hit site knew was formatted into a log line and dropped. What was missing per kind: - exec: hit count, condition, symbol. - memory: the address actually accessed, read vs write, size, and who did it. The address that reached the client was the *start of the watched range*, so a client watching 4KB learned only that something in it was touched. - register: which register. Entirely - the event carried pc and nothing else. There's now a BreakpointHit captured where the hit happens and carried through Core_Break() on the stepping reason, rendered as a "hit" object on cpu.stepping. It's absent rather than empty when the break wasn't a breakpoint (a pause, a savestate load, an exception), so presence is the test. relatedAddress keeps reporting the range start for compatibility; hit.address is the accurate one. The formatter is shared with the new event below, so the two can't drift. And a new cpu.breakpoint.hit broadcast fires on *every* hit whose condition passes, whether or not it stops the CPU. That's the part that makes log-only breakpoints usable for automation: until now their only trace was a line in the log stream, so a client couldn't count hits, or react to one, without scraping text. Same "hit" object, plus a sequence number. Volume needed handling, since a log-only breakpoint in a hot loop produces events far faster than a connection drains them - measured 13719 hits in three seconds of one homebrew's draw function. The per-connection queue is capped and drops rather than growing without bound, and the sequence number is what makes that honest: a gap tells a client exactly how many it missed. Clients that don't want the traffic at all can disallow the new "breakpoint" broadcast category. Building the hit record is skipped entirely when no debugger is connected, which is one relaxed atomic load on that path. Verified against a running game, all three kinds. The memory case shows why the address/range split matters - accessed address 200540160 against a watched range starting at 200941120, with source "ThreadFillStack" identifying the HLE call responsible. libretro gets stubs: it builds Core.cpp and Breakpoints.cpp but not Core/Debugger/WebSocket.cpp. pspautotests 314/314, UnitTest 55/55. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2039 lines
65 KiB
C++
2039 lines
65 KiB
C++
#include "ppsspp_config.h"
|
|
#include <cstring>
|
|
#include <cassert>
|
|
#include <thread>
|
|
#include <atomic>
|
|
#include <vector>
|
|
#include <cstdlib>
|
|
#include <mutex>
|
|
|
|
#include "Common/CPUDetect.h"
|
|
#include "Common/Log.h"
|
|
#include "Common/Log/LogManager.h"
|
|
#include "Common/System/Display.h"
|
|
#include "Common/System/NativeApp.h"
|
|
#include "Common/System/System.h"
|
|
#include "Common/TimeUtil.h"
|
|
#include "Common/File/FileUtil.h"
|
|
#include "Common/Serialize/Serializer.h"
|
|
#include "Common/Input/InputState.h"
|
|
#include "Common/Thread/ThreadUtil.h"
|
|
#include "Common/Thread/ThreadManager.h"
|
|
#include "Common/File/VFS/VFS.h"
|
|
#include "Common/File/VFS/DirectoryReader.h"
|
|
#include "Common/Data/Text/I18n.h"
|
|
#include "Common/StringUtils.h"
|
|
|
|
#include "Core/Config.h"
|
|
#include "Core/ConfigValues.h"
|
|
#include "Core/Core.h"
|
|
#include "Core/MIPS/MIPS.h"
|
|
#include "Core/HLE/sceCtrl.h"
|
|
#include "Core/HLE/sceUtility.h"
|
|
#include "Core/HLE/__sceAudio.h"
|
|
#include "Core/HW/MemoryStick.h"
|
|
#include "Core/MemMap.h"
|
|
#include "Core/System.h"
|
|
#include "Core/CoreTiming.h"
|
|
#include "Core/HW/Display.h"
|
|
#include "Core/CwCheat.h"
|
|
#include "Core/ELF/ParamSFO.h"
|
|
|
|
#include "GPU/GPUState.h"
|
|
#include "GPU/GPUCommon.h"
|
|
#include "GPU/Common/FramebufferManagerCommon.h"
|
|
#include "GPU/Common/TextureScalerCommon.h"
|
|
#include "GPU/Common/PresentationCommon.h"
|
|
|
|
#include "UI/AudioCommon.h"
|
|
|
|
#include <libretro.h>
|
|
#include "libretro/LibretroGraphicsContext.h"
|
|
#include "libretro/libretro_core_options.h"
|
|
|
|
#if PPSSPP_PLATFORM(ANDROID)
|
|
#include <sys/system_properties.h>
|
|
#endif
|
|
|
|
#define DIR_SEP "/"
|
|
#ifdef _WIN32
|
|
#define DIR_SEP_CHRS "/\\"
|
|
#else
|
|
#define DIR_SEP_CHRS "/"
|
|
#endif
|
|
|
|
#ifdef HAVE_LIBRETRO_VFS
|
|
#include "streams/file_stream.h"
|
|
#endif
|
|
|
|
#define SAMPLERATE 44100
|
|
|
|
/* AUDIO output buffer */
|
|
static struct {
|
|
int16_t *data;
|
|
int32_t size;
|
|
int32_t capacity;
|
|
} output_audio_buffer = {NULL, 0, 0};
|
|
// output_audio_buffer is filled by System_AudioPushSamples() on the emu
|
|
// thread (used with the GL backends, where PSP-side audio pushes arrive
|
|
// via the GL command queue) and drained + realloc'd on the libretro
|
|
// thread in retro_run()/init/shutdown — every access must hold this
|
|
// mutex or a realloc can free the pointer mid-read on the other thread.
|
|
static std::mutex output_audio_buffer_mutex;
|
|
|
|
// Calculated swap interval is 'stable' if the same
|
|
// value is recorded for a number of retro_run()
|
|
// calls equal to VSYNC_SWAP_INTERVAL_FRAMES
|
|
#define VSYNC_SWAP_INTERVAL_FRAMES 6
|
|
// Calculated swap interval is 'valid' if it is
|
|
// within VSYNC_SWAP_INTERVAL_THRESHOLD of an integer
|
|
// value
|
|
#define VSYNC_SWAP_INTERVAL_THRESHOLD 0.05f
|
|
// Swap interval detection is only enabled if the
|
|
// core is running at 'normal' speed - i.e. if
|
|
// run speed is within VSYNC_SWAP_INTERVAL_RUN_SPEED_THRESHOLD
|
|
// percent of 100
|
|
#define VSYNC_SWAP_INTERVAL_RUN_SPEED_THRESHOLD 5.0f
|
|
|
|
static bool libretro_supports_bitmasks = false;
|
|
static bool show_ip_address_options = true;
|
|
static bool show_upnp_port_option = true;
|
|
static bool show_detect_frame_rate_option = true;
|
|
static std::string changeProAdhocServer;
|
|
|
|
void* unserialize_data = NULL;
|
|
size_t unserialize_size = 0;
|
|
|
|
namespace Libretro
|
|
{
|
|
LibretroGraphicsContext *ctx;
|
|
retro_environment_t environ_cb;
|
|
retro_hw_context_type backend = RETRO_HW_CONTEXT_DUMMY;
|
|
static retro_audio_sample_batch_t audio_batch_cb;
|
|
static retro_input_poll_t input_poll_cb;
|
|
static retro_input_state_t input_state_cb;
|
|
static retro_log_printf_t log_cb;
|
|
|
|
bool g_pendingBoot = false;
|
|
std::string g_bootErrorString;
|
|
|
|
static bool detectVsyncSwapInterval = false;
|
|
static bool detectVsyncSwapIntervalOptShown = true;
|
|
static bool softwareRenderInitHack = false;
|
|
|
|
static s64 expectedTimeUsPerRun = 0;
|
|
static uint32_t vsyncSwapInterval = 1;
|
|
static uint32_t vsyncSwapIntervalLast = 1;
|
|
static uint32_t vsyncSwapIntervalCounter = 0;
|
|
static int numVBlanksLast = 0;
|
|
static double fpsTimeLast = 0.0;
|
|
static float runSpeed = 0.0f;
|
|
static s64 runTicksLast = 0;
|
|
|
|
// Must be called with output_audio_buffer_mutex held.
|
|
static void ensure_output_audio_buffer_capacity(int32_t capacity)
|
|
{
|
|
if (capacity <= output_audio_buffer.capacity) {
|
|
return;
|
|
}
|
|
|
|
output_audio_buffer.data = (int16_t*)realloc(output_audio_buffer.data, capacity * sizeof(*output_audio_buffer.data));
|
|
output_audio_buffer.capacity = capacity;
|
|
log_cb(RETRO_LOG_DEBUG, "Output audio buffer capacity set to %d\n", capacity);
|
|
}
|
|
|
|
static void init_output_audio_buffer(int32_t capacity)
|
|
{
|
|
std::lock_guard<std::mutex> lock(output_audio_buffer_mutex);
|
|
output_audio_buffer.data = NULL;
|
|
output_audio_buffer.size = 0;
|
|
output_audio_buffer.capacity = 0;
|
|
ensure_output_audio_buffer_capacity(capacity);
|
|
}
|
|
|
|
static void free_output_audio_buffer()
|
|
{
|
|
std::lock_guard<std::mutex> lock(output_audio_buffer_mutex);
|
|
free(output_audio_buffer.data);
|
|
output_audio_buffer.data = NULL;
|
|
output_audio_buffer.size = 0;
|
|
output_audio_buffer.capacity = 0;
|
|
}
|
|
|
|
static void upload_output_audio_buffer()
|
|
{
|
|
std::lock_guard<std::mutex> lock(output_audio_buffer_mutex);
|
|
audio_batch_cb(output_audio_buffer.data, output_audio_buffer.size / 2);
|
|
output_audio_buffer.size = 0;
|
|
}
|
|
|
|
|
|
/**
|
|
* Clamp a value to a given range.
|
|
*
|
|
* This implementation was taken from `RGBAUtil.cpp` to allow building when `std::clamp()` is unavailable.
|
|
*
|
|
* @param f The value to clamp.
|
|
* @param low The lower bound of the range.
|
|
* @param high The upper bound of the range.
|
|
* @return The clamped value.
|
|
*/
|
|
template <typename T>
|
|
static T clamp(T f, T low, T high) {
|
|
if (f < low)
|
|
return low;
|
|
if (f > high)
|
|
return high;
|
|
return f;
|
|
}
|
|
|
|
static void VsyncSwapIntervalReset()
|
|
{
|
|
expectedTimeUsPerRun = (s64)(1000000.0f / (60.0f / 1.001f));
|
|
vsyncSwapInterval = 1;
|
|
vsyncSwapIntervalLast = 1;
|
|
vsyncSwapIntervalCounter = 0;
|
|
|
|
numVBlanksLast = 0;
|
|
fpsTimeLast = 0.0;
|
|
runSpeed = 0.0f;
|
|
runTicksLast = 0;
|
|
|
|
detectVsyncSwapIntervalOptShown = true;
|
|
}
|
|
|
|
static void VsyncSwapIntervalDetect()
|
|
{
|
|
if (!detectVsyncSwapInterval)
|
|
return;
|
|
|
|
// All bets are off if core is running at
|
|
// the 'wrong' speed (i.e. cycle count for
|
|
// this run will be meaningless if internal
|
|
// frame rate is dropping below expected
|
|
// value, or fast forward is enabled)
|
|
double fpsTime = time_now_d();
|
|
int numVBlanks = __DisplayGetNumVblanks();
|
|
int frames = numVBlanks - numVBlanksLast;
|
|
|
|
if (frames >= VSYNC_SWAP_INTERVAL_FRAMES << 1)
|
|
{
|
|
double fps = (double)frames / (fpsTime - fpsTimeLast);
|
|
runSpeed = fps / ((60.0f / 1.001f) / 100.0f);
|
|
|
|
fpsTimeLast = fpsTime;
|
|
numVBlanksLast = numVBlanks;
|
|
}
|
|
|
|
float speedDelta = 100.0f - runSpeed;
|
|
speedDelta = (speedDelta < 0.0f) ? -speedDelta : speedDelta;
|
|
|
|
// Speed is measured relative to a 60 Hz refresh
|
|
// rate. If we are transitioning from a low internal
|
|
// frame rate to a higher internal frame rate, then
|
|
// 'full speed' may actually equate to
|
|
// (100 / current_swap_interval)...
|
|
if ((vsyncSwapInterval > 1) &&
|
|
(speedDelta >= VSYNC_SWAP_INTERVAL_RUN_SPEED_THRESHOLD))
|
|
{
|
|
speedDelta = 100.0f - (runSpeed * (float)vsyncSwapInterval);
|
|
speedDelta = (speedDelta < 0.0f) ? -speedDelta : speedDelta;
|
|
}
|
|
|
|
if (speedDelta >= VSYNC_SWAP_INTERVAL_RUN_SPEED_THRESHOLD)
|
|
{
|
|
// Swap interval detection is invalid - bail out
|
|
vsyncSwapIntervalCounter = 0;
|
|
return;
|
|
}
|
|
|
|
// Get elapsed time (us) for this run
|
|
s64 runTicks = CoreTiming::GetTicks(currentMIPS);
|
|
s64 runTimeUs = cyclesToUs(runTicks - runTicksLast);
|
|
|
|
// Check if current internal frame rate is a
|
|
// factor of the default ~60 Hz
|
|
float swapRatio = (float)runTimeUs / (float)expectedTimeUsPerRun;
|
|
uint32_t swapInteger;
|
|
float swapRemainder;
|
|
|
|
// If internal frame rate is equal to (within threshold)
|
|
// or higher than the default ~60 Hz, fall back to a
|
|
// swap interval of 1
|
|
if (swapRatio < (1.0f + VSYNC_SWAP_INTERVAL_THRESHOLD))
|
|
{
|
|
swapInteger = 1;
|
|
swapRemainder = 0.0f;
|
|
}
|
|
else
|
|
{
|
|
swapInteger = (uint32_t)(swapRatio + 0.5f);
|
|
swapRemainder = swapRatio - (float)swapInteger;
|
|
swapRemainder = (swapRemainder < 0.0f) ?
|
|
-swapRemainder : swapRemainder;
|
|
}
|
|
|
|
// > Swap interval is considered 'valid' if it is
|
|
// within VSYNC_SWAP_INTERVAL_THRESHOLD of an integer
|
|
// value
|
|
// > If valid, check if new swap interval differs from
|
|
// previously logged value
|
|
if ((swapRemainder <= VSYNC_SWAP_INTERVAL_THRESHOLD) &&
|
|
(swapInteger != vsyncSwapInterval))
|
|
{
|
|
vsyncSwapIntervalCounter =
|
|
(swapInteger == vsyncSwapIntervalLast) ?
|
|
(vsyncSwapIntervalCounter + 1) : 0;
|
|
|
|
// Check whether swap interval is 'stable'
|
|
if (vsyncSwapIntervalCounter >= VSYNC_SWAP_INTERVAL_FRAMES)
|
|
{
|
|
vsyncSwapInterval = swapInteger;
|
|
vsyncSwapIntervalCounter = 0;
|
|
|
|
// Notify frontend
|
|
retro_system_av_info avInfo;
|
|
retro_get_system_av_info(&avInfo);
|
|
environ_cb(RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO, &avInfo);
|
|
}
|
|
|
|
vsyncSwapIntervalLast = swapInteger;
|
|
}
|
|
else
|
|
vsyncSwapIntervalCounter = 0;
|
|
|
|
runTicksLast = runTicks;
|
|
}
|
|
} // namespace Libretro
|
|
|
|
using namespace Libretro;
|
|
|
|
void RetroLogCallback(const LogMessage &message, void *userdata) {
|
|
retro_log_printf_t fn = (retro_log_printf_t)userdata;
|
|
switch (message.level) {
|
|
case LogLevel::LVERBOSE:
|
|
case LogLevel::LDEBUG:
|
|
(fn)(RETRO_LOG_DEBUG, "[%s] %s", message.log, message.msg.c_str());
|
|
break;
|
|
|
|
case LogLevel::LERROR:
|
|
(fn)(RETRO_LOG_ERROR, "[%s] %s", message.log, message.msg.c_str());
|
|
break;
|
|
case LogLevel::LNOTICE:
|
|
case LogLevel::LWARNING:
|
|
(fn)(RETRO_LOG_WARN, "[%s] %s", message.log, message.msg.c_str());
|
|
break;
|
|
case LogLevel::LINFO:
|
|
default:
|
|
(fn)(RETRO_LOG_INFO, "[%s] %s", message.log, message.msg.c_str());
|
|
break;
|
|
}
|
|
}
|
|
|
|
static bool set_variable_visibility(void)
|
|
{
|
|
struct retro_core_option_display option_display;
|
|
struct retro_variable var;
|
|
bool updated = false;
|
|
|
|
// Show/hide IP address options
|
|
bool show_ip_address_options_prev = show_ip_address_options;
|
|
show_ip_address_options = true;
|
|
|
|
var.key = "ppsspp_change_pro_ad_hoc_server_address";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value && strcmp(var.value, "IP address"))
|
|
show_ip_address_options = false;
|
|
|
|
if (show_ip_address_options != show_ip_address_options_prev)
|
|
{
|
|
option_display.visible = show_ip_address_options;
|
|
for (int i = 0; i < 12; i++)
|
|
{
|
|
char key[64] = {0};
|
|
option_display.key = key;
|
|
snprintf(key, sizeof(key), "ppsspp_pro_ad_hoc_server_address%02d", i + 1);
|
|
environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display);
|
|
}
|
|
updated = true;
|
|
}
|
|
|
|
// Show/hide 'UPnP Use Original Port' option
|
|
bool show_upnp_port_option_prev = show_upnp_port_option;
|
|
show_upnp_port_option = true;
|
|
|
|
var.key = "ppsspp_enable_upnp";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value && !strcmp(var.value, "disabled"))
|
|
show_upnp_port_option = false;
|
|
|
|
if (show_upnp_port_option != show_upnp_port_option_prev)
|
|
{
|
|
option_display.visible = show_upnp_port_option;
|
|
option_display.key = "ppsspp_upnp_use_original_port";
|
|
environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display);
|
|
updated = true;
|
|
}
|
|
|
|
// Show/hide 'Detect Frame Rate Changes' option
|
|
bool show_detect_frame_rate_option_prev = show_detect_frame_rate_option;
|
|
int frameskip = 0;
|
|
bool auto_frameskip = false;
|
|
bool dupe_frames = false;
|
|
show_detect_frame_rate_option = true;
|
|
|
|
var.key = "ppsspp_frameskip";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value && strcmp(var.value, "disabled"))
|
|
frameskip = atoi(var.value);
|
|
var.key = "ppsspp_auto_frameskip";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value && !strcmp(var.value, "enabled"))
|
|
auto_frameskip = true;
|
|
var.key = "ppsspp_frame_duplication";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value && !strcmp(var.value, "enabled"))
|
|
dupe_frames = true;
|
|
|
|
show_detect_frame_rate_option = (frameskip == 0) && !auto_frameskip && !dupe_frames;
|
|
if (show_detect_frame_rate_option != show_detect_frame_rate_option_prev)
|
|
{
|
|
option_display.visible = show_detect_frame_rate_option;
|
|
option_display.key = "ppsspp_detect_vsync_swap_interval";
|
|
environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display);
|
|
updated = true;
|
|
}
|
|
|
|
return updated;
|
|
}
|
|
|
|
void retro_set_environment(retro_environment_t cb)
|
|
{
|
|
environ_cb = cb;
|
|
|
|
bool option_categories = false;
|
|
libretro_set_core_options(environ_cb, &option_categories);
|
|
struct retro_core_options_update_display_callback update_display_cb;
|
|
update_display_cb.callback = set_variable_visibility;
|
|
environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_UPDATE_DISPLAY_CALLBACK, &update_display_cb);
|
|
|
|
#ifdef HAVE_LIBRETRO_VFS
|
|
struct retro_vfs_interface_info vfs_iface_info { 2, nullptr };
|
|
if (cb(RETRO_ENVIRONMENT_GET_VFS_INTERFACE, &vfs_iface_info))
|
|
File::InitLibretroVFS(&vfs_iface_info);
|
|
#endif
|
|
}
|
|
|
|
static int get_language_auto(void)
|
|
{
|
|
retro_language val = RETRO_LANGUAGE_ENGLISH;
|
|
environ_cb(RETRO_ENVIRONMENT_GET_LANGUAGE, &val);
|
|
|
|
switch (val)
|
|
{
|
|
default:
|
|
case RETRO_LANGUAGE_ENGLISH:
|
|
return PSP_SYSTEMPARAM_LANGUAGE_ENGLISH;
|
|
case RETRO_LANGUAGE_JAPANESE:
|
|
return PSP_SYSTEMPARAM_LANGUAGE_JAPANESE;
|
|
case RETRO_LANGUAGE_FRENCH:
|
|
return PSP_SYSTEMPARAM_LANGUAGE_FRENCH;
|
|
case RETRO_LANGUAGE_GERMAN:
|
|
return PSP_SYSTEMPARAM_LANGUAGE_GERMAN;
|
|
case RETRO_LANGUAGE_SPANISH:
|
|
return PSP_SYSTEMPARAM_LANGUAGE_SPANISH;
|
|
case RETRO_LANGUAGE_ITALIAN:
|
|
return PSP_SYSTEMPARAM_LANGUAGE_ITALIAN;
|
|
case RETRO_LANGUAGE_PORTUGUESE_BRAZIL:
|
|
case RETRO_LANGUAGE_PORTUGUESE_PORTUGAL:
|
|
return PSP_SYSTEMPARAM_LANGUAGE_PORTUGUESE;
|
|
case RETRO_LANGUAGE_RUSSIAN:
|
|
return PSP_SYSTEMPARAM_LANGUAGE_RUSSIAN;
|
|
case RETRO_LANGUAGE_DUTCH:
|
|
return PSP_SYSTEMPARAM_LANGUAGE_DUTCH;
|
|
case RETRO_LANGUAGE_KOREAN:
|
|
return PSP_SYSTEMPARAM_LANGUAGE_KOREAN;
|
|
case RETRO_LANGUAGE_CHINESE_TRADITIONAL:
|
|
return PSP_SYSTEMPARAM_LANGUAGE_CHINESE_TRADITIONAL;
|
|
case RETRO_LANGUAGE_CHINESE_SIMPLIFIED:
|
|
return PSP_SYSTEMPARAM_LANGUAGE_CHINESE_SIMPLIFIED;
|
|
}
|
|
}
|
|
|
|
static std::string map_psp_language_to_i18n_locale(int val)
|
|
{
|
|
switch (val)
|
|
{
|
|
default:
|
|
case PSP_SYSTEMPARAM_LANGUAGE_ENGLISH:
|
|
return "en_US";
|
|
case PSP_SYSTEMPARAM_LANGUAGE_JAPANESE:
|
|
return "ja_JP";
|
|
case PSP_SYSTEMPARAM_LANGUAGE_FRENCH:
|
|
return "fr_FR";
|
|
case PSP_SYSTEMPARAM_LANGUAGE_GERMAN:
|
|
return "de_DE";
|
|
case PSP_SYSTEMPARAM_LANGUAGE_SPANISH:
|
|
return "es_ES";
|
|
case PSP_SYSTEMPARAM_LANGUAGE_ITALIAN:
|
|
return "it_IT";
|
|
case PSP_SYSTEMPARAM_LANGUAGE_PORTUGUESE:
|
|
return "pt_PT";
|
|
case PSP_SYSTEMPARAM_LANGUAGE_RUSSIAN:
|
|
return "ru_RU";
|
|
case PSP_SYSTEMPARAM_LANGUAGE_DUTCH:
|
|
return "nl_NL";
|
|
case PSP_SYSTEMPARAM_LANGUAGE_KOREAN:
|
|
return "ko_KR";
|
|
case PSP_SYSTEMPARAM_LANGUAGE_CHINESE_TRADITIONAL:
|
|
return "zh_TW";
|
|
case PSP_SYSTEMPARAM_LANGUAGE_CHINESE_SIMPLIFIED:
|
|
return "zh_CN";
|
|
}
|
|
}
|
|
|
|
static void check_dynamic_variables(CoreParameter &coreParam) {
|
|
if (g_Config.bForceLagSync)
|
|
{
|
|
bool isFastForwarding;
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_FASTFORWARDING, &isFastForwarding))
|
|
coreParam.fastForward = isFastForwarding;
|
|
}
|
|
}
|
|
|
|
static void check_variables(CoreParameter &coreParam)
|
|
{
|
|
check_dynamic_variables(coreParam);
|
|
|
|
struct retro_variable var = {0};
|
|
std::string sTextureShaderName_prev;
|
|
int iInternalResolution_prev;
|
|
int iTexScalingType_prev;
|
|
int iTexScalingLevel_prev;
|
|
int iMultiSampleLevel_prev;
|
|
bool bDisplayCropTo16x9_prev;
|
|
|
|
var.key = "ppsspp_language";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
{
|
|
if (!strcmp(var.value, "Automatic"))
|
|
g_Config.iLanguage = -1;
|
|
else if (!strcmp(var.value, "English"))
|
|
g_Config.iLanguage = PSP_SYSTEMPARAM_LANGUAGE_ENGLISH;
|
|
else if (!strcmp(var.value, "Japanese"))
|
|
g_Config.iLanguage = PSP_SYSTEMPARAM_LANGUAGE_JAPANESE;
|
|
else if (!strcmp(var.value, "French"))
|
|
g_Config.iLanguage = PSP_SYSTEMPARAM_LANGUAGE_FRENCH;
|
|
else if (!strcmp(var.value, "Spanish"))
|
|
g_Config.iLanguage = PSP_SYSTEMPARAM_LANGUAGE_SPANISH;
|
|
else if (!strcmp(var.value, "German"))
|
|
g_Config.iLanguage = PSP_SYSTEMPARAM_LANGUAGE_GERMAN;
|
|
else if (!strcmp(var.value, "Italian"))
|
|
g_Config.iLanguage = PSP_SYSTEMPARAM_LANGUAGE_ITALIAN;
|
|
else if (!strcmp(var.value, "Dutch"))
|
|
g_Config.iLanguage = PSP_SYSTEMPARAM_LANGUAGE_DUTCH;
|
|
else if (!strcmp(var.value, "Portuguese"))
|
|
g_Config.iLanguage = PSP_SYSTEMPARAM_LANGUAGE_PORTUGUESE;
|
|
else if (!strcmp(var.value, "Russian"))
|
|
g_Config.iLanguage = PSP_SYSTEMPARAM_LANGUAGE_RUSSIAN;
|
|
else if (!strcmp(var.value, "Korean"))
|
|
g_Config.iLanguage = PSP_SYSTEMPARAM_LANGUAGE_KOREAN;
|
|
else if (!strcmp(var.value, "Chinese Traditional"))
|
|
g_Config.iLanguage = PSP_SYSTEMPARAM_LANGUAGE_CHINESE_TRADITIONAL;
|
|
else if (!strcmp(var.value, "Chinese Simplified"))
|
|
g_Config.iLanguage = PSP_SYSTEMPARAM_LANGUAGE_CHINESE_SIMPLIFIED;
|
|
}
|
|
|
|
#ifndef __EMSCRIPTEN__
|
|
var.key = "ppsspp_cpu_core";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
{
|
|
if (!strcmp(var.value, "JIT"))
|
|
g_Config.iCpuCore = (int)CPUCore::JIT;
|
|
else if (!strcmp(var.value, "IR JIT"))
|
|
g_Config.iCpuCore = (int)CPUCore::IR_INTERPRETER;
|
|
else if (!strcmp(var.value, "Interpreter"))
|
|
g_Config.iCpuCore = (int)CPUCore::INTERPRETER;
|
|
}
|
|
|
|
if (System_GetPropertyBool(SYSPROP_CAN_JIT) == false && g_Config.iCpuCore == (int)CPUCore::JIT) {
|
|
// Just gonna force it to the IR interpreter on startup.
|
|
// We don't hide the option, but we make sure it's off on bootup. In case someone wants
|
|
// to experiment in future iOS versions or something...
|
|
g_Config.iCpuCore = (int)CPUCore::IR_INTERPRETER;
|
|
}
|
|
#else
|
|
g_Config.iCpuCore = (int)CPUCore::INTERPRETER;
|
|
#endif
|
|
|
|
var.key = "ppsspp_fast_memory";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
{
|
|
if (!strcmp(var.value, "disabled"))
|
|
g_Config.bFastMemory = false;
|
|
else
|
|
g_Config.bFastMemory = true;
|
|
}
|
|
|
|
var.key = "ppsspp_ignore_bad_memory_access";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
{
|
|
if (!strcmp(var.value, "disabled"))
|
|
g_Config.bIgnoreBadMemAccess = false;
|
|
else
|
|
g_Config.bIgnoreBadMemAccess = true;
|
|
}
|
|
|
|
var.key = "ppsspp_io_timing_method";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
{
|
|
if (!strcmp(var.value, "Fast"))
|
|
g_Config.iIOTimingMethod = IOTIMING_FAST;
|
|
else if (!strcmp(var.value, "Host"))
|
|
g_Config.iIOTimingMethod = IOTIMING_HOST;
|
|
else if (!strcmp(var.value, "Simulate UMD delays"))
|
|
g_Config.iIOTimingMethod = IOTIMING_REALISTIC;
|
|
else if (!strcmp(var.value, "Simulate UMD slow reading speed"))
|
|
g_Config.iIOTimingMethod = IOTIMING_UMDSLOWREALISTIC;
|
|
}
|
|
|
|
var.key = "ppsspp_force_lag_sync";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
{
|
|
if (!strcmp(var.value, "disabled"))
|
|
g_Config.bForceLagSync = false;
|
|
else
|
|
g_Config.bForceLagSync = true;
|
|
}
|
|
|
|
var.key = "ppsspp_locked_cpu_speed";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
g_Config.iLockedCPUSpeed = atoi(var.value);
|
|
|
|
var.key = "ppsspp_cache_iso";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
{
|
|
if (!strcmp(var.value, "disabled"))
|
|
g_Config.bCacheFullIsoInRam = false;
|
|
else
|
|
g_Config.bCacheFullIsoInRam = true;
|
|
}
|
|
|
|
var.key = "ppsspp_cheats";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
{
|
|
if (!strcmp(var.value, "disabled"))
|
|
g_Config.bEnableCheats = false;
|
|
else
|
|
g_Config.bEnableCheats = true;
|
|
}
|
|
|
|
var.key = "ppsspp_psp_model";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
{
|
|
if (!strcmp(var.value, "psp_1000"))
|
|
g_Config.iPSPModel = PSP_MODEL_FAT;
|
|
else if (!strcmp(var.value, "psp_2000_3000"))
|
|
g_Config.iPSPModel = PSP_MODEL_SLIM;
|
|
}
|
|
|
|
var.key = "ppsspp_button_preference";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
{
|
|
if (!strcmp(var.value, "Cross"))
|
|
g_Config.iButtonPreference = PSP_SYSTEMPARAM_BUTTON_CROSS;
|
|
else if (!strcmp(var.value, "Circle"))
|
|
g_Config.iButtonPreference = PSP_SYSTEMPARAM_BUTTON_CIRCLE;
|
|
}
|
|
|
|
var.key = "ppsspp_analog_is_circular";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
{
|
|
if (!strcmp(var.value, "disabled"))
|
|
g_Config.bAnalogIsCircular = false;
|
|
else
|
|
g_Config.bAnalogIsCircular = true;
|
|
}
|
|
|
|
var.key = "ppsspp_analog_deadzone";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
g_Config.fAnalogDeadzone = atof(var.value);
|
|
|
|
var.key = "ppsspp_analog_sensitivity";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
g_Config.fAnalogSensitivity = atof(var.value);
|
|
|
|
var.key = "ppsspp_memstick_inserted";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
{
|
|
if (!strcmp(var.value, "disabled"))
|
|
g_Config.bMemStickInserted = false;
|
|
else
|
|
g_Config.bMemStickInserted = true;
|
|
}
|
|
|
|
var.key = "ppsspp_internal_resolution";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
{
|
|
iInternalResolution_prev = g_Config.iInternalResolution;
|
|
|
|
if (!strcmp(var.value, "480x272"))
|
|
g_Config.iInternalResolution = 1;
|
|
else if (!strcmp(var.value, "960x544"))
|
|
g_Config.iInternalResolution = 2;
|
|
else if (!strcmp(var.value, "1440x816"))
|
|
g_Config.iInternalResolution = 3;
|
|
else if (!strcmp(var.value, "1920x1088"))
|
|
g_Config.iInternalResolution = 4;
|
|
else if (!strcmp(var.value, "2400x1360"))
|
|
g_Config.iInternalResolution = 5;
|
|
else if (!strcmp(var.value, "2880x1632"))
|
|
g_Config.iInternalResolution = 6;
|
|
else if (!strcmp(var.value, "3360x1904"))
|
|
g_Config.iInternalResolution = 7;
|
|
else if (!strcmp(var.value, "3840x2176"))
|
|
g_Config.iInternalResolution = 8;
|
|
else if (!strcmp(var.value, "4320x2448"))
|
|
g_Config.iInternalResolution = 9;
|
|
else if (!strcmp(var.value, "4800x2720"))
|
|
g_Config.iInternalResolution = 10;
|
|
|
|
// Force resolution to 1x without hardware context
|
|
if (backend == RETRO_HW_CONTEXT_NONE)
|
|
g_Config.iInternalResolution = 1;
|
|
}
|
|
|
|
var.key = "ppsspp_software_rendering";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
{
|
|
if (!PSP_IsInited())
|
|
{
|
|
if (!strcmp(var.value, "disabled") && backend != RETRO_HW_CONTEXT_NONE)
|
|
g_Config.bSoftwareRendering = false;
|
|
else
|
|
g_Config.bSoftwareRendering = true;
|
|
}
|
|
|
|
// Force resolution to 1x with software rendering
|
|
if (g_Config.bSoftwareRendering)
|
|
g_Config.iInternalResolution = 1;
|
|
}
|
|
|
|
var.key = "ppsspp_mulitsample_level";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
{
|
|
iMultiSampleLevel_prev = g_Config.iMultiSampleLevel;
|
|
|
|
if (!strcmp(var.value, "Disabled"))
|
|
g_Config.iMultiSampleLevel = 0;
|
|
else if (!strcmp(var.value, "x2"))
|
|
g_Config.iMultiSampleLevel = 1;
|
|
else if (!strcmp(var.value, "x4"))
|
|
g_Config.iMultiSampleLevel = 2;
|
|
else if (!strcmp(var.value, "x8"))
|
|
g_Config.iMultiSampleLevel = 3;
|
|
}
|
|
|
|
var.key = "ppsspp_cropto16x9";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
{
|
|
bDisplayCropTo16x9_prev = g_Config.bDisplayCropTo16x9;
|
|
|
|
if (!strcmp(var.value, "disabled"))
|
|
g_Config.bDisplayCropTo16x9 = false;
|
|
else
|
|
g_Config.bDisplayCropTo16x9 = true;
|
|
}
|
|
|
|
var.key = "ppsspp_frameskip";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
g_Config.iFrameSkip = atoi(var.value);
|
|
|
|
var.key = "ppsspp_auto_frameskip";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
{
|
|
if (!strcmp(var.value, "disabled"))
|
|
g_Config.bAutoFrameSkip = false;
|
|
else
|
|
g_Config.bAutoFrameSkip = true;
|
|
}
|
|
|
|
var.key = "ppsspp_frame_duplication";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
{
|
|
if (!strcmp(var.value, "disabled"))
|
|
g_Config.bRenderDuplicateFrames = false;
|
|
else
|
|
g_Config.bRenderDuplicateFrames = true;
|
|
}
|
|
|
|
var.key = "ppsspp_detect_vsync_swap_interval";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
{
|
|
if (!strcmp(var.value, "disabled"))
|
|
detectVsyncSwapInterval = false;
|
|
else
|
|
detectVsyncSwapInterval = true;
|
|
}
|
|
|
|
var.key = "ppsspp_inflight_frames";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
{
|
|
if (!strcmp(var.value, "No buffer"))
|
|
g_Config.iInflightFrames = 1;
|
|
else if (!strcmp(var.value, "Up to 1"))
|
|
g_Config.iInflightFrames = 2;
|
|
else if (!strcmp(var.value, "Up to 2"))
|
|
g_Config.iInflightFrames = 3;
|
|
}
|
|
|
|
var.key = "ppsspp_skip_buffer_effects";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
{
|
|
if (!strcmp(var.value, "disabled"))
|
|
g_Config.bSkipBufferEffects = false;
|
|
else
|
|
g_Config.bSkipBufferEffects = true;
|
|
}
|
|
|
|
var.key = "ppsspp_skip_gpu_readbacks";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
{
|
|
if (!strcmp(var.value, "disabled"))
|
|
g_Config.iSkipGPUReadbackMode = (int)SkipGPUReadbackMode::NO_SKIP;
|
|
else
|
|
g_Config.iSkipGPUReadbackMode = (int)SkipGPUReadbackMode::SKIP;
|
|
}
|
|
|
|
var.key = "ppsspp_spline_quality";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
{
|
|
if (!strcmp(var.value, "Low"))
|
|
g_Config.iSplineBezierQuality = 0;
|
|
else if (!strcmp(var.value, "Medium"))
|
|
g_Config.iSplineBezierQuality = 1;
|
|
else if (!strcmp(var.value, "High"))
|
|
g_Config.iSplineBezierQuality = 2;
|
|
}
|
|
|
|
var.key = "ppsspp_gpu_hardware_transform";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
{
|
|
if (!strcmp(var.value, "disabled"))
|
|
g_Config.bHardwareTransform = false;
|
|
else
|
|
g_Config.bHardwareTransform = true;
|
|
}
|
|
|
|
var.key = "ppsspp_lower_resolution_for_effects";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
{
|
|
if (!strcmp(var.value, "disabled"))
|
|
g_Config.iBloomHack = 0;
|
|
else if (!strcmp(var.value, "Safe"))
|
|
g_Config.iBloomHack = 1;
|
|
else if (!strcmp(var.value, "Balanced"))
|
|
g_Config.iBloomHack = 2;
|
|
else if (!strcmp(var.value, "Aggressive"))
|
|
g_Config.iBloomHack = 3;
|
|
}
|
|
|
|
var.key = "ppsspp_texture_scaling_type";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
{
|
|
iTexScalingType_prev = g_Config.iTexScalingType;
|
|
|
|
if (!strcmp(var.value, "xbrz"))
|
|
g_Config.iTexScalingType = TextureScalerCommon::XBRZ;
|
|
else if (!strcmp(var.value, "hybrid"))
|
|
g_Config.iTexScalingType = TextureScalerCommon::HYBRID;
|
|
else if (!strcmp(var.value, "bicubic"))
|
|
g_Config.iTexScalingType = TextureScalerCommon::BICUBIC;
|
|
else if (!strcmp(var.value, "hybrid_bicubic"))
|
|
g_Config.iTexScalingType = TextureScalerCommon::HYBRID_BICUBIC;
|
|
}
|
|
|
|
var.key = "ppsspp_texture_scaling_level";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
{
|
|
iTexScalingLevel_prev = g_Config.iTexScalingLevel;
|
|
|
|
if (!strcmp(var.value, "disabled"))
|
|
g_Config.iTexScalingLevel = 1;
|
|
else if (!strcmp(var.value, "2x"))
|
|
g_Config.iTexScalingLevel = 2;
|
|
else if (!strcmp(var.value, "3x"))
|
|
g_Config.iTexScalingLevel = 3;
|
|
else if (!strcmp(var.value, "4x"))
|
|
g_Config.iTexScalingLevel = 4;
|
|
else if (!strcmp(var.value, "5x"))
|
|
g_Config.iTexScalingLevel = 5;
|
|
}
|
|
|
|
var.key = "ppsspp_texture_deposterize";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
{
|
|
if (!strcmp(var.value, "disabled"))
|
|
g_Config.bTexDeposterize = false;
|
|
else
|
|
g_Config.bTexDeposterize = true;
|
|
}
|
|
|
|
var.key = "ppsspp_texture_shader";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
{
|
|
sTextureShaderName_prev = g_Config.sTextureShaderName;
|
|
|
|
if (!strcmp(var.value, "disabled"))
|
|
g_Config.sTextureShaderName = "Off";
|
|
else if (!strcmp(var.value, "2xBRZ"))
|
|
g_Config.sTextureShaderName = "Tex2xBRZ";
|
|
else if (!strcmp(var.value, "4xBRZ"))
|
|
g_Config.sTextureShaderName = "Tex4xBRZ";
|
|
else if (!strcmp(var.value, "MMPX"))
|
|
g_Config.sTextureShaderName = "TexMMPX";
|
|
}
|
|
|
|
var.key = "ppsspp_texture_anisotropic_filtering";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
{
|
|
if (!strcmp(var.value, "disabled"))
|
|
g_Config.iAnisotropyLevel = 0;
|
|
else if (!strcmp(var.value, "2x"))
|
|
g_Config.iAnisotropyLevel = 1;
|
|
else if (!strcmp(var.value, "4x"))
|
|
g_Config.iAnisotropyLevel = 2;
|
|
else if (!strcmp(var.value, "8x"))
|
|
g_Config.iAnisotropyLevel = 3;
|
|
else if (!strcmp(var.value, "16x"))
|
|
g_Config.iAnisotropyLevel = 4;
|
|
}
|
|
|
|
var.key = "ppsspp_texture_filtering";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
{
|
|
if (!strcmp(var.value, "Auto"))
|
|
g_Config.iTexFiltering = 1;
|
|
else if (!strcmp(var.value, "Nearest"))
|
|
g_Config.iTexFiltering = 2;
|
|
else if (!strcmp(var.value, "Linear"))
|
|
g_Config.iTexFiltering = 3;
|
|
else if (!strcmp(var.value, "Auto max quality"))
|
|
g_Config.iTexFiltering = 4;
|
|
}
|
|
|
|
var.key = "ppsspp_smart_2d_texture_filtering";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
{
|
|
if (!strcmp(var.value, "disabled"))
|
|
g_Config.bSmart2DTexFiltering = false;
|
|
else
|
|
g_Config.bSmart2DTexFiltering = true;
|
|
}
|
|
|
|
var.key = "ppsspp_texture_replacement";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
{
|
|
if (!strcmp(var.value, "disabled"))
|
|
g_Config.bReplaceTextures = false;
|
|
else
|
|
g_Config.bReplaceTextures = true;
|
|
}
|
|
|
|
var.key = "ppsspp_enable_wlan";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
{
|
|
if (!strcmp(var.value, "disabled"))
|
|
g_Config.bEnableWlan = false;
|
|
else
|
|
g_Config.bEnableWlan = true;
|
|
}
|
|
|
|
var.key = "ppsspp_wlan_channel";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
g_Config.iWlanAdhocChannel = atoi(var.value);
|
|
|
|
var.key = "ppsspp_enable_builtin_pro_ad_hoc_server";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
{
|
|
if (!strcmp(var.value, "disabled"))
|
|
g_Config.bEnableAdhocServer = false;
|
|
else
|
|
g_Config.bEnableAdhocServer = true;
|
|
}
|
|
|
|
var.key = "ppsspp_change_pro_ad_hoc_server_address";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
changeProAdhocServer = var.value;
|
|
|
|
var.key = "ppsspp_enable_upnp";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
{
|
|
if (!strcmp(var.value, "disabled"))
|
|
g_Config.bEnableUPnP = false;
|
|
else
|
|
g_Config.bEnableUPnP = true;
|
|
}
|
|
|
|
var.key = "ppsspp_upnp_use_original_port";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
{
|
|
if (!strcmp(var.value, "disabled"))
|
|
g_Config.bUPnPUseOriginalPort = false;
|
|
else
|
|
g_Config.bUPnPUseOriginalPort = true;
|
|
}
|
|
|
|
var.key = "ppsspp_port_offset";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
g_Config.iPortOffset = atoi(var.value);
|
|
|
|
var.key = "ppsspp_minimum_timeout";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
g_Config.iMinTimeout = atoi(var.value);
|
|
|
|
var.key = "ppsspp_forced_first_connect";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
{
|
|
if (!strcmp(var.value, "disabled"))
|
|
g_Config.bForcedFirstConnect = false;
|
|
else
|
|
g_Config.bForcedFirstConnect = true;
|
|
}
|
|
|
|
std::string ppsspp_change_mac_address[12];
|
|
int ppsspp_pro_ad_hoc_ipv4[12];
|
|
char key[64] = {0};
|
|
var.key = key;
|
|
g_Config.sMACAddress = "";
|
|
g_Config.sProAdhocServer = "";
|
|
for (int i = 0; i < 12; i++)
|
|
{
|
|
snprintf(key, sizeof(key), "ppsspp_change_mac_address%02d", i + 1);
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
{
|
|
ppsspp_change_mac_address[i] = var.value;
|
|
|
|
if (i && i % 2 == 0)
|
|
g_Config.sMACAddress += ":";
|
|
|
|
g_Config.sMACAddress += ppsspp_change_mac_address[i];
|
|
}
|
|
|
|
snprintf(key, sizeof(key), "ppsspp_pro_ad_hoc_server_address%02d", i + 1);
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
|
ppsspp_pro_ad_hoc_ipv4[i] = atoi(var.value);
|
|
}
|
|
|
|
if (g_Config.sMACAddress == "00:00:00:00:00:00")
|
|
{
|
|
g_Config.sMACAddress = CreateRandMAC();
|
|
|
|
for (int i = 0; i < 12; i++)
|
|
{
|
|
snprintf(key, sizeof(key), "ppsspp_change_mac_address%02d", i + 1);
|
|
std::string digit = {g_Config.sMACAddress[i + i / 2]};
|
|
var.value = digit.c_str();
|
|
environ_cb(RETRO_ENVIRONMENT_SET_VARIABLE, &var);
|
|
}
|
|
}
|
|
|
|
if (changeProAdhocServer == "IP address")
|
|
{
|
|
g_Config.sProAdhocServer = "";
|
|
bool leadingZero = true;
|
|
for (int i = 0; i < 12; i++)
|
|
{
|
|
if (i && i % 3 == 0)
|
|
{
|
|
g_Config.sProAdhocServer += '.';
|
|
leadingZero = true;
|
|
}
|
|
|
|
int addressPt = ppsspp_pro_ad_hoc_ipv4[i];
|
|
if (addressPt || i % 3 == 2)
|
|
leadingZero = false; // We are either non-zero or the last digit of a byte
|
|
|
|
if (! leadingZero)
|
|
g_Config.sProAdhocServer += static_cast<char>('0' + addressPt);
|
|
}
|
|
}
|
|
else
|
|
g_Config.sProAdhocServer = changeProAdhocServer;
|
|
|
|
g_Config.bTexHardwareScaling = g_Config.sTextureShaderName != "Off";
|
|
|
|
if (gpu && (g_Config.iTexScalingType != iTexScalingType_prev
|
|
|| g_Config.iTexScalingLevel != iTexScalingLevel_prev
|
|
|| g_Config.sTextureShaderName != sTextureShaderName_prev))
|
|
{
|
|
gpu->NotifyConfigChanged();
|
|
}
|
|
|
|
if (g_Config.iLanguage < 0)
|
|
g_Config.iLanguage = get_language_auto();
|
|
|
|
g_Config.sLanguageIni = map_psp_language_to_i18n_locale(g_Config.iLanguage);
|
|
g_i18nrepo.LoadIni(g_Config.sLanguageIni);
|
|
|
|
// Cannot detect refresh rate changes if:
|
|
// > Frame skipping is enabled
|
|
// > Frame duplication is enabled
|
|
detectVsyncSwapInterval &=
|
|
!g_Config.bAutoFrameSkip &&
|
|
(g_Config.iFrameSkip == 0) &&
|
|
!g_Config.bRenderDuplicateFrames;
|
|
|
|
bool updateAvInfo = false;
|
|
bool updateGeometry = false;
|
|
bool resetHWContext = false;
|
|
|
|
if (!detectVsyncSwapInterval && (vsyncSwapInterval != 1))
|
|
{
|
|
vsyncSwapInterval = 1;
|
|
updateAvInfo = true;
|
|
}
|
|
|
|
if (g_Config.iInternalResolution != iInternalResolution_prev && backend != RETRO_HW_CONTEXT_NONE)
|
|
{
|
|
coreParam.pixelWidth = coreParam.renderWidth = g_Config.iInternalResolution * NATIVEWIDTH;
|
|
coreParam.pixelHeight = coreParam.renderHeight = g_Config.iInternalResolution * NATIVEHEIGHT;
|
|
|
|
if (gpu)
|
|
{
|
|
retro_system_av_info avInfo;
|
|
retro_get_system_av_info(&avInfo);
|
|
environ_cb(RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO, &avInfo);
|
|
updateAvInfo = false;
|
|
gpu->NotifyDisplayResized();
|
|
if (ctx && ctx->GetGPUCore() != GPUCORE_VULKAN)
|
|
resetHWContext = true;
|
|
}
|
|
}
|
|
|
|
if (g_Config.bDisplayCropTo16x9 != bDisplayCropTo16x9_prev && PSP_IsInited())
|
|
{
|
|
updateGeometry = true;
|
|
if (gpu)
|
|
gpu->NotifyDisplayResized();
|
|
if (ctx && backend != RETRO_HW_CONTEXT_NONE && ctx->GetGPUCore() != GPUCORE_VULKAN)
|
|
resetHWContext = true;
|
|
}
|
|
|
|
if (g_Config.iMultiSampleLevel != iMultiSampleLevel_prev && PSP_IsInited())
|
|
{
|
|
if (gpu)
|
|
{
|
|
const DisplayLayoutConfig &config = g_Config.GetDisplayLayoutConfig(g_display.GetDeviceOrientation());
|
|
gpu->NotifyRenderResized(config);
|
|
}
|
|
}
|
|
|
|
if (updateAvInfo)
|
|
{
|
|
retro_system_av_info avInfo;
|
|
retro_get_system_av_info(&avInfo);
|
|
environ_cb(RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO, &avInfo);
|
|
}
|
|
else if (updateGeometry)
|
|
{
|
|
retro_system_av_info avInfo;
|
|
retro_get_system_av_info(&avInfo);
|
|
environ_cb(RETRO_ENVIRONMENT_SET_GEOMETRY, &avInfo);
|
|
}
|
|
|
|
/* Must reset context to resize render area properly while running,
|
|
* but not necessary with software, and not working with Vulkan.. (TODO) */
|
|
if (resetHWContext)
|
|
((LibretroHWRenderContext *)ctx)->ContextReset();
|
|
|
|
set_variable_visibility();
|
|
}
|
|
|
|
void retro_set_audio_sample_batch(retro_audio_sample_batch_t cb) { audio_batch_cb = cb; }
|
|
void retro_set_audio_sample(retro_audio_sample_t cb) { (void)cb; }
|
|
void retro_set_input_poll(retro_input_poll_t cb) { input_poll_cb = cb; }
|
|
void retro_set_input_state(retro_input_state_t cb) { input_state_cb = cb; }
|
|
|
|
static const struct retro_controller_description psp_controllers[] =
|
|
{
|
|
{ "PSP", RETRO_DEVICE_JOYPAD },
|
|
{ NULL, 0 }
|
|
};
|
|
|
|
static const struct retro_controller_info ports[] =
|
|
{
|
|
{ psp_controllers, 1 },
|
|
{ NULL, 0 }
|
|
};
|
|
|
|
void retro_init(void)
|
|
{
|
|
TimeInit();
|
|
SetCurrentThreadName("Main");
|
|
|
|
struct retro_log_callback log;
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_LOG_INTERFACE, &log))
|
|
{
|
|
log_cb = log.log;
|
|
g_logManager.Init(&g_Config.bEnableLogging);
|
|
// Also enable LogOutput::DebugString unconditionally (not just via Init()'s IsDebuggerPresent()
|
|
// auto-detection, which only fires if a debugger was already attached before Init() ran) so the
|
|
// log always shows up in the debugger's Output window when debugging the core in-process with
|
|
// RetroArch, regardless of where RetroArch itself routes the ExternalCallback log messages.
|
|
g_logManager.EnableOutput(LogOutput::ExternalCallback | LogOutput::DebugString);
|
|
g_logManager.SetExternalLogCallback(&RetroLogCallback, (void *)log_cb);
|
|
}
|
|
|
|
VsyncSwapIntervalReset();
|
|
|
|
struct retro_input_descriptor desc[] = {
|
|
{ 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_LEFT, "D-Pad Left" },
|
|
{ 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_UP, "D-Pad Up" },
|
|
{ 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_DOWN, "D-Pad Down" },
|
|
{ 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_RIGHT, "D-Pad Right" },
|
|
{ 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_B, "Cross" },
|
|
{ 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_A, "Circle" },
|
|
{ 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_X, "Triangle" },
|
|
{ 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_Y, "Square" },
|
|
{ 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_L, "L" },
|
|
{ 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_R, "R" },
|
|
{ 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_SELECT, "Select" },
|
|
{ 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_START, "Start" },
|
|
{ 0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_RIGHT, RETRO_DEVICE_ID_ANALOG_X, "Right Analog X" },
|
|
{ 0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_RIGHT, RETRO_DEVICE_ID_ANALOG_Y, "Right Analog Y" },
|
|
{ 0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_X, "Left Analog X" },
|
|
{ 0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_Y, "Left Analog Y" },
|
|
{ 0 },
|
|
};
|
|
environ_cb(RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS, desc);
|
|
environ_cb(RETRO_ENVIRONMENT_SET_CONTROLLER_INFO, (void*)ports);
|
|
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_INPUT_BITMASKS, NULL))
|
|
libretro_supports_bitmasks = true;
|
|
|
|
g_Config.Load("", "");
|
|
g_Config.iInternalResolution = 0;
|
|
|
|
// Log levels must be set after g_Config.Load
|
|
g_logManager.SetAllLogLevels(LogLevel::LINFO);
|
|
|
|
const char* nickname = NULL;
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_USERNAME, &nickname) && nickname)
|
|
g_Config.sNickName = std::string(nickname);
|
|
|
|
Path retro_base_dir;
|
|
Path retro_save_dir;
|
|
const char* dir_ptr = NULL;
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY, &dir_ptr) && dir_ptr)
|
|
retro_base_dir = Path(dir_ptr);
|
|
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY, &dir_ptr) && dir_ptr)
|
|
retro_save_dir = Path(dir_ptr);
|
|
|
|
retro_base_dir /= "PPSSPP";
|
|
|
|
// Check if '<system_dir>/PPSSPP/compat.ini' exists, if not we can assume
|
|
// the user is missing the assets entirely, so let's warn them about it.
|
|
if (!File::Exists(Path(retro_base_dir / "compat.ini")))
|
|
{
|
|
const char* str = "Core system files missing, expect bugs.";
|
|
unsigned msg_interface_version = 0;
|
|
environ_cb(RETRO_ENVIRONMENT_GET_MESSAGE_INTERFACE_VERSION, &msg_interface_version);
|
|
|
|
if (msg_interface_version >= 1)
|
|
{
|
|
retro_message_ext msg = {
|
|
str,
|
|
3000,
|
|
3,
|
|
RETRO_LOG_WARN,
|
|
RETRO_MESSAGE_TARGET_ALL,
|
|
RETRO_MESSAGE_TYPE_NOTIFICATION,
|
|
-1
|
|
};
|
|
environ_cb(RETRO_ENVIRONMENT_SET_MESSAGE_EXT, &msg);
|
|
}
|
|
else
|
|
{
|
|
retro_message msg = {
|
|
str,
|
|
180
|
|
};
|
|
environ_cb(RETRO_ENVIRONMENT_SET_MESSAGE, &msg);
|
|
}
|
|
|
|
// OSD messages should be kept pretty short, but
|
|
// let's give the user a bit more info in logs.
|
|
WARN_LOG(Log::System, "Please check the docs for more informations on how to install "
|
|
"the PPSSPP assets: https://docs.libretro.com/library/ppsspp/");
|
|
}
|
|
|
|
g_Config.currentDirectory = retro_base_dir;
|
|
g_Config.defaultCurrentDirectory = retro_base_dir;
|
|
g_Config.memStickDirectory = retro_save_dir;
|
|
g_Config.flash0Directory = retro_base_dir / "flash0";
|
|
g_Config.internalDataDirectory = retro_base_dir;
|
|
g_Config.bEnableNetworkChat = false;
|
|
g_Config.bDiscordRichPresence = false;
|
|
|
|
g_VFS.Register("", new DirectoryReader(retro_base_dir));
|
|
|
|
g_threadManager.Init(cpu_info.num_cores, cpu_info.logical_cpu_count);
|
|
|
|
init_output_audio_buffer(2048);
|
|
}
|
|
|
|
void retro_deinit(void)
|
|
{
|
|
g_threadManager.Teardown();
|
|
g_logManager.Shutdown();
|
|
log_cb = NULL;
|
|
|
|
libretro_supports_bitmasks = false;
|
|
|
|
VsyncSwapIntervalReset();
|
|
|
|
free_output_audio_buffer();
|
|
}
|
|
|
|
void retro_set_controller_port_device(unsigned port, unsigned device)
|
|
{
|
|
(void)port;
|
|
(void)device;
|
|
}
|
|
|
|
void retro_get_system_info(struct retro_system_info *info)
|
|
{
|
|
*info = {};
|
|
info->library_name = "PPSSPP";
|
|
info->library_version = PPSSPP_GIT_VERSION;
|
|
info->need_fullpath = true;
|
|
info->valid_extensions = "elf|iso|cso|prx|pbp|chd";
|
|
}
|
|
|
|
void retro_get_system_av_info(struct retro_system_av_info *info)
|
|
{
|
|
*info = {};
|
|
info->timing.fps = (60.0 / 1.001) / (double)vsyncSwapInterval;
|
|
info->timing.sample_rate = SAMPLERATE;
|
|
|
|
_dbg_assert_(g_Config.iInternalResolution != 0);
|
|
|
|
info->geometry.base_width = g_Config.iInternalResolution * NATIVEWIDTH;
|
|
info->geometry.base_height = g_Config.iInternalResolution * NATIVEHEIGHT;
|
|
info->geometry.max_width = g_Config.iInternalResolution * NATIVEWIDTH;
|
|
info->geometry.max_height = g_Config.iInternalResolution * NATIVEHEIGHT;
|
|
|
|
if (g_Config.bDisplayCropTo16x9)
|
|
info->geometry.base_height -= g_Config.iInternalResolution * 2;
|
|
|
|
info->geometry.aspect_ratio = (float)info->geometry.base_width / (float)info->geometry.base_height;
|
|
|
|
PSP_CoreParameter().pixelWidth = PSP_CoreParameter().renderWidth = info->geometry.base_width;
|
|
PSP_CoreParameter().pixelHeight = PSP_CoreParameter().renderHeight = info->geometry.base_height;
|
|
}
|
|
|
|
unsigned retro_api_version(void) { return RETRO_API_VERSION; }
|
|
|
|
namespace Libretro {
|
|
bool useEmuThread = false;
|
|
std::atomic<EmuThreadState> emuThreadState(EmuThreadState::DISABLED);
|
|
|
|
static std::thread emuThread;
|
|
static void EmuFrame() {
|
|
ctx->SetRenderTarget();
|
|
Draw::DrawContext *draw = ctx->GetDrawContext();
|
|
if (draw) {
|
|
draw->BeginFrame(Draw::DebugFlags::NONE);
|
|
}
|
|
|
|
const DisplayLayoutConfig &displayLayoutConfig = g_Config.GetDisplayLayoutConfig(g_display.GetDeviceOrientation());
|
|
if (gpu) {
|
|
gpu->BeginHostFrame(displayLayoutConfig);
|
|
}
|
|
|
|
PSP_RunLoopWhileState();
|
|
switch (coreState) {
|
|
case CORE_NEXTFRAME:
|
|
case CORE_POWERDOWN:
|
|
// Reached the end of the frame while running at full blast, all good. Set back to running for the next frame
|
|
coreState = CORE_RUNNING_CPU;
|
|
break;
|
|
default:
|
|
// We're not handling the various states used for debugging in the libretro port.
|
|
break;
|
|
}
|
|
|
|
if (gpu) {
|
|
gpu->EndHostFrame();
|
|
if (draw) {
|
|
gpu->PrepareCopyDisplayToOutput(displayLayoutConfig);
|
|
}
|
|
}
|
|
|
|
// gotta do the backbuffer bind somewhere.
|
|
using namespace Draw;
|
|
if (draw) {
|
|
draw->BindFramebufferAsRenderTarget(nullptr, {RPAction::CLEAR, RPAction::CLEAR, RPAction::CLEAR}, "BackBuffer");
|
|
}
|
|
|
|
if (gpu && draw) {
|
|
gpu->CopyDisplayToOutput(displayLayoutConfig);
|
|
}
|
|
|
|
if (draw) {
|
|
draw->EndFrame();
|
|
draw->Present(Draw::PresentMode::FIFO);
|
|
}
|
|
}
|
|
|
|
static void EmuThreadFunc() {
|
|
SetCurrentThreadName("EmuThread");
|
|
|
|
for (;;) {
|
|
switch ((EmuThreadState)emuThreadState)
|
|
{
|
|
case EmuThreadState::RUNNING:
|
|
EmuFrame();
|
|
break;
|
|
case EmuThreadState::PAUSED:
|
|
sleep_ms(1, "libretro-paused");
|
|
break;
|
|
case EmuThreadState::QUIT_REQUESTED:
|
|
ctx->NotifyEmuThreadExit();
|
|
emuThreadState = EmuThreadState::STOPPED;
|
|
return;
|
|
default:
|
|
_dbg_assert_(false);
|
|
return;
|
|
}
|
|
}
|
|
// Unreachable
|
|
}
|
|
|
|
void EmuThreadStart() {
|
|
EmuThreadState state = emuThreadState;
|
|
bool wasPaused = state == EmuThreadState::PAUSED;
|
|
|
|
if (state == EmuThreadState::RUNNING || (emuThread.joinable() && !wasPaused))
|
|
return;
|
|
|
|
emuThreadState = EmuThreadState::RUNNING;
|
|
|
|
if (!wasPaused)
|
|
{
|
|
ctx->ThreadStart();
|
|
emuThread = std::thread(&EmuThreadFunc);
|
|
}
|
|
}
|
|
|
|
void EmuThreadStop() {
|
|
if (emuThreadState != EmuThreadState::RUNNING)
|
|
return;
|
|
|
|
emuThreadState = EmuThreadState::QUIT_REQUESTED;
|
|
|
|
// Eat remaining frames.
|
|
while (ctx->ThreadFrame()) {}
|
|
|
|
emuThread.join();
|
|
emuThread = std::thread();
|
|
ctx->ThreadEnd();
|
|
}
|
|
|
|
void EmuThreadPause() {
|
|
if (emuThreadState != EmuThreadState::RUNNING)
|
|
return;
|
|
|
|
emuThreadState = EmuThreadState::PAUSED;
|
|
|
|
// Is this safe?
|
|
ctx->ThreadFrame(); // Eat 1 frame
|
|
|
|
while (emuThreadState != EmuThreadState::PAUSED)
|
|
sleep_ms(1, "libretro-pause-poll");
|
|
}
|
|
|
|
} // namespace Libretro
|
|
|
|
static void retro_check_backend(void) {
|
|
struct retro_variable var = {0};
|
|
|
|
var.key = "ppsspp_backend";
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) {
|
|
if (!strcmp(var.value, "auto"))
|
|
backend = RETRO_HW_CONTEXT_DUMMY;
|
|
else if (!strcmp(var.value, "opengl"))
|
|
backend = RETRO_HW_CONTEXT_OPENGL;
|
|
else if (!strcmp(var.value, "vulkan"))
|
|
backend = RETRO_HW_CONTEXT_VULKAN;
|
|
else if (!strcmp(var.value, "d3d11"))
|
|
backend = RETRO_HW_CONTEXT_D3D11;
|
|
else if (!strcmp(var.value, "none"))
|
|
backend = RETRO_HW_CONTEXT_NONE;
|
|
}
|
|
}
|
|
|
|
bool retro_load_game(const struct retro_game_info *game) {
|
|
retro_pixel_format fmt = retro_pixel_format::RETRO_PIXEL_FORMAT_XRGB8888;
|
|
if (!environ_cb(RETRO_ENVIRONMENT_SET_PIXEL_FORMAT, &fmt)) {
|
|
ERROR_LOG(Log::System, "XRGB8888 is not supported.\n");
|
|
return false;
|
|
}
|
|
|
|
retro_check_backend();
|
|
|
|
ctx = LibretroGraphicsContext::CreateGraphicsContext();
|
|
|
|
INFO_LOG(Log::System, "Using %s backend", ctx->Ident());
|
|
|
|
Core_SetGraphicsContext(ctx);
|
|
SetGPUBackend((GPUBackend)g_Config.iGPUBackend);
|
|
|
|
useEmuThread = ctx->GetGPUCore() == GPUCORE_GLES;
|
|
|
|
// default to interpreter to allow startup in platforms w/o JIT capability
|
|
// TODO: I guess we should auto detect? And also, default to IR Interpreter...
|
|
g_Config.iCpuCore = (int)CPUCore::INTERPRETER;
|
|
|
|
CoreParameter coreParam = {};
|
|
coreParam.enableSound = true;
|
|
coreParam.fileToStart = Path(game->path);
|
|
coreParam.startBreak = false;
|
|
coreParam.headLess = true; // really?
|
|
coreParam.graphicsContext = ctx;
|
|
coreParam.gpuCore = ctx->GetGPUCore();
|
|
check_variables(coreParam);
|
|
|
|
// TODO: OpenGL goes black when inited with software rendering,
|
|
// therefore start without, set back after init, and reset.
|
|
softwareRenderInitHack = ctx->GetGPUCore() == GPUCORE_GLES && g_Config.bSoftwareRendering;
|
|
if (softwareRenderInitHack)
|
|
g_Config.bSoftwareRendering = false;
|
|
|
|
// set cpuCore from libretro setting variable
|
|
coreParam.cpuCore = (CPUCore)g_Config.iCpuCore;
|
|
|
|
g_pendingBoot = true;
|
|
|
|
struct retro_core_option_display option_display;
|
|
|
|
// Show/hide 'MSAA' and 'Texture Shader' options, Vulkan only
|
|
option_display.visible = (g_Config.iGPUBackend == (int)GPUBackend::VULKAN);
|
|
option_display.key = "ppsspp_mulitsample_level";
|
|
environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display);
|
|
option_display.key = "ppsspp_texture_shader";
|
|
environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display);
|
|
|
|
// Show/hide 'Buffered Frames' option, Vulkan/GL only
|
|
option_display.visible = (g_Config.iGPUBackend == (int)GPUBackend::VULKAN ||
|
|
g_Config.iGPUBackend == (int)GPUBackend::OPENGL);
|
|
option_display.key = "ppsspp_inflight_frames";
|
|
environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display);
|
|
|
|
set_variable_visibility();
|
|
|
|
// NOTE: At this point we haven't really booted yet, but "in-game" we'll just keep polling
|
|
// PSP_InitUpdate until done.
|
|
|
|
// Launch the init process.
|
|
if (!PSP_InitStart(coreParam)) {
|
|
g_bootErrorString = coreParam.errorString;
|
|
// Can't really fail, the errors normally happen later during InitUpdate
|
|
ERROR_LOG(Log::Boot, "%s", g_bootErrorString.c_str());
|
|
g_pendingBoot = false;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void retro_unload_game(void) {
|
|
if (Libretro::useEmuThread) {
|
|
Libretro::EmuThreadStop();
|
|
}
|
|
|
|
PSP_Shutdown(true);
|
|
g_VFS.Clear();
|
|
|
|
delete ctx;
|
|
ctx = nullptr;
|
|
PSP_CoreParameter().graphicsContext = nullptr;
|
|
}
|
|
|
|
void retro_reset(void) {
|
|
PSP_Shutdown(true);
|
|
|
|
if (BootState::Complete != PSP_Init(PSP_CoreParameter(), &g_bootErrorString)) {
|
|
ERROR_LOG(Log::Boot, "%s", g_bootErrorString.c_str());
|
|
environ_cb(RETRO_ENVIRONMENT_SHUTDOWN, nullptr);
|
|
}
|
|
}
|
|
|
|
static void retro_input(void) {
|
|
unsigned i;
|
|
int16_t ret = 0;
|
|
// clang-format off
|
|
static struct
|
|
{
|
|
u32 retro;
|
|
u32 sceCtrl;
|
|
} map[] = {
|
|
{ RETRO_DEVICE_ID_JOYPAD_UP, CTRL_UP },
|
|
{ RETRO_DEVICE_ID_JOYPAD_DOWN, CTRL_DOWN },
|
|
{ RETRO_DEVICE_ID_JOYPAD_LEFT, CTRL_LEFT },
|
|
{ RETRO_DEVICE_ID_JOYPAD_RIGHT, CTRL_RIGHT },
|
|
{ RETRO_DEVICE_ID_JOYPAD_X, CTRL_TRIANGLE },
|
|
{ RETRO_DEVICE_ID_JOYPAD_A, CTRL_CIRCLE },
|
|
{ RETRO_DEVICE_ID_JOYPAD_B, CTRL_CROSS },
|
|
{ RETRO_DEVICE_ID_JOYPAD_Y, CTRL_SQUARE },
|
|
{ RETRO_DEVICE_ID_JOYPAD_L, CTRL_LTRIGGER },
|
|
{ RETRO_DEVICE_ID_JOYPAD_R, CTRL_RTRIGGER },
|
|
{ RETRO_DEVICE_ID_JOYPAD_START, CTRL_START },
|
|
{ RETRO_DEVICE_ID_JOYPAD_SELECT, CTRL_SELECT },
|
|
};
|
|
// clang-format on
|
|
|
|
input_poll_cb();
|
|
|
|
if (libretro_supports_bitmasks) {
|
|
ret = input_state_cb(0, RETRO_DEVICE_JOYPAD,
|
|
0, RETRO_DEVICE_ID_JOYPAD_MASK);
|
|
} else {
|
|
for (i = RETRO_DEVICE_ID_JOYPAD_B; i <= RETRO_DEVICE_ID_JOYPAD_R; i++)
|
|
if (input_state_cb(0, RETRO_DEVICE_JOYPAD, 0, i))
|
|
ret |= (1 << i);
|
|
}
|
|
|
|
for (i = 0; i < sizeof(map) / sizeof(*map); i++) {
|
|
bool pressed = ret & (1 << map[i].retro);
|
|
if (pressed) {
|
|
__CtrlUpdateButtons(map[i].sceCtrl, 0);
|
|
} else {
|
|
__CtrlUpdateButtons(0, map[i].sceCtrl);
|
|
}
|
|
}
|
|
|
|
float x_left = input_state_cb(0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_X) / 32767.0f;
|
|
float y_left = input_state_cb(0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_Y) / -32767.0f;
|
|
float x_right = input_state_cb(0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_RIGHT, RETRO_DEVICE_ID_ANALOG_X) / 32767.0f;
|
|
float y_right = input_state_cb(0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_RIGHT, RETRO_DEVICE_ID_ANALOG_Y) / -32767.0f;
|
|
|
|
// Analog circle vs square gate compensation,
|
|
// deadzone and sensitivity copied from ControlMapper.cpp's
|
|
// ConvertAnalogStick and MapAxisValue functions
|
|
const bool isCircular = g_Config.bAnalogIsCircular;
|
|
|
|
float norm = std::max(fabsf(x_left), fabsf(y_left));
|
|
|
|
if (norm == 0.0f)
|
|
{
|
|
__CtrlSetAnalogXY(CTRL_STICK_LEFT, x_left, y_left);
|
|
__CtrlSetAnalogXY(CTRL_STICK_RIGHT, x_right, y_right);
|
|
return;
|
|
}
|
|
|
|
if (isCircular)
|
|
{
|
|
float newNorm = sqrtf(x_left * x_left + y_left * y_left);
|
|
float factor = newNorm / norm;
|
|
x_left *= factor;
|
|
y_left *= factor;
|
|
norm = newNorm;
|
|
}
|
|
|
|
const float deadzone = g_Config.fAnalogDeadzone;
|
|
const float sensitivity = g_Config.fAnalogSensitivity;
|
|
const float sign = norm >= 0.0f ? 1.0f : -1.0f;
|
|
float mappedNorm = norm;
|
|
|
|
// Apply deadzone
|
|
mappedNorm = Libretro::clamp((fabsf(mappedNorm) - deadzone) / (1.0f - deadzone), 0.0f, 1.0f);
|
|
|
|
// Apply sensitivity
|
|
if (mappedNorm != 0.0f)
|
|
mappedNorm = Libretro::clamp(mappedNorm * sensitivity * sign, -1.0f, 1.0f);
|
|
|
|
x_left = Libretro::clamp(x_left / norm * mappedNorm, -1.0f, 1.0f);
|
|
y_left = Libretro::clamp(y_left / norm * mappedNorm, -1.0f, 1.0f);
|
|
|
|
__CtrlSetAnalogXY(CTRL_STICK_LEFT, x_left, y_left);
|
|
__CtrlSetAnalogXY(CTRL_STICK_RIGHT, x_right, y_right);
|
|
}
|
|
|
|
// Called every frame by retroarch.
|
|
void retro_run(void) {
|
|
if (g_pendingBoot) {
|
|
BootState state = PSP_InitUpdate(&g_bootErrorString);
|
|
switch (state) {
|
|
case BootState::Failed:
|
|
g_pendingBoot = false;
|
|
ERROR_LOG(Log::Boot, "%s", g_bootErrorString.c_str());
|
|
environ_cb(RETRO_ENVIRONMENT_SHUTDOWN, nullptr);
|
|
return;
|
|
case BootState::Booting:
|
|
// Not done yet. Do maintenance stuff and bail.
|
|
retro_input();
|
|
ctx->SwapBuffers();
|
|
return;
|
|
case BootState::Off:
|
|
// shouldn't happen.
|
|
_dbg_assert_(false);
|
|
return;
|
|
case BootState::Complete:
|
|
// done, continue.
|
|
break;
|
|
}
|
|
|
|
// BootState is BootState::Complete.
|
|
// Here's where we finish the boot process.
|
|
coreState = CORE_RUNNING_CPU;
|
|
g_bootErrorString.clear();
|
|
g_pendingBoot = false;
|
|
|
|
if (unserialize_data) {
|
|
retro_unserialize(unserialize_data, unserialize_size);
|
|
|
|
free(unserialize_data);
|
|
unserialize_data = NULL;
|
|
}
|
|
}
|
|
|
|
// TODO: This seems dubious.
|
|
if (softwareRenderInitHack) {
|
|
log_cb(RETRO_LOG_DEBUG, "Software rendering init hack for opengl triggered.\n");
|
|
softwareRenderInitHack = false;
|
|
g_Config.bSoftwareRendering = true;
|
|
retro_reset();
|
|
}
|
|
|
|
// Update setting if any have changed.
|
|
bool updated;
|
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE, &updated) && updated)
|
|
check_variables(PSP_CoreParameter());
|
|
else
|
|
check_dynamic_variables(PSP_CoreParameter());
|
|
|
|
// Process input.
|
|
retro_input();
|
|
|
|
// Handle thread pumping.
|
|
if (useEmuThread) {
|
|
if (emuThreadState == EmuThreadState::PAUSED) {
|
|
VsyncSwapIntervalDetect();
|
|
ctx->SwapBuffers();
|
|
return;
|
|
}
|
|
|
|
if (emuThreadState != EmuThreadState::RUNNING) {
|
|
EmuThreadStart();
|
|
}
|
|
|
|
if (!ctx->ThreadFrame()) {
|
|
// We're done processing the last frame from the emu thread.
|
|
VsyncSwapIntervalDetect();
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
EmuFrame();
|
|
|
|
VsyncSwapIntervalDetect();
|
|
ctx->SwapBuffers();
|
|
upload_output_audio_buffer();
|
|
}
|
|
|
|
unsigned retro_get_region(void) { return RETRO_REGION_NTSC; }
|
|
|
|
bool retro_load_game_special(unsigned type, const struct retro_game_info *info, size_t num) { return false; }
|
|
|
|
namespace SaveState
|
|
{
|
|
struct SaveStart
|
|
{
|
|
void DoState(PointerWrap &p);
|
|
};
|
|
} // namespace SaveState
|
|
|
|
size_t retro_serialize_size(void)
|
|
{
|
|
if (!gpu) // The HW renderer isn't ready on first pass.
|
|
return 134217728; // 128MB ought to be enough for anybody.
|
|
|
|
SaveState::SaveStart state;
|
|
// TODO: Libretro API extension to use the savestate queue
|
|
if (useEmuThread)
|
|
EmuThreadPause();
|
|
|
|
return (CChunkFileReader::MeasurePtr(state) + 0x800000) & ~0x7FFFFF;
|
|
// We don't unpause intentionally
|
|
}
|
|
|
|
bool retro_serialize(void *data, size_t size) {
|
|
if (!gpu) // The HW renderer isn't ready on first pass.
|
|
return false;
|
|
|
|
// TODO: Libretro API extension to use the savestate queue
|
|
if (useEmuThread) {
|
|
EmuThreadPause(); // Does nothing if already paused
|
|
}
|
|
|
|
size_t measuredSize;
|
|
SaveState::SaveStart state;
|
|
auto err = CChunkFileReader::MeasureAndSavePtr(state, (u8 **)&data, &measuredSize);
|
|
bool retVal = err == CChunkFileReader::ERROR_NONE;
|
|
|
|
if (useEmuThread) {
|
|
EmuThreadStart();
|
|
sleep_ms(4, "libretro-serialize");
|
|
}
|
|
|
|
return retVal;
|
|
}
|
|
|
|
bool retro_unserialize(const void *data, size_t size) {
|
|
// The HW renderer isn't ready on first pass.
|
|
// So we save the data until we are ready to use it.
|
|
if (!gpu) {
|
|
unserialize_data = malloc(size);
|
|
memcpy(unserialize_data, data, size);
|
|
return true;
|
|
}
|
|
|
|
// TODO: Libretro API extension to use the savestate queue
|
|
if (useEmuThread)
|
|
EmuThreadPause(); // Does nothing if already paused
|
|
|
|
std::string errorString;
|
|
SaveState::SaveStart state;
|
|
bool retVal = CChunkFileReader::LoadPtr((u8 *)data, size, state, &errorString)
|
|
== CChunkFileReader::ERROR_NONE;
|
|
|
|
if (useEmuThread) {
|
|
EmuThreadStart();
|
|
sleep_ms(4, "libretro-unserialize");
|
|
}
|
|
|
|
return retVal;
|
|
}
|
|
|
|
void *retro_get_memory_data(unsigned id)
|
|
{
|
|
if ( id == RETRO_MEMORY_SYSTEM_RAM )
|
|
return Memory::GetPointerWriteUnchecked(PSP_GetKernelMemoryBase()) ;
|
|
return NULL;
|
|
}
|
|
|
|
size_t retro_get_memory_size(unsigned id)
|
|
{
|
|
if ( id == RETRO_MEMORY_SYSTEM_RAM )
|
|
return Memory::g_MemorySize ;
|
|
return 0;
|
|
}
|
|
|
|
void retro_cheat_reset(void) {
|
|
// Init Cheat Engine
|
|
CWCheatEngine *cheatEngine = new CWCheatEngine(g_paramSFO.GetDiscID());
|
|
Path file=cheatEngine->CheatFilename();
|
|
|
|
// Output cheats to cheat file
|
|
FILE *outFile = File::OpenCFile(file, "wb");
|
|
if (outFile != nullptr) {
|
|
fprintf(outFile, "_S %s\n", g_paramSFO.GetDiscID().c_str());
|
|
fclose(outFile);
|
|
}
|
|
|
|
g_Config.bReloadCheats = true;
|
|
|
|
// Parse and Run the Cheats
|
|
cheatEngine->ParseCheats();
|
|
if (cheatEngine->HasCheats()) {
|
|
cheatEngine->Run();
|
|
}
|
|
|
|
}
|
|
|
|
void retro_cheat_set(unsigned index, bool enabled, const char *code) {
|
|
// Initialize Cheat Engine
|
|
CWCheatEngine *cheatEngine = new CWCheatEngine(g_paramSFO.GetDiscID());
|
|
cheatEngine->CreateCheatFile();
|
|
Path file=cheatEngine->CheatFilename();
|
|
|
|
// Read cheats file
|
|
std::vector<std::string> cheats;
|
|
std::string cheat_content;
|
|
FILE *inFile = File::OpenCFile(file, "rb");
|
|
if (inFile != nullptr) {
|
|
std::array<uint8_t, 4096> buffer;
|
|
for (;;) {
|
|
size_t n = fread(buffer.data(), 1, buffer.size(), inFile);
|
|
cheat_content.append((const char *)buffer.data(), n);
|
|
if (n < buffer.size()) {
|
|
break;
|
|
}
|
|
}
|
|
fclose(inFile);
|
|
}
|
|
std::string existing_cheats=ReplaceAll(cheat_content, std::string("\n_C"), std::string("|"));
|
|
SplitString(existing_cheats, '|', cheats);
|
|
|
|
// Generate Cheat String
|
|
std::stringstream cheat("");
|
|
cheat << (enabled ? "1 " : "0 ") << index << std::endl;
|
|
std::string code_str(code);
|
|
std::vector<std::string> codes;
|
|
code_str=ReplaceAll(code_str, std::string(" "), std::string("+"));
|
|
SplitString(code_str, '+', codes);
|
|
int part=0;
|
|
for (int i=0; i < codes.size(); i++) {
|
|
if (codes[i].size() <= 2) {
|
|
// _L _M ..etc
|
|
// Assume _L
|
|
} else if (part == 0) {
|
|
cheat << "_L " << codes[i] << " ";
|
|
part++;
|
|
} else {
|
|
cheat << codes[i] << std::endl;
|
|
part=0;
|
|
}
|
|
}
|
|
|
|
// Add or Replace the Cheat
|
|
if (index + 1 < cheats.size()) {
|
|
cheats[index + 1]=cheat.str();
|
|
} else {
|
|
cheats.push_back(cheat.str());
|
|
}
|
|
|
|
// Output cheats to cheat file
|
|
FILE *outFile = File::OpenCFile(file, "wb");
|
|
if (outFile != nullptr) {
|
|
fprintf(outFile, "_S %s\n", g_paramSFO.GetDiscID().c_str());
|
|
for (const std::string &cheat : cheats) {
|
|
fprintf(outFile, "_C%s\n", cheat.c_str());
|
|
}
|
|
fclose(outFile);
|
|
}
|
|
|
|
g_Config.bReloadCheats = true;
|
|
|
|
// Parse and Run the Cheats
|
|
cheatEngine->ParseCheats();
|
|
if (cheatEngine->HasCheats()) {
|
|
cheatEngine->Run();
|
|
}
|
|
}
|
|
|
|
int64_t System_GetPropertyInt(SystemProperty prop) {
|
|
switch (prop) {
|
|
case SYSPROP_AUDIO_SAMPLE_RATE:
|
|
return SAMPLERATE;
|
|
#if PPSSPP_PLATFORM(ANDROID)
|
|
case SYSPROP_SYSTEMVERSION: {
|
|
char sdk[PROP_VALUE_MAX] = {0};
|
|
if (__system_property_get("ro.build.version.sdk", sdk) != 0) {
|
|
return atoi(sdk);
|
|
}
|
|
return -1;
|
|
}
|
|
#endif
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
float System_GetPropertyFloat(SystemProperty prop) {
|
|
switch (prop) {
|
|
case SYSPROP_DISPLAY_REFRESH_RATE:
|
|
return 60.0f / 1.001f;
|
|
case SYSPROP_DISPLAY_SAFE_INSET_LEFT:
|
|
case SYSPROP_DISPLAY_SAFE_INSET_RIGHT:
|
|
case SYSPROP_DISPLAY_SAFE_INSET_TOP:
|
|
case SYSPROP_DISPLAY_SAFE_INSET_BOTTOM:
|
|
return 0.0f;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
bool System_GetPropertyBool(SystemProperty prop) {
|
|
switch (prop) {
|
|
case SYSPROP_CAN_JIT:
|
|
#if PPSSPP_PLATFORM(IOS)
|
|
bool can_jit;
|
|
return (environ_cb(RETRO_ENVIRONMENT_GET_JIT_CAPABLE, &can_jit) && can_jit);
|
|
#else
|
|
return true;
|
|
#endif
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
std::string System_GetProperty(SystemProperty prop) { return ""; }
|
|
std::vector<std::string> System_GetPropertyStringVec(SystemProperty prop) { return std::vector<std::string>(); }
|
|
|
|
void System_Notify(SystemNotification notification) {
|
|
switch (notification) {
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
bool System_MakeRequest(SystemRequestType type, int requestId, const std::string ¶m1, const std::string ¶m2, int64_t param3, int64_t param4) { return false; }
|
|
void System_PostUIMessage(UIMessage message, std::string_view param) {}
|
|
void System_RunOnMainThread(std::function<void()>) {}
|
|
void NativeFrame(GraphicsContext *graphicsContext) {}
|
|
void NativeResized() {}
|
|
// Stubs to let things link - libretro builds Core.cpp and Breakpoints.cpp, which call into the
|
|
// WebSocket debugger, but doesn't build Core/Debugger/WebSocket.cpp itself.
|
|
void WebSocketDebuggerTick() {}
|
|
bool WebSocketDebuggerHasClients() { return false; }
|
|
void WebSocketNotifyBreakpointHit(const BreakpointHit &hit) {}
|
|
void System_Toast(std::string_view str) {}
|
|
|
|
inline int16_t Clamp16(int32_t sample) {
|
|
if (sample < -32767) return -32767;
|
|
if (sample > 32767) return 32767;
|
|
return sample;
|
|
}
|
|
|
|
void System_AudioPushSamples(const int32_t *audio, int numSamples, float volume) {
|
|
// We ignore volume here, because it's handled by libretro presumably.
|
|
|
|
std::lock_guard<std::mutex> lock(output_audio_buffer_mutex);
|
|
|
|
// Convert to 16-bit audio for further processing.
|
|
int16_t buffer[1024 * 2];
|
|
int origSamples = numSamples * 2;
|
|
|
|
while (numSamples > 0) {
|
|
int blockSize = std::min(1024, numSamples);
|
|
for (int i = 0; i < blockSize; i++) {
|
|
buffer[i * 2] = Clamp16(audio[i * 2]);
|
|
buffer[i * 2 + 1] = Clamp16(audio[i * 2 + 1]);
|
|
}
|
|
|
|
numSamples -= blockSize;
|
|
}
|
|
|
|
if (output_audio_buffer.capacity - output_audio_buffer.size < origSamples)
|
|
ensure_output_audio_buffer_capacity((output_audio_buffer.capacity + origSamples) * 1.5);
|
|
memcpy(output_audio_buffer.data + output_audio_buffer.size, buffer, origSamples * sizeof(*output_audio_buffer.data));
|
|
output_audio_buffer.size += origSamples;
|
|
}
|
|
|
|
void System_AudioGetDebugStats(char *buf, size_t bufSize) { if (buf) buf[0] = '\0'; }
|
|
void System_AudioClear() {}
|
|
#if PPSSPP_PLATFORM(ANDROID) || PPSSPP_PLATFORM(IOS)
|
|
bool System_AudioRecordingIsAvailable() { return false; }
|
|
bool System_AudioRecordingState() { return false; }
|
|
#endif
|
|
// Stub for now.
|
|
std::vector<std::string> System_GetCameraDeviceList() { return std::vector<std::string>(); }
|
|
|
|
// TODO: To avoid having to define these here, these should probably be turned into system "requests".
|
|
bool NativeSaveSecret(std::string_view nameOfSecret, std::string_view data) { return false; }
|
|
std::string NativeLoadSecret(std::string_view nameOfSecret) {
|
|
return "";
|
|
}
|