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

@ -31,6 +31,8 @@ endif()
# Required libraries.
if(NOT ANDROID)
find_package(SDL2 REQUIRED)
else()
find_package(EGL REQUIRED)
endif()

View File

@ -0,0 +1,297 @@
#.rst:
# ECMFindModuleHelpers
# --------------------
#
# Helper macros for find modules: ecm_find_package_version_check(),
# ecm_find_package_parse_components() and
# ecm_find_package_handle_library_components().
#
# ::
#
# ecm_find_package_version_check(<name>)
#
# Prints warnings if the CMake version or the project's required CMake version
# is older than that required by extra-cmake-modules.
#
# ::
#
# ecm_find_package_parse_components(<name>
# RESULT_VAR <variable>
# KNOWN_COMPONENTS <component1> [<component2> [...]]
# [SKIP_DEPENDENCY_HANDLING])
#
# This macro will populate <variable> with a list of components found in
# <name>_FIND_COMPONENTS, after checking that all those components are in the
# list of KNOWN_COMPONENTS; if there are any unknown components, it will print
# an error or warning (depending on the value of <name>_FIND_REQUIRED) and call
# return().
#
# The order of components in <variable> is guaranteed to match the order they
# are listed in the KNOWN_COMPONENTS argument.
#
# If SKIP_DEPENDENCY_HANDLING is not set, for each component the variable
# <name>_<component>_component_deps will be checked for dependent components.
# If <component> is listed in <name>_FIND_COMPONENTS, then all its (transitive)
# dependencies will also be added to <variable>.
#
# ::
#
# ecm_find_package_handle_library_components(<name>
# COMPONENTS <component> [<component> [...]]
# [SKIP_DEPENDENCY_HANDLING])
# [SKIP_PKG_CONFIG])
#
# Creates an imported library target for each component. The operation of this
# macro depends on the presence of a number of CMake variables.
#
# The <name>_<component>_lib variable should contain the name of this library,
# and <name>_<component>_header variable should contain the name of a header
# file associated with it (whatever relative path is normally passed to
# '#include'). <name>_<component>_header_subdir variable can be used to specify
# which subdirectory of the include path the headers will be found in.
# ecm_find_package_components() will then search for the library
# and include directory (creating appropriate cache variables) and create an
# imported library target named <name>::<component>.
#
# Additional variables can be used to provide additional information:
#
# If SKIP_PKG_CONFIG, the <name>_<component>_pkg_config variable is set, and
# pkg-config is found, the pkg-config module given by
# <name>_<component>_pkg_config will be searched for and used to help locate the
# library and header file. It will also be used to set
# <name>_<component>_VERSION.
#
# Note that if version information is found via pkg-config,
# <name>_<component>_FIND_VERSION can be set to require a particular version
# for each component.
#
# If SKIP_DEPENDENCY_HANDLING is not set, the INTERFACE_LINK_LIBRARIES property
# of the imported target for <component> will be set to contain the imported
# targets for the components listed in <name>_<component>_component_deps.
# <component>_FOUND will also be set to false if any of the compoments in
# <name>_<component>_component_deps are not found. This requires the components
# in <name>_<component>_component_deps to be listed before <component> in the
# COMPONENTS argument.
#
# The following variables will be set:
#
# ``<name>_TARGETS``
# the imported targets
# ``<name>_LIBRARIES``
# the found libraries
# ``<name>_INCLUDE_DIRS``
# the combined required include directories for the components
# ``<name>_DEFINITIONS``
# the "other" CFLAGS provided by pkg-config, if any
# ``<name>_VERSION``
# the value of ``<name>_<component>_VERSION`` for the first component that
# has this variable set (note that components are searched for in the order
# they are passed to the macro), although if it is already set, it will not
# be altered
#
# Note that these variables are never cleared, so if
# ecm_find_package_handle_library_components() is called multiple times with
# different components (typically because of multiple find_package() calls) then
# ``<name>_TARGETS``, for example, will contain all the targets found in any
# call (although no duplicates).
#
# Since pre-1.0.0.
#=============================================================================
# Copyright 2014 Alex Merry <alex.merry@kde.org>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# 3. The name of the author may not be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
include(CMakeParseArguments)
macro(ecm_find_package_version_check module_name)
if(CMAKE_VERSION VERSION_LESS 2.8.12)
message(FATAL_ERROR "CMake 2.8.12 is required by Find${module_name}.cmake")
endif()
if(CMAKE_MINIMUM_REQUIRED_VERSION VERSION_LESS 2.8.12)
message(AUTHOR_WARNING "Your project should require at least CMake 2.8.12 to use Find${module_name}.cmake")
endif()
endmacro()
macro(ecm_find_package_parse_components module_name)
set(ecm_fppc_options SKIP_DEPENDENCY_HANDLING)
set(ecm_fppc_oneValueArgs RESULT_VAR)
set(ecm_fppc_multiValueArgs KNOWN_COMPONENTS DEFAULT_COMPONENTS)
cmake_parse_arguments(ECM_FPPC "${ecm_fppc_options}" "${ecm_fppc_oneValueArgs}" "${ecm_fppc_multiValueArgs}" ${ARGN})
if(ECM_FPPC_UNPARSED_ARGUMENTS)
message(FATAL_ERROR "Unexpected arguments to ecm_find_package_parse_components: ${ECM_FPPC_UNPARSED_ARGUMENTS}")
endif()
if(NOT ECM_FPPC_RESULT_VAR)
message(FATAL_ERROR "Missing RESULT_VAR argument to ecm_find_package_parse_components")
endif()
if(NOT ECM_FPPC_KNOWN_COMPONENTS)
message(FATAL_ERROR "Missing KNOWN_COMPONENTS argument to ecm_find_package_parse_components")
endif()
if(NOT ECM_FPPC_DEFAULT_COMPONENTS)
set(ECM_FPPC_DEFAULT_COMPONENTS ${ECM_FPPC_KNOWN_COMPONENTS})
endif()
if(${module_name}_FIND_COMPONENTS)
set(ecm_fppc_requestedComps ${${module_name}_FIND_COMPONENTS})
if(NOT ECM_FPPC_SKIP_DEPENDENCY_HANDLING)
# Make sure deps are included
foreach(ecm_fppc_comp ${ecm_fppc_requestedComps})
foreach(ecm_fppc_dep_comp ${${module_name}_${ecm_fppc_comp}_component_deps})
list(FIND ecm_fppc_requestedComps "${ecm_fppc_dep_comp}" ecm_fppc_index)
if("${ecm_fppc_index}" STREQUAL "-1")
if(NOT ${module_name}_FIND_QUIETLY)
message(STATUS "${module_name}: ${ecm_fppc_comp} requires ${${module_name}_${ecm_fppc_comp}_component_deps}")
endif()
list(APPEND ecm_fppc_requestedComps "${ecm_fppc_dep_comp}")
endif()
endforeach()
endforeach()
else()
message(STATUS "Skipping dependency handling for ${module_name}")
endif()
list(REMOVE_DUPLICATES ecm_fppc_requestedComps)
# This makes sure components are listed in the same order as
# KNOWN_COMPONENTS (potentially important for inter-dependencies)
set(${ECM_FPPC_RESULT_VAR})
foreach(ecm_fppc_comp ${ECM_FPPC_KNOWN_COMPONENTS})
list(FIND ecm_fppc_requestedComps "${ecm_fppc_comp}" ecm_fppc_index)
if(NOT "${ecm_fppc_index}" STREQUAL "-1")
list(APPEND ${ECM_FPPC_RESULT_VAR} "${ecm_fppc_comp}")
list(REMOVE_AT ecm_fppc_requestedComps ${ecm_fppc_index})
endif()
endforeach()
# if there are any left, they are unknown components
if(ecm_fppc_requestedComps)
set(ecm_fppc_msgType STATUS)
if(${module_name}_FIND_REQUIRED)
set(ecm_fppc_msgType FATAL_ERROR)
endif()
if(NOT ${module_name}_FIND_QUIETLY)
message(${ecm_fppc_msgType} "${module_name}: requested unknown components ${ecm_fppc_requestedComps}")
endif()
return()
endif()
else()
set(${ECM_FPPC_RESULT_VAR} ${ECM_FPPC_DEFAULT_COMPONENTS})
endif()
endmacro()
macro(ecm_find_package_handle_library_components module_name)
set(ecm_fpwc_options SKIP_PKG_CONFIG SKIP_DEPENDENCY_HANDLING)
set(ecm_fpwc_oneValueArgs)
set(ecm_fpwc_multiValueArgs COMPONENTS)
cmake_parse_arguments(ECM_FPWC "${ecm_fpwc_options}" "${ecm_fpwc_oneValueArgs}" "${ecm_fpwc_multiValueArgs}" ${ARGN})
if(ECM_FPWC_UNPARSED_ARGUMENTS)
message(FATAL_ERROR "Unexpected arguments to ecm_find_package_handle_components: ${ECM_FPWC_UNPARSED_ARGUMENTS}")
endif()
if(NOT ECM_FPWC_COMPONENTS)
message(FATAL_ERROR "Missing COMPONENTS argument to ecm_find_package_handle_components")
endif()
include(FindPackageHandleStandardArgs)
find_package(PkgConfig)
foreach(ecm_fpwc_comp ${ECM_FPWC_COMPONENTS})
set(ecm_fpwc_dep_vars)
set(ecm_fpwc_dep_targets)
if(NOT SKIP_DEPENDENCY_HANDLING)
foreach(ecm_fpwc_dep ${${module_name}_${ecm_fpwc_comp}_component_deps})
list(APPEND ecm_fpwc_dep_vars "${module_name}_${ecm_fpwc_dep}_FOUND")
list(APPEND ecm_fpwc_dep_targets "${module_name}::${ecm_fpwc_dep}")
endforeach()
endif()
if(NOT ECM_FPWC_SKIP_PKG_CONFIG AND ${module_name}_${ecm_fpwc_comp}_pkg_config)
pkg_check_modules(PKG_${module_name}_${ecm_fpwc_comp} QUIET
${${module_name}_${ecm_fpwc_comp}_pkg_config})
endif()
find_path(${module_name}_${ecm_fpwc_comp}_INCLUDE_DIR
NAMES ${${module_name}_${ecm_fpwc_comp}_header}
HINTS ${PKG_${module_name}_${ecm_fpwc_comp}_INCLUDE_DIRS}
PATH_SUFFIXES ${${module_name}_${ecm_fpwc_comp}_header_subdir}
)
find_library(${module_name}_${ecm_fpwc_comp}_LIBRARY
NAMES ${${module_name}_${ecm_fpwc_comp}_lib}
HINTS ${PKG_${module_name}_${ecm_fpwc_comp}_LIBRARY_DIRS}
)
set(${module_name}_${ecm_fpwc_comp}_VERSION "${PKG_${module_name}_${ecm_fpwc_comp}_VERSION}")
if(NOT ${module_name}_VERSION)
set(${module_name}_VERSION ${${module_name}_${ecm_fpwc_comp}_VERSION})
endif()
find_package_handle_standard_args(${module_name}_${ecm_fpwc_comp}
FOUND_VAR
${module_name}_${ecm_fpwc_comp}_FOUND
REQUIRED_VARS
${module_name}_${ecm_fpwc_comp}_LIBRARY
${module_name}_${ecm_fpwc_comp}_INCLUDE_DIR
${ecm_fpwc_dep_vars}
VERSION_VAR
${module_name}_${ecm_fpwc_comp}_VERSION
)
mark_as_advanced(
${module_name}_${ecm_fpwc_comp}_LIBRARY
${module_name}_${ecm_fpwc_comp}_INCLUDE_DIR
)
if(${module_name}_${ecm_fpwc_comp}_FOUND)
list(APPEND ${module_name}_LIBRARIES
"${${module_name}_${ecm_fpwc_comp}_LIBRARY}")
list(APPEND ${module_name}_INCLUDE_DIRS
"${${module_name}_${ecm_fpwc_comp}_INCLUDE_DIR}")
set(${module_name}_DEFINITIONS
${${module_name}_DEFINITIONS}
${PKG_${module_name}_${ecm_fpwc_comp}_DEFINITIONS})
if(NOT TARGET ${module_name}::${ecm_fpwc_comp})
add_library(${module_name}::${ecm_fpwc_comp} UNKNOWN IMPORTED)
set_target_properties(${module_name}::${ecm_fpwc_comp} PROPERTIES
IMPORTED_LOCATION "${${module_name}_${ecm_fpwc_comp}_LIBRARY}"
INTERFACE_COMPILE_OPTIONS "${PKG_${module_name}_${ecm_fpwc_comp}_DEFINITIONS}"
INTERFACE_INCLUDE_DIRECTORIES "${${module_name}_${ecm_fpwc_comp}_INCLUDE_DIR}"
INTERFACE_LINK_LIBRARIES "${ecm_fpwc_dep_targets}"
)
endif()
list(APPEND ${module_name}_TARGETS
"${module_name}::${ecm_fpwc_comp}")
endif()
endforeach()
if(${module_name}_LIBRARIES)
list(REMOVE_DUPLICATES ${module_name}_LIBRARIES)
endif()
if(${module_name}_INCLUDE_DIRS)
list(REMOVE_DUPLICATES ${module_name}_INCLUDE_DIRS)
endif()
if(${module_name}_DEFINITIONS)
list(REMOVE_DUPLICATES ${module_name}_DEFINITIONS)
endif()
if(${module_name}_TARGETS)
list(REMOVE_DUPLICATES ${module_name}_TARGETS)
endif()
endmacro()

