Import initial work on Android frontend

This commit is contained in:
Connor McLaughlin
2019-11-29 00:17:24 +10:00
parent ea0b13a05c
commit ea35c5f3bc
63 changed files with 3124 additions and 0 deletions

View File

@ -0,0 +1,12 @@
set(SRCS
android_audio_stream.cpp
android_audio_stream.h
android_host_interface.cpp
android_host_interface.h
android_gles2_host_display.cpp
android_gles2_host_display.h
main.cpp
)
add_library(duckstation-native SHARED ${SRCS})
target_link_libraries(duckstation-native PRIVATE android core common glad imgui EGL::EGL)

View File

@ -0,0 +1,68 @@
#include "android_audio_stream.h"
#include "YBaseLib/Assert.h"
#include "YBaseLib/Log.h"
Log_SetChannel(AndroidAudioStream);
AndroidAudioStream::AndroidAudioStream() = default;
AndroidAudioStream::~AndroidAudioStream()
{
if (m_is_open)
AndroidAudioStream::CloseDevice();
}
std::unique_ptr<AudioStream> AndroidAudioStream::Create()
{
return std::make_unique<AndroidAudioStream>();
}
bool AndroidAudioStream::OpenDevice()
{
DebugAssert(!m_is_open);
#if 0
SDL_AudioSpec spec = {};
spec.freq = m_output_sample_rate;
spec.channels = static_cast<Uint8>(m_channels);
spec.format = AUDIO_S16;
spec.samples = static_cast<Uint16>(m_buffer_size);
spec.callback = AudioCallback;
spec.userdata = static_cast<void*>(this);
SDL_AudioSpec obtained = {};
if (SDL_OpenAudio(&spec, &obtained) < 0)
{
Log_ErrorPrintf("SDL_OpenAudio failed");
return false;
}
#endif
m_is_open = true;
return true;
}
void AndroidAudioStream::PauseDevice(bool paused)
{
// SDL_PauseAudio(paused ? 1 : 0);
}
void AndroidAudioStream::CloseDevice()
{
DebugAssert(m_is_open);
// SDL_CloseAudio();
m_is_open = false;
}
#if 0
void AndroidAudioStream::AudioCallback(void* userdata, uint8_t* stream, int len)
{
AndroidAudioStream* const this_ptr = static_cast<AndroidAudioStream*>(userdata);
const u32 num_samples = len / sizeof(SampleType) / this_ptr->m_channels;
const u32 read_samples = this_ptr->ReadSamples(reinterpret_cast<SampleType*>(stream), num_samples);
const u32 silence_samples = num_samples - read_samples;
if (silence_samples > 0)
{
std::memset(reinterpret_cast<SampleType*>(stream) + (read_samples * this_ptr->m_channels), 0,
silence_samples * this_ptr->m_channels * sizeof(SampleType));
}
}
#endif

View File

@ -0,0 +1,21 @@
#pragma once
#include "common/audio_stream.h"
#include <cstdint>
class AndroidAudioStream final : public AudioStream
{
public:
AndroidAudioStream();
~AndroidAudioStream();
static std::unique_ptr<AudioStream> Create();
protected:
bool OpenDevice() override;
void PauseDevice(bool paused) override;
void CloseDevice() override;
// static void AudioCallback(void* userdata, uint8_t* stream, int len);
bool m_is_open = false;
};

View File

