Initial commit: multi-platform emulator with PS1 support

Jetpack Compose app with adaptive layouts (phone/tablet/foldable) using
Window Size Classes. Integrates a C++ libretro frontend via JNI for
actual PS1 emulation using pcsx_rearmed, with AAudio output and
ANativeWindow rendering.

- Emulation core architecture with interface for NES, SNES, N64,
  Genesis, PS1, PS2 (PS1 implemented, others placeholder)
- Touch controller overlay with D-pad (diagonal support), face buttons,
  shoulders, and analog sticks
- Bluetooth/USB controller detection and input mapping
- Customizable controller layout editor with drag-to-reposition
- Game library with auto-scan of /sdcard/Games/ subfolders
- Auto-import of libretro core .so from /sdcard/Games/cores/
- Save states and SRAM persistence
This commit is contained in:
2026-04-08 20:34:37 -04:00
commit 39e3f8b7c0
67 changed files with 4520 additions and 0 deletions

View File

@@ -0,0 +1,556 @@
#include "libretro_frontend.h"
#include <android/log.h>
#include <android/native_window.h>
#include <dlfcn.h>
#include <chrono>
#include <cstdarg>
#include <cstring>
#include <fstream>
#include <vector>
#define LOG_TAG "LibretroFrontend"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
LibretroFrontend& LibretroFrontend::instance() {
static LibretroFrontend inst;
return inst;
}
LibretroFrontend::~LibretroFrontend() {
stop();
unloadCore();
releaseWindow();
}
// --- Core Loading ---
bool LibretroFrontend::loadCore(const char* soPath) {
unloadCore();
LOGI("Loading core: %s", soPath);
core_handle_ = dlopen(soPath, RTLD_LAZY);
if (!core_handle_) {
LOGE("dlopen failed: %s", dlerror());
return false;
}
#define LOAD_SYM(field, name) \
field = (decltype(field))dlsym(core_handle_, #name); \
if (!field) { LOGE("Missing symbol: %s", #name); unloadCore(); return false; }
LOAD_SYM(core_init_, retro_init);
LOAD_SYM(core_deinit_, retro_deinit);
LOAD_SYM(core_api_version_, retro_api_version);
LOAD_SYM(core_get_system_info_, retro_get_system_info);
LOAD_SYM(core_get_system_av_info_, retro_get_system_av_info);
LOAD_SYM(core_set_environment_, retro_set_environment);
LOAD_SYM(core_set_video_refresh_, retro_set_video_refresh);
LOAD_SYM(core_set_audio_sample_, retro_set_audio_sample);
LOAD_SYM(core_set_audio_sample_batch_, retro_set_audio_sample_batch);
LOAD_SYM(core_set_input_poll_, retro_set_input_poll);
LOAD_SYM(core_set_input_state_, retro_set_input_state);
LOAD_SYM(core_set_controller_port_device_, retro_set_controller_port_device);
LOAD_SYM(core_load_game_, retro_load_game);
LOAD_SYM(core_unload_game_, retro_unload_game);
LOAD_SYM(core_run_, retro_run);
LOAD_SYM(core_reset_, retro_reset);
LOAD_SYM(core_serialize_size_, retro_serialize_size);
LOAD_SYM(core_serialize_, retro_serialize);
LOAD_SYM(core_unserialize_, retro_unserialize);
LOAD_SYM(core_get_memory_data_, retro_get_memory_data);
LOAD_SYM(core_get_memory_size_, retro_get_memory_size);
#undef LOAD_SYM
// Set callbacks before init
core_set_environment_(environmentCallback);
core_init_();
core_set_video_refresh_(videoRefreshCallback);
core_set_audio_sample_(audioSampleCallback);
core_set_audio_sample_batch_(audioSampleBatchCallback);
core_set_input_poll_(inputPollCallback);
core_set_input_state_(inputStateCallback);
// Set controller to analog pad (DualShock) for PS1
core_set_controller_port_device_(0, RETRO_DEVICE_ANALOG);
retro_system_info sys_info{};
core_get_system_info_(&sys_info);
LOGI("Core loaded: %s %s", sys_info.library_name, sys_info.library_version);
LOGI(" need_fullpath: %d, block_extract: %d", sys_info.need_fullpath, sys_info.block_extract);
// Set default core options for pcsx_rearmed
core_options_["pcsx_rearmed_drc"] = "enabled";
core_options_["pcsx_rearmed_pad1type"] = "analog";
core_options_["pcsx_rearmed_frameskip_type"] = "disabled";
core_options_["pcsx_rearmed_region"] = "auto";
core_options_["pcsx_rearmed_memcard2"] = "disabled";
return true;
}
bool LibretroFrontend::loadGame(const char* romPath) {
if (!core_handle_) {
LOGE("No core loaded");
return false;
}
LOGI("Loading game: %s", romPath);
// pcsx_rearmed uses need_fullpath=true, so we just pass the path
retro_game_info game_info{};
game_info.path = romPath;
game_info.data = nullptr;
game_info.size = 0;
game_info.meta = nullptr;
if (!core_load_game_(&game_info)) {
LOGE("retro_load_game failed");
return false;
}
game_loaded_.store(true);
// Get AV info for timing and geometry
retro_system_av_info av_info{};
core_get_system_av_info_(&av_info);
target_fps_ = av_info.timing.fps;
double sample_rate = av_info.timing.sample_rate;
LOGI("Game loaded - %ux%u @ %.1f fps, audio %.0f Hz",
av_info.geometry.base_width, av_info.geometry.base_height,
target_fps_, sample_rate);
// Start audio
audio_engine_.start(static_cast<int32_t>(sample_rate));
return true;
}
// --- Lifecycle ---
void LibretroFrontend::start() {
if (running_.load()) return;
if (!game_loaded_.load()) {
LOGE("Cannot start: no game loaded");
return;
}
running_.store(true);
paused_.store(false);
emu_thread_ = std::thread(&LibretroFrontend::runLoop, this);
LOGI("Emulation started");
}
void LibretroFrontend::pause() {
paused_.store(true);
LOGD("Emulation paused");
}
void LibretroFrontend::resume() {
paused_.store(false);
LOGD("Emulation resumed");
}
void LibretroFrontend::stop() {
running_.store(false);
paused_.store(false);
if (emu_thread_.joinable()) {
emu_thread_.join();
}
// Save SRAM before unloading
if (game_loaded_.load() && core_handle_) {
void* sram = core_get_memory_data_(RETRO_MEMORY_SAVE_RAM);
size_t sram_size = core_get_memory_size_(RETRO_MEMORY_SAVE_RAM);
if (sram && sram_size > 0 && !save_dir_.empty()) {
std::string save_path = save_dir_ + "/game.srm";
std::ofstream out(save_path, std::ios::binary);
if (out) {
out.write(static_cast<const char*>(sram), sram_size);
LOGI("SRAM saved: %zu bytes", sram_size);
}
}
core_unload_game_();
game_loaded_.store(false);
}
audio_engine_.stop();
LOGI("Emulation stopped");
}
void LibretroFrontend::reset() {
if (core_handle_ && game_loaded_.load()) {
core_reset_();
LOGI("Emulation reset");
}
}
void LibretroFrontend::unloadCore() {
if (core_handle_) {
if (game_loaded_.load()) {
core_unload_game_();
game_loaded_.store(false);
}
core_deinit_();
dlclose(core_handle_);
core_handle_ = nullptr;
LOGI("Core unloaded");
}
}
// --- Emulation Loop ---
void LibretroFrontend::runLoop() {
using clock = std::chrono::high_resolution_clock;
auto frame_duration = std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::duration<double>(1.0 / target_fps_));
while (running_.load()) {
if (paused_.load()) {
std::this_thread::sleep_for(std::chrono::milliseconds(16));
continue;
}
auto frame_start = clock::now();
core_run_();
// Frame pacing
auto elapsed = clock::now() - frame_start;
if (elapsed < frame_duration) {
std::this_thread::sleep_for(frame_duration - elapsed);
}
}
}
// --- Video ---
void LibretroFrontend::setWindow(ANativeWindow* window) {
std::lock_guard<std::mutex> lock(window_mutex_);
if (window_) {
ANativeWindow_release(window_);
}
window_ = window;
if (window_) {
ANativeWindow_acquire(window_);
}
}
void LibretroFrontend::releaseWindow() {
std::lock_guard<std::mutex> lock(window_mutex_);
if (window_) {
ANativeWindow_release(window_);
window_ = nullptr;
}
}
void LibretroFrontend::videoRefreshCallback(const void* data, unsigned width, unsigned height, size_t pitch) {
if (!data) return; // dupe frame
auto& fe = instance();
std::lock_guard<std::mutex> lock(fe.window_mutex_);
if (!fe.window_) return;
int32_t windowFormat;
int bytesPerPixel;
switch (fe.pixel_format_) {
case RETRO_PIXEL_FORMAT_RGB565:
windowFormat = AHARDWAREBUFFER_FORMAT_R5G6B5_UNORM;
bytesPerPixel = 2;
break;
case RETRO_PIXEL_FORMAT_XRGB8888:
windowFormat = AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM;
bytesPerPixel = 4;
break;
case RETRO_PIXEL_FORMAT_0RGB1555:
default:
windowFormat = AHARDWAREBUFFER_FORMAT_R5G6B5_UNORM;
bytesPerPixel = 2;
break;
}
ANativeWindow_setBuffersGeometry(fe.window_, width, height, windowFormat);
ANativeWindow_Buffer buffer;
if (ANativeWindow_lock(fe.window_, &buffer, nullptr) != 0) return;
auto* dst = static_cast<uint8_t*>(buffer.bits);
auto* src = static_cast<const uint8_t*>(data);
size_t dstStride = buffer.stride * bytesPerPixel;
size_t copyWidth = width * bytesPerPixel;
for (unsigned y = 0; y < height; y++) {
memcpy(dst + y * dstStride, src + y * pitch, copyWidth);
}
ANativeWindow_unlockAndPost(fe.window_);
}
// --- Audio ---
size_t LibretroFrontend::audioSampleBatchCallback(const int16_t* data, size_t frames) {
instance().audio_engine_.pushSamples(data, frames);
return frames;
}
void LibretroFrontend::audioSampleCallback(int16_t left, int16_t right) {
int16_t buf[2] = {left, right};
instance().audio_engine_.pushSamples(buf, 1);
}
// --- Input ---
void LibretroFrontend::setButtonState(uint16_t state) {
button_state_.store(state);
}
void LibretroFrontend::setAnalogState(int stick, int16_t x, int16_t y) {
if (stick == 0) {
analog_lx_.store(x);
analog_ly_.store(y);
} else {
analog_rx_.store(x);
analog_ry_.store(y);
}
}
void LibretroFrontend::inputPollCallback() {
// No-op: input state is always current via atomics
}
int16_t LibretroFrontend::inputStateCallback(unsigned port, unsigned device, unsigned index, unsigned id) {
if (port != 0) return 0;
auto& fe = instance();
if (device == RETRO_DEVICE_JOYPAD) {
return (fe.button_state_.load() >> id) & 1;
}
if (device == RETRO_DEVICE_ANALOG) {
if (index == RETRO_DEVICE_INDEX_ANALOG_LEFT) {
return (id == RETRO_DEVICE_ID_ANALOG_X) ? fe.analog_lx_.load() : fe.analog_ly_.load();
} else if (index == RETRO_DEVICE_INDEX_ANALOG_RIGHT) {
return (id == RETRO_DEVICE_ID_ANALOG_X) ? fe.analog_rx_.load() : fe.analog_ry_.load();
}
}
return 0;
}
// --- Environment ---
bool LibretroFrontend::environmentCallback(unsigned cmd, void* data) {
auto& fe = instance();
switch (cmd) {
case RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY: {
auto** dir = static_cast<const char**>(data);
*dir = fe.system_dir_.c_str();
return true;
}
case RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY: {
auto** dir = static_cast<const char**>(data);
*dir = fe.save_dir_.c_str();
return true;
}
case RETRO_ENVIRONMENT_GET_CORE_ASSETS_DIRECTORY: {
auto** dir = static_cast<const char**>(data);
*dir = fe.system_dir_.c_str();
return true;
}
case RETRO_ENVIRONMENT_SET_PIXEL_FORMAT: {
auto* fmt = static_cast<const unsigned*>(data);
fe.pixel_format_ = *fmt;
LOGD("Pixel format set to: %u", *fmt);
return true;
}
case RETRO_ENVIRONMENT_GET_VARIABLE: {
auto* var = static_cast<retro_variable*>(data);
if (var->key) {
auto it = fe.core_options_.find(var->key);
if (it != fe.core_options_.end()) {
var->value = it->second.c_str();
return true;
}
}
var->value = nullptr;
return false;
}
case RETRO_ENVIRONMENT_SET_VARIABLES: {
// Core is telling us what variables it supports
auto* vars = static_cast<const retro_variable*>(data);
while (vars && vars->key) {
// Only set defaults if we haven't already set a value
if (fe.core_options_.find(vars->key) == fe.core_options_.end()) {
// Parse default from the description (format: "Description; val1|val2|val3")
if (vars->value) {
std::string desc(vars->value);
auto semi = desc.find("; ");
if (semi != std::string::npos) {
std::string values = desc.substr(semi + 2);
auto pipe = values.find('|');
std::string first = (pipe != std::string::npos) ?
values.substr(0, pipe) : values;
fe.core_options_[vars->key] = first;
}
}
}
vars++;
}
return true;
}
case RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE: {
auto* updated = static_cast<bool*>(data);
*updated = fe.options_updated_.exchange(false);
return true;
}
case RETRO_ENVIRONMENT_GET_CAN_DUPE: {
auto* can = static_cast<bool*>(data);
*can = true;
return true;
}
case RETRO_ENVIRONMENT_GET_LOG_INTERFACE: {
auto* cb = static_cast<retro_log_callback*>(data);
cb->log = logCallback;
return true;
}
case RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS:
case RETRO_ENVIRONMENT_SET_CONTROLLER_INFO:
case RETRO_ENVIRONMENT_SET_MEMORY_MAPS:
case RETRO_ENVIRONMENT_SET_SUPPORT_NO_GAME:
case RETRO_ENVIRONMENT_SET_SUBSYSTEM_INFO:
case RETRO_ENVIRONMENT_SET_SERIALIZATION_QUIRKS:
case RETRO_ENVIRONMENT_SET_CORE_OPTIONS:
case RETRO_ENVIRONMENT_SET_CORE_OPTIONS_INTL:
case RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2:
case RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2_INTL:
case RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY:
case RETRO_ENVIRONMENT_SET_CORE_OPTIONS_UPDATE_DISPLAY_CALLBACK:
return true; // acknowledge but ignore
case RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION: {
auto* version = static_cast<unsigned*>(data);
*version = 0; // use v0 variables API (simplest)
return true;
}
case RETRO_ENVIRONMENT_GET_AUDIO_VIDEO_ENABLE: {
auto* bits = static_cast<int*>(data);
*bits = 3; // enable both audio (bit 1) and video (bit 0)
return true;
}
case RETRO_ENVIRONMENT_GET_INPUT_DEVICE_CAPABILITIES: {
auto* caps = static_cast<uint64_t*>(data);
*caps = (1 << RETRO_DEVICE_JOYPAD) | (1 << RETRO_DEVICE_ANALOG);
return true;
}
case RETRO_ENVIRONMENT_SET_GEOMETRY: {
auto* geom = static_cast<const retro_game_geometry*>(data);
LOGD("Geometry changed: %ux%u", geom->base_width, geom->base_height);
return true;
}
case RETRO_ENVIRONMENT_GET_LANGUAGE: {
auto* lang = static_cast<unsigned*>(data);
*lang = 0; // RETRO_LANGUAGE_ENGLISH
return true;
}
default:
LOGD("Unhandled env cmd: %u", cmd);
return false;
}
}
void LibretroFrontend::logCallback(enum retro_log_level level, const char* fmt, ...) {
va_list args;
va_start(args, fmt);
int priority;
switch (level) {
case RETRO_LOG_DEBUG: priority = ANDROID_LOG_DEBUG; break;
case RETRO_LOG_INFO: priority = ANDROID_LOG_INFO; break;
case RETRO_LOG_WARN: priority = ANDROID_LOG_WARN; break;
case RETRO_LOG_ERROR: priority = ANDROID_LOG_ERROR; break;
default: priority = ANDROID_LOG_VERBOSE; break;
}
__android_log_vprint(priority, "RetroCore", fmt, args);
va_end(args);
}
// --- Save States ---
bool LibretroFrontend::saveState(const char* path) {
if (!core_handle_ || !game_loaded_.load()) return false;
size_t size = core_serialize_size_();
if (size == 0) return false;
std::vector<uint8_t> buf(size);
if (!core_serialize_(buf.data(), size)) {
LOGE("retro_serialize failed");
return false;
}
std::ofstream out(path, std::ios::binary);
if (!out) {
LOGE("Failed to open save state file: %s", path);
return false;
}
out.write(reinterpret_cast<const char*>(buf.data()), size);
LOGI("State saved: %zu bytes -> %s", size, path);
return true;
}
bool LibretroFrontend::loadState(const char* path) {
if (!core_handle_ || !game_loaded_.load()) return false;
std::ifstream in(path, std::ios::binary | std::ios::ate);
if (!in) {
LOGE("Failed to open state file: %s", path);
return false;
}
size_t size = in.tellg();
in.seekg(0);
std::vector<uint8_t> buf(size);
in.read(reinterpret_cast<char*>(buf.data()), size);
if (!core_unserialize_(buf.data(), size)) {
LOGE("retro_unserialize failed");
return false;
}
LOGI("State loaded: %zu bytes <- %s", size, path);
return true;
}
// --- Config ---
void LibretroFrontend::setAudioEnabled(bool enabled) {
audio_engine_.setEnabled(enabled);
}
void LibretroFrontend::setFrameSkip(int skip) {
frame_skip_ = skip;
if (skip > 0) {
core_options_["pcsx_rearmed_frameskip_type"] = "auto";
core_options_["pcsx_rearmed_frameskip_threshold"] = "33";
} else {
core_options_["pcsx_rearmed_frameskip_type"] = "disabled";
}
options_updated_.store(true);
}
void LibretroFrontend::setSystemDir(const char* dir) {
system_dir_ = dir;
}
void LibretroFrontend::setSaveDir(const char* dir) {
save_dir_ = dir;
}