View File

@ -0,0 +1 @@
include(${CMAKE_CURRENT_LIST_DIR}/ECMFindModuleHelpers.cmake)

172
CMakeModules/FindEGL.cmake Normal file
View File

@ -0,0 +1,172 @@
#.rst:
# FindEGL
# -------
#
# Try to find EGL.
#
# This will define the following variables:
#
# ``EGL_FOUND``
# True if (the requested version of) EGL is available
# ``EGL_VERSION``
# The version of EGL; note that this is the API version defined in the
# headers, rather than the version of the implementation (eg: Mesa)
# ``EGL_LIBRARIES``
# This can be passed to target_link_libraries() instead of the ``EGL::EGL``
# target
# ``EGL_INCLUDE_DIRS``
# This should be passed to target_include_directories() if the target is not
# used for linking
# ``EGL_DEFINITIONS``
# This should be passed to target_compile_options() if the target is not
# used for linking
#
# If ``EGL_FOUND`` is TRUE, it will also define the following imported target:
#
# ``EGL::EGL``
# The EGL library
#
# In general we recommend using the imported target, as it is easier to use.
# Bear in mind, however, that if the target is in the link interface of an
# exported library, it must be made available by the package config file.
#
# Since pre-1.0.0.
#=============================================================================
# Copyright 2014 Alex Merry <alex.merry@kde.org>
# Copyright 2014 Martin Gräßlin <mgraesslin@kde.org>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# 3. The name of the author may not be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#=============================================================================
include(${CMAKE_CURRENT_LIST_DIR}/ECMFindModuleHelpersStub.cmake)
include(CheckCXXSourceCompiles)
include(CMakePushCheckState)
ecm_find_package_version_check(EGL)
# Use pkg-config to get the directories and then use these values
# in the FIND_PATH() and FIND_LIBRARY() calls
find_package(PkgConfig)
pkg_check_modules(PKG_EGL QUIET egl)
set(EGL_DEFINITIONS ${PKG_EGL_CFLAGS_OTHER})
find_path(EGL_INCLUDE_DIR
NAMES
EGL/egl.h
HINTS
${PKG_EGL_INCLUDE_DIRS}
)
find_library(EGL_LIBRARY
NAMES
EGL
HINTS
${PKG_EGL_LIBRARY_DIRS}
)
# NB: We do *not* use the version information from pkg-config, as that
# is the implementation version (eg: the Mesa version)
if(EGL_INCLUDE_DIR)
# egl.h has defines of the form EGL_VERSION_x_y for each supported
# version; so the header for EGL 1.1 will define EGL_VERSION_1_0 and
# EGL_VERSION_1_1. Finding the highest supported version involves
# finding all these defines and selecting the highest numbered.
file(READ "${EGL_INCLUDE_DIR}/EGL/egl.h" _EGL_header_contents)
string(REGEX MATCHALL
"[ \t]EGL_VERSION_[0-9_]+"
_EGL_version_lines
"${_EGL_header_contents}"
)
unset(_EGL_header_contents)
foreach(_EGL_version_line ${_EGL_version_lines})
string(REGEX REPLACE
"[ \t]EGL_VERSION_([0-9_]+)"
"\\1"
_version_candidate
"${_EGL_version_line}"
)
string(REPLACE "_" "." _version_candidate "${_version_candidate}")
if(NOT DEFINED EGL_VERSION OR EGL_VERSION VERSION_LESS _version_candidate)
set(EGL_VERSION "${_version_candidate}")
endif()
endforeach()
unset(_EGL_version_lines)
endif()
cmake_push_check_state(RESET)
list(APPEND CMAKE_REQUIRED_LIBRARIES "${EGL_LIBRARY}")
list(APPEND CMAKE_REQUIRED_INCLUDES "${EGL_INCLUDE_DIR}")
check_cxx_source_compiles("
#include <EGL/egl.h>
int main(int argc, char *argv[]) {
EGLint x = 0; EGLDisplay dpy = 0; EGLContext ctx = 0;
eglDestroyContext(dpy, ctx);
}" HAVE_EGL)
cmake_pop_check_state()
set(required_vars EGL_INCLUDE_DIR HAVE_EGL)
if(NOT EMSCRIPTEN)
list(APPEND required_vars EGL_LIBRARY)
endif()
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(EGL
FOUND_VAR
EGL_FOUND
REQUIRED_VARS
${required_vars}
VERSION_VAR
EGL_VERSION
)
if(EGL_FOUND AND NOT TARGET EGL::EGL)
if (EMSCRIPTEN)
add_library(EGL::EGL INTERFACE IMPORTED)
# Nothing further to be done, system include paths have headers and linkage is implicit.
else()
add_library(EGL::EGL UNKNOWN IMPORTED)
set_target_properties(EGL::EGL PROPERTIES
IMPORTED_LOCATION "${EGL_LIBRARY}"
INTERFACE_COMPILE_OPTIONS "${EGL_DEFINITIONS}"
INTERFACE_INCLUDE_DIRECTORIES "${EGL_INCLUDE_DIR}"
)
endif()
endif()
mark_as_advanced(EGL_LIBRARY EGL_INCLUDE_DIR HAVE_EGL)
# compatibility variables
set(EGL_LIBRARIES ${EGL_LIBRARY})
set(EGL_INCLUDE_DIRS ${EGL_INCLUDE_DIR})
set(EGL_VERSION_STRING ${EGL_VERSION})
include(FeatureSummary)
set_package_properties(EGL PROPERTIES
URL "https://www.khronos.org/egl/"
DESCRIPTION "A platform-agnostic mechanism for creating rendering surfaces for use with other graphics libraries, such as OpenGL|ES and OpenVG."
)

14
android/.gitignore vendored Normal file
View File

@ -0,0 +1,14 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx

1
android/.idea/.name generated Normal file
View File

@ -0,0 +1 @@
DuckStation

116
android/.idea/codeStyles/Project.xml generated Normal file
View File

@ -0,0 +1,116 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<codeStyleSettings language="XML">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
</code_scheme>
</component>

16
android/.idea/gradle.xml generated Normal file
View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<compositeConfiguration>
<compositeBuild compositeDefinitionSource="SCRIPT" />
</compositeConfiguration>
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="resolveModulePerSourceSet" value="false" />
<option name="testRunner" value="PLATFORM" />
</GradleProjectSettings>
</option>
</component>
</project>

9
android/.idea/misc.xml generated Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

12
android/.idea/runConfigurations.xml generated Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
</set>
</option>
</component>
</project>

6
android/.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

1
android/app/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

47
android/app/build.gradle Normal file
View File

@ -0,0 +1,47 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 29
buildToolsVersion "29.0.2"
defaultConfig {
applicationId "com.github.stenzek.duckstation"
minSdkVersion 21
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
externalNativeBuild {
cmake {
path "../../CMakeLists.txt"
version "3.10.2"
}
}
defaultConfig {
externalNativeBuild {
cmake {
arguments "-DCMAKE_BUILD_TYPE=RelWithDebInfo"
abiFilters "x86"
}
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'com.google.android.material:material:1.0.0'
implementation 'androidx.preference:preference:1.1.0-alpha05'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
}

21
android/app/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -0,0 +1,27 @@
package com.github.stenzek.duckstation;
import android.content.Context;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.github.stenzek.duckstation", appContext.getPackageName());
}
}

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) {}

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.github.stenzek.duckstation">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:requestLegacyExternalStorage="true">
<activity
android:name=".EmulationActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:label="@string/title_activity_emulation"
android:parentActivityName=".MainActivity"
android:theme="@style/FullscreenTheme">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.github.stenzek.duckstation.MainActivity" />
</activity>
<activity
android:name=".SettingsActivity"
android:label="@string/title_activity_settings"
android:parentActivityName=".MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.github.stenzek.duckstation.MainActivity" />
</activity>
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -0,0 +1,25 @@
package com.github.stenzek.duckstation;
import android.view.Surface;
public class AndroidHostInterface
{
private long nativePointer;
static {
System.loadLibrary("duckstation-native");
}
static public native AndroidHostInterface create();
public AndroidHostInterface(long nativePointer)
{
this.nativePointer = nativePointer;
}
public native boolean isEmulationThreadRunning();
public native boolean startEmulationThread(Surface surface, String filename, String state_filename);
public native void stopEmulationThread();
public native void surfaceChanged(Surface surface, int format, int width, int height);
}