@ -0,0 +1,392 @@
#include "android_gles2_host_display.h"
#include "YBaseLib/Log.h"
#include <EGL/eglext.h>
#include <array>
#include <imgui.h>
#include <imgui_impl_opengl3.h>
#include <tuple>
Log_SetChannel(AndroidGLES2HostDisplay);
class AndroidGLES2HostDisplayTexture : public HostDisplayTexture
{
public:
AndroidGLES2HostDisplayTexture(GLuint id, u32 width, u32 height) : m_id(id), m_width(width), m_height(height) {}
~AndroidGLES2HostDisplayTexture() override { glDeleteTextures(1, &m_id); }
void* GetHandle() const override { return reinterpret_cast<void*>(static_cast<uintptr_t>(m_id)); }
u32 GetWidth() const override { return m_width; }
u32 GetHeight() const override { return m_height; }
GLuint GetGLID() const { return m_id; }
static std::unique_ptr<AndroidGLES2HostDisplayTexture> Create(u32 width, u32 height, const void* initial_data,
u32 initial_data_stride)
{
GLuint id;
glGenTextures(1, &id);
GLint old_texture_binding = 0;
glGetIntegerv(GL_TEXTURE_BINDING_2D, &old_texture_binding);
// TODO: Set pack width
Assert(!initial_data || initial_data_stride == (width * sizeof(u32)));
glBindTexture(GL_TEXTURE_2D, id);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, initial_data);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glBindTexture(GL_TEXTURE_2D, id);
return std::make_unique<AndroidGLES2HostDisplayTexture>(id, width, height);
}
private:
GLuint m_id;
u32 m_width;
u32 m_height;
};
AndroidGLES2HostDisplay::AndroidGLES2HostDisplay(ANativeWindow* window)
: m_window(window), m_window_width(ANativeWindow_getWidth(window)), m_window_height(ANativeWindow_getHeight(window))
{
}
AndroidGLES2HostDisplay::~AndroidGLES2HostDisplay()
{
if (m_egl_context != EGL_NO_CONTEXT)
{
m_display_program.Destroy();
ImGui_ImplOpenGL3_Shutdown();
eglMakeCurrent(m_egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
eglDestroyContext(m_egl_display, m_egl_context);
}
if (m_egl_surface != EGL_NO_SURFACE)
eglDestroySurface(m_egl_display, m_egl_surface);
}
HostDisplay::RenderAPI AndroidGLES2HostDisplay::GetRenderAPI() const
{
return HostDisplay::RenderAPI::OpenGLES;
}
void* AndroidGLES2HostDisplay::GetRenderDevice() const
{
return nullptr;
}
void* AndroidGLES2HostDisplay::GetRenderContext() const
{
return m_egl_context;
}
void* AndroidGLES2HostDisplay::GetRenderWindow() const
{
return m_window;
}
void AndroidGLES2HostDisplay::ChangeRenderWindow(void* new_window)
{
eglMakeCurrent(m_egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
DestroySurface();
m_window = static_cast<ANativeWindow*>(new_window);
if (!CreateSurface())
Panic("Failed to recreate surface after window change");
if (!eglMakeCurrent(m_egl_display, m_egl_surface, m_egl_surface, m_egl_context))
Panic("Failed to make context current after window change");
}
std::unique_ptr<HostDisplayTexture> AndroidGLES2HostDisplay::CreateTexture(u32 width, u32 height, const void* data,
u32 data_stride, bool dynamic)
{
return AndroidGLES2HostDisplayTexture::Create(width, height, data, data_stride);
}
void AndroidGLES2HostDisplay::UpdateTexture(HostDisplayTexture* texture, u32 x, u32 y, u32 width, u32 height,
const void* data, u32 data_stride)
{
AndroidGLES2HostDisplayTexture* tex = static_cast<AndroidGLES2HostDisplayTexture*>(texture);
Assert(data_stride == (width * sizeof(u32)));
GLint old_texture_binding = 0;
glGetIntegerv(GL_TEXTURE_BINDING_2D, &old_texture_binding);
glBindTexture(GL_TEXTURE_2D, tex->GetGLID());
glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, width, height, GL_RGBA, GL_UNSIGNED_BYTE, data);
glBindTexture(GL_TEXTURE_2D, old_texture_binding);
}
void AndroidGLES2HostDisplay::SetDisplayTexture(void* texture, s32 offset_x, s32 offset_y, s32 width, s32 height,
u32 texture_width, u32 texture_height, float aspect_ratio)
{
m_display_texture_id = static_cast<GLuint>(reinterpret_cast<uintptr_t>(texture));
m_display_offset_x = offset_x;
m_display_offset_y = offset_y;
m_display_width = width;
m_display_height = height;
m_display_texture_width = texture_width;
m_display_texture_height = texture_height;
m_display_aspect_ratio = aspect_ratio;
m_display_texture_changed = true;
}
void AndroidGLES2HostDisplay::SetDisplayLinearFiltering(bool enabled)
{
m_display_linear_filtering = enabled;
}
void AndroidGLES2HostDisplay::SetDisplayTopMargin(int height)
{
m_display_top_margin = height;
}
void AndroidGLES2HostDisplay::SetVSync(bool enabled)
{
eglSwapInterval(m_egl_display, enabled ? 1 : 0);
}
std::tuple<u32, u32> AndroidGLES2HostDisplay::GetWindowSize() const
{
return std::make_tuple(static_cast<u32>(m_window_width), static_cast<u32>(m_window_height));
}
void AndroidGLES2HostDisplay::WindowResized()
{
m_window_width = ANativeWindow_getWidth(m_window);
m_window_height = ANativeWindow_getHeight(m_window);
Log_InfoPrintf("WindowResized %dx%d", m_window_width, m_window_height);
}
const char* AndroidGLES2HostDisplay::GetGLSLVersionString() const
{
return "#version 100";
}
std::string AndroidGLES2HostDisplay::GetGLSLVersionHeader() const
{
return R"(
#version 100
precision highp float;
precision highp int;
)";
}
bool AndroidGLES2HostDisplay::CreateGLContext()
{
m_egl_display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
if (!m_egl_display)
{
Log_ErrorPrint("eglGetDisplay() failed");
return false;
}
EGLint egl_major_version, egl_minor_version;
if (!eglInitialize(m_egl_display, &egl_major_version, &egl_minor_version))
{
Log_ErrorPrint("eglInitialize() failed");
return false;
}
Log_InfoPrintf("EGL version %d.%d initialized", egl_major_version, egl_minor_version);
static constexpr std::array<int, 11> egl_surface_attribs = {{EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, EGL_RED_SIZE, 8,
EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_SURFACE_TYPE,
EGL_WINDOW_BIT, EGL_NONE}};
int num_m_egl_configs;
if (!eglChooseConfig(m_egl_display, egl_surface_attribs.data(), &m_egl_config, 1, &num_m_egl_configs))
{
Log_ErrorPrint("eglChooseConfig() failed");
return false;
}
eglBindAPI(EGL_OPENGL_ES_API);
static constexpr std::array<int, 3> egl_context_attribs = {{EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE}};
m_egl_context = eglCreateContext(m_egl_display, m_egl_config, EGL_NO_CONTEXT, egl_context_attribs.data());
if (!m_egl_context)
{
Log_ErrorPrint("eglCreateContext() failed");
return false;
}
if (!CreateSurface())
return false;
if (!eglMakeCurrent(m_egl_display, m_egl_surface, m_egl_surface, m_egl_context))
{
Log_ErrorPrint("eglMakeCurrent() failed");
return false;
}
// Load GLAD.
if (!gladLoadGLES2Loader(reinterpret_cast<GLADloadproc>(eglGetProcAddress)))
{
Log_ErrorPrintf("Failed to load GL functions");
return false;
}
return true;
}
bool AndroidGLES2HostDisplay::CreateSurface()
{
EGLint native_visual;
eglGetConfigAttrib(m_egl_display, m_egl_config, EGL_NATIVE_VISUAL_ID, &native_visual);
ANativeWindow_setBuffersGeometry(m_window, 0, 0, native_visual);
m_window_width = ANativeWindow_getWidth(m_window);
m_window_height = ANativeWindow_getHeight(m_window);
m_egl_surface = eglCreateWindowSurface(m_egl_display, m_egl_config, m_window, nullptr);
if (!m_egl_surface)
{
Log_ErrorPrint("eglCreateWindowSurface() failed");
return false;
}
WindowResized();
return true;
}
void AndroidGLES2HostDisplay::DestroySurface()
{
eglDestroySurface(m_egl_display, m_egl_surface);
m_egl_surface = EGL_NO_SURFACE;
}
bool AndroidGLES2HostDisplay::CreateImGuiContext()
{
if (!ImGui_ImplOpenGL3_Init(GetGLSLVersionString()))
return false;
ImGui_ImplOpenGL3_NewFrame();
ImGui::GetIO().DisplaySize.x = static_cast<float>(m_window_width);
ImGui::GetIO().DisplaySize.y = static_cast<float>(m_window_height);
return true;
}
bool AndroidGLES2HostDisplay::CreateGLResources()
{
static constexpr char fullscreen_quad_vertex_shader[] = R"(
attribute vec2 a_pos;
attribute vec2 a_tex0;
varying vec2 v_tex0;
void main()
{
v_tex0 = a_tex0;
gl_Position = vec4(a_pos, 0.0, 1.0);
}
)";
static constexpr char display_fragment_shader[] = R"(
uniform sampler2D samp0;
varying vec2 v_tex0;
void main()
{
gl_FragColor = texture2D(samp0, v_tex0);
}
)";
if (!m_display_program.Compile(GetGLSLVersionHeader() + fullscreen_quad_vertex_shader,
GetGLSLVersionHeader() + display_fragment_shader))
{
Log_ErrorPrintf("Failed to compile display shaders");
return false;
}
m_display_program.BindAttribute(0, "a_pos");
m_display_program.BindAttribute(1, "a_tex0");
if (!m_display_program.Link())
{
Log_ErrorPrintf("Failed to link display program");
return false;
}
m_display_program.Bind();
m_display_program.RegisterUniform("samp0");
m_display_program.Uniform1i(0, 0);
return true;
}
std::unique_ptr<HostDisplay> AndroidGLES2HostDisplay::Create(ANativeWindow* window)
{
std::unique_ptr<AndroidGLES2HostDisplay> display = std::make_unique<AndroidGLES2HostDisplay>(window);
if (!display->CreateGLContext() || !display->CreateImGuiContext() || !display->CreateGLResources())
return nullptr;
Log_DevPrintf("%dx%d display created", display->m_window_width, display->m_window_height);
return display;
}
void AndroidGLES2HostDisplay::Render()
{
glDisable(GL_SCISSOR_TEST);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
glClear(GL_COLOR_BUFFER_BIT);
RenderDisplay();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
eglSwapBuffers(m_egl_display, m_egl_surface);
ImGui_ImplOpenGL3_NewFrame();
GL::Program::ResetLastProgram();
}
void AndroidGLES2HostDisplay::RenderDisplay()
{
if (!m_display_texture_id)
return;
// - 20 for main menu padding
const auto [vp_left, vp_top, vp_width, vp_height] =
CalculateDrawRect(m_window_width, std::max(m_window_height - m_display_top_margin, 1), m_display_aspect_ratio);
glViewport(vp_left, m_window_height - (m_display_top_margin + vp_top) - vp_height, vp_width, vp_height);
glDisable(GL_BLEND);
glDisable(GL_CULL_FACE);
glDisable(GL_DEPTH_TEST);
glDisable(GL_SCISSOR_TEST);
glDepthMask(GL_FALSE);
m_display_program.Bind();
const float tex_left = static_cast<float>(m_display_offset_x) / static_cast<float>(m_display_texture_width);
const float tex_right = tex_left + static_cast<float>(m_display_width) / static_cast<float>(m_display_texture_width);
const float tex_top = static_cast<float>(m_display_offset_y) / static_cast<float>(m_display_texture_height);
const float tex_bottom =
tex_top + static_cast<float>(m_display_height) / static_cast<float>(m_display_texture_height);
const std::array<std::array<float, 4>, 4> vertices = {{
{{-1.0f, -1.0f, tex_left, tex_bottom}}, // bottom-left
{{1.0f, -1.0f, tex_right, tex_bottom}}, // bottom-right
{{-1.0f, 1.0f, tex_left, tex_top}}, // top-left
{{1.0f, 1.0f, tex_right, tex_top}}, // top-right
}};
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(vertices[0]), &vertices[0][0]);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(vertices[0]), &vertices[0][2]);
glEnableVertexAttribArray(1);
glBindTexture(GL_TEXTURE_2D, m_display_texture_id);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glDisableVertexAttribArray(1);
glDisableVertexAttribArray(0);
}

