#include "libretro_frontend.h" #include #include #include #include #include #include #include #include #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(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(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::duration(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 lock(window_mutex_); if (window_) { ANativeWindow_release(window_); } window_ = window; if (window_) { ANativeWindow_acquire(window_); } } void LibretroFrontend::releaseWindow() { std::lock_guard 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 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(buffer.bits); auto* src = static_cast(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(data); *dir = fe.system_dir_.c_str(); return true; } case RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY: { auto** dir = static_cast(data); *dir = fe.save_dir_.c_str(); return true; } case RETRO_ENVIRONMENT_GET_CORE_ASSETS_DIRECTORY: { auto** dir = static_cast(data); *dir = fe.system_dir_.c_str(); return true; } case RETRO_ENVIRONMENT_SET_PIXEL_FORMAT: { auto* fmt = static_cast(data); fe.pixel_format_ = *fmt; LOGD("Pixel format set to: %u", *fmt); return true; } case RETRO_ENVIRONMENT_GET_VARIABLE: { auto* var = static_cast(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(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(data); *updated = fe.options_updated_.exchange(false); return true; } case RETRO_ENVIRONMENT_GET_CAN_DUPE: { auto* can = static_cast(data); *can = true; return true; } case RETRO_ENVIRONMENT_GET_LOG_INTERFACE: { auto* cb = static_cast(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(data); *version = 0; // use v0 variables API (simplest) return true; } case RETRO_ENVIRONMENT_GET_AUDIO_VIDEO_ENABLE: { auto* bits = static_cast(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(data); *caps = (1 << RETRO_DEVICE_JOYPAD) | (1 << RETRO_DEVICE_ANALOG); return true; } case RETRO_ENVIRONMENT_SET_GEOMETRY: { auto* geom = static_cast(data); LOGD("Geometry changed: %ux%u", geom->base_width, geom->base_height); return true; } case RETRO_ENVIRONMENT_GET_LANGUAGE: { auto* lang = static_cast(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 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(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 buf(size); in.read(reinterpret_cast(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; }