View File

@ -0,0 +1,216 @@
package com.github.stenzek.duckstation;
import android.annotation.SuppressLint;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.MenuItem;
import androidx.core.app.NavUtils;
/**
* An example full-screen activity that shows and hides the system UI (i.e.
* status bar and navigation/system bar) with user interaction.
*/
public class EmulationActivity extends AppCompatActivity implements SurfaceHolder.Callback {
/** Interface to the native emulator core */
AndroidHostInterface mHostInterface;
/**
* Whether or not the system UI should be auto-hidden after
* {@link #AUTO_HIDE_DELAY_MILLIS} milliseconds.
*/
private static final boolean AUTO_HIDE = true;
/**
* If {@link #AUTO_HIDE} is set, the number of milliseconds to wait after
* user interaction before hiding the system UI.
*/
private static final int AUTO_HIDE_DELAY_MILLIS = 3000;
/**
* Some older devices needs a small delay between UI widget updates
* and a change of the status and navigation bar.
*/
private static final int UI_ANIMATION_DELAY = 300;
private final Handler mHideHandler = new Handler();
private SurfaceView mContentView;
private final Runnable mHidePart2Runnable = new Runnable() {
@SuppressLint("InlinedApi")
@Override
public void run() {
// Delayed removal of status and navigation bar
// Note that some of these constants are new as of API 16 (Jelly Bean)
// and API 19 (KitKat). It is safe to use them, as they are inlined
// at compile-time and do nothing on earlier devices.
mContentView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE
| View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
}
};
private final Runnable mShowPart2Runnable = new Runnable() {
@Override
public void run() {
// Delayed display of UI elements
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.show();
}
}
};
private boolean mVisible;
private final Runnable mHideRunnable = new Runnable() {
@Override
public void run() {
hide();
}
};
/**
* Touch listener to use for in-layout UI controls to delay hiding the
* system UI. This is to prevent the jarring behavior of controls going away
* while interacting with activity UI.
*/
private final View.OnTouchListener mDelayHideTouchListener = new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
if (AUTO_HIDE) {
delayedHide(AUTO_HIDE_DELAY_MILLIS);
}
return false;
}
};
@Override
public void surfaceCreated(SurfaceHolder holder) {
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
// Once we get a surface, we can boot.
if (mHostInterface.isEmulationThreadRunning()) {
mHostInterface.surfaceChanged(holder.getSurface(), format, width, height);
return;
}
String filename = new String();
String state_filename = new String();
if (!mHostInterface.startEmulationThread(holder.getSurface(),filename, state_filename))
{
Log.e("EmulationActivity", "Failed to start emulation thread");
return;
}
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
if (!mHostInterface.isEmulationThreadRunning())
return;
Log.i("EmulationActivity", "Stopping emulation thread");
mHostInterface.stopEmulationThread();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_emulation);
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
}
mVisible = true;
mContentView = (SurfaceView)findViewById(R.id.fullscreen_content);
Log.e("EmulationActivity", "adding callback");
mContentView.getHolder().addCallback(this);
// Set up the user interaction to manually show or hide the system UI.
mContentView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
toggle();
}
});
mHostInterface = AndroidHostInterface.create();
if (mHostInterface == null)
throw new InstantiationError("Failed to create host interface");
}
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
// Trigger the initial hide() shortly after the activity has been
// created, to briefly hint to the user that UI controls
// are available.
delayedHide(100);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == android.R.id.home) {
// This ID represents the Home or Up button.
NavUtils.navigateUpFromSameTask(this);
return true;
}
return super.onOptionsItemSelected(item);
}
private void toggle() {
if (mVisible) {
hide();
} else {
show();
}
}
private void hide() {
// Hide UI first
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.hide();
}
mVisible = false;
// Schedule a runnable to remove the status and navigation bar after a delay
mHideHandler.removeCallbacks(mShowPart2Runnable);
mHideHandler.postDelayed(mHidePart2Runnable, UI_ANIMATION_DELAY);
}
@SuppressLint("InlinedApi")
private void show() {
// Show the system bar
mContentView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
mVisible = true;
// Schedule a runnable to display UI elements after a delay
mHideHandler.removeCallbacks(mHidePart2Runnable);
mHideHandler.postDelayed(mShowPart2Runnable, UI_ANIMATION_DELAY);
}
/**
* Schedules a call to hide() in delay milliseconds, canceling any
* previously scheduled calls.
*/
private void delayedHide(int delayMillis) {
mHideHandler.removeCallbacks(mHideRunnable);
mHideHandler.postDelayed(mHideRunnable, delayMillis);
}
}