View File

@ -0,0 +1,78 @@
#pragma once
#include "common/gl/program.h"
#include "common/gl/texture.h"
#include "core/host_display.h"
#include <EGL/egl.h>
#include <android/native_window.h>
#include <glad.h>
#include <memory>
#include <string>
class AndroidGLES2HostDisplay final : public HostDisplay
{
public:
AndroidGLES2HostDisplay(ANativeWindow* window);
~AndroidGLES2HostDisplay();
static std::unique_ptr<HostDisplay> Create(ANativeWindow* window);
RenderAPI GetRenderAPI() const override;
void* GetRenderDevice() const override;
void* GetRenderContext() const override;
void* GetRenderWindow() const override;
void ChangeRenderWindow(void* new_window) override;
std::unique_ptr<HostDisplayTexture> CreateTexture(u32 width, u32 height, const void* data, u32 data_stride,
bool dynamic) override;
void UpdateTexture(HostDisplayTexture* texture, u32 x, u32 y, u32 width, u32 height, const void* data,
u32 data_stride) override;
void SetDisplayTexture(void* texture, s32 offset_x, s32 offset_y, s32 width, s32 height, u32 texture_width,
u32 texture_height, float aspect_ratio) override;
void SetDisplayLinearFiltering(bool enabled) override;
void SetDisplayTopMargin(int height) override;
void SetVSync(bool enabled) override;
void Render() override;
std::tuple<u32, u32> GetWindowSize() const override;
void WindowResized() override;
private:
const char* GetGLSLVersionString() const;
std::string GetGLSLVersionHeader() const;
bool CreateSurface();
void DestroySurface();
bool CreateGLContext();
bool CreateImGuiContext();
bool CreateGLResources();
void RenderDisplay();
ANativeWindow* m_window = nullptr;
int m_window_width = 0;
int m_window_height = 0;
EGLDisplay m_egl_display = EGL_NO_DISPLAY;
EGLSurface m_egl_surface = EGL_NO_SURFACE;
EGLContext m_egl_context = EGL_NO_CONTEXT;
EGLConfig m_egl_config = {};
GL::Program m_display_program;
GLuint m_display_texture_id = 0;
s32 m_display_offset_x = 0;
s32 m_display_offset_y = 0;
s32 m_display_width = 0;
s32 m_display_height = 0;
u32 m_display_texture_width = 0;
u32 m_display_texture_height = 0;
int m_display_top_margin = 0;
float m_display_aspect_ratio = 1.0f;
bool m_display_texture_changed = false;
bool m_display_linear_filtering = false;
};

