diff --git a/.github/gles.patch b/.github/gles.patch new file mode 100644 index 00000000..f1dc2c73 --- /dev/null +++ b/.github/gles.patch @@ -0,0 +1,280 @@ +diff --git a/src/core/renderer_gl/renderer_gl.cpp b/src/core/renderer_gl/renderer_gl.cpp +index a11a6ffa..77486a09 100644 +--- a/src/core/renderer_gl/renderer_gl.cpp ++++ b/src/core/renderer_gl/renderer_gl.cpp +@@ -357,27 +357,27 @@ void RendererGL::bindTexturesToSlots() { + } + + glActiveTexture(GL_TEXTURE0 + 3); +- glBindTexture(GL_TEXTURE_1D_ARRAY, lightLUTTextureArray); ++ // glBindTexture(GL_TEXTURE_1D_ARRAY, lightLUTTextureArray); + glActiveTexture(GL_TEXTURE0); + } + + void RendererGL::updateLightingLUT() { +- gpu.lightingLUTDirty = false; +- std::array u16_lightinglut; +- +- for (int i = 0; i < gpu.lightingLUT.size(); i++) { +- uint64_t value = gpu.lightingLUT[i] & ((1 << 12) - 1); +- u16_lightinglut[i] = value * 65535 / 4095; +- } +- +- glActiveTexture(GL_TEXTURE0 + 3); +- glBindTexture(GL_TEXTURE_1D_ARRAY, lightLUTTextureArray); +- glTexImage2D(GL_TEXTURE_1D_ARRAY, 0, GL_R16, 256, Lights::LUT_Count, 0, GL_RED, GL_UNSIGNED_SHORT, u16_lightinglut.data()); +- glTexParameteri(GL_TEXTURE_1D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR); +- glTexParameteri(GL_TEXTURE_1D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR); +- glTexParameteri(GL_TEXTURE_1D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); +- glTexParameteri(GL_TEXTURE_1D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); +- glActiveTexture(GL_TEXTURE0); ++ // gpu.lightingLUTDirty = false; ++ // std::array u16_lightinglut; ++ ++ // for (int i = 0; i < gpu.lightingLUT.size(); i++) { ++ // uint64_t value = gpu.lightingLUT[i] & ((1 << 12) - 1); ++ // u16_lightinglut[i] = value * 65535 / 4095; ++ // } ++ ++ // glActiveTexture(GL_TEXTURE0 + 3); ++ // glBindTexture(GL_TEXTURE_1D_ARRAY, lightLUTTextureArray); ++ // glTexImage2D(GL_TEXTURE_1D_ARRAY, 0, GL_R16, 256, Lights::LUT_Count, 0, GL_RED, GL_UNSIGNED_SHORT, u16_lightinglut.data()); ++ // glTexParameteri(GL_TEXTURE_1D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR); ++ // glTexParameteri(GL_TEXTURE_1D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR); ++ // glTexParameteri(GL_TEXTURE_1D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); ++ // glTexParameteri(GL_TEXTURE_1D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); ++ // glActiveTexture(GL_TEXTURE0); + } + + void RendererGL::drawVertices(PICA::PrimType primType, std::span vertices) { +diff --git a/src/host_shaders/opengl_display.frag b/src/host_shaders/opengl_display.frag +index 612671c8..1937f711 100644 +--- a/src/host_shaders/opengl_display.frag ++++ b/src/host_shaders/opengl_display.frag +@@ -1,4 +1,5 @@ +-#version 410 core ++#version 300 es ++precision mediump float; + in vec2 UV; + out vec4 FragColor; + +diff --git a/src/host_shaders/opengl_display.vert b/src/host_shaders/opengl_display.vert +index 990e2f80..2e7842ac 100644 +--- a/src/host_shaders/opengl_display.vert ++++ b/src/host_shaders/opengl_display.vert +@@ -1,4 +1,5 @@ +-#version 410 core ++#version 300 es ++precision mediump float; + out vec2 UV; + + void main() { +diff --git a/src/host_shaders/opengl_fragment_shader.frag b/src/host_shaders/opengl_fragment_shader.frag +index f6fa6c55..bb88e278 100644 +--- a/src/host_shaders/opengl_fragment_shader.frag ++++ b/src/host_shaders/opengl_fragment_shader.frag +@@ -1,4 +1,5 @@ +-#version 410 core ++#version 300 es ++precision mediump float; + + in vec3 v_tangent; + in vec3 v_normal; +@@ -27,7 +28,7 @@ uniform bool u_depthmapEnable; + uniform sampler2D u_tex0; + uniform sampler2D u_tex1; + uniform sampler2D u_tex2; +-uniform sampler1DArray u_tex_lighting_lut; ++// uniform sampler1DArray u_tex_lighting_lut; + + uniform uint u_picaRegs[0x200 - 0x48]; + +@@ -145,16 +146,23 @@ vec4 tevCalculateCombiner(int tev_id) { + #define RR_LUT 6u + + float lutLookup(uint lut, uint light, float value) { +- if (lut >= FR_LUT && lut <= RR_LUT) lut -= 1; +- if (lut == SP_LUT) lut = light + 8; +- return texture(u_tex_lighting_lut, vec2(value, lut)).r; ++ // if (lut >= FR_LUT && lut <= RR_LUT) lut -= 1; ++ // if (lut == SP_LUT) lut = light + 8; ++ // return texture(u_tex_lighting_lut, vec2(value, lut)).r; ++ return 0.0; ++} ++ ++// some gles versions have bitfieldExtract and complain if you redefine it, some don't and compile error, using this instead ++uint bitfieldExtractCompat(uint val, int off, int size) { ++ uint mask = uint((1 << size) - 1); ++ return uint(val >> off) & mask; + } + + vec3 regToColor(uint reg) { + // Normalization scale to convert from [0...255] to [0.0...1.0] + const float scale = 1.0 / 255.0; + +- return scale * vec3(float(bitfieldExtract(reg, 20, 8)), float(bitfieldExtract(reg, 10, 8)), float(bitfieldExtract(reg, 00, 8))); ++ return scale * vec3(float(bitfieldExtractCompat(reg, 20, 8)), float(bitfieldExtractCompat(reg, 10, 8)), float(bitfieldExtractCompat(reg, 00, 8))); + } + + // Convert an arbitrary-width floating point literal to an f32 +@@ -189,7 +197,7 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { + vec3 view = normalize(v_view); + + uint GPUREG_LIGHTING_ENABLE = readPicaReg(0x008Fu); +- if (bitfieldExtract(GPUREG_LIGHTING_ENABLE, 0, 1) == 0u) { ++ if (bitfieldExtractCompat(GPUREG_LIGHTING_ENABLE, 0, 1) == 0u) { + primary_color = secondary_color = vec4(1.0); + return; + } +@@ -213,7 +221,7 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { + bool error_unimpl = false; + + for (uint i = 0u; i < GPUREG_LIGHTING_NUM_LIGHTS; i++) { +- uint light_id = bitfieldExtract(GPUREG_LIGHTING_LIGHT_PERMUTATION, int(i * 3u), 3); ++ uint light_id = bitfieldExtractCompat(GPUREG_LIGHTING_LIGHT_PERMUTATION, int(i * 3u), 3); + + uint GPUREG_LIGHTi_SPECULAR0 = readPicaReg(0x0140u + 0x10u * light_id); + uint GPUREG_LIGHTi_SPECULAR1 = readPicaReg(0x0141u + 0x10u * light_id); +@@ -224,14 +232,14 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { + uint GPUREG_LIGHTi_CONFIG = readPicaReg(0x0149u + 0x10u * light_id); + + vec3 light_vector = normalize(vec3( +- decodeFP(bitfieldExtract(GPUREG_LIGHTi_VECTOR_LOW, 0, 16), 5u, 10u), decodeFP(bitfieldExtract(GPUREG_LIGHTi_VECTOR_LOW, 16, 16), 5u, 10u), +- decodeFP(bitfieldExtract(GPUREG_LIGHTi_VECTOR_HIGH, 0, 16), 5u, 10u) ++ decodeFP(bitfieldExtractCompat(GPUREG_LIGHTi_VECTOR_LOW, 0, 16), 5u, 10u), decodeFP(bitfieldExtractCompat(GPUREG_LIGHTi_VECTOR_LOW, 16, 16), 5u, 10u), ++ decodeFP(bitfieldExtractCompat(GPUREG_LIGHTi_VECTOR_HIGH, 0, 16), 5u, 10u) + )); + + vec3 half_vector; + + // Positional Light +- if (bitfieldExtract(GPUREG_LIGHTi_CONFIG, 0, 1) == 0u) { ++ if (bitfieldExtractCompat(GPUREG_LIGHTi_CONFIG, 0, 1) == 0u) { + // error_unimpl = true; + half_vector = normalize(normalize(light_vector + v_view) + view); + } +@@ -242,12 +250,12 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { + } + + for (int c = 0; c < 7; c++) { +- if (bitfieldExtract(GPUREG_LIGHTING_CONFIG1, 16 + c, 1) == 0u) { +- uint scale_id = bitfieldExtract(GPUREG_LIGHTING_LUTINPUT_SCALE, c * 4, 3); ++ if (bitfieldExtractCompat(GPUREG_LIGHTING_CONFIG1, 16 + c, 1) == 0u) { ++ uint scale_id = bitfieldExtractCompat(GPUREG_LIGHTING_LUTINPUT_SCALE, c * 4, 3); + float scale = float(1u << scale_id); + if (scale_id >= 6u) scale /= 256.0; + +- uint input_id = bitfieldExtract(GPUREG_LIGHTING_LUTINPUT_SELECT, c * 4, 3); ++ uint input_id = bitfieldExtractCompat(GPUREG_LIGHTING_LUTINPUT_SELECT, c * 4, 3); + if (input_id == 0u) + d[c] = dot(normal, half_vector); + else if (input_id == 1u) +@@ -260,9 +268,9 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { + uint GPUREG_LIGHTi_SPOTDIR_LOW = readPicaReg(0x0146u + 0x10u * light_id); + uint GPUREG_LIGHTi_SPOTDIR_HIGH = readPicaReg(0x0147u + 0x10u * light_id); + vec3 spot_light_vector = normalize(vec3( +- decodeFP(bitfieldExtract(GPUREG_LIGHTi_SPOTDIR_LOW, 0, 16), 1u, 11u), +- decodeFP(bitfieldExtract(GPUREG_LIGHTi_SPOTDIR_LOW, 16, 16), 1u, 11u), +- decodeFP(bitfieldExtract(GPUREG_LIGHTi_SPOTDIR_HIGH, 0, 16), 1u, 11u) ++ decodeFP(bitfieldExtractCompat(GPUREG_LIGHTi_SPOTDIR_LOW, 0, 16), 1u, 11u), ++ decodeFP(bitfieldExtractCompat(GPUREG_LIGHTi_SPOTDIR_LOW, 16, 16), 1u, 11u), ++ decodeFP(bitfieldExtractCompat(GPUREG_LIGHTi_SPOTDIR_HIGH, 0, 16), 1u, 11u) + )); + d[c] = dot(-light_vector, spot_light_vector); // -L dot P (aka Spotlight aka SP); + } else if (input_id == 5u) { +@@ -273,13 +281,13 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { + } + + d[c] = lutLookup(uint(c), light_id, d[c] * 0.5 + 0.5) * scale; +- if (bitfieldExtract(GPUREG_LIGHTING_LUTINPUT_ABS, 2 * c, 1) != 0u) d[c] = abs(d[c]); ++ if (bitfieldExtractCompat(GPUREG_LIGHTING_LUTINPUT_ABS, 2 * c, 1) != 0u) d[c] = abs(d[c]); + } else { + d[c] = 1.0; + } + } + +- uint lookup_config = bitfieldExtract(GPUREG_LIGHTi_CONFIG, 4, 4); ++ uint lookup_config = bitfieldExtractCompat(GPUREG_LIGHTi_CONFIG, 4, 4); + if (lookup_config == 0u) { + d[D1_LUT] = 0.0; + d[FR_LUT] = 0.0; +@@ -310,7 +318,7 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { + float NdotL = dot(normal, light_vector); // Li dot N + + // Two sided diffuse +- if (bitfieldExtract(GPUREG_LIGHTi_CONFIG, 1, 1) == 0u) ++ if (bitfieldExtractCompat(GPUREG_LIGHTi_CONFIG, 1, 1) == 0u) + NdotL = max(0.0, NdotL); + else + NdotL = abs(NdotL); +@@ -321,8 +329,8 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { + secondary_color.rgb += light_factor * (regToColor(GPUREG_LIGHTi_SPECULAR0) * d[D0_LUT] + + regToColor(GPUREG_LIGHTi_SPECULAR1) * d[D1_LUT] * vec3(d[RR_LUT], d[RG_LUT], d[RB_LUT])); + } +- uint fresnel_output1 = bitfieldExtract(GPUREG_LIGHTING_CONFIG0, 2, 1); +- uint fresnel_output2 = bitfieldExtract(GPUREG_LIGHTING_CONFIG0, 3, 1); ++ uint fresnel_output1 = bitfieldExtractCompat(GPUREG_LIGHTING_CONFIG0, 2, 1); ++ uint fresnel_output2 = bitfieldExtractCompat(GPUREG_LIGHTING_CONFIG0, 3, 1); + + if (fresnel_output1 == 1u) primary_color.a = d[FR_LUT]; + if (fresnel_output2 == 1u) secondary_color.a = d[FR_LUT]; +diff --git a/src/host_shaders/opengl_vertex_shader.vert b/src/host_shaders/opengl_vertex_shader.vert +index a25d7a6d..7cf40398 100644 +--- a/src/host_shaders/opengl_vertex_shader.vert ++++ b/src/host_shaders/opengl_vertex_shader.vert +@@ -1,4 +1,6 @@ +-#version 410 core ++#version 300 es ++precision mediump float; ++precision mediump int; + + layout(location = 0) in vec4 a_coords; + layout(location = 1) in vec4 a_quaternion; +@@ -20,7 +22,7 @@ out vec2 v_texcoord2; + flat out vec4 v_textureEnvColor[6]; + flat out vec4 v_textureEnvBufferColor; + +-out float gl_ClipDistance[2]; ++// out float gl_ClipDistance[2]; + + // TEV uniforms + uniform uint u_textureEnvColor[6]; +@@ -93,6 +95,6 @@ void main() { + ); + + // There's also another, always-on clipping plane based on vertex z +- gl_ClipDistance[0] = -a_coords.z; +- gl_ClipDistance[1] = dot(clipData, a_coords); ++ // gl_ClipDistance[0] = -a_coords.z; ++ // gl_ClipDistance[1] = dot(clipData, a_coords); + } +diff --git a/third_party/opengl/opengl.hpp b/third_party/opengl/opengl.hpp +index f368f573..5ead7f63 100644 +--- a/third_party/opengl/opengl.hpp ++++ b/third_party/opengl/opengl.hpp +@@ -520,21 +520,21 @@ namespace OpenGL { + static void enableBlend() { glEnable(GL_BLEND); } + static void disableBlend() { glDisable(GL_BLEND); } + static void enableLogicOp() { glEnable(GL_COLOR_LOGIC_OP); } +- static void disableLogicOp() { glDisable(GL_COLOR_LOGIC_OP); } ++ static void disableLogicOp() { /* glDisable(GL_COLOR_LOGIC_OP); */ } + static void enableDepth() { glEnable(GL_DEPTH_TEST); } + static void disableDepth() { glDisable(GL_DEPTH_TEST); } + static void enableStencil() { glEnable(GL_STENCIL_TEST); } + static void disableStencil() { glDisable(GL_STENCIL_TEST); } + +- static void enableClipPlane(GLuint index) { glEnable(GL_CLIP_DISTANCE0 + index); } +- static void disableClipPlane(GLuint index) { glDisable(GL_CLIP_DISTANCE0 + index); } ++ static void enableClipPlane(GLuint index) { /* glEnable(GL_CLIP_DISTANCE0 + index); */ } ++ static void disableClipPlane(GLuint index) { /* glDisable(GL_CLIP_DISTANCE0 + index); */ } + + static void setDepthFunc(DepthFunc func) { glDepthFunc(static_cast(func)); } + static void setColourMask(GLboolean r, GLboolean g, GLboolean b, GLboolean a) { glColorMask(r, g, b, a); } + static void setDepthMask(GLboolean mask) { glDepthMask(mask); } + + // TODO: Add a proper enum for this +- static void setLogicOp(GLenum op) { glLogicOp(op); } ++ static void setLogicOp(GLenum op) { /* glLogicOp(op); */ } + + enum Primitives { + Triangle = GL_TRIANGLES, diff --git a/.github/workflows/Android_Build.yml b/.github/workflows/Android_Build.yml new file mode 100644 index 00000000..137577c1 --- /dev/null +++ b/.github/workflows/Android_Build.yml @@ -0,0 +1,113 @@ +name: Android Build + +on: + push: + branches: + - master + pull_request: + +jobs: + x64: + runs-on: ubuntu-latest + + strategy: + matrix: + build_type: + - release + + steps: + - name: Set BUILD_TYPE variable + run: echo "BUILD_TYPE=${{ matrix.build_type }}" >> $GITHUB_ENV + + - uses: actions/checkout@v2 + - name: Fetch submodules + run: git submodule update --init --recursive + + - name: Setup Vulkan SDK + uses: humbletim/setup-vulkan-sdk@v1.2.0 + with: + vulkan-query-version: latest + vulkan-use-cache: true + vulkan-components: Vulkan-Headers, Vulkan-Loader, SPIRV-Tools, Glslang + + - name: Setup Java + uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: '17' + + - name: Configure CMake + run: cmake -B ${{github.workspace}}/build -DBUILD_HYDRA_CORE=1 -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_TOOLCHAIN_FILE=${ANDROID_NDK_ROOT}/build/cmake/android.toolchain.cmake -DANDROID_ABI=x86_64 -DENABLE_VULKAN=0 -DENABLE_USER_BUILD=ON + + - name: Build + run: | + # Apply patch for GLES compatibility + git apply ./.github/gles.patch + # Build the project with CMake + cmake --build ${{github.workspace}}/build --config ${{ env.BUILD_TYPE }} + # Move the generated library to the appropriate location + mv ./build/libAlber.so ./src/pandroid/app/src/main/jniLibs/x86_64/ + # Build the Android app with Gradle + cd src/pandroid + ./gradlew assemble${{ env.BUILD_TYPE }} + cd ../.. + + - name: Upload artifacts + uses: actions/upload-artifact@v2 + with: + name: Android APKs (x86-64) + path: | + ./src/pandroid/app/build/outputs/apk/${{ env.BUILD_TYPE }}/app-${{ env.BUILD_TYPE }}.apk + + arm64: + runs-on: ubuntu-latest + + strategy: + matrix: + build_type: + - release + + steps: + - name: Set BUILD_TYPE variable + run: echo "BUILD_TYPE=${{ matrix.build_type }}" >> $GITHUB_ENV + + - uses: actions/checkout@v2 + - name: Fetch submodules + run: git submodule update --init --recursive + + - name: Setup Vulkan SDK + uses: humbletim/setup-vulkan-sdk@v1.2.0 + with: + vulkan-query-version: latest + vulkan-use-cache: true + vulkan-components: Vulkan-Headers, Vulkan-Loader, SPIRV-Tools, Glslang + + - name: Setup Java + uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: '17' + + - name: Configure CMake + run: cmake -B ${{github.workspace}}/build -DBUILD_HYDRA_CORE=1 -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_TOOLCHAIN_FILE=${ANDROID_NDK_ROOT}/build/cmake/android.toolchain.cmake -DANDROID_ABI=arm64-v8a -DENABLE_VULKAN=0 -DENABLE_USER_BUILD=ON -DCMAKE_CXX_FLAGS="-march=armv8-a+crypto" + + - name: Build + run: | + # Apply patch for GLES compatibility + git apply ./.github/gles.patch + # Build the project with CMake + cmake --build ${{github.workspace}}/build --config ${{ env.BUILD_TYPE }} + # Move the generated library to the appropriate location + mv ./build/libAlber.so ./src/pandroid/app/src/main/jniLibs/arm64-v8a/ + # Build the Android app with Gradle + cd src/pandroid + ./gradlew assemble${{ env.BUILD_TYPE }} + ls -R app/build/outputs + cd ../.. + + - name: Upload artifacts + uses: actions/upload-artifact@v2 + with: + name: Android APKs (arm64) + path: | + ./src/pandroid/app/build/outputs/apk/${{ env.BUILD_TYPE }}/app-${{ env.BUILD_TYPE }}.apk diff --git a/.github/workflows/HTTP_Build.yml b/.github/workflows/HTTP_Build.yml index 24cd19bc..7bfe9c7f 100644 --- a/.github/workflows/HTTP_Build.yml +++ b/.github/workflows/HTTP_Build.yml @@ -22,18 +22,24 @@ jobs: - uses: actions/checkout@v2 - name: Fetch submodules run: git submodule update --init --recursive + + - name: Install newer Clang + run: | + wget https://apt.llvm.org/llvm.sh + chmod +x ./llvm.sh + sudo ./llvm.sh 17 - name: Setup Vulkan SDK uses: humbletim/setup-vulkan-sdk@v1.2.0 with: vulkan-query-version: latest vulkan-use-cache: true - vulkan-components: Vulkan-Headers, Vulkan-Loader, Glslang + vulkan-components: Vulkan-Headers, Vulkan-Loader, SPIRV-Tools, Glslang - name: Configure CMake # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type - run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DENABLE_USER_BUILD=ON -DENABLE_HTTP_SERVER=ON + run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_C_COMPILER=clang-17 -DCMAKE_CXX_COMPILER=clang++-17 -DENABLE_USER_BUILD=ON -DENABLE_HTTP_SERVER=ON - name: Build # Build your program with the given configuration diff --git a/.github/workflows/Hydra_Build.yml b/.github/workflows/Hydra_Build.yml index 18194059..3387d46d 100644 --- a/.github/workflows/Hydra_Build.yml +++ b/.github/workflows/Hydra_Build.yml @@ -24,7 +24,7 @@ jobs: with: vulkan-query-version: latest vulkan-use-cache: true - vulkan-components: Vulkan-Headers, Vulkan-Loader, Glslang + vulkan-components: Vulkan-Headers, Vulkan-Loader, SPIRV-Tools, Glslang - name: Configure CMake run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DENABLE_USER_BUILD=ON -DBUILD_HYDRA_CORE=ON @@ -52,7 +52,7 @@ jobs: with: vulkan-query-version: latest vulkan-use-cache: true - vulkan-components: Vulkan-Headers, Vulkan-Loader, Glslang + vulkan-components: Vulkan-Headers, Vulkan-Loader, SPIRV-Tools, Glslang - name: Configure CMake run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DENABLE_USER_BUILD=ON -DBUILD_HYDRA_CORE=ON @@ -77,16 +77,22 @@ jobs: - name: Install misc packages run: | sudo apt-get update && sudo apt install libx11-dev libgl1-mesa-glx mesa-common-dev libfuse2 libwayland-dev + + - name: Install newer Clang + run: | + wget https://apt.llvm.org/llvm.sh + chmod +x ./llvm.sh + sudo ./llvm.sh 17 - name: Setup Vulkan SDK uses: humbletim/setup-vulkan-sdk@v1.2.0 with: vulkan-query-version: latest vulkan-use-cache: true - vulkan-components: Vulkan-Headers, Vulkan-Loader, Glslang + vulkan-components: Vulkan-Headers, Vulkan-Loader, SPIRV-Tools, Glslang - name: Configure CMake - run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DENABLE_USER_BUILD=ON -DBUILD_HYDRA_CORE=ON + run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_C_COMPILER=clang-17 -DCMAKE_CXX_COMPILER=clang++-17 -DENABLE_USER_BUILD=ON -DBUILD_HYDRA_CORE=ON - name: Build run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} @@ -114,7 +120,7 @@ jobs: with: vulkan-query-version: latest vulkan-use-cache: true - vulkan-components: Vulkan-Headers, Vulkan-Loader, Glslang + vulkan-components: Vulkan-Headers, Vulkan-Loader, SPIRV-Tools, Glslang - name: Configure CMake run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_TOOLCHAIN_FILE=${ANDROID_NDK_ROOT}/build/cmake/android.toolchain.cmake -DANDROID_ABI=x86_64 -DBUILD_HYDRA_CORE=1 -DENABLE_VULKAN=0 diff --git a/.github/workflows/Linux_AppImage_Build.yml b/.github/workflows/Linux_AppImage_Build.yml index 0a304109..507187a3 100644 --- a/.github/workflows/Linux_AppImage_Build.yml +++ b/.github/workflows/Linux_AppImage_Build.yml @@ -30,15 +30,7 @@ jobs: run: | wget https://apt.llvm.org/llvm.sh chmod +x ./llvm.sh - sudo ./llvm.sh 16 - - - name: Install newer CMake - run: | - sudo curl -s https://apt.kitware.com/keys/kitware-archive-latest.asc | gpg --dearmor | tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null - sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 42D5A192B819C5DA - sudo add-apt-repository -y 'deb https://apt.kitware.com/ubuntu/ focal main' - sudo apt-get update - sudo apt-get install cmake + sudo ./llvm.sh 17 - name: Setup Vulkan SDK run: | @@ -50,7 +42,7 @@ jobs: - name: Configure CMake # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type - run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_C_COMPILER=clang-16 -DCMAKE_CXX_COMPILER=clang++-16 -DENABLE_USER_BUILD=ON + run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_C_COMPILER=clang-17 -DCMAKE_CXX_COMPILER=clang++-17 -DENABLE_USER_BUILD=ON - name: Build # Build your program with the given configuration diff --git a/.github/workflows/Linux_Build.yml b/.github/workflows/Linux_Build.yml index bbc79d81..78e5cc5a 100644 --- a/.github/workflows/Linux_Build.yml +++ b/.github/workflows/Linux_Build.yml @@ -26,17 +26,23 @@ jobs: - name: Install misc packages run: sudo apt-get update && sudo apt install libx11-dev libgl1-mesa-glx mesa-common-dev + - name: Install newer Clang + run: | + wget https://apt.llvm.org/llvm.sh + chmod +x ./llvm.sh + sudo ./llvm.sh 17 + - name: Setup Vulkan SDK uses: humbletim/setup-vulkan-sdk@v1.2.0 with: vulkan-query-version: latest vulkan-use-cache: true - vulkan-components: Vulkan-Headers, Vulkan-Loader, Glslang + vulkan-components: Vulkan-Headers, Vulkan-Loader, SPIRV-Tools, Glslang - name: Configure CMake # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type - run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DENABLE_USER_BUILD=ON + run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_C_COMPILER=clang-17 -DCMAKE_CXX_COMPILER=clang++-17 -DENABLE_USER_BUILD=ON - name: Build # Build your program with the given configuration diff --git a/.github/workflows/MacOS_Build.yml b/.github/workflows/MacOS_Build.yml index 7e54c5a5..b659e3fa 100644 --- a/.github/workflows/MacOS_Build.yml +++ b/.github/workflows/MacOS_Build.yml @@ -28,7 +28,7 @@ jobs: with: vulkan-query-version: latest vulkan-use-cache: true - vulkan-components: Vulkan-Headers, Vulkan-Loader, Glslang + vulkan-components: Vulkan-Headers, Vulkan-Loader, SPIRV-Tools, Glslang - name: Configure CMake # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. diff --git a/.github/workflows/Qt_Build.yml b/.github/workflows/Qt_Build.yml index 0cb007f4..0b3910d7 100644 --- a/.github/workflows/Qt_Build.yml +++ b/.github/workflows/Qt_Build.yml @@ -30,7 +30,7 @@ jobs: with: vulkan-query-version: latest vulkan-use-cache: true - vulkan-components: Vulkan-Headers, Vulkan-Loader, Glslang + vulkan-components: Vulkan-Headers, Vulkan-Loader, SPIRV-Tools, Glslang - name: Configure CMake run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DENABLE_USER_BUILD=ON -DENABLE_QT_GUI=ON @@ -63,7 +63,7 @@ jobs: with: vulkan-query-version: latest vulkan-use-cache: true - vulkan-components: Vulkan-Headers, Vulkan-Loader, Glslang + vulkan-components: Vulkan-Headers, Vulkan-Loader, SPIRV-Tools, Glslang - name: Install bundle dependencies run: | @@ -114,15 +114,7 @@ jobs: run: | wget https://apt.llvm.org/llvm.sh chmod +x ./llvm.sh - sudo ./llvm.sh 16 - - - name: Install newer CMake - run: | - sudo curl -s https://apt.kitware.com/keys/kitware-archive-latest.asc | gpg --dearmor | tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null - sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 42D5A192B819C5DA - sudo add-apt-repository -y 'deb https://apt.kitware.com/ubuntu/ focal main' - sudo apt-get update - sudo apt-get install cmake + sudo ./llvm.sh 17 - name: Setup Vulkan SDK run: | @@ -132,7 +124,7 @@ jobs: sudo apt install vulkan-sdk - name: Configure CMake - run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_C_COMPILER=clang-16 -DCMAKE_CXX_COMPILER=clang++-16 -DENABLE_USER_BUILD=ON -DENABLE_QT_GUI=ON + run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_C_COMPILER=clang-17 -DCMAKE_CXX_COMPILER=clang++-17 -DENABLE_USER_BUILD=ON -DENABLE_QT_GUI=ON - name: Build run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} diff --git a/.github/workflows/Windows_Build.yml b/.github/workflows/Windows_Build.yml index 653692f3..ae9fd587 100644 --- a/.github/workflows/Windows_Build.yml +++ b/.github/workflows/Windows_Build.yml @@ -28,7 +28,7 @@ jobs: with: vulkan-query-version: latest vulkan-use-cache: true - vulkan-components: Vulkan-Headers, Vulkan-Loader, Glslang + vulkan-components: Vulkan-Headers, Vulkan-Loader, SPIRV-Tools, Glslang - name: Configure CMake # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. diff --git a/.gitignore b/.gitignore index ee5f1331..7214ef50 100644 --- a/.gitignore +++ b/.gitignore @@ -58,9 +58,10 @@ fb.bat *.3ds *.3dsx *.app +*.cia *.cci *.cxi *.elf *.smdh -config.toml \ No newline at end of file +config.toml diff --git a/.gitmodules b/.gitmodules index 93bb8f9d..f1e8f469 100644 --- a/.gitmodules +++ b/.gitmodules @@ -43,6 +43,12 @@ [submodule "third_party/hydra_core"] path = third_party/hydra_core url = https://github.com/hydra-emu/core +[submodule "third_party/zep"] + path = third_party/zep + url = https://github.com/Panda3DS-emu/zep +[submodule "third_party/oaknut"] + path = third_party/oaknut + url = https://github.com/merryhime/oaknut [submodule "third_party/luv"] path = third_party/luv url = https://github.com/luvit/luv diff --git a/CMakeLists.txt b/CMakeLists.txt index 0d8278cd..f9238264 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -103,6 +103,10 @@ add_compile_definitions(BOOST_NO_CXX98_FUNCTION_BASE) # Forbid Boost from using add_library(boost INTERFACE) target_include_directories(boost SYSTEM INTERFACE ${Boost_INCLUDE_DIR}) +if(ANDROID) + set(CRYPTOPP_OPT_DISABLE_ASM ON CACHE BOOL "" FORCE) +endif() + set(CRYPTOPP_BUILD_TESTING OFF) add_subdirectory(third_party/cryptopp) add_subdirectory(third_party/glad) @@ -133,6 +137,9 @@ endif() # Check for arm64 if (CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64" OR CMAKE_SYSTEM_PROCESSOR STREQUAL "arm64") set(HOST_ARM64 TRUE) + add_subdirectory(third_party/oaknut) # Add Oaknut submodule for arm64 JITs + include_directories(third_party/oaknut/include) + add_compile_definitions(PANDA3DS_DYNAPICA_SUPPORTED) add_compile_definitions(PANDA3DS_ARM64_HOST) else() set(HOST_ARM64 FALSE) @@ -177,6 +184,7 @@ set(SERVICE_SOURCE_FILES src/core/services/service_manager.cpp src/core/services set(PICA_SOURCE_FILES src/core/PICA/gpu.cpp src/core/PICA/regs.cpp src/core/PICA/shader_unit.cpp src/core/PICA/shader_interpreter.cpp src/core/PICA/dynapica/shader_rec.cpp src/core/PICA/dynapica/shader_rec_emitter_x64.cpp src/core/PICA/pica_hash.cpp + src/core/PICA/dynapica/shader_rec_emitter_arm64.cpp ) set(LOADER_SOURCE_FILES src/core/loader/elf.cpp src/core/loader/ncsd.cpp src/core/loader/ncch.cpp src/core/loader/3dsx.cpp src/core/loader/lz77.cpp) @@ -185,20 +193,32 @@ set(FS_SOURCE_FILES src/core/fs/archive_self_ncch.cpp src/core/fs/archive_save_d src/core/fs/ivfc.cpp src/core/fs/archive_user_save_data.cpp src/core/fs/archive_system_save_data.cpp ) -set(APPLET_SOURCE_FILES src/core/applets/applet.cpp src/core/applets/mii_selector.cpp src/core/applets/software_keyboard.cpp src/core/applets/applet_manager.cpp) +set(APPLET_SOURCE_FILES src/core/applets/applet.cpp src/core/applets/mii_selector.cpp src/core/applets/software_keyboard.cpp src/core/applets/applet_manager.cpp + src/core/applets/error_applet.cpp +) set(RENDERER_SW_SOURCE_FILES src/core/renderer_sw/renderer_sw.cpp) # Frontend source files -if(ENABLE_QT_GUI) - set(FRONTEND_SOURCE_FILES src/panda_qt/main.cpp src/panda_qt/screen.cpp src/panda_qt/main_window.cpp) - set(FRONTEND_HEADER_FILES include/panda_qt/screen.hpp include/panda_qt/main_window.hpp) +if(NOT ANDROID) + if(ENABLE_QT_GUI) + set(FRONTEND_SOURCE_FILES src/panda_qt/main.cpp src/panda_qt/screen.cpp src/panda_qt/main_window.cpp src/panda_qt/about_window.cpp + src/panda_qt/config_window.cpp src/panda_qt/zep.cpp src/panda_qt/text_editor.cpp src/panda_qt/cheats_window.cpp + ) + set(FRONTEND_HEADER_FILES include/panda_qt/screen.hpp include/panda_qt/main_window.hpp include/panda_qt/about_window.hpp + include/panda_qt/config_window.hpp include/panda_qt/text_editor.hpp include/panda_qt/cheats_window.hpp + ) - source_group("Source Files\\Qt" FILES ${FRONTEND_SOURCE_FILES}) - source_group("Header Files\\Qt" FILES ${FRONTEND_HEADER_FILES}) - include_directories(${Qt6Gui_PRIVATE_INCLUDE_DIRS}) -else() - set(FRONTEND_SOURCE_FILES src/panda_sdl/main.cpp src/panda_sdl/frontend_sdl.cpp) - set(FRONTEND_HEADER_FILES "") + source_group("Source Files\\Qt" FILES ${FRONTEND_SOURCE_FILES}) + source_group("Header Files\\Qt" FILES ${FRONTEND_HEADER_FILES}) + include_directories(${Qt6Gui_PRIVATE_INCLUDE_DIRS}) + + include_directories(third_party/zep/include) # Include zep for text editor usage + configure_file(third_party/zep/cmake/config_app.h.cmake ${CMAKE_BINARY_DIR}/zep_config/config_app.h) + include_directories(${CMAKE_BINARY_DIR}/zep_config) + else() + set(FRONTEND_SOURCE_FILES src/panda_sdl/main.cpp src/panda_sdl/frontend_sdl.cpp) + set(FRONTEND_HEADER_FILES "") + endif() endif() set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp @@ -231,6 +251,7 @@ set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp include/services/news_u.hpp include/applets/software_keyboard.hpp include/applets/applet_manager.hpp include/fs/archive_user_save_data.hpp include/services/amiibo_device.hpp include/services/nfc_types.hpp include/swap.hpp include/services/csnd.hpp include/services/nwm_uds.hpp include/fs/archive_system_save_data.hpp include/lua_manager.hpp include/memory_mapped_file.hpp include/hydra_icon.hpp + include/PICA/dynapica/shader_rec_emitter_arm64.hpp include/scheduler.hpp include/applets/error_applet.hpp ) cmrc_add_resource_library( @@ -392,6 +413,10 @@ if(ENABLE_VULKAN) set(ALL_SOURCES ${ALL_SOURCES} ${RENDERER_VK_SOURCE_FILES}) endif() +if(ANDROID) + set(ALL_SOURCES ${ALL_SOURCES} src/jni_driver.cpp) +endif() + if(BUILD_HYDRA_CORE) include_directories(third_party/hydra_core/include) add_library(Alber SHARED ${ALL_SOURCES} src/hydra_core.cpp) @@ -400,11 +425,19 @@ else() add_executable(Alber ${ALL_SOURCES}) endif() +if(ANDROID) + target_link_libraries(Alber PRIVATE EGL log) +endif() + if(ENABLE_LTO OR ENABLE_USER_BUILD) set_target_properties(Alber PROPERTIES INTERPROCEDURAL_OPTIMIZATION TRUE) endif() -target_link_libraries(Alber PRIVATE dynarmic SDL2-static cryptopp glad resources_console_fonts) +target_link_libraries(Alber PRIVATE dynarmic cryptopp glad resources_console_fonts) + +if(NOT ANDROID) + target_link_libraries(Alber PRIVATE SDL2-static) +endif() if(ENABLE_DISCORD_RPC AND NOT ANDROID) target_compile_definitions(Alber PUBLIC "PANDA3DS_ENABLE_DISCORD_RPC=1") @@ -428,6 +461,9 @@ endif() if(ENABLE_QT_GUI) target_compile_definitions(Alber PUBLIC "PANDA3DS_FRONTEND_QT=1") + target_compile_definitions(Alber PUBLIC "ZEP_QT=1") + target_compile_definitions(Alber PUBLIC "ZEP_FEATURE_CPP_FILE_SYSTEM=1") + target_link_libraries(Alber PRIVATE Qt6::Widgets) if(LINUX OR FREEBSD) @@ -443,7 +479,7 @@ if(ENABLE_QT_GUI) qt_add_resources(Alber "app_images" PREFIX "/" FILES - docs/img/rsob_icon.png + docs/img/rsob_icon.png docs/img/rstarstruck_icon.png ) else() target_compile_definitions(Alber PUBLIC "PANDA3DS_FRONTEND_SDL=1") diff --git a/docs/img/rstarstruck_icon.png b/docs/img/rstarstruck_icon.png new file mode 100644 index 00000000..2b4aab06 Binary files /dev/null and b/docs/img/rstarstruck_icon.png differ diff --git a/include/PICA/dynapica/shader_rec.hpp b/include/PICA/dynapica/shader_rec.hpp index e8b6afed..2dabc128 100644 --- a/include/PICA/dynapica/shader_rec.hpp +++ b/include/PICA/dynapica/shader_rec.hpp @@ -1,13 +1,15 @@ #pragma once #include "PICA/shader.hpp" -#if defined(PANDA3DS_DYNAPICA_SUPPORTED) && defined(PANDA3DS_X64_HOST) +#if defined(PANDA3DS_DYNAPICA_SUPPORTED) && (defined(PANDA3DS_X64_HOST) || defined(PANDA3DS_ARM64_HOST)) #define PANDA3DS_SHADER_JIT_SUPPORTED #include #include #ifdef PANDA3DS_X64_HOST #include "shader_rec_emitter_x64.hpp" +#elif defined(PANDA3DS_ARM64_HOST) +#include "shader_rec_emitter_arm64.hpp" #endif #endif diff --git a/include/PICA/dynapica/shader_rec_emitter_arm64.hpp b/include/PICA/dynapica/shader_rec_emitter_arm64.hpp new file mode 100644 index 00000000..bb716d91 --- /dev/null +++ b/include/PICA/dynapica/shader_rec_emitter_arm64.hpp @@ -0,0 +1,130 @@ +#pragma once + +// Only do anything if we're on an x64 target with JIT support enabled +#if defined(PANDA3DS_DYNAPICA_SUPPORTED) && defined(PANDA3DS_ARM64_HOST) +#include +#include +#include + +#include "PICA/shader.hpp" +#include "helpers.hpp" +#include "logger.hpp" + +class ShaderEmitter : private oaknut::CodeBlock, public oaknut::CodeGenerator { + static constexpr size_t executableMemorySize = PICAShader::maxInstructionCount * 96; // How much executable memory to alloc for each shader + // Allocate some extra space as padding for security purposes in the extremely unlikely occasion we manage to overflow the above size + static constexpr size_t allocSize = executableMemorySize + 0x1000; + + // If the swizzle field is this value then the swizzle pattern is .xyzw so we don't need a shuffle + static constexpr uint noSwizzle = 0x1B; + + using f24 = Floats::f24; + using vec4f = std::array; + + // An array of labels (incl pointers) to each compiled (to x64) PICA instruction + std::array instructionLabels; + // A vector of PCs that can potentially return based on the state of the PICA callstack. + // Filled before compiling a shader by scanning the code for call instructions + std::vector returnPCs; + + // An array of 128-bit masks for blending registers together to perform masked writes. + // Eg for writing only the x and y components, the mask is 0x00000000'00000000'FFFFFFFF'FFFF + oaknut::Label blendMasks; + + u32 recompilerPC = 0; // PC the recompiler is currently recompiling @ + u32 loopLevel = 0; // The current loop nesting level (0 = not in a loop) + + // Shows whether the loaded shader has any log2 and exp2 instructions + bool codeHasLog2 = false; + bool codeHasExp2 = false; + + oaknut::Label log2Func, exp2Func; + oaknut::Label emitLog2Func(); + oaknut::Label emitExp2Func(); + + // Compile all instructions from [current recompiler PC, end) + void compileUntil(const PICAShader& shaderUnit, u32 endPC); + // Compile instruction "instr" + void compileInstruction(const PICAShader& shaderUnit); + + bool isCall(u32 instruction) { + const u32 opcode = instruction >> 26; + return (opcode == ShaderOpcodes::CALL) || (opcode == ShaderOpcodes::CALLC) || (opcode == ShaderOpcodes::CALLU); + } + + // Scan the shader code for call instructions to fill up the returnPCs vector before starting compilation + // We also scan for log2/exp2 instructions to see whether to emit the relevant functions + void scanCode(const PICAShader& shaderUnit); + + // Load register with number "srcReg" indexed by index "idx" into the arm64 register "reg" + template + void loadRegister(oaknut::QReg dest, const PICAShader& shader, u32 src, u32 idx, u32 operandDescriptor); + void storeRegister(oaknut::QReg source, const PICAShader& shader, u32 dest, u32 operandDescriptor); + + const vec4f& getSourceRef(const PICAShader& shader, u32 src); + const vec4f& getDestRef(const PICAShader& shader, u32 dest); + + // Check the value of the cmp register for instructions like ifc and callc + // Result is returned in the zero flag. If the comparison is true then zero == 1, else zero == 0 + void checkCmpRegister(const PICAShader& shader, u32 instruction); + + // Check the value of the bool uniform for instructions like ifu and callu + // Result is returned in the zero flag. If the comparison is true then zero == 0, else zero == 1 (Opposite of checkCmpRegister) + void checkBoolUniform(const PICAShader& shader, u32 instruction); + + // Instruction recompilation functions + void recADD(const PICAShader& shader, u32 instruction); + void recCALL(const PICAShader& shader, u32 instruction); + void recCALLC(const PICAShader& shader, u32 instruction); + void recCALLU(const PICAShader& shader, u32 instruction); + void recCMP(const PICAShader& shader, u32 instruction); + void recDP3(const PICAShader& shader, u32 instruction); + void recDP4(const PICAShader& shader, u32 instruction); + void recDPH(const PICAShader& shader, u32 instruction); + void recEMIT(const PICAShader& shader, u32 instruction); + void recEND(const PICAShader& shader, u32 instruction); + void recEX2(const PICAShader& shader, u32 instruction); + void recFLR(const PICAShader& shader, u32 instruction); + void recIFC(const PICAShader& shader, u32 instruction); + void recIFU(const PICAShader& shader, u32 instruction); + void recJMPC(const PICAShader& shader, u32 instruction); + void recJMPU(const PICAShader& shader, u32 instruction); + void recLG2(const PICAShader& shader, u32 instruction); + void recLOOP(const PICAShader& shader, u32 instruction); + void recMAD(const PICAShader& shader, u32 instruction); + void recMAX(const PICAShader& shader, u32 instruction); + void recMIN(const PICAShader& shader, u32 instruction); + void recMOVA(const PICAShader& shader, u32 instruction); + void recMOV(const PICAShader& shader, u32 instruction); + void recMUL(const PICAShader& shader, u32 instruction); + void recRCP(const PICAShader& shader, u32 instruction); + void recRSQ(const PICAShader& shader, u32 instruction); + void recSETEMIT(const PICAShader& shader, u32 instruction); + void recSGE(const PICAShader& shader, u32 instruction); + void recSLT(const PICAShader& shader, u32 instruction); + + MAKE_LOG_FUNCTION(log, shaderJITLogger) + + public: + // Callback type used for instructions + using InstructionCallback = const void (*)(PICAShader& shaderUnit); + // Callback type used for the JIT prologue. This is what the caller will call + using PrologueCallback = const void (*)(PICAShader& shaderUnit, InstructionCallback cb); + + PrologueCallback prologueCb = nullptr; + + // Initialize our emitter with "allocSize" bytes of memory allocated for the code buffer + ShaderEmitter() : oaknut::CodeBlock(allocSize), oaknut::CodeGenerator(oaknut::CodeBlock::ptr()) {} + + // PC must be a valid entrypoint here. It doesn't have that much overhead in this case, so we use std::array<>::at() to assert it does + InstructionCallback getInstructionCallback(u32 pc) { + // Cast away the constness because casting to a function pointer is hard otherwise. Legal as long as we don't write to *ptr + uint8_t* ptr = instructionLabels.at(pc).ptr(); + return reinterpret_cast(ptr); + } + + PrologueCallback getPrologueCallback() { return prologueCb; } + void compile(const PICAShader& shaderUnit); +}; + +#endif // arm64 recompiler check \ No newline at end of file diff --git a/include/applets/applet.hpp b/include/applets/applet.hpp index 0c3ab519..48f20b03 100644 --- a/include/applets/applet.hpp +++ b/include/applets/applet.hpp @@ -1,6 +1,9 @@ #pragma once +#include + #include "helpers.hpp" +#include "kernel/kernel_types.hpp" #include "memory.hpp" #include "result/result.hpp" @@ -65,24 +68,27 @@ namespace Applets { }; struct Parameter { - u32 senderID; - u32 destID; - APTSignal signal; - std::vector data; + u32 senderID; // ID of the parameter sender + u32 destID; // ID of the app to receive parameter + u32 signal; // Signal type (eg request) + u32 object; // Some applets will also respond with shared memory handles for transferring data between the sender and called + std::vector data; // Misc data }; class AppletBase { + protected: Memory& mem; + std::optional& nextParameter; public: virtual const char* name() = 0; // Called by APT::StartLibraryApplet and similar - virtual Result::HorizonResult start() = 0; + virtual Result::HorizonResult start(const MemoryBlock* sharedMem, const std::vector& parameters, u32 appID) = 0; // Transfer parameters from application -> applet - virtual Result::HorizonResult receiveParameter() = 0; + virtual Result::HorizonResult receiveParameter(const Parameter& parameter) = 0; virtual void reset() = 0; - AppletBase(Memory& mem) : mem(mem) {} + AppletBase(Memory& mem, std::optional& nextParam) : mem(mem), nextParameter(nextParam) {} }; } // namespace Applets \ No newline at end of file diff --git a/include/applets/applet_manager.hpp b/include/applets/applet_manager.hpp index 95b54009..d8cfff12 100644 --- a/include/applets/applet_manager.hpp +++ b/include/applets/applet_manager.hpp @@ -1,3 +1,7 @@ +#pragma once +#include + +#include "applets/error_applet.hpp" #include "applets/mii_selector.hpp" #include "applets/software_keyboard.hpp" #include "helpers.hpp" @@ -8,10 +12,15 @@ namespace Applets { class AppletManager { MiiSelectorApplet miiSelector; SoftwareKeyboardApplet swkbd; + ErrorApplet error; + std::optional nextParameter = std::nullopt; public: AppletManager(Memory& mem); void reset(); AppletBase* getApplet(u32 id); + + Applets::Parameter glanceParameter(); + Applets::Parameter receiveParameter(); }; } // namespace Applets \ No newline at end of file diff --git a/include/applets/error_applet.hpp b/include/applets/error_applet.hpp new file mode 100644 index 00000000..4dcc319d --- /dev/null +++ b/include/applets/error_applet.hpp @@ -0,0 +1,15 @@ +#include + +#include "applets/applet.hpp" + +namespace Applets { + class ErrorApplet final : public AppletBase { + public: + virtual const char* name() override { return "Error/EULA Agreement"; } + virtual Result::HorizonResult start(const MemoryBlock* sharedMem, const std::vector& parameters, u32 appID) override; + virtual Result::HorizonResult receiveParameter(const Applets::Parameter& parameter) override; + virtual void reset() override; + + ErrorApplet(Memory& memory, std::optional& nextParam) : AppletBase(memory, nextParam) {} + }; +} // namespace Applets \ No newline at end of file diff --git a/include/applets/mii_selector.hpp b/include/applets/mii_selector.hpp index e40547fb..36f9fe79 100644 --- a/include/applets/mii_selector.hpp +++ b/include/applets/mii_selector.hpp @@ -1,13 +1,83 @@ +#include + #include "applets/applet.hpp" +#include "swap.hpp" namespace Applets { + struct MiiConfig { + u8 enableCancelButton; + u8 enableGuestMii; + u8 showOnTopScreen; + std::array pad1; + std::array title; + std::array pad2; + u8 showGuestMiis; + std::array pad3; + u32 initiallySelectedIndex; + std::array guestMiiWhitelist; + std::array userMiiWhitelist; + std::array pad4; + u32 magicValue; + }; + static_assert(sizeof(MiiConfig) == 0x104, "Mii config size is wrong"); + + // Some members of this struct are not properly aligned so we need pragma pack +#pragma pack(push, 1) + struct MiiData { + u8 version; + u8 miiOptions; + u8 miiPos; + u8 consoleID; + + u64_be systemID; + u32_be miiID; + std::array creatorMAC; + u16 padding; + + u16_be miiDetails; + std::array miiName; + u8 height; + u8 width; + + u8 faceStyle; + u8 faceDetails; + u8 hairStyle; + u8 hairDetails; + u32_be eyeDetails; + u32_be eyebrowDetails; + u16_be noseDetails; + u16_be mouthDetails; + u16_be moustacheDetails; + u16_be beardDetails; + u16_be glassesDetails; + u16_be moleDetails; + + std::array authorName; + }; +#pragma pack(pop) + static_assert(sizeof(MiiData) == 0x5C, "MiiData structure has incorrect size"); + + struct MiiResult { + u32_be returnCode; + u32_be isGuestMiiSelected; + u32_be selectedGuestMiiIndex; + MiiData selectedMiiData; + u16_be unknown1; + u16_be miiChecksum; + std::array guestMiiName; + }; + static_assert(sizeof(MiiResult) == 0x84, "MiiResult structure has incorrect size"); + class MiiSelectorApplet final : public AppletBase { public: virtual const char* name() override { return "Mii Selector"; } - virtual Result::HorizonResult start() override; - virtual Result::HorizonResult receiveParameter() override; + virtual Result::HorizonResult start(const MemoryBlock* sharedMem, const std::vector& parameters, u32 appID) override; + virtual Result::HorizonResult receiveParameter(const Applets::Parameter& parameter) override; virtual void reset() override; - MiiSelectorApplet(Memory& memory) : AppletBase(memory) {} + MiiResult output; + MiiConfig config; + MiiResult getDefaultMii(); + MiiSelectorApplet(Memory& memory, std::optional& nextParam) : AppletBase(memory, nextParam) {} }; } // namespace Applets \ No newline at end of file diff --git a/include/applets/software_keyboard.hpp b/include/applets/software_keyboard.hpp index 1fb721a1..f753566d 100644 --- a/include/applets/software_keyboard.hpp +++ b/include/applets/software_keyboard.hpp @@ -1,13 +1,162 @@ +#include + #include "applets/applet.hpp" +#include "swap.hpp" namespace Applets { + // Software keyboard definitions adapted from libctru/Citra + // Keyboard input filtering flags. Allows the caller to specify what input is explicitly not allowed + namespace SoftwareKeyboardFilter { + enum Filter : u32 { + Digits = 1, // Disallow the use of more than a certain number of digits (0 or more) + At = 1 << 1, // Disallow the use of the @ sign. + Percent = 1 << 2, // Disallow the use of the % sign. + Backslash = 1 << 3, // Disallow the use of the \ sign. + Profanity = 1 << 4, // Disallow profanity using Nintendo's profanity filter. + Callback = 1 << 5, // Use a callback in order to check the input. + }; + } // namespace SoftwareKeyboardFilter + + // Keyboard features. + namespace SoftwareKeyboardFeature { + enum Feature { + Parental = 1, // Parental PIN mode. + DarkenTopScreen = 1 << 1, // Darken the top screen when the keyboard is shown. + PredictiveInput = 1 << 2, // Enable predictive input (necessary for Kanji input in JPN systems). + Multiline = 1 << 3, // Enable multiline input. + FixedWidth = 1 << 4, // Enable fixed-width mode. + AllowHome = 1 << 5, // Allow the usage of the HOME button. + AllowReset = 1 << 6, // Allow the usage of a software-reset combination. + AllowPower = 1 << 7, // Allow the usage of the POWER button. + DefaultQWERTY = 1 << 9, // Default to the QWERTY page when the keyboard is shown. + }; + } // namespace SoftwareKeyboardFeature + class SoftwareKeyboardApplet final : public AppletBase { public: + static constexpr int MAX_BUTTON = 3; // Maximum number of buttons that can be in the keyboard. + static constexpr int MAX_BUTTON_TEXT_LEN = 16; // Maximum button text length, in UTF-16 code units. + static constexpr int MAX_HINT_TEXT_LEN = 64; // Maximum hint text length, in UTF-16 code units. + static constexpr int MAX_CALLBACK_MSG_LEN = 256; // Maximum filter callback error message length, in UTF-16 code units. + + // Keyboard types + enum class SoftwareKeyboardType : u32 { + Normal, // Normal keyboard with several pages (QWERTY/accents/symbol/mobile) + QWERTY, // QWERTY keyboard only. + NumPad, // Number pad. + Western, // On JPN systems, a text keyboard without Japanese input capabilities, otherwise same as SWKBD_TYPE_NORMAL. + }; + + // Keyboard dialog buttons. + enum class SoftwareKeyboardButtonConfig : u32 { + SingleButton, // Ok button + DualButton, // Cancel | Ok buttons + TripleButton, // Cancel | I Forgot | Ok buttons + NoButton, // No button (returned by swkbdInputText in special cases) + }; + + // Accepted input types. + enum class SoftwareKeyboardValidInput : u32 { + Anything, // All inputs are accepted. + NotEmpty, // Empty inputs are not accepted. + NotEmptyNotBlank, // Empty or blank inputs (consisting solely of whitespace) are not accepted. + NotBlank, // Blank inputs (consisting solely of whitespace) are not accepted, but empty inputs are. + FixedLen, // The input must have a fixed length (specified by maxTextLength in swkbdInit) + }; + + // Keyboard password modes. + enum class SoftwareKeyboardPasswordMode : u32 { + None, // Characters are not concealed. + Hide, // Characters are concealed immediately. + HideDelay, // Characters are concealed a second after they've been typed. + }; + + // Keyboard filter callback return values. + enum class SoftwareKeyboardCallbackResult : u32 { + OK, // Specifies that the input is valid. + Close, // Displays an error message, then closes the keyboard. + Continue, // Displays an error message and continues displaying the keyboard. + }; + + // Keyboard return values. + enum class SoftwareKeyboardResult : s32 { + None = -1, // Dummy/unused. + InvalidInput = -2, // Invalid parameters to swkbd. + OutOfMem = -3, // Out of memory. + + D0Click = 0, // The button was clicked in 1-button dialogs. + D1Click0, // The left button was clicked in 2-button dialogs. + D1Click1, // The right button was clicked in 2-button dialogs. + D2Click0, // The left button was clicked in 3-button dialogs. + D2Click1, // The middle button was clicked in 3-button dialogs. + D2Click2, // The right button was clicked in 3-button dialogs. + + HomePressed = 10, // The HOME button was pressed. + ResetPressed, // The soft-reset key combination was pressed. + PowerPressed, // The POWER button was pressed. + + ParentalOK = 20, // The parental PIN was verified successfully. + ParentalFail, // The parental PIN was incorrect. + + BannedInput = 30, // The filter callback returned SoftwareKeyboardCallback::CLOSE. + }; + + struct SoftwareKeyboardConfig { + enum_le type; + enum_le numButtonsM1; + enum_le validInput; + enum_le passwordMode; + s32_le isParentalScreen; + s32_le darkenTopScreen; + u32_le filterFlags; + u32_le saveStateFlags; + u16_le maxTextLength; + u16_le dictWordCount; + u16_le maxDigits; + std::array, MAX_BUTTON> buttonText; + std::array numpadKeys; + std::array hintText; // Text to display when asking the user for input + bool predictiveInput; + bool multiline; + bool fixedWidth; + bool allowHome; + bool allowReset; + bool allowPower; + bool unknown; + bool defaultQwerty; + std::array buttonSubmitsText; + u16_le language; + + u32_le initialTextOffset; // Offset of the default text in the output SharedMemory + u32_le dictOffset; + u32_le initialStatusOffset; + u32_le initialLearningOffset; + u32_le sharedMemorySize; // Size of the SharedMemory + u32_le version; + + enum_le returnCode; + + u32_le statusOffset; + u32_le learningOffset; + + u32_le textOffset; // Offset in the SharedMemory where the output text starts + u16_le textLength; // Length in characters of the output text + + enum_le callbackResult; + std::array callbackMessage; + bool skipAtCheck; + std::array pad; + }; + static_assert(sizeof(SoftwareKeyboardConfig) == 0x400, "Software keyboard config size is wrong"); + virtual const char* name() override { return "Software Keyboard"; } - virtual Result::HorizonResult start() override; - virtual Result::HorizonResult receiveParameter() override; + virtual Result::HorizonResult start(const MemoryBlock* sharedMem, const std::vector& parameters, u32 appID) override; + virtual Result::HorizonResult receiveParameter(const Applets::Parameter& parameter) override; virtual void reset() override; - SoftwareKeyboardApplet(Memory& memory) : AppletBase(memory) {} + SoftwareKeyboardApplet(Memory& memory, std::optional& nextParam) : AppletBase(memory, nextParam) {} + void closeKeyboard(u32 appID); + + SoftwareKeyboardConfig config; }; } // namespace Applets \ No newline at end of file diff --git a/include/cheats.hpp b/include/cheats.hpp index c8d7c763..b90c080b 100644 --- a/include/cheats.hpp +++ b/include/cheats.hpp @@ -12,25 +12,31 @@ class Memory; class Cheats { public: enum class CheatType { + None, // Cheat has been removed by the frontend or is invalid ActionReplay, // CTRPF cheats - // TODO: Other cheat devices and standards? }; struct Cheat { - CheatType type; + bool enabled = true; + CheatType type = CheatType::ActionReplay; std::vector instructions; }; Cheats(Memory& mem, HIDService& hid); - void addCheat(const Cheat& cheat); + u32 addCheat(const Cheat& cheat); + u32 addCheat(const u8* data, size_t size); + void removeCheat(u32 id); + void enableCheat(u32 id); + void disableCheat(u32 id); void reset(); void run(); void clear(); bool haveCheats() const { return cheatsLoaded; } + static constexpr u32 badCheatHandle = 0xFFFFFFFF; private: ActionReplay ar; // An ActionReplay cheat machine for executing CTRPF codes std::vector cheats; bool cheatsLoaded = false; -}; \ No newline at end of file +}; diff --git a/include/config.hpp b/include/config.hpp index 326ab203..155f5961 100644 --- a/include/config.hpp +++ b/include/config.hpp @@ -5,7 +5,14 @@ // Remember to initialize every field here to its default value otherwise bad things will happen struct EmulatorConfig { - bool shaderJitEnabled = true; + // Only enable the shader JIT by default on platforms where it's completely tested +#ifdef PANDA3DS_X64_HOST + static constexpr bool shaderJitDefault = true; +#else + static constexpr bool shaderJitDefault = false; +#endif + + bool shaderJitEnabled = shaderJitDefault; bool discordRpcEnabled = false; RendererType rendererType = RendererType::OpenGL; @@ -17,7 +24,9 @@ struct EmulatorConfig { // Default to 3% battery to make users suffer int batteryPercentage = 3; + std::filesystem::path filePath; + EmulatorConfig(const std::filesystem::path& path); - void load(const std::filesystem::path& path); - void save(const std::filesystem::path& path); + void load(); + void save(); }; \ No newline at end of file diff --git a/include/cpu_dynarmic.hpp b/include/cpu_dynarmic.hpp index 8f1e277b..048bc62a 100644 --- a/include/cpu_dynarmic.hpp +++ b/include/cpu_dynarmic.hpp @@ -9,15 +9,18 @@ #include "helpers.hpp" #include "kernel.hpp" #include "memory.hpp" +#include "scheduler.hpp" +class Emulator; class CPU; class MyEnvironment final : public Dynarmic::A32::UserCallbacks { -public: - u64 ticksLeft = 0; - u64 totalTicks = 0; - Memory& mem; - Kernel& kernel; + public: + u64 ticksLeft = 0; + u64 totalTicks = 0; + Memory& mem; + Kernel& kernel; + Scheduler& scheduler; u64 getCyclesForInstruction(bool isThumb, u32 instruction); @@ -76,54 +79,56 @@ public: std::terminate(); } - void CallSVC(u32 swi) override { - kernel.serviceSVC(swi); - } + void CallSVC(u32 swi) override { + kernel.serviceSVC(swi); + } - void ExceptionRaised(u32 pc, Dynarmic::A32::Exception exception) override { - switch (exception) { - case Dynarmic::A32::Exception::UnpredictableInstruction: - Helpers::panic("Unpredictable instruction at pc = %08X", pc); - break; + void ExceptionRaised(u32 pc, Dynarmic::A32::Exception exception) override { + switch (exception) { + case Dynarmic::A32::Exception::UnpredictableInstruction: + Helpers::panic("Unpredictable instruction at pc = %08X", pc); + break; - default: Helpers::panic("Fired exception oops"); - } - } + default: Helpers::panic("Fired exception oops"); + } + } - void AddTicks(u64 ticks) override { - totalTicks += ticks; + void AddTicks(u64 ticks) override { + scheduler.currentTimestamp += ticks; - if (ticks > ticksLeft) { - ticksLeft = 0; - return; - } - ticksLeft -= ticks; - } + if (ticks > ticksLeft) { + ticksLeft = 0; + return; + } + ticksLeft -= ticks; + } - u64 GetTicksRemaining() override { - return ticksLeft; - } + u64 GetTicksRemaining() override { + return ticksLeft; + } - u64 GetTicksForCode(bool isThumb, u32 vaddr, u32 instruction) override { - return getCyclesForInstruction(isThumb, instruction); - } + u64 GetTicksForCode(bool isThumb, u32 vaddr, u32 instruction) override { + return getCyclesForInstruction(isThumb, instruction); + } - MyEnvironment(Memory& mem, Kernel& kernel) : mem(mem), kernel(kernel) {} + MyEnvironment(Memory& mem, Kernel& kernel, Scheduler& scheduler) : mem(mem), kernel(kernel), scheduler(scheduler) {} }; class CPU { - std::unique_ptr jit; - std::shared_ptr cp15; + std::unique_ptr jit; + std::shared_ptr cp15; - // Make exclusive monitor with only 1 CPU core - Dynarmic::ExclusiveMonitor exclusiveMonitor{1}; - MyEnvironment env; - Memory& mem; + // Make exclusive monitor with only 1 CPU core + Dynarmic::ExclusiveMonitor exclusiveMonitor{1}; + MyEnvironment env; + Memory& mem; + Scheduler& scheduler; + Emulator& emu; -public: - static constexpr u64 ticksPerSec = 268111856; + public: + static constexpr u64 ticksPerSec = Scheduler::arm11Clock; - CPU(Memory& mem, Kernel& kernel); + CPU(Memory& mem, Kernel& kernel, Emulator& emu); void reset(); void setReg(int index, u32 value) { @@ -162,29 +167,20 @@ public: } u64 getTicks() { - return env.totalTicks; + return scheduler.currentTimestamp; } // Get reference to tick count. Memory needs access to this u64& getTicksRef() { - return env.totalTicks; + return scheduler.currentTimestamp; } - void clearCache() { jit->ClearCache(); } - - void runFrame() { - env.ticksLeft = ticksPerSec / 60; - execute: - const auto exitReason = jit->Run(); - - if (static_cast(exitReason) != 0) [[unlikely]] { - // Cache invalidation needs to exit the JIT so it returns a CacheInvalidation HaltReason. In our case, we just go back to executing - // The goto might be terrible but it does guarantee that this does not recursively call run and crash, instead getting optimized to a jump - if (Dynarmic::Has(exitReason, Dynarmic::HaltReason::CacheInvalidation)) { - goto execute; - } else { - Helpers::panic("Exit reason: %d\nPC: %08X", static_cast(exitReason), getReg(15)); - } - } + Scheduler& getScheduler() { + return scheduler; } + + void addTicks(u64 ticks) { env.AddTicks(ticks); } + + void clearCache() { jit->ClearCache(); } + void runFrame(); }; \ No newline at end of file diff --git a/include/emulator.hpp b/include/emulator.hpp index 2b96b0cf..d3377f6c 100644 --- a/include/emulator.hpp +++ b/include/emulator.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include "PICA/gpu.hpp" #include "cheats.hpp" @@ -14,6 +15,7 @@ #include "io_file.hpp" #include "lua_manager.hpp" #include "memory.hpp" +#include "scheduler.hpp" #ifdef PANDA3DS_ENABLE_HTTP_SERVER #include "http_server.hpp" @@ -23,7 +25,7 @@ #include "gl/context.h" #endif -class SDL_Window; +struct SDL_Window; enum class ROMType { None, @@ -41,6 +43,7 @@ class Emulator { Kernel kernel; Crypto::AESEngine aesEngine; Cheats cheats; + Scheduler scheduler; // Variables to keep track of whether the user is controlling the 3DS analog stick with their keyboard // This is done so when a gamepad is connected, we won't automatically override the 3DS analog stick settings with the gamepad's state @@ -52,12 +55,14 @@ class Emulator { // We bind gyro to right click + mouse movement bool holdingRightClick = false; + public: static constexpr u32 width = 400; static constexpr u32 height = 240 * 2; // * 2 because 2 screens ROMType romType = ROMType::None; bool running = false; // Is the emulator running a game? bool programRunning = false; // Is the emulator program itself running? + private: #ifdef PANDA3DS_ENABLE_HTTP_SERVER HttpServer httpServer; friend struct HttpServer; @@ -82,6 +87,8 @@ class Emulator { // change ROMs. If Reload is selected, the emulator will reload its selected ROM. This is useful for eg a "reset" button that keeps the current // ROM and just resets the emu enum class ReloadOption { NoReload, Reload }; + // Used in CPU::runFrame + bool frameDone = false; Emulator(); ~Emulator(); @@ -91,6 +98,8 @@ class Emulator { void reset(ReloadOption reload); void run(void* frontend = nullptr); void runFrame(); + // Poll the scheduler for events + void pollScheduler(); void resume(); // Resume the emulator void pause(); // Pause the emulator @@ -115,8 +124,19 @@ class Emulator { void deinitGraphicsContext() { gpu.deinitGraphicsContext(); } EmulatorConfig& getConfig() { return config; } + Cheats& getCheats() { return cheats; } ServiceManager& getServiceManager() { return kernel.getServiceManager(); } + LuaManager& getLua() { return lua; } + Scheduler& getScheduler() { return scheduler; } + RendererType getRendererType() const { return config.rendererType; } Renderer* getRenderer() { return gpu.getRenderer(); } u64 getTicks() { return cpu.getTicks(); } + + std::filesystem::path getConfigPath(); + std::filesystem::path getAndroidAppPath(); + // Get the root path for the emulator's app data + std::filesystem::path getAppDataRoot(); + + std::span getSMDH(); }; diff --git a/include/fs/archive_sdmc.hpp b/include/fs/archive_sdmc.hpp index 4aa80eab..f63731c4 100644 --- a/include/fs/archive_sdmc.hpp +++ b/include/fs/archive_sdmc.hpp @@ -5,8 +5,10 @@ using Result::HorizonResult; class SDMCArchive : public ArchiveBase { -public: - SDMCArchive(Memory& mem) : ArchiveBase(mem) {} + bool isWriteOnly = false; // There's 2 variants of the SDMC archive: Regular one (Read/Write) and write-only + + public: + SDMCArchive(Memory& mem, bool writeOnly = false) : ArchiveBase(mem), isWriteOnly(writeOnly) {} u64 getFreeBytes() override { return 1_GB; } std::string name() override { return "SDMC"; } diff --git a/include/helpers.hpp b/include/helpers.hpp index 9082cc54..037c8976 100644 --- a/include/helpers.hpp +++ b/include/helpers.hpp @@ -90,6 +90,13 @@ namespace Helpers { return false; } + static constexpr bool isAndroid() { +#ifdef __ANDROID__ + return true; +#endif + return false; + } + static void debug_printf(const char* fmt, ...) { if constexpr (buildingInDebugMode()) { std::va_list args; diff --git a/include/kernel/handles.hpp b/include/kernel/handles.hpp index b038049f..fe746b65 100644 --- a/include/kernel/handles.hpp +++ b/include/kernel/handles.hpp @@ -53,6 +53,7 @@ namespace KernelHandles { GSPSharedMemHandle = MaxServiceHandle + 1, // Handle for the GSP shared memory FontSharedMemHandle, CSNDSharedMemHandle, + APTCaptureSharedMemHandle, // Shared memory for display capture info, HIDSharedMemHandle, MinSharedMemHandle = GSPSharedMemHandle, diff --git a/include/kernel/kernel.hpp b/include/kernel/kernel.hpp index dc821a68..e78a588a 100644 --- a/include/kernel/kernel.hpp +++ b/include/kernel/kernel.hpp @@ -36,6 +36,7 @@ class Kernel { std::vector objects; std::vector portHandles; std::vector mutexHandles; + std::vector timerHandles; // Thread indices, sorted by priority std::vector threadIndices; @@ -69,6 +70,7 @@ public: Handle makeMutex(bool locked = false); // Needs to be public to be accessible to the APT/DSP services Handle makeSemaphore(u32 initialCount, u32 maximumCount); // Needs to be public to be accessible to the service manager port Handle makeTimer(ResetType resetType); + void pollTimers(); // Signals an event, returns true on success or false if the event does not exist bool signalEvent(Handle e); @@ -93,7 +95,7 @@ public: void releaseMutex(Mutex* moo); void cancelTimer(Timer* timer); void signalTimer(Handle timerHandle, Timer* timer); - void updateTimer(Handle timerHandle, Timer* timer); + u64 getWakeupTick(s64 ns); // Wake up the thread with the highest priority out of all threads in the waitlist // Returns the index of the woken up thread @@ -136,6 +138,7 @@ public: void duplicateHandle(); void exitThread(); void mapMemoryBlock(); + void unmapMemoryBlock(); void queryMemory(); void getCurrentProcessorNumber(); void getProcessID(); @@ -145,6 +148,7 @@ public: void getResourceLimitCurrentValues(); void getSystemInfo(); void getSystemTick(); + void getThreadContext(); void getThreadID(); void getThreadIdealProcessor(); void getThreadPriority(); diff --git a/include/kernel/kernel_types.hpp b/include/kernel/kernel_types.hpp index 53c60774..01af4bd9 100644 --- a/include/kernel/kernel_types.hpp +++ b/include/kernel/kernel_types.hpp @@ -83,56 +83,53 @@ struct Port { }; struct Session { - Handle portHandle; // The port this session is subscribed to - Session(Handle portHandle) : portHandle(portHandle) {} + Handle portHandle; // The port this session is subscribed to + Session(Handle portHandle) : portHandle(portHandle) {} }; enum class ThreadStatus { - Running, // Currently running - Ready, // Ready to run - WaitArbiter, // Waiting on an address arbiter - WaitSleep, // Waiting due to a SleepThread SVC - WaitSync1, // Waiting for the single object in the wait list to be ready - WaitSyncAny, // Wait for one object of the many that might be in the wait list to be ready - WaitSyncAll, // Waiting for ALL sync objects in its wait list to be ready - WaitIPC, // Waiting for the reply from an IPC request - Dormant, // Created but not yet made ready - Dead // Run to completion, or forcefully terminated + Running, // Currently running + Ready, // Ready to run + WaitArbiter, // Waiting on an address arbiter + WaitSleep, // Waiting due to a SleepThread SVC + WaitSync1, // Waiting for the single object in the wait list to be ready + WaitSyncAny, // Wait for one object of the many that might be in the wait list to be ready + WaitSyncAll, // Waiting for ALL sync objects in its wait list to be ready + WaitIPC, // Waiting for the reply from an IPC request + Dormant, // Created but not yet made ready + Dead // Run to completion, or forcefully terminated }; struct Thread { - u32 initialSP; // Initial r13 value - u32 entrypoint; // Initial r15 value - u32 priority; - u32 arg; - ProcessorID processorID; - ThreadStatus status; - Handle handle; // OS handle for this thread - int index; // Index of the thread. 0 for the first thread, 1 for the second, and so on + u32 initialSP; // Initial r13 value + u32 entrypoint; // Initial r15 value + u32 priority; + u32 arg; + ProcessorID processorID; + ThreadStatus status; + Handle handle; // OS handle for this thread + int index; // Index of the thread. 0 for the first thread, 1 for the second, and so on - // The waiting address for threads that are waiting on an AddressArbiter - u32 waitingAddress; + // The waiting address for threads that are waiting on an AddressArbiter + u32 waitingAddress; - // The nanoseconds until a thread wakes up from being asleep or from timing out while waiting on an arbiter - u64 waitingNanoseconds; - // The tick this thread went to sleep on - u64 sleepTick; - // For WaitSynchronization(N): A vector of objects this thread is waiting for - std::vector waitList; - // For WaitSynchronizationN: Shows whether the object should wait for all objects in the wait list or just one - bool waitAll; - // For WaitSynchronizationN: The "out" pointer - u32 outPointer; + // For WaitSynchronization(N): A vector of objects this thread is waiting for + std::vector waitList; + // For WaitSynchronizationN: Shows whether the object should wait for all objects in the wait list or just one + bool waitAll; + // For WaitSynchronizationN: The "out" pointer + u32 outPointer; + u64 wakeupTick; - // Thread context used for switching between threads - std::array gprs; - std::array fprs; // Stored as u32 because dynarmic does it - u32 cpsr; - u32 fpscr; - u32 tlsBase; // Base pointer for thread-local storage + // Thread context used for switching between threads + std::array gprs; + std::array fprs; // Stored as u32 because dynarmic does it + u32 cpsr; + u32 fpscr; + u32 tlsBase; // Base pointer for thread-local storage - // A list of threads waiting for this thread to terminate. Yes, threads are sync objects too. - u64 threadsWaitingForTermination; + // A list of threads waiting for this thread to terminate. Yes, threads are sync objects too. + u64 threadsWaitingForTermination; }; static const char* kernelObjectTypeToString(KernelObjectType t) { @@ -177,13 +174,12 @@ struct Timer { u64 waitlist; // Refer to the getWaitlist function below for documentation ResetType resetType = ResetType::OneShot; - u64 startTick; // CPU tick the timer started - u64 currentDelay; // Number of ns until the timer fires next time + u64 fireTick; // CPU tick the timer will be fired u64 interval; // Number of ns until the timer fires for the second and future times bool fired; // Has this timer been signalled? bool running; // Is this timer running or stopped? - Timer(ResetType type) : resetType(type), startTick(0), currentDelay(0), interval(0), waitlist(0), fired(false), running(false) {} + Timer(ResetType type) : resetType(type), fireTick(0), interval(0), waitlist(0), fired(false), running(false) {} }; struct MemoryBlock { diff --git a/include/loader/ncch.hpp b/include/loader/ncch.hpp index 5e2ad1d8..c5ef2465 100644 --- a/include/loader/ncch.hpp +++ b/include/loader/ncch.hpp @@ -65,6 +65,7 @@ struct NCCH { std::vector saveData; // The cart region. Only the CXI's region matters to us. Necessary to get past region locking std::optional region = std::nullopt; + std::vector smdh; // Returns true on success, false on failure // Partition index/offset/size must have been set before this diff --git a/include/logger.hpp b/include/logger.hpp index 82d90410..e021a685 100644 --- a/include/logger.hpp +++ b/include/logger.hpp @@ -2,6 +2,10 @@ #include #include +#ifdef __ANDROID__ +#include +#endif + namespace Log { // Our logger class template @@ -12,7 +16,11 @@ namespace Log { std::va_list args; va_start(args, fmt); +#ifdef __ANDROID__ + __android_log_vprint(ANDROID_LOG_DEFAULT, "Panda3DS", fmt, args); +#else std::vprintf(fmt, args); +#endif va_end(args); } }; @@ -81,4 +89,4 @@ namespace Log { #else #define MAKE_LOG_FUNCTION(functionName, logger) MAKE_LOG_FUNCTION_USER(functionName, logger) #endif -} +} \ No newline at end of file diff --git a/include/lua_manager.hpp b/include/lua_manager.hpp index 7ef96f18..50b8dd61 100644 --- a/include/lua_manager.hpp +++ b/include/lua_manager.hpp @@ -1,4 +1,6 @@ #pragma once +#include + #include "helpers.hpp" #include "memory.hpp" @@ -36,6 +38,8 @@ class LuaManager { void initialize(); void initializeThunks(); void loadFile(const char* path); + void loadString(const std::string& code); + void reset(); void signalEvent(LuaEvent e) { if (haveScript) [[unlikely]] { @@ -52,6 +56,7 @@ class LuaManager { void close() {} void initialize() {} void loadFile(const char* path) {} + void loadString(const std::string& code) {} void reset() {} void signalEvent(LuaEvent e) {} }; diff --git a/include/memory.hpp b/include/memory.hpp index 3ddd00cc..640ae5f0 100644 --- a/include/memory.hpp +++ b/include/memory.hpp @@ -112,11 +112,12 @@ class Memory { // This tracks our OS' memory allocations std::vector memoryInfo; - std::array sharedMemBlocks = { + std::array sharedMemBlocks = { SharedMemoryBlock(0, 0, KernelHandles::FontSharedMemHandle), // Shared memory for the system font (size is 0 because we read the size from the cmrc filesystem SharedMemoryBlock(0, 0x1000, KernelHandles::GSPSharedMemHandle), // GSP shared memory SharedMemoryBlock(0, 0x1000, KernelHandles::HIDSharedMemHandle), // HID shared memory SharedMemoryBlock(0, 0x3000, KernelHandles::CSNDSharedMemHandle), // CSND shared memory + SharedMemoryBlock(0, 0xE7000, KernelHandles::APTCaptureSharedMemHandle), // APT Capture Buffer memory }; public: diff --git a/include/panda_qt/about_window.hpp b/include/panda_qt/about_window.hpp new file mode 100644 index 00000000..78812d14 --- /dev/null +++ b/include/panda_qt/about_window.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include +#include +#include + +class AboutWindow : public QDialog { + Q_OBJECT + + public: + AboutWindow(QWidget* parent = nullptr); +}; \ No newline at end of file diff --git a/include/panda_qt/cheats_window.hpp b/include/panda_qt/cheats_window.hpp new file mode 100644 index 00000000..c82b2bd8 --- /dev/null +++ b/include/panda_qt/cheats_window.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include +#include + +#include "emulator.hpp" + +class QListWidget; + +class CheatsWindow final : public QWidget { + Q_OBJECT + + public: + CheatsWindow(Emulator* emu, const std::filesystem::path& path, QWidget* parent = nullptr); + ~CheatsWindow() = default; + + private: + void addEntry(); + void removeClicked(); + + QListWidget* cheatList; + std::filesystem::path cheatPath; + Emulator* emu; +}; diff --git a/include/panda_qt/config_window.hpp b/include/panda_qt/config_window.hpp new file mode 100644 index 00000000..e91936c4 --- /dev/null +++ b/include/panda_qt/config_window.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +class ConfigWindow : public QDialog { + Q_OBJECT + + private: + enum class Theme : int { + System = 0, + Light = 1, + Dark = 2, + GreetingsCat = 3, + }; + + Theme currentTheme; + QComboBox* themeSelect = nullptr; + + void setTheme(Theme theme); + + public: + ConfigWindow(QWidget* parent = nullptr); + ~ConfigWindow(); +}; \ No newline at end of file diff --git a/include/panda_qt/main_window.hpp b/include/panda_qt/main_window.hpp index b5b93d56..c2db9ac1 100644 --- a/include/panda_qt/main_window.hpp +++ b/include/panda_qt/main_window.hpp @@ -1,48 +1,109 @@ #pragma once #include -#include #include -#include #include #include #include +#include #include #include +#include #include "emulator.hpp" +#include "panda_qt/about_window.hpp" +#include "panda_qt/config_window.hpp" +#include "panda_qt/cheats_window.hpp" #include "panda_qt/screen.hpp" +#include "panda_qt/text_editor.hpp" +#include "services/hid.hpp" + +struct CheatMessage { + u32 handle; + std::vector cheat; + std::function callback; +}; class MainWindow : public QMainWindow { Q_OBJECT private: - enum class Theme : int { - System = 0, - Light = 1, - Dark = 2, + // Types of messages we might send from the GUI thread to the emulator thread + enum class MessageType { + LoadROM, + Reset, + Pause, + Resume, + TogglePause, + DumpRomFS, + PressKey, + ReleaseKey, + SetCirclePadX, + SetCirclePadY, + LoadLuaScript, + EditCheat, + PressTouchscreen, + ReleaseTouchscreen, + }; + + // Tagged union representing our message queue messages + struct EmulatorMessage { + MessageType type; + + union { + struct { + std::filesystem::path* p; + } path; + + struct { + u32 key; + } key; + + struct { + s16 value; + } circlepad; + + struct { + std::string* str; + } string; + + struct { + CheatMessage* c; + } cheat; + + struct { + u16 x; + u16 y; + } touchscreen; + }; }; // This would normally be an std::unique_ptr but it's shared between threads so definitely not Emulator* emu = nullptr; std::thread emuThread; - std::atomic appRunning = true; // Is the application itself running? - std::mutex messageQueueMutex; // Used for synchronizing messages between the emulator and UI - std::filesystem::path romToLoad = ""; - - bool needToLoadROM = false; + std::atomic appRunning = true; // Is the application itself running? + // Used for synchronizing messages between the emulator and UI + std::mutex messageQueueMutex; + std::vector messageQueue; ScreenWidget screen; - QComboBox* themeSelect = nullptr; + AboutWindow* aboutWindow; + ConfigWindow* configWindow; + CheatsWindow* cheatsEditor; + TextEditorWindow* luaEditor; QMenuBar* menuBar = nullptr; - Theme currentTheme; - void setTheme(Theme theme); void swapEmuBuffer(); void emuThreadMainLoop(); + void selectLuaFile(); void selectROM(); void dumpRomFS(); + void openLuaEditor(); + void openCheatsEditor(); + void showAboutMenu(); + void sendMessage(const EmulatorMessage& message); + void dispatchMessage(const EmulatorMessage& message); // Tracks whether we are using an OpenGL-backed renderer or a Vulkan-backed renderer bool usingGL = false; @@ -51,4 +112,12 @@ class MainWindow : public QMainWindow { public: MainWindow(QApplication* app, QWidget* parent = nullptr); ~MainWindow(); + + void keyPressEvent(QKeyEvent* event) override; + void keyReleaseEvent(QKeyEvent* event) override; + void mousePressEvent(QMouseEvent* event) override; + void mouseReleaseEvent(QMouseEvent* event) override; + + void loadLuaScript(const std::string& code); + void editCheat(u32 handle, const std::vector& cheat, const std::function& callback); }; \ No newline at end of file diff --git a/include/panda_qt/text_editor.hpp b/include/panda_qt/text_editor.hpp new file mode 100644 index 00000000..0da98294 --- /dev/null +++ b/include/panda_qt/text_editor.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include +#include + +#include "zep.h" +#include "zep/mode_repl.h" +#include "zep/regress.h" + +class TextEditorWindow : public QDialog { + Q_OBJECT + + private: + Zep::ZepWidget_Qt zepWidget; + Zep::IZepReplProvider replProvider; + static constexpr float fontSize = 14.0f; + + public: + TextEditorWindow(QWidget* parent, const std::string& filename, const std::string& initialText); + void setText(const std::string& text) { zepWidget.GetEditor().GetMRUBuffer()->SetText(text); } +}; \ No newline at end of file diff --git a/include/renderer_gl/surfaces.hpp b/include/renderer_gl/surfaces.hpp index 1c2976d6..15304a7a 100644 --- a/include/renderer_gl/surfaces.hpp +++ b/include/renderer_gl/surfaces.hpp @@ -48,7 +48,7 @@ struct ColourBuffer { fbo.bind(OpenGL::DrawAndReadFramebuffer); if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { - Helpers::panic("Incomplete framebuffer"); + Helpers::warn("ColourBuffer: Incomplete framebuffer"); } // TODO: This should not clear the framebuffer contents. It should load them from VRAM. diff --git a/include/result/result_os.hpp b/include/result/result_os.hpp index 35d1f378..92025366 100644 --- a/include/result/result_os.hpp +++ b/include/result/result_os.hpp @@ -5,10 +5,11 @@ DEFINE_HORIZON_RESULT_MODULE(Result::OS, OS); namespace Result::OS { DEFINE_HORIZON_RESULT(PortNameTooLong, 30, InvalidArgument, Usage); - DEFINE_HORIZON_RESULT(InvalidHandle, 1015, WrongArgument, Permanent); DEFINE_HORIZON_RESULT(InvalidCombination, 1006, InvalidArgument, Usage); DEFINE_HORIZON_RESULT(MisalignedAddress, 1009, InvalidArgument, Usage); DEFINE_HORIZON_RESULT(MisalignedSize, 1010, InvalidArgument, Usage); + DEFINE_HORIZON_RESULT(NotImplemented, 1012, InvalidArgument, Usage); + DEFINE_HORIZON_RESULT(InvalidHandle, 1015, WrongArgument, Permanent); DEFINE_HORIZON_RESULT(OutOfRange, 1021, InvalidArgument, Usage); DEFINE_HORIZON_RESULT(Timeout, 1022, StatusChanged, Info); }; // namespace Result::OS diff --git a/include/scheduler.hpp b/include/scheduler.hpp new file mode 100644 index 00000000..5645f47d --- /dev/null +++ b/include/scheduler.hpp @@ -0,0 +1,89 @@ +#pragma once +#include +#include +#include + +#include "helpers.hpp" +#include "logger.hpp" + +struct Scheduler { + enum class EventType { + VBlank = 0, // End of frame event + UpdateTimers = 1, // Update kernel timer objects + Panic = 2, // Dummy event that is always pending and should never be triggered (Timestamp = UINT64_MAX) + TotalNumberOfEvents // How many event types do we have in total? + }; + static constexpr usize totalNumberOfEvents = static_cast(EventType::TotalNumberOfEvents); + static constexpr u64 arm11Clock = 268111856; + + template + using EventMap = boost::container::flat_multimap, boost::container::static_vector, size>>; + + EventMap events; + u64 currentTimestamp = 0; + u64 nextTimestamp = 0; + + // Set nextTimestamp to the timestamp of the next event + void updateNextTimestamp() { nextTimestamp = events.cbegin()->first; } + + void addEvent(EventType type, u64 timestamp) { + events.emplace(timestamp, type); + updateNextTimestamp(); + } + + void removeEvent(EventType type) { + for (auto it = events.begin(); it != events.end(); it++) { + // Find first event of type "type" and remove it. + // Our scheduler shouldn't have duplicate events, so it's safe to exit when an event is found + if (it->second == type) { + events.erase(it); + updateNextTimestamp(); + break; + } + } + }; + + void reset() { + currentTimestamp = 0; + + // Clear any pending events + events.clear(); + addEvent(Scheduler::EventType::VBlank, arm11Clock / 60); + + // Add a dummy event to always keep the scheduler non-empty + addEvent(EventType::Panic, std::numeric_limits::max()); + } + + private: + static constexpr u64 MAX_VALUE_TO_MULTIPLY = std::numeric_limits::max() / arm11Clock; + + public: + // Function for converting time units to cycles for various kernel functions + // Thank you Citra + static constexpr s64 nsToCycles(float ns) { return s64(arm11Clock * (0.000000001f) * ns); } + static constexpr s64 nsToCycles(int ns) { return arm11Clock * s64(ns) / 1000000000; } + + static constexpr s64 nsToCycles(s64 ns) { + if (ns / 1000000000 > static_cast(MAX_VALUE_TO_MULTIPLY)) { + return std::numeric_limits::max(); + } + + if (ns > static_cast(MAX_VALUE_TO_MULTIPLY)) { + return arm11Clock * (ns / 1000000000); + } + + return (arm11Clock * ns) / 1000000000; + } + + static constexpr s64 nsToCycles(u64 ns) { + if (ns / 1000000000 > MAX_VALUE_TO_MULTIPLY) { + return std::numeric_limits::max(); + } + + if (ns > MAX_VALUE_TO_MULTIPLY) { + return arm11Clock * (s64(ns) / 1000000000); + } + + return (arm11Clock * s64(ns)) / 1000000000; + } +}; \ No newline at end of file diff --git a/include/services/ac.hpp b/include/services/ac.hpp index a6f68698..4ba53033 100644 --- a/include/services/ac.hpp +++ b/include/services/ac.hpp @@ -19,6 +19,7 @@ class ACService { void getConnectingInfraPriority(u32 messagePointer); void getStatus(u32 messagePointer); void getLastErrorCode(u32 messagePointer); + void getWifiStatus(u32 messagePointer); void isConnected(u32 messagePointer); void registerDisconnectEvent(u32 messagePointer); void setClientVersion(u32 messagePointer); diff --git a/include/services/apt.hpp b/include/services/apt.hpp index a7df056f..48a59c2d 100644 --- a/include/services/apt.hpp +++ b/include/services/apt.hpp @@ -15,6 +15,31 @@ enum class ConsoleModel : u32 { Old3DS, New3DS }; +// https://www.3dbrew.org/wiki/NS_and_APT_Services#Command +namespace APT::Transitions { + enum : u32 { + None = 0, + Wakeup = 1, + Request = 2, + Response = 3, + Exit = 4, + Message = 5, + HomeButtonSingle = 6, + HomeButtonDouble = 7, + DSPSleep = 8, + DSPWakeup = 9, + WakeupByExit = 10, + WakuepByPause = 11, + WakeupByCancel = 12, + WakeupByCancelAll = 13, + WakeupByPowerButton = 14, + WakeupToJumpHome = 15, + RequestForApplet = 16, + WakeupToLaunchApp = 17, + ProcessDed = 0x41 + }; +} + class APTService { Handle handle = KernelHandles::APT; Memory& mem; diff --git a/include/services/boss.hpp b/include/services/boss.hpp index 57452e2d..769184e5 100644 --- a/include/services/boss.hpp +++ b/include/services/boss.hpp @@ -14,6 +14,7 @@ class BOSSService { void cancelTask(u32 messagePointer); void initializeSession(u32 messagePointer); void getErrorCode(u32 messagePointer); + void getNewArrivalFlag(u32 messagePointer); void getNsDataIdList(u32 messagePointer, u32 commandWord); void getOptoutFlag(u32 messagePointer); void getStorageEntryInfo(u32 messagePointer); // Unknown what this is, name taken from Citra diff --git a/include/services/cam.hpp b/include/services/cam.hpp index 611a3b6d..60ede3b9 100644 --- a/include/services/cam.hpp +++ b/include/services/cam.hpp @@ -12,22 +12,45 @@ class Kernel; class CAMService { + using Event = std::optional; + + struct Port { + Event bufferErrorInterruptEvent = std::nullopt; + Event receiveEvent = std::nullopt; + u16 transferBytes; + + void reset() { + bufferErrorInterruptEvent = std::nullopt; + receiveEvent = std::nullopt; + transferBytes = 256; + } + }; + Handle handle = KernelHandles::CAM; Memory& mem; Kernel& kernel; MAKE_LOG_FUNCTION(log, camLogger) - using Event = std::optional; - static constexpr size_t portCount = 4; // PORT_NONE, PORT_CAM1, PORT_CAM2, PORT_BOTH - std::array bufferErrorInterruptEvents; + static constexpr size_t portCount = 2; + std::array ports; // Service commands void driverInitialize(u32 messagePointer); + void driverFinalize(u32 messagePointer); + void getMaxBytes(u32 messagePointer); void getMaxLines(u32 messagePointer); void getBufferErrorInterruptEvent(u32 messagePointer); + void getSuitableY2RCoefficients(u32 messagePointer); + void getTransferBytes(u32 messagePointer); void setContrast(u32 messagePointer); void setFrameRate(u32 messagePointer); + void setReceiving(u32 messagePointer); + void setSize(u32 messagePointer); + void setTransferBytes(u32 messagePointer); void setTransferLines(u32 messagePointer); + void setTrimming(u32 messagePointer); + void setTrimmingParamsCenter(u32 messagePointer); + void startCapture(u32 messagePointer); public: CAMService(Memory& mem, Kernel& kernel) : mem(mem), kernel(kernel) {} diff --git a/include/services/fs.hpp b/include/services/fs.hpp index 2fe3ba5d..4a613121 100644 --- a/include/services/fs.hpp +++ b/include/services/fs.hpp @@ -26,6 +26,7 @@ class FSService { SelfNCCHArchive selfNcch; SaveDataArchive saveData; SDMCArchive sdmc; + SDMCArchive sdmcWriteOnly; NCCHArchive ncch; // UserSaveData archives @@ -61,6 +62,7 @@ class FSService { void getFreeBytes(u32 messagePointer); void getFormatInfo(u32 messagePointer); void getPriority(u32 messagePointer); + void getSdmcArchiveResource(u32 messagePointer); void getThisSaveDataSecureValue(u32 messagePointer); void theGameboyVCFunction(u32 messagePointer); void initialize(u32 messagePointer); @@ -81,9 +83,9 @@ class FSService { public: FSService(Memory& mem, Kernel& kernel, const EmulatorConfig& config) - : mem(mem), saveData(mem), sharedExtSaveData_nand(mem, "../SharedFiles/NAND", true), extSaveData_sdmc(mem, "SDMC"), sdmc(mem), selfNcch(mem), - ncch(mem), userSaveData1(mem, ArchiveID::UserSaveData1), userSaveData2(mem, ArchiveID::UserSaveData2), kernel(kernel), config(config), - systemSaveData(mem) {} + : mem(mem), saveData(mem), sharedExtSaveData_nand(mem, "../SharedFiles/NAND", true), extSaveData_sdmc(mem, "SDMC"), sdmc(mem), + sdmcWriteOnly(mem, true), selfNcch(mem), ncch(mem), userSaveData1(mem, ArchiveID::UserSaveData1), + userSaveData2(mem, ArchiveID::UserSaveData2), kernel(kernel), config(config), systemSaveData(mem) {} void reset(); void handleSyncRequest(u32 messagePointer); diff --git a/include/services/gsp_gpu.hpp b/include/services/gsp_gpu.hpp index 92ca36e2..1e48190c 100644 --- a/include/services/gsp_gpu.hpp +++ b/include/services/gsp_gpu.hpp @@ -60,11 +60,22 @@ class GPUService { }; static_assert(sizeof(FramebufferUpdate) == 64, "GSP::GPU::FramebufferUpdate has the wrong size"); + // Used for saving and restoring GPU state via ImportDisplayCaptureInfo + struct CaptureInfo { + u32 leftFramebuffer; // Left framebuffer VA + u32 rightFramebuffer; // Right framebuffer VA (Top screen only) + u32 format; + u32 stride; + }; + static_assert(sizeof(CaptureInfo) == 16, "GSP::GPU::CaptureInfo has the wrong size"); + // Service commands void acquireRight(u32 messagePointer); void flushDataCache(u32 messagePointer); void importDisplayCaptureInfo(u32 messagePointer); void registerInterruptRelayQueue(u32 messagePointer); + void releaseRight(u32 messagePointer); + void restoreVramSysArea(u32 messagePointer); void saveVramSysArea(u32 messagePointer); void setAxiConfigQoSMode(u32 messagePointer); void setBufferSwap(u32 messagePointer); @@ -85,6 +96,15 @@ class GPUService { void setBufferSwapImpl(u32 screen_id, const FramebufferInfo& info); + // Get the framebuffer info in shared memory for a given screen + FramebufferUpdate* getFramebufferInfo(int screen) { + // TODO: Offset depends on GSP thread being triggered + return reinterpret_cast(&sharedMem[0x200 + screen * sizeof(FramebufferUpdate)]); + } + + FramebufferUpdate* getTopFramebufferInfo() { return getFramebufferInfo(0); } + FramebufferUpdate* getBottomFramebufferInfo() { return getFramebufferInfo(1); } + public: GPUService(Memory& mem, GPU& gpu, Kernel& kernel, u32& currentPID) : mem(mem), gpu(gpu), kernel(kernel), currentPID(currentPID) {} diff --git a/include/services/ptm.hpp b/include/services/ptm.hpp index ae845725..f752839b 100644 --- a/include/services/ptm.hpp +++ b/include/services/ptm.hpp @@ -17,6 +17,7 @@ class PTMService { void getAdapterState(u32 messagePointer); void getBatteryChargeState(u32 messagePointer); void getBatteryLevel(u32 messagePointer); + void getPedometerState(u32 messagePointer); void getStepHistory(u32 messagePointer); void getStepHistoryAll(u32 messagePointer); void getTotalStepCount(u32 messagePointer); diff --git a/include/services/y2r.hpp b/include/services/y2r.hpp index 17ceaafa..a08c41a2 100644 --- a/include/services/y2r.hpp +++ b/include/services/y2r.hpp @@ -1,4 +1,5 @@ #pragma once +#include #include #include "helpers.hpp" #include "kernel_types.hpp" @@ -50,6 +51,17 @@ class Y2RService { Block8x8 = 1, // Output buffer's pixels are morton swizzled. Used when outputting to a GPU texture. }; + // https://github.com/citra-emu/citra/blob/ac9d72a95ca9a60de8d39484a14aecf489d6d016/src/core/hle/service/cam/y2r_u.cpp#L33 + using CoefficientSet = std::array; + static constexpr std::array standardCoefficients{{ + {{0x100, 0x166, 0xB6, 0x58, 0x1C5, -0x166F, 0x10EE, -0x1C5B}}, // ITU_Rec601 + {{0x100, 0x193, 0x77, 0x2F, 0x1DB, -0x1933, 0xA7C, -0x1D51}}, // ITU_Rec709 + {{0x12A, 0x198, 0xD0, 0x64, 0x204, -0x1BDE, 0x10F2, -0x229B}}, // ITU_Rec601_Scaling + {{0x12A, 0x1CA, 0x88, 0x36, 0x21C, -0x1F04, 0x99C, -0x2421}}, // ITU_Rec709_Scaling + }}; + + CoefficientSet conversionCoefficients; // Current conversion coefficients + InputFormat inputFmt; OutputFormat outputFmt; Rotation rotation; @@ -75,6 +87,7 @@ class Y2RService { void setAlpha(u32 messagePointer); void setBlockAlignment(u32 messagePointer); + void setCoefficientParams(u32 messagePointer); void setInputFormat(u32 messagePointer); void setInputLineWidth(u32 messagePointer); void setInputLines(u32 messagePointer); @@ -85,9 +98,12 @@ class Y2RService { void setSendingY(u32 messagePointer); void setSendingU(u32 messagePointer); void setSendingV(u32 messagePointer); + void setSendingYUV(u32 messagePointer); void setSpacialDithering(u32 messagePointer); void setStandardCoeff(u32 messagePointer); void setTemporalDithering(u32 messagePointer); + void getCoefficientParams(u32 messagePointer); + void getStandardCoefficientParams(u32 messagePointer); void startConversion(u32 messagePointer); void stopConversion(u32 messagePointer); diff --git a/readme.md b/readme.md index 8667862d..9e27194c 100644 --- a/readme.md +++ b/readme.md @@ -13,14 +13,23 @@ Join our Discord server by pressing on the banner below, or find us on other pla ![screenshot1](docs/img/KirbyRobobot.png) ![screenshot2](docs/img/OoT_Title.png) ![screenshot3](docs/img/pokegang.png) # Download -You can download stable builds from the Releases tab, or you can download the latest build from the table below +You can download stable builds from the Releases tab, or you can download the latest build from the tables below. Additionally, Panda3DS comes in 2 flavours on PC: A minimal SDL frontend, which does not have a GUI, and an experimental Qt 6 frontend with a proper user interface. +SDL builds (No GUI): |Platform|Status|Download| |--------|------------|--------| |Windows build|[![Windows Build](https://github.com/wheremyfoodat/Panda3DS/actions/workflows/Windows_Build.yml/badge.svg?branch=master)](https://github.com/wheremyfoodat/Panda3DS/actions/workflows/Windows_Build.yml)|[Windows Executable](https://nightly.link/wheremyfoodat/Panda3DS/workflows/Windows_Build/master/Windows%20executable.zip)| |MacOS build|[![MacOS Build](https://github.com/wheremyfoodat/Panda3DS/actions/workflows/MacOS_Build.yml/badge.svg?branch=master)](https://github.com/wheremyfoodat/Panda3DS/actions/workflows/MacOS_Build.yml)|[MacOS App Bundle](https://nightly.link/wheremyfoodat/Panda3DS/workflows/MacOS_Build/master/MacOS%20Alber%20App%20Bundle.zip)| |Linux build|[![Linux Build](https://github.com/wheremyfoodat/Panda3DS/actions/workflows/Linux_Build.yml/badge.svg?branch=master)](https://github.com/wheremyfoodat/Panda3DS/actions/workflows/Linux_Build.yml)|[Linux AppImage](https://nightly.link/wheremyfoodat/Panda3DS/workflows/Linux_AppImage_Build/master/Linux%20executable.zip)| +Qt builds: +|Platform|Status|Download| +|--------|------------|--------| +|Windows build|[![Qt Build](https://github.com/wheremyfoodat/Panda3DS/actions/workflows/Qt_Build.yml/badge.svg)](https://github.com/wheremyfoodat/Panda3DS/actions/workflows/Qt_Build.yml)|[Windows Executable](https://nightly.link/wheremyfoodat/Panda3DS/workflows/Qt_Build/master/Windows%20executable.zip)| +|MacOS build|[![Qt Build](https://github.com/wheremyfoodat/Panda3DS/actions/workflows/Qt_Build.yml/badge.svg)](https://github.com/wheremyfoodat/Panda3DS/actions/workflows/Qt_Build.yml)|[MacOS App Bundle](https://nightly.link/wheremyfoodat/Panda3DS/workflows/Qt_Build/master/MacOS%20Alber%20App%20Bundle.zip)| +|Linux build|[![Qt Build](https://github.com/wheremyfoodat/Panda3DS/actions/workflows/Qt_Build.yml/badge.svg)](https://github.com/wheremyfoodat/Panda3DS/actions/workflows/Qt_Build.yml)|[Linux AppImage](https://nightly.link/wheremyfoodat/Panda3DS/workflows/Qt_Build/master/Linux%20executable.zip)| + + # Compatibility Panda3DS is still in the early stages of development. Many games boot, many don't. Lots of games have at least some hilariously broken graphics, audio is not supported, and some QoL features (including a GUI) are missing. However, even more things are implemented, such as most of the 3DS core required to play games, and various neat features, such as Lua scripting, discord bot support, support for some system apps, cheats, controller support, WIP amiibo support and many more! The emulator is constantly evolving, so make sure to take a peek every now and then! diff --git a/src/config.cpp b/src/config.cpp index 59ee78f6..cd4e1f79 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -11,13 +11,15 @@ // We are legally allowed, as per the author's wish, to use the above code without any licensing restrictions // However we still want to follow the license as closely as possible and offer the proper attributions. -EmulatorConfig::EmulatorConfig(const std::filesystem::path& path) { load(path); } +EmulatorConfig::EmulatorConfig(const std::filesystem::path& path) : filePath(path) { load(); } + +void EmulatorConfig::load() { + const std::filesystem::path& path = filePath; -void EmulatorConfig::load(const std::filesystem::path& path) { // If the configuration file does not exist, create it and return std::error_code error; if (!std::filesystem::exists(path, error)) { - save(path); + save(); return; } @@ -56,7 +58,7 @@ void EmulatorConfig::load(const std::filesystem::path& path) { rendererType = RendererType::OpenGL; } - shaderJitEnabled = toml::find_or(gpu, "EnableShaderJIT", true); + shaderJitEnabled = toml::find_or(gpu, "EnableShaderJIT", shaderJitDefault); } } @@ -84,8 +86,9 @@ void EmulatorConfig::load(const std::filesystem::path& path) { } } -void EmulatorConfig::save(const std::filesystem::path& path) { +void EmulatorConfig::save() { toml::basic_value data; + const std::filesystem::path& path = filePath; std::error_code error; if (std::filesystem::exists(path, error)) { diff --git a/src/core/CPU/cpu_dynarmic.cpp b/src/core/CPU/cpu_dynarmic.cpp index 29ca49d1..da5270b4 100644 --- a/src/core/CPU/cpu_dynarmic.cpp +++ b/src/core/CPU/cpu_dynarmic.cpp @@ -1,32 +1,59 @@ #ifdef CPU_DYNARMIC #include "cpu_dynarmic.hpp" + #include "arm_defs.hpp" +#include "emulator.hpp" -CPU::CPU(Memory& mem, Kernel& kernel) : mem(mem), env(mem, kernel) { - cp15 = std::make_shared(); +CPU::CPU(Memory& mem, Kernel& kernel, Emulator& emu) : mem(mem), emu(emu), scheduler(emu.getScheduler()), env(mem, kernel, emu.getScheduler()) { + cp15 = std::make_shared(); - Dynarmic::A32::UserConfig config; - config.arch_version = Dynarmic::A32::ArchVersion::v6K; - config.callbacks = &env; - config.coprocessors[15] = cp15; - config.define_unpredictable_behaviour = true; - config.global_monitor = &exclusiveMonitor; - config.processor_id = 0; - - jit = std::make_unique(config); + Dynarmic::A32::UserConfig config; + config.arch_version = Dynarmic::A32::ArchVersion::v6K; + config.callbacks = &env; + config.coprocessors[15] = cp15; + config.define_unpredictable_behaviour = true; + config.global_monitor = &exclusiveMonitor; + config.processor_id = 0; + + jit = std::make_unique(config); } void CPU::reset() { - setCPSR(CPSR::UserMode); - setFPSCR(FPSCR::MainThreadDefault); - env.totalTicks = 0; + setCPSR(CPSR::UserMode); + setFPSCR(FPSCR::MainThreadDefault); + env.totalTicks = 0; - cp15->reset(); - cp15->setTLSBase(VirtualAddrs::TLSBase); // Set cp15 TLS pointer to the main thread's thread-local storage - jit->Reset(); - jit->ClearCache(); - jit->Regs().fill(0); - jit->ExtRegs().fill(0); + cp15->reset(); + cp15->setTLSBase(VirtualAddrs::TLSBase); // Set cp15 TLS pointer to the main thread's thread-local storage + jit->Reset(); + jit->ClearCache(); + jit->Regs().fill(0); + jit->ExtRegs().fill(0); } -#endif // CPU_DYNARMIC \ No newline at end of file +void CPU::runFrame() { + emu.frameDone = false; + + while (!emu.frameDone) { + // Run CPU until the next scheduler event + env.ticksLeft = scheduler.nextTimestamp - scheduler.currentTimestamp; + + execute: + const auto exitReason = jit->Run(); + + // Handle any scheduler events that need handling. + emu.pollScheduler(); + + if (static_cast(exitReason) != 0) [[unlikely]] { + // Cache invalidation needs to exit the JIT so it returns a CacheInvalidation HaltReason. In our case, we just go back to executing + // The goto might be terrible but it does guarantee that this does not recursively call run and crash, instead getting optimized to a jump + if (Dynarmic::Has(exitReason, Dynarmic::HaltReason::CacheInvalidation)) { + goto execute; + } else { + Helpers::panic("Exit reason: %d\nPC: %08X", static_cast(exitReason), getReg(15)); + } + } + } +} + +#endif // CPU_DYNARMIC \ No newline at end of file diff --git a/src/core/PICA/dynapica/shader_rec_emitter_arm64.cpp b/src/core/PICA/dynapica/shader_rec_emitter_arm64.cpp new file mode 100644 index 00000000..e2ff59f5 --- /dev/null +++ b/src/core/PICA/dynapica/shader_rec_emitter_arm64.cpp @@ -0,0 +1,947 @@ +#if defined(PANDA3DS_DYNAPICA_SUPPORTED) && defined(PANDA3DS_ARM64_HOST) +#include "PICA/dynapica/shader_rec_emitter_arm64.hpp" + +#include + +using namespace Helpers; +using namespace oaknut; +using namespace oaknut::util; + +// Similar to the x64 recompiler, we use an odd internal ABI, which abuses the fact that we'll very rarely be calling C++ functions +// So to avoid pushing and popping, we'll be making use of volatile registers as much as possible +static constexpr QReg scratch1 = Q0; +static constexpr QReg scratch2 = Q1; +static constexpr QReg src1_vec = Q2; +static constexpr QReg src2_vec = Q3; +static constexpr QReg src3_vec = Q4; +static constexpr QReg onesVector = Q5; + +static constexpr XReg arg1 = X0; +static constexpr XReg arg2 = X1; +static constexpr XReg statePointer = X9; + +void ShaderEmitter::compile(const PICAShader& shaderUnit) { + oaknut::CodeBlock::unprotect(); // Unprotect the memory before writing to it + + // Constants + align(16); + // Generate blending masks for doing masked writes to registers + l(blendMasks); + for (int i = 0; i < 16; i++) { + dw((i & 0x8) ? 0xFFFFFFFF : 0); // Mask for x component + dw((i & 0x4) ? 0xFFFFFFFF : 0); // Mask for y component + dw((i & 0x2) ? 0xFFFFFFFF : 0); // Mask for z component + dw((i & 0x1) ? 0xFFFFFFFF : 0); // Mask for w component + } + + // Emit prologue first + oaknut::Label prologueLabel; + align(16); + + l(prologueLabel); + prologueCb = prologueLabel.ptr(); + + // Set state pointer to the proper pointer + // state pointer is volatile, no need to preserve it + MOV(statePointer, arg1); + // Generate a vector of all 1.0s for SLT/SGE/RCP/RSQ + FMOV(onesVector.S4(), FImm8(0x70)); + + // Push a return guard on the stack. This happens due to the way we handle the PICA callstack, by pushing the return PC to stack + // By pushing -1, we make it impossible for a return check to erroneously pass + MOV(arg1, 0xffffffffffffffffll); + // Backup link register (X30) and push return guard + STP(arg1, X30, SP, PRE_INDEXED, -16); + + // Jump to code with a tail call + BR(arg2); + + // Scan the code for call, exp2, log2, etc instructions which need some special care + // After that, emit exp2 and log2 functions if the corresponding instructions are present + scanCode(shaderUnit); + if (codeHasExp2) Helpers::panic("arm64 shader JIT: Code has exp2"); + if (codeHasLog2) Helpers::panic("arm64 shader JIT: Code has log2"); + + align(16); + // Compile every instruction in the shader + // This sounds horrible but the PICA instruction memory is tiny, and most of the time it's padded wtih nops that compile to nothing + recompilerPC = 0; + loopLevel = 0; + compileUntil(shaderUnit, PICAShader::maxInstructionCount); + + // Protect the memory and invalidate icache before executing the code + oaknut::CodeBlock::protect(); + oaknut::CodeBlock::invalidate_all(); +} + +void ShaderEmitter::scanCode(const PICAShader& shaderUnit) { + returnPCs.clear(); + + for (u32 i = 0; i < PICAShader::maxInstructionCount; i++) { + const u32 instruction = shaderUnit.loadedShader[i]; + const u32 opcode = instruction >> 26; + + if (isCall(instruction)) { + const u32 num = instruction & 0xff; + const u32 dest = getBits<10, 12>(instruction); + const u32 returnPC = num + dest; // Add them to get the return PC + + returnPCs.push_back(returnPC); + } else if (opcode == ShaderOpcodes::EX2) { + codeHasExp2 = true; + } else if (opcode == ShaderOpcodes::LG2) { + codeHasLog2 = true; + } + } + + // Sort return PCs so they can be binary searched + std::sort(returnPCs.begin(), returnPCs.end()); +} + +void ShaderEmitter::compileUntil(const PICAShader& shaderUnit, u32 end) { + while (recompilerPC < end) { + compileInstruction(shaderUnit); + } +} + +void ShaderEmitter::compileInstruction(const PICAShader& shaderUnit) { + // Write current location to label for this instruction + l(instructionLabels[recompilerPC]); + + // See if PC is a possible return PC and emit the proper code if so + if (std::binary_search(returnPCs.begin(), returnPCs.end(), recompilerPC)) { + Label skipReturn; + + LDP(X0, XZR, SP); // W0 = Next return address + MOV(W1, recompilerPC); // W1 = Current PC + CMP(W0, W1); // If they're equal, execute a RET, otherwise skip it + B(NE, skipReturn); + RET(); + + l(skipReturn); + } + + // Fetch instruction and inc PC + const u32 instruction = shaderUnit.loadedShader[recompilerPC++]; + const u32 opcode = instruction >> 26; + + switch (opcode) { + case ShaderOpcodes::ADD: recADD(shaderUnit, instruction); break; + case ShaderOpcodes::CALL: recCALL(shaderUnit, instruction); break; + case ShaderOpcodes::CALLC: recCALLC(shaderUnit, instruction); break; + case ShaderOpcodes::CALLU: recCALLU(shaderUnit, instruction); break; + case ShaderOpcodes::CMP1: + case ShaderOpcodes::CMP2: recCMP(shaderUnit, instruction); break; + case ShaderOpcodes::DP3: recDP3(shaderUnit, instruction); break; + case ShaderOpcodes::DP4: recDP4(shaderUnit, instruction); break; + // case ShaderOpcodes::DPH: + // case ShaderOpcodes::DPHI: recDPH(shaderUnit, instruction); break; + case ShaderOpcodes::END: recEND(shaderUnit, instruction); break; + // case ShaderOpcodes::EX2: recEX2(shaderUnit, instruction); break; + case ShaderOpcodes::FLR: recFLR(shaderUnit, instruction); break; + case ShaderOpcodes::IFC: recIFC(shaderUnit, instruction); break; + case ShaderOpcodes::IFU: recIFU(shaderUnit, instruction); break; + case ShaderOpcodes::JMPC: recJMPC(shaderUnit, instruction); break; + case ShaderOpcodes::JMPU: recJMPU(shaderUnit, instruction); break; + // case ShaderOpcodes::LG2: recLG2(shaderUnit, instruction); break; + case ShaderOpcodes::LOOP: recLOOP(shaderUnit, instruction); break; + case ShaderOpcodes::MOV: recMOV(shaderUnit, instruction); break; + case ShaderOpcodes::MOVA: recMOVA(shaderUnit, instruction); break; + case ShaderOpcodes::MAX: recMAX(shaderUnit, instruction); break; + case ShaderOpcodes::MIN: recMIN(shaderUnit, instruction); break; + case ShaderOpcodes::MUL: recMUL(shaderUnit, instruction); break; + case ShaderOpcodes::NOP: break; + case ShaderOpcodes::RCP: recRCP(shaderUnit, instruction); break; + case ShaderOpcodes::RSQ: recRSQ(shaderUnit, instruction); break; + + // Unimplemented opcodes that don't seem to actually be used but exist in the binary + // EMIT/SETEMIT are used in geometry shaders, however are sometimes found in vertex shaders? + case ShaderOpcodes::EMIT: + case ShaderOpcodes::SETEMIT: log("[ShaderJIT] Unimplemented PICA opcode: %02X\n", opcode); break; + + case ShaderOpcodes::BREAK: + case ShaderOpcodes::BREAKC: Helpers::warn("[Shader JIT] Unimplemented BREAK(C) instruction!"); break; + + // We consider both MAD and MADI to be the same instruction and decode which one we actually have in recMAD + case 0x30: + case 0x31: + case 0x32: + case 0x33: + case 0x34: + case 0x35: + case 0x36: + case 0x37: + case 0x38: + case 0x39: + case 0x3A: + case 0x3B: + case 0x3C: + case 0x3D: + case 0x3E: + case 0x3F: recMAD(shaderUnit, instruction); break; + + case ShaderOpcodes::SLT: + case ShaderOpcodes::SLTI: recSLT(shaderUnit, instruction); break; + + case ShaderOpcodes::SGE: + case ShaderOpcodes::SGEI: recSGE(shaderUnit, instruction); break; + + default: Helpers::panic("Shader JIT: Unimplemented PICA opcode %X", opcode); + } +} + +const ShaderEmitter::vec4f& ShaderEmitter::getSourceRef(const PICAShader& shader, u32 src) { + if (src < 0x10) + return shader.inputs[src]; + else if (src < 0x20) + return shader.tempRegisters[src - 0x10]; + else if (src <= 0x7f) + return shader.floatUniforms[src - 0x20]; + else { + Helpers::warn("[Shader JIT] Unimplemented source value: %X\n", src); + return shader.dummy; + } +} + +const ShaderEmitter::vec4f& ShaderEmitter::getDestRef(const PICAShader& shader, u32 dest) { + if (dest < 0x10) { + return shader.outputs[dest]; + } else if (dest < 0x20) { + return shader.tempRegisters[dest - 0x10]; + } + Helpers::panic("[Shader JIT] Unimplemented dest: %X", dest); +} + +// See shader.hpp header for docs on how the swizzle and negate works +template +void ShaderEmitter::loadRegister(QReg dest, const PICAShader& shader, u32 src, u32 index, u32 operandDescriptor) { + u32 compSwizzle; // Component swizzle pattern for the register + bool negate; // If true, negate all lanes of the register + + if constexpr (sourceIndex == 1) { // SRC1 + negate = (getBit<4>(operandDescriptor)) != 0; + compSwizzle = getBits<5, 8>(operandDescriptor); + } else if constexpr (sourceIndex == 2) { // SRC2 + negate = (getBit<13>(operandDescriptor)) != 0; + compSwizzle = getBits<14, 8>(operandDescriptor); + } else if constexpr (sourceIndex == 3) { // SRC3 + negate = (getBit<22>(operandDescriptor)) != 0; + compSwizzle = getBits<23, 8>(operandDescriptor); + } + + // TODO: Do indexes get applied if src < 0x20? + + switch (index) { + case 0: + [[likely]] { // Keep src as is, no need to offset it + const vec4f& srcRef = getSourceRef(shader, src); + const uintptr_t offset = uintptr_t(&srcRef) - uintptr_t(&shader); // Calculate offset of register from start of the state struct + + LDR(dest, statePointer, offset); + switch (compSwizzle) { + case noSwizzle: break; // .xyzw + case 0x0: DUP(dest.S4(), dest.Selem()[0]); break; // .xxxx + case 0x55: DUP(dest.S4(), dest.Selem()[1]); break; // .yyyy + case 0xAA: DUP(dest.S4(), dest.Selem()[2]); break; // .zzzz + case 0xFF: + DUP(dest.S4(), dest.Selem()[3]); + break; // .wwww + + // Some of these cases may still be optimizable + default: { + MOV(scratch1.B16(), dest.B16()); // Make a copy of the register + + const auto newX = getBits<6, 2>(compSwizzle); + const auto newY = getBits<4, 2>(compSwizzle); + const auto newZ = getBits<2, 2>(compSwizzle); + const auto newW = getBits<0, 2>(compSwizzle); + + // If the lane swizzled into the new x component is NOT the current x component, swizzle the correct lane with a mov + // Repeat for each component of the vector + if (newX != 0) { + MOV(dest.Selem()[0], scratch1.Selem()[newX]); + } + + if (newY != 1) { + MOV(dest.Selem()[1], scratch1.Selem()[newY]); + } + + if (newZ != 2) { + MOV(dest.Selem()[2], scratch1.Selem()[newZ]); + } + + if (newW != 3) { + MOV(dest.Selem()[3], scratch1.Selem()[newW]); + } + + break; + } + } + + // Negate the register if necessary + if (negate) { + FNEG(dest.S4(), dest.S4()); + } + return; // Return. Rest of the function handles indexing which is not used if index == 0 + } + + case 1: { + const uintptr_t addrXOffset = uintptr_t(&shader.addrRegister[0]) - uintptr_t(&shader); + LDRSW(X0, statePointer, addrXOffset); // X0 = address register X + break; + } + + case 2: { + const uintptr_t addrYOffset = uintptr_t(&shader.addrRegister[1]) - uintptr_t(&shader); + LDRSW(X0, statePointer, addrYOffset); // X0 = address register Y + break; + } + + case 3: { + const uintptr_t loopCounterOffset = uintptr_t(&shader.loopCounter) - uintptr_t(&shader); + LDR(W0, statePointer, loopCounterOffset); // X0 = loop counter + break; + } + + default: Helpers::panic("[ShaderJIT]: Unimplemented source index type %d", index); + } + + // Swizzle and load register into dest, from [state pointer + X1 + offset] and apply the relevant swizzle. Thrashes X2 + auto swizzleAndLoadReg = [this, &dest, &compSwizzle](size_t offset) { + MOV(X2, offset); + ADD(X1, X1, X2); + LDR(dest, statePointer, X1); + + switch (compSwizzle) { + case noSwizzle: break; // .xyzw + case 0x0: DUP(dest.S4(), dest.Selem()[0]); break; // .xxxx + case 0x55: DUP(dest.S4(), dest.Selem()[1]); break; // .yyyy + case 0xAA: DUP(dest.S4(), dest.Selem()[2]); break; // .zzzz + case 0xFF: + DUP(dest.S4(), dest.Selem()[3]); + break; // .wwww + + // Some of these cases may still be optimizable + default: { + MOV(scratch1.B16(), dest.B16()); // Make a copy of the register + + const auto newX = getBits<6, 2>(compSwizzle); + const auto newY = getBits<4, 2>(compSwizzle); + const auto newZ = getBits<2, 2>(compSwizzle); + const auto newW = getBits<0, 2>(compSwizzle); + + // If the lane swizzled into the new x component is NOT the current x component, swizzle the correct lane with a mov + // Repeat for each component of the vector + if (newX != 0) { + MOV(dest.Selem()[0], scratch1.Selem()[newX]); + } + + if (newY != 1) { + MOV(dest.Selem()[1], scratch1.Selem()[newY]); + } + + if (newZ != 2) { + MOV(dest.Selem()[2], scratch1.Selem()[newZ]); + } + + if (newW != 3) { + MOV(dest.Selem()[3], scratch1.Selem()[newW]); + } + + break; + } + } + }; + + // Here we handle what happens when using indexed addressing & we can't predict what register will be read at compile time + // The index of the access is assumed to be in X0 + // Add source register (src) and index (X0) to form the final register + ADD(X0, X0, src); + + Label maybeTemp, maybeUniform, unknownReg, end; + const uintptr_t inputOffset = uintptr_t(&shader.inputs[0]) - uintptr_t(&shader); + const uintptr_t tempOffset = uintptr_t(&shader.tempRegisters[0]) - uintptr_t(&shader); + const uintptr_t uniformOffset = uintptr_t(&shader.floatUniforms[0]) - uintptr_t(&shader); + + // If reg < 0x10, return inputRegisters[reg] + CMP(X0, 0x10); + B(HS, maybeTemp); + LSL(X1, X0, 4); + swizzleAndLoadReg(inputOffset); + B(end); + + // If (reg < 0x1F) return tempRegisters[reg - 0x10] + l(maybeTemp); + CMP(X0, 0x20); + B(HS, maybeUniform); + SUB(X1, X0, 0x10); + LSL(X1, X1, 4); + swizzleAndLoadReg(tempOffset); + B(end); + + // If (reg < 0x80) return floatUniforms[reg - 0x20] + l(maybeUniform); + CMP(X0, 0x80); + B(HS, unknownReg); + SUB(X1, X0, 0x20); + LSL(X1, X1, 4); + swizzleAndLoadReg(uniformOffset); + B(end); + + l(unknownReg); + MOVI(dest.S4(), 0); // Set dest to 0 if we're reading from a garbage register + + l(end); + // Negate the register if necessary + if (negate) { + FNEG(dest.S4(), dest.S4()); + } +} + +void ShaderEmitter::storeRegister(QReg source, const PICAShader& shader, u32 dest, u32 operandDescriptor) { + const vec4f& destRef = getDestRef(shader, dest); + const uintptr_t offset = uintptr_t(&destRef) - uintptr_t(&shader); // Calculate offset of register from start of the state struct + + // Mask of which lanes to write + u32 writeMask = operandDescriptor & 0xf; + if (writeMask == 0xf) { // No lanes are masked, just use STR + STR(source, statePointer, offset); + } else { + LDR(scratch1, statePointer, offset); // Load current value + LDR(scratch2, blendMasks.ptr() + writeMask * 16); // Load write mask for blending + + BSL(scratch2.B16(), source.B16(), scratch1.B16()); // Scratch2 = (Source & mask) | (original & ~mask) + STR(scratch2, statePointer, offset); // Write it back + } +} + +void ShaderEmitter::recMOV(const PICAShader& shader, u32 instruction) { + const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x7f]; + const u32 src = getBits<12, 7>(instruction); + const u32 idx = getBits<19, 2>(instruction); + const u32 dest = getBits<21, 5>(instruction); + + loadRegister<1>(src1_vec, shader, src, idx, operandDescriptor); // Load source 1 into scratch1 + storeRegister(src1_vec, shader, dest, operandDescriptor); +} + +void ShaderEmitter::recFLR(const PICAShader& shader, u32 instruction) { + const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x7f]; + const u32 src = getBits<12, 7>(instruction); + const u32 idx = getBits<19, 2>(instruction); + const u32 dest = getBits<21, 5>(instruction); + + loadRegister<1>(src1_vec, shader, src, idx, operandDescriptor); // Load source 1 into scratch1 + FRINTM(src1_vec.S4(), src1_vec.S4()); // Floor it and store into dest + storeRegister(src1_vec, shader, dest, operandDescriptor); +} + +void ShaderEmitter::recMOVA(const PICAShader& shader, u32 instruction) { + const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x7f]; + const u32 src = getBits<12, 7>(instruction); + const u32 idx = getBits<19, 2>(instruction); + + const bool writeX = getBit<3>(operandDescriptor); // Should we write the x component of the address register? + const bool writeY = getBit<2>(operandDescriptor); + + static_assert(sizeof(shader.addrRegister) == 2 * sizeof(s32)); // Assert that the address register is 2 s32s + const uintptr_t addrRegisterOffset = uintptr_t(&shader.addrRegister[0]) - uintptr_t(&shader); + const uintptr_t addrRegisterYOffset = addrRegisterOffset + sizeof(shader.addrRegister[0]); + + // If no register is being written to then it is a nop. Probably not common but whatever + if (!writeX && !writeY) return; + + loadRegister<1>(src1_vec, shader, src, idx, operandDescriptor); + FCVTZS(src1_vec.S4(), src1_vec.S4()); // Convert src1 from floats to s32s with truncation + + // Write both together + if (writeX && writeY) { + STR(src1_vec.toD(), statePointer, addrRegisterOffset); + } else if (writeX) { + STR(src1_vec.toS(), statePointer, addrRegisterOffset); + } else if (writeY) { + MOV(W0, src1_vec.Selem()[1]); // W0 = Y component + STR(W0, statePointer, addrRegisterYOffset); + } +} + +void ShaderEmitter::recDP3(const PICAShader& shader, u32 instruction) { + const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x7f]; + const u32 src1 = getBits<12, 7>(instruction); + const u32 src2 = getBits<7, 5>(instruction); + const u32 idx = getBits<19, 2>(instruction); + const u32 dest = getBits<21, 5>(instruction); + const u32 writeMask = getBits<0, 4>(operandDescriptor); + + // TODO: Safe multiplication equivalent (Multiplication is not IEEE compliant on the PICA) + loadRegister<1>(src1_vec, shader, src1, idx, operandDescriptor); + loadRegister<2>(src2_vec, shader, src2, 0, operandDescriptor); + // Set W component of src1 to 0.0, so that the w factor of the following dp4 will become 0, making it equivalent to a dp3 + INS(src1_vec.Selem()[3], WZR); + + // Now do a full DP4 + FMUL(src1_vec.S4(), src1_vec.S4(), src2_vec.S4()); // Do a piecewise multiplication of the vectors first + FADDP(src1_vec.S4(), src1_vec.S4(), src1_vec.S4()); // Now add the adjacent components together + FADDP(src1_vec.toS(), src1_vec.toD().S2()); // Again for the bottom 2 lanes. Now the bottom lane contains the dot product + + if (writeMask != 0x8) { // Copy bottom lane to all lanes if we're not simply writing back x + DUP(src1_vec.S4(), src1_vec.Selem()[0]); // src1_vec = src1_vec.xxxx + } + + storeRegister(src1_vec, shader, dest, operandDescriptor); +} + +void ShaderEmitter::recDP4(const PICAShader& shader, u32 instruction) { + const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x7f]; + const u32 src1 = getBits<12, 7>(instruction); + const u32 src2 = getBits<7, 5>(instruction); + const u32 idx = getBits<19, 2>(instruction); + const u32 dest = getBits<21, 5>(instruction); + const u32 writeMask = getBits<0, 4>(operandDescriptor); + + // TODO: Safe multiplication equivalent (Multiplication is not IEEE compliant on the PICA) + loadRegister<1>(src1_vec, shader, src1, idx, operandDescriptor); + loadRegister<2>(src2_vec, shader, src2, 0, operandDescriptor); + + FMUL(src1_vec.S4(), src1_vec.S4(), src2_vec.S4()); // Do a piecewise multiplication of the vectors first + FADDP(src1_vec.S4(), src1_vec.S4(), src1_vec.S4()); // Now add the adjacent components together + FADDP(src1_vec.toS(), src1_vec.toD().S2()); // Again for the bottom 2 lanes. Now the bottom lane contains the dot product + + if (writeMask != 0x8) { // Copy bottom lane to all lanes if we're not simply writing back x + DUP(src1_vec.S4(), src1_vec.Selem()[0]); // src1_vec = src1_vec.xxxx + } + + storeRegister(src1_vec, shader, dest, operandDescriptor); +} + +void ShaderEmitter::recADD(const PICAShader& shader, u32 instruction) { + const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x7f]; + const u32 src1 = getBits<12, 7>(instruction); + const u32 src2 = getBits<7, 5>(instruction); + const u32 idx = getBits<19, 2>(instruction); + const u32 dest = getBits<21, 5>(instruction); + + loadRegister<1>(src1_vec, shader, src1, idx, operandDescriptor); + loadRegister<2>(src2_vec, shader, src2, 0, operandDescriptor); + FADD(src1_vec.S4(), src1_vec.S4(), src2_vec.S4()); + storeRegister(src1_vec, shader, dest, operandDescriptor); +} + +void ShaderEmitter::recMAX(const PICAShader& shader, u32 instruction) { + const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x7f]; + const u32 src1 = getBits<12, 7>(instruction); + const u32 src2 = getBits<7, 5>(instruction); + const u32 idx = getBits<19, 2>(instruction); + const u32 dest = getBits<21, 5>(instruction); + + loadRegister<1>(src1_vec, shader, src1, idx, operandDescriptor); + loadRegister<2>(src2_vec, shader, src2, 0, operandDescriptor); + FMAX(src1_vec.S4(), src1_vec.S4(), src2_vec.S4()); + storeRegister(src1_vec, shader, dest, operandDescriptor); +} + +void ShaderEmitter::recMIN(const PICAShader& shader, u32 instruction) { + const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x7f]; + const u32 src1 = getBits<12, 7>(instruction); + const u32 src2 = getBits<7, 5>(instruction); + const u32 idx = getBits<19, 2>(instruction); + const u32 dest = getBits<21, 5>(instruction); + + loadRegister<1>(src1_vec, shader, src1, idx, operandDescriptor); + loadRegister<2>(src2_vec, shader, src2, 0, operandDescriptor); + FMIN(src1_vec.S4(), src1_vec.S4(), src2_vec.S4()); + storeRegister(src1_vec, shader, dest, operandDescriptor); +} + +void ShaderEmitter::recMUL(const PICAShader& shader, u32 instruction) { + const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x7f]; + const u32 src1 = getBits<12, 7>(instruction); + const u32 src2 = getBits<7, 5>(instruction); // src2 coming first because PICA moment + const u32 idx = getBits<19, 2>(instruction); + const u32 dest = getBits<21, 5>(instruction); + + // TODO: Safe multiplication equivalent (Multiplication is not IEEE compliant on the PICA) + loadRegister<1>(src1_vec, shader, src1, idx, operandDescriptor); + loadRegister<2>(src2_vec, shader, src2, 0, operandDescriptor); + FMUL(src1_vec.S4(), src1_vec.S4(), src2_vec.S4()); + storeRegister(src1_vec, shader, dest, operandDescriptor); +} + +void ShaderEmitter::recRCP(const PICAShader& shader, u32 instruction) { + const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x7f]; + const u32 src = getBits<12, 7>(instruction); + const u32 idx = getBits<19, 2>(instruction); + const u32 dest = getBits<21, 5>(instruction); + const u32 writeMask = operandDescriptor & 0xf; + + loadRegister<1>(src1_vec, shader, src, idx, operandDescriptor); // Load source 1 into scratch1 + FDIV(src1_vec.toS(), onesVector.toS(), src1_vec.toS()); // src1 = 1.0 / src1 + + // If we only write back the x component to the result, we needn't perform a shuffle to do res = res.xxxx + // Otherwise we do + if (writeMask != 0x8) { // Copy bottom lane to all lanes if we're not simply writing back x + DUP(src1_vec.S4(), src1_vec.Selem()[0]); // src1_vec = src1_vec.xxxx + } + + storeRegister(src1_vec, shader, dest, operandDescriptor); +} + +void ShaderEmitter::recRSQ(const PICAShader& shader, u32 instruction) { + const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x7f]; + const u32 src = getBits<12, 7>(instruction); + const u32 idx = getBits<19, 2>(instruction); + const u32 dest = getBits<21, 5>(instruction); + const u32 writeMask = operandDescriptor & 0xf; + constexpr bool useAccurateRSQ = true; + + loadRegister<1>(src1_vec, shader, src, idx, operandDescriptor); // Load source 1 into scratch1 + + // Compute reciprocal square root approximation + // TODO: Should this use frsqte or fsqrt+div? The former is faster but less accurate + // PICA RSQ uses f24 precision though, so it'll be inherently innacurate, and it's likely using an inaccurate approximation too, seeing as + // It doesn't have regular sqrt/div instructions. + // For now, we default to accurate inverse square root + if constexpr (useAccurateRSQ) { + FSQRT(src1_vec.toS(), src1_vec.toS()); // src1 = sqrt(src1), scalar + FDIV(src1_vec.toS(), onesVector.toS(), src1_vec.toS()); // Now invert src1 + } else { + FRSQRTE(src1_vec.toS(), src1_vec.toS()); // Much nicer + } + + // If we only write back the x component to the result, we needn't perform a shuffle to do res = res.xxxx + // Otherwise we do + if (writeMask != 0x8) { // Copy bottom lane to all lanes if we're not simply writing back x + DUP(src1_vec.S4(), src1_vec.Selem()[0]); // src1_vec = src1_vec.xxxx + } + + storeRegister(src1_vec, shader, dest, operandDescriptor); +} + +void ShaderEmitter::recMAD(const PICAShader& shader, u32 instruction) { + const bool isMADI = getBit<29>(instruction) == 0; + + const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x1f]; + const u32 src1 = getBits<17, 5>(instruction); + const u32 src2 = isMADI ? getBits<12, 5>(instruction) : getBits<10, 7>(instruction); + const u32 src3 = isMADI ? getBits<5, 7>(instruction) : getBits<5, 5>(instruction); + const u32 idx = getBits<22, 2>(instruction); + const u32 dest = getBits<24, 5>(instruction); + + loadRegister<1>(src1_vec, shader, src1, 0, operandDescriptor); + loadRegister<2>(src2_vec, shader, src2, isMADI ? 0 : idx, operandDescriptor); + loadRegister<3>(src3_vec, shader, src3, isMADI ? idx : 0, operandDescriptor); + + // TODO: Safe PICA multiplication + FMLA(src3_vec.S4(), src1_vec.S4(), src2_vec.S4()); + storeRegister(src3_vec, shader, dest, operandDescriptor); +} + +void ShaderEmitter::recSLT(const PICAShader& shader, u32 instruction) { + const bool isSLTI = (instruction >> 26) == ShaderOpcodes::SLTI; + const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x7f]; + + const u32 src1 = isSLTI ? getBits<14, 5>(instruction) : getBits<12, 7>(instruction); + const u32 src2 = isSLTI ? getBits<7, 7>(instruction) : getBits<7, 5>(instruction); + const u32 idx = getBits<19, 2>(instruction); + const u32 dest = getBits<21, 5>(instruction); + + loadRegister<1>(src1_vec, shader, src1, isSLTI ? 0 : idx, operandDescriptor); + loadRegister<2>(src2_vec, shader, src2, isSLTI ? idx : 0, operandDescriptor); + // Set each lane of SRC1 to FFFFFFFF if src2 > src1, else to 0. NEON does not have FCMLT so we use FCMGT with inverted operands + // This is more or less a direct port of the relevant x64 JIT code + FCMGT(src1_vec.S4(), src2_vec.S4(), src1_vec.S4()); + AND(src1_vec.B16(), src1_vec.B16(), onesVector.B16()); // AND with vec4(1.0) to convert the FFFFFFFF lanes into 1.0 + storeRegister(src1_vec, shader, dest, operandDescriptor); +} + +void ShaderEmitter::recSGE(const PICAShader& shader, u32 instruction) { + const bool isSGEI = (instruction >> 26) == ShaderOpcodes::SGEI; + const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x7f]; + + const u32 src1 = isSGEI ? getBits<14, 5>(instruction) : getBits<12, 7>(instruction); + const u32 src2 = isSGEI ? getBits<7, 7>(instruction) : getBits<7, 5>(instruction); + const u32 idx = getBits<19, 2>(instruction); + const u32 dest = getBits<21, 5>(instruction); + + loadRegister<1>(src1_vec, shader, src1, isSGEI ? 0 : idx, operandDescriptor); + loadRegister<2>(src2_vec, shader, src2, isSGEI ? idx : 0, operandDescriptor); + // Set each lane of SRC1 to FFFFFFFF if src1 >= src2, else to 0. + // This is more or less a direct port of the relevant x64 JIT code + FCMGE(src1_vec.S4(), src1_vec.S4(), src2_vec.S4()); + AND(src1_vec.B16(), src1_vec.B16(), onesVector.B16()); // AND with vec4(1.0) to convert the FFFFFFFF lanes into 1.0 + storeRegister(src1_vec, shader, dest, operandDescriptor); +} + +void ShaderEmitter::recCMP(const PICAShader& shader, u32 instruction) { + const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x7f]; + const u32 src1 = getBits<12, 7>(instruction); + const u32 src2 = getBits<7, 5>(instruction); // src2 coming first because PICA moment + const u32 idx = getBits<19, 2>(instruction); + const u32 cmpY = getBits<21, 3>(instruction); + const u32 cmpX = getBits<24, 3>(instruction); + + loadRegister<1>(src1_vec, shader, src1, idx, operandDescriptor); + loadRegister<2>(src2_vec, shader, src2, 0, operandDescriptor); + + // Map from PICA condition codes (used as index) to x86 condition codes + // We treat invalid condition codes as "always" as suggested by 3DBrew + static constexpr std::array conditionCodes = { + oaknut::util::EQ, oaknut::util::NE, oaknut::util::LT, oaknut::util::LE, + oaknut::util::GT, oaknut::util::GE, oaknut::util::AL, oaknut::util::AL, + }; + + static_assert(sizeof(shader.cmpRegister[0]) == 1 && sizeof(shader.cmpRegister) == 2); // The code below relies on bool being 1 byte exactly + const size_t cmpRegXOffset = uintptr_t(&shader.cmpRegister[0]) - uintptr_t(&shader); + + // NEON doesn't have SIMD comparisons to do fun stuff with like on x64 + FCMP(src1_vec.toS(), src2_vec.toS()); + CSET(W0, conditionCodes[cmpX]); + + // Compare Y components, which annoyingly enough can't be done without moving + MOV(scratch1.toS(), src1_vec.Selem()[1]); + MOV(scratch2.toS(), src2_vec.Selem()[1]); + FCMP(scratch1.toS(), scratch2.toS()); + CSET(W1, conditionCodes[cmpY]); + + // Merge the booleans and write them back in one STRh + ORR(W0, W0, W1, LogShift::LSL, 8); + STRH(W0, statePointer, cmpRegXOffset); +} + +void ShaderEmitter::checkBoolUniform(const PICAShader& shader, u32 instruction) { + const u32 bit = getBits<22, 4>(instruction); // Bit of the bool uniform to check + const uintptr_t boolUniformOffset = uintptr_t(&shader.boolUniform) - uintptr_t(&shader); + + LDRH(W0, statePointer, boolUniformOffset); // Load bool uniform into w0 + TST(W0, 1 << bit); // Check if bit is set +} + +void ShaderEmitter::checkCmpRegister(const PICAShader& shader, u32 instruction) { + static_assert(sizeof(bool) == 1 && sizeof(shader.cmpRegister) == 2); // The code below relies on bool being 1 byte exactly + const size_t cmpRegXOffset = uintptr_t(&shader.cmpRegister[0]) - uintptr_t(&shader); + const size_t cmpRegYOffset = cmpRegXOffset + sizeof(bool); + + const u32 condition = getBits<22, 2>(instruction); + const uint refY = getBit<24>(instruction); + const uint refX = getBit<25>(instruction); + + // refX in the bottom byte, refY in the top byte. This is done for condition codes 0 and 1 which check both x and y, so we can emit a single + // instruction that checks both + const u16 refX_refY_merged = refX | (refY << 8); + + switch (condition) { + case 0: // Either cmp register matches + LDRB(W0, statePointer, cmpRegXOffset); + LDRB(W1, statePointer, cmpRegYOffset); + + // Check if x matches refX + CMP(W0, refX); + CSET(W0, EQ); + + // Check if y matches refY + CMP(W1, refY); + CSET(W1, EQ); + + // Set Z to 1 if at least one of them matches + ORR(W0, W0, W1); + CMP(W0, 1); + break; + case 1: // Both cmp registers match + LDRH(W0, statePointer, cmpRegXOffset); + + // If ref fits in 8 bits, use a single CMP, otherwise move into register and then CMP + if (refX_refY_merged <= 0xff) { + CMP(W0, refX_refY_merged); + } else { + MOV(W1, refX_refY_merged); + CMP(W0, W1); + } + break; + case 2: // At least cmp.x matches + LDRB(W0, statePointer, cmpRegXOffset); + CMP(W0, refX); + break; + default: // At least cmp.y matches + LDRB(W0, statePointer, cmpRegYOffset); + CMP(W0, refY); + break; + } +} + +void ShaderEmitter::recCALL(const PICAShader& shader, u32 instruction) { + const u32 num = instruction & 0xff; + const u32 dest = getBits<10, 12>(instruction); + + // Push return PC as stack parameter. This is a decently fast solution and Citra does the same but we should probably switch to a proper PICA-like + // Callstack, because it's not great to have an infinitely expanding call stack + MOV(X0, dest + num); + // Push return PC + current link register so that we'll be able to return later + STP(X0, X30, SP, PRE_INDEXED, -16); + // Call subroutine, Oaknut will update the label if it hasn't been initialized yet + BL(instructionLabels[dest]); + + // Fetch original LR and return. This also restores SP to its original value, discarding the return guard into XZR + LDP(XZR, X30, SP, POST_INDEXED, 16); +} + +void ShaderEmitter::recCALLC(const PICAShader& shader, u32 instruction) { + Label skipCall; + + // z is 1 if the call should be taken, 0 otherwise + checkCmpRegister(shader, instruction); + B(NE, skipCall); + recCALL(shader, instruction); + + l(skipCall); +} + +void ShaderEmitter::recCALLU(const PICAShader& shader, u32 instruction) { + Label skipCall; + + // z is 0 if the call should be taken, 1 otherwise + checkBoolUniform(shader, instruction); + B(EQ, skipCall); + recCALL(shader, instruction); + + l(skipCall); +} + +void ShaderEmitter::recIFC(const PICAShader& shader, u32 instruction) { + // z is 1 if true, else 0 + checkCmpRegister(shader, instruction); + const u32 num = instruction & 0xff; + const u32 dest = getBits<10, 12>(instruction); + + if (dest < recompilerPC) { + Helpers::warn("Shader JIT: IFC instruction with dest < current PC\n"); + } + Label elseBlock, endIf; + + // Jump to else block if z is 0 + B(NE, elseBlock); + compileUntil(shader, dest); + + if (num == 0) { // Else block is empty, + l(elseBlock); + } else { // Else block is NOT empty + B(endIf); // Skip executing the else branch if the if branch was ran + l(elseBlock); + compileUntil(shader, dest + num); + l(endIf); + } +} + +void ShaderEmitter::recIFU(const PICAShader& shader, u32 instruction) { + // z is 0 if true, else 1 + checkBoolUniform(shader, instruction); + const u32 num = instruction & 0xff; + const u32 dest = getBits<10, 12>(instruction); + + if (dest < recompilerPC) { + Helpers::warn("Shader JIT: IFC instruction with dest < current PC\n"); + } + Label elseBlock, endIf; + + // Jump to else block if z is 1 + B(EQ, elseBlock); + compileUntil(shader, dest); + + if (num == 0) { // Else block is empty, + l(elseBlock); + } else { // Else block is NOT empty + B(endIf); // Skip executing the else branch if the if branch was ran + l(elseBlock); + compileUntil(shader, dest + num); + l(endIf); + } +} + +void ShaderEmitter::recJMPC(const PICAShader& shader, u32 instruction) { + const u32 dest = getBits<10, 12>(instruction); + + Label& l = instructionLabels[dest]; + // Z is 1 if the comparison is true + checkCmpRegister(shader, instruction); + B(EQ, l); +} + +void ShaderEmitter::recJMPU(const PICAShader& shader, u32 instruction) { + bool jumpIfFalse = instruction & 1; // If the LSB is 0 we want to compare to true, otherwise compare to false + const u32 dest = getBits<10, 12>(instruction); + + Label& l = instructionLabels[dest]; + // Z is 0 if the uniform is true + checkBoolUniform(shader, instruction); + + if (jumpIfFalse) { + B(EQ, l); + } else { + B(NE, l); + } +} + +void ShaderEmitter::recLOOP(const PICAShader& shader, u32 instruction) { + const u32 dest = getBits<10, 12>(instruction); + const u32 uniformIndex = getBits<22, 2>(instruction); + + if (loopLevel > 0) { + log("[Shader JIT] Detected nested loop. Might be broken?\n"); + } + + if (dest < recompilerPC) { + Helpers::panic("[Shader JIT] Detected backwards loop\n"); + } + + loopLevel++; + + // Offset of the uniform + const auto& uniform = shader.intUniforms[uniformIndex]; + const uintptr_t uniformOffset = uintptr_t(&uniform[0]) - uintptr_t(&shader); + // Offset of the loop register + const uintptr_t loopRegOffset = uintptr_t(&shader.loopCounter) - uintptr_t(&shader); + + LDRB(W0, statePointer, uniformOffset); // W0 = loop iteration count + LDRB(W1, statePointer, uniformOffset + sizeof(u8)); // W1 = initial loop counter value + LDRB(W2, statePointer, uniformOffset + 2 * sizeof(u8)); // W2 = Loop increment + + ADD(W0, W0, 1); // The iteration count is actually uniform.x + 1 + STR(W1, statePointer, loopRegOffset); // Set loop counter + + // Push loop iteration counter & loop increment + // TODO: This might break if an instruction in a loop decides to yield... + STP(X0, X2, SP, PRE_INDEXED, -16); + + Label loopStart, loopEnd; + l(loopStart); + compileUntil(shader, dest + 1); + + const size_t stackOffsetOfLoopIncrement = 0; + const size_t stackOffsetOfIterationCounter = stackOffsetOfLoopIncrement + 8; + + LDP(X0, X2, SP); // W0 = loop iteration, W2 = loop increment + LDR(W1, statePointer, loopRegOffset); // W1 = loop register + + // Increment loop counter + ADD(W1, W1, W2); + STR(W1, statePointer, loopRegOffset); + // Subtract 1 from loop iteration counter, + SUBS(W0, W0, 1); + B(EQ, loopEnd); + + // Loop hasn't ended: Write back new iteration counter and go back to the start + STR(X0, SP); + B(loopStart); + + l(loopEnd); + // Remove the stuff we pushed on the stack earlier + ADD(SP, SP, 16); + loopLevel--; +} + +void ShaderEmitter::recEND(const PICAShader& shader, u32 instruction) { + // Fetch original LR and return. This also restores SP to its original value, discarding the return guard into XZR + LDP(XZR, X30, SP, POST_INDEXED, 16); + RET(); +} + +#endif diff --git a/src/core/PICA/dynapica/shader_rec_emitter_x64.cpp b/src/core/PICA/dynapica/shader_rec_emitter_x64.cpp index 046c7813..c134b72f 100644 --- a/src/core/PICA/dynapica/shader_rec_emitter_x64.cpp +++ b/src/core/PICA/dynapica/shader_rec_emitter_x64.cpp @@ -235,6 +235,8 @@ void ShaderEmitter::loadRegister(Xmm dest, const PICAShader& shader, u32 src, u3 compSwizzle = getBits<23, 8>(operandDescriptor); } + // TODO: Do indexes get applied if src < 0x20? + // PICA has the swizzle descriptor inverted in comparison to x86. For the PICA, the descriptor is (lowest to highest bits) wzyx while it's xyzw for x86 u32 convertedSwizzle = ((compSwizzle >> 6) & 0b11) | (((compSwizzle >> 4) & 0b11) << 2) | (((compSwizzle >> 2) & 0b11) << 4) | ((compSwizzle & 0b11) << 6); @@ -342,10 +344,10 @@ void ShaderEmitter::storeRegister(Xmm source, const PICAShader& shader, u32 dest } else if (std::popcount(writeMask) == 1) { // Only 1 register needs to be written back. This can be done with a simple shift right + movss int bit = std::countr_zero(writeMask); // Get which PICA register needs to be written to (0 = w, 1 = z, etc) size_t index = 3 - bit; - const uintptr_t lane_offset = offset + index * sizeof(float); + const uintptr_t laneOffset = offset + index * sizeof(float); if (index == 0) { // Bottom lane, no need to shift - movss(dword[statePointer + lane_offset], source); + movss(dword[statePointer + laneOffset], source); } else { // Shift right by 32 * index, then write bottom lane if (haveAVX) { vpsrldq(scratch1, source, index * sizeof(float)); @@ -353,7 +355,7 @@ void ShaderEmitter::storeRegister(Xmm source, const PICAShader& shader, u32 dest movaps(scratch1, source); psrldq(scratch1, index * sizeof(float)); } - movss(dword[statePointer + lane_offset], scratch1); + movss(dword[statePointer + laneOffset], scratch1); } } else if (haveSSE4_1) { // Bit reverse the write mask because that is what blendps expects @@ -403,11 +405,18 @@ void ShaderEmitter::checkCmpRegister(const PICAShader& shader, u32 instruction) switch (condition) { case 0: // Either cmp register matches // Z flag is 0 if at least 1 of them is set - test(word[statePointer + cmpRegXOffset], refX_refY_merged); - // Invert z flag - setz(al); - test(al, al); + // Check if X matches + cmp(byte[statePointer + cmpRegXOffset], refX); + sete(al); + + // Or if Y matches + cmp(byte[statePointer + cmpRegYOffset], refY); + sete(cl); + or_(al, cl); + + // If either of them matches, set Z to 1, else set it to 0 + xor_(al, 1); break; case 1: // Both cmp registers match cmp(word[statePointer + cmpRegXOffset], refX_refY_merged); @@ -838,7 +847,7 @@ void ShaderEmitter::recCALL(const PICAShader& shader, u32 instruction) { const u32 dest = getBits<10, 12>(instruction); // Push return PC as stack parameter. This is a decently fast solution and Citra does the same but we should probably switch to a proper PICA-like - // Callstack, because it's not great to have an infinitely expanding call stack where popping from empty stack is undefined as hell + // Callstack, because it's not great to have an infinitely expanding call stack where popping from empty stack is undefined push(qword, dest + num); // Call subroutine, Xbyak will update the label if it hasn't been initialized yet call(instructionLabels[dest]); diff --git a/src/core/action_replay.cpp b/src/core/action_replay.cpp index e8467425..1ef494a2 100644 --- a/src/core/action_replay.cpp +++ b/src/core/action_replay.cpp @@ -139,6 +139,65 @@ void ActionReplay::executeDType(const Cheat& cheat, u32 instruction) { switch (instruction) { case 0xD3000000: offset1 = cheat[pc++]; break; case 0xD3000001: offset2 = cheat[pc++]; break; + + case 0xD6000000: + write32(*activeOffset + cheat[pc++], u32(*activeData)); + *activeOffset += 4; + break; + + case 0xD6000001: + write32(*activeOffset + cheat[pc++], u32(data1)); + *activeOffset += 4; + break; + + case 0xD6000002: + write32(*activeOffset + cheat[pc++], u32(data2)); + *activeOffset += 4; + break; + + case 0xD7000000: + write16(*activeOffset + cheat[pc++], u16(*activeData)); + *activeOffset += 2; + break; + + case 0xD7000001: + write16(*activeOffset + cheat[pc++], u16(data1)); + *activeOffset += 2; + break; + + case 0xD7000002: + write16(*activeOffset + cheat[pc++], u16(data2)); + *activeOffset += 2; + break; + + case 0xD8000000: + write8(*activeOffset + cheat[pc++], u8(*activeData)); + *activeOffset += 1; + break; + + case 0xD8000001: + write8(*activeOffset + cheat[pc++], u8(data1)); + *activeOffset += 1; + break; + + case 0xD8000002: + write8(*activeOffset + cheat[pc++], u8(data2)); + *activeOffset += 1; + break; + + + case 0xD9000000: *activeData = read32(cheat[pc++] + *activeOffset); break; + case 0xD9000001: data1 = read32(cheat[pc++] + *activeOffset); break; + case 0xD9000002: data2 = read32(cheat[pc++] + *activeOffset); break; + + case 0xDA000000: *activeData = read16(cheat[pc++] + *activeOffset); break; + case 0xDA000001: data1 = read16(cheat[pc++] + *activeOffset); break; + case 0xDA000002: data2 = read16(cheat[pc++] + *activeOffset); break; + + case 0xDB000000: *activeData = read8(cheat[pc++] + *activeOffset); break; + case 0xDB000001: data1 = read8(cheat[pc++] + *activeOffset); break; + case 0xDB000002: data2 = read8(cheat[pc++] + *activeOffset); break; + case 0xDC000000: *activeOffset += cheat[pc++]; break; // DD000000 XXXXXXXX - if KEYPAD has value XXXXXXXX execute next block diff --git a/src/core/applets/applet_manager.cpp b/src/core/applets/applet_manager.cpp index c94eee28..cdb19319 100644 --- a/src/core/applets/applet_manager.cpp +++ b/src/core/applets/applet_manager.cpp @@ -1,11 +1,17 @@ #include "applets/applet_manager.hpp" + +#include "services/apt.hpp" + using namespace Applets; -AppletManager::AppletManager(Memory& mem) : miiSelector(mem), swkbd(mem) {} +AppletManager::AppletManager(Memory& mem) : miiSelector(mem, nextParameter), swkbd(mem, nextParameter), error(mem, nextParameter) {} void AppletManager::reset() { + nextParameter = std::nullopt; + miiSelector.reset(); swkbd.reset(); + error.reset(); } AppletBase* AppletManager::getApplet(u32 id) { @@ -16,6 +22,40 @@ AppletBase* AppletManager::getApplet(u32 id) { case AppletIDs::SoftwareKeyboard: case AppletIDs::SoftwareKeyboard2: return &swkbd; + case AppletIDs::ErrDisp: + case AppletIDs::ErrDisp2: return &error; + default: return nullptr; } +} + +Applets::Parameter AppletManager::glanceParameter() { + if (nextParameter) { + // Copy parameter + Applets::Parameter param = nextParameter.value(); + // APT module clears next parameter even for GlanceParameter for these 2 signals + if (param.signal == static_cast(APTSignal::DspWakeup) || param.signal == static_cast(APTSignal::DspSleep)) { + nextParameter = std::nullopt; + } + + return param; + } + + // Default return value. This is legacy code from before applets were implemented. TODO: Update it + else { + return Applets::Parameter{ + .senderID = 0, + .destID = Applets::AppletIDs::Application, + .signal = static_cast(APTSignal::Wakeup), + .data = {}, + }; + } +} + +Applets::Parameter AppletManager::receiveParameter() { + Applets::Parameter param = glanceParameter(); + // ReceiveParameter always clears nextParameter whereas glanceParameter does not + nextParameter = std::nullopt; + + return param; } \ No newline at end of file diff --git a/src/core/applets/error_applet.cpp b/src/core/applets/error_applet.cpp new file mode 100644 index 00000000..5acbcbba --- /dev/null +++ b/src/core/applets/error_applet.cpp @@ -0,0 +1,32 @@ +#include "applets/error_applet.hpp" +#include "kernel/handles.hpp" + +using namespace Applets; + +void ErrorApplet::reset() {} + +Result::HorizonResult ErrorApplet::start(const MemoryBlock* sharedMem, const std::vector& parameters, u32 appID) { + Applets::Parameter param = Applets::Parameter{ + .senderID = appID, + .destID = AppletIDs::Application, + .signal = static_cast(APTSignal::WakeupByExit), + .object = 0, + .data = parameters, // TODO: Figure out how the data format for this applet + }; + + nextParameter = param; + return Result::Success; +} + +Result::HorizonResult ErrorApplet::receiveParameter(const Applets::Parameter& parameter) { + Applets::Parameter param = Applets::Parameter{ + .senderID = parameter.destID, + .destID = AppletIDs::Application, + .signal = static_cast(APTSignal::Response), + .object = KernelHandles::APTCaptureSharedMemHandle, + .data = {}, + }; + + nextParameter = param; + return Result::Success; +} \ No newline at end of file diff --git a/src/core/applets/mii_selector.cpp b/src/core/applets/mii_selector.cpp index 211e6f07..ab6455fc 100644 --- a/src/core/applets/mii_selector.cpp +++ b/src/core/applets/mii_selector.cpp @@ -1,11 +1,86 @@ #include "applets/mii_selector.hpp" +#include +#include + +#include "kernel/handles.hpp" + using namespace Applets; void MiiSelectorApplet::reset() {} -Result::HorizonResult MiiSelectorApplet::start() { return Result::Success; } +Result::HorizonResult MiiSelectorApplet::start(const MemoryBlock* sharedMem, const std::vector& parameters, u32 appID) { + // Get mii configuration from the application + std::memcpy(&config, ¶meters[0], sizeof(config)); -Result::HorizonResult MiiSelectorApplet::receiveParameter() { - Helpers::warn("Mii Selector: Unimplemented ReceiveParameter"); + Applets::Parameter param = Applets::Parameter{ + .senderID = appID, + .destID = AppletIDs::Application, + .signal = static_cast(APTSignal::WakeupByExit), + .object = 0, + }; + + // Thanks to Citra devs as always for the default mii data and other applet help + output = getDefaultMii(); + output.returnCode = 0; // Success + output.selectedGuestMiiIndex = std::numeric_limits::max(); + output.miiChecksum = boost::crc<16, 0x1021, 0, 0, false, false>(&output.selectedMiiData, sizeof(MiiData) + sizeof(output.unknown1)); + + // Copy output into the response parameter + param.data.resize(sizeof(output)); + std::memcpy(¶m.data[0], &output, sizeof(output)); + + nextParameter = param; return Result::Success; -} \ No newline at end of file +} + +Result::HorizonResult MiiSelectorApplet::receiveParameter(const Applets::Parameter& parameter) { + Applets::Parameter param = Applets::Parameter{ + .senderID = parameter.destID, + .destID = AppletIDs::Application, + .signal = static_cast(APTSignal::Response), + .object = KernelHandles::APTCaptureSharedMemHandle, + .data = {}, + }; + + nextParameter = param; + return Result::Success; +} + +MiiResult MiiSelectorApplet::getDefaultMii() { + // This data was obtained from Citra + MiiData miiData; + miiData.version = 0x03; + miiData.miiOptions = 0x00; + miiData.miiPos = 0x10; + miiData.consoleID = 0x30; + miiData.systemID = 0xD285B6B300C8850A; + miiData.miiID = 0x98391EE4; + miiData.creatorMAC = {0x40, 0xF4, 0x07, 0xB7, 0x37, 0x10}; + miiData.padding = 0x0000; + miiData.miiDetails = 0xA600; + miiData.miiName = {'P', 'a', 'n', 'd', 'a', '3', 'D', 'S', 0x0, 0x0}; + miiData.height = 0x40; + miiData.width = 0x40; + miiData.faceStyle = 0x00; + miiData.faceDetails = 0x00; + miiData.hairStyle = 0x21; + miiData.hairDetails = 0x01; + miiData.eyeDetails = 0x02684418; + miiData.eyebrowDetails = 0x26344614; + miiData.noseDetails = 0x8112; + miiData.mouthDetails = 0x1768; + miiData.moustacheDetails = 0x0D00; + miiData.beardDetails = 0x0029; + miiData.glassesDetails = 0x0052; + miiData.moleDetails = 0x4850; + miiData.authorName = {u'B', u'O', u'N', u'K', u'E', u'R'}; + + MiiResult result; + result.returnCode = 0x0; + result.isGuestMiiSelected = 0x0; + result.selectedGuestMiiIndex = std::numeric_limits::max(); + result.selectedMiiData = miiData; + result.guestMiiName.fill(0x0); + + return result; +} diff --git a/src/core/applets/software_keyboard.cpp b/src/core/applets/software_keyboard.cpp index 2ff22792..4a91b790 100644 --- a/src/core/applets/software_keyboard.cpp +++ b/src/core/applets/software_keyboard.cpp @@ -1,11 +1,93 @@ #include "applets/software_keyboard.hpp" +#include +#include + +#include "kernel/handles.hpp" + using namespace Applets; void SoftwareKeyboardApplet::reset() {} -Result::HorizonResult SoftwareKeyboardApplet::start() { return Result::Success; } -Result::HorizonResult SoftwareKeyboardApplet::receiveParameter() { - Helpers::warn("Software keyboard: Unimplemented ReceiveParameter"); +Result::HorizonResult SoftwareKeyboardApplet::receiveParameter(const Applets::Parameter& parameter) { + switch (parameter.signal) { + // Signal == request -> Applet is asking swkbd for a shared memory handle for backing up the framebuffer before opening the applet + case u32(APTSignal::Request): { + Applets::Parameter param = Applets::Parameter{ + .senderID = parameter.destID, + .destID = AppletIDs::Application, + .signal = static_cast(APTSignal::Response), + .object = KernelHandles::APTCaptureSharedMemHandle, + .data = {}, + }; + + nextParameter = param; + break; + } + + default: Helpers::panic("Unimplemented swkbd signal %d\n", parameter.signal); + } + return Result::Success; +} + +Result::HorizonResult SoftwareKeyboardApplet::start(const MemoryBlock* sharedMem, const std::vector& parameters, u32 appID) { + if (parameters.size() < sizeof(SoftwareKeyboardConfig)) { + Helpers::warn("SoftwareKeyboard::Start: Invalid size for keyboard configuration"); + return Result::Success; + } + + if (sharedMem == nullptr) { + Helpers::warn("SoftwareKeyboard: Missing shared memory"); + return Result::Success; + } + + // Get keyboard configuration from the application + std::memcpy(&config, ¶meters[0], sizeof(config)); + + const std::u16string text = u"Pand"; + u32 textAddress = sharedMem->addr; + + // Copy text to shared memory the app gave us + for (u32 i = 0; i < text.size(); i++) { + mem.write16(textAddress, u16(text[i])); + textAddress += sizeof(u16); + } + mem.write16(textAddress, 0); // Write UTF-16 null terminator + + // Temporarily hardcode the pressed button to be the firs tone + switch (config.numButtonsM1) { + case SoftwareKeyboardButtonConfig::SingleButton: config.returnCode = SoftwareKeyboardResult::D0Click; break; + case SoftwareKeyboardButtonConfig::DualButton: config.returnCode = SoftwareKeyboardResult::D1Click1; break; + case SoftwareKeyboardButtonConfig::TripleButton: config.returnCode = SoftwareKeyboardResult::D2Click2; break; + case SoftwareKeyboardButtonConfig::NoButton: config.returnCode = SoftwareKeyboardResult::None; break; + default: Helpers::warn("Software keyboard: Invalid button mode specification"); break; + } + + config.textOffset = 0; + config.textLength = static_cast(text.size()); + static_assert(offsetof(SoftwareKeyboardConfig, textOffset) == 324); + static_assert(offsetof(SoftwareKeyboardConfig, textLength) == 328); + + if (config.filterFlags & SoftwareKeyboardFilter::Callback) { + Helpers::warn("Unimplemented software keyboard profanity callback"); + } + + closeKeyboard(appID); + return Result::Success; +} + +void SoftwareKeyboardApplet::closeKeyboard(u32 appID) { + Applets::Parameter param = Applets::Parameter{ + .senderID = appID, + .destID = AppletIDs::Application, + .signal = static_cast(APTSignal::WakeupByExit), + .object = 0, + }; + + // Copy software keyboard configuration into the response parameter + param.data.resize(sizeof(config)); + std::memcpy(¶m.data[0], &config, sizeof(config)); + + nextParameter = param; } \ No newline at end of file diff --git a/src/core/cheats.cpp b/src/core/cheats.cpp index 618460c5..7b8b71c2 100644 --- a/src/core/cheats.cpp +++ b/src/core/cheats.cpp @@ -1,4 +1,5 @@ #include "cheats.hpp" +#include "swap.hpp" Cheats::Cheats(Memory& mem, HIDService& hid) : ar(mem, hid) { reset(); } @@ -7,9 +8,70 @@ void Cheats::reset() { ar.reset(); // Reset ActionReplay } -void Cheats::addCheat(const Cheat& cheat) { - cheats.push_back(cheat); +u32 Cheats::addCheat(const Cheat& cheat) { cheatsLoaded = true; + + // Find an empty slot if a cheat was previously removed + for (size_t i = 0; i < cheats.size(); i++) { + if (cheats[i].type == CheatType::None) { + cheats[i] = cheat; + return i; + } + } + + // Otherwise, just add a new slot + cheats.push_back(cheat); + return cheats.size() - 1; +} + +u32 Cheats::addCheat(const u8* data, size_t size) { + if ((size % 8) != 0) { + return badCheatHandle; + } + + Cheats::Cheat cheat; + cheat.enabled = true; + cheat.type = Cheats::CheatType::ActionReplay; + + for (size_t i = 0; i < size; i += 8) { + auto read32 = [](const u8* ptr) { return (u32(ptr[3]) << 24) | (u32(ptr[2]) << 16) | (u32(ptr[1]) << 8) | u32(ptr[0]); }; + + // Data is passed to us in big endian so we bswap + u32 firstWord = Common::swap32(read32(data + i)); + u32 secondWord = Common::swap32(read32(data + i + 4)); + cheat.instructions.insert(cheat.instructions.end(), {firstWord, secondWord}); + } + + return addCheat(cheat); +} + +void Cheats::removeCheat(u32 id) { + if (id >= cheats.size()) { + return; + } + + // Not using std::erase because we don't want to invalidate cheat IDs + cheats[id].type = CheatType::None; + cheats[id].instructions.clear(); + + // Check if no cheats are loaded + for (const auto& cheat : cheats) { + if (cheat.type != CheatType::None) return; + } + + cheatsLoaded = false; +} + +void Cheats::enableCheat(u32 id) { + if (id < cheats.size()) { + cheats[id].enabled = true; + } +} + +void Cheats::disableCheat(u32 id) { + if (id < cheats.size()) { + cheats[id].enabled = false; + } } void Cheats::clear() { @@ -19,12 +81,15 @@ void Cheats::clear() { void Cheats::run() { for (const Cheat& cheat : cheats) { + if (!cheat.enabled) continue; + switch (cheat.type) { case CheatType::ActionReplay: { ar.runCheat(cheat.instructions); break; } + case CheatType::None: break; default: Helpers::panic("Unknown cheat device!"); } } diff --git a/src/core/fs/archive_ncch.cpp b/src/core/fs/archive_ncch.cpp index 5935a0c5..d5a4bab5 100644 --- a/src/core/fs/archive_ncch.cpp +++ b/src/core/fs/archive_ncch.cpp @@ -142,7 +142,7 @@ std::optional NCCHArchive::readFile(FileSession* file, u64 offset, u32 size case PathType::RomFS: { const u64 romFSSize = cxi->romFS.size; const u64 romFSOffset = cxi->romFS.offset; - if ((offset >> 32) || (offset >= romFSSize) || (offset + size >= romFSSize)) { + if ((offset >> 32) || (offset >= romFSSize) || (offset + size > romFSSize)) { Helpers::panic("Tried to read from NCCH with too big of an offset"); } @@ -166,4 +166,4 @@ std::optional NCCHArchive::readFile(FileSession* file, u64 offset, u32 size } return u32(bytesRead); -} \ No newline at end of file +} diff --git a/src/core/fs/archive_sdmc.cpp b/src/core/fs/archive_sdmc.cpp index 232335d0..6c34de7a 100644 --- a/src/core/fs/archive_sdmc.cpp +++ b/src/core/fs/archive_sdmc.cpp @@ -45,8 +45,16 @@ HorizonResult SDMCArchive::deleteFile(const FSPath& path) { FileDescriptor SDMCArchive::openFile(const FSPath& path, const FilePerms& perms) { FilePerms realPerms = perms; - // SD card always has read permission - realPerms.raw |= (1 << 0); + + if (isWriteOnly) { + if (perms.read()) { + Helpers::warn("SDMC: Read flag is not allowed in SDMC Write-Only archive"); + return FileError; + } + } else { + // Regular SDMC archive always has read permission + realPerms.raw |= (1 << 0); + } if ((realPerms.create() && !realPerms.write())) { Helpers::panic("[SDMC] Unsupported flags for OpenFile"); @@ -130,6 +138,11 @@ HorizonResult SDMCArchive::createDirectory(const FSPath& path) { } Rust::Result SDMCArchive::openDirectory(const FSPath& path) { + if (isWriteOnly) { + Helpers::warn("SDMC: OpenDirectory is not allowed in SDMC Write-Only archive"); + return Err(Result::FS::UnexpectedFileOrDir); + } + if (path.type == PathType::UTF16) { if (!isPathSafe(path)) { Helpers::panic("Unsafe path in SaveData::OpenDirectory"); diff --git a/src/core/fs/archive_self_ncch.cpp b/src/core/fs/archive_self_ncch.cpp index 9c911769..9369152d 100644 --- a/src/core/fs/archive_self_ncch.cpp +++ b/src/core/fs/archive_self_ncch.cpp @@ -83,7 +83,7 @@ std::optional SelfNCCHArchive::readFile(FileSession* file, u64 offset, u32 case PathType::RomFS: { const u64 romFSSize = cxi->romFS.size; const u64 romFSOffset = cxi->romFS.offset; - if ((offset >> 32) || (offset >= romFSSize) || (offset + size >= romFSSize)) { + if ((offset >> 32) || (offset >= romFSSize) || (offset + size > romFSSize)) { Helpers::panic("Tried to read from SelfNCCH with too big of an offset"); } @@ -95,7 +95,7 @@ std::optional SelfNCCHArchive::readFile(FileSession* file, u64 offset, u32 case PathType::ExeFS: { const u64 exeFSSize = cxi->exeFS.size; const u64 exeFSOffset = cxi->exeFS.offset; - if ((offset >> 32) || (offset >= exeFSSize) || (offset + size >= exeFSSize)) { + if ((offset >> 32) || (offset >= exeFSSize) || (offset + size > exeFSSize)) { Helpers::panic("Tried to read from SelfNCCH with too big of an offset"); } @@ -110,7 +110,7 @@ std::optional SelfNCCHArchive::readFile(FileSession* file, u64 offset, u32 const u64 romFSSize = cxi->romFS.size; const u64 romFSOffset = cxi->romFS.offset; - if ((offset >> 32) || (offset >= romFSSize) || (offset + size >= romFSSize)) { + if ((offset >> 32) || (offset >= romFSSize) || (offset + size > romFSSize)) { Helpers::panic("Tried to read from SelfNCCH with too big of an offset"); } @@ -129,7 +129,7 @@ std::optional SelfNCCHArchive::readFile(FileSession* file, u64 offset, u32 switch (type) { case PathType::RomFS: { const u64 romFSSize = hb3dsx->romFSSize; - if ((offset >> 32) || (offset >= romFSSize) || (offset + size >= romFSSize)) { + if ((offset >> 32) || (offset >= romFSSize) || (offset + size > romFSSize)) { Helpers::panic("Tried to read from SelfNCCH with too big of an offset"); } break; @@ -150,4 +150,4 @@ std::optional SelfNCCHArchive::readFile(FileSession* file, u64 offset, u32 } return u32(bytesRead); -} \ No newline at end of file +} diff --git a/src/core/kernel/events.cpp b/src/core/kernel/events.cpp index 06841720..b2f89fbf 100644 --- a/src/core/kernel/events.cpp +++ b/src/core/kernel/events.cpp @@ -96,7 +96,7 @@ void Kernel::svcSignalEvent() { // Result WaitSynchronization1(Handle handle, s64 timeout_nanoseconds) void Kernel::waitSynchronization1() { const Handle handle = regs[0]; - const s64 ns = s64(u64(regs[1]) | (u64(regs[2]) << 32)); + const s64 ns = s64(u64(regs[2]) | (u64(regs[3]) << 32)); logSVC("WaitSynchronization1(handle = %X, ns = %lld)\n", handle, ns); const auto object = getObject(handle); @@ -126,8 +126,7 @@ void Kernel::waitSynchronization1() { auto& t = threads[currentThreadIndex]; t.waitList.resize(1); t.status = ThreadStatus::WaitSync1; - t.sleepTick = cpu.getTicks(); - t.waitingNanoseconds = ns; + t.wakeupTick = getWakeupTick(ns); t.waitList[0] = handle; // Add the current thread to the object's wait list @@ -220,8 +219,7 @@ void Kernel::waitSynchronizationN() { t.waitList.resize(handleCount); t.status = ThreadStatus::WaitSyncAny; t.outPointer = outPointer; - t.waitingNanoseconds = ns; - t.sleepTick = cpu.getTicks(); + t.wakeupTick = getWakeupTick(ns); for (s32 i = 0; i < handleCount; i++) { t.waitList[i] = waitObjects[i].first; // Add object to this thread's waitlist diff --git a/src/core/kernel/file_operations.cpp b/src/core/kernel/file_operations.cpp index c7837100..972190fa 100644 --- a/src/core/kernel/file_operations.cpp +++ b/src/core/kernel/file_operations.cpp @@ -148,6 +148,11 @@ void Kernel::writeFile(u32 messagePointer, Handle fileHandle) { IOFile f(file->fd); auto [success, bytesWritten] = f.writeBytes(data.get(), size); + // TODO: Should this check only the byte? + if (writeOption) { + f.flush(); + } + mem.write32(messagePointer, IPC::responseHeader(0x0803, 2, 2)); if (!success) { Helpers::panic("Kernel::WriteFile failed"); diff --git a/src/core/kernel/idle_thread.cpp b/src/core/kernel/idle_thread.cpp index 5abba373..d666968b 100644 --- a/src/core/kernel/idle_thread.cpp +++ b/src/core/kernel/idle_thread.cpp @@ -8,13 +8,6 @@ The code for our idle thread looks like this idle_thread_main: - mov r0, #4096 @ Loop counter - - .loop: - nop; nop; nop; nop @ NOP 4 times to waste some cycles - subs r0, #1 @ Decrement counter by 1, go back to looping if loop counter != 0 - bne .loop - // Sleep for 0 seconds with the SleepThread SVC, which just yields execution mov r0, #0 mov r1, #0 @@ -24,14 +17,10 @@ idle_thread_main: */ static constexpr u8 idleThreadCode[] = { - 0x01, 0x0A, 0xA0, 0xE3, // mov r0, #4096 - 0x00, 0xF0, 0x20, 0xE3, 0x00, 0xF0, 0x20, 0xE3, 0x00, 0xF0, 0x20, 0xE3, 0x00, 0xF0, 0x20, 0xE3, // nop (4 times) - 0x01, 0x00, 0x50, 0xE2, // subs r0, #1 - 0xF9, 0xFF, 0xFF, 0x1A, // bne loop 0x00, 0x00, 0xA0, 0xE3, // mov r0, #0 0x00, 0x10, 0xA0, 0xE3, // mov r1, #0 0x0A, 0x00, 0x00, 0xEF, // svc SleepThread - 0xF4, 0xFF, 0xFF, 0xEA // b idle_thread_main + 0xFB, 0xFF, 0xFF, 0xEA // b idle_thread_main }; // Set up an idle thread to run when no thread is able to run diff --git a/src/core/kernel/kernel.cpp b/src/core/kernel/kernel.cpp index b21c4c58..392b87fd 100644 --- a/src/core/kernel/kernel.cpp +++ b/src/core/kernel/kernel.cpp @@ -50,6 +50,7 @@ void Kernel::serviceSVC(u32 svc) { case 0x1D: svcClearTimer(); break; case 0x1E: createMemoryBlock(); break; case 0x1F: mapMemoryBlock(); break; + case 0x20: unmapMemoryBlock(); break; case 0x21: createAddressArbiter(); break; case 0x22: arbitrateAddress(); break; case 0x23: svcCloseHandle(); break; @@ -66,6 +67,7 @@ void Kernel::serviceSVC(u32 svc) { case 0x38: getResourceLimit(); break; case 0x39: getResourceLimitLimitValues(); break; case 0x3A: getResourceLimitCurrentValues(); break; + case 0x3B: getThreadContext(); break; case 0x3D: outputDebugString(); break; default: Helpers::panic("Unimplemented svc: %X @ %08X", svc, regs[15]); break; } @@ -148,6 +150,7 @@ void Kernel::reset() { } objects.clear(); mutexHandles.clear(); + timerHandles.clear(); portHandles.clear(); threadIndices.clear(); serviceManager.reset(); @@ -178,6 +181,30 @@ u32 Kernel::getTLSPointer() { // Result CloseHandle(Handle handle) void Kernel::svcCloseHandle() { logSVC("CloseHandle(handle = %d) (Unimplemented)\n", regs[0]); + const Handle handle = regs[0]; + + KernelObject* object = getObject(handle); + if (object != nullptr) { + switch (object->type) { + // Close file descriptor when closing a file to prevent leaks and properly flush file contents + case KernelObjectType::File: { + FileSession* file = object->getData(); + if (file->isOpen) { + file->isOpen = false; + + if (file->fd != nullptr) { + fclose(file->fd); + file->fd = nullptr; + } + } + break; + } + + default: break; + } + } + + // Stub to always succeed for now regs[0] = Result::Success; } diff --git a/src/core/kernel/memory_management.cpp b/src/core/kernel/memory_management.cpp index e90a5697..0d234be5 100644 --- a/src/core/kernel/memory_management.cpp +++ b/src/core/kernel/memory_management.cpp @@ -144,6 +144,7 @@ void Kernel::mapMemoryBlock() { printf("Mapping CSND memory block\n"); break; + case KernelHandles::APTCaptureSharedMemHandle: break; default: Helpers::panic("Mapping unknown shared memory block: %X", block); } } else { @@ -206,3 +207,12 @@ void Kernel::createMemoryBlock() { regs[0] = Result::Success; regs[1] = makeMemoryBlock(addr, size, myPermission, otherPermission); } + +void Kernel::unmapMemoryBlock() { + Handle block = regs[0]; + u32 addr = regs[1]; + logSVC("Unmap memory block (block handle = %X, addr = %08X)\n", block, addr); + + Helpers::warn("Stubbed svcUnmapMemoryBlock!"); + regs[0] = Result::Success; +} diff --git a/src/core/kernel/threads.cpp b/src/core/kernel/threads.cpp index 5c21d507..3a6201c1 100644 --- a/src/core/kernel/threads.cpp +++ b/src/core/kernel/threads.cpp @@ -52,14 +52,8 @@ bool Kernel::canThreadRun(const Thread& t) { return true; } else if (t.status == ThreadStatus::WaitSleep || t.status == ThreadStatus::WaitSync1 || t.status == ThreadStatus::WaitSyncAny || t.status == ThreadStatus::WaitSyncAll) { - const u64 elapsedTicks = cpu.getTicks() - t.sleepTick; - - constexpr double ticksPerSec = double(CPU::ticksPerSec); - constexpr double nsPerTick = ticksPerSec / 1000000000.0; - // TODO: Set r0 to the correct error code on timeout for WaitSync{1/Any/All} - const s64 elapsedNs = s64(double(elapsedTicks) * nsPerTick); - return elapsedNs >= t.waitingNanoseconds; + return cpu.getTicks() >= t.wakeupTick; } // Handle timeouts and stuff here @@ -82,6 +76,15 @@ std::optional Kernel::getNextThread() { return std::nullopt; } +u64 Kernel::getWakeupTick(s64 ns) { + // Timeout == -1 means that the thread doesn't plan on waking up automatically + if (ns == -1) { + return std::numeric_limits::max(); + } + + return cpu.getTicks() + Scheduler::nsToCycles(ns); +} + // See if there is a higher priority, ready thread and switch to that void Kernel::rescheduleThreads() { Thread& current = threads[currentThreadIndex]; // Current running thread @@ -368,13 +371,30 @@ void Kernel::sleepThread(s64 ns) { if (index != idleThreadIndex) { switchThread(index); } + } else { + if (currentThreadIndex == idleThreadIndex) { + const Scheduler& scheduler = cpu.getScheduler(); + u64 timestamp = scheduler.nextTimestamp; + + for (auto i : threadIndices) { + const Thread& t = threads[i]; + if (t.status == ThreadStatus::WaitSleep || t.status == ThreadStatus::WaitSync1 || t.status == ThreadStatus::WaitSyncAny || + t.status == ThreadStatus::WaitSyncAll) { + timestamp = std::min(timestamp, t.wakeupTick); + } + } + + if (timestamp > scheduler.currentTimestamp) { + u64 idleCycles = timestamp - scheduler.currentTimestamp; + cpu.addTicks(idleCycles); + } + } } } else { // If we're sleeping for >= 0 ns Thread& t = threads[currentThreadIndex]; t.status = ThreadStatus::WaitSleep; - t.waitingNanoseconds = ns; - t.sleepTick = cpu.getTicks(); + t.wakeupTick = getWakeupTick(ns); requireReschedule(); } @@ -462,6 +482,13 @@ void Kernel::getThreadIdealProcessor() { regs[1] = static_cast(ProcessorID::AppCore); } +void Kernel::getThreadContext() { + Helpers::warn("Stubbed Kernel::GetThreadContext"); + + // TODO: Decompile this from Kernel11. 3DBrew says function is stubbed. + regs[0] = Result::Success; +} + void Kernel::setThreadPriority() { const Handle handle = regs[0]; const u32 priority = regs[1]; diff --git a/src/core/kernel/timers.cpp b/src/core/kernel/timers.cpp index 1c18d9de..35fc57a4 100644 --- a/src/core/kernel/timers.cpp +++ b/src/core/kernel/timers.cpp @@ -1,5 +1,8 @@ -#include "kernel.hpp" +#include + #include "cpu.hpp" +#include "kernel.hpp" +#include "scheduler.hpp" Handle Kernel::makeTimer(ResetType type) { Handle ret = makeObject(KernelObjectType::Timer); @@ -9,31 +12,48 @@ Handle Kernel::makeTimer(ResetType type) { Helpers::panic("Created pulse timer"); } - // timerHandles.push_back(ret); + timerHandles.push_back(ret); return ret; } -void Kernel::updateTimer(Handle handle, Timer* timer) { - if (timer->running) { - const u64 currentTicks = cpu.getTicks(); - u64 elapsedTicks = currentTicks - timer->startTick; +void Kernel::pollTimers() { + u64 currentTick = cpu.getTicks(); - constexpr double ticksPerSec = double(CPU::ticksPerSec); - constexpr double nsPerTick = ticksPerSec / 1000000000.0; - const s64 elapsedNs = s64(double(elapsedTicks) * nsPerTick); + // Find the next timestamp we'll poll KTimers on. To do this, we find the minimum tick one of our timers will fire + u64 nextTimestamp = std::numeric_limits::max(); + // Do we have any active timers anymore? If not, then we won't need to schedule a new timer poll event + bool haveActiveTimers = false; - // Timer has fired - if (elapsedNs >= timer->currentDelay) { - timer->startTick = currentTicks; - timer->currentDelay = timer->interval; - signalTimer(handle, timer); + for (auto handle : timerHandles) { + KernelObject* object = getObject(handle, KernelObjectType::Timer); + if (object != nullptr) { + Timer* timer = object->getData(); + + if (timer->running) { + // If timer has fired, signal it and set the tick it will next time + if (currentTick >= timer->fireTick) { + signalTimer(handle, timer); + } + + // Update our next timer fire timestamp and mark that we should schedule a new event to poll timers + // We recheck timer->running because signalling a timer stops it if interval == 0 + if (timer->running) { + nextTimestamp = std::min(nextTimestamp, timer->fireTick); + haveActiveTimers = true; + } + } } } + + // If we still have active timers, schedule next poll event + if (haveActiveTimers) { + Scheduler& scheduler = cpu.getScheduler(); + scheduler.addEvent(Scheduler::EventType::UpdateTimers, nextTimestamp); + } } void Kernel::cancelTimer(Timer* timer) { timer->running = false; - // TODO: When we have a scheduler this should properly cancel timer events in the scheduler } void Kernel::signalTimer(Handle timerHandle, Timer* timer) { @@ -51,6 +71,12 @@ void Kernel::signalTimer(Handle timerHandle, Timer* timer) { case ResetType::Pulse: Helpers::panic("Signalled pulsing timer"); break; } } + + if (timer->interval == 0) { + cancelTimer(timer); + } else { + timer->fireTick = cpu.getTicks() + Scheduler::nsToCycles(timer->interval); + } } void Kernel::svcCreateTimer() { @@ -70,8 +96,8 @@ void Kernel::svcCreateTimer() { void Kernel::svcSetTimer() { Handle handle = regs[0]; // TODO: Is this actually s64 or u64? 3DBrew says s64, but u64 makes more sense - const s64 initial = s64(u64(regs[1]) | (u64(regs[2]) << 32)); - const s64 interval = s64(u64(regs[3]) | (u64(regs[4]) << 32)); + const s64 initial = s64(u64(regs[2]) | (u64(regs[3]) << 32)); + const s64 interval = s64(u64(regs[1]) | (u64(regs[4]) << 32)); logSVC("SetTimer (handle = %X, initial delay = %llX, interval delay = %llX)\n", handle, initial, interval); KernelObject* object = getObject(handle, KernelObjectType::Timer); @@ -83,18 +109,20 @@ void Kernel::svcSetTimer() { Timer* timer = object->getData(); cancelTimer(timer); - timer->currentDelay = initial; timer->interval = interval; timer->running = true; - timer->startTick = cpu.getTicks(); + timer->fireTick = cpu.getTicks() + Scheduler::nsToCycles(initial); + + Scheduler& scheduler = cpu.getScheduler(); + // Signal an event to poll timers as soon as possible + scheduler.removeEvent(Scheduler::EventType::UpdateTimers); + scheduler.addEvent(Scheduler::EventType::UpdateTimers, cpu.getTicks() + 1); // If the initial delay is 0 then instantly signal the timer if (initial == 0) { signalTimer(handle, timer); - } else { - // This should schedule an event in the scheduler when we have one } - + regs[0] = Result::Success; } diff --git a/src/core/loader/ncch.cpp b/src/core/loader/ncch.cpp index 2546aa01..1f534fa0 100644 --- a/src/core/loader/ncch.cpp +++ b/src/core/loader/ncch.cpp @@ -26,6 +26,7 @@ bool NCCH::loadFromHeader(Crypto::AESEngine &aesEngine, IOFile& file, const FSIn codeFile.clear(); saveData.clear(); + smdh.clear(); partitionInfo = info; size = u64(*(u32*)&header[0x104]) * mediaUnit; // TODO: Maybe don't type pun because big endian will break @@ -219,11 +220,10 @@ bool NCCH::loadFromHeader(Crypto::AESEngine &aesEngine, IOFile& file, const FSIn } } else if (std::strcmp(name, "icon") == 0) { // Parse icon file to extract region info and more in the future (logo, etc) - std::vector tmp; - tmp.resize(fileSize); - readFromFile(file, exeFS, tmp.data(), fileOffset + exeFSHeaderSize, fileSize); + smdh.resize(fileSize); + readFromFile(file, exeFS, smdh.data(), fileOffset + exeFSHeaderSize, fileSize); - if (!parseSMDH(tmp)) { + if (!parseSMDH(smdh)) { printf("Failed to parse SMDH!\n"); } } diff --git a/src/core/services/ac.cpp b/src/core/services/ac.cpp index 3feddebe..8f5545fe 100644 --- a/src/core/services/ac.cpp +++ b/src/core/services/ac.cpp @@ -8,6 +8,7 @@ namespace ACCommands { CloseAsync = 0x00080004, GetLastErrorCode = 0x000A0000, GetStatus = 0x000C0000, + GetWifiStatus = 0x000D0000, GetConnectingInfraPriority = 0x000F0000, RegisterDisconnectEvent = 0x00300004, IsConnected = 0x003E0042, @@ -29,6 +30,7 @@ void ACService::handleSyncRequest(u32 messagePointer) { case ACCommands::GetConnectingInfraPriority: getConnectingInfraPriority(messagePointer); break; case ACCommands::GetLastErrorCode: getLastErrorCode(messagePointer); break; case ACCommands::GetStatus: getStatus(messagePointer); break; + case ACCommands::GetWifiStatus: getWifiStatus(messagePointer); break; case ACCommands::IsConnected: isConnected(messagePointer); break; case ACCommands::RegisterDisconnectEvent: registerDisconnectEvent(messagePointer); break; case ACCommands::SetClientVersion: setClientVersion(messagePointer); break; @@ -91,6 +93,20 @@ void ACService::getStatus(u32 messagePointer) { mem.write32(messagePointer + 8, 0); } +void ACService::getWifiStatus(u32 messagePointer) { + log("AC::GetWifiStatus (stubbed)\n"); + + enum class WifiStatus : u32 { + None = 0, + Slot1 = 1, + Slot2 = 2, + Slot3 = 4, + }; + + mem.write32(messagePointer, IPC::responseHeader(0x0D, 2, 0)); + mem.write32(messagePointer + 4, Result::Success); + mem.write32(messagePointer + 8, static_cast(WifiStatus::None)); +} void ACService::isConnected(u32 messagePointer) { log("AC::IsConnected\n"); diff --git a/src/core/services/apt.cpp b/src/core/services/apt.cpp index d969f2a4..404a0e59 100644 --- a/src/core/services/apt.cpp +++ b/src/core/services/apt.cpp @@ -2,6 +2,7 @@ #include "ipc.hpp" #include "kernel.hpp" +#include #include namespace APTCommands { @@ -32,34 +33,9 @@ namespace APTCommands { }; } -// https://www.3dbrew.org/wiki/NS_and_APT_Services#Command -namespace APTTransitions { - enum : u32 { - None = 0, - Wakeup = 1, - Request = 2, - Response = 3, - Exit = 4, - Message = 5, - HomeButtonSingle = 6, - HomeButtonDouble = 7, - DSPSleep = 8, - DSPWakeup = 9, - WakeupByExit = 10, - WakuepByPause = 11, - WakeupByCancel = 12, - WakeupByCancelAll = 13, - WakeupByPowerButton = 14, - WakeupToJumpHome = 15, - RequestForApplet = 16, - WakeupToLaunchApp = 17, - ProcessDed = 0x41 - }; -} - void APTService::reset() { - // Set the default CPU time limit to 30%. Seems safe, as this is what Metroid 2 uses by default - cpuTimeLimit = 30; + // Set the default CPU time limit to 0%. Appears to be the default value on hardware + cpuTimeLimit = 0; // Reset the handles for the various service objects lockHandle = std::nullopt; @@ -88,6 +64,7 @@ void APTService::handleSyncRequest(u32 messagePointer) { case APTCommands::NotifyToWait: notifyToWait(messagePointer); break; case APTCommands::PreloadLibraryApplet: preloadLibraryApplet(messagePointer); break; case APTCommands::PrepareToStartLibraryApplet: prepareToStartLibraryApplet(messagePointer); break; + case APTCommands::StartLibraryApplet: startLibraryApplet(messagePointer); break; case APTCommands::ReceiveParameter: [[likely]] receiveParameter(messagePointer); break; case APTCommands::ReplySleepQuery: replySleepQuery(messagePointer); break; case APTCommands::SetApplicationCpuTimeLimit: setApplicationCpuTimeLimit(messagePointer); break; @@ -164,6 +141,39 @@ void APTService::prepareToStartLibraryApplet(u32 messagePointer) { mem.write32(messagePointer + 4, Result::Success); } +void APTService::startLibraryApplet(u32 messagePointer) { + const u32 appID = mem.read32(messagePointer + 4); + const u32 bufferSize = mem.read32(messagePointer + 8); + const Handle parameters = mem.read32(messagePointer + 16); + const u32 buffer = mem.read32(messagePointer + 24); + log("APT::StartLibraryApplet (app ID = %X)\n", appID); + + Applets::AppletBase* destApplet = appletManager.getApplet(appID); + if (destApplet == nullptr) { + Helpers::warn("APT::StartLibraryApplet: Unimplemented dest applet ID"); + mem.write32(messagePointer, IPC::responseHeader(0x1E, 1, 0)); + mem.write32(messagePointer + 4, Result::Success); + } else { + KernelObject* sharedMemObject = kernel.getObject(parameters); + + const MemoryBlock* sharedMem = sharedMemObject ? sharedMemObject->getData() : nullptr; + std::vector data; + data.reserve(bufferSize); + + for (u32 i = 0; i < bufferSize; i++) { + data.push_back(mem.read8(buffer + i)); + } + + Result::HorizonResult result = destApplet->start(sharedMem, data, appID); + if (resumeEvent.has_value()) { + kernel.signalEvent(resumeEvent.value()); + } + + mem.write32(messagePointer, IPC::responseHeader(0x1E, 1, 0)); + mem.write32(messagePointer + 4, result); + } +} + void APTService::checkNew3DS(u32 messagePointer) { log("APT::CheckNew3DS\n"); mem.write32(messagePointer, IPC::responseHeader(0x102, 2, 0)); @@ -246,7 +256,7 @@ void APTService::sendParameter(u32 messagePointer) { const u32 parameterHandle = mem.read32(messagePointer + 24); // What dis? const u32 parameterPointer = mem.read32(messagePointer + 32); - log("APT::SendParameter (source app = %X, dest app = %X, cmd = %X, size = %X) (Stubbed)", sourceAppID, destAppID, cmd, paramSize); + log("APT::SendParameter (source app = %X, dest app = %X, cmd = %X, size = %X)", sourceAppID, destAppID, cmd, paramSize); mem.write32(messagePointer, IPC::responseHeader(0x0C, 1, 0)); mem.write32(messagePointer + 4, Result::Success); @@ -259,7 +269,21 @@ void APTService::sendParameter(u32 messagePointer) { if (destApplet == nullptr) { Helpers::warn("APT::SendParameter: Unimplemented dest applet ID"); } else { - auto result = destApplet->receiveParameter(); + // Construct parameter, send it to applet + Applets::Parameter param; + param.senderID = sourceAppID; + param.destID = destAppID; + param.signal = cmd; + + // Fetch parameter data buffer + param.data.reserve(paramSize); + u32 pointer = parameterPointer; + + for (u32 i = 0; i < paramSize; i++) { + param.data.push_back(mem.read8(pointer++)); + } + + auto result = destApplet->receiveParameter(param); } if (resumeEvent.has_value()) { @@ -270,37 +294,58 @@ void APTService::sendParameter(u32 messagePointer) { void APTService::receiveParameter(u32 messagePointer) { const u32 app = mem.read32(messagePointer + 4); const u32 size = mem.read32(messagePointer + 8); - log("APT::ReceiveParameter(app ID = %X, size = %04X) (STUBBED)\n", app, size); + // Parameter data pointer is in the thread static buffer, which starts 0x100 bytes after the command buffer + const u32 buffer = mem.read32(messagePointer + 0x100 + 4); + log("APT::ReceiveParameter(app ID = %X, size = %04X)\n", app, size); if (size > 0x1000) Helpers::panic("APT::ReceiveParameter with size > 0x1000"); + auto parameter = appletManager.receiveParameter(); - // TODO: Properly implement this. We currently stub somewhat like 3dmoo mem.write32(messagePointer, IPC::responseHeader(0xD, 4, 4)); mem.write32(messagePointer + 4, Result::Success); - mem.write32(messagePointer + 8, 0); // Sender App ID - mem.write32(messagePointer + 12, APTTransitions::Wakeup); // Command - mem.write32(messagePointer + 16, 0); + // Sender App ID + mem.write32(messagePointer + 8, parameter.senderID); + // Command + mem.write32(messagePointer + 12, parameter.signal); + // Size of parameter data + mem.write32(messagePointer + 16, parameter.data.size()); mem.write32(messagePointer + 20, 0x10); - mem.write32(messagePointer + 24, 0); + mem.write32(messagePointer + 24, parameter.object); mem.write32(messagePointer + 28, 0); + + const u32 transferSize = std::min(size, parameter.data.size()); + for (u32 i = 0; i < transferSize; i++) { + mem.write8(buffer + i, parameter.data[i]); + } } void APTService::glanceParameter(u32 messagePointer) { const u32 app = mem.read32(messagePointer + 4); const u32 size = mem.read32(messagePointer + 8); - log("APT::GlanceParameter(app ID = %X, size = %04X) (STUBBED)\n", app, size); + // Parameter data pointer is in the thread static buffer, which starts 0x100 bytes after the command buffer + const u32 buffer = mem.read32(messagePointer + 0x100 + 4); + log("APT::GlanceParameter(app ID = %X, size = %04X)\n", app, size); if (size > 0x1000) Helpers::panic("APT::GlanceParameter with size > 0x1000"); + auto parameter = appletManager.glanceParameter(); // TODO: Properly implement this. We currently stub it similar mem.write32(messagePointer, IPC::responseHeader(0xE, 4, 4)); mem.write32(messagePointer + 4, Result::Success); - mem.write32(messagePointer + 8, 0); // Sender App ID - mem.write32(messagePointer + 12, APTTransitions::Wakeup); // Command - mem.write32(messagePointer + 16, 0); + // Sender App ID + mem.write32(messagePointer + 8, parameter.senderID); + // Command + mem.write32(messagePointer + 12, parameter.signal); + // Size of parameter data + mem.write32(messagePointer + 16, parameter.data.size()); mem.write32(messagePointer + 20, 0); - mem.write32(messagePointer + 24, 0); + mem.write32(messagePointer + 24, parameter.object); mem.write32(messagePointer + 28, 0); + + const u32 transferSize = std::min(size, parameter.data.size()); + for (u32 i = 0; i < transferSize; i++) { + mem.write8(buffer + i, parameter.data[i]); + } } void APTService::replySleepQuery(u32 messagePointer) { @@ -314,10 +359,13 @@ void APTService::setApplicationCpuTimeLimit(u32 messagePointer) { u32 percentage = mem.read32(messagePointer + 8); // CPU time percentage between 5% and 89% log("APT::SetApplicationCpuTimeLimit (percentage = %d%%)\n", percentage); + mem.write32(messagePointer, IPC::responseHeader(0x4F, 1, 0)); + + // If called with invalid parameters, the current time limit is left unchanged, and OS::NotImplemented is returned if (percentage < 5 || percentage > 89 || fixed != 1) { - Helpers::panic("Invalid parameters passed to APT::SetApplicationCpuTimeLimit"); + Helpers::warn("Invalid parameter passed to APT::SetApplicationCpuTimeLimit: (percentage, fixed) = (%d, %d)\n", percentage, fixed); + mem.write32(messagePointer + 4, Result::OS::NotImplemented); } else { - mem.write32(messagePointer, IPC::responseHeader(0x4F, 1, 0)); mem.write32(messagePointer + 4, Result::Success); cpuTimeLimit = percentage; } diff --git a/src/core/services/boss.cpp b/src/core/services/boss.cpp index 30190cd3..a8f7194b 100644 --- a/src/core/services/boss.cpp +++ b/src/core/services/boss.cpp @@ -6,6 +6,7 @@ namespace BOSSCommands { InitializeSession = 0x00010082, UnregisterStorage = 0x00030000, GetTaskStorageInfo = 0x00040000, + GetNewArrivalFlag = 0x00070000, RegisterNewArrivalEvent = 0x00080002, SetOptoutFlag = 0x00090040, GetOptoutFlag = 0x000A0000, @@ -37,6 +38,7 @@ void BOSSService::handleSyncRequest(u32 messagePointer) { switch (command) { case BOSSCommands::CancelTask: cancelTask(messagePointer); break; case BOSSCommands::GetErrorCode: getErrorCode(messagePointer); break; + case BOSSCommands::GetNewArrivalFlag: getNewArrivalFlag(messagePointer); break; case BOSSCommands::GetNsDataIdList: case BOSSCommands::GetNsDataIdList1: getNsDataIdList(messagePointer, command); break; @@ -240,4 +242,11 @@ void BOSSService::unregisterStorage(u32 messagePointer) { log("BOSS::UnregisterStorage (stubbed)\n"); mem.write32(messagePointer, IPC::responseHeader(0x3, 1, 0)); mem.write32(messagePointer + 4, Result::Success); +} + +void BOSSService::getNewArrivalFlag(u32 messagePointer) { + log("BOSS::GetNewArrivalFlag (stubbed)\n"); + mem.write32(messagePointer, IPC::responseHeader(0x7, 2, 0)); + mem.write32(messagePointer + 4, Result::Success); + mem.write8(messagePointer + 8, 0); // Flag } \ No newline at end of file diff --git a/src/core/services/cam.cpp b/src/core/services/cam.cpp index a0206077..b3dfd1dc 100644 --- a/src/core/services/cam.cpp +++ b/src/core/services/cam.cpp @@ -1,31 +1,96 @@ #include "services/cam.hpp" + +#include + #include "ipc.hpp" #include "kernel.hpp" namespace CAMCommands { enum : u32 { + StartCapture = 0x00010040, GetBufferErrorInterruptEvent = 0x00060040, + SetReceiving = 0x00070102, DriverInitialize = 0x00390000, + DriverFinalize = 0x003A0000, SetTransferLines = 0x00090100, GetMaxLines = 0x000A0080, + SetTransferBytes = 0x000B0100, + GetTransferBytes = 0x000C0040, + GetMaxBytes = 0x000D0080, + SetTrimming = 0x000E0080, + SetTrimmingParamsCenter = 0x00120140, + SetSize = 0x001F00C0, // Set size has different headers between cam:u and New3DS QTM module SetFrameRate = 0x00200080, SetContrast = 0x00230080, + GetSuitableY2rStandardCoefficient = 0x00360000, }; } -void CAMService::reset() { bufferErrorInterruptEvents.fill(std::nullopt); } +// Helper struct for working with camera ports +class PortSelect { + u32 value; + + public: + PortSelect(u32 val) : value(val) {} + bool isValid() const { return value < 4; } + + bool isSinglePort() const { + // 1 corresponds to the first camera port and 2 corresponds to the second port + return value == 1 || value == 2; + } + + bool isBothPorts() const { + // 3 corresponds to both ports + return value == 3; + } + + // Returns the index of the camera port, assuming that it's only a single port + int getSingleIndex() const { + if (!isSinglePort()) [[unlikely]] { + Helpers::panic("Camera: getSingleIndex called for port with invalid value"); + } + + return value - 1; + } + + std::vector getPortIndices() const { + switch (value) { + case 1: return {0}; // Only port 1 + case 2: return {1}; // Only port 2 + case 3: return {0, 1}; // Both port 1 and port 2 + default: return {}; // No ports or invalid ports + } + } +}; + +void CAMService::reset() { + for (auto& port : ports) { + port.reset(); + } +} void CAMService::handleSyncRequest(u32 messagePointer) { const u32 command = mem.read32(messagePointer); switch (command) { case CAMCommands::DriverInitialize: driverInitialize(messagePointer); break; + case CAMCommands::DriverFinalize: driverFinalize(messagePointer); break; case CAMCommands::GetBufferErrorInterruptEvent: getBufferErrorInterruptEvent(messagePointer); break; + case CAMCommands::GetMaxBytes: getMaxBytes(messagePointer); break; case CAMCommands::GetMaxLines: getMaxLines(messagePointer); break; + case CAMCommands::GetSuitableY2rStandardCoefficient: getSuitableY2RCoefficients(messagePointer); break; + case CAMCommands::GetTransferBytes: getTransferBytes(messagePointer); break; case CAMCommands::SetContrast: setContrast(messagePointer); break; case CAMCommands::SetFrameRate: setFrameRate(messagePointer); break; + case CAMCommands::SetReceiving: setReceiving(messagePointer); break; + case CAMCommands::SetSize: setSize(messagePointer); break; case CAMCommands::SetTransferLines: setTransferLines(messagePointer); break; + case CAMCommands::SetTrimming: setTrimming(messagePointer); break; + case CAMCommands::SetTrimmingParamsCenter: setTrimmingParamsCenter(messagePointer); break; + case CAMCommands::StartCapture: startCapture(messagePointer); break; + default: - Helpers::panic("Unimplemented CAM service requested. Command: %08X\n", command); + Helpers::warn("Unimplemented CAM service requested. Command: %08X\n", command); + mem.write32(messagePointer + 4, Result::Success); break; } } @@ -36,6 +101,12 @@ void CAMService::driverInitialize(u32 messagePointer) { mem.write32(messagePointer + 4, Result::Success); } +void CAMService::driverFinalize(u32 messagePointer) { + log("CAM::DriverFinalize\n"); + mem.write32(messagePointer, IPC::responseHeader(0x3A, 1, 0)); + mem.write32(messagePointer + 4, Result::Success); +} + void CAMService::setContrast(u32 messagePointer) { const u32 cameraSelect = mem.read32(messagePointer + 4); const u32 contrast = mem.read32(messagePointer + 8); @@ -46,13 +117,46 @@ void CAMService::setContrast(u32 messagePointer) { mem.write32(messagePointer + 4, Result::Success); } -void CAMService::setTransferLines(u32 messagePointer) { - const u32 port = mem.read32(messagePointer + 4); - const s16 lines = mem.read16(messagePointer + 8); - const s16 width = mem.read16(messagePointer + 12); - const s16 height = mem.read16(messagePointer + 16); +void CAMService::setTransferBytes(u32 messagePointer) { + const u32 portIndex = mem.read8(messagePointer + 4); + const u32 bytes = mem.read16(messagePointer + 8); + // ...why do these parameters even exist? + const u16 width = mem.read16(messagePointer + 12); + const u16 height = mem.read16(messagePointer + 16); + const PortSelect port(portIndex); - log("CAM::SetTransferLines (port = %d, lines = %d, width = %d, height = %d)\n", port, lines, width, height); + if (port.isValid()) { + for (int i : port.getPortIndices()) { + ports[i].transferBytes = bytes; + } + } else { + Helpers::warn("CAM::SetTransferBytes: Invalid port\n"); + } + + log("CAM::SetTransferBytes (port = %d, bytes = %d, width = %d, height = %d)\n", portIndex, bytes, width, height); + + mem.write32(messagePointer, IPC::responseHeader(0x9, 1, 0)); + mem.write32(messagePointer + 4, Result::Success); +} + +void CAMService::setTransferLines(u32 messagePointer) { + const u32 portIndex = mem.read8(messagePointer + 4); + const u16 lines = mem.read16(messagePointer + 8); + const u16 width = mem.read16(messagePointer + 12); + const u16 height = mem.read16(messagePointer + 16); + const PortSelect port(portIndex); + + if (port.isValid()) { + const u32 transferBytes = lines * width * 2; + + for (int i : port.getPortIndices()) { + ports[i].transferBytes = transferBytes; + } + } else { + Helpers::warn("CAM::SetTransferLines: Invalid port\n"); + } + + log("CAM::SetTransferLines (port = %d, lines = %d, width = %d, height = %d)\n", portIndex, lines, width, height); mem.write32(messagePointer, IPC::responseHeader(0x9, 1, 0)); mem.write32(messagePointer + 4, Result::Success); @@ -68,6 +172,41 @@ void CAMService::setFrameRate(u32 messagePointer) { mem.write32(messagePointer + 4, Result::Success); } +void CAMService::setSize(u32 messagePointer) { + const u32 cameraSelect = mem.read32(messagePointer + 4); + const u32 size = mem.read32(messagePointer + 8); + const u32 context = mem.read32(messagePointer + 12); + + log("CAM::SetSize (camera select = %d, size = %d, context = %d)\n", cameraSelect, size, context); + + mem.write32(messagePointer, IPC::responseHeader(0x1F, 1, 0)); + mem.write32(messagePointer + 4, Result::Success); +} + +void CAMService::setTrimming(u32 messagePointer) { + const u32 port = mem.read8(messagePointer + 4); + const bool trim = mem.read8(messagePointer + 8) != 0; + + log("CAM::SetTrimming (port = %d, trimming = %s)\n", port, trim ? "enabled" : "disabled"); + + mem.write32(messagePointer, IPC::responseHeader(0x0E, 1, 0)); + mem.write32(messagePointer + 4, Result::Success); +} + +void CAMService::setTrimmingParamsCenter(u32 messagePointer) { + const u32 port = mem.read8(messagePointer + 4); + const s16 trimWidth = s16(mem.read16(messagePointer + 8)); + const s16 trimHeight = s16(mem.read16(messagePointer + 12)); + const s16 cameraWidth = s16(mem.read16(messagePointer + 16)); + const s16 cameraHeight = s16(mem.read16(messagePointer + 20)); + + log("CAM::SetTrimmingParamsCenter (port = %d), trim size = (%d, %d), camera size = (%d, %d)\n", port, trimWidth, trimHeight, cameraWidth, + cameraHeight); + + mem.write32(messagePointer, IPC::responseHeader(0x12, 1, 0)); + mem.write32(messagePointer + 4, Result::Success); +} + // Algorithm taken from Citra // https://github.com/citra-emu/citra/blob/master/src/core/hle/service/cam/cam.cpp#L465 void CAMService::getMaxLines(u32 messagePointer) { @@ -100,16 +239,62 @@ void CAMService::getMaxLines(u32 messagePointer) { } } +void CAMService::getMaxBytes(u32 messagePointer) { + const u16 width = mem.read16(messagePointer + 4); + const u16 height = mem.read16(messagePointer + 8); + log("CAM::GetMaxBytes (width = %d, height = %d)\n", width, height); + + constexpr u32 MIN_TRANSFER_UNIT = 256; + constexpr u32 MAX_BUFFER_SIZE = 2560; + if (width * height * 2 % MIN_TRANSFER_UNIT != 0) { + Helpers::panic("CAM::GetMaxLines out of range"); + } else { + u32 bytes = MAX_BUFFER_SIZE; + + while (width * height * 2 % bytes != 0) { + bytes -= MIN_TRANSFER_UNIT; + } + + mem.write32(messagePointer, IPC::responseHeader(0xA, 2, 0)); + mem.write32(messagePointer + 4, Result::Success); + mem.write32(messagePointer + 8, bytes); + } +} + +void CAMService::getSuitableY2RCoefficients(u32 messagePointer) { + log("CAM::GetSuitableY2RCoefficients\n"); + mem.write32(messagePointer, IPC::responseHeader(0x36, 2, 0)); + mem.write32(messagePointer + 4, Result::Success); + // Y2R standard coefficient value + mem.write32(messagePointer + 8, 0); +} + +void CAMService::getTransferBytes(u32 messagePointer) { + const u32 portIndex = mem.read8(messagePointer + 4); + const PortSelect port(portIndex); + log("CAM::GetTransferBytes (port = %d)\n", portIndex); + + mem.write32(messagePointer, IPC::responseHeader(0x0C, 2, 0)); + mem.write32(messagePointer + 4, Result::Success); + + if (port.isSinglePort()) { + mem.write32(messagePointer + 8, ports[port.getSingleIndex()].transferBytes); + } else { + // TODO: This should return the proper error code + Helpers::warn("CAM::GetTransferBytes: Invalid port index"); + mem.write32(messagePointer + 8, 0); + } +} + void CAMService::getBufferErrorInterruptEvent(u32 messagePointer) { - const u32 port = mem.read32(messagePointer + 4); - log("CAM::GetBufferErrorInterruptEvent (port = %d)\n", port); + const u32 portIndex = mem.read8(messagePointer + 4); + const PortSelect port(portIndex); + log("CAM::GetBufferErrorInterruptEvent (port = %d)\n", portIndex); mem.write32(messagePointer, IPC::responseHeader(0x6, 1, 2)); - if (port >= portCount) { - Helpers::panic("CAM::GetBufferErrorInterruptEvent: Invalid port"); - } else { - auto& event = bufferErrorInterruptEvents[port]; + if (port.isSinglePort()) { + auto& event = ports[port.getSingleIndex()].bufferErrorInterruptEvent; if (!event.has_value()) { event = kernel.makeEvent(ResetType::OneShot); } @@ -117,5 +302,55 @@ void CAMService::getBufferErrorInterruptEvent(u32 messagePointer) { mem.write32(messagePointer + 4, Result::Success); mem.write32(messagePointer + 8, 0); mem.write32(messagePointer + 12, event.value()); + } else { + Helpers::panic("CAM::GetBufferErrorInterruptEvent: Invalid port"); } -} \ No newline at end of file +} + +void CAMService::setReceiving(u32 messagePointer) { + const u32 destination = mem.read32(messagePointer + 4); + const u32 portIndex = mem.read8(messagePointer + 8); + const u32 size = mem.read32(messagePointer + 12); + const u16 transferUnit = mem.read16(messagePointer + 16); + const Handle process = mem.read32(messagePointer + 24); + + const PortSelect port(portIndex); + log("CAM::SetReceiving (port = %d)\n", portIndex); + + mem.write32(messagePointer, IPC::responseHeader(0x7, 1, 2)); + + if (port.isSinglePort()) { + auto& event = ports[port.getSingleIndex()].receiveEvent; + if (!event.has_value()) { + event = kernel.makeEvent(ResetType::OneShot); + } + + mem.write32(messagePointer + 4, Result::Success); + mem.write32(messagePointer + 8, 0); + mem.write32(messagePointer + 12, event.value()); + } else { + Helpers::panic("CAM::SetReceiving: Invalid port"); + } +} + +void CAMService::startCapture(u32 messagePointer) { + const u32 portIndex = mem.read8(messagePointer + 4); + const PortSelect port(portIndex); + log("CAM::StartCapture (port = %d)\n", portIndex); + + mem.write32(messagePointer, IPC::responseHeader(0x01, 1, 0)); + mem.write32(messagePointer + 4, Result::Success); + + if (port.isValid()) { + for (int i : port.getPortIndices()) { + auto& event = ports[port.getSingleIndex()].receiveEvent; + + // Until we properly implement cameras, immediately signal the receive event + if (event.has_value()) { + kernel.signalEvent(event.value()); + } + } + } else { + Helpers::warn("CAM::StartCapture: Invalid port index"); + } +} diff --git a/src/core/services/fs.cpp b/src/core/services/fs.cpp index 0bcbab37..2e102958 100644 --- a/src/core/services/fs.cpp +++ b/src/core/services/fs.cpp @@ -27,6 +27,7 @@ namespace FSCommands { CloseArchive = 0x080E0080, FormatThisUserSaveData = 0x080F0180, GetFreeBytes = 0x08120080, + GetSdmcArchiveResource = 0x08140000, IsSdmcDetected = 0x08170000, IsSdmcWritable = 0x08180000, CardSlotIsInserted = 0x08210000, @@ -96,6 +97,7 @@ ArchiveBase* FSService::getArchiveFromID(u32 id, const FSPath& archivePath) { case ArchiveID::SystemSaveData: return &systemSaveData; case ArchiveID::SDMC: return &sdmc; + case ArchiveID::SDMCWriteOnly: return &sdmcWriteOnly; case ArchiveID::SavedataAndNcch: return &ncch; // This can only access NCCH outside of FSPXI default: Helpers::panic("Unknown archive. ID: %d\n", id); @@ -179,6 +181,7 @@ void FSService::handleSyncRequest(u32 messagePointer) { case FSCommands::GetFreeBytes: getFreeBytes(messagePointer); break; case FSCommands::GetFormatInfo: getFormatInfo(messagePointer); break; case FSCommands::GetPriority: getPriority(messagePointer); break; + case FSCommands::GetSdmcArchiveResource: getSdmcArchiveResource(messagePointer); break; case FSCommands::GetThisSaveDataSecureValue: getThisSaveDataSecureValue(messagePointer); break; case FSCommands::Initialize: initialize(messagePointer); break; case FSCommands::InitializeWithSdkVersion: initializeWithSdkVersion(messagePointer); break; @@ -670,6 +673,9 @@ void FSService::getThisSaveDataSecureValue(u32 messagePointer) { mem.write32(messagePointer, IPC::responseHeader(0x86F, 1, 0)); mem.write32(messagePointer + 4, Result::Success); + mem.write8(messagePointer + 8, 0); // Secure value does not exist + mem.write8(messagePointer + 12, 1); // TODO: What is this? + mem.write64(messagePointer + 16, 0); // Secure value } void FSService::setThisSaveDataSecureValue(u32 messagePointer) { @@ -760,4 +766,23 @@ void FSService::renameFile(u32 messagePointer) { // Everything is OK, let's do the rename. Both archives should match so we don't need the dest anymore const HorizonResult res = sourceArchive->archive->renameFile(sourcePath, destPath); mem.write32(messagePointer + 4, static_cast(res)); +} + +void FSService::getSdmcArchiveResource(u32 messagePointer) { + log("FS::GetSdmcArchiveResource"); // For the time being, return the same stubbed archive resource for every media type + + static constexpr ArchiveResource resource = { + .sectorSize = 512, + .clusterSize = 16_KB, + .partitionCapacityInClusters = 0x80000, // 0x80000 * 16 KB = 8GB + .freeSpaceInClusters = 0x80000, // Same here + }; + + mem.write32(messagePointer, IPC::responseHeader(0x814, 5, 0)); + mem.write32(messagePointer + 4, Result::Success); + + mem.write32(messagePointer + 8, resource.sectorSize); + mem.write32(messagePointer + 12, resource.clusterSize); + mem.write32(messagePointer + 16, resource.partitionCapacityInClusters); + mem.write32(messagePointer + 20, resource.freeSpaceInClusters); } \ No newline at end of file diff --git a/src/core/services/gsp_gpu.cpp b/src/core/services/gsp_gpu.cpp index 0e27c0ec..8dff6a79 100644 --- a/src/core/services/gsp_gpu.cpp +++ b/src/core/services/gsp_gpu.cpp @@ -15,8 +15,10 @@ namespace ServiceCommands { FlushDataCache = 0x00080082, SetLCDForceBlack = 0x000B0040, TriggerCmdReqQueue = 0x000C0000, + ReleaseRight = 0x00170000, ImportDisplayCaptureInfo = 0x00180000, SaveVramSysArea = 0x00190000, + RestoreVramSysArea = 0x001A0000, SetInternalPriorities = 0x001E0080, StoreDataCache = 0x001F0082 }; @@ -49,6 +51,8 @@ void GPUService::handleSyncRequest(u32 messagePointer) { case ServiceCommands::FlushDataCache: flushDataCache(messagePointer); break; case ServiceCommands::ImportDisplayCaptureInfo: importDisplayCaptureInfo(messagePointer); break; case ServiceCommands::RegisterInterruptRelayQueue: registerInterruptRelayQueue(messagePointer); break; + case ServiceCommands::ReleaseRight: releaseRight(messagePointer); break; + case ServiceCommands::RestoreVramSysArea: restoreVramSysArea(messagePointer); break; case ServiceCommands::SaveVramSysArea: saveVramSysArea(messagePointer); break; case ServiceCommands::SetAxiConfigQoSMode: setAxiConfigQoSMode(messagePointer); break; case ServiceCommands::SetBufferSwap: setBufferSwap(messagePointer); break; @@ -80,6 +84,16 @@ void GPUService::acquireRight(u32 messagePointer) { mem.write32(messagePointer + 4, Result::Success); } +void GPUService::releaseRight(u32 messagePointer) { + log("GSP::GPU::ReleaseRight\n"); + if (privilegedProcess == currentPID) { + privilegedProcess = 0xFFFFFFFF; + } + + mem.write32(messagePointer, IPC::responseHeader(0x17, 1, 0)); + mem.write32(messagePointer + 4, Result::Success); +} + // TODO: What is the flags field meant to be? // What is the "GSP module thread index" meant to be? // How does the shared memory handle thing work? @@ -131,8 +145,7 @@ void GPUService::requestInterrupt(GPUInterrupt type) { // Not emulating this causes Yoshi's Wooly World, Captain Toad, Metroid 2 et al to hang if (type == GPUInterrupt::VBlank0 || type == GPUInterrupt::VBlank1) { int screen = static_cast(type) - static_cast(GPUInterrupt::VBlank0); // 0 for top screen, 1 for bottom - // TODO: Offset depends on GSP thread being triggered - FramebufferUpdate* update = reinterpret_cast(&sharedMem[0x200 + screen * sizeof(FramebufferUpdate)]); + FramebufferUpdate* update = getFramebufferInfo(screen); if (update->dirtyFlag & 1) { setBufferSwapImpl(screen, update->framebufferInfo[update->index]); @@ -470,10 +483,50 @@ void GPUService::saveVramSysArea(u32 messagePointer) { mem.write32(messagePointer + 4, Result::Success); } +void GPUService::restoreVramSysArea(u32 messagePointer) { + Helpers::warn("GSP::GPU::RestoreVramSysArea (stubbed)"); + + mem.write32(messagePointer, IPC::responseHeader(0x1A, 1, 0)); + mem.write32(messagePointer + 4, Result::Success); +} + // Used in similar fashion to the SaveVramSysArea function void GPUService::importDisplayCaptureInfo(u32 messagePointer) { Helpers::warn("GSP::GPU::ImportDisplayCaptureInfo (stubbed)"); mem.write32(messagePointer, IPC::responseHeader(0x18, 9, 0)); mem.write32(messagePointer + 4, Result::Success); + + if (sharedMem == nullptr) { + Helpers::warn("GSP::GPU::ImportDisplayCaptureInfo called without GSP module being properly initialized!"); + return; + } + + FramebufferUpdate* topScreen = getTopFramebufferInfo(); + FramebufferUpdate* bottomScreen = getBottomFramebufferInfo(); + + // Capture the relevant data for both screens and return them to the caller + CaptureInfo topScreenCapture = { + .leftFramebuffer = topScreen->framebufferInfo[topScreen->index].leftFramebufferVaddr, + .rightFramebuffer = topScreen->framebufferInfo[topScreen->index].rightFramebufferVaddr, + .format = topScreen->framebufferInfo[topScreen->index].format, + .stride = topScreen->framebufferInfo[topScreen->index].stride, + }; + + CaptureInfo bottomScreenCapture = { + .leftFramebuffer = bottomScreen->framebufferInfo[bottomScreen->index].leftFramebufferVaddr, + .rightFramebuffer = bottomScreen->framebufferInfo[bottomScreen->index].rightFramebufferVaddr, + .format = bottomScreen->framebufferInfo[bottomScreen->index].format, + .stride = bottomScreen->framebufferInfo[bottomScreen->index].stride, + }; + + mem.write32(messagePointer + 8, topScreenCapture.leftFramebuffer); + mem.write32(messagePointer + 12, topScreenCapture.rightFramebuffer); + mem.write32(messagePointer + 16, topScreenCapture.format); + mem.write32(messagePointer + 20, topScreenCapture.stride); + + mem.write32(messagePointer + 24, bottomScreenCapture.leftFramebuffer); + mem.write32(messagePointer + 28, bottomScreenCapture.rightFramebuffer); + mem.write32(messagePointer + 32, bottomScreenCapture.format); + mem.write32(messagePointer + 36, bottomScreenCapture.stride); } \ No newline at end of file diff --git a/src/core/services/ir_user.cpp b/src/core/services/ir_user.cpp index 47a2299a..ce4f94c4 100644 --- a/src/core/services/ir_user.cpp +++ b/src/core/services/ir_user.cpp @@ -120,9 +120,14 @@ void IRUserService::requireConnection(u32 messagePointer) { u32 sharedMemAddress = sharedMemory.value().addr; if (deviceID == u8(DeviceID::CirclePadPro)) { - mem.write8(sharedMemAddress + offsetof(SharedMemoryStatus, connectionStatus), 2); // Citra uses 2 here but only 1 works?? - mem.write8(sharedMemAddress + offsetof(SharedMemoryStatus, connectionRole), 2); - mem.write8(sharedMemAddress + offsetof(SharedMemoryStatus, isConnected), 1); + // Note: We temporarily pretend we don't have a CirclePad Pro. This code must change when we emulate it or N3DS C-stick + constexpr u8 status = 1; // Not connected. Any value other than 2 is considered not connected. + constexpr u8 role = 0; + constexpr u8 connected = 0; + + mem.write8(sharedMemAddress + offsetof(SharedMemoryStatus, connectionStatus), status); + mem.write8(sharedMemAddress + offsetof(SharedMemoryStatus, connectionRole), role); + mem.write8(sharedMemAddress + offsetof(SharedMemoryStatus, isConnected), connected); connectedDevice = true; if (connectionStatusEvent.has_value()) { diff --git a/src/core/services/ptm.cpp b/src/core/services/ptm.cpp index 071fa012..67451cc2 100644 --- a/src/core/services/ptm.cpp +++ b/src/core/services/ptm.cpp @@ -6,6 +6,7 @@ namespace PTMCommands { GetAdapterState = 0x00050000, GetBatteryLevel = 0x00070000, GetBatteryChargeState = 0x00080000, + GetPedometerState = 0x00090000, GetStepHistory = 0x000B00C2, GetTotalStepCount = 0x000C0000, GetStepHistoryAll = 0x000F0084, @@ -30,6 +31,7 @@ void PTMService::handleSyncRequest(u32 messagePointer, PTMService::Type type) { case PTMCommands::GetAdapterState: getAdapterState(messagePointer); break; case PTMCommands::GetBatteryChargeState: getBatteryChargeState(messagePointer); break; case PTMCommands::GetBatteryLevel: getBatteryLevel(messagePointer); break; + case PTMCommands::GetPedometerState: getPedometerState(messagePointer); break; case PTMCommands::GetStepHistory: getStepHistory(messagePointer); break; case PTMCommands::GetStepHistoryAll: getStepHistoryAll(messagePointer); break; case PTMCommands::GetTotalStepCount: getTotalStepCount(messagePointer); break; @@ -67,11 +69,20 @@ void PTMService::getBatteryChargeState(u32 messagePointer) { // We're only charging if the battery is not already full const bool charging = config.chargerPlugged && (config.batteryPercentage < 100); - mem.write32(messagePointer, IPC::responseHeader(0x7, 2, 0)); + mem.write32(messagePointer, IPC::responseHeader(0x8, 2, 0)); mem.write32(messagePointer + 4, Result::Success); mem.write8(messagePointer + 8, charging ? 1 : 0); } +void PTMService::getPedometerState(u32 messagePointer) { + log("PTM::GetPedometerState"); + constexpr bool countingSteps = true; + + mem.write32(messagePointer, IPC::responseHeader(0x9, 2, 0)); + mem.write32(messagePointer + 4, Result::Success); + mem.write8(messagePointer + 8, countingSteps ? 1 : 0); +} + void PTMService::getBatteryLevel(u32 messagePointer) { log("PTM::GetBatteryLevel"); diff --git a/src/core/services/y2r.cpp b/src/core/services/y2r.cpp index 40a6a04d..99b18418 100644 --- a/src/core/services/y2r.cpp +++ b/src/core/services/y2r.cpp @@ -18,12 +18,16 @@ namespace Y2RCommands { SetSendingY = 0x00100102, SetSendingU = 0x00110102, SetSendingV = 0x00120102, + SetSendingYUV = 0x00130102, SetReceiving = 0x00180102, SetInputLineWidth = 0x001A0040, GetInputLineWidth = 0x001B0000, SetInputLines = 0x001C0040, GetInputLines = 0x001D0000, + SetCoefficientParams = 0x001E0100, + GetCoefficientParams = 0x001F0000, SetStandardCoeff = 0x00200040, + GetStandardCoefficientParams = 0x00210040, SetAlpha = 0x00220040, StartConversion = 0x00260000, StopConversion = 0x00270000, @@ -50,6 +54,8 @@ void Y2RService::reset() { alpha = 0xFFFF; inputLines = 69; inputLineWidth = 420; + + conversionCoefficients.fill(0); } void Y2RService::handleSyncRequest(u32 messagePointer) { @@ -62,6 +68,7 @@ void Y2RService::handleSyncRequest(u32 messagePointer) { case Y2RCommands::GetInputLineWidth: getInputLineWidth(messagePointer); break; case Y2RCommands::GetOutputFormat: getOutputFormat(messagePointer); break; case Y2RCommands::GetTransferEndEvent: getTransferEndEvent(messagePointer); break; + case Y2RCommands::GetStandardCoefficientParams: getStandardCoefficientParams(messagePointer); break; case Y2RCommands::IsBusyConversion: isBusyConversion(messagePointer); break; case Y2RCommands::PingProcess: pingProcess(messagePointer); break; case Y2RCommands::SetAlpha: setAlpha(messagePointer); break; @@ -76,12 +83,17 @@ void Y2RService::handleSyncRequest(u32 messagePointer) { case Y2RCommands::SetSendingY: setSendingY(messagePointer); break; case Y2RCommands::SetSendingU: setSendingU(messagePointer); break; case Y2RCommands::SetSendingV: setSendingV(messagePointer); break; + case Y2RCommands::SetSendingYUV: setSendingYUV(messagePointer); break; case Y2RCommands::SetSpacialDithering: setSpacialDithering(messagePointer); break; case Y2RCommands::SetStandardCoeff: setStandardCoeff(messagePointer); break; case Y2RCommands::SetTemporalDithering: setTemporalDithering(messagePointer); break; case Y2RCommands::SetTransferEndInterrupt: setTransferEndInterrupt(messagePointer); break; case Y2RCommands::StartConversion: [[likely]] startConversion(messagePointer); break; case Y2RCommands::StopConversion: stopConversion(messagePointer); break; + + // Intentionally break ordering a bit for less-used Y2R functions + case Y2RCommands::SetCoefficientParams: setCoefficientParams(messagePointer); break; + case Y2RCommands::GetCoefficientParams: getCoefficientParams(messagePointer); break; default: Helpers::panic("Y2R service requested. Command: %08X\n", command); } } @@ -97,6 +109,8 @@ void Y2RService::driverInitialize(u32 messagePointer) { log("Y2R::DriverInitialize\n"); mem.write32(messagePointer, IPC::responseHeader(0x2B, 1, 0)); mem.write32(messagePointer + 4, Result::Success); + + conversionCoefficients.fill(0); } void Y2RService::driverFinalize(u32 messagePointer) { @@ -276,6 +290,7 @@ void Y2RService::getInputLineWidth(u32 messagePointer) { mem.write32(messagePointer + 4, Result::Success); mem.write32(messagePointer + 8, inputLineWidth); } + void Y2RService::setInputLines(u32 messagePointer) { const u16 lines = mem.read16(messagePointer + 4); log("Y2R::SetInputLines (lines = %d)\n", lines); @@ -306,7 +321,7 @@ void Y2RService::setStandardCoeff(u32 messagePointer) { log("Y2R::SetStandardCoeff (coefficient = %d)\n", coeff); mem.write32(messagePointer, IPC::responseHeader(0x20, 1, 0)); - if (coeff > 3) { + if (coeff > 3) { // Invalid coefficient, should have an error code Helpers::panic("Y2R: Invalid standard coefficient (coefficient = %d)\n", coeff); } @@ -316,6 +331,52 @@ void Y2RService::setStandardCoeff(u32 messagePointer) { } } +void Y2RService::getStandardCoefficientParams(u32 messagePointer) { + const u32 coefficientIndex = mem.read32(messagePointer + 4); + log("Y2R::GetStandardCoefficientParams (coefficient = %d)\n", coefficientIndex); + + if (coefficientIndex > 3) { // Invalid coefficient, should have an error code + Helpers::panic("Y2R: Invalid standard coefficient (coefficient = %d)\n", coefficientIndex); + } else { + mem.write32(messagePointer, IPC::responseHeader(0x21, 5, 0)); + mem.write32(messagePointer + 4, Result::Success); + const auto& coeff = standardCoefficients[coefficientIndex]; + + // Write standard coefficient parameters to output buffer + for (int i = 0; i < 8; i++) { + const u32 pointer = messagePointer + 8 + i * sizeof(u16); // Pointer to write parameter to + mem.write16(pointer, coeff[i]); + } + } +} + +void Y2RService::setCoefficientParams(u32 messagePointer) { + log("Y2R::SetCoefficientParams\n"); + auto& coeff = conversionCoefficients; + + // Write coefficient parameters to output buffer + for (int i = 0; i < 8; i++) { + const u32 pointer = messagePointer + 4 + i * sizeof(u16); // Pointer to write parameter to + coeff[i] = mem.read16(pointer); + } + + mem.write32(messagePointer, IPC::responseHeader(0x1E, 1, 0)); + mem.write32(messagePointer + 4, Result::Success); +} + +void Y2RService::getCoefficientParams(u32 messagePointer) { + log("Y2R::GetCoefficientParams\n"); + mem.write32(messagePointer, IPC::responseHeader(0x1F, 5, 0)); + mem.write32(messagePointer + 4, Result::Success); + const auto& coeff = conversionCoefficients; + + // Write coefficient parameters to output buffer + for (int i = 0; i < 8; i++) { + const u32 pointer = messagePointer + 8 + i * sizeof(u16); // Pointer to write parameter to + mem.write16(pointer, coeff[i]); + } +} + void Y2RService::setSendingY(u32 messagePointer) { log("Y2R::SetSendingY\n"); Helpers::warn("Unimplemented Y2R::SetSendingY"); @@ -340,6 +401,14 @@ void Y2RService::setSendingV(u32 messagePointer) { mem.write32(messagePointer + 4, Result::Success); } +void Y2RService::setSendingYUV(u32 messagePointer) { + log("Y2R::SetSendingYUV\n"); + Helpers::warn("Unimplemented Y2R::SetSendingYUV"); + + mem.write32(messagePointer, IPC::responseHeader(0x13, 1, 0)); + mem.write32(messagePointer + 4, Result::Success); +} + void Y2RService::setReceiving(u32 messagePointer) { log("Y2R::SetReceiving\n"); Helpers::warn("Unimplemented Y2R::setReceiving"); diff --git a/src/emulator.cpp b/src/emulator.cpp index 81d3ac5c..c567cbc7 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -1,6 +1,8 @@ #include "emulator.hpp" +#ifndef __ANDROID__ #include +#endif #include @@ -15,10 +17,11 @@ __declspec(dllexport) DWORD AmdPowerXpressRequestHighPerformance = 1; #endif Emulator::Emulator() - : config(std::filesystem::current_path() / "config.toml"), kernel(cpu, memory, gpu, config), cpu(memory, kernel), gpu(memory, config), - memory(cpu.getTicksRef(), config), cheats(memory, kernel.getServiceManager().getHID()), lua(memory), running(false), programRunning(false) + : config(getConfigPath()), kernel(cpu, memory, gpu, config), cpu(memory, kernel, *this), gpu(memory, config), memory(cpu.getTicksRef(), config), + cheats(memory, kernel.getServiceManager().getHID()), lua(memory), running(false), programRunning(false) #ifdef PANDA3DS_ENABLE_HTTP_SERVER - , httpServer(this) + , + httpServer(this) #endif { #ifdef PANDA3DS_ENABLE_DISCORD_RPC @@ -31,7 +34,7 @@ Emulator::Emulator() } Emulator::~Emulator() { - config.save(std::filesystem::current_path() / "config.toml"); + config.save(); lua.close(); #ifdef PANDA3DS_ENABLE_DISCORD_RPC @@ -43,6 +46,9 @@ void Emulator::reset(ReloadOption reload) { cpu.reset(); gpu.reset(); memory.reset(); + // Reset scheduler and add a VBlank event + scheduler.reset(); + // Kernel must be reset last because it depends on CPU/Memory state kernel.reset(); @@ -68,6 +74,23 @@ void Emulator::reset(ReloadOption reload) { } } +std::filesystem::path Emulator::getAndroidAppPath() { + // SDL_GetPrefPath fails to get the path due to no JNI environment + std::ifstream cmdline("/proc/self/cmdline"); + std::string applicationName; + std::getline(cmdline, applicationName, '\0'); + + return std::filesystem::path("/data") / "data" / applicationName / "files"; +} + +std::filesystem::path Emulator::getConfigPath() { + if constexpr (Helpers::isAndroid()) { + return getAndroidAppPath() / "config.toml"; + } else { + return std::filesystem::current_path() / "config.toml"; + } +} + void Emulator::step() {} void Emulator::render() {} @@ -80,12 +103,6 @@ void Emulator::runFrame() { if (running) { cpu.runFrame(); // Run 1 frame of instructions gpu.display(); // Display graphics - lua.signalEvent(LuaEvent::Frame); - - // Send VBlank interrupts - ServiceManager& srv = kernel.getServiceManager(); - srv.sendGPUInterrupt(GPUInterrupt::VBlank0); - srv.sendGPUInterrupt(GPUInterrupt::VBlank1); // Run cheats if any are loaded if (cheats.haveCheats()) [[unlikely]] { @@ -98,6 +115,67 @@ void Emulator::runFrame() { } } +void Emulator::pollScheduler() { + auto& events = scheduler.events; + + // Pop events until there's none pending anymore + while (scheduler.currentTimestamp >= scheduler.nextTimestamp) { + // Read event timestamp and type, pop it from the scheduler and handle it + auto [time, eventType] = std::move(*events.begin()); + events.erase(events.begin()); + + scheduler.updateNextTimestamp(); + + switch (eventType) { + case Scheduler::EventType::VBlank: [[likely]] { + // Signal that we've reached the end of a frame + frameDone = true; + lua.signalEvent(LuaEvent::Frame); + + // Send VBlank interrupts + ServiceManager& srv = kernel.getServiceManager(); + srv.sendGPUInterrupt(GPUInterrupt::VBlank0); + srv.sendGPUInterrupt(GPUInterrupt::VBlank1); + + // Queue next VBlank event + scheduler.addEvent(Scheduler::EventType::VBlank, time + CPU::ticksPerSec / 60); + break; + } + + case Scheduler::EventType::UpdateTimers: kernel.pollTimers(); break; + + default: { + Helpers::panic("Scheduler: Unimplemented event type received: %d\n", static_cast(eventType)); + break; + } + } + } +} + +// Get path for saving files (AppData on Windows, /home/user/.local/share/ApplicationName on Linux, etc) +// Inside that path, we be use a game-specific folder as well. Eg if we were loading a ROM called PenguinDemo.3ds, the savedata would be in +// %APPDATA%/Alber/PenguinDemo/SaveData on Windows, and so on. We do this because games save data in their own filesystem on the cart. +// If the portable build setting is enabled, then those saves go in the executable directory instead +std::filesystem::path Emulator::getAppDataRoot() { + std::filesystem::path appDataPath; + +#ifdef __ANDROID__ + appDataPath = getAndroidAppPath(); +#else + char* appData; + if (!config.usePortableBuild) { + appData = SDL_GetPrefPath(nullptr, "Alber"); + appDataPath = std::filesystem::path(appData); + } else { + appData = SDL_GetBasePath(); + appDataPath = std::filesystem::path(appData) / "Emulator Files"; + } + SDL_free(appData); +#endif + + return appDataPath; +} + bool Emulator::loadROM(const std::filesystem::path& path) { // Reset the emulator if we've already loaded a ROM if (romType != ROMType::None) { @@ -108,30 +186,7 @@ bool Emulator::loadROM(const std::filesystem::path& path) { memory.loadedCXI = std::nullopt; memory.loaded3DSX = std::nullopt; - // Get path for saving files (AppData on Windows, /home/user/.local/share/ApplicationName on Linux, etc) - // Inside that path, we be use a game-specific folder as well. Eg if we were loading a ROM called PenguinDemo.3ds, the savedata would be in - // %APPDATA%/Alber/PenguinDemo/SaveData on Windows, and so on. We do this because games save data in their own filesystem on the cart. - // If the portable build setting is enabled, then those saves go in the executable directory instead - std::filesystem::path appDataPath; - - #ifdef __ANDROID__ - // SDL_GetPrefPath fails to get the path due to no JNI environment - std::ifstream cmdline("/proc/self/cmdline"); - std::string applicationName; - std::getline(cmdline, applicationName, '\0'); - appDataPath = std::filesystem::path("/data") / "data" / applicationName / "files"; - #else - char* appData; - if (!config.usePortableBuild) { - appData = SDL_GetPrefPath(nullptr, "Alber"); - appDataPath = std::filesystem::path(appData); - } else { - appData = SDL_GetBasePath(); - appDataPath = std::filesystem::path(appData) / "Emulator Files"; - } - SDL_free(appData); - #endif - + const std::filesystem::path appDataPath = getAppDataRoot(); const std::filesystem::path dataPath = appDataPath / path.filename().stem(); const std::filesystem::path aesKeysPath = appDataPath / "sysdata" / "aes_keys.txt"; IOFile::setAppDataDir(dataPath); @@ -237,6 +292,17 @@ bool Emulator::loadELF(std::ifstream& file) { return true; } +std::span Emulator::getSMDH() { + switch (romType) { + case ROMType::NCSD: + case ROMType::CXI: + return memory.getCXI()->smdh; + default: { + return std::span(); + } + } +} + #ifdef PANDA3DS_ENABLE_DISCORD_RPC void Emulator::updateDiscord() { if (config.discordRpcEnabled) { diff --git a/src/hydra_core.cpp b/src/hydra_core.cpp index 3c809dc3..acbf30a8 100644 --- a/src/hydra_core.cpp +++ b/src/hydra_core.cpp @@ -4,8 +4,13 @@ #include #include "hydra_icon.hpp" +#include "swap.hpp" -class HC_GLOBAL HydraCore final : public hydra::IBase, public hydra::IOpenGlRendered, public hydra::IFrontendDriven, public hydra::IInput { +class HC_GLOBAL HydraCore final : public hydra::IBase, + public hydra::IOpenGlRendered, + public hydra::IFrontendDriven, + public hydra::IInput, + public hydra::ICheat { HYDRA_CLASS public: HydraCore(); @@ -18,22 +23,30 @@ class HC_GLOBAL HydraCore final : public hydra::IBase, public hydra::IOpenGlRend void setOutputSize(hydra::Size size) override; // IOpenGlRendered + void resetContext() override; + void destroyContext() override; void setFbo(unsigned handle) override; - void setContext(void* context) override; void setGetProcAddress(void* function) override; // IFrontendDriven void runFrame() override; - uint16_t getFps() override; + u16 getFps() override; // IInput void setPollInputCallback(void (*callback)()) override; - void setCheckButtonCallback(int32_t (*callback)(uint32_t player, hydra::ButtonType button)) override; + void setCheckButtonCallback(s32 (*callback)(u32 player, hydra::ButtonType button)) override; + + // ICheat + u32 addCheat(const u8* data, u32 size) override; + void removeCheat(u32 id) override; + void enableCheat(u32 id) override; + void disableCheat(u32 id) override; std::unique_ptr emulator; RendererGL* renderer; void (*pollInputCallback)() = nullptr; int32_t (*checkButtonCallback)(uint32_t player, hydra::ButtonType button) = nullptr; + void* getProcAddress = nullptr; }; HydraCore::HydraCore() : emulator(new Emulator) { @@ -88,11 +101,10 @@ void HydraCore::runFrame() { } hid.updateInputs(emulator->getTicks()); - emulator->runFrame(); } -uint16_t HydraCore::getFps() { return 60; } +u16 HydraCore::getFps() { return 60; } void HydraCore::reset() { emulator->reset(Emulator::ReloadOption::Reload); } hydra::Size HydraCore::getNativeSize() { return {400, 480}; } @@ -100,13 +112,13 @@ hydra::Size HydraCore::getNativeSize() { return {400, 480}; } // Size doesn't matter as the glBlitFramebuffer call is commented out for the core void HydraCore::setOutputSize(hydra::Size size) {} -void HydraCore::setGetProcAddress(void* function) { +void HydraCore::resetContext() { #ifdef __ANDROID__ - if (!gladLoadGLES2Loader(reinterpret_cast(function))) { + if (!gladLoadGLES2Loader(reinterpret_cast(getProcAddress))) { Helpers::panic("OpenGL ES init failed"); } #else - if (!gladLoadGLLoader(reinterpret_cast(function))) { + if (!gladLoadGLLoader(reinterpret_cast(getProcAddress))) { Helpers::panic("OpenGL init failed"); } #endif @@ -114,13 +126,22 @@ void HydraCore::setGetProcAddress(void* function) { emulator->initGraphicsContext(nullptr); } -void HydraCore::setContext(void*) {} +void HydraCore::destroyContext() { emulator->deinitGraphicsContext(); } void HydraCore::setFbo(unsigned handle) { renderer->setFBO(handle); } +void HydraCore::setGetProcAddress(void* function) { getProcAddress = function; } void HydraCore::setPollInputCallback(void (*callback)()) { pollInputCallback = callback; } -void HydraCore::setCheckButtonCallback(int32_t (*callback)(uint32_t player, hydra::ButtonType button)) { checkButtonCallback = callback; } +void HydraCore::setCheckButtonCallback(s32 (*callback)(u32 player, hydra::ButtonType button)) { checkButtonCallback = callback; } -HC_API hydra::IBase* createEmulator() { return new HydraCore; } +u32 HydraCore::addCheat(const u8* data, u32 size) { + return emulator->getCheats().addCheat(data, size); +}; + +void HydraCore::removeCheat(u32 id) { emulator->getCheats().removeCheat(id); } +void HydraCore::enableCheat(u32 id) { emulator->getCheats().enableCheat(id); } +void HydraCore::disableCheat(u32 id) { emulator->getCheats().disableCheat(id); } + +HC_API hydra::IBase* createEmulator() { return new HydraCore(); } HC_API void destroyEmulator(hydra::IBase* emulator) { delete emulator; } HC_API const char* getInfo(hydra::InfoType type) { @@ -140,4 +161,4 @@ HC_API const char* getInfo(hydra::InfoType type) { default: return nullptr; } -} \ No newline at end of file +} diff --git a/src/jni_driver.cpp b/src/jni_driver.cpp new file mode 100644 index 00000000..d962f23e --- /dev/null +++ b/src/jni_driver.cpp @@ -0,0 +1,116 @@ +#include +#include +#include + +#include + +#include "emulator.hpp" +#include "renderer_gl/renderer_gl.hpp" +#include "services/hid.hpp" + +std::unique_ptr emulator = nullptr; +HIDService* hidService = nullptr; +RendererGL* renderer = nullptr; +bool romLoaded = false; +JavaVM* jvm = nullptr; + +#define AlberFunction(type, name) JNIEXPORT type JNICALL Java_com_panda3ds_pandroid_AlberDriver_##name + +void throwException(JNIEnv* env, const char* message) { + jclass exceptionClass = env->FindClass("java/lang/RuntimeException"); + env->ThrowNew(exceptionClass, message); +} + +JNIEnv* jniEnv() { + JNIEnv* env; + auto status = jvm->GetEnv((void**)&env, JNI_VERSION_1_6); + if (status == JNI_EDETACHED) { + jvm->AttachCurrentThread(&env, nullptr); + } else if (status != JNI_OK) { + throw std::runtime_error("Failed to obtain JNIEnv from JVM!!"); + } + + return env; +} + +extern "C" { + +#define MAKE_SETTING(functionName, type, settingName) \ +AlberFunction(void, functionName) (JNIEnv* env, jobject obj, type value) { emulator->getConfig().settingName = value; } + +MAKE_SETTING(setShaderJitEnabled, jboolean, shaderJitEnabled) + +#undef MAKE_SETTING + +AlberFunction(void, Setup)(JNIEnv* env, jobject obj) { env->GetJavaVM(&jvm); } +AlberFunction(void, Pause)(JNIEnv* env, jobject obj) { emulator->pause(); } +AlberFunction(void, Resume)(JNIEnv* env, jobject obj) { emulator->resume(); } + +AlberFunction(void, Initialize)(JNIEnv* env, jobject obj) { + emulator = std::make_unique(); + + if (emulator->getRendererType() != RendererType::OpenGL) { + return throwException(env, "Renderer type is not OpenGL"); + } + + renderer = static_cast(emulator->getRenderer()); + hidService = &emulator->getServiceManager().getHID(); + + if (!gladLoadGLES2Loader(reinterpret_cast(eglGetProcAddress))) { + return throwException(env, "Failed to load OpenGL ES 2.0"); + } + + __android_log_print(ANDROID_LOG_INFO, "AlberDriver", "OpenGL ES %d.%d", GLVersion.major, GLVersion.minor); + emulator->initGraphicsContext(nullptr); +} + +AlberFunction(void, RunFrame)(JNIEnv* env, jobject obj, jint fbo) { + renderer->setFBO(fbo); + // TODO: don't reset entire state manager + renderer->resetStateManager(); + emulator->runFrame(); + + hidService->updateInputs(emulator->getTicks()); +} + +AlberFunction(void, Finalize)(JNIEnv* env, jobject obj) { + emulator = nullptr; + hidService = nullptr; + renderer = nullptr; +} + +AlberFunction(jboolean, HasRomLoaded)(JNIEnv* env, jobject obj) { return romLoaded; } + +AlberFunction(void, LoadRom)(JNIEnv* env, jobject obj, jstring path) { + const char* pathStr = env->GetStringUTFChars(path, nullptr); + romLoaded = emulator->loadROM(pathStr); + env->ReleaseStringUTFChars(path, pathStr); +} + +AlberFunction(void, LoadLuaScript)(JNIEnv* env, jobject obj, jstring script) { + const char* scriptStr = env->GetStringUTFChars(script, nullptr); + emulator->getLua().loadString(scriptStr); + env->ReleaseStringUTFChars(script, scriptStr); +} + +AlberFunction(void, TouchScreenDown)(JNIEnv* env, jobject obj, jint x, jint y) { hidService->setTouchScreenPress((u16)x, (u16)y); } +AlberFunction(void, TouchScreenUp)(JNIEnv* env, jobject obj) { hidService->releaseTouchScreen(); } +AlberFunction(void, KeyUp)(JNIEnv* env, jobject obj, jint keyCode) { hidService->releaseKey((u32)keyCode); } +AlberFunction(void, KeyDown)(JNIEnv* env, jobject obj, jint keyCode) { hidService->pressKey((u32)keyCode); } + +AlberFunction(void, SetCirclepadAxis)(JNIEnv* env, jobject obj, jint x, jint y) { + hidService->setCirclepadX((s16)x); + hidService->setCirclepadY((s16)y); +} + +AlberFunction(jbyteArray, GetSmdh)(JNIEnv* env, jobject obj) { + std::span smdh = emulator->getSMDH(); + + jbyteArray result = env->NewByteArray(smdh.size()); + env->SetByteArrayRegion(result, 0, smdh.size(), (jbyte*)smdh.data()); + + return result; +} +} + +#undef AlberFunction diff --git a/src/lua.cpp b/src/lua.cpp index f7e0b719..ccfe955d 100644 --- a/src/lua.cpp +++ b/src/lua.cpp @@ -58,6 +58,31 @@ void LuaManager::loadFile(const char* path) { } } +void LuaManager::loadString(const std::string& code) { + // Initialize Lua if it has not been initialized + if (!initialized) { + initialize(); + } + + // If init failed, don't execute + if (!initialized) { + printf("Lua initialization failed, file won't run\n"); + haveScript = false; + + return; + } + + int status = luaL_loadstring(L, code.c_str()); // load Lua script + int ret = lua_pcall(L, 0, 0, 0); // tell Lua to run the script + + if (ret != 0) { + haveScript = false; + fprintf(stderr, "%s\n", lua_tostring(L, -1)); // tell us what mistake we made + } else { + haveScript = true; + } +} + void LuaManager::signalEventInternal(LuaEvent e) { lua_getglobal(L, "eventHandler"); // We want to call the event handler lua_pushnumber(L, static_cast(e)); // Push event type diff --git a/src/panda_qt/about_window.cpp b/src/panda_qt/about_window.cpp new file mode 100644 index 00000000..67767198 --- /dev/null +++ b/src/panda_qt/about_window.cpp @@ -0,0 +1,63 @@ +#include "panda_qt/about_window.hpp" + +#include +#include +#include +#include + +// Based on https://github.com/dolphin-emu/dolphin/blob/master/Source/Core/DolphinQt/AboutDialog.cpp + +AboutWindow::AboutWindow(QWidget* parent) : QDialog(parent) { + resize(200, 200); + + setWindowTitle(tr("About Panda3DS")); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + + const QString text = + QStringLiteral(R"( +

Panda3DS

+ +

+%ABOUT_PANDA3DS%
+%SUPPORT%
+

+ +

+%AUTHORS% +

+)") + .replace(QStringLiteral("%ABOUT_PANDA3DS%"), tr("Panda3DS is a free and open source Nintendo 3DS emulator, for Windows, MacOS and Linux")) + .replace(QStringLiteral("%SUPPORT%"), tr("Visit panda3ds.com for help with Panda3DS and links to our official support sites.")) + .replace( + QStringLiteral("%AUTHORS%"), tr("Panda3DS is developed by volunteers in their spare time. Below is a list of some of these" + " volunteers who've agreed to be listed here, in no particular order.
If you think you should be " + "listed here too, please inform us

" + "- Peach (wheremyfoodat)
" + "- noumidev
" + "- liuk707
" + "- Wunk
" + "- marysaka
" + "- Sky
" + "- merryhime
" + "- TGP17
" + "- Shadow
") + ); + + QLabel* textLabel = new QLabel(text); + textLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); + textLabel->setOpenExternalLinks(true); + + QLabel* logo = new QLabel(); + logo->setPixmap(QPixmap(":/docs/img/rstarstruck_icon.png")); + logo->setContentsMargins(30, 0, 30, 0); + + QVBoxLayout* mainLayout = new QVBoxLayout; + QHBoxLayout* hLayout = new QHBoxLayout; + + setLayout(mainLayout); + mainLayout->addLayout(hLayout); + + hLayout->setAlignment(Qt::AlignLeft); + hLayout->addWidget(logo); + hLayout->addWidget(textLabel); +} \ No newline at end of file diff --git a/src/panda_qt/cheats_window.cpp b/src/panda_qt/cheats_window.cpp new file mode 100644 index 00000000..dbd251cc --- /dev/null +++ b/src/panda_qt/cheats_window.cpp @@ -0,0 +1,268 @@ +#include "panda_qt/cheats_window.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "cheats.hpp" +#include "emulator.hpp" +#include "panda_qt/main_window.hpp" + +MainWindow* mainWindow = nullptr; + +struct CheatMetadata { + u32 handle = Cheats::badCheatHandle; + std::string name = "New cheat"; + std::string code; + bool enabled = true; +}; + +void dispatchToMainThread(std::function callback) { + QTimer* timer = new QTimer(); + timer->moveToThread(qApp->thread()); + timer->setSingleShot(true); + QObject::connect(timer, &QTimer::timeout, [=]() + { + callback(); + timer->deleteLater(); + }); + QMetaObject::invokeMethod(timer, "start", Qt::QueuedConnection, Q_ARG(int, 0)); +} + +class CheatEntryWidget : public QWidget { + public: + CheatEntryWidget(Emulator* emu, CheatMetadata metadata, QListWidget* parent); + + void Update() { + name->setText(metadata.name.c_str()); + enabled->setChecked(metadata.enabled); + update(); + } + + void Remove() { + emu->getCheats().removeCheat(metadata.handle); + cheatList->takeItem(cheatList->row(listItem)); + deleteLater(); + } + + const CheatMetadata& getMetadata() { return metadata; } + void setMetadata(const CheatMetadata& metadata) { this->metadata = metadata; } + + private: + void checkboxChanged(int state); + void editClicked(); + + Emulator* emu; + CheatMetadata metadata; + u32 handle; + QLabel* name; + QCheckBox* enabled; + QListWidget* cheatList; + QListWidgetItem* listItem; +}; + +class CheatEditDialog : public QDialog { + public: + CheatEditDialog(Emulator* emu, CheatEntryWidget& cheatEntry); + + void accepted(); + void rejected(); + + private: + Emulator* emu; + CheatEntryWidget& cheatEntry; + QTextEdit* codeEdit; + QLineEdit* nameEdit; +}; + +CheatEntryWidget::CheatEntryWidget(Emulator* emu, CheatMetadata metadata, QListWidget* parent) + : QWidget(), emu(emu), metadata(metadata), cheatList(parent) { + QHBoxLayout* layout = new QHBoxLayout; + + enabled = new QCheckBox; + enabled->setChecked(metadata.enabled); + + name = new QLabel(metadata.name.c_str()); + QPushButton* buttonEdit = new QPushButton(tr("Edit")); + + connect(enabled, &QCheckBox::stateChanged, this, &CheatEntryWidget::checkboxChanged); + connect(buttonEdit, &QPushButton::clicked, this, &CheatEntryWidget::editClicked); + + layout->addWidget(enabled); + layout->addWidget(name); + layout->addWidget(buttonEdit); + setLayout(layout); + + listItem = new QListWidgetItem; + listItem->setSizeHint(sizeHint()); + parent->addItem(listItem); + parent->setItemWidget(listItem, this); +} + +void CheatEntryWidget::checkboxChanged(int state) { + bool enabled = state == Qt::Checked; + if (metadata.handle == Cheats::badCheatHandle) { + printf("Cheat handle is bad, this shouldn't happen\n"); + return; + } + + if (enabled) { + emu->getCheats().enableCheat(metadata.handle); + metadata.enabled = true; + } else { + emu->getCheats().disableCheat(metadata.handle); + metadata.enabled = false; + } +} + +void CheatEntryWidget::editClicked() { + CheatEditDialog* dialog = new CheatEditDialog(emu, *this); + dialog->show(); +} + +CheatEditDialog::CheatEditDialog(Emulator* emu, CheatEntryWidget& cheatEntry) : QDialog(), emu(emu), cheatEntry(cheatEntry) { + setAttribute(Qt::WA_DeleteOnClose); + setModal(true); + + QVBoxLayout* layout = new QVBoxLayout; + const CheatMetadata& metadata = cheatEntry.getMetadata(); + codeEdit = new QTextEdit; + nameEdit = new QLineEdit; + nameEdit->setText(metadata.name.c_str()); + nameEdit->setPlaceholderText(tr("Cheat name")); + layout->addWidget(nameEdit); + + QFont font; + font.setFamily("Courier"); + font.setFixedPitch(true); + font.setPointSize(10); + codeEdit->setFont(font); + + if (metadata.code.size() != 0) { + // Nicely format it like so: + // 01234567 89ABCDEF + // 01234567 89ABCDEF + std::string formattedCode; + for (size_t i = 0; i < metadata.code.size(); i += 2) { + if (i != 0) { + if (i % 8 == 0 && i % 16 != 0) { + formattedCode += " "; + } else if (i % 16 == 0) { + formattedCode += "\n"; + } + } + + formattedCode += metadata.code[i]; + formattedCode += metadata.code[i + 1]; + } + codeEdit->setText(formattedCode.c_str()); + } + + layout->addWidget(codeEdit); + setLayout(layout); + + auto buttons = QDialogButtonBox::Ok | QDialogButtonBox::Cancel; + QDialogButtonBox* buttonBox = new QDialogButtonBox(buttons); + layout->addWidget(buttonBox); + + connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + connect(this, &QDialog::rejected, this, &CheatEditDialog::rejected); + connect(this, &QDialog::accepted, this, &CheatEditDialog::accepted); +} + +void CheatEditDialog::accepted() { + QString code = codeEdit->toPlainText(); + code.replace(QRegularExpression("[^0-9a-fA-F]"), ""); + + CheatMetadata metadata = cheatEntry.getMetadata(); + metadata.name = nameEdit->text().toStdString(); + metadata.code = code.toStdString(); + cheatEntry.setMetadata(metadata); + + std::vector bytes; + for (size_t i = 0; i < metadata.code.size(); i += 2) { + std::string hex = metadata.code.substr(i, 2); + bytes.push_back((u8)std::stoul(hex, nullptr, 16)); + } + + mainWindow->editCheat(cheatEntry.getMetadata().handle, bytes, [this](u32 handle) { + dispatchToMainThread([this, handle]() { + if (handle == Cheats::badCheatHandle) { + cheatEntry.Remove(); + return; + } else { + CheatMetadata metadata = cheatEntry.getMetadata(); + metadata.handle = handle; + cheatEntry.setMetadata(metadata); + cheatEntry.Update(); + } + }); + }); +} + +void CheatEditDialog::rejected() { + bool isEditing = cheatEntry.getMetadata().handle != Cheats::badCheatHandle; + if (!isEditing) { + // Was adding a cheat but user pressed cancel + cheatEntry.Remove(); + } +} + +CheatsWindow::CheatsWindow(Emulator* emu, const std::filesystem::path& cheatPath, QWidget* parent) + : QWidget(parent, Qt::Window), emu(emu), cheatPath(cheatPath) { + mainWindow = static_cast(parent); + + QVBoxLayout* layout = new QVBoxLayout; + layout->setContentsMargins(6, 6, 6, 6); + setLayout(layout); + + cheatList = new QListWidget; + layout->addWidget(cheatList); + + QWidget* buttonBox = new QWidget; + QHBoxLayout* buttonLayout = new QHBoxLayout; + + QPushButton* buttonAdd = new QPushButton(tr("Add")); + QPushButton* buttonRemove = new QPushButton(tr("Remove")); + + connect(buttonAdd, &QPushButton::clicked, this, &CheatsWindow::addEntry); + connect(buttonRemove, &QPushButton::clicked, this, &CheatsWindow::removeClicked); + + buttonLayout->addWidget(buttonAdd); + buttonLayout->addWidget(buttonRemove); + buttonBox->setLayout(buttonLayout); + + layout->addWidget(buttonBox); + + // TODO: load cheats from saved cheats per game + // for (const CheatMetadata& metadata : getSavedCheats()) + // { + // new CheatEntryWidget(emu, metadata, cheatList); + // } +} + +void CheatsWindow::addEntry() { + // CheatEntryWidget is added to the list when it's created + CheatEntryWidget* entry = new CheatEntryWidget(emu, {Cheats::badCheatHandle, "New cheat", "", true}, cheatList); + CheatEditDialog* dialog = new CheatEditDialog(emu, *entry); + dialog->show(); +} + +void CheatsWindow::removeClicked() { + QListWidgetItem* item = cheatList->currentItem(); + if (item == nullptr) { + return; + } + + CheatEntryWidget* entry = static_cast(cheatList->itemWidget(item)); + entry->Remove(); +} diff --git a/src/panda_qt/config_window.cpp b/src/panda_qt/config_window.cpp new file mode 100644 index 00000000..44debc32 --- /dev/null +++ b/src/panda_qt/config_window.cpp @@ -0,0 +1,99 @@ +#include "panda_qt/config_window.hpp" + +ConfigWindow::ConfigWindow(QWidget* parent) : QDialog(parent) { + setWindowTitle(tr("Configuration")); + + // Set up theme selection + setTheme(Theme::Dark); + themeSelect = new QComboBox(this); + themeSelect->addItem(tr("System")); + themeSelect->addItem(tr("Light")); + themeSelect->addItem(tr("Dark")); + themeSelect->addItem(tr("Greetings Cat")); + themeSelect->setCurrentIndex(static_cast(currentTheme)); + + themeSelect->setGeometry(40, 40, 100, 50); + themeSelect->show(); + connect(themeSelect, &QComboBox::currentIndexChanged, this, [&](int index) { setTheme(static_cast(index)); }); +} + +void ConfigWindow::setTheme(Theme theme) { + currentTheme = theme; + + switch (theme) { + case Theme::Dark: { + QApplication::setStyle(QStyleFactory::create("Fusion")); + + QPalette p; + p.setColor(QPalette::Window, QColor(53, 53, 53)); + p.setColor(QPalette::WindowText, Qt::white); + p.setColor(QPalette::Base, QColor(25, 25, 25)); + p.setColor(QPalette::AlternateBase, QColor(53, 53, 53)); + p.setColor(QPalette::ToolTipBase, Qt::white); + p.setColor(QPalette::ToolTipText, Qt::white); + p.setColor(QPalette::Text, Qt::white); + p.setColor(QPalette::Button, QColor(53, 53, 53)); + p.setColor(QPalette::ButtonText, Qt::white); + p.setColor(QPalette::BrightText, Qt::red); + p.setColor(QPalette::Link, QColor(42, 130, 218)); + + p.setColor(QPalette::Highlight, QColor(42, 130, 218)); + p.setColor(QPalette::HighlightedText, Qt::black); + qApp->setPalette(p); + break; + } + + case Theme::Light: { + QApplication::setStyle(QStyleFactory::create("Fusion")); + + QPalette p; + p.setColor(QPalette::Window, Qt::white); + p.setColor(QPalette::WindowText, Qt::black); + p.setColor(QPalette::Base, QColor(243, 243, 243)); + p.setColor(QPalette::AlternateBase, Qt::white); + p.setColor(QPalette::ToolTipBase, Qt::black); + p.setColor(QPalette::ToolTipText, Qt::black); + p.setColor(QPalette::Text, Qt::black); + p.setColor(QPalette::Button, Qt::white); + p.setColor(QPalette::ButtonText, Qt::black); + p.setColor(QPalette::BrightText, Qt::red); + p.setColor(QPalette::Link, QColor(42, 130, 218)); + + p.setColor(QPalette::Highlight, QColor(42, 130, 218)); + p.setColor(QPalette::HighlightedText, Qt::white); + qApp->setPalette(p); + break; + } + + case Theme::GreetingsCat: { + QApplication::setStyle(QStyleFactory::create("Fusion")); + + QPalette p; + p.setColor(QPalette::Window, QColor(250, 207, 228)); + p.setColor(QPalette::WindowText, QColor(225, 22, 137)); + p.setColor(QPalette::Base, QColor(250, 207, 228)); + p.setColor(QPalette::AlternateBase, QColor(250, 207, 228)); + p.setColor(QPalette::ToolTipBase, QColor(225, 22, 137)); + p.setColor(QPalette::ToolTipText, QColor(225, 22, 137)); + p.setColor(QPalette::Text, QColor(225, 22, 137)); + p.setColor(QPalette::Button, QColor(250, 207, 228)); + p.setColor(QPalette::ButtonText, QColor(225, 22, 137)); + p.setColor(QPalette::BrightText, Qt::black); + p.setColor(QPalette::Link, QColor(42, 130, 218)); + + p.setColor(QPalette::Highlight, QColor(42, 130, 218)); + p.setColor(QPalette::HighlightedText, Qt::black); + qApp->setPalette(p); + break; + } + + case Theme::System: { + qApp->setPalette(this->style()->standardPalette()); + qApp->setStyle(QStyleFactory::create("WindowsVista")); + qApp->setStyleSheet(""); + break; + } + } +} + +ConfigWindow::~ConfigWindow() { delete themeSelect; } \ No newline at end of file diff --git a/src/panda_qt/main_window.cpp b/src/panda_qt/main_window.cpp index 2c2cc64f..de70cc18 100644 --- a/src/panda_qt/main_window.cpp +++ b/src/panda_qt/main_window.cpp @@ -1,7 +1,13 @@ #include "panda_qt/main_window.hpp" +#include #include +#include +#include #include +#include + +#include "cheats.hpp" MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent), screen(this) { setWindowTitle("Alber"); @@ -20,32 +26,58 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent) auto fileMenu = menuBar->addMenu(tr("File")); auto emulationMenu = menuBar->addMenu(tr("Emulation")); auto toolsMenu = menuBar->addMenu(tr("Tools")); - auto helpMenu = menuBar->addMenu(tr("Help")); auto aboutMenu = menuBar->addMenu(tr("About")); // Create and bind actions for them - auto pandaAction = fileMenu->addAction(tr("panda...")); - connect(pandaAction, &QAction::triggered, this, &MainWindow::selectROM); + auto loadGameAction = fileMenu->addAction(tr("Load game")); + auto loadLuaAction = fileMenu->addAction(tr("Load Lua script")); + auto openAppFolderAction = fileMenu->addAction(tr("Open Panda3DS folder")); + + connect(loadGameAction, &QAction::triggered, this, &MainWindow::selectROM); + connect(loadLuaAction, &QAction::triggered, this, &MainWindow::selectLuaFile); + connect(openAppFolderAction, &QAction::triggered, this, [this]() { + QString path = QString::fromStdU16String(emu->getAppDataRoot().u16string()); + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); + }); + + auto pauseAction = emulationMenu->addAction(tr("Pause")); + auto resumeAction = emulationMenu->addAction(tr("Resume")); + auto resetAction = emulationMenu->addAction(tr("Reset")); + auto configureAction = emulationMenu->addAction(tr("Configure")); + connect(pauseAction, &QAction::triggered, this, [this]() { sendMessage(EmulatorMessage{.type = MessageType::Pause}); }); + connect(resumeAction, &QAction::triggered, this, [this]() { sendMessage(EmulatorMessage{.type = MessageType::Resume}); }); + connect(resetAction, &QAction::triggered, this, [this]() { sendMessage(EmulatorMessage{.type = MessageType::Reset}); }); + connect(configureAction, &QAction::triggered, this, [this]() { configWindow->show(); }); auto dumpRomFSAction = toolsMenu->addAction(tr("Dump RomFS")); + auto luaEditorAction = toolsMenu->addAction(tr("Open Lua Editor")); + auto cheatsEditorAction = toolsMenu->addAction(tr("Open Cheats Editor")); connect(dumpRomFSAction, &QAction::triggered, this, &MainWindow::dumpRomFS); + connect(luaEditorAction, &QAction::triggered, this, &MainWindow::openLuaEditor); + connect(cheatsEditorAction, &QAction::triggered, this, &MainWindow::openCheatsEditor); - // Set up theme selection - setTheme(Theme::Dark); - themeSelect = new QComboBox(this); - themeSelect->addItem(tr("System")); - themeSelect->addItem(tr("Light")); - themeSelect->addItem(tr("Dark")); - themeSelect->setCurrentIndex(static_cast(currentTheme)); - - themeSelect->setGeometry(40, 40, 100, 50); - themeSelect->show(); - connect(themeSelect, &QComboBox::currentIndexChanged, this, [&](int index) { setTheme(static_cast(index)); }); + auto aboutAction = aboutMenu->addAction(tr("About Panda3DS")); + connect(aboutAction, &QAction::triggered, this, &MainWindow::showAboutMenu); emu = new Emulator(); emu->setOutputSize(screen.surfaceWidth, screen.surfaceHeight); - // The emulator graphics context for the thread should be initialized in the emulator thread due to how GL contexts work + // Set up misc objects + aboutWindow = new AboutWindow(nullptr); + configWindow = new ConfigWindow(this); + cheatsEditor = new CheatsWindow(emu, {}, this); + luaEditor = new TextEditorWindow(this, "script.lua", ""); + + auto args = QCoreApplication::arguments(); + if (args.size() > 1) { + auto romPath = std::filesystem::current_path() / args.at(1).toStdU16String(); + if (!emu->loadROM(romPath)) { + // For some reason just .c_str() doesn't show the proper path + Helpers::warn("Failed to load ROM file: %s", romPath.string().c_str()); + } + } + + // The emulator graphics context for the thread should be initialized in the emulator thread due to how GL contexts work emuThread = std::thread([this]() { const RendererType rendererType = emu->getConfig().rendererType; usingGL = (rendererType == RendererType::OpenGL || rendererType == RendererType::Software || rendererType == RendererType::Null); @@ -73,17 +105,21 @@ void MainWindow::emuThreadMainLoop() { { std::unique_lock lock(messageQueueMutex); - if (needToLoadROM) { - needToLoadROM = false; - - bool success = emu->loadROM(romToLoad); - if (!success) { - printf("Failed to load ROM"); + // Dispatch all messages in the message queue + if (!messageQueue.empty()) { + for (const auto& msg : messageQueue) { + dispatchMessage(msg); } + + messageQueue.clear(); } } emu->runFrame(); + if (emu->romType != ROMType::None) { + emu->getServiceManager().getHID().updateInputs(emu->getTicks()); + } + swapEmuBuffer(); } @@ -102,94 +138,66 @@ void MainWindow::swapEmuBuffer() { } void MainWindow::selectROM() { - // Are we already waiting for a ROM to be loaded? Then complain about it! - { - std::unique_lock lock(messageQueueMutex); - if (needToLoadROM) { - QMessageBox::warning(this, tr("Already loading ROM"), tr("Panda3DS is already busy loading a ROM, please wait")); - return; - } - } - auto path = QFileDialog::getOpenFileName(this, tr("Select 3DS ROM to load"), "", tr("Nintendo 3DS ROMs (*.3ds *.cci *.cxi *.app *.3dsx *.elf *.axf)")); if (!path.isEmpty()) { - std::unique_lock lock(messageQueueMutex); + std::filesystem::path* p = new std::filesystem::path(path.toStdU16String()); - romToLoad = path.toStdU16String(); - needToLoadROM = true; + EmulatorMessage message{.type = MessageType::LoadROM}; + message.path.p = p; + sendMessage(message); + } +} + +void MainWindow::selectLuaFile() { + auto path = QFileDialog::getOpenFileName(this, tr("Select Lua script to load"), "", tr("Lua scripts (*.lua *.txt)")); + + if (!path.isEmpty()) { + std::ifstream file(std::filesystem::path(path.toStdU16String()), std::ios::in); + + if (file.fail()) { + printf("Failed to load selected lua file\n"); + return; + } + + // Read whole file into an std::string string + // Get file size, preallocate std::string to avoid furthermemory allocations + std::string code; + file.seekg(0, std::ios::end); + code.resize(file.tellg()); + + // Rewind and read the whole file + file.seekg(0, std::ios::beg); + file.read(&code[0], code.size()); + file.close(); + + loadLuaScript(code); + // Copy the Lua script to the Lua editor + luaEditor->setText(code); } } // Cleanup when the main window closes MainWindow::~MainWindow() { - appRunning = false; // Set our running atomic to false in order to make the emulator thread stop, and join it - + appRunning = false; // Set our running atomic to false in order to make the emulator thread stop, and join it + if (emuThread.joinable()) { emuThread.join(); } delete emu; delete menuBar; - delete themeSelect; + delete aboutWindow; + delete configWindow; + delete cheatsEditor; + delete luaEditor; } -void MainWindow::setTheme(Theme theme) { - currentTheme = theme; - - switch (theme) { - case Theme::Dark: { - QApplication::setStyle(QStyleFactory::create("Fusion")); - - QPalette p; - p.setColor(QPalette::Window, QColor(53, 53, 53)); - p.setColor(QPalette::WindowText, Qt::white); - p.setColor(QPalette::Base, QColor(25, 25, 25)); - p.setColor(QPalette::AlternateBase, QColor(53, 53, 53)); - p.setColor(QPalette::ToolTipBase, Qt::white); - p.setColor(QPalette::ToolTipText, Qt::white); - p.setColor(QPalette::Text, Qt::white); - p.setColor(QPalette::Button, QColor(53, 53, 53)); - p.setColor(QPalette::ButtonText, Qt::white); - p.setColor(QPalette::BrightText, Qt::red); - p.setColor(QPalette::Link, QColor(42, 130, 218)); - - p.setColor(QPalette::Highlight, QColor(42, 130, 218)); - p.setColor(QPalette::HighlightedText, Qt::black); - qApp->setPalette(p); - break; - } - - case Theme::Light: { - QApplication::setStyle(QStyleFactory::create("Fusion")); - - QPalette p; - p.setColor(QPalette::Window, Qt::white); - p.setColor(QPalette::WindowText, Qt::black); - p.setColor(QPalette::Base, QColor(243, 243, 243)); - p.setColor(QPalette::AlternateBase, Qt::white); - p.setColor(QPalette::ToolTipBase, Qt::black); - p.setColor(QPalette::ToolTipText, Qt::black); - p.setColor(QPalette::Text, Qt::black); - p.setColor(QPalette::Button, Qt::white); - p.setColor(QPalette::ButtonText, Qt::black); - p.setColor(QPalette::BrightText, Qt::red); - p.setColor(QPalette::Link, QColor(42, 130, 218)); - - p.setColor(QPalette::Highlight, QColor(42, 130, 218)); - p.setColor(QPalette::HighlightedText, Qt::white); - qApp->setPalette(p); - break; - } - - case Theme::System: { - qApp->setPalette(this->style()->standardPalette()); - qApp->setStyle(QStyleFactory::create("WindowsVista")); - qApp->setStyleSheet(""); - break; - } - } +// Send a message to the emulator thread. Lock the mutex and just push back to the vector. +void MainWindow::sendMessage(const EmulatorMessage& message) { + std::unique_lock lock(messageQueueMutex); + messageQueue.push_back(message); } void MainWindow::dumpRomFS() { @@ -201,14 +209,13 @@ void MainWindow::dumpRomFS() { return; } std::filesystem::path path(folder.toStdU16String()); - - // TODO: This might break if the game accesses RomFS while we're dumping, we should move it to the emulator thread when we've got a message queue going + messageQueueMutex.lock(); RomFS::DumpingResult res = emu->dumpRomFS(path); messageQueueMutex.unlock(); switch (res) { - case RomFS::DumpingResult::Success: break; // Yay! + case RomFS::DumpingResult::Success: break; // Yay! case RomFS::DumpingResult::InvalidFormat: { QMessageBox messageBox( QMessageBox::Icon::Warning, tr("Invalid format for RomFS dumping"), @@ -225,4 +232,186 @@ void MainWindow::dumpRomFS() { QMessageBox::warning(this, tr("No RomFS found"), tr("No RomFS partition was found in the loaded app")); break; } +} + +void MainWindow::showAboutMenu() { + AboutWindow about(this); + about.exec(); +} + +void MainWindow::openLuaEditor() { luaEditor->show(); } +void MainWindow::openCheatsEditor() { cheatsEditor->show(); } + +void MainWindow::dispatchMessage(const EmulatorMessage& message) { + switch (message.type) { + case MessageType::LoadROM: + emu->loadROM(*message.path.p); + // Clean up the allocated path + delete message.path.p; + break; + + case MessageType::LoadLuaScript: + emu->getLua().loadString(*message.string.str); + delete message.string.str; + break; + + case MessageType::EditCheat: { + u32 handle = message.cheat.c->handle; + const std::vector& cheat = message.cheat.c->cheat; + const std::function& callback = message.cheat.c->callback; + bool isEditing = handle != Cheats::badCheatHandle; + if (isEditing) { + emu->getCheats().removeCheat(handle); + u32 handle = emu->getCheats().addCheat(cheat.data(), cheat.size()); + } else { + u32 handle = emu->getCheats().addCheat(cheat.data(), cheat.size()); + callback(handle); + } + delete message.cheat.c; + } break; + + case MessageType::Pause: emu->pause(); break; + case MessageType::Resume: emu->resume(); break; + case MessageType::TogglePause: emu->togglePause(); break; + case MessageType::Reset: emu->reset(Emulator::ReloadOption::Reload); break; + case MessageType::PressKey: emu->getServiceManager().getHID().pressKey(message.key.key); break; + case MessageType::ReleaseKey: emu->getServiceManager().getHID().releaseKey(message.key.key); break; + case MessageType::SetCirclePadX: emu->getServiceManager().getHID().setCirclepadX(message.circlepad.value); break; + case MessageType::SetCirclePadY: emu->getServiceManager().getHID().setCirclepadY(message.circlepad.value); break; + case MessageType::PressTouchscreen: + emu->getServiceManager().getHID().setTouchScreenPress(message.touchscreen.x, message.touchscreen.y); + break; + case MessageType::ReleaseTouchscreen: emu->getServiceManager().getHID().releaseTouchScreen(); break; + } +} + +void MainWindow::keyPressEvent(QKeyEvent* event) { + auto pressKey = [this](u32 key) { + EmulatorMessage message{.type = MessageType::PressKey}; + message.key.key = key; + sendMessage(message); + }; + + auto setCirclePad = [this](MessageType type, s16 value) { + EmulatorMessage message{.type = type}; + message.circlepad.value = value; + sendMessage(message); + }; + + switch (event->key()) { + case Qt::Key_L: pressKey(HID::Keys::A); break; + case Qt::Key_K: pressKey(HID::Keys::B); break; + case Qt::Key_O: pressKey(HID::Keys::X); break; + case Qt::Key_I: pressKey(HID::Keys::Y); break; + + case Qt::Key_Q: pressKey(HID::Keys::L); break; + case Qt::Key_P: pressKey(HID::Keys::R); break; + + case Qt::Key_W: setCirclePad(MessageType::SetCirclePadY, 0x9C); break; + case Qt::Key_A: setCirclePad(MessageType::SetCirclePadX, -0x9C); break; + case Qt::Key_S: setCirclePad(MessageType::SetCirclePadY, -0x9C); break; + case Qt::Key_D: setCirclePad(MessageType::SetCirclePadX, 0x9C); break; + + case Qt::Key_Right: pressKey(HID::Keys::Right); break; + case Qt::Key_Left: pressKey(HID::Keys::Left); break; + case Qt::Key_Up: pressKey(HID::Keys::Up); break; + case Qt::Key_Down: pressKey(HID::Keys::Down); break; + + case Qt::Key_Return: pressKey(HID::Keys::Start); break; + case Qt::Key_Backspace: pressKey(HID::Keys::Select); break; + case Qt::Key_F4: sendMessage(EmulatorMessage{.type = MessageType::TogglePause}); break; + case Qt::Key_F5: sendMessage(EmulatorMessage{.type = MessageType::Reset}); break; + } +} + +void MainWindow::keyReleaseEvent(QKeyEvent* event) { + auto releaseKey = [this](u32 key) { + EmulatorMessage message{.type = MessageType::ReleaseKey}; + message.key.key = key; + sendMessage(message); + }; + + auto releaseCirclePad = [this](MessageType type) { + EmulatorMessage message{.type = type}; + message.circlepad.value = 0; + sendMessage(message); + }; + + switch (event->key()) { + case Qt::Key_L: releaseKey(HID::Keys::A); break; + case Qt::Key_K: releaseKey(HID::Keys::B); break; + case Qt::Key_O: releaseKey(HID::Keys::X); break; + case Qt::Key_I: releaseKey(HID::Keys::Y); break; + + case Qt::Key_Q: releaseKey(HID::Keys::L); break; + case Qt::Key_P: releaseKey(HID::Keys::R); break; + + case Qt::Key_W: + case Qt::Key_S: releaseCirclePad(MessageType::SetCirclePadY); break; + + case Qt::Key_A: + case Qt::Key_D: releaseCirclePad(MessageType::SetCirclePadX); break; + + case Qt::Key_Right: releaseKey(HID::Keys::Right); break; + case Qt::Key_Left: releaseKey(HID::Keys::Left); break; + case Qt::Key_Up: releaseKey(HID::Keys::Up); break; + case Qt::Key_Down: releaseKey(HID::Keys::Down); break; + + case Qt::Key_Return: releaseKey(HID::Keys::Start); break; + case Qt::Key_Backspace: releaseKey(HID::Keys::Select); break; + } +} + +void MainWindow::mousePressEvent(QMouseEvent* event) { + if (event->button() == Qt::MouseButton::LeftButton) { + const QPointF clickPos = event->globalPosition(); + const QPointF widgetPos = screen.mapFromGlobal(clickPos); + + // Press is inside the screen area + if (widgetPos.x() >= 0 && widgetPos.x() < screen.width() && widgetPos.y() >= 0 && widgetPos.y() < screen.height()) { + // Go from widget positions to [0, 400) for x and [0, 480) for y + uint x = (uint)std::round(widgetPos.x() / screen.width() * 400.f); + uint y = (uint)std::round(widgetPos.y() / screen.height() * 480.f); + + // Check if touch falls in the touch screen area + if (y >= 240 && y <= 480 && x >= 40 && x < 40 + 320) { + // Convert to 3DS coordinates + u16 x_converted = static_cast(x) - 40; + u16 y_converted = static_cast(y) - 240; + + EmulatorMessage message{.type = MessageType::PressTouchscreen}; + message.touchscreen.x = x_converted; + message.touchscreen.y = y_converted; + sendMessage(message); + } else { + sendMessage(EmulatorMessage{.type = MessageType::ReleaseTouchscreen}); + } + } + } +} + +void MainWindow::mouseReleaseEvent(QMouseEvent* event) { + if (event->button() == Qt::MouseButton::LeftButton) { + sendMessage(EmulatorMessage{.type = MessageType::ReleaseTouchscreen}); + } +} + +void MainWindow::loadLuaScript(const std::string& code) { + EmulatorMessage message{.type = MessageType::LoadLuaScript}; + + // Make a copy of the code on the heap to send via the message queue + message.string.str = new std::string(code); + sendMessage(message); +} + +void MainWindow::editCheat(u32 handle, const std::vector& cheat, const std::function& callback) { + EmulatorMessage message{.type = MessageType::EditCheat}; + + CheatMessage* c = new CheatMessage(); + c->handle = handle; + c->cheat = cheat; + c->callback = callback; + + message.cheat.c = c; + sendMessage(message); } \ No newline at end of file diff --git a/src/panda_qt/text_editor.cpp b/src/panda_qt/text_editor.cpp new file mode 100644 index 00000000..a31a829f --- /dev/null +++ b/src/panda_qt/text_editor.cpp @@ -0,0 +1,44 @@ +#include "panda_qt/text_editor.hpp" + +#include +#include + +#include "panda_qt/main_window.hpp" + +using namespace Zep; + +TextEditorWindow::TextEditorWindow(QWidget* parent, const std::string& filename, const std::string& initialText) + : QDialog(parent), zepWidget(this, qApp->applicationDirPath().toStdString(), fontSize) { + resize(600, 600); + + // Register our extensions + ZepRegressExCommand::Register(zepWidget.GetEditor()); + ZepReplExCommand::Register(zepWidget.GetEditor(), &replProvider); + + // Default to standard mode instead of vim mode, initialize text box + zepWidget.GetEditor().InitWithText(filename, initialText); + zepWidget.GetEditor().SetGlobalMode(Zep::ZepMode_Standard::StaticName()); + + // Layout for widgets + QVBoxLayout* mainLayout = new QVBoxLayout(); + setLayout(mainLayout); + + QPushButton* button = new QPushButton(tr("Load script"), this); + button->setFixedSize(100, 20); + + // When the Load Script button is pressed, send the current text to the MainWindow, which will upload it to the emulator's lua object + connect(button, &QPushButton::pressed, this, [this]() { + if (parentWidget()) { + auto buffer = zepWidget.GetEditor().GetMRUBuffer(); + const std::string text = buffer->GetBufferText(buffer->Begin(), buffer->End()); + + static_cast(parentWidget())->loadLuaScript(text); + } else { + // This should be unreachable, only here for safety purposes + printf("Text editor does not have any parent widget, click doesn't work :(\n"); + } + }); + + mainLayout->addWidget(button); + mainLayout->addWidget(&zepWidget); +} diff --git a/src/panda_qt/zep.cpp b/src/panda_qt/zep.cpp new file mode 100644 index 00000000..570f0e64 --- /dev/null +++ b/src/panda_qt/zep.cpp @@ -0,0 +1,2 @@ +#define ZEP_SINGLE_HEADER_BUILD +#include "zep.h" \ No newline at end of file diff --git a/src/pandroid/.gitignore b/src/pandroid/.gitignore new file mode 100644 index 00000000..aa724b77 --- /dev/null +++ b/src/pandroid/.gitignore @@ -0,0 +1,15 @@ +*.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 +local.properties diff --git a/src/pandroid/app/build.gradle.kts b/src/pandroid/app/build.gradle.kts new file mode 100644 index 00000000..201d5db1 --- /dev/null +++ b/src/pandroid/app/build.gradle.kts @@ -0,0 +1,56 @@ +plugins { + id("com.android.application") +} + +android { + namespace = "com.panda3ds.pandroid" + compileSdk = 33 + + defaultConfig { + applicationId = "com.panda3ds.pandroid" + minSdk = 24 + targetSdk = 33 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + ndk { + abiFilters += listOf("x86_64", "arm64-v8a") + } + } + + buildTypes { + getByName("release") { + isMinifyEnabled = false + isShrinkResources = false + isDebuggable = false + signingConfig = signingConfigs.getByName("debug") + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + getByName("debug") { + isMinifyEnabled = false + isShrinkResources = false + isDebuggable = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } +} + +dependencies { + implementation("androidx.appcompat:appcompat:1.6.1") + implementation("com.google.android.material:material:1.8.0") + implementation("androidx.preference:preference:1.2.1") + implementation("androidx.constraintlayout:constraintlayout:2.1.4") + implementation("com.google.code.gson:gson:2.10.1") +} diff --git a/src/pandroid/app/proguard-rules.pro b/src/pandroid/app/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/src/pandroid/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 \ No newline at end of file diff --git a/src/pandroid/app/src/main/AndroidManifest.xml b/src/pandroid/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..c66d37af --- /dev/null +++ b/src/pandroid/app/src/main/AndroidManifest.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pandroid/app/src/main/assets/fonts/comic_mono.ttf b/src/pandroid/app/src/main/assets/fonts/comic_mono.ttf new file mode 100644 index 00000000..9bc7354e Binary files /dev/null and b/src/pandroid/app/src/main/assets/fonts/comic_mono.ttf differ diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/AlberDriver.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/AlberDriver.java new file mode 100644 index 00000000..00b7842b --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/AlberDriver.java @@ -0,0 +1,28 @@ +package com.panda3ds.pandroid; + +import android.util.Log; + +public class AlberDriver { + AlberDriver() { super(); } + + public static native void Setup(); + public static native void Initialize(); + public static native void RunFrame(int fbo); + public static native boolean HasRomLoaded(); + public static native void LoadRom(String path); + public static native void Finalize(); + + public static native void KeyDown(int code); + public static native void KeyUp(int code); + public static native void SetCirclepadAxis(int x, int y); + public static native void TouchScreenUp(); + public static native void TouchScreenDown(int x, int y); + public static native void Pause(); + public static native void Resume(); + public static native void LoadLuaScript(String script); + public static native byte[] GetSmdh(); + + public static native void setShaderJitEnabled(boolean enable); + + static { System.loadLibrary("Alber"); } +} \ No newline at end of file diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/BaseActivity.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/BaseActivity.java new file mode 100644 index 00000000..56c82d96 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/BaseActivity.java @@ -0,0 +1,32 @@ +package com.panda3ds.pandroid.app; + +import android.os.Bundle; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.data.config.GlobalConfig; + + +public class BaseActivity extends AppCompatActivity { + private int currentTheme = PandroidApplication.getThemeId(); + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + applyTheme(); + super.onCreate(savedInstanceState); + } + + @Override + protected void onResume() { + super.onResume(); + + if (PandroidApplication.getThemeId() != currentTheme) { + recreate(); + } + } + + private void applyTheme() { + currentTheme = PandroidApplication.getThemeId(); + setTheme(currentTheme); + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/GameActivity.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/GameActivity.java new file mode 100644 index 00000000..f7050e99 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/GameActivity.java @@ -0,0 +1,120 @@ +package com.panda3ds.pandroid.app; + +import android.content.Intent; +import android.os.Bundle; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.CheckBox; +import android.widget.FrameLayout; +import android.widget.Toast; +import androidx.annotation.Nullable; +import com.panda3ds.pandroid.AlberDriver; +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.app.game.AlberInputListener; +import com.panda3ds.pandroid.app.game.DrawerFragment; +import com.panda3ds.pandroid.data.config.GlobalConfig; +import com.panda3ds.pandroid.input.InputHandler; +import com.panda3ds.pandroid.input.InputMap; +import com.panda3ds.pandroid.utils.Constants; +import com.panda3ds.pandroid.view.PandaGlSurfaceView; +import com.panda3ds.pandroid.view.PandaLayoutController; +import com.panda3ds.pandroid.view.utils.PerformanceView; + +public class GameActivity extends BaseActivity { + private final DrawerFragment drawerFragment = new DrawerFragment(); + private final AlberInputListener inputListener = new AlberInputListener(this::onBackPressed); + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Intent intent = getIntent(); + if (!intent.hasExtra(Constants.ACTIVITY_PARAMETER_PATH)) { + setContentView(new FrameLayout(this)); + Toast.makeText(this, "Invalid rom path!", Toast.LENGTH_LONG).show(); + finish(); + return; + } + + PandaGlSurfaceView pandaSurface = new PandaGlSurfaceView(this, intent.getStringExtra(Constants.ACTIVITY_PARAMETER_PATH)); + setContentView(R.layout.game_activity); + + ((FrameLayout) findViewById(R.id.panda_gl_frame)) + .addView(pandaSurface, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + PandaLayoutController controllerLayout = findViewById(R.id.controller_layout); + controllerLayout.initialize(); + + ((CheckBox) findViewById(R.id.hide_screen_controller)).setOnCheckedChangeListener((buttonView, checked) -> { + findViewById(R.id.overlay_controller).setVisibility(checked ? View.VISIBLE : View.GONE); + findViewById(R.id.overlay_controller).invalidate(); + findViewById(R.id.overlay_controller).requestLayout(); + GlobalConfig.set(GlobalConfig.KEY_SCREEN_GAMEPAD_VISIBLE, checked); + }); + ((CheckBox) findViewById(R.id.hide_screen_controller)).setChecked(GlobalConfig.get(GlobalConfig.KEY_SCREEN_GAMEPAD_VISIBLE)); + + getSupportFragmentManager().beginTransaction().replace(R.id.drawer_fragment, drawerFragment).commitNow(); + + if (GlobalConfig.get(GlobalConfig.KEY_SHOW_PERFORMANCE_OVERLAY)) { + PerformanceView view = new PerformanceView(this); + ((FrameLayout) findViewById(R.id.panda_gl_frame)).addView(view, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + } + } + + @Override + protected void onResume() { + super.onResume(); + getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + InputHandler.reset(); + InputHandler.setMotionDeadZone(InputMap.getDeadZone()); + InputHandler.setEventListener(inputListener); + } + + @Override + protected void onPause() { + super.onPause(); + + InputHandler.reset(); + drawerFragment.open(); + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if ((!drawerFragment.isOpened()) && InputHandler.processKeyEvent(event)) { + return true; + } + + return super.dispatchKeyEvent(event); + } + + @Override + public void onBackPressed() { + if (drawerFragment.isOpened()) { + drawerFragment.close(); + } else { + drawerFragment.open(); + } + } + + @Override + public boolean dispatchGenericMotionEvent(MotionEvent ev) { + if ((!drawerFragment.isOpened()) && InputHandler.processMotionEvent(ev)) { + return true; + } + + return super.dispatchGenericMotionEvent(ev); + } + + @Override + protected void onDestroy() { + if (AlberDriver.HasRomLoaded()) { + AlberDriver.Finalize(); + } + + super.onDestroy(); + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/MainActivity.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/MainActivity.java new file mode 100644 index 00000000..18914a80 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/MainActivity.java @@ -0,0 +1,72 @@ +package com.panda3ds.pandroid.app; + +import static android.Manifest.permission.READ_EXTERNAL_STORAGE; +import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE; +import static android.provider.Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION; + +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.view.MenuItem; +import androidx.annotation.NonNull; +import androidx.core.app.ActivityCompat; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import com.google.android.material.navigation.NavigationBarView; +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.app.editor.CodeEditorActivity; +import com.panda3ds.pandroid.app.main.GamesFragment; +import com.panda3ds.pandroid.app.main.SearchFragment; +import com.panda3ds.pandroid.app.main.SettingsFragment; + +import java.io.File; + + +public class MainActivity extends BaseActivity implements NavigationBarView.OnItemSelectedListener { + private static final int PICK_ROM = 2; + private static final int PERMISSION_REQUEST_CODE = 3; + + private final GamesFragment gamesFragment = new GamesFragment(); + private final SearchFragment searchFragment = new SearchFragment(); + private final SettingsFragment settingsFragment = new SettingsFragment(); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + if (!Environment.isExternalStorageManager()) { + Intent intent = new Intent(ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION); + startActivity(intent); + } + } else { + ActivityCompat.requestPermissions(this, new String[] {READ_EXTERNAL_STORAGE}, PERMISSION_REQUEST_CODE); + ActivityCompat.requestPermissions(this, new String[] {WRITE_EXTERNAL_STORAGE}, PERMISSION_REQUEST_CODE); + } + + setContentView(R.layout.activity_main); + + NavigationBarView bar = findViewById(R.id.navigation); + bar.setOnItemSelectedListener(this); + bar.postDelayed(() -> bar.setSelectedItemId(bar.getSelectedItemId()), 5); + } + + @Override + public boolean onNavigationItemSelected(@NonNull MenuItem item) { + int id = item.getItemId(); + FragmentManager manager = getSupportFragmentManager(); + Fragment fragment; + if (id == R.id.games) { + fragment = gamesFragment; + } else if (id == R.id.search) { + fragment = searchFragment; + } else if (id == R.id.settings) { + fragment = settingsFragment; + } else { + return false; + } + + manager.beginTransaction().replace(R.id.fragment_container, fragment).commitNow(); + return true; + } +} \ No newline at end of file diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/PandroidApplication.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/PandroidApplication.java new file mode 100644 index 00000000..b0cdc935 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/PandroidApplication.java @@ -0,0 +1,63 @@ +package com.panda3ds.pandroid.app; + +import android.app.Application; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.content.res.Resources; + +import com.panda3ds.pandroid.AlberDriver; +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.app.services.LoggerService; +import com.panda3ds.pandroid.data.config.GlobalConfig; +import com.panda3ds.pandroid.input.InputMap; +import com.panda3ds.pandroid.utils.GameUtils; + + +public class PandroidApplication extends Application { + private static Context appContext; + + @Override + public void onCreate() { + super.onCreate(); + appContext = this; + + GlobalConfig.initialize(); + GameUtils.initialize(); + InputMap.initialize(); + AlberDriver.Setup(); + + if (GlobalConfig.get(GlobalConfig.KEY_LOGGER_SERVICE)) { + startService(new Intent(this, LoggerService.class)); + } + } + + public static int getThemeId() { + switch (GlobalConfig.get(GlobalConfig.KEY_APP_THEME)) { + case GlobalConfig.THEME_LIGHT: + return R.style.Theme_Pandroid_Light; + case GlobalConfig.THEME_DARK: + return R.style.Theme_Pandroid_Dark; + case GlobalConfig.THEME_BLACK: + return R.style.Theme_Pandroid_Black; + } + + return R.style.Theme_Pandroid; + } + + public static boolean isDarkMode() { + switch (GlobalConfig.get(GlobalConfig.KEY_APP_THEME)) { + case GlobalConfig.THEME_DARK: + case GlobalConfig.THEME_BLACK: + return true; + case GlobalConfig.THEME_LIGHT: + return false; + } + + Resources res = Resources.getSystem(); + int nightFlags = res.getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; + return nightFlags == Configuration.UI_MODE_NIGHT_YES; + } + + public static Context getAppContext() { return appContext; } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/PreferenceActivity.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/PreferenceActivity.java new file mode 100644 index 00000000..9bc9900b --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/PreferenceActivity.java @@ -0,0 +1,59 @@ +package com.panda3ds.pandroid.app; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.view.MenuItem; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.utils.Constants; + +public class PreferenceActivity extends BaseActivity { + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Intent intent = getIntent(); + + setContentView(R.layout.activity_preference); + setSupportActionBar(findViewById(R.id.toolbar)); + + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + if (!intent.hasExtra(Constants.ACTIVITY_PARAMETER_FRAGMENT)) { + finish(); + return; + } + + try { + Class clazz = getClassLoader().loadClass(intent.getStringExtra(Constants.ACTIVITY_PARAMETER_FRAGMENT)); + Fragment fragment = (Fragment) clazz.newInstance(); + fragment.setArguments(intent.getExtras()); + getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container, fragment).commitNow(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static void launch(Context context, Class clazz) { + launch(context, clazz, new Intent()); + } + + public static void launch(Context context, Class clazz, Intent extras) { + context.startActivity(new Intent(context, PreferenceActivity.class) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .putExtras(extras) + .putExtra(Constants.ACTIVITY_PARAMETER_FRAGMENT, clazz.getName())); + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + if (item.getItemId() == android.R.id.home) { + finish(); + } + + return super.onOptionsItemSelected(item); + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/base/BasePreferenceFragment.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/base/BasePreferenceFragment.java new file mode 100644 index 00000000..9482df1d --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/base/BasePreferenceFragment.java @@ -0,0 +1,27 @@ +package com.panda3ds.pandroid.app.base; + +import android.annotation.SuppressLint; + +import androidx.annotation.StringRes; +import androidx.appcompat.app.AppCompatActivity; +import androidx.preference.Preference; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.SwitchPreference; + +import com.panda3ds.pandroid.lang.Function; + + +public abstract class BasePreferenceFragment extends PreferenceFragmentCompat { + @SuppressLint("RestrictedApi") + protected void setItemClick(String key, Function listener) { + findPreference(key).setOnPreferenceClickListener(preference -> { + listener.run(preference); + getPreferenceScreen().performClick(); + return false; + }); + } + + protected void setActivityTitle(@StringRes int titleId) { + ((AppCompatActivity) requireActivity()).getSupportActionBar().setTitle(titleId); + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/base/BottomAlertDialog.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/base/BottomAlertDialog.java new file mode 100644 index 00000000..7c5470f1 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/base/BottomAlertDialog.java @@ -0,0 +1,57 @@ +package com.panda3ds.pandroid.app.base; + +import android.content.Context; +import android.view.Gravity; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.widget.AppCompatEditText; +import androidx.appcompat.widget.LinearLayoutCompat; + +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.lang.Function; + +public class BottomAlertDialog extends AlertDialog.Builder { + private final LinearLayoutCompat layoutCompat; + + public BottomAlertDialog(@NonNull Context context) { + super(context, R.style.AlertDialog); + layoutCompat = new LinearLayoutCompat(context); + layoutCompat.setOrientation(LinearLayoutCompat.VERTICAL); + + int padding = getContext().getResources().getDimensionPixelSize(androidx.appcompat.R.dimen.abc_dialog_padding_material); + layoutCompat.setPadding(padding, 0, padding, 0); + + setView(layoutCompat); + } + + @NonNull + @Override + public AlertDialog create() { + AlertDialog dialog = super.create(); + dialog.getWindow().setGravity(Gravity.BOTTOM | Gravity.CENTER); + dialog.getWindow().getAttributes().y = Math.round(getContext().getResources().getDisplayMetrics().density * 15); + return dialog; + } + + public BottomAlertDialog setTextInput(String hint, Function listener) { + AppCompatEditText edit = new AppCompatEditText(getContext()); + edit.setHint(hint); + int margin = layoutCompat.getPaddingLeft() / 2; + LinearLayoutCompat.LayoutParams params = new LinearLayoutCompat.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + params.setMargins(0, margin, 0, margin); + layoutCompat.addView(edit, params); + setPositiveButton(android.R.string.ok, (dialog, which) -> listener.run(String.valueOf(edit.getText()))); + setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.dismiss()); + return this; + } + + @Override + public AlertDialog show() { + AlertDialog dialog = create(); + dialog.show(); + + return dialog; + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/base/BottomDialogFragment.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/base/BottomDialogFragment.java new file mode 100644 index 00000000..4e54dc61 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/base/BottomDialogFragment.java @@ -0,0 +1,28 @@ +package com.panda3ds.pandroid.app.base; + +import android.app.Dialog; +import android.os.Bundle; +import android.view.Gravity; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; + +import com.panda3ds.pandroid.R; + +public class BottomDialogFragment extends DialogFragment { + @Override + public int getTheme() { + return R.style.AlertDialog; + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + Dialog dialog = super.onCreateDialog(savedInstanceState); + dialog.getWindow().setGravity(Gravity.CENTER | Gravity.BOTTOM); + dialog.getWindow().getAttributes().y = Math.round(getContext().getResources().getDisplayMetrics().density * 15); + + return dialog; + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/editor/CodeEditorActivity.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/editor/CodeEditorActivity.java new file mode 100644 index 00000000..e5ced4b4 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/editor/CodeEditorActivity.java @@ -0,0 +1,196 @@ +package com.panda3ds.pandroid.app.editor; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.graphics.Rect; +import android.os.Bundle; +import android.view.KeyEvent; +import android.view.View; +import android.view.inputmethod.InputMethodManager; + +import androidx.activity.result.contract.ActivityResultContract; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatTextView; + +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.app.BaseActivity; +import com.panda3ds.pandroid.app.base.BottomAlertDialog; +import com.panda3ds.pandroid.lang.Task; +import com.panda3ds.pandroid.utils.FileUtils; +import com.panda3ds.pandroid.view.code.CodeEditor; +import com.panda3ds.pandroid.view.code.syntax.CodeSyntax; + +import java.io.Serializable; + +public class CodeEditorActivity extends BaseActivity { + private static final String TAB = " "; + private String path; + private String fileName; + private CodeEditor editor; + private AppCompatTextView title; + private View saveButton; + private boolean changed = false; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_code_editor); + Arguments args = (Arguments) getIntent().getSerializableExtra("args"); + + editor = findViewById(R.id.editor); + getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(this::onGlobalLayoutChanged); + + path = args.path; + fileName = args.fileName; + title = findViewById(R.id.title); + title.setText(fileName); + + saveButton = findViewById(R.id.save); + + saveButton.setVisibility(View.GONE); + saveButton.setOnClickListener(v -> save()); + + new Task(() -> { + String content = FileUtils.readTextFile(path + "/" + fileName); + + editor.post(() -> { + editor.setText(content); + editor.setSyntax(CodeSyntax.getFromFilename(fileName)); + editor.setOnContentChangedListener(this::onDocumentContentChanged); + }); + }).start(); + + switch (args.type) { + case LUA_SCRIPT_EDITOR: + setupLuaPatchEditor(); + break; + case READ_ONLY_EDITOR: + setupReadOnlyEditor(); + break; + } + + onGlobalLayoutChanged(); + + findViewById(R.id.key_hide).setOnClickListener(v -> { + ((InputMethodManager) getSystemService(INPUT_METHOD_SERVICE)).hideSoftInputFromWindow(v.getWindowToken(), 0); + }); + findViewById(R.id.key_tab).setOnClickListener(v -> { + editor.insert(TAB); + }); + } + + // Detect virtual keyboard is visible + private void onGlobalLayoutChanged() { + View view = getWindow().getDecorView(); + Rect rect = new Rect(); + view.getWindowVisibleDisplayFrame(rect); + int currentHeight = rect.height(); + int height = view.getHeight(); + + if (currentHeight < height * 0.8) { + findViewById(R.id.keybar).setVisibility(View.VISIBLE); + } else { + findViewById(R.id.keybar).setVisibility(View.GONE); + } + } + + private void setupReadOnlyEditor() { + editor.setEnabled(false); + editor.setFocusable(false); + } + + private void setupLuaPatchEditor() { + findViewById(R.id.lua_toolbar).setVisibility(View.VISIBLE); + findViewById(R.id.lua_play).setOnClickListener(v -> { + if (changed) { + save(); + } + setResult(Activity.RESULT_OK, new Intent(Result.ACTION_PLAY.name())); + finish(); + }); + } + + @SuppressLint("SetTextI18n") + private void onDocumentContentChanged() { + changed = true; + + title.setText("*" + fileName); + saveButton.setVisibility(View.VISIBLE); + } + + public void save() { + title.setText(fileName); + saveButton.setVisibility(View.GONE); + + changed = false; + new Task(() -> FileUtils.writeTextFile(path, fileName, String.valueOf(editor.getText()))).runSync(); + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if (event.getKeyCode() == KeyEvent.KEYCODE_TAB) { + if (event.getAction() == KeyEvent.ACTION_UP) { + editor.insert(TAB); + } + + return true; + } + + return super.dispatchKeyEvent(event); + } + + @Override + public void onBackPressed() { + if (changed) { + new BottomAlertDialog(this) + .setNeutralButton(android.R.string.cancel, (dialog, which) -> dialog.dismiss()) + .setPositiveButton(R.string.save_and_exit, (dialog, which) -> { + save(); + finish(); + }) + .setNegativeButton(R.string.exit_without_saving, (dialog, which) -> finish()) + .setTitle(String.format(getString(R.string.exit_without_saving_title_ff), fileName)).show(); + } else { + super.onBackPressed(); + } + } + + public static final class Arguments implements Serializable { + private final String path; + private final String fileName; + private final EditorType type; + + public Arguments(String path, String fileName, EditorType type) { + this.path = path; + this.fileName = fileName; + this.type = type; + } + } + + public enum Result { + ACTION_PLAY, + NULL + } + + public enum EditorType { + LUA_SCRIPT_EDITOR, + READ_ONLY_EDITOR, + TEXT_EDITOR + } + + public static final class Contract extends ActivityResultContract { + @NonNull + @Override + public Intent createIntent(@NonNull Context context, Arguments args) { + return new Intent(context, CodeEditorActivity.class).putExtra("args", args); + } + + @Override + public Result parseResult(int i, @Nullable Intent intent) { + return i == RESULT_OK && intent != null ? Result.valueOf(intent.getAction()) : Result.NULL; + } + } +} \ No newline at end of file diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/game/AlberInputListener.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/game/AlberInputListener.java new file mode 100644 index 00000000..ec15a9bb --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/game/AlberInputListener.java @@ -0,0 +1,64 @@ +package com.panda3ds.pandroid.app.game; + +import com.panda3ds.pandroid.AlberDriver; +import com.panda3ds.pandroid.input.InputEvent; +import com.panda3ds.pandroid.input.InputMap; +import com.panda3ds.pandroid.input.KeyName; +import com.panda3ds.pandroid.lang.Function; +import com.panda3ds.pandroid.math.Vector2; + +import java.util.Objects; + + +public class AlberInputListener implements Function { + private final Runnable backListener; + public AlberInputListener(Runnable backListener) { this.backListener = backListener; } + + private final Vector2 axis = new Vector2(0.0f, 0.0f); + + @Override + public void run(InputEvent event) { + KeyName key = InputMap.relative(event.getName()); + + if (Objects.equals(event.getName(), "KEYCODE_BACK")) { + backListener.run(); + return; + } + + if (key == KeyName.NULL) { + return; + } + + boolean axisChanged = false; + + switch (key) { + case AXIS_UP: + axis.y = event.getValue(); + axisChanged = true; + break; + case AXIS_DOWN: + axis.y = -event.getValue(); + axisChanged = true; + break; + case AXIS_LEFT: + axis.x = -event.getValue(); + axisChanged = true; + break; + case AXIS_RIGHT: + axis.x = event.getValue(); + axisChanged = true; + break; + default: + if (event.isDown()) { + AlberDriver.KeyDown(key.getKeyId()); + } else { + AlberDriver.KeyUp(key.getKeyId()); + } + break; + } + + if (axisChanged) { + AlberDriver.SetCirclepadAxis(Math.round(axis.x * 0x9C), Math.round(axis.y * 0x9C)); + } + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/game/DrawerFragment.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/game/DrawerFragment.java new file mode 100644 index 00000000..bd402b52 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/game/DrawerFragment.java @@ -0,0 +1,117 @@ +package com.panda3ds.pandroid.app.game; + +import android.graphics.Color; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatTextView; +import androidx.drawerlayout.widget.DrawerLayout; +import androidx.fragment.app.Fragment; + +import com.google.android.material.navigation.NavigationView; +import com.panda3ds.pandroid.AlberDriver; +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.data.game.GameMetadata; +import com.panda3ds.pandroid.utils.GameUtils; +import com.panda3ds.pandroid.view.gamesgrid.GameIconView; + +public class DrawerFragment extends Fragment implements DrawerLayout.DrawerListener, NavigationView.OnNavigationItemSelectedListener { + private DrawerLayout drawerContainer; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + drawerContainer = requireActivity().findViewById(R.id.drawer_container); + drawerContainer.removeDrawerListener(this); + drawerContainer.addDrawerListener(this); + drawerContainer.setScrimColor(Color.argb(160, 0,0,0)); + drawerContainer.setVisibility(View.GONE); + + return inflater.inflate(R.layout.fragment_game_drawer, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + drawerContainer.setVisibility(View.GONE); + + GameMetadata game = GameUtils.getCurrentGame(); + + ((GameIconView)view.findViewById(R.id.game_icon)).setImageBitmap(game.getIcon()); + ((AppCompatTextView)view.findViewById(R.id.game_title)).setText(game.getTitle()); + ((AppCompatTextView)view.findViewById(R.id.game_publisher)).setText(game.getPublisher()); + + ((NavigationView)view.findViewById(R.id.action_navigation)).setNavigationItemSelectedListener(this); + ((NavigationView)view.findViewById(R.id.others_navigation)).setNavigationItemSelectedListener(this); + } + + @Override + public void onDetach() { + if (drawerContainer != null) { + drawerContainer.removeDrawerListener(this); + } + + super.onDetach(); + } + + private void refreshLayout() { + drawerContainer.measure(View.MeasureSpec.EXACTLY, View.MeasureSpec.EXACTLY); + drawerContainer.requestLayout(); + drawerContainer.invalidate(); + drawerContainer.forceLayout(); + } + + public void open() { + if (!drawerContainer.isOpen()) { + drawerContainer.setVisibility(View.VISIBLE); + drawerContainer.open(); + drawerContainer.postDelayed(this::refreshLayout, 20); + } + } + + public void close() { + if (drawerContainer.isOpen()) { + drawerContainer.close(); + } + } + + @Override + public void onDrawerSlide(@NonNull View drawerView, float slideOffset) {} + + @Override + public void onDrawerOpened(@NonNull View drawerView) { + AlberDriver.Pause(); + } + + @Override + public void onDrawerClosed(@NonNull View drawerView) { + drawerContainer.setVisibility(View.GONE); + AlberDriver.Resume(); + } + + @Override + public void onDrawerStateChanged(int newState) {} + + @Override + public boolean onNavigationItemSelected(@NonNull MenuItem item) { + int id = item.getItemId(); + if (id == R.id.resume) { + close(); + } else if (id == R.id.exit) { + requireActivity().finish(); + } else if (id == R.id.lua_script){ + new LuaDialogFragment().show(getParentFragmentManager(), null); + } + + return false; + } + + public boolean isOpened() { + return drawerContainer.isOpen(); + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/game/LuaDialogFragment.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/game/LuaDialogFragment.java new file mode 100644 index 00000000..1db9f9c7 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/game/LuaDialogFragment.java @@ -0,0 +1,185 @@ +package com.panda3ds.pandroid.app.game; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +import com.panda3ds.pandroid.AlberDriver; +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.app.base.BottomAlertDialog; +import com.panda3ds.pandroid.app.base.BottomDialogFragment; +import com.panda3ds.pandroid.app.editor.CodeEditorActivity; +import com.panda3ds.pandroid.lang.Task; +import com.panda3ds.pandroid.utils.FileUtils; +import com.panda3ds.pandroid.view.recycler.AutoFitGridLayout; +import com.panda3ds.pandroid.view.recycler.SimpleListAdapter; + +import java.util.ArrayList; +import java.util.UUID; + +public class LuaDialogFragment extends BottomDialogFragment { + private final SimpleListAdapter adapter = new SimpleListAdapter<>(R.layout.holder_lua_script, this::onCreateListItem); + private ActivityResultLauncher codeEditorLauncher; + private LuaFile currentEditorFile; + + private ActivityResultLauncher openDocumentLauncher; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.dialog_lua_scripts, container, false); + } + + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + openDocumentLauncher = registerForActivityResult(new ActivityResultContracts.OpenDocument(), result -> { + if (result != null) { + String fileName = FileUtils.getName(result.toString()); + + if (fileName.toLowerCase().endsWith(".lua")) { + new Task(() -> { + String content = FileUtils.readTextFile(result.toString()); + createFile(FileUtils.getName(result.toString()), content); + }).start(); + } else { + Toast.makeText(getContext(), R.string.file_not_supported, Toast.LENGTH_SHORT).show(); + } + } + }); + + codeEditorLauncher = registerForActivityResult(new CodeEditorActivity.Contract(), result -> { + if (result != null) { + switch (result) { + case ACTION_PLAY: + loadScript(currentEditorFile); + break; + } + } + + orderByModified(); + }); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + view.findViewById(R.id.open_file).setOnClickListener(v -> { + openDocumentLauncher.launch(new String[]{"*/*"}); + }); + view.findViewById(R.id.create).setOnClickListener(v -> { + new BottomAlertDialog(requireContext()) + .setTextInput(getString(R.string.name), arg -> { + String name = arg.trim(); + if (name.length() > 1) { + new Task(() -> { + LuaFile file = createFile(name, ""); + currentEditorFile = file; + codeEditorLauncher.launch(new CodeEditorActivity.Arguments(file.path, file.name, CodeEditorActivity.EditorType.LUA_SCRIPT_EDITOR)); + }).start(); + } + }).setTitle(R.string.create_new) + .show(); + }); + + ((RecyclerView) view.findViewById(R.id.recycler)).setAdapter(adapter); + ((RecyclerView) view.findViewById(R.id.recycler)).setLayoutManager(new AutoFitGridLayout(getContext(), 140)); + FileUtils.createDir(FileUtils.getResourcesPath(), "Lua Scripts"); + ArrayList files = new ArrayList<>(); + String path = FileUtils.getResourcesPath() + "/Lua Scripts/"; + for (String file : FileUtils.listFiles(path)) { + files.add(new LuaFile(file)); + } + + adapter.addAll(files); + orderByModified(); + } + + private LuaFile createFile(String name, String content) { + if (name.toLowerCase().endsWith(".lua")) { + name = name.substring(0, name.length() - 4); + } + + name = name.replaceAll("[^[a-zA-Z0-9-_ ]]", "-"); + + String fileName = name + "." + UUID.randomUUID().toString().substring(0, 4) + ".lua"; + LuaFile file = new LuaFile(fileName); + FileUtils.writeTextFile(file.path, fileName, content); + getView().post(() -> { + adapter.addAll(file); + orderByModified(); + }); + + return file; + } + + private void orderByModified() { + adapter.sort((o1, o2) -> Long.compare(o2.lastModified(), o1.lastModified())); + } + + private void onCreateListItem(int position, LuaFile file, View view) { + ((TextView) view.findViewById(R.id.title)) + .setText(file.name.split("\\.")[0]); + + view.setOnClickListener(v -> loadScript(file)); + view.findViewById(R.id.edit).setOnClickListener(v -> { + currentEditorFile = file; + codeEditorLauncher.launch(new CodeEditorActivity.Arguments(file.path, file.name, CodeEditorActivity.EditorType.LUA_SCRIPT_EDITOR)); + }); + } + + private void loadScript(LuaFile file) { + dismiss(); + + Toast.makeText(getContext(), String.format(getString(R.string.running_ff), file.name), Toast.LENGTH_SHORT).show(); + new Task(() -> { + String script = FileUtils.readTextFile(file.absolutePath()); + file.update(); + AlberDriver.LoadLuaScript(script); + }).start(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + + openDocumentLauncher.unregister(); + codeEditorLauncher.unregister(); + } + + private static class LuaFile { + private final String name; + private final String path; + + private LuaFile(String path, String name) { + this.name = name; + this.path = path; + } + + private LuaFile(String name) { + this(FileUtils.getResourcesPath() + "/Lua Scripts/", name); + } + + private String absolutePath() { + return path + "/" + name; + } + + private void update() { + FileUtils.updateFile(absolutePath()); + } + + private long lastModified() { + return FileUtils.getLastModified(absolutePath()); + } + } +} \ No newline at end of file diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/GamesFragment.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/GamesFragment.java new file mode 100644 index 00000000..ff6e4dca --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/GamesFragment.java @@ -0,0 +1,79 @@ +package com.panda3ds.pandroid.app.main; + +import android.net.Uri; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; +import androidx.activity.result.ActivityResultCallback; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.data.game.GameMetadata; +import com.panda3ds.pandroid.utils.FileUtils; +import com.panda3ds.pandroid.utils.GameUtils; +import com.panda3ds.pandroid.view.gamesgrid.GamesGridView; + + +public class GamesFragment extends Fragment implements ActivityResultCallback { + private final ActivityResultContracts.OpenDocument openRomContract = new ActivityResultContracts.OpenDocument(); + private ActivityResultLauncher pickFileRequest; + private GamesGridView gameListView; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_games, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + gameListView = view.findViewById(R.id.games); + + view.findViewById(R.id.add_rom).setOnClickListener((v) -> pickFileRequest.launch(new String[] {"*/*"})); + } + + @Override + public void onResume() { + super.onResume(); + gameListView.setGameList(GameUtils.getGames()); + } + + @Override + public void onActivityResult(Uri result) { + if (result != null) { + String uri = result.toString(); + if (GameUtils.findByRomPath(uri) == null) { + if (FileUtils.obtainRealPath(uri) == null) { + Toast.makeText(getContext(), "Invalid file path", Toast.LENGTH_LONG).show(); + return; + } + FileUtils.makeUriPermanent(uri, FileUtils.MODE_READ); + GameMetadata game = new GameMetadata(uri, FileUtils.getName(uri).split("\\.")[0], "Unknown"); + GameUtils.addGame(game); + GameUtils.launch(requireActivity(), game); + } + } + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + pickFileRequest = registerForActivityResult(openRomContract, this); + } + + @Override + public void onDestroy() { + if (pickFileRequest != null) { + pickFileRequest.unregister(); + pickFileRequest = null; + } + + super.onDestroy(); + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/SearchFragment.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/SearchFragment.java new file mode 100644 index 00000000..e9db7f80 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/SearchFragment.java @@ -0,0 +1,62 @@ +package com.panda3ds.pandroid.app.main; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatEditText; +import androidx.fragment.app.Fragment; +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.data.game.GameMetadata; +import com.panda3ds.pandroid.utils.GameUtils; +import com.panda3ds.pandroid.utils.SearchAgent; +import com.panda3ds.pandroid.view.SimpleTextWatcher; +import com.panda3ds.pandroid.view.gamesgrid.GamesGridView; +import java.util.ArrayList; +import java.util.List; + + +public class SearchFragment extends Fragment { + private final SearchAgent searchAgent = new SearchAgent(); + private GamesGridView gamesListView; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_search, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + gamesListView = view.findViewById(R.id.games); + ((AppCompatEditText) view.findViewById(R.id.search_bar)).addTextChangedListener((SimpleTextWatcher) this::search); + } + + @Override + public void onResume() { + super.onResume(); + searchAgent.clearBuffer(); + for (GameMetadata game : GameUtils.getGames()) { + searchAgent.addToBuffer(game.getId(), game.getTitle(), game.getPublisher()); + } + + search(""); + } + + private void search(String query) { + List resultIds = searchAgent.search(query); + ArrayList games = new ArrayList<>(GameUtils.getGames()); + Object[] resultObj = games.stream().filter(gameMetadata -> resultIds.contains(gameMetadata.getId())).toArray(); + + games.clear(); + for (Object res : resultObj) { + games.add((GameMetadata) res); + } + + gamesListView.setGameList(games); + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/SettingsFragment.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/SettingsFragment.java new file mode 100644 index 00000000..bfe33a2b --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/SettingsFragment.java @@ -0,0 +1,22 @@ +package com.panda3ds.pandroid.app.main; + +import android.os.Bundle; + +import androidx.annotation.Nullable; + +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.app.PreferenceActivity; +import com.panda3ds.pandroid.app.base.BasePreferenceFragment; +import com.panda3ds.pandroid.app.preferences.AppearancePreferences; +import com.panda3ds.pandroid.app.preferences.DeveloperPreferences; +import com.panda3ds.pandroid.app.preferences.InputPreferences; + +public class SettingsFragment extends BasePreferenceFragment { + @Override + public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) { + setPreferencesFromResource(R.xml.start_preferences, rootKey); + setItemClick("input", (item) -> PreferenceActivity.launch(requireContext(), InputPreferences.class)); + setItemClick("appearance", (item)-> PreferenceActivity.launch(requireContext(), AppearancePreferences.class)); + setItemClick("developer", (item)-> PreferenceActivity.launch(requireContext(), DeveloperPreferences.class)); + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/AppearancePreferences.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/AppearancePreferences.java new file mode 100644 index 00000000..dea4e261 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/AppearancePreferences.java @@ -0,0 +1,27 @@ +package com.panda3ds.pandroid.app.preferences; + +import android.os.Bundle; + +import androidx.annotation.Nullable; + +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.app.BaseActivity; +import com.panda3ds.pandroid.app.base.BasePreferenceFragment; +import com.panda3ds.pandroid.data.config.GlobalConfig; +import com.panda3ds.pandroid.view.preferences.SingleSelectionPreferences; + +public class AppearancePreferences extends BasePreferenceFragment { + @Override + public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) { + setPreferencesFromResource(R.xml.appearance_preference, rootKey); + + ((BaseActivity) requireActivity()).getSupportActionBar().setTitle(R.string.appearance); + + SingleSelectionPreferences themePreference = findPreference("theme"); + themePreference.setSelectedItem(GlobalConfig.get(GlobalConfig.KEY_APP_THEME)); + themePreference.setOnPreferenceChangeListener((preference, value) -> { + GlobalConfig.set(GlobalConfig.KEY_APP_THEME, (int) value); + return false; + }); + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/ControllerMapperPreferences.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/ControllerMapperPreferences.java new file mode 100644 index 00000000..e59adfbe --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/ControllerMapperPreferences.java @@ -0,0 +1,101 @@ +package com.panda3ds.pandroid.app.preferences; + +import android.content.pm.ActivityInfo; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.app.BaseActivity; +import com.panda3ds.pandroid.app.base.BottomAlertDialog; +import com.panda3ds.pandroid.view.controller.mapping.ControllerMapper; +import com.panda3ds.pandroid.view.controller.mapping.ControllerProfileManager; +import com.panda3ds.pandroid.view.controller.mapping.ControllerItem; +import com.panda3ds.pandroid.view.controller.mapping.Profile; + +public class ControllerMapperPreferences extends Fragment { + private Profile currentProfile; + private ControllerMapper mapper; + private View saveButton; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.preference_controller_mapper, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + + currentProfile = ControllerProfileManager.get(getArguments().getString("profile")).clone(); + + ((BaseActivity) requireActivity()).getSupportActionBar().hide(); + mapper = view.findViewById(R.id.mapper); + mapper.initialize(this::onLocationChanged, currentProfile); + + view.findViewById(R.id.change_visibility).setOnClickListener(v -> { + BottomAlertDialog builder = new BottomAlertDialog(v.getContext()); + builder.setTitle("Visibility"); + boolean[] visibleList = { + currentProfile.isVisible(ControllerItem.START), + currentProfile.isVisible(ControllerItem.SELECT), + currentProfile.isVisible(ControllerItem.L), + currentProfile.isVisible(ControllerItem.R), + currentProfile.isVisible(ControllerItem.DPAD), + currentProfile.isVisible(ControllerItem.JOYSTICK), + currentProfile.isVisible(ControllerItem.GAMEPAD), + }; + builder.setMultiChoiceItems(new CharSequence[]{ + "Start", "Select", "L", "R", "Dpad", getString(R.string.axis), "A/B/X/Y" + }, visibleList, (dialog, index, visibility) -> { + visibleList[index] = visibility; + }).setPositiveButton(android.R.string.ok, (dialog, which) -> { + + saveButton.setVisibility(View.VISIBLE); + + currentProfile.setVisible(ControllerItem.START, visibleList[0]); + currentProfile.setVisible(ControllerItem.SELECT, visibleList[1]); + currentProfile.setVisible(ControllerItem.L, visibleList[2]); + currentProfile.setVisible(ControllerItem.R, visibleList[3]); + currentProfile.setVisible(ControllerItem.DPAD, visibleList[4]); + currentProfile.setVisible(ControllerItem.JOYSTICK, visibleList[5]); + currentProfile.setVisible(ControllerItem.GAMEPAD, visibleList[6]); + + mapper.refreshLayout(); + }).setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.dismiss()); + builder.show(); + }); + + saveButton = view.findViewById(R.id.save); + saveButton.setOnClickListener(v -> { + ControllerProfileManager.add(currentProfile); + Toast.makeText(v.getContext(), R.string.saved, Toast.LENGTH_SHORT).show(); + requireActivity().finish(); + }); + + view.findViewById(R.id.delete).setOnClickListener(v -> { + ControllerProfileManager.remove(currentProfile.getId()); + requireActivity().finish(); + }); + + view.findViewById(R.id.rotate).setOnClickListener(v -> { + requireActivity().setRequestedOrientation(mapper.getCurrentWidth() > mapper.getCurrentHeight() ? ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT : ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE); + }); + + view.findViewById(R.id.delete).setVisibility(ControllerProfileManager.getProfileCount() > 1 ? View.VISIBLE : View.GONE); + + saveButton.setVisibility(View.GONE); + } + + public void onLocationChanged(ControllerItem id) { + saveButton.setVisibility(View.VISIBLE); + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/DeveloperPreferences.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/DeveloperPreferences.java new file mode 100644 index 00000000..f131f0a0 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/DeveloperPreferences.java @@ -0,0 +1,49 @@ +package com.panda3ds.pandroid.app.preferences; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +import androidx.annotation.Nullable; +import androidx.preference.SwitchPreference; + +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.app.PandroidApplication; +import com.panda3ds.pandroid.app.base.BasePreferenceFragment; +import com.panda3ds.pandroid.app.services.LoggerService; +import com.panda3ds.pandroid.data.config.GlobalConfig; + +public class DeveloperPreferences extends BasePreferenceFragment { + @Override + public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) { + setPreferencesFromResource(R.xml.developer_preferences, rootKey); + setActivityTitle(R.string.developer_options); + + setItemClick("performanceMonitor", pref -> GlobalConfig.set(GlobalConfig.KEY_SHOW_PERFORMANCE_OVERLAY, ((SwitchPreference) pref).isChecked())); + setItemClick("shaderJit", pref -> GlobalConfig.set(GlobalConfig.KEY_SHADER_JIT, ((SwitchPreference) pref).isChecked())); + setItemClick("loggerService", pref -> { + boolean checked = ((SwitchPreference) pref).isChecked(); + Context ctx = PandroidApplication.getAppContext(); + if (checked) { + ctx.startService(new Intent(ctx, LoggerService.class)); + } else { + ctx.stopService(new Intent(ctx, LoggerService.class)); + } + GlobalConfig.set(GlobalConfig.KEY_LOGGER_SERVICE, checked); + }); + + refresh(); + } + + @Override + public void onResume() { + super.onResume(); + refresh(); + } + + private void refresh() { + ((SwitchPreference) findPreference("performanceMonitor")).setChecked(GlobalConfig.get(GlobalConfig.KEY_SHOW_PERFORMANCE_OVERLAY)); + ((SwitchPreference) findPreference("loggerService")).setChecked(GlobalConfig.get(GlobalConfig.KEY_LOGGER_SERVICE)); + ((SwitchPreference) findPreference("shaderJit")).setChecked(GlobalConfig.get(GlobalConfig.KEY_SHADER_JIT)); + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/InputMapActivity.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/InputMapActivity.java new file mode 100644 index 00000000..dce56a5f --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/InputMapActivity.java @@ -0,0 +1,76 @@ +package com.panda3ds.pandroid.app.preferences; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.widget.Toast; + +import androidx.activity.result.contract.ActivityResultContract; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.app.BaseActivity; +import com.panda3ds.pandroid.input.InputEvent; +import com.panda3ds.pandroid.input.InputHandler; + +import java.util.Objects; + +public class InputMapActivity extends BaseActivity { + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_input_map); + } + + @Override + protected void onResume() { + super.onResume(); + + InputHandler.reset(); + InputHandler.setMotionDeadZone(0.8f); + InputHandler.setEventListener(this::onInputEvent); + } + + @Override + protected void onPause() { + super.onPause(); + InputHandler.reset(); + } + + @Override + public boolean dispatchGenericMotionEvent(MotionEvent ev) { + return InputHandler.processMotionEvent(ev); + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + return InputHandler.processKeyEvent(event); + } + + private void onInputEvent(InputEvent event) { + if (Objects.equals(event.getName(), "KEYCODE_BACK")) { + onBackPressed(); + return; + } + setResult(RESULT_OK, new Intent(event.getName())); + Toast.makeText(this, event.getName(), Toast.LENGTH_SHORT).show(); + finish(); + } + + + public static final class Contract extends ActivityResultContract { + @NonNull + @Override + public Intent createIntent(@NonNull Context context, String s) { + return new Intent(context, InputMapActivity.class); + } + + @Override + public String parseResult(int i, @Nullable Intent intent) { + return i == RESULT_OK ? intent.getAction() : null; + } + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/InputMapPreferences.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/InputMapPreferences.java new file mode 100644 index 00000000..b4d148b9 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/InputMapPreferences.java @@ -0,0 +1,104 @@ +package com.panda3ds.pandroid.app.preferences; + +import android.content.Context; +import android.os.Bundle; + +import androidx.activity.result.ActivityResultCallback; +import androidx.activity.result.ActivityResultLauncher; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.preference.Preference; +import androidx.preference.SeekBarPreference; + +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.app.BaseActivity; +import com.panda3ds.pandroid.app.base.BasePreferenceFragment; +import com.panda3ds.pandroid.input.InputMap; +import com.panda3ds.pandroid.input.KeyName; + +public class InputMapPreferences extends BasePreferenceFragment implements ActivityResultCallback { + + private ActivityResultLauncher requestKey; + private String currentKey; + + private SeekBarPreference deadZonePreference; + + @Override + public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) { + setPreferencesFromResource(R.xml.input_map_preferences, rootKey); + + ((BaseActivity) requireActivity()).getSupportActionBar().setTitle(R.string.controller_mapping); + + for (KeyName key : KeyName.values()) { + if (key == KeyName.NULL) { + continue; + } + + setItemClick(key.name(), this::onItemPressed); + } + + deadZonePreference = getPreferenceScreen().findPreference("dead_zone"); + + deadZonePreference.setOnPreferenceChangeListener((preference, value) -> { + InputMap.setDeadZone(((int)value / 100.0f)); + refreshList(); + + return false; + }); + + refreshList(); + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + requestKey = registerForActivityResult(new InputMapActivity.Contract(), this); + } + + @Override + public void onDetach() { + super.onDetach(); + + if (requestKey != null) { + requestKey.unregister(); + requestKey = null; + } + } + + private void onItemPressed(Preference pref) { + currentKey = pref.getKey(); + requestKey.launch(null); + } + + @Override + public void onResume() { + super.onResume(); + refreshList(); + } + + private void refreshList() { + deadZonePreference.setValue((int)(InputMap.getDeadZone() * 100)); + deadZonePreference.setSummary(deadZonePreference.getValue() + "%"); + + for (KeyName key : KeyName.values()) { + if (key == KeyName.NULL) { + continue; + } + + findPreference(key.name()).setSummary(InputMap.relative(key)); + } + } + + @Override + public void onActivityResult(String result) { + if (result != null) { + InputMap.set(KeyName.valueOf(currentKey), result); + refreshList(); + } + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/InputPreferences.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/InputPreferences.java new file mode 100644 index 00000000..aa5d5eff --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/InputPreferences.java @@ -0,0 +1,109 @@ +package com.panda3ds.pandroid.app.preferences; + +import android.annotation.SuppressLint; +import android.content.Intent; +import android.os.Bundle; +import android.widget.Toast; + +import androidx.annotation.Nullable; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; + +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.app.BaseActivity; +import com.panda3ds.pandroid.app.PreferenceActivity; +import com.panda3ds.pandroid.app.base.BasePreferenceFragment; +import com.panda3ds.pandroid.app.base.BottomAlertDialog; +import com.panda3ds.pandroid.view.controller.mapping.ControllerProfileManager; +import com.panda3ds.pandroid.view.controller.mapping.Profile; + +import java.util.List; +import java.util.Objects; + +public class InputPreferences extends BasePreferenceFragment { + + public static final String ID_DEFAULT_CONTROLLER_PROFILE = "defaultControllerProfile"; + public static final String ID_INPUT_MAP = "inputMap"; + public static final String ID_CREATE_PROFILE = "createProfile"; + private static final CharSequence ID_GAMEPAD_PROFILE_LIST = "gamepadProfileList"; + + @Override + public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) { + setPreferencesFromResource(R.xml.input_preference, rootKey); + setItemClick(ID_INPUT_MAP, (item) -> PreferenceActivity.launch(requireContext(), InputMapPreferences.class)); + setItemClick(ID_CREATE_PROFILE, (item) -> { + new BottomAlertDialog(requireContext()) + .setTextInput(getString(R.string.name), (name) -> { + name = formatName(name); + if (name.length() > 0) { + Profile profile = ControllerProfileManager.makeDefaultProfile(); + profile.setName(name); + ControllerProfileManager.add(profile); + refreshScreenProfileList(); + } else { + Toast.makeText(requireContext(), R.string.invalid_name, Toast.LENGTH_SHORT).show(); + } + }).setTitle(R.string.create_profile).show(); + }); + + setItemClick(ID_DEFAULT_CONTROLLER_PROFILE, (item) -> { + List profiles = ControllerProfileManager.listAll(); + String defaultProfileId = ControllerProfileManager.getDefaultProfile().getId(); + int defaultProfileIndex = 0; + CharSequence[] names = new CharSequence[profiles.size()]; + for (int i = 0; i < names.length; i++) { + names[i] = profiles.get(i).getName(); + if (Objects.equals(profiles.get(i).getId(), defaultProfileId)) { + defaultProfileIndex = i; + } + } + new BottomAlertDialog(item.getContext()) + .setSingleChoiceItems(names, defaultProfileIndex, (dialog, which) -> { + dialog.dismiss(); + ControllerProfileManager.setDefaultProfileId(profiles.get(which).getId()); + item.setSummary(profiles.get(which).getName()); + }).setTitle(R.string.pref_default_controller_title).show(); + }); + + ((BaseActivity) requireActivity()).getSupportActionBar().setTitle(R.string.input); + } + + public String formatName(String name) { + return name.trim().replaceAll("\\s\\s", " "); + } + + private void refresh() { + findPreference(ID_DEFAULT_CONTROLLER_PROFILE).setSummary(ControllerProfileManager.getDefaultProfile().getName()); + refreshScreenProfileList(); + } + + @SuppressLint("RestrictedApi") + private void refreshScreenProfileList() { + PreferenceCategory category = findPreference(ID_GAMEPAD_PROFILE_LIST); + Preference add = category.getPreference(category.getPreferenceCount() - 1); + category.removeAll(); + category.setOrderingAsAdded(true); + + for (Profile profile : ControllerProfileManager.listAll()) { + Preference item = new Preference(category.getContext()); + item.setOnPreferenceClickListener(preference -> { + category.performClick(); + PreferenceActivity.launch(requireActivity(), ControllerMapperPreferences.class, new Intent().putExtra("profile", profile.getId())); + return false; + }); + item.setOrder(category.getPreferenceCount()); + item.setIconSpaceReserved(false); + item.setTitle(profile.getName()); + category.addPreference(item); + } + + add.setOrder(category.getPreferenceCount()); + category.addPreference(add); + } + + @Override + public void onResume() { + super.onResume(); + refresh(); + } +} \ No newline at end of file diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/services/LoggerService.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/services/LoggerService.java new file mode 100644 index 00000000..e44f3503 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/services/LoggerService.java @@ -0,0 +1,104 @@ +package com.panda3ds.pandroid.app.services; + +import android.app.Service; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.os.Build; +import android.os.IBinder; +import android.util.Log; + +import androidx.annotation.Nullable; + +import com.panda3ds.pandroid.lang.PipeStreamTask; +import com.panda3ds.pandroid.lang.Task; +import com.panda3ds.pandroid.utils.Constants; +import com.panda3ds.pandroid.utils.FileUtils; + +import java.io.OutputStream; +import java.util.Arrays; + +public class LoggerService extends Service { + private static final long MAX_LOG_SIZE = 1024 * 1024 * 4; // 4MB + + private PipeStreamTask errorTask; + private PipeStreamTask outputTask; + private Process logcat; + + @Nullable + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public void onCreate() { + super.onCreate(); + try { + Runtime.getRuntime().exec(new String[]{"logcat", "-c"}).waitFor(); + logcat = Runtime.getRuntime().exec(new String[]{"logcat"}); + String logPath = getExternalMediaDirs()[0].getAbsolutePath(); + FileUtils.createDir(logPath, "logs"); + logPath = logPath + "/logs"; + + if (FileUtils.exists(logPath + "/last.txt")) { + FileUtils.delete(logPath + "/last.txt"); + } + + if (FileUtils.exists(logPath + "/current.txt")) { + FileUtils.rename(logPath + "/current.txt", "last.txt"); + } + + OutputStream stream = FileUtils.getOutputStream(logPath + "/current.txt"); + errorTask = new PipeStreamTask(logcat.getErrorStream(), stream, MAX_LOG_SIZE); + outputTask = new PipeStreamTask(logcat.getInputStream(), stream, MAX_LOG_SIZE); + + errorTask.start(); + outputTask.start(); + + Log.i(Constants.LOG_TAG, "Started logger service"); + logDeviceInfo(); + } catch (Exception e) { + stopSelf(); + Log.e(Constants.LOG_TAG, "Failed to start logger service"); + } + } + + private void logDeviceInfo() { + Log.i(Constants.LOG_TAG, "----------------------"); + Log.i(Constants.LOG_TAG, "Android SDK: " + Build.VERSION.SDK_INT); + Log.i(Constants.LOG_TAG, "Device: " + Build.DEVICE); + Log.i(Constants.LOG_TAG, "Model: " + Build.MANUFACTURER + " " + Build.MODEL); + Log.i(Constants.LOG_TAG, "ABIs: " + Arrays.toString(Build.SUPPORTED_ABIS)); + try { + PackageInfo info = getPackageManager().getPackageInfo(getPackageName(), 0); + Log.i(Constants.LOG_TAG, ""); + Log.i(Constants.LOG_TAG, "Package: " + info.packageName); + Log.i(Constants.LOG_TAG, "Install location: " + info.installLocation); + Log.i(Constants.LOG_TAG, "App version: " + info.versionName + " (" + info.versionCode + ")"); + } catch (Exception e) { + Log.e(Constants.LOG_TAG, "Error obtaining package info: " + e); + } + Log.i(Constants.LOG_TAG, "----------------------"); + } + + @Override + public void onTaskRemoved(Intent rootIntent) { + stopSelf(); + //This is a time for app save save log file + try { + Thread.sleep(1000); + } catch (Exception e) {} + super.onTaskRemoved(rootIntent); + } + + @Override + public void onDestroy() { + Log.i(Constants.LOG_TAG, "Logger service terminating"); + errorTask.close(); + outputTask.close(); + try { + logcat.destroy(); + } catch (Throwable t) {} + super.onDestroy(); + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/GsonConfigParser.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/GsonConfigParser.java new file mode 100644 index 00000000..0fde3d2f --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/GsonConfigParser.java @@ -0,0 +1,39 @@ +package com.panda3ds.pandroid.data; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.panda3ds.pandroid.lang.Task; +import com.panda3ds.pandroid.utils.FileUtils; + +public class GsonConfigParser { + private final Gson gson = new GsonBuilder().setPrettyPrinting().create(); + private final String name; + + public GsonConfigParser(String name) { + this.name = name; + } + + private String getPath() { + return FileUtils.getConfigPath()+ "/" + name + ".json"; + } + + public void save(Object data) { + synchronized (this) { + new Task(() -> { + String json = gson.toJson(data, data.getClass()); + FileUtils.writeTextFile(FileUtils.getConfigPath(), name + ".json", json); + }).runSync(); + } + } + + public T load(Class myClass) { + String[] content = new String[] {"{}"}; + new Task(()->{ + if (FileUtils.exists(getPath())) { + content[0] = FileUtils.readTextFile(getPath()); + } + }).runSync(); + + return gson.fromJson(content[0], myClass); + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/SMDH.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/SMDH.java new file mode 100644 index 00000000..e1230f24 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/SMDH.java @@ -0,0 +1,182 @@ +package com.panda3ds.pandroid.data; + +import android.graphics.Bitmap; +import android.graphics.Color; + +import com.panda3ds.pandroid.data.game.GameRegion; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +public class SMDH { + public static final int LANGUAGE_JAPANESE = 0; + public static final int LANGUAGE_ENGLISH = 1; + public static final int LANGUAGE_CHINESE = 6; + public static final int LANGUAGE_KOREAN = 7; + + public static final int REGION_JAPAN_MASK = 0x1; + public static final int REGION_NORTH_AMERICAN_MASK = 0x2; + public static final int REGION_EUROPE_MASK = 0x4; + public static final int REGION_AUSTRALIA_MASK = 0x8; + public static final int REGION_CHINA_MASK = 0x10; + public static final int REGION_KOREAN_MASK = 0x20; + public static final int REGION_TAIWAN_MASK = 0x40; + + private static final int ICON_SIZE = 48; + private static final int META_OFFSET = 0x8; + private static final int META_REGION_OFFSET = 0x2018; + private static final int IMAGE_OFFSET = 0x24C0; + + private int metaLanguage = LANGUAGE_ENGLISH; + private final ByteBuffer smdh; + private final String[] title = new String[12]; + private final String[] publisher = new String[12]; + private final int[] icon; + + private final GameRegion region; + + public SMDH(byte[] source) { + smdh = ByteBuffer.allocate(source.length); + smdh.position(0); + smdh.put(source); + smdh.position(0); + + region = parseRegion(); + icon = parseIcon(); + parseMeta(); + } + + private GameRegion parseRegion() { + GameRegion region; + smdh.position(META_REGION_OFFSET); + + int regionMasks = smdh.get() & 0xFF; + + final boolean japan = (regionMasks & REGION_JAPAN_MASK) != 0; + final boolean northAmerica = (regionMasks & REGION_NORTH_AMERICAN_MASK) != 0; + final boolean europe = (regionMasks & REGION_EUROPE_MASK) != 0; + final boolean australia = (regionMasks & REGION_AUSTRALIA_MASK) != 0; + final boolean china = (regionMasks & REGION_CHINA_MASK) != 0; + final boolean korea = (regionMasks & REGION_KOREAN_MASK) != 0; + final boolean taiwan = (regionMasks & REGION_TAIWAN_MASK) != 0; + + // Depending on the regions allowed in the region mask, pick one of the regions to use + // We prioritize English-speaking regions both here and in the emulator core, since users are most likely to speak English at least + if (northAmerica) { + region = GameRegion.NorthAmerican; + } else if (europe) { + region = GameRegion.Europe; + } else if (australia) { + region = GameRegion.Australia; + } else if (japan) { + region = GameRegion.Japan; + metaLanguage = LANGUAGE_JAPANESE; + } else if (korea) { + region = GameRegion.Korean; + metaLanguage = LANGUAGE_KOREAN; + } else if (china) { + region = GameRegion.China; + metaLanguage = LANGUAGE_CHINESE; + } else if (taiwan) { + region = GameRegion.Taiwan; + metaLanguage = LANGUAGE_CHINESE; + } else { + region = GameRegion.None; + } + + return region; + } + + private void parseMeta() { + byte[] data; + for (int i = 0; i < 12; i++) { + smdh.position(META_OFFSET + (512 * i) + 0x80); + data = new byte[0x100]; + smdh.get(data); + title[i] = convertString(data).replaceAll("\n", " "); + + smdh.position(META_OFFSET + (512 * i) + 0x180); + data = new byte[0x80]; + smdh.get(data); + publisher[i] = convertString(data); + } + } + + // The icons are stored in RGB562 but android need RGB888 + private int[] parseIcon() { + int[] icon = new int[ICON_SIZE * ICON_SIZE]; + smdh.position(0); + + for (int x = 0; x < ICON_SIZE; x++) { + for (int y = 0; y < ICON_SIZE; y++) { + int indexY = y & ~7; + int indexX = x & ~7; + + int interleave = mortonInterleave(x, y); + int offset = (interleave + (indexX * 8)) * 2; + + offset = offset + indexY * ICON_SIZE * 2; + + smdh.position(offset + IMAGE_OFFSET); + + int lowByte = smdh.get() & 0xFF; + int highByte = smdh.get() & 0xFF; + int texel = (highByte << 8) | lowByte; + + // Convert texel from RGB565 to RGB888 + int r = (texel >> 11) & 0x1F; + int g = (texel >> 5) & 0x3F; + int b = texel & 0x1F; + + r = (r << 3) | (r >> 2); + g = (g << 2) | (g >> 4); + b = (b << 3) | (b >> 2); + + icon[x + ICON_SIZE * y] = Color.rgb(r, g, b); + } + } + + return icon; + } + + + public GameRegion getRegion() { + return region; + } + + public Bitmap getBitmapIcon() { + Bitmap bitmap = Bitmap.createBitmap(ICON_SIZE, ICON_SIZE, Bitmap.Config.RGB_565); + bitmap.setPixels(icon, 0, ICON_SIZE, 0, 0, ICON_SIZE, ICON_SIZE); + return bitmap; + } + + public int[] getIcon() { + return icon; + } + + public String getTitle() { + return title[metaLanguage]; + } + + public String getPublisher() { + return publisher[metaLanguage]; + } + + // Strings in SMDH files are stored as UTF-16LE + private static String convertString(byte[] buffer) { + try { + return new String(buffer, 0, buffer.length, StandardCharsets.UTF_16LE) + .replaceAll("\0", ""); + } catch (Exception e) { + return ""; + } + } + + // Reference: https://github.com/wheremyfoodat/Panda3DS/blob/master/src/core/renderer_gl/textures.cpp#L88 + private static int mortonInterleave(int u, int v) { + int[] xlut = {0, 1, 4, 5, 16, 17, 20, 21}; + int[] ylut = {0, 2, 8, 10, 32, 34, 40, 42}; + + return xlut[u % 8] + ylut[v % 8]; + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/config/GlobalConfig.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/config/GlobalConfig.java new file mode 100644 index 00000000..bff1f9e0 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/config/GlobalConfig.java @@ -0,0 +1,78 @@ +package com.panda3ds.pandroid.data.config; + +import com.google.gson.internal.LinkedTreeMap; +import com.panda3ds.pandroid.data.GsonConfigParser; +import com.panda3ds.pandroid.utils.Constants; + +import java.io.Serializable; +import java.util.Map; + +public class GlobalConfig { + + private static final GsonConfigParser parser = new GsonConfigParser(Constants.PREF_GLOBAL_CONFIG); + + public static final int THEME_ANDROID = 0; + public static final int THEME_LIGHT = 1; + public static final int THEME_DARK = 2; + public static final int THEME_BLACK = 3; + + public static DataModel data; + + public static final Key KEY_SHADER_JIT = new Key<>("emu.shader_jit", false); + public static final Key KEY_SHOW_PERFORMANCE_OVERLAY = new Key<>("dev.performanceOverlay", false); + public static final Key KEY_LOGGER_SERVICE = new Key<>("dev.loggerService", false); + public static final Key KEY_APP_THEME = new Key<>("app.theme", THEME_ANDROID); + public static final Key KEY_SCREEN_GAMEPAD_VISIBLE = new Key<>("app.screen_gamepad.visible", true); + + public static void initialize() { + data = parser.load(DataModel.class); + } + + public static T get(Key key) { + Serializable value; + + if (!data.configs.containsKey(key.name)) { + return key.defaultValue; + } + + if (key.defaultValue instanceof String) { + value = (String) data.configs.get(key.name); + } else if (key.defaultValue instanceof Integer) { + value = ((Number) data.get(key.name)).intValue(); + } else if (key.defaultValue instanceof Boolean) { + value = (boolean) data.get(key.name); + } else if (key.defaultValue instanceof Long) { + value = ((Number) data.get(key.name)).longValue(); + } else { + value = ((Number) data.get(key.name)).floatValue(); + } + return (T) value; + } + + public static synchronized void set(Key key, T value) { + data.configs.put(key.name, value); + writeChanges(); + } + + private static void writeChanges() { + parser.save(data); + } + + private static class Key { + private final String name; + private final T defaultValue; + + private Key(String name, T defaultValue) { + this.name = name; + this.defaultValue = defaultValue; + } + } + + private static class DataModel { + private final Map configs = new LinkedTreeMap<>(); + + public Object get(String key) { + return configs.get(key); + } + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/game/GameMetadata.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/game/GameMetadata.java new file mode 100644 index 00000000..512a3725 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/game/GameMetadata.java @@ -0,0 +1,83 @@ +package com.panda3ds.pandroid.data.game; + +import android.graphics.Bitmap; +import android.util.Log; + +import androidx.annotation.Nullable; + +import com.panda3ds.pandroid.data.SMDH; +import com.panda3ds.pandroid.utils.Constants; +import com.panda3ds.pandroid.utils.GameUtils; + +import java.util.Objects; +import java.util.UUID; + +public class GameMetadata { + private final String id; + private final String romPath; + private final String title; + private final String publisher; + private final GameRegion[] regions; + private transient Bitmap icon; + + private GameMetadata(String id, String romPath, String title, String publisher, Bitmap icon, GameRegion[] regions) { + this.id = id; + this.title = title; + this.publisher = publisher; + this.romPath = romPath; + this.regions = regions; + if (icon != null) { + GameUtils.setGameIcon(id, icon); + } + } + + public GameMetadata(String romPath,String title, String publisher, GameRegion[] regions) { + this(UUID.randomUUID().toString(), romPath, title, publisher, null, regions); + } + + public GameMetadata(String romPath,String title, String publisher) { + this(romPath,title, publisher, new GameRegion[]{GameRegion.None}); + } + + public String getRomPath() { + return romPath; + } + + public String getId() { + return id; + } + + public String getTitle() { + return title; + } + + public String getPublisher() { + return publisher; + } + + public Bitmap getIcon() { + if (icon == null) { + icon = GameUtils.loadGameIcon(id); + } + return icon; + } + + public GameRegion[] getRegions() { + return regions; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (obj instanceof GameMetadata) { + return Objects.equals(((GameMetadata) obj).id, id); + } + return false; + } + + public static GameMetadata applySMDH(GameMetadata meta, SMDH smdh) { + Bitmap icon = smdh.getBitmapIcon(); + GameMetadata newMeta = new GameMetadata(meta.getId(), meta.getRomPath(), smdh.getTitle(), smdh.getPublisher(), icon, new GameRegion[]{smdh.getRegion()}); + icon.recycle(); + return newMeta; + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/game/GameRegion.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/game/GameRegion.java new file mode 100644 index 00000000..9b99b095 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/game/GameRegion.java @@ -0,0 +1,12 @@ +package com.panda3ds.pandroid.data.game; + +public enum GameRegion { + NorthAmerican, + Japan, + Europe, + Australia, + China, + Korean, + Taiwan, + None +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/input/InputEvent.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/input/InputEvent.java new file mode 100644 index 00000000..7869e00a --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/input/InputEvent.java @@ -0,0 +1,23 @@ +package com.panda3ds.pandroid.input; + +public class InputEvent { + private final String name; + private final float value; + + public InputEvent(String name, float value) { + this.name = name; + this.value = Math.max(0.0f, Math.min(1.0f, value)); + } + + public boolean isDown() { + return value > 0.0f; + } + + public String getName() { + return name; + } + + public float getValue() { + return value; + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/input/InputHandler.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/input/InputHandler.java new file mode 100644 index 00000000..390708b9 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/input/InputHandler.java @@ -0,0 +1,121 @@ +package com.panda3ds.pandroid.input; + +import android.view.InputDevice; +import android.view.KeyEvent; +import android.view.MotionEvent; + +import com.panda3ds.pandroid.lang.Function; + +import java.util.HashMap; + +public class InputHandler { + private static Function eventListener; + private static float motionDeadZone = 0.0f; + + private static final int[] gamepadSources = { + InputDevice.SOURCE_GAMEPAD, + InputDevice.SOURCE_JOYSTICK + }; + + private static final int[] validSources = { + InputDevice.SOURCE_GAMEPAD, + InputDevice.SOURCE_JOYSTICK, + InputDevice.SOURCE_DPAD, + InputDevice.SOURCE_KEYBOARD + }; + + private static final HashMap motionDownEvents = new HashMap<>(); + + private static boolean containsSource(int[] sources, int sourceMask) { + for (int source : sources) { + if ((source & sourceMask) == source) { + return true; + } + } + + return false; + } + + private static boolean isGamepadSource(int sourceMask) { + return containsSource(gamepadSources, sourceMask); + } + + private static boolean isSourceValid(int sourceMasked) { + return containsSource(validSources, sourceMasked); + } + + public static void setEventListener(Function eventListener) { + InputHandler.eventListener = eventListener; + } + + private static void handleEvent(InputEvent event) { + if (eventListener != null) { + eventListener.run(event); + } + } + + public static void setMotionDeadZone(float motionDeadZone) { + InputHandler.motionDeadZone = motionDeadZone; + } + + public static boolean processMotionEvent(MotionEvent event) { + if (!isSourceValid(event.getSource())) { + return false; + } + + if (isGamepadSource(event.getSource())) { + for (InputDevice.MotionRange range : event.getDevice().getMotionRanges()) { + float axisValue = event.getAxisValue(range.getAxis()); + float value = Math.abs(axisValue); + String name = (MotionEvent.axisToString(range.getAxis()) + (axisValue >= 0 ? "+" : "-")).toUpperCase(); + String reverseName = (MotionEvent.axisToString(range.getAxis()) + (axisValue >= 0 ? "-" : "+")).toUpperCase(); + + if (motionDownEvents.containsKey(reverseName)) { + motionDownEvents.remove(reverseName); + handleEvent(new InputEvent(reverseName.toUpperCase(), 0.0f)); + } + + if (value > motionDeadZone) { + motionDownEvents.put(name, value); + handleEvent(new InputEvent(name.toUpperCase(), (value - motionDeadZone) / (1.0f - motionDeadZone))); + } else if (motionDownEvents.containsKey(name)) { + motionDownEvents.remove(name); + handleEvent(new InputEvent(name.toUpperCase(), 0.0f)); + } + + } + } + + return true; + } + + public static boolean processKeyEvent(KeyEvent event) { + if (!isSourceValid(event.getSource())) { + return false; + } + + if (isGamepadSource(event.getSource())) { + // Dpad return motion event + key event, this remove the key event + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_DPAD_UP: + case KeyEvent.KEYCODE_DPAD_UP_LEFT: + case KeyEvent.KEYCODE_DPAD_UP_RIGHT: + case KeyEvent.KEYCODE_DPAD_DOWN: + case KeyEvent.KEYCODE_DPAD_DOWN_LEFT: + case KeyEvent.KEYCODE_DPAD_DOWN_RIGHT: + case KeyEvent.KEYCODE_DPAD_RIGHT: + case KeyEvent.KEYCODE_DPAD_LEFT: + return true; + } + } + + handleEvent(new InputEvent(KeyEvent.keyCodeToString(event.getKeyCode()), event.getAction() == KeyEvent.ACTION_UP ? 0.0f : 1.0f)); + return true; + } + + public static void reset() { + eventListener = null; + motionDeadZone = 0.0f; + motionDownEvents.clear(); + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/input/InputMap.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/input/InputMap.java new file mode 100644 index 00000000..ab2cdd04 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/input/InputMap.java @@ -0,0 +1,48 @@ +package com.panda3ds.pandroid.input; + +import com.panda3ds.pandroid.data.GsonConfigParser; +import com.panda3ds.pandroid.utils.Constants; + +public class InputMap { + public static final GsonConfigParser parser = new GsonConfigParser(Constants.PREF_INPUT_MAP); + private static DataModel data; + + public static void initialize() { + data = parser.load(DataModel.class); + } + + public static float getDeadZone() { + return data.deadZone; + } + + public static void set(KeyName key, String name) { + data.keys[key.ordinal()] = name; + writeConfig(); + } + + public static String relative(KeyName key) { + return data.keys[key.ordinal()] == null ? "-" : data.keys[key.ordinal()]; + } + + public static KeyName relative(String name) { + for (KeyName key : KeyName.values()) { + if (relative(key).equalsIgnoreCase(name)) + return key; + } + return KeyName.NULL; + } + + public static void setDeadZone(float value) { + data.deadZone = Math.max(0.0f, Math.min(1.0f, value)); + writeConfig(); + } + + private static void writeConfig() { + parser.save(data); + } + + private static class DataModel { + public float deadZone = 0.2f; + public final String[] keys = new String[32]; + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/input/KeyName.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/input/KeyName.java new file mode 100644 index 00000000..1253529f --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/input/KeyName.java @@ -0,0 +1,38 @@ +package com.panda3ds.pandroid.input; + +import com.panda3ds.pandroid.utils.Constants; + +public enum KeyName { + A(Constants.INPUT_KEY_A), + B(Constants.INPUT_KEY_B), + X(Constants.INPUT_KEY_X), + Y(Constants.INPUT_KEY_Y), + UP(Constants.INPUT_KEY_UP), + DOWN(Constants.INPUT_KEY_DOWN), + LEFT(Constants.INPUT_KEY_LEFT), + RIGHT(Constants.INPUT_KEY_RIGHT), + AXIS_LEFT, + AXIS_RIGHT, + AXIS_UP, + AXIS_DOWN, + START(Constants.INPUT_KEY_START), + SELECT(Constants.INPUT_KEY_SELECT), + L(Constants.INPUT_KEY_L), + R(Constants.INPUT_KEY_R), + NULL; + + private final int keyId; + + KeyName() { + this(-1); + } + + KeyName(int keyId) { + this.keyId = keyId; + } + + public int getKeyId() { + return keyId; + } + +} \ No newline at end of file diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/lang/Function.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/lang/Function.java new file mode 100644 index 00000000..25a15875 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/lang/Function.java @@ -0,0 +1,5 @@ +package com.panda3ds.pandroid.lang; + +public interface Function { + void run(T arg); +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/lang/PipeStreamTask.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/lang/PipeStreamTask.java new file mode 100644 index 00000000..e4bbda98 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/lang/PipeStreamTask.java @@ -0,0 +1,40 @@ +package com.panda3ds.pandroid.lang; + +import java.io.InputStream; +import java.io.OutputStream; + +public class PipeStreamTask extends Task { + private final InputStream input; + private final OutputStream output; + private final long limit; + private long size; + + public PipeStreamTask(InputStream input, OutputStream output, long limit) { + this.input = input; + this.output = output; + this.limit = limit; + } + + @Override + public void run() { + super.run(); + int data; + try { + while ((data = input.read()) != -1) { + output.write(data); + if (++size > limit) { + break; + } + } + } catch (Exception e) {} + close(); + } + + public void close() { + try { + output.flush(); + output.close(); + input.close(); + } catch (Exception e) {} + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/lang/Task.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/lang/Task.java new file mode 100644 index 00000000..8de344b4 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/lang/Task.java @@ -0,0 +1,22 @@ +package com.panda3ds.pandroid.lang; + +public class Task extends Thread { + public Task(Runnable runnable) { + super(runnable); + } + + protected Task() {} + + public void runSync() { + start(); + waitFinish(); + } + + public void waitFinish() { + try { + join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/math/Vector2.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/math/Vector2.java new file mode 100644 index 00000000..becec9e1 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/math/Vector2.java @@ -0,0 +1,16 @@ +package com.panda3ds.pandroid.math; + +public class Vector2 { + public float x, y; + public Vector2(float x, float y) { + this.x = x; + this.y = y; + } + + public static float distance(float x, float y, float x2, float y2) { return (float) Math.sqrt((x - x2) * (x - x2) + (y - y2) * (y - y2)); } + + public void set(float x, float y) { + this.x = x; + this.y = y; + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/Constants.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/Constants.java new file mode 100644 index 00000000..c72a516a --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/Constants.java @@ -0,0 +1,29 @@ +package com.panda3ds.pandroid.utils; + +public class Constants { + public static final int INPUT_KEY_A = 1 << 0; + public static final int INPUT_KEY_B = 1 << 1; + public static final int INPUT_KEY_SELECT = 1 << 2; + public static final int INPUT_KEY_START = 1 << 3; + public static final int INPUT_KEY_RIGHT = 1 << 4; + public static final int INPUT_KEY_LEFT = 1 << 5; + public static final int INPUT_KEY_UP = 1 << 6; + public static final int INPUT_KEY_DOWN = 1 << 7; + public static final int INPUT_KEY_R = 1 << 8; + public static final int INPUT_KEY_L = 1 << 9; + public static final int INPUT_KEY_X = 1 << 10; + public static final int INPUT_KEY_Y = 1 << 11; + + public static final int N3DS_WIDTH = 400; + public static final int N3DS_FULL_HEIGHT = 480; + public static final int N3DS_HALF_HEIGHT = N3DS_FULL_HEIGHT / 2; + + public static final String ACTIVITY_PARAMETER_PATH = "path"; + public static final String ACTIVITY_PARAMETER_FRAGMENT = "fragment"; + public static final String LOG_TAG = "pandroid"; + + public static final String PREF_GLOBAL_CONFIG = "app.GlobalConfig"; + public static final String PREF_GAME_UTILS = "app.GameUtils"; + public static final String PREF_INPUT_MAP = "app.InputMap"; + public static final String PREF_SCREEN_CONTROLLER_PROFILES = "app.input.ScreenControllerManager"; +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/FileUtils.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/FileUtils.java new file mode 100644 index 00000000..1746f1c9 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/FileUtils.java @@ -0,0 +1,246 @@ +package com.panda3ds.pandroid.utils; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import android.system.Os; +import android.util.Log; + +import androidx.documentfile.provider.DocumentFile; + +import com.panda3ds.pandroid.app.PandroidApplication; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.Objects; + +public class FileUtils { + public static final String MODE_READ = "r"; + public static final int CANONICAL_SEARCH_DEEP = 8; + + private static DocumentFile parseFile(String path) { + if (path.startsWith("/")) { + return DocumentFile.fromFile(new File(path)); + } + Uri uri = Uri.parse(path); + return DocumentFile.fromSingleUri(getContext(), uri); + } + + private static Context getContext() { + return PandroidApplication.getAppContext(); + } + + public static String getName(String path) { + return parseFile(path).getName(); + } + + public static String getResourcesPath(){ + File file = new File(getPrivatePath(), "config/resources"); + if (!file.exists()) { + file.mkdirs(); + } + + return file.getAbsolutePath(); + } + + public static String getPrivatePath() { + File file = getContext().getFilesDir(); + if (!file.exists()) { + file.mkdirs(); + } + + return file.getAbsolutePath(); + } + + public static String getConfigPath() { + File file = new File(getPrivatePath(), "config"); + if (!file.exists()) { + file.mkdirs(); + } + + return file.getAbsolutePath(); + } + + public static boolean exists(String path) { + return parseFile(path).exists(); + } + + public static void rename(String path, String newName){ + parseFile(path).renameTo(newName); + } + + public static void delete(String path) { + DocumentFile file = parseFile(path); + + if (file.exists()) { + if (file.isDirectory()) { + String[] children = listFiles(path); + for (String child : children) { + delete(path + "/" + child); + } + } + + file.delete(); + } + } + + public static boolean createDir(String path, String name) { + DocumentFile folder = parseFile(path); + if (folder.findFile(name) != null) { + return true; + } + + return folder.createDirectory(name) != null; + } + + public static boolean createFile(String path, String name) { + DocumentFile folder = parseFile(path); + if (folder.findFile(name) != null) { + folder.findFile(name).delete(); + } + + return folder.createFile("", name) != null; + } + + public static boolean writeTextFile(String path, String name, String content) { + try { + createFile(path, name); + OutputStream stream = getOutputStream(path + "/" + name); + stream.write(content.getBytes(StandardCharsets.UTF_8)); + stream.flush(); + stream.close(); + } catch (Exception e) { + Log.e(Constants.LOG_TAG, "Error on write text file: ", e); + return false; + } + + return true; + } + + public static String readTextFile(String path) { + if (!exists(path)) { + return null; + } + + try { + InputStream stream = getInputStream(path); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + + int len; + byte[] buffer = new byte[1024 * 8]; + while ((len = stream.read(buffer)) != -1) { + output.write(buffer, 0, len); + } + + stream.close(); + output.flush(); + output.close(); + + byte[] data = output.toByteArray(); + return new String(data, 0, data.length); + } catch (Exception e) { + return null; + } + } + + public static InputStream getInputStream(String path) throws FileNotFoundException { + return getContext().getContentResolver().openInputStream(parseFile(path).getUri()); + } + + public static OutputStream getOutputStream(String path) throws FileNotFoundException { + return getContext().getContentResolver().openOutputStream(parseFile(path).getUri()); + } + + public static void makeUriPermanent(String uri, String mode) { + int flags = Intent.FLAG_GRANT_READ_URI_PERMISSION; + if (mode.toLowerCase().contains("w")) { + flags &= Intent.FLAG_GRANT_WRITE_URI_PERMISSION; + } + + getContext().getContentResolver().takePersistableUriPermission(Uri.parse(uri), flags); + } + + /** + * When call ContentProvider.openFileDescriptor() android opens a file descriptor + * on app process in /proc/self/fd/[file descriptor id] this is a link to real file path + * can use File.getCanonicalPath() for get a link origin, but in some android version + * need use Os.readlink(path) to get a real path. + */ + public static String obtainRealPath(String uri) { + try { + ParcelFileDescriptor parcelDescriptor = getContext().getContentResolver().openFileDescriptor(Uri.parse(uri), "r"); + int fd = parcelDescriptor.getFd(); + File file = new File("/proc/self/fd/" + fd).getAbsoluteFile(); + + for (int i = 0; i < CANONICAL_SEARCH_DEEP; i++) { + try { + String canonical = file.getCanonicalPath(); + if (!Objects.equals(canonical, file.getAbsolutePath())) { + file = new File(canonical).getAbsoluteFile(); + } + } catch (Exception x) { + break; + } + } + + if (!file.getAbsolutePath().startsWith("/proc/self/")) { + parcelDescriptor.close(); + return file.getAbsolutePath(); + } + + String path = Os.readlink(file.getAbsolutePath()); + parcelDescriptor.close(); + + if (new File(path).exists()) { + return path; + } + + return null; + } catch (Exception e) { + return null; + } + } + + public static void updateFile(String path){ + DocumentFile file = parseFile(path); + Uri uri = file.getUri(); + + switch (uri.getScheme()) { + case "file": { + new File(uri.getPath()).setLastModified(System.currentTimeMillis()); + break; + } + + case "content": { + getContext().getContentResolver().update(uri, null, null, null); + break; + } + + default: { + Log.w(Constants.LOG_TAG, "Cannot update file from scheme: " + uri.getScheme()); + break; + } + } + } + + public static long getLastModified(String path) { + return parseFile(path).lastModified(); + } + + public static String[] listFiles(String path){ + DocumentFile folder = parseFile(path); + DocumentFile[] files = folder.listFiles(); + + String[] result = new String[files.length]; + for (int i = 0; i < result.length; i++){ + result[i] = files[i].getName(); + } + + return result; + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/GameUtils.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/GameUtils.java new file mode 100644 index 00000000..b763f7b2 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/GameUtils.java @@ -0,0 +1,101 @@ +package com.panda3ds.pandroid.utils; + +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.util.Log; + +import com.panda3ds.pandroid.app.GameActivity; +import com.panda3ds.pandroid.data.GsonConfigParser; +import com.panda3ds.pandroid.data.game.GameMetadata; + +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class GameUtils { + private static final Bitmap DEFAULT_ICON = Bitmap.createBitmap(48, 48, Bitmap.Config.ARGB_8888); + public static GsonConfigParser parser = new GsonConfigParser(Constants.PREF_GAME_UTILS); + + private static DataModel data; + + private static GameMetadata currentGame; + + public static void initialize() { + data = parser.load(DataModel.class); + } + + public static GameMetadata findByRomPath(String romPath) { + for (GameMetadata game : data.games) { + if (Objects.equals(romPath, game.getRomPath())) { + return game; + } + } + return null; + } + + public static void launch(Context context, GameMetadata game) { + currentGame = game; + String path = FileUtils.obtainRealPath(game.getRomPath()); + context.startActivity(new Intent(context, GameActivity.class).putExtra(Constants.ACTIVITY_PARAMETER_PATH, path)); + } + + public static GameMetadata getCurrentGame() { + return currentGame; + } + + public static void removeGame(GameMetadata game) { + data.games.remove(game); + writeChanges(); + } + + public static void addGame(GameMetadata game) { + data.games.add(0, game); + writeChanges(); + } + + public static ArrayList getGames() { + return new ArrayList<>(data.games); + } + + private static void writeChanges() { + parser.save(data); + } + + public static void setGameIcon(String id, Bitmap icon) { + try { + String appPath = FileUtils.getPrivatePath(); + FileUtils.createDir(appPath, "cache_icons"); + FileUtils.createFile(appPath + "/cache_icons/", id + ".png"); + + OutputStream output = FileUtils.getOutputStream(appPath + "/cache_icons/" + id + ".png"); + icon.compress(Bitmap.CompressFormat.PNG, 100, output); + output.close(); + } catch (Exception e) { + Log.e(Constants.LOG_TAG, "Error on save game icon: ", e); + } + } + + public static Bitmap loadGameIcon(String id) { + try { + String path = FileUtils.getPrivatePath() + "/cache_icons/" + id + ".png"; + if (FileUtils.exists(path)) { + InputStream stream = FileUtils.getInputStream(path); + Bitmap image = BitmapFactory.decodeStream(stream); + stream.close(); + return image; + } + } catch (Exception e) { + Log.e(Constants.LOG_TAG, "Error on load game icon: ", e); + } + return DEFAULT_ICON; + } + + private static class DataModel { + public final List games = new ArrayList<>(); + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/PerformanceMonitor.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/PerformanceMonitor.java new file mode 100644 index 00000000..23adbf13 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/PerformanceMonitor.java @@ -0,0 +1,64 @@ +package com.panda3ds.pandroid.utils; + +import android.app.ActivityManager; +import android.content.Context; +import android.os.Debug; +import android.os.Process; + +import com.panda3ds.pandroid.app.PandroidApplication; +import com.panda3ds.pandroid.data.config.GlobalConfig; + +public class PerformanceMonitor { + private static int fps = 1; + private static String backend = ""; + private static int frames = 0; + private static long lastUpdate = 0; + private static long totalMemory = 1; + private static long availableMemory = 0; + + public static void initialize(String backendName) { + fps = 1; + backend = backendName; + } + + public static void runFrame() { + if (GlobalConfig.get(GlobalConfig.KEY_SHOW_PERFORMANCE_OVERLAY)) { + frames++; + if (System.currentTimeMillis() - lastUpdate > 1000) { + lastUpdate = System.currentTimeMillis(); + fps = frames; + frames = 0; + try { + Context ctx = PandroidApplication.getAppContext(); + ActivityManager manager = (ActivityManager) ctx.getSystemService(Context.ACTIVITY_SERVICE); + ActivityManager.MemoryInfo info = new ActivityManager.MemoryInfo(); + manager.getMemoryInfo(info); + totalMemory = info.totalMem; + availableMemory = info.availMem; + } catch (Exception e) {} + } + } + } + + public static long getUsedMemory() { + return Math.max(1, totalMemory - availableMemory); + } + + public static long getTotalMemory() { + return totalMemory; + } + + public static long getAvailableMemory() { + return availableMemory; + } + + public static int getFps() { + return fps; + } + + public static String getBackend() { + return backend; + } + + public static void destroy() {} +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/SearchAgent.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/SearchAgent.java new file mode 100644 index 00000000..749e1bd6 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/SearchAgent.java @@ -0,0 +1,90 @@ +package com.panda3ds.pandroid.utils; + +import java.text.Normalizer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +public class SearchAgent { + // Store all results in a hashmap + // Matches IDs -> Result string + private final HashMap searchBuffer = new HashMap<>(); + + // Add search item to list + public void addToBuffer(String id, String... words) { + StringBuilder string = new StringBuilder(); + for (String word : words) { + string.append(normalize(word)).append(" "); + } + + searchBuffer.put(id, string.toString()); + } + + // Convert string to lowercase alphanumeric string, converting all characters to ASCII and turning double spaces into single ones + // For example, é will be converted to e + private String normalize(String string) { + string = Normalizer.normalize(string, Normalizer.Form.NFD).replaceAll("[^\\p{ASCII}]", ""); + + return string.toLowerCase() + .replaceAll("(?!([a-z0-9 ])).*", "") + .replaceAll("\\s\\s", " "); + } + + // Execute search and return array with item id. + public List search(String query) { + String[] words = normalize(query).split("\\s"); + + if (words.length == 0) { + return Collections.emptyList(); + } + + // Map for add all search result: id -> probability + HashMap results = new HashMap<>(); + for (String key : searchBuffer.keySet()) { + int probability = 0; + String value = searchBuffer.get(key); + + for (String word : words) { + if (value.contains(word)) + probability++; + } + + if (probability > 0) { + results.put(key, probability); + } + } + + + // Filter by probability average, ie by how closely they match to our query + // Ex: A = 10% B = 30% C = 70% (formula is (10+30+70)/3=36) + // Afterwards remove all results with probability < 36 + int average = 0; + for (String key : results.keySet()) { + average += results.get(key); + } + average = average / Math.max(1, results.size()); + + int i = 0; + ArrayList resultKeys = new ArrayList<>(Arrays.asList(results.keySet().toArray(new String[0]))); + while ((i < resultKeys.size() && resultKeys.size() > 1)) { + if (results.get(resultKeys.get(i)) < average) { + String key = resultKeys.get(i); + resultKeys.remove(i); + results.remove(key); + i = 0; + continue; + } + + i++; + } + + return Arrays.asList(results.keySet().toArray(new String[0])); + } + + // Clear search buffer + public void clearBuffer() { + searchBuffer.clear(); + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/PandaGlRenderer.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/PandaGlRenderer.java new file mode 100644 index 00000000..c39b36b3 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/PandaGlRenderer.java @@ -0,0 +1,153 @@ +package com.panda3ds.pandroid.view; + +import static android.opengl.GLES32.*; + +import android.content.res.Resources; +import android.graphics.Rect; +import android.opengl.GLSurfaceView; +import android.util.Log; +import com.panda3ds.pandroid.AlberDriver; +import com.panda3ds.pandroid.data.config.GlobalConfig; +import com.panda3ds.pandroid.utils.Constants; +import com.panda3ds.pandroid.utils.GameUtils; +import com.panda3ds.pandroid.utils.PerformanceMonitor; +import com.panda3ds.pandroid.view.renderer.ConsoleRenderer; +import com.panda3ds.pandroid.view.renderer.layout.ConsoleLayout; +import com.panda3ds.pandroid.view.renderer.layout.DefaultScreenLayout; +import com.panda3ds.pandroid.data.SMDH; +import com.panda3ds.pandroid.data.game.GameMetadata; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; + +public class PandaGlRenderer implements GLSurfaceView.Renderer, ConsoleRenderer { + private final String romPath; + private ConsoleLayout displayLayout; + private int screenWidth, screenHeight; + private int screenTexture; + public int screenFbo; + + PandaGlRenderer(String romPath) { + super(); + this.romPath = romPath; + + screenWidth = Resources.getSystem().getDisplayMetrics().widthPixels; + screenHeight = Resources.getSystem().getDisplayMetrics().heightPixels; + setLayout(new DefaultScreenLayout()); + } + + @Override + protected void finalize() throws Throwable { + if (screenTexture != 0) { + glDeleteTextures(1, new int[] {screenTexture}, 0); + } + + if (screenFbo != 0) { + glDeleteFramebuffers(1, new int[] {screenFbo}, 0); + } + + PerformanceMonitor.destroy(); + super.finalize(); + } + + public void onSurfaceCreated(GL10 unused, EGLConfig config) { + Log.i(Constants.LOG_TAG, glGetString(GL_EXTENSIONS)); + Log.w(Constants.LOG_TAG, glGetString(GL_VERSION)); + + int[] version = new int[2]; + glGetIntegerv(GL_MAJOR_VERSION, version, 0); + glGetIntegerv(GL_MINOR_VERSION, version, 1); + + if (version[0] < 3 || (version[0] == 3 && version[1] < 1)) { + Log.e(Constants.LOG_TAG, "OpenGL 3.1 or higher is required"); + } + + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + + int[] generateBuffer = new int[1]; + glGenTextures(1, generateBuffer, 0); + screenTexture = generateBuffer[0]; + glBindTexture(GL_TEXTURE_2D, screenTexture); + glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, screenWidth, screenHeight); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glBindTexture(GL_TEXTURE_2D, 0); + + glGenFramebuffers(1, generateBuffer, 0); + screenFbo = generateBuffer[0]; + glBindFramebuffer(GL_FRAMEBUFFER, screenFbo); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, screenTexture, 0); + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + Log.e(Constants.LOG_TAG, "Framebuffer is not complete"); + } + glBindFramebuffer(GL_FRAMEBUFFER, 0); + + AlberDriver.Initialize(); + AlberDriver.setShaderJitEnabled(GlobalConfig.get(GlobalConfig.KEY_SHADER_JIT)); + AlberDriver.LoadRom(romPath); + + // Load the SMDH + byte[] smdhData = AlberDriver.GetSmdh(); + if (smdhData.length == 0) { + Log.w(Constants.LOG_TAG, "Failed to load SMDH"); + } else { + SMDH smdh = new SMDH(smdhData); + Log.i(Constants.LOG_TAG, "Loaded rom SDMH"); + Log.i(Constants.LOG_TAG, String.format("Are you playing '%s' published by '%s'", smdh.getTitle(), smdh.getPublisher())); + GameMetadata game = GameUtils.getCurrentGame(); + GameUtils.removeGame(game); + GameUtils.addGame(GameMetadata.applySMDH(game, smdh)); + } + + PerformanceMonitor.initialize(getBackendName()); + } + + public void onDrawFrame(GL10 unused) { + if (AlberDriver.HasRomLoaded()) { + AlberDriver.RunFrame(screenFbo); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + glBindFramebuffer(GL_READ_FRAMEBUFFER, screenFbo); + + Rect topScreen = displayLayout.getTopDisplayBounds(); + Rect bottomScreen = displayLayout.getBottomDisplayBounds(); + + glBlitFramebuffer( + 0, Constants.N3DS_FULL_HEIGHT, Constants.N3DS_WIDTH, Constants.N3DS_HALF_HEIGHT, topScreen.left, screenHeight - topScreen.top, + topScreen.right, screenHeight - topScreen.bottom, GL_COLOR_BUFFER_BIT, GL_LINEAR + ); + + // Remove the black bars on the bottom screen + glBlitFramebuffer( + 40, Constants.N3DS_HALF_HEIGHT, Constants.N3DS_WIDTH - 40, 0, bottomScreen.left, screenHeight - bottomScreen.top, bottomScreen.right, + screenHeight - bottomScreen.bottom, GL_COLOR_BUFFER_BIT, GL_LINEAR + ); + } + + PerformanceMonitor.runFrame(); + } + + public void onSurfaceChanged(GL10 unused, int width, int height) { + screenWidth = width; + screenHeight = height; + + displayLayout.update(screenWidth, screenHeight); + } + + @Override + public void setLayout(ConsoleLayout layout) { + displayLayout = layout; + displayLayout.setTopDisplaySourceSize(Constants.N3DS_WIDTH, Constants.N3DS_HALF_HEIGHT); + displayLayout.setBottomDisplaySourceSize(Constants.N3DS_WIDTH - 40 - 40, Constants.N3DS_HALF_HEIGHT); + displayLayout.update(screenWidth, screenHeight); + } + + @Override + public ConsoleLayout getLayout() { + return displayLayout; + } + + @Override + public String getBackendName() { + return "OpenGL"; + } +} \ No newline at end of file diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/PandaGlSurfaceView.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/PandaGlSurfaceView.java new file mode 100644 index 00000000..c813294c --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/PandaGlSurfaceView.java @@ -0,0 +1,47 @@ +package com.panda3ds.pandroid.view; + +import android.content.Context; +import android.opengl.GLSurfaceView; +import android.os.Debug; + +import androidx.annotation.NonNull; +import com.panda3ds.pandroid.math.Vector2; +import com.panda3ds.pandroid.view.controller.TouchEvent; +import com.panda3ds.pandroid.view.controller.nodes.TouchScreenNodeImpl; +import com.panda3ds.pandroid.view.renderer.ConsoleRenderer; + +public class PandaGlSurfaceView extends GLSurfaceView implements TouchScreenNodeImpl { + final PandaGlRenderer renderer; + private int width; + private int height; + + public PandaGlSurfaceView(Context context, String romPath) { + super(context); + setEGLContextClientVersion(3); + if (Debug.isDebuggerConnected()) { + setDebugFlags(DEBUG_LOG_GL_CALLS); + } + renderer = new PandaGlRenderer(romPath); + setRenderer(renderer); + } + + public ConsoleRenderer getRenderer() { return renderer; } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + width = getMeasuredWidth(); + height = getMeasuredHeight(); + } + + @NonNull + @Override + public Vector2 getSize() { + return new Vector2(width, height); + } + + @Override + public void onTouch(TouchEvent event) { + onTouchScreenPress(renderer, event); + } +} \ No newline at end of file diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/PandaLayoutController.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/PandaLayoutController.java new file mode 100644 index 00000000..2f341c7b --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/PandaLayoutController.java @@ -0,0 +1,81 @@ +package com.panda3ds.pandroid.view; + +import android.content.Context; +import android.util.AttributeSet; +import com.panda3ds.pandroid.AlberDriver; +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.utils.Constants; +import com.panda3ds.pandroid.view.controller.ControllerLayout; +import com.panda3ds.pandroid.view.controller.mapping.ControllerProfileManager; +import com.panda3ds.pandroid.view.controller.mapping.ControllerItem; +import com.panda3ds.pandroid.view.controller.mapping.Profile; +import com.panda3ds.pandroid.view.controller.nodes.Button; +import com.panda3ds.pandroid.view.controller.nodes.Joystick; + +public class PandaLayoutController extends ControllerLayout { + + private int width = -1; + private int height = -1; + + public PandaLayoutController(Context context) { super(context); } + public PandaLayoutController(Context context, AttributeSet attrs) { super(context, attrs); } + public PandaLayoutController(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } + + public PandaLayoutController(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public void initialize() { + int[] keyButtonList = {R.id.button_a, Constants.INPUT_KEY_A, R.id.button_b, Constants.INPUT_KEY_B, + R.id.button_y, Constants.INPUT_KEY_Y, R.id.button_x, Constants.INPUT_KEY_X, + + R.id.button_left, Constants.INPUT_KEY_LEFT, R.id.button_right, Constants.INPUT_KEY_RIGHT, + R.id.button_up, Constants.INPUT_KEY_UP, R.id.button_down, Constants.INPUT_KEY_DOWN, + + R.id.button_start, Constants.INPUT_KEY_START, R.id.button_select, Constants.INPUT_KEY_SELECT, + + R.id.button_l, Constants.INPUT_KEY_L, R.id.button_r, Constants.INPUT_KEY_R}; + + for (int i = 0; i < keyButtonList.length; i += 2) { + final int keyCode = keyButtonList[i + 1]; + ((Button) findViewById(keyButtonList[i])).setStateListener((btn, pressed) -> { + if (pressed) + AlberDriver.KeyDown(keyCode); + else + AlberDriver.KeyUp(keyCode); + }); + } + + ((Joystick) findViewById(R.id.left_analog)).setJoystickListener((joystick, axisX, axisY) -> { + AlberDriver.SetCirclepadAxis((int) (axisX * 0x9C), (int) (axisY * 0x9C) * -1); + }); + + refreshChildren(); + measure(MeasureSpec.EXACTLY, MeasureSpec.EXACTLY); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + int measuredWidth = getMeasuredWidth(); + int measuredHeight = getMeasuredHeight(); + + if (measuredWidth != width || measuredHeight != height) { + width = measuredWidth; + height = measuredHeight; + applyProfileMap(); + } + } + + private void applyProfileMap() { + Profile profile = ControllerProfileManager.getDefaultProfile(); + + profile.applyToView(ControllerItem.L,findViewById(R.id.button_l), width, height); + profile.applyToView(ControllerItem.R, findViewById(R.id.button_r), width, height); + profile.applyToView(ControllerItem.START, findViewById(R.id.button_start), width, height); + profile.applyToView(ControllerItem.SELECT, findViewById(R.id.button_select), width, height); + profile.applyToView(ControllerItem.JOYSTICK, findViewById(R.id.left_analog), width, height); + profile.applyToView(ControllerItem.GAMEPAD, findViewById(R.id.gamepad), width, height); + profile.applyToView(ControllerItem.DPAD, findViewById(R.id.dpad), width, height); + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/SimpleTextWatcher.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/SimpleTextWatcher.java new file mode 100644 index 00000000..baacd743 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/SimpleTextWatcher.java @@ -0,0 +1,19 @@ +package com.panda3ds.pandroid.view; + +import android.text.Editable; +import android.text.TextWatcher; + +public interface SimpleTextWatcher extends TextWatcher { + void onChange(String value); + + @Override + default void onTextChanged(CharSequence s, int start, int before, int count) {} + + @Override + default void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + default void afterTextChanged(Editable s) { + onChange(s.toString()); + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/BaseEditor.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/BaseEditor.java new file mode 100644 index 00000000..4dba9f7c --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/BaseEditor.java @@ -0,0 +1,322 @@ +package com.panda3ds.pandroid.view.code; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.text.Editable; +import android.text.Layout; +import android.util.AttributeSet; +import android.view.ViewTreeObserver; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Arrays; + +public class BaseEditor extends BasicTextEditor { + private static final String HELLO_WORLD = "Hello World"; + private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.SUBPIXEL_TEXT_FLAG); + private final Rect rect = new Rect(); + private int currentLine; + private float spaceWidth; + private int lineHeight; + private int textOffset; + private int beginLine; + private int beginIndex; + private int endLine; + private int endIndex; + private int visibleHeight; + private int contentWidth; + private Layout textLayout; + private int currentWidth = -1; + private int currentHeight = -1; + + private final char[] textBuffer = new char[1]; + protected final int[] colors = new int[256]; + + // Allocate 512KB for the buffer + protected final byte[] syntaxBuffer = new byte[512 * 1024]; + private boolean requireUpdate = true; + + public BaseEditor(@NonNull Context context) { + super(context); + } + + public BaseEditor(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public BaseEditor(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + { + EditorColors.obtainColorScheme(colors, getContext()); + } + + @Override + protected void initialize() { + super.initialize(); + getViewTreeObserver().addOnGlobalLayoutListener(() -> { + adjustScroll(); + requireUpdate = true; + }); + } + + @SuppressLint("MissingSuperCall") + @Override + public void draw(Canvas canvas) { + //super.draw(canvas); + canvas.drawColor(colors[EditorColors.COLOR_BACKGROUND]); + textLayout = getLayout(); + if (textLayout == null) { + postDelayed(this::invalidate, 25); + return; + } + + try { + prepareDraw(); + if (requireUpdate) { + onVisibleContentChanged(beginIndex, endIndex - beginIndex); + } + + if (getSelectionStart() == getSelectionEnd()) { + drawCaret(canvas); + drawCurrentLine(canvas); + } else { + drawSelection(canvas); + } + + drawText(canvas); + drawLineCount(canvas); + } catch (Throwable e) { + drawError(canvas, e); + } + } + + private void drawError(Canvas canvas, Throwable e) { + canvas.drawColor(Color.RED); + paint.setTextSize(getTextSize()); + paint.setColor(Color.WHITE); + canvas.drawText("Editor draw error:", getPaddingLeft(), getLineHeight(), paint); + canvas.drawText(String.valueOf(e), getPaddingLeft(), getLineHeight() * 2, paint); + + int index = 2; + for (StackTraceElement trace : e.getStackTrace()) { + index++; + if (index > 5) break; + canvas.drawText(trace.getClassName() + ":" + trace.getMethodName() + ":" + trace.getLineNumber(), getPaddingLeft(), getLineHeight() * index, paint); + } + } + + private void prepareDraw() { + paint.setTypeface(getTypeface()); + paint.setTextSize(getTextSize()); + + Paint.FontMetrics fontMetrics = paint.getFontMetrics(); + spaceWidth = paint.measureText(" "); + lineHeight = getLineHeight(); + + //Align text to center of line + { + int ascent = (int) Math.abs(fontMetrics.ascent); + paint.getTextBounds(HELLO_WORLD, 0, HELLO_WORLD.length(), rect); + textOffset = Math.max(((lineHeight - rect.height()) / 2), 0) + ascent; + } + + int lineCount = textLayout.getLineCount(); + currentLine = textLayout.getLineForOffset(getSelectionStart()); + + int oldBeginLine = beginLine; + int oldEndLine = endLine; + + beginLine = Math.max(0, Math.min((getScrollY() / lineHeight) - 1, lineCount)); + beginIndex = textLayout.getLineStart(beginLine); + + if (oldEndLine != endLine || beginLine != oldBeginLine) { + requireUpdate = true; + } + + getGlobalVisibleRect(rect); + visibleHeight = rect.height(); + + endLine = Math.round(((float) visibleHeight / lineHeight) + 2) + beginLine; + endIndex = getLayout().getLineStart(Math.min(lineCount, endLine)); + + int padding = (int) (paint.measureText(String.valueOf(lineCount)) + (spaceWidth * 4)); + if (getPaddingLeft() != padding) { + setPadding(padding, 0, 0, 0); + } + + contentWidth = getWidth() + getScrollX(); + } + + private void drawLineCount(Canvas canvas) { + int colorEnable = colors[EditorColors.COLOR_TEXT]; + int colorDisable = applyAlphaToColor(colors[EditorColors.COLOR_TEXT], 100); + + paint.setColor(colors[EditorColors.COLOR_BACKGROUND_SECONDARY]); + int scrollY = getScrollY(); + float x = getScrollX(); + + canvas.translate(x, 0); + canvas.drawRect(0, scrollY, getPaddingLeft() - spaceWidth, visibleHeight + scrollY, paint); + paint.setColor(colors[EditorColors.COLOR_CURRENT_LINE]); + canvas.drawRect(0, currentLine * lineHeight, getPaddingLeft() - spaceWidth, (currentLine * lineHeight) + lineHeight, paint); + + for (int i = beginLine; i < Math.min(getLineCount(), endLine); i++) { + String text = String.valueOf(i + 1); + if (i == currentLine) { + paint.setColor(colorEnable); + } else { + paint.setColor(colorDisable); + } + + float width = paint.measureText(text); + canvas.drawText(text, getPaddingLeft() - width - (spaceWidth * 2.5f), (i * lineHeight) + textOffset, paint); + } + + paint.setColor(applyAlphaToColor(colorEnable, 10)); + canvas.drawRect(getPaddingLeft() - spaceWidth - (spaceWidth / 4), scrollY, getPaddingLeft() - spaceWidth, visibleHeight + scrollY, paint); + + canvas.translate(-x, 0); + } + + private void drawCurrentLine(Canvas canvas) { + float y = currentLine * lineHeight; + paint.setColor(colors[EditorColors.COLOR_CURRENT_LINE]); + canvas.drawRect(0, y, contentWidth, y + lineHeight, paint); + } + + private void drawText(Canvas canvas) { + Editable edit = getText(); + float x = 0; + float y = textOffset; + int line = 0; + + canvas.translate(getPaddingLeft(), beginLine * lineHeight); + + paint.setColor(colors[EditorColors.COLOR_TEXT]); + for (int i = beginIndex; i < endIndex; i++) { + textBuffer[0] = edit.charAt(i); + switch (textBuffer[0]) { + case '\n': + line++; + x = 0; + y = (line * lineHeight) + textOffset; + break; + + case ' ': + x += spaceWidth; + break; + + default: + paint.setColor(colors[syntaxBuffer[i - beginIndex]]); + canvas.drawText(textBuffer, 0, 1, x, y, paint); + x += paint.measureText(textBuffer, 0, 1); + break; + } + } + + canvas.translate(-getPaddingLeft(), -(beginLine * lineHeight)); + } + + private void drawCaret(Canvas canvas) { + int start = textLayout.getLineStart(currentLine); + int end = textLayout.getLineEnd(currentLine); + int position = getSelectionStart(); + float x = getPaddingLeft(); + float y = (currentLine * lineHeight); + Editable text = getText(); + for (int i = start; i < end; i++) { + if (i == position) { + break; + } + + textBuffer[0] = text.charAt(i); + x += paint.measureText(textBuffer, 0, 1); + } + + paint.setColor(colors[EditorColors.COLOR_CARET]); + float caretWidth = spaceWidth / 2; + canvas.drawRect(x - (caretWidth / 2), y, x + (caretWidth / 2), y + lineHeight, paint); + } + + private void drawSelection(Canvas canvas) { + int start = getSelectionStart(); + int end = getSelectionEnd(); + int endLine = textLayout.getLineForOffset(end); + canvas.translate(getPaddingLeft(), 0); + + paint.setColor(colors[EditorColors.COLOR_SELECTION]); + + Editable text = getText(); + + for (int line = currentLine; line <= endLine; line++) { + + if (line < beginLine) continue; + if (line > this.endLine) break; + + if (line == endLine || line == currentLine) { + int lineStart = textLayout.getLineStart(line); + float x = 0; + + if (lineStart <= start) { + x = paint.measureText(text, lineStart, start); + lineStart = start; + } + float width; + if (line < endLine) { + width = contentWidth; + } else { + width = paint.measureText(text, lineStart, end); + } + + canvas.drawRect(x, lineHeight * line, x + width, (lineHeight * line) + lineHeight, paint); + } else { + canvas.drawRect(0, lineHeight * line, contentWidth, (lineHeight * line) + lineHeight, paint); + } + } + canvas.translate(-getPaddingLeft(), 0); + } + + public int applyAlphaToColor(int color, int alpha) { + return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color)); + } + + protected void onVisibleContentChanged(int index, int length) { + requireUpdate = false; + + Arrays.fill(syntaxBuffer, (byte) 0); + if (length > 0) { + onRefreshColorScheme(syntaxBuffer, index, length); + } + } + + protected void onRefreshColorScheme(byte[] buffer, int index, int length) { + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + if (currentWidth != getMeasuredWidth() || currentHeight != getMeasuredHeight()) { + currentWidth = getMeasuredWidth(); + currentHeight = getMeasuredHeight(); + invalidateAll(); + } + } + + protected void invalidateAll() { + requireUpdate = true; + invalidate(); + } + + @Override + protected void onTextChanged() { + requireUpdate = true; + super.onTextChanged(); + } +} \ No newline at end of file diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/BasicTextEditor.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/BasicTextEditor.java new file mode 100644 index 00000000..1d497656 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/BasicTextEditor.java @@ -0,0 +1,154 @@ +package com.panda3ds.pandroid.view.code; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Color; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.text.Editable; +import android.text.InputType; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.GestureDetector; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.inputmethod.EditorInfo; +import android.widget.Scroller; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatEditText; + +import com.panda3ds.pandroid.view.SimpleTextWatcher; + +public class BasicTextEditor extends AppCompatEditText { + private GestureDetector gestureDetector; + private final Rect visibleRect = new Rect(); + + public BasicTextEditor(@NonNull Context context) { + super(context); + initialize(); + } + + public BasicTextEditor(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + initialize(); + } + + public BasicTextEditor(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initialize(); + } + + protected void initialize() { + setTypeface(Typeface.MONOSPACE); + gestureDetector = new GestureDetector(getContext(), new ScrollGesture()); + + setTypeface(Typeface.createFromAsset(getContext().getAssets(), "fonts/comic_mono.ttf")); + setGravity(Gravity.START | Gravity.TOP); + setTextSize(TypedValue.COMPLEX_UNIT_SP, 16); + setLineSpacing(0, 1.3f); + setScroller(new Scroller(getContext())); + + setInputType(InputType.TYPE_CLASS_TEXT | + InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | + InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE | + InputType.TYPE_TEXT_FLAG_MULTI_LINE | + InputType.TYPE_TEXT_FLAG_AUTO_CORRECT); + + setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI); + setBackgroundColor(Color.BLACK); + setTextColor(Color.WHITE); + + setFocusableInTouchMode(true); + setHorizontallyScrolling(true); + setHorizontalScrollBarEnabled(true); + + addTextChangedListener((SimpleTextWatcher) value -> BasicTextEditor.this.onTextChanged()); + } + + // Disable default Android scroll + @Override + public void scrollBy(int x, int y) {} + + @Override + public void scrollTo(int x, int y) {} + + public void setScroll(int x, int y) { + x = Math.max(0, x); + y = Math.max(0, y); + + int maxHeight = Math.round(getLineCount() * getLineHeight()); + getGlobalVisibleRect(visibleRect); + maxHeight = Math.max(0, maxHeight - visibleRect.height()); + + int maxWidth = (int) getPaint().measureText(getText(), 0, length()); + maxWidth += getPaddingLeft() + getPaddingRight(); + + int scrollX = x - Math.max(Math.min(maxWidth - visibleRect.width(), x), 0); + int scrollY = Math.min(maxHeight, y); + + super.scrollTo(scrollX, scrollY); + } + + public void adjustScroll(){ + setScroll(getScrollX(), getScrollY()); + } + + protected void onTextChanged() {} + + private boolean onSuperTouchListener(MotionEvent event) { + return super.onTouchEvent(event); + } + + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onTouchEvent(MotionEvent event) { + return gestureDetector.onTouchEvent(event); + } + + private class ScrollGesture implements GestureDetector.OnGestureListener { + @Override + public boolean onDown(@NonNull MotionEvent e) { + return true; + } + + @Override + public void onShowPress(@NonNull MotionEvent e) { + onSuperTouchListener(e); + } + + @Override + public boolean onSingleTapUp(@NonNull MotionEvent e) { + return onSuperTouchListener(e); + } + + @Override + public boolean onScroll(@NonNull MotionEvent e1, @NonNull MotionEvent e2, float distanceX, float distanceY) { + int scrollX = (int) Math.max(0, getScrollX() + distanceX); + int scrollY = (int) Math.max(0, getScrollY() + distanceY); + setScroll(scrollX, scrollY); + return true; + } + + @Override + public void onLongPress(@NonNull MotionEvent e) { + onSuperTouchListener(e); + } + + @Override + public boolean onFling(@NonNull MotionEvent e1, @NonNull MotionEvent e2, float velocityX, float velocityY) { + return false; + } + } + + public void insert(CharSequence text) { + if (getSelectionStart() == getSelectionEnd()) { + getText().insert(getSelectionStart(), text); + } else { + getText().replace(getSelectionStart(), getSelectionEnd(), text); + } + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/CodeEditor.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/CodeEditor.java new file mode 100644 index 00000000..96a8637b --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/CodeEditor.java @@ -0,0 +1,51 @@ +package com.panda3ds.pandroid.view.code; + +import android.content.Context; +import android.util.AttributeSet; + +import com.panda3ds.pandroid.view.code.syntax.CodeSyntax; + +public class CodeEditor extends BaseEditor { + private CodeSyntax syntax; + private Runnable contentChangeListener; + + public CodeEditor(Context context) { + super(context); + } + + public CodeEditor(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public CodeEditor(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public void setSyntax(CodeSyntax syntax) { + this.syntax = syntax; + invalidateAll(); + } + + public void setOnContentChangedListener(Runnable contentChangeListener) { + this.contentChangeListener = contentChangeListener; + } + + @Override + protected void onTextChanged() { + super.onTextChanged(); + if (contentChangeListener != null) { + contentChangeListener.run(); + } + } + + @Override + protected void onRefreshColorScheme(byte[] buffer, int index, int length) { + super.onRefreshColorScheme(buffer, index, length); + + if (syntax != null) { + final CharSequence text = getText().subSequence(index, index + length); + syntax.apply(syntaxBuffer, text); + System.gc(); + } + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/EditorColors.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/EditorColors.java new file mode 100644 index 00000000..3b12ddf9 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/EditorColors.java @@ -0,0 +1,63 @@ +package com.panda3ds.pandroid.view.code; + +import android.content.Context; + +import com.panda3ds.pandroid.app.PandroidApplication; + +public class EditorColors { + public static final byte COLOR_TEXT = 0x0; + public static final byte COLOR_KEYWORDS = 0x1; + public static final byte COLOR_NUMBERS = 0x2; + public static final byte COLOR_STRING = 0x3; + public static final byte COLOR_METADATA = 0x4; + public static final byte COLOR_COMMENT = 0x5; + public static final byte COLOR_SYMBOLS = 0x6; + public static final byte COLOR_FIELDS = 0x7; + public static final byte COLOR_BACKGROUND = 0x1D; + public static final byte COLOR_BACKGROUND_SECONDARY = 0x2D; + public static final byte COLOR_SELECTION = 0x3D; + public static final byte COLOR_CARET = 0x4D; + public static final byte COLOR_CURRENT_LINE = 0x5D; + + public static void obtainColorScheme(int[] colors, Context context) { + if (PandroidApplication.isDarkMode()) { + applyDarkTheme(colors); + } else { + applyLightTheme(colors); + } + } + + private static void applyLightTheme(int[] colors) { + colors[EditorColors.COLOR_TEXT] = 0xFF000000; + colors[EditorColors.COLOR_KEYWORDS] = 0xFF3AE666; + colors[EditorColors.COLOR_NUMBERS] = 0xFF3A9EE6; + colors[EditorColors.COLOR_METADATA] = 0xFF806AE6; + colors[EditorColors.COLOR_SYMBOLS] = 0xFF202020; + colors[EditorColors.COLOR_STRING] = 0xFF2EB541; + colors[EditorColors.COLOR_FIELDS] = 0xFF9876AA; + colors[EditorColors.COLOR_COMMENT] = 0xFF808080; + + colors[EditorColors.COLOR_BACKGROUND] = 0xFFFFFFFF; + colors[EditorColors.COLOR_BACKGROUND_SECONDARY] = 0xFFF0F0F0; + colors[EditorColors.COLOR_SELECTION] = 0x701F9EDE; + colors[EditorColors.COLOR_CARET] = 0xFF000000; + colors[EditorColors.COLOR_CURRENT_LINE] = 0x05000050; + } + + private static void applyDarkTheme(int[] colors) { + colors[EditorColors.COLOR_TEXT] = 0xFFFFFFFF; + colors[EditorColors.COLOR_KEYWORDS] = 0xFFE37F3E; + colors[EditorColors.COLOR_NUMBERS] = 0xFF3A9EE6; + colors[EditorColors.COLOR_METADATA] = 0xFFC5CA1D; + colors[EditorColors.COLOR_SYMBOLS] = 0xFFC0C0C0; + colors[EditorColors.COLOR_STRING] = 0xFF2EB541; + colors[EditorColors.COLOR_FIELDS] = 0xFF9876AA; + colors[EditorColors.COLOR_COMMENT] = 0xFFBBBBBB; + + colors[EditorColors.COLOR_BACKGROUND] = 0xFF2B2B2B; + colors[EditorColors.COLOR_BACKGROUND_SECONDARY] = 0xFF313335; + colors[EditorColors.COLOR_SELECTION] = 0x701F9EDE; + colors[EditorColors.COLOR_CARET] = 0x60FFFFFF; + colors[EditorColors.COLOR_CURRENT_LINE] = 0x10FFFFFF; + } +} \ No newline at end of file diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/syntax/CodeSyntax.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/syntax/CodeSyntax.java new file mode 100644 index 00000000..6c50865f --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/syntax/CodeSyntax.java @@ -0,0 +1,21 @@ +package com.panda3ds.pandroid.view.code.syntax; + +public abstract class CodeSyntax { + public abstract void apply(byte[] syntaxBuffer, final CharSequence text); + + // Get syntax highlighting data for a file based on its filename, by looking at the extension + public static CodeSyntax getFromFilename(String name) { + name = name.trim().toLowerCase(); + String[] parts = name.split("\\."); + if (parts.length == 0) + return null; + + // Get syntax based on file extension + switch (parts[parts.length - 1]) { + case "lua": + return new LuaSyntax(); + default: + return null; + } + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/syntax/LuaSyntax.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/syntax/LuaSyntax.java new file mode 100644 index 00000000..d53fb1d7 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/syntax/LuaSyntax.java @@ -0,0 +1,58 @@ +package com.panda3ds.pandroid.view.code.syntax; + +import com.panda3ds.pandroid.view.code.EditorColors; + +import java.util.Arrays; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +class LuaSyntax extends CodeSyntax { + public static final Pattern comment = Pattern.compile("(\\-\\-.*)"); + + public static final Pattern keywords = PatternUtils.buildGenericKeywords( + "and", "break", "do", "else", "elseif", "end", "false", "for", "function", "if", "in", + "local", "nil", "not", "or", "repeat", "return", "then", "true", "until", "while"); + + public static final Pattern identifiers = PatternUtils.buildGenericKeywords( + "assert", "collectgarbage", "dofile", "error", "getmetatable", "ipairs", "loadfile", "load", "loadstring", "next", "pairs", "pcall", "print", "rawequal", "rawlen", "rawget", "rawset", + "select", "setmetatable", "tonumber", "tostring", "type", "xpcall", "_G", "_VERSION", "arshift", "band", "bnot", "bor", "bxor", "btest", "extract", "lrotate", "lshift", "replace", + "rrotate", "rshift", "create", "resume", "running", "status", "wrap", "yield", "isyieldable", "debug", "getuservalue", "gethook", "getinfo", "getlocal", "getregistry", "getmetatable", + "getupvalue", "upvaluejoin", "upvalueid", "setuservalue", "sethook", "setlocal", "setmetatable", "setupvalue", "traceback", "close", "flush", "input", "lines", "open", "output", "popen", + "read", "tmpfile", "type", "write", "close", "flush", "lines", "read", "seek", "setvbuf", "write", "__gc", "__tostring", "abs", "acos", "asin", "atan", "ceil", "cos", "deg", "exp", "tointeger", + "floor", "fmod", "ult", "log", "max", "min", "modf", "rad", "random", "randomseed", "sin", "sqrt", "string", "tan", "type", "atan2", "cosh", "sinh", "tanh", + "pow", "frexp", "ldexp", "log10", "pi", "huge", "maxinteger", "mininteger", "loadlib", "searchpath", "seeall", "preload", "cpath", "path", "searchers", "loaded", "module", "require", "clock", + "date", "difftime", "execute", "exit", "getenv", "remove", "rename", "setlocale", "time", "tmpname", "byte", "char", "dump", "find", "format", "gmatch", "gsub", "len", "lower", "match", "rep", + "reverse", "sub", "upper", "pack", "packsize", "unpack", "concat", "maxn", "insert", "pack", "unpack", "remove", "move", "sort", "offset", "codepoint", "char", "len", "codes", "charpattern", + "coroutine", "table", "io", "os", "string", "uint8_t", "bit32", "math", "debug", "package"); + + public static final Pattern string = Pattern.compile("((\")(.*?)([^\\\\]\"))|((\")(.+))|((')(.?)('))"); + public static final Pattern symbols = Pattern.compile("([.!&?:;*+/{}()\\]\\[,=-])"); + public static final Pattern numbers = Pattern.compile("\\b((\\d*[.]?\\d+([Ee][+-]?[\\d]+)?[LlfFdD]?)|(0[xX][0-9a-zA-Z]+)|(0[bB][0-1]+)|(0[0-7]+))\\b"); + + @Override + public void apply(byte[] syntaxBuffer, CharSequence text) { + for (Matcher matcher = keywords.matcher(text); matcher.find(); ) { + Arrays.fill(syntaxBuffer, matcher.start(), matcher.end(), EditorColors.COLOR_KEYWORDS); + } + + for (Matcher matcher = identifiers.matcher(text); matcher.find(); ) { + Arrays.fill(syntaxBuffer, matcher.start(), matcher.end(), EditorColors.COLOR_FIELDS); + } + + for (Matcher matcher = symbols.matcher(text); matcher.find(); ) { + Arrays.fill(syntaxBuffer, matcher.start(), matcher.end(), EditorColors.COLOR_SYMBOLS); + } + + for (Matcher matcher = numbers.matcher(text); matcher.find(); ) { + Arrays.fill(syntaxBuffer, matcher.start(), matcher.end(), EditorColors.COLOR_NUMBERS); + } + + for (Matcher matcher = string.matcher(text); matcher.find(); ) { + Arrays.fill(syntaxBuffer, matcher.start(), matcher.end(), EditorColors.COLOR_STRING); + } + + for (Matcher matcher = comment.matcher(text); matcher.find(); ) { + Arrays.fill(syntaxBuffer, matcher.start(), matcher.end(), EditorColors.COLOR_COMMENT); + } + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/syntax/PatternUtils.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/syntax/PatternUtils.java new file mode 100644 index 00000000..e3e5128a --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/syntax/PatternUtils.java @@ -0,0 +1,18 @@ +package com.panda3ds.pandroid.view.code.syntax; + +import java.util.regex.Pattern; + +class PatternUtils { + public static Pattern buildGenericKeywords(String... keywords){ + StringBuilder builder = new StringBuilder(); + builder.append("\\b("); + for (int i = 0; i < keywords.length; i++){ + builder.append(keywords[i]); + if (i+1 != keywords.length){ + builder.append("|"); + } + } + builder.append(")\\b"); + return Pattern.compile(builder.toString()); + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/ControllerLayout.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/ControllerLayout.java new file mode 100644 index 00000000..32451bc7 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/ControllerLayout.java @@ -0,0 +1,143 @@ +package com.panda3ds.pandroid.view.controller; + +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.RelativeLayout; +import com.panda3ds.pandroid.math.Vector2; +import com.panda3ds.pandroid.utils.Constants; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; + +public class ControllerLayout extends RelativeLayout { + private final HashMap activeTouchEvents = new HashMap<>(); + private final ArrayList controllerNodes = new ArrayList<>(); + + public ControllerLayout(Context context) { this(context, null); } + public ControllerLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } + public ControllerLayout(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } + + public ControllerLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public void refreshChildren() { + ArrayList nodes = new ArrayList<>(); + populateNodesArray(this, nodes); + + // Need Reverse: First view is in back and last view is in front for respect android View hierarchy + Collections.reverse(nodes); + + controllerNodes.clear(); + controllerNodes.addAll(nodes); + } + + private void populateNodesArray(ViewGroup group, ArrayList list) { + for (int i = 0; i < group.getChildCount(); i++) { + View view = group.getChildAt(i); + if (view instanceof ControllerNode) { + list.add((ControllerNode) view); + } else if (view instanceof ViewGroup) { + populateNodesArray((ViewGroup) view, list); + } + } + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + int index = event.getActionIndex(); + + switch (event.getActionMasked()) { + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_POINTER_UP: { + int id = event.getPointerId(index); + processTouch(true, event.getX(index), event.getY(index), id); + } break; + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_DOWN: { + int id = event.getPointerId(index); + processTouch(false, event.getX(index), event.getY(index), id); + } break; + case MotionEvent.ACTION_MOVE: + for (int id = 0; id < event.getPointerCount(); id++) { + processTouch(false, event.getX(id), event.getY(id), id); + } + break; + } + return true; + } + + private void processTouch(boolean up, float x, float y, int index) { + int[] globalPosition = new int[2]; + getLocationInWindow(globalPosition); + + TouchType action = TouchType.ACTION_MOVE; + if ((!activeTouchEvents.containsKey(index))) { + if (up) return; + ControllerNode node = null; + for (ControllerNode item : controllerNodes) { + Vector2 pos = item.getPosition(); + Vector2 size = item.getSize(); + + float cx = (pos.x - globalPosition[0]); + float cy = (pos.y - globalPosition[1]); + if (item.isVisible() && x > cx && x < cx + size.x && y > cy && y < cy + size.y) { + node = item; + break; + } + } + if (node != null) { + activeTouchEvents.put(index, node); + action = TouchType.ACTION_DOWN; + } else { + return; + } + } + + if (up) action = TouchType.ACTION_UP; + + ControllerNode node = activeTouchEvents.get(index); + Vector2 pos = node.getPosition(); + pos.x -= globalPosition[0]; + pos.y -= globalPosition[1]; + + x -= pos.x; + y -= pos.y; + + node.onTouch(new TouchEvent(x, y, action)); + + if (up) { + activeTouchEvents.remove(index); + } + } + + @Override + public void onViewAdded(View child) { + super.onViewAdded(child); + refreshChildren(); + } + + @Override + public void onViewRemoved(View child) { + super.onViewRemoved(child); + refreshChildren(); + } + + // TODO: Need to replace these methods to prevent Android sending events directly to children + + @Override + public ArrayList getTouchables() { + return new ArrayList<>(); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + return true; + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/ControllerNode.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/ControllerNode.java new file mode 100644 index 00000000..7d45550a --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/ControllerNode.java @@ -0,0 +1,21 @@ +package com.panda3ds.pandroid.view.controller; + +import android.view.View; +import androidx.annotation.NonNull; +import com.panda3ds.pandroid.math.Vector2; + +public interface ControllerNode { + @NonNull + default Vector2 getPosition() { + View view = (View) this; + + int[] position = new int[2]; + view.getLocationInWindow(position); + return new Vector2(position[0], position[1]); + } + + default boolean isVisible() { return ((View) this).isShown(); } + + @NonNull Vector2 getSize(); + void onTouch(TouchEvent event); +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/TouchEvent.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/TouchEvent.java new file mode 100644 index 00000000..6b82d201 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/TouchEvent.java @@ -0,0 +1,16 @@ +package com.panda3ds.pandroid.view.controller; + +public class TouchEvent { + private final TouchType action; + private final float x, y; + + public float getX() { return x; } + public float getY() { return y; } + public TouchType getAction() { return action; } + + public TouchEvent(float x, float y, TouchType action) { + this.x = x; + this.y = y; + this.action = action; + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/TouchType.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/TouchType.java new file mode 100644 index 00000000..69772915 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/TouchType.java @@ -0,0 +1,7 @@ +package com.panda3ds.pandroid.view.controller; + +public enum TouchType { + ACTION_DOWN, + ACTION_MOVE, + ACTION_UP +}; \ No newline at end of file diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/listeners/ButtonStateListener.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/listeners/ButtonStateListener.java new file mode 100644 index 00000000..eb5a693a --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/listeners/ButtonStateListener.java @@ -0,0 +1,7 @@ +package com.panda3ds.pandroid.view.controller.listeners; + +import com.panda3ds.pandroid.view.controller.nodes.Button; + +public interface ButtonStateListener { + void onButtonPressedChange(Button button, boolean pressed); +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/listeners/JoystickListener.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/listeners/JoystickListener.java new file mode 100644 index 00000000..77225223 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/listeners/JoystickListener.java @@ -0,0 +1,7 @@ +package com.panda3ds.pandroid.view.controller.listeners; + +import com.panda3ds.pandroid.view.controller.nodes.Joystick; + +public interface JoystickListener { + void onJoystickAxisChange(Joystick joystick, float axisX, float axisY); +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/mapping/ControllerItem.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/mapping/ControllerItem.java new file mode 100644 index 00000000..6be617b5 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/mapping/ControllerItem.java @@ -0,0 +1,9 @@ +package com.panda3ds.pandroid.view.controller.mapping; + +public enum ControllerItem { + START, + SELECT, + L,R, + GAMEPAD, + DPAD, JOYSTICK +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/mapping/ControllerMapper.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/mapping/ControllerMapper.java new file mode 100644 index 00000000..04cb75c8 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/mapping/ControllerMapper.java @@ -0,0 +1,202 @@ +package com.panda3ds.pandroid.view.controller.mapping; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.DashPathEffect; +import android.graphics.Paint; +import android.graphics.drawable.ColorDrawable; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.lang.Function; +import com.panda3ds.pandroid.math.Vector2; + +public class ControllerMapper extends FrameLayout { + public static int COLOR_DARK = Color.rgb(20, 20, 20); + public static int COLOR_LIGHT = Color.rgb(60, 60, 60); + private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + private Profile profile; + private View selectedView; + private final Paint selectionPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private int width = -1; + private int height = -1; + private Function changeListener; + + public ControllerMapper(@NonNull Context context) { + this(context, null); + } + + public ControllerMapper(@NonNull Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public ControllerMapper(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public ControllerMapper(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + setBackground(new ColorDrawable(Color.YELLOW)); + float dp = getResources().getDisplayMetrics().density; + + selectionPaint.setColor(Color.RED); + selectionPaint.setStrokeWidth(dp * 2); + selectionPaint.setStyle(Paint.Style.STROKE); + selectionPaint.setPathEffect(new DashPathEffect(new float[]{dp * 10, dp * 10}, 0.0f)); + } + + public void initialize(Function changeListener, Profile profile) { + this.profile = profile; + this.changeListener = changeListener; + + measure(MeasureSpec.EXACTLY, MeasureSpec.EXACTLY); + + new MoveElementListener(ControllerItem.L, findViewById(R.id.button_l)); + new MoveElementListener(ControllerItem.R, findViewById(R.id.button_r)); + new MoveElementListener(ControllerItem.START, findViewById(R.id.button_start)); + new MoveElementListener(ControllerItem.SELECT, findViewById(R.id.button_select)); + new MoveElementListener(ControllerItem.DPAD, findViewById(R.id.dpad)); + new MoveElementListener(ControllerItem.GAMEPAD, findViewById(R.id.gamepad)); + new MoveElementListener(ControllerItem.JOYSTICK, findViewById(R.id.left_analog)); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + drawBackground(canvas); + if (selectedView != null) { + paint.setColor(Color.argb(30, 255, 0, 0)); + drawSelected(canvas, paint); + } + } + + @Override + protected void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + if (selectedView != null) { + drawSelected(canvas, selectionPaint); + } + } + + public void drawSelected(Canvas canvas, Paint paint) { + int[] absolutePosition = new int[2]; + int[] selectedViewPosition = new int[2]; + + selectedView.getLocationOnScreen(selectedViewPosition); + getLocationOnScreen(absolutePosition); + + int width = selectedView.getLayoutParams().width; + int height = selectedView.getLayoutParams().height; + + int x = selectedViewPosition[0] - absolutePosition[0]; + int y = selectedViewPosition[1] - absolutePosition[1]; + + canvas.drawRect(x, y, x + width, y + height, paint); + } + + + private void drawBackground(Canvas canvas) { + paint.setStyle(Paint.Style.FILL); + + int shapeSize = Math.round(getResources().getDimension(R.dimen.SizePt) * 7.2f); + boolean dark = true; + boolean start = true; + + for (int x = 0; x < width + shapeSize; x += shapeSize) { + for (int y = 0; y < height + shapeSize; y += shapeSize) { + paint.setColor(dark ? COLOR_DARK : COLOR_LIGHT); + canvas.drawRect(x, y, x + shapeSize, y + shapeSize, paint); + dark = !dark; + } + start = !start; + dark = start; + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + int measuredWidth = getMeasuredWidth(); + int measuredHeight = getMeasuredHeight(); + + if (measuredWidth != width || measuredHeight != height) { + width = measuredWidth; + height = measuredHeight; + refreshLayout(); + } + } + + public void refreshLayout() { + if (profile != null) { + profile.applyToView(ControllerItem.L, findViewById(R.id.button_l), width, height); + profile.applyToView(ControllerItem.R, findViewById(R.id.button_r), width, height); + profile.applyToView(ControllerItem.START, findViewById(R.id.button_start), width, height); + profile.applyToView(ControllerItem.SELECT, findViewById(R.id.button_select), width, height); + profile.applyToView(ControllerItem.DPAD, findViewById(R.id.dpad), width, height); + profile.applyToView(ControllerItem.GAMEPAD, findViewById(R.id.gamepad), width, height); + profile.applyToView(ControllerItem.JOYSTICK, findViewById(R.id.left_analog), width, height); + } + } + + public int getCurrentWidth() { + return width; + } + + public int getCurrentHeight() { + return height; + } + + public class MoveElementListener implements OnTouchListener { + private final ControllerItem id; + private final View view; + private final Vector2 downPosition = new Vector2(0.0f, 0.0f); + private boolean down = false; + + public MoveElementListener(ControllerItem id, View view) { + this.view = view; + this.id = id; + this.view.setOnTouchListener(this); + } + + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onTouch(View v, MotionEvent event) { + if (!down) { + down = true; + downPosition.set(event.getX() - (view.getLayoutParams().width / 2.0f), event.getY() - (view.getLayoutParams().height / 2.0f)); + } + + int[] viewPosition = new int[2]; + getLocationOnScreen(viewPosition); + + int x = Math.max(0, Math.min(Math.round(event.getRawX() - viewPosition[0] - downPosition.x), width)); + int y = Math.max(0, Math.min(Math.round(event.getRawY() - viewPosition[1] - downPosition.y), height)); + + profile.setLocation(id, x, y, width, height); + profile.applyToView(id, view, width, height); + + if (changeListener != null) { + changeListener.run(id); + } + + selectedView = view; + + if (event.getAction() == MotionEvent.ACTION_UP) { + selectedView = null; + down = false; + invalidate(); + return false; + } else { + return true; + } + } + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/mapping/ControllerProfileManager.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/mapping/ControllerProfileManager.java new file mode 100644 index 00000000..c5b753d2 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/mapping/ControllerProfileManager.java @@ -0,0 +1,103 @@ +package com.panda3ds.pandroid.view.controller.mapping; + +import android.annotation.SuppressLint; +import android.view.Gravity; + +import com.panda3ds.pandroid.data.GsonConfigParser; +import com.panda3ds.pandroid.utils.Constants; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; + +public class ControllerProfileManager { + + public static final GsonConfigParser parser; + private static final DataModel data; + + static { + parser = new GsonConfigParser(Constants.PREF_SCREEN_CONTROLLER_PROFILES); + data = parser.load(DataModel.class); + if (data.profiles.size() == 0) { + add(makeDefaultProfile()); + } + } + + public static void remove(String id) { + data.profiles.remove(id); + save(); + } + + public static void add(Profile profile) { + data.profiles.put(profile.getId(), profile); + save(); + } + + public static List listAll() { + return new ArrayList<>(data.profiles.values()); + } + + public static int getProfileCount() { + return data.profiles.size(); + } + + public static Profile getDefaultProfile() { + if (data.profiles.containsKey(data.profileId)) { + return data.profiles.get(data.profileId); + } else if (getProfileCount() > 0) { + data.profileId = data.profiles.keySet().iterator().next(); + save(); + return getDefaultProfile(); + } else { + add(makeDefaultProfile()); + return getDefaultProfile(); + } + } + + private static void save() { + if ((!data.profiles.containsKey(data.profileId)) && getProfileCount() > 0) { + data.profileId = data.profiles.keySet().iterator().next(); + } + parser.save(data); + } + + public static Profile makeDefaultProfile() { + return new Profile(UUID.randomUUID().toString(), "Default", createDefaultLayout(), createDefaultLayout()); + } + + @SuppressLint("RtlHardcoded") + public static Layout createDefaultLayout() { + Layout layout = new Layout(); + + layout.setLocation(ControllerItem.L, new Location(39, 145, Gravity.LEFT, true)); + layout.setLocation(ControllerItem.R, new Location(39, 145, Gravity.RIGHT, true)); + + layout.setLocation(ControllerItem.SELECT, new Location(32, 131, Gravity.LEFT, true)); + layout.setLocation(ControllerItem.START, new Location(32, 131, Gravity.RIGHT, true)); + + layout.setLocation(ControllerItem.DPAD, new Location(42, 90, Gravity.LEFT, true)); + layout.setLocation(ControllerItem.JOYSTICK, new Location(74, 45, Gravity.LEFT, true)); + layout.setLocation(ControllerItem.GAMEPAD, new Location(42, 75, Gravity.RIGHT, true)); + + return layout; + } + + public static Profile get(String profile) { + return data.profiles.getOrDefault(profile, null); + } + + public static void setDefaultProfileId(String id) { + if (data.profiles.containsKey(id) && !Objects.equals(id, data.profileId)) { + data.profileId = id; + save(); + } + } + + public static class DataModel { + public final Map profiles = new HashMap<>(); + public String profileId; + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/mapping/Layout.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/mapping/Layout.java new file mode 100644 index 00000000..a2e5f3ce --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/mapping/Layout.java @@ -0,0 +1,36 @@ +package com.panda3ds.pandroid.view.controller.mapping; + + +import androidx.annotation.NonNull; + +import org.jetbrains.annotations.NotNull; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +public class Layout { + private final Map mapLocations = new HashMap<>(); + + public void setLocation(ControllerItem item, Location location) { + mapLocations.put(item, location); + } + + @NotNull + public Location getLocation(ControllerItem item) { + if (!mapLocations.containsKey(item)) { + setLocation(item, new Location()); + } + return Objects.requireNonNull(mapLocations.get(item)); + } + + @NonNull + @Override + public Layout clone() { + Layout cloned = new Layout(); + for (ControllerItem key : mapLocations.keySet()) { + cloned.setLocation(key, getLocation(key).clone()); + } + return cloned; + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/mapping/Location.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/mapping/Location.java new file mode 100644 index 00000000..1d28758a --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/mapping/Location.java @@ -0,0 +1,57 @@ +package com.panda3ds.pandroid.view.controller.mapping; + +import android.view.Gravity; + +import androidx.annotation.NonNull; + +public class Location { + private float x = 0.0f; + private float y = 0.0f; + private int gravity = Gravity.LEFT; + private boolean visible = false; + + public Location() {} + + public Location(float x, float y, int gravity, boolean visible) { + this.x = x; + this.y = y; + this.gravity = gravity; + this.visible = visible; + } + + public int getGravity() { + return gravity; + } + + public float getX() { + return x; + } + + public float getY() { + return y; + } + + public void setVisible(boolean visible) { + this.visible = visible; + } + + public boolean isVisible() { + return visible; + } + + public void setGravity(int gravity) { + this.gravity = gravity; + } + + public void setPosition(float x, float y) { + this.x = x; + this.y = y; + } + + @NonNull + @Override + public Location clone() { + return new Location(x, y, gravity, visible); + } + +} \ No newline at end of file diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/mapping/Profile.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/mapping/Profile.java new file mode 100644 index 00000000..b537133b --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/mapping/Profile.java @@ -0,0 +1,104 @@ +package com.panda3ds.pandroid.view.controller.mapping; + +import android.view.Gravity; +import android.view.View; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; + +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.app.PandroidApplication; + +import java.util.UUID; + +public class Profile { + private final String id; + private final Layout landscapeLayout; + private final Layout portraitLayout; + private String name; + + public Profile(String id, String name, Layout landscape, Layout portrait) { + this.id = id; + this.name = name; + this.landscapeLayout = landscape; + this.portraitLayout = portrait; + } + + public void applyToView(ControllerItem id, View view, int viewportWidth, int viewportHeight) { + float pt = view.getResources().getDimension(R.dimen.SizePt); + + int width = view.getLayoutParams().width; + int height = view.getLayoutParams().height; + + Layout layout = getLayoutBySize(viewportWidth, viewportHeight); + + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(width, height); + Location location = layout.getLocation(id); + + int x = Math.round(location.getX() * pt); + int y = Math.round(location.getY() * pt); + + params.gravity = location.getGravity() | Gravity.BOTTOM; + params.bottomMargin = Math.max(Math.min(y - (height / 2), viewportHeight - height), 0); + + int gravity = location.getGravity() & Gravity.HORIZONTAL_GRAVITY_MASK; + if (gravity == Gravity.RIGHT) { + params.rightMargin = Math.max(x - (width / 2), 0); + } else { + params.leftMargin = Math.max(x - (width / 2), 0); + } + + view.setVisibility(location.isVisible() ? View.VISIBLE : View.GONE); + view.setLayoutParams(params); + } + + public void setLocation(ControllerItem item, int x, int y, int viewportWidth, int viewportHeight) { + float pt = PandroidApplication.getAppContext().getResources().getDimension(R.dimen.SizePt); + + Layout layout = getLayoutBySize(viewportWidth, viewportHeight); + Location location = layout.getLocation(item); + + y = viewportHeight - y; + + if (x < viewportWidth / 2) { + location.setGravity(Gravity.LEFT); + location.setPosition(x / pt, y / pt); + } else { + x = (viewportWidth / 2) - (x - (viewportWidth / 2)); + location.setGravity(Gravity.RIGHT); + location.setPosition(x / pt, y / pt); + } + } + + + public void setName(String name) { + this.name = name; + } + + public void setVisible(ControllerItem id, boolean visible) { + landscapeLayout.getLocation(id).setVisible(visible); + portraitLayout.getLocation(id).setVisible(visible); + } + + private Layout getLayoutBySize(int width, int height) { + return width > height ? landscapeLayout : portraitLayout; + } + + public String getName() { + return name; + } + + public String getId() { + return id; + } + + @NonNull + @Override + public Profile clone() { + return new Profile(id, name, landscapeLayout.clone(), portraitLayout.clone()); + } + + public boolean isVisible(ControllerItem id) { + return landscapeLayout.getLocation(id).isVisible(); + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/nodes/BasicControllerNode.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/nodes/BasicControllerNode.java new file mode 100644 index 00000000..d196c7ec --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/nodes/BasicControllerNode.java @@ -0,0 +1,14 @@ +package com.panda3ds.pandroid.view.controller.nodes; + +import android.content.Context; +import android.util.AttributeSet; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatTextView; +import com.panda3ds.pandroid.view.controller.ControllerNode; + +public abstract class BasicControllerNode extends AppCompatTextView implements ControllerNode { + public BasicControllerNode(@NonNull Context context) { super(context); } + public BasicControllerNode(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); } + public BasicControllerNode(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/nodes/Button.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/nodes/Button.java new file mode 100644 index 00000000..83c38d48 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/nodes/Button.java @@ -0,0 +1,68 @@ +package com.panda3ds.pandroid.view.controller.nodes; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Canvas; +import android.util.AttributeSet; +import android.view.Gravity; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatTextView; +import com.panda3ds.pandroid.math.Vector2; +import com.panda3ds.pandroid.view.controller.ControllerNode; +import com.panda3ds.pandroid.view.controller.TouchEvent; +import com.panda3ds.pandroid.view.controller.TouchType; +import com.panda3ds.pandroid.view.controller.listeners.ButtonStateListener; + +public class Button extends BasicControllerNode { + private boolean pressed = false; + private int width, height; + + private ButtonStateListener stateListener; + + public Button(@NonNull Context context) { + super(context); + init(); + } + + public Button(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + init(); + } + + public Button(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + width = getWidth(); + height = getHeight(); + } + + private void init() { + setTextAlignment(TEXT_ALIGNMENT_CENTER); + setGravity(Gravity.CENTER); + } + + public void setStateListener(ButtonStateListener stateListener) { this.stateListener = stateListener; } + + public boolean isPressed() { return pressed; } + + @NonNull + @Override + public Vector2 getSize() { + return new Vector2(width, height); + } + + @Override + public void onTouch(TouchEvent event) { + pressed = event.getAction() != TouchType.ACTION_UP; + setAlpha(pressed ? 0.2f : 1.0f); + if (stateListener != null) { + stateListener.onButtonPressedChange(this, pressed); + } + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/nodes/Joystick.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/nodes/Joystick.java new file mode 100644 index 00000000..cf33afb6 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/nodes/Joystick.java @@ -0,0 +1,113 @@ +package com.panda3ds.pandroid.view.controller.nodes; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import androidx.annotation.NonNull; + +import com.panda3ds.pandroid.math.Vector2; +import com.panda3ds.pandroid.view.controller.ControllerNode; +import com.panda3ds.pandroid.view.controller.TouchEvent; +import com.panda3ds.pandroid.view.controller.TouchType; +import com.panda3ds.pandroid.view.controller.listeners.JoystickListener; + +public class Joystick extends BasicControllerNode implements ControllerNode { + private float axisX = 0; + private float axisY = 0; + + private int width = 0; + private int height = 0; + private JoystickListener joystickListener; + + public Joystick(Context context) { this(context, null); } + public Joystick(Context context, AttributeSet attrs) { this(context, attrs, 0); } + + public Joystick(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + paint.setColor(Color.RED); + invalidate(); + } + + private final Paint paint = new Paint(); + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + } + + @Override + public void onDrawForeground(Canvas canvas) { + width = getWidth(); + height = getHeight(); + + int analogIconSize = width - getPaddingLeft(); + + float middleIconSize = analogIconSize / 2.0f; + float middle = width / 2.0f; + + float maxDistance = (middle - middleIconSize) * 0.9f; + + float tx = maxDistance * axisX; + float ty = maxDistance * axisY; + + float radius = Vector2.distance(0.0f, 0.0f, Math.abs(tx), Math.abs(ty)); + radius = Math.min(maxDistance, radius); + + double deg = Math.atan2(ty, tx) * (180.0 / Math.PI); + float rx = (float) (radius * Math.cos(2 * Math.PI * deg / 360.0)); + float ry = (float) (radius * Math.sin(2 * Math.PI * deg / 360.0)); + + axisX = Math.max(-1.0f, Math.min(1.0f, axisX)); + axisY = Math.max(-1.0f, Math.min(1.0f, axisY)); + + float x = middle - middleIconSize + rx; + float y = middle - middleIconSize + ry; + + Drawable foreground = getForeground(); + if (foreground != null) { + foreground.setBounds((int) x, (int) y, (int) (x + analogIconSize), (int) (y + analogIconSize)); + foreground.draw(canvas); + } else { + canvas.drawOval(x, y, x + analogIconSize, y + analogIconSize, paint); + } + } + + public Vector2 getAxis() { return new Vector2(Math.max(-1.0f, Math.min(1.0f, axisX)), Math.max(-1.0f, Math.min(1.0f, axisY))); } + + public void setJoystickListener(JoystickListener joystickListener) { this.joystickListener = joystickListener; } + + @NonNull + @Override + public Vector2 getSize() { + return new Vector2(width, height); + } + + @Override + public void onTouch(TouchEvent event) { + float middle = width / 2.0f; + + float x = event.getX(); + float y = event.getY(); + + x = Math.max(0, Math.min(middle * 2, x)); + y = Math.max(0, Math.min(middle * 2, y)); + + axisX = ((x - middle) / middle); + axisY = ((y - middle) / middle); + + if (event.getAction() == TouchType.ACTION_UP) { + axisX = 0; + axisY = 0; + } + + if (joystickListener != null) { + joystickListener.onJoystickAxisChange(this, axisX, axisY); + } + + invalidate(); + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/nodes/TouchScreenNodeImpl.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/nodes/TouchScreenNodeImpl.java new file mode 100644 index 00000000..bf51d4fe --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/nodes/TouchScreenNodeImpl.java @@ -0,0 +1,37 @@ +package com.panda3ds.pandroid.view.controller.nodes; + +import android.graphics.Rect; +import android.view.View; + +import com.panda3ds.pandroid.AlberDriver; +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.view.controller.ControllerNode; +import com.panda3ds.pandroid.view.controller.TouchEvent; +import com.panda3ds.pandroid.view.controller.TouchType; +import com.panda3ds.pandroid.view.renderer.ConsoleRenderer; + +public interface TouchScreenNodeImpl extends ControllerNode { + default void onTouchScreenPress(ConsoleRenderer renderer, TouchEvent event) { + View view = (View) this; + boolean hasDownEvent = view.getTag(R.id.TagEventHasDown) != null && (boolean) view.getTag(R.id.TagEventHasDown); + + Rect bounds = renderer.getLayout().getBottomDisplayBounds(); + + if (event.getX() >= bounds.left && event.getY() >= bounds.top && event.getX() <= bounds.right && event.getY() <= bounds.bottom) { + int x = (int) (event.getX() - bounds.left); + int y = (int) (event.getY() - bounds.top); + + x = Math.round((x / (float) bounds.width()) * 320); + y = Math.round((y / (float) bounds.height()) * 240); + + AlberDriver.TouchScreenDown(x, y); + + view.setTag(R.id.TagEventHasDown, true); + } + + if (hasDownEvent && event.getAction() == TouchType.ACTION_UP) { + AlberDriver.TouchScreenUp(); + view.setTag(R.id.TagEventHasDown, false); + } + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/gamesgrid/GameAdapter.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/gamesgrid/GameAdapter.java new file mode 100644 index 00000000..1a3febd4 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/gamesgrid/GameAdapter.java @@ -0,0 +1,42 @@ +package com.panda3ds.pandroid.view.gamesgrid; + +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.data.game.GameMetadata; + +import java.util.ArrayList; +import java.util.List; + +class GameAdapter extends RecyclerView.Adapter { + private final ArrayList games = new ArrayList<>(); + + @NonNull + @Override + public ItemHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new ItemHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.holder_game, parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull ItemHolder holder, int position) { + holder.apply(games.get(position)); + } + + public void replace(List games) { + int oldCount = getItemCount(); + this.games.clear(); + notifyItemRangeRemoved(0, oldCount); + this.games.addAll(games); + notifyItemRangeInserted(0, getItemCount()); + } + + @Override + public int getItemCount() { + return games.size(); + } + +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/gamesgrid/GameIconView.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/gamesgrid/GameIconView.java new file mode 100644 index 00000000..e73d8d08 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/gamesgrid/GameIconView.java @@ -0,0 +1,41 @@ +package com.panda3ds.pandroid.view.gamesgrid; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatImageView; + +public class GameIconView extends AppCompatImageView { + public GameIconView(@NonNull Context context) { + super(context); + } + + public GameIconView(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public GameIconView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + int size = getMeasuredWidth(); + setMeasuredDimension(size, size); + } + + @Override + public void setImageBitmap(Bitmap bm) { + super.setImageBitmap(bm); + Drawable bitmapDrawable = getDrawable(); + if (bitmapDrawable instanceof BitmapDrawable) { + bitmapDrawable.setFilterBitmap(false); + } + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/gamesgrid/GamesGridView.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/gamesgrid/GamesGridView.java new file mode 100644 index 00000000..24e65e2f --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/gamesgrid/GamesGridView.java @@ -0,0 +1,35 @@ +package com.panda3ds.pandroid.view.gamesgrid; + +import android.content.Context; +import android.util.AttributeSet; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +import com.panda3ds.pandroid.data.game.GameMetadata; +import com.panda3ds.pandroid.view.recycler.AutoFitGridLayout; + +import java.util.List; + +public class GamesGridView extends RecyclerView { + private final GameAdapter adapter; + + public GamesGridView(@NonNull Context context) { + this(context, null); + } + + public GamesGridView(@NonNull Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public GamesGridView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + setLayoutManager(new AutoFitGridLayout(getContext(), 170)); + setAdapter(adapter = new GameAdapter()); + } + + public void setGameList(List games) { + adapter.replace(games); + } +} \ No newline at end of file diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/gamesgrid/ItemHolder.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/gamesgrid/ItemHolder.java new file mode 100644 index 00000000..54f86dae --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/gamesgrid/ItemHolder.java @@ -0,0 +1,30 @@ +package com.panda3ds.pandroid.view.gamesgrid; + +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.appcompat.widget.AppCompatTextView; +import androidx.recyclerview.widget.RecyclerView; + +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.data.game.GameMetadata; +import com.panda3ds.pandroid.utils.GameUtils; + +class ItemHolder extends RecyclerView.ViewHolder { + public ItemHolder(@NonNull View itemView) { + super(itemView); + } + + public void apply(GameMetadata game) { + ((AppCompatTextView) itemView.findViewById(R.id.title)) + .setText(game.getTitle()); + ((GameIconView) itemView.findViewById(R.id.icon)) + .setImageBitmap(game.getIcon()); + ((AppCompatTextView) itemView.findViewById(R.id.description)) + .setText(game.getPublisher()); + + itemView.setOnClickListener((v) -> { + GameUtils.launch(v.getContext(), game); + }); + } +} \ No newline at end of file diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/preferences/SingleSelectionPreferences.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/preferences/SingleSelectionPreferences.java new file mode 100644 index 00000000..49fabd6a --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/preferences/SingleSelectionPreferences.java @@ -0,0 +1,86 @@ +package com.panda3ds.pandroid.view.preferences; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.util.AttributeSet; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; + +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.utils.Constants; + +public class SingleSelectionPreferences extends PreferenceCategory implements Preference.OnPreferenceClickListener { + private final Drawable transparent = new ColorDrawable(Color.TRANSPARENT); + private final Drawable doneDrawable = ContextCompat.getDrawable(getContext(), R.drawable.ic_done); + + public SingleSelectionPreferences(@NonNull Context context) { + super(context); + } + + public SingleSelectionPreferences(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public SingleSelectionPreferences(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public SingleSelectionPreferences(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + { + try { + TypedArray color = getContext().obtainStyledAttributes(new int[]{ + android.R.attr.textColorSecondary + }); + doneDrawable.setTint(color.getColor(0, Color.RED)); + color.recycle(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + color.close(); + } + } catch (Exception e) { + Log.e(Constants.LOG_TAG, "Error on obtain text color secondary: ", e); + } + } + + @Override + public void onAttached() { + super.onAttached(); + + for (int i = 0; i < getPreferenceCount();i++) { + getPreference(i).setOnPreferenceClickListener(this); + } + } + + public void setSelectedItem(int index) { + onPreferenceClick(getPreference(index)); + } + + @Override + public boolean onPreferenceClick(@NonNull Preference preference) { + int index = 0; + + for (int i = 0; i < getPreferenceCount(); i++) { + Preference item = getPreference(i); + if (item == preference) { + index = i; + item.setIcon(R.drawable.ic_done); + } else { + item.setIcon(transparent); + } + } + + callChangeListener(index); + return false; + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/recycler/AutoFitGridLayout.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/recycler/AutoFitGridLayout.java new file mode 100644 index 00000000..26f80adb --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/recycler/AutoFitGridLayout.java @@ -0,0 +1,33 @@ +package com.panda3ds.pandroid.view.recycler; + +import android.content.Context; +import android.util.TypedValue; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +public final class AutoFitGridLayout extends GridLayoutManager { + private final int iconSize; + private final Context context; + + public AutoFitGridLayout(Context context, int iconSize) { + super(context, 1); + + this.iconSize = iconSize; + this.context = context; + } + + @Override + public void onMeasure(@NonNull RecyclerView.Recycler recycler, @NonNull RecyclerView.State state, int widthSpec, int heightSpec) { + super.onMeasure(recycler, state, widthSpec, heightSpec); + int width = View.MeasureSpec.getSize(widthSpec); + int iconSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this.iconSize, context.getResources().getDisplayMetrics()); + int iconCount = Math.max(1, width / iconSize); + + if (getSpanCount() != iconCount) { + setSpanCount(iconCount); + } + } +} \ No newline at end of file diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/recycler/SimpleListAdapter.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/recycler/SimpleListAdapter.java new file mode 100644 index 00000000..7d4fa7c3 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/recycler/SimpleListAdapter.java @@ -0,0 +1,76 @@ +package com.panda3ds.pandroid.view.recycler; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.LayoutRes; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +public class SimpleListAdapter extends RecyclerView.Adapter { + private final ArrayList list = new ArrayList<>(); + private final Binder binder; + private final int layoutId; + + public SimpleListAdapter(@LayoutRes int layoutId, Binder binder) { + this.layoutId = layoutId; + this.binder = binder; + } + + @NonNull + @Override + public Holder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new Holder(LayoutInflater.from(parent.getContext()).inflate(layoutId, parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull Holder holder, int position) { + binder.bind(position, list.get(position), holder.getView()); + } + + public void addAll(T... items) { + addAll(Arrays.asList(items)); + } + + public void addAll(List items) { + int index = list.size(); + this.list.addAll(items); + notifyItemRangeInserted(index, getItemCount() - index); + } + + public void clear() { + int count = getItemCount(); + list.clear(); + notifyItemRangeRemoved(0, count); + } + + public void sort(Comparator comparator) { + list.sort(comparator); + notifyItemRangeChanged(0, getItemCount()); + } + + @Override + public int getItemCount() { + return list.size(); + } + + public interface Binder { + void bind(int position, I item, View view); + } + + public static class Holder extends RecyclerView.ViewHolder { + public Holder(@NonNull View itemView) { + super(itemView); + } + + public View getView() { + return itemView; + } + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/renderer/ConsoleRenderer.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/renderer/ConsoleRenderer.java new file mode 100644 index 00000000..d5c99e8d --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/renderer/ConsoleRenderer.java @@ -0,0 +1,9 @@ +package com.panda3ds.pandroid.view.renderer; + +import com.panda3ds.pandroid.view.renderer.layout.ConsoleLayout; + +public interface ConsoleRenderer { + void setLayout(ConsoleLayout layout); + ConsoleLayout getLayout(); + String getBackendName(); +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/renderer/layout/ConsoleLayout.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/renderer/layout/ConsoleLayout.java new file mode 100644 index 00000000..7ec00974 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/renderer/layout/ConsoleLayout.java @@ -0,0 +1,13 @@ +package com.panda3ds.pandroid.view.renderer.layout; + +import android.graphics.Rect; + +public interface ConsoleLayout { + void update(int screenWidth, int screenHeight); + + void setBottomDisplaySourceSize(int width, int height); + void setTopDisplaySourceSize(int width, int height); + + Rect getBottomDisplayBounds(); + Rect getTopDisplayBounds(); +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/renderer/layout/DefaultScreenLayout.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/renderer/layout/DefaultScreenLayout.java new file mode 100644 index 00000000..a726b2e6 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/renderer/layout/DefaultScreenLayout.java @@ -0,0 +1,77 @@ +package com.panda3ds.pandroid.view.renderer.layout; + +import android.graphics.Rect; + +import com.panda3ds.pandroid.math.Vector2; + +public class DefaultScreenLayout implements ConsoleLayout { + private final Rect topDisplay = new Rect(); + private final Rect bottomDisplay = new Rect(); + + private final Vector2 screenSize = new Vector2(1.0f, 1.0f); + private final Vector2 topSourceSize = new Vector2(1.0f, 1.0f); + private final Vector2 bottomSourceSize = new Vector2(1.0f, 1.0f); + + @Override + public void update(int screenWidth, int screenHeight) { + screenSize.set(screenWidth, screenHeight); + updateBounds(); + } + + @Override + public void setBottomDisplaySourceSize(int width, int height) { + bottomSourceSize.set(width, height); + updateBounds(); + } + @Override + public void setTopDisplaySourceSize(int width, int height) { + topSourceSize.set(width, height); + updateBounds(); + } + + private void updateBounds() { + int screenWidth = (int) screenSize.x; + int screenHeight = (int) screenSize.y; + + if (screenWidth > screenHeight) { + int topDisplayWidth = (int) ((screenHeight / topSourceSize.y) * topSourceSize.x); + int topDisplayHeight = screenHeight; + + if (topDisplayWidth > (screenWidth * 0.7)) { + topDisplayWidth = (int) (screenWidth * 0.7); + topDisplayHeight = (int) ((topDisplayWidth / topSourceSize.x) * topSourceSize.y); + } + + int bottomDisplayHeight = (int) (((screenWidth - topDisplayWidth) / bottomSourceSize.x) * bottomSourceSize.y); + + topDisplay.set(0, 0, topDisplayWidth, topDisplayHeight); + bottomDisplay.set(topDisplayWidth, 0, topDisplayWidth + (screenWidth - topDisplayWidth), bottomDisplayHeight); + } else { + int topScreenHeight = (int) ((screenWidth / topSourceSize.x) * topSourceSize.y); + topDisplay.set(0, 0, screenWidth, topScreenHeight); + + int bottomDisplayHeight = (int) ((screenWidth / bottomSourceSize.x) * bottomSourceSize.y); + int bottomDisplayWidth = screenWidth; + int bottomDisplayX = 0; + + if (topScreenHeight + bottomDisplayHeight > screenHeight) { + bottomDisplayHeight = (screenHeight - topScreenHeight); + bottomDisplayWidth = (int) ((bottomDisplayHeight / bottomSourceSize.y) * bottomSourceSize.x); + bottomDisplayX = (screenWidth - bottomDisplayX) / 2; + } + + topDisplay.set(0, 0, screenWidth, topScreenHeight); + bottomDisplay.set(bottomDisplayX, topScreenHeight, bottomDisplayX + bottomDisplayWidth, topScreenHeight + bottomDisplayHeight); + } + } + + @Override + public Rect getBottomDisplayBounds() { + return bottomDisplay; + } + + @Override + public Rect getTopDisplayBounds() { + return topDisplay; + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/utils/PerformanceView.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/utils/PerformanceView.java new file mode 100644 index 00000000..e4d7be15 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/utils/PerformanceView.java @@ -0,0 +1,64 @@ +package com.panda3ds.pandroid.view.utils; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.text.Html; +import android.util.AttributeSet; +import android.util.TypedValue; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatTextView; + +import com.panda3ds.pandroid.data.config.GlobalConfig; +import com.panda3ds.pandroid.utils.PerformanceMonitor; + +public class PerformanceView extends AppCompatTextView { + private boolean running = false; + + public PerformanceView(@NonNull Context context) { + this(context, null); + } + + public PerformanceView(@NonNull Context context, @Nullable AttributeSet attrs) { + this(context, attrs,0); + } + + public PerformanceView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + int padding = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10, getResources().getDisplayMetrics()); + setPadding(padding,padding,padding,padding); + setTextColor(Color.WHITE); + setShadowLayer(padding,0,0,Color.BLACK); + } + + public void refresh(){ + running = isShown(); + if (!running) { + return; + } + + String debug = ""; + + // Calculate total memory in MB and the current memory usage + int memoryTotalMb = (int) Math.round(PerformanceMonitor.getTotalMemory() / (1024.0 * 1024.0)); + int memoryUsageMb = (int) Math.round(PerformanceMonitor.getUsedMemory() / (1024.0 * 1024.0)); + + debug += "FPS: " + PerformanceMonitor.getFps() + "
"; + debug += "RAM: " + Math.round(((float) memoryUsageMb / memoryTotalMb) * 100) + "% (" + memoryUsageMb + "MB/" + memoryTotalMb + "MB)
"; + debug += "BACKEND: " + PerformanceMonitor.getBackend() + (GlobalConfig.get(GlobalConfig.KEY_SHADER_JIT) ? " + JIT" : "") + "
"; + setText(Html.fromHtml(debug, Html.FROM_HTML_MODE_COMPACT)); + postDelayed(this::refresh, 250); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + if (!running) { + refresh(); + } + } +} diff --git a/src/pandroid/app/src/main/jniLibs/.gitignore b/src/pandroid/app/src/main/jniLibs/.gitignore new file mode 100644 index 00000000..0b469178 --- /dev/null +++ b/src/pandroid/app/src/main/jniLibs/.gitignore @@ -0,0 +1,2 @@ +# Prebuilt Alber libraries will be placed in this directory, but we don't want to push them to the repo +libAlber.so \ No newline at end of file diff --git a/src/pandroid/app/src/main/jniLibs/arm64-v8a/.gitkeep b/src/pandroid/app/src/main/jniLibs/arm64-v8a/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/pandroid/app/src/main/jniLibs/x86_64/.gitkeep b/src/pandroid/app/src/main/jniLibs/x86_64/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/pandroid/app/src/main/res/color/bottom_navigation_indicator_tint.xml b/src/pandroid/app/src/main/res/color/bottom_navigation_indicator_tint.xml new file mode 100644 index 00000000..e0fc9112 --- /dev/null +++ b/src/pandroid/app/src/main/res/color/bottom_navigation_indicator_tint.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/color/red_color.xml b/src/pandroid/app/src/main/res/color/red_color.xml new file mode 100644 index 00000000..c42336f5 --- /dev/null +++ b/src/pandroid/app/src/main/res/color/red_color.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/color/text_secondary_dark.xml b/src/pandroid/app/src/main/res/color/text_secondary_dark.xml new file mode 100644 index 00000000..53a9cd15 --- /dev/null +++ b/src/pandroid/app/src/main/res/color/text_secondary_dark.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/color/text_secondary_light.xml b/src/pandroid/app/src/main/res/color/text_secondary_light.xml new file mode 100644 index 00000000..35136058 --- /dev/null +++ b/src/pandroid/app/src/main/res/color/text_secondary_light.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/drawable/alert_dialog_background.xml b/src/pandroid/app/src/main/res/drawable/alert_dialog_background.xml new file mode 100644 index 00000000..20a71e46 --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/alert_dialog_background.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/drawable/analog_background.xml b/src/pandroid/app/src/main/res/drawable/analog_background.xml new file mode 100644 index 00000000..3f3747cd --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/analog_background.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + diff --git a/src/pandroid/app/src/main/res/drawable/analog_foreground.xml b/src/pandroid/app/src/main/res/drawable/analog_foreground.xml new file mode 100644 index 00000000..c339e69f --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/analog_foreground.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/pandroid/app/src/main/res/drawable/button_a.xml b/src/pandroid/app/src/main/res/drawable/button_a.xml new file mode 100644 index 00000000..d04306b9 --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/button_a.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + diff --git a/src/pandroid/app/src/main/res/drawable/button_b.xml b/src/pandroid/app/src/main/res/drawable/button_b.xml new file mode 100644 index 00000000..1611f28d --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/button_b.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + diff --git a/src/pandroid/app/src/main/res/drawable/button_l.xml b/src/pandroid/app/src/main/res/drawable/button_l.xml new file mode 100644 index 00000000..a2b2f5ad --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/button_l.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + diff --git a/src/pandroid/app/src/main/res/drawable/button_r.xml b/src/pandroid/app/src/main/res/drawable/button_r.xml new file mode 100644 index 00000000..24cf739b --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/button_r.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + diff --git a/src/pandroid/app/src/main/res/drawable/button_select.xml b/src/pandroid/app/src/main/res/drawable/button_select.xml new file mode 100644 index 00000000..c71a3bff --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/button_select.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + diff --git a/src/pandroid/app/src/main/res/drawable/button_start.xml b/src/pandroid/app/src/main/res/drawable/button_start.xml new file mode 100644 index 00000000..33e0a58b --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/button_start.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + diff --git a/src/pandroid/app/src/main/res/drawable/button_x.xml b/src/pandroid/app/src/main/res/drawable/button_x.xml new file mode 100644 index 00000000..4050c539 --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/button_x.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + diff --git a/src/pandroid/app/src/main/res/drawable/button_y.xml b/src/pandroid/app/src/main/res/drawable/button_y.xml new file mode 100644 index 00000000..87799691 --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/button_y.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + diff --git a/src/pandroid/app/src/main/res/drawable/dpad_down.xml b/src/pandroid/app/src/main/res/drawable/dpad_down.xml new file mode 100644 index 00000000..3a904b2d --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/dpad_down.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + diff --git a/src/pandroid/app/src/main/res/drawable/dpad_left.xml b/src/pandroid/app/src/main/res/drawable/dpad_left.xml new file mode 100644 index 00000000..8dad75be --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/dpad_left.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + diff --git a/src/pandroid/app/src/main/res/drawable/dpad_right.xml b/src/pandroid/app/src/main/res/drawable/dpad_right.xml new file mode 100644 index 00000000..e0bea7ec --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/dpad_right.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + diff --git a/src/pandroid/app/src/main/res/drawable/dpad_up.xml b/src/pandroid/app/src/main/res/drawable/dpad_up.xml new file mode 100644 index 00000000..4bc0a96b --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/dpad_up.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + diff --git a/src/pandroid/app/src/main/res/drawable/ic_add.xml b/src/pandroid/app/src/main/res/drawable/ic_add.xml new file mode 100644 index 00000000..db709d0b --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/ic_add.xml @@ -0,0 +1,5 @@ + + + diff --git a/src/pandroid/app/src/main/res/drawable/ic_code.xml b/src/pandroid/app/src/main/res/drawable/ic_code.xml new file mode 100644 index 00000000..8ef40bd2 --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/ic_code.xml @@ -0,0 +1,5 @@ + + + diff --git a/src/pandroid/app/src/main/res/drawable/ic_delete.xml b/src/pandroid/app/src/main/res/drawable/ic_delete.xml new file mode 100644 index 00000000..1efcc802 --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/ic_delete.xml @@ -0,0 +1,5 @@ + + + diff --git a/src/pandroid/app/src/main/res/drawable/ic_done.xml b/src/pandroid/app/src/main/res/drawable/ic_done.xml new file mode 100644 index 00000000..25e52514 --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/ic_done.xml @@ -0,0 +1,5 @@ + + + diff --git a/src/pandroid/app/src/main/res/drawable/ic_edit.xml b/src/pandroid/app/src/main/res/drawable/ic_edit.xml new file mode 100644 index 00000000..1c9bd3e6 --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/ic_edit.xml @@ -0,0 +1,5 @@ + + + diff --git a/src/pandroid/app/src/main/res/drawable/ic_exit.xml b/src/pandroid/app/src/main/res/drawable/ic_exit.xml new file mode 100644 index 00000000..a17ca78b --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/ic_exit.xml @@ -0,0 +1,5 @@ + + + diff --git a/src/pandroid/app/src/main/res/drawable/ic_key_a.xml b/src/pandroid/app/src/main/res/drawable/ic_key_a.xml new file mode 100644 index 00000000..3081c462 --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/ic_key_a.xml @@ -0,0 +1,5 @@ + + + diff --git a/src/pandroid/app/src/main/res/drawable/ic_keyboard_hide.xml b/src/pandroid/app/src/main/res/drawable/ic_keyboard_hide.xml new file mode 100644 index 00000000..d4e7929b --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/ic_keyboard_hide.xml @@ -0,0 +1,5 @@ + + + diff --git a/src/pandroid/app/src/main/res/drawable/ic_launcher_background.xml b/src/pandroid/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..07d5da9c --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pandroid/app/src/main/res/drawable/ic_launcher_foreground.xml b/src/pandroid/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 00000000..2b068d11 --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/drawable/ic_play.xml b/src/pandroid/app/src/main/res/drawable/ic_play.xml new file mode 100644 index 00000000..d75d7da3 --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/ic_play.xml @@ -0,0 +1,5 @@ + + + diff --git a/src/pandroid/app/src/main/res/drawable/ic_rotate_screen.xml b/src/pandroid/app/src/main/res/drawable/ic_rotate_screen.xml new file mode 100644 index 00000000..4e95fb72 --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/ic_rotate_screen.xml @@ -0,0 +1,5 @@ + + + diff --git a/src/pandroid/app/src/main/res/drawable/ic_save.xml b/src/pandroid/app/src/main/res/drawable/ic_save.xml new file mode 100644 index 00000000..82070aa2 --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/ic_save.xml @@ -0,0 +1,5 @@ + + + diff --git a/src/pandroid/app/src/main/res/drawable/ic_search.xml b/src/pandroid/app/src/main/res/drawable/ic_search.xml new file mode 100644 index 00000000..a5687c63 --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/ic_search.xml @@ -0,0 +1,5 @@ + + + diff --git a/src/pandroid/app/src/main/res/drawable/ic_settings.xml b/src/pandroid/app/src/main/res/drawable/ic_settings.xml new file mode 100644 index 00000000..298a5a1f --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/ic_settings.xml @@ -0,0 +1,5 @@ + + + diff --git a/src/pandroid/app/src/main/res/drawable/ic_shortcut.xml b/src/pandroid/app/src/main/res/drawable/ic_shortcut.xml new file mode 100644 index 00000000..13be1ace --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/ic_shortcut.xml @@ -0,0 +1,5 @@ + + + diff --git a/src/pandroid/app/src/main/res/drawable/ic_tab.xml b/src/pandroid/app/src/main/res/drawable/ic_tab.xml new file mode 100644 index 00000000..3f7efd95 --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/ic_tab.xml @@ -0,0 +1,5 @@ + + + diff --git a/src/pandroid/app/src/main/res/drawable/ic_theme.xml b/src/pandroid/app/src/main/res/drawable/ic_theme.xml new file mode 100644 index 00000000..c3d2c7b3 --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/ic_theme.xml @@ -0,0 +1,5 @@ + + + diff --git a/src/pandroid/app/src/main/res/drawable/ic_videogame.xml b/src/pandroid/app/src/main/res/drawable/ic_videogame.xml new file mode 100644 index 00000000..8693be5f --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/ic_videogame.xml @@ -0,0 +1,5 @@ + + + diff --git a/src/pandroid/app/src/main/res/drawable/ic_visibility.xml b/src/pandroid/app/src/main/res/drawable/ic_visibility.xml new file mode 100644 index 00000000..b923c39d --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/ic_visibility.xml @@ -0,0 +1,5 @@ + + + diff --git a/src/pandroid/app/src/main/res/drawable/screen_gamepad_checkbox.xml b/src/pandroid/app/src/main/res/drawable/screen_gamepad_checkbox.xml new file mode 100644 index 00000000..8c61f4c8 --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/screen_gamepad_checkbox.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/drawable/screen_gamepad_hide.xml b/src/pandroid/app/src/main/res/drawable/screen_gamepad_hide.xml new file mode 100644 index 00000000..f22e8c9d --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/screen_gamepad_hide.xml @@ -0,0 +1,9 @@ + + + diff --git a/src/pandroid/app/src/main/res/drawable/screen_gamepad_show.xml b/src/pandroid/app/src/main/res/drawable/screen_gamepad_show.xml new file mode 100644 index 00000000..133f2e70 --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/screen_gamepad_show.xml @@ -0,0 +1,5 @@ + + + diff --git a/src/pandroid/app/src/main/res/drawable/search_bar_background.xml b/src/pandroid/app/src/main/res/drawable/search_bar_background.xml new file mode 100644 index 00000000..44a1c5b4 --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/search_bar_background.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/drawable/simple_card_background.xml b/src/pandroid/app/src/main/res/drawable/simple_card_background.xml new file mode 100644 index 00000000..88845ce4 --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/simple_card_background.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/layout-land/activity_main.xml b/src/pandroid/app/src/main/res/layout-land/activity_main.xml new file mode 100644 index 00000000..fa4cfbca --- /dev/null +++ b/src/pandroid/app/src/main/res/layout-land/activity_main.xml @@ -0,0 +1,30 @@ + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/layout/activity_code_editor.xml b/src/pandroid/app/src/main/res/layout/activity_code_editor.xml new file mode 100644 index 00000000..5cef8609 --- /dev/null +++ b/src/pandroid/app/src/main/res/layout/activity_code_editor.xml @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/layout/activity_input_map.xml b/src/pandroid/app/src/main/res/layout/activity_input_map.xml new file mode 100644 index 00000000..cbacc64e --- /dev/null +++ b/src/pandroid/app/src/main/res/layout/activity_input_map.xml @@ -0,0 +1,23 @@ + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/layout/activity_main.xml b/src/pandroid/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..6de64020 --- /dev/null +++ b/src/pandroid/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,29 @@ + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/layout/activity_preference.xml b/src/pandroid/app/src/main/res/layout/activity_preference.xml new file mode 100644 index 00000000..54b3d364 --- /dev/null +++ b/src/pandroid/app/src/main/res/layout/activity_preference.xml @@ -0,0 +1,19 @@ + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/layout/controller_dpad.xml b/src/pandroid/app/src/main/res/layout/controller_dpad.xml new file mode 100644 index 00000000..b6f0f626 --- /dev/null +++ b/src/pandroid/app/src/main/res/layout/controller_dpad.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/layout/controller_gamepad.xml b/src/pandroid/app/src/main/res/layout/controller_gamepad.xml new file mode 100644 index 00000000..b2186a9d --- /dev/null +++ b/src/pandroid/app/src/main/res/layout/controller_gamepad.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/layout/controller_joystick.xml b/src/pandroid/app/src/main/res/layout/controller_joystick.xml new file mode 100644 index 00000000..d758822f --- /dev/null +++ b/src/pandroid/app/src/main/res/layout/controller_joystick.xml @@ -0,0 +1,11 @@ + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/layout/controller_l.xml b/src/pandroid/app/src/main/res/layout/controller_l.xml new file mode 100644 index 00000000..96ec7582 --- /dev/null +++ b/src/pandroid/app/src/main/res/layout/controller_l.xml @@ -0,0 +1,10 @@ + + diff --git a/src/pandroid/app/src/main/res/layout/controller_r.xml b/src/pandroid/app/src/main/res/layout/controller_r.xml new file mode 100644 index 00000000..8aa2fba1 --- /dev/null +++ b/src/pandroid/app/src/main/res/layout/controller_r.xml @@ -0,0 +1,10 @@ + + diff --git a/src/pandroid/app/src/main/res/layout/controller_select.xml b/src/pandroid/app/src/main/res/layout/controller_select.xml new file mode 100644 index 00000000..a77855be --- /dev/null +++ b/src/pandroid/app/src/main/res/layout/controller_select.xml @@ -0,0 +1,10 @@ + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/layout/controller_start.xml b/src/pandroid/app/src/main/res/layout/controller_start.xml new file mode 100644 index 00000000..94f12eb7 --- /dev/null +++ b/src/pandroid/app/src/main/res/layout/controller_start.xml @@ -0,0 +1,10 @@ + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/layout/dialog_lua_scripts.xml b/src/pandroid/app/src/main/res/layout/dialog_lua_scripts.xml new file mode 100644 index 00000000..69a9d0a4 --- /dev/null +++ b/src/pandroid/app/src/main/res/layout/dialog_lua_scripts.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/layout/drawer_game_container.xml b/src/pandroid/app/src/main/res/layout/drawer_game_container.xml new file mode 100644 index 00000000..4b094e75 --- /dev/null +++ b/src/pandroid/app/src/main/res/layout/drawer_game_container.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/layout/fragment_game_drawer.xml b/src/pandroid/app/src/main/res/layout/fragment_game_drawer.xml new file mode 100644 index 00000000..fa81a503 --- /dev/null +++ b/src/pandroid/app/src/main/res/layout/fragment_game_drawer.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/layout/fragment_games.xml b/src/pandroid/app/src/main/res/layout/fragment_games.xml new file mode 100644 index 00000000..ee69fdc8 --- /dev/null +++ b/src/pandroid/app/src/main/res/layout/fragment_games.xml @@ -0,0 +1,25 @@ + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/layout/fragment_search.xml b/src/pandroid/app/src/main/res/layout/fragment_search.xml new file mode 100644 index 00000000..5872a404 --- /dev/null +++ b/src/pandroid/app/src/main/res/layout/fragment_search.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/layout/game_activity.xml b/src/pandroid/app/src/main/res/layout/game_activity.xml new file mode 100644 index 00000000..9677ac70 --- /dev/null +++ b/src/pandroid/app/src/main/res/layout/game_activity.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/layout/holder_game.xml b/src/pandroid/app/src/main/res/layout/holder_game.xml new file mode 100644 index 00000000..b2d46d1b --- /dev/null +++ b/src/pandroid/app/src/main/res/layout/holder_game.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/layout/holder_lua_script.xml b/src/pandroid/app/src/main/res/layout/holder_lua_script.xml new file mode 100644 index 00000000..a1865c3f --- /dev/null +++ b/src/pandroid/app/src/main/res/layout/holder_lua_script.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/layout/preference_controller_mapper.xml b/src/pandroid/app/src/main/res/layout/preference_controller_mapper.xml new file mode 100644 index 00000000..daecd2b2 --- /dev/null +++ b/src/pandroid/app/src/main/res/layout/preference_controller_mapper.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/layout/preference_simple_about.xml b/src/pandroid/app/src/main/res/layout/preference_simple_about.xml new file mode 100644 index 00000000..9364de36 --- /dev/null +++ b/src/pandroid/app/src/main/res/layout/preference_simple_about.xml @@ -0,0 +1,29 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/layout/preference_start_item.xml b/src/pandroid/app/src/main/res/layout/preference_start_item.xml new file mode 100644 index 00000000..ecb10436 --- /dev/null +++ b/src/pandroid/app/src/main/res/layout/preference_start_item.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/menu/game_drawer_actions.xml b/src/pandroid/app/src/main/res/menu/game_drawer_actions.xml new file mode 100644 index 00000000..9fd3264a --- /dev/null +++ b/src/pandroid/app/src/main/res/menu/game_drawer_actions.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/menu/game_drawer_others.xml b/src/pandroid/app/src/main/res/menu/game_drawer_others.xml new file mode 100644 index 00000000..b6dd4897 --- /dev/null +++ b/src/pandroid/app/src/main/res/menu/game_drawer_others.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/menu/main_activity_navigation.xml b/src/pandroid/app/src/main/res/menu/main_activity_navigation.xml new file mode 100644 index 00000000..f457d8cf --- /dev/null +++ b/src/pandroid/app/src/main/res/menu/main_activity_navigation.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/src/pandroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..6f3b755b --- /dev/null +++ b/src/pandroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/src/pandroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000..6f3b755b --- /dev/null +++ b/src/pandroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/src/pandroid/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 00000000..c209e78e Binary files /dev/null and b/src/pandroid/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/src/pandroid/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/src/pandroid/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 00000000..b2dfe3d1 Binary files /dev/null and b/src/pandroid/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/src/pandroid/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/src/pandroid/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 00000000..4f0f1d64 Binary files /dev/null and b/src/pandroid/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/src/pandroid/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/src/pandroid/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 00000000..62b611da Binary files /dev/null and b/src/pandroid/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/src/pandroid/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/src/pandroid/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 00000000..948a3070 Binary files /dev/null and b/src/pandroid/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/src/pandroid/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/src/pandroid/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 00000000..1b9a6956 Binary files /dev/null and b/src/pandroid/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/src/pandroid/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/src/pandroid/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 00000000..28d4b77f Binary files /dev/null and b/src/pandroid/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/src/pandroid/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/src/pandroid/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 00000000..9287f508 Binary files /dev/null and b/src/pandroid/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/src/pandroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/src/pandroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 00000000..aa7d6427 Binary files /dev/null and b/src/pandroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/src/pandroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/src/pandroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 00000000..9126ae37 Binary files /dev/null and b/src/pandroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/src/pandroid/app/src/main/res/values-pt-rBR/strings.xml b/src/pandroid/app/src/main/res/values-pt-rBR/strings.xml new file mode 100644 index 00000000..1198d66b --- /dev/null +++ b/src/pandroid/app/src/main/res/values-pt-rBR/strings.xml @@ -0,0 +1,57 @@ + + + pandroid + Carregar ROM + Jogos + Configurações + Pesquisar + Desconhecido + Esquerda + Direita + Cima + Baixo + Outros + Pressione qualquer tecla + Eixos + Zona Morta + Opções + Mapeie um controle ou teclado + Mapeamento de controle + Tema + Defina a aparência do aplicativo + Aparência + Mesmo do dispositivo + Claro + Escuro + Preto + Ações + Sair + Continuar + Salvo + Criar perfil + Entrada + Altere o mapeamento de controles, disposição de controle na tela etc. + Nome + Disposições de controle + Disposição de controle padrão + Nome Invalido + Trapaças + Script Lua + Scripts + Esse arquivo não é suportado + Salvar e sair + Sair sem salvar + Salvar \"%s\" antes de sair? + Abrir arquivo + Criar novo + Executando \"%s\" ... + Opções de desenvolvedor + Depuração, mostrar fps, etc. + Monitor de desempenho + Mostrar um overlay com fps, memoria, etc. + Depuração + Grave os registros para um arquivo. + Shader Jit + Usar recompilador de shaders. + Gráficos + diff --git a/src/pandroid/app/src/main/res/values/colors.xml b/src/pandroid/app/src/main/res/values/colors.xml new file mode 100644 index 00000000..c8524cd9 --- /dev/null +++ b/src/pandroid/app/src/main/res/values/colors.xml @@ -0,0 +1,5 @@ + + + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/values/dimens.xml b/src/pandroid/app/src/main/res/values/dimens.xml new file mode 100644 index 00000000..5119ce72 --- /dev/null +++ b/src/pandroid/app/src/main/res/values/dimens.xml @@ -0,0 +1,4 @@ + + + 1pt + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/values/strings.xml b/src/pandroid/app/src/main/res/values/strings.xml new file mode 100644 index 00000000..4c64439c --- /dev/null +++ b/src/pandroid/app/src/main/res/values/strings.xml @@ -0,0 +1,58 @@ + + pandroid + Load ROM + Games + Settings + Search + Unknown + + Left + Right + Up + Down + + Others + Press any key + Axis + Dead zone + Options + Map physical controller or keyboard + Controller mapping + Theme + Set application theme + Appearance + Device + Light + Dark + Black + Actions + Exit + Resume + Saved + Create profile + Input + Change input map, screen gamepad, etc. + Name + Screen gamepad layouts + Default screen gamepad layout + Invalid name + Hacks + Lua script + Scripts + File type isn\'t supported + Save and exit + Exit without saving + Exit without saving \"%s\"? + Open file + Create new + Running \"%s\" ... + Developer options + Logger, FPS Counter, etc. + Performance monitor + Show overlay with fps, memory, etc. + Logger + Store application logs to file. + Shader JIT + Use shader recompiler. + Graphics + diff --git a/src/pandroid/app/src/main/res/values/styleable.xml b/src/pandroid/app/src/main/res/values/styleable.xml new file mode 100644 index 00000000..cc26e7d9 --- /dev/null +++ b/src/pandroid/app/src/main/res/values/styleable.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/values/themes.xml b/src/pandroid/app/src/main/res/values/themes.xml new file mode 100644 index 00000000..343fab28 --- /dev/null +++ b/src/pandroid/app/src/main/res/values/themes.xml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pandroid/app/src/main/res/values/values.xml b/src/pandroid/app/src/main/res/values/values.xml new file mode 100644 index 00000000..9a4071c7 --- /dev/null +++ b/src/pandroid/app/src/main/res/values/values.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/xml/appearance_preference.xml b/src/pandroid/app/src/main/res/xml/appearance_preference.xml new file mode 100644 index 00000000..dd1ed483 --- /dev/null +++ b/src/pandroid/app/src/main/res/xml/appearance_preference.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/xml/backup_rules.xml b/src/pandroid/app/src/main/res/xml/backup_rules.xml new file mode 100644 index 00000000..fa0f996d --- /dev/null +++ b/src/pandroid/app/src/main/res/xml/backup_rules.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/xml/data_extraction_rules.xml b/src/pandroid/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 00000000..9ee9997b --- /dev/null +++ b/src/pandroid/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/xml/developer_preferences.xml b/src/pandroid/app/src/main/res/xml/developer_preferences.xml new file mode 100644 index 00000000..96ce8906 --- /dev/null +++ b/src/pandroid/app/src/main/res/xml/developer_preferences.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/xml/input_map_preferences.xml b/src/pandroid/app/src/main/res/xml/input_map_preferences.xml new file mode 100644 index 00000000..fbb6221f --- /dev/null +++ b/src/pandroid/app/src/main/res/xml/input_map_preferences.xml @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/xml/input_preference.xml b/src/pandroid/app/src/main/res/xml/input_preference.xml new file mode 100644 index 00000000..8f91ab40 --- /dev/null +++ b/src/pandroid/app/src/main/res/xml/input_preference.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/xml/start_preferences.xml b/src/pandroid/app/src/main/res/xml/start_preferences.xml new file mode 100644 index 00000000..5eeb1954 --- /dev/null +++ b/src/pandroid/app/src/main/res/xml/start_preferences.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/build.gradle.kts b/src/pandroid/build.gradle.kts new file mode 100644 index 00000000..c21801c7 --- /dev/null +++ b/src/pandroid/build.gradle.kts @@ -0,0 +1,4 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { + id("com.android.application") version "8.1.2" apply false +} \ No newline at end of file diff --git a/src/pandroid/gradle.properties b/src/pandroid/gradle.properties new file mode 100644 index 00000000..3e927b11 --- /dev/null +++ b/src/pandroid/gradle.properties @@ -0,0 +1,21 @@ +# 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=-Xmx2048m -Dfile.encoding=UTF-8 +# 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 +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true \ No newline at end of file diff --git a/src/pandroid/gradle/wrapper/gradle-wrapper.jar b/src/pandroid/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..e708b1c0 Binary files /dev/null and b/src/pandroid/gradle/wrapper/gradle-wrapper.jar differ diff --git a/src/pandroid/gradle/wrapper/gradle-wrapper.properties b/src/pandroid/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..22c9e5ab --- /dev/null +++ b/src/pandroid/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue Nov 14 14:40:27 EET 2023 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/src/pandroid/gradlew b/src/pandroid/gradlew new file mode 100755 index 00000000..4f906e0c --- /dev/null +++ b/src/pandroid/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# 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 +# +# https://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. +# + +############################################################################## +## +## 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='"-Xmx64m" "-Xms64m"' + +# 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 or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; 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=`expr $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" + +exec "$JAVACMD" "$@" diff --git a/src/pandroid/gradlew.bat b/src/pandroid/gradlew.bat new file mode 100644 index 00000000..ac1b06f9 --- /dev/null +++ b/src/pandroid/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@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 Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@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="-Xmx64m" "-Xms64m" + +@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 execute + +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 execute + +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 + +: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 %* + +: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/src/pandroid/settings.gradle.kts b/src/pandroid/settings.gradle.kts new file mode 100644 index 00000000..84d1ce30 --- /dev/null +++ b/src/pandroid/settings.gradle.kts @@ -0,0 +1,17 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "pandroid" +include(":app") diff --git a/tests/AppCpuTimeLimit/Makefile b/tests/AppCpuTimeLimit/Makefile new file mode 100644 index 00000000..9fc3a849 --- /dev/null +++ b/tests/AppCpuTimeLimit/Makefile @@ -0,0 +1,258 @@ +#--------------------------------------------------------------------------------- +.SUFFIXES: +#--------------------------------------------------------------------------------- + +ifeq ($(strip $(DEVKITARM)),) +$(error "Please set DEVKITARM in your environment. export DEVKITARM=devkitARM") +endif + +TOPDIR ?= $(CURDIR) +include $(DEVKITARM)/3ds_rules + +#--------------------------------------------------------------------------------- +# TARGET is the name of the output +# BUILD is the directory where object files & intermediate files will be placed +# SOURCES is a list of directories containing source code +# DATA is a list of directories containing data files +# INCLUDES is a list of directories containing header files +# GRAPHICS is a list of directories containing graphics files +# GFXBUILD is the directory where converted graphics files will be placed +# If set to $(BUILD), it will statically link in the converted +# files as if they were data files. +# +# NO_SMDH: if set to anything, no SMDH file is generated. +# ROMFS is the directory which contains the RomFS, relative to the Makefile (Optional) +# APP_TITLE is the name of the app stored in the SMDH file (Optional) +# APP_DESCRIPTION is the description of the app stored in the SMDH file (Optional) +# APP_AUTHOR is the author of the app stored in the SMDH file (Optional) +# ICON is the filename of the icon (.png), relative to the project folder. +# If not set, it attempts to use one of the following (in this order): +# - .png +# - icon.png +# - /default_icon.png +#--------------------------------------------------------------------------------- +TARGET := AppCpuTimeLimit +BUILD := build +SOURCES := source +DATA := data +INCLUDES := include +GRAPHICS := gfx +GFXBUILD := $(BUILD) +#ROMFS := romfs +#GFXBUILD := $(ROMFS)/gfx +APP_TITLE := AppCpuTimeLimit +APP_DESCRIPTION := Tests Set/GetAppCpuTimeLimit +APP_AUTHOR := noumidev + +#--------------------------------------------------------------------------------- +# options for code generation +#--------------------------------------------------------------------------------- +ARCH := -march=armv6k -mtune=mpcore -mfloat-abi=hard -mtp=soft + +CFLAGS := -g -Wall -O2 -mword-relocations \ + -ffunction-sections \ + $(ARCH) + +CFLAGS += $(INCLUDE) -D__3DS__ + +CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions -std=gnu++11 + +ASFLAGS := -g $(ARCH) +LDFLAGS = -specs=3dsx.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map) + +LIBS := -lctru -lm + +#--------------------------------------------------------------------------------- +# list of directories containing libraries, this must be the top level containing +# include and lib +#--------------------------------------------------------------------------------- +LIBDIRS := $(CTRULIB) + + +#--------------------------------------------------------------------------------- +# no real need to edit anything past this point unless you need to add additional +# rules for different file extensions +#--------------------------------------------------------------------------------- +ifneq ($(BUILD),$(notdir $(CURDIR))) +#--------------------------------------------------------------------------------- + +export OUTPUT := $(CURDIR)/$(TARGET) +export TOPDIR := $(CURDIR) + +export VPATH := $(foreach dir,$(SOURCES),$(CURDIR)/$(dir)) \ + $(foreach dir,$(GRAPHICS),$(CURDIR)/$(dir)) \ + $(foreach dir,$(DATA),$(CURDIR)/$(dir)) + +export DEPSDIR := $(CURDIR)/$(BUILD) + +CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c))) +CPPFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.cpp))) +SFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.s))) +PICAFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.v.pica))) +SHLISTFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.shlist))) +GFXFILES := $(foreach dir,$(GRAPHICS),$(notdir $(wildcard $(dir)/*.t3s))) +BINFILES := $(foreach dir,$(DATA),$(notdir $(wildcard $(dir)/*.*))) + +#--------------------------------------------------------------------------------- +# use CXX for linking C++ projects, CC for standard C +#--------------------------------------------------------------------------------- +ifeq ($(strip $(CPPFILES)),) +#--------------------------------------------------------------------------------- + export LD := $(CC) +#--------------------------------------------------------------------------------- +else +#--------------------------------------------------------------------------------- + export LD := $(CXX) +#--------------------------------------------------------------------------------- +endif +#--------------------------------------------------------------------------------- + +#--------------------------------------------------------------------------------- +ifeq ($(GFXBUILD),$(BUILD)) +#--------------------------------------------------------------------------------- +export T3XFILES := $(GFXFILES:.t3s=.t3x) +#--------------------------------------------------------------------------------- +else +#--------------------------------------------------------------------------------- +export ROMFS_T3XFILES := $(patsubst %.t3s, $(GFXBUILD)/%.t3x, $(GFXFILES)) +export T3XHFILES := $(patsubst %.t3s, $(BUILD)/%.h, $(GFXFILES)) +#--------------------------------------------------------------------------------- +endif +#--------------------------------------------------------------------------------- + +export OFILES_SOURCES := $(CPPFILES:.cpp=.o) $(CFILES:.c=.o) $(SFILES:.s=.o) + +export OFILES_BIN := $(addsuffix .o,$(BINFILES)) \ + $(PICAFILES:.v.pica=.shbin.o) $(SHLISTFILES:.shlist=.shbin.o) \ + $(addsuffix .o,$(T3XFILES)) + +export OFILES := $(OFILES_BIN) $(OFILES_SOURCES) + +export HFILES := $(PICAFILES:.v.pica=_shbin.h) $(SHLISTFILES:.shlist=_shbin.h) \ + $(addsuffix .h,$(subst .,_,$(BINFILES))) \ + $(GFXFILES:.t3s=.h) + +export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \ + $(foreach dir,$(LIBDIRS),-I$(dir)/include) \ + -I$(CURDIR)/$(BUILD) + +export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib) + +export _3DSXDEPS := $(if $(NO_SMDH),,$(OUTPUT).smdh) + +ifeq ($(strip $(ICON)),) + icons := $(wildcard *.png) + ifneq (,$(findstring $(TARGET).png,$(icons))) + export APP_ICON := $(TOPDIR)/$(TARGET).png + else + ifneq (,$(findstring icon.png,$(icons))) + export APP_ICON := $(TOPDIR)/icon.png + endif + endif +else + export APP_ICON := $(TOPDIR)/$(ICON) +endif + +ifeq ($(strip $(NO_SMDH)),) + export _3DSXFLAGS += --smdh=$(CURDIR)/$(TARGET).smdh +endif + +ifneq ($(ROMFS),) + export _3DSXFLAGS += --romfs=$(CURDIR)/$(ROMFS) +endif + +.PHONY: all clean + +#--------------------------------------------------------------------------------- +all: $(BUILD) $(GFXBUILD) $(DEPSDIR) $(ROMFS_T3XFILES) $(T3XHFILES) + @$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile + +$(BUILD): + @mkdir -p $@ + +ifneq ($(GFXBUILD),$(BUILD)) +$(GFXBUILD): + @mkdir -p $@ +endif + +ifneq ($(DEPSDIR),$(BUILD)) +$(DEPSDIR): + @mkdir -p $@ +endif + +#--------------------------------------------------------------------------------- +clean: + @echo clean ... + @rm -fr $(BUILD) $(TARGET).3dsx $(OUTPUT).smdh $(TARGET).elf $(GFXBUILD) + +#--------------------------------------------------------------------------------- +$(GFXBUILD)/%.t3x $(BUILD)/%.h : %.t3s +#--------------------------------------------------------------------------------- + @echo $(notdir $<) + @tex3ds -i $< -H $(BUILD)/$*.h -d $(DEPSDIR)/$*.d -o $(GFXBUILD)/$*.t3x + +#--------------------------------------------------------------------------------- +else + +#--------------------------------------------------------------------------------- +# main targets +#--------------------------------------------------------------------------------- +$(OUTPUT).3dsx : $(OUTPUT).elf $(_3DSXDEPS) + +$(OFILES_SOURCES) : $(HFILES) + +$(OUTPUT).elf : $(OFILES) + +#--------------------------------------------------------------------------------- +# you need a rule like this for each extension you use as binary data +#--------------------------------------------------------------------------------- +%.bin.o %_bin.h : %.bin +#--------------------------------------------------------------------------------- + @echo $(notdir $<) + @$(bin2o) + +#--------------------------------------------------------------------------------- +.PRECIOUS : %.t3x +#--------------------------------------------------------------------------------- +%.t3x.o %_t3x.h : %.t3x +#--------------------------------------------------------------------------------- + @echo $(notdir $<) + @$(bin2o) + +#--------------------------------------------------------------------------------- +# rules for assembling GPU shaders +#--------------------------------------------------------------------------------- +define shader-as + $(eval CURBIN := $*.shbin) + $(eval DEPSFILE := $(DEPSDIR)/$*.shbin.d) + echo "$(CURBIN).o: $< $1" > $(DEPSFILE) + echo "extern const u8" `(echo $(CURBIN) | sed -e 's/^\([0-9]\)/_\1/' | tr . _)`"_end[];" > `(echo $(CURBIN) | tr . _)`.h + echo "extern const u8" `(echo $(CURBIN) | sed -e 's/^\([0-9]\)/_\1/' | tr . _)`"[];" >> `(echo $(CURBIN) | tr . _)`.h + echo "extern const u32" `(echo $(CURBIN) | sed -e 's/^\([0-9]\)/_\1/' | tr . _)`_size";" >> `(echo $(CURBIN) | tr . _)`.h + picasso -o $(CURBIN) $1 + bin2s $(CURBIN) | $(AS) -o $*.shbin.o +endef + +%.shbin.o %_shbin.h : %.v.pica %.g.pica + @echo $(notdir $^) + @$(call shader-as,$^) + +%.shbin.o %_shbin.h : %.v.pica + @echo $(notdir $<) + @$(call shader-as,$<) + +%.shbin.o %_shbin.h : %.shlist + @echo $(notdir $<) + @$(call shader-as,$(foreach file,$(shell cat $<),$(dir $<)$(file))) + +#--------------------------------------------------------------------------------- +%.t3x %.h : %.t3s +#--------------------------------------------------------------------------------- + @echo $(notdir $<) + @tex3ds -i $< -H $*.h -d $*.d -o $*.t3x + +-include $(DEPSDIR)/*.d + +#--------------------------------------------------------------------------------------- +endif +#--------------------------------------------------------------------------------------- diff --git a/tests/AppCpuTimeLimit/o3ds.png b/tests/AppCpuTimeLimit/o3ds.png new file mode 100644 index 00000000..b3da7ce7 Binary files /dev/null and b/tests/AppCpuTimeLimit/o3ds.png differ diff --git a/tests/AppCpuTimeLimit/source/main.c b/tests/AppCpuTimeLimit/source/main.c new file mode 100644 index 00000000..e61dc498 --- /dev/null +++ b/tests/AppCpuTimeLimit/source/main.c @@ -0,0 +1,55 @@ +#include <3ds.h> + +#include + +int main(int argc, char **argv) { + gfxInitDefault(); + + consoleInit(GFX_TOP, NULL); + + printf("--- APT::SetAppCpuTimeLimit ---\n\n"); + + // Get initial percentage + u32 percentage; + APT_GetAppCpuTimeLimit(&percentage); + + printf("Initial percentage: %lu\n\n", percentage); + + // Try all percentages from 0-100%, print failed calls + for (int i = 0; i <= 100; i++) { + const Result res = APT_SetAppCpuTimeLimit(i); + + if (R_FAILED(res)) { + APT_GetAppCpuTimeLimit(&percentage); + + printf("[%d:%lu:%lX]\n", i, percentage, res); + } + } + + // Send command with invalid fixed value + u32 aptcmdbuf[16]; + aptcmdbuf[0] = 0x004F0080; + aptcmdbuf[1] = 0; + aptcmdbuf[2] = 20; + + aptSendCommand(aptcmdbuf); + + printf("\nWith fixed = 0: [%08lX:%08lX]\n", aptcmdbuf[0], aptcmdbuf[1]); + + while (aptMainLoop()) { + hidScanInput(); + + if ((hidKeysDown() & KEY_START) != 0) { + break; + } + + gfxFlushBuffers(); + gfxSwapBuffers(); + + gspWaitForVBlank(); + } + + gfxExit(); + + return 0; +} diff --git a/third_party/hydra_core b/third_party/hydra_core index e4cc6b0f..1cdb1eda 160000 --- a/third_party/hydra_core +++ b/third_party/hydra_core @@ -1 +1 @@ -Subproject commit e4cc6b0fc224583e509bc3472a4c11eafb69c041 +Subproject commit 1cdb1eda5f368481e216416a119c85664e8c72ab diff --git a/third_party/oaknut b/third_party/oaknut new file mode 160000 index 00000000..1d51f551 --- /dev/null +++ b/third_party/oaknut @@ -0,0 +1 @@ +Subproject commit 1d51f551294897ab4c8001c5259c8c5dee7e2a85 diff --git a/third_party/zep b/third_party/zep new file mode 160000 index 00000000..75406e1c --- /dev/null +++ b/third_party/zep @@ -0,0 +1 @@ +Subproject commit 75406e1c854b9fa6ede697d6165664e0e11b09ff