View File

@ -0,0 +1,85 @@
package com.github.stenzek.duckstation;
import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Bundle;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import android.content.Intent;
import android.view.View;
import android.view.Menu;
import android.view.MenuItem;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
FloatingActionButton fab = findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
/*Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show();*/
startEmulation("nonexistant.cue");
}
});
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
Intent intent = new Intent(this, SettingsActivity.class);
startActivity(intent);
return true;
}
return super.onOptionsItemSelected(item);
}
private boolean checkForExternalStoragePermissions() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED &&
ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED)
{
return true;
}
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
return false;
}
private boolean startEmulation(String bootPath) {
if (!checkForExternalStoragePermissions()) {
Snackbar.make(findViewById(R.id.fab), "External storage permissions are required to start emulation.", Snackbar.LENGTH_LONG);
return false;
}
Intent intent = new Intent(this, EmulationActivity.class);
intent.putExtra("bootPath", bootPath);
startActivity(intent);
return true;
}
}

View File

@ -0,0 +1,12 @@
package com.github.stenzek.duckstation;
public class NativeLibrary {
static
{
System.loadLibrary("duckstation-native");
}
public native boolean createSystem();
public native boolean bootSystem(String filename, String stateFilename);
public native void runFrame();
}

View File

@ -0,0 +1,31 @@
package com.github.stenzek.duckstation;
import android.os.Bundle;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.preference.PreferenceFragmentCompat;
public class SettingsActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.settings_activity);
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.settings, new SettingsFragment())
.commit();
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
}
}
public static class SettingsFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
setPreferencesFromResource(R.xml.root_preferences, rootKey);
}
}
}