View File

@ -0,0 +1,416 @@
#include "android_host_interface.h"
#include "YBaseLib/Assert.h"
#include "YBaseLib/Log.h"
#include "YBaseLib/String.h"
#include "android_audio_stream.h"
#include "android_gles2_host_display.h"
#include "core/gpu.h"
#include "core/host_display.h"
#include "core/system.h"
#include <android/native_window_jni.h>
#include <imgui.h>
Log_SetChannel(AndroidHostInterface);
static JavaVM* s_jvm;
static jclass s_AndroidHostInterface_class;
static jmethodID s_AndroidHostInterface_constructor;
static jfieldID s_AndroidHostInterface_field_nativePointer;
// helper for retrieving the current per-thread jni environment
static JNIEnv* GetJNIEnv()
{
JNIEnv* env;
if (s_jvm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK)
return nullptr;
else
return env;
}
static AndroidHostInterface* GetNativeClass(JNIEnv* env, jobject obj)
{
return reinterpret_cast<AndroidHostInterface*>(
static_cast<uintptr_t>(env->GetLongField(obj, s_AndroidHostInterface_field_nativePointer)));
}
static std::string JStringToString(JNIEnv* env, jstring str)
{
jsize length = env->GetStringUTFLength(str);
if (length == 0)
return {};
const char* data = env->GetStringUTFChars(str, nullptr);
Assert(data != nullptr);
std::string ret(data, length);
env->ReleaseStringUTFChars(str, data);
return ret;
}
AndroidHostInterface::AndroidHostInterface(jobject java_object) : m_java_object(java_object)
{
m_settings.SetDefaults();
m_settings.bios_path = "/sdcard/PSX/BIOS/scph1001.bin";
m_settings.memory_card_a_path = "/sdcard/PSX/memory_card_a.mcd";
m_settings.gpu_renderer = GPURenderer::Software;
m_settings.video_sync_enabled = true;
m_settings.audio_sync_enabled = false;
// m_settings.debugging.show_vram = true;
}
AndroidHostInterface::~AndroidHostInterface()
{
ImGui::DestroyContext();
GetJNIEnv()->DeleteGlobalRef(m_java_object);
}
void AndroidHostInterface::ReportError(const char* message)
{
HostInterface::ReportError(message);
}
void AndroidHostInterface::ReportMessage(const char* message)
{
HostInterface::ReportMessage(message);
}
bool AndroidHostInterface::StartEmulationThread(ANativeWindow* initial_surface, std::string initial_filename,
std::string initial_state_filename)
{
Assert(!IsEmulationThreadRunning());
Log_DevPrintf("Starting emulation thread...");
m_emulation_thread_stop_request.store(false);
m_emulation_thread = std::thread(&AndroidHostInterface::EmulationThreadEntryPoint, this, initial_surface,
std::move(initial_filename), std::move(initial_state_filename));
m_emulation_thread_started.Wait();
if (!m_emulation_thread_start_result.load())
{
m_emulation_thread.join();
Log_ErrorPrint("Failed to start emulation in thread");
return false;
}
return true;
}
void AndroidHostInterface::StopEmulationThread()
{
Assert(IsEmulationThreadRunning());
Log_InfoPrint("Stopping emulation thread...");
m_emulation_thread_stop_request.store(true);
m_emulation_thread.join();
Log_InfoPrint("Emulation thread stopped");
}
void AndroidHostInterface::RunOnEmulationThread(std::function<void()> function, bool blocking)
{
if (!IsEmulationThreadRunning())
{
function();
return;
}
m_callback_mutex.lock();
m_callback_queue.push_back(std::move(function));
if (blocking)
{
// TODO: Don't spin
for (;;)
{
if (m_callback_queue.empty())
break;
m_callback_mutex.unlock();
m_callback_mutex.lock();
}
}
m_callback_mutex.unlock();
}
void AndroidHostInterface::EmulationThreadEntryPoint(ANativeWindow* initial_surface, std::string initial_filename,
std::string initial_state_filename)
{
CreateImGuiContext();
// Create display.
m_display = AndroidGLES2HostDisplay::Create(initial_surface);
if (!m_display)
{
Log_ErrorPrint("Failed to create display on emulation thread.");
DestroyImGuiContext();
m_emulation_thread_start_result.store(false);
m_emulation_thread_started.Signal();
return;
}
// Create audio stream.
m_audio_stream = AndroidAudioStream::Create();
if (!m_audio_stream || !m_audio_stream->Reconfigure(44100, 2))
{
Log_ErrorPrint("Failed to create audio stream on emulation thread.");
m_audio_stream.reset();
m_display.reset();
DestroyImGuiContext();
m_emulation_thread_start_result.store(false);
m_emulation_thread_started.Signal();
return;
}
// Boot system.
if (!CreateSystem() || !BootSystem(initial_filename.empty() ? nullptr : initial_filename.c_str(),
initial_state_filename.empty() ? nullptr : initial_state_filename.c_str()))
{
Log_ErrorPrintf("Failed to boot system on emulation thread (file:%s state:%s).", initial_filename.c_str(),
initial_state_filename.c_str());
m_audio_stream.reset();
m_display.reset();
DestroyImGuiContext();
m_emulation_thread_start_result.store(false);
m_emulation_thread_started.Signal();
return;
}
// System is ready to go.
m_emulation_thread_start_result.store(true);
m_emulation_thread_started.Signal();
ImGui::NewFrame();
while (!m_emulation_thread_stop_request.load())
{
// run any events
m_callback_mutex.lock();
for (;;)
{
if (m_callback_queue.empty())
break;
auto callback = std::move(m_callback_queue.front());
m_callback_queue.pop_front();
m_callback_mutex.unlock();
callback();
m_callback_mutex.lock();
}
m_callback_mutex.unlock();
// simulate the system if not paused
if (m_system && !m_paused)
m_system->RunFrame();
// rendering
{
DrawImGui();
if (m_system)
m_system->GetGPU()->ResetGraphicsAPIState();
ImGui::Render();
m_display->Render();
ImGui::NewFrame();
if (m_system)
{
m_system->GetGPU()->RestoreGraphicsAPIState();
if (m_speed_limiter_enabled)
Throttle();
}
}
}
m_display.reset();
m_audio_stream.reset();
DestroyImGuiContext();
}
void AndroidHostInterface::CreateImGuiContext()
{
ImGui::CreateContext();
ImGui::GetIO().IniFilename = nullptr;
// ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;
// ImGui::GetIO().BackendFlags |= ImGuiBackendFlags_HasGamepad;
}
void AndroidHostInterface::DestroyImGuiContext()
{
ImGui::DestroyContext();
}
void AndroidHostInterface::DrawImGui()
{
DrawOSDMessages();
ImGui::Render();
}
void AndroidHostInterface::AddOSDMessage(const char* message, float duration)
{
OSDMessage msg;
msg.text = message;
msg.duration = duration;
std::unique_lock<std::mutex> lock(m_osd_messages_lock);
m_osd_messages.push_back(std::move(msg));
}
void AndroidHostInterface::DrawOSDMessages()
{
constexpr ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoInputs |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings |
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoNav |
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoFocusOnAppearing;
std::unique_lock<std::mutex> lock(m_osd_messages_lock);
const float scale = ImGui::GetIO().DisplayFramebufferScale.x;
auto iter = m_osd_messages.begin();
float position_x = 10.0f * scale;
float position_y = (10.0f + (m_settings.display_fullscreen ? 0.0f : 20.0f)) * scale;
u32 index = 0;
while (iter != m_osd_messages.end())
{
const OSDMessage& msg = *iter;
const double time = msg.time.GetTimeSeconds();
const float time_remaining = static_cast<float>(msg.duration - time);
if (time_remaining <= 0.0f)
{
iter = m_osd_messages.erase(iter);
continue;
}
const float opacity = std::min(time_remaining, 1.0f);
ImGui::SetNextWindowPos(ImVec2(position_x, position_y));
ImGui::SetNextWindowSize(ImVec2(0.0f, 0.0f));
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, opacity);
if (ImGui::Begin(SmallString::FromFormat("osd_%u", index++), nullptr, window_flags))
{
ImGui::TextUnformatted(msg.text.c_str());
position_y += ImGui::GetWindowSize().y + (4.0f * scale);
}
ImGui::End();
ImGui::PopStyleVar();
++iter;
}
}
void AndroidHostInterface::SurfaceChanged(ANativeWindow* window, int format, int width, int height)
{
Log_InfoPrintf("SurfaceChanged %p %d %d %d", window, format, width, height);
if (m_display->GetRenderWindow() == window)
{
m_display->WindowResized();
return;
}
m_display->ChangeRenderWindow(window);
}
extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
Log::GetInstance().SetDebugOutputParams(true, nullptr, LOGLEVEL_DEV);
s_jvm = vm;
JNIEnv* env = GetJNIEnv();
if ((s_AndroidHostInterface_class = env->FindClass("com/github/stenzek/duckstation/AndroidHostInterface")) == nullptr)
{
Log_ErrorPrint("AndroidHostInterface class lookup failed");
return -1;
}
// Create global reference so it doesn't get cleaned up.
s_AndroidHostInterface_class = static_cast<jclass>(env->NewGlobalRef(s_AndroidHostInterface_class));
if (!s_AndroidHostInterface_class)
{
Log_ErrorPrint("Failed to get reference to AndroidHostInterface");
return -1;
}
if ((s_AndroidHostInterface_constructor = env->GetMethodID(s_AndroidHostInterface_class, "<init>", "()V")) ==
nullptr ||
(s_AndroidHostInterface_field_nativePointer =
env->GetFieldID(s_AndroidHostInterface_class, "nativePointer", "J")) == nullptr)
{
Log_ErrorPrint("AndroidHostInterface lookups failed");
return -1;
}
return JNI_VERSION_1_6;
}
#define DEFINE_JNI_METHOD(return_type, name) \
extern "C" JNIEXPORT return_type JNICALL Java_com_github_stenzek_duckstation_##name(JNIEnv* env)
#define DEFINE_JNI_ARGS_METHOD(return_type, name, ...) \
extern "C" JNIEXPORT return_type JNICALL Java_com_github_stenzek_duckstation_##name(JNIEnv* env, __VA_ARGS__)
DEFINE_JNI_ARGS_METHOD(jobject, AndroidHostInterface_create, jobject unused)
{
// initialize the java side
jobject java_obj = env->NewObject(s_AndroidHostInterface_class, s_AndroidHostInterface_constructor);
if (!java_obj)
{
Log_ErrorPrint("Failed to create Java AndroidHostInterface");
return nullptr;
}
jobject java_obj_ref = env->NewGlobalRef(java_obj);
Assert(java_obj_ref != nullptr);
// initialize the C++ side
AndroidHostInterface* cpp_obj = new AndroidHostInterface(java_obj_ref);
if (!cpp_obj)
{
// TODO: Do we need to release the original java object reference?
Log_ErrorPrint("Failed to create C++ AndroidHostInterface");
env->DeleteGlobalRef(java_obj_ref);
return nullptr;
}
env->SetLongField(java_obj, s_AndroidHostInterface_field_nativePointer,
static_cast<long>(reinterpret_cast<uintptr_t>(cpp_obj)));
return java_obj;
}
DEFINE_JNI_ARGS_METHOD(jboolean, AndroidHostInterface_isEmulationThreadRunning, jobject obj)
{
return GetNativeClass(env, obj)->IsEmulationThreadRunning();
}
DEFINE_JNI_ARGS_METHOD(jboolean, AndroidHostInterface_startEmulationThread, jobject obj, jobject surface,
jstring filename, jstring state_filename)
{
ANativeWindow* native_surface = ANativeWindow_fromSurface(env, surface);
if (!native_surface)
{
Log_ErrorPrint("ANativeWindow_fromSurface() returned null");
return false;
}
return GetNativeClass(env, obj)->StartEmulationThread(native_surface, JStringToString(env, filename),
JStringToString(env, state_filename));
}
DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_stopEmulationThread, jobject obj)
{
GetNativeClass(env, obj)->StopEmulationThread();
}
DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_surfaceChanged, jobject obj, jobject surface, jint format, jint width,
jint height)
{
ANativeWindow* native_surface = ANativeWindow_fromSurface(env, surface);
if (!native_surface)
Log_ErrorPrint("ANativeWindow_fromSurface() returned null");
AndroidHostInterface* hi = GetNativeClass(env, obj);
hi->RunOnEmulationThread(
[hi, native_surface, format, width, height]() { hi->SurfaceChanged(native_surface, format, width, height); }, true);
}

