mirror of
https://github.com/WinampDesktop/winamp.git
synced 2025-06-16 23:55:45 -04:00
Import initial work on Android frontend
This commit is contained in:
12
android/app/src/cpp/CMakeLists.txt
Normal file
12
android/app/src/cpp/CMakeLists.txt
Normal 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)
|
68
android/app/src/cpp/android_audio_stream.cpp
Normal file
68
android/app/src/cpp/android_audio_stream.cpp
Normal 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
|
21
android/app/src/cpp/android_audio_stream.h
Normal file
21
android/app/src/cpp/android_audio_stream.h
Normal 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;
|
||||
};
|
392
android/app/src/cpp/android_gles2_host_display.cpp
Normal file
392
android/app/src/cpp/android_gles2_host_display.cpp
Normal 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);
|
||||
}
|
78
android/app/src/cpp/android_gles2_host_display.h
Normal file
78
android/app/src/cpp/android_gles2_host_display.h
Normal 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;
|
||||
};
|
416
android/app/src/cpp/android_host_interface.cpp
Normal file
416
android/app/src/cpp/android_host_interface.cpp
Normal 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);
|
||||
}
|
60
android/app/src/cpp/android_host_interface.h
Normal file
60
android/app/src/cpp/android_host_interface.h
Normal 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;
|
||||
};
|
17
android/app/src/cpp/main.cpp
Normal file
17
android/app/src/cpp/main.cpp
Normal 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) {}
|
Reference in New Issue
Block a user