View File

@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeWidth="1"
android:strokeColor="#00000000">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#008577"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#0099cc"
tools:context=".EmulationActivity">
<!-- The primary full-screen view. This can be replaced with whatever view
is needed to present your content, e.g. VideoView, SurfaceView,
TextureView, etc. -->
<SurfaceView
android:id="@+id/fullscreen_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:keepScreenOn="true" />
</FrameLayout>

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:theme="@style/AppTheme.AppBarOverlay">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</com.google.android.material.appbar.AppBarLayout>
<include layout="@layout/content_main"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margin"
app:srcCompat="@android:drawable/ic_dialog_email" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:showIn="@layout/activity_main"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,9 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/settings"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

View File

@ -0,0 +1,9 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="com.github.stenzek.duckstation.MainActivity" >
<item android:id="@+id/action_settings"
android:title="@string/action_settings"
android:orderInCategory="100"
app:showAsAction="never" />
</menu>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,68 @@
<resources>
<string-array name="settings_console_region_entries">
<item>Auto-Detect</item>
<item>NTSC-J (Japan)</item>
<item>NTSC-U (US)</item>
<item>PAL (Europe, Australia)</item>
</string-array>
<string-array name="settings_console_region_values">
<item>Auto</item>
<item>NTSC-J</item>
<item>NTSC-U</item>
<item>PAL</item>
</string-array>
<string-array name="settings_cpu_execution_mode_entries">
<item>Interpreter (Slowest)</item>
<item>Cached Interpreter (Faster)</item>
<item>Recompiler (Fastest)</item>
</string-array>
<string-array name="settings_cpu_execution_mode_values">
<item>Interpreter</item>
<item>CachedInterpreter</item>
<item>Recompiler</item>
</string-array>
<string-array name="gpu_renderer_entries">
<item>Hardware (OpenGL)</item>
<item>Software</item>
</string-array>
<string-array name="gpu_renderer_values">
<item>OpenGL</item>
<item>Software</item>
</string-array>
<string-array name="settings_gpu_resolution_scale_entries">
<item>1x (1024x512)</item>
<item>2x (2048x1024)</item>
<item>3x (3072x1536)</item>
<item>4x (4096x2048)</item>
<item>5x (5120x2560)</item>
<item>6x (6144x3072)</item>
<item>7x (7168x3584)</item>
<item>8x (8192x4096)</item>
<item>9x (9216x4608)</item>
<item>10x (10240x5120)</item>
<item>11x (11264x5632)</item>
<item>12x (12288x6144)</item>
<item>13x (13312x6656)</item>
<item>14x (14336x7168)</item>
<item>15x (15360x7680)</item>
<item>16x (16384x8192)</item>
</string-array>
<string-array name="settings_gpu_resolution_scale_values">
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
<item>5</item>
<item>6</item>
<item>7</item>
<item>8</item>
<item>9</item>
<item>10</item>
<item>11</item>
<item>12</item>
<item>13</item>
<item>14</item>
<item>15</item>
<item>16</item>
</string-array>
</resources>

