diff --git a/CMakeLists.txt b/CMakeLists.txt index bb7bf996b..0b87e6975 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,6 +31,8 @@ endif() # Required libraries. if(NOT ANDROID) find_package(SDL2 REQUIRED) +else() + find_package(EGL REQUIRED) endif() diff --git a/CMakeModules/ECMFindModuleHelpers.cmake b/CMakeModules/ECMFindModuleHelpers.cmake new file mode 100644 index 000000000..f2e32f959 --- /dev/null +++ b/CMakeModules/ECMFindModuleHelpers.cmake @@ -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() +# +# 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( +# RESULT_VAR +# KNOWN_COMPONENTS [ [...]] +# [SKIP_DEPENDENCY_HANDLING]) +# +# This macro will populate with a list of components found in +# _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 _FIND_REQUIRED) and call +# return(). +# +# The order of components in 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 +# __component_deps will be checked for dependent components. +# If is listed in _FIND_COMPONENTS, then all its (transitive) +# dependencies will also be added to . +# +# :: +# +# ecm_find_package_handle_library_components( +# COMPONENTS [ [...]] +# [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 __lib variable should contain the name of this library, +# and __header variable should contain the name of a header +# file associated with it (whatever relative path is normally passed to +# '#include'). __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 ::. +# +# Additional variables can be used to provide additional information: +# +# If SKIP_PKG_CONFIG, the __pkg_config variable is set, and +# pkg-config is found, the pkg-config module given by +# __pkg_config will be searched for and used to help locate the +# library and header file. It will also be used to set +# __VERSION. +# +# Note that if version information is found via pkg-config, +# __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 will be set to contain the imported +# targets for the components listed in __component_deps. +# _FOUND will also be set to false if any of the compoments in +# __component_deps are not found. This requires the components +# in __component_deps to be listed before in the +# COMPONENTS argument. +# +# The following variables will be set: +# +# ``_TARGETS`` +# the imported targets +# ``_LIBRARIES`` +# the found libraries +# ``_INCLUDE_DIRS`` +# the combined required include directories for the components +# ``_DEFINITIONS`` +# the "other" CFLAGS provided by pkg-config, if any +# ``_VERSION`` +# the value of ``__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 +# ``_TARGETS``, for example, will contain all the targets found in any +# call (although no duplicates). +# +# Since pre-1.0.0. + +#============================================================================= +# Copyright 2014 Alex Merry +# +# 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() diff --git a/CMakeModules/ECMFindModuleHelpersStub.cmake b/CMakeModules/ECMFindModuleHelpersStub.cmake new file mode 100644 index 000000000..acc8c8042 --- /dev/null +++ b/CMakeModules/ECMFindModuleHelpersStub.cmake @@ -0,0 +1 @@ +include(${CMAKE_CURRENT_LIST_DIR}/ECMFindModuleHelpers.cmake) diff --git a/CMakeModules/FindEGL.cmake b/CMakeModules/FindEGL.cmake new file mode 100644 index 000000000..48e517287 --- /dev/null +++ b/CMakeModules/FindEGL.cmake @@ -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 +# Copyright 2014 Martin Gräßlin +# +# 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 + +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." +) diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 000000000..603b14077 --- /dev/null +++ b/android/.gitignore @@ -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 diff --git a/android/.idea/.name b/android/.idea/.name new file mode 100644 index 000000000..eea1d2e85 --- /dev/null +++ b/android/.idea/.name @@ -0,0 +1 @@ +DuckStation \ No newline at end of file diff --git a/android/.idea/codeStyles/Project.xml b/android/.idea/codeStyles/Project.xml new file mode 100644 index 000000000..681f41ae2 --- /dev/null +++ b/android/.idea/codeStyles/Project.xml @@ -0,0 +1,116 @@ + + + + + + + +
+ + + + xmlns:android + + ^$ + + + +
+
+ + + + xmlns:.* + + ^$ + + + BY_NAME + +
+
+ + + + .*:id + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:name + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + name + + ^$ + + + +
+
+ + + + style + + ^$ + + + +
+
+ + + + .* + + ^$ + + + BY_NAME + +
+
+ + + + .* + + http://schemas.android.com/apk/res/android + + + ANDROID_ATTRIBUTE_ORDER + +
+
+ + + + .* + + .* + + + BY_NAME + +
+
+
+
+
+
\ No newline at end of file diff --git a/android/.idea/gradle.xml b/android/.idea/gradle.xml new file mode 100644 index 000000000..d291b3d7c --- /dev/null +++ b/android/.idea/gradle.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/android/.idea/misc.xml b/android/.idea/misc.xml new file mode 100644 index 000000000..37a750962 --- /dev/null +++ b/android/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/android/.idea/runConfigurations.xml b/android/.idea/runConfigurations.xml new file mode 100644 index 000000000..7f68460d8 --- /dev/null +++ b/android/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/android/.idea/vcs.xml b/android/.idea/vcs.xml new file mode 100644 index 000000000..6c0b86358 --- /dev/null +++ b/android/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/android/app/.gitignore b/android/app/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/android/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 000000000..c19e12add --- /dev/null +++ b/android/app/build.gradle @@ -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' +} diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 000000000..f1b424510 --- /dev/null +++ b/android/app/proguard-rules.pro @@ -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 diff --git a/android/app/src/androidTest/java/com/github/stenzek/duckstation/ExampleInstrumentedTest.java b/android/app/src/androidTest/java/com/github/stenzek/duckstation/ExampleInstrumentedTest.java new file mode 100644 index 000000000..71cc603ed --- /dev/null +++ b/android/app/src/androidTest/java/com/github/stenzek/duckstation/ExampleInstrumentedTest.java @@ -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 Testing documentation + */ +@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()); + } +} diff --git a/android/app/src/cpp/CMakeLists.txt b/android/app/src/cpp/CMakeLists.txt new file mode 100644 index 000000000..bd48713f1 --- /dev/null +++ b/android/app/src/cpp/CMakeLists.txt @@ -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) diff --git a/android/app/src/cpp/android_audio_stream.cpp b/android/app/src/cpp/android_audio_stream.cpp new file mode 100644 index 000000000..3b4f134ee --- /dev/null +++ b/android/app/src/cpp/android_audio_stream.cpp @@ -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 AndroidAudioStream::Create() +{ + return std::make_unique(); +} + +bool AndroidAudioStream::OpenDevice() +{ + DebugAssert(!m_is_open); +#if 0 + SDL_AudioSpec spec = {}; + spec.freq = m_output_sample_rate; + spec.channels = static_cast(m_channels); + spec.format = AUDIO_S16; + spec.samples = static_cast(m_buffer_size); + spec.callback = AudioCallback; + spec.userdata = static_cast(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(userdata); + const u32 num_samples = len / sizeof(SampleType) / this_ptr->m_channels; + const u32 read_samples = this_ptr->ReadSamples(reinterpret_cast(stream), num_samples); + const u32 silence_samples = num_samples - read_samples; + if (silence_samples > 0) + { + std::memset(reinterpret_cast(stream) + (read_samples * this_ptr->m_channels), 0, + silence_samples * this_ptr->m_channels * sizeof(SampleType)); + } +} +#endif \ No newline at end of file diff --git a/android/app/src/cpp/android_audio_stream.h b/android/app/src/cpp/android_audio_stream.h new file mode 100644 index 000000000..84a78f808 --- /dev/null +++ b/android/app/src/cpp/android_audio_stream.h @@ -0,0 +1,21 @@ +#pragma once +#include "common/audio_stream.h" +#include + +class AndroidAudioStream final : public AudioStream +{ +public: + AndroidAudioStream(); + ~AndroidAudioStream(); + + static std::unique_ptr 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; +}; diff --git a/android/app/src/cpp/android_gles2_host_display.cpp b/android/app/src/cpp/android_gles2_host_display.cpp new file mode 100644 index 000000000..5504f4195 --- /dev/null +++ b/android/app/src/cpp/android_gles2_host_display.cpp @@ -0,0 +1,392 @@ +#include "android_gles2_host_display.h" +#include "YBaseLib/Log.h" +#include +#include +#include +#include +#include +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(static_cast(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 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(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(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 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(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(reinterpret_cast(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 AndroidGLES2HostDisplay::GetWindowSize() const +{ + return std::make_tuple(static_cast(m_window_width), static_cast(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 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 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(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(m_window_width); + ImGui::GetIO().DisplaySize.y = static_cast(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 AndroidGLES2HostDisplay::Create(ANativeWindow* window) +{ + std::unique_ptr display = std::make_unique(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(m_display_offset_x) / static_cast(m_display_texture_width); + const float tex_right = tex_left + static_cast(m_display_width) / static_cast(m_display_texture_width); + const float tex_top = static_cast(m_display_offset_y) / static_cast(m_display_texture_height); + const float tex_bottom = + tex_top + static_cast(m_display_height) / static_cast(m_display_texture_height); + const std::array, 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); +} diff --git a/android/app/src/cpp/android_gles2_host_display.h b/android/app/src/cpp/android_gles2_host_display.h new file mode 100644 index 000000000..9b093c14f --- /dev/null +++ b/android/app/src/cpp/android_gles2_host_display.h @@ -0,0 +1,78 @@ +#pragma once +#include "common/gl/program.h" +#include "common/gl/texture.h" +#include "core/host_display.h" +#include +#include +#include +#include +#include + +class AndroidGLES2HostDisplay final : public HostDisplay +{ +public: + AndroidGLES2HostDisplay(ANativeWindow* window); + ~AndroidGLES2HostDisplay(); + + static std::unique_ptr 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 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 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; +}; diff --git a/android/app/src/cpp/android_host_interface.cpp b/android/app/src/cpp/android_host_interface.cpp new file mode 100644 index 000000000..a53c3b1c6 --- /dev/null +++ b/android/app/src/cpp/android_host_interface.cpp @@ -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 +#include +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(&env), JNI_VERSION_1_6) != JNI_OK) + return nullptr; + else + return env; +} + +static AndroidHostInterface* GetNativeClass(JNIEnv* env, jobject obj) +{ + return reinterpret_cast( + static_cast(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 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 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 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(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(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, "", "()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(reinterpret_cast(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); +} diff --git a/android/app/src/cpp/android_host_interface.h b/android/app/src/cpp/android_host_interface.h new file mode 100644 index 000000000..0e922d991 --- /dev/null +++ b/android/app/src/cpp/android_host_interface.h @@ -0,0 +1,60 @@ +#pragma once +#include "YBaseLib/Event.h" +#include "YBaseLib/Timer.h" +#include "core/host_interface.h" +#include +#include +#include +#include +#include +#include + +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 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 m_osd_messages; + std::mutex m_osd_messages_lock; + + std::mutex m_callback_mutex; + std::deque> 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; +}; diff --git a/android/app/src/cpp/main.cpp b/android/app/src/cpp/main.cpp new file mode 100644 index 000000000..4ba74ea11 --- /dev/null +++ b/android/app/src/cpp/main.cpp @@ -0,0 +1,17 @@ +#include "core/host_interface.h" +#include + +#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) {} \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..715f01421 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/AndroidHostInterface.java b/android/app/src/main/java/com/github/stenzek/duckstation/AndroidHostInterface.java new file mode 100644 index 000000000..a846f0eab --- /dev/null +++ b/android/app/src/main/java/com/github/stenzek/duckstation/AndroidHostInterface.java @@ -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); +} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java b/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java new file mode 100644 index 000000000..f598d5fd1 --- /dev/null +++ b/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java @@ -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); + } +} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/MainActivity.java b/android/app/src/main/java/com/github/stenzek/duckstation/MainActivity.java new file mode 100644 index 000000000..3250312a8 --- /dev/null +++ b/android/app/src/main/java/com/github/stenzek/duckstation/MainActivity.java @@ -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; + } +} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/NativeLibrary.java b/android/app/src/main/java/com/github/stenzek/duckstation/NativeLibrary.java new file mode 100644 index 000000000..2f7e759ed --- /dev/null +++ b/android/app/src/main/java/com/github/stenzek/duckstation/NativeLibrary.java @@ -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(); +} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/SettingsActivity.java b/android/app/src/main/java/com/github/stenzek/duckstation/SettingsActivity.java new file mode 100644 index 000000000..3ebf70f66 --- /dev/null +++ b/android/app/src/main/java/com/github/stenzek/duckstation/SettingsActivity.java @@ -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); + } + } +} \ No newline at end of file diff --git a/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 000000000..1f6bb2906 --- /dev/null +++ b/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable/ic_launcher_background.xml b/android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..0d025f9bf --- /dev/null +++ b/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/layout/activity_emulation.xml b/android/app/src/main/res/layout/activity_emulation.xml new file mode 100644 index 000000000..56df99a8d --- /dev/null +++ b/android/app/src/main/res/layout/activity_emulation.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 000000000..ddd8a31b1 --- /dev/null +++ b/android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/content_main.xml b/android/app/src/main/res/layout/content_main.xml new file mode 100644 index 000000000..a2189464f --- /dev/null +++ b/android/app/src/main/res/layout/content_main.xml @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/settings_activity.xml b/android/app/src/main/res/layout/settings_activity.xml new file mode 100644 index 000000000..de6591a20 --- /dev/null +++ b/android/app/src/main/res/layout/settings_activity.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/android/app/src/main/res/menu/menu_main.xml b/android/app/src/main/res/menu/menu_main.xml new file mode 100644 index 000000000..979c37381 --- /dev/null +++ b/android/app/src/main/res/menu/menu_main.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..eca70cfe5 --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..eca70cfe5 --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..898f3ed59 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 000000000..dffca3601 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..64ba76f75 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 000000000..dae5e0823 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..e5ed46597 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 000000000..14ed0af35 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..b0907cac3 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 000000000..d8ae03154 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..2c18de9e6 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 000000000..beed3cdd2 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/values/arrays.xml b/android/app/src/main/res/values/arrays.xml new file mode 100644 index 000000000..50881a898 --- /dev/null +++ b/android/app/src/main/res/values/arrays.xml @@ -0,0 +1,68 @@ + + + Auto-Detect + NTSC-J (Japan) + NTSC-U (US) + PAL (Europe, Australia) + + + Auto + NTSC-J + NTSC-U + PAL + + + Interpreter (Slowest) + Cached Interpreter (Faster) + Recompiler (Fastest) + + + Interpreter + CachedInterpreter + Recompiler + + + Hardware (OpenGL) + Software + + + OpenGL + Software + + + 1x (1024x512) + 2x (2048x1024) + 3x (3072x1536) + 4x (4096x2048) + 5x (5120x2560) + 6x (6144x3072) + 7x (7168x3584) + 8x (8192x4096) + 9x (9216x4608) + 10x (10240x5120) + 11x (11264x5632) + 12x (12288x6144) + 13x (13312x6656) + 14x (14336x7168) + 15x (15360x7680) + 16x (16384x8192) + + + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + 15 + 16 + + \ No newline at end of file diff --git a/android/app/src/main/res/values/attrs.xml b/android/app/src/main/res/values/attrs.xml new file mode 100644 index 000000000..7ce840eb6 --- /dev/null +++ b/android/app/src/main/res/values/attrs.xml @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml new file mode 100644 index 000000000..cbd5bb00e --- /dev/null +++ b/android/app/src/main/res/values/colors.xml @@ -0,0 +1,8 @@ + + + #008577 + #00574B + #D81B60 + + #66000000 + diff --git a/android/app/src/main/res/values/dimens.xml b/android/app/src/main/res/values/dimens.xml new file mode 100644 index 000000000..59a0b0c4f --- /dev/null +++ b/android/app/src/main/res/values/dimens.xml @@ -0,0 +1,3 @@ + + 16dp + diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml new file mode 100644 index 000000000..e967a7982 --- /dev/null +++ b/android/app/src/main/res/values/strings.xml @@ -0,0 +1,42 @@ + + DuckStation + Settings + Settings + + + Console + Behavior + Host Synchronization + CPU + GPU + + + Region + Auto + BIOS Path + Enable TTY Output + Fast Boot + + + Enable Speed Limiter + Pause On Start + + + Sync To Audio + Sync To Video + + + Execution Mode + Interpreter + + + Renderer + OpenGL + Display Linear Filtering + Resolution Scale + True 24-Bit Color (Disables Dithering) + + EmulationActivity + Dummy Button + DUMMY\nCONTENT + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 000000000..21184e4b1 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + diff --git a/android/app/src/main/res/xml/root_preferences.xml b/android/app/src/main/res/xml/root_preferences.xml new file mode 100644 index 000000000..39d5902c4 --- /dev/null +++ b/android/app/src/main/res/xml/root_preferences.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/test/java/com/github/stenzek/duckstation/ExampleUnitTest.java b/android/app/src/test/java/com/github/stenzek/duckstation/ExampleUnitTest.java new file mode 100644 index 000000000..b90ac4a49 --- /dev/null +++ b/android/app/src/test/java/com/github/stenzek/duckstation/ExampleUnitTest.java @@ -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 Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 000000000..fb516be20 --- /dev/null +++ b/android/build.gradle @@ -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 +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 000000000..199d16ede --- /dev/null +++ b/android/gradle.properties @@ -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 + diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..f6b961fd5 Binary files /dev/null and b/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..a7e334424 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/android/gradlew b/android/gradlew new file mode 100755 index 000000000..cccdd3d51 --- /dev/null +++ b/android/gradlew @@ -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" "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat new file mode 100644 index 000000000..e95643d6a --- /dev/null +++ b/android/gradlew.bat @@ -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 diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 000000000..7f5ee4013 --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,2 @@ +include ':app' +rootProject.name='DuckStation'