View File

@ -0,0 +1,60 @@
#pragma once
#include "YBaseLib/Event.h"
#include "YBaseLib/Timer.h"
#include "core/host_interface.h"
#include <atomic>
#include <deque>
#include <functional>
#include <jni.h>
#include <mutex>
#include <thread>
struct ANativeWindow;
class AndroidHostInterface final : public HostInterface
{
public:
AndroidHostInterface(jobject java_object);
~AndroidHostInterface() override;
void ReportError(const char* message) override;
void ReportMessage(const char* message) override;
void AddOSDMessage(const char* message, float duration = 2.0f) override;
bool IsEmulationThreadRunning() const { return m_emulation_thread.joinable(); }
bool StartEmulationThread(ANativeWindow* initial_surface, std::string initial_filename,
std::string initial_state_filename);
void RunOnEmulationThread(std::function<void()> function, bool blocking = false);
void StopEmulationThread();
void SurfaceChanged(ANativeWindow* window, int format, int width, int height);
private:
struct OSDMessage
{
std::string text;
Timer time;
float duration;
};
void EmulationThreadEntryPoint(ANativeWindow* initial_surface, std::string initial_filename,
std::string initial_state_filename);
void CreateImGuiContext();
void DestroyImGuiContext();
void DrawImGui();
void DrawOSDMessages();
jobject m_java_object = {};
std::deque<OSDMessage> m_osd_messages;
std::mutex m_osd_messages_lock;
std::mutex m_callback_mutex;
std::deque<std::function<void()>> m_callback_queue;
std::thread m_emulation_thread;
std::atomic_bool m_emulation_thread_stop_request{false};
std::atomic_bool m_emulation_thread_start_result{false};
Event m_emulation_thread_started;
};

View File

@ -0,0 +1,17 @@
#include "core/host_interface.h"
#include <jni.h>
#define DEFINE_JNI_METHOD(return_type, name, ...) \
extern "C" JNIEXPORT return_type JNICALL Java_com_github_stenzek_duckstation_##name(__VA_ARGS__)
DEFINE_JNI_METHOD(bool, createSystem)
{
return false;
}
DEFINE_JNI_METHOD(bool, bootSystem, const char* filename, const char* state_filename)
{
return false;
}
DEFINE_JNI_METHOD(void, runFrame) {}