View File

@ -0,0 +1,12 @@
<resources>
<!-- Declare custom theme attributes that allow changing which styles are
used for button bars depending on the API level.
?android:attr/buttonBarStyle is new as of API 11 so this is
necessary to support previous API levels. -->
<declare-styleable name="ButtonBarContainerTheme">
<attr name="metaButtonBarStyle" format="reference" />
<attr name="metaButtonBarButtonStyle" format="reference" />
</declare-styleable>
</resources>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#008577</color>
<color name="colorPrimaryDark">#00574B</color>
<color name="colorAccent">#D81B60</color>
<color name="black_overlay">#66000000</color>
</resources>

View File

@ -0,0 +1,3 @@
<resources>
<dimen name="fab_margin">16dp</dimen>
</resources>

View File

@ -0,0 +1,42 @@
<resources>
<string name="app_name">DuckStation</string>
<string name="action_settings">Settings</string>
<string name="title_activity_settings">Settings</string>
<!-- Preference Titles -->
<string name="settings_console_header">Console</string>
<string name="settings_behavior_header">Behavior</string>
<string name="settings_host_synchronization_header">Host Synchronization</string>
<string name="settings_cpu_header">CPU</string>
<string name="settings_gpu_header">GPU</string>
<!-- Console Preferences -->
<string name="settings_console_region">Region</string>
<string name="settings_console_region_default">Auto</string>
<string name="settings_console_bios_path">BIOS Path</string>
<string name="settings_console_tty_output">Enable TTY Output</string>
<string name="settings_console_fast_boot">Fast Boot</string>
<!-- Behavior Preferences -->
<string name="settings_behavior_enable_speed_limiter">Enable Speed Limiter</string>
<string name="settings_behavior_pause_on_start">Pause On Start</string>
<!-- Host Synchronization Preferences -->
<string name="settings_host_synchronization_sync_to_audio">Sync To Audio</string>
<string name="settings_host_synchronization_sync_to_video">Sync To Video</string>
<!-- CPU Preferences -->
<string name="settings_cpu_execution_mode">Execution Mode</string>
<string name="settings_cpu_execution_mode_default">Interpreter</string>
<!-- GPU Preferences -->
<string name="settings_gpu_renderer">Renderer</string>
<string name="settings_gpu_renderer_default">OpenGL</string>
<string name="settings_gpu_display_linear_filtering">Display Linear Filtering</string>
<string name="settings_gpu_resolution_scale">Resolution Scale</string>
<string name="settings_gpu_true_color">True 24-Bit Color (Disables Dithering)</string>
<string name="title_activity_emulation">EmulationActivity</string>
<string name="dummy_button">Dummy Button</string>
<string name="dummy_content">DUMMY\nCONTENT</string>
</resources>

