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:
22
app/src/main/cpp/CMakeLists.txt
Normal file
22
app/src/main/cpp/CMakeLists.txt
Normal file
@@ -0,0 +1,22 @@
|
||||
cmake_minimum_required(VERSION 3.22.1)
|
||||
project(emulate_native)
|
||||
|
||||
add_library(emulate_native SHARED
|
||||
jni_bridge.cpp
|
||||
libretro_frontend.cpp
|
||||
audio_engine.cpp
|
||||
)
|
||||
|
||||
target_include_directories(emulate_native PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
find_library(log-lib log)
|
||||
find_library(android-lib android)
|
||||
|
||||
target_link_libraries(emulate_native
|
||||
${log-lib}
|
||||
${android-lib}
|
||||
aaudio
|
||||
dl
|
||||
)
|
||||
|
||||
target_compile_options(emulate_native PRIVATE -std=c++20 -Wall -O2)
|
||||
114
app/src/main/cpp/audio_engine.cpp
Normal file
114
app/src/main/cpp/audio_engine.cpp
Normal file
@@ -0,0 +1,114 @@
|
||||
#include "audio_engine.h"
|
||||
#include <android/log.h>
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
#define LOG_TAG "AudioEngine"
|
||||
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
|
||||
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
|
||||
|
||||
// --- RingBuffer ---
|
||||
|
||||
void RingBuffer::push(const int16_t* data, size_t frames) {
|
||||
size_t wp = write_pos_.load(std::memory_order_relaxed);
|
||||
size_t rp = read_pos_.load(std::memory_order_acquire);
|
||||
|
||||
for (size_t i = 0; i < frames; i++) {
|
||||
size_t next = (wp + 1) % CAPACITY;
|
||||
if (next == rp) break; // full, drop samples
|
||||
buffer_[wp * 2] = data[i * 2];
|
||||
buffer_[wp * 2 + 1] = data[i * 2 + 1];
|
||||
wp = next;
|
||||
}
|
||||
write_pos_.store(wp, std::memory_order_release);
|
||||
}
|
||||
|
||||
size_t RingBuffer::pop(int16_t* out, size_t frames) {
|
||||
size_t rp = read_pos_.load(std::memory_order_relaxed);
|
||||
size_t wp = write_pos_.load(std::memory_order_acquire);
|
||||
|
||||
size_t read = 0;
|
||||
while (read < frames && rp != wp) {
|
||||
out[read * 2] = buffer_[rp * 2];
|
||||
out[read * 2 + 1] = buffer_[rp * 2 + 1];
|
||||
rp = (rp + 1) % CAPACITY;
|
||||
read++;
|
||||
}
|
||||
read_pos_.store(rp, std::memory_order_release);
|
||||
return read;
|
||||
}
|
||||
|
||||
size_t RingBuffer::available() const {
|
||||
size_t wp = write_pos_.load(std::memory_order_acquire);
|
||||
size_t rp = read_pos_.load(std::memory_order_acquire);
|
||||
return (wp >= rp) ? (wp - rp) : (CAPACITY - rp + wp);
|
||||
}
|
||||
|
||||
// --- AudioEngine ---
|
||||
|
||||
aaudio_data_callback_result_t AudioEngine::dataCallback(
|
||||
AAudioStream* /*stream*/, void* userData, void* audioData, int32_t numFrames) {
|
||||
auto* engine = static_cast<AudioEngine*>(userData);
|
||||
auto* output = static_cast<int16_t*>(audioData);
|
||||
|
||||
if (!engine->isEnabled()) {
|
||||
memset(output, 0, numFrames * 2 * sizeof(int16_t));
|
||||
return AAUDIO_CALLBACK_RESULT_CONTINUE;
|
||||
}
|
||||
|
||||
size_t read = engine->ringBuffer().pop(output, numFrames);
|
||||
// Fill remainder with silence to prevent noise
|
||||
if (read < static_cast<size_t>(numFrames)) {
|
||||
memset(output + read * 2, 0, (numFrames - read) * 2 * sizeof(int16_t));
|
||||
}
|
||||
return AAUDIO_CALLBACK_RESULT_CONTINUE;
|
||||
}
|
||||
|
||||
bool AudioEngine::start(int32_t sampleRate) {
|
||||
AAudioStreamBuilder* builder = nullptr;
|
||||
aaudio_result_t result = AAudio_createStreamBuilder(&builder);
|
||||
if (result != AAUDIO_OK) {
|
||||
LOGE("Failed to create stream builder: %d", result);
|
||||
return false;
|
||||
}
|
||||
|
||||
AAudioStreamBuilder_setDirection(builder, AAUDIO_DIRECTION_OUTPUT);
|
||||
AAudioStreamBuilder_setSharingMode(builder, AAUDIO_SHARING_MODE_SHARED);
|
||||
AAudioStreamBuilder_setSampleRate(builder, sampleRate);
|
||||
AAudioStreamBuilder_setChannelCount(builder, 2);
|
||||
AAudioStreamBuilder_setFormat(builder, AAUDIO_FORMAT_PCM_I16);
|
||||
AAudioStreamBuilder_setPerformanceMode(builder, AAUDIO_PERFORMANCE_MODE_LOW_LATENCY);
|
||||
AAudioStreamBuilder_setDataCallback(builder, dataCallback, this);
|
||||
|
||||
result = AAudioStreamBuilder_openStream(builder, &stream_);
|
||||
AAudioStreamBuilder_delete(builder);
|
||||
|
||||
if (result != AAUDIO_OK) {
|
||||
LOGE("Failed to open stream: %d", result);
|
||||
return false;
|
||||
}
|
||||
|
||||
result = AAudioStream_requestStart(stream_);
|
||||
if (result != AAUDIO_OK) {
|
||||
LOGE("Failed to start stream: %d", result);
|
||||
AAudioStream_close(stream_);
|
||||
stream_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
LOGD("Audio started: %d Hz stereo", sampleRate);
|
||||
return true;
|
||||
}
|
||||
|
||||
void AudioEngine::stop() {
|
||||
if (stream_) {
|
||||
AAudioStream_requestStop(stream_);
|
||||
AAudioStream_close(stream_);
|
||||
stream_ = nullptr;
|
||||
LOGD("Audio stopped");
|
||||
}
|
||||
}
|
||||
|
||||
void AudioEngine::pushSamples(const int16_t* data, size_t frames) {
|
||||
ring_buffer_.push(data, frames);
|
||||
}
|
||||
39
app/src/main/cpp/audio_engine.h
Normal file
39
app/src/main/cpp/audio_engine.h
Normal file
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include <aaudio/AAudio.h>
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
|
||||
// Lock-free ring buffer for audio samples (interleaved stereo int16)
|
||||
class RingBuffer {
|
||||
public:
|
||||
static constexpr size_t CAPACITY = 8192; // frames (each frame = 2 samples L+R)
|
||||
|
||||
void push(const int16_t* data, size_t frames);
|
||||
size_t pop(int16_t* out, size_t frames);
|
||||
size_t available() const;
|
||||
|
||||
private:
|
||||
int16_t buffer_[CAPACITY * 2]{}; // stereo
|
||||
std::atomic<size_t> read_pos_{0};
|
||||
std::atomic<size_t> write_pos_{0};
|
||||
};
|
||||
|
||||
class AudioEngine {
|
||||
public:
|
||||
bool start(int32_t sampleRate);
|
||||
void stop();
|
||||
void pushSamples(const int16_t* data, size_t frames);
|
||||
void setEnabled(bool enabled) { enabled_.store(enabled); }
|
||||
|
||||
RingBuffer& ringBuffer() { return ring_buffer_; }
|
||||
bool isEnabled() const { return enabled_.load(); }
|
||||
|
||||
private:
|
||||
AAudioStream* stream_ = nullptr;
|
||||
RingBuffer ring_buffer_;
|
||||
std::atomic<bool> enabled_{true};
|
||||
|
||||
static aaudio_data_callback_result_t dataCallback(
|
||||
AAudioStream* stream, void* userData, void* audioData, int32_t numFrames);
|
||||
};
|
||||
119
app/src/main/cpp/jni_bridge.cpp
Normal file
119
app/src/main/cpp/jni_bridge.cpp
Normal file
@@ -0,0 +1,119 @@
|
||||
#include "libretro_frontend.h"
|
||||
|
||||
#include <jni.h>
|
||||
#include <android/log.h>
|
||||
#include <android/native_window_jni.h>
|
||||
|
||||
#define LOG_TAG "JniBridge"
|
||||
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
|
||||
|
||||
extern "C" {
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_com_lazy_emulate_emulation_NativeLibretro_nativeLoadCore(JNIEnv* env, jobject, jstring corePath) {
|
||||
const char* path = env->GetStringUTFChars(corePath, nullptr);
|
||||
bool result = LibretroFrontend::instance().loadCore(path);
|
||||
env->ReleaseStringUTFChars(corePath, path);
|
||||
return result;
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_com_lazy_emulate_emulation_NativeLibretro_nativeLoadGame(JNIEnv* env, jobject, jstring romPath) {
|
||||
const char* path = env->GetStringUTFChars(romPath, nullptr);
|
||||
bool result = LibretroFrontend::instance().loadGame(path);
|
||||
env->ReleaseStringUTFChars(romPath, path);
|
||||
return result;
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_lazy_emulate_emulation_NativeLibretro_nativeStart(JNIEnv*, jobject) {
|
||||
LibretroFrontend::instance().start();
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_lazy_emulate_emulation_NativeLibretro_nativePause(JNIEnv*, jobject) {
|
||||
LibretroFrontend::instance().pause();
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_lazy_emulate_emulation_NativeLibretro_nativeResume(JNIEnv*, jobject) {
|
||||
LibretroFrontend::instance().resume();
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_lazy_emulate_emulation_NativeLibretro_nativeStop(JNIEnv*, jobject) {
|
||||
LibretroFrontend::instance().stop();
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_lazy_emulate_emulation_NativeLibretro_nativeReset(JNIEnv*, jobject) {
|
||||
LibretroFrontend::instance().reset();
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_lazy_emulate_emulation_NativeLibretro_nativeSetSurface(JNIEnv* env, jobject, jobject surface) {
|
||||
if (surface) {
|
||||
ANativeWindow* window = ANativeWindow_fromSurface(env, surface);
|
||||
LibretroFrontend::instance().setWindow(window);
|
||||
ANativeWindow_release(window); // setWindow does its own acquire
|
||||
} else {
|
||||
LibretroFrontend::instance().releaseWindow();
|
||||
}
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_lazy_emulate_emulation_NativeLibretro_nativeReleaseSurface(JNIEnv*, jobject) {
|
||||
LibretroFrontend::instance().releaseWindow();
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_lazy_emulate_emulation_NativeLibretro_nativeSetButtonState(JNIEnv*, jobject, jint state) {
|
||||
LibretroFrontend::instance().setButtonState(static_cast<uint16_t>(state));
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_lazy_emulate_emulation_NativeLibretro_nativeSetAnalogState(JNIEnv*, jobject, jint stick, jshort x, jshort y) {
|
||||
LibretroFrontend::instance().setAnalogState(stick, x, y);
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_com_lazy_emulate_emulation_NativeLibretro_nativeSaveState(JNIEnv* env, jobject, jstring path) {
|
||||
const char* p = env->GetStringUTFChars(path, nullptr);
|
||||
bool result = LibretroFrontend::instance().saveState(p);
|
||||
env->ReleaseStringUTFChars(path, p);
|
||||
return result;
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_com_lazy_emulate_emulation_NativeLibretro_nativeLoadState(JNIEnv* env, jobject, jstring path) {
|
||||
const char* p = env->GetStringUTFChars(path, nullptr);
|
||||
bool result = LibretroFrontend::instance().loadState(p);
|
||||
env->ReleaseStringUTFChars(path, p);
|
||||
return result;
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_lazy_emulate_emulation_NativeLibretro_nativeSetAudioEnabled(JNIEnv*, jobject, jboolean enabled) {
|
||||
LibretroFrontend::instance().setAudioEnabled(enabled);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_lazy_emulate_emulation_NativeLibretro_nativeSetFrameSkip(JNIEnv*, jobject, jint skip) {
|
||||
LibretroFrontend::instance().setFrameSkip(skip);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_lazy_emulate_emulation_NativeLibretro_nativeSetSystemDir(JNIEnv* env, jobject, jstring dir) {
|
||||
const char* d = env->GetStringUTFChars(dir, nullptr);
|
||||
LibretroFrontend::instance().setSystemDir(d);
|
||||
env->ReleaseStringUTFChars(dir, d);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_lazy_emulate_emulation_NativeLibretro_nativeSetSaveDir(JNIEnv* env, jobject, jstring dir) {
|
||||
const char* d = env->GetStringUTFChars(dir, nullptr);
|
||||
LibretroFrontend::instance().setSaveDir(d);
|
||||
env->ReleaseStringUTFChars(dir, d);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
245
app/src/main/cpp/libretro.h
Normal file
245
app/src/main/cpp/libretro.h
Normal file
@@ -0,0 +1,245 @@
|
||||
/* Minimal vendored libretro.h - stable C API for emulator cores.
|
||||
* Based on the canonical libretro API specification.
|
||||
* Only includes types/constants needed by our frontend. */
|
||||
|
||||
#ifndef LIBRETRO_H__
|
||||
#define LIBRETRO_H__
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define RETRO_API_VERSION 1
|
||||
|
||||
/* Pixel formats */
|
||||
#define RETRO_PIXEL_FORMAT_0RGB1555 0
|
||||
#define RETRO_PIXEL_FORMAT_XRGB8888 1
|
||||
#define RETRO_PIXEL_FORMAT_RGB565 2
|
||||
|
||||
/* Device types */
|
||||
#define RETRO_DEVICE_NONE 0
|
||||
#define RETRO_DEVICE_JOYPAD 1
|
||||
#define RETRO_DEVICE_MOUSE 2
|
||||
#define RETRO_DEVICE_KEYBOARD 3
|
||||
#define RETRO_DEVICE_LIGHTGUN 4
|
||||
#define RETRO_DEVICE_ANALOG 5
|
||||
|
||||
/* Joypad button IDs */
|
||||
#define RETRO_DEVICE_ID_JOYPAD_B 0
|
||||
#define RETRO_DEVICE_ID_JOYPAD_Y 1
|
||||
#define RETRO_DEVICE_ID_JOYPAD_SELECT 2
|
||||
#define RETRO_DEVICE_ID_JOYPAD_START 3
|
||||
#define RETRO_DEVICE_ID_JOYPAD_UP 4
|
||||
#define RETRO_DEVICE_ID_JOYPAD_DOWN 5
|
||||
#define RETRO_DEVICE_ID_JOYPAD_LEFT 6
|
||||
#define RETRO_DEVICE_ID_JOYPAD_RIGHT 7
|
||||
#define RETRO_DEVICE_ID_JOYPAD_A 8
|
||||
#define RETRO_DEVICE_ID_JOYPAD_X 9
|
||||
#define RETRO_DEVICE_ID_JOYPAD_L 10
|
||||
#define RETRO_DEVICE_ID_JOYPAD_R 11
|
||||
#define RETRO_DEVICE_ID_JOYPAD_L2 12
|
||||
#define RETRO_DEVICE_ID_JOYPAD_R2 13
|
||||
#define RETRO_DEVICE_ID_JOYPAD_L3 14
|
||||
#define RETRO_DEVICE_ID_JOYPAD_R3 15
|
||||
|
||||
/* Analog indices */
|
||||
#define RETRO_DEVICE_INDEX_ANALOG_LEFT 0
|
||||
#define RETRO_DEVICE_INDEX_ANALOG_RIGHT 1
|
||||
#define RETRO_DEVICE_ID_ANALOG_X 0
|
||||
#define RETRO_DEVICE_ID_ANALOG_Y 1
|
||||
|
||||
/* Memory types */
|
||||
#define RETRO_MEMORY_SAVE_RAM 0
|
||||
#define RETRO_MEMORY_RTC 1
|
||||
#define RETRO_MEMORY_SYSTEM_RAM 2
|
||||
#define RETRO_MEMORY_VIDEO_RAM 3
|
||||
|
||||
/* Environment callback commands */
|
||||
#define RETRO_ENVIRONMENT_SET_ROTATION 1
|
||||
#define RETRO_ENVIRONMENT_GET_OVERSCAN 2
|
||||
#define RETRO_ENVIRONMENT_GET_CAN_DUPE 3
|
||||
#define RETRO_ENVIRONMENT_SET_MESSAGE 6
|
||||
#define RETRO_ENVIRONMENT_SHUTDOWN 7
|
||||
#define RETRO_ENVIRONMENT_SET_PERFORMANCE_LEVEL 8
|
||||
#define RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY 9
|
||||
#define RETRO_ENVIRONMENT_SET_PIXEL_FORMAT 10
|
||||
#define RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS 11
|
||||
#define RETRO_ENVIRONMENT_SET_KEYBOARD_CALLBACK 12
|
||||
#define RETRO_ENVIRONMENT_SET_DISK_CONTROL_INTERFACE 13
|
||||
#define RETRO_ENVIRONMENT_SET_HW_RENDER 14
|
||||
#define RETRO_ENVIRONMENT_GET_VARIABLE 15
|
||||
#define RETRO_ENVIRONMENT_SET_VARIABLES 16
|
||||
#define RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE 17
|
||||
#define RETRO_ENVIRONMENT_SET_SUPPORT_NO_GAME 18
|
||||
#define RETRO_ENVIRONMENT_GET_LIBRETRO_PATH 19
|
||||
#define RETRO_ENVIRONMENT_SET_FRAME_TIME_CALLBACK 21
|
||||
#define RETRO_ENVIRONMENT_SET_AUDIO_CALLBACK 22
|
||||
#define RETRO_ENVIRONMENT_GET_RUMBLE_INTERFACE 23
|
||||
#define RETRO_ENVIRONMENT_GET_INPUT_DEVICE_CAPABILITIES 24
|
||||
#define RETRO_ENVIRONMENT_GET_LOG_INTERFACE 27
|
||||
#define RETRO_ENVIRONMENT_GET_PERF_INTERFACE 28
|
||||
#define RETRO_ENVIRONMENT_GET_CORE_ASSETS_DIRECTORY 30
|
||||
#define RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY 31
|
||||
#define RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO 32
|
||||
#define RETRO_ENVIRONMENT_SET_SUBSYSTEM_INFO 34
|
||||
#define RETRO_ENVIRONMENT_SET_CONTROLLER_INFO 35
|
||||
#define RETRO_ENVIRONMENT_SET_MEMORY_MAPS 36
|
||||
#define RETRO_ENVIRONMENT_SET_GEOMETRY 37
|
||||
#define RETRO_ENVIRONMENT_GET_USERNAME 38
|
||||
#define RETRO_ENVIRONMENT_GET_LANGUAGE 39
|
||||
#define RETRO_ENVIRONMENT_SET_SUPPORT_ACHIEVEMENTS 42
|
||||
#define RETRO_ENVIRONMENT_SET_SERIALIZATION_QUIRKS 44
|
||||
#define RETRO_ENVIRONMENT_GET_AUDIO_VIDEO_ENABLE 47
|
||||
#define RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION 52
|
||||
#define RETRO_ENVIRONMENT_SET_CORE_OPTIONS 53
|
||||
#define RETRO_ENVIRONMENT_SET_CORE_OPTIONS_INTL 54
|
||||
#define RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY 55
|
||||
#define RETRO_ENVIRONMENT_GET_PREFERRED_HW_RENDER 56
|
||||
#define RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2 67
|
||||
#define RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2_INTL 68
|
||||
#define RETRO_ENVIRONMENT_SET_CORE_OPTIONS_UPDATE_DISPLAY_CALLBACK 69
|
||||
|
||||
/* Log levels */
|
||||
enum retro_log_level {
|
||||
RETRO_LOG_DEBUG = 0,
|
||||
RETRO_LOG_INFO,
|
||||
RETRO_LOG_WARN,
|
||||
RETRO_LOG_ERROR
|
||||
};
|
||||
|
||||
typedef void (*retro_log_printf_t)(enum retro_log_level level, const char *fmt, ...);
|
||||
|
||||
struct retro_log_callback {
|
||||
retro_log_printf_t log;
|
||||
};
|
||||
|
||||
struct retro_variable {
|
||||
const char *key;
|
||||
const char *value;
|
||||
};
|
||||
|
||||
struct retro_game_info {
|
||||
const char *path;
|
||||
const void *data;
|
||||
size_t size;
|
||||
const char *meta;
|
||||
};
|
||||
|
||||
struct retro_system_timing {
|
||||
double fps;
|
||||
double sample_rate;
|
||||
};
|
||||
|
||||
struct retro_game_geometry {
|
||||
unsigned base_width;
|
||||
unsigned base_height;
|
||||
unsigned max_width;
|
||||
unsigned max_height;
|
||||
float aspect_ratio;
|
||||
};
|
||||
|
||||
struct retro_system_av_info {
|
||||
struct retro_game_geometry geometry;
|
||||
struct retro_system_timing timing;
|
||||
};
|
||||
|
||||
struct retro_system_info {
|
||||
const char *library_name;
|
||||
const char *library_version;
|
||||
const char *valid_extensions;
|
||||
bool need_fullpath;
|
||||
bool block_extract;
|
||||
};
|
||||
|
||||
struct retro_message {
|
||||
const char *msg;
|
||||
unsigned frames;
|
||||
};
|
||||
|
||||
struct retro_input_descriptor {
|
||||
unsigned port;
|
||||
unsigned device;
|
||||
unsigned index;
|
||||
unsigned id;
|
||||
const char *description;
|
||||
};
|
||||
|
||||
struct retro_core_option_value {
|
||||
const char *value;
|
||||
const char *label;
|
||||
};
|
||||
|
||||
struct retro_core_option_definition {
|
||||
const char *key;
|
||||
const char *desc;
|
||||
const char *info;
|
||||
struct retro_core_option_value values[128];
|
||||
const char *default_value;
|
||||
};
|
||||
|
||||
struct retro_core_option_v2_category {
|
||||
const char *key;
|
||||
const char *desc;
|
||||
const char *info;
|
||||
};
|
||||
|
||||
struct retro_core_option_v2_definition {
|
||||
const char *key;
|
||||
const char *desc;
|
||||
const char *desc_categorized;
|
||||
const char *info;
|
||||
const char *info_categorized;
|
||||
const char *category_key;
|
||||
struct retro_core_option_value values[128];
|
||||
const char *default_value;
|
||||
};
|
||||
|
||||
struct retro_core_options_v2 {
|
||||
struct retro_core_option_v2_category *categories;
|
||||
struct retro_core_option_v2_definition *definitions;
|
||||
};
|
||||
|
||||
struct retro_core_options_v2_intl {
|
||||
struct retro_core_options_v2 *us;
|
||||
struct retro_core_options_v2 *local;
|
||||
};
|
||||
|
||||
/* Callback types */
|
||||
typedef bool (*retro_environment_t)(unsigned cmd, void *data);
|
||||
typedef void (*retro_video_refresh_t)(const void *data, unsigned width, unsigned height, size_t pitch);
|
||||
typedef void (*retro_audio_sample_t)(int16_t left, int16_t right);
|
||||
typedef size_t (*retro_audio_sample_batch_t)(const int16_t *data, size_t frames);
|
||||
typedef void (*retro_input_poll_t)(void);
|
||||
typedef int16_t (*retro_input_state_t)(unsigned port, unsigned device, unsigned index, unsigned id);
|
||||
|
||||
/* Core function pointer types */
|
||||
typedef void (*retro_set_environment_t)(retro_environment_t);
|
||||
typedef void (*retro_set_video_refresh_t)(retro_video_refresh_t);
|
||||
typedef void (*retro_set_audio_sample_t)(retro_audio_sample_t);
|
||||
typedef void (*retro_set_audio_sample_batch_t)(retro_audio_sample_batch_t);
|
||||
typedef void (*retro_set_input_poll_t)(retro_input_poll_t);
|
||||
typedef void (*retro_set_input_state_t)(retro_input_state_t);
|
||||
typedef void (*retro_init_t)(void);
|
||||
typedef void (*retro_deinit_t)(void);
|
||||
typedef unsigned (*retro_api_version_t)(void);
|
||||
typedef void (*retro_get_system_info_t)(struct retro_system_info *info);
|
||||
typedef void (*retro_get_system_av_info_t)(struct retro_system_av_info *info);
|
||||
typedef void (*retro_set_controller_port_device_t)(unsigned port, unsigned device);
|
||||
typedef void (*retro_reset_t)(void);
|
||||
typedef void (*retro_run_t)(void);
|
||||
typedef bool (*retro_load_game_t)(const struct retro_game_info *game);
|
||||
typedef void (*retro_unload_game_t)(void);
|
||||
typedef size_t (*retro_serialize_size_t)(void);
|
||||
typedef bool (*retro_serialize_t)(void *data, size_t size);
|
||||
typedef bool (*retro_unserialize_t)(const void *data, size_t size);
|
||||
typedef void *(*retro_get_memory_data_t)(unsigned id);
|
||||
typedef size_t (*retro_get_memory_size_t)(unsigned id);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* LIBRETRO_H__ */
|
||||
556
app/src/main/cpp/libretro_frontend.cpp
Normal file
556
app/src/main/cpp/libretro_frontend.cpp
Normal 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;
|
||||
}
|
||||
113
app/src/main/cpp/libretro_frontend.h
Normal file
113
app/src/main/cpp/libretro_frontend.h
Normal file
@@ -0,0 +1,113 @@
|
||||
#pragma once
|
||||
|
||||
#include "libretro.h"
|
||||
#include "audio_engine.h"
|
||||
|
||||
#include <android/native_window.h>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
|
||||
class LibretroFrontend {
|
||||
public:
|
||||
static LibretroFrontend& instance();
|
||||
|
||||
bool loadCore(const char* soPath);
|
||||
bool loadGame(const char* romPath);
|
||||
void start();
|
||||
void pause();
|
||||
void resume();
|
||||
void stop();
|
||||
void reset();
|
||||
|
||||
void setWindow(ANativeWindow* window);
|
||||
void releaseWindow();
|
||||
|
||||
void setButtonState(uint16_t state);
|
||||
void setAnalogState(int stick, int16_t x, int16_t y);
|
||||
|
||||
bool saveState(const char* path);
|
||||
bool loadState(const char* path);
|
||||
|
||||
void setAudioEnabled(bool enabled);
|
||||
void setFrameSkip(int skip);
|
||||
|
||||
void setSystemDir(const char* dir);
|
||||
void setSaveDir(const char* dir);
|
||||
|
||||
bool isRunning() const { return running_.load(); }
|
||||
|
||||
private:
|
||||
LibretroFrontend() = default;
|
||||
~LibretroFrontend();
|
||||
|
||||
void runLoop();
|
||||
void unloadCore();
|
||||
|
||||
// Libretro callbacks (static because libretro uses C function pointers)
|
||||
static bool environmentCallback(unsigned cmd, void* data);
|
||||
static void videoRefreshCallback(const void* data, unsigned width, unsigned height, size_t pitch);
|
||||
static size_t audioSampleBatchCallback(const int16_t* data, size_t frames);
|
||||
static void audioSampleCallback(int16_t left, int16_t right);
|
||||
static void inputPollCallback();
|
||||
static int16_t inputStateCallback(unsigned port, unsigned device, unsigned index, unsigned id);
|
||||
static void logCallback(enum retro_log_level level, const char* fmt, ...);
|
||||
|
||||
// Core function pointers (resolved via dlsym)
|
||||
void* core_handle_ = nullptr;
|
||||
|
||||
retro_init_t core_init_ = nullptr;
|
||||
retro_deinit_t core_deinit_ = nullptr;
|
||||
retro_api_version_t core_api_version_ = nullptr;
|
||||
retro_get_system_info_t core_get_system_info_ = nullptr;
|
||||
retro_get_system_av_info_t core_get_system_av_info_ = nullptr;
|
||||
retro_set_environment_t core_set_environment_ = nullptr;
|
||||
retro_set_video_refresh_t core_set_video_refresh_ = nullptr;
|
||||
retro_set_audio_sample_t core_set_audio_sample_ = nullptr;
|
||||
retro_set_audio_sample_batch_t core_set_audio_sample_batch_ = nullptr;
|
||||
retro_set_input_poll_t core_set_input_poll_ = nullptr;
|
||||
retro_set_input_state_t core_set_input_state_ = nullptr;
|
||||
retro_set_controller_port_device_t core_set_controller_port_device_ = nullptr;
|
||||
retro_load_game_t core_load_game_ = nullptr;
|
||||
retro_unload_game_t core_unload_game_ = nullptr;
|
||||
retro_run_t core_run_ = nullptr;
|
||||
retro_reset_t core_reset_ = nullptr;
|
||||
retro_serialize_size_t core_serialize_size_ = nullptr;
|
||||
retro_serialize_t core_serialize_ = nullptr;
|
||||
retro_unserialize_t core_unserialize_ = nullptr;
|
||||
retro_get_memory_data_t core_get_memory_data_ = nullptr;
|
||||
retro_get_memory_size_t core_get_memory_size_ = nullptr;
|
||||
|
||||
// State
|
||||
std::atomic<bool> running_{false};
|
||||
std::atomic<bool> paused_{false};
|
||||
std::atomic<bool> game_loaded_{false};
|
||||
std::thread emu_thread_;
|
||||
|
||||
// Video
|
||||
ANativeWindow* window_ = nullptr;
|
||||
std::mutex window_mutex_;
|
||||
unsigned pixel_format_ = RETRO_PIXEL_FORMAT_RGB565;
|
||||
|
||||
// Audio
|
||||
AudioEngine audio_engine_;
|
||||
|
||||
// Input
|
||||
std::atomic<uint16_t> button_state_{0};
|
||||
std::atomic<int16_t> analog_lx_{0}, analog_ly_{0};
|
||||
std::atomic<int16_t> analog_rx_{0}, analog_ry_{0};
|
||||
|
||||
// Paths
|
||||
std::string system_dir_;
|
||||
std::string save_dir_;
|
||||
|
||||
// Frame timing
|
||||
double target_fps_ = 60.0;
|
||||
int frame_skip_ = 0;
|
||||
|
||||
// Core options
|
||||
std::unordered_map<std::string, std::string> core_options_;
|
||||
std::atomic<bool> options_updated_{false};
|
||||
};
|
||||
Reference in New Issue
Block a user