View File

@ -0,0 +1,32 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="AppTheme.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
<style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
<style name="FullscreenTheme" parent="AppTheme">
<item name="android:actionBarStyle">@style/FullscreenActionBarStyle</item>
<item name="android:windowActionBarOverlay">true</item>
<item name="android:windowBackground">@null</item>
<item name="metaButtonBarStyle">?android:attr/buttonBarStyle</item>
<item name="metaButtonBarButtonStyle">?android:attr/buttonBarButtonStyle</item>
</style>
<style name="FullscreenActionBarStyle" parent="Widget.AppCompat.ActionBar">
<item name="android:background">@color/black_overlay</item>
</style>
</resources>

View File

@ -0,0 +1,104 @@
<!--
~ Copyright 2018 The app Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory app:title="@string/settings_console_header">
<ListPreference
app:key="Console/Region"
app:title="@string/settings_console_region"
app:entries="@array/settings_console_region_entries"
app:entryValues="@array/settings_console_region_values"
app:defaultValue="@string/settings_console_region_default"
app:useSimpleSummaryProvider="true" />
<EditTextPreference
app:key="BIOS/Path"
app:title="@string/settings_console_bios_path"
app:useSimpleSummaryProvider="true" />
<SwitchPreferenceCompat
app:key="BIOS/PatchTTYEnable"
app:title="@string/settings_console_tty_output"
app:useSimpleSummaryProvider="true" />
<SwitchPreferenceCompat
app:key="BIOS/PatchFastBoot"
app:title="@string/settings_console_fast_boot"
app:useSimpleSummaryProvider="true" />
</PreferenceCategory>
<PreferenceCategory app:title="@string/settings_behavior_header">
<SwitchPreferenceCompat
app:key="General/SpeedLimiterEnabled"
app:title="@string/settings_behavior_enable_speed_limiter"
app:useSimpleSummaryProvider="true" />
<SwitchPreferenceCompat
app:key="General/StartPaused"
app:title="@string/settings_behavior_pause_on_start"
app:useSimpleSummaryProvider="true" />
<SwitchPreferenceCompat
app:key="General/SyncToAudio"
app:title="@string/settings_host_synchronization_sync_to_audio"
app:defaultValue="true"
app:useSimpleSummaryProvider="true" />
<SwitchPreferenceCompat
app:key="General/SyncToVideo"
app:title="@string/settings_host_synchronization_sync_to_video"
app:defaultValue="true"
app:useSimpleSummaryProvider="true" />
</PreferenceCategory>
<PreferenceCategory app:title="@string/settings_cpu_header">
<ListPreference
app:key="CPU/ExecutionMode"
app:title="@string/settings_cpu_execution_mode"
app:entries="@array/settings_cpu_execution_mode_entries"
app:entryValues="@array/settings_cpu_execution_mode_values"
app:defaultValue="@string/settings_cpu_execution_mode_default"
app:useSimpleSummaryProvider="true" />
</PreferenceCategory>
<PreferenceCategory app:title="@string/settings_gpu_header">
<ListPreference
app:key="GPU/Renderer"
app:title="@string/settings_gpu_renderer"
app:entries="@array/gpu_renderer_entries"
app:entryValues="@array/gpu_renderer_values"
app:defaultValue="@string/settings_gpu_renderer_default"/>
<ListPreference
app:key="GPU/ResolutionScale"
app:title="@string/settings_gpu_resolution_scale"
app:entries="@array/settings_gpu_resolution_scale_entries"
app:entryValues="@array/settings_gpu_resolution_scale_values"
app:defaultValue="@string/settings_gpu_renderer_default"/>
<SwitchPreferenceCompat
app:key="GPU/TrueColor"
app:title="@string/settings_gpu_true_color"
app:defaultValue="true"/>
<SwitchPreferenceCompat
app:key="Display/LinearFiltering"
app:title="@string/settings_gpu_display_linear_filtering"
app:defaultValue="true"/>
</PreferenceCategory>
</PreferenceScreen>

View File

@ -0,0 +1,17 @@
package com.github.stenzek.duckstation;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() {
assertEquals(4, 2 + 2);
}
}

27
android/build.gradle Normal file
View File

@ -0,0 +1,27 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.2'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}

20
android/gradle.properties Normal file
View File

@ -0,0 +1,20 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true

Binary file not shown.

View File

@ -0,0 +1,6 @@
#Wed Nov 27 22:04:24 AEST 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip

172
android/gradlew vendored Executable file
View File

@ -0,0 +1,172 @@
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

84
android/gradlew.bat vendored Normal file
View File

@ -0,0 +1,84 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

2
android/settings.gradle Normal file
View File

@ -0,0 +1,2 @@
include ':app'
rootProject.name